exchange

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

test_batch_deposits.c (20124B)


      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_batch_deposits.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `batch_deposits`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_do_deposit(),
     23  * #TALER_EXCHANGEDB_do_check_deposit_idempotent(),
     24  * #TALER_EXCHANGEDB_get_exists_deposit(),
     25  * #TALER_EXCHANGEDB_get_ready_deposit(),
     26  * #TALER_EXCHANGEDB_iterate_batch_deposits_missing_wire() and
     27  * #TALER_EXCHANGEDB_get_wire_hash_for_contract().
     28  *
     29  * `batch_deposits` references `wire_targets` and, through `coin_deposits`,
     30  * `known_coins`.  A deposit is "ready" only once its wire deadline has
     31  * passed *and* its refund deadline is behind the aggregation shift
     32  * boundary, so the checks use deadlines in the past for that.
     33  */
     34 #include "test_common.h"
     35 #include "exchange-database/do_check_deposit_idempotent.h"
     36 #include "exchange-database/do_deposit.h"
     37 #include "exchange-database/get_exists_deposit.h"
     38 #include "exchange-database/get_ready_deposit.h"
     39 #include "exchange-database/get_wire_hash_for_contract.h"
     40 #include "exchange-database/iterate_batch_deposits_missing_wire.h"
     41 #include "exchange-database/rollback.h"
     42 #include "exchange-database/start.h"
     43 
     44 
     45 /**
     46  * Account the merchants of the checks are paid at.
     47  */
     48 static struct TDB_Account account;
     49 
     50 
     51 /**
     52  * Denomination the checks deposit.
     53  */
     54 static struct TDB_Denom denom;
     55 
     56 
     57 /**
     58  * Build a timestamp from a number of seconds since the epoch.
     59  *
     60  * @param secs seconds since the epoch
     61  * @return the timestamp
     62  */
     63 static struct GNUNET_TIME_Timestamp
     64 ts (uint64_t secs)
     65 {
     66   struct GNUNET_TIME_Absolute abs = {
     67     .abs_value_us = secs * 1000LLU * 1000LLU
     68   };
     69 
     70   return GNUNET_TIME_absolute_to_timestamp (abs);
     71 }
     72 
     73 
     74 /**
     75  * Closure for #missing_cb().
     76  */
     77 struct MissingContext
     78 {
     79   /**
     80    * How many deposits did the callback see?
     81    */
     82   unsigned int total;
     83 
     84   /**
     85    * Row of the first deposit seen.
     86    */
     87   uint64_t first_row;
     88 
     89   /**
     90    * Sum of the whole-unit parts of the amounts seen.
     91    */
     92   uint64_t value_sum;
     93 
     94   /**
     95    * Account of the last deposit seen.
     96    */
     97   struct TALER_FullPaytoHashP h_payto;
     98 };
     99 
    100 
    101 /**
    102  * Callback for #TALER_EXCHANGEDB_iterate_batch_deposits_missing_wire().
    103  *
    104  * @param cls a `struct MissingContext *`
    105  * @param batch_deposit_serial_id row of the deposit
    106  * @param total_amount how much is owed
    107  * @param wire_target_h_payto where it is owed
    108  * @param deadline when it was due
    109  */
    110 static void
    111 missing_cb (void *cls,
    112             uint64_t batch_deposit_serial_id,
    113             const struct TALER_Amount *total_amount,
    114             const struct TALER_FullPaytoHashP *wire_target_h_payto,
    115             struct GNUNET_TIME_Timestamp deadline)
    116 {
    117   struct MissingContext *ctx = cls;
    118 
    119   (void) deadline;
    120   if (0 == ctx->total++)
    121     ctx->first_row = batch_deposit_serial_id;
    122   ctx->value_sum += total_amount->value;
    123   ctx->h_payto = *wire_target_h_payto;
    124 }
    125 
    126 
    127 /**
    128  * Nothing is known while the table is empty.
    129  *
    130  * @param pg the database context
    131  * @return 0 on success
    132  */
    133 static int
    134 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
    135 {
    136   struct TALER_MerchantPublicKeyP merchant_pub;
    137   struct TALER_PrivateContractHashP h_contract_terms;
    138   struct TALER_MerchantWireHashP h_wire;
    139   struct TALER_CoinSpendPublicKeyP coin_pub;
    140   struct TALER_Amount deposit_fee;
    141   struct GNUNET_TIME_Timestamp exchange_timestamp;
    142   struct TALER_FullPayto payto_uri = { NULL };
    143   struct MissingContext ctx = { 0 };
    144   char *metadata = NULL;
    145 
    146   TDB_FILL (merchant_pub,
    147             1);
    148   TDB_FILL (h_contract_terms,
    149             1);
    150   TDB_FILL (h_wire,
    151             1);
    152   TDB_FILL (coin_pub,
    153             1);
    154   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    155           TALER_EXCHANGEDB_get_wire_hash_for_contract (pg,
    156                                                        &merchant_pub,
    157                                                        &h_contract_terms,
    158                                                        &h_wire));
    159   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    160           TALER_EXCHANGEDB_get_exists_deposit (pg,
    161                                                &h_contract_terms,
    162                                                &h_wire,
    163                                                &coin_pub,
    164                                                &merchant_pub,
    165                                                ts (1600000000),
    166                                                &deposit_fee,
    167                                                &exchange_timestamp));
    168   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    169           TALER_EXCHANGEDB_get_ready_deposit (pg,
    170                                               0,
    171                                               UINT32_MAX,
    172                                               &merchant_pub,
    173                                               &payto_uri,
    174                                               &metadata));
    175   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    176           TALER_EXCHANGEDB_iterate_batch_deposits_missing_wire (pg,
    177                                                                 0,
    178                                                                 &missing_cb,
    179                                                                 &ctx));
    180   FAILIF (0 != ctx.total);
    181   return 0;
    182 }
    183 
    184 
    185 /**
    186  * A deposit of more than the coin holds is refused.
    187  *
    188  * @param pg the database context
    189  * @return 0 on success
    190  */
    191 static int
    192 check_insufficient_balance (struct TALER_EXCHANGEDB_PostgresContext *pg)
    193 {
    194   struct TALER_CoinPublicInfo coin;
    195   struct TALER_EXCHANGEDB_CoinDepositInformation cdi;
    196   struct TALER_EXCHANGEDB_BatchDeposit bd;
    197   struct TALER_Amount fees[1];
    198   struct TALER_Amount total;
    199   struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
    200   uint32_t bad_balance_index = 42;
    201   bool balance_ok = true;
    202   bool in_conflict = true;
    203 
    204   TDB_denom (pg,
    205              10,
    206              "5",
    207              "0.1",
    208              &denom);
    209   TDB_account (pg,
    210                10,
    211                &account);
    212   TDB_coin (pg,
    213             &denom,
    214             20,
    215             &coin,
    216             NULL);
    217   memset (&cdi,
    218           0,
    219           sizeof (cdi));
    220   cdi.coin = coin;
    221   TDB_FILL (cdi.csig,
    222             20);
    223   cdi.amount_with_fee = TDB_amount ("6");
    224   fees[0] = TDB_amount ("0.1");
    225   memset (&bd,
    226           0,
    227           sizeof (bd));
    228   TDB_FILL (bd.merchant_pub,
    229             20);
    230   TDB_FILL (bd.merchant_sig,
    231             20);
    232   TDB_FILL (bd.h_contract_terms,
    233             20);
    234   bd.no_wallet_data_hash = true;
    235   bd.wire_target_h_payto = account.h_full;
    236   TDB_FILL (bd.wire_salt,
    237             20);
    238   bd.wallet_timestamp = now;
    239   bd.refund_deadline = now;
    240   bd.wire_deadline = now;
    241   bd.receiver_wire_account = account.payto;
    242   bd.cdis = &cdi;
    243   bd.num_cdis = 1;
    244   /* The batch row goes in before the per-coin balance is checked, so this
    245      is how the /deposit handler uses the function: inside a transaction it
    246      can roll back. */
    247   FAILIF_C (GNUNET_OK !=
    248             TALER_EXCHANGEDB_start (pg,
    249                                     "test-deposit-insufficient"),
    250             TDB_coin_free (&coin));
    251   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    252             TALER_EXCHANGEDB_do_deposit (pg,
    253                                          &bd,
    254                                          fees,
    255                                          &now,
    256                                          &total,
    257                                          &balance_ok,
    258                                          &bad_balance_index,
    259                                          &in_conflict),
    260             TALER_EXCHANGEDB_rollback (pg); TDB_coin_free (&coin));
    261   TALER_EXCHANGEDB_rollback (pg);
    262   TDB_coin_free (&coin);
    263   FAILIF (balance_ok);
    264   FAILIF (in_conflict);
    265   FAILIF (0 != bad_balance_index);
    266   FAILIF (0 != TDB_count (pg,
    267                           "FROM batch_deposits"));
    268   return 0;
    269 }
    270 
    271 
    272 /**
    273  * A deposit within the coin's balance is recorded and can be found again.
    274  *
    275  * @param pg the database context
    276  * @return 0 on success
    277  */
    278 static int
    279 check_deposit (struct TALER_EXCHANGEDB_PostgresContext *pg)
    280 {
    281   struct TALER_CoinPublicInfo coin;
    282   struct TDB_Deposit dep;
    283   struct TALER_MerchantWireHashP h_wire;
    284   struct TALER_Amount deposit_fee;
    285   struct TALER_Amount expect_fee = TDB_amount ("0.1");
    286   struct GNUNET_TIME_Timestamp exchange_timestamp;
    287 
    288   TDB_coin (pg,
    289             &denom,
    290             21,
    291             &coin,
    292             NULL);
    293   TDB_deposit (pg,
    294                &account,
    295                &coin,
    296                21,
    297                "1",
    298                "0.1",
    299                ts (1600000000),
    300                ts (1600000000),
    301                &dep);
    302   FAILIF_C (1 != TDB_count (pg,
    303                             "FROM batch_deposits"),
    304             TDB_coin_free (&coin));
    305   FAILIF_C (1 != TDB_count (pg,
    306                             "FROM coin_deposits"),
    307             TDB_coin_free (&coin));
    308 
    309   /* the salted account hash comes back from the contract */
    310   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    311             TALER_EXCHANGEDB_get_wire_hash_for_contract (pg,
    312                                                          &dep.merchant_pub,
    313                                                          &dep.h_contract_terms,
    314                                                          &h_wire),
    315             TDB_coin_free (&coin));
    316   FAILIF_C (0 != GNUNET_memcmp (&h_wire,
    317                                 &dep.h_wire),
    318             TDB_coin_free (&coin));
    319 
    320   /* ...and the deposit itself, with its fee */
    321   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    322             TALER_EXCHANGEDB_get_exists_deposit (pg,
    323                                                  &dep.h_contract_terms,
    324                                                  &dep.h_wire,
    325                                                  &coin.coin_pub,
    326                                                  &dep.merchant_pub,
    327                                                  dep.refund_deadline,
    328                                                  &deposit_fee,
    329                                                  &exchange_timestamp),
    330             TDB_coin_free (&coin));
    331   FAILIF_C (0 != TALER_amount_cmp (&deposit_fee,
    332                                    &expect_fee),
    333             TDB_coin_free (&coin));
    334 
    335   /* a different refund deadline makes it a different deposit */
    336   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    337             TALER_EXCHANGEDB_get_exists_deposit (pg,
    338                                                  &dep.h_contract_terms,
    339                                                  &dep.h_wire,
    340                                                  &coin.coin_pub,
    341                                                  &dep.merchant_pub,
    342                                                  ts (1700000000),
    343                                                  &deposit_fee,
    344                                                  &exchange_timestamp),
    345             TDB_coin_free (&coin));
    346   /* ...and so does a different account hash */
    347   {
    348     struct TALER_MerchantWireHashP other;
    349 
    350     TDB_FILL (other,
    351               99);
    352     FAILIF_C (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    353               TALER_EXCHANGEDB_get_exists_deposit (pg,
    354                                                    &dep.h_contract_terms,
    355                                                    &other,
    356                                                    &coin.coin_pub,
    357                                                    &dep.merchant_pub,
    358                                                    dep.refund_deadline,
    359                                                    &deposit_fee,
    360                                                    &exchange_timestamp),
    361               TDB_coin_free (&coin));
    362   }
    363   TDB_coin_free (&coin);
    364   return 0;
    365 }
    366 
    367 
    368 /**
    369  * A replayed deposit is recognised as idempotent; a different one is not.
    370  *
    371  * @param pg the database context
    372  * @return 0 on success
    373  */
    374 static int
    375 check_idempotent (struct TALER_EXCHANGEDB_PostgresContext *pg)
    376 {
    377   struct TALER_CoinPublicInfo coin;
    378   struct TALER_EXCHANGEDB_CoinDepositInformation cdi;
    379   struct TALER_EXCHANGEDB_BatchDeposit bd;
    380   struct GNUNET_TIME_Timestamp exchange_timestamp;
    381   bool is_idempotent = false;
    382 
    383   TDB_coin (pg,
    384             &denom,
    385             21,
    386             &coin,
    387             NULL);
    388   memset (&cdi,
    389           0,
    390           sizeof (cdi));
    391   cdi.coin = coin;
    392   TDB_FILL (cdi.csig,
    393             21);
    394   cdi.amount_with_fee = TDB_amount ("1");
    395   memset (&bd,
    396           0,
    397           sizeof (bd));
    398   TDB_FILL (bd.merchant_pub,
    399             21);
    400   TDB_FILL (bd.merchant_sig,
    401             21);
    402   TDB_FILL (bd.h_contract_terms,
    403             21);
    404   bd.no_wallet_data_hash = true;
    405   bd.wire_target_h_payto = account.h_full;
    406   TDB_FILL (bd.wire_salt,
    407             21);
    408   bd.wallet_timestamp = ts (1600000000);
    409   bd.refund_deadline = ts (1600000000);
    410   bd.wire_deadline = ts (1600000000);
    411   bd.receiver_wire_account = account.payto;
    412   bd.cdis = &cdi;
    413   bd.num_cdis = 1;
    414   exchange_timestamp = GNUNET_TIME_timestamp_get ();
    415   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    416             TALER_EXCHANGEDB_do_check_deposit_idempotent (
    417               pg,
    418               &bd,
    419               &exchange_timestamp,
    420               &is_idempotent),
    421             TDB_coin_free (&coin));
    422   FAILIF_C (! is_idempotent,
    423             TDB_coin_free (&coin));
    424 
    425   /* a different amount for the same contract is not the same request */
    426   cdi.amount_with_fee = TDB_amount ("2");
    427   is_idempotent = true;
    428   FAILIF_C (0 >
    429             TALER_EXCHANGEDB_do_check_deposit_idempotent (
    430               pg,
    431               &bd,
    432               &exchange_timestamp,
    433               &is_idempotent),
    434             TDB_coin_free (&coin));
    435   FAILIF_C (is_idempotent,
    436             TDB_coin_free (&coin));
    437 
    438   /* neither is a contract we never saw */
    439   cdi.amount_with_fee = TDB_amount ("1");
    440   TDB_FILL (bd.h_contract_terms,
    441             98);
    442   is_idempotent = true;
    443   FAILIF_C (0 >
    444             TALER_EXCHANGEDB_do_check_deposit_idempotent (
    445               pg,
    446               &bd,
    447               &exchange_timestamp,
    448               &is_idempotent),
    449             TDB_coin_free (&coin));
    450   FAILIF_C (is_idempotent,
    451             TDB_coin_free (&coin));
    452   TDB_coin_free (&coin);
    453   return 0;
    454 }
    455 
    456 
    457 /**
    458  * A deposit whose deadlines have passed becomes ready for the aggregator,
    459  * and shows up as missing a wire transfer.
    460  *
    461  * @param pg the database context
    462  * @return 0 on success
    463  */
    464 static int
    465 check_ready (struct TALER_EXCHANGEDB_PostgresContext *pg)
    466 {
    467   struct TALER_MerchantPublicKeyP merchant_pub;
    468   struct TALER_FullPayto payto_uri = { NULL };
    469   struct MissingContext ctx;
    470   char *metadata = NULL;
    471 
    472   /* the deposit from check_deposit() has deadlines in 2020 */
    473   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    474           TALER_EXCHANGEDB_get_ready_deposit (pg,
    475                                               0,
    476                                               UINT32_MAX,
    477                                               &merchant_pub,
    478                                               &payto_uri,
    479                                               &metadata));
    480   FAILIF_C (0 != strcmp (payto_uri.full_payto,
    481                          account.payto.full_payto),
    482             GNUNET_free (payto_uri.full_payto); GNUNET_free (metadata));
    483   GNUNET_free (payto_uri.full_payto);
    484   GNUNET_free (metadata);
    485   /* no extra metadata was set */
    486   FAILIF (NULL != metadata);
    487 
    488   /* a shard range the merchant is not in has nothing ready */
    489   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    490           TALER_EXCHANGEDB_get_ready_deposit (pg,
    491                                               0,
    492                                               0,
    493                                               &merchant_pub,
    494                                               &payto_uri,
    495                                               &metadata));
    496 
    497   memset (&ctx,
    498           0,
    499           sizeof (ctx));
    500   FAILIF (0 >=
    501           TALER_EXCHANGEDB_iterate_batch_deposits_missing_wire (pg,
    502                                                                 0,
    503                                                                 &missing_cb,
    504                                                                 &ctx));
    505   FAILIF (1 != ctx.total);
    506   FAILIF (0 != GNUNET_memcmp (&ctx.h_payto,
    507                               &account.h_full));
    508   /* the amount reported is the gross deposit, fee included */
    509   FAILIF (1 != ctx.value_sum);
    510 
    511   /* the serial bound excludes it again */
    512   memset (&ctx,
    513           0,
    514           sizeof (ctx));
    515   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    516           TALER_EXCHANGEDB_iterate_batch_deposits_missing_wire (pg,
    517                                                                 1000,
    518                                                                 &missing_cb,
    519                                                                 &ctx));
    520   FAILIF (0 != ctx.total);
    521   return 0;
    522 }
    523 
    524 
    525 /**
    526  * A deposit that is done or policy-blocked is not ready.
    527  *
    528  * @param pg the database context
    529  * @return 0 on success
    530  */
    531 static int
    532 check_not_ready (struct TALER_EXCHANGEDB_PostgresContext *pg)
    533 {
    534   struct TALER_MerchantPublicKeyP merchant_pub;
    535   struct TALER_FullPayto payto_uri = { NULL };
    536   char *metadata = NULL;
    537 
    538   FAILIF (GNUNET_OK !=
    539           TDB_exec (pg,
    540                     "UPDATE batch_deposits SET done=TRUE;"));
    541   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    542           TALER_EXCHANGEDB_get_ready_deposit (pg,
    543                                               0,
    544                                               UINT32_MAX,
    545                                               &merchant_pub,
    546                                               &payto_uri,
    547                                               &metadata));
    548   FAILIF (GNUNET_OK !=
    549           TDB_exec (pg,
    550                     "UPDATE batch_deposits"
    551                     " SET done=FALSE, policy_blocked=TRUE;"));
    552   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    553           TALER_EXCHANGEDB_get_ready_deposit (pg,
    554                                               0,
    555                                               UINT32_MAX,
    556                                               &merchant_pub,
    557                                               &payto_uri,
    558                                               &metadata));
    559   FAILIF (GNUNET_OK !=
    560           TDB_exec (pg,
    561                     "UPDATE batch_deposits SET policy_blocked=FALSE;"));
    562   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    563           TALER_EXCHANGEDB_get_ready_deposit (pg,
    564                                               0,
    565                                               UINT32_MAX,
    566                                               &merchant_pub,
    567                                               &payto_uri,
    568                                               &metadata));
    569   GNUNET_free (payto_uri.full_payto);
    570   GNUNET_free (metadata);
    571   return 0;
    572 }
    573 
    574 
    575 /**
    576  * The checks to run, in order.
    577  */
    578 static const struct TDB_Test tests[] = {
    579   { "batch-deposits-empty",
    580     &check_empty },
    581   { "batch-deposits-insufficient-balance",
    582     &check_insufficient_balance },
    583   { "batch-deposits-deposit",
    584     &check_deposit },
    585   { "batch-deposits-idempotent",
    586     &check_idempotent },
    587   { "batch-deposits-ready",
    588     &check_ready },
    589   { "batch-deposits-not-ready",
    590     &check_not_ready },
    591   { NULL, NULL }
    592 };
    593 
    594 
    595 int
    596 main (int argc,
    597       char *const *argv)
    598 {
    599   int ret;
    600 
    601   ret = TDB_main (argc,
    602                   argv,
    603                   "test-batch-deposits",
    604                   "Tests for the exchangedb `batch_deposits' table",
    605                   tests);
    606   TDB_account_free (&account);
    607   TDB_denom_free (&denom);
    608   return ret;
    609 }
    610 
    611 
    612 /* end of test_batch_deposits.c */