exchange

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

commit 5607928347bddf59f5f750611ec9d8093e6b9840
parent a8a24394de532a907f2127d6337a4c53cddb3b5f
Author: Özgür Kesim <oec@codeblau.de>
Date:   Fri,  5 Dec 2025 13:41:44 +0100

[exchange] refresh vDOLDPLUS implemented

- handler: melt and reveal-melt handlers updated
- DB schema: added columns revealed and transfer_pub to refresh table
- vDOLDPLUS refresh support in lib/ added
- cleanup unused functions from (very) old refresh implementation

Tests now all pass in src/testing!

Diffstat:
Msrc/auditor/taler-helper-auditor-aggregation.c | 4++--
Msrc/auditor/taler-helper-auditor-coins.c | 4++--
Msrc/exchange/Makefile.am | 2+-
Msrc/exchange/taler-exchange-httpd.c | 6+++---
Msrc/exchange/taler-exchange-httpd_coins_get.c | 8++++----
Asrc/exchange/taler-exchange-httpd_melt.c | 1861+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_melt.h | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/exchange/taler-exchange-httpd_melt_v27.c | 1725-------------------------------------------------------------------------------
Dsrc/exchange/taler-exchange-httpd_melt_v27.h | 52----------------------------------------------------
Msrc/exchange/taler-exchange-httpd_reveal-melt.c | 254+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Asrc/exchangedb/0004-refresh.sql | 42++++++++++++++++++++++++++++++++++++++++++
Msrc/exchangedb/Makefile.am | 7++++---
Msrc/exchangedb/exchange-0004.sql.in | 1+
Msrc/exchangedb/exchange_do_refresh.sql | 3+++
Msrc/exchangedb/exchangedb_transactions.c | 4++--
Msrc/exchangedb/perf_deposits_get_ready.c | 16+++++++++++++---
Msrc/exchangedb/perf_select_refunds_by_coin.c | 16+++++++++++++---
Dsrc/exchangedb/pg_do_melt.c | 82-------------------------------------------------------------------------------
Dsrc/exchangedb/pg_do_melt.h | 49-------------------------------------------------
Msrc/exchangedb/pg_do_refresh.c | 19++++++++++++-------
Msrc/exchangedb/pg_do_refresh.h | 2+-
Msrc/exchangedb/pg_get_coin_transactions.c | 14+++++++-------
Msrc/exchangedb/pg_get_refresh.c | 38+++++++++++++++++++++++++++++++++++++-
Msrc/exchangedb/pg_get_refresh.h | 2+-
Dsrc/exchangedb/pg_get_refresh_reveal.c | 213-------------------------------------------------------------------------------
Dsrc/exchangedb/pg_get_refresh_reveal.h | 44--------------------------------------------
Asrc/exchangedb/pg_mark_refresh_reveal_success.c | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchangedb/pg_mark_refresh_reveal_success.h | 41+++++++++++++++++++++++++++++++++++++++++
Msrc/exchangedb/plugin_exchangedb_common.c | 4++--
Msrc/exchangedb/plugin_exchangedb_postgres.c | 5+++--
Msrc/include/taler/taler_crypto_lib.h | 319++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/include/taler/taler_exchange_service.h | 28++++++++++++++--------------
Msrc/include/taler/taler_exchangedb_plugin.h | 115++++++++++++++++++++++++++++++++++---------------------------------------------
Msrc/include/taler/taler_json_lib.h | 54+++++++++++++++++++++++++++++++++++-------------------
Msrc/include/taler/taler_testing_lib.h | 2+-
Msrc/lib/Makefile.am | 2+-
Asrc/lib/exchange_api_melt.c | 645+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/lib/exchange_api_melt_v27.c | 626-------------------------------------------------------------------------------
Msrc/lib/exchange_api_recoup_refresh.c | 19+++----------------
Msrc/lib/exchange_api_refresh_common.c | 99+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/lib/exchange_api_refresh_common.h | 32++++++++++++++++----------------
Msrc/lib/exchange_api_reveal_melt.c | 22+++++++++++-----------
Msrc/testing/testing_api_cmd_recoup_refresh.c | 8++++----
Msrc/testing/testing_api_cmd_refresh.c | 132++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/util/crypto.c | 267+++++++++++++++++++++++++++++++++++++++----------------------------------------
45 files changed, 3619 insertions(+), 3371 deletions(-)

