exchange

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

test_reserve_history.c (19090B)


      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_reserve_history.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `reserve_history`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_get_reserve_history().
     23  *
     24  * `reserve_history` is an index: AFTER INSERT triggers on the tables that
     25  * change a reserve add one row each, naming the table and the row.  The
     26  * function walks that index newest-first and looks each entry up in the
     27  * table it names.  So the checks build a reserve with several kinds of
     28  * operation on it and read the history back, and they check the ETag
     29  * short-circuit -- which must not leave a transaction open.
     30  */
     31 #include "test_common.h"
     32 #include "exchange-database/do_recoup.h"
     33 #include "exchange-database/get_reserve_history.h"
     34 #include "exchange-database/insert_close_request.h"
     35 #include "exchange-database/start.h"
     36 #include "exchange-database/rollback.h"
     37 
     38 
     39 /**
     40  * Account the checks use.
     41  */
     42 static struct TDB_Account account;
     43 
     44 
     45 /**
     46  * Count the entries of @a rh, and how many of them have type @a type.
     47  *
     48  * @param rh history to walk
     49  * @param type operation to count
     50  * @param[out] matched set to the number of entries of that type
     51  * @return total number of entries
     52  */
     53 static unsigned int
     54 count_history (const struct TALER_EXCHANGEDB_ReserveHistory *rh,
     55                enum TALER_EXCHANGEDB_ReserveOperation type,
     56                unsigned int *matched)
     57 {
     58   unsigned int total = 0;
     59 
     60   *matched = 0;
     61   for (const struct TALER_EXCHANGEDB_ReserveHistory *p = rh;
     62        NULL != p;
     63        p = p->next)
     64   {
     65     total++;
     66     if (type == p->type)
     67       (*matched)++;
     68   }
     69   return total;
     70 }
     71 
     72 
     73 /**
     74  * An unknown reserve has no history at all.
     75  *
     76  * @param pg the database context
     77  * @return 0 on success
     78  */
     79 static int
     80 check_unknown_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg)
     81 {
     82   struct TALER_ReservePublicKeyP reserve_pub;
     83   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
     84   struct TALER_Amount balance;
     85   uint64_t etag_out = 42;
     86 
     87   TDB_FILL (reserve_pub,
     88             1);
     89   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     90           TALER_EXCHANGEDB_get_reserve_history (pg,
     91                                                 &reserve_pub,
     92                                                 0,
     93                                                 0,
     94                                                 &etag_out,
     95                                                 &balance,
     96                                                 &rh));
     97   FAILIF (NULL != rh);
     98   return 0;
     99 }
    100 
    101 
    102 /**
    103  * A reserve that exists but was never touched has no history either: the
    104  * `reserves` row alone does not create an index entry.
    105  *
    106  * @param pg the database context
    107  * @return 0 on success
    108  */
    109 static int
    110 check_untouched_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg)
    111 {
    112   struct TALER_ReservePublicKeyP reserve_pub;
    113   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
    114   struct TALER_Amount balance;
    115   uint64_t etag_out = 42;
    116 
    117   TDB_reserve (pg,
    118                2,
    119                "5",
    120                &reserve_pub);
    121   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    122           TALER_EXCHANGEDB_get_reserve_history (pg,
    123                                                 &reserve_pub,
    124                                                 0,
    125                                                 0,
    126                                                 &etag_out,
    127                                                 &balance,
    128                                                 &rh));
    129   FAILIF (NULL != rh);
    130   return 0;
    131 }
    132 
    133 
    134 /**
    135  * A bank transfer into the reserve shows up as one history entry.
    136  *
    137  * @param pg the database context
    138  * @return 0 on success
    139  */
    140 static int
    141 check_bank_transfer (struct TALER_EXCHANGEDB_PostgresContext *pg)
    142 {
    143   struct TALER_ReservePublicKeyP reserve_pub;
    144   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
    145   struct TALER_Amount balance;
    146   struct TALER_Amount expect = TDB_amount ("10");
    147   unsigned int matched;
    148   uint64_t etag_out = 0;
    149 
    150   TDB_account (pg,
    151                10,
    152                &account);
    153   TDB_reserve_in (pg,
    154                   &account,
    155                   10,
    156                   "10",
    157                   &reserve_pub);
    158   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    159           TALER_EXCHANGEDB_get_reserve_history (pg,
    160                                                 &reserve_pub,
    161                                                 0,
    162                                                 0,
    163                                                 &etag_out,
    164                                                 &balance,
    165                                                 &rh));
    166   FAILIF_C (NULL == rh,
    167             TALER_EXCHANGEDB_free_reserve_history (rh));
    168   FAILIF_C (0 == etag_out,
    169             TALER_EXCHANGEDB_free_reserve_history (rh));
    170   FAILIF_C (0 != TALER_amount_cmp (&balance,
    171                                    &expect),
    172             TALER_EXCHANGEDB_free_reserve_history (rh));
    173   FAILIF_C (1 != count_history (rh,
    174                                 TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE,
    175                                 &matched),
    176             TALER_EXCHANGEDB_free_reserve_history (rh));
    177   FAILIF_C (1 != matched,
    178             TALER_EXCHANGEDB_free_reserve_history (rh));
    179   FAILIF_C (0 != TALER_amount_cmp (&rh->details.bank->amount,
    180                                    &expect),
    181             TALER_EXCHANGEDB_free_reserve_history (rh));
    182   FAILIF_C (0 != strcmp (rh->details.bank->sender_account_details.full_payto,
    183                          account.payto.full_payto),
    184             TALER_EXCHANGEDB_free_reserve_history (rh));
    185   TALER_EXCHANGEDB_free_reserve_history (rh);
    186   return 0;
    187 }
    188 
    189 
    190 /**
    191  * A close request adds a second entry, and the offset filter hides the
    192  * older one.
    193  *
    194  * @param pg the database context
    195  * @return 0 on success
    196  */
    197 static int
    198 check_two_entries (struct TALER_EXCHANGEDB_PostgresContext *pg)
    199 {
    200   struct TALER_ReservePublicKeyP reserve_pub;
    201   struct TALER_ReserveSignatureP reserve_sig;
    202   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
    203   struct TALER_Amount balance;
    204   struct TALER_Amount close_balance = TDB_amount ("10");
    205   struct TALER_Amount close_fee = TDB_amount ("0.5");
    206   unsigned int matched;
    207   uint64_t etag_first;
    208   uint64_t etag_out = 0;
    209   uint64_t first_off;
    210 
    211   TDB_fill (&reserve_pub,
    212             sizeof (reserve_pub),
    213             10);
    214   TDB_FILL (reserve_sig,
    215             10);
    216   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    217           TALER_EXCHANGEDB_get_reserve_history (pg,
    218                                                 &reserve_pub,
    219                                                 0,
    220                                                 0,
    221                                                 &etag_first,
    222                                                 &balance,
    223                                                 &rh));
    224   first_off = rh->history_offset;
    225   TALER_EXCHANGEDB_free_reserve_history (rh);
    226   rh = NULL;
    227 
    228   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    229           TALER_EXCHANGEDB_insert_close_request (pg,
    230                                                  &reserve_pub,
    231                                                  account.payto,
    232                                                  &reserve_sig,
    233                                                  GNUNET_TIME_timestamp_get (),
    234                                                  &close_balance,
    235                                                  &close_fee));
    236   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    237           TALER_EXCHANGEDB_get_reserve_history (pg,
    238                                                 &reserve_pub,
    239                                                 0,
    240                                                 0,
    241                                                 &etag_out,
    242                                                 &balance,
    243                                                 &rh));
    244   FAILIF_C (2 != count_history (rh,
    245                                 TALER_EXCHANGEDB_RO_CLOSE_REQUEST,
    246                                 &matched),
    247             TALER_EXCHANGEDB_free_reserve_history (rh));
    248   FAILIF_C (1 != matched,
    249             TALER_EXCHANGEDB_free_reserve_history (rh));
    250   /* The query runs newest-first but the rows are walked back to front, so
    251      the list that comes out is oldest-first. */
    252   FAILIF_C (TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE != rh->type,
    253             TALER_EXCHANGEDB_free_reserve_history (rh));
    254   FAILIF_C (TALER_EXCHANGEDB_RO_CLOSE_REQUEST != rh->next->type,
    255             TALER_EXCHANGEDB_free_reserve_history (rh));
    256   FAILIF_C (rh->history_offset >= rh->next->history_offset,
    257             TALER_EXCHANGEDB_free_reserve_history (rh));
    258   FAILIF_C (etag_out <= etag_first,
    259             TALER_EXCHANGEDB_free_reserve_history (rh));
    260   TALER_EXCHANGEDB_free_reserve_history (rh);
    261   rh = NULL;
    262 
    263   /* starting past the bank transfer leaves only the close request */
    264   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    265           TALER_EXCHANGEDB_get_reserve_history (pg,
    266                                                 &reserve_pub,
    267                                                 first_off,
    268                                                 0,
    269                                                 &etag_out,
    270                                                 &balance,
    271                                                 &rh));
    272   FAILIF_C (1 != count_history (rh,
    273                                 TALER_EXCHANGEDB_RO_CLOSE_REQUEST,
    274                                 &matched),
    275             TALER_EXCHANGEDB_free_reserve_history (rh));
    276   FAILIF_C (1 != matched,
    277             TALER_EXCHANGEDB_free_reserve_history (rh));
    278   TALER_EXCHANGEDB_free_reserve_history (rh);
    279   rh = NULL;
    280 
    281   /* starting past everything leaves an empty history, but the reserve is
    282      still known and the ETag is still reported */
    283   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    284           TALER_EXCHANGEDB_get_reserve_history (pg,
    285                                                 &reserve_pub,
    286                                                 etag_out,
    287                                                 0,
    288                                                 &etag_out,
    289                                                 &balance,
    290                                                 &rh));
    291   FAILIF_C (NULL != rh,
    292             TALER_EXCHANGEDB_free_reserve_history (rh));
    293   return 0;
    294 }
    295 
    296 
    297 /**
    298  * A recoup shows up as a RECOUP entry that names the coin, the amount
    299  * and the withdraw operation the coin came from.
    300  *
    301  * @param pg the database context
    302  * @return 0 on success
    303  */
    304 static int
    305 check_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg)
    306 {
    307   struct TDB_Denom denom;
    308   struct TALER_CoinPublicInfo coin;
    309   struct TALER_ReservePublicKeyP reserve_pub;
    310   struct TALER_HashBlindedPlanchetsP planchets_h;
    311   struct TALER_CoinSpendSignatureP coin_sig;
    312   union GNUNET_CRYPTO_BlindingSecretP coin_bks;
    313   struct GNUNET_TIME_Timestamp recoup_timestamp;
    314   struct TALER_Amount recoup_amount;
    315   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
    316   struct TALER_Amount balance;
    317   struct TALER_Amount expect_value = TDB_amount ("5");
    318   struct TALER_Amount expect_balance = TDB_amount ("10");
    319   const struct TALER_EXCHANGEDB_Recoup *recoup = NULL;
    320   unsigned int matched = 0;
    321   uint64_t withdraw_id;
    322   uint64_t known_coin_id;
    323   uint64_t etag = 0;
    324   bool recoup_ok;
    325   bool internal_failure;
    326 
    327   TDB_denom (pg,
    328              30,
    329              "5",
    330              "0.1",
    331              &denom);
    332   TDB_reserve_in (pg,
    333                   &account,
    334                   30,
    335                   "10",
    336                   &reserve_pub);
    337   withdraw_id = TDB_withdraw (pg,
    338                               &denom,
    339                               &reserve_pub,
    340                               30,
    341                               "5");
    342   /* TDB_withdraw() seeds the commitment with its seed */
    343   TDB_FILL (planchets_h,
    344             30);
    345   TDB_coin (pg,
    346             &denom,
    347             31,
    348             &coin,
    349             &known_coin_id);
    350   TDB_FILL (coin_sig,
    351             31);
    352   TDB_FILL (coin_bks,
    353             31);
    354   recoup_timestamp = GNUNET_TIME_timestamp_get ();
    355   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    356             TALER_EXCHANGEDB_do_recoup (pg,
    357                                         &reserve_pub,
    358                                         withdraw_id,
    359                                         2,
    360                                         &coin_bks,
    361                                         &coin.coin_pub,
    362                                         known_coin_id,
    363                                         &coin_sig,
    364                                         &recoup_timestamp,
    365                                         &recoup_amount,
    366                                         &recoup_ok,
    367                                         &internal_failure),
    368             TDB_coin_free (&coin); TDB_denom_free (&denom));
    369   FAILIF_C (internal_failure || ! recoup_ok,
    370             TDB_coin_free (&coin); TDB_denom_free (&denom));
    371   FAILIF_C (0 != TALER_amount_cmp (&recoup_amount,
    372                                    &expect_value),
    373             TDB_coin_free (&coin); TDB_denom_free (&denom));
    374   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    375             TALER_EXCHANGEDB_get_reserve_history (pg,
    376                                                   &reserve_pub,
    377                                                   0,
    378                                                   0,
    379                                                   &etag,
    380                                                   &balance,
    381                                                   &rh),
    382             TDB_coin_free (&coin); TDB_denom_free (&denom));
    383   /* bank transfer in, withdraw, recoup */
    384   FAILIF_C (3 != count_history (rh,
    385                                 TALER_EXCHANGEDB_RO_RECOUP_COIN,
    386                                 &matched),
    387             TALER_EXCHANGEDB_free_reserve_history (rh);
    388             TDB_coin_free (&coin); TDB_denom_free (&denom));
    389   FAILIF_C (1 != matched,
    390             TALER_EXCHANGEDB_free_reserve_history (rh);
    391             TDB_coin_free (&coin); TDB_denom_free (&denom));
    392   for (const struct TALER_EXCHANGEDB_ReserveHistory *p = rh;
    393        NULL != p;
    394        p = p->next)
    395     if (TALER_EXCHANGEDB_RO_RECOUP_COIN == p->type)
    396       recoup = p->details.recoup;
    397   FAILIF_C (0 != GNUNET_memcmp (&recoup->coin.coin_pub,
    398                                 &coin.coin_pub),
    399             TALER_EXCHANGEDB_free_reserve_history (rh);
    400             TDB_coin_free (&coin); TDB_denom_free (&denom));
    401   FAILIF_C (0 != GNUNET_memcmp (&recoup->planchets_h,
    402                                 &planchets_h),
    403             TALER_EXCHANGEDB_free_reserve_history (rh);
    404             TDB_coin_free (&coin); TDB_denom_free (&denom));
    405   FAILIF_C (0 != GNUNET_memcmp (&recoup->reserve_pub,
    406                                 &reserve_pub),
    407             TALER_EXCHANGEDB_free_reserve_history (rh);
    408             TDB_coin_free (&coin); TDB_denom_free (&denom));
    409   FAILIF_C (0 != TALER_amount_cmp (&recoup->value,
    410                                    &expect_value),
    411             TALER_EXCHANGEDB_free_reserve_history (rh);
    412             TDB_coin_free (&coin); TDB_denom_free (&denom));
    413   /* 10 in, 5 withdrawn, 5 recouped */
    414   FAILIF_C (0 != TALER_amount_cmp (&balance,
    415                                    &expect_balance),
    416             TALER_EXCHANGEDB_free_reserve_history (rh);
    417             TDB_coin_free (&coin); TDB_denom_free (&denom));
    418   TALER_EXCHANGEDB_free_reserve_history (rh);
    419   TDB_coin_free (&coin);
    420   TDB_denom_free (&denom);
    421   return 0;
    422 }
    423 
    424 
    425 /**
    426  * Passing the current ETag short-circuits the lookup -- and must not leave
    427  * the transaction it opened behind.
    428  *
    429  * @param pg the database context
    430  * @return 0 on success
    431  */
    432 static int
    433 check_etag (struct TALER_EXCHANGEDB_PostgresContext *pg)
    434 {
    435   struct TALER_ReservePublicKeyP reserve_pub;
    436   struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
    437   struct TALER_Amount balance;
    438   uint64_t etag_out = 0;
    439   uint64_t etag_again = 0;
    440 
    441   TDB_fill (&reserve_pub,
    442             sizeof (reserve_pub),
    443             10);
    444   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    445           TALER_EXCHANGEDB_get_reserve_history (pg,
    446                                                 &reserve_pub,
    447                                                 0,
    448                                                 0,
    449                                                 &etag_out,
    450                                                 &balance,
    451                                                 &rh));
    452   TALER_EXCHANGEDB_free_reserve_history (rh);
    453   rh = NULL;
    454 
    455   /* the client already has this version */
    456   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    457           TALER_EXCHANGEDB_get_reserve_history (pg,
    458                                                 &reserve_pub,
    459                                                 0,
    460                                                 etag_out,
    461                                                 &etag_again,
    462                                                 &balance,
    463                                                 &rh));
    464   FAILIF (etag_again != etag_out);
    465   FAILIF (NULL != rh);
    466 
    467   /* The short-circuit above returns early.  If it forgets to close the
    468      read-committed transaction it opened, the connection is parked in
    469      "idle in transaction" -- which the next TALER_EXCHANGEDB_start() would
    470      paper over, since its preflight rolls the stale transaction back and
    471      only logs it.  So check the bookkeeping preflight itself looks at. */
    472   FAILIF (NULL != pg->transaction_name);
    473   FAILIF (GNUNET_OK !=
    474           TALER_EXCHANGEDB_start (pg,
    475                                   "test-reserve-history-etag"));
    476   FAILIF (NULL == pg->transaction_name);
    477   TALER_EXCHANGEDB_rollback (pg);
    478   FAILIF (NULL != pg->transaction_name);
    479   return 0;
    480 }
    481 
    482 
    483 /**
    484  * The checks to run, in order.
    485  */
    486 static const struct TDB_Test tests[] = {
    487   { "reserve-history-unknown-reserve",
    488     &check_unknown_reserve },
    489   { "reserve-history-untouched-reserve",
    490     &check_untouched_reserve },
    491   { "reserve-history-bank-transfer",
    492     &check_bank_transfer },
    493   { "reserve-history-two-entries",
    494     &check_two_entries },
    495   { "reserve-history-recoup",
    496     &check_recoup },
    497   { "reserve-history-etag",
    498     &check_etag },
    499   { NULL, NULL }
    500 };
    501 
    502 
    503 int
    504 main (int argc,
    505       char *const *argv)
    506 {
    507   int ret;
    508 
    509   ret = TDB_main (argc,
    510                   argv,
    511                   "test-reserve-history",
    512                   "Tests for the exchangedb `reserve_history' table",
    513                   tests);
    514   TDB_account_free (&account);
    515   return ret;
    516 }
    517 
    518 
    519 /* end of test_reserve_history.c */