exchange

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

test_recoup.c (25022B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12 
     13   You should have received a copy of the GNU General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file exchangedb/test_recoup.c
     18  * @brief tests for the exchangedb functions whose primary table is `recoup`
     19  * @author Christian Grothoff
     20  *
     21  * Covers #TALER_EXCHANGEDB_do_recoup() and
     22  * #TALER_EXCHANGEDB_iterate_recoups_above_serial_id().
     23  *
     24  * `recoup` references `known_coins` and `withdraw`, so a full chain is
     25  * built for each check: a funded reserve, a withdraw from it and a coin.
     26  * do_recoup() moves the coin's whole remaining balance back to the reserve,
     27  * which is why the checks look at both sides afterwards.
     28  */
     29 #include "test_common.h"
     30 #include "exchange-database/do_recoup.h"
     31 #include "exchange-database/get_reserve.h"
     32 #include "exchange-database/iterate_recoups_above_serial_id.h"
     33 #include "exchange-database/iterate_records_by_table.h"
     34 #include "exchange-database/insert_records_by_table.h"
     35 
     36 
     37 /**
     38  * Account the checks fund their reserves from.
     39  */
     40 static struct TDB_Account account;
     41 
     42 
     43 /**
     44  * Denomination the checks use.
     45  */
     46 static struct TDB_Denom denom;
     47 
     48 
     49 /**
     50  * Closure for #recoup_cb().
     51  */
     52 struct RecoupContext
     53 {
     54   /**
     55    * How many rows did the callback see?
     56    */
     57   unsigned int total;
     58 
     59   /**
     60    * Stop after this many rows; 0 for no limit.
     61    */
     62   unsigned int stop_after;
     63 
     64   /**
     65    * Coin we are looking for, NULL to match nothing.
     66    */
     67   const struct TALER_CoinSpendPublicKeyP *coin_pub;
     68 
     69   /**
     70    * How many times did we see it?
     71    */
     72   unsigned int matched;
     73 
     74   /**
     75    * Amount reported for it.
     76    */
     77   struct TALER_Amount amount;
     78 
     79   /**
     80    * Reserve reported for it.
     81    */
     82   struct TALER_ReservePublicKeyP reserve_pub;
     83 
     84   /**
     85    * Blinding secret reported for it.
     86    */
     87   union GNUNET_CRYPTO_BlindingSecretP coin_blind;
     88 };
     89 
     90 
     91 /**
     92  * Callback for #TALER_EXCHANGEDB_iterate_recoups_above_serial_id().
     93  *
     94  * @param cls a `struct RecoupContext *`
     95  * @param rowid row of the recoup
     96  * @param timestamp when the recoup was requested
     97  * @param amount how much went back to the reserve
     98  * @param reserve_pub the reserve that was credited
     99  * @param coin the coin that was recouped
    100  * @param denom_pub denomination of @a coin
    101  * @param coin_sig signature over the request
    102  * @param coin_blind blinding secret of the coin
    103  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
    104  */
    105 static enum GNUNET_GenericReturnValue
    106 recoup_cb (void *cls,
    107            uint64_t rowid,
    108            struct GNUNET_TIME_Timestamp timestamp,
    109            const struct TALER_Amount *amount,
    110            const struct TALER_ReservePublicKeyP *reserve_pub,
    111            const struct TALER_CoinPublicInfo *coin,
    112            const struct TALER_DenominationPublicKey *denom_pub,
    113            const struct TALER_CoinSpendSignatureP *coin_sig,
    114            const union GNUNET_CRYPTO_BlindingSecretP *coin_blind)
    115 {
    116   struct RecoupContext *ctx = cls;
    117 
    118   (void) rowid;
    119   (void) timestamp;
    120   (void) denom_pub;
    121   (void) coin_sig;
    122   ctx->total++;
    123   if ( (NULL != ctx->coin_pub) &&
    124        (0 == GNUNET_memcmp (&coin->coin_pub,
    125                             ctx->coin_pub)) )
    126   {
    127     ctx->matched++;
    128     ctx->amount = *amount;
    129     ctx->reserve_pub = *reserve_pub;
    130     ctx->coin_blind = *coin_blind;
    131   }
    132   if ( (0 != ctx->stop_after) &&
    133        (ctx->total >= ctx->stop_after) )
    134     return GNUNET_SYSERR;
    135   return GNUNET_OK;
    136 }
    137 
    138 
    139 /**
    140  * Outcome of a recoup request.
    141  */
    142 struct RecoupStatus
    143 {
    144   /**
    145    * Was the recoup accepted?
    146    */
    147   bool recoup_ok;
    148 
    149   /**
    150    * Did something go wrong inside the database?
    151    */
    152   bool internal_failure;
    153 
    154   /**
    155    * When the recoup happened.
    156    */
    157   struct GNUNET_TIME_Timestamp recoup_timestamp;
    158 
    159   /**
    160    * Amount the recoup credited.
    161    */
    162   struct TALER_Amount recoup_amount;
    163 };
    164 
    165 
    166 /**
    167  * Recoup a coin into a reserve.
    168  *
    169  * @param pg the database context
    170  * @param reserve_pub reserve to credit
    171  * @param withdraw_id withdraw that justifies the recoup
    172  * @param coin_pub coin to recoup
    173  * @param known_coin_id row of @a coin_pub
    174  * @param seed seed for the blinding secret and coin signature
    175  * @param[out] st set to the outcome
    176  * @return transaction status
    177  */
    178 static enum GNUNET_DB_QueryStatus
    179 run_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg,
    180             const struct TALER_ReservePublicKeyP *reserve_pub,
    181             uint64_t withdraw_id,
    182             const struct TALER_CoinSpendPublicKeyP *coin_pub,
    183             uint64_t known_coin_id,
    184             uint32_t seed,
    185             struct RecoupStatus *st)
    186 {
    187   union GNUNET_CRYPTO_BlindingSecretP coin_bks;
    188   struct TALER_CoinSpendSignatureP coin_sig;
    189 
    190   TDB_fill (&coin_bks,
    191             sizeof (coin_bks),
    192             seed);
    193   TDB_fill (&coin_sig,
    194             sizeof (coin_sig),
    195             seed);
    196   memset (st,
    197           0,
    198           sizeof (*st));
    199   st->recoup_timestamp = GNUNET_TIME_timestamp_get ();
    200   return TALER_EXCHANGEDB_do_recoup (pg,
    201                                      reserve_pub,
    202                                      withdraw_id,
    203                                      0,
    204                                      &coin_bks,
    205                                      coin_pub,
    206                                      known_coin_id,
    207                                      &coin_sig,
    208                                      &st->recoup_timestamp,
    209                                      &st->recoup_amount,
    210                                      &st->recoup_ok,
    211                                      &st->internal_failure);
    212 }
    213 
    214 
    215 /**
    216  * Recouping a coin the exchange does not know is an internal failure.
    217  *
    218  * @param pg the database context
    219  * @return 0 on success
    220  */
    221 static int
    222 check_unknown_coin (struct TALER_EXCHANGEDB_PostgresContext *pg)
    223 {
    224   struct TALER_ReservePublicKeyP reserve_pub;
    225   struct TALER_CoinSpendPublicKeyP coin_pub;
    226   struct RecoupContext ctx = { 0 };
    227   struct RecoupStatus st;
    228   uint64_t withdraw_id;
    229 
    230   TDB_denom (pg,
    231              10,
    232              "5",
    233              "0.1",
    234              &denom);
    235   TDB_account (pg,
    236                10,
    237                &account);
    238   TDB_reserve_in (pg,
    239                   &account,
    240                   10,
    241                   "10",
    242                   &reserve_pub);
    243   withdraw_id = TDB_withdraw (pg,
    244                               &denom,
    245                               &reserve_pub,
    246                               10,
    247                               "5");
    248   TDB_FILL (coin_pub,
    249             99);
    250   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    251           run_recoup (pg,
    252                       &reserve_pub,
    253                       withdraw_id,
    254                       &coin_pub,
    255                       1,
    256                       99,
    257                       &st));
    258   FAILIF (! st.internal_failure);
    259   FAILIF (st.recoup_ok);
    260   FAILIF (0 != TDB_count (pg,
    261                           "FROM recoup"));
    262   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    263           TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
    264                                                             0,
    265                                                             &recoup_cb,
    266                                                             &ctx));
    267   FAILIF (0 != ctx.total);
    268   return 0;
    269 }
    270 
    271 
    272 /**
    273  * A coin that is already empty and was never recouped is refused.
    274  *
    275  * @param pg the database context
    276  * @return 0 on success
    277  */
    278 static int
    279 check_empty_coin (struct TALER_EXCHANGEDB_PostgresContext *pg)
    280 {
    281   struct TALER_ReservePublicKeyP reserve_pub;
    282   struct TALER_CoinPublicInfo coin;
    283   struct RecoupStatus st;
    284   uint64_t known_coin_id;
    285   uint64_t withdraw_id;
    286   char *hex;
    287 
    288   TDB_reserve_in (pg,
    289                   &account,
    290                   11,
    291                   "10",
    292                   &reserve_pub);
    293   withdraw_id = TDB_withdraw (pg,
    294                               &denom,
    295                               &reserve_pub,
    296                               11,
    297                               "5");
    298   TDB_coin (pg,
    299             &denom,
    300             20,
    301             &coin,
    302             &known_coin_id);
    303   hex = TDB_hex (&coin.coin_pub,
    304                  sizeof (coin.coin_pub));
    305   FAILIF_C (GNUNET_OK !=
    306             TDB_exec (pg,
    307                       "UPDATE known_coins"
    308                       " SET remaining=ROW(0,0)::taler_amount"
    309                       " WHERE coin_pub=decode('%s','hex');",
    310                       hex),
    311             GNUNET_free (hex); TDB_coin_free (&coin));
    312   GNUNET_free (hex);
    313   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    314             run_recoup (pg,
    315                         &reserve_pub,
    316                         withdraw_id,
    317                         &coin.coin_pub,
    318                         known_coin_id,
    319                         20,
    320                         &st),
    321             TDB_coin_free (&coin));
    322   TDB_coin_free (&coin);
    323   FAILIF (st.internal_failure);
    324   /* nothing to give back, and no earlier recoup to report */
    325   FAILIF (st.recoup_ok);
    326   FAILIF (0 != TDB_count (pg,
    327                           "FROM recoup"));
    328   return 0;
    329 }
    330 
    331 
    332 /**
    333  * A funded coin is drained and the reserve is credited.
    334  *
    335  * @param pg the database context
    336  * @return 0 on success
    337  */
    338 static int
    339 check_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg)
    340 {
    341   struct TALER_ReservePublicKeyP reserve_pub;
    342   struct TALER_CoinPublicInfo coin;
    343   struct TALER_EXCHANGEDB_Reserve reserve;
    344   struct RecoupStatus st;
    345   struct RecoupContext ctx;
    346   struct TALER_Amount expect_coin = TDB_amount ("5");
    347   struct TALER_Amount expect_reserve = TDB_amount ("10");
    348   union GNUNET_CRYPTO_BlindingSecretP expect_bks;
    349   uint64_t known_coin_id;
    350   uint64_t withdraw_id;
    351   char *hex;
    352 
    353   TDB_reserve_in (pg,
    354                   &account,
    355                   12,
    356                   "10",
    357                   &reserve_pub);
    358   withdraw_id = TDB_withdraw (pg,
    359                               &denom,
    360                               &reserve_pub,
    361                               12,
    362                               "5");
    363   TDB_coin (pg,
    364             &denom,
    365             21,
    366             &coin,
    367             &known_coin_id);
    368   /* EUR:10 came in and EUR:5 was withdrawn, so EUR:5 is left */
    369   memset (&reserve,
    370           0,
    371           sizeof (reserve));
    372   reserve.pub = reserve_pub;
    373   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    374             TALER_EXCHANGEDB_get_reserve (pg,
    375                                           &reserve),
    376             TDB_coin_free (&coin));
    377 
    378   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    379             run_recoup (pg,
    380                         &reserve_pub,
    381                         withdraw_id,
    382                         &coin.coin_pub,
    383                         known_coin_id,
    384                         21,
    385                         &st),
    386             TDB_coin_free (&coin));
    387   FAILIF_C (st.internal_failure,
    388             TDB_coin_free (&coin));
    389   FAILIF_C (! st.recoup_ok,
    390             TDB_coin_free (&coin));
    391   FAILIF_C (1 != TDB_count (pg,
    392                             "FROM recoup"),
    393             TDB_coin_free (&coin));
    394 
    395   /* the coin is empty... */
    396   hex = TDB_hex (&coin.coin_pub,
    397                  sizeof (coin.coin_pub));
    398   FAILIF_C (1 != TDB_count (pg,
    399                             "FROM known_coins"
    400                             " WHERE coin_pub=decode('%s','hex')"
    401                             "   AND remaining=ROW(0,0)::taler_amount",
    402                             hex),
    403             GNUNET_free (hex); TDB_coin_free (&coin));
    404   GNUNET_free (hex);
    405   /* ...and the reserve got the money */
    406   memset (&reserve,
    407           0,
    408           sizeof (reserve));
    409   reserve.pub = reserve_pub;
    410   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    411             TALER_EXCHANGEDB_get_reserve (pg,
    412                                           &reserve),
    413             TDB_coin_free (&coin));
    414   FAILIF_C (0 != TALER_amount_cmp (&reserve.balance,
    415                                    &expect_reserve),
    416             TDB_coin_free (&coin));
    417 
    418   /* the recoup is reported with everything it was made with */
    419   TDB_FILL (expect_bks,
    420             21);
    421   memset (&ctx,
    422           0,
    423           sizeof (ctx));
    424   ctx.coin_pub = &coin.coin_pub;
    425   FAILIF_C (0 >=
    426             TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
    427                                                               0,
    428                                                               &recoup_cb,
    429                                                               &ctx),
    430             TDB_coin_free (&coin));
    431   FAILIF_C (1 != ctx.matched,
    432             TDB_coin_free (&coin));
    433   FAILIF_C (0 != TALER_amount_cmp (&ctx.amount,
    434                                    &expect_coin),
    435             TDB_coin_free (&coin));
    436   FAILIF_C (0 != GNUNET_memcmp (&ctx.reserve_pub,
    437                                 &reserve_pub),
    438             TDB_coin_free (&coin));
    439   FAILIF_C (0 != GNUNET_memcmp (&ctx.coin_blind,
    440                                 &expect_bks),
    441             TDB_coin_free (&coin));
    442 
    443   /* recouping the same coin again finds the earlier recoup */
    444   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    445             run_recoup (pg,
    446                         &reserve_pub,
    447                         withdraw_id,
    448                         &coin.coin_pub,
    449                         known_coin_id,
    450                         21,
    451                         &st),
    452             TDB_coin_free (&coin));
    453   FAILIF_C (! st.recoup_ok,
    454             TDB_coin_free (&coin));
    455   FAILIF_C (st.internal_failure,
    456             TDB_coin_free (&coin));
    457   FAILIF_C (1 != TDB_count (pg,
    458                             "FROM recoup"),
    459             TDB_coin_free (&coin));
    460 
    461   /* ...but a recoup of the same coin for another withdraw operation
    462      is not the same recoup: the coin has nothing left for it */
    463   {
    464     uint64_t other_withdraw_id;
    465 
    466     other_withdraw_id = TDB_withdraw (pg,
    467                                       &denom,
    468                                       &reserve_pub,
    469                                       13,
    470                                       "5");
    471     FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    472               run_recoup (pg,
    473                           &reserve_pub,
    474                           other_withdraw_id,
    475                           &coin.coin_pub,
    476                           known_coin_id,
    477                           21,
    478                           &st),
    479               TDB_coin_free (&coin));
    480     FAILIF_C (st.recoup_ok,
    481               TDB_coin_free (&coin));
    482     FAILIF_C (st.internal_failure,
    483               TDB_coin_free (&coin));
    484     FAILIF_C (1 != TDB_count (pg,
    485                               "FROM recoup"),
    486               TDB_coin_free (&coin));
    487   }
    488   TDB_coin_free (&coin);
    489   return 0;
    490 }
    491 
    492 
    493 /**
    494  * Closure for #replication_cb().
    495  */
    496 struct ReplicationContext
    497 {
    498   /**
    499    * Coin index every record must carry.
    500    */
    501   uint32_t expected_coin_index;
    502 
    503   /**
    504    * Records seen.
    505    */
    506   unsigned int seen;
    507 
    508   /**
    509    * Set if a record carried another coin index.
    510    */
    511   bool mismatch;
    512 };
    513 
    514 
    515 /**
    516  * Counts the replication records of the recoup table and checks
    517  * their coin index.
    518  *
    519  * @param cls a `struct ReplicationContext *`
    520  * @param td the record
    521  * @return #GNUNET_OK to continue
    522  */
    523 static int
    524 replication_cb (void *cls,
    525                 const struct TALER_EXCHANGEDB_TableData *td)
    526 {
    527   struct ReplicationContext *rc = cls;
    528 
    529   if (TALER_EXCHANGEDB_RT_RECOUP != td->table)
    530   {
    531     rc->mismatch = true;
    532     return GNUNET_OK;
    533   }
    534   rc->seen++;
    535   if (rc->expected_coin_index != td->details.recoup.coin_index)
    536     rc->mismatch = true;
    537   return GNUNET_OK;
    538 }
    539 
    540 
    541 /**
    542  * Replication carries the coin index of a recoup.
    543  *
    544  * @param pg the database context
    545  * @return 0 on success
    546  */
    547 static int
    548 check_replication (struct TALER_EXCHANGEDB_PostgresContext *pg)
    549 {
    550   struct ReplicationContext rc = {
    551     .expected_coin_index = 0
    552   };
    553   struct TALER_EXCHANGEDB_TableData td = {
    554     .table = TALER_EXCHANGEDB_RT_RECOUP,
    555     .serial = 77,
    556     .details.recoup.coin_index = 7,
    557     .details.recoup.amount = TDB_amount ("5"),
    558     .details.recoup.timestamp = GNUNET_TIME_timestamp_get ()
    559   };
    560 
    561   /* the row made by check_recoup() is reported with its index 0 */
    562   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    563           TALER_EXCHANGEDB_iterate_records_by_table (pg,
    564                                                      TALER_EXCHANGEDB_RT_RECOUP,
    565                                                      0,
    566                                                      &replication_cb,
    567                                                      &rc));
    568   FAILIF (1 != rc.seen);
    569   /* a replicated row keeps its index */
    570   {
    571     struct TALER_CoinPublicInfo coin;
    572     uint64_t known_coin_id;
    573 
    574     TDB_coin (pg,
    575               &denom,
    576               21,
    577               &coin,
    578               &known_coin_id);
    579     td.details.recoup.coin_pub = coin.coin_pub;
    580     TDB_coin_free (&coin);
    581   }
    582   {
    583     struct TALER_ReservePublicKeyP reserve_pub;
    584 
    585     TDB_reserve_in (pg,
    586                     &account,
    587                     14,
    588                     "10",
    589                     &reserve_pub);
    590     td.details.recoup.withdraw_serial_id = TDB_withdraw (pg,
    591                                                          &denom,
    592                                                          &reserve_pub,
    593                                                          14,
    594                                                          "5");
    595   }
    596   TDB_FILL (td.details.recoup.coin_sig,
    597             77);
    598   TDB_FILL (td.details.recoup.coin_blind,
    599             77);
    600   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    601           TALER_EXCHANGEDB_insert_records_by_table (pg,
    602                                                     &td));
    603   FAILIF (1 != TDB_count (pg,
    604                           "FROM recoup WHERE coin_index=7"));
    605   rc.seen = 0;
    606   rc.expected_coin_index = 7;
    607   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    608           TALER_EXCHANGEDB_iterate_records_by_table (pg,
    609                                                      TALER_EXCHANGEDB_RT_RECOUP,
    610                                                      76,
    611                                                      &replication_cb,
    612                                                      &rc));
    613   FAILIF (1 != rc.seen);
    614   FAILIF (rc.mismatch);
    615   return 0;
    616 }
    617 
    618 
    619 /**
    620  * The iterator's serial bound and abort return behave as documented.
    621  *
    622  * @param pg the database context
    623  * @return 0 on success
    624  */
    625 static int
    626 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
    627 {
    628   struct RecoupContext ctx;
    629 
    630   memset (&ctx,
    631           0,
    632           sizeof (ctx));
    633   FAILIF (1 !=
    634           TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
    635                                                             0,
    636                                                             &recoup_cb,
    637                                                             &ctx));
    638   memset (&ctx,
    639           0,
    640           sizeof (ctx));
    641   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    642           TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
    643                                                             1000,
    644                                                             &recoup_cb,
    645                                                             &ctx));
    646   FAILIF (0 != ctx.total);
    647   memset (&ctx,
    648           0,
    649           sizeof (ctx));
    650   ctx.stop_after = 1;
    651   FAILIF (1 !=
    652           TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
    653                                                             0,
    654                                                             &recoup_cb,
    655                                                             &ctx));
    656   FAILIF (1 != ctx.total);
    657   return 0;
    658 }
    659 
    660 
    661 /**
    662  * A recouped coin that was credited again (a refund does that) can be
    663  * recouped a second time.  Replaying the request afterwards must report
    664  * the latest recoup, not an arbitrary earlier one.
    665  *
    666  * @param pg the database context
    667  * @return 0 on success
    668  */
    669 static int
    670 check_replay_latest (struct TALER_EXCHANGEDB_PostgresContext *pg)
    671 {
    672   struct TALER_ReservePublicKeyP reserve_pub;
    673   struct TALER_CoinPublicInfo coin;
    674   struct TALER_EXCHANGEDB_Reserve reserve;
    675   struct RecoupStatus st;
    676   struct TALER_Amount expect_first = TDB_amount ("5");
    677   struct TALER_Amount expect_second = TDB_amount ("2");
    678   struct TALER_Amount expect_reserve = TDB_amount ("12");
    679   uint64_t known_coin_id;
    680   uint64_t withdraw_id;
    681   char *hex;
    682 
    683   TDB_reserve_in (pg,
    684                   &account,
    685                   15,
    686                   "10",
    687                   &reserve_pub);
    688   withdraw_id = TDB_withdraw (pg,
    689                               &denom,
    690                               &reserve_pub,
    691                               15,
    692                               "5");
    693   TDB_coin (pg,
    694             &denom,
    695             22,
    696             &coin,
    697             &known_coin_id);
    698   hex = TDB_hex (&coin.coin_pub,
    699                  sizeof (coin.coin_pub));
    700 
    701   /* first recoup drains the coin's EUR:5 */
    702   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    703             run_recoup (pg,
    704                         &reserve_pub,
    705                         withdraw_id,
    706                         &coin.coin_pub,
    707                         known_coin_id,
    708                         22,
    709                         &st),
    710             GNUNET_free (hex); TDB_coin_free (&coin));
    711   FAILIF_C ( (! st.recoup_ok) ||
    712              (st.internal_failure) ||
    713              (0 != TALER_amount_cmp (&st.recoup_amount,
    714                                      &expect_first)),
    715              GNUNET_free (hex); TDB_coin_free (&coin));
    716 
    717   /* the coin gets EUR:2 back, as a refund would do */
    718   FAILIF_C (GNUNET_OK !=
    719             TDB_exec (pg,
    720                       "UPDATE known_coins"
    721                       " SET remaining=ROW(2,0)::taler_amount"
    722                       " WHERE coin_pub=decode('%s','hex');",
    723                       hex),
    724             GNUNET_free (hex); TDB_coin_free (&coin));
    725 
    726   /* second recoup for the same withdraw drains the EUR:2 */
    727   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    728             run_recoup (pg,
    729                         &reserve_pub,
    730                         withdraw_id,
    731                         &coin.coin_pub,
    732                         known_coin_id,
    733                         22,
    734                         &st),
    735             GNUNET_free (hex); TDB_coin_free (&coin));
    736   FAILIF_C ( (! st.recoup_ok) ||
    737              (st.internal_failure) ||
    738              (0 != TALER_amount_cmp (&st.recoup_amount,
    739                                      &expect_second)),
    740              GNUNET_free (hex); TDB_coin_free (&coin));
    741   FAILIF_C (2 != TDB_count (pg,
    742                             "FROM recoup"
    743                             " WHERE coin_pub=decode('%s','hex')",
    744                             hex),
    745             GNUNET_free (hex); TDB_coin_free (&coin));
    746 
    747   /* replaying the request reports the latest recoup... */
    748   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    749             run_recoup (pg,
    750                         &reserve_pub,
    751                         withdraw_id,
    752                         &coin.coin_pub,
    753                         known_coin_id,
    754                         22,
    755                         &st),
    756             GNUNET_free (hex); TDB_coin_free (&coin));
    757   FAILIF_C ( (! st.recoup_ok) ||
    758              (st.internal_failure),
    759              GNUNET_free (hex); TDB_coin_free (&coin));
    760   FAILIF_C (0 != TALER_amount_cmp (&st.recoup_amount,
    761                                    &expect_second),
    762             GNUNET_free (hex); TDB_coin_free (&coin));
    763   /* ...and does not touch the tables again */
    764   FAILIF_C (2 != TDB_count (pg,
    765                             "FROM recoup"
    766                             " WHERE coin_pub=decode('%s','hex')",
    767                             hex),
    768             GNUNET_free (hex); TDB_coin_free (&coin));
    769   GNUNET_free (hex);
    770   TDB_coin_free (&coin);
    771   /* EUR:10 in, EUR:5 withdrawn, EUR:5 and EUR:2 recouped */
    772   memset (&reserve,
    773           0,
    774           sizeof (reserve));
    775   reserve.pub = reserve_pub;
    776   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    777           TALER_EXCHANGEDB_get_reserve (pg,
    778                                         &reserve));
    779   FAILIF (0 != TALER_amount_cmp (&reserve.balance,
    780                                  &expect_reserve));
    781   return 0;
    782 }
    783 
    784 
    785 /**
    786  * The checks to run, in order.
    787  */
    788 static const struct TDB_Test tests[] = {
    789   { "recoup-unknown-coin",
    790     &check_unknown_coin },
    791   { "recoup-empty-coin",
    792     &check_empty_coin },
    793   { "recoup-recoup",
    794     &check_recoup },
    795   { "recoup-iterate",
    796     &check_iterate },
    797   { "recoup-replication",
    798     &check_replication },
    799   { "recoup-replay-latest",
    800     &check_replay_latest },
    801   { NULL, NULL }
    802 };
    803 
    804 
    805 int
    806 main (int argc,
    807       char *const *argv)
    808 {
    809   int ret;
    810 
    811   ret = TDB_main (argc,
    812                   argv,
    813                   "test-recoup",
    814                   "Tests for the exchangedb `recoup' table",
    815                   tests);
    816   TDB_account_free (&account);
    817   TDB_denom_free (&denom);
    818   return ret;
    819 }
    820 
    821 
    822 /* end of test_recoup.c */