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:
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