exchange

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

test_withdraw.c (22857B)


      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_withdraw.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `withdraw`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_do_withdraw(),
     23  * #TALER_EXCHANGEDB_get_withdraw(),
     24  * #TALER_EXCHANGEDB_iterate_withdrawals_above_serial_id() and
     25  * #TALER_EXCHANGEDB_iterate_withdraw_amounts_for_kyc_check().
     26  *
     27  * `withdraw` references `reserves` and `denominations`.  do_withdraw() has
     28  * four distinct "did not do the work" answers -- unknown reserve,
     29  * insufficient balance, age requirement not met and blinding-seed reuse --
     30  * plus an idempotent replay, and each of them is checked.
     31  */
     32 #include "test_common.h"
     33 #include "exchange-database/do_withdraw.h"
     34 #include "exchange-database/get_withdraw.h"
     35 #include "exchange-database/get_reserve.h"
     36 #include "exchange-database/iterate_withdrawals_above_serial_id.h"
     37 #include "exchange-database/iterate_withdraw_amounts_for_kyc_check.h"
     38 
     39 
     40 /**
     41  * Account the checks fund their reserves from.
     42  */
     43 static struct TDB_Account account;
     44 
     45 
     46 /**
     47  * Denomination the checks withdraw.
     48  */
     49 static struct TDB_Denom denom;
     50 
     51 
     52 /**
     53  * Fill in a withdraw request for one coin of #denom.
     54  *
     55  * @param seed seed for the planchet hash, signature and blinding seed
     56  * @param amount amount to withdraw, e.g. "5"
     57  * @param reserve_pub reserve to withdraw from
     58  * @param with_seed true to pass a blinding seed
     59  * @param[out] wd set to the request; release with free_withdraw()
     60  */
     61 static void
     62 make_withdraw (uint32_t seed,
     63                const char *amount,
     64                const struct TALER_ReservePublicKeyP *reserve_pub,
     65                bool with_seed,
     66                struct TALER_EXCHANGEDB_Withdraw *wd)
     67 {
     68   memset (wd,
     69           0,
     70           sizeof (*wd));
     71   wd->amount_with_fee = TDB_amount (amount);
     72   wd->age_proof_required = false;
     73   wd->reserve_pub = *reserve_pub;
     74   TDB_fill (&wd->planchets_h,
     75             sizeof (wd->planchets_h),
     76             seed);
     77   TDB_fill (&wd->reserve_sig,
     78             sizeof (wd->reserve_sig),
     79             seed);
     80   wd->num_coins = 1;
     81   wd->denom_serials = GNUNET_new (uint64_t);
     82   wd->denom_serials[0] = denom.serial;
     83   wd->denom_sigs = GNUNET_new (struct TALER_BlindedDenominationSignature);
     84   TDB_blinded_denom_sig (seed,
     85                          &wd->denom_sigs[0]);
     86   wd->no_blinding_seed = ! with_seed;
     87   if (with_seed)
     88     TDB_fill (&wd->blinding_seed,
     89               sizeof (wd->blinding_seed),
     90               seed);
     91 }
     92 
     93 
     94 /**
     95  * Release what make_withdraw() allocated.
     96  *
     97  * @param[in,out] wd request to clean up
     98  */
     99 static void
    100 free_withdraw (struct TALER_EXCHANGEDB_Withdraw *wd)
    101 {
    102   for (size_t i = 0; i<wd->num_coins; i++)
    103     TALER_blinded_denom_sig_free (&wd->denom_sigs[i]);
    104   GNUNET_free (wd->denom_sigs);
    105   GNUNET_free (wd->denom_serials);
    106 }
    107 
    108 
    109 /**
    110  * Run a withdraw request.
    111  *
    112  * @param pg the database context
    113  * @param wd the request
    114  * @param[out] st set to the outcome flags
    115  * @return transaction status
    116  */
    117 struct WithdrawStatus
    118 {
    119   /**
    120    * Was the balance sufficient?
    121    */
    122   bool balance_ok;
    123 
    124   /**
    125    * Were the age requirements met?
    126    */
    127   bool age_ok;
    128 
    129   /**
    130    * Was this a replay?
    131    */
    132   bool idempotent;
    133 
    134   /**
    135    * Was the blinding seed used before?
    136    */
    137   bool nonce_reuse;
    138 
    139   /**
    140    * Balance the reserve had.
    141    */
    142   struct TALER_Amount reserve_balance;
    143 
    144   /**
    145    * Maximum age the reserve allows.
    146    */
    147   uint16_t allowed_maximum_age;
    148 
    149   /**
    150    * Birthday recorded for the reserve.
    151    */
    152   uint32_t reserve_birthday;
    153 
    154   /**
    155    * Index the exchange chose not to reveal.
    156    */
    157   uint16_t noreveal_index;
    158 };
    159 
    160 
    161 /**
    162  * Perform a withdraw request.
    163  *
    164  * @param pg the database context
    165  * @param wd the request
    166  * @param[out] st set to the outcome
    167  * @return transaction status
    168  */
    169 static enum GNUNET_DB_QueryStatus
    170 run_withdraw (struct TALER_EXCHANGEDB_PostgresContext *pg,
    171               const struct TALER_EXCHANGEDB_Withdraw *wd,
    172               struct WithdrawStatus *st)
    173 {
    174   struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
    175 
    176   memset (st,
    177           0,
    178           sizeof (*st));
    179   return TALER_EXCHANGEDB_do_withdraw (pg,
    180                                        wd,
    181                                        &now,
    182                                        &st->balance_ok,
    183                                        &st->reserve_balance,
    184                                        &st->age_ok,
    185                                        &st->allowed_maximum_age,
    186                                        &st->reserve_birthday,
    187                                        &st->idempotent,
    188                                        &st->noreveal_index,
    189                                        &st->nonce_reuse);
    190 }
    191 
    192 
    193 /**
    194  * Closure for #withdraw_cb().
    195  */
    196 struct WithdrawContext
    197 {
    198   /**
    199    * How many rows did the callback see?
    200    */
    201   unsigned int total;
    202 
    203   /**
    204    * Stop after this many rows; 0 for no limit.
    205    */
    206   unsigned int stop_after;
    207 
    208   /**
    209    * Planchet hash we are looking for, NULL to match nothing.
    210    */
    211   const struct TALER_HashBlindedPlanchetsP *planchets_h;
    212 
    213   /**
    214    * How many times did we see it?
    215    */
    216   unsigned int matched;
    217 
    218   /**
    219    * Amount reported for it.
    220    */
    221   struct TALER_Amount amount;
    222 
    223   /**
    224    * Denomination serials reported for it.
    225    */
    226   uint64_t denom_serial;
    227 
    228   /**
    229    * Number of denominations reported for it.
    230    */
    231   size_t num_denom_serials;
    232 
    233   /**
    234    * Was a blinding seed reported for it?
    235    */
    236   bool have_seed;
    237 };
    238 
    239 
    240 /**
    241  * Callback for #TALER_EXCHANGEDB_iterate_withdrawals_above_serial_id().
    242  *
    243  * @param cls a `struct WithdrawContext *`
    244  * @param rowid row of the withdraw
    245  * @param num_denom_serials number of denominations withdrawn
    246  * @param denom_serials the denominations withdrawn
    247  * @param selected_h hash over the selected planchets
    248  * @param h_planchets hash over all planchets
    249  * @param blinding_seed blinding seed, NULL if none
    250  * @param age_proof_required was an age proof required?
    251  * @param max_age maximum age of the coins
    252  * @param noreveal_index index the exchange did not reveal
    253  * @param reserve_pub reserve that was drained
    254  * @param reserve_sig signature over the request
    255  * @param execution_date when the withdraw happened
    256  * @param amount_with_fee how much was withdrawn
    257  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
    258  */
    259 static enum GNUNET_GenericReturnValue
    260 withdraw_cb (void *cls,
    261              uint64_t rowid,
    262              size_t num_denom_serials,
    263              const uint64_t *denom_serials,
    264              const struct TALER_HashBlindedPlanchetsP *selected_h,
    265              const struct TALER_HashBlindedPlanchetsP *h_planchets,
    266              const struct TALER_BlindingMasterSeedP *blinding_seed,
    267              bool age_proof_required,
    268              uint8_t max_age,
    269              uint8_t noreveal_index,
    270              const struct TALER_ReservePublicKeyP *reserve_pub,
    271              const struct TALER_ReserveSignatureP *reserve_sig,
    272              struct GNUNET_TIME_Timestamp execution_date,
    273              const struct TALER_Amount *amount_with_fee)
    274 {
    275   struct WithdrawContext *ctx = cls;
    276 
    277   (void) rowid;
    278   (void) selected_h;
    279   (void) age_proof_required;
    280   (void) max_age;
    281   (void) noreveal_index;
    282   (void) reserve_pub;
    283   (void) reserve_sig;
    284   (void) execution_date;
    285   ctx->total++;
    286   if ( (NULL != ctx->planchets_h) &&
    287        (0 == GNUNET_memcmp (h_planchets,
    288                             ctx->planchets_h)) )
    289   {
    290     ctx->matched++;
    291     ctx->amount = *amount_with_fee;
    292     ctx->num_denom_serials = num_denom_serials;
    293     if (0 < num_denom_serials)
    294       ctx->denom_serial = denom_serials[0];
    295     ctx->have_seed = (NULL != blinding_seed);
    296   }
    297   if ( (0 != ctx->stop_after) &&
    298        (ctx->total >= ctx->stop_after) )
    299     return GNUNET_SYSERR;
    300   return GNUNET_OK;
    301 }
    302 
    303 
    304 /**
    305  * Closure for #amount_cb().
    306  */
    307 struct AmountContext
    308 {
    309   /**
    310    * How many amounts did the callback see?
    311    */
    312   unsigned int total;
    313 
    314   /**
    315    * Sum of the whole-unit parts of the amounts seen.
    316    */
    317   uint64_t value_sum;
    318 
    319   /**
    320    * Return this from the callback.
    321    */
    322   enum GNUNET_GenericReturnValue ret;
    323 };
    324 
    325 
    326 /**
    327  * Callback for #TALER_EXCHANGEDB_iterate_withdraw_amounts_for_kyc_check().
    328  *
    329  * @param cls a `struct AmountContext *`
    330  * @param amount the withdrawn amount
    331  * @param date when it was withdrawn
    332  * @return what @e ret of the closure says
    333  */
    334 static enum GNUNET_GenericReturnValue
    335 amount_cb (void *cls,
    336            const struct TALER_Amount *amount,
    337            struct GNUNET_TIME_Absolute date)
    338 {
    339   struct AmountContext *ctx = cls;
    340 
    341   (void) date;
    342   ctx->total++;
    343   ctx->value_sum += amount->value;
    344   return ctx->ret;
    345 }
    346 
    347 
    348 /**
    349  * Withdrawing from a reserve that does not exist does nothing.
    350  *
    351  * @param pg the database context
    352  * @return 0 on success
    353  */
    354 static int
    355 check_unknown_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg)
    356 {
    357   struct TALER_ReservePublicKeyP reserve_pub;
    358   struct TALER_EXCHANGEDB_Withdraw wd;
    359   struct WithdrawStatus st;
    360   struct TALER_HashBlindedPlanchetsP h;
    361 
    362   TDB_denom (pg,
    363              10,
    364              "5",
    365              "0.1",
    366              &denom);
    367   TDB_FILL (reserve_pub,
    368             1);
    369   make_withdraw (1,
    370                  "5",
    371                  &reserve_pub,
    372                  false,
    373                  &wd);
    374   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    375             run_withdraw (pg,
    376                           &wd,
    377                           &st),
    378             free_withdraw (&wd));
    379   free_withdraw (&wd);
    380   FAILIF (0 != TDB_count (pg,
    381                           "FROM withdraw"));
    382   TDB_FILL (h,
    383             1);
    384   {
    385     struct TALER_EXCHANGEDB_Withdraw got;
    386 
    387     FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    388             TALER_EXCHANGEDB_get_withdraw (pg,
    389                                            &h,
    390                                            &got));
    391   }
    392   return 0;
    393 }
    394 
    395 
    396 /**
    397  * A reserve without enough money keeps it.
    398  *
    399  * @param pg the database context
    400  * @return 0 on success
    401  */
    402 static int
    403 check_insufficient_balance (struct TALER_EXCHANGEDB_PostgresContext *pg)
    404 {
    405   struct TALER_ReservePublicKeyP reserve_pub;
    406   struct TALER_EXCHANGEDB_Withdraw wd;
    407   struct WithdrawStatus st;
    408   struct TALER_Amount expect = TDB_amount ("1");
    409 
    410   TDB_account (pg,
    411                10,
    412                &account);
    413   TDB_reserve_in (pg,
    414                   &account,
    415                   10,
    416                   "1",
    417                   &reserve_pub);
    418   make_withdraw (2,
    419                  "5",
    420                  &reserve_pub,
    421                  false,
    422                  &wd);
    423   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    424             run_withdraw (pg,
    425                           &wd,
    426                           &st),
    427             free_withdraw (&wd));
    428   free_withdraw (&wd);
    429   FAILIF (st.balance_ok);
    430   FAILIF (! st.age_ok);
    431   FAILIF (st.idempotent);
    432   FAILIF (0 != TALER_amount_cmp (&st.reserve_balance,
    433                                  &expect));
    434   FAILIF (0 != TDB_count (pg,
    435                           "FROM withdraw"));
    436   return 0;
    437 }
    438 
    439 
    440 /**
    441  * A funded reserve is drained and the withdraw is recorded.
    442  *
    443  * @param pg the database context
    444  * @return 0 on success
    445  */
    446 static int
    447 check_withdraw (struct TALER_EXCHANGEDB_PostgresContext *pg)
    448 {
    449   struct TALER_ReservePublicKeyP reserve_pub;
    450   struct TALER_EXCHANGEDB_Withdraw wd;
    451   struct TALER_EXCHANGEDB_Withdraw got;
    452   struct TALER_EXCHANGEDB_Reserve reserve;
    453   struct WithdrawStatus st;
    454   struct TALER_Amount expect_balance = TDB_amount ("5");
    455   struct TALER_Amount expect_left = TDB_amount ("5");
    456 
    457   TDB_reserve_in (pg,
    458                   &account,
    459                   11,
    460                   "10",
    461                   &reserve_pub);
    462   make_withdraw (11,
    463                  "5",
    464                  &reserve_pub,
    465                  true,
    466                  &wd);
    467   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    468             run_withdraw (pg,
    469                           &wd,
    470                           &st),
    471             free_withdraw (&wd));
    472   FAILIF_C (! st.balance_ok,
    473             free_withdraw (&wd));
    474   FAILIF_C (! st.age_ok,
    475             free_withdraw (&wd));
    476   FAILIF_C (st.idempotent,
    477             free_withdraw (&wd));
    478   FAILIF_C (st.nonce_reuse,
    479             free_withdraw (&wd));
    480   FAILIF_C (1 != TDB_count (pg,
    481                             "FROM withdraw"),
    482             free_withdraw (&wd));
    483 
    484   /* the reserve was debited */
    485   memset (&reserve,
    486           0,
    487           sizeof (reserve));
    488   reserve.pub = reserve_pub;
    489   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    490             TALER_EXCHANGEDB_get_reserve (pg,
    491                                           &reserve),
    492             free_withdraw (&wd));
    493   FAILIF_C (0 != TALER_amount_cmp (&reserve.balance,
    494                                    &expect_left),
    495             free_withdraw (&wd));
    496 
    497   /* the request can be looked up by its planchet hash */
    498   memset (&got,
    499           0,
    500           sizeof (got));
    501   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    502             TALER_EXCHANGEDB_get_withdraw (pg,
    503                                            &wd.planchets_h,
    504                                            &got),
    505             free_withdraw (&wd));
    506   FAILIF_C (0 != TALER_amount_cmp (&got.amount_with_fee,
    507                                    &expect_balance),
    508             free_withdraw (&got); free_withdraw (&wd));
    509   /* ... and names its reserve and its row */
    510   FAILIF_C (0 != GNUNET_memcmp (&got.reserve_pub,
    511                                 &reserve_pub),
    512             free_withdraw (&got); free_withdraw (&wd));
    513   FAILIF_C (0 == got.withdraw_id,
    514             free_withdraw (&got); free_withdraw (&wd));
    515   FAILIF_C (0 != GNUNET_memcmp (&got.reserve_pub,
    516                                 &reserve_pub),
    517             free_withdraw (&got); free_withdraw (&wd));
    518   FAILIF_C (0 != GNUNET_memcmp (&got.reserve_sig,
    519                                 &wd.reserve_sig),
    520             free_withdraw (&got); free_withdraw (&wd));
    521   FAILIF_C (1 != got.num_coins,
    522             free_withdraw (&got); free_withdraw (&wd));
    523   FAILIF_C (denom.serial != got.denom_serials[0],
    524             free_withdraw (&got); free_withdraw (&wd));
    525   FAILIF_C (got.no_blinding_seed,
    526             free_withdraw (&got); free_withdraw (&wd));
    527   free_withdraw (&got);
    528 
    529   /* a replay of the same request is idempotent and does not debit again */
    530   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    531             run_withdraw (pg,
    532                           &wd,
    533                           &st),
    534             free_withdraw (&wd));
    535   FAILIF_C (! st.idempotent,
    536             free_withdraw (&wd));
    537   FAILIF_C (1 != TDB_count (pg,
    538                             "FROM withdraw"),
    539             free_withdraw (&wd));
    540   memset (&reserve,
    541           0,
    542           sizeof (reserve));
    543   reserve.pub = reserve_pub;
    544   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    545             TALER_EXCHANGEDB_get_reserve (pg,
    546                                           &reserve),
    547             free_withdraw (&wd));
    548   FAILIF_C (0 != TALER_amount_cmp (&reserve.balance,
    549                                    &expect_left),
    550             free_withdraw (&wd));
    551   free_withdraw (&wd);
    552   return 0;
    553 }
    554 
    555 
    556 /**
    557  * Reusing a blinding seed for a different withdraw is refused, and the
    558  * reserve is left debited -- the caller has to roll back.
    559  *
    560  * @param pg the database context
    561  * @return 0 on success
    562  */
    563 static int
    564 check_nonce_reuse (struct TALER_EXCHANGEDB_PostgresContext *pg)
    565 {
    566   struct TALER_ReservePublicKeyP reserve_pub;
    567   struct TALER_EXCHANGEDB_Withdraw wd;
    568   struct WithdrawStatus st;
    569 
    570   TDB_reserve_in (pg,
    571                   &account,
    572                   12,
    573                   "10",
    574                   &reserve_pub);
    575   /* a fresh planchet hash, but the blinding seed of check_withdraw() */
    576   make_withdraw (13,
    577                  "5",
    578                  &reserve_pub,
    579                  true,
    580                  &wd);
    581   TDB_fill (&wd.blinding_seed,
    582             sizeof (wd.blinding_seed),
    583             11);
    584   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    585             run_withdraw (pg,
    586                           &wd,
    587                           &st),
    588             free_withdraw (&wd));
    589   FAILIF_C (! st.nonce_reuse,
    590             free_withdraw (&wd));
    591   FAILIF_C (st.idempotent,
    592             free_withdraw (&wd));
    593   free_withdraw (&wd);
    594   /* No withdraw row was written... */
    595   FAILIF (1 != TDB_count (pg,
    596                           "FROM withdraw"));
    597   /* ...but the reserve was debited before the seed was checked, so the
    598      caller has to roll the transaction back.  This is what the check is
    599      really about: the function is not safe to call outside a transaction. */
    600   {
    601     struct TALER_EXCHANGEDB_Reserve reserve;
    602     struct TALER_Amount debited = TDB_amount ("5");
    603 
    604     memset (&reserve,
    605             0,
    606             sizeof (reserve));
    607     reserve.pub = reserve_pub;
    608     FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    609             TALER_EXCHANGEDB_get_reserve (pg,
    610                                           &reserve));
    611     FAILIF (0 != TALER_amount_cmp (&reserve.balance,
    612                                    &debited));
    613   }
    614   return 0;
    615 }
    616 
    617 
    618 /**
    619  * A reserve with a birthday refuses a withdraw that commits to too high
    620  * an age.
    621  *
    622  * @param pg the database context
    623  * @return 0 on success
    624  */
    625 static int
    626 check_age_restriction (struct TALER_EXCHANGEDB_PostgresContext *pg)
    627 {
    628   struct TALER_ReservePublicKeyP reserve_pub;
    629   struct TALER_EXCHANGEDB_Withdraw wd;
    630   struct WithdrawStatus st;
    631   char *hex;
    632 
    633   TDB_reserve_in (pg,
    634                   &account,
    635                   14,
    636                   "10",
    637                   &reserve_pub);
    638   /* born 20000 days after the epoch, i.e. in 2024 */
    639   hex = TDB_hex (&reserve_pub,
    640                  sizeof (reserve_pub));
    641   FAILIF_C (GNUNET_OK !=
    642             TDB_exec (pg,
    643                       "UPDATE reserves"
    644                       " SET birthday=20000"
    645                       " WHERE reserve_pub=decode('%s','hex');",
    646                       hex),
    647             GNUNET_free (hex));
    648   GNUNET_free (hex);
    649 
    650   make_withdraw (14,
    651                  "5",
    652                  &reserve_pub,
    653                  false,
    654                  &wd);
    655   /* no age commitment at all, from a reserve that has a birthday */
    656   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    657             run_withdraw (pg,
    658                           &wd,
    659                           &st),
    660             free_withdraw (&wd));
    661   free_withdraw (&wd);
    662   FAILIF (st.age_ok);
    663   FAILIF (20000 != st.reserve_birthday);
    664   FAILIF (1 != TDB_count (pg,
    665                           "FROM withdraw"));
    666   return 0;
    667 }
    668 
    669 
    670 /**
    671  * The iterators see the withdraw, and the KYC view attributes it to the
    672  * account that funded the reserve.
    673  *
    674  * @param pg the database context
    675  * @return 0 on success
    676  */
    677 static int
    678 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
    679 {
    680   struct TALER_HashBlindedPlanchetsP h;
    681   struct WithdrawContext ctx;
    682   struct AmountContext actx;
    683 
    684   TDB_FILL (h,
    685             11);
    686   memset (&ctx,
    687           0,
    688           sizeof (ctx));
    689   ctx.planchets_h = &h;
    690   FAILIF (1 !=
    691           TALER_EXCHANGEDB_iterate_withdrawals_above_serial_id (pg,
    692                                                                 0,
    693                                                                 &withdraw_cb,
    694                                                                 &ctx));
    695   FAILIF (1 != ctx.matched);
    696   FAILIF (1 != ctx.num_denom_serials);
    697   FAILIF (denom.serial != ctx.denom_serial);
    698   FAILIF (! ctx.have_seed);
    699 
    700   memset (&ctx,
    701           0,
    702           sizeof (ctx));
    703   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    704           TALER_EXCHANGEDB_iterate_withdrawals_above_serial_id (pg,
    705                                                                 1000,
    706                                                                 &withdraw_cb,
    707                                                                 &ctx));
    708   FAILIF (0 != ctx.total);
    709 
    710   memset (&ctx,
    711           0,
    712           sizeof (ctx));
    713   ctx.stop_after = 1;
    714   FAILIF (1 !=
    715           TALER_EXCHANGEDB_iterate_withdrawals_above_serial_id (pg,
    716                                                                 0,
    717                                                                 &withdraw_cb,
    718                                                                 &ctx));
    719   FAILIF (1 != ctx.total);
    720 
    721   /* the KYC view groups by the account that funded the reserve */
    722   memset (&actx,
    723           0,
    724           sizeof (actx));
    725   actx.ret = GNUNET_OK;
    726   FAILIF (1 !=
    727           TALER_EXCHANGEDB_iterate_withdraw_amounts_for_kyc_check (
    728             pg,
    729             &account.h_normalized,
    730             GNUNET_TIME_UNIT_ZERO_ABS,
    731             &amount_cb,
    732             &actx));
    733   FAILIF (5 != actx.value_sum);
    734 
    735   /* an account nobody withdrew against has nothing */
    736   {
    737     struct TALER_NormalizedPaytoHashP other;
    738 
    739     TDB_FILL (other,
    740               77);
    741     memset (&actx,
    742             0,
    743             sizeof (actx));
    744     actx.ret = GNUNET_OK;
    745     FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    746             TALER_EXCHANGEDB_iterate_withdraw_amounts_for_kyc_check (
    747               pg,
    748               &other,
    749               GNUNET_TIME_UNIT_ZERO_ABS,
    750               &amount_cb,
    751               &actx));
    752     FAILIF (0 != actx.total);
    753   }
    754 
    755   /* a time limit in the future hides everything */
    756   memset (&actx,
    757           0,
    758           sizeof (actx));
    759   actx.ret = GNUNET_OK;
    760   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    761           TALER_EXCHANGEDB_iterate_withdraw_amounts_for_kyc_check (
    762             pg,
    763             &account.h_normalized,
    764             GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS),
    765             &amount_cb,
    766             &actx));
    767   FAILIF (0 != actx.total);
    768   return 0;
    769 }
    770 
    771 
    772 /**
    773  * The checks to run, in order.
    774  */
    775 static const struct TDB_Test tests[] = {
    776   { "withdraw-unknown-reserve",
    777     &check_unknown_reserve },
    778   { "withdraw-insufficient-balance",
    779     &check_insufficient_balance },
    780   { "withdraw-withdraw",
    781     &check_withdraw },
    782   { "withdraw-nonce-reuse",
    783     &check_nonce_reuse },
    784   { "withdraw-age-restriction",
    785     &check_age_restriction },
    786   { "withdraw-iterate",
    787     &check_iterate },
    788   { NULL, NULL }
    789 };
    790 
    791 
    792 int
    793 main (int argc,
    794       char *const *argv)
    795 {
    796   int ret;
    797 
    798   ret = TDB_main (argc,
    799                   argv,
    800                   "test-withdraw",
    801                   "Tests for the exchangedb `withdraw' table",
    802                   tests);
    803   TDB_account_free (&account);
    804   TDB_denom_free (&denom);
    805   return ret;
    806 }
    807 
    808 
    809 /* end of test_withdraw.c */