diff --git a/src/auditor/taler-helper-auditor-aggregation.c b/src/auditor/taler-helper-auditor-aggregation.c @@ -567,7 +567,7 @@ check_transaction_history_for_deposit ( } break; } - case TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP: + case TALER_EXCHANGEDB_TT_RECOUP_REFRESH_RECEIVER: { const struct TALER_Amount *amount_with_fee; @@ -580,7 +580,7 @@ check_transaction_history_for_deposit ( amount_with_fee); break; } - case TALER_EXCHANGEDB_TT_RECOUP: + case TALER_EXCHANGEDB_TT_RECOUP_WITHDRAW: { const struct TALER_Amount *amount_with_fee; diff --git a/src/auditor/taler-helper-auditor-coins.c b/src/auditor/taler-helper-auditor-coins.c @@ -513,13 +513,13 @@ check_coin_history (const struct TALER_CoinSpendPublicKeyP *coin_pub, &pos->details.refund->refund_fee); have_refund = true; break; - case TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP: + case TALER_EXCHANGEDB_TT_RECOUP_REFRESH_RECEIVER: /* refunded += pos->value */ TALER_ARL_amount_add (&refunded, &refunded, &pos->details.old_coin_recoup->value); break; - case TALER_EXCHANGEDB_TT_RECOUP: + case TALER_EXCHANGEDB_TT_RECOUP_WITHDRAW: /* spent += pos->value */ TALER_ARL_amount_add (&spent, &spent, diff --git a/src/exchange/Makefile.am b/src/exchange/Makefile.am @@ -186,7 +186,7 @@ taler_exchange_httpd_SOURCES = \ taler-exchange-httpd_management_wire_enable.c \ taler-exchange-httpd_management_wire_disable.c \ taler-exchange-httpd_management_wire_fees.c \ - taler-exchange-httpd_melt_v27.c taler-exchange-httpd_melt_v27.h \ + taler-exchange-httpd_melt.c taler-exchange-httpd_melt.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 @@ -57,7 +57,7 @@ #include "taler-exchange-httpd_kyc-webhook.h" #include "taler-exchange-httpd_aml-decision.h" #include "taler-exchange-httpd_management.h" -#include "taler-exchange-httpd_melt_v27.h" +#include "taler-exchange-httpd_melt.h" #include "taler-exchange-httpd_metrics.h" #include "taler-exchange-httpd_mhd.h" #include "taler-exchange-httpd_purses_create.h" @@ -1745,7 +1745,7 @@ handle_mhd_request (void *cls, { .url = "melt", .method = MHD_HTTP_METHOD_POST, - .handler.post = &TEH_handler_melt_v27, + .handler.post = &TEH_handler_melt, .nargs = 0 }, /* tracking transfers */ @@ -2752,7 +2752,7 @@ do_shutdown (void *cls) TEH_resume_keys_requests (true); TEH_batch_deposit_cleanup (); TEH_withdraw_cleanup (); - TEH_melt_v27_cleanup (); + TEH_melt_cleanup (); TEH_reserves_close_cleanup (); TEH_reserves_purse_cleanup (); TEH_purses_merge_cleanup (); diff --git a/src/exchange/taler-exchange-httpd_coins_get.c b/src/exchange/taler-exchange-httpd_coins_get.c @@ -286,7 +286,7 @@ compile_transaction_history ( } } break; - case TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP: + case TALER_EXCHANGEDB_TT_RECOUP_REFRESH_RECEIVER: { struct TALER_EXCHANGEDB_RecoupRefreshListEntry *pr = pos->details.old_coin_recoup; @@ -317,7 +317,7 @@ compile_transaction_history ( history, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("type", - "OLD-COIN-RECOUP"), + "RECOUP-REFRESH-RECEIVER"), GNUNET_JSON_pack_uint64 ("history_offset", pos->coin_history_id), TALER_JSON_pack_amount ("amount", @@ -337,7 +337,7 @@ compile_transaction_history ( } break; } - case TALER_EXCHANGEDB_TT_RECOUP: + case TALER_EXCHANGEDB_TT_RECOUP_WITHDRAW: { const struct TALER_EXCHANGEDB_RecoupListEntry *recoup = pos->details.recoup; @@ -363,7 +363,7 @@ compile_transaction_history ( history, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("type", - "RECOUP"), + "RECOUP-WITHDRAW"), GNUNET_JSON_pack_uint64 ("history_offset", pos->coin_history_id), TALER_JSON_pack_amount ("amount", diff --git a/src/exchange/taler-exchange-httpd_melt.c b/src/exchange/taler-exchange-httpd_melt.c @@ -0,0 +1,1861 @@ +/* + 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.c + * @brief Handle /melt requests + * @note This endpoint is active since vDOLDPLUS of the protocol API + * @author Özgür Kesim + */ + +#include "taler/platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler-exchange-httpd.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_melt.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_keys.h" +#include "taler/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_COMMITMENT_INVALID, + MELT_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, + MELT_ERROR_AMOUNT_OVERFLOW, + MELT_ERROR_AMOUNT_PLUS_FEE_OVERFLOW, + MELT_ERROR_AMOUNT_WITH_FEE_INCORRECT, + MELT_ERROR_BLINDING_SEED_REQUIRED, + MELT_ERROR_COIN_CIPHER_MISMATCH, + MELT_COIN_CONFLICTING_DENOMINATION_KEY, + MELT_ERROR_COIN_EXPIRED_NO_ZOMBIE, + MELT_ERROR_COIN_SIGNATURE_INVALID, + MELT_ERROR_COIN_UNKNOWN, + MELT_ERROR_CONFIRMATION_SIGN, + MELT_ERROR_CRYPTO_HELPER, + MELT_ERROR_DB_FETCH_FAILED, + MELT_ERROR_DB_INVARIANT_FAILURE, + MELT_ERROR_DB_MAKE_COIN_KNOW_FAILURE, + MELT_ERROR_DB_PREFLIGHT_FAILURE, + MELT_ERROR_DENOMINATION_EXPIRED, + MELT_ERROR_DENOMINATION_KEY_UNKNOWN, + MELT_ERROR_DENOMINATION_REVOKED, + MELT_ERROR_DENOMINATION_SIGN, + MELT_ERROR_DENOMINATION_SIGNATURE_INVALID, + MELT_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + MELT_ERROR_DUPLICATE_PLANCHET, + MELT_ERROR_DUPLICATE_TRANSFER_PUB, + MELT_ERROR_INSUFFICIENT_FUNDS, + MELT_ERROR_KEYS_MISSING, + MELT_ERROR_FEES_EXCEED_CONTRIBUTION, + 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) /* TODO: is this still correct? Compare exchange_do_refresh.sql */ + | (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 MeltPhase + { + MELT_PHASE_PARSE, + MELT_PHASE_CHECK_MELT_VALID, + 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; + + /** + * The current key state + */ + struct TEH_KeyStateHandle *ksh; + + /** + * The melted coin's denomination key + */ + struct TEH_DenominationKey *melted_coin_denom; + + /** + * 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. + * + * TODO: find a better terminology. The sentences in the comments containing + * "zombie" make semantically _no sense_! + */ + bool zombie_required; + + /** + * We already checked and noticed that the coin is known. Hence we + * can skip the "ensure_coin_known" step of the transaction. + */ + bool coin_is_known; + + /** + * UUID of the coin in the known_coins table. + */ + uint64_t known_coin_id; + + /** + * 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_vDOLDPLUS 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_vDOLDPLUS refresh_idem; + + /** + * True if @e blinding_seed is missing in the request + */ + bool no_blinding_seed; + + /** + * Array @e persis.num_coins of hashes of the public keys + * of the denominations to refresh. + */ + struct TALER_DenominationHashP *denoms_h; + + /** + * Array of @e refresh.num_coins blinded coin planchets, arranged + * in runs of @e refresh.num_coins coins, [0..num_coins)..[0..num_coins), + * one for each kappa value. + */ + struct TALER_BlindedPlanchet *planchets[TALER_CNC_KAPPA]; + + /** + * @since vDOLDPLUS + * #TALER_CNC_KAPPA arrays of @e refresh.num_coins transfer public keys + * in runs of @e num_coins coins, [0..num_coins)..[0..num_coins), + * one for each kappa value. + * + * MAYBE null. If the client has NOT provided the transfer_pubs in the request, + * @e refresh.is_v27_refresh will be true. + * + * TODO: Once v27 clients are gone, this MUST NOT be nulls. + */ + struct TALER_TransferPublicKeyP *transfer_pubs[TALER_CNC_KAPPA]; + + /** + * #TALER_CNC_KAPPA hashes of the batches of @e num_coins coins. + */ + struct TALER_KappaHashBlindedPlanchetsP kappa_planchets_h; + + /** + * Array @e withdraw.num_r_pubs of indices into @e denoms_h + * of CS denominations. + */ + uint32_t *cs_indices; + + /** + * Total (over all coins) amount (excluding fee) committed for the refresh + */ + struct TALER_Amount amount; + + } 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. + */ + 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 \ + { GNUNET_static_assert (MELT_ERROR_NONE != ec); \ + (mc)->error.code = (ec); \ + (mc)->phase = MELT_PHASE_GENERATE_REPLY_ERROR; } while (0) + +#define SET_ERROR_WITH_FIELD(mc, ec, field) \ + do \ + { GNUNET_static_assert (MELT_ERROR_NONE != ec); \ + (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 \ + { GNUNET_static_assert (MELT_ERROR_NONE != ec); \ + (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_cleanup () +{ + struct MeltContext *mc; + + while (NULL != (mc = mc_head)) + { + GNUNET_CONTAINER_DLL_remove (mc_head, + mc_tail, + mc); + MHD_resume_connection (mc->rc->connection); + } +} + + +/** + * 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; +} + + +/** + * Free information in @a re, but not @a re itself. + * + * @param[in] re refresh data to free + */ +static void +free_refresh (struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *re) +{ + if (NULL != re->denom_sigs) + { + for (size_t i = 0; i<re->num_coins; i++) + TALER_blinded_denom_sig_free (&re->denom_sigs[i]); + GNUNET_free (re->denom_sigs); + } + GNUNET_free (re->cs_r_values); + GNUNET_free (re->denom_serials); + GNUNET_free (re->denom_pub_hashes); + TALER_denom_sig_free (&re->coin.denom_sig); +} + + +/** + * Cleanup routine for melt request. + * The function is called upon completion of the request + * that should clean up @a rh_ctx. + * + * @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 (uint8_t k = 0; k<TALER_CNC_KAPPA; k++) + { + for (size_t i = 0; i<mc->request.refresh.num_coins; i++) + TALER_blinded_planchet_free (&mc->request.planchets[k][i]); + GNUNET_free (mc->request.planchets[k]); + if (! mc->request.refresh.is_v27_refresh) + GNUNET_free (mc->request.transfer_pubs[k]); + } + free_refresh (&mc->request.refresh); + if (mc->request.is_idempotent) + free_refresh (&mc->request.refresh_idem); + GNUNET_free (mc->request.cs_indices); + GNUNET_free (mc); +} + + +/** + * Creates a new context for the incoming melt request + * + * @param mc melt request context + * @param root json body of the request + */ +static void +phase_parse_request ( + struct MeltContext *mc, + const json_t *root) +{ + const json_t *j_denoms_h; + const json_t *j_coin_evs; + const json_t *j_transfer_pubs; + 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_denom_sig ("old_denom_sig", + &mc->request.refresh.coin.denom_sig), + GNUNET_JSON_spec_fixed_auto ("refresh_seed", + &mc->request.refresh.refresh_seed), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("blinding_seed", + &mc->request.refresh.blinding_seed), + &mc->request.refresh.no_blinding_seed), + TALER_JSON_spec_amount ("value_with_fee", + TEH_currency, + &mc->request.refresh.amount_with_fee), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("transfer_pubs", + &j_transfer_pubs), + &mc->request.refresh.is_v27_refresh), + 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) + { + GNUNET_break_op (0); + mc->phase = (GNUNET_NO == res) + ? MELT_PHASE_RETURN_YES + : MELT_PHASE_RETURN_NO; + return; + } + + /* validate array size */ + GNUNET_static_assert ( + TALER_MAX_COINS < INT_MAX / TALER_CNC_KAPPA); + + mc->request.refresh.num_coins = json_array_size (j_denoms_h); + if (0 == mc->request.refresh.num_coins) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + "denoms_h must not be empty"); + return; + } + if (TALER_MAX_COINS < mc->request.refresh.num_coins) + { + /** + * 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! + */ + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + "maximum number of coins that can be refreshed has been exceeded"); + return; + } + if (TALER_CNC_KAPPA != json_array_size (j_coin_evs)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + "coin_evs must be an array of length "TALER_CNC_KAPPA_STR); + return; + } + if (! mc->request.refresh.is_v27_refresh && + (TALER_CNC_KAPPA != json_array_size (j_transfer_pubs))) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + "transfer_pubs must be an array of length "TALER_CNC_KAPPA_STR); + return; + } + + + /* 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_YES != res) + { + GNUNET_break_op (0); + mc->phase = (GNUNET_NO == res) + ? MELT_PHASE_RETURN_YES + : MELT_PHASE_RETURN_NO; + return; + } + } + } + + /* Parse blinded envelopes. */ + { + json_t *j_kappa_planchets; + size_t kappa; + struct GNUNET_HashContext *ctx; + + /* ctx to calculate the planchet_h */ + ctx = GNUNET_CRYPTO_hash_context_start (); + GNUNET_assert (NULL != ctx); + + json_array_foreach (j_coin_evs, kappa, j_kappa_planchets) + { + json_t *j_cev; + size_t idx; + + if (mc->request.refresh.num_coins != json_array_size (j_kappa_planchets)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + "coin_evs[i][] size"); + return; + } + + mc->request.planchets[kappa] = + GNUNET_new_array (mc->request.refresh.num_coins, + struct TALER_BlindedPlanchet); + + json_array_foreach (j_kappa_planchets, 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[kappa][idx]), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (mc->rc->connection, + j_cev, + kspec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + mc->phase = (GNUNET_NO == res) + ? MELT_PHASE_RETURN_YES + : MELT_PHASE_RETURN_NO; + return; + } + } + /* Save the hash of the batch of planchets for index kappa */ + TALER_wallet_blinded_planchets_hash ( + mc->request.refresh.num_coins, + mc->request.planchets[kappa], + mc->request.denoms_h, + &mc->request.kappa_planchets_h.tuple[kappa]); + GNUNET_CRYPTO_hash_context_read ( + ctx, + &mc->request.kappa_planchets_h.tuple[kappa], + sizeof(mc->request.kappa_planchets_h.tuple[kappa])); + } + /* Finally calculate the total hash over all planchets */ + GNUNET_CRYPTO_hash_context_finish ( + ctx, + &mc->request.refresh.planchets_h.hash); + } + /* Check for duplicate planchets. Technically a bug on + * the client side that is harmless for us, but still + * not allowed per protocol + */ + { + size_t max_idx = TALER_CNC_KAPPA * mc->request.refresh.num_coins; + + for (size_t I = 0; I < max_idx - 1; I++) + { + size_t ki = I / mc->request.refresh.num_coins; + size_t ni = I % mc->request.refresh.num_coins; + + for (size_t J = I + 1; J < max_idx; J++) + { + size_t kj = J / mc->request.refresh.num_coins; + size_t nj = J % mc->request.refresh.num_coins; + + if (0 == TALER_blinded_planchet_cmp ( + &mc->request.planchets[ki][ni], + &mc->request.planchets[kj][nj])) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_DUPLICATE_TRANSFER_PUB); + return; + } + } + } + } + + /* Parse the transfer public keys, if applicable */ + if (! mc->request.refresh.is_v27_refresh) + { + json_t *j_ktp; + size_t kappa; + + json_array_foreach (j_transfer_pubs, kappa, j_ktp) + { + if (mc->request.refresh.num_coins != + json_array_size (j_ktp)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + "transfer_pubs[i][] size"); + return; + } + + mc->request.transfer_pubs[kappa] = + GNUNET_new_array (mc->request.refresh.num_coins, + struct TALER_TransferPublicKeyP); + + /* Parse the batch of @e num_coins transfer public keys + * at index kappa */ + { + struct GNUNET_JSON_Specification ktp_spec[] = { + TALER_JSON_spec_array_fixed (NULL, + mc->request.refresh.num_coins, + mc->request.transfer_pubs[kappa], + sizeof(*mc->request.transfer_pubs[kappa])), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (mc->rc->connection, + j_ktp, + ktp_spec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + mc->phase = (GNUNET_NO == res) + ? MELT_PHASE_RETURN_YES + : MELT_PHASE_RETURN_NO; + return; + } + } + } + /* Check for duplicate transfer public keys. Technically a bug on + * the client side that is harmless for us, but still + * not allowed per protocol + */ + size_t max_idx = TALER_CNC_KAPPA * mc->request.refresh.num_coins; + + for (size_t I = 0; I < max_idx - 1; I++) + { + size_t ki = I / mc->request.refresh.num_coins; + size_t ni = I % mc->request.refresh.num_coins; + + for (size_t J = I + 1; J < max_idx; J++) + { + size_t kj = J / mc->request.refresh.num_coins; + size_t nj = J % mc->request.refresh.num_coins; + + if (0 == GNUNET_memcmp ( + &mc->request.transfer_pubs[ki][ni], + &mc->request.transfer_pubs[kj][nj])) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_DUPLICATE_TRANSFER_PUB); + return; + } + } + } + } + + mc->ksh = TEH_keys_get_state (); + if (NULL == mc->ksh) + { + GNUNET_break (0); + SET_ERROR (mc, + MELT_ERROR_KEYS_MISSING); + return; + } + mc->phase = MELT_PHASE_CHECK_MELT_VALID; +} + + +/** + * 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 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 is not valid at this time + * #GNUNET_SYSERR otherwise (denomination invalid), with finish_loop called. + */ +static enum GNUNET_GenericReturnValue +find_denomination ( + struct MeltContext *mc, + const struct TALER_DenominationHashP *denom_h, + struct TEH_DenominationKey **pdk) +{ + struct TEH_DenominationKey *dk; + + *pdk = NULL; + GNUNET_assert (NULL != mc->ksh); + dk = TEH_keys_denomination_by_hash_from_state (mc->ksh, + denom_h, + NULL, + NULL); + if (NULL == dk) + { + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_DENOMINATION_KEY_UNKNOWN, + denom_h, + *denom_h); + return GNUNET_SYSERR; + } + *pdk = dk; + + if (GNUNET_TIME_absolute_is_past ( + dk->meta.expire_withdraw.abs_time)) + { + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_DENOMINATION_EXPIRED, + denom_h, + *denom_h); + /** + * Note that we return GNUNET_NO here. + * This way phase_check_melt_valid can react + * to it as a non-error case and do the zombie check. + */ + return GNUNET_NO; + } + + if (GNUNET_TIME_absolute_is_future ( + dk->meta.start.abs_time)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + denom_h, + *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 denomination supports age restriction */ + if (! (mc->request.refresh.coin.no_age_commitment) && + (0 == dk->denom_pub.age_mask.bits)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, + denom_h, + *denom_h); + return GNUNET_SYSERR; + } + if ((mc->request.refresh.coin.no_age_commitment) && + (0 != dk->denom_pub.age_mask.bits)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_AGE_RESTRICTION_COMMITMENT_INVALID); + return GNUNET_SYSERR; + } + + 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) +{ + bool is_cs_denom[mc->request.refresh.num_coins]; + + memset (is_cs_denom, + 0, + sizeof(is_cs_denom)); + + 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)); + + /* Calculate the total value and withdraw fees for the fresh coins */ + for (size_t i = 0; i < mc->request.refresh.num_coins; i++) + { + struct TEH_DenominationKey *dk; + + if (GNUNET_OK != + find_denomination (mc, + &mc->request.denoms_h[i], + &dk)) + return; + + if (GNUNET_CRYPTO_BSA_CS == + dk->denom_pub.bsign_pub_key->cipher) + { + if (mc->request.refresh.no_blinding_seed) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_BLINDING_SEED_REQUIRED); + return; + } + mc->request.refresh.num_cs_r_values++; + is_cs_denom[i] = true; + } + /* Ensure the ciphers from the planchets match the denominations'. */ + { + for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) + { + if (dk->denom_pub.bsign_pub_key->cipher != + mc->request.planchets[k][i].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 withdraw fees for the fresh coins */ + if (0 > TALER_amount_add (&mc->request.amount, + &mc->request.amount, + &dk->meta.fees.withdraw)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_AMOUNT_PLUS_FEE_OVERFLOW); + return; + } + mc->request.refresh.denom_serials[i] = dk->meta.serial; + } + + /** + * Calculate the amount (with withdraw fee) plus refresh fee and + * compare with the value provided by the client in the request. + */ + { + struct TALER_Amount amount_with_fee; + + if (0 > TALER_amount_add (&amount_with_fee, + &mc->request.amount, + &mc->melted_coin_denom->meta.fees.refresh)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_AMOUNT_PLUS_FEE_OVERFLOW); + return; + } + + if (0 != TALER_amount_cmp (&amount_with_fee, + &mc->request.refresh.amount_with_fee)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_AMOUNT_WITH_FEE_INCORRECT); + return; + } + } + + /* Save the indices of CS denominations */ + if (0 < mc->request.refresh.num_cs_r_values) + { + size_t j = 0; + + mc->request.cs_indices = GNUNET_new_array ( + mc->request.refresh.num_cs_r_values, + uint32_t); + + for (size_t i = 0; i < mc->request.refresh.num_coins; i++) + { + if (is_cs_denom[i]) + mc->request.cs_indices[j++] = i; + } + } + 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) +{ + /* We can now compute the commitment */ + { + struct TALER_KappaHashBlindedPlanchetsP k_bps_h = {0}; + struct TALER_KappaTransferPublicKeys k_transfer_pubs = {0}; + + for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) + TALER_wallet_blinded_planchets_hash ( + mc->request.refresh.num_coins, + mc->request.planchets[k], + mc->request.denoms_h, + &k_bps_h.tuple[k]); + + if (! mc->request.refresh.is_v27_refresh) + { + k_transfer_pubs.num_transfer_pubs = mc->request.refresh.num_coins; + for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) + k_transfer_pubs.batch[k] = &mc->request.refresh.transfer_pubs[k]; + } + + TALER_refresh_get_commitment ( + &mc->request.refresh.rc, + &mc->request.refresh.refresh_seed, + mc->request.no_blinding_seed + ? NULL + : &mc->request.refresh.blinding_seed, + mc->request.refresh.is_v27_refresh + ? NULL + : &k_transfer_pubs, + &k_bps_h, + &mc->request.refresh.coin.coin_pub, + &mc->request.refresh.amount_with_fee); + } + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_melt_verify ( + &mc->request.refresh.amount_with_fee, + &mc->melted_coin_denom->meta.fees.refresh, + &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++; +} + + +/** + * Check for information about the melted coin's denomination, + * extracting its validity status and fee structure. + * Baseline: check if deposits/refreshes are generally + * simply still allowed for this denomination. + * + * @param mc parsed request information + */ +static void +phase_check_melt_valid (struct MeltContext *mc) +{ + enum MeltPhase current_phase = mc->phase; + /** + * Find the old coin's denomination. + * Note that we return only on GNUNET_SYSERR, + * because GNUNET_NO for the expired denomination + * will be handled below, with the zombie-check. + */ + if (GNUNET_SYSERR == + find_denomination (mc, + &mc->request.refresh.coin.denom_pub_hash, + &mc->melted_coin_denom)) + return; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Melted coin's denomination is worth %s\n", + TALER_amount2s (&mc->melted_coin_denom->meta.value)); + + /* sanity-check that "total melt amount > melt fee" */ + if (0 < + TALER_amount_cmp (&mc->melted_coin_denom->meta.fees.refresh, + &mc->request.refresh.amount_with_fee)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_FEES_EXCEED_CONTRIBUTION); + return; + } + + if (GNUNET_OK != + TALER_test_coin_valid (&mc->request.refresh.coin, + &mc->melted_coin_denom->denom_pub)) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_DENOMINATION_SIGNATURE_INVALID); + return; + } + + /** + * find_denomination might have set the phase to + * produce an error, but we are still investigating. + * We reset the phase. + */ + mc->phase = current_phase; + mc->error.code = MELT_ERROR_NONE; + + if (GNUNET_TIME_absolute_is_past ( + mc->melted_coin_denom->meta.expire_deposit.abs_time)) + { + /** + * We are past deposit expiration time, but maybe this is a zombie? + */ + struct TALER_DenominationHashP denom_hash; + enum GNUNET_DB_QueryStatus qs; + + /* Check that the coin is dirty (we have seen it before), as we will + not just allow melting of a *fresh* coin where the denomination was + revoked (those must be recouped) */ + qs = TEH_plugin->get_coin_denomination ( + TEH_plugin->cls, + &mc->request.refresh.coin.coin_pub, + &mc->known_coin_id, + &denom_hash); + if (0 > qs) + { + /* There is no good reason for a serialization failure here: */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + SET_ERROR (mc, + MELT_ERROR_DB_FETCH_FAILED); + return; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + /* We never saw this coin before, so _this_ justification is not OK. + * Note that the error was already set in find_denominations. */ + GNUNET_assert (MELT_ERROR_DENOMINATION_EXPIRED == + mc->error.code); + GNUNET_assert (MELT_PHASE_GENERATE_REPLY_ERROR == + mc->phase); + return; + } + /* sanity check */ + if (0 != + GNUNET_memcmp (&denom_hash, + &mc->request.refresh.coin.denom_pub_hash)) + { + GNUNET_break_op (0); + SET_ERROR_WITH_DETAIL (mc, + MELT_COIN_CONFLICTING_DENOMINATION_KEY, + denom_h, + denom_hash); + return; + } + /* Minor optimization: no need to run the + "ensure_coin_known" part of the transaction */ + mc->coin_is_known = true; + /* check later that zombie is satisfied */ + mc->zombie_required = true; + } + 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) +{ + mc->request.refresh.denom_sigs + = GNUNET_new_array ( + mc->request.refresh.num_coins, + struct TALER_BlindedDenominationSignature); + mc->request.refresh.noreveal_index = + GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_STRONG, + TALER_CNC_KAPPA); + + /* Choose and sign the coins */ + { + struct TEH_CoinSignData csds[mc->request.refresh.num_coins]; + enum TALER_ErrorCode ec_denomination_sign; + size_t noreveal_idx = mc->request.refresh.noreveal_index; + + memset (csds, + 0, + sizeof(csds)); + + /* Pick the chosen blinded coins */ + for (size_t i = 0; i<mc->request.refresh.num_coins; i++) + { + csds[i].bp = &mc->request.planchets[noreveal_idx][i]; + csds[i].h_denom_pub = &mc->request.denoms_h[i]; + } + + ec_denomination_sign = TEH_keys_denomination_batch_sign ( + mc->request.refresh.num_coins, + csds, + true, /* for melt */ + 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; + } + + /* Save the hash of chosen planchets */ + mc->request.refresh.selected_h = + mc->request.kappa_planchets_h.tuple[noreveal_idx]; + + /* If applicable, save the chosen transfer public keys */ + if (! mc->request.refresh.is_v27_refresh) + mc->request.refresh.transfer_pubs = + mc->request.transfer_pubs[noreveal_idx]; + + /** + * For the denominations with cipher CS, calculate the R-values + * and save the choices we made now, as at a later point, the + * private keys for the denominations might now be available anymore + * to make the same choice again. + */ + if (0 < mc->request.refresh.num_cs_r_values) + { + size_t num_cs_r_values = mc->request.refresh.num_cs_r_values; + struct TEH_CsDeriveData cdds[num_cs_r_values]; + struct GNUNET_CRYPTO_CsSessionNonce nonces[num_cs_r_values]; + + memset (nonces, + 0, + sizeof(nonces)); + mc->request.refresh.cs_r_values = + GNUNET_new_array (num_cs_r_values, + struct GNUNET_CRYPTO_CSPublicRPairP); + mc->request.refresh.cs_r_choices = 0; + + GNUNET_assert (! mc->request.refresh.no_blinding_seed); + TALER_cs_derive_nonces_from_seed ( + &mc->request.refresh.blinding_seed, + true, /* for melt */ + num_cs_r_values, + mc->request.cs_indices, + nonces); + + for (size_t i = 0; i < num_cs_r_values; i++) + { + size_t idx = mc->request.cs_indices[i]; + + GNUNET_assert (idx < mc->request.refresh.num_coins); + cdds[i].h_denom_pub = &mc->request.denoms_h[idx]; + cdds[i].nonce = &nonces[i]; + } + + /** + * Let the crypto helper generate the R-values and + * make the choices + */ + if (TALER_EC_NONE != + TEH_keys_denomination_cs_batch_r_pub_simple ( + mc->request.refresh.num_cs_r_values, + cdds, + true, /* for melt */ + mc->request.refresh.cs_r_values)) + { + GNUNET_break (0); + SET_ERROR (mc, + MELT_ERROR_CRYPTO_HELPER); + return; + } + + /* Now save the choices for the selected bits */ + for (size_t i = 0; i < num_cs_r_values; i++) + { + size_t idx = mc->request.cs_indices[i]; + + struct TALER_BlindedDenominationSignature *sig = + &mc->request.refresh.denom_sigs[idx]; + uint8_t bit = sig->blinded_sig->details.blinded_cs_answer.b; + + mc->request.refresh.cs_r_choices |= bit << i; + GNUNET_static_assert ( + TALER_MAX_COINS <= + sizeof(mc->request.refresh.cs_r_choices) * 8); + } + } + } + mc->phase++; +} + + +/** + * Generates response for the melt request. + * + * @param mc melt operation context + */ +static void +phase_generate_reply_success (struct MeltContext *mc) +{ + struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *db_obj; + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + enum TALER_ErrorCode ec_confirmation_sign; + + db_obj = mc->request.is_idempotent + ? &mc->request.refresh_idem + : &mc->request.refresh; + 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_OK, + 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_refresh"); + 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; + mc->error.code = MELT_ERROR_NONE; + 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: + break; + case MELT_ERROR_REQUEST_PARAMETER_MALFORMED: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + mc->error.details.request_parameter_malformed)); + return; + 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)); + return; + 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)); + return; + 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)); + return; + case MELT_ERROR_DB_PREFLIGHT_FAILURE: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + "make_coin_known")); + return; + case MELT_ERROR_DB_MAKE_COIN_KNOW_FAILURE: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + "preflight failure")); + return; + case MELT_ERROR_COIN_UNKNOWN: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_COIN_UNKNOWN, + NULL)); + return; + case MELT_COIN_CONFLICTING_DENOMINATION_KEY: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY, + TALER_B2S (&mc->error.details.denom_h))); + return; + case MELT_ERROR_COIN_EXPIRED_NO_ZOMBIE: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_MELT_COIN_EXPIRED_NO_ZOMBIE, + NULL)); + return; + case MELT_ERROR_DENOMINATION_SIGN: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + mc->error.details.ec_denomination_sign, + NULL)); + return; + case MELT_ERROR_DENOMINATION_SIGNATURE_INVALID: + finish_loop (mc, + TALER_MHD_reply_with_error (mc->rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, + NULL)); + return; + 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)); + return; + 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, + "MELT")); + return; + 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, + "MELT")); + return; + 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)); + return; + case MELT_ERROR_COIN_CIPHER_MISMATCH: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, + NULL)); + return; + case MELT_ERROR_BLINDING_SEED_REQUIRED: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_GENERIC_PARAMETER_MISSING, + "blinding_seed")); + return; + case MELT_ERROR_CRYPTO_HELPER: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL)); + return; + 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)); + return; + } + case MELT_ERROR_AGE_RESTRICTION_COMMITMENT_INVALID: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID, + "old_age_commitment_h")); + return; + 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")); + return; + 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")); + return; + case MELT_ERROR_FEES_EXCEED_CONTRIBUTION: + finish_loop (mc, + TALER_MHD_reply_with_error (mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_MELT_FEES_EXCEED_CONTRIBUTION, + NULL)); + return; + case MELT_ERROR_AMOUNT_WITH_FEE_INCORRECT: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW, + "value_with_fee incorrect")); + return; + case MELT_ERROR_CONFIRMATION_SIGN: + finish_loop (mc, + TALER_MHD_reply_with_ec ( + mc->rc->connection, + mc->error.details.ec_confirmation_sign, + NULL)); + return; + 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)); + return; + case MELT_ERROR_DUPLICATE_PLANCHET: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, /* FIXME: new error! */ + "duplicate planchet")); + return; + case MELT_ERROR_DUPLICATE_TRANSFER_PUB: + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, /* FIXME: new error! */ + "duplicate transfer_pub")); + return; + 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")); + return; + 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)); + return; + } + GNUNET_break (0); + finish_loop (mc, + TALER_MHD_reply_with_error ( + mc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "error phase without error")); +} + + +/** + * 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 found; + bool nonce_reuse; + uint32_t noreveal_index; + struct TALER_Amount insufficient_funds; + + (void) connection; + (void) mhd_ret; + + qs = TEH_plugin->do_refresh (TEH_plugin->cls, + &mc->request.refresh, + &mc->now, + &found, + &noreveal_index, + &mc->zombie_required, + &nonce_reuse, + &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_refresh"); + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_COIN_UNKNOWN); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (found) + { + /** + * This request is idempotent, set the nonreveal_index + * to the previous one and reply success. + */ + mc->request.refresh.noreveal_index = noreveal_index; + mc->phase = MELT_PHASE_GENERATE_REPLY_SUCCESS; + mc->error.code = MELT_ERROR_NONE; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + } + if (nonce_reuse) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_NONCE_RESUSE); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (! balance_ok) + { + GNUNET_break_op (0); + SET_ERROR_WITH_FIELD (mc, + MELT_ERROR_INSUFFICIENT_FUNDS, + insufficient_funds); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (mc->zombie_required) + { + GNUNET_break_op (0); + SET_ERROR (mc, + MELT_ERROR_COIN_EXPIRED_NO_ZOMBIE); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + 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) +{ + if (GNUNET_SYSERR == + TEH_plugin->preflight (TEH_plugin->cls)) + { + GNUNET_break (0); + SET_ERROR (mc, + MELT_ERROR_DB_PREFLIGHT_FAILURE); + return; + } + + /* first, make sure coin is known */ + if (! mc->coin_is_known) + { + MHD_RESULT mhd_ret = -1; + enum GNUNET_DB_QueryStatus qs; + + for (unsigned int tries = 0; tries<MAX_TRANSACTION_COMMIT_RETRIES; tries++) + { + qs = TEH_make_coin_known (&mc->request.refresh.coin, + mc->rc->connection, + &mc->known_coin_id, + &mhd_ret); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + if (0 > qs) + { + GNUNET_break (0); + /* Check if an answer has been queued */ + switch (mhd_ret) + { + case MHD_NO: + mc->phase = MELT_PHASE_RETURN_NO; + return; + case MHD_YES: + mc->phase = MELT_PHASE_RETURN_YES; + return; + default: /* ignore */ + ; + } + SET_ERROR (mc, + MELT_ERROR_DB_MAKE_COIN_KNOW_FAILURE); + return; + } + } + + /* run main database transaction */ + { + MHD_RESULT mhd_ret = -1; + enum GNUNET_GenericReturnValue ret; + enum MeltPhase current_phase = mc->phase; + + GNUNET_assert (MELT_PHASE_RUN_TRANSACTION == + current_phase); + ret = TEH_DB_run_transaction (mc->rc->connection, + "run melt", + TEH_MT_REQUEST_MELT, + &mhd_ret, + &melt_transaction, + mc); + if (GNUNET_OK != ret) + { + GNUNET_break (0); + /* Check if an answer has been queued */ + switch (mhd_ret) + { + case MHD_NO: + mc->phase = MELT_PHASE_RETURN_NO; + return; + case MHD_YES: + mc->phase = MELT_PHASE_RETURN_YES; + return; + default: /* ignore */ + ; + } + GNUNET_assert (MELT_ERROR_NONE != mc->error.code); + GNUNET_assert (MELT_PHASE_GENERATE_REPLY_ERROR == mc->phase); + return; + } + /** + * In case of idempotency (which is not an error condition), + * the phase has changed in melt_transaction. + * We simple return. + */ + if (current_phase != mc->phase) + return; + } + mc->phase++; +} + + +MHD_RESULT +TEH_handler_melt ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[0]) +{ + struct MeltContext *mc = rc->rh_ctx; + + (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 (); + } + + while (true) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "melt processing in phase %d\n", + mc->phase); + switch (mc->phase) + { + case MELT_PHASE_PARSE: + phase_parse_request (mc, + root); + break; + case MELT_PHASE_CHECK_MELT_VALID: + phase_check_melt_valid (mc); + break; + 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.h b/src/exchange/taler-exchange-httpd_melt.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.h + * @brief Handle /melt requests, starting with vDOLDPLUS of the protocol + * @author Özgür Kesim + */ +#ifndef TALER_EXCHANGE_HTTPD_MELT_H +#define TALER_EXCHANGE_HTTPD_MELT_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_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 ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[0]); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_melt_v27.c b/src/exchange/taler-exchange-httpd_melt_v27.c @@ -1,1725 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2025 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU 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_v27.c - * @brief Handle /melt requests - * @note This endpoint is active since v27 of the protocol API - * @author Özgür Kesim - */ - -#include "taler/platform.h" -#include <gnunet/gnunet_util_lib.h> -#include <jansson.h> -#include "taler-exchange-httpd.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_mhd_lib.h" -#include "taler-exchange-httpd_melt_v27.h" -#include "taler-exchange-httpd_responses.h" -#include "taler-exchange-httpd_keys.h" -#include "taler/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_COMMITMENT_INVALID, - MELT_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, - MELT_ERROR_AMOUNT_OVERFLOW, - MELT_ERROR_AMOUNT_PLUS_FEE_OVERFLOW, - MELT_ERROR_AMOUNT_WITH_FEE_INCORRECT, - MELT_ERROR_BLINDING_SEED_REQUIRED, - MELT_ERROR_COIN_CIPHER_MISMATCH, - MELT_COIN_CONFLICTING_DENOMINATION_KEY, - MELT_ERROR_COIN_EXPIRED_NO_ZOMBIE, - MELT_ERROR_COIN_SIGNATURE_INVALID, - MELT_ERROR_COIN_UNKNOWN, - MELT_ERROR_CONFIRMATION_SIGN, - MELT_ERROR_CRYPTO_HELPER, - MELT_ERROR_DB_FETCH_FAILED, - MELT_ERROR_DB_INVARIANT_FAILURE, - MELT_ERROR_DB_MAKE_COIN_KNOW_FAILURE, - MELT_ERROR_DB_PREFLIGHT_FAILURE, - MELT_ERROR_DENOMINATION_EXPIRED, - MELT_ERROR_DENOMINATION_KEY_UNKNOWN, - MELT_ERROR_DENOMINATION_REVOKED, - MELT_ERROR_DENOMINATION_SIGN, - MELT_ERROR_DENOMINATION_SIGNATURE_INVALID, - MELT_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, - MELT_ERROR_IDEMPOTENT_PLANCHET, - MELT_ERROR_INSUFFICIENT_FUNDS, - MELT_ERROR_KEYS_MISSING, - MELT_ERROR_FEES_EXCEED_CONTRIBUTION, - 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) /* TODO: is this still correct? Compare exchange_do_refresh.sql */ - | (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 MeltPhase - { - MELT_PHASE_PARSE, - MELT_PHASE_CHECK_MELT_VALID, - 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; - - /** - * The current key state - */ - struct TEH_KeyStateHandle *ksh; - - /** - * The melted coin's denomination key - */ - struct TEH_DenominationKey *melted_coin_denom; - - /** - * 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; - - /** - * We already checked and noticed that the coin is known. Hence we - * can skip the "ensure_coin_known" step of the transaction. - */ - bool coin_is_dirty; - - /** - * UUID of the coin in the known_coins table. - */ - uint64_t known_coin_id; - - /** - * 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_v27 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_v27 refresh_idem; - - /** - * True if @e blinding_seed is missing in the request - */ - bool no_blinding_seed; - - /** - * Array @e persis.num_coins of hashes of the public keys - * of the denominations to refresh. - */ - struct TALER_DenominationHashP *denoms_h; - - /** - * 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[TALER_CNC_KAPPA]; - - /** - * #TALER_CNC_KAPPA hashes of the batches of @e num_coins coins. - */ - struct TALER_KappaHashBlindedPlanchetsP kappa_planchets_h; - - /** - * Array @e withdraw.num_r_pubs of indices into @e denoms_h - * of CS denominations. - */ - uint32_t *cs_indices; - - /** - * Total (over all coins) amount (excluding fee) committed for the refresh - */ - struct TALER_Amount amount; - - } 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. - */ - 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 \ - { GNUNET_static_assert (MELT_ERROR_NONE != ec); \ - (mc)->error.code = (ec); \ - (mc)->phase = MELT_PHASE_GENERATE_REPLY_ERROR; } while (0) - -#define SET_ERROR_WITH_FIELD(mc, ec, field) \ - do \ - { GNUNET_static_assert (MELT_ERROR_NONE != ec); \ - (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 \ - { GNUNET_static_assert (MELT_ERROR_NONE != ec); \ - (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_v27_cleanup () -{ - struct MeltContext *mc; - - while (NULL != (mc = mc_head)) - { - GNUNET_CONTAINER_DLL_remove (mc_head, - mc_tail, - mc); - MHD_resume_connection (mc->rc->connection); - } -} - - -/** - * 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; -} - - -/** - * Free information in @a re, but not @a re itself. - * - * @param[in] re refresh data to free - */ -static void -free_refresh (struct TALER_EXCHANGEDB_Refresh_v27 *re) -{ - if (NULL != re->denom_sigs) - { - for (size_t i = 0; i<re->num_coins; i++) - TALER_blinded_denom_sig_free (&re->denom_sigs[i]); - GNUNET_free (re->denom_sigs); - } - GNUNET_free (re->cs_r_values); - GNUNET_free (re->denom_serials); - GNUNET_free (re->denom_pub_hashes); - TALER_denom_sig_free (&re->coin.denom_sig); -} - - -/** - * Cleanup routine for melt request. - * The function is called upon completion of the request - * that should clean up @a rh_ctx. - * - * @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 (uint8_t k = 0; k<TALER_CNC_KAPPA; k++) - { - for (size_t i = 0; i<mc->request.refresh.num_coins; i++) - TALER_blinded_planchet_free (&mc->request.planchets[k][i]); - GNUNET_free (mc->request.planchets[k]); - } - free_refresh (&mc->request.refresh); - if (mc->request.is_idempotent) - { - free_refresh (&mc->request.refresh_idem); - } - GNUNET_free (mc->request.cs_indices); - GNUNET_free (mc); -} - - -/** - * Creates a new context for the incoming melt request - * - * @param mc melt request context - * @param root json body of the request - */ -static void -phase_parse_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_denom_sig ("old_denom_sig", - &mc->request.refresh.coin.denom_sig), - GNUNET_JSON_spec_fixed_auto ("refresh_seed", - &mc->request.refresh.refresh_seed), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("blinding_seed", - &mc->request.refresh.blinding_seed), - &mc->request.refresh.no_blinding_seed), - TALER_JSON_spec_amount ("value_with_fee", - TEH_currency, - &mc->request.refresh.amount_with_fee), - 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) - { - GNUNET_break_op (0); - mc->phase = (GNUNET_NO == res) - ? MELT_PHASE_RETURN_YES - : MELT_PHASE_RETURN_NO; - return; - } - - /* validate array size */ - GNUNET_static_assert ( - TALER_MAX_COINS < INT_MAX / TALER_CNC_KAPPA); - - mc->request.refresh.num_coins = json_array_size (j_denoms_h); - if (0 == mc->request.refresh.num_coins) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_REQUEST_PARAMETER_MALFORMED, - request_parameter_malformed, - "denoms_h must not be empty"); - return; - } - else if (TALER_MAX_COINS < mc->request.refresh.num_coins) - { - /** - * 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! - */ - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_REQUEST_PARAMETER_MALFORMED, - request_parameter_malformed, - "maximum number of coins that can be refreshed has been exceeded"); - return; - } - else if (TALER_CNC_KAPPA != json_array_size (j_coin_evs)) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_REQUEST_PARAMETER_MALFORMED, - request_parameter_malformed, - "coin_evs must be an array of length "TALER_CNC_KAPPA_STR); - return; - } - - /* 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_YES != res) - { - GNUNET_break_op (0); - mc->phase = (GNUNET_NO == res) - ? MELT_PHASE_RETURN_YES - : MELT_PHASE_RETURN_NO; - return; - } - } - } - - /* Calculate the hash over the blinded coin envelopes */ - for (size_t k = 0; k<TALER_CNC_KAPPA; k++) - { - mc->request.planchets[k] = - GNUNET_new_array (mc->request.refresh.num_coins, - struct TALER_BlindedPlanchet); - } - - /* Parse blinded envelopes. */ - { - json_t *j_kappa_planchets; - size_t kappa; - struct GNUNET_HashContext *ctx; - - /* ctx to calculate the planchet_h */ - ctx = GNUNET_CRYPTO_hash_context_start (); - GNUNET_assert (NULL != ctx); - - json_array_foreach (j_coin_evs, kappa, j_kappa_planchets) - { - json_t *j_cev; - size_t idx; - - if (mc->request.refresh.num_coins != json_array_size (j_kappa_planchets)) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_REQUEST_PARAMETER_MALFORMED, - request_parameter_malformed, - "coin_evs[] size"); - return; - } - - json_array_foreach (j_kappa_planchets, 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[kappa][idx]), - GNUNET_JSON_spec_end () - }; - - res = TALER_MHD_parse_json_data (mc->rc->connection, - j_cev, - kspec); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - mc->phase = (GNUNET_NO == res) - ? MELT_PHASE_RETURN_YES - : MELT_PHASE_RETURN_NO; - return; - } - /* 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 k = 0; k <= kappa; k++) - { - size_t max = (k == kappa) - ? idx - : mc->request.refresh.num_coins; - for (size_t i = 0; i < max; i++) - { - if (0 == - TALER_blinded_planchet_cmp ( - &mc->request.planchets[kappa][idx], - &mc->request.planchets[k][i])) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (kspec); - SET_ERROR (mc, - MELT_ERROR_IDEMPOTENT_PLANCHET); - return; - } - } - } - } - /* Save the hash of the batch of planchets for index kappa */ - TALER_wallet_blinded_planchets_hash ( - mc->request.refresh.num_coins, - mc->request.planchets[kappa], - mc->request.denoms_h, - &mc->request.kappa_planchets_h.tuple[kappa]); - GNUNET_CRYPTO_hash_context_read ( - ctx, - &mc->request.kappa_planchets_h.tuple[kappa], - sizeof(mc->request.kappa_planchets_h.tuple[kappa])); - } - - /* Finally calculate the total hash over all planchets */ - GNUNET_CRYPTO_hash_context_finish ( - ctx, - &mc->request.refresh.planchets_h.hash); - } - mc->ksh = TEH_keys_get_state (); - if (NULL == mc->ksh) - { - GNUNET_break (0); - SET_ERROR (mc, - MELT_ERROR_KEYS_MISSING); - return; - } - mc->phase = MELT_PHASE_CHECK_MELT_VALID; -} - - -/** - * 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 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 is not valid at this time - * #GNUNET_SYSERR otherwise (denomination invalid), with finish_loop called. - */ -static enum GNUNET_GenericReturnValue -find_denomination ( - struct MeltContext *mc, - const struct TALER_DenominationHashP *denom_h, - struct TEH_DenominationKey **pdk) -{ - struct TEH_DenominationKey *dk; - - *pdk = NULL; - GNUNET_assert (NULL != mc->ksh); - dk = TEH_keys_denomination_by_hash_from_state ( - mc->ksh, - denom_h, - NULL, - NULL); - if (NULL == dk) - { - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_DENOMINATION_KEY_UNKNOWN, - denom_h, - *denom_h); - return GNUNET_SYSERR; - } - *pdk = dk; - - if (GNUNET_TIME_absolute_is_past ( - dk->meta.expire_withdraw.abs_time)) - { - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_DENOMINATION_EXPIRED, - denom_h, - *denom_h); - /** - * Note that we return GNUNET_NO here. - * This way phase_check_melt_valid can react - * to it as a non-error case and do the zombie check. - */ - return GNUNET_NO; - } - - if (GNUNET_TIME_absolute_is_future ( - dk->meta.start.abs_time)) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, - denom_h, - *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) && - (0 == dk->denom_pub.age_mask.bits)) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (mc, - MELT_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION, - denom_h, - *denom_h); - return GNUNET_SYSERR; - } - if ((mc->request.refresh.coin.no_age_commitment) && - (0 != dk->denom_pub.age_mask.bits)) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_AGE_RESTRICTION_COMMITMENT_INVALID); - return GNUNET_SYSERR; - } - - 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) -{ - bool is_cs_denom[mc->request.refresh.num_coins]; - - memset (is_cs_denom, - 0, - sizeof(is_cs_denom)); - - 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)); - - /* Calculate the total value and withdraw fees for the fresh coins */ - for (size_t i = 0; i < mc->request.refresh.num_coins; i++) - { - struct TEH_DenominationKey *dk; - - if (GNUNET_OK != - find_denomination ( - mc, - &mc->request.denoms_h[i], - &dk)) - return; - - if (GNUNET_CRYPTO_BSA_CS == - dk->denom_pub.bsign_pub_key->cipher) - { - if (mc->request.refresh.no_blinding_seed) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_BLINDING_SEED_REQUIRED); - return; - } - mc->request.refresh.num_cs_r_values++; - is_cs_denom[i] = true; - } - /* Ensure the ciphers from the planchets match the denominations'. */ - { - for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) - { - if (dk->denom_pub.bsign_pub_key->cipher != - mc->request.planchets[k][i].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 withdraw fees for the fresh coins */ - if (0 > TALER_amount_add (&mc->request.amount, - &mc->request.amount, - &dk->meta.fees.withdraw)) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_AMOUNT_PLUS_FEE_OVERFLOW); - return; - } - mc->request.refresh.denom_serials[i] = dk->meta.serial; - } - - /** - * Calculate the amount (with withdraw fee) plus refresh fee and - * compare with the value provided by the client in the request. - */ - { - struct TALER_Amount amount_with_fee; - - if (0 > TALER_amount_add (&amount_with_fee, - &mc->request.amount, - &mc->melted_coin_denom->meta.fees.refresh)) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_AMOUNT_PLUS_FEE_OVERFLOW); - return; - } - - if (0 != TALER_amount_cmp (&amount_with_fee, - &mc->request.refresh.amount_with_fee)) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_AMOUNT_WITH_FEE_INCORRECT); - return; - } - } - - /* Save the indices of CS denominations */ - if (0 < mc->request.refresh.num_cs_r_values) - { - size_t j = 0; - - mc->request.cs_indices = GNUNET_new_array ( - mc->request.refresh.num_cs_r_values, - uint32_t); - - for (size_t i = 0; i < mc->request.refresh.num_coins; i++) - { - if (is_cs_denom[i]) - mc->request.cs_indices[j++] = i; - } - } - 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) -{ - /* We can now compute the commitment */ - { - struct TALER_KappaHashBlindedPlanchetsP k_bps_h = {0}; - - for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) - TALER_wallet_blinded_planchets_hash ( - mc->request.refresh.num_coins, - mc->request.planchets[k], - mc->request.denoms_h, - &k_bps_h.tuple[k]); - - TALER_refresh_get_commitment_v27 ( - &mc->request.refresh.rc, - &mc->request.refresh.refresh_seed, - mc->request.no_blinding_seed - ? NULL - : &mc->request.refresh.blinding_seed, - &k_bps_h, - &mc->request.refresh.coin.coin_pub, - &mc->request.refresh.amount_with_fee); - } - - - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_melt_verify ( - &mc->request.refresh.amount_with_fee, - &mc->melted_coin_denom->meta.fees.refresh, - &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++; -} - - -/** - * Check for information about the melted coin's denomination, - * extracting its validity status and fee structure. - * Baseline: check if deposits/refreshes are generally - * simply still allowed for this denomination. - * - * @param mc parsed request information - */ -static void -phase_check_melt_valid (struct MeltContext *mc) -{ - enum MeltPhase current_phase = mc->phase; - /** - * Find the old coin's denomination. - * Note that we return only on GNUNET_SYSERR, - * because GNUNET_NO for the expired denomination - * will be handled below, with the zombie-check. - */ - if (GNUNET_SYSERR == - find_denomination (mc, - &mc->request.refresh.coin.denom_pub_hash, - &mc->melted_coin_denom)) - return; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Melted coin's denomination is worth %s\n", - TALER_amount2s (&mc->melted_coin_denom->meta.value)); - - /* sanity-check that "total melt amount > melt fee" */ - if (0 < - TALER_amount_cmp (&mc->melted_coin_denom->meta.fees.refresh, - &mc->request.refresh.amount_with_fee)) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_FEES_EXCEED_CONTRIBUTION); - return; - } - - if (GNUNET_OK != - TALER_test_coin_valid (&mc->request.refresh.coin, - &mc->melted_coin_denom->denom_pub)) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_DENOMINATION_SIGNATURE_INVALID); - return; - } - - /** - * find_denomination might have set the phase to - * produce an error, but we are still investigating. - * We reset the phase. - */ - mc->phase = current_phase; - mc->error.code = MELT_ERROR_NONE; - - if (GNUNET_TIME_absolute_is_past ( - mc->melted_coin_denom->meta.expire_deposit.abs_time)) - { - /** - * We are past deposit expiration time, but maybe this is a zombie? - */ - struct TALER_DenominationHashP denom_hash; - enum GNUNET_DB_QueryStatus qs; - - /* Check that the coin is dirty (we have seen it before), as we will - not just allow melting of a *fresh* coin where the denomination was - revoked (those must be recouped) */ - qs = TEH_plugin->get_coin_denomination ( - TEH_plugin->cls, - &mc->request.refresh.coin.coin_pub, - &mc->known_coin_id, - &denom_hash); - if (0 > qs) - { - /* There is no good reason for a serialization failure here: */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - SET_ERROR (mc, - MELT_ERROR_DB_FETCH_FAILED); - return; - } - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) - { - /* We never saw this coin before, so _this_ justification is not OK. - * Note that the error was already set in find_denominations. */ - GNUNET_assert (MELT_ERROR_DENOMINATION_EXPIRED == - mc->error.code); - GNUNET_assert (MELT_PHASE_GENERATE_REPLY_ERROR == - mc->phase); - return; - } - /* sanity check */ - if (0 != - GNUNET_memcmp (&denom_hash, - &mc->request.refresh.coin.denom_pub_hash)) - { - GNUNET_break_op (0); - SET_ERROR_WITH_DETAIL (mc, - MELT_COIN_CONFLICTING_DENOMINATION_KEY, - denom_h, - denom_hash); - return; - } - /* Minor optimization: no need to run the - "ensure_coin_known" part of the transaction */ - mc->coin_is_dirty = true; - /* check later that zombie is satisfied */ - mc->zombie_required = true; - } - 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) -{ - mc->request.refresh.denom_sigs - = GNUNET_new_array ( - mc->request.refresh.num_coins, - struct TALER_BlindedDenominationSignature); - mc->request.refresh.noreveal_index = - GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_STRONG, - TALER_CNC_KAPPA); - - /* Choose and sign the coins */ - { - struct TEH_CoinSignData csds[mc->request.refresh.num_coins]; - enum TALER_ErrorCode ec_denomination_sign; - size_t noreveal_idx = mc->request.refresh.noreveal_index; - - memset (csds, - 0, - sizeof(csds)); - - /* Pick the chosen blinded coins */ - for (size_t i = 0; i<mc->request.refresh.num_coins; i++) - { - csds[i].bp = &mc->request.planchets[noreveal_idx][i]; - csds[i].h_denom_pub = &mc->request.denoms_h[i]; - } - - ec_denomination_sign = TEH_keys_denomination_batch_sign ( - mc->request.refresh.num_coins, - csds, - true, /* for melt */ - 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; - } - - /* Save the hash of chosen planchets */ - mc->request.refresh.selected_h = - mc->request.kappa_planchets_h.tuple[noreveal_idx]; - - /** - * For the denominations with cipher CS, calculate the R-values - * and save the choices we made now, as at a later point, the - * private keys for the denominations might now be available anymore - * to make the same choice again. - */ - if (0 < mc->request.refresh.num_cs_r_values) - { - size_t num_cs_r_values = mc->request.refresh.num_cs_r_values; - struct TEH_CsDeriveData cdds[num_cs_r_values]; - struct GNUNET_CRYPTO_CsSessionNonce nonces[num_cs_r_values]; - - memset (nonces, 0, sizeof(nonces)); - mc->request.refresh.cs_r_values - = GNUNET_new_array ( - num_cs_r_values, - struct GNUNET_CRYPTO_CSPublicRPairP); - mc->request.refresh.cs_r_choices = 0; - - GNUNET_assert (! mc->request.refresh.no_blinding_seed); - TALER_cs_derive_nonces_from_seed ( - &mc->request.refresh.blinding_seed, - true, /* for melt */ - num_cs_r_values, - mc->request.cs_indices, - nonces); - - for (size_t i = 0; i < num_cs_r_values; i++) - { - size_t idx = mc->request.cs_indices[i]; - - GNUNET_assert (idx < mc->request.refresh.num_coins); - cdds[i].h_denom_pub = &mc->request.denoms_h[idx]; - cdds[i].nonce = &nonces[i]; - } - - /** - * Let the crypto helper generate the R-values and - * make the choices - */ - if (TALER_EC_NONE != - TEH_keys_denomination_cs_batch_r_pub_simple ( - mc->request.refresh.num_cs_r_values, - cdds, - true, /* for melt */ - mc->request.refresh.cs_r_values)) - { - GNUNET_break (0); - SET_ERROR (mc, - MELT_ERROR_CRYPTO_HELPER); - return; - } - - /* Now save the choices for the selected bits */ - for (size_t i = 0; i < num_cs_r_values; i++) - { - size_t idx = mc->request.cs_indices[i]; - - struct TALER_BlindedDenominationSignature *sig = - &mc->request.refresh.denom_sigs[idx]; - uint8_t bit = sig->blinded_sig->details.blinded_cs_answer.b; - - mc->request.refresh.cs_r_choices |= bit << i; - GNUNET_static_assert ( - TALER_MAX_COINS <= - sizeof(mc->request.refresh.cs_r_choices) * 8); - } - } - } - mc->phase++; -} - - -/** - * Generates response for the melt request. - * - * @param mc melt operation context - */ -static void -phase_generate_reply_success (struct MeltContext *mc) -{ - struct TALER_EXCHANGEDB_Refresh_v27 *db_obj; - struct TALER_ExchangePublicKeyP pub; - struct TALER_ExchangeSignatureP sig; - enum TALER_ErrorCode ec_confirmation_sign; - - db_obj = mc->request.is_idempotent - ? &mc->request.refresh_idem - : &mc->request.refresh; - 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_OK, - 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_refresh"); - 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; - mc->error.code = MELT_ERROR_NONE; - 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: - break; - case MELT_ERROR_REQUEST_PARAMETER_MALFORMED: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - mc->error.details.request_parameter_malformed)); - return; - 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)); - return; - 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)); - return; - 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)); - return; - case MELT_ERROR_DB_PREFLIGHT_FAILURE: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - "make_coin_known")); - return; - case MELT_ERROR_DB_MAKE_COIN_KNOW_FAILURE: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - "preflight failure")); - return; - case MELT_ERROR_COIN_UNKNOWN: - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - TALER_EC_EXCHANGE_GENERIC_COIN_UNKNOWN, - NULL)); - return; - case MELT_COIN_CONFLICTING_DENOMINATION_KEY: - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY, - TALER_B2S (&mc->error.details.denom_h))); - return; - case MELT_ERROR_COIN_EXPIRED_NO_ZOMBIE: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_MELT_COIN_EXPIRED_NO_ZOMBIE, - NULL)); - return; - case MELT_ERROR_DENOMINATION_SIGN: - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - mc->error.details.ec_denomination_sign, - NULL)); - return; - case MELT_ERROR_DENOMINATION_SIGNATURE_INVALID: - finish_loop (mc, - TALER_MHD_reply_with_error (mc->rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, - NULL)); - return; - 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)); - return; - 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, - "MELT")); - return; - 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, - "MELT")); - return; - 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)); - return; - case MELT_ERROR_COIN_CIPHER_MISMATCH: - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, - NULL)); - return; - case MELT_ERROR_BLINDING_SEED_REQUIRED: - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - TALER_EC_GENERIC_PARAMETER_MISSING, - "blinding_seed")); - return; - case MELT_ERROR_CRYPTO_HELPER: - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL)); - return; - 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)); - return; - } - case MELT_ERROR_AGE_RESTRICTION_COMMITMENT_INVALID: - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - TALER_EC_EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID, - "old_age_commitment_h")); - return; - 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")); - return; - 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")); - return; - case MELT_ERROR_FEES_EXCEED_CONTRIBUTION: - finish_loop (mc, - TALER_MHD_reply_with_error (mc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_MELT_FEES_EXCEED_CONTRIBUTION, - NULL)); - return; - case MELT_ERROR_AMOUNT_WITH_FEE_INCORRECT: - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW, - "value_with_fee incorrect")); - return; - case MELT_ERROR_CONFIRMATION_SIGN: - finish_loop (mc, - TALER_MHD_reply_with_ec ( - mc->rc->connection, - mc->error.details.ec_confirmation_sign, - NULL)); - return; - 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)); - return; - 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")); - return; - 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")); - return; - 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)); - return; - } - GNUNET_break (0); - finish_loop (mc, - TALER_MHD_reply_with_error ( - mc->rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "error phase without error")); -} - - -/** - * 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 found; - bool nonce_reuse; - uint32_t noreveal_index; - struct TALER_Amount insufficient_funds; - - qs = TEH_plugin->do_refresh (TEH_plugin->cls, - &mc->request.refresh, - &mc->now, - &found, - &noreveal_index, - &mc->zombie_required, - &nonce_reuse, - &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_refresh"); - return qs; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_COIN_UNKNOWN); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (found) - { - /** - * This request is idempotent, set the nonreveal_index - * to the previous one and reply success. - */ - mc->request.refresh.noreveal_index = noreveal_index; - mc->phase = MELT_PHASE_GENERATE_REPLY_SUCCESS; - mc->error.code = MELT_ERROR_NONE; - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; - } - if (nonce_reuse) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_NONCE_RESUSE); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (! balance_ok) - { - GNUNET_break_op (0); - SET_ERROR_WITH_FIELD (mc, - MELT_ERROR_INSUFFICIENT_FUNDS, - insufficient_funds); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (mc->zombie_required) - { - GNUNET_break_op (0); - SET_ERROR (mc, - MELT_ERROR_COIN_EXPIRED_NO_ZOMBIE); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - 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) -{ - if (GNUNET_SYSERR == - TEH_plugin->preflight (TEH_plugin->cls)) - { - GNUNET_break (0); - SET_ERROR (mc, - MELT_ERROR_DB_PREFLIGHT_FAILURE); - return; - } - - /* first, make sure coin is known */ - if (! mc->coin_is_dirty) - { - MHD_RESULT mhd_ret = -1; - enum GNUNET_DB_QueryStatus qs; - - for (unsigned int tries = 0; tries<MAX_TRANSACTION_COMMIT_RETRIES; tries++) - { - qs = TEH_make_coin_known (&mc->request.refresh.coin, - mc->rc->connection, - &mc->known_coin_id, - &mhd_ret); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - if (0 > qs) - { - GNUNET_break (0); - /* Check if an answer has been queued */ - switch (mhd_ret) - { - case MHD_NO: - mc->phase = MELT_PHASE_RETURN_NO; - return; - case MHD_YES: - mc->phase = MELT_PHASE_RETURN_YES; - return; - default: - /* ignore */ - } - SET_ERROR (mc, - MELT_ERROR_DB_MAKE_COIN_KNOW_FAILURE); - return; - } - } - - /* run main database transaction */ - { - MHD_RESULT mhd_ret = -1; - enum GNUNET_GenericReturnValue ret; - enum MeltPhase current_phase = mc->phase; - - GNUNET_assert (MELT_PHASE_RUN_TRANSACTION == - current_phase); - ret = TEH_DB_run_transaction (mc->rc->connection, - "run melt", - TEH_MT_REQUEST_MELT, - &mhd_ret, - &melt_transaction, - mc); - if (GNUNET_OK != ret) - { - GNUNET_break (0); - /* Check if an answer has been queued */ - switch (mhd_ret) - { - case MHD_NO: - mc->phase = MELT_PHASE_RETURN_NO; - return; - case MHD_YES: - mc->phase = MELT_PHASE_RETURN_YES; - return; - default: - /* ignore */ - } - GNUNET_assert (MELT_ERROR_NONE != mc->error.code); - GNUNET_assert (MELT_PHASE_GENERATE_REPLY_ERROR == mc->phase); - return; - } - /** - * In case of idempotency (which is not an error condition), - * the phase has changed in melt_transaction. - * We simple return. - */ - if (current_phase != mc->phase) - return; - } - mc->phase++; -} - - -MHD_RESULT -TEH_handler_melt_v27 ( - struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[0]) -{ - struct MeltContext *mc = rc->rh_ctx; - - (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 (); - } - - while (true) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "melt processing in phase %d\n", - mc->phase); - switch (mc->phase) - { - case MELT_PHASE_PARSE: - phase_parse_request (mc, - root); - break; - case MELT_PHASE_CHECK_MELT_VALID: - phase_check_melt_valid (mc); - break; - 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_v27.h b/src/exchange/taler-exchange-httpd_melt_v27.h @@ -1,52 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2025 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU 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_v27.h - * @brief Handle /melt requests, starting with v27 of the protocol - * @author Özgür Kesim - */ -#ifndef TALER_EXCHANGE_HTTPD_MELT_V27_H -#define TALER_EXCHANGE_HTTPD_MELT_V27_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_v27_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_v27 ( - struct TEH_RequestContext *rc, - const json_t *root, - const char *const args[0]); - - -#endif diff --git a/src/exchange/taler-exchange-httpd_reveal-melt.c b/src/exchange/taler-exchange-httpd_reveal-melt.c @@ -51,15 +51,35 @@ struct MeltRevealContext * The data from the original melt. Will be retrieved from * the DB via @a rc. */ - struct TALER_EXCHANGEDB_Refresh_v27 refresh; + struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS refresh; /** + * True, if @e signatures were not provided in the request. + */ + bool no_signatures; + + /** + * @since v27 + * @deprecated after vDOLDPLUS + * * TALER_CNC_KAPPA-1 disclosed signatures for public refresh nonces. */ struct TALER_PrivateRefreshNonceSignatureP signatures[KAPPA_MINUS_1]; /** - * False, if no age commitment was provided + * True, if @e transfer_secret_seeds were not provided in the request. + */ + bool no_transfer_secret_seeds; + + /** + * @since vDOLDPLUS + * + * The transfer secret seeds for the revealed batches of coins. + */ + struct TALER_PrivateRefreshBatchSeedP transfer_secret_seeds[KAPPA_MINUS_1]; + + /** + * True, if no @e age_commitment was provided */ bool no_age_commitment; @@ -88,7 +108,7 @@ static enum GNUNET_GenericReturnValue find_original_refresh ( struct MHD_Connection *connection, const struct TALER_RefreshCommitmentP *rc, - struct TALER_EXCHANGEDB_Refresh_v27 *refresh, + struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *refresh, MHD_RESULT *result) { enum GNUNET_DB_QueryStatus qs; @@ -280,7 +300,7 @@ calculate_blinded_detail ( /** * @brief Checks the validity of the disclosed signatures as follows: * - Verifies the validity of the disclosed signatures with the old coin's public key - * - Derives the seeds from those signatures for disclosed fresh coins + * - Derives the seeds for disclosed fresh coins * - Derives the fresh coins from the seeds * - Derives new age commitment * - Calculates the blinded coin planchet hashes @@ -296,16 +316,18 @@ calculate_blinded_detail ( * @param con HTTP-connection to the client * @param rf Original refresh object from the previous /melt request * @param old_age_commitment The age commitment of the original coin - * @param signatures The secrets of the disclosed coins, KAPPA_MINUS_1*num_coins many + * @param signatures The secrets of the disclosed coins, KAPPA_MINUS_1*num_coins many, maybe NULL + * @param rev_batch_seeds The seeds for the transfer secrets of the disclosed coins, KAPPA_MINUS_1 many, maybe NULL * @param[out] result On error, a HTTP-response will be queued and result set accordingly * @return #GNUNET_OK on success, #GNUNET_SYSERR otherwise */ static enum GNUNET_GenericReturnValue verify_commitment ( struct MHD_Connection *con, - const struct TALER_EXCHANGEDB_Refresh_v27 *rf, + const struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *rf, const struct TALER_AgeCommitment *old_age_commitment, - const struct TALER_PrivateRefreshNonceSignatureP signatures[KAPPA_MINUS_1], + const struct TALER_PrivateRefreshNonceSignatureP (*signatures)[KAPPA_MINUS_1], + const struct TALER_PrivateRefreshBatchSeedP (*rev_batch_seeds)[KAPPA_MINUS_1], MHD_RESULT *result) { enum GNUNET_GenericReturnValue ret; @@ -314,7 +336,6 @@ verify_commitment ( struct TALER_DenominationHashP *denoms_h[rf->num_coins]; struct TALER_Amount total_amount; struct TALER_Amount total_fee; - struct TALER_KappaPublicRefreshNoncesP kappa_nonces; bool is_cs[rf->num_coins]; size_t cs_count = 0; @@ -333,6 +354,33 @@ verify_commitment ( sizeof(is_cs)); /** + * Consistency check: + * If the refresh was for v27, signatures must not be NULL, + * otherwise the revealed batch seeds must not be NULL. + */ + if (rf->is_v27_refresh) + { + if (NULL == signatures) + { + *result = TALER_MHD_reply_with_error (con, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "signatures missing"); + return GNUNET_SYSERR; + } + } + else /* vDOLDPLUS */ + { + if (NULL == rev_batch_seeds) + { + *result = TALER_MHD_reply_with_error (con, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "batch_seeds missing"); + return GNUNET_SYSERR; + } + } + /** * We need the current keys in memory for the meta-data of the denominations */ keys = TEH_keys_get_state (); @@ -394,23 +442,26 @@ verify_commitment ( NULL); return GNUNET_SYSERR; } - /** - * We expand the provided refresh_seed from the original call to /melt, - * into kappa many batch seeds, from which we will later use all except the - * noreveal_index one. - */ - TALER_refresh_expand_kappa_nonces ( - &rf->refresh_seed, - &kappa_nonces); - - /** - * First things first: Verify the signature of the old coin + * First things first for v27 clients: Verify the signature of the old coin * over the refresh nonce. This proves the ownership * for the fresh coin. */ + if (rf->is_v27_refresh) { size_t sig_idx = 0; + struct TALER_KappaPublicRefreshNoncesP kappa_nonces; + + GNUNET_assert (NULL != signatures); + + /** + * We expand the provided refresh_seed from the original call to /melt, + * into kappa many batch seeds, from which we will later use all except the + * noreveal_index one. + */ + TALER_refresh_expand_kappa_nonces_v27 ( + &rf->refresh_seed, + &kappa_nonces); for (uint8_t k=0; k < TALER_CNC_KAPPA; k++) { @@ -423,7 +474,7 @@ verify_commitment ( rf->num_coins, denoms_h, k, - &signatures[sig_idx++])) + &(*signatures)[sig_idx++])) { GNUNET_break_op (0); *result = TALER_MHD_reply_with_ec (con, @@ -433,7 +484,6 @@ verify_commitment ( } } } - /** * In the following scope, we start collecting blinded coin planchet hashes, * either those persisted from the original request to /melt, or we @@ -449,7 +499,11 @@ verify_commitment ( union GNUNET_CRYPTO_BlindSessionNonce b_nonces[GNUNET_NZL (cs_count)]; size_t cs_idx = 0; /* [0...cs_count) */ uint8_t sig_idx = 0; /* [0..KAPPA_MINUS_1) */ - + /* These two are only necessary for non-v27 clients, but this is the common case. */ + struct TALER_TransferPublicKeyP k_tpbs[TALER_CNC_KAPPA][rf->num_coins]; + struct TALER_KappaTransferPublicKeys kappa_transfer_pubs = { + .num_transfer_pubs = rf->num_coins + }; /** * First, derive the blinding nonces for the CS denominations all at once. */ @@ -487,24 +541,40 @@ verify_commitment ( * the disclosed input material and generate the * hashes of them. */ - struct TALER_PlanchetMasterSecretP secrets[rf->num_coins]; + struct TALER_PlanchetMasterSecretP planchet_secrets[rf->num_coins]; struct TALER_PlanchetDetail details[rf->num_coins]; - memset (secrets, + memset (planchet_secrets, 0, - sizeof(secrets)); + sizeof(planchet_secrets)); memset (details, 0, sizeof(details)); + + GNUNET_assert (sig_idx < KAPPA_MINUS_1); + /** * Expand from the k-th signature all num_coin planchet secrets, * except for the noreveal_index. */ - GNUNET_assert (sig_idx < KAPPA_MINUS_1); - TALER_refresh_signature_to_secrets ( - &signatures[sig_idx++], - rf->num_coins, - secrets); + if (rf->is_v27_refresh) + { + TALER_refresh_signature_to_secrets_v27 ( + &(*signatures)[sig_idx++], + rf->num_coins, + planchet_secrets); + } + else /* vDOLDPLUS */ + { + TALER_refresh_expand_batch_seed_to_transfer_data ( + &(*rev_batch_seeds)[sig_idx++], + &rf->coin.coin_pub, + rf->num_coins, + planchet_secrets, + k_tpbs[k]); + + kappa_transfer_pubs.batch[k] = k_tpbs[k]; + } /** * Reset the index for the CS denominations. */ @@ -529,7 +599,7 @@ verify_commitment ( } ret = calculate_blinded_detail (con, denom_keys[coin_idx], - &secrets[coin_idx], + &planchet_secrets[coin_idx], rp, np, old_age_commitment, @@ -556,14 +626,30 @@ verify_commitment ( { struct TALER_RefreshCommitmentP rc; - TALER_refresh_get_commitment_v27 (&rc, - &rf->refresh_seed, - rf->no_blinding_seed + if (rf->is_v27_refresh) + { + TALER_refresh_get_commitment_v27 (&rc, + &rf->refresh_seed, + rf->no_blinding_seed ? NULL : &rf->blinding_seed, - &kappa_planchets_h, - &rf->coin.coin_pub, - &rf->amount_with_fee); + &kappa_planchets_h, + &rf->coin.coin_pub, + &rf->amount_with_fee); + } + else + { + TALER_refresh_get_commitment (&rc, + &rf->refresh_seed, + rf->no_blinding_seed + ? NULL + : &rf->blinding_seed, + &kappa_transfer_pubs, + &kappa_planchets_h, + &rf->coin.coin_pub, + &rf->amount_with_fee); + } + if (0 != GNUNET_CRYPTO_hash_cmp ( &rf->rc.session_hash, &rc.session_hash)) @@ -571,7 +657,7 @@ verify_commitment ( GNUNET_break_op (0); *result = TALER_MHD_reply_with_ec (con, TALER_EC_EXCHANGE_REFRESHES_REVEAL_INVALID_RCH, - NULL); + "rc"); return GNUNET_SYSERR; } } @@ -581,6 +667,61 @@ verify_commitment ( /** + * @brief Commit the successful reveal to the database + * + * @param con HTTP-connection to the client + * @param rc Original refresh commitment from the previous /melt request + * @param[out] result On error, a HTTP-response will be queued and result set accordingly + * @return #GNUNET_OK on success, #GNUNET_SYSERR otherwise + */ +static enum GNUNET_GenericReturnValue +commit_reveal ( + struct MHD_Connection *con, + const struct TALER_RefreshCommitmentP *rc, + MHD_RESULT *result) +{ + enum GNUNET_DB_QueryStatus qs; + + for (unsigned int retry = 0; retry < 3; retry++) + { + qs = TEH_plugin->mark_refresh_reveal_success ( + TEH_plugin->cls, + rc); + switch (qs) + { + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return GNUNET_OK; /* Only happy case */ + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + *result = TALER_MHD_reply_with_error (con, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_REFRESHES_REVEAL_SESSION_UNKNOWN, + NULL); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_HARD_ERROR: + *result = TALER_MHD_reply_with_ec (con, + TALER_EC_GENERIC_DB_STORE_FAILED, + "mark_refresh_reveal_success"); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SOFT_ERROR: + break; /* try again */ + default: + GNUNET_break (0); + *result = TALER_MHD_reply_with_ec (con, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL); + return GNUNET_SYSERR; + } + } + /* after unsuccessful retries*/ + *result = TALER_MHD_reply_with_ec (con, + TALER_EC_GENERIC_DB_STORE_FAILED, + "mark_refresh_reveal_success"); + return GNUNET_SYSERR; + +} + + +/** * @brief Send a response for "/reveal-melt" * * @param connection The http connection to the client to send the response to @@ -590,7 +731,7 @@ verify_commitment ( static MHD_RESULT reply_melt_reveal_success ( struct MHD_Connection *connection, - const struct TALER_EXCHANGEDB_Refresh_v27 *refresh) + const struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *refresh) { json_t *list = json_array (); GNUNET_assert (NULL != list); @@ -622,18 +763,31 @@ TEH_handler_reveal_melt ( MHD_RESULT result = MHD_NO; enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; struct MeltRevealContext actx = {0}; - struct GNUNET_JSON_Specification tuple[] = { + struct GNUNET_JSON_Specification sig_tuple[] = { GNUNET_JSON_spec_fixed_auto (NULL, &actx.signatures[0]), GNUNET_JSON_spec_fixed_auto (NULL, &actx.signatures[1]), GNUNET_JSON_spec_end () }; + struct GNUNET_JSON_Specification seeds_tuple[] = { + GNUNET_JSON_spec_fixed_auto (NULL, + &actx.transfer_secret_seeds[0]), + GNUNET_JSON_spec_fixed_auto (NULL, + &actx.transfer_secret_seeds[1]), + GNUNET_JSON_spec_end () + }; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("rc", &actx.rc), - TALER_JSON_spec_tuple_of ("signatures", - tuple), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_tuple_of ("signatures", + sig_tuple), + &actx.no_signatures), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_tuple_of ("batch_seeds", + seeds_tuple), + &actx.no_transfer_secret_seeds), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_age_commitment ("age_commitment", &actx.age_commitment), @@ -646,7 +800,8 @@ TEH_handler_reveal_melt ( * the size of TALER_CNC_KAPPA. * Let's make sure we keep this in sync. */ - _Static_assert (KAPPA_MINUS_1 == 2); + _Static_assert (KAPPA_MINUS_1 == 2, + "TALER_CNC_KAPPA isn't 3!?!?"); /* Parse JSON body*/ ret = TALER_MHD_parse_json_data (rc->connection, @@ -686,10 +841,21 @@ TEH_handler_reveal_melt ( actx.no_age_commitment ? NULL : &actx.age_commitment, - actx.signatures, + actx.no_signatures + ? NULL + : &actx.signatures, + actx.no_transfer_secret_seeds + ? NULL + : &actx.transfer_secret_seeds, &result)) break; + if (GNUNET_OK != + commit_reveal (rc->connection, + &actx.rc, + &result)) + break; + /* Finally, return the signatures */ result = reply_melt_reveal_success (rc->connection, &actx.refresh); diff --git a/src/exchangedb/0004-refresh.sql b/src/exchangedb/0004-refresh.sql @@ -0,0 +1,42 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +CREATE FUNCTION alter_table_refresh4() +RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + ALTER TABLE refresh + ADD COLUMN revealed BOOLEAN NOT NULL DEFAULT(FALSE), + ADD COLUMN transfer_pubs BYTEA[]; + COMMENT ON COLUMN refresh.revealed + IS 'TRUE if the client has successfully revealed the secrets in the cut-and-choose step.'; + COMMENT ON COLUMN refresh.transfer_pubs + IS 'The selected batch of transfer public keys, at noreveal_index'; +END $$; + +INSERT INTO exchange_tables + (name + ,version + ,action + ,partitioned + ,by_range) + VALUES + ('refresh4' + ,'exchange-0004' + ,'alter' + ,TRUE + ,FALSE); diff --git a/src/exchangedb/Makefile.am b/src/exchangedb/Makefile.am @@ -29,7 +29,9 @@ sqlinputs = \ 0002-*.sql \ exchange-0002.sql.in \ 0003-*.sql \ - exchange-0003.sql.in + exchange-0003.sql.in \ + 0004-*.sql \ + exchange-0004.sql.in sql_DATA = \ benchmark-0001.sql \ @@ -120,7 +122,6 @@ libtaler_plugin_exchangedb_postgres_la_SOURCES = \ pg_do_withdraw.h pg_do_withdraw.c \ pg_do_check_deposit_idempotent.h pg_do_check_deposit_idempotent.c \ pg_do_deposit.h pg_do_deposit.c \ - pg_do_melt.h pg_do_melt.c \ pg_do_refresh.h pg_do_refresh.c \ pg_do_purse_delete.c pg_do_purse_delete.h \ pg_do_purse_deposit.h pg_do_purse_deposit.c \ @@ -159,7 +160,6 @@ libtaler_plugin_exchangedb_postgres_la_SOURCES = \ pg_get_purse_deposit.h pg_get_purse_deposit.c \ pg_get_purse_request.c pg_get_purse_request.h \ pg_get_ready_deposit.h pg_get_ready_deposit.c \ - pg_get_refresh_reveal.h pg_get_refresh_reveal.c \ pg_get_reserve_balance.h pg_get_reserve_balance.c \ pg_get_reserve_by_h_planchets.h pg_get_reserve_by_h_planchets.c \ pg_get_reserve_history.c pg_get_reserve_history.h \ @@ -231,6 +231,7 @@ libtaler_plugin_exchangedb_postgres_la_SOURCES = \ pg_lookup_wire_fee_by_time.h pg_lookup_wire_fee_by_time.c \ pg_lookup_wire_timestamp.h pg_lookup_wire_timestamp.c \ pg_lookup_wire_transfer.h pg_lookup_wire_transfer.c \ + pg_mark_refresh_reveal_success.h pg_mark_refresh_reveal_success.c \ pg_persist_kyc_attributes.h pg_persist_kyc_attributes.c \ pg_persist_policy_details.h pg_persist_policy_details.c \ pg_preflight.h pg_preflight.c \ diff --git a/src/exchangedb/exchange-0004.sql.in b/src/exchangedb/exchange-0004.sql.in @@ -20,5 +20,6 @@ SELECT _v.register_patch('exchange-0004', NULL, NULL); SET search_path TO exchange; #include "0004-kyc_attributes.sql" +#include "0004-refresh.sql" COMMIT; diff --git a/src/exchangedb/exchange_do_refresh.sql b/src/exchangedb/exchange_do_refresh.sql @@ -21,6 +21,7 @@ CREATE FUNCTION exchange_do_refresh( IN in_rc BYTEA, IN in_now INT8, IN in_refresh_seed BYTEA, + IN in_transfer_pubs BYTEA[], IN in_planchets_h BYTEA, IN in_amount_with_fee taler_amount, IN in_blinding_seed BYTEA, @@ -122,6 +123,7 @@ INSERT INTO exchange.refresh ,old_coin_pub ,old_coin_sig ,planchets_h + ,transfer_pubs ,amount_with_fee ,noreveal_index ,refresh_seed @@ -138,6 +140,7 @@ INSERT INTO exchange.refresh ,in_old_coin_pub ,in_old_coin_sig ,in_planchets_h + ,in_transfer_pubs ,in_amount_with_fee ,in_noreveal_index ,in_refresh_seed diff --git a/src/exchangedb/exchangedb_transactions.c b/src/exchangedb/exchangedb_transactions.c @@ -86,7 +86,7 @@ TALER_EXCHANGEDB_calculate_transaction_list_totals ( } have_refund = true; break; - case TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP: + case TALER_EXCHANGEDB_TT_RECOUP_REFRESH_RECEIVER: /* refunded += pos->value */ if (0 > TALER_amount_add (&refunded, @@ -97,7 +97,7 @@ TALER_EXCHANGEDB_calculate_transaction_list_totals ( return GNUNET_SYSERR; } break; - case TALER_EXCHANGEDB_TT_RECOUP: + case TALER_EXCHANGEDB_TT_RECOUP_WITHDRAW: /* spent += pos->value */ if (0 > TALER_amount_add (&spent, diff --git a/src/exchangedb/perf_deposits_get_ready.c b/src/exchangedb/perf_deposits_get_ready.c @@ -75,7 +75,7 @@ static struct TALER_MerchantWireHashP h_wire_wt; */ static struct DenomKeyPair **new_dkp; -static struct TALER_EXCHANGEDB_RefreshRevealedCoin *revealed_coins; +static struct CoinInfo *revealed_coins; struct DenomKeyPair { @@ -84,6 +84,16 @@ struct DenomKeyPair }; +struct CoinInfo +{ + struct TALER_DenominationHashP h_denom_pub; + struct TALER_CoinSpendSignatureP orig_coin_link_sig; + struct TALER_BlindedCoinHashP coin_envelope_hash; + struct TALER_BlindedDenominationSignature coin_sig; + struct TALER_ExchangeBlindingValues exchange_vals; + struct TALER_BlindedPlanchet blinded_planchet; +}; + /** * Destroy a denomination key pair. The key is not necessarily removed from the DB. * @@ -201,7 +211,7 @@ run (void *cls) struct TALER_EXCHANGEDB_Refund *ref; unsigned int *perm; unsigned long long duration_sq; - struct TALER_EXCHANGEDB_RefreshRevealedCoin *ccoin; + struct CoinInfo *ccoin; struct TALER_DenomFeeSet fees; struct GNUNET_CRYPTO_BlindingInputValues bi = { .cipher = GNUNET_CRYPTO_BSA_RSA, @@ -264,7 +274,7 @@ run (void *cls) struct TALER_DenominationPublicKey); revealed_coins = GNUNET_new_array (MELT_NEW_COINS, - struct TALER_EXCHANGEDB_RefreshRevealedCoin); + struct CoinInfo); for (unsigned int cnt = 0; cnt < MELT_NEW_COINS; cnt++) { struct GNUNET_TIME_Timestamp now; diff --git a/src/exchangedb/perf_select_refunds_by_coin.c b/src/exchangedb/perf_select_refunds_by_coin.c @@ -72,7 +72,7 @@ static struct TALER_MerchantWireHashP h_wire_wt; static struct DenomKeyPair **new_dkp; -static struct TALER_EXCHANGEDB_RefreshRevealedCoin *revealed_coins; +static struct CoinInfo *revealed_coins; struct DenomKeyPair { @@ -81,6 +81,16 @@ struct DenomKeyPair }; +struct CoinInfo +{ + struct TALER_DenominationHashP h_denom_pub; + struct TALER_CoinSpendSignatureP orig_coin_link_sig; + struct TALER_BlindedCoinHashP coin_envelope_hash; + struct TALER_BlindedDenominationSignature coin_sig; + struct TALER_ExchangeBlindingValues exchange_vals; + struct TALER_BlindedPlanchet blinded_planchet; +}; + /** * Destroy a denomination key pair. The key is not necessarily removed from the DB. * @@ -229,7 +239,7 @@ run (void *cls) struct TALER_EXCHANGEDB_Refund *ref = NULL; unsigned int *perm; unsigned long long duration_sq; - struct TALER_EXCHANGEDB_RefreshRevealedCoin *ccoin; + struct CoinInfo *ccoin; struct TALER_DenominationPublicKey *new_denom_pubs = NULL; struct TALER_DenomFeeSet fees; unsigned int count = 0; @@ -290,7 +300,7 @@ run (void *cls) struct TALER_DenominationPublicKey); revealed_coins = GNUNET_new_array (MELT_NEW_COINS, - struct TALER_EXCHANGEDB_RefreshRevealedCoin); + struct CoinInfo); for (unsigned int cnt = 0; cnt < MELT_NEW_COINS; cnt++) { struct GNUNET_TIME_Timestamp now; diff --git a/src/exchangedb/pg_do_melt.c b/src/exchangedb/pg_do_melt.c @@ -1,82 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2022 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file exchangedb/pg_do_melt.c - * @brief Implementation of the do_melt function for Postgres - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler/taler_error_codes.h" -#include "taler/taler_dbevents.h" -#include "taler/taler_pq_lib.h" -#include "pg_do_melt.h" -#include "pg_helper.h" - - -enum GNUNET_DB_QueryStatus -TEH_PG_do_melt ( - void *cls, - const struct TALER_RefreshMasterSecretP *rms, - struct TALER_EXCHANGEDB_Refresh *refresh, - uint64_t known_coin_id, - bool *zombie_required, - bool *balance_ok) -{ - struct PostgresClosure *pg = cls; - struct GNUNET_PQ_QueryParam params[] = { - NULL == rms - ? GNUNET_PQ_query_param_null () - : GNUNET_PQ_query_param_auto_from_type (rms), - TALER_PQ_query_param_amount (pg->conn, - &refresh->amount_with_fee), - GNUNET_PQ_query_param_auto_from_type (&refresh->rc), - GNUNET_PQ_query_param_auto_from_type (&refresh->coin.coin_pub), - GNUNET_PQ_query_param_auto_from_type (&refresh->coin_sig), - GNUNET_PQ_query_param_uint64 (&known_coin_id), - GNUNET_PQ_query_param_uint32 (&refresh->noreveal_index), - GNUNET_PQ_query_param_bool (*zombie_required), - GNUNET_PQ_query_param_end - }; - bool is_null; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_bool ("balance_ok", - balance_ok), - GNUNET_PQ_result_spec_bool ("zombie_required", - zombie_required), - GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_uint32 ("noreveal_index", - &refresh->noreveal_index), - &is_null), - GNUNET_PQ_result_spec_end - }; - enum GNUNET_DB_QueryStatus qs; - - PREPARE (pg, - "call_melt", - "SELECT " - " out_balance_ok AS balance_ok" - ",out_zombie_bad AS zombie_required" - ",out_noreveal_index AS noreveal_index" - " FROM exchange_do_melt" - " ($1,$2,$3,$4,$5,$6,$7,$8);"); - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "call_melt", - params, - rs); - if (is_null) - refresh->noreveal_index = UINT32_MAX; /* set to very invalid value */ - return qs; -} diff --git a/src/exchangedb/pg_do_melt.h b/src/exchangedb/pg_do_melt.h @@ -1,49 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2022 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file exchangedb/pg_do_melt.h - * @brief implementation of the do_melt function for Postgres - * @author Christian Grothoff - */ -#ifndef PG_DO_MELT_H -#define PG_DO_MELT_H - -#include "taler/taler_util.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_exchangedb_plugin.h" -/** - * Perform melt operation, checking for sufficient balance - * of the coin and possibly persisting the melt details. - * - * @param cls the `struct PostgresClosure` with the plugin-specific state - * @param rms client-contributed input for CS denominations that must be checked for idempotency, or NULL for non-CS withdrawals - * @param[in,out] refresh refresh operation details; the noreveal_index - * is set in case the coin was already melted before - * @param known_coin_id row of the coin in the known_coins table - * @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] balance_ok set to true if the balance was sufficient - * @return query execution status - */ -enum GNUNET_DB_QueryStatus -TEH_PG_do_melt ( - void *cls, - const struct TALER_RefreshMasterSecretP *rms, - struct TALER_EXCHANGEDB_Refresh *refresh, - uint64_t known_coin_id, - bool *zombie_required, - bool *balance_ok); - -#endif diff --git a/src/exchangedb/pg_do_refresh.c b/src/exchangedb/pg_do_refresh.c @@ -30,7 +30,7 @@ enum GNUNET_DB_QueryStatus TEH_PG_do_refresh ( void *cls, - struct TALER_EXCHANGEDB_Refresh_v27 *refresh, + struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *refresh, const struct GNUNET_TIME_Timestamp *timestamp, bool *found, uint32_t *noreveal_index, @@ -43,9 +43,14 @@ TEH_PG_do_refresh ( struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (&refresh->rc), GNUNET_PQ_query_param_timestamp (timestamp), - GNUNET_PQ_query_param_auto_from_type (&refresh->refresh_seed), + GNUNET_PQ_query_param_auto_from_type (&refresh->refresh_seed), /* 3 */ + (refresh->is_v27_refresh) + ? GNUNET_PQ_query_param_null () + : GNUNET_PQ_query_param_array_auto_from_type (refresh->num_coins, + refresh->transfer_pubs, + pg->conn), GNUNET_PQ_query_param_auto_from_type (&refresh->planchets_h), - TALER_PQ_query_param_amount (pg->conn, + TALER_PQ_query_param_amount (pg->conn, /* 6 */ &refresh->amount_with_fee), (refresh->no_blinding_seed) ? GNUNET_PQ_query_param_null () @@ -56,18 +61,18 @@ TEH_PG_do_refresh ( pg->conn) : GNUNET_PQ_query_param_null (), (0 < refresh->num_cs_r_values) - ? GNUNET_PQ_query_param_uint64 (&refresh->cs_r_choices) + ? GNUNET_PQ_query_param_uint64 (&refresh->cs_r_choices) /* 9 */ : GNUNET_PQ_query_param_null (), GNUNET_PQ_query_param_auto_from_type (&refresh->selected_h), TALER_PQ_query_param_array_blinded_denom_sig (refresh->num_coins, refresh->denom_sigs, pg->conn), - GNUNET_PQ_query_param_array_uint64 (refresh->num_coins, + GNUNET_PQ_query_param_array_uint64 (refresh->num_coins, /* 12 */ refresh->denom_serials, pg->conn), GNUNET_PQ_query_param_auto_from_type (&refresh->coin.coin_pub), GNUNET_PQ_query_param_auto_from_type (&refresh->coin_sig), - GNUNET_PQ_query_param_uint32 (&refresh->noreveal_index), + GNUNET_PQ_query_param_uint32 (&refresh->noreveal_index), /* 15 */ GNUNET_PQ_query_param_bool (*zombie_required), GNUNET_PQ_query_param_end }; @@ -108,7 +113,7 @@ TEH_PG_do_refresh ( ",out_noreveal_index AS noreveal_index" ",out_coin_balance AS coin_balance" " FROM exchange_do_refresh" - " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15);"); + " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "call_refresh", params, diff --git a/src/exchangedb/pg_do_refresh.h b/src/exchangedb/pg_do_refresh.h @@ -45,7 +45,7 @@ enum GNUNET_DB_QueryStatus TEH_PG_do_refresh ( void *cls, - struct TALER_EXCHANGEDB_Refresh_v27 *refresh, + struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *refresh, const struct GNUNET_TIME_Timestamp *timestamp, bool *found, uint32_t *noreveal_index, diff --git a/src/exchangedb/pg_get_coin_transactions.c b/src/exchangedb/pg_get_coin_transactions.c @@ -489,7 +489,7 @@ add_old_coin_recoup (void *cls, } tl = GNUNET_new (struct TALER_EXCHANGEDB_TransactionList); tl->next = chc->head; - tl->type = TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP; + tl->type = TALER_EXCHANGEDB_TT_RECOUP_REFRESH_RECEIVER; tl->details.old_coin_recoup = recoup; tl->serial_id = serial_id; tl->coin_history_id = chc->chid; @@ -553,7 +553,7 @@ add_coin_recoup (void *cls, } tl = GNUNET_new (struct TALER_EXCHANGEDB_TransactionList); tl->next = chc->head; - tl->type = TALER_EXCHANGEDB_TT_RECOUP; + tl->type = TALER_EXCHANGEDB_TT_RECOUP_WITHDRAW; tl->details.recoup = recoup; tl->serial_id = serial_id; tl->coin_history_id = chc->chid; @@ -745,11 +745,7 @@ handle_history_entry (void *cls, { "refunds", "get_refunds_by_coin", &add_coin_refund }, - [TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP] = - { "recoup_refresh::OLD", - "recoup_by_old_coin", - &add_old_coin_recoup }, - [TALER_EXCHANGEDB_TT_RECOUP] = + [TALER_EXCHANGEDB_TT_RECOUP_WITHDRAW] = { "recoup", "recoup_by_coin", &add_coin_recoup }, @@ -757,6 +753,10 @@ handle_history_entry (void *cls, { "recoup_refresh::NEW", "recoup_by_refreshed_coin", &add_coin_recoup_refresh }, + [TALER_EXCHANGEDB_TT_RECOUP_REFRESH_RECEIVER] = + { "recoup_refresh::OLD", + "recoup_by_old_coin", + &add_old_coin_recoup }, [TALER_EXCHANGEDB_TT_RESERVE_OPEN] = { "reserves_open_deposits", "reserve_open_by_coin", diff --git a/src/exchangedb/pg_get_refresh.c b/src/exchangedb/pg_get_refresh.c @@ -29,7 +29,7 @@ enum GNUNET_DB_QueryStatus TEH_PG_get_refresh (void *cls, const struct TALER_RefreshCommitmentP *rc, - struct TALER_EXCHANGEDB_Refresh_v27 *refresh) + struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *refresh) { struct PostgresClosure *pg = cls; struct GNUNET_PQ_QueryParam params[] = { @@ -38,9 +38,12 @@ TEH_PG_get_refresh (void *cls, }; bool no_cs_r_values; bool no_cs_r_choices; + bool no_transfer_pubs; size_t num_denom_sigs; + size_t num_transfer_pubs; struct TALER_BlindedDenominationSignature *denom_sigs = NULL; struct GNUNET_CRYPTO_CSPublicRPairP *cs_r_values = NULL; + struct TALER_TransferPublicKeyP *transfer_pubs = NULL; uint64_t *denom_serials = NULL; struct GNUNET_PQ_ResultSpec rs[] = { TALER_PQ_RESULT_SPEC_AMOUNT ("amount_with_fee", @@ -75,6 +78,13 @@ TEH_PG_get_refresh (void *cls, &refresh->planchets_h), GNUNET_PQ_result_spec_auto_from_type ("selected_h", &refresh->selected_h), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_array_fixed_size (pg->conn, + "transfer_pubs", + sizeof(*transfer_pubs), + &num_transfer_pubs, + (void **) &transfer_pubs), + &no_transfer_pubs), GNUNET_PQ_result_spec_array_uint64 (pg->conn, "denom_serials", &refresh->num_coins, @@ -83,6 +93,8 @@ TEH_PG_get_refresh (void *cls, "denom_sigs", &num_denom_sigs, &denom_sigs), + GNUNET_PQ_result_spec_bool ("revealed", + &refresh->revealed), GNUNET_PQ_result_spec_end }; enum GNUNET_DB_QueryStatus qs; @@ -103,9 +115,11 @@ TEH_PG_get_refresh (void *cls, ",cs_r_values" ",cs_r_choices" ",planchets_h" + ",transfer_pubs" ",selected_h" ",denom_serials" ",denom_sigs" + ",revealed" " FROM refresh" " JOIN known_coins kc" " ON (old_coin_pub = kc.coin_pub)" @@ -138,6 +152,27 @@ TEH_PG_get_refresh (void *cls, GNUNET_PQ_cleanup_result (rs); return GNUNET_DB_STATUS_HARD_ERROR; } + if (no_transfer_pubs) + { + refresh->is_v27_refresh = true; + refresh->transfer_pubs = NULL; + } + else + { + if (num_transfer_pubs != refresh->num_coins) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "got inconsistent number of transfer_pubs in refresh from DB: " + "num_coins=%ld, num_transfer_pubs=%ld\n", + refresh->num_coins, + num_transfer_pubs); + GNUNET_PQ_cleanup_result (rs); + return GNUNET_DB_STATUS_HARD_ERROR; + } + refresh->is_v27_refresh = false; + refresh->transfer_pubs = transfer_pubs; + } if (refresh->no_blinding_seed != no_cs_r_values) { GNUNET_break (0); @@ -164,6 +199,7 @@ TEH_PG_get_refresh (void *cls, refresh->denom_sigs = denom_sigs; refresh->denom_serials = denom_serials; refresh->cs_r_values = cs_r_values; + transfer_pubs = NULL; denom_sigs = NULL; denom_serials = NULL; cs_r_values = NULL; diff --git a/src/exchangedb/pg_get_refresh.h b/src/exchangedb/pg_get_refresh.h @@ -37,6 +37,6 @@ enum GNUNET_DB_QueryStatus TEH_PG_get_refresh (void *cls, const struct TALER_RefreshCommitmentP *rc, - struct TALER_EXCHANGEDB_Refresh_v27 *refresh); + struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *refresh); #endif diff --git a/src/exchangedb/pg_get_refresh_reveal.c b/src/exchangedb/pg_get_refresh_reveal.c @@ -1,213 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2022 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file exchangedb/pg_get_refresh_reveal.c - * @brief Implementation of the get_refresh_reveal function for Postgres - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler/taler_error_codes.h" -#include "taler/taler_dbevents.h" -#include "taler/taler_pq_lib.h" -#include "pg_get_refresh_reveal.h" -#include "pg_helper.h" - - -/** - * Context where we aggregate data from the database. - * Closure for #add_revealed_coins(). - */ -struct GetRevealContext -{ - /** - * Array of revealed coins we obtained from the DB. - */ - struct TALER_EXCHANGEDB_RefreshRevealedCoin *rrcs; - - /** - * Length of the @a rrcs array. - */ - unsigned int rrcs_len; - - /** - * Set to an error code if we ran into trouble. - */ - enum GNUNET_DB_QueryStatus qs; -}; - - -/** - * Function to be called with the results of a SELECT statement - * that has returned @a num_results results. - * - * @param cls closure of type `struct GetRevealContext` - * @param result the postgres result - * @param num_results the number of results in @a result - */ -static void -add_revealed_coins (void *cls, - PGresult *result, - unsigned int num_results) -{ - struct GetRevealContext *grctx = cls; - - if (0 == num_results) - return; - grctx->rrcs = GNUNET_new_array (num_results, - struct TALER_EXCHANGEDB_RefreshRevealedCoin); - grctx->rrcs_len = num_results; - for (unsigned int i = 0; i < num_results; i++) - { - uint32_t off; - struct GNUNET_PQ_ResultSpec rso[] = { - GNUNET_PQ_result_spec_uint32 ("freshcoin_index", - &off), - GNUNET_PQ_result_spec_end - }; - - if (GNUNET_OK != - GNUNET_PQ_extract_result (result, - rso, - i)) - { - GNUNET_break (0); - grctx->qs = GNUNET_DB_STATUS_HARD_ERROR; - return; - } - if (off >= num_results) - { - GNUNET_break (0); - grctx->qs = GNUNET_DB_STATUS_HARD_ERROR; - return; - } - { - struct TALER_EXCHANGEDB_RefreshRevealedCoin *rrc = &grctx->rrcs[off]; - struct GNUNET_PQ_ResultSpec rsi[] = { - /* NOTE: freshcoin_index selected and discarded here... */ - GNUNET_PQ_result_spec_auto_from_type ("denom_pub_hash", - &rrc->h_denom_pub), - GNUNET_PQ_result_spec_auto_from_type ("link_sig", - &rrc->orig_coin_link_sig), - GNUNET_PQ_result_spec_auto_from_type ("h_coin_ev", - &rrc->coin_envelope_hash), - TALER_PQ_result_spec_blinded_planchet ("coin_ev", - &rrc->blinded_planchet), - TALER_PQ_result_spec_exchange_withdraw_values ("ewv", - &rrc->exchange_vals), - TALER_PQ_result_spec_blinded_denom_sig ("ev_sig", - &rrc->coin_sig), - GNUNET_PQ_result_spec_end - }; - - if (NULL != - rrc->blinded_planchet.blinded_message) - { - /* duplicate offset, not allowed */ - GNUNET_break (0); - grctx->qs = GNUNET_DB_STATUS_HARD_ERROR; - return; - } - if (GNUNET_OK != - GNUNET_PQ_extract_result (result, - rsi, - i)) - { - GNUNET_break (0); - grctx->qs = GNUNET_DB_STATUS_HARD_ERROR; - return; - } - } - } -} - - -enum GNUNET_DB_QueryStatus -TEH_PG_get_refresh_reveal (void *cls, - const struct TALER_RefreshCommitmentP *rc, - TALER_EXCHANGEDB_RefreshCallback cb, - void *cb_cls) -{ - struct PostgresClosure *pg = cls; - struct GetRevealContext grctx; - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (rc), - GNUNET_PQ_query_param_end - }; - - memset (&grctx, - 0, - sizeof (grctx)); - - /* Obtain information about the coins created in a refresh - operation, used in #postgres_get_refresh_reveal() */ - PREPARE (pg, - "get_refresh_revealed_coins", - "SELECT " - " rrc.freshcoin_index" - ",denom.denom_pub_hash" - ",rrc.h_coin_ev" - ",rrc.link_sig" - ",rrc.coin_ev" - ",rrc.ewv" - ",rrc.ev_sig" - " FROM refresh_commitments" - " JOIN refresh_revealed_coins rrc" - " USING (melt_serial_id)" - " JOIN denominations denom " - " USING (denominations_serial)" - " WHERE rc=$1;"); - qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "get_refresh_revealed_coins", - params, - &add_revealed_coins, - &grctx); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - goto cleanup; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - default: /* can have more than one result */ - break; - } - switch (grctx.qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - goto cleanup; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: /* should be impossible */ - break; - } - - /* Pass result back to application */ - cb (cb_cls, - grctx.rrcs_len, - grctx.rrcs); -cleanup: - for (unsigned int i = 0; i < grctx.rrcs_len; i++) - { - struct TALER_EXCHANGEDB_RefreshRevealedCoin *rrc = &grctx.rrcs[i]; - - TALER_blinded_denom_sig_free (&rrc->coin_sig); - TALER_blinded_planchet_free (&rrc->blinded_planchet); - TALER_denom_ewv_free (&rrc->exchange_vals); - } - GNUNET_free (grctx.rrcs); - return qs; -} diff --git a/src/exchangedb/pg_get_refresh_reveal.h b/src/exchangedb/pg_get_refresh_reveal.h @@ -1,44 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2022 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file exchangedb/pg_get_refresh_reveal.h - * @brief implementation of the get_refresh_reveal function for Postgres - * @author Christian Grothoff - */ -#ifndef PG_GET_REFRESH_REVEAL_H -#define PG_GET_REFRESH_REVEAL_H - -#include "taler/taler_util.h" -#include "taler/taler_json_lib.h" -#include "taler/taler_exchangedb_plugin.h" - -/** - * Lookup in the database the coins that we want to - * create in the given refresh operation. - * - * @param cls the `struct PostgresClosure` with the plugin-specific state - * @param rc identify commitment and thus refresh operation - * @param cb function to call with the results - * @param cb_cls closure for @a cb - * @return transaction status - */ -enum GNUNET_DB_QueryStatus -TEH_PG_get_refresh_reveal (void *cls, - const struct TALER_RefreshCommitmentP *rc, - TALER_EXCHANGEDB_RefreshCallback cb, - void *cb_cls); - -#endif diff --git a/src/exchangedb/pg_mark_refresh_reveal_success.c b/src/exchangedb/pg_mark_refresh_reveal_success.c @@ -0,0 +1,49 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file exchangedb/pg_mark_refresh_reveal_success.c + * @brief Implementation of the mark_refresh_reveal_success function for Postgres + * @author Özgür Kesim + */ +#include "taler/platform.h" +#include "taler/taler_error_codes.h" +#include "taler/taler_dbevents.h" +#include "taler/taler_pq_lib.h" +#include "pg_mark_refresh_reveal_success.h" +#include "pg_helper.h" + +enum GNUNET_DB_QueryStatus +TEH_PG_mark_refresh_reveal_success ( + void *cls, + const struct TALER_RefreshCommitmentP *rc) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (rc), + GNUNET_PQ_query_param_end + }; + + PREPARE (pg, + "mark_refresh_reveal_success", + "UPDATE refresh" + " SET revealed=true" + " WHERE rc = $1"); + + return GNUNET_PQ_eval_prepared_non_select ( + pg->conn, + "mark_refresh_reveal_success", + params); +} +\ No newline at end of file diff --git a/src/exchangedb/pg_mark_refresh_reveal_success.h b/src/exchangedb/pg_mark_refresh_reveal_success.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file exchangedb/pg_mark_refresh_reveal_success.h + * @brief Mark the successful reveal of a refresh in the database + * @author Özgür Kesim + */ +#ifndef PG_MARK_REFRESH_REVEAL_SUCCESS_H +#define PG_MARK_REFRESH_REVEAL_SUCCESS_H + +#include "taler/taler_util.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_exchangedb_plugin.h" + + +/** + * Mark a refresh, given by the commitment, as successfully revealed. + * + * @param cls closure + * @param rc commitment for the refresh + * @return transaction status code + */ +enum GNUNET_DB_QueryStatus +TEH_PG_mark_refresh_reveal_success ( + void *cls, + const struct TALER_RefreshCommitmentP *rc); + +#endif diff --git a/src/exchangedb/plugin_exchangedb_common.c b/src/exchangedb/plugin_exchangedb_common.c @@ -134,7 +134,7 @@ TEH_COMMON_free_coin_transaction_list ( case TALER_EXCHANGEDB_TT_MELT: GNUNET_free (tl->details.melt); break; - case TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP: + case TALER_EXCHANGEDB_TT_RECOUP_REFRESH_RECEIVER: { struct TALER_EXCHANGEDB_RecoupRefreshListEntry *rr; @@ -146,7 +146,7 @@ TEH_COMMON_free_coin_transaction_list ( case TALER_EXCHANGEDB_TT_REFUND: GNUNET_free (tl->details.refund); break; - case TALER_EXCHANGEDB_TT_RECOUP: + case TALER_EXCHANGEDB_TT_RECOUP_WITHDRAW: GNUNET_free (tl->details.recoup); break; case TALER_EXCHANGEDB_TT_RECOUP_REFRESH: diff --git a/src/exchangedb/plugin_exchangedb_postgres.c b/src/exchangedb/plugin_exchangedb_postgres.c @@ -48,7 +48,6 @@ #include "pg_do_withdraw.h" #include "pg_do_check_deposit_idempotent.h" #include "pg_do_deposit.h" -#include "pg_do_melt.h" #include "pg_do_purse_delete.h" #include "pg_do_purse_deposit.h" #include "pg_do_purse_merge.h" @@ -88,7 +87,6 @@ #include "pg_get_purse_request.h" #include "pg_get_ready_deposit.h" #include "pg_get_refresh.h" -#include "pg_get_refresh_reveal.h" #include "pg_get_reserve_balance.h" #include "pg_get_reserve_by_h_planchets.h" #include "pg_get_reserve_history.h" @@ -159,6 +157,7 @@ #include "pg_lookup_wire_fee_by_time.h" #include "pg_lookup_wire_timestamp.h" #include "pg_lookup_wire_transfer.h" +#include "pg_mark_refresh_reveal_success.h" #include "pg_persist_kyc_attributes.h" #include "pg_persist_policy_details.h" #include "pg_preflight.h" @@ -558,6 +557,8 @@ libtaler_plugin_exchangedb_postgres_init (void *cls) = &TEH_PG_add_policy_fulfillment_proof; plugin->do_refresh = &TEH_PG_do_refresh; + plugin->mark_refresh_reveal_success + = &TEH_PG_mark_refresh_reveal_success; plugin->do_refund = &TEH_PG_do_refund; plugin->do_recoup diff --git a/src/include/taler/taler_crypto_lib.h b/src/include/taler/taler_crypto_lib.h @@ -670,15 +670,43 @@ struct TALER_AmlOfficerSignatureP struct GNUNET_CRYPTO_EddsaSignature eddsa_signature; }; +/** + * @since vDOLDPLUS + * + * The seed from which a specific batch of fresh coin material is + * derived from #TALER_PublicRefreshMasterSeedP for a refresh request. + */ +struct TALER_PrivateRefreshBatchSeedP +{ + /** + * The seed is a hash code. + */ + struct GNUNET_HashCode batch_seed; +}; + +/** + * @since vDOLDPLUS + * + * From a #TALER_PublicRefreshMasterSeedP, we derive first + * kappa many seeds for batches of n coins in the struct + * #TALER_KappaPrivateRefreshBatchSeedsP. Each batch seed is + * used to derive n individual fresh coin master secrets. + */ +struct TALER_KappaPrivateRefreshBatchSeedsP +{ + /** + * A tuple of #TALER_CNC_KAPPA many batch seeds. + */ + struct TALER_PrivateRefreshBatchSeedP tuple[TALER_CNC_KAPPA]; +}; /** - * The public master seed from which public nonces are derived - * for new fresh coin candidates. The seed (and its derived nonces) - * itself are public and are used to proof ownership of the old coin - * by signing a message derived from those seeds. The signature - * from that operation is then used as seed for a fresh coin. - * See https://docs.taler.net/design-documents/062-pq-refresh.html - * for more details. + * @since vDOLDPLUS + * + * The public master seed from which private coin seeds are derived + * for new fresh coin candidates, using the old coins private key. + * + * Note that this value has changed its meaning from v27. */ struct TALER_PublicRefreshMasterSeedP { @@ -689,6 +717,9 @@ struct TALER_PublicRefreshMasterSeedP }; /** + * @since v27 + * @deprecated after vDOLDPLUS + * * A batch seed is signed by the old coin's private key * and from that signature n fresh coin's secrets are derived. */ @@ -701,6 +732,9 @@ struct TALER_PublicRefreshNonceP }; /** + * @since v27 + * @deprecated after vDOLDPLUS + * * From a #TALER_PublicRefreshMasterSeedP, we derive first * kappa many seeds for batches of n coins in the struct * #TALER_KappaPublicRefreshNoncesP. @@ -848,20 +882,6 @@ struct TALER_PlanchetMasterSecretP /** * Master key material for the deriviation of - * private coins and blinding factors. - */ -struct TALER_RefreshMasterSecretP -{ - - /** - * Key material. - */ - uint32_t key_data[8]; - -}; - -/** - * Master key material for the deriviation of * blinding factors. */ struct TALER_BlindingMasterSeedP @@ -1722,11 +1742,13 @@ TALER_cs_withdraw_seed_to_blinding_seed ( * @brief Method to derive a seed for blinding from a seed for refresh * * @param seed input refresh seed + * @param coin_priv the private key of the old coin * @param[out] blinding_seed derived blinding seed */ void -TALER_cs_refresh_secret_to_blinding_seed ( - const struct TALER_RefreshMasterSecretP *seed, +TALER_cs_refresh_seed_to_blinding_seed ( + const struct TALER_PublicRefreshMasterSeedP *seed, + const struct TALER_CoinSpendPrivateKeyP *coin_priv, struct TALER_BlindingMasterSeedP *blinding_seed); @@ -1763,6 +1785,7 @@ TALER_cs_derive_nonces_from_seed ( const uint32_t indices[static num], struct GNUNET_CRYPTO_CsSessionNonce nonces[static num]); + /** * @brief method to find the indices of denominations for CS * @@ -1815,21 +1838,6 @@ TALER_cs_derive_only_cs_blind_nonces_from_seed ( /** - * @brief Method to derive /csr nonce - * to be used during refresh/melt operation. - * - * @param rms secret input for the refresh operation - * @param idx index of the fresh coin - * @param[out] nonce set to nonce included in the request to generate R_0 and R_1 - */ -void -TALER_cs_refresh_nonce_derive ( - const struct TALER_RefreshMasterSecretP *rms, - uint32_t idx, - struct GNUNET_CRYPTO_CsSessionNonce *nonce); - - -/** * Initialize denomination public-private key pair. * * For #GNUNET_CRYPTO_BSA_RSA, an additional "unsigned int" @@ -2349,6 +2357,9 @@ GNUNET_NETWORK_STRUCT_END /** + * @since v27 + * @deprecated after vDOLDPLUS + * * The signature of the old coin over a public nonce, * provided during the /reveal-melt operation as * a) proof of ownership of the old coin @@ -2365,6 +2376,9 @@ struct TALER_PrivateRefreshNonceSignatureP }; /** + * @since v27 + * @deprecated after vDOLDPLUS + * * Sign a public nonce with the old coin, to prove * coin ownership, with purpose * TALER_SIGNATURE_WALLET_COIN_LINK @@ -2387,6 +2401,9 @@ TALER_wallet_refresh_nonce_sign ( /** + * @since v27 + * @deprecated after vDOLDPLUS + * * Verify the signature on a public nonce, provided by the old coin, * to prove coin ownership during a melt/refresh operation. * @@ -2409,6 +2426,9 @@ TALER_wallet_refresh_nonce_verify ( /** + * @since v27 + * @deprecated after vDOLDPLUS + * * From a given signature for a refresh nonce, * derive a fresh master secret for the planchet * of a fresh coin. @@ -2418,7 +2438,7 @@ TALER_wallet_refresh_nonce_verify ( * @param[out] secrets the master secrets for @a num fresh coins */ void -TALER_refresh_signature_to_secrets ( +TALER_refresh_signature_to_secrets_v27 ( const struct TALER_PrivateRefreshNonceSignatureP *sig, size_t num_secrets, struct TALER_PlanchetMasterSecretP secrets[static num_secrets]); @@ -2442,25 +2462,6 @@ TALER_transfer_secret_to_planchet_secret ( /** - * Derive the @a coin_num transfer private key @a tpriv from a refresh from - * the @a rms seed and the @a old_coin_pub of the refresh operation. The - * transfer private key derivation is based on the @a ps with a KDF salted by - * the @a coin_num. - * - * @param rms seed to use for KDF to derive transfer keys - * @param old_coin_priv private key of the old coin - * @param cnc_num cut and choose number to include in KDF - * @param[out] tpriv value to initialize - */ -void -TALER_planchet_secret_to_transfer_priv ( - const struct TALER_RefreshMasterSecretP *rms, - const struct TALER_CoinSpendPrivateKeyP *old_coin_priv, - uint32_t cnc_num, - struct TALER_TransferPrivateKeyP *tpriv); - - -/** * Setup secret information for fresh a coin to be * withdrawn. * @@ -2518,19 +2519,118 @@ TALER_withdraw_expand_secrets ( */ void TALER_refresh_master_setup_random ( - struct TALER_RefreshMasterSecretP *rms); + struct TALER_PublicRefreshMasterSeedP *rms); /** - * Derive a master refresh seed from the refresh master secret + * @since vDOLDPLUS + * + * Expand a master refresh seed using the old coins private + * into kappa many batch seeds, key each of which expands into + * private refresh seeds for n coins. + * + * @param refresh_master_seed master seed to expand from + * @param coin_priv the old coin's private key to expand from. + * @param[out] kappa_refresh_seeds tuple of #TALER_CNC_KAPPA many batch nonces */ void -TALER_refresh_master_secret_to_refresh_seed ( - const struct TALER_RefreshMasterSecretP *rms, - struct TALER_PublicRefreshMasterSeedP *r_seed); +TALER_refresh_expand_seed_to_kappa_batch_seeds ( + const struct TALER_PublicRefreshMasterSeedP *refresh_master_seed, + const struct TALER_CoinSpendPrivateKeyP *coin_priv, + struct TALER_KappaPrivateRefreshBatchSeedsP *kappa_refresh_seeds); +/** + * @since vDOLDPLUS + * + * Expands a secret seed for a batch of coin candidates for refresh + * to the array of transfer private keys. + * + * @param batch_seed secret seed for a batch of coin candidates + * @param num_transfer_pks number of elements in @a transfer_pks + * @param[out] transfer_pks output array of transfer private keys + */ +void +TALER_refresh_expand_batch_seed_to_transfer_private_keys ( + const struct TALER_PrivateRefreshBatchSeedP *batch_seed, + size_t num_transfer_pks, + struct TALER_TransferPrivateKeyP transfer_pks[num_transfer_pks]); /** + * @since vDOLDPLUS + * + * Expands a secret seed for a batch of coin candidates for refresh + * to the array of transfer secrets. + * + * This is a simple helper function that calls under the hood + * TALER_refresh_expand_batch_seed_to_transfer_private_keys + * and TALER_link_reveal_transfer_secret. + * + * @param batch_seed secret seed for a batch of coin candidates + * @param coin_pub the old coin's public key + * @param num_transfer_secrets number of elements in @a planchet_secrets + * @param[out] transfer_secrets output array of transfer secrets + */ +void +TALER_refresh_expand_batch_seed_to_transfer_secrets ( + const struct TALER_PrivateRefreshBatchSeedP *batch_seed, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + size_t num_transfer_secrets, + struct TALER_TransferSecretP transfer_secrets[num_transfer_secrets]); + + +/** + * @since vDOLDPLUS + * + * Expands a secret seed for a batch of coin candidates for refresh + * to the array of planchet master secrets. + * + * This is a simple helper function that calls under the hood + * TALER_refresh_expand_batch_seed_to_transfer_private_keys, + * TALER_link_reveal_transfer_secret and + * TALER_transfer_secret_to_planchet_secret. + * + * @param batch_seed secret seed for a batch of coin candidates + * @param coin_pub the old coin's public key + * @param num_planchet_secrets number of elements in @a planchet_secrets + * @param[out] planchet_secrets output array of planchet master secrets + */ +void +TALER_refresh_expand_batch_seed_to_planchet_master_secrets ( + const struct TALER_PrivateRefreshBatchSeedP *batch_seed, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + size_t num_planchet_secrets, + struct TALER_PlanchetMasterSecretP planchet_secrets[num_planchet_secrets]); + +/** + * @since vDOLDPLUS + * + * Expands a secret seed for a batch of coin candidates for refresh + * to the array of planchet secrets and the array of transfer public keys. + * + * This is a simple helper function that calls under the hood + * TALER_refresh_expand_batch_seed_to_transfer_private_keys, + * TALER_link_reveal_transfer_secret and + * TALER_transfer_secret_to_planchet_secret + * and the public key generation. + * + * @param batch_seed secret seed for a batch of coin candidates + * @param coin_pub the old coin's public key + * @param num number of elements in @a transfer_secrets and @a transfer_pubs + * @param[out] planchet_secrets output array of transfer planchet_secrets + * @param[out] transfer_pubs output array of transfer public keys + */ +void +TALER_refresh_expand_batch_seed_to_transfer_data ( + const struct TALER_PrivateRefreshBatchSeedP *batch_seed, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + size_t num, + struct TALER_PlanchetMasterSecretP planchet_secrets[num], + struct TALER_TransferPublicKeyP transfer_pubs[num]); + +/** + * @since v27 + * @deprecated after vDOLDPLUS + * * Expand a refresh seed into kappa many batch seeds, each * of which expands into nonces for n coins. * @@ -2538,7 +2638,7 @@ TALER_refresh_master_secret_to_refresh_seed ( * @param[out] kappa_nonces tuple of #TALER_CNC_KAPPA many batch nonces */ void -TALER_refresh_expand_kappa_nonces ( +TALER_refresh_expand_kappa_nonces_v27 ( const struct TALER_PublicRefreshMasterSeedP *refresh_seed, struct TALER_KappaPublicRefreshNoncesP *kappa_nonces); @@ -2727,29 +2827,6 @@ struct TALER_RefreshCommitmentEntry /** - * Compute the commitment for a /refresh/melt operation from - * the respective public inputs. - * - * @param[out] rc set to the value the wallet must commit to - * @param kappa number of transfer public keys involved (must be #TALER_CNC_KAPPA) - * @param rms refresh master secret to include, can be NULL! - * @param num_new_coins number of new coins to be created - * @param rcs array of @a kappa commitments - * @param coin_pub public key of the coin to be melted - * @param amount_with_fee amount to be melted, including fee - */ -void -TALER_refresh_get_commitment ( - struct TALER_RefreshCommitmentP *rc, - uint32_t kappa, - const struct TALER_RefreshMasterSecretP *rms, - uint32_t num_new_coins, - const struct TALER_RefreshCommitmentEntry *rcs, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_Amount *amount_with_fee); - - -/** * Helper struct to carry kappa many #TALER_BlindedCoinHashP. */ struct TALER_KappaHashBlindedPlanchetsP @@ -2758,28 +2835,82 @@ struct TALER_KappaHashBlindedPlanchetsP }; /** + * Helper struct to carry information about TALER_CNC_KAPPA + * many batches of transfer public keys + */ +struct TALER_KappaTransferPublicKeys +{ + /** + * Number of elements in each @e batch + */ + size_t num_transfer_pubs; + + /** + * Pointers to the individual batches of @e num_transfer_pubs + */ + const struct TALER_TransferPublicKeyP *batch[TALER_CNC_KAPPA]; +}; + + +/** + * @since vDOLDPLUS + * * Compute the commitment for a /melt operation from - * the respective public inputs. The endpoint is available starting - * with protocol version v27. + * the respective public inputs. * * @param[out] rc set to the value the wallet must commit to * @param refresh_seed refresh master seed to include * @param blinding_seed blinding master seed for CS denominations, might be NULL + * @param k_transfer_pubs TALER_CNC_KAPPA many batches of n transfer public keys, * @param k_bps_h TALER_CNC_KAPPA many hashes of blinded coin envelopes, - * one for each batch of n coin candidates. - * Note that these were over planchets and denominations. + * one for each batch of n coin candidates. + * Note that these were over planchets and denominations. * @param coin_pub public key of the coin to be melted * @param amount_with_fee amount to be melted, including fee */ void -TALER_refresh_get_commitment_v27 ( +TALER_refresh_get_commitment ( struct TALER_RefreshCommitmentP *rc, const struct TALER_PublicRefreshMasterSeedP *refresh_seed, const struct TALER_BlindingMasterSeedP *blinding_seed, + const struct TALER_KappaTransferPublicKeys *k_transfer_pubs, const struct TALER_KappaHashBlindedPlanchetsP *k_bps_h, const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_Amount *amount_with_fee); +/** + * @since v27 + * @deprecated vDOLDPLUS + * + * Note: this function is only relevant for the exchange as it has to + * support clients that still use v27 of the refresh protocol. + * + * Compute the commitment for a /melt operation from + * the respective public inputs, for refreshes of v27. + * + * @param[out] rc set to the value the wallet must commit to + * @param refresh_seed refresh master seed to include + * @param blinding_seed blinding master seed for CS denominations, might be NULL + * @param k_bps_h TALER_CNC_KAPPA many hashes of blinded coin envelopes, + * one for each batch of n coin candidates. + * Note that these were over planchets and denominations. + * @param coin_pub public key of the coin to be melted + * @param amount_with_fee amount to be melted, including fee + */ +#define TALER_refresh_get_commitment_v27(rc, \ + refresh_seed, \ + blinding_seed, \ + k_bps_h, \ + coin_pub, \ + amount_with_fee) \ + TALER_refresh_get_commitment ((rc), \ + (refresh_seed), \ + (blinding_seed), \ + NULL, \ + (k_bps_h), \ + (coin_pub), \ + (amount_with_fee)) + /** * Encrypt contract for transmission to a party that will @@ -4598,7 +4729,7 @@ TALER_wallet_withdraw_sign ( NULL, \ 0, \ (reserve_priv), \ - (reserve_sig)); + (reserve_sig)) /** @@ -4651,7 +4782,7 @@ TALER_wallet_withdraw_verify ( NULL, \ 0, \ (reserve_pub), \ - (reserve_sig)); + (reserve_sig)) /** diff --git a/src/include/taler/taler_exchange_service.h b/src/include/taler/taler_exchange_service.h @@ -2987,10 +2987,10 @@ struct TALER_EXCHANGE_MeltInput struct TALER_EXCHANGE_RevealMeltInput { /** - * The master secret for the melt/reveal, from which all other - * secret material is derived from + * The public master seed for the melt/reveal, from which all other + * secret material is derived from, using the old coin's private key. */ - const struct TALER_RefreshMasterSecretP *rms; + const struct TALER_PublicRefreshMasterSeedP *rms; /** * The input data from the prior call to /melt @@ -3064,13 +3064,13 @@ TALER_EXCHANGE_reveal_melt_cancel ( /** * @brief A /melt Handle */ -struct TALER_EXCHANGE_MeltHandle_v27; +struct TALER_EXCHANGE_MeltHandle; /** * Response returned to a /melt request. */ -struct TALER_EXCHANGE_MeltResponse_v27 +struct TALER_EXCHANGE_MeltResponse { /** * Full HTTP response details. @@ -3129,9 +3129,9 @@ struct TALER_EXCHANGE_MeltResponse_v27 * @param mr response details */ typedef void -(*TALER_EXCHANGE_MeltCallback_v27) ( +(*TALER_EXCHANGE_MeltCallback) ( void *cls, - const struct TALER_EXCHANGE_MeltResponse_v27 *mr); + const struct TALER_EXCHANGE_MeltResponse *mr); /** * Submit a melt request to the exchange and get the exchange's @@ -3152,14 +3152,14 @@ typedef void * @return a handle for this request; NULL if the argument was invalid. * In this case, neither callback will be called. */ -struct TALER_EXCHANGE_MeltHandle_v27 * -TALER_EXCHANGE_melt_v27 ( +struct TALER_EXCHANGE_MeltHandle * +TALER_EXCHANGE_melt ( struct GNUNET_CURL_Context *ctx, const char *url, struct TALER_EXCHANGE_Keys *keys, - const struct TALER_RefreshMasterSecretP *rms, + const struct TALER_PublicRefreshMasterSeedP *rms, const struct TALER_EXCHANGE_MeltInput *melt_input, - TALER_EXCHANGE_MeltCallback_v27 melt_cb, + TALER_EXCHANGE_MeltCallback melt_cb, void *melt_cb_cls); @@ -3170,7 +3170,7 @@ TALER_EXCHANGE_melt_v27 ( * @param mh the melt handle */ void -TALER_EXCHANGE_melt_v27_cancel (struct TALER_EXCHANGE_MeltHandle_v27 *mh); +TALER_EXCHANGE_melt_cancel (struct TALER_EXCHANGE_MeltHandle *mh); /* ********************* /transfers/$WTID *********************** */ @@ -3621,7 +3621,7 @@ typedef void * @param pk kind of coin to pay back * @param denom_sig signature over the coin by the exchange using @a pk * @param exchange_vals contribution from the exchange on the withdraw - * @param rms melt secret of the refreshing operation + * @param rms the public seed of the refreshing operation * @param ps coin-specific secrets derived for this coin during the refreshing operation * @param idx index of the fresh coin in the refresh operation that is now being recouped * @param recoup_cb the callback to call when the final result for this request is available @@ -3638,7 +3638,7 @@ TALER_EXCHANGE_recoup_refresh ( const struct TALER_EXCHANGE_DenomPublicKey *pk, const struct TALER_DenominationSignature *denom_sig, const struct TALER_ExchangeBlindingValues *exchange_vals, - const struct TALER_RefreshMasterSecretP *rms, + const struct TALER_PublicRefreshMasterSeedP *rms, const struct TALER_PlanchetMasterSecretP *ps, unsigned int idx, TALER_EXCHANGE_RecoupRefreshResultCallback recoup_cb, diff --git a/src/include/taler/taler_exchangedb_plugin.h b/src/include/taler/taler_exchangedb_plugin.h @@ -2511,10 +2511,10 @@ struct TALER_EXCHANGEDB_Melt /** - * Information about a melt operation since v27 of the protocol. + * Information about a melt operation since vDOLDPLUS of the protocol. * This also includes the information for the reveal phase. */ -struct TALER_EXCHANGEDB_Refresh_v27 +struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS { /** * Information about the coin that is being melted. @@ -2532,8 +2532,23 @@ struct TALER_EXCHANGEDB_Refresh_v27 struct TALER_RefreshCommitmentP rc; /** - * Public nonce from which the refresh nonces per coin candidate were - * derived from + * True if the client has successfully performed the reveal part + * of the refresh protocol, after the melt. + */ + bool is_revealed; + + /** + * @since vDOLDPLUS + * Mark if we have a v27 Refresh object. + * That is, the @a refresh_seed refers to the vDOLDPLUS master_refresh_seed + * from the original request, AND the client has provided transfer public keys, + * see below, @a transfer_public_keys + */ + bool is_v27_refresh; + + /** + * Public seed from which the refresh nonces (v27) or transfer secrets (vDOLDPLUS) + * per coin candidate were derived from. */ struct TALER_PublicRefreshMasterSeedP refresh_seed; @@ -2569,6 +2584,13 @@ struct TALER_EXCHANGEDB_Refresh_v27 struct TALER_BlindedDenominationSignature *denom_sigs; /** + * If @a is_v27_refresh is false, the client performed a vDOLDPLUS refresh, + * and has provided @a num_coins * kappa transfer public keys. + * This is the chosen (at index @a noreveal_index) array of @a num_coins transfer public keys. + */ + struct TALER_TransferPublicKeyP *transfer_pubs; + + /** * Array of @a num_coins serial id's of the denominations. * If @e coin.no_age_commitment is false, the denominations * MUST support age restriction. @@ -2582,6 +2604,11 @@ struct TALER_EXCHANGEDB_Refresh_v27 uint32_t noreveal_index; /** + * True, if the client has successfully performed the reveal step + */ + bool revealed; + + /** * If true, no @e blinding_seed is set and @e num_cs_r_values is 0. */ bool no_blinding_seed; @@ -2695,12 +2722,12 @@ enum TALER_EXCHANGEDB_TransactionType /** * Recoup-refresh operation (on the old coin, adding to the old coin's value) */ - TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP = 3, + TALER_EXCHANGEDB_TT_RECOUP_REFRESH_RECEIVER = 3, /** * Recoup operation. */ - TALER_EXCHANGEDB_TT_RECOUP = 4, + TALER_EXCHANGEDB_TT_RECOUP_WITHDRAW = 4, /** * Recoup-refresh operation (on the new coin, eliminating its value) @@ -2778,13 +2805,13 @@ struct TALER_EXCHANGEDB_TransactionList /** * Details if transaction was a recoup-refund operation where * this coin was the OLD coin. - * (#TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP). + * (#TALER_EXCHANGEDB_TT_RECOUP_REFRESH_RECEIVER). */ struct TALER_EXCHANGEDB_RecoupRefreshListEntry *old_coin_recoup; /** * Details if transaction was a recoup operation. - * (#TALER_EXCHANGEDB_TT_RECOUP) + * (#TALER_EXCHANGEDB_TT_RECOUP_WITHDRAW) */ struct TALER_EXCHANGEDB_RecoupListEntry *recoup; @@ -3095,47 +3122,6 @@ typedef enum GNUNET_GenericReturnValue /** - * Information about a coin that was revealed to the exchange - * during reveal. - */ -struct TALER_EXCHANGEDB_RefreshRevealedCoin -{ - /** - * Hash of the public denomination key of the coin. - */ - struct TALER_DenominationHashP h_denom_pub; - - /** - * Signature of the original coin being refreshed over the - * link data, of type #TALER_SIGNATURE_WALLET_COIN_LINK - */ - struct TALER_CoinSpendSignatureP orig_coin_link_sig; - - /** - * Hash of the blinded new coin, that is @e coin_ev. - */ - struct TALER_BlindedCoinHashP coin_envelope_hash; - - /** - * Signature generated by the exchange over the coin (in blinded format). - */ - struct TALER_BlindedDenominationSignature coin_sig; - - /** - * Values contributed from the exchange to the - * coin generation (see /blinding-prepare). - */ - struct TALER_ExchangeBlindingValues exchange_vals; - - /** - * Blinded message to be signed (in envelope). - */ - struct TALER_BlindedPlanchet blinded_planchet; - -}; - - -/** * Information per Clause-Schnorr (CS) fresh coin to * be persisted for idempotency during refreshes-reveal. */ @@ -3225,20 +3211,6 @@ typedef enum GNUNET_GenericReturnValue /** - * Function called with information about a refresh order. - * - * @param cls closure - * @param num_freshcoins size of the @a rrcs array - * @param rrcs array of @a num_freshcoins information about coins to be created - */ -typedef void -(*TALER_EXCHANGEDB_RefreshCallback)( - void *cls, - uint32_t num_freshcoins, - const struct TALER_EXCHANGEDB_RefreshRevealedCoin *rrcs); - - -/** * Function called with details about coins that were refunding, * with the goal of auditing the refund's execution. * @@ -4524,7 +4496,7 @@ struct TALER_EXCHANGEDB_Plugin enum GNUNET_DB_QueryStatus (*do_refresh)( void *cls, - struct TALER_EXCHANGEDB_Refresh_v27 *refresh, + struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *refresh, const struct GNUNET_TIME_Timestamp *timestamp, bool *found, uint32_t *noreveal_index, @@ -4547,8 +4519,21 @@ struct TALER_EXCHANGEDB_Plugin enum GNUNET_DB_QueryStatus (*get_refresh)(void *cls, const struct TALER_RefreshCommitmentP *rc, - struct TALER_EXCHANGEDB_Refresh_v27 *refresh); + struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *refresh); + /** + * @since vDOLDPLUS + * + * Mark the successful reveal for a refresh in the database. + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param rc commitment to use for the lookup + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*mark_refresh_reveal_success)( + void *cls, + const struct TALER_RefreshCommitmentP *rc); /** * Add a proof of fulfillment of an policy diff --git a/src/include/taler/taler_json_lib.h b/src/include/taler/taler_json_lib.h @@ -41,9 +41,9 @@ * @deprecated */ #define TALER_json_warn(error) \ - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, \ - "JSON parsing failed at %s:%u: %s (%s)\n", \ - __FILE__, __LINE__, error.text, error.source) + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, \ + "JSON parsing failed at %s:%u: %s (%s)\n", \ + __FILE__, __LINE__, error.text, error.source) /** @@ -67,8 +67,8 @@ TALER_JSON_pack_time_abs_human ( * @param ec error code to encode using canonical field names */ #define TALER_JSON_pack_ec(ec) \ - GNUNET_JSON_pack_string ("hint", TALER_ErrorCode_get_hint (ec)), \ - GNUNET_JSON_pack_uint64 ("code", ec) + GNUNET_JSON_pack_string ("hint", TALER_ErrorCode_get_hint (ec)), \ + GNUNET_JSON_pack_uint64 ("code", ec) /** @@ -341,6 +341,22 @@ TALER_JSON_pack_array_of_data ( const void *data, size_t size); +/** + * Generate packer for an array of data of the same size, + * encoded in Crockford base32-encoding. + * The size is taken via sizeof(). + * + * @param name name of the field to add to the object + * @param num number of elements in the @a array + * @param data pointer to the list of elements + * @return json pack specification + */ +#define TALER_JSON_pack_array_of_data_auto(name,num,data) \ + TALER_JSON_pack_array_of_data ((name), \ + (num), \ + (data), \ + sizeof(*(data))) + /****************** Specs For Parsing JSON *********************/ @@ -481,10 +497,10 @@ TALER_JSON_spec_kycte (const char *name, * @param[out] dfs a `struct TALER_DenomFeeSet` to initialize */ #define TALER_JSON_SPEC_DENOM_FEES(pfx,currency,dfs) \ - TALER_JSON_spec_amount (pfx "_withdraw", (currency), &(dfs)->withdraw), \ - TALER_JSON_spec_amount (pfx "_deposit", (currency), &(dfs)->deposit), \ - TALER_JSON_spec_amount (pfx "_refresh", (currency), &(dfs)->refresh), \ - TALER_JSON_spec_amount (pfx "_refund", (currency), &(dfs)->refund) + TALER_JSON_spec_amount (pfx "_withdraw", (currency), &(dfs)->withdraw), \ + TALER_JSON_spec_amount (pfx "_deposit", (currency), &(dfs)->deposit), \ + TALER_JSON_spec_amount (pfx "_refresh", (currency), &(dfs)->refresh), \ + TALER_JSON_spec_amount (pfx "_refund", (currency), &(dfs)->refund) /** @@ -495,10 +511,10 @@ TALER_JSON_spec_kycte (const char *name, * @param dfs a `struct TALER_DenomFeeSet` to pack */ #define TALER_JSON_PACK_DENOM_FEES(pfx, dfs) \ - TALER_JSON_pack_amount (pfx "_withdraw", &(dfs)->withdraw), \ - TALER_JSON_pack_amount (pfx "_deposit", &(dfs)->deposit), \ - TALER_JSON_pack_amount (pfx "_refresh", &(dfs)->refresh), \ - TALER_JSON_pack_amount (pfx "_refund", &(dfs)->refund) + TALER_JSON_pack_amount (pfx "_withdraw", &(dfs)->withdraw), \ + TALER_JSON_pack_amount (pfx "_deposit", &(dfs)->deposit), \ + TALER_JSON_pack_amount (pfx "_refresh", &(dfs)->refresh), \ + TALER_JSON_pack_amount (pfx "_refund", &(dfs)->refund) /** @@ -508,9 +524,9 @@ TALER_JSON_spec_kycte (const char *name, * @param[out] gfs a `struct TALER_GlobalFeeSet` to initialize */ #define TALER_JSON_SPEC_GLOBAL_FEES(currency,gfs) \ - TALER_JSON_spec_amount ("history_fee", (currency), &(gfs)->history), \ - TALER_JSON_spec_amount ("account_fee", (currency), &(gfs)->account), \ - TALER_JSON_spec_amount ("purse_fee", (currency), &(gfs)->purse) + TALER_JSON_spec_amount ("history_fee", (currency), &(gfs)->history), \ + TALER_JSON_spec_amount ("account_fee", (currency), &(gfs)->account), \ + TALER_JSON_spec_amount ("purse_fee", (currency), &(gfs)->purse) /** * Macro to pack all of the global fees. @@ -518,9 +534,9 @@ TALER_JSON_spec_kycte (const char *name, * @param gfs a `struct TALER_GlobalFeeSet` to pack */ #define TALER_JSON_PACK_GLOBAL_FEES(gfs) \ - TALER_JSON_pack_amount ("history_fee", &(gfs)->history), \ - TALER_JSON_pack_amount ("account_fee", &(gfs)->account), \ - TALER_JSON_pack_amount ("purse_fee", &(gfs)->purse) + TALER_JSON_pack_amount ("history_fee", &(gfs)->history), \ + TALER_JSON_pack_amount ("account_fee", &(gfs)->account), \ + TALER_JSON_pack_amount ("purse_fee", &(gfs)->purse) /** diff --git a/src/include/taler/taler_testing_lib.h b/src/include/taler/taler_testing_lib.h @@ -2803,7 +2803,7 @@ TALER_TESTING_get_trait (const struct TALER_TESTING_Trait *traits, op (blinding_seed, const struct TALER_BlindingMasterSeedP) \ op (withdraw_commitment, const struct TALER_HashBlindedPlanchetsP) \ op (kappa_seed, const struct TALER_KappaWithdrawMasterSeedP) \ - op (refresh_secret, const struct TALER_RefreshMasterSecretP) \ + op (refresh_seed, const struct TALER_PublicRefreshMasterSeedP) \ op (reserve_pub, const struct TALER_ReservePublicKeyP) \ op (merchant_priv, const struct TALER_MerchantPrivateKeyP) \ op (merchant_pub, const struct TALER_MerchantPublicKeyP) \ diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am @@ -55,7 +55,7 @@ libtalerexchange_la_SOURCES = \ exchange_api_management_update_aml_officer.c \ exchange_api_management_wire_disable.c \ exchange_api_management_wire_enable.c \ - exchange_api_melt_v27.c \ + exchange_api_melt.c \ exchange_api_purse_create_with_deposit.c \ exchange_api_purse_create_with_merge.c \ exchange_api_purse_delete.c \ diff --git a/src/lib/exchange_api_melt.c b/src/lib/exchange_api_melt.c @@ -0,0 +1,645 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/exchange_api_melt.c + * @brief Implementation of the /melt request + * @author Özgür Kesim + */ +#include "taler/platform.h" +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP status codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_exchange_service.h" +#include "exchange_api_common.h" +#include "exchange_api_handle.h" +#include "taler/taler_signatures.h" +#include "exchange_api_curl_defaults.h" +#include "exchange_api_refresh_common.h" + + +/** + * @brief A /melt Handle + */ +struct TALER_EXCHANGE_MeltHandle +{ + + /** + * The keys of the this request handle will use + */ + struct TALER_EXCHANGE_Keys *keys; + + /** + * The url for this request. + */ + char *url; + + /** + * The exchange base url. + */ + char *exchange_url; + + /** + * Curl context. + */ + struct GNUNET_CURL_Context *cctx; + + /** + * Context for #TEH_curl_easy_post(). Keeps the data that must + * persist for Curl to make the upload. + */ + struct TALER_CURL_PostContext ctx; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with refresh melt failure results. + */ + TALER_EXCHANGE_MeltCallback melt_cb; + + /** + * Closure for @e result_cb and @e melt_failure_cb. + */ + void *melt_cb_cls; + + /** + * Actual information about the melt operation. + */ + struct MeltData md; + + /** + * The seed for the melt operation. + */ + struct TALER_PublicRefreshMasterSeedP rms; + + /** + * Details about the characteristics of the requested melt operation. + */ + const struct TALER_EXCHANGE_MeltInput *rd; + + /** + * True, if no blinding_seed is needed (no CS denominations involved) + */ + bool no_blinding_seed; + + /** + * If @e no_blinding_seed is false, the blinding seed for the intermediate + * call to /blinding-prepare, in order to retrieve the R-values from the + * exchange for the blind Clause-Schnorr signature. + */ + struct TALER_BlindingMasterSeedP blinding_seed; + + /** + * Array of `num_fresh_denom_pubs` per-coin values + * returned from melt operation. + */ + struct TALER_ExchangeBlindingValues *melt_blinding_values; + + /** + * Handle for the preflight request, or NULL. + */ + struct TALER_EXCHANGE_BlindingPrepareHandle *bpr; + + /** + * Public key of the coin being melted. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Signature affirming the melt. + */ + struct TALER_CoinSpendSignatureP coin_sig; + + /** + * @brief Public information about the coin's denomination key + */ + const struct TALER_EXCHANGE_DenomPublicKey *dki; + + /** + * Gamma value chosen by the exchange during melt. + */ + uint32_t noreveal_index; + +}; + + +/** + * Verify that the signature on the "200 OK" response + * from the exchange is valid. + * + * @param[in,out] mh melt handle + * @param json json reply with the signature + * @param[out] exchange_pub public key of the exchange used for the signature + * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR if not + */ +static enum GNUNET_GenericReturnValue +verify_melt_signature_ok (struct TALER_EXCHANGE_MeltHandle *mh, + const json_t *json, + struct TALER_ExchangePublicKeyP *exchange_pub) +{ + struct TALER_ExchangeSignatureP exchange_sig; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("exchange_sig", + &exchange_sig), + GNUNET_JSON_spec_fixed_auto ("exchange_pub", + exchange_pub), + GNUNET_JSON_spec_uint32 ("noreveal_index", + &mh->noreveal_index), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (json, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + /* check that exchange signing key is permitted */ + if (GNUNET_OK != + TALER_EXCHANGE_test_signing_key (mh->keys, + exchange_pub)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + /* check that noreveal index is in permitted range */ + if (TALER_CNC_KAPPA <= mh->noreveal_index) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + if (GNUNET_OK != + TALER_exchange_online_melt_confirmation_verify ( + &mh->md.rc, + mh->noreveal_index, + exchange_pub, + &exchange_sig)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the + * HTTP /melt request. + * + * @param cls the `struct TALER_EXCHANGE_MeltHandle` + * @param response_code HTTP response code, 0 on error + * @param response parsed JSON result, NULL on error + */ +static void +handle_melt_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_MeltHandle *mh = cls; + const json_t *j = response; + struct TALER_EXCHANGE_MeltResponse mr = { + .hr.reply = j, + .hr.http_status = (unsigned int) response_code + }; + + mh->job = NULL; + switch (response_code) + { + case 0: + mr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + case MHD_HTTP_OK: + if (GNUNET_OK != + verify_melt_signature_ok (mh, + j, + &mr.details.ok.sign_key)) + { + GNUNET_break_op (0); + mr.hr.http_status = 0; + mr.hr.ec = TALER_EC_EXCHANGE_MELT_INVALID_SIGNATURE_BY_EXCHANGE; + break; + } + mr.details.ok.noreveal_index = mh->noreveal_index; + mr.details.ok.num_melt_blinding_values = mh->rd->num_fresh_denom_pubs; + mr.details.ok.melt_blinding_values = mh->melt_blinding_values; + mr.details.ok.blinding_seed = mh->no_blinding_seed + ? NULL + : &mh->blinding_seed; + mh->melt_cb (mh->melt_cb_cls, + &mr); + mh->melt_cb = NULL; + break; + case MHD_HTTP_BAD_REQUEST: + /* This should never happen, either us or the exchange is buggy + (or API version conflict); just pass JSON reply to the application */ + mr.hr.ec = TALER_JSON_get_error_code (j); + mr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_CONFLICT: + mr.hr.ec = TALER_JSON_get_error_code (j); + mr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_FORBIDDEN: + /* Nothing really to verify, exchange says one of the signatures is + invalid; assuming we checked them, this should never happen, we + should pass the JSON reply to the application */ + mr.hr.ec = TALER_JSON_get_error_code (j); + mr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + mr.hr.ec = TALER_JSON_get_error_code (j); + mr.hr.hint = TALER_JSON_get_error_hint (j); + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + mr.hr.ec = TALER_JSON_get_error_code (j); + mr.hr.hint = TALER_JSON_get_error_hint (j); + break; + default: + /* unexpected response code */ + mr.hr.ec = TALER_JSON_get_error_code (j); + mr.hr.hint = TALER_JSON_get_error_hint (j); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange melt\n", + (unsigned int) response_code, + mr.hr.ec); + GNUNET_break_op (0); + break; + } + if (NULL != mh->melt_cb) + mh->melt_cb (mh->melt_cb_cls, + &mr); + TALER_EXCHANGE_melt_cancel (mh); +} + + +/** + * Start the actual melt operation, now that we have + * the exchange's input values. + * + * @param[in,out] mh melt operation to run + * @return #GNUNET_OK if we could start the operation + */ +static enum GNUNET_GenericReturnValue +start_melt (struct TALER_EXCHANGE_MeltHandle *mh) +{ + json_t *j_request_body; + json_t *j_transfer_pubs; + json_t *j_coin_evs; + CURL *eh; + struct TALER_DenominationHashP h_denom_pub; + + if (GNUNET_OK != + TALER_EXCHANGE_get_melt_data (&mh->rms, + mh->rd, + mh->no_blinding_seed + ? NULL + : &mh->blinding_seed, + mh->melt_blinding_values, + &mh->md)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + TALER_denom_pub_hash ( + &mh->md.melted_coin.pub_key, + &h_denom_pub); + TALER_wallet_melt_sign ( + &mh->md.melted_coin.melt_amount_with_fee, + &mh->md.melted_coin.fee_melt, + &mh->md.rc, + &h_denom_pub, + mh->md.melted_coin.h_age_commitment, + &mh->md.melted_coin.coin_priv, + &mh->coin_sig); + GNUNET_CRYPTO_eddsa_key_get_public ( + &mh->md.melted_coin.coin_priv.eddsa_priv, + &mh->coin_pub.eddsa_pub); + mh->dki = TALER_EXCHANGE_get_denomination_key ( + mh->keys, + &mh->md.melted_coin.pub_key); + j_request_body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("old_coin_pub", + &mh->coin_pub), + GNUNET_JSON_pack_data_auto ("old_denom_pub_h", + &h_denom_pub), + TALER_JSON_pack_denom_sig ("old_denom_sig", + &mh->md.melted_coin.sig), + GNUNET_JSON_pack_data_auto ("confirm_sig", + &mh->coin_sig), + TALER_JSON_pack_amount ("value_with_fee", + &mh->md.melted_coin.melt_amount_with_fee), + GNUNET_JSON_pack_allow_null ( + (NULL != mh->md.melted_coin.h_age_commitment) + ? GNUNET_JSON_pack_data_auto ("old_age_commitment_h", + mh->md.melted_coin.h_age_commitment) + : GNUNET_JSON_pack_string ("old_age_commitment_h", + NULL)), + GNUNET_JSON_pack_data_auto ("refresh_seed", + &mh->md.refresh_seed), + GNUNET_JSON_pack_allow_null ( + (mh->md.no_blinding_seed) + ? GNUNET_JSON_pack_string ("blinding_seed", + NULL) + : GNUNET_JSON_pack_data_auto ("blinding_seed", + &mh->md.blinding_seed)), + TALER_JSON_pack_array_of_data_auto ("denoms_h", + mh->md.num_fresh_coins, + mh->md.denoms_h) + ); + GNUNET_assert (NULL != j_request_body); + GNUNET_assert (NULL != + (j_transfer_pubs = json_array ())); + GNUNET_assert (NULL != + (j_coin_evs = json_array ())); + /** + * Fill the kappa array of coin envelopes and + * the array of transfer pubs. + */ + for (uint8_t k=0; k<TALER_CNC_KAPPA; k++) + { + json_t *j_envs; + json_t *j_tbs = GNUNET_JSON_PACK ( + TALER_JSON_pack_array_of_data_auto (NULL, + mh->md.num_fresh_coins, + mh->md.kappa_transfer_pubs[k]) + ); + + GNUNET_assert (NULL != (j_envs = json_array ())); + GNUNET_assert (NULL !=j_tbs); + + for (size_t i = 0; i < mh->md.num_fresh_coins; i++) + { + json_t *j_coin = GNUNET_JSON_PACK ( + TALER_JSON_pack_blinded_planchet (NULL, + &mh->md.kappa_blinded_planchets[k][i])); + GNUNET_assert (NULL != j_coin); + GNUNET_assert (0 == + json_array_append_new (j_envs, j_coin)); + } + GNUNET_assert (0 == + json_array_append_new (j_coin_evs, j_envs)); + GNUNET_assert (0 == + json_array_append_new (j_transfer_pubs, j_tbs)); + } + GNUNET_assert (0 == + json_object_set_new (j_request_body, + "coin_evs", + j_coin_evs)); + GNUNET_assert (0 == + json_object_set_new (j_request_body, + "transfer_pubs", + j_transfer_pubs)); + /* and now we can at last begin the actual request handling */ + mh->url = TALER_url_join (mh->exchange_url, + "melt", + NULL); + if (NULL == mh->url) + { + json_decref (j_request_body); + return GNUNET_SYSERR; + } + eh = TALER_EXCHANGE_curl_easy_get_ (mh->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&mh->ctx, + eh, + j_request_body)) ) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (j_request_body); + return GNUNET_SYSERR; + } + json_decref (j_request_body); + mh->job = GNUNET_CURL_job_add2 (mh->cctx, + eh, + mh->ctx.headers, + &handle_melt_finished, + mh); + return GNUNET_OK; +} + + +/** + * The melt request @a mh failed, return an error to + * the application and cancel the operation. + * + * @param[in] mh melt request that failed + * @param ec error code to fail with + */ +static void +fail_mh (struct TALER_EXCHANGE_MeltHandle *mh, + enum TALER_ErrorCode ec) +{ + struct TALER_EXCHANGE_MeltResponse mr = { + .hr.ec = ec + }; + + mh->melt_cb (mh->melt_cb_cls, + &mr); + TALER_EXCHANGE_melt_cancel (mh); +} + + +/** + * Callbacks of this type are used to serve the result of submitting a + * /blinding-prepare request to a exchange. + * + * @param cls closure with our `struct TALER_EXCHANGE_MeltHandle *` + * @param bpr response details + */ +static void +blinding_prepare_cb (void *cls, + const struct TALER_EXCHANGE_BlindingPrepareResponse *bpr) +{ + struct TALER_EXCHANGE_MeltHandle *mh = cls; + unsigned int nks_off = 0; + + mh->bpr = NULL; + if (MHD_HTTP_OK != bpr->hr.http_status) + { + struct TALER_EXCHANGE_MeltResponse mr = { + .hr = bpr->hr + }; + + mr.hr.hint = "/blinding-prepare failed"; + mh->melt_cb (mh->melt_cb_cls, + &mr); + TALER_EXCHANGE_melt_cancel (mh); + return; + } + for (unsigned int i = 0; i<mh->rd->num_fresh_denom_pubs; i++) + { + const struct TALER_EXCHANGE_DenomPublicKey *fresh_pk = + &mh->rd->fresh_denom_pubs[i]; + struct TALER_ExchangeBlindingValues *wv = &mh->melt_blinding_values[i]; + + switch (fresh_pk->key.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_INVALID: + GNUNET_break (0); + fail_mh (mh, + TALER_EC_GENERIC_CLIENT_INTERNAL_ERROR); + return; + case GNUNET_CRYPTO_BSA_RSA: + break; + case GNUNET_CRYPTO_BSA_CS: + TALER_denom_ewv_copy (wv, + &bpr->details.ok.blinding_values[nks_off]); + nks_off++; + break; + } + } + if (GNUNET_OK != + start_melt (mh)) + { + GNUNET_break (0); + fail_mh (mh, + TALER_EC_GENERIC_CLIENT_INTERNAL_ERROR); + return; + } +} + + +struct TALER_EXCHANGE_MeltHandle * +TALER_EXCHANGE_melt ( + struct GNUNET_CURL_Context *ctx, + const char *url, + struct TALER_EXCHANGE_Keys *keys, + const struct TALER_PublicRefreshMasterSeedP *rms, + const struct TALER_EXCHANGE_MeltInput *rd, + TALER_EXCHANGE_MeltCallback melt_cb, + void *melt_cb_cls) +{ + struct TALER_EXCHANGE_NonceKey nks[GNUNET_NZL (rd->num_fresh_denom_pubs)]; + unsigned int nks_off = 0; + struct TALER_EXCHANGE_MeltHandle *mh; + + if (0 == rd->num_fresh_denom_pubs) + { + GNUNET_break (0); + return NULL; + } + mh = GNUNET_new (struct TALER_EXCHANGE_MeltHandle); + mh->noreveal_index = TALER_CNC_KAPPA; /* invalid value */ + mh->cctx = ctx; + mh->exchange_url = GNUNET_strdup (url); + mh->rd = rd; + mh->rms = *rms; + mh->melt_cb = melt_cb; + mh->melt_cb_cls = melt_cb_cls; + mh->no_blinding_seed = true; + mh->melt_blinding_values = + GNUNET_new_array (rd->num_fresh_denom_pubs, + struct TALER_ExchangeBlindingValues); + for (unsigned int i = 0; i<rd->num_fresh_denom_pubs; i++) + { + const struct TALER_EXCHANGE_DenomPublicKey *fresh_pk = + &rd->fresh_denom_pubs[i]; + + switch (fresh_pk->key.bsign_pub_key->cipher) + { + case GNUNET_CRYPTO_BSA_INVALID: + GNUNET_break (0); + GNUNET_free (mh->melt_blinding_values); + GNUNET_free (mh); + return NULL; + case GNUNET_CRYPTO_BSA_RSA: + TALER_denom_ewv_copy (&mh->melt_blinding_values[i], + TALER_denom_ewv_rsa_singleton ()); + break; + case GNUNET_CRYPTO_BSA_CS: + nks[nks_off].pk = fresh_pk; + nks[nks_off].cnc_num = i; + nks_off++; + break; + } + } + mh->keys = TALER_EXCHANGE_keys_incref (keys); + if (0 != nks_off) + { + mh->no_blinding_seed = false; + TALER_cs_refresh_seed_to_blinding_seed ( + rms, + &mh->md.melted_coin.coin_priv, + &mh->blinding_seed); + mh->bpr = TALER_EXCHANGE_blinding_prepare_for_melt (ctx, + url, + &mh->blinding_seed, + nks_off, + nks, + &blinding_prepare_cb, + mh); + if (NULL == mh->bpr) + { + GNUNET_break (0); + TALER_EXCHANGE_melt_cancel (mh); + return NULL; + } + return mh; + } + if (GNUNET_OK != + start_melt (mh)) + { + GNUNET_break (0); + TALER_EXCHANGE_melt_cancel (mh); + return NULL; + } + return mh; +} + + +void +TALER_EXCHANGE_melt_cancel (struct TALER_EXCHANGE_MeltHandle *mh) +{ + for (unsigned int i = 0; i<mh->rd->num_fresh_denom_pubs; i++) + TALER_denom_ewv_free (&mh->melt_blinding_values[i]); + if (NULL != mh->job) + { + GNUNET_CURL_job_cancel (mh->job); + mh->job = NULL; + } + if (NULL != mh->bpr) + { + TALER_EXCHANGE_blinding_prepare_cancel (mh->bpr); + mh->bpr = NULL; + } + TALER_EXCHANGE_free_melt_data (&mh->md); /* does not free 'md' itself */ + GNUNET_free (mh->melt_blinding_values); + GNUNET_free (mh->url); + GNUNET_free (mh->exchange_url); + TALER_curl_easy_post_finished (&mh->ctx); + TALER_EXCHANGE_keys_decref (mh->keys); + GNUNET_free (mh); +} + + +/* end of exchange_api_melt.c */ diff --git a/src/lib/exchange_api_melt_v27.c b/src/lib/exchange_api_melt_v27.c @@ -1,626 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2025 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see - <http://www.gnu.org/licenses/> -*/ -/** - * @file lib/exchange_api_melt_v27.c - * @brief Implementation of the /melt request - * @author Özgür Kesim - */ -#include "taler/platform.h" -#include <jansson.h> -#include <microhttpd.h> /* just for HTTP status codes */ -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <gnunet/gnunet_curl_lib.h> -#include "taler/taler_json_lib.h" -#include "taler/taler_exchange_service.h" -#include "exchange_api_common.h" -#include "exchange_api_handle.h" -#include "taler/taler_signatures.h" -#include "exchange_api_curl_defaults.h" -#include "exchange_api_refresh_common.h" - - -/** - * @brief A /melt Handle - */ -struct TALER_EXCHANGE_MeltHandle_v27 -{ - - /** - * The keys of the this request handle will use - */ - struct TALER_EXCHANGE_Keys *keys; - - /** - * The url for this request. - */ - char *url; - - /** - * The exchange base url. - */ - char *exchange_url; - - /** - * Curl context. - */ - struct GNUNET_CURL_Context *cctx; - - /** - * Context for #TEH_curl_easy_post(). Keeps the data that must - * persist for Curl to make the upload. - */ - struct TALER_CURL_PostContext ctx; - - /** - * Handle for the request. - */ - struct GNUNET_CURL_Job *job; - - /** - * Function to call with refresh melt failure results. - */ - TALER_EXCHANGE_MeltCallback_v27 melt_cb; - - /** - * Closure for @e result_cb and @e melt_failure_cb. - */ - void *melt_cb_cls; - - /** - * Actual information about the melt operation. - */ - struct MeltData_v27 md; - - /** - * The secret the entire melt operation is seeded from. - */ - struct TALER_RefreshMasterSecretP rms; - - /** - * Details about the characteristics of the requested melt operation. - */ - const struct TALER_EXCHANGE_MeltInput *rd; - - /** - * True, if no blinding_seed is needed (no CS denominations involved) - */ - bool no_blinding_seed; - - /** - * If @e no_blinding_seed is false, the blinding seed for the intermediate - * call to /blinding-prepare, in order to retrieve the R-values from the - * exchange for the blind Clause-Schnorr signature. - */ - struct TALER_BlindingMasterSeedP blinding_seed; - - /** - * Array of `num_fresh_denom_pubs` per-coin values - * returned from melt operation. - */ - struct TALER_ExchangeBlindingValues *melt_blinding_values; - - /** - * Handle for the preflight request, or NULL. - */ - struct TALER_EXCHANGE_BlindingPrepareHandle *bpr; - - /** - * Public key of the coin being melted. - */ - struct TALER_CoinSpendPublicKeyP coin_pub; - - /** - * Signature affirming the melt. - */ - struct TALER_CoinSpendSignatureP coin_sig; - - /** - * @brief Public information about the coin's denomination key - */ - const struct TALER_EXCHANGE_DenomPublicKey *dki; - - /** - * Gamma value chosen by the exchange during melt. - */ - uint32_t noreveal_index; - -}; - - -/** - * Verify that the signature on the "200 OK" response - * from the exchange is valid. - * - * @param[in,out] mh melt handle - * @param json json reply with the signature - * @param[out] exchange_pub public key of the exchange used for the signature - * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR if not - */ -static enum GNUNET_GenericReturnValue -verify_melt_v27_signature_ok (struct TALER_EXCHANGE_MeltHandle_v27 *mh, - const json_t *json, - struct TALER_ExchangePublicKeyP *exchange_pub) -{ - struct TALER_ExchangeSignatureP exchange_sig; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("exchange_sig", - &exchange_sig), - GNUNET_JSON_spec_fixed_auto ("exchange_pub", - exchange_pub), - GNUNET_JSON_spec_uint32 ("noreveal_index", - &mh->noreveal_index), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (json, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - /* check that exchange signing key is permitted */ - if (GNUNET_OK != - TALER_EXCHANGE_test_signing_key (mh->keys, - exchange_pub)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - /* check that noreveal index is in permitted range */ - if (TALER_CNC_KAPPA <= mh->noreveal_index) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - if (GNUNET_OK != - TALER_exchange_online_melt_confirmation_verify ( - &mh->md.rc, - mh->noreveal_index, - exchange_pub, - &exchange_sig)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/** - * Function called when we're done processing the - * HTTP /coins/$COIN_PUB/melt request. - * - * @param cls the `struct TALER_EXCHANGE_MeltHandle_v27` - * @param response_code HTTP response code, 0 on error - * @param response parsed JSON result, NULL on error - */ -static void -handle_melt_v27_finished (void *cls, - long response_code, - const void *response) -{ - struct TALER_EXCHANGE_MeltHandle_v27 *mh = cls; - const json_t *j = response; - struct TALER_EXCHANGE_MeltResponse_v27 mr = { - .hr.reply = j, - .hr.http_status = (unsigned int) response_code - }; - - mh->job = NULL; - switch (response_code) - { - case 0: - mr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - break; - case MHD_HTTP_OK: - if (GNUNET_OK != - verify_melt_v27_signature_ok (mh, - j, - &mr.details.ok.sign_key)) - { - GNUNET_break_op (0); - mr.hr.http_status = 0; - mr.hr.ec = TALER_EC_EXCHANGE_MELT_INVALID_SIGNATURE_BY_EXCHANGE; - break; - } - mr.details.ok.noreveal_index = mh->noreveal_index; - mr.details.ok.num_melt_blinding_values = mh->rd->num_fresh_denom_pubs; - mr.details.ok.melt_blinding_values = mh->melt_blinding_values; - mr.details.ok.blinding_seed = mh->no_blinding_seed - ? NULL - : &mh->blinding_seed; - mh->melt_cb (mh->melt_cb_cls, - &mr); - mh->melt_cb = NULL; - break; - case MHD_HTTP_BAD_REQUEST: - /* This should never happen, either us or the exchange is buggy - (or API version conflict); just pass JSON reply to the application */ - mr.hr.ec = TALER_JSON_get_error_code (j); - mr.hr.hint = TALER_JSON_get_error_hint (j); - break; - case MHD_HTTP_CONFLICT: - mr.hr.ec = TALER_JSON_get_error_code (j); - mr.hr.hint = TALER_JSON_get_error_hint (j); - break; - case MHD_HTTP_FORBIDDEN: - /* Nothing really to verify, exchange says one of the signatures is - invalid; assuming we checked them, this should never happen, we - should pass the JSON reply to the application */ - mr.hr.ec = TALER_JSON_get_error_code (j); - mr.hr.hint = TALER_JSON_get_error_hint (j); - break; - case MHD_HTTP_NOT_FOUND: - /* Nothing really to verify, this should never - happen, we should pass the JSON reply to the application */ - mr.hr.ec = TALER_JSON_get_error_code (j); - mr.hr.hint = TALER_JSON_get_error_hint (j); - break; - case MHD_HTTP_INTERNAL_SERVER_ERROR: - /* Server had an internal issue; we should retry, but this API - leaves this to the application */ - mr.hr.ec = TALER_JSON_get_error_code (j); - mr.hr.hint = TALER_JSON_get_error_hint (j); - break; - default: - /* unexpected response code */ - mr.hr.ec = TALER_JSON_get_error_code (j); - mr.hr.hint = TALER_JSON_get_error_hint (j); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Unexpected response code %u/%d for exchange melt\n", - (unsigned int) response_code, - mr.hr.ec); - GNUNET_break_op (0); - break; - } - if (NULL != mh->melt_cb) - mh->melt_cb (mh->melt_cb_cls, - &mr); - TALER_EXCHANGE_melt_v27_cancel (mh); -} - - -/** - * Start the actual melt operation, now that we have - * the exchange's input values. - * - * @param[in,out] mh melt operation to run - * @return #GNUNET_OK if we could start the operation - */ -static enum GNUNET_GenericReturnValue -start_melt (struct TALER_EXCHANGE_MeltHandle_v27 *mh) -{ - json_t *j_request_body; - json_t *j_coin_evs; - CURL *eh; - struct TALER_DenominationHashP h_denom_pub; - - if (GNUNET_OK != - TALER_EXCHANGE_get_melt_data_v27 (&mh->rms, - mh->rd, - mh->no_blinding_seed - ? NULL - : &mh->blinding_seed, - mh->melt_blinding_values, - &mh->md)) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - TALER_denom_pub_hash (&mh->md.melted_coin.pub_key, - &h_denom_pub); - TALER_wallet_melt_sign ( - &mh->md.melted_coin.melt_amount_with_fee, - &mh->md.melted_coin.fee_melt, - &mh->md.rc, - &h_denom_pub, - mh->md.melted_coin.h_age_commitment, - &mh->md.melted_coin.coin_priv, - &mh->coin_sig); - GNUNET_CRYPTO_eddsa_key_get_public ( - &mh->md.melted_coin.coin_priv.eddsa_priv, - &mh->coin_pub.eddsa_pub); - mh->dki = TALER_EXCHANGE_get_denomination_key (mh->keys, - &mh->md.melted_coin.pub_key); - j_request_body = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("old_coin_pub", - &mh->coin_pub), - GNUNET_JSON_pack_data_auto ("old_denom_pub_h", - &h_denom_pub), - TALER_JSON_pack_denom_sig ("old_denom_sig", - &mh->md.melted_coin.sig), - GNUNET_JSON_pack_data_auto ("confirm_sig", - &mh->coin_sig), - TALER_JSON_pack_amount ("value_with_fee", - &mh->md.melted_coin.melt_amount_with_fee), - GNUNET_JSON_pack_allow_null ( - (NULL != mh->md.melted_coin.h_age_commitment) - ? GNUNET_JSON_pack_data_auto ("old_age_commitment_h", - mh->md.melted_coin.h_age_commitment) - : GNUNET_JSON_pack_string ("old_age_commitment_h", - NULL)), - GNUNET_JSON_pack_data_auto ("refresh_seed", - &mh->md.refresh_seed), - GNUNET_JSON_pack_allow_null ( - (mh->md.no_blinding_seed) - ? GNUNET_JSON_pack_string ("blinding_seed", - NULL) - : GNUNET_JSON_pack_data_auto ("blinding_seed", - &mh->md.blinding_seed)), - TALER_JSON_pack_array_of_data ("denoms_h", - mh->md.num_fresh_coins, - mh->md.denoms_h, - sizeof(*mh->md.denoms_h)) - ); - GNUNET_assert (NULL != j_request_body); - j_coin_evs = json_array (); - GNUNET_assert (NULL != j_coin_evs); - /** - * Fill the kappa array of coin envelopes - */ - for (uint8_t k=0; k<TALER_CNC_KAPPA; k++) - { - json_t *j_envs = json_array (); - GNUNET_assert (NULL != j_envs); - for (size_t i = 0; i < mh->md.num_fresh_coins; i++) - { - json_t *j_coin = GNUNET_JSON_PACK ( - TALER_JSON_pack_blinded_planchet (NULL, - &mh->md.kappa_blinded_planchets[k][i]) - ); - GNUNET_assert (NULL != j_coin); - GNUNET_assert (0 == - json_array_append_new (j_envs, j_coin)); - } - GNUNET_assert (0 == - json_array_append_new (j_coin_evs, j_envs)); - } - GNUNET_assert (0 == - json_object_set_new (j_request_body, - "coin_evs", - j_coin_evs)); - /* and now we can at last begin the actual request handling */ - mh->url = TALER_url_join (mh->exchange_url, - "melt", - NULL); - if (NULL == mh->url) - { - json_decref (j_request_body); - return GNUNET_SYSERR; - } - eh = TALER_EXCHANGE_curl_easy_get_ (mh->url); - if ( (NULL == eh) || - (GNUNET_OK != - TALER_curl_easy_post (&mh->ctx, - eh, - j_request_body)) ) - { - GNUNET_break (0); - if (NULL != eh) - curl_easy_cleanup (eh); - json_decref (j_request_body); - return GNUNET_SYSERR; - } - json_decref (j_request_body); - mh->job = GNUNET_CURL_job_add2 (mh->cctx, - eh, - mh->ctx.headers, - &handle_melt_v27_finished, - mh); - return GNUNET_OK; -} - - -/** - * The melt request @a mh failed, return an error to - * the application and cancel the operation. - * - * @param[in] mh melt request that failed - * @param ec error code to fail with - */ -static void -fail_mh (struct TALER_EXCHANGE_MeltHandle_v27 *mh, - enum TALER_ErrorCode ec) -{ - struct TALER_EXCHANGE_MeltResponse_v27 mr = { - .hr.ec = ec - }; - - mh->melt_cb (mh->melt_cb_cls, - &mr); - TALER_EXCHANGE_melt_v27_cancel (mh); -} - - -/** - * Callbacks of this type are used to serve the result of submitting a - * /blinding-prepare request to a exchange. - * - * @param cls closure with our `struct TALER_EXCHANGE_MeltHandle_v27 *` - * @param bpr response details - */ -static void -blinding_prepare_cb (void *cls, - const struct TALER_EXCHANGE_BlindingPrepareResponse *bpr) -{ - struct TALER_EXCHANGE_MeltHandle_v27 *mh = cls; - unsigned int nks_off = 0; - - mh->bpr = NULL; - if (MHD_HTTP_OK != bpr->hr.http_status) - { - struct TALER_EXCHANGE_MeltResponse_v27 mr = { - .hr = bpr->hr - }; - - mr.hr.hint = "/blinding-prepare failed"; - mh->melt_cb (mh->melt_cb_cls, - &mr); - TALER_EXCHANGE_melt_v27_cancel (mh); - return; - } - for (unsigned int i = 0; i<mh->rd->num_fresh_denom_pubs; i++) - { - const struct TALER_EXCHANGE_DenomPublicKey *fresh_pk = - &mh->rd->fresh_denom_pubs[i]; - struct TALER_ExchangeBlindingValues *wv = &mh->melt_blinding_values[i]; - - switch (fresh_pk->key.bsign_pub_key->cipher) - { - case GNUNET_CRYPTO_BSA_INVALID: - GNUNET_break (0); - fail_mh (mh, - TALER_EC_GENERIC_CLIENT_INTERNAL_ERROR); - return; - case GNUNET_CRYPTO_BSA_RSA: - break; - case GNUNET_CRYPTO_BSA_CS: - TALER_denom_ewv_copy (wv, - &bpr->details.ok.blinding_values[nks_off]); - nks_off++; - break; - } - } - if (GNUNET_OK != - start_melt (mh)) - { - GNUNET_break (0); - fail_mh (mh, - TALER_EC_GENERIC_CLIENT_INTERNAL_ERROR); - return; - } -} - - -struct TALER_EXCHANGE_MeltHandle_v27 * -TALER_EXCHANGE_melt_v27 ( - struct GNUNET_CURL_Context *ctx, - const char *url, - struct TALER_EXCHANGE_Keys *keys, - const struct TALER_RefreshMasterSecretP *rms, - const struct TALER_EXCHANGE_MeltInput *rd, - TALER_EXCHANGE_MeltCallback_v27 melt_cb, - void *melt_cb_cls) -{ - struct TALER_EXCHANGE_NonceKey nks[GNUNET_NZL (rd->num_fresh_denom_pubs)]; - unsigned int nks_off = 0; - struct TALER_EXCHANGE_MeltHandle_v27 *mh; - - if (0 == rd->num_fresh_denom_pubs) - { - GNUNET_break (0); - return NULL; - } - mh = GNUNET_new (struct TALER_EXCHANGE_MeltHandle_v27); - mh->noreveal_index = TALER_CNC_KAPPA; /* invalid value */ - mh->cctx = ctx; - mh->exchange_url = GNUNET_strdup (url); - mh->rd = rd; - mh->rms = *rms; - mh->melt_cb = melt_cb; - mh->melt_cb_cls = melt_cb_cls; - mh->no_blinding_seed = true; - mh->melt_blinding_values = - GNUNET_new_array (rd->num_fresh_denom_pubs, - struct TALER_ExchangeBlindingValues); - for (unsigned int i = 0; i<rd->num_fresh_denom_pubs; i++) - { - const struct TALER_EXCHANGE_DenomPublicKey *fresh_pk = - &rd->fresh_denom_pubs[i]; - - switch (fresh_pk->key.bsign_pub_key->cipher) - { - case GNUNET_CRYPTO_BSA_INVALID: - GNUNET_break (0); - GNUNET_free (mh->melt_blinding_values); - GNUNET_free (mh); - return NULL; - case GNUNET_CRYPTO_BSA_RSA: - TALER_denom_ewv_copy (&mh->melt_blinding_values[i], - TALER_denom_ewv_rsa_singleton ()); - break; - case GNUNET_CRYPTO_BSA_CS: - nks[nks_off].pk = fresh_pk; - nks[nks_off].cnc_num = i; - nks_off++; - break; - } - } - mh->keys = TALER_EXCHANGE_keys_incref (keys); - if (0 != nks_off) - { - mh->no_blinding_seed = false; - TALER_cs_refresh_secret_to_blinding_seed ( - rms, - &mh->blinding_seed); - mh->bpr = TALER_EXCHANGE_blinding_prepare_for_melt (ctx, - url, - &mh->blinding_seed, - nks_off, - nks, - &blinding_prepare_cb, - mh); - if (NULL == mh->bpr) - { - GNUNET_break (0); - TALER_EXCHANGE_melt_v27_cancel (mh); - return NULL; - } - return mh; - } - if (GNUNET_OK != - start_melt (mh)) - { - GNUNET_break (0); - TALER_EXCHANGE_melt_v27_cancel (mh); - return NULL; - } - return mh; -} - - -void -TALER_EXCHANGE_melt_v27_cancel (struct TALER_EXCHANGE_MeltHandle_v27 *mh) -{ - for (unsigned int i = 0; i<mh->rd->num_fresh_denom_pubs; i++) - TALER_denom_ewv_free (&mh->melt_blinding_values[i]); - if (NULL != mh->job) - { - GNUNET_CURL_job_cancel (mh->job); - mh->job = NULL; - } - if (NULL != mh->bpr) - { - TALER_EXCHANGE_blinding_prepare_cancel (mh->bpr); - mh->bpr = NULL; - } - TALER_EXCHANGE_free_melt_data_v27 (&mh->md); /* does not free 'md' itself */ - GNUNET_free (mh->melt_blinding_values); - GNUNET_free (mh->url); - GNUNET_free (mh->exchange_url); - TALER_curl_easy_post_finished (&mh->ctx); - TALER_EXCHANGE_keys_decref (mh->keys); - GNUNET_free (mh); -} - - -/* end of exchange_api_melt.c */ diff --git a/src/lib/exchange_api_recoup_refresh.c b/src/lib/exchange_api_recoup_refresh.c @@ -215,6 +215,7 @@ handle_recoup_refresh_finished (void *cls, } +#pragma message "TODO[oec]: rewrite TALER_EXCHANGE_RecoupRefreshHandle" struct TALER_EXCHANGE_RecoupRefreshHandle * TALER_EXCHANGE_recoup_refresh ( struct GNUNET_CURL_Context *ctx, @@ -223,7 +224,7 @@ TALER_EXCHANGE_recoup_refresh ( const struct TALER_EXCHANGE_DenomPublicKey *pk, const struct TALER_DenominationSignature *denom_sig, const struct TALER_ExchangeBlindingValues *blinding_values, - const struct TALER_RefreshMasterSecretP *rms, + const struct TALER_PublicRefreshMasterSeedP *rms, const struct TALER_PlanchetMasterSecretP *ps, unsigned int idx, TALER_EXCHANGE_RecoupRefreshResultCallback recoup_cb, @@ -282,21 +283,7 @@ TALER_EXCHANGE_recoup_refresh ( break; case GNUNET_CRYPTO_BSA_CS: { - union GNUNET_CRYPTO_BlindSessionNonce nonce; - - /* NOTE: this is not elegant, and as per the note in TALER_coin_ev_hash() - it is not strictly clear that the nonce is needed. Best case would be - to find a way to include it more 'naturally' somehow, for example with - the variant union version of bks! */ - TALER_cs_refresh_nonce_derive (rms, - idx, - &nonce.cs_nonce); - GNUNET_assert ( - 0 == - json_object_set_new (recoup_obj, - "nonce", - GNUNET_JSON_from_data_auto ( - &nonce))); + } break; } diff --git a/src/lib/exchange_api_refresh_common.c b/src/lib/exchange_api_refresh_common.c @@ -25,13 +25,14 @@ void -TALER_EXCHANGE_free_melt_data_v27 (struct MeltData_v27 *md) +TALER_EXCHANGE_free_melt_data (struct MeltData *md) { for (unsigned int k = 0; k < TALER_CNC_KAPPA; k++) { for (unsigned int i = 0; i < md->num_fresh_coins; i++) TALER_blinded_planchet_free (&md->kappa_blinded_planchets[k][i]); GNUNET_free (md->kappa_blinded_planchets[k]); + GNUNET_free (md->kappa_transfer_pubs[k]); } GNUNET_free (md->denoms_h); GNUNET_free (md->denom_pubs); @@ -54,23 +55,22 @@ TALER_EXCHANGE_free_melt_data_v27 (struct MeltData_v27 *md) } /* Finally, clean up a bit... */ GNUNET_CRYPTO_zero_keys (md, - sizeof (struct MeltData_v27)); + sizeof (struct MeltData)); } enum GNUNET_GenericReturnValue -TALER_EXCHANGE_get_melt_data_v27 ( - const struct TALER_RefreshMasterSecretP *rms, +TALER_EXCHANGE_get_melt_data ( + const struct TALER_PublicRefreshMasterSeedP *rms, const struct TALER_EXCHANGE_MeltInput *rd, const struct TALER_BlindingMasterSeedP *blinding_seed, const struct TALER_ExchangeBlindingValues *blinding_values, - struct MeltData_v27 *md) + struct MeltData *md) { struct TALER_Amount total; struct TALER_CoinSpendPublicKeyP coin_pub; struct TALER_KappaHashBlindedPlanchetsP k_h_bps; union GNUNET_CRYPTO_BlindSessionNonce nonces[rd->num_fresh_denom_pubs]; - const struct TALER_DenominationHashP *denoms_h[rd->num_fresh_denom_pubs]; bool is_cs[rd->num_fresh_denom_pubs]; bool uses_cs = false; @@ -80,6 +80,7 @@ TALER_EXCHANGE_get_melt_data_v27 ( 0, sizeof (*md)); /* build up melt data structure */ + md->refresh_seed = *rms; md->num_fresh_coins = rd->num_fresh_denom_pubs; md->melted_coin.coin_priv = rd->melt_priv; md->melted_coin.melt_amount_with_fee = rd->melt_amount; @@ -108,11 +109,10 @@ TALER_EXCHANGE_get_melt_data_v27 ( GNUNET_new_array (md->num_fresh_coins, const struct TALER_DenominationPublicKey*); - for (unsigned int j = 0; j<rd->num_fresh_denom_pubs; j++) + for (unsigned int j = 0; j<md->num_fresh_coins; j++) { struct FreshCoinData *fcd = &md->fcds[j]; - denoms_h[j] = &rd->fresh_denom_pubs[j].h_key; md->denoms_h[j] = rd->fresh_denom_pubs[j].h_key; md->denom_pubs[j] = &rd->fresh_denom_pubs[j].key; @@ -123,14 +123,14 @@ TALER_EXCHANGE_get_melt_data_v27 ( fcd->fresh_pk.bsign_pub_key->cipher) { GNUNET_break (0); - TALER_EXCHANGE_free_melt_data_v27 (md); + TALER_EXCHANGE_free_melt_data (md); return GNUNET_SYSERR; } switch (fcd->fresh_pk.bsign_pub_key->cipher) { case GNUNET_CRYPTO_BSA_INVALID: GNUNET_break (0); - TALER_EXCHANGE_free_melt_data_v27 (md); + TALER_EXCHANGE_free_melt_data (md); return GNUNET_SYSERR; case GNUNET_CRYPTO_BSA_RSA: is_cs[j] = false; @@ -150,7 +150,7 @@ TALER_EXCHANGE_get_melt_data_v27 ( &rd->fresh_denom_pubs[j].fees.withdraw)) ) { GNUNET_break (0); - TALER_EXCHANGE_free_melt_data_v27 (md); + TALER_EXCHANGE_free_melt_data (md); return GNUNET_SYSERR; } } @@ -163,13 +163,13 @@ TALER_EXCHANGE_get_melt_data_v27 ( /* Eh, this operation is more expensive than the @a melt_amount. This is not OK. */ GNUNET_break (0); - TALER_EXCHANGE_free_melt_data_v27 (md); + TALER_EXCHANGE_free_melt_data (md); return GNUNET_SYSERR; } /** * Generate the blinding seeds and nonces for CS. * Note that blinding_seed was prepared upstream, - * in TALER_EXCHANGE_melt_v27(), as preparation for + * in TALER_EXCHANGE_melt(), as preparation for * the request to `/blinding-prepare`. */ memset (nonces, @@ -181,46 +181,41 @@ TALER_EXCHANGE_get_melt_data_v27 ( TALER_cs_derive_blind_nonces_from_seed ( blinding_seed, true, /* for melt */ - rd->num_fresh_denom_pubs, + md->num_fresh_coins, is_cs, nonces); } /** - * Generate from the master secret the refresh seed - * and kappa nonces + * Generate the kappa private seeds for the batches. */ - TALER_refresh_master_secret_to_refresh_seed ( - rms, - &md->refresh_seed); - TALER_refresh_expand_kappa_nonces ( + TALER_refresh_expand_seed_to_kappa_batch_seeds ( &md->refresh_seed, - &md->kappa_nonces); + &rd->melt_priv, + &md->kappa_batch_seeds); /** * Build up all candidates for coin planchets */ for (unsigned int k = 0; k<TALER_CNC_KAPPA; k++) { struct TALER_PlanchetMasterSecretP - planchet_secrets[rd->num_fresh_denom_pubs]; - struct TALER_PlanchetDetail planchet_details[rd->num_fresh_denom_pubs]; - - TALER_wallet_refresh_nonce_sign ( - &rd->melt_priv, - &md->kappa_nonces.tuple[k], - rd->num_fresh_denom_pubs, - denoms_h, - k, - &md->signatures[k]); - TALER_refresh_signature_to_secrets ( - &md->signatures[k], - rd->num_fresh_denom_pubs, - planchet_secrets); + planchet_secrets[md->num_fresh_coins]; + struct TALER_PlanchetDetail planchet_details[md->num_fresh_coins]; md->kappa_blinded_planchets[k] = - GNUNET_new_array (rd->num_fresh_denom_pubs, - struct TALER_BlindedPlanchet); + GNUNET_new_array (md->num_fresh_coins, + struct TALER_BlindedPlanchet); + md->kappa_transfer_pubs[k] = + GNUNET_new_array (md->num_fresh_coins, + struct TALER_TransferPublicKeyP); + + TALER_refresh_expand_batch_seed_to_transfer_data ( + &md->kappa_batch_seeds.tuple[k], + &coin_pub, + md->num_fresh_coins, + planchet_secrets, + md->kappa_transfer_pubs[k]); - for (unsigned int j = 0; j<rd->num_fresh_denom_pubs; j++) + for (unsigned int j = 0; j<md->num_fresh_coins; j++) { struct FreshCoinData *fcd = &md->fcds[j]; struct TALER_CoinSpendPrivateKeyP *coin_priv = &fcd->coin_priv; @@ -238,8 +233,8 @@ TALER_EXCHANGE_get_melt_data_v27 ( bks); if (NULL != rd->melt_age_commitment_proof) { - fcd->age_commitment_proofs[k] = GNUNET_new (struct - TALER_AgeCommitmentProof); + fcd->age_commitment_proofs[k] = + GNUNET_new (struct TALER_AgeCommitmentProof); GNUNET_assert (GNUNET_OK == TALER_age_commitment_proof_derive_from_secret ( md->melted_coin.age_commitment_proof, @@ -266,7 +261,7 @@ TALER_EXCHANGE_get_melt_data_v27 ( &planchet_details[j])) { GNUNET_break_op (0); - TALER_EXCHANGE_free_melt_data_v27 (md); + TALER_EXCHANGE_free_melt_data (md); return GNUNET_SYSERR; } md->kappa_blinded_planchets[k][j] = @@ -282,13 +277,23 @@ TALER_EXCHANGE_get_melt_data_v27 ( } /* Finally, compute refresh commitment */ - TALER_refresh_get_commitment_v27 (&md->rc, - &md->refresh_seed, - uses_cs + { + struct TALER_KappaTransferPublicKeys k_tr_pubs = { + .num_transfer_pubs = md->num_fresh_coins, + }; + + for (uint8_t k=0; k<TALER_CNC_KAPPA; k++) + k_tr_pubs.batch[k] = md->kappa_transfer_pubs[k]; + + TALER_refresh_get_commitment (&md->rc, + &md->refresh_seed, + uses_cs ? &md->blinding_seed : NULL, - &k_h_bps, - &coin_pub, - &rd->melt_amount); + &k_tr_pubs, + &k_h_bps, + &coin_pub, + &rd->melt_amount); + } return GNUNET_OK; } diff --git a/src/lib/exchange_api_refresh_common.h b/src/lib/exchange_api_refresh_common.h @@ -117,7 +117,7 @@ struct FreshCoinData /** * Melt data in non-serialized format for convenient processing. */ -struct MeltData_v27 +struct MeltData { /** @@ -153,17 +153,10 @@ struct MeltData_v27 struct TALER_PublicRefreshMasterSeedP refresh_seed; /** - * The tuple of kappa nonces which we sign with the melted coin. - * From those signatures, all the planchet seeds are derived from. + * The tuple of kappa batch seeds, derived from the @a refresh_seed + * and the coin's private key. */ - struct TALER_KappaPublicRefreshNoncesP kappa_nonces; - - /** - * The tuple of kappa signatures of the @e kappa_nonces, signed - * by the @e melted_coin, from which we derive all - * kappa * @e num_fresh_coins planchet secrets. - */ - struct TALER_PrivateRefreshNonceSignatureP signatures[TALER_CNC_KAPPA]; + struct TALER_KappaPrivateRefreshBatchSeedsP kappa_batch_seeds; /** * Array of length @e num_fresh_coins with information @@ -182,6 +175,13 @@ struct MeltData_v27 struct TALER_DenominationHashP *denoms_h; /** + * kappa many arrays fo @e num_fresh_coins transfer public keys that are + * derived from the @e kappa_batch_seeds and the private key of the coin. + * This will be sent to the exchange during the melt request. + */ + struct TALER_TransferPublicKeyP *kappa_transfer_pubs[TALER_CNC_KAPPA]; + + /** * Blinded planchets of the fresh coins, depending on the cut-and-choose. * Array of length @e num_fresh_coins. */ @@ -192,19 +192,19 @@ struct MeltData_v27 * Compute the melt data from the refresh data and secret * for v27 of the protocol. * - * @param rms secret internals of the refresh-reveal operation + * @param rms public seed of the refresh-reveal operation * @param rd refresh data with the characteristics of the operation * @param blinding_seed seed for blinding in case of CS denominations, can be NULL * @param alg_values contributions from the exchange into the melt * @param[out] md where to write the derived melt data */ enum GNUNET_GenericReturnValue -TALER_EXCHANGE_get_melt_data_v27 ( - const struct TALER_RefreshMasterSecretP *rms, +TALER_EXCHANGE_get_melt_data ( + const struct TALER_PublicRefreshMasterSeedP *rms, const struct TALER_EXCHANGE_MeltInput *rd, const struct TALER_BlindingMasterSeedP *blinding_seed, const struct TALER_ExchangeBlindingValues *alg_values, - struct MeltData_v27 *md); + struct MeltData *md); /** * Free all information associated with a melting session. Note @@ -216,6 +216,6 @@ TALER_EXCHANGE_get_melt_data_v27 ( * freed (as it is typically not allocated by itself) */ void -TALER_EXCHANGE_free_melt_data_v27 (struct MeltData_v27 *md); +TALER_EXCHANGE_free_melt_data (struct MeltData *md); #endif diff --git a/src/lib/exchange_api_reveal_melt.c b/src/lib/exchange_api_reveal_melt.c @@ -67,7 +67,7 @@ struct TALER_EXCHANGE_RevealMeltHandle /** * The melt data */ - struct MeltData_v27 md; + struct MeltData md; /** * Callback to pass the result onto @@ -296,11 +296,11 @@ perform_protocol ( struct TALER_EXCHANGE_RevealMeltHandle *mrh) { CURL *curlh; - json_t *j_array_of_signatures; + json_t *j_batch_seeds; - j_array_of_signatures = json_array (); - GNUNET_assert (NULL != j_array_of_signatures); + j_batch_seeds = json_array (); + GNUNET_assert (NULL != j_batch_seeds); for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) { @@ -308,8 +308,9 @@ perform_protocol ( continue; GNUNET_assert (0 == json_array_append_new ( - j_array_of_signatures, - GNUNET_JSON_from_data_auto (&mrh->md.signatures[k]))); + j_batch_seeds, + GNUNET_JSON_from_data_auto ( + &mrh->md.kappa_batch_seeds.tuple[k]))); } { json_t *j_request_body; @@ -317,8 +318,8 @@ perform_protocol ( j_request_body = GNUNET_JSON_PACK ( GNUNET_JSON_pack_data_auto ("rc", &mrh->md.rc), - GNUNET_JSON_pack_array_steal ("signatures", - j_array_of_signatures)); + GNUNET_JSON_pack_array_steal ("batch_seeds", + j_batch_seeds)); GNUNET_assert (NULL != j_request_body); if (NULL != mrh->reveal_input->melt_input->melt_age_commitment_proof) @@ -341,7 +342,6 @@ perform_protocol ( j_request_body)); json_decref (j_request_body); } - mrh->job = GNUNET_CURL_job_add2 ( curl_ctx, curlh, @@ -388,7 +388,7 @@ TALER_EXCHANGE_reveal_melt ( GNUNET_free (mrh); return NULL; } - TALER_EXCHANGE_get_melt_data_v27 ( + TALER_EXCHANGE_get_melt_data ( reveal_melt_input->rms, reveal_melt_input->melt_input, reveal_melt_input->blinding_seed, @@ -410,7 +410,7 @@ TALER_EXCHANGE_reveal_melt_cancel ( mrh->job = NULL; } TALER_curl_easy_post_finished (&mrh->post_ctx); - TALER_EXCHANGE_free_melt_data_v27 (&mrh->md); + TALER_EXCHANGE_free_melt_data (&mrh->md); GNUNET_free (mrh->request_url); GNUNET_free (mrh); } diff --git a/src/testing/testing_api_cmd_recoup_refresh.c b/src/testing/testing_api_cmd_recoup_refresh.c @@ -201,7 +201,7 @@ recoup_refresh_run (void *cls, const struct TALER_CoinSpendPrivateKeyP *coin_priv_old; const struct TALER_EXCHANGE_DenomPublicKey *denom_pub; const struct TALER_DenominationSignature *coin_sig; - const struct TALER_RefreshMasterSecretP *rplanchet; + const struct TALER_PublicRefreshMasterSeedP *rms; const struct TALER_PlanchetMasterSecretP *planchet; const struct TALER_ExchangeBlindingValues *ewv; char *cref; @@ -280,8 +280,8 @@ recoup_refresh_run (void *cls, return; } if (GNUNET_OK != - TALER_TESTING_get_trait_refresh_secret (melt_cmd, - &rplanchet)) + TALER_TESTING_get_trait_refresh_seed (melt_cmd, + &rms)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (is); @@ -338,7 +338,7 @@ recoup_refresh_run (void *cls, denom_pub, coin_sig, ewv, - rplanchet, + rms, planchet, idx, &recoup_refresh_cb, diff --git a/src/testing/testing_api_cmd_refresh.c b/src/testing/testing_api_cmd_refresh.c @@ -116,7 +116,7 @@ struct MeltState /** * Melt handle while operation is running. */ - struct TALER_EXCHANGE_MeltHandle_v27 *mh; + struct TALER_EXCHANGE_MeltHandle *mh; /** * Expected entry in the coin history created by this @@ -169,12 +169,7 @@ struct MeltState /** * Entropy seed for the refresh-melt operation. */ - struct TALER_RefreshMasterSecretP rms; - - /** - * The master seed to derive the nonces from - */ - struct TALER_PublicRefreshMasterSeedP refresh_seed; + struct TALER_PublicRefreshMasterSeedP rms; /** * If false, @e blinding_seed contains the seed for the @@ -461,9 +456,10 @@ reveal_cb (void *cls, return; } } - TALER_TESTING_unexpected_status (rrs->is, - hr->http_status, - rrs->expected_response_code); + TALER_TESTING_unexpected_status_with_body (rrs->is, + hr->http_status, + rrs->expected_response_code, + hr->reply); return; } melt_cmd = TALER_TESTING_interpreter_lookup_command (rrs->is, @@ -558,7 +554,7 @@ melt_reveal_run (void *cls, struct TALER_TESTING_Interpreter *is) { struct RevealMeltState *rrs = cls; - struct MeltState *rms; + struct MeltState *ms; const struct TALER_TESTING_Command *melt_cmd; rrs->cmd = cmd; @@ -572,19 +568,19 @@ melt_reveal_run (void *cls, return; } GNUNET_assert (melt_cmd->run == &melt_run); - rms = melt_cmd->cls; - rms->reveal_melt_input.rms = &rms->rms; - rms->reveal_melt_input.melt_input = &rms->melt_input; - rms->reveal_melt_input.blinding_seed = rms->no_blinding_seed + ms = melt_cmd->cls; + ms->reveal_melt_input.rms = &ms->rms; + ms->reveal_melt_input.melt_input = &ms->melt_input; + ms->reveal_melt_input.blinding_seed = ms->no_blinding_seed ? NULL - : &rms->blinding_seed; - rms->reveal_melt_input.num_blinding_values = rms->num_blinding_values; - rms->reveal_melt_input.blinding_values = rms->blinding_values; - rms->reveal_melt_input.noreveal_index = rms->noreveal_index; + : &ms->blinding_seed; + ms->reveal_melt_input.num_blinding_values = ms->num_blinding_values; + ms->reveal_melt_input.blinding_values = ms->blinding_values; + ms->reveal_melt_input.noreveal_index = ms->noreveal_index; rrs->rmh = TALER_EXCHANGE_reveal_melt ( TALER_TESTING_interpreter_get_context (is), TALER_TESTING_get_exchange_url (is), - &rms->reveal_melt_input, + &ms->reveal_melt_input, &reveal_cb, rrs); if (NULL == rrs->rmh) @@ -664,17 +660,17 @@ do_melt_retry (void *cls) */ static void melt_cb (void *cls, - const struct TALER_EXCHANGE_MeltResponse_v27 *mr) + const struct TALER_EXCHANGE_MeltResponse *mr) { - struct MeltState *rms = cls; + struct MeltState *ms = cls; const struct TALER_EXCHANGE_HttpResponse *hr = &mr->hr; - rms->mh = NULL; - if (rms->expected_response_code != hr->http_status) + ms->mh = NULL; + if (ms->expected_response_code != hr->http_status) { - if (0 != rms->do_retry) + if (0 != ms->do_retry) { - rms->do_retry--; + ms->do_retry--; if ( (0 == hr->http_status) || (TALER_EC_GENERIC_DB_SOFT_FAILURE == hr->ec) || (MHD_HTTP_INTERNAL_SERVER_ERROR == hr->http_status) ) @@ -685,78 +681,78 @@ melt_cb (void *cls, (int) hr->ec); /* on DB conflicts, do not use backoff */ if (TALER_EC_GENERIC_DB_SOFT_FAILURE == hr->ec) - rms->backoff = GNUNET_TIME_UNIT_ZERO; + ms->backoff = GNUNET_TIME_UNIT_ZERO; else - rms->backoff = GNUNET_TIME_randomized_backoff (rms->backoff, - MAX_BACKOFF); - rms->total_backoff = GNUNET_TIME_relative_add (rms->total_backoff, - rms->backoff); - TALER_TESTING_inc_tries (rms->is); - rms->retry_task = GNUNET_SCHEDULER_add_delayed (rms->backoff, - &do_melt_retry, - rms); + ms->backoff = GNUNET_TIME_randomized_backoff (ms->backoff, + MAX_BACKOFF); + ms->total_backoff = GNUNET_TIME_relative_add (ms->total_backoff, + ms->backoff); + TALER_TESTING_inc_tries (ms->is); + ms->retry_task = GNUNET_SCHEDULER_add_delayed (ms->backoff, + &do_melt_retry, + ms); return; } } - TALER_TESTING_unexpected_status_with_body (rms->is, + TALER_TESTING_unexpected_status_with_body (ms->is, hr->http_status, - rms->expected_response_code, + ms->expected_response_code, hr->reply); return; } if (MHD_HTTP_OK == hr->http_status) { - rms->noreveal_index = mr->details.ok.noreveal_index; - if (mr->details.ok.num_melt_blinding_values != rms->num_fresh_coins) + ms->noreveal_index = mr->details.ok.noreveal_index; + if (mr->details.ok.num_melt_blinding_values != ms->num_fresh_coins) { GNUNET_break (0); - TALER_TESTING_interpreter_fail (rms->is); + TALER_TESTING_interpreter_fail (ms->is); return; } - rms->no_blinding_seed = (NULL == mr->details.ok.blinding_seed); + ms->no_blinding_seed = (NULL == mr->details.ok.blinding_seed); if (NULL != mr->details.ok.blinding_seed) - rms->blinding_seed = *mr->details.ok.blinding_seed; - rms->num_blinding_values = mr->details.ok.num_melt_blinding_values; - if (NULL != rms->blinding_values) + ms->blinding_seed = *mr->details.ok.blinding_seed; + ms->num_blinding_values = mr->details.ok.num_melt_blinding_values; + if (NULL != ms->blinding_values) { GNUNET_break (0); /* can this this happen? Check! */ - for (unsigned int i = 0; i < rms->num_blinding_values; i++) - TALER_denom_ewv_free (&rms->blinding_values[i]); - GNUNET_free (rms->blinding_values); + for (unsigned int i = 0; i < ms->num_blinding_values; i++) + TALER_denom_ewv_free (&ms->blinding_values[i]); + GNUNET_free (ms->blinding_values); } - rms->blinding_values = GNUNET_new_array ( - rms->num_blinding_values, + ms->blinding_values = GNUNET_new_array ( + ms->num_blinding_values, struct TALER_ExchangeBlindingValues); - for (unsigned int i = 0; i<rms->num_blinding_values; i++) + for (unsigned int i = 0; i<ms->num_blinding_values; i++) { - TALER_denom_ewv_copy (&rms->blinding_values[i], + TALER_denom_ewv_copy (&ms->blinding_values[i], &mr->details.ok.melt_blinding_values[i]); } } - if (0 != rms->total_backoff.rel_value_us) + if (0 != ms->total_backoff.rel_value_us) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Total melt backoff for %s was %s\n", - rms->cmd->label, - GNUNET_STRINGS_relative_time_to_string (rms->total_backoff, + ms->cmd->label, + GNUNET_STRINGS_relative_time_to_string (ms->total_backoff, true)); } - if (rms->double_melt) + if (ms->double_melt) { TALER_LOG_DEBUG ("Doubling the melt (%s)\n", - rms->cmd->label); - rms->mh = TALER_EXCHANGE_melt_v27 ( - TALER_TESTING_interpreter_get_context (rms->is), - TALER_TESTING_get_exchange_url (rms->is), - TALER_TESTING_get_keys (rms->is), - &rms->rms, - &rms->melt_input, + ms->cmd->label); + ms->mh = TALER_EXCHANGE_melt ( + TALER_TESTING_interpreter_get_context (ms->is), + TALER_TESTING_get_exchange_url (ms->is), + TALER_TESTING_get_keys (ms->is), + &ms->rms, + &ms->melt_input, &melt_cb, - rms); - rms->double_melt = false; + ms); + ms->double_melt = false; return; } - TALER_TESTING_interpreter_next (rms->is); + TALER_TESTING_interpreter_next (ms->is); } @@ -943,7 +939,7 @@ melt_run (void *cls, else rms->che.details.melt.no_hac = true; - rms->mh = TALER_EXCHANGE_melt_v27 ( + rms->mh = TALER_EXCHANGE_melt ( TALER_TESTING_interpreter_get_context (is), TALER_TESTING_get_exchange_url (is), TALER_TESTING_get_keys (is), @@ -980,7 +976,7 @@ melt_cleanup (void *cls, { TALER_TESTING_command_incomplete (rms->is, cmd->label); - TALER_EXCHANGE_melt_v27_cancel (rms->mh); + TALER_EXCHANGE_melt_cancel (rms->mh); rms->mh = NULL; } if (NULL != rms->retry_task) @@ -1043,7 +1039,7 @@ melt_traits (void *cls, TALER_TESTING_make_trait_h_age_commitment ( index, rms->melt_input.melt_h_age_commitment), - TALER_TESTING_make_trait_refresh_secret (&rms->rms), + TALER_TESTING_make_trait_refresh_seed (&rms->rms), (NULL != rms->reveal_melt_input.blinding_values) ? TALER_TESTING_make_trait_exchange_blinding_values ( index, diff --git a/src/util/crypto.c b/src/util/crypto.c @@ -221,7 +221,7 @@ TALER_withdraw_master_seed_setup_random ( void TALER_refresh_master_setup_random ( - struct TALER_RefreshMasterSecretP *rms) + struct TALER_PublicRefreshMasterSeedP *rms) { GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_STRONG, rms, @@ -251,30 +251,6 @@ TALER_transfer_secret_to_planchet_secret ( void -TALER_planchet_secret_to_transfer_priv ( - const struct TALER_RefreshMasterSecretP *rms, - const struct TALER_CoinSpendPrivateKeyP *old_coin_priv, - uint32_t cnc_num, - struct TALER_TransferPrivateKeyP *tpriv) -{ - uint32_t be_salt = htonl (cnc_num); - - GNUNET_assert (GNUNET_OK == - GNUNET_CRYPTO_kdf (tpriv, - sizeof (*tpriv), - &be_salt, - sizeof (be_salt), - old_coin_priv, - sizeof (*old_coin_priv), - rms, - sizeof (*rms), - "taler-transfer-priv-derivation", - strlen ("taler-transfer-priv-derivation"), - NULL, 0)); -} - - -void TALER_cs_withdraw_nonce_derive ( const struct TALER_PlanchetMasterSecretP *ps, struct GNUNET_CRYPTO_CsSessionNonce *nonce) @@ -309,8 +285,9 @@ TALER_cs_withdraw_seed_to_blinding_seed ( void -TALER_cs_refresh_secret_to_blinding_seed ( - const struct TALER_RefreshMasterSecretP *secret, +TALER_cs_refresh_seed_to_blinding_seed ( + const struct TALER_PublicRefreshMasterSeedP *seed, + const struct TALER_CoinSpendPrivateKeyP *coin_priv, struct TALER_BlindingMasterSeedP *blinding_seed) { GNUNET_assert (GNUNET_YES == @@ -318,8 +295,10 @@ TALER_cs_refresh_secret_to_blinding_seed ( sizeof (*blinding_seed), "refresh-blinding", strlen ("refresh-blinding"), - secret, - sizeof(*secret), + coin_priv, + sizeof(*coin_priv), + seed, + sizeof(*seed), NULL, 0)); } @@ -387,28 +366,6 @@ TALER_cs_derive_only_cs_blind_nonces_from_seed ( } -void -TALER_cs_refresh_nonce_derive ( - const struct TALER_RefreshMasterSecretP *rms, - uint32_t coin_num_salt, - struct GNUNET_CRYPTO_CsSessionNonce *nonce) -{ - uint32_t be_salt = htonl (coin_num_salt); - - GNUNET_assert (GNUNET_YES == - GNUNET_CRYPTO_kdf (nonce, - sizeof (*nonce), - &be_salt, - sizeof (be_salt), - "refresh-n", - strlen ("refresh-n"), - rms, - sizeof(*rms), - NULL, - 0)); -} - - bool TALER_cs_mark_indices ( size_t num, @@ -568,83 +525,11 @@ TALER_planchet_to_coin ( void -TALER_refresh_get_commitment (struct TALER_RefreshCommitmentP *rc, - uint32_t kappa, - const struct TALER_RefreshMasterSecretP *rms, - uint32_t num_new_coins, - const struct TALER_RefreshCommitmentEntry *rcs, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_Amount *amount_with_fee) -{ - struct GNUNET_HashContext *hash_context; - - hash_context = GNUNET_CRYPTO_hash_context_start (); - if (NULL != rms) - GNUNET_CRYPTO_hash_context_read (hash_context, - rms, - sizeof (*rms)); - /* first, iterate over transfer public keys for hash_context */ - for (unsigned int i = 0; i<kappa; i++) - { - GNUNET_CRYPTO_hash_context_read (hash_context, - &rcs[i].transfer_pub, - sizeof (struct TALER_TransferPublicKeyP)); - } - /* next, add all of the hashes from the denomination keys to the - hash_context */ - for (unsigned int i = 0; i<num_new_coins; i++) - { - struct TALER_DenominationHashP denom_hash; - - /* The denomination keys should / must all be identical regardless - of what offset we use, so we use [0]. */ - GNUNET_assert (kappa > 0); /* sanity check */ - TALER_denom_pub_hash (rcs[0].new_coins[i].dk, - &denom_hash); - GNUNET_CRYPTO_hash_context_read (hash_context, - &denom_hash, - sizeof (denom_hash)); - } - - /* next, add public key of coin and amount being refreshed */ - { - struct TALER_AmountNBO melt_amountn; - - GNUNET_CRYPTO_hash_context_read (hash_context, - coin_pub, - sizeof (struct TALER_CoinSpendPublicKeyP)); - TALER_amount_hton (&melt_amountn, - amount_with_fee); - GNUNET_CRYPTO_hash_context_read (hash_context, - &melt_amountn, - sizeof (struct TALER_AmountNBO)); - } - - /* finally, add all the envelopes */ - for (unsigned int i = 0; i<kappa; i++) - { - const struct TALER_RefreshCommitmentEntry *rce = &rcs[i]; - - for (unsigned int j = 0; j<num_new_coins; j++) - { - const struct TALER_RefreshCoinData *rcd = &rce->new_coins[j]; - - TALER_blinded_planchet_hash_ (&rcd->blinded_planchet, - hash_context); - } - } - - /* Conclude */ - GNUNET_CRYPTO_hash_context_finish (hash_context, - &rc->session_hash); -} - - -void -TALER_refresh_get_commitment_v27 ( +TALER_refresh_get_commitment ( struct TALER_RefreshCommitmentP *rc, const struct TALER_PublicRefreshMasterSeedP *refresh_seed, const struct TALER_BlindingMasterSeedP *blinding_seed, + const struct TALER_KappaTransferPublicKeys *k_tpbs, const struct TALER_KappaHashBlindedPlanchetsP *k_bps_h, const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_Amount *amount_with_fee) @@ -701,23 +586,133 @@ TALER_refresh_get_commitment_v27 ( void -TALER_refresh_master_secret_to_refresh_seed ( - const struct TALER_RefreshMasterSecretP *rms, - struct TALER_PublicRefreshMasterSeedP *r_seed) +TALER_refresh_expand_seed_to_kappa_batch_seeds ( + const struct TALER_PublicRefreshMasterSeedP *refresh_master_seed, + const struct TALER_CoinSpendPrivateKeyP *coin_priv, + struct TALER_KappaPrivateRefreshBatchSeedsP *kappa_batch_seeds) { GNUNET_assert (GNUNET_OK == - GNUNET_CRYPTO_kdf (r_seed, - sizeof (*r_seed), - "refresh-seed", - strlen ("refresh-seed"), - rms, - sizeof (*rms), + GNUNET_CRYPTO_kdf (kappa_batch_seeds, + sizeof (*kappa_batch_seeds), + "refresh-batch-seeds", + strlen ("refresh-batch-seeds"), + refresh_master_seed, + sizeof (*refresh_master_seed), + coin_priv, + sizeof(*coin_priv), NULL, 0)); } void -TALER_refresh_expand_kappa_nonces ( +TALER_refresh_expand_batch_seed_to_transfer_private_keys ( + const struct TALER_PrivateRefreshBatchSeedP *batch_seed, + size_t num_transfer_pks, + struct TALER_TransferPrivateKeyP transfer_pks[num_transfer_pks]) +{ + GNUNET_assert (GNUNET_OK == + GNUNET_CRYPTO_kdf ( + transfer_pks, + sizeof (*transfer_pks) * num_transfer_pks, + "refresh-transfer-private-keys", + strlen ("refresh-transfer-private-keys"), + batch_seed, + sizeof (*batch_seed), + NULL, 0)); +} + + +void +TALER_refresh_expand_batch_seed_to_transfer_secrets ( + const struct TALER_PrivateRefreshBatchSeedP *batch_seed, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + size_t num_transfer_secrets, + struct TALER_TransferSecretP transfer_secrets[num_transfer_secrets]) +{ + struct TALER_TransferPrivateKeyP transfer_pks[num_transfer_secrets]; + + TALER_refresh_expand_batch_seed_to_transfer_private_keys ( + batch_seed, + num_transfer_secrets, + transfer_pks); + + for (size_t i = 0; i < num_transfer_secrets; i++) + { + TALER_link_reveal_transfer_secret ( + &transfer_pks[i], + coin_pub, + &transfer_secrets[i]); + } +} + + +void +TALER_refresh_expand_batch_seed_to_planchet_master_secrets ( + const struct TALER_PrivateRefreshBatchSeedP *batch_seed, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + size_t num_planchet_secrets, + struct TALER_PlanchetMasterSecretP planchet_secrets[num_planchet_secrets]) +{ + struct TALER_TransferPrivateKeyP transfer_pks[num_planchet_secrets]; + struct TALER_TransferSecretP transfer_secrets[num_planchet_secrets]; + + TALER_refresh_expand_batch_seed_to_transfer_private_keys ( + batch_seed, + num_planchet_secrets, + transfer_pks); + + for (size_t i = 0; i < num_planchet_secrets; i++) + { + TALER_link_reveal_transfer_secret ( + &transfer_pks[i], + coin_pub, + &transfer_secrets[i]); + + TALER_transfer_secret_to_planchet_secret ( + &transfer_secrets[i], + i, + &planchet_secrets[i]); + } +} + + +void +TALER_refresh_expand_batch_seed_to_transfer_data ( + const struct TALER_PrivateRefreshBatchSeedP *batch_seed, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + size_t num, + struct TALER_PlanchetMasterSecretP planchet_secrets[num], + struct TALER_TransferPublicKeyP transfer_pubs[num]) +{ + struct TALER_TransferPrivateKeyP transfer_pks[num]; + struct TALER_TransferSecretP transfer_secrets[num]; + + TALER_refresh_expand_batch_seed_to_transfer_private_keys ( + batch_seed, + num, + transfer_pks); + + for (size_t i = 0; i < num; i++) + { + TALER_link_reveal_transfer_secret ( + &transfer_pks[i], + coin_pub, + &transfer_secrets[i]); + + TALER_transfer_secret_to_planchet_secret ( + &transfer_secrets[i], + i, + &planchet_secrets[i]); + + GNUNET_CRYPTO_ecdhe_key_get_public ( + &transfer_pks[i].ecdhe_priv, + &transfer_pubs[i].ecdhe_pub); + } +} + + +void +TALER_refresh_expand_kappa_nonces_v27 ( const struct TALER_PublicRefreshMasterSeedP *refresh_seed, struct TALER_KappaPublicRefreshNoncesP *kappa_nonces) { @@ -733,7 +728,7 @@ TALER_refresh_expand_kappa_nonces ( void -TALER_refresh_signature_to_secrets ( +TALER_refresh_signature_to_secrets_v27 ( const struct TALER_PrivateRefreshNonceSignatureP *sig, size_t num_secrets, struct TALER_PlanchetMasterSecretP secrets[static num_secrets])