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


      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, as far as it can be,
     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  * The iterator cannot be checked beyond "does not invent an answer": its
     30  * statement joins `refresh_revealed_coins` and `refresh_commitments`, which
     31  * the schema no longer has (EDB-16 in bugs.txt, in-tree FIXME-#9828).
     32  */
     33 #include "test_common.h"
     34 #include "exchange-database/do_recoup_refresh.h"
     35 #include "exchange-database/do_refresh.h"
     36 #include "exchange-database/iterate_recoup_refreshes_above_serial_id.h"
     37 
     38 
     39 /**
     40  * Account the checks fund their reserves from.
     41  */
     42 static struct TDB_Account account;
     43 
     44 
     45 /**
     46  * Denomination the checks use.
     47  */
     48 static struct TDB_Denom denom;
     49 
     50 
     51 /**
     52  * Melt @a coin and return the row of the melt.
     53  *
     54  * @param pg the database context
     55  * @param coin coin to melt
     56  * @param seed seed for the commitment and signatures
     57  * @param amount how much to melt, e.g. "1"
     58  * @return row of the melt in `refresh`
     59  */
     60 static uint64_t
     61 melt (struct TALER_EXCHANGEDB_PostgresContext *pg,
     62       const struct TALER_CoinPublicInfo *coin,
     63       uint32_t seed,
     64       const char *amount)
     65 {
     66   struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS rf;
     67   struct TALER_BlindedDenominationSignature denom_sig;
     68   struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
     69   struct TALER_Amount coin_balance;
     70   uint64_t denom_serial = denom.serial;
     71   uint64_t refresh_id;
     72   uint32_t noreveal_index;
     73   bool found;
     74   bool zombie_required = false;
     75   bool nonce_reuse;
     76   bool balance_ok;
     77 
     78   memset (&rf,
     79           0,
     80           sizeof (rf));
     81   rf.coin.coin_pub = coin->coin_pub;
     82   rf.coin.denom_pub_hash = coin->denom_pub_hash;
     83   rf.coin.no_age_commitment = coin->no_age_commitment;
     84   TDB_fill (&rf.coin_sig,
     85             sizeof (rf.coin_sig),
     86             seed);
     87   TDB_fill (&rf.rc,
     88             sizeof (rf.rc),
     89             seed);
     90   TDB_fill (&rf.refresh_seed,
     91             sizeof (rf.refresh_seed),
     92             seed);
     93   TDB_fill (&rf.planchets_h,
     94             sizeof (rf.planchets_h),
     95             seed);
     96   TDB_fill (&rf.selected_h,
     97             sizeof (rf.selected_h),
     98             seed + 1);
     99   rf.amount_with_fee = TDB_amount (amount);
    100   rf.num_coins = 1;
    101   rf.denom_serials = &denom_serial;
    102   TDB_blinded_denom_sig (seed,
    103                          &denom_sig);
    104   rf.denom_sigs = &denom_sig;
    105   rf.noreveal_index = 0;
    106   rf.is_v27_refresh = true;
    107   rf.no_blinding_seed = true;
    108   GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
    109                  TALER_EXCHANGEDB_do_refresh (pg,
    110                                               &rf,
    111                                               &now,
    112                                               &found,
    113                                               &noreveal_index,
    114                                               &zombie_required,
    115                                               &nonce_reuse,
    116                                               &balance_ok,
    117                                               &coin_balance));
    118   GNUNET_assert (balance_ok);
    119   TALER_blinded_denom_sig_free (&denom_sig);
    120   {
    121     struct GNUNET_PQ_QueryParam params[] = {
    122       GNUNET_PQ_query_param_auto_from_type (&rf.rc),
    123       GNUNET_PQ_query_param_end
    124     };
    125     struct GNUNET_PQ_ResultSpec rs[] = {
    126       GNUNET_PQ_result_spec_uint64 ("refresh_id",
    127                                     &refresh_id),
    128       GNUNET_PQ_result_spec_end
    129     };
    130 
    131     GNUNET_assert (GNUNET_OK ==
    132                    GNUNET_PQ_prepare_anon (pg->conn,
    133                                            "SELECT refresh_id"
    134                                            " FROM refresh"
    135                                            " WHERE rc=$1;"));
    136     GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
    137                    GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
    138                                                              "",
    139                                                              params,
    140                                                              rs));
    141   }
    142   return refresh_id;
    143 }
    144 
    145 
    146 /**
    147  * Callback for
    148  * #TALER_EXCHANGEDB_iterate_recoup_refreshes_above_serial_id().
    149  *
    150  * @param cls a pointer to an `unsigned int` counter
    151  * @param rowid row of the recoup
    152  * @param timestamp when it happened
    153  * @param amount how much went back
    154  * @param old_coin_pub the old coin
    155  * @param old_denom_pub_hash denomination of the old coin
    156  * @param coin the fresh coin
    157  * @param denom_pub denomination of @a coin
    158  * @param coin_sig signature over the request
    159  * @param coin_blind blinding secret of the fresh coin
    160  * @return #GNUNET_OK
    161  */
    162 static enum GNUNET_GenericReturnValue
    163 recoup_refresh_cb (void *cls,
    164                    uint64_t rowid,
    165                    struct GNUNET_TIME_Timestamp timestamp,
    166                    const struct TALER_Amount *amount,
    167                    const struct TALER_CoinSpendPublicKeyP *old_coin_pub,
    168                    const struct TALER_DenominationHashP *old_denom_pub_hash,
    169                    const struct TALER_CoinPublicInfo *coin,
    170                    const struct TALER_DenominationPublicKey *denom_pub,
    171                    const struct TALER_CoinSpendSignatureP *coin_sig,
    172                    const union GNUNET_CRYPTO_BlindingSecretP *coin_blind)
    173 {
    174   unsigned int *total = cls;
    175 
    176   (void) rowid;
    177   (void) timestamp;
    178   (void) amount;
    179   (void) old_coin_pub;
    180   (void) old_denom_pub_hash;
    181   (void) coin;
    182   (void) denom_pub;
    183   (void) coin_sig;
    184   (void) coin_blind;
    185   (*total)++;
    186   return GNUNET_OK;
    187 }
    188 
    189 
    190 /**
    191  * Outcome of a recoup-refresh request.
    192  */
    193 struct RecoupStatus
    194 {
    195   /**
    196    * Was the recoup accepted?
    197    */
    198   bool recoup_ok;
    199 
    200   /**
    201    * Did something go wrong inside the database?
    202    */
    203   bool internal_failure;
    204 
    205   /**
    206    * When the recoup happened.
    207    */
    208   struct GNUNET_TIME_Timestamp recoup_timestamp;
    209 };
    210 
    211 
    212 /**
    213  * Recoup a fresh coin back onto the coin it was refreshed from.
    214  *
    215  * @param pg the database context
    216  * @param old_coin_pub coin to credit
    217  * @param refresh_id melt that justifies the recoup
    218  * @param coin_pub fresh coin to drain
    219  * @param known_coin_id row of @a coin_pub
    220  * @param seed seed for the blinding secret and coin signature
    221  * @param[out] st set to the outcome
    222  * @return transaction status
    223  */
    224 static enum GNUNET_DB_QueryStatus
    225 run_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg,
    226             const struct TALER_CoinSpendPublicKeyP *old_coin_pub,
    227             uint64_t refresh_id,
    228             const struct TALER_CoinSpendPublicKeyP *coin_pub,
    229             uint64_t known_coin_id,
    230             uint32_t seed,
    231             struct RecoupStatus *st)
    232 {
    233   union GNUNET_CRYPTO_BlindingSecretP coin_bks;
    234   struct TALER_CoinSpendSignatureP coin_sig;
    235 
    236   TDB_fill (&coin_bks,
    237             sizeof (coin_bks),
    238             seed);
    239   TDB_fill (&coin_sig,
    240             sizeof (coin_sig),
    241             seed);
    242   memset (st,
    243           0,
    244           sizeof (*st));
    245   st->recoup_timestamp = GNUNET_TIME_timestamp_get ();
    246   return TALER_EXCHANGEDB_do_recoup_refresh (pg,
    247                                              old_coin_pub,
    248                                              refresh_id,
    249                                              &coin_bks,
    250                                              coin_pub,
    251                                              known_coin_id,
    252                                              &coin_sig,
    253                                              &st->recoup_timestamp,
    254                                              &st->recoup_ok,
    255                                              &st->internal_failure);
    256 }
    257 
    258 
    259 /**
    260  * Recouping a coin the exchange does not know is an internal failure.
    261  *
    262  * @param pg the database context
    263  * @return 0 on success
    264  */
    265 static int
    266 check_unknown_coin (struct TALER_EXCHANGEDB_PostgresContext *pg)
    267 {
    268   struct TALER_ReservePublicKeyP reserve_pub;
    269   struct TALER_CoinPublicInfo old_coin;
    270   struct TALER_CoinSpendPublicKeyP coin_pub;
    271   struct RecoupStatus st;
    272   uint64_t refresh_id;
    273 
    274   TDB_denom (pg,
    275              10,
    276              "5",
    277              "0.1",
    278              &denom);
    279   TDB_account (pg,
    280                10,
    281                &account);
    282   TDB_reserve_in (pg,
    283                   &account,
    284                   10,
    285                   "10",
    286                   &reserve_pub);
    287   TDB_coin (pg,
    288             &denom,
    289             20,
    290             &old_coin,
    291             NULL);
    292   refresh_id = melt (pg,
    293                      &old_coin,
    294                      1,
    295                      "1");
    296   TDB_FILL (coin_pub,
    297             99);
    298   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    299             run_recoup (pg,
    300                         &old_coin.coin_pub,
    301                         refresh_id,
    302                         &coin_pub,
    303                         1,
    304                         99,
    305                         &st),
    306             TDB_coin_free (&old_coin));
    307   TDB_coin_free (&old_coin);
    308   FAILIF (! st.internal_failure);
    309   FAILIF (st.recoup_ok);
    310   FAILIF (0 != TDB_count (pg,
    311                           "FROM recoup_refresh"));
    312   return 0;
    313 }
    314 
    315 
    316 /**
    317  * A fresh coin with nothing left on it and no earlier recoup is refused.
    318  *
    319  * @param pg the database context
    320  * @return 0 on success
    321  */
    322 static int
    323 check_empty_coin (struct TALER_EXCHANGEDB_PostgresContext *pg)
    324 {
    325   struct TALER_CoinPublicInfo old_coin;
    326   struct TALER_CoinPublicInfo fresh;
    327   struct RecoupStatus st;
    328   uint64_t known_coin_id;
    329   uint64_t refresh_id;
    330   char *hex;
    331 
    332   TDB_coin (pg,
    333             &denom,
    334             21,
    335             &old_coin,
    336             NULL);
    337   refresh_id = melt (pg,
    338                      &old_coin,
    339                      2,
    340                      "1");
    341   TDB_coin (pg,
    342             &denom,
    343             22,
    344             &fresh,
    345             &known_coin_id);
    346   hex = TDB_hex (&fresh.coin_pub,
    347                  sizeof (fresh.coin_pub));
    348   FAILIF_C (GNUNET_OK !=
    349             TDB_exec (pg,
    350                       "UPDATE known_coins"
    351                       " SET remaining=ROW(0,0)::taler_amount"
    352                       " WHERE coin_pub=decode('%s','hex');",
    353                       hex),
    354             GNUNET_free (hex);
    355             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    356   GNUNET_free (hex);
    357   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    358             run_recoup (pg,
    359                         &old_coin.coin_pub,
    360                         refresh_id,
    361                         &fresh.coin_pub,
    362                         known_coin_id,
    363                         22,
    364                         &st),
    365             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    366   TDB_coin_free (&fresh);
    367   TDB_coin_free (&old_coin);
    368   FAILIF (st.internal_failure);
    369   FAILIF (st.recoup_ok);
    370   FAILIF (0 != TDB_count (pg,
    371                           "FROM recoup_refresh"));
    372   return 0;
    373 }
    374 
    375 
    376 /**
    377  * A funded fresh coin is drained back onto the old coin.
    378  *
    379  * @param pg the database context
    380  * @return 0 on success
    381  */
    382 static int
    383 check_recoup_refresh (struct TALER_EXCHANGEDB_PostgresContext *pg)
    384 {
    385   struct TALER_CoinPublicInfo old_coin;
    386   struct TALER_CoinPublicInfo fresh;
    387   struct RecoupStatus st;
    388   uint64_t known_coin_id;
    389   uint64_t refresh_id;
    390   char *hex;
    391 
    392   TDB_coin (pg,
    393             &denom,
    394             23,
    395             &old_coin,
    396             NULL);
    397   refresh_id = melt (pg,
    398                      &old_coin,
    399                      3,
    400                      "1");
    401   TDB_coin (pg,
    402             &denom,
    403             24,
    404             &fresh,
    405             &known_coin_id);
    406   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    407             run_recoup (pg,
    408                         &old_coin.coin_pub,
    409                         refresh_id,
    410                         &fresh.coin_pub,
    411                         known_coin_id,
    412                         24,
    413                         &st),
    414             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    415   FAILIF_C (st.internal_failure,
    416             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    417   FAILIF_C (! st.recoup_ok,
    418             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    419   FAILIF_C (1 != TDB_count (pg,
    420                             "FROM recoup_refresh"),
    421             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    422 
    423   /* the fresh coin is empty */
    424   hex = TDB_hex (&fresh.coin_pub,
    425                  sizeof (fresh.coin_pub));
    426   FAILIF_C (1 != TDB_count (pg,
    427                             "FROM known_coins"
    428                             " WHERE coin_pub=decode('%s','hex')"
    429                             "   AND remaining=ROW(0,0)::taler_amount",
    430                             hex),
    431             GNUNET_free (hex);
    432             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    433   GNUNET_free (hex);
    434   /* the old coin got the EUR:5 back on top of the EUR:4 it had left */
    435   hex = TDB_hex (&old_coin.coin_pub,
    436                  sizeof (old_coin.coin_pub));
    437   FAILIF_C (1 != TDB_count (pg,
    438                             "FROM known_coins"
    439                             " WHERE coin_pub=decode('%s','hex')"
    440                             "   AND remaining=ROW(9,0)::taler_amount",
    441                             hex),
    442             GNUNET_free (hex);
    443             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    444   GNUNET_free (hex);
    445 
    446   /* recouping the same coin again finds the earlier recoup */
    447   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    448             run_recoup (pg,
    449                         &old_coin.coin_pub,
    450                         refresh_id,
    451                         &fresh.coin_pub,
    452                         known_coin_id,
    453                         24,
    454                         &st),
    455             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    456   FAILIF_C (! st.recoup_ok,
    457             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    458   FAILIF_C (1 != TDB_count (pg,
    459                             "FROM recoup_refresh"),
    460             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
    461   TDB_coin_free (&fresh);
    462   TDB_coin_free (&old_coin);
    463   return 0;
    464 }
    465 
    466 
    467 /**
    468  * The iterator must not report a recoup-refresh that is not there.
    469  *
    470  * This is all it can be checked for today: its statement joins
    471  * `refresh_revealed_coins` and `refresh_commitments`, tables the schema no
    472  * longer has, so it cannot return a row at all (EDB-16).  The assertion
    473  * below holds both now and once that is repaired.
    474  *
    475  * @param pg the database context
    476  * @return 0 on success
    477  */
    478 static int
    479 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
    480 {
    481   unsigned int total = 0;
    482 
    483   FAILIF (0 <
    484           TALER_EXCHANGEDB_iterate_recoup_refreshes_above_serial_id (
    485             pg,
    486             1000,
    487             &recoup_refresh_cb,
    488             &total));
    489   FAILIF (0 != total);
    490   return 0;
    491 }
    492 
    493 
    494 /**
    495  * The checks to run, in order.
    496  */
    497 static const struct TDB_Test tests[] = {
    498   { "recoup-refresh-unknown-coin",
    499     &check_unknown_coin },
    500   { "recoup-refresh-empty-coin",
    501     &check_empty_coin },
    502   { "recoup-refresh-recoup",
    503     &check_recoup_refresh },
    504   { "recoup-refresh-iterate",
    505     &check_iterate },
    506   { NULL, NULL }
    507 };
    508 
    509 
    510 int
    511 main (int argc,
    512       char *const *argv)
    513 {
    514   int ret;
    515 
    516   ret = TDB_main (argc,
    517                   argv,
    518                   "test-recoup-refresh",
    519                   "Tests for the exchangedb `recoup_refresh' table",
    520                   tests);
    521   TDB_account_free (&account);
    522   TDB_denom_free (&denom);
    523   return ret;
    524 }
    525 
    526 
    527 /* end of test_recoup_refresh.c */