test_coin_history.c (22868B)
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_coin_history.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `coin_history` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_get_coin_transactions(). 23 * 24 * Nothing writes to `coin_history` directly: every table that can spend or 25 * credit a coin has an INSERT trigger that appends a row naming itself and 26 * its own serial. get_coin_transactions() walks those rows and looks each 27 * one up in the table it names. So the interesting part of this test is 28 * spending one coin in as many different ways as possible and then checking 29 * that every one of them comes back with the right type and amount. 30 */ 31 #include "test_common.h" 32 #include "exchange-database/do_purse_deposit.h" 33 #include "exchange-database/rollback.h" 34 #include "exchange-database/start.h" 35 #include "exchange-database/do_recoup.h" 36 #include "exchange-database/do_refresh.h" 37 #include "exchange-database/do_refund.h" 38 #include "exchange-database/get_coin_transactions.h" 39 #include "exchange-database/insert_reserve_open_deposit.h" 40 41 42 /** 43 * Account the checks fund their reserves from. 44 */ 45 static struct TDB_Account account; 46 47 48 /** 49 * Denomination the checks use. 50 */ 51 static struct TDB_Denom denom; 52 53 54 /** 55 * The coin whose history we build up. 56 */ 57 static struct TALER_CoinPublicInfo coin; 58 59 60 /** 61 * Row of @e coin in `known_coins`. 62 */ 63 static uint64_t known_coin_id; 64 65 66 /** 67 * Reserve @e coin was withdrawn from. 68 */ 69 static struct TALER_ReservePublicKeyP reserve_pub; 70 71 72 /** 73 * Row of the withdraw that created @e coin. 74 */ 75 static uint64_t withdraw_id; 76 77 78 /** 79 * The deposit we later refund. 80 */ 81 static struct TDB_Deposit deposit; 82 83 84 /** 85 * ETag of the history right after the deposit was refunded. 86 */ 87 static uint64_t etag_after_refund; 88 89 90 /** 91 * Count the entries of @a tl and remember which types occurred. 92 * 93 * @param tl transaction list to walk 94 * @param[out] types set to the bitmask of the types seen 95 * @return number of entries in @a tl 96 */ 97 static unsigned int 98 summarize (const struct TALER_EXCHANGEDB_TransactionList *tl, 99 unsigned int *types) 100 { 101 unsigned int cnt = 0; 102 103 *types = 0; 104 for (const struct TALER_EXCHANGEDB_TransactionList *pos = tl; 105 NULL != pos; 106 pos = pos->next) 107 { 108 cnt++; 109 *types |= 1U << pos->type; 110 } 111 return cnt; 112 } 113 114 115 /** 116 * Ask for the full history of @e coin. 117 * 118 * @param pg the database context 119 * @param start_off offset to start from 120 * @param etag_in ETag the caller already has 121 * @param[out] etag_out set to the current ETag 122 * @param[out] tlp set to the history 123 * @return transaction status 124 */ 125 static enum GNUNET_DB_QueryStatus 126 history (struct TALER_EXCHANGEDB_PostgresContext *pg, 127 uint64_t start_off, 128 uint64_t etag_in, 129 uint64_t *etag_out, 130 struct TALER_EXCHANGEDB_TransactionList **tlp) 131 { 132 struct TALER_Amount balance; 133 struct TALER_DenominationHashP h_denom_pub; 134 135 return TALER_EXCHANGEDB_get_coin_transactions (pg, 136 true, 137 &coin.coin_pub, 138 start_off, 139 etag_in, 140 etag_out, 141 &balance, 142 &h_denom_pub, 143 tlp); 144 } 145 146 147 /** 148 * A coin the exchange never saw has no history. 149 * 150 * @param pg the database context 151 * @return 0 on success 152 */ 153 static int 154 check_unknown_coin (struct TALER_EXCHANGEDB_PostgresContext *pg) 155 { 156 struct TALER_EXCHANGEDB_TransactionList *tl = (void *) 1; 157 struct TALER_CoinSpendPublicKeyP unknown; 158 struct TALER_Amount balance; 159 struct TALER_DenominationHashP h_denom_pub; 160 uint64_t etag = 42; 161 162 TDB_account (pg, 163 10, 164 &account); 165 TDB_denom (pg, 166 10, 167 "5", 168 "0.1", 169 &denom); 170 TDB_FILL (unknown, 171 99); 172 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 173 TALER_EXCHANGEDB_get_coin_transactions (pg, 174 true, 175 &unknown, 176 0, 177 0, 178 &etag, 179 &balance, 180 &h_denom_pub, 181 &tl)); 182 FAILIF (NULL != tl); 183 return 0; 184 } 185 186 187 /** 188 * A coin that was withdrawn but never spent has no history either: the 189 * `withdraw` table has no coin history trigger. 190 * 191 * @param pg the database context 192 * @return 0 on success 193 */ 194 static int 195 check_fresh_coin (struct TALER_EXCHANGEDB_PostgresContext *pg) 196 { 197 struct TALER_EXCHANGEDB_TransactionList *tl = (void *) 1; 198 uint64_t etag = 42; 199 200 TDB_reserve_in (pg, 201 &account, 202 11, 203 "10", 204 &reserve_pub); 205 withdraw_id = TDB_withdraw (pg, 206 &denom, 207 &reserve_pub, 208 11, 209 "5"); 210 TDB_coin (pg, 211 &denom, 212 20, 213 &coin, 214 &known_coin_id); 215 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 216 history (pg, 217 0, 218 0, 219 &etag, 220 &tl)); 221 FAILIF (NULL != tl); 222 FAILIF (0 != TDB_count (pg, 223 "FROM coin_history")); 224 return 0; 225 } 226 227 228 /** 229 * A deposit and its refund show up as two entries. 230 * 231 * @param pg the database context 232 * @return 0 on success 233 */ 234 static int 235 check_deposit_and_refund (struct TALER_EXCHANGEDB_PostgresContext *pg) 236 { 237 struct TALER_EXCHANGEDB_TransactionList *tl = NULL; 238 struct TALER_EXCHANGEDB_Refund refund; 239 struct TALER_Amount deposit_fee = TDB_amount ("0.1"); 240 struct TALER_Amount balance; 241 struct TALER_Amount expect = TDB_amount ("4.5"); 242 struct TALER_DenominationHashP h_denom_pub; 243 unsigned int types; 244 uint64_t etag = 0; 245 bool not_found; 246 bool refund_ok; 247 bool gone; 248 bool conflict; 249 250 TDB_deposit (pg, 251 &account, 252 &coin, 253 30, 254 "1", 255 "0.1", 256 GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS), 257 GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS), 258 &deposit); 259 FAILIF (1 != TDB_count (pg, 260 "FROM coin_history")); 261 262 memset (&refund, 263 0, 264 sizeof (refund)); 265 refund.coin = coin; 266 refund.details.merchant_pub = deposit.merchant_pub; 267 TDB_FILL (refund.details.merchant_sig, 268 31); 269 refund.details.h_contract_terms = deposit.h_contract_terms; 270 refund.details.rtransaction_id = 1; 271 refund.details.refund_amount = TDB_amount ("0.5"); 272 refund.details.refund_fee = TDB_amount ("0"); 273 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 274 TALER_EXCHANGEDB_do_refund (pg, 275 &refund, 276 &deposit_fee, 277 0, 278 ¬_found, 279 &refund_ok, 280 &gone, 281 &conflict)); 282 FAILIF (not_found); 283 FAILIF (! refund_ok); 284 FAILIF (2 != TDB_count (pg, 285 "FROM coin_history")); 286 287 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 288 TALER_EXCHANGEDB_get_coin_transactions (pg, 289 true, 290 &coin.coin_pub, 291 0, 292 0, 293 &etag, 294 &balance, 295 &h_denom_pub, 296 &tl)); 297 FAILIF_C (2 != summarize (tl, 298 &types), 299 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 300 FAILIF_C (0 == (types & (1U << TALER_EXCHANGEDB_TT_DEPOSIT)), 301 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 302 FAILIF_C (0 == (types & (1U << TALER_EXCHANGEDB_TT_REFUND)), 303 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 304 /* 5 - 1 + 0.5 */ 305 FAILIF_C (0 != TALER_amount_cmp (&balance, 306 &expect), 307 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 308 FAILIF_C (0 != GNUNET_memcmp (&h_denom_pub, 309 &denom.h_denom_pub), 310 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 311 TALER_EXCHANGEDB_free_coin_transaction_list (tl); 312 FAILIF (0 == etag); 313 etag_after_refund = etag; 314 return 0; 315 } 316 317 318 /** 319 * A caller that is already up to date gets no list back, and the 320 * transaction the lookup opened is not left dangling. 321 * 322 * @param pg the database context 323 * @return 0 on success 324 */ 325 static int 326 check_etag (struct TALER_EXCHANGEDB_PostgresContext *pg) 327 { 328 struct TALER_EXCHANGEDB_TransactionList *tl = (void *) 1; 329 uint64_t etag = 0; 330 331 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 332 history (pg, 333 0, 334 etag_after_refund, 335 &etag, 336 &tl)); 337 FAILIF (NULL != tl); 338 FAILIF (etag != etag_after_refund); 339 /* the lookup must not leave a transaction open (it starts one of its 340 own when begin_transaction is true) */ 341 FAILIF (NULL != pg->transaction_name); 342 343 /* an ETag that is not the current one still returns the history */ 344 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 345 history (pg, 346 0, 347 etag_after_refund - 1, 348 &etag, 349 &tl)); 350 FAILIF (NULL == tl); 351 TALER_EXCHANGEDB_free_coin_transaction_list (tl); 352 FAILIF (NULL != pg->transaction_name); 353 return 0; 354 } 355 356 357 /** 358 * The offset skips everything up to and including it. 359 * 360 * @param pg the database context 361 * @return 0 on success 362 */ 363 static int 364 check_offset (struct TALER_EXCHANGEDB_PostgresContext *pg) 365 { 366 struct TALER_EXCHANGEDB_TransactionList *tl = NULL; 367 unsigned int types; 368 uint64_t etag = 0; 369 370 /* everything after the deposit: only the refund is left */ 371 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 372 history (pg, 373 etag_after_refund - 1, 374 0, 375 &etag, 376 &tl)); 377 FAILIF_C (1 != summarize (tl, 378 &types), 379 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 380 FAILIF_C (0 == (types & (1U << TALER_EXCHANGEDB_TT_REFUND)), 381 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 382 TALER_EXCHANGEDB_free_coin_transaction_list (tl); 383 384 /* everything after the last entry: nothing, but the ETag is still 385 reported */ 386 tl = (void *) 1; 387 etag = 0; 388 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 389 history (pg, 390 etag_after_refund, 391 0, 392 &etag, 393 &tl)); 394 FAILIF (NULL != tl); 395 FAILIF (etag != etag_after_refund); 396 return 0; 397 } 398 399 400 /** 401 * Spending the coin into a purse, on a reserve and in a melt adds one 402 * entry each. 403 * 404 * @param pg the database context 405 * @return 0 on success 406 */ 407 static int 408 check_other_spends (struct TALER_EXCHANGEDB_PostgresContext *pg) 409 { 410 struct TALER_EXCHANGEDB_TransactionList *tl = NULL; 411 struct TDB_Purse purse; 412 struct TALER_CoinSpendSignatureP coin_sig; 413 struct TALER_ReserveSignatureP reserve_sig; 414 struct TALER_Amount one = TDB_amount ("1"); 415 struct TALER_Amount zero = TDB_amount ("0"); 416 unsigned int types; 417 uint64_t etag = 0; 418 bool balance_ok; 419 bool too_late; 420 bool conflict; 421 bool insufficient_funds; 422 423 /* into a purse */ 424 TDB_purse (pg, 425 40, 426 "1", 427 GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS), 428 &purse); 429 TDB_FILL (coin_sig, 430 41); 431 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 432 TALER_EXCHANGEDB_do_purse_deposit (pg, 433 &purse.purse_pub, 434 &coin.coin_pub, 435 &one, 436 &coin_sig, 437 &one, 438 &balance_ok, 439 &too_late, 440 &conflict)); 441 FAILIF (! balance_ok); 442 FAILIF (conflict); 443 444 /* to keep a reserve open */ 445 TDB_FILL (coin_sig, 446 42); 447 TDB_FILL (reserve_sig, 448 42); 449 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 450 TALER_EXCHANGEDB_insert_reserve_open_deposit (pg, 451 &coin, 452 &coin_sig, 453 known_coin_id, 454 &one, 455 &reserve_sig, 456 &reserve_pub, 457 &insufficient_funds)); 458 FAILIF (insufficient_funds); 459 460 /* and into a melt */ 461 { 462 struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS rf; 463 struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); 464 struct TALER_Amount coin_balance; 465 bool found; 466 bool zombie_required = false; 467 bool nonce_reuse; 468 bool melt_balance_ok; 469 uint32_t noreveal_index; 470 enum GNUNET_DB_QueryStatus qs; 471 472 memset (&rf, 473 0, 474 sizeof (rf)); 475 rf.coin.coin_pub = coin.coin_pub; 476 rf.coin.denom_pub_hash = coin.denom_pub_hash; 477 rf.coin.no_age_commitment = coin.no_age_commitment; 478 TDB_FILL (rf.coin_sig, 479 43); 480 TDB_FILL (rf.rc, 481 43); 482 TDB_FILL (rf.refresh_seed, 483 43); 484 TDB_FILL (rf.planchets_h, 485 43); 486 TDB_FILL (rf.selected_h, 487 44); 488 rf.amount_with_fee = one; 489 rf.num_coins = 1; 490 rf.denom_serials = GNUNET_new (uint64_t); 491 rf.denom_serials[0] = denom.serial; 492 rf.denom_sigs = GNUNET_new (struct TALER_BlindedDenominationSignature); 493 TDB_blinded_denom_sig (43, 494 &rf.denom_sigs[0]); 495 rf.noreveal_index = 1; 496 rf.is_v27_refresh = true; 497 rf.no_blinding_seed = true; 498 qs = TALER_EXCHANGEDB_do_refresh (pg, 499 &rf, 500 &now, 501 &found, 502 &noreveal_index, 503 &zombie_required, 504 &nonce_reuse, 505 &melt_balance_ok, 506 &coin_balance); 507 TALER_blinded_denom_sig_free (&rf.denom_sigs[0]); 508 GNUNET_free (rf.denom_sigs); 509 GNUNET_free (rf.denom_serials); 510 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs); 511 FAILIF (! melt_balance_ok); 512 } 513 514 FAILIF (5 != TDB_count (pg, 515 "FROM coin_history")); 516 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 517 history (pg, 518 0, 519 0, 520 &etag, 521 &tl)); 522 FAILIF_C (5 != summarize (tl, 523 &types), 524 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 525 FAILIF_C (0 == (types & (1U << TALER_EXCHANGEDB_TT_PURSE_DEPOSIT)), 526 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 527 FAILIF_C (0 == (types & (1U << TALER_EXCHANGEDB_TT_RESERVE_OPEN)), 528 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 529 FAILIF_C (0 == (types & (1U << TALER_EXCHANGEDB_TT_MELT)), 530 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 531 532 /* the ETag is the highest coin history row of the list */ 533 { 534 uint64_t max = 0; 535 536 for (const struct TALER_EXCHANGEDB_TransactionList *pos = tl; 537 NULL != pos; 538 pos = pos->next) 539 max = GNUNET_MAX (max, 540 pos->coin_history_id); 541 FAILIF_C (etag != max, 542 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 543 } 544 545 /* the spends add up to what the coin no longer has */ 546 { 547 struct TALER_Amount total; 548 struct TALER_Amount expect = TDB_amount ("3.5"); 549 550 FAILIF_C (GNUNET_OK != 551 TALER_EXCHANGEDB_calculate_transaction_list_totals (tl, 552 &zero, 553 &total), 554 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 555 FAILIF_C (0 != TALER_amount_cmp (&total, 556 &expect), 557 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 558 } 559 TALER_EXCHANGEDB_free_coin_transaction_list (tl); 560 return 0; 561 } 562 563 564 /** 565 * Recouping the coin adds the last entry and empties it. 566 * 567 * @param pg the database context 568 * @return 0 on success 569 */ 570 static int 571 check_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg) 572 { 573 struct TALER_EXCHANGEDB_TransactionList *tl = NULL; 574 struct TALER_CoinSpendSignatureP coin_sig; 575 union GNUNET_CRYPTO_BlindingSecretP coin_bks; 576 struct GNUNET_TIME_Timestamp recoup_timestamp; 577 struct TALER_Amount balance; 578 struct TALER_Amount zero = TDB_amount ("0"); 579 struct TALER_DenominationHashP h_denom_pub; 580 unsigned int types; 581 uint64_t etag = 0; 582 bool recoup_ok; 583 bool internal_failure; 584 585 TDB_FILL (coin_sig, 586 50); 587 TDB_FILL (coin_bks, 588 50); 589 /* in/out: the caller picks the time, the callee only overwrites it if 590 the coin was recouped before */ 591 recoup_timestamp = GNUNET_TIME_timestamp_get (); 592 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 593 TALER_EXCHANGEDB_do_recoup (pg, 594 &reserve_pub, 595 withdraw_id, 596 &coin_bks, 597 &coin.coin_pub, 598 known_coin_id, 599 &coin_sig, 600 &recoup_timestamp, 601 &recoup_ok, 602 &internal_failure)); 603 FAILIF (internal_failure); 604 FAILIF (! recoup_ok); 605 FAILIF (6 != TDB_count (pg, 606 "FROM coin_history")); 607 608 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 609 TALER_EXCHANGEDB_get_coin_transactions (pg, 610 true, 611 &coin.coin_pub, 612 0, 613 0, 614 &etag, 615 &balance, 616 &h_denom_pub, 617 &tl)); 618 FAILIF_C (6 != summarize (tl, 619 &types), 620 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 621 FAILIF_C (0 == (types & (1U << TALER_EXCHANGEDB_TT_RECOUP_WITHDRAW)), 622 TALER_EXCHANGEDB_free_coin_transaction_list (tl)); 623 TALER_EXCHANGEDB_free_coin_transaction_list (tl); 624 FAILIF (0 != TALER_amount_cmp (&balance, 625 &zero)); 626 return 0; 627 } 628 629 630 /** 631 * With begin_transaction false the caller's transaction is used and left 632 * open. 633 * 634 * @param pg the database context 635 * @return 0 on success 636 */ 637 static int 638 check_in_transaction (struct TALER_EXCHANGEDB_PostgresContext *pg) 639 { 640 struct TALER_EXCHANGEDB_TransactionList *tl = NULL; 641 struct TALER_Amount balance; 642 struct TALER_DenominationHashP h_denom_pub; 643 unsigned int types; 644 uint64_t etag = 0; 645 646 FAILIF (GNUNET_OK != 647 TALER_EXCHANGEDB_start (pg, 648 "coin-history")); 649 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 650 TALER_EXCHANGEDB_get_coin_transactions (pg, 651 false, 652 &coin.coin_pub, 653 0, 654 0, 655 &etag, 656 &balance, 657 &h_denom_pub, 658 &tl), 659 TALER_EXCHANGEDB_rollback (pg)); 660 FAILIF_C (6 != summarize (tl, 661 &types), 662 TALER_EXCHANGEDB_free_coin_transaction_list (tl); 663 TALER_EXCHANGEDB_rollback (pg)); 664 TALER_EXCHANGEDB_free_coin_transaction_list (tl); 665 FAILIF_C (NULL == pg->transaction_name, 666 TALER_EXCHANGEDB_rollback (pg)); 667 TALER_EXCHANGEDB_rollback (pg); 668 return 0; 669 } 670 671 672 /** 673 * The checks to run, in order. 674 */ 675 static const struct TDB_Test tests[] = { 676 { "coin-history-unknown-coin", 677 &check_unknown_coin }, 678 { "coin-history-fresh-coin", 679 &check_fresh_coin }, 680 { "coin-history-deposit-and-refund", 681 &check_deposit_and_refund }, 682 { "coin-history-etag", 683 &check_etag }, 684 { "coin-history-offset", 685 &check_offset }, 686 { "coin-history-other-spends", 687 &check_other_spends }, 688 { "coin-history-recoup", 689 &check_recoup }, 690 { "coin-history-in-transaction", 691 &check_in_transaction }, 692 { NULL, NULL } 693 }; 694 695 696 int 697 main (int argc, 698 char *const *argv) 699 { 700 int ret; 701 702 ret = TDB_main (argc, 703 argv, 704 "test-coin-history", 705 "Tests for the exchangedb `coin_history' table", 706 tests); 707 TDB_coin_free (&coin); 708 TDB_denom_free (&denom); 709 TDB_account_free (&account); 710 return ret; 711 } 712 713 714 /* end of test_coin_history.c */