test_reserve_history.c (19090B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2026 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 13 You should have received a copy of the GNU General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @file exchangedb/test_reserve_history.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `reserve_history` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_get_reserve_history(). 23 * 24 * `reserve_history` is an index: AFTER INSERT triggers on the tables that 25 * change a reserve add one row each, naming the table and the row. The 26 * function walks that index newest-first and looks each entry up in the 27 * table it names. So the checks build a reserve with several kinds of 28 * operation on it and read the history back, and they check the ETag 29 * short-circuit -- which must not leave a transaction open. 30 */ 31 #include "test_common.h" 32 #include "exchange-database/do_recoup.h" 33 #include "exchange-database/get_reserve_history.h" 34 #include "exchange-database/insert_close_request.h" 35 #include "exchange-database/start.h" 36 #include "exchange-database/rollback.h" 37 38 39 /** 40 * Account the checks use. 41 */ 42 static struct TDB_Account account; 43 44 45 /** 46 * Count the entries of @a rh, and how many of them have type @a type. 47 * 48 * @param rh history to walk 49 * @param type operation to count 50 * @param[out] matched set to the number of entries of that type 51 * @return total number of entries 52 */ 53 static unsigned int 54 count_history (const struct TALER_EXCHANGEDB_ReserveHistory *rh, 55 enum TALER_EXCHANGEDB_ReserveOperation type, 56 unsigned int *matched) 57 { 58 unsigned int total = 0; 59 60 *matched = 0; 61 for (const struct TALER_EXCHANGEDB_ReserveHistory *p = rh; 62 NULL != p; 63 p = p->next) 64 { 65 total++; 66 if (type == p->type) 67 (*matched)++; 68 } 69 return total; 70 } 71 72 73 /** 74 * An unknown reserve has no history at all. 75 * 76 * @param pg the database context 77 * @return 0 on success 78 */ 79 static int 80 check_unknown_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg) 81 { 82 struct TALER_ReservePublicKeyP reserve_pub; 83 struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; 84 struct TALER_Amount balance; 85 uint64_t etag_out = 42; 86 87 TDB_FILL (reserve_pub, 88 1); 89 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 90 TALER_EXCHANGEDB_get_reserve_history (pg, 91 &reserve_pub, 92 0, 93 0, 94 &etag_out, 95 &balance, 96 &rh)); 97 FAILIF (NULL != rh); 98 return 0; 99 } 100 101 102 /** 103 * A reserve that exists but was never touched has no history either: the 104 * `reserves` row alone does not create an index entry. 105 * 106 * @param pg the database context 107 * @return 0 on success 108 */ 109 static int 110 check_untouched_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg) 111 { 112 struct TALER_ReservePublicKeyP reserve_pub; 113 struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; 114 struct TALER_Amount balance; 115 uint64_t etag_out = 42; 116 117 TDB_reserve (pg, 118 2, 119 "5", 120 &reserve_pub); 121 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 122 TALER_EXCHANGEDB_get_reserve_history (pg, 123 &reserve_pub, 124 0, 125 0, 126 &etag_out, 127 &balance, 128 &rh)); 129 FAILIF (NULL != rh); 130 return 0; 131 } 132 133 134 /** 135 * A bank transfer into the reserve shows up as one history entry. 136 * 137 * @param pg the database context 138 * @return 0 on success 139 */ 140 static int 141 check_bank_transfer (struct TALER_EXCHANGEDB_PostgresContext *pg) 142 { 143 struct TALER_ReservePublicKeyP reserve_pub; 144 struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; 145 struct TALER_Amount balance; 146 struct TALER_Amount expect = TDB_amount ("10"); 147 unsigned int matched; 148 uint64_t etag_out = 0; 149 150 TDB_account (pg, 151 10, 152 &account); 153 TDB_reserve_in (pg, 154 &account, 155 10, 156 "10", 157 &reserve_pub); 158 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 159 TALER_EXCHANGEDB_get_reserve_history (pg, 160 &reserve_pub, 161 0, 162 0, 163 &etag_out, 164 &balance, 165 &rh)); 166 FAILIF_C (NULL == rh, 167 TALER_EXCHANGEDB_free_reserve_history (rh)); 168 FAILIF_C (0 == etag_out, 169 TALER_EXCHANGEDB_free_reserve_history (rh)); 170 FAILIF_C (0 != TALER_amount_cmp (&balance, 171 &expect), 172 TALER_EXCHANGEDB_free_reserve_history (rh)); 173 FAILIF_C (1 != count_history (rh, 174 TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE, 175 &matched), 176 TALER_EXCHANGEDB_free_reserve_history (rh)); 177 FAILIF_C (1 != matched, 178 TALER_EXCHANGEDB_free_reserve_history (rh)); 179 FAILIF_C (0 != TALER_amount_cmp (&rh->details.bank->amount, 180 &expect), 181 TALER_EXCHANGEDB_free_reserve_history (rh)); 182 FAILIF_C (0 != strcmp (rh->details.bank->sender_account_details.full_payto, 183 account.payto.full_payto), 184 TALER_EXCHANGEDB_free_reserve_history (rh)); 185 TALER_EXCHANGEDB_free_reserve_history (rh); 186 return 0; 187 } 188 189 190 /** 191 * A close request adds a second entry, and the offset filter hides the 192 * older one. 193 * 194 * @param pg the database context 195 * @return 0 on success 196 */ 197 static int 198 check_two_entries (struct TALER_EXCHANGEDB_PostgresContext *pg) 199 { 200 struct TALER_ReservePublicKeyP reserve_pub; 201 struct TALER_ReserveSignatureP reserve_sig; 202 struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; 203 struct TALER_Amount balance; 204 struct TALER_Amount close_balance = TDB_amount ("10"); 205 struct TALER_Amount close_fee = TDB_amount ("0.5"); 206 unsigned int matched; 207 uint64_t etag_first; 208 uint64_t etag_out = 0; 209 uint64_t first_off; 210 211 TDB_fill (&reserve_pub, 212 sizeof (reserve_pub), 213 10); 214 TDB_FILL (reserve_sig, 215 10); 216 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 217 TALER_EXCHANGEDB_get_reserve_history (pg, 218 &reserve_pub, 219 0, 220 0, 221 &etag_first, 222 &balance, 223 &rh)); 224 first_off = rh->history_offset; 225 TALER_EXCHANGEDB_free_reserve_history (rh); 226 rh = NULL; 227 228 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 229 TALER_EXCHANGEDB_insert_close_request (pg, 230 &reserve_pub, 231 account.payto, 232 &reserve_sig, 233 GNUNET_TIME_timestamp_get (), 234 &close_balance, 235 &close_fee)); 236 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 237 TALER_EXCHANGEDB_get_reserve_history (pg, 238 &reserve_pub, 239 0, 240 0, 241 &etag_out, 242 &balance, 243 &rh)); 244 FAILIF_C (2 != count_history (rh, 245 TALER_EXCHANGEDB_RO_CLOSE_REQUEST, 246 &matched), 247 TALER_EXCHANGEDB_free_reserve_history (rh)); 248 FAILIF_C (1 != matched, 249 TALER_EXCHANGEDB_free_reserve_history (rh)); 250 /* The query runs newest-first but the rows are walked back to front, so 251 the list that comes out is oldest-first. */ 252 FAILIF_C (TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE != rh->type, 253 TALER_EXCHANGEDB_free_reserve_history (rh)); 254 FAILIF_C (TALER_EXCHANGEDB_RO_CLOSE_REQUEST != rh->next->type, 255 TALER_EXCHANGEDB_free_reserve_history (rh)); 256 FAILIF_C (rh->history_offset >= rh->next->history_offset, 257 TALER_EXCHANGEDB_free_reserve_history (rh)); 258 FAILIF_C (etag_out <= etag_first, 259 TALER_EXCHANGEDB_free_reserve_history (rh)); 260 TALER_EXCHANGEDB_free_reserve_history (rh); 261 rh = NULL; 262 263 /* starting past the bank transfer leaves only the close request */ 264 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 265 TALER_EXCHANGEDB_get_reserve_history (pg, 266 &reserve_pub, 267 first_off, 268 0, 269 &etag_out, 270 &balance, 271 &rh)); 272 FAILIF_C (1 != count_history (rh, 273 TALER_EXCHANGEDB_RO_CLOSE_REQUEST, 274 &matched), 275 TALER_EXCHANGEDB_free_reserve_history (rh)); 276 FAILIF_C (1 != matched, 277 TALER_EXCHANGEDB_free_reserve_history (rh)); 278 TALER_EXCHANGEDB_free_reserve_history (rh); 279 rh = NULL; 280 281 /* starting past everything leaves an empty history, but the reserve is 282 still known and the ETag is still reported */ 283 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 284 TALER_EXCHANGEDB_get_reserve_history (pg, 285 &reserve_pub, 286 etag_out, 287 0, 288 &etag_out, 289 &balance, 290 &rh)); 291 FAILIF_C (NULL != rh, 292 TALER_EXCHANGEDB_free_reserve_history (rh)); 293 return 0; 294 } 295 296 297 /** 298 * A recoup shows up as a RECOUP entry that names the coin, the amount 299 * and the withdraw operation the coin came from. 300 * 301 * @param pg the database context 302 * @return 0 on success 303 */ 304 static int 305 check_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg) 306 { 307 struct TDB_Denom denom; 308 struct TALER_CoinPublicInfo coin; 309 struct TALER_ReservePublicKeyP reserve_pub; 310 struct TALER_HashBlindedPlanchetsP planchets_h; 311 struct TALER_CoinSpendSignatureP coin_sig; 312 union GNUNET_CRYPTO_BlindingSecretP coin_bks; 313 struct GNUNET_TIME_Timestamp recoup_timestamp; 314 struct TALER_Amount recoup_amount; 315 struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; 316 struct TALER_Amount balance; 317 struct TALER_Amount expect_value = TDB_amount ("5"); 318 struct TALER_Amount expect_balance = TDB_amount ("10"); 319 const struct TALER_EXCHANGEDB_Recoup *recoup = NULL; 320 unsigned int matched = 0; 321 uint64_t withdraw_id; 322 uint64_t known_coin_id; 323 uint64_t etag = 0; 324 bool recoup_ok; 325 bool internal_failure; 326 327 TDB_denom (pg, 328 30, 329 "5", 330 "0.1", 331 &denom); 332 TDB_reserve_in (pg, 333 &account, 334 30, 335 "10", 336 &reserve_pub); 337 withdraw_id = TDB_withdraw (pg, 338 &denom, 339 &reserve_pub, 340 30, 341 "5"); 342 /* TDB_withdraw() seeds the commitment with its seed */ 343 TDB_FILL (planchets_h, 344 30); 345 TDB_coin (pg, 346 &denom, 347 31, 348 &coin, 349 &known_coin_id); 350 TDB_FILL (coin_sig, 351 31); 352 TDB_FILL (coin_bks, 353 31); 354 recoup_timestamp = GNUNET_TIME_timestamp_get (); 355 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 356 TALER_EXCHANGEDB_do_recoup (pg, 357 &reserve_pub, 358 withdraw_id, 359 2, 360 &coin_bks, 361 &coin.coin_pub, 362 known_coin_id, 363 &coin_sig, 364 &recoup_timestamp, 365 &recoup_amount, 366 &recoup_ok, 367 &internal_failure), 368 TDB_coin_free (&coin); TDB_denom_free (&denom)); 369 FAILIF_C (internal_failure || ! recoup_ok, 370 TDB_coin_free (&coin); TDB_denom_free (&denom)); 371 FAILIF_C (0 != TALER_amount_cmp (&recoup_amount, 372 &expect_value), 373 TDB_coin_free (&coin); TDB_denom_free (&denom)); 374 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 375 TALER_EXCHANGEDB_get_reserve_history (pg, 376 &reserve_pub, 377 0, 378 0, 379 &etag, 380 &balance, 381 &rh), 382 TDB_coin_free (&coin); TDB_denom_free (&denom)); 383 /* bank transfer in, withdraw, recoup */ 384 FAILIF_C (3 != count_history (rh, 385 TALER_EXCHANGEDB_RO_RECOUP_COIN, 386 &matched), 387 TALER_EXCHANGEDB_free_reserve_history (rh); 388 TDB_coin_free (&coin); TDB_denom_free (&denom)); 389 FAILIF_C (1 != matched, 390 TALER_EXCHANGEDB_free_reserve_history (rh); 391 TDB_coin_free (&coin); TDB_denom_free (&denom)); 392 for (const struct TALER_EXCHANGEDB_ReserveHistory *p = rh; 393 NULL != p; 394 p = p->next) 395 if (TALER_EXCHANGEDB_RO_RECOUP_COIN == p->type) 396 recoup = p->details.recoup; 397 FAILIF_C (0 != GNUNET_memcmp (&recoup->coin.coin_pub, 398 &coin.coin_pub), 399 TALER_EXCHANGEDB_free_reserve_history (rh); 400 TDB_coin_free (&coin); TDB_denom_free (&denom)); 401 FAILIF_C (0 != GNUNET_memcmp (&recoup->planchets_h, 402 &planchets_h), 403 TALER_EXCHANGEDB_free_reserve_history (rh); 404 TDB_coin_free (&coin); TDB_denom_free (&denom)); 405 FAILIF_C (0 != GNUNET_memcmp (&recoup->reserve_pub, 406 &reserve_pub), 407 TALER_EXCHANGEDB_free_reserve_history (rh); 408 TDB_coin_free (&coin); TDB_denom_free (&denom)); 409 FAILIF_C (0 != TALER_amount_cmp (&recoup->value, 410 &expect_value), 411 TALER_EXCHANGEDB_free_reserve_history (rh); 412 TDB_coin_free (&coin); TDB_denom_free (&denom)); 413 /* 10 in, 5 withdrawn, 5 recouped */ 414 FAILIF_C (0 != TALER_amount_cmp (&balance, 415 &expect_balance), 416 TALER_EXCHANGEDB_free_reserve_history (rh); 417 TDB_coin_free (&coin); TDB_denom_free (&denom)); 418 TALER_EXCHANGEDB_free_reserve_history (rh); 419 TDB_coin_free (&coin); 420 TDB_denom_free (&denom); 421 return 0; 422 } 423 424 425 /** 426 * Passing the current ETag short-circuits the lookup -- and must not leave 427 * the transaction it opened behind. 428 * 429 * @param pg the database context 430 * @return 0 on success 431 */ 432 static int 433 check_etag (struct TALER_EXCHANGEDB_PostgresContext *pg) 434 { 435 struct TALER_ReservePublicKeyP reserve_pub; 436 struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; 437 struct TALER_Amount balance; 438 uint64_t etag_out = 0; 439 uint64_t etag_again = 0; 440 441 TDB_fill (&reserve_pub, 442 sizeof (reserve_pub), 443 10); 444 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 445 TALER_EXCHANGEDB_get_reserve_history (pg, 446 &reserve_pub, 447 0, 448 0, 449 &etag_out, 450 &balance, 451 &rh)); 452 TALER_EXCHANGEDB_free_reserve_history (rh); 453 rh = NULL; 454 455 /* the client already has this version */ 456 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 457 TALER_EXCHANGEDB_get_reserve_history (pg, 458 &reserve_pub, 459 0, 460 etag_out, 461 &etag_again, 462 &balance, 463 &rh)); 464 FAILIF (etag_again != etag_out); 465 FAILIF (NULL != rh); 466 467 /* The short-circuit above returns early. If it forgets to close the 468 read-committed transaction it opened, the connection is parked in 469 "idle in transaction" -- which the next TALER_EXCHANGEDB_start() would 470 paper over, since its preflight rolls the stale transaction back and 471 only logs it. So check the bookkeeping preflight itself looks at. */ 472 FAILIF (NULL != pg->transaction_name); 473 FAILIF (GNUNET_OK != 474 TALER_EXCHANGEDB_start (pg, 475 "test-reserve-history-etag")); 476 FAILIF (NULL == pg->transaction_name); 477 TALER_EXCHANGEDB_rollback (pg); 478 FAILIF (NULL != pg->transaction_name); 479 return 0; 480 } 481 482 483 /** 484 * The checks to run, in order. 485 */ 486 static const struct TDB_Test tests[] = { 487 { "reserve-history-unknown-reserve", 488 &check_unknown_reserve }, 489 { "reserve-history-untouched-reserve", 490 &check_untouched_reserve }, 491 { "reserve-history-bank-transfer", 492 &check_bank_transfer }, 493 { "reserve-history-two-entries", 494 &check_two_entries }, 495 { "reserve-history-recoup", 496 &check_recoup }, 497 { "reserve-history-etag", 498 &check_etag }, 499 { NULL, NULL } 500 }; 501 502 503 int 504 main (int argc, 505 char *const *argv) 506 { 507 int ret; 508 509 ret = TDB_main (argc, 510 argv, 511 "test-reserve-history", 512 "Tests for the exchangedb `reserve_history' table", 513 tests); 514 TDB_account_free (&account); 515 return ret; 516 } 517 518 519 /* end of test_reserve_history.c */