donau

Donation authority for GNU Taler (experimental)
Log | Files | Refs | Submodules | README | LICENSE

commit acd6092a1acc43f3efbf6cf0a539341ee933baed
parent 208d112a6ec33adebc7ab6b08a0b400fb5314bc8
Author: Christian Grothoff <christian@grothoff.org>
Date:   Fri,  7 Aug 2026 00:26:43 +0200

unknown key is 404, not 500

Diffstat:
Msrc/donau/donau-httpd_post-batch-submit.c | 20+++++++++++++++++++-
Msrc/donaudb/insert_receipts_submitted.c | 23++++++++++++++++++++---
Msrc/donaudb/insert_receipts_submitted.sql | 33++++++++++++++++++++++++++++++---
Msrc/donaudb/test_donaudb.c | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/include/donau-database/insert_receipts_submitted.h | 7++++++-
5 files changed, 133 insertions(+), 10 deletions(-)

diff --git a/src/donau/donau-httpd_post-batch-submit.c b/src/donau/donau-httpd_post-batch-submit.c @@ -247,6 +247,7 @@ DH_handler_post_batch_submit (struct DH_RequestContext *rc, { enum GNUNET_DB_QueryStatus qs; size_t conflict_index = num_dr; + size_t unknown_du_index = num_dr; /* Concurrent submissions can serialize against each other; the database reports that as a soft error, which means "retry me". */ @@ -259,7 +260,8 @@ DH_handler_post_batch_submit (struct DH_RequestContext *rc, num_dr, irc.donation_receipts, irc.donation_year, - &conflict_index); + &conflict_index, + &unknown_du_index); if (GNUNET_DB_STATUS_SOFT_ERROR != qs) break; } @@ -277,6 +279,22 @@ DH_handler_post_batch_submit (struct DH_RequestContext *rc, TALER_EC_GENERIC_DB_STORE_FAILED, "insert_receipts_submitted"); } + /* The handler pre-checks h_donation_unit_pub against the *in-memory* + key map; if that map and the donation_units table disagree, the + insert hits a foreign key violation. That is a 404, not a 500. */ + if (unknown_du_index < num_dr) + { + char idx_str[24]; + + GNUNET_snprintf (idx_str, + sizeof (idx_str), + "%llu", + (unsigned long long) unknown_du_index); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_DONAU_GENERIC_DONATION_UNIT_UNKNOWN, + idx_str); + } /* `receipts_submitted.nonce' is globally unique, so a nonce this donor already used for a *different* donation unit is refused by the database. That receipt was NOT stored -- reporting 201 Created for diff --git a/src/donaudb/insert_receipts_submitted.c b/src/donaudb/insert_receipts_submitted.c @@ -35,7 +35,8 @@ DONAUDB_insert_receipts_submitted ( size_t num_dr, const struct DONAU_DonationReceipt donation_receipts[static num_dr], uint64_t donation_year, - size_t *conflict_index) + size_t *conflict_index, + size_t *unknown_du_index) { struct GNUNET_HashCode h_donation_unit_pubs[GNUNET_NZL (num_dr)]; struct DONAU_UniqueDonorIdentifierNonce nonces[GNUNET_NZL (num_dr)]; @@ -55,16 +56,21 @@ DONAUDB_insert_receipts_submitted ( GNUNET_PQ_query_param_end }; bool *conflicted = NULL; + size_t num_conflicted = num_dr; + uint32_t unknown_du; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_array_bool (ctx->conn, "conflicted", - &num_dr, + &num_conflicted, &conflicted), + GNUNET_PQ_result_spec_uint32 ("unknown_du", + &unknown_du), GNUNET_PQ_result_spec_end }; enum GNUNET_DB_QueryStatus qs; *conflict_index = num_dr; + *unknown_du_index = num_dr; for (unsigned int i = 0; i < num_dr; i++) { const struct DONAU_DonationReceipt *dr = &donation_receipts[i]; @@ -80,6 +86,7 @@ DONAUDB_insert_receipts_submitted ( "insert_receipts_submitted", "SELECT " " out_conflict AS conflicted" + ",out_unknown_du AS unknown_du" " FROM do_insert_submitted_receipts" "($1,$2,$3,$4,$5);"); qs = GNUNET_PQ_eval_prepared_singleton_select (ctx->conn, @@ -89,7 +96,17 @@ DONAUDB_insert_receipts_submitted ( GNUNET_PQ_cleanup_query_params_closures (params); if (qs > 0) { - for (size_t i = 0; i < num_dr; i++) + if (0 != unknown_du) + { + /* Nothing was written; the batch names a donation unit we do not + know. Report it instead of letting the foreign key violation + escape as a hard error. */ + *unknown_du_index = (size_t) (unknown_du - 1); + GNUNET_free (conflicted); + GNUNET_PQ_cleanup_result (rs); + return qs; + } + for (size_t i = 0; i < num_conflicted; i++) { /* NOTE: `conflicted[i]' is TRUE only for the *non*-idempotent case, i.e. the row was not inserted AND the stored row is not this diff --git a/src/donaudb/insert_receipts_submitted.sql b/src/donaudb/insert_receipts_submitted.sql @@ -22,7 +22,8 @@ CREATE FUNCTION do_insert_submitted_receipts( IN ina_donation_unit_sigs BYTEA[], IN in_donation_year INT8, -- - OUT out_conflict BOOL[] + OUT out_conflict BOOL[], + OUT out_unknown_du INT4 ) LANGUAGE plpgsql AS $$ @@ -33,6 +34,34 @@ DECLARE ini_donation_unit_sig BYTEA; BEGIN +-- First pass: h_donation_unit_pub is client-supplied and is a foreign key +-- into donation_units. 'ON CONFLICT DO NOTHING' below absorbs unique +-- violations only, so an unknown donation unit would escape as SQLSTATE +-- 23503 -> HARD_ERROR -> HTTP 500, where 404 belongs. Reject the whole +-- batch up front, before anything has been written. + +-- out_unknown_du is 0 if every unit is known, otherwise the 1-based index +-- of the first unknown one. +out_unknown_du = 0; +FOR i IN 1..array_length(ina_h_donation_unit_pubs,1) +LOOP + out_conflict[i] = FALSE; + IF (out_unknown_du = 0) + THEN + PERFORM FROM donation_units + WHERE h_donation_unit_pub=ina_h_donation_unit_pubs[i]; + IF NOT FOUND + THEN + out_unknown_du = i; + END IF; + END IF; +END LOOP; + +IF (out_unknown_du > 0) +THEN + RETURN; +END IF; + -- Insert each donation receipt FOR i IN 1..array_length(ina_h_donation_unit_pubs,1) @@ -41,8 +70,6 @@ LOOP ini_h_donation_unit_pub = ina_h_donation_unit_pubs[i]; ini_donation_unit_sig = ina_donation_unit_sigs[i]; - out_conflict[i] = FALSE; - INSERT INTO receipts_submitted (h_tax_number ,nonce diff --git a/src/donaudb/test_donaudb.c b/src/donaudb/test_donaudb.c @@ -764,6 +764,7 @@ run (void *cls) struct TALER_Amount total; struct TALER_Amount expected; size_t conflict_index = 42; + size_t unknown_index = 42; uint64_t year = GNUNET_TIME_get_current_year (); RND_BLK (&donor); @@ -786,7 +787,8 @@ run (void *cls) 1, &dr, year, - &conflict_index)); + &conflict_index, + &unknown_index)); FAILIF (1 != conflict_index); /* == num_dr, i.e. no conflict */ GNUNET_CRYPTO_unblinded_sig_decref (dr.donation_unit_sig.unblinded_sig); @@ -800,7 +802,8 @@ run (void *cls) 1, &dr, year, - &conflict_index)); + &conflict_index, + &unknown_index)); FAILIF (0 != conflict_index); GNUNET_CRYPTO_unblinded_sig_decref (dr.donation_unit_sig.unblinded_sig); @@ -817,6 +820,59 @@ run (void *cls) &expected)); } + /* D-10: h_donation_unit_pub is client-supplied and is a foreign key. + ON CONFLICT DO NOTHING does not absorb foreign key violations, so an + unknown unit used to escape as a hard error (HTTP 500). It must be + reported as "unknown donation unit at index i", and nothing at all + may be written for that batch. */ + { + struct DONAU_HashDonorTaxId donor; + struct DONAU_DonationUnitHashP known_du; + struct DONAU_DonationReceipt drs[2]; + struct TALER_Amount total; + struct TALER_Amount zero; + size_t conflict_index = 42; + size_t unknown_index = 42; + uint64_t year = GNUNET_TIME_get_current_year (); + + RND_BLK (&donor); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + make_donation_unit (&known_du, + year, + CURRENCY ":10")); + drs[0].h_donation_unit_pub = known_du; + RND_BLK (&drs[0].nonce); + drs[0].donation_unit_sig.unblinded_sig = make_unblinded_sig (); + /* Second receipt names a donation unit that was never registered. */ + RND_BLK (&drs[1].h_donation_unit_pub); + RND_BLK (&drs[1].nonce); + drs[1].donation_unit_sig.unblinded_sig = make_unblinded_sig (); + + FAILIF (0 > + DONAUDB_insert_receipts_submitted (ctx, + &donor, + 2, + drs, + year, + &conflict_index, + &unknown_index)); + FAILIF (1 != unknown_index); + GNUNET_CRYPTO_unblinded_sig_decref (drs[0].donation_unit_sig.unblinded_sig); + GNUNET_CRYPTO_unblinded_sig_decref (drs[1].donation_unit_sig.unblinded_sig); + + /* The valid receipt in the same batch must NOT have been stored. */ + FAILIF (0 > + DONAUDB_get_receipts_submitted_total (ctx, + year, + &donor, + &total)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (CURRENCY, + &zero)); + FAILIF (0 != TALER_amount_cmp (&total, + &zero)); + } + result = 0; drop: diff --git a/src/include/donau-database/insert_receipts_submitted.h b/src/include/donau-database/insert_receipts_submitted.h @@ -37,6 +37,10 @@ * same nonce already exists, or to @a num_dr if all receipts * were stored (or were idempotent repetitions of this donor's * own receipts) + * @param[out] unknown_du_index set to the index of the first receipt whose + * @e h_donation_unit_pub is not in the donation_units table, or + * to @a num_dr if all units are known; if this is not @a num_dr + * then *nothing* was written and @a conflict_index is unset * @return transaction status code */ enum GNUNET_DB_QueryStatus @@ -46,6 +50,7 @@ DONAUDB_insert_receipts_submitted ( size_t num_dr, const struct DONAU_DonationReceipt donation_receipts[static num_dr], uint64_t donation_year, - size_t *conflict_index); + size_t *conflict_index, + size_t *unknown_du_index); #endif