merchant

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

commit 107c2b6a765fc288087f6a6b63eaf18dd02f86ae
parent 6b4ca8adb3bab9fc0e61e094999d6a29620801a3
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sat, 16 Aug 2025 15:33:00 +0200

fix idempotency check for spent tokens (#9434)

Diffstat:
Msrc/backend/taler-merchant-httpd_post-orders-ID-pay.c | 18++++++++++--------
Msrc/backenddb/pg_insert_spent_token.c | 14+++++++++-----
Msrc/backenddb/pg_insert_spent_token.sql | 19+++++++++++++++----
3 files changed, 34 insertions(+), 17 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -2672,22 +2672,21 @@ phase_execute_pay_transaction (struct PayContext *pc) &tuc->sig, &tuc->unblinded_sig); - if (0 > qs) + switch (qs) { + case GNUNET_DB_STATUS_SOFT_ERROR: TMH_db->rollback (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return; /* do it again */ + return; /* do it again */ + case GNUNET_DB_STATUS_HARD_ERROR: /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + TMH_db->rollback (TMH_db->cls); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, "insert used token")); return; - } - else if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: /* UNIQUE constraint violation, meaning this token was already used. */ TMH_db->rollback (TMH_db->cls); pay_end (pc, @@ -2696,8 +2695,11 @@ phase_execute_pay_transaction (struct PayContext *pc) TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_INVALID, NULL)); return; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* Good, proceed! */ + break; } - } + } /* for all tokens */ { enum GNUNET_DB_QueryStatus qs; diff --git a/src/backenddb/pg_insert_spent_token.c b/src/backenddb/pg_insert_spent_token.c @@ -45,12 +45,12 @@ TMH_PG_insert_spent_token ( GNUNET_PQ_query_param_end }; bool no_fam; - bool existed; + bool conflict; /* used to signal double-spending */ struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_bool ("out_no_family", &no_fam), - GNUNET_PQ_result_spec_bool ("out_existed", - &existed), + GNUNET_PQ_result_spec_bool ("out_conflict", + &conflict), GNUNET_PQ_result_spec_end }; enum GNUNET_DB_QueryStatus qs; @@ -63,7 +63,7 @@ TMH_PG_insert_spent_token ( "spent_token_insert", "SELECT" " out_no_family" - " ,out_existed" + " ,out_conflict" " FROM merchant_do_insert_spent_token" "($1, $2, $3, $4, $5);"); qs = GNUNET_PQ_eval_prepared_singleton_select ( @@ -78,7 +78,11 @@ TMH_PG_insert_spent_token ( GNUNET_break (0); return GNUNET_DB_STATUS_HARD_ERROR; } - if (existed) + if (conflict) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client attempted to double-spend token\n"); return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } return qs; } diff --git a/src/backenddb/pg_insert_spent_token.sql b/src/backenddb/pg_insert_spent_token.sql @@ -23,7 +23,7 @@ CREATE FUNCTION merchant_do_insert_spent_token ( IN in_use_sig BYTEA, IN in_issue_sig BYTEA, OUT out_no_family BOOL, - OUT out_existed BOOL) + OUT out_conflict BOOL) LANGUAGE plpgsql AS $$ DECLARE @@ -41,7 +41,7 @@ SELECT token_family_key_serial IF NOT FOUND THEN out_no_family = TRUE; - out_existed = FALSE; + out_conflict = FALSE; return; END IF; @@ -49,6 +49,8 @@ out_no_family = FALSE; my_tfk_serial = my_rec.token_family_key_serial; my_tf_serial = my_rec.token_family_serial; +-- This will fail due to the UNIQUE constrained on 'token_pub' +-- if a client attempts double-spending. INSERT INTO merchant_used_tokens (token_family_key_serial ,h_contract_terms @@ -65,10 +67,19 @@ INSERT INTO merchant_used_tokens IF NOT FOUND THEN - out_existed = TRUE; + -- Double spending or idempotent? check! + PERFORM FROM merchant_used_tokens + WHERE token_family_key_serial=my_tfk_serial + AND h_contract_terms=in_h_contract_terms + AND token_pub=in_use_pub + AND token_sig=in_use_sig + AND blind_sig=in_issue_sig; + -- if FOUND, we are idempotent and it is OK; + -- if NOT FOUND, someone tries to double-spend the token + out_conflict = NOT FOUND; return; END IF; -out_existed = FALSE; +out_conflict = FALSE; UPDATE merchant_token_families SET used=used+1