test_aggregation_tracking.c (23077B)
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_aggregation_tracking.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `aggregation_tracking` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_do_aggregate(), 23 * #TALER_EXCHANGEDB_get_pending_aggregation(), 24 * #TALER_EXCHANGEDB_get_transfer_by_deposit(), 25 * #TALER_EXCHANGEDB_iterate_aggregation_wtids_above_serial_id(), 26 * #TALER_EXCHANGEDB_iterate_aggregations_above_serial_id() and 27 * #TALER_EXCHANGEDB_iterate_wire_transfers(). 28 * 29 * `aggregation_tracking` records which deposit went into which wire 30 * transfer. do_aggregate() writes it, netting deposits against refunds and 31 * charging the deposit fee only for coins that were not refunded in full -- 32 * the checks below pin that arithmetic down. 33 */ 34 #include "test_common.h" 35 #include "exchange-database/do_aggregate.h" 36 #include "exchange-database/do_refund.h" 37 #include "exchange-database/get_pending_aggregation.h" 38 #include "exchange-database/get_transfer_by_deposit.h" 39 #include "exchange-database/insert_wire_out.h" 40 #include "exchange-database/iterate_aggregation_wtids_above_serial_id.h" 41 #include "exchange-database/iterate_aggregations_above_serial_id.h" 42 #include "exchange-database/iterate_wire_transfers.h" 43 44 45 /** 46 * Account the merchants of the checks are paid at. 47 */ 48 static struct TDB_Account account; 49 50 51 /** 52 * Denomination the checks deposit. 53 */ 54 static struct TDB_Denom denom; 55 56 57 /** 58 * Build a timestamp from a number of seconds since the epoch. 59 * 60 * @param secs seconds since the epoch 61 * @return the timestamp 62 */ 63 static struct GNUNET_TIME_Timestamp 64 ts (uint64_t secs) 65 { 66 struct GNUNET_TIME_Absolute abs = { 67 .abs_value_us = secs * 1000LLU * 1000LLU 68 }; 69 70 return GNUNET_TIME_absolute_to_timestamp (abs); 71 } 72 73 74 /** 75 * Closure for #wtid_cb(). 76 */ 77 struct WtidContext 78 { 79 /** 80 * How many rows did the callback see? 81 */ 82 unsigned int total; 83 84 /** 85 * Wire transfer we are looking for, NULL to match nothing. 86 */ 87 const struct TALER_WireTransferIdentifierRawP *wtid; 88 89 /** 90 * How many times did we see it? 91 */ 92 unsigned int matched; 93 94 /** 95 * Was the last matching row reported as pending? 96 */ 97 bool pending; 98 }; 99 100 101 /** 102 * Callback for 103 * #TALER_EXCHANGEDB_iterate_aggregation_wtids_above_serial_id(). 104 * 105 * @param cls a `struct WtidContext *` 106 * @param rowid row of the tracking entry 107 * @param wtid wire transfer the deposit was aggregated into 108 * @param wire_target_h_payto where the transfer goes 109 * @param pending whether the transfer has not been made yet 110 * @return #GNUNET_OK 111 */ 112 static enum GNUNET_GenericReturnValue 113 wtid_cb (void *cls, 114 uint64_t rowid, 115 const struct TALER_WireTransferIdentifierRawP *wtid, 116 const struct TALER_FullPaytoHashP *wire_target_h_payto, 117 bool pending) 118 { 119 struct WtidContext *ctx = cls; 120 121 (void) rowid; 122 (void) wire_target_h_payto; 123 ctx->total++; 124 if ( (NULL != ctx->wtid) && 125 (0 == GNUNET_memcmp (wtid, 126 ctx->wtid)) ) 127 { 128 ctx->matched++; 129 ctx->pending = pending; 130 } 131 return GNUNET_OK; 132 } 133 134 135 /** 136 * Closure for #aggregation_cb() and #transfer_cb(). 137 */ 138 struct SumContext 139 { 140 /** 141 * How many rows did the callback see? 142 */ 143 unsigned int total; 144 145 /** 146 * Sum of the whole-unit parts of the amounts seen. 147 */ 148 uint64_t value_sum; 149 150 /** 151 * Row of the first entry seen. 152 */ 153 uint64_t first_row; 154 }; 155 156 157 /** 158 * Callback for #TALER_EXCHANGEDB_iterate_aggregations_above_serial_id(). 159 * 160 * @param cls a `struct SumContext *` 161 * @param amount how much was aggregated 162 * @param tracking_serial_id row of the tracking entry 163 * @param batch_deposit_serial_id deposit that was aggregated 164 */ 165 static void 166 aggregation_cb (void *cls, 167 const struct TALER_Amount *amount, 168 uint64_t tracking_serial_id, 169 uint64_t batch_deposit_serial_id) 170 { 171 struct SumContext *ctx = cls; 172 173 (void) batch_deposit_serial_id; 174 if (0 == ctx->total++) 175 ctx->first_row = tracking_serial_id; 176 ctx->value_sum += amount->value; 177 } 178 179 180 /** 181 * Callback for #TALER_EXCHANGEDB_iterate_wire_transfers(). 182 * 183 * @param cls a `struct SumContext *` 184 * @param rowid row of the tracking entry 185 * @param merchant_pub merchant that was paid 186 * @param account_payto_uri account of the merchant 187 * @param h_payto hash of that account 188 * @param exchange_payto_uri account of the exchange 189 * @param exec_time when the transfer was made 190 * @param h_contract_terms contract that was paid 191 * @param denom_pub denomination of the coin 192 * @param coin_pub the coin that was deposited 193 * @param coin_value how much the coin contributed 194 * @param coin_fee how much the exchange charged for it 195 */ 196 static void 197 transfer_cb (void *cls, 198 uint64_t rowid, 199 const struct TALER_MerchantPublicKeyP *merchant_pub, 200 const struct TALER_FullPayto account_payto_uri, 201 const struct TALER_FullPaytoHashP *h_payto, 202 const struct TALER_FullPayto exchange_payto_uri, 203 struct GNUNET_TIME_Timestamp exec_time, 204 const struct TALER_PrivateContractHashP *h_contract_terms, 205 const struct TALER_DenominationPublicKey *denom_pub, 206 const struct TALER_CoinSpendPublicKeyP *coin_pub, 207 const struct TALER_Amount *coin_value, 208 const struct TALER_Amount *coin_fee) 209 { 210 struct SumContext *ctx = cls; 211 212 (void) merchant_pub; 213 (void) account_payto_uri; 214 (void) h_payto; 215 (void) exchange_payto_uri; 216 (void) exec_time; 217 (void) h_contract_terms; 218 (void) denom_pub; 219 (void) coin_pub; 220 (void) coin_fee; 221 if (0 == ctx->total++) 222 ctx->first_row = rowid; 223 ctx->value_sum += coin_value->value; 224 } 225 226 227 /** 228 * Nothing is reported while the table is empty, and aggregating an 229 * account with no deposits comes out at zero. 230 * 231 * @param pg the database context 232 * @return 0 on success 233 */ 234 static int 235 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg) 236 { 237 struct TALER_MerchantPublicKeyP merchant_pub; 238 struct TALER_WireTransferIdentifierRawP wtid; 239 struct TALER_FullPaytoHashP h_payto; 240 struct TALER_FullPayto payto_uri = { NULL }; 241 struct TALER_Amount total; 242 struct TALER_Amount zero = TDB_amount ("0"); 243 struct TALER_Amount deposited; 244 struct TALER_Amount refunded; 245 struct TALER_Amount fee; 246 struct WtidContext ctx = { 0 }; 247 struct SumContext sctx = { 0 }; 248 249 TDB_denom (pg, 250 10, 251 "5", 252 "0.1", 253 &denom); 254 TDB_account (pg, 255 10, 256 &account); 257 TDB_FILL (merchant_pub, 258 1); 259 TDB_FILL (wtid, 260 1); 261 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 262 TALER_EXCHANGEDB_do_aggregate (pg, 263 &account.h_full, 264 &merchant_pub, 265 &wtid, 266 &total)); 267 FAILIF (0 != TALER_amount_cmp (&total, 268 &zero)); 269 FAILIF (0 != TDB_count (pg, 270 "FROM aggregation_tracking")); 271 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 272 TALER_EXCHANGEDB_get_pending_aggregation (pg, 273 &wtid, 274 &h_payto, 275 &payto_uri, 276 &deposited, 277 &refunded, 278 &fee)); 279 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 280 TALER_EXCHANGEDB_iterate_aggregation_wtids_above_serial_id ( 281 pg, 282 0, 283 &wtid_cb, 284 &ctx)); 285 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 286 TALER_EXCHANGEDB_iterate_aggregations_above_serial_id ( 287 pg, 288 0, 289 &aggregation_cb, 290 &sctx)); 291 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 292 TALER_EXCHANGEDB_iterate_wire_transfers (pg, 293 &wtid, 294 &transfer_cb, 295 &sctx)); 296 FAILIF (0 != ctx.total); 297 FAILIF (0 != sctx.total); 298 return 0; 299 } 300 301 302 /** 303 * Aggregating a deposit records it and nets out the deposit fee. 304 * 305 * @param pg the database context 306 * @return 0 on success 307 */ 308 static int 309 check_aggregate (struct TALER_EXCHANGEDB_PostgresContext *pg) 310 { 311 struct TALER_CoinPublicInfo coin; 312 struct TDB_Deposit dep; 313 struct TALER_WireTransferIdentifierRawP wtid; 314 struct TALER_FullPaytoHashP h_payto; 315 struct TALER_FullPayto payto_uri = { NULL }; 316 struct TALER_Amount total; 317 struct TALER_Amount deposited; 318 struct TALER_Amount refunded; 319 struct TALER_Amount fee; 320 struct TALER_Amount expect_total = TDB_amount ("0.9"); 321 struct TALER_Amount expect_deposited = TDB_amount ("1"); 322 struct TALER_Amount expect_refunded = TDB_amount ("0"); 323 struct TALER_Amount expect_fee = TDB_amount ("0.1"); 324 struct WtidContext ctx; 325 326 TDB_coin (pg, 327 &denom, 328 20, 329 &coin, 330 NULL); 331 TDB_deposit (pg, 332 &account, 333 &coin, 334 20, 335 "1", 336 "0.1", 337 ts (1600000000), 338 ts (1600000000), 339 &dep); 340 TDB_FILL (wtid, 341 20); 342 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 343 TALER_EXCHANGEDB_do_aggregate (pg, 344 &account.h_full, 345 &dep.merchant_pub, 346 &wtid, 347 &total), 348 TDB_coin_free (&coin)); 349 FAILIF_C (0 != TALER_amount_cmp (&total, 350 &expect_total), 351 TDB_coin_free (&coin)); 352 FAILIF_C (1 != TDB_count (pg, 353 "FROM aggregation_tracking"), 354 TDB_coin_free (&coin)); 355 /* the deposit is now marked as done */ 356 FAILIF_C (1 != TDB_count (pg, 357 "FROM batch_deposits WHERE done"), 358 TDB_coin_free (&coin)); 359 360 /* the transfer has not been made yet, so it is pending */ 361 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 362 TALER_EXCHANGEDB_get_pending_aggregation (pg, 363 &wtid, 364 &h_payto, 365 &payto_uri, 366 &deposited, 367 &refunded, 368 &fee), 369 TDB_coin_free (&coin)); 370 FAILIF_C (0 != GNUNET_memcmp (&h_payto, 371 &account.h_full), 372 GNUNET_free (payto_uri.full_payto); TDB_coin_free (&coin)); 373 FAILIF_C (0 != strcmp (payto_uri.full_payto, 374 account.payto.full_payto), 375 GNUNET_free (payto_uri.full_payto); TDB_coin_free (&coin)); 376 GNUNET_free (payto_uri.full_payto); 377 FAILIF_C (0 != TALER_amount_cmp (&deposited, 378 &expect_deposited), 379 TDB_coin_free (&coin)); 380 FAILIF_C (0 != TALER_amount_cmp (&refunded, 381 &expect_refunded), 382 TDB_coin_free (&coin)); 383 FAILIF_C (0 != TALER_amount_cmp (&fee, 384 &expect_fee), 385 TDB_coin_free (&coin)); 386 387 memset (&ctx, 388 0, 389 sizeof (ctx)); 390 ctx.wtid = &wtid; 391 FAILIF_C (0 >= 392 TALER_EXCHANGEDB_iterate_aggregation_wtids_above_serial_id ( 393 pg, 394 0, 395 &wtid_cb, 396 &ctx), 397 TDB_coin_free (&coin)); 398 FAILIF_C (1 != ctx.matched, 399 TDB_coin_free (&coin)); 400 FAILIF_C (! ctx.pending, 401 TDB_coin_free (&coin)); 402 TDB_coin_free (&coin); 403 return 0; 404 } 405 406 407 /** 408 * A deposit that was refunded in full is aggregated at zero and is not 409 * charged a deposit fee. 410 * 411 * @param pg the database context 412 * @return 0 on success 413 */ 414 static int 415 check_full_refund (struct TALER_EXCHANGEDB_PostgresContext *pg) 416 { 417 struct TALER_CoinPublicInfo coin; 418 struct TDB_Deposit dep; 419 struct TALER_EXCHANGEDB_Refund refund; 420 struct TALER_Amount deposit_fee = TDB_amount ("0.1"); 421 struct TALER_WireTransferIdentifierRawP wtid; 422 struct TALER_Amount total; 423 struct TALER_Amount zero = TDB_amount ("0"); 424 bool not_found; 425 bool refund_ok; 426 bool gone; 427 bool conflict; 428 429 TDB_coin (pg, 430 &denom, 431 21, 432 &coin, 433 NULL); 434 TDB_deposit (pg, 435 &account, 436 &coin, 437 21, 438 "1", 439 "0.1", 440 ts (1600000000), 441 ts (1600000000), 442 &dep); 443 memset (&refund, 444 0, 445 sizeof (refund)); 446 refund.coin = coin; 447 refund.details.merchant_pub = dep.merchant_pub; 448 TDB_FILL (refund.details.merchant_sig, 449 21); 450 refund.details.h_contract_terms = dep.h_contract_terms; 451 refund.details.rtransaction_id = 1; 452 refund.details.refund_amount = TDB_amount ("1"); 453 refund.details.refund_fee = TDB_amount ("0"); 454 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 455 TALER_EXCHANGEDB_do_refund (pg, 456 &refund, 457 &deposit_fee, 458 0, 459 ¬_found, 460 &refund_ok, 461 &gone, 462 &conflict), 463 TDB_coin_free (&coin)); 464 FAILIF_C (! refund_ok, 465 TDB_coin_free (&coin)); 466 467 TDB_FILL (wtid, 468 21); 469 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 470 TALER_EXCHANGEDB_do_aggregate (pg, 471 &account.h_full, 472 &dep.merchant_pub, 473 &wtid, 474 &total), 475 TDB_coin_free (&coin)); 476 /* deposit EUR:1 minus refund EUR:1, and no deposit fee on a coin that 477 was refunded in full */ 478 FAILIF_C (0 != TALER_amount_cmp (&total, 479 &zero), 480 TDB_coin_free (&coin)); 481 TDB_coin_free (&coin); 482 return 0; 483 } 484 485 486 /** 487 * Executing the wire transfer takes the aggregation out of "pending". 488 * 489 * @param pg the database context 490 * @return 0 on success 491 */ 492 static int 493 check_executed (struct TALER_EXCHANGEDB_PostgresContext *pg) 494 { 495 struct TALER_WireTransferIdentifierRawP wtid; 496 struct TALER_FullPaytoHashP h_payto; 497 struct TALER_FullPayto payto_uri = { NULL }; 498 struct TALER_FullPayto exchange_payto = { 499 .full_payto = (char *) "payto://x-taler-bank/localhost/exchange" 500 }; 501 struct TALER_Amount amount = TDB_amount ("0.9"); 502 struct TALER_Amount deposited; 503 struct TALER_Amount refunded; 504 struct TALER_Amount fee; 505 struct WtidContext ctx; 506 507 TDB_FILL (wtid, 508 20); 509 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 510 TALER_EXCHANGEDB_insert_wire_out (pg, 511 ts (1600003600), 512 &wtid, 513 &account.h_full, 514 "exchange-account-1", 515 exchange_payto, 516 &amount, 517 NULL)); 518 /* the aggregation is no longer pending... */ 519 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 520 TALER_EXCHANGEDB_get_pending_aggregation (pg, 521 &wtid, 522 &h_payto, 523 &payto_uri, 524 &deposited, 525 &refunded, 526 &fee)); 527 /* ...and the wtid iterator says so too */ 528 memset (&ctx, 529 0, 530 sizeof (ctx)); 531 ctx.wtid = &wtid; 532 FAILIF (0 >= 533 TALER_EXCHANGEDB_iterate_aggregation_wtids_above_serial_id ( 534 pg, 535 0, 536 &wtid_cb, 537 &ctx)); 538 FAILIF (1 != ctx.matched); 539 FAILIF (ctx.pending); 540 return 0; 541 } 542 543 544 /** 545 * The deposit can be traced to the wire transfer it went into, and the 546 * transfer back to the deposits it paid. 547 * 548 * @param pg the database context 549 * @return 0 on success 550 */ 551 static int 552 check_transfer_by_deposit (struct TALER_EXCHANGEDB_PostgresContext *pg) 553 { 554 struct TALER_CoinSpendPublicKeyP coin_pub; 555 struct TDB_Deposit dep; 556 struct TALER_WireTransferIdentifierRawP wtid; 557 struct TALER_WireTransferIdentifierRawP got_wtid; 558 struct GNUNET_TIME_Timestamp exec_time; 559 struct TALER_Amount amount_with_fee; 560 struct TALER_Amount deposit_fee; 561 struct TALER_Amount expect_amount = TDB_amount ("1"); 562 struct TALER_EXCHANGEDB_KycStatus kyc; 563 union TALER_AccountPublicKeyP account_pub; 564 struct SumContext sctx; 565 bool pending = true; 566 567 /* the deposit of check_aggregate() */ 568 TDB_FILL (coin_pub, 569 20); 570 TDB_FILL (wtid, 571 20); 572 memset (&dep, 573 0, 574 sizeof (dep)); 575 TDB_FILL (dep.merchant_pub, 576 20); 577 TDB_FILL (dep.h_contract_terms, 578 20); 579 TDB_FILL (dep.wire_salt, 580 20); 581 TALER_merchant_wire_signature_hash (account.payto, 582 &dep.wire_salt, 583 &dep.h_wire); 584 memset (&kyc, 585 0, 586 sizeof (kyc)); 587 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 588 TALER_EXCHANGEDB_get_transfer_by_deposit (pg, 589 &dep.h_contract_terms, 590 &dep.h_wire, 591 &coin_pub, 592 &dep.merchant_pub, 593 &pending, 594 &got_wtid, 595 &exec_time, 596 &amount_with_fee, 597 &deposit_fee, 598 &kyc, 599 &account_pub)); 600 FAILIF (pending); 601 FAILIF (0 != GNUNET_memcmp (&got_wtid, 602 &wtid)); 603 FAILIF (0 != TALER_amount_cmp (&amount_with_fee, 604 &expect_amount)); 605 606 /* a contract nobody deposited is not found */ 607 { 608 struct TALER_PrivateContractHashP other; 609 610 TDB_FILL (other, 611 97); 612 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 613 TALER_EXCHANGEDB_get_transfer_by_deposit (pg, 614 &other, 615 &dep.h_wire, 616 &coin_pub, 617 &dep.merchant_pub, 618 &pending, 619 &got_wtid, 620 &exec_time, 621 &amount_with_fee, 622 &deposit_fee, 623 &kyc, 624 &account_pub)); 625 } 626 627 /* and the transfer lists the coins it paid for */ 628 memset (&sctx, 629 0, 630 sizeof (sctx)); 631 FAILIF (0 >= 632 TALER_EXCHANGEDB_iterate_wire_transfers (pg, 633 &wtid, 634 &transfer_cb, 635 &sctx)); 636 FAILIF (1 != sctx.total); 637 FAILIF (1 != sctx.value_sum); 638 { 639 struct TALER_WireTransferIdentifierRawP other; 640 641 TDB_FILL (other, 642 96); 643 memset (&sctx, 644 0, 645 sizeof (sctx)); 646 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 647 TALER_EXCHANGEDB_iterate_wire_transfers (pg, 648 &other, 649 &transfer_cb, 650 &sctx)); 651 FAILIF (0 != sctx.total); 652 } 653 return 0; 654 } 655 656 657 /** 658 * The aggregation iterator walks the table by serial. 659 * 660 * @param pg the database context 661 * @return 0 on success 662 */ 663 static int 664 check_iterate_aggregations (struct TALER_EXCHANGEDB_PostgresContext *pg) 665 { 666 struct SumContext ctx; 667 668 memset (&ctx, 669 0, 670 sizeof (ctx)); 671 FAILIF (2 != 672 TALER_EXCHANGEDB_iterate_aggregations_above_serial_id ( 673 pg, 674 0, 675 &aggregation_cb, 676 &ctx)); 677 FAILIF (2 != ctx.total); 678 memset (&ctx, 679 0, 680 sizeof (ctx)); 681 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 682 TALER_EXCHANGEDB_iterate_aggregations_above_serial_id ( 683 pg, 684 1000, 685 &aggregation_cb, 686 &ctx)); 687 FAILIF (0 != 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 { "aggregation-tracking-empty", 697 &check_empty }, 698 { "aggregation-tracking-aggregate", 699 &check_aggregate }, 700 { "aggregation-tracking-full-refund", 701 &check_full_refund }, 702 { "aggregation-tracking-executed", 703 &check_executed }, 704 { "aggregation-tracking-transfer-by-deposit", 705 &check_transfer_by_deposit }, 706 { "aggregation-tracking-iterate-aggregations", 707 &check_iterate_aggregations }, 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-aggregation-tracking", 721 "Tests for the exchangedb `aggregation_tracking' table", 722 tests); 723 TDB_account_free (&account); 724 TDB_denom_free (&denom); 725 return ret; 726 } 727 728 729 /* end of test_aggregation_tracking.c */