exchange

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

test_recoup_refresh.c (24562B)


      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_refresh.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `recoup_refresh`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_do_recoup_refresh() and
     23  * #TALER_EXCHANGEDB_iterate_recoup_refreshes_above_serial_id().
     24  *
     25  * `recoup_refresh` references `known_coins` and `refresh`, so each check
     26  * builds a melt first.  do_recoup_refresh() moves the fresh coin's whole
     27  * remaining balance back to the old coin.
     28  */
     29 #include "test_common.h"
     30 #include "exchange-database/do_recoup_refresh.h"
     31 #include "exchange-database/do_refresh.h"
     32 #include "exchange-database/iterate_recoup_refreshes_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  * Melt @a coin and return the row of the melt.
     51  *
     52  * @param pg the database context
     53  * @param coin coin to melt
     54  * @param seed seed for the commitment and signatures
     55  * @param amount how much to melt, e.g. "1"
     56  * @return row of the melt in `refresh`
     57  */
     58 static uint64_t
     59 melt (struct TALER_EXCHANGEDB_PostgresContext *pg,
     60       const struct TALER_CoinPublicInfo *coin,
     61       uint32_t seed,
     62       const char *amount)
     63 {
     64   struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS rf;
     65   struct TALER_BlindedDenominationSignature denom_sig;
     66   struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
     67   struct TALER_Amount coin_balance;
     68   uint64_t denom_serial = denom.serial;
     69   uint64_t refresh_id;
     70   uint32_t noreveal_index;
     71   bool found;
     72   bool zombie_required = false;
     73   bool nonce_reuse;
     74   bool balance_ok;
     75 
     76   memset (&rf,
     77           0,
     78           sizeof (rf));
     79   rf.coin.coin_pub = coin->coin_pub;
     80   rf.coin.denom_pub_hash = coin->denom_pub_hash;
     81   rf.coin.no_age_commitment = coin->no_age_commitment;
     82   TDB_fill (&rf.coin_sig,
     83             sizeof (rf.coin_sig),
     84             seed);
     85   TDB_fill (&rf.rc,
     86             sizeof (rf.rc),
     87             seed);
     88   TDB_fill (&rf.refresh_seed,
     89             sizeof (rf.refresh_seed),
     90             seed);
     91   TDB_fill (&rf.planchets_h,
     92             sizeof (rf.planchets_h),
     93             seed);
     94   TDB_fill (&rf.selected_h,
     95             sizeof (rf.selected_h),
     96             seed + 1);
     97   rf.amount_with_fee = TDB_amount (amount);
     98   rf.num_coins = 1;
     99   rf.denom_serials = &denom_serial;
    100   TDB_blinded_denom_sig (seed,
    101                          &denom_sig);
    102   rf.denom_sigs = &denom_sig;
    103   rf.noreveal_index = 0;
    104   rf.is_v27_refresh = true;
    105   rf.no_blinding_seed = true;
    106   GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
    107                  TALER_EXCHANGEDB_do_refresh (pg,
    108                                               &rf,
    109                                               &now,
    110                                               &found,
    111                                               &noreveal_index,
    112                                               &zombie_required,
    113                                               &nonce_reuse,
    114                                               &balance_ok,
    115                                               &coin_balance));
    116   GNUNET_assert (balance_ok);
    117   TALER_blinded_denom_sig_free (&denom_sig);
    118   {
    119     struct GNUNET_PQ_QueryParam params[] = {
    120       GNUNET_PQ_query_param_auto_from_type (&rf.rc),
    121       GNUNET_PQ_query_param_end
    122     };
    123     struct GNUNET_PQ_ResultSpec rs[] = {
    124       GNUNET_PQ_result_spec_uint64 ("refresh_id",
    125                                     &refresh_id),
    126       GNUNET_PQ_result_spec_end
    127     };
    128 
    129     GNUNET_assert (GNUNET_OK ==
    130                    GNUNET_PQ_prepare_anon (pg->conn,
    131                                            "SELECT refresh_id"
    132                                            " FROM refresh"
    133                                            " WHERE rc=$1;"));
    134     GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
    135                    GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
    136                                                              "",
    137                                                              params,
    138                                                              rs));
    139   }
    140   return refresh_id;
    141 }
    142 
    143 
    144 /**
    145  * Callback for
    146  * #TALER_EXCHANGEDB_iterate_recoup_refreshes_above_serial_id().
    147  *
    148  * @param cls a pointer to an `unsigned int` counter
    149  * @param rowid row of the recoup
    150  * @param timestamp when it happened
    151  * @param amount how much went back
    152  * @param old_coin_pub the old coin
    153  * @param old_denom_pub_hash denomination of the old coin
    154  * @param coin the fresh coin
    155  * @param denom_pub denomination of @a coin
    156  * @param coin_sig signature over the request
    157  * @param coin_blind blinding secret of the fresh coin
    158  * @return #GNUNET_OK
    159  */
    160 static enum GNUNET_GenericReturnValue
    161 recoup_refresh_cb (void *cls,
    162                    uint64_t rowid,
    163                    struct GNUNET_TIME_Timestamp timestamp,
    164                    const struct TALER_Amount *amount,
    165                    const struct TALER_CoinSpendPublicKeyP *old_coin_pub,
    166                    const struct TALER_DenominationHashP *old_denom_pub_hash,
    167                    const struct TALER_CoinPublicInfo *coin,
    168                    const struct TALER_DenominationPublicKey *denom_pub,
    169                    const struct TALER_CoinSpendSignatureP *coin_sig,
    170                    const union GNUNET_CRYPTO_BlindingSecretP *coin_blind)
    171 {
    172   unsigned int *total = cls;
    173 
    174   (void) rowid;
    175   (void) timestamp;
    176   (void) amount;
    177   (void) old_coin_pub;
    178   (void) old_denom_pub_hash;
    179   (void) coin;
    180   (void) denom_pub;
    181   (void) coin_sig;
    182   (void) coin_blind;
    183   (*total)++;
    184   return GNUNET_OK;
    185 }
    186 
    187 
    188 /**
    189  * Outcome of a recoup-refresh request.
    190  */
    191 struct RecoupStatus
    192 {
    193   /**
    194    * Was the recoup accepted?
    195    */
    196   bool recoup_ok;
    197 
    198   /**
    199    * Did something go wrong inside the database?
    200    */
    201   bool internal_failure;
    202 
    203   /**
    204    * When the recoup happened.
    205    */
    206   struct GNUNET_TIME_Timestamp recoup_timestamp;
    207 
    208   /**
    209    * Amount the recoup credited.
    210    */
    211   struct TALER_Amount recoup_amount;
    212 };
    213 
    214 
    215 /**
    216  * Recoup a fresh coin back onto the coin it was refreshed from.
    217  *
    218  * @param pg the database context
    219  * @param old_coin_pub coin to credit
    220  * @param refresh_id melt that justifies the recoup
    221  * @param coin_pub fresh coin to drain
    222  * @param known_coin_id row of @a coin_pub
    223  * @param seed seed for the blinding secret and coin signature
    224  * @param[out] st set to the outcome
    225  * @return transaction status
    226  */
    227 static enum GNUNET_DB_QueryStatus
    228 run_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg,
    229             const struct TALER_CoinSpendPublicKeyP *old_coin_pub,
    230             uint64_t refresh_id,
    231             const struct TALER_CoinSpendPublicKeyP *coin_pub,
    232             uint64_t known_coin_id,
    233             uint32_t seed,
    234             struct RecoupStatus *st)
    235 {
    236   union GNUNET_CRYPTO_BlindingSecretP coin_bks;
    237   struct TALER_CoinSpendSignatureP coin_sig;
    238 
    239   TDB_fill (&coin_bks,
    240             sizeof (coin_bks),
    241             seed);
    242   TDB_fill (&coin_sig,
    243             sizeof (coin_sig),
    244             seed);
    245   memset (st,
    246           0,
    247           sizeof (*st));
    248   st->recoup_timestamp = GNUNET_TIME_timestamp_get ();
    249   return TALER_EXCHANGEDB_do_recoup_refresh (pg,
    250                                              old_coin_pub,
    251                                              refresh_id,
    252                                              0,
    253                                              &coin_bks,
    254                                              coin_pub,
    255                                              known_coin_id,
    256                                              &coin_sig,
    257                                              &st->recoup_timestamp,
    258                                              &st->recoup_amount,
    259                                              &st->recoup_ok,
    260                                              &st->internal_failure);
    261 }
    262 
    263 
    264 /**
    265  * Recouping a coin the exchange does not know is an internal failure.
    266  *
    267  * @param pg the database context
    268  * @return 0 on success
    269  */
    270 static int
    271 check_unknown_coin (struct TALER_EXCHANGEDB_PostgresContext *pg)
    272 {
    273   struct TALER_ReservePublicKeyP reserve_pub;
    274   struct TALER_CoinPublicInfo old_coin;
    275   struct TALER_CoinSpendPublicKeyP coin_pub;
    276   struct RecoupStatus st;
    277   uint64_t refresh_id;
    278 
    279   TDB_denom (pg,
    280              10,
    281              "5",
    282              "0.1",
    283              &denom);
    284   TDB_account (pg,
    285                10,
    286                &account);
    287   TDB_reserve_in (pg,
    288                   &account,
    289                   10,
    290                   "10",
    291                   &reserve_pub);
    292   TDB_coin (pg,
    293             &denom,
    294             20,
    295             &old_coin,
    296             NULL);
    297   refresh_id = melt (pg,
    298                      &old_coin,
    299                      1,
    300                      "1");
    301   TDB_FILL (coin_pub,
    302             99);
    303   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    304             run_recoup (pg,
    305                         &old_coin.coin_pub,
    306                         refresh_id,
    307                         &coin_pub,
    308                         1,
    309                         99,
    310                         &st),
    311             TDB_coin_free (&old_coin));
    312   TDB_coin_free (&old_coin);
    313   FAILIF (! st.internal_failure);
    314   FAILIF (st.recoup_ok);
    315   FAILIF (0 != TDB_count (pg,
    316                           "FROM recoup_refresh"));
    317   return 0;
    318 }
    319 
    320 
    321 /**
    322  * A fresh coin with nothing left on it and no earlier recoup is refused.
    323  *
    324  * @param pg the database context
    325  * @return 0 on success
    326  */
    327 static int
    328 check_empty_coin (struct TALER_EXCHANGEDB_PostgresContext *pg)
    329 {
    330   struct TALER_CoinPublicInfo old_coin;
    331   struct TALER_CoinPublicInfo fresh;
    332   struct RecoupStatus st;
    333   uint64_t known_coin_id;
    334   uint64_t refresh_id;
    335   char *hex;
    336 
    337   TDB_coin (pg,
    338             &denom,
    339             21,
    340             &old_coin,
    341             NULL);
    342   refresh_id = melt (pg,
    343                      &old_coin,
    344                      2,
    345                      "1");
    346   TDB_coin (pg,
    347             &denom,
    348             22,
    349             &fresh,
    350             &known_coin_id);
    351   hex = TDB_hex (&fresh.coin_pub,
    352                  sizeof (fresh.coin_pub));
    353   FAILIF_C (GNUNET_OK !=
    354             TDB_exec (pg,
    355                       "UPDATE known_coins"
    356                       " SET remaining=ROW(0,0)::taler_amount"
    357                       " WHERE coin_pub=decode('%s','hex');",
    358                       hex),
    359             GNUNET_free (hex);
    360             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    361   GNUNET_free (hex);
    362   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    363             run_recoup (pg,
    364                         &old_coin.coin_pub,
    365                         refresh_id,
    366                         &fresh.coin_pub,
    367                         known_coin_id,
    368                         22,
    369                         &st),
    370             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    371   TDB_coin_free (&fresh);
    372   TDB_coin_free (&old_coin);
    373   FAILIF (st.internal_failure);
    374   FAILIF (st.recoup_ok);
    375   FAILIF (0 != TDB_count (pg,
    376                           "FROM recoup_refresh"));
    377   return 0;
    378 }
    379 
    380 
    381 /**
    382  * A funded fresh coin is drained back onto the old coin.
    383  *
    384  * @param pg the database context
    385  * @return 0 on success
    386  */
    387 static int
    388 check_recoup_refresh (struct TALER_EXCHANGEDB_PostgresContext *pg)
    389 {
    390   struct TALER_CoinPublicInfo old_coin;
    391   struct TALER_CoinPublicInfo fresh;
    392   struct RecoupStatus st;
    393   uint64_t known_coin_id;
    394   uint64_t refresh_id;
    395   char *hex;
    396 
    397   TDB_coin (pg,
    398             &denom,
    399             23,
    400             &old_coin,
    401             NULL);
    402   refresh_id = melt (pg,
    403                      &old_coin,
    404                      3,
    405                      "1");
    406   TDB_coin (pg,
    407             &denom,
    408             24,
    409             &fresh,
    410             &known_coin_id);
    411   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    412             run_recoup (pg,
    413                         &old_coin.coin_pub,
    414                         refresh_id,
    415                         &fresh.coin_pub,
    416                         known_coin_id,
    417                         24,
    418                         &st),
    419             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    420   FAILIF_C (st.internal_failure,
    421             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    422   FAILIF_C (! st.recoup_ok,
    423             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    424   FAILIF_C (1 != TDB_count (pg,
    425                             "FROM recoup_refresh"),
    426             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    427 
    428   /* the fresh coin is empty */
    429   hex = TDB_hex (&fresh.coin_pub,
    430                  sizeof (fresh.coin_pub));
    431   FAILIF_C (1 != TDB_count (pg,
    432                             "FROM known_coins"
    433                             " WHERE coin_pub=decode('%s','hex')"
    434                             "   AND remaining=ROW(0,0)::taler_amount",
    435                             hex),
    436             GNUNET_free (hex);
    437             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    438   GNUNET_free (hex);
    439   /* the old coin got the EUR:5 back on top of the EUR:4 it had left */
    440   hex = TDB_hex (&old_coin.coin_pub,
    441                  sizeof (old_coin.coin_pub));
    442   FAILIF_C (1 != TDB_count (pg,
    443                             "FROM known_coins"
    444                             " WHERE coin_pub=decode('%s','hex')"
    445                             "   AND remaining=ROW(9,0)::taler_amount",
    446                             hex),
    447             GNUNET_free (hex);
    448             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    449   GNUNET_free (hex);
    450 
    451   /* recouping the same coin again finds the earlier recoup */
    452   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    453             run_recoup (pg,
    454                         &old_coin.coin_pub,
    455                         refresh_id,
    456                         &fresh.coin_pub,
    457                         known_coin_id,
    458                         24,
    459                         &st),
    460             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    461   FAILIF_C (! st.recoup_ok,
    462             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    463   FAILIF_C (1 != TDB_count (pg,
    464                             "FROM recoup_refresh"),
    465             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    466 
    467   /* ...but a recoup of the same coin for another refresh operation
    468      is not the same recoup: the coin has nothing left for it */
    469   {
    470     uint64_t other_refresh_id;
    471 
    472     other_refresh_id = melt (pg,
    473                              &old_coin,
    474                              5,
    475                              "1");
    476     FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    477               run_recoup (pg,
    478                           &old_coin.coin_pub,
    479                           other_refresh_id,
    480                           &fresh.coin_pub,
    481                           known_coin_id,
    482                           24,
    483                           &st),
    484               TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    485     FAILIF_C (st.recoup_ok,
    486               TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    487     FAILIF_C (st.internal_failure,
    488               TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    489     FAILIF_C (1 != TDB_count (pg,
    490                               "FROM recoup_refresh"),
    491               TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    492   }
    493   TDB_coin_free (&fresh);
    494   TDB_coin_free (&old_coin);
    495   return 0;
    496 }
    497 
    498 
    499 /**
    500  * Closure for #replication_cb().
    501  */
    502 struct ReplicationContext
    503 {
    504   /**
    505    * Coin index every record must carry.
    506    */
    507   uint32_t expected_coin_index;
    508 
    509   /**
    510    * Records seen.
    511    */
    512   unsigned int seen;
    513 
    514   /**
    515    * Set if a record carried another coin index.
    516    */
    517   bool mismatch;
    518 };
    519 
    520 
    521 /**
    522  * Counts the replication records of the recoup table and checks
    523  * their coin index.
    524  *
    525  * @param cls a `struct ReplicationContext *`
    526  * @param td the record
    527  * @return #GNUNET_OK to continue
    528  */
    529 static int
    530 replication_cb (void *cls,
    531                 const struct TALER_EXCHANGEDB_TableData *td)
    532 {
    533   struct ReplicationContext *rc = cls;
    534 
    535   if (TALER_EXCHANGEDB_RT_RECOUP_REFRESH != td->table)
    536   {
    537     rc->mismatch = true;
    538     return GNUNET_OK;
    539   }
    540   rc->seen++;
    541   if (rc->expected_coin_index != td->details.recoup_refresh.coin_index)
    542     rc->mismatch = true;
    543   return GNUNET_OK;
    544 }
    545 
    546 
    547 /**
    548  * Replication carries the coin index of a recoup-refresh.
    549  *
    550  * @param pg the database context
    551  * @return 0 on success
    552  */
    553 static int
    554 check_replication (struct TALER_EXCHANGEDB_PostgresContext *pg)
    555 {
    556   struct ReplicationContext rc = {
    557     .expected_coin_index = 0
    558   };
    559   struct TALER_EXCHANGEDB_TableData td = {
    560     .table = TALER_EXCHANGEDB_RT_RECOUP_REFRESH,
    561     .serial = 77,
    562     .details.recoup_refresh.coin_index = 7,
    563     .details.recoup_refresh.amount = TDB_amount ("1"),
    564     .details.recoup_refresh.recoup_timestamp = GNUNET_TIME_timestamp_get ()
    565   };
    566   struct TALER_CoinPublicInfo old_coin;
    567   struct TALER_CoinPublicInfo fresh;
    568 
    569   /* the row made by check_recoup_refresh() is reported with its index 0 */
    570   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    571           TALER_EXCHANGEDB_iterate_records_by_table (
    572             pg,
    573             TALER_EXCHANGEDB_RT_RECOUP_REFRESH,
    574             0,
    575             &replication_cb,
    576             &rc));
    577   FAILIF (1 != rc.seen);
    578   FAILIF (rc.mismatch);
    579   /* a replicated row keeps its index */
    580   TDB_coin (pg,
    581             &denom,
    582             25,
    583             &old_coin,
    584             NULL);
    585   TDB_coin (pg,
    586             &denom,
    587             26,
    588             &fresh,
    589             &td.details.recoup_refresh.known_coin_id);
    590   td.details.recoup_refresh.coin_pub = fresh.coin_pub;
    591   td.details.recoup_refresh.refresh_id = melt (pg,
    592                                                &old_coin,
    593                                                6,
    594                                                "1");
    595   TDB_coin_free (&old_coin);
    596   TDB_coin_free (&fresh);
    597   TDB_FILL (td.details.recoup_refresh.coin_sig,
    598             77);
    599   TDB_FILL (td.details.recoup_refresh.coin_blind,
    600             77);
    601   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    602           TALER_EXCHANGEDB_insert_records_by_table (pg,
    603                                                     &td));
    604   FAILIF (1 != TDB_count (pg,
    605                           "FROM recoup_refresh WHERE coin_index=7"));
    606   rc.seen = 0;
    607   rc.expected_coin_index = 7;
    608   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    609           TALER_EXCHANGEDB_iterate_records_by_table (
    610             pg,
    611             TALER_EXCHANGEDB_RT_RECOUP_REFRESH,
    612             76,
    613             &replication_cb,
    614             &rc));
    615   FAILIF (1 != rc.seen);
    616   FAILIF (rc.mismatch);
    617   return 0;
    618 }
    619 
    620 
    621 /**
    622  * The iterator must not report a recoup-refresh that is not there.
    623  *
    624  * This is all it can be checked for today: its statement joins
    625  * `refresh_revealed_coins` and `refresh_commitments`, tables the schema no
    626  * longer has, so it cannot return a row at all (EDB-16).  The assertion
    627  * below holds both now and once that is repaired.
    628  *
    629  * @param pg the database context
    630  * @return 0 on success
    631  */
    632 static int
    633 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
    634 {
    635   unsigned int total = 0;
    636 
    637   /* the one recoup made in check_recoup_refresh() */
    638   FAILIF (1 !=
    639           TALER_EXCHANGEDB_iterate_recoup_refreshes_above_serial_id (
    640             pg,
    641             0,
    642             &recoup_refresh_cb,
    643             &total));
    644   FAILIF (1 != total);
    645   total = 0;
    646   FAILIF (0 <
    647           TALER_EXCHANGEDB_iterate_recoup_refreshes_above_serial_id (
    648             pg,
    649             1000,
    650             &recoup_refresh_cb,
    651             &total));
    652   FAILIF (0 != total);
    653   return 0;
    654 }
    655 
    656 
    657 /**
    658  * A recouped coin that was credited again (a refund does that) can be
    659  * recouped a second time.  Replaying the request afterwards must report
    660  * the latest recoup, not an arbitrary earlier one.
    661  *
    662  * @param pg the database context
    663  * @return 0 on success
    664  */
    665 static int
    666 check_replay_latest (struct TALER_EXCHANGEDB_PostgresContext *pg)
    667 {
    668   struct TALER_CoinPublicInfo old_coin;
    669   struct TALER_CoinPublicInfo fresh;
    670   struct RecoupStatus st;
    671   struct TALER_Amount expect_first = TDB_amount ("5");
    672   struct TALER_Amount expect_second = TDB_amount ("2");
    673   uint64_t known_coin_id;
    674   uint64_t refresh_id;
    675   char *hex;
    676   int ret = 1;
    677 
    678   TDB_coin (pg,
    679             &denom,
    680             27,
    681             &old_coin,
    682             NULL);
    683   refresh_id = melt (pg,
    684                      &old_coin,
    685                      7,
    686                      "1");
    687   TDB_coin (pg,
    688             &denom,
    689             28,
    690             &fresh,
    691             &known_coin_id);
    692   hex = TDB_hex (&fresh.coin_pub,
    693                  sizeof (fresh.coin_pub));
    694 
    695   /* first recoup drains the fresh coin's EUR:5 */
    696   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    697             run_recoup (pg,
    698                         &old_coin.coin_pub,
    699                         refresh_id,
    700                         &fresh.coin_pub,
    701                         known_coin_id,
    702                         28,
    703                         &st),
    704             goto cleanup);
    705   FAILIF_C ( (! st.recoup_ok) ||
    706              (st.internal_failure) ||
    707              (0 != TALER_amount_cmp (&st.recoup_amount,
    708                                      &expect_first)),
    709              goto cleanup);
    710 
    711   /* the fresh coin gets EUR:2 back, as a refund would do */
    712   FAILIF_C (GNUNET_OK !=
    713             TDB_exec (pg,
    714                       "UPDATE known_coins"
    715                       " SET remaining=ROW(2,0)::taler_amount"
    716                       " WHERE coin_pub=decode('%s','hex');",
    717                       hex),
    718             goto cleanup);
    719 
    720   /* second recoup for the same refresh drains the EUR:2 */
    721   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    722             run_recoup (pg,
    723                         &old_coin.coin_pub,
    724                         refresh_id,
    725                         &fresh.coin_pub,
    726                         known_coin_id,
    727                         28,
    728                         &st),
    729             goto cleanup);
    730   FAILIF_C ( (! st.recoup_ok) ||
    731              (st.internal_failure) ||
    732              (0 != TALER_amount_cmp (&st.recoup_amount,
    733                                      &expect_second)),
    734              goto cleanup);
    735   FAILIF_C (2 != TDB_count (pg,
    736                             "FROM recoup_refresh"
    737                             " WHERE coin_pub=decode('%s','hex')",
    738                             hex),
    739             goto cleanup);
    740 
    741   /* replaying the request reports the latest recoup... */
    742   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    743             run_recoup (pg,
    744                         &old_coin.coin_pub,
    745                         refresh_id,
    746                         &fresh.coin_pub,
    747                         known_coin_id,
    748                         28,
    749                         &st),
    750             goto cleanup);
    751   FAILIF_C ( (! st.recoup_ok) ||
    752              (st.internal_failure),
    753              goto cleanup);
    754   FAILIF_C (0 != TALER_amount_cmp (&st.recoup_amount,
    755                                    &expect_second),
    756             goto cleanup);
    757   /* ...and does not touch the tables again */
    758   FAILIF_C (2 != TDB_count (pg,
    759                             "FROM recoup_refresh"
    760                             " WHERE coin_pub=decode('%s','hex')",
    761                             hex),
    762             goto cleanup);
    763   ret = 0;
    764 cleanup:
    765   GNUNET_free (hex);
    766   TDB_coin_free (&fresh);
    767   TDB_coin_free (&old_coin);
    768   return ret;
    769 }
    770 
    771 
    772 /**
    773  * The checks to run, in order.
    774  */
    775 static const struct TDB_Test tests[] = {
    776   { "recoup-refresh-unknown-coin",
    777     &check_unknown_coin },
    778   { "recoup-refresh-empty-coin",
    779     &check_empty_coin },
    780   { "recoup-refresh-recoup",
    781     &check_recoup_refresh },
    782   { "recoup-refresh-iterate",
    783     &check_iterate },
    784   { "recoup-refresh-replication",
    785     &check_replication },
    786   { "recoup-refresh-replay-latest",
    787     &check_replay_latest },
    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-recoup-refresh",
    801                   "Tests for the exchangedb `recoup_refresh' table",
    802                   tests);
    803   TDB_account_free (&account);
    804   TDB_denom_free (&denom);
    805   return ret;
    806 }
    807 
    808 
    809 /* end of test_recoup_refresh.c */