test_purse_merges.c (21679B)
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_purse_merges.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `purse_merges` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_do_purse_merge(), 23 * #TALER_EXCHANGEDB_get_purse_merge(), 24 * #TALER_EXCHANGEDB_iterate_purse_merges_above_serial_id(), 25 * #TALER_EXCHANGEDB_iterate_wallet_merges(). 26 * 27 * A merge is the exchange acting on the merge capability key: it moves a 28 * paid-up purse into a reserve. do_purse_merge() therefore declines when 29 * the purse is not paid up and when the named partner exchange is unknown, 30 * and reports a conflict when the purse was already merged elsewhere. 31 */ 32 #include "test_common.h" 33 #include "exchange-database/do_purse_deposit.h" 34 #include "exchange-database/do_purse_merge.h" 35 #include "exchange-database/get_purse_merge.h" 36 #include "exchange-database/get_reserve.h" 37 #include "exchange-database/iterate_purse_merges_above_serial_id.h" 38 #include "exchange-database/iterate_wallet_merges.h" 39 40 41 /** 42 * Denomination the checks pay purses with. 43 */ 44 static struct TDB_Denom denom; 45 46 47 /** 48 * Wallet account the reserve of the checks belongs to. 49 */ 50 static struct TDB_Account account; 51 52 53 /** 54 * Build a timestamp from a number of seconds since the epoch. 55 * 56 * @param secs seconds since the epoch 57 * @return the timestamp 58 */ 59 static struct GNUNET_TIME_Timestamp 60 ts (uint64_t secs) 61 { 62 struct GNUNET_TIME_Absolute abs = { 63 .abs_value_us = secs * 1000LLU * 1000LLU 64 }; 65 66 return GNUNET_TIME_absolute_to_timestamp (abs); 67 } 68 69 70 /** 71 * Closure for #merge_cb(). 72 */ 73 struct MergeContext 74 { 75 /** 76 * How many rows did the callback see? 77 */ 78 unsigned int total; 79 80 /** 81 * Stop after this many rows; 0 for no limit. 82 */ 83 unsigned int stop_after; 84 85 /** 86 * Purse we are looking for, NULL to match nothing. 87 */ 88 const struct TALER_PurseContractPublicKeyP *purse_pub; 89 90 /** 91 * How many times did we see it? 92 */ 93 unsigned int matched; 94 95 /** 96 * Reserve reported for it. 97 */ 98 struct TALER_ReservePublicKeyP reserve_pub; 99 100 /** 101 * Amount reported for it. 102 */ 103 struct TALER_Amount amount; 104 105 /** 106 * Was a partner exchange reported for it? 107 */ 108 bool have_partner; 109 }; 110 111 112 /** 113 * Callback for 114 * #TALER_EXCHANGEDB_iterate_purse_merges_above_serial_id(). 115 * 116 * @param cls a `struct MergeContext *` 117 * @param rowid row of the merge 118 * @param partner_base_url partner exchange, NULL for us 119 * @param amount target amount of the purse 120 * @param balance balance of the purse 121 * @param flags flags of the purse 122 * @param merge_pub merge capability key 123 * @param reserve_pub reserve the purse went into 124 * @param merge_sig signature affirming the merge 125 * @param purse_pub the purse 126 * @param merge_timestamp when it was merged 127 * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop 128 */ 129 static enum GNUNET_GenericReturnValue 130 merge_cb (void *cls, 131 uint64_t rowid, 132 const char *partner_base_url, 133 const struct TALER_Amount *amount, 134 const struct TALER_Amount *balance, 135 enum TALER_WalletAccountMergeFlags flags, 136 const struct TALER_PurseMergePublicKeyP *merge_pub, 137 const struct TALER_ReservePublicKeyP *reserve_pub, 138 const struct TALER_PurseMergeSignatureP *merge_sig, 139 const struct TALER_PurseContractPublicKeyP *purse_pub, 140 struct GNUNET_TIME_Timestamp merge_timestamp) 141 { 142 struct MergeContext *ctx = cls; 143 144 (void) rowid; 145 (void) balance; 146 (void) flags; 147 (void) merge_pub; 148 (void) merge_sig; 149 (void) merge_timestamp; 150 ctx->total++; 151 if ( (NULL != ctx->purse_pub) && 152 (0 == GNUNET_memcmp (purse_pub, 153 ctx->purse_pub)) ) 154 { 155 ctx->matched++; 156 ctx->reserve_pub = *reserve_pub; 157 ctx->amount = *amount; 158 ctx->have_partner = (NULL != partner_base_url); 159 } 160 if ( (0 != ctx->stop_after) && 161 (ctx->total >= ctx->stop_after) ) 162 return GNUNET_SYSERR; 163 return GNUNET_OK; 164 } 165 166 167 /** 168 * Closure for #transfer_cb() and #amount_cb(). 169 */ 170 struct SumContext 171 { 172 /** 173 * How many entries did the callback see? 174 */ 175 unsigned int total; 176 177 /** 178 * Sum of the whole-unit parts of the amounts seen. 179 */ 180 uint64_t value_sum; 181 182 }; 183 184 185 /** 186 * Callback for #TALER_EXCHANGEDB_iterate_wallet_merges(). 187 * 188 * @param cls a `struct SumContext *` 189 * @param row_id row of the merge 190 * @param payto_uri wallet account involved 191 * @param execution_time when the merge happened 192 * @param amount how much was merged 193 */ 194 static void 195 transfer_cb (void *cls, 196 uint64_t row_id, 197 const char *payto_uri, 198 struct GNUNET_TIME_Absolute execution_time, 199 const struct TALER_Amount *amount) 200 { 201 struct SumContext *ctx = cls; 202 203 (void) row_id; 204 (void) payto_uri; 205 (void) execution_time; 206 ctx->total++; 207 ctx->value_sum += amount->value; 208 } 209 210 211 /** 212 * Outcome of a merge. 213 */ 214 struct MergeStatus 215 { 216 /** 217 * Was the named partner exchange unknown? 218 */ 219 bool no_partner; 220 221 /** 222 * Was the purse not paid up? 223 */ 224 bool no_balance; 225 226 /** 227 * Was the purse already merged elsewhere? 228 */ 229 bool in_conflict; 230 }; 231 232 233 /** 234 * Merge a purse into a reserve. 235 * 236 * @param pg the database context 237 * @param purse purse to merge 238 * @param reserve_pub reserve to credit 239 * @param h_payto normalized account of the reserve's wallet 240 * @param seed seed for the merge and reserve signatures 241 * @param partner_url partner exchange, NULL for us 242 * @param[out] st set to the outcome 243 * @return transaction status 244 */ 245 static enum GNUNET_DB_QueryStatus 246 run_merge (struct TALER_EXCHANGEDB_PostgresContext *pg, 247 const struct TDB_Purse *purse, 248 const struct TALER_ReservePublicKeyP *reserve_pub, 249 uint32_t seed, 250 const char *partner_url, 251 struct MergeStatus *st) 252 { 253 struct TALER_PurseMergeSignatureP merge_sig; 254 struct TALER_ReserveSignatureP reserve_sig; 255 256 TDB_fill (&merge_sig, 257 sizeof (merge_sig), 258 seed); 259 TDB_fill (&reserve_sig, 260 sizeof (reserve_sig), 261 seed); 262 memset (st, 263 0, 264 sizeof (*st)); 265 return TALER_EXCHANGEDB_do_purse_merge (pg, 266 &purse->purse_pub, 267 &merge_sig, 268 ts (1600000000), 269 &reserve_sig, 270 partner_url, 271 reserve_pub, 272 &st->no_partner, 273 &st->no_balance, 274 &st->in_conflict); 275 } 276 277 278 /** 279 * Pay a purse up to its target amount. 280 * 281 * @param pg the database context 282 * @param purse purse to fill 283 * @param seed seed for the coin 284 * @param amount how much to pay in, e.g. "5" 285 */ 286 static void 287 fill_purse (struct TALER_EXCHANGEDB_PostgresContext *pg, 288 const struct TDB_Purse *purse, 289 uint32_t seed, 290 const char *amount) 291 { 292 struct TALER_CoinPublicInfo coin; 293 struct TALER_CoinSpendSignatureP coin_sig; 294 struct TALER_Amount a = TDB_amount (amount); 295 bool balance_ok; 296 bool too_late; 297 bool conflict; 298 299 TDB_coin (pg, 300 &denom, 301 seed, 302 &coin, 303 NULL); 304 TDB_fill (&coin_sig, 305 sizeof (coin_sig), 306 seed); 307 GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == 308 TALER_EXCHANGEDB_do_purse_deposit (pg, 309 &purse->purse_pub, 310 &coin.coin_pub, 311 &a, 312 &coin_sig, 313 &a, 314 &balance_ok, 315 &too_late, 316 &conflict)); 317 GNUNET_assert (balance_ok); 318 TDB_coin_free (&coin); 319 } 320 321 322 /** 323 * Nothing is reported while the table is empty. 324 * 325 * @param pg the database context 326 * @return 0 on success 327 */ 328 static int 329 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg) 330 { 331 struct TALER_PurseContractPublicKeyP purse_pub; 332 struct TALER_PurseMergeSignatureP merge_sig; 333 struct GNUNET_TIME_Timestamp merge_timestamp; 334 struct TALER_ReservePublicKeyP reserve_pub; 335 struct MergeContext ctx = { 0 }; 336 struct SumContext sctx = { 0 }; 337 struct TALER_Amount zero = TDB_amount ("0"); 338 char *partner_url = NULL; 339 bool refunded; 340 341 TDB_denom (pg, 342 10, 343 "5", 344 "0.1", 345 &denom); 346 TDB_account (pg, 347 10, 348 &account); 349 TDB_FILL (purse_pub, 350 1); 351 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 352 TALER_EXCHANGEDB_get_purse_merge (pg, 353 &purse_pub, 354 &merge_sig, 355 &merge_timestamp, 356 &partner_url, 357 &reserve_pub, 358 &refunded)); 359 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 360 TALER_EXCHANGEDB_iterate_purse_merges_above_serial_id ( 361 pg, 362 0, 363 &merge_cb, 364 &ctx)); 365 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 366 TALER_EXCHANGEDB_iterate_wallet_merges (pg, 367 &zero, 368 0, 369 10, 370 NULL, 371 &transfer_cb, 372 &sctx)); 373 FAILIF (0 != ctx.total); 374 FAILIF (0 != sctx.total); 375 return 0; 376 } 377 378 379 /** 380 * A purse that has not been paid up cannot be merged. 381 * 382 * @param pg the database context 383 * @return 0 on success 384 */ 385 static int 386 check_no_balance (struct TALER_EXCHANGEDB_PostgresContext *pg) 387 { 388 struct TDB_Purse purse; 389 struct TALER_ReservePublicKeyP reserve_pub; 390 struct MergeStatus st; 391 392 TDB_purse (pg, 393 10, 394 "5", 395 ts (1700000000), 396 &purse); 397 TDB_reserve (pg, 398 10, 399 "0", 400 &reserve_pub); 401 /* only EUR:1 of the EUR:5 has been paid in */ 402 fill_purse (pg, 403 &purse, 404 20, 405 "1"); 406 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 407 run_merge (pg, 408 &purse, 409 &reserve_pub, 410 10, 411 NULL, 412 &st)); 413 FAILIF (! st.no_balance); 414 FAILIF (st.no_partner); 415 FAILIF (st.in_conflict); 416 FAILIF (0 != TDB_count (pg, 417 "FROM purse_merges")); 418 return 0; 419 } 420 421 422 /** 423 * A partner exchange the exchange does not know cannot receive a merge. 424 * 425 * @param pg the database context 426 * @return 0 on success 427 */ 428 static int 429 check_no_partner (struct TALER_EXCHANGEDB_PostgresContext *pg) 430 { 431 struct TDB_Purse purse; 432 struct TALER_ReservePublicKeyP reserve_pub; 433 struct MergeStatus st; 434 435 TDB_purse (pg, 436 10, 437 "5", 438 ts (1700000000), 439 &purse); 440 TDB_fill (&reserve_pub, 441 sizeof (reserve_pub), 442 10); 443 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 444 run_merge (pg, 445 &purse, 446 &reserve_pub, 447 10, 448 "https://partner.example/", 449 &st)); 450 FAILIF (! st.no_partner); 451 FAILIF (0 != TDB_count (pg, 452 "FROM purse_merges")); 453 return 0; 454 } 455 456 457 /** 458 * A paid-up purse is merged into the reserve, which is credited. 459 * 460 * @param pg the database context 461 * @return 0 on success 462 */ 463 static int 464 check_merge (struct TALER_EXCHANGEDB_PostgresContext *pg) 465 { 466 struct TDB_Purse purse; 467 struct TALER_ReservePublicKeyP reserve_pub; 468 struct TALER_ReservePublicKeyP got_reserve; 469 struct TALER_EXCHANGEDB_Reserve reserve; 470 struct MergeStatus st; 471 struct MergeContext ctx; 472 struct TALER_PurseMergeSignatureP merge_sig; 473 struct TALER_PurseMergeSignatureP expect_sig; 474 struct GNUNET_TIME_Timestamp merge_timestamp; 475 struct TALER_Amount expect_balance = TDB_amount ("5"); 476 char *partner_url = NULL; 477 bool refunded = true; 478 479 TDB_purse (pg, 480 10, 481 "5", 482 ts (1700000000), 483 &purse); 484 TDB_fill (&reserve_pub, 485 sizeof (reserve_pub), 486 10); 487 /* pay in the remaining EUR:4 */ 488 fill_purse (pg, 489 &purse, 490 21, 491 "4"); 492 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 493 run_merge (pg, 494 &purse, 495 &reserve_pub, 496 10, 497 NULL, 498 &st)); 499 FAILIF (st.no_balance); 500 FAILIF (st.no_partner); 501 FAILIF (st.in_conflict); 502 FAILIF (1 != TDB_count (pg, 503 "FROM purse_merges")); 504 505 /* the reserve got the money */ 506 memset (&reserve, 507 0, 508 sizeof (reserve)); 509 reserve.pub = reserve_pub; 510 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 511 TALER_EXCHANGEDB_get_reserve (pg, 512 &reserve)); 513 FAILIF (0 != TALER_amount_cmp (&reserve.balance, 514 &expect_balance)); 515 516 TDB_FILL (expect_sig, 517 10); 518 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 519 TALER_EXCHANGEDB_get_purse_merge (pg, 520 &purse.purse_pub, 521 &merge_sig, 522 &merge_timestamp, 523 &partner_url, 524 &got_reserve, 525 &refunded)); 526 FAILIF (0 != GNUNET_memcmp (&merge_sig, 527 &expect_sig)); 528 FAILIF (0 != GNUNET_memcmp (&got_reserve, 529 &reserve_pub)); 530 FAILIF (GNUNET_TIME_timestamp_cmp (merge_timestamp, 531 !=, 532 ts (1600000000))); 533 FAILIF (refunded); 534 /* the purse stayed with us, so there is no partner exchange */ 535 FAILIF (NULL != partner_url); 536 537 memset (&ctx, 538 0, 539 sizeof (ctx)); 540 ctx.purse_pub = &purse.purse_pub; 541 FAILIF (0 >= 542 TALER_EXCHANGEDB_iterate_purse_merges_above_serial_id ( 543 pg, 544 0, 545 &merge_cb, 546 &ctx)); 547 FAILIF (1 != ctx.matched); 548 FAILIF (0 != GNUNET_memcmp (&ctx.reserve_pub, 549 &reserve_pub)); 550 FAILIF (0 != TALER_amount_cmp (&ctx.amount, 551 &expect_balance)); 552 FAILIF (ctx.have_partner); 553 554 /* merging the same purse into a different reserve is a conflict */ 555 { 556 struct TALER_ReservePublicKeyP other; 557 558 TDB_reserve (pg, 559 12, 560 "0", 561 &other); 562 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 563 run_merge (pg, 564 &purse, 565 &other, 566 12, 567 NULL, 568 &st)); 569 FAILIF (! st.in_conflict); 570 FAILIF (1 != TDB_count (pg, 571 "FROM purse_merges")); 572 } 573 return 0; 574 } 575 576 577 /** 578 * The wallet's view of its merges reports the purse once the reserve is 579 * tied to a wallet account. 580 * 581 * @param pg the database context 582 * @return 0 on success 583 */ 584 static int 585 check_wallet_merges (struct TALER_EXCHANGEDB_PostgresContext *pg) 586 { 587 struct TALER_ReservePublicKeyP reserve_pub; 588 struct TALER_Amount zero = TDB_amount ("0"); 589 struct TALER_Amount huge = TDB_amount ("1000"); 590 struct SumContext ctx; 591 char *rhex; 592 char *nhex; 593 594 /* Until the reserve is known to belong to a wallet account, the view 595 has nothing to join against. */ 596 memset (&ctx, 597 0, 598 sizeof (ctx)); 599 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 600 TALER_EXCHANGEDB_iterate_wallet_merges (pg, 601 &zero, 602 0, 603 10, 604 NULL, 605 &transfer_cb, 606 &ctx)); 607 FAILIF (0 != ctx.total); 608 609 TDB_fill (&reserve_pub, 610 sizeof (reserve_pub), 611 10); 612 rhex = TDB_hex (&reserve_pub, 613 sizeof (reserve_pub)); 614 nhex = TDB_hex (&account.h_normalized, 615 sizeof (account.h_normalized)); 616 FAILIF_C (GNUNET_OK != 617 TDB_exec (pg, 618 "UPDATE kyc_targets" 619 " SET target_pub=decode('%s','hex')" 620 " ,is_wallet=TRUE" 621 " WHERE h_normalized_payto=decode('%s','hex');", 622 rhex, 623 nhex), 624 GNUNET_free (rhex); GNUNET_free (nhex)); 625 GNUNET_free (rhex); 626 GNUNET_free (nhex); 627 628 memset (&ctx, 629 0, 630 sizeof (ctx)); 631 FAILIF (1 != 632 TALER_EXCHANGEDB_iterate_wallet_merges (pg, 633 &zero, 634 0, 635 10, 636 NULL, 637 &transfer_cb, 638 &ctx)); 639 FAILIF (5 != ctx.value_sum); 640 641 /* filtering by that very account keeps it */ 642 memset (&ctx, 643 0, 644 sizeof (ctx)); 645 FAILIF (1 != 646 TALER_EXCHANGEDB_iterate_wallet_merges (pg, 647 &zero, 648 0, 649 10, 650 &account.h_normalized, 651 &transfer_cb, 652 &ctx)); 653 FAILIF (1 != ctx.total); 654 655 /* a threshold nobody meets drops it */ 656 memset (&ctx, 657 0, 658 sizeof (ctx)); 659 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 660 TALER_EXCHANGEDB_iterate_wallet_merges (pg, 661 &huge, 662 0, 663 10, 664 NULL, 665 &transfer_cb, 666 &ctx)); 667 FAILIF (0 != ctx.total); 668 669 /* and the negative limit walks the table the other way */ 670 memset (&ctx, 671 0, 672 sizeof (ctx)); 673 FAILIF (1 != 674 TALER_EXCHANGEDB_iterate_wallet_merges (pg, 675 &zero, 676 1000, 677 -10, 678 NULL, 679 &transfer_cb, 680 &ctx)); 681 FAILIF (1 != ctx.total); 682 return 0; 683 } 684 685 686 /** 687 * The iterators' bounds and abort return behave as documented. 688 * 689 * @param pg the database context 690 * @return 0 on success 691 */ 692 static int 693 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg) 694 { 695 struct MergeContext ctx; 696 697 memset (&ctx, 698 0, 699 sizeof (ctx)); 700 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 701 TALER_EXCHANGEDB_iterate_purse_merges_above_serial_id ( 702 pg, 703 1000, 704 &merge_cb, 705 &ctx)); 706 FAILIF (0 != ctx.total); 707 memset (&ctx, 708 0, 709 sizeof (ctx)); 710 ctx.stop_after = 1; 711 FAILIF (1 != 712 TALER_EXCHANGEDB_iterate_purse_merges_above_serial_id ( 713 pg, 714 0, 715 &merge_cb, 716 &ctx)); 717 FAILIF (1 != ctx.total); 718 return 0; 719 } 720 721 722 /** 723 * The checks to run, in order. 724 */ 725 static const struct TDB_Test tests[] = { 726 { "purse-merges-empty", 727 &check_empty }, 728 { "purse-merges-no-balance", 729 &check_no_balance }, 730 { "purse-merges-no-partner", 731 &check_no_partner }, 732 { "purse-merges-merge", 733 &check_merge }, 734 { "purse-merges-wallet-merges", 735 &check_wallet_merges }, 736 { "purse-merges-iterate", 737 &check_iterate }, 738 { NULL, NULL } 739 }; 740 741 742 int 743 main (int argc, 744 char *const *argv) 745 { 746 int ret; 747 748 ret = TDB_main (argc, 749 argv, 750 "test-purse-merges", 751 "Tests for the exchangedb `purse_merges' table", 752 tests); 753 TDB_account_free (&account); 754 TDB_denom_free (&denom); 755 return ret; 756 } 757 758 759 /* end of test_purse_merges.c */