test_reserves_open_requests.c (19331B)
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_reserves_open_requests.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `reserves_open_requests` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_do_reserve_open() and 23 * #TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id(). 24 * 25 * The table references `reserves`, which TDB_reserve() creates. The 26 * interesting cases of do_reserve_open() are the ones where it declines to 27 * do the work: an unknown reserve, a payment that does not cover the cost, 28 * a reserve that cannot pay its share, and a request that asks for nothing 29 * that is not already granted. Only the success path writes the table. 30 */ 31 #include "test_common.h" 32 #include "exchange-database/do_reserve_open.h" 33 #include "exchange-database/get_reserve.h" 34 #include "exchange-database/iterate_reserve_open_above_serial_id.h" 35 36 37 /** 38 * A year in microseconds, as exchange_do_reserve_open() counts it. 39 */ 40 #define YEAR_US 31536000000000LLU 41 42 43 /** 44 * Closure for #open_cb(). 45 */ 46 struct OpenContext 47 { 48 /** 49 * How many rows did the callback see? 50 */ 51 unsigned int total; 52 53 /** 54 * Stop after this many rows; 0 for no limit. 55 */ 56 unsigned int stop_after; 57 58 /** 59 * Reserve we are looking for, NULL to match nothing. 60 */ 61 const struct TALER_ReservePublicKeyP *reserve_pub; 62 63 /** 64 * How many times did we see @e reserve_pub? 65 */ 66 unsigned int matched; 67 68 /** 69 * Reserve payment reported for @e reserve_pub. 70 */ 71 struct TALER_Amount reserve_payment; 72 73 /** 74 * Purse limit reported for @e reserve_pub. 75 */ 76 uint32_t purse_limit; 77 78 /** 79 * Signature reported for @e reserve_pub. 80 */ 81 struct TALER_ReserveSignatureP reserve_sig; 82 83 /** 84 * Expiration reported for @e reserve_pub. 85 */ 86 struct GNUNET_TIME_Timestamp expiration; 87 88 /** 89 * Request timestamp reported for @e reserve_pub. 90 */ 91 struct GNUNET_TIME_Timestamp request_timestamp; 92 }; 93 94 95 /** 96 * Callback for #TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id(). 97 * 98 * @param cls a `struct OpenContext *` 99 * @param rowid row of the request 100 * @param reserve_payment how much the reserve itself paid 101 * @param request_timestamp when the request was made 102 * @param reserve_expiration until when the reserve is now open 103 * @param purse_limit how many purses were asked for 104 * @param reserve_pub the reserve in question 105 * @param reserve_sig signature affirming the request 106 * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop 107 */ 108 static enum GNUNET_GenericReturnValue 109 open_cb (void *cls, 110 uint64_t rowid, 111 const struct TALER_Amount *reserve_payment, 112 struct GNUNET_TIME_Timestamp request_timestamp, 113 struct GNUNET_TIME_Timestamp reserve_expiration, 114 uint32_t purse_limit, 115 const struct TALER_ReservePublicKeyP *reserve_pub, 116 const struct TALER_ReserveSignatureP *reserve_sig) 117 { 118 struct OpenContext *ctx = cls; 119 120 (void) rowid; 121 ctx->total++; 122 if ( (NULL != ctx->reserve_pub) && 123 (0 == GNUNET_memcmp (reserve_pub, 124 ctx->reserve_pub)) ) 125 { 126 ctx->matched++; 127 ctx->reserve_payment = *reserve_payment; 128 ctx->purse_limit = purse_limit; 129 ctx->reserve_sig = *reserve_sig; 130 ctx->expiration = reserve_expiration; 131 ctx->request_timestamp = request_timestamp; 132 } 133 if ( (0 != ctx->stop_after) && 134 (ctx->total >= ctx->stop_after) ) 135 return GNUNET_SYSERR; 136 return GNUNET_OK; 137 } 138 139 140 /** 141 * Ask to keep a reserve open. 142 * 143 * @param pg the database context 144 * @param reserve_pub reserve to open 145 * @param seed seed for the reserve signature 146 * @param total_paid total the client is paying, e.g. "1" 147 * @param reserve_payment share of that taken from the reserve, e.g. "1" 148 * @param purse_limit how many purses the client wants 149 * @param desired_expiration until when the client wants the reserve open 150 * @param now when the client made the request 151 * @param open_fee annual fee, e.g. "1" 152 * @param[out] no_funds set if the reserve cannot pay its share 153 * @param[out] open_cost set to what the operation costs 154 * @param[out] final_expiration set to the expiration that was granted 155 * @return transaction status 156 */ 157 static enum GNUNET_DB_QueryStatus 158 open_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg, 159 const struct TALER_ReservePublicKeyP *reserve_pub, 160 uint32_t seed, 161 const char *total_paid, 162 const char *reserve_payment, 163 uint32_t purse_limit, 164 struct GNUNET_TIME_Timestamp desired_expiration, 165 struct GNUNET_TIME_Timestamp now, 166 const char *open_fee, 167 bool *no_funds, 168 struct TALER_Amount *open_cost, 169 struct GNUNET_TIME_Timestamp *final_expiration) 170 { 171 struct TALER_ReserveSignatureP reserve_sig; 172 struct TALER_Amount paid = TDB_amount (total_paid); 173 struct TALER_Amount payment = TDB_amount (reserve_payment); 174 struct TALER_Amount fee = TDB_amount (open_fee); 175 struct TALER_Amount balance; 176 177 TDB_fill (&reserve_sig, 178 sizeof (reserve_sig), 179 seed); 180 return TALER_EXCHANGEDB_do_reserve_open (pg, 181 reserve_pub, 182 &paid, 183 &payment, 184 purse_limit, 185 &reserve_sig, 186 desired_expiration, 187 now, 188 &fee, 189 no_funds, 190 &balance, 191 open_cost, 192 final_expiration); 193 } 194 195 196 /** 197 * Build a timestamp from a number of seconds since the epoch. 198 * 199 * @param secs seconds since the epoch 200 * @return the timestamp 201 */ 202 static struct GNUNET_TIME_Timestamp 203 ts (uint64_t secs) 204 { 205 struct GNUNET_TIME_Absolute abs = { 206 .abs_value_us = secs * 1000LLU * 1000LLU 207 }; 208 209 return GNUNET_TIME_absolute_to_timestamp (abs); 210 } 211 212 213 /** 214 * A reserve that does not exist cannot be opened, and nothing is on file. 215 * 216 * @param pg the database context 217 * @return 0 on success 218 */ 219 static int 220 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg) 221 { 222 struct TALER_ReservePublicKeyP reserve_pub; 223 struct TALER_Amount open_cost; 224 struct GNUNET_TIME_Timestamp final_expiration; 225 struct OpenContext ctx = { 0 }; 226 bool no_funds = false; 227 228 TDB_FILL (reserve_pub, 229 1); 230 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 231 open_reserve (pg, 232 &reserve_pub, 233 1, 234 "1", 235 "1", 236 1, 237 ts (2000000000), 238 ts (1600000000), 239 "1", 240 &no_funds, 241 &open_cost, 242 &final_expiration)); 243 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 244 TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id ( 245 pg, 246 0, 247 &open_cb, 248 &ctx)); 249 FAILIF (0 != ctx.total); 250 FAILIF (0 != TDB_count (pg, 251 "FROM reserves_open_requests")); 252 return 0; 253 } 254 255 256 /** 257 * Opening a reserve charges it, extends it and records the request. 258 * 259 * @param pg the database context 260 * @return 0 on success 261 */ 262 static int 263 check_open (struct TALER_EXCHANGEDB_PostgresContext *pg) 264 { 265 struct TALER_ReservePublicKeyP reserve_pub; 266 struct TALER_EXCHANGEDB_Reserve reserve; 267 struct TALER_Amount open_cost; 268 struct TALER_Amount expect_cost = TDB_amount ("1"); 269 struct TALER_Amount expect_balance = TDB_amount ("9"); 270 struct TALER_ReserveSignatureP expect_sig; 271 struct GNUNET_TIME_Timestamp final_expiration; 272 struct GNUNET_TIME_Timestamp now = ts (1600000000); 273 struct OpenContext ctx; 274 bool no_funds = true; 275 276 TDB_reserve (pg, 277 10, 278 "10", 279 &reserve_pub); 280 /* TDB_reserve() gives the reserve an expiration far in the future, which 281 would leave nothing for this call to do; pull it back to `now'. */ 282 { 283 char *hex = TDB_hex (&reserve_pub, 284 sizeof (reserve_pub)); 285 286 FAILIF_C (GNUNET_OK != 287 TDB_exec (pg, 288 "UPDATE reserves" 289 " SET expiration_date=%llu" 290 " WHERE reserve_pub=decode('%s','hex');", 291 (unsigned long long) now.abs_time.abs_value_us, 292 hex), 293 GNUNET_free (hex)); 294 GNUNET_free (hex); 295 } 296 297 /* one year of extension at an annual fee of EUR:1 */ 298 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 299 open_reserve (pg, 300 &reserve_pub, 301 10, 302 "1", 303 "1", 304 1, 305 ts (1600000000 + YEAR_US / 1000000), 306 now, 307 "1", 308 &no_funds, 309 &open_cost, 310 &final_expiration)); 311 FAILIF (no_funds); 312 FAILIF (0 != TALER_amount_cmp (&open_cost, 313 &expect_cost)); 314 FAILIF (final_expiration.abs_time.abs_value_us != 315 now.abs_time.abs_value_us + YEAR_US); 316 317 /* the reserve paid for it */ 318 memset (&reserve, 319 0, 320 sizeof (reserve)); 321 reserve.pub = reserve_pub; 322 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 323 TALER_EXCHANGEDB_get_reserve (pg, 324 &reserve)); 325 FAILIF (0 != TALER_amount_cmp (&reserve.balance, 326 &expect_balance)); 327 FAILIF (GNUNET_TIME_timestamp_cmp (reserve.expiry, 328 !=, 329 final_expiration)); 330 331 /* ...and the request itself was recorded */ 332 TDB_FILL (expect_sig, 333 10); 334 memset (&ctx, 335 0, 336 sizeof (ctx)); 337 ctx.reserve_pub = &reserve_pub; 338 FAILIF (0 >= 339 TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id ( 340 pg, 341 0, 342 &open_cb, 343 &ctx)); 344 FAILIF (1 != ctx.matched); 345 FAILIF (0 != GNUNET_memcmp (&ctx.reserve_sig, 346 &expect_sig)); 347 FAILIF (0 != TALER_amount_cmp (&ctx.reserve_payment, 348 &expect_cost)); 349 FAILIF (1 != ctx.purse_limit); 350 FAILIF (GNUNET_TIME_timestamp_cmp (ctx.expiration, 351 !=, 352 final_expiration)); 353 FAILIF (GNUNET_TIME_timestamp_cmp (ctx.request_timestamp, 354 !=, 355 now)); 356 357 /* a retry of the very same request is absorbed */ 358 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 359 open_reserve (pg, 360 &reserve_pub, 361 10, 362 "1", 363 "1", 364 1, 365 ts (1600000000 + YEAR_US / 1000000), 366 now, 367 "1", 368 &no_funds, 369 &open_cost, 370 &final_expiration)); 371 FAILIF (1 != TDB_count (pg, 372 "FROM reserves_open_requests")); 373 return 0; 374 } 375 376 377 /** 378 * A request that asks for nothing beyond what the reserve already has 379 * costs nothing and is not recorded. 380 * 381 * @param pg the database context 382 * @return 0 on success 383 */ 384 static int 385 check_nothing_to_do (struct TALER_EXCHANGEDB_PostgresContext *pg) 386 { 387 struct TALER_ReservePublicKeyP reserve_pub; 388 struct TALER_Amount open_cost; 389 struct TALER_Amount zero = TDB_amount ("0"); 390 struct GNUNET_TIME_Timestamp final_expiration; 391 bool no_funds = true; 392 uint64_t before; 393 394 TDB_reserve (pg, 395 11, 396 "10", 397 &reserve_pub); 398 before = TDB_count (pg, 399 "FROM reserves_open_requests"); 400 /* The reserve already runs to 2096 and no additional purses are asked 401 for, so there is nothing left to grant. Note that asking for even one 402 purse would *not* be a no-op: a fresh reserve has purses_allowed=0. */ 403 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 404 open_reserve (pg, 405 &reserve_pub, 406 11, 407 "1", 408 "1", 409 0, 410 ts (1600000000), 411 ts (1600000000), 412 "1", 413 &no_funds, 414 &open_cost, 415 &final_expiration)); 416 FAILIF (no_funds); 417 FAILIF (0 != TALER_amount_cmp (&open_cost, 418 &zero)); 419 FAILIF (before != TDB_count (pg, 420 "FROM reserves_open_requests")); 421 return 0; 422 } 423 424 425 /** 426 * Too little paid, or too little in the reserve, both decline the work. 427 * 428 * @param pg the database context 429 * @return 0 on success 430 */ 431 static int 432 check_insufficient (struct TALER_EXCHANGEDB_PostgresContext *pg) 433 { 434 struct TALER_ReservePublicKeyP reserve_pub; 435 struct TALER_Amount open_cost; 436 struct TALER_Amount expect_cost = TDB_amount ("2"); 437 struct GNUNET_TIME_Timestamp final_expiration; 438 struct GNUNET_TIME_Timestamp now = ts (1600000000); 439 bool no_funds = false; 440 uint64_t before; 441 442 TDB_reserve (pg, 443 12, 444 "1", 445 &reserve_pub); 446 { 447 char *hex = TDB_hex (&reserve_pub, 448 sizeof (reserve_pub)); 449 450 FAILIF_C (GNUNET_OK != 451 TDB_exec (pg, 452 "UPDATE reserves" 453 " SET expiration_date=%llu" 454 " WHERE reserve_pub=decode('%s','hex');", 455 (unsigned long long) now.abs_time.abs_value_us, 456 hex), 457 GNUNET_free (hex)); 458 GNUNET_free (hex); 459 } 460 before = TDB_count (pg, 461 "FROM reserves_open_requests"); 462 463 /* two years at EUR:1 a year, but only EUR:1 offered */ 464 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 465 open_reserve (pg, 466 &reserve_pub, 467 12, 468 "1", 469 "1", 470 1, 471 ts (1600000000 + 2 * (YEAR_US / 1000000)), 472 now, 473 "1", 474 &no_funds, 475 &open_cost, 476 &final_expiration)); 477 FAILIF (no_funds); 478 FAILIF (0 != TALER_amount_cmp (&open_cost, 479 &expect_cost)); 480 FAILIF (before != TDB_count (pg, 481 "FROM reserves_open_requests")); 482 483 /* enough offered, but the reserve holds only EUR:1 of the EUR:2 */ 484 no_funds = false; 485 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 486 open_reserve (pg, 487 &reserve_pub, 488 12, 489 "2", 490 "2", 491 1, 492 ts (1600000000 + 2 * (YEAR_US / 1000000)), 493 now, 494 "1", 495 &no_funds, 496 &open_cost, 497 &final_expiration)); 498 FAILIF (! no_funds); 499 FAILIF (before != TDB_count (pg, 500 "FROM reserves_open_requests")); 501 return 0; 502 } 503 504 505 /** 506 * The iterator's serial bound and its abort return behave as documented. 507 * 508 * @param pg the database context 509 * @return 0 on success 510 */ 511 static int 512 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg) 513 { 514 struct TALER_ReservePublicKeyP reserve_pub; 515 struct TALER_Amount open_cost; 516 struct GNUNET_TIME_Timestamp final_expiration; 517 struct GNUNET_TIME_Timestamp now = ts (1600000000); 518 struct OpenContext ctx; 519 bool no_funds = true; 520 521 /* a second recorded request, from another reserve */ 522 TDB_reserve (pg, 523 13, 524 "10", 525 &reserve_pub); 526 { 527 char *hex = TDB_hex (&reserve_pub, 528 sizeof (reserve_pub)); 529 530 FAILIF_C (GNUNET_OK != 531 TDB_exec (pg, 532 "UPDATE reserves" 533 " SET expiration_date=%llu" 534 " WHERE reserve_pub=decode('%s','hex');", 535 (unsigned long long) now.abs_time.abs_value_us, 536 hex), 537 GNUNET_free (hex)); 538 GNUNET_free (hex); 539 } 540 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 541 open_reserve (pg, 542 &reserve_pub, 543 13, 544 "1", 545 "1", 546 1, 547 ts (1600000000 + YEAR_US / 1000000), 548 now, 549 "1", 550 &no_funds, 551 &open_cost, 552 &final_expiration)); 553 FAILIF (2 != TDB_count (pg, 554 "FROM reserves_open_requests")); 555 556 memset (&ctx, 557 0, 558 sizeof (ctx)); 559 FAILIF (2 != 560 TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id ( 561 pg, 562 0, 563 &open_cb, 564 &ctx)); 565 memset (&ctx, 566 0, 567 sizeof (ctx)); 568 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 569 TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id ( 570 pg, 571 1000, 572 &open_cb, 573 &ctx)); 574 FAILIF (0 != ctx.total); 575 576 memset (&ctx, 577 0, 578 sizeof (ctx)); 579 ctx.stop_after = 1; 580 FAILIF (2 != 581 TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id ( 582 pg, 583 0, 584 &open_cb, 585 &ctx)); 586 FAILIF (1 != ctx.total); 587 return 0; 588 } 589 590 591 /** 592 * The checks to run, in order. 593 */ 594 static const struct TDB_Test tests[] = { 595 { "reserves-open-requests-empty", 596 &check_empty }, 597 { "reserves-open-requests-open", 598 &check_open }, 599 { "reserves-open-requests-nothing-to-do", 600 &check_nothing_to_do }, 601 { "reserves-open-requests-insufficient", 602 &check_insufficient }, 603 { "reserves-open-requests-iterate", 604 &check_iterate }, 605 { NULL, NULL } 606 }; 607 608 609 int 610 main (int argc, 611 char *const *argv) 612 { 613 return TDB_main (argc, 614 argv, 615 "test-reserves-open-requests", 616 "Tests for the exchangedb `reserves_open_requests' table", 617 tests); 618 } 619 620 621 /* end of test_reserves_open_requests.c */