exchange

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

commit 44740603715c03d7b5af1bec3fe3f404525b7525
parent 1f66a0f72638f071dd1a30cc617db87f8b8dabef
Author: Özgür Kesim <oec@codeblau.de>
Date:   Mon, 14 Sep 2026 20:47:08 +0200

exchange: batch recoup endpoints /recoup-withdraw and /recoup-refresh

Implements the vRECOUP protocol (#9828):

A request identifies one withdraw operation (reserve_pub, planchets_h)
or one refresh operation (old_coin_pub, rc) and carries one coin_data
entry per coin the exchange signed: either the hash of the blinded
envelope or the disclosed coin (coin_pub, denom_pub_h, denom_sig,
coin_blinding_secret, h_age_commitment, coin_sig).

The exchange re-blinds the disclosed coins with the blinding seed
and CS R-values it recorded, recomputes the hash over the batch
and compares it with the commitment it signed (planchets_h,or selected_h
for age-restricted withdraws and for refreshes), then credits the residual
value of every disclosed coin in one transaction and answers with one
batch confirmation signature.

Both handlers share the parsing, key checks, re-blinding and summary in
taler-exchange-httpd_common_recoup.c.  The old per-coin handlers and the
dead lookups get_reserve_by_h_planchets() and get_old_coin_by_h_blind()
are removed; get_withdraw()/get_refresh() serve the lookups now.

The coin and reserve history entries carry the new fields of the
specification: coin_blinding_secret, planchets_h resp. rc, coin_index
and h_denom_pub, removing the FIXME-9828 markers.

Diffstat:
Msrc/auditor/taler-auditor-sync.c | 2--
Msrc/exchange/meson.build | 1+
Msrc/exchange/taler-exchange-httpd.c | 24++++++++++++++----------
Asrc/exchange/taler-exchange-httpd_common_recoup.c | 472+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchange/taler-exchange-httpd_common_recoup.h | 313+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/exchange/taler-exchange-httpd_get-coins-COIN_PUB-history.c | 33++++++++++++++-------------------
Msrc/exchange/taler-exchange-httpd_get-reserves-RESERVE_PUB-history.c | 4+++-
Msrc/exchange/taler-exchange-httpd_post-recoup-refresh.c | 1106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/exchange/taler-exchange-httpd_post-recoup-refresh.h | 24++++++++++++------------
Msrc/exchange/taler-exchange-httpd_post-recoup-withdraw.c | 1118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/exchange/taler-exchange-httpd_post-recoup-withdraw.h | 26+++++++++++++-------------
Asrc/exchangedb/get_denomination_pub_by_serial.c | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/exchangedb/get_old_coin_by_h_blind.c | 60------------------------------------------------------------
Dsrc/exchangedb/get_reserve_by_h_planchets.c | 58----------------------------------------------------------
Msrc/exchangedb/meson.build | 3+--
Msrc/exchangedb/test_denominations.c | 21+++++++++++++++++++++
Msrc/exchangedb/test_refresh.c | 41+----------------------------------------
Msrc/exchangedb/test_withdraw.c | 28++++++----------------------
Asrc/include/exchange-database/get_denomination_pub_by_serial.h | 47+++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/include/exchange-database/get_old_coin_by_h_blind.h | 48------------------------------------------------
Dsrc/include/exchange-database/get_reserve_by_h_planchets.h | 51---------------------------------------------------
21 files changed, 2584 insertions(+), 953 deletions(-)

diff --git a/src/auditor/taler-auditor-sync.c b/src/auditor/taler-auditor-sync.c @@ -69,14 +69,12 @@ #include "exchange-database/iterate_global_fees.h" #include "exchange-database/get_known_coin.h" #include "exchange-database/get_kyc_rules.h" -#include "exchange-database/get_old_coin_by_h_blind.h" #include "exchange-database/get_pending_legitimization_process.h" #include "exchange-database/get_purse_deposit.h" #include "exchange-database/get_purse_request.h" #include "exchange-database/get_ready_deposit.h" #include "exchange-database/get_refresh.h" #include "exchange-database/get_reserve_balance.h" -#include "exchange-database/get_reserve_by_h_planchets.h" #include "exchange-database/get_reserve_history.h" #include "exchange-database/get_signature_for_known_coin.h" #include "exchange-database/iterate_unfinished_close_requests.h" diff --git a/src/exchange/meson.build b/src/exchange/meson.build @@ -213,6 +213,7 @@ taler_exchange_httpd_SOURCES = [ 'taler-exchange-httpd_delete-purses-PURSE_PUB.c', 'taler-exchange-httpd_get-purses-PURSE_PUB-merge.c', 'taler-exchange-httpd_post-purses-PURSE_PUB-merge.c', + 'taler-exchange-httpd_common_recoup.c', 'taler-exchange-httpd_post-recoup-withdraw.c', 'taler-exchange-httpd_post-recoup-refresh.c', 'taler-exchange-httpd_post-coins-COIN_PUB-refund.c', diff --git a/src/exchange/taler-exchange-httpd.c b/src/exchange/taler-exchange-httpd.c @@ -329,16 +329,6 @@ handle_post_coins (struct TEH_RequestContext *rc, CoinOpHandler handler; } h[] = { -#if FIXME_9828 - { - .op = "recoup", - .handler = &TEH_handler_recoup - }, - { - .op = "recoup-refresh", - .handler = &TEH_handler_recoup_refresh - }, -#endif { .op = "refund", .handler = &TEH_handler_refund @@ -1735,6 +1725,20 @@ handle_mhd_request (void *cls, .handler.post = &TEH_handler_withdraw, .nargs = 0 }, + /* batch recoup of withdrawn coins, introduced with vRECOUP */ + { + .url = "recoup-withdraw", + .method = MHD_HTTP_METHOD_POST, + .handler.post = &TEH_handler_recoup_withdraw, + .nargs = 0 + }, + /* batch recoup of refreshed coins, introduced with vRECOUP */ + { + .url = "recoup-refresh", + .method = MHD_HTTP_METHOD_POST, + .handler.post = &TEH_handler_recoup_refresh, + .nargs = 0 + }, { .url = "reserves", .method = MHD_HTTP_METHOD_GET, diff --git a/src/exchange/taler-exchange-httpd_common_recoup.c b/src/exchange/taler-exchange-httpd_common_recoup.c @@ -0,0 +1,472 @@ +/* + This file is part of TALER + Copyright (C) 2026 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_common_recoup.c + * @brief logic shared by the /recoup-withdraw and /recoup-refresh handlers + * @author Özgür Kesim + */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd_common_recoup.h" +#include "exchange-database/get_denomination_pub_by_serial.h" +#include "taler-exchange-httpd_get-metrics.h" + + +enum GNUNET_GenericReturnValue +TEH_recoup_parse_coin_data ( + struct MHD_Connection *connection, + const json_t *j_coin_data, + size_t *num_coins, + struct TEH_RecoupCoin **coins, + const char **hint, + enum MHD_Result *mret) +{ + size_t num = json_array_size (j_coin_data); + struct TEH_RecoupCoin *cs; + size_t num_disclosed = 0; + size_t idx; + json_t *j_entry; + + *coins = NULL; + *num_coins = 0; + if (0 == num) + { + GNUNET_break_op (0); + *hint = "coin_data must not be empty"; + return GNUNET_SYSERR; + } + if (num > TALER_MAX_COINS) + { + GNUNET_break_op (0); + *hint = "coin_data exceeds the maximum number of coins"; + return GNUNET_SYSERR; + } + cs = GNUNET_new_array (num, + struct TEH_RecoupCoin); + json_array_foreach (j_coin_data, idx, j_entry) + { + struct TEH_RecoupCoin *c = &cs[idx]; + const char *type; + struct GNUNET_JSON_Specification tspec[] = { + GNUNET_JSON_spec_string ("type", + &type), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + j_entry, + tspec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + *mret = (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; + TEH_recoup_free_coins (num, + cs); + return GNUNET_NO; + } + if (0 == strcmp ("hash", + type)) + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("h_coin_ev", + &c->h_coin_ev), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (connection, + j_entry, + spec); + } + else if (0 == strcmp ("recoup", + type)) + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &c->coin.coin_pub), + GNUNET_JSON_spec_fixed_auto ("denom_pub_h", + &c->coin.denom_pub_hash), + TALER_JSON_spec_denom_sig ("denom_sig", + &c->coin.denom_sig), + GNUNET_JSON_spec_fixed_auto ("coin_blinding_secret", + &c->coin_blinding_secret), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("h_age_commitment", + &c->coin.h_age_commitment), + &c->coin.no_age_commitment), + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &c->coin_sig), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (connection, + j_entry, + spec); + if (GNUNET_OK == res) + { + c->disclosed = true; + num_disclosed++; + } + } + else + { + GNUNET_break_op (0); + *hint = "coin_data entry has unknown type"; + TEH_recoup_free_coins (num, + cs); + return GNUNET_SYSERR; + } + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + *mret = (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; + TEH_recoup_free_coins (num, + cs); + return GNUNET_NO; + } + } + if (0 == num_disclosed) + { + GNUNET_break_op (0); + *hint = "coin_data must disclose at least one coin"; + TEH_recoup_free_coins (num, + cs); + return GNUNET_SYSERR; + } + *num_coins = num; + *coins = cs; + return GNUNET_OK; +} + + +void +TEH_recoup_free_coins (size_t num_coins, + struct TEH_RecoupCoin *coins) +{ + if (NULL == coins) + return; + for (size_t i = 0; i < num_coins; i++) + if (coins[i].disclosed) + TALER_denom_sig_free (&coins[i].coin.denom_sig); + GNUNET_free (coins); +} + + +enum TEH_RecoupError +TEH_recoup_check_keys ( + size_t num_coins, + struct TEH_RecoupCoin coins[static num_coins], + const uint64_t denom_serials[static num_coins], + bool have_blinding_seed, + union TEH_RecoupErrorDetails *details) +{ + struct TEH_KeyStateHandle *ksh; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + GNUNET_break (0); + return TEH_RECOUP_ERROR_KEYS_MISSING; + } + for (size_t i = 0; i < num_coins; i++) + { + struct TEH_RecoupCoin *c = &coins[i]; + struct TEH_DenominationKey *dk; + + /* The denomination recorded for this position; we need its + cipher for every coin to know which of them are CS coins. */ + dk = TEH_keys_denomination_by_serial_from_state (ksh, + denom_serials[i]); + if (NULL != dk) + { + c->dk = dk; + c->cipher = dk->denom_pub.bsign_pub_key->cipher; + } + else + { + /* Not in the key state: an undisclosed coin's denomination may + have expired since the original operation (the key state drops + expired denominations), which must not block the recoup of the + other coins. Get the cipher from the database instead. */ + struct TALER_DenominationPublicKey denom_pub; + enum GNUNET_DB_QueryStatus qs; + + if (c->disclosed) + { + GNUNET_break_op (0); + details->denom_h = &c->coin.denom_pub_hash; + return TEH_RECOUP_ERROR_DENOMINATION_KEY_UNKNOWN; + } + qs = TALER_EXCHANGEDB_get_denomination_pub_by_serial ( + TEH_pg, + denom_serials[i], + &denom_pub); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + GNUNET_break (0); + return TEH_RECOUP_ERROR_KEYS_MISSING; + } + c->dk = NULL; + c->cipher = denom_pub.bsign_pub_key->cipher; + TALER_denom_pub_free (&denom_pub); + } + if (! c->disclosed) + continue; + dk = TEH_keys_denomination_by_hash_from_state (ksh, + &c->coin.denom_pub_hash, + NULL, + NULL); + details->denom_h = &c->coin.denom_pub_hash; + if (NULL == dk) + { + GNUNET_break_op (0); + return TEH_RECOUP_ERROR_DENOMINATION_KEY_UNKNOWN; + } + if (dk->meta.serial != denom_serials[i]) + { + GNUNET_break_op (0); + return TEH_RECOUP_ERROR_DENOMINATION_MISMATCH; + } + if (! dk->recoup_possible) + { + GNUNET_break_op (0); + return TEH_RECOUP_ERROR_DENOMINATION_NOT_ELIGIBLE; + } + if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time)) + { + GNUNET_break_op (0); + return TEH_RECOUP_ERROR_DENOMINATION_EXPIRED; + } + if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) + { + GNUNET_break_op (0); + return TEH_RECOUP_ERROR_DENOMINATION_VALIDITY_IN_FUTURE; + } + if ( (0 != dk->denom_pub.age_mask.bits) == + c->coin.no_age_commitment) + { + GNUNET_break_op (0); + details->hint = + "h_age_commitment must be given if and only if the denomination supports age restriction"; + return TEH_RECOUP_ERROR_REQUEST_PARAMETER_MALFORMED; + } + if ( (GNUNET_CRYPTO_BSA_CS == dk->denom_pub.bsign_pub_key->cipher) && + (! have_blinding_seed) ) + { + /* A CS coin cannot have been signed without a blinding seed. */ + GNUNET_break (0); + return TEH_RECOUP_ERROR_DB_INVARIANT_FAILURE; + } + } + return TEH_RECOUP_ERROR_NONE; +} + + +enum TEH_RecoupError +TEH_recoup_verify_coins ( + size_t num_coins, + struct TEH_RecoupCoin coins[static num_coins], + bool for_melt, + const struct TALER_BlindingMasterSeedP *blinding_seed, + size_t num_cs_r_values, + const struct GNUNET_CRYPTO_CSPublicRPairP *cs_r_values, + const struct TALER_HashBlindedPlanchetsP *expected, + union TEH_RecoupErrorDetails *details) +{ + struct GNUNET_HashContext *hc; + struct TALER_HashBlindedPlanchetsP computed; + size_t cs_idx = 0; + + hc = GNUNET_CRYPTO_hash_context_start (); + GNUNET_assert (NULL != hc); + for (size_t i = 0; i < num_coins; i++) + { + struct TEH_RecoupCoin *c = &coins[i]; + const struct TEH_DenominationKey *dk = c->dk; + const bool is_cs + = (GNUNET_CRYPTO_BSA_CS == c->cipher); + + if (c->disclosed) + { + struct GNUNET_CRYPTO_BlindingInputValues bi = { + .cipher = dk->denom_pub.bsign_pub_key->cipher + }; + struct TALER_ExchangeBlindingValues ev = { + .blinding_inputs = &bi + }; + union GNUNET_CRYPTO_BlindSessionNonce nonce; + const union GNUNET_CRYPTO_BlindSessionNonce *np = NULL; + struct TALER_CoinPubHashP c_hash; + struct TALER_BlindedPlanchet bp; + enum GNUNET_GenericReturnValue ret; + + details->denom_h = &c->coin.denom_pub_hash; + switch (bi.cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; + break; + case GNUNET_CRYPTO_BSA_CS: + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; + break; + default: + break; + } + if (GNUNET_YES != + TALER_test_coin_valid (&c->coin, + &dk->denom_pub)) + { + GNUNET_break_op (0); + GNUNET_CRYPTO_hash_context_abort (hc); + return TEH_RECOUP_ERROR_DENOMINATION_SIGNATURE_INVALID; + } + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + ret = for_melt + ? TALER_wallet_recoup_refresh_verify (&c->coin.denom_pub_hash, + &c->coin_blinding_secret, + &c->coin.coin_pub, + &c->coin_sig) + : TALER_wallet_recoup_verify (&c->coin.denom_pub_hash, + &c->coin_blinding_secret, + &c->coin.coin_pub, + &c->coin_sig); + if (GNUNET_OK != ret) + { + GNUNET_break_op (0); + GNUNET_CRYPTO_hash_context_abort (hc); + return TEH_RECOUP_ERROR_COIN_SIGNATURE_INVALID; + } + if (is_cs) + { + if ( (NULL == blinding_seed) || + (cs_idx >= num_cs_r_values) ) + { + GNUNET_break (0); + GNUNET_CRYPTO_hash_context_abort (hc); + return TEH_RECOUP_ERROR_DB_INVARIANT_FAILURE; + } + bi.details.cs_values = cs_r_values[cs_idx]; + TALER_cs_nonce_derive_indexed (blinding_seed, + for_melt, + (uint32_t) i, + &nonce.cs_nonce); + np = &nonce; + } + if (GNUNET_OK != + TALER_denom_blind (&dk->denom_pub, + &c->coin_blinding_secret, + np, + c->coin.no_age_commitment + ? NULL + : &c->coin.h_age_commitment, + &c->coin.coin_pub, + &ev, + &c_hash, + &bp)) + { + GNUNET_break (0); + GNUNET_CRYPTO_hash_context_abort (hc); + return TEH_RECOUP_ERROR_BLINDING_FAILED; + } + TALER_coin_ev_hash (&bp, + &c->coin.denom_pub_hash, + &c->h_coin_ev); + TALER_blinded_planchet_free (&bp); + } + if (is_cs) + cs_idx++; + GNUNET_CRYPTO_hash_context_read (hc, + &c->h_coin_ev, + sizeof (c->h_coin_ev)); + } + GNUNET_CRYPTO_hash_context_finish (hc, + &computed.hash); + if (0 != + GNUNET_CRYPTO_hash_cmp (&computed.hash, + &expected->hash)) + { + GNUNET_break_op (0); + return TEH_RECOUP_ERROR_COMMITMENT_MISMATCH; + } + return TEH_RECOUP_ERROR_NONE; +} + + +enum GNUNET_GenericReturnValue +TEH_recoup_summarize ( + size_t num_coins, + const struct TEH_RecoupCoin coins[static num_coins], + struct TALER_Amount *total_amount, + struct GNUNET_HashCode *h_recoups, + struct GNUNET_TIME_Timestamp *timestamp, + json_t **recoups) +{ + struct TALER_RecoupedCoin *rcs; + size_t num = 0; + json_t *arr; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + total_amount)); + *timestamp = GNUNET_TIME_UNIT_ZERO_TS; + rcs = GNUNET_new_array (num_coins, + struct TALER_RecoupedCoin); + arr = json_array (); + GNUNET_assert (NULL != arr); + for (size_t i = 0; i < num_coins; i++) + { + const struct TEH_RecoupCoin *c = &coins[i]; + + if (! c->disclosed) + continue; + if (0 > + TALER_amount_add (total_amount, + total_amount, + &c->amount)) + { + GNUNET_break (0); + GNUNET_free (rcs); + json_decref (arr); + return GNUNET_SYSERR; + } + rcs[num].coin_pub = c->coin.coin_pub; + rcs[num].amount = c->amount; + num++; + *timestamp = GNUNET_TIME_timestamp_max (*timestamp, + c->timestamp); + GNUNET_assert ( + 0 == + json_array_append_new ( + arr, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("coin_pub", + &c->coin.coin_pub), + TALER_JSON_pack_amount ("amount", + &c->amount)))); + } + TALER_recoup_batch_hash (num, + rcs, + h_recoups); + GNUNET_free (rcs); + *recoups = arr; + return GNUNET_OK; +} + + +/* end of taler-exchange-httpd_common_recoup.c */ diff --git a/src/exchange/taler-exchange-httpd_common_recoup.h b/src/exchange/taler-exchange-httpd_common_recoup.h @@ -0,0 +1,313 @@ +/* + This file is part of TALER + Copyright (C) 2026 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_common_recoup.h + * @brief logic shared by the /recoup-withdraw and /recoup-refresh handlers: + * parsing the coin_data array, checking the disclosed coins against + * the denominations recorded for the original operation, and + * recomputing the hash over the batch the exchange signed + * @author Özgür Kesim + */ +#ifndef TALER_EXCHANGE_HTTPD_COMMON_RECOUP_H +#define TALER_EXCHANGE_HTTPD_COMMON_RECOUP_H + +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler/taler_util.h" +#include "taler-exchange-httpd.h" +#include "taler-exchange-httpd_get-keys.h" + + +/** + * One entry of the ``coin_data`` array of a recoup request. + */ +struct TEH_RecoupCoin +{ + + /** + * True if the client disclosed the coin (type "recoup"), + * false if it only provided the hash of the blinded envelope + * (type "hash"). + */ + bool disclosed; + + /** + * Hash of the blinded envelope of the coin: provided by the + * client if the coin is not @e disclosed, recomputed by + * #TEH_recoup_verify_coins() otherwise. + */ + struct TALER_BlindedCoinHashP h_coin_ev; + + /** + * Public information about the coin, including the denomination + * signature. Only valid if @e disclosed. + */ + struct TALER_CoinPublicInfo coin; + + /** + * Blinding secret used when the coin was blinded in the original + * operation. Only valid if @e disclosed. + */ + union GNUNET_CRYPTO_BlindingSecretP coin_blinding_secret; + + /** + * Signature by the coin authorizing the recoup. + * Only valid if @e disclosed. + */ + struct TALER_CoinSpendSignatureP coin_sig; + + /** + * Denomination of the coin, as recorded for this position of the + * original operation. Set by #TEH_recoup_check_keys(). Only + * valid if @e disclosed: for the other coins the denomination may + * have expired and left the key state, see @e cipher. + */ + struct TEH_DenominationKey *dk; + + /** + * Cipher of the denomination recorded for this position of the + * original operation. Set by #TEH_recoup_check_keys() for every + * coin, disclosed or not. + */ + enum GNUNET_CRYPTO_BlindSignatureAlgorithm cipher; + + /** + * Row of the coin in the known_coins table. Set by the handler + * once the coin was made known. Only valid if @e disclosed. + */ + uint64_t known_coin_id; + + /** + * Amount the recoup credited for this coin. Set by the handler's + * database transaction. Only valid if @e disclosed. + */ + struct TALER_Amount amount; + + /** + * When the exchange accepted the recoup of this coin. Set by the + * handler's database transaction. Only valid if @e disclosed. + */ + struct GNUNET_TIME_Timestamp timestamp; + +}; + + +/** + * Errors the shared logic can report. The handlers map them to + * their endpoint-specific HTTP responses. + */ +enum TEH_RecoupError +{ + TEH_RECOUP_ERROR_NONE, + + /** + * The request is malformed; @a details is a hint for the client. + */ + TEH_RECOUP_ERROR_REQUEST_PARAMETER_MALFORMED, + + /** + * The exchange has no key state. + */ + TEH_RECOUP_ERROR_KEYS_MISSING, + + /** + * A disclosed coin's denomination is unknown; @a details is the hash. + */ + TEH_RECOUP_ERROR_DENOMINATION_KEY_UNKNOWN, + + /** + * A disclosed coin's denomination is not the one recorded for + * its position; @a details is the hash. + */ + TEH_RECOUP_ERROR_DENOMINATION_MISMATCH, + + /** + * A disclosed coin's denomination was not revoked; @a details is the hash. + */ + TEH_RECOUP_ERROR_DENOMINATION_NOT_ELIGIBLE, + + /** + * A disclosed coin's denomination is past its deposit expiration; + * @a details is the hash. + */ + TEH_RECOUP_ERROR_DENOMINATION_EXPIRED, + + /** + * A disclosed coin's denomination is not yet valid; @a details is the hash. + */ + TEH_RECOUP_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + + /** + * The database record of the operation is inconsistent with + * the denominations (e.g. CS denomination without blinding seed). + */ + TEH_RECOUP_ERROR_DB_INVARIANT_FAILURE, + + /** + * A disclosed coin's denomination signature is invalid. + */ + TEH_RECOUP_ERROR_DENOMINATION_SIGNATURE_INVALID, + + /** + * A disclosed coin's recoup signature is invalid. + */ + TEH_RECOUP_ERROR_COIN_SIGNATURE_INVALID, + + /** + * Re-blinding a disclosed coin failed. + */ + TEH_RECOUP_ERROR_BLINDING_FAILED, + + /** + * The hash over the batch does not match the commitment. + */ + TEH_RECOUP_ERROR_COMMITMENT_MISMATCH, +}; + + +/** + * Details accompanying a #TEH_RecoupError. + */ +union TEH_RecoupErrorDetails +{ + /** + * For #TEH_RECOUP_ERROR_REQUEST_PARAMETER_MALFORMED. + */ + const char *hint; + + /** + * For the denomination-related errors. + */ + const struct TALER_DenominationHashP *denom_h; +}; + + +/** + * Parse the ``coin_data`` array of a recoup request. + * + * @param connection connection to report parse errors on + * @param j_coin_data the JSON array + * @param[out] num_coins set to the number of entries + * @param[out] coins set to an array of @a num_coins parsed entries; + * to be released with #TEH_recoup_free_coins() + * @param[out] hint set to a hint for the client on #GNUNET_SYSERR + * @param[out] mret set to the MHD result on #GNUNET_NO + * @return #GNUNET_OK on success and @a coins set; + * #GNUNET_SYSERR if the array has the wrong shape, with + * @a hint set and no reply queued yet; + * #GNUNET_NO if a parse error was already reported to the + * client and @a mret is set + */ +enum GNUNET_GenericReturnValue +TEH_recoup_parse_coin_data ( + struct MHD_Connection *connection, + const json_t *j_coin_data, + size_t *num_coins, + struct TEH_RecoupCoin **coins, + const char **hint, + enum MHD_Result *mret); + + +/** + * Release the memory held by @a coins. + * + * @param num_coins length of @a coins + * @param[in] coins array to release, may be NULL + */ +void +TEH_recoup_free_coins (size_t num_coins, + struct TEH_RecoupCoin *coins); + + +/** + * Check the denominations of the batch: find the denomination + * recorded for every position in @a denom_serials, and for every + * disclosed coin check that it names that denomination and that the + * denomination is eligible for recoup. + * + * @param num_coins length of @a coins and @a denom_serials + * @param[in,out] coins the batch, @e dk is set on success + * @param denom_serials denomination serials recorded for the operation + * @param have_blinding_seed true if the operation recorded a blinding seed + * @param[out] details set on error + * @return #TEH_RECOUP_ERROR_NONE on success + */ +enum TEH_RecoupError +TEH_recoup_check_keys ( + size_t num_coins, + struct TEH_RecoupCoin coins[static num_coins], + const uint64_t denom_serials[static num_coins], + bool have_blinding_seed, + union TEH_RecoupErrorDetails *details); + + +/** + * Verify the disclosed coins of the batch and recompute the hash + * the exchange committed to: checks denomination and recoup + * signatures, re-blinds every disclosed coin from its secrets and the + * values recorded for the operation, and hashes the batch. + * Must be called after #TEH_recoup_check_keys(). + * + * @param num_coins length of @a coins + * @param[in,out] coins the batch, @e h_coin_ev is set for disclosed coins + * @param for_melt true if the operation was a refresh (affects the + * derivation of the CS nonces and the signature purpose) + * @param blinding_seed blinding seed recorded for the operation, + * NULL if none + * @param num_cs_r_values length of @a cs_r_values + * @param cs_r_values R-value pairs recorded for the CS coins of the + * operation, in the order of the coins + * @param expected hash the exchange committed to for the signed batch + * @param[out] details set on error + * @return #TEH_RECOUP_ERROR_NONE if all coins verify and the batch + * hash matches @a expected + */ +enum TEH_RecoupError +TEH_recoup_verify_coins ( + size_t num_coins, + struct TEH_RecoupCoin coins[static num_coins], + bool for_melt, + const struct TALER_BlindingMasterSeedP *blinding_seed, + size_t num_cs_r_values, + const struct GNUNET_CRYPTO_CSPublicRPairP *cs_r_values, + const struct TALER_HashBlindedPlanchetsP *expected, + union TEH_RecoupErrorDetails *details); + + +/** + * Compute the total amount and the hash over the disclosed coins of + * @a coins for the batch confirmation signature. Must be called after + * the handler's transaction set @e amount for every disclosed coin. + * + * @param num_coins length of @a coins + * @param coins the batch + * @param[out] total_amount set to the sum of the recouped amounts + * @param[out] h_recoups set to the hash over the recouped coins + * @param[out] timestamp set to the latest timestamp of the recoups + * @param[out] recoups set to a JSON array of the recouped coins + * (coin_pub and amount) for the response + * @return #GNUNET_OK on success, #GNUNET_SYSERR on amount overflow + */ +enum GNUNET_GenericReturnValue +TEH_recoup_summarize ( + size_t num_coins, + const struct TEH_RecoupCoin coins[static num_coins], + struct TALER_Amount *total_amount, + struct GNUNET_HashCode *h_recoups, + struct GNUNET_TIME_Timestamp *timestamp, + json_t **recoups); + +#endif diff --git a/src/exchange/taler-exchange-httpd_get-coins-COIN_PUB-history.c b/src/exchange/taler-exchange-httpd_get-coins-COIN_PUB-history.c @@ -312,11 +312,6 @@ compile_transaction_history ( json_decref (history); return NULL; } - /* NOTE: we could also provide coin_pub's coin_sig, denomination key hash and - the denomination key's RSA signature over coin_pub, but as the - wallet should really already have this information (and cannot - check or do anything with it anyway if it doesn't), it seems - strictly unnecessary. */ if (0 != json_array_append_new ( history, @@ -333,6 +328,8 @@ compile_transaction_history ( &epub), GNUNET_JSON_pack_data_auto ("coin_pub", &pr->coin.coin_pub), + GNUNET_JSON_pack_data_auto ("rc", + &pr->rc), GNUNET_JSON_pack_timestamp ("timestamp", pr->timestamp)))) { @@ -383,10 +380,12 @@ compile_transaction_history ( &recoup->coin_sig), GNUNET_JSON_pack_data_auto ("h_denom_pub", &recoup->h_denom_pub), - GNUNET_JSON_pack_data_auto ("coin_blind", + GNUNET_JSON_pack_data_auto ("coin_blinding_secret", &recoup->coin_blind), - // FIXME-9828: spec says we should have h_commitment? - // FIXME-9828: spec says we should have coin_index? + GNUNET_JSON_pack_data_auto ("planchets_h", + &recoup->planchets_h), + GNUNET_JSON_pack_uint64 ("coin_index", + recoup->coin_index), GNUNET_JSON_pack_timestamp ("timestamp", recoup->timestamp)))) { @@ -417,11 +416,6 @@ compile_transaction_history ( json_decref (history); return NULL; } - /* NOTE: we could also provide coin_pub's coin_sig, denomination key - hash and the denomination key's RSA signature over coin_pub, but as - the wallet should really already have this information (and cannot - check or do anything with it anyway if it doesn't), it seems - strictly unnecessary. */ if (0 != json_array_append_new ( history, @@ -440,13 +434,14 @@ compile_transaction_history ( &pr->old_coin_pub), GNUNET_JSON_pack_data_auto ("coin_sig", &pr->coin_sig), - // FIXME-#9828: spec says to return h_denom_pub - GNUNET_JSON_pack_data_auto ("coin_blind", + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &pr->coin.denom_pub_hash), + GNUNET_JSON_pack_data_auto ("coin_blinding_secret", &pr->coin_blind), - // FIXME-#9828: spec says to return h_commitment - // FIXME-#9828: spec says to return coin_index - // FIXME-#9828: spec says to return new_coin_blinding_secret - // FIXME-#9828: spec says to return new_coin_ev + GNUNET_JSON_pack_data_auto ("rc", + &pr->rc), + GNUNET_JSON_pack_uint64 ("coin_index", + pr->coin_index), GNUNET_JSON_pack_timestamp ("timestamp", pr->timestamp)))) { diff --git a/src/exchange/taler-exchange-httpd_get-reserves-RESERVE_PUB-history.c b/src/exchange/taler-exchange-httpd_get-reserves-RESERVE_PUB-history.c @@ -264,7 +264,9 @@ compile_reserve_history ( TALER_JSON_pack_amount ("amount", &recoup->value), GNUNET_JSON_pack_data_auto ("coin_pub", - &recoup->coin.coin_pub)))) + &recoup->coin.coin_pub), + GNUNET_JSON_pack_data_auto ("planchets_h", + &recoup->planchets_h)))) { GNUNET_break (0); json_decref (json_history); diff --git a/src/exchange/taler-exchange-httpd_post-recoup-refresh.c b/src/exchange/taler-exchange-httpd_post-recoup-refresh.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2017-2023 Taler Systems SA + Copyright (C) 2017-2026 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -15,419 +15,921 @@ */ /** * @file taler-exchange-httpd_post-recoup-refresh.c - * @brief Handle /recoup-refresh requests; parses the POST and JSON and - * verifies the coin signature before handing things off - * to the database. + * @brief Handle /recoup-refresh requests: coins of revoked denominations + * that originated from one refresh operation are paid back to the + * old coin that was melted. Structured like the /withdraw handler + * as a phase state machine. * @author Christian Grothoff + * @author Özgür Kesim */ #include <gnunet/gnunet_util_lib.h> #include <gnunet/gnunet_json_lib.h> #include <jansson.h> #include <microhttpd.h> -#include <pthread.h> #include "taler/taler_json_lib.h" #include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd.h" #include "taler-exchange-httpd_db.h" +#include "taler-exchange-httpd_common_recoup.h" #include "taler-exchange-httpd_post-recoup-refresh.h" #include "taler-exchange-httpd_responses.h" #include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_get-metrics.h" #include "exchangedb_lib.h" -#include "exchange-database/get_old_coin_by_h_blind.h" #include "exchange-database/do_recoup_refresh.h" +#include "exchange-database/get_refresh.h" +#include "exchange-database/rollback.h" /** - * Closure for #recoup_refresh_transaction(). + * The different types of errors that might occur, sorted by name. */ -struct RecoupContext +enum RecoupRefreshError +{ + RECOUP_REFRESH_ERROR_NONE, + RECOUP_REFRESH_ERROR_BATCH_SIZE_MISMATCH, + RECOUP_REFRESH_ERROR_BLINDING_FAILED, + RECOUP_REFRESH_ERROR_COIN_SIGNATURE_INVALID, + RECOUP_REFRESH_ERROR_COMMITMENT_MISMATCH, + RECOUP_REFRESH_ERROR_CONFIRMATION_SIGN, + RECOUP_REFRESH_ERROR_DB_FETCH_FAILED, + RECOUP_REFRESH_ERROR_DB_INVARIANT_FAILURE, + RECOUP_REFRESH_ERROR_DENOMINATION_EXPIRED, + RECOUP_REFRESH_ERROR_DENOMINATION_KEY_UNKNOWN, + RECOUP_REFRESH_ERROR_DENOMINATION_MISMATCH, + RECOUP_REFRESH_ERROR_DENOMINATION_NOT_ELIGIBLE, + RECOUP_REFRESH_ERROR_DENOMINATION_SIGNATURE_INVALID, + RECOUP_REFRESH_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + RECOUP_REFRESH_ERROR_INSUFFICIENT_FUNDS, + RECOUP_REFRESH_ERROR_KEYS_MISSING, + RECOUP_REFRESH_ERROR_REQUEST_PARAMETER_MALFORMED, + RECOUP_REFRESH_ERROR_MELT_NOT_FOUND, +}; + + +/** + * Context for a /recoup-refresh request. + */ +struct RecoupRefreshContext { /** - * Set by #recoup_transaction() to the old coin that will - * receive the recoup. + * Processing phase we are in. The ordering matters, as we + * progress through them by incrementing the phase in the happy path. */ - struct TALER_CoinSpendPublicKeyP old_coin_pub; + enum + { + RECOUP_REFRESH_PHASE_PARSE = 0, + RECOUP_REFRESH_PHASE_LOOKUP_OPERATION, + RECOUP_REFRESH_PHASE_CHECK_KEYS, + RECOUP_REFRESH_PHASE_VERIFY_COINS, + RECOUP_REFRESH_PHASE_MAKE_COINS_KNOWN, + RECOUP_REFRESH_PHASE_RUN_TRANSACTION, + RECOUP_REFRESH_PHASE_GENERATE_REPLY_SUCCESS, + RECOUP_REFRESH_PHASE_GENERATE_REPLY_ERROR, + RECOUP_REFRESH_PHASE_RETURN_NO, + RECOUP_REFRESH_PHASE_RETURN_YES, + } phase; /** - * Details about the coin. + * Request context. */ - const struct TALER_CoinPublicInfo *coin; + const struct TEH_RequestContext *rc; /** - * Key used to blind the coin. + * Current time for the DB transaction. */ - const union GNUNET_CRYPTO_BlindingSecretP *coin_bks; + struct GNUNET_TIME_Timestamp now; /** - * Signature of the coin requesting recoup. + * Captures all parameters provided in the JSON request. */ - const struct TALER_CoinSpendSignatureP *coin_sig; + struct + { + /** + * Old coin the coins were refreshed from and that is credited. + */ + struct TALER_CoinSpendPublicKeyP old_coin_pub; + + /** + * Commitment of the refresh operation. + */ + struct TALER_RefreshCommitmentP rc; + + /** + * Number of entries in @e coins. + */ + size_t num_coins; + + /** + * The coins of the signed batch, in order. + */ + struct TEH_RecoupCoin *coins; + } request; /** - * Unique ID of the coin in the known_coins table. + * The refresh operation, as recorded in the database. */ - uint64_t known_coin_id; + struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS refresh; /** - * Unique ID of the refresh reveal context of the melt for the new coin. + * True once @e refresh holds data that must be released. */ - uint64_t rrc_serial; + bool have_refresh; /** - * Set by #recoup_transaction to the timestamp when the recoup - * was accepted. + * Errors occurring during evaluation of the request. In phase + * #RECOUP_REFRESH_PHASE_GENERATE_REPLY_ERROR an appropriate error + * message is prepared and sent to the client. */ - struct GNUNET_TIME_Timestamp now; - + struct + { + /** + * The (internal) error code. + */ + enum RecoupRefreshError code; + + /** + * Details for some of the errors. + */ + union + { + const char *request_parameter_malformed; + const char *db_fetch_context; + /** + * For all errors related to a particular denomination. + */ + const struct TALER_DenominationHashP *denom_h; + enum TALER_ErrorCode ec_confirmation_sign; + /** + * The coin that has no residual value left. + */ + const struct TEH_RecoupCoin *insufficient_funds; + } details; + } error; }; /** - * Execute a "recoup-refresh". The validity of the coin and signature have - * already been checked. The database must now check that the coin is not - * (double) spent, and execute the transaction. + * The following macros set the given error code, + * set the phase to #RECOUP_REFRESH_PHASE_GENERATE_REPLY_ERROR, + * and optionally set the given field to the given value. + */ +#define SET_ERROR(wc, ec) \ + do \ + { GNUNET_static_assert (RECOUP_REFRESH_ERROR_NONE != ec); \ + (wc)->error.code = (ec); \ + (wc)->phase = RECOUP_REFRESH_PHASE_GENERATE_REPLY_ERROR; \ + } while (0) +#define SET_ERROR_WITH_DETAIL(wc, ec, field, value) \ + do \ + { GNUNET_static_assert (RECOUP_REFRESH_ERROR_NONE != ec); \ + (wc)->error.code = (ec); \ + (wc)->error.details.field = (value); \ + (wc)->phase = RECOUP_REFRESH_PHASE_GENERATE_REPLY_ERROR; \ + } while (0) + + +/** + * Terminate the phase loop with the given MHD result. * - * 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[in,out] wc context to finish + * @param mres result to return from the handler + */ +static void +finish_loop (struct RecoupRefreshContext *wc, + enum MHD_Result mres) +{ + wc->phase = (MHD_YES == mres) + ? RECOUP_REFRESH_PHASE_RETURN_YES + : RECOUP_REFRESH_PHASE_RETURN_NO; +} + + +/** + * Translate an error of the shared recoup logic into our error state. * - * @param cls the `struct RecoupContext *` - * @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 code + * @param[in,out] wc context to set the error on + * @param err error reported by the shared logic + * @param details details reported by the shared logic */ -static enum GNUNET_DB_QueryStatus -recoup_refresh_transaction (void *cls, - struct MHD_Connection *connection, - enum MHD_Result *mhd_ret) +static void +set_common_error (struct RecoupRefreshContext *wc, + enum TEH_RecoupError err, + const union TEH_RecoupErrorDetails *details) { - struct RecoupContext *pc = cls; - enum GNUNET_DB_QueryStatus qs; - bool recoup_ok; - bool internal_failure; - - /* Finally, store new refund data */ - pc->now = GNUNET_TIME_timestamp_get (); - qs = TALER_EXCHANGEDB_do_recoup_refresh (TEH_pg, - &pc->old_coin_pub, - pc->rrc_serial, - 0, /* coin_index, FIXME_9828 */ - pc->coin_bks, - &pc->coin->coin_pub, - pc->known_coin_id, - pc->coin_sig, - &pc->now, - &recoup_ok, - &internal_failure); - if (0 > qs) + switch (err) { - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "do_recoup_refresh"); - return qs; + case TEH_RECOUP_ERROR_NONE: + GNUNET_assert (0); + return; + case TEH_RECOUP_ERROR_REQUEST_PARAMETER_MALFORMED: + SET_ERROR_WITH_DETAIL (wc, + RECOUP_REFRESH_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + details->hint); + return; + case TEH_RECOUP_ERROR_KEYS_MISSING: + SET_ERROR (wc, + RECOUP_REFRESH_ERROR_KEYS_MISSING); + return; + case TEH_RECOUP_ERROR_DENOMINATION_KEY_UNKNOWN: + SET_ERROR_WITH_DETAIL (wc, + RECOUP_REFRESH_ERROR_DENOMINATION_KEY_UNKNOWN, + denom_h, + details->denom_h); + return; + case TEH_RECOUP_ERROR_DENOMINATION_MISMATCH: + SET_ERROR_WITH_DETAIL (wc, + RECOUP_REFRESH_ERROR_DENOMINATION_MISMATCH, + denom_h, + details->denom_h); + return; + case TEH_RECOUP_ERROR_DENOMINATION_NOT_ELIGIBLE: + SET_ERROR_WITH_DETAIL (wc, + RECOUP_REFRESH_ERROR_DENOMINATION_NOT_ELIGIBLE, + denom_h, + details->denom_h); + return; + case TEH_RECOUP_ERROR_DENOMINATION_EXPIRED: + SET_ERROR_WITH_DETAIL (wc, + RECOUP_REFRESH_ERROR_DENOMINATION_EXPIRED, + denom_h, + details->denom_h); + return; + case TEH_RECOUP_ERROR_DENOMINATION_VALIDITY_IN_FUTURE: + SET_ERROR_WITH_DETAIL (wc, + RECOUP_REFRESH_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + denom_h, + details->denom_h); + return; + case TEH_RECOUP_ERROR_DB_INVARIANT_FAILURE: + SET_ERROR (wc, + RECOUP_REFRESH_ERROR_DB_INVARIANT_FAILURE); + return; + case TEH_RECOUP_ERROR_DENOMINATION_SIGNATURE_INVALID: + SET_ERROR (wc, + RECOUP_REFRESH_ERROR_DENOMINATION_SIGNATURE_INVALID); + return; + case TEH_RECOUP_ERROR_COIN_SIGNATURE_INVALID: + SET_ERROR (wc, + RECOUP_REFRESH_ERROR_COIN_SIGNATURE_INVALID); + return; + case TEH_RECOUP_ERROR_BLINDING_FAILED: + SET_ERROR (wc, + RECOUP_REFRESH_ERROR_BLINDING_FAILED); + return; + case TEH_RECOUP_ERROR_COMMITMENT_MISMATCH: + SET_ERROR (wc, + RECOUP_REFRESH_ERROR_COMMITMENT_MISMATCH); + return; } + GNUNET_assert (0); +} + + +/** + * Parse the request. + * + * @param[in,out] wc context of the request + * @param root the JSON body + */ +static void +phase_parse (struct RecoupRefreshContext *wc, + const json_t *root) +{ + const json_t *j_coin_data; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("old_coin_pub", + &wc->request.old_coin_pub), + GNUNET_JSON_spec_fixed_auto ("rc", + &wc->request.rc), + GNUNET_JSON_spec_array_const ("coin_data", + &j_coin_data), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + const char *hint; + enum MHD_Result mret; - if (internal_failure) + res = TALER_MHD_parse_json_data (wc->rc->connection, + root, + spec); + if (GNUNET_OK != res) { - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "coin transaction history"); - return GNUNET_DB_STATUS_HARD_ERROR; + GNUNET_break_op (0); + finish_loop (wc, + (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES); + return; } - if (! recoup_ok) + res = TEH_recoup_parse_coin_data (wc->rc->connection, + j_coin_data, + &wc->request.num_coins, + &wc->request.coins, + &hint, + &mret); + switch (res) { - *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, - &pc->coin->denom_pub_hash, - &pc->coin->coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_OK: + wc->phase++; + return; + case GNUNET_NO: + finish_loop (wc, + mret); + return; + case GNUNET_SYSERR: + SET_ERROR_WITH_DETAIL (wc, + RECOUP_REFRESH_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + hint); + return; } - return qs; + GNUNET_assert (0); } /** - * We have parsed the JSON information about the recoup request. Do - * some basic sanity checks (especially that the signature on the - * request and coin is valid) and then execute the recoup operation. - * Note that we need the DB to check the fee structure, so this is not - * done here but during the recoup_transaction(). + * Find the refresh operation the coins originated from. * - * @param connection the MHD connection to handle - * @param coin information about the coin - * @param exchange_vals values contributed by the exchange - * during refresh - * @param coin_bks blinding data of the coin (to be checked) - * @param nonce withdraw nonce (if CS is used) - * @param coin_sig signature of the coin - * @return MHD result code + * @param[in,out] wc context of the request */ -static enum MHD_Result -verify_and_execute_recoup_refresh ( - struct MHD_Connection *connection, - const struct TALER_CoinPublicInfo *coin, - const struct TALER_ExchangeBlindingValues *exchange_vals, - const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, - const union GNUNET_CRYPTO_BlindSessionNonce *nonce, - const struct TALER_CoinSpendSignatureP *coin_sig) +static void +phase_lookup_operation (struct RecoupRefreshContext *wc) { - struct RecoupContext pc; - const struct TEH_DenominationKey *dk; - enum MHD_Result mret; - struct TALER_BlindedCoinHashP h_blind; - - /* check denomination exists and is in recoup mode */ - dk = TEH_keys_denomination_by_hash (&coin->denom_pub_hash, - connection, - &mret); - if (NULL == dk) - return mret; - if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time)) + enum GNUNET_DB_QueryStatus qs; + uint8_t max_retries = 3; + + while (0 < max_retries--) { - /* This denomination is past the expiration time for recoup */ - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - connection, - &coin->denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, - "RECOUP-REFRESH"); + qs = TALER_EXCHANGEDB_get_refresh (TEH_pg, + &wc->request.rc, + &wc->refresh); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; } - if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) + if (0 > qs) { - /* This denomination is not yet valid */ - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - connection, - &coin->denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, - "RECOUP-REFRESH"); + GNUNET_break (0); + SET_ERROR_WITH_DETAIL (wc, + RECOUP_REFRESH_ERROR_DB_FETCH_FAILED, + db_fetch_context, + "get_refresh"); + return; } - if (! dk->recoup_possible) + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { - /* This denomination is not eligible for recoup */ - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - connection, - &coin->denom_pub_hash, - TALER_EC_EXCHANGE_RECOUP_REFRESH_NOT_ELIGIBLE, - "RECOUP-REFRESH"); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Recoup requested for unknown refresh commitment %s\n", + GNUNET_h2s (&wc->request.rc.session_hash)); + SET_ERROR (wc, + RECOUP_REFRESH_ERROR_MELT_NOT_FOUND); + return; } - - /* check denomination signature */ - switch (dk->denom_pub.bsign_pub_key->cipher) + wc->have_refresh = true; + if (0 != + GNUNET_memcmp (&wc->refresh.coin.coin_pub, + &wc->request.old_coin_pub)) { - case GNUNET_CRYPTO_BSA_RSA: - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; - break; - case GNUNET_CRYPTO_BSA_CS: - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; - break; - default: - break; + GNUNET_break_op (0); + SET_ERROR (wc, + RECOUP_REFRESH_ERROR_MELT_NOT_FOUND); + return; } - if (GNUNET_YES != - TALER_test_coin_valid (coin, - &dk->denom_pub)) + if (wc->refresh.num_coins != wc->request.num_coins) { - TALER_LOG_WARNING ("Invalid coin passed for recoup\n"); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, - NULL); + GNUNET_break_op (0); + SET_ERROR (wc, + RECOUP_REFRESH_ERROR_BATCH_SIZE_MISMATCH); + return; } + wc->phase++; +} - /* check recoup request signature */ - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_recoup_refresh_verify (&coin->denom_pub_hash, - coin_bks, - &coin->coin_pub, - coin_sig)) + +/** + * Check the denominations of the batch. + * + * @param[in,out] wc context of the request + */ +static void +phase_check_keys (struct RecoupRefreshContext *wc) +{ + union TEH_RecoupErrorDetails details; + enum TEH_RecoupError err; + + err = TEH_recoup_check_keys (wc->request.num_coins, + wc->request.coins, + wc->refresh.denom_serials, + ! wc->refresh.no_blinding_seed, + &details); + if (TEH_RECOUP_ERROR_NONE != err) { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_RECOUP_REFRESH_SIGNATURE_INVALID, - NULL); + set_common_error (wc, + err, + &details); + return; } + wc->phase++; +} + +/** + * Verify the disclosed coins and that the batch matches the + * commitment of the refresh operation. + * + * @param[in,out] wc context of the request + */ +static void +phase_verify_coins (struct RecoupRefreshContext *wc) +{ + union TEH_RecoupErrorDetails details; + enum TEH_RecoupError err; + + /* The exchange signed the batch at the noreveal_index, whose + hash was recorded as selected_h. */ + err = TEH_recoup_verify_coins (wc->request.num_coins, + wc->request.coins, + true, /* for melt */ + wc->refresh.no_blinding_seed + ? NULL + : &wc->refresh.blinding_seed, + wc->refresh.num_cs_r_values, + wc->refresh.cs_r_values, + &wc->refresh.selected_h, + &details); + if (TEH_RECOUP_ERROR_NONE != err) { - struct TALER_CoinPubHashP c_hash; - struct TALER_BlindedPlanchet blinded_planchet; - - if (GNUNET_OK != - TALER_denom_blind (&dk->denom_pub, - coin_bks, - nonce, - &coin->h_age_commitment, - &coin->coin_pub, - exchange_vals, - &c_hash, - &blinded_planchet)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_RECOUP_REFRESH_BLINDING_FAILED, - NULL); - } - TALER_coin_ev_hash (&blinded_planchet, - &coin->denom_pub_hash, - &h_blind); - TALER_blinded_planchet_free (&blinded_planchet); + set_common_error (wc, + err, + &details); + return; } + wc->phase++; +} - pc.coin_sig = coin_sig; - pc.coin_bks = coin_bks; - pc.coin = coin; +/** + * Make sure all disclosed coins are known in the database. + * + * @param[in,out] wc context of the request + */ +static void +phase_make_coins_known (struct RecoupRefreshContext *wc) +{ + for (size_t i = 0; i < wc->request.num_coins; i++) { + struct TEH_RecoupCoin *c = &wc->request.coins[i]; enum MHD_Result mhd_ret = MHD_NO; enum GNUNET_DB_QueryStatus qs; - /* make sure coin is 'known' in database */ - qs = TEH_make_coin_known (coin, - connection, - &pc.known_coin_id, + if (! c->disclosed) + continue; + qs = TEH_make_coin_known (&c->coin, + wc->rc->connection, + &c->known_coin_id, &mhd_ret); /* no transaction => no serialization failures should be possible */ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); if (qs < 0) - return mhd_ret; + { + finish_loop (wc, + mhd_ret); + return; + } } + wc->phase++; +} + +/** + * Function implementing the recoup transaction: credits the old coin for + * every disclosed coin. IF it returns a non-error code, the transaction + * logic MUST NOT queue a MHD response. IF it returns a hard error, it + * sets the error state (and the reply is generated by the error phase). + * 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 RecoupRefreshContext *` + * @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 +recoup_transaction (void *cls, + struct MHD_Connection *connection, + enum MHD_Result *mhd_ret) +{ + struct RecoupRefreshContext *wc = cls; + + (void) connection; + (void) mhd_ret; + for (size_t i = 0; i < wc->request.num_coins; i++) { + struct TEH_RecoupCoin *c = &wc->request.coins[i]; enum GNUNET_DB_QueryStatus qs; - - qs = TALER_EXCHANGEDB_get_old_coin_by_h_blind (TEH_pg, - &h_blind, - &pc.old_coin_pub, - &pc.rrc_serial); + bool recoup_ok; + bool internal_failure; + + if (! c->disclosed) + continue; + c->timestamp = wc->now; + qs = TALER_EXCHANGEDB_do_recoup_refresh (TEH_pg, + &wc->request.old_coin_pub, + wc->refresh.refresh_id, + (uint32_t) i, + &c->coin_blinding_secret, + &c->coin.coin_pub, + c->known_coin_id, + &c->coin_sig, + &c->timestamp, + &c->amount, + &recoup_ok, + &internal_failure); if (0 > qs) { + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + SET_ERROR_WITH_DETAIL (wc, + RECOUP_REFRESH_ERROR_DB_FETCH_FAILED, + db_fetch_context, + "do_recoup_refresh"); + return qs; + } + if (internal_failure) + { GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_old_coin_by_h_blind"); + SET_ERROR (wc, + RECOUP_REFRESH_ERROR_DB_INVARIANT_FAILURE); + return GNUNET_DB_STATUS_HARD_ERROR; } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + if (! recoup_ok) { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Recoup-refresh requested for unknown envelope %s\n", - GNUNET_h2s (&h_blind.hash)); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_RECOUP_REFRESH_MELT_NOT_FOUND, - NULL); + /* The reply looks at the coin's history, so end our transaction. */ + TALER_EXCHANGEDB_rollback (TEH_pg); + SET_ERROR_WITH_DETAIL (wc, + RECOUP_REFRESH_ERROR_INSUFFICIENT_FUNDS, + insufficient_funds, + c); + return GNUNET_DB_STATUS_HARD_ERROR; } } + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} + - /* Perform actual recoup transaction */ +/** + * Run the main DB transaction. + * + * @param[in,out] wc context of the request + */ +static void +phase_run_transaction (struct RecoupRefreshContext *wc) +{ + enum MHD_Result mhd_ret; + enum GNUNET_GenericReturnValue qs; + + GNUNET_assert (RECOUP_REFRESH_PHASE_RUN_TRANSACTION == wc->phase); + qs = TEH_DB_run_transaction (wc->rc->connection, + "run recoup-refresh", + TEH_MT_REQUEST_OTHER, + &mhd_ret, + &recoup_transaction, + wc); + /* If the transaction has changed the phase, we don't alter it. */ + if (RECOUP_REFRESH_PHASE_RUN_TRANSACTION != wc->phase) + return; + if (GNUNET_OK != qs) { - enum MHD_Result mhd_ret; - - if (GNUNET_OK != - TEH_DB_run_transaction (connection, - "run recoup-refresh", - TEH_MT_REQUEST_OTHER, - &mhd_ret, - &recoup_refresh_transaction, - &pc)) - return mhd_ret; + /* persistent soft error, reply already queued */ + finish_loop (wc, + mhd_ret); + return; } - /* Recoup succeeded, return result */ - return TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_data_auto ( - "old_coin_pub", - &pc.old_coin_pub)); + wc->phase++; } /** - * Handle a "/coins/$COIN_PUB/recoup-refresh" request. Parses the JSON, and, if - * successful, passes the JSON data to #verify_and_execute_recoup_refresh() to further - * check the details of the operation specified. If everything checks out, - * this will ultimately lead to the refund being executed, or rejected. + * Generate the success response: the recouped coins and the batch + * confirmation signature. * - * @param connection the MHD connection to handle - * @param coin_pub public key of the coin - * @param root uploaded JSON data - * @return MHD result code - */ + * @param[in,out] wc context of the request + */ +static void +phase_generate_reply_success (struct RecoupRefreshContext *wc) +{ + struct TALER_Amount total_amount; + struct GNUNET_HashCode h_recoups; + struct GNUNET_TIME_Timestamp timestamp; + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + json_t *recoups; + enum TALER_ErrorCode ec; + + if (GNUNET_OK != + TEH_recoup_summarize (wc->request.num_coins, + wc->request.coins, + &total_amount, + &h_recoups, + &timestamp, + &recoups)) + { + GNUNET_break (0); + SET_ERROR (wc, + RECOUP_REFRESH_ERROR_DB_INVARIANT_FAILURE); + return; + } + ec = TALER_exchange_online_confirm_recoup_refresh_batch_sign ( + &TEH_keys_exchange_sign_, + timestamp, + &wc->request.old_coin_pub, + &wc->request.rc, + &total_amount, + &h_recoups, + &pub, + &sig); + if (TALER_EC_NONE != ec) + { + json_decref (recoups); + SET_ERROR_WITH_DETAIL (wc, + RECOUP_REFRESH_ERROR_CONFIRMATION_SIGN, + ec_confirmation_sign, + ec); + return; + } + finish_loop (wc, + TALER_MHD_REPLY_JSON_PACK ( + wc->rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ("old_coin_pub", + &wc->request.old_coin_pub), + GNUNET_JSON_pack_data_auto ("rc", + &wc->request.rc), + GNUNET_JSON_pack_timestamp ("timestamp", + timestamp), + TALER_JSON_pack_amount ("total_amount", + &total_amount), + GNUNET_JSON_pack_array_steal ("recoups", + recoups), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub))); +} + + +/** + * Report the error in @a wc to the client. + * + * @param[in,out] wc context of the request + */ +static void +phase_generate_reply_error (struct RecoupRefreshContext *wc) +{ + GNUNET_assert (RECOUP_REFRESH_PHASE_GENERATE_REPLY_ERROR == wc->phase); + switch (wc->error.code) + { + case RECOUP_REFRESH_ERROR_NONE: + break; + case RECOUP_REFRESH_ERROR_BATCH_SIZE_MISMATCH: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_RECOUP_REFRESH_BATCH_SIZE_MISMATCH, + NULL)); + return; + case RECOUP_REFRESH_ERROR_BLINDING_FAILED: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_RECOUP_REFRESH_BLINDING_FAILED, + NULL)); + return; + case RECOUP_REFRESH_ERROR_COIN_SIGNATURE_INVALID: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RECOUP_REFRESH_SIGNATURE_INVALID, + NULL)); + return; + case RECOUP_REFRESH_ERROR_COMMITMENT_MISMATCH: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_RECOUP_REFRESH_COMMITMENT_MISMATCH, + NULL)); + return; + case RECOUP_REFRESH_ERROR_CONFIRMATION_SIGN: + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + wc->error.details.ec_confirmation_sign, + NULL)); + return; + case RECOUP_REFRESH_ERROR_DB_FETCH_FAILED: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + wc->error.details.db_fetch_context)); + return; + case RECOUP_REFRESH_ERROR_DB_INVARIANT_FAILURE: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + NULL)); + return; + case RECOUP_REFRESH_ERROR_DENOMINATION_EXPIRED: + finish_loop (wc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + wc->rc->connection, + wc->error.details.denom_h, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "RECOUP-REFRESH")); + return; + case RECOUP_REFRESH_ERROR_DENOMINATION_KEY_UNKNOWN: + finish_loop (wc, + TEH_RESPONSE_reply_unknown_denom_pub_hash ( + wc->rc->connection, + wc->error.details.denom_h)); + return; + case RECOUP_REFRESH_ERROR_DENOMINATION_MISMATCH: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_RECOUP_REFRESH_DENOMINATION_MISMATCH, + NULL)); + return; + case RECOUP_REFRESH_ERROR_DENOMINATION_NOT_ELIGIBLE: + finish_loop (wc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + wc->rc->connection, + wc->error.details.denom_h, + TALER_EC_EXCHANGE_RECOUP_REFRESH_NOT_ELIGIBLE, + "RECOUP-REFRESH")); + return; + case RECOUP_REFRESH_ERROR_DENOMINATION_SIGNATURE_INVALID: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, + NULL)); + return; + case RECOUP_REFRESH_ERROR_DENOMINATION_VALIDITY_IN_FUTURE: + finish_loop (wc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + wc->rc->connection, + wc->error.details.denom_h, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "RECOUP-REFRESH")); + return; + case RECOUP_REFRESH_ERROR_INSUFFICIENT_FUNDS: + { + const struct TEH_RecoupCoin *c = wc->error.details.insufficient_funds; + + finish_loop (wc, + TEH_RESPONSE_reply_coin_insufficient_funds ( + wc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &c->coin.denom_pub_hash, + &c->coin.coin_pub)); + return; + } + case RECOUP_REFRESH_ERROR_KEYS_MISSING: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_SERVICE_UNAVAILABLE, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL)); + return; + case RECOUP_REFRESH_ERROR_REQUEST_PARAMETER_MALFORMED: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + wc->error.details.request_parameter_malformed)); + return; + case RECOUP_REFRESH_ERROR_MELT_NOT_FOUND: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_RECOUP_REFRESH_MELT_NOT_FOUND, + NULL)); + return; + } + GNUNET_break (0); + finish_loop (wc, + MHD_NO); +} + + +/** + * Release the data of a refresh operation as returned by the database. + * + * @param[in,out] rf data to release + */ +static void +free_db_refresh_data (struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS *rf) +{ + if (NULL != rf->denom_sigs) + { + for (size_t i = 0; i < rf->num_coins; i++) + TALER_blinded_denom_sig_free (&rf->denom_sigs[i]); + GNUNET_free (rf->denom_sigs); + } + GNUNET_free (rf->denom_serials); + GNUNET_free (rf->cs_r_values); + GNUNET_free (rf->transfer_pubs); + GNUNET_free (rf->denom_pub_hashes); +} + + +/** + * Cleanup routine for the request, called upon completion. + * + * @param rc request context to clean up + */ +static void +clean_recoup_refresh_rc (struct TEH_RequestContext *rc) +{ + struct RecoupRefreshContext *wc = rc->rh_ctx; + + TEH_recoup_free_coins (wc->request.num_coins, + wc->request.coins); + if (wc->have_refresh) + free_db_refresh_data (&wc->refresh); + GNUNET_free (wc); +} + + enum MHD_Result -TEH_handler_recoup_refresh (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const json_t *root) +TEH_handler_recoup_refresh ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[0]) { - enum GNUNET_GenericReturnValue ret; - struct TALER_CoinPublicInfo coin = {0}; - union GNUNET_CRYPTO_BlindingSecretP coin_bks; - struct TALER_CoinSpendSignatureP coin_sig; - struct TALER_ExchangeBlindingValues exchange_vals; - union GNUNET_CRYPTO_BlindSessionNonce nonce; - bool no_nonce; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", - &coin.denom_pub_hash), - TALER_JSON_spec_denom_sig ("denom_sig", - &coin.denom_sig), - TALER_JSON_spec_exchange_blinding_values ("ewv", - &exchange_vals), - GNUNET_JSON_spec_fixed_auto ("coin_blind_key_secret", - &coin_bks), - GNUNET_JSON_spec_fixed_auto ("coin_sig", - &coin_sig), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("h_age_commitment", - &coin.h_age_commitment), - &coin.no_age_commitment), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("nonce", - &nonce), - &no_nonce), - GNUNET_JSON_spec_end () - }; + struct RecoupRefreshContext *wc = rc->rh_ctx; - memset (&coin, - 0, - sizeof (coin)); - coin.coin_pub = *coin_pub; - ret = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == ret) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == ret) - return MHD_YES; /* failure */ + (void) args; + if (NULL == wc) { - enum MHD_Result res; - - res = verify_and_execute_recoup_refresh (connection, - &coin, - &exchange_vals, - &coin_bks, - no_nonce - ? NULL - : &nonce, - &coin_sig); - GNUNET_JSON_parse_free (spec); - return res; + wc = GNUNET_new (struct RecoupRefreshContext); + rc->rh_ctx = wc; + rc->rh_cleaner = &clean_recoup_refresh_rc; + wc->rc = rc; + wc->now = GNUNET_TIME_timestamp_get (); + } + while (true) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "recoup-refresh processing in phase %d\n", + wc->phase); + switch (wc->phase) + { + case RECOUP_REFRESH_PHASE_PARSE: + phase_parse (wc, + root); + break; + case RECOUP_REFRESH_PHASE_LOOKUP_OPERATION: + phase_lookup_operation (wc); + break; + case RECOUP_REFRESH_PHASE_CHECK_KEYS: + phase_check_keys (wc); + break; + case RECOUP_REFRESH_PHASE_VERIFY_COINS: + phase_verify_coins (wc); + break; + case RECOUP_REFRESH_PHASE_MAKE_COINS_KNOWN: + phase_make_coins_known (wc); + break; + case RECOUP_REFRESH_PHASE_RUN_TRANSACTION: + phase_run_transaction (wc); + break; + case RECOUP_REFRESH_PHASE_GENERATE_REPLY_SUCCESS: + phase_generate_reply_success (wc); + break; + case RECOUP_REFRESH_PHASE_GENERATE_REPLY_ERROR: + phase_generate_reply_error (wc); + break; + case RECOUP_REFRESH_PHASE_RETURN_YES: + return MHD_YES; + case RECOUP_REFRESH_PHASE_RETURN_NO: + return MHD_NO; + } } } -/* end of taler-exchange-httpd_recoup-refresh.c */ +/* end of taler-exchange-httpd_post-recoup-refresh.c */ diff --git a/src/exchange/taler-exchange-httpd_post-recoup-refresh.h b/src/exchange/taler-exchange-httpd_post-recoup-refresh.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2017, 2021 Taler Systems SA + Copyright (C) 2017-2026 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 @@ -17,6 +17,7 @@ * @file taler-exchange-httpd_post-recoup-refresh.h * @brief Handle /recoup-refresh requests * @author Christian Grothoff + * @author Özgür Kesim */ #ifndef TALER_EXCHANGE_HTTPD_POST_RECOUP_REFRESH_H #define TALER_EXCHANGE_HTTPD_POST_RECOUP_REFRESH_H @@ -27,20 +28,19 @@ /** - * Handle a "/coins/$COIN_PUB/recoup-refresh" request. Parses the JSON, and, if - * successful, passes the JSON data to #verify_and_execute_recoup_refresh() to further - * check the details of the operation specified. If everything checks out, - * this will ultimately lead to the refund being executed, or rejected. + * Handle a "/recoup-refresh" request: pay the residual value of coins + * of revoked denominations, all originating from one refresh operation, + * back to the old coin they were refreshed from. * - * @param connection the MHD connection to handle - * @param coin_pub public key of the coin + * @param rc request context * @param root uploaded JSON data + * @param args empty array * @return MHD result code - */ + */ enum MHD_Result -TEH_handler_recoup_refresh (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const json_t *root); - +TEH_handler_recoup_refresh ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[0]); #endif diff --git a/src/exchange/taler-exchange-httpd_post-recoup-withdraw.c b/src/exchange/taler-exchange-httpd_post-recoup-withdraw.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2017-2022 Taler Systems SA + Copyright (C) 2017-2026 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -15,431 +15,923 @@ */ /** * @file taler-exchange-httpd_post-recoup-withdraw.c - * @brief Handle /recoup requests; parses the POST and JSON and - * verifies the coin signature before handing things off - * to the database. + * @brief Handle /recoup-withdraw requests: coins of revoked denominations + * that originated from one withdraw operation are paid back to the + * reserve. Structured like the /withdraw handler as a phase state + * machine. * @author Christian Grothoff + * @author Özgür Kesim */ #include <gnunet/gnunet_util_lib.h> #include <gnunet/gnunet_json_lib.h> #include <jansson.h> #include <microhttpd.h> -#include <pthread.h> #include "taler/taler_json_lib.h" #include "taler/taler_mhd_lib.h" +#include "taler-exchange-httpd.h" #include "taler-exchange-httpd_db.h" +#include "taler-exchange-httpd_common_recoup.h" #include "taler-exchange-httpd_post-recoup-withdraw.h" #include "taler-exchange-httpd_responses.h" #include "taler-exchange-httpd_get-keys.h" +#include "taler-exchange-httpd_get-metrics.h" #include "exchangedb_lib.h" -#include "exchange-database/get_reserve_by_h_planchets.h" #include "exchange-database/do_recoup.h" +#include "exchange-database/get_withdraw.h" +#include "exchange-database/rollback.h" + /** - * Closure for #recoup_transaction. + * The different types of errors that might occur, sorted by name. */ -struct RecoupContext +enum RecoupWithdrawError +{ + RECOUP_WITHDRAW_ERROR_NONE, + RECOUP_WITHDRAW_ERROR_BATCH_SIZE_MISMATCH, + RECOUP_WITHDRAW_ERROR_BLINDING_FAILED, + RECOUP_WITHDRAW_ERROR_COIN_SIGNATURE_INVALID, + RECOUP_WITHDRAW_ERROR_COMMITMENT_MISMATCH, + RECOUP_WITHDRAW_ERROR_CONFIRMATION_SIGN, + RECOUP_WITHDRAW_ERROR_DB_FETCH_FAILED, + RECOUP_WITHDRAW_ERROR_DB_INVARIANT_FAILURE, + RECOUP_WITHDRAW_ERROR_DENOMINATION_EXPIRED, + RECOUP_WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN, + RECOUP_WITHDRAW_ERROR_DENOMINATION_MISMATCH, + RECOUP_WITHDRAW_ERROR_DENOMINATION_NOT_ELIGIBLE, + RECOUP_WITHDRAW_ERROR_DENOMINATION_SIGNATURE_INVALID, + RECOUP_WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + RECOUP_WITHDRAW_ERROR_INSUFFICIENT_FUNDS, + RECOUP_WITHDRAW_ERROR_KEYS_MISSING, + RECOUP_WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED, + RECOUP_WITHDRAW_ERROR_WITHDRAW_NOT_FOUND, +}; + + +/** + * Context for a /recoup-withdraw request. + */ +struct RecoupWithdrawContext { - /** - * Hash identifying the withdraw request. - */ - struct TALER_BlindedCoinHashP h_coin_ev; /** - * Set by #recoup_transaction() to the reserve that will - * receive the recoup, if #refreshed is #GNUNET_NO. + * Processing phase we are in. The ordering matters, as we + * progress through them by incrementing the phase in the happy path. */ - struct TALER_ReservePublicKeyP reserve_pub; + enum + { + RECOUP_WITHDRAW_PHASE_PARSE = 0, + RECOUP_WITHDRAW_PHASE_LOOKUP_OPERATION, + RECOUP_WITHDRAW_PHASE_CHECK_KEYS, + RECOUP_WITHDRAW_PHASE_VERIFY_COINS, + RECOUP_WITHDRAW_PHASE_MAKE_COINS_KNOWN, + RECOUP_WITHDRAW_PHASE_RUN_TRANSACTION, + RECOUP_WITHDRAW_PHASE_GENERATE_REPLY_SUCCESS, + RECOUP_WITHDRAW_PHASE_GENERATE_REPLY_ERROR, + RECOUP_WITHDRAW_PHASE_RETURN_NO, + RECOUP_WITHDRAW_PHASE_RETURN_YES, + } phase; /** - * Details about the coin. + * Request context. */ - const struct TALER_CoinPublicInfo *coin; + const struct TEH_RequestContext *rc; /** - * Key used to blind the coin. + * Current time for the DB transaction. */ - const union GNUNET_CRYPTO_BlindingSecretP *coin_bks; + struct GNUNET_TIME_Timestamp now; /** - * Signature of the coin requesting recoup. + * Captures all parameters provided in the JSON request. */ - const struct TALER_CoinSpendSignatureP *coin_sig; + struct + { + /** + * Reserve the coins were withdrawn from and that is credited. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Commitment of the withdraw operation. + */ + struct TALER_HashBlindedPlanchetsP planchets_h; + + /** + * Number of entries in @e coins. + */ + size_t num_coins; + + /** + * The coins of the signed batch, in order. + */ + struct TEH_RecoupCoin *coins; + } request; /** - * Unique ID of the withdraw operation in the withdraw table. + * The withdraw operation, as recorded in the database. */ - uint64_t withdraw_serial_id; + struct TALER_EXCHANGEDB_Withdraw withdraw; /** - * Unique ID of the coin in the known_coins table. + * True once @e withdraw holds data that must be released. */ - uint64_t known_coin_id; + bool have_withdraw; /** - * Set by #recoup_transaction to the timestamp when the recoup - * was accepted. + * Errors occurring during evaluation of the request. In phase + * #RECOUP_WITHDRAW_PHASE_GENERATE_REPLY_ERROR an appropriate error + * message is prepared and sent to the client. */ - struct GNUNET_TIME_Timestamp now; - + struct + { + /** + * The (internal) error code. + */ + enum RecoupWithdrawError code; + + /** + * Details for some of the errors. + */ + union + { + const char *request_parameter_malformed; + const char *db_fetch_context; + /** + * For all errors related to a particular denomination. + */ + const struct TALER_DenominationHashP *denom_h; + enum TALER_ErrorCode ec_confirmation_sign; + /** + * The coin that has no residual value left. + */ + const struct TEH_RecoupCoin *insufficient_funds; + } details; + } error; }; /** - * Execute a "recoup". The validity of the coin and signature have - * already been checked. The database must now check that the coin is - * not (double) spent, and execute the transaction. + * The following macros set the given error code, + * set the phase to #RECOUP_WITHDRAW_PHASE_GENERATE_REPLY_ERROR, + * and optionally set the given field to the given value. + */ +#define SET_ERROR(wc, ec) \ + do \ + { GNUNET_static_assert (RECOUP_WITHDRAW_ERROR_NONE != ec); \ + (wc)->error.code = (ec); \ + (wc)->phase = RECOUP_WITHDRAW_PHASE_GENERATE_REPLY_ERROR; \ + } while (0) +#define SET_ERROR_WITH_DETAIL(wc, ec, field, value) \ + do \ + { GNUNET_static_assert (RECOUP_WITHDRAW_ERROR_NONE != ec); \ + (wc)->error.code = (ec); \ + (wc)->error.details.field = (value); \ + (wc)->phase = RECOUP_WITHDRAW_PHASE_GENERATE_REPLY_ERROR; \ + } while (0) + + +/** + * Terminate the phase loop with the given MHD result. * - * 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[in,out] wc context to finish + * @param mres result to return from the handler + */ +static void +finish_loop (struct RecoupWithdrawContext *wc, + enum MHD_Result mres) +{ + wc->phase = (MHD_YES == mres) + ? RECOUP_WITHDRAW_PHASE_RETURN_YES + : RECOUP_WITHDRAW_PHASE_RETURN_NO; +} + + +/** + * Translate an error of the shared recoup logic into our error state. * - * @param cls the `struct RecoupContext *` - * @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 code + * @param[in,out] wc context to set the error on + * @param err error reported by the shared logic + * @param details details reported by the shared logic */ -static enum GNUNET_DB_QueryStatus -recoup_transaction (void *cls, - struct MHD_Connection *connection, - enum MHD_Result *mhd_ret) +static void +set_common_error (struct RecoupWithdrawContext *wc, + enum TEH_RecoupError err, + const union TEH_RecoupErrorDetails *details) { - struct RecoupContext *pc = cls; - enum GNUNET_DB_QueryStatus qs; - bool recoup_ok; - bool internal_failure; - - /* Finally, store new refund data */ - pc->now = GNUNET_TIME_timestamp_get (); - qs = TALER_EXCHANGEDB_do_recoup (TEH_pg, - &pc->reserve_pub, - pc->withdraw_serial_id, - 0, /* coin_index, FIXME_9828 */ - pc->coin_bks, - &pc->coin->coin_pub, - pc->known_coin_id, - pc->coin_sig, - &pc->now, - &recoup_ok, - &internal_failure); - if (0 > qs) + switch (err) { - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "do_recoup"); - return qs; + case TEH_RECOUP_ERROR_NONE: + GNUNET_assert (0); + return; + case TEH_RECOUP_ERROR_REQUEST_PARAMETER_MALFORMED: + SET_ERROR_WITH_DETAIL (wc, + RECOUP_WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + details->hint); + return; + case TEH_RECOUP_ERROR_KEYS_MISSING: + SET_ERROR (wc, + RECOUP_WITHDRAW_ERROR_KEYS_MISSING); + return; + case TEH_RECOUP_ERROR_DENOMINATION_KEY_UNKNOWN: + SET_ERROR_WITH_DETAIL (wc, + RECOUP_WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN, + denom_h, + details->denom_h); + return; + case TEH_RECOUP_ERROR_DENOMINATION_MISMATCH: + SET_ERROR_WITH_DETAIL (wc, + RECOUP_WITHDRAW_ERROR_DENOMINATION_MISMATCH, + denom_h, + details->denom_h); + return; + case TEH_RECOUP_ERROR_DENOMINATION_NOT_ELIGIBLE: + SET_ERROR_WITH_DETAIL (wc, + RECOUP_WITHDRAW_ERROR_DENOMINATION_NOT_ELIGIBLE, + denom_h, + details->denom_h); + return; + case TEH_RECOUP_ERROR_DENOMINATION_EXPIRED: + SET_ERROR_WITH_DETAIL (wc, + RECOUP_WITHDRAW_ERROR_DENOMINATION_EXPIRED, + denom_h, + details->denom_h); + return; + case TEH_RECOUP_ERROR_DENOMINATION_VALIDITY_IN_FUTURE: + SET_ERROR_WITH_DETAIL (wc, + RECOUP_WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE, + denom_h, + details->denom_h); + return; + case TEH_RECOUP_ERROR_DB_INVARIANT_FAILURE: + SET_ERROR (wc, + RECOUP_WITHDRAW_ERROR_DB_INVARIANT_FAILURE); + return; + case TEH_RECOUP_ERROR_DENOMINATION_SIGNATURE_INVALID: + SET_ERROR (wc, + RECOUP_WITHDRAW_ERROR_DENOMINATION_SIGNATURE_INVALID); + return; + case TEH_RECOUP_ERROR_COIN_SIGNATURE_INVALID: + SET_ERROR (wc, + RECOUP_WITHDRAW_ERROR_COIN_SIGNATURE_INVALID); + return; + case TEH_RECOUP_ERROR_BLINDING_FAILED: + SET_ERROR (wc, + RECOUP_WITHDRAW_ERROR_BLINDING_FAILED); + return; + case TEH_RECOUP_ERROR_COMMITMENT_MISMATCH: + SET_ERROR (wc, + RECOUP_WITHDRAW_ERROR_COMMITMENT_MISMATCH); + return; } + GNUNET_assert (0); +} + - if (internal_failure) +/** + * Parse the request. + * + * @param[in,out] wc context of the request + * @param root the JSON body + */ +static void +phase_parse (struct RecoupWithdrawContext *wc, + const json_t *root) +{ + const json_t *j_coin_data; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("reserve_pub", + &wc->request.reserve_pub), + GNUNET_JSON_spec_fixed_auto ("planchets_h", + &wc->request.planchets_h), + GNUNET_JSON_spec_array_const ("coin_data", + &j_coin_data), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + const char *hint; + enum MHD_Result mret; + + res = TALER_MHD_parse_json_data (wc->rc->connection, + root, + spec); + if (GNUNET_OK != res) { - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "do_recoup"); - return GNUNET_DB_STATUS_HARD_ERROR; + GNUNET_break_op (0); + finish_loop (wc, + (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES); + return; } - if (! recoup_ok) + res = TEH_recoup_parse_coin_data (wc->rc->connection, + j_coin_data, + &wc->request.num_coins, + &wc->request.coins, + &hint, + &mret); + switch (res) { - *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, - &pc->coin->denom_pub_hash, - &pc->coin->coin_pub); - return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_OK: + wc->phase++; + return; + case GNUNET_NO: + finish_loop (wc, + mret); + return; + case GNUNET_SYSERR: + SET_ERROR_WITH_DETAIL (wc, + RECOUP_WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED, + request_parameter_malformed, + hint); + return; } - return qs; + GNUNET_assert (0); } /** - * We have parsed the JSON information about the recoup request. Do - * some basic sanity checks (especially that the signature on the - * request and coin is valid) and then execute the recoup operation. - * Note that we need the DB to check the fee structure, so this is not - * done here but during the recoup_transaction(). + * Find the withdraw operation the coins originated from. * - * @param connection the MHD connection to handle - * @param coin information about the coin - * @param exchange_vals values contributed by the exchange - * during withdrawal - * @param coin_bks blinding data of the coin (to be checked) - * @param h_planchets The hash of the commitment of the original withdraw request - * @param nonce coin's nonce if CS is used - * @param coin_sig signature of the coin - * @return MHD result code + * @param[in,out] wc context of the request */ -static enum MHD_Result -verify_and_execute_recoup ( - struct MHD_Connection *connection, - const struct TALER_CoinPublicInfo *coin, - const struct TALER_ExchangeBlindingValues *exchange_vals, - const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, - const struct TALER_HashBlindedPlanchetsP *h_planchets, - const union GNUNET_CRYPTO_BlindSessionNonce *nonce, - const struct TALER_CoinSpendSignatureP *coin_sig) +static void +phase_lookup_operation (struct RecoupWithdrawContext *wc) { - struct RecoupContext pc; - const struct TEH_DenominationKey *dk; - enum MHD_Result mret; + enum GNUNET_DB_QueryStatus qs; + uint8_t max_retries = 3; - /* check denomination exists and is in recoup mode */ - dk = TEH_keys_denomination_by_hash (&coin->denom_pub_hash, - connection, - &mret); - if (NULL == dk) - return mret; - if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time)) + while (0 < max_retries--) { - /* This denomination is past the expiration time for recoup */ - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - connection, - &coin->denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, - "RECOUP"); + qs = TALER_EXCHANGEDB_get_withdraw (TEH_pg, + &wc->request.planchets_h, + &wc->withdraw); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; } - if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) + if (0 > qs) { - /* This denomination is not yet valid */ - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - connection, - &coin->denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, - "RECOUP"); + GNUNET_break (0); + SET_ERROR_WITH_DETAIL (wc, + RECOUP_WITHDRAW_ERROR_DB_FETCH_FAILED, + db_fetch_context, + "get_withdraw"); + return; } - if (! dk->recoup_possible) + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { - /* This denomination is not eligible for recoup */ - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - connection, - &coin->denom_pub_hash, - TALER_EC_EXCHANGE_RECOUP_NOT_ELIGIBLE, - "RECOUP"); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Recoup requested for unknown withdraw commitment %s\n", + GNUNET_h2s (&wc->request.planchets_h.hash)); + SET_ERROR (wc, + RECOUP_WITHDRAW_ERROR_WITHDRAW_NOT_FOUND); + return; } - - /* check denomination signature */ - switch (dk->denom_pub.bsign_pub_key->cipher) + wc->have_withdraw = true; + if (0 != + GNUNET_memcmp (&wc->withdraw.reserve_pub, + &wc->request.reserve_pub)) { - case GNUNET_CRYPTO_BSA_RSA: - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_RSA]++; - break; - case GNUNET_CRYPTO_BSA_CS: - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_CS]++; - break; - default: - break; + GNUNET_break_op (0); + SET_ERROR (wc, + RECOUP_WITHDRAW_ERROR_WITHDRAW_NOT_FOUND); + return; } - if (GNUNET_YES != - TALER_test_coin_valid (coin, - &dk->denom_pub)) + if (wc->withdraw.num_coins != wc->request.num_coins) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, - NULL); + SET_ERROR (wc, + RECOUP_WITHDRAW_ERROR_BATCH_SIZE_MISMATCH); + return; } + wc->phase++; +} - /* check recoup request signature */ - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_recoup_verify (&coin->denom_pub_hash, - coin_bks, - &coin->coin_pub, - coin_sig)) + +/** + * Check the denominations of the batch. + * + * @param[in,out] wc context of the request + */ +static void +phase_check_keys (struct RecoupWithdrawContext *wc) +{ + union TEH_RecoupErrorDetails details; + enum TEH_RecoupError err; + + err = TEH_recoup_check_keys (wc->request.num_coins, + wc->request.coins, + wc->withdraw.denom_serials, + ! wc->withdraw.no_blinding_seed, + &details); + if (TEH_RECOUP_ERROR_NONE != err) { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_RECOUP_SIGNATURE_INVALID, - NULL); + set_common_error (wc, + err, + &details); + return; } + wc->phase++; +} + - /* re-compute client-side blinding so we can - (a bit later) check that this coin was indeed - signed by us. */ +/** + * Verify the disclosed coins and that the batch matches the + * commitment of the withdraw operation. + * + * @param[in,out] wc context of the request + */ +static void +phase_verify_coins (struct RecoupWithdrawContext *wc) +{ + union TEH_RecoupErrorDetails details; + enum TEH_RecoupError err; + + /* Without age restriction the batch hash is the commitment + itself; with it, the exchange signed the batch at the + noreveal_index, whose hash was recorded separately. */ + err = TEH_recoup_verify_coins (wc->request.num_coins, + wc->request.coins, + false, /* not for melt */ + wc->withdraw.no_blinding_seed + ? NULL + : &wc->withdraw.blinding_seed, + wc->withdraw.num_cs_r_values, + wc->withdraw.cs_r_values, + wc->withdraw.age_proof_required + ? &wc->withdraw.selected_h + : &wc->withdraw.planchets_h, + &details); + if (TEH_RECOUP_ERROR_NONE != err) { - struct TALER_CoinPubHashP c_hash; - struct TALER_BlindedPlanchet blinded_planchet; - - if (GNUNET_OK != - TALER_denom_blind (&dk->denom_pub, - coin_bks, - nonce, - &coin->h_age_commitment, - &coin->coin_pub, - exchange_vals, - &c_hash, - &blinded_planchet)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_RECOUP_BLINDING_FAILED, - NULL); - } - TALER_coin_ev_hash (&blinded_planchet, - &coin->denom_pub_hash, - &pc.h_coin_ev); - TALER_blinded_planchet_free (&blinded_planchet); + set_common_error (wc, + err, + &details); + return; } + wc->phase++; +} - pc.coin_sig = coin_sig; - pc.coin_bks = coin_bks; - pc.coin = coin; +/** + * Make sure all disclosed coins are known in the database. + * + * @param[in,out] wc context of the request + */ +static void +phase_make_coins_known (struct RecoupWithdrawContext *wc) +{ + for (size_t i = 0; i < wc->request.num_coins; i++) { + struct TEH_RecoupCoin *c = &wc->request.coins[i]; enum MHD_Result mhd_ret = MHD_NO; enum GNUNET_DB_QueryStatus qs; - /* make sure coin is 'known' in database */ - qs = TEH_make_coin_known (coin, - connection, - &pc.known_coin_id, + if (! c->disclosed) + continue; + qs = TEH_make_coin_known (&c->coin, + wc->rc->connection, + &c->known_coin_id, &mhd_ret); /* no transaction => no serialization failures should be possible */ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); if (qs < 0) - return mhd_ret; + { + finish_loop (wc, + mhd_ret); + return; + } } + wc->phase++; +} + + +/** + * Function implementing the recoup transaction: credits the reserve for + * every disclosed coin. IF it returns a non-error code, the transaction + * logic MUST NOT queue a MHD response. IF it returns a hard error, it + * sets the error state (and the reply is generated by the error phase). + * 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 RecoupWithdrawContext *` + * @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 +recoup_transaction (void *cls, + struct MHD_Connection *connection, + enum MHD_Result *mhd_ret) +{ + struct RecoupWithdrawContext *wc = cls; + (void) connection; + (void) mhd_ret; + for (size_t i = 0; i < wc->request.num_coins; i++) { + struct TEH_RecoupCoin *c = &wc->request.coins[i]; enum GNUNET_DB_QueryStatus qs; - - qs = TALER_EXCHANGEDB_get_reserve_by_h_planchets ( - TEH_pg, - h_planchets, - &pc.reserve_pub, - &pc.withdraw_serial_id); + bool recoup_ok; + bool internal_failure; + + if (! c->disclosed) + continue; + c->timestamp = wc->now; + qs = TALER_EXCHANGEDB_do_recoup (TEH_pg, + &wc->request.reserve_pub, + wc->withdraw.withdraw_id, + (uint32_t) i, + &c->coin_blinding_secret, + &c->coin.coin_pub, + c->known_coin_id, + &c->coin_sig, + &c->timestamp, + &c->amount, + &recoup_ok, + &internal_failure); if (0 > qs) { + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + SET_ERROR_WITH_DETAIL (wc, + RECOUP_WITHDRAW_ERROR_DB_FETCH_FAILED, + db_fetch_context, + "do_recoup"); + return qs; + } + if (internal_failure) + { GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_reserve_by_commitment"); + SET_ERROR (wc, + RECOUP_WITHDRAW_ERROR_DB_INVARIANT_FAILURE); + return GNUNET_DB_STATUS_HARD_ERROR; } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + if (! recoup_ok) { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Recoup requested for unknown envelope %s\n", - GNUNET_h2s (&pc.h_coin_ev.hash)); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_RECOUP_WITHDRAW_NOT_FOUND, - NULL); + /* The reply looks at the coin's history, so end our transaction. */ + TALER_EXCHANGEDB_rollback (TEH_pg); + SET_ERROR_WITH_DETAIL (wc, + RECOUP_WITHDRAW_ERROR_INSUFFICIENT_FUNDS, + insufficient_funds, + c); + return GNUNET_DB_STATUS_HARD_ERROR; } } + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} + - /* Perform actual recoup transaction */ +/** + * Run the main DB transaction. + * + * @param[in,out] wc context of the request + */ +static void +phase_run_transaction (struct RecoupWithdrawContext *wc) +{ + enum MHD_Result mhd_ret; + enum GNUNET_GenericReturnValue qs; + + GNUNET_assert (RECOUP_WITHDRAW_PHASE_RUN_TRANSACTION == wc->phase); + qs = TEH_DB_run_transaction (wc->rc->connection, + "run recoup-withdraw", + TEH_MT_REQUEST_OTHER, + &mhd_ret, + &recoup_transaction, + wc); + /* If the transaction has changed the phase, we don't alter it. */ + if (RECOUP_WITHDRAW_PHASE_RUN_TRANSACTION != wc->phase) + return; + if (GNUNET_OK != qs) { - enum MHD_Result mhd_ret; - - if (GNUNET_OK != - TEH_DB_run_transaction (connection, - "run recoup", - TEH_MT_REQUEST_OTHER, - &mhd_ret, - &recoup_transaction, - &pc)) - return mhd_ret; + /* persistent soft error, reply already queued */ + finish_loop (wc, + mhd_ret); + return; } - /* Recoup succeeded, return result */ - return TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_data_auto ( - "reserve_pub", - &pc.reserve_pub)); + wc->phase++; } /** - * Handle a "/coins/$COIN_PUB/recoup" request. Parses the JSON, and, if - * successful, passes the JSON data to #verify_and_execute_recoup() to further - * check the details of the operation specified. If everything checks out, - * this will ultimately lead to the refund being executed, or rejected. + * Generate the success response: the recouped coins and the batch + * confirmation signature. * - * @param connection the MHD connection to handle - * @param coin_pub public key of the coin - * @param root uploaded JSON data - * @return MHD result code - */ + * @param[in,out] wc context of the request + */ +static void +phase_generate_reply_success (struct RecoupWithdrawContext *wc) +{ + struct TALER_Amount total_amount; + struct GNUNET_HashCode h_recoups; + struct GNUNET_TIME_Timestamp timestamp; + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + json_t *recoups; + enum TALER_ErrorCode ec; + + if (GNUNET_OK != + TEH_recoup_summarize (wc->request.num_coins, + wc->request.coins, + &total_amount, + &h_recoups, + &timestamp, + &recoups)) + { + GNUNET_break (0); + SET_ERROR (wc, + RECOUP_WITHDRAW_ERROR_DB_INVARIANT_FAILURE); + return; + } + ec = TALER_exchange_online_confirm_recoup_withdraw_batch_sign ( + &TEH_keys_exchange_sign_, + timestamp, + &wc->request.reserve_pub, + &wc->request.planchets_h, + &total_amount, + &h_recoups, + &pub, + &sig); + if (TALER_EC_NONE != ec) + { + json_decref (recoups); + SET_ERROR_WITH_DETAIL (wc, + RECOUP_WITHDRAW_ERROR_CONFIRMATION_SIGN, + ec_confirmation_sign, + ec); + return; + } + finish_loop (wc, + TALER_MHD_REPLY_JSON_PACK ( + wc->rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ("reserve_pub", + &wc->request.reserve_pub), + GNUNET_JSON_pack_data_auto ("planchets_h", + &wc->request.planchets_h), + GNUNET_JSON_pack_timestamp ("timestamp", + timestamp), + TALER_JSON_pack_amount ("total_amount", + &total_amount), + GNUNET_JSON_pack_array_steal ("recoups", + recoups), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub))); +} + + +/** + * Report the error in @a wc to the client. + * + * @param[in,out] wc context of the request + */ +static void +phase_generate_reply_error (struct RecoupWithdrawContext *wc) +{ + GNUNET_assert (RECOUP_WITHDRAW_PHASE_GENERATE_REPLY_ERROR == wc->phase); + switch (wc->error.code) + { + case RECOUP_WITHDRAW_ERROR_NONE: + break; + case RECOUP_WITHDRAW_ERROR_BATCH_SIZE_MISMATCH: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_RECOUP_WITHDRAW_BATCH_SIZE_MISMATCH, + NULL)); + return; + case RECOUP_WITHDRAW_ERROR_BLINDING_FAILED: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_RECOUP_BLINDING_FAILED, + NULL)); + return; + case RECOUP_WITHDRAW_ERROR_COIN_SIGNATURE_INVALID: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RECOUP_SIGNATURE_INVALID, + NULL)); + return; + case RECOUP_WITHDRAW_ERROR_COMMITMENT_MISMATCH: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_RECOUP_WITHDRAW_COMMITMENT_MISMATCH, + NULL)); + return; + case RECOUP_WITHDRAW_ERROR_CONFIRMATION_SIGN: + finish_loop (wc, + TALER_MHD_reply_with_ec ( + wc->rc->connection, + wc->error.details.ec_confirmation_sign, + NULL)); + return; + case RECOUP_WITHDRAW_ERROR_DB_FETCH_FAILED: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + wc->error.details.db_fetch_context)); + return; + case RECOUP_WITHDRAW_ERROR_DB_INVARIANT_FAILURE: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + NULL)); + return; + case RECOUP_WITHDRAW_ERROR_DENOMINATION_EXPIRED: + finish_loop (wc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + wc->rc->connection, + wc->error.details.denom_h, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "RECOUP-WITHDRAW")); + return; + case RECOUP_WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN: + finish_loop (wc, + TEH_RESPONSE_reply_unknown_denom_pub_hash ( + wc->rc->connection, + wc->error.details.denom_h)); + return; + case RECOUP_WITHDRAW_ERROR_DENOMINATION_MISMATCH: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_RECOUP_WITHDRAW_DENOMINATION_MISMATCH, + NULL)); + return; + case RECOUP_WITHDRAW_ERROR_DENOMINATION_NOT_ELIGIBLE: + finish_loop (wc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + wc->rc->connection, + wc->error.details.denom_h, + TALER_EC_EXCHANGE_RECOUP_NOT_ELIGIBLE, + "RECOUP-WITHDRAW")); + return; + case RECOUP_WITHDRAW_ERROR_DENOMINATION_SIGNATURE_INVALID: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, + NULL)); + return; + case RECOUP_WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE: + finish_loop (wc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + wc->rc->connection, + wc->error.details.denom_h, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "RECOUP-WITHDRAW")); + return; + case RECOUP_WITHDRAW_ERROR_INSUFFICIENT_FUNDS: + { + const struct TEH_RecoupCoin *c = wc->error.details.insufficient_funds; + + finish_loop (wc, + TEH_RESPONSE_reply_coin_insufficient_funds ( + wc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &c->coin.denom_pub_hash, + &c->coin.coin_pub)); + return; + } + case RECOUP_WITHDRAW_ERROR_KEYS_MISSING: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_SERVICE_UNAVAILABLE, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL)); + return; + case RECOUP_WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + wc->error.details.request_parameter_malformed)); + return; + case RECOUP_WITHDRAW_ERROR_WITHDRAW_NOT_FOUND: + finish_loop (wc, + TALER_MHD_reply_with_error ( + wc->rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_RECOUP_WITHDRAW_NOT_FOUND, + NULL)); + return; + } + GNUNET_break (0); + finish_loop (wc, + MHD_NO); +} + + +/** + * Release the data of a withdraw operation as returned by the database. + * + * @param[in,out] wd data to release + */ +static void +free_db_withdraw_data (struct TALER_EXCHANGEDB_Withdraw *wd) +{ + if (NULL != wd->denom_sigs) + { + for (size_t i = 0; i < wd->num_coins; i++) + TALER_blinded_denom_sig_free (&wd->denom_sigs[i]); + GNUNET_free (wd->denom_sigs); + } + GNUNET_free (wd->denom_serials); + GNUNET_free (wd->cs_r_values); + GNUNET_free (wd->denom_pub_hashes); +} + + +/** + * Cleanup routine for the request, called upon completion. + * + * @param rc request context to clean up + */ +static void +clean_recoup_withdraw_rc (struct TEH_RequestContext *rc) +{ + struct RecoupWithdrawContext *wc = rc->rh_ctx; + + TEH_recoup_free_coins (wc->request.num_coins, + wc->request.coins); + if (wc->have_withdraw) + free_db_withdraw_data (&wc->withdraw); + GNUNET_free (wc); +} + + enum MHD_Result -TEH_handler_recoup (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const json_t *root) +TEH_handler_recoup_withdraw ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[0]) { - enum GNUNET_GenericReturnValue ret; - struct TALER_CoinPublicInfo coin; - union GNUNET_CRYPTO_BlindingSecretP coin_bks; - struct TALER_CoinSpendSignatureP coin_sig; - struct TALER_ExchangeBlindingValues exchange_vals; - struct TALER_HashBlindedPlanchetsP h_planchets; - union GNUNET_CRYPTO_BlindSessionNonce nonce; - bool no_nonce; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", - &coin.denom_pub_hash), - TALER_JSON_spec_denom_sig ("denom_sig", - &coin.denom_sig), - GNUNET_JSON_spec_fixed_auto ("h_planchets", - &h_planchets), - TALER_JSON_spec_exchange_blinding_values ("ewv", - &exchange_vals), - GNUNET_JSON_spec_fixed_auto ("coin_blind_key_secret", - &coin_bks), - GNUNET_JSON_spec_fixed_auto ("coin_sig", - &coin_sig), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("h_age_commitment", - &coin.h_age_commitment), - &coin.no_age_commitment), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("nonce", - &nonce), - &no_nonce), - GNUNET_JSON_spec_end () - }; + struct RecoupWithdrawContext *wc = rc->rh_ctx; - memset (&coin, - 0, - sizeof (coin)); - coin.coin_pub = *coin_pub; - ret = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == ret) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == ret) - return MHD_YES; /* failure */ + (void) args; + if (NULL == wc) { - enum MHD_Result res; - - res = verify_and_execute_recoup (connection, - &coin, - &exchange_vals, - &coin_bks, - &h_planchets, - no_nonce - ? NULL - : &nonce, - &coin_sig); - GNUNET_JSON_parse_free (spec); - return res; + wc = GNUNET_new (struct RecoupWithdrawContext); + rc->rh_ctx = wc; + rc->rh_cleaner = &clean_recoup_withdraw_rc; + wc->rc = rc; + wc->now = GNUNET_TIME_timestamp_get (); + } + while (true) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "recoup-withdraw processing in phase %d\n", + wc->phase); + switch (wc->phase) + { + case RECOUP_WITHDRAW_PHASE_PARSE: + phase_parse (wc, + root); + break; + case RECOUP_WITHDRAW_PHASE_LOOKUP_OPERATION: + phase_lookup_operation (wc); + break; + case RECOUP_WITHDRAW_PHASE_CHECK_KEYS: + phase_check_keys (wc); + break; + case RECOUP_WITHDRAW_PHASE_VERIFY_COINS: + phase_verify_coins (wc); + break; + case RECOUP_WITHDRAW_PHASE_MAKE_COINS_KNOWN: + phase_make_coins_known (wc); + break; + case RECOUP_WITHDRAW_PHASE_RUN_TRANSACTION: + phase_run_transaction (wc); + break; + case RECOUP_WITHDRAW_PHASE_GENERATE_REPLY_SUCCESS: + phase_generate_reply_success (wc); + break; + case RECOUP_WITHDRAW_PHASE_GENERATE_REPLY_ERROR: + phase_generate_reply_error (wc); + break; + case RECOUP_WITHDRAW_PHASE_RETURN_YES: + return MHD_YES; + case RECOUP_WITHDRAW_PHASE_RETURN_NO: + return MHD_NO; + } } } -/* end of taler-exchange-httpd_recoup.c */ +/* end of taler-exchange-httpd_post-recoup-withdraw.c */ diff --git a/src/exchange/taler-exchange-httpd_post-recoup-withdraw.h b/src/exchange/taler-exchange-httpd_post-recoup-withdraw.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2017 Taler Systems SA + Copyright (C) 2017-2026 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -15,8 +15,9 @@ */ /** * @file taler-exchange-httpd_post-recoup-withdraw.h - * @brief Handle /recoup requests + * @brief Handle /recoup-withdraw requests * @author Christian Grothoff + * @author Özgür Kesim */ #ifndef TALER_EXCHANGE_HTTPD_POST_RECOUP_WITHDRAW_H #define TALER_EXCHANGE_HTTPD_POST_RECOUP_WITHDRAW_H @@ -27,20 +28,19 @@ /** - * Handle a "/coins/$COIN_PUB/recoup" request. Parses the JSON, and, if - * successful, passes the JSON data to #verify_and_execute_recoup() to further - * check the details of the operation specified. If everything checks out, - * this will ultimately lead to the refund being executed, or rejected. + * Handle a "/recoup-withdraw" request: pay the residual value of coins + * of revoked denominations, all originating from one withdraw operation, + * back to the reserve they were withdrawn from. * - * @param connection the MHD connection to handle - * @param coin_pub public key of the coin + * @param rc request context * @param root uploaded JSON data + * @param args empty array * @return MHD result code - */ + */ enum MHD_Result -TEH_handler_recoup (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const json_t *root); - +TEH_handler_recoup_withdraw ( + struct TEH_RequestContext *rc, + const json_t *root, + const char *const args[0]); #endif diff --git a/src/exchangedb/get_denomination_pub_by_serial.c b/src/exchangedb/get_denomination_pub_by_serial.c @@ -0,0 +1,57 @@ +/* + This file is part of TALER + Copyright (C) 2026 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/get_denomination_pub_by_serial.c + * @brief Implementation of the get_denomination_pub_by_serial function for Postgres + * @author Özgür Kesim + */ +#include "platform.h" +#include "taler/taler_error_codes.h" +#include "taler/taler_dbevents.h" +#include "exchangedb_lib.h" +#include "taler/taler_pq_lib.h" +#include "exchange-database/get_denomination_pub_by_serial.h" +#include "helper.h" + + +enum GNUNET_DB_QueryStatus +TALER_EXCHANGEDB_get_denomination_pub_by_serial ( + struct TALER_EXCHANGEDB_PostgresContext *pg, + uint64_t denom_serial, + struct TALER_DenominationPublicKey *denom_pub) +{ + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_uint64 (&denom_serial), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + TALER_PQ_result_spec_denom_pub ("denom_pub", + denom_pub), + GNUNET_PQ_result_spec_end + }; + + PREPARE (pg, + "get_denomination_pub_by_serial", + "SELECT" + " denom_pub" + " FROM denominations" + " WHERE denominations_serial=$1;"); + return GNUNET_PQ_eval_prepared_singleton_select ( + pg->conn, + "get_denomination_pub_by_serial", + params, + rs); +} diff --git a/src/exchangedb/get_old_coin_by_h_blind.c b/src/exchangedb/get_old_coin_by_h_blind.c @@ -1,60 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2022, 2025 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file exchangedb/get_old_coin_by_h_blind.c - * @brief Implementation of the get_old_coin_by_h_blind function for Postgres - * @author Christian Grothoff - * @author Özgür Kesim - */ -#include "taler/taler_pq_lib.h" -#include "exchange-database/get_old_coin_by_h_blind.h" -#include "helper.h" - - -enum GNUNET_DB_QueryStatus -TALER_EXCHANGEDB_get_old_coin_by_h_blind ( - struct TALER_EXCHANGEDB_PostgresContext *pg, - const struct TALER_BlindedCoinHashP *h_blind_ev, - struct TALER_CoinSpendPublicKeyP *old_coin_pub, - uint64_t *refresh_id) -{ - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (h_blind_ev), - GNUNET_PQ_query_param_end - }; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("old_coin_pub", - old_coin_pub), - GNUNET_PQ_result_spec_uint64 ("refresh_id", - refresh_id), - GNUNET_PQ_result_spec_end - }; - - /* Used in #postgres_get_old_coin_by_h_blind() */ - PREPARE (pg, - "get_old_coin_by_h_blind", - "SELECT" - " okc.coin_pub AS old_coin_pub" - ",refresh_id" - " FROM refresh " - " JOIN known_coins okc ON (refresh.old_coin_pub = okc.coin_pub)" - " WHERE $1=ANY(h_blind_evs)" - " LIMIT 1;"); - return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "get_old_coin_by_h_blind", - params, - rs); -} diff --git a/src/exchangedb/get_reserve_by_h_planchets.c b/src/exchangedb/get_reserve_by_h_planchets.c @@ -1,58 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2022,2025 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file exchangedb/get_reserve_by_h_planchets.c - * @brief Implementation of the get_reserve_by_h_planchets function for Postgres - * @author Christian Grothoff - * @author Özgür Kesim - */ -#include "taler/taler_pq_lib.h" -#include "exchange-database/get_reserve_by_h_planchets.h" -#include "helper.h" - - -enum GNUNET_DB_QueryStatus -TALER_EXCHANGEDB_get_reserve_by_h_planchets ( - struct TALER_EXCHANGEDB_PostgresContext *pg, - const struct TALER_HashBlindedPlanchetsP *h_planchets, - struct TALER_ReservePublicKeyP *reserve_pub, - uint64_t *withdraw_serial_id) -{ - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (h_planchets), - GNUNET_PQ_query_param_end - }; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("reserve_pub", - reserve_pub), - GNUNET_PQ_result_spec_uint64 ("withdraw_id", - withdraw_serial_id), - GNUNET_PQ_result_spec_end - }; - /* Used in #postgres_get_reserve_by_h_planchets() */ - PREPARE (pg, - "get_reserve_by_h_planchets", - "SELECT" - " reserve_pub" - ",withdraw_id" - " FROM withdraw" - " WHERE planchets_h=$1" - " LIMIT 1;"); - return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "get_reserve_by_h_planchets", - params, - rs); -} diff --git a/src/exchangedb/meson.build b/src/exchangedb/meson.build @@ -92,6 +92,7 @@ libtalerexchangedb = library( 'get_coin_denomination.c', 'get_coin_transactions.c', 'get_denomination_by_serial.c', + 'get_denomination_pub_by_serial.c', 'get_denomination_info.c', 'get_denomination_revocation.c', 'get_profit_drain.c', @@ -100,14 +101,12 @@ libtalerexchangedb = library( 'iterate_global_fees.c', 'get_known_coin.c', 'get_kyc_rules.c', - 'get_old_coin_by_h_blind.c', 'get_pending_legitimization_process.c', 'get_purse_deposit.c', 'get_purse_request.c', 'get_ready_deposit.c', 'get_refresh.c', 'get_reserve_balance.c', - 'get_reserve_by_h_planchets.c', 'get_reserve_history.c', 'get_signature_for_known_coin.c', 'iterate_unfinished_close_requests.c', diff --git a/src/exchangedb/test_denominations.c b/src/exchangedb/test_denominations.c @@ -22,6 +22,7 @@ * Covers #TALER_EXCHANGEDB_insert_denomination_info(), * #TALER_EXCHANGEDB_get_denomination_info(), * #TALER_EXCHANGEDB_get_denomination_by_serial(), + * #TALER_EXCHANGEDB_get_denomination_pub_by_serial(), * #TALER_EXCHANGEDB_get_denomination_meta(), * #TALER_EXCHANGEDB_iterate_denomination_info() and * #TALER_EXCHANGEDB_iterate_denominations(). @@ -33,6 +34,7 @@ #include "exchange-database/insert_denomination_info.h" #include "exchange-database/get_denomination_info.h" #include "exchange-database/get_denomination_by_serial.h" +#include "exchange-database/get_denomination_pub_by_serial.h" #include "exchange-database/get_denomination_meta.h" #include "exchange-database/iterate_denomination_info.h" #include "exchange-database/iterate_denominations.h" @@ -237,6 +239,25 @@ check_insert_and_lookup (struct TALER_EXCHANGEDB_PostgresContext *pg) FAILIF_C (0 != GNUNET_memcmp (&issue, &denom.issue), TDB_denom_free (&denom)); + /* the public key itself is available by serial, too */ + { + struct TALER_DenominationPublicKey pub = { 0 }; + + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_get_denomination_pub_by_serial (pg, + serial, + &pub), + TDB_denom_free (&denom)); + FAILIF_C (0 != TALER_denom_pub_cmp (&pub, + &denom.pub), + TALER_denom_pub_free (&pub); TDB_denom_free (&denom)); + TALER_denom_pub_free (&pub); + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != + TALER_EXCHANGEDB_get_denomination_pub_by_serial (pg, + serial + 1000, + &pub), + TDB_denom_free (&denom)); + } memset (&meta, 0, diff --git a/src/exchangedb/test_refresh.c b/src/exchangedb/test_refresh.c @@ -21,23 +21,16 @@ * * Covers #TALER_EXCHANGEDB_do_refresh(), #TALER_EXCHANGEDB_get_refresh(), * #TALER_EXCHANGEDB_update_to_refresh_revealed(), - * #TALER_EXCHANGEDB_iterate_refreshes_above_serial_id() and, as far as it - * can be, #TALER_EXCHANGEDB_get_old_coin_by_h_blind(). + * #TALER_EXCHANGEDB_iterate_refreshes_above_serial_id(). * * `refresh` references `known_coins`, which TDB_coin() creates. The * do_refresh() answers checked here are: unknown coin, insufficient coin * balance, the zombie requirement, blinding-seed reuse and an idempotent * replay. - * - * get_old_coin_by_h_blind() cannot be checked beyond "does not invent an - * answer": its statement selects on a `h_blind_evs` column that the current - * `refresh` table does not have, so it cannot succeed at all -- see EDBT-8 - * in bugs.txt. */ #include "test_common.h" #include "exchange-database/do_refresh.h" #include "exchange-database/get_known_coin.h" -#include "exchange-database/get_old_coin_by_h_blind.h" #include "exchange-database/get_refresh.h" #include "exchange-database/iterate_refreshes_above_serial_id.h" #include "exchange-database/update_to_refresh_revealed.h" @@ -709,36 +702,6 @@ check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg) /** - * A blinded coin hash that was never seen must not be attributed to any - * old coin. - * - * This is all get_old_coin_by_h_blind() can be checked for today: its - * statement selects on a `h_blind_evs` column that `refresh` does not - * have, so it cannot return a row at all (EDBT-8). The assertion below - * holds both now and once that is repaired. - * - * @param pg the database context - * @return 0 on success - */ -static int -check_old_coin_by_h_blind (struct TALER_EXCHANGEDB_PostgresContext *pg) -{ - struct TALER_BlindedCoinHashP h_blind_ev; - struct TALER_CoinSpendPublicKeyP old_coin_pub; - uint64_t rrc_serial; - - TDB_FILL (h_blind_ev, - 123); - FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == - TALER_EXCHANGEDB_get_old_coin_by_h_blind (pg, - &h_blind_ev, - &old_coin_pub, - &rrc_serial)); - return 0; -} - - -/** * The checks to run, in order. */ static const struct TDB_Test tests[] = { @@ -756,8 +719,6 @@ static const struct TDB_Test tests[] = { &check_revealed }, { "refresh-iterate", &check_iterate }, - { "refresh-old-coin-by-h-blind", - &check_old_coin_by_h_blind }, { NULL, NULL } }; diff --git a/src/exchangedb/test_withdraw.c b/src/exchangedb/test_withdraw.c @@ -21,7 +21,6 @@ * * Covers #TALER_EXCHANGEDB_do_withdraw(), * #TALER_EXCHANGEDB_get_withdraw(), - * #TALER_EXCHANGEDB_get_reserve_by_h_planchets(), * #TALER_EXCHANGEDB_iterate_withdrawals_above_serial_id() and * #TALER_EXCHANGEDB_iterate_withdraw_amounts_for_kyc_check(). * @@ -34,7 +33,6 @@ #include "exchange-database/do_withdraw.h" #include "exchange-database/get_withdraw.h" #include "exchange-database/get_reserve.h" -#include "exchange-database/get_reserve_by_h_planchets.h" #include "exchange-database/iterate_withdrawals_above_serial_id.h" #include "exchange-database/iterate_withdraw_amounts_for_kyc_check.h" @@ -360,7 +358,6 @@ check_unknown_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg) struct TALER_EXCHANGEDB_Withdraw wd; struct WithdrawStatus st; struct TALER_HashBlindedPlanchetsP h; - uint64_t withdraw_serial_id; TDB_denom (pg, 10, @@ -384,11 +381,6 @@ check_unknown_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg) "FROM withdraw")); TDB_FILL (h, 1); - FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != - TALER_EXCHANGEDB_get_reserve_by_h_planchets (pg, - &h, - &reserve_pub, - &withdraw_serial_id)); { struct TALER_EXCHANGEDB_Withdraw got; @@ -461,8 +453,6 @@ check_withdraw (struct TALER_EXCHANGEDB_PostgresContext *pg) struct WithdrawStatus st; struct TALER_Amount expect_balance = TDB_amount ("5"); struct TALER_Amount expect_left = TDB_amount ("5"); - struct TALER_ReservePublicKeyP got_pub; - uint64_t withdraw_serial_id = 0; TDB_reserve_in (pg, &account, @@ -505,18 +495,6 @@ check_withdraw (struct TALER_EXCHANGEDB_PostgresContext *pg) free_withdraw (&wd)); /* the request can be looked up by its planchet hash */ - FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != - TALER_EXCHANGEDB_get_reserve_by_h_planchets (pg, - &wd.planchets_h, - &got_pub, - &withdraw_serial_id), - free_withdraw (&wd)); - FAILIF_C (0 != GNUNET_memcmp (&got_pub, - &reserve_pub), - free_withdraw (&wd)); - FAILIF_C (0 == withdraw_serial_id, - free_withdraw (&wd)); - memset (&got, 0, sizeof (got)); @@ -528,6 +506,12 @@ check_withdraw (struct TALER_EXCHANGEDB_PostgresContext *pg) FAILIF_C (0 != TALER_amount_cmp (&got.amount_with_fee, &expect_balance), free_withdraw (&got); free_withdraw (&wd)); + /* ... and names its reserve and its row */ + FAILIF_C (0 != GNUNET_memcmp (&got.reserve_pub, + &reserve_pub), + free_withdraw (&got); free_withdraw (&wd)); + FAILIF_C (0 == got.withdraw_id, + free_withdraw (&got); free_withdraw (&wd)); FAILIF_C (0 != GNUNET_memcmp (&got.reserve_pub, &reserve_pub), free_withdraw (&got); free_withdraw (&wd)); diff --git a/src/include/exchange-database/get_denomination_pub_by_serial.h b/src/include/exchange-database/get_denomination_pub_by_serial.h @@ -0,0 +1,47 @@ +/* + This file is part of TALER + Copyright (C) 2026 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 src/include/exchange-database/get_denomination_pub_by_serial.h + * @brief look up the public key of a denomination by its serial + * @author Özgür Kesim + */ +#ifndef EXCHANGE_DATABASE_GET_DENOMINATION_PUB_BY_SERIAL_H +#define EXCHANGE_DATABASE_GET_DENOMINATION_PUB_BY_SERIAL_H + +#include "exchangedb_lib.h" + + +/** + * Fetch the public key of the denomination with serial @a denom_serial, + * regardless of whether the denomination is still active. Used where + * the exchange has to reconstruct an operation on coins of a + * denomination that may have expired since, such as a recoup. + * + * Primary test table: `denominations` (see test_denominations.c). + * + * @param pg the database context + * @param denom_serial serial of the denomination + * @param[out] denom_pub set to the public key; must be released with + * #TALER_denom_pub_free() on success + * @return transaction status code + */ +enum GNUNET_DB_QueryStatus +TALER_EXCHANGEDB_get_denomination_pub_by_serial ( + struct TALER_EXCHANGEDB_PostgresContext *pg, + uint64_t denom_serial, + struct TALER_DenominationPublicKey *denom_pub); + +#endif diff --git a/src/include/exchange-database/get_old_coin_by_h_blind.h b/src/include/exchange-database/get_old_coin_by_h_blind.h @@ -1,48 +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 src/include/exchange-database/get_old_coin_by_h_blind.h - * @brief implementation of the get_old_coin_by_h_blind function for Postgres - * @author Christian Grothoff - */ -#ifndef EXCHANGE_DATABASE_GET_OLD_COIN_BY_H_BLIND_H -#define EXCHANGE_DATABASE_GET_OLD_COIN_BY_H_BLIND_H - -#include "exchangedb_lib.h" - - -/** - * Obtain information about which old coin a coin was refreshed - * given the hash of the blinded (fresh) coin. - * - * Primary test table: `refresh` (see test_refresh.c). - * - * @param pg the database context - * @param h_blind_ev hash of the blinded coin - * @param[out] old_coin_pub set to information about the old coin (on success only) - * @param[out] rrc_serial set to serial number of the entry in the database - * @return transaction status code - */ -enum GNUNET_DB_QueryStatus -TALER_EXCHANGEDB_get_old_coin_by_h_blind (struct - TALER_EXCHANGEDB_PostgresContext *pg, - const struct TALER_BlindedCoinHashP * - h_blind_ev, - struct TALER_CoinSpendPublicKeyP * - old_coin_pub, - uint64_t *rrc_serial); - -#endif diff --git a/src/include/exchange-database/get_reserve_by_h_planchets.h b/src/include/exchange-database/get_reserve_by_h_planchets.h @@ -1,51 +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 src/include/exchange-database/get_reserve_by_h_planchets.h - * @brief implementation of the get_reserve_by_h_planchets function for Postgres - * @author Christian Grothoff - * @author Özgür Kesim - */ -#ifndef EXCHANGE_DATABASE_GET_RESERVE_BY_H_PLANCHETS_H -#define EXCHANGE_DATABASE_GET_RESERVE_BY_H_PLANCHETS_H - -#include "taler/taler_util.h" -#include "exchangedb_lib.h" - -/** - * Obtain information about which reserve a coin was generated - * from given the hash of the blinded coin. - * - * Primary test table: `withdraw` (see test_withdraw.c). - * - * @param pg the database context - * @param h_planchets hash that uniquely identifies the withdraw request - * @param[out] reserve_pub set to information about the reserve (on success only) - * @param[out] withdraw_serial_id set to row of the @a h_commitment in withdraw - * @return transaction status code - */ -enum GNUNET_DB_QueryStatus -TALER_EXCHANGEDB_get_reserve_by_h_planchets (struct - TALER_EXCHANGEDB_PostgresContext * - pg, - const struct - TALER_HashBlindedPlanchetsP * - h_planchets, - struct TALER_ReservePublicKeyP * - reserve_pub, - uint64_t *withdraw_serial_id); - -#endif