test_batch_deposits.c (20124B)
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_batch_deposits.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `batch_deposits` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_do_deposit(), 23 * #TALER_EXCHANGEDB_do_check_deposit_idempotent(), 24 * #TALER_EXCHANGEDB_get_exists_deposit(), 25 * #TALER_EXCHANGEDB_get_ready_deposit(), 26 * #TALER_EXCHANGEDB_iterate_batch_deposits_missing_wire() and 27 * #TALER_EXCHANGEDB_get_wire_hash_for_contract(). 28 * 29 * `batch_deposits` references `wire_targets` and, through `coin_deposits`, 30 * `known_coins`. A deposit is "ready" only once its wire deadline has 31 * passed *and* its refund deadline is behind the aggregation shift 32 * boundary, so the checks use deadlines in the past for that. 33 */ 34 #include "test_common.h" 35 #include "exchange-database/do_check_deposit_idempotent.h" 36 #include "exchange-database/do_deposit.h" 37 #include "exchange-database/get_exists_deposit.h" 38 #include "exchange-database/get_ready_deposit.h" 39 #include "exchange-database/get_wire_hash_for_contract.h" 40 #include "exchange-database/iterate_batch_deposits_missing_wire.h" 41 #include "exchange-database/rollback.h" 42 #include "exchange-database/start.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 #missing_cb(). 76 */ 77 struct MissingContext 78 { 79 /** 80 * How many deposits did the callback see? 81 */ 82 unsigned int total; 83 84 /** 85 * Row of the first deposit seen. 86 */ 87 uint64_t first_row; 88 89 /** 90 * Sum of the whole-unit parts of the amounts seen. 91 */ 92 uint64_t value_sum; 93 94 /** 95 * Account of the last deposit seen. 96 */ 97 struct TALER_FullPaytoHashP h_payto; 98 }; 99 100 101 /** 102 * Callback for #TALER_EXCHANGEDB_iterate_batch_deposits_missing_wire(). 103 * 104 * @param cls a `struct MissingContext *` 105 * @param batch_deposit_serial_id row of the deposit 106 * @param total_amount how much is owed 107 * @param wire_target_h_payto where it is owed 108 * @param deadline when it was due 109 */ 110 static void 111 missing_cb (void *cls, 112 uint64_t batch_deposit_serial_id, 113 const struct TALER_Amount *total_amount, 114 const struct TALER_FullPaytoHashP *wire_target_h_payto, 115 struct GNUNET_TIME_Timestamp deadline) 116 { 117 struct MissingContext *ctx = cls; 118 119 (void) deadline; 120 if (0 == ctx->total++) 121 ctx->first_row = batch_deposit_serial_id; 122 ctx->value_sum += total_amount->value; 123 ctx->h_payto = *wire_target_h_payto; 124 } 125 126 127 /** 128 * Nothing is known while the table is empty. 129 * 130 * @param pg the database context 131 * @return 0 on success 132 */ 133 static int 134 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg) 135 { 136 struct TALER_MerchantPublicKeyP merchant_pub; 137 struct TALER_PrivateContractHashP h_contract_terms; 138 struct TALER_MerchantWireHashP h_wire; 139 struct TALER_CoinSpendPublicKeyP coin_pub; 140 struct TALER_Amount deposit_fee; 141 struct GNUNET_TIME_Timestamp exchange_timestamp; 142 struct TALER_FullPayto payto_uri = { NULL }; 143 struct MissingContext ctx = { 0 }; 144 char *metadata = NULL; 145 146 TDB_FILL (merchant_pub, 147 1); 148 TDB_FILL (h_contract_terms, 149 1); 150 TDB_FILL (h_wire, 151 1); 152 TDB_FILL (coin_pub, 153 1); 154 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 155 TALER_EXCHANGEDB_get_wire_hash_for_contract (pg, 156 &merchant_pub, 157 &h_contract_terms, 158 &h_wire)); 159 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 160 TALER_EXCHANGEDB_get_exists_deposit (pg, 161 &h_contract_terms, 162 &h_wire, 163 &coin_pub, 164 &merchant_pub, 165 ts (1600000000), 166 &deposit_fee, 167 &exchange_timestamp)); 168 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 169 TALER_EXCHANGEDB_get_ready_deposit (pg, 170 0, 171 UINT32_MAX, 172 &merchant_pub, 173 &payto_uri, 174 &metadata)); 175 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 176 TALER_EXCHANGEDB_iterate_batch_deposits_missing_wire (pg, 177 0, 178 &missing_cb, 179 &ctx)); 180 FAILIF (0 != ctx.total); 181 return 0; 182 } 183 184 185 /** 186 * A deposit of more than the coin holds is refused. 187 * 188 * @param pg the database context 189 * @return 0 on success 190 */ 191 static int 192 check_insufficient_balance (struct TALER_EXCHANGEDB_PostgresContext *pg) 193 { 194 struct TALER_CoinPublicInfo coin; 195 struct TALER_EXCHANGEDB_CoinDepositInformation cdi; 196 struct TALER_EXCHANGEDB_BatchDeposit bd; 197 struct TALER_Amount fees[1]; 198 struct TALER_Amount total; 199 struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); 200 uint32_t bad_balance_index = 42; 201 bool balance_ok = true; 202 bool in_conflict = true; 203 204 TDB_denom (pg, 205 10, 206 "5", 207 "0.1", 208 &denom); 209 TDB_account (pg, 210 10, 211 &account); 212 TDB_coin (pg, 213 &denom, 214 20, 215 &coin, 216 NULL); 217 memset (&cdi, 218 0, 219 sizeof (cdi)); 220 cdi.coin = coin; 221 TDB_FILL (cdi.csig, 222 20); 223 cdi.amount_with_fee = TDB_amount ("6"); 224 fees[0] = TDB_amount ("0.1"); 225 memset (&bd, 226 0, 227 sizeof (bd)); 228 TDB_FILL (bd.merchant_pub, 229 20); 230 TDB_FILL (bd.merchant_sig, 231 20); 232 TDB_FILL (bd.h_contract_terms, 233 20); 234 bd.no_wallet_data_hash = true; 235 bd.wire_target_h_payto = account.h_full; 236 TDB_FILL (bd.wire_salt, 237 20); 238 bd.wallet_timestamp = now; 239 bd.refund_deadline = now; 240 bd.wire_deadline = now; 241 bd.receiver_wire_account = account.payto; 242 bd.cdis = &cdi; 243 bd.num_cdis = 1; 244 /* The batch row goes in before the per-coin balance is checked, so this 245 is how the /deposit handler uses the function: inside a transaction it 246 can roll back. */ 247 FAILIF_C (GNUNET_OK != 248 TALER_EXCHANGEDB_start (pg, 249 "test-deposit-insufficient"), 250 TDB_coin_free (&coin)); 251 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 252 TALER_EXCHANGEDB_do_deposit (pg, 253 &bd, 254 fees, 255 &now, 256 &total, 257 &balance_ok, 258 &bad_balance_index, 259 &in_conflict), 260 TALER_EXCHANGEDB_rollback (pg); TDB_coin_free (&coin)); 261 TALER_EXCHANGEDB_rollback (pg); 262 TDB_coin_free (&coin); 263 FAILIF (balance_ok); 264 FAILIF (in_conflict); 265 FAILIF (0 != bad_balance_index); 266 FAILIF (0 != TDB_count (pg, 267 "FROM batch_deposits")); 268 return 0; 269 } 270 271 272 /** 273 * A deposit within the coin's balance is recorded and can be found again. 274 * 275 * @param pg the database context 276 * @return 0 on success 277 */ 278 static int 279 check_deposit (struct TALER_EXCHANGEDB_PostgresContext *pg) 280 { 281 struct TALER_CoinPublicInfo coin; 282 struct TDB_Deposit dep; 283 struct TALER_MerchantWireHashP h_wire; 284 struct TALER_Amount deposit_fee; 285 struct TALER_Amount expect_fee = TDB_amount ("0.1"); 286 struct GNUNET_TIME_Timestamp exchange_timestamp; 287 288 TDB_coin (pg, 289 &denom, 290 21, 291 &coin, 292 NULL); 293 TDB_deposit (pg, 294 &account, 295 &coin, 296 21, 297 "1", 298 "0.1", 299 ts (1600000000), 300 ts (1600000000), 301 &dep); 302 FAILIF_C (1 != TDB_count (pg, 303 "FROM batch_deposits"), 304 TDB_coin_free (&coin)); 305 FAILIF_C (1 != TDB_count (pg, 306 "FROM coin_deposits"), 307 TDB_coin_free (&coin)); 308 309 /* the salted account hash comes back from the contract */ 310 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 311 TALER_EXCHANGEDB_get_wire_hash_for_contract (pg, 312 &dep.merchant_pub, 313 &dep.h_contract_terms, 314 &h_wire), 315 TDB_coin_free (&coin)); 316 FAILIF_C (0 != GNUNET_memcmp (&h_wire, 317 &dep.h_wire), 318 TDB_coin_free (&coin)); 319 320 /* ...and the deposit itself, with its fee */ 321 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 322 TALER_EXCHANGEDB_get_exists_deposit (pg, 323 &dep.h_contract_terms, 324 &dep.h_wire, 325 &coin.coin_pub, 326 &dep.merchant_pub, 327 dep.refund_deadline, 328 &deposit_fee, 329 &exchange_timestamp), 330 TDB_coin_free (&coin)); 331 FAILIF_C (0 != TALER_amount_cmp (&deposit_fee, 332 &expect_fee), 333 TDB_coin_free (&coin)); 334 335 /* a different refund deadline makes it a different deposit */ 336 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 337 TALER_EXCHANGEDB_get_exists_deposit (pg, 338 &dep.h_contract_terms, 339 &dep.h_wire, 340 &coin.coin_pub, 341 &dep.merchant_pub, 342 ts (1700000000), 343 &deposit_fee, 344 &exchange_timestamp), 345 TDB_coin_free (&coin)); 346 /* ...and so does a different account hash */ 347 { 348 struct TALER_MerchantWireHashP other; 349 350 TDB_FILL (other, 351 99); 352 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 353 TALER_EXCHANGEDB_get_exists_deposit (pg, 354 &dep.h_contract_terms, 355 &other, 356 &coin.coin_pub, 357 &dep.merchant_pub, 358 dep.refund_deadline, 359 &deposit_fee, 360 &exchange_timestamp), 361 TDB_coin_free (&coin)); 362 } 363 TDB_coin_free (&coin); 364 return 0; 365 } 366 367 368 /** 369 * A replayed deposit is recognised as idempotent; a different one is not. 370 * 371 * @param pg the database context 372 * @return 0 on success 373 */ 374 static int 375 check_idempotent (struct TALER_EXCHANGEDB_PostgresContext *pg) 376 { 377 struct TALER_CoinPublicInfo coin; 378 struct TALER_EXCHANGEDB_CoinDepositInformation cdi; 379 struct TALER_EXCHANGEDB_BatchDeposit bd; 380 struct GNUNET_TIME_Timestamp exchange_timestamp; 381 bool is_idempotent = false; 382 383 TDB_coin (pg, 384 &denom, 385 21, 386 &coin, 387 NULL); 388 memset (&cdi, 389 0, 390 sizeof (cdi)); 391 cdi.coin = coin; 392 TDB_FILL (cdi.csig, 393 21); 394 cdi.amount_with_fee = TDB_amount ("1"); 395 memset (&bd, 396 0, 397 sizeof (bd)); 398 TDB_FILL (bd.merchant_pub, 399 21); 400 TDB_FILL (bd.merchant_sig, 401 21); 402 TDB_FILL (bd.h_contract_terms, 403 21); 404 bd.no_wallet_data_hash = true; 405 bd.wire_target_h_payto = account.h_full; 406 TDB_FILL (bd.wire_salt, 407 21); 408 bd.wallet_timestamp = ts (1600000000); 409 bd.refund_deadline = ts (1600000000); 410 bd.wire_deadline = ts (1600000000); 411 bd.receiver_wire_account = account.payto; 412 bd.cdis = &cdi; 413 bd.num_cdis = 1; 414 exchange_timestamp = GNUNET_TIME_timestamp_get (); 415 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 416 TALER_EXCHANGEDB_do_check_deposit_idempotent ( 417 pg, 418 &bd, 419 &exchange_timestamp, 420 &is_idempotent), 421 TDB_coin_free (&coin)); 422 FAILIF_C (! is_idempotent, 423 TDB_coin_free (&coin)); 424 425 /* a different amount for the same contract is not the same request */ 426 cdi.amount_with_fee = TDB_amount ("2"); 427 is_idempotent = true; 428 FAILIF_C (0 > 429 TALER_EXCHANGEDB_do_check_deposit_idempotent ( 430 pg, 431 &bd, 432 &exchange_timestamp, 433 &is_idempotent), 434 TDB_coin_free (&coin)); 435 FAILIF_C (is_idempotent, 436 TDB_coin_free (&coin)); 437 438 /* neither is a contract we never saw */ 439 cdi.amount_with_fee = TDB_amount ("1"); 440 TDB_FILL (bd.h_contract_terms, 441 98); 442 is_idempotent = true; 443 FAILIF_C (0 > 444 TALER_EXCHANGEDB_do_check_deposit_idempotent ( 445 pg, 446 &bd, 447 &exchange_timestamp, 448 &is_idempotent), 449 TDB_coin_free (&coin)); 450 FAILIF_C (is_idempotent, 451 TDB_coin_free (&coin)); 452 TDB_coin_free (&coin); 453 return 0; 454 } 455 456 457 /** 458 * A deposit whose deadlines have passed becomes ready for the aggregator, 459 * and shows up as missing a wire transfer. 460 * 461 * @param pg the database context 462 * @return 0 on success 463 */ 464 static int 465 check_ready (struct TALER_EXCHANGEDB_PostgresContext *pg) 466 { 467 struct TALER_MerchantPublicKeyP merchant_pub; 468 struct TALER_FullPayto payto_uri = { NULL }; 469 struct MissingContext ctx; 470 char *metadata = NULL; 471 472 /* the deposit from check_deposit() has deadlines in 2020 */ 473 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 474 TALER_EXCHANGEDB_get_ready_deposit (pg, 475 0, 476 UINT32_MAX, 477 &merchant_pub, 478 &payto_uri, 479 &metadata)); 480 FAILIF_C (0 != strcmp (payto_uri.full_payto, 481 account.payto.full_payto), 482 GNUNET_free (payto_uri.full_payto); GNUNET_free (metadata)); 483 GNUNET_free (payto_uri.full_payto); 484 GNUNET_free (metadata); 485 /* no extra metadata was set */ 486 FAILIF (NULL != metadata); 487 488 /* a shard range the merchant is not in has nothing ready */ 489 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 490 TALER_EXCHANGEDB_get_ready_deposit (pg, 491 0, 492 0, 493 &merchant_pub, 494 &payto_uri, 495 &metadata)); 496 497 memset (&ctx, 498 0, 499 sizeof (ctx)); 500 FAILIF (0 >= 501 TALER_EXCHANGEDB_iterate_batch_deposits_missing_wire (pg, 502 0, 503 &missing_cb, 504 &ctx)); 505 FAILIF (1 != ctx.total); 506 FAILIF (0 != GNUNET_memcmp (&ctx.h_payto, 507 &account.h_full)); 508 /* the amount reported is the gross deposit, fee included */ 509 FAILIF (1 != ctx.value_sum); 510 511 /* the serial bound excludes it again */ 512 memset (&ctx, 513 0, 514 sizeof (ctx)); 515 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 516 TALER_EXCHANGEDB_iterate_batch_deposits_missing_wire (pg, 517 1000, 518 &missing_cb, 519 &ctx)); 520 FAILIF (0 != ctx.total); 521 return 0; 522 } 523 524 525 /** 526 * A deposit that is done or policy-blocked is not ready. 527 * 528 * @param pg the database context 529 * @return 0 on success 530 */ 531 static int 532 check_not_ready (struct TALER_EXCHANGEDB_PostgresContext *pg) 533 { 534 struct TALER_MerchantPublicKeyP merchant_pub; 535 struct TALER_FullPayto payto_uri = { NULL }; 536 char *metadata = NULL; 537 538 FAILIF (GNUNET_OK != 539 TDB_exec (pg, 540 "UPDATE batch_deposits SET done=TRUE;")); 541 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 542 TALER_EXCHANGEDB_get_ready_deposit (pg, 543 0, 544 UINT32_MAX, 545 &merchant_pub, 546 &payto_uri, 547 &metadata)); 548 FAILIF (GNUNET_OK != 549 TDB_exec (pg, 550 "UPDATE batch_deposits" 551 " SET done=FALSE, policy_blocked=TRUE;")); 552 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 553 TALER_EXCHANGEDB_get_ready_deposit (pg, 554 0, 555 UINT32_MAX, 556 &merchant_pub, 557 &payto_uri, 558 &metadata)); 559 FAILIF (GNUNET_OK != 560 TDB_exec (pg, 561 "UPDATE batch_deposits SET policy_blocked=FALSE;")); 562 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 563 TALER_EXCHANGEDB_get_ready_deposit (pg, 564 0, 565 UINT32_MAX, 566 &merchant_pub, 567 &payto_uri, 568 &metadata)); 569 GNUNET_free (payto_uri.full_payto); 570 GNUNET_free (metadata); 571 return 0; 572 } 573 574 575 /** 576 * The checks to run, in order. 577 */ 578 static const struct TDB_Test tests[] = { 579 { "batch-deposits-empty", 580 &check_empty }, 581 { "batch-deposits-insufficient-balance", 582 &check_insufficient_balance }, 583 { "batch-deposits-deposit", 584 &check_deposit }, 585 { "batch-deposits-idempotent", 586 &check_idempotent }, 587 { "batch-deposits-ready", 588 &check_ready }, 589 { "batch-deposits-not-ready", 590 &check_not_ready }, 591 { NULL, NULL } 592 }; 593 594 595 int 596 main (int argc, 597 char *const *argv) 598 { 599 int ret; 600 601 ret = TDB_main (argc, 602 argv, 603 "test-batch-deposits", 604 "Tests for the exchangedb `batch_deposits' table", 605 tests); 606 TDB_account_free (&account); 607 TDB_denom_free (&denom); 608 return ret; 609 } 610 611 612 /* end of test_batch_deposits.c */