merchant

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

commit d52b27c3fd88729312aef29b0850d59de94535fa
parent 925507d139091fec722820faf09971ab4f76e014
Author: Christian Grothoff <christian@grothoff.org>
Date:   Wed,  5 Aug 2026 23:16:30 +0200

deleted token family should yield 404 on attempt to pay, not 500

Diffstat:
Msrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c | 38++++++++++++++++++++++++++++++++++----
Msrc/backenddb/insert_issued_token.c | 10+++++++---
Msrc/backenddb/insert_used_token.c | 11+++++++----
Msrc/backenddb/test_merchantdb.c | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/include/merchant-database/insert_issued_token.h | 8++++++--
Msrc/include/merchant-database/insert_used_token.h | 8++++++--
6 files changed, 117 insertions(+), 25 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 @@ -3036,6 +3036,7 @@ phase_execute_pay_transaction (struct PayContext *pc) { struct TokenUseConfirmation *tuc = &pc->parse_pay.tokens[i]; enum GNUNET_DB_QueryStatus qs; + bool no_family; /* Insert used token into database, the unique constraint will case an error if this token was used before. */ @@ -3044,7 +3045,8 @@ phase_execute_pay_transaction (struct PayContext *pc) &tuc->h_issue, &tuc->pub, &tuc->sig, - &tuc->unblinded_sig); + &tuc->unblinded_sig, + &no_family); switch (qs) { @@ -3061,8 +3063,21 @@ phase_execute_pay_transaction (struct PayContext *pc) "insert used token")); return; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* UNIQUE constraint violation, meaning this token was already used. */ TALER_MERCHANTDB_rollback (TMH_db); + if (no_family) + { + /* The token family key was deleted after the order was created, + so we cannot accept this token anymore. */ + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_TOKEN_KEY_UNKNOWN, + NULL)); + return; + } + /* UNIQUE constraint violation, meaning this token was already used. */ pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_CONFLICT, @@ -3269,6 +3284,7 @@ phase_execute_pay_transaction (struct PayContext *pc) struct SignedOutputToken *output = &pc->output_tokens[i]; enum GNUNET_DB_QueryStatus qs; + bool no_family; if (NULL == output->sig.signature) continue; /* must have been optional and not provided by wallet */ @@ -3276,7 +3292,8 @@ phase_execute_pay_transaction (struct PayContext *pc) TMH_db, &pc->check_contract.h_contract_terms, &output->h_issue, - &output->sig); + &output->sig, + &no_family); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: @@ -3294,8 +3311,21 @@ phase_execute_pay_transaction (struct PayContext *pc) TALER_MERCHANTDB_rollback (TMH_db); return; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* UNIQUE constraint violation, meaning this token was already used. */ TALER_MERCHANTDB_rollback (TMH_db); + if (no_family) + { + /* The token family key was deleted after the order was + created, so we cannot issue this token anymore. */ + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_TOKEN_KEY_UNKNOWN, + NULL)); + return; + } + /* UNIQUE constraint violation, meaning this token was already used. */ pay_end (pc, TALER_MHD_reply_with_error ( pc->connection, diff --git a/src/backenddb/insert_issued_token.c b/src/backenddb/insert_issued_token.c @@ -29,7 +29,8 @@ TALER_MERCHANTDB_insert_issued_token ( struct TALER_MERCHANTDB_PostgresContext *pg, const struct TALER_PrivateContractHashP *h_contract_terms, const struct TALER_TokenIssuePublicKeyHashP *h_issue_pub, - const struct TALER_BlindedTokenIssueSignature *blind_sig) + const struct TALER_BlindedTokenIssueSignature *blind_sig, + bool *no_family) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (h_issue_pub), @@ -60,12 +61,15 @@ TALER_MERCHANTDB_insert_issued_token ( "", params, rs); + *no_family = false; if (qs < 0) return qs; if (no_fam) { - GNUNET_break (0); - return GNUNET_DB_STATUS_HARD_ERROR; + /* The token family (key) was deleted after the order was created; + this is a client-visible conflict, not an internal failure. */ + *no_family = true; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; } if (existed) return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; diff --git a/src/backenddb/insert_used_token.c b/src/backenddb/insert_used_token.c @@ -31,7 +31,8 @@ TALER_MERCHANTDB_insert_used_token ( const struct TALER_TokenIssuePublicKeyHashP *h_issue_pub, const struct TALER_TokenUsePublicKeyP *use_pub, const struct TALER_TokenUseSignatureP *use_sig, - const struct TALER_TokenIssueSignature *issue_sig) + const struct TALER_TokenIssueSignature *issue_sig, + bool *no_family) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (h_contract_terms), @@ -67,13 +68,15 @@ TALER_MERCHANTDB_insert_used_token ( "", params, rs); + *no_family = false; if (qs < 0) return qs; - // FIXME: return specific errors to caller if (no_fam) { - GNUNET_break (0); - return GNUNET_DB_STATUS_HARD_ERROR; + /* The token family (key) was deleted after the order was created; + this is a client-visible conflict, not an internal failure. */ + *no_family = true; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; } if (conflict) { diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c @@ -67,6 +67,7 @@ #include "merchant-database/insert_token_family.h" #include "merchant-database/insert_token_family_key.h" #include "merchant-database/get_token_family.h" +#include "merchant-database/delete_token_family.h" #include "merchant-database/get_account_serial.h" #include "merchant-database/get_contract_terms.h" #include "merchant-database/get_contract_terms_status.h" @@ -2227,14 +2228,19 @@ test_token_family_counters (const struct TestTokens_Closure *cls, * @param cls the test data. * @param blind_sig the blinded signature of the issued token. * @param expected_result the result we expect the db to return. + * @param expect_no_family whether we expect the token family key to be + * reported as unknown. * @return 0 when successful, 1 otherwise. */ static int test_insert_issued_token (const struct TestTokens_Closure *cls, const struct TALER_BlindedTokenIssueSignature * blind_sig, - enum GNUNET_DB_QueryStatus expected_result) + enum GNUNET_DB_QueryStatus expected_result, + bool expect_no_family) { + bool no_family; + TEST_SET_INSTANCE (cls->instance.instance.id, expected_result); TEST_COND_RET_ON_FAIL ( @@ -2242,8 +2248,11 @@ test_insert_issued_token (const struct TestTokens_Closure *cls, TALER_MERCHANTDB_insert_issued_token (pg, &cls->h_contract_terms, &cls->h_pub, - blind_sig), + blind_sig, + &no_family), "Insert issued token returned unexpected status\n"); + TEST_COND_RET_ON_FAIL (expect_no_family == no_family, + "Insert issued token 'no_family' mismatch\n"); return 0; } @@ -2256,6 +2265,8 @@ test_insert_issued_token (const struct TestTokens_Closure *cls, * @param use_sig signature made with the token use key. * @param issue_sig signature of the merchant over the token. * @param expected_result the result we expect the db to return. + * @param expect_no_family whether we expect the token family key to be + * reported as unknown. * @return 0 when successful, 1 otherwise. */ static int @@ -2263,8 +2274,11 @@ test_insert_used_token (const struct TestTokens_Closure *cls, const struct TALER_TokenUsePublicKeyP *use_pub, const struct TALER_TokenUseSignatureP *use_sig, const struct TALER_TokenIssueSignature *issue_sig, - enum GNUNET_DB_QueryStatus expected_result) + enum GNUNET_DB_QueryStatus expected_result, + bool expect_no_family) { + bool no_family; + TEST_SET_INSTANCE (cls->instance.instance.id, expected_result); TEST_COND_RET_ON_FAIL ( @@ -2274,8 +2288,11 @@ test_insert_used_token (const struct TestTokens_Closure *cls, &cls->h_pub, use_pub, use_sig, - issue_sig), + issue_sig, + &no_family), "Insert used token returned unexpected status\n"); + TEST_COND_RET_ON_FAIL (expect_no_family == no_family, + "Insert used token 'no_family' mismatch\n"); return 0; } @@ -2392,7 +2409,8 @@ run_test_tokens (struct TestTokens_Closure *cls) TEST_RET_ON_FAIL (test_insert_issued_token ( cls, &blind_sig1, - GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false)); TEST_RET_ON_FAIL (test_token_family_counters (cls, 1, 0)); @@ -2401,7 +2419,8 @@ run_test_tokens (struct TestTokens_Closure *cls) TEST_RET_ON_FAIL (test_insert_issued_token ( cls, &blind_sig1, - GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); + GNUNET_DB_STATUS_SUCCESS_NO_RESULTS, + false)); TEST_RET_ON_FAIL (test_token_family_counters (cls, 1, 0)); @@ -2409,7 +2428,8 @@ run_test_tokens (struct TestTokens_Closure *cls) TEST_RET_ON_FAIL (test_insert_issued_token ( cls, &blind_sig2, - GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false)); TEST_RET_ON_FAIL (test_token_family_counters (cls, 2, 0)); @@ -2419,7 +2439,8 @@ run_test_tokens (struct TestTokens_Closure *cls) &use_pub, &use_sig, &issue_sig, - GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false)); TEST_RET_ON_FAIL (test_token_family_counters (cls, 2, 1)); @@ -2430,7 +2451,8 @@ run_test_tokens (struct TestTokens_Closure *cls) &use_pub, &use_sig, &issue_sig, - GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false)); TEST_RET_ON_FAIL (test_token_family_counters (cls, 2, 1)); @@ -2441,10 +2463,35 @@ run_test_tokens (struct TestTokens_Closure *cls) &use_pub, &other_sig, &issue_sig, - GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); + GNUNET_DB_STATUS_SUCCESS_NO_RESULTS, + false)); TEST_RET_ON_FAIL (test_token_family_counters (cls, 2, 1)); + /* Deleting the token family cascades to the token family keys. An + order created before the deletion may still be paid; the missing + key must then be reported to the caller (which turns it into a 404 + TOKEN_KEY_UNKNOWN) instead of being an internal hard error. */ + TEST_SET_INSTANCE (cls->instance.instance.id, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); + TEST_COND_RET_ON_FAIL ( + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_delete_token_family (pg, + cls->instance.instance.id, + cls->family.slug), + "Delete token family failed\n"); + TEST_RET_ON_FAIL (test_insert_issued_token ( + cls, + &blind_sig1, + GNUNET_DB_STATUS_SUCCESS_NO_RESULTS, + true)); + TEST_RET_ON_FAIL (test_insert_used_token ( + cls, + &use_pub, + &use_sig, + &issue_sig, + GNUNET_DB_STATUS_SUCCESS_NO_RESULTS, + true)); return 0; } diff --git a/src/include/merchant-database/insert_issued_token.h b/src/include/merchant-database/insert_issued_token.h @@ -32,7 +32,10 @@ struct TALER_MERCHANTDB_PostgresContext; * @param h_contract_terms hash of the contract the token was issued for * @param h_issue_pub hash of the token issue public key used to sign the issued token * @param blind_sig resulting blind token issue signature - * @return database result code + * @param[out] no_family set to true if the token family key is unknown, + * in which case #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS is returned + * @return database result code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if + * the token was already issued or @a no_family is set */ enum GNUNET_DB_QueryStatus TALER_MERCHANTDB_insert_issued_token (struct TALER_MERCHANTDB_PostgresContext *pg, @@ -41,6 +44,7 @@ TALER_MERCHANTDB_insert_issued_token (struct TALER_MERCHANTDB_PostgresContext *p const struct TALER_TokenIssuePublicKeyHashP * h_issue_pub, const struct TALER_BlindedTokenIssueSignature * - blind_sig); + blind_sig, + bool *no_family); #endif diff --git a/src/include/merchant-database/insert_used_token.h b/src/include/merchant-database/insert_used_token.h @@ -34,7 +34,10 @@ struct TALER_MERCHANTDB_PostgresContext; * @param use_pub token use public key * @param use_sig token use signature * @param issue_sig token issue signature - * @return database result code + * @param[out] no_family set to true if the token family key is unknown, + * in which case #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS is returned + * @return database result code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS on + * double-spending or if @a no_family is set */ enum GNUNET_DB_QueryStatus TALER_MERCHANTDB_insert_used_token (struct TALER_MERCHANTDB_PostgresContext *pg, @@ -42,6 +45,7 @@ TALER_MERCHANTDB_insert_used_token (struct TALER_MERCHANTDB_PostgresContext *pg, const struct TALER_TokenIssuePublicKeyHashP *h_issue_pub, const struct TALER_TokenUsePublicKeyP *use_pub, const struct TALER_TokenUseSignatureP *use_sig, - const struct TALER_TokenIssueSignature *issue_sig); + const struct TALER_TokenIssueSignature *issue_sig, + bool *no_family); #endif