exchange

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

commit 0acdec7b3de4e2019660dbbfa25a19f3c90a2b38
parent 4eb89e723fcc79de51a88ad889495407db9300bb
Author: Christian Grothoff <christian@grothoff.org>
Date:   Tue, 13 Oct 2026 00:33:52 +0200

prevent full-table scan by selecting also on reserve_pub

Diffstat:
Msrc/exchange/taler-exchange-httpd.c | 3++-
Msrc/exchange/taler-exchange-httpd_post-recoup-withdraw.c | 12+++---------
Msrc/exchange/taler-exchange-httpd_post-reveal-withdraw.c | 13++++++++++++-
Msrc/exchange/taler-exchange-httpd_post-withdraw.c | 1+
Msrc/exchangedb/get_withdraw.c | 5++++-
Msrc/exchangedb/test_withdraw.c | 22+++++++++++++++++++++-
Msrc/include/exchange-database/get_withdraw.h | 29+++++++++++++++++------------
Msrc/include/taler/exchange/post-reveal-withdraw.h | 2++
Msrc/lib/exchange_api_post-reveal-withdraw.c | 9+++++++++
Msrc/testing/testing_api_cmd_age_withdraw.c | 1+
10 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/src/exchange/taler-exchange-httpd.c b/src/exchange/taler-exchange-httpd.c @@ -1765,7 +1765,8 @@ handle_mhd_request (void *cls, .handler.post = &handle_post_reserves, .nargs = 2 }, - /* reveal-withdraw operation, introduced with v26 */ + /* reveal-withdraw operation, introduced with v26; + reserve_pub required in the body since v42 */ { .url = "reveal-withdraw", .method = MHD_HTTP_METHOD_POST, diff --git a/src/exchange/taler-exchange-httpd_post-recoup-withdraw.c b/src/exchange/taler-exchange-httpd_post-recoup-withdraw.c @@ -361,6 +361,7 @@ phase_lookup_operation (struct RecoupWithdrawContext *wc) while (0 < max_retries--) { qs = TALER_EXCHANGEDB_get_withdraw (TEH_pg, + &wc->request.reserve_pub, &wc->request.planchets_h, &wc->withdraw); if (GNUNET_DB_STATUS_SOFT_ERROR != qs) @@ -385,15 +386,8 @@ phase_lookup_operation (struct RecoupWithdrawContext *wc) return; } wc->have_withdraw = true; - if (0 != - GNUNET_memcmp (&wc->withdraw.reserve_pub, - &wc->request.reserve_pub)) - { - GNUNET_break_op (0); - SET_ERROR (wc, - RECOUP_WITHDRAW_ERROR_WITHDRAW_NOT_FOUND); - return; - } + /* The lookup is keyed on (reserve_pub, planchets_h), so the + withdraw found belongs to the reserve given in the request. */ if (wc->withdraw.num_coins != wc->request.num_coins) { GNUNET_break_op (0); diff --git a/src/exchange/taler-exchange-httpd_post-reveal-withdraw.c b/src/exchange/taler-exchange-httpd_post-reveal-withdraw.c @@ -38,6 +38,11 @@ struct WithdrawRevealContext { /** + * Public key of the reserve the original withdraw was made from. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** * Commitment for the withdraw operation, previously called by the * client. */ @@ -50,7 +55,7 @@ struct WithdrawRevealContext /** * The data from the original withdraw. Will be retrieved from - * the DB via @a wch. + * the DB via @e reserve_pub and @e planchets_h. */ struct TALER_EXCHANGEDB_Withdraw withdraw; }; @@ -136,6 +141,7 @@ parse_withdraw_reveal_json ( * Otherwise, it queues an appropriate MHD response. * * @param connection The HTTP connection to the client + * @param reserve_pub Reserve the original withdraw request was made from * @param planchets_h Original commitment value sent with the withdraw request * @param[out] withdraw Data from the original withdraw request * @param[out] result In the error cases, a response will be queued with MHD and this will be the result. @@ -145,6 +151,7 @@ parse_withdraw_reveal_json ( static enum GNUNET_GenericReturnValue find_original_withdraw ( struct MHD_Connection *connection, + const struct TALER_ReservePublicKeyP *reserve_pub, const struct TALER_HashBlindedPlanchetsP *planchets_h, struct TALER_EXCHANGEDB_Withdraw *withdraw, enum MHD_Result *result) @@ -154,6 +161,7 @@ find_original_withdraw ( for (unsigned int try = 0; try < 3; try++) { qs = TALER_EXCHANGEDB_get_withdraw (TEH_pg, + reserve_pub, planchets_h, withdraw); switch (qs) @@ -615,6 +623,8 @@ TEH_handler_reveal_withdraw ( struct WithdrawRevealContext actx = {0}; const json_t *j_disclosed_batch_seeds; struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("reserve_pub", + &actx.reserve_pub), GNUNET_JSON_spec_fixed_auto ("planchets_h", &actx.planchets_h), GNUNET_JSON_spec_array_const ("disclosed_batch_seeds", @@ -646,6 +656,7 @@ TEH_handler_reveal_withdraw ( if (GNUNET_OK != find_original_withdraw ( rc->connection, + &actx.reserve_pub, &actx.planchets_h, &actx.withdraw, &result)) diff --git a/src/exchange/taler-exchange-httpd_post-withdraw.c b/src/exchange/taler-exchange-httpd_post-withdraw.c @@ -383,6 +383,7 @@ withdraw_is_idempotent ( { qs = TALER_EXCHANGEDB_get_withdraw ( TEH_pg, + &wc->request.withdraw.reserve_pub, &wc->request.withdraw.planchets_h, &wc->request.withdraw_idem); if (GNUNET_DB_STATUS_SOFT_ERROR != qs) diff --git a/src/exchangedb/get_withdraw.c b/src/exchangedb/get_withdraw.c @@ -26,11 +26,13 @@ enum GNUNET_DB_QueryStatus TALER_EXCHANGEDB_get_withdraw ( struct TALER_EXCHANGEDB_PostgresContext *pg, + const struct TALER_ReservePublicKeyP *reserve_pub, const struct TALER_HashBlindedPlanchetsP *wch, struct TALER_EXCHANGEDB_Withdraw *wd) { enum GNUNET_DB_QueryStatus ret; struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (reserve_pub), GNUNET_PQ_query_param_auto_from_type (wch), GNUNET_PQ_query_param_end }; @@ -117,7 +119,8 @@ TALER_EXCHANGEDB_get_withdraw ( ",denom_sigs" ",denom_serials" " FROM withdraw" - " WHERE planchets_h=$1;"); + " WHERE reserve_pub=$1" + " AND planchets_h=$2;"); ret = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "get_withdraw", params, diff --git a/src/exchangedb/test_withdraw.c b/src/exchangedb/test_withdraw.c @@ -386,6 +386,7 @@ check_unknown_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg) FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != TALER_EXCHANGEDB_get_withdraw (pg, + &reserve_pub, &h, &got)); } @@ -494,12 +495,13 @@ check_withdraw (struct TALER_EXCHANGEDB_PostgresContext *pg) &expect_left), free_withdraw (&wd)); - /* the request can be looked up by its planchet hash */ + /* the request can be looked up by its reserve and planchet hash */ memset (&got, 0, sizeof (got)); FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != TALER_EXCHANGEDB_get_withdraw (pg, + &reserve_pub, &wd.planchets_h, &got), free_withdraw (&wd)); @@ -526,6 +528,24 @@ check_withdraw (struct TALER_EXCHANGEDB_PostgresContext *pg) free_withdraw (&got); free_withdraw (&wd)); free_withdraw (&got); + /* the planchet hash is only unique per reserve: another reserve does + not see this request */ + { + struct TALER_ReservePublicKeyP other_pub; + + TDB_FILL (other_pub, + 99); + memset (&got, + 0, + sizeof (got)); + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != + TALER_EXCHANGEDB_get_withdraw (pg, + &other_pub, + &wd.planchets_h, + &got), + free_withdraw (&wd)); + } + /* a replay of the same request is idempotent and does not debit again */ FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != run_withdraw (pg, diff --git a/src/include/exchange-database/get_withdraw.h b/src/include/exchange-database/get_withdraw.h @@ -26,20 +26,25 @@ /** - * Locate the response for a withdraw request under a hash that uniquely - * identifies the withdraw operation. Used to ensure idempotency of the - * request. - * + * Locate the response for a withdraw request under its primary key, the + * reserve and the hash over the blinded planchets. Used to ensure + * idempotency of the request, for /reveal-withdraw and for + * /recoup-withdraw. The same @a wch may legitimately exist under + * several reserves, so the lookup must always include the reserve. + * * Primary test table: `withdraw` (see test_withdraw.c). * - * @param pg the database context - * @param wch hash over all hashes of blinded planchets that uniquely identifies the withdraw operation - * @param[out] wd corresponding details of the previous withdraw request if an entry was found - * @return statement execution status - */ + * @param pg the database context + * @param reserve_pub public key of the reserve the coins were withdrawn from + * @param wch hash over all hashes of blinded planchets that uniquely identifies the withdraw operation of @a reserve_pub + * @param[out] wd corresponding details of the previous withdraw request if an entry was found + * @return statement execution status + */ enum GNUNET_DB_QueryStatus -TALER_EXCHANGEDB_get_withdraw (struct TALER_EXCHANGEDB_PostgresContext *pg, - const struct TALER_HashBlindedPlanchetsP *wch, - struct TALER_EXCHANGEDB_Withdraw *wd); +TALER_EXCHANGEDB_get_withdraw ( + struct TALER_EXCHANGEDB_PostgresContext *pg, + const struct TALER_ReservePublicKeyP *reserve_pub, + const struct TALER_HashBlindedPlanchetsP *wch, + struct TALER_EXCHANGEDB_Withdraw *wd); #endif diff --git a/src/include/taler/exchange/post-reveal-withdraw.h b/src/include/taler/exchange/post-reveal-withdraw.h @@ -44,6 +44,7 @@ struct TALER_EXCHANGE_PostRevealWithdrawHandle; * @param curl_ctx The curl context * @param exchange_url The base url of the exchange * @param num_coins Number of coin signatures to expect from the reveal + * @param reserve_pub The reserve the previous call to withdraw was made from * @param h_planchets The commitment from the previous call to withdraw * @param seeds TALER_CNC_KAPPA-1 tuple of seeds to reveal * @return handle to operation, NULL on error @@ -53,6 +54,7 @@ TALER_EXCHANGE_post_reveal_withdraw_create ( struct GNUNET_CURL_Context *curl_ctx, const char *exchange_url, size_t num_coins, + const struct TALER_ReservePublicKeyP *reserve_pub, const struct TALER_HashBlindedPlanchetsP *h_planchets, const struct TALER_RevealWithdrawMasterSeedsP *seeds); diff --git a/src/lib/exchange_api_post-reveal-withdraw.c b/src/lib/exchange_api_post-reveal-withdraw.c @@ -40,6 +40,11 @@ struct TALER_EXCHANGE_PostRevealWithdrawHandle { /** + * The reserve the previous call to withdraw was made from + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** * The commitment from the previous call to withdraw */ struct TALER_HashBlindedPlanchetsP planchets_h; @@ -287,6 +292,8 @@ perform_protocol ( json_t *j_request_body; j_request_body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("reserve_pub", + &wrh->reserve_pub), GNUNET_JSON_pack_data_auto ("planchets_h", &wrh->planchets_h), GNUNET_JSON_pack_array_steal ("disclosed_batch_seeds", @@ -324,6 +331,7 @@ TALER_EXCHANGE_post_reveal_withdraw_create ( struct GNUNET_CURL_Context *curl_ctx, const char *exchange_url, size_t num_coins, + const struct TALER_ReservePublicKeyP *reserve_pub, const struct TALER_HashBlindedPlanchetsP *h_planchets, const struct TALER_RevealWithdrawMasterSeedsP *seeds) { @@ -333,6 +341,7 @@ TALER_EXCHANGE_post_reveal_withdraw_create ( wrh->curl_ctx = curl_ctx; wrh->exchange_url = GNUNET_strdup (exchange_url); wrh->num_coins = num_coins; + wrh->reserve_pub = *reserve_pub; wrh->planchets_h = *h_planchets; wrh->seeds = *seeds; return wrh; diff --git a/src/testing/testing_api_cmd_age_withdraw.c b/src/testing/testing_api_cmd_age_withdraw.c @@ -750,6 +750,7 @@ age_reveal_withdraw_run ( TALER_TESTING_interpreter_get_context (is), TALER_TESTING_get_exchange_url (is), aws->num_coins, + &aws->reserve_pub, &aws->planchets_h, &revealed_seeds); GNUNET_assert (NULL != awrs->handle);