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