commit 0da2155fc1017a211eb4d33cc5a0bc550c41d8b4
parent b82f0ef5b50227008e319785c616194ab3497b8e
Author: Christian Grothoff <christian@grothoff.org>
Date: Fri, 7 Aug 2026 00:21:59 +0200
do not return 201 if we had a conflict
Diffstat:
5 files changed, 181 insertions(+), 7 deletions(-)
diff --git a/src/donau/donau-httpd_get-config.h b/src/donau/donau-httpd_get-config.h
@@ -41,7 +41,12 @@
*
* Returned via both /config and /keys endpoints.
*/
-#define DONAU_PROTOCOL_VERSION "1:0:1"
+/* Revision bumped from 0 to 1: POST /batch-submit now answers 409
+ DONAU_DONOR_IDENTIFIER_NONCE_REUSE for receipts it cannot store,
+ where it used to answer 201 Created and drop them. `current' is
+ unchanged: no request or response schema changed, and a client that
+ never reuses a nonce sees no difference. */
+#define DONAU_PROTOCOL_VERSION "1:1:1"
/**
diff --git a/src/donau/donau-httpd_post-batch-submit.c b/src/donau/donau-httpd_post-batch-submit.c
@@ -246,6 +246,7 @@ DH_handler_post_batch_submit (struct DH_RequestContext *rc,
{
enum GNUNET_DB_QueryStatus qs;
+ size_t conflict_index = num_dr;
/* Concurrent submissions can serialize against each other; the
database reports that as a soft error, which means "retry me". */
@@ -257,7 +258,8 @@ DH_handler_post_batch_submit (struct DH_RequestContext *rc,
&irc.h_donor_tax_id,
num_dr,
irc.donation_receipts,
- irc.donation_year);
+ irc.donation_year,
+ &conflict_index);
if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
break;
}
@@ -275,9 +277,24 @@ DH_handler_post_batch_submit (struct DH_RequestContext *rc,
TALER_EC_GENERIC_DB_STORE_FAILED,
"insert_receipts_submitted");
}
+ /* `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
+ it silently loses a tax-relevant donation. */
+ if (conflict_index < num_dr)
+ {
+ char idx_str[24];
+
+ GNUNET_snprintf (idx_str,
+ sizeof (idx_str),
+ "%llu",
+ (unsigned long long) conflict_index);
+ return TALER_MHD_reply_with_error (rc->connection,
+ MHD_HTTP_CONFLICT,
+ TALER_EC_DONAU_DONOR_IDENTIFIER_NONCE_REUSE,
+ idx_str);
+ }
}
- /* Note: we do not care here about conflicts on insert,
- as conflicts only happen for idempotent requests. */
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"submitted receipts inserted!\n");
diff --git a/src/donaudb/insert_receipts_submitted.c b/src/donaudb/insert_receipts_submitted.c
@@ -34,7 +34,8 @@ DONAUDB_insert_receipts_submitted (
struct DONAU_HashDonorTaxId *h_donor_tax_id,
size_t num_dr,
const struct DONAU_DonationReceipt donation_receipts[static num_dr],
- uint64_t donation_year)
+ uint64_t donation_year,
+ size_t *conflict_index)
{
struct GNUNET_HashCode h_donation_unit_pubs[GNUNET_NZL (num_dr)];
struct DONAU_UniqueDonorIdentifierNonce nonces[GNUNET_NZL (num_dr)];
@@ -63,6 +64,7 @@ DONAUDB_insert_receipts_submitted (
};
enum GNUNET_DB_QueryStatus qs;
+ *conflict_index = num_dr;
for (unsigned int i = 0; i < num_dr; i++)
{
const struct DONAU_DonationReceipt *dr = &donation_receipts[i];
@@ -89,11 +91,16 @@ DONAUDB_insert_receipts_submitted (
{
for (size_t i = 0; i < num_dr; 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
+ donor's. That receipt was dropped and the caller must be told. */
if (conflicted[i])
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Submitted donation receipt at index %llu already present!\n",
+ "Submitted donation receipt at index %llu conflicts with an existing entry!\n",
(unsigned long long) i);
+ if (num_dr == *conflict_index)
+ *conflict_index = i;
}
}
}
diff --git a/src/donaudb/test_donaudb.c b/src/donaudb/test_donaudb.c
@@ -149,6 +149,78 @@ donation_unit_info_cb (
/**
+ * Create an unblinded donation unit signature. The database layer only
+ * serializes it, so any well-formed signature will do.
+ *
+ * @return freshly allocated unblinded signature
+ */
+static struct GNUNET_CRYPTO_UnblindedSignature *
+make_unblinded_sig (void)
+{
+ struct GNUNET_CRYPTO_UnblindedSignature *us;
+ struct GNUNET_HashCode msg;
+
+ if (NULL == test_denom_priv.bsign_priv_key)
+ GNUNET_assert (GNUNET_OK ==
+ TALER_denom_priv_create (&test_denom_priv,
+ &test_denom_pub,
+ GNUNET_CRYPTO_BSA_RSA,
+ RSA_KEY_SIZE));
+ RND_BLK (&msg);
+ us = GNUNET_new (struct GNUNET_CRYPTO_UnblindedSignature);
+ us->cipher = GNUNET_CRYPTO_BSA_RSA;
+ us->rc = 1;
+ us->details.rsa_signature
+ = GNUNET_CRYPTO_rsa_sign_fdh (
+ test_denom_priv.bsign_priv_key->details.rsa_private_key,
+ &msg,
+ sizeof (msg));
+ GNUNET_assert (NULL != us->details.rsa_signature);
+ return us;
+}
+
+
+/**
+ * Register a donation unit with a random key hash.
+ *
+ * @param[out] h_donation_unit_pub set to the hash identifying the unit
+ * @param year validity year of the unit
+ * @param value value of the unit
+ * @return database status of the operation
+ */
+static enum GNUNET_DB_QueryStatus
+make_donation_unit (struct DONAU_DonationUnitHashP *h_donation_unit_pub,
+ uint64_t year,
+ const char *value)
+{
+ struct TALER_DenominationPrivateKey dpriv;
+ struct TALER_DenominationPublicKey dpub;
+ struct DONAU_DonationUnitPublicKey du_pub;
+ struct TALER_Amount val;
+ enum GNUNET_DB_QueryStatus qs;
+
+ RND_BLK (h_donation_unit_pub);
+ GNUNET_assert (GNUNET_OK ==
+ TALER_denom_priv_create (&dpriv,
+ &dpub,
+ GNUNET_CRYPTO_BSA_RSA,
+ RSA_KEY_SIZE));
+ du_pub.bsign_pub_key = dpub.bsign_pub_key;
+ GNUNET_assert (GNUNET_OK ==
+ TALER_string_to_amount (value,
+ &val));
+ qs = DONAUDB_insert_donation_unit (ctx,
+ h_donation_unit_pub,
+ &du_pub,
+ year,
+ &val);
+ TALER_denom_priv_free (&dpriv);
+ TALER_denom_pub_free (&dpub);
+ return qs;
+}
+
+
+/**
* Function called with information about the donau's online signing keys.
*
* @param cls NULL
@@ -678,6 +750,73 @@ run (void *cls)
GNUNET_free (second_meta.charity_url);
}
+ /* D-7: `receipts_submitted.nonce' is globally unique, so a donor who
+ reuses one UDI nonce for a *different* donation unit in a second
+ request has that receipt silently dropped. The DB layer must tell
+ the caller which index was refused, so /batch-submit can answer 409
+ instead of 201. */
+ {
+ struct DONAU_HashDonorTaxId donor;
+ struct DONAU_DonationUnitHashP du_a;
+ struct DONAU_DonationUnitHashP du_b;
+ struct DONAU_DonationReceipt dr;
+ struct DONAU_UniqueDonorIdentifierNonce nonce;
+ struct TALER_Amount total;
+ struct TALER_Amount expected;
+ size_t conflict_index = 42;
+ uint64_t year = GNUNET_TIME_get_current_year ();
+
+ RND_BLK (&donor);
+ RND_BLK (&nonce);
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ make_donation_unit (&du_a,
+ year,
+ CURRENCY ":10"));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ make_donation_unit (&du_b,
+ year,
+ CURRENCY ":25"));
+
+ dr.h_donation_unit_pub = du_a;
+ dr.nonce = nonce;
+ dr.donation_unit_sig.unblinded_sig = make_unblinded_sig ();
+ FAILIF (0 >
+ DONAUDB_insert_receipts_submitted (ctx,
+ &donor,
+ 1,
+ &dr,
+ year,
+ &conflict_index));
+ FAILIF (1 != conflict_index); /* == num_dr, i.e. no conflict */
+ GNUNET_CRYPTO_unblinded_sig_decref (dr.donation_unit_sig.unblinded_sig);
+
+ /* Same donor, same nonce, different donation unit: refused. */
+ dr.h_donation_unit_pub = du_b;
+ dr.nonce = nonce;
+ dr.donation_unit_sig.unblinded_sig = make_unblinded_sig ();
+ FAILIF (0 >
+ DONAUDB_insert_receipts_submitted (ctx,
+ &donor,
+ 1,
+ &dr,
+ year,
+ &conflict_index));
+ FAILIF (0 != conflict_index);
+ GNUNET_CRYPTO_unblinded_sig_decref (dr.donation_unit_sig.unblinded_sig);
+
+ /* Only the first receipt is deductible; the second one was dropped. */
+ FAILIF (0 >
+ DONAUDB_get_receipts_submitted_total (ctx,
+ year,
+ &donor,
+ &total));
+ GNUNET_assert (GNUNET_OK ==
+ TALER_string_to_amount (CURRENCY ":10",
+ &expected));
+ FAILIF (0 != TALER_amount_cmp (&total,
+ &expected));
+ }
+
result = 0;
drop:
diff --git a/src/include/donau-database/insert_receipts_submitted.h b/src/include/donau-database/insert_receipts_submitted.h
@@ -32,6 +32,11 @@
* @param num_dr number of donation receipts
* @param donation_receipts array of donation receipts
* @param donation_year year of the donation
+ * @param[out] conflict_index set to the index of the first receipt that
+ * could NOT be stored because a *different* receipt with the
+ * same nonce already exists, or to @a num_dr if all receipts
+ * were stored (or were idempotent repetitions of this donor's
+ * own receipts)
* @return transaction status code
*/
enum GNUNET_DB_QueryStatus
@@ -40,6 +45,7 @@ DONAUDB_insert_receipts_submitted (
struct DONAU_HashDonorTaxId *h_donor_tax_id,
size_t num_dr,
const struct DONAU_DonationReceipt donation_receipts[static num_dr],
- uint64_t donation_year);
+ uint64_t donation_year,
+ size_t *conflict_index);
#endif