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:
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