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 (23027B)


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