exchange

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

test_reserves_open_deposits.c (12917B)


      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_reserves_open_deposits.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `reserves_open_deposits`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_insert_reserve_open_deposit(), the only
     23  * exported function that touches the table.  The rows are read back by the
     24  * coin history, which test_coin_history.c covers.
     25  *
     26  * The table's primary key is (coin_pub, coin_sig), which is what makes a
     27  * replayed request idempotent.  On insufficient coin balance the row is
     28  * inserted first and the balance check fails afterwards, so the caller has
     29  * to roll back -- the check below does exactly that, since that is how the
     30  * /reserves/$RP/open handler uses the function.
     31  */
     32 #include "test_common.h"
     33 #include "exchange-database/commit.h"
     34 #include "exchange-database/get_known_coin.h"
     35 #include "exchange-database/insert_reserve_open_deposit.h"
     36 #include "exchange-database/rollback.h"
     37 #include "exchange-database/start.h"
     38 
     39 
     40 /**
     41  * Read the remaining balance of a coin.
     42  *
     43  * @param pg the database context
     44  * @param coin_pub coin to look up
     45  * @param[out] remaining set to the coin's remaining value
     46  */
     47 static void
     48 coin_remaining (struct TALER_EXCHANGEDB_PostgresContext *pg,
     49                 const struct TALER_CoinSpendPublicKeyP *coin_pub,
     50                 struct TALER_Amount *remaining)
     51 {
     52   struct GNUNET_PQ_QueryParam params[] = {
     53     GNUNET_PQ_query_param_auto_from_type (coin_pub),
     54     GNUNET_PQ_query_param_end
     55   };
     56   struct GNUNET_PQ_ResultSpec rs[] = {
     57     TALER_PQ_result_spec_amount ("remaining",
     58                                  pg->currency,
     59                                  remaining),
     60     GNUNET_PQ_result_spec_end
     61   };
     62 
     63   GNUNET_assert (GNUNET_OK ==
     64                  GNUNET_PQ_prepare_anon (pg->conn,
     65                                          "SELECT remaining"
     66                                          " FROM known_coins"
     67                                          " WHERE coin_pub=$1;"));
     68   GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
     69                  GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
     70                                                            "",
     71                                                            params,
     72                                                            rs));
     73 }
     74 
     75 
     76 /**
     77  * Spend part of a coin on keeping a reserve open.
     78  *
     79  * @param pg the database context
     80  * @param coin the coin to spend from
     81  * @param known_coin_id row of @a coin
     82  * @param reserve_pub reserve being kept open
     83  * @param seed seed for the coin and reserve signatures
     84  * @param amount how much of the coin to spend, e.g. "1"
     85  * @param[out] insufficient_funds set if the coin does not have that much
     86  * @return transaction status
     87  */
     88 static enum GNUNET_DB_QueryStatus
     89 open_deposit (struct TALER_EXCHANGEDB_PostgresContext *pg,
     90               const struct TALER_CoinPublicInfo *coin,
     91               uint64_t known_coin_id,
     92               const struct TALER_ReservePublicKeyP *reserve_pub,
     93               uint32_t seed,
     94               const char *amount,
     95               bool *insufficient_funds)
     96 {
     97   struct TALER_CoinSpendSignatureP coin_sig;
     98   struct TALER_ReserveSignatureP reserve_sig;
     99   struct TALER_Amount a = TDB_amount (amount);
    100 
    101   TDB_fill (&coin_sig,
    102             sizeof (coin_sig),
    103             seed);
    104   TDB_fill (&reserve_sig,
    105             sizeof (reserve_sig),
    106             seed);
    107   return TALER_EXCHANGEDB_insert_reserve_open_deposit (pg,
    108                                                        coin,
    109                                                        &coin_sig,
    110                                                        known_coin_id,
    111                                                        &a,
    112                                                        &reserve_sig,
    113                                                        reserve_pub,
    114                                                        insufficient_funds);
    115 }
    116 
    117 
    118 /**
    119  * Spending part of a coin records the deposit and debits the coin.
    120  *
    121  * @param pg the database context
    122  * @return 0 on success
    123  */
    124 static int
    125 check_deposit (struct TALER_EXCHANGEDB_PostgresContext *pg)
    126 {
    127   struct TDB_Denom denom;
    128   struct TALER_CoinPublicInfo coin;
    129   struct TALER_ReservePublicKeyP reserve_pub;
    130   struct TALER_Amount remaining;
    131   struct TALER_Amount expect = TDB_amount ("4");
    132   uint64_t known_coin_id;
    133   bool insufficient = true;
    134 
    135   FAILIF (0 != TDB_count (pg,
    136                           "FROM reserves_open_deposits"));
    137   TDB_denom (pg,
    138              10,
    139              "5",
    140              "0.1",
    141              &denom);
    142   TDB_coin (pg,
    143             &denom,
    144             10,
    145             &coin,
    146             &known_coin_id);
    147   TDB_reserve (pg,
    148                10,
    149                "0",
    150                &reserve_pub);
    151 
    152   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    153             open_deposit (pg,
    154                           &coin,
    155                           known_coin_id,
    156                           &reserve_pub,
    157                           10,
    158                           "1",
    159                           &insufficient),
    160             TDB_coin_free (&coin); TDB_denom_free (&denom));
    161   FAILIF_C (insufficient,
    162             TDB_coin_free (&coin); TDB_denom_free (&denom));
    163   FAILIF_C (1 != TDB_count (pg,
    164                             "FROM reserves_open_deposits"),
    165             TDB_coin_free (&coin); TDB_denom_free (&denom));
    166   coin_remaining (pg,
    167                   &coin.coin_pub,
    168                   &remaining);
    169   FAILIF_C (0 != TALER_amount_cmp (&remaining,
    170                                    &expect),
    171             TDB_coin_free (&coin); TDB_denom_free (&denom));
    172 
    173   /* a replay of the very same request is absorbed and does not debit the
    174      coin a second time */
    175   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    176             open_deposit (pg,
    177                           &coin,
    178                           known_coin_id,
    179                           &reserve_pub,
    180                           10,
    181                           "1",
    182                           &insufficient),
    183             TDB_coin_free (&coin); TDB_denom_free (&denom));
    184   FAILIF_C (insufficient,
    185             TDB_coin_free (&coin); TDB_denom_free (&denom));
    186   FAILIF_C (1 != TDB_count (pg,
    187                             "FROM reserves_open_deposits"),
    188             TDB_coin_free (&coin); TDB_denom_free (&denom));
    189   coin_remaining (pg,
    190                   &coin.coin_pub,
    191                   &remaining);
    192   FAILIF_C (0 != TALER_amount_cmp (&remaining,
    193                                    &expect),
    194             TDB_coin_free (&coin); TDB_denom_free (&denom));
    195   TDB_coin_free (&coin);
    196   TDB_denom_free (&denom);
    197   return 0;
    198 }
    199 
    200 
    201 /**
    202  * A second, larger deposit from the same coin is refused, and the caller
    203  * has to roll back to undo the row the function inserted first.
    204  *
    205  * @param pg the database context
    206  * @return 0 on success
    207  */
    208 static int
    209 check_insufficient_funds (struct TALER_EXCHANGEDB_PostgresContext *pg)
    210 {
    211   struct TDB_Denom denom;
    212   struct TALER_CoinPublicInfo coin;
    213   struct TALER_ReservePublicKeyP reserve_pub;
    214   struct TALER_Amount remaining;
    215   struct TALER_Amount expect = TDB_amount ("4");
    216   uint64_t known_coin_id;
    217   bool insufficient = false;
    218 
    219   TDB_denom (pg,
    220              10,
    221              "5",
    222              "0.1",
    223              &denom);
    224   TDB_coin (pg,
    225             &denom,
    226             10,
    227             &coin,
    228             &known_coin_id);
    229   TDB_reserve (pg,
    230                10,
    231                "0",
    232                &reserve_pub);
    233 
    234   FAILIF_C (GNUNET_OK !=
    235             TALER_EXCHANGEDB_start (pg,
    236                                     "test-open-deposit-insufficient"),
    237             TDB_coin_free (&coin); TDB_denom_free (&denom));
    238   /* EUR:4 is left, so EUR:5 cannot be taken */
    239   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    240             open_deposit (pg,
    241                           &coin,
    242                           known_coin_id,
    243                           &reserve_pub,
    244                           11,
    245                           "5",
    246                           &insufficient),
    247             TALER_EXCHANGEDB_rollback (pg);
    248             TDB_coin_free (&coin); TDB_denom_free (&denom));
    249   FAILIF_C (! insufficient,
    250             TALER_EXCHANGEDB_rollback (pg);
    251             TDB_coin_free (&coin); TDB_denom_free (&denom));
    252   TALER_EXCHANGEDB_rollback (pg);
    253   /* the rollback took the row with it */
    254   FAILIF_C (1 != TDB_count (pg,
    255                             "FROM reserves_open_deposits"),
    256             TDB_coin_free (&coin); TDB_denom_free (&denom));
    257   coin_remaining (pg,
    258                   &coin.coin_pub,
    259                   &remaining);
    260   FAILIF_C (0 != TALER_amount_cmp (&remaining,
    261                                    &expect),
    262             TDB_coin_free (&coin); TDB_denom_free (&denom));
    263   TDB_coin_free (&coin);
    264   TDB_denom_free (&denom);
    265   return 0;
    266 }
    267 
    268 
    269 /**
    270  * Spending exactly what is left works and leaves the coin at zero.
    271  *
    272  * @param pg the database context
    273  * @return 0 on success
    274  */
    275 static int
    276 check_exact_balance (struct TALER_EXCHANGEDB_PostgresContext *pg)
    277 {
    278   struct TDB_Denom denom;
    279   struct TALER_CoinPublicInfo coin;
    280   struct TALER_ReservePublicKeyP reserve_pub;
    281   struct TALER_Amount remaining;
    282   struct TALER_Amount zero = TDB_amount ("0");
    283   uint64_t known_coin_id;
    284   bool insufficient = true;
    285 
    286   TDB_denom (pg,
    287              10,
    288              "5",
    289              "0.1",
    290              &denom);
    291   TDB_coin (pg,
    292             &denom,
    293             10,
    294             &coin,
    295             &known_coin_id);
    296   TDB_reserve (pg,
    297                10,
    298                "0",
    299                &reserve_pub);
    300   FAILIF_C (GNUNET_OK !=
    301             TALER_EXCHANGEDB_start (pg,
    302                                     "test-open-deposit-exact"),
    303             TDB_coin_free (&coin); TDB_denom_free (&denom));
    304   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    305             open_deposit (pg,
    306                           &coin,
    307                           known_coin_id,
    308                           &reserve_pub,
    309                           12,
    310                           "4",
    311                           &insufficient),
    312             TALER_EXCHANGEDB_rollback (pg);
    313             TDB_coin_free (&coin); TDB_denom_free (&denom));
    314   FAILIF_C (insufficient,
    315             TALER_EXCHANGEDB_rollback (pg);
    316             TDB_coin_free (&coin); TDB_denom_free (&denom));
    317   FAILIF_C (0 >
    318             TALER_EXCHANGEDB_commit (pg),
    319             TDB_coin_free (&coin); TDB_denom_free (&denom));
    320   coin_remaining (pg,
    321                   &coin.coin_pub,
    322                   &remaining);
    323   FAILIF_C (0 != TALER_amount_cmp (&remaining,
    324                                    &zero),
    325             TDB_coin_free (&coin); TDB_denom_free (&denom));
    326   FAILIF_C (2 != TDB_count (pg,
    327                             "FROM reserves_open_deposits"),
    328             TDB_coin_free (&coin); TDB_denom_free (&denom));
    329   /* and now even the smallest further deposit is refused */
    330   insufficient = false;
    331   FAILIF_C (GNUNET_OK !=
    332             TALER_EXCHANGEDB_start (pg,
    333                                     "test-open-deposit-drained"),
    334             TDB_coin_free (&coin); TDB_denom_free (&denom));
    335   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    336             open_deposit (pg,
    337                           &coin,
    338                           known_coin_id,
    339                           &reserve_pub,
    340                           13,
    341                           "0.01",
    342                           &insufficient),
    343             TALER_EXCHANGEDB_rollback (pg);
    344             TDB_coin_free (&coin); TDB_denom_free (&denom));
    345   FAILIF_C (! insufficient,
    346             TALER_EXCHANGEDB_rollback (pg);
    347             TDB_coin_free (&coin); TDB_denom_free (&denom));
    348   TALER_EXCHANGEDB_rollback (pg);
    349   TDB_coin_free (&coin);
    350   TDB_denom_free (&denom);
    351   return 0;
    352 }
    353 
    354 
    355 /**
    356  * The checks to run, in order.
    357  */
    358 static const struct TDB_Test tests[] = {
    359   { "reserves-open-deposits-deposit",
    360     &check_deposit },
    361   { "reserves-open-deposits-insufficient-funds",
    362     &check_insufficient_funds },
    363   { "reserves-open-deposits-exact-balance",
    364     &check_exact_balance },
    365   { NULL, NULL }
    366 };
    367 
    368 
    369 int
    370 main (int argc,
    371       char *const *argv)
    372 {
    373   return TDB_main (argc,
    374                    argv,
    375                    "test-reserves-open-deposits",
    376                    "Tests for the exchangedb `reserves_open_deposits' table",
    377                    tests);
    378 }
    379 
    380 
    381 /* end of test_reserves_open_deposits.c */