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_requests.c (19331B)


      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_requests.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `reserves_open_requests`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_do_reserve_open() and
     23  * #TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id().
     24  *
     25  * The table references `reserves`, which TDB_reserve() creates.  The
     26  * interesting cases of do_reserve_open() are the ones where it declines to
     27  * do the work: an unknown reserve, a payment that does not cover the cost,
     28  * a reserve that cannot pay its share, and a request that asks for nothing
     29  * that is not already granted.  Only the success path writes the table.
     30  */
     31 #include "test_common.h"
     32 #include "exchange-database/do_reserve_open.h"
     33 #include "exchange-database/get_reserve.h"
     34 #include "exchange-database/iterate_reserve_open_above_serial_id.h"
     35 
     36 
     37 /**
     38  * A year in microseconds, as exchange_do_reserve_open() counts it.
     39  */
     40 #define YEAR_US 31536000000000LLU
     41 
     42 
     43 /**
     44  * Closure for #open_cb().
     45  */
     46 struct OpenContext
     47 {
     48   /**
     49    * How many rows did the callback see?
     50    */
     51   unsigned int total;
     52 
     53   /**
     54    * Stop after this many rows; 0 for no limit.
     55    */
     56   unsigned int stop_after;
     57 
     58   /**
     59    * Reserve we are looking for, NULL to match nothing.
     60    */
     61   const struct TALER_ReservePublicKeyP *reserve_pub;
     62 
     63   /**
     64    * How many times did we see @e reserve_pub?
     65    */
     66   unsigned int matched;
     67 
     68   /**
     69    * Reserve payment reported for @e reserve_pub.
     70    */
     71   struct TALER_Amount reserve_payment;
     72 
     73   /**
     74    * Purse limit reported for @e reserve_pub.
     75    */
     76   uint32_t purse_limit;
     77 
     78   /**
     79    * Signature reported for @e reserve_pub.
     80    */
     81   struct TALER_ReserveSignatureP reserve_sig;
     82 
     83   /**
     84    * Expiration reported for @e reserve_pub.
     85    */
     86   struct GNUNET_TIME_Timestamp expiration;
     87 
     88   /**
     89    * Request timestamp reported for @e reserve_pub.
     90    */
     91   struct GNUNET_TIME_Timestamp request_timestamp;
     92 };
     93 
     94 
     95 /**
     96  * Callback for #TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id().
     97  *
     98  * @param cls a `struct OpenContext *`
     99  * @param rowid row of the request
    100  * @param reserve_payment how much the reserve itself paid
    101  * @param request_timestamp when the request was made
    102  * @param reserve_expiration until when the reserve is now open
    103  * @param purse_limit how many purses were asked for
    104  * @param reserve_pub the reserve in question
    105  * @param reserve_sig signature affirming the request
    106  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
    107  */
    108 static enum GNUNET_GenericReturnValue
    109 open_cb (void *cls,
    110          uint64_t rowid,
    111          const struct TALER_Amount *reserve_payment,
    112          struct GNUNET_TIME_Timestamp request_timestamp,
    113          struct GNUNET_TIME_Timestamp reserve_expiration,
    114          uint32_t purse_limit,
    115          const struct TALER_ReservePublicKeyP *reserve_pub,
    116          const struct TALER_ReserveSignatureP *reserve_sig)
    117 {
    118   struct OpenContext *ctx = cls;
    119 
    120   (void) rowid;
    121   ctx->total++;
    122   if ( (NULL != ctx->reserve_pub) &&
    123        (0 == GNUNET_memcmp (reserve_pub,
    124                             ctx->reserve_pub)) )
    125   {
    126     ctx->matched++;
    127     ctx->reserve_payment = *reserve_payment;
    128     ctx->purse_limit = purse_limit;
    129     ctx->reserve_sig = *reserve_sig;
    130     ctx->expiration = reserve_expiration;
    131     ctx->request_timestamp = request_timestamp;
    132   }
    133   if ( (0 != ctx->stop_after) &&
    134        (ctx->total >= ctx->stop_after) )
    135     return GNUNET_SYSERR;
    136   return GNUNET_OK;
    137 }
    138 
    139 
    140 /**
    141  * Ask to keep a reserve open.
    142  *
    143  * @param pg the database context
    144  * @param reserve_pub reserve to open
    145  * @param seed seed for the reserve signature
    146  * @param total_paid total the client is paying, e.g. "1"
    147  * @param reserve_payment share of that taken from the reserve, e.g. "1"
    148  * @param purse_limit how many purses the client wants
    149  * @param desired_expiration until when the client wants the reserve open
    150  * @param now when the client made the request
    151  * @param open_fee annual fee, e.g. "1"
    152  * @param[out] no_funds set if the reserve cannot pay its share
    153  * @param[out] open_cost set to what the operation costs
    154  * @param[out] final_expiration set to the expiration that was granted
    155  * @return transaction status
    156  */
    157 static enum GNUNET_DB_QueryStatus
    158 open_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg,
    159               const struct TALER_ReservePublicKeyP *reserve_pub,
    160               uint32_t seed,
    161               const char *total_paid,
    162               const char *reserve_payment,
    163               uint32_t purse_limit,
    164               struct GNUNET_TIME_Timestamp desired_expiration,
    165               struct GNUNET_TIME_Timestamp now,
    166               const char *open_fee,
    167               bool *no_funds,
    168               struct TALER_Amount *open_cost,
    169               struct GNUNET_TIME_Timestamp *final_expiration)
    170 {
    171   struct TALER_ReserveSignatureP reserve_sig;
    172   struct TALER_Amount paid = TDB_amount (total_paid);
    173   struct TALER_Amount payment = TDB_amount (reserve_payment);
    174   struct TALER_Amount fee = TDB_amount (open_fee);
    175   struct TALER_Amount balance;
    176 
    177   TDB_fill (&reserve_sig,
    178             sizeof (reserve_sig),
    179             seed);
    180   return TALER_EXCHANGEDB_do_reserve_open (pg,
    181                                            reserve_pub,
    182                                            &paid,
    183                                            &payment,
    184                                            purse_limit,
    185                                            &reserve_sig,
    186                                            desired_expiration,
    187                                            now,
    188                                            &fee,
    189                                            no_funds,
    190                                            &balance,
    191                                            open_cost,
    192                                            final_expiration);
    193 }
    194 
    195 
    196 /**
    197  * Build a timestamp from a number of seconds since the epoch.
    198  *
    199  * @param secs seconds since the epoch
    200  * @return the timestamp
    201  */
    202 static struct GNUNET_TIME_Timestamp
    203 ts (uint64_t secs)
    204 {
    205   struct GNUNET_TIME_Absolute abs = {
    206     .abs_value_us = secs * 1000LLU * 1000LLU
    207   };
    208 
    209   return GNUNET_TIME_absolute_to_timestamp (abs);
    210 }
    211 
    212 
    213 /**
    214  * A reserve that does not exist cannot be opened, and nothing is on file.
    215  *
    216  * @param pg the database context
    217  * @return 0 on success
    218  */
    219 static int
    220 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
    221 {
    222   struct TALER_ReservePublicKeyP reserve_pub;
    223   struct TALER_Amount open_cost;
    224   struct GNUNET_TIME_Timestamp final_expiration;
    225   struct OpenContext ctx = { 0 };
    226   bool no_funds = false;
    227 
    228   TDB_FILL (reserve_pub,
    229             1);
    230   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    231           open_reserve (pg,
    232                         &reserve_pub,
    233                         1,
    234                         "1",
    235                         "1",
    236                         1,
    237                         ts (2000000000),
    238                         ts (1600000000),
    239                         "1",
    240                         &no_funds,
    241                         &open_cost,
    242                         &final_expiration));
    243   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    244           TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id (
    245             pg,
    246             0,
    247             &open_cb,
    248             &ctx));
    249   FAILIF (0 != ctx.total);
    250   FAILIF (0 != TDB_count (pg,
    251                           "FROM reserves_open_requests"));
    252   return 0;
    253 }
    254 
    255 
    256 /**
    257  * Opening a reserve charges it, extends it and records the request.
    258  *
    259  * @param pg the database context
    260  * @return 0 on success
    261  */
    262 static int
    263 check_open (struct TALER_EXCHANGEDB_PostgresContext *pg)
    264 {
    265   struct TALER_ReservePublicKeyP reserve_pub;
    266   struct TALER_EXCHANGEDB_Reserve reserve;
    267   struct TALER_Amount open_cost;
    268   struct TALER_Amount expect_cost = TDB_amount ("1");
    269   struct TALER_Amount expect_balance = TDB_amount ("9");
    270   struct TALER_ReserveSignatureP expect_sig;
    271   struct GNUNET_TIME_Timestamp final_expiration;
    272   struct GNUNET_TIME_Timestamp now = ts (1600000000);
    273   struct OpenContext ctx;
    274   bool no_funds = true;
    275 
    276   TDB_reserve (pg,
    277                10,
    278                "10",
    279                &reserve_pub);
    280   /* TDB_reserve() gives the reserve an expiration far in the future, which
    281      would leave nothing for this call to do; pull it back to `now'. */
    282   {
    283     char *hex = TDB_hex (&reserve_pub,
    284                          sizeof (reserve_pub));
    285 
    286     FAILIF_C (GNUNET_OK !=
    287               TDB_exec (pg,
    288                         "UPDATE reserves"
    289                         " SET expiration_date=%llu"
    290                         " WHERE reserve_pub=decode('%s','hex');",
    291                         (unsigned long long) now.abs_time.abs_value_us,
    292                         hex),
    293               GNUNET_free (hex));
    294     GNUNET_free (hex);
    295   }
    296 
    297   /* one year of extension at an annual fee of EUR:1 */
    298   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    299           open_reserve (pg,
    300                         &reserve_pub,
    301                         10,
    302                         "1",
    303                         "1",
    304                         1,
    305                         ts (1600000000 + YEAR_US / 1000000),
    306                         now,
    307                         "1",
    308                         &no_funds,
    309                         &open_cost,
    310                         &final_expiration));
    311   FAILIF (no_funds);
    312   FAILIF (0 != TALER_amount_cmp (&open_cost,
    313                                  &expect_cost));
    314   FAILIF (final_expiration.abs_time.abs_value_us !=
    315           now.abs_time.abs_value_us + YEAR_US);
    316 
    317   /* the reserve paid for it */
    318   memset (&reserve,
    319           0,
    320           sizeof (reserve));
    321   reserve.pub = reserve_pub;
    322   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    323           TALER_EXCHANGEDB_get_reserve (pg,
    324                                         &reserve));
    325   FAILIF (0 != TALER_amount_cmp (&reserve.balance,
    326                                  &expect_balance));
    327   FAILIF (GNUNET_TIME_timestamp_cmp (reserve.expiry,
    328                                      !=,
    329                                      final_expiration));
    330 
    331   /* ...and the request itself was recorded */
    332   TDB_FILL (expect_sig,
    333             10);
    334   memset (&ctx,
    335           0,
    336           sizeof (ctx));
    337   ctx.reserve_pub = &reserve_pub;
    338   FAILIF (0 >=
    339           TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id (
    340             pg,
    341             0,
    342             &open_cb,
    343             &ctx));
    344   FAILIF (1 != ctx.matched);
    345   FAILIF (0 != GNUNET_memcmp (&ctx.reserve_sig,
    346                               &expect_sig));
    347   FAILIF (0 != TALER_amount_cmp (&ctx.reserve_payment,
    348                                  &expect_cost));
    349   FAILIF (1 != ctx.purse_limit);
    350   FAILIF (GNUNET_TIME_timestamp_cmp (ctx.expiration,
    351                                      !=,
    352                                      final_expiration));
    353   FAILIF (GNUNET_TIME_timestamp_cmp (ctx.request_timestamp,
    354                                      !=,
    355                                      now));
    356 
    357   /* a retry of the very same request is absorbed */
    358   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    359           open_reserve (pg,
    360                         &reserve_pub,
    361                         10,
    362                         "1",
    363                         "1",
    364                         1,
    365                         ts (1600000000 + YEAR_US / 1000000),
    366                         now,
    367                         "1",
    368                         &no_funds,
    369                         &open_cost,
    370                         &final_expiration));
    371   FAILIF (1 != TDB_count (pg,
    372                           "FROM reserves_open_requests"));
    373   return 0;
    374 }
    375 
    376 
    377 /**
    378  * A request that asks for nothing beyond what the reserve already has
    379  * costs nothing and is not recorded.
    380  *
    381  * @param pg the database context
    382  * @return 0 on success
    383  */
    384 static int
    385 check_nothing_to_do (struct TALER_EXCHANGEDB_PostgresContext *pg)
    386 {
    387   struct TALER_ReservePublicKeyP reserve_pub;
    388   struct TALER_Amount open_cost;
    389   struct TALER_Amount zero = TDB_amount ("0");
    390   struct GNUNET_TIME_Timestamp final_expiration;
    391   bool no_funds = true;
    392   uint64_t before;
    393 
    394   TDB_reserve (pg,
    395                11,
    396                "10",
    397                &reserve_pub);
    398   before = TDB_count (pg,
    399                       "FROM reserves_open_requests");
    400   /* The reserve already runs to 2096 and no additional purses are asked
    401      for, so there is nothing left to grant.  Note that asking for even one
    402      purse would *not* be a no-op: a fresh reserve has purses_allowed=0. */
    403   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    404           open_reserve (pg,
    405                         &reserve_pub,
    406                         11,
    407                         "1",
    408                         "1",
    409                         0,
    410                         ts (1600000000),
    411                         ts (1600000000),
    412                         "1",
    413                         &no_funds,
    414                         &open_cost,
    415                         &final_expiration));
    416   FAILIF (no_funds);
    417   FAILIF (0 != TALER_amount_cmp (&open_cost,
    418                                  &zero));
    419   FAILIF (before != TDB_count (pg,
    420                                "FROM reserves_open_requests"));
    421   return 0;
    422 }
    423 
    424 
    425 /**
    426  * Too little paid, or too little in the reserve, both decline the work.
    427  *
    428  * @param pg the database context
    429  * @return 0 on success
    430  */
    431 static int
    432 check_insufficient (struct TALER_EXCHANGEDB_PostgresContext *pg)
    433 {
    434   struct TALER_ReservePublicKeyP reserve_pub;
    435   struct TALER_Amount open_cost;
    436   struct TALER_Amount expect_cost = TDB_amount ("2");
    437   struct GNUNET_TIME_Timestamp final_expiration;
    438   struct GNUNET_TIME_Timestamp now = ts (1600000000);
    439   bool no_funds = false;
    440   uint64_t before;
    441 
    442   TDB_reserve (pg,
    443                12,
    444                "1",
    445                &reserve_pub);
    446   {
    447     char *hex = TDB_hex (&reserve_pub,
    448                          sizeof (reserve_pub));
    449 
    450     FAILIF_C (GNUNET_OK !=
    451               TDB_exec (pg,
    452                         "UPDATE reserves"
    453                         " SET expiration_date=%llu"
    454                         " WHERE reserve_pub=decode('%s','hex');",
    455                         (unsigned long long) now.abs_time.abs_value_us,
    456                         hex),
    457               GNUNET_free (hex));
    458     GNUNET_free (hex);
    459   }
    460   before = TDB_count (pg,
    461                       "FROM reserves_open_requests");
    462 
    463   /* two years at EUR:1 a year, but only EUR:1 offered */
    464   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    465           open_reserve (pg,
    466                         &reserve_pub,
    467                         12,
    468                         "1",
    469                         "1",
    470                         1,
    471                         ts (1600000000 + 2 * (YEAR_US / 1000000)),
    472                         now,
    473                         "1",
    474                         &no_funds,
    475                         &open_cost,
    476                         &final_expiration));
    477   FAILIF (no_funds);
    478   FAILIF (0 != TALER_amount_cmp (&open_cost,
    479                                  &expect_cost));
    480   FAILIF (before != TDB_count (pg,
    481                                "FROM reserves_open_requests"));
    482 
    483   /* enough offered, but the reserve holds only EUR:1 of the EUR:2 */
    484   no_funds = false;
    485   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    486           open_reserve (pg,
    487                         &reserve_pub,
    488                         12,
    489                         "2",
    490                         "2",
    491                         1,
    492                         ts (1600000000 + 2 * (YEAR_US / 1000000)),
    493                         now,
    494                         "1",
    495                         &no_funds,
    496                         &open_cost,
    497                         &final_expiration));
    498   FAILIF (! no_funds);
    499   FAILIF (before != TDB_count (pg,
    500                                "FROM reserves_open_requests"));
    501   return 0;
    502 }
    503 
    504 
    505 /**
    506  * The iterator's serial bound and its abort return behave as documented.
    507  *
    508  * @param pg the database context
    509  * @return 0 on success
    510  */
    511 static int
    512 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
    513 {
    514   struct TALER_ReservePublicKeyP reserve_pub;
    515   struct TALER_Amount open_cost;
    516   struct GNUNET_TIME_Timestamp final_expiration;
    517   struct GNUNET_TIME_Timestamp now = ts (1600000000);
    518   struct OpenContext ctx;
    519   bool no_funds = true;
    520 
    521   /* a second recorded request, from another reserve */
    522   TDB_reserve (pg,
    523                13,
    524                "10",
    525                &reserve_pub);
    526   {
    527     char *hex = TDB_hex (&reserve_pub,
    528                          sizeof (reserve_pub));
    529 
    530     FAILIF_C (GNUNET_OK !=
    531               TDB_exec (pg,
    532                         "UPDATE reserves"
    533                         " SET expiration_date=%llu"
    534                         " WHERE reserve_pub=decode('%s','hex');",
    535                         (unsigned long long) now.abs_time.abs_value_us,
    536                         hex),
    537               GNUNET_free (hex));
    538     GNUNET_free (hex);
    539   }
    540   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    541           open_reserve (pg,
    542                         &reserve_pub,
    543                         13,
    544                         "1",
    545                         "1",
    546                         1,
    547                         ts (1600000000 + YEAR_US / 1000000),
    548                         now,
    549                         "1",
    550                         &no_funds,
    551                         &open_cost,
    552                         &final_expiration));
    553   FAILIF (2 != TDB_count (pg,
    554                           "FROM reserves_open_requests"));
    555 
    556   memset (&ctx,
    557           0,
    558           sizeof (ctx));
    559   FAILIF (2 !=
    560           TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id (
    561             pg,
    562             0,
    563             &open_cb,
    564             &ctx));
    565   memset (&ctx,
    566           0,
    567           sizeof (ctx));
    568   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    569           TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id (
    570             pg,
    571             1000,
    572             &open_cb,
    573             &ctx));
    574   FAILIF (0 != ctx.total);
    575 
    576   memset (&ctx,
    577           0,
    578           sizeof (ctx));
    579   ctx.stop_after = 1;
    580   FAILIF (2 !=
    581           TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id (
    582             pg,
    583             0,
    584             &open_cb,
    585             &ctx));
    586   FAILIF (1 != ctx.total);
    587   return 0;
    588 }
    589 
    590 
    591 /**
    592  * The checks to run, in order.
    593  */
    594 static const struct TDB_Test tests[] = {
    595   { "reserves-open-requests-empty",
    596     &check_empty },
    597   { "reserves-open-requests-open",
    598     &check_open },
    599   { "reserves-open-requests-nothing-to-do",
    600     &check_nothing_to_do },
    601   { "reserves-open-requests-insufficient",
    602     &check_insufficient },
    603   { "reserves-open-requests-iterate",
    604     &check_iterate },
    605   { NULL, NULL }
    606 };
    607 
    608 
    609 int
    610 main (int argc,
    611       char *const *argv)
    612 {
    613   return TDB_main (argc,
    614                    argv,
    615                    "test-reserves-open-requests",
    616                    "Tests for the exchangedb `reserves_open_requests' table",
    617                    tests);
    618 }
    619 
    620 
    621 /* end of test_reserves_open_requests.c */