commit 0bd064af00a7c924c7071d0b284885618d0a0355
parent 9c882c3c06caf6bce1f248e68e496cb6283efdf8
Author: Özgür Kesim <oec-taler@kesim.org>
Date: Mon, 9 Dec 2024 20:12:13 +0100
code dedup in {age,batch}-withdraw
The code for batch- and age-withdraw have been de-duplicated and merged
into a single file src/exchange/taler-exchange-httpd_withdraw.c
This is the initial steps towards refactoring the withdraw
implementation and preparing the future withdraw-endpoint.
Fixes #9371
Diffstat:
8 files changed, 2259 insertions(+), 2369 deletions(-)
diff --git a/src/exchange/Makefile.am b/src/exchange/Makefile.am
@@ -130,7 +130,6 @@ taler_exchange_wirewatch_LDADD = \
taler_exchange_httpd_SOURCES = \
taler-exchange-httpd.c taler-exchange-httpd.h \
- taler-exchange-httpd_age-withdraw.c taler-exchange-httpd_age-withdraw.h \
taler-exchange-httpd_age-withdraw_reveal.c taler-exchange-httpd_age-withdraw_reveal.h \
taler-exchange-httpd_aml-attributes-get.c taler-exchange-httpd_aml-attributes-get.h \
taler-exchange-httpd_aml-decision.c taler-exchange-httpd_aml-decision.h \
@@ -139,7 +138,6 @@ taler_exchange_httpd_SOURCES = \
taler-exchange-httpd_aml-measures-get.c taler-exchange-httpd_aml-measures-get.h \
taler-exchange-httpd_auditors.c taler-exchange-httpd_auditors.h \
taler-exchange-httpd_batch-deposit.c taler-exchange-httpd_batch-deposit.h \
- taler-exchange-httpd_batch-withdraw.c taler-exchange-httpd_batch-withdraw.h \
taler-exchange-httpd_coins_get.c taler-exchange-httpd_coins_get.h \
taler-exchange-httpd_common_deposit.c taler-exchange-httpd_common_deposit.h \
taler-exchange-httpd_common_kyc.c taler-exchange-httpd_common_kyc.h \
@@ -195,7 +193,8 @@ taler_exchange_httpd_SOURCES = \
taler-exchange-httpd_responses.c taler-exchange-httpd_responses.h \
taler-exchange-httpd_spa.c taler-exchange-httpd_spa.h \
taler-exchange-httpd_terms.c taler-exchange-httpd_terms.h \
- taler-exchange-httpd_transfers_get.c taler-exchange-httpd_transfers_get.h
+ taler-exchange-httpd_transfers_get.c taler-exchange-httpd_transfers_get.h \
+ taler-exchange-httpd_withdraw.c taler-exchange-httpd_withdraw.h
taler_exchange_httpd_LDADD = \
diff --git a/src/exchange/taler-exchange-httpd.c b/src/exchange/taler-exchange-httpd.c
@@ -31,7 +31,7 @@
#include "taler_kyclogic_lib.h"
#include "taler_templating_lib.h"
#include "taler_mhd_lib.h"
-#include "taler-exchange-httpd_age-withdraw.h"
+#include "taler-exchange-httpd_withdraw.h"
#include "taler-exchange-httpd_age-withdraw_reveal.h"
#include "taler-exchange-httpd_aml-attributes-get.h"
#include "taler-exchange-httpd_aml-decision.h"
@@ -39,7 +39,6 @@
#include "taler-exchange-httpd_aml-measures-get.h"
#include "taler-exchange-httpd_auditors.h"
#include "taler-exchange-httpd_batch-deposit.h"
-#include "taler-exchange-httpd_batch-withdraw.h"
#include "taler-exchange-httpd_coins_get.h"
#include "taler-exchange-httpd_config.h"
#include "taler-exchange-httpd_contract.h"
@@ -2686,8 +2685,7 @@ do_shutdown (void *cls)
my_mhd = TALER_MHD_daemon_stop ();
TEH_resume_keys_requests (true);
TEH_batch_deposit_cleanup ();
- TEH_age_withdraw_cleanup ();
- TEH_batch_withdraw_cleanup ();
+ TEH_withdraw_cleanup ();
TEH_reserves_close_cleanup ();
TEH_reserves_purse_cleanup ();
TEH_purses_merge_cleanup ();
diff --git a/src/exchange/taler-exchange-httpd_age-withdraw.c b/src/exchange/taler-exchange-httpd_age-withdraw.c
@@ -1,1176 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2023, 2024 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation; either version 3,
- or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty
- of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
- See the GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General
- Public License along with TALER; see the file COPYING. If not,
- see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file taler-exchange-httpd_age-withdraw.c
- * @brief Handle /reserves/$RESERVE_PUB/age-withdraw requests
- * @author Özgür Kesim
- * @author Christian Grothoff
- */
-#include "platform.h"
-#include <gnunet/gnunet_common.h>
-#include <gnunet/gnunet_json_lib.h>
-#include <gnunet/gnunet_util_lib.h>
-#include <jansson.h>
-#include <microhttpd.h>
-#include "taler-exchange-httpd.h"
-#include "taler_error_codes.h"
-#include "taler_json_lib.h"
-#include "taler_kyclogic_lib.h"
-#include "taler_mhd_lib.h"
-#include "taler-exchange-httpd_age-withdraw.h"
-#include "taler-exchange-httpd_common_kyc.h"
-#include "taler-exchange-httpd_responses.h"
-#include "taler-exchange-httpd_keys.h"
-#include "taler_util.h"
-
-
-/**
- * Context for #age_withdraw_transaction.
- */
-struct AgeWithdrawContext
-{
-
- /**
- * Kept in a DLL.
- */
- struct AgeWithdrawContext *next;
-
- /**
- * Kept in a DLL.
- */
- struct AgeWithdrawContext *prev;
-
- /**
- * Handle for the legitimization check.
- */
- struct TEH_LegitimizationCheckHandle *lch;
-
- /**
- * request context
- */
- const struct TEH_RequestContext *rc;
-
- /**
- * Response to return, if set.
- */
- struct MHD_Response *response;
-
- /**
- * Public key of the reserve.
- */
- struct TALER_ReservePublicKeyP reserve_pub;
-
- /**
- * KYC status for the operation.
- */
- struct TALER_EXCHANGEDB_KycStatus kyc;
-
- /**
- * Set to the hash of the normalized payto URI that established
- * the reserve.
- */
- struct TALER_NormalizedPaytoHashP h_normalized_payto;
-
- /**
- * value the client committed to
- */
- struct TALER_AgeWithdrawCommitmentHashP ach;
-
- /**
- * Timestamp
- */
- struct GNUNET_TIME_Timestamp now;
-
- /**
- * The data from the age-withdraw request, as we persist it
- */
- struct TALER_EXCHANGEDB_AgeWithdraw commitment;
-
- /**
- * HTTP status to return with @e response, or 0.
- */
- unsigned int http_status;
-
- /**
- * Number of coins/denonations in the reveal
- */
- unsigned int num_coins;
-
- /**
- * #num_coins * #kappa hashes of blinded coin planchets.
- */
- struct TALER_BlindedPlanchet (*coin_evs) [ TALER_CNC_KAPPA];
-
- /**
- * #num_coins hashes of the denominations from which the coins are withdrawn.
- * Those must support age restriction.
- */
- struct TALER_DenominationHashP *denom_hs;
-
- /**
- * Current processing phase we are in.
- */
- enum
- {
- AWC_PHASE_CHECK_KEYS = 1,
- AWC_PHASE_CHECK_RESERVE_SIGNATURE,
- AWC_PHASE_RUN_LEGI_CHECK,
- AWC_PHASE_SUSPENDED,
- AWC_PHASE_CHECK_KYC_RESULT,
- AWC_PHASE_PREPARE_TRANSACTION,
- AWC_PHASE_RUN_TRANSACTION,
- AWC_PHASE_GENERATE_REPLY_SUCCESS,
- AWC_PHASE_GENERATE_REPLY_FAILURE,
- AWC_PHASE_RETURN_YES,
- AWC_PHASE_RETURN_NO
- } phase;
-
-};
-
-
-/**
- * Kept in a DLL.
- */
-static struct AgeWithdrawContext *awc_head;
-
-/**
- * Kept in a DLL.
- */
-static struct AgeWithdrawContext *awc_tail;
-
-
-void
-TEH_age_withdraw_cleanup ()
-{
- struct AgeWithdrawContext *awc;
-
- while (NULL != (awc = awc_head))
- {
- GNUNET_CONTAINER_DLL_remove (awc_head,
- awc_tail,
- awc);
- MHD_resume_connection (awc->rc->connection);
- }
-}
-
-
-/**
- * Terminate the main loop by returning the final
- * result.
- *
- * @param[in,out] awc context to update phase for
- * @param mres MHD status to return
- */
-static void
-finish_loop (struct AgeWithdrawContext *awc,
- MHD_RESULT mres)
-{
- awc->phase = (MHD_YES == mres)
- ? AWC_PHASE_RETURN_YES
- : AWC_PHASE_RETURN_NO;
-}
-
-
-/**
- * Send a response to a "age-withdraw" request.
- *
- * @param[in,out] awc context for the operation
- */
-static void
-reply_age_withdraw_success (
- struct AgeWithdrawContext *awc)
-{
- struct MHD_Connection *connection
- = awc->rc->connection;
- const struct TALER_AgeWithdrawCommitmentHashP *ach
- = &awc->commitment.h_commitment;
- uint32_t noreveal_index
- = awc->commitment.noreveal_index;
- struct TALER_ExchangePublicKeyP pub;
- struct TALER_ExchangeSignatureP sig;
- enum TALER_ErrorCode ec;
-
- ec = TALER_exchange_online_age_withdraw_confirmation_sign (
- &TEH_keys_exchange_sign_,
- ach,
- noreveal_index,
- &pub,
- &sig);
- if (TALER_EC_NONE != ec)
- {
- finish_loop (awc,
- TALER_MHD_reply_with_ec (connection,
- ec,
- NULL));
- return;
- }
- finish_loop (awc,
- TALER_MHD_REPLY_JSON_PACK (
- connection,
- MHD_HTTP_OK,
- GNUNET_JSON_pack_uint64 ("noreveal_index",
- noreveal_index),
- GNUNET_JSON_pack_data_auto ("exchange_sig",
- &sig),
- GNUNET_JSON_pack_data_auto ("exchange_pub",
- &pub)));
-}
-
-
-/**
- * Check if the request is replayed and we already have an
- * answer. If so, replay the existing answer and return the
- * HTTP response.
- *
- * @param[in,out] awc parsed request data
- * @return true if the request is idempotent with an existing request
- * false if we did not find the request in the DB and did not set @a mret
- */
-static bool
-check_request_idempotent (
- struct AgeWithdrawContext *awc)
-{
- enum GNUNET_DB_QueryStatus qs;
- struct TALER_EXCHANGEDB_AgeWithdraw commitment;
-
- qs = TEH_plugin->get_age_withdraw (
- TEH_plugin->cls,
- &awc->commitment.reserve_pub,
- &awc->commitment.h_commitment,
- &commitment);
- if (0 > qs)
- {
- /* FIXME: soft error not handled correctly! */
- GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
- if (GNUNET_DB_STATUS_HARD_ERROR == qs)
- finish_loop (awc,
- TALER_MHD_reply_with_ec (
- awc->rc->connection,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- "get_age_withdraw"));
- return true; /* Well, kind-of. */
- }
- if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
- return false;
-
- /* Generate idempotent reply */
- TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_AGE_WITHDRAW]++;
- awc->phase = AWC_PHASE_GENERATE_REPLY_SUCCESS;
- return true;
-}
-
-
-/**
- * Function implementing age withdraw transaction. Runs the
- * transaction logic; IF it returns a non-error code, the transaction
- * logic MUST NOT queue a MHD response. IF it returns an hard error,
- * the transaction logic MUST queue a MHD response and set @a mhd_ret.
- * IF it returns the soft error code, the function MAY be called again
- * to retry and MUST not queue a MHD response.
- *
- * @param cls a `struct AgeWithdrawContext *`
- * @param connection MHD request which triggered the transaction
- * @param[out] mhd_ret set to MHD response status for @a connection,
- * if transaction failed (!)
- * @return transaction status
- */
-static enum GNUNET_DB_QueryStatus
-age_withdraw_transaction (
- void *cls,
- struct MHD_Connection *connection,
- MHD_RESULT *mhd_ret)
-{
- struct AgeWithdrawContext *awc = cls;
- enum GNUNET_DB_QueryStatus qs;
- bool found = false;
- bool balance_ok = false;
- bool age_ok = false;
- bool conflict = false;
- uint16_t allowed_maximum_age = 0;
- uint32_t reserve_birthday = 0;
- struct TALER_Amount reserve_balance;
-
- qs = TEH_plugin->do_age_withdraw (
- TEH_plugin->cls,
- &awc->commitment,
- awc->now,
- &found,
- &balance_ok,
- &reserve_balance,
- &age_ok,
- &allowed_maximum_age,
- &reserve_birthday,
- &conflict);
- if (0 > qs)
- {
- if (GNUNET_DB_STATUS_HARD_ERROR == qs)
- finish_loop (awc,
- TALER_MHD_reply_with_ec (
- awc->rc->connection,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- "do_age_withdraw"));
- return qs;
- }
- if (! found)
- {
- finish_loop (awc,
- TALER_MHD_reply_with_ec (
- awc->rc->connection,
- TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN,
- NULL));
- return GNUNET_DB_STATUS_HARD_ERROR;
- }
- if (! age_ok)
- {
- finish_loop (awc,
- TALER_MHD_REPLY_JSON_PACK (
- awc->rc->connection,
- MHD_HTTP_CONFLICT,
- TALER_MHD_PACK_EC (
- TALER_EC_EXCHANGE_AGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE),
- GNUNET_JSON_pack_uint64 (
- "allowed_maximum_age",
- allowed_maximum_age),
- GNUNET_JSON_pack_uint64 (
- "reserve_birthday",
- reserve_birthday)));
- return GNUNET_DB_STATUS_HARD_ERROR;
- }
- if (! balance_ok)
- {
- TEH_plugin->rollback (TEH_plugin->cls);
- finish_loop (awc,
- TEH_RESPONSE_reply_reserve_insufficient_balance (
- awc->rc->connection,
- TALER_EC_EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS,
- &reserve_balance,
- &awc->commitment.amount_with_fee,
- &awc->commitment.reserve_pub));
- return GNUNET_DB_STATUS_HARD_ERROR;
- }
- if (conflict)
- {
- /* do_age_withdraw signaled a conflict, so there MUST be an entry
- * in the DB. Put that into the response */
- if (check_request_idempotent (awc))
- return GNUNET_DB_STATUS_HARD_ERROR;
- GNUNET_break (0);
- return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
- }
- if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
- TEH_METRICS_num_success[TEH_MT_SUCCESS_AGE_WITHDRAW]++;
- return qs;
-}
-
-
-/**
- * @brief Persist the commitment.
- *
- * On conflict, the noreveal_index from the previous, existing
- * commitment is returned to the client, returning success.
- *
- * On error (like, insufficient funds), the client is notified.
- *
- * @param awc The context for the current age withdraw request
- */
-static void
-run_transaction (
- struct AgeWithdrawContext *awc)
-{
- MHD_RESULT mhd_ret;
-
- GNUNET_assert (AWC_PHASE_RUN_TRANSACTION ==
- awc->phase);
- if (GNUNET_OK !=
- TEH_DB_run_transaction (awc->rc->connection,
- "run age withdraw",
- TEH_MT_REQUEST_AGE_WITHDRAW,
- &mhd_ret,
- &age_withdraw_transaction,
- awc))
- {
- if (AWC_PHASE_RUN_TRANSACTION == awc->phase)
- finish_loop (awc,
- mhd_ret);
- return;
- }
- awc->phase++;
-}
-
-
-/**
- * @brief Sign the chosen blinded coins.
- *
- * @param awc The context for the current age withdraw request
- */
-static void
-prepare_transaction (
- struct AgeWithdrawContext *awc)
-{
- uint8_t noreveal_index;
-
- awc->commitment.denom_sigs
- = GNUNET_new_array (
- awc->num_coins,
- struct TALER_BlindedDenominationSignature);
- awc->commitment.h_coin_evs
- = GNUNET_new_array (
- awc->num_coins,
- struct TALER_BlindedCoinHashP);
- /* Pick the challenge */
- noreveal_index =
- GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_STRONG,
- TALER_CNC_KAPPA);
- awc->commitment.noreveal_index = noreveal_index;
-
- /* Choose and sign the coins */
- {
- struct TEH_CoinSignData csds[awc->num_coins];
- enum TALER_ErrorCode ec;
-
- /* Pick the chosen blinded coins */
- for (uint32_t i = 0; i<awc->num_coins; i++)
- {
- struct TEH_CoinSignData *csdsi = &csds[i];
-
- csdsi->bp = &awc->coin_evs[i][noreveal_index];
- csdsi->h_denom_pub = &awc->denom_hs[i];
- }
-
- ec = TEH_keys_denomination_batch_sign (
- awc->num_coins,
- csds,
- false,
- awc->commitment.denom_sigs);
- if (TALER_EC_NONE != ec)
- {
- GNUNET_break (0);
- finish_loop (awc,
- TALER_MHD_reply_with_ec (
- awc->rc->connection,
- ec,
- NULL));
- return;
- }
- }
-
- /* Prepare the hashes of the coins for insertion */
- for (uint32_t i = 0; i<awc->num_coins; i++)
- {
- TALER_coin_ev_hash (&awc->coin_evs[i][noreveal_index],
- &awc->denom_hs[i],
- &awc->commitment.h_coin_evs[i]);
- }
- awc->phase++;
-}
-
-
-/**
- * Check the KYC result.
- *
- * @param awc storage for request processing
- */
-static void
-check_kyc_result (struct AgeWithdrawContext *awc)
-{
- /* return final positive response */
- if (! awc->kyc.ok)
- {
- if (check_request_idempotent (awc))
- return;
- /* KYC required */
- finish_loop (awc,
- TEH_RESPONSE_reply_kyc_required (
- awc->rc->connection,
- &awc->h_normalized_payto,
- &awc->kyc,
- false));
- return;
- }
- awc->phase++;
-}
-
-
-/**
- * Function called with the result of a legitimization
- * check.
- *
- * @param cls closure
- * @param lcr legitimization check result
- */
-static void
-withdraw_legi_cb (
- void *cls,
- const struct TEH_LegitimizationCheckResult *lcr)
-{
- struct AgeWithdrawContext *awc = cls;
-
- awc->lch = NULL;
- GNUNET_assert (AWC_PHASE_SUSPENDED ==
- awc->phase);
- MHD_resume_connection (awc->rc->connection);
- GNUNET_CONTAINER_DLL_remove (awc_head,
- awc_tail,
- awc);
- TALER_MHD_daemon_trigger ();
- if (NULL != lcr->response)
- {
- awc->response = lcr->response;
- awc->http_status = lcr->http_status;
- awc->phase = AWC_PHASE_GENERATE_REPLY_FAILURE;
- return;
- }
- awc->kyc = lcr->kyc;
- awc->phase = AWC_PHASE_CHECK_KYC_RESULT;
-}
-
-
-/**
- * Function called to iterate over KYC-relevant transaction amounts for a
- * particular time range. Called within a database transaction, so must
- * not start a new one.
- *
- * @param cls closure, identifies the event type and account to iterate
- * over events for
- * @param limit maximum time-range for which events should be fetched
- * (timestamp in the past)
- * @param cb function to call on each event found, events must be returned
- * in reverse chronological order
- * @param cb_cls closure for @a cb, of type struct AgeWithdrawContext
- * @return transaction status
- */
-static enum GNUNET_DB_QueryStatus
-withdraw_amount_cb (
- void *cls,
- struct GNUNET_TIME_Absolute limit,
- TALER_EXCHANGEDB_KycAmountCallback cb,
- void *cb_cls)
-{
- struct AgeWithdrawContext *awc = cls;
- enum GNUNET_GenericReturnValue ret;
- enum GNUNET_DB_QueryStatus qs;
-
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Signaling amount %s for KYC check during age-withdrawal\n",
- TALER_amount2s (&awc->commitment.amount_with_fee));
- ret = cb (cb_cls,
- &awc->commitment.amount_with_fee,
- awc->now.abs_time);
- GNUNET_break (GNUNET_SYSERR != ret);
- if (GNUNET_OK != ret)
- return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS;
- qs = TEH_plugin->select_withdraw_amounts_for_kyc_check (
- TEH_plugin->cls,
- &awc->h_normalized_payto,
- limit,
- cb,
- cb_cls);
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Got %d additional transactions for this age-withdrawal and limit %llu\n",
- qs,
- (unsigned long long) limit.abs_value_us);
- GNUNET_break (qs >= 0);
- return qs;
-}
-
-
-/**
- * Do legitimization check.
- *
- * @param awc operation context
- */
-static void
-run_legi_check (struct AgeWithdrawContext *awc)
-{
- enum GNUNET_DB_QueryStatus qs;
- struct TALER_FullPayto payto_uri;
- struct TALER_FullPaytoHashP h_full_payto;
-
- /* Check if the money came from a wire transfer */
- qs = TEH_plugin->reserves_get_origin (
- TEH_plugin->cls,
- &awc->commitment.reserve_pub,
- &h_full_payto,
- &payto_uri);
- if (qs < 0)
- {
- finish_loop (awc,
- TALER_MHD_reply_with_error (
- awc->rc->connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- "reserves_get_origin"));
- return;
- }
- /* If _no_ results, reserve was created by merge,
- in which case no KYC check is required as the
- merge already did that. */
- if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
- {
- awc->phase = AWC_PHASE_PREPARE_TRANSACTION;
- return;
- }
- TALER_full_payto_normalize_and_hash (payto_uri,
- &awc->h_normalized_payto);
- awc->lch = TEH_legitimization_check (
- &awc->rc->async_scope_id,
- TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW,
- payto_uri,
- &awc->h_normalized_payto,
- NULL, /* no account pub: this is about the origin account */
- &withdraw_amount_cb,
- awc,
- &withdraw_legi_cb,
- awc);
- GNUNET_assert (NULL != awc->lch);
- GNUNET_free (payto_uri.full_payto);
- GNUNET_CONTAINER_DLL_insert (awc_head,
- awc_tail,
- awc);
- MHD_suspend_connection (awc->rc->connection);
- awc->phase = AWC_PHASE_SUSPENDED;
-}
-
-
-/**
- * Check that the client signature authorizing the
- * withdrawal is valid.
- *
- * @param[in,out] awc request context to check
- */
-static void
-check_reserve_signature (
- struct AgeWithdrawContext *awc)
-{
- TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++;
- if (GNUNET_OK !=
- TALER_wallet_age_withdraw_verify (
- &awc->commitment.h_commitment,
- &awc->commitment.amount_with_fee,
- &TEH_age_restriction_config.mask,
- awc->commitment.max_age,
- &awc->commitment.reserve_pub,
- &awc->commitment.reserve_sig))
- {
- GNUNET_break_op (0);
- finish_loop (awc,
- TALER_MHD_reply_with_ec (
- awc->rc->connection,
- TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID,
- NULL));
- return;
- }
- awc->phase++;
-}
-
-
-/**
- * Check if the given denomination is still or already valid, has not been
- * revoked and supports age restriction.
- *
- * @param[in,out] awc context for the operation
- * @param ksh The handle to the current state of (denomination) keys in the exchange
- * @param denom_h Hash of the denomination key to check
- * @return NULL on failure (denomination invalid)
- */
-static struct TEH_DenominationKey *
-denomination_is_valid (
- struct AgeWithdrawContext *awc,
- struct TEH_KeyStateHandle *ksh,
- const struct TALER_DenominationHashP *denom_h)
-{
- struct MHD_Connection *connection = awc->rc->connection;
- struct TEH_DenominationKey *dk;
- MHD_RESULT result;
-
- dk = TEH_keys_denomination_by_hash_from_state (
- ksh,
- denom_h,
- connection,
- &result);
- if (NULL == dk)
- {
- /* The denomination doesn't exist */
- /* Note: a HTTP-response has been queued and result has been set by
- * TEH_keys_denominations_by_hash_from_state */
- /* FIXME-Oec: lacks idempotency check... */
- finish_loop (awc,
- result);
- return NULL;
- }
-
- if (GNUNET_TIME_absolute_is_past (dk->meta.expire_withdraw.abs_time))
- {
- /* This denomination is past the expiration time for withdrawc */
- /* FIXME[oec]: add idempotency check */
- finish_loop (awc,
- TEH_RESPONSE_reply_expired_denom_pub_hash (
- connection,
- denom_h,
- TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED,
- "age-withdraw_reveal"));
- return NULL;
- }
-
- if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time))
- {
- /* This denomination is not yet valid */
- finish_loop (awc,
- TEH_RESPONSE_reply_expired_denom_pub_hash (
- connection,
- denom_h,
- TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE,
- "age-withdraw_reveal"));
- return NULL;
- }
-
- if (dk->recoup_possible)
- {
- /* This denomination has been revoked */
- finish_loop (awc,
- TALER_MHD_reply_with_ec (
- connection,
- TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED,
- NULL));
- return NULL;
- }
-
- if (0 == dk->denom_pub.age_mask.bits)
- {
- /* This denomation does not support age restriction */
- char msg[256];
-
- GNUNET_snprintf (msg,
- sizeof(msg),
- "denomination %s does not support age restriction",
- GNUNET_h2s (&denom_h->hash));
-
- finish_loop (awc,
- TALER_MHD_reply_with_ec (
- connection,
- TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN,
- msg));
- return NULL;
- }
-
- return dk;
-}
-
-
-/**
- * Check if the given array of hashes of denomination_keys a) belong
- * to valid denominations and b) those are marked as age restricted.
- * Also, calculate the total amount of the denominations including fees
- * for withdraw.
- *
- * @param awc context to check keys for
- */
-static void
-check_keys (
- struct AgeWithdrawContext *awc)
-{
- struct MHD_Connection *connection
- = awc->rc->connection;
- unsigned int len
- = awc->num_coins;
- struct TALER_Amount total_amount;
- struct TALER_Amount total_fee;
- struct TEH_KeyStateHandle *ksh;
-
- ksh = TEH_keys_get_state ();
- if (NULL == ksh)
- {
- finish_loop (awc,
- TALER_MHD_reply_with_ec (
- connection,
- TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
- NULL));
- return;
- }
-
- awc->commitment.denom_serials
- = GNUNET_new_array (len,
- uint64_t);
- GNUNET_assert (GNUNET_OK ==
- TALER_amount_set_zero (TEH_currency,
- &total_amount));
- GNUNET_assert (GNUNET_OK ==
- TALER_amount_set_zero (TEH_currency,
- &total_fee));
- for (unsigned int i = 0; i < len; i++)
- {
- struct TEH_DenominationKey *dk;
-
- dk = denomination_is_valid (awc,
- ksh,
- &awc->denom_hs[i]);
- if (NULL == dk)
- /* FIXME[oec]: add idempotency check */
- return;
-
- /* Ensure the ciphers from the planchets match the denominations' */
- for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++)
- {
- if (dk->denom_pub.bsign_pub_key->cipher !=
- awc->coin_evs[i][k].blinded_message->cipher)
- {
- GNUNET_break_op (0);
- finish_loop (awc,
- TALER_MHD_reply_with_ec (
- connection,
- TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH,
- NULL));
- return;
- }
- }
-
- /* Accumulate the values */
- if (0 > TALER_amount_add (&total_amount,
- &total_amount,
- &dk->meta.value))
- {
- GNUNET_break_op (0);
- finish_loop (awc,
- TALER_MHD_reply_with_error (
- connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW,
- "amount"));
- return;
- }
-
- /* Accumulate the withdraw fees */
- if (0 > TALER_amount_add (&total_fee,
- &total_fee,
- &dk->meta.fees.withdraw))
- {
- GNUNET_break_op (0);
- finish_loop (awc,
- TALER_MHD_reply_with_error (
- connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW,
- "fee"));
- return;
- }
- awc->commitment.denom_serials[i] = dk->meta.serial;
- }
-
- /* Save the total amount including fees */
- GNUNET_assert (0 <
- TALER_amount_add (
- &awc->commitment.amount_with_fee,
- &total_amount,
- &total_fee));
- awc->phase++;
-}
-
-
-/**
- * Age-withdraw-specific cleanup routine. Function called
- * upon completion of the request that should
- * clean up @a rh_ctx. Can be NULL.
- *
- * @param rc request context to clean up
- */
-static void
-clean_age_withdraw_rc (struct TEH_RequestContext *rc)
-{
- struct AgeWithdrawContext *awc = rc->rh_ctx;
-
- for (unsigned int i = 0; i<awc->num_coins; i++)
- {
- for (unsigned int kappa = 0; kappa<TALER_CNC_KAPPA; kappa++)
- {
- TALER_blinded_planchet_free (&awc->coin_evs[i][kappa]);
- }
- }
- for (unsigned int i = 0; i<awc->num_coins; i++)
- {
- TALER_blinded_denom_sig_free (&awc->commitment.denom_sigs[i]);
- }
- GNUNET_free (awc->commitment.h_coin_evs);
- GNUNET_free (awc->commitment.denom_sigs);
- GNUNET_free (awc->denom_hs);
- GNUNET_free (awc->coin_evs);
- GNUNET_free (awc->commitment.denom_serials);
- GNUNET_free (awc);
-}
-
-
-MHD_RESULT
-TEH_handler_age_withdraw (
- struct TEH_RequestContext *rc,
- const struct TALER_ReservePublicKeyP *reserve_pub,
- const json_t *root)
-{
- struct AgeWithdrawContext *awc = rc->rh_ctx;
-
- if (NULL == awc)
- {
- awc = GNUNET_new (struct AgeWithdrawContext);
- rc->rh_ctx = awc;
- rc->rh_cleaner = &clean_age_withdraw_rc;
- awc->rc = rc;
- awc->commitment.reserve_pub = *reserve_pub;
- awc->now = GNUNET_TIME_timestamp_get ();
-
- {
- const json_t *j_denom_hs;
- const json_t *j_blinded_coin_evs;
- struct GNUNET_JSON_Specification spec[] = {
- GNUNET_JSON_spec_array_const ("denom_hs",
- &j_denom_hs),
- GNUNET_JSON_spec_array_const ("blinded_coin_evs",
- &j_blinded_coin_evs),
- GNUNET_JSON_spec_uint16 ("max_age",
- &awc->commitment.max_age),
- GNUNET_JSON_spec_fixed_auto ("reserve_sig",
- &awc->commitment.reserve_sig),
- GNUNET_JSON_spec_end ()
- };
- enum GNUNET_GenericReturnValue res;
-
- res = TALER_MHD_parse_json_data (rc->connection,
- root,
- spec);
- if (GNUNET_OK != res)
- return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES;
-
- /* The age value MUST be on the beginning of an age group */
- if (awc->commitment.max_age !=
- TALER_get_lowest_age (&TEH_age_restriction_config.mask,
- awc->commitment.max_age))
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_ec (
- rc->connection,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "max_age must be the lower edge of an age group");
- }
-
- {
- size_t num_coins = json_array_size (j_denom_hs);
- const char *error = NULL;
-
- _Static_assert ((TALER_MAX_FRESH_COINS < INT_MAX / TALER_CNC_KAPPA),
- "TALER_MAX_FRESH_COINS too large");
- if (0 == num_coins)
- error = "denoms_h must not be empty";
- else if (num_coins != json_array_size (j_blinded_coin_evs))
- error = "denoms_h and coins_evs must be arrays of the same size";
- else if (num_coins > TALER_MAX_FRESH_COINS)
- /**
- * The wallet had committed to more than the maximum coins allowed, the
- * reserve has been charged, but now the user can not withdraw any money
- * from it. Note that the user can't get their money back in this case!
- */
- error =
- "maximum number of coins that can be withdrawn has been exceeded";
-
- if (NULL != error)
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_ec (
- rc->connection,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- error);
- }
- awc->num_coins = (unsigned int) num_coins;
- awc->commitment.num_coins = (unsigned int) num_coins;
- }
-
- awc->denom_hs
- = GNUNET_new_array (awc->num_coins,
- struct TALER_DenominationHashP);
- {
- size_t idx;
- json_t *value;
-
- json_array_foreach (j_denom_hs, idx, value) {
- struct GNUNET_JSON_Specification ispec[] = {
- GNUNET_JSON_spec_fixed_auto (NULL,
- &awc->denom_hs[idx]),
- GNUNET_JSON_spec_end ()
- };
-
- res = TALER_MHD_parse_json_data (rc->connection,
- value,
- ispec);
- if (GNUNET_OK != res)
- return (GNUNET_SYSERR == res)
- ? MHD_NO
- : MHD_YES;
- }
- }
- {
- typedef struct TALER_BlindedPlanchet
- _array_of_kappa_planchets[TALER_CNC_KAPPA];
-
- awc->coin_evs = GNUNET_new_array (awc->num_coins,
- _array_of_kappa_planchets);
- }
- {
- struct GNUNET_HashContext *hash_context;
-
- hash_context = GNUNET_CRYPTO_hash_context_start ();
- GNUNET_assert (NULL != hash_context);
-
- /* Parse blinded envelopes. */
- {
- json_t *j_kappa_coin_evs;
- size_t idx;
-
- json_array_foreach (j_blinded_coin_evs, idx, j_kappa_coin_evs) {
- if (! json_is_array (j_kappa_coin_evs))
- {
- char buf[256];
-
- GNUNET_snprintf (
- buf,
- sizeof(buf),
- "entry %u in array blinded_coin_evs must be an array",
- (unsigned int) (idx + 1));
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_ec (
- rc->connection,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- buf);
- }
- if (TALER_CNC_KAPPA != json_array_size (j_kappa_coin_evs))
- {
- char buf[256];
-
- GNUNET_snprintf (buf,
- sizeof(buf),
- "array no. %u in coin_evs must have length %u",
- (unsigned int) (idx + 1),
- (unsigned int) TALER_CNC_KAPPA);
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_ec (
- rc->connection,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- buf);
- }
-
- /* Now parse the individual kappa envelopes and calculate the hash of
- * the commitment along the way. */
- {
- size_t kappa;
- json_t *kvalue;
-
- json_array_foreach (j_kappa_coin_evs, kappa, kvalue) {
- struct GNUNET_JSON_Specification kspec[] = {
- TALER_JSON_spec_blinded_planchet (NULL,
- &awc->coin_evs[idx][kappa]),
- GNUNET_JSON_spec_end ()
- };
-
- res = TALER_MHD_parse_json_data (rc->connection,
- kvalue,
- kspec);
- if (GNUNET_OK != res)
- return (GNUNET_SYSERR == res)
- ? MHD_NO
- : MHD_YES;
- /* Continue to hash of the coin candidates */
- {
- struct TALER_BlindedCoinHashP bch;
-
- TALER_coin_ev_hash (&awc->coin_evs[idx][kappa],
- &awc->denom_hs[idx],
- &bch);
- GNUNET_CRYPTO_hash_context_read (hash_context,
- &bch,
- sizeof(bch));
- }
-
- /* Check for duplicate planchets. Technically a bug on
- * the client side that is harmless for us, but still
- * not allowed per protocol */
- for (unsigned int i = 0; i < idx; i++)
- {
- if (0 ==
- TALER_blinded_planchet_cmp (
- &awc->coin_evs[idx][kappa],
- &awc->coin_evs[i][kappa]))
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_ec (
- rc->connection,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "duplicate planchet");
- }
- } /* end duplicate check */
- } /* json_array_foreach over j_kappa_coin_evs */
- } /* scope of kappa/kvalue */
- } /* json_array_foreach over j_blinded_coin_evs */
- } /* scope of j_kappa_coin_evs, idx */
-
- /* Finally, calculate the h_commitment from all blinded envelopes */
- GNUNET_CRYPTO_hash_context_finish (hash_context,
- &awc->commitment.h_commitment.hash);
-
- } /* scope of hash_context */
- } /* scope of j_denom_hs, j_blinded_coin_evs */
-
- awc->phase = AWC_PHASE_CHECK_KEYS;
- } /* end of if NULL == awc */
-
- while (true)
- {
- switch (awc->phase)
- {
- case AWC_PHASE_CHECK_KEYS:
- check_keys (awc);
- break;
- case AWC_PHASE_CHECK_RESERVE_SIGNATURE:
- check_reserve_signature (awc);
- break;
- case AWC_PHASE_RUN_LEGI_CHECK:
- run_legi_check (awc);
- break;
- case AWC_PHASE_SUSPENDED:
- return MHD_YES;
- case AWC_PHASE_CHECK_KYC_RESULT:
- check_kyc_result (awc);
- break;
- case AWC_PHASE_PREPARE_TRANSACTION:
- prepare_transaction (awc);
- break;
- case AWC_PHASE_RUN_TRANSACTION:
- run_transaction (awc);
- break;
- case AWC_PHASE_GENERATE_REPLY_SUCCESS:
- reply_age_withdraw_success (awc);
- break;
- case AWC_PHASE_GENERATE_REPLY_FAILURE:
- return MHD_queue_response (rc->connection,
- awc->http_status,
- awc->response);
- case AWC_PHASE_RETURN_YES:
- return MHD_YES;
- case AWC_PHASE_RETURN_NO:
- return MHD_NO;
- }
- }
-}
-
-
-/* end of taler-exchange-httpd_age-withdraw.c */
diff --git a/src/exchange/taler-exchange-httpd_age-withdraw.h b/src/exchange/taler-exchange-httpd_age-withdraw.h
@@ -1,55 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2023 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU Affero General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file taler-exchange-httpd_age-withdraw.h
- * @brief Handle /reserve/$RESERVE_PUB/age-withdraw requests
- * @author Özgür Kesim
- */
-#ifndef TALER_EXCHANGE_HTTPD_AGE_WITHDRAW_H
-#define TALER_EXCHANGE_HTTPD_AGE_WITHDRAW_H
-
-#include <microhttpd.h>
-#include "taler-exchange-httpd.h"
-
-
-/**
- * Resume suspended connections, we are shutting down.
- */
-void
-TEH_age_withdraw_cleanup (void);
-
-
-/**
- * Handle a "/reserves/$RESERVE_PUB/age-withdraw" request.
- *
- * Parses the batch of commitments to withdraw age restricted coins, and checks
- * that the signature "reserve_sig" makes this a valid withdrawal request from
- * the specified reserve. If the request is valid, the response contains a
- * noreveal_index which the client has to use for the subsequent call to
- * /age-withdraw/$ACH/reveal.
- *
- * @param rc request context
- * @param root uploaded JSON data
- * @param reserve_pub public key of the reserve
- * @return MHD result code
- */
-MHD_RESULT
-TEH_handler_age_withdraw (
- struct TEH_RequestContext *rc,
- const struct TALER_ReservePublicKeyP *reserve_pub,
- const json_t *root);
-
-#endif
diff --git a/src/exchange/taler-exchange-httpd_batch-withdraw.c b/src/exchange/taler-exchange-httpd_batch-withdraw.c
@@ -1,1077 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2014-2024 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation; either version 3,
- or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty
- of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
- See the GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General
- Public License along with TALER; see the file COPYING. If not,
- see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file taler-exchange-httpd_batch-withdraw.c
- * @brief Handle /reserves/$RESERVE_PUB/batch-withdraw requests
- * @author Florian Dold
- * @author Benedikt Mueller
- * @author Christian Grothoff
- */
-#include "platform.h"
-#include <gnunet/gnunet_util_lib.h>
-#include <jansson.h>
-#include "taler-exchange-httpd.h"
-#include "taler_json_lib.h"
-#include "taler_kyclogic_lib.h"
-#include "taler_mhd_lib.h"
-#include "taler-exchange-httpd_batch-withdraw.h"
-#include "taler-exchange-httpd_common_kyc.h"
-#include "taler-exchange-httpd_responses.h"
-#include "taler-exchange-httpd_keys.h"
-#include "taler_util.h"
-
-
-/**
- * Information per planchet in the batch.
- */
-struct PlanchetContext
-{
-
- /**
- * Value of the coin being exchanged (matching the denomination key)
- * plus the transaction fee. We include this in what is being
- * signed so that we can verify a reserve's remaining total balance
- * without needing to access the respective denomination key
- * information each time.
- */
- struct TALER_Amount amount_with_fee;
-
- /**
- * Blinded planchet.
- */
- struct TALER_BlindedPlanchet blinded_planchet;
-
- /**
- * Set to the resulting signed coin data to be returned to the client.
- */
- struct TALER_EXCHANGEDB_CollectableBlindcoin collectable;
-
-};
-
-/**
- * Context for #batch_withdraw_transaction.
- */
-struct BatchWithdrawContext
-{
-
- /**
- * Kept in a DLL.
- */
- struct BatchWithdrawContext *prev;
-
- /**
- * Kept in a DLL.
- */
- struct BatchWithdrawContext *next;
-
- /**
- * Handle for the legitimization check.
- */
- struct TEH_LegitimizationCheckHandle *lch;
-
- /**
- * request context
- */
- const struct TEH_RequestContext *rc;
-
- /**
- * Response to return, if set.
- */
- struct MHD_Response *response;
-
- /**
- * Public key of the reserve.
- */
- struct TALER_ReservePublicKeyP reserve_pub;
-
- /**
- * KYC status of the reserve used for the operation.
- */
- struct TALER_EXCHANGEDB_KycStatus kyc;
-
- /**
- * Hash of payto:// URI of the bank account that
- * established the reserve, set during the @e kyc
- * check (if any).
- */
- struct TALER_NormalizedPaytoHashP h_normalized_payto;
-
- /**
- * Array of @e planchets_length planchets we are processing.
- */
- struct PlanchetContext *planchets;
-
- /**
- * Current time for the DB transaction.
- */
- struct GNUNET_TIME_Timestamp now;
-
- /**
- * Total amount from all coins with fees.
- */
- struct TALER_Amount batch_total;
-
- /**
- * Length of the @e planchets array.
- */
- unsigned int planchets_length;
-
- /**
- * HTTP status to return with @e response, or 0.
- */
- unsigned int http_status;
-
- /**
- * Processing phase we are in.
- */
- enum
- {
- BWC_PHASE_CHECK_KEYS,
- BWC_PHASE_RUN_LEGI_CHECK,
- BWC_PHASE_SUSPENDED,
- BWC_PHASE_CHECK_KYC_RESULT,
- BWC_PHASE_PREPARE_TRANSACTION,
- BWC_PHASE_RUN_TRANSACTION,
- BWC_PHASE_GENERATE_REPLY_SUCCESS,
- BWC_PHASE_GENERATE_REPLY_FAILURE,
- BWC_PHASE_RETURN_YES,
- BWC_PHASE_RETURN_NO
- } phase;
-
-};
-
-
-/**
- * Kept in a DLL.
- */
-static struct BatchWithdrawContext *bwc_head;
-
-/**
- * Kept in a DLL.
- */
-static struct BatchWithdrawContext *bwc_tail;
-
-
-void
-TEH_batch_withdraw_cleanup ()
-{
- struct BatchWithdrawContext *bwc;
-
- while (NULL != (bwc = bwc_head))
- {
- GNUNET_CONTAINER_DLL_remove (bwc_head,
- bwc_tail,
- bwc);
- MHD_resume_connection (bwc->rc->connection);
- }
-}
-
-
-/**
- * Terminate the main loop by returning the final
- * result.
- *
- * @param[in,out] bwc context to update phase for
- * @param mres MHD status to return
- */
-static void
-finish_loop (struct BatchWithdrawContext *bwc,
- MHD_RESULT mres)
-{
- bwc->phase = (MHD_YES == mres)
- ? BWC_PHASE_RETURN_YES
- : BWC_PHASE_RETURN_NO;
-}
-
-
-/**
- * Generates our final (successful) response.
- *
- * @param bwc operation context
- */
-static void
-generate_reply_success (struct BatchWithdrawContext *bwc)
-{
- const struct TEH_RequestContext *rc = bwc->rc;
- json_t *sigs;
-
- sigs = json_array ();
- GNUNET_assert (NULL != sigs);
- for (unsigned int i = 0; i<bwc->planchets_length; i++)
- {
- struct PlanchetContext *pc = &bwc->planchets[i];
-
- GNUNET_assert (
- 0 ==
- json_array_append_new (
- sigs,
- GNUNET_JSON_PACK (
- TALER_JSON_pack_blinded_denom_sig (
- "ev_sig",
- &pc->collectable.sig))));
- }
- TEH_METRICS_batch_withdraw_num_coins += bwc->planchets_length;
- finish_loop (bwc,
- TALER_MHD_REPLY_JSON_PACK (
- rc->connection,
- MHD_HTTP_OK,
- GNUNET_JSON_pack_array_steal ("ev_sigs",
- sigs)));
-}
-
-
-/**
- * Check if the @a bwc is replayed and we already have an
- * answer. If so, replay the existing answer and return the
- * HTTP response.
- *
- * @param bwc parsed request data
- * @return true if the request is idempotent with an existing request
- * false if we did not find the request in the DB and did not set @a mret
- */
-static bool
-check_request_idempotent (
- struct BatchWithdrawContext *bwc)
-{
- const struct TEH_RequestContext *rc = bwc->rc;
-
- for (unsigned int i = 0; i<bwc->planchets_length; i++)
- {
- struct PlanchetContext *pc = &bwc->planchets[i];
- enum GNUNET_DB_QueryStatus qs;
- struct TALER_EXCHANGEDB_CollectableBlindcoin collectable;
-
- qs = TEH_plugin->get_withdraw_info (
- TEH_plugin->cls,
- &pc->collectable.h_coin_envelope,
- &collectable);
- if (0 > qs)
- {
- /* FIXME: soft error not handled correctly! */
- GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
- finish_loop (bwc,
- TALER_MHD_reply_with_error (
- rc->connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- "get_withdraw_info"));
- return true;
- }
- if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
- return false;
- pc->collectable = collectable;
- }
- /* generate idempotent reply */
- TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_BATCH_WITHDRAW]++;
- bwc->phase = BWC_PHASE_GENERATE_REPLY_SUCCESS;
- return true;
-}
-
-
-/**
- * Function implementing withdraw transaction. Runs the
- * transaction logic; IF it returns a non-error code, the transaction
- * logic MUST NOT queue a MHD response. IF it returns an hard error,
- * the transaction logic MUST queue a MHD response and set @a mhd_ret.
- * IF it returns the soft error code, the function MAY be called again
- * to retry and MUST not queue a MHD response.
- *
- * Note that "bwc->collectable.sig" is set before entering this function as we
- * signed before entering the transaction.
- *
- * @param cls a `struct BatchWithdrawContext *`
- * @param connection MHD request which triggered the transaction
- * @param[out] mhd_ret set to MHD response status for @a connection,
- * if transaction failed (!)
- * @return transaction status
- */
-static enum GNUNET_DB_QueryStatus
-batch_withdraw_transaction (
- void *cls,
- struct MHD_Connection *connection,
- MHD_RESULT *mhd_ret)
-{
- struct BatchWithdrawContext *bwc = cls;
- uint64_t ruuid;
- enum GNUNET_DB_QueryStatus qs;
- bool found = false;
- bool balance_ok = false;
- bool age_ok = false;
- uint16_t allowed_maximum_age = 0;
- struct TALER_Amount reserve_balance;
-
- qs = TEH_plugin->do_batch_withdraw (
- TEH_plugin->cls,
- bwc->now,
- &bwc->reserve_pub,
- &bwc->batch_total,
- TEH_age_restriction_enabled,
- &found,
- &balance_ok,
- &reserve_balance,
- &age_ok,
- &allowed_maximum_age,
- &ruuid);
- if (0 > qs)
- {
- if (GNUNET_DB_STATUS_HARD_ERROR == qs)
- {
- GNUNET_break (0);
- finish_loop (bwc,
- TALER_MHD_reply_with_error (
- connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- "update_reserve_batch_withdraw"));
- return qs;
- }
- return qs;
- }
- if (! found)
- {
- finish_loop (bwc,
- TALER_MHD_reply_with_error (
- connection,
- MHD_HTTP_NOT_FOUND,
- TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN,
- NULL));
- return GNUNET_DB_STATUS_HARD_ERROR;
- }
-
- if (! age_ok)
- {
- /* We respond with the lowest age in the corresponding age group
- * of the required age */
- uint16_t lowest_age = TALER_get_lowest_age (
- &TEH_age_restriction_config.mask,
- allowed_maximum_age);
-
- finish_loop (bwc,
- TEH_RESPONSE_reply_reserve_age_restriction_required (
- connection,
- lowest_age));
- return GNUNET_DB_STATUS_HARD_ERROR;
- }
-
- if (! balance_ok)
- {
- if (check_request_idempotent (bwc))
- return GNUNET_DB_STATUS_HARD_ERROR;
- finish_loop (bwc,
- TEH_RESPONSE_reply_reserve_insufficient_balance (
- connection,
- TALER_EC_EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS,
- &reserve_balance,
- &bwc->batch_total,
- &bwc->reserve_pub));
- return GNUNET_DB_STATUS_HARD_ERROR;
- }
-
- /* Add information about each planchet in the batch */
- for (unsigned int i = 0; i<bwc->planchets_length; i++)
- {
- struct PlanchetContext *pc = &bwc->planchets[i];
- const struct TALER_BlindedPlanchet *bp = &pc->blinded_planchet;
- const union GNUNET_CRYPTO_BlindSessionNonce *nonce = NULL;
- bool denom_unknown = true;
- bool conflict = true;
- bool nonce_reuse = true;
-
- switch (bp->blinded_message->cipher)
- {
- case GNUNET_CRYPTO_BSA_INVALID:
- break;
- case GNUNET_CRYPTO_BSA_RSA:
- break;
- case GNUNET_CRYPTO_BSA_CS:
- nonce = (const union GNUNET_CRYPTO_BlindSessionNonce *)
- &bp->blinded_message->details.cs_blinded_message.nonce;
- break;
- }
- qs = TEH_plugin->do_batch_withdraw_insert (
- TEH_plugin->cls,
- nonce,
- &pc->collectable,
- bwc->now,
- ruuid,
- &denom_unknown,
- &conflict,
- &nonce_reuse);
- if (0 > qs)
- {
- if (GNUNET_DB_STATUS_HARD_ERROR == qs)
- finish_loop (bwc,
- TALER_MHD_reply_with_error (
- connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- "do_batch_withdraw_insert"));
- return qs;
- }
- if (denom_unknown)
- {
- GNUNET_break (0);
- finish_loop (bwc,
- TALER_MHD_reply_with_error (
- connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
- NULL));
- return GNUNET_DB_STATUS_HARD_ERROR;
- }
- if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) ||
- (conflict) )
- {
- if (check_request_idempotent (bwc))
- return GNUNET_DB_STATUS_HARD_ERROR;
- /* We do not support *some* of the coins of the request being
- idempotent while others being fresh. */
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Idempotent coin in batch, not allowed. Aborting.\n");
- finish_loop (bwc,
- TALER_MHD_reply_with_error (
- connection,
- MHD_HTTP_CONFLICT,
- TALER_EC_EXCHANGE_WITHDRAW_BATCH_IDEMPOTENT_PLANCHET,
- NULL));
- return GNUNET_DB_STATUS_HARD_ERROR;
- }
- if (nonce_reuse)
- {
- GNUNET_break_op (0);
- finish_loop (bwc,
- TALER_MHD_reply_with_error (
- connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_EXCHANGE_WITHDRAW_NONCE_REUSE,
- NULL));
- return GNUNET_DB_STATUS_HARD_ERROR;
- }
- }
- TEH_METRICS_num_success[TEH_MT_SUCCESS_BATCH_WITHDRAW]++;
- return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
-}
-
-
-/**
- * The request was prepared successfully. Run
- * the main DB transaction.
- *
- * @param bwc storage for request processing
- */
-static void
-run_transaction (struct BatchWithdrawContext *bwc)
-{
- MHD_RESULT mhd_ret;
-
- GNUNET_assert (BWC_PHASE_RUN_TRANSACTION ==
- bwc->phase);
- if (GNUNET_OK !=
- TEH_DB_run_transaction (bwc->rc->connection,
- "run batch withdraw",
- TEH_MT_REQUEST_WITHDRAW,
- &mhd_ret,
- &batch_withdraw_transaction,
- bwc))
- {
- if (BWC_PHASE_RUN_TRANSACTION == bwc->phase)
- finish_loop (bwc,
- mhd_ret);
- return;
- }
- bwc->phase++;
-}
-
-
-/**
- * The request was parsed successfully. Prepare
- * our side for the main DB transaction.
- *
- * @param bwc storage for request processing
- */
-static void
-prepare_transaction (struct BatchWithdrawContext *bwc)
-{
- const struct TEH_RequestContext *rc = bwc->rc;
- struct TALER_BlindedDenominationSignature bss[bwc->planchets_length];
- struct TEH_CoinSignData csds[bwc->planchets_length];
-
- for (unsigned int i = 0; i<bwc->planchets_length; i++)
- {
- struct PlanchetContext *pc = &bwc->planchets[i];
- struct TEH_CoinSignData *csdsi = &csds[i];
-
- csdsi->h_denom_pub = &pc->collectable.denom_pub_hash;
- csdsi->bp = &pc->blinded_planchet;
- }
- {
- enum TALER_ErrorCode ec;
-
- ec = TEH_keys_denomination_batch_sign (
- bwc->planchets_length,
- csds,
- false,
- bss);
- if (TALER_EC_NONE != ec)
- {
- GNUNET_break (0);
- finish_loop (bwc,
- TALER_MHD_reply_with_ec (
- rc->connection,
- ec,
- NULL));
- return;
- }
- }
- for (unsigned int i = 0; i<bwc->planchets_length; i++)
- {
- struct PlanchetContext *pc = &bwc->planchets[i];
-
- pc->collectable.sig = bss[i];
- }
- bwc->phase++;
-}
-
-
-/**
- * Check the KYC result.
- *
- * @param bwc storage for request processing
- */
-static void
-check_kyc_result (struct BatchWithdrawContext *bwc)
-{
- /* return final positive response */
- if (! bwc->kyc.ok)
- {
- if (check_request_idempotent (bwc))
- {
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Request is idempotent!\n");
- return;
- }
- /* KYC required */
- finish_loop (bwc,
- TEH_RESPONSE_reply_kyc_required (
- bwc->rc->connection,
- &bwc->h_normalized_payto,
- &bwc->kyc,
- false));
- return;
- }
- bwc->phase++;
-}
-
-
-/**
- * Function called with the result of a legitimization
- * check.
- *
- * @param cls closure
- * @param lcr legitimization check result
- */
-static void
-withdraw_legi_cb (
- void *cls,
- const struct TEH_LegitimizationCheckResult *lcr)
-{
- struct BatchWithdrawContext *bwc = cls;
-
- bwc->lch = NULL;
- GNUNET_assert (BWC_PHASE_SUSPENDED ==
- bwc->phase);
- MHD_resume_connection (bwc->rc->connection);
- GNUNET_CONTAINER_DLL_remove (bwc_head,
- bwc_tail,
- bwc);
- TALER_MHD_daemon_trigger ();
- if (NULL != lcr->response)
- {
- bwc->response = lcr->response;
- bwc->http_status = lcr->http_status;
- bwc->phase = BWC_PHASE_GENERATE_REPLY_FAILURE;
- return;
- }
- bwc->kyc = lcr->kyc;
- bwc->phase = BWC_PHASE_CHECK_KYC_RESULT;
-}
-
-
-/**
- * Function called to iterate over KYC-relevant transaction amounts for a
- * particular time range. Called within a database transaction, so must
- * not start a new one.
- *
- * @param cls closure, identifies the event type and account to iterate
- * over events for
- * @param limit maximum time-range for which events should be fetched
- * (timestamp in the past)
- * @param cb function to call on each event found, events must be returned
- * in reverse chronological order
- * @param cb_cls closure for @a cb, of type struct AgeWithdrawContext
- * @return transaction status
- */
-static enum GNUNET_DB_QueryStatus
-withdraw_amount_cb (
- void *cls,
- struct GNUNET_TIME_Absolute limit,
- TALER_EXCHANGEDB_KycAmountCallback cb,
- void *cb_cls)
-{
- struct BatchWithdrawContext *bwc = cls;
- enum GNUNET_GenericReturnValue ret;
- enum GNUNET_DB_QueryStatus qs;
-
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Signaling amount %s for KYC check during age-withdrawal\n",
- TALER_amount2s (&bwc->batch_total));
- ret = cb (cb_cls,
- &bwc->batch_total,
- bwc->now.abs_time);
- GNUNET_break (GNUNET_SYSERR != ret);
- if (GNUNET_OK != ret)
- return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS;
- qs = TEH_plugin->select_withdraw_amounts_for_kyc_check (
- TEH_plugin->cls,
- &bwc->h_normalized_payto,
- limit,
- cb,
- cb_cls);
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Got %d additional transactions for this age-withdrawal and limit %llu\n",
- qs,
- (unsigned long long) limit.abs_value_us);
- GNUNET_break (qs >= 0);
- return qs;
-}
-
-
-/**
- * Do legitimization check.
- *
- * @param bwc operation context
- */
-static void
-run_legi_check (struct BatchWithdrawContext *bwc)
-{
- enum GNUNET_DB_QueryStatus qs;
- struct TALER_FullPaytoHashP h_payto;
- struct TALER_FullPayto payto_uri;
-
- if (GNUNET_YES != TEH_enable_kyc)
- {
- bwc->phase = BWC_PHASE_PREPARE_TRANSACTION;
- return;
- }
- /* Check if the money came from a wire transfer */
- qs = TEH_plugin->reserves_get_origin (
- TEH_plugin->cls,
- &bwc->reserve_pub,
- &h_payto,
- &payto_uri);
- if (qs < 0)
- {
- finish_loop (bwc,
- TALER_MHD_reply_with_error (
- bwc->rc->connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- "reserves_get_origin"));
- return;
- }
- /* If _no_ results, reserve was created by merge,
- in which case no KYC check is required as the
- merge already did that. */
- if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
- {
- bwc->phase = BWC_PHASE_PREPARE_TRANSACTION;
- return;
- }
-
- TALER_full_payto_normalize_and_hash (payto_uri,
- &bwc->h_normalized_payto);
-
- bwc->lch = TEH_legitimization_check (
- &bwc->rc->async_scope_id,
- TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW,
- payto_uri,
- &bwc->h_normalized_payto,
- NULL, /* no account pub: this is about the origin account */
- &withdraw_amount_cb,
- bwc,
- &withdraw_legi_cb,
- bwc);
- GNUNET_assert (NULL != bwc->lch);
- GNUNET_free (payto_uri.full_payto);
- GNUNET_CONTAINER_DLL_insert (bwc_head,
- bwc_tail,
- bwc);
- MHD_suspend_connection (bwc->rc->connection);
- bwc->phase = BWC_PHASE_SUSPENDED;
-}
-
-
-/**
- * Check if the keys in the request are valid for
- * withdrawing.
- *
- * @param[in,out] bwc storage for request processing
- */
-static void
-check_keys (struct BatchWithdrawContext *bwc)
-{
- const struct TEH_RequestContext *rc = bwc->rc;
- struct TEH_KeyStateHandle *ksh;
-
- ksh = TEH_keys_get_state ();
- if (NULL == ksh)
- {
- if (check_request_idempotent (bwc))
- return;
- GNUNET_break (0);
- finish_loop (bwc,
- TALER_MHD_reply_with_error (
- rc->connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
- NULL));
- return;
- }
- for (unsigned int i = 0; i<bwc->planchets_length; i++)
- {
- struct PlanchetContext *pc = &bwc->planchets[i];
- struct TEH_DenominationKey *dk;
-
-
- dk = TEH_keys_denomination_by_hash_from_state (
- ksh,
- &pc->collectable.denom_pub_hash,
- NULL,
- NULL);
- if (NULL == dk)
- {
- if (check_request_idempotent (bwc))
- return;
- GNUNET_break_op (0);
- finish_loop (bwc,
- TEH_RESPONSE_reply_unknown_denom_pub_hash (
- rc->connection,
- &pc->collectable.denom_pub_hash));
- return;
- }
- if (GNUNET_TIME_absolute_is_past (
- dk->meta.expire_withdraw.abs_time))
- {
- /* This denomination is past the expiration time for withdraws */
- if (check_request_idempotent (bwc))
- return;
- GNUNET_break_op (0);
- finish_loop (bwc,
- TEH_RESPONSE_reply_expired_denom_pub_hash (
- rc->connection,
- &pc->collectable.denom_pub_hash,
- TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED,
- "WITHDRAW"));
- return;
- }
- if (GNUNET_TIME_absolute_is_future (
- dk->meta.start.abs_time))
- {
- /* This denomination is not yet valid, no need to check
- for idempotency! */
- GNUNET_break_op (0);
- finish_loop (bwc,
- TEH_RESPONSE_reply_expired_denom_pub_hash (
- rc->connection,
- &pc->collectable.denom_pub_hash,
- TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE,
- "WITHDRAW"));
- }
- if (dk->recoup_possible)
- {
- /* This denomination has been revoked */
- if (check_request_idempotent (bwc))
- return;
- GNUNET_break_op (0);
- finish_loop (bwc,
- TEH_RESPONSE_reply_expired_denom_pub_hash (
- rc->connection,
- &pc->collectable.denom_pub_hash,
- TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED,
- "WITHDRAW"));
- return;
- }
- if (dk->denom_pub.bsign_pub_key->cipher !=
- pc->blinded_planchet.blinded_message->cipher)
- {
- /* denomination cipher and blinded planchet cipher not the same */
- GNUNET_break_op (0);
- finish_loop (bwc,
- TALER_MHD_reply_with_error (
- rc->connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH,
- NULL));
- }
- if (0 >
- TALER_amount_add (&pc->collectable.amount_with_fee,
- &dk->meta.value,
- &dk->meta.fees.withdraw))
- {
- GNUNET_break (0);
- finish_loop (bwc,
- TALER_MHD_reply_with_error (rc->connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW,
- NULL));
- return;
- }
- if (0 >
- TALER_amount_add (&bwc->batch_total,
- &bwc->batch_total,
- &pc->collectable.amount_with_fee))
- {
- GNUNET_break (0);
- finish_loop (bwc,
- TALER_MHD_reply_with_error (
- rc->connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW,
- NULL));
- return;
- }
-
- TALER_coin_ev_hash (&pc->blinded_planchet,
- &pc->collectable.denom_pub_hash,
- &pc->collectable.h_coin_envelope);
-
- TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++;
- if (GNUNET_OK !=
- TALER_wallet_withdraw_verify (
- &pc->collectable.denom_pub_hash,
- &pc->collectable.amount_with_fee,
- &pc->collectable.h_coin_envelope,
- &pc->collectable.reserve_pub,
- &pc->collectable.reserve_sig))
- {
- GNUNET_break_op (0);
- finish_loop (bwc,
- TALER_MHD_reply_with_error (
- rc->connection,
- MHD_HTTP_FORBIDDEN,
- TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID,
- NULL));
- return;
- }
- }
- bwc->phase++;
- /* everything parsed */
-}
-
-
-/**
- * Batch-withdraw-specific cleanup routine. Function called
- * upon completion of the request that should
- * clean up @a rh_ctx. Can be NULL.
- *
- * @param rc request context to clean up
- */
-static void
-clean_batch_withdraw_rc (struct TEH_RequestContext *rc)
-{
- struct BatchWithdrawContext *bwc = rc->rh_ctx;
-
- if (NULL != bwc->lch)
- {
- TEH_legitimization_check_cancel (bwc->lch);
- bwc->lch = NULL;
- }
- for (unsigned int i = 0; i<bwc->planchets_length; i++)
- {
- struct PlanchetContext *pc = &bwc->planchets[i];
-
- TALER_blinded_planchet_free (&pc->blinded_planchet);
- TALER_blinded_denom_sig_free (&pc->collectable.sig);
- }
- GNUNET_free (bwc->planchets);
- if (NULL != bwc->response)
- {
- MHD_destroy_response (bwc->response);
- bwc->response = NULL;
- }
- GNUNET_free (bwc);
-}
-
-
-MHD_RESULT
-TEH_handler_batch_withdraw (
- struct TEH_RequestContext *rc,
- const struct TALER_ReservePublicKeyP *reserve_pub,
- const json_t *root)
-{
- struct BatchWithdrawContext *bwc = rc->rh_ctx;
-
- if (NULL == bwc)
- {
- const json_t *planchets;
-
- bwc = GNUNET_new (struct BatchWithdrawContext);
- rc->rh_ctx = bwc;
- rc->rh_cleaner = &clean_batch_withdraw_rc;
- bwc->rc = rc;
- bwc->reserve_pub = *reserve_pub;
- bwc->now = GNUNET_TIME_timestamp_get ();
- GNUNET_assert (GNUNET_OK ==
- TALER_amount_set_zero (TEH_currency,
- &bwc->batch_total));
-
- {
- struct GNUNET_JSON_Specification spec[] = {
- GNUNET_JSON_spec_array_const ("planchets",
- &planchets),
- GNUNET_JSON_spec_end ()
- };
-
- {
- enum GNUNET_GenericReturnValue res;
-
- res = TALER_MHD_parse_json_data (rc->connection,
- root,
- spec);
- if (GNUNET_OK != res)
- return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES;
- }
- }
-
- bwc->planchets_length = json_array_size (planchets);
- if (0 == bwc->planchets_length)
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (
- rc->connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "planchets");
- }
- if (bwc->planchets_length > TALER_MAX_FRESH_COINS)
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (
- rc->connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "too many planchets");
- }
-
- bwc->planchets
- = GNUNET_new_array (bwc->planchets_length,
- struct PlanchetContext);
-
- for (unsigned int i = 0; i<bwc->planchets_length; i++)
- {
- struct PlanchetContext *pc = &bwc->planchets[i];
- struct GNUNET_JSON_Specification ispec[] = {
- GNUNET_JSON_spec_fixed_auto (
- "reserve_sig",
- &pc->collectable.reserve_sig),
- GNUNET_JSON_spec_fixed_auto (
- "denom_pub_hash",
- &pc->collectable.denom_pub_hash),
- TALER_JSON_spec_blinded_planchet (
- "coin_ev",
- &pc->blinded_planchet),
- GNUNET_JSON_spec_end ()
- };
-
- {
- enum GNUNET_GenericReturnValue res;
-
- res = TALER_MHD_parse_json_data (
- rc->connection,
- json_array_get (planchets,
- i),
- ispec);
- if (GNUNET_OK != res)
- return (GNUNET_SYSERR == res)
- ? MHD_NO
- : MHD_YES;
- }
- pc->collectable.reserve_pub = bwc->reserve_pub;
- for (unsigned int k = 0; k<i; k++)
- {
- const struct PlanchetContext *kpc = &bwc->planchets[k];
-
- if (0 ==
- TALER_blinded_planchet_cmp (
- &kpc->blinded_planchet,
- &pc->blinded_planchet))
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (
- rc->connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "duplicate planchet");
- }
- }
- }
- bwc->phase = BWC_PHASE_CHECK_KEYS;
- }
-
- while (true)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Batch withdraw processing in phase %d\n",
- bwc->phase);
- switch (bwc->phase)
- {
- case BWC_PHASE_CHECK_KEYS:
- check_keys (bwc);
- break;
- case BWC_PHASE_RUN_LEGI_CHECK:
- run_legi_check (bwc);
- break;
- case BWC_PHASE_SUSPENDED:
- return MHD_YES;
- case BWC_PHASE_CHECK_KYC_RESULT:
- check_kyc_result (bwc);
- break;
- case BWC_PHASE_PREPARE_TRANSACTION:
- prepare_transaction (bwc);
- break;
- case BWC_PHASE_RUN_TRANSACTION:
- run_transaction (bwc);
- break;
- case BWC_PHASE_GENERATE_REPLY_SUCCESS:
- generate_reply_success (bwc);
- break;
- case BWC_PHASE_GENERATE_REPLY_FAILURE:
- return MHD_queue_response (rc->connection,
- bwc->http_status,
- bwc->response);
- case BWC_PHASE_RETURN_YES:
- return MHD_YES;
- case BWC_PHASE_RETURN_NO:
- return MHD_NO;
- }
- }
-}
-
-
-/* end of taler-exchange-httpd_batch-withdraw.c */
diff --git a/src/exchange/taler-exchange-httpd_batch-withdraw.h b/src/exchange/taler-exchange-httpd_batch-withdraw.h
@@ -1,54 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2014-2022 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU 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_batch-withdraw.h
- * @brief Handle /reserve/batch-withdraw requests
- * @author Florian Dold
- * @author Benedikt Mueller
- * @author Christian Grothoff
- */
-#ifndef TALER_EXCHANGE_HTTPD_BATCH_WITHDRAW_H
-#define TALER_EXCHANGE_HTTPD_BATCH_WITHDRAW_H
-
-#include <microhttpd.h>
-#include "taler-exchange-httpd.h"
-
-
-/**
- * Resume suspended connections, we are shutting down.
- */
-void
-TEH_batch_withdraw_cleanup (void);
-
-/**
- * Handle a "/reserves/$RESERVE_PUB/batch-withdraw" request. Parses the batch of
- * requested "denom_pub" which specifies the key/value of the coin to be
- * withdrawn, and checks that the signature "reserve_sig" makes this a valid
- * withdrawal request from the specified reserve. If so, the envelope with
- * the blinded coin "coin_ev" is passed down to execute the withdrawal
- * operation.
- *
- * @param rc request context
- * @param root uploaded JSON data
- * @param reserve_pub public key of the reserve
- * @return MHD result code
- */
-MHD_RESULT
-TEH_handler_batch_withdraw (struct TEH_RequestContext *rc,
- const struct TALER_ReservePublicKeyP *reserve_pub,
- const json_t *root);
-
-#endif
diff --git a/src/exchange/taler-exchange-httpd_withdraw.c b/src/exchange/taler-exchange-httpd_withdraw.c
@@ -0,0 +1,2178 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2024 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as
+ published by the Free Software Foundation; either version 3,
+ or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty
+ of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General
+ Public License along with TALER; see the file COPYING. If not,
+ see <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * @file taler-exchange-httpd_withdraw.c
+ * @brief Common code to handle /reserves/$RESERVE_PUB/{age,batch}-withdraw requests
+ * @author Florian Dold
+ * @author Benedikt Mueller
+ * @author Christian Grothoff
+ * @author Ozgur Kesim
+ */
+#include "platform.h"
+#include <gnunet/gnunet_util_lib.h>
+#include <jansson.h>
+#include "taler-exchange-httpd.h"
+#include "taler_json_lib.h"
+#include "taler_kyclogic_lib.h"
+#include "taler_mhd_lib.h"
+#include "taler-exchange-httpd_withdraw.h"
+#include "taler-exchange-httpd_common_kyc.h"
+#include "taler-exchange-httpd_responses.h"
+#include "taler-exchange-httpd_keys.h"
+#include "taler_util.h"
+
+
+/**
+ * Information per planchet in a batch withdraw.
+ */
+struct PlanchetContext
+{
+
+ /**
+ * Value of the coin being exchanged (matching the denomination key)
+ * plus the transaction fee. We include this in what is being
+ * signed so that we can verify a reserve's remaining total balance
+ * without needing to access the respective denomination key
+ * information each time.
+ */
+ struct TALER_Amount amount_with_fee;
+
+ /**
+ * Blinded planchet.
+ */
+ struct TALER_BlindedPlanchet blinded_planchet;
+
+ /**
+ * Set to the resulting signed coin data to be returned to the client.
+ */
+ struct TALER_EXCHANGEDB_CollectableBlindcoin collectable;
+
+};
+
+/**
+ * Context for both,
+ * 1.) #batch_withdraw_transaction
+ * 2.) #age_withdraw_transaction
+ */
+struct WithdrawContext
+{
+
+ /**
+ * This struct is kept in a DLL.
+ */
+ struct WithdrawContext *prev;
+ struct WithdrawContext *next;
+
+ /**
+ * What type of withdraw is represented here.
+ * See union #.typ for type-specific data.
+ */
+ enum WithdrawType
+ {
+ WITHDRAW_TYPE_BATCH,
+ WITHDRAW_TYPE_AGE
+ } withdraw_type;
+
+ /**
+ * Processing phase we are in for any of the withdraw types.
+ * The ordering here partially matters, as we progress through
+ * them by incrementing the phase in the happy path.
+ */
+ enum
+ {
+ WC_PHASE_CHECK_KEYS,
+ WC_PHASE_CHECK_RESERVE_SIGNATURE,
+ WC_PHASE_RUN_LEGI_CHECK,
+ WC_PHASE_SUSPENDED,
+ WC_PHASE_CHECK_KYC_RESULT,
+ WC_PHASE_PREPARE_TRANSACTION,
+ WC_PHASE_RUN_TRANSACTION,
+ WC_PHASE_GENERATE_REPLY_SUCCESS,
+ WC_PHASE_GENERATE_REPLY_FAILURE,
+ WC_PHASE_RETURN_NO,
+ WC_PHASE_RETURN_YES,
+ } phase;
+
+
+ /**
+ * Handle for the legitimization check.
+ */
+ struct TEH_LegitimizationCheckHandle *lch;
+
+ /**
+ * Request context
+ */
+ const struct TEH_RequestContext *rc;
+
+ /**
+ * Response to return, if set.
+ */
+ struct MHD_Response *response;
+
+ /**
+ * Public key of the reserve.
+ */
+ struct TALER_ReservePublicKeyP reserve_pub;
+
+ /**
+ * KYC status for the operation.
+ */
+ struct TALER_EXCHANGEDB_KycStatus kyc;
+
+ /**
+ * Current time for the DB transaction.
+ */
+ struct GNUNET_TIME_Timestamp now;
+
+ /**
+ * Set to the hash of the normalized payto URI that established
+ * the reserve.
+ */
+ struct TALER_NormalizedPaytoHashP h_normalized_payto;
+
+ /**
+ * HTTP status to return with @e response, or 0.
+ */
+ unsigned int http_status;
+
+
+ /**
+ * Depending on @e withdraw_type, this union
+ * contains the details of a withdraw operation:
+ * 1.) WITHDRAW_TYPE_BATCH: see @e typ.batch
+ * 2.) WITHDRAW_TYPE_AGE: see @e typ.age
+ */
+ union
+ {
+ /**
+ * Data specific to batch_withdraw
+ */
+ struct
+ {
+ /**
+ * Array of @e planchets_length planchets we are processing.
+ */
+ struct PlanchetContext *planchets;
+
+ /**
+ * Total amount from all coins with fees.
+ */
+ struct TALER_Amount batch_total;
+
+ /**
+ * Length of the @e planchets array.
+ */
+ unsigned int planchets_length;
+ } batch;
+
+ /**
+ * Data specific to age_withdraw
+ */
+ struct
+ {
+ /**
+ * value the client committed to
+ */
+ struct TALER_AgeWithdrawCommitmentHashP ach;
+
+ /**
+ * The data from the age-withdraw request, as we persist it
+ */
+ struct TALER_EXCHANGEDB_AgeWithdraw commitment;
+
+ /**
+ * Number of coins/denonations in the reveal
+ */
+ unsigned int num_coins;
+
+ /**
+ * #num_coins * #kappa hashes of blinded coin planchets.
+ */
+ struct TALER_BlindedPlanchet (*coin_evs) [ TALER_CNC_KAPPA];
+
+ /**
+ * #num_coins hashes of the denominations from which the coins are withdrawn.
+ * Those must support age restriction.
+ */
+ struct TALER_DenominationHashP *denom_hs;
+ } age;
+
+ } typ;
+
+};
+
+
+/**
+ * All withdraw context is kept in a DLL.
+ */
+static struct WithdrawContext *wc_head;
+static struct WithdrawContext *wc_tail;
+
+void
+TEH_withdraw_cleanup ()
+{
+ struct WithdrawContext *wc;
+
+ while (NULL != (wc = wc_head))
+ {
+ GNUNET_CONTAINER_DLL_remove (wc_head,
+ wc_tail,
+ wc);
+ MHD_resume_connection (wc->rc->connection);
+ }
+}
+
+
+/**
+ * Terminate the main loop by returning the final
+ * result.
+ *
+ * @param[in,out] wc context to update phase for
+ * @param mres MHD status to return
+ */
+static void
+finish_loop (struct WithdrawContext *wc,
+ MHD_RESULT mres)
+{
+ wc->phase = (MHD_YES == mres)
+ ? WC_PHASE_RETURN_YES
+ : WC_PHASE_RETURN_NO;
+}
+
+
+/**
+ * Generates our final (successful) response to a batch withdraw request.
+ *
+ * @param wc operation context
+ */
+static void
+batch_withdraw_generate_reply_success (struct WithdrawContext *wc)
+{
+ const struct TEH_RequestContext *rc = wc->rc;
+ json_t *sigs;
+
+ sigs = json_array ();
+ GNUNET_assert (NULL != sigs);
+ for (unsigned int i = 0; i<wc->typ.batch.planchets_length; i++)
+ {
+ struct PlanchetContext *pc = &wc->typ.batch.planchets[i];
+
+ GNUNET_assert (
+ 0 ==
+ json_array_append_new (
+ sigs,
+ GNUNET_JSON_PACK (
+ TALER_JSON_pack_blinded_denom_sig (
+ "ev_sig",
+ &pc->collectable.sig))));
+ }
+ TEH_METRICS_batch_withdraw_num_coins += wc->typ.batch.planchets_length;
+ finish_loop (wc,
+ TALER_MHD_REPLY_JSON_PACK (
+ rc->connection,
+ MHD_HTTP_OK,
+ GNUNET_JSON_pack_array_steal ("ev_sigs",
+ sigs)));
+}
+
+
+/**
+ * Send a response to a "age-withdraw" request.
+ *
+ * @param[in,out] wc context for the operation
+ */
+static void
+age_withdraw_generate_reply_success (
+ struct WithdrawContext *wc)
+{
+ struct MHD_Connection *connection
+ = wc->rc->connection;
+ const struct TALER_AgeWithdrawCommitmentHashP *ach
+ = &wc->typ.age.commitment.h_commitment;
+ uint32_t noreveal_index
+ = wc->typ.age.commitment.noreveal_index;
+ struct TALER_ExchangePublicKeyP pub;
+ struct TALER_ExchangeSignatureP sig;
+ enum TALER_ErrorCode ec;
+
+ ec = TALER_exchange_online_age_withdraw_confirmation_sign (
+ &TEH_keys_exchange_sign_,
+ ach,
+ noreveal_index,
+ &pub,
+ &sig);
+ if (TALER_EC_NONE != ec)
+ {
+ finish_loop (wc,
+ TALER_MHD_reply_with_ec (connection,
+ ec,
+ NULL));
+ return;
+ }
+
+ finish_loop (wc,
+ TALER_MHD_REPLY_JSON_PACK (
+ connection,
+ MHD_HTTP_OK,
+ GNUNET_JSON_pack_uint64 ("noreveal_index",
+ noreveal_index),
+ GNUNET_JSON_pack_data_auto ("exchange_sig",
+ &sig),
+ GNUNET_JSON_pack_data_auto ("exchange_pub",
+ &pub)));
+}
+
+
+/**
+ * Generates response for the batch- or age-withdraw request.
+ *
+ * @param[in, ou] wc withdraw operation context
+ */
+static void
+generate_reply_success (struct WithdrawContext *wc)
+{
+ switch (wc->withdraw_type)
+ {
+ case WITHDRAW_TYPE_BATCH:
+ batch_withdraw_generate_reply_success (wc);
+ break;
+ case WITHDRAW_TYPE_AGE:
+ age_withdraw_generate_reply_success (wc);
+ break;
+ default:
+ GNUNET_break (0);
+ }
+}
+
+
+/**
+ * Check if the batch withdraw in @a wc is replayed
+ * and we already have an answer.
+ * If so, replay the existing answer and return the HTTP response.
+ *
+ * @param wc parsed request data
+ * @return true if the request is idempotent with an existing request
+ * false if we did not find the request in the DB and did not set @a mret
+ */
+static bool
+batch_withdraw_check_idempotency (
+ struct WithdrawContext *wc)
+{
+ const struct TEH_RequestContext *rc = wc->rc;
+ GNUNET_assert (wc->withdraw_type == WITHDRAW_TYPE_BATCH);
+
+ for (unsigned int i = 0; i<wc->typ.batch.planchets_length; i++)
+ {
+ struct PlanchetContext *pc = &wc->typ.batch.planchets[i];
+ enum GNUNET_DB_QueryStatus qs;
+ struct TALER_EXCHANGEDB_CollectableBlindcoin collectable;
+
+ qs = TEH_plugin->get_withdraw_info (
+ TEH_plugin->cls,
+ &pc->collectable.h_coin_envelope,
+ &collectable);
+ if (0 > qs)
+ {
+ /* FIXME: soft error not handled correctly! */
+ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
+ finish_loop (wc,
+ TALER_MHD_reply_with_error (
+ rc->connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "get_withdraw_info"));
+ return true;
+ }
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ return false;
+ pc->collectable = collectable;
+ }
+ /* generate idempotent reply */
+ TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_BATCH_WITHDRAW]++;
+ wc->phase = WC_PHASE_GENERATE_REPLY_SUCCESS;
+ return true;
+}
+
+
+/**
+ * Check if the age-withdraw request is replayed
+ * and we already have an answer.
+ * If so, replay the existing answer and return the HTTP response.
+ *
+ * @param[in,out] wc parsed request data
+ * @return true if the request is idempotent with an existing request
+ * false if we did not find the request in the DB and did not set @a mret
+ */
+static bool
+age_withdraw_check_idempotency (
+ struct WithdrawContext *wc)
+{
+ enum GNUNET_DB_QueryStatus qs;
+ struct TALER_EXCHANGEDB_AgeWithdraw commitment;
+ GNUNET_assert (wc->withdraw_type == WITHDRAW_TYPE_AGE);
+
+ qs = TEH_plugin->get_age_withdraw (
+ TEH_plugin->cls,
+ &wc->typ.age.commitment.reserve_pub,
+ &wc->typ.age.commitment.h_commitment,
+ &commitment);
+ if (0 > qs)
+ {
+ /* FIXME: soft error not handled correctly! */
+ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
+ if (GNUNET_DB_STATUS_HARD_ERROR == qs)
+ finish_loop (wc,
+ TALER_MHD_reply_with_ec (
+ wc->rc->connection,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "get_age_withdraw"));
+ return true; /* Well, kind-of. */
+ }
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ return false;
+
+ /* Generate idempotent reply */
+ TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_AGE_WITHDRAW]++;
+ wc->phase = WC_PHASE_GENERATE_REPLY_SUCCESS;
+ return true;
+}
+
+
+/**
+ * Check if the @a wc is replayed and we already have an
+ * answer. If so, replay the existing answer and return the
+ * HTTP response.
+ *
+ * @param wc parsed request data
+ * @return true if the request is idempotent with an existing request
+ * false if we did not find the request in the DB and did not set @a mret
+ */
+static bool
+check_request_idempotent (
+ struct WithdrawContext *wc)
+{
+ switch (wc->withdraw_type)
+ {
+ case WITHDRAW_TYPE_BATCH:
+ return batch_withdraw_check_idempotency (wc);
+ break;
+ case WITHDRAW_TYPE_AGE:
+ return age_withdraw_check_idempotency (wc);
+ break;
+ default:
+ GNUNET_break (0);
+ }
+ return false;
+}
+
+
+/**
+ * Function implementing age withdraw transaction. Runs the
+ * transaction logic; IF it returns a non-error code, the transaction
+ * logic MUST NOT queue a MHD response. IF it returns an hard error,
+ * the transaction logic MUST queue a MHD response and set @a mhd_ret.
+ * IF it returns the soft error code, the function MAY be called again
+ * to retry and MUST not queue a MHD response.
+ *
+ * @param cls a `struct WithdrawContext *`, with @e withdraw_type == WITHDRAW_TYPE_AGE
+ * @param connection MHD request which triggered the transaction
+ * @param[out] mhd_ret set to MHD response status for @a connection,
+ * if transaction failed (!)
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+age_withdraw_transaction (
+ void *cls,
+ struct MHD_Connection *connection,
+ MHD_RESULT *mhd_ret)
+{
+ struct WithdrawContext *wc = cls;
+ enum GNUNET_DB_QueryStatus qs;
+ bool found = false;
+ bool balance_ok = false;
+ bool age_ok = false;
+ bool conflict = false;
+ uint16_t allowed_maximum_age = 0;
+ uint32_t reserve_birthday = 0;
+ struct TALER_Amount reserve_balance;
+
+ qs = TEH_plugin->do_age_withdraw (
+ TEH_plugin->cls,
+ &wc->typ.age.commitment,
+ wc->now,
+ &found,
+ &balance_ok,
+ &reserve_balance,
+ &age_ok,
+ &allowed_maximum_age,
+ &reserve_birthday,
+ &conflict);
+ if (0 > qs)
+ {
+ if (GNUNET_DB_STATUS_HARD_ERROR == qs)
+ finish_loop (wc,
+ TALER_MHD_reply_with_ec (
+ wc->rc->connection,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "do_age_withdraw"));
+ return qs;
+ }
+ if (! found)
+ {
+ finish_loop (wc,
+ TALER_MHD_reply_with_ec (
+ wc->rc->connection,
+ TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN,
+ NULL));
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ if (! age_ok)
+ {
+ finish_loop (wc,
+ TALER_MHD_REPLY_JSON_PACK (
+ wc->rc->connection,
+ MHD_HTTP_CONFLICT,
+ TALER_MHD_PACK_EC (
+ TALER_EC_EXCHANGE_AGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE),
+ GNUNET_JSON_pack_uint64 (
+ "allowed_maximum_age",
+ allowed_maximum_age),
+ GNUNET_JSON_pack_uint64 (
+ "reserve_birthday",
+ reserve_birthday)));
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ if (! balance_ok)
+ {
+ TEH_plugin->rollback (TEH_plugin->cls);
+ finish_loop (wc,
+ TEH_RESPONSE_reply_reserve_insufficient_balance (
+ wc->rc->connection,
+ TALER_EC_EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS,
+ &reserve_balance,
+ &wc->typ.age.commitment.amount_with_fee,
+ &wc->typ.age.commitment.reserve_pub));
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ if (conflict)
+ {
+ /* do_age_withdraw signaled a conflict, so there MUST be an entry
+ * in the DB. Put that into the response */
+ if (check_request_idempotent (wc))
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ GNUNET_break (0);
+ return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
+ }
+ if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
+ TEH_METRICS_num_success[TEH_MT_SUCCESS_AGE_WITHDRAW]++;
+ return qs;
+}
+
+
+/**
+ * Function implementing withdraw transaction. Runs the
+ * transaction logic; IF it returns a non-error code, the transaction
+ * logic MUST NOT queue a MHD response. IF it returns an hard error,
+ * the transaction logic MUST queue a MHD response and set @a mhd_ret.
+ * IF it returns the soft error code, the function MAY be called again
+ * to retry and MUST not queue a MHD response.
+ *
+ * Note that "wc->collectable.sig" is set before entering this function as we
+ * signed before entering the transaction.
+ *
+ * @param cls a `struct WithdrawContext *`, with @e withdraw_type set to WITHDRAW_TYPE_BATCH
+ * @param connection MHD request which triggered the transaction
+ * @param[out] mhd_ret set to MHD response status for @a connection,
+ * if transaction failed (!)
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+batch_withdraw_transaction (
+ void *cls,
+ struct MHD_Connection *connection,
+ MHD_RESULT *mhd_ret)
+{
+ struct WithdrawContext *wc = cls;
+ uint64_t ruuid;
+ enum GNUNET_DB_QueryStatus qs;
+ bool found = false;
+ bool balance_ok = false;
+ bool age_ok = false;
+ uint16_t allowed_maximum_age = 0;
+ struct TALER_Amount reserve_balance;
+
+ qs = TEH_plugin->do_batch_withdraw (
+ TEH_plugin->cls,
+ wc->now,
+ &wc->reserve_pub,
+ &wc->typ.batch.batch_total,
+ TEH_age_restriction_enabled,
+ &found,
+ &balance_ok,
+ &reserve_balance,
+ &age_ok,
+ &allowed_maximum_age,
+ &ruuid);
+ if (0 > qs)
+ {
+ if (GNUNET_DB_STATUS_HARD_ERROR == qs)
+ {
+ GNUNET_break (0);
+ finish_loop (wc,
+ TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "update_reserve_batch_withdraw"));
+ return qs;
+ }
+ return qs;
+ }
+ if (! found)
+ {
+ finish_loop (wc,
+ TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN,
+ NULL));
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+
+ if (! age_ok)
+ {
+ /* We respond with the lowest age in the corresponding age group
+ * of the required age */
+ uint16_t lowest_age = TALER_get_lowest_age (
+ &TEH_age_restriction_config.mask,
+ allowed_maximum_age);
+
+ finish_loop (wc,
+ TEH_RESPONSE_reply_reserve_age_restriction_required (
+ connection,
+ lowest_age));
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+
+ if (! balance_ok)
+ {
+ if (check_request_idempotent (wc))
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ finish_loop (wc,
+ TEH_RESPONSE_reply_reserve_insufficient_balance (
+ connection,
+ TALER_EC_EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS,
+ &reserve_balance,
+ &wc->typ.batch.batch_total,
+ &wc->reserve_pub));
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+
+ /* Add information about each planchet in the batch */
+ for (unsigned int i = 0; i<wc->typ.batch.planchets_length; i++)
+ {
+ struct PlanchetContext *pc = &wc->typ.batch.planchets[i];
+ const struct TALER_BlindedPlanchet *bp = &pc->blinded_planchet;
+ const union GNUNET_CRYPTO_BlindSessionNonce *nonce = NULL;
+ bool denom_unknown = true;
+ bool conflict = true;
+ bool nonce_reuse = true;
+
+ switch (bp->blinded_message->cipher)
+ {
+ case GNUNET_CRYPTO_BSA_INVALID:
+ break;
+ case GNUNET_CRYPTO_BSA_RSA:
+ break;
+ case GNUNET_CRYPTO_BSA_CS:
+ nonce = (const union GNUNET_CRYPTO_BlindSessionNonce *)
+ &bp->blinded_message->details.cs_blinded_message.nonce;
+ break;
+ }
+ qs = TEH_plugin->do_batch_withdraw_insert (
+ TEH_plugin->cls,
+ nonce,
+ &pc->collectable,
+ wc->now,
+ ruuid,
+ &denom_unknown,
+ &conflict,
+ &nonce_reuse);
+ if (0 > qs)
+ {
+ if (GNUNET_DB_STATUS_HARD_ERROR == qs)
+ finish_loop (wc,
+ TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "do_batch_withdraw_insert"));
+ return qs;
+ }
+ if (denom_unknown)
+ {
+ GNUNET_break (0);
+ finish_loop (wc,
+ TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
+ NULL));
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) ||
+ (conflict) )
+ {
+ if (check_request_idempotent (wc))
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ /* We do not support *some* of the coins of the request being
+ idempotent while others being fresh. */
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Idempotent coin in batch, not allowed. Aborting.\n");
+ finish_loop (wc,
+ TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_CONFLICT,
+ TALER_EC_EXCHANGE_WITHDRAW_BATCH_IDEMPOTENT_PLANCHET,
+ NULL));
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ if (nonce_reuse)
+ {
+ GNUNET_break_op (0);
+ finish_loop (wc,
+ TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_EXCHANGE_WITHDRAW_NONCE_REUSE,
+ NULL));
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ }
+ TEH_METRICS_num_success[TEH_MT_SUCCESS_BATCH_WITHDRAW]++;
+ return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
+}
+
+
+/**
+ * The request was prepared successfully.
+ * Run the main DB transaction.
+ *
+ * @param awc The context for the current withdraw request
+ */
+static void
+run_transaction (
+ struct WithdrawContext *wc)
+{
+ MHD_RESULT mhd_ret;
+ enum GNUNET_GenericReturnValue qs;
+
+ GNUNET_assert (WC_PHASE_RUN_TRANSACTION ==
+ wc->phase);
+
+ switch (wc->withdraw_type)
+ {
+ case WITHDRAW_TYPE_AGE:
+ qs = TEH_DB_run_transaction (wc->rc->connection,
+ "run age withdraw",
+ TEH_MT_REQUEST_AGE_WITHDRAW,
+ &mhd_ret,
+ &age_withdraw_transaction,
+ wc);
+ break;
+ case WITHDRAW_TYPE_BATCH:
+ qs = TEH_DB_run_transaction (wc->rc->connection,
+ "run batch withdraw",
+ TEH_MT_REQUEST_WITHDRAW,
+ &mhd_ret,
+ &batch_withdraw_transaction,
+ wc);
+ break;
+ default:
+ GNUNET_break (0);
+ qs = GNUNET_SYSERR;
+ }
+ if (GNUNET_OK != qs)
+ {
+ if (WC_PHASE_RUN_TRANSACTION == wc->phase)
+ finish_loop (wc,
+ mhd_ret);
+ return;
+ }
+ wc->phase++;
+}
+
+
+/**
+ * The request for batch withdraw was parsed successfully.
+ * Prepare our side for the main DB transaction.
+ *
+ * @param wc context for request processing, with @e withdraw_type set to WITHDRAW_TYPE_BATCH
+ * @return GNUNET_OK on success
+ */
+enum GNUNET_GenericReturnValue
+batch_withdraw_prepare_transaction (struct WithdrawContext *wc)
+{
+ const struct TEH_RequestContext *rc = wc->rc;
+ struct TALER_BlindedDenominationSignature bss[wc->typ.batch.planchets_length];
+ struct TEH_CoinSignData csds[wc->typ.batch.planchets_length];
+
+ for (unsigned int i = 0; i<wc->typ.batch.planchets_length; i++)
+ {
+ struct PlanchetContext *pc = &wc->typ.batch.planchets[i];
+ struct TEH_CoinSignData *csdsi = &csds[i];
+
+ csdsi->h_denom_pub = &pc->collectable.denom_pub_hash;
+ csdsi->bp = &pc->blinded_planchet;
+ }
+ {
+ enum TALER_ErrorCode ec;
+
+ ec = TEH_keys_denomination_batch_sign (
+ wc->typ.batch.planchets_length,
+ csds,
+ false,
+ bss);
+ if (TALER_EC_NONE != ec)
+ {
+ GNUNET_break (0);
+ finish_loop (wc,
+ TALER_MHD_reply_with_ec (
+ rc->connection,
+ ec,
+ NULL));
+ return GNUNET_SYSERR;
+ }
+ }
+
+ for (unsigned int i = 0; i<wc->typ.batch.planchets_length; i++)
+ {
+ struct PlanchetContext *pc = &wc->typ.batch.planchets[i];
+
+ pc->collectable.sig = bss[i];
+ }
+
+ return GNUNET_OK;
+}
+
+
+/**
+ * The request for age-withdraw was parsed succesfully.
+ * Sign and persist the chosen blinded coins for the reveal step.
+ *
+ * @param wc The context for the current withdraw request, with @e withdraw_type set to WITHDRAW_TYPE_AGE
+ * @return GNUNET_OK on success
+ */
+enum GNUNET_GenericReturnValue
+age_withdraw_prepare_transaction (
+ struct WithdrawContext *wc)
+{
+ uint8_t noreveal_index;
+
+ wc->typ.age.commitment.denom_sigs
+ = GNUNET_new_array (
+ wc->typ.age.num_coins,
+ struct TALER_BlindedDenominationSignature);
+ wc->typ.age.commitment.h_coin_evs
+ = GNUNET_new_array (
+ wc->typ.age.num_coins,
+ struct TALER_BlindedCoinHashP);
+ /* Pick the challenge */
+ noreveal_index =
+ GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_STRONG,
+ TALER_CNC_KAPPA);
+ wc->typ.age.commitment.noreveal_index = noreveal_index;
+
+ /* Choose and sign the coins */
+ {
+ struct TEH_CoinSignData csds[wc->typ.age.num_coins];
+ enum TALER_ErrorCode ec;
+
+ /* Pick the chosen blinded coins */
+ for (uint32_t i = 0; i<wc->typ.age.num_coins; i++)
+ {
+ struct TEH_CoinSignData *csdsi = &csds[i];
+
+ csdsi->bp = &wc->typ.age.coin_evs[i][noreveal_index];
+ csdsi->h_denom_pub = &wc->typ.age.denom_hs[i];
+ }
+
+ ec = TEH_keys_denomination_batch_sign (
+ wc->typ.age.num_coins,
+ csds,
+ false,
+ wc->typ.age.commitment.denom_sigs);
+ if (TALER_EC_NONE != ec)
+ {
+ GNUNET_break (0);
+ finish_loop (wc,
+ TALER_MHD_reply_with_ec (
+ wc->rc->connection,
+ ec,
+ NULL));
+ return GNUNET_SYSERR;
+ }
+ }
+
+ /* Prepare the hashes of the coins for insertion */
+ for (uint32_t i = 0; i<wc->typ.age.num_coins; i++)
+ {
+ TALER_coin_ev_hash (&wc->typ.age.coin_evs[i][noreveal_index],
+ &wc->typ.age.denom_hs[i],
+ &wc->typ.age.commitment.h_coin_evs[i]);
+ }
+ return GNUNET_OK;
+}
+
+
+/**
+ * The request for withdraw was parsed succesfully.
+ * Chooose the appropriate preparation step depending on @e withdraw_type
+ */
+static void
+prepare_transaction (
+ struct WithdrawContext *wc)
+{
+ enum GNUNET_GenericReturnValue r;
+ switch (wc->withdraw_type)
+ {
+ case WITHDRAW_TYPE_BATCH:
+ r = batch_withdraw_prepare_transaction (wc);
+ break;
+ case WITHDRAW_TYPE_AGE:
+ r = age_withdraw_prepare_transaction (wc);
+ break;
+ default:
+ GNUNET_break (0);
+ return;
+ }
+ if (GNUNET_OK != r)
+ return;
+ wc->phase++;
+}
+
+
+/**
+ * Check the KYC result.
+ *
+ * @param wc context for request processing
+ */
+static void
+check_kyc_result (struct WithdrawContext *wc)
+{
+ /* return final positive response */
+ if (! wc->kyc.ok)
+ {
+ if (check_request_idempotent (wc))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Request is idempotent!\n");
+ return;
+ }
+ /* KYC required */
+ finish_loop (wc,
+ TEH_RESPONSE_reply_kyc_required (
+ wc->rc->connection,
+ &wc->h_normalized_payto,
+ &wc->kyc,
+ false));
+ return;
+ }
+ wc->phase++;
+}
+
+
+/**
+ * Function called with the result of a legitimization
+ * check.
+ *
+ * @param cls closure
+ * @param lcr legitimization check result
+ */
+static void
+withdraw_legi_cb (
+ void *cls,
+ const struct TEH_LegitimizationCheckResult *lcr)
+{
+ struct WithdrawContext *wc = cls;
+
+ wc->lch = NULL;
+ GNUNET_assert (WC_PHASE_SUSPENDED ==
+ wc->phase);
+ MHD_resume_connection (wc->rc->connection);
+ GNUNET_CONTAINER_DLL_remove (wc_head,
+ wc_tail,
+ wc);
+ TALER_MHD_daemon_trigger ();
+ if (NULL != lcr->response)
+ {
+ wc->response = lcr->response;
+ wc->http_status = lcr->http_status;
+ wc->phase = WC_PHASE_GENERATE_REPLY_FAILURE;
+ return;
+ }
+ wc->kyc = lcr->kyc;
+ wc->phase = WC_PHASE_CHECK_KYC_RESULT;
+}
+
+
+/**
+ * Helper function to return a string representing the type of withdraw (age or batch).
+ *
+ * @param wc withdraw context
+ */
+static const char *
+typ2str (
+ const struct WithdrawContext *wc)
+{
+ switch (wc->withdraw_type)
+ {
+ case WITHDRAW_TYPE_BATCH:
+ return "batch-withdraw";
+ case WITHDRAW_TYPE_AGE:
+ return "age-withdraw";
+ default:
+ GNUNET_break (0);
+ return "unknown";
+ }
+}
+
+
+/**
+ * Return the total amount including fees to be withdrawn
+ *
+ * @param wc withdraw context
+ * @return total amount including fees
+ */
+static struct TALER_Amount *
+withdraw_amount_with_fee (
+ struct WithdrawContext *wc)
+{
+ switch (wc->withdraw_type)
+ {
+ case WITHDRAW_TYPE_BATCH:
+ return &wc->typ.batch.batch_total;
+ case WITHDRAW_TYPE_AGE:
+ return &wc->typ.age.commitment.amount_with_fee;
+ default:
+ GNUNET_break (0);
+ return NULL;
+ }
+}
+
+
+/**
+ * Function called to iterate over KYC-relevant transaction amounts for a
+ * particular time range. Called within a database transaction, so must
+ * not start a new one.
+ *
+ * @param cls closure, identifies the event type and account to iterate
+ * over events for
+ * @param limit maximum time-range for which events should be fetched
+ * (timestamp in the past)
+ * @param cb function to call on each event found, events must be returned
+ * in reverse chronological order
+ * @param cb_cls closure for @a cb, of type struct WithdrawContext
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+withdraw_amount_cb (
+ void *cls,
+ struct GNUNET_TIME_Absolute limit,
+ TALER_EXCHANGEDB_KycAmountCallback cb,
+ void *cb_cls)
+{
+ struct WithdrawContext *wc = cls;
+ enum GNUNET_GenericReturnValue ret;
+ enum GNUNET_DB_QueryStatus qs;
+
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Signaling amount %s for KYC check during %sal\n",
+ TALER_amount2s (withdraw_amount_with_fee (wc)),
+ typ2str (wc));
+ ret = cb (cb_cls,
+ withdraw_amount_with_fee (wc),
+ wc->now.abs_time);
+ GNUNET_break (GNUNET_SYSERR != ret);
+ if (GNUNET_OK != ret)
+ return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS;
+ qs = TEH_plugin->select_withdraw_amounts_for_kyc_check (
+ TEH_plugin->cls,
+ &wc->h_normalized_payto,
+ limit,
+ cb,
+ cb_cls);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Got %d additional transactions for this %sal and limit %llu\n",
+ qs,
+ typ2str (wc),
+ (unsigned long long) limit.abs_value_us);
+ GNUNET_break (qs >= 0);
+ return qs;
+}
+
+
+/**
+ * Do legitimization check.
+ *
+ * @param wc operation context
+ */
+static void
+run_legi_check (struct WithdrawContext *wc)
+{
+ enum GNUNET_DB_QueryStatus qs;
+ struct TALER_FullPayto payto_uri;
+ struct TALER_FullPaytoHashP h_full_payto;
+
+ /* Check if the money came from a wire transfer */
+ qs = TEH_plugin->reserves_get_origin (
+ TEH_plugin->cls,
+ &wc->reserve_pub,
+ &h_full_payto,
+ &payto_uri);
+ if (qs < 0)
+ {
+ finish_loop (wc,
+ TALER_MHD_reply_with_error (
+ wc->rc->connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "reserves_get_origin"));
+ return;
+ }
+ /* If _no_ results, reserve was created by merge,
+ in which case no KYC check is required as the
+ merge already did that. */
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ {
+ wc->phase = WC_PHASE_PREPARE_TRANSACTION;
+ return;
+ }
+ TALER_full_payto_normalize_and_hash (payto_uri,
+ &wc->h_normalized_payto);
+ wc->lch = TEH_legitimization_check (
+ &wc->rc->async_scope_id,
+ TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW,
+ payto_uri,
+ &wc->h_normalized_payto,
+ NULL, /* no account pub: this is about the origin account */
+ &withdraw_amount_cb,
+ wc,
+ &withdraw_legi_cb,
+ wc);
+ GNUNET_assert (NULL != wc->lch);
+ GNUNET_free (payto_uri.full_payto);
+ GNUNET_CONTAINER_DLL_insert (wc_head,
+ wc_tail,
+ wc);
+ MHD_suspend_connection (wc->rc->connection);
+ wc->phase = WC_PHASE_SUSPENDED;
+}
+
+
+/**
+ * Check if the given denomination is still or already valid, has not been
+ * revoked and potentically supports age restriction.
+ *
+ * @param[in,out] wc context for the withdraw operation
+ * @param ksh The handle to the current state of (denomination) keys in the exchange
+ * @param denom_h Hash of the denomination key to check
+ * @param[out] pdk denomination key found, might be NULL
+ * @return GNUNET_OK when denomation was found and valid,
+ * GNUNET_NO when denomination was not valid but request was idempotent,
+ * GNUNET_SYSERR otherwise (denomination invalid), with finish_loop called.
+ */
+static enum GNUNET_GenericReturnValue
+find_denomination (
+ struct WithdrawContext *wc,
+ struct TEH_KeyStateHandle *ksh,
+ const struct TALER_DenominationHashP *denom_h,
+ struct TEH_DenominationKey **pdk)
+{
+ struct MHD_Connection *connection = wc->rc->connection;
+ struct TEH_DenominationKey *dk;
+
+ *pdk = NULL;
+
+ dk = TEH_keys_denomination_by_hash_from_state (
+ ksh,
+ denom_h,
+ NULL,
+ NULL);
+
+ if (NULL == dk)
+ {
+ /* The denomination doesn't exist */
+ if (check_request_idempotent (wc))
+ return GNUNET_NO;
+ GNUNET_break_op (0);
+ finish_loop (wc,
+ TEH_RESPONSE_reply_unknown_denom_pub_hash (
+ connection,
+ denom_h));
+ return GNUNET_NO;
+ }
+
+ if (GNUNET_TIME_absolute_is_past (
+ dk->meta.expire_withdraw.abs_time))
+ {
+ /* This denomination is past the expiration time for withdraw */
+ if (check_request_idempotent (wc))
+ return GNUNET_NO;
+ GNUNET_break_op (0);
+ finish_loop (wc,
+ TEH_RESPONSE_reply_expired_denom_pub_hash (
+ connection,
+ denom_h,
+ TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED,
+ typ2str (wc)));
+ return GNUNET_SYSERR;
+ }
+
+ if (GNUNET_TIME_absolute_is_future (
+ dk->meta.start.abs_time))
+ {
+ /* This denomination is not yet valid, no need to check
+ for idempotency! */
+ GNUNET_break_op (0);
+ finish_loop (wc,
+ TEH_RESPONSE_reply_expired_denom_pub_hash (
+ connection,
+ denom_h,
+ TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE,
+ typ2str (wc)));
+ return GNUNET_SYSERR;
+ }
+
+ if (dk->recoup_possible)
+ {
+ /* This denomination has been revoked */
+ if (check_request_idempotent (wc))
+ return GNUNET_NO;
+ GNUNET_break_op (0);
+ finish_loop (wc,
+ TALER_MHD_reply_with_ec (
+ connection,
+ TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED,
+ typ2str (wc)));
+ return GNUNET_SYSERR;
+ }
+
+ /* In case of age withdraw, make sure that the denomitation supports age restriction */
+ if (WITHDRAW_TYPE_AGE == wc->withdraw_type)
+ {
+ if (0 == dk->denom_pub.age_mask.bits)
+ {
+ /* This denomation does not support age restriction */
+ char msg[256];
+
+ GNUNET_snprintf (msg,
+ sizeof(msg),
+ "denomination %s does not support age restriction",
+ GNUNET_h2s (&denom_h->hash));
+
+ finish_loop (wc,
+ TALER_MHD_reply_with_ec (
+ connection,
+ TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN,
+ msg));
+ return GNUNET_SYSERR;
+ }
+ }
+
+ *pdk = dk;
+ return GNUNET_OK;
+}
+
+
+/**
+ * Check if the given array of hashes of denomination_keys a) belong
+ * to valid denominations and b) those are marked as age restricted.
+ * Also, calculate the total amount of the denominations including fees
+ * for withdraw.
+ *
+ * @param wc context of the age withdrawal to check keys for
+ * @param ksh key state handle
+ * @return GNUNET_OK on success,
+ * GNUNET_NO on error (and response beeing sent)
+ */
+static enum GNUNET_GenericReturnValue
+age_withdraw_check_keys (
+ struct WithdrawContext *wc,
+ struct TEH_KeyStateHandle *ksh)
+{
+ struct MHD_Connection *connection
+ = wc->rc->connection;
+ unsigned int len
+ = wc->typ.age.num_coins;
+ struct TALER_Amount total_amount;
+ struct TALER_Amount total_fee;
+
+ wc->typ.age.commitment.denom_serials
+ = GNUNET_new_array (len,
+ uint64_t);
+ GNUNET_assert (GNUNET_OK ==
+ TALER_amount_set_zero (TEH_currency,
+ &total_amount));
+ GNUNET_assert (GNUNET_OK ==
+ TALER_amount_set_zero (TEH_currency,
+ &total_fee));
+
+ for (unsigned int i = 0; i < len; i++)
+ {
+ struct TEH_DenominationKey *dk;
+ enum GNUNET_GenericReturnValue r;
+
+ r = find_denomination (wc,
+ ksh,
+ &wc->typ.age.denom_hs[i],
+ &dk);
+
+ if (GNUNET_OK != r)
+ return GNUNET_NO;
+
+ /* Ensure the ciphers from the planchets match the denominations' */
+ for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++)
+ {
+ if (dk->denom_pub.bsign_pub_key->cipher !=
+ wc->typ.age.coin_evs[i][k].blinded_message->cipher)
+ {
+ GNUNET_break_op (0);
+ finish_loop (wc,
+ TALER_MHD_reply_with_ec (
+ connection,
+ TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH,
+ NULL));
+ return GNUNET_NO;
+ }
+ }
+
+ /* Accumulate the values */
+ if (0 > TALER_amount_add (&total_amount,
+ &total_amount,
+ &dk->meta.value))
+ {
+ GNUNET_break_op (0);
+ finish_loop (wc,
+ TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW,
+ "amount"));
+ return GNUNET_NO;
+ }
+
+ /* Accumulate the withdraw fees */
+ if (0 > TALER_amount_add (&total_fee,
+ &total_fee,
+ &dk->meta.fees.withdraw))
+ {
+ GNUNET_break_op (0);
+ finish_loop (wc,
+ TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW,
+ "fee"));
+ return GNUNET_NO;
+ }
+ wc->typ.age.commitment.denom_serials[i] = dk->meta.serial;
+ }
+
+ /* Save the total amount including fees */
+ GNUNET_assert (0 <
+ TALER_amount_add (
+ &wc->typ.age.commitment.amount_with_fee,
+ &total_amount,
+ &total_fee));
+
+ /* Check that the client signature authorizing the withdrawal is valid. */
+ TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++;
+ if (GNUNET_OK !=
+ TALER_wallet_age_withdraw_verify (
+ &wc->typ.age.commitment.h_commitment,
+ &wc->typ.age.commitment.amount_with_fee,
+ &TEH_age_restriction_config.mask,
+ wc->typ.age.commitment.max_age,
+ &wc->typ.age.commitment.reserve_pub,
+ &wc->typ.age.commitment.reserve_sig))
+ {
+ GNUNET_break_op (0);
+ finish_loop (wc,
+ TALER_MHD_reply_with_ec (
+ wc->rc->connection,
+ TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID,
+ NULL));
+ return GNUNET_NO;
+ }
+
+ return GNUNET_OK;
+}
+
+
+/**
+ * Check if the keys in the request are valid for batch withdrawal.
+ *
+ * @param[in,out] wc context for the batch withdraw request processing
+ * @param ksh key state handle
+ * @return GNUNET_OK on success,
+ * GNUNET_NO on error (and response beeing sent)
+ */
+static enum GNUNET_GenericReturnValue
+batch_withdraw_check_keys (
+ struct WithdrawContext *wc,
+ struct TEH_KeyStateHandle *ksh)
+{
+ const struct TEH_RequestContext *rc = wc->rc;
+
+ for (unsigned int i = 0; i<wc->typ.batch.planchets_length; i++)
+ {
+ struct PlanchetContext *pc = &wc->typ.batch.planchets[i];
+ struct TEH_DenominationKey *dk;
+ enum GNUNET_GenericReturnValue r;
+
+ r = find_denomination (wc,
+ ksh,
+ &pc->collectable.denom_pub_hash,
+ &dk);
+
+ if (GNUNET_OK != r)
+ return GNUNET_NO;
+
+ GNUNET_assert (NULL != dk);
+
+ if (dk->denom_pub.bsign_pub_key->cipher !=
+ pc->blinded_planchet.blinded_message->cipher)
+ {
+ /* denomination cipher and blinded planchet cipher not the same */
+ GNUNET_break_op (0);
+ finish_loop (wc,
+ TALER_MHD_reply_with_error (
+ rc->connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH,
+ NULL));
+ return GNUNET_NO;
+ }
+
+ if (0 >
+ TALER_amount_add (&pc->collectable.amount_with_fee,
+ &dk->meta.value,
+ &dk->meta.fees.withdraw))
+ {
+ GNUNET_break (0);
+ finish_loop (wc,
+ TALER_MHD_reply_with_error (rc->connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW,
+ NULL));
+ return GNUNET_NO;
+ }
+ if (0 >
+ TALER_amount_add (&wc->typ.batch.batch_total,
+ &wc->typ.batch.batch_total,
+ &pc->collectable.amount_with_fee))
+ {
+ GNUNET_break (0);
+ finish_loop (wc,
+ TALER_MHD_reply_with_error (
+ rc->connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW,
+ NULL));
+ return GNUNET_NO;
+ }
+
+ TALER_coin_ev_hash (&pc->blinded_planchet,
+ &pc->collectable.denom_pub_hash,
+ &pc->collectable.h_coin_envelope);
+
+ TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++;
+ if (GNUNET_OK !=
+ TALER_wallet_withdraw_verify (
+ &pc->collectable.denom_pub_hash,
+ &pc->collectable.amount_with_fee,
+ &pc->collectable.h_coin_envelope,
+ &pc->collectable.reserve_pub,
+ &pc->collectable.reserve_sig))
+ {
+ GNUNET_break_op (0);
+ finish_loop (wc,
+ TALER_MHD_reply_with_error (
+ rc->connection,
+ MHD_HTTP_FORBIDDEN,
+ TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID,
+ NULL));
+ return GNUNET_NO;
+ }
+ }
+ /* everything parsed */
+
+ return GNUNET_OK;
+}
+
+
+/**
+ * Check if the keys in the request are valid for withdrawing.
+ *
+ * @param[in,out] wc context for request processing
+ */
+static void
+check_keys (struct WithdrawContext *wc)
+{
+ const struct TEH_RequestContext *rc = wc->rc;
+ struct TEH_KeyStateHandle *ksh;
+ enum GNUNET_GenericReturnValue r;
+
+ ksh = TEH_keys_get_state ();
+ if (NULL == ksh)
+ {
+ if (check_request_idempotent (wc))
+ return;
+ GNUNET_break (0);
+ finish_loop (wc,
+ TALER_MHD_reply_with_error (
+ rc->connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
+ NULL));
+ return;
+ }
+
+ switch (wc->withdraw_type)
+ {
+ case WITHDRAW_TYPE_BATCH:
+ r = batch_withdraw_check_keys (wc, ksh);
+ break;
+ case WITHDRAW_TYPE_AGE:
+ r = age_withdraw_check_keys (wc, ksh);
+ break;
+ default:
+ GNUNET_break (0);
+ r = GNUNET_SYSERR;
+ }
+
+ switch (r)
+ {
+ case GNUNET_OK:
+ wc->phase++;
+ break;
+ case GNUNET_NO:
+ /* error generated by function, simply return*/
+ break;
+ case GNUNET_SYSERR:
+ GNUNET_break (0);
+ finish_loop (wc,
+ TALER_MHD_reply_with_error (
+ rc->connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
+ typ2str (wc)));
+ default:
+ GNUNET_break (0);
+ }
+
+ return;
+}
+
+
+/**
+ * Check that the client signature authorizing the withdrawal is valid.
+ * NOTE: this is only applicable to age-withdraw; the existing
+ * batch-withdraw REST-API signs each planchet and they have to be
+ * checked during the call to check_keys.
+ *
+ * @param[in,out] wc request context to check
+ */
+static void
+check_reserve_signature (
+ struct WithdrawContext *wc)
+{
+ switch (wc->withdraw_type)
+ {
+ case WITHDRAW_TYPE_BATCH:
+ /* signature checks has occured in batch_withdraw_check_keys */
+ break;
+ case WITHDRAW_TYPE_AGE:
+ TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++;
+ if (GNUNET_OK !=
+ TALER_wallet_age_withdraw_verify (
+ &wc->typ.age.commitment.h_commitment,
+ &wc->typ.age.commitment.amount_with_fee,
+ &TEH_age_restriction_config.mask,
+ wc->typ.age.commitment.max_age,
+ &wc->typ.age.commitment.reserve_pub,
+ &wc->typ.age.commitment.reserve_sig))
+ {
+ GNUNET_break_op (0);
+ finish_loop (wc,
+ TALER_MHD_reply_with_ec (
+ wc->rc->connection,
+ TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID,
+ NULL));
+ return;
+ }
+ break;
+ default:
+ GNUNET_break (0);
+ return;
+ }
+
+ wc->phase++;
+}
+
+
+/**
+ * Cleanup routine for withdraw reqwuest.
+ * The function is called upon completion of the request
+ * that should clean up @a rh_ctx. Can be NULL.
+ *
+ * @param rc request context to clean up
+ */
+static void
+clean_withdraw_rc (struct TEH_RequestContext *rc)
+{
+ struct WithdrawContext *wc = rc->rh_ctx;
+
+ if (NULL != wc->lch)
+ {
+ TEH_legitimization_check_cancel (wc->lch);
+ wc->lch = NULL;
+ }
+
+ switch (wc->withdraw_type)
+ {
+ case WITHDRAW_TYPE_BATCH:
+ for (unsigned int i = 0; i<wc->typ.batch.planchets_length; i++)
+ {
+ struct PlanchetContext *pc = &wc->typ.batch.planchets[i];
+
+ TALER_blinded_planchet_free (&pc->blinded_planchet);
+ TALER_blinded_denom_sig_free (&pc->collectable.sig);
+ }
+ GNUNET_free (wc->typ.batch.planchets);
+ break;
+
+ case WITHDRAW_TYPE_AGE:
+ for (unsigned int i = 0; i<wc->typ.age.num_coins; i++)
+ {
+ for (unsigned int kappa = 0; kappa<TALER_CNC_KAPPA; kappa++)
+ {
+ TALER_blinded_planchet_free (&wc->typ.age.coin_evs[i][kappa]);
+ }
+ }
+ for (unsigned int i = 0; i<wc->typ.age.num_coins; i++)
+ {
+ TALER_blinded_denom_sig_free (&wc->typ.age.commitment.denom_sigs[i]);
+ }
+ GNUNET_free (wc->typ.age.commitment.h_coin_evs);
+ GNUNET_free (wc->typ.age.commitment.denom_sigs);
+ GNUNET_free (wc->typ.age.denom_hs);
+ GNUNET_free (wc->typ.age.coin_evs);
+ GNUNET_free (wc->typ.age.commitment.denom_serials);
+ break;
+
+ default:
+ GNUNET_break (0);
+ }
+
+ if (NULL != wc->response)
+ {
+ MHD_destroy_response (wc->response);
+ wc->response = NULL;
+ }
+
+ GNUNET_free (wc);
+}
+
+
+/**
+ * Creates a new context for the incoming batch-withdraw request
+ *
+ * @param[in,out] wc context of the batch-witrhdraw, to be filled
+ * @param reserve_pub public key of the reserve for the withdraw
+ * @param root json body of the request
+ * @return GNUNET_OK on success, GNUNET_SYSERR otherwise (response sent)
+ */
+static enum GNUNET_GenericReturnValue
+batch_withdraw_new_request (
+ struct WithdrawContext *wc,
+ const struct TALER_ReservePublicKeyP *reserve_pub,
+ const json_t *root)
+{
+ const json_t *planchets;
+
+ GNUNET_assert (GNUNET_OK ==
+ TALER_amount_set_zero (TEH_currency,
+ &wc->typ.batch.batch_total));
+
+ {
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_array_const ("planchets",
+ &planchets),
+ GNUNET_JSON_spec_end ()
+ };
+
+ {
+ enum GNUNET_GenericReturnValue res;
+
+ res = TALER_MHD_parse_json_data (wc->rc->connection,
+ root,
+ spec);
+ if (GNUNET_OK != res)
+ return res;
+ }
+ }
+
+ wc->typ.batch.planchets_length = json_array_size (planchets);
+ if (0 == wc->typ.batch.planchets_length)
+ {
+ GNUNET_break_op (0);
+ TALER_MHD_reply_with_error (
+ wc->rc->connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "planchets");
+ return GNUNET_SYSERR;
+ }
+
+ if (wc->typ.batch.planchets_length > TALER_MAX_FRESH_COINS)
+ {
+ GNUNET_break_op (0);
+ TALER_MHD_reply_with_error (
+ wc->rc->connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "too many planchets");
+ return GNUNET_SYSERR;
+ }
+
+ wc->typ.batch.planchets
+ = GNUNET_new_array (wc->typ.batch.planchets_length,
+ struct PlanchetContext);
+
+ for (unsigned int i = 0; i<wc->typ.batch.planchets_length; i++)
+ {
+ struct PlanchetContext *pc = &wc->typ.batch.planchets[i];
+ struct GNUNET_JSON_Specification ispec[] = {
+ GNUNET_JSON_spec_fixed_auto (
+ "reserve_sig",
+ &pc->collectable.reserve_sig),
+ GNUNET_JSON_spec_fixed_auto (
+ "denom_pub_hash",
+ &pc->collectable.denom_pub_hash),
+ TALER_JSON_spec_blinded_planchet (
+ "coin_ev",
+ &pc->blinded_planchet),
+ GNUNET_JSON_spec_end ()
+ };
+
+ {
+ enum GNUNET_GenericReturnValue res;
+
+ res = TALER_MHD_parse_json_data (
+ wc->rc->connection,
+ json_array_get (planchets, i),
+ ispec);
+ if (GNUNET_OK != res)
+ return res;
+ }
+
+ pc->collectable.reserve_pub = wc->reserve_pub;
+ for (unsigned int k = 0; k<i; k++)
+ {
+ const struct PlanchetContext *kpc = &wc->typ.batch.planchets[k];
+
+ if (0 ==
+ TALER_blinded_planchet_cmp (
+ &kpc->blinded_planchet,
+ &pc->blinded_planchet))
+ {
+ GNUNET_break_op (0);
+ TALER_MHD_reply_with_error (
+ wc->rc->connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "duplicate planchet");
+ return GNUNET_SYSERR;
+ }
+ }
+ }
+ return GNUNET_OK;
+}
+
+
+/**
+ * Creates a new context for the incoming age-withdraw request
+ *
+ * @param[in,out] rc request context
+ * @param reserve_pub public key of the reserve for the withdraw
+ * @param root json body of the request
+ * @param[out] pwc pointer to be set to the new WithdrawContext
+ * @return GNUNET_OK on success, GNUNET_SYSERR otherwise (response sent)
+ */
+static enum GNUNET_GenericReturnValue
+age_withdraw_new_request (
+ struct WithdrawContext *wc,
+ const struct TALER_ReservePublicKeyP *reserve_pub,
+ const json_t *root)
+{
+
+ wc->typ.age.commitment.reserve_pub = *reserve_pub;
+
+ /* parse the json body */
+ {
+ const json_t *j_denom_hs;
+ const json_t *j_blinded_coin_evs;
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_array_const ("denom_hs",
+ &j_denom_hs),
+ GNUNET_JSON_spec_array_const ("blinded_coin_evs",
+ &j_blinded_coin_evs),
+ GNUNET_JSON_spec_uint16 ("max_age",
+ &wc->typ.age.commitment.max_age),
+ GNUNET_JSON_spec_fixed_auto ("reserve_sig",
+ &wc->typ.age.commitment.reserve_sig),
+ GNUNET_JSON_spec_end ()
+ };
+ enum GNUNET_GenericReturnValue res;
+
+ res = TALER_MHD_parse_json_data (wc->rc->connection,
+ root,
+ spec);
+ if (GNUNET_OK != res)
+ return res;
+
+ /* The age value MUST be on the beginning of an age group */
+ if (wc->typ.age.commitment.max_age !=
+ TALER_get_lowest_age (&TEH_age_restriction_config.mask,
+ wc->typ.age.commitment.max_age))
+ {
+ GNUNET_break_op (0);
+ TALER_MHD_reply_with_ec (
+ wc->rc->connection,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "max_age must be the lower edge of an age group");
+ return GNUNET_SYSERR;
+ }
+
+ /* validate array size */
+ {
+ size_t num_coins = json_array_size (j_denom_hs);
+ const char *error = NULL;
+
+ _Static_assert ((TALER_MAX_FRESH_COINS < INT_MAX / TALER_CNC_KAPPA),
+ "TALER_MAX_FRESH_COINS too large");
+ if (0 == num_coins)
+ error = "denoms_h must not be empty";
+ else if (num_coins != json_array_size (j_blinded_coin_evs))
+ error = "denoms_h and coins_evs must be arrays of the same size";
+ else if (num_coins > TALER_MAX_FRESH_COINS)
+ /**
+ * The wallet had committed to more than the maximum coins allowed, the
+ * reserve has been charged, but now the user can not withdraw any money
+ * from it. Note that the user can't get their money back in this case!
+ */
+ error =
+ "maximum number of coins that can be withdrawn has been exceeded";
+
+ if (NULL != error)
+ {
+ GNUNET_break_op (0);
+ TALER_MHD_reply_with_ec (
+ wc->rc->connection,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ error);
+ return GNUNET_SYSERR;
+ }
+ wc->typ.age.num_coins = (unsigned int) num_coins;
+ wc->typ.age.commitment.num_coins = (unsigned int) num_coins;
+ }
+
+ wc->typ.age.denom_hs
+ = GNUNET_new_array (wc->typ.age.num_coins,
+ struct TALER_DenominationHashP);
+ {
+ size_t idx;
+ json_t *value;
+
+ json_array_foreach (j_denom_hs, idx, value) {
+ struct GNUNET_JSON_Specification ispec[] = {
+ GNUNET_JSON_spec_fixed_auto (NULL,
+ &wc->typ.age.denom_hs[idx]),
+ GNUNET_JSON_spec_end ()
+ };
+
+ res = TALER_MHD_parse_json_data (wc->rc->connection,
+ value,
+ ispec);
+ if (GNUNET_OK != res)
+ return res;
+ }
+ }
+
+ {
+ typedef struct TALER_BlindedPlanchet
+ _array_of_kappa_planchets[TALER_CNC_KAPPA];
+
+ wc->typ.age.coin_evs = GNUNET_new_array (wc->typ.age.num_coins,
+ _array_of_kappa_planchets);
+ }
+
+ /* calculate the hash over the data */
+ {
+ struct GNUNET_HashContext *hash_context;
+
+ hash_context = GNUNET_CRYPTO_hash_context_start ();
+ GNUNET_assert (NULL != hash_context);
+
+ /* Parse blinded envelopes. */
+ {
+ json_t *j_kappa_coin_evs;
+ size_t idx;
+
+ json_array_foreach (j_blinded_coin_evs, idx, j_kappa_coin_evs) {
+ if (! json_is_array (j_kappa_coin_evs))
+ {
+ char buf[256];
+
+ GNUNET_snprintf (
+ buf,
+ sizeof(buf),
+ "entry %u in array blinded_coin_evs must be an array",
+ (unsigned int) (idx + 1));
+ GNUNET_break_op (0);
+ TALER_MHD_reply_with_ec (
+ wc->rc->connection,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ buf);
+ return GNUNET_SYSERR;
+ }
+ if (TALER_CNC_KAPPA != json_array_size (j_kappa_coin_evs))
+ {
+ char buf[256];
+
+ GNUNET_snprintf (buf,
+ sizeof(buf),
+ "array no. %u in coin_evs must have length %u",
+ (unsigned int) (idx + 1),
+ (unsigned int) TALER_CNC_KAPPA);
+ GNUNET_break_op (0);
+ TALER_MHD_reply_with_ec (
+ wc->rc->connection,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ buf);
+ return GNUNET_SYSERR;
+ }
+
+ /* Now parse the individual kappa envelopes and calculate the hash of
+ * the commitment along the way. */
+ {
+ size_t kappa;
+ json_t *kvalue;
+
+ json_array_foreach (j_kappa_coin_evs, kappa, kvalue) {
+ struct GNUNET_JSON_Specification kspec[] = {
+ TALER_JSON_spec_blinded_planchet (NULL,
+ &wc->typ.age.coin_evs[idx][
+ kappa]),
+ GNUNET_JSON_spec_end ()
+ };
+
+ res = TALER_MHD_parse_json_data (wc->rc->connection,
+ kvalue,
+ kspec);
+ if (GNUNET_OK != res)
+ return res;
+
+ /* Continue to hash of the coin candidates */
+ {
+ struct TALER_BlindedCoinHashP bch;
+
+ TALER_coin_ev_hash (&wc->typ.age.coin_evs[idx][kappa],
+ &wc->typ.age.denom_hs[idx],
+ &bch);
+ GNUNET_CRYPTO_hash_context_read (hash_context,
+ &bch,
+ sizeof(bch));
+ }
+
+ /* Check for duplicate planchets. Technically a bug on
+ * the client side that is harmless for us, but still
+ * not allowed per protocol */
+ for (unsigned int i = 0; i < idx; i++)
+ {
+ if (0 ==
+ TALER_blinded_planchet_cmp (
+ &wc->typ.age.coin_evs[idx][kappa],
+ &wc->typ.age.coin_evs[i][kappa]))
+ {
+ GNUNET_break_op (0);
+ TALER_MHD_reply_with_ec (
+ wc->rc->connection,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "duplicate planchet");
+ return GNUNET_SYSERR;
+ }
+ } /* end duplicate check */
+ } /* json_array_foreach over j_kappa_coin_evs */
+ } /* scope of kappa/kvalue */
+ } /* json_array_foreach over j_blinded_coin_evs */
+ } /* scope of j_kappa_coin_evs, idx */
+
+ /* Finally, calculate the h_commitment from all blinded envelopes */
+ GNUNET_CRYPTO_hash_context_finish (hash_context,
+ &wc->typ.age.commitment.h_commitment.
+ hash);
+
+ } /* scope of hash_context */
+ } /* scope of j_denom_hs, j_blinded_coin_evs */
+
+ return GNUNET_OK;
+}
+
+
+/**
+ * Handle a "/reserves/$RESERVE_PUB/{age,batch}-withdraw" request.
+ *
+ * @param rc request context
+ * @param typ withdraw type
+ * @param root uploaded JSON data
+ * @param reserve_pub public key of the reserve
+ * @return MHD result code
+ */
+MHD_RESULT
+static
+handler_withdraw (
+ struct TEH_RequestContext *rc,
+ enum WithdrawType typ,
+ const struct TALER_ReservePublicKeyP *reserve_pub,
+ const json_t *root)
+{
+ struct WithdrawContext *wc = rc->rh_ctx;
+ enum GNUNET_GenericReturnValue r;
+
+ if (NULL == wc)
+ {
+ wc = GNUNET_new (struct WithdrawContext);
+ rc->rh_ctx = wc;
+ rc->rh_cleaner = &clean_withdraw_rc;
+ wc->rc = rc;
+ wc->now = GNUNET_TIME_timestamp_get ();
+ wc->withdraw_type = typ;
+ wc->reserve_pub = *reserve_pub;
+
+ switch (typ)
+ {
+ case WITHDRAW_TYPE_BATCH:
+ r = batch_withdraw_new_request (wc, reserve_pub, root);
+ break;
+ case WITHDRAW_TYPE_AGE:
+ r = age_withdraw_new_request (wc, reserve_pub, root);
+ break;
+ default:
+ GNUNET_break (0);
+ r = GNUNET_SYSERR;
+ TALER_MHD_reply_with_error (
+ wc->rc->connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ /* TODO: find better error code here:? */
+ TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
+ NULL);
+ }
+
+ if (GNUNET_OK != r)
+ return (GNUNET_SYSERR == r) ? MHD_NO : MHD_YES;
+
+ wc->phase = WC_PHASE_CHECK_KEYS;
+ }
+
+ while (true)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "%s processing in phase %d\n",
+ typ2str (wc),
+ wc->phase);
+
+ switch (wc->phase)
+ {
+ case WC_PHASE_CHECK_KEYS:
+ check_keys (wc);
+ break;
+ case WC_PHASE_CHECK_RESERVE_SIGNATURE:
+ check_reserve_signature (wc);
+ break;
+ case WC_PHASE_RUN_LEGI_CHECK:
+ run_legi_check (wc);
+ break;
+ case WC_PHASE_SUSPENDED:
+ return MHD_YES;
+ case WC_PHASE_CHECK_KYC_RESULT:
+ check_kyc_result (wc);
+ break;
+ case WC_PHASE_PREPARE_TRANSACTION:
+ prepare_transaction (wc);
+ break;
+ case WC_PHASE_RUN_TRANSACTION:
+ run_transaction (wc);
+ break;
+ case WC_PHASE_GENERATE_REPLY_SUCCESS:
+ generate_reply_success (wc);
+ break;
+ case WC_PHASE_GENERATE_REPLY_FAILURE:
+ return MHD_queue_response (rc->connection,
+ wc->http_status,
+ wc->response);
+ case WC_PHASE_RETURN_YES:
+ return MHD_YES;
+ case WC_PHASE_RETURN_NO:
+ return MHD_NO;
+ }
+ }
+}
+
+
+MHD_RESULT
+TEH_handler_batch_withdraw (
+ struct TEH_RequestContext *rc,
+ const struct TALER_ReservePublicKeyP *reserve_pub,
+ const json_t *root)
+{
+ return handler_withdraw (rc,
+ WITHDRAW_TYPE_BATCH,
+ reserve_pub,
+ root);
+}
+
+
+MHD_RESULT
+TEH_handler_age_withdraw (
+ struct TEH_RequestContext *rc,
+ const struct TALER_ReservePublicKeyP *reserve_pub,
+ const json_t *root)
+{
+ return handler_withdraw (rc,
+ WITHDRAW_TYPE_AGE,
+ reserve_pub,
+ root);
+}
+
+
+/* end of taler-exchange-httpd_withdraw.c */
diff --git a/src/exchange/taler-exchange-httpd_withdraw.h b/src/exchange/taler-exchange-httpd_withdraw.h
@@ -0,0 +1,77 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2024 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file taler-exchange-httpd_withdraw.h
+ * @brief Handle /reserve/$RESERVE_PUB/{age,batch}-withdraw requests
+ * @author Florian Dold
+ * @author Benedikt Mueller
+ * @author Christian Grothoff
+ * @author Özgür Kesim
+ */
+#ifndef TALER_EXCHANGE_HTTPD_WITHDRAW_H
+#define TALER_EXCHANGE_HTTPD_WITHDRAW_H
+
+#include <microhttpd.h>
+#include "taler-exchange-httpd.h"
+
+/**
+ * Resume suspended connections, we are shutting down.
+ */
+void
+TEH_withdraw_cleanup (void);
+
+
+/**
+ * Handle a "/reserves/$RESERVE_PUB/age-withdraw" request.
+ *
+ * Parses the batch of commitments to withdraw age restricted coins, and checks
+ * that the signature "reserve_sig" makes this a valid withdrawal request from
+ * the specified reserve. If the request is valid, the response contains a
+ * noreveal_index which the client has to use for the subsequent call to
+ * /age-withdraw/$ACH/reveal.
+ *
+ * @param rc request context
+ * @param root uploaded JSON data
+ * @param reserve_pub public key of the reserve
+ * @return MHD result code
+ */
+MHD_RESULT
+TEH_handler_age_withdraw (
+ struct TEH_RequestContext *rc,
+ const struct TALER_ReservePublicKeyP *reserve_pub,
+ const json_t *root);
+
+
+/**
+ * Handle a "/reserves/$RESERVE_PUB/batch-withdraw" request. Parses the batch of
+ * requested "denom_pub" which specifies the key/value of the coin to be
+ * withdrawn, and checks that the signature "reserve_sig" makes this a valid
+ * withdrawal request from the specified reserve. If so, the envelope with
+ * the blinded coin "coin_ev" is passed down to execute the withdrawal
+ * operation.
+ *
+ * @param rc request context
+ * @param root uploaded JSON data
+ * @param reserve_pub public key of the reserve
+ * @return MHD result code
+ */
+MHD_RESULT
+TEH_handler_batch_withdraw (
+ struct TEH_RequestContext *rc,
+ const struct TALER_ReservePublicKeyP *reserve_pub,
+ const json_t *root);
+
+#endif