test_recoup_refresh.c (16150B)
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_refresh.c 18 * @brief tests for the exchangedb functions whose primary table is 19 * `recoup_refresh` 20 * @author Christian Grothoff 21 * 22 * Covers #TALER_EXCHANGEDB_do_recoup_refresh() and, as far as it can be, 23 * #TALER_EXCHANGEDB_iterate_recoup_refreshes_above_serial_id(). 24 * 25 * `recoup_refresh` references `known_coins` and `refresh`, so each check 26 * builds a melt first. do_recoup_refresh() moves the fresh coin's whole 27 * remaining balance back to the old coin. 28 * 29 * The iterator cannot be checked beyond "does not invent an answer": its 30 * statement joins `refresh_revealed_coins` and `refresh_commitments`, which 31 * the schema no longer has (EDB-16 in bugs.txt, in-tree FIXME-#9828). 32 */ 33 #include "test_common.h" 34 #include "exchange-database/do_recoup_refresh.h" 35 #include "exchange-database/do_refresh.h" 36 #include "exchange-database/iterate_recoup_refreshes_above_serial_id.h" 37 38 39 /** 40 * Account the checks fund their reserves from. 41 */ 42 static struct TDB_Account account; 43 44 45 /** 46 * Denomination the checks use. 47 */ 48 static struct TDB_Denom denom; 49 50 51 /** 52 * Melt @a coin and return the row of the melt. 53 * 54 * @param pg the database context 55 * @param coin coin to melt 56 * @param seed seed for the commitment and signatures 57 * @param amount how much to melt, e.g. "1" 58 * @return row of the melt in `refresh` 59 */ 60 static uint64_t 61 melt (struct TALER_EXCHANGEDB_PostgresContext *pg, 62 const struct TALER_CoinPublicInfo *coin, 63 uint32_t seed, 64 const char *amount) 65 { 66 struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS rf; 67 struct TALER_BlindedDenominationSignature denom_sig; 68 struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); 69 struct TALER_Amount coin_balance; 70 uint64_t denom_serial = denom.serial; 71 uint64_t refresh_id; 72 uint32_t noreveal_index; 73 bool found; 74 bool zombie_required = false; 75 bool nonce_reuse; 76 bool balance_ok; 77 78 memset (&rf, 79 0, 80 sizeof (rf)); 81 rf.coin.coin_pub = coin->coin_pub; 82 rf.coin.denom_pub_hash = coin->denom_pub_hash; 83 rf.coin.no_age_commitment = coin->no_age_commitment; 84 TDB_fill (&rf.coin_sig, 85 sizeof (rf.coin_sig), 86 seed); 87 TDB_fill (&rf.rc, 88 sizeof (rf.rc), 89 seed); 90 TDB_fill (&rf.refresh_seed, 91 sizeof (rf.refresh_seed), 92 seed); 93 TDB_fill (&rf.planchets_h, 94 sizeof (rf.planchets_h), 95 seed); 96 TDB_fill (&rf.selected_h, 97 sizeof (rf.selected_h), 98 seed + 1); 99 rf.amount_with_fee = TDB_amount (amount); 100 rf.num_coins = 1; 101 rf.denom_serials = &denom_serial; 102 TDB_blinded_denom_sig (seed, 103 &denom_sig); 104 rf.denom_sigs = &denom_sig; 105 rf.noreveal_index = 0; 106 rf.is_v27_refresh = true; 107 rf.no_blinding_seed = true; 108 GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == 109 TALER_EXCHANGEDB_do_refresh (pg, 110 &rf, 111 &now, 112 &found, 113 &noreveal_index, 114 &zombie_required, 115 &nonce_reuse, 116 &balance_ok, 117 &coin_balance)); 118 GNUNET_assert (balance_ok); 119 TALER_blinded_denom_sig_free (&denom_sig); 120 { 121 struct GNUNET_PQ_QueryParam params[] = { 122 GNUNET_PQ_query_param_auto_from_type (&rf.rc), 123 GNUNET_PQ_query_param_end 124 }; 125 struct GNUNET_PQ_ResultSpec rs[] = { 126 GNUNET_PQ_result_spec_uint64 ("refresh_id", 127 &refresh_id), 128 GNUNET_PQ_result_spec_end 129 }; 130 131 GNUNET_assert (GNUNET_OK == 132 GNUNET_PQ_prepare_anon (pg->conn, 133 "SELECT refresh_id" 134 " FROM refresh" 135 " WHERE rc=$1;")); 136 GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == 137 GNUNET_PQ_eval_prepared_singleton_select (pg->conn, 138 "", 139 params, 140 rs)); 141 } 142 return refresh_id; 143 } 144 145 146 /** 147 * Callback for 148 * #TALER_EXCHANGEDB_iterate_recoup_refreshes_above_serial_id(). 149 * 150 * @param cls a pointer to an `unsigned int` counter 151 * @param rowid row of the recoup 152 * @param timestamp when it happened 153 * @param amount how much went back 154 * @param old_coin_pub the old coin 155 * @param old_denom_pub_hash denomination of the old coin 156 * @param coin the fresh coin 157 * @param denom_pub denomination of @a coin 158 * @param coin_sig signature over the request 159 * @param coin_blind blinding secret of the fresh coin 160 * @return #GNUNET_OK 161 */ 162 static enum GNUNET_GenericReturnValue 163 recoup_refresh_cb (void *cls, 164 uint64_t rowid, 165 struct GNUNET_TIME_Timestamp timestamp, 166 const struct TALER_Amount *amount, 167 const struct TALER_CoinSpendPublicKeyP *old_coin_pub, 168 const struct TALER_DenominationHashP *old_denom_pub_hash, 169 const struct TALER_CoinPublicInfo *coin, 170 const struct TALER_DenominationPublicKey *denom_pub, 171 const struct TALER_CoinSpendSignatureP *coin_sig, 172 const union GNUNET_CRYPTO_BlindingSecretP *coin_blind) 173 { 174 unsigned int *total = cls; 175 176 (void) rowid; 177 (void) timestamp; 178 (void) amount; 179 (void) old_coin_pub; 180 (void) old_denom_pub_hash; 181 (void) coin; 182 (void) denom_pub; 183 (void) coin_sig; 184 (void) coin_blind; 185 (*total)++; 186 return GNUNET_OK; 187 } 188 189 190 /** 191 * Outcome of a recoup-refresh request. 192 */ 193 struct RecoupStatus 194 { 195 /** 196 * Was the recoup accepted? 197 */ 198 bool recoup_ok; 199 200 /** 201 * Did something go wrong inside the database? 202 */ 203 bool internal_failure; 204 205 /** 206 * When the recoup happened. 207 */ 208 struct GNUNET_TIME_Timestamp recoup_timestamp; 209 }; 210 211 212 /** 213 * Recoup a fresh coin back onto the coin it was refreshed from. 214 * 215 * @param pg the database context 216 * @param old_coin_pub coin to credit 217 * @param refresh_id melt that justifies the recoup 218 * @param coin_pub fresh coin to drain 219 * @param known_coin_id row of @a coin_pub 220 * @param seed seed for the blinding secret and coin signature 221 * @param[out] st set to the outcome 222 * @return transaction status 223 */ 224 static enum GNUNET_DB_QueryStatus 225 run_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg, 226 const struct TALER_CoinSpendPublicKeyP *old_coin_pub, 227 uint64_t refresh_id, 228 const struct TALER_CoinSpendPublicKeyP *coin_pub, 229 uint64_t known_coin_id, 230 uint32_t seed, 231 struct RecoupStatus *st) 232 { 233 union GNUNET_CRYPTO_BlindingSecretP coin_bks; 234 struct TALER_CoinSpendSignatureP coin_sig; 235 236 TDB_fill (&coin_bks, 237 sizeof (coin_bks), 238 seed); 239 TDB_fill (&coin_sig, 240 sizeof (coin_sig), 241 seed); 242 memset (st, 243 0, 244 sizeof (*st)); 245 st->recoup_timestamp = GNUNET_TIME_timestamp_get (); 246 return TALER_EXCHANGEDB_do_recoup_refresh (pg, 247 old_coin_pub, 248 refresh_id, 249 &coin_bks, 250 coin_pub, 251 known_coin_id, 252 &coin_sig, 253 &st->recoup_timestamp, 254 &st->recoup_ok, 255 &st->internal_failure); 256 } 257 258 259 /** 260 * Recouping a coin the exchange does not know is an internal failure. 261 * 262 * @param pg the database context 263 * @return 0 on success 264 */ 265 static int 266 check_unknown_coin (struct TALER_EXCHANGEDB_PostgresContext *pg) 267 { 268 struct TALER_ReservePublicKeyP reserve_pub; 269 struct TALER_CoinPublicInfo old_coin; 270 struct TALER_CoinSpendPublicKeyP coin_pub; 271 struct RecoupStatus st; 272 uint64_t refresh_id; 273 274 TDB_denom (pg, 275 10, 276 "5", 277 "0.1", 278 &denom); 279 TDB_account (pg, 280 10, 281 &account); 282 TDB_reserve_in (pg, 283 &account, 284 10, 285 "10", 286 &reserve_pub); 287 TDB_coin (pg, 288 &denom, 289 20, 290 &old_coin, 291 NULL); 292 refresh_id = melt (pg, 293 &old_coin, 294 1, 295 "1"); 296 TDB_FILL (coin_pub, 297 99); 298 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 299 run_recoup (pg, 300 &old_coin.coin_pub, 301 refresh_id, 302 &coin_pub, 303 1, 304 99, 305 &st), 306 TDB_coin_free (&old_coin)); 307 TDB_coin_free (&old_coin); 308 FAILIF (! st.internal_failure); 309 FAILIF (st.recoup_ok); 310 FAILIF (0 != TDB_count (pg, 311 "FROM recoup_refresh")); 312 return 0; 313 } 314 315 316 /** 317 * A fresh coin with nothing left on it and no earlier recoup is refused. 318 * 319 * @param pg the database context 320 * @return 0 on success 321 */ 322 static int 323 check_empty_coin (struct TALER_EXCHANGEDB_PostgresContext *pg) 324 { 325 struct TALER_CoinPublicInfo old_coin; 326 struct TALER_CoinPublicInfo fresh; 327 struct RecoupStatus st; 328 uint64_t known_coin_id; 329 uint64_t refresh_id; 330 char *hex; 331 332 TDB_coin (pg, 333 &denom, 334 21, 335 &old_coin, 336 NULL); 337 refresh_id = melt (pg, 338 &old_coin, 339 2, 340 "1"); 341 TDB_coin (pg, 342 &denom, 343 22, 344 &fresh, 345 &known_coin_id); 346 hex = TDB_hex (&fresh.coin_pub, 347 sizeof (fresh.coin_pub)); 348 FAILIF_C (GNUNET_OK != 349 TDB_exec (pg, 350 "UPDATE known_coins" 351 " SET remaining=ROW(0,0)::taler_amount" 352 " WHERE coin_pub=decode('%s','hex');", 353 hex), 354 GNUNET_free (hex); 355 TDB_coin_free (&fresh); TDB_coin_free (&old_coin)); 356 GNUNET_free (hex); 357 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 358 run_recoup (pg, 359 &old_coin.coin_pub, 360 refresh_id, 361 &fresh.coin_pub, 362 known_coin_id, 363 22, 364 &st), 365 TDB_coin_free (&fresh); TDB_coin_free (&old_coin)); 366 TDB_coin_free (&fresh); 367 TDB_coin_free (&old_coin); 368 FAILIF (st.internal_failure); 369 FAILIF (st.recoup_ok); 370 FAILIF (0 != TDB_count (pg, 371 "FROM recoup_refresh")); 372 return 0; 373 } 374 375 376 /** 377 * A funded fresh coin is drained back onto the old coin. 378 * 379 * @param pg the database context 380 * @return 0 on success 381 */ 382 static int 383 check_recoup_refresh (struct TALER_EXCHANGEDB_PostgresContext *pg) 384 { 385 struct TALER_CoinPublicInfo old_coin; 386 struct TALER_CoinPublicInfo fresh; 387 struct RecoupStatus st; 388 uint64_t known_coin_id; 389 uint64_t refresh_id; 390 char *hex; 391 392 TDB_coin (pg, 393 &denom, 394 23, 395 &old_coin, 396 NULL); 397 refresh_id = melt (pg, 398 &old_coin, 399 3, 400 "1"); 401 TDB_coin (pg, 402 &denom, 403 24, 404 &fresh, 405 &known_coin_id); 406 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 407 run_recoup (pg, 408 &old_coin.coin_pub, 409 refresh_id, 410 &fresh.coin_pub, 411 known_coin_id, 412 24, 413 &st), 414 TDB_coin_free (&fresh); TDB_coin_free (&old_coin)); 415 FAILIF_C (st.internal_failure, 416 TDB_coin_free (&fresh); TDB_coin_free (&old_coin)); 417 FAILIF_C (! st.recoup_ok, 418 TDB_coin_free (&fresh); TDB_coin_free (&old_coin)); 419 FAILIF_C (1 != TDB_count (pg, 420 "FROM recoup_refresh"), 421 TDB_coin_free (&fresh); TDB_coin_free (&old_coin)); 422 423 /* the fresh coin is empty */ 424 hex = TDB_hex (&fresh.coin_pub, 425 sizeof (fresh.coin_pub)); 426 FAILIF_C (1 != TDB_count (pg, 427 "FROM known_coins" 428 " WHERE coin_pub=decode('%s','hex')" 429 " AND remaining=ROW(0,0)::taler_amount", 430 hex), 431 GNUNET_free (hex); 432 TDB_coin_free (&fresh); TDB_coin_free (&old_coin)); 433 GNUNET_free (hex); 434 /* the old coin got the EUR:5 back on top of the EUR:4 it had left */ 435 hex = TDB_hex (&old_coin.coin_pub, 436 sizeof (old_coin.coin_pub)); 437 FAILIF_C (1 != TDB_count (pg, 438 "FROM known_coins" 439 " WHERE coin_pub=decode('%s','hex')" 440 " AND remaining=ROW(9,0)::taler_amount", 441 hex), 442 GNUNET_free (hex); 443 TDB_coin_free (&fresh); TDB_coin_free (&old_coin)); 444 GNUNET_free (hex); 445 446 /* recouping the same coin again finds the earlier recoup */ 447 FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != 448 run_recoup (pg, 449 &old_coin.coin_pub, 450 refresh_id, 451 &fresh.coin_pub, 452 known_coin_id, 453 24, 454 &st), 455 TDB_coin_free (&fresh); TDB_coin_free (&old_coin)); 456 FAILIF_C (! st.recoup_ok, 457 TDB_coin_free (&fresh); TDB_coin_free (&old_coin)); 458 FAILIF_C (1 != TDB_count (pg, 459 "FROM recoup_refresh"), 460 TDB_coin_free (&fresh); TDB_coin_free (&old_coin)); 461 TDB_coin_free (&fresh); 462 TDB_coin_free (&old_coin); 463 return 0; 464 } 465 466 467 /** 468 * The iterator must not report a recoup-refresh that is not there. 469 * 470 * This is all it can be checked for today: its statement joins 471 * `refresh_revealed_coins` and `refresh_commitments`, tables the schema no 472 * longer has, so it cannot return a row at all (EDB-16). The assertion 473 * below holds both now and once that is repaired. 474 * 475 * @param pg the database context 476 * @return 0 on success 477 */ 478 static int 479 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg) 480 { 481 unsigned int total = 0; 482 483 FAILIF (0 < 484 TALER_EXCHANGEDB_iterate_recoup_refreshes_above_serial_id ( 485 pg, 486 1000, 487 &recoup_refresh_cb, 488 &total)); 489 FAILIF (0 != total); 490 return 0; 491 } 492 493 494 /** 495 * The checks to run, in order. 496 */ 497 static const struct TDB_Test tests[] = { 498 { "recoup-refresh-unknown-coin", 499 &check_unknown_coin }, 500 { "recoup-refresh-empty-coin", 501 &check_empty_coin }, 502 { "recoup-refresh-recoup", 503 &check_recoup_refresh }, 504 { "recoup-refresh-iterate", 505 &check_iterate }, 506 { NULL, NULL } 507 }; 508 509 510 int 511 main (int argc, 512 char *const *argv) 513 { 514 int ret; 515 516 ret = TDB_main (argc, 517 argv, 518 "test-recoup-refresh", 519 "Tests for the exchangedb `recoup_refresh' table", 520 tests); 521 TDB_account_free (&account); 522 TDB_denom_free (&denom); 523 return ret; 524 } 525 526 527 /* end of test_recoup_refresh.c */