test_recoup.c (15584B)
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_recoup.c 18 * @brief tests for the exchangedb functions whose primary table is `recoup` 19 * @author Christian Grothoff 20 * 21 * Covers #TALER_EXCHANGEDB_do_recoup() and 22 * #TALER_EXCHANGEDB_iterate_recoups_above_serial_id(). 23 * 24 * `recoup` references `known_coins` and `withdraw`, so a full chain is 25 * built for each check: a funded reserve, a withdraw from it and a coin. 26 * do_recoup() moves the coin's whole remaining balance back to the reserve, 27 * which is why the checks look at both sides afterwards. 28 */ 29 #include "test_common.h" 30 #include "exchange-database/do_recoup.h" 31 #include "exchange-database/get_reserve.h" 32 #include "exchange-database/iterate_recoups_above_serial_id.h" 33 34 35 /** 36 * Account the checks fund their reserves from. 37 */ 38 static struct TDB_Account account; 39 40 41 /** 42 * Denomination the checks use. 43 */ 44 static struct TDB_Denom denom; 45 46 47 /** 48 * Closure for #recoup_cb(). 49 */ 50 struct RecoupContext 51 { 52 /** 53 * How many rows did the callback see? 54 */ 55 unsigned int total; 56 57 /** 58 * Stop after this many rows; 0 for no limit. 59 */ 60 unsigned int stop_after; 61 62 /** 63 * Coin we are looking for, NULL to match nothing. 64 */ 65 const struct TALER_CoinSpendPublicKeyP *coin_pub; 66 67 /** 68 * How many times did we see it? 69 */ 70 unsigned int matched; 71 72 /** 73 * Amount reported for it. 74 */ 75 struct TALER_Amount amount; 76 77 /** 78 * Reserve reported for it. 79 */ 80 struct TALER_ReservePublicKeyP reserve_pub; 81 82 /** 83 * Blinding secret reported for it. 84 */ 85 union GNUNET_CRYPTO_BlindingSecretP coin_blind; 86 }; 87 88 89 /** 90 * Callback for #TALER_EXCHANGEDB_iterate_recoups_above_serial_id(). 91 * 92 * @param cls a `struct RecoupContext *` 93 * @param rowid row of the recoup 94 * @param timestamp when the recoup was requested 95 * @param amount how much went back to the reserve 96 * @param reserve_pub the reserve that was credited 97 * @param coin the coin that was recouped 98 * @param denom_pub denomination of @a coin 99 * @param coin_sig signature over the request 100 * @param coin_blind blinding secret of the coin 101 * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop 102 */ 103 static enum GNUNET_GenericReturnValue 104 recoup_cb (void *cls, 105 uint64_t rowid, 106 struct GNUNET_TIME_Timestamp timestamp, 107 const struct TALER_Amount *amount, 108 const struct TALER_ReservePublicKeyP *reserve_pub, 109 const struct TALER_CoinPublicInfo *coin, 110 const struct TALER_DenominationPublicKey *denom_pub, 111 const struct TALER_CoinSpendSignatureP *coin_sig, 112 const union GNUNET_CRYPTO_BlindingSecretP *coin_blind) 113 { 114 struct RecoupContext *ctx = cls; 115 116 (void) rowid; 117 (void) timestamp; 118 (void) denom_pub; 119 (void) coin_sig; 120 ctx->total++; 121 if ( (NULL != ctx->coin_pub) && 122 (0 == GNUNET_memcmp (&coin->coin_pub, 123 ctx->coin_pub)) ) 124 { 125 ctx->matched++; 126 ctx->amount = *amount; 127 ctx->reserve_pub = *reserve_pub; 128 ctx->coin_blind = *coin_blind; 129 } 130 if ( (0 != ctx->stop_after) && 131 (ctx->total >= ctx->stop_after) ) 132 return GNUNET_SYSERR; 133 return GNUNET_OK; 134 } 135 136 137 /** 138 * Outcome of a recoup request. 139 */ 140 struct RecoupStatus 141 { 142 /** 143 * Was the recoup accepted? 144 */ 145 bool recoup_ok; 146 147 /** 148 * Did something go wrong inside the database? 149 */ 150 bool internal_failure; 151 152 /** 153 * When the recoup happened. 154 */ 155 struct GNUNET_TIME_Timestamp recoup_timestamp; 156 }; 157 158 159 /** 160 * Recoup a coin into a reserve. 161 * 162 * @param pg the database context 163 * @param reserve_pub reserve to credit 164 * @param withdraw_id withdraw that justifies the recoup 165 * @param coin_pub coin to recoup 166 * @param known_coin_id row of @a coin_pub 167 * @param seed seed for the blinding secret and coin signature 168 * @param[out] st set to the outcome 169 * @return transaction status 170 */ 171 static enum GNUNET_DB_QueryStatus 172 run_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg, 173 const struct TALER_ReservePublicKeyP *reserve_pub, 174 uint64_t withdraw_id, 175 const struct TALER_CoinSpendPublicKeyP *coin_pub, 176 uint64_t known_coin_id, 177 uint32_t seed, 178 struct RecoupStatus *st) 179 { 180 union GNUNET_CRYPTO_BlindingSecretP coin_bks; 181 struct TALER_CoinSpendSignatureP coin_sig; 182 183 TDB_fill (&coin_bks, 184 sizeof (coin_bks), 185 seed); 186 TDB_fill (&coin_sig, 187 sizeof (coin_sig), 188 seed); 189 memset (st, 190 0, 191 sizeof (*st)); 192 st->recoup_timestamp = GNUNET_TIME_timestamp_get (); 193 return TALER_EXCHANGEDB_do_recoup (pg, 194 reserve_pub, 195 withdraw_id, 196 &coin_bks, 197 coin_pub, 198 known_coin_id, 199 &coin_sig, 200 &st->recoup_timestamp, 201 &st->recoup_ok, 202 &st->internal_failure); 203 } 204 205 206 /** 207 * Recouping a coin the exchange does not know is an internal failure. 208 * 209 * @param pg the database context 210 * @return 0 on success 211 */ 212 static int 213 check_unknown_coin (struct TALER_EXCHANGEDB_PostgresContext *pg) 214 { 215 struct TALER_ReservePublicKeyP reserve_pub; 216 struct TALER_CoinSpendPublicKeyP coin_pub; 217 struct RecoupContext ctx = { 0 }; 218 struct RecoupStatus st; 219 uint64_t withdraw_id; 220 221 TDB_denom (pg, 222 10, 223 "5", 224 "0.1", 225 &denom); 226 TDB_account (pg, 227 10, 228 &account); 229 TDB_reserve_in (pg, 230 &account, 231 10, 232 "10", 233 &reserve_pub); 234 withdraw_id = TDB_withdraw (pg, 235 &denom, 236 &reserve_pub, 237 10, 238 "5"); 239 TDB_FILL (coin_pub, 240 99); 241 FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 242 run_recoup (pg, 243 &reserve_pub, 244 withdraw_id, 245 &coin_pub, 246 1, 247 99, 248 &st)); 249 FAILIF (! st.internal_failure); 250 FAILIF (st.recoup_ok); 251 FAILIF (0 != TDB_count (pg, 252 "FROM recoup")); 253 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 254 TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg, 255 0, 256 &recoup_cb, 257 &ctx)); 258 FAILIF (0 != ctx.total); 259 return 0; 260 } 261 262 263 /** 264 * A coin that is already empty and was never recouped is refused. 265 * 266 * @param pg the database context 267 * @return 0 on success 268 */ 269 static int 270 check_empty_coin (struct TALER_EXCHANGEDB_PostgresContext *pg) 271 { 272 struct TALER_ReservePublicKeyP reserve_pub; 273 struct TALER_CoinPublicInfo coin; 274 struct RecoupStatus st; 275 uint64_t known_coin_id; 276 uint64_t withdraw_id; 277 char *hex; 278 279 TDB_reserve_in (pg, 280 &account, 281 11, 282 "10", 283 &reserve_pub); 284 withdraw_id = TDB_withdraw (pg, 285 &denom, 286 &reserve_pub, 287 11, 288 "5"); 289 TDB_coin (pg, 290 &denom, 291 20, 292 &coin, 293 &known_coin_id); 294 hex = TDB_hex (&coin.coin_pub, 295 sizeof (coin.coin_pub)); 296 FAILIF_C (GNUNET_OK != 297 TDB_exec (pg, 298 "UPDATE known_coins" 299 " SET remaining=ROW(0,0)::taler_amount" 300 " WHERE coin_pub=decode('%s','hex');", 301 hex), 302 GNUNET_free (hex); TDB_coin_free (&coin)); 303 GNUNET_free (hex); 304 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 305 run_recoup (pg, 306 &reserve_pub, 307 withdraw_id, 308 &coin.coin_pub, 309 known_coin_id, 310 20, 311 &st), 312 TDB_coin_free (&coin)); 313 TDB_coin_free (&coin); 314 FAILIF (st.internal_failure); 315 /* nothing to give back, and no earlier recoup to report */ 316 FAILIF (st.recoup_ok); 317 FAILIF (0 != TDB_count (pg, 318 "FROM recoup")); 319 return 0; 320 } 321 322 323 /** 324 * A funded coin is drained and the reserve is credited. 325 * 326 * @param pg the database context 327 * @return 0 on success 328 */ 329 static int 330 check_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg) 331 { 332 struct TALER_ReservePublicKeyP reserve_pub; 333 struct TALER_CoinPublicInfo coin; 334 struct TALER_EXCHANGEDB_Reserve reserve; 335 struct RecoupStatus st; 336 struct RecoupContext ctx; 337 struct TALER_Amount expect_coin = TDB_amount ("5"); 338 struct TALER_Amount expect_reserve = TDB_amount ("10"); 339 union GNUNET_CRYPTO_BlindingSecretP expect_bks; 340 uint64_t known_coin_id; 341 uint64_t withdraw_id; 342 char *hex; 343 344 TDB_reserve_in (pg, 345 &account, 346 12, 347 "10", 348 &reserve_pub); 349 withdraw_id = TDB_withdraw (pg, 350 &denom, 351 &reserve_pub, 352 12, 353 "5"); 354 TDB_coin (pg, 355 &denom, 356 21, 357 &coin, 358 &known_coin_id); 359 /* EUR:10 came in and EUR:5 was withdrawn, so EUR:5 is left */ 360 memset (&reserve, 361 0, 362 sizeof (reserve)); 363 reserve.pub = reserve_pub; 364 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 365 TALER_EXCHANGEDB_get_reserve (pg, 366 &reserve), 367 TDB_coin_free (&coin)); 368 369 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 370 run_recoup (pg, 371 &reserve_pub, 372 withdraw_id, 373 &coin.coin_pub, 374 known_coin_id, 375 21, 376 &st), 377 TDB_coin_free (&coin)); 378 FAILIF_C (st.internal_failure, 379 TDB_coin_free (&coin)); 380 FAILIF_C (! st.recoup_ok, 381 TDB_coin_free (&coin)); 382 FAILIF_C (1 != TDB_count (pg, 383 "FROM recoup"), 384 TDB_coin_free (&coin)); 385 386 /* the coin is empty... */ 387 hex = TDB_hex (&coin.coin_pub, 388 sizeof (coin.coin_pub)); 389 FAILIF_C (1 != TDB_count (pg, 390 "FROM known_coins" 391 " WHERE coin_pub=decode('%s','hex')" 392 " AND remaining=ROW(0,0)::taler_amount", 393 hex), 394 GNUNET_free (hex); TDB_coin_free (&coin)); 395 GNUNET_free (hex); 396 /* ...and the reserve got the money */ 397 memset (&reserve, 398 0, 399 sizeof (reserve)); 400 reserve.pub = reserve_pub; 401 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 402 TALER_EXCHANGEDB_get_reserve (pg, 403 &reserve), 404 TDB_coin_free (&coin)); 405 FAILIF_C (0 != TALER_amount_cmp (&reserve.balance, 406 &expect_reserve), 407 TDB_coin_free (&coin)); 408 409 /* the recoup is reported with everything it was made with */ 410 TDB_FILL (expect_bks, 411 21); 412 memset (&ctx, 413 0, 414 sizeof (ctx)); 415 ctx.coin_pub = &coin.coin_pub; 416 FAILIF_C (0 >= 417 TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg, 418 0, 419 &recoup_cb, 420 &ctx), 421 TDB_coin_free (&coin)); 422 FAILIF_C (1 != ctx.matched, 423 TDB_coin_free (&coin)); 424 FAILIF_C (0 != TALER_amount_cmp (&ctx.amount, 425 &expect_coin), 426 TDB_coin_free (&coin)); 427 FAILIF_C (0 != GNUNET_memcmp (&ctx.reserve_pub, 428 &reserve_pub), 429 TDB_coin_free (&coin)); 430 FAILIF_C (0 != GNUNET_memcmp (&ctx.coin_blind, 431 &expect_bks), 432 TDB_coin_free (&coin)); 433 434 /* recouping the same coin again finds the earlier recoup */ 435 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 436 run_recoup (pg, 437 &reserve_pub, 438 withdraw_id, 439 &coin.coin_pub, 440 known_coin_id, 441 21, 442 &st), 443 TDB_coin_free (&coin)); 444 FAILIF_C (! st.recoup_ok, 445 TDB_coin_free (&coin)); 446 FAILIF_C (st.internal_failure, 447 TDB_coin_free (&coin)); 448 FAILIF_C (1 != TDB_count (pg, 449 "FROM recoup"), 450 TDB_coin_free (&coin)); 451 TDB_coin_free (&coin); 452 return 0; 453 } 454 455 456 /** 457 * The iterator's serial bound and abort return behave as documented. 458 * 459 * @param pg the database context 460 * @return 0 on success 461 */ 462 static int 463 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg) 464 { 465 struct RecoupContext ctx; 466 467 memset (&ctx, 468 0, 469 sizeof (ctx)); 470 FAILIF (1 != 471 TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg, 472 0, 473 &recoup_cb, 474 &ctx)); 475 memset (&ctx, 476 0, 477 sizeof (ctx)); 478 FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != 479 TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg, 480 1000, 481 &recoup_cb, 482 &ctx)); 483 FAILIF (0 != ctx.total); 484 memset (&ctx, 485 0, 486 sizeof (ctx)); 487 ctx.stop_after = 1; 488 FAILIF (1 != 489 TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg, 490 0, 491 &recoup_cb, 492 &ctx)); 493 FAILIF (1 != ctx.total); 494 return 0; 495 } 496 497 498 /** 499 * The checks to run, in order. 500 */ 501 static const struct TDB_Test tests[] = { 502 { "recoup-unknown-coin", 503 &check_unknown_coin }, 504 { "recoup-empty-coin", 505 &check_empty_coin }, 506 { "recoup-recoup", 507 &check_recoup }, 508 { "recoup-iterate", 509 &check_iterate }, 510 { NULL, NULL } 511 }; 512 513 514 int 515 main (int argc, 516 char *const *argv) 517 { 518 int ret; 519 520 ret = TDB_main (argc, 521 argv, 522 "test-recoup", 523 "Tests for the exchangedb `recoup' table", 524 tests); 525 TDB_account_free (&account); 526 TDB_denom_free (&denom); 527 return ret; 528 } 529 530 531 /* end of test_recoup.c */