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


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