exchange

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

test_recoup.c (15584B)


      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 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   struct RecoupContext *ctx = cls;
    115 
    116   (void) rowid;
    117   (void) timestamp;
    118   (void) denom_pub;
    119   (void) coin_sig;
    120   ctx->total++;
    121   if ( (NULL != ctx->coin_pub) &&
    122        (0 == GNUNET_memcmp (&coin->coin_pub,
    123                             ctx->coin_pub)) )
    124   {
    125     ctx->matched++;
    126     ctx->amount = *amount;
    127     ctx->reserve_pub = *reserve_pub;
    128     ctx->coin_blind = *coin_blind;
    129   }
    130   if ( (0 != ctx->stop_after) &&
    131        (ctx->total >= ctx->stop_after) )
    132     return GNUNET_SYSERR;
    133   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 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   TDB_fill (&coin_bks,
    184             sizeof (coin_bks),
    185             seed);
    186   TDB_fill (&coin_sig,
    187             sizeof (coin_sig),
    188             seed);
    189   memset (st,
    190           0,
    191           sizeof (*st));
    192   st->recoup_timestamp = GNUNET_TIME_timestamp_get ();
    193   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 check_unknown_coin (struct TALER_EXCHANGEDB_PostgresContext *pg)
    214 {
    215   struct TALER_ReservePublicKeyP reserve_pub;
    216   struct TALER_CoinSpendPublicKeyP coin_pub;
    217   struct RecoupContext ctx = { 0 };
    218   struct RecoupStatus st;
    219   uint64_t withdraw_id;
    220 
    221   TDB_denom (pg,
    222              10,
    223              "5",
    224              "0.1",
    225              &denom);
    226   TDB_account (pg,
    227                10,
    228                &account);
    229   TDB_reserve_in (pg,
    230                   &account,
    231                   10,
    232                   "10",
    233                   &reserve_pub);
    234   withdraw_id = TDB_withdraw (pg,
    235                               &denom,
    236                               &reserve_pub,
    237                               10,
    238                               "5");
    239   TDB_FILL (coin_pub,
    240             99);
    241   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   FAILIF (! st.internal_failure);
    250   FAILIF (st.recoup_ok);
    251   FAILIF (0 != TDB_count (pg,
    252                           "FROM recoup"));
    253   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    254           TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
    255                                                             0,
    256                                                             &recoup_cb,
    257                                                             &ctx));
    258   FAILIF (0 != ctx.total);
    259   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 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   TDB_reserve_in (pg,
    280                   &account,
    281                   11,
    282                   "10",
    283                   &reserve_pub);
    284   withdraw_id = TDB_withdraw (pg,
    285                               &denom,
    286                               &reserve_pub,
    287                               11,
    288                               "5");
    289   TDB_coin (pg,
    290             &denom,
    291             20,
    292             &coin,
    293             &known_coin_id);
    294   hex = TDB_hex (&coin.coin_pub,
    295                  sizeof (coin.coin_pub));
    296   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   GNUNET_free (hex);
    304   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   TDB_coin_free (&coin);
    314   FAILIF (st.internal_failure);
    315   /* nothing to give back, and no earlier recoup to report */
    316   FAILIF (st.recoup_ok);
    317   FAILIF (0 != TDB_count (pg,
    318                           "FROM recoup"));
    319   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 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   struct TALER_Amount expect_coin = TDB_amount ("5");
    338   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   TDB_reserve_in (pg,
    345                   &account,
    346                   12,
    347                   "10",
    348                   &reserve_pub);
    349   withdraw_id = TDB_withdraw (pg,
    350                               &denom,
    351                               &reserve_pub,
    352                               12,
    353                               "5");
    354   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   memset (&reserve,
    361           0,
    362           sizeof (reserve));
    363   reserve.pub = reserve_pub;
    364   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    365             TALER_EXCHANGEDB_get_reserve (pg,
    366                                           &reserve),
    367             TDB_coin_free (&coin));
    368 
    369   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   FAILIF_C (st.internal_failure,
    379             TDB_coin_free (&coin));
    380   FAILIF_C (! st.recoup_ok,
    381             TDB_coin_free (&coin));
    382   FAILIF_C (1 != TDB_count (pg,
    383                             "FROM recoup"),
    384             TDB_coin_free (&coin));
    385 
    386   /* the coin is empty... */
    387   hex = TDB_hex (&coin.coin_pub,
    388                  sizeof (coin.coin_pub));
    389   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   GNUNET_free (hex);
    396   /* ...and the reserve got the money */
    397   memset (&reserve,
    398           0,
    399           sizeof (reserve));
    400   reserve.pub = reserve_pub;
    401   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    402             TALER_EXCHANGEDB_get_reserve (pg,
    403                                           &reserve),
    404             TDB_coin_free (&coin));
    405   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   TDB_FILL (expect_bks,
    411             21);
    412   memset (&ctx,
    413           0,
    414           sizeof (ctx));
    415   ctx.coin_pub = &coin.coin_pub;
    416   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   FAILIF_C (1 != ctx.matched,
    423             TDB_coin_free (&coin));
    424   FAILIF_C (0 != TALER_amount_cmp (&ctx.amount,
    425                                    &expect_coin),
    426             TDB_coin_free (&coin));
    427   FAILIF_C (0 != GNUNET_memcmp (&ctx.reserve_pub,
    428                                 &reserve_pub),
    429             TDB_coin_free (&coin));
    430   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   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   FAILIF_C (! st.recoup_ok,
    445             TDB_coin_free (&coin));
    446   FAILIF_C (st.internal_failure,
    447             TDB_coin_free (&coin));
    448   FAILIF_C (1 != TDB_count (pg,
    449                             "FROM recoup"),
    450             TDB_coin_free (&coin));
    451   TDB_coin_free (&coin);
    452   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 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
    464 {
    465   struct RecoupContext ctx;
    466 
    467   memset (&ctx,
    468           0,
    469           sizeof (ctx));
    470   FAILIF (1 !=
    471           TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
    472                                                             0,
    473                                                             &recoup_cb,
    474                                                             &ctx));
    475   memset (&ctx,
    476           0,
    477           sizeof (ctx));
    478   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    479           TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
    480                                                             1000,
    481                                                             &recoup_cb,
    482                                                             &ctx));
    483   FAILIF (0 != ctx.total);
    484   memset (&ctx,
    485           0,
    486           sizeof (ctx));
    487   ctx.stop_after = 1;
    488   FAILIF (1 !=
    489           TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
    490                                                             0,
    491                                                             &recoup_cb,
    492                                                             &ctx));
    493   FAILIF (1 != ctx.total);
    494   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 main (int argc,
    516       char *const *argv)
    517 {
    518   int ret;
    519 
    520   ret = TDB_main (argc,
    521                   argv,
    522                   "test-recoup",
    523                   "Tests for the exchangedb `recoup' table",
    524                   tests);
    525   TDB_account_free (&account);
    526   TDB_denom_free (&denom);
    527   return ret;
    528 }
    529 
    530 
    531 /* end of test_recoup.c */