exchange

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

test_known_coins.c (28480B)


      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_known_coins.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `known_coins`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_do_insert_known_coins(),
     23  * #TALER_EXCHANGEDB_get_known_coin(),
     24  * #TALER_EXCHANGEDB_get_coin_denomination(),
     25  * #TALER_EXCHANGEDB_get_signature_for_known_coin() and
     26  * #TALER_EXCHANGEDB_get_count_known_coins().
     27  *
     28  * `known_coins` references `denominations`, so a denomination is created
     29  * first with TDB_denom().  do_insert_known_coins() is the interesting one:
     30  * it is idempotent, but only for a coin that comes back with the *same*
     31  * denomination and age commitment -- the conflicting cases are what its
     32  * negative per-coin status codes are for.  It takes a whole batch of
     33  * coins in one round trip, so the last check mixes all cases in one call
     34  * and verifies that every coin is reported at its own position.
     35  */
     36 #include "test_common.h"
     37 #include "exchange-database/do_insert_known_coins.h"
     38 #include "exchange-database/get_known_coin.h"
     39 #include "exchange-database/get_coin_denomination.h"
     40 #include "exchange-database/get_signature_for_known_coin.h"
     41 #include "exchange-database/get_count_known_coins.h"
     42 #include "exchange-database/start.h"
     43 #include "exchange-database/rollback.h"
     44 
     45 
     46 /**
     47  * Make a single @a coin known via the batch function.
     48  *
     49  * @param pg the database context
     50  * @param coin the coin to make known
     51  * @param[out] res outcome for the coin
     52  * @return database status, #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT on success
     53  */
     54 static enum GNUNET_DB_QueryStatus
     55 insert_one (struct TALER_EXCHANGEDB_PostgresContext *pg,
     56             const struct TALER_CoinPublicInfo *coin,
     57             struct TALER_EXCHANGEDB_CoinKnownResult *res)
     58 {
     59   const struct TALER_CoinPublicInfo *coins[1] = { coin };
     60 
     61   return TALER_EXCHANGEDB_do_insert_known_coins (pg,
     62                                                  1,
     63                                                  coins,
     64                                                  res);
     65 }
     66 
     67 
     68 /**
     69  * Nothing is known about a coin that was never inserted.
     70  *
     71  * @param pg the database context
     72  * @return 0 on success
     73  */
     74 static int
     75 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
     76 {
     77   struct TALER_CoinSpendPublicKeyP coin_pub;
     78   struct TALER_CoinPublicInfo info;
     79   struct TALER_DenominationHashP h_denom_pub;
     80   struct TALER_DenominationPublicKey denom_pub = { 0 };
     81   struct TALER_DenominationSignature denom_sig = { 0 };
     82   uint64_t known_coin_id;
     83 
     84   TDB_FILL (coin_pub,
     85             1);
     86   TDB_FILL (h_denom_pub,
     87             1);
     88   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     89           TALER_EXCHANGEDB_get_known_coin (pg,
     90                                            &coin_pub,
     91                                            &info));
     92   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     93           TALER_EXCHANGEDB_get_coin_denomination (pg,
     94                                                   &coin_pub,
     95                                                   &known_coin_id,
     96                                                   &h_denom_pub));
     97   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
     98           TALER_EXCHANGEDB_get_signature_for_known_coin (pg,
     99                                                          &coin_pub,
    100                                                          &denom_pub,
    101                                                          &denom_sig));
    102   FAILIF (NULL != denom_pub.bsign_pub_key);
    103   FAILIF (NULL != denom_sig.unblinded_sig);
    104   /* a denomination that does not exist has no coins */
    105   FAILIF (0 != TALER_EXCHANGEDB_get_count_known_coins (pg,
    106                                                        &h_denom_pub));
    107   return 0;
    108 }
    109 
    110 
    111 /**
    112  * A coin of an unknown denomination cannot be made known.
    113  *
    114  * @param pg the database context
    115  * @return 0 on success
    116  */
    117 static int
    118 check_unknown_denomination (struct TALER_EXCHANGEDB_PostgresContext *pg)
    119 {
    120   struct TALER_CoinPublicInfo coin;
    121   struct TALER_EXCHANGEDB_CoinKnownResult res;
    122 
    123   memset (&coin,
    124           0,
    125           sizeof (coin));
    126   TDB_FILL (coin.coin_pub,
    127             2);
    128   TDB_FILL (coin.denom_pub_hash,
    129             2);
    130   coin.no_age_commitment = true;
    131   TDB_denom_sig (2,
    132                  &coin.denom_sig);
    133   /* no denomination row, so nothing is inserted and no coin is found
    134      either: the row for the coin has no known_coin_id, a hard error */
    135   FAILIF_C (GNUNET_DB_STATUS_HARD_ERROR !=
    136             insert_one (pg,
    137                         &coin,
    138                         &res),
    139             TALER_denom_sig_free (&coin.denom_sig));
    140   TALER_denom_sig_free (&coin.denom_sig);
    141   FAILIF (0 != TDB_count (pg,
    142                           "FROM known_coins"));
    143   return 0;
    144 }
    145 
    146 
    147 /**
    148  * Making a coin known stores it, and every lookup finds it.
    149  *
    150  * @param pg the database context
    151  * @return 0 on success
    152  */
    153 static int
    154 check_insert_and_lookup (struct TALER_EXCHANGEDB_PostgresContext *pg)
    155 {
    156   struct TDB_Denom denom;
    157   struct TALER_CoinPublicInfo coin;
    158   struct TALER_CoinPublicInfo got;
    159   struct TALER_DenominationHashP h_denom_pub;
    160   struct TALER_DenominationPublicKey denom_pub = { 0 };
    161   struct TALER_DenominationSignature denom_sig = { 0 };
    162   uint64_t known_coin_id = 0;
    163   uint64_t id2 = 0;
    164 
    165   TDB_denom (pg,
    166              10,
    167              "5",
    168              "0.1",
    169              &denom);
    170   TDB_coin (pg,
    171             &denom,
    172             20,
    173             &coin,
    174             &known_coin_id);
    175   FAILIF_C (0 == known_coin_id,
    176             TDB_coin_free (&coin); TDB_denom_free (&denom));
    177 
    178   memset (&got,
    179           0,
    180           sizeof (got));
    181   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    182             TALER_EXCHANGEDB_get_known_coin (pg,
    183                                              &coin.coin_pub,
    184                                              &got),
    185             TDB_coin_free (&coin); TDB_denom_free (&denom));
    186   FAILIF_C (0 != GNUNET_memcmp (&got.denom_pub_hash,
    187                                 &denom.h_denom_pub),
    188             TALER_denom_sig_free (&got.denom_sig);
    189             TDB_coin_free (&coin); TDB_denom_free (&denom));
    190   FAILIF_C (! got.no_age_commitment,
    191             TALER_denom_sig_free (&got.denom_sig);
    192             TDB_coin_free (&coin); TDB_denom_free (&denom));
    193   FAILIF_C (0 != TALER_denom_sig_cmp (&got.denom_sig,
    194                                       &coin.denom_sig),
    195             TALER_denom_sig_free (&got.denom_sig);
    196             TDB_coin_free (&coin); TDB_denom_free (&denom));
    197   TALER_denom_sig_free (&got.denom_sig);
    198 
    199   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    200             TALER_EXCHANGEDB_get_coin_denomination (pg,
    201                                                     &coin.coin_pub,
    202                                                     &id2,
    203                                                     &h_denom_pub),
    204             TDB_coin_free (&coin); TDB_denom_free (&denom));
    205   FAILIF_C (id2 != known_coin_id,
    206             TDB_coin_free (&coin); TDB_denom_free (&denom));
    207   FAILIF_C (0 != GNUNET_memcmp (&h_denom_pub,
    208                                 &denom.h_denom_pub),
    209             TDB_coin_free (&coin); TDB_denom_free (&denom));
    210 
    211   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    212             TALER_EXCHANGEDB_get_signature_for_known_coin (pg,
    213                                                            &coin.coin_pub,
    214                                                            &denom_pub,
    215                                                            &denom_sig),
    216             TDB_coin_free (&coin); TDB_denom_free (&denom));
    217   FAILIF_C (0 != TALER_denom_pub_cmp (&denom_pub,
    218                                       &denom.pub),
    219             TALER_denom_pub_free (&denom_pub);
    220             TALER_denom_sig_free (&denom_sig);
    221             TDB_coin_free (&coin); TDB_denom_free (&denom));
    222   FAILIF_C (0 != TALER_denom_sig_cmp (&denom_sig,
    223                                       &coin.denom_sig),
    224             TALER_denom_pub_free (&denom_pub);
    225             TALER_denom_sig_free (&denom_sig);
    226             TDB_coin_free (&coin); TDB_denom_free (&denom));
    227   TALER_denom_pub_free (&denom_pub);
    228   TALER_denom_sig_free (&denom_sig);
    229 
    230   FAILIF_C (1 != TALER_EXCHANGEDB_get_count_known_coins (pg,
    231                                                          &denom.h_denom_pub),
    232             TDB_coin_free (&coin); TDB_denom_free (&denom));
    233   TDB_coin_free (&coin);
    234   TDB_denom_free (&denom);
    235   return 0;
    236 }
    237 
    238 
    239 /**
    240  * Re-inserting the same coin is a no-op; re-inserting it under a different
    241  * denomination or with an age commitment is a conflict.
    242  *
    243  * @param pg the database context
    244  * @return 0 on success
    245  */
    246 static int
    247 check_conflicts (struct TALER_EXCHANGEDB_PostgresContext *pg)
    248 {
    249   struct TDB_Denom denom;
    250   struct TDB_Denom other;
    251   struct TALER_CoinPublicInfo coin;
    252   struct TALER_EXCHANGEDB_CoinKnownResult res;
    253 
    254   TDB_denom (pg,
    255              10,
    256              "5",
    257              "0.1",
    258              &denom);
    259   TDB_denom (pg,
    260              11,
    261              "5",
    262              "0.1",
    263              &other);
    264   memset (&coin,
    265           0,
    266           sizeof (coin));
    267   TDB_FILL (coin.coin_pub,
    268             20);
    269   coin.denom_pub_hash = denom.h_denom_pub;
    270   coin.no_age_commitment = true;
    271   TDB_denom_sig (20,
    272                  &coin.denom_sig);
    273 
    274   /* the coin from the previous check: already present, no conflict */
    275   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    276             insert_one (pg,
    277                         &coin,
    278                         &res),
    279             TALER_denom_sig_free (&coin.denom_sig);
    280             TDB_denom_free (&denom); TDB_denom_free (&other));
    281   FAILIF_C (TALER_EXCHANGEDB_CKS_PRESENT != res.status,
    282             TALER_denom_sig_free (&coin.denom_sig);
    283             TDB_denom_free (&denom); TDB_denom_free (&other));
    284   FAILIF_C (0 == res.known_coin_id,
    285             TALER_denom_sig_free (&coin.denom_sig);
    286             TDB_denom_free (&denom); TDB_denom_free (&other));
    287   FAILIF_C (! res.no_age_commitment,
    288             TALER_denom_sig_free (&coin.denom_sig);
    289             TDB_denom_free (&denom); TDB_denom_free (&other));
    290 
    291   /* same coin key, different denomination: conflict, and the stored
    292      denomination is handed back so the caller can report it */
    293   coin.denom_pub_hash = other.h_denom_pub;
    294   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    295             insert_one (pg,
    296                         &coin,
    297                         &res),
    298             TALER_denom_sig_free (&coin.denom_sig);
    299             TDB_denom_free (&denom); TDB_denom_free (&other));
    300   FAILIF_C (TALER_EXCHANGEDB_CKS_DENOM_CONFLICT != res.status,
    301             TALER_denom_sig_free (&coin.denom_sig);
    302             TDB_denom_free (&denom); TDB_denom_free (&other));
    303   FAILIF_C (0 != GNUNET_memcmp (&res.h_denom_pub,
    304                                 &denom.h_denom_pub),
    305             TALER_denom_sig_free (&coin.denom_sig);
    306             TDB_denom_free (&denom); TDB_denom_free (&other));
    307 
    308   /* same coin key and denomination, but now with an age commitment where
    309      the stored row has none: the caller should have passed none */
    310   coin.denom_pub_hash = denom.h_denom_pub;
    311   coin.no_age_commitment = false;
    312   TDB_FILL (coin.h_age_commitment,
    313             21);
    314   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    315             insert_one (pg,
    316                         &coin,
    317                         &res),
    318             TALER_denom_sig_free (&coin.denom_sig);
    319             TDB_denom_free (&denom); TDB_denom_free (&other));
    320   FAILIF_C (TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NULL != res.status,
    321             TALER_denom_sig_free (&coin.denom_sig);
    322             TDB_denom_free (&denom); TDB_denom_free (&other));
    323   TALER_denom_sig_free (&coin.denom_sig);
    324 
    325   /* the conflicting attempts did not add anything */
    326   FAILIF_C (1 != TALER_EXCHANGEDB_get_count_known_coins (pg,
    327                                                          &denom.h_denom_pub),
    328             TDB_denom_free (&denom); TDB_denom_free (&other));
    329   FAILIF_C (0 != TALER_EXCHANGEDB_get_count_known_coins (pg,
    330                                                          &other.h_denom_pub),
    331             TDB_denom_free (&denom); TDB_denom_free (&other));
    332   TDB_denom_free (&denom);
    333   TDB_denom_free (&other);
    334   return 0;
    335 }
    336 
    337 
    338 /**
    339  * A coin with an age commitment round-trips, and the mirror-image
    340  * conflict is reported.
    341  *
    342  * @param pg the database context
    343  * @return 0 on success
    344  */
    345 static int
    346 check_age_commitment (struct TALER_EXCHANGEDB_PostgresContext *pg)
    347 {
    348   struct TDB_Denom denom;
    349   struct TALER_CoinPublicInfo coin;
    350   struct TALER_CoinPublicInfo got;
    351   struct TALER_EXCHANGEDB_CoinKnownResult res;
    352 
    353   TDB_denom (pg,
    354              10,
    355              "5",
    356              "0.1",
    357              &denom);
    358   memset (&coin,
    359           0,
    360           sizeof (coin));
    361   TDB_FILL (coin.coin_pub,
    362             30);
    363   coin.denom_pub_hash = denom.h_denom_pub;
    364   coin.no_age_commitment = false;
    365   TDB_FILL (coin.h_age_commitment,
    366             31);
    367   TDB_denom_sig (30,
    368                  &coin.denom_sig);
    369   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    370             insert_one (pg,
    371                         &coin,
    372                         &res),
    373             TALER_denom_sig_free (&coin.denom_sig);
    374             TDB_denom_free (&denom));
    375   FAILIF_C (TALER_EXCHANGEDB_CKS_ADDED != res.status,
    376             TALER_denom_sig_free (&coin.denom_sig);
    377             TDB_denom_free (&denom));
    378   FAILIF_C (0 == res.known_coin_id,
    379             TALER_denom_sig_free (&coin.denom_sig);
    380             TDB_denom_free (&denom));
    381   memset (&got,
    382           0,
    383           sizeof (got));
    384   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    385             TALER_EXCHANGEDB_get_known_coin (pg,
    386                                              &coin.coin_pub,
    387                                              &got),
    388             TALER_denom_sig_free (&coin.denom_sig);
    389             TDB_denom_free (&denom));
    390   FAILIF_C (got.no_age_commitment,
    391             TALER_denom_sig_free (&got.denom_sig);
    392             TALER_denom_sig_free (&coin.denom_sig);
    393             TDB_denom_free (&denom));
    394   FAILIF_C (0 != GNUNET_memcmp (&got.h_age_commitment,
    395                                 &coin.h_age_commitment),
    396             TALER_denom_sig_free (&got.denom_sig);
    397             TALER_denom_sig_free (&coin.denom_sig);
    398             TDB_denom_free (&denom));
    399   TALER_denom_sig_free (&got.denom_sig);
    400 
    401   /* a different age commitment for the same coin is a conflict... */
    402   TDB_FILL (coin.h_age_commitment,
    403             32);
    404   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    405             insert_one (pg,
    406                         &coin,
    407                         &res),
    408             TALER_denom_sig_free (&coin.denom_sig);
    409             TDB_denom_free (&denom));
    410   FAILIF_C (TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS != res.status,
    411             TALER_denom_sig_free (&coin.denom_sig);
    412             TDB_denom_free (&denom));
    413   /* the stored age commitment is handed back for the error report */
    414   FAILIF_C (res.no_age_commitment,
    415             TALER_denom_sig_free (&coin.denom_sig);
    416             TDB_denom_free (&denom));
    417   FAILIF_C (0 != GNUNET_memcmp (&res.h_age_commitment,
    418                                 &got.h_age_commitment),
    419             TALER_denom_sig_free (&coin.denom_sig);
    420             TDB_denom_free (&denom));
    421   /* ...and so is no age commitment at all, where one is on file */
    422   coin.no_age_commitment = true;
    423   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    424             insert_one (pg,
    425                         &coin,
    426                         &res),
    427             TALER_denom_sig_free (&coin.denom_sig);
    428             TDB_denom_free (&denom));
    429   FAILIF_C (TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NON_NULL != res.status,
    430             TALER_denom_sig_free (&coin.denom_sig);
    431             TDB_denom_free (&denom));
    432   TALER_denom_sig_free (&coin.denom_sig);
    433 
    434   /* two coins of this denomination by now */
    435   FAILIF_C (2 != TALER_EXCHANGEDB_get_count_known_coins (pg,
    436                                                          &denom.h_denom_pub),
    437             TDB_denom_free (&denom));
    438   TDB_denom_free (&denom);
    439   return 0;
    440 }
    441 
    442 
    443 /**
    444  * One call with a mixed batch: two new coins (one with an age
    445  * commitment), one already known, one with a conflicting denomination
    446  * and one with a conflicting age commitment.  Every coin must be
    447  * reported at its own position, the new ones must be inserted, and a
    448  * batch with a repeated coin or an unknown denomination is refused
    449  * without inserting anything.
    450  *
    451  * @param pg the database context
    452  * @return 0 on success
    453  */
    454 static int
    455 check_batch (struct TALER_EXCHANGEDB_PostgresContext *pg)
    456 {
    457   struct TDB_Denom denom;
    458   struct TDB_Denom other;
    459   struct TALER_CoinPublicInfo coins[5];
    460   const struct TALER_CoinPublicInfo *pcoins[5];
    461   struct TALER_EXCHANGEDB_CoinKnownResult res[5];
    462   struct TALER_DenominationHashP dh;
    463   uint64_t id;
    464   uint64_t count_before;
    465   int ret = 1;
    466 
    467   TDB_denom (pg,
    468              10,
    469              "5",
    470              "0.1",
    471              &denom);
    472   TDB_denom (pg,
    473              11,
    474              "5",
    475              "0.1",
    476              &other);
    477   memset (coins,
    478           0,
    479           sizeof (coins));
    480   for (unsigned int i = 0; i < 5; i++)
    481   {
    482     coins[i].no_age_commitment = true;
    483     coins[i].denom_pub_hash = denom.h_denom_pub;
    484     pcoins[i] = &coins[i];
    485   }
    486   /* [0]: new, no age commitment */
    487   TDB_FILL (coins[0].coin_pub,
    488             40);
    489   TDB_denom_sig (40,
    490                  &coins[0].denom_sig);
    491   /* [1]: new, with age commitment */
    492   TDB_FILL (coins[1].coin_pub,
    493             41);
    494   coins[1].no_age_commitment = false;
    495   TDB_FILL (coins[1].h_age_commitment,
    496             410);
    497   TDB_denom_sig (41,
    498                  &coins[1].denom_sig);
    499   /* [2]: known from check_insert_and_lookup (seed 20), same data */
    500   TDB_FILL (coins[2].coin_pub,
    501             20);
    502   TDB_denom_sig (20,
    503                  &coins[2].denom_sig);
    504   /* [3]: known (seed 20 again, different key below), other denomination */
    505   TDB_FILL (coins[3].coin_pub,
    506             20);
    507   coins[3].denom_pub_hash = other.h_denom_pub;
    508   TDB_denom_sig (20,
    509                  &coins[3].denom_sig);
    510   /* [4]: known from check_age_commitment (seed 30), wrong age commitment */
    511   TDB_FILL (coins[4].coin_pub,
    512             30);
    513   coins[4].no_age_commitment = false;
    514   TDB_FILL (coins[4].h_age_commitment,
    515             33);
    516   TDB_denom_sig (30,
    517                  &coins[4].denom_sig);
    518 
    519   count_before = TDB_count (pg,
    520                             "FROM known_coins");
    521 
    522   /* [2] and [3] share a key: refused, nothing inserted */
    523   FAILIF_C (GNUNET_DB_STATUS_HARD_ERROR !=
    524             TALER_EXCHANGEDB_do_insert_known_coins (pg,
    525                                                     5,
    526                                                     pcoins,
    527                                                     res),
    528             goto cleanup);
    529   FAILIF_C (count_before != TDB_count (pg,
    530                                        "FROM known_coins"),
    531             goto cleanup);
    532 
    533   /* give [3] its own key: a coin known under the other denomination */
    534   {
    535     struct TALER_EXCHANGEDB_CoinKnownResult r1;
    536     struct TALER_CoinPublicInfo tmp = coins[3];
    537 
    538     TDB_FILL (tmp.coin_pub,
    539               43);
    540     FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    541               insert_one (pg,
    542                           &tmp,
    543                           &r1),
    544               goto cleanup);
    545     FAILIF_C (TALER_EXCHANGEDB_CKS_ADDED != r1.status,
    546               goto cleanup);
    547     count_before++;
    548     TDB_FILL (coins[3].coin_pub,
    549               43);
    550     coins[3].denom_pub_hash = denom.h_denom_pub;
    551   }
    552 
    553   /* a new coin of an unknown denomination anywhere in the batch: the
    554      batch is refused.  The INSERT itself is one statement, so this is
    555      done inside a transaction as the callers do it, and the rollback
    556      removes the other new coin again. */
    557   TDB_FILL (coins[0].denom_pub_hash,
    558             44);
    559   FAILIF_C (GNUNET_OK !=
    560             TALER_EXCHANGEDB_start (pg,
    561                                     "test unknown denomination"),
    562             goto cleanup);
    563   FAILIF_C (GNUNET_DB_STATUS_HARD_ERROR !=
    564             TALER_EXCHANGEDB_do_insert_known_coins (pg,
    565                                                     5,
    566                                                     pcoins,
    567                                                     res),
    568             TALER_EXCHANGEDB_rollback (pg); goto cleanup);
    569   TALER_EXCHANGEDB_rollback (pg);
    570   FAILIF_C (count_before != TDB_count (pg,
    571                                        "FROM known_coins"),
    572             goto cleanup);
    573   coins[0].denom_pub_hash = denom.h_denom_pub;
    574 
    575   /* now the mixed batch goes through, with one row per coin */
    576   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    577             TALER_EXCHANGEDB_do_insert_known_coins (pg,
    578                                                     5,
    579                                                     pcoins,
    580                                                     res),
    581             goto cleanup);
    582   FAILIF_C (TALER_EXCHANGEDB_CKS_ADDED != res[0].status,
    583             goto cleanup);
    584   FAILIF_C (0 == res[0].known_coin_id,
    585             goto cleanup);
    586   FAILIF_C (TALER_EXCHANGEDB_CKS_ADDED != res[1].status,
    587             goto cleanup);
    588   FAILIF_C (0 == res[1].known_coin_id,
    589             goto cleanup);
    590   /* for added coins, the result reflects what was stored */
    591   FAILIF_C (! res[0].no_age_commitment,
    592             goto cleanup);
    593   FAILIF_C (0 != GNUNET_memcmp (&res[0].h_denom_pub,
    594                                 &denom.h_denom_pub),
    595             goto cleanup);
    596   FAILIF_C (res[1].no_age_commitment,
    597             goto cleanup);
    598   FAILIF_C (0 != GNUNET_memcmp (&res[1].h_age_commitment,
    599                                 &coins[1].h_age_commitment),
    600             goto cleanup);
    601   FAILIF_C (res[0].known_coin_id == res[1].known_coin_id,
    602             goto cleanup);
    603   FAILIF_C (TALER_EXCHANGEDB_CKS_PRESENT != res[2].status,
    604             goto cleanup);
    605   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    606             TALER_EXCHANGEDB_get_coin_denomination (pg,
    607                                                     &coins[2].coin_pub,
    608                                                     &id,
    609                                                     &dh),
    610             goto cleanup);
    611   FAILIF_C (id != res[2].known_coin_id,
    612             goto cleanup);
    613   FAILIF_C (TALER_EXCHANGEDB_CKS_DENOM_CONFLICT != res[3].status,
    614             goto cleanup);
    615   FAILIF_C (0 != GNUNET_memcmp (&res[3].h_denom_pub,
    616                                 &other.h_denom_pub),
    617             goto cleanup);
    618   FAILIF_C (TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS != res[4].status,
    619             goto cleanup);
    620   FAILIF_C (res[4].no_age_commitment,
    621             goto cleanup);
    622   FAILIF_C (0 == GNUNET_memcmp (&res[4].h_age_commitment,
    623                                 &coins[4].h_age_commitment),
    624             goto cleanup);
    625   /* exactly the two new coins were added */
    626   FAILIF_C (count_before + 2 != TDB_count (pg,
    627                                            "FROM known_coins"),
    628             goto cleanup);
    629   {
    630     struct TALER_CoinPublicInfo got;
    631 
    632     memset (&got,
    633             0,
    634             sizeof (got));
    635     FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    636               TALER_EXCHANGEDB_get_known_coin (pg,
    637                                                &coins[1].coin_pub,
    638                                                &got),
    639               goto cleanup);
    640     FAILIF_C (got.no_age_commitment,
    641               TALER_denom_sig_free (&got.denom_sig); goto cleanup);
    642     FAILIF_C (0 != GNUNET_memcmp (&got.h_age_commitment,
    643                                   &coins[1].h_age_commitment),
    644               TALER_denom_sig_free (&got.denom_sig); goto cleanup);
    645     FAILIF_C (0 != TALER_denom_sig_cmp (&got.denom_sig,
    646                                         &coins[1].denom_sig),
    647               TALER_denom_sig_free (&got.denom_sig); goto cleanup);
    648     TALER_denom_sig_free (&got.denom_sig);
    649   }
    650 
    651   /* running the same batch again changes nothing: the new coins are
    652      now merely present, the conflicts are reported as before */
    653   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    654             TALER_EXCHANGEDB_do_insert_known_coins (pg,
    655                                                     5,
    656                                                     pcoins,
    657                                                     res),
    658             goto cleanup);
    659   FAILIF_C (TALER_EXCHANGEDB_CKS_PRESENT != res[0].status,
    660             goto cleanup);
    661   FAILIF_C (TALER_EXCHANGEDB_CKS_PRESENT != res[1].status,
    662             goto cleanup);
    663   FAILIF_C (TALER_EXCHANGEDB_CKS_PRESENT != res[2].status,
    664             goto cleanup);
    665   FAILIF_C (TALER_EXCHANGEDB_CKS_DENOM_CONFLICT != res[3].status,
    666             goto cleanup);
    667   FAILIF_C (TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS != res[4].status,
    668             goto cleanup);
    669   FAILIF_C (count_before + 2 != TDB_count (pg,
    670                                            "FROM known_coins"),
    671             goto cleanup);
    672   ret = 0;
    673 cleanup:
    674   for (unsigned int i = 0; i < 5; i++)
    675     TALER_denom_sig_free (&coins[i].denom_sig);
    676   TDB_denom_free (&denom);
    677   TDB_denom_free (&other);
    678   return ret;
    679 }
    680 
    681 
    682 /**
    683  * Both ways out of do_insert_known_coins() release the closures its
    684  * query parameters allocate.  Nothing to assert here: the leak shows up
    685  * when the test runs under valgrind (meson test --setup=valgrind).
    686  *
    687  * @param pg the database context
    688  * @return 0 on success
    689  */
    690 static int
    691 check_no_leak (struct TALER_EXCHANGEDB_PostgresContext *pg)
    692 {
    693   struct TDB_Denom denom;
    694   struct TALER_CoinPublicInfo coins[2];
    695   const struct TALER_CoinPublicInfo *pcoins[2];
    696   const struct TALER_CoinPublicInfo *dups[2];
    697   struct TALER_EXCHANGEDB_CoinKnownResult res[2];
    698   int ret = 1;
    699 
    700   TDB_denom (pg,
    701              10,
    702              "5",
    703              "0.1",
    704              &denom);
    705   memset (coins,
    706           0,
    707           sizeof (coins));
    708   for (unsigned int i = 0; i < 2; i++)
    709   {
    710     coins[i].no_age_commitment = true;
    711     coins[i].denom_pub_hash = denom.h_denom_pub;
    712     TDB_FILL (coins[i].coin_pub,
    713               50 + i);
    714     TDB_denom_sig (50 + i,
    715                    &coins[i].denom_sig);
    716     pcoins[i] = &coins[i];
    717     dups[i] = &coins[0];
    718   }
    719   /* the batch goes through: first inserted, then found */
    720   for (unsigned int i = 0; i < 2; i++)
    721   {
    722     FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    723               TALER_EXCHANGEDB_do_insert_known_coins (pg,
    724                                                       2,
    725                                                       pcoins,
    726                                                       res),
    727               goto cleanup);
    728     FAILIF_C ( (i == 0)
    729                ? (TALER_EXCHANGEDB_CKS_ADDED != res[1].status)
    730                : (TALER_EXCHANGEDB_CKS_PRESENT != res[1].status),
    731                goto cleanup);
    732   }
    733   /* the batch is refused before the query for repeating a coin */
    734   FAILIF_C (GNUNET_DB_STATUS_HARD_ERROR !=
    735             TALER_EXCHANGEDB_do_insert_known_coins (pg,
    736                                                     2,
    737                                                     dups,
    738                                                     res),
    739             goto cleanup);
    740   ret = 0;
    741 cleanup:
    742   for (unsigned int i = 0; i < 2; i++)
    743     TALER_denom_sig_free (&coins[i].denom_sig);
    744   TDB_denom_free (&denom);
    745   return ret;
    746 }
    747 
    748 
    749 /**
    750  * The checks to run, in order.
    751  */
    752 static const struct TDB_Test tests[] = {
    753   { "known-coins-empty",
    754     &check_empty },
    755   { "known-coins-unknown-denomination",
    756     &check_unknown_denomination },
    757   { "known-coins-insert-and-lookup",
    758     &check_insert_and_lookup },
    759   { "known-coins-conflicts",
    760     &check_conflicts },
    761   { "known-coins-age-commitment",
    762     &check_age_commitment },
    763   { "known-coins-batch",
    764     &check_batch },
    765   { "known-coins-no-leak",
    766     &check_no_leak },
    767   { NULL, NULL }
    768 };
    769 
    770 
    771 int
    772 main (int argc,
    773       char *const *argv)
    774 {
    775   return TDB_main (argc,
    776                    argv,
    777                    "test-known-coins",
    778                    "Tests for the exchangedb `known_coins' table",
    779                    tests);
    780 }
    781 
    782 
    783 /* end of test_known_coins.c */