exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

commit 0702b2666888d7dd8a4266e9b32627188ef55e8a
parent e8364cd23197633552528e15bb21ab7c7f5f2689
Author: Christian Grothoff <christian@grothoff.org>
Date:   Thu, 27 Mar 2025 14:42:48 +0100

fix conflicts

Diffstat:
Msrc/auditor/taler-helper-auditor-coins.c | 8+++++---
Msrc/auditor/taler-helper-auditor-reserves.c | 16+++++++++-------
Msrc/exchange/Makefile.am | 4+++-
Msrc/exchange/taler-exchange-httpd.c | 71++++++++++++++++++++++-------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_age-withdraw_reveal.c | 611-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_age-withdraw_reveal.h | 56--------------------------------------------------------
Asrc/exchange/taler-exchange-httpd_batch-withdraw.c | 1460+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_batch-withdraw.h | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_blinding-prepare.c | 212+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_blinding-prepare.h | 40++++++++++++++++++++++++++++++++++++++++
Msrc/exchange/taler-exchange-httpd_csr.c | 9+++++----
Msrc/exchange/taler-exchange-httpd_keys.c | 61++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/exchange/taler-exchange-httpd_keys.h | 25++++++++++++++++++++++++-
Msrc/exchange/taler-exchange-httpd_metrics.c | 19++++++-------------
Msrc/exchange/taler-exchange-httpd_metrics.h | 39+++++++++++++++++----------------------
Msrc/exchange/taler-exchange-httpd_recoup.c | 23+++++++++++++++--------
Msrc/exchange/taler-exchange-httpd_reserves_history.c | 138++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Asrc/exchange/taler-exchange-httpd_reveal-withdraw.c | 589+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_reveal-withdraw.h | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/exchange/taler-exchange-httpd_withdraw.c | 1811+++++++++++++++++++++++++------------------------------------------------------
Msrc/exchange/taler-exchange-httpd_withdraw.h | 57++++++++++++++++++++-------------------------------------
Asrc/exchangedb/0009-age_withdraw.sql | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchangedb/0009-aggregation_transient.sql | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchangedb/0009-recoup.sql | 295+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchangedb/0009-statistics.sql | 587+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchangedb/0009-withdraw.sql | 207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/exchangedb/Makefile.am | 21+++++++++++++++++----
Dsrc/exchangedb/exchange-0009.sql | 653-------------------------------------------------------------------------------
Asrc/exchangedb/exchange-0009.sql.in | 27+++++++++++++++++++++++++++
Dsrc/exchangedb/exchange_do_age_withdraw.sql | 165-------------------------------------------------------------------------------
Msrc/exchangedb/exchange_do_main_gc.sql | 12++++++------
Msrc/exchangedb/exchange_do_recoup_by_reserve.sql | 18+++++++++---------
Msrc/exchangedb/exchange_do_recoup_to_reserve.sql | 6+++---
Asrc/exchangedb/exchange_do_withdraw.sql | 171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/exchangedb/pg_do_age_withdraw.c | 108-------------------------------------------------------------------------------
Dsrc/exchangedb/pg_do_age_withdraw.h | 57---------------------------------------------------------
Msrc/exchangedb/pg_do_recoup.c | 4++--
Msrc/exchangedb/pg_do_recoup.h | 4++--
Asrc/exchangedb/pg_do_withdraw.c | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchangedb/pg_do_withdraw.h | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/exchangedb/pg_get_age_withdraw.c | 119-------------------------------------------------------------------------------
Dsrc/exchangedb/pg_get_age_withdraw.h | 45---------------------------------------------
Asrc/exchangedb/pg_get_batch_withdraw_info.c | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchangedb/pg_get_batch_withdraw_info.h | 43+++++++++++++++++++++++++++++++++++++++++++
Msrc/exchangedb/pg_get_coin_transactions.c | 6+++---
Dsrc/exchangedb/pg_get_reserve_by_h_blind.c | 63---------------------------------------------------------------
Dsrc/exchangedb/pg_get_reserve_by_h_blind.h | 44--------------------------------------------
Asrc/exchangedb/pg_get_reserve_by_h_commitment.c | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchangedb/pg_get_reserve_by_h_commitment.h | 45+++++++++++++++++++++++++++++++++++++++++++++
Msrc/exchangedb/pg_get_reserve_history.c | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Asrc/exchangedb/pg_get_withdraw.c | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchangedb/pg_get_withdraw.h | 44++++++++++++++++++++++++++++++++++++++++++++
Dsrc/exchangedb/pg_get_withdraw_info.c | 79-------------------------------------------------------------------------------
Dsrc/exchangedb/pg_get_withdraw_info.h | 43-------------------------------------------
Msrc/exchangedb/pg_insert_records_by_table.c | 42++++++++++++++++++++++++------------------
Msrc/exchangedb/pg_lookup_records_by_table.c | 70++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msrc/exchangedb/pg_lookup_serial_by_table.c | 12++++++------
Msrc/exchangedb/pg_select_recoup_above_serial_id.c | 12+++---------
Msrc/exchangedb/pg_select_withdraw_amounts_for_kyc_check.c | 10+++++-----
Msrc/exchangedb/pg_select_withdrawals_above_serial_id.c | 55+++++++++++++++++++++++++++++++------------------------
Msrc/exchangedb/plugin_exchangedb_common.c | 13+++++++++++--
Msrc/exchangedb/plugin_exchangedb_postgres.c | 40++++++++++++++++++++--------------------
Msrc/exchangedb/procedures.sql.in | 2+-
Msrc/exchangedb/test_exchangedb.c | 200+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Msrc/include/taler_crypto_lib.h | 332+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/include/taler_exchange_service.h | 921++++++++++++++++++++++++++++++-------------------------------------------------
Msrc/include/taler_exchangedb_plugin.h | 153++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/include/taler_json_lib.h | 62+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/include/taler_testing_lib.h | 80++++++++++++++++++++++++++-----------------------------------------------------
Msrc/json/json_helper.c | 77++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/json/json_pack.c | 127++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/lib/Makefile.am | 19+++++++++----------
Dsrc/lib/exchange_api_age_withdraw.c | 1130-------------------------------------------------------------------------------
Dsrc/lib/exchange_api_age_withdraw_reveal.c | 496-------------------------------------------------------------------------------
Dsrc/lib/exchange_api_batch_withdraw.c | 463-------------------------------------------------------------------------------
Dsrc/lib/exchange_api_batch_withdraw2.c | 446-------------------------------------------------------------------------------
Asrc/lib/exchange_api_blinding_prepare.c | 393+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/lib/exchange_api_csr_withdraw.c | 281-------------------------------------------------------------------------------
Msrc/lib/exchange_api_recoup.c | 3+++
Msrc/lib/exchange_api_reserves_history.c | 198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Asrc/lib/exchange_api_reveal_withdraw.c | 386+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/exchange_api_withdraw.c | 1751+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/testing/Makefile.am | 37-------------------------------------
Msrc/testing/test_exchange_api.c | 1-
Msrc/testing/test_exchange_api_age_restriction.c | 8++++----
Msrc/testing/test_exchange_api_age_restriction.conf | 2+-
Dsrc/testing/test_exchange_api_conflicts-cs.conf | 4----
Dsrc/testing/test_exchange_api_conflicts-rsa.conf | 4----
Dsrc/testing/test_exchange_api_conflicts.c | 312-------------------------------------------------------------------------------
Dsrc/testing/test_exchange_api_conflicts.conf | 81-------------------------------------------------------------------------------
Msrc/testing/test_kyc_api.c | 40+++++++++++++++++++++++-----------------
Msrc/testing/testing_api_cmd_age_withdraw.c | 288+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/testing/testing_api_cmd_batch_deposit.c | 5++++-
Msrc/testing/testing_api_cmd_batch_withdraw.c | 229+++++++++++++++++++++++++++++++++++++++----------------------------------------
Msrc/testing/testing_api_cmd_coin_history.c | 5+++--
Msrc/testing/testing_api_cmd_deposit.c | 62+++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/testing/testing_api_cmd_recoup.c | 24+++++++++++++++++++-----
Msrc/testing/testing_api_cmd_reserve_history.c | 53+++++++++++++++++++++++++++--------------------------
Msrc/testing/testing_api_cmd_withdraw.c | 100++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/util/amount.c | 9++++++++-
Msrc/util/crypto.c | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/util/exchange_signatures.c | 80++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/util/wallet_signatures.c | 286+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
103 files changed, 10326 insertions(+), 8113 deletions(-)

diff --git a/src/auditor/taler-helper-auditor-coins.c b/src/auditor/taler-helper-auditor-coins.c @@ -970,7 +970,8 @@ cleanup_denomination (void *cls, * * @param cls our `struct CoinContext` * @param rowid unique serial ID for the refresh session in our DB - * @param h_blind_ev blinded hash of the coin's public key + * @param num_evs number of elements in @e h_blind_evs + * @param h_blind_evs array @e num_evs of blinded hash of the coin's public keys * @param denom_pub public denomination key of the deposited coin * @param reserve_pub public key of the reserve * @param reserve_sig signature over the withdraw operation (verified elsewhere) @@ -981,7 +982,8 @@ cleanup_denomination (void *cls, static enum GNUNET_GenericReturnValue withdraw_cb (void *cls, uint64_t rowid, - const struct TALER_BlindedCoinHashP *h_blind_ev, + size_t num_evs, + const struct TALER_BlindedCoinHashP *h_blind_evs, const struct TALER_DenominationPublicKey *denom_pub, const struct TALER_ReservePublicKeyP *reserve_pub, const struct TALER_ReserveSignatureP *reserve_sig, @@ -996,7 +998,7 @@ withdraw_cb (void *cls, /* Note: some optimization potential here: lots of fields we could avoid fetching from the database with a custom function. */ - (void) h_blind_ev; + (void) h_blind_evs; (void) reserve_pub; (void) reserve_sig; (void) execution_date; diff --git a/src/auditor/taler-helper-auditor-reserves.c b/src/auditor/taler-helper-auditor-reserves.c @@ -512,7 +512,8 @@ handle_reserve_in ( * * @param cls our `struct ReserveContext` * @param rowid unique serial ID for the refresh session in our DB - * @param h_blind_ev blinded hash of the coin's public key + * @param num_evs number of elements in @e h_blind_evs + * @param h_blind_evs array @e num_evs of blinded hash of the coin's public keys * @param denom_pub public denomination key of the deposited coin * @param reserve_pub public key of the reserve * @param reserve_sig signature over the withdraw operation @@ -524,7 +525,8 @@ static enum GNUNET_GenericReturnValue handle_reserve_out ( void *cls, uint64_t rowid, - const struct TALER_BlindedCoinHashP *h_blind_ev, + size_t num_evs, + const struct TALER_BlindedCoinHashP *h_blind_evs, const struct TALER_DenominationPublicKey *denom_pub, const struct TALER_ReservePublicKeyP *reserve_pub, const struct TALER_ReserveSignatureP *reserve_sig, @@ -603,11 +605,11 @@ handle_reserve_out ( /* check reserve_sig (first: setup remaining members of wsrd) */ if (GNUNET_OK != - TALER_wallet_withdraw_verify (&h_denom_pub, - amount_with_fee, - h_blind_ev, - reserve_pub, - reserve_sig)) + TALER_wallet_withdraw_verify_pre26 (&h_denom_pub, + amount_with_fee, + &h_blind_evs[0], + reserve_pub, + reserve_sig)) { struct TALER_AUDITORDB_BadSigLosses bsl = { .problem_row_id = rowid, diff --git a/src/exchange/Makefile.am b/src/exchange/Makefile.am @@ -144,7 +144,6 @@ taler_exchange_wirewatch_LDADD = \ taler_exchange_httpd_SOURCES = \ taler-exchange-httpd.c taler-exchange-httpd.h \ - taler-exchange-httpd_age-withdraw_reveal.c taler-exchange-httpd_age-withdraw_reveal.h \ taler-exchange-httpd_aml-attributes-get.c taler-exchange-httpd_aml-attributes-get.h \ taler-exchange-httpd_aml-decision.c taler-exchange-httpd_aml-decision.h \ taler-exchange-httpd_aml-decisions-get.c \ @@ -153,6 +152,8 @@ taler_exchange_httpd_SOURCES = \ taler-exchange-httpd_aml-transfer-get.c taler-exchange-httpd_aml-transfer-get.h \ taler-exchange-httpd_auditors.c taler-exchange-httpd_auditors.h \ taler-exchange-httpd_batch-deposit.c taler-exchange-httpd_batch-deposit.h \ + taler-exchange-httpd_batch-withdraw.c taler-exchange-httpd_batch-withdraw.h \ + taler-exchange-httpd_blinding-prepare.c taler-exchange-httpd_blinding-prepare.h \ taler-exchange-httpd_coins_get.c taler-exchange-httpd_coins_get.h \ taler-exchange-httpd_common_deposit.c taler-exchange-httpd_common_deposit.h \ taler-exchange-httpd_common_kyc.c taler-exchange-httpd_common_kyc.h \ @@ -206,6 +207,7 @@ taler_exchange_httpd_SOURCES = \ taler-exchange-httpd_reserves_open.c taler-exchange-httpd_reserves_open.h \ taler-exchange-httpd_reserves_purse.c taler-exchange-httpd_reserves_purse.h \ taler-exchange-httpd_responses.c taler-exchange-httpd_responses.h \ + taler-exchange-httpd_reveal-withdraw.c taler-exchange-httpd_reveal-withdraw.h \ taler-exchange-httpd_spa.c taler-exchange-httpd_spa.h \ taler-exchange-httpd_terms.c taler-exchange-httpd_terms.h \ taler-exchange-httpd_transfers_get.c taler-exchange-httpd_transfers_get.h \ diff --git a/src/exchange/taler-exchange-httpd.c b/src/exchange/taler-exchange-httpd.c @@ -32,7 +32,8 @@ #include "taler_templating_lib.h" #include "taler_mhd_lib.h" #include "taler-exchange-httpd_withdraw.h" -#include "taler-exchange-httpd_age-withdraw_reveal.h" +#include "taler-exchange-httpd_batch-withdraw.h" +#include "taler-exchange-httpd_reveal-withdraw.h" #include "taler-exchange-httpd_aml-attributes-get.h" #include "taler-exchange-httpd_aml-decision.h" #include "taler-exchange-httpd_aml-statistics-get.h" @@ -40,6 +41,7 @@ #include "taler-exchange-httpd_aml-measures-get.h" #include "taler-exchange-httpd_auditors.h" #include "taler-exchange-httpd_batch-deposit.h" +#include "taler-exchange-httpd_blinding-prepare.h" #include "taler-exchange-httpd_coins_get.h" #include "taler-exchange-httpd_config.h" #include "taler-exchange-httpd_contract.h" @@ -719,46 +721,6 @@ handle_get_aml (struct TEH_RequestContext *rc, /** - * Handle a "/age-withdraw/$ACH/reveal" POST request. Parses the "ACH" - * hash of the commitment from a previous call to - * /reserves/$reserve_pub/age-withdraw - * - * @param rc request context - * @param root uploaded JSON data - * @param args array of additional options - * @return MHD result code - */ -static MHD_RESULT -handle_post_age_withdraw (struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[2]) -{ - struct TALER_AgeWithdrawCommitmentHashP ach; - - if (0 != strcmp ("reveal", args[1])) - return r404 (rc->connection, - args[1]); - - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[0], - strlen (args[0]), - &ach, - sizeof (ach))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_RESERVE_PUB_MALFORMED, - args[0]); - } - - return TEH_handler_age_withdraw_reveal (rc, - &ach, - root); -} - - -/** * Signature of functions that handle operations on reserves. * * @param rc request context @@ -801,14 +763,11 @@ handle_post_reserves (struct TEH_RequestContext *rc, } h[] = { { + /* FIXME: deprecated since v24 */ .op = "batch-withdraw", .handler = &TEH_handler_batch_withdraw }, { - .op = "age-withdraw", - .handler = &TEH_handler_age_withdraw - }, - { .op = "purse", .handler = &TEH_handler_reserves_purse }, @@ -1715,13 +1674,26 @@ handle_mhd_request (void *cls, .handler.post = &TEH_handler_csr_melt, .nargs = 0 }, + /* request R's input for Clause-Schnorr signatures in batches */ + { + .url = "blinding-prepare", + .method = MHD_HTTP_METHOD_POST, + .handler.post = &TEH_handler_blinding_prepare, + .nargs = 0 + }, { .url = "csr-withdraw", .method = MHD_HTTP_METHOD_POST, .handler.post = &TEH_handler_csr_withdraw, .nargs = 0 }, - /* Withdrawing coins / interaction with reserves */ + /* withdraw request, available since v24 of the API */ + { + .url = "withdraw", + .method = MHD_HTTP_METHOD_POST, + .handler.post = &TEH_handler_withdraw, + .nargs = 0 + }, { .url = "reserves", .method = MHD_HTTP_METHOD_GET, @@ -1736,10 +1708,10 @@ handle_mhd_request (void *cls, .nargs = 2 }, { - .url = "age-withdraw", + .url = "reveal-withdraw", .method = MHD_HTTP_METHOD_POST, - .handler.post = &handle_post_age_withdraw, - .nargs = 2 + .handler.post = &TEH_handler_reveal_withdraw, + .nargs = 0 }, { .url = "reserves-attest", @@ -2749,6 +2721,7 @@ do_shutdown (void *cls) my_mhd = TALER_MHD_daemon_stop (); TEH_resume_keys_requests (true); TEH_batch_deposit_cleanup (); + TEH_batch_withdraw_cleanup (); TEH_withdraw_cleanup (); TEH_reserves_close_cleanup (); TEH_reserves_purse_cleanup (); diff --git a/src/exchange/taler-exchange-httpd_age-withdraw_reveal.c b/src/exchange/taler-exchange-httpd_age-withdraw_reveal.c @@ -1,611 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2023 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_age-withdraw_reveal.c - * @brief Handle /age-withdraw/$ACH/reveal requests - * @author Özgür Kesim - */ -#include "platform.h" -#include <gnunet/gnunet_common.h> -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include "taler-exchange-httpd_metrics.h" -#include "taler_error_codes.h" -#include "taler_exchangedb_plugin.h" -#include "taler_mhd_lib.h" -#include "taler-exchange-httpd_mhd.h" -#include "taler-exchange-httpd_age-withdraw_reveal.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" - -/** - * State for an /age-withdraw/$ACH/reveal operation. - */ -struct AgeRevealContext -{ - - /** - * Commitment for the age-withdraw operation, previously called by the - * client. - */ - struct TALER_AgeWithdrawCommitmentHashP ach; - - /** - * Public key of the reserve for with the age-withdraw commitment was - * originally made. This parameter is provided by the client again - * during the call to reveal in order to save a database-lookup. - */ - struct TALER_ReservePublicKeyP reserve_pub; - - /** - * Number of coins to reveal. MUST be equal to - * @e num_secrets/(kappa -1). - */ - uint32_t num_coins; - - /** - * Number of secrets in the reveal. MUST be a multiple of (kappa-1). - */ - uint32_t num_secrets; - - /** - * @e num_secrets secrets for disclosed coins. - */ - struct TALER_PlanchetMasterSecretP *disclosed_coin_secrets; - - /** - * The data from the original age-withdraw. Will be retrieved from - * the DB via @a ach and @a reserve_pub. - */ - struct TALER_EXCHANGEDB_AgeWithdraw commitment; -}; - - -/** - * Parse the json body of an '/age-withdraw/$ACH/reveal' request. It extracts - * the denomination hashes, blinded coins and disclosed coins and allocates - * memory for those. - * - * @param connection The MHD connection to handle - * @param j_disclosed_coin_secrets The n*(kappa-1) disclosed coins' private keys in JSON format, from which all other attributes (age restriction, blinding, nonce) will be derived from - * @param[out] actx The context of the operation, only partially built at call time - * @param[out] mhd_ret The result if a reply is queued for MHD - * @return true on success, false on failure, with a reply already queued for MHD. - */ -static enum GNUNET_GenericReturnValue -parse_age_withdraw_reveal_json ( - struct MHD_Connection *connection, - const json_t *j_disclosed_coin_secrets, - struct AgeRevealContext *actx, - MHD_RESULT *mhd_ret) -{ - enum GNUNET_GenericReturnValue result = GNUNET_SYSERR; - size_t num_entries; - - /* Verify JSON-structure consistency */ - { - const char *error = NULL; - - num_entries = json_array_size (j_disclosed_coin_secrets); /* 0, if not an array */ - - if (! json_is_array (j_disclosed_coin_secrets)) - error = "disclosed_coin_secrets must be an array"; - else if (num_entries == 0) - error = "disclosed_coin_secrets must not be empty"; - else if (num_entries > TALER_MAX_FRESH_COINS) - error = "maximum number of coins that can be withdrawn has been exceeded"; - - if (NULL != error) - { - *mhd_ret = TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - error); - return GNUNET_SYSERR; - } - - actx->num_secrets = num_entries * (TALER_CNC_KAPPA - 1); - actx->num_coins = num_entries; - - } - - /* Continue parsing the parts */ - { - unsigned int idx = 0; - unsigned int k = 0; - json_t *array = NULL; - json_t *value = NULL; - - /* Parse diclosed keys */ - actx->disclosed_coin_secrets = - GNUNET_new_array (actx->num_secrets, - struct TALER_PlanchetMasterSecretP); - - json_array_foreach (j_disclosed_coin_secrets, idx, array) { - if (! json_is_array (array) || - (TALER_CNC_KAPPA - 1 != json_array_size (array))) - { - char msg[256] = {0}; - GNUNET_snprintf (msg, - sizeof(msg), - "couldn't parse entry no. %d in array disclosed_coin_secrets", - idx + 1); - *mhd_ret = TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - msg); - goto EXIT; - - } - - json_array_foreach (array, k, value) - { - struct TALER_PlanchetMasterSecretP *secret = - &actx->disclosed_coin_secrets[2 * idx + k]; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto (NULL, secret), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (value, spec, NULL, NULL)) - { - char msg[256] = {0}; - GNUNET_snprintf (msg, - sizeof(msg), - "couldn't parse entry no. %d in array disclosed_coin_secrets[%d]", - k + 1, - idx + 1); - *mhd_ret = TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - msg); - goto EXIT; - } - } - }; - } - - result = GNUNET_OK; - -EXIT: - return result; -} - - -/** - * Check if the request belongs to an existing age-withdraw request. - * If so, sets the commitment object with the request data. - * Otherwise, it queues an appropriate MHD response. - * - * @param connection The HTTP connection to the client - * @param h_commitment Original commitment value sent with the age-withdraw request - * @param reserve_pub Reserve public key used in the original age-withdraw request - * @param[out] commitment Data from the original age-withdraw request - * @param[out] result In the error cases, a response will be queued with MHD and this will be the result. - * @return #GNUNET_OK if the withdraw request has been found, - * #GNUNET_SYSERR if we did not find the request in the DB - */ -static enum GNUNET_GenericReturnValue -find_original_commitment ( - struct MHD_Connection *connection, - const struct TALER_AgeWithdrawCommitmentHashP *h_commitment, - const struct TALER_ReservePublicKeyP *reserve_pub, - struct TALER_EXCHANGEDB_AgeWithdraw *commitment, - MHD_RESULT *result) -{ - enum GNUNET_DB_QueryStatus qs; - - for (unsigned int try = 0; try < 3; try++) - { - qs = TEH_plugin->get_age_withdraw (TEH_plugin->cls, - reserve_pub, - h_commitment, - commitment); - switch (qs) - { - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return GNUNET_OK; /* Only happy case */ - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - *result = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_WITHDRAW_COMMITMENT_UNKNOWN, - NULL); - return GNUNET_SYSERR; - case GNUNET_DB_STATUS_HARD_ERROR: - *result = TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_age_withdraw_info"); - return GNUNET_SYSERR; - case GNUNET_DB_STATUS_SOFT_ERROR: - break; /* try again */ - default: - GNUNET_break (0); - *result = TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL); - return GNUNET_SYSERR; - } - } - /* after unsuccessful retries*/ - *result = TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_age_withdraw_info"); - return GNUNET_SYSERR; -} - - -/** - * @brief Derives a age-restricted planchet from a given secret and calculates the hash - * - * @param connection Connection to the client - * @param keys The denomination keys in memory - * @param secret The secret to a planchet - * @param denom_pub_h The hash of the denomination for the planchet - * @param max_age The maximum age allowed - * @param[out] bch Hashcode to write - * @param[out] result On error, a HTTP-response will be queued and result set accordingly - * @return GNUNET_OK on success, GNUNET_SYSERR otherwise, with an error message - * written to the client and @e result set. - */ -static enum GNUNET_GenericReturnValue -calculate_blinded_hash ( - struct MHD_Connection *connection, - const struct TEH_KeyStateHandle *keys, - const struct TALER_PlanchetMasterSecretP *secret, - const struct TALER_DenominationHashP *denom_pub_h, - uint8_t max_age, - struct TALER_BlindedCoinHashP *bch, - MHD_RESULT *result) -{ - enum GNUNET_GenericReturnValue ret; - struct TEH_DenominationKey *denom_key; - struct TALER_AgeCommitmentHash ach; - - /* First, retrieve denomination details */ - denom_key = TEH_keys_denomination_by_hash_from_state (keys, - denom_pub_h, - connection, - result); - if (NULL == denom_key) - { - GNUNET_break_op (0); - *result = TALER_MHD_reply_with_ec (connection, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - NULL); - return GNUNET_SYSERR; - } - - /* calculate age commitment hash */ - { - struct TALER_AgeCommitmentProof acp; - - TALER_age_restriction_from_secret (secret, - &denom_key->denom_pub.age_mask, - max_age, - &acp); - TALER_age_commitment_hash (&acp.commitment, - &ach); - TALER_age_commitment_proof_free (&acp); - } - - /* Next: calculate planchet */ - { - struct TALER_CoinPubHashP c_hash; - struct TALER_PlanchetDetail detail = {0}; - struct TALER_CoinSpendPrivateKeyP coin_priv; - union GNUNET_CRYPTO_BlindingSecretP bks; - struct GNUNET_CRYPTO_BlindingInputValues bi = { - .cipher = denom_key->denom_pub.bsign_pub_key->cipher - }; - struct TALER_ExchangeWithdrawValues alg_values = { - .blinding_inputs = &bi - }; - union GNUNET_CRYPTO_BlindSessionNonce nonce; - union GNUNET_CRYPTO_BlindSessionNonce *noncep = NULL; - - // FIXME[oec?]: add logic to denom.c to do this! - if (GNUNET_CRYPTO_BSA_CS == bi.cipher) - { - struct TEH_CsDeriveData cdd = { - .h_denom_pub = &denom_key->h_denom_pub, - .nonce = &nonce.cs_nonce, - }; - - TALER_cs_withdraw_nonce_derive (secret, - &nonce.cs_nonce); - noncep = &nonce; - GNUNET_assert (TALER_EC_NONE == - TEH_keys_denomination_cs_r_pub ( - &cdd, - false, - &bi.details.cs_values)); - } - TALER_planchet_blinding_secret_create (secret, - &alg_values, - &bks); - TALER_planchet_setup_coin_priv (secret, - &alg_values, - &coin_priv); - ret = TALER_planchet_prepare (&denom_key->denom_pub, - &alg_values, - &bks, - noncep, - &coin_priv, - &ach, - &c_hash, - &detail); - if (GNUNET_OK != ret) - { - GNUNET_break (0); - *result = TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - GNUNET_JSON_pack_string ( - "details", - "failed to prepare planchet from base key")); - return ret; - } - - TALER_coin_ev_hash (&detail.blinded_planchet, - &denom_key->h_denom_pub, - bch); - TALER_blinded_planchet_free (&detail.blinded_planchet); - } - - return ret; -} - - -/** - * @brief Checks the validity of the disclosed coins as follows: - * - Derives and calculates the disclosed coins' - * - public keys, - * - nonces (if applicable), - * - age commitments, - * - blindings - * - blinded hashes - * - Computes h_commitment with those calculated and the undisclosed hashes - * - Compares h_commitment with the value from the original commitment - * - Verifies that all public keys in indices larger than the age group - * corresponding to max_age are derived from the constant public key. - * - * The derivation of the blindings, (potential) nonces and age-commitment from - * a coin's private keys is defined in - * https://docs.taler.net/design-documents/024-age-restriction.html#withdraw - * - * @param connection HTTP-connection to the client - * @param commitment Original commitment - * @param disclosed_coin_secrets The secrets of the disclosed coins, (TALER_CNC_KAPPA - 1)*num_coins many - * @param num_coins number of coins to reveal via @a disclosed_coin_secrets - * @param[out] result On error, a HTTP-response will be queued and result set accordingly - * @return GNUNET_OK on success, GNUNET_SYSERR otherwise - */ -static enum GNUNET_GenericReturnValue -verify_commitment_and_max_age ( - struct MHD_Connection *connection, - const struct TALER_EXCHANGEDB_AgeWithdraw *commitment, - const struct TALER_PlanchetMasterSecretP *disclosed_coin_secrets, - uint32_t num_coins, - MHD_RESULT *result) -{ - enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; - struct GNUNET_HashContext *hash_context; - struct TEH_KeyStateHandle *keys; - - if (num_coins != commitment->num_coins) - { - GNUNET_break_op (0); - *result = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "#coins"); - return GNUNET_SYSERR; - } - - /* We need the current keys in memory for the meta-data of the denominations */ - keys = TEH_keys_get_state (); - if (NULL == keys) - { - *result = TALER_MHD_reply_with_ec (connection, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - NULL); - return GNUNET_SYSERR; - } - - hash_context = GNUNET_CRYPTO_hash_context_start (); - - for (size_t coin_idx = 0; coin_idx < num_coins; coin_idx++) - { - size_t i = 0; /* either 0 or 1, to index into coin_evs */ - - for (size_t k = 0; k<TALER_CNC_KAPPA; k++) - { - if (k == (size_t) commitment->noreveal_index) - { - GNUNET_CRYPTO_hash_context_read (hash_context, - &commitment->h_coin_evs[coin_idx], - sizeof(commitment->h_coin_evs[coin_idx] - )); - } - else - { - /* j is the index into disclosed_coin_secrets[] */ - size_t j = (TALER_CNC_KAPPA - 1) * coin_idx + i; - const struct TALER_PlanchetMasterSecretP *secret; - struct TALER_BlindedCoinHashP bch; - - GNUNET_assert (2>i); - GNUNET_assert ((TALER_CNC_KAPPA - 1) * num_coins > j); - - secret = &disclosed_coin_secrets[j]; - i++; - - ret = calculate_blinded_hash (connection, - keys, - secret, - &commitment->denom_pub_hashes[coin_idx], - commitment->max_age, - &bch, - result); - - if (GNUNET_OK != ret) - { - GNUNET_CRYPTO_hash_context_abort (hash_context); - return GNUNET_SYSERR; - } - - /* Continue the running hash of all coin hashes with the calculated - * hash-value of the current, disclosed coin */ - GNUNET_CRYPTO_hash_context_read (hash_context, - &bch, - sizeof(bch)); - } - } - } - - /* Finally, compare the calculated hash with the original commitment */ - { - struct GNUNET_HashCode calc_hash; - GNUNET_CRYPTO_hash_context_finish (hash_context, - &calc_hash); - - if (0 != GNUNET_CRYPTO_hash_cmp (&commitment->h_commitment.hash, - &calc_hash)) - { - GNUNET_break_op (0); - *result = TALER_MHD_reply_with_ec (connection, - TALER_EC_EXCHANGE_WITHDRAW_REVEAL_INVALID_HASH, - NULL); - return GNUNET_SYSERR; - } - - } - return GNUNET_OK; -} - - -/** - * @brief Send a response for "/age-withdraw/$RCH/reveal" - * - * @param connection The http connection to the client to send the response to - * @param commitment The data from the commitment with signatures - * @return a MHD result code - */ -static MHD_RESULT -reply_age_withdraw_reveal_success ( - struct MHD_Connection *connection, - const struct TALER_EXCHANGEDB_AgeWithdraw *commitment) -{ - json_t *list = json_array (); - GNUNET_assert (NULL != list); - - for (unsigned int i = 0; i < commitment->num_coins; i++) - { - json_t *obj = GNUNET_JSON_PACK ( - TALER_JSON_pack_blinded_denom_sig (NULL, - &commitment->denom_sigs[i])); - GNUNET_assert (0 == - json_array_append_new (list, - obj)); - } - - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("ev_sigs", - list)); -} - - -MHD_RESULT -TEH_handler_age_withdraw_reveal ( - struct TEH_RequestContext *rc, - const struct TALER_AgeWithdrawCommitmentHashP *ach, - const json_t *root) -{ - MHD_RESULT result = MHD_NO; - enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; - struct AgeRevealContext actx = {0}; - const json_t *j_disclosed_coin_secrets; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("reserve_pub", - &actx.reserve_pub), - GNUNET_JSON_spec_array_const ("disclosed_coin_secrets", - &j_disclosed_coin_secrets), - GNUNET_JSON_spec_end () - }; - - actx.ach = *ach; - - /* Parse JSON body*/ - ret = TALER_MHD_parse_json_data (rc->connection, - root, - spec); - if (GNUNET_OK != ret) - { - GNUNET_break_op (0); - return (GNUNET_SYSERR == ret) ? MHD_NO : MHD_YES; - } - - - do { - /* Extract denominations, blinded and disclosed coins */ - if (GNUNET_OK != - parse_age_withdraw_reveal_json ( - rc->connection, - j_disclosed_coin_secrets, - &actx, - &result)) - break; - - /* Find original commitment */ - if (GNUNET_OK != - find_original_commitment ( - rc->connection, - &actx.ach, - &actx.reserve_pub, - &actx.commitment, - &result)) - break; - - /* Verify the computed h_commitment equals the committed one and that coins - * have a maximum age group corresponding max_age (age-mask dependent) */ - if (GNUNET_OK != - verify_commitment_and_max_age ( - rc->connection, - &actx.commitment, - actx.disclosed_coin_secrets, - actx.num_coins, - &result)) - break; - - /* Finally, return the signatures */ - result = reply_age_withdraw_reveal_success (rc->connection, - &actx.commitment); - - } while (0); - - GNUNET_JSON_parse_free (spec); - if (NULL != actx.commitment.denom_sigs) - for (unsigned int i = 0; i<actx.num_coins; i++) - TALER_blinded_denom_sig_free (&actx.commitment.denom_sigs[i]); - GNUNET_free (actx.commitment.denom_sigs); - GNUNET_free (actx.commitment.denom_pub_hashes); - GNUNET_free (actx.commitment.denom_serials); - GNUNET_free (actx.disclosed_coin_secrets); - return result; -} - - -/* end of taler-exchange-httpd_age-withdraw_reveal.c */ diff --git a/src/exchange/taler-exchange-httpd_age-withdraw_reveal.h b/src/exchange/taler-exchange-httpd_age-withdraw_reveal.h @@ -1,56 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2023 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file taler-exchange-httpd_age-withdraw_reveal.h - * @brief Handle /age-withdraw/$ACH/reveal requests - * @author Özgür Kesim - */ -#ifndef TALER_EXCHANGE_HTTPD_AGE_WITHDRAW_REVEAL_H -#define TALER_EXCHANGE_HTTPD_AGE_WITHDRAW_REVEAL_H - -#include <microhttpd.h> -#include "taler-exchange-httpd.h" - - -/** - * Handle a "/age-withdraw/$ACH/reveal" request. - * - * The client got a noreveal_index in response to a previous request - * /reserve/$RESERVE_PUB/age-withdraw. It now has to reveal all n*(kappa-1) - * coin's private keys (except for the noreveal_index), from which all other - * coin-relevant data (blinding, age restriction, nonce) is derived from. - * - * The exchange computes those values, ensures that the maximum age is - * correctly applied, calculates the hash of the blinded envelopes, and - - * together with the non-disclosed blinded envelopes - compares the hash of - * those against the original commitment $ACH. - * - * If all those checks and the used denominations turn out to be correct, the - * exchange signs all blinded envelopes with their appropriate denomination - * keys. - * - * @param rc request context - * @param root uploaded JSON data - * @param ach commitment to the age restricted coints from the age-withdraw request - * @return MHD result code - */ -MHD_RESULT -TEH_handler_age_withdraw_reveal ( - struct TEH_RequestContext *rc, - const struct TALER_AgeWithdrawCommitmentHashP *ach, - const json_t *root); - -#endif diff --git a/src/exchange/taler-exchange-httpd_batch-withdraw.c b/src/exchange/taler-exchange-httpd_batch-withdraw.c @@ -0,0 +1,1460 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation; either version 3, + or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty + of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General + Public License along with TALER; see the file COPYING. If not, + see <http://www.gnu.org/licenses/> +*/ + +/* + * NOTE: These endpoints are deprecated starting with v24 of the protocol and will be removed, + * including this file. + */ + +/** + * @file taler-exchange-httpd_batch-withdraw.c + * @brief Common code to handle /reserves/$RESERVE_PUB/{age,batch}-withdraw requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + * @author Ozgur Kesim + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler-exchange-httpd.h" +#include "taler_json_lib.h" +#include "taler_kyclogic_lib.h" +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_batch-withdraw.h" +#include "taler-exchange-httpd_common_kyc.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_keys.h" +#include "taler_util.h" + +/** + * Context for batch-withdraw requests + */ +struct BatchWithdrawContext +{ + + /** + * This struct is kept in a DLL. + */ + struct BatchWithdrawContext *prev; + struct BatchWithdrawContext *next; + + /** + * Processing phase we are in for any of the withdraw types. + * The ordering here partially matters, as we progress through + * them by incrementing the phase in the happy path. + */ + enum + { + PHASE_CHECK_KEYS, + PHASE_CHECK_RESERVE_SIGNATURE, + PHASE_RUN_LEGI_CHECK, + PHASE_SUSPENDED, + PHASE_CHECK_KYC_RESULT, + PHASE_PREPARE_TRANSACTION, + PHASE_RUN_TRANSACTION, + PHASE_GENERATE_REPLY_SUCCESS, + PHASE_GENERATE_REPLY_ERROR, + PHASE_RETURN_NO, + PHASE_RETURN_YES, + } phase; + + + /** + * Handle for the legitimization check. + */ + struct TEH_LegitimizationCheckHandle *lch; + + /** + * Request context + */ + const struct TEH_RequestContext *rc; + + /** + * Public key of the reserve. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * KYC status for the operation. + */ + struct TALER_EXCHANGEDB_KycStatus kyc; + + /** + * Current time for the DB transaction. + */ + struct GNUNET_TIME_Timestamp now; + + /** + * Set to the hash of the normalized payto URI that established + * the reserve. + */ + struct TALER_NormalizedPaytoHashP h_normalized_payto; + + /** + * Number of coins, length of @e planchets array. + */ + unsigned int num_coins; + + /** + * Array of @e num_coins planchets we are processing, + * containing the information per planchet in a batch withdraw. + */ + struct PlanchetContext + { + + /** + * Value of the coin being exchanged (matching the denomination key) + * plus the transaction fee. We include this in what is being + * signed so that we can verify a reserve's remaining total balance + * without needing to access the respective denomination key + * information each time. + */ + struct TALER_Amount amount_with_fee; + + /** + * Blinded planchet. + */ + struct TALER_BlindedPlanchet blinded_planchet; + + /** + * Set to the resulting signed coin data to be returned to the client. + */ + struct TALER_EXCHANGEDB_CollectableBlindcoin collectable; + + } *planchets; + + /** + * Total amount from all coins with fees. + */ + struct TALER_Amount batch_total; + + /** + * Errors occurring during evaluation of the request are captured in this struct. + * In phase PHASE_GENERATE_REPLY_ERROR an appropriate error message is prepared + * and sent to the client. + */ + struct + { + /** + * The different type of errors that might occur, sorted by name. + * Some of them require idempotency checks, which are marked + * in array @a needs_idempotency_check_error below. + */ + enum + { + ERROR_NONE, + + ERROR_AGE_RESTRICTION_REQUIRED, + ERROR_BATCH_AMOUNT_FEE_OVERFLOW, + ERROR_BATCH_IDEMPOTENT_PLANCHET, + ERROR_BATCH_INSUFFICIENT_FUNDS, + ERROR_BATCH_NONCE_RESUSE, + ERROR_CIPHER_MISMATCH, + ERROR_DB_FETCH_FAILED, + ERROR_DB_INVARIANT_FAILURE, + ERROR_DENOMINATION_BATCH_SIGN, + ERROR_DENOMINATION_EXPIRED, + ERROR_DENOMINATION_KEY_UNKNOWN, + ERROR_DENOMINATION_REVOKED, + ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + ERROR_INTERNAL_INVARIANT_FAILURE, + ERROR_KEYS_MISSING, + ERROR_KYC_REQUIRED, + ERROR_LEGITIMIZATION_RESULT, + ERROR_RESERVE_SIGNATURE_INVALID, + ERROR_RESERVE_UNKNOWN, + ERROR_REQUEST_PARAMETER_MALFORMED, + + ERROR_MAX + } code; + + /** + * Some errors require details to be sent to the client. + * These are captured in this union. + * Each field is marked with a comment, referring to the error(s) + * that is/are using it. + */ + union + { + /* ERROR_REQUEST_PARAMETER_MALFORMED */ + const char *hint; + + /* ERROR_DENOMINATION_KEY_UNKNOWN */ + const struct TALER_DenominationHashP *denom_h; + + /* ERROR_DB_FETCH_FAILED */ + const char *db_fetch_context; + + /* ERROR_AGE_RESTRICTION_REQUIRED */ + uint16_t lowest_age; + + /* ERROR_BATCH_INSUFFICIENT_FUNDS */ + struct TALER_Amount reserve_balance; + + /* ERROR_DENOMINATION_BATCH_SIGN */ + enum TALER_ErrorCode ec; + + /* ERROR_LEGITIMIZATION_RESULT */ + struct + { + struct MHD_Response *response; + unsigned int http_status; + } legi; + + } details; + } error; +}; + +/** + * This table marks which @a BatchWithdrawContext.error.code + * needs a idempotency check prior to actually sending an error message. + */ +static const bool + needs_idempotency_check[] = { + [ERROR_NONE] = false, + + [ERROR_AGE_RESTRICTION_REQUIRED] = false, + [ERROR_BATCH_AMOUNT_FEE_OVERFLOW] = false, + [ERROR_BATCH_NONCE_RESUSE] = false, + [ERROR_CIPHER_MISMATCH] = false, + [ERROR_DB_FETCH_FAILED] = false, + [ERROR_DB_INVARIANT_FAILURE] = false, + [ERROR_DENOMINATION_BATCH_SIGN] = false, + [ERROR_DENOMINATION_VALIDITY_IN_FUTURE] = false, + [ERROR_INTERNAL_INVARIANT_FAILURE] = false, + [ERROR_LEGITIMIZATION_RESULT] = false, + [ERROR_REQUEST_PARAMETER_MALFORMED] = false, + [ERROR_RESERVE_SIGNATURE_INVALID] = false, + [ERROR_RESERVE_UNKNOWN] = false, + + /* These require idempotency checks */ + [ERROR_BATCH_IDEMPOTENT_PLANCHET] = true, + [ERROR_BATCH_INSUFFICIENT_FUNDS] = true, + [ERROR_DENOMINATION_EXPIRED] = true, + [ERROR_DENOMINATION_KEY_UNKNOWN] = true, + [ERROR_DENOMINATION_REVOKED] = true, + [ERROR_KEYS_MISSING] = true, + [ERROR_KYC_REQUIRED] = true, +}; + +_Static_assert ( + (sizeof (needs_idempotency_check) == + ERROR_MAX), + "needs_idempotency_check size mismatch with enum WithdrawErrorCode"); + +/** + * The following macros set the given error code, + * set the phase to PHASE_GENERATE_REPLY_ERROR, + * and optionally set the given field (with an optionally given value). + */ +#define SET_ERROR(wc, ec) \ + do \ + { (wc)->error.code = (ec); \ + (wc)->phase = PHASE_GENERATE_REPLY_ERROR; } while (0) + +#define SET_ERROR_WITH_FIELD(wc, ec, field) \ + do \ + { (wc)->error.code = (ec); \ + (wc)->error.details.field = (field); \ + (wc)->phase = PHASE_GENERATE_REPLY_ERROR; } while (0) + +#define SET_ERROR_WITH_DETAIL(wc, ec, field, value) \ + do \ + { (wc)->error.code = (ec); \ + (wc)->error.details.field = (value); \ + (wc)->phase = PHASE_GENERATE_REPLY_ERROR; } while (0) + + +/** + * All withdraw context is kept in a DLL. + */ +static struct BatchWithdrawContext *wc_head; +static struct BatchWithdrawContext *wc_tail; + +void +TEH_batch_withdraw_cleanup () +{ + struct BatchWithdrawContext *wc; + + while (NULL != (wc = wc_head)) + { + GNUNET_CONTAINER_DLL_remove (wc_head, + wc_tail, + wc); + MHD_resume_connection (wc->rc->connection); + } +} + + +/** + * Terminate the main loop by returning the final + * result. + * + * @param[in,out] wc context to update phase for + * @param mres MHD status to return + */ +static void +finish_loop (struct BatchWithdrawContext *wc, + MHD_RESULT mres) +{ + wc->phase = (MHD_YES == mres) + ? PHASE_RETURN_YES + : PHASE_RETURN_NO; +} + + +/** + * Generates our final (successful) response to a batch withdraw request. + * + * @param wc operation context + */ +static void +phase_generate_reply_success (struct BatchWithdrawContext *wc) +{ + const struct TEH_RequestContext *rc = wc->rc; + json_t *sigs; + + sigs = json_array (); + GNUNET_assert (NULL != sigs); + for (unsigned int i = 0; i<wc->num_coins; i++) + { + struct PlanchetContext *pc = &wc->planchets[i]; + + GNUNET_assert ( + 0 == + json_array_append_new ( + sigs, + GNUNET_JSON_PACK ( + TALER_JSON_pack_blinded_denom_sig ( + "ev_sig", + &pc->collectable.sig)))); + } + TEH_METRICS_withdraw_num_coins += wc->num_coins; + finish_loop (wc, + TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("ev_sigs", + sigs))); +} + + +/** + * Check if the batch withdraw in @a wc is replayed + * and we already have an answer. + * If so, replay the existing answer and return the HTTP response. + * + * @param wc parsed request data + * @return true if the request is idempotent with an existing request + * false if we did not find the request in the DB and did not set @a mret + */ +static bool +check_idempotency ( + struct BatchWithdrawContext *wc) +{ + + for (unsigned int i = 0; i<wc->num_coins; i++) + { + struct PlanchetContext *pc = &wc->planchets[i]; + enum GNUNET_DB_QueryStatus qs; + struct TALER_EXCHANGEDB_CollectableBlindcoin collectable; + + qs = TEH_plugin->get_batch_withdraw_info ( + TEH_plugin->cls, + &pc->collectable.h_coin_envelope, + &collectable); + if (0 > qs) + { + /* FIXME: soft error not handled correctly! */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + SET_ERROR_WITH_DETAIL (wc, + ERROR_DB_FETCH_FAILED, + db_fetch_context, + "get_withdraw_info"); + return true; /* Well, kind-of. */ + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return false; + pc->collectable = collectable; + } + + /* generate idempotent reply */ + TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_WITHDRAW]++; + wc->phase = PHASE_GENERATE_REPLY_SUCCESS; + return true; +} + + +/** + * Function implementing withdraw transaction. Runs the + * transaction logic; IF it returns a non-error code, the transaction + * logic MUST NOT queue a MHD response. IF it returns an hard error, + * the transaction logic MUST queue a MHD response and set @a mhd_ret. + * IF it returns the soft error code, the function MAY be called again + * to retry and MUST not queue a MHD response. + * + * Note that "wc->collectable.sig" is set before entering this function as we + * signed before entering the transaction. + * + * @param cls a `struct BatchWithdrawContext *`, with @e withdraw_type set to WITHDRAW_TYPE_BATCH + * @param connection MHD request which triggered the transaction + * @param[out] mhd_ret set to MHD response status for @a connection, + * if transaction failed (!) + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +batch_withdraw_transaction ( + void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct BatchWithdrawContext *wc = cls; + uint64_t ruuid; + enum GNUNET_DB_QueryStatus qs; + bool found = false; + bool balance_ok = false; + bool age_ok = false; + uint16_t allowed_maximum_age = 0; + struct TALER_Amount reserve_balance; + + qs = TEH_plugin->do_batch_withdraw ( + TEH_plugin->cls, + wc->now, + &wc->reserve_pub, + &wc->batch_total, + TEH_age_restriction_enabled, + &found, + &balance_ok, + &reserve_balance, + &age_ok, + &allowed_maximum_age, + &ruuid); + + if (0 > qs) + { + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + GNUNET_break (0); + SET_ERROR_WITH_DETAIL (wc, + ERROR_DB_FETCH_FAILED, + db_fetch_context, + "update_reserve_batch_withdraw"); + } + return qs; + } + + if (! found) + { + GNUNET_break_op (0); + SET_ERROR (wc, + ERROR_RESERVE_UNKNOWN); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if (! age_ok) + { + /* We respond with the lowest age in the corresponding age group + * of the required age */ + uint16_t lowest_age = TALER_get_lowest_age ( + &TEH_age_restriction_config.mask, + allowed_maximum_age); + SET_ERROR_WITH_FIELD (wc, + ERROR_AGE_RESTRICTION_REQUIRED, + lowest_age); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if (! balance_ok) + { + GNUNET_break_op (0); + SET_ERROR_WITH_FIELD (wc, + ERROR_BATCH_INSUFFICIENT_FUNDS, + reserve_balance); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + /* Add information about each planchet in the batch */ + for (unsigned int i = 0; i<wc->num_coins; i++) + { + struct PlanchetContext *pc = &wc->planchets[i]; + const struct TALER_BlindedPlanchet *bp = &pc->blinded_planchet; + const union GNUNET_CRYPTO_BlindSessionNonce *nonce = NULL; + bool denom_unknown = true; + bool conflict = true; + bool nonce_reuse = true; + + switch (bp->blinded_message->cipher) + { + case GNUNET_CRYPTO_BSA_INVALID: + break; + case GNUNET_CRYPTO_BSA_RSA: + break; + case GNUNET_CRYPTO_BSA_CS: + nonce = (const union GNUNET_CRYPTO_BlindSessionNonce *) + &bp->blinded_message->details.cs_blinded_message.nonce; + break; + } + + qs = TEH_plugin->do_batch_withdraw_insert ( + TEH_plugin->cls, + nonce, + &pc->collectable, + wc->now, + ruuid, + &denom_unknown, + &conflict, + &nonce_reuse); + + if (0 > qs) + { + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + SET_ERROR_WITH_DETAIL (wc, + ERROR_DB_FETCH_FAILED, + db_fetch_context, + "do_batch_withdraw_insert"); + return qs; + } + + if (denom_unknown) + { + GNUNET_break (0); + SET_ERROR (wc, + ERROR_DB_INVARIANT_FAILURE); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) || + (conflict) ) + { + SET_ERROR (wc, + ERROR_BATCH_IDEMPOTENT_PLANCHET); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if (nonce_reuse) + { + GNUNET_break_op (0); + SET_ERROR (wc, + ERROR_BATCH_NONCE_RESUSE); + + return GNUNET_DB_STATUS_HARD_ERROR; + } + } + TEH_METRICS_num_success[TEH_MT_SUCCESS_WITHDRAW]++; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} + + +/** + * The request was prepared successfully. + * Run the main DB transaction. + * + * @param wc The context for the current withdraw request + */ +static void +phase_run_transaction ( + struct BatchWithdrawContext *wc) +{ + MHD_RESULT mhd_ret; + enum GNUNET_GenericReturnValue qs; + + GNUNET_assert (PHASE_RUN_TRANSACTION == + wc->phase); + + qs = TEH_DB_run_transaction (wc->rc->connection, + "run batch withdraw", + TEH_MT_REQUEST_WITHDRAW, + &mhd_ret, + &batch_withdraw_transaction, + wc); + if (GNUNET_OK != qs) + { + /* TODO[oec]: Logic still ok with new error handling? */ + if (PHASE_RUN_TRANSACTION == wc->phase) + finish_loop (wc, + mhd_ret); + return; + } + wc->phase++; +} + + +/** + * The request for batch withdraw was parsed successfully. + * Prepare our side for the main DB transaction. + * + * @param wc context for request processing, with @e withdraw_type set to WITHDRAW_TYPE_BATCH + * @return GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +batch_withdraw_phase_prepare_transaction (struct BatchWithdrawContext *wc) +{ + struct TALER_BlindedDenominationSignature bss[wc->num_coins]; + struct TEH_CoinSignData csds[wc->num_coins]; + + for (unsigned int i = 0; i<wc->num_coins; i++) + { + struct PlanchetContext *pc = &wc->planchets[i]; + struct TEH_CoinSignData *csdsi = &csds[i]; + + csdsi->h_denom_pub = &pc->collectable.denom_pub_hash; + csdsi->bp = &pc->blinded_planchet; + } + { + enum TALER_ErrorCode ec; + + ec = TEH_keys_denomination_batch_sign ( + wc->num_coins, + csds, + false, + bss); + if (TALER_EC_NONE != ec) + { + GNUNET_break (0); + SET_ERROR_WITH_FIELD (wc, + ERROR_DENOMINATION_BATCH_SIGN, + ec); + return GNUNET_SYSERR; + } + } + + for (unsigned int i = 0; i<wc->num_coins; i++) + { + struct PlanchetContext *pc = &wc->planchets[i]; + + pc->collectable.sig = bss[i]; + } + + return GNUNET_OK; +} + + +/** + * The request for withdraw was parsed successfully. + * Choose the appropriate preparation step depending on @e withdraw_type + */ +static void +phase_prepare_transaction ( + struct BatchWithdrawContext *wc) +{ + enum GNUNET_GenericReturnValue r; + r = batch_withdraw_phase_prepare_transaction (wc); + if (GNUNET_OK != r) + return; + wc->phase++; +} + + +/** + * Check the KYC result. + * + * @param wc context for request processing + */ +static void +phase_check_kyc_result (struct BatchWithdrawContext *wc) +{ + /* return final positive response */ + if (! wc->kyc.ok) + { + SET_ERROR (wc, + ERROR_KYC_REQUIRED); + return; + } + wc->phase++; +} + + +/** + * Function called with the result of a legitimization + * check. + * + * @param cls closure + * @param lcr legitimization check result + */ +static void +withdraw_legi_cb ( + void *cls, + const struct TEH_LegitimizationCheckResult *lcr) +{ + struct BatchWithdrawContext *wc = cls; + + wc->lch = NULL; + GNUNET_assert (PHASE_SUSPENDED == + wc->phase); + MHD_resume_connection (wc->rc->connection); + GNUNET_CONTAINER_DLL_remove (wc_head, + wc_tail, + wc); + TALER_MHD_daemon_trigger (); + if (NULL != lcr->response) + { + wc->error.details.legi.response = lcr->response; + wc->error.details.legi.http_status = lcr->http_status; + SET_ERROR (wc, + ERROR_LEGITIMIZATION_RESULT); + return; + } + wc->kyc = lcr->kyc; + wc->phase = PHASE_CHECK_KYC_RESULT; +} + + +/** + * Function called to iterate over KYC-relevant transaction amounts for a + * particular time range. Called within a database transaction, so must + * not start a new one. + * + * @param cls closure, identifies the event type and account to iterate + * over events for + * @param limit maximum time-range for which events should be fetched + * (timestamp in the past) + * @param cb function to call on each event found, events must be returned + * in reverse chronological order + * @param cb_cls closure for @a cb, of type struct BatchWithdrawContext + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +withdraw_amount_cb ( + void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + struct BatchWithdrawContext *wc = cls; + enum GNUNET_GenericReturnValue ret; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Signaling amount %s for KYC check during batch-withdrawal\n", + TALER_amount2s (&wc->batch_total)); + + ret = cb (cb_cls, + &wc->batch_total, + wc->now.abs_time); + + GNUNET_break (GNUNET_SYSERR != ret); + + if (GNUNET_OK != ret) + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + + qs = TEH_plugin->select_withdraw_amounts_for_kyc_check ( + TEH_plugin->cls, + &wc->h_normalized_payto, + limit, + cb, + cb_cls); + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got %d additional transactions for this batch-withdrawal and limit %llu\n", + qs, + (unsigned long long) limit.abs_value_us); + + GNUNET_break (qs >= 0); + + return qs; +} + + +/** + * Do legitimization check. + * + * @param wc operation context + */ +static void +phase_run_legi_check (struct BatchWithdrawContext *wc) +{ + enum GNUNET_DB_QueryStatus qs; + struct TALER_FullPayto payto_uri; + struct TALER_FullPaytoHashP h_full_payto; + + /* Check if the money came from a wire transfer */ + qs = TEH_plugin->reserves_get_origin ( + TEH_plugin->cls, + &wc->reserve_pub, + &h_full_payto, + &payto_uri); + if (qs < 0) + { + SET_ERROR_WITH_DETAIL (wc, + ERROR_DB_FETCH_FAILED, + db_fetch_context, + "reserves_get_origin"); + return; + } + /* If _no_ results, reserve was created by merge, + in which case no KYC check is required as the + merge already did that. */ + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + wc->phase = PHASE_PREPARE_TRANSACTION; + return; + } + TALER_full_payto_normalize_and_hash (payto_uri, + &wc->h_normalized_payto); + wc->lch = TEH_legitimization_check ( + &wc->rc->async_scope_id, + TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW, + payto_uri, + &wc->h_normalized_payto, + NULL, /* no account pub: this is about the origin account */ + &withdraw_amount_cb, + wc, + &withdraw_legi_cb, + wc); + GNUNET_assert (NULL != wc->lch); + GNUNET_free (payto_uri.full_payto); + GNUNET_CONTAINER_DLL_insert (wc_head, + wc_tail, + wc); + MHD_suspend_connection (wc->rc->connection); + wc->phase = PHASE_SUSPENDED; +} + + +/** + * Check if the given denomination is still or already valid, has not been + * revoked and potentically supports age restriction. + * + * @param[in,out] wc context for the withdraw operation + * @param ksh The handle to the current state of (denomination) keys in the exchange + * @param denom_h Hash of the denomination key to check + * @param[out] pdk denomination key found, might be NULL + * @return GNUNET_OK when denomation was found and valid, + * GNUNET_NO when denomination was not valid but request was idempotent, + * GNUNET_SYSERR otherwise (denomination invalid), with finish_loop called. + */ +static enum GNUNET_GenericReturnValue +find_denomination ( + struct BatchWithdrawContext *wc, + struct TEH_KeyStateHandle *ksh, + const struct TALER_DenominationHashP *denom_h, + struct TEH_DenominationKey **pdk) +{ + struct TEH_DenominationKey *dk; + + *pdk = NULL; + + dk = TEH_keys_denomination_by_hash_from_state ( + ksh, + denom_h, + NULL, + NULL); + + if (NULL == dk) + { + SET_ERROR_WITH_FIELD (wc, + ERROR_DENOMINATION_KEY_UNKNOWN, + denom_h); + return GNUNET_NO; + } + + if (GNUNET_TIME_absolute_is_past ( + dk->meta.expire_withdraw.abs_time)) + { + SET_ERROR_WITH_FIELD (wc, + ERROR_DENOMINATION_EXPIRED, + denom_h); + return GNUNET_SYSERR; + } + + if (GNUNET_TIME_absolute_is_future ( + dk->meta.start.abs_time)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_FIELD (wc, + ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + denom_h); + return GNUNET_SYSERR; + } + + if (dk->recoup_possible) + { + SET_ERROR (wc, + ERROR_DENOMINATION_REVOKED); + return GNUNET_SYSERR; + } + + *pdk = dk; + return GNUNET_OK; +} + + +/** + * Check if the keys in the request are valid for batch withdrawal. + * + * @param[in,out] wc context for the batch withdraw request processing + * @param ksh key state handle + * @return GNUNET_OK on success, + * GNUNET_NO on error (and response being sent) + */ +static enum GNUNET_GenericReturnValue +batch_withdraw_phase_check_keys ( + struct BatchWithdrawContext *wc, + struct TEH_KeyStateHandle *ksh) +{ + for (unsigned int i = 0; i<wc->num_coins; i++) + { + struct PlanchetContext *pc = &wc->planchets[i]; + struct TEH_DenominationKey *dk; + enum GNUNET_GenericReturnValue r; + + r = find_denomination (wc, + ksh, + &pc->collectable.denom_pub_hash, + &dk); + + if (GNUNET_OK != r) + return GNUNET_NO; + + GNUNET_assert (NULL != dk); + + if (dk->denom_pub.bsign_pub_key->cipher != + pc->blinded_planchet.blinded_message->cipher) + { + /* denomination cipher and blinded planchet cipher not the same */ + GNUNET_break_op (0); + SET_ERROR (wc, + ERROR_CIPHER_MISMATCH); + return GNUNET_NO; + } + + if (0 > + TALER_amount_add (&pc->collectable.amount_with_fee, + &dk->meta.value, + &dk->meta.fees.withdraw)) + { + GNUNET_break (0); + SET_ERROR (wc, + ERROR_BATCH_AMOUNT_FEE_OVERFLOW); + return GNUNET_NO; + } + + if (0 > + TALER_amount_add (&wc->batch_total, + &wc->batch_total, + &pc->collectable.amount_with_fee)) + { + GNUNET_break (0); + SET_ERROR (wc, + ERROR_BATCH_AMOUNT_FEE_OVERFLOW); + return GNUNET_NO; + } + + TALER_coin_ev_hash (&pc->blinded_planchet, + &pc->collectable.denom_pub_hash, + &pc->collectable.h_coin_envelope); + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_withdraw_verify_pre26 ( + &pc->collectable.denom_pub_hash, + &pc->collectable.amount_with_fee, + &pc->collectable.h_coin_envelope, + &pc->collectable.reserve_pub, + &pc->collectable.reserve_sig)) + { + GNUNET_break_op (0); + SET_ERROR (wc, + ERROR_RESERVE_SIGNATURE_INVALID); + return GNUNET_NO; + } + } + /* everything parsed */ + + return GNUNET_OK; +} + + +/** + * Check if the keys in the request are valid for withdrawing. + * + * @param[in,out] wc context for request processing + */ +static void +phase_check_keys (struct BatchWithdrawContext *wc) +{ + struct TEH_KeyStateHandle *ksh; + enum GNUNET_GenericReturnValue r; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + GNUNET_break (0); + SET_ERROR (wc, + ERROR_KEYS_MISSING); + return; + } + + r = batch_withdraw_phase_check_keys (wc, ksh); + + switch (r) + { + case GNUNET_OK: + wc->phase++; + break; + case GNUNET_NO: + /* error generated by function, simply return*/ + break; + case GNUNET_SYSERR: + GNUNET_break (0); + SET_ERROR (wc, + ERROR_KEYS_MISSING); + break; + default: + GNUNET_break (0); + } +} + + +/** + * Cleanup routine for withdraw reqwuest. + * The function is called upon completion of the request + * that should clean up @a rh_ctx. Can be NULL. + * + * @param rc request context to clean up + */ +static void +clean_withdraw_rc (struct TEH_RequestContext *rc) +{ + struct BatchWithdrawContext *wc = rc->rh_ctx; + + if (NULL != wc->lch) + { + TEH_legitimization_check_cancel (wc->lch); + wc->lch = NULL; + } + + for (unsigned int i = 0; i<wc->num_coins; i++) + { + struct PlanchetContext *pc = &wc->planchets[i]; + + TALER_blinded_planchet_free (&pc->blinded_planchet); + TALER_blinded_denom_sig_free (&pc->collectable.sig); + } + GNUNET_free (wc->planchets); + + if (ERROR_LEGITIMIZATION_RESULT == wc->error.code && + NULL != wc->error.details.legi.response) + { + MHD_destroy_response (wc->error.details.legi.response); + wc->error.details.legi.response = NULL; + } + + GNUNET_free (wc); +} + + +/** + * Reports an error, potentially with details. + * That is, it puts a error-type specific response into the MHD queue. + * It will do a idempotency check first, if needed for the error type. + * + * @param wc withdraw context + */ +static void +phase_generate_reply_error ( + struct BatchWithdrawContext *wc) +{ + GNUNET_assert (PHASE_GENERATE_REPLY_ERROR == wc->phase); + GNUNET_assert (ERROR_NONE != wc->error.code); + GNUNET_assert (ERROR_MAX != wc->error.code); + + if (needs_idempotency_check[wc->error.code] && + check_idempotency (wc)) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "request is idempotent\n"); + return; + } + + switch (wc->error.code) + { + case ERROR_MAX: + case ERROR_NONE: + { + GNUNET_break (0); + wc->phase = PHASE_RETURN_YES; + return; + } + + case ERROR_REQUEST_PARAMETER_MALFORMED: + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + wc->error.details.hint); + break; + + case ERROR_KEYS_MISSING: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL)); + break; + + case ERROR_DB_FETCH_FAILED: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + wc->error.details.db_fetch_context)); + break; + + case ERROR_DB_INVARIANT_FAILURE: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + NULL)); + break; + + case ERROR_INTERNAL_INVARIANT_FAILURE: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL)); + break; + + case ERROR_RESERVE_UNKNOWN: + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + NULL)); + break; + + case ERROR_DENOMINATION_BATCH_SIGN: + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + wc->error.details.ec, + NULL)); + break; + + case ERROR_KYC_REQUIRED: + finish_loop (wc, + TEH_RESPONSE_reply_kyc_required ( + wc->rc->connection, + &wc->h_normalized_payto, + &wc->kyc, + false)); + break; + + case ERROR_DENOMINATION_KEY_UNKNOWN: + { + GNUNET_break_op (0); + finish_loop (wc, + TEH_RESPONSE_reply_unknown_denom_pub_hash ( + wc->rc->connection, + wc->error.details.denom_h)); + break; + } + + case ERROR_DENOMINATION_EXPIRED: + GNUNET_break_op (0); + finish_loop (wc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + wc->rc->connection, + wc->error.details.denom_h, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "batch-withdraw")); + break; + + case ERROR_DENOMINATION_VALIDITY_IN_FUTURE: + finish_loop (wc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + wc->rc->connection, + wc->error.details.denom_h, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "batch-withdraw")); + break; + + case ERROR_DENOMINATION_REVOKED: + GNUNET_break_op (0); + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, + "batch-withdraw")); + break; + + case ERROR_CIPHER_MISMATCH: + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, + NULL)); + break; + + case ERROR_AGE_RESTRICTION_REQUIRED: + finish_loop (wc, + TEH_RESPONSE_reply_reserve_age_restriction_required ( + wc->rc->connection, + wc->error.details.lowest_age)); + break; + + case ERROR_BATCH_INSUFFICIENT_FUNDS: + finish_loop (wc, + TEH_RESPONSE_reply_reserve_insufficient_balance ( + wc->rc->connection, + TALER_EC_EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS, + &wc->error.details.reserve_balance, + &wc->batch_total, + &wc->reserve_pub)); + break; + + case ERROR_BATCH_IDEMPOTENT_PLANCHET: + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Idempotent coin in batch, not allowed. Aborting.\n"); + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_WITHDRAW_IDEMPOTENT_PLANCHET, + NULL)); + break; + } + + case ERROR_BATCH_NONCE_RESUSE: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_WITHDRAW_NONCE_REUSE, + NULL)); + break; + + case ERROR_BATCH_AMOUNT_FEE_OVERFLOW: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW, + NULL)); + break; + + case ERROR_RESERVE_SIGNATURE_INVALID: + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID, + NULL)); + break; + + case ERROR_LEGITIMIZATION_RESULT: { + finish_loop (wc, + MHD_queue_response (wc->rc->connection, + wc->error.details.legi.http_status, + wc->error.details.legi.response)); + break; + } + } +} + + +/** + * Creates a new context for the incoming batch-withdraw request + * + * @param[in,out] wc context of the batch-witrhdraw, to be filled + * @param root json body of the request + * @return GNUNET_OK on success, GNUNET_SYSERR otherwise (response sent) + */ +static enum GNUNET_GenericReturnValue +batch_withdraw_new_request ( + struct BatchWithdrawContext *wc, + const json_t *root) +{ + const json_t *planchets; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &wc->batch_total)); + + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("planchets", + &planchets), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (wc->rc->connection, + root, + spec); + if (GNUNET_OK != res) + return res; + } + } + + wc->num_coins = json_array_size (planchets); + if (0 == wc->num_coins) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (wc, + ERROR_REQUEST_PARAMETER_MALFORMED, + hint, + "planchets"); + return GNUNET_SYSERR; + } + + if (wc->num_coins > TALER_MAX_FRESH_COINS) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (wc, + ERROR_REQUEST_PARAMETER_MALFORMED, + hint, + "too many planchets"); + return GNUNET_SYSERR; + } + + wc->planchets + = GNUNET_new_array (wc->num_coins, + struct PlanchetContext); + + for (unsigned int i = 0; i<wc->num_coins; i++) + { + struct PlanchetContext *pc = &wc->planchets[i]; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_fixed_auto ( + "reserve_sig", + &pc->collectable.reserve_sig), + GNUNET_JSON_spec_fixed_auto ( + "denom_pub_hash", + &pc->collectable.denom_pub_hash), + TALER_JSON_spec_blinded_planchet ( + "coin_ev", + &pc->blinded_planchet), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data ( + wc->rc->connection, + json_array_get (planchets, i), + ispec); + if (GNUNET_OK != res) + return res; + } + + pc->collectable.reserve_pub = wc->reserve_pub; + for (unsigned int k = 0; k<i; k++) + { + const struct PlanchetContext *kpc = &wc->planchets[k]; + + if (0 == + TALER_blinded_planchet_cmp ( + &kpc->blinded_planchet, + &pc->blinded_planchet)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (wc, + ERROR_REQUEST_PARAMETER_MALFORMED, + hint, + "duplicate planchet"); + return GNUNET_SYSERR; + } + } + } + return GNUNET_OK; +} + + +MHD_RESULT +TEH_handler_batch_withdraw ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root) +{ + struct BatchWithdrawContext *wc = rc->rh_ctx; + enum GNUNET_GenericReturnValue r; + + if (NULL == wc) + { + wc = GNUNET_new (struct BatchWithdrawContext); + rc->rh_ctx = wc; + rc->rh_cleaner = &clean_withdraw_rc; + wc->rc = rc; + wc->now = GNUNET_TIME_timestamp_get (); + wc->reserve_pub = *reserve_pub; + + r = batch_withdraw_new_request (wc, root); + + if (GNUNET_OK != r) + return (GNUNET_SYSERR == r) ? MHD_NO : MHD_YES; + + wc->phase = PHASE_CHECK_KEYS; + } + + while (true) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "batch-withdraw processing in phase %d\n", + wc->phase); + + switch (wc->phase) + { + case PHASE_CHECK_KEYS: + phase_check_keys (wc); + break; + case PHASE_CHECK_RESERVE_SIGNATURE: + /* signature checks has occurred in batch_withdraw_phase_check_keys */ + wc->phase++; + break; + case PHASE_RUN_LEGI_CHECK: + phase_run_legi_check (wc); + break; + case PHASE_SUSPENDED: + return MHD_YES; + case PHASE_CHECK_KYC_RESULT: + phase_check_kyc_result (wc); + break; + case PHASE_PREPARE_TRANSACTION: + phase_prepare_transaction (wc); + break; + case PHASE_RUN_TRANSACTION: + phase_run_transaction (wc); + break; + case PHASE_GENERATE_REPLY_SUCCESS: + phase_generate_reply_success (wc); + break; + case PHASE_GENERATE_REPLY_ERROR: + phase_generate_reply_error (wc); + break; + case PHASE_RETURN_YES: + return MHD_YES; + case PHASE_RETURN_NO: + return MHD_NO; + } + } +} + + +/* end of taler-exchange-httpd_batch-withdraw.c */ diff --git a/src/exchange/taler-exchange-httpd_batch-withdraw.h b/src/exchange/taler-exchange-httpd_batch-withdraw.h @@ -0,0 +1,62 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +/* + * NOTE: This endpoint is deprecated starting with v24 of the protocol and + * will be removed, including this file. + */ + +/** + * @file taler-exchange-httpd_batch-withdraw.h + * @brief Handle /reserve/$RESERVE_PUB/batch-withdraw requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + * @author Özgür Kesim + */ +#ifndef TALER_EXCHANGE_HTTPD_BATCH_WITHDRAW_H +#define TALER_EXCHANGE_HTTPD_BATCH_WITHDRAW_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + +/** + * Resume suspended connections, we are shutting down. + */ +void +TEH_batch_withdraw_cleanup (void); + + +/** + * Handle a "/reserves/$RESERVE_PUB/batch-withdraw" request. Parses the batch of + * requested "denom_pub" which specifies the key/value of the coin to be + * withdrawn, and checks that the signature "reserve_sig" makes this a valid + * withdrawal request from the specified reserve. If so, the envelope with + * the blinded coin "coin_ev" is passed down to execute the withdrawal + * operation. + * + * @param rc request context + * @param root uploaded JSON data + * @param reserve_pub public key of the reserve + * @return MHD result code + */ +MHD_RESULT +TEH_handler_batch_withdraw ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root); + +#endif diff --git a/src/exchange/taler-exchange-httpd_blinding-prepare.c b/src/exchange/taler-exchange-httpd_blinding-prepare.c @@ -0,0 +1,212 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation; either version 3, + or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty + of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General + Public License along with TALER; see the file COPYING. If not, + see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_blinding-prepare.c + * @brief Handle /blinding-prepare requests + * @author Özgür Kesim + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_blinding-prepare.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_keys.h" + +MHD_RESULT +TEH_handler_blinding_prepare (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[]) +{ + const json_t *j_nonces; + const json_t *j_denoms_h; + const char *cipher; + size_t num; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("cipher", + &cipher), + GNUNET_JSON_spec_array_const ("nonces", + &j_nonces), + GNUNET_JSON_spec_array_const ("denoms_h", + &j_denoms_h), + GNUNET_JSON_spec_end () + }; + + (void) args; + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_OK != res) + return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; + } + + num = json_array_size (j_nonces); + + { + const char *error = NULL; + + if (num != json_array_size (j_denoms_h)) + { + GNUNET_break_op (0); + error = "arrays not of equal size"; + } + else if (0 == num || TALER_MAX_FRESH_COINS < num) + { + GNUNET_break_op (0); + error = "invalid number of entries"; + } + else if (0 != strcmp (cipher, "CS")) + { + GNUNET_break_op (0); + error = "wrong cipher"; + } + + if (NULL != error) + { + TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + error); + } + } + + { + struct GNUNET_CRYPTO_CsSessionNonce nonces[num]; + struct TALER_DenominationHashP h_denom_pubs[num]; + size_t idx; + + + for (idx = 0; idx < num; idx++) + { + struct GNUNET_JSON_Specification denom_spec[] = { + GNUNET_JSON_spec_fixed_auto (NULL, + &h_denom_pubs[idx]), + GNUNET_JSON_spec_end () + }; + struct GNUNET_JSON_Specification nonce_spec[] = { + GNUNET_JSON_spec_fixed_auto (NULL, + &nonces[idx]), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_array (rc->connection, + j_denoms_h, + denom_spec, + idx, + -1); + if (GNUNET_OK != res) + return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; + + res = TALER_MHD_parse_json_array (rc->connection, + j_nonces, + nonce_spec, + idx, + -1); + if (GNUNET_OK != res) + return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; + } + + { + struct TEH_KeyStateHandle *ksh; + struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[num]; + size_t err_idx; + enum TALER_ErrorCode ec; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + + ec = + TEH_keys_denomination_cs_batch_r_pub (ksh, + num, + h_denom_pubs, + nonces, + false, + r_pubs, + &err_idx); + switch (ec) + { + case TALER_EC_NONE: + break; + + case TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN: + return TEH_RESPONSE_reply_unknown_denom_pub_hash ( + rc->connection, + &h_denom_pubs[idx]); + break; + + case TALER_EC_EXCHANGE_GENERIC_INVALID_DENOMINATION_CIPHER_FOR_OPERATION: + return TEH_RESPONSE_reply_invalid_denom_cipher_for_operation ( + rc->connection, + &h_denom_pubs[idx]); + break; + + case TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED: + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + rc->connection, + &h_denom_pubs[idx], + ec, + "blinding-prepare"); + break; + + case TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE: + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + rc->connection, + &h_denom_pubs[idx], + ec, + "blinding-prepare"); + break; + + default: + GNUNET_break (0); + return TALER_MHD_reply_with_ec (rc->connection, + ec, + NULL); + break; + } + + /* Finally, create the response */ + { + struct TALER_BlindingPrepareResponse response = { + .num = num, + .cipher = GNUNET_CRYPTO_BSA_CS, + .details.cs = r_pubs, + }; + + return TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + TALER_JSON_pack_blinding_prepare_response (NULL, + &response)); + } + } + } +} + + +/* end of taler-exchange-httpd_blinding_prepare.c */ diff --git a/src/exchange/taler-exchange-httpd_blinding-prepare.h b/src/exchange/taler-exchange-httpd_blinding-prepare.h @@ -0,0 +1,40 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_blinding-prepare.h + * @brief Handle /blinding-prepare requests + * @author Özgür Kesim + */ +#ifndef TALER_EXCHANGE_HTTPD_BLINDING_PREPARE_H +#define TALER_EXCHANGE_HTTPD_BLINDING_PREPARE_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + +/** + * Handle a "/blinding-prepare" request. + * + * @param rc request context + * @param root uploaded JSON data + * @param args empty array + * @return MHD result code + */ +MHD_RESULT +TEH_handler_blinding_prepare (struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[]); + +#endif diff --git a/src/exchange/taler-exchange-httpd_csr.c b/src/exchange/taler-exchange-httpd_csr.c @@ -178,10 +178,11 @@ TEH_handler_csr_melt (struct TEH_RequestContext *rc, cdds[i].h_denom_pub = denom_pub_hash; cdds[i].nonce = nonce; } /* for (i) */ - ec = TEH_keys_denomination_cs_batch_r_pub (csr_requests_num, - cdds, - true, - r_pubs); + ec = TEH_keys_denomination_cs_batch_r_pub_simple ( + csr_requests_num, + cdds, + true, + r_pubs); if (TALER_EC_NONE != ec) { GNUNET_break (0); diff --git a/src/exchange/taler-exchange-httpd_keys.c b/src/exchange/taler-exchange-httpd_keys.c @@ -3658,7 +3658,7 @@ TEH_keys_denomination_cs_r_pub ( enum TALER_ErrorCode -TEH_keys_denomination_cs_batch_r_pub ( +TEH_keys_denomination_cs_batch_r_pub_simple ( unsigned int cdds_length, const struct TEH_CsDeriveData cdds[static cdds_length], bool for_melt, @@ -3701,6 +3701,65 @@ TEH_keys_denomination_cs_batch_r_pub ( } +enum TALER_ErrorCode +TEH_keys_denomination_cs_batch_r_pub ( + const struct TEH_KeyStateHandle *ksh, + size_t num, + const struct TALER_DenominationHashP h_denom_pubs[static num], + const struct GNUNET_CRYPTO_CsSessionNonce nonces[static num], + bool for_melt, + struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[static num], + size_t *err_idx) +{ + struct TALER_CRYPTO_CsDeriveRequest cdrs[num]; + + for (unsigned int i = 0; i<num; i++) + { + const struct TEH_DenominationKey *dk; + const struct HelperDenomination *hd; + + *err_idx = i; + + /* FIXME: right now we need both, + * TEH_DenominationKey and HelperDenomination, + * because only TEH_DenominationKey has .recoup_possible + */ + hd = GNUNET_CONTAINER_multihashmap_get (ksh->helpers->denom_keys, + &h_denom_pubs[i].hash); + dk = TEH_keys_denomination_by_hash_from_state (ksh, + &h_denom_pubs[i], + NULL, + NULL); + if (NULL == hd) + return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; + + GNUNET_assert (NULL != dk); + + if (GNUNET_CRYPTO_BSA_CS != + hd->denom_pub.bsign_pub_key->cipher) + return TALER_EC_EXCHANGE_GENERIC_INVALID_DENOMINATION_CIPHER_FOR_OPERATION; + + if (GNUNET_TIME_absolute_is_future (hd->start_time.abs_time)) + return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE; + + if (dk->recoup_possible) + return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED; + + if (GNUNET_TIME_relative_is_zero (hd->validity_duration)) + return TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED; + + cdrs[i].h_cs = &hd->h_details.h_cs; + cdrs[i].nonce = &nonces[i]; + } + + return TALER_CRYPTO_helper_cs_r_batch_derive (ksh->helpers->csdh, + num, + cdrs, + for_melt, + r_pubs); +} + + void TEH_keys_denomination_revoke (const struct TALER_DenominationHashP *h_denom_pub) { diff --git a/src/exchange/taler-exchange-httpd_keys.h b/src/exchange/taler-exchange-httpd_keys.h @@ -360,7 +360,7 @@ TEH_keys_denomination_cs_r_pub ( * @return #TALER_EC_NONE on success */ enum TALER_ErrorCode -TEH_keys_denomination_cs_batch_r_pub ( +TEH_keys_denomination_cs_batch_r_pub_simple ( unsigned int cdds_length, const struct TEH_CsDeriveData cdds[static cdds_length], bool for_melt, @@ -368,6 +368,29 @@ TEH_keys_denomination_cs_batch_r_pub ( /** + * Request to derive a bunch of CS @a r_pubs using the + * denominations and nonces from @a cdds. + * + * @param ksh keys state to load the keys from + * @param num number of input elements + * @param h_denom_pubs array @a num of hashes of keys to sign with + * @param nonces array @a num of nonces to use + * @param for_melt true if this is for a melt operation + * @param[out] r_pubs array where to write the result; must be of length @a num + * @param[out] err_idx in case of error, the index into @e cdds that caused it + * @return #TALER_EC_NONE on success + */ +enum TALER_ErrorCode +TEH_keys_denomination_cs_batch_r_pub ( + const struct TEH_KeyStateHandle *ksh, + size_t num, + const struct TALER_DenominationHashP h_denom_pubs[static num], + const struct GNUNET_CRYPTO_CsSessionNonce nonces[static num], + bool for_melt, + struct GNUNET_CRYPTO_CSPublicRPairP r_pubs[static num], + size_t *err_idx); + +/** * Revoke the public key associated with @a h_denom_pub. * This function should be called AFTER the database was * updated, as it also triggers #TEH_keys_update_states(). diff --git a/src/exchange/taler-exchange-httpd_metrics.c b/src/exchange/taler-exchange-httpd_metrics.c @@ -31,7 +31,7 @@ unsigned long long TEH_METRICS_num_requests[TEH_MT_REQUEST_COUNT]; -unsigned long long TEH_METRICS_batch_withdraw_num_coins; +unsigned long long TEH_METRICS_withdraw_num_coins; unsigned long long TEH_METRICS_num_conflict[TEH_MT_REQUEST_COUNT]; @@ -58,7 +58,6 @@ TEH_handler_metrics (struct TEH_RequestContext *rc, "taler_exchange_success_transactions{type=\"%s\"} %llu\n" "taler_exchange_success_transactions{type=\"%s\"} %llu\n" "taler_exchange_success_transactions{type=\"%s\"} %llu\n" - "taler_exchange_success_transactions{type=\"%s\"} %llu\n" "# HELP taler_exchange_serialization_failures " " number of database serialization errors by type\n" "# TYPE taler_exchange_serialization_failures counter\n" @@ -78,7 +77,6 @@ TEH_handler_metrics (struct TEH_RequestContext *rc, "taler_exchange_idempotent_requests{type=\"%s\"} %llu\n" "taler_exchange_idempotent_requests{type=\"%s\"} %llu\n" #endif - "taler_exchange_idempotent_requests{type=\"%s\"} %llu\n" "# HELP taler_exchange_num_signatures " " number of signatures created by cipher\n" "# TYPE taler_exchange_num_signatures counter\n" @@ -95,16 +93,14 @@ TEH_handler_metrics (struct TEH_RequestContext *rc, " number of key exchanges done by cipher\n" "# TYPE taler_exchange_num_keyexchanges counter\n" "taler_exchange_num_keyexchanges{type=\"%s\"} %llu\n" - "# HELP taler_exchange_batch_withdraw_num_coins " - " number of coins withdrawn in a batch-withdraw request\n" - "# TYPE taler_exchange_batch_withdraw_num_coins counter\n" - "taler_exchange_batch_withdraw_num_coins{} %llu\n", + "# HELP taler_exchange_withdraw_num_coins " + " number of coins withdrawn in a withdraw request\n" + "# TYPE taler_exchange_withdraw_num_coins counter\n" + "taler_exchange_withdraw_num_coins{} %llu\n", "deposit", TEH_METRICS_num_success[TEH_MT_SUCCESS_DEPOSIT], "withdraw", TEH_METRICS_num_success[TEH_MT_SUCCESS_WITHDRAW], - "batch-withdraw", - TEH_METRICS_num_success[TEH_MT_SUCCESS_BATCH_WITHDRAW], "melt", TEH_METRICS_num_success[TEH_MT_SUCCESS_MELT], "refresh-reveal", @@ -133,9 +129,6 @@ TEH_handler_metrics (struct TEH_RequestContext *rc, "melt", TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_MELT], #endif - "batch-withdraw", - TEH_METRICS_num_requests[ - TEH_MT_REQUEST_IDEMPOTENT_BATCH_WITHDRAW], "rsa", TEH_METRICS_num_signatures[TEH_MT_SIGNATURE_RSA], "cs", @@ -150,7 +143,7 @@ TEH_handler_metrics (struct TEH_RequestContext *rc, TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA], "ecdh", TEH_METRICS_num_keyexchanges[TEH_MT_KEYX_ECDH], - TEH_METRICS_batch_withdraw_num_coins); + TEH_METRICS_withdraw_num_coins); resp = MHD_create_response_from_buffer (strlen (reply), reply, MHD_RESPMEM_MUST_FREE); diff --git a/src/exchange/taler-exchange-httpd_metrics.h b/src/exchange/taler-exchange-httpd_metrics.h @@ -34,20 +34,17 @@ enum TEH_MetricTypeRequest TEH_MT_REQUEST_OTHER = 0, TEH_MT_REQUEST_DEPOSIT = 1, TEH_MT_REQUEST_WITHDRAW = 2, - TEH_MT_REQUEST_AGE_WITHDRAW = 3, - TEH_MT_REQUEST_MELT = 4, - TEH_MT_REQUEST_PURSE_CREATE = 5, - TEH_MT_REQUEST_PURSE_MERGE = 6, - TEH_MT_REQUEST_RESERVE_PURSE = 7, - TEH_MT_REQUEST_PURSE_DEPOSIT = 8, - TEH_MT_REQUEST_IDEMPOTENT_DEPOSIT = 9, - TEH_MT_REQUEST_IDEMPOTENT_WITHDRAW = 10, - TEH_MT_REQUEST_IDEMPOTENT_AGE_WITHDRAW = 11, - TEH_MT_REQUEST_IDEMPOTENT_MELT = 12, - TEH_MT_REQUEST_IDEMPOTENT_BATCH_WITHDRAW = 13, - TEH_MT_REQUEST_BATCH_DEPOSIT = 14, - TEH_MT_REQUEST_POLICY_FULFILLMENT = 15, - TEH_MT_REQUEST_COUNT = 16 /* MUST BE LAST! */ + TEH_MT_REQUEST_MELT = 3, + TEH_MT_REQUEST_PURSE_CREATE = 4, + TEH_MT_REQUEST_PURSE_MERGE = 5, + TEH_MT_REQUEST_RESERVE_PURSE = 6, + TEH_MT_REQUEST_PURSE_DEPOSIT = 7, + TEH_MT_REQUEST_IDEMPOTENT_DEPOSIT = 8, + TEH_MT_REQUEST_IDEMPOTENT_WITHDRAW = 9, + TEH_MT_REQUEST_IDEMPOTENT_MELT = 10, + TEH_MT_REQUEST_BATCH_DEPOSIT = 11, + TEH_MT_REQUEST_POLICY_FULFILLMENT = 12, + TEH_MT_REQUEST_COUNT = 13 /* MUST BE LAST! */ }; /** @@ -57,12 +54,10 @@ enum TEH_MetricTypeSuccess { TEH_MT_SUCCESS_DEPOSIT = 0, TEH_MT_SUCCESS_WITHDRAW = 1, - TEH_MT_SUCCESS_AGE_WITHDRAW = 2, - TEH_MT_SUCCESS_BATCH_WITHDRAW = 3, - TEH_MT_SUCCESS_MELT = 4, - TEH_MT_SUCCESS_REFRESH_REVEAL = 5, - TEH_MT_SUCCESS_AGE_WITHDRAW_REVEAL = 6, - TEH_MT_SUCCESS_COUNT = 7 /* MUST BE LAST! */ + TEH_MT_SUCCESS_MELT = 2, + TEH_MT_SUCCESS_REFRESH_REVEAL = 3, + TEH_MT_SUCCESS_WITHDRAW_REVEAL = 4, + TEH_MT_SUCCESS_COUNT = 5 /* MUST BE LAST! */ }; /** @@ -96,9 +91,9 @@ extern unsigned long long TEH_METRICS_num_requests[TEH_MT_REQUEST_COUNT]; extern unsigned long long TEH_METRICS_num_success[TEH_MT_SUCCESS_COUNT]; /** - * Number of coins withdrawn in a batch-withdraw request + * Number of coins withdrawn in a withdraw request */ -extern unsigned long long TEH_METRICS_batch_withdraw_num_coins; +extern unsigned long long TEH_METRICS_withdraw_num_coins; /** * Number of serialization errors encountered when diff --git a/src/exchange/taler-exchange-httpd_recoup.c b/src/exchange/taler-exchange-httpd_recoup.c @@ -66,9 +66,9 @@ struct RecoupContext const struct TALER_CoinSpendSignatureP *coin_sig; /** - * Unique ID of the withdraw operation in the reserves_out table. + * Unique ID of the withdraw operation in the withdraw table. */ - uint64_t reserve_out_serial_id; + uint64_t withdraw_serial_id; /** * Unique ID of the coin in the known_coins table. @@ -115,7 +115,7 @@ recoup_transaction (void *cls, pc->now = GNUNET_TIME_timestamp_get (); qs = TEH_plugin->do_recoup (TEH_plugin->cls, &pc->reserve_pub, - pc->reserve_out_serial_id, + pc->withdraw_serial_id, pc->coin_bks, &pc->coin->coin_pub, pc->known_coin_id, @@ -169,6 +169,7 @@ recoup_transaction (void *cls, * @param exchange_vals values contributed by the exchange * during withdrawal * @param coin_bks blinding data of the coin (to be checked) + * @param h_commitment The hash of the commitment of the original withdraw request * @param nonce coin's nonce if CS is used * @param coin_sig signature of the coin * @return MHD result code @@ -179,6 +180,7 @@ verify_and_execute_recoup ( const struct TALER_CoinPublicInfo *coin, const struct TALER_ExchangeWithdrawValues *exchange_vals, const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, + const struct TALER_WithdrawCommitmentHashP *h_commitment, const union GNUNET_CRYPTO_BlindSessionNonce *nonce, const struct TALER_CoinSpendSignatureP *coin_sig) { @@ -312,10 +314,11 @@ verify_and_execute_recoup ( { enum GNUNET_DB_QueryStatus qs; - qs = TEH_plugin->get_reserve_by_h_blind (TEH_plugin->cls, - &pc.h_coin_ev, - &pc.reserve_pub, - &pc.reserve_out_serial_id); + qs = TEH_plugin->get_reserve_by_h_commitment ( + TEH_plugin->cls, + h_commitment, + &pc.reserve_pub, + &pc.withdraw_serial_id); if (0 > qs) { GNUNET_break (0); @@ -323,7 +326,7 @@ verify_and_execute_recoup ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_reserve_by_h_blind"); + "get_reserve_by_commitment"); } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { @@ -381,6 +384,7 @@ TEH_handler_recoup (struct MHD_Connection *connection, union GNUNET_CRYPTO_BlindingSecretP coin_bks; struct TALER_CoinSpendSignatureP coin_sig; struct TALER_ExchangeWithdrawValues exchange_vals; + struct TALER_WithdrawCommitmentHashP withdraw_commitment_h = {0}; union GNUNET_CRYPTO_BlindSessionNonce nonce; bool no_nonce; struct GNUNET_JSON_Specification spec[] = { @@ -388,6 +392,8 @@ TEH_handler_recoup (struct MHD_Connection *connection, &coin.denom_pub_hash), TALER_JSON_spec_denom_sig ("denom_sig", &coin.denom_sig), + GNUNET_JSON_spec_fixed_auto ("withdraw_commitment_hash", + &withdraw_commitment_h), TALER_JSON_spec_exchange_withdraw_values ("ewv", &exchange_vals), GNUNET_JSON_spec_fixed_auto ("coin_blind_key_secret", @@ -423,6 +429,7 @@ TEH_handler_recoup (struct MHD_Connection *connection, &coin, &exchange_vals, &coin_bks, + &withdraw_commitment_h, no_nonce ? NULL : &nonce, diff --git a/src/exchange/taler-exchange-httpd_reserves_history.c b/src/exchange/taler-exchange-httpd_reserves_history.c @@ -76,27 +76,148 @@ compile_reserve_history ( } break; } - case TALER_EXCHANGEDB_RO_WITHDRAW_COIN: + case TALER_EXCHANGEDB_RO_WITHDRAW_COINS: { - const struct TALER_EXCHANGEDB_CollectableBlindcoin *withdraw + const struct TALER_EXCHANGEDB_Withdraw *withdraw = pos->details.withdraw; + struct TALER_Amount withdraw_fee; + struct TEH_KeyStateHandle *ksh; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero ( + TEH_currency, + &withdraw_fee)); + + /* + * We need to calculate the fees for the withdraw. + * Therefore, we need to get access to the key state. + */ + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + + for (size_t i = 0; i < withdraw->num_coins; i++) + { + /* Find the denomination and accumulate the fee */ + { + struct TEH_DenominationKey *dk; + dk = TEH_keys_denomination_by_hash_from_state ( + ksh, + &withdraw->denom_pub_hashes[i], + NULL, + NULL); + + if (NULL == dk) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + + if (0 > TALER_amount_add (&withdraw_fee, + &withdraw_fee, + &dk->meta.fees.withdraw)) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + } + + /* Prepare the entry for the history */ + { + json_t *j_entry = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ( + "type", + "WITHDRAW"), + GNUNET_JSON_pack_data_auto ( + "reserve_sig", + &withdraw->reserve_sig), + GNUNET_JSON_pack_data_auto ( + "h_commitment", + &withdraw->h_commitment), + GNUNET_JSON_pack_data_auto ( + "h_planchets", + &withdraw->h_planchets), + GNUNET_JSON_pack_uint64 ( + "num_coins", + withdraw->num_coins), + TALER_JSON_pack_array_of_data ( + "denom_pub_hashes", + withdraw->num_coins, + withdraw->denom_pub_hashes, + sizeof(withdraw->denom_pub_hashes[0])), + TALER_JSON_pack_array_of_data ( + "h_coin_evs", + withdraw->num_coins, + withdraw->h_coin_evs, + sizeof(withdraw->h_coin_evs[0])), + TALER_JSON_pack_amount ( + "withdraw_fee", + &withdraw_fee), + TALER_JSON_pack_amount ( + "amount", + &withdraw->amount_with_fee) + ); + + if (withdraw->age_restricted) + { + if (0 != + json_object_update_new ( + j_entry, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ( + "noreveal_index", + withdraw->noreveal_index), + GNUNET_JSON_pack_uint64 ( + "max_age", + withdraw->max_age)))) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + + if (0 != + json_array_append_new ( + json_history, + j_entry)) + { + GNUNET_break (0); + json_decref (json_history); + return NULL; + } + } + } + + break; + case TALER_EXCHANGEDB_RO_BATCH_WITHDRAW_COIN: + { + const struct TALER_EXCHANGEDB_CollectableBlindcoin *batch_withdraw + = pos->details.batch_withdraw; if (0 != json_array_append_new ( json_history, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("type", - "WITHDRAW"), + "BATCH_WITHDRAW"), GNUNET_JSON_pack_data_auto ("reserve_sig", - &withdraw->reserve_sig), + &batch_withdraw->reserve_sig), GNUNET_JSON_pack_data_auto ("h_coin_envelope", - &withdraw->h_coin_envelope), + &batch_withdraw->h_coin_envelope), GNUNET_JSON_pack_data_auto ("h_denom_pub", - &withdraw->denom_pub_hash), + &batch_withdraw->denom_pub_hash), TALER_JSON_pack_amount ("withdraw_fee", - &withdraw->withdraw_fee), + &batch_withdraw->withdraw_fee), TALER_JSON_pack_amount ("amount", - &withdraw->amount_with_fee)))) + &batch_withdraw->amount_with_fee)))) { GNUNET_break (0); json_decref (json_history); @@ -324,6 +445,7 @@ compile_reserve_history ( } } + return json_history; } diff --git a/src/exchange/taler-exchange-httpd_reveal-withdraw.c b/src/exchange/taler-exchange-httpd_reveal-withdraw.c @@ -0,0 +1,589 @@ +/* + This file is part of TALER + Copyright (C) 2023 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_reveal-withdraw.c + * @brief Handle /reveal-withdraw requests + * @author Özgür Kesim + */ +#include "platform.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include "taler-exchange-httpd_metrics.h" +#include "taler_error_codes.h" +#include "taler_exchangedb_plugin.h" +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_mhd.h" +#include "taler-exchange-httpd_reveal-withdraw.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_keys.h" + +/** + * State for an /reveal-withdraw operation. + */ +struct AgeRevealContext +{ + + /** + * Commitment for the withdraw operation, previously called by the + * client. + */ + struct TALER_WithdrawCommitmentHashP ach; + + /** + * TALER_CNC_KAPPA-1 secrets for disclosed coin batches. + */ + struct TALER_RevealWithdrawMasterSeedsP disclosed_batch_seeds; + + /** + * The data from the original withdraw. Will be retrieved from + * the DB via @a wch. + */ + struct TALER_EXCHANGEDB_Withdraw commitment; +}; + + +/** + * Parse the json body of an '/reveal-withdraw' request. It extracts + * the denomination hashes, blinded coins and disclosed coins and allocates + * memory for those. + * + * @param connection The MHD connection to handle + * @param j_disclosed_batch_seeds The n*(kappa-1) disclosed coins' private keys in JSON format, from which all other attributes (age restriction, blinding, nonce) will be derived from + * @param[out] actx The context of the operation, only partially built at call time + * @param[out] mhd_ret The result if a reply is queued for MHD + * @return true on success, false on failure, with a reply already queued for MHD. + */ +static enum GNUNET_GenericReturnValue +parse_withdraw_reveal_json ( + struct MHD_Connection *connection, + const json_t *j_disclosed_batch_seeds, + struct AgeRevealContext *actx, + MHD_RESULT *mhd_ret) +{ + size_t num_entries; + const char *error = NULL; + struct GNUNET_JSON_Specification tuple[] = { + GNUNET_JSON_spec_fixed (NULL, + &actx->disclosed_batch_seeds.tuple[0], + sizeof(actx->disclosed_batch_seeds.tuple[0])), + GNUNET_JSON_spec_fixed (NULL, + &actx->disclosed_batch_seeds.tuple[1], + sizeof(actx->disclosed_batch_seeds.tuple[1])), + GNUNET_JSON_spec_end () + }; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_tuple_of (NULL, tuple), + GNUNET_JSON_spec_end () + }; + + /** + * Note that above, in tuple[], we have hard-wired + * the size of TALER_CNC_KAPPA. + * Let's make sure we keep this in sync. + */ + _Static_assert ((TALER_CNC_KAPPA - 1) == 2); + + num_entries = json_array_size (j_disclosed_batch_seeds); /* 0, if not an array */ + if (! json_is_array (j_disclosed_batch_seeds)) + error = "disclosed_batch_seeds must be an array"; + else if (num_entries == 0) + error = "disclosed_batch_seeds must not be empty"; + else if (num_entries != 2) + error = + "disclosed_batch_seeds must be an array of size " + TALER_CNC_KAPPA_MINUS_ONE_STR; + + if ((NULL != error) || + (GNUNET_OK != GNUNET_JSON_parse (j_disclosed_batch_seeds, + spec, + &error, + NULL))) + { + GNUNET_break_op (0); + *mhd_ret = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + error); + return GNUNET_SYSERR; + } + + + return GNUNET_OK; + +} + + +/** + * Check if the request belongs to an existing withdraw request. + * If so, sets the commitment object with the request data. + * Otherwise, it queues an appropriate MHD response. + * + * @param connection The HTTP connection to the client + * @param h_commitment Original commitment value sent with the withdraw request + * @param[out] commitment Data from the original withdraw request + * @param[out] result In the error cases, a response will be queued with MHD and this will be the result. + * @return #GNUNET_OK if the withdraw request has been found, + * #GNUNET_SYSERR if we did not find the request in the DB + */ +static enum GNUNET_GenericReturnValue +find_original_commitment ( + struct MHD_Connection *connection, + const struct TALER_WithdrawCommitmentHashP *h_commitment, + struct TALER_EXCHANGEDB_Withdraw *commitment, + MHD_RESULT *result) +{ + enum GNUNET_DB_QueryStatus qs; + + for (unsigned int try = 0; try < 3; try++) + { + qs = TEH_plugin->get_withdraw (TEH_plugin->cls, + h_commitment, + commitment); + switch (qs) + { + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return GNUNET_OK; /* Only happy case */ + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + *result = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_WITHDRAW_COMMITMENT_UNKNOWN, + NULL); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_HARD_ERROR: + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_withdraw_info"); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SOFT_ERROR: + break; /* try again */ + default: + GNUNET_break (0); + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL); + return GNUNET_SYSERR; + } + } + /* after unsuccessful retries*/ + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_withdraw_info"); + return GNUNET_SYSERR; +} + + +/** + * @brief Derives an age-restricted planchet from a given secret and calculates the hash + * + * @param connection Connection to the client + * @param denom_key The denomination key + * @param secret The secret to a planchet + * @param max_age The maximum age allowed + * @param[out] bch Hashcode to write + * @param[out] result On error, a HTTP-response will be queued and result set accordingly + * @return GNUNET_OK on success, GNUNET_SYSERR otherwise, with an error message + * written to the client and @e result set. + */ +static enum GNUNET_GenericReturnValue +calculate_blinded_hash ( + struct MHD_Connection *connection, + struct TEH_DenominationKey *denom_key, + const struct TALER_PlanchetMasterSecretP *secret, + uint8_t max_age, + struct TALER_BlindedCoinHashP *bch, + MHD_RESULT *result) +{ + enum GNUNET_GenericReturnValue ret; + struct TALER_AgeCommitmentHash ach; + + /* calculate age commitment hash */ + { + struct TALER_AgeCommitmentProof acp; + + TALER_age_restriction_from_secret (secret, + &denom_key->denom_pub.age_mask, + max_age, + &acp); + TALER_age_commitment_hash (&acp.commitment, + &ach); + TALER_age_commitment_proof_free (&acp); + } + + /* Next: calculate planchet */ + { + struct TALER_CoinPubHashP c_hash; + struct TALER_PlanchetDetail detail = {0}; + struct TALER_CoinSpendPrivateKeyP coin_priv; + union GNUNET_CRYPTO_BlindingSecretP bks; + struct GNUNET_CRYPTO_BlindingInputValues bi = { + .cipher = denom_key->denom_pub.bsign_pub_key->cipher + }; + struct TALER_ExchangeWithdrawValues alg_values = { + .blinding_inputs = &bi + }; + union GNUNET_CRYPTO_BlindSessionNonce nonce; + union GNUNET_CRYPTO_BlindSessionNonce *noncep = NULL; + + // FIXME[oec?]: add logic to denom.c to do this! + if (GNUNET_CRYPTO_BSA_CS == bi.cipher) + { + struct TEH_CsDeriveData cdd = { + .h_denom_pub = &denom_key->h_denom_pub, + .nonce = &nonce.cs_nonce, + }; + + TALER_cs_withdraw_nonce_derive (secret, + &nonce.cs_nonce); + noncep = &nonce; + GNUNET_assert (TALER_EC_NONE == + TEH_keys_denomination_cs_r_pub ( + &cdd, + false, + &bi.details.cs_values)); + } + TALER_planchet_blinding_secret_create (secret, + &alg_values, + &bks); + TALER_planchet_setup_coin_priv (secret, + &alg_values, + &coin_priv); + ret = TALER_planchet_prepare (&denom_key->denom_pub, + &alg_values, + &bks, + noncep, + &coin_priv, + &ach, + &c_hash, + &detail); + if (GNUNET_OK != ret) + { + GNUNET_break (0); + *result = TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + GNUNET_JSON_pack_string ( + "details", + "failed to prepare planchet from base key")); + return ret; + } + + TALER_coin_ev_hash (&detail.blinded_planchet, + &denom_key->h_denom_pub, + bch); + TALER_blinded_planchet_free (&detail.blinded_planchet); + } + + return ret; +} + + +/** + * @brief Checks the validity of the disclosed coins as follows: + * - Derives and calculates the disclosed coins' + * - public keys, + * - nonces (if applicable), + * - age commitments, + * - blindings + * - blinded hashes + * - Computes h_commitment with those calculated and the undisclosed hashes + * - Compares h_commitment with the value from the original commitment + * - Verifies that all public keys in indices larger than the age group + * corresponding to max_age are derived from the constant public key. + * + * The derivation of the blindings, (potential) nonces and age-commitment from + * a coin's private keys is defined in + * https://docs.taler.net/design-documents/024-age-restriction.html#withdraw + * + * @param connection HTTP-connection to the client + * @param commitment Original commitment + * @param disclosed_batch_seeds The secrets of the disclosed coins, (TALER_CNC_KAPPA - 1)*num_coins many + * @param[out] result On error, a HTTP-response will be queued and result set accordingly + * @return GNUNET_OK on success, GNUNET_SYSERR otherwise + */ +static enum GNUNET_GenericReturnValue +verify_commitment_and_max_age ( + struct MHD_Connection *connection, + const struct TALER_EXCHANGEDB_Withdraw *commitment, + const struct TALER_RevealWithdrawMasterSeedsP *disclosed_batch_seeds, + MHD_RESULT *result) +{ + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; + struct GNUNET_HashContext *hash_context; + struct TEH_KeyStateHandle *keys; + struct TEH_DenominationKey *denom_keys[commitment->num_coins]; + struct TALER_Amount total_amount; + struct TALER_Amount total_fee; + struct TALER_AgeMask mask; + struct TALER_PlanchetMasterSecretP + secrets[TALER_CNC_KAPPA - 1][commitment->num_coins]; + uint8_t secrets_idx = 0; /* first index into secrets */ + + GNUNET_assert (commitment->noreveal_index < TALER_CNC_KAPPA); + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &total_amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &total_fee)); + + memset (denom_keys, + 0, + sizeof(denom_keys)); + + /* We need the current keys in memory for the meta-data of the denominations */ + keys = TEH_keys_get_state (); + if (NULL == keys) + { + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + return GNUNET_SYSERR; + } + + /* Find the denomination keys */ + for (size_t i = 0; i < commitment->num_coins; i++) + { + denom_keys[i] = + TEH_keys_denomination_by_hash_from_state ( + keys, + &commitment->denom_pub_hashes[i], + connection, + result); + if (NULL == denom_keys[i]) + { + GNUNET_break_op (0); + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + return GNUNET_SYSERR; + } + + /* Accumulate amount and fees */ + GNUNET_assert (0 <= TALER_amount_add (&total_amount, + &total_amount, + &denom_keys[i]->meta.value)); + GNUNET_assert (0 <= TALER_amount_add (&total_fee, + &total_fee, + &denom_keys[i]->meta.fees.withdraw)); + + if (i == 0) + mask = denom_keys[i]->meta.age_mask; + GNUNET_assert (mask.bits == denom_keys[i]->meta.age_mask.bits); + } + + hash_context = GNUNET_CRYPTO_hash_context_start (); + + for (uint8_t gamma = 0; gamma<TALER_CNC_KAPPA; gamma++) + { + /* Expand the secrets for a disclosed batch */ + if (gamma != commitment->noreveal_index) + { + GNUNET_assert (secrets_idx < (TALER_CNC_KAPPA - 1)); + TALER_expand_withdraw_secrets (commitment->num_coins, + &disclosed_batch_seeds->tuple[secrets_idx], + secrets[secrets_idx]); + } + + for (size_t coin_idx = 0; coin_idx < commitment->num_coins; coin_idx++) + { + /* Now find or create the actual hash of the blinded planchet */ + if (gamma == commitment->noreveal_index) + { + GNUNET_CRYPTO_hash_context_read ( + hash_context, + &commitment->h_coin_evs[coin_idx], + sizeof(commitment->h_coin_evs[coin_idx])); + } + else /* disclosed case: we need to create the blinded hash ourselves */ + { + struct TALER_BlindedCoinHashP bch; + + ret = calculate_blinded_hash (connection, + denom_keys[coin_idx], + &secrets[secrets_idx][coin_idx], + commitment->max_age, + &bch, + result); + + + if (GNUNET_OK != ret) + { + GNUNET_CRYPTO_hash_context_abort (hash_context); + return GNUNET_SYSERR; + } + + /* Continue the running hash of all coin hashes with the calculated + * hash-value of the current, disclosed coin */ + GNUNET_CRYPTO_hash_context_read (hash_context, + &bch, + sizeof(bch)); + } + } /* for-loop over num_coins */ + + /** + * Only for the disclosed indices do we increment the index + * into secrets. + */ + if (gamma != commitment->noreveal_index) + secrets_idx++; + + } + + /* Finally, compare the calculated hash with the original commitment */ + { + struct TALER_HashBlindedPlanchetsP h_planchets; + struct TALER_WithdrawCommitmentHashP wch; + + GNUNET_CRYPTO_hash_context_finish ( + hash_context, + &h_planchets.hash); + + TALER_wallet_withdraw_commit ( + &commitment->reserve_pub, + &total_amount, + &total_fee, + &h_planchets, + &mask, + commitment->max_age, + &wch); + + if (0 != GNUNET_CRYPTO_hash_cmp ( + &commitment->h_commitment.hash, + &wch.hash)) + { + GNUNET_break_op (0); + *result = TALER_MHD_reply_with_ec (connection, + TALER_EC_EXCHANGE_WITHDRAW_REVEAL_INVALID_HASH, + NULL); + return GNUNET_SYSERR; + } + + } + return GNUNET_OK; +} + + +/** + * @brief Send a response for "/withdraw/$RCH/reveal" + * + * @param connection The http connection to the client to send the response to + * @param commitment The data from the commitment with signatures + * @return a MHD result code + */ +static MHD_RESULT +reply_withdraw_reveal_success ( + struct MHD_Connection *connection, + const struct TALER_EXCHANGEDB_Withdraw *commitment) +{ + json_t *list = json_array (); + GNUNET_assert (NULL != list); + + for (unsigned int i = 0; i < commitment->num_coins; i++) + { + json_t *obj = GNUNET_JSON_PACK ( + TALER_JSON_pack_blinded_denom_sig (NULL, + &commitment->denom_sigs[i])); + GNUNET_assert (0 == + json_array_append_new (list, + obj)); + } + + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("ev_sigs", + list)); +} + + +MHD_RESULT +TEH_handler_reveal_withdraw ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[2]) +{ + MHD_RESULT result = MHD_NO; + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; + struct AgeRevealContext actx = {0}; + const json_t *j_disclosed_batch_seeds; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("withdraw_commitment_h", + &actx.ach), + GNUNET_JSON_spec_array_const ("disclosed_batch_seeds", + &j_disclosed_batch_seeds), + GNUNET_JSON_spec_end () + }; + + /* Parse JSON body*/ + ret = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_OK != ret) + { + GNUNET_break_op (0); + return (GNUNET_SYSERR == ret) ? MHD_NO : MHD_YES; + } + + (void) args; + + do { + /* Extract denominations, blinded and disclosed coins */ + if (GNUNET_OK != + parse_withdraw_reveal_json ( + rc->connection, + j_disclosed_batch_seeds, + &actx, + &result)) + break; + + /* Find original commitment */ + if (GNUNET_OK != + find_original_commitment ( + rc->connection, + &actx.ach, + &actx.commitment, + &result)) + break; + + /* Verify the computed h_commitment equals the committed one and that coins + * have a maximum age group corresponding max_age (age-mask dependent) */ + if (GNUNET_OK != + verify_commitment_and_max_age ( + rc->connection, + &actx.commitment, + &actx.disclosed_batch_seeds, + &result)) + break; + + /* Finally, return the signatures */ + result = reply_withdraw_reveal_success (rc->connection, + &actx.commitment); + + } while (0); + + GNUNET_JSON_parse_free (spec); + if (NULL != actx.commitment.denom_sigs) + for (unsigned int i = 0; i<actx.commitment.num_coins; i++) + TALER_blinded_denom_sig_free (&actx.commitment.denom_sigs[i]); + GNUNET_free (actx.commitment.denom_sigs); + GNUNET_free (actx.commitment.denom_pub_hashes); + GNUNET_free (actx.commitment.denom_serials); + return result; +} + + +/* end of taler-exchange-httpd_withdraw_reveal.c */ diff --git a/src/exchange/taler-exchange-httpd_reveal-withdraw.h b/src/exchange/taler-exchange-httpd_reveal-withdraw.h @@ -0,0 +1,56 @@ +/* + This file is part of TALER + Copyright (C) 2023 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_reveal-withdraw.h + * @brief Handle /reveal-withdraw/$ACH requests + * @author Özgür Kesim + */ +#ifndef TALER_EXCHANGE_HTTPD_WITHDRAW_REVEAL_H +#define TALER_EXCHANGE_HTTPD_WITHDRAW_REVEAL_H + +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/reveal-withdraw" request. + * + * The client got a noreveal_index in response to a previous request + * /withdraw. It now has to reveal all n*(kappa-1) + * coin's private keys (except for the noreveal_index), from which all other + * coin-relevant data (blinding, age restriction, nonce) is derived from. + * + * The exchange computes those values, ensures that the maximum age is + * correctly applied, calculates the hash of the blinded envelopes, and - + * together with the non-disclosed blinded envelopes - compares the hash of + * the calculated withdraw commitment against the original. + * + * If all those checks and the used denominations turn out to be correct, the + * exchange signs all blinded envelopes with their appropriate denomination + * keys. + * + * @param rc request context + * @param root uploaded JSON data + * @param args not used + * @return MHD result code + */ +MHD_RESULT +TEH_handler_reveal_withdraw ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[2]); + +#endif diff --git a/src/exchange/taler-exchange-httpd_withdraw.c b/src/exchange/taler-exchange-httpd_withdraw.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2024 Taler Systems SA + Copyright (C) 2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -16,15 +16,13 @@ Public License along with TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - /** * @file taler-exchange-httpd_withdraw.c - * @brief Common code to handle /reserves/$RESERVE_PUB/{age,batch}-withdraw requests - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - * @author Ozgur Kesim + * @brief Code to handle /withdraw requests + * @note This endpoint is active since v24 of the protocol API + * @author Özgür Kesim */ + #include "platform.h" #include <gnunet/gnunet_util_lib.h> #include <jansson.h> @@ -39,9 +37,7 @@ #include "taler_util.h" /** - * Context for both types of requests - * 1.) #batch-withdraw - * 2.) #age-withdraw + * Context for a /withdraw requests */ struct WithdrawContext { @@ -53,17 +49,7 @@ struct WithdrawContext struct WithdrawContext *next; /** - * What type of withdraw is represented here. - * See union #.typ for type-specific data. - */ - enum WithdrawType - { - WITHDRAW_TYPE_BATCH, - WITHDRAW_TYPE_AGE - } withdraw_type; - - /** - * Processing phase we are in for any of the withdraw types. + * Processing phase we are in. * The ordering here partially matters, as we progress through * them by incrementing the phase in the happy path. */ @@ -94,11 +80,6 @@ struct WithdrawContext const struct TEH_RequestContext *rc; /** - * Public key of the reserve. - */ - struct TALER_ReservePublicKeyP reserve_pub; - - /** * KYC status for the operation. */ struct TALER_EXCHANGEDB_KycStatus kyc; @@ -114,95 +95,71 @@ struct WithdrawContext */ struct TALER_NormalizedPaytoHashP h_normalized_payto; - /** - * Number of coins, depending on the @e withdraw_type: - * 1) WITHDRAW_TYPE_BATCH: length of @e typ.batch.planchets array. - * 2) WITHDRAW_TYPE_AGE: length of @e typ.age.planchets array - */ - unsigned int num_coins; /** - * Depending on @e withdraw_type, this union - * contains the details of a withdraw operation: - * 1.) WITHDRAW_TYPE_BATCH: see @e typ.batch - * 2.) WITHDRAW_TYPE_AGE: see @e typ.age + * Captures all parameters provided in the JSON request */ - union + struct { /** - * Data specific to batch_withdraw + * All fields (from the request or computed) + * that we persist in the database. */ - struct - { - /** - * Array of @e num_coins planchets we are processing, - * containing the information per planchet in a batch withdraw. - */ - struct PlanchetContext - { - - /** - * Value of the coin being exchanged (matching the denomination key) - * plus the transaction fee. We include this in what is being - * signed so that we can verify a reserve's remaining total balance - * without needing to access the respective denomination key - * information each time. - */ - struct TALER_Amount amount_with_fee; + struct TALER_EXCHANGEDB_Withdraw persist; - /** - * Blinded planchet. - */ - struct TALER_BlindedPlanchet blinded_planchet; - - /** - * Set to the resulting signed coin data to be returned to the client. - */ - struct TALER_EXCHANGEDB_CollectableBlindcoin collectable; + /** + * In some error cases we check for idempotency. + * If we find an entry in the database, we mark this here. + */ + bool is_idempotent; - } *planchets; + /** + * In some error conditions the request is checked + * for idempotency and the result from the database + * is stored here. + */ + struct TALER_EXCHANGEDB_Withdraw idem; - /** - * Total amount from all coins with fees. - */ - struct TALER_Amount batch_total; + /** + * Array ``num_coins`` of hashes of the public keys + * of the denominations to withdraw. + */ + struct TALER_DenominationHashP *denoms_h; - } batch; + /** + * Number of planchets. If ``max_age`` was _not_ set, this is equal to ``num_coins``. + * Otherwise (``max_age`` was set) it is ``num_coins * kappa``. + */ + size_t num_planchets; /** - * Data specific to age_withdraw + * Array of ``num_planchets`` coin planchets. + * Note that the size depends on the age restriction: + * If ``age_restricted`` is false, this is an array of length ``num_coins``. + * Otherwise it is an array of length ``kappa*num_coins``, arranged + * in runs of ``num_coins`` coins, [0..num_coins)..[0..num_coins), + * one for each kappa value. */ - struct - { - /** - * value the client committed to - */ - struct TALER_AgeWithdrawCommitmentHashP ach; + struct TALER_BlindedPlanchet *planchets; - /** - * The data from the age-withdraw request, as we persist it + /** + * Total (over all coins) amount (excluding fee) committed to withdraw */ - struct TALER_EXCHANGEDB_AgeWithdraw commitment; + struct TALER_Amount amount; - /** - * # @e num_coins * TALER_CNC_KAPPA hashes of blinded coin planchets. - */ - struct TALER_BlindedPlanchet (*planchets) [ TALER_CNC_KAPPA]; + /** + * Total fees for the withdraw + */ + struct TALER_Amount fee; - /** - * #num_coins hashes of the denominations from which the coins are withdrawn. - * Those must support age restriction. - */ - struct TALER_DenominationHashP *denom_hs; - } age; + } request; - } typ; /** - * Errors occurring during evaluation of the request are captured in this struct. - * In phase PHASE_GENERATE_REPLY_ERROR an appropriate error message is prepared - * and sent to the client. - */ + * Errors occurring during evaluation of the request are captured in this struct. + * In phase PHASE_GENERATE_REPLY_ERROR an appropriate error message is prepared + * and sent to the client. + */ struct { /** @@ -214,31 +171,32 @@ struct WithdrawContext { ERROR_NONE, - ERROR_AGE_AMOUNT_OVERFLOW, - ERROR_AGE_INSUFFICIENT_FUNDS, - ERROR_AGE_MAXIMUM_AGE_TOO_LARGE, ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, ERROR_AGE_RESTRICTION_REQUIRED, - ERROR_AGE_CONFIRMATION_SIGN, - ERROR_BATCH_AMOUNT_FEE_OVERFLOW, - ERROR_BATCH_IDEMPOTENT_PLANCHET, - ERROR_BATCH_INSUFFICIENT_FUNDS, - ERROR_BATCH_NONCE_RESUSE, + ERROR_AMOUNT_OVERFLOW, + ERROR_AMOUNT_PLUS_FEE_OVERFLOW, ERROR_CIPHER_MISMATCH, + ERROR_CONFIRMATION_SIGN, ERROR_DB_FETCH_FAILED, ERROR_DB_INVARIANT_FAILURE, - ERROR_DENOMINATION_BATCH_SIGN, ERROR_DENOMINATION_EXPIRED, ERROR_DENOMINATION_KEY_UNKNOWN, ERROR_DENOMINATION_REVOKED, + ERROR_DENOMINATION_SIGN, ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + ERROR_FEE_OVERFLOW, + ERROR_IDEMPOTENT_PLANCHET, + ERROR_INSUFFICIENT_FUNDS, ERROR_INTERNAL_INVARIANT_FAILURE, ERROR_KEYS_MISSING, ERROR_KYC_REQUIRED, ERROR_LEGITIMIZATION_RESULT, + ERROR_MAXIMUM_AGE_TOO_LARGE, + ERROR_NONCE_RESUSE, + ERROR_REQUEST_PARAMETER_MALFORMED, + ERROR_RESERVE_CIPHER_UNKNOWN, ERROR_RESERVE_SIGNATURE_INVALID, ERROR_RESERVE_UNKNOWN, - ERROR_REQUEST_PARAMETER_MALFORMED, ERROR_MAX } code; @@ -254,16 +212,21 @@ struct WithdrawContext /* ERROR_REQUEST_PARAMETER_MALFORMED */ const char *hint; - /* ERROR_AGE_AMOUNT_OVERFLOW */ + /* ERROR_AMOUNT_OVERFLOW */ + /* ERROR_FEE_OVERFLOW */ + /* ERROR_AMOUNT_PLUS_FEE_OVERFLOW */ const char *which; + /* ERROR_RESERVE_CIPHER_UNKNOWN */ + const char *cipher; + /* ERROR_DENOMINATION_KEY_UNKNOWN */ const struct TALER_DenominationHashP *denom_h; /* ERROR_DB_FETCH_FAILED */ const char *db_fetch_context; - /* ERROR_AGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE */ + /* ERROR_MAXIMUM_AGE_TOO_LARGE */ struct { uint16_t max; @@ -273,12 +236,11 @@ struct WithdrawContext /* ERROR_AGE_RESTRICTION_REQUIRED */ uint16_t lowest_age; - /* ERROR_AGE_INSUFFICIENT_FUNDS */ - /* ERROR_BATCH_INSUFFICIENT_FUNDS */ + /* ERROR_INSUFFICIENT_FUNDS */ struct TALER_Amount reserve_balance; - /* ERROR_AGE_CONFIRMATION_SIGN */ - /* ERROR_DENOMINATION_BATCH_SIGN */ + /* ERROR_CONFIRMATION_SIGN */ + /* ERROR_DENOMINATION_SIGN */ enum TALER_ErrorCode ec; /* ERROR_LEGITIMIZATION_RESULT */ @@ -300,28 +262,28 @@ static const bool needs_idempotency_check[] = { [ERROR_NONE] = false, - [ERROR_AGE_AMOUNT_OVERFLOW] = false, - [ERROR_AGE_CONFIRMATION_SIGN] = false, - [ERROR_AGE_MAXIMUM_AGE_TOO_LARGE] = false, + [ERROR_MAXIMUM_AGE_TOO_LARGE] = false, [ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION] = false, [ERROR_AGE_RESTRICTION_REQUIRED] = false, - [ERROR_BATCH_AMOUNT_FEE_OVERFLOW] = false, - [ERROR_BATCH_NONCE_RESUSE] = false, + [ERROR_AMOUNT_OVERFLOW] = false, + [ERROR_AMOUNT_PLUS_FEE_OVERFLOW] = false, [ERROR_CIPHER_MISMATCH] = false, + [ERROR_CONFIRMATION_SIGN] = false, [ERROR_DB_FETCH_FAILED] = false, [ERROR_DB_INVARIANT_FAILURE] = false, - [ERROR_DENOMINATION_BATCH_SIGN] = false, + [ERROR_DENOMINATION_SIGN] = false, [ERROR_DENOMINATION_VALIDITY_IN_FUTURE] = false, [ERROR_INTERNAL_INVARIANT_FAILURE] = false, [ERROR_LEGITIMIZATION_RESULT] = false, + [ERROR_NONCE_RESUSE] = false, [ERROR_REQUEST_PARAMETER_MALFORMED] = false, + [ERROR_RESERVE_CIPHER_UNKNOWN] = false, [ERROR_RESERVE_SIGNATURE_INVALID] = false, [ERROR_RESERVE_UNKNOWN] = false, /* These require idempotency checks */ - [ERROR_AGE_INSUFFICIENT_FUNDS] = true, - [ERROR_BATCH_IDEMPOTENT_PLANCHET] = true, - [ERROR_BATCH_INSUFFICIENT_FUNDS] = true, + [ERROR_INSUFFICIENT_FUNDS] = true, + [ERROR_IDEMPOTENT_PLANCHET] = true, [ERROR_DENOMINATION_EXPIRED] = true, [ERROR_DENOMINATION_KEY_UNKNOWN] = true, [ERROR_DENOMINATION_REVOKED] = true, @@ -332,7 +294,7 @@ static const bool _Static_assert ( (sizeof (needs_idempotency_check) == ERROR_MAX), - "needs_idempotency_check size mismatch with enum WithdrawErrorCode"); + "needs_idempotency_check size mismatch with enum for errors"); /** * The following macros set the given error code, @@ -340,21 +302,21 @@ _Static_assert ( * and optionally set the given field (with an optionally given value). */ #define SET_ERROR(wc, ec) \ - do \ - { (wc)->error.code = (ec); \ - (wc)->phase = PHASE_GENERATE_REPLY_ERROR; } while (0) + do \ + { (wc)->error.code = (ec); \ + (wc)->phase = PHASE_GENERATE_REPLY_ERROR; } while (0) #define SET_ERROR_WITH_FIELD(wc, ec, field) \ - do \ - { (wc)->error.code = (ec); \ - (wc)->error.details.field = (field); \ - (wc)->phase = PHASE_GENERATE_REPLY_ERROR; } while (0) + do \ + { (wc)->error.code = (ec); \ + (wc)->error.details.field = (field); \ + (wc)->phase = PHASE_GENERATE_REPLY_ERROR; } while (0) #define SET_ERROR_WITH_DETAIL(wc, ec, field, value) \ - do \ - { (wc)->error.code = (ec); \ - (wc)->error.details.field = (value); \ - (wc)->phase = PHASE_GENERATE_REPLY_ERROR; } while (0) + do \ + { (wc)->error.code = (ec); \ + (wc)->error.details.field = (value); \ + (wc)->phase = PHASE_GENERATE_REPLY_ERROR; } while (0) /** @@ -396,90 +358,7 @@ finish_loop (struct WithdrawContext *wc, /** - * Generates our final (successful) response to a batch withdraw request. - * - * @param wc operation context - */ -static void -batch_withdraw_phase_generate_reply_success (struct WithdrawContext *wc) -{ - const struct TEH_RequestContext *rc = wc->rc; - json_t *sigs; - - sigs = json_array (); - GNUNET_assert (NULL != sigs); - for (unsigned int i = 0; i<wc->num_coins; i++) - { - struct PlanchetContext *pc = &wc->typ.batch.planchets[i]; - - GNUNET_assert ( - 0 == - json_array_append_new ( - sigs, - GNUNET_JSON_PACK ( - TALER_JSON_pack_blinded_denom_sig ( - "ev_sig", - &pc->collectable.sig)))); - } - TEH_METRICS_batch_withdraw_num_coins += wc->num_coins; - finish_loop (wc, - TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("ev_sigs", - sigs))); -} - - -/** - * Check if the batch withdraw in @a wc is replayed - * and we already have an answer. - * If so, replay the existing answer and return the HTTP response. - * - * @param wc parsed request data - * @return true if the request is idempotent with an existing request - * false if we did not find the request in the DB and did not set @a mret - */ -static bool -batch_withdraw_check_idempotency ( - struct WithdrawContext *wc) -{ - GNUNET_assert (wc->withdraw_type == WITHDRAW_TYPE_BATCH); - - for (unsigned int i = 0; i<wc->num_coins; i++) - { - struct PlanchetContext *pc = &wc->typ.batch.planchets[i]; - enum GNUNET_DB_QueryStatus qs; - struct TALER_EXCHANGEDB_CollectableBlindcoin collectable; - - qs = TEH_plugin->get_withdraw_info ( - TEH_plugin->cls, - &pc->collectable.h_coin_envelope, - &collectable); - if (0 > qs) - { - /* FIXME: soft error not handled correctly! */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - SET_ERROR_WITH_DETAIL (wc, - ERROR_DB_FETCH_FAILED, - db_fetch_context, - "get_withdraw_info"); - return true; /* Well, kind-of. */ - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - return false; - pc->collectable = collectable; - } - - /* generate idempotent reply */ - TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_BATCH_WITHDRAW]++; - wc->phase = PHASE_GENERATE_REPLY_SUCCESS; - return true; -} - - -/** - * Check if the age-withdraw request is replayed + * Check if the withdraw request is replayed * and we already have an answer. * If so, replay the existing answer and return the HTTP response. * @@ -488,18 +367,15 @@ batch_withdraw_check_idempotency ( * false if we did not find the request in the DB and did not set @a mret */ static bool -age_withdraw_check_idempotency ( +withdraw_is_idempotent ( struct WithdrawContext *wc) { enum GNUNET_DB_QueryStatus qs; - struct TALER_EXCHANGEDB_AgeWithdraw commitment; - GNUNET_assert (wc->withdraw_type == WITHDRAW_TYPE_AGE); - qs = TEH_plugin->get_age_withdraw ( + qs = TEH_plugin->get_withdraw ( TEH_plugin->cls, - &wc->typ.age.commitment.reserve_pub, - &wc->typ.age.commitment.h_commitment, - &commitment); + &wc->request.persist.h_commitment, + &wc->request.idem); if (0 > qs) { /* FIXME: soft error not handled correctly! */ @@ -508,63 +384,39 @@ age_withdraw_check_idempotency ( SET_ERROR_WITH_DETAIL (wc, ERROR_DB_FETCH_FAILED, db_fetch_context, - "get_age_withdraw"); + "get_withdraw"); return true; /* Well, kind-of. */ } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) return false; + wc->request.is_idempotent = true; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "request is idempotent\n"); + /* Generate idempotent reply */ - TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_AGE_WITHDRAW]++; + TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_WITHDRAW]++; wc->phase = PHASE_GENERATE_REPLY_SUCCESS; return true; } /** - * Check if the @a wc is replayed and we already have an - * answer. If so, replay the existing answer and return the - * HTTP response. - * - * @param wc parsed request data - * @return true if the request is idempotent with an existing request - * false if we did not find the request in the DB and did not set @a mret - */ -static bool -check_idempotency ( - struct WithdrawContext *wc) -{ - switch (wc->withdraw_type) - { - case WITHDRAW_TYPE_BATCH: - return batch_withdraw_check_idempotency (wc); - break; - case WITHDRAW_TYPE_AGE: - return age_withdraw_check_idempotency (wc); - break; - default: - GNUNET_break (0); - } - return false; -} - - -/** - * Function implementing age withdraw transaction. Runs the + * Function implementing withdraw transaction. Runs the * transaction logic; IF it returns a non-error code, the transaction * logic MUST NOT queue a MHD response. IF it returns an hard error, * the transaction logic MUST queue a MHD response and set @a mhd_ret. * IF it returns the soft error code, the function MAY be called again * to retry and MUST not queue a MHD response. * - * @param cls a `struct WithdrawContext *`, with @e withdraw_type == WITHDRAW_TYPE_AGE + * @param cls a `struct WithdrawContext *` * @param connection MHD request which triggered the transaction * @param[out] mhd_ret set to MHD response status for @a connection, * if transaction failed (!) * @return transaction status */ static enum GNUNET_DB_QueryStatus -age_withdraw_transaction ( +withdraw_transaction ( void *cls, struct MHD_Connection *connection, MHD_RESULT *mhd_ret) @@ -579,28 +431,29 @@ age_withdraw_transaction ( uint32_t reserve_birthday = 0; struct TALER_Amount reserve_balance; - qs = TEH_plugin->do_age_withdraw ( - TEH_plugin->cls, - &wc->typ.age.commitment, - wc->now, - &found, - &balance_ok, - &reserve_balance, - &age_ok, - &allowed_maximum_age, - &reserve_birthday, - &conflict); - if (0 > qs) + qs = TEH_plugin->do_withdraw (TEH_plugin->cls, + &wc->request.persist, + wc->now, + &found, + &balance_ok, + &reserve_balance, + &age_ok, + &allowed_maximum_age, + &reserve_birthday, + &conflict); + + if (0 > qs) { if (GNUNET_DB_STATUS_HARD_ERROR == qs) SET_ERROR_WITH_DETAIL (wc, ERROR_DB_FETCH_FAILED, db_fetch_context, - "do_age_withdraw"); + "do_withdraw"); return qs; } + if (! found) { SET_ERROR (wc, @@ -613,7 +466,7 @@ age_withdraw_transaction ( wc->error.details.age.max = allowed_maximum_age; wc->error.details.age.birthday = reserve_birthday; SET_ERROR (wc, - ERROR_AGE_MAXIMUM_AGE_TOO_LARGE); + ERROR_MAXIMUM_AGE_TOO_LARGE); return GNUNET_DB_STATUS_HARD_ERROR; } @@ -622,7 +475,7 @@ age_withdraw_transaction ( TEH_plugin->rollback (TEH_plugin->cls); SET_ERROR_WITH_FIELD (wc, - ERROR_AGE_INSUFFICIENT_FUNDS, + ERROR_INSUFFICIENT_FUNDS, reserve_balance); return GNUNET_DB_STATUS_HARD_ERROR; @@ -630,181 +483,21 @@ age_withdraw_transaction ( if (conflict) { - /* do_age_withdraw signaled a conflict, so there MUST be an entry + /* do_withdraw signaled a conflict, so there MUST be an entry * in the DB. Put that into the response */ - if (check_idempotency (wc)) - return GNUNET_DB_STATUS_HARD_ERROR; + if (withdraw_is_idempotent (wc)) + return GNUNET_DB_STATUS_HARD_ERROR; /* Done, not error really. */ GNUNET_break (0); return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; } if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - TEH_METRICS_num_success[TEH_MT_SUCCESS_AGE_WITHDRAW]++; + TEH_METRICS_num_success[TEH_MT_SUCCESS_WITHDRAW]++; return qs; } /** - * Function implementing withdraw transaction. Runs the - * transaction logic; IF it returns a non-error code, the transaction - * logic MUST NOT queue a MHD response. IF it returns an hard error, - * the transaction logic MUST queue a MHD response and set @a mhd_ret. - * IF it returns the soft error code, the function MAY be called again - * to retry and MUST not queue a MHD response. - * - * Note that "wc->collectable.sig" is set before entering this function as we - * signed before entering the transaction. - * - * @param cls a `struct WithdrawContext *`, with @e withdraw_type set to WITHDRAW_TYPE_BATCH - * @param connection MHD request which triggered the transaction - * @param[out] mhd_ret set to MHD response status for @a connection, - * if transaction failed (!) - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -batch_withdraw_transaction ( - void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct WithdrawContext *wc = cls; - uint64_t ruuid; - enum GNUNET_DB_QueryStatus qs; - bool found = false; - bool balance_ok = false; - bool age_ok = false; - uint16_t allowed_maximum_age = 0; - struct TALER_Amount reserve_balance; - - qs = TEH_plugin->do_batch_withdraw ( - TEH_plugin->cls, - wc->now, - &wc->reserve_pub, - &wc->typ.batch.batch_total, - TEH_age_restriction_enabled, - &found, - &balance_ok, - &reserve_balance, - &age_ok, - &allowed_maximum_age, - &ruuid); - - if (0 > qs) - { - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - GNUNET_break (0); - SET_ERROR_WITH_DETAIL (wc, - ERROR_DB_FETCH_FAILED, - db_fetch_context, - "update_reserve_batch_withdraw"); - } - return qs; - } - - if (! found) - { - GNUNET_break_op (0); - SET_ERROR (wc, - ERROR_RESERVE_UNKNOWN); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - if (! age_ok) - { - /* We respond with the lowest age in the corresponding age group - * of the required age */ - uint16_t lowest_age = TALER_get_lowest_age ( - &TEH_age_restriction_config.mask, - allowed_maximum_age); - SET_ERROR_WITH_FIELD (wc, - ERROR_AGE_RESTRICTION_REQUIRED, - lowest_age); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - if (! balance_ok) - { - GNUNET_break_op (0); - SET_ERROR_WITH_FIELD (wc, - ERROR_BATCH_INSUFFICIENT_FUNDS, - reserve_balance); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - /* Add information about each planchet in the batch */ - for (unsigned int i = 0; i<wc->num_coins; i++) - { - struct PlanchetContext *pc = &wc->typ.batch.planchets[i]; - const struct TALER_BlindedPlanchet *bp = &pc->blinded_planchet; - const union GNUNET_CRYPTO_BlindSessionNonce *nonce = NULL; - bool denom_unknown = true; - bool conflict = true; - bool nonce_reuse = true; - - switch (bp->blinded_message->cipher) - { - case GNUNET_CRYPTO_BSA_INVALID: - break; - case GNUNET_CRYPTO_BSA_RSA: - break; - case GNUNET_CRYPTO_BSA_CS: - nonce = (const union GNUNET_CRYPTO_BlindSessionNonce *) - &bp->blinded_message->details.cs_blinded_message.nonce; - break; - } - - qs = TEH_plugin->do_batch_withdraw_insert ( - TEH_plugin->cls, - nonce, - &pc->collectable, - wc->now, - ruuid, - &denom_unknown, - &conflict, - &nonce_reuse); - - if (0 > qs) - { - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - SET_ERROR_WITH_DETAIL (wc, - ERROR_DB_FETCH_FAILED, - db_fetch_context, - "do_batch_withdraw_insert"); - return qs; - } - - if (denom_unknown) - { - GNUNET_break (0); - SET_ERROR (wc, - ERROR_DB_INVARIANT_FAILURE); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) || - (conflict) ) - { - SET_ERROR (wc, - ERROR_BATCH_IDEMPOTENT_PLANCHET); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - if (nonce_reuse) - { - GNUNET_break_op (0); - SET_ERROR (wc, - ERROR_BATCH_NONCE_RESUSE); - - return GNUNET_DB_STATUS_HARD_ERROR; - } - } - TEH_METRICS_num_success[TEH_MT_SUCCESS_BATCH_WITHDRAW]++; - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; -} - - -/** * The request was prepared successfully. * Run the main DB transaction. * @@ -820,28 +513,13 @@ phase_run_transaction ( GNUNET_assert (PHASE_RUN_TRANSACTION == wc->phase); - switch (wc->withdraw_type) - { - case WITHDRAW_TYPE_AGE: - qs = TEH_DB_run_transaction (wc->rc->connection, - "run age withdraw", - TEH_MT_REQUEST_AGE_WITHDRAW, - &mhd_ret, - &age_withdraw_transaction, - wc); - break; - case WITHDRAW_TYPE_BATCH: - qs = TEH_DB_run_transaction (wc->rc->connection, - "run batch withdraw", - TEH_MT_REQUEST_WITHDRAW, - &mhd_ret, - &batch_withdraw_transaction, - wc); - break; - default: - GNUNET_break (0); - qs = GNUNET_SYSERR; - } + + qs = TEH_DB_run_transaction (wc->rc->connection, + "run withdraw", + TEH_MT_REQUEST_WITHDRAW, + &mhd_ret, + &withdraw_transaction, + wc); if (GNUNET_OK != qs) { @@ -856,145 +534,83 @@ phase_run_transaction ( /** - * The request for batch withdraw was parsed successfully. - * Prepare our side for the main DB transaction. - * - * @param wc context for request processing, with @e withdraw_type set to WITHDRAW_TYPE_BATCH - * @return GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -batch_withdraw_phase_prepare_transaction (struct WithdrawContext *wc) -{ - struct TALER_BlindedDenominationSignature bss[wc->num_coins]; - struct TEH_CoinSignData csds[wc->num_coins]; - - for (unsigned int i = 0; i<wc->num_coins; i++) - { - struct PlanchetContext *pc = &wc->typ.batch.planchets[i]; - struct TEH_CoinSignData *csdsi = &csds[i]; - - csdsi->h_denom_pub = &pc->collectable.denom_pub_hash; - csdsi->bp = &pc->blinded_planchet; - } - { - enum TALER_ErrorCode ec; - - ec = TEH_keys_denomination_batch_sign ( - wc->num_coins, - csds, - false, - bss); - if (TALER_EC_NONE != ec) - { - GNUNET_break (0); - SET_ERROR_WITH_FIELD (wc, - ERROR_DENOMINATION_BATCH_SIGN, - ec); - return GNUNET_SYSERR; - } - } - - for (unsigned int i = 0; i<wc->num_coins; i++) - { - struct PlanchetContext *pc = &wc->typ.batch.planchets[i]; - - pc->collectable.sig = bss[i]; - } - - return GNUNET_OK; -} - - -/** - * The request for age-withdraw was parsed successfully. + * The request for withdraw was parsed successfully. * Sign and persist the chosen blinded coins for the reveal step. * - * @param wc The context for the current withdraw request, with @e withdraw_type set to WITHDRAW_TYPE_AGE - * @return GNUNET_OK on success + * @param wc The context for the current withdraw request */ -static enum GNUNET_GenericReturnValue -age_withdraw_phase_prepare_transaction ( +static void +phase_prepare_transaction ( struct WithdrawContext *wc) { - uint8_t noreveal_index; + size_t offset = 0; - wc->typ.age.commitment.denom_sigs + wc->request.persist.denom_sigs = GNUNET_new_array ( - wc->num_coins, + wc->request.persist.num_coins, struct TALER_BlindedDenominationSignature); - wc->typ.age.commitment.h_coin_evs + + wc->request.persist.h_coin_evs = GNUNET_new_array ( - wc->num_coins, + wc->request.persist.num_coins, struct TALER_BlindedCoinHashP); - /* Pick the challenge */ - noreveal_index = - GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_STRONG, - TALER_CNC_KAPPA); - wc->typ.age.commitment.noreveal_index = noreveal_index; + + /* Pick the challenge in case of age restriction */ + if (wc->request.persist.age_restricted) + { + wc->request.persist.noreveal_index = + GNUNET_CRYPTO_random_u32 ( + GNUNET_CRYPTO_QUALITY_STRONG, + TALER_CNC_KAPPA); + /** + * In case of age restriction, we use the corresponding offset in the planchet + * array to the beginning of the coins corresponding to the noreveal_index. + */ + offset = wc->request.persist.noreveal_index * wc->request.persist.num_coins; + + GNUNET_assert (offset + wc->request.persist.num_coins <= + wc->request.num_planchets); + } /* Choose and sign the coins */ { - struct TEH_CoinSignData csds[wc->num_coins]; + struct TEH_CoinSignData csds[wc->request.persist.num_coins]; enum TALER_ErrorCode ec; + memset (csds, + 0, + sizeof(csds)); + /* Pick the chosen blinded coins */ - for (uint32_t i = 0; i<wc->num_coins; i++) + for (uint32_t i = 0; i<wc->request.persist.num_coins; i++) { - struct TEH_CoinSignData *csdsi = &csds[i]; - - csdsi->bp = &wc->typ.age.planchets[i][noreveal_index]; - csdsi->h_denom_pub = &wc->typ.age.denom_hs[i]; + csds[i].bp = &wc->request.planchets[i + offset]; + csds[i].h_denom_pub = &wc->request.denoms_h[i]; } ec = TEH_keys_denomination_batch_sign ( - wc->num_coins, + wc->request.persist.num_coins, csds, false, - wc->typ.age.commitment.denom_sigs); + wc->request.persist.denom_sigs); if (TALER_EC_NONE != ec) { GNUNET_break (0); SET_ERROR_WITH_FIELD (wc, - ERROR_DENOMINATION_BATCH_SIGN, + ERROR_DENOMINATION_SIGN, ec); - return GNUNET_SYSERR; + return; } - } - /* Prepare the hashes of the coins for insertion */ - for (uint32_t i = 0; i<wc->num_coins; i++) - { - TALER_coin_ev_hash (&wc->typ.age.planchets[i][noreveal_index], - &wc->typ.age.denom_hs[i], - &wc->typ.age.commitment.h_coin_evs[i]); + /* Prepare the hashes of the coins for insertion */ + for (uint32_t i = 0; i<wc->request.persist.num_coins; i++) + { + TALER_coin_ev_hash (&wc->request.planchets[i + offset], + &wc->request.denoms_h[i], + &wc->request.persist.h_coin_evs[i]); + } } - return GNUNET_OK; -} - -/** - * The request for withdraw was parsed successfully. - * Choose the appropriate preparation step depending on @e withdraw_type - */ -static void -phase_prepare_transaction ( - struct WithdrawContext *wc) -{ - enum GNUNET_GenericReturnValue r; - switch (wc->withdraw_type) - { - case WITHDRAW_TYPE_BATCH: - r = batch_withdraw_phase_prepare_transaction (wc); - break; - case WITHDRAW_TYPE_AGE: - r = age_withdraw_phase_prepare_transaction (wc); - break; - default: - GNUNET_break (0); - return; - } - if (GNUNET_OK != r) - return; wc->phase++; } @@ -1062,39 +678,8 @@ static const char * typ2str ( const struct WithdrawContext *wc) { - switch (wc->withdraw_type) - { - case WITHDRAW_TYPE_BATCH: - return "batch-withdraw"; - case WITHDRAW_TYPE_AGE: - return "age-withdraw"; - default: - GNUNET_break (0); - return "unknown"; - } -} - - -/** - * Return the total amount including fees to be withdrawn - * - * @param wc withdraw context - * @return total amount including fees - */ -static struct TALER_Amount * -withdraw_amount_with_fee ( - struct WithdrawContext *wc) -{ - switch (wc->withdraw_type) - { - case WITHDRAW_TYPE_BATCH: - return &wc->typ.batch.batch_total; - case WITHDRAW_TYPE_AGE: - return &wc->typ.age.commitment.amount_with_fee; - default: - GNUNET_break (0); - return NULL; - } + return (wc->request.persist.age_restricted) ? "age-withdraw" : + "batch-withdraw"; } @@ -1125,11 +710,11 @@ withdraw_amount_cb ( GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Signaling amount %s for KYC check during %sal\n", - TALER_amount2s (withdraw_amount_with_fee (wc)), + TALER_amount2s (&wc->request.persist.amount_with_fee), typ2str (wc)); ret = cb (cb_cls, - withdraw_amount_with_fee (wc), + &wc->request.persist.amount_with_fee, wc->now.abs_time); GNUNET_break (GNUNET_SYSERR != ret); @@ -1171,7 +756,7 @@ phase_run_legi_check (struct WithdrawContext *wc) /* Check if the money came from a wire transfer */ qs = TEH_plugin->reserves_get_origin ( TEH_plugin->cls, - &wc->reserve_pub, + &wc->request.persist.reserve_pub, &h_full_payto, &payto_uri); if (qs < 0) @@ -1276,7 +861,7 @@ find_denomination ( } /* In case of age withdraw, make sure that the denomitation supports age restriction */ - if (WITHDRAW_TYPE_AGE == wc->withdraw_type) + if (wc->request.persist.age_restricted) { if (0 == dk->denom_pub.age_mask.bits) { @@ -1294,260 +879,137 @@ find_denomination ( /** - * Check if the given array of hashes of denomination_keys a) belong - * to valid denominations and b) those are marked as age restricted. - * Also, calculate the total amount of the denominations including fees + * Check if the given array of hashes of denomination_keys + * a) belong to valid denominations + * b) those are marked as age restricted, if the request is age restricted + * c) calculate the total amount of the denominations including fees * for withdraw. * * @param wc context of the age withdrawal to check keys for - * @param ksh key state handle - * @return GNUNET_OK on success, - * GNUNET_NO on error (and response being sent) */ -static enum GNUNET_GenericReturnValue -age_withdraw_phase_check_keys ( - struct WithdrawContext *wc, - struct TEH_KeyStateHandle *ksh) +static void +phase_check_keys ( + struct WithdrawContext *wc) { - unsigned int len = wc->num_coins; - struct TALER_Amount total_amount; - struct TALER_Amount total_fee; + struct TEH_KeyStateHandle *ksh; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + GNUNET_break (0); + SET_ERROR (wc, + ERROR_KEYS_MISSING); + return; + } + + wc->request.persist.denom_serials = + GNUNET_new_array (wc->request.persist.num_coins, + uint64_t); + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &wc->request.amount)); - wc->typ.age.commitment.denom_serials - = GNUNET_new_array (len, - uint64_t); GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (TEH_currency, - &total_amount)); + &wc->request.fee)); + GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (TEH_currency, - &total_fee)); + &wc->request.persist.amount_with_fee)); - for (unsigned int i = 0; i < len; i++) + for (unsigned int i = 0; i < wc->request.persist.num_coins; i++) { struct TEH_DenominationKey *dk; - enum GNUNET_GenericReturnValue r; - - r = find_denomination (wc, - ksh, - &wc->typ.age.denom_hs[i], - &dk); - if (GNUNET_OK != r) - return GNUNET_NO; + if (GNUNET_OK != find_denomination ( + wc, + ksh, + &wc->request.denoms_h[i], + &dk)) + return; - /* Ensure the ciphers from the planchets match the denominations' */ - for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) + /* Ensure the ciphers from the planchets match the denominations'. */ { - if (dk->denom_pub.bsign_pub_key->cipher != - wc->typ.age.planchets[i][k].blinded_message->cipher) + /** + * Slightly ugly because we have to handle two cases: + * + * a) When age restriction is not applicable, we only have request.persist.num_coins + * to deal with, matching the number of denominations. + * + * b) However, when request.persist.age_restricted is set, the list of planchets + * is kappa*num_coins, and there are kappa many coin candidates per denomination. + * At each index i, the coins at i+j*num_coins (for j from 0 to kappa) + * belong to the same denomination. + * + * We try to be smart here and handle both variants in one loop + * that either runs only once or kappa times. + */ + uint8_t kappa = wc->request.persist.age_restricted ? TALER_CNC_KAPPA : 0; + + for (uint8_t k = 0; k < kappa; k++) { - GNUNET_break_op (0); - SET_ERROR (wc, - ERROR_CIPHER_MISMATCH); - return GNUNET_NO; + size_t off = k * wc->request.persist.num_coins; + + if (dk->denom_pub.bsign_pub_key->cipher != + wc->request.planchets[i + off].blinded_message->cipher) + { + GNUNET_break_op (0); + SET_ERROR (wc, + ERROR_CIPHER_MISMATCH); + return; + } } } /* Accumulate the values */ - if (0 > TALER_amount_add (&total_amount, - &total_amount, + if (0 > TALER_amount_add (&wc->request.amount, + &wc->request.amount, &dk->meta.value)) { GNUNET_break_op (0); SET_ERROR_WITH_DETAIL (wc, - ERROR_AGE_AMOUNT_OVERFLOW, + ERROR_AMOUNT_OVERFLOW, which, "amount"); - return GNUNET_NO; + return; } /* Accumulate the withdraw fees */ - if (0 > TALER_amount_add (&total_fee, - &total_fee, + if (0 > TALER_amount_add (&wc->request.fee, + &wc->request.fee, &dk->meta.fees.withdraw)) { GNUNET_break_op (0); SET_ERROR_WITH_DETAIL (wc, - ERROR_AGE_AMOUNT_OVERFLOW, + ERROR_AMOUNT_OVERFLOW, which, "fee"); - return GNUNET_NO; - } - wc->typ.age.commitment.denom_serials[i] = dk->meta.serial; - } - - /* Save the total amount including fees */ - GNUNET_assert (0 < - TALER_amount_add ( - &wc->typ.age.commitment.amount_with_fee, - &total_amount, - &total_fee)); - - /* Check that the client signature authorizing the withdrawal is valid. */ - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_age_withdraw_verify ( - &wc->typ.age.commitment.h_commitment, - &wc->typ.age.commitment.amount_with_fee, - &TEH_age_restriction_config.mask, - wc->typ.age.commitment.max_age, - &wc->typ.age.commitment.reserve_pub, - &wc->typ.age.commitment.reserve_sig)) - { - GNUNET_break_op (0); - SET_ERROR (wc, - ERROR_RESERVE_SIGNATURE_INVALID); - return GNUNET_NO; - } - - return GNUNET_OK; -} - - -/** - * Check if the keys in the request are valid for batch withdrawal. - * - * @param[in,out] wc context for the batch withdraw request processing - * @param ksh key state handle - * @return GNUNET_OK on success, - * GNUNET_NO on error (and response being sent) - */ -static enum GNUNET_GenericReturnValue -batch_withdraw_phase_check_keys ( - struct WithdrawContext *wc, - struct TEH_KeyStateHandle *ksh) -{ - for (unsigned int i = 0; i<wc->num_coins; i++) - { - struct PlanchetContext *pc = &wc->typ.batch.planchets[i]; - struct TEH_DenominationKey *dk; - enum GNUNET_GenericReturnValue r; - - r = find_denomination (wc, - ksh, - &pc->collectable.denom_pub_hash, - &dk); - - if (GNUNET_OK != r) - return GNUNET_NO; - - GNUNET_assert (NULL != dk); - - if (dk->denom_pub.bsign_pub_key->cipher != - pc->blinded_planchet.blinded_message->cipher) - { - /* denomination cipher and blinded planchet cipher not the same */ - GNUNET_break_op (0); - SET_ERROR (wc, - ERROR_CIPHER_MISMATCH); - return GNUNET_NO; - } - - if (0 > - TALER_amount_add (&pc->collectable.amount_with_fee, - &dk->meta.value, - &dk->meta.fees.withdraw)) - { - GNUNET_break (0); - SET_ERROR (wc, - ERROR_BATCH_AMOUNT_FEE_OVERFLOW); - return GNUNET_NO; - } - - if (0 > - TALER_amount_add (&wc->typ.batch.batch_total, - &wc->typ.batch.batch_total, - &pc->collectable.amount_with_fee)) - { - GNUNET_break (0); - SET_ERROR (wc, - ERROR_BATCH_AMOUNT_FEE_OVERFLOW); - return GNUNET_NO; - } - - TALER_coin_ev_hash (&pc->blinded_planchet, - &pc->collectable.denom_pub_hash, - &pc->collectable.h_coin_envelope); - - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_withdraw_verify ( - &pc->collectable.denom_pub_hash, - &pc->collectable.amount_with_fee, - &pc->collectable.h_coin_envelope, - &pc->collectable.reserve_pub, - &pc->collectable.reserve_sig)) - { - GNUNET_break_op (0); - SET_ERROR (wc, - ERROR_RESERVE_SIGNATURE_INVALID); - return GNUNET_NO; + return; } + wc->request.persist.denom_serials[i] = dk->meta.serial; } - /* everything parsed */ - return GNUNET_OK; -} - - -/** - * Check if the keys in the request are valid for withdrawing. - * - * @param[in,out] wc context for request processing - */ -static void -phase_check_keys (struct WithdrawContext *wc) -{ - struct TEH_KeyStateHandle *ksh; - enum GNUNET_GenericReturnValue r; - - ksh = TEH_keys_get_state (); - if (NULL == ksh) + /* Save the total amount including fees */ + if (0 > TALER_amount_add ( + &wc->request.persist.amount_with_fee, + &wc->request.amount, + &wc->request.fee)) { - GNUNET_break (0); - SET_ERROR (wc, - ERROR_KEYS_MISSING); + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (wc, + ERROR_AMOUNT_OVERFLOW, + which, + "amount+fee"); return; } - switch (wc->withdraw_type) - { - case WITHDRAW_TYPE_BATCH: - r = batch_withdraw_phase_check_keys (wc, ksh); - break; - case WITHDRAW_TYPE_AGE: - r = age_withdraw_phase_check_keys (wc, ksh); - break; - default: - GNUNET_break (0); - r = GNUNET_SYSERR; - } - - switch (r) - { - case GNUNET_OK: - wc->phase++; - break; - case GNUNET_NO: - /* error generated by function, simply return*/ - break; - case GNUNET_SYSERR: - GNUNET_break (0); - SET_ERROR (wc, - ERROR_KEYS_MISSING); - break; - default: - GNUNET_break (0); - } + wc->phase++; } /** * Check that the client signature authorizing the withdrawal is valid. - * NOTE: this is only applicable to age-withdraw; the existing - * batch-withdraw REST-API signs each planchet and they have to be - * checked during the call to phase_check_keys. * * @param[in,out] wc request context to check */ @@ -1555,39 +1017,49 @@ static void phase_check_reserve_signature ( struct WithdrawContext *wc) { - switch (wc->withdraw_type) + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_withdraw_verify ( + &wc->request.amount, + &wc->request.fee, + &wc->request.persist.h_planchets, + (wc->request.persist.age_restricted) ? + &TEH_age_restriction_config.mask : + NULL, + (wc->request.persist.age_restricted) ? wc->request.persist.max_age : 0, + &wc->request.persist.reserve_pub, + &wc->request.persist.reserve_sig)) { - case WITHDRAW_TYPE_BATCH: - /* signature checks has occurred in batch_withdraw_phase_check_keys */ - break; - case WITHDRAW_TYPE_AGE: - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_age_withdraw_verify ( - &wc->typ.age.commitment.h_commitment, - &wc->typ.age.commitment.amount_with_fee, - &TEH_age_restriction_config.mask, - wc->typ.age.commitment.max_age, - &wc->typ.age.commitment.reserve_pub, - &wc->typ.age.commitment.reserve_sig)) - { - GNUNET_break_op (0); - SET_ERROR (wc, - ERROR_RESERVE_SIGNATURE_INVALID); - return; - } - break; - default: - GNUNET_break (0); + GNUNET_break_op (0); + SET_ERROR (wc, + ERROR_RESERVE_SIGNATURE_INVALID); return; } + /** + * Now that request is verified, calculate the h_commitment for persistence + */ + TALER_wallet_withdraw_commit ( + &wc->request.persist.reserve_pub, + &wc->request.amount, + &wc->request.fee, + &wc->request.persist.h_planchets, + (wc->request.persist.age_restricted) ? + &TEH_age_restriction_config.mask : + NULL, + (wc->request.persist.age_restricted) ? + wc->request.persist.max_age : + 0, + &wc->request.persist.h_commitment); + + wc->phase++; } /** - * Cleanup routine for withdraw reqwuest. + * Cleanup routine for withdraw request. * The function is called upon completion of the request * that should clean up @a rh_ctx. Can be NULL. * @@ -1604,40 +1076,28 @@ clean_withdraw_rc (struct TEH_RequestContext *rc) wc->lch = NULL; } - switch (wc->withdraw_type) - { - case WITHDRAW_TYPE_BATCH: - for (unsigned int i = 0; i<wc->num_coins; i++) - { - struct PlanchetContext *pc = &wc->typ.batch.planchets[i]; + GNUNET_free (wc->request.denoms_h); + for (unsigned int i = 0; i<wc->request.num_planchets; i++) + TALER_blinded_planchet_free (&wc->request.planchets[i]); - TALER_blinded_planchet_free (&pc->blinded_planchet); - TALER_blinded_denom_sig_free (&pc->collectable.sig); - } - GNUNET_free (wc->typ.batch.planchets); - break; + GNUNET_free (wc->request.planchets); - case WITHDRAW_TYPE_AGE: - for (unsigned int i = 0; i<wc->num_coins; i++) - { - for (unsigned int kappa = 0; kappa<TALER_CNC_KAPPA; kappa++) - { - TALER_blinded_planchet_free (&wc->typ.age.planchets[i][kappa]); - } - } - for (unsigned int i = 0; i<wc->num_coins; i++) - { - TALER_blinded_denom_sig_free (&wc->typ.age.commitment.denom_sigs[i]); - } - GNUNET_free (wc->typ.age.commitment.h_coin_evs); - GNUNET_free (wc->typ.age.commitment.denom_sigs); - GNUNET_free (wc->typ.age.denom_hs); - GNUNET_free (wc->typ.age.planchets); - GNUNET_free (wc->typ.age.commitment.denom_serials); - break; + if (NULL != wc->request.persist.denom_sigs) + for (unsigned int i = 0; i<wc->request.persist.num_coins; i++) + TALER_blinded_denom_sig_free (&wc->request.persist.denom_sigs[i]); - default: - GNUNET_break (0); + GNUNET_free (wc->request.persist.denom_sigs); + GNUNET_free (wc->request.persist.h_coin_evs); + GNUNET_free (wc->request.persist.denom_serials); + + if (wc->request.is_idempotent) + { + for (unsigned int i = 0; i<wc->request.idem.num_coins; i++) + TALER_blinded_denom_sig_free (&wc->request.idem.denom_sigs[i]); + + GNUNET_free (wc->request.idem.h_coin_evs); + GNUNET_free (wc->request.idem.denom_sigs); + GNUNET_free (wc->request.idem.denom_serials); } if (ERROR_LEGITIMIZATION_RESULT == wc->error.code && @@ -1652,52 +1112,6 @@ clean_withdraw_rc (struct TEH_RequestContext *rc) /** - * Send a response to a "age-withdraw" request. - * - * @param[in,out] wc context for the operation - */ -static void -age_withdraw_phase_generate_reply_success ( - struct WithdrawContext *wc) -{ - struct MHD_Connection *connection - = wc->rc->connection; - const struct TALER_AgeWithdrawCommitmentHashP *ach - = &wc->typ.age.commitment.h_commitment; - uint32_t noreveal_index - = wc->typ.age.commitment.noreveal_index; - struct TALER_ExchangePublicKeyP pub; - struct TALER_ExchangeSignatureP sig; - enum TALER_ErrorCode ec; - - ec = TALER_exchange_online_age_withdraw_confirmation_sign ( - &TEH_keys_exchange_sign_, - ach, - noreveal_index, - &pub, - &sig); - if (TALER_EC_NONE != ec) - { - SET_ERROR_WITH_FIELD (wc, - ERROR_AGE_CONFIRMATION_SIGN, - ec); - return; - } - - finish_loop (wc, - TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_uint64 ("noreveal_index", - noreveal_index), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &sig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &pub))); -} - - -/** * Generates response for the batch- or age-withdraw request. * * @param wc withdraw operation context @@ -1705,17 +1119,69 @@ age_withdraw_phase_generate_reply_success ( static void phase_generate_reply_success (struct WithdrawContext *wc) { - switch (wc->withdraw_type) + struct TALER_EXCHANGEDB_Withdraw *db_obj; + + db_obj = wc->request.is_idempotent ? + &wc->request.idem : + &wc->request.persist; + + if (wc->request.persist.age_restricted) { - case WITHDRAW_TYPE_BATCH: - batch_withdraw_phase_generate_reply_success (wc); - break; - case WITHDRAW_TYPE_AGE: - age_withdraw_phase_generate_reply_success (wc); - break; - default: - GNUNET_break (0); + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + enum TALER_ErrorCode ec; + + ec = TALER_exchange_online_withdraw_age_confirmation_sign ( + &TEH_keys_exchange_sign_, + &db_obj->h_commitment, + db_obj->noreveal_index, + &pub, + &sig); + if (TALER_EC_NONE != ec) + { + SET_ERROR_WITH_FIELD (wc, + ERROR_CONFIRMATION_SIGN, + ec); + return; + } + + finish_loop (wc, + TALER_MHD_REPLY_JSON_PACK ( + wc->rc->connection, + MHD_HTTP_CREATED, + GNUNET_JSON_pack_uint64 ("noreveal_index", + db_obj->noreveal_index), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub))); + } + else /* not age restricted */ + { + json_t *sigs; + + sigs = json_array (); + GNUNET_assert (NULL != sigs); + for (unsigned int i = 0; i<db_obj->num_coins; i++) + { + GNUNET_assert ( + 0 == + json_array_append_new ( + sigs, + GNUNET_JSON_PACK ( + TALER_JSON_pack_blinded_denom_sig ( + NULL, + &db_obj->denom_sigs[i])))); + } + finish_loop (wc, + TALER_MHD_REPLY_JSON_PACK ( + wc->rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("ev_sigs", + sigs))); } + + TEH_METRICS_withdraw_num_coins += db_obj->num_coins; } @@ -1735,10 +1201,8 @@ phase_generate_reply_error ( GNUNET_assert (ERROR_MAX != wc->error.code); if (needs_idempotency_check[wc->error.code] && - check_idempotency (wc)) + withdraw_is_idempotent (wc)) { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "request is idempotent\n"); return; } @@ -1804,7 +1268,7 @@ phase_generate_reply_error ( NULL)); break; - case ERROR_DENOMINATION_BATCH_SIGN: + case ERROR_DENOMINATION_SIGN: finish_loop (wc, TALER_MHD_reply_with_ec ( wc->rc->connection, @@ -1867,6 +1331,14 @@ phase_generate_reply_error ( NULL)); break; + case ERROR_RESERVE_CIPHER_UNKNOWN: + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, + "cipher")); + break; + case ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION: { char msg[256]; @@ -1882,7 +1354,7 @@ phase_generate_reply_error ( break; } - case ERROR_AGE_MAXIMUM_AGE_TOO_LARGE: + case ERROR_MAXIMUM_AGE_TOO_LARGE: finish_loop (wc, TALER_MHD_REPLY_JSON_PACK ( wc->rc->connection, @@ -1904,26 +1376,27 @@ phase_generate_reply_error ( wc->error.details.lowest_age)); break; - case ERROR_AGE_INSUFFICIENT_FUNDS: + case ERROR_AMOUNT_OVERFLOW: + case ERROR_FEE_OVERFLOW: finish_loop (wc, - TEH_RESPONSE_reply_reserve_insufficient_balance ( + TALER_MHD_reply_with_error ( wc->rc->connection, - TALER_EC_EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS, - &wc->error.details.reserve_balance, - &wc->typ.age.commitment.amount_with_fee, - &wc->reserve_pub)); + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_OVERFLOW, + wc->error.details.which)); break; - case ERROR_AGE_AMOUNT_OVERFLOW: + case ERROR_AMOUNT_PLUS_FEE_OVERFLOW: finish_loop (wc, TALER_MHD_reply_with_error ( wc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_OVERFLOW, - wc->error.details.which)); + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW, + NULL)); break; - case ERROR_AGE_CONFIRMATION_SIGN: + + case ERROR_CONFIRMATION_SIGN: finish_loop (wc, TALER_MHD_reply_with_ec ( wc->rc->connection, @@ -1931,17 +1404,17 @@ phase_generate_reply_error ( NULL)); break; - case ERROR_BATCH_INSUFFICIENT_FUNDS: + case ERROR_INSUFFICIENT_FUNDS: finish_loop (wc, TEH_RESPONSE_reply_reserve_insufficient_balance ( wc->rc->connection, TALER_EC_EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS, &wc->error.details.reserve_balance, - &wc->typ.batch.batch_total, - &wc->reserve_pub)); + &wc->request.persist.amount_with_fee, + &wc->request.persist.reserve_pub)); break; - case ERROR_BATCH_IDEMPOTENT_PLANCHET: + case ERROR_IDEMPOTENT_PLANCHET: { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Idempotent coin in batch, not allowed. Aborting.\n"); @@ -1954,7 +1427,7 @@ phase_generate_reply_error ( break; } - case ERROR_BATCH_NONCE_RESUSE: + case ERROR_NONCE_RESUSE: finish_loop (wc, TALER_MHD_reply_with_error ( wc->rc->connection, @@ -1963,15 +1436,6 @@ phase_generate_reply_error ( NULL)); break; - case ERROR_BATCH_AMOUNT_FEE_OVERFLOW: - finish_loop (wc, - TALER_MHD_reply_with_error ( - wc->rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW, - NULL)); - break; - case ERROR_RESERVE_SIGNATURE_INVALID: finish_loop (wc, TALER_MHD_reply_with_ec ( @@ -1992,144 +1456,40 @@ phase_generate_reply_error ( /** - * Creates a new context for the incoming batch-withdraw request - * - * @param[in,out] wc context of the batch-witrhdraw, to be filled - * @param root json body of the request - * @return GNUNET_OK on success, GNUNET_SYSERR otherwise (response sent) - */ -static enum GNUNET_GenericReturnValue -batch_withdraw_new_request ( - struct WithdrawContext *wc, - const json_t *root) -{ - const json_t *planchets; - - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &wc->typ.batch.batch_total)); - - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_array_const ("planchets", - &planchets), - GNUNET_JSON_spec_end () - }; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (wc->rc->connection, - root, - spec); - if (GNUNET_OK != res) - return res; - } - } - - wc->num_coins = json_array_size (planchets); - if (0 == wc->num_coins) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (wc, - ERROR_REQUEST_PARAMETER_MALFORMED, - hint, - "planchets"); - return GNUNET_SYSERR; - } - - if (wc->num_coins > TALER_MAX_FRESH_COINS) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (wc, - ERROR_REQUEST_PARAMETER_MALFORMED, - hint, - "too many planchets"); - return GNUNET_SYSERR; - } - - wc->typ.batch.planchets - = GNUNET_new_array (wc->num_coins, - struct PlanchetContext); - - for (unsigned int i = 0; i<wc->num_coins; i++) - { - struct PlanchetContext *pc = &wc->typ.batch.planchets[i]; - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_fixed_auto ( - "reserve_sig", - &pc->collectable.reserve_sig), - GNUNET_JSON_spec_fixed_auto ( - "denom_pub_hash", - &pc->collectable.denom_pub_hash), - TALER_JSON_spec_blinded_planchet ( - "coin_ev", - &pc->blinded_planchet), - GNUNET_JSON_spec_end () - }; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data ( - wc->rc->connection, - json_array_get (planchets, i), - ispec); - if (GNUNET_OK != res) - return res; - } - - pc->collectable.reserve_pub = wc->reserve_pub; - for (unsigned int k = 0; k<i; k++) - { - const struct PlanchetContext *kpc = &wc->typ.batch.planchets[k]; - - if (0 == - TALER_blinded_planchet_cmp ( - &kpc->blinded_planchet, - &pc->blinded_planchet)) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (wc, - ERROR_REQUEST_PARAMETER_MALFORMED, - hint, - "duplicate planchet"); - return GNUNET_SYSERR; - } - } - } - return GNUNET_OK; -} - - -/** - * Creates a new context for the incoming age-withdraw request + * Creates a new context for the incoming withdraw request * * @param wc withdraw request context * @param root json body of the request * @return GNUNET_OK on success, GNUNET_SYSERR otherwise (response sent) */ static enum GNUNET_GenericReturnValue -age_withdraw_new_request ( +withdraw_new_request ( struct WithdrawContext *wc, const json_t *root) { - wc->typ.age.commitment.reserve_pub = wc->reserve_pub; - /* parse the json body */ { - const json_t *j_denom_hs; - const json_t *j_blinded_planchets; + const json_t *j_denoms_h; + const json_t *j_coin_evs; + const char *cipher; + bool no_max_age; + struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_array_const ("denom_hs", - &j_denom_hs), - GNUNET_JSON_spec_array_const ("blinded_planchets", - &j_blinded_planchets), - GNUNET_JSON_spec_uint16 ("max_age", - &wc->typ.age.commitment.max_age), + GNUNET_JSON_spec_string ("cipher", + &cipher), + GNUNET_JSON_spec_fixed_auto ("reserve_pub", + &wc->request.persist.reserve_pub), + GNUNET_JSON_spec_array_const ("denoms_h", + &j_denoms_h), + GNUNET_JSON_spec_array_const ("coin_evs", + &j_coin_evs), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint16 ("max_age", + &wc->request.persist.max_age), + &no_max_age), GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &wc->typ.age.commitment.reserve_sig), + &wc->request.persist.reserve_sig), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue res; @@ -2140,39 +1500,79 @@ age_withdraw_new_request ( if (GNUNET_OK != res) return res; - /* The age value MUST be on the beginning of an age group */ - if (wc->typ.age.commitment.max_age != - TALER_get_lowest_age (&TEH_age_restriction_config.mask, - wc->typ.age.commitment.max_age)) + /* For now, we only support cipher "ED25519" for signatures by the reserve */ + if (strcmp ("ED25519", cipher)) { GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL ( - wc, - ERROR_REQUEST_PARAMETER_MALFORMED, - hint, - "max_age must be the lower edge of an age group"); + SET_ERROR_WITH_FIELD (wc, + ERROR_RESERVE_CIPHER_UNKNOWN, + cipher); return GNUNET_SYSERR; } + wc->request.persist.age_restricted = ! no_max_age; + + if (wc->request.persist.age_restricted) + { + /* The age value MUST be on the beginning of an age group */ + if (wc->request.persist.max_age != + TALER_get_lowest_age (&TEH_age_restriction_config.mask, + wc->request.persist.max_age)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL ( + wc, + ERROR_REQUEST_PARAMETER_MALFORMED, + hint, + "max_age must be the lower edge of an age group"); + return GNUNET_SYSERR; + } + } + /* validate array size */ { - size_t num_coins = json_array_size (j_denom_hs); + size_t num_coins = json_array_size (j_denoms_h); + size_t array_size = json_array_size (j_coin_evs); const char *error = NULL; - _Static_assert ((TALER_MAX_FRESH_COINS < INT_MAX / TALER_CNC_KAPPA), - "TALER_MAX_FRESH_COINS too large"); - if (0 == num_coins) - error = "denoms_h must not be empty"; - else if (num_coins != json_array_size (j_blinded_planchets)) - error = "denoms_h and coins_evs must be arrays of the same size"; - else if (num_coins > TALER_MAX_FRESH_COINS) + +#define BAIL_IF(cond, msg) \ + if ((cond)) { \ + error = (msg); break; \ + } + + do { + BAIL_IF (0 == num_coins, + "denoms_h must not be empty") + /** - * The wallet had committed to more than the maximum coins allowed, the - * reserve has been charged, but now the user can not withdraw any money - * from it. Note that the user can't get their money back in this case! - */ - error = - "maximum number of coins that can be withdrawn has been exceeded"; + * The wallet had committed to more than the maximum coins allowed, the + * reserve has been charged, but now the user can not withdraw any money + * from it. Note that the user can't get their money back in this case! + */ + BAIL_IF (num_coins > TALER_MAX_FRESH_COINS, + "maximum number of coins that can be withdrawn has been exceeded") + + BAIL_IF ((! wc->request.persist.age_restricted) && + (num_coins !=array_size), + "denoms_h and coin_evs must be arrays of the same size") + + _Static_assert ( + TALER_MAX_FRESH_COINS < INT_MAX / TALER_CNC_KAPPA, + "TALER_MAX_FRESH_COINS too large"); + + BAIL_IF (wc->request.persist.age_restricted && + ((TALER_CNC_KAPPA * num_coins) != array_size), + "coin_evs must be an array of length " + TALER_CNC_KAPPA_STR + "*len(denoms_h)") + + wc->request.persist.num_coins = num_coins; + wc->request.num_planchets = array_size; + + } while (0); + +#undef BAIL_IF if (NULL != error) { @@ -2183,21 +1583,22 @@ age_withdraw_new_request ( error); return GNUNET_SYSERR; } - wc->num_coins = (unsigned int) num_coins; - wc->typ.age.commitment.num_coins = (unsigned int) num_coins; + } - wc->typ.age.denom_hs - = GNUNET_new_array (wc->num_coins, + wc->request.denoms_h + = GNUNET_new_array (wc->request.persist.num_coins, struct TALER_DenominationHashP); + + /* extract the denomination hashes */ { size_t idx; json_t *value; - json_array_foreach (j_denom_hs, idx, value) { + json_array_foreach (j_denoms_h, idx, value) { struct GNUNET_JSON_Specification ispec[] = { GNUNET_JSON_spec_fixed_auto (NULL, - &wc->typ.age.denom_hs[idx]), + &wc->request.denoms_h[idx]), GNUNET_JSON_spec_end () }; @@ -2209,15 +1610,11 @@ age_withdraw_new_request ( } } - { - typedef struct TALER_BlindedPlanchet - _array_of_kappa_planchets[TALER_CNC_KAPPA]; - - wc->typ.age.planchets = GNUNET_new_array (wc->num_coins, - _array_of_kappa_planchets); - } + wc->request.planchets = + GNUNET_new_array (wc->request.num_planchets, + struct TALER_BlindedPlanchet); - /* calculate the hash over the data */ + /* calculate the hash over the blinded coin envelopes */ { struct GNUNET_HashContext *hash_context; @@ -2226,142 +1623,91 @@ age_withdraw_new_request ( /* Parse blinded envelopes. */ { - json_t *j_kappa_planchets; + json_t *j_cev; size_t idx; - json_array_foreach (j_blinded_planchets, idx, j_kappa_planchets) { - if (! json_is_array (j_kappa_planchets)) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (wc, - ERROR_REQUEST_PARAMETER_MALFORMED, - hint, - "all entries in array blinded_planchets must be arrays"); - return GNUNET_SYSERR; - } - if (TALER_CNC_KAPPA != json_array_size (j_kappa_planchets)) + json_array_foreach (j_coin_evs, idx, j_cev) { + /* Now parse the individual envelopes and calculate the hash of + * the commitment along the way. */ + + struct GNUNET_JSON_Specification kspec[] = { + TALER_JSON_spec_blinded_planchet (NULL, + &wc->request.planchets[idx]), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (wc->rc->connection, + j_cev, + kspec); + if (GNUNET_OK != res) + return res; + + + /* Continue to hash of the coin candidates */ { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (wc, - ERROR_REQUEST_PARAMETER_MALFORMED, - hint, - "all entries in array blinded_planchets must be arrays of size " - TALER_CNC_KAPPA_STR); - return GNUNET_SYSERR; + struct TALER_BlindedCoinHashP bch; + + TALER_coin_ev_hash ( + &wc->request.planchets[idx], + &wc->request.denoms_h[idx % wc->request.persist.num_coins], + &bch); + GNUNET_CRYPTO_hash_context_read (hash_context, + &bch, + sizeof(bch)); } - /* Now parse the individual kappa envelopes and calculate the hash of - * the commitment along the way. */ + /* Check for duplicate planchets. Technically a bug on + * the client side that is harmless for us, but still + * not allowed per protocol */ + for (unsigned int i = 0; i < idx; i++) { - size_t kappa; - json_t *kvalue; - - json_array_foreach (j_kappa_planchets, kappa, kvalue) { - struct GNUNET_JSON_Specification kspec[] = { - TALER_JSON_spec_blinded_planchet (NULL, - &wc->typ.age.planchets[idx][ - kappa]), - GNUNET_JSON_spec_end () - }; - - res = TALER_MHD_parse_json_data (wc->rc->connection, - kvalue, - kspec); - if (GNUNET_OK != res) - return res; - - /* Continue to hash of the coin candidates */ - { - struct TALER_BlindedCoinHashP bch; - - TALER_coin_ev_hash (&wc->typ.age.planchets[idx][kappa], - &wc->typ.age.denom_hs[idx], - &bch); - GNUNET_CRYPTO_hash_context_read (hash_context, - &bch, - sizeof(bch)); - } - - /* Check for duplicate planchets. Technically a bug on - * the client side that is harmless for us, but still - * not allowed per protocol */ - for (unsigned int i = 0; i < idx; i++) - { - if (0 == - TALER_blinded_planchet_cmp ( - &wc->typ.age.planchets[idx][kappa], - &wc->typ.age.planchets[i][kappa])) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (wc, - ERROR_REQUEST_PARAMETER_MALFORMED, - hint, - "duplicate planchet"); - return GNUNET_SYSERR; - } - } /* end duplicate check */ - } /* json_array_foreach over j_kappa_planchets */ - } /* scope of kappa/kvalue */ - } /* json_array_foreach over j_blinded_planchets */ + if (0 == + TALER_blinded_planchet_cmp ( + &wc->request.planchets[idx], + &wc->request.planchets[i])) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (wc, + ERROR_REQUEST_PARAMETER_MALFORMED, + hint, + "duplicate planchet"); + return GNUNET_SYSERR; + } + } /* end duplicate check */ + } /* json_array_foreach over j_coin_evs */ } /* scope of j_kappa_planchets, idx */ - /* Finally, calculate the h_commitment from all blinded envelopes */ + /* Finally, calculate the hash from all blinded envelopes */ GNUNET_CRYPTO_hash_context_finish (hash_context, - &wc->typ.age.commitment.h_commitment. - hash); + &wc->request.persist.h_planchets.hash); - } /* scope of hash_context */ - } /* scope of j_denom_hs, j_blinded_planchets */ + } /* scope of hash_context */ + } /* scope of j_denoms_h, j_blinded_coin_evs */ return GNUNET_OK; } -/** - * Handle a "/reserves/$RESERVE_PUB/{age,batch}-withdraw" request. - * - * @param rc request context - * @param typ withdraw type - * @param root uploaded JSON data - * @param reserve_pub public key of the reserve - * @return MHD result code - */ -static MHD_RESULT -handler_withdraw ( +MHD_RESULT +TEH_handler_withdraw ( struct TEH_RequestContext *rc, - enum WithdrawType typ, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root) + const json_t *root, + const char *const args[2]) { struct WithdrawContext *wc = rc->rh_ctx; enum GNUNET_GenericReturnValue r; - if (NULL == wc) + (void) args; + + while (NULL == wc) { wc = GNUNET_new (struct WithdrawContext); rc->rh_ctx = wc; rc->rh_cleaner = &clean_withdraw_rc; wc->rc = rc; wc->now = GNUNET_TIME_timestamp_get (); - wc->withdraw_type = typ; - wc->reserve_pub = *reserve_pub; - - switch (typ) - { - case WITHDRAW_TYPE_BATCH: - r = batch_withdraw_new_request (wc, root); - break; - case WITHDRAW_TYPE_AGE: - r = age_withdraw_new_request (wc, root); - break; - default: - GNUNET_break (0); - r = GNUNET_SYSERR; - /* TODO: find better error code here:? */ - SET_ERROR (wc, - ERROR_INTERNAL_INVARIANT_FAILURE); - } + r = withdraw_new_request (wc, root); if (GNUNET_OK != r) return (GNUNET_SYSERR == r) ? MHD_NO : MHD_YES; @@ -2410,32 +1756,3 @@ handler_withdraw ( } } } - - -MHD_RESULT -TEH_handler_batch_withdraw ( - struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root) -{ - return handler_withdraw (rc, - WITHDRAW_TYPE_BATCH, - reserve_pub, - root); -} - - -MHD_RESULT -TEH_handler_age_withdraw ( - struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root) -{ - return handler_withdraw (rc, - WITHDRAW_TYPE_AGE, - reserve_pub, - root); -} - - -/* end of taler-exchange-httpd_withdraw.c */ diff --git a/src/exchange/taler-exchange-httpd_withdraw.h b/src/exchange/taler-exchange-httpd_withdraw.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2024 Taler Systems SA + Copyright (C) 2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -15,10 +15,8 @@ */ /** * @file taler-exchange-httpd_withdraw.h - * @brief Handle /reserve/$RESERVE_PUB/{age,batch}-withdraw requests - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff + * @brief Handle /withdraw requests + * @note This endpoint was introduced in v24 of the protocol. * @author Özgür Kesim */ #ifndef TALER_EXCHANGE_HTTPD_WITHDRAW_H @@ -26,52 +24,37 @@ #include <microhttpd.h> #include "taler-exchange-httpd.h" - /** * Resume suspended connections, we are shutting down. */ void TEH_withdraw_cleanup (void); - /** - * Handle a "/reserves/$RESERVE_PUB/age-withdraw" request. - * - * Parses the batch of commitments to withdraw age restricted coins, and checks - * that the signature "reserve_sig" makes this a valid withdrawal request from - * the specified reserve. If the request is valid, the response contains a - * noreveal_index which the client has to use for the subsequent call to - * /age-withdraw/$ACH/reveal. + * @brief Handle a "/withdraw" request. + * @note This endpoint was introduced in v24 of the protocol. * - * @param rc request context - * @param root uploaded JSON data - * @param reserve_pub public key of the reserve - * @return MHD result code - */ -MHD_RESULT -TEH_handler_age_withdraw ( - struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root); - - -/** - * Handle a "/reserves/$RESERVE_PUB/batch-withdraw" request. Parses the batch of - * requested "denom_pub" which specifies the key/value of the coin to be - * withdrawn, and checks that the signature "reserve_sig" makes this a valid - * withdrawal request from the specified reserve. If so, the envelope with - * the blinded coin "coin_ev" is passed down to execute the withdrawal - * operation. + * Parses the batch of requested "denom_pub" which specifies + * the key/value of the respective coin to be withdrawn, + * and checks the signature "reserve_sig" for given "reserve_pub" + * makes this a valid withdrawal request from the specific reserve. + * If the "max_age" value is set in the request, + * it is considered a commitment to withdraw age restricted coins. + * If the request is valid, the response contains a noreveal_index + * which the client has to use for the subsequent call to /reveal-withdraw/$ACH. + * If "max_age" value is not set, and the request is valid, the envelopes + * with the blinded coins "blinded_coin_evs" is processed + * and the client receives the blinded signatures as response. * * @param rc request context * @param root uploaded JSON data - * @param reserve_pub public key of the reserve + * @param args array of additional options * @return MHD result code */ MHD_RESULT -TEH_handler_batch_withdraw ( +TEH_handler_withdraw ( struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root); + const json_t *root, + const char *const args[2]); #endif diff --git a/src/exchangedb/0009-age_withdraw.sql b/src/exchangedb/0009-age_withdraw.sql @@ -0,0 +1,45 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- +-- @author Özgür Kesim + + +-- Drop the table age_withdraw +CREATE FUNCTION alter_table_age_withdraw9( + IN partition_suffix TEXT DEFAULT NULL) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + old_name TEXT; + new_name TEXT; +BEGIN + DROP table age_withdraw; +END; +$$; + + +INSERT INTO exchange.exchange_tables + ( name + , version + , action + , partitioned + , by_range) +VALUES + ( 'age_withdraw9' + , 'exchange-0009' + , 'alter' + , TRUE + , FALSE); +\ No newline at end of file diff --git a/src/exchangedb/0009-aggregation_transient.sql b/src/exchangedb/0009-aggregation_transient.sql @@ -0,0 +1,68 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + + +CREATE FUNCTION foreign_table_aggregation_transient9() +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'aggregation_transient'; +BEGIN + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_foreign_wire_target_h_payto' + ' FOREIGN KEY (wire_target_h_payto) ' + ' REFERENCES wire_targets (wire_target_h_payto) ON DELETE RESTRICT' + ); +END +$$; + +CREATE FUNCTION constrain_table_aggregation_transient9( + IN partition_suffix TEXT +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'aggregation_transient'; +BEGIN + table_name = concat_ws('_', table_name, partition_suffix); + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_wire_target_h_payto_and_wtid_unique' + ' UNIQUE (wire_target_h_payto,wtid_raw)' + ); +END $$; + + +INSERT INTO exchange_tables + (name + ,version + ,action + ,partitioned + ,by_range) + VALUES + ('aggregation_transient9' + ,'exchange-0009' + ,'foreign' + ,TRUE + ,FALSE), + ('aggregation_transient9' + ,'exchange-0009' + ,'constrain' + ,TRUE + ,FALSE); diff --git a/src/exchangedb/0009-recoup.sql b/src/exchangedb/0009-recoup.sql @@ -0,0 +1,295 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +CREATE FUNCTION alter_table_recoup2() +RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + -- We first drop the existing tables + RAISE NOTICE 'Dropping old recoup tables'; + DROP TABLE recoup_by_reserve; + DROP TABLE recoup; + + -- Altering the function names below for creating the + -- new tables to become the functions to be called + -- by the versioning framework, as it expects the + -- suffix to be the same as the table name. + ALTER FUNCTION create_table_recoup9(TEXT) + RENAME TO create_table_recoup; + ALTER FUNCTION create_table_recoup_by_reserve9(TEXT) + RENAME TO create_table_recoup_by_reserve; +END +$$; + + +CREATE FUNCTION create_table_recoup9( + IN partition_suffix TEXT DEFAULT NULL +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'recoup'; +BEGIN + + PERFORM create_partitioned_table( + 'CREATE TABLE %I' + '(recoup_uuid BIGINT GENERATED BY DEFAULT AS IDENTITY' + ',coin_pub BYTEA NOT NULL CHECK (LENGTH(coin_pub)=32)' + ',coin_sig BYTEA NOT NULL CHECK(LENGTH(coin_sig)=64)' + ',coin_blind BYTEA NOT NULL CHECK(LENGTH(coin_blind)=32)' + ',amount taler_amount NOT NULL' + ',recoup_timestamp INT8 NOT NULL' + ',withdraw_id INT8 NOT NULL' + ') %s ;' + ,table_name + ,'PARTITION BY HASH (coin_pub);' + ,partition_suffix + ); + PERFORM comment_partitioned_table( + 'Information about recoups that were executed between a coin and a reserve. In this type of recoup, the amount is credited back to the reserve from which the coin originated.' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Coin that is being debited in the recoup. Do not CASCADE ON DROP on the coin_pub, as we may keep the coin alive!' + ,'coin_pub' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Identifies the h_commitment of the recouped coin and provides the link to the credited reserve.' + ,'withdraw_id' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Signature by the coin affirming the recoup, of type TALER_SIGNATURE_WALLET_COIN_RECOUP' + ,'coin_sig' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Denomination blinding key used when creating the blinded coin from the planchet. Secret revealed during the recoup to provide the linkage between the coin and the withdraw operation.' + ,'coin_blind' + ,table_name + ,partition_suffix + ); +END +$$; + + +CREATE FUNCTION constrain_table_recoup9( + IN partition_suffix TEXT +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'recoup'; +BEGIN + table_name = concat_ws('_', table_name, partition_suffix); + EXECUTE FORMAT ( + 'CREATE INDEX ' || table_name || '_by_coin_pub_index ' + 'ON ' || table_name || ' ' + '(coin_pub);' + ); + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_recoup_uuid_key' + ' UNIQUE (recoup_uuid) ' + ); +END +$$; + + +CREATE OR REPLACE FUNCTION foreign_table_recoup9() +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'recoup'; +BEGIN + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_foreign_withdraw' + ' FOREIGN KEY (withdraw_id) ' + ' REFERENCES withdraw (withdraw_id) ON DELETE CASCADE' + ',ADD CONSTRAINT ' || table_name || '_foreign_coin_pub' + ' FOREIGN KEY (coin_pub) ' + ' REFERENCES known_coins (coin_pub)' + ); +END +$$; + + +CREATE FUNCTION create_table_recoup_by_reserve9( + IN partition_suffix TEXT DEFAULT NULL +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'recoup_by_reserve'; +BEGIN + PERFORM create_partitioned_table( + 'CREATE TABLE %I' + '(withdraw_id INT8 NOT NULL' -- REFERENCES withdraw (withdraw_id) ON DELETE CASCADE + ',coin_pub BYTEA CHECK (LENGTH(coin_pub)=32)' -- REFERENCES known_coins (coin_pub) + ') %s ;' + ,table_name + ,'PARTITION BY HASH (withdraw_id)' + ,partition_suffix + ); + PERFORM comment_partitioned_table( + 'Information in this table is strictly redundant with that of recoup, but saved by a different primary key for fast lookups by withdraw_id.' + ,table_name + ,partition_suffix + ); +END +$$; + + +CREATE FUNCTION constrain_table_recoup_by_reserve9( + IN partition_suffix TEXT DEFAULT NULL +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'recoup_by_reserve'; +BEGIN + table_name = concat_ws('_', table_name, partition_suffix); + EXECUTE FORMAT ( + 'CREATE INDEX ' || table_name || '_main_index ' + 'ON ' || table_name || ' ' + '(withdraw_id);' + ); +END +$$; + + +CREATE OR REPLACE FUNCTION recoup_insert_trigger() + RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + INSERT INTO recoup_by_reserve + (withdraw_id + ,coin_pub) + VALUES + (NEW.withdraw_id + ,NEW.coin_pub); + INSERT INTO coin_history + (coin_pub + ,table_name + ,serial_id) + VALUES + (NEW.coin_pub + ,'recoup' + ,NEW.recoup_uuid); + INSERT INTO reserve_history + (reserve_pub + ,table_name + ,serial_id) + SELECT + res.reserve_pub + ,'recoup' + ,NEW.recoup_uuid + FROM withdraw wd + JOIN reserves res + USING (reserve_pub) + WHERE wd.withdraw_id = NEW.withdraw_id; + RETURN NEW; +END $$; +COMMENT ON FUNCTION recoup_insert_trigger() + IS 'Replicates recoup inserts into recoup_by_reserve table and updates the coin_history table.'; + + +CREATE OR REPLACE FUNCTION recoup_delete_trigger() + RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + DELETE FROM recoup_by_reserve + WHERE withdraw_id = OLD.withdraw_id + AND coin_pub = OLD.coin_pub; + RETURN OLD; +END $$; +COMMENT ON FUNCTION recoup_delete_trigger() + IS 'Replicate recoup deletions into recoup_by_reserve table.'; + + +CREATE FUNCTION master_table_recoup9() +RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + CREATE TRIGGER recoup_on_insert + AFTER INSERT + ON recoup + FOR EACH ROW EXECUTE FUNCTION recoup_insert_trigger(); + CREATE TRIGGER recoup_on_delete + AFTER DELETE + ON recoup + FOR EACH ROW EXECUTE FUNCTION recoup_delete_trigger(); +END +$$; + + +INSERT INTO exchange_tables + (name + ,version + ,action + ,partitioned + ,by_range) + VALUES + ('recoup2' + ,'exchange-0009' + ,'alter' + ,TRUE + ,FALSE), + ('recoup' -- Note: actual table name needed for create by the versioning framework + ,'exchange-0009' + ,'create' + ,TRUE + ,FALSE), + ('recoup9' + ,'exchange-0009' + ,'constrain' + ,TRUE + ,FALSE), + ('recoup9' + ,'exchange-0009' + ,'foreign' + ,TRUE + ,FALSE), + ('recoup_by_reserve' -- Note: actual table name needed for create by the versioning framework + ,'exchange-0009' + ,'create' + ,TRUE + ,FALSE), + ('recoup_by_reserve9' + ,'exchange-0009' + ,'constrain' + ,TRUE + ,FALSE), + ('recoup9' + ,'exchange-0009' + ,'master' + ,TRUE + ,FALSE); diff --git a/src/exchangedb/0009-statistics.sql b/src/exchangedb/0009-statistics.sql @@ -0,0 +1,587 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +-- Ranges given here must be supported by the date_trunc function of Postgresql! +CREATE TYPE statistic_range AS + ENUM('century', 'decade', 'year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second'); + +CREATE TYPE statistic_type AS + ENUM('amount', 'number'); + +-- -------------- Bucket statistics --------------------- + +CREATE TABLE exchange_statistic_bucket_meta + (bmeta_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY + ,origin TEXT NOT NULL + ,slug TEXT NOT NULL + ,description TEXT NOT NULL + ,stype statistic_type NOT NULL + ,ranges statistic_range[] NOT NULL + ,ages INT4[] NOT NULL + ,UNIQUE(slug,stype) + ,CONSTRAINT equal_array_length + CHECK (array_length(ranges,1) = + array_length(ages,1)) + ); +COMMENT ON TABLE exchange_statistic_bucket_meta + IS 'meta data about a statistic with events falling into buckets we are tracking'; +COMMENT ON COLUMN exchange_statistic_bucket_meta.bmeta_serial_id + IS 'unique identifier for this type of bucket statistic we are tracking'; +COMMENT ON COLUMN exchange_statistic_bucket_meta.origin + IS 'which customization schema does this statistic originate from (used for easy deletion)'; +COMMENT ON COLUMN exchange_statistic_bucket_meta.slug + IS 'keyword (or name) of the statistic; identifies what the statistic is about; should be a slug suitable for a URI path'; +COMMENT ON COLUMN exchange_statistic_bucket_meta.description + IS 'description of the statistic being tracked'; +COMMENT ON COLUMN exchange_statistic_bucket_meta.stype + IS 'statistic type, what kind of data is being tracked, amount or number'; +COMMENT ON COLUMN exchange_statistic_bucket_meta.ranges + IS 'size of the buckets that are being kept for this statistic'; +COMMENT ON COLUMN exchange_statistic_bucket_meta.ages + IS 'determines how long into the past we keep buckets for the range at the given index around (in generations)'; +CREATE INDEX exchange_statistic_bucket_meta_by_origin + ON exchange_statistic_bucket_meta + (origin); + + +CREATE FUNCTION create_table_exchange_statistic_bucket_counter ( + IN partition_suffix TEXT DEFAULT NULL +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM create_partitioned_table ( + 'CREATE TABLE %I' + '(bmeta_serial_id INT8 NOT NULL' + ' REFERENCES exchange_statistic_bucket_meta (bmeta_serial_id) ON DELETE CASCADE' + ',h_payto BYTEA CHECK (LENGTH(h_payto)=32)' + ',bucket_start INT8 NOT NULL' + ',bucket_range statistic_range NOT NULL' + ',cumulative_number INT8 NOT NULL' + ',UNIQUE (h_payto,bmeta_serial_id,bucket_start,bucket_range)' + ') %s;' + ,'exchange_statistic_bucket_counter' + ,'PARTITION BY HASH (h_payto)' + ,partition_suffix + ); + PERFORM comment_partitioned_table( + 'various numeric statistics (cumulative counters) being tracked by bucket into which they fall' + ,'exchange_statistic_bucket_counter' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies what the statistic is about' + ,'bmeta_serial_id' + ,'exchange_statistic_bucket_counter' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies an account (hash of normalized payto) for which the statistic is kept, NULL for global statistics' + ,'h_payto' + ,'exchange_statistic_bucket_counter' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'start date for the bucket in seconds since the epoch' + ,'bucket_start' + ,'exchange_statistic_bucket_counter' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'range of the bucket' + ,'bucket_range' + ,'exchange_statistic_bucket_counter' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'aggregate (sum) of tracked by the statistic; what exactly is tracked is determined by the keyword' + ,'cumulative_number' + ,'exchange_statistic_bucket_counter' + ,partition_suffix + ); +END $$; + + +CREATE FUNCTION create_table_exchange_statistic_bucket_amount ( + IN partition_suffix TEXT DEFAULT NULL +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM create_partitioned_table ( + 'CREATE TABLE %I' + '(bmeta_serial_id INT8 NOT NULL' + ' REFERENCES exchange_statistic_bucket_meta (bmeta_serial_id) ON DELETE CASCADE' + ',h_payto BYTEA CHECK (LENGTH(h_payto)=32)' + ',bucket_start INT8 NOT NULL' + ',bucket_range statistic_range NOT NULL' + ',cumulative_value taler_amount NOT NULL' + ',UNIQUE (h_payto,bmeta_serial_id,bucket_start,bucket_range)' + ') %s;' + ,'exchange_statistic_bucket_amount' + ,'PARTITION BY HASH(h_payto)' + ,partition_suffix + ); + PERFORM comment_partitioned_table ( + 'various amount statistics being tracked' + ,'exchange_statistic_bucket_amount' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies what the statistic is about' + ,'bmeta_serial_id' + ,'exchange_statistic_bucket_amount' + ,partition_suffix + ); + PERFORM comment_partitioned_column ( + 'identifies an account (hash of normalized payto) for which the statistic is kept, NULL for global statistics' + ,'h_payto' + ,'exchange_statistic_bucket_amount' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'start date for the bucket in seconds since the epoch' + ,'bucket_start' + ,'exchange_statistic_bucket_amount' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'range of the bucket' + ,'bucket_range' + ,'exchange_statistic_bucket_amount' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'amount being tracked' + ,'cumulative_value' + ,'exchange_statistic_bucket_amount' + ,partition_suffix + ); +END $$; + + +-- -------------- Interval statistics --------------------- + + +CREATE TABLE exchange_statistic_interval_meta + (imeta_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY + ,origin TEXT NOT NULL + ,slug TEXT NOT NULL + ,description TEXT NOT NULL + ,stype statistic_type NOT NULL + ,ranges INT8[] NOT NULL CHECK (array_length(ranges,1) > 0) + ,precisions INT8[] NOT NULL CHECK (array_length(precisions,1) > 0) + ,UNIQUE(slug,stype) + ,CONSTRAINT equal_array_length + CHECK (array_length(ranges,1) = + array_length(precisions,1)) + ); +COMMENT ON TABLE exchange_statistic_interval_meta + IS 'meta data about an interval statistic we are tracking'; +COMMENT ON COLUMN exchange_statistic_interval_meta.imeta_serial_id + IS 'unique identifier for this type of interval statistic we are tracking'; +COMMENT ON COLUMN exchange_statistic_interval_meta.origin + IS 'which customization schema does this statistic originate from (used for easy deletion)'; +COMMENT ON COLUMN exchange_statistic_interval_meta.slug + IS 'keyword (or name) of the statistic; identifies what the statistic is about; should be a slug suitable for a URI path'; +COMMENT ON COLUMN exchange_statistic_interval_meta.description + IS 'description of the statistic being tracked'; +COMMENT ON COLUMN exchange_statistic_interval_meta.stype + IS 'statistic type, what kind of data is being tracked, amount or number'; +COMMENT ON COLUMN exchange_statistic_interval_meta.ranges + IS 'range of values that is being kept for this statistic, in seconds, must be monotonically increasing'; +COMMENT ON COLUMN exchange_statistic_interval_meta.precisions + IS 'determines how precisely we track which events fall into the range at the same index (allowing us to coalesce events with timestamps in proximity close to the given precision), in seconds, 0 is not allowed'; +CREATE INDEX exchange_statistic_interval_meta_by_origin + ON exchange_statistic_interval_meta + (origin); + + +CREATE FUNCTION create_table_exchange_statistic_counter_event ( + IN partition_suffix TEXT DEFAULT NULL +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM create_partitioned_table( + 'CREATE TABLE %I' + '(nevent_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY' + ',imeta_serial_id INT8' + ' REFERENCES exchange_statistic_interval_meta (imeta_serial_id) ON DELETE CASCADE' + ',h_payto BYTEA CHECK (LENGTH(h_payto)=32)' + ',slot INT8 NOT NULL' + ',delta INT8 NOT NULL' + ',UNIQUE (h_payto,imeta_serial_id,slot)' + ') %s ;' + ,'exchange_statistic_counter_event' + ,'PARTITION BY HASH(h_payto)' + ,partition_suffix + ); + PERFORM comment_partitioned_table( + 'number to decrement an interval statistic by when a certain time value is reached' + ,'exchange_statistic_counter_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'unique identifier for this number event' + ,'nevent_serial_id' + ,'exchange_statistic_counter_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies what the statistic is about; must be of stype number' + ,'imeta_serial_id' + ,'exchange_statistic_counter_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies an account (hash of normalized payto) for which the statistic is kept, NULL for global statistics' + ,'h_payto' + ,'exchange_statistic_counter_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies the time slot at which the given event(s) happened, rounded down by the respective precisions value' + ,'slot' + ,'exchange_statistic_counter_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'total cumulative number that was added at the time identified by slot' + ,'delta' + ,'exchange_statistic_counter_event' + ,partition_suffix + ); +END $$; + + +CREATE FUNCTION constrain_table_exchange_statistic_counter_event( + IN partition_suffix TEXT +) +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT default 'exchange_statistic_counter_event'; +BEGIN + table_name = concat_ws('_', table_name, partition_suffix); + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_nevent_serial_id_key' + ' UNIQUE (nevent_serial_id)' + ); +END $$; + + +CREATE FUNCTION create_table_exchange_statistic_interval_counter ( + IN partition_suffix TEXT DEFAULT NULL +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM create_partitioned_table( + 'CREATE TABLE %I' + '(imeta_serial_id INT8 NOT NULL' + ' REFERENCES exchange_statistic_interval_meta (imeta_serial_id) ON DELETE CASCADE' + ',h_payto BYTEA CHECK (LENGTH(h_payto)=32)' + ',range INT8 NOT NULL' + ',event_delimiter INT8 NOT NULL' + ',cumulative_number INT8 NOT NULL' + ',UNIQUE (h_payto,imeta_serial_id,range)' + ') %s ;' + ,'exchange_statistic_interval_counter' + ,'PARTITION BY HASH(h_payto)' + ,partition_suffix + ); + PERFORM comment_partitioned_table( + 'various numeric statistics (cumulative counters) being tracked' + ,'exchange_statistic_interval_counter' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies what the statistic is about' + ,'imeta_serial_id' + ,'exchange_statistic_interval_counter' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies an account (hash of normalized payto) for which the statistic is kept, NULL for global statistics' + ,'h_payto' + ,'exchange_statistic_interval_counter' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'for which range is this the counter; note that the cumulative_number excludes the values already stored in smaller ranges' + ,'range' + ,'exchange_statistic_interval_counter' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'determines the last event currently included in the interval' + ,'event_delimiter' + ,'exchange_statistic_interval_counter' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'aggregate (sum) of tracked by the statistic; what exactly is tracked is determined by the keyword' + ,'cumulative_number' + ,'exchange_statistic_interval_counter' + ,partition_suffix + ); +END $$; + + +CREATE FUNCTION foreign_table_exchange_statistic_interval_counter() +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'exchange_statistic_interval_counter'; +BEGIN + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_event_delimiter_foreign_key' + ' FOREIGN KEY (event_delimiter) ' + ' REFERENCES exchange_statistic_counter_event (nevent_serial_id) ON DELETE RESTRICT' + ); +END $$; + + +CREATE FUNCTION create_table_exchange_statistic_amount_event ( + IN partition_suffix TEXT DEFAULT NULL +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM create_partitioned_table( + 'CREATE TABLE %I' + '(aevent_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY' + ',imeta_serial_id INT8' + ' REFERENCES exchange_statistic_interval_meta (imeta_serial_id) ON DELETE CASCADE' + ',h_payto BYTEA CHECK (LENGTH(h_payto)=32)' + ',slot INT8 NOT NULL' + ',delta taler_amount NOT NULL' + ',CONSTRAINT event_key UNIQUE (h_payto,imeta_serial_id,slot)' + ') %s ;' + ,'exchange_statistic_amount_event' + ,'PARTITION BY HASH(h_payto)' + ,partition_suffix + ); + PERFORM comment_partitioned_table( + 'amount to decrement an interval statistic by when a certain time value is reached' + ,'exchange_statistic_amount_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'unique identifier for this amount event' + ,'aevent_serial_id' + ,'exchange_statistic_amount_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies what the statistic is about; must be of clazz interval and of stype amount' + ,'imeta_serial_id' + ,'exchange_statistic_amount_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies an account (hash of normalized payto) for which the statistic is kept, NULL for global statistics' + ,'h_payto' + ,'exchange_statistic_amount_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies the time slot at which the given event(s) happened' + ,'slot' + ,'exchange_statistic_amount_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'total cumulative amount that was added at the time identified by slot' + ,'delta' + ,'exchange_statistic_amount_event' + ,partition_suffix + ); +END $$; + + +CREATE FUNCTION constrain_table_exchange_statistic_amount_event( + IN partition_suffix TEXT +) +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT default 'exchange_statistic_amount_event'; +BEGIN + table_name = concat_ws('_', table_name, partition_suffix); + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_aevent_serial_id_key' + ' UNIQUE (aevent_serial_id)' + ); +END $$; + + + +CREATE FUNCTION create_table_exchange_statistic_interval_amount ( + IN partition_suffix TEXT DEFAULT NULL +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM create_partitioned_table( + 'CREATE TABLE %I' + '(imeta_serial_id INT8 NOT NULL' + ' REFERENCES exchange_statistic_interval_meta (imeta_serial_id) ON DELETE CASCADE' + ',h_payto BYTEA CHECK (LENGTH(h_payto)=32)' + ',event_delimiter INT8 NOT NULL' + ',range INT8 NOT NULL' + ',cumulative_value taler_amount NOT NULL' + ',UNIQUE (h_payto,imeta_serial_id,range)' + ') %s ;' + ,'exchange_statistic_interval_amount' + ,'PARTITION BY HASH(h_payto)' + ,partition_suffix + ); + PERFORM comment_partitioned_table( + 'various amount statistics being tracked' + ,'exchange_statistic_interval_amount' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies what the statistic is about' + ,'imeta_serial_id' + ,'exchange_statistic_interval_amount' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies an account (hash of normalized payto) for which the statistic is kept, NULL for global statistics' + ,'h_payto' + ,'exchange_statistic_interval_amount' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'for which range is this the counter; note that the cumulative_number excludes the values already stored in smaller ranges' + ,'range' + ,'exchange_statistic_interval_amount' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'amount affected by the event' + ,'cumulative_value' + ,'exchange_statistic_interval_amount' + ,partition_suffix + ); +END $$; + + +CREATE FUNCTION foreign_table_exchange_statistic_interval_amount() +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'exchange_statistic_interval_amount'; +BEGIN + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_event_delimiter_foreign_key' + ' FOREIGN KEY (event_delimiter) ' + ' REFERENCES exchange_statistic_amount_event (aevent_serial_id) ON DELETE RESTRICT' + ); +END $$; + + +CREATE TYPE exchange_statistic_interval_number_get_return_value + AS + (range INT8 + ,rvalue INT8 + ); +COMMENT ON TYPE exchange_statistic_interval_number_get_return_value + IS 'Return type for exchange_statistic_interval_number_get stored procedure'; + +CREATE TYPE exchange_statistic_interval_amount_get_return_value + AS + (range INT8 + ,rvalue taler_amount + ); +COMMENT ON TYPE exchange_statistic_interval_amount_get_return_value + IS 'Return type for exchange_statistic_interval_amount_get stored procedure'; + + +INSERT INTO exchange_tables + (name + ,version + ,action + ,partitioned + ,by_range) + VALUES + ('exchange_statistic_bucket_counter' + ,'exchange-0009' + ,'create' + ,TRUE + ,FALSE), + ('exchange_statistic_bucket_amount' + ,'exchange-0009' + ,'create' + ,TRUE + ,FALSE), + ('exchange_statistic_counter_event' + ,'exchange-0009' + ,'create' + ,TRUE + ,FALSE), + ('exchange_statistic_counter_event' + ,'exchange-0009' + ,'constrain' + ,TRUE + ,FALSE), + ('exchange_statistic_interval_counter' + ,'exchange-0009' + ,'create' + ,TRUE + ,FALSE), + ('exchange_statistic_interval_counter' + ,'exchange-0009' + ,'foreign' + ,TRUE + ,FALSE), + ('exchange_statistic_amount_event' + ,'exchange-0009' + ,'create' + ,TRUE + ,FALSE), + ('exchange_statistic_amount_event' + ,'exchange-0009' + ,'constrain' + ,TRUE + ,FALSE), + ('exchange_statistic_interval_amount' + ,'exchange-0009' + ,'create' + ,TRUE + ,FALSE), + ('exchange_statistic_interval_amount' + ,'exchange-0009' + ,'foreign' + ,TRUE + ,FALSE); diff --git a/src/exchangedb/0009-withdraw.sql b/src/exchangedb/0009-withdraw.sql @@ -0,0 +1,207 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- +-- @author Özgür Kesim + +CREATE FUNCTION create_table_withdraw( + IN partition_suffix TEXT DEFAULT NULL +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'withdraw'; +BEGIN + PERFORM create_partitioned_table( + 'CREATE TABLE %I' + '(withdraw_id BIGINT GENERATED BY DEFAULT AS IDENTITY' + ',h_commitment BYTEA NOT NULL CONSTRAINT h_commitment_length CHECK(LENGTH(h_commitment)=64)' + ',execution_date INT8 NOT NULL' + ',amount_with_fee taler_amount NOT NULL' + ',reserve_pub BYTEA NOT NULL CONSTRAINT reserve_pub_length CHECK(LENGTH(reserve_pub)=32)' + ',reserve_sig BYTEA NOT NULL CONSTRAINT reserve_sig_length CHECK(LENGTH(reserve_sig)=64)' + ',max_age SMALLINT CONSTRAINT max_age_positive CHECK(max_age>=0)' + ',noreveal_index SMALLINT CONSTRAINT noreveal_index_positive CHECK(noreveal_index>=0)' + ',h_planchets BYTEA CONSTRAINT h_planchets_length CHECK(LENGTH(h_planchets)=64)' + ',h_blind_evs BYTEA[] NOT NULL CONSTRAINT h_blind_evs_length CHECK(cardinality(h_blind_evs)=cardinality(denom_serials))' + ',denom_serials INT8[] NOT NULL CONSTRAINT denom_serials_array_length CHECK(cardinality(denom_serials)=cardinality(denom_sigs))' + ',denom_sigs BYTEA[] NOT NULL CONSTRAINT denom_sigs_array_length CHECK(cardinality(denom_sigs)=cardinality(denom_serials))' + ') %s ;' + ,table_name + ,'PARTITION BY HASH (reserve_pub)' + ,partition_suffix + ); + PERFORM comment_partitioned_table( + 'Commitments made when withdrawing coins with age restriction and the gamma value chosen by the exchange. ' + 'It also contains the blindly signed coins, their signatures and denominations.' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'If the client explicitly commits to age-restricted coins, the gamma value chosen by the exchange in the cut-and-choose protocol; might be NULL.' + ,'noreveal_index' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'If the client explicitly commits to age-restricted coins, the running hash over all committed blinded planchets; might be NULL.' + ,'h_planchets' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The date of execution of this withdrawal, according to the exchange' + ,'execution_date' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'If the clients commits to age-restricted coins, the maximum age (in years) that the client explicitly commits to with this request; might be NULL.' + ,'max_age' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Commitment made by the client, hash over the various client inputs. Needed in the cut-and-choose protocol when aproof of age-restriction is required, and recoup.' + ,'h_commitment' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Reference to the public key of the reserve from which the coins are going to be withdrawn' + ,'reserve_pub' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Signature of the reserve''s private key over the age-withdraw request' + ,'reserve_sig' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Array of references to the denominations' + ,'denom_serials' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Array of the blinded envelopes of the chosen fresh coins, with value as given by the denomination in the corresponding slot in denom_serials' + ,'h_blind_evs' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Array of signatures over each blinded envelope' + ,'denom_sigs' + ,table_name + ,partition_suffix + ); +END +$$; + + +CREATE FUNCTION constrain_table_withdraw( + IN partition_suffix TEXT +) +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'withdraw'; +BEGIN + table_name = concat_ws('_', table_name, partition_suffix); + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD PRIMARY KEY (h_commitment);' + ); + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_h_commitment_reserve_pub_key' + ' UNIQUE (h_commitment, reserve_pub);' + ); + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_withdraw_id_key' + ' UNIQUE (withdraw_id);' + ); +END +$$; + + +CREATE FUNCTION foreign_table_withdraw() +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'withdraw'; +BEGIN + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_foreign_reserve_pub' + ' FOREIGN KEY (reserve_pub)' + ' REFERENCES reserves(reserve_pub) ON DELETE CASCADE;' + ); +END +$$; + + +-- Trigger to update the reserve_history table +CREATE FUNCTION withdraw_insert_trigger() + RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + INSERT INTO reserve_history + (reserve_pub + ,table_name + ,serial_id) + VALUES + (NEW.reserve_pub + ,'withdraw' + ,NEW.withdraw_id); + RETURN NEW; +END $$; +COMMENT ON FUNCTION withdraw_insert_trigger() + IS 'Keep track of a particular withdraw in the reserve_history table.'; + + +-- Put the trigger into the master table +CREATE FUNCTION master_table_withdraw() + RETURNS void + LANGUAGE plpgsql + AS $$ +BEGIN + CREATE TRIGGER withdraw_on_insert + AFTER INSERT + ON withdraw + FOR EACH ROW EXECUTE FUNCTION withdraw_insert_trigger(); +END $$; +COMMENT ON FUNCTION master_table_withdraw() + IS 'Setup triggers to replicate withdraw into reserve_history'; + + +INSERT INTO exchange_tables + (name + ,version + ,action + ,partitioned + ,by_range) +VALUES + ('withdraw', 'exchange-0009', 'create', TRUE ,FALSE), + ('withdraw', 'exchange-0009', 'constrain',TRUE ,FALSE), + ('withdraw', 'exchange-0009', 'foreign', TRUE ,FALSE), + ('withdraw', 'exchange-0009', 'master', TRUE ,FALSE); + diff --git a/src/exchangedb/Makefile.am b/src/exchangedb/Makefile.am @@ -23,6 +23,7 @@ sqldir = $(prefix)/share/taler-exchange/sql/ sqlinputs = \ exchange_do_*.sql \ + exchange_statistics_*.sql \ procedures.sql.in \ 0002-*.sql \ 0003-*.sql \ @@ -30,6 +31,8 @@ sqlinputs = \ 0005-*.sql \ 0006-*.sql \ 0007-*.sql \ + 0008-*.sql \ + 0009-*.sql \ exchange-0002.sql.in \ exchange-0003.sql.in \ exchange-0004.sql.in \ @@ -37,7 +40,7 @@ sqlinputs = \ exchange-0006.sql.in \ exchange-0007.sql.in \ exchange-0008.sql.in \ - exchange_statistics_*.sql + exchange-0009.sql.in sql_DATA = \ benchmark-0001.sql \ @@ -70,6 +73,7 @@ CLEANFILES = \ exchange-0006.sql \ exchange-0007.sql \ exchange-0008.sql \ + exchange-0009.sql \ procedures.sql procedures.sql: procedures.sql.in exchange_do_*.sql exchange_statistics_helpers.sql @@ -112,6 +116,11 @@ exchange-0008.sql: exchange-0008.sql.in 0008-*.sql gcc -E -P -undef - < exchange-0008.sql.in 2>/dev/null | sed -e "s/--.*//" | awk 'NF' - >$@ chmod ugo-w $@ +exchange-0009.sql: exchange-0009.sql.in 0009-*.sql + chmod +w $@ 2> /dev/null || true + gcc -E -P -undef - < exchange-0009.sql.in 2>/dev/null | sed -e "s/--.*//" | awk 'NF' - >$@ + chmod ugo-w $@ + check_SCRIPTS = \ test_idempotency.sh @@ -151,8 +160,12 @@ libtaler_plugin_exchangedb_postgres_la_SOURCES = \ pg_create_tables.h pg_create_tables.c \ pg_delete_aggregation_transient.h pg_delete_aggregation_transient.c \ pg_delete_shard_locks.h pg_delete_shard_locks.c \ +<<<<<<< HEAD pg_disable_rules.h pg_disable_rules.c \ pg_do_age_withdraw.h pg_do_age_withdraw.c \ +======= + pg_do_withdraw.h pg_do_withdraw.c \ +>>>>>>> 2e17389c8 (implementation for /withdraw endpoint ready) pg_do_batch_withdraw.h pg_do_batch_withdraw.c \ pg_do_batch_withdraw_insert.h pg_do_batch_withdraw_insert.c \ pg_do_check_deposit_idempotent.h pg_do_check_deposit_idempotent.c \ @@ -176,7 +189,7 @@ libtaler_plugin_exchangedb_postgres_la_SOURCES = \ pg_expire_purse.h pg_expire_purse.c \ pg_find_aggregation_transient.h pg_find_aggregation_transient.c \ pg_gc.h pg_gc.c \ - pg_get_age_withdraw.h pg_get_age_withdraw.c \ + pg_get_batch_withdraw_info.h pg_get_batch_withdraw_info.c \ pg_get_coin_denomination.h pg_get_coin_denomination.c \ pg_get_coin_transactions.c pg_get_coin_transactions.h \ pg_get_denomination_info.h pg_get_denomination_info.c \ @@ -198,7 +211,7 @@ libtaler_plugin_exchangedb_postgres_la_SOURCES = \ pg_get_ready_deposit.h pg_get_ready_deposit.c \ pg_get_refresh_reveal.h pg_get_refresh_reveal.c \ pg_get_reserve_balance.h pg_get_reserve_balance.c \ - pg_get_reserve_by_h_blind.h pg_get_reserve_by_h_blind.c \ + pg_get_reserve_by_h_commitment.h pg_get_reserve_by_h_commitment.c \ pg_get_reserve_history.c pg_get_reserve_history.h \ pg_get_signature_for_known_coin.h pg_get_signature_for_known_coin.c \ pg_get_unfinished_close_requests.c pg_get_unfinished_close_requests.h \ @@ -206,7 +219,7 @@ libtaler_plugin_exchangedb_postgres_la_SOURCES = \ pg_get_wire_fee.h pg_get_wire_fee.c \ pg_get_wire_fees.h pg_get_wire_fees.c \ pg_get_wire_hash_for_contract.h pg_get_wire_hash_for_contract.c \ - pg_get_withdraw_info.h pg_get_withdraw_info.c \ + pg_get_withdraw.h pg_get_withdraw.c \ pg_have_deposit2.h pg_have_deposit2.c \ pg_helper.h \ pg_inject_auditor_triggers.h pg_inject_auditor_triggers.c \ diff --git a/src/exchangedb/exchange-0009.sql b/src/exchangedb/exchange-0009.sql @@ -1,653 +0,0 @@ --- --- This file is part of TALER --- Copyright (C) 2025 Taler Systems SA --- --- TALER is free software; you can redistribute it and/or modify it under the --- terms of the GNU General Public License as published by the Free Software --- Foundation; either version 3, or (at your option) any later version. --- --- TALER is distributed in the hope that it will be useful, but WITHOUT ANY --- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR --- A PARTICULAR PURPOSE. See the GNU General Public License for more details. --- --- You should have received a copy of the GNU General Public License along with --- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> --- - -BEGIN; - -SELECT _v.register_patch('exchange-0009', NULL, NULL); - -SET search_path TO exchange; - -CREATE INDEX exchange_tables_by_pending - ON exchange_tables (table_serial_id) - WHERE NOT finished; -COMMENT ON INDEX exchange_tables_by_pending - IS 'Used by exchange_do_create_tables'; - -CREATE FUNCTION foreign_table_aggregation_transient9() -RETURNS void -LANGUAGE plpgsql -AS $$ -DECLARE - table_name TEXT DEFAULT 'aggregation_transient'; -BEGIN - EXECUTE FORMAT ( - 'ALTER TABLE ' || table_name || - ' ADD CONSTRAINT ' || table_name || '_foreign_wire_target_h_payto' - ' FOREIGN KEY (wire_target_h_payto) ' - ' REFERENCES wire_targets (wire_target_h_payto) ON DELETE RESTRICT' - ); -END -$$; - -CREATE FUNCTION constrain_table_aggregation_transient9( - IN partition_suffix TEXT -) -RETURNS VOID -LANGUAGE plpgsql -AS $$ -DECLARE - table_name TEXT DEFAULT 'aggregation_transient'; -BEGIN - table_name = concat_ws('_', table_name, partition_suffix); - EXECUTE FORMAT ( - 'ALTER TABLE ' || table_name || - ' ADD CONSTRAINT ' || table_name || '_wire_target_h_payto_and_wtid_unique' - ' UNIQUE (wire_target_h_payto,wtid_raw)' - ); -END $$; - - -INSERT INTO exchange_tables - (name - ,version - ,action - ,partitioned - ,by_range) - VALUES - ('aggregation_transient9' - ,'exchange-0009' - ,'foreign' - ,TRUE - ,FALSE), - ('aggregation_transient9' - ,'exchange-0009' - ,'constrain' - ,TRUE - ,FALSE); - --- Ranges given here must be supported by the date_trunc function of Postgresql! -CREATE TYPE statistic_range AS - ENUM('century', 'decade', 'year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second'); - -CREATE TYPE statistic_type AS - ENUM('amount', 'number'); - --- -------------- Bucket statistics --------------------- - -CREATE TABLE exchange_statistic_bucket_meta - (bmeta_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY - ,origin TEXT NOT NULL - ,slug TEXT NOT NULL - ,description TEXT NOT NULL - ,stype statistic_type NOT NULL - ,ranges statistic_range[] NOT NULL - ,ages INT4[] NOT NULL - ,UNIQUE(slug,stype) - ,CONSTRAINT equal_array_length - CHECK (array_length(ranges,1) = - array_length(ages,1)) - ); -COMMENT ON TABLE exchange_statistic_bucket_meta - IS 'meta data about a statistic with events falling into buckets we are tracking'; -COMMENT ON COLUMN exchange_statistic_bucket_meta.bmeta_serial_id - IS 'unique identifier for this type of bucket statistic we are tracking'; -COMMENT ON COLUMN exchange_statistic_bucket_meta.origin - IS 'which customization schema does this statistic originate from (used for easy deletion)'; -COMMENT ON COLUMN exchange_statistic_bucket_meta.slug - IS 'keyword (or name) of the statistic; identifies what the statistic is about; should be a slug suitable for a URI path'; -COMMENT ON COLUMN exchange_statistic_bucket_meta.description - IS 'description of the statistic being tracked'; -COMMENT ON COLUMN exchange_statistic_bucket_meta.stype - IS 'statistic type, what kind of data is being tracked, amount or number'; -COMMENT ON COLUMN exchange_statistic_bucket_meta.ranges - IS 'size of the buckets that are being kept for this statistic'; -COMMENT ON COLUMN exchange_statistic_bucket_meta.ages - IS 'determines how long into the past we keep buckets for the range at the given index around (in generations)'; -CREATE INDEX exchange_statistic_bucket_meta_by_origin - ON exchange_statistic_bucket_meta - (origin); - - -CREATE FUNCTION create_table_exchange_statistic_bucket_counter ( - IN partition_suffix TEXT DEFAULT NULL -) -RETURNS VOID -LANGUAGE plpgsql -AS $$ -BEGIN - PERFORM create_partitioned_table ( - 'CREATE TABLE %I' - '(bmeta_serial_id INT8 NOT NULL' - ' REFERENCES exchange_statistic_bucket_meta (bmeta_serial_id) ON DELETE CASCADE' - ',h_payto BYTEA CHECK (LENGTH(h_payto)=32)' - ',bucket_start INT8 NOT NULL' - ',bucket_range statistic_range NOT NULL' - ',cumulative_number INT8 NOT NULL' - ',UNIQUE (h_payto,bmeta_serial_id,bucket_start,bucket_range)' - ') %s;' - ,'exchange_statistic_bucket_counter' - ,'PARTITION BY HASH (h_payto)' - ,partition_suffix - ); - PERFORM comment_partitioned_table( - 'various numeric statistics (cumulative counters) being tracked by bucket into which they fall' - ,'exchange_statistic_bucket_counter' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'identifies what the statistic is about' - ,'bmeta_serial_id' - ,'exchange_statistic_bucket_counter' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'identifies an account (hash of normalized payto) for which the statistic is kept, NULL for global statistics' - ,'h_payto' - ,'exchange_statistic_bucket_counter' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'start date for the bucket in seconds since the epoch' - ,'bucket_start' - ,'exchange_statistic_bucket_counter' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'range of the bucket' - ,'bucket_range' - ,'exchange_statistic_bucket_counter' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'aggregate (sum) of tracked by the statistic; what exactly is tracked is determined by the keyword' - ,'cumulative_number' - ,'exchange_statistic_bucket_counter' - ,partition_suffix - ); -END $$; - - -CREATE FUNCTION create_table_exchange_statistic_bucket_amount ( - IN partition_suffix TEXT DEFAULT NULL -) -RETURNS VOID -LANGUAGE plpgsql -AS $$ -BEGIN - PERFORM create_partitioned_table ( - 'CREATE TABLE %I' - '(bmeta_serial_id INT8 NOT NULL' - ' REFERENCES exchange_statistic_bucket_meta (bmeta_serial_id) ON DELETE CASCADE' - ',h_payto BYTEA CHECK (LENGTH(h_payto)=32)' - ',bucket_start INT8 NOT NULL' - ',bucket_range statistic_range NOT NULL' - ',cumulative_value taler_amount NOT NULL' - ',UNIQUE (h_payto,bmeta_serial_id,bucket_start,bucket_range)' - ') %s;' - ,'exchange_statistic_bucket_amount' - ,'PARTITION BY HASH(h_payto)' - ,partition_suffix - ); - PERFORM comment_partitioned_table ( - 'various amount statistics being tracked' - ,'exchange_statistic_bucket_amount' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'identifies what the statistic is about' - ,'bmeta_serial_id' - ,'exchange_statistic_bucket_amount' - ,partition_suffix - ); - PERFORM comment_partitioned_column ( - 'identifies an account (hash of normalized payto) for which the statistic is kept, NULL for global statistics' - ,'h_payto' - ,'exchange_statistic_bucket_amount' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'start date for the bucket in seconds since the epoch' - ,'bucket_start' - ,'exchange_statistic_bucket_amount' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'range of the bucket' - ,'bucket_range' - ,'exchange_statistic_bucket_amount' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'amount being tracked' - ,'cumulative_value' - ,'exchange_statistic_bucket_amount' - ,partition_suffix - ); -END $$; - - --- -------------- Interval statistics --------------------- - - -CREATE TABLE exchange_statistic_interval_meta - (imeta_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY - ,origin TEXT NOT NULL - ,slug TEXT NOT NULL - ,description TEXT NOT NULL - ,stype statistic_type NOT NULL - ,ranges INT8[] NOT NULL CHECK (array_length(ranges,1) > 0) - ,precisions INT8[] NOT NULL CHECK (array_length(precisions,1) > 0) - ,UNIQUE(slug,stype) - ,CONSTRAINT equal_array_length - CHECK (array_length(ranges,1) = - array_length(precisions,1)) - ); -COMMENT ON TABLE exchange_statistic_interval_meta - IS 'meta data about an interval statistic we are tracking'; -COMMENT ON COLUMN exchange_statistic_interval_meta.imeta_serial_id - IS 'unique identifier for this type of interval statistic we are tracking'; -COMMENT ON COLUMN exchange_statistic_interval_meta.origin - IS 'which customization schema does this statistic originate from (used for easy deletion)'; -COMMENT ON COLUMN exchange_statistic_interval_meta.slug - IS 'keyword (or name) of the statistic; identifies what the statistic is about; should be a slug suitable for a URI path'; -COMMENT ON COLUMN exchange_statistic_interval_meta.description - IS 'description of the statistic being tracked'; -COMMENT ON COLUMN exchange_statistic_interval_meta.stype - IS 'statistic type, what kind of data is being tracked, amount or number'; -COMMENT ON COLUMN exchange_statistic_interval_meta.ranges - IS 'range of values that is being kept for this statistic, in seconds, must be monotonically increasing'; -COMMENT ON COLUMN exchange_statistic_interval_meta.precisions - IS 'determines how precisely we track which events fall into the range at the same index (allowing us to coalesce events with timestamps in proximity close to the given precision), in seconds, 0 is not allowed'; -CREATE INDEX exchange_statistic_interval_meta_by_origin - ON exchange_statistic_interval_meta - (origin); - - -CREATE FUNCTION create_table_exchange_statistic_counter_event ( - IN partition_suffix TEXT DEFAULT NULL -) -RETURNS VOID -LANGUAGE plpgsql -AS $$ -BEGIN - PERFORM create_partitioned_table( - 'CREATE TABLE %I' - '(nevent_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY' - ',imeta_serial_id INT8' - ' REFERENCES exchange_statistic_interval_meta (imeta_serial_id) ON DELETE CASCADE' - ',h_payto BYTEA CHECK (LENGTH(h_payto)=32)' - ',slot INT8 NOT NULL' - ',delta INT8 NOT NULL' - ',UNIQUE (h_payto,imeta_serial_id,slot)' - ') %s ;' - ,'exchange_statistic_counter_event' - ,'PARTITION BY HASH(h_payto)' - ,partition_suffix - ); - PERFORM comment_partitioned_table( - 'number to decrement an interval statistic by when a certain time value is reached' - ,'exchange_statistic_counter_event' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'unique identifier for this number event' - ,'nevent_serial_id' - ,'exchange_statistic_counter_event' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'identifies what the statistic is about; must be of stype number' - ,'imeta_serial_id' - ,'exchange_statistic_counter_event' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'identifies an account (hash of normalized payto) for which the statistic is kept, NULL for global statistics' - ,'h_payto' - ,'exchange_statistic_counter_event' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'identifies the time slot at which the given event(s) happened, rounded down by the respective precisions value' - ,'slot' - ,'exchange_statistic_counter_event' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'total cumulative number that was added at the time identified by slot' - ,'delta' - ,'exchange_statistic_counter_event' - ,partition_suffix - ); -END $$; - - -CREATE FUNCTION constrain_table_exchange_statistic_counter_event( - IN partition_suffix TEXT -) -RETURNS void -LANGUAGE plpgsql -AS $$ -DECLARE - table_name TEXT default 'exchange_statistic_counter_event'; -BEGIN - table_name = concat_ws('_', table_name, partition_suffix); - EXECUTE FORMAT ( - 'ALTER TABLE ' || table_name || - ' ADD CONSTRAINT ' || table_name || '_nevent_serial_id_key' - ' UNIQUE (nevent_serial_id)' - ); -END $$; - - -CREATE FUNCTION create_table_exchange_statistic_interval_counter ( - IN partition_suffix TEXT DEFAULT NULL -) -RETURNS VOID -LANGUAGE plpgsql -AS $$ -BEGIN - PERFORM create_partitioned_table( - 'CREATE TABLE %I' - '(imeta_serial_id INT8 NOT NULL' - ' REFERENCES exchange_statistic_interval_meta (imeta_serial_id) ON DELETE CASCADE' - ',h_payto BYTEA CHECK (LENGTH(h_payto)=32)' - ',range INT8 NOT NULL' - ',event_delimiter INT8 NOT NULL' - ',cumulative_number INT8 NOT NULL' - ',UNIQUE (h_payto,imeta_serial_id,range)' - ') %s ;' - ,'exchange_statistic_interval_counter' - ,'PARTITION BY HASH(h_payto)' - ,partition_suffix - ); - PERFORM comment_partitioned_table( - 'various numeric statistics (cumulative counters) being tracked' - ,'exchange_statistic_interval_counter' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'identifies what the statistic is about' - ,'imeta_serial_id' - ,'exchange_statistic_interval_counter' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'identifies an account (hash of normalized payto) for which the statistic is kept, NULL for global statistics' - ,'h_payto' - ,'exchange_statistic_interval_counter' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'for which range is this the counter; note that the cumulative_number excludes the values already stored in smaller ranges' - ,'range' - ,'exchange_statistic_interval_counter' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'determines the last event currently included in the interval' - ,'event_delimiter' - ,'exchange_statistic_interval_counter' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'aggregate (sum) of tracked by the statistic; what exactly is tracked is determined by the keyword' - ,'cumulative_number' - ,'exchange_statistic_interval_counter' - ,partition_suffix - ); -END $$; - - -CREATE FUNCTION foreign_table_exchange_statistic_interval_counter() -RETURNS VOID -LANGUAGE plpgsql -AS $$ -DECLARE - table_name TEXT DEFAULT 'exchange_statistic_interval_counter'; -BEGIN - EXECUTE FORMAT ( - 'ALTER TABLE ' || table_name || - ' ADD CONSTRAINT ' || table_name || '_event_delimiter_foreign_key' - ' FOREIGN KEY (event_delimiter) ' - ' REFERENCES exchange_statistic_counter_event (nevent_serial_id) ON DELETE RESTRICT' - ); -END $$; - - -CREATE FUNCTION create_table_exchange_statistic_amount_event ( - IN partition_suffix TEXT DEFAULT NULL -) -RETURNS VOID -LANGUAGE plpgsql -AS $$ -BEGIN - PERFORM create_partitioned_table( - 'CREATE TABLE %I' - '(aevent_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY' - ',imeta_serial_id INT8' - ' REFERENCES exchange_statistic_interval_meta (imeta_serial_id) ON DELETE CASCADE' - ',h_payto BYTEA CHECK (LENGTH(h_payto)=32)' - ',slot INT8 NOT NULL' - ',delta taler_amount NOT NULL' - ',CONSTRAINT event_key UNIQUE (h_payto,imeta_serial_id,slot)' - ') %s ;' - ,'exchange_statistic_amount_event' - ,'PARTITION BY HASH(h_payto)' - ,partition_suffix - ); - PERFORM comment_partitioned_table( - 'amount to decrement an interval statistic by when a certain time value is reached' - ,'exchange_statistic_amount_event' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'unique identifier for this amount event' - ,'aevent_serial_id' - ,'exchange_statistic_amount_event' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'identifies what the statistic is about; must be of clazz interval and of stype amount' - ,'imeta_serial_id' - ,'exchange_statistic_amount_event' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'identifies an account (hash of normalized payto) for which the statistic is kept, NULL for global statistics' - ,'h_payto' - ,'exchange_statistic_amount_event' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'identifies the time slot at which the given event(s) happened' - ,'slot' - ,'exchange_statistic_amount_event' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'total cumulative amount that was added at the time identified by slot' - ,'delta' - ,'exchange_statistic_amount_event' - ,partition_suffix - ); -END $$; - - -CREATE FUNCTION constrain_table_exchange_statistic_amount_event( - IN partition_suffix TEXT -) -RETURNS void -LANGUAGE plpgsql -AS $$ -DECLARE - table_name TEXT default 'exchange_statistic_amount_event'; -BEGIN - table_name = concat_ws('_', table_name, partition_suffix); - EXECUTE FORMAT ( - 'ALTER TABLE ' || table_name || - ' ADD CONSTRAINT ' || table_name || '_aevent_serial_id_key' - ' UNIQUE (aevent_serial_id)' - ); -END $$; - - - -CREATE FUNCTION create_table_exchange_statistic_interval_amount ( - IN partition_suffix TEXT DEFAULT NULL -) -RETURNS VOID -LANGUAGE plpgsql -AS $$ -BEGIN - PERFORM create_partitioned_table( - 'CREATE TABLE %I' - '(imeta_serial_id INT8 NOT NULL' - ' REFERENCES exchange_statistic_interval_meta (imeta_serial_id) ON DELETE CASCADE' - ',h_payto BYTEA CHECK (LENGTH(h_payto)=32)' - ',event_delimiter INT8 NOT NULL' - ',range INT8 NOT NULL' - ',cumulative_value taler_amount NOT NULL' - ',UNIQUE (h_payto,imeta_serial_id,range)' - ') %s ;' - ,'exchange_statistic_interval_amount' - ,'PARTITION BY HASH(h_payto)' - ,partition_suffix - ); - PERFORM comment_partitioned_table( - 'various amount statistics being tracked' - ,'exchange_statistic_interval_amount' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'identifies what the statistic is about' - ,'imeta_serial_id' - ,'exchange_statistic_interval_amount' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'identifies an account (hash of normalized payto) for which the statistic is kept, NULL for global statistics' - ,'h_payto' - ,'exchange_statistic_interval_amount' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'for which range is this the counter; note that the cumulative_number excludes the values already stored in smaller ranges' - ,'range' - ,'exchange_statistic_interval_amount' - ,partition_suffix - ); - PERFORM comment_partitioned_column( - 'amount affected by the event' - ,'cumulative_value' - ,'exchange_statistic_interval_amount' - ,partition_suffix - ); -END $$; - - -CREATE FUNCTION foreign_table_exchange_statistic_interval_amount() -RETURNS VOID -LANGUAGE plpgsql -AS $$ -DECLARE - table_name TEXT DEFAULT 'exchange_statistic_interval_amount'; -BEGIN - EXECUTE FORMAT ( - 'ALTER TABLE ' || table_name || - ' ADD CONSTRAINT ' || table_name || '_event_delimiter_foreign_key' - ' FOREIGN KEY (event_delimiter) ' - ' REFERENCES exchange_statistic_amount_event (aevent_serial_id) ON DELETE RESTRICT' - ); -END $$; - - -CREATE TYPE exchange_statistic_interval_number_get_return_value - AS - (range INT8 - ,rvalue INT8 - ); -COMMENT ON TYPE exchange_statistic_interval_number_get_return_value - IS 'Return type for exchange_statistic_interval_number_get stored procedure'; - -CREATE TYPE exchange_statistic_interval_amount_get_return_value - AS - (range INT8 - ,rvalue taler_amount - ); -COMMENT ON TYPE exchange_statistic_interval_amount_get_return_value - IS 'Return type for exchange_statistic_interval_amount_get stored procedure'; - - -INSERT INTO exchange_tables - (name - ,version - ,action - ,partitioned - ,by_range) - VALUES - ('exchange_statistic_bucket_counter' - ,'exchange-0009' - ,'create' - ,TRUE - ,FALSE), - ('exchange_statistic_bucket_amount' - ,'exchange-0009' - ,'create' - ,TRUE - ,FALSE), - ('exchange_statistic_counter_event' - ,'exchange-0009' - ,'create' - ,TRUE - ,FALSE), - ('exchange_statistic_counter_event' - ,'exchange-0009' - ,'constrain' - ,TRUE - ,FALSE), - ('exchange_statistic_interval_counter' - ,'exchange-0009' - ,'create' - ,TRUE - ,FALSE), - ('exchange_statistic_interval_counter' - ,'exchange-0009' - ,'foreign' - ,TRUE - ,FALSE), - ('exchange_statistic_amount_event' - ,'exchange-0009' - ,'create' - ,TRUE - ,FALSE), - ('exchange_statistic_amount_event' - ,'exchange-0009' - ,'constrain' - ,TRUE - ,FALSE), - ('exchange_statistic_interval_amount' - ,'exchange-0009' - ,'create' - ,TRUE - ,FALSE), - ('exchange_statistic_interval_amount' - ,'exchange-0009' - ,'foreign' - ,TRUE - ,FALSE); - -COMMIT; diff --git a/src/exchangedb/exchange-0009.sql.in b/src/exchangedb/exchange-0009.sql.in @@ -0,0 +1,27 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- +-- @author Özgür Kesim + +BEGIN; + +SELECT _v.register_patch('exchange-0009', NULL, NULL); +SET search_path TO exchange; + +#include "0009-age_withdraw.sql" +#include "0009-withdraw.sql" +#include "0009-recoup.sql" + +COMMIT; diff --git a/src/exchangedb/exchange_do_age_withdraw.sql b/src/exchangedb/exchange_do_age_withdraw.sql @@ -1,165 +0,0 @@ --- --- This file is part of TALER --- Copyright (C) 2023 Taler Systems SA --- --- TALER is free software; you can redistribute it and/or modify it under the --- terms of the GNU General Public License as published by the Free Software --- Foundation; either version 3, or (at your option) any later version. --- --- TALER is distributed in the hope that it will be useful, but WITHOUT ANY --- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR --- A PARTICULAR PURPOSE. See the GNU General Public License for more details. --- --- You should have received a copy of the GNU General Public License along with --- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> --- --- @author Özgür Kesim - -CREATE OR REPLACE FUNCTION exchange_do_age_withdraw( - IN amount_with_fee taler_amount, - IN rpub BYTEA, - IN rsig BYTEA, - IN now INT8, - IN min_reserve_gc INT8, - IN h_commitment BYTEA, - IN maximum_age_committed INT2, -- in years ϵ [0,1..) - IN noreveal_index INT2, - IN blinded_evs BYTEA[], - IN denom_serials INT8[], - IN denom_sigs BYTEA[], - OUT reserve_found BOOLEAN, - OUT balance_ok BOOLEAN, - OUT reserve_balance taler_amount, - OUT age_ok BOOLEAN, - OUT required_age INT2, -- in years ϵ [0,1..) - OUT reserve_birthday INT4, - OUT conflict BOOLEAN) -LANGUAGE plpgsql -AS $$ -DECLARE - reserve RECORD; - difference RECORD; - balance taler_amount; - not_before date; - earliest_date date; -BEGIN --- Shards: reserves by reserve_pub (SELECT) --- reserves_out (INSERT, with CONFLICT detection) by wih --- reserves by reserve_pub (UPDATE) --- reserves_in by reserve_pub (SELECT) --- wire_targets by wire_target_h_payto - -SELECT current_balance - ,birthday - ,gc_date - INTO reserve - FROM reserves - WHERE reserve_pub=rpub; - -IF NOT FOUND -THEN - reserve_found=FALSE; - age_ok = FALSE; - required_age=-1; - conflict=FALSE; - reserve_balance.val = 0; - reserve_balance.frac = 0; - balance_ok=FALSE; - RETURN; -END IF; - -reserve_found = TRUE; -conflict=FALSE; -- not really yet determined - -reserve_balance = reserve.current_balance; -reserve_birthday = reserve.birthday; - --- Check age requirements -IF (reserve.birthday <> 0) -THEN - not_before=date '1970-01-01' + reserve.birthday; - earliest_date = current_date - make_interval(maximum_age_committed); - -- - -- 1970-01-01 + birthday == not_before now - -- | | | - -- <.......not allowed......>[<.....allowed range......>] - -- | | | - -- ____*_____________________*_________*________________* timeline - -- | - -- earliest_date == - -- now - maximum_age_committed*year - -- - IF (earliest_date < not_before) - THEN - required_age = extract(year from age(current_date, not_before)); - age_ok = FALSE; - balance_ok=TRUE; -- NOT REALLY - RETURN; - END IF; -END IF; - -age_ok = TRUE; -required_age=0; - --- Check reserve balance is sufficient. -SELECT * -INTO difference -FROM amount_left_minus_right(reserve_balance - ,amount_with_fee); - -balance_ok = difference.ok; - -IF NOT balance_ok -THEN - RETURN; -END IF; - -balance = difference.diff; - --- Calculate new expiration dates. -min_reserve_gc=GREATEST(min_reserve_gc,reserve.gc_date); - --- Update reserve balance. -UPDATE reserves SET - gc_date=min_reserve_gc - ,current_balance=balance -WHERE - reserve_pub=rpub; - --- Write the commitment into the age-withdraw table -INSERT INTO age_withdraw - (h_commitment - ,max_age - ,amount_with_fee - ,reserve_pub - ,reserve_sig - ,noreveal_index - ,denom_serials - ,h_blind_evs - ,denom_sigs) -VALUES - (h_commitment - ,maximum_age_committed - ,amount_with_fee - ,rpub - ,rsig - ,noreveal_index - ,denom_serials - ,blinded_evs - ,denom_sigs) -ON CONFLICT DO NOTHING; - -IF NOT FOUND -THEN - -- Signal a conflict so that the caller - -- can fetch the actual data from the DB. - conflict=TRUE; - RETURN; -ELSE - conflict=FALSE; -END IF; - -END $$; - -COMMENT ON FUNCTION exchange_do_age_withdraw(taler_amount, BYTEA, BYTEA, INT8, INT8, BYTEA, INT2, INT2, BYTEA[], INT8[], BYTEA[]) - IS 'Checks whether the reserve has sufficient balance for an age-withdraw operation (or the request is repeated and was previously approved) and that age requirements are met. If so updates the database with the result. Includes storing the blinded planchets and denomination signatures, or signaling conflict'; diff --git a/src/exchangedb/exchange_do_main_gc.sql b/src/exchangedb/exchange_do_main_gc.sql @@ -24,7 +24,7 @@ DECLARE melt_min INT8; -- minimum melt still alive coin_min INT8; -- minimum known_coin still alive batch_deposit_min INT8; -- minimum deposit still alive - reserve_out_min INT8; -- minimum reserve_out still alive + withdraw_min INT8; -- minimum withdraw still alive denom_min INT8; -- minimum denomination still alive BEGIN @@ -39,14 +39,14 @@ DELETE FROM reserves WHERE gc_date < in_now AND current_balance = (0, 0); -SELECT reserve_out_serial_id - INTO reserve_out_min - FROM reserves_out - ORDER BY reserve_out_serial_id ASC +SELECT withdraw_id + INTO withdraw_min + FROM withdraw + ORDER BY withdraw_id ASC LIMIT 1; DELETE FROM recoup - WHERE reserve_out_serial_id < reserve_out_min; + WHERE withdraw_id < withdraw_min; SELECT reserve_uuid INTO reserve_uuid_min diff --git a/src/exchangedb/exchange_do_recoup_by_reserve.sql b/src/exchangedb/exchange_do_recoup_by_reserve.sql @@ -1,6 +1,6 @@ -- -- This file is part of TALER --- Copyright (C) 2014--2022 Taler Systems SA +-- Copyright (C) 2014--2025 Taler Systems SA -- -- TALER is free software; you can redistribute it and/or modify it under the -- terms of the GNU General Public License as published by the Free Software @@ -41,20 +41,20 @@ BEGIN WHERE reserve_pub = res_pub; FOR blind_ev IN - SELECT h_blind_ev - FROM reserves_out ro + SELECT unnest(h_blind_evs) + FROM withdraw wd JOIN reserve_history rh - ON (rh.serial_id = ro.reserve_out_serial_id) + ON (rh.serial_id = wd.withdraw_id) WHERE rh.reserve_pub = res_pub - AND rh.table_name='reserves_out' + AND rh.table_name='withdraw' LOOP SELECT robr.coin_pub INTO c_pub FROM exchange.recoup_by_reserve robr - WHERE robr.reserve_out_serial_id = ( - SELECT reserve_out_serial_id - FROM reserves_out - WHERE h_blind_ev = blind_ev + WHERE robr.withdraw_id = ( + SELECT withdraw_id + FROM withdraw + WHERE h_blind_ev = ANY(h_blind_evs) ); RETURN QUERY SELECT kc.denom_sig, diff --git a/src/exchangedb/exchange_do_recoup_to_reserve.sql b/src/exchangedb/exchange_do_recoup_to_reserve.sql @@ -17,7 +17,7 @@ CREATE OR REPLACE FUNCTION exchange_do_recoup_to_reserve( IN in_reserve_pub BYTEA, - IN in_reserve_out_serial_id INT8, + IN in_withdraw_id INT8, IN in_coin_blind BYTEA, IN in_coin_pub BYTEA, IN in_known_coin_id INT8, @@ -130,7 +130,7 @@ INSERT INTO exchange.recoup ,coin_blind ,amount ,recoup_timestamp - ,reserve_out_serial_id + ,withdraw_id ) VALUES (in_coin_pub @@ -138,7 +138,7 @@ VALUES ,in_coin_blind ,tmp ,in_recoup_timestamp - ,in_reserve_out_serial_id); + ,in_withdraw_id); -- Normal end, everything is fine. out_recoup_ok=TRUE; diff --git a/src/exchangedb/exchange_do_withdraw.sql b/src/exchangedb/exchange_do_withdraw.sql @@ -0,0 +1,171 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2023 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- +-- @author Özgür Kesim + +CREATE OR REPLACE FUNCTION exchange_do_withdraw( + IN amount_with_fee taler_amount, + IN rpub BYTEA, + IN rsig BYTEA, + IN now INT8, + IN min_reserve_gc INT8, + IN h_commitment BYTEA, + IN h_planchets BYTEA, + IN maximum_age_committed INT2, -- in years ϵ [0,1..) + IN noreveal_index INT2, + IN blinded_evs BYTEA[], + IN denom_serials INT8[], + IN denom_sigs BYTEA[], + OUT reserve_found BOOLEAN, + OUT balance_ok BOOLEAN, + OUT reserve_balance taler_amount, + OUT age_ok BOOLEAN, + OUT required_age INT2, -- in years ϵ [0,1..) + OUT reserve_birthday INT4, + OUT conflict BOOLEAN) +LANGUAGE plpgsql +AS $$ +DECLARE + reserve RECORD; + difference RECORD; + balance taler_amount; + not_before date; + earliest_date date; +BEGIN +-- Shards: reserves by reserve_pub (SELECT) +-- reserves_out (INSERT, with CONFLICT detection) by wih +-- reserves by reserve_pub (UPDATE) +-- reserves_in by reserve_pub (SELECT) +-- wire_targets by wire_target_h_payto + +SELECT current_balance + ,birthday + ,gc_date + INTO reserve + FROM reserves + WHERE reserve_pub=rpub; + +IF NOT FOUND +THEN + reserve_found=FALSE; + age_ok = FALSE; + required_age=-1; + conflict=FALSE; + reserve_balance.val = 0; + reserve_balance.frac = 0; + balance_ok=FALSE; + RETURN; +END IF; + +reserve_found = TRUE; +conflict=FALSE; -- not really yet determined + +reserve_balance = reserve.current_balance; +reserve_birthday = reserve.birthday; + +-- Check age requirements +IF (reserve.birthday <> 0) +THEN + not_before=date '1970-01-01' + reserve.birthday; + earliest_date = current_date - make_interval(maximum_age_committed); + -- + -- 1970-01-01 + birthday == not_before now + -- | | | + -- <.......not allowed......>[<.....allowed range......>] + -- | | | + -- ____*_____________________*_________*________________* timeline + -- | + -- earliest_date == + -- now - maximum_age_committed*year + -- + IF ((maximum_age_committed IS NULL) OR + (earliest_date < not_before)) + THEN + required_age = extract(year from age(current_date, not_before)); + age_ok = FALSE; + balance_ok=TRUE; -- NOT REALLY + RETURN; + END IF; +END IF; + +age_ok = TRUE; +required_age=0; + +-- Check reserve balance is sufficient. +SELECT * +INTO difference +FROM amount_left_minus_right(reserve_balance + ,amount_with_fee); + +balance_ok = difference.ok; + +IF NOT balance_ok +THEN + RETURN; +END IF; + +balance = difference.diff; + +-- Calculate new expiration dates. +min_reserve_gc=GREATEST(min_reserve_gc,reserve.gc_date); + +-- Update reserve balance. +UPDATE reserves SET + gc_date=min_reserve_gc + ,current_balance=balance +WHERE + reserve_pub=rpub; + +-- Write the commitment into the withdraw table +INSERT INTO withdraw + (h_commitment + ,h_planchets + ,execution_date + ,max_age + ,amount_with_fee + ,reserve_pub + ,reserve_sig + ,noreveal_index + ,denom_serials + ,h_blind_evs + ,denom_sigs) +VALUES + (h_commitment + ,h_planchets + ,now + ,maximum_age_committed + ,amount_with_fee + ,rpub + ,rsig + ,noreveal_index + ,denom_serials + ,blinded_evs + ,denom_sigs) +ON CONFLICT DO NOTHING; + +IF NOT FOUND +THEN + -- Signal a conflict so that the caller + -- can fetch the actual data from the DB. + conflict=TRUE; + RETURN; +ELSE + conflict=FALSE; +END IF; + +END $$; + +COMMENT ON FUNCTION exchange_do_withdraw(taler_amount, BYTEA, BYTEA, INT8, INT8, BYTEA, BYTEA, INT2, INT2, BYTEA[], INT8[], BYTEA[]) + IS 'Checks whether the reserve has sufficient balance for an withdraw operation (or the request is repeated and was previously approved) and that age requirements are met. If so updates the database with the result. Includes storing the blinded planchets and denomination signatures, or signaling conflict'; diff --git a/src/exchangedb/pg_do_age_withdraw.c b/src/exchangedb/pg_do_age_withdraw.c @@ -1,108 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2023 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file exchangedb/pg_do_batch_withdraw.c - * @brief Implementation of the do_batch_withdraw function for Postgres - * @author Özgür Kesim - */ -#include "platform.h" -#include "taler_error_codes.h" -#include "taler_dbevents.h" -#include "taler_exchangedb_plugin.h" -#include "taler_pq_lib.h" -#include "taler_pq_lib.h" -#include "pg_do_age_withdraw.h" -#include "pg_helper.h" -#include <gnunet/gnunet_time_lib.h> - - -enum GNUNET_DB_QueryStatus -TEH_PG_do_age_withdraw ( - void *cls, - const struct TALER_EXCHANGEDB_AgeWithdraw *commitment, - struct GNUNET_TIME_Timestamp now, - bool *found, - bool *balance_ok, - struct TALER_Amount *reserve_balance, - bool *age_ok, - uint16_t *required_age, - uint32_t *reserve_birthday, - bool *conflict) -{ - struct PostgresClosure *pg = cls; - struct GNUNET_TIME_Timestamp gc; - struct GNUNET_PQ_QueryParam params[] = { - TALER_PQ_query_param_amount (pg->conn, - &commitment->amount_with_fee), - GNUNET_PQ_query_param_auto_from_type (&commitment->reserve_pub), - GNUNET_PQ_query_param_auto_from_type (&commitment->reserve_sig), - GNUNET_PQ_query_param_timestamp (&now), - GNUNET_PQ_query_param_timestamp (&gc), - GNUNET_PQ_query_param_auto_from_type (&commitment->h_commitment), - GNUNET_PQ_query_param_uint16 (&commitment->max_age), - GNUNET_PQ_query_param_uint16 (&commitment->noreveal_index), - TALER_PQ_query_param_array_blinded_coin_hash (commitment->num_coins, - commitment->h_coin_evs, - pg->conn), - GNUNET_PQ_query_param_array_uint64 (commitment->num_coins, - commitment->denom_serials, - pg->conn), - TALER_PQ_query_param_array_blinded_denom_sig (commitment->num_coins, - commitment->denom_sigs, - pg->conn), - GNUNET_PQ_query_param_end - }; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_bool ("reserve_found", - found), - GNUNET_PQ_result_spec_bool ("balance_ok", - balance_ok), - TALER_PQ_RESULT_SPEC_AMOUNT ("reserve_balance", - reserve_balance), - GNUNET_PQ_result_spec_bool ("age_ok", - age_ok), - GNUNET_PQ_result_spec_uint16 ("required_age", - required_age), - GNUNET_PQ_result_spec_uint32 ("reserve_birthday", - reserve_birthday), - GNUNET_PQ_result_spec_bool ("conflict", - conflict), - GNUNET_PQ_result_spec_end - }; - enum GNUNET_DB_QueryStatus qs; - - gc = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_absolute_add (now.abs_time, - pg->legal_reserve_expiration_time)); - PREPARE (pg, - "call_age_withdraw", - "SELECT " - " reserve_found" - ",balance_ok" - ",reserve_balance" - ",age_ok" - ",required_age" - ",reserve_birthday" - ",conflict" - " FROM exchange_do_age_withdraw" - " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11);"); - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "call_age_withdraw", - params, - rs); - GNUNET_PQ_cleanup_query_params_closures (params); - return qs; -} diff --git a/src/exchangedb/pg_do_age_withdraw.h b/src/exchangedb/pg_do_age_withdraw.h @@ -1,57 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2023 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file exchangedb/pg_do_age_withdraw.h - * @brief implementation of the do_age_withdraw function for Postgres - * @author Özgür Kesim - */ -#ifndef PG_DO_AGE_WITHDRAW_H -#define PG_DO_AGE_WITHDRAW_H - -#include "taler_util.h" -#include "taler_json_lib.h" -#include "taler_exchangedb_plugin.h" -/** - * Perform reserve update as part of an age-withdraw operation, checking for - * sufficient balance and fulfillment of age requirements. Finally persisting - * the withdrawal details. - * - * @param cls the `struct PostgresClosure` with the plugin-specific state - * @param commitment the commitment with all parameters - * @param now current time (rounded) - * @param[out] found set to true if the reserve was found - * @param[out] balance_ok set to true if the balance was sufficient - * @param[out] reserve_balance set to the original reserve balance (at the start of this transaction) - * @param[out] age_ok set to true if no age requirements are present on the reserve - * @param[out] required_age if @e age_ok is false, set to the maximum allowed age when withdrawing from this reserve - * @param[out] reserve_birthday if @e age_ok is false, set to the birthday of the reserve - * @param[out] conflict set to true if there already is an entry in the database for the given pair (h_commitment, reserve_pub) - * @return query execution status - */ -enum GNUNET_DB_QueryStatus -TEH_PG_do_age_withdraw ( - void *cls, - const struct TALER_EXCHANGEDB_AgeWithdraw *commitment, - const struct GNUNET_TIME_Timestamp now, - bool *found, - bool *balance_ok, - struct TALER_Amount *reserve_balance, - bool *age_ok, - uint16_t *required_age, - uint32_t *reserve_birthday, - bool *conflict); - -#endif diff --git a/src/exchangedb/pg_do_recoup.c b/src/exchangedb/pg_do_recoup.c @@ -30,7 +30,7 @@ enum GNUNET_DB_QueryStatus TEH_PG_do_recoup ( void *cls, const struct TALER_ReservePublicKeyP *reserve_pub, - uint64_t reserve_out_serial_id, + uint64_t withdraw_id, const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, const struct TALER_CoinSpendPublicKeyP *coin_pub, uint64_t known_coin_id, @@ -46,7 +46,7 @@ TEH_PG_do_recoup ( = GNUNET_TIME_relative_to_timestamp (pg->idle_reserve_expiration_time); struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (reserve_pub), - GNUNET_PQ_query_param_uint64 (&reserve_out_serial_id), + GNUNET_PQ_query_param_uint64 (&withdraw_id), GNUNET_PQ_query_param_auto_from_type (coin_bks), GNUNET_PQ_query_param_auto_from_type (coin_pub), GNUNET_PQ_query_param_uint64 (&known_coin_id), diff --git a/src/exchangedb/pg_do_recoup.h b/src/exchangedb/pg_do_recoup.h @@ -30,7 +30,7 @@ * * @param cls the `struct PostgresClosure` with the plugin-specific state * @param reserve_pub public key of the reserve to credit - * @param reserve_out_serial_id row in the reserves_out table justifying the recoup + * @param withdraw_serial_id row in the withdraw table justifying the recoup * @param coin_bks coin blinding key secret to persist * @param coin_pub public key of the coin being recouped * @param known_coin_id row of the @a coin_pub in the known_coins table @@ -44,7 +44,7 @@ enum GNUNET_DB_QueryStatus TEH_PG_do_recoup ( void *cls, const struct TALER_ReservePublicKeyP *reserve_pub, - uint64_t reserve_out_serial_id, + uint64_t withdraw_serial_id, const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, const struct TALER_CoinSpendPublicKeyP *coin_pub, uint64_t known_coin_id, diff --git a/src/exchangedb/pg_do_withdraw.c b/src/exchangedb/pg_do_withdraw.c @@ -0,0 +1,115 @@ +/* + This file is part of TALER + Copyright (C) 2023-2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file exchangedb/pg_do_withdraw.c + * @brief Implementation of the do_withdraw function for Postgres + * @author Özgür Kesim + */ +#include "platform.h" +#include "taler_error_codes.h" +#include "taler_dbevents.h" +#include "taler_exchangedb_plugin.h" +#include "taler_pq_lib.h" +#include "taler_pq_lib.h" +#include "pg_do_withdraw.h" +#include "pg_helper.h" +#include <gnunet/gnunet_time_lib.h> + + +enum GNUNET_DB_QueryStatus +TEH_PG_do_withdraw ( + void *cls, + const struct TALER_EXCHANGEDB_Withdraw *withdraw, + struct GNUNET_TIME_Timestamp now, + bool *found, + bool *balance_ok, + struct TALER_Amount *reserve_balance, + bool *age_ok, + uint16_t *required_age, + uint32_t *reserve_birthday, + bool *conflict) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_TIME_Timestamp gc; + struct GNUNET_PQ_QueryParam params[] = { + TALER_PQ_query_param_amount (pg->conn, + &withdraw->amount_with_fee), + GNUNET_PQ_query_param_auto_from_type (&withdraw->reserve_pub), + GNUNET_PQ_query_param_auto_from_type (&withdraw->reserve_sig), + GNUNET_PQ_query_param_timestamp (&now), + GNUNET_PQ_query_param_timestamp (&gc), + GNUNET_PQ_query_param_auto_from_type (&withdraw->h_commitment), + withdraw->age_restricted ? + GNUNET_PQ_query_param_auto_from_type (&withdraw->h_planchets) : + GNUNET_PQ_query_param_null (), + withdraw->age_restricted ? + GNUNET_PQ_query_param_uint16 (&withdraw->max_age) : + GNUNET_PQ_query_param_null (), + withdraw->age_restricted ? + GNUNET_PQ_query_param_uint16 (&withdraw->noreveal_index) : + GNUNET_PQ_query_param_null (), + TALER_PQ_query_param_array_blinded_coin_hash (withdraw->num_coins, + withdraw->h_coin_evs, + pg->conn), + GNUNET_PQ_query_param_array_uint64 (withdraw->num_coins, + withdraw->denom_serials, + pg->conn), + TALER_PQ_query_param_array_blinded_denom_sig (withdraw->num_coins, + withdraw->denom_sigs, + pg->conn), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("reserve_found", + found), + GNUNET_PQ_result_spec_bool ("balance_ok", + balance_ok), + TALER_PQ_RESULT_SPEC_AMOUNT ("reserve_balance", + reserve_balance), + GNUNET_PQ_result_spec_bool ("age_ok", + age_ok), + GNUNET_PQ_result_spec_uint16 ("required_age", + required_age), + GNUNET_PQ_result_spec_uint32 ("reserve_birthday", + reserve_birthday), + GNUNET_PQ_result_spec_bool ("conflict", + conflict), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; + + gc = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_absolute_add (now.abs_time, + pg->legal_reserve_expiration_time)); + PREPARE (pg, + "call_withdraw", + "SELECT " + " reserve_found" + ",balance_ok" + ",reserve_balance" + ",age_ok" + ",required_age" + ",reserve_birthday" + ",conflict" + " FROM exchange_do_withdraw" + " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12);"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "call_withdraw", + params, + rs); + GNUNET_PQ_cleanup_query_params_closures (params); + return qs; +} diff --git a/src/exchangedb/pg_do_withdraw.h b/src/exchangedb/pg_do_withdraw.h @@ -0,0 +1,57 @@ +/* + This file is part of TALER + Copyright (C) 2023 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file exchangedb/pg_do_withdraw.h + * @brief implementation of the do_withdraw function for Postgres + * @author Özgür Kesim + */ +#ifndef PG_DO_WITHDRAW_H +#define PG_DO_WITHDRAW_H + +#include "taler_util.h" +#include "taler_json_lib.h" +#include "taler_exchangedb_plugin.h" +/** + * Perform reserve update as part of an age-withdraw operation, checking for + * sufficient balance and fulfillment of age requirements. Finally persisting + * the withdrawal details. + * + * @param cls the `struct PostgresClosure` with the plugin-specific state + * @param withdraw the request with all parameters + * @param now current time (rounded) + * @param[out] found set to true if the reserve was found + * @param[out] balance_ok set to true if the balance was sufficient + * @param[out] reserve_balance set to the original reserve balance (at the start of this transaction) + * @param[out] age_ok set to true if no age requirements are present on the reserve + * @param[out] required_age if @e age_ok is false, set to the maximum allowed age when withdrawing from this reserve + * @param[out] reserve_birthday if @e age_ok is false, set to the birthday of the reserve + * @param[out] conflict set to true if there already is an entry in the database for the given pair (h_commitment, reserve_pub) + * @return query execution status + */ +enum GNUNET_DB_QueryStatus +TEH_PG_do_withdraw ( + void *cls, + const struct TALER_EXCHANGEDB_Withdraw *withdraw, + const struct GNUNET_TIME_Timestamp now, + bool *found, + bool *balance_ok, + struct TALER_Amount *reserve_balance, + bool *age_ok, + uint16_t *required_age, + uint32_t *reserve_birthday, + bool *conflict); + +#endif diff --git a/src/exchangedb/pg_get_age_withdraw.c b/src/exchangedb/pg_get_age_withdraw.c @@ -1,119 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2023 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file exchangedb/pg_get_age_withdraw.c - * @brief Implementation of the get_age_withdraw function for Postgres - * @author Özgür Kesim - */ -#include "platform.h" -#include "taler_error_codes.h" -#include "taler_dbevents.h" -#include "taler_pq_lib.h" -#include "pg_get_age_withdraw.h" -#include "pg_helper.h" - - -enum GNUNET_DB_QueryStatus -TEH_PG_get_age_withdraw ( - void *cls, - const struct TALER_ReservePublicKeyP *reserve_pub, - const struct TALER_AgeWithdrawCommitmentHashP *ach, - struct TALER_EXCHANGEDB_AgeWithdraw *aw) -{ - enum GNUNET_DB_QueryStatus ret; - struct PostgresClosure *pg = cls; - size_t num_sigs; - size_t num_hashes; - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (reserve_pub), - GNUNET_PQ_query_param_auto_from_type (ach), - GNUNET_PQ_query_param_end - }; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("h_commitment", - &aw->h_commitment), - GNUNET_PQ_result_spec_auto_from_type ("reserve_sig", - &aw->reserve_sig), - GNUNET_PQ_result_spec_auto_from_type ("reserve_pub", - &aw->reserve_pub), - GNUNET_PQ_result_spec_uint16 ("max_age", - &aw->max_age), - TALER_PQ_result_spec_amount ("amount_with_fee", - pg->currency, - &aw->amount_with_fee), - GNUNET_PQ_result_spec_uint16 ("noreveal_index", - &aw->noreveal_index), - TALER_PQ_result_spec_array_blinded_coin_hash ( - pg->conn, - "h_blind_evs", - &aw->num_coins, - &aw->h_coin_evs), - TALER_PQ_result_spec_array_blinded_denom_sig ( - pg->conn, - "denom_sigs", - &num_sigs, - &aw->denom_sigs), - TALER_PQ_result_spec_array_denom_hash ( - pg->conn, - "denom_pub_hashes", - &num_hashes, - &aw->denom_pub_hashes), - GNUNET_PQ_result_spec_end - }; - - PREPARE (pg, - "get_age_withdraw", - "SELECT" - " h_commitment" - ",reserve_sig" - ",reserve_pub" - ",max_age" - ",amount_with_fee" - ",noreveal_index" - ",h_blind_evs" - ",denom_sigs" - ",ARRAY(" - " SELECT denominations.denom_pub_hash FROM (" - " SELECT UNNEST(denom_serials) AS id," - " generate_subscripts(denom_serials, 1) AS nr" /* for order */ - " ) AS denoms" - " LEFT JOIN denominations ON denominations.denominations_serial=denoms.id" - ") AS denom_pub_hashes" - " FROM age_withdraw" - " WHERE reserve_pub=$1 and h_commitment=$2;"); - - ret = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "get_age_withdraw", - params, - rs); - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != ret) - return ret; - - if ((aw->num_coins != num_sigs) || - (aw->num_coins != num_hashes)) - { - GNUNET_break (0); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "got inconsistent number of entries from DB: " - "num_coins=%ld, num_sigs=%ld, num_hashes=%ld\n", - aw->num_coins, - num_sigs, - num_hashes); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - return ret; -} diff --git a/src/exchangedb/pg_get_age_withdraw.h b/src/exchangedb/pg_get_age_withdraw.h @@ -1,45 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2023 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file exchangedb/pg_get_age_withdraw.h - * @brief implementation of the get_age_withdraw function for Postgres - * @author Özgür KESIM - */ -#ifndef PG_GET_AGE_WITHDRAW_H -#define PG_GET_AGE_WITHDRAW_H - -#include "taler_util.h" -#include "taler_json_lib.h" -#include "taler_exchangedb_plugin.h" - -/** - * Locate the response for a age-withdraw request under a hash that uniquely - * identifies the age-withdraw operation. Used to ensure idempotency of the - * request. - * - * @param cls the @e cls of this struct with the plugin-specific state - * @param reserve_pub public key of the reserve for which the age-withdraw request is made - * @param ach hash that uniquely identifies the age-withdraw operation - * @param[out] aw corresponding details of the previous age-withdraw request if an entry was found - * @return statement execution status - */ -enum GNUNET_DB_QueryStatus -TEH_PG_get_age_withdraw ( - void *cls, - const struct TALER_ReservePublicKeyP *reserve_pub, - const struct TALER_AgeWithdrawCommitmentHashP *ach, - struct TALER_EXCHANGEDB_AgeWithdraw *aw); -#endif diff --git a/src/exchangedb/pg_get_batch_withdraw_info.c b/src/exchangedb/pg_get_batch_withdraw_info.c @@ -0,0 +1,79 @@ +/* + This file is part of TALER + Copyright (C) 2022 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file exchangedb/pg_get_batch_withdraw_info.c + * @brief Implementation of the get_withdraw_info function for Postgres + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_error_codes.h" +#include "taler_dbevents.h" +#include "taler_pq_lib.h" +#include "pg_get_batch_withdraw_info.h" +#include "pg_helper.h" + + +enum GNUNET_DB_QueryStatus +TEH_PG_get_batch_withdraw_info ( + void *cls, + const struct TALER_BlindedCoinHashP *bch, + struct TALER_EXCHANGEDB_CollectableBlindcoin *collectable) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (bch), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_auto_from_type ("denom_pub_hash", + &collectable->denom_pub_hash), + TALER_PQ_result_spec_blinded_denom_sig ("denom_sig", + &collectable->sig), + GNUNET_PQ_result_spec_auto_from_type ("reserve_sig", + &collectable->reserve_sig), + GNUNET_PQ_result_spec_auto_from_type ("reserve_pub", + &collectable->reserve_pub), + GNUNET_PQ_result_spec_auto_from_type ("h_blind_ev", + &collectable->h_coin_envelope), + TALER_PQ_RESULT_SPEC_AMOUNT ("amount_with_fee", + &collectable->amount_with_fee), + TALER_PQ_RESULT_SPEC_AMOUNT ("fee_withdraw", + &collectable->withdraw_fee), + GNUNET_PQ_result_spec_end + }; + + PREPARE (pg, + "get_batch_withdraw_info", + "SELECT" + " denom.denom_pub_hash" + ",denom_sig" + ",reserve_sig" + ",reserves.reserve_pub" + ",execution_date" + ",h_blind_ev" + ",amount_with_fee" + ",denom.fee_withdraw" + " FROM reserves_out" + " JOIN reserves" + " USING (reserve_uuid)" + " JOIN denominations denom" + " USING (denominations_serial)" + " WHERE h_blind_ev=$1;"); + return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "get_batch_withdraw_info", + params, + rs); +} diff --git a/src/exchangedb/pg_get_batch_withdraw_info.h b/src/exchangedb/pg_get_batch_withdraw_info.h @@ -0,0 +1,43 @@ +/* + This file is part of TALER + Copyright (C) 2022 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file exchangedb/pg_get_batch_withdraw_info.h + * @brief implementation of the get_batch_withdraw_info function for Postgres + * @author Christian Grothoff + */ +#ifndef PG_GET_BATCH_WITHDRAW_INFO_H +#define PG_GET_BATCH_WITHDRAW_INFO_H + +#include "taler_util.h" +#include "taler_json_lib.h" +#include "taler_exchangedb_plugin.h" +/** + * Locate the response for a /batch-withdraw request under the + * key of the hash of the blinded message. + * + * @param cls the `struct PostgresClosure` with the plugin-specific state + * @param bch hash that uniquely identifies the withdraw operation + * @param collectable corresponding collectable coin (blind signature) + * if a coin is found + * @return statement execution status + */ +enum GNUNET_DB_QueryStatus +TEH_PG_get_batch_withdraw_info ( + void *cls, + const struct TALER_BlindedCoinHashP *bch, + struct TALER_EXCHANGEDB_CollectableBlindcoin *collectable); + +#endif diff --git a/src/exchangedb/pg_get_coin_transactions.c b/src/exchangedb/pg_get_coin_transactions.c @@ -1002,10 +1002,10 @@ TEH_PG_get_coin_transactions ( ",rcp.recoup_timestamp" ",rcp.recoup_uuid" " FROM recoup rcp" - " JOIN reserves_out ro" - " USING (reserve_out_serial_id)" + " JOIN withdraw ro" + " USING (withdraw_id)" " JOIN reserves res" - " USING (reserve_uuid)" + " USING (reserve_pub)" " JOIN known_coins coins" " USING (coin_pub)" " JOIN denominations denoms" diff --git a/src/exchangedb/pg_get_reserve_by_h_blind.c b/src/exchangedb/pg_get_reserve_by_h_blind.c @@ -1,63 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2022 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file exchangedb/pg_get_reserve_by_h_blind.c - * @brief Implementation of the get_reserve_by_h_blind function for Postgres - * @author Christian Grothoff - */ -#include "platform.h" -#include "taler_error_codes.h" -#include "taler_dbevents.h" -#include "taler_pq_lib.h" -#include "pg_get_reserve_by_h_blind.h" -#include "pg_helper.h" - - -enum GNUNET_DB_QueryStatus -TEH_PG_get_reserve_by_h_blind ( - void *cls, - const struct TALER_BlindedCoinHashP *bch, - struct TALER_ReservePublicKeyP *reserve_pub, - uint64_t *reserve_out_serial_id) -{ - struct PostgresClosure *pg = cls; - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (bch), - GNUNET_PQ_query_param_end - }; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("reserve_pub", - reserve_pub), - GNUNET_PQ_result_spec_uint64 ("reserve_out_serial_id", - reserve_out_serial_id), - GNUNET_PQ_result_spec_end - }; - /* Used in #postgres_get_reserve_by_h_blind() */ - PREPARE (pg, - "reserve_by_h_blind", - "SELECT" - " reserves.reserve_pub" - ",reserve_out_serial_id" - " FROM reserves_out" - " JOIN reserves" - " USING (reserve_uuid)" - " WHERE h_blind_ev=$1" - " LIMIT 1;"); - return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "reserve_by_h_blind", - params, - rs); -} diff --git a/src/exchangedb/pg_get_reserve_by_h_blind.h b/src/exchangedb/pg_get_reserve_by_h_blind.h @@ -1,44 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2022 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file exchangedb/pg_get_reserve_by_h_blind.h - * @brief implementation of the get_reserve_by_h_blind function for Postgres - * @author Christian Grothoff - */ -#ifndef PG_GET_RESERVE_BY_H_BLIND_H -#define PG_GET_RESERVE_BY_H_BLIND_H - -#include "taler_util.h" -#include "taler_json_lib.h" -#include "taler_exchangedb_plugin.h" -/** - * Obtain information about which reserve a coin was generated - * from given the hash of the blinded coin. - * - * @param cls closure - * @param bch hash that uniquely identifies the withdraw request - * @param[out] reserve_pub set to information about the reserve (on success only) - * @param[out] reserve_out_serial_id set to row of the @a h_blind_ev in reserves_out - * @return transaction status code - */ -enum GNUNET_DB_QueryStatus -TEH_PG_get_reserve_by_h_blind ( - void *cls, - const struct TALER_BlindedCoinHashP *bch, - struct TALER_ReservePublicKeyP *reserve_pub, - uint64_t *reserve_out_serial_id); - -#endif diff --git a/src/exchangedb/pg_get_reserve_by_h_commitment.c b/src/exchangedb/pg_get_reserve_by_h_commitment.c @@ -0,0 +1,62 @@ +/* + This file is part of TALER + Copyright (C) 2022,2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file exchangedb/pg_get_reserve_by_h_commitment.c + * @brief Implementation of the get_reserve_by_h_commitment function for Postgres + * @author Christian Grothoff + * @author Özgür Kesim + */ +#include "platform.h" +#include "taler_error_codes.h" +#include "taler_dbevents.h" +#include "taler_pq_lib.h" +#include "pg_get_reserve_by_h_commitment.h" +#include "pg_helper.h" + + +enum GNUNET_DB_QueryStatus +TEH_PG_get_reserve_by_h_commitment ( + void *cls, + const struct TALER_WithdrawCommitmentHashP *h_commitment, + struct TALER_ReservePublicKeyP *reserve_pub, + uint64_t *withdraw_serial_id) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (h_commitment), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_auto_from_type ("reserve_pub", + reserve_pub), + GNUNET_PQ_result_spec_uint64 ("withdraw_id", + withdraw_serial_id), + GNUNET_PQ_result_spec_end + }; + /* Used in #postgres_get_reserve_by_h_commitment() */ + PREPARE (pg, + "reserve_by_h_commitment", + "SELECT" + " reserve_pub" + ",withdraw_id" + " FROM withdraw" + " WHERE h_commitment=$1" + " LIMIT 1;"); + return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "reserve_by_h_commitment", + params, + rs); +} diff --git a/src/exchangedb/pg_get_reserve_by_h_commitment.h b/src/exchangedb/pg_get_reserve_by_h_commitment.h @@ -0,0 +1,45 @@ +/* + This file is part of TALER + Copyright (C) 2022 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file exchangedb/pg_get_reserve_by_h_commitment.h + * @brief implementation of the get_reserve_by_h_commitment function for Postgres + * @author Christian Grothoff + * @author Özgür Kesim + */ +#ifndef PG_GET_RESERVE_BY_H_COMMITMENT_H +#define PG_GET_RESERVE_BY_H_COMMITMENT_H + +#include "taler_util.h" +#include "taler_json_lib.h" +#include "taler_exchangedb_plugin.h" +/** + * Obtain information about which reserve a coin was generated + * from given the hash of the blinded coin. + * + * @param cls closure + * @param h_commitment hash that uniquely identifies the withdraw request + * @param[out] reserve_pub set to information about the reserve (on success only) + * @param[out] withdraw_serial_id set to row of the @a h_commitment in withdraw + * @return transaction status code + */ +enum GNUNET_DB_QueryStatus +TEH_PG_get_reserve_by_h_commitment ( + void *cls, + const struct TALER_WithdrawCommitmentHashP *h_commitment, + struct TALER_ReservePublicKeyP *reserve_pub, + uint64_t *withdraw_serial_id); + +#endif diff --git a/src/exchangedb/pg_get_reserve_history.c b/src/exchangedb/pg_get_reserve_history.c @@ -173,9 +173,111 @@ add_bank_to_exchange (void *cls, * @param num_results number of rows in @a result */ static void -add_withdraw_coin (void *cls, - PGresult *result, - unsigned int num_results) +add_withdraw (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct ReserveHistoryContext *rhc = cls; + struct PostgresClosure *pg = rhc->pg; + + while (0 < num_results) + { + struct TALER_EXCHANGEDB_Withdraw *wd; + struct TALER_EXCHANGEDB_ReserveHistory *tail; + + wd = GNUNET_new (struct TALER_EXCHANGEDB_Withdraw); + { + bool no_noreveal_index; + bool no_h_planchets; + bool no_max_age; + size_t num_h_coin_evs; + size_t num_denom_hs; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_auto_from_type ("h_commitment", + &wd->h_commitment), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_auto_from_type ("h_planchets", + &wd->h_planchets), + &no_h_planchets), + GNUNET_PQ_result_spec_auto_from_type ("reserve_sig", + &wd->reserve_sig), + TALER_PQ_RESULT_SPEC_AMOUNT ("amount_with_fee", + &wd->amount_with_fee), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_uint16 ("max_age", + &wd->max_age), + &no_max_age), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_uint16 ("noreveal_index", + &wd->noreveal_index), + &no_noreveal_index), + TALER_PQ_result_spec_array_blinded_coin_hash (pg->conn, + "h_blind_evs", + &num_h_coin_evs, + &wd->h_coin_evs), + TALER_PQ_result_spec_array_denom_hash (pg->conn, + "denom_pub_hashes", + &num_denom_hs, + &wd->denom_pub_hashes), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + --num_results)) + { + GNUNET_break (0); + GNUNET_free (wd); + rhc->failed = true; + return; + } + + if ((no_noreveal_index != no_max_age) || + (no_max_age != no_h_planchets)) + { + GNUNET_break (0); + GNUNET_free (wd); + rhc->failed = true; + return; + } + wd->age_restricted = ! no_max_age; + wd->num_coins = num_denom_hs; + wd->reserve_pub = *rhc->reserve_pub; + + if (no_h_planchets) + { + struct GNUNET_HashContext *ctx = GNUNET_CRYPTO_hash_context_start (); + GNUNET_assert (NULL != ctx); + for (size_t i = 0; i < num_h_coin_evs; i++) + { + GNUNET_CRYPTO_hash_context_read (ctx, + &wd->h_coin_evs[i], + sizeof(wd->h_coin_evs[0])); + } + GNUNET_CRYPTO_hash_context_finish (ctx, + &wd->h_planchets.hash); + } + } + + tail = append_rh (rhc); + tail->type = TALER_EXCHANGEDB_RO_WITHDRAW_COINS; + tail->details.withdraw = wd; + } +} + + +/** + * Add pre26 coin batch-withdrawals to result set for #TEH_PG_get_reserve_history. + * + * @param cls a `struct ReserveHistoryContext *` + * @param result SQL result + * @param num_results number of rows in @a result + */ +static void +add_batch_withdraw_coin (void *cls, + PGresult *result, + unsigned int num_results) { struct ReserveHistoryContext *rhc = cls; struct PostgresClosure *pg = rhc->pg; @@ -220,8 +322,8 @@ add_withdraw_coin (void *cls, &cbc->amount_with_fee)); cbc->reserve_pub = *rhc->reserve_pub; tail = append_rh (rhc); - tail->type = TALER_EXCHANGEDB_RO_WITHDRAW_COIN; - tail->details.withdraw = cbc; + tail->type = TALER_EXCHANGEDB_RO_BATCH_WITHDRAW_COIN; + tail->details.batch_withdraw = cbc; } } @@ -582,10 +684,10 @@ handle_history_entry (void *cls, { "reserves_in", "reserves_in_get_transactions", add_bank_to_exchange }, - /** #TALER_EXCHANGEDB_RO_WITHDRAW_COIN */ - { "reserves_out", - "get_reserves_out", - &add_withdraw_coin }, + /** #TALER_EXCHANGEDB_RO_WITHDRAW_COINS */ + { "withdraw", + "get_withdraw_details", + &add_withdraw }, /** #TALER_EXCHANGEDB_RO_RECOUP_COIN */ { "recoup", "recoup_by_reserve", @@ -606,6 +708,10 @@ handle_history_entry (void *cls, { "close_requests", "close_request_by_reserve", &add_close_requests }, + /** #TALER_EXCHANGEDB_RO_BATCH_WITHDRAW_COIN */ + { "reserves_out", + "get_reserves_out", + &add_batch_withdraw_coin }, /* List terminator */ { NULL, NULL, NULL } }; @@ -666,7 +772,7 @@ handle_history_entry (void *cls, if (! found) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Coin history includes unsupported table `%s`\n", + "Reserve history includes unsupported table `%s`\n", table_name); rhc->failed = true; } @@ -732,7 +838,6 @@ TEH_PG_get_reserve_history ( " WHERE reserve_pub=$1" " AND reserve_history_serial_id > $2" " ORDER BY reserve_history_serial_id DESC;"); - PREPARE (pg, "reserves_in_get_transactions", "SELECT" @@ -746,6 +851,27 @@ TEH_PG_get_reserve_history ( " WHERE ri.reserve_pub=$1" " AND ri.reserve_in_serial_id=$2;"); PREPARE (pg, + "get_withdraw_details", + "SELECT" + " h_commitment" + ",h_planchets" + ",amount_with_fee" + ",reserve_sig" + ",max_age" + ",noreveal_index" + ",h_blind_evs" + ",ARRAY(" + " SELECT denominations.denom_pub_hash FROM (" + " SELECT UNNEST(denom_serials) AS id," + " generate_subscripts(denom_serials, 1) AS nr" /* for order */ + " ) AS denoms" + " LEFT JOIN denominations ON denominations.denominations_serial=denoms.id" + ") AS denom_pub_hashes" + " FROM withdraw " + " WHERE withdraw_id=$2" + " AND reserve_pub=$1;"); +#pragma message "get_reserves_out must be removed, once reserves_out is removed" + PREPARE (pg, "get_reserves_out", "SELECT" " ro.h_blind_ev" @@ -773,10 +899,10 @@ TEH_PG_get_reserve_history ( ",denom.denom_pub_hash" ",kc.denom_sig" " FROM recoup rec" - " JOIN reserves_out ro" - " USING (reserve_out_serial_id)" + " JOIN withdraw ro" + " USING (withdraw_id)" " JOIN reserves res" - " USING (reserve_uuid)" + " USING (reserve_pub)" " JOIN known_coins kc" " USING (coin_pub)" " JOIN denominations denom" diff --git a/src/exchangedb/pg_get_withdraw.c b/src/exchangedb/pg_get_withdraw.c @@ -0,0 +1,161 @@ +/* + This file is part of TALER + Copyright (C) 2023 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file exchangedb/pg_get_withdraw.c + * @brief Implementation of the get_withdraw function for Postgres + * @author Özgür Kesim + */ +#include "platform.h" +#include "taler_error_codes.h" +#include "taler_dbevents.h" +#include "taler_pq_lib.h" +#include "pg_get_withdraw.h" +#include "pg_helper.h" + + +enum GNUNET_DB_QueryStatus +TEH_PG_get_withdraw ( + void *cls, + const struct TALER_WithdrawCommitmentHashP *wch, + struct TALER_EXCHANGEDB_Withdraw *wd) +{ + enum GNUNET_DB_QueryStatus ret; + struct PostgresClosure *pg = cls; + size_t num_sigs; + size_t num_hashes; + bool no_noreveal_index; + bool no_h_planchets; + bool no_max_age; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (wch), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_auto_from_type ("h_commitment", + &wd->h_commitment), + GNUNET_PQ_result_spec_auto_from_type ("reserve_sig", + &wd->reserve_sig), + GNUNET_PQ_result_spec_auto_from_type ("reserve_pub", + &wd->reserve_pub), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_uint16 ("max_age", + &wd->max_age), + &no_max_age), + TALER_PQ_result_spec_amount ("amount_with_fee", + pg->currency, + &wd->amount_with_fee), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_uint16 ("noreveal_index", + &wd->noreveal_index), + &no_noreveal_index), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_auto_from_type ("h_planchets", + &wd->h_planchets), + &no_h_planchets), + TALER_PQ_result_spec_array_blinded_coin_hash ( + pg->conn, + "h_blind_evs", + &wd->num_coins, + &wd->h_coin_evs), + TALER_PQ_result_spec_array_blinded_denom_sig ( + pg->conn, + "denom_sigs", + &num_sigs, + &wd->denom_sigs), + TALER_PQ_result_spec_array_denom_hash ( + pg->conn, + "denom_pub_hashes", + &num_hashes, + &wd->denom_pub_hashes), + GNUNET_PQ_result_spec_end + }; + + PREPARE (pg, + "get_withdraw", + "SELECT" + " h_commitment" + ",h_planchets" + ",reserve_sig" + ",reserve_pub" + ",max_age" + ",amount_with_fee" + ",noreveal_index" + ",h_blind_evs" + ",denom_sigs" + ",ARRAY(" + " SELECT denominations.denom_pub_hash FROM (" + " SELECT UNNEST(denom_serials) AS id," + " generate_subscripts(denom_serials, 1) AS nr" /* for order */ + " ) AS denoms" + " LEFT JOIN denominations ON denominations.denominations_serial=denoms.id" + ") AS denom_pub_hashes" + " FROM withdraw" + " WHERE h_commitment=$1;"); + + ret = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "get_withdraw", + params, + rs); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != ret) + return ret; + + if ((no_max_age != no_noreveal_index) || + (no_max_age != no_h_planchets)) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "got inconsistent state for max_age, noreveal_index and h_planchets in DB: " + "no_max_age=%s, no_noreveal_index=%s, no_h_planchets=%s\n", + no_max_age ? "true" : "false", + no_noreveal_index ? "true" : "false", + no_h_planchets ? "true" : "false"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + wd->age_restricted = ! no_max_age; + + if ((wd->num_coins != num_sigs) || + (wd->num_coins != num_hashes)) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "got inconsistent number of entries from DB: " + "num_coins=%ld, num_sigs=%ld, num_hashes=%ld\n", + wd->num_coins, + num_sigs, + num_hashes); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + /* In the non-age-restricted case, we calculate the h_planchets + * from the h_blind_evs themselves. + */ + if (no_h_planchets) + { + struct GNUNET_HashContext *ctx = GNUNET_CRYPTO_hash_context_start (); + GNUNET_assert (NULL != ctx); + for (size_t i = 0; i<wd->num_coins; i++) + { + GNUNET_CRYPTO_hash_context_read (ctx, + &wd->h_coin_evs[i], + sizeof(wd->h_coin_evs[0])); + } + GNUNET_CRYPTO_hash_context_finish (ctx, + &wd->h_planchets.hash); + } + + return ret; +} diff --git a/src/exchangedb/pg_get_withdraw.h b/src/exchangedb/pg_get_withdraw.h @@ -0,0 +1,44 @@ +/* + This file is part of TALER + Copyright (C) 2023 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file exchangedb/pg_get_withdraw.h + * @brief implementation of the get_withdraw function for Postgres + * @author Özgür KESIM + */ +#ifndef PG_GET_WITHDRAW_H +#define PG_GET_WITHDRAW_H + +#include "taler_util.h" +#include "taler_json_lib.h" +#include "taler_exchangedb_plugin.h" + +/** + * Locate the response for a withdraw request under a hash that uniquely + * identifies the withdraw operation. Used to ensure idempotency of the + * request. + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param wch hash that uniquely identifies the withdraw operation + * @param[out] wd corresponding details of the previous withdraw request if an entry was found + * @return statement execution status + */ +enum GNUNET_DB_QueryStatus +TEH_PG_get_withdraw ( + void *cls, + const struct TALER_WithdrawCommitmentHashP *wch, + struct TALER_EXCHANGEDB_Withdraw *wd); + +#endif diff --git a/src/exchangedb/pg_get_withdraw_info.c b/src/exchangedb/pg_get_withdraw_info.c @@ -1,79 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2022 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file exchangedb/pg_get_withdraw_info.c - * @brief Implementation of the get_withdraw_info function for Postgres - * @author Christian Grothoff - */ -#include "platform.h" -#include "taler_error_codes.h" -#include "taler_dbevents.h" -#include "taler_pq_lib.h" -#include "pg_get_withdraw_info.h" -#include "pg_helper.h" - - -enum GNUNET_DB_QueryStatus -TEH_PG_get_withdraw_info ( - void *cls, - const struct TALER_BlindedCoinHashP *bch, - struct TALER_EXCHANGEDB_CollectableBlindcoin *collectable) -{ - struct PostgresClosure *pg = cls; - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (bch), - GNUNET_PQ_query_param_end - }; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("denom_pub_hash", - &collectable->denom_pub_hash), - TALER_PQ_result_spec_blinded_denom_sig ("denom_sig", - &collectable->sig), - GNUNET_PQ_result_spec_auto_from_type ("reserve_sig", - &collectable->reserve_sig), - GNUNET_PQ_result_spec_auto_from_type ("reserve_pub", - &collectable->reserve_pub), - GNUNET_PQ_result_spec_auto_from_type ("h_blind_ev", - &collectable->h_coin_envelope), - TALER_PQ_RESULT_SPEC_AMOUNT ("amount_with_fee", - &collectable->amount_with_fee), - TALER_PQ_RESULT_SPEC_AMOUNT ("fee_withdraw", - &collectable->withdraw_fee), - GNUNET_PQ_result_spec_end - }; - - PREPARE (pg, - "get_withdraw_info", - "SELECT" - " denom.denom_pub_hash" - ",denom_sig" - ",reserve_sig" - ",reserves.reserve_pub" - ",execution_date" - ",h_blind_ev" - ",amount_with_fee" - ",denom.fee_withdraw" - " FROM reserves_out" - " JOIN reserves" - " USING (reserve_uuid)" - " JOIN denominations denom" - " USING (denominations_serial)" - " WHERE h_blind_ev=$1;"); - return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "get_withdraw_info", - params, - rs); -} diff --git a/src/exchangedb/pg_get_withdraw_info.h b/src/exchangedb/pg_get_withdraw_info.h @@ -1,43 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2022 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file exchangedb/pg_get_withdraw_info.h - * @brief implementation of the get_withdraw_info function for Postgres - * @author Christian Grothoff - */ -#ifndef PG_GET_WITHDRAW_INFO_H -#define PG_GET_WITHDRAW_INFO_H - -#include "taler_util.h" -#include "taler_json_lib.h" -#include "taler_exchangedb_plugin.h" -/** - * Locate the response for a /reserve/withdraw request under the - * key of the hash of the blinded message. - * - * @param cls the `struct PostgresClosure` with the plugin-specific state - * @param bch hash that uniquely identifies the withdraw operation - * @param collectable corresponding collectable coin (blind signature) - * if a coin is found - * @return statement execution status - */ -enum GNUNET_DB_QueryStatus -TEH_PG_get_withdraw_info ( - void *cls, - const struct TALER_BlindedCoinHashP *bch, - struct TALER_EXCHANGEDB_CollectableBlindcoin *collectable); - -#endif diff --git a/src/exchangedb/pg_insert_records_by_table.c b/src/exchangedb/pg_insert_records_by_table.c @@ -1311,7 +1311,7 @@ irbt_cb_table_recoup (struct PostgresClosure *pg, GNUNET_PQ_query_param_timestamp (&td->details.recoup.timestamp), GNUNET_PQ_query_param_auto_from_type ( &td->details.recoup.coin_pub), - GNUNET_PQ_query_param_uint64 (&td->details.recoup.reserve_out_serial_id), + GNUNET_PQ_query_param_uint64 (&td->details.recoup.withdraw_serial_id), GNUNET_PQ_query_param_end }; @@ -1324,7 +1324,7 @@ irbt_cb_table_recoup (struct PostgresClosure *pg, ",amount" ",recoup_timestamp" ",coin_pub" - ",reserve_out_serial_id" + ",withdraw_serial_id" ") VALUES " "($1, $2, $3, $4, $5, $6, $7);"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, @@ -2254,49 +2254,55 @@ irbt_cb_table_purse_deletion (struct PostgresClosure *pg, /** - * Function called with age_withdraw records to insert into table. + * Function called with withdraw records to insert into table. * * @param pg plugin context * @param td record to insert */ static enum GNUNET_DB_QueryStatus -irbt_cb_table_age_withdraw ( +irbt_cb_table_withdraw ( struct PostgresClosure *pg, const struct TALER_EXCHANGEDB_TableData *td) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint64 (&td->serial), GNUNET_PQ_query_param_auto_from_type ( - &td->details.age_withdraw.h_commitment), + &td->details.withdraw.h_commitment), TALER_PQ_query_param_amount ( pg->conn, - &td->details.age_withdraw.amount_with_fee), + &td->details.withdraw.amount_with_fee), + td->details.withdraw.age_restricted ? GNUNET_PQ_query_param_uint16 ( - &td->details.age_withdraw.max_age), + &td->details.withdraw.max_age) : + GNUNET_PQ_query_param_null (), GNUNET_PQ_query_param_auto_from_type ( - &td->details.age_withdraw.reserve_pub), + &td->details.withdraw.reserve_pub), GNUNET_PQ_query_param_auto_from_type ( - &td->details.age_withdraw.reserve_sig), + &td->details.withdraw.reserve_sig), + td->details.withdraw.age_restricted ? GNUNET_PQ_query_param_uint32 ( - &td->details.age_withdraw.noreveal_index), - /* FIXME[oec]: other fields, too! */ + &td->details.withdraw.noreveal_index) : + GNUNET_PQ_query_param_null (), + GNUNET_PQ_query_param_timestamp ( + &td->details.withdraw.execution_date), GNUNET_PQ_query_param_end }; PREPARE (pg, - "insert_into_table_age_withdraw", - "INSERT INTO age_withdraw" - "(age_withdraw_commitment_id" + "insert_into_table_withdraw", + "INSERT INTO withdraw" + "(withdraw_commitment_id" ",h_commitment" ",amount_with_fee" ",max_age" ",reserve_pub" ",reserve_sig" ",noreveal_index" + ",execution_date" ") VALUES " - "($1, $2, $3, $4, $5, $6, $7, $8);"); + "($1, $2, $3, $4, $5, $6, $7, $8,$9);"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_into_table_age_withdraw", + "insert_into_table_withdraw", params); } @@ -2442,8 +2448,8 @@ TEH_PG_insert_records_by_table (void *cls, case TALER_EXCHANGEDB_RT_PURSE_DELETION: rh = &irbt_cb_table_purse_deletion; break; - case TALER_EXCHANGEDB_RT_AGE_WITHDRAW: - rh = &irbt_cb_table_age_withdraw; + case TALER_EXCHANGEDB_RT_WITHDRAW: + rh = &irbt_cb_table_withdraw; break; case TALER_EXCHANGEDB_RT_LEGITIMIZATION_MEASURES: rh = &irbt_cb_table_legitimization_measures; diff --git a/src/exchangedb/pg_lookup_records_by_table.c b/src/exchangedb/pg_lookup_records_by_table.c @@ -1557,8 +1557,8 @@ lrbt_cb_table_recoup (void *cls, GNUNET_PQ_result_spec_auto_from_type ( "coin_pub", &td.details.recoup.coin_pub), - GNUNET_PQ_result_spec_uint64 ("reserve_out_serial_id", - &td.details.recoup.reserve_out_serial_id), + GNUNET_PQ_result_spec_uint64 ("withdraw_serial_id", + &td.details.recoup.withdraw_serial_id), GNUNET_PQ_result_spec_end }; @@ -2643,48 +2643,56 @@ lrbt_cb_table_purse_deletion (void *cls, /** - * Function called with age_withdraw table entries. + * Function called with withdraw table entries. * * @param cls closure * @param result the postgres result * @param num_results the number of results in @a result */ static void -lrbt_cb_table_age_withdraw (void *cls, - PGresult *result, - unsigned int num_results) +lrbt_cb_table_withdraw (void *cls, + PGresult *result, + unsigned int num_results) { struct LookupRecordsByTableContext *ctx = cls; struct PostgresClosure *pg = ctx->pg; struct TALER_EXCHANGEDB_TableData td = { - .table = TALER_EXCHANGEDB_RT_AGE_WITHDRAW + .table = TALER_EXCHANGEDB_RT_WITHDRAW }; for (unsigned int i = 0; i<num_results; i++) { + bool no_max_age = false; + bool no_noreveal_index = false; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_uint64 ( - "age_withdraw_id", + "withdraw_id", &td.serial), GNUNET_PQ_result_spec_auto_from_type ( "h_commitment", - &td.details.age_withdraw.h_commitment), - GNUNET_PQ_result_spec_uint16 ( - "max_age", - &td.details.age_withdraw.max_age), + &td.details.withdraw.h_commitment), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_uint16 ( + "max_age", + &td.details.withdraw.max_age), + &no_max_age), TALER_PQ_RESULT_SPEC_AMOUNT ( "amount_with_fee", - &td.details.age_withdraw.amount_with_fee), + &td.details.withdraw.amount_with_fee), GNUNET_PQ_result_spec_auto_from_type ( "reserve_pub", - &td.details.age_withdraw.reserve_pub), + &td.details.withdraw.reserve_pub), GNUNET_PQ_result_spec_auto_from_type ( "reserve_sig", - &td.details.age_withdraw.reserve_sig), - GNUNET_PQ_result_spec_uint32 ( - "noreveal_index", - &td.details.age_withdraw.noreveal_index), - /* FIXME[oec]: more fields! */ + &td.details.withdraw.reserve_sig), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_uint32 ( + "noreveal_index", + &td.details.withdraw.noreveal_index), + &no_noreveal_index), + GNUNET_PQ_result_spec_timestamp ( + "execution_date", + &td.details.withdraw.execution_date), GNUNET_PQ_result_spec_end }; @@ -2697,6 +2705,15 @@ lrbt_cb_table_age_withdraw (void *cls, ctx->error = true; return; } + + if (no_max_age != no_noreveal_index) + { + GNUNET_break (0); + ctx->error = true; + return; + } + td.details.withdraw.age_restricted = ! no_max_age; + ctx->cb (ctx->cb_cls, &td); GNUNET_PQ_cleanup_result (rs); @@ -3691,21 +3708,22 @@ TEH_PG_lookup_records_by_table (void *cls, " ORDER BY purse_deletion_serial_id ASC;"); rh = &lrbt_cb_table_purse_deletion; break; - case TALER_EXCHANGEDB_RT_AGE_WITHDRAW: - XPREPARE ("select_above_serial_by_table_age_withdraw", + case TALER_EXCHANGEDB_RT_WITHDRAW: + XPREPARE ("select_above_serial_by_table_withdraw", "SELECT" - " age_withdraw_id" + " withdraw_id" ",h_commitment" ",amount_with_fee" ",max_age" ",reserve_pub" ",reserve_sig" + ",execution_date" ",noreveal_index" - " FROM age_withdraw" - " WHERE age_withdraw_id > $1" - " ORDER BY age_withdraw_id ASC;"); + " FROM withdraw" + " WHERE withdraw_id > $1" + " ORDER BY withdraw_id ASC;"); /* FIXME[oec]: MORE FIELDS! */ - rh = &lrbt_cb_table_age_withdraw; + rh = &lrbt_cb_table_withdraw; break; case TALER_EXCHANGEDB_RT_LEGITIMIZATION_MEASURES: XPREPARE ("select_above_serial_by_table_legitimization_measures", diff --git a/src/exchangedb/pg_lookup_serial_by_table.c b/src/exchangedb/pg_lookup_serial_by_table.c @@ -408,14 +408,14 @@ TEH_PG_lookup_serial_by_table (void *cls, " LIMIT 1;"); statement = "select_serial_by_table_purse_deletion"; break; - case TALER_EXCHANGEDB_RT_AGE_WITHDRAW: - XPREPARE ("select_serial_by_table_age_withdraw", + case TALER_EXCHANGEDB_RT_WITHDRAW: + XPREPARE ("select_serial_by_table_withdraw", "SELECT" - " age_withdraw_id AS serial" - " FROM age_withdraw" - " ORDER BY age_withdraw_id DESC" + " withdraw_id AS serial" + " FROM withdraw" + " ORDER BY withdraw_id DESC" " LIMIT 1;"); - statement = "select_serial_by_table_age_withdraw"; + statement = "select_serial_by_table_withdraw"; break; case TALER_EXCHANGEDB_RT_LEGITIMIZATION_MEASURES: XPREPARE ("select_serial_by_table_legitimization_measures", diff --git a/src/exchangedb/pg_select_recoup_above_serial_id.c b/src/exchangedb/pg_select_recoup_above_serial_id.c @@ -79,7 +79,6 @@ recoup_serial_helper_cb (void *cls, union GNUNET_CRYPTO_BlindingSecretP coin_blind; struct TALER_Amount amount; struct TALER_DenominationPublicKey denom_pub; - struct TALER_BlindedCoinHashP h_blind_ev; struct GNUNET_TIME_Timestamp timestamp; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_uint64 ("recoup_uuid", @@ -96,8 +95,6 @@ recoup_serial_helper_cb (void *cls, &coin_sig), GNUNET_PQ_result_spec_auto_from_type ("coin_blind", &coin_blind), - GNUNET_PQ_result_spec_auto_from_type ("h_blind_ev", - &h_blind_ev), GNUNET_PQ_result_spec_auto_from_type ("denom_pub_hash", &coin.denom_pub_hash), GNUNET_PQ_result_spec_allow_null ( @@ -162,11 +159,10 @@ TEH_PG_select_recoup_above_serial_id ( "SELECT" " recoup_uuid" ",recoup_timestamp" - ",reserves.reserve_pub" + ",withdraw.reserve_pub" ",coins.coin_pub" ",coin_sig" ",coin_blind" - ",ro.h_blind_ev" ",denoms.denom_pub_hash" ",coins.denom_sig" ",coins.age_commitment_hash" @@ -175,10 +171,8 @@ TEH_PG_select_recoup_above_serial_id ( " FROM recoup" " JOIN known_coins coins" " USING (coin_pub)" - " JOIN reserves_out ro" - " USING (reserve_out_serial_id)" - " JOIN reserves" - " USING (reserve_uuid)" + " JOIN withdraw" + " USING (withdraw_id)" " JOIN denominations denoms" " ON (coins.denominations_serial = denoms.denominations_serial)" " WHERE recoup_uuid>=$1" diff --git a/src/exchangedb/pg_select_withdraw_amounts_for_kyc_check.c b/src/exchangedb/pg_select_withdraw_amounts_for_kyc_check.c @@ -135,20 +135,20 @@ TEH_PG_select_withdraw_amounts_for_kyc_check ( PREPARE (pg, "select_kyc_relevant_withdraw_events", "SELECT" - " ro.amount_with_fee AS amount" - ",ro.execution_date AS date" + " wd.amount_with_fee AS amount" + ",wd.execution_date AS date" " FROM reserves_in ri" " JOIN reserve_history rh" " ON (rh.reserve_pub = ri.reserve_pub)" - " JOIN reserves_out ro" - " ON (ro.reserve_out_serial_id = rh.serial_id)" + " JOIN withdraw wd" + " ON (wd.withdraw_id = rh.serial_id)" " WHERE ri.wire_source_h_payto IN (" " SELECT wire_target_h_payto" " FROM wire_targets" " WHERE h_normalized_payto=$1" " )" " AND rh.table_name='reserves_out'" - " AND ro.execution_date >= $2" + " AND wd.execution_date >= $2" " ORDER BY rh.reserve_history_serial_id DESC"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, diff --git a/src/exchangedb/pg_select_withdrawals_above_serial_id.c b/src/exchangedb/pg_select_withdrawals_above_serial_id.c @@ -17,6 +17,7 @@ * @file exchangedb/pg_select_withdrawals_above_serial_id.c * @brief Implementation of the select_withdrawals_above_serial_id function for Postgres * @author Christian Grothoff + * @author Özgür Kesim */ #include "platform.h" #include "taler_error_codes.h" @@ -26,9 +27,9 @@ #include "pg_helper.h" /** - * Closure for #reserves_out_serial_helper_cb(). + * Closure for #withdraw_serial_helper_cb(). */ -struct ReservesOutSerialContext +struct WithdrawSerialContext { /** @@ -57,21 +58,22 @@ struct ReservesOutSerialContext * Helper function to be called with the results of a SELECT statement * that has returned @a num_results results. * - * @param cls closure of type `struct ReservesOutSerialContext` + * @param cls closure of type `struct WithdrawSerialContext` * @param result the postgres result * @param num_results the number of results in @a result */ static void -reserves_out_serial_helper_cb (void *cls, - PGresult *result, - unsigned int num_results) +withdraw_serial_helper_cb (void *cls, + PGresult *result, + unsigned int num_results) { - struct ReservesOutSerialContext *rosc = cls; + struct WithdrawSerialContext *rosc = cls; struct PostgresClosure *pg = rosc->pg; for (unsigned int i = 0; i<num_results; i++) { - struct TALER_BlindedCoinHashP h_blind_ev; + size_t num_evs; + struct TALER_BlindedCoinHashP *h_blind_evs; struct TALER_DenominationPublicKey denom_pub; struct TALER_ReservePublicKeyP reserve_pub; struct TALER_ReserveSignatureP reserve_sig; @@ -79,8 +81,10 @@ reserves_out_serial_helper_cb (void *cls, struct TALER_Amount amount_with_fee; uint64_t rowid; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("h_blind_ev", - &h_blind_ev), + TALER_PQ_result_spec_array_blinded_coin_hash (pg->conn, + "h_blind_evs", + &num_evs, + &h_blind_evs), TALER_PQ_result_spec_denom_pub ("denom_pub", &denom_pub), GNUNET_PQ_result_spec_auto_from_type ("reserve_pub", @@ -91,7 +95,7 @@ reserves_out_serial_helper_cb (void *cls, &execution_date), TALER_PQ_RESULT_SPEC_AMOUNT ("amount_with_fee", &amount_with_fee), - GNUNET_PQ_result_spec_uint64 ("reserve_out_serial_id", + GNUNET_PQ_result_spec_uint64 ("withdraw_id", &rowid), GNUNET_PQ_result_spec_end }; @@ -108,7 +112,8 @@ reserves_out_serial_helper_cb (void *cls, } ret = rosc->cb (rosc->cb_cls, rowid, - &h_blind_ev, + num_evs, + h_blind_evs, &denom_pub, &reserve_pub, &reserve_sig, @@ -133,7 +138,7 @@ TEH_PG_select_withdrawals_above_serial_id ( GNUNET_PQ_query_param_uint64 (&serial_id), GNUNET_PQ_query_param_end }; - struct ReservesOutSerialContext rosc = { + struct WithdrawSerialContext rosc = { .cb = cb, .cb_cls = cb_cls, .pg = pg, @@ -143,26 +148,28 @@ TEH_PG_select_withdrawals_above_serial_id ( /* Fetch deposits with rowid '\geq' the given parameter */ PREPARE (pg, - "audit_get_reserves_out_incr", + "audit_get_withdraw_incr", "SELECT" - " h_blind_ev" + " h_blind_evs" ",denom.denom_pub" ",reserve_sig" - ",reserves.reserve_pub" + ",reserve_pub" ",execution_date" ",amount_with_fee" - ",reserve_out_serial_id" - " FROM reserves_out" - " JOIN reserves" - " USING (reserve_uuid)" + ",wd.withdraw_id" + " FROM withdraw AS wd" + " JOIN (" + " SELECT withdraw_id, unnest(denom_serials) AS denominations_serial" + " FROM withdraw" + " ) AS unnested ON wd.withdraw_id = unnested.withdraw_id" " JOIN denominations denom" " USING (denominations_serial)" - " WHERE reserve_out_serial_id>=$1" - " ORDER BY reserve_out_serial_id ASC;"); + " WHERE wd.withdraw_id>=$1" + " ORDER BY wd.withdraw_id ASC;"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "audit_get_reserves_out_incr", + "audit_get_withdraw_incr", params, - &reserves_out_serial_helper_cb, + &withdraw_serial_helper_cb, &rosc); if (GNUNET_OK != rosc.status) return GNUNET_DB_STATUS_HARD_ERROR; diff --git a/src/exchangedb/plugin_exchangedb_common.c b/src/exchangedb/plugin_exchangedb_common.c @@ -42,11 +42,20 @@ TEH_COMMON_free_reserve_history ( GNUNET_free (bt); break; } - case TALER_EXCHANGEDB_RO_WITHDRAW_COIN: + case TALER_EXCHANGEDB_RO_WITHDRAW_COINS: + { + struct TALER_EXCHANGEDB_Withdraw *wd; + wd = rh->details.withdraw; + GNUNET_free (wd->h_coin_evs); + GNUNET_free (wd->denom_pub_hashes); + GNUNET_free (wd); + break; + } + case TALER_EXCHANGEDB_RO_BATCH_WITHDRAW_COIN: { struct TALER_EXCHANGEDB_CollectableBlindcoin *cbc; - cbc = rh->details.withdraw; + cbc = rh->details.batch_withdraw; TALER_blinded_denom_sig_free (&cbc->sig); GNUNET_free (cbc); break; diff --git a/src/exchangedb/plugin_exchangedb_postgres.c b/src/exchangedb/plugin_exchangedb_postgres.c @@ -45,7 +45,7 @@ #include "pg_delete_aggregation_transient.h" #include "pg_delete_shard_locks.h" #include "pg_disable_rules.h" -#include "pg_do_age_withdraw.h" +#include "pg_do_withdraw.h" #include "pg_do_batch_withdraw.h" #include "pg_do_batch_withdraw_insert.h" #include "pg_do_check_deposit_idempotent.h" @@ -69,7 +69,7 @@ #include "pg_expire_purse.h" #include "pg_find_aggregation_transient.h" #include "pg_gc.h" -#include "pg_get_age_withdraw.h" +#include "pg_get_withdraw.h" #include "pg_get_coin_denomination.h" #include "pg_get_coin_transactions.h" #include "pg_get_denomination_info.h" @@ -91,7 +91,7 @@ #include "pg_get_ready_deposit.h" #include "pg_get_refresh_reveal.h" #include "pg_get_reserve_balance.h" -#include "pg_get_reserve_by_h_blind.h" +#include "pg_get_reserve_by_h_commitment.h" #include "pg_get_reserve_history.h" #include "pg_get_signature_for_known_coin.h" #include "pg_get_unfinished_close_requests.h" @@ -99,7 +99,7 @@ #include "pg_get_wire_fee.h" #include "pg_get_wire_fees.h" #include "pg_get_wire_hash_for_contract.h" -#include "pg_get_withdraw_info.h" +#include "pg_get_batch_withdraw_info.h" #include "pg_have_deposit2.h" #include "pg_helper.h" #include "pg_inject_auditor_triggers.h" @@ -258,14 +258,14 @@ * @param conn SQL connection that was used */ #define BREAK_DB_ERR(result,conn) do { \ - GNUNET_break (0); \ - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, \ - "Database failure: %s/%s/%s/%s/%s", \ - PQresultErrorField (result, PG_DIAG_MESSAGE_PRIMARY), \ - PQresultErrorField (result, PG_DIAG_MESSAGE_DETAIL), \ - PQresultErrorMessage (result), \ - PQresStatus (PQresultStatus (result)), \ - PQerrorMessage (conn)); \ + GNUNET_break (0); \ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, \ + "Database failure: %s/%s/%s/%s/%s", \ + PQresultErrorField (result, PG_DIAG_MESSAGE_PRIMARY), \ + PQresultErrorField (result, PG_DIAG_MESSAGE_DETAIL), \ + PQresultErrorMessage (result), \ + PQresStatus (PQresultStatus (result)), \ + PQerrorMessage (conn)); \ } while (0) @@ -540,14 +540,14 @@ libtaler_plugin_exchangedb_postgres_init (void *cls) = &TEH_PG_drain_kyc_alert; plugin->reserves_in_insert = &TEH_PG_reserves_in_insert; - plugin->get_withdraw_info - = &TEH_PG_get_withdraw_info; + plugin->get_batch_withdraw_info + = &TEH_PG_get_batch_withdraw_info; plugin->do_batch_withdraw = &TEH_PG_do_batch_withdraw; - plugin->do_age_withdraw - = &TEH_PG_do_age_withdraw; - plugin->get_age_withdraw - = &TEH_PG_get_age_withdraw; + plugin->do_withdraw + = &TEH_PG_do_withdraw; + plugin->get_withdraw + = &TEH_PG_get_withdraw; plugin->wad_in_insert = &TEH_PG_wad_in_insert; plugin->kycauth_in_insert @@ -666,8 +666,8 @@ libtaler_plugin_exchangedb_postgres_init (void *cls) = &TEH_PG_select_recoup_above_serial_id; plugin->select_recoup_refresh_above_serial_id = &TEH_PG_select_recoup_refresh_above_serial_id; - plugin->get_reserve_by_h_blind - = &TEH_PG_get_reserve_by_h_blind; + plugin->get_reserve_by_h_commitment + = &TEH_PG_get_reserve_by_h_commitment; plugin->get_old_coin_by_h_blind = &TEH_PG_get_old_coin_by_h_blind; plugin->insert_denomination_revocation diff --git a/src/exchangedb/procedures.sql.in b/src/exchangedb/procedures.sql.in @@ -25,7 +25,7 @@ SET search_path TO exchange; #include "exchange_do_amount_specific.sql" #include "exchange_do_batch_withdraw.sql" #include "exchange_do_batch_withdraw_insert.sql" -#include "exchange_do_age_withdraw.sql" +#include "exchange_do_withdraw.sql" #include "exchange_do_deposit.sql" #include "exchange_do_check_deposit_idempotent.sql" #include "exchange_do_melt.sql" diff --git a/src/exchangedb/test_exchangedb.c b/src/exchangedb/test_exchangedb.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2023 Taler Systems SA + Copyright (C) 2014-2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -19,6 +19,7 @@ * @author Sree Harsha Totakura * @author Christian Grothoff * @author Marcello Stanisci + * @author Özgür Kesim */ #include "platform.h" #include "taler_exchangedb_lib.h" @@ -339,6 +340,7 @@ create_denom_key_pair (unsigned int size, } +static struct TALER_Amount amount; static struct TALER_Amount value; static struct TALER_DenomFeeSet global_fees; static struct TALER_Amount fee_closing; @@ -714,7 +716,8 @@ audit_reserve_in_cb (void *cls, * * @param cls closure * @param rowid unique serial ID for the refresh session in our DB - * @param h_blind_ev blinded hash of the coin's public key + * @param num_evs number of elements in @e h_blind_evs + * @param h_blind_evs array @e num_evs of blinded hashes of the coin's public keys * @param denom_pub public denomination key of the deposited coin * @param reserve_pub public key of the reserve * @param reserve_sig signature over the withdraw operation @@ -725,7 +728,8 @@ audit_reserve_in_cb (void *cls, static enum GNUNET_GenericReturnValue audit_reserve_out_cb (void *cls, uint64_t rowid, - const struct TALER_BlindedCoinHashP *h_blind_ev, + size_t num_evs, + const struct TALER_BlindedCoinHashP *h_blind_evs, const struct TALER_DenominationPublicKey *denom_pub, const struct TALER_ReservePublicKeyP *reserve_pub, const struct TALER_ReserveSignatureP *reserve_sig, @@ -734,7 +738,7 @@ audit_reserve_out_cb (void *cls, { (void) cls; (void) rowid; - (void) h_blind_ev; + (void) h_blind_evs; (void) denom_pub; (void) reserve_pub; (void) reserve_sig; @@ -1186,11 +1190,12 @@ run (void *cls) struct DenomKeyPair *dkp = NULL; struct TALER_MasterSignatureP master_sig; struct TALER_EXCHANGEDB_CollectableBlindcoin cbc; - struct TALER_EXCHANGEDB_CollectableBlindcoin cbc2; struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; struct TALER_EXCHANGEDB_ReserveHistory *rh_head; struct TALER_EXCHANGEDB_BankTransfer *bt; - struct TALER_EXCHANGEDB_CollectableBlindcoin *withdraw; + struct TALER_EXCHANGEDB_CollectableBlindcoin *batch_withdraw; + struct TALER_EXCHANGEDB_Withdraw withdraw; + struct TALER_HashBlindedPlanchetsP h_planchets; struct TALER_EXCHANGEDB_CoinDepositInformation deposit; struct TALER_EXCHANGEDB_BatchDeposit bd; struct TALER_CoinSpendPublicKeyP cpub2; @@ -1212,7 +1217,8 @@ run (void *cls) uint64_t rrc_serial; struct TALER_EXCHANGEDB_Refresh refresh; struct TALER_DenominationPublicKey *new_denom_pubs = NULL; - uint64_t reserve_out_serial_id; + struct TALER_WithdrawCommitmentHashP h_commitment; + uint64_t withdraw_serial_id; uint64_t melt_serial_id; struct TALER_PlanchetMasterSecretP ps; union GNUNET_CRYPTO_BlindingSecretP bks; @@ -1236,7 +1242,7 @@ run (void *cls) 0, sizeof (refresh)); ZR_BLK (&cbc); - ZR_BLK (&cbc2); + ZR_BLK (&withdraw); if (NULL == (plugin = TALER_EXCHANGEDB_plugin_load (cfg, true))) @@ -1258,18 +1264,23 @@ run (void *cls) plugin->start (plugin->cls, "test-1")); + /* test DB is empty */ FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != plugin->select_recoup_above_serial_id (plugin->cls, 0, &recoup_cb, NULL)); + /* simple extension check */ FAILIF (GNUNET_OK != test_extension_manifest ()); RND_BLK (&reserve_pub); GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (CURRENCY ":1.000000", + &amount)); + GNUNET_assert (GNUNET_OK == TALER_string_to_amount (CURRENCY ":1.000010", &value)); GNUNET_assert (GNUNET_OK == @@ -1366,42 +1377,37 @@ run (void *cls) { struct TALER_PlanchetDetail pd; struct TALER_CoinSpendPublicKeyP coin_pub; - struct TALER_AgeCommitmentHash age_hash; - struct TALER_AgeCommitmentHash *p_ah[2] = { - NULL, - &age_hash - }; - /* Call TALER_denom_blind()/TALER_denom_sign_blinded() twice, once without - * age_hash, once with age_hash */ - RND_BLK (&age_hash); - for (size_t i = 0; i < sizeof(p_ah) / sizeof(p_ah[0]); i++) - { - RND_BLK (&coin_pub); - GNUNET_assert (GNUNET_OK == - TALER_denom_blind (&dkp->pub, - &bks, - NULL, - p_ah[i], - &coin_pub, - alg_values, - &c_hash, - &pd.blinded_planchet)); - TALER_coin_ev_hash (&pd.blinded_planchet, - &cbc.denom_pub_hash, - &cbc.h_coin_envelope); - if (i != 0) - TALER_blinded_denom_sig_free (&cbc.sig); - GNUNET_assert ( - GNUNET_OK == - TALER_denom_sign_blinded ( - &cbc.sig, - &dkp->priv, - false, - &pd.blinded_planchet)); - TALER_blinded_planchet_free (&pd.blinded_planchet); - } + RND_BLK (&coin_pub); + GNUNET_assert (GNUNET_OK == + TALER_denom_blind (&dkp->pub, + &bks, + NULL, + NULL, + &coin_pub, + alg_values, + &c_hash, + &pd.blinded_planchet)); + TALER_coin_ev_hash (&pd.blinded_planchet, + &cbc.denom_pub_hash, + &cbc.h_coin_envelope); + + GNUNET_assert ( + GNUNET_OK == + TALER_denom_sign_blinded ( + &cbc.sig, + &dkp->priv, + false, + &pd.blinded_planchet)); + + TALER_wallet_blinded_planchets_hash ( + 1, + &pd.blinded_planchet, + &cbc.denom_pub_hash, + &h_planchets); + + TALER_blinded_planchet_free (&pd.blinded_planchet); } cbc.reserve_pub = reserve_pub; @@ -1418,36 +1424,58 @@ run (void *cls) bool conflict; bool denom_unknown; uint16_t maximum_age; - uint64_t ruuid; + uint32_t reserve_birthday; + uint64_t denom_serial = 1; /* FIXME: this should be taken from the database */ struct TALER_Amount reserve_balance; + struct TALER_EXCHANGEDB_Withdraw commitment = { + .amount_with_fee = value, + .age_restricted = false, + .max_age = 0, + .noreveal_index = 0, + .reserve_pub = reserve_pub, + .reserve_sig = cbc.reserve_sig, + .num_coins = 1, + .h_coin_evs = &cbc.h_coin_envelope, + .denom_sigs = &cbc.sig, + .denom_serials = &denom_serial, + }; + +#pragma message "find the denomination serial" + /* Find denomination's serial */ + { + } + + + /** + * Calculate the commitment + */ + TALER_wallet_withdraw_commit ( + &reserve_pub, + &amount, + &global_fees.withdraw, + &h_planchets, + NULL, + 0, + &commitment.h_commitment); FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != - plugin->do_batch_withdraw (plugin->cls, - now, - &reserve_pub, - &value, - true, - &found, - &balance_ok, - &reserve_balance, - &age_ok, - &maximum_age, - &ruuid)); - FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != - plugin->do_batch_withdraw_insert (plugin->cls, - NULL, - &cbc, - now, - ruuid, - &denom_unknown, - &conflict, - &nonce_reuse)); + plugin->do_withdraw (plugin->cls, + &commitment, + now, + &found, + &balance_ok, + &reserve_balance, + &age_ok, + &maximum_age, + &reserve_birthday, + &conflict)); GNUNET_assert (found); GNUNET_assert (! nonce_reuse); GNUNET_assert (! denom_unknown); GNUNET_assert (balance_ok); - } + h_commitment = commitment.h_commitment; + } FAILIF (GNUNET_OK != check_reserve (&reserve_pub, @@ -1460,20 +1488,19 @@ run (void *cls) value.fraction, value.currency)); FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != - plugin->get_reserve_by_h_blind (plugin->cls, - &cbc.h_coin_envelope, - &reserve_pub3, - &reserve_out_serial_id)); + plugin->get_reserve_by_h_commitment (plugin->cls, + &h_commitment, + &reserve_pub3, + &withdraw_serial_id)); FAILIF (0 != GNUNET_memcmp (&reserve_pub, &reserve_pub3)); - FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != - plugin->get_withdraw_info (plugin->cls, - &cbc.h_coin_envelope, - &cbc2)); - FAILIF (0 != GNUNET_memcmp (&cbc2.reserve_sig, + plugin->get_withdraw (plugin->cls, + &h_commitment, + &withdraw)); + FAILIF (0 != GNUNET_memcmp (&withdraw.reserve_sig, &cbc.reserve_sig)); - FAILIF (0 != GNUNET_memcmp (&cbc2.reserve_pub, + FAILIF (0 != GNUNET_memcmp (&withdraw.reserve_pub, &cbc.reserve_pub)); result = 6; @@ -1482,7 +1509,7 @@ run (void *cls) GNUNET_assert (GNUNET_OK == TALER_denom_sig_unblind (&ds, - &cbc2.sig, + &withdraw.denom_sigs[0], &bks, &c_hash, alg_values, @@ -1815,7 +1842,7 @@ run (void *cls) FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != plugin->do_recoup (plugin->cls, &reserve_pub, - reserve_out_serial_id, + withdraw_serial_id, &coin_blind, &deposit.coin.coin_pub, known_coin_id, @@ -1885,6 +1912,7 @@ run (void *cls) FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != plugin->commit (plugin->cls)); + /* check reserve history */ { struct TALER_Amount balance; @@ -1922,15 +1950,25 @@ run (void *cls) bt->amount.currency)); FAILIF (NULL == bt->sender_account_details.full_payto); break; - case TALER_EXCHANGEDB_RO_WITHDRAW_COIN: - withdraw = rh_head->details.withdraw; + case TALER_EXCHANGEDB_RO_BATCH_WITHDRAW_COIN: + batch_withdraw = rh_head->details.batch_withdraw; FAILIF (0 != - GNUNET_memcmp (&withdraw->reserve_pub, + GNUNET_memcmp (&batch_withdraw->reserve_pub, &reserve_pub)); FAILIF (0 != - GNUNET_memcmp (&withdraw->h_coin_envelope, + GNUNET_memcmp (&batch_withdraw->h_coin_envelope, &cbc.h_coin_envelope)); break; + case TALER_EXCHANGEDB_RO_WITHDRAW_COINS: + { + struct TALER_EXCHANGEDB_Withdraw *withdraw = + rh_head->details.withdraw; + FAILIF (0 != + GNUNET_memcmp (&withdraw->reserve_pub, + &reserve_pub)); +#pragma message "maybe more tests!?" + } + break; case TALER_EXCHANGEDB_RO_RECOUP_COIN: { struct TALER_EXCHANGEDB_Recoup *recoup = rh_head->details.recoup; @@ -2469,7 +2507,7 @@ cleanup: GNUNET_free (new_dkp); TALER_denom_sig_free (&deposit.coin.denom_sig); TALER_blinded_denom_sig_free (&cbc.sig); - TALER_blinded_denom_sig_free (&cbc2.sig); + TALER_blinded_denom_sig_free (withdraw.denom_sigs); dkp = NULL; TALER_EXCHANGEDB_plugin_unload (plugin); plugin = NULL; diff --git a/src/include/taler_crypto_lib.h b/src/include/taler_crypto_lib.h @@ -154,6 +154,17 @@ struct TALER_ReservePublicKeyP struct GNUNET_CRYPTO_EddsaPublicKey eddsa_pub; }; +/** + * @brief Type of hashes of public keys for Taler reserves. + */ +struct TALER_HashReservePublicKeyP +{ + /** + * Hash of the public key. + */ + struct GNUNET_HashCode hash; +}; + /** * @brief Type of private keys for Taler reserves. @@ -528,11 +539,27 @@ struct TALER_AgeCommitmentPublicKeyP /** - * @brief Hash to represent the commitment to n*kappa blinded keys during a - * age-withdrawal. It is the running SHA512 hash over the hashes of the blinded - * envelopes of n*kappa coins. + * @brief This is the running SHA512-hash over all + * `TALER_BlindedCoinHashP` values of an array of coins. + * Note that each `TALER_BlindedCoinHashP` itself + * captures the hash of the corresponding denomination's + * public key. */ -struct TALER_AgeWithdrawCommitmentHashP +struct TALER_HashBlindedPlanchetsP +{ + struct GNUNET_HashCode hash; +}; + + +/** + * @brief Hash to represent the commitment to a withdraw operation, + * which needs to be signed. It is calculated as hash over the struct + * TALER_WithdrawCommitmentP. + * + * The hash is needed for later calls to /reveal-withdraw (in case of age restriction) + * and /recoup. + */ +struct TALER_WithdrawCommitmentHashP { struct GNUNET_HashCode hash; }; @@ -731,6 +758,40 @@ struct TALER_RsaPubHashP /** + * Master seed material for the deriviation of all secrets + * for a batch of coins in a withdraw request. + */ +struct TALER_WithdrawMasterSeedP +{ + /** + * Seed material. + */ + uint32_t seed_data[8]; + +}; + + +/** + * The tuple of TALER_CNC_KAPPA many seeds + * for candidates for a batch of age-restricted coins. + */ +struct TALER_KappaWithdrawMasterSeedP +{ + struct TALER_WithdrawMasterSeedP tuple[TALER_CNC_KAPPA]; +}; + + +/** + * Tuple of secrets for TALER_CNC_KAPPA-1 many coin candidates, + * that need to be disclosed during the /reveal-withdraw step. + */ +struct TALER_RevealWithdrawMasterSeedsP +{ + struct TALER_WithdrawMasterSeedP tuple[TALER_CNC_KAPPA - 1]; +}; + + +/** * Master key material for the deriviation of * private coins and blinding factors during * withdraw or refresh. @@ -1456,6 +1517,35 @@ struct TALER_ExchangeWithdrawValues struct GNUNET_CRYPTO_BlindingInputValues *blinding_inputs; }; +/** + * @brief Response to a blinding prepare request. + * + */ +struct TALER_BlindingPrepareResponse +{ + /** + * Type of signature + */ + enum GNUNET_CRYPTO_BlindSignatureAlgorithm cipher; + + /** + * Number of entries in @e details. + */ + size_t num; + + /** + * Details, depending on @e cipher. + */ + union + { + /** + * Array @a num public pairs, if we use #GNUNET_CRYPTO_BSA_CS in @a cipher. + */ + struct GNUNET_CRYPTO_CSPublicRPairP *cs; + + } details; + +}; /** * Return the alg value singleton for creation of @@ -2076,7 +2166,7 @@ TALER_planchet_secret_to_transfer_priv ( /** - * Setup secret seed information for fresh coins to be + * Setup secret information for fresh a coin to be * withdrawn. * * @param[out] ps value to initialize @@ -2085,6 +2175,46 @@ void TALER_planchet_master_setup_random ( struct TALER_PlanchetMasterSecretP *ps); +/** + * Setup secret seed information for a batch of fresh coins to be + * withdrawn. + * + * @param[out] seed value to initialize + */ +void +TALER_withdraw_master_seed_setup_random ( + struct TALER_WithdrawMasterSeedP *seed); + + +/** + * Setup the seeds for a batch of age-restricted coins from a seed. + * The withdraw of provably age-restricted coins requires TALER_CNC_KAPPA many + * candidates during the initial /withdraw. + * + * @param seed The input seed + * @param[out] tuple tuples of secrets to fill + */ +void +TALER_expand_seed_to_kappa_seeds ( + const struct TALER_WithdrawMasterSeedP *seed, + struct TALER_KappaWithdrawMasterSeedP *tuple); + + +/** + * Setup the secrets for a batch of coins from a seed. + * Note that if the number of coins is one, the secret will + * be a copy of the seed. + * + * @param num_coins The number of coins, i.e. elements in @e secrets + * @param seed The input seed + * @param[out] secrets Array of @e num_coins secrets to fill + */ +void +TALER_expand_withdraw_secrets ( + size_t num_coins, + const struct TALER_WithdrawMasterSeedP *seed, + struct TALER_PlanchetMasterSecretP secrets[static num_coins]); + /** * Setup secret seed for fresh coins to be refreshed. @@ -3992,7 +4122,8 @@ TALER_wallet_link_verify ( /** - * Sign withdraw request. + * Sign withdraw request, pre-v26 of the protocol + * @note: this function will be removed in future releases. * * @param h_denom_pub hash of the denomiantion public key of the coin to withdraw * @param amount_with_fee amount to debit the reserve for @@ -4001,7 +4132,7 @@ TALER_wallet_link_verify ( * @param[out] reserve_sig resulting signature */ void -TALER_wallet_withdraw_sign ( +TALER_wallet_withdraw_sign_pre26 ( const struct TALER_DenominationHashP *h_denom_pub, const struct TALER_Amount *amount_with_fee, const struct TALER_BlindedCoinHashP *bch, @@ -4010,7 +4141,8 @@ TALER_wallet_withdraw_sign ( /** - * Verify withdraw request. + * Verify withdraw request, pre-v26 of the protocol + * @note: this function will be removed in future releases. * * @param h_denom_pub hash of the denomiantion public key of the coin to withdraw * @param amount_with_fee amount to debit the reserve for @@ -4020,7 +4152,7 @@ TALER_wallet_withdraw_sign ( * @return #GNUNET_OK if the signature is valid */ enum GNUNET_GenericReturnValue -TALER_wallet_withdraw_verify ( +TALER_wallet_withdraw_verify_pre26 ( const struct TALER_DenominationHashP *h_denom_pub, const struct TALER_Amount *amount_with_fee, const struct TALER_BlindedCoinHashP *bch, @@ -4029,44 +4161,148 @@ TALER_wallet_withdraw_verify ( /** - * Sign age-withdraw request. + * @brief Calculate the hash of a reserve public key * - * @param h_commitment hash over all n*kappa blinded coins in the commitment for the age-withdraw - * @param amount_with_fee amount to debit the reserve for - * @param mask the mask that defines the age groups - * @param max_age maximum age from which the age group is derived, that the withdrawn coins must be restricted to. + * @param reserve_pub public key of the reserve + * @return hash value + */ +struct TALER_HashReservePublicKeyP +TALER_wallet_hash_reserve_pub ( + const struct TALER_ReservePublicKeyP *reserve_pub); + +/** + * @brief Calculate the hash of a batch of blinded planchets + * + * @param num_planchets Number of planchets in @e planchets + * @param blinded_planchets Array @e num_planchets of blinded coin planchets + * @param h_denom_pubs Array @e num_planchets of hashes of corresponding denomination public keys + * @param[out] h_planchets Calculated hash + */ +void +TALER_wallet_blinded_planchets_hash ( + size_t num_planchets, + const struct TALER_BlindedPlanchet blinded_planchets[static num_planchets], + const struct TALER_DenominationHashP h_denom_pubs[static num_planchets], + struct TALER_HashBlindedPlanchetsP *h_planchets); + +/** + * Calculate a windraw commitment hash for a withdraw request, + * optionally with age restriction. If age restriction is applied, + * the denominations for the coins MUST support it. + * + * @param reserve_pub public key of the reserve + * @param amount total amount to withdraw, excluding fees + * @param fee total amount of fees + * @param h_planchets running hash over all coins' TALER_BlindingCoinHash values + * @param mask age mask to apply, or NULL, if not applicable. + * @param max_age maximum age (in years) to commit to. Must be 0 if age restriction does not apply + * @param[out] wch resulting hash + */ +void +TALER_wallet_withdraw_commit ( + const struct TALER_ReservePublicKeyP *reserve_pub, + const struct TALER_Amount *amount, + const struct TALER_Amount *fee, + const struct TALER_HashBlindedPlanchetsP *h_planchets, + const struct TALER_AgeMask *mask, + uint8_t max_age, + struct TALER_WithdrawCommitmentHashP *wch); + +/** + * Calculate a windraw commitment hash for a withdraw request + * without age restriction. Note that the denominations for + * the coins MUST NOT support age restriction. + * + * @param reserve_pub public key of the reserve + * @param amount total amount to withdraw, excluding fees + * @param fee total amount of fees + * @param h_planchets running hash over all coins' TALER_BlindingCoinHash values + * @param[out] wch resulting hash + */ +#define TALER_wallet_withdraw_commit_without_age(reserve_pub, \ + amount, \ + fee, \ + h_planchets, \ + wch) \ + TALER_wallet_withdraw_commit ((reserve_pub), \ + (amount), \ + (fee), \ + (h_planchets), \ + NULL, \ + 0, \ + (wch)); + + +/** + * @brief Sign the hash of a withdraw request with the reserve's private key. + * + * @param amount total amount to withdraw, excluding fees + * @param fee total amount of fees + * @param h_planchets running hash over all coins' TALER_BlindingCoinHash values + * @param mask age mask to apply, or NULL, if not applicable. + * @param max_age maximum age (in years) to commit to. Must be 0 if age restriction does not apply * @param reserve_priv private key to sign with * @param[out] reserve_sig resulting signature */ void -TALER_wallet_age_withdraw_sign ( - const struct TALER_AgeWithdrawCommitmentHashP *h_commitment, - const struct TALER_Amount *amount_with_fee, +TALER_wallet_withdraw_sign ( + const struct TALER_Amount *amount, + const struct TALER_Amount *fee, + const struct TALER_HashBlindedPlanchetsP *h_planchets, const struct TALER_AgeMask *mask, uint8_t max_age, const struct TALER_ReservePrivateKeyP *reserve_priv, struct TALER_ReserveSignatureP *reserve_sig); + /** - * Verify an age-withdraw request. + * Verify withdraw request, with the reserve's public key * - * @param h_commitment hash all n*kappa blinded coins in the commitment for the age-withdraw - * @param amount_with_fee amount to debit the reserve for - * @param mask the mask that defines the age groups - * @param max_age maximum age from which the age group is derived, that the withdrawn coins must be restricted to. + * @param amount total amount to withdraw, excluding fees + * @param fee total amount of fees + * @param h_planchets running hash over all coins' TALER_BlindingCoinHash values + * @param mask age mask to apply, or NULL, if not applicable. + * @param max_age maximum age (in years) to commit to. Must be 0 if age restriction does not apply * @param reserve_pub public key of the reserve * @param reserve_sig resulting signature * @return #GNUNET_OK if the signature is valid */ enum GNUNET_GenericReturnValue -TALER_wallet_age_withdraw_verify ( - const struct TALER_AgeWithdrawCommitmentHashP *h_commitment, - const struct TALER_Amount *amount_with_fee, +TALER_wallet_withdraw_verify ( + const struct TALER_Amount *amount, + const struct TALER_Amount *fee, + const struct TALER_HashBlindedPlanchetsP *h_planchets, const struct TALER_AgeMask *mask, uint8_t max_age, const struct TALER_ReservePublicKeyP *reserve_pub, const struct TALER_ReserveSignatureP *reserve_sig); + +/** + * Verify withdraw request with no age restriction, + * using the reserve's public key + * + * @param amount total amount to withdraw, excluding fees + * @param fee total amount of fees + * @param h_planchets running hash over all coins' TALER_BlindingCoinHash values + * @param reserve_pub public key of the reserve + * @param reserve_sig resulting signature + * @return #GNUNET_OK if the signature is valid + */ +#define TALER_wallet_withdraw_verify_without_age(amount, \ + fee, \ + h_planchets, \ + reserve_pub, \ + reserve_sig) \ + TALER_wallet_withdraw_verify ((amount), \ + (fee), \ + (h_planchets), \ + NULL, \ + 0, \ + (reserve_pub), \ + (reserve_sig)); + + /** * Verify exchange melt confirmation. * @@ -5147,40 +5383,68 @@ TALER_exchange_online_purse_status_verify ( /** - * Create age-withdraw confirmation signature. + * Create withdraw confirmation signature, for a request with age restriction set. * * @param scb function to call to create the signature - * @param h_commitment age-withdraw commitment that identifies the n*kappa blinded coins + * @param h_commitment withdraw commitment that identifies the n*kappa blinded coins * @param noreveal_index gamma cut-and-choose value chosen by the exchange * @param[out] pub where to write the exchange public key * @param[out] sig where to write the exchange signature * @return #TALER_EC_NONE on success */ enum TALER_ErrorCode -TALER_exchange_online_age_withdraw_confirmation_sign ( +TALER_exchange_online_withdraw_age_confirmation_sign ( TALER_ExchangeSignCallback scb, - const struct TALER_AgeWithdrawCommitmentHashP *h_commitment, + const struct TALER_WithdrawCommitmentHashP *h_commitment, uint32_t noreveal_index, struct TALER_ExchangePublicKeyP *pub, struct TALER_ExchangeSignatureP *sig); +/** + * Create withdraw confirmation signature, for a request without age restriction. + * + * @param scb function to call to create the signature + * @param h_commitment withdraw commitment that identifies the n blinded coins + * @param[out] pub where to write the exchange public key + * @param[out] sig where to write the exchange signature + * @return #TALER_EC_NONE on success + */ +enum TALER_ErrorCode +TALER_exchange_online_withdraw_confirmation_sign ( + TALER_ExchangeSignCallback scb, + const struct TALER_WithdrawCommitmentHashP *h_commitment, + struct TALER_ExchangePublicKeyP *pub, + struct TALER_ExchangeSignatureP *sig); + + +/** + * Verify an exchange withdraw confirmation, for a request without age restriction + * + * @param h_commitment Commitment over all n coin candidates from the original request to withdraw + * @param exchange_pub The public key used for signing + * @param exchange_sig The signature from the exchange + */ +enum GNUNET_GenericReturnValue +TALER_exchange_online_withdraw_confirmation_verify ( + const struct TALER_WithdrawCommitmentHashP *h_commitment, + const struct TALER_ExchangePublicKeyP *exchange_pub, + const struct TALER_ExchangeSignatureP *exchange_sig); /** - * Verify an exchange age-withdraw confirmation + * Verify an exchange withdraw confirmation, for a withdraw request with age restriction * - * @param h_commitment Commitment over all n*kappa coin candidates from the original request to age-withdraw + * @param h_commitment Commitment over all n (or n*kappa) coin candidates from the original request to withdraw * @param noreveal_index The index returned by the exchange * @param exchange_pub The public key used for signing * @param exchange_sig The signature from the exchange */ enum GNUNET_GenericReturnValue -TALER_exchange_online_age_withdraw_confirmation_verify ( - const struct TALER_AgeWithdrawCommitmentHashP *h_commitment, +TALER_exchange_online_withdraw_age_confirmation_verify ( + const struct TALER_WithdrawCommitmentHashP *h_commitment, uint32_t noreveal_index, const struct TALER_ExchangePublicKeyP *exchange_pub, const struct TALER_ExchangeSignatureP *exchange_sig); - /* ********************* offline signing ************************** */ diff --git a/src/include/taler_exchange_service.h b/src/include/taler_exchange_service.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2024 Taler Systems SA + Copyright (C) 2014-2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -1718,103 +1718,6 @@ void TALER_EXCHANGE_csr_melt_cancel (struct TALER_EXCHANGE_CsRMeltHandle *csrh); -/* ********************* POST /csr-withdraw *********************** */ - - -/** - * @brief A /csr-withdraw Handle - */ -struct TALER_EXCHANGE_CsRWithdrawHandle; - - -/** - * Details about a response for a CS R request. - */ -struct TALER_EXCHANGE_CsRWithdrawResponse -{ - /** - * HTTP response data. - */ - struct TALER_EXCHANGE_HttpResponse hr; - - /** - * Details about the response. - */ - union - { - /** - * Details if the status is #MHD_HTTP_OK. - */ - struct - { - /** - * Values contributed by the exchange for the - * respective coin's withdraw operation. - */ - struct TALER_ExchangeWithdrawValues alg_values; - - } ok; - - /** - * Details if the status is #MHD_HTTP_GONE. - */ - struct - { - /* TODO: returning full details is not implemented */ - } gone; - - } details; -}; - - -/** - * Callbacks of this type are used to serve the result of submitting a - * CS R withdraw request to a exchange. - * - * @param cls closure - * @param csrr response details - */ -typedef void -(*TALER_EXCHANGE_CsRWithdrawCallback) ( - void *cls, - const struct TALER_EXCHANGE_CsRWithdrawResponse *csrr); - - -/** - * Get a CS R using a /csr-withdraw request. - * - * @param curl_ctx The curl context to use for the requests - * @param exchange_url Base-URL to the excnange - * @param pk Which denomination key is the /csr request for - * @param nonce client nonce for the request - * @param res_cb the callback to call when the final result for this request is available - * @param res_cb_cls closure for the above callback - * @return handle for the operation on success, NULL on error, i.e. - * if the inputs are invalid (i.e. denomination key not with this exchange). - * In this case, the callback is not called. - */ -struct TALER_EXCHANGE_CsRWithdrawHandle * -TALER_EXCHANGE_csr_withdraw ( - struct GNUNET_CURL_Context *curl_ctx, - const char *exchange_url, - const struct TALER_EXCHANGE_DenomPublicKey *pk, - const struct GNUNET_CRYPTO_CsSessionNonce *nonce, - TALER_EXCHANGE_CsRWithdrawCallback res_cb, - void *res_cb_cls); - - -/** - * - * Cancel a CS R withdraw request. This function cannot be used - * on a request handle if a response is already served for it. - * - * @param csrh the withdraw handle - */ -void -TALER_EXCHANGE_csr_withdraw_cancel ( - struct TALER_EXCHANGE_CsRWithdrawHandle *csrh); - - /* ********************* GET /coins/$COIN_PUB *********************** */ /** @@ -2157,9 +2060,9 @@ enum TALER_EXCHANGE_ReserveTransactionType TALER_EXCHANGE_RTT_WITHDRAWAL, /** - * Age-Withdrawal from the reserve. + * Batch-Withdrawal from the reserve (pre26) */ - TALER_EXCHANGE_RTT_AGEWITHDRAWAL, + TALER_EXCHANGE_RTT_BATCH_WITHDRAWAL, /** * /recoup operation. @@ -2235,8 +2138,8 @@ struct TALER_EXCHANGE_ReserveHistoryEntry } in_details; /** - * Information about withdraw operation. - * @e type is #TALER_EXCHANGE_RTT_WITHDRAWAL. + * Information about batch withdraw operation. + * @e type is #TALER_EXCHANGE_RTT_BATCH_WITHDRAWAL. */ struct { @@ -2249,11 +2152,12 @@ struct TALER_EXCHANGE_ReserveHistoryEntry * Fee that was charged for the withdrawal. */ struct TALER_Amount fee; - } withdraw; + } batch_withdraw; + /** - * Information about withdraw operation. - * @e type is #TALER_EXCHANGE_RTT_AGEWITHDRAWAL. + * Information about withdraw operation with age-restriction. + * @e type is #TALER_EXCHANGE_RTT_WITHDRAWAL. */ struct { @@ -2263,15 +2167,42 @@ struct TALER_EXCHANGE_ReserveHistoryEntry json_t *out_authorization_sig; /** - * Maximum age committed + * If age restriction was required during the protocol + */ + bool age_restricted; + + /** + * Maximum age committed, if age_restricted is true */ uint8_t max_age; /** + * If age_restricted is true, the index that is not to be revealed + * after the initial commitment in /withdraw + */ + uint8_t noreveal_index; + + /** + * The commitment of the withdrawal + */ + struct TALER_WithdrawCommitmentHashP h_commitment; + + /** + * The running hash over all hashes of blinded planchets of the withrdawal + */ + struct TALER_HashBlindedPlanchetsP h_planchets; + + /** * Fee that was charged for the withdrawal. */ struct TALER_Amount fee; - } age_withdraw; + + /** + * Number of coins withdrawn + */ + uint16_t num_coins; + + } withdraw; /** * Information provided if the reserve was filled via /recoup. @@ -2691,77 +2622,32 @@ TALER_EXCHANGE_reserves_history_cancel ( struct TALER_EXCHANGE_ReservesHistoryHandle *rsh); -/** - * Information input into the withdraw process per coin. - */ -struct TALER_EXCHANGE_WithdrawCoinInput -{ - /** - * Denomination of the coin. - */ - const struct TALER_EXCHANGE_DenomPublicKey *pk; - - /** - * Master key material for the coin. - */ - const struct TALER_PlanchetMasterSecretP *ps; - - /** - * Age commitment for the coin. - */ - const struct TALER_AgeCommitmentHash *ach; - -}; - +/* ********************* /blinding-prepare *************** */ /** - * All the details about a coin that are generated during withdrawal and that - * may be needed for future operations on the coin. + * @brief Input for the a /blinding-prepare request + * + * It consists of a number of nonces needed for Clause-Schnorr signatures + * and the corresponding denomination public keys. */ -struct TALER_EXCHANGE_PrivateCoinDetails -{ - /** - * Private key of the coin. - */ - struct TALER_CoinSpendPrivateKeyP coin_priv; - - /** - * Value used to blind the key for the signature. - * Needed for recoup operations. - */ - union GNUNET_CRYPTO_BlindingSecretP bks; - - /** - * Signature over the coin. - */ - struct TALER_DenominationSignature sig; - - /** - * Values contributed from the exchange during the - * withdraw protocol. - */ - struct TALER_ExchangeWithdrawValues exchange_vals; -}; - /** - * @brief A /reserves/$RESERVE_PUB/batch-withdraw Handle + * @brief A handle to a /blinding-prepare request */ -struct TALER_EXCHANGE_BatchWithdrawHandle; - +struct TALER_EXCHANGE_BlindingPrepareHandle; /** - * Details about a response for a batch withdraw request. + * Response from the exchange for a /blinding-prepare request */ -struct TALER_EXCHANGE_BatchWithdrawResponse +struct TALER_EXCHANGE_BlindingPrepareResponse { /** - * HTTP response data. + * The HTTP Response object */ struct TALER_EXCHANGE_HttpResponse hr; /** - * Details about the response. + * Details of the response, depending on the http status */ union { @@ -2770,336 +2656,83 @@ struct TALER_EXCHANGE_BatchWithdrawResponse */ struct { - /** - * Array of coins returned by the batch withdraw operation. + * Number of entries in @e alg_values, etc. */ - struct TALER_EXCHANGE_PrivateCoinDetails *coins; + size_t num; /** - * Length of the @e coins array. + * Array @a num withdraw values form the exchange. */ - unsigned int num_coins; - } ok; - - /** - * Details if the status is #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS. - */ - struct TALER_EXCHANGE_KycNeededRedirect unavailable_for_legal_reasons; - - /** - * Details if the status is #MHD_HTTP_CONFLICT. - */ - struct - { - /* TODO: returning full details is not implemented */ - } conflict; - - /** - * Details if the status is #MHD_HTTP_GONE. - */ - struct - { - /* TODO: returning full details is not implemented */ - } gone; - - } details; -}; - - -/** - * Callbacks of this type are used to serve the result of submitting a - * batch withdraw request to a exchange. - * - * @param cls closure - * @param wr response details - */ -typedef void -(*TALER_EXCHANGE_BatchWithdrawCallback) ( - void *cls, - const struct TALER_EXCHANGE_BatchWithdrawResponse *wr); - - -/** - * Withdraw multiple coins from the exchange using a /reserves/$RESERVE_PUB/batch-withdraw - * request. This API is typically used by a wallet to withdraw many coins from a - * reserve. The blind signatures are unblinded and verified before being returned - * to the caller at @a res_cb. - * - * Note that to ensure that no money is lost in case of hardware - * failures, the caller must have committed (most of) the arguments to - * disk before calling, and be ready to repeat the request with the - * same arguments in case of failures. - * - * @param curl_ctx The curl context to use - * @param exchange_url The base-URL of the exchange - * @param keys The /keys material from the exchange - * @param reserve_priv private key of the reserve to withdraw from - * @param wci_length number of entries in @a wcis - * @param wcis inputs that determine the planchets - * @param res_cb the callback to call when the final result for this request is available - * @param res_cb_cls closure for @a res_cb - * @return NULL - * if the inputs are invalid (i.e. denomination key not with this exchange). - * In this case, the callback is not called. - */ -struct TALER_EXCHANGE_BatchWithdrawHandle * -TALER_EXCHANGE_batch_withdraw ( - struct GNUNET_CURL_Context *curl_ctx, - const char *exchange_url, - const struct TALER_EXCHANGE_Keys *keys, - const struct TALER_ReservePrivateKeyP *reserve_priv, - unsigned int wci_length, - const struct TALER_EXCHANGE_WithdrawCoinInput wcis[static wci_length], - TALER_EXCHANGE_BatchWithdrawCallback res_cb, - void *res_cb_cls); - - -/** - * Cancel a batch withdraw status request. This function cannot be used on a - * request handle if a response is already served for it. - * - * @param wh the batch withdraw handle - */ -void -TALER_EXCHANGE_batch_withdraw_cancel ( - struct TALER_EXCHANGE_BatchWithdrawHandle *wh); - - -/** - * Response from a withdraw2 request. - */ -struct TALER_EXCHANGE_Withdraw2Response -{ - /** - * HTTP response data - */ - struct TALER_EXCHANGE_HttpResponse hr; + const struct TALER_ExchangeWithdrawValues *alg_values; - /** - * Response details depending on the HTTP status. - */ - union - { - /** - * Details if HTTP status is #MHD_HTTP_OK. - */ - struct - { - /** - * blind signature over the coin - */ - struct TALER_BlindedDenominationSignature blind_sig; } ok; - } details; - -}; - -/** - * Callbacks of this type are used to serve the result of submitting a - * withdraw request to a exchange without the (un)blinding factor. - * - * @param cls closure - * @param w2r response data - */ -typedef void -(*TALER_EXCHANGE_Withdraw2Callback) ( - void *cls, - const struct TALER_EXCHANGE_Withdraw2Response *w2r); - -/** - * @brief A /reserves/$RESERVE_PUB/withdraw Handle, 2nd variant. - * This variant does not do the blinding/unblinding and only - * fetches the blind signature on the already blinded planchet. - * Used internally by the `struct TALER_EXCHANGE_WithdrawHandle` - * implementation as well as for the tipping logic of merchants. - */ -struct TALER_EXCHANGE_Withdraw2Handle; - - -/** - * Withdraw a coin from the exchange using a /reserves/$RESERVE_PUB/withdraw - * request. This API is typically used by a merchant to withdraw a tip - * where the blinding factor is unknown to the merchant. Note that unlike - * the #TALER_EXCHANGE_batch_withdraw() API, this API neither unblinds the signatures - * nor can it verify that the exchange signatures are valid, so these tasks - * are left to the caller. Wallets probably should use #TALER_EXCHANGE_batch_withdraw() - * which integrates these steps. - * - * Note that to ensure that no money is lost in case of hardware - * failures, the caller must have committed (most of) the arguments to - * disk before calling, and be ready to repeat the request with the - * same arguments in case of failures. - * - * @param curl_ctx The curl-context to use - * @param exchange_url The base-URL of the exchange - * @param keys The /keys material from the exchange - * @param pd planchet details of the planchet to withdraw - * @param reserve_priv private key of the reserve to withdraw from - * @param res_cb the callback to call when the final result for this request is available - * @param res_cb_cls closure for @a res_cb - * @return NULL - * if the inputs are invalid (i.e. denomination key not with this exchange). - * In this case, the callback is not called. - */ -struct TALER_EXCHANGE_Withdraw2Handle * -TALER_EXCHANGE_withdraw2 ( - struct GNUNET_CURL_Context *curl_ctx, - const char *exchange_url, - struct TALER_EXCHANGE_Keys *keys, - const struct TALER_PlanchetDetail *pd, - const struct TALER_ReservePrivateKeyP *reserve_priv, - TALER_EXCHANGE_Withdraw2Callback res_cb, - void *res_cb_cls); - - -/** - * Cancel a withdraw status request. This function cannot be used - * on a request handle if a response is already served for it. - * - * @param wh the withdraw handle - */ -void -TALER_EXCHANGE_withdraw2_cancel (struct TALER_EXCHANGE_Withdraw2Handle *wh); - - -/** - * Response from a batch-withdraw request (2nd variant). - */ -struct TALER_EXCHANGE_BatchWithdraw2Response -{ - /** - * HTTP response data - */ - struct TALER_EXCHANGE_HttpResponse hr; - - /** - * Response details depending on the HTTP status. - */ - union - { - /** - * Details if HTTP status is #MHD_HTTP_OK. - */ struct { - /** - * array of blind signatures over the coins. - */ - const struct TALER_BlindedDenominationSignature *blind_sigs; - - /** - * length of @e blind_sigs - */ - unsigned int blind_sigs_length; - - } ok; - - /** - * Details if the status is #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS. - */ - struct TALER_EXCHANGE_KycNeededRedirect unavailable_for_legal_reasons; - + /* TODO: maybe add details for status #MHD_HTTP_GONE */ + } gone; } details; - }; /** - * Callbacks of this type are used to serve the result of submitting a batch - * withdraw request to a exchange without the (un)blinding factor. + * @brief Callback forthe response to a /blinding-prepare request * - * @param cls closure - * @param bw2r response data + * @param cls closure provided earlier + * @param bpr the response */ typedef void -(*TALER_EXCHANGE_BatchWithdraw2Callback) ( +(*TALER_EXCHANGE_BlindingPrepareCallback)( void *cls, - const struct TALER_EXCHANGE_BatchWithdraw2Response *bw2r); - + const struct TALER_EXCHANGE_BlindingPrepareResponse *bpr); /** - * @brief A /reserves/$RESERVE_PUB/batch-withdraw Handle, 2nd variant. - * This variant does not do the blinding/unblinding and only - * fetches the blind signatures on the already blinded planchets. - * Used internally by the `struct TALER_EXCHANGE_BatchWithdrawHandle` - * implementation as well as for the tipping logic of merchants. - */ -struct TALER_EXCHANGE_BatchWithdraw2Handle; - - -/** - * Withdraw a coin from the exchange using a /reserves/$RESERVE_PUB/batch-withdraw - * request. This API is typically used by a merchant to withdraw a tip - * where the blinding factor is unknown to the merchant. + * Submit an blinding-prepare request to the exchange and get the exchange's + * response. * - * Note that to ensure that no money is lost in case of hardware - * failures, the caller must have committed (most of) the arguments to - * disk before calling, and be ready to repeat the request with the - * same arguments in case of failures. + * This API is typically used by a wallet in preparation for a withdraw + * of coins that require additional input from the exchange for blinding, + * such as for Clause-Schnorr signatures. * - * @param curl_ctx The curl context to use - * @param exchange_url The base-URL of the exchange - * @param keys The /keys material from the exchange - * @param pds array of planchet details of the planchet to withdraw - * @param pds_length number of entries in the @a pds array - * @param reserve_priv private key of the reserve to withdraw from - * @param res_cb the callback to call when the final result for this request is available - * @param res_cb_cls closure for @a res_cb - * @return NULL - * if the inputs are invalid (i.e. denomination key not with this exchange). - * In this case, the callback is not called. - */ -struct TALER_EXCHANGE_BatchWithdraw2Handle * -TALER_EXCHANGE_batch_withdraw2 ( + * @param curl_ctx The curl context + * @param exchange_url The base url of the exchange + * @param num Number of elements in @e nonces and @e denoms_h + * @param nonces The @e num nonces for Clause-Schnorr + * @param denoms_h The corresponding @e num denomination public keys + * @param callback A callback for the result, maybe NULL + * @param callback_cls A closure for @e res_cb, maybe NULL + * @return a handle for this request on success; NULL if an argument was invalid. In this case, the callback will not be called. + */ +struct TALER_EXCHANGE_BlindingPrepareHandle * +TALER_EXCHANGE_blinding_prepare ( struct GNUNET_CURL_Context *curl_ctx, const char *exchange_url, - const struct TALER_EXCHANGE_Keys *keys, - const struct TALER_ReservePrivateKeyP *reserve_priv, - unsigned int pds_length, - const struct TALER_PlanchetDetail pds[static pds_length], - TALER_EXCHANGE_BatchWithdraw2Callback res_cb, - void *res_cb_cls); + size_t num, + const union GNUNET_CRYPTO_BlindSessionNonce *nonces, + const struct TALER_DenominationHashP *denoms_h, + TALER_EXCHANGE_BlindingPrepareCallback callback, + void *callback_cls); /** - * Cancel a batch withdraw request. This function cannot be used + * Cancel a blinding-prepare request. This function cannot be used * on a request handle if a response is already served for it. * - * @param wh the withdraw handle + * @param bph the blinding-preapre handle */ void -TALER_EXCHANGE_batch_withdraw2_cancel ( - struct TALER_EXCHANGE_BatchWithdraw2Handle *wh); +TALER_EXCHANGE_blinding_prepare_cancel ( + struct TALER_EXCHANGE_BlindingPrepareHandle *bph); -/* ********************* /reserve/$RESERVE_PUB/age-withdraw *************** */ +/* ********************* /withdraw *************** */ /** - * @brief Information needed to withdraw (and reveal) age restricted coins. - */ -struct TALER_EXCHANGE_AgeWithdrawCoinInput -{ - /** - * The master secret from which we derive all other relevant values for - * the coin: private key, nonces (if applicable) and age restriction - */ - struct TALER_PlanchetMasterSecretP secrets[TALER_CNC_KAPPA]; - - /** - * The denomination of the coin. Must support age restriction, i.e - * its .keys.age_mask MUST not be 0 - */ - struct TALER_EXCHANGE_DenomPublicKey *denom_pub; -}; - - -/** - * All the details about a coin that are generated during age-withdrawal and + * All the details about a coin that are generated during withdrawal and * that may be needed for future operations on the coin. */ -struct TALER_EXCHANGE_AgeWithdrawCoinPrivateDetails +struct TALER_EXCHANGE_WithdrawCoinPrivateDetails { /** * Private key of the coin. @@ -3107,6 +2740,11 @@ struct TALER_EXCHANGE_AgeWithdrawCoinPrivateDetails struct TALER_CoinSpendPrivateKeyP coin_priv; /** + * Public key of the coin. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** * Hash of the public key of the coin. */ struct TALER_CoinPubHashP h_coin_pub; @@ -3119,12 +2757,14 @@ struct TALER_EXCHANGE_AgeWithdrawCoinPrivateDetails /** * The age commitment, proof for the coin, derived from the - * Master secret and maximum age in the originating request + * Master secret and maximum age in the originating request. + * Only relevant for denominations with age-restriction support. */ struct TALER_AgeCommitmentProof age_commitment_proof; /** - * The hash of the age commitment + * The hash of the age commitment. + * Only relevant for denominations with age-restriction support. */ struct TALER_AgeCommitmentHash h_age_commitment; @@ -3138,17 +2778,23 @@ struct TALER_EXCHANGE_AgeWithdrawCoinPrivateDetails * The planchet constructed */ struct TALER_PlanchetDetail planchet; + + /** + * Signature over the coin. + */ + struct TALER_DenominationSignature denom_sig; + }; /** - * @brief A handle to a /reserves/$RESERVE_PUB/age-withdraw request + * @brief A handle to a /withdraw request */ -struct TALER_EXCHANGE_AgeWithdrawHandle; +struct TALER_EXCHANGE_WithdrawHandle; /** - * @brief Details about the response for a age withdraw request. + * @brief Details about the response for a withdraw request */ -struct TALER_EXCHANGE_AgeWithdrawResponse +struct TALER_EXCHANGE_WithdrawResponse { /** * HTTP response data. @@ -3160,22 +2806,48 @@ struct TALER_EXCHANGE_AgeWithdrawResponse */ union { + /** - * Details if the status is #MHD_HTTP_OK. + * Details if the status is #MHD_HTTP_OK */ struct { /** - * Index that should not be revealed during the age-withdraw reveal + * Number of signatures returned. + */ + unsigned int num_sigs; + + /** + * The computed details of the@e num_coins coins to keep, + * including the denomination-signatures in their @e .denom_sig field. + */ + const struct TALER_EXCHANGE_WithdrawCoinPrivateDetails *coin_details; + + /** + * The commitment of the withdraw request, needed for the later calls to /recoup + */ + struct TALER_WithdrawCommitmentHashP h_commitment; + + } ok; + + /** + * Details if the status is MHD_HTTP_CREATED, i.e. in case of + * age-restriction. The response is input to prepare the required + * follow-up call to /reveal-withdraw. + */ + struct TALER_EXCHANGE_WithdrawCreated + { + /** + * Index that should not be revealed during the reveal-withdraw * phase. */ uint8_t noreveal_index; /** - * The commitment of the age-withdraw request, needed for the - * subsequent call to /age-withdraw/$ACH/reveal + * The commitment of the withdraw request with age restriction, needed for the + * subsequent call to /reveal-withdraw and later calls to /recoup */ - struct TALER_AgeWithdrawCommitmentHashP h_commitment; + struct TALER_WithdrawCommitmentHashP h_commitment; /** * The number of elements in @e coins, each referring to @@ -3186,23 +2858,17 @@ struct TALER_EXCHANGE_AgeWithdrawResponse /** * The computed details of the non-revealed @e num_coins coins to keep. */ - const struct TALER_EXCHANGE_AgeWithdrawCoinPrivateDetails *coin_details; - - /** - * The array of blinded hashes of the non-revealed - * @e num_coins coins, needed for the reveal step; - */ - const struct TALER_BlindedCoinHashP *blinded_coin_hs; + const struct TALER_EXCHANGE_WithdrawCoinPrivateDetails *coin_details; /** * Key used by the exchange to sign the response. */ struct TALER_ExchangePublicKeyP exchange_pub; - } ok; + } created; /** - * Details if the status is #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS. - */ + * Details if the status is #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS. + */ struct TALER_EXCHANGE_KycNeededRedirect unavailable_for_legal_reasons; } details; @@ -3210,13 +2876,20 @@ struct TALER_EXCHANGE_AgeWithdrawResponse typedef void -(*TALER_EXCHANGE_AgeWithdrawCallback)( +(*TALER_EXCHANGE_WithdrawCallback)( void *cls, - const struct TALER_EXCHANGE_AgeWithdrawResponse *awr); + const struct TALER_EXCHANGE_WithdrawResponse *awr); /** - * Submit an age-withdraw request to the exchange and get the exchange's - * response. + * Submit an withdraw request to the exchange and get the exchange's + * response. The coin's planchet secrets are derived from a single seed. + * + * Note that a client can opaquely set a maximum age on the coins + * (if the denominations do support it), that remains opaque to the exchange. + * Only when the corresponding reserve has a birthday associated with it, the + * exchange might require a _proof_ for the correct age-restriction to be set. + * In that case, the exchange will respond with 409 and the client will need + * to call TALER_EXCHANGE_withdraw_with_age_proof instead. * * This API is typically used by a wallet. Note that to ensure that * no money is lost in case of hardware failures, the provided @@ -3227,97 +2900,134 @@ typedef void * @param exchange_url The base url of the exchange * @param keys The denomination keys from the exchange * @param reserve_priv The private key to the reserve - * @param num_coins The number of elements in @e coin_inputs - * @param coin_inputs The input for the coins to withdraw - * @param max_age The maximum age we commit to. - * @param res_cb A callback for the result, maybe NULL - * @param res_cb_cls A closure for @e res_cb, maybe NULL + * @param num_coins Number of coins to withdraw in a batch + * @param opaque_max_age The age to commit to opaquely. If not zero, the denominations MUST support age restriction. + * @param denoms_pub Array of @e num_coins of denominations of the coins to withdraw + * @param seed seed from which @e num_coins secrets for each coin are derived from + * @param callback A callback for the result, maybe NULL + * @param callback_cls A closure for @e res_cb, maybe NULL * @return a handle for this request; NULL if the argument was invalid. * In this case, the callback will not be called. */ -struct TALER_EXCHANGE_AgeWithdrawHandle * -TALER_EXCHANGE_age_withdraw ( +struct TALER_EXCHANGE_WithdrawHandle * +TALER_EXCHANGE_withdraw ( struct GNUNET_CURL_Context *curl_ctx, struct TALER_EXCHANGE_Keys *keys, const char *exchange_url, const struct TALER_ReservePrivateKeyP *reserve_priv, size_t num_coins, - const struct TALER_EXCHANGE_AgeWithdrawCoinInput coin_inputs[static - num_coins], + uint8_t opaque_max_age, + const struct TALER_EXCHANGE_DenomPublicKey denoms_pub[static num_coins], + const struct TALER_WithdrawMasterSeedP *seed, + TALER_EXCHANGE_WithdrawCallback callback, + void *callback_cls); + + +/** + * Submit an withdraw request for age-restricted coins + * to the exchange and get the exchange's response. + * Note that this variant of withdraw requires an additional + * protocol step, /reval-withdraw, to proof that the age-restriction + * is correctly set by the client. + * + * This API is typically used by a wallet. Note that to ensure that + * no money is lost in case of hardware failures, the provided + * argument @a rd should be committed to persistent storage + * prior to calling this function. + * + * @param curl_ctx The curl context + * @param exchange_url The base url of the exchange + * @param keys The denomination keys from the exchange + * @param reserve_priv The private key to the reserve + * @param max_age maximum age to commit to + * @param num_coins Number of coins to withdraw in a batch + * @param denoms_pub Array of @e num_coins of denominations of the coins to withdraw, MUST support age restriction + * @param seeds tuple of TALER_CNC_KAPPA seeds from which all @e num_coins coin candidates are derived from + * @param callback A callback for the result, maybe NULL + * @param callback_cls A closure for @e res_cb, maybe NULL + * @return a handle for this request; NULL if the argument was invalid. + * In this case, the callback will not be called. + */ +struct TALER_EXCHANGE_WithdrawHandle * +TALER_EXCHANGE_withdraw_with_age_proof ( + struct GNUNET_CURL_Context *curl_ctx, + struct TALER_EXCHANGE_Keys *keys, + const char *exchange_url, + const struct TALER_ReservePrivateKeyP *reserve_priv, uint8_t max_age, - TALER_EXCHANGE_AgeWithdrawCallback res_cb, - void *res_cb_cls); + size_t num_coins, + const struct TALER_EXCHANGE_DenomPublicKey denoms_pub[static num_coins], + const struct TALER_KappaWithdrawMasterSeedP *seeds, + TALER_EXCHANGE_WithdrawCallback callback, + void *callback_cls); + /** - * Cancel a age-withdraw request. This function cannot be used + * Cancel a withdraw request. This function cannot be used * on a request handle if a response is already served for it. * - * @param awh the age-withdraw handle + * @param awh the withdraw handle */ void -TALER_EXCHANGE_age_withdraw_cancel ( - struct TALER_EXCHANGE_AgeWithdrawHandle *awh); +TALER_EXCHANGE_withdraw_cancel ( + struct TALER_EXCHANGE_WithdrawHandle *awh); -/**++++++ age-withdraw with pre-blinded planchets ***************************/ +/**++++++ withdraw with pre-blinded planchets ***************************/ + /** - * @brief Information needed to withdraw (and reveal) age restricted coins. + * @brief A handle to a /withdraw request with pre-blinded coins */ -struct TALER_EXCHANGE_AgeWithdrawBlindedInput -{ - /** - * The denomination of the coin. Must support age restriction, i.e - * its .keys.age_mask MUST not be 0 - */ - const struct TALER_EXCHANGE_DenomPublicKey *denom_pub; - - /** - * Blinded Planchets - */ - struct TALER_PlanchetDetail planchet_details[TALER_CNC_KAPPA]; -}; +struct TALER_EXCHANGE_WithdrawBlindedHandle; /** - * Response from an age-withdraw request with pre-blinded planchets + * @brief Details about the response for a withdraw request */ -struct TALER_EXCHANGE_AgeWithdrawBlindedResponse +struct TALER_EXCHANGE_WithdrawBlindedResponse { /** - * HTTP response data + * HTTP response data. */ struct TALER_EXCHANGE_HttpResponse hr; /** - * Response details depending on the HTTP status. + * Details about the response */ union { + /** - * Details if HTTP status is #MHD_HTTP_OK. + * Details if the status is #MHD_HTTP_OK */ struct { /** - * Index that should not be revealed during the age-withdraw reveal phase. - * The struct TALER_PlanchetMasterSecretP * from the request - * with this index are the ones to keep. + * Number of signatures returned. */ - uint8_t noreveal_index; + unsigned int num_sigs; /** - * The commitment of the call to age-withdraw, needed for the subsequent - * call to /age-withdraw/$ACH/reveal. + * Array of @e num_coins blinded denomination signatures, giving each + * coin its value and validity. The array give these coins in the same + * order (and should have the same length) in which the original + * withdraw request specified the respective denomination keys. */ - struct TALER_AgeWithdrawCommitmentHashP h_commitment; + const struct TALER_BlindedDenominationSignature *blinded_denom_sigs; /** - * Key used by the exchange to sign the response. + * The commitment of the withdraw request, needed for the later calls to /recoup */ - struct TALER_ExchangePublicKeyP exchange_pub; + struct TALER_WithdrawCommitmentHashP h_commitment; } ok; + /** + * Details if the status is MHD_HTTP_CREATED, i.e. in case of + * age-restriction. The response is input to prepare the required + * follow-up call to /reveal-withdraw. + */ + struct TALER_EXCHANGE_WithdrawCreated created; /** * Details if the status is #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS. @@ -3325,40 +3035,70 @@ struct TALER_EXCHANGE_AgeWithdrawBlindedResponse struct TALER_EXCHANGE_KycNeededRedirect unavailable_for_legal_reasons; } details; +}; +/** + * @brief Information needed to withdraw coins. + */ +struct TALER_EXCHANGE_WithdrawBlindedCoinInput +{ + /** + * The denomination of the coin. + */ + const struct TALER_EXCHANGE_DenomPublicKey *denom_pub; + + /** + * Blinded planchet for a single coin. + */ + struct TALER_PlanchetDetail planchet_details; }; +/** + * @brief Information needed to withdraw (and potentially reveal age-restriced) coins. + */ +struct TALER_EXCHANGE_WithdrawBlindedAgeRestrictedCoinInput +{ + /** + * The denomination of the coin. MUST support age restriction. + */ + const struct TALER_EXCHANGE_DenomPublicKey *denom_pub; + + /** + * Tuple of length kappa of planchet candidates for a single coin. + */ + struct TALER_PlanchetDetail planchet_details[TALER_CNC_KAPPA]; +}; /** * Callbacks of this type are used to serve the result of submitting an - * age-withdraw request to a exchange with pre-blinded planchets + * withdraw request to a exchange with pre-blinded planchets * without the (un)blinding factor. * * @param cls closure * @param awbr response data */ typedef void -(*TALER_EXCHANGE_AgeWithdrawBlindedCallback) ( +(*TALER_EXCHANGE_WithdrawBlindedCallback) ( void *cls, - const struct TALER_EXCHANGE_AgeWithdrawBlindedResponse *awbr); + const struct TALER_EXCHANGE_WithdrawBlindedResponse *awbr); /** - * @brief A /reserves/$RESERVE_PUB/age-withdraw Handle, 2nd variant with - * pre-blinded planchets. + * @brief A /withdraw Handle, 2nd variant with pre-blinded planchets. * * This variant does not do the blinding/unblinding and only * fetches the blind signatures on the already blinded planchets. - * Used internally by the `struct TALER_EXCHANGE_BatchWithdrawHandle` + * Used internally by the `struct TALER_EXCHANGE_WithdrawHandle` * implementation as well as for the reward logic of merchants. */ -struct TALER_EXCHANGE_AgeWithdrawBlindedHandle; +struct TALER_EXCHANGE_WithdrawBlindedHandle; /** - * Withdraw age-restricted coins from the exchange using a - * /reserves/$RESERVE_PUB/age-withdraw request. This API is typically used - * by a merchant to withdraw a reward where the planchets are pre-blinded and - * the blinding factor is unknown to the merchant. + * Withdraw coins from the exchange via a /withdraw request. + * The coins are not age restricted. + * This API is typically used on behalf of a wallet + * by an intermediary who receives pre-blinded planchets + * and the blinding factor is unknown to the intermediary. * * Note that to ensure that no money is lost in case of hardware * failures, the caller must have committed (most of) the arguments to @@ -3368,7 +3108,6 @@ struct TALER_EXCHANGE_AgeWithdrawBlindedHandle; * @param curl_ctx The curl context to use * @param exchange_url The base-URL of the exchange * @param keys The /keys material from the exchange - * @param max_age The maximum age that the coins are committed to. * @param num_input number of entries in the @a blinded_input array * @param blinded_input array of planchet details of the planchet to withdraw * @param reserve_priv private key of the reserve to withdraw from @@ -3378,42 +3117,79 @@ struct TALER_EXCHANGE_AgeWithdrawBlindedHandle; * if the inputs are invalid (i.e. denomination key not with this exchange). * In this case, the callback is not called. */ -struct TALER_EXCHANGE_AgeWithdrawBlindedHandle * -TALER_EXCHANGE_age_withdraw_blinded ( +struct TALER_EXCHANGE_WithdrawBlindedHandle * +TALER_EXCHANGE_withdraw_blinded ( + struct GNUNET_CURL_Context *curl_ctx, + struct TALER_EXCHANGE_Keys *keys, + const char *exchange_url, + const struct TALER_ReservePrivateKeyP *reserve_priv, + size_t num_input, + const struct TALER_EXCHANGE_WithdrawBlindedCoinInput + blinded_input[static num_input], + TALER_EXCHANGE_WithdrawBlindedCallback res_cb, + void *res_cb_cls); + +/** + * Withdraw age-restricted coins from the exchange via a /withdraw request, + * similar to @a TALER_EXCHANGE_withdraw_blinded. + * In comparison to the unrestricted variant, this function requires a maximum + * age parameter and the @a num_coin coin inputs contain + * TALER_CNC_KAPPA candidates of blinded planchets. + * + * Note: Withdrawing age-restricted coins with age-proof requires an additional + * step, /reveal-withdraw, for which the wallet will need to provide + * to all but one index all of the coin secrets to the intermediary. + * + * @param curl_ctx The curl context to use + * @param exchange_url The base-URL of the exchange + * @param keys The /keys material from the exchange + * @param max_age the maximum age to commit to + * @param num_coins number of entries in the @a blinded_input array + * @param blinded_input array of planchet details of the planchet to withdraw + * @param reserve_priv private key of the reserve to withdraw from + * @param res_cb the callback to call when the final result for this request is available + * @param res_cb_cls closure for @a res_cb + * @return NULL + * if the inputs are invalid (i.e. denomination keys not with this exchange or + * they do not all support age-restriction). + * In this case, the callback is not called. + */ +struct TALER_EXCHANGE_WithdrawBlindedHandle * +TALER_EXCHANGE_withdraw_blinded_with_age_proof ( struct GNUNET_CURL_Context *curl_ctx, struct TALER_EXCHANGE_Keys *keys, const char *exchange_url, const struct TALER_ReservePrivateKeyP *reserve_priv, uint8_t max_age, - unsigned int num_input, - const struct TALER_EXCHANGE_AgeWithdrawBlindedInput blinded_input[static - num_input], - TALER_EXCHANGE_AgeWithdrawBlindedCallback res_cb, + unsigned int num_coins, + const struct TALER_EXCHANGE_WithdrawBlindedAgeRestrictedCoinInput + blinded_input[static num_coins], + TALER_EXCHANGE_WithdrawBlindedCallback res_cb, void *res_cb_cls); /** - * Cancel an age-withdraw request. This function cannot be used + * Cancel an withdraw request. This function cannot be used * on a request handle if a response is already served for it. * - * @param awbh the age-withdraw handle + * @param awbh the withdraw handle */ void -TALER_EXCHANGE_age_withdraw_blinded_cancel ( - struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh); +TALER_EXCHANGE_withdraw_blinded_cancel ( + struct TALER_EXCHANGE_WithdrawBlindedHandle *awbh); -/* ********************* /age-withdraw/$ACH/reveal ************************ */ +/* ********************* /reveal-withdraw ************************ */ /** - * @brief A handle to a /age-withdraw/$ACH/reveal request + * @brief A handle to a /reveal-withdraw request */ -struct TALER_EXCHANGE_AgeWithdrawRevealHandle; +struct TALER_EXCHANGE_RevealWithdrawHandle; /** - * The response from a /age-withdraw/$ACH/reveal request + * The response from a /reveal-withdraw request */ -struct TALER_EXCHANGE_AgeWithdrawRevealResponse +struct TALER_EXCHANGE_RevealWithdrawResponse { /** * HTTP response data. @@ -3439,7 +3215,7 @@ struct TALER_EXCHANGE_AgeWithdrawRevealResponse * Array of @e num_coins blinded denomination signatures, giving each * coin its value and validity. The array give these coins in the same * order (and should have the same length) in which the original - * age-withdraw request specified the respective denomination keys. + * withdraw request specified the respective denomination keys. */ const struct TALER_BlindedDenominationSignature *blinded_denom_sigs; @@ -3455,13 +3231,15 @@ struct TALER_EXCHANGE_AgeWithdrawRevealResponse }; typedef void -(*TALER_EXCHANGE_AgeWithdrawRevealCallback)( +(*TALER_EXCHANGE_RevealWithdrawCallback)( void *cls, - const struct TALER_EXCHANGE_AgeWithdrawRevealResponse *awr); + const struct TALER_EXCHANGE_RevealWithdrawResponse *awr); + /** - * Submit an age-withdraw-reveal request to the exchange and get the exchange's - * response. + * Submit an reveal-withdraw request to the exchange and get the exchange's + * response. This is required as the second step to a withdraw of age-restricted + * coins with proof that the correct age-restriction has been applied. * * This API is typically used by a wallet. Note that to ensure that * no money is lost in case of hardware failures, the provided @@ -3470,38 +3248,33 @@ typedef void * * @param curl_ctx The curl context * @param exchange_url The base url of the exchange - * @param num_coins The number of elements in @e coin_inputs and @e alg_values - * @param coin_inputs The input for the coins to withdraw, same as in the previous call to /age-withdraw - * @param noreveal_index The index into each of the kappa coin candidates, that should not be revealed to the exchange - * @param h_commitment The commmitment from the previous call to /age-withdraw - * @param reserve_pub The public key of the reserve the original call to /age-withdraw was made to + * @param num_coins Number of coin signatures to expect from the reveal + * @param h_commitment The commitment from the previous call to withdraw + * @param seeds TALER_CNC_KAPPA-1 tuple of seeds to reveal * @param res_cb A callback for the result, maybe NULL * @param res_cb_cls A closure for @e res_cb, maybe NULL * @return a handle for this request; NULL if the argument was invalid. * In this case, the callback will not be called. */ -struct TALER_EXCHANGE_AgeWithdrawRevealHandle * -TALER_EXCHANGE_age_withdraw_reveal ( +struct TALER_EXCHANGE_RevealWithdrawHandle * +TALER_EXCHANGE_reveal_withdraw ( struct GNUNET_CURL_Context *curl_ctx, const char *exchange_url, size_t num_coins, - const struct TALER_EXCHANGE_AgeWithdrawCoinInput coin_inputs[static - num_coins], - uint8_t noreveal_index, - const struct TALER_AgeWithdrawCommitmentHashP *h_commitment, - const struct TALER_ReservePublicKeyP *reserve_pub, - TALER_EXCHANGE_AgeWithdrawRevealCallback res_cb, + const struct TALER_WithdrawCommitmentHashP *h_commitment, + const struct TALER_RevealWithdrawMasterSeedsP *seeds, + TALER_EXCHANGE_RevealWithdrawCallback res_cb, void *res_cb_cls); /** - * @brief Cancel an age-withdraw-reveal request + * @brief Cancel an reveal-withdraw request * - * @param awrh Handle to an age-withdraw-reqveal request + * @param awrh Handle to an withdraw-reqveal request */ void -TALER_EXCHANGE_age_withdraw_reveal_cancel ( - struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh); +TALER_EXCHANGE_reveal_withdraw_cancel ( + struct TALER_EXCHANGE_RevealWithdrawHandle *awrh); /* ********************* /refresh/melt+reveal ***************************** */ @@ -4312,6 +4085,7 @@ typedef void * @param denom_sig signature over the coin by the exchange using @a pk * @param exchange_vals contribution from the exchange on the withdraw * @param ps secret internals of the original planchet + * @param h_commitment hash of the commitment of the corresponding original withdraw request * @param recoup_cb the callback to call when the final result for this request is available * @param recoup_cb_cls closure for @a recoup_cb * @return NULL @@ -4327,6 +4101,7 @@ TALER_EXCHANGE_recoup ( const struct TALER_DenominationSignature *denom_sig, const struct TALER_ExchangeWithdrawValues *exchange_vals, const struct TALER_PlanchetMasterSecretP *ps, + const struct TALER_WithdrawCommitmentHashP *h_commitment, TALER_EXCHANGE_RecoupResultCallback recoup_cb, void *recoup_cb_cls); diff --git a/src/include/taler_exchangedb_plugin.h b/src/include/taler_exchangedb_plugin.h @@ -324,7 +324,7 @@ enum TALER_EXCHANGEDB_ReplicatedTable /* From exchange-0003.sql: */ TALER_EXCHANGEDB_RT_AML_STAFF, TALER_EXCHANGEDB_RT_PURSE_DELETION, - TALER_EXCHANGEDB_RT_AGE_WITHDRAW, + TALER_EXCHANGEDB_RT_WITHDRAW, /* From exchange-0005.sql: */ TALER_EXCHANGEDB_RT_LEGITIMIZATION_MEASURES, TALER_EXCHANGEDB_RT_LEGITIMIZATION_OUTCOMES, @@ -665,7 +665,7 @@ struct TALER_EXCHANGEDB_TableData union GNUNET_CRYPTO_BlindingSecretP coin_blind; struct TALER_Amount amount; struct GNUNET_TIME_Timestamp timestamp; - uint64_t reserve_out_serial_id; + uint64_t withdraw_serial_id; } recoup; struct @@ -838,8 +838,10 @@ struct TALER_EXCHANGEDB_TableData struct { - struct TALER_AgeWithdrawCommitmentHashP h_commitment; + struct TALER_WithdrawCommitmentHashP h_commitment; struct TALER_Amount amount_with_fee; + struct GNUNET_TIME_Timestamp execution_date; + bool age_restricted; uint16_t max_age; uint32_t noreveal_index; struct TALER_ReservePublicKeyP reserve_pub; @@ -848,7 +850,7 @@ struct TALER_EXCHANGEDB_TableData uint64_t *denominations_serials; void *h_blind_evs; struct TALER_BlindedDenominationSignature denom_sigs; - } age_withdraw; + } withdraw; } details; @@ -1179,8 +1181,11 @@ typedef void /** * @brief Information we keep for a withdrawn coin to reproduce - * the /withdraw operation if needed, and to have proof + * the /batch-withdraw operation if needed, and to have proof * that a reserve was drained by this amount. + * + * @note This structure will be removed at some point after v24 of the protocol + * FIXME: to be deleted. */ struct TALER_EXCHANGEDB_CollectableBlindcoin { @@ -1235,13 +1240,12 @@ struct TALER_EXCHANGEDB_CollectableBlindcoin struct TALER_ReserveSignatureP reserve_sig; }; - /** - * @brief Information we keep for an age-withdraw request - * to reproduce the /age-withdraw operation if needed, and to have proof + * @brief Information we keep for a withdraw request + * to reproduce the /withdraw operation if needed, and to have proof * that a reserve was drained by this amount. */ -struct TALER_EXCHANGEDB_AgeWithdraw +struct TALER_EXCHANGEDB_Withdraw { /** * Total amount (with fee) committed to withdraw @@ -1249,30 +1253,48 @@ struct TALER_EXCHANGEDB_AgeWithdraw struct TALER_Amount amount_with_fee; /** - * Maximum age (in years) that the coins are restricted to. + * true, if age restriction was set for this withdraw. + * In this case, @e max_age, @e h_commitment and + * @e noreveal_index are to be taken into account */ - uint16_t max_age; + bool age_restricted; /** - * The hash of the commitment of all n*kappa coins + * Maximum age (in years) that the coins are restricted to, + * if ``age_restricted`` is true. */ - struct TALER_AgeWithdrawCommitmentHashP h_commitment; + uint16_t max_age; /** - * Index (smaller #TALER_CNC_KAPPA) which the exchange has chosen to not have - * revealed during cut and choose. This value applies to all n coins in the - * commitment. + * If ``age_restricted`` is true, index (smaller #TALER_CNC_KAPPA) + * which the exchange has chosen to keep unrevealed + * during the next cut and choose (aka /reveal-age) step. + * This value applies to all n coins in the commitment. */ uint16_t noreveal_index; /** + * The hash over the struct TALER_WithdrawCommitmentP for + * this withdraw request. + */ + struct TALER_WithdrawCommitmentHashP h_commitment; + + /** + * If @e age_restricted is true, the running hash over all blinded coin + * envelope's TALER_BlindedCoinHashP values. + * It runs over ``kappa*num_coins``, starting with the hashes for the coins + * for kappa index=0, then index=1 etc., + * i.e. h[0][0]...h[0][n-1]h[1][0]...h[1][n-1]...h[κ-1][0]...h[κ-1][n-1] + */ + struct TALER_HashBlindedPlanchetsP h_planchets; + + /** * Public key of the reserve that was drained. */ struct TALER_ReservePublicKeyP reserve_pub; /** - * Signature confirming the age withdrawal commitment, matching @e - * reserve_pub, @e max_age and @e h_commitment and @e amount_with_fee. + * Signature confirming the withdrawal commitment */ struct TALER_ReserveSignatureP reserve_sig; @@ -1282,9 +1304,10 @@ struct TALER_EXCHANGEDB_AgeWithdraw size_t num_coins; /** - * Array of @a num_coins blinded coins. These are the chosen coins - * (according to @a noreveal_index) from the request, which contained - * kappa*num_coins blinded coins. + * Array of @a num_coins blinded coins envelopes. + * In case of @e age_restricted = true, these are the chosen coins + * (according to @e noreveal_index) from the request, which contained + * kappa*num_coins blinded coins envelopes. */ struct TALER_BlindedCoinHashP *h_coin_evs; @@ -1297,13 +1320,14 @@ struct TALER_EXCHANGEDB_AgeWithdraw /** * Array of @a num_coins serial id's of the denominations, corresponding to * the coins in @a h_coin_evs. + * If @e age_restricted is true, the denominations MUST support age restriction. */ uint64_t *denom_serials; /** * [out]-Array of @a num_coins hashes of the public keys of the denominations * identified by @e denom_serials. This field is set when calling - * get_age_withdraw + * get_withdraw */ struct TALER_DenominationHashP *denom_pub_hashes; }; @@ -1632,9 +1656,9 @@ enum TALER_EXCHANGEDB_ReserveOperation TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE = 0, /** - * A Coin was withdrawn from the reserve using /withdraw. + * A batch of coins was withdrawn from the reserve using /withdraw. */ - TALER_EXCHANGEDB_RO_WITHDRAW_COIN = 1, + TALER_EXCHANGEDB_RO_WITHDRAW_COINS = 1, /** * A coin was returned to the reserve using /recoup. @@ -1666,7 +1690,13 @@ enum TALER_EXCHANGEDB_ReserveOperation /** * Event where a wallet requested a reserve to be closed. */ - TALER_EXCHANGEDB_RO_CLOSE_REQUEST = 7 + TALER_EXCHANGEDB_RO_CLOSE_REQUEST = 7, + + /** + * A Coin was withdrawn from the reserve using pre26 version /batch-withdraw. + */ + TALER_EXCHANGEDB_RO_BATCH_WITHDRAW_COIN = 8, + }; @@ -1702,9 +1732,14 @@ struct TALER_EXCHANGEDB_ReserveHistory struct TALER_EXCHANGEDB_BankTransfer *bank; /** + * Details about a pre26 /batch-withdraw operation. + */ + struct TALER_EXCHANGEDB_CollectableBlindcoin *batch_withdraw; + + /** * Details about a /withdraw operation. */ - struct TALER_EXCHANGEDB_CollectableBlindcoin *withdraw; + struct TALER_EXCHANGEDB_Withdraw *withdraw; /** * Details about a /recoup operation. @@ -3261,7 +3296,8 @@ typedef void * * @param cls closure * @param rowid unique serial ID for the refresh session in our DB - * @param h_blind_ev blinded hash of the coin's public key + * @param num_evs number of elements in @e h_blind_evs + * @param h_blind_evs Array @e num_evs of blinded hashes of the coin's public keys * @param denom_pub public denomination key of the deposited coin * @param reserve_pub public key of the reserve * @param reserve_sig signature over the withdraw operation @@ -3273,7 +3309,8 @@ typedef enum GNUNET_GenericReturnValue (*TALER_EXCHANGEDB_WithdrawCallback)( void *cls, uint64_t rowid, - const struct TALER_BlindedCoinHashP *h_blind_ev, + size_t num_evs, + const struct TALER_BlindedCoinHashP *h_blind_evs, const struct TALER_DenominationPublicKey *denom_pub, const struct TALER_ReservePublicKeyP *reserve_pub, const struct TALER_ReserveSignatureP *reserve_sig, @@ -4118,7 +4155,7 @@ struct TALER_EXCHANGEDB_Plugin /** - * Locate the response for a withdraw request under a hash that uniquely + * Locate the response for a batch withdraw request under a hash that uniquely * identifies the withdraw operation. Used to ensure idempotency of the * request. * @@ -4203,25 +4240,23 @@ struct TALER_EXCHANGEDB_Plugin bool *nonce_reuse); /** - * Locate the response for a age-withdraw request under a hash of the - * commitment and reserve_pub that uniquely identifies the age-withdraw - * operation. Used to ensure idempotency of the request. + * Locate the response for a withdraw request under a hash of the + * commitment that uniquely identifies the withdraw operation. + * Used to ensure idempotency of the request. * * @param cls the @e cls of this struct with the plugin-specific state - * @param reserve_pub public key of the reserve for which the age-withdraw request is made - * @param ach hash that uniquely identifies the age-withdraw operation - * @param[out] aw corresponding details of the previous age-withdraw request if an entry was found + * @param wch hash that uniquely identifies the withdraw operation + * @param[out] wr corresponding details of the previous withdraw request if an entry was found * @return statement execution status */ enum GNUNET_DB_QueryStatus - (*get_age_withdraw)( + (*get_withdraw)( void *cls, - const struct TALER_ReservePublicKeyP *reserve_pub, - const struct TALER_AgeWithdrawCommitmentHashP *ach, - struct TALER_EXCHANGEDB_AgeWithdraw *aw); + const struct TALER_WithdrawCommitmentHashP *ch, + struct TALER_EXCHANGEDB_Withdraw *wr); /** - * Perform an age-withdraw operation, checking for sufficient balance and + * Perform an withdraw operation, checking for sufficient balance and * fulfillment of age requirements and possibly persisting the withdrawal * details. * @@ -4236,9 +4271,9 @@ struct TALER_EXCHANGEDB_Plugin * @return query execution status */ enum GNUNET_DB_QueryStatus - (*do_age_withdraw)( + (*do_withdraw)( void *cls, - const struct TALER_EXCHANGEDB_AgeWithdraw *commitment, + const struct TALER_EXCHANGEDB_Withdraw *commitment, struct GNUNET_TIME_Timestamp now, bool *found, bool *balance_ok, @@ -4413,7 +4448,7 @@ struct TALER_EXCHANGEDB_Plugin * * @param cls the `struct PostgresClosure` with the plugin-specific state * @param reserve_pub public key of the reserve to credit - * @param reserve_out_serial_id row in the reserves_out table justifying the recoup + * @param withdraw_serial_id row in the withdraw table justifying the recoup * @param coin_bks coin blinding key secret to persist * @param coin_pub public key of the coin being recouped * @param known_coin_id row of the @a coin_pub in the known_coins table @@ -4427,7 +4462,7 @@ struct TALER_EXCHANGEDB_Plugin (*do_recoup)( void *cls, const struct TALER_ReservePublicKeyP *reserve_pub, - uint64_t reserve_out_serial_id, + uint64_t withdraw_serial_id, const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, const struct TALER_CoinSpendPublicKeyP *coin_pub, uint64_t known_coin_id, @@ -5824,21 +5859,21 @@ struct TALER_EXCHANGEDB_Plugin /** - * Obtain information about which reserve a coin was generated - * from given the hash of the blinded coin. + * Obtain information about which reserve was involved in a + * withdraw protocol, given the commitment. * * @param cls closure - * @param bch hash identifying the withdraw operation + * @param h_commitment hash of the commitment, identifying the withdraw operation * @param[out] reserve_pub set to information about the reserve (on success only) - * @param[out] reserve_out_serial_id set to row of the @a h_blind_ev in reserves_out + * @param[out] withdraw_serial_id set to row of the @a h_commitment in withdraw * @return transaction status code */ enum GNUNET_DB_QueryStatus - (*get_reserve_by_h_blind)( + (*get_reserve_by_h_commitment)( void *cls, - const struct TALER_BlindedCoinHashP *bch, + const struct TALER_WithdrawCommitmentHashP *h_commitment, struct TALER_ReservePublicKeyP *reserve_pub, - uint64_t *reserve_out_serial_id); + uint64_t *withdraw_serial_id); /** @@ -7407,7 +7442,7 @@ struct TALER_EXCHANGEDB_Plugin * @return database transaction status */ enum GNUNET_DB_QueryStatus - (*insert_aml_program_failure)( + (*insert_aml_program_failure) ( void *cls, uint64_t process_row, const struct TALER_NormalizedPaytoHashP *h_payto, @@ -7744,7 +7779,7 @@ struct TALER_EXCHANGEDB_Plugin * @return database transaction status */ enum GNUNET_DB_QueryStatus - (*lookup_active_legitimization)( + (*lookup_active_legitimization) ( void *cls, uint64_t legitimization_process_serial_id, uint32_t *measure_index, @@ -7764,7 +7799,7 @@ struct TALER_EXCHANGEDB_Plugin * @return database transaction status */ enum GNUNET_DB_QueryStatus - (*insert_active_legitimization_measure)( + (*insert_active_legitimization_measure) ( void *cls, const struct TALER_AccountAccessTokenP *access_token, const json_t *jmeasures, @@ -7847,7 +7882,7 @@ struct TALER_EXCHANGEDB_Plugin * @return database transaction status */ enum GNUNET_DB_QueryStatus - (*persist_kyc_attributes)( + (*persist_kyc_attributes) ( void *cls, uint64_t process_row, const struct TALER_NormalizedPaytoHashP *h_payto, @@ -7910,7 +7945,7 @@ struct TALER_EXCHANGEDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*set_aml_lock)( + (*set_aml_lock) ( void *cls, const struct TALER_NormalizedPaytoHashP *h_payto, struct GNUNET_TIME_Relative lock_duration, @@ -7930,7 +7965,7 @@ struct TALER_EXCHANGEDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*clear_aml_lock)( + (*clear_aml_lock) ( void *cls, const struct TALER_NormalizedPaytoHashP *h_payto); @@ -7947,7 +7982,7 @@ struct TALER_EXCHANGEDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*select_exchange_credit_transfers)( + (*select_exchange_credit_transfers) ( void *cls, const struct TALER_Amount *threshold, uint64_t offset, @@ -7968,7 +8003,7 @@ struct TALER_EXCHANGEDB_Plugin * @return transaction status */ enum GNUNET_DB_QueryStatus - (*select_exchange_debit_transfers)( + (*select_exchange_debit_transfers) ( void *cls, const struct TALER_Amount *threshold, uint64_t offset, diff --git a/src/include/taler_json_lib.h b/src/include/taler_json_lib.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2024 Taler Systems SA + Copyright (C) 2014-2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -18,6 +18,7 @@ * @brief helper functions for JSON processing using libjansson * @author Sree Harsha Totakura <sreeharsha@totakura.in> * @author Christian Grothoff + * @author Özgür Kesim */ #ifndef TALER_JSON_LIB_H_ #define TALER_JSON_LIB_H_ @@ -179,6 +180,19 @@ TALER_JSON_pack_exchange_withdraw_values ( const char *name, const struct TALER_ExchangeWithdrawValues *ewv); +/** + * Generate packer instruction for a JSON field of type + * blinding prepare response (/blinding-prepare). + * + * @param name name of the field to add to the object + * @param bpr blinding prepare response to transmit + * @return json pack specification + */ +struct GNUNET_JSON_PackSpec +TALER_JSON_pack_blinding_prepare_response ( + const char *name, + const struct TALER_BlindingPrepareResponse *bpr); + /** * Generate packer instruction for a JSON field of type @@ -270,6 +284,40 @@ TALER_JSON_from_amount (const struct TALER_Amount *amount); /** + * Generate packer for a fixed length array (tuple) of packers. + * The packers should be build with GNUNET_JSON_PACK. + * + * @param name name of the field to add to the object + * @param packers packers to take the values from, in order. + * @return json pack specification + */ +struct GNUNET_JSON_PackSpec +TALER_JSON_pack_tuple ( + const char *name, + const struct GNUNET_JSON_PackSpec packers[]); + +/** + * Generate packer for an array of data of the same size, + * encoded in Crockford base32-encoding. + * + * @param name name of the field to add to the object + * @param num number of elements in the @a array + * @param data pointer to the list of elements + * @param size size of an individual element + * @return json pack specification + */ +struct GNUNET_JSON_PackSpec +TALER_JSON_pack_array_of_data ( + const char *name, + size_t num, + const void *data, + size_t size); + + +/****************** Specs For Parsing JSON *********************/ + + +/** * Provide specification to parse given JSON object to an amount. * The @a currency must be a valid pointer while the * parsing is done, a copy is not made. @@ -716,6 +764,18 @@ TALER_JSON_spec_token_envelope ( /** + * Generate a parser for a tuple, i.e. fixed-length array, + * of individual values, parsed with individual parsers. + * + * @param field name of the field, might be NULL + * @param specs array of specs, must end with GNUNET_JSON_spec_end + */ +struct GNUNET_JSON_Specification +TALER_JSON_spec_tuple_of ( + const char *field, + struct GNUNET_JSON_Specification specs[]); + +/** * Hash a JSON for binary signing. * * See https://tools.ietf.org/html/draft-rundgren-json-canonicalization-scheme-15 diff --git a/src/include/taler_testing_lib.h b/src/include/taler_testing_lib.h @@ -320,10 +320,10 @@ struct TALER_TESTING_Command * @return #GNUNET_OK on success */ enum GNUNET_GenericReturnValue - (*traits)(void *cls, - const void **ret, - const char *trait, - unsigned int index); + (*traits)(void *cls, + const void **ret, + const char *trait, + unsigned int index); /** * When did the execution of this command start? @@ -1114,17 +1114,15 @@ TALER_TESTING_cmd_withdraw_amount ( /** - * Create a batch withdraw command, letting the caller specify the type of - * conflict between the coins and the desired amounts as string. - * + * Create a batch withdraw command, letting the caller specify + * the desired amounts as string. Takes a variable, non-empty + * list of the denomination amounts via VARARGS. * Takes a variable, non-empty list of the denomination amounts via VARARGS, * similar to #TALER_TESTING_cmd_withdraw_amount(), just using a batch * withdraw. * * @param label command label. * @param reserve_reference command providing us with a reserve to withdraw from - * @param conflict if true, enforce a conflict (same priv key, different denom and age commiment) - * @param age if > 0, age restriction applies (same for all coins) * @param expected_response_code which HTTP response code * we expect from the exchange. * @param amount how much we withdraw for the first coin @@ -1132,48 +1130,16 @@ TALER_TESTING_cmd_withdraw_amount ( * @return the withdraw command to be executed by the interpreter. */ struct TALER_TESTING_Command -TALER_TESTING_cmd_batch_withdraw_with_conflict ( +TALER_TESTING_cmd_batch_withdraw ( const char *label, const char *reserve_reference, - bool conflict, - uint8_t age, unsigned int expected_response_code, const char *amount, ...); + /** - * Create a batch withdraw command, letting the caller specify - * the desired amounts as string. Takes a variable, non-empty - * list of the denomination amounts via VARARGS, similar to - * #TALER_TESTING_cmd_withdraw_amount(), just using a batch withdraw. - * The coins are generated without a conflict (different private keys). - * - * @param label command label. - * @param reserve_reference command providing us with a reserve to withdraw from - * @param age if > 0, age restriction applies (same for all coins) - * @param expected_response_code which HTTP response code - * we expect from the exchange. - * @param amount how much we withdraw for the first coin - * @param ... NULL-terminated list of additional amounts to withdraw (one per coin) - * @return the withdraw command to be executed by the interpreter. - */ -#define TALER_TESTING_cmd_batch_withdraw(label, \ - reserve_reference, \ - age, \ - expected_response_code, \ - amount, \ - ...) \ - TALER_TESTING_cmd_batch_withdraw_with_conflict ( \ - (label), \ - (reserve_reference), \ - false, \ - (age), \ - (expected_response_code), \ - (amount), \ - __VA_ARGS__) - -/** - * Create an age-withdraw command, letting the caller specify + * Create an withdraw command with age proof, letting the caller specify * the maximum agend and desired amounts as string. Takes a variable, * non-empty list of the denomination amounts via VARARGS, similar to * #TALER_TESTING_cmd_withdraw_amount(), just using a batch withdraw. @@ -1188,7 +1154,7 @@ TALER_TESTING_cmd_batch_withdraw_with_conflict ( * @return the withdraw command to be executed by the interpreter. */ struct TALER_TESTING_Command -TALER_TESTING_cmd_age_withdraw ( +TALER_TESTING_cmd_withdraw_with_age_proof ( const char *label, const char *reserve_reference, uint8_t max_age, @@ -1196,8 +1162,9 @@ TALER_TESTING_cmd_age_withdraw ( const char *amount, ...); + /** - * Create a "age-withdraw reveal" command. + * Create a "withdraw-reveal" command, in case of a withdraw with age proof. * * @param label command label. * @param age_withdraw_reference reference to a "age-withdraw" command. @@ -1205,7 +1172,7 @@ TALER_TESTING_cmd_age_withdraw ( * @return the command. */ struct TALER_TESTING_Command -TALER_TESTING_cmd_age_withdraw_reveal ( +TALER_TESTING_cmd_withdraw_reveal_age_proof ( const char *label, const char *age_withdraw_reference, unsigned int expected_response_code); @@ -2693,7 +2660,7 @@ TALER_TESTING_get_trait (const struct TALER_TESTING_Trait *traits, enum GNUNET_GenericReturnValue \ TALER_TESTING_get_trait_ ## name ( \ const struct TALER_TESTING_Command *cmd, \ - type **ret); \ + type * *ret); \ struct TALER_TESTING_Trait \ TALER_TESTING_make_trait_ ## name ( \ type * value); @@ -2736,11 +2703,11 @@ TALER_TESTING_get_trait (const struct TALER_TESTING_Trait *traits, TALER_TESTING_get_trait_ ## name ( \ const struct TALER_TESTING_Command *cmd, \ unsigned int index, \ - type **ret); \ + type * *ret); \ struct TALER_TESTING_Trait \ TALER_TESTING_make_trait_ ## name ( \ unsigned int index, \ - type *value); + type * value); /** @@ -2801,6 +2768,9 @@ TALER_TESTING_get_trait (const struct TALER_TESTING_Trait *traits, op (account_priv, const union TALER_AccountPrivateKeyP) \ op (account_pub, const union TALER_AccountPublicKeyP) \ op (planchet_secret, const struct TALER_PlanchetMasterSecretP) \ + op (withdraw_seed, const struct TALER_WithdrawMasterSeedP) \ + op (withdraw_commitment, const struct TALER_WithdrawCommitmentHashP) \ + op (kappa_seed, const struct TALER_KappaWithdrawMasterSeedP) \ op (refresh_secret, const struct TALER_RefreshMasterSecretP) \ op (reserve_pub, const struct TALER_ReservePublicKeyP) \ op (merchant_priv, const struct TALER_MerchantPrivateKeyP) \ @@ -2839,7 +2809,7 @@ TALER_TESTING_get_trait (const struct TALER_TESTING_Trait *traits, /** * Call #op on all indexed traits. */ -#define TALER_TESTING_INDEXED_TRAITS(op) \ +#define TALER_TESTING_INDEXED_TRAITS(op) \ op (denom_pub, const struct TALER_EXCHANGE_DenomPublicKey) \ op (denom_sig, const struct TALER_DenominationSignature) \ op (amounts, const struct TALER_Amount) \ @@ -2849,9 +2819,10 @@ TALER_TESTING_get_trait (const struct TALER_TESTING_Trait *traits, op (age_commitment, const struct TALER_AgeCommitment) \ op (age_commitment_proof, const struct TALER_AgeCommitmentProof) \ op (h_age_commitment, const struct TALER_AgeCommitmentHash) \ - op (reserve_history, const struct TALER_EXCHANGE_ReserveHistoryEntry) \ - op (coin_history, const struct TALER_EXCHANGE_CoinHistoryEntry) \ + op (coin_history, const struct TALER_EXCHANGE_CoinHistoryEntry) \ op (planchet_secrets, const struct TALER_PlanchetMasterSecretP) \ + op (withdraw_seeds, const struct TALER_WithdrawMasterSeedP) \ + op (kappa_seeds, const struct TALER_KappaWithdrawMasterSeedP) \ op (exchange_wd_value, const struct TALER_ExchangeWithdrawValues) \ op (coin_priv, const struct TALER_CoinSpendPrivateKeyP) \ op (coin_pub, const struct TALER_CoinSpendPublicKeyP) \ @@ -2862,7 +2833,8 @@ TALER_TESTING_get_trait (const struct TALER_TESTING_Trait *traits, op (refund_deadline, const struct GNUNET_TIME_Timestamp) \ op (exchange_pub, const struct TALER_ExchangePublicKeyP) \ op (exchange_sig, const struct TALER_ExchangeSignatureP) \ - op (blinding_key, const union GNUNET_CRYPTO_BlindingSecretP) \ + op (reserve_history, const struct TALER_EXCHANGE_ReserveHistoryEntry) \ + op (blinding_key, const union GNUNET_CRYPTO_BlindingSecretP) \ op (h_blinded_coin, const struct TALER_BlindedCoinHashP) TALER_TESTING_SIMPLE_TRAITS (TALER_TESTING_MAKE_DECL_SIMPLE_TRAIT) diff --git a/src/json/json_helper.c b/src/json/json_helper.c @@ -947,9 +947,9 @@ TALER_JSON_spec_blinded_denom_sig ( struct GNUNET_JSON_Specification -TALER_JSON_spec_blinded_planchet (const char *field, - struct TALER_BlindedPlanchet *blinded_planchet - ) +TALER_JSON_spec_blinded_planchet ( + const char *field, + struct TALER_BlindedPlanchet *blinded_planchet) { blinded_planchet->blinded_message = NULL; return GNUNET_JSON_spec_blinded_message (field, @@ -1734,4 +1734,75 @@ TALER_JSON_spec_kycte (const char *name, } +static enum GNUNET_GenericReturnValue +parse_tuple_of (void *cls, + json_t *root, + struct GNUNET_JSON_Specification *spec) +{ + struct GNUNET_JSON_Specification *specs = cls; + static size_t max_specs = 100; + bool found_end = false; + + enum GNUNET_GenericReturnValue ret; + + if (! json_is_array (root)) + { + return GNUNET_SYSERR; + } + + { + size_t num; + for (num = 0; num< max_specs; num++) + { + if (NULL == specs[num].parser) + { + found_end = true; + break; + } + } + GNUNET_assert (found_end); + + if (num != json_array_size (root)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + + { + json_t *j_entry; + size_t idx; + + json_array_foreach (root, idx, j_entry) { + ret = GNUNET_JSON_parse (j_entry, + &specs[idx], + NULL, + NULL); + if (GNUNET_OK != ret) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + } + + return GNUNET_OK; +} + + +struct GNUNET_JSON_Specification +TALER_JSON_spec_tuple_of ( + const char *field, + struct GNUNET_JSON_Specification specs[]) +{ + struct GNUNET_JSON_Specification ret = { + .parser = &parse_tuple_of, + .field = field, + .cls = specs + }; + + return ret; +} + + /* end of json/json_helper.c */ diff --git a/src/json/json_pack.c b/src/json/json_pack.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2021, 2022 Taler Systems SA + Copyright (C) 2021-2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -17,6 +17,7 @@ * @file json/json_pack.c * @brief helper functions for JSON object packing * @author Christian Grothoff + * @author Özgür Kesim */ #include "platform.h" #include <gnunet/gnunet_util_lib.h> @@ -310,6 +311,66 @@ TALER_JSON_pack_exchange_withdraw_values ( struct GNUNET_JSON_PackSpec +TALER_JSON_pack_blinding_prepare_response ( + const char *name, + const struct TALER_BlindingPrepareResponse *bpr) +{ + struct GNUNET_JSON_PackSpec ps = { + .field_name = name, + }; + if (NULL == bpr) + return ps; + switch (bpr->cipher) + { + case GNUNET_CRYPTO_BSA_INVALID: + break; + case GNUNET_CRYPTO_BSA_RSA: + ps.object = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("cipher", + "RSA")); + return ps; + case GNUNET_CRYPTO_BSA_CS: { + json_t *j_rpubs = json_array (); + + GNUNET_assert (NULL!=j_rpubs); + + for (size_t i = 0; i < bpr->num; i++) + { + struct GNUNET_CRYPTO_CSPublicRPairP *pair = + &bpr->details.cs[i]; + json_t *j_pubs[2]; + json_t *j_pair; + + j_pair = json_array (); + GNUNET_assert (NULL != j_pair); + + j_pubs[0] = GNUNET_JSON_from_data ( + &pair->r_pub[0], + sizeof(pair->r_pub[0])); + GNUNET_assert (NULL != j_pubs[0]); + + j_pubs[1] = GNUNET_JSON_from_data ( + &pair->r_pub[1], + sizeof(pair->r_pub[1])); + GNUNET_assert (NULL != j_pubs[1]); + + GNUNET_assert (0 == json_array_append_new (j_pair, j_pubs[0])); + GNUNET_assert (0 == json_array_append_new (j_pair, j_pubs[1])); + GNUNET_assert (0 == json_array_append_new (j_rpubs, j_pair)); + } + + ps.object = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("cipher", "CS"), + GNUNET_JSON_pack_array_steal ("r_pubs", j_rpubs)); + return ps; + } + } + GNUNET_assert (0); + return ps; +} + + +struct GNUNET_JSON_PackSpec TALER_JSON_pack_blinded_denom_sig ( const char *name, const struct TALER_BlindedDenominationSignature *sig) @@ -399,4 +460,68 @@ TALER_JSON_pack_normalized_payto ( } +struct GNUNET_JSON_PackSpec +TALER_JSON_pack_tuple ( + const char *name, + const struct GNUNET_JSON_PackSpec packers[]) +{ + static size_t max_packers = 256; + struct GNUNET_JSON_PackSpec ps = { + .field_name = name, + }; + size_t idx; + json_t *j_array = json_array (); + + GNUNET_assert (NULL!=j_array); + + for (idx = 0; idx < max_packers; idx++) + { + if (NULL == packers[idx].object) + break; + + GNUNET_assert (0 == + json_array_append_new (j_array, + packers[idx].object)); + } + + GNUNET_assert (idx != max_packers); + + ps.object = j_array; + return ps; + +} + + +struct GNUNET_JSON_PackSpec +TALER_JSON_pack_array_of_data ( + const char *name, + size_t num, + const void *data, + size_t size) +{ + const char *blob = data; + struct GNUNET_JSON_PackSpec ps = { + .field_name = name, + }; + json_t *j_array = json_array (); + + GNUNET_assert (NULL!=j_array); + GNUNET_assert (num * size > num); + + for (size_t idx = 0; idx < num; idx++) + { + GNUNET_assert (0 == + json_array_append_new ( + j_array, + GNUNET_JSON_from_data ( + blob, + size))); + blob += size; + } + + ps.object = j_array; + return ps; +} + + /* End of json/json_pack.c */ diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am @@ -22,26 +22,22 @@ libtalerexchange_la_LDFLAGS = \ -no-undefined libtalerexchange_la_SOURCES = \ exchange_api_add_aml_decision.c \ - exchange_api_age_withdraw.c \ - exchange_api_age_withdraw_reveal.c \ exchange_api_auditor_add_denomination.c \ exchange_api_batch_deposit.c \ - exchange_api_batch_withdraw.c \ - exchange_api_batch_withdraw2.c \ - exchange_api_curl_defaults.c exchange_api_curl_defaults.h \ + exchange_api_blinding_prepare.c \ exchange_api_coins_history.c \ exchange_api_common.c exchange_api_common.h \ exchange_api_contracts_get.c \ exchange_api_csr_melt.c \ - exchange_api_csr_withdraw.c \ - exchange_api_handle.c exchange_api_handle.h \ + exchange_api_curl_defaults.c exchange_api_curl_defaults.h \ exchange_api_deposits_get.c \ exchange_api_get_aml_measures.c \ exchange_api_get_kyc_statistics.c \ + exchange_api_handle.c exchange_api_handle.h \ exchange_api_kyc_check.c \ exchange_api_kyc_info.c \ - exchange_api_kyc_start.c \ exchange_api_kyc_proof.c \ + exchange_api_kyc_start.c \ exchange_api_kyc_wallet.c \ exchange_api_link.c \ exchange_api_lookup_aml_decisions.c \ @@ -51,8 +47,8 @@ libtalerexchange_la_SOURCES = \ exchange_api_management_auditor_enable.c \ exchange_api_management_drain_profits.c \ exchange_api_management_get_keys.c \ - exchange_api_management_post_keys.c \ exchange_api_management_post_extensions.c \ + exchange_api_management_post_keys.c \ exchange_api_management_revoke_denomination_key.c \ exchange_api_management_revoke_signing_key.c \ exchange_api_management_set_global_fee.c \ @@ -79,8 +75,11 @@ libtalerexchange_la_SOURCES = \ exchange_api_reserves_history.c \ exchange_api_reserves_open.c \ exchange_api_restrictions.c \ + exchange_api_reveal_withdraw.c \ exchange_api_stefan.c \ - exchange_api_transfers_get.c + exchange_api_transfers_get.c \ + exchange_api_withdraw.c + libtalerexchange_la_LIBADD = \ libtalerauditor.la \ $(top_builddir)/src/json/libtalerjson.la \ diff --git a/src/lib/exchange_api_age_withdraw.c b/src/lib/exchange_api_age_withdraw.c @@ -1,1130 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2023 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see - <http://www.gnu.org/licenses/> -*/ -/** - * @file lib/exchange_api_age_withdraw.c - * @brief Implementation of /reserves/$RESERVE_PUB/age-withdraw requests - * @author Özgür Kesim - */ - -#include "platform.h" -#include <gnunet/gnunet_common.h> -#include <jansson.h> -#include <microhttpd.h> /* just for HTTP status codes */ -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <gnunet/gnunet_curl_lib.h> -#include <sys/wait.h> -#include "taler_curl_lib.h" -#include "taler_error_codes.h" -#include "taler_json_lib.h" -#include "taler_exchange_service.h" -#include "exchange_api_common.h" -#include "exchange_api_handle.h" -#include "taler_signatures.h" -#include "exchange_api_curl_defaults.h" -#include "taler_util.h" - -/** - * A CoinCandidate is populated from a master secret - */ -struct CoinCandidate -{ - /** - * Master key material for the coin candidates. - */ - struct TALER_PlanchetMasterSecretP secret; - - /** - * The details derived form the master secrets - */ - struct TALER_EXCHANGE_AgeWithdrawCoinPrivateDetails details; - - /** - * Blinded hash of the coin - **/ - struct TALER_BlindedCoinHashP blinded_coin_h; - -}; - - -/** - * Closure for a call to /csr-withdraw, contains data that is needed to process - * the result. - */ -struct CSRClosure -{ - /** - * Points to the actual candidate in CoinData.coin_candidates, to continue - * to build its contents based on the results from /csr-withdraw - */ - struct CoinCandidate *candidate; - - /** - * The planchet to finally generate. Points to the corresponding candidate - * in CoindData.planchet_details - */ - struct TALER_PlanchetDetail *planchet; - - /** - * Handler to the originating call to /age-withdraw, needed to either - * cancel the running age-withdraw request (on failure of the current call - * to /csr-withdraw), or to eventually perform the protocol, once all - * csr-withdraw requests have successfully finished. - */ - struct TALER_EXCHANGE_AgeWithdrawHandle *age_withdraw_handle; - - /** - * Session nonce. - */ - union GNUNET_CRYPTO_BlindSessionNonce nonce; - - /** - * Denomination information, needed for CS coins for the - * step after /csr-withdraw - */ - const struct TALER_EXCHANGE_DenomPublicKey *denom_pub; - - /** - * Handler for the CS R request - */ - struct TALER_EXCHANGE_CsRWithdrawHandle *csr_withdraw_handle; -}; - -/** - * Data we keep per coin in the batch. - */ -struct CoinData -{ - /** - * The denomination of the coin. Must support age restriction, i.e - * its .keys.age_mask MUST not be 0 - */ - struct TALER_EXCHANGE_DenomPublicKey denom_pub; - - /** - * The Candidates for the coin - */ - struct CoinCandidate coin_candidates[TALER_CNC_KAPPA]; - - /** - * Details of the planchet(s). - */ - struct TALER_PlanchetDetail planchet_details[TALER_CNC_KAPPA]; - - /** - * Closure for each candidate of type CS for the preflight request to - * /csr-withdraw - */ - struct CSRClosure csr_cls[TALER_CNC_KAPPA]; -}; - -/** - * A /reserves/$RESERVE_PUB/age-withdraw request-handle for calls with - * pre-blinded planchets. Returned by TALER_EXCHANGE_age_withdraw_blinded. - */ -struct TALER_EXCHANGE_AgeWithdrawBlindedHandle -{ - - /** - * Reserve private key. - */ - const struct TALER_ReservePrivateKeyP *reserve_priv; - - /** - * Reserve public key, calculated - */ - struct TALER_ReservePublicKeyP reserve_pub; - - /** - * Signature of the reserve for the request, calculated after all - * parameters for the coins are collected. - */ - struct TALER_ReserveSignatureP reserve_sig; - - /* - * The denomination keys of the exchange - */ - struct TALER_EXCHANGE_Keys *keys; - - /** - * The age mask, extracted from the denominations. - * MUST be the same for all denominations - * - */ - struct TALER_AgeMask age_mask; - - /** - * Maximum age to commit to. - */ - uint8_t max_age; - - /** - * The commitment calculated as SHA512 hash over all blinded_coin_h - */ - struct TALER_AgeWithdrawCommitmentHashP h_commitment; - - /** - * Total amount requested (value plus withdraw fee). - */ - struct TALER_Amount amount_with_fee; - - /** - * Length of the @e blinded_input Array - */ - size_t num_input; - - /** - * The blinded planchet input for the call to /age-withdraw via - * TALER_EXCHANGE_age_withdraw_blinded - */ - const struct TALER_EXCHANGE_AgeWithdrawBlindedInput *blinded_input; - - /** - * The url for this request. - */ - char *request_url; - - /** - * Context for curl. - */ - struct GNUNET_CURL_Context *curl_ctx; - - /** - * CURL handle for the request job. - */ - struct GNUNET_CURL_Job *job; - - /** - * Post Context - */ - struct TALER_CURL_PostContext post_ctx; - - /** - * Function to call with age-withdraw response results. - */ - TALER_EXCHANGE_AgeWithdrawBlindedCallback callback; - - /** - * Closure for @e blinded_callback - */ - void *callback_cls; -}; - -/** - * A /reserves/$RESERVE_PUB/age-withdraw request-handle for calls from - * a wallet, i. e. when blinding data is available. - */ -struct TALER_EXCHANGE_AgeWithdrawHandle -{ - - /** - * Length of the @e coin_data Array - */ - size_t num_coins; - - /** - * The base-URL of the exchange. - */ - const char *exchange_url; - - /** - * Reserve private key. - */ - const struct TALER_ReservePrivateKeyP *reserve_priv; - - /** - * Reserve public key, calculated - */ - struct TALER_ReservePublicKeyP reserve_pub; - - /** - * Signature of the reserve for the request, calculated after all - * parameters for the coins are collected. - */ - struct TALER_ReserveSignatureP reserve_sig; - - /* - * The denomination keys of the exchange - */ - struct TALER_EXCHANGE_Keys *keys; - - /** - * The age mask, extracted from the denominations. - * MUST be the same for all denominations - * - */ - struct TALER_AgeMask age_mask; - - /** - * Maximum age to commit to. - */ - uint8_t max_age; - - /** - * Array of per-coin data - */ - struct CoinData *coin_data; - - /** - * Context for curl. - */ - struct GNUNET_CURL_Context *curl_ctx; - - struct - { - /** - * Number of /csr-withdraw requests still pending. - */ - unsigned int pending; - - /** - * CURL handle for the request job. - */ - struct GNUNET_CURL_Job *job; - } csr; - - - /** - * Function to call with age-withdraw response results. - */ - TALER_EXCHANGE_AgeWithdrawCallback callback; - - /** - * Closure for @e age_withdraw_cb - */ - void *callback_cls; - - /* The Handler for the actual call to the exchange */ - struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *protocol_handle; -}; - -/** - * We got a 200 OK response for the /reserves/$RESERVE_PUB/age-withdraw operation. - * Extract the noreveal_index and return it to the caller. - * - * @param awbh operation handle - * @param j_response reply from the exchange - * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors - */ -static enum GNUNET_GenericReturnValue -reserve_age_withdraw_ok ( - struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh, - const json_t *j_response) -{ - struct TALER_EXCHANGE_AgeWithdrawBlindedResponse response = { - .hr.reply = j_response, - .hr.http_status = MHD_HTTP_OK, - .details.ok.h_commitment = awbh->h_commitment - }; - struct TALER_ExchangeSignatureP exchange_sig; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_uint8 ("noreveal_index", - &response.details.ok.noreveal_index), - GNUNET_JSON_spec_fixed_auto ("exchange_sig", - &exchange_sig), - GNUNET_JSON_spec_fixed_auto ("exchange_pub", - &response.details.ok.exchange_pub), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK!= - GNUNET_JSON_parse (j_response, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - if (GNUNET_OK != - TALER_exchange_online_age_withdraw_confirmation_verify ( - &awbh->h_commitment, - response.details.ok.noreveal_index, - &response.details.ok.exchange_pub, - &exchange_sig)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - - } - - awbh->callback (awbh->callback_cls, - &response); - /* make sure the callback isn't called again */ - awbh->callback = NULL; - - return GNUNET_OK; -} - - -/** - * Function called when we're done processing the - * HTTP /reserves/$RESERVE_PUB/age-withdraw request. - * - * @param cls the `struct TALER_EXCHANGE_AgeWithdrawHandle` - * @param response_code The HTTP response code - * @param response response data - */ -static void -handle_reserve_age_withdraw_blinded_finished ( - void *cls, - long response_code, - const void *response) -{ - struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh = cls; - const json_t *j_response = response; - struct TALER_EXCHANGE_AgeWithdrawBlindedResponse awbr = { - .hr.reply = j_response, - .hr.http_status = (unsigned int) response_code - }; - - awbh->job = NULL; - switch (response_code) - { - case 0: - awbr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - break; - case MHD_HTTP_OK: - if (GNUNET_OK != - reserve_age_withdraw_ok (awbh, - j_response)) - { - GNUNET_break_op (0); - awbr.hr.http_status = 0; - awbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - GNUNET_assert (NULL == awbh->callback); - TALER_EXCHANGE_age_withdraw_blinded_cancel (awbh); - return; - case MHD_HTTP_BAD_REQUEST: - /* This should never happen, either us or the exchange is buggy - (or API version conflict); just pass JSON reply to the application */ - awbr.hr.ec = TALER_JSON_get_error_code (j_response); - awbr.hr.hint = TALER_JSON_get_error_hint (j_response); - break; - case MHD_HTTP_FORBIDDEN: - GNUNET_break_op (0); - /* Nothing really to verify, exchange says one of the signatures is - invalid; as we checked them, this should never happen, we - should pass the JSON reply to the application */ - awbr.hr.ec = TALER_JSON_get_error_code (j_response); - awbr.hr.hint = TALER_JSON_get_error_hint (j_response); - break; - case MHD_HTTP_NOT_FOUND: - /* Nothing really to verify, the exchange basically just says - that it doesn't know this reserve. Can happen if we - query before the wire transfer went through. - We should simply pass the JSON reply to the application. */ - awbr.hr.ec = TALER_JSON_get_error_code (j_response); - awbr.hr.hint = TALER_JSON_get_error_hint (j_response); - break; - case MHD_HTTP_CONFLICT: - /* The age requirements might not have been met */ - awbr.hr.ec = TALER_JSON_get_error_code (j_response); - awbr.hr.hint = TALER_JSON_get_error_hint (j_response); - break; - case MHD_HTTP_GONE: - /* could happen if denomination was revoked */ - /* Note: one might want to check /keys for revocation - signature here, alas tricky in case our /keys - is outdated => left to clients */ - awbr.hr.ec = TALER_JSON_get_error_code (j_response); - awbr.hr.hint = TALER_JSON_get_error_hint (j_response); - break; - case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: - /* only validate reply is well-formed */ - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ( - "h_payto", - &awbr.details.unavailable_for_legal_reasons.h_payto), - GNUNET_JSON_spec_uint64 ( - "requirement_row", - &awbr.details.unavailable_for_legal_reasons.requirement_row), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (j_response, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - awbr.hr.http_status = 0; - awbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - break; - } - case MHD_HTTP_INTERNAL_SERVER_ERROR: - /* Server had an internal issue; we should retry, but this API - leaves this to the application */ - awbr.hr.ec = TALER_JSON_get_error_code (j_response); - awbr.hr.hint = TALER_JSON_get_error_hint (j_response); - break; - default: - /* unexpected response code */ - GNUNET_break_op (0); - awbr.hr.ec = TALER_JSON_get_error_code (j_response); - awbr.hr.hint = TALER_JSON_get_error_hint (j_response); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Unexpected response code %u/%d for exchange age-withdraw\n", - (unsigned int) response_code, - (int) awbr.hr.ec); - break; - } - awbh->callback (awbh->callback_cls, - &awbr); - TALER_EXCHANGE_age_withdraw_blinded_cancel (awbh); -} - - -/** - * Runs the actual age-withdraw operation with the blinded planchets. - * - * @param[in,out] awbh age withdraw handler - */ -static void -perform_protocol ( - struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh) -{ -#define FAIL_IF(cond) \ - do { \ - if ((cond)) \ - { \ - GNUNET_break (! (cond)); \ - goto ERROR; \ - } \ - } while (0) - - struct GNUNET_HashContext *coins_hctx = NULL; - json_t *j_denoms = NULL; - json_t *j_array_candidates = NULL; - json_t *j_request_body = NULL; - CURL *curlh = NULL; - - GNUNET_assert (0 < awbh->num_input); - awbh->age_mask = awbh->blinded_input[0].denom_pub->key.age_mask; - - FAIL_IF (GNUNET_OK != - TALER_amount_set_zero (awbh->keys->currency, - &awbh->amount_with_fee)); - /* Accumulate total value with fees */ - for (size_t i = 0; i < awbh->num_input; i++) - { - struct TALER_Amount coin_total; - const struct TALER_EXCHANGE_DenomPublicKey *dpub = - awbh->blinded_input[i].denom_pub; - - FAIL_IF (0 > - TALER_amount_add (&coin_total, - &dpub->fees.withdraw, - &dpub->value)); - FAIL_IF (0 > - TALER_amount_add (&awbh->amount_with_fee, - &awbh->amount_with_fee, - &coin_total)); - } - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Attempting to age-withdraw from reserve %s with maximum age %d\n", - TALER_B2S (&awbh->reserve_pub), - awbh->max_age); - - coins_hctx = GNUNET_CRYPTO_hash_context_start (); - FAIL_IF (NULL == coins_hctx); - - - j_denoms = json_array (); - j_array_candidates = json_array (); - FAIL_IF ((NULL == j_denoms) || - (NULL == j_array_candidates)); - - for (size_t i = 0; i< awbh->num_input; i++) - { - /* Build the denomination array */ - { - const struct TALER_EXCHANGE_DenomPublicKey *denom_pub = - awbh->blinded_input[i].denom_pub; - const struct TALER_DenominationHashP *denom_h = &denom_pub->h_key; - json_t *jdenom; - - /* The mask must be the same for all coins */ - FAIL_IF (awbh->age_mask.bits != denom_pub->key.age_mask.bits); - - jdenom = GNUNET_JSON_from_data_auto (denom_h); - FAIL_IF (NULL == jdenom); - FAIL_IF (0 > json_array_append_new (j_denoms, - jdenom)); - - /* Build the candidate array */ - { - json_t *j_can = json_array (); - FAIL_IF (NULL == j_can); - - for (size_t k = 0; k < TALER_CNC_KAPPA; k++) - { - struct TALER_BlindedCoinHashP bch; - const struct TALER_PlanchetDetail *planchet = - &awbh->blinded_input[i].planchet_details[k]; - json_t *jc = GNUNET_JSON_PACK ( - TALER_JSON_pack_blinded_planchet ( - NULL, - &planchet->blinded_planchet)); - - FAIL_IF (NULL == jc); - FAIL_IF (0 > json_array_append_new (j_can, - jc)); - - TALER_coin_ev_hash (&planchet->blinded_planchet, - &planchet->denom_pub_hash, - &bch); - - GNUNET_CRYPTO_hash_context_read (coins_hctx, - &bch, - sizeof(bch)); - } - - FAIL_IF (0 > json_array_append_new (j_array_candidates, - j_can)); - } - } - } - - /* Build the hash of the commitment */ - GNUNET_CRYPTO_hash_context_finish (coins_hctx, - &awbh->h_commitment.hash); - coins_hctx = NULL; - - /* Sign the request */ - TALER_wallet_age_withdraw_sign (&awbh->h_commitment, - &awbh->amount_with_fee, - &awbh->age_mask, - awbh->max_age, - awbh->reserve_priv, - &awbh->reserve_sig); - - /* Initiate the POST-request */ - j_request_body = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_array_steal ("denom_hs", j_denoms), - GNUNET_JSON_pack_array_steal ("blinded_planchets", j_array_candidates), - GNUNET_JSON_pack_uint64 ("max_age", awbh->max_age), - GNUNET_JSON_pack_data_auto ("reserve_sig", &awbh->reserve_sig)); - FAIL_IF (NULL == j_request_body); - - curlh = TALER_EXCHANGE_curl_easy_get_ (awbh->request_url); - FAIL_IF (NULL == curlh); - FAIL_IF (GNUNET_OK != - TALER_curl_easy_post (&awbh->post_ctx, - curlh, - j_request_body)); - json_decref (j_request_body); - j_request_body = NULL; - - awbh->job = GNUNET_CURL_job_add2 ( - awbh->curl_ctx, - curlh, - awbh->post_ctx.headers, - &handle_reserve_age_withdraw_blinded_finished, - awbh); - FAIL_IF (NULL == awbh->job); - - /* No errors, return */ - return; - -ERROR: - if (NULL != j_denoms) - json_decref (j_denoms); - if (NULL != j_array_candidates) - json_decref (j_array_candidates); - if (NULL != j_request_body) - json_decref (j_request_body); - if (NULL != curlh) - curl_easy_cleanup (curlh); - if (NULL != coins_hctx) - GNUNET_CRYPTO_hash_context_abort (coins_hctx); - TALER_EXCHANGE_age_withdraw_blinded_cancel (awbh); - return; -#undef FAIL_IF -} - - -/** - * @brief Callback to copy the results from the call to TALER_age_withdraw_blinded - * to the result for the originating call from TALER_age_withdraw. - * - * @param cls struct TALER_AgeWithdrawHandle - * @param awbr The response - */ -static void -copy_results ( - void *cls, - const struct TALER_EXCHANGE_AgeWithdrawBlindedResponse *awbr) -{ - struct TALER_EXCHANGE_AgeWithdrawHandle *awh = cls; - uint8_t k = awbr->details.ok.noreveal_index; - struct TALER_EXCHANGE_AgeWithdrawCoinPrivateDetails details[awh->num_coins]; - struct TALER_BlindedCoinHashP blinded_coin_hs[awh->num_coins]; - struct TALER_EXCHANGE_AgeWithdrawResponse resp = { - .hr = awbr->hr, - .details = { - .ok = { - .noreveal_index = awbr->details.ok.noreveal_index, - .h_commitment = awbr->details.ok.h_commitment, - .exchange_pub = awbr->details.ok.exchange_pub, - .num_coins = awh->num_coins, - .coin_details = details, - .blinded_coin_hs = blinded_coin_hs - }, - }, - }; - - awh->protocol_handle = NULL; - for (size_t n = 0; n< awh->num_coins; n++) - { - details[n] = awh->coin_data[n].coin_candidates[k].details; - details[n].planchet = awh->coin_data[n].planchet_details[k]; - blinded_coin_hs[n] = awh->coin_data[n].coin_candidates[k].blinded_coin_h; - } - awh->callback (awh->callback_cls, - &resp); - awh->callback = NULL; - TALER_EXCHANGE_age_withdraw_cancel (awh); -} - - -/** - * @brief Prepares and executes TALER_EXCHANGE_age_withdraw_blinded. - * If there were CS-denominations involved, started once the all calls - * to /csr-withdraw are done. - */ -static void -call_age_withdraw_blinded ( - struct TALER_EXCHANGE_AgeWithdrawHandle *awh) -{ - struct TALER_EXCHANGE_AgeWithdrawBlindedInput blinded_input[awh->num_coins]; - - /* Prepare the blinded planchets as input */ - for (size_t n = 0; n < awh->num_coins; n++) - { - blinded_input[n].denom_pub = &awh->coin_data[n].denom_pub; - for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) - blinded_input[n].planchet_details[k] = - awh->coin_data[n].planchet_details[k]; - } - - awh->protocol_handle = - TALER_EXCHANGE_age_withdraw_blinded ( - awh->curl_ctx, - awh->keys, - awh->exchange_url, - awh->reserve_priv, - awh->max_age, - awh->num_coins, - blinded_input, - &copy_results, - awh); -} - - -/** - * Prepares the request URL for the age-withdraw request - * - * @param awbh The handler - * @param exchange_url The base-URL to the exchange - */ -static enum GNUNET_GenericReturnValue -prepare_url ( - struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh, - const char *exchange_url) -{ - char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32]; - char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2]; - char *end; - - end = GNUNET_STRINGS_data_to_string ( - &awbh->reserve_pub, - sizeof (awbh->reserve_pub), - pub_str, - sizeof (pub_str)); - *end = '\0'; - GNUNET_snprintf (arg_str, - sizeof (arg_str), - "reserves/%s/age-withdraw", - pub_str); - - awbh->request_url = TALER_url_join (exchange_url, - arg_str, - NULL); - if (NULL == awbh->request_url) - { - GNUNET_break (0); - TALER_EXCHANGE_age_withdraw_blinded_cancel (awbh); - return GNUNET_SYSERR; - } - - return GNUNET_OK; -} - - -/** - * @brief Function called when CSR withdraw retrieval is finished - * - * @param cls the `struct CSRClosure *` - * @param csrr replies from the /csr-withdraw request - */ -static void -csr_withdraw_done ( - void *cls, - const struct TALER_EXCHANGE_CsRWithdrawResponse *csrr) -{ - struct CSRClosure *csr = cls; - struct CoinCandidate *can; - struct TALER_PlanchetDetail *planchet; - struct TALER_EXCHANGE_AgeWithdrawHandle *awh; - - GNUNET_assert (NULL != csr); - awh = csr->age_withdraw_handle; - planchet = csr->planchet; - can = csr->candidate; - - GNUNET_assert (NULL != can); - GNUNET_assert (NULL != planchet); - GNUNET_assert (NULL != awh); - - csr->csr_withdraw_handle = NULL; - - switch (csrr->hr.http_status) - { - case MHD_HTTP_OK: - { - bool success = false; - /* Complete the initialization of the coin with CS denomination */ - - TALER_denom_ewv_copy (&can->details.alg_values, - &csrr->details.ok.alg_values); - GNUNET_assert (can->details.alg_values.blinding_inputs->cipher - == GNUNET_CRYPTO_BSA_CS); - TALER_planchet_setup_coin_priv (&can->secret, - &can->details.alg_values, - &can->details.coin_priv); - TALER_planchet_blinding_secret_create (&can->secret, - &can->details.alg_values, - &can->details.blinding_key); - /* This initializes the 2nd half of the - can->planchet_detail.blinded_planchet! */ - do { - if (GNUNET_OK != - TALER_planchet_prepare ( - &csr->denom_pub->key, - &can->details.alg_values, - &can->details.blinding_key, - &csr->nonce, - &can->details.coin_priv, - &can->details.h_age_commitment, - &can->details.h_coin_pub, - planchet)) - { - GNUNET_break (0); - break; - } - - TALER_coin_ev_hash (&planchet->blinded_planchet, - &planchet->denom_pub_hash, - &can->blinded_coin_h); - success = true; - } while (0); - - awh->csr.pending--; - - /* No more pending requests to /csr-withdraw, we can now perform the - * actual age-withdraw operation */ - if (0 == awh->csr.pending && success) - call_age_withdraw_blinded (awh); - return; - } - default: - break; - } - TALER_EXCHANGE_age_withdraw_cancel (awh); -} - - -/** - * @brief Prepare the coins for the call to age-withdraw and calculates - * the total amount with fees. - * - * For denomination with CS as cipher, initiates the preflight to retrieve the - * csr-parameter via /csr-withdraw. - * - * @param awh The handler to the age-withdraw - * @param num_coins The number of coins in @e coin_inputs - * @param coin_inputs The input for the individual coin(-candidates) - * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure - */ -static enum GNUNET_GenericReturnValue -prepare_coins ( - struct TALER_EXCHANGE_AgeWithdrawHandle *awh, - size_t num_coins, - const struct TALER_EXCHANGE_AgeWithdrawCoinInput coin_inputs[ - static num_coins]) -{ -#define FAIL_IF(cond) \ - do { \ - if ((cond)) \ - { \ - GNUNET_break (! (cond)); \ - goto ERROR; \ - } \ - } while (0) - - GNUNET_assert (0 < num_coins); - awh->age_mask = coin_inputs[0].denom_pub->key.age_mask; - - awh->coin_data = GNUNET_new_array (awh->num_coins, - struct CoinData); - - for (size_t i = 0; i < num_coins; i++) - { - struct CoinData *cd = &awh->coin_data[i]; - const struct TALER_EXCHANGE_AgeWithdrawCoinInput *input = &coin_inputs[i]; - - cd->denom_pub = *input->denom_pub; - /* The mask must be the same for all coins */ - FAIL_IF (awh->age_mask.bits != input->denom_pub->key.age_mask.bits); - TALER_denom_pub_copy (&cd->denom_pub.key, - &input->denom_pub->key); - - for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) - { - struct CoinCandidate *can = &cd->coin_candidates[k]; - struct TALER_PlanchetDetail *planchet = &cd->planchet_details[k]; - - can->secret = input->secrets[k]; - /* Derive the age restriction from the given secret and - * the maximum age */ - TALER_age_restriction_from_secret ( - &can->secret, - &input->denom_pub->key.age_mask, - awh->max_age, - &can->details.age_commitment_proof); - - TALER_age_commitment_hash (&can->details.age_commitment_proof.commitment, - &can->details.h_age_commitment); - - switch (input->denom_pub->key.bsign_pub_key->cipher) - { - case GNUNET_CRYPTO_BSA_RSA: - TALER_denom_ewv_copy (&can->details.alg_values, - TALER_denom_ewv_rsa_singleton ()); - TALER_planchet_setup_coin_priv (&can->secret, - &can->details.alg_values, - &can->details.coin_priv); - TALER_planchet_blinding_secret_create ( - &can->secret, - &can->details.alg_values, - &can->details.blinding_key); - FAIL_IF (GNUNET_OK != - TALER_planchet_prepare ( - &cd->denom_pub.key, - &can->details.alg_values, - &can->details.blinding_key, - NULL, - &can->details.coin_priv, - &can->details.h_age_commitment, - &can->details.h_coin_pub, - planchet)); - TALER_coin_ev_hash (&planchet->blinded_planchet, - &planchet->denom_pub_hash, - &can->blinded_coin_h); - break; - case GNUNET_CRYPTO_BSA_CS: - { - struct CSRClosure *cls = &cd->csr_cls[k]; - /** - * Save the handler and the denomination for the callback - * after the call to csr-withdraw */ - cls->age_withdraw_handle = awh; - cls->candidate = can; - cls->planchet = planchet; - cls->denom_pub = &cd->denom_pub; - TALER_cs_withdraw_nonce_derive ( - &can->secret, - &cls->nonce.cs_nonce); - cls->csr_withdraw_handle = - TALER_EXCHANGE_csr_withdraw ( - awh->curl_ctx, - awh->exchange_url, - &cd->denom_pub, - &cls->nonce.cs_nonce, - &csr_withdraw_done, - cls); - FAIL_IF (NULL == cls->csr_withdraw_handle); - - awh->csr.pending++; - break; - } - default: - FAIL_IF (1); - } - } - } - return GNUNET_OK; - -ERROR: - TALER_EXCHANGE_age_withdraw_cancel (awh); - return GNUNET_SYSERR; -#undef FAIL_IF -} - - -struct TALER_EXCHANGE_AgeWithdrawHandle * -TALER_EXCHANGE_age_withdraw ( - struct GNUNET_CURL_Context *curl_ctx, - struct TALER_EXCHANGE_Keys *keys, - const char *exchange_url, - const struct TALER_ReservePrivateKeyP *reserve_priv, - size_t num_coins, - const struct TALER_EXCHANGE_AgeWithdrawCoinInput coin_inputs[const static - num_coins], - uint8_t max_age, - TALER_EXCHANGE_AgeWithdrawCallback res_cb, - void *res_cb_cls) -{ - struct TALER_EXCHANGE_AgeWithdrawHandle *awh; - - awh = GNUNET_new (struct TALER_EXCHANGE_AgeWithdrawHandle); - awh->exchange_url = exchange_url; - awh->keys = TALER_EXCHANGE_keys_incref (keys); - awh->curl_ctx = curl_ctx; - awh->reserve_priv = reserve_priv; - awh->callback = res_cb; - awh->callback_cls = res_cb_cls; - awh->num_coins = num_coins; - awh->max_age = max_age; - if (GNUNET_OK != - prepare_coins (awh, - num_coins, - coin_inputs)) - { - GNUNET_free (awh); - return NULL; - } - - /* If there were no CS denominations, we can now perform the actual - * age-withdraw protocol. Otherwise, there are calls to /csr-withdraw - * in flight and once they finish, the age-withdraw-protocol will be - * called from within the csr_withdraw_done-function. - */ - if (0 == awh->csr.pending) - call_age_withdraw_blinded (awh); - - return awh; -} - - -void -TALER_EXCHANGE_age_withdraw_cancel ( - struct TALER_EXCHANGE_AgeWithdrawHandle *awh) -{ - /* Cleanup coin data */ - for (unsigned int i = 0; i<awh->num_coins; i++) - { - struct CoinData *cd = &awh->coin_data[i]; - - for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) - { - struct TALER_PlanchetDetail *planchet = &cd->planchet_details[k]; - struct CSRClosure *cls = &cd->csr_cls[k]; - struct CoinCandidate *can = &cd->coin_candidates[k]; - - if (NULL != cls->csr_withdraw_handle) - { - TALER_EXCHANGE_csr_withdraw_cancel (cls->csr_withdraw_handle); - cls->csr_withdraw_handle = NULL; - } - TALER_blinded_planchet_free (&planchet->blinded_planchet); - TALER_denom_ewv_free (&can->details.alg_values); - TALER_age_commitment_proof_free (&can->details.age_commitment_proof); - } - TALER_denom_pub_free (&cd->denom_pub.key); - } - GNUNET_free (awh->coin_data); - TALER_EXCHANGE_keys_decref (awh->keys); - TALER_EXCHANGE_age_withdraw_blinded_cancel (awh->protocol_handle); - awh->protocol_handle = NULL; - GNUNET_free (awh); -} - - -struct TALER_EXCHANGE_AgeWithdrawBlindedHandle * -TALER_EXCHANGE_age_withdraw_blinded ( - struct GNUNET_CURL_Context *curl_ctx, - struct TALER_EXCHANGE_Keys *keys, - const char *exchange_url, - const struct TALER_ReservePrivateKeyP *reserve_priv, - uint8_t max_age, - unsigned int num_input, - const struct TALER_EXCHANGE_AgeWithdrawBlindedInput blinded_input[static - num_input], - TALER_EXCHANGE_AgeWithdrawBlindedCallback res_cb, - void *res_cb_cls) -{ - struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh = - GNUNET_new (struct TALER_EXCHANGE_AgeWithdrawBlindedHandle); - - awbh->num_input = num_input; - awbh->blinded_input = blinded_input; - awbh->keys = TALER_EXCHANGE_keys_incref (keys); - awbh->curl_ctx = curl_ctx; - awbh->reserve_priv = reserve_priv; - awbh->callback = res_cb; - awbh->callback_cls = res_cb_cls; - awbh->max_age = max_age; - GNUNET_CRYPTO_eddsa_key_get_public ( - &awbh->reserve_priv->eddsa_priv, - &awbh->reserve_pub.eddsa_pub); - if (GNUNET_OK != - prepare_url (awbh, - exchange_url)) - return NULL; - - perform_protocol (awbh); - return awbh; -} - - -void -TALER_EXCHANGE_age_withdraw_blinded_cancel ( - struct TALER_EXCHANGE_AgeWithdrawBlindedHandle *awbh) -{ - if (NULL == awbh) - return; - if (NULL != awbh->job) - { - GNUNET_CURL_job_cancel (awbh->job); - awbh->job = NULL; - } - GNUNET_free (awbh->request_url); - TALER_EXCHANGE_keys_decref (awbh->keys); - TALER_curl_easy_post_finished (&awbh->post_ctx); - GNUNET_free (awbh); -} - - -/* exchange_api_age_withdraw.c */ diff --git a/src/lib/exchange_api_age_withdraw_reveal.c b/src/lib/exchange_api_age_withdraw_reveal.c @@ -1,496 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2023 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see - <http://www.gnu.org/licenses/> -*/ -/** - * @file lib/exchange_api_age_withdraw_reveal.c - * @brief Implementation of /age-withdraw/$ACH/reveal requests - * @author Özgür Kesim - */ - -#include "platform.h" -#include <gnunet/gnunet_common.h> -#include <jansson.h> -#include <microhttpd.h> /* just for HTTP status codes */ -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <gnunet/gnunet_curl_lib.h> -#include "taler_curl_lib.h" -#include "taler_json_lib.h" -#include "taler_exchange_service.h" -#include "exchange_api_common.h" -#include "exchange_api_handle.h" -#include "taler_signatures.h" -#include "exchange_api_curl_defaults.h" - -/** - * Handler for a running age-withdraw-reveal request - */ -struct TALER_EXCHANGE_AgeWithdrawRevealHandle -{ - - /** - * The index not to be disclosed - */ - uint8_t noreveal_index; - - /** - * The age-withdraw commitment - */ - struct TALER_AgeWithdrawCommitmentHashP h_commitment; - - /** - * The reserve's public key - */ - const struct TALER_ReservePublicKeyP *reserve_pub; - - /** - * Number of coins - */ - size_t num_coins; - - /** - * The @e num_coins * kappa coin secrets from the age-withdraw commitment - */ - const struct TALER_EXCHANGE_AgeWithdrawCoinInput *coins_input; - - /** - * The url for the reveal request - */ - char *request_url; - - /** - * CURL handle for the request job. - */ - struct GNUNET_CURL_Job *job; - - /** - * Post Context - */ - struct TALER_CURL_PostContext post_ctx; - - /** - * Callback - */ - TALER_EXCHANGE_AgeWithdrawRevealCallback callback; - - /** - * Reveal - */ - void *callback_cls; -}; - - -/** - * We got a 200 OK response for the /age-withdraw/$ACH/reveal operation. - * Extract the signed blindedcoins and return it to the caller. - * - * @param awrh operation handle - * @param j_response reply from the exchange - * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors - */ -static enum GNUNET_GenericReturnValue -age_withdraw_reveal_ok ( - struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh, - const json_t *j_response) -{ - struct TALER_EXCHANGE_AgeWithdrawRevealResponse response = { - .hr.reply = j_response, - .hr.http_status = MHD_HTTP_OK, - }; - const json_t *j_sigs; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_array_const ("ev_sigs", - &j_sigs), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (j_response, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - if (awrh->num_coins != json_array_size (j_sigs)) - { - /* Number of coins generated does not match our expectation */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - { - struct TALER_BlindedDenominationSignature denom_sigs[awrh->num_coins]; - json_t *j_sig; - size_t n; - - /* Reconstruct the coins and unblind the signatures */ - json_array_foreach (j_sigs, n, j_sig) - { - struct GNUNET_JSON_Specification ispec[] = { - TALER_JSON_spec_blinded_denom_sig (NULL, - &denom_sigs[n]), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (j_sig, - ispec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - } - - response.details.ok.num_sigs = awrh->num_coins; - response.details.ok.blinded_denom_sigs = denom_sigs; - awrh->callback (awrh->callback_cls, - &response); - /* Make sure the callback isn't called again */ - awrh->callback = NULL; - /* Free resources */ - for (size_t i = 0; i < awrh->num_coins; i++) - TALER_blinded_denom_sig_free (&denom_sigs[i]); - } - - return GNUNET_OK; -} - - -/** - * Function called when we're done processing the - * HTTP /age-withdraw/$ACH/reveal request. - * - * @param cls the `struct TALER_EXCHANGE_AgeWithdrawRevealHandle` - * @param response_code The HTTP response code - * @param response response data - */ -static void -handle_age_withdraw_reveal_finished ( - void *cls, - long response_code, - const void *response) -{ - struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh = cls; - const json_t *j_response = response; - struct TALER_EXCHANGE_AgeWithdrawRevealResponse awr = { - .hr.reply = j_response, - .hr.http_status = (unsigned int) response_code - }; - - awrh->job = NULL; - /* FIXME[oec]: Only handle response-codes that are in the spec */ - switch (response_code) - { - case 0: - awr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - break; - case MHD_HTTP_OK: - { - enum GNUNET_GenericReturnValue ret; - - ret = age_withdraw_reveal_ok (awrh, - j_response); - if (GNUNET_OK != ret) - { - GNUNET_break_op (0); - awr.hr.http_status = 0; - awr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - GNUNET_assert (NULL == awrh->callback); - TALER_EXCHANGE_age_withdraw_reveal_cancel (awrh); - return; - } - case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: - /* only validate reply is well-formed */ - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ( - "h_payto", - &awr.details.unavailable_for_legal_reasons.h_payto), - GNUNET_JSON_spec_uint64 ( - "requirement_row", - &awr.details.unavailable_for_legal_reasons.requirement_row), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (j_response, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - awr.hr.http_status = 0; - awr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - } - case MHD_HTTP_BAD_REQUEST: - /* This should never happen, either us or the exchange is buggy - (or API version conflict); just pass JSON reply to the application */ - awr.hr.ec = TALER_JSON_get_error_code (j_response); - awr.hr.hint = TALER_JSON_get_error_hint (j_response); - break; - case MHD_HTTP_FORBIDDEN: - GNUNET_break_op (0); - /** - * This should never happen, as we don't sent any signatures - * to the exchange to verify. We should simply pass the JSON reply - * to the application - **/ - awr.hr.ec = TALER_JSON_get_error_code (j_response); - awr.hr.hint = TALER_JSON_get_error_hint (j_response); - break; - case MHD_HTTP_NOT_FOUND: - /* Nothing really to verify, the exchange basically just says - that it doesn't know this age-withdraw commitment. */ - awr.hr.ec = TALER_JSON_get_error_code (j_response); - awr.hr.hint = TALER_JSON_get_error_hint (j_response); - break; - case MHD_HTTP_CONFLICT: - /* An age commitment for one of the coins did not fulfill - * the required maximum age requirement of the corresponding - * reserve. - * Error code: TALER_EC_EXCHANGE_GENERIC_COIN_AGE_REQUIREMENT_FAILURE. - */ - awr.hr.ec = TALER_JSON_get_error_code (j_response); - awr.hr.hint = TALER_JSON_get_error_hint (j_response); - break; - case MHD_HTTP_GONE: - /* could happen if denomination was revoked */ - /* Note: one might want to check /keys for revocation - signature here, alas tricky in case our /keys - is outdated => left to clients */ - awr.hr.ec = TALER_JSON_get_error_code (j_response); - awr.hr.hint = TALER_JSON_get_error_hint (j_response); - break; - case MHD_HTTP_INTERNAL_SERVER_ERROR: - /* Server had an internal issue; we should retry, but this API - leaves this to the application */ - awr.hr.ec = TALER_JSON_get_error_code (j_response); - awr.hr.hint = TALER_JSON_get_error_hint (j_response); - break; - default: - /* unexpected response code */ - GNUNET_break_op (0); - awr.hr.ec = TALER_JSON_get_error_code (j_response); - awr.hr.hint = TALER_JSON_get_error_hint (j_response); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Unexpected response code %u/%d for exchange age-withdraw\n", - (unsigned int) response_code, - (int) awr.hr.ec); - break; - } - awrh->callback (awrh->callback_cls, - &awr); - TALER_EXCHANGE_age_withdraw_reveal_cancel (awrh); -} - - -/** - * Prepares the request URL for the age-withdraw-reveal request - * - * @param exchange_url The base-URL to the exchange - * @param[in,out] awrh The handler - * @return GNUNET_OK on success, GNUNET_SYSERR otherwise - */ -static -enum GNUNET_GenericReturnValue -prepare_url ( - const char *exchange_url, - struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh) -{ - char arg_str[sizeof (struct TALER_AgeWithdrawCommitmentHashP) * 2 + 32]; - char pub_str[sizeof (struct TALER_AgeWithdrawCommitmentHashP) * 2]; - char *end; - - end = GNUNET_STRINGS_data_to_string ( - &awrh->h_commitment, - sizeof (awrh->h_commitment), - pub_str, - sizeof (pub_str)); - *end = '\0'; - GNUNET_snprintf (arg_str, - sizeof (arg_str), - "age-withdraw/%s/reveal", - pub_str); - - awrh->request_url = TALER_url_join (exchange_url, - arg_str, - NULL); - if (NULL == awrh->request_url) - { - GNUNET_break (0); - TALER_EXCHANGE_age_withdraw_reveal_cancel (awrh); - return GNUNET_SYSERR; - } - - return GNUNET_OK; -} - - -/** - * Call /age-withdraw/$ACH/reveal - * - * @param curl_ctx The context for CURL - * @param awrh The handler - */ -static void -perform_protocol ( - struct GNUNET_CURL_Context *curl_ctx, - struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh) -{ - CURL *curlh = NULL; - json_t *j_request_body = NULL; - json_t *j_array_of_secrets = NULL; - json_t *j_secrets = NULL; - json_t *j_sec = NULL; - -#define FAIL_IF(cond) \ - do { \ - if ((cond)) \ - { \ - GNUNET_break (! (cond)); \ - goto ERROR; \ - } \ - } while (0) - - j_array_of_secrets = json_array (); - FAIL_IF (NULL == j_array_of_secrets); - - for (size_t n = 0; n < awrh->num_coins; n++) - { - const struct TALER_PlanchetMasterSecretP *secrets - = awrh->coins_input[n].secrets; - - j_secrets = json_array (); - FAIL_IF (NULL == j_secrets); - - for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) - { - const struct TALER_PlanchetMasterSecretP *secret = &secrets[k]; - if (awrh->noreveal_index == k) - continue; - - j_sec = GNUNET_JSON_from_data_auto (secret); - FAIL_IF (NULL == j_sec); - FAIL_IF (0 < json_array_append_new (j_secrets, - j_sec)); - } - - FAIL_IF (0 < json_array_append_new (j_array_of_secrets, - j_secrets)); - } - j_request_body = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("reserve_pub", - awrh->reserve_pub), - GNUNET_JSON_pack_array_steal ("disclosed_coin_secrets", - j_array_of_secrets)); - FAIL_IF (NULL == j_request_body); - - curlh = TALER_EXCHANGE_curl_easy_get_ (awrh->request_url); - FAIL_IF (NULL == curlh); - FAIL_IF (GNUNET_OK != - TALER_curl_easy_post (&awrh->post_ctx, - curlh, - j_request_body)); - json_decref (j_request_body); - j_request_body = NULL; - - awrh->job = GNUNET_CURL_job_add2 ( - curl_ctx, - curlh, - awrh->post_ctx.headers, - &handle_age_withdraw_reveal_finished, - awrh); - FAIL_IF (NULL == awrh->job); - - /* No error, return */ - return; - -ERROR: - if (NULL != j_sec) - json_decref (j_sec); - if (NULL != j_secrets) - json_decref (j_secrets); - if (NULL != j_array_of_secrets) - json_decref (j_array_of_secrets); - if (NULL != j_request_body) - json_decref (j_request_body); - if (NULL != curlh) - curl_easy_cleanup (curlh); - TALER_EXCHANGE_age_withdraw_reveal_cancel (awrh); - return; -#undef FAIL_IF -} - - -struct TALER_EXCHANGE_AgeWithdrawRevealHandle * -TALER_EXCHANGE_age_withdraw_reveal ( - struct GNUNET_CURL_Context *curl_ctx, - const char *exchange_url, - size_t num_coins, - const struct TALER_EXCHANGE_AgeWithdrawCoinInput coins_input[static - num_coins], - uint8_t noreveal_index, - const struct TALER_AgeWithdrawCommitmentHashP *h_commitment, - const struct TALER_ReservePublicKeyP *reserve_pub, - TALER_EXCHANGE_AgeWithdrawRevealCallback reveal_cb, - void *reveal_cb_cls) -{ - struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh = - GNUNET_new (struct TALER_EXCHANGE_AgeWithdrawRevealHandle); - awrh->noreveal_index = noreveal_index; - awrh->h_commitment = *h_commitment; - awrh->num_coins = num_coins; - awrh->coins_input = coins_input; - awrh->callback = reveal_cb; - awrh->callback_cls = reveal_cb_cls; - awrh->reserve_pub = reserve_pub; - - if (GNUNET_OK != - prepare_url (exchange_url, - awrh)) - return NULL; - - perform_protocol (curl_ctx, awrh); - - return awrh; -} - - -void -TALER_EXCHANGE_age_withdraw_reveal_cancel ( - struct TALER_EXCHANGE_AgeWithdrawRevealHandle *awrh) -{ - if (NULL != awrh->job) - { - GNUNET_CURL_job_cancel (awrh->job); - awrh->job = NULL; - } - TALER_curl_easy_post_finished (&awrh->post_ctx); - - if (NULL != awrh->request_url) - GNUNET_free (awrh->request_url); - - GNUNET_free (awrh); -} - - -/* exchange_api_age_withdraw_reveal.c */ diff --git a/src/lib/exchange_api_batch_withdraw.c b/src/lib/exchange_api_batch_withdraw.c @@ -1,463 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2022 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see - <http://www.gnu.org/licenses/> -*/ -/** - * @file lib/exchange_api_batch_withdraw.c - * @brief Implementation of /reserves/$RESERVE_PUB/batch-withdraw requests with blinding/unblinding - * @author Christian Grothoff - */ -#include "platform.h" -#include <jansson.h> -#include <microhttpd.h> /* just for HTTP status codes */ -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <gnunet/gnunet_curl_lib.h> -#include "taler_exchange_service.h" -#include "taler_json_lib.h" -#include "exchange_api_handle.h" -#include "taler_signatures.h" -#include "exchange_api_curl_defaults.h" - - -/** - * Data we keep per coin in the batch. - */ -struct CoinData -{ - - /** - * Denomination key we are withdrawing. - */ - struct TALER_EXCHANGE_DenomPublicKey pk; - - /** - * Master key material for the coin. - */ - struct TALER_PlanchetMasterSecretP ps; - - /** - * Age commitment for the coin. - */ - const struct TALER_AgeCommitmentHash *ach; - - /** - * blinding secret - */ - union GNUNET_CRYPTO_BlindingSecretP bks; - - /** - * Session nonce. - */ - union GNUNET_CRYPTO_BlindSessionNonce nonce; - - /** - * Private key of the coin we are withdrawing. - */ - struct TALER_CoinSpendPrivateKeyP priv; - - /** - * Details of the planchet. - */ - struct TALER_PlanchetDetail pd; - - /** - * Values of the cipher selected - */ - struct TALER_ExchangeWithdrawValues alg_values; - - /** - * Hash of the public key of the coin we are signing. - */ - struct TALER_CoinPubHashP c_hash; - - /** - * Handler for the CS R request (only used for GNUNET_CRYPTO_BSA_CS denominations) - */ - struct TALER_EXCHANGE_CsRWithdrawHandle *csrh; - - /** - * Batch withdraw this coin is part of. - */ - struct TALER_EXCHANGE_BatchWithdrawHandle *wh; -}; - - -/** - * @brief A batch withdraw handle - */ -struct TALER_EXCHANGE_BatchWithdrawHandle -{ - - /** - * The curl context to use - */ - struct GNUNET_CURL_Context *curl_ctx; - - /** - * The base URL to the exchange - */ - const char *exchange_url; - - /** - * The /keys information from the exchange - */ - const struct TALER_EXCHANGE_Keys *keys; - - /** - * Handle for the actual (internal) batch withdraw operation. - */ - struct TALER_EXCHANGE_BatchWithdraw2Handle *wh2; - - /** - * Function to call with the result. - */ - TALER_EXCHANGE_BatchWithdrawCallback cb; - - /** - * Closure for @a cb. - */ - void *cb_cls; - - /** - * Reserve private key. - */ - const struct TALER_ReservePrivateKeyP *reserve_priv; - - /** - * Array of per-coin data. - */ - struct CoinData *coins; - - /** - * Length of the @e coins array. - */ - unsigned int num_coins; - - /** - * Number of CS requests still pending. - */ - unsigned int cs_pending; - -}; - - -/** - * Function called when we're done processing the - * HTTP /reserves/$RESERVE_PUB/batch-withdraw request. - * - * @param cls the `struct TALER_EXCHANGE_BatchWithdrawHandle` - * @param bw2r response data - */ -static void -handle_reserve_batch_withdraw_finished ( - void *cls, - const struct TALER_EXCHANGE_BatchWithdraw2Response *bw2r) -{ - struct TALER_EXCHANGE_BatchWithdrawHandle *wh = cls; - struct TALER_EXCHANGE_BatchWithdrawResponse wr = { - .hr = bw2r->hr - }; - struct TALER_EXCHANGE_PrivateCoinDetails coins[GNUNET_NZL (wh->num_coins)]; - - wh->wh2 = NULL; - memset (coins, - 0, - sizeof (coins)); - switch (bw2r->hr.http_status) - { - case MHD_HTTP_OK: - { - if (bw2r->details.ok.blind_sigs_length != wh->num_coins) - { - GNUNET_break_op (0); - wr.hr.http_status = 0; - wr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - for (unsigned int i = 0; i<wh->num_coins; i++) - { - struct CoinData *cd = &wh->coins[i]; - struct TALER_EXCHANGE_PrivateCoinDetails *coin = &coins[i]; - struct TALER_FreshCoin fc; - - if (GNUNET_OK != - TALER_planchet_to_coin (&cd->pk.key, - &bw2r->details.ok.blind_sigs[i], - &cd->bks, - &cd->priv, - cd->ach, - &cd->c_hash, - &cd->alg_values, - &fc)) - { - wr.hr.http_status = 0; - wr.hr.ec = TALER_EC_EXCHANGE_WITHDRAW_UNBLIND_FAILURE; - break; - } - coin->coin_priv = cd->priv; - coin->bks = cd->bks; - coin->sig = fc.sig; - coin->exchange_vals = cd->alg_values; - } - wr.details.ok.coins = coins; - wr.details.ok.num_coins = wh->num_coins; - break; - } - case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ( - "h_payto", - &wr.details.unavailable_for_legal_reasons.h_payto), - GNUNET_JSON_spec_uint64 ( - "requirement_row", - &wr.details.unavailable_for_legal_reasons.requirement_row), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (bw2r->hr.reply, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - wr.hr.http_status = 0; - wr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - } - break; - default: - break; - } - wh->cb (wh->cb_cls, - &wr); - for (unsigned int i = 0; i<wh->num_coins; i++) - TALER_denom_sig_free (&coins[i].sig); - TALER_EXCHANGE_batch_withdraw_cancel (wh); -} - - -/** - * Runs phase two, the actual withdraw operation. - * Started once the preparation for CS-denominations is - * done. - * - * @param[in,out] wh batch withdraw to start phase 2 for - */ -static void -phase_two (struct TALER_EXCHANGE_BatchWithdrawHandle *wh) -{ - struct TALER_PlanchetDetail pds[wh->num_coins]; - - for (unsigned int i = 0; i<wh->num_coins; i++) - { - struct CoinData *cd = &wh->coins[i]; - - pds[i] = cd->pd; - } - wh->wh2 = TALER_EXCHANGE_batch_withdraw2 ( - wh->curl_ctx, - wh->exchange_url, - wh->keys, - wh->reserve_priv, - wh->num_coins, - pds, - &handle_reserve_batch_withdraw_finished, - wh); -} - - -/** - * Function called when stage 1 of CS withdraw is finished (request r_pub's) - * - * @param cls the `struct CoinData *` - * @param csrr replies from the /csr-withdraw request - */ -static void -withdraw_cs_stage_two_callback ( - void *cls, - const struct TALER_EXCHANGE_CsRWithdrawResponse *csrr) -{ - struct CoinData *cd = cls; - struct TALER_EXCHANGE_BatchWithdrawHandle *wh = cd->wh; - struct TALER_EXCHANGE_BatchWithdrawResponse wr = { - .hr = csrr->hr - }; - - cd->csrh = NULL; - GNUNET_assert (GNUNET_CRYPTO_BSA_CS == - cd->pk.key.bsign_pub_key->cipher); - switch (csrr->hr.http_status) - { - case MHD_HTTP_OK: - GNUNET_assert (NULL == - cd->alg_values.blinding_inputs); - TALER_denom_ewv_copy (&cd->alg_values, - &csrr->details.ok.alg_values); - TALER_planchet_setup_coin_priv (&cd->ps, - &cd->alg_values, - &cd->priv); - TALER_planchet_blinding_secret_create (&cd->ps, - &cd->alg_values, - &cd->bks); - if (GNUNET_OK != - TALER_planchet_prepare (&cd->pk.key, - &cd->alg_values, - &cd->bks, - &cd->nonce, - &cd->priv, - cd->ach, - &cd->c_hash, - &cd->pd)) - { - GNUNET_break (0); - wr.hr.http_status = 0; - wr.hr.ec = TALER_EC_GENERIC_CLIENT_INTERNAL_ERROR; - wh->cb (wh->cb_cls, - &wr); - TALER_EXCHANGE_batch_withdraw_cancel (wh); - return; - } - wh->cs_pending--; - if (0 == wh->cs_pending) - phase_two (wh); - return; - default: - break; - } - wh->cb (wh->cb_cls, - &wr); - TALER_EXCHANGE_batch_withdraw_cancel (wh); -} - - -struct TALER_EXCHANGE_BatchWithdrawHandle * -TALER_EXCHANGE_batch_withdraw ( - struct GNUNET_CURL_Context *curl_ctx, - const char *exchange_url, - const struct TALER_EXCHANGE_Keys *keys, - const struct TALER_ReservePrivateKeyP *reserve_priv, - unsigned int wci_length, - const struct TALER_EXCHANGE_WithdrawCoinInput wcis[static wci_length], - TALER_EXCHANGE_BatchWithdrawCallback res_cb, - void *res_cb_cls) -{ - struct TALER_EXCHANGE_BatchWithdrawHandle *wh; - - wh = GNUNET_new (struct TALER_EXCHANGE_BatchWithdrawHandle); - wh->curl_ctx = curl_ctx; - wh->exchange_url = exchange_url; - wh->keys = keys; - wh->cb = res_cb; - wh->cb_cls = res_cb_cls; - wh->reserve_priv = reserve_priv; - wh->num_coins = wci_length; - wh->coins = GNUNET_new_array (wh->num_coins, - struct CoinData); - for (unsigned int i = 0; i<wci_length; i++) - { - struct CoinData *cd = &wh->coins[i]; - const struct TALER_EXCHANGE_WithdrawCoinInput *wci = &wcis[i]; - - cd->wh = wh; - cd->ps = *wci->ps; - cd->ach = wci->ach; - cd->pk = *wci->pk; - TALER_denom_pub_copy (&cd->pk.key, - &wci->pk->key); - switch (wci->pk->key.bsign_pub_key->cipher) - { - case GNUNET_CRYPTO_BSA_RSA: - TALER_denom_ewv_copy (&cd->alg_values, - TALER_denom_ewv_rsa_singleton ()); - TALER_planchet_setup_coin_priv (&cd->ps, - &cd->alg_values, - &cd->priv); - TALER_planchet_blinding_secret_create (&cd->ps, - &cd->alg_values, - &cd->bks); - if (GNUNET_OK != - TALER_planchet_prepare (&cd->pk.key, - &cd->alg_values, - &cd->bks, - NULL, - &cd->priv, - cd->ach, - &cd->c_hash, - &cd->pd)) - { - GNUNET_break (0); - TALER_EXCHANGE_batch_withdraw_cancel (wh); - return NULL; - } - break; - case GNUNET_CRYPTO_BSA_CS: - TALER_cs_withdraw_nonce_derive ( - &cd->ps, - &cd->nonce.cs_nonce); - cd->csrh = TALER_EXCHANGE_csr_withdraw ( - curl_ctx, - exchange_url, - &cd->pk, - &cd->nonce.cs_nonce, - &withdraw_cs_stage_two_callback, - cd); - if (NULL == cd->csrh) - { - GNUNET_break (0); - TALER_EXCHANGE_batch_withdraw_cancel (wh); - return NULL; - } - wh->cs_pending++; - break; - default: - GNUNET_break (0); - TALER_EXCHANGE_batch_withdraw_cancel (wh); - return NULL; - } - } - if (0 == wh->cs_pending) - phase_two (wh); - return wh; -} - - -void -TALER_EXCHANGE_batch_withdraw_cancel ( - struct TALER_EXCHANGE_BatchWithdrawHandle *wh) -{ - for (unsigned int i = 0; i<wh->num_coins; i++) - { - struct CoinData *cd = &wh->coins[i]; - - if (NULL != cd->csrh) - { - TALER_EXCHANGE_csr_withdraw_cancel (cd->csrh); - cd->csrh = NULL; - } - TALER_denom_ewv_free (&cd->alg_values); - TALER_blinded_planchet_free (&cd->pd.blinded_planchet); - TALER_denom_pub_free (&cd->pk.key); - } - GNUNET_free (wh->coins); - if (NULL != wh->wh2) - { - TALER_EXCHANGE_batch_withdraw2_cancel (wh->wh2); - wh->wh2 = NULL; - } - GNUNET_free (wh); -} diff --git a/src/lib/exchange_api_batch_withdraw2.c b/src/lib/exchange_api_batch_withdraw2.c @@ -1,446 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2023 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see - <http://www.gnu.org/licenses/> -*/ -/** - * @file lib/exchange_api_batch_withdraw2.c - * @brief Implementation of /reserves/$RESERVE_PUB/batch-withdraw requests without blinding/unblinding - * @author Christian Grothoff - */ -#include "platform.h" -#include <jansson.h> -#include <microhttpd.h> /* just for HTTP status codes */ -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <gnunet/gnunet_curl_lib.h> -#include "taler_exchange_service.h" -#include "taler_json_lib.h" -#include "exchange_api_handle.h" -#include "taler_signatures.h" -#include "exchange_api_curl_defaults.h" - - -/** - * @brief A batch withdraw handle - */ -struct TALER_EXCHANGE_BatchWithdraw2Handle -{ - - /** - * The url for this request. - */ - char *url; - - /** - * The /keys material from the exchange - */ - const struct TALER_EXCHANGE_Keys *keys; - - /** - * Handle for the request. - */ - struct GNUNET_CURL_Job *job; - - /** - * Function to call with the result. - */ - TALER_EXCHANGE_BatchWithdraw2Callback cb; - - /** - * Closure for @a cb. - */ - void *cb_cls; - - /** - * Context for #TEH_curl_easy_post(). Keeps the data that must - * persist for Curl to make the upload. - */ - struct TALER_CURL_PostContext post_ctx; - - /** - * Total amount requested (value plus withdraw fee). - */ - struct TALER_Amount requested_amount; - - /** - * Public key of the reserve we are withdrawing from. - */ - struct TALER_ReservePublicKeyP reserve_pub; - - /** - * Number of coins expected. - */ - unsigned int num_coins; -}; - - -/** - * We got a 200 OK response for the /reserves/$RESERVE_PUB/batch-withdraw operation. - * Extract the coin's signature and return it to the caller. The signature we - * get from the exchange is for the blinded value. As we do not have the - * blinding factor, the signature CANNOT be verified. - * - * If everything checks out, we return the unblinded signature - * to the application via the callback. - * - * @param wh operation handle - * @param json reply from the exchange - * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors - */ -static enum GNUNET_GenericReturnValue -reserve_batch_withdraw_ok (struct TALER_EXCHANGE_BatchWithdraw2Handle *wh, - const json_t *json) -{ - struct TALER_BlindedDenominationSignature blind_sigs[GNUNET_NZL ( - wh->num_coins)]; - const json_t *ja = json_object_get (json, - "ev_sigs"); - const json_t *j; - size_t index; - struct TALER_EXCHANGE_BatchWithdraw2Response bwr = { - .hr.reply = json, - .hr.http_status = MHD_HTTP_OK - }; - - if ( (NULL == ja) || - (! json_is_array (ja)) || - (wh->num_coins != json_array_size (ja)) ) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - json_array_foreach (ja, index, j) - { - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_blinded_denom_sig ("ev_sig", - &blind_sigs[index]), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (j, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - for (size_t i = 0; i<index; i++) - TALER_blinded_denom_sig_free (&blind_sigs[i]); - return GNUNET_SYSERR; - } - } - - /* signature is valid, return it to the application */ - bwr.details.ok.blind_sigs = blind_sigs; - bwr.details.ok.blind_sigs_length = wh->num_coins; - wh->cb (wh->cb_cls, - &bwr); - /* make sure callback isn't called again after return */ - wh->cb = NULL; - for (unsigned int i = 0; i<wh->num_coins; i++) - TALER_blinded_denom_sig_free (&blind_sigs[i]); - - return GNUNET_OK; -} - - -/** - * Function called when we're done processing the - * HTTP /reserves/$RESERVE_PUB/batch-withdraw request. - * - * @param cls the `struct TALER_EXCHANGE_BatchWithdraw2Handle` - * @param response_code HTTP response code, 0 on error - * @param response parsed JSON result, NULL on error - */ -static void -handle_reserve_batch_withdraw_finished (void *cls, - long response_code, - const void *response) -{ - struct TALER_EXCHANGE_BatchWithdraw2Handle *wh = cls; - const json_t *j = response; - struct TALER_EXCHANGE_BatchWithdraw2Response bwr = { - .hr.reply = j, - .hr.http_status = (unsigned int) response_code - }; - - wh->job = NULL; - switch (response_code) - { - case 0: - bwr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - break; - case MHD_HTTP_OK: - if (GNUNET_OK != - reserve_batch_withdraw_ok (wh, - j)) - { - GNUNET_break_op (0); - bwr.hr.http_status = 0; - bwr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - GNUNET_assert (NULL == wh->cb); - TALER_EXCHANGE_batch_withdraw2_cancel (wh); - return; - case MHD_HTTP_BAD_REQUEST: - /* This should never happen, either us or the exchange is buggy - (or API version conflict); just pass JSON reply to the application */ - bwr.hr.ec = TALER_JSON_get_error_code (j); - bwr.hr.hint = TALER_JSON_get_error_hint (j); - break; - case MHD_HTTP_FORBIDDEN: - GNUNET_break_op (0); - /* Nothing really to verify, exchange says one of the signatures is - invalid; as we checked them, this should never happen, we - should pass the JSON reply to the application */ - bwr.hr.ec = TALER_JSON_get_error_code (j); - bwr.hr.hint = TALER_JSON_get_error_hint (j); - break; - case MHD_HTTP_NOT_FOUND: - /* Nothing really to verify, the exchange basically just says - that it doesn't know this reserve. Can happen if we - query before the wire transfer went through. - We should simply pass the JSON reply to the application. */ - bwr.hr.ec = TALER_JSON_get_error_code (j); - bwr.hr.hint = TALER_JSON_get_error_hint (j); - break; - case MHD_HTTP_CONFLICT: - bwr.hr.ec = TALER_JSON_get_error_code (j); - bwr.hr.hint = TALER_JSON_get_error_hint (j); - break; - case MHD_HTTP_GONE: - /* could happen if denomination was revoked */ - /* Note: one might want to check /keys for revocation - signature here, alas tricky in case our /keys - is outdated => left to clients */ - bwr.hr.ec = TALER_JSON_get_error_code (j); - bwr.hr.hint = TALER_JSON_get_error_hint (j); - break; - case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ( - "h_payto", - &bwr.details.unavailable_for_legal_reasons.h_payto), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ( - "account_pub", - &bwr.details.unavailable_for_legal_reasons.account_pub), - NULL), - GNUNET_JSON_spec_uint64 ( - "requirement_row", - &bwr.details.unavailable_for_legal_reasons.requirement_row), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (j, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - bwr.hr.http_status = 0; - bwr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - } - case MHD_HTTP_INTERNAL_SERVER_ERROR: - /* Server had an internal issue; we should retry, but this API - leaves this to the application */ - bwr.hr.ec = TALER_JSON_get_error_code (j); - bwr.hr.hint = TALER_JSON_get_error_hint (j); - break; - default: - /* unexpected response code */ - GNUNET_break_op (0); - bwr.hr.ec = TALER_JSON_get_error_code (j); - bwr.hr.hint = TALER_JSON_get_error_hint (j); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Unexpected response code %u/%d for exchange batch withdraw\n", - (unsigned int) response_code, - (int) bwr.hr.ec); - break; - } - if (NULL != wh->cb) - { - wh->cb (wh->cb_cls, - &bwr); - wh->cb = NULL; - } - TALER_EXCHANGE_batch_withdraw2_cancel (wh); -} - - -struct TALER_EXCHANGE_BatchWithdraw2Handle * -TALER_EXCHANGE_batch_withdraw2 ( - struct GNUNET_CURL_Context *curl_ctx, - const char *exchange_url, - const struct TALER_EXCHANGE_Keys *keys, - const struct TALER_ReservePrivateKeyP *reserve_priv, - unsigned int pds_length, - const struct TALER_PlanchetDetail pds[static pds_length], - TALER_EXCHANGE_BatchWithdraw2Callback res_cb, - void *res_cb_cls) -{ - struct TALER_EXCHANGE_BatchWithdraw2Handle *wh; - const struct TALER_EXCHANGE_DenomPublicKey *dk; - struct TALER_ReserveSignatureP reserve_sig; - char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32]; - struct TALER_BlindedCoinHashP bch; - json_t *jc; - - GNUNET_assert (NULL != keys); - wh = GNUNET_new (struct TALER_EXCHANGE_BatchWithdraw2Handle); - wh->keys = keys; - wh->cb = res_cb; - wh->cb_cls = res_cb_cls; - wh->num_coins = pds_length; - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (keys->currency, - &wh->requested_amount)); - GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv, - &wh->reserve_pub.eddsa_pub); - { - char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2]; - char *end; - - end = GNUNET_STRINGS_data_to_string ( - &wh->reserve_pub, - sizeof (struct TALER_ReservePublicKeyP), - pub_str, - sizeof (pub_str)); - *end = '\0'; - GNUNET_snprintf (arg_str, - sizeof (arg_str), - "reserves/%s/batch-withdraw", - pub_str); - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Attempting to batch-withdraw from reserve %s\n", - TALER_B2S (&wh->reserve_pub)); - wh->url = TALER_url_join (exchange_url, - arg_str, - NULL); - if (NULL == wh->url) - { - GNUNET_break (0); - TALER_EXCHANGE_batch_withdraw2_cancel (wh); - return NULL; - } - jc = json_array (); - GNUNET_assert (NULL != jc); - for (unsigned int i = 0; i<pds_length; i++) - { - const struct TALER_PlanchetDetail *pd = &pds[i]; - struct TALER_Amount coin_total; - json_t *withdraw_obj; - - dk = TALER_EXCHANGE_get_denomination_key_by_hash (keys, - &pd->denom_pub_hash); - if (NULL == dk) - { - TALER_EXCHANGE_batch_withdraw2_cancel (wh); - json_decref (jc); - GNUNET_break (0); - return NULL; - } - /* Compute how much we expected to charge to the reserve */ - if (0 > - TALER_amount_add (&coin_total, - &dk->fees.withdraw, - &dk->value)) - { - /* Overflow here? Very strange, our CPU must be fried... */ - GNUNET_break (0); - TALER_EXCHANGE_batch_withdraw2_cancel (wh); - json_decref (jc); - return NULL; - } - if (0 > - TALER_amount_add (&wh->requested_amount, - &wh->requested_amount, - &coin_total)) - { - /* Overflow here? Very strange, our CPU must be fried... */ - GNUNET_break (0); - TALER_EXCHANGE_batch_withdraw2_cancel (wh); - json_decref (jc); - return NULL; - } - TALER_coin_ev_hash (&pd->blinded_planchet, - &pd->denom_pub_hash, - &bch); - TALER_wallet_withdraw_sign (&pd->denom_pub_hash, - &coin_total, - &bch, - reserve_priv, - &reserve_sig); - withdraw_obj = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("denom_pub_hash", - &pd->denom_pub_hash), - TALER_JSON_pack_blinded_planchet ("coin_ev", - &pd->blinded_planchet), - GNUNET_JSON_pack_data_auto ("reserve_sig", - &reserve_sig)); - GNUNET_assert (NULL != withdraw_obj); - GNUNET_assert (0 == - json_array_append_new (jc, - withdraw_obj)); - } - { - CURL *eh; - json_t *req; - - req = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_array_steal ("planchets", - jc)); - eh = TALER_EXCHANGE_curl_easy_get_ (wh->url); - if ( (NULL == eh) || - (GNUNET_OK != - TALER_curl_easy_post (&wh->post_ctx, - eh, - req)) ) - { - GNUNET_break (0); - if (NULL != eh) - curl_easy_cleanup (eh); - json_decref (req); - TALER_EXCHANGE_batch_withdraw2_cancel (wh); - return NULL; - } - json_decref (req); - wh->job = GNUNET_CURL_job_add2 (curl_ctx, - eh, - wh->post_ctx.headers, - &handle_reserve_batch_withdraw_finished, - wh); - } - return wh; -} - - -void -TALER_EXCHANGE_batch_withdraw2_cancel ( - struct TALER_EXCHANGE_BatchWithdraw2Handle *wh) -{ - if (NULL != wh->job) - { - GNUNET_CURL_job_cancel (wh->job); - wh->job = NULL; - } - GNUNET_free (wh->url); - TALER_curl_easy_post_finished (&wh->post_ctx); - GNUNET_free (wh); -} diff --git a/src/lib/exchange_api_blinding_prepare.c b/src/lib/exchange_api_blinding_prepare.c @@ -0,0 +1,393 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_blinding_prepare.c + * @brief Implementation of /blinding-prepare requests + * @author Özgür Kesim + */ + +#include "platform.h" +#include <gnunet/gnunet_common.h> +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP status codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include <sys/wait.h> +#include "taler_curl_lib.h" +#include "taler_error_codes.h" +#include "taler_json_lib.h" +#include "taler_exchange_service.h" +#include "exchange_api_common.h" +#include "exchange_api_handle.h" +#include "taler_signatures.h" +#include "exchange_api_curl_defaults.h" +#include "taler_util.h" + +/** + * A /blinding-prepare request-handle + */ +struct TALER_EXCHANGE_BlindingPrepareHandle +{ + /** + * number of elements to prepare + */ + size_t num; + + /** + * The @a num nonces for Clause-Schnorr + */ + const union GNUNET_CRYPTO_BlindSessionNonce *nonces; + + /** + * The corresponding @a num denomination public keys + */ + const struct TALER_DenominationHashP *denoms_h; + /** + * The url for this request. + */ + char *url; + + /** + * Context for curl. + */ + struct GNUNET_CURL_Context *curl_ctx; + + /** + * CURL handle for the request job. + */ + struct GNUNET_CURL_Job *job; + + /** + * Post Context + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Function to call with withdraw response results. + */ + TALER_EXCHANGE_BlindingPrepareCallback callback; + + /** + * Closure for @e callback + */ + void *callback_cls; + +}; + + +/** + * We got a 200 OK response for the /blinding-prepare operation. + * Extract the r_pub values and return them to the caller via the callback + * + * @param handle operation handle + * @param response response details + * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors + */ +static enum GNUNET_GenericReturnValue +blinding_prepare_ok (struct TALER_EXCHANGE_BlindingPrepareHandle *handle, + struct TALER_EXCHANGE_BlindingPrepareResponse *response) +{ + const json_t *j_r_pubs; + const char *cipher; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("cipher", + &cipher), + GNUNET_JSON_spec_array_const ("r_pubs", + &j_r_pubs), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (response->hr.reply, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + if (strcmp ("CS", cipher)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + if (json_array_size (j_r_pubs) + != handle->num) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + { + size_t num = handle->num; + const json_t *j_pair; + size_t idx; + struct TALER_ExchangeWithdrawValues alg_values[GNUNET_NZL (num)]; + + memset (alg_values, + 0, + sizeof(alg_values)); + + json_array_foreach (j_r_pubs, idx, j_pair) { + struct GNUNET_CRYPTO_BlindingInputValues *bi = + GNUNET_new (struct GNUNET_CRYPTO_BlindingInputValues); + struct GNUNET_CRYPTO_CSPublicRPairP *csv = &bi->details.cs_values; + + struct GNUNET_JSON_Specification tuple[] = { + GNUNET_JSON_spec_fixed (NULL, + &csv->r_pub[0], + sizeof(csv->r_pub[0])), + GNUNET_JSON_spec_fixed (NULL, + &csv->r_pub[1], + sizeof(csv->r_pub[1])), + GNUNET_JSON_spec_end () + }; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_tuple_of (NULL, tuple), + GNUNET_JSON_spec_end () + }; + const char *err_msg; + unsigned int err_line; + + if (GNUNET_OK != + GNUNET_JSON_parse (j_pair, + spec, + &err_msg, + &err_line)) + { + GNUNET_break_op (0); + GNUNET_free (bi); + for (size_t i=0; i < idx; i++) + TALER_denom_ewv_free (&alg_values[i]); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Error while parsing response: in line %d: %s", + err_line, + err_msg); + return GNUNET_SYSERR; + } + + bi->cipher = GNUNET_CRYPTO_BSA_CS; + bi->rc = 1; + alg_values[idx].blinding_inputs = bi; + } + + response->details.ok.alg_values = alg_values; + response->details.ok.num = num; + + handle->callback ( + handle->callback_cls, + response); + + for (size_t i = 0; i < num; i++) + TALER_denom_ewv_free (&alg_values[i]); + } + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the HTTP /blinding-prepare request. + * + * @param cls the `struct TALER_EXCHANGE_BlindingPrepareHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_blinding_prepare_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_BlindingPrepareHandle *handle = cls; + const json_t *j_response = response; + struct TALER_EXCHANGE_BlindingPrepareResponse bpr = { + .hr = { + .reply = j_response, + .http_status = (unsigned int) response_code + }, + }; + + handle->job = NULL; + + switch (response_code) + { + case 0: + bpr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + + case MHD_HTTP_OK: + { + if (GNUNET_OK != + blinding_prepare_ok (handle, + &bpr)) + { + GNUNET_break_op (0); + bpr.hr.http_status = 0; + bpr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + } + TALER_EXCHANGE_blinding_prepare_cancel (handle); + return; + + case MHD_HTTP_BAD_REQUEST: + /* This should never happen, either us or the exchange is buggy + (or API version conflict); just pass JSON reply to the application */ + bpr.hr.ec = TALER_JSON_get_error_code (j_response); + bpr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify, the exchange basically just says + that it doesn't know the /csr endpoint or denomination. + Can happen if the exchange doesn't support Clause Schnorr. + We should simply pass the JSON reply to the application. */ + bpr.hr.ec = TALER_JSON_get_error_code (j_response); + bpr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + + case MHD_HTTP_GONE: + /* could happen if denomination was revoked */ + /* Note: one might want to check /keys for revocation + signature here, alas tricky in case our /keys + is outdated => left to clients */ + bpr.hr.ec = TALER_JSON_get_error_code (j_response); + bpr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + bpr.hr.ec = TALER_JSON_get_error_code (j_response); + bpr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + + default: + /* unexpected response code */ + GNUNET_break_op (0); + bpr.hr.ec = TALER_JSON_get_error_code (j_response); + bpr.hr.hint = TALER_JSON_get_error_hint (j_response); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for the blinding-prepare request\n", + (unsigned int) response_code, + (int) bpr.hr.ec); + break; + + } + + handle->callback (handle->callback_cls, + &bpr); + handle->callback = NULL; + TALER_EXCHANGE_blinding_prepare_cancel (handle); +} + + +struct TALER_EXCHANGE_BlindingPrepareHandle * +TALER_EXCHANGE_blinding_prepare ( + struct GNUNET_CURL_Context *curl_ctx, + const char *exchange_url, + size_t num, + const union GNUNET_CRYPTO_BlindSessionNonce *nonces, + const struct TALER_DenominationHashP *denoms_h, + TALER_EXCHANGE_BlindingPrepareCallback callback, + void *callback_cls) +{ + struct TALER_EXCHANGE_BlindingPrepareHandle *bph; + + bph = GNUNET_new (struct TALER_EXCHANGE_BlindingPrepareHandle); + bph->num = num; + bph->nonces = nonces; + bph->denoms_h = denoms_h; + bph->callback = callback; + bph->callback_cls = callback_cls; + bph->url = TALER_url_join (exchange_url, + "blinding-prepare", + NULL); + if (NULL == bph->url) + { + GNUNET_break (0); + GNUNET_free (bph); + return NULL; + } + + { + CURL *eh; + json_t *j_request = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("cipher", + "CS"), + TALER_JSON_pack_array_of_data ("nonces", + num, + nonces, + sizeof(nonces[0])), + TALER_JSON_pack_array_of_data ("denoms_h", + num, + denoms_h, + sizeof(denoms_h[0]))); + GNUNET_assert (NULL != j_request); + + eh = TALER_EXCHANGE_curl_easy_get_ (bph->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&bph->post_ctx, + eh, + j_request))) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (j_request); + GNUNET_free (bph->url); + GNUNET_free (bph); + return NULL; + } + + json_decref (j_request); + bph->job = GNUNET_CURL_job_add2 (curl_ctx, + eh, + bph->post_ctx.headers, + &handle_blinding_prepare_finished, + bph); + if (NULL == bph->job) + { + GNUNET_break (0); + TALER_EXCHANGE_blinding_prepare_cancel (bph); + return NULL; + } + } + return bph; +} + + +void +TALER_EXCHANGE_blinding_prepare_cancel ( + struct TALER_EXCHANGE_BlindingPrepareHandle *bph) +{ + if (NULL == bph) + return; + if (NULL != bph->job) + { + GNUNET_CURL_job_cancel (bph->job); + bph->job = NULL; + } + GNUNET_free (bph->url); + TALER_curl_easy_post_finished (&bph->post_ctx); + GNUNET_free (bph); +} + + +/* end of lib/exchange_api_blinding_prepare.c */ diff --git a/src/lib/exchange_api_csr_withdraw.c b/src/lib/exchange_api_csr_withdraw.c @@ -1,281 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2022 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see - <http://www.gnu.org/licenses/> -*/ -/** - * @file lib/exchange_api_csr_withdraw.c - * @brief Implementation of /csr-withdraw requests (get R in exchange used for Clause Schnorr withdraw and refresh) - * @author Lucien Heuzeveldt - * @author Gian Demarmels - */ -#include "platform.h" -#include <jansson.h> -#include <microhttpd.h> /* just for HTTP status codes */ -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <gnunet/gnunet_curl_lib.h> -#include "taler_exchange_service.h" -#include "taler_json_lib.h" -#include "exchange_api_handle.h" -#include "taler_signatures.h" -#include "exchange_api_curl_defaults.h" - - -/** - * @brief A Clause Schnorr R Handle - */ -struct TALER_EXCHANGE_CsRWithdrawHandle -{ - /** - * Function to call with the result. - */ - TALER_EXCHANGE_CsRWithdrawCallback cb; - - /** - * Closure for @a cb. - */ - void *cb_cls; - - /** - * The url for this request. - */ - char *url; - - /** - * Handle for the request. - */ - struct GNUNET_CURL_Job *job; - - /** - * Context for #TEH_curl_easy_post(). Keeps the data that must - * persist for Curl to make the upload. - */ - struct TALER_CURL_PostContext post_ctx; -}; - - -/** - * We got a 200 OK response for the /reserves/$RESERVE_PUB/withdraw operation. - * Extract the coin's signature and return it to the caller. The signature we - * get from the exchange is for the blinded value. Thus, we first must - * unblind it and then should verify its validity against our coin's hash. - * - * If everything checks out, we return the unblinded signature - * to the application via the callback. - * - * @param csrh operation handle - * @param av reply from the exchange - * @param hr http response details - * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors - */ -static enum GNUNET_GenericReturnValue -csr_ok (struct TALER_EXCHANGE_CsRWithdrawHandle *csrh, - const json_t *av, - struct TALER_EXCHANGE_HttpResponse *hr) -{ - struct TALER_EXCHANGE_CsRWithdrawResponse csrr = { - .hr = *hr, - }; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_exchange_withdraw_values ( - "ewv", - &csrr.details.ok.alg_values), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (av, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - csrh->cb (csrh->cb_cls, - &csrr); - TALER_denom_ewv_free (&csrr.details.ok.alg_values); - return GNUNET_OK; -} - - -/** - * Function called when we're done processing the HTTP /csr request. - * - * @param cls the `struct TALER_EXCHANGE_CsRWithdrawHandle` - * @param response_code HTTP response code, 0 on error - * @param response parsed JSON result, NULL on error - */ -static void -handle_csr_finished (void *cls, - long response_code, - const void *response) -{ - struct TALER_EXCHANGE_CsRWithdrawHandle *csrh = cls; - const json_t *j = response; - struct TALER_EXCHANGE_HttpResponse hr = { - .reply = j, - .http_status = (unsigned int) response_code - }; - struct TALER_EXCHANGE_CsRWithdrawResponse csrr = { - .hr = hr - }; - - csrh->job = NULL; - switch (response_code) - { - case 0: - csrr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - break; - case MHD_HTTP_OK: - { - if (GNUNET_OK != - csr_ok (csrh, - response, - &hr)) - { - GNUNET_break_op (0); - csrr.hr.http_status = 0; - csrr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - } - TALER_EXCHANGE_csr_withdraw_cancel (csrh); - return; - case MHD_HTTP_BAD_REQUEST: - /* This should never happen, either us or the exchange is buggy - (or API version conflict); just pass JSON reply to the application */ - csrr.hr.ec = TALER_JSON_get_error_code (j); - csrr.hr.hint = TALER_JSON_get_error_hint (j); - break; - case MHD_HTTP_NOT_FOUND: - /* Nothing really to verify, the exchange basically just says - that it doesn't know the /csr endpoint or denomination. - Can happen if the exchange doesn't support Clause Schnorr. - We should simply pass the JSON reply to the application. */ - csrr.hr.ec = TALER_JSON_get_error_code (j); - csrr.hr.hint = TALER_JSON_get_error_hint (j); - break; - case MHD_HTTP_GONE: - /* could happen if denomination was revoked */ - /* Note: one might want to check /keys for revocation - signature here, alas tricky in case our /keys - is outdated => left to clients */ - csrr.hr.ec = TALER_JSON_get_error_code (j); - csrr.hr.hint = TALER_JSON_get_error_hint (j); - break; - case MHD_HTTP_INTERNAL_SERVER_ERROR: - /* Server had an internal issue; we should retry, but this API - leaves this to the application */ - csrr.hr.ec = TALER_JSON_get_error_code (j); - csrr.hr.hint = TALER_JSON_get_error_hint (j); - break; - default: - /* unexpected response code */ - GNUNET_break_op (0); - csrr.hr.ec = TALER_JSON_get_error_code (j); - csrr.hr.hint = TALER_JSON_get_error_hint (j); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Unexpected response code %u/%d for CS R request\n", - (unsigned int) response_code, - (int) hr.ec); - break; - } - csrh->cb (csrh->cb_cls, - &csrr); - csrh->cb = NULL; - TALER_EXCHANGE_csr_withdraw_cancel (csrh); -} - - -struct TALER_EXCHANGE_CsRWithdrawHandle * -TALER_EXCHANGE_csr_withdraw ( - struct GNUNET_CURL_Context *curl_ctx, - const char *exchange_url, - const struct TALER_EXCHANGE_DenomPublicKey *pk, - const struct GNUNET_CRYPTO_CsSessionNonce *nonce, - TALER_EXCHANGE_CsRWithdrawCallback res_cb, - void *res_cb_cls) -{ - struct TALER_EXCHANGE_CsRWithdrawHandle *csrh; - - if (GNUNET_CRYPTO_BSA_CS != - pk->key.bsign_pub_key->cipher) - { - GNUNET_break (0); - return NULL; - } - csrh = GNUNET_new (struct TALER_EXCHANGE_CsRWithdrawHandle); - csrh->cb = res_cb; - csrh->cb_cls = res_cb_cls; - csrh->url = TALER_url_join (exchange_url, - "csr-withdraw", - NULL); - if (NULL == csrh->url) - { - GNUNET_free (csrh); - return NULL; - } - - { - CURL *eh; - json_t *req; - - req = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_varsize ("nonce", - nonce, - sizeof(*nonce)), - GNUNET_JSON_pack_data_varsize ("denom_pub_hash", - &pk->h_key, - sizeof(pk->h_key))); - GNUNET_assert (NULL != req); - eh = TALER_EXCHANGE_curl_easy_get_ (csrh->url); - if ( (NULL == eh) || - (GNUNET_OK != - TALER_curl_easy_post (&csrh->post_ctx, - eh, - req)) ) - { - GNUNET_break (0); - if (NULL != eh) - curl_easy_cleanup (eh); - json_decref (req); - GNUNET_free (csrh->url); - GNUNET_free (csrh); - return NULL; - } - json_decref (req); - csrh->job = GNUNET_CURL_job_add2 (curl_ctx, - eh, - csrh->post_ctx.headers, - &handle_csr_finished, - csrh); - } - return csrh; -} - - -void -TALER_EXCHANGE_csr_withdraw_cancel ( - struct TALER_EXCHANGE_CsRWithdrawHandle *csrh) -{ - if (NULL != csrh->job) - { - GNUNET_CURL_job_cancel (csrh->job); - csrh->job = NULL; - } - GNUNET_free (csrh->url); - TALER_curl_easy_post_finished (&csrh->post_ctx); - GNUNET_free (csrh); -} diff --git a/src/lib/exchange_api_recoup.c b/src/lib/exchange_api_recoup.c @@ -236,6 +236,7 @@ TALER_EXCHANGE_recoup ( const struct TALER_DenominationSignature *denom_sig, const struct TALER_ExchangeWithdrawValues *exchange_vals, const struct TALER_PlanchetMasterSecretP *ps, + const struct TALER_WithdrawCommitmentHashP *h_commitment, TALER_EXCHANGE_RecoupResultCallback recoup_cb, void *recoup_cb_cls) { @@ -271,6 +272,8 @@ TALER_EXCHANGE_recoup ( exchange_vals), GNUNET_JSON_pack_data_auto ("coin_sig", &ph->coin_sig), + GNUNET_JSON_pack_data_auto ("withdraw_commitment_hash", + h_commitment), GNUNET_JSON_pack_data_auto ("coin_blind_key_secret", &bks)); switch (denom_sig->unblinded_sig->cipher) diff --git a/src/lib/exchange_api_reserves_history.c b/src/lib/exchange_api_reserves_history.c @@ -188,7 +188,7 @@ parse_credit (struct TALER_EXCHANGE_ReserveHistoryEntry *rh, /** - * Parse "credit" reserve history entry. + * Parse "withdraw" reserve history entry. * * @param[in,out] rh entry to parse * @param uc our context @@ -200,6 +200,183 @@ parse_withdraw (struct TALER_EXCHANGE_ReserveHistoryEntry *rh, struct HistoryParseContext *uc, const json_t *transaction) { + uint16_t num_coins; + struct TALER_Amount withdraw_fee; + struct TALER_Amount withdraw_amount; + uint8_t max_age = 0; + uint8_t noreveal_index = 0; + struct TALER_WithdrawCommitmentHashP h_commitment; + struct TALER_HashBlindedPlanchetsP h_planchets = {0}; + struct TALER_ReserveSignatureP reserve_sig; + const json_t *j_h_coin_evs; + const json_t *j_denom_pub_hashes; + bool no_max_age; + bool no_noreveal_index; + struct GNUNET_JSON_Specification withdraw_spec[] = { + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &reserve_sig), + GNUNET_JSON_spec_uint16 ("num_coins", + &num_coins), + GNUNET_JSON_spec_fixed_auto ("h_commitment", + &h_commitment), + GNUNET_JSON_spec_fixed_auto ("h_planchets", + &h_planchets), + TALER_JSON_spec_amount_any ("amount", + &withdraw_amount), + TALER_JSON_spec_amount_any ("withdraw_fee", + &withdraw_fee), + GNUNET_JSON_spec_array_const ("h_coin_evs", + &j_h_coin_evs), + GNUNET_JSON_spec_array_const ("denom_pub_hashes", + &j_denom_pub_hashes), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint8 ("max_age", + &max_age), + &no_max_age), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint8 ("noreveal_index", + &noreveal_index), + &no_noreveal_index), + GNUNET_JSON_spec_end () + }; + + rh->type = TALER_EXCHANGE_RTT_WITHDRAWAL; + if (GNUNET_OK != + GNUNET_JSON_parse (transaction, + withdraw_spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + if (no_max_age != no_noreveal_index) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + rh->details.withdraw.age_restricted = ! no_max_age; + + if ((num_coins != json_array_size (j_h_coin_evs)) || + (num_coins != json_array_size (j_denom_pub_hashes))) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + /* Check that the signature is a valid withdraw request */ + { + struct TALER_Amount amount_without_fee; + + if (0>TALER_amount_subtract ( + &amount_without_fee, + &withdraw_amount, + &withdraw_fee)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + if (GNUNET_OK != + TALER_wallet_withdraw_verify ( + &amount_without_fee, + &withdraw_fee, + &h_planchets, + no_max_age ? NULL : &uc->keys->age_mask, + no_max_age ? 0 : max_age, + uc->reserve_pub, + &reserve_sig)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (withdraw_spec); + return GNUNET_SYSERR; + } + } + + rh->details.withdraw.num_coins = num_coins; + rh->details.withdraw.fee = withdraw_fee; + rh->details.withdraw.age_restricted = ! no_max_age; + rh->details.withdraw.max_age = max_age; + rh->details.withdraw.h_planchets = h_planchets; + rh->details.withdraw.h_commitment = h_commitment; + rh->details.withdraw.noreveal_index = noreveal_index; + + +#pragma message "finish parsing and verification of withdraw history entry" +#if 0 + /* check that withdraw fee matches expectations! */ + { + const struct TALER_EXCHANGE_Keys *key_state; + const struct TALER_EXCHANGE_DenomPublicKey *dki; + + key_state = uc->keys; + dki = TALER_EXCHANGE_get_denomination_key_by_hash (key_state, + &h_denom_pub); + if ( (GNUNET_YES != + TALER_amount_cmp_currency (&amount_with_fee, + &dki->fees.withdraw)) || + (0 != + TALER_amount_cmp (&amount_with_fee, + &dki->fees.withdraw)) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (withdraw_spec); + return GNUNET_SYSERR; + } + rh->details.withdraw.fee = amount_with_fee; + } + rh->details.withdraw.out_authorization_sig + = json_object_get (transaction, + "signature"); + /* Check check that the same withdraw transaction + isn't listed twice by the exchange. We use the + "uuid" array to remember the hashes of all + signatures, and compare the hashes to find + duplicates. */ + GNUNET_CRYPTO_hash (&sig, + sizeof (sig), + &uc->uuids[uc->uuid_off]); + for (unsigned int i = 0; i<uc->uuid_off; i++) + { + if (0 == GNUNET_memcmp (&uc->uuids[uc->uuid_off], + &uc->uuids[i])) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (withdraw_spec); + return GNUNET_SYSERR; + } + } + uc->uuid_off++; + + if (0 > + TALER_amount_add (uc->total_out, + uc->total_out, + &rh->amount)) + { + /* overflow in history already!? inconceivable! Bad exchange! */ + GNUNET_break_op (0); + GNUNET_JSON_parse_free (withdraw_spec); + return GNUNET_SYSERR; + } +#endif + + return GNUNET_OK; +} + + +/** + * Parse "batch withdraw" reserve history entry. + * + * @param[in,out] rh entry to parse + * @param uc our context + * @param transaction the transaction to parse + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_batch_withdraw (struct TALER_EXCHANGE_ReserveHistoryEntry *rh, + struct HistoryParseContext *uc, + const json_t *transaction) +{ struct TALER_ReserveSignatureP sig; struct TALER_DenominationHashP h_denom_pub; struct TALER_BlindedCoinHashP bch; @@ -216,7 +393,7 @@ parse_withdraw (struct TALER_EXCHANGE_ReserveHistoryEntry *rh, GNUNET_JSON_spec_end () }; - rh->type = TALER_EXCHANGE_RTT_WITHDRAWAL; + rh->type = TALER_EXCHANGE_RTT_BATCH_WITHDRAWAL; if (GNUNET_OK != GNUNET_JSON_parse (transaction, withdraw_spec, @@ -228,11 +405,11 @@ parse_withdraw (struct TALER_EXCHANGE_ReserveHistoryEntry *rh, /* Check that the signature is a valid withdraw request */ if (GNUNET_OK != - TALER_wallet_withdraw_verify (&h_denom_pub, - &rh->amount, - &bch, - uc->reserve_pub, - &sig)) + TALER_wallet_withdraw_verify_pre26 (&h_denom_pub, + &rh->amount, + &bch, + uc->reserve_pub, + &sig)) { GNUNET_break_op (0); GNUNET_JSON_parse_free (withdraw_spec); @@ -257,9 +434,9 @@ parse_withdraw (struct TALER_EXCHANGE_ReserveHistoryEntry *rh, GNUNET_JSON_parse_free (withdraw_spec); return GNUNET_SYSERR; } - rh->details.withdraw.fee = withdraw_fee; + rh->details.batch_withdraw.fee = withdraw_fee; } - rh->details.withdraw.out_authorization_sig + rh->details.batch_withdraw.out_authorization_sig = json_object_get (transaction, "signature"); /* Check check that the same withdraw transaction @@ -667,7 +844,7 @@ free_reserve_history ( break; case TALER_EXCHANGE_RTT_WITHDRAWAL: break; - case TALER_EXCHANGE_RTT_AGEWITHDRAWAL: + case TALER_EXCHANGE_RTT_BATCH_WITHDRAWAL: break; case TALER_EXCHANGE_RTT_RECOUP: break; @@ -721,6 +898,7 @@ parse_reserve_history ( ParseHelper helper; } map[] = { { "CREDIT", &parse_credit }, + { "BATCH_WITHDRAW", &parse_batch_withdraw }, { "WITHDRAW", &parse_withdraw }, { "RECOUP", &parse_recoup }, { "MERGE", &parse_merge }, diff --git a/src/lib/exchange_api_reveal_withdraw.c b/src/lib/exchange_api_reveal_withdraw.c @@ -0,0 +1,386 @@ +/* + This file is part of TALER + Copyright (C) 2023-2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_reveal_withdraw.c + * @brief Implementation of /reveal-withdraw requests + * @author Özgür Kesim + */ + +#include "platform.h" +#include <gnunet/gnunet_common.h> +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP status codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include "taler_curl_lib.h" +#include "taler_json_lib.h" +#include "taler_exchange_service.h" +#include "exchange_api_common.h" +#include "exchange_api_handle.h" +#include "taler_signatures.h" +#include "exchange_api_curl_defaults.h" + +/** + * Handler for a running reveal-withdraw request + */ +struct TALER_EXCHANGE_RevealWithdrawHandle +{ + /** + * The commitment from the previous call withdraw + */ + const struct TALER_WithdrawCommitmentHashP *h_commitment; + + /** + * Number of coins for which to reveal tuples of seeds + */ + size_t num_coins; + + /** + * The TALER_CNC_KAPPA-1 tuple of seeds to reveal + */ + struct TALER_RevealWithdrawMasterSeedsP seeds; + + /** + * The url for the reveal request + */ + char *request_url; + + /** + * CURL handle for the request job. + */ + struct GNUNET_CURL_Job *job; + + /** + * Post Context + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Callback + */ + TALER_EXCHANGE_RevealWithdrawCallback callback; + + /** + * Reveal + */ + void *callback_cls; +}; + + +/** + * We got a 200 OK response for the /reveal-withdraw operation. + * Extract the signed blindedcoins and return it to the caller. + * + * @param wrh operation handle + * @param j_response reply from the exchange + * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors + */ +static enum GNUNET_GenericReturnValue +reveal_withdraw_ok ( + struct TALER_EXCHANGE_RevealWithdrawHandle *wrh, + const json_t *j_response) +{ + struct TALER_EXCHANGE_RevealWithdrawResponse response = { + .hr.reply = j_response, + .hr.http_status = MHD_HTTP_OK, + }; + const json_t *j_sigs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("ev_sigs", + &j_sigs), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j_response, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + if (wrh->num_coins != json_array_size (j_sigs)) + { + /* Number of coins generated does not match our expectation */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + { + struct TALER_BlindedDenominationSignature denom_sigs[wrh->num_coins]; + json_t *j_sig; + size_t n; + + /* Reconstruct the coins and unblind the signatures */ + json_array_foreach (j_sigs, n, j_sig) + { + struct GNUNET_JSON_Specification ispec[] = { + TALER_JSON_spec_blinded_denom_sig (NULL, + &denom_sigs[n]), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j_sig, + ispec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + + response.details.ok.num_sigs = wrh->num_coins; + response.details.ok.blinded_denom_sigs = denom_sigs; + wrh->callback (wrh->callback_cls, + &response); + /* Make sure the callback isn't called again */ + wrh->callback = NULL; + /* Free resources */ + for (size_t i = 0; i < wrh->num_coins; i++) + TALER_blinded_denom_sig_free (&denom_sigs[i]); + } + + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the + * HTTP /reveal-withdraw request. + * + * @param cls the `struct TALER_EXCHANGE_RevealWithdrawHandle` + * @param response_code The HTTP response code + * @param response response data + */ +static void +handle_reveal_withdraw_finished ( + void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_RevealWithdrawHandle *wrh = cls; + const json_t *j_response = response; + struct TALER_EXCHANGE_RevealWithdrawResponse awr = { + .hr.reply = j_response, + .hr.http_status = (unsigned int) response_code + }; + + wrh->job = NULL; + switch (response_code) + { + case 0: + awr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + { + enum GNUNET_GenericReturnValue ret; + + ret = reveal_withdraw_ok (wrh, + j_response); + if (GNUNET_OK != ret) + { + GNUNET_break_op (0); + awr.hr.http_status = 0; + awr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + GNUNET_assert (NULL == wrh->callback); + TALER_EXCHANGE_reveal_withdraw_cancel (wrh); + return; + } + case MHD_HTTP_BAD_REQUEST: + /* This should never happen, either us or the exchange is buggy + (or API version conflict); just pass JSON reply to the application */ + awr.hr.ec = TALER_JSON_get_error_code (j_response); + awr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify, the exchange basically just says + that it doesn't know this age-withdraw commitment. */ + awr.hr.ec = TALER_JSON_get_error_code (j_response); + awr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + case MHD_HTTP_CONFLICT: + /* An age commitment for one of the coins did not fulfill + * the required maximum age requirement of the corresponding + * reserve. + * Error code: TALER_EC_EXCHANGE_GENERIC_COIN_AGE_REQUIREMENT_FAILURE + * or TALER_EC_EXCHANGE_AGE_WITHDRAW_REVEAL_INVALID_HASH. + */ + awr.hr.ec = TALER_JSON_get_error_code (j_response); + awr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + awr.hr.ec = TALER_JSON_get_error_code (j_response); + awr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + awr.hr.ec = TALER_JSON_get_error_code (j_response); + awr.hr.hint = TALER_JSON_get_error_hint (j_response); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange age-withdraw\n", + (unsigned int) response_code, + (int) awr.hr.ec); + break; + } + wrh->callback (wrh->callback_cls, + &awr); + TALER_EXCHANGE_reveal_withdraw_cancel (wrh); +} + + +/** + * Call /reveal-withdraw + * + * @param curl_ctx The context for CURL + * @param wrh The handler + */ +static void +perform_protocol ( + struct GNUNET_CURL_Context *curl_ctx, + struct TALER_EXCHANGE_RevealWithdrawHandle *wrh) +{ + CURL *curlh = NULL; + json_t *j_request_body = NULL; + json_t *j_array_of_secrets = NULL; + json_t *j_secrets = NULL; + json_t *j_sec = NULL; + +#define FAIL_IF(cond) \ + do { \ + if ((cond)) \ + { \ + GNUNET_break (! (cond)); \ + goto ERROR; \ + } \ + } while (0) + + j_array_of_secrets = json_array (); + FAIL_IF (NULL == j_array_of_secrets); + + + for (uint8_t k = 0; k < TALER_CNC_KAPPA - 1; k++) + { + j_sec = GNUNET_JSON_from_data_auto (&wrh->seeds.tuple[k]); + FAIL_IF (NULL == j_sec); + FAIL_IF (0 < json_array_append_new (j_array_of_secrets, + j_sec)); + } + + j_request_body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("withdraw_commitment_h", + wrh->h_commitment), + GNUNET_JSON_pack_array_steal ("disclosed_batch_seeds", + j_array_of_secrets)); + FAIL_IF (NULL == j_request_body); + + curlh = TALER_EXCHANGE_curl_easy_get_ (wrh->request_url); + FAIL_IF (NULL == curlh); + FAIL_IF (GNUNET_OK != + TALER_curl_easy_post (&wrh->post_ctx, + curlh, + j_request_body)); + json_decref (j_request_body); + j_request_body = NULL; + + wrh->job = GNUNET_CURL_job_add2 ( + curl_ctx, + curlh, + wrh->post_ctx.headers, + &handle_reveal_withdraw_finished, + wrh); + FAIL_IF (NULL == wrh->job); + + /* No error, return */ + return; + +ERROR: + if (NULL != j_sec) + json_decref (j_sec); + if (NULL != j_secrets) + json_decref (j_secrets); + if (NULL != j_array_of_secrets) + json_decref (j_array_of_secrets); + if (NULL != j_request_body) + json_decref (j_request_body); + if (NULL != curlh) + curl_easy_cleanup (curlh); + TALER_EXCHANGE_reveal_withdraw_cancel (wrh); + return; +#undef FAIL_IF +} + + +struct TALER_EXCHANGE_RevealWithdrawHandle * +TALER_EXCHANGE_reveal_withdraw ( + struct GNUNET_CURL_Context *curl_ctx, + const char *exchange_url, + size_t num_coins, + const struct TALER_WithdrawCommitmentHashP *h_commitment, + const struct TALER_RevealWithdrawMasterSeedsP *seeds, + TALER_EXCHANGE_RevealWithdrawCallback reveal_cb, + void *reveal_cb_cls) +{ + struct TALER_EXCHANGE_RevealWithdrawHandle *wrh = + GNUNET_new (struct TALER_EXCHANGE_RevealWithdrawHandle); + wrh->h_commitment = h_commitment; + wrh->num_coins = num_coins; + wrh->seeds = *seeds; + wrh->callback = reveal_cb; + wrh->callback_cls = reveal_cb_cls; + wrh->request_url = TALER_url_join (exchange_url, + "reveal-withdraw", + NULL); + if (NULL == wrh->request_url) + { + GNUNET_break (0); + GNUNET_free (wrh); + return NULL; + } + + perform_protocol (curl_ctx, wrh); + + return wrh; +} + + +void +TALER_EXCHANGE_reveal_withdraw_cancel ( + struct TALER_EXCHANGE_RevealWithdrawHandle *wrh) +{ + if (NULL != wrh->job) + { + GNUNET_CURL_job_cancel (wrh->job); + wrh->job = NULL; + } + TALER_curl_easy_post_finished (&wrh->post_ctx); + + if (NULL != wrh->request_url) + GNUNET_free (wrh->request_url); + + GNUNET_free (wrh); +} + + +/* exchange_api_reveal_withdraw.c */ diff --git a/src/lib/exchange_api_withdraw.c b/src/lib/exchange_api_withdraw.c @@ -0,0 +1,1751 @@ +/* + This file is part of TALER + Copyright (C) 2023-2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_withdraw.c + * @brief Implementation of /withdraw requests + * @author Özgür Kesim + */ + +#include "platform.h" +#include <gnunet/gnunet_common.h> +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP status codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include <sys/wait.h> +#include "taler_curl_lib.h" +#include "taler_error_codes.h" +#include "taler_json_lib.h" +#include "taler_exchange_service.h" +#include "exchange_api_common.h" +#include "exchange_api_handle.h" +#include "taler_signatures.h" +#include "exchange_api_curl_defaults.h" +#include "taler_util.h" + +/** + * A CoinCandidate is populated from a master secret. + * The data is copied from and generated out of the client's input. + */ +struct CoinCandidate +{ + /** + * Master key material for the coin candidates. + */ + struct TALER_PlanchetMasterSecretP secret; + + /** + * The details derived form the master secrets + */ + struct TALER_EXCHANGE_WithdrawCoinPrivateDetails details; + + /** + * Blinded hash of the coin + **/ + struct TALER_BlindedCoinHashP blinded_coin_h; + +}; + + +/** + * Closure for a call to /blinding-prepare, contains data that is needed to process + * the result. + */ +struct BlindingPrepareClosure +{ + /** + * Number of coins in the blinding-prepare step. + * Not that this number might be smaller than the total number + * of coins in the withdraw, as the prepare is only necessary + * for CS denominations + */ + size_t num_prepare_coins; + + /** + * Array of @e num_prepare_coins of data per coin + */ + struct BlindingPrepareCoinData + { + /** + * Pointer to the candidate in CoinData.candidates, + * to continue to build its contents based on the results from /blinding-prepare + */ + struct CoinCandidate *candidate; + + /** + * Planchet to finally generate in the corresponding candidate + * in CoindData.planchet_details + */ + struct TALER_PlanchetDetail *planchet; + + /** + * Denomination information, needed for the + * step after /blinding-prepare + */ + const struct TALER_DenominationPublicKey *denom_pub; + } *coins; + + /** + * Array @a num of original nonces from the request, + * needs to be freed. + */ + union GNUNET_CRYPTO_BlindSessionNonce *nonces; + + /** + * Handler to the originating call to /withdraw, needed to either + * cancel the running withdraw request (on failure of the current call + * to /blinding-prepare), or to eventually perform the protocol, once all + * blinding-prepare requests have successfully finished. + */ + struct TALER_EXCHANGE_WithdrawHandle *withdraw_handle; + +}; + + +/** + * Data we keep per coin in the batch. + * This is copied from and generated out of the input provided + * by the client. + */ +struct CoinData +{ + /** + * The denomination of the coin. + */ + struct TALER_EXCHANGE_DenomPublicKey denom_pub; + + /** + * The Candidates for the coin. If the batch is not age-restricted, + * only index 0 is used. + */ + struct CoinCandidate candidates[TALER_CNC_KAPPA]; + + /** + * Details of the planchet(s). If the batch is not age-restricted, + * only index 0 is used. + */ + struct TALER_PlanchetDetail planchet_details[TALER_CNC_KAPPA]; +}; + + +/** + * A /withdraw request-handle for calls with pre-blinded planchets. + * Returned by TALER_EXCHANGE_withdraw_blinded. + */ +struct TALER_EXCHANGE_WithdrawBlindedHandle +{ + + /** + * Reserve private key. + */ + const struct TALER_ReservePrivateKeyP *reserve_priv; + + /** + * Reserve public key, calculated + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Signature of the reserve for the request, calculated after all + * parameters for the coins are collected. + */ + struct TALER_ReserveSignatureP reserve_sig; + + /* + * The denomination keys of the exchange + */ + struct TALER_EXCHANGE_Keys *keys; + + /** + * The hash of the withdraw commitment + */ + struct TALER_WithdrawCommitmentHashP h_commitment; + + /** + * The hash of the planchets + */ + struct TALER_HashBlindedPlanchetsP h_planchets; + + /** + * Total amount requested (without fee). + */ + struct TALER_Amount amount; + + /** + * Total withdraw fee + */ + struct TALER_Amount fee; + + /** + * Is this call for age-restriced coins, with age proof? + */ + bool with_age_proof; + + /** + * If @e with_age_proof is true or @max_age is > 0, + * the age mask to use, extracted from the denominations. + * MUST be the same for all denominations. + */ + struct TALER_AgeMask age_mask; + + /** + * The maximum age to commit to. If @e with_age_proof + * is true, the client will need to proof the correct setting + * of age-restriction on the coins via an additional call + * to /reveal-withdraw. + */ + uint8_t max_age; + + + /** + * Length of the either the @e blinded.input or + * the @e blinded.with_age_proof_input array, + * depending on @e with_age_proof. + */ + size_t num_input; + + union + { + /** + * The blinded planchet input candidates for age-restricted coins + * for the call to /withdraw + */ + const struct + TALER_EXCHANGE_WithdrawBlindedAgeRestrictedCoinInput *with_age_proof_input; + + /** + * The blinded planchet input for the call to /withdraw via + * TALER_EXCHANGE_withdraw_blinded, for age-unrestricted coins. + */ + const struct TALER_EXCHANGE_WithdrawBlindedCoinInput *input; + + } blinded; + + /** + * The url for this request. + */ + char *request_url; + + /** + * Context for curl. + */ + struct GNUNET_CURL_Context *curl_ctx; + + /** + * CURL handle for the request job. + */ + struct GNUNET_CURL_Job *job; + + /** + * Post Context + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Function to call with withdraw response results. + */ + TALER_EXCHANGE_WithdrawBlindedCallback callback; + + /** + * Closure for @e blinded_callback + */ + void *callback_cls; +}; + +/** + * A /withdraw request-handle for calls from + * a wallet, i. e. when blinding data is available. + */ +struct TALER_EXCHANGE_WithdrawHandle +{ + + /** + * The base-URL of the exchange. + */ + const char *exchange_url; + + /** + * Reserve private key. + */ + const struct TALER_ReservePrivateKeyP *reserve_priv; + + /** + * Reserve public key, calculated + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Signature of the reserve for the request, calculated after all + * parameters for the coins are collected. + */ + struct TALER_ReserveSignatureP reserve_sig; + + /* + * The denomination keys of the exchange + */ + struct TALER_EXCHANGE_Keys *keys; + + /** + * True, if the withdraw is for age-restricted coins, with age-proof. + * The denominations MUST support age restriction. + */ + bool with_age_proof; + + /** + * If @e with_age_proof is true, the age mask, extracted + * from the denominations. + * MUST be the same for all denominations. + * + */ + struct TALER_AgeMask age_mask; + + /** + * The maximum age to commit to. If @e with_age_proof + * is true, the client will need to proof the correct setting + * of age-restriction on the coins via an additional call + * to /reveal-withdraw. + */ + uint8_t max_age; + + /** + * Length of the @e coin_data Array + */ + size_t num_coins; + + /** + * Array of per-coin data + */ + struct CoinData *coin_data; + + /** + * Context for curl. + */ + struct GNUNET_CURL_Context *curl_ctx; + + /** + * Function to call with withdraw response results. + */ + TALER_EXCHANGE_WithdrawCallback callback; + + /** + * Closure for @e callback + */ + void *callback_cls; + + /* The handler for the call to /blinding-prepare, needed for CS denominations */ + struct TALER_EXCHANGE_BlindingPrepareHandle *blinding_prepare_handle; + + /* The Handler for the actual call to the exchange */ + struct TALER_EXCHANGE_WithdrawBlindedHandle *withdraw_blinded_handle; +}; + + +/** + * We got a 200 OK response for the /withdraw operation. + * Extract the signatures and return them to the caller. + * + * @param wbh operation handle + * @param j_response reply from the exchange + * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors + */ +static enum GNUNET_GenericReturnValue +withdraw_blinded_ok ( + struct TALER_EXCHANGE_WithdrawBlindedHandle *wbh, + const json_t *j_response) +{ + struct TALER_EXCHANGE_WithdrawBlindedResponse response = { + .hr.reply = j_response, + .hr.http_status = MHD_HTTP_OK, + }; + const json_t *j_sigs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("ev_sigs", + &j_sigs), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j_response, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + if (wbh->num_input != json_array_size (j_sigs)) + { + /* Number of coins generated does not match our expectation */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + { + struct TALER_BlindedDenominationSignature denoms_sig[wbh->num_input]; + json_t *j_sig; + size_t i; + + memset (denoms_sig, + 0, + sizeof(denoms_sig)); + + /* Reconstruct the coins and unblind the signatures */ + json_array_foreach (j_sigs, i, j_sig) + { + struct GNUNET_JSON_Specification ispec[] = { + TALER_JSON_spec_blinded_denom_sig (NULL, + &denoms_sig[i]), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j_sig, + ispec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + + response.details.ok.num_sigs = wbh->num_input; + response.details.ok.blinded_denom_sigs = denoms_sig; + response.details.ok.h_commitment = wbh->h_commitment; + wbh->callback ( + wbh->callback_cls, + &response); + /* Make sure the callback isn't called again */ + wbh->callback = NULL; + /* Free resources */ + for (size_t i = 0; i < wbh->num_input; i++) + TALER_blinded_denom_sig_free (&denoms_sig[i]); + } + + return GNUNET_OK; +} + + +/** + * We got a 201 CREATED response for the /withdraw operation. + * Extract the noreveal_index and return it to the caller. + * + * @param wbh operation handle + * @param j_response reply from the exchange + * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors + */ +static enum GNUNET_GenericReturnValue +withdraw_blinded_created ( + struct TALER_EXCHANGE_WithdrawBlindedHandle *wbh, + const json_t *j_response) +{ + struct TALER_EXCHANGE_WithdrawBlindedResponse response = { + .hr.reply = j_response, + .hr.http_status = MHD_HTTP_CREATED, + .details.created.h_commitment = wbh->h_commitment, + .details.created.num_coins = wbh->num_input, + }; + struct TALER_ExchangeSignatureP exchange_sig; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint8 ("noreveal_index", + &response.details.created.noreveal_index), + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + &response.details.created.exchange_pub), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK!= + GNUNET_JSON_parse (j_response, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + if (GNUNET_OK != + TALER_exchange_online_withdraw_age_confirmation_verify ( + &wbh->h_commitment, + response.details.created.noreveal_index, + &response.details.created.exchange_pub, + &exchange_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + + } + + wbh->callback (wbh->callback_cls, + &response); + /* make sure the callback isn't called again */ + wbh->callback = NULL; + + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the + * HTTP /withdraw request. + * + * @param cls the `struct TALER_EXCHANGE_WithdrawBlindedHandle` + * @param response_code The HTTP response code + * @param response response data + */ +static void +handle_withdraw_blinded_finished ( + void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_WithdrawBlindedHandle *wbh = cls; + const json_t *j_response = response; + struct TALER_EXCHANGE_WithdrawBlindedResponse wbr = { + .hr.reply = j_response, + .hr.http_status = (unsigned int) response_code + }; + + wbh->job = NULL; + switch (response_code) + { + case 0: + wbr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + { + if (GNUNET_OK != + withdraw_blinded_ok ( + wbh, + j_response)) + { + GNUNET_break_op (0); + wbr.hr.http_status = 0; + wbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + GNUNET_assert (NULL == wbh->callback); + TALER_EXCHANGE_withdraw_blinded_cancel (wbh); + return; + } + case MHD_HTTP_CREATED: + if (GNUNET_OK != + withdraw_blinded_created ( + wbh, + j_response)) + { + GNUNET_break_op (0); + wbr.hr.http_status = 0; + wbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + GNUNET_assert (NULL == wbh->callback); + TALER_EXCHANGE_withdraw_blinded_cancel (wbh); + return; + case MHD_HTTP_BAD_REQUEST: + /* This should never happen, either us or the exchange is buggy + (or API version conflict); just pass JSON reply to the application */ + wbr.hr.ec = TALER_JSON_get_error_code (j_response); + wbr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + case MHD_HTTP_FORBIDDEN: + GNUNET_break_op (0); + /* Nothing really to verify, exchange says one of the signatures is + invalid; as we checked them, this should never happen, we + should pass the JSON reply to the application */ + wbr.hr.ec = TALER_JSON_get_error_code (j_response); + wbr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify, the exchange basically just says + that it doesn't know this reserve. Can happen if we + query before the wire transfer went through. + We should simply pass the JSON reply to the application. */ + wbr.hr.ec = TALER_JSON_get_error_code (j_response); + wbr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + case MHD_HTTP_CONFLICT: + /* The age requirements might not have been met */ + wbr.hr.ec = TALER_JSON_get_error_code (j_response); + wbr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + case MHD_HTTP_GONE: + /* could happen if denomination was revoked */ + /* Note: one might want to check /keys for revocation + signature here, alas tricky in case our /keys + is outdated => left to clients */ + wbr.hr.ec = TALER_JSON_get_error_code (j_response); + wbr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: + /* only validate reply is well-formed */ + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ( + "h_payto", + &wbr.details.unavailable_for_legal_reasons.h_payto), + GNUNET_JSON_spec_uint64 ( + "requirement_row", + &wbr.details.unavailable_for_legal_reasons.requirement_row), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (j_response, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + wbr.hr.http_status = 0; + wbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + break; + } + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + wbr.hr.ec = TALER_JSON_get_error_code (j_response); + wbr.hr.hint = TALER_JSON_get_error_hint (j_response); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + wbr.hr.ec = TALER_JSON_get_error_code (j_response); + wbr.hr.hint = TALER_JSON_get_error_hint (j_response); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange withdraw\n", + (unsigned int) response_code, + (int) wbr.hr.ec); + break; + } + wbh->callback (wbh->callback_cls, + &wbr); + TALER_EXCHANGE_withdraw_blinded_cancel (wbh); +} + + +/** + * Runs the actual withdraw operation with the blinded planchets. + * + * @param[in,out] wbh withdraw blinded handle + */ +static void +perform_withdraw_protocol ( + struct TALER_EXCHANGE_WithdrawBlindedHandle *wbh) +{ +#define FAIL_IF(cond) \ + do { \ + if ((cond)) \ + { \ + GNUNET_break (! (cond)); \ + goto ERROR; \ + } \ + } while (0) + + json_t *j_denoms = NULL; + json_t *j_planchets = NULL; + json_t *j_request_body = NULL; + CURL *curlh = NULL; + struct GNUNET_HashContext *coins_hctx = NULL; + struct TALER_BlindedCoinHashP bch; + + GNUNET_assert (0 < wbh->num_input); + + FAIL_IF (GNUNET_OK != + TALER_amount_set_zero (wbh->keys->currency, + &wbh->amount)); + FAIL_IF (GNUNET_OK != + TALER_amount_set_zero (wbh->keys->currency, + &wbh->fee)); + + /* Accumulate total value with fees */ + for (size_t i = 0; i < wbh->num_input; i++) + { + const struct TALER_EXCHANGE_DenomPublicKey *dpub = + wbh->with_age_proof ? + wbh->blinded.with_age_proof_input[i].denom_pub : + wbh->blinded.input[i].denom_pub; + + FAIL_IF (0 > + TALER_amount_add (&wbh->amount, + &wbh->amount, + &dpub->value)); + FAIL_IF (0 > + TALER_amount_add (&wbh->fee, + &wbh->fee, + &dpub->fees.withdraw)); + } + + if (wbh->with_age_proof || wbh->max_age > 0) + { + wbh->age_mask = + wbh->blinded.with_age_proof_input[0].denom_pub->key.age_mask; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Attempting to withdraw from reserve %s with maximum age %d to proof\n", + TALER_B2S (&wbh->reserve_pub), + wbh->max_age); + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Attempting to withdraw from reserve %s\n", + TALER_B2S (&wbh->reserve_pub)); + } + + coins_hctx = GNUNET_CRYPTO_hash_context_start (); + FAIL_IF (NULL == coins_hctx); + + j_denoms = json_array (); + j_planchets = json_array (); + FAIL_IF ((NULL == j_denoms) || + (NULL == j_planchets)); + + for (size_t i = 0; i< wbh->num_input; i++) + { + /* Build the denomination array */ + const struct TALER_EXCHANGE_DenomPublicKey *denom_pub = + wbh->with_age_proof ? + wbh->blinded.with_age_proof_input[i].denom_pub : + wbh->blinded.input[i].denom_pub; + const struct TALER_DenominationHashP *denom_h = &denom_pub->h_key; + json_t *jdenom; + + /* The mask must be the same for all coins */ + FAIL_IF (wbh->with_age_proof && + (wbh->age_mask.bits != denom_pub->key.age_mask.bits)); + + jdenom = GNUNET_JSON_from_data_auto (denom_h); + FAIL_IF (NULL == jdenom); + FAIL_IF (0 > json_array_append_new (j_denoms, + jdenom)); + } + + + /* Build the planchet array and calculate the hash over all planchets. */ + if (! wbh->with_age_proof) + { + for (size_t i = 0; i< wbh->num_input; i++) + { + const struct TALER_PlanchetDetail *planchet = + &wbh->blinded.input[i].planchet_details; + json_t *jc = GNUNET_JSON_PACK ( + TALER_JSON_pack_blinded_planchet ( + NULL, + &planchet->blinded_planchet)); + FAIL_IF (NULL == jc); + FAIL_IF (0 > json_array_append_new (j_planchets, + jc)); + + TALER_coin_ev_hash (&planchet->blinded_planchet, + &planchet->denom_pub_hash, + &bch); + + GNUNET_CRYPTO_hash_context_read (coins_hctx, + &bch, + sizeof(bch)); + } + } + else + { /* Age restricted case with required age-proof. */ + + /** + * We collect the run of all coin candidates for the same γ index + * first, then γ+1 etc. + */ + for (size_t k = 0; k < TALER_CNC_KAPPA; k++) + { + for (size_t i = 0; i< wbh->num_input; i++) + { + const struct TALER_PlanchetDetail *planchet = + &wbh->blinded.with_age_proof_input[i].planchet_details[k]; + json_t *jc = GNUNET_JSON_PACK ( + TALER_JSON_pack_blinded_planchet ( + NULL, + &planchet->blinded_planchet)); + + FAIL_IF (NULL == jc); + FAIL_IF (0 > json_array_append_new ( + j_planchets, + jc)); + + TALER_coin_ev_hash ( + &planchet->blinded_planchet, + &planchet->denom_pub_hash, + &bch); + + GNUNET_CRYPTO_hash_context_read ( + coins_hctx, + &bch, + sizeof(bch)); + + } + } + } + + /* Build the hash of the planchets */ + GNUNET_CRYPTO_hash_context_finish ( + coins_hctx, + &wbh->h_planchets.hash); + coins_hctx = NULL; + + /** + * Calculate the commitment, needed when verify the response, + * or - in case of age restriction with required proof - a subsequent + * call to /reveal-withdraw + */ + TALER_wallet_withdraw_commit ( + &wbh->reserve_pub, + &wbh->amount, + &wbh->fee, + &wbh->h_planchets, + wbh->with_age_proof ? &wbh->age_mask : NULL, + wbh->with_age_proof ? wbh->max_age : 0, + &wbh->h_commitment); + + /* Sign the request */ + TALER_wallet_withdraw_sign ( + &wbh->amount, + &wbh->fee, + &wbh->h_planchets, + wbh->with_age_proof ? &wbh->age_mask: NULL, + wbh->with_age_proof ? wbh->max_age : 0, + wbh->reserve_priv, + &wbh->reserve_sig); + + /* Initiate the POST-request */ + j_request_body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("cipher", + "ED25519"), + GNUNET_JSON_pack_data_auto ("reserve_pub", + &wbh->reserve_pub), + GNUNET_JSON_pack_array_steal ("denoms_h", + j_denoms), + GNUNET_JSON_pack_array_steal ("coin_evs", + j_planchets), + GNUNET_JSON_pack_allow_null ( + wbh->with_age_proof + ? GNUNET_JSON_pack_int64 ("max_age", + wbh->max_age) + : GNUNET_JSON_pack_string ("max_age", + NULL) ), + GNUNET_JSON_pack_data_auto ("reserve_sig", + &wbh->reserve_sig)); + FAIL_IF (NULL == j_request_body); + + curlh = TALER_EXCHANGE_curl_easy_get_ (wbh->request_url); + FAIL_IF (NULL == curlh); + FAIL_IF (GNUNET_OK != + TALER_curl_easy_post ( + &wbh->post_ctx, + curlh, + j_request_body)); + json_decref (j_request_body); + j_request_body = NULL; + + wbh->job = GNUNET_CURL_job_add2 ( + wbh->curl_ctx, + curlh, + wbh->post_ctx.headers, + &handle_withdraw_blinded_finished, + wbh); + FAIL_IF (NULL == wbh->job); + + /* No errors, return */ + return; + +ERROR: + if (NULL != coins_hctx) + GNUNET_CRYPTO_hash_context_abort (coins_hctx); + if (NULL != j_denoms) + json_decref (j_denoms); + if (NULL != j_planchets) + json_decref (j_planchets); + if (NULL != j_request_body) + json_decref (j_request_body); + if (NULL != curlh) + curl_easy_cleanup (curlh); + TALER_EXCHANGE_withdraw_blinded_cancel (wbh); + return; +#undef FAIL_IF +} + + +/** + * @brief Callback to copy the results from the call to TALER_withdraw_blinded + * in the non-age-restricted case to the result for the originating call from TALER_withdraw. + * + * @param cls struct TALER_WithdrawHandle + * @param wbr The response + */ +static void +copy_results ( + void *cls, + const struct TALER_EXCHANGE_WithdrawBlindedResponse *wbr) +{ + /* The original handle from the top-level call to withdraw */ + struct TALER_EXCHANGE_WithdrawHandle *wh = cls; + struct TALER_EXCHANGE_WithdrawResponse resp = { + .hr = wbr->hr, + }; + struct TALER_EXCHANGE_WithdrawCoinPrivateDetails + details[GNUNET_NZL (wh->num_coins)]; + + wh->withdraw_blinded_handle = NULL; + + /** + * The withdraw protocol has been performed with blinded data. + * Now the response can be copied as is, except for the MHD_HTTP_OK case, + * in which we now need to perform the unblinding. + */ + switch (wbr->hr.http_status) + { + case MHD_HTTP_OK: + { + + GNUNET_assert (wh->num_coins == wbr->details.ok.num_sigs); + + resp.details.ok.num_sigs = wbr->details.ok.num_sigs; + resp.details.ok.coin_details = details; + resp.details.ok.h_commitment = wbr->details.ok.h_commitment; + memset (details, + 0, + sizeof(details)); + + for (size_t n = 0; n< wh->num_coins; n++) + { + const struct TALER_BlindedDenominationSignature *bsig = + &wbr->details.ok.blinded_denom_sigs[n]; + struct CoinData *cd = &wh->coin_data[n]; + struct TALER_EXCHANGE_WithdrawCoinPrivateDetails *coin = &details[n]; + struct TALER_FreshCoin fc; + + *coin = wh->coin_data[n].candidates[0].details; + coin->planchet = wh->coin_data[n].planchet_details[0]; + GNUNET_CRYPTO_eddsa_key_get_public ( + &coin->coin_priv.eddsa_priv, + &coin->coin_pub.eddsa_pub); + + if (GNUNET_OK != + TALER_planchet_to_coin (&cd->denom_pub.key, + bsig, + &coin->blinding_key, + &coin->coin_priv, + &coin->h_age_commitment, + &coin->h_coin_pub, + &coin->alg_values, + &fc)) + { + resp.hr.http_status = 0; + resp.hr.ec = TALER_EC_EXCHANGE_WITHDRAW_UNBLIND_FAILURE; + GNUNET_break_op (0); + break; + } + coin->denom_sig = fc.sig; + + } + break; + } + + case MHD_HTTP_CREATED: + resp.details.created = wbr->details.created; + break; + + case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: + resp.details.unavailable_for_legal_reasons = + wbr->details.unavailable_for_legal_reasons; + break; + + default: + /* nothing to do here, .hr.ec and .hr.hint are all set already from previous response */ + break; + } + + wh->callback ( + wh->callback_cls, + &resp); + + wh->callback = NULL; + TALER_EXCHANGE_withdraw_cancel (wh); +} + + +/** + * @brief Callback to copy the results from the call to TALER_withdraw_blinded + * in the age-restricted case to the result for the originating call from TALER_withdraw. + * + * @param cls struct TALER_WithdrawHandle + * @param wbr The response + */ +static void +copy_results_with_age_proof ( + void *cls, + const struct TALER_EXCHANGE_WithdrawBlindedResponse *wbr) +{ + /* The original handle from the top-level call to withdraw */ + struct TALER_EXCHANGE_WithdrawHandle *wh = cls; + uint8_t k = wbr->details.created.noreveal_index; + struct TALER_EXCHANGE_WithdrawCoinPrivateDetails details[wh->num_coins]; + struct TALER_EXCHANGE_WithdrawResponse resp = { + .hr = wbr->hr, + }; + + wh->withdraw_blinded_handle = NULL; + + switch (wbr->hr.http_status) + { + case MHD_HTTP_OK: + /* in the age-restricted case, this should not happen */ + GNUNET_break_op (0); + break; + + case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: + resp.details.unavailable_for_legal_reasons = + wbr->details.unavailable_for_legal_reasons; + break; + + case MHD_HTTP_CREATED: + { + GNUNET_assert (wh->num_coins == wbr->details.created.num_coins); + resp.details.created = wbr->details.created; + resp.details.created.coin_details = details; + memset (details, + 0, + sizeof(details)); + for (size_t n = 0; n< wh->num_coins; n++) + { + details[n] = wh->coin_data[n].candidates[k].details; + details[n].planchet = wh->coin_data[n].planchet_details[k]; + } + break; + } + + default: + break; + } + + wh->callback ( + wh->callback_cls, + &resp); + wh->callback = NULL; + TALER_EXCHANGE_withdraw_cancel (wh); +} + + +/** + * @brief Prepares and executes TALER_EXCHANGE_withdraw_blinded. + * If there were CS-denominations involved, started once the all calls + * to /blinding-prepare are done. + */ +static void +call_withdraw_blinded ( + struct TALER_EXCHANGE_WithdrawHandle *wh) +{ + + GNUNET_assert (NULL == wh->blinding_prepare_handle); + + if (! wh->with_age_proof) + { + struct TALER_EXCHANGE_WithdrawBlindedCoinInput input[wh->num_coins]; + + memset (input, + 0, + sizeof(input)); + + /* Prepare the blinded planchets as input */ + for (size_t n = 0; n < wh->num_coins; n++) + { + input[n].denom_pub = + &wh->coin_data[n].denom_pub; + input[n].planchet_details = + *wh->coin_data[n].planchet_details; + } + + wh->withdraw_blinded_handle = + TALER_EXCHANGE_withdraw_blinded ( + wh->curl_ctx, + wh->keys, + wh->exchange_url, + wh->reserve_priv, + wh->num_coins, + input, + &copy_results, + wh); + } + else + { /* age restricted case */ + struct TALER_EXCHANGE_WithdrawBlindedAgeRestrictedCoinInput + ari[wh->num_coins]; + + memset (ari, + 0, + sizeof(ari)); + + /* Prepare the blinded planchets as input */ + for (size_t n = 0; n < wh->num_coins; n++) + { + ari[n].denom_pub = &wh->coin_data[n].denom_pub; + for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) + ari[n].planchet_details[k] = + wh->coin_data[n].planchet_details[k]; + } + + wh->withdraw_blinded_handle = + TALER_EXCHANGE_withdraw_blinded_with_age_proof ( + wh->curl_ctx, + wh->keys, + wh->exchange_url, + wh->reserve_priv, + wh->max_age, + wh->num_coins, + ari, + &copy_results_with_age_proof, + wh); + } +} + + +/** + * @brief Function called when /blinding-prepare is finished + * + * @param cls the `struct BlindingPrepareClosure *` + * @param bpr replies from the /blinding-prepare request + */ +static void +blinding_prepare_done ( + void *cls, + const struct TALER_EXCHANGE_BlindingPrepareResponse *bpr) +{ + struct BlindingPrepareClosure *bpcls = cls; + struct TALER_EXCHANGE_WithdrawHandle *wh; + + GNUNET_assert (NULL != bpcls); + wh = bpcls->withdraw_handle; + GNUNET_assert (NULL != wh); + + wh->blinding_prepare_handle = NULL; + switch (bpr->hr.http_status) + { + case MHD_HTTP_OK: + { + bool success = false; + size_t num = bpr->details.ok.num; + GNUNET_assert (0 != num); + GNUNET_assert (num == bpcls->num_prepare_coins); + + for (size_t i = 0; i < num; i++) + { + struct TALER_PlanchetDetail *planchet = bpcls->coins[i].planchet; + struct CoinCandidate *can = bpcls->coins[i].candidate; + + GNUNET_assert (NULL != can); + GNUNET_assert (NULL != planchet); + success = false; + + /* Complete the initialization of the coin with CS denomination */ + TALER_denom_ewv_copy ( + &can->details.alg_values, + &bpr->details.ok.alg_values[i]); + + GNUNET_assert (GNUNET_CRYPTO_BSA_CS == + can->details.alg_values.blinding_inputs->cipher); + + TALER_planchet_setup_coin_priv ( + &can->secret, + &can->details.alg_values, + &can->details.coin_priv); + + TALER_planchet_blinding_secret_create ( + &can->secret, + &can->details.alg_values, + &can->details.blinding_key); + + /* This initializes the 2nd half of the + can->planchet_detail.blinded_planchet */ + if (GNUNET_OK != + TALER_planchet_prepare ( + bpcls->coins[i].denom_pub, + &can->details.alg_values, + &can->details.blinding_key, + &bpcls->nonces[i], + &can->details.coin_priv, + &can->details.h_age_commitment, + &can->details.h_coin_pub, + planchet)) + { + GNUNET_break (0); + break; + } + + TALER_coin_ev_hash (&planchet->blinded_planchet, + &planchet->denom_pub_hash, + &can->blinded_coin_h); + success = true; + } + + GNUNET_free (bpcls->coins); + GNUNET_free (bpcls->nonces); + + /* /blinding-prepare is done, we can now perform the + * actual withdraw operation */ + if (success) + call_withdraw_blinded (wh); + return; + } + default: + { + /* We got an error condition during blinding prepare that we need to report */ + struct TALER_EXCHANGE_WithdrawResponse resp = { + .hr = bpr->hr + }; + + wh->callback ( + wh->callback_cls, + &resp); + + wh->callback = NULL; + break; + } + } + TALER_EXCHANGE_withdraw_cancel (wh); +} + + +/** + * @brief Prepares non age-restricted coins for the call to withdraw and + * calculates the total amount with fees. + * For denomination with CS as cipher, initiates the preflight to retrieve the + * bpcls-parameter via /blinding-prepare. + * Note that only one of the three parameters seed, tuples or secrets must not be NULL + * + * @param wh The handler to the withdraw + * @param num_coins Number of coins to withdraw + * @param max_age The maximum age to commit to + * @param denoms_pub Array @e num_coins of denominations + * @param tuples tuple of seeds to derive @e num_coins secrets for age-restricted coins, might be NULL + * @param secrets Array of @e num_coins planchet secrets for the coins, might be NULL + * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure + */ +static enum GNUNET_GenericReturnValue +prepare_coins ( + struct TALER_EXCHANGE_WithdrawHandle *wh, + size_t num_coins, + uint8_t max_age, + const struct TALER_EXCHANGE_DenomPublicKey *denoms_pub, + const struct TALER_KappaWithdrawMasterSeedP *tuples, + const struct TALER_PlanchetMasterSecretP *secrets) +{ + size_t cs_num = 0; + struct BlindingPrepareClosure *cs_closure = NULL; + uint8_t kappa; + +#define FAIL_IF(cond) \ + do \ + { \ + if ((cond)) \ + { \ + GNUNET_break (! (cond)); \ + goto ERROR; \ + } \ + } while (0) + + GNUNET_assert ( + ((NULL != tuples) && (NULL == secrets)) || + ((NULL == tuples) && (NULL != secrets))); + GNUNET_assert (0 < num_coins); + + wh->num_coins = num_coins; + wh->max_age = max_age; + wh->age_mask = denoms_pub[0].key.age_mask; + wh->coin_data = GNUNET_new_array ( + wh->num_coins, + struct CoinData); + + /* First, figure out how many Clause-Schnorr denominations we have */ + for (size_t i =0; i< wh->num_coins; i++) + { + if (GNUNET_CRYPTO_BSA_CS == + denoms_pub[i].key.bsign_pub_key->cipher) + cs_num++; + } + + if (wh->with_age_proof) + { + kappa = TALER_CNC_KAPPA; + cs_num *= TALER_CNC_KAPPA; + } + else + { + kappa = 1; + } + + { + size_t cs_cnt = 0; + struct TALER_DenominationHashP cs_denoms_h[cs_num > 0 ? cs_num : 1]; + struct TALER_PlanchetMasterSecretP kappa_secrets[kappa][num_coins]; + + if (wh->with_age_proof) + { + GNUNET_assert (NULL != tuples); + for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) + { + TALER_expand_withdraw_secrets ( + num_coins, + &tuples->tuple[k], + kappa_secrets[k]); + } + } + + if (0 < cs_num) + { + memset (cs_denoms_h, + 0, + sizeof(cs_denoms_h)); + cs_closure = GNUNET_new (struct BlindingPrepareClosure); + cs_closure->num_prepare_coins = cs_num; + cs_closure->withdraw_handle = wh; + cs_closure->coins = + GNUNET_new_array (cs_num, + struct BlindingPrepareCoinData); + cs_closure->nonces = + GNUNET_new_array (cs_num, + union GNUNET_CRYPTO_BlindSessionNonce); + } + + for (size_t i = 0; i < wh->num_coins; i++) + { + struct CoinData *cd = &wh->coin_data[i]; + + cd->denom_pub = denoms_pub[i]; + /* The age mask must be the same for all coins */ + FAIL_IF (wh->with_age_proof && + (0 == denoms_pub[i].key.age_mask.bits)); + FAIL_IF (wh->age_mask.bits != + denoms_pub[i].key.age_mask.bits); + TALER_denom_pub_copy (&cd->denom_pub.key, + &denoms_pub[i].key); + + /* + * Note that we "loop" here either only once (if with_age_proof is false), + * or TALER_CNC_KAPPA times. + */ + for (uint8_t k = 0; k < kappa; k++) + { + struct CoinCandidate *can = &cd->candidates[k]; + struct TALER_PlanchetDetail *planchet = &cd->planchet_details[k]; + + if (wh->with_age_proof) + can->secret = kappa_secrets[k][i]; + else + can->secret = secrets[i]; + + /* + * The age restriction needs to be set on a coin if the denomination + * support age restriction. Note that his is regardless of weither + * with_age_proof is set or not. + */ + if (0 != wh->age_mask.bits) + { + /* Derive the age restriction from the given secret and + * the maximum age */ + TALER_age_restriction_from_secret ( + &can->secret, + &wh->age_mask, + wh->max_age, + &can->details.age_commitment_proof); + + TALER_age_commitment_hash ( + &can->details.age_commitment_proof.commitment, + &can->details.h_age_commitment); + } + + switch (cd->denom_pub.key.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + TALER_denom_ewv_copy (&can->details.alg_values, + TALER_denom_ewv_rsa_singleton ()); + TALER_planchet_setup_coin_priv (&can->secret, + &can->details.alg_values, + &can->details.coin_priv); + TALER_planchet_blinding_secret_create (&can->secret, + &can->details.alg_values, + &can->details.blinding_key); + FAIL_IF (GNUNET_OK != + TALER_planchet_prepare (&cd->denom_pub.key, + &can->details.alg_values, + &can->details.blinding_key, + NULL, + &can->details.coin_priv, + &can->details.h_age_commitment, + &can->details.h_coin_pub, + planchet)); + TALER_coin_ev_hash (&planchet->blinded_planchet, + &planchet->denom_pub_hash, + &can->blinded_coin_h); + + break; + + case GNUNET_CRYPTO_BSA_CS: + { + /** + * Prepare the nonce and save the index and the denomination for the callback + * after the call to blinding-prepare + */ + + cs_closure->coins[cs_cnt].candidate = can; + cs_closure->coins[cs_cnt].planchet = planchet; + cs_closure->coins[cs_cnt].denom_pub = &cd->denom_pub.key; + cs_denoms_h[cs_cnt] = cd->denom_pub.h_key; + + TALER_cs_withdraw_nonce_derive ( + &can->secret, + &cs_closure->nonces[cs_cnt].cs_nonce); + + cs_cnt++; + break; + } + default: + FAIL_IF (1); + } + } + } + + if (0 < cs_num) + { + wh->blinding_prepare_handle = + TALER_EXCHANGE_blinding_prepare ( + wh->curl_ctx, + wh->exchange_url, + cs_num, + cs_closure->nonces, + cs_denoms_h, + &blinding_prepare_done, + cs_closure); + FAIL_IF (NULL == wh->blinding_prepare_handle); + } + } + return GNUNET_OK; + +ERROR: + if (0<cs_num) + { + GNUNET_free (cs_closure->nonces); + GNUNET_free (cs_closure); + } + TALER_EXCHANGE_withdraw_cancel (wh); + return GNUNET_SYSERR; +#undef FAIL_IF + +} + + +/** + * Prepare a withdraw handle for both, the non-restricted + * and age-restricted case. + * + * @param curl_ctx The curl context to use + * @param keys The keys from the exchange + * @param exchange_url The base url to the exchange + * @param reserve_priv The private key of the exchange + * @param res_cb The callback to call on response + * @param res_cb_cls The closure to pass to the callback + */ +static struct TALER_EXCHANGE_WithdrawHandle * +setup_withdraw_handle ( + struct GNUNET_CURL_Context *curl_ctx, + struct TALER_EXCHANGE_Keys *keys, + const char *exchange_url, + const struct TALER_ReservePrivateKeyP *reserve_priv, + TALER_EXCHANGE_WithdrawCallback res_cb, + void *res_cb_cls) +{ + struct TALER_EXCHANGE_WithdrawHandle *wh; + + wh = GNUNET_new (struct TALER_EXCHANGE_WithdrawHandle); + wh->exchange_url = exchange_url; + wh->keys = TALER_EXCHANGE_keys_incref (keys); + wh->curl_ctx = curl_ctx; + wh->reserve_priv = reserve_priv; + wh->callback = res_cb; + wh->callback_cls = res_cb_cls; + + return wh; +} + + +struct TALER_EXCHANGE_WithdrawHandle * +TALER_EXCHANGE_withdraw ( + struct GNUNET_CURL_Context *curl_ctx, + struct TALER_EXCHANGE_Keys *keys, + const char *exchange_url, + const struct TALER_ReservePrivateKeyP *reserve_priv, + size_t num_coins, + uint8_t opaque_max_age, + const struct TALER_EXCHANGE_DenomPublicKey denoms_pub[static num_coins], + const struct TALER_WithdrawMasterSeedP *seed, + TALER_EXCHANGE_WithdrawCallback res_cb, + void *res_cb_cls) +{ + struct TALER_EXCHANGE_WithdrawHandle *wh; + struct TALER_PlanchetMasterSecretP secrets[num_coins]; + + TALER_expand_withdraw_secrets ( + num_coins, + seed, + secrets); + + wh = setup_withdraw_handle (curl_ctx, + keys, + exchange_url, + reserve_priv, + res_cb, + res_cb_cls); + GNUNET_assert (NULL != wh); + wh->with_age_proof = false; + + if (GNUNET_OK != + prepare_coins (wh, + num_coins, + opaque_max_age, + denoms_pub, + NULL, + secrets)) + { + GNUNET_free (wh); + return NULL; + } + + /* If there were no CS denominations, we can now perform the actual + * withdraw protocol. Otherwise, there are calls to /blinding-prepare + * in flight and once they finish, the withdraw-protocol will be + * called from within the blinding_prepare_done-function. + */ + if (NULL == wh->blinding_prepare_handle) + call_withdraw_blinded (wh); + + return wh; +} + + +struct TALER_EXCHANGE_WithdrawHandle * +TALER_EXCHANGE_withdraw_with_age_proof ( + struct GNUNET_CURL_Context *curl_ctx, + struct TALER_EXCHANGE_Keys *keys, + const char *exchange_url, + const struct TALER_ReservePrivateKeyP *reserve_priv, + uint8_t max_age, + size_t num_coins, + const struct TALER_EXCHANGE_DenomPublicKey denoms_pub[static num_coins], + const struct TALER_KappaWithdrawMasterSeedP *seeds, + TALER_EXCHANGE_WithdrawCallback res_cb, + void *res_cb_cls) +{ + struct TALER_EXCHANGE_WithdrawHandle *wh; + + wh = setup_withdraw_handle (curl_ctx, + keys, + exchange_url, + reserve_priv, + res_cb, + res_cb_cls); + GNUNET_assert (NULL != wh); + + wh->with_age_proof = true; + + if (GNUNET_OK != + prepare_coins (wh, + num_coins, + max_age, + denoms_pub, + seeds, + NULL)) + { + GNUNET_free (wh); + return NULL; + } + + /* If there were no CS denominations, we can now perform the actual + * withdraw protocol. Otherwise, there are calls to /blinding-prepare + * in flight and once they finish, the withdraw-protocol will be + * called from within the blinding_prepare_done-function. + */ + if (NULL == wh->blinding_prepare_handle) + call_withdraw_blinded (wh); + + return wh; +} + + +void +TALER_EXCHANGE_withdraw_cancel ( + struct TALER_EXCHANGE_WithdrawHandle *wh) +{ + uint8_t kappa = wh->with_age_proof ? TALER_CNC_KAPPA : 1; + + /* Cleanup coin data */ + for (unsigned int i = 0; i<wh->num_coins; i++) + { + struct CoinData *cd = &wh->coin_data[i]; + + for (uint8_t k = 0; k < kappa; k++) + { + struct TALER_PlanchetDetail *planchet = &cd->planchet_details[k]; + struct CoinCandidate *can = &cd->candidates[k]; + + TALER_blinded_planchet_free (&planchet->blinded_planchet); + TALER_denom_ewv_free (&can->details.alg_values); + TALER_age_commitment_proof_free (&can->details.age_commitment_proof); + } + TALER_denom_pub_free (&cd->denom_pub.key); + } + + TALER_EXCHANGE_blinding_prepare_cancel (wh->blinding_prepare_handle); + TALER_EXCHANGE_withdraw_blinded_cancel (wh->withdraw_blinded_handle); + wh->blinding_prepare_handle = NULL; + wh->withdraw_blinded_handle = NULL; + + GNUNET_free (wh->coin_data); + TALER_EXCHANGE_keys_decref (wh->keys); + GNUNET_free (wh); +} + + +/** + * @brief Prepare the handler for blinded withdraw + * + * Allocates the handler struct and prepares all fields of the handler + * except the blinded planchets, + * which depend on them being age-restricted or not. + * + * @param curl_ctx the context for curl + * @param keys the exchange keys + * @param exchange_url the url to the exchange + * @param reserve_priv the reserve's private key + * @param res_cb the callback on result + * @param res_cb_cls the closure to pass on to the callback + * @return the handler + */ +static struct TALER_EXCHANGE_WithdrawBlindedHandle * +setup_handler_common ( + struct GNUNET_CURL_Context *curl_ctx, + struct TALER_EXCHANGE_Keys *keys, + const char *exchange_url, + const struct TALER_ReservePrivateKeyP *reserve_priv, + TALER_EXCHANGE_WithdrawBlindedCallback res_cb, + void *res_cb_cls) +{ + + struct TALER_EXCHANGE_WithdrawBlindedHandle *wbh = + GNUNET_new (struct TALER_EXCHANGE_WithdrawBlindedHandle); + + wbh->keys = TALER_EXCHANGE_keys_incref (keys); + wbh->curl_ctx = curl_ctx; + wbh->reserve_priv = reserve_priv; + wbh->callback = res_cb; + wbh->callback_cls = res_cb_cls; + wbh->request_url = TALER_url_join (exchange_url, + "withdraw", + NULL); + GNUNET_CRYPTO_eddsa_key_get_public ( + &wbh->reserve_priv->eddsa_priv, + &wbh->reserve_pub.eddsa_pub); + + return wbh; +} + + +struct TALER_EXCHANGE_WithdrawBlindedHandle * +TALER_EXCHANGE_withdraw_blinded ( + struct GNUNET_CURL_Context *curl_ctx, + struct TALER_EXCHANGE_Keys *keys, + const char *exchange_url, + const struct TALER_ReservePrivateKeyP *reserve_priv, + size_t num_input, + const struct TALER_EXCHANGE_WithdrawBlindedCoinInput + blinded_input[static num_input], + TALER_EXCHANGE_WithdrawBlindedCallback res_cb, + void *res_cb_cls) +{ + struct TALER_EXCHANGE_WithdrawBlindedHandle *wbh = + setup_handler_common (curl_ctx, + keys, + exchange_url, + reserve_priv, + res_cb, + res_cb_cls); + + wbh->with_age_proof = false; + wbh->num_input = num_input; + wbh->blinded.input = blinded_input; + + perform_withdraw_protocol (wbh); + return wbh; +} + + +struct TALER_EXCHANGE_WithdrawBlindedHandle * +TALER_EXCHANGE_withdraw_blinded_with_age_proof ( + struct GNUNET_CURL_Context *curl_ctx, + struct TALER_EXCHANGE_Keys *keys, + const char *exchange_url, + const struct TALER_ReservePrivateKeyP *reserve_priv, + uint8_t max_age, + unsigned int num_input, + const struct TALER_EXCHANGE_WithdrawBlindedAgeRestrictedCoinInput + blinded_input[static num_input], + TALER_EXCHANGE_WithdrawBlindedCallback res_cb, + void *res_cb_cls) +{ + struct TALER_EXCHANGE_WithdrawBlindedHandle *wbh = + setup_handler_common (curl_ctx, + keys, + exchange_url, + reserve_priv, + res_cb, + res_cb_cls); + + wbh->with_age_proof = true; + wbh->max_age = max_age; + wbh->num_input = num_input; + wbh->blinded.with_age_proof_input = blinded_input; + + perform_withdraw_protocol (wbh); + return wbh; +} + + +void +TALER_EXCHANGE_withdraw_blinded_cancel ( + struct TALER_EXCHANGE_WithdrawBlindedHandle *wbh) +{ + if (NULL == wbh) + return; + if (NULL != wbh->job) + { + GNUNET_CURL_job_cancel (wbh->job); + wbh->job = NULL; + } + GNUNET_free (wbh->request_url); + TALER_EXCHANGE_keys_decref (wbh->keys); + TALER_curl_easy_post_finished (&wbh->post_ctx); + GNUNET_free (wbh); +} + + +/* exchange_api_withdraw.c */ diff --git a/src/testing/Makefile.am b/src/testing/Makefile.am @@ -159,8 +159,6 @@ check_PROGRAMS = \ test_exchange_api_rsa \ test_exchange_api_age_restriction_cs \ test_exchange_api_age_restriction_rsa \ - test_exchange_api_conflicts_cs \ - test_exchange_api_conflicts_rsa \ test_exchange_api_keys_cherry_picking_cs \ test_exchange_api_keys_cherry_picking_rsa \ test_exchange_api_revocation_cs \ @@ -316,38 +314,6 @@ test_exchange_api_age_restriction_rsa_LDADD = \ -ljansson \ $(XLIB) -test_exchange_api_conflicts_cs_SOURCES = \ - test_exchange_api_conflicts.c -test_exchange_api_conflicts_cs_LDADD = \ - libtalertesting.la \ - $(top_builddir)/src/lib/libtalerexchange.la \ - $(LIBGCRYPT_LIBS) \ - $(top_builddir)/src/bank-lib/libtalerfakebank.la \ - $(top_builddir)/src/bank-lib/libtalerbank.la \ - $(top_builddir)/src/json/libtalerjson.la \ - $(top_builddir)/src/util/libtalerutil.la \ - $(top_builddir)/src/extensions/libtalerextensions.la \ - -lgnunetcurl \ - -lgnunetutil \ - -ljansson \ - $(XLIB) - -test_exchange_api_conflicts_rsa_SOURCES = \ - test_exchange_api_conflicts.c -test_exchange_api_conflicts_rsa_LDADD = \ - libtalertesting.la \ - $(top_builddir)/src/lib/libtalerexchange.la \ - $(LIBGCRYPT_LIBS) \ - $(top_builddir)/src/bank-lib/libtalerfakebank.la \ - $(top_builddir)/src/bank-lib/libtalerbank.la \ - $(top_builddir)/src/json/libtalerjson.la \ - $(top_builddir)/src/util/libtalerutil.la \ - $(top_builddir)/src/extensions/libtalerextensions.la \ - -lgnunetcurl \ - -lgnunetutil \ - -ljansson \ - $(XLIB) - test_exchange_p2p_cs_SOURCES = \ test_exchange_p2p.c test_exchange_p2p_cs_LDADD = \ @@ -608,9 +574,6 @@ EXTRA_DIST = \ test_exchange_api_age_restriction.conf \ test_exchange_api_age_restriction-cs.conf \ test_exchange_api_age_restriction-rsa.conf \ - test_exchange_api_conflicts.conf \ - test_exchange_api_conflicts-cs.conf \ - test_exchange_api_conflicts-rsa.conf \ test_exchange_api-twisted.conf \ test_exchange_api_twisted-cs.conf \ test_exchange_api_twisted-rsa.conf \ diff --git a/src/testing/test_exchange_api.c b/src/testing/test_exchange_api.c @@ -1255,7 +1255,6 @@ run (void *cls, TALER_TESTING_cmd_batch_withdraw ( "batch-withdraw-coin-1", "create-batch-reserve-1", - 0, /* age restriction off */ MHD_HTTP_OK, "EUR:5", "EUR:1", diff --git a/src/testing/test_exchange_api_age_restriction.c b/src/testing/test_exchange_api_age_restriction.c @@ -349,23 +349,23 @@ run (void *cls, "EUR:10", 0, /* age restriction off */ MHD_HTTP_CONFLICT), - TALER_TESTING_cmd_age_withdraw ( + TALER_TESTING_cmd_withdraw_with_age_proof ( "age-withdraw-coin-1-too-low", "create-reserve-kyc-1", 18, /* Too high */ MHD_HTTP_CONFLICT, "EUR:10", NULL), - TALER_TESTING_cmd_age_withdraw ( + TALER_TESTING_cmd_withdraw_with_age_proof ( "age-withdraw-coins-1", "create-reserve-kyc-1", 8, - MHD_HTTP_OK, + MHD_HTTP_CREATED, "EUR:10", "EUR:10", "EUR:5", NULL), - TALER_TESTING_cmd_age_withdraw_reveal ( + TALER_TESTING_cmd_withdraw_reveal_age_proof ( "age-withdraw-coins-reveal-1", "age-withdraw-coins-1", MHD_HTTP_OK), diff --git a/src/testing/test_exchange_api_age_restriction.conf b/src/testing/test_exchange_api_age_restriction.conf @@ -192,7 +192,7 @@ IS_AND_COMBINATOR = YES # This happens if the reserve is closed. OPERATION_TYPE = WITHDRAW # Threshold is 0, so any amount. -THRESHOLD = EUR:15 +THRESHOLD = EUR:10 # Timeframe doesn't exactly matter with a threshold of EUR:0. TIMEFRAME = 1d # If the rule is triggered, ask the user to provide diff --git a/src/testing/test_exchange_api_conflicts-cs.conf b/src/testing/test_exchange_api_conflicts-cs.conf @@ -1,4 +0,0 @@ -# This file is in the public domain. -# -@INLINE@ test_exchange_api_conflicts.conf -@INLINE@ coins-cs.conf diff --git a/src/testing/test_exchange_api_conflicts-rsa.conf b/src/testing/test_exchange_api_conflicts-rsa.conf @@ -1,4 +0,0 @@ -# This file is in the public domain. -# -@INLINE@ test_exchange_api_conflicts.conf -@INLINE@ coins-rsa.conf diff --git a/src/testing/test_exchange_api_conflicts.c b/src/testing/test_exchange_api_conflicts.c @@ -1,312 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2023 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as - published by the Free Software Foundation; either version 3, or - (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public - License along with TALER; see the file COPYING. If not, see - <http://www.gnu.org/licenses/> -*/ -/** - * @file testing/test_exchange_api_conflicts.c - * @brief testcase to test exchange's handling of coin conflicts: same private - * keys but different denominations or age restrictions - * @author Özgür Kesim - */ -#include "platform.h" -#include "taler_util.h" -#include "taler_signatures.h" -#include "taler_exchange_service.h" -#include "taler_json_lib.h" -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_testing_lib.h> -#include <microhttpd.h> -#include "taler_bank_service.h" -#include "taler_fakebank_lib.h" -#include "taler_testing_lib.h" -#include "taler_extensions.h" - -/** - * Configuration file we use. One (big) configuration is used - * for the various components for this test. - */ -static char *config_file; - -/** - * Our credentials. - */ -static struct TALER_TESTING_Credentials cred; - -/** - * Some tests behave differently when using CS as we cannot - * reuse the coin private key for different denominations - * due to the derivation of it with the /csr values. Hence - * some tests behave differently in CS mode, hence this - * flag. - */ -static bool uses_cs; - -/** - * Execute the taler-exchange-wirewatch command with - * our configuration file. - * - * @param label label to use for the command. - */ -#define CMD_EXEC_WIREWATCH(label) \ - TALER_TESTING_cmd_exec_wirewatch2 (label, config_file, \ - "exchange-account-2") - -/** - * Execute the taler-exchange-aggregator, closer and transfer commands with - * our configuration file. - * - * @param label label to use for the command. - */ -#define CMD_EXEC_AGGREGATOR(label) \ - TALER_TESTING_cmd_sleep ("sleep-before-aggregator", 2), \ - TALER_TESTING_cmd_exec_aggregator (label "-aggregator", config_file), \ - TALER_TESTING_cmd_exec_transfer (label "-transfer", config_file) - - -/** - * Run wire transfer of funds from some user's account to the - * exchange. - * - * @param label label to use for the command. - * @param amount amount to transfer, i.e. "EUR:1" - */ -#define CMD_TRANSFER_TO_EXCHANGE(label,amount) \ - TALER_TESTING_cmd_admin_add_incoming (label, amount, \ - &cred.ba, \ - cred.user42_payto) - -/** - * Main function that will tell the interpreter what commands to - * run. - * - * @param cls closure - * @param is interpreter we use to run commands - */ -static void -run (void *cls, - struct TALER_TESTING_Interpreter *is) -{ - /** - * Test withdrawal with conflicting coins. - */ - struct TALER_TESTING_Command withdraw_conflict_denom[] = { - /** - * Move money to the exchange's bank account. - */ - CMD_TRANSFER_TO_EXCHANGE ("create-reserve-denom", - "EUR:21.14"), - TALER_TESTING_cmd_check_bank_admin_transfer ("check-create-reserve-denom", - "EUR:21.14", - cred.user42_payto, - cred.exchange_payto, - "create-reserve-denom"), - /** - * Make a reserve exist, according to the previous - * transfer. - */ - CMD_EXEC_WIREWATCH ("wirewatch-conflict-denom"), - /** - * Withdraw EUR:0.10, EUR:1, EUR:5, EUR:15, but using the same private key each time. - */ - TALER_TESTING_cmd_batch_withdraw_with_conflict ("withdraw-coin-denom-1", - "create-reserve-denom", - true, - 0, /* age */ - MHD_HTTP_OK, - "EUR:1", - "EUR:5", - "EUR:10", - "EUR:0.10", - NULL), - - TALER_TESTING_cmd_end () - }; - - struct TALER_TESTING_Command spend_conflict_denom[] = { - /** - * Spend the coin. - */ - TALER_TESTING_cmd_deposit ("deposit-denom", - "withdraw-coin-denom-1", - 0, - cred.user42_payto, - "{\"items\":[{\"name\":\"ice cream\",\"value\":1}]}", - GNUNET_TIME_UNIT_ZERO, - "EUR:0.99", - MHD_HTTP_OK), - TALER_TESTING_cmd_deposit ("deposit-denom-conflict", - "withdraw-coin-denom-1", - 1, - cred.user42_payto, - "{\"items\":[{\"name\":\"ice cream\",\"value\":1}]}", - GNUNET_TIME_UNIT_ZERO, - "EUR:4.99", - /* Note: For CS, even though the master secret is the - * same for each coin, their private keys differ due - * to the random choice of the nonce by the exchange. */ - uses_cs ? MHD_HTTP_OK : MHD_HTTP_CONFLICT), - TALER_TESTING_cmd_deposit ("deposit-denom-conflict-2", - "withdraw-coin-denom-1", - 2, - cred.user42_payto, - "{\"items\":[{\"name\":\"ice cream\",\"value\":1}]}", - GNUNET_TIME_UNIT_ZERO, - "EUR:9.99", - /* Note: For CS, even though the master secret is the - * same for each coin, their private keys differ due - * to the random choice of the nonce by the exchange. */ - uses_cs ? MHD_HTTP_OK : MHD_HTTP_CONFLICT), - TALER_TESTING_cmd_deposit ("deposit-denom-conflict-3", - "withdraw-coin-denom-1", - 3, - cred.user42_payto, - "{\"items\":[{\"name\":\"ice cream\",\"value\":1}]}", - GNUNET_TIME_UNIT_ZERO, - "EUR:0.09", - /* Note: For CS, even though the master secret is the - * same for each coin, their private keys differ due - * to the random choice of the nonce by the exchange. */ - uses_cs ? MHD_HTTP_OK : MHD_HTTP_CONFLICT), - TALER_TESTING_cmd_end () - }; - - struct TALER_TESTING_Command withdraw_conflict_age[] = { - /** - * Move money to the exchange's bank account. - */ - CMD_TRANSFER_TO_EXCHANGE ("create-reserve-age", - "EUR:3.03"), - TALER_TESTING_cmd_check_bank_admin_transfer ("check-create-reserve-age", - "EUR:3.03", - cred.user42_payto, - cred.exchange_payto, - "create-reserve-age"), - /** - * Make a reserve exist, according to the previous - * transfer. - */ - CMD_EXEC_WIREWATCH ("wirewatch-conflict-age"), - /** - * Withdraw EUR:1, EUR:5, EUR:15, but using the same private key each time. - */ - TALER_TESTING_cmd_batch_withdraw_with_conflict ("withdraw-coin-age-1", - "create-reserve-age", - true, - 10, /* age */ - MHD_HTTP_OK, - "EUR:1", - "EUR:1", - "EUR:1", - NULL), - - TALER_TESTING_cmd_end () - }; - - struct TALER_TESTING_Command spend_conflict_age[] = { - /** - * Spend the coin. - */ - TALER_TESTING_cmd_deposit ("deposit-age", - "withdraw-coin-age-1", - 0, - cred.user42_payto, - "{\"items\":[{\"name\":\"ice cream\",\"value\":1}]}", - GNUNET_TIME_UNIT_ZERO, - "EUR:0.99", - MHD_HTTP_OK), - TALER_TESTING_cmd_deposit ("deposit-age-conflict", - "withdraw-coin-age-1", - 1, - cred.user42_payto, - "{\"items\":[{\"name\":\"ice cream\",\"value\":1}]}", - GNUNET_TIME_UNIT_ZERO, - "EUR:0.99", - MHD_HTTP_CONFLICT), - TALER_TESTING_cmd_deposit ("deposit-age-conflict-2", - "withdraw-coin-age-1", - 2, - cred.user42_payto, - "{\"items\":[{\"name\":\"ice cream\",\"value\":1}]}", - GNUNET_TIME_UNIT_ZERO, - "EUR:0.99", - MHD_HTTP_CONFLICT), - TALER_TESTING_cmd_end () - }; - - - { - struct TALER_TESTING_Command commands[] = { - TALER_TESTING_cmd_run_fakebank ("run-fakebank", - cred.cfg, - "exchange-account-2"), - TALER_TESTING_cmd_system_start ("start-taler", - config_file, - "-e", - NULL), - TALER_TESTING_cmd_get_exchange ("get-exchange", - cred.cfg, - NULL, - true, - true), - TALER_TESTING_cmd_batch ("withdraw-conflict-denom", - withdraw_conflict_denom), - TALER_TESTING_cmd_batch ("spend-conflict-denom", - spend_conflict_denom), - TALER_TESTING_cmd_batch ("withdraw-conflict-age", - withdraw_conflict_age), - TALER_TESTING_cmd_batch ("spend-conflict-age", - spend_conflict_age), - /* End the suite. */ - TALER_TESTING_cmd_end () - }; - - (void) cls; - TALER_TESTING_run (is, - commands); - } -} - - -int -main (int argc, - char *const *argv) -{ - (void) argc; - { - char *cipher; - - cipher = GNUNET_STRINGS_get_suffix_from_binary_name (argv[0]); - GNUNET_assert (NULL != cipher); - uses_cs = (0 == strcmp (cipher, - "cs")); - GNUNET_asprintf (&config_file, - "test_exchange_api_conflicts-%s.conf", - cipher); - GNUNET_free (cipher); - } - return TALER_TESTING_main (argv, - "INFO", - config_file, - "exchange-account-2", - TALER_TESTING_BS_FAKEBANK, - &cred, - &run, - NULL); -} - - -/* end of test_exchange_api_conflicts.c */ diff --git a/src/testing/test_exchange_api_conflicts.conf b/src/testing/test_exchange_api_conflicts.conf @@ -1,81 +0,0 @@ -# This file is in the public domain. -# - -[PATHS] -TALER_TEST_HOME = test_exchange_api_home/ - -[exchange] -CURRENCY = EUR -CURRENCY_ROUND_UNIT = EUR:0.01 - -[auditor] -BASE_URL = "http://localhost:8083/" -PORT = 8083 -PUBLIC_KEY = T0XJ9QZ59YDN7QG3RE40SB2HY7W0ASR1EKF4WZDGZ1G159RSQC80 -TINY_AMOUNT = EUR:0.01 - -[auditordb-postgres] -CONFIG = "postgres:///talercheck" - -[bank] -HTTP_PORT = 8082 - -[exchange] -TERMS_ETAG = tos -PRIVACY_ETAG = 0 -PORT = 8081 -AML_THRESHOLD = "EUR:99999999" -MASTER_PUBLIC_KEY = 98NJW3CQHZQGQXTY3K85K531XKPAPAVV4Q5V8PYYRR00NJGZWNVG -DB = postgres -BASE_URL = "http://localhost:8081/" -EXPIRE_SHARD_SIZE ="300 ms" -EXPIRE_IDLE_SLEEP_INTERVAL ="1 s" - -[exchangedb-postgres] -CONFIG = "postgres:///talercheck" - -[taler-exchange-secmod-cs] -LOOKAHEAD_SIGN = "24 days" - -[taler-exchange-secmod-rsa] -LOOKAHEAD_SIGN = "24 days" - -[taler-exchange-secmod-eddsa] -LOOKAHEAD_SIGN = "24 days" -DURATION = "14 days" - - -[exchange-account-1] -PAYTO_URI = "payto://x-taler-bank/localhost/42?receiver-name=42" -ENABLE_DEBIT = YES -ENABLE_CREDIT = YES - -[exchange-accountcredentials-1] -WIRE_GATEWAY_AUTH_METHOD = none -WIRE_GATEWAY_URL = "http://localhost:8082/accounts/42/taler-wire-gateway/" - -[admin-accountcredentials-1] -WIRE_GATEWAY_AUTH_METHOD = none -WIRE_GATEWAY_URL = "http://localhost:8082/accounts/42/taler-wire-gateway/" - -[exchange-account-2] -PAYTO_URI = "payto://x-taler-bank/localhost/2?receiver-name=2" -ENABLE_DEBIT = YES -ENABLE_CREDIT = YES - -[exchange-accountcredentials-2] -WIRE_GATEWAY_AUTH_METHOD = basic -USERNAME = Exchange -PASSWORD = password -WIRE_GATEWAY_URL = "http://localhost:8082/accounts/2/taler-wire-gateway/" - -[admin-accountcredentials-2] -WIRE_GATEWAY_AUTH_METHOD = basic -USERNAME = Exchange -PASSWORD = password -WIRE_GATEWAY_URL = "http://localhost:8082/accounts/2/taler-wire-gateway/" - - -[exchange-extension-age_restriction] -ENABLED = YES -#AGE_GROUPS = "8:10:12:14:16:18:21" diff --git a/src/testing/test_kyc_api.c b/src/testing/test_kyc_api.c @@ -121,15 +121,25 @@ run (void *cls, MHD_HTTP_OK), TALER_TESTING_cmd_end () }; + /** * Test withdraw with KYC. */ struct TALER_TESTING_Command withdraw_kyc[] = { - CMD_EXEC_WIREWATCH ("wirewatch-1"), + CMD_TRANSFER_TO_EXCHANGE ( + "create-reserve-kyc", + "EUR:15.02"), + TALER_TESTING_cmd_check_bank_admin_transfer ( + "check-create-reserve-kyc", + "EUR:15.02", + cred.user42_payto, + cred.exchange_payto, + "create-reserve-kyc"), + CMD_EXEC_WIREWATCH ("wirewatch-kyc"), TALER_TESTING_cmd_withdraw_amount ( "withdraw-coin-1-lacking-kyc", - "create-reserve-1", - "EUR:5", + "create-reserve-kyc", + "EUR:10", 0, /* age restriction off */ MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS), TALER_TESTING_cmd_admin_add_kycauth ( @@ -161,27 +171,22 @@ run (void *cls, "test-oauth2", "pass", MHD_HTTP_SEE_OTHER), -#if FIXME_OEC - TALER_TESTING_cmd_withdraw_amount ( - "withdraw-coin-1-with-kyc", - "create-reserve-1", - "EUR:5", - 0, /* age restriction off -- also fails with other values! */ - MHD_HTTP_OK), -#else - TALER_TESTING_cmd_age_withdraw ( - "withdraw-coin-1-with-kyc", - "create-reserve-1", + TALER_TESTING_cmd_withdraw_with_age_proof ( + "age-withdraw-coin-1-with-kyc", + "create-reserve-kyc", 1, - MHD_HTTP_OK, + MHD_HTTP_CREATED, "EUR:5", NULL), -#endif + TALER_TESTING_cmd_withdraw_reveal_age_proof ( + "reveal-age-withdraw-coin-1-with-kyc", + "age-withdraw-coin-1-with-kyc", + MHD_HTTP_OK), /* Attestations above are bound to the originating *bank* account, not to the reserve (!). Hence, they are NOT found here! */ TALER_TESTING_cmd_reserve_get_attestable ( "reserve-get-attestable", - "create-reserve-1", + "create-reserve-kyc", MHD_HTTP_NOT_FOUND, NULL), TALER_TESTING_cmd_end () @@ -230,6 +235,7 @@ run (void *cls, TALER_TESTING_cmd_end () }; + struct TALER_TESTING_Command track[] = { CMD_EXEC_AGGREGATOR ("run-aggregator-before-kyc"), TALER_TESTING_cmd_check_bank_empty ( diff --git a/src/testing/testing_api_cmd_age_withdraw.c b/src/testing/testing_api_cmd_age_withdraw.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2023, 2024 Taler Systems SA + Copyright (C) 2023-2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,7 +18,7 @@ */ /** * @file testing/testing_api_cmd_age_withdraw.c - * @brief implements the age-withdraw command + * @brief implements the withdraw command for age-restricted coins * @author Özgür Kesim */ @@ -39,20 +39,15 @@ struct CoinOutputState { /** - * The calculated details during "age-withdraw", for the selected coin. + * The calculated details during "withdraw", for the selected coin. */ - struct TALER_EXCHANGE_AgeWithdrawCoinPrivateDetails details; + struct TALER_EXCHANGE_WithdrawCoinPrivateDetails details; /** * The (wanted) value of the coin, MUST be the same as input.denom_pub.value; */ struct TALER_Amount amount; - /** - * Reserve history entry that corresponds to this coin. - * Will be of type #TALER_EXCHANGE_RTT_AGEWITHDRAWAL. - */ - struct TALER_EXCHANGE_ReserveHistoryEntry reserve_history; }; /** @@ -70,7 +65,7 @@ struct AgeWithdrawState /** * The age-withdraw handle */ - struct TALER_EXCHANGE_AgeWithdrawHandle *handle; + struct TALER_EXCHANGE_WithdrawHandle *handle; /** * Exchange base URL. Only used as offered trait. @@ -118,12 +113,23 @@ struct AgeWithdrawState size_t num_coins; /** - * The @e num_coins input that is provided to the - * `TALER_EXCHANGE_age_withdraw` API. - * Each contains kappa secrets, from which we will have - * to disclose kappa-1 in a subsequent age-withdraw-reveal operation. + * The @e num_coins denomination public keys that are provided + * to the `TALER_EXCHANGE_withdraw_with_age_proof` API. + */ + struct TALER_EXCHANGE_DenomPublicKey *denoms_pub; + + /** + * The seeds for the batch of @e num_coins coin candidates. + * It contains kappa secrets, from which we will have + * to disclose kappa-1 in a subsequent reveal-withdraw operation. + */ + struct TALER_KappaWithdrawMasterSeedP seeds; + + /** + * The TALER_CNC_KAPPA tuple of @e num_coins secrets, + * derived from the @e seeds; */ - struct TALER_EXCHANGE_AgeWithdrawCoinInput *coin_inputs; + struct TALER_PlanchetMasterSecretP *secrets[TALER_CNC_KAPPA]; /** * The output state of @e num_coins coins, calculated during the @@ -138,14 +144,9 @@ struct AgeWithdrawState uint8_t noreveal_index; /** - * The blinded hashes of the non-revealed (to keep) @e num_coins coins. - */ - const struct TALER_BlindedCoinHashP *blinded_coin_hs; - - /** * The hash of the commitment, needed for the reveal step. */ - struct TALER_AgeWithdrawCommitmentHashP h_commitment; + struct TALER_WithdrawCommitmentHashP h_commitment; /** * Set to the KYC requirement payto hash *if* the exchange replied with a @@ -159,6 +160,11 @@ struct AgeWithdrawState */ uint64_t requirement_row; + /** + * Reserve history entry that corresponds to this withdraw. + * Will be of type #TALER_EXCHANGE_RTT_WITHDRAWAL. + */ + struct TALER_EXCHANGE_ReserveHistoryEntry reserve_history; }; /** @@ -171,7 +177,7 @@ struct AgeWithdrawState static void age_withdraw_cb ( void *cls, - const struct TALER_EXCHANGE_AgeWithdrawResponse *response) + const struct TALER_EXCHANGE_WithdrawResponse *response) { struct AgeWithdrawState *aws = cls; struct TALER_TESTING_Interpreter *is = aws->is; @@ -188,22 +194,23 @@ age_withdraw_cb ( switch (response->hr.http_status) { - case MHD_HTTP_OK: - aws->noreveal_index = response->details.ok.noreveal_index; - aws->h_commitment = response->details.ok.h_commitment; + case MHD_HTTP_CREATED: + aws->noreveal_index = response->details.created.noreveal_index; + aws->h_commitment = response->details.created.h_commitment; + aws->reserve_history.details.withdraw.h_commitment = aws->h_commitment; + aws->reserve_history.details.withdraw.noreveal_index = aws->noreveal_index; - GNUNET_assert (aws->num_coins == response->details.ok.num_coins); + GNUNET_assert (aws->num_coins == response->details.created.num_coins); for (size_t n = 0; n < aws->num_coins; n++) { - aws->coin_outputs[n].details = response->details.ok.coin_details[n]; + aws->coin_outputs[n].details = response->details.created.coin_details[n]; TALER_age_commitment_proof_deep_copy ( &aws->coin_outputs[n].details.age_commitment_proof, - &response->details.ok.coin_details[n].age_commitment_proof); + &response->details.created.coin_details[n].age_commitment_proof); TALER_denom_ewv_copy ( &aws->coin_outputs[n].details.alg_values, - &response->details.ok.coin_details[n].alg_values); + &response->details.created.coin_details[n].alg_values); } - aws->blinded_coin_hs = response->details.ok.blinded_coin_hs; break; case MHD_HTTP_FORBIDDEN: case MHD_HTTP_NOT_FOUND: @@ -277,19 +284,41 @@ age_withdraw_run ( = TALER_reserve_make_payto (aws->exchange_url, &aws->reserve_pub); - aws->coin_inputs = GNUNET_new_array ( - aws->num_coins, - struct TALER_EXCHANGE_AgeWithdrawCoinInput); + aws->denoms_pub = GNUNET_new_array (aws->num_coins, + struct TALER_EXCHANGE_DenomPublicKey); + + { + struct TALER_WithdrawMasterSeedP seed; + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, + &seed, + sizeof(seed)); + TALER_expand_seed_to_kappa_seeds (&seed, + &aws->seeds); + + /** + * Although the secrets are calculated within the call to + * TALER_EXCHANGE_withdraw_with_age_proof below, + * we do the same here in order to be able to store + * the individual coin's planchet secrets. + */ + for (size_t k = 0; k < TALER_CNC_KAPPA; k++) + { + aws->secrets[k] = GNUNET_new_array ( + aws->num_coins, + struct TALER_PlanchetMasterSecretP); + + TALER_expand_withdraw_secrets ( + aws->num_coins, + &aws->seeds.tuple[k], + aws->secrets[k]); + } + } for (unsigned int i = 0; i<aws->num_coins; i++) { - struct TALER_EXCHANGE_AgeWithdrawCoinInput *input = &aws->coin_inputs[i]; + struct TALER_EXCHANGE_DenomPublicKey *denom_pub = &aws->denoms_pub[i]; struct CoinOutputState *cos = &aws->coin_outputs[i]; - /* randomly create the secrets for the kappa coin-candidates */ - GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, - &input->secrets, - sizeof(input->secrets)); /* Find denomination */ dpk = TALER_TESTING_find_pk (keys, &cos->amount, @@ -303,27 +332,47 @@ age_withdraw_run ( TALER_TESTING_interpreter_fail (is); return; } + /* We copy the denomination key, as re-querying /keys * would free the old one. */ - input->denom_pub = TALER_EXCHANGE_copy_denomination_key (dpk); - cos->reserve_history.type = TALER_EXCHANGE_RTT_AGEWITHDRAWAL; + *denom_pub = *dpk; + TALER_denom_pub_copy (&denom_pub->key, + &dpk->key); + + /* Accumulate the expected total amount and fee for the history */ GNUNET_assert (0 <= - TALER_amount_add (&cos->reserve_history.amount, + TALER_amount_add (&aws->reserve_history.amount, &cos->amount, - &input->denom_pub->fees.withdraw)); - cos->reserve_history.details.withdraw.fee = input->denom_pub->fees.withdraw; + &denom_pub->fees.withdraw)); + if (i == 0) + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero ( + denom_pub->fees.withdraw.currency, + &aws->reserve_history.details.withdraw.fee)); + + GNUNET_assert (0 <= + TALER_amount_add (&aws->reserve_history.details.withdraw.fee, + &aws->reserve_history.details.withdraw.fee, + &denom_pub->fees.withdraw)); + } + /* Save the expected history entry */ + aws->reserve_history.type = TALER_EXCHANGE_RTT_WITHDRAWAL; + aws->reserve_history.details.withdraw.age_restricted = true; + aws->reserve_history.details.withdraw.max_age = aws->max_age; - /* Execute the age-withdraw protocol */ + + /* Execute the age-restricted variant of withdraw protocol */ aws->handle = - TALER_EXCHANGE_age_withdraw ( + TALER_EXCHANGE_withdraw_with_age_proof ( TALER_TESTING_interpreter_get_context (is), keys, TALER_TESTING_get_exchange_url (is), rp, - aws->num_coins, - aws->coin_inputs, aws->max_age, + aws->num_coins, + aws->denoms_pub, + &aws->seeds, &age_withdraw_cb, aws); @@ -354,33 +403,42 @@ age_withdraw_cleanup ( { TALER_TESTING_command_incomplete (aws->is, cmd->label); - TALER_EXCHANGE_age_withdraw_cancel (aws->handle); + TALER_EXCHANGE_withdraw_cancel (aws->handle); aws->handle = NULL; } - if (NULL != aws->coin_inputs) + for (size_t k = 0; k < TALER_CNC_KAPPA; k++) + { + if (NULL != aws->secrets[k]) + GNUNET_free (aws->secrets[k]); + aws->secrets[k] = NULL; + } + + if (NULL != aws->denoms_pub) + { + for (size_t n = 0; n < aws->num_coins; n++) + TALER_denom_pub_free (&aws->denoms_pub[n].key); + + GNUNET_free (aws->denoms_pub); + aws->denoms_pub = NULL; + } + + if (NULL != aws->coin_outputs) { for (size_t n = 0; n < aws->num_coins; n++) { - struct TALER_EXCHANGE_AgeWithdrawCoinInput *in = &aws->coin_inputs[n]; struct CoinOutputState *out = &aws->coin_outputs[n]; - - if (NULL != in && NULL != in->denom_pub) - { - TALER_EXCHANGE_destroy_denomination_key (in->denom_pub); - in->denom_pub = NULL; - } - if (NULL != out) - { - TALER_age_commitment_proof_free (&out->details.age_commitment_proof); - TALER_denom_ewv_free (&out->details.alg_values); - } + TALER_age_commitment_proof_free (&out->details.age_commitment_proof); + TALER_denom_ewv_free (&out->details.alg_values); } - GNUNET_free (aws->coin_inputs); + GNUNET_free (aws->coin_outputs); + aws->coin_outputs = NULL; } - GNUNET_free (aws->coin_outputs); + GNUNET_free (aws->exchange_url); + aws->exchange_url = NULL; GNUNET_free (aws->reserve_payto_uri.normalized_payto); + aws->reserve_payto_uri.normalized_payto = NULL; GNUNET_free (aws); } @@ -403,32 +461,31 @@ age_withdraw_traits ( { struct AgeWithdrawState *aws = cls; uint8_t k = aws->noreveal_index; - struct TALER_EXCHANGE_AgeWithdrawCoinInput *in = &aws->coin_inputs[idx]; struct CoinOutputState *out = &aws->coin_outputs[idx]; - struct TALER_EXCHANGE_AgeWithdrawCoinPrivateDetails *details = + struct TALER_EXCHANGE_WithdrawCoinPrivateDetails *details = &aws->coin_outputs[idx].details; struct TALER_TESTING_Trait traits[] = { /* history entry MUST be first due to response code logic below! */ TALER_TESTING_make_trait_reserve_history (idx, - &out->reserve_history), + &aws->reserve_history), TALER_TESTING_make_trait_denom_pub (idx, - in->denom_pub), + &aws->denoms_pub[idx]), TALER_TESTING_make_trait_reserve_priv (&aws->reserve_priv), TALER_TESTING_make_trait_reserve_pub (&aws->reserve_pub), + TALER_TESTING_make_trait_withdraw_commitment (&aws->h_commitment), TALER_TESTING_make_trait_amounts (idx, &out->amount), /* FIXME[oec]: add legal requirement to response and handle it here, as well TALER_TESTING_make_trait_legi_requirement_row (&aws->requirement_row), TALER_TESTING_make_trait_h_payto (&aws->h_payto), */ - TALER_TESTING_make_trait_h_blinded_coin (idx, - &aws->blinded_coin_hs[idx]), TALER_TESTING_make_trait_normalized_payto_uri (&aws->reserve_payto_uri), TALER_TESTING_make_trait_exchange_url (aws->exchange_url), TALER_TESTING_make_trait_coin_priv (idx, &details->coin_priv), + TALER_TESTING_make_trait_kappa_seed (&aws->seeds), TALER_TESTING_make_trait_planchet_secrets (idx, - &in->secrets[k]), + &aws->secrets[k][idx]), TALER_TESTING_make_trait_blinding_key (idx, &details->blinding_key), TALER_TESTING_make_trait_exchange_wd_value (idx, @@ -455,12 +512,13 @@ age_withdraw_traits ( struct TALER_TESTING_Command -TALER_TESTING_cmd_age_withdraw (const char *label, - const char *reserve_reference, - uint8_t max_age, - unsigned int expected_response_code, - const char *amount, - ...) +TALER_TESTING_cmd_withdraw_with_age_proof (const char *label, + const char *reserve_reference, + uint8_t max_age, + unsigned int + expected_response_code, + const char *amount, + ...) { struct AgeWithdrawState *aws; unsigned int cnt; @@ -519,7 +577,7 @@ TALER_TESTING_cmd_age_withdraw (const char *label, /** * The state for the age-withdraw-reveal operation */ -struct AgeWithdrawRevealState +struct AgeRevealWithdrawState { /** * The reference to the CMD resembling the previous call to age-withdraw @@ -545,7 +603,7 @@ struct AgeWithdrawRevealState /** * The handle to the reveal-operation */ - struct TALER_EXCHANGE_AgeWithdrawRevealHandle *handle; + struct TALER_EXCHANGE_RevealWithdrawHandle *handle; /** @@ -564,15 +622,15 @@ struct AgeWithdrawRevealState /** * Callback for the reveal response * - * @param cls Closure of type `struct AgeWithdrawRevealState` + * @param cls Closure of type `struct AgeRevealWithdrawState` * @param response The response */ static void -age_withdraw_reveal_cb ( +age_reveal_withdraw_cb ( void *cls, - const struct TALER_EXCHANGE_AgeWithdrawRevealResponse *response) + const struct TALER_EXCHANGE_RevealWithdrawResponse *response) { - struct AgeWithdrawRevealState *awrs = cls; + struct AgeRevealWithdrawState *awrs = cls; struct TALER_TESTING_Interpreter *is = awrs->is; awrs->handle = NULL; @@ -601,7 +659,7 @@ age_withdraw_reveal_cb ( &aws->coin_outputs[n].details.blinding_key, &aws->coin_outputs[n].details.h_coin_pub, &aws->coin_outputs[n].details.alg_values, - &aws->coin_inputs[n].denom_pub->key)); + &aws->denoms_pub[n].key)); TALER_denom_sig_free (&awrs->denom_sigs[n]); } @@ -633,12 +691,12 @@ age_withdraw_reveal_cb ( * Run the command for age-withdraw-reveal */ static void -age_withdraw_reveal_run ( +age_reveal_withdraw_run ( void *cls, const struct TALER_TESTING_Command *cmd, struct TALER_TESTING_Interpreter *is) { - struct AgeWithdrawRevealState *awrs = cls; + struct AgeRevealWithdrawState *awrs = cls; const struct TALER_TESTING_Command *age_withdraw_cmd; const struct AgeWithdrawState *aws; @@ -662,17 +720,29 @@ age_withdraw_reveal_run ( awrs->aws = aws; awrs->num_coins = aws->num_coins; - awrs->handle = - TALER_EXCHANGE_age_withdraw_reveal ( - TALER_TESTING_interpreter_get_context (is), - TALER_TESTING_get_exchange_url (is), - aws->num_coins, - aws->coin_inputs, - aws->noreveal_index, - &aws->h_commitment, - &aws->reserve_pub, - age_withdraw_reveal_cb, - awrs); + { + struct TALER_RevealWithdrawMasterSeedsP revealed_seeds; + size_t j = 0; + for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) + { + if (aws->noreveal_index == k) + continue; + + revealed_seeds.tuple[j] = aws->seeds.tuple[k]; + j++; + } + + awrs->handle = + TALER_EXCHANGE_reveal_withdraw ( + TALER_TESTING_interpreter_get_context (is), + TALER_TESTING_get_exchange_url (is), + aws->num_coins, + &aws->h_commitment, + &revealed_seeds, + age_reveal_withdraw_cb, + awrs); + } + } @@ -680,21 +750,21 @@ age_withdraw_reveal_run ( * Free the state of a "age-withdraw-reveal" CMD, and possibly * cancel a pending operation thereof * - * @param cls Closure of type `struct AgeWithdrawRevealState` + * @param cls Closure of type `struct AgeRevealWithdrawState` * @param cmd The command being freed. */ static void -age_withdraw_reveal_cleanup ( +age_reveal_withdraw_cleanup ( void *cls, const struct TALER_TESTING_Command *cmd) { - struct AgeWithdrawRevealState *awrs = cls; + struct AgeRevealWithdrawState *awrs = cls; if (NULL != awrs->handle) { TALER_TESTING_command_incomplete (awrs->is, cmd->label); - TALER_EXCHANGE_age_withdraw_reveal_cancel (awrs->handle); + TALER_EXCHANGE_reveal_withdraw_cancel (awrs->handle); awrs->handle = NULL; } GNUNET_free (awrs->denom_sigs); @@ -706,20 +776,20 @@ age_withdraw_reveal_cleanup ( /** * Offer internal data of a "age withdraw reveal" CMD state to other commands. * - * @param cls Closure of they `struct AgeWithdrawRevealState` + * @param cls Closure of they `struct AgeRevealWithdrawState` * @param[out] ret result (could be anything) * @param trait name of the trait * @param idx index number of the object to offer. * @return #GNUNET_OK on success */ static enum GNUNET_GenericReturnValue -age_withdraw_reveal_traits ( +age_reveal_withdraw_traits ( void *cls, const void **ret, const char *trait, unsigned int idx) { - struct AgeWithdrawRevealState *awrs = cls; + struct AgeRevealWithdrawState *awrs = cls; struct TALER_TESTING_Trait traits[] = { TALER_TESTING_make_trait_denom_sig (idx, &awrs->denom_sigs[idx]), @@ -739,13 +809,13 @@ age_withdraw_reveal_traits ( struct TALER_TESTING_Command -TALER_TESTING_cmd_age_withdraw_reveal ( +TALER_TESTING_cmd_withdraw_reveal_age_proof ( const char *label, const char *age_withdraw_reference, unsigned int expected_response_code) { - struct AgeWithdrawRevealState *awrs = - GNUNET_new (struct AgeWithdrawRevealState); + struct AgeRevealWithdrawState *awrs = + GNUNET_new (struct AgeRevealWithdrawState); awrs->age_withdraw_reference = age_withdraw_reference; awrs->expected_response_code = expected_response_code; @@ -753,9 +823,9 @@ TALER_TESTING_cmd_age_withdraw_reveal ( struct TALER_TESTING_Command cmd = { .cls = awrs, .label = label, - .run = age_withdraw_reveal_run, - .cleanup = age_withdraw_reveal_cleanup, - .traits = age_withdraw_reveal_traits, + .run = age_reveal_withdraw_run, + .cleanup = age_reveal_withdraw_cleanup, + .traits = age_reveal_withdraw_traits, }; return cmd; diff --git a/src/testing/testing_api_cmd_batch_deposit.c b/src/testing/testing_api_cmd_batch_deposit.c @@ -550,7 +550,10 @@ batch_deposit_traits (void *cls, if (index >= ds->num_coins) { - GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "[batch_deposit_traits] asked for index #%u while num_coins is #%u\n", + index, + ds->num_coins); return GNUNET_NO; } if (NULL == coin->coin_cmd) diff --git a/src/testing/testing_api_cmd_batch_withdraw.c b/src/testing/testing_api_cmd_batch_withdraw.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2018-2024 Taler Systems SA + Copyright (C) 2018-2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,6 +21,7 @@ * @brief implements the batch withdraw command * @author Christian Grothoff * @author Marcello Stanisci + * @author Özgür Kesim */ #include "platform.h" #include "taler_exchange_service.h" @@ -52,49 +53,20 @@ struct CoinState struct TALER_EXCHANGE_DenomPublicKey *pk; /** - * Private key of the coin. + * Coin Details, as returned by the withdrawal operation */ - struct TALER_CoinSpendPrivateKeyP coin_priv; - - /** - * Public key of the coin. - */ - struct TALER_CoinSpendPublicKeyP coin_pub; - - /** - * Blinding key used during the operation. - */ - union GNUNET_CRYPTO_BlindingSecretP bks; - - /** - * Values contributed from the exchange during the - * withdraw protocol. - */ - struct TALER_ExchangeWithdrawValues exchange_vals; + struct TALER_EXCHANGE_WithdrawCoinPrivateDetails details; /** * Set (by the interpreter) to the exchange's signature over the * coin's public key. */ - struct TALER_DenominationSignature sig; + struct TALER_BlindedDenominationSignature blinded_denom_sig; /** * Private key material of the coin, set by the interpreter. */ - struct TALER_PlanchetMasterSecretP ps; - - /** - * If age > 0, put here the corresponding age commitment with its proof and - * its hash, respectively. - */ - struct TALER_AgeCommitmentProof age_commitment_proof; - struct TALER_AgeCommitmentHash h_age_commitment; - - /** - * Reserve history entry that corresponds to this coin. - * Will be of type #TALER_EXCHANGE_RTT_WITHDRAWAL. - */ - struct TALER_EXCHANGE_ReserveHistoryEntry reserve_history; + struct TALER_PlanchetMasterSecretP secret; }; @@ -139,7 +111,7 @@ struct BatchWithdrawState /** * Withdraw handle (while operation is running). */ - struct TALER_EXCHANGE_BatchWithdrawHandle *wsh; + struct TALER_EXCHANGE_WithdrawHandle *wsh; /** * Array of coin states. @@ -147,6 +119,12 @@ struct BatchWithdrawState struct CoinState *coins; /** + * The seed from which the batch of seeds for the coins is derived + */ + struct TALER_WithdrawMasterSeedP seed; + + + /** * Set to the KYC requirement payto hash *if* the exchange replied with a * request for KYC. */ @@ -164,20 +142,28 @@ struct BatchWithdrawState unsigned int num_coins; /** + * An age > 0 signifies age restriction is applied. + * Same for all coins in the batch. + */ + uint8_t age; + + /** * Expected HTTP response code to the request. */ unsigned int expected_response_code; + /** - * An age > 0 signifies age restriction is required. - * Same for all coins in the batch. + * Reserve history entry that corresponds to this withdrawal. + * Will be of type #TALER_EXCHANGE_RTT_WITHDRAWAL. */ - uint8_t age; + struct TALER_EXCHANGE_ReserveHistoryEntry reserve_history; /** - * Force a conflict: + * The commitment of the call to withdraw, needed later for recoup. */ - bool force_conflict; + struct TALER_WithdrawCommitmentHashP h_commitment; + }; @@ -190,9 +176,9 @@ struct BatchWithdrawState * @param wr withdraw response details */ static void -reserve_batch_withdraw_cb (void *cls, - const struct - TALER_EXCHANGE_BatchWithdrawResponse *wr) +batch_withdraw_cb (void *cls, + const struct + TALER_EXCHANGE_WithdrawResponse *wr) { struct BatchWithdrawState *ws = cls; struct TALER_TESTING_Interpreter *is = ws->is; @@ -212,19 +198,14 @@ reserve_batch_withdraw_cb (void *cls, for (unsigned int i = 0; i<ws->num_coins; i++) { struct CoinState *cs = &ws->coins[i]; - const struct TALER_EXCHANGE_PrivateCoinDetails *pcd - = &wr->details.ok.coins[i]; - - TALER_denom_sig_copy (&cs->sig, - &pcd->sig); - cs->coin_priv = pcd->coin_priv; - GNUNET_CRYPTO_eddsa_key_get_public (&cs->coin_priv.eddsa_priv, - &cs->coin_pub.eddsa_pub); - - cs->bks = pcd->bks; - TALER_denom_ewv_copy (&cs->exchange_vals, - &pcd->exchange_vals); + + cs->details = wr->details.ok.coin_details[i]; + TALER_denom_sig_copy (&cs->details.denom_sig, + &wr->details.ok.coin_details[i].denom_sig); + TALER_denom_ewv_copy (&cs->details.alg_values, + &wr->details.ok.coin_details[i].alg_values); } + ws->h_commitment = wr->details.ok.h_commitment; break; case MHD_HTTP_FORBIDDEN: /* nothing to check */ @@ -266,13 +247,13 @@ batch_withdraw_run (void *cls, struct TALER_TESTING_Interpreter *is) { struct BatchWithdrawState *ws = cls; - const struct TALER_EXCHANGE_Keys *keys = TALER_TESTING_get_keys (is); + struct TALER_EXCHANGE_Keys *keys = TALER_TESTING_get_keys (is); const struct TALER_ReservePrivateKeyP *rp; const struct TALER_TESTING_Command *create_reserve; const struct TALER_EXCHANGE_DenomPublicKey *dpk; - struct TALER_EXCHANGE_WithdrawCoinInput wcis[ws->num_coins]; - struct TALER_PlanchetMasterSecretP conflict_ps = {0}; - struct TALER_AgeMask mask = {0}; + struct TALER_EXCHANGE_DenomPublicKey denoms_pub[ws->num_coins]; + struct TALER_PlanchetMasterSecretP secrets[ws->num_coins]; + (void) cmd; ws->is = is; @@ -305,40 +286,41 @@ batch_withdraw_run (void *cls, = TALER_reserve_make_payto (ws->exchange_url, &ws->reserve_pub); - if (0 < ws->age) - mask = TALER_extensions_get_age_restriction_mask (); - if (ws->force_conflict) - TALER_planchet_master_setup_random (&conflict_ps); + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, + &ws->seed, + sizeof(ws->seed)); + + /** + * This is the same expansion that happens inside the call to + * TALER_EXCHANGE_withdraw. We save the expanded + * secrets later per coin state. + */ + TALER_expand_withdraw_secrets (ws->num_coins, + &ws->seed, + secrets); + + GNUNET_assert (ws->num_coins > 0); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero ( + ws->coins[0].amount.currency, + &ws->reserve_history.amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero ( + ws->coins[0].amount.currency, + &ws->reserve_history.details.withdraw.fee)); for (unsigned int i = 0; i<ws->num_coins; i++) { struct CoinState *cs = &ws->coins[i]; - struct TALER_EXCHANGE_WithdrawCoinInput *wci = &wcis[i]; - - if (ws->force_conflict) - cs->ps = conflict_ps; - else - TALER_planchet_master_setup_random (&cs->ps); + struct TALER_Amount amount; - if (0 < ws->age) - { - struct GNUNET_HashCode seed = {0}; - GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, - &seed, - sizeof(seed)); - TALER_age_restriction_commit (&mask, - ws->age, - &seed, - &cs->age_commitment_proof); - TALER_age_commitment_hash (&cs->age_commitment_proof.commitment, - &cs->h_age_commitment); - } + cs->secret = secrets[i]; dpk = TALER_TESTING_find_pk (keys, &cs->amount, - ws->age > 0); + false); /* no age restriction */ if (NULL == dpk) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, @@ -351,25 +333,44 @@ batch_withdraw_run (void *cls, /* We copy the denomination key, as re-querying /keys * would free the old one. */ cs->pk = TALER_EXCHANGE_copy_denomination_key (dpk); - cs->reserve_history.type = TALER_EXCHANGE_RTT_WITHDRAWAL; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero ( + cs->amount.currency, + &amount)); + GNUNET_assert (0 <= + TALER_amount_add ( + &amount, + &cs->amount, + &cs->pk->fees.withdraw)); GNUNET_assert (0 <= - TALER_amount_add (&cs->reserve_history.amount, - &cs->amount, - &cs->pk->fees.withdraw)); - cs->reserve_history.details.withdraw.fee = cs->pk->fees.withdraw; - - wci->pk = cs->pk; - wci->ps = &cs->ps; - wci->ach = &cs->h_age_commitment; + TALER_amount_add ( + &ws->reserve_history.amount, + &ws->reserve_history.amount, + &amount)); + GNUNET_assert (0 <= + TALER_amount_add ( + &ws->reserve_history.details.withdraw.fee, + &ws->reserve_history.details.withdraw.fee, + &cs->pk->fees.withdraw)); + + denoms_pub[i] = *cs->pk; + TALER_denom_pub_copy (&denoms_pub[i].key, + &cs->pk->key); } - ws->wsh = TALER_EXCHANGE_batch_withdraw ( + + ws->reserve_history.type = TALER_EXCHANGE_RTT_WITHDRAWAL; + + ws->wsh = TALER_EXCHANGE_withdraw ( TALER_TESTING_interpreter_get_context (is), - TALER_TESTING_get_exchange_url (is), keys, + TALER_TESTING_get_exchange_url (is), rp, ws->num_coins, - wcis, - &reserve_batch_withdraw_cb, + 0, + denoms_pub, + &ws->seed, + &batch_withdraw_cb, ws); if (NULL == ws->wsh) { @@ -397,22 +398,19 @@ batch_withdraw_cleanup (void *cls, { TALER_TESTING_command_incomplete (ws->is, cmd->label); - TALER_EXCHANGE_batch_withdraw_cancel (ws->wsh); + TALER_EXCHANGE_withdraw_cancel (ws->wsh); ws->wsh = NULL; } for (unsigned int i = 0; i<ws->num_coins; i++) { struct CoinState *cs = &ws->coins[i]; - - TALER_denom_ewv_free (&cs->exchange_vals); - TALER_denom_sig_free (&cs->sig); + TALER_denom_ewv_free (&cs->details.alg_values); + TALER_denom_sig_free (&cs->details.denom_sig); if (NULL != cs->pk) { TALER_EXCHANGE_destroy_denomination_key (cs->pk); cs->pk = NULL; } - if (0 < ws->age) - TALER_age_commitment_proof_free (&cs->age_commitment_proof); } GNUNET_free (ws->coins); GNUNET_free (ws->exchange_url); @@ -442,21 +440,23 @@ batch_withdraw_traits (void *cls, struct TALER_TESTING_Trait traits[] = { /* history entry MUST be first due to response code logic below! */ TALER_TESTING_make_trait_reserve_history (index, - &cs->reserve_history), + &ws->reserve_history), TALER_TESTING_make_trait_coin_priv (index, - &cs->coin_priv), + &cs->details.coin_priv), TALER_TESTING_make_trait_coin_pub (index, - &cs->coin_pub), + &cs->details.coin_pub), TALER_TESTING_make_trait_planchet_secrets (index, - &cs->ps), + &cs->secret), TALER_TESTING_make_trait_blinding_key (index, - &cs->bks), + &cs->details.blinding_key), TALER_TESTING_make_trait_exchange_wd_value (index, - &cs->exchange_vals), + &cs->details.alg_values), TALER_TESTING_make_trait_denom_pub (index, cs->pk), TALER_TESTING_make_trait_denom_sig (index, - &cs->sig), + &cs->details.denom_sig), + TALER_TESTING_make_trait_withdraw_seed (&ws->seed), + TALER_TESTING_make_trait_withdraw_commitment (&ws->h_commitment), TALER_TESTING_make_trait_reserve_priv (&ws->reserve_priv), TALER_TESTING_make_trait_reserve_pub (&ws->reserve_pub), TALER_TESTING_make_trait_amounts (index, @@ -467,11 +467,12 @@ batch_withdraw_traits (void *cls, TALER_TESTING_make_trait_exchange_url (ws->exchange_url), TALER_TESTING_make_trait_age_commitment_proof (index, ws->age > 0 ? - &cs->age_commitment_proof: + &cs->details. + age_commitment_proof: NULL), TALER_TESTING_make_trait_h_age_commitment (index, ws->age > 0 ? - &cs->h_age_commitment : + &cs->details.h_age_commitment : NULL), TALER_TESTING_trait_end () }; @@ -488,11 +489,9 @@ batch_withdraw_traits (void *cls, struct TALER_TESTING_Command -TALER_TESTING_cmd_batch_withdraw_with_conflict ( +TALER_TESTING_cmd_batch_withdraw ( const char *label, const char *reserve_reference, - bool conflict, - uint8_t age, unsigned int expected_response_code, const char *amount, ...) @@ -502,10 +501,8 @@ TALER_TESTING_cmd_batch_withdraw_with_conflict ( va_list ap; ws = GNUNET_new (struct BatchWithdrawState); - ws->age = age; ws->reserve_reference = reserve_reference; ws->expected_response_code = expected_response_code; - ws->force_conflict = conflict; cnt = 1; va_start (ap, diff --git a/src/testing/testing_api_cmd_coin_history.c b/src/testing/testing_api_cmd_coin_history.c @@ -255,7 +255,7 @@ analyze_command (void *cls, if (ac->failure) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Entry for batch step `%s' missing in history\n", + "Entry for batch step `%s' missing in coin history\n", step->label); return; } @@ -282,6 +282,7 @@ analyze_command (void *cls, j); break; /* command does nothing for coins */ } + if (0 != GNUNET_memcmp (rp, coin_pub)) @@ -547,7 +548,7 @@ history_traits (void *cls, { struct HistoryState *hs = cls; struct TALER_TESTING_Trait traits[] = { - TALER_TESTING_make_trait_coin_pub (0, + TALER_TESTING_make_trait_coin_pub (index, &hs->coin_pub), TALER_TESTING_trait_end () }; diff --git a/src/testing/testing_api_cmd_deposit.c b/src/testing/testing_api_cmd_deposit.c @@ -458,25 +458,45 @@ deposit_run (void *cls, stderr, JSON_INDENT (2)); #endif - if ( (GNUNET_OK != - TALER_TESTING_get_trait_coin_priv (coin_cmd, - ds->coin_index, - &coin_priv)) || - (GNUNET_OK != - TALER_TESTING_get_trait_h_age_commitment (coin_cmd, - ds->coin_index, - &phac)) || - (GNUNET_OK != - TALER_TESTING_get_trait_denom_pub (coin_cmd, - ds->coin_index, - &ds->denom_pub)) || - (GNUNET_OK != - TALER_TESTING_get_trait_denom_sig (coin_cmd, - ds->coin_index, - &denom_pub_sig)) || - (GNUNET_OK != - TALER_JSON_contract_hash (ds->contract_terms, - &h_contract_terms)) ) + if (GNUNET_OK != + TALER_TESTING_get_trait_coin_priv (coin_cmd, + ds->coin_index, + &coin_priv)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (is); + return; + } + if (GNUNET_OK != + TALER_TESTING_get_trait_h_age_commitment (coin_cmd, + ds->coin_index, + &phac)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (is); + return; + } + if (GNUNET_OK != + TALER_TESTING_get_trait_denom_pub (coin_cmd, + ds->coin_index, + &ds->denom_pub)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (is); + return; + } + if (GNUNET_OK != + TALER_TESTING_get_trait_denom_sig (coin_cmd, + ds->coin_index, + &denom_pub_sig)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (is); + return; + } + if (GNUNET_OK != + TALER_JSON_contract_hash (ds->contract_terms, + &h_contract_terms)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (is); @@ -616,8 +636,8 @@ deposit_traits (void *cls, /* Will point to coin cmd internals. */ const struct TALER_CoinSpendPrivateKeyP *coin_spent_priv; struct TALER_CoinSpendPublicKeyP coin_spent_pub; - const struct TALER_AgeCommitmentProof *age_commitment_proof; - const struct TALER_AgeCommitmentHash *h_age_commitment; + const struct TALER_AgeCommitmentProof *age_commitment_proof=NULL; + const struct TALER_AgeCommitmentHash *h_age_commitment=NULL; if (! ds->command_initialized) { diff --git a/src/testing/testing_api_cmd_recoup.c b/src/testing/testing_api_cmd_recoup.c @@ -190,7 +190,9 @@ recoup_run (void *cls, const struct TALER_CoinSpendPrivateKeyP *coin_priv; const struct TALER_EXCHANGE_DenomPublicKey *denom_pub; const struct TALER_DenominationSignature *coin_sig; - const struct TALER_PlanchetMasterSecretP *planchet; + const struct TALER_WithdrawMasterSeedP *seed; + const struct TALER_WithdrawCommitmentHashP *h_commitment; + struct TALER_PlanchetMasterSecretP secret; char *cref; unsigned int idx; const struct TALER_ExchangeWithdrawValues *ewv; @@ -238,8 +240,8 @@ recoup_run (void *cls, return; } if (GNUNET_OK != - TALER_TESTING_get_trait_planchet_secret (coin_cmd, - &planchet)) + TALER_TESTING_get_trait_withdraw_seed (coin_cmd, + &seed)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (is); @@ -267,12 +269,23 @@ recoup_run (void *cls, TALER_TESTING_interpreter_fail (is); return; } + if (GNUNET_OK != + TALER_TESTING_get_trait_withdraw_commitment (coin_cmd, + &h_commitment)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (is); + return; + } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Trying to recoup denomination '%s'\n", TALER_B2S (&denom_pub->h_key)); ps->che.type = TALER_EXCHANGE_CTT_RECOUP; ps->che.amount = ps->reserve_history.amount; - TALER_planchet_blinding_secret_create (planchet, + TALER_expand_withdraw_secrets (1, + seed, + &secret); + TALER_planchet_blinding_secret_create (&secret, ewv, &ps->che.details.recoup.coin_bks); TALER_denom_pub_hash (&denom_pub->key, @@ -288,7 +301,8 @@ recoup_run (void *cls, denom_pub, coin_sig, ewv, - planchet, + &secret, + h_commitment, &recoup_cb, ps); GNUNET_assert (NULL != ps->ph); diff --git a/src/testing/testing_api_cmd_reserve_history.c b/src/testing/testing_api_cmd_reserve_history.c @@ -134,30 +134,29 @@ history_entry_cmp ( h2->details.in_details.timestamp)) ) return 0; return 1; - case TALER_EXCHANGE_RTT_WITHDRAWAL: + case TALER_EXCHANGE_RTT_BATCH_WITHDRAWAL: if ( (0 == TALER_amount_cmp (&h1->amount, &h2->amount)) && (0 == - TALER_amount_cmp (&h1->details.withdraw.fee, - &h2->details.withdraw.fee)) ) + TALER_amount_cmp (&h1->details.batch_withdraw.fee, + &h2->details.batch_withdraw.fee)) ) /* testing_api_cmd_withdraw doesn't set the out_authorization_sig, so we cannot test for it here. but if the amount matches, that should be good enough. */ return 0; return 1; - case TALER_EXCHANGE_RTT_AGEWITHDRAWAL: - /* testing_api_cmd_age_withdraw doesn't set the out_authorization_sig, - so we cannot test for it here. but if the amount matches, - that should be good enough. */ + case TALER_EXCHANGE_RTT_WITHDRAWAL: if ( (0 == TALER_amount_cmp (&h1->amount, &h2->amount)) && (0 == - TALER_amount_cmp (&h1->details.age_withdraw.fee, - &h2->details.age_withdraw.fee)) && - (h1->details.age_withdraw.max_age == - h2->details.age_withdraw.max_age)) + TALER_amount_cmp (&h1->details.withdraw.fee, + &h2->details.withdraw.fee)) && + (h1->details.withdraw.age_restricted == + h2->details.withdraw.age_restricted) && + ((! h1->details.withdraw.age_restricted) || + (h1->details.withdraw.max_age == h2->details.withdraw.max_age) )) return 0; return 1; case TALER_EXCHANGE_RTT_RECOUP: @@ -303,7 +302,7 @@ analyze_command (void *cls, if (ac->failure) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Entry for batch step `%s' missing in history\n", + "Entry for batch step `%s' missing in reserve history\n", step->label); return; } @@ -315,6 +314,7 @@ analyze_command (void *cls, { const struct TALER_ReservePublicKeyP *rp; + bool matched = false; if (GNUNET_OK != TALER_TESTING_get_trait_reserve_pub (cmd, @@ -327,7 +327,6 @@ analyze_command (void *cls, for (unsigned int j = 0; true; j++) { const struct TALER_EXCHANGE_ReserveHistoryEntry *he; - bool matched = false; if (GNUNET_OK != TALER_TESTING_get_trait_reserve_history (cmd, @@ -335,10 +334,10 @@ analyze_command (void *cls, &he)) { /* NOTE: only for debugging... */ - if (0 == j) - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Command `%s' has the reserve_pub, but lacks reserve history trait\n", - cmd->label); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Command `%s' has the reserve_pub, but lacks reserve history trait for index #%u\n", + cmd->label, + j); return; /* command does nothing for reserves */ } for (unsigned int i = 0; i<history_length; i++) @@ -351,18 +350,20 @@ analyze_command (void *cls, { found[i] = true; matched = true; + ac->failure = false; break; } } - if (! matched) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Command `%s' reserve history entry #%u not found\n", - cmd->label, - j); - ac->failure = true; - return; - } + if (matched) + break; + } + if (! matched) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Command `%s' no relevant reserve history entry not found\n", + cmd->label); + ac->failure = true; + ; } } } diff --git a/src/testing/testing_api_cmd_withdraw.c b/src/testing/testing_api_cmd_withdraw.c @@ -133,9 +133,9 @@ struct WithdrawState struct TALER_DenominationSignature sig; /** - * Private key material of the coin, set by the interpreter. + * Seed for the key material of the coin, set by the interpreter. */ - struct TALER_PlanchetMasterSecretP ps; + struct TALER_WithdrawMasterSeedP seed; /** * An age > 0 signifies age restriction is required @@ -158,7 +158,12 @@ struct WithdrawState /** * Withdraw handle (while operation is running). */ - struct TALER_EXCHANGE_BatchWithdrawHandle *wsh; + struct TALER_EXCHANGE_WithdrawHandle *wsh; + + /** + * The commitment for the withdraw operation, later needed for /recoup + */ + struct TALER_WithdrawCommitmentHashP h_commitment; /** * Task scheduled to try later. @@ -241,8 +246,8 @@ do_retry (void *cls) * @param wr withdraw response details */ static void -reserve_withdraw_cb (void *cls, - const struct TALER_EXCHANGE_BatchWithdrawResponse *wr) +withdraw_cb (void *cls, + const struct TALER_EXCHANGE_WithdrawResponse *wr) { struct WithdrawState *ws = cls; struct TALER_TESTING_Interpreter *is = ws->is; @@ -292,13 +297,23 @@ reserve_withdraw_cb (void *cls, switch (wr->hr.http_status) { case MHD_HTTP_OK: - GNUNET_assert (1 == wr->details.ok.num_coins); + GNUNET_assert (1 == wr->details.ok.num_sigs); TALER_denom_sig_copy (&ws->sig, - &wr->details.ok.coins[0].sig); - ws->coin_priv = wr->details.ok.coins[0].coin_priv; - ws->bks = wr->details.ok.coins[0].bks; + &wr->details.ok.coin_details[0].denom_sig); + ws->coin_priv = wr->details.ok.coin_details[0].coin_priv; + ws->bks = wr->details.ok.coin_details[0].blinding_key; TALER_denom_ewv_copy (&ws->exchange_vals, - &wr->details.ok.coins[0].exchange_vals); + &wr->details.ok.coin_details[0].alg_values); + ws->h_commitment = wr->details.ok.h_commitment; + if (0<ws->age) + { + /* copy the age-commitment data */ + ws->h_age_commitment = wr->details.ok.coin_details[0].h_age_commitment; + TALER_age_commitment_proof_deep_copy ( + &ws->age_commitment_proof, + &wr->details.ok.coin_details[0].age_commitment_proof); + } + if (0 != ws->total_backoff.rel_value_us) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -385,11 +400,11 @@ withdraw_run (void *cls, if (NULL == ws->reuse_coin_key_ref) { - TALER_planchet_master_setup_random (&ws->ps); + TALER_withdraw_master_seed_setup_random (&ws->seed); } else { - const struct TALER_PlanchetMasterSecretP *ps; + const struct TALER_WithdrawMasterSeedP *seed; const struct TALER_TESTING_Command *cref; char *cstr; unsigned int index; @@ -404,9 +419,9 @@ withdraw_run (void *cls, GNUNET_assert (NULL != cref); GNUNET_free (cstr); GNUNET_assert (GNUNET_OK == - TALER_TESTING_get_trait_planchet_secret (cref, - &ps)); - ws->ps = *ps; + TALER_TESTING_get_trait_withdraw_seed (cref, + &seed)); + ws->seed = *seed; } if (NULL == ws->pk) @@ -437,24 +452,21 @@ withdraw_run (void *cls, TALER_amount_add (&ws->reserve_history.amount, &ws->amount, &ws->pk->fees.withdraw)); - ws->reserve_history.details.withdraw.fee = ws->pk->fees.withdraw; - { - struct TALER_EXCHANGE_WithdrawCoinInput wci = { - .pk = ws->pk, - .ps = &ws->ps, - .ach = 0 < ws->age ? &ws->h_age_commitment : NULL - }; + ws->reserve_history.details.withdraw.fee = + ws->pk->fees.withdraw; + + ws->wsh = TALER_EXCHANGE_withdraw ( + TALER_TESTING_interpreter_get_context (is), + TALER_TESTING_get_keys (is), + TALER_TESTING_get_exchange_url (is), + rp, + 1, + ws->age, + ws->pk, + &ws->seed, + &withdraw_cb, + ws); - ws->wsh = TALER_EXCHANGE_batch_withdraw ( - TALER_TESTING_interpreter_get_context (is), - TALER_TESTING_get_exchange_url (is), - TALER_TESTING_get_keys (is), - rp, - 1, - &wci, - &reserve_withdraw_cb, - ws); - } if (NULL == ws->wsh) { GNUNET_break (0); @@ -481,7 +493,7 @@ withdraw_cleanup (void *cls, { TALER_TESTING_command_incomplete (ws->is, cmd->label); - TALER_EXCHANGE_batch_withdraw_cancel (ws->wsh); + TALER_EXCHANGE_withdraw_cancel (ws->wsh); ws->wsh = NULL; } if (NULL != ws->retry_task) @@ -523,11 +535,12 @@ withdraw_traits (void *cls, struct WithdrawState *ws = cls; struct TALER_TESTING_Trait traits[] = { /* history entry MUST be first due to response code logic below! */ - TALER_TESTING_make_trait_reserve_history (0, + TALER_TESTING_make_trait_reserve_history (0 /* only one coin */, &ws->reserve_history), TALER_TESTING_make_trait_coin_priv (0 /* only one coin */, &ws->coin_priv), - TALER_TESTING_make_trait_planchet_secret (&ws->ps), + TALER_TESTING_make_trait_withdraw_seed (&ws->seed), + TALER_TESTING_make_trait_withdraw_commitment (&ws->h_commitment), TALER_TESTING_make_trait_blinding_key (0 /* only one coin */, &ws->bks), TALER_TESTING_make_trait_exchange_wd_value (0 /* only one coin */, @@ -574,23 +587,6 @@ TALER_TESTING_cmd_withdraw_amount (const char *label, ws = GNUNET_new (struct WithdrawState); ws->age = age; - if (0 < age) - { - struct GNUNET_HashCode seed; - struct TALER_AgeMask mask; - - mask = TALER_extensions_get_age_restriction_mask (); - GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, - &seed, - sizeof(seed)); - TALER_age_restriction_commit (&mask, - age, - &seed, - &ws->age_commitment_proof); - TALER_age_commitment_hash (&ws->age_commitment_proof.commitment, - &ws->h_age_commitment); - } - ws->reserve_reference = reserve_reference; if (GNUNET_OK != TALER_string_to_amount (amount, diff --git a/src/util/amount.c b/src/util/amount.c @@ -42,13 +42,20 @@ invalidate (struct TALER_Amount *a) enum GNUNET_GenericReturnValue TALER_check_currency (const char *str) { - if (strlen (str) >= TALER_CURRENCY_LEN) + size_t len = strlen (str); + if (len >= TALER_CURRENCY_LEN) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Currency code name `%s' is too long\n", str); return GNUNET_SYSERR; } + if (len == 0) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Currency code name must be set\n"); + return GNUNET_SYSERR; + } /* validate str has only legal characters in it! */ for (unsigned int i = 0; '\0' != str[i]; i++) { diff --git a/src/util/crypto.c b/src/util/crypto.c @@ -147,6 +147,56 @@ TALER_link_recover_transfer_secret ( void +TALER_expand_withdraw_secrets ( + size_t num_coins, + const struct TALER_WithdrawMasterSeedP *seed, + struct TALER_PlanchetMasterSecretP secrets[static num_coins]) +{ + GNUNET_assert (0<num_coins); + _Static_assert (sizeof(seed->seed_data) == sizeof(secrets->key_data)); + + if (num_coins ==1) + GNUNET_memcpy (&secrets[0].key_data, + &seed->seed_data, + sizeof(secrets[0].key_data)); + else + { + uint32_t be_salt = htonl (num_coins); + GNUNET_assert (GNUNET_OK == + GNUNET_CRYPTO_kdf (secrets, + sizeof (*secrets) * num_coins, + &be_salt, + sizeof (be_salt), + seed, + sizeof (*seed), + "taler-withdraw-secrets", + strlen ("taler-withdraw-secrets"), + NULL, 0)); + } +} + + +void +TALER_expand_seed_to_kappa_seeds ( + const struct TALER_WithdrawMasterSeedP *seed, + struct TALER_KappaWithdrawMasterSeedP *seeds) +{ + uint32_t be_salt = htonl (TALER_CNC_KAPPA); + + GNUNET_assert (GNUNET_OK == + GNUNET_CRYPTO_kdf (seeds, + sizeof (*seeds), + &be_salt, + sizeof (be_salt), + seed, + sizeof (*seed), + "taler-kappa-seeds", + strlen ("taler-kappa-seeds"), + NULL, 0)); +} + + +void TALER_planchet_master_setup_random ( struct TALER_PlanchetMasterSecretP *ps) { @@ -157,6 +207,16 @@ TALER_planchet_master_setup_random ( void +TALER_withdraw_master_seed_setup_random ( + struct TALER_WithdrawMasterSeedP *seed) +{ + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_STRONG, + seed, + sizeof (*seed)); +} + + +void TALER_refresh_master_setup_random ( struct TALER_RefreshMasterSecretP *rms) { diff --git a/src/util/exchange_signatures.c b/src/util/exchange_signatures.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2021-2024 Taler Systems SA + Copyright (C) 2021-2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -380,12 +380,13 @@ GNUNET_NETWORK_STRUCT_BEGIN /** * @brief Format of the block signed by the Exchange in response to a - * successful "/reserves/$RESERVE_PUB/age-withdraw" request. Hereby the - * exchange affirms that the commitment along with the maximum age group and + * successful "/reserves/$RESERVE_PUB/withdraw" request. + * If age restriction is set, the exchange hereby also + * affirms that the commitment along with the maximum age group and * the amount were accepted. This also commits the exchange to a particular * index to not be revealed during the reveal. */ -struct TALER_AgeWithdrawConfirmationPS +struct TALER_WithdrawConfirmationPS { /** * Purpose is #TALER_SIGNATURE_EXCHANGE_CONFIRM_WITHDRAW. Signed by a @@ -394,13 +395,17 @@ struct TALER_AgeWithdrawConfirmationPS struct GNUNET_CRYPTO_EccSignaturePurpose purpose; /** - * Commitment made in the /reserves/$RESERVE_PUB/age-withdraw. + * Commitment made in the /reserves/$RESERVE_PUB/withdraw. */ - struct TALER_AgeWithdrawCommitmentHashP h_commitment GNUNET_PACKED; + struct TALER_WithdrawCommitmentHashP h_commitment GNUNET_PACKED; /** - * Index that the client will not have to reveal, in NBO. - * Must be smaller than #TALER_CNC_KAPPA. + * If age restriction does not apply to this withdrawal, + * (i.e. max_age was not set during the request) + * MUST be 0xFFFFFFFF. + * Otherwise (i.e. age restriction applies): + * index that the client will not have to reveal, in NBO, + * MUST be smaller than #TALER_CNC_KAPPA. */ uint32_t noreveal_index GNUNET_PACKED; @@ -409,15 +414,15 @@ struct TALER_AgeWithdrawConfirmationPS GNUNET_NETWORK_STRUCT_END enum TALER_ErrorCode -TALER_exchange_online_age_withdraw_confirmation_sign ( +TALER_exchange_online_withdraw_age_confirmation_sign ( TALER_ExchangeSignCallback scb, - const struct TALER_AgeWithdrawCommitmentHashP *h_commitment, + const struct TALER_WithdrawCommitmentHashP *h_commitment, uint32_t noreveal_index, struct TALER_ExchangePublicKeyP *pub, struct TALER_ExchangeSignatureP *sig) { - struct TALER_AgeWithdrawConfirmationPS confirm = { + struct TALER_WithdrawConfirmationPS confirm = { .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_WITHDRAW), .purpose.size = htonl (sizeof (confirm)), .h_commitment = *h_commitment, @@ -430,14 +435,35 @@ TALER_exchange_online_age_withdraw_confirmation_sign ( } +enum TALER_ErrorCode +TALER_exchange_online_withdraw_confirmation_sign ( + TALER_ExchangeSignCallback scb, + const struct TALER_WithdrawCommitmentHashP *h_commitment, + struct TALER_ExchangePublicKeyP *pub, + struct TALER_ExchangeSignatureP *sig) +{ + + struct TALER_WithdrawConfirmationPS confirm = { + .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_WITHDRAW), + .purpose.size = htonl (sizeof (confirm)), + .h_commitment = *h_commitment, + .noreveal_index = htonl (0xFFFFFFFF) + }; + + return scb (&confirm.purpose, + pub, + sig); +} + + enum GNUNET_GenericReturnValue -TALER_exchange_online_age_withdraw_confirmation_verify ( - const struct TALER_AgeWithdrawCommitmentHashP *h_commitment, +TALER_exchange_online_withdraw_age_confirmation_verify ( + const struct TALER_WithdrawCommitmentHashP *h_commitment, uint32_t noreveal_index, const struct TALER_ExchangePublicKeyP *exchange_pub, const struct TALER_ExchangeSignatureP *exchange_sig) { - struct TALER_AgeWithdrawConfirmationPS confirm = { + struct TALER_WithdrawConfirmationPS confirm = { .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_WITHDRAW), .purpose.size = htonl (sizeof (confirm)), .h_commitment = *h_commitment, @@ -458,7 +484,31 @@ TALER_exchange_online_age_withdraw_confirmation_verify ( } -/* FIXME[oec] add signature for age-withdraw, age-reveal */ +enum GNUNET_GenericReturnValue +TALER_exchange_online_withdraw_confirmation_verify ( + const struct TALER_WithdrawCommitmentHashP *h_commitment, + const struct TALER_ExchangePublicKeyP *exchange_pub, + const struct TALER_ExchangeSignatureP *exchange_sig) +{ + struct TALER_WithdrawConfirmationPS confirm = { + .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_WITHDRAW), + .purpose.size = htonl (sizeof (confirm)), + .h_commitment = *h_commitment, + .noreveal_index = htonl (0xFFFFFFFF) + }; + + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_verify ( + TALER_SIGNATURE_EXCHANGE_CONFIRM_WITHDRAW, + &confirm, + &exchange_sig->eddsa_signature, + &exchange_pub->eddsa_pub)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} GNUNET_NETWORK_STRUCT_BEGIN diff --git a/src/util/wallet_signatures.c b/src/util/wallet_signatures.c @@ -536,8 +536,9 @@ GNUNET_NETWORK_STRUCT_BEGIN /** * @brief Format used for to generate the signature on a request to withdraw * coins from a reserve. + * @note: deprecated. Will be removed at some point after v24 of the protocol. */ -struct TALER_WithdrawRequestPS +struct TALER_WithdrawCommitmentPre24PS { /** @@ -570,14 +571,14 @@ struct TALER_WithdrawRequestPS GNUNET_NETWORK_STRUCT_END void -TALER_wallet_withdraw_sign ( +TALER_wallet_withdraw_sign_pre26 ( const struct TALER_DenominationHashP *h_denom_pub, const struct TALER_Amount *amount_with_fee, const struct TALER_BlindedCoinHashP *bch, const struct TALER_ReservePrivateKeyP *reserve_priv, struct TALER_ReserveSignatureP *reserve_sig) { - struct TALER_WithdrawRequestPS req = { + struct TALER_WithdrawCommitmentPre24PS req = { .purpose.size = htonl (sizeof (req)), .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW), .h_denomination_pub = *h_denom_pub, @@ -593,25 +594,25 @@ TALER_wallet_withdraw_sign ( enum GNUNET_GenericReturnValue -TALER_wallet_withdraw_verify ( +TALER_wallet_withdraw_verify_pre26 ( const struct TALER_DenominationHashP *h_denom_pub, const struct TALER_Amount *amount_with_fee, const struct TALER_BlindedCoinHashP *bch, const struct TALER_ReservePublicKeyP *reserve_pub, const struct TALER_ReserveSignatureP *reserve_sig) { - struct TALER_WithdrawRequestPS wsrd = { - .purpose.size = htonl (sizeof (wsrd)), + struct TALER_WithdrawCommitmentPre24PS req = { + .purpose.size = htonl (sizeof (req)), .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW), .h_denomination_pub = *h_denom_pub, .h_coin_envelope = *bch }; - TALER_amount_hton (&wsrd.amount_with_fee, + TALER_amount_hton (&req.amount_with_fee, amount_with_fee); return GNUNET_CRYPTO_eddsa_verify ( TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW, - &wsrd, + &req, &reserve_sig->eddsa_signature, &reserve_pub->eddsa_pub); } @@ -620,101 +621,258 @@ TALER_wallet_withdraw_verify ( GNUNET_NETWORK_STRUCT_BEGIN /** - * @brief Format used for to generate the signature on a request to - * age-withdraw from a reserve. + * @brief Format used to generate the commitment for a request to + * withdraw from a reserve. The hash of this struct is needed + * to sign a withdraw request and also in subsequent calls to + * /reveal-withdraw (in case of age restriction) or /recoup. */ -struct TALER_AgeWithdrawRequestPS +struct TALER_WithdrawCommitmentP { /** - * Purpose must be #TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW. - * Used with an EdDSA signature of a `struct TALER_ReservePublicKeyP`. + * The hash of the reserve's public key */ - struct GNUNET_CRYPTO_EccSignaturePurpose purpose; + struct TALER_HashReservePublicKeyP h_reserve_pub; /** - * The reserve's public key + * The details of the withdraw request. + * This struct is also used in TALER_WithdrawRequestPS. */ - struct TALER_ReservePublicKeyP reserve_pub; + struct TALER_WithdrawRequestDetailsP + { - /** - * Value of the coin being exchanged (matching the denomination key) - * plus the transaction fee. We include this in what is being - * signed so that we can verify a reserve's remaining total balance - * without needing to access the respective denomination key - * information each time. - */ - struct TALER_AmountNBO amount_with_fee; + /** + * Total value of all coins being exchanged (matching the denomination keys), + * without the fee. + * Note that the reserve must have a value of at least amount+fee. + */ + struct TALER_AmountNBO amount; + + /** + * Total fee for the withdrawal. + * Note that the reserve must have a value of at least amount+fee. + */ + struct TALER_AmountNBO fee; + + /** + * Running SHA512 hash of all TALER_BlindedCoinHashP's + * of the of n coins, or n*kappa candidate coins in case of age restriction. + * In the later case, the coins' hashes are arranged [0..num_coins)...[0..num_coins), + * i.e. the coins are grouped per kappa-index. + * Note that each coin's TALER_BlindedCoinHashP also captures + * the hash of the public key of the corresponding denomination. + */ + struct TALER_HashBlindedPlanchetsP h_planchets GNUNET_PACKED; + + /** + * Maximum age group that the coins are going to be restricted to. + * MUST be 0 if no age restriction applies. + */ + uint32_t max_age_group; + + /** + * The mask that defines the age groups. + * MUST be the same for all denominations. + * MUST be 0 if no age restriction applies. + */ + struct TALER_AgeMask mask; + + } details GNUNET_PACKED; - /** - * Running SHA512 hash of the commitment of n*kappa coins - */ - struct TALER_AgeWithdrawCommitmentHashP h_commitment; +}; - /** - * The mask that defines the age groups. MUST be the same for all denominations. - */ - struct TALER_AgeMask mask; + +/** + * @brief Format used for to generate the signature on a request to withdraw + * coins from a reserve. + * + */ +struct TALER_WithdrawRequestPS +{ +/** + * Purpose is #TALER_SIGNATURE_WALLET_WITHDRAW + */ + struct GNUNET_CRYPTO_EccSignaturePurpose purpose; /** - * Maximum age group that the coins are going to be restricted to. + * The details of the withdraw request */ - uint8_t max_age_group; + struct TALER_WithdrawRequestDetailsP details GNUNET_PACKED; }; GNUNET_NETWORK_STRUCT_END void -TALER_wallet_age_withdraw_sign ( - const struct TALER_AgeWithdrawCommitmentHashP *h_commitment, - const struct TALER_Amount *amount_with_fee, +TALER_wallet_blinded_planchets_hash ( + size_t num_planchets, + const struct TALER_BlindedPlanchet blinded_planchets[static num_planchets], + const struct TALER_DenominationHashP h_denom_pubs[static num_planchets], + struct TALER_HashBlindedPlanchetsP *h_planchets) +{ + struct TALER_BlindedCoinHashP bch; + struct GNUNET_HashContext *coins_hctx; + + GNUNET_assert (num_planchets >0); + GNUNET_assert (NULL != blinded_planchets); + GNUNET_assert (NULL != h_planchets); + + coins_hctx = GNUNET_CRYPTO_hash_context_start (); + GNUNET_assert (NULL != coins_hctx); + + for (size_t i = 0; i < num_planchets; i++) + { + TALER_coin_ev_hash ( + &blinded_planchets[i], + &h_denom_pubs[i], + &bch); + GNUNET_CRYPTO_hash_context_read ( + coins_hctx, + &bch, + sizeof(bch)); + } + + GNUNET_CRYPTO_hash_context_finish ( + coins_hctx, + &h_planchets->hash); +} + + +/** + * @brief fill a withdraw request details object from parameters + * + * @param amount the amount to withdraw, without fee's. + * @param fee the fee's for the request + * @param h_planchets the running hash over all coin's planchets + * @param mask the age mask, in case of age restriction, or NULL + * @param max_age the maximum age to commit to, if age restriction applies + * @param[out] req details of the withdraw request + */ +static void +fill_withdraw_request_details ( + const struct TALER_Amount *amount, + const struct TALER_Amount *fee, + const struct TALER_HashBlindedPlanchetsP *h_planchets, + const struct TALER_AgeMask *mask, + uint8_t max_age, + struct TALER_WithdrawRequestDetailsP *req) +{ + GNUNET_assert (NULL != req); + memset (req, + 0, + sizeof(*req)); + + GNUNET_assert (NULL != h_planchets); + req->h_planchets = *h_planchets; + if (NULL != mask) + { + req->mask = *mask; + req->max_age_group = + TALER_get_age_group (mask, + max_age); + } + TALER_amount_hton (&req->amount, + amount); + TALER_amount_hton (&req->fee, + fee); + +} + + +struct TALER_HashReservePublicKeyP +TALER_wallet_hash_reserve_pub ( + const struct TALER_ReservePublicKeyP *reserve_pub) +{ + struct TALER_HashReservePublicKeyP hr; + + GNUNET_CRYPTO_hash (reserve_pub, + sizeof(*reserve_pub), + &hr.hash); + return hr; +} + + +void +TALER_wallet_withdraw_commit ( + const struct TALER_ReservePublicKeyP *reserve_pub, + const struct TALER_Amount *amount, + const struct TALER_Amount *fee, + const struct TALER_HashBlindedPlanchetsP *h_planchets, + const struct TALER_AgeMask *mask, + uint8_t max_age, + struct TALER_WithdrawCommitmentHashP *wch) +{ + struct TALER_WithdrawCommitmentP com = { + .h_reserve_pub = TALER_wallet_hash_reserve_pub (reserve_pub), + }; + fill_withdraw_request_details (amount, + fee, + h_planchets, + mask, + max_age, + &com.details); + + GNUNET_CRYPTO_hash (&com, + sizeof(com), + &wch->hash); +} + + +void +TALER_wallet_withdraw_sign ( + const struct TALER_Amount *amount, + const struct TALER_Amount *fee, + const struct TALER_HashBlindedPlanchetsP *h_planchets, const struct TALER_AgeMask *mask, uint8_t max_age, const struct TALER_ReservePrivateKeyP *reserve_priv, struct TALER_ReserveSignatureP *reserve_sig) { - struct TALER_AgeWithdrawRequestPS req = { - .purpose.size = htonl (sizeof (req)), - .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_RESERVE_AGE_WITHDRAW), - .h_commitment = *h_commitment, - .mask = *mask, - .max_age_group = TALER_get_age_group (mask, max_age) + struct TALER_WithdrawRequestPS req = { + .purpose.size = htonl (sizeof(req)), + .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW), }; - GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv, - &req.reserve_pub.eddsa_pub); - TALER_amount_hton (&req.amount_with_fee, - amount_with_fee); - GNUNET_CRYPTO_eddsa_sign (&reserve_priv->eddsa_priv, - &req, - &reserve_sig->eddsa_signature); + fill_withdraw_request_details (amount, + fee, + h_planchets, + mask, + max_age, + &req.details); + + GNUNET_CRYPTO_eddsa_sign ( + &reserve_priv->eddsa_priv, + &req, + &reserve_sig->eddsa_signature); + } enum GNUNET_GenericReturnValue -TALER_wallet_age_withdraw_verify ( - const struct TALER_AgeWithdrawCommitmentHashP *h_commitment, - const struct TALER_Amount *amount_with_fee, +TALER_wallet_withdraw_verify ( + const struct TALER_Amount *amount, + const struct TALER_Amount *fee, + const struct TALER_HashBlindedPlanchetsP *h_planchets, const struct TALER_AgeMask *mask, uint8_t max_age, const struct TALER_ReservePublicKeyP *reserve_pub, const struct TALER_ReserveSignatureP *reserve_sig) { - struct TALER_AgeWithdrawRequestPS awsrd = { - .purpose.size = htonl (sizeof (awsrd)), - .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_RESERVE_AGE_WITHDRAW), - .reserve_pub = *reserve_pub, - .h_commitment = *h_commitment, - .mask = *mask, - .max_age_group = TALER_get_age_group (mask, max_age) + struct TALER_WithdrawRequestPS req = { + .purpose.size = htonl (sizeof(req)), + .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW), }; - TALER_amount_hton (&awsrd.amount_with_fee, - amount_with_fee); + fill_withdraw_request_details (amount, + fee, + h_planchets, + mask, + max_age, + &req.details); + return GNUNET_CRYPTO_eddsa_verify ( - TALER_SIGNATURE_WALLET_RESERVE_AGE_WITHDRAW, - &awsrd, + TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW, + &req, &reserve_sig->eddsa_signature, &reserve_pub->eddsa_pub); }