exchange

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

commit b087c4c7c06441d24bd088f5d31fc9b556487c18
parent 40f20de4584f21989d9ba876d4cda5e9b75f823a
Author: Özgür Kesim <oec@codeblau.de>
Date:   Sat,  5 Apr 2025 18:19:12 +0200

[melt:wip] started with new /melt handler for v26

- new /melt handler finished, but not activated
- simplified idempotency logic in withdraw and melt
- removed not needed error conditions, also in withdraw

Diffstat:
Msrc/exchange/Makefile.am | 1+
Msrc/exchange/taler-exchange-httpd.c | 11+++++++++++
Asrc/exchange/taler-exchange-httpd_melt_v26.c | 1290+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_melt_v26.h | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_reveal-melt.h | 0
Msrc/exchange/taler-exchange-httpd_withdraw.c | 530++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/exchange/taler-exchange-httpd_withdraw.h | 5+++--
Msrc/include/taler_exchangedb_plugin.h | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 1746 insertions(+), 261 deletions(-)

diff --git a/src/exchange/Makefile.am b/src/exchange/Makefile.am @@ -188,6 +188,7 @@ taler_exchange_httpd_SOURCES = \ taler-exchange-httpd_management_wire_disable.c \ taler-exchange-httpd_management_wire_fees.c \ taler-exchange-httpd_melt.c taler-exchange-httpd_melt.h \ + taler-exchange-httpd_melt_v26.c taler-exchange-httpd_melt_v26.h \ taler-exchange-httpd_metrics.c taler-exchange-httpd_metrics.h \ taler-exchange-httpd_mhd.c taler-exchange-httpd_mhd.h \ taler-exchange-httpd_purses_create.c taler-exchange-httpd_purses_create.h \ diff --git a/src/exchange/taler-exchange-httpd.c b/src/exchange/taler-exchange-httpd.c @@ -61,6 +61,7 @@ #include "taler-exchange-httpd_link.h" #include "taler-exchange-httpd_management.h" #include "taler-exchange-httpd_melt.h" +#include "taler-exchange-httpd_melt_v26.h" #include "taler-exchange-httpd_metrics.h" #include "taler-exchange-httpd_mhd.h" #include "taler-exchange-httpd_purses_create.h" @@ -1739,6 +1740,15 @@ handle_mhd_request (void *cls, .nargs = 2, .nargs_is_upper_bound = true }, +#ifdef MELT_V26 + /* melting operation, introduced with v26 */ + { + .url = "melt", + .method = MHD_HTTP_METHOD_POST, + .handler.post = &TEH_handler_melt_v26, + .nargs = 0 + }, +#endif /* refreshes/$RCH/reveal */ { .url = "refreshes", @@ -2723,6 +2733,7 @@ do_shutdown (void *cls) TEH_batch_deposit_cleanup (); TEH_batch_withdraw_cleanup (); TEH_withdraw_cleanup (); + TEH_melt_v26_cleanup (); TEH_reserves_close_cleanup (); TEH_reserves_purse_cleanup (); TEH_purses_merge_cleanup (); diff --git a/src/exchange/taler-exchange-httpd_melt_v26.c b/src/exchange/taler-exchange-httpd_melt_v26.c @@ -0,0 +1,1290 @@ +/* + 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_melt_v26.c + * @brief Handle /melt requests + * @note This endpoint is active since v26 of the protocol API + * @author Özgür 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_mhd_lib.h" +#include "taler-exchange-httpd_melt_v26.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_keys.h" +#include "taler_util.h" + +/** + * The different type of errors that might occur, sorted by name. + * Some of them require idempotency checks, which are marked + * in @e idempotency_check_required below. + */ +enum MeltError +{ + MELT_ERROR_NONE = 0, + MELT_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, + MELT_ERROR_AMOUNT_OVERFLOW, + MELT_ERROR_AMOUNT_PLUS_FEE_OVERFLOW, + MELT_ERROR_COIN_CIPHER_MISMATCH, + MELT_ERROR_COIN_SIGNATURE_INVALID, + MELT_ERROR_COIN_UNKNOWN, + MELT_ERROR_CONFIRMATION_SIGN, + MELT_ERROR_DB_FETCH_FAILED, + MELT_ERROR_DB_INVARIANT_FAILURE, + MELT_ERROR_DENOMINATION_EXPIRED, + MELT_ERROR_DENOMINATION_KEY_UNKNOWN, + MELT_ERROR_DENOMINATION_REVOKED, + MELT_ERROR_DENOMINATION_SIGN, + MELT_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + MELT_ERROR_FEE_OVERFLOW, + MELT_ERROR_IDEMPOTENT_PLANCHET, + MELT_ERROR_INSUFFICIENT_FUNDS, + MELT_ERROR_KEYS_MISSING, + MELT_ERROR_NONCE_RESUSE, + MELT_ERROR_REQUEST_PARAMETER_MALFORMED, +}; + +/** + * With the bits set in this value will be mark the errors + * that require a check for idempotency before actually + * returning an error. + */ +static const uint64_t idempotency_check_required = + 0 + | (1 << MELT_ERROR_DENOMINATION_EXPIRED) + | (1 << MELT_ERROR_DENOMINATION_KEY_UNKNOWN) + | (1 << MELT_ERROR_DENOMINATION_REVOKED) + | (1 << MELT_ERROR_INSUFFICIENT_FUNDS) + | (1 << MELT_ERROR_KEYS_MISSING); + +#define IDEMPOTENCY_CHECK_REQUIRED(error) \ + (0 != (idempotency_check_required & (1 << (error)))) + +/** + * Context for a /melt request + */ +struct MeltContext +{ + + /** + * This struct is kept in a DLL. + */ + struct MeltContext *prev; + struct MeltContext *next; + + /** + * Processing phase we are in. + * The ordering here partially matters, as we progress through + * them by incrementing the phase in the happy path. + */ + enum + { + MELT_PHASE_CHECK_KEYS, + MELT_PHASE_CHECK_COIN_SIGNATURE, + MELT_PHASE_PREPARE_TRANSACTION, + MELT_PHASE_RUN_TRANSACTION, + MELT_PHASE_GENERATE_REPLY_SUCCESS, + MELT_PHASE_GENERATE_REPLY_ERROR, + MELT_PHASE_RETURN_NO, + MELT_PHASE_RETURN_YES, + } phase; + + + /** + * Request context + */ + const struct TEH_RequestContext *rc; + + /** + * Current time for the DB transaction. + */ + struct GNUNET_TIME_Timestamp now; + + /** + * Captures all parameters provided in the JSON request + */ + struct + { + + /** + * All fields (from the request or computed) + * that we persist in the database. + */ + struct TALER_EXCHANGEDB_Refresh_v26 refresh; + + /** + * In some error cases we check for idempotency. + * If we find an entry in the database, we mark this here. + */ + bool is_idempotent; + + /** + * In some error conditions the request is checked + * for idempotency and the result from the database + * is stored here. + */ + struct TALER_EXCHANGEDB_Refresh_v26 refresh_idem; + + /** + * Refresh master secret, if any of the fresh denominations use CS. + */ + struct TALER_RefreshMasterSecretP rms; + + /** + * True if @e rms is missing. + */ + bool no_rms; + + /** + * Set to true if this coin's denomination was revoked and the operation + * is thus only allowed for zombie coins where the transaction + * history includes a #TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP. + */ + bool zombie_required; + + /** + * Array @e persis.num_coins of hashes of the public keys + * of the denominations to refresh. + */ + struct TALER_DenominationHashP *denoms_h; + + /** + * Number of planchets, that is: @e num_coins * TALER_CNC_KAPPA + */ + size_t num_planchets; + + /** + * Array of @e num_planchets coin planchets, arranged + * in runs of @e num_coins coins, [0..num_coins)..[0..num_coins), + * one for each kappa value. + */ + struct TALER_BlindedPlanchet *planchets; + + /** + * Total (over all coins) amount (excluding fee) committed for the refresh + */ + struct TALER_Amount amount; + + /** + * Melt fee the exchange charged. + */ + struct TALER_Amount fee; + + } request; + + /** + * Errors occurring during evaluation of the request are captured in this + * struct. In phase WITHDRAW_PHASE_GENERATE_REPLY_ERROR an appropriate error + * message is prepared and sent to the client. + */ + struct + { + /* The (internal) error code */ + enum MeltError code; + + /** + * Some errors require details to be sent to the client. + * These are captured in this union. + * Each field is named according to the error that is using it, except + * commented otherwise. + */ + union + { + const char *request_parameter_malformed; + + /** + * For all errors related to a particular denomination, i.e. + * MELT_ERROR_DENOMINATION_KEY_UNKNOWN, + * MELT_ERROR_DENOMINATION_EXPIRED, + * MELT_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + * MELT_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, + * we use this one field. + */ + const struct TALER_DenominationHashP *denom_h; + + const char *db_fetch_context; + + enum TALER_ErrorCode ec_confirmation_sign; + + enum TALER_ErrorCode ec_denomination_sign; + + /* remaining value of the coin */ + struct TALER_Amount insufficient_funds; + + } details; + } error; +}; + +/** + * The following macros set the given error code, + * set the phase to Melt_PHASE_GENERATE_REPLY_ERROR, + * and optionally set the given field (with an optionally given value). + */ +#define SET_ERROR(mc, ec) \ + do \ + { (mc)->error.code = (ec); \ + (mc)->phase = MELT_PHASE_GENERATE_REPLY_ERROR; } while (0) + +#define SET_ERROR_WITH_FIELD(mc, ec, field) \ + do \ + { (mc)->error.code = (ec); \ + (mc)->error.details.field = (field); \ + (mc)->phase = MELT_PHASE_GENERATE_REPLY_ERROR; } while (0) + +#define SET_ERROR_WITH_DETAIL(mc, ec, field, value) \ + do \ + { (mc)->error.code = (ec); \ + (mc)->error.details.field = (value); \ + (mc)->phase = MELT_PHASE_GENERATE_REPLY_ERROR; } while (0) + + +/** + * All melt context is kept in a DLL. + */ +static struct MeltContext *mc_head; +static struct MeltContext *mc_tail; + +void +TEH_melt_v26_cleanup () +{ + struct MeltContext *mc; + + while (NULL != (mc = mc_head)) + { + GNUNET_CONTAINER_DLL_remove (mc_head, + mc_tail, + mc); + MHD_resume_connection (mc->rc->connection); + } +} + + +/** + * Cleanup routine for melt request. + * 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_melt_rc (struct TEH_RequestContext *rc) +{ + struct MeltContext *mc = rc->rh_ctx; + + GNUNET_free (mc->request.denoms_h); + for (unsigned int i = 0; i<mc->request.num_planchets; i++) + TALER_blinded_planchet_free (&mc->request.planchets[i]); + + GNUNET_free (mc->request.planchets); + + if (NULL != mc->request.refresh.denom_sigs) + for (unsigned int i = 0; i<mc->request.refresh.num_coins; i++) + TALER_blinded_denom_sig_free (&mc->request.refresh.denom_sigs[i]); + + GNUNET_free (mc->request.refresh.denom_sigs); + GNUNET_free (mc->request.refresh.h_coin_evs); + GNUNET_free (mc->request.refresh.denom_serials); + + if (mc->request.is_idempotent) + { + for (unsigned int i = 0; i<mc->request.refresh_idem.num_coins; i++) + TALER_blinded_denom_sig_free (&mc->request.refresh_idem.denom_sigs[i]); + + GNUNET_free (mc->request.refresh_idem.h_coin_evs); + GNUNET_free (mc->request.refresh_idem.denom_sigs); + GNUNET_free (mc->request.refresh_idem.denom_serials); + } + + GNUNET_free (mc); +} + + +/** + * Creates a new context for the incoming melt request + * + * @param mc melt request context + * @param root json body of the request + * @return GNUNET_OK on success, GNUNET_SYSERR otherwise (response sent) + */ +static enum GNUNET_GenericReturnValue +melt_new_request ( + struct MeltContext *mc, + const json_t *root) +{ + const json_t *j_denoms_h; + const json_t *j_coin_evs; + enum GNUNET_GenericReturnValue res; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("old_coin_pub", + &mc->request.refresh.coin.coin_pub), + GNUNET_JSON_spec_fixed_auto ("old_denom_pub_h", + &mc->request.refresh.coin.denom_pub_hash), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("old_age_commitment_h", + &mc->request.refresh.coin.h_age_commitment), + &mc->request.refresh.coin.no_age_commitment), + TALER_JSON_spec_amount ("value_with_fee", + TEH_currency, + &mc->request.refresh.amount_with_fee), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("rms", + &mc->request.rms), + &mc->request.no_rms), + GNUNET_JSON_spec_array_const ("denoms_h", + &j_denoms_h), + GNUNET_JSON_spec_array_const ("coin_evs", + &j_coin_evs), + GNUNET_JSON_spec_fixed_auto ("confirm_sig", + &mc->request.refresh.coin_sig), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (mc->rc->connection, + root, + spec); + if (GNUNET_OK != res) + return res; + + /* validate array size */ + { + size_t num_coins = json_array_size (j_denoms_h); + size_t array_size = json_array_size (j_coin_evs); + const char *error; + + _Static_assert ( + TALER_MAX_FRESH_COINS < INT_MAX / TALER_CNC_KAPPA, + "TALER_MAX_FRESH_COINS too large"); + +#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 melt 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 meltn has been exceeded") + + BAIL_IF ((TALER_CNC_KAPPA * num_coins) != array_size, + "coin_evs must be an array of length " + TALER_CNC_KAPPA_STR + "*len(denoms_h)") + + mc->request.refresh.num_coins = num_coins; + error = NULL; + + } while (0); +#undef BAIL_IF + + if (NULL != error) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + error); + return GNUNET_SYSERR; + } + } + /* extract the denomination hashes */ + { + size_t idx; + json_t *value; + + mc->request.denoms_h + = GNUNET_new_array (mc->request.refresh.num_coins, + struct TALER_DenominationHashP); + + json_array_foreach (j_denoms_h, idx, value) { + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_fixed_auto (NULL, + &mc->request.denoms_h[idx]), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (mc->rc->connection, + value, + ispec); + if (GNUNET_OK != res) + return res; + } + } + /* Calculate the hash over the blinded coin envelopes */ + { + struct GNUNET_HashContext *hash_context; + + hash_context = GNUNET_CRYPTO_hash_context_start (); + GNUNET_assert (NULL != hash_context); + + mc->request.planchets = + GNUNET_new_array (mc->request.num_planchets, + struct TALER_BlindedPlanchet); + + /* Parse blinded envelopes. */ + { + json_t *j_cev; + size_t idx; + + 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, + &mc->request.planchets[idx]), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (mc->rc->connection, + j_cev, + kspec); + if (GNUNET_OK != res) + return res; + + /* Continue to hash of the coin candidates */ + { + struct TALER_BlindedCoinHashP bch; + + TALER_coin_ev_hash ( + &mc->request.planchets[idx], + &mc->request.denoms_h[idx % mc->request.refresh.num_coins], + &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 (size_t i = 0; i < idx; i++) + { + if (0 == + TALER_blinded_planchet_cmp ( + &mc->request.planchets[idx], + &mc->request.planchets[i])) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_IDEMPOTENT_PLANCHET); + return GNUNET_SYSERR; + } + } /* end duplicate check */ + } /* json_array_foreach over j_coin_evs */ + } /* scope of j_kappa_planchets, idx */ + + /* Finally, calculate the hash from all blinded envelopes */ + GNUNET_CRYPTO_hash_context_finish (hash_context, + &mc->request.refresh.rc.session_hash); + } /* scope of hash_context */ + return GNUNET_OK; +} + + +/** + * Check if the given denomination is still or already valid, has not been + * revoked and potentically supports age restriction. + * + * @param[in,out] mc context for the melt 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 MeltContext *mc, + 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 (mc, + MELT_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 (mc, + MELT_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 (mc, + MELT_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + denom_h); + return GNUNET_SYSERR; + } + + if (dk->recoup_possible) + { + SET_ERROR (mc, + MELT_ERROR_DENOMINATION_REVOKED); + return GNUNET_SYSERR; + } + + /* In case of age melt, make sure that the denomitation supports age restriction */ + if (! mc->request.refresh.coin.no_age_commitment) + { + if (0 == dk->denom_pub.age_mask.bits) + { + GNUNET_break_op (0); + SET_ERROR_WITH_FIELD (mc, + MELT_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, + denom_h); + return GNUNET_SYSERR; + } + } + + *pdk = dk; + return GNUNET_OK; +} + + +/** + * Check if the given array of hashes of denomination_keys + * - belong to valid denominations + * - calculate the total amount of the denominations including fees + * for melt. + * + * @param mc context of the melt to check keys for + */ +static void +phase_check_keys ( + struct MeltContext *mc) +{ + struct TEH_KeyStateHandle *ksh; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + GNUNET_break (0); + SET_ERROR (mc, + MELT_ERROR_KEYS_MISSING); + return; + } + + mc->request.refresh.denom_serials = + GNUNET_new_array (mc->request.refresh.num_coins, + uint64_t); + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &mc->request.amount)); + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &mc->request.fee)); + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &mc->request.refresh.amount_with_fee)); + + for (unsigned int i = 0; i < mc->request.refresh.num_coins; i++) + { + struct TEH_DenominationKey *dk; + + if (GNUNET_OK != find_denomination ( + mc, + ksh, + &mc->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++) + { + size_t off = k * mc->request.refresh.num_coins; + + if (dk->denom_pub.bsign_pub_key->cipher != + mc->request.planchets[i + off].blinded_message->cipher) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_COIN_CIPHER_MISMATCH); + return; + } + } + } + + /* Accumulate the values */ + if (0 > TALER_amount_add (&mc->request.amount, + &mc->request.amount, + &dk->meta.value)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_AMOUNT_OVERFLOW); + return; + } + + /* Accumulate the melt fees */ + if (0 > TALER_amount_add (&mc->request.fee, + &mc->request.fee, + &dk->meta.fees.refresh)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_FEE_OVERFLOW); + return; + } + mc->request.refresh.denom_serials[i] = dk->meta.serial; + } + + /* Save the total amount including fees */ + if (0 > TALER_amount_add ( + &mc->request.refresh.amount_with_fee, + &mc->request.amount, + &mc->request.fee)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_AMOUNT_PLUS_FEE_OVERFLOW); + return; + } + + mc->phase++; +} + + +/** + * Check that the client signature authorizing the melt is valid. + * + * @param[in,out] mc request context to check + */ +static void +phase_check_coin_signature ( + struct MeltContext *mc) +{ + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_melt_verify ( + &mc->request.refresh.amount_with_fee, + &mc->request.fee, + &mc->request.refresh.rc, + &mc->request.refresh.coin.denom_pub_hash, + &mc->request.refresh.coin.h_age_commitment, + &mc->request.refresh.coin.coin_pub, + &mc->request.refresh.coin_sig)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_COIN_SIGNATURE_INVALID); + return; + } + + mc->phase++; +} + + +/** + * The request for melt was parsed successfully. + * Sign and persist the chosen blinded coins for the reveal step. + * + * @param mc The context for the current melt request + */ +static void +phase_prepare_transaction ( + struct MeltContext *mc) +{ + size_t offset = 0; + + mc->request.refresh.denom_sigs + = GNUNET_new_array ( + mc->request.refresh.num_coins, + struct TALER_BlindedDenominationSignature); + mc->request.refresh.h_coin_evs + = GNUNET_new_array ( + mc->request.refresh.num_coins, + struct TALER_BlindedCoinHashP); + mc->request.refresh.noreveal_index = + GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_STRONG, + TALER_CNC_KAPPA); + offset = mc->request.refresh.noreveal_index * mc->request.refresh.num_coins; + GNUNET_assert (offset + mc->request.refresh.num_coins <= + mc->request.num_planchets); + + /* Choose and sign the coins */ + { + struct TEH_CoinSignData csds[mc->request.refresh.num_coins]; + enum TALER_ErrorCode ec_denomination_sign; + + memset (csds, + 0, + sizeof(csds)); + + /* Pick the chosen blinded coins */ + for (uint32_t i = 0; i<mc->request.refresh.num_coins; i++) + { + csds[i].bp = &mc->request.planchets[i + offset]; + csds[i].h_denom_pub = &mc->request.denoms_h[i]; + } + + ec_denomination_sign = TEH_keys_denomination_batch_sign ( + mc->request.refresh.num_coins, + csds, + false, + mc->request.refresh.denom_sigs); + if (TALER_EC_NONE != ec_denomination_sign) + { + GNUNET_break (0); + SET_ERROR_WITH_FIELD (mc, + MELT_ERROR_DENOMINATION_SIGN, + ec_denomination_sign); + return; + } + + /* Prepare the hashes of the coins for insertion */ + for (uint32_t i = 0; i<mc->request.refresh.num_coins; i++) + { + TALER_coin_ev_hash (&mc->request.planchets[i + offset], + &mc->request.denoms_h[i], + &mc->request.refresh.h_coin_evs[i]); + } + } + + mc->phase++; +} + + +/** + * Terminate the main loop by returning the final result. + * + * @param[in,out] mc context to update phase for + * @param mres MHD status to return + */ +static void +finish_loop (struct MeltContext *mc, + MHD_RESULT mres) +{ + mc->phase = (MHD_YES == mres) + ? MELT_PHASE_RETURN_YES + : MELT_PHASE_RETURN_NO; +} + + +/** + * Generates response for the melt request. + * + * @param mc melt operation context + */ +static void +phase_generate_reply_success (struct MeltContext *mc) +{ + struct TALER_EXCHANGEDB_Refresh_v26 *db_obj; + + db_obj = mc->request.is_idempotent + ? &mc->request.refresh_idem + : &mc->request.refresh; + + { + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + enum TALER_ErrorCode ec_confirmation_sign; + + ec_confirmation_sign = + TALER_exchange_online_melt_confirmation_sign ( + &TEH_keys_exchange_sign_, + &db_obj->rc, + db_obj->noreveal_index, + &pub, + &sig); + if (TALER_EC_NONE != ec_confirmation_sign) + { + SET_ERROR_WITH_FIELD (mc, + MELT_ERROR_CONFIRMATION_SIGN, + ec_confirmation_sign); + return; + } + + finish_loop (mc, + TALER_MHD_REPLY_JSON_PACK ( + mc->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))); + } +} + + +/** + * Check if the melt request is replayed and we already have an answer. + * If so, replay the existing answer and return the HTTP response. + * + * @param[in,out] mc 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 +melt_is_idempotent ( + struct MeltContext *mc) +{ + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->get_refresh ( + TEH_plugin->cls, + &mc->request.refresh.rc, + &mc->request.refresh_idem); + if (0 > qs) + { + /* FIXME: soft error not handled correctly! */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_DB_FETCH_FAILED, + db_fetch_context, + "get_melt"); + return true; /* Well, kind-of. */ + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return false; + + mc->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_MELT]++; + mc->phase = MELT_PHASE_GENERATE_REPLY_SUCCESS; + return true; +} + + +/** + * 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 mc melt context + */ +static void +phase_generate_reply_error ( + struct MeltContext *mc) +{ + GNUNET_assert (MELT_PHASE_GENERATE_REPLY_ERROR == mc->phase); + GNUNET_assert (MELT_ERROR_NONE != mc->error.code); + + if (IDEMPOTENCY_CHECK_REQUIRED (mc->error.code) + && melt_is_idempotent (mc)) + { + return; + } + + switch (mc->error.code) + { + case MELT_ERROR_NONE: + { + GNUNET_break (0); + mc->phase = MELT_PHASE_RETURN_YES; + return; + } + + case MELT_ERROR_REQUEST_PARAMETER_MALFORMED: + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + mc->error.details.request_parameter_malformed); + break; + + case MELT_ERROR_KEYS_MISSING: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL)); + break; + + case MELT_ERROR_DB_FETCH_FAILED: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + mc->error.details.db_fetch_context)); + break; + + case MELT_ERROR_DB_INVARIANT_FAILURE: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + NULL)); + break; + + case MELT_ERROR_COIN_UNKNOWN: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_COIN_UNKNOWN, + NULL)); + break; + + case MELT_ERROR_DENOMINATION_SIGN: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + mc->error.details.ec_denomination_sign, + NULL)); + break; + + case MELT_ERROR_DENOMINATION_KEY_UNKNOWN: + GNUNET_break_op (0); + finish_loop (mc, + TEH_RESPONSE_reply_unknown_denom_pub_hash ( + mc->rc->connection, + mc->error.details.denom_h)); + break; + + case MELT_ERROR_DENOMINATION_EXPIRED: + GNUNET_break_op (0); + finish_loop (mc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + mc->rc->connection, + mc->error.details.denom_h, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + NULL)); + break; + + case MELT_ERROR_DENOMINATION_VALIDITY_IN_FUTURE: + finish_loop (mc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + mc->rc->connection, + mc->error.details.denom_h, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + NULL)); + break; + + case MELT_ERROR_DENOMINATION_REVOKED: + GNUNET_break_op (0); + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, + NULL)); + break; + + case MELT_ERROR_COIN_CIPHER_MISMATCH: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, + NULL)); + break; + + case MELT_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION: + { + char msg[256]; + GNUNET_snprintf (msg, + sizeof(msg), + "denomination %s does not support age restriction", + GNUNET_h2s (&mc->error.details.denom_h->hash)); + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN, + msg)); + break; + } + + case MELT_ERROR_AMOUNT_OVERFLOW: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW, + "amount")); + + case MELT_ERROR_FEE_OVERFLOW: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW, + "fee")); + break; + + case MELT_ERROR_AMOUNT_PLUS_FEE_OVERFLOW: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW, + "amount+fee")); + break; + + + case MELT_ERROR_CONFIRMATION_SIGN: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + mc->error.details.ec_confirmation_sign, + NULL)); + break; + + case MELT_ERROR_INSUFFICIENT_FUNDS: + finish_loop (mc, + TEH_RESPONSE_reply_coin_insufficient_funds ( + mc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &mc->request.refresh.coin.denom_pub_hash, + &mc->request.refresh.coin.coin_pub)); + break; + + case MELT_ERROR_IDEMPOTENT_PLANCHET: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, /* FIXME: new error! */ + "idempotent planchet")); + break; + + case MELT_ERROR_NONCE_RESUSE: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, /* FIXME: new error */ + "nonce reuse")); + break; + + case MELT_ERROR_COIN_SIGNATURE_INVALID: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_EXCHANGE_MELT_COIN_SIGNATURE_INVALID, + NULL)); + break; + } +} + + +/** + * Function implementing melt 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 MeltContext *` + * @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 +melt_transaction ( + void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct MeltContext *mc = cls; + enum GNUNET_DB_QueryStatus qs; + bool balance_ok; + bool zombie_required; + bool conflict; + struct TALER_Amount insufficient_funds; + + qs = TEH_plugin->do_melt_v26 (TEH_plugin->cls, + &mc->request.refresh, + &mc->request.rms, + &mc->now, + &conflict, + &zombie_required, + &balance_ok, + &insufficient_funds); + if (0 > qs) + { + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_DB_FETCH_FAILED, + db_fetch_context, + "do_melt_v26"); + return qs; + } + + if (0 == qs) + { + SET_ERROR (mc, + MELT_ERROR_COIN_UNKNOWN); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if (! balance_ok) + { + TEH_plugin->rollback (TEH_plugin->cls); + + SET_ERROR_WITH_FIELD (mc, + MELT_ERROR_INSUFFICIENT_FUNDS, + insufficient_funds); + + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if (conflict) + { + /* do_melt_v26 signaled a conflict, so there MUST be an entry + * in the DB. Put that into the response */ + if (! melt_is_idempotent (mc)) + { + GNUNET_break (0); + SET_ERROR (mc, + MELT_ERROR_DB_INVARIANT_FAILURE); + } + return GNUNET_DB_STATUS_HARD_ERROR; /* Done, not error really. */ + } + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + TEH_METRICS_num_success[TEH_MT_SUCCESS_MELT]++; + return qs; +} + + +/** + * The request was prepared successfully. + * Run the main DB transaction. + * + * @param mc The context for the current melt request + */ +static void +phase_run_transaction ( + struct MeltContext *mc) +{ + MHD_RESULT mhd_ret; + enum GNUNET_GenericReturnValue qs; + + GNUNET_assert (MELT_PHASE_RUN_TRANSACTION == + mc->phase); + + qs = TEH_DB_run_transaction (mc->rc->connection, + "run melt", + TEH_MT_REQUEST_MELT, + &mhd_ret, + &melt_transaction, + mc); + + if (GNUNET_OK != qs) + { + /* TODO[oec]: Logic still ok with new error handling? */ + if (MELT_PHASE_RUN_TRANSACTION == mc->phase) + finish_loop (mc, + mhd_ret); + return; + } + mc->phase++; +} + + +MHD_RESULT +TEH_handler_melt_v26 ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[0]) +{ + + struct MeltContext *mc = rc->rh_ctx; + enum GNUNET_GenericReturnValue r; + + (void) args; + + if (NULL == mc) + { + mc = GNUNET_new (struct MeltContext); + rc->rh_ctx = mc; + rc->rh_cleaner = &clean_melt_rc; + mc->rc = rc; + mc->now = GNUNET_TIME_timestamp_get (); + r = melt_new_request (mc, + root); + if (GNUNET_OK != r) + return (GNUNET_SYSERR == r) ? MHD_NO : MHD_YES; + mc->phase = MELT_PHASE_CHECK_KEYS; + } + + while (true) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "melt processing in phase %d\n", + mc->phase); + switch (mc->phase) + { + case MELT_PHASE_CHECK_KEYS: + phase_check_keys (mc); + break; + case MELT_PHASE_CHECK_COIN_SIGNATURE: + phase_check_coin_signature (mc); + break; + case MELT_PHASE_PREPARE_TRANSACTION: + phase_prepare_transaction (mc); + break; + case MELT_PHASE_RUN_TRANSACTION: + phase_run_transaction (mc); + break; + case MELT_PHASE_GENERATE_REPLY_SUCCESS: + phase_generate_reply_success (mc); + break; + case MELT_PHASE_GENERATE_REPLY_ERROR: + phase_generate_reply_error (mc); + break; + case MELT_PHASE_RETURN_YES: + return MHD_YES; + case MELT_PHASE_RETURN_NO: + return MHD_NO; + } + } +} diff --git a/src/exchange/taler-exchange-httpd_melt_v26.h b/src/exchange/taler-exchange-httpd_melt_v26.h @@ -0,0 +1,52 @@ +/* + 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_melt_v26.h + * @brief Handle /melt requests, starting with v26 of the protocol + * @author Özgür Kesim + */ +#ifndef TALER_EXCHANGE_HTTPD_MELT_V26_H +#define TALER_EXCHANGE_HTTPD_MELT_V26_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-exchange-httpd.h" + +/** + * Resume suspended connections, we are shutting down. + */ +void +TEH_melt_v26_cleanup (void); + + +/** + * Handle a "/melt" request. Parses the request into the JSON + * components and validates the melted coins, the signature and + * execute the melt as database transaction. + * + * @param rc the request context + * @param root uploaded JSON data + * @param args array of additional options, not used + * @return MHD result code + */ +MHD_RESULT +TEH_handler_melt_v26 ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[0]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_reveal-melt.h b/src/exchange/taler-exchange-httpd_reveal-melt.h diff --git a/src/exchange/taler-exchange-httpd_withdraw.c b/src/exchange/taler-exchange-httpd_withdraw.c @@ -37,50 +37,59 @@ #include "taler_util.h" /** - * This bit will be set on errors of type #WithdrawError - * that require a check for idempotency before actually - * returning an error. - * This value must be larger than the number of entries in #WithdrawError. - */ -#define IDEMPOTENCY_FLAG 1 << 5 - -/** * 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. + * in @e idempotency_check_required below. */ enum WithdrawError { - WITHDRAW_ERROR_NONE = 0, - WITHDRAW_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION = 1, - WITHDRAW_ERROR_AGE_RESTRICTION_REQUIRED = 2, - WITHDRAW_ERROR_AMOUNT_OVERFLOW = 3, - WITHDRAW_ERROR_AMOUNT_PLUS_FEE_OVERFLOW = 4, - WITHDRAW_ERROR_CIPHER_MISMATCH = 5, - WITHDRAW_ERROR_CONFIRMATION_SIGN = 6, - WITHDRAW_ERROR_DB_FETCH_FAILED = 7, - WITHDRAW_ERROR_DB_INVARIANT_FAILURE = 8, - WITHDRAW_ERROR_DENOMINATION_EXPIRED = 9 | IDEMPOTENCY_FLAG, - WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN = 10 | IDEMPOTENCY_FLAG, - WITHDRAW_ERROR_DENOMINATION_REVOKED = 11 | IDEMPOTENCY_FLAG, - WITHDRAW_ERROR_DENOMINATION_SIGN = 12, - WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE = 13, - WITHDRAW_ERROR_FEE_OVERFLOW = 14, - WITHDRAW_ERROR_IDEMPOTENT_PLANCHET = 15 | IDEMPOTENCY_FLAG, - WITHDRAW_ERROR_INSUFFICIENT_FUNDS = 16 | IDEMPOTENCY_FLAG, - WITHDRAW_ERROR_INTERNAL_INVARIANT_FAILURE = 17, - WITHDRAW_ERROR_KEYS_MISSING = 18 | IDEMPOTENCY_FLAG, - WITHDRAW_ERROR_KYC_REQUIRED = 19 | IDEMPOTENCY_FLAG, - WITHDRAW_ERROR_LEGITIMIZATION_RESULT = 20, - WITHDRAW_ERROR_MAXIMUM_AGE_TOO_LARGE = 21, - WITHDRAW_ERROR_NONCE_RESUSE = 22, - WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED = 23, - WITHDRAW_ERROR_RESERVE_CIPHER_UNKNOWN = 24, - WITHDRAW_ERROR_RESERVE_SIGNATURE_INVALID = 25, - WITHDRAW_ERROR_RESERVE_UNKNOWN = 26, + WITHDRAW_ERROR_NONE, + WITHDRAW_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, + WITHDRAW_ERROR_AGE_RESTRICTION_REQUIRED, + WITHDRAW_ERROR_AMOUNT_OVERFLOW, + WITHDRAW_ERROR_AMOUNT_PLUS_FEE_OVERFLOW, + WITHDRAW_ERROR_CIPHER_MISMATCH, + WITHDRAW_ERROR_CONFIRMATION_SIGN, + WITHDRAW_ERROR_DB_FETCH_FAILED, + WITHDRAW_ERROR_DB_INVARIANT_FAILURE, + WITHDRAW_ERROR_DENOMINATION_EXPIRED, + WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN, + WITHDRAW_ERROR_DENOMINATION_REVOKED, + WITHDRAW_ERROR_DENOMINATION_SIGN, + WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + WITHDRAW_ERROR_FEE_OVERFLOW, + WITHDRAW_ERROR_IDEMPOTENT_PLANCHET, + WITHDRAW_ERROR_INSUFFICIENT_FUNDS, + WITHDRAW_ERROR_KEYS_MISSING, + WITHDRAW_ERROR_KYC_REQUIRED, + WITHDRAW_ERROR_LEGITIMIZATION_RESULT, + WITHDRAW_ERROR_MAXIMUM_AGE_TOO_LARGE, + WITHDRAW_ERROR_NONCE_RESUSE, + WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED, + WITHDRAW_ERROR_RESERVE_CIPHER_UNKNOWN, + WITHDRAW_ERROR_RESERVE_SIGNATURE_INVALID, + WITHDRAW_ERROR_RESERVE_UNKNOWN, }; /** + * With the bits set in this value will be mark the errors + * that require a check for idempotency before actually + * returning an error. + */ +static const uint64_t idempotency_check_required = + 0 + | (1 << WITHDRAW_ERROR_DENOMINATION_EXPIRED) + | (1 << WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN) + | (1 << WITHDRAW_ERROR_DENOMINATION_REVOKED) + | (1 << WITHDRAW_ERROR_INSUFFICIENT_FUNDS) + | (1 << WITHDRAW_ERROR_KEYS_MISSING) + | (1 << WITHDRAW_ERROR_KYC_REQUIRED); + +#define IDEMPOTENCY_CHECK_REQUIRED(ec) \ + (0 != (idempotency_check_required & (1 << (ec)))) + + +/** * Context for a /withdraw requests */ struct WithdrawContext @@ -334,20 +343,29 @@ withdraw_is_idempotent ( struct WithdrawContext *wc) { enum GNUNET_DB_QueryStatus qs; + uint8_t max_retries = 3; + + while (0 < max_retries--) + { + qs = TEH_plugin->get_withdraw ( + TEH_plugin->cls, + &wc->request.persist.h_planchets, + &wc->request.idem); + + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + continue; + + break; + } - qs = TEH_plugin->get_withdraw ( - TEH_plugin->cls, - &wc->request.persist.h_planchets, - &wc->request.idem); if (0 > qs) { - /* FIXME: soft error not handled correctly! */ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - SET_ERROR_WITH_DETAIL (wc, - WITHDRAW_ERROR_DB_FETCH_FAILED, - db_fetch_context, - "get_withdraw"); + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + SET_ERROR_WITH_DETAIL (wc, + WITHDRAW_ERROR_DB_FETCH_FAILED, + db_fetch_context, + "get_withdraw"); return true; /* Well, kind-of. */ } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) @@ -412,7 +430,7 @@ withdraw_transaction ( return qs; } - if (0 == qs) + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { SET_ERROR (wc, WITHDRAW_ERROR_RESERVE_UNKNOWN); @@ -421,10 +439,19 @@ withdraw_transaction ( if (! age_ok) { - wc->error.details.maximum_age_too_large.max_allowed = allowed_maximum_age; - wc->error.details.maximum_age_too_large.birthday = reserve_birthday; - SET_ERROR (wc, - WITHDRAW_ERROR_MAXIMUM_AGE_TOO_LARGE); + if (wc->request.persist.age_proof_required) + { + wc->error.details.maximum_age_too_large.max_allowed = allowed_maximum_age; + wc->error.details.maximum_age_too_large.birthday = reserve_birthday; + SET_ERROR (wc, + WITHDRAW_ERROR_MAXIMUM_AGE_TOO_LARGE); + } + else + { + wc->error.details.age_restriction_required = allowed_maximum_age; + SET_ERROR (wc, + WITHDRAW_ERROR_AGE_RESTRICTION_REQUIRED); + } return GNUNET_DB_STATUS_HARD_ERROR; } @@ -443,10 +470,13 @@ withdraw_transaction ( { /* do_withdraw signaled a conflict, so there MUST be an entry * in the DB. Put that into the response */ - 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 (! withdraw_is_idempotent (wc)) + { + GNUNET_break (0); + SET_ERROR (wc, + WITHDRAW_ERROR_DB_INVARIANT_FAILURE); + } + return GNUNET_DB_STATUS_HARD_ERROR; /* Done, not error really. */ } if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) @@ -1048,7 +1078,7 @@ clean_withdraw_rc (struct TEH_RequestContext *rc) /** - * Generates response for the batch- or age-withdraw request. + * Generates response for the withdraw request. * * @param wc withdraw operation context */ @@ -1136,8 +1166,8 @@ phase_generate_reply_error ( GNUNET_assert (WITHDRAW_PHASE_GENERATE_REPLY_ERROR == wc->phase); GNUNET_assert (WITHDRAW_ERROR_NONE != wc->error.code); - if ((0 != (wc->error.code & IDEMPOTENCY_FLAG)) && - withdraw_is_idempotent (wc)) + if (IDEMPOTENCY_CHECK_REQUIRED (wc->error.code) + && withdraw_is_idempotent (wc)) { return; } @@ -1186,15 +1216,6 @@ phase_generate_reply_error ( NULL)); break; - case WITHDRAW_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 WITHDRAW_ERROR_RESERVE_UNKNOWN: finish_loop (wc, TALER_MHD_reply_with_ec ( @@ -1354,17 +1375,13 @@ phase_generate_reply_error ( break; case WITHDRAW_ERROR_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; - } + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_WITHDRAW_IDEMPOTENT_PLANCHET, + NULL)); + break; case WITHDRAW_ERROR_NONCE_RESUSE: finish_loop (wc, @@ -1407,218 +1424,213 @@ withdraw_new_request ( struct WithdrawContext *wc, const json_t *root) { + 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_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->request.persist.reserve_sig), + 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; + + /* For now, we only support cipher "ED25519" for signatures by the reserve */ + if (0 != strcmp ("ED25519", + cipher)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (wc, + WITHDRAW_ERROR_RESERVE_CIPHER_UNKNOWN, + reserve_cipher_unknown, + cipher); + return GNUNET_SYSERR; + } + + wc->request.persist.age_proof_required = ! no_max_age; - /* parse the json body */ + if (wc->request.persist.age_proof_required) { - 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_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->request.persist.reserve_sig), - 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; - - /* For now, we only support cipher "ED25519" for signatures by the reserve */ - if (0 != strcmp ("ED25519", - cipher)) + /* 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, - WITHDRAW_ERROR_RESERVE_CIPHER_UNKNOWN, - reserve_cipher_unknown, - cipher); + SET_ERROR_WITH_DETAIL ( + wc, + WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + "max_age must be the lower edge of an age group"); return GNUNET_SYSERR; } + } - wc->request.persist.age_proof_required = ! no_max_age; - - if (wc->request.persist.age_proof_required) - { - /* 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, - WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED, - request_parameter_malformed, - "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_denoms_h); - size_t array_size = json_array_size (j_coin_evs); - const char *error; + /* validate array size */ + { + size_t num_coins = json_array_size (j_denoms_h); + size_t array_size = json_array_size (j_coin_evs); + const char *error; - _Static_assert ( - TALER_MAX_FRESH_COINS < INT_MAX / TALER_CNC_KAPPA, - "TALER_MAX_FRESH_COINS too large"); + _Static_assert ( + TALER_MAX_FRESH_COINS < INT_MAX / TALER_CNC_KAPPA, + "TALER_MAX_FRESH_COINS too large"); #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! - */ - 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_proof_required) && - (num_coins !=array_size), - "denoms_h and coin_evs must be arrays of the same size") - - BAIL_IF (wc->request.persist.age_proof_required && - ((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; - error = NULL; - - } while (0); + 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! + */ + 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_proof_required) && + (num_coins !=array_size), + "denoms_h and coin_evs must be arrays of the same size") + + BAIL_IF (wc->request.persist.age_proof_required && + ((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; + error = NULL; + + } while (0); #undef BAIL_IF - if (NULL != error) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (wc, - WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED, - request_parameter_malformed, - error); - return GNUNET_SYSERR; - } + if (NULL != error) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (wc, + WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + error); + return GNUNET_SYSERR; + } + } + /* extract the denomination hashes */ + { + size_t idx; + json_t *value; + + wc->request.denoms_h + = GNUNET_new_array (wc->request.persist.num_coins, + struct TALER_DenominationHashP); + + json_array_foreach (j_denoms_h, idx, value) { + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_fixed_auto (NULL, + &wc->request.denoms_h[idx]), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (wc->rc->connection, + value, + ispec); + if (GNUNET_OK != res) + return res; } - /* extract the denomination hashes */ + } + /* Calculate the hash over the blinded coin envelopes */ + { + struct GNUNET_HashContext *hash_context; + + hash_context = GNUNET_CRYPTO_hash_context_start (); + GNUNET_assert (NULL != hash_context); + + wc->request.planchets = + GNUNET_new_array (wc->request.num_planchets, + struct TALER_BlindedPlanchet); + + /* Parse blinded envelopes. */ { + json_t *j_cev; size_t idx; - json_t *value; - wc->request.denoms_h - = GNUNET_new_array (wc->request.persist.num_coins, - struct TALER_DenominationHashP); + json_array_foreach (j_coin_evs, idx, j_cev) { + /* Now parse the individual envelopes and calculate the hash of + * the commitment along the way. */ - json_array_foreach (j_denoms_h, idx, value) { - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_fixed_auto (NULL, - &wc->request.denoms_h[idx]), + 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, - value, - ispec); + j_cev, + kspec); if (GNUNET_OK != res) return res; - } - } - /* Calculate the hash over the blinded coin envelopes */ - { - struct GNUNET_HashContext *hash_context; - - hash_context = GNUNET_CRYPTO_hash_context_start (); - GNUNET_assert (NULL != hash_context); - wc->request.planchets = - GNUNET_new_array (wc->request.num_planchets, - struct TALER_BlindedPlanchet); + /* Continue to hash of the coin candidates */ + { + 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)); + } - /* Parse blinded envelopes. */ - { - json_t *j_cev; - size_t idx; - - 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 */ + /* Check for duplicate planchets. Technically a bug on + * the client side that is harmless for us, but still + * not allowed per protocol */ + for (size_t i = 0; i < idx; i++) + { + if (0 == + TALER_blinded_planchet_cmp ( + &wc->request.planchets[idx], + &wc->request.planchets[i])) { - 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)); + GNUNET_break_op (0); + SET_ERROR (wc, + WITHDRAW_ERROR_IDEMPOTENT_PLANCHET); + return GNUNET_SYSERR; } + } /* end duplicate check */ + } /* json_array_foreach over j_coin_evs */ + } /* scope of j_kappa_planchets, idx */ + + /* Finally, calculate the hash from all blinded envelopes */ + GNUNET_CRYPTO_hash_context_finish (hash_context, + &wc->request.persist.h_planchets.hash); + } /* scope of hash_context */ - /* Check for duplicate planchets. Technically a bug on - * the client side that is harmless for us, but still - * not allowed per protocol */ - for (size_t i = 0; i < idx; i++) - { - if (0 == - TALER_blinded_planchet_cmp ( - &wc->request.planchets[idx], - &wc->request.planchets[i])) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (wc, - WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED, - request_parameter_malformed, - "duplicate planchet"); - return GNUNET_SYSERR; - } - } /* end duplicate check */ - } /* json_array_foreach over j_coin_evs */ - } /* scope of j_kappa_planchets, idx */ - - /* Finally, calculate the hash from all blinded envelopes */ - GNUNET_CRYPTO_hash_context_finish (hash_context, - &wc->request.persist.h_planchets.hash); - } /* scope of hash_context */ - } /* scope of j_denoms_h, j_blinded_coin_evs */ return GNUNET_OK; } @@ -1627,7 +1639,7 @@ MHD_RESULT TEH_handler_withdraw ( struct TEH_RequestContext *rc, const json_t *root, - const char *const args[2]) + const char *const args[0]) { struct WithdrawContext *wc = rc->rh_ctx; enum GNUNET_GenericReturnValue r; diff --git a/src/exchange/taler-exchange-httpd_withdraw.h b/src/exchange/taler-exchange-httpd_withdraw.h @@ -24,6 +24,7 @@ #include <microhttpd.h> #include "taler-exchange-httpd.h" + /** * Resume suspended connections, we are shutting down. */ @@ -48,13 +49,13 @@ TEH_withdraw_cleanup (void); * * @param rc request context * @param root uploaded JSON data - * @param args array of additional options + * @param args array of additional options, not used. * @return MHD result code */ MHD_RESULT TEH_handler_withdraw ( struct TEH_RequestContext *rc, const json_t *root, - const char *const args[2]); + const char *const args[0]); #endif diff --git a/src/include/taler_exchangedb_plugin.h b/src/include/taler_exchangedb_plugin.h @@ -2475,6 +2475,78 @@ struct TALER_EXCHANGEDB_Melt /** + * Information about a melt operation since v26 of the protocol. + * This also includes the information for the reveal phase. + */ +struct TALER_EXCHANGEDB_Refresh_v26 +{ + /** + * Information about the coin that is being melted. + */ + struct TALER_CoinPublicInfo coin; + + /** + * Signature over the melting operation. + */ + struct TALER_CoinSpendSignatureP coin_sig; + + /** + * Refresh commitment this coin is melted into. + */ + struct TALER_RefreshCommitmentP rc; + + /** + * How much value is being melted? This amount includes the fees, + * so the final amount contributed to the melt is this value minus + * the fee for melting the coin. We include the fee 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; + + /** + * Number of coins to be refreshed into + */ + size_t num_coins; + + /** + * Array of @a num_coins blinded coin envelopes, chosen by the exchange + * at @e noreveal_index from the kappa*num_coins candidates provided by + * the client. + */ + struct TALER_BlindedCoinHashP *h_coin_evs; + + /** + * Array of @a num_coins denomination signatures of the blinded coins + * @a h_coin_evs. + */ + struct TALER_BlindedDenominationSignature *denom_sigs; + + /** + * Array of @a num_coins serial id's of the denominations, corresponding to + * the coins in @a h_coin_evs. + * If @e coin.no_age_commitment is false, the denominations + * MUST support age restriction. + */ + uint64_t *denom_serials; + + /** + * Index (smaller #TALER_CNC_KAPPA) which the exchange has chosen to not + * have revealed during cut and choose. + */ + uint32_t noreveal_index; + + /** + * [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_melt_v26 + */ + struct TALER_DenominationHashP *denom_pub_hashes; +}; + + +/** * @brief Linked list of refresh information linked to a coin. */ struct TALER_EXCHANGEDB_LinkList @@ -4401,6 +4473,52 @@ struct TALER_EXCHANGEDB_Plugin /** + * Perform melt operation--introduced with v26 of the API--, + * checking for sufficient balance of the coin and possibly persisting the melt details. + * + * @param cls the plugin-specific state + * @param[in,out] refresh refresh operation details; the noreveal_index + * is set in case the coin was already melted before + * @param rms client-contributed input for CS denominations that must be checked + * for idempotency, or NULL for non-CS withdrawals + * @param timestamp the timestamp of this melt operation, helpful for the coin history. + * @param[in,out] zombie_required true if the melt must only succeed + * if the coin is a zombie, set to false if the requirement was satisfied + * @param[out] conflict set to true if there exists already an entry in the database for + * the calculated commitment hash. + * @param[out] balance_ok set to true if the balance was sufficient + * @param[out] insufficient_funds if balance_ok is false, set to the remaining value of the coin + * @return query execution status + */ + enum GNUNET_DB_QueryStatus + (*do_melt_v26)( + void *cls, + struct TALER_EXCHANGEDB_Refresh_v26 *refresh, + const struct TALER_RefreshMasterSecretP *rms, + const struct GNUNET_TIME_Timestamp *timestamp, + bool *conflict, + bool *zombie_required, + bool *balance_ok, + struct TALER_Amount *insufficient_funds); + + + /** + * Lookup refresh data under the given @a rc, starting with protocol version v26 + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param rc commitment to use for the lookup + * @param[out] refresh where to store the result; note that + * refresh->coin.denom_sig will be set to NULL + * and is not fetched by this routine (as it is not needed by the client) + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*get_refresh)(void *cls, + const struct TALER_RefreshCommitmentP *rc, + struct TALER_EXCHANGEDB_Refresh_v26 *refresh); + + + /** * Add a proof of fulfillment of an policy * * @param cls the plugin-specific state