exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

test_refresh.c (21602B)


      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_refresh.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `refresh`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_do_refresh(), #TALER_EXCHANGEDB_get_refresh(),
     23  * #TALER_EXCHANGEDB_update_to_refresh_revealed(),
     24  * #TALER_EXCHANGEDB_iterate_refreshes_above_serial_id().
     25  *
     26  * `refresh` references `known_coins`, which TDB_coin() creates.  The
     27  * do_refresh() answers checked here are: unknown coin, insufficient coin
     28  * balance, the zombie requirement, blinding-seed reuse and an idempotent
     29  * replay.
     30  */
     31 #include "test_common.h"
     32 #include "exchange-database/do_refresh.h"
     33 #include "exchange-database/get_known_coin.h"
     34 #include "exchange-database/get_refresh.h"
     35 #include "exchange-database/iterate_refreshes_above_serial_id.h"
     36 #include "exchange-database/update_to_refresh_revealed.h"
     37 
     38 
     39 /**
     40  * Denomination the checks melt.
     41  */
     42 static struct TDB_Denom denom;
     43 
     44 
     45 /**
     46  * Fill in a refresh (melt) request for one fresh coin.
     47  *
     48  * @param seed seed for the commitment, signatures and seeds
     49  * @param amount amount to melt, e.g. "1"
     50  * @param coin coin being melted
     51  * @param with_seed true to pass a blinding seed and a CS R value
     52  * @param[out] rf set to the request; release with free_refresh()
     53  */
     54 static void
     55 make_refresh (uint32_t seed,
     56               const char *amount,
     57               const struct TALER_CoinPublicInfo *coin,
     58               bool with_seed,
     59               struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *rf)
     60 {
     61   memset (rf,
     62           0,
     63           sizeof (*rf));
     64   rf->coin.coin_pub = coin->coin_pub;
     65   rf->coin.denom_pub_hash = coin->denom_pub_hash;
     66   rf->coin.no_age_commitment = coin->no_age_commitment;
     67   TDB_fill (&rf->coin_sig,
     68             sizeof (rf->coin_sig),
     69             seed);
     70   TDB_fill (&rf->rc,
     71             sizeof (rf->rc),
     72             seed);
     73   TDB_fill (&rf->refresh_seed,
     74             sizeof (rf->refresh_seed),
     75             seed);
     76   TDB_fill (&rf->planchets_h,
     77             sizeof (rf->planchets_h),
     78             seed);
     79   TDB_fill (&rf->selected_h,
     80             sizeof (rf->selected_h),
     81             seed + 1);
     82   rf->amount_with_fee = TDB_amount (amount);
     83   rf->num_coins = 1;
     84   rf->denom_serials = GNUNET_new (uint64_t);
     85   rf->denom_serials[0] = denom.serial;
     86   rf->denom_sigs = GNUNET_new (struct TALER_BlindedDenominationSignature);
     87   TDB_blinded_denom_sig (seed,
     88                          &rf->denom_sigs[0]);
     89   rf->noreveal_index = 1;
     90   /* v27 refresh: the client provides no transfer public keys */
     91   rf->is_v27_refresh = true;
     92   rf->no_blinding_seed = ! with_seed;
     93   if (with_seed)
     94   {
     95     TDB_fill (&rf->blinding_seed,
     96               sizeof (rf->blinding_seed),
     97               seed);
     98     rf->num_cs_r_values = 1;
     99     rf->cs_r_values = GNUNET_new (struct GNUNET_CRYPTO_CSPublicRPairP);
    100     TDB_fill (rf->cs_r_values,
    101               sizeof (*rf->cs_r_values),
    102               seed);
    103     rf->cs_r_choices = 0;
    104   }
    105 }
    106 
    107 
    108 /**
    109  * Release what make_refresh() allocated.
    110  *
    111  * @param[in,out] rf request to clean up
    112  */
    113 static void
    114 free_refresh (struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *rf)
    115 {
    116   for (size_t i = 0; i<rf->num_coins; i++)
    117     TALER_blinded_denom_sig_free (&rf->denom_sigs[i]);
    118   GNUNET_free (rf->denom_sigs);
    119   GNUNET_free (rf->denom_serials);
    120   GNUNET_free (rf->cs_r_values);
    121   GNUNET_free (rf->transfer_pubs);
    122   GNUNET_free (rf->denom_pub_hashes);
    123 }
    124 
    125 
    126 /**
    127  * Outcome flags of a refresh request.
    128  */
    129 struct RefreshStatus
    130 {
    131   /**
    132    * Was there already a row for this commitment?
    133    */
    134   bool found;
    135 
    136   /**
    137    * Did the melt have to be of a zombie coin?
    138    */
    139   bool zombie_required;
    140 
    141   /**
    142    * Was the blinding seed used before?
    143    */
    144   bool nonce_reuse;
    145 
    146   /**
    147    * Was the coin balance sufficient?
    148    */
    149   bool balance_ok;
    150 
    151   /**
    152    * Balance the coin had.
    153    */
    154   struct TALER_Amount coin_balance;
    155 
    156   /**
    157    * Index the exchange chose not to reveal.
    158    */
    159   uint32_t noreveal_index;
    160 };
    161 
    162 
    163 /**
    164  * Perform a refresh request.
    165  *
    166  * @param pg the database context
    167  * @param rf the request
    168  * @param zombie_required whether the coin must be a zombie
    169  * @param[out] st set to the outcome
    170  * @return transaction status
    171  */
    172 static enum GNUNET_DB_QueryStatus
    173 run_refresh (struct TALER_EXCHANGEDB_PostgresContext *pg,
    174              struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *rf,
    175              bool zombie_required,
    176              struct RefreshStatus *st)
    177 {
    178   struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
    179 
    180   memset (st,
    181           0,
    182           sizeof (*st));
    183   st->zombie_required = zombie_required;
    184   return TALER_EXCHANGEDB_do_refresh (pg,
    185                                       rf,
    186                                       &now,
    187                                       &st->found,
    188                                       &st->noreveal_index,
    189                                       &st->zombie_required,
    190                                       &st->nonce_reuse,
    191                                       &st->balance_ok,
    192                                       &st->coin_balance);
    193 }
    194 
    195 
    196 /**
    197  * Melting a coin that is not known does nothing.
    198  *
    199  * @param pg the database context
    200  * @return 0 on success
    201  */
    202 static int
    203 check_unknown_coin (struct TALER_EXCHANGEDB_PostgresContext *pg)
    204 {
    205   struct TALER_CoinPublicInfo coin;
    206   struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS rf;
    207   struct RefreshStatus st;
    208 
    209   TDB_denom (pg,
    210              10,
    211              "5",
    212              "0.1",
    213              &denom);
    214   memset (&coin,
    215           0,
    216           sizeof (coin));
    217   TDB_FILL (coin.coin_pub,
    218             1);
    219   coin.denom_pub_hash = denom.h_denom_pub;
    220   coin.no_age_commitment = true;
    221   make_refresh (1,
    222                 "1",
    223                 &coin,
    224                 false,
    225                 &rf);
    226   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    227             run_refresh (pg,
    228                          &rf,
    229                          false,
    230                          &st),
    231             free_refresh (&rf));
    232   free_refresh (&rf);
    233   FAILIF (0 != TDB_count (pg,
    234                           "FROM refresh"));
    235   return 0;
    236 }
    237 
    238 
    239 /**
    240  * Melting more than the coin is worth does nothing.
    241  *
    242  * @param pg the database context
    243  * @return 0 on success
    244  */
    245 static int
    246 check_insufficient_balance (struct TALER_EXCHANGEDB_PostgresContext *pg)
    247 {
    248   struct TALER_CoinPublicInfo coin;
    249   struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS rf;
    250   struct RefreshStatus st;
    251   struct TALER_Amount expect = TDB_amount ("5");
    252 
    253   TDB_coin (pg,
    254             &denom,
    255             20,
    256             &coin,
    257             NULL);
    258   make_refresh (2,
    259                 "6",
    260                 &coin,
    261                 false,
    262                 &rf);
    263   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    264             run_refresh (pg,
    265                          &rf,
    266                          false,
    267                          &st),
    268             free_refresh (&rf); TDB_coin_free (&coin));
    269   free_refresh (&rf);
    270   FAILIF_C (st.balance_ok,
    271             TDB_coin_free (&coin));
    272   FAILIF_C (0 != TALER_amount_cmp (&st.coin_balance,
    273                                    &expect),
    274             TDB_coin_free (&coin));
    275   /* the row was written before the balance was checked, so the caller has
    276      to roll back -- here the check simply notes that it is there */
    277   FAILIF_C (1 != TDB_count (pg,
    278                             "FROM refresh"),
    279             TDB_coin_free (&coin));
    280   FAILIF_C (GNUNET_OK !=
    281             TDB_exec (pg,
    282                       "DELETE FROM refresh;"),
    283             TDB_coin_free (&coin));
    284   TDB_coin_free (&coin);
    285   return 0;
    286 }
    287 
    288 
    289 /**
    290  * A melt within the coin's balance is recorded and debits the coin.
    291  *
    292  * @param pg the database context
    293  * @return 0 on success
    294  */
    295 static int
    296 check_refresh (struct TALER_EXCHANGEDB_PostgresContext *pg)
    297 {
    298   struct TALER_CoinPublicInfo coin;
    299   struct TALER_CoinPublicInfo got_coin;
    300   struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS rf;
    301   struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS got;
    302   struct RefreshStatus st;
    303   struct TALER_Amount expect_amount = TDB_amount ("1");
    304 
    305   TDB_coin (pg,
    306             &denom,
    307             20,
    308             &coin,
    309             NULL);
    310   make_refresh (3,
    311                 "1",
    312                 &coin,
    313                 true,
    314                 &rf);
    315   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    316             run_refresh (pg,
    317                          &rf,
    318                          false,
    319                          &st),
    320             free_refresh (&rf); TDB_coin_free (&coin));
    321   FAILIF_C (! st.balance_ok,
    322             free_refresh (&rf); TDB_coin_free (&coin));
    323   FAILIF_C (st.found,
    324             free_refresh (&rf); TDB_coin_free (&coin));
    325   FAILIF_C (st.nonce_reuse,
    326             free_refresh (&rf); TDB_coin_free (&coin));
    327   FAILIF_C (1 != TDB_count (pg,
    328                             "FROM refresh"),
    329             free_refresh (&rf); TDB_coin_free (&coin));
    330 
    331   /* the melt is on file with everything it was created with */
    332   memset (&got,
    333           0,
    334           sizeof (got));
    335   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    336             TALER_EXCHANGEDB_get_refresh (pg,
    337                                           &rf.rc,
    338                                           &got),
    339             free_refresh (&rf); TDB_coin_free (&coin));
    340   FAILIF_C (0 != TALER_amount_cmp (&got.amount_with_fee,
    341                                    &expect_amount),
    342             free_refresh (&got); free_refresh (&rf); TDB_coin_free (&coin));
    343   FAILIF_C (0 != GNUNET_memcmp (&got.coin.coin_pub,
    344                                 &coin.coin_pub),
    345             free_refresh (&got); free_refresh (&rf); TDB_coin_free (&coin));
    346   FAILIF_C (0 != GNUNET_memcmp (&got.coin_sig,
    347                                 &rf.coin_sig),
    348             free_refresh (&got); free_refresh (&rf); TDB_coin_free (&coin));
    349   FAILIF_C (0 != GNUNET_memcmp (&got.refresh_seed,
    350                                 &rf.refresh_seed),
    351             free_refresh (&got); free_refresh (&rf); TDB_coin_free (&coin));
    352   FAILIF_C (1 != got.num_coins,
    353             free_refresh (&got); free_refresh (&rf); TDB_coin_free (&coin));
    354   FAILIF_C (denom.serial != got.denom_serials[0],
    355             free_refresh (&got); free_refresh (&rf); TDB_coin_free (&coin));
    356   FAILIF_C (rf.noreveal_index != got.noreveal_index,
    357             free_refresh (&got); free_refresh (&rf); TDB_coin_free (&coin));
    358   FAILIF_C (got.revealed,
    359             free_refresh (&got); free_refresh (&rf); TDB_coin_free (&coin));
    360   FAILIF_C (got.no_blinding_seed,
    361             free_refresh (&got); free_refresh (&rf); TDB_coin_free (&coin));
    362   FAILIF_C (! got.is_v27_refresh,
    363             free_refresh (&got); free_refresh (&rf); TDB_coin_free (&coin));
    364   free_refresh (&got);
    365 
    366   /* the coin was debited */
    367   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    368             TALER_EXCHANGEDB_get_known_coin (pg,
    369                                              &coin.coin_pub,
    370                                              &got_coin),
    371             free_refresh (&rf); TDB_coin_free (&coin));
    372   TALER_denom_sig_free (&got_coin.denom_sig);
    373   {
    374     char *hex = TDB_hex (&coin.coin_pub,
    375                          sizeof (coin.coin_pub));
    376 
    377     FAILIF_C (1 != TDB_count (pg,
    378                               "FROM known_coins"
    379                               " WHERE coin_pub=decode('%s','hex')"
    380                               "   AND remaining=ROW(4,0)::taler_amount",
    381                               hex),
    382               GNUNET_free (hex);
    383               free_refresh (&rf); TDB_coin_free (&coin));
    384     GNUNET_free (hex);
    385   }
    386 
    387   /* a replay is idempotent and reports the index we chose */
    388   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    389             run_refresh (pg,
    390                          &rf,
    391                          false,
    392                          &st),
    393             free_refresh (&rf); TDB_coin_free (&coin));
    394   FAILIF_C (! st.found,
    395             free_refresh (&rf); TDB_coin_free (&coin));
    396   FAILIF_C (rf.noreveal_index != st.noreveal_index,
    397             free_refresh (&rf); TDB_coin_free (&coin));
    398   FAILIF_C (1 != TDB_count (pg,
    399                             "FROM refresh"),
    400             free_refresh (&rf); TDB_coin_free (&coin));
    401   free_refresh (&rf);
    402   TDB_coin_free (&coin);
    403   return 0;
    404 }
    405 
    406 
    407 /**
    408  * Reusing a blinding seed is refused.
    409  *
    410  * @param pg the database context
    411  * @return 0 on success
    412  */
    413 static int
    414 check_nonce_reuse (struct TALER_EXCHANGEDB_PostgresContext *pg)
    415 {
    416   struct TALER_CoinPublicInfo coin;
    417   struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS rf;
    418   struct RefreshStatus st;
    419 
    420   TDB_coin (pg,
    421             &denom,
    422             20,
    423             &coin,
    424             NULL);
    425   /* a fresh commitment, but the blinding seed of check_refresh() */
    426   make_refresh (4,
    427                 "1",
    428                 &coin,
    429                 true,
    430                 &rf);
    431   TDB_fill (&rf.blinding_seed,
    432             sizeof (rf.blinding_seed),
    433             3);
    434   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    435             run_refresh (pg,
    436                          &rf,
    437                          false,
    438                          &st),
    439             free_refresh (&rf); TDB_coin_free (&coin));
    440   FAILIF_C (! st.nonce_reuse,
    441             free_refresh (&rf); TDB_coin_free (&coin));
    442   FAILIF_C (st.found,
    443             free_refresh (&rf); TDB_coin_free (&coin));
    444   free_refresh (&rf);
    445   TDB_coin_free (&coin);
    446   FAILIF (1 != TDB_count (pg,
    447                           "FROM refresh"));
    448   return 0;
    449 }
    450 
    451 
    452 /**
    453  * A melt that insists on a zombie coin is refused when the coin never was
    454  * one.
    455  *
    456  * @param pg the database context
    457  * @return 0 on success
    458  */
    459 static int
    460 check_zombie_required (struct TALER_EXCHANGEDB_PostgresContext *pg)
    461 {
    462   struct TALER_CoinPublicInfo coin;
    463   struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS rf;
    464   struct RefreshStatus st;
    465 
    466   TDB_coin (pg,
    467             &denom,
    468             21,
    469             &coin,
    470             NULL);
    471   make_refresh (5,
    472                 "1",
    473                 &coin,
    474                 false,
    475                 &rf);
    476   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    477             run_refresh (pg,
    478                          &rf,
    479                          true,
    480                          &st),
    481             free_refresh (&rf); TDB_coin_free (&coin));
    482   FAILIF_C (! st.zombie_required,
    483             free_refresh (&rf); TDB_coin_free (&coin));
    484   FAILIF_C (st.balance_ok,
    485             free_refresh (&rf); TDB_coin_free (&coin));
    486   free_refresh (&rf);
    487   TDB_coin_free (&coin);
    488   /* the row went in before the zombie check, so the caller must roll back */
    489   FAILIF (2 != TDB_count (pg,
    490                           "FROM refresh"));
    491   FAILIF (GNUNET_OK !=
    492           TDB_exec (pg,
    493                     "DELETE FROM refresh WHERE NOT revealed"
    494                     " AND rc <> (SELECT rc FROM refresh"
    495                     "             ORDER BY refresh_id ASC LIMIT 1);"));
    496   return 0;
    497 }
    498 
    499 
    500 /**
    501  * A melt can be marked as revealed, and marking one that does not exist
    502  * does nothing.
    503  *
    504  * @param pg the database context
    505  * @return 0 on success
    506  */
    507 static int
    508 check_revealed (struct TALER_EXCHANGEDB_PostgresContext *pg)
    509 {
    510   struct TALER_RefreshCommitmentP rc;
    511   struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS got;
    512 
    513   TDB_FILL (rc,
    514             99);
    515   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    516           TALER_EXCHANGEDB_update_to_refresh_revealed (pg,
    517                                                        &rc));
    518   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    519           TALER_EXCHANGEDB_get_refresh (pg,
    520                                         &rc,
    521                                         &got));
    522 
    523   TDB_FILL (rc,
    524             3);
    525   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    526           TALER_EXCHANGEDB_update_to_refresh_revealed (pg,
    527                                                        &rc));
    528   memset (&got,
    529           0,
    530           sizeof (got));
    531   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    532           TALER_EXCHANGEDB_get_refresh (pg,
    533                                         &rc,
    534                                         &got));
    535   FAILIF_C (! got.revealed,
    536             free_refresh (&got));
    537   free_refresh (&got);
    538   /* marking it again is still reported as a row touched */
    539   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    540           TALER_EXCHANGEDB_update_to_refresh_revealed (pg,
    541                                                        &rc));
    542   return 0;
    543 }
    544 
    545 
    546 /**
    547  * Closure for #refreshes_cb().
    548  */
    549 struct RefreshesContext
    550 {
    551   /**
    552    * How many melts did the callback see?
    553    */
    554   unsigned int total;
    555 
    556   /**
    557    * Stop after this many melts; 0 for no limit.
    558    */
    559   unsigned int stop_after;
    560 
    561   /**
    562    * Row of the last melt.
    563    */
    564   uint64_t rowid;
    565 
    566   /**
    567    * Amount of the last melt.
    568    */
    569   struct TALER_Amount amount;
    570 
    571   /**
    572    * Number of new denominations of the last melt.
    573    */
    574   size_t num_nds;
    575 };
    576 
    577 
    578 /**
    579  * Callback for #TALER_EXCHANGEDB_iterate_refreshes_above_serial_id().
    580  *
    581  * @param cls a `struct RefreshesContext *`
    582  * @param rowid row of the melt
    583  * @param old_denom_pub denomination of the melted coin
    584  * @param coin_pub the melted coin
    585  * @param coin_sig signature authorising the melt
    586  * @param h_age_commitment age commitment of the coin, NULL if none
    587  * @param amount_with_fee how much was melted
    588  * @param num_nds length of @a new_denom_serials
    589  * @param new_denom_serials denominations of the fresh coins
    590  * @param rc commitment of the melt
    591  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
    592  */
    593 static enum GNUNET_GenericReturnValue
    594 refreshes_cb (void *cls,
    595               uint64_t rowid,
    596               const struct TALER_DenominationPublicKey *old_denom_pub,
    597               const struct TALER_CoinSpendPublicKeyP *coin_pub,
    598               const struct TALER_CoinSpendSignatureP *coin_sig,
    599               const struct TALER_AgeCommitmentHashP *h_age_commitment,
    600               const struct TALER_Amount *amount_with_fee,
    601               size_t num_nds,
    602               uint64_t new_denom_serials[static num_nds],
    603               const struct TALER_RefreshCommitmentP *rc)
    604 {
    605   struct RefreshesContext *ctx = cls;
    606 
    607   (void) old_denom_pub;
    608   (void) coin_pub;
    609   (void) coin_sig;
    610   (void) h_age_commitment;
    611   (void) new_denom_serials;
    612   (void) rc;
    613   ctx->total++;
    614   ctx->rowid = rowid;
    615   ctx->amount = *amount_with_fee;
    616   ctx->num_nds = num_nds;
    617   if ( (0 != ctx->stop_after) &&
    618        (ctx->total >= ctx->stop_after) )
    619     return GNUNET_SYSERR;
    620   return GNUNET_OK;
    621 }
    622 
    623 
    624 /**
    625  * The auditor's view walks the melts by serial and stops when asked to.
    626  *
    627  * @param pg the database context
    628  * @return 0 on success
    629  */
    630 static int
    631 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
    632 {
    633   struct TALER_CoinPublicInfo coin;
    634   struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS rf;
    635   struct RefreshStatus st;
    636   struct RefreshesContext ctx;
    637   uint64_t rows;
    638 
    639   /* one more melt, so that "stop after the first" is distinguishable
    640      from "saw everything" */
    641   TDB_coin (pg,
    642             &denom,
    643             42,
    644             &coin,
    645             NULL);
    646   make_refresh (42,
    647                 "1",
    648                 &coin,
    649                 false,
    650                 &rf);
    651   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    652             run_refresh (pg,
    653                          &rf,
    654                          false,
    655                          &st),
    656             free_refresh (&rf); TDB_coin_free (&coin));
    657   free_refresh (&rf);
    658   TDB_coin_free (&coin);
    659   FAILIF (! st.balance_ok);
    660   rows = TDB_count (pg,
    661                     "FROM refresh");
    662   FAILIF (2 != rows);
    663   memset (&ctx,
    664           0,
    665           sizeof (ctx));
    666   FAILIF (0 >=
    667           TALER_EXCHANGEDB_iterate_refreshes_above_serial_id (
    668             pg,
    669             0,
    670             &refreshes_cb,
    671             &ctx));
    672   FAILIF (rows != ctx.total);
    673   /* every melt here asked for exactly one fresh coin */
    674   FAILIF (1 != ctx.num_nds);
    675 
    676   /* everything above the last row is nothing */
    677   memset (&ctx,
    678           0,
    679           sizeof (ctx));
    680   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    681           TALER_EXCHANGEDB_iterate_refreshes_above_serial_id (
    682             pg,
    683             UINT32_MAX,
    684             &refreshes_cb,
    685             &ctx));
    686   FAILIF (0 != ctx.total);
    687 
    688   /* a callback that says stop is not called again */
    689   memset (&ctx,
    690           0,
    691           sizeof (ctx));
    692   ctx.stop_after = 1;
    693   FAILIF (0 >=
    694           TALER_EXCHANGEDB_iterate_refreshes_above_serial_id (
    695             pg,
    696             0,
    697             &refreshes_cb,
    698             &ctx));
    699   FAILIF (1 != ctx.total);
    700   return 0;
    701 }
    702 
    703 
    704 /**
    705  * The checks to run, in order.
    706  */
    707 static const struct TDB_Test tests[] = {
    708   { "refresh-unknown-coin",
    709     &check_unknown_coin },
    710   { "refresh-insufficient-balance",
    711     &check_insufficient_balance },
    712   { "refresh-refresh",
    713     &check_refresh },
    714   { "refresh-nonce-reuse",
    715     &check_nonce_reuse },
    716   { "refresh-zombie-required",
    717     &check_zombie_required },
    718   { "refresh-revealed",
    719     &check_revealed },
    720   { "refresh-iterate",
    721     &check_iterate },
    722   { NULL, NULL }
    723 };
    724 
    725 
    726 int
    727 main (int argc,
    728       char *const *argv)
    729 {
    730   int ret;
    731 
    732   ret = TDB_main (argc,
    733                   argv,
    734                   "test-refresh",
    735                   "Tests for the exchangedb `refresh' table",
    736                   tests);
    737   TDB_denom_free (&denom);
    738   return ret;
    739 }
    740 
    741 
    742 /* end of test_refresh.c */