LCOV - code coverage report
Current view: top level - exchangedb - test_recoup.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 96 96
Test Date: 2026-09-11 18:55:36 Functions: 100.0 % 7 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.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              : 
      34              : 
      35              : /**
      36              :  * Account the checks fund their reserves from.
      37              :  */
      38              : static struct TDB_Account account;
      39              : 
      40              : 
      41              : /**
      42              :  * Denomination the checks use.
      43              :  */
      44              : static struct TDB_Denom denom;
      45              : 
      46              : 
      47              : /**
      48              :  * Closure for #recoup_cb().
      49              :  */
      50              : struct RecoupContext
      51              : {
      52              :   /**
      53              :    * How many rows did the callback see?
      54              :    */
      55              :   unsigned int total;
      56              : 
      57              :   /**
      58              :    * Stop after this many rows; 0 for no limit.
      59              :    */
      60              :   unsigned int stop_after;
      61              : 
      62              :   /**
      63              :    * Coin we are looking for, NULL to match nothing.
      64              :    */
      65              :   const struct TALER_CoinSpendPublicKeyP *coin_pub;
      66              : 
      67              :   /**
      68              :    * How many times did we see it?
      69              :    */
      70              :   unsigned int matched;
      71              : 
      72              :   /**
      73              :    * Amount reported for it.
      74              :    */
      75              :   struct TALER_Amount amount;
      76              : 
      77              :   /**
      78              :    * Reserve reported for it.
      79              :    */
      80              :   struct TALER_ReservePublicKeyP reserve_pub;
      81              : 
      82              :   /**
      83              :    * Blinding secret reported for it.
      84              :    */
      85              :   union GNUNET_CRYPTO_BlindingSecretP coin_blind;
      86              : };
      87              : 
      88              : 
      89              : /**
      90              :  * Callback for #TALER_EXCHANGEDB_iterate_recoups_above_serial_id().
      91              :  *
      92              :  * @param cls a `struct RecoupContext *`
      93              :  * @param rowid row of the recoup
      94              :  * @param timestamp when the recoup was requested
      95              :  * @param amount how much went back to the reserve
      96              :  * @param reserve_pub the reserve that was credited
      97              :  * @param coin the coin that was recouped
      98              :  * @param denom_pub denomination of @a coin
      99              :  * @param coin_sig signature over the request
     100              :  * @param coin_blind blinding secret of the coin
     101              :  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
     102              :  */
     103              : static enum GNUNET_GenericReturnValue
     104            3 : recoup_cb (void *cls,
     105              :            uint64_t rowid,
     106              :            struct GNUNET_TIME_Timestamp timestamp,
     107              :            const struct TALER_Amount *amount,
     108              :            const struct TALER_ReservePublicKeyP *reserve_pub,
     109              :            const struct TALER_CoinPublicInfo *coin,
     110              :            const struct TALER_DenominationPublicKey *denom_pub,
     111              :            const struct TALER_CoinSpendSignatureP *coin_sig,
     112              :            const union GNUNET_CRYPTO_BlindingSecretP *coin_blind)
     113              : {
     114            3 :   struct RecoupContext *ctx = cls;
     115              : 
     116              :   (void) rowid;
     117              :   (void) timestamp;
     118              :   (void) denom_pub;
     119              :   (void) coin_sig;
     120            3 :   ctx->total++;
     121            3 :   if ( (NULL != ctx->coin_pub) &&
     122            1 :        (0 == GNUNET_memcmp (&coin->coin_pub,
     123              :                             ctx->coin_pub)) )
     124              :   {
     125            1 :     ctx->matched++;
     126            1 :     ctx->amount = *amount;
     127            1 :     ctx->reserve_pub = *reserve_pub;
     128            1 :     ctx->coin_blind = *coin_blind;
     129              :   }
     130            3 :   if ( (0 != ctx->stop_after) &&
     131            1 :        (ctx->total >= ctx->stop_after) )
     132            1 :     return GNUNET_SYSERR;
     133            2 :   return GNUNET_OK;
     134              : }
     135              : 
     136              : 
     137              : /**
     138              :  * Outcome of a recoup request.
     139              :  */
     140              : struct RecoupStatus
     141              : {
     142              :   /**
     143              :    * Was the recoup accepted?
     144              :    */
     145              :   bool recoup_ok;
     146              : 
     147              :   /**
     148              :    * Did something go wrong inside the database?
     149              :    */
     150              :   bool internal_failure;
     151              : 
     152              :   /**
     153              :    * When the recoup happened.
     154              :    */
     155              :   struct GNUNET_TIME_Timestamp recoup_timestamp;
     156              : };
     157              : 
     158              : 
     159              : /**
     160              :  * Recoup a coin into a reserve.
     161              :  *
     162              :  * @param pg the database context
     163              :  * @param reserve_pub reserve to credit
     164              :  * @param withdraw_id withdraw that justifies the recoup
     165              :  * @param coin_pub coin to recoup
     166              :  * @param known_coin_id row of @a coin_pub
     167              :  * @param seed seed for the blinding secret and coin signature
     168              :  * @param[out] st set to the outcome
     169              :  * @return transaction status
     170              :  */
     171              : static enum GNUNET_DB_QueryStatus
     172            4 : run_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg,
     173              :             const struct TALER_ReservePublicKeyP *reserve_pub,
     174              :             uint64_t withdraw_id,
     175              :             const struct TALER_CoinSpendPublicKeyP *coin_pub,
     176              :             uint64_t known_coin_id,
     177              :             uint32_t seed,
     178              :             struct RecoupStatus *st)
     179              : {
     180              :   union GNUNET_CRYPTO_BlindingSecretP coin_bks;
     181              :   struct TALER_CoinSpendSignatureP coin_sig;
     182              : 
     183            4 :   TDB_fill (&coin_bks,
     184              :             sizeof (coin_bks),
     185              :             seed);
     186            4 :   TDB_fill (&coin_sig,
     187              :             sizeof (coin_sig),
     188              :             seed);
     189            4 :   memset (st,
     190              :           0,
     191              :           sizeof (*st));
     192            4 :   st->recoup_timestamp = GNUNET_TIME_timestamp_get ();
     193            4 :   return TALER_EXCHANGEDB_do_recoup (pg,
     194              :                                      reserve_pub,
     195              :                                      withdraw_id,
     196              :                                      &coin_bks,
     197              :                                      coin_pub,
     198              :                                      known_coin_id,
     199              :                                      &coin_sig,
     200              :                                      &st->recoup_timestamp,
     201              :                                      &st->recoup_ok,
     202              :                                      &st->internal_failure);
     203              : }
     204              : 
     205              : 
     206              : /**
     207              :  * Recouping a coin the exchange does not know is an internal failure.
     208              :  *
     209              :  * @param pg the database context
     210              :  * @return 0 on success
     211              :  */
     212              : static int
     213            1 : check_unknown_coin (struct TALER_EXCHANGEDB_PostgresContext *pg)
     214              : {
     215              :   struct TALER_ReservePublicKeyP reserve_pub;
     216              :   struct TALER_CoinSpendPublicKeyP coin_pub;
     217            1 :   struct RecoupContext ctx = { 0 };
     218              :   struct RecoupStatus st;
     219              :   uint64_t withdraw_id;
     220              : 
     221            1 :   TDB_denom (pg,
     222              :              10,
     223              :              "5",
     224              :              "0.1",
     225              :              &denom);
     226            1 :   TDB_account (pg,
     227              :                10,
     228              :                &account);
     229            1 :   TDB_reserve_in (pg,
     230              :                   &account,
     231              :                   10,
     232              :                   "10",
     233              :                   &reserve_pub);
     234            1 :   withdraw_id = TDB_withdraw (pg,
     235              :                               &denom,
     236              :                               &reserve_pub,
     237              :                               10,
     238              :                               "5");
     239            1 :   TDB_FILL (coin_pub,
     240              :             99);
     241            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     242              :           run_recoup (pg,
     243              :                       &reserve_pub,
     244              :                       withdraw_id,
     245              :                       &coin_pub,
     246              :                       1,
     247              :                       99,
     248              :                       &st));
     249            1 :   FAILIF (! st.internal_failure);
     250            1 :   FAILIF (st.recoup_ok);
     251            1 :   FAILIF (0 != TDB_count (pg,
     252              :                           "FROM recoup"));
     253            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     254              :           TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
     255              :                                                             0,
     256              :                                                             &recoup_cb,
     257              :                                                             &ctx));
     258            1 :   FAILIF (0 != ctx.total);
     259            1 :   return 0;
     260              : }
     261              : 
     262              : 
     263              : /**
     264              :  * A coin that is already empty and was never recouped is refused.
     265              :  *
     266              :  * @param pg the database context
     267              :  * @return 0 on success
     268              :  */
     269              : static int
     270            1 : check_empty_coin (struct TALER_EXCHANGEDB_PostgresContext *pg)
     271              : {
     272              :   struct TALER_ReservePublicKeyP reserve_pub;
     273              :   struct TALER_CoinPublicInfo coin;
     274              :   struct RecoupStatus st;
     275              :   uint64_t known_coin_id;
     276              :   uint64_t withdraw_id;
     277              :   char *hex;
     278              : 
     279            1 :   TDB_reserve_in (pg,
     280              :                   &account,
     281              :                   11,
     282              :                   "10",
     283              :                   &reserve_pub);
     284            1 :   withdraw_id = TDB_withdraw (pg,
     285              :                               &denom,
     286              :                               &reserve_pub,
     287              :                               11,
     288              :                               "5");
     289            1 :   TDB_coin (pg,
     290              :             &denom,
     291              :             20,
     292              :             &coin,
     293              :             &known_coin_id);
     294            1 :   hex = TDB_hex (&coin.coin_pub,
     295              :                  sizeof (coin.coin_pub));
     296            1 :   FAILIF_C (GNUNET_OK !=
     297              :             TDB_exec (pg,
     298              :                       "UPDATE known_coins"
     299              :                       " SET remaining=ROW(0,0)::taler_amount"
     300              :                       " WHERE coin_pub=decode('%s','hex');",
     301              :                       hex),
     302              :             GNUNET_free (hex); TDB_coin_free (&coin));
     303            1 :   GNUNET_free (hex);
     304            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     305              :             run_recoup (pg,
     306              :                         &reserve_pub,
     307              :                         withdraw_id,
     308              :                         &coin.coin_pub,
     309              :                         known_coin_id,
     310              :                         20,
     311              :                         &st),
     312              :             TDB_coin_free (&coin));
     313            1 :   TDB_coin_free (&coin);
     314            1 :   FAILIF (st.internal_failure);
     315              :   /* nothing to give back, and no earlier recoup to report */
     316            1 :   FAILIF (st.recoup_ok);
     317            1 :   FAILIF (0 != TDB_count (pg,
     318              :                           "FROM recoup"));
     319            1 :   return 0;
     320              : }
     321              : 
     322              : 
     323              : /**
     324              :  * A funded coin is drained and the reserve is credited.
     325              :  *
     326              :  * @param pg the database context
     327              :  * @return 0 on success
     328              :  */
     329              : static int
     330            1 : check_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg)
     331              : {
     332              :   struct TALER_ReservePublicKeyP reserve_pub;
     333              :   struct TALER_CoinPublicInfo coin;
     334              :   struct TALER_EXCHANGEDB_Reserve reserve;
     335              :   struct RecoupStatus st;
     336              :   struct RecoupContext ctx;
     337            1 :   struct TALER_Amount expect_coin = TDB_amount ("5");
     338            1 :   struct TALER_Amount expect_reserve = TDB_amount ("10");
     339              :   union GNUNET_CRYPTO_BlindingSecretP expect_bks;
     340              :   uint64_t known_coin_id;
     341              :   uint64_t withdraw_id;
     342              :   char *hex;
     343              : 
     344            1 :   TDB_reserve_in (pg,
     345              :                   &account,
     346              :                   12,
     347              :                   "10",
     348              :                   &reserve_pub);
     349            1 :   withdraw_id = TDB_withdraw (pg,
     350              :                               &denom,
     351              :                               &reserve_pub,
     352              :                               12,
     353              :                               "5");
     354            1 :   TDB_coin (pg,
     355              :             &denom,
     356              :             21,
     357              :             &coin,
     358              :             &known_coin_id);
     359              :   /* EUR:10 came in and EUR:5 was withdrawn, so EUR:5 is left */
     360            1 :   memset (&reserve,
     361              :           0,
     362              :           sizeof (reserve));
     363            1 :   reserve.pub = reserve_pub;
     364            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     365              :             TALER_EXCHANGEDB_get_reserve (pg,
     366              :                                           &reserve),
     367              :             TDB_coin_free (&coin));
     368              : 
     369            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     370              :             run_recoup (pg,
     371              :                         &reserve_pub,
     372              :                         withdraw_id,
     373              :                         &coin.coin_pub,
     374              :                         known_coin_id,
     375              :                         21,
     376              :                         &st),
     377              :             TDB_coin_free (&coin));
     378            1 :   FAILIF_C (st.internal_failure,
     379              :             TDB_coin_free (&coin));
     380            1 :   FAILIF_C (! st.recoup_ok,
     381              :             TDB_coin_free (&coin));
     382            1 :   FAILIF_C (1 != TDB_count (pg,
     383              :                             "FROM recoup"),
     384              :             TDB_coin_free (&coin));
     385              : 
     386              :   /* the coin is empty... */
     387            1 :   hex = TDB_hex (&coin.coin_pub,
     388              :                  sizeof (coin.coin_pub));
     389            1 :   FAILIF_C (1 != TDB_count (pg,
     390              :                             "FROM known_coins"
     391              :                             " WHERE coin_pub=decode('%s','hex')"
     392              :                             "   AND remaining=ROW(0,0)::taler_amount",
     393              :                             hex),
     394              :             GNUNET_free (hex); TDB_coin_free (&coin));
     395            1 :   GNUNET_free (hex);
     396              :   /* ...and the reserve got the money */
     397            1 :   memset (&reserve,
     398              :           0,
     399              :           sizeof (reserve));
     400            1 :   reserve.pub = reserve_pub;
     401            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     402              :             TALER_EXCHANGEDB_get_reserve (pg,
     403              :                                           &reserve),
     404              :             TDB_coin_free (&coin));
     405            1 :   FAILIF_C (0 != TALER_amount_cmp (&reserve.balance,
     406              :                                    &expect_reserve),
     407              :             TDB_coin_free (&coin));
     408              : 
     409              :   /* the recoup is reported with everything it was made with */
     410            1 :   TDB_FILL (expect_bks,
     411              :             21);
     412            1 :   memset (&ctx,
     413              :           0,
     414              :           sizeof (ctx));
     415            1 :   ctx.coin_pub = &coin.coin_pub;
     416            1 :   FAILIF_C (0 >=
     417              :             TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
     418              :                                                               0,
     419              :                                                               &recoup_cb,
     420              :                                                               &ctx),
     421              :             TDB_coin_free (&coin));
     422            1 :   FAILIF_C (1 != ctx.matched,
     423              :             TDB_coin_free (&coin));
     424            1 :   FAILIF_C (0 != TALER_amount_cmp (&ctx.amount,
     425              :                                    &expect_coin),
     426              :             TDB_coin_free (&coin));
     427            1 :   FAILIF_C (0 != GNUNET_memcmp (&ctx.reserve_pub,
     428              :                                 &reserve_pub),
     429              :             TDB_coin_free (&coin));
     430            1 :   FAILIF_C (0 != GNUNET_memcmp (&ctx.coin_blind,
     431              :                                 &expect_bks),
     432              :             TDB_coin_free (&coin));
     433              : 
     434              :   /* recouping the same coin again finds the earlier recoup */
     435            1 :   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
     436              :             run_recoup (pg,
     437              :                         &reserve_pub,
     438              :                         withdraw_id,
     439              :                         &coin.coin_pub,
     440              :                         known_coin_id,
     441              :                         21,
     442              :                         &st),
     443              :             TDB_coin_free (&coin));
     444            1 :   FAILIF_C (! st.recoup_ok,
     445              :             TDB_coin_free (&coin));
     446            1 :   FAILIF_C (st.internal_failure,
     447              :             TDB_coin_free (&coin));
     448            1 :   FAILIF_C (1 != TDB_count (pg,
     449              :                             "FROM recoup"),
     450              :             TDB_coin_free (&coin));
     451            1 :   TDB_coin_free (&coin);
     452            1 :   return 0;
     453              : }
     454              : 
     455              : 
     456              : /**
     457              :  * The iterator's serial bound and abort return behave as documented.
     458              :  *
     459              :  * @param pg the database context
     460              :  * @return 0 on success
     461              :  */
     462              : static int
     463            1 : check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
     464              : {
     465              :   struct RecoupContext ctx;
     466              : 
     467            1 :   memset (&ctx,
     468              :           0,
     469              :           sizeof (ctx));
     470            1 :   FAILIF (1 !=
     471              :           TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
     472              :                                                             0,
     473              :                                                             &recoup_cb,
     474              :                                                             &ctx));
     475            1 :   memset (&ctx,
     476              :           0,
     477              :           sizeof (ctx));
     478            1 :   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     479              :           TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
     480              :                                                             1000,
     481              :                                                             &recoup_cb,
     482              :                                                             &ctx));
     483            1 :   FAILIF (0 != ctx.total);
     484            1 :   memset (&ctx,
     485              :           0,
     486              :           sizeof (ctx));
     487            1 :   ctx.stop_after = 1;
     488            1 :   FAILIF (1 !=
     489              :           TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
     490              :                                                             0,
     491              :                                                             &recoup_cb,
     492              :                                                             &ctx));
     493            1 :   FAILIF (1 != ctx.total);
     494            1 :   return 0;
     495              : }
     496              : 
     497              : 
     498              : /**
     499              :  * The checks to run, in order.
     500              :  */
     501              : static const struct TDB_Test tests[] = {
     502              :   { "recoup-unknown-coin",
     503              :     &check_unknown_coin },
     504              :   { "recoup-empty-coin",
     505              :     &check_empty_coin },
     506              :   { "recoup-recoup",
     507              :     &check_recoup },
     508              :   { "recoup-iterate",
     509              :     &check_iterate },
     510              :   { NULL, NULL }
     511              : };
     512              : 
     513              : 
     514              : int
     515            1 : main (int argc,
     516              :       char *const *argv)
     517              : {
     518              :   int ret;
     519              : 
     520            1 :   ret = TDB_main (argc,
     521              :                   argv,
     522              :                   "test-recoup",
     523              :                   "Tests for the exchangedb `recoup' table",
     524              :                   tests);
     525            1 :   TDB_account_free (&account);
     526            1 :   TDB_denom_free (&denom);
     527            1 :   return ret;
     528              : }
     529              : 
     530              : 
     531              : /* end of test_recoup.c */
        

Generated by: LCOV version 2.0-1