commit 438402f33dd096c3a6209b2fe4ed808eea6a3930
parent df2ab968fb572457c40ce84d2323bb5272dbcdba
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date: Fri, 8 Aug 2025 01:15:15 +0200
somewhat way to select donation_units for the issue receipts for specific amount
Diffstat:
6 files changed, 253 insertions(+), 9 deletions(-)
diff --git a/src/include/donau_service.h b/src/include/donau_service.h
@@ -412,6 +412,27 @@ DONAU_get_donation_unit_key (
/**
+ * Greedily build a multiset of donation-unit public keys that sums EXACTLY to
+ * @a requested_amount, using donation units from @a keys for the given @a year.
+ *
+ * @param keys Donau keys (must match requested_amount currency)
+ * @param requested_amount target amount
+ * @param year only consider donation units for this year
+ * @param[out] out_keys array of selected public keys (owned by caller)
+ * @param[out] out_len length of @a out_keys
+ * @return GNUNET_OK on exact match; GNUNET_NO if exact match not possible;
+ * GNUNET_SYSERR on invalid input/currency mismatch.
+ */
+enum GNUNET_GenericReturnValue
+DONAU_select_donation_unit_keys_for_amount (
+ const struct DONAU_Keys *keys,
+ const struct TALER_Amount *requested_amount,
+ uint64_t year,
+ struct DONAU_DonationUnitPublicKey **out_keys,
+ uint32_t *out_len);
+
+
+/**
* Obtain the donation unit key details from the donau.
*
* @param keys the donau's key set
diff --git a/src/include/donau_testing_lib.h b/src/include/donau_testing_lib.h
@@ -131,6 +131,7 @@ TALER_TESTING_cmd_issue_receipts (const char *label,
const uint64_t year,
const char *donor_tax_id,
const char *salt,
+ const char *issue_amount,
unsigned int expected_response_code);
diff --git a/src/lib/donau_api_handle.c b/src/lib/donau_api_handle.c
@@ -541,7 +541,178 @@ DONAU_get_donation_unit_key_by_hash (
}
+/**
+ * Local helper for the #DONAU_select_donation_unit_keys_for_amount()
+ */
+struct DUEntry
+{
+ struct TALER_Amount value;
+ const struct DONAU_DonationUnitPublicKey *pub; /* points into keys */
+};
+
+/**
+ * Small helper function for sorting
+ */
+static int
+du_amount_desc_cmp (const void *a, const void *b)
+{
+ const struct DUEntry *ea = a;
+ const struct DUEntry *eb = b;
+ int c = TALER_amount_cmp (&ea->value, &eb->value);
+ /* Descending */
+ return (c < 0) ? 1 : (c > 0 ? -1 : 0);
+}
+
+
+/**
+ * Greedily build a multiset of donation-unit public keys that sums EXACTLY to
+ * @a requested_amount, using donation units from @a keys for the given @a year.
+ *
+ * @param keys Donau keys (must match requested_amount currency)
+ * @param requested_amount target amount
+ * @param year only consider donation units for this year
+ * @param[out] out_keys array of selected public keys (owned by caller)
+ * @param[out] out_len length of @a out_keys
+ * @return GNUNET_OK on exact match; GNUNET_NO if exact match not possible;
+ * GNUNET_SYSERR on invalid input/currency mismatch.
+ */
+enum GNUNET_GenericReturnValue
+DONAU_select_donation_unit_keys_for_amount (
+ const struct DONAU_Keys *keys,
+ const struct TALER_Amount *requested_amount,
+ uint64_t year,
+ struct DONAU_DonationUnitPublicKey **out_keys,
+ uint32_t *out_len)
+{
+ struct DONAU_DonationUnitPublicKey *result = NULL;
+ uint32_t result_len = 0;
+ struct TALER_Amount remaining, zero;
+ struct DUEntry *duv = NULL;
+ unsigned int n_duv = 0;
+ unsigned int i = 0;
+
+ if ( (NULL == keys) ||
+ (NULL == requested_amount) ||
+ (NULL == out_keys) ||
+ (NULL == out_len) )
+ {
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+
+ if (0 != strcasecmp (keys->currency,
+ requested_amount->currency))
+ {
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+
+ remaining = *requested_amount;
+ TALER_amount_set_zero (keys->currency,
+ &zero);
+
+ if (0 == TALER_amount_cmp (&remaining,
+ &zero))
+ {
+ *out_keys = NULL;
+ *out_len = 0;
+ return GNUNET_OK;
+ }
+
+ /* Build and sort (desc) the eligible units */
+ for (unsigned int j = 0;
+ j < keys->num_donation_unit_keys;
+ j++)
+ {
+ const struct DONAU_DonationUnitInformation *du
+ = &keys->donation_unit_keys[j];
+
+ if (du->lost)
+ continue;
+ if (du->year != year)
+ continue;
+
+ GNUNET_array_grow (duv,
+ n_duv,
+ n_duv + 1);
+ duv[n_duv - 1].value = du->value;
+ duv[n_duv - 1].pub = &du->key;
+ }
+
+ if (0 == n_duv)
+ {
+ *out_keys = NULL;
+ *out_len = 0;
+ return GNUNET_NO;
+ }
+
+ qsort (duv,
+ n_duv,
+ sizeof(struct DUEntry),
+ &du_amount_desc_cmp);
+
+ while (i < n_duv)
+ {
+ int cmp = TALER_amount_cmp (&duv[i].value, &remaining);
+ if (cmp <= 0)
+ {
+ /* Take as many as we can of duv[i] without overshooting */
+ for (;;)
+ {
+ struct TALER_Amount tmp;
+ int rc;
+
+ if (TALER_amount_cmp (&duv[i].value, &remaining) > 0)
+ break;
+
+ GNUNET_array_append (result,
+ result_len,
+ *duv[i].pub);
+
+ rc = TALER_amount_subtract (&tmp,
+ &remaining,
+ &duv[i].value);
+
+ if (TALER_AAR_RESULT_ZERO == rc)
+ {
+ remaining = tmp; /* zero */
+ GNUNET_free (duv);
+ *out_keys = result;
+ *out_len = result_len;
+ return GNUNET_OK; /* exact match */
+ }
+ if (TALER_AAR_RESULT_POSITIVE == rc)
+ {
+ remaining = tmp; /* keep taking this same value */
+ continue;
+ }
+
+ /* Shouldn't happen because we guard with cmp <= 0 */
+ GNUNET_break (0);
+ GNUNET_free (duv);
+ GNUNET_array_grow (result, result_len, 0);
+ *out_keys = NULL;
+ *out_len = 0;
+ return GNUNET_SYSERR;
+ }
+ /* current no longer fits, move to next smaller */
+ i++;
+ continue;
+ }
+ i++;
+ }
+
+ /* No exact combination found */
+ GNUNET_free (duv);
+ GNUNET_array_grow (result, result_len, 0);
+ *out_keys = NULL;
+ *out_len = 0;
+ return GNUNET_NO;
+}
+
+
// FIXME: this API is bad *and* will leak memory!
+// put point to the amount and return GNUNET_GENERIC_return_value
const struct TALER_Amount *
DONAU_get_donation_amount_from_bkps (
const struct DONAU_Keys *keys,
diff --git a/src/testing/test_donau_api.c b/src/testing/test_donau_api.c
@@ -74,7 +74,7 @@ run (void *cls,
TALER_TESTING_cmd_charity_post ("post-charity",
"example",
"example.com",
- "EUR:5", // max_per_year
+ "EUR:10", // max_per_year
"EUR:0", // receipts_to_date
2025, // current year
&bearer,
@@ -93,6 +93,7 @@ run (void *cls,
2025,
"7560001010000", // tax id
"1234", // salt for tax id hash
+ "EUR:5", /* Amount to be issued*/
MHD_HTTP_CREATED),
TALER_TESTING_cmd_submit_receipts ("submit-receipts",
"issue-receipts", // cmd trait reference
diff --git a/src/testing/testing_api_cmd_charity_get.c b/src/testing/testing_api_cmd_charity_get.c
@@ -130,7 +130,7 @@ status_run (void *cls,
TALER_TESTING_interpreter_fail (is);
return;
}
- ss->charity_id = (uint64_t) *(charity_id);
+ ss->charity_id = *(charity_id);
}
ss->cgh = DONAU_charity_get (
diff --git a/src/testing/testing_api_cmd_issue_receipts.c b/src/testing/testing_api_cmd_issue_receipts.c
@@ -44,6 +44,11 @@ struct StatusState
const char *charity_reference;
/**
+ * Issue amount
+ */
+ struct TALER_Amount amount;
+
+ /**
* Expected HTTP response code.
*/
unsigned int expected_response_code;
@@ -75,7 +80,7 @@ struct StatusState
/**
* number of budi key pair.
*/
- size_t num_bkp;
+ uint32_t num_bkp;
/**
* budi key pair array
@@ -103,6 +108,11 @@ struct StatusState
struct DONAU_HashDonorTaxId h_donor_tax_id;
/**
+ * Selected donation-unit pubkeys for this request
+ */
+ struct DONAU_DonationUnitPublicKey *selected_pks;
+
+ /**
* Array of donation receipts;
*/
struct DONAU_DonationReceipt *receipts;
@@ -191,7 +201,7 @@ issue_receipts_status_cb (void *cls,
&ss->blinding_secrets[i],
&ss->h_udis[i],
ss->alg_values[i],
- &ss->keys->donation_unit_keys[0].key));
+ &ss->selected_pks[i]));
/* check udi message */
struct DONAU_UniqueDonorIdentifierHashP checkudi_hash;
@@ -203,7 +213,7 @@ issue_receipts_status_cb (void *cls,
&ss->h_udis[i].hash));
/* check signature */
if (GNUNET_OK != DONAU_donation_receipt_verify (
- &ss->keys->donation_unit_keys[0].key,
+ &ss->selected_pks[i],
&checkudi_hash,
&ss->receipts[i].donation_unit_sig))
{
@@ -374,6 +384,32 @@ status_run (void *cls,
ss->keys = keys;
}
+ /* Get the donation_unit_keys for requested amount */
+ {
+ enum GNUNET_GenericReturnValue sret;
+
+ sret = DONAU_select_donation_unit_keys_for_amount (
+ ss->keys,
+ &ss->amount,
+ ss->year,
+ &ss->selected_pks,
+ &ss->num_bkp);
+
+ if (GNUNET_SYSERR == sret)
+ {
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (is);
+ return;
+ }
+ if ((GNUNET_NO == sret) || (0 == ss->num_bkp))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Could not compose exact amount from donation units\n");
+ TALER_TESTING_interpreter_fail (is);
+ return;
+ }
+ }
+
ss->bkps =
GNUNET_new_array (ss->num_bkp, struct
DONAU_BlindedUniqueDonorIdentifierKeyPair);
@@ -387,7 +423,7 @@ status_run (void *cls,
GNUNET_new_array (ss->num_bkp, struct DONAU_UniqueDonorIdentifierHashP);
for (size_t cnt = 0; cnt < ss->num_bkp; cnt++)
{
- DONAU_donation_unit_pub_hash (&ss->keys->donation_unit_keys[0].key,
+ DONAU_donation_unit_pub_hash (&ss->selected_pks[cnt],
&ss->bkps[cnt].h_donation_unit_pub);
ss->receipts[cnt].h_donation_unit_pub = ss->bkps[cnt].h_donation_unit_pub;
struct DONAU_UniqueDonorIdentifierNonce *udi_nonce =
@@ -403,7 +439,7 @@ status_run (void *cls,
GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE,
udi_nonce,
sizeof (*udi_nonce));
- switch (ss->keys->donation_unit_keys[0].key.bsign_pub_key->cipher)
+ switch (ss->selected_pks[cnt].bsign_pub_key->cipher)
{
case GNUNET_CRYPTO_BSA_RSA:
alg_values = DONAU_donation_unit_ewv_rsa_singleton ();
@@ -412,7 +448,7 @@ status_run (void *cls,
&ss->blinding_secrets[cnt]);
GNUNET_assert (GNUNET_OK ==
DONAU_donation_unit_blind (
- &ss->keys->donation_unit_keys[0].key,
+ &ss->selected_pks[cnt],
&ss->blinding_secrets[cnt],
NULL, /* no cs-nonce needed for rsa */
udi_nonce,
@@ -432,7 +468,7 @@ status_run (void *cls,
csr_data->csr_handle = DONAU_csr_issue (
TALER_TESTING_interpreter_get_context (is),
TALER_TESTING_get_donau_url (is),
- &ss->keys->donation_unit_keys[0].key,
+ &ss->selected_pks[cnt],
&csr_data->nonce.cs_nonce,
&cs_stage_two_callback,
csr_data);
@@ -473,6 +509,9 @@ cleanup (void *cls,
ss->birh = NULL;
}
+ if (ss->selected_pks)
+ GNUNET_array_grow (ss->selected_pks, ss->num_bkp, 0);
+
GNUNET_free (ss->h_udis);
GNUNET_free (ss->alg_values);
GNUNET_free (ss->blinding_secrets);
@@ -522,6 +561,7 @@ TALER_TESTING_cmd_issue_receipts (const char *label,
const uint64_t year,
const char *donor_tax_id,
const char *salt,
+ const char *issue_amount,
unsigned int expected_response_code)
{
struct StatusState *ss;
@@ -535,6 +575,16 @@ TALER_TESTING_cmd_issue_receipts (const char *label,
ss->uses_cs = uses_cs;
ss->donor_salt = (const char*) salt;
ss->donor_tax_id = (const char*) donor_tax_id;
+ if (GNUNET_OK !=
+ TALER_string_to_amount (issue_amount,
+ &ss->amount))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to parse amount `%s' at %s\n",
+ issue_amount,
+ label);
+ GNUNET_assert (0);
+ }
// use libsodium SHA-512 Hash for compatibility reasons with the Donau Verify app.
{