LCOV - code coverage report
Current view: top level - exchangedb - test_recoup_refresh.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 95.9 % 98 94
Test Date: 2026-09-11 18:55:36 Functions: 87.5 % 8 7

            Line data    Source code
       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            3 : 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            3 :   struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
      69              :   struct TALER_Amount coin_balance;
      70            3 :   uint64_t denom_serial = denom.serial;
      71              :   uint64_t refresh_id;
      72              :   uint32_t noreveal_index;
      73              :   bool found;
      74            3 :   bool zombie_required = false;
      75              :   bool nonce_reuse;
      76              :   bool balance_ok;
      77              : 
      78            3 :   memset (&rf,
      79              :           0,
      80              :           sizeof (rf));
      81            3 :   rf.coin.coin_pub = coin->coin_pub;
      82            3 :   rf.coin.denom_pub_hash = coin->denom_pub_hash;
      83            3 :   rf.coin.no_age_commitment = coin->no_age_commitment;
      84            3 :   TDB_fill (&rf.coin_sig,
      85              :             sizeof (rf.coin_sig),
      86              :             seed);
      87            3 :   TDB_fill (&rf.rc,
      88              :             sizeof (rf.rc),
      89              :             seed);
      90            3 :   TDB_fill (&rf.refresh_seed,
      91              :             sizeof (rf.refresh_seed),
      92              :             seed);
      93            3 :   TDB_fill (&rf.planchets_h,
      94              :             sizeof (rf.planchets_h),
      95              :             seed);
      96            3 :   TDB_fill (&rf.selected_h,
      97              :             sizeof (rf.selected_h),
      98              :             seed + 1);
      99            3 :   rf.amount_with_fee = TDB_amount (amount);
     100            3 :   rf.num_coins = 1;
     101            3 :   rf.denom_serials = &denom_serial;
     102            3 :   TDB_blinded_denom_sig (seed,
     103              :                          &denom_sig);
     104            3 :   rf.denom_sigs = &denom_sig;
     105            3 :   rf.noreveal_index = 0;
     106            3 :   rf.is_v27_refresh = true;
     107            3 :   rf.no_blinding_seed = true;
     108            3 :   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            3 :   GNUNET_assert (balance_ok);
     119            3 :   TALER_blinded_denom_sig_free (&denom_sig);
     120              :   {
     121            3 :     struct GNUNET_PQ_QueryParam params[] = {
     122            3 :       GNUNET_PQ_query_param_auto_from_type (&rf.rc),
     123              :       GNUNET_PQ_query_param_end
     124              :     };
     125            3 :     struct GNUNET_PQ_ResultSpec rs[] = {
     126            3 :       GNUNET_PQ_result_spec_uint64 ("refresh_id",
     127              :                                     &refresh_id),
     128              :       GNUNET_PQ_result_spec_end
     129              :     };
     130              : 
     131            3 :     GNUNET_assert (GNUNET_OK ==
     132              :                    GNUNET_PQ_prepare_anon (pg->conn,
     133              :                                            "SELECT refresh_id"
     134              :                                            " FROM refresh"
     135              :                                            " WHERE rc=$1;"));
     136            3 :     GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
     137              :                    GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
     138              :                                                              "",
     139              :                                                              params,
     140              :                                                              rs));
     141              :   }
     142            3 :   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            0 : 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            0 :   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            0 :   (*total)++;
     186            0 :   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            4 : 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            4 :   TDB_fill (&coin_bks,
     237              :             sizeof (coin_bks),
     238              :             seed);
     239            4 :   TDB_fill (&coin_sig,
     240              :             sizeof (coin_sig),
     241              :             seed);
     242            4 :   memset (st,
     243              :           0,
     244              :           sizeof (*st));
     245            4 :   st->recoup_timestamp = GNUNET_TIME_timestamp_get ();
     246            4 :   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            1 : 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            1 :   TDB_denom (pg,
     275              :              10,
     276              :              "5",
     277              :              "0.1",
     278              :              &denom);
     279            1 :   TDB_account (pg,
     280              :                10,
     281              :                &account);
     282            1 :   TDB_reserve_in (pg,
     283              :                   &account,
     284              :                   10,
     285              :                   "10",
     286              :                   &reserve_pub);
     287            1 :   TDB_coin (pg,
     288              :             &denom,
     289              :             20,
     290              :             &old_coin,
     291              :             NULL);
     292            1 :   refresh_id = melt (pg,
     293              :                      &old_coin,
     294              :                      1,
     295              :                      "1");
     296            1 :   TDB_FILL (coin_pub,
     297              :             99);
     298            1 :   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            1 :   TDB_coin_free (&old_coin);
     308            1 :   FAILIF (! st.internal_failure);
     309            1 :   FAILIF (st.recoup_ok);
     310            1 :   FAILIF (0 != TDB_count (pg,
     311              :                           "FROM recoup_refresh"));
     312            1 :   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            1 : 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            1 :   TDB_coin (pg,
     333              :             &denom,
     334              :             21,
     335              :             &old_coin,
     336              :             NULL);
     337            1 :   refresh_id = melt (pg,
     338              :                      &old_coin,
     339              :                      2,
     340              :                      "1");
     341            1 :   TDB_coin (pg,
     342              :             &denom,
     343              :             22,
     344              :             &fresh,
     345              :             &known_coin_id);
     346            1 :   hex = TDB_hex (&fresh.coin_pub,
     347              :                  sizeof (fresh.coin_pub));
     348            1 :   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            1 :   GNUNET_free (hex);
     357            1 :   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            1 :   TDB_coin_free (&fresh);
     367            1 :   TDB_coin_free (&old_coin);
     368            1 :   FAILIF (st.internal_failure);
     369            1 :   FAILIF (st.recoup_ok);
     370            1 :   FAILIF (0 != TDB_count (pg,
     371              :                           "FROM recoup_refresh"));
     372            1 :   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            1 : 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            1 :   TDB_coin (pg,
     393              :             &denom,
     394              :             23,
     395              :             &old_coin,
     396              :             NULL);
     397            1 :   refresh_id = melt (pg,
     398              :                      &old_coin,
     399              :                      3,
     400              :                      "1");
     401            1 :   TDB_coin (pg,
     402              :             &denom,
     403              :             24,
     404              :             &fresh,
     405              :             &known_coin_id);
     406            1 :   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            1 :   FAILIF_C (st.internal_failure,
     416              :             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
     417            1 :   FAILIF_C (! st.recoup_ok,
     418              :             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
     419            1 :   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            1 :   hex = TDB_hex (&fresh.coin_pub,
     425              :                  sizeof (fresh.coin_pub));
     426            1 :   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            1 :   GNUNET_free (hex);
     434              :   /* the old coin got the EUR:5 back on top of the EUR:4 it had left */
     435            1 :   hex = TDB_hex (&old_coin.coin_pub,
     436              :                  sizeof (old_coin.coin_pub));
     437            1 :   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            1 :   GNUNET_free (hex);
     445              : 
     446              :   /* recouping the same coin again finds the earlier recoup */
     447            1 :   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            1 :   FAILIF_C (! st.recoup_ok,
     457              :             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
     458            1 :   FAILIF_C (1 != TDB_count (pg,
     459              :                             "FROM recoup_refresh"),
     460              :             TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
     461            1 :   TDB_coin_free (&fresh);
     462            1 :   TDB_coin_free (&old_coin);
     463            1 :   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            1 : check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
     480              : {
     481            1 :   unsigned int total = 0;
     482              : 
     483            1 :   FAILIF (0 <
     484              :           TALER_EXCHANGEDB_iterate_recoup_refreshes_above_serial_id (
     485              :             pg,
     486              :             1000,
     487              :             &recoup_refresh_cb,
     488              :             &total));
     489            1 :   FAILIF (0 != total);
     490            1 :   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            1 : main (int argc,
     512              :       char *const *argv)
     513              : {
     514              :   int ret;
     515              : 
     516            1 :   ret = TDB_main (argc,
     517              :                   argv,
     518              :                   "test-recoup-refresh",
     519              :                   "Tests for the exchangedb `recoup_refresh' table",
     520              :                   tests);
     521            1 :   TDB_account_free (&account);
     522            1 :   TDB_denom_free (&denom);
     523            1 :   return ret;
     524              : }
     525              : 
     526              : 
     527              : /* end of test_recoup_refresh.c */
        

Generated by: LCOV version 2.0-1