merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit d221b1d66cc2670f07f003d48dcbd18eb0c23a88
parent 674f4c61b1e2d1ad1b74e4a5f2cc1590bcc6bd4b
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sun,  8 Mar 2026 00:31:18 +0100

proposed fix for #11206

Diffstat:
Msrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c | 284++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
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);