commit d221b1d66cc2670f07f003d48dcbd18eb0c23a88
parent 674f4c61b1e2d1ad1b74e4a5f2cc1590bcc6bd4b
Author: Christian Grothoff <christian@grothoff.org>
Date: Sun, 8 Mar 2026 00:31:18 +0100
proposed fix for #11206
Diffstat:
1 file changed, 180 insertions(+), 104 deletions(-)
diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c
@@ -264,6 +264,11 @@ struct DepositConfirmation
*/
bool matched_in_db;
+ /**
+ * True if this coin is in the current batch.
+ */
+ bool in_batch;
+
};
struct TokenUseConfirmation
@@ -350,8 +355,8 @@ struct ExchangeGroup
struct PayContext *pc;
/**
- * Handle to the batch deposit operation we are performing for this
- * exchange, NULL after the operation is done.
+ * Handle to the batch deposit operation currently in flight for this
+ * exchange, NULL when no operation is pending.
*/
struct TALER_EXCHANGE_PostBatchDepositHandle *bdh;
@@ -369,6 +374,11 @@ struct ExchangeGroup
const char *exchange_url;
/**
+ * The keys of the exchange.
+ */
+ struct TALER_EXCHANGE_Keys *keys;
+
+ /**
* Total deposit amount in this exchange group.
*/
struct TALER_Amount total;
@@ -1020,9 +1030,9 @@ phase_fail_for_legal_reasons (struct PayContext *pc)
* @return transaction status
*/
static enum GNUNET_DB_QueryStatus
-batch_deposit_transaction (const struct ExchangeGroup *eg,
- const struct
- TALER_EXCHANGE_PostBatchDepositResponse *dr)
+batch_deposit_transaction (
+ const struct ExchangeGroup *eg,
+ const struct TALER_EXCHANGE_PostBatchDepositResponse *dr)
{
const struct PayContext *pc = eg->pc;
enum GNUNET_DB_QueryStatus qs;
@@ -1044,6 +1054,8 @@ batch_deposit_transaction (const struct ExchangeGroup *eg,
continue;
if (dc->found_in_db)
continue;
+ if (! dc->in_batch)
+ continue;
GNUNET_assert (0 <=
TALER_amount_subtract (&amount_without_fees,
&dc->cdd.amount,
@@ -1079,6 +1091,8 @@ batch_deposit_transaction (const struct ExchangeGroup *eg,
continue;
if (dc->found_in_db)
continue;
+ if (! dc->in_batch)
+ continue;
/* FIXME-#9457: We might want to check if the order was fully paid concurrently
by some other wallet here, and if so, issue an auto-refund. Right now,
it is possible to over-pay if two wallets literally make a concurrent
@@ -1112,9 +1126,9 @@ batch_deposit_transaction (const struct ExchangeGroup *eg,
* @param dr response from the server
*/
static void
-handle_batch_deposit_ok (struct ExchangeGroup *eg,
- const struct TALER_EXCHANGE_PostBatchDepositResponse *
- dr)
+handle_batch_deposit_ok (
+ struct ExchangeGroup *eg,
+ const struct TALER_EXCHANGE_PostBatchDepositResponse *dr)
{
struct PayContext *pc = eg->pc;
enum GNUNET_DB_QueryStatus qs
@@ -1187,11 +1201,14 @@ handle_batch_deposit_ok (struct ExchangeGroup *eg,
struct DepositConfirmation *dc = &pc->parse_pay.dc[i];
if (0 != strcmp (eg->exchange_url,
- pc->parse_pay.dc[i].exchange_url))
+ dc->exchange_url))
continue;
if (dc->found_in_db)
continue;
+ if (! dc->in_batch)
+ continue;
dc->found_in_db = true; /* well, at least NOW it'd be true ;-) */
+ dc->in_batch = false;
pc->pay_transaction.pending--;
}
}
@@ -1231,6 +1248,15 @@ notify_kyc_required (const struct ExchangeGroup *eg)
/**
+ * Run batch deposits for @a eg.
+ *
+ * @param[in,out] eg group to do batch deposits for
+ */
+static void
+do_batch_deposits (struct ExchangeGroup *eg);
+
+
+/**
* Callback to handle a batch deposit permission's response.
*
* @param cls a `struct ExchangeGroup`
@@ -1255,14 +1281,20 @@ batch_deposit_cb (
case MHD_HTTP_OK:
handle_batch_deposit_ok (eg,
dr);
- if ( (GNUNET_YES == pc->suspended) &&
- (0 == pc->batch_deposits.pending_at_eg) )
- {
- pc->phase = PP_COMPUTE_MONEY_POTS;
- pay_resume (pc);
- }
+ if (GNUNET_YES != pc->suspended)
+ return; /* handle_batch_deposit_ok already resumed with an error */
+ do_batch_deposits (eg);
return;
case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
+ for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++)
+ {
+ struct DepositConfirmation *dc = &pc->parse_pay.dc[i];
+
+ if (0 != strcmp (eg->exchange_url,
+ dc->exchange_url))
+ continue;
+ dc->in_batch = false;
+ }
notify_kyc_required (eg);
eg->got_451 = true;
pc->batch_deposits.got_451 = true;
@@ -1289,6 +1321,15 @@ batch_deposit_cb (
"Deposit operation failed with HTTP code %u/%d\n",
dr->hr.http_status,
(int) dr->hr.ec);
+ for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++)
+ {
+ struct DepositConfirmation *dc = &pc->parse_pay.dc[i];
+
+ if (0 != strcmp (eg->exchange_url,
+ dc->exchange_url))
+ continue;
+ dc->in_batch = false;
+ }
/* Transaction failed */
if (5 == dr->hr.http_status / 100)
{
@@ -1343,6 +1384,122 @@ batch_deposit_cb (
}
+static void
+do_batch_deposits (struct ExchangeGroup *eg)
+{
+ struct PayContext *pc = eg->pc;
+ struct TMH_HandlerContext *hc = pc->hc;
+ unsigned int group_size = 0;
+ /* Initiate /batch-deposit operation for all coins of
+ the current exchange (!) */
+
+ GNUNET_assert (NULL != eg->keys);
+ for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++)
+ {
+ struct DepositConfirmation *dc = &pc->parse_pay.dc[i];
+
+ if (0 != strcmp (eg->exchange_url,
+ pc->parse_pay.dc[i].exchange_url))
+ continue;
+ if (dc->found_in_db)
+ continue;
+ group_size++;
+ if (group_size >= TALER_MAX_COINS)
+ break;
+ }
+ if (0 == group_size)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Group size zero, %u batch transactions remain pending\n",
+ pc->batch_deposits.pending_at_eg);
+ if (0 == pc->batch_deposits.pending_at_eg)
+ {
+ pc->phase = PP_COMPUTE_MONEY_POTS;
+ pay_resume (pc);
+ return;
+ }
+ return;
+ }
+ /* Dispatch the next batch of up to TALER_MAX_COINS coins.
+ On success, batch_deposit_cb() will re-invoke
+ do_batch_deposits() to send further batches until
+ all coins are done. */
+ {
+ struct TALER_EXCHANGE_DepositContractDetail dcd = {
+ .wire_deadline = pc->check_contract.contract_terms->wire_deadline,
+ .merchant_payto_uri = pc->check_contract.wm->payto_uri,
+ .extra_wire_subject_metadata = pc->check_contract.wm->
+ extra_wire_subject_metadata,
+ .wire_salt = pc->check_contract.wm->wire_salt,
+ .h_contract_terms = pc->check_contract.h_contract_terms,
+ .wallet_data_hash = pc->parse_wallet_data.h_wallet_data,
+ .wallet_timestamp = pc->check_contract.contract_terms->timestamp,
+ .merchant_pub = hc->instance->merchant_pub,
+ .refund_deadline = pc->check_contract.contract_terms->refund_deadline
+ };
+ /* Collect up to TALER_MAX_COINS eligible coins for this batch */
+ struct TALER_EXCHANGE_CoinDepositDetail cdds[group_size];
+ unsigned int batch_size = 0;
+ enum TALER_ErrorCode ec;
+
+ /* FIXME-optimization: move signing outside of this 'loop'
+ and into the code that runs long before we look at a
+ specific exchange, otherwise we sign repeatedly! */
+ TALER_merchant_contract_sign (&pc->check_contract.h_contract_terms,
+ &pc->hc->instance->merchant_priv,
+ &dcd.merchant_sig);
+ for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++)
+ {
+ struct DepositConfirmation *dc = &pc->parse_pay.dc[i];
+
+ if (dc->found_in_db)
+ continue;
+ if (0 != strcmp (dc->exchange_url,
+ eg->exchange_url))
+ continue;
+ dc->in_batch = true;
+ cdds[batch_size++] = dc->cdd;
+ if (batch_size == group_size)
+ break;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Initiating batch deposit with %u coins\n",
+ batch_size);
+ /* Note: the coin signatures over the wallet_data_hash are
+ checked inside of this call */
+ eg->bdh = TALER_EXCHANGE_post_batch_deposit_create (
+ TMH_curl_ctx,
+ eg->exchange_url,
+ eg->keys,
+ &dcd,
+ batch_size,
+ cdds,
+ &ec);
+ if (NULL == eg->bdh)
+ {
+ /* Signature was invalid or some other constraint was not satisfied. If
+ the exchange was unavailable, we'd get that information in the
+ callback. */
+ GNUNET_break_op (0);
+ resume_pay_with_response (
+ pc,
+ TALER_ErrorCode_get_http_status_safe (ec),
+ TALER_MHD_MAKE_JSON_PACK (
+ TALER_JSON_pack_ec (ec),
+ GNUNET_JSON_pack_string ("exchange_url",
+ eg->exchange_url)));
+ return;
+ }
+ pc->batch_deposits.pending_at_eg++;
+ if (TMH_force_audit)
+ TALER_EXCHANGE_post_batch_deposit_force_dc (eg->bdh);
+ TALER_EXCHANGE_post_batch_deposit_start (eg->bdh,
+ &batch_deposit_cb,
+ eg);
+ }
+}
+
+
/**
* Force re-downloading keys for @a eg.
*
@@ -1368,13 +1525,13 @@ process_pay_with_keys (
struct ExchangeGroup *eg = cls;
struct PayContext *pc = eg->pc;
struct TMH_HandlerContext *hc = pc->hc;
- unsigned int group_size;
struct TALER_Amount max_amount;
enum TMH_ExchangeStatus es;
eg->fo = NULL;
pc->batch_deposits.pending_at_eg--;
GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id);
+ eg->keys = TALER_EXCHANGE_keys_incref (keys);
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Processing payment with keys from exchange %s\n",
eg->exchange_url);
@@ -1456,9 +1613,8 @@ process_pay_with_keys (
"Got wire data for %s\n",
eg->exchange_url);
- /* Initiate /batch-deposit operation for all coins of
- the current exchange (!) */
- group_size = 0;
+ /* Check all coins satisfy constraints like deposit deadlines
+ and age restrictions */
for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->parse_pay.dc[i];
@@ -1591,93 +1747,9 @@ AGE_FAIL:
&denom_details->h_key)));
return;
}
- group_size++;
}
- if (0 == group_size)
- {
- GNUNET_break (0);
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Group size zero, %u batch transactions remain pending\n",
- pc->batch_deposits.pending_at_eg);
- if (0 == pc->batch_deposits.pending_at_eg)
- {
- pc->phase = PP_COMPUTE_MONEY_POTS;
- pay_resume (pc);
- return;
- }
- return;
- }
- if (group_size > TALER_MAX_COINS)
- group_size = TALER_MAX_COINS;
- {
- struct TALER_EXCHANGE_CoinDepositDetail cdds[group_size];
- struct TALER_EXCHANGE_DepositContractDetail dcd = {
- .wire_deadline = pc->check_contract.contract_terms->wire_deadline,
- .merchant_payto_uri = pc->check_contract.wm->payto_uri,
- .extra_wire_subject_metadata = pc->check_contract.wm->
- extra_wire_subject_metadata,
- .wire_salt = pc->check_contract.wm->wire_salt,
- .h_contract_terms = pc->check_contract.h_contract_terms,
- .wallet_data_hash = pc->parse_wallet_data.h_wallet_data,
- .wallet_timestamp = pc->check_contract.contract_terms->timestamp,
- .merchant_pub = hc->instance->merchant_pub,
- .refund_deadline = pc->check_contract.contract_terms->refund_deadline
- };
- enum TALER_ErrorCode ec;
- size_t off = 0;
-
- TALER_merchant_contract_sign (&pc->check_contract.h_contract_terms,
- &pc->hc->instance->merchant_priv,
- &dcd.merchant_sig);
- for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++)
- {
- struct DepositConfirmation *dc = &pc->parse_pay.dc[i];
-
- if (dc->found_in_db)
- continue;
- if (0 != strcmp (dc->exchange_url,
- eg->exchange_url))
- continue;
- cdds[off++] = dc->cdd;
- if (off >= group_size)
- break;
- }
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Initiating batch deposit with %u coins\n",
- group_size);
- /* Note: the coin signatures over the wallet_data_hash are
- checked inside of this call */
- eg->bdh = TALER_EXCHANGE_post_batch_deposit_create (
- TMH_curl_ctx,
- eg->exchange_url,
- keys,
- &dcd,
- group_size,
- cdds,
- &ec);
- if (NULL == eg->bdh)
- {
- /* Signature was invalid or some other constraint was not satisfied. If
- the exchange was unavailable, we'd get that information in the
- callback. */
- GNUNET_break_op (0);
- resume_pay_with_response (
- pc,
- TALER_ErrorCode_get_http_status_safe (ec),
- TALER_MHD_MAKE_JSON_PACK (
- TALER_JSON_pack_ec (ec),
- GNUNET_JSON_pack_string ("exchange_url",
- eg->exchange_url)));
- return;
- }
- pc->batch_deposits.pending_at_eg++;
- if (TMH_force_audit)
- TALER_EXCHANGE_post_batch_deposit_force_dc (eg->bdh);
- TALER_EXCHANGE_post_batch_deposit_start (eg->bdh,
- &batch_deposit_cb,
- eg);
- }
+ do_batch_deposits (eg);
}
@@ -5143,6 +5215,10 @@ pay_context_cleanup (void *cls)
if (NULL != eg->fo)
TMH_EXCHANGES_keys4exchange_cancel (eg->fo);
+ if (NULL != eg->bdh)
+ TALER_EXCHANGE_post_batch_deposit_cancel (eg->bdh);
+ if (NULL != eg->keys)
+ TALER_EXCHANGE_keys_decref (eg->keys);
GNUNET_free (eg);
}
GNUNET_free (pc->parse_pay.egs);