exchange

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

test_regressions.c (33456B)


      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_regressions.c
     18  * @brief regression tests for individual exchangedb operations
     19  *
     20  * Each check in the #tests table pins down one previously broken behaviour.
     21  * The tests share one scratch database, which test_regressions.sh creates and
     22  * removes; every check must therefore use fresh keys rather than assume an
     23  * empty database.
     24  */
     25 #include "exchangedb_lib.h"
     26 #include "taler/taler_json_lib.h"
     27 #include "helper.h"
     28 #include "exchange-database/create_tables.h"
     29 #include "exchange-database/do_aggregate.h"
     30 #include "exchange-database/compute_shard.h"
     31 #include "exchange-database/start.h"
     32 #include "exchange-database/commit.h"
     33 #include "exchange-database/rollback.h"
     34 #include "exchange-database/do_reserve_open.h"
     35 #include "exchange-database/get_purse_deposit.h"
     36 #include "exchange-database/get_reserve_close_info.h"
     37 #include "exchange-database/begin_shard.h"
     38 #include "exchange-database/abort_shard.h"
     39 #include "exchange-database/update_shard_progress.h"
     40 #include "exchange-database/do_import_credits.h"
     41 
     42 
     43 /**
     44  * Currency we use, must match test_regressions.sh.
     45  */
     46 #define CURRENCY "EUR"
     47 
     48 /**
     49  * Report a failed expectation and return 1 from the calling check.
     50  */
     51 #define FAILIF(cond)                            \
     52         do {                                          \
     53           if (! (cond)) break;                        \
     54           GNUNET_break (0);                           \
     55           fprintf (stderr,                            \
     56                    "FAILED: %s at %s:%u\n",           \
     57                    # cond, __FILE__, __LINE__);       \
     58           return 1;                                   \
     59         } while (0)
     60 
     61 
     62 /**
     63  * Our database context.
     64  */
     65 static struct TALER_EXCHANGEDB_PostgresContext *pg;
     66 
     67 /**
     68  * Name of the single check to run, NULL to run all of them.
     69  */
     70 static char *only;
     71 
     72 /**
     73  * Return value of the process.
     74  */
     75 static int result;
     76 
     77 
     78 /**
     79  * Run @a sql on our database connection, outside of any transaction.
     80  *
     81  * @param sql statement(s) to run
     82  * @return #GNUNET_OK on success
     83  */
     84 static enum GNUNET_GenericReturnValue
     85 exec_sql (const char *sql)
     86 {
     87   struct GNUNET_PQ_ExecuteStatement es[] = {
     88     GNUNET_PQ_make_execute (sql),
     89     GNUNET_PQ_EXECUTE_STATEMENT_END
     90   };
     91 
     92   return GNUNET_PQ_exec_statements (pg->conn,
     93                                     es);
     94 }
     95 
     96 
     97 /**
     98  * Convert @a val to an amount in our currency.
     99  *
    100  * @param str amount without the currency prefix, e.g. "10.5"
    101  * @param[out] amount set to the parsed amount
    102  */
    103 static void
    104 parse_amount (const char *str,
    105               struct TALER_Amount *amount)
    106 {
    107   char *s;
    108 
    109   GNUNET_asprintf (&s,
    110                    CURRENCY ":%s",
    111                    str);
    112   GNUNET_assert (GNUNET_OK ==
    113                  TALER_string_to_amount (s,
    114                                          amount));
    115   GNUNET_free (s);
    116 }
    117 
    118 
    119 /**
    120  * Build an INSERT that creates a reserve with a zero balance and no
    121  * `reserves_in` row, the way exchange_do_purse_merge() does.
    122  *
    123  * @param reserve_pub public key of the reserve to create
    124  * @return SQL statement, to be freed by the caller
    125  */
    126 static char *
    127 hex_insert_reserve (const struct TALER_ReservePublicKeyP *reserve_pub)
    128 {
    129   char hex[sizeof (*reserve_pub) * 2 + 1];
    130   const unsigned char *raw = (const unsigned char *) reserve_pub;
    131   char *sql;
    132 
    133   for (unsigned int i = 0; i<sizeof (*reserve_pub); i++)
    134     GNUNET_snprintf (&hex[i * 2],
    135                      3,
    136                      "%02x",
    137                      raw[i]);
    138   GNUNET_asprintf (&sql,
    139                    "INSERT INTO reserves"
    140                    " (reserve_pub,current_balance,expiration_date,gc_date)"
    141                    " VALUES"
    142                    " (decode('%s','hex')"
    143                    " ,ROW(0,0)::taler_amount"
    144                    " ,1770000000000000"
    145                    " ,1780000000000000);",
    146                    hex);
    147   return sql;
    148 }
    149 
    150 
    151 /**
    152  * Render @a data as a lowercase hex string for use in an SQL literal.
    153  *
    154  * @param data binary data
    155  * @param size number of bytes in @a data
    156  * @return hex string, to be freed by the caller
    157  */
    158 static char *
    159 to_hex (const void *data,
    160         size_t size)
    161 {
    162   const unsigned char *raw = data;
    163   char *hex;
    164 
    165   hex = GNUNET_malloc (size * 2 + 1);
    166   for (size_t i = 0; i<size; i++)
    167     GNUNET_snprintf (&hex[i * 2],
    168                      3,
    169                      "%02x",
    170                      raw[i]);
    171   return hex;
    172 }
    173 
    174 
    175 /**
    176  * E-2: a merchant may refund more than (deposit - deposit fee).  The
    177  * aggregator used to charge the full deposit fee on top of such a refund,
    178  * making the amount to be wired out negative, and then abort() on the
    179  * GNUNET_assert() guarding the subtraction -- stopping every payout for the
    180  * shard, and doing so again on every restart.
    181  *
    182  * Deposit EUR:1.00 of a denomination with a EUR:0.10 deposit fee, refund
    183  * EUR:0.95 of it, and require the aggregation to come out at EUR:0.
    184  */
    185 static int
    186 check_aggregate_refund_below_deposit_fee (void)
    187 {
    188   struct TALER_MerchantPublicKeyP merchant_pub;
    189   struct TALER_FullPaytoHashP h_payto;
    190   struct TALER_NormalizedPaytoHashP h_norm;
    191   struct TALER_CoinSpendPublicKeyP coin_pub;
    192   struct TALER_WireTransferIdentifierRawP wtid;
    193   struct TALER_Amount total;
    194   struct TALER_Amount zero;
    195   char *sql;
    196   char *m_hex;
    197   char *p_hex;
    198   char *n_hex;
    199   char *c_hex;
    200   enum GNUNET_GenericReturnValue ok;
    201 
    202   parse_amount ("0",
    203                 &zero);
    204   memset (&merchant_pub,
    205           0x41,
    206           sizeof (merchant_pub));
    207   memset (&h_payto,
    208           0x42,
    209           sizeof (h_payto));
    210   memset (&h_norm,
    211           0x43,
    212           sizeof (h_norm));
    213   memset (&coin_pub,
    214           0x44,
    215           sizeof (coin_pub));
    216   memset (&wtid,
    217           0x45,
    218           sizeof (wtid));
    219   m_hex = to_hex (&merchant_pub,
    220                   sizeof (merchant_pub));
    221   p_hex = to_hex (&h_payto,
    222                   sizeof (h_payto));
    223   n_hex = to_hex (&h_norm,
    224                   sizeof (h_norm));
    225   c_hex = to_hex (&coin_pub,
    226                   sizeof (coin_pub));
    227   GNUNET_asprintf (
    228     &sql,
    229     "INSERT INTO kyc_targets (h_normalized_payto)"
    230     " VALUES (decode('%s','hex')) ON CONFLICT DO NOTHING;"
    231     "INSERT INTO wire_targets"
    232     " (wire_target_h_payto,payto_uri,h_normalized_payto)"
    233     " VALUES (decode('%s','hex'),'payto://x-taler-bank/h/e2',"
    234     "         decode('%s','hex')) ON CONFLICT DO NOTHING;"
    235     "INSERT INTO denominations"
    236     " (denom_pub_hash,denom_type,age_mask,denom_pub,master_sig"
    237     " ,valid_from,expire_withdraw,expire_deposit,expire_legal"
    238     " ,coin,fee_withdraw,fee_deposit,fee_refresh,fee_refund)"
    239     " VALUES (decode(repeat('e2',64),'hex'),1,0,decode('00','hex')"
    240     "        ,decode(repeat('00',64),'hex'),0,0,0,0"
    241     "        ,ROW(1,0)::taler_amount,ROW(0,0)::taler_amount"
    242     "        ,ROW(0,10000000)::taler_amount,ROW(0,0)::taler_amount"
    243     "        ,ROW(0,0)::taler_amount);"
    244     "INSERT INTO known_coins"
    245     " (denominations_serial,coin_pub,denom_sig,remaining)"
    246     " VALUES ((SELECT denominations_serial FROM denominations"
    247     "           WHERE denom_pub_hash=decode(repeat('e2',64),'hex'))"
    248     "        ,decode('%s','hex'),decode('00','hex'),ROW(0,0)::taler_amount);"
    249     "INSERT INTO batch_deposits"
    250     " (shard,merchant_pub,wallet_timestamp,exchange_timestamp"
    251     " ,refund_deadline,wire_deadline,h_contract_terms,wire_salt"
    252     " ,wire_target_h_payto,policy_blocked,total_amount,merchant_sig"
    253     " ,done,total_without_fee)"
    254     " VALUES (%llu,decode('%s','hex'),0,0,1,1"
    255     "        ,decode(repeat('e2',64),'hex'),decode(repeat('e2',16),'hex')"
    256     "        ,decode('%s','hex'),FALSE,ROW(1,0)::taler_amount"
    257     "        ,decode(repeat('00',64),'hex'),FALSE"
    258     "        ,ROW(0,90000000)::taler_amount);"
    259     "INSERT INTO coin_deposits"
    260     " (batch_deposit_serial_id,coin_pub,coin_sig,amount_with_fee)"
    261     " VALUES ((SELECT batch_deposit_serial_id FROM batch_deposits"
    262     "           WHERE merchant_pub=decode('%s','hex'))"
    263     "        ,decode('%s','hex'),decode(repeat('e2',64),'hex')"
    264     "        ,ROW(1,0)::taler_amount);"
    265     "INSERT INTO refunds"
    266     " (coin_pub,batch_deposit_serial_id,merchant_sig,rtransaction_id"
    267     " ,amount_with_fee)"
    268     " VALUES (decode('%s','hex')"
    269     "        ,(SELECT batch_deposit_serial_id FROM batch_deposits"
    270     "           WHERE merchant_pub=decode('%s','hex'))"
    271     "        ,decode(repeat('00',64),'hex'),1"
    272     "        ,ROW(0,95000000)::taler_amount);",
    273     n_hex,
    274     p_hex,
    275     n_hex,
    276     c_hex,
    277     (unsigned long long) TALER_EXCHANGEDB_compute_shard (&merchant_pub),
    278     m_hex,
    279     p_hex,
    280     m_hex,
    281     c_hex,
    282     c_hex,
    283     m_hex);
    284   ok = exec_sql (sql);
    285   GNUNET_free (sql);
    286   GNUNET_free (m_hex);
    287   GNUNET_free (p_hex);
    288   GNUNET_free (n_hex);
    289   GNUNET_free (c_hex);
    290   FAILIF (GNUNET_OK != ok);
    291 
    292   /* Before the fix this abort()ed inside TALER_EXCHANGEDB_do_aggregate(). */
    293   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    294           TALER_EXCHANGEDB_do_aggregate (pg,
    295                                          &h_payto,
    296                                          &merchant_pub,
    297                                          &wtid,
    298                                          &total));
    299   FAILIF (0 !=
    300           TALER_amount_cmp (&zero,
    301                             &total));
    302   return 0;
    303 }
    304 
    305 
    306 /**
    307  * E-13: PostgreSQL accepts COMMIT on a transaction it has already aborted,
    308  * rolls it back and answers with the command tag ROLLBACK and no error at
    309  * all.  TALER_EXCHANGEDB_commit() used to pass that straight through as
    310  * GNUNET_DB_STATUS_SUCCESS_NO_RESULTS, i.e. every caller was told the
    311  * transaction had committed while its writes were gone.
    312  */
    313 static int
    314 check_commit_detects_rolled_back_transaction (void)
    315 {
    316   /* A transaction that is aborted mid-way must NOT commit successfully. */
    317   FAILIF (GNUNET_OK !=
    318           TALER_EXCHANGEDB_start (pg,
    319                                   "test-e13-aborted"));
    320   FAILIF (GNUNET_OK !=
    321           exec_sql ("INSERT INTO kyc_targets (h_normalized_payto)"
    322                     " VALUES (decode(repeat('13',32),'hex'));"));
    323   /* Provoke an error; from here on the transaction is doomed. */
    324   FAILIF (GNUNET_OK ==
    325           exec_sql ("SELECT 1/0;"));
    326   /* Before the fix this returned SUCCESS_NO_RESULTS (0). */
    327   FAILIF (0 <=
    328           TALER_EXCHANGEDB_commit (pg));
    329   /* ...and the row is indeed gone, so 'success' would have been a lie. */
    330   FAILIF (GNUNET_OK !=
    331           exec_sql ("DO $$ BEGIN"
    332                     " IF EXISTS (SELECT FROM kyc_targets"
    333                     "             WHERE h_normalized_payto"
    334                     "                   =decode(repeat('13',32),'hex'))"
    335                     " THEN RAISE EXCEPTION 'row survived a rollback';"
    336                     " END IF; END $$;"));
    337 
    338   /* A clean transaction must still commit and still return exactly 0. */
    339   FAILIF (GNUNET_OK !=
    340           TALER_EXCHANGEDB_start (pg,
    341                                   "test-e13-clean"));
    342   FAILIF (GNUNET_OK !=
    343           exec_sql ("INSERT INTO kyc_targets (h_normalized_payto)"
    344                     " VALUES (decode(repeat('14',32),'hex'));"));
    345   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    346           TALER_EXCHANGEDB_commit (pg));
    347   FAILIF (GNUNET_OK !=
    348           exec_sql ("DO $$ BEGIN"
    349                     " IF NOT EXISTS (SELECT FROM kyc_targets"
    350                     "                 WHERE h_normalized_payto"
    351                     "                       =decode(repeat('14',32),'hex'))"
    352                     " THEN RAISE EXCEPTION 'committed row is missing';"
    353                     " END IF; END $$;"));
    354   return 0;
    355 }
    356 
    357 
    358 /**
    359  * Helper for the reserve-open checks: create a reserve with the given
    360  * balance and run exchange_do_reserve_open() on it.
    361  *
    362  * @param reserve_pub reserve to create and open
    363  * @param desired_expiration expiration the client asks for
    364  * @param now current time to assume
    365  * @param min_purse_limit number of purses the client asks for
    366  * @param open_fee annual account fee of the exchange
    367  * @param[out] open_cost set to the cost the exchange computed
    368  * @return transaction status
    369  */
    370 static enum GNUNET_DB_QueryStatus
    371 try_reserve_open (const struct TALER_ReservePublicKeyP *reserve_pub,
    372                   struct GNUNET_TIME_Timestamp desired_expiration,
    373                   struct GNUNET_TIME_Timestamp now,
    374                   uint32_t min_purse_limit,
    375                   const struct TALER_Amount *open_fee,
    376                   struct TALER_Amount *open_cost)
    377 {
    378   struct TALER_ReserveSignatureP reserve_sig;
    379   struct TALER_Amount zero;
    380   struct TALER_Amount balance;
    381   struct GNUNET_TIME_Timestamp final_expiration;
    382   bool no_funds;
    383   char *sql;
    384 
    385   memset (&reserve_sig,
    386           0x51,
    387           sizeof (reserve_sig));
    388   parse_amount ("0",
    389                 &zero);
    390   sql = hex_insert_reserve (reserve_pub);
    391   GNUNET_assert (GNUNET_OK ==
    392                  exec_sql (sql));
    393   GNUNET_free (sql);
    394   return TALER_EXCHANGEDB_do_reserve_open (pg,
    395                                            reserve_pub,
    396                                            &zero,
    397                                            &zero,
    398                                            min_purse_limit,
    399                                            &reserve_sig,
    400                                            desired_expiration,
    401                                            now,
    402                                            open_fee,
    403                                            &no_funds,
    404                                            &balance,
    405                                            open_cost,
    406                                            &final_expiration);
    407 }
    408 
    409 
    410 /**
    411  * E-3: `{"reserve_expiration":"never"}` is a perfectly well-formed request:
    412  * GNUNET_JSON_spec_timestamp() turns it into GNUNET_TIME_UNIT_FOREVER_ABS and
    413  * qconv_abs_time() clamps that to INT64_MAX.  The stored procedure then
    414  * overflowed INT8 computing the number of years, which SQLSTATE 22003 turns
    415  * into a hard error and the handler into an HTTP 500.
    416  */
    417 static int
    418 check_reserve_open_never_expires (void)
    419 {
    420   struct TALER_ReservePublicKeyP reserve_pub;
    421   struct TALER_Amount open_fee;
    422   struct TALER_Amount open_cost;
    423 
    424   memset (&reserve_pub,
    425           0x33,
    426           sizeof (reserve_pub));
    427   /* No fractional part here; that is E-4's business. */
    428   parse_amount ("1",
    429                 &open_fee);
    430   /* Before the fix: 'bigint out of range' -> HARD_ERROR. */
    431   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    432           try_reserve_open (&reserve_pub,
    433                             GNUNET_TIME_UNIT_FOREVER_TS,
    434                             GNUNET_TIME_timestamp_get (),
    435                             1,
    436                             &open_fee,
    437                             &open_cost));
    438   return 0;
    439 }
    440 
    441 
    442 /**
    443  * E-4: `my_years * in_open_fee.frac` was an INT4 multiplication that
    444  * overflowed *before* the division, so the author's own overflow guard could
    445  * never be reached: an exchange whose ACCOUNT_FEE has a fractional part
    446  * answered a far-future reserve-open request with an HTTP 500.
    447  *
    448  * The same block computed the new purse limit in INT4 from the client's
    449  * `purse_limit`, which overflows for any value near INT32_MAX.
    450  */
    451 static int
    452 check_reserve_open_int4_overflows (void)
    453 {
    454   struct TALER_ReservePublicKeyP reserve_pub;
    455   struct TALER_Amount open_fee;
    456   struct TALER_Amount open_cost;
    457   struct TALER_Amount expected;
    458   struct GNUNET_TIME_Timestamp now;
    459 
    460   parse_amount ("0.5",
    461                 &open_fee);
    462   parse_amount ("25",
    463                 &expected);
    464 
    465   /* 50 years at EUR:0.50/year: 50 * 50000000 does not fit into an INT4. */
    466   memset (&reserve_pub,
    467           0x34,
    468           sizeof (reserve_pub));
    469   now = GNUNET_TIME_timestamp_get ();
    470   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    471           try_reserve_open (&reserve_pub,
    472                             GNUNET_TIME_absolute_to_timestamp (
    473                               GNUNET_TIME_absolute_add (
    474                                 now.abs_time,
    475                                 GNUNET_TIME_relative_multiply (
    476                                   GNUNET_TIME_UNIT_YEARS,
    477                                   50))),
    478                             now,
    479                             1,
    480                             &open_fee,
    481                             &open_cost));
    482   FAILIF (0 !=
    483           TALER_amount_cmp (&expected,
    484                             &open_cost));
    485 
    486   /* An absurd purse_limit must be priced out of range, not overflow. */
    487   memset (&reserve_pub,
    488           0x35,
    489           sizeof (reserve_pub));
    490   now = GNUNET_TIME_timestamp_get ();
    491   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    492           try_reserve_open (&reserve_pub,
    493                             now,
    494                             now,
    495                             2147483647,
    496                             &open_fee,
    497                             &open_cost));
    498   return 0;
    499 }
    500 
    501 
    502 /**
    503  * E-5: `known_coins.age_commitment_hash` is NULL for every coin without an
    504  * age commitment -- the common case -- but the result spec read it without
    505  * GNUNET_PQ_result_spec_allow_null(), so building the 409 conflict proof for
    506  * POST /purses/$PURSE_PUB/deposit failed with an HTTP 500 instead.
    507  */
    508 static int
    509 check_purse_deposit_without_age_commitment (void)
    510 {
    511   struct TALER_PurseContractPublicKeyP purse_pub;
    512   struct TALER_CoinSpendPublicKeyP coin_pub;
    513   struct TALER_Amount amount;
    514   struct TALER_DenominationHashP h_denom_pub;
    515   struct TALER_AgeCommitmentHashP hac;
    516   struct TALER_CoinSpendSignatureP coin_sig;
    517   bool no_age_commitment;
    518   char *partner_url = NULL;
    519   char *sql;
    520   char *p_hex;
    521   char *c_hex;
    522   enum GNUNET_GenericReturnValue ok;
    523 
    524   memset (&purse_pub,
    525           0x55,
    526           sizeof (purse_pub));
    527   memset (&coin_pub,
    528           0x56,
    529           sizeof (coin_pub));
    530   p_hex = to_hex (&purse_pub,
    531                   sizeof (purse_pub));
    532   c_hex = to_hex (&coin_pub,
    533                   sizeof (coin_pub));
    534   GNUNET_asprintf (
    535     &sql,
    536     "INSERT INTO denominations"
    537     " (denom_pub_hash,denom_type,age_mask,denom_pub,master_sig"
    538     " ,valid_from,expire_withdraw,expire_deposit,expire_legal"
    539     " ,coin,fee_withdraw,fee_deposit,fee_refresh,fee_refund)"
    540     " VALUES (decode(repeat('e5',64),'hex'),1,0,decode('00','hex')"
    541     "        ,decode(repeat('00',64),'hex'),0,0,0,0"
    542     "        ,ROW(1,0)::taler_amount,ROW(0,0)::taler_amount"
    543     "        ,ROW(0,0)::taler_amount,ROW(0,0)::taler_amount"
    544     "        ,ROW(0,0)::taler_amount);"
    545     /* age_commitment_hash deliberately left NULL */
    546     "INSERT INTO known_coins"
    547     " (denominations_serial,coin_pub,denom_sig,remaining)"
    548     " VALUES ((SELECT denominations_serial FROM denominations"
    549     "           WHERE denom_pub_hash=decode(repeat('e5',64),'hex'))"
    550     "        ,decode('%s','hex'),decode('00','hex')"
    551     "        ,ROW(0,0)::taler_amount);"
    552     "INSERT INTO purse_deposits"
    553     " (purse_pub,coin_pub,amount_with_fee,coin_sig)"
    554     " VALUES (decode('%s','hex'),decode('%s','hex')"
    555     "        ,ROW(1,0)::taler_amount,decode(repeat('e5',64),'hex'));",
    556     c_hex,
    557     p_hex,
    558     c_hex);
    559   ok = exec_sql (sql);
    560   GNUNET_free (sql);
    561   GNUNET_free (p_hex);
    562   GNUNET_free (c_hex);
    563   FAILIF (GNUNET_OK != ok);
    564 
    565   /* Before the fix: HARD_ERROR from the failed NULL extraction. */
    566   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    567           TALER_EXCHANGEDB_get_purse_deposit (pg,
    568                                               &purse_pub,
    569                                               &coin_pub,
    570                                               &amount,
    571                                               &h_denom_pub,
    572                                               &hac,
    573                                               &no_age_commitment,
    574                                               &coin_sig,
    575                                               &partner_url));
    576   GNUNET_free (partner_url);
    577   FAILIF (! no_age_commitment);
    578   return 0;
    579 }
    580 
    581 
    582 /**
    583  * E-6: a reserve funded by a purse merge has no `reserves_in` row, so the
    584  * LEFT JOIN in get_reserve_close_info() returns a NULL payto_uri.  Without
    585  * allow_null the extraction failed and POST /reserves/$RP/close answered 500
    586  * instead of the 409 the handler already implements.
    587  */
    588 static int
    589 check_reserve_close_info_without_origin (void)
    590 {
    591   struct TALER_ReservePublicKeyP reserve_pub;
    592   struct TALER_Amount balance;
    593   struct TALER_FullPayto payto_uri;
    594   char *sql;
    595   enum GNUNET_GenericReturnValue ok;
    596 
    597   memset (&reserve_pub,
    598           0x36,
    599           sizeof (reserve_pub));
    600   sql = hex_insert_reserve (&reserve_pub);
    601   ok = exec_sql (sql);
    602   GNUNET_free (sql);
    603   FAILIF (GNUNET_OK != ok);
    604 
    605   /* Before the fix: HARD_ERROR from the failed NULL extraction. */
    606   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    607           TALER_EXCHANGEDB_get_reserve_close_info (pg,
    608                                                    &reserve_pub,
    609                                                    &balance,
    610                                                    &payto_uri));
    611   FAILIF (NULL != payto_uri.full_payto);
    612   return 0;
    613 }
    614 
    615 
    616 /**
    617  * Read back the bookkeeping of one work shard.
    618  *
    619  * @param job_name job the shard belongs to
    620  * @param start_row inclusive start row of the shard
    621  * @param end_row exclusive end row of the shard
    622  * @param[out] progress_row how far the shard has come
    623  * @param[out] completed whether the shard is done
    624  * @return transaction status code
    625  */
    626 static enum GNUNET_DB_QueryStatus
    627 get_shard_state (const char *job_name,
    628                  uint64_t start_row,
    629                  uint64_t end_row,
    630                  uint64_t *progress_row,
    631                  bool *completed)
    632 {
    633   struct GNUNET_PQ_QueryParam params[] = {
    634     GNUNET_PQ_query_param_string (job_name),
    635     GNUNET_PQ_query_param_uint64 (&start_row),
    636     GNUNET_PQ_query_param_uint64 (&end_row),
    637     GNUNET_PQ_query_param_end
    638   };
    639   struct GNUNET_PQ_ResultSpec rs[] = {
    640     GNUNET_PQ_result_spec_uint64 ("progress_row",
    641                                   progress_row),
    642     GNUNET_PQ_result_spec_bool ("completed",
    643                                 completed),
    644     GNUNET_PQ_result_spec_end
    645   };
    646 
    647   PREPARE (pg,
    648            "test_get_shard_state",
    649            "SELECT"
    650            " progress_row"
    651            ",completed"
    652            " FROM work_shards"
    653            " WHERE job_name=$1"
    654            "   AND start_row=$2"
    655            "   AND end_row=$3;");
    656   return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
    657                                                    "test_get_shard_state",
    658                                                    params,
    659                                                    rs);
    660 }
    661 
    662 
    663 /**
    664  * A worker that gets part of the way through a shard and then stops used to
    665  * leave nothing behind: the shard was either untouched or completed, so the
    666  * next worker to pick it up redid all of it, and nothing it had imported was
    667  * visible until the whole shard was done.  Check that the progress marker is
    668  * kept, that it survives releasing the shard, that it never moves backwards,
    669  * and that reaching the end of the shard is what completes it.
    670  */
    671 static int
    672 check_shard_progress_survives_abort (void)
    673 {
    674   const char *job = "test-shard-progress";
    675   uint64_t start;
    676   uint64_t end;
    677   uint64_t progress;
    678   uint64_t start2;
    679   uint64_t end2;
    680   uint64_t progress2;
    681   bool completed;
    682 
    683   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    684           TALER_EXCHANGEDB_begin_shard (pg,
    685                                         job,
    686                                         GNUNET_TIME_UNIT_HOURS,
    687                                         1024,
    688                                         &start,
    689                                         &end,
    690                                         &progress));
    691   /* A fresh shard has nothing done yet. */
    692   FAILIF (progress != start);
    693 
    694   /* Get half way, then let go of the shard. */
    695   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    696           TALER_EXCHANGEDB_update_shard_progress (pg,
    697                                                   job,
    698                                                   start,
    699                                                   end,
    700                                                   start + 512,
    701                                                   GNUNET_TIME_UNIT_HOURS));
    702   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    703           TALER_EXCHANGEDB_abort_shard (pg,
    704                                         job,
    705                                         start,
    706                                         end));
    707 
    708   /* The next worker gets the same shard back, but resumes in the middle. */
    709   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    710           TALER_EXCHANGEDB_begin_shard (pg,
    711                                         job,
    712                                         GNUNET_TIME_UNIT_HOURS,
    713                                         1024,
    714                                         &start2,
    715                                         &end2,
    716                                         &progress2));
    717   FAILIF (start2 != start);
    718   FAILIF (end2 != end);
    719   FAILIF (progress2 != start + 512);
    720 
    721   /* A straggler reporting older progress must not rewind the shard. */
    722   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    723           TALER_EXCHANGEDB_update_shard_progress (pg,
    724                                                   job,
    725                                                   start,
    726                                                   end,
    727                                                   start + 1,
    728                                                   GNUNET_TIME_UNIT_HOURS));
    729   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    730           get_shard_state (job,
    731                            start,
    732                            end,
    733                            &progress,
    734                            &completed));
    735   FAILIF (progress != start + 512);
    736   FAILIF (completed);
    737 
    738   /* Reaching the end completes the shard; there is no second statement for
    739      the caller to forget, or to crash before. */
    740   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    741           TALER_EXCHANGEDB_update_shard_progress (pg,
    742                                                   job,
    743                                                   start,
    744                                                   end,
    745                                                   end,
    746                                                   GNUNET_TIME_UNIT_HOURS));
    747   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    748           get_shard_state (job,
    749                            start,
    750                            end,
    751                            &progress,
    752                            &completed));
    753   FAILIF (progress != end);
    754   FAILIF (! completed);
    755   return 0;
    756 }
    757 
    758 
    759 /**
    760  * Importing a batch of incoming wire transfers and recording how far the
    761  * shard has come is one statement, so a crash cannot land one without the
    762  * other.  Check that a batch moves both, that importing it again is harmless
    763  * and reported as a duplicate, and that the shard completes as part of the
    764  * import rather than in a transaction of its own.
    765  */
    766 static int
    767 check_import_credits_advances_shard (void)
    768 {
    769   const char *job = "test-import-credits";
    770   struct TALER_ReservePublicKeyP reserve_pub;
    771   struct TALER_Amount balance;
    772   struct TALER_EXCHANGEDB_ReserveInInfo reserve;
    773   struct TALER_EXCHANGEDB_CreditBatch batch;
    774   enum GNUNET_DB_QueryStatus results[1];
    775   uint64_t start;
    776   uint64_t end;
    777   uint64_t progress;
    778   bool completed;
    779 
    780   memset (&reserve_pub,
    781           0x51,
    782           sizeof (reserve_pub));
    783   parse_amount ("4.00",
    784                 &balance);
    785   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    786           TALER_EXCHANGEDB_begin_shard (pg,
    787                                         job,
    788                                         GNUNET_TIME_UNIT_HOURS,
    789                                         1024,
    790                                         &start,
    791                                         &end,
    792                                         &progress));
    793   reserve.reserve_pub = &reserve_pub;
    794   reserve.balance = &balance;
    795   reserve.execution_time = GNUNET_TIME_timestamp_get ();
    796   reserve.sender_account_details.full_payto
    797     = (char *) "payto://x-taler-bank/localhost/shard-test?receiver-name=Shard";
    798   reserve.wire_reference = start + 1;
    799   memset (&batch,
    800           0,
    801           sizeof (batch));
    802   batch.exchange_account_name = "exchange-account-test";
    803   batch.reserves = &reserve;
    804   batch.reserves_length = 1;
    805   batch.job_name = job;
    806   batch.shard_start = start;
    807   batch.shard_end = end;
    808   batch.progress_row = start + 1;
    809   batch.lease = GNUNET_TIME_UNIT_HOURS;
    810 
    811   FAILIF (0 >
    812           TALER_EXCHANGEDB_do_import_credits (pg,
    813                                               &batch,
    814                                               results));
    815   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != results[0]);
    816   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    817           get_shard_state (job,
    818                            start,
    819                            end,
    820                            &progress,
    821                            &completed));
    822   /* The transfer is committed and so is the fact that we consumed its row --
    823      long before the rest of the shard has been looked at. */
    824   FAILIF (progress != start + 1);
    825   FAILIF (completed);
    826 
    827   /* Re-importing the same batch is what happens whenever a shard is picked up
    828      twice.  It has to be harmless. */
    829   batch.progress_row = end;
    830   FAILIF (0 >
    831           TALER_EXCHANGEDB_do_import_credits (pg,
    832                                               &batch,
    833                                               results));
    834   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != results[0]);
    835   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    836           get_shard_state (job,
    837                            start,
    838                            end,
    839                            &progress,
    840                            &completed));
    841   FAILIF (progress != end);
    842   FAILIF (! completed);
    843   return 0;
    844 }
    845 
    846 
    847 /**
    848  * All checks we know about.
    849  */
    850 static const struct
    851 {
    852   const char *name;
    853   int (*fn)(void);
    854 } tests[] = {
    855   { "aggregate-refund-below-deposit-fee",
    856     &check_aggregate_refund_below_deposit_fee },
    857   { "commit-detects-rolled-back-transaction",
    858     &check_commit_detects_rolled_back_transaction },
    859   { "reserve-open-never-expires",
    860     &check_reserve_open_never_expires },
    861   { "reserve-open-int4-overflows",
    862     &check_reserve_open_int4_overflows },
    863   { "purse-deposit-without-age-commitment",
    864     &check_purse_deposit_without_age_commitment },
    865   { "reserve-close-info-without-origin",
    866     &check_reserve_close_info_without_origin },
    867   { "shard-progress-survives-abort",
    868     &check_shard_progress_survives_abort },
    869   { "import-credits-advances-shard",
    870     &check_import_credits_advances_shard },
    871   { NULL, NULL }
    872 };
    873 
    874 
    875 /**
    876  * Main function that runs the checks.
    877  *
    878  * @param cls closure
    879  * @param args remaining command-line arguments
    880  * @param cfgfile name of the configuration file used
    881  * @param cfg configuration
    882  */
    883 static void
    884 run (void *cls,
    885      char *const *args,
    886      const char *cfgfile,
    887      const struct GNUNET_CONFIGURATION_Handle *cfg)
    888 {
    889   unsigned int ran = 0;
    890 
    891   (void) cls;
    892   (void) args;
    893   (void) cfgfile;
    894   pg = TALER_EXCHANGEDB_connect_admin (cfg);
    895   if (NULL == pg)
    896   {
    897     fprintf (stderr,
    898              "Failed to connect to the database\n");
    899     result = 77;
    900     return;
    901   }
    902   if (GNUNET_OK !=
    903       TALER_EXCHANGEDB_create_tables (pg,
    904                                       false,
    905                                       0))
    906   {
    907     fprintf (stderr,
    908              "Failed to create the database schema\n");
    909     result = 77;
    910     goto cleanup;
    911   }
    912   for (unsigned int i = 0; NULL != tests[i].name; i++)
    913   {
    914     if ( (NULL != only) &&
    915          (0 != strcmp (only,
    916                        tests[i].name)) )
    917       continue;
    918     fprintf (stderr,
    919              "Running check `%s'\n",
    920              tests[i].name);
    921     ran++;
    922     if (0 != tests[i].fn ())
    923     {
    924       fprintf (stderr,
    925                "Check `%s' FAILED\n",
    926                tests[i].name);
    927       result = 1;
    928     }
    929   }
    930   if (0 == ran)
    931   {
    932     fprintf (stderr,
    933              "No check matched `%s'\n",
    934              only);
    935     result = 1;
    936   }
    937 cleanup:
    938   TALER_EXCHANGEDB_disconnect (pg);
    939   pg = NULL;
    940 }
    941 
    942 
    943 int
    944 main (int argc,
    945       char *const *argv)
    946 {
    947   struct GNUNET_GETOPT_CommandLineOption options[] = {
    948     GNUNET_GETOPT_option_string ('t',
    949                                  "test",
    950                                  "NAME",
    951                                  "only run the check called NAME",
    952                                  &only),
    953     GNUNET_GETOPT_OPTION_END
    954   };
    955   enum GNUNET_GenericReturnValue ret;
    956 
    957   result = 0;
    958   ret = GNUNET_PROGRAM_run (TALER_EXCHANGE_project_data (),
    959                             argc,
    960                             argv,
    961                             "test-regressions",
    962                             "Regression tests for the exchange database layer",
    963                             options,
    964                             &run,
    965                             NULL);
    966   if (GNUNET_SYSERR == ret)
    967     return 3;
    968   if (GNUNET_NO == ret)
    969     return 0;
    970   return result;
    971 }
    972 
    973 
    974 /* end of test_regressions.c */