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