donau

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

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:
Msrc/include/donau_service.h | 21+++++++++++++++++++++
Msrc/include/donau_testing_lib.h | 1+
Msrc/lib/donau_api_handle.c | 171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/testing/test_donau_api.c | 3++-
Msrc/testing/testing_api_cmd_charity_get.c | 2+-
Msrc/testing/testing_api_cmd_issue_receipts.c | 64+++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
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. {