commit 3d5a97c36b35c4a9d5a854dcbca5842274bcf5bf parent 5b6579b711971cdc04e8435c65e1f041136d7ef5 Author: Özgür Kesim <oec@codeblau.de> Date: Tue, 15 Sep 2026 10:06:21 +0200 exchangedb: make coins known in batches Replace TALER_EXCHANGEDB_do_insert_known_coin() by TALER_EXCHANGEDB_do_insert_known_coins(), which takes all coins of a request as array parameters and runs one statement: a stored procedure UNNESTs the arrays WITH ORDINALITY, inserts the new coins with ON CONFLICT DO NOTHING and returns one row per input coin with the stored denomination and age commitment hashes, so conflicts are reported per coin at its input position. Batches with a repeated coin public key or with an unknown denomination are rejected. TEH_make_coins_known() replaces TEH_make_coin_known() and is used by batch deposit (closes the FIXME for #9373), purse create, purse deposit, reserve open, melt and both recoup handlers. The recoup handlers now make their coins known inside the recoup transaction, so a conflict rolls back the whole batch; their parser rejects requests that disclose the same coin twice. Based on the array approach from dev/oec/vRECOUP. Fixes issue: https://bugs.taler.net/n/9373 Diffstat:
24 files changed, 1217 insertions(+), 291 deletions(-)
diff --git a/meson.build b/meson.build @@ -277,19 +277,19 @@ if not get_option('only-doc') libltversions = [ - ['libtalerutil', '17:0:3'], + ['libtalerutil', '18:0:4'], ['libtalerjson', '10:0:6'], ['libtalercurl', '1:0:1'], - ['libtalerpq', '1:0:0'], + ['libtalerpq', '2:0:1'], ['libtalermhd', '8:0:1'], ['libtalertemplating', '1:1:1'], ['libtalerbank', '6:0:0'], ['libtalerkyclogic', '3:0:0'], - ['libtalerexchangedb', '3:0:0'], + ['libtalerexchangedb', '4:0:0'], ['libtalerauditordb', '1:0:0'], - ['libtalerexchange', '24:0:2'], + ['libtalerexchange', '25:0:0'], ['libtalerauditor', '1:0:1'], - ['libtalertesting', '4:0:0'], + ['libtalertesting', '5:0:0'], ['libtalertwistertesting', '0:1:0'], ] diff --git a/src/auditor/taler-auditor-sync.c b/src/auditor/taler-auditor-sync.c @@ -52,7 +52,6 @@ #include "exchange-database/do_drain_kyc_alert.h" #include "exchange-database/drop_tables.h" #include "exchange-database/enable_rules.h" -#include "exchange-database/do_insert_known_coin.h" #include "exchange-database/event_listen_cancel.h" #include "exchange-database/event_listen.h" #include "exchange-database/do_expire_purse.h" diff --git a/src/benchmark/taler-aggregator-benchmark.c b/src/benchmark/taler-aggregator-benchmark.c @@ -38,7 +38,7 @@ #include "exchange-database/do_deposit.h" #include "exchange-database/insert_wire_fee.h" #include "exchange-database/insert_denomination_info.h" -#include "exchange-database/do_insert_known_coin.h" +#include "exchange-database/do_insert_known_coins.h" /** * Exit code. @@ -288,8 +288,6 @@ add_deposit (const struct Merchant *m) .num_cdis = 1 }; uint64_t known_coin_id; - struct TALER_DenominationHashP dph; - struct TALER_AgeCommitmentHashP agh; RANDOMIZE (&d.coin.coin_pub); d.coin.denom_pub_hash = h_denom_pub; @@ -300,12 +298,21 @@ add_deposit (const struct Merchant *m) 0, sizeof (d.coin.h_age_commitment)); - if (0 >= - TALER_EXCHANGEDB_do_insert_known_coin (pg, - &d.coin, - &known_coin_id, - &dph, - &agh)) + { + const struct TALER_CoinPublicInfo *coins[1] = { &d.coin }; + struct TALER_EXCHANGEDB_CoinKnownResult ckr; + + if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_do_insert_known_coins (pg, + 1, + coins, + &ckr)) || + (TALER_EXCHANGEDB_CKS_ADDED != ckr.status) ) + known_coin_id = 0; + else + known_coin_id = ckr.known_coin_id; + } + if (0 == known_coin_id) { GNUNET_break (0); global_ret = EXIT_FAILURE; diff --git a/src/exchange/taler-exchange-httpd_common_recoup.c b/src/exchange/taler-exchange-httpd_common_recoup.c @@ -149,6 +149,26 @@ TEH_recoup_parse_coin_data ( cs); return GNUNET_SYSERR; } + /* The coins are made known in one batch, which requires + distinct coin public keys. */ + for (size_t i = 0; i < num; i++) + { + if (! cs[i].disclosed) + continue; + for (size_t j = 0; j < i; j++) + { + if ( (cs[j].disclosed) && + (0 == GNUNET_memcmp (&cs[i].coin.coin_pub, + &cs[j].coin.coin_pub)) ) + { + GNUNET_break_op (0); + *hint = "coin_data discloses the same coin twice"; + TEH_recoup_free_coins (num, + cs); + return GNUNET_SYSERR; + } + } + } *num_coins = num; *coins = cs; return GNUNET_OK; diff --git a/src/exchange/taler-exchange-httpd_db.c b/src/exchange/taler-exchange-httpd_db.c @@ -30,93 +30,131 @@ #include "taler-exchange-httpd_responses.h" #include "exchange-database/start.h" #include "exchange-database/commit.h" -#include "exchange-database/do_insert_known_coin.h" +#include "exchange-database/do_insert_known_coins.h" #include "exchange-database/get_signature_for_known_coin.h" #include "exchange-database/preflight.h" #include "exchange-database/rollback.h" enum GNUNET_DB_QueryStatus -TEH_make_coin_known (const struct TALER_CoinPublicInfo *coin, - struct MHD_Connection *connection, - uint64_t *known_coin_id, - enum MHD_Result *mhd_ret) +TEH_make_coins_known ( + unsigned int num_coins, + const struct TALER_CoinPublicInfo *const coins[static num_coins], + struct MHD_Connection *connection, + uint64_t known_coin_ids[static num_coins], + enum MHD_Result *mhd_ret) { - enum TALER_EXCHANGEDB_CoinKnownStatus cks; - struct TALER_DenominationHashP h_denom_pub; - struct TALER_AgeCommitmentHashP h_age_commitment = {{{0}}}; + struct TALER_EXCHANGEDB_CoinKnownResult results[num_coins]; + enum GNUNET_DB_QueryStatus qs; - /* make sure coin is 'known' in database */ - cks = TALER_EXCHANGEDB_do_insert_known_coin (TEH_pg, - coin, - known_coin_id, - &h_denom_pub, - &h_age_commitment); - switch (cks) + /* A request that names the same coin twice is malformed; the + batch insert cannot process it and none of the callers + check for it. */ + for (unsigned int i = 0; i < num_coins; i++) + for (unsigned int j = 0; j < i; j++) + if (0 == GNUNET_memcmp (&coins[i]->coin_pub, + &coins[j]->coin_pub)) + { + GNUNET_break_op (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "coin_pub repeated"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + /* make sure all coins are 'known' in database */ + qs = TALER_EXCHANGEDB_do_insert_known_coins (TEH_pg, + num_coins, + coins, + results); + switch (qs) { - case TALER_EXCHANGEDB_CKS_ADDED: - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; - case TALER_EXCHANGEDB_CKS_PRESENT: - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - case TALER_EXCHANGEDB_CKS_SOFT_FAIL: - return GNUNET_DB_STATUS_SOFT_ERROR; - case TALER_EXCHANGEDB_CKS_HARD_FAIL: + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_known_coins"); + return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + return qs; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); /* should be impossible */ *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, - NULL); + "insert_known_coins"); return GNUNET_DB_STATUS_HARD_ERROR; - case TALER_EXCHANGEDB_CKS_DENOM_CONFLICT: - /* The exchange has a seen this coin before, but with a different denomination. - * Get the corresponding signature and sent it to the client as proof */ + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; /* continued below */ + } + for (unsigned int i = 0; i < num_coins; i++) + { + const struct TALER_CoinPublicInfo *coin = coins[i]; + const struct TALER_EXCHANGEDB_CoinKnownResult *res = &results[i]; + + switch (res->status) { - struct + case TALER_EXCHANGEDB_CKS_ADDED: + case TALER_EXCHANGEDB_CKS_PRESENT: + known_coin_ids[i] = res->known_coin_id; + continue; + case TALER_EXCHANGEDB_CKS_DENOM_CONFLICT: + /* The exchange has seen this coin before, but with a different + * denomination. Get the corresponding signature and send it to + * the client as proof */ { - struct TALER_DenominationPublicKey pub; - struct TALER_DenominationSignature sig; - } prev_denom = {0}; + struct + { + struct TALER_DenominationPublicKey pub; + struct TALER_DenominationSignature sig; + } prev_denom = {0}; - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != - TALER_EXCHANGEDB_get_signature_for_known_coin (TEH_pg, - &coin->coin_pub, - &prev_denom.pub, - &prev_denom.sig)) - { - /* There _should_ have been a result, because - * we ended here due to a conflict! */ - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_get_signature_for_known_coin (TEH_pg, + &coin->coin_pub, + &prev_denom.pub, + &prev_denom.sig)) + { + /* There _should_ have been a result, because + * we ended here due to a conflict! */ + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + *mhd_ret = TEH_RESPONSE_reply_coin_denomination_conflict ( + connection, + TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY, + &coin->coin_pub, + &prev_denom.pub, + &prev_denom.sig); + TALER_denom_pub_free (&prev_denom.pub); + TALER_denom_sig_free (&prev_denom.sig); return GNUNET_DB_STATUS_HARD_ERROR; } - - *mhd_ret = TEH_RESPONSE_reply_coin_denomination_conflict ( + case TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NULL: + case TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NON_NULL: + case TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS: + *mhd_ret = TEH_RESPONSE_reply_coin_age_commitment_conflict ( connection, - TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY, + TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH, + res->status, + &res->h_denom_pub, &coin->coin_pub, - &prev_denom.pub, - &prev_denom.sig); - TALER_denom_pub_free (&prev_denom.pub); - TALER_denom_sig_free (&prev_denom.sig); + &res->h_age_commitment); return GNUNET_DB_STATUS_HARD_ERROR; } - case TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NULL: - case TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NON_NULL: - case TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS: - *mhd_ret = TEH_RESPONSE_reply_coin_age_commitment_conflict ( - connection, - TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH, - cks, - &h_denom_pub, - &coin->coin_pub, - &h_age_commitment); + GNUNET_assert (0); return GNUNET_DB_STATUS_HARD_ERROR; } - GNUNET_assert (0); - return GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; } diff --git a/src/exchange/taler-exchange-httpd_db.h b/src/exchange/taler-exchange-httpd_db.h @@ -41,19 +41,27 @@ /** - * Ensure coin is known in the database, and handle conflicts and errors. + * Ensure the @a coins are known in the database, in one round trip, and + * handle conflicts and errors. Must be called inside of a database + * transaction: on a conflict the reply for the first conflicting coin is + * queued and #GNUNET_DB_STATUS_HARD_ERROR is returned, and the caller's + * transaction is expected to be rolled back so that none of the coins of + * the batch remain inserted. * - * @param coin the coin to make known + * @param num_coins number of entries in @a coins and @a known_coin_ids + * @param coins the coins to make known (must have distinct public keys) * @param connection MHD request context - * @param[out] known_coin_id set to the unique ID for the coin in the DB + * @param[out] known_coin_ids set to the unique ID for each coin in the DB * @param[out] mhd_ret set to MHD status on error * @return transaction status, negative on error (@a mhd_ret will be set in this case) */ enum GNUNET_DB_QueryStatus -TEH_make_coin_known (const struct TALER_CoinPublicInfo *coin, - struct MHD_Connection *connection, - uint64_t *known_coin_id, - enum MHD_Result *mhd_ret); +TEH_make_coins_known ( + unsigned int num_coins, + const struct TALER_CoinPublicInfo *const coins[static num_coins], + struct MHD_Connection *connection, + uint64_t known_coin_ids[static num_coins], + enum MHD_Result *mhd_ret); /** diff --git a/src/exchange/taler-exchange-httpd_post-batch-deposit.c b/src/exchange/taler-exchange-httpd_post-batch-deposit.c @@ -275,20 +275,21 @@ batch_deposit_transaction (void *cls, bool balance_ok; bool in_conflict; - /* FIXME-#9373: replace by batch insert! */ - for (unsigned int i = 0; i<bdc->bd.num_cdis; i++) + if (0 < bdc->bd.num_cdis) { - const struct TALER_EXCHANGEDB_CoinDepositInformation *cdi - = &bdc->cdis[i]; - uint64_t known_coin_id; - - qs = TEH_make_coin_known (&cdi->coin, - connection, - &known_coin_id, - mhd_ret); + const struct TALER_CoinPublicInfo *coins[bdc->bd.num_cdis]; + uint64_t known_coin_ids[bdc->bd.num_cdis]; + + for (unsigned int i = 0; i<bdc->bd.num_cdis; i++) + coins[i] = &bdc->cdis[i].coin; + qs = TEH_make_coins_known (bdc->bd.num_cdis, + coins, + connection, + known_coin_ids, + mhd_ret); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "make coin known (%s) returned %d\n", - TALER_B2S (&cdi->coin.coin_pub), + "make %u coins known returned %d\n", + bdc->bd.num_cdis, qs); if (qs < 0) return qs; diff --git a/src/exchange/taler-exchange-httpd_post-melt.c b/src/exchange/taler-exchange-httpd_post-melt.c @@ -1723,10 +1723,15 @@ phase_run_transaction ( for (unsigned int tries = 0; tries<MAX_TRANSACTION_COMMIT_RETRIES; tries++) { - qs = TEH_make_coin_known (&mc->request.refresh.coin, - mc->rc->connection, - &mc->known_coin_id, - &mhd_ret); + const struct TALER_CoinPublicInfo *coins[1] = { + &mc->request.refresh.coin + }; + + qs = TEH_make_coins_known (1, + coins, + mc->rc->connection, + &mc->known_coin_id, + &mhd_ret); if (GNUNET_DB_STATUS_SOFT_ERROR != qs) break; } diff --git a/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-create.c b/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-create.c @@ -202,7 +202,25 @@ create_transaction (void *cls, &merge_pub)); return GNUNET_DB_STATUS_HARD_ERROR; } - /* 2) deposit all coins */ + /* 2) make all coins known */ + if (0 < pcc->num_coins) + { + const struct TALER_CoinPublicInfo *coins[pcc->num_coins]; + uint64_t known_coin_ids[pcc->num_coins]; + + for (unsigned int i = 0; i<pcc->num_coins; i++) + coins[i] = &pcc->coins[i].cpi; + qs = TEH_make_coins_known (pcc->num_coins, + coins, + connection, + known_coin_ids, + mhd_ret); + if (qs < 0) + return qs; + for (unsigned int i = 0; i<pcc->num_coins; i++) + pcc->coins[i].known_coin_id = known_coin_ids[i]; + } + /* 3) deposit all coins */ for (unsigned int i = 0; i<pcc->num_coins; i++) { struct TEH_PurseDepositedCoin *coin = &pcc->coins[i]; @@ -210,12 +228,6 @@ create_transaction (void *cls, bool conflict = true; bool too_late = true; - qs = TEH_make_coin_known (&coin->cpi, - connection, - &coin->known_coin_id, - mhd_ret); - if (qs < 0) - return qs; qs = TALER_EXCHANGEDB_do_purse_deposit (TEH_pg, &pcc->pd.purse_pub, &coin->cpi.coin_pub, diff --git a/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-deposit.c b/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-deposit.c @@ -168,6 +168,23 @@ deposit_transaction (void *cls, enum GNUNET_DB_QueryStatus qs; qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + if (0 < pcc->num_coins) + { + const struct TALER_CoinPublicInfo *coins[pcc->num_coins]; + uint64_t known_coin_ids[pcc->num_coins]; + + for (unsigned int i = 0; i<pcc->num_coins; i++) + coins[i] = &pcc->coins[i].cpi; + qs = TEH_make_coins_known (pcc->num_coins, + coins, + connection, + known_coin_ids, + mhd_ret); + if (qs < 0) + return qs; + for (unsigned int i = 0; i<pcc->num_coins; i++) + pcc->coins[i].known_coin_id = known_coin_ids[i]; + } for (unsigned int i = 0; i<pcc->num_coins; i++) { struct TEH_PurseDepositedCoin *coin = &pcc->coins[i]; @@ -175,12 +192,6 @@ deposit_transaction (void *cls, bool conflict = true; bool too_late = true; - qs = TEH_make_coin_known (&coin->cpi, - connection, - &coin->known_coin_id, - mhd_ret); - if (qs < 0) - return qs; qs = TALER_EXCHANGEDB_do_purse_deposit (TEH_pg, pcc->purse_pub, &coin->cpi.coin_pub, diff --git a/src/exchange/taler-exchange-httpd_post-recoup-refresh.c b/src/exchange/taler-exchange-httpd_post-recoup-refresh.c @@ -83,7 +83,6 @@ struct RecoupRefreshContext RECOUP_REFRESH_PHASE_LOOKUP_OPERATION, RECOUP_REFRESH_PHASE_CHECK_KEYS, RECOUP_REFRESH_PHASE_VERIFY_COINS, - RECOUP_REFRESH_PHASE_MAKE_COINS_KNOWN, RECOUP_REFRESH_PHASE_RUN_TRANSACTION, RECOUP_REFRESH_PHASE_GENERATE_REPLY_SUCCESS, RECOUP_REFRESH_PHASE_GENERATE_REPLY_ERROR, @@ -469,39 +468,6 @@ phase_verify_coins (struct RecoupRefreshContext *wc) /** - * Make sure all disclosed coins are known in the database. - * - * @param[in,out] wc context of the request - */ -static void -phase_make_coins_known (struct RecoupRefreshContext *wc) -{ - for (size_t i = 0; i < wc->request.num_coins; i++) - { - struct TEH_RecoupCoin *c = &wc->request.coins[i]; - enum MHD_Result mhd_ret = MHD_NO; - enum GNUNET_DB_QueryStatus qs; - - if (! c->disclosed) - continue; - qs = TEH_make_coin_known (&c->coin, - wc->rc->connection, - &c->known_coin_id, - &mhd_ret); - /* no transaction => no serialization failures should be possible */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - if (qs < 0) - { - finish_loop (wc, - mhd_ret); - return; - } - } - wc->phase++; -} - - -/** * Function implementing the recoup transaction: credits the old coin for * every disclosed coin. IF it returns a non-error code, the transaction * logic MUST NOT queue a MHD response. IF it returns a hard error, it @@ -522,8 +488,39 @@ recoup_transaction (void *cls, { struct RecoupRefreshContext *wc = cls; - (void) connection; - (void) mhd_ret; + /* First, make sure all disclosed coins are known. This runs inside + of the transaction: on a conflict, the reply is queued and the + rollback removes every coin of this batch again. */ + { + const struct TALER_CoinPublicInfo *coins[wc->request.num_coins]; + uint64_t known_coin_ids[wc->request.num_coins]; + unsigned int num_disclosed = 0; + enum GNUNET_DB_QueryStatus qs; + + for (size_t i = 0; i < wc->request.num_coins; i++) + { + const struct TEH_RecoupCoin *c = &wc->request.coins[i]; + + if (c->disclosed) + coins[num_disclosed++] = &c->coin; + } + GNUNET_assert (0 < num_disclosed); + qs = TEH_make_coins_known (num_disclosed, + coins, + connection, + known_coin_ids, + mhd_ret); + if (qs < 0) + return qs; + num_disclosed = 0; + for (size_t i = 0; i < wc->request.num_coins; i++) + { + struct TEH_RecoupCoin *c = &wc->request.coins[i]; + + if (c->disclosed) + c->known_coin_id = known_coin_ids[num_disclosed++]; + } + } for (size_t i = 0; i < wc->request.num_coins; i++) { struct TEH_RecoupCoin *c = &wc->request.coins[i]; @@ -911,9 +908,6 @@ TEH_handler_recoup_refresh ( case RECOUP_REFRESH_PHASE_VERIFY_COINS: phase_verify_coins (wc); break; - case RECOUP_REFRESH_PHASE_MAKE_COINS_KNOWN: - phase_make_coins_known (wc); - break; case RECOUP_REFRESH_PHASE_RUN_TRANSACTION: phase_run_transaction (wc); break; diff --git a/src/exchange/taler-exchange-httpd_post-recoup-withdraw.c b/src/exchange/taler-exchange-httpd_post-recoup-withdraw.c @@ -83,7 +83,6 @@ struct RecoupWithdrawContext RECOUP_WITHDRAW_PHASE_LOOKUP_OPERATION, RECOUP_WITHDRAW_PHASE_CHECK_KEYS, RECOUP_WITHDRAW_PHASE_VERIFY_COINS, - RECOUP_WITHDRAW_PHASE_MAKE_COINS_KNOWN, RECOUP_WITHDRAW_PHASE_RUN_TRANSACTION, RECOUP_WITHDRAW_PHASE_GENERATE_REPLY_SUCCESS, RECOUP_WITHDRAW_PHASE_GENERATE_REPLY_ERROR, @@ -472,39 +471,6 @@ phase_verify_coins (struct RecoupWithdrawContext *wc) /** - * Make sure all disclosed coins are known in the database. - * - * @param[in,out] wc context of the request - */ -static void -phase_make_coins_known (struct RecoupWithdrawContext *wc) -{ - for (size_t i = 0; i < wc->request.num_coins; i++) - { - struct TEH_RecoupCoin *c = &wc->request.coins[i]; - enum MHD_Result mhd_ret = MHD_NO; - enum GNUNET_DB_QueryStatus qs; - - if (! c->disclosed) - continue; - qs = TEH_make_coin_known (&c->coin, - wc->rc->connection, - &c->known_coin_id, - &mhd_ret); - /* no transaction => no serialization failures should be possible */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - if (qs < 0) - { - finish_loop (wc, - mhd_ret); - return; - } - } - wc->phase++; -} - - -/** * Function implementing the recoup transaction: credits the reserve for * every disclosed coin. IF it returns a non-error code, the transaction * logic MUST NOT queue a MHD response. IF it returns a hard error, it @@ -525,8 +491,39 @@ recoup_transaction (void *cls, { struct RecoupWithdrawContext *wc = cls; - (void) connection; - (void) mhd_ret; + /* First, make sure all disclosed coins are known. This runs inside + of the transaction: on a conflict, the reply is queued and the + rollback removes every coin of this batch again. */ + { + const struct TALER_CoinPublicInfo *coins[wc->request.num_coins]; + uint64_t known_coin_ids[wc->request.num_coins]; + unsigned int num_disclosed = 0; + enum GNUNET_DB_QueryStatus qs; + + for (size_t i = 0; i < wc->request.num_coins; i++) + { + const struct TEH_RecoupCoin *c = &wc->request.coins[i]; + + if (c->disclosed) + coins[num_disclosed++] = &c->coin; + } + GNUNET_assert (0 < num_disclosed); + qs = TEH_make_coins_known (num_disclosed, + coins, + connection, + known_coin_ids, + mhd_ret); + if (qs < 0) + return qs; + num_disclosed = 0; + for (size_t i = 0; i < wc->request.num_coins; i++) + { + struct TEH_RecoupCoin *c = &wc->request.coins[i]; + + if (c->disclosed) + c->known_coin_id = known_coin_ids[num_disclosed++]; + } + } for (size_t i = 0; i < wc->request.num_coins; i++) { struct TEH_RecoupCoin *c = &wc->request.coins[i]; @@ -913,9 +910,6 @@ TEH_handler_recoup_withdraw ( case RECOUP_WITHDRAW_PHASE_VERIFY_COINS: phase_verify_coins (wc); break; - case RECOUP_WITHDRAW_PHASE_MAKE_COINS_KNOWN: - phase_make_coins_known (wc); - break; case RECOUP_WITHDRAW_PHASE_RUN_TRANSACTION: phase_run_transaction (wc); break; diff --git a/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-open.c b/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-open.c @@ -192,20 +192,31 @@ reserve_open_transaction (void *cls, enum GNUNET_DB_QueryStatus qs; struct TALER_Amount reserve_balance; - for (unsigned int i = 0; i<rsc->payments_len; i++) + if (0 < rsc->payments_len) { - struct TEH_PurseDepositedCoin *coin = &rsc->payments[i]; - bool insufficient_funds = true; + const struct TALER_CoinPublicInfo *coins[rsc->payments_len]; + uint64_t known_coin_ids[rsc->payments_len]; GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Make coin %u known\n", - i); - qs = TEH_make_coin_known (&coin->cpi, - connection, - &coin->known_coin_id, - mhd_ret); + "Make %u coins known\n", + rsc->payments_len); + for (unsigned int i = 0; i<rsc->payments_len; i++) + coins[i] = &rsc->payments[i].cpi; + qs = TEH_make_coins_known (rsc->payments_len, + coins, + connection, + known_coin_ids, + mhd_ret); if (qs < 0) return qs; + for (unsigned int i = 0; i<rsc->payments_len; i++) + rsc->payments[i].known_coin_id = known_coin_ids[i]; + } + for (unsigned int i = 0; i<rsc->payments_len; i++) + { + struct TEH_PurseDepositedCoin *coin = &rsc->payments[i]; + bool insufficient_funds = true; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Insert open deposit %u known\n", i); diff --git a/src/exchange/taler-exchange-httpd_responses.h b/src/exchange/taler-exchange-httpd_responses.h @@ -32,7 +32,7 @@ #include "taler-exchange-httpd.h" #include "taler-exchange-httpd_db.h" #include "exchangedb_lib.h" -#include "exchange-database/do_insert_known_coin.h" +#include "exchange-database/do_insert_known_coins.h" /** diff --git a/src/exchangedb/do_insert_known_coins.c b/src/exchangedb/do_insert_known_coins.c @@ -0,0 +1,276 @@ +/* + This file is part of TALER + Copyright (C) 2022-2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file exchangedb/do_insert_known_coins.c + * @brief Implementation of the do_insert_known_coins function for Postgres + * @author Christian Grothoff + * @author Özgür Kesim + */ +#include "platform.h" +#include "taler/taler_error_codes.h" +#include "taler/taler_dbevents.h" +#include "exchangedb_lib.h" +#include "taler/taler_pq_lib.h" +#include "exchange-database/do_insert_known_coins.h" +#include "helper.h" + + +/** + * Closure for #known_coins_cb(). + */ +struct KnownCoinsContext +{ + /** + * The coins we asked about. + */ + const struct TALER_CoinPublicInfo *const *coins; + + /** + * Where to put the per-coin outcome. + */ + struct TALER_EXCHANGEDB_CoinKnownResult *results; + + /** + * Number of entries in @e coins and @e results. + */ + unsigned int num_coins; + + /** + * Number of rows we have processed so far. + */ + unsigned int num_rows; + + /** + * Set to true if a row could not be processed. + */ + bool failed; +}; + + +/** + * Function called with one row per coin. Fills the + * result entry at the coin's input position. + * + * @param cls a `struct KnownCoinsContext *` + * @param result the result + * @param num_results number of rows in @a result + */ +static void +known_coins_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct KnownCoinsContext *kcc = cls; + + for (unsigned int i = 0; i < num_results; i++) + { + uint64_t idx; + bool existed; + uint64_t known_coin_id; + bool no_known_coin_id; + struct TALER_DenominationHashP h_denom_pub; + bool no_denom_pub_hash; + struct TALER_AgeCommitmentHashP h_age_commitment; + bool no_age_commitment_hash; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("out_idx", + &idx), + GNUNET_PQ_result_spec_bool ("out_existed", + &existed), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_uint64 ("out_known_coin_id", + &known_coin_id), + &no_known_coin_id), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_auto_from_type ("out_denom_pub_hash", + &h_denom_pub), + &no_denom_pub_hash), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_auto_from_type ("out_age_commitment_hash", + &h_age_commitment), + &no_age_commitment_hash), + GNUNET_PQ_result_spec_end + }; + const struct TALER_CoinPublicInfo *coin; + struct TALER_EXCHANGEDB_CoinKnownResult *res; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + kcc->failed = true; + return; + } + /* out_idx is the 1-based ordinality of the input arrays */ + if ( (0 == idx) || + (idx > kcc->num_coins) ) + { + GNUNET_break (0); + kcc->failed = true; + return; + } + coin = kcc->coins[idx - 1]; + res = &kcc->results[idx - 1]; + if (no_known_coin_id) + { + /* neither inserted nor found: the denomination is unknown */ + GNUNET_break (0); + kcc->failed = true; + return; + } + res->known_coin_id = known_coin_id; + kcc->num_rows++; + if (! existed) + { + /* The row was inserted by this very statement, which the + query cannot see yet: what is stored is what we passed. */ + res->status = TALER_EXCHANGEDB_CKS_ADDED; + res->h_denom_pub = coin->denom_pub_hash; + res->no_age_commitment = coin->no_age_commitment; + if (! coin->no_age_commitment) + res->h_age_commitment = coin->h_age_commitment; + continue; + } + res->no_age_commitment = no_age_commitment_hash; + if (! no_denom_pub_hash) + res->h_denom_pub = h_denom_pub; + if (! no_age_commitment_hash) + res->h_age_commitment = h_age_commitment; + if ( (! no_denom_pub_hash) && + (0 != GNUNET_memcmp (&h_denom_pub, + &coin->denom_pub_hash)) ) + { + GNUNET_break_op (0); + res->status = TALER_EXCHANGEDB_CKS_DENOM_CONFLICT; + continue; + } + if (no_age_commitment_hash != coin->no_age_commitment) + { + GNUNET_break_op (0); + res->status = no_age_commitment_hash + ? TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NULL + : TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NON_NULL; + continue; + } + if ( (! no_age_commitment_hash) && + (0 != GNUNET_memcmp (&h_age_commitment, + &coin->h_age_commitment)) ) + { + GNUNET_break_op (0); + res->status = TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS; + continue; + } + res->status = TALER_EXCHANGEDB_CKS_PRESENT; + } +} + + +enum GNUNET_DB_QueryStatus +TALER_EXCHANGEDB_do_insert_known_coins ( + struct TALER_EXCHANGEDB_PostgresContext *pg, + unsigned int num_coins, + const struct TALER_CoinPublicInfo *const coins[static num_coins], + struct TALER_EXCHANGEDB_CoinKnownResult results[static num_coins]) +{ + struct TALER_CoinSpendPublicKeyP coin_pubs[num_coins]; + struct TALER_DenominationHashP denom_pub_hashes[num_coins]; + bool has_age_commitments[num_coins]; + struct TALER_AgeCommitmentHashP age_commitment_hashes[num_coins]; + struct TALER_DenominationSignature denom_sigs[num_coins]; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_array_auto_from_type (num_coins, + coin_pubs, + pg->conn), + GNUNET_PQ_query_param_array_auto_from_type (num_coins, + denom_pub_hashes, + pg->conn), + GNUNET_PQ_query_param_array_bool (num_coins, + has_age_commitments, + pg->conn), + GNUNET_PQ_query_param_array_auto_from_type (num_coins, + age_commitment_hashes, + pg->conn), + TALER_PQ_query_param_array_denom_sig (num_coins, + denom_sigs, + pg->conn), + GNUNET_PQ_query_param_end + }; + struct KnownCoinsContext kcc = { + .coins = coins, + .results = results, + .num_coins = num_coins + }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_assert (0 < num_coins); + /* Gather the columns; the arrays cannot carry NULL elements, so coins + without age commitment get a zero hash and a false flag. */ + for (unsigned int i = 0; i < num_coins; i++) + { + const struct TALER_CoinPublicInfo *coin = coins[i]; + + coin_pubs[i] = coin->coin_pub; + denom_pub_hashes[i] = coin->denom_pub_hash; + has_age_commitments[i] = ! coin->no_age_commitment; + if (coin->no_age_commitment) + memset (&age_commitment_hashes[i], + 0, + sizeof (age_commitment_hashes[i])); + else + age_commitment_hashes[i] = coin->h_age_commitment; + denom_sigs[i] = coin->denom_sig; + /* A single INSERT cannot handle the same key twice. */ + for (unsigned int j = 0; j < i; j++) + { + if (0 == GNUNET_memcmp (&coin_pubs[i], + &coin_pubs[j])) + { + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + } + } + } + memset (results, + 0, + num_coins * sizeof (*results)); + PREPARE (pg, + "do_insert_known_coins", + "SELECT" + " out_idx" + ",out_existed" + ",out_known_coin_id" + ",out_denom_pub_hash" + ",out_age_commitment_hash" + " FROM exchange_do_insert_known_coins" + " ($1, $2, $3, $4, $5);"); + qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, + "do_insert_known_coins", + params, + &known_coins_cb, + &kcc); + if (0 > qs) + return qs; + if ( (kcc.failed) || + (kcc.num_rows != num_coins) ) + { + /* an unknown denomination or a malformed reply */ + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + } + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} diff --git a/src/exchangedb/do_insert_known_coins.sql b/src/exchangedb/do_insert_known_coins.sql @@ -0,0 +1,110 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2014--2026 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +DROP FUNCTION IF EXISTS exchange_do_insert_known_coins; + +-- Make a batch of coins known. The five input arrays are parallel; entry +-- i of each describes coin i. Age commitment hashes are only used where +-- in_has_age_commitments is TRUE (the arrays cannot carry NULL elements). +-- +-- One row is returned per input coin, ordered by the input position: +-- out_idx 1-based position of the coin in the input +-- out_existed FALSE if the coin was inserted by this call +-- out_known_coin_id row of the coin, NULL if the coin was neither +-- inserted (unknown denomination) nor found +-- out_denom_pub_hash denomination stored for an existing coin, NULL +-- for a freshly inserted one +-- out_age_commitment_hash age commitment stored for an existing coin +-- (NULL if none, or if freshly inserted) +-- +-- The coin public keys in the input must be distinct. +CREATE FUNCTION exchange_do_insert_known_coins( + IN in_coin_pubs BYTEA[], + IN in_denom_pub_hashes BYTEA[], + IN in_has_age_commitments BOOLEAN[], + IN in_age_commitment_hashes BYTEA[], + IN in_denom_sigs BYTEA[], + OUT out_idx INT8, + OUT out_existed BOOLEAN, + OUT out_known_coin_id INT8, + OUT out_denom_pub_hash BYTEA, + OUT out_age_commitment_hash BYTEA) +RETURNS SETOF RECORD +LANGUAGE plpgsql +AS $$ +BEGIN + RETURN QUERY + WITH input_rows AS ( + SELECT + t.coin_pub + ,t.denom_pub_hash + ,CASE WHEN t.has_age_commitment + THEN t.age_commitment_hash + ELSE NULL + END AS age_commitment_hash + ,t.denom_sig + ,t.idx + FROM UNNEST (in_coin_pubs + ,in_denom_pub_hashes + ,in_has_age_commitments + ,in_age_commitment_hashes + ,in_denom_sigs) + WITH ORDINALITY + AS t (coin_pub + ,denom_pub_hash + ,has_age_commitment + ,age_commitment_hash + ,denom_sig + ,idx) + ), ins AS ( + INSERT INTO known_coins + (coin_pub + ,denominations_serial + ,age_commitment_hash + ,denom_sig + ,remaining) + SELECT + ir.coin_pub + ,d.denominations_serial + ,ir.age_commitment_hash + ,ir.denom_sig + ,d.coin + FROM input_rows ir + JOIN denominations d + ON (d.denom_pub_hash = ir.denom_pub_hash) + ON CONFLICT (coin_pub) DO NOTHING + RETURNING + known_coins.coin_pub + ,known_coins.known_coin_id + ) + SELECT + ir.idx + ,(ins.known_coin_id IS NULL) AS existed + ,COALESCE (ins.known_coin_id, kc.known_coin_id) + ,kd.denom_pub_hash + ,kc.age_commitment_hash + FROM input_rows ir + LEFT JOIN ins + ON (ins.coin_pub = ir.coin_pub) + LEFT JOIN known_coins kc + ON (kc.coin_pub = ir.coin_pub) + LEFT JOIN denominations kd + ON (kd.denominations_serial = kc.denominations_serial) + ORDER BY ir.idx ASC; +END $$; + +COMMENT ON FUNCTION exchange_do_insert_known_coins + IS 'Inserts the coins of a batch into known_coins where they are new and returns, for every coin, whether it was known before together with the stored denomination and age commitment hashes'; diff --git a/src/exchangedb/meson.build b/src/exchangedb/meson.build @@ -82,7 +82,7 @@ libtalerexchangedb = library( 'do_drain_kyc_alert.c', 'drop_tables.c', 'enable_rules.c', - 'do_insert_known_coin.c', + 'do_insert_known_coins.c', 'event_listen.c', 'event_listen_cancel.c', 'event_notify.c', diff --git a/src/exchangedb/sql-schema/meson.build b/src/exchangedb/sql-schema/meson.build @@ -12,6 +12,7 @@ procedures_sql = [ '../do_withdraw.sql', '../do_refresh.sql', '../do_deposit.sql', + '../do_insert_known_coins.sql', '../do_check_deposit_idempotent.sql', # dead!? 'exchange_do_melt.sql', diff --git a/src/exchangedb/test_common.c b/src/exchangedb/test_common.c @@ -20,7 +20,7 @@ */ #include "test_common.h" #include "exchange-database/create_tables.h" -#include "exchange-database/do_insert_known_coin.h" +#include "exchange-database/do_insert_known_coins.h" #include "exchange-database/insert_denomination_info.h" #include "exchange-database/get_denomination_info.h" #include "exchange-database/do_deposit.h" @@ -346,10 +346,8 @@ TDB_coin (struct TALER_EXCHANGEDB_PostgresContext *pg, struct TALER_CoinPublicInfo *coin, uint64_t *known_coin_id) { - struct TALER_DenominationHashP dh; - struct TALER_AgeCommitmentHashP hac; - enum TALER_EXCHANGEDB_CoinKnownStatus cks; - uint64_t kci; + struct TALER_EXCHANGEDB_CoinKnownResult res; + const struct TALER_CoinPublicInfo *coins[1] = { coin }; memset (coin, 0, @@ -360,17 +358,17 @@ TDB_coin (struct TALER_EXCHANGEDB_PostgresContext *pg, coin->no_age_commitment = true; TDB_denom_sig (seed, &coin->denom_sig); - cks = TALER_EXCHANGEDB_do_insert_known_coin (pg, - coin, - &kci, - &dh, - &hac); + GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_EXCHANGEDB_do_insert_known_coins (pg, + 1, + coins, + &res)); /* CKS_PRESENT is fine: the same seed may name a coin an earlier check already made known. */ - GNUNET_assert ( (TALER_EXCHANGEDB_CKS_ADDED == cks) || - (TALER_EXCHANGEDB_CKS_PRESENT == cks) ); + GNUNET_assert ( (TALER_EXCHANGEDB_CKS_ADDED == res.status) || + (TALER_EXCHANGEDB_CKS_PRESENT == res.status) ); if (NULL != known_coin_id) - *known_coin_id = kci; + *known_coin_id = res.known_coin_id; } diff --git a/src/exchangedb/test_known_coins.c b/src/exchangedb/test_known_coins.c @@ -19,24 +19,50 @@ * `known_coins` * @author Christian Grothoff * - * Covers #TALER_EXCHANGEDB_do_insert_known_coin(), + * Covers #TALER_EXCHANGEDB_do_insert_known_coins(), * #TALER_EXCHANGEDB_get_known_coin(), * #TALER_EXCHANGEDB_get_coin_denomination(), * #TALER_EXCHANGEDB_get_signature_for_known_coin() and * #TALER_EXCHANGEDB_get_count_known_coins(). * * `known_coins` references `denominations`, so a denomination is created - * first with TDB_denom(). do_insert_known_coin() is the interesting one: + * first with TDB_denom(). do_insert_known_coins() is the interesting one: * it is idempotent, but only for a coin that comes back with the *same* * denomination and age commitment -- the conflicting cases are what its - * negative status codes are for. + * negative per-coin status codes are for. It takes a whole batch of + * coins in one round trip, so the last check mixes all cases in one call + * and verifies that every coin is reported at its own position. */ #include "test_common.h" -#include "exchange-database/do_insert_known_coin.h" +#include "exchange-database/do_insert_known_coins.h" #include "exchange-database/get_known_coin.h" #include "exchange-database/get_coin_denomination.h" #include "exchange-database/get_signature_for_known_coin.h" #include "exchange-database/get_count_known_coins.h" +#include "exchange-database/start.h" +#include "exchange-database/rollback.h" + + +/** + * Make a single @a coin known via the batch function. + * + * @param pg the database context + * @param coin the coin to make known + * @param[out] res outcome for the coin + * @return database status, #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT on success + */ +static enum GNUNET_DB_QueryStatus +insert_one (struct TALER_EXCHANGEDB_PostgresContext *pg, + const struct TALER_CoinPublicInfo *coin, + struct TALER_EXCHANGEDB_CoinKnownResult *res) +{ + const struct TALER_CoinPublicInfo *coins[1] = { coin }; + + return TALER_EXCHANGEDB_do_insert_known_coins (pg, + 1, + coins, + res); +} /** @@ -92,9 +118,7 @@ static int check_unknown_denomination (struct TALER_EXCHANGEDB_PostgresContext *pg) { struct TALER_CoinPublicInfo coin; - struct TALER_DenominationHashP dh; - struct TALER_AgeCommitmentHashP hac; - uint64_t known_coin_id; + struct TALER_EXCHANGEDB_CoinKnownResult res; memset (&coin, 0, @@ -106,14 +130,12 @@ check_unknown_denomination (struct TALER_EXCHANGEDB_PostgresContext *pg) coin.no_age_commitment = true; TDB_denom_sig (2, &coin.denom_sig); - /* the "dd" CTE finds no denomination, so nothing is inserted and the - UNION's second branch finds no coin either */ - FAILIF_C (TALER_EXCHANGEDB_CKS_HARD_FAIL != - TALER_EXCHANGEDB_do_insert_known_coin (pg, - &coin, - &known_coin_id, - &dh, - &hac), + /* no denomination row, so nothing is inserted and no coin is found + either: the row for the coin has no known_coin_id, a hard error */ + FAILIF_C (GNUNET_DB_STATUS_HARD_ERROR != + insert_one (pg, + &coin, + &res), TALER_denom_sig_free (&coin.denom_sig)); TALER_denom_sig_free (&coin.denom_sig); FAILIF (0 != TDB_count (pg, @@ -227,10 +249,7 @@ check_conflicts (struct TALER_EXCHANGEDB_PostgresContext *pg) struct TDB_Denom denom; struct TDB_Denom other; struct TALER_CoinPublicInfo coin; - struct TALER_DenominationHashP dh; - struct TALER_AgeCommitmentHashP hac; - uint64_t known_coin_id = 0; - uint64_t id2 = 0; + struct TALER_EXCHANGEDB_CoinKnownResult res; TDB_denom (pg, 10, @@ -253,46 +272,52 @@ check_conflicts (struct TALER_EXCHANGEDB_PostgresContext *pg) &coin.denom_sig); /* the coin from the previous check: already present, no conflict */ - FAILIF_C (TALER_EXCHANGEDB_CKS_PRESENT != - TALER_EXCHANGEDB_do_insert_known_coin (pg, - &coin, - &known_coin_id, - &dh, - &hac), + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + insert_one (pg, + &coin, + &res), TALER_denom_sig_free (&coin.denom_sig); TDB_denom_free (&denom); TDB_denom_free (&other)); - FAILIF_C (0 == known_coin_id, + FAILIF_C (TALER_EXCHANGEDB_CKS_PRESENT != res.status, + TALER_denom_sig_free (&coin.denom_sig); + TDB_denom_free (&denom); TDB_denom_free (&other)); + FAILIF_C (0 == res.known_coin_id, + TALER_denom_sig_free (&coin.denom_sig); + TDB_denom_free (&denom); TDB_denom_free (&other)); + FAILIF_C (! res.no_age_commitment, TALER_denom_sig_free (&coin.denom_sig); TDB_denom_free (&denom); TDB_denom_free (&other)); /* same coin key, different denomination: conflict, and the stored denomination is handed back so the caller can report it */ coin.denom_pub_hash = other.h_denom_pub; - FAILIF_C (TALER_EXCHANGEDB_CKS_DENOM_CONFLICT != - TALER_EXCHANGEDB_do_insert_known_coin (pg, - &coin, - &id2, - &dh, - &hac), + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + insert_one (pg, + &coin, + &res), TALER_denom_sig_free (&coin.denom_sig); TDB_denom_free (&denom); TDB_denom_free (&other)); - FAILIF_C (0 != GNUNET_memcmp (&dh, + FAILIF_C (TALER_EXCHANGEDB_CKS_DENOM_CONFLICT != res.status, + TALER_denom_sig_free (&coin.denom_sig); + TDB_denom_free (&denom); TDB_denom_free (&other)); + FAILIF_C (0 != GNUNET_memcmp (&res.h_denom_pub, &denom.h_denom_pub), TALER_denom_sig_free (&coin.denom_sig); TDB_denom_free (&denom); TDB_denom_free (&other)); /* same coin key and denomination, but now with an age commitment where - the stored row has none: the caller should have passed NULL */ + the stored row has none: the caller should have passed none */ coin.denom_pub_hash = denom.h_denom_pub; coin.no_age_commitment = false; TDB_FILL (coin.h_age_commitment, 21); - FAILIF_C (TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NULL != - TALER_EXCHANGEDB_do_insert_known_coin (pg, - &coin, - &id2, - &dh, - &hac), + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + insert_one (pg, + &coin, + &res), + TALER_denom_sig_free (&coin.denom_sig); + TDB_denom_free (&denom); TDB_denom_free (&other)); + FAILIF_C (TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NULL != res.status, TALER_denom_sig_free (&coin.denom_sig); TDB_denom_free (&denom); TDB_denom_free (&other)); TALER_denom_sig_free (&coin.denom_sig); @@ -323,9 +348,7 @@ check_age_commitment (struct TALER_EXCHANGEDB_PostgresContext *pg) struct TDB_Denom denom; struct TALER_CoinPublicInfo coin; struct TALER_CoinPublicInfo got; - struct TALER_DenominationHashP dh; - struct TALER_AgeCommitmentHashP hac; - uint64_t known_coin_id = 0; + struct TALER_EXCHANGEDB_CoinKnownResult res; TDB_denom (pg, 10, @@ -343,12 +366,16 @@ check_age_commitment (struct TALER_EXCHANGEDB_PostgresContext *pg) 31); TDB_denom_sig (30, &coin.denom_sig); - FAILIF_C (TALER_EXCHANGEDB_CKS_ADDED != - TALER_EXCHANGEDB_do_insert_known_coin (pg, - &coin, - &known_coin_id, - &dh, - &hac), + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + insert_one (pg, + &coin, + &res), + TALER_denom_sig_free (&coin.denom_sig); + TDB_denom_free (&denom)); + FAILIF_C (TALER_EXCHANGEDB_CKS_ADDED != res.status, + TALER_denom_sig_free (&coin.denom_sig); + TDB_denom_free (&denom)); + FAILIF_C (0 == res.known_coin_id, TALER_denom_sig_free (&coin.denom_sig); TDB_denom_free (&denom)); memset (&got, @@ -374,22 +401,32 @@ check_age_commitment (struct TALER_EXCHANGEDB_PostgresContext *pg) /* a different age commitment for the same coin is a conflict... */ TDB_FILL (coin.h_age_commitment, 32); - FAILIF_C (TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS != - TALER_EXCHANGEDB_do_insert_known_coin (pg, - &coin, - &known_coin_id, - &dh, - &hac), + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + insert_one (pg, + &coin, + &res), + TALER_denom_sig_free (&coin.denom_sig); + TDB_denom_free (&denom)); + FAILIF_C (TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS != res.status, + TALER_denom_sig_free (&coin.denom_sig); + TDB_denom_free (&denom)); + /* the stored age commitment is handed back for the error report */ + FAILIF_C (res.no_age_commitment, + TALER_denom_sig_free (&coin.denom_sig); + TDB_denom_free (&denom)); + FAILIF_C (0 != GNUNET_memcmp (&res.h_age_commitment, + &got.h_age_commitment), TALER_denom_sig_free (&coin.denom_sig); TDB_denom_free (&denom)); /* ...and so is no age commitment at all, where one is on file */ coin.no_age_commitment = true; - FAILIF_C (TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NON_NULL != - TALER_EXCHANGEDB_do_insert_known_coin (pg, - &coin, - &known_coin_id, - &dh, - &hac), + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + insert_one (pg, + &coin, + &res), + TALER_denom_sig_free (&coin.denom_sig); + TDB_denom_free (&denom)); + FAILIF_C (TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NON_NULL != res.status, TALER_denom_sig_free (&coin.denom_sig); TDB_denom_free (&denom)); TALER_denom_sig_free (&coin.denom_sig); @@ -404,6 +441,245 @@ check_age_commitment (struct TALER_EXCHANGEDB_PostgresContext *pg) /** + * One call with a mixed batch: two new coins (one with an age + * commitment), one already known, one with a conflicting denomination + * and one with a conflicting age commitment. Every coin must be + * reported at its own position, the new ones must be inserted, and a + * batch with a repeated coin or an unknown denomination is refused + * without inserting anything. + * + * @param pg the database context + * @return 0 on success + */ +static int +check_batch (struct TALER_EXCHANGEDB_PostgresContext *pg) +{ + struct TDB_Denom denom; + struct TDB_Denom other; + struct TALER_CoinPublicInfo coins[5]; + const struct TALER_CoinPublicInfo *pcoins[5]; + struct TALER_EXCHANGEDB_CoinKnownResult res[5]; + struct TALER_DenominationHashP dh; + uint64_t id; + uint64_t count_before; + int ret = 1; + + TDB_denom (pg, + 10, + "5", + "0.1", + &denom); + TDB_denom (pg, + 11, + "5", + "0.1", + &other); + memset (coins, + 0, + sizeof (coins)); + for (unsigned int i = 0; i < 5; i++) + { + coins[i].no_age_commitment = true; + coins[i].denom_pub_hash = denom.h_denom_pub; + pcoins[i] = &coins[i]; + } + /* [0]: new, no age commitment */ + TDB_FILL (coins[0].coin_pub, + 40); + TDB_denom_sig (40, + &coins[0].denom_sig); + /* [1]: new, with age commitment */ + TDB_FILL (coins[1].coin_pub, + 41); + coins[1].no_age_commitment = false; + TDB_FILL (coins[1].h_age_commitment, + 410); + TDB_denom_sig (41, + &coins[1].denom_sig); + /* [2]: known from check_insert_and_lookup (seed 20), same data */ + TDB_FILL (coins[2].coin_pub, + 20); + TDB_denom_sig (20, + &coins[2].denom_sig); + /* [3]: known (seed 20 again, different key below), other denomination */ + TDB_FILL (coins[3].coin_pub, + 20); + coins[3].denom_pub_hash = other.h_denom_pub; + TDB_denom_sig (20, + &coins[3].denom_sig); + /* [4]: known from check_age_commitment (seed 30), wrong age commitment */ + TDB_FILL (coins[4].coin_pub, + 30); + coins[4].no_age_commitment = false; + TDB_FILL (coins[4].h_age_commitment, + 33); + TDB_denom_sig (30, + &coins[4].denom_sig); + + count_before = TDB_count (pg, + "FROM known_coins"); + + /* [2] and [3] share a key: refused, nothing inserted */ + FAILIF_C (GNUNET_DB_STATUS_HARD_ERROR != + TALER_EXCHANGEDB_do_insert_known_coins (pg, + 5, + pcoins, + res), + goto cleanup); + FAILIF_C (count_before != TDB_count (pg, + "FROM known_coins"), + goto cleanup); + + /* give [3] its own key: a coin known under the other denomination */ + { + struct TALER_EXCHANGEDB_CoinKnownResult r1; + struct TALER_CoinPublicInfo tmp = coins[3]; + + TDB_FILL (tmp.coin_pub, + 43); + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + insert_one (pg, + &tmp, + &r1), + goto cleanup); + FAILIF_C (TALER_EXCHANGEDB_CKS_ADDED != r1.status, + goto cleanup); + count_before++; + TDB_FILL (coins[3].coin_pub, + 43); + coins[3].denom_pub_hash = denom.h_denom_pub; + } + + /* a new coin of an unknown denomination anywhere in the batch: the + batch is refused. The INSERT itself is one statement, so this is + done inside a transaction as the callers do it, and the rollback + removes the other new coin again. */ + TDB_FILL (coins[0].denom_pub_hash, + 44); + FAILIF_C (GNUNET_OK != + TALER_EXCHANGEDB_start (pg, + "test unknown denomination"), + goto cleanup); + FAILIF_C (GNUNET_DB_STATUS_HARD_ERROR != + TALER_EXCHANGEDB_do_insert_known_coins (pg, + 5, + pcoins, + res), + TALER_EXCHANGEDB_rollback (pg); goto cleanup); + TALER_EXCHANGEDB_rollback (pg); + FAILIF_C (count_before != TDB_count (pg, + "FROM known_coins"), + goto cleanup); + coins[0].denom_pub_hash = denom.h_denom_pub; + + /* now the mixed batch goes through, with one row per coin */ + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_do_insert_known_coins (pg, + 5, + pcoins, + res), + goto cleanup); + FAILIF_C (TALER_EXCHANGEDB_CKS_ADDED != res[0].status, + goto cleanup); + FAILIF_C (0 == res[0].known_coin_id, + goto cleanup); + FAILIF_C (TALER_EXCHANGEDB_CKS_ADDED != res[1].status, + goto cleanup); + FAILIF_C (0 == res[1].known_coin_id, + goto cleanup); + /* for added coins, the result reflects what was stored */ + FAILIF_C (! res[0].no_age_commitment, + goto cleanup); + FAILIF_C (0 != GNUNET_memcmp (&res[0].h_denom_pub, + &denom.h_denom_pub), + goto cleanup); + FAILIF_C (res[1].no_age_commitment, + goto cleanup); + FAILIF_C (0 != GNUNET_memcmp (&res[1].h_age_commitment, + &coins[1].h_age_commitment), + goto cleanup); + FAILIF_C (res[0].known_coin_id == res[1].known_coin_id, + goto cleanup); + FAILIF_C (TALER_EXCHANGEDB_CKS_PRESENT != res[2].status, + goto cleanup); + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_get_coin_denomination (pg, + &coins[2].coin_pub, + &id, + &dh), + goto cleanup); + FAILIF_C (id != res[2].known_coin_id, + goto cleanup); + FAILIF_C (TALER_EXCHANGEDB_CKS_DENOM_CONFLICT != res[3].status, + goto cleanup); + FAILIF_C (0 != GNUNET_memcmp (&res[3].h_denom_pub, + &other.h_denom_pub), + goto cleanup); + FAILIF_C (TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS != res[4].status, + goto cleanup); + FAILIF_C (res[4].no_age_commitment, + goto cleanup); + FAILIF_C (0 == GNUNET_memcmp (&res[4].h_age_commitment, + &coins[4].h_age_commitment), + goto cleanup); + /* exactly the two new coins were added */ + FAILIF_C (count_before + 2 != TDB_count (pg, + "FROM known_coins"), + goto cleanup); + { + struct TALER_CoinPublicInfo got; + + memset (&got, + 0, + sizeof (got)); + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_get_known_coin (pg, + &coins[1].coin_pub, + &got), + goto cleanup); + FAILIF_C (got.no_age_commitment, + TALER_denom_sig_free (&got.denom_sig); goto cleanup); + FAILIF_C (0 != GNUNET_memcmp (&got.h_age_commitment, + &coins[1].h_age_commitment), + TALER_denom_sig_free (&got.denom_sig); goto cleanup); + FAILIF_C (0 != TALER_denom_sig_cmp (&got.denom_sig, + &coins[1].denom_sig), + TALER_denom_sig_free (&got.denom_sig); goto cleanup); + TALER_denom_sig_free (&got.denom_sig); + } + + /* running the same batch again changes nothing: the new coins are + now merely present, the conflicts are reported as before */ + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_do_insert_known_coins (pg, + 5, + pcoins, + res), + goto cleanup); + FAILIF_C (TALER_EXCHANGEDB_CKS_PRESENT != res[0].status, + goto cleanup); + FAILIF_C (TALER_EXCHANGEDB_CKS_PRESENT != res[1].status, + goto cleanup); + FAILIF_C (TALER_EXCHANGEDB_CKS_PRESENT != res[2].status, + goto cleanup); + FAILIF_C (TALER_EXCHANGEDB_CKS_DENOM_CONFLICT != res[3].status, + goto cleanup); + FAILIF_C (TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS != res[4].status, + goto cleanup); + FAILIF_C (count_before + 2 != TDB_count (pg, + "FROM known_coins"), + goto cleanup); + ret = 0; +cleanup: + for (unsigned int i = 0; i < 5; i++) + TALER_denom_sig_free (&coins[i].denom_sig); + TDB_denom_free (&denom); + TDB_denom_free (&other); + return ret; +} + + +/** * The checks to run, in order. */ static const struct TDB_Test tests[] = { @@ -417,6 +693,8 @@ static const struct TDB_Test tests[] = { &check_conflicts }, { "known-coins-age-commitment", &check_age_commitment }, + { "known-coins-batch", + &check_batch }, { NULL, NULL } }; diff --git a/src/include/exchange-database/do_insert_known_coins.h b/src/include/exchange-database/do_insert_known_coins.h @@ -0,0 +1,152 @@ +/* + This file is part of TALER + Copyright (C) 2022-2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file src/include/exchange-database/do_insert_known_coins.h + * @brief make a batch of coins known to the database + * @author Christian Grothoff + * @author Özgür Kesim + */ +#ifndef EXCHANGE_DATABASE_DO_INSERT_KNOWN_COINS_H +#define EXCHANGE_DATABASE_DO_INSERT_KNOWN_COINS_H + +#include "exchangedb_lib.h" + +/** + * Possible outcomes for one coin of a batch that is made known. + */ +enum TALER_EXCHANGEDB_CoinKnownStatus +{ + /** + * The coin was successfully added. + */ + TALER_EXCHANGEDB_CKS_ADDED = 1, + + /** + * The coin was already present, with the same denomination + * and age commitment. + */ + TALER_EXCHANGEDB_CKS_PRESENT = 0, + + /** + * Conflicting coin (different denomination key) already in database. + */ + TALER_EXCHANGEDB_CKS_DENOM_CONFLICT = -3, + + /** + * Conflicting coin already in database: the caller passed an age + * commitment hash, but the stored coin has none. + * + * Whether a coin has an age commitment hash is determined by its + * denomination (age-restricted or not), and the denomination + * signature covers it. For a caller that verified that signature, + * this and the following two conflicts can therefore only arise + * together with a #TALER_EXCHANGEDB_CKS_DENOM_CONFLICT, which is + * reported instead. They are kept for callers that do not verify + * the signature. + */ + TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NULL = -4, + + /** + * Conflicting coin already in database: the caller passed no age + * commitment hash, but the stored coin has one. + */ + TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NON_NULL = -5, + + /** + * Conflicting coin already in database: the stored age commitment + * hash differs from the one passed by the caller. + */ + TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS = -6, + +}; + + +/** + * Result of making one coin known. + */ +struct TALER_EXCHANGEDB_CoinKnownResult +{ + /** + * What happened to the coin. + */ + enum TALER_EXCHANGEDB_CoinKnownStatus status; + + /** + * Row of the coin in the `known_coins` table. Valid unless + * @e status is #TALER_EXCHANGEDB_CKS_DENOM_CONFLICT or one of + * the age conflicts. + */ + uint64_t known_coin_id; + + /** + * Denomination hash of the coin as stored in the database. For a + * conflict, this is what the client must be told. Not valid for the + * age conflicts. + */ + struct TALER_DenominationHashP h_denom_pub; + + /** + * Age commitment hash of the coin as stored in the database. Only + * valid if @e no_age_commitment is false. For a conflict, this is + * what the client must be told. + */ + struct TALER_AgeCommitmentHashP h_age_commitment; + + /** + * True if the stored coin has no age commitment hash, which is the + * case exactly if its denomination is not age-restricted. Mirrors + * `struct TALER_CoinPublicInfo`. + */ + bool no_age_commitment; +}; + + +/** + * Make sure the given @a coins are known to the database, in one + * round trip. Coins that are not yet known are inserted; for coins + * that are already known, their stored denomination and age commitment + * are compared with what the caller passed and a conflict is reported + * per coin in @a results. The function does not abort on a conflict: + * the caller is expected to run it inside of a transaction and to roll + * that back if any entry of @a results reports a conflict, so that + * either all or none of the coins end up in the database. + * + * The coin public keys in @a coins must be distinct. A batch + * containing the same coin twice is rejected with + * #GNUNET_DB_STATUS_HARD_ERROR (a single INSERT cannot process the + * same key twice, and the caller should never build such a batch). + * The denomination of every coin must exist in the `denominations` + * table, otherwise #GNUNET_DB_STATUS_HARD_ERROR is returned as well. + * + * Primary test table: `known_coins` (see test_known_coins.c). + * + * @param pg the database context + * @param num_coins number of entries in @a coins and @a results + * @param coins the coins that must be made known + * @param[out] results set to the outcome for each coin, in the order + * of @a coins; only valid on success + * @return database transaction status; #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT + * if every coin was processed (check @a results for conflicts), + * negative on error + */ +enum GNUNET_DB_QueryStatus +TALER_EXCHANGEDB_do_insert_known_coins ( + struct TALER_EXCHANGEDB_PostgresContext *pg, + unsigned int num_coins, + const struct TALER_CoinPublicInfo *const coins[static num_coins], + struct TALER_EXCHANGEDB_CoinKnownResult results[static num_coins]); + +#endif diff --git a/src/testing/test_exchange_api.c b/src/testing/test_exchange_api.c @@ -1267,6 +1267,18 @@ run (void *cls, "create-batch-reserve-1", "EUR:0.01", MHD_HTTP_OK), + /* the same coin twice in one batch is a malformed request */ + TALER_TESTING_cmd_batch_deposit ( + "batch-deposit-duplicate-coin", + cred.user42_payto, + "{\"items\":[{\"name\":\"double ice cream\",\"value\":1}]}", + GNUNET_TIME_UNIT_ZERO, + MHD_HTTP_BAD_REQUEST, + "batch-withdraw-coin-1#0", + "EUR:1", + "batch-withdraw-coin-1#0", + "EUR:1", + NULL), /** * Spend the coins. */ diff --git a/src/testing/testing_api_cmd_batch_deposit.c b/src/testing/testing_api_cmd_batch_deposit.c @@ -586,12 +586,14 @@ batch_deposit_traits (void *cls, { struct TALER_TESTING_Trait traits[] = { - /* First two traits are only available if - ds->traits is #GNUNET_YES */ + /* First three traits are only available if + the deposit succeeded */ TALER_TESTING_make_trait_exchange_pub (0, &ds->exchange_pub), TALER_TESTING_make_trait_exchange_sig (0, &ds->exchange_sig), + TALER_TESTING_make_trait_coin_history (index, + &coin->che), /* These traits are always available */ TALER_TESTING_make_trait_wire_details (ds->wire_details), TALER_TESTING_make_trait_contract_terms (ds->contract_terms), @@ -601,8 +603,6 @@ batch_deposit_traits (void *cls, TALER_TESTING_make_trait_account_pub (&ds->account_pub), TALER_TESTING_make_trait_age_commitment_proof (index, age_commitment_proof), - TALER_TESTING_make_trait_coin_history (index, - &coin->che), TALER_TESTING_make_trait_coin_pub (index, coin_spent_pub), TALER_TESTING_make_trait_denom_pub (index, @@ -628,7 +628,7 @@ batch_deposit_traits (void *cls, return TALER_TESTING_get_trait ((ds->deposit_succeeded) ? traits - : &traits[2], + : &traits[3], ret, trait, index); diff --git a/src/testing/testing_api_cmd_insert_deposit.c b/src/testing/testing_api_cmd_insert_deposit.c @@ -36,7 +36,7 @@ #include "exchange-database/preflight.h" #include "exchange-database/do_deposit.h" #include "exchange-database/insert_denomination_info.h" -#include "exchange-database/do_insert_known_coin.h" +#include "exchange-database/do_insert_known_coins.h" /** * State for a "insert-deposit" CMD. @@ -282,10 +282,9 @@ insert_deposit_run (void *cls, ids->wire_deadline); /* finally, actually perform the DB operation */ { - uint64_t known_coin_id; + const struct TALER_CoinPublicInfo *coins[1] = { &deposit.coin }; + struct TALER_EXCHANGEDB_CoinKnownResult ckr; struct TALER_Amount total; - struct TALER_DenominationHashP dph; - struct TALER_AgeCommitmentHashP agh; bool balance_ok; uint32_t bad_index; bool ctr_conflict; @@ -293,12 +292,12 @@ insert_deposit_run (void *cls, if ( (GNUNET_OK != TALER_EXCHANGEDB_start (ids->plugin, "libtalertesting: insert deposit")) || - (0 > - TALER_EXCHANGEDB_do_insert_known_coin (ids->plugin, - &deposit.coin, - &known_coin_id, - &dph, - &agh)) || + (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_do_insert_known_coins (ids->plugin, + 1, + coins, + &ckr)) || + (0 > ckr.status) || (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != TALER_EXCHANGEDB_do_deposit (ids->plugin, &bd,