test_account_merges.c (20150B)
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_account_merges.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `account_merges` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_do_reserve_purse(), 23 * #TALER_EXCHANGEDB_iterate_account_merges_above_serial_id() and 24 * #TALER_EXCHANGEDB_iterate_merge_amounts_for_kyc_check(). 25 * 26 * `account_merges` is the account holder's side of a merge: the request 27 * the wallet signs to have a purse land in its reserve. do_reserve_purse() 28 * writes it (together with the `purse_merges` row) and declines when the 29 * reserve is unknown, when its purse quota is used up, and when the purse 30 * was already merged into a different reserve. 31 */ 32 #include "test_common.h" 33 #include "exchange-database/do_reserve_purse.h" 34 #include "exchange-database/insert_purse_request.h" 35 #include "exchange-database/iterate_account_merges_above_serial_id.h" 36 #include "exchange-database/iterate_merge_amounts_for_kyc_check.h" 37 #include "exchange-database/rollback.h" 38 #include "exchange-database/start.h" 39 40 41 /** 42 * Wallet account the reserves of the checks belong to. 43 */ 44 static struct TDB_Account account; 45 46 47 /** 48 * Build a timestamp from a number of seconds since the epoch. 49 * 50 * @param secs seconds since the epoch 51 * @return the timestamp 52 */ 53 static struct GNUNET_TIME_Timestamp 54 ts (uint64_t secs) 55 { 56 struct GNUNET_TIME_Absolute abs = { 57 .abs_value_us = secs * 1000LLU * 1000LLU 58 }; 59 60 return GNUNET_TIME_absolute_to_timestamp (abs); 61 } 62 63 64 /** 65 * Closure for #merge_cb(). 66 */ 67 struct MergeContext 68 { 69 /** 70 * How many rows did the callback see? 71 */ 72 unsigned int total; 73 74 /** 75 * Stop after this many rows; 0 for no limit. 76 */ 77 unsigned int stop_after; 78 79 /** 80 * Purse we are looking for, NULL to match nothing. 81 */ 82 const struct TALER_PurseContractPublicKeyP *purse_pub; 83 84 /** 85 * How many times did we see it? 86 */ 87 unsigned int matched; 88 89 /** 90 * Reserve reported for it. 91 */ 92 struct TALER_ReservePublicKeyP reserve_pub; 93 94 /** 95 * Amount reported for it. 96 */ 97 struct TALER_Amount amount; 98 99 /** 100 * Purse fee reported for it. 101 */ 102 struct TALER_Amount purse_fee; 103 }; 104 105 106 /** 107 * Callback for 108 * #TALER_EXCHANGEDB_iterate_account_merges_above_serial_id(). 109 * 110 * @param cls a `struct MergeContext *` 111 * @param rowid row of the request 112 * @param reserve_pub reserve the purse is to land in 113 * @param purse_pub the purse 114 * @param h_contract_terms contract of the purse 115 * @param purse_expiration when the purse expires 116 * @param amount target amount of the purse 117 * @param min_age age limit of the purse 118 * @param flags flags of the purse 119 * @param purse_fee fee charged to the reserve for the purse 120 * @param merge_timestamp when the request was made 121 * @param reserve_sig signature of the account holder 122 * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop 123 */ 124 static enum GNUNET_GenericReturnValue 125 merge_cb (void *cls, 126 uint64_t rowid, 127 const struct TALER_ReservePublicKeyP *reserve_pub, 128 const struct TALER_PurseContractPublicKeyP *purse_pub, 129 const struct TALER_PrivateContractHashP *h_contract_terms, 130 struct GNUNET_TIME_Timestamp purse_expiration, 131 const struct TALER_Amount *amount, 132 uint32_t min_age, 133 enum TALER_WalletAccountMergeFlags flags, 134 const struct TALER_Amount *purse_fee, 135 struct GNUNET_TIME_Timestamp merge_timestamp, 136 const struct TALER_ReserveSignatureP *reserve_sig) 137 { 138 struct MergeContext *ctx = cls; 139 140 (void) rowid; 141 (void) h_contract_terms; 142 (void) purse_expiration; 143 (void) min_age; 144 (void) flags; 145 (void) merge_timestamp; 146 (void) reserve_sig; 147 ctx->total++; 148 if ( (NULL != ctx->purse_pub) && 149 (0 == GNUNET_memcmp (purse_pub, 150 ctx->purse_pub)) ) 151 { 152 ctx->matched++; 153 ctx->reserve_pub = *reserve_pub; 154 ctx->amount = *amount; 155 ctx->purse_fee = *purse_fee; 156 } 157 if ( (0 != ctx->stop_after) && 158 (ctx->total >= ctx->stop_after) ) 159 return GNUNET_SYSERR; 160 return GNUNET_OK; 161 } 162 163 164 /** 165 * Closure for #amount_cb(). 166 */ 167 struct AmountContext 168 { 169 /** 170 * How many amounts did the callback see? 171 */ 172 unsigned int total; 173 174 /** 175 * Sum of the whole-unit parts of the amounts seen. 176 */ 177 uint64_t value_sum; 178 179 /** 180 * Return this from the callback. 181 */ 182 enum GNUNET_GenericReturnValue ret; 183 }; 184 185 186 /** 187 * Callback for 188 * #TALER_EXCHANGEDB_iterate_merge_amounts_for_kyc_check(). 189 * 190 * @param cls a `struct AmountContext *` 191 * @param amount how much was merged 192 * @param date when it was merged 193 * @return what @e ret of the closure says 194 */ 195 static enum GNUNET_GenericReturnValue 196 amount_cb (void *cls, 197 const struct TALER_Amount *amount, 198 struct GNUNET_TIME_Absolute date) 199 { 200 struct AmountContext *ctx = cls; 201 202 (void) date; 203 ctx->total++; 204 ctx->value_sum += amount->value; 205 return ctx->ret; 206 } 207 208 209 /** 210 * Outcome of a reserve-purse request. 211 */ 212 struct PurseStatus 213 { 214 /** 215 * Was the purse already merged into another reserve? 216 */ 217 bool in_conflict; 218 219 /** 220 * Is the reserve unknown? 221 */ 222 bool no_reserve; 223 224 /** 225 * Has the reserve run out of purses? 226 */ 227 bool insufficient_funds; 228 }; 229 230 231 /** 232 * Ask for a purse to be merged into a reserve. 233 * 234 * @param pg the database context 235 * @param purse purse to merge 236 * @param reserve_pub reserve to merge it into 237 * @param seed seed for the signatures 238 * @param purse_fee fee to charge the reserve, NULL to use the quota 239 * @param[out] st set to the outcome 240 * @return transaction status 241 */ 242 static enum GNUNET_DB_QueryStatus 243 run_reserve_purse (struct TALER_EXCHANGEDB_PostgresContext *pg, 244 const struct TDB_Purse *purse, 245 const struct TALER_ReservePublicKeyP *reserve_pub, 246 uint32_t seed, 247 const struct TALER_Amount *purse_fee, 248 struct PurseStatus *st) 249 { 250 struct TALER_PurseMergeSignatureP merge_sig; 251 struct TALER_ReserveSignatureP reserve_sig; 252 253 TDB_fill (&merge_sig, 254 sizeof (merge_sig), 255 seed); 256 TDB_fill (&reserve_sig, 257 sizeof (reserve_sig), 258 seed); 259 memset (st, 260 0, 261 sizeof (*st)); 262 return TALER_EXCHANGEDB_do_reserve_purse (pg, 263 &purse->purse_pub, 264 &merge_sig, 265 ts (1600000000), 266 &reserve_sig, 267 purse_fee, 268 reserve_pub, 269 &st->in_conflict, 270 &st->no_reserve, 271 &st->insufficient_funds); 272 } 273 274 275 /** 276 * Create a purse that records a purse fee, the way the /reserves/$RP/purse 277 * endpoint does when the account holder offers to pay for the purse. 278 * TDB_purse() always records a zero fee, and the auditor's view reports 279 * the fee the *purse* recorded, not the one do_reserve_purse() charged. 280 * 281 * @param pg the database context 282 * @param seed seed for the purse's keys and signature 283 * @param amount target amount of the purse, e.g. "5" 284 * @param fee purse fee the account holder offers, e.g. "1" 285 * @param[out] purse set to the purse 286 */ 287 static void 288 make_purse (struct TALER_EXCHANGEDB_PostgresContext *pg, 289 uint32_t seed, 290 const char *amount, 291 const char *fee, 292 struct TDB_Purse *purse) 293 { 294 struct TALER_Amount purse_fee = TDB_amount (fee); 295 enum GNUNET_DB_QueryStatus qs; 296 bool in_conflict = false; 297 298 memset (purse, 299 0, 300 sizeof (*purse)); 301 TDB_fill (&purse->purse_pub, 302 sizeof (purse->purse_pub), 303 seed); 304 TDB_fill (&purse->merge_pub, 305 sizeof (purse->merge_pub), 306 seed); 307 TDB_fill (&purse->h_contract_terms, 308 sizeof (purse->h_contract_terms), 309 seed); 310 TDB_fill (&purse->purse_sig, 311 sizeof (purse->purse_sig), 312 seed); 313 purse->purse_expiration = ts (1700000000); 314 purse->amount = TDB_amount (amount); 315 qs = TALER_EXCHANGEDB_insert_purse_request ( 316 pg, 317 &purse->purse_pub, 318 &purse->merge_pub, 319 purse->purse_expiration, 320 &purse->h_contract_terms, 321 0, 322 TALER_WAMF_MODE_CREATE_WITH_PURSE_FEE, 323 &purse_fee, 324 &purse->amount, 325 &purse->purse_sig, 326 &in_conflict); 327 GNUNET_assert (0 <= qs); 328 GNUNET_assert (! in_conflict); 329 } 330 331 332 /** 333 * Nothing is reported while the table is empty. 334 * 335 * @param pg the database context 336 * @return 0 on success 337 */ 338 static int 339 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg) 340 { 341 struct MergeContext ctx = { 0 }; 342 struct AmountContext actx = { 0 }; 343 344 TDB_account (pg, 345 10, 346 &account); 347 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 348 TALER_EXCHANGEDB_iterate_account_merges_above_serial_id ( 349 pg, 350 0, 351 &merge_cb, 352 &ctx)); 353 actx.ret = GNUNET_OK; 354 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 355 TALER_EXCHANGEDB_iterate_merge_amounts_for_kyc_check ( 356 pg, 357 &account.h_normalized, 358 GNUNET_TIME_UNIT_ZERO_ABS, 359 &amount_cb, 360 &actx)); 361 FAILIF (0 != ctx.total); 362 FAILIF (0 != actx.total); 363 return 0; 364 } 365 366 367 /** 368 * A reserve that does not exist cannot take a purse. 369 * 370 * @param pg the database context 371 * @return 0 on success 372 */ 373 static int 374 check_no_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg) 375 { 376 struct TDB_Purse purse; 377 struct TALER_ReservePublicKeyP reserve_pub; 378 struct TALER_Amount purse_fee = TDB_amount ("1"); 379 struct PurseStatus st; 380 381 TDB_purse (pg, 382 9, 383 "5", 384 ts (1700000000), 385 &purse); 386 TDB_FILL (reserve_pub, 387 99); 388 /* A non-zero purse fee is what makes an unknown reserve fatal: with a 389 zero fee the function creates the reserve instead. The `purse_merges` 390 row goes in before the reserve is looked at, so the caller has to roll 391 back -- which is what the /purses/.../merge handler does. */ 392 FAILIF (GNUNET_OK != 393 TALER_EXCHANGEDB_start (pg, 394 "test-account-merges-no-reserve")); 395 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 396 run_reserve_purse (pg, 397 &purse, 398 &reserve_pub, 399 9, 400 &purse_fee, 401 &st), 402 TALER_EXCHANGEDB_rollback (pg)); 403 TALER_EXCHANGEDB_rollback (pg); 404 FAILIF (! st.no_reserve); 405 FAILIF (! st.insufficient_funds); 406 FAILIF (0 != TDB_count (pg, 407 "FROM account_merges")); 408 FAILIF (0 != TDB_count (pg, 409 "FROM purse_merges")); 410 return 0; 411 } 412 413 414 /** 415 * Paying the purse fee from the reserve creates the merge request. 416 * 417 * @param pg the database context 418 * @return 0 on success 419 */ 420 static int 421 check_reserve_purse (struct TALER_EXCHANGEDB_PostgresContext *pg) 422 { 423 struct TDB_Purse purse; 424 struct TALER_ReservePublicKeyP reserve_pub; 425 struct TALER_Amount purse_fee = TDB_amount ("1"); 426 struct TALER_Amount expect_balance = TDB_amount ("9"); 427 struct MergeContext ctx; 428 struct PurseStatus st; 429 char *hex; 430 431 make_purse (pg, 432 10, 433 "5", 434 "1", 435 &purse); 436 TDB_reserve (pg, 437 10, 438 "10", 439 &reserve_pub); 440 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 441 run_reserve_purse (pg, 442 &purse, 443 &reserve_pub, 444 10, 445 &purse_fee, 446 &st)); 447 FAILIF (st.no_reserve); 448 FAILIF (st.in_conflict); 449 FAILIF (st.insufficient_funds); 450 FAILIF (1 != TDB_count (pg, 451 "FROM account_merges")); 452 /* the merge is also recorded from the purse's side */ 453 FAILIF (1 != TDB_count (pg, 454 "FROM purse_merges")); 455 /* the reserve paid the purse fee */ 456 hex = TDB_hex (&reserve_pub, 457 sizeof (reserve_pub)); 458 FAILIF_C (1 != TDB_count (pg, 459 "FROM reserves" 460 " WHERE reserve_pub=decode('%s','hex')" 461 " AND current_balance=ROW(9,0)::taler_amount", 462 hex), 463 GNUNET_free (hex)); 464 GNUNET_free (hex); 465 (void) expect_balance; 466 467 memset (&ctx, 468 0, 469 sizeof (ctx)); 470 ctx.purse_pub = &purse.purse_pub; 471 FAILIF (0 >= 472 TALER_EXCHANGEDB_iterate_account_merges_above_serial_id ( 473 pg, 474 0, 475 &merge_cb, 476 &ctx)); 477 FAILIF (1 != ctx.matched); 478 FAILIF (0 != GNUNET_memcmp (&ctx.reserve_pub, 479 &reserve_pub)); 480 FAILIF (0 != TALER_amount_cmp (&ctx.amount, 481 &purse.amount)); 482 FAILIF (0 != TALER_amount_cmp (&ctx.purse_fee, 483 &purse_fee)); 484 485 /* the same purse into a different reserve is a conflict */ 486 { 487 struct TALER_ReservePublicKeyP other; 488 489 TDB_reserve (pg, 490 11, 491 "10", 492 &other); 493 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 494 run_reserve_purse (pg, 495 &purse, 496 &other, 497 11, 498 &purse_fee, 499 &st)); 500 FAILIF (! st.in_conflict); 501 FAILIF (1 != TDB_count (pg, 502 "FROM account_merges")); 503 } 504 return 0; 505 } 506 507 508 /** 509 * A reserve that cannot pay the purse fee is refused. 510 * 511 * @param pg the database context 512 * @return 0 on success 513 */ 514 static int 515 check_insufficient_funds (struct TALER_EXCHANGEDB_PostgresContext *pg) 516 { 517 struct TDB_Purse purse; 518 struct TALER_ReservePublicKeyP reserve_pub; 519 struct TALER_Amount purse_fee = TDB_amount ("5"); 520 struct PurseStatus st; 521 522 TDB_purse (pg, 523 12, 524 "5", 525 ts (1700000000), 526 &purse); 527 TDB_reserve (pg, 528 12, 529 "1", 530 &reserve_pub); 531 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 532 run_reserve_purse (pg, 533 &purse, 534 &reserve_pub, 535 12, 536 &purse_fee, 537 &st)); 538 FAILIF (! st.insufficient_funds); 539 FAILIF (1 != TDB_count (pg, 540 "FROM account_merges")); 541 return 0; 542 } 543 544 545 /** 546 * The wallet's KYC view sums the merges that landed in its account. 547 * 548 * The account in question is the reserve's own `taler-reserve://` account, 549 * which is what do_reserve_purse() records in `wallet_h_payto` -- not a 550 * bank account. 551 * 552 * @param pg the database context 553 * @return 0 on success 554 */ 555 static int 556 check_kyc_amounts (struct TALER_EXCHANGEDB_PostgresContext *pg) 557 { 558 struct AmountContext ctx; 559 struct TALER_ReservePublicKeyP reserve_pub; 560 struct TALER_NormalizedPayto payto; 561 struct TALER_NormalizedPaytoHashP h_payto; 562 struct TALER_NormalizedPaytoHashP other; 563 564 TDB_fill (&reserve_pub, 565 sizeof (reserve_pub), 566 10); 567 payto = TALER_reserve_make_payto (pg->exchange_url, 568 &reserve_pub); 569 TALER_normalized_payto_hash (payto, 570 &h_payto); 571 GNUNET_free (payto.normalized_payto); 572 573 /* the merge of check_reserve_purse() is charged to our account, but the 574 purse has not been decided yet, so the KYC view has nothing */ 575 memset (&ctx, 576 0, 577 sizeof (ctx)); 578 ctx.ret = GNUNET_OK; 579 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 580 TALER_EXCHANGEDB_iterate_merge_amounts_for_kyc_check ( 581 pg, 582 &h_payto, 583 GNUNET_TIME_UNIT_ZERO_ABS, 584 &amount_cb, 585 &ctx)); 586 FAILIF (0 != ctx.total); 587 588 /* once the purse is decided in favour of the reserve, it counts */ 589 FAILIF (GNUNET_OK != 590 TDB_exec (pg, 591 "INSERT INTO purse_decision" 592 " (purse_pub,action_timestamp,refunded)" 593 " SELECT purse_pub,1000000,FALSE" 594 " FROM account_merges;")); 595 memset (&ctx, 596 0, 597 sizeof (ctx)); 598 ctx.ret = GNUNET_OK; 599 FAILIF (0 >= 600 TALER_EXCHANGEDB_iterate_merge_amounts_for_kyc_check ( 601 pg, 602 &h_payto, 603 GNUNET_TIME_UNIT_ZERO_ABS, 604 &amount_cb, 605 &ctx)); 606 FAILIF (0 == ctx.total); 607 FAILIF (0 == ctx.value_sum); 608 609 /* an account nobody merged into has nothing */ 610 TDB_FILL (other, 611 97); 612 memset (&ctx, 613 0, 614 sizeof (ctx)); 615 ctx.ret = GNUNET_OK; 616 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 617 TALER_EXCHANGEDB_iterate_merge_amounts_for_kyc_check ( 618 pg, 619 &other, 620 GNUNET_TIME_UNIT_ZERO_ABS, 621 &amount_cb, 622 &ctx)); 623 FAILIF (0 != ctx.total); 624 625 /* a time limit past the merges hides them */ 626 memset (&ctx, 627 0, 628 sizeof (ctx)); 629 ctx.ret = GNUNET_OK; 630 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 631 TALER_EXCHANGEDB_iterate_merge_amounts_for_kyc_check ( 632 pg, 633 &h_payto, 634 ts (1700000000).abs_time, 635 &amount_cb, 636 &ctx)); 637 FAILIF (0 != ctx.total); 638 639 /* and an aborting callback is not an error */ 640 memset (&ctx, 641 0, 642 sizeof (ctx)); 643 ctx.ret = GNUNET_NO; 644 FAILIF (0 > 645 TALER_EXCHANGEDB_iterate_merge_amounts_for_kyc_check ( 646 pg, 647 &h_payto, 648 GNUNET_TIME_UNIT_ZERO_ABS, 649 &amount_cb, 650 &ctx)); 651 FAILIF (1 != ctx.total); 652 return 0; 653 } 654 655 656 /** 657 * The iterator's bound and abort return behave as documented. 658 * 659 * @param pg the database context 660 * @return 0 on success 661 */ 662 static int 663 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg) 664 { 665 struct MergeContext ctx; 666 667 memset (&ctx, 668 0, 669 sizeof (ctx)); 670 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 671 TALER_EXCHANGEDB_iterate_account_merges_above_serial_id ( 672 pg, 673 1000, 674 &merge_cb, 675 &ctx)); 676 FAILIF (0 != ctx.total); 677 memset (&ctx, 678 0, 679 sizeof (ctx)); 680 ctx.stop_after = 1; 681 FAILIF (0 >= 682 TALER_EXCHANGEDB_iterate_account_merges_above_serial_id ( 683 pg, 684 0, 685 &merge_cb, 686 &ctx)); 687 FAILIF (1 != ctx.total); 688 return 0; 689 } 690 691 692 /** 693 * The checks to run, in order. 694 */ 695 static const struct TDB_Test tests[] = { 696 { "account-merges-empty", 697 &check_empty }, 698 { "account-merges-no-reserve", 699 &check_no_reserve }, 700 { "account-merges-reserve-purse", 701 &check_reserve_purse }, 702 { "account-merges-insufficient-funds", 703 &check_insufficient_funds }, 704 { "account-merges-kyc-amounts", 705 &check_kyc_amounts }, 706 { "account-merges-iterate", 707 &check_iterate }, 708 { NULL, NULL } 709 }; 710 711 712 int 713 main (int argc, 714 char *const *argv) 715 { 716 int ret; 717 718 ret = TDB_main (argc, 719 argv, 720 "test-account-merges", 721 "Tests for the exchangedb `account_merges' table", 722 tests); 723 TDB_account_free (&account); 724 return ret; 725 } 726 727 728 /* end of test_account_merges.c */