/*
  This file is part of TALER
  Copyright (C) 2025 Taler Systems SA

  TALER is free software; you can redistribute it and/or modify
  it under the terms of the GNU Affero General Public License as
  published by the Free Software Foundation; either version 3,
  or (at your option) any later version.

  TALER is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty
  of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  See the GNU Affero General Public License for more details.

  You should have received a copy of the GNU Affero General
  Public License along with TALER; see the file COPYING.  If not,
  see <http://www.gnu.org/licenses/>
*/
/**
 * @file taler-exchange-httpd_withdraw.c
 * @brief Code to handle /withdraw requests
 * @note This endpoint is active since v26 of the protocol API
 * @author Özgür Kesim
 */

#include "platform.h"
#include <gnunet/gnunet_util_lib.h>
#include <jansson.h>
#include "taler-exchange-httpd.h"
#include "taler_json_lib.h"
#include "taler_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"

/**
 * The different type of errors that might occur, sorted by name.
 * Some of them require idempotency checks, which are marked
 * in @e idempotency_check_required below.
 */
enum WithdrawError
{
  WITHDRAW_ERROR_NONE,
  WITHDRAW_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION,
  WITHDRAW_ERROR_AGE_RESTRICTION_REQUIRED,
  WITHDRAW_ERROR_AMOUNT_OVERFLOW,
  WITHDRAW_ERROR_AMOUNT_PLUS_FEE_OVERFLOW,
  WITHDRAW_ERROR_BLINDING_SEED_REQUIRED,
  WITHDRAW_ERROR_CIPHER_MISMATCH,
  WITHDRAW_ERROR_CONFIRMATION_SIGN,
  WITHDRAW_ERROR_DB_FETCH_FAILED,
  WITHDRAW_ERROR_DB_INVARIANT_FAILURE,
  WITHDRAW_ERROR_DENOMINATION_EXPIRED,
  WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN,
  WITHDRAW_ERROR_DENOMINATION_REVOKED,
  WITHDRAW_ERROR_DENOMINATION_SIGN,
  WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE,
  WITHDRAW_ERROR_FEE_OVERFLOW,
  WITHDRAW_ERROR_IDEMPOTENT_PLANCHET,
  WITHDRAW_ERROR_INSUFFICIENT_FUNDS,
  WITHDRAW_ERROR_CRYPTO_HELPER,
  WITHDRAW_ERROR_KEYS_MISSING,
  WITHDRAW_ERROR_KYC_REQUIRED,
  WITHDRAW_ERROR_LEGITIMIZATION_RESULT,
  WITHDRAW_ERROR_MAXIMUM_AGE_TOO_LARGE,
  WITHDRAW_ERROR_NONCE_RESUSE,
  WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED,
  WITHDRAW_ERROR_RESERVE_CIPHER_UNKNOWN,
  WITHDRAW_ERROR_RESERVE_SIGNATURE_INVALID,
  WITHDRAW_ERROR_RESERVE_UNKNOWN,
};

/**
 * With the bits set in this value will be mark the errors
 * that require a check for idempotency before actually
 * returning an error.
 */
static const uint64_t idempotency_check_required =
  0
  | (1LLU << WITHDRAW_ERROR_DENOMINATION_EXPIRED)
  | (1LLU << WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN)
  | (1LLU << WITHDRAW_ERROR_DENOMINATION_REVOKED)
  | (1LLU << WITHDRAW_ERROR_INSUFFICIENT_FUNDS)
  | (1LLU << WITHDRAW_ERROR_KEYS_MISSING)
  | (1LLU << WITHDRAW_ERROR_KYC_REQUIRED);

#define IDEMPOTENCY_CHECK_REQUIRED(ec) \
        (0LLU != (idempotency_check_required & (1LLU << (ec))))


/**
 * Context for a /withdraw requests
 */
struct WithdrawContext
{

  /**
   * This struct is kept in a DLL.
   */
  struct WithdrawContext *prev;
  struct WithdrawContext *next;

  /**
     * Processing phase we are in.
     * The ordering here partially matters, as we progress through
     * them by incrementing the phase in the happy path.
     */
  enum
  {
    WITHDRAW_PHASE_PARSE = 0,
    WITHDRAW_PHASE_CHECK_KEYS,
    WITHDRAW_PHASE_CHECK_RESERVE_SIGNATURE,
    WITHDRAW_PHASE_RUN_LEGI_CHECK,
    WITHDRAW_PHASE_SUSPENDED,
    WITHDRAW_PHASE_CHECK_KYC_RESULT,
    WITHDRAW_PHASE_PREPARE_TRANSACTION,
    WITHDRAW_PHASE_RUN_TRANSACTION,
    WITHDRAW_PHASE_GENERATE_REPLY_SUCCESS,
    WITHDRAW_PHASE_GENERATE_REPLY_ERROR,
    WITHDRAW_PHASE_RETURN_NO,
    WITHDRAW_PHASE_RETURN_YES,
  } phase;


  /**
   * Handle for the legitimization check.
   */
  struct TEH_LegitimizationCheckHandle *lch;

  /**
   * Request context
   */
  const struct TEH_RequestContext *rc;

  /**
   * 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;

  /**
   * Captures all parameters provided in the JSON request
   */
  struct
  {
    /**
     * All fields (from the request or computed)
     * that we persist in the database.
     */
    struct TALER_EXCHANGEDB_Withdraw withdraw;

    /**
     * In some error cases we check for idempotency.
     * If we find an entry in the database, we mark this here.
     */
    bool is_idempotent;

    /**
     * In some error conditions the request is checked
     * for idempotency and the result from the database
     * is stored here.
     */
    struct TALER_EXCHANGEDB_Withdraw withdraw_idem;

    /**
     * Array of ``withdraw.num_coins`` hashes of the public keys
     * of the denominations to withdraw.
     */
    struct TALER_DenominationHashP *denoms_h;

    /**
     * Number of planchets.  If ``withdraw.max_age`` was _not_ set, this is equal to ``num_coins``.
     * Otherwise (``withdraw.max_age`` was set) it is ``withdraw.num_coins * kappa``.
     */
    size_t num_planchets;

    /**
     * Array of ``withdraw.num_planchets`` coin planchets.
     * Note that the size depends on the age restriction:
     * If ``withdraw.age_proof_required`` is false,
     * this is an array of length ``withdraw.num_coins``.
     * Otherwise it is an array of length ``kappa*withdraw.num_coins``,
     * arranged in runs of ``num_coins`` coins,
     * [0..num_coins)..[0..num_coins),
     * one for each #TALER_CNC_KAPPA value.
     */
    struct TALER_BlindedPlanchet *planchets;

    /**
     * If proof of age-restriction is required, the #TALER_CNC_KAPPA hashes
     * of the batches of ``withdraw.num_coins`` coins.
     */
    struct TALER_HashBlindedPlanchetsP kappa_planchets_h[TALER_CNC_KAPPA];

    /**
     * Total (over all coins) amount (excluding fee) committed to withdraw
     */
    struct TALER_Amount amount;

    /**
     * Total fees for the withdraw
     */
    struct TALER_Amount fee;

    /**
     * Array of length ``withdraw.num_cs_r_values`` of indices into
     * @e denoms_h of CS denominations.
     */
    uint32_t *cs_indices;

  } request;


  /**
   * Errors occurring during evaluation of the request are captured in this
   * struct.  In phase WITHDRAW_PHASE_GENERATE_REPLY_ERROR an appropriate error
   * message is prepared and sent to the client.
   */
  struct
  {
    /* The (internal) error code */
    enum WithdrawError code;

    /**
     * Some errors require details to be sent to the client.
     * These are captured in this union.
     * Each field is named according to the error that is using it, except
     * commented otherwise.
     */
    union
    {
      const char *request_parameter_malformed;

      const char *reserve_cipher_unknown;

      /**
       * For all errors related to a particular denomination, i.e.
       *  WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN,
       *  WITHDRAW_ERROR_DENOMINATION_EXPIRED,
       *  WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE,
       *  WITHDRAW_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION,
       * we use this one field.
       */
      const struct TALER_DenominationHashP *denom_h;

      const char *db_fetch_context;

      struct
      {
        uint16_t max_allowed;
        uint32_t birthday;
      } maximum_age_too_large;

      /**
       * The lowest age required
       */
      uint16_t age_restriction_required;

      /**
       * Balance of the reserve
       */
      struct TALER_Amount insufficient_funds;

      enum TALER_ErrorCode ec_confirmation_sign;

      enum TALER_ErrorCode ec_denomination_sign;

      struct
      {
        struct MHD_Response *response;
        unsigned int http_status;
      } legitimization_result;

    } details;
  } error;
};

/**
 * The following macros set the given error code,
 * set the phase to WITHDRAW_PHASE_GENERATE_REPLY_ERROR,
 * and optionally set the given field (with an optionally given value).
 */
#define SET_ERROR(wc, ec) \
        do \
        { GNUNET_static_assert (WITHDRAW_ERROR_NONE != ec); \
          (wc)->error.code = (ec);                          \
          (wc)->phase = WITHDRAW_PHASE_GENERATE_REPLY_ERROR; } while (0)

#define SET_ERROR_WITH_FIELD(wc, ec, field) \
        do \
        { GNUNET_static_assert (WITHDRAW_ERROR_NONE != ec); \
          (wc)->error.code = (ec);                          \
          (wc)->error.details.field = (field);              \
          (wc)->phase = WITHDRAW_PHASE_GENERATE_REPLY_ERROR; } while (0)

#define SET_ERROR_WITH_DETAIL(wc, ec, field, value) \
        do \
        { GNUNET_static_assert (WITHDRAW_ERROR_NONE != ec); \
          (wc)->error.code = (ec);                          \
          (wc)->error.details.field = (value);              \
          (wc)->phase = WITHDRAW_PHASE_GENERATE_REPLY_ERROR; } while (0)


/**
 * 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);
    wc->phase = WITHDRAW_PHASE_RETURN_NO;
    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)
    ? WITHDRAW_PHASE_RETURN_YES
    : WITHDRAW_PHASE_RETURN_NO;
}


/**
 * Check if the 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
withdraw_is_idempotent (
  struct WithdrawContext *wc)
{
  enum GNUNET_DB_QueryStatus qs;
  uint8_t max_retries = 3;

  /* We should at most be called once */
  GNUNET_assert (! wc->request.is_idempotent);
  while (0 < max_retries--)
  {
    qs = TEH_plugin->get_withdraw (
      TEH_plugin->cls,
      &wc->request.withdraw.planchets_h,
      &wc->request.withdraw_idem);
    if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
      break;
  }

  if (0 > qs)
  {
    GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
    GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
    SET_ERROR_WITH_DETAIL (wc,
                           WITHDRAW_ERROR_DB_FETCH_FAILED,
                           db_fetch_context,
                           "get_withdraw");
    return true; /* Well, kind-of. */
  }
  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    return false;

  wc->request.is_idempotent = true;
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "request is idempotent\n");

  /* Generate idempotent reply */
  TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_WITHDRAW]++;
  wc->phase = WITHDRAW_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.
 *
 * @param cls a `struct WithdrawContext *`
 * @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
withdraw_transaction (
  void *cls,
  struct MHD_Connection *connection,
  MHD_RESULT *mhd_ret)
{
  struct WithdrawContext *wc = cls;
  enum GNUNET_DB_QueryStatus qs;
  bool balance_ok;
  bool age_ok;
  bool found;
  uint16_t noreveal_index;
  bool nonce_reuse;
  uint16_t allowed_maximum_age;
  uint32_t reserve_birthday;
  struct TALER_Amount insufficient_funds;

  qs = TEH_plugin->do_withdraw (TEH_plugin->cls,
                                &wc->request.withdraw,
                                &wc->now,
                                &balance_ok,
                                &insufficient_funds,
                                &age_ok,
                                &allowed_maximum_age,
                                &reserve_birthday,
                                &found,
                                &noreveal_index,
                                &nonce_reuse);
  if (0 > qs)
  {
    if (GNUNET_DB_STATUS_HARD_ERROR == qs)
      SET_ERROR_WITH_DETAIL (wc,
                             WITHDRAW_ERROR_DB_FETCH_FAILED,
                             db_fetch_context,
                             "do_withdraw");
    return qs;
  }
  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
  {
    SET_ERROR (wc,
               WITHDRAW_ERROR_RESERVE_UNKNOWN);
    return GNUNET_DB_STATUS_HARD_ERROR;
  }

  if (found)
  {
    /**
     * The request was idempotent and we got the previous noreveal_index.
     * We simply overwrite that value in our current withdraw object and
     * move on to reply success.
     */
    wc->request.withdraw.noreveal_index = noreveal_index;
    wc->phase = WITHDRAW_PHASE_GENERATE_REPLY_SUCCESS;
    return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
  }

  if (! age_ok)
  {
    if (wc->request.withdraw.age_proof_required)
    {
      wc->error.details.maximum_age_too_large.max_allowed = allowed_maximum_age;
      wc->error.details.maximum_age_too_large.birthday = reserve_birthday;
      SET_ERROR (wc,
                 WITHDRAW_ERROR_MAXIMUM_AGE_TOO_LARGE);
    }
    else
    {
      wc->error.details.age_restriction_required = allowed_maximum_age;
      SET_ERROR (wc,
                 WITHDRAW_ERROR_AGE_RESTRICTION_REQUIRED);
    }
    return GNUNET_DB_STATUS_HARD_ERROR;
  }

  if (! balance_ok)
  {
    TEH_plugin->rollback (TEH_plugin->cls);
    SET_ERROR_WITH_FIELD (wc,
                          WITHDRAW_ERROR_INSUFFICIENT_FUNDS,
                          insufficient_funds);
    return GNUNET_DB_STATUS_HARD_ERROR;
  }

  if (nonce_reuse)
  {
    GNUNET_break (0);
    SET_ERROR (wc,
               WITHDRAW_ERROR_NONCE_RESUSE);
    return GNUNET_DB_STATUS_HARD_ERROR;
  }

  if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
    TEH_METRICS_num_success[TEH_MT_SUCCESS_WITHDRAW]++;
  return qs;
}


/**
 * The request was prepared successfully.
 * Run the main DB transaction.
 *
 * @param wc The context for the current withdraw request
 */
static void
phase_run_transaction (
  struct WithdrawContext *wc)
{
  MHD_RESULT mhd_ret;
  enum GNUNET_GenericReturnValue qs;

  GNUNET_assert (WITHDRAW_PHASE_RUN_TRANSACTION ==
                 wc->phase);
  qs = TEH_DB_run_transaction (wc->rc->connection,
                               "run withdraw",
                               TEH_MT_REQUEST_WITHDRAW,
                               &mhd_ret,
                               &withdraw_transaction,
                               wc);
  GNUNET_break (GNUNET_OK == qs);
  /* If the transaction has changed the phase, we don't alter it and return.*/
  if (WITHDRAW_PHASE_RUN_TRANSACTION != wc->phase)
    return;
  wc->phase++;
}


/**
 * The request for withdraw was parsed successfully.
 * Sign and persist the chosen blinded coins for the reveal step.
 *
 * @param wc The context for the current withdraw request
 */
static void
phase_prepare_transaction (
  struct WithdrawContext *wc)
{
  size_t offset = 0;

  wc->request.withdraw.denom_sigs
    = GNUNET_new_array (
        wc->request.withdraw.num_coins,
        struct TALER_BlindedDenominationSignature);
  /* Pick the challenge in case of age restriction  */
  if (wc->request.withdraw.age_proof_required)
  {
    wc->request.withdraw.noreveal_index =
      GNUNET_CRYPTO_random_u32 (
        GNUNET_CRYPTO_QUALITY_STRONG,
        TALER_CNC_KAPPA);
    /**
     * In case of age restriction, we use the corresponding offset in the planchet
     * array to the beginning of the coins corresponding to the noreveal_index.
     */
    offset = wc->request.withdraw.noreveal_index
             * wc->request.withdraw.num_coins;
    GNUNET_assert (offset + wc->request.withdraw.num_coins <=
                   wc->request.num_planchets);
  }

  /* Choose and sign the coins */
  {
    struct TEH_CoinSignData csds[wc->request.withdraw.num_coins];
    enum TALER_ErrorCode ec_denomination_sign;

    memset (csds,
            0,
            sizeof(csds));

    /* Pick the chosen blinded coins */
    for (uint32_t i = 0; i<wc->request.withdraw.num_coins; i++)
    {
      csds[i].bp = &wc->request.planchets[i + offset];
      csds[i].h_denom_pub = &wc->request.denoms_h[i];
    }

    ec_denomination_sign = TEH_keys_denomination_batch_sign (
      wc->request.withdraw.num_coins,
      csds,
      false,
      wc->request.withdraw.denom_sigs);
    if (TALER_EC_NONE != ec_denomination_sign)
    {
      GNUNET_break (0);
      SET_ERROR_WITH_FIELD (wc,
                            WITHDRAW_ERROR_DENOMINATION_SIGN,
                            ec_denomination_sign);
      return;
    }

    /* Save the hash value of the selected batch of coins */
    wc->request.withdraw.selected_h =
      wc->request.kappa_planchets_h[wc->request.withdraw.noreveal_index];
  }

  /**
   * For the denominations with cipher CS, calculate the R-values
   * and save the choices we made now, as at a later point, the
   * private keys for the denominations might now be available anymore
   * to make the same choice again.
   */
  if (0 <  wc->request.withdraw.num_cs_r_values)
  {
    size_t num_cs_r_values = wc->request.withdraw.num_cs_r_values;
    struct TEH_CsDeriveData cdds[num_cs_r_values];
    struct GNUNET_CRYPTO_CsSessionNonce nonces[num_cs_r_values];

    memset (nonces,
            0,
            sizeof(nonces));
    wc->request.withdraw.cs_r_values
      = GNUNET_new_array (
          num_cs_r_values,
          struct GNUNET_CRYPTO_CSPublicRPairP);
    wc->request.withdraw.cs_r_choices = 0;

    GNUNET_assert (! wc->request.withdraw.no_blinding_seed);
    TALER_cs_derive_nonces_from_seed (
      &wc->request.withdraw.blinding_seed,
      false,   /* not for melt */
      num_cs_r_values,
      wc->request.cs_indices,
      nonces);

    for (size_t i = 0; i < num_cs_r_values; i++)
    {
      size_t idx = wc->request.cs_indices[i];

      GNUNET_assert (idx < wc->request.withdraw.num_coins);
      cdds[i].h_denom_pub = &wc->request.denoms_h[idx];
      cdds[i].nonce = &nonces[i];
    }

    /**
     * Let the crypto helper generate the R-values and make the choices.
     */
    if (TALER_EC_NONE !=
        TEH_keys_denomination_cs_batch_r_pub_simple (
          wc->request.withdraw.num_cs_r_values,
          cdds,
          false,
          wc->request.withdraw.cs_r_values))
    {
      GNUNET_break (0);
      SET_ERROR (wc,
                 WITHDRAW_ERROR_CRYPTO_HELPER);
      return;
    }

    /* Now save the choices for the selected bits */
    for (size_t i = 0; i < num_cs_r_values; i++)
    {
      size_t idx = wc->request.cs_indices[i];

      struct TALER_BlindedDenominationSignature *sig =
        &wc->request.withdraw.denom_sigs[idx];
      uint8_t bit = sig->blinded_sig->details.blinded_cs_answer.b;

      wc->request.withdraw.cs_r_choices |= bit << i;
      GNUNET_static_assert (
        TALER_MAX_COINS <=
        sizeof(wc->request.withdraw.cs_r_choices) * 8);
    }
  }
  wc->phase++;
}


/**
 * Check the KYC result.
 *
 * @param wc context for request processing
 */
static void
phase_check_kyc_result (struct WithdrawContext *wc)
{
  /* return final positive response */
  if (! wc->kyc.ok)
  {
    SET_ERROR (wc,
               WITHDRAW_ERROR_KYC_REQUIRED);
    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 (WITHDRAW_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->error.details.legitimization_result.response = lcr->response;
    wc->error.details.legitimization_result.http_status = lcr->http_status;
    SET_ERROR (wc,
               WITHDRAW_ERROR_LEGITIMIZATION_RESULT);
    return;
  }
  wc->kyc = lcr->kyc;
  wc->phase = WITHDRAW_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 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 witdrawal\n",
              TALER_amount2s (&wc->request.withdraw.amount_with_fee));

  ret = cb (cb_cls,
            &wc->request.withdraw.amount_with_fee,
            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 withdrawal and limit %llu\n",
              qs,
              (unsigned long long) limit.abs_value_us);
  GNUNET_break (qs >= 0);
  return qs;
}


/**
 * Do legitimization check.
 *
 * @param wc operation context
 */
static void
phase_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->request.withdraw.reserve_pub,
    &h_full_payto,
    &payto_uri);
  if (qs < 0)
  {
    SET_ERROR_WITH_DETAIL (wc,
                           WITHDRAW_ERROR_DB_FETCH_FAILED,
                           db_fetch_context,
                           "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 = WITHDRAW_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 = WITHDRAW_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 true when denomation was found and valid,
 *         false when denomination was not valid and the state machine was advanced
 */
static enum GNUNET_GenericReturnValue
find_denomination (
  struct WithdrawContext *wc,
  struct TEH_KeyStateHandle *ksh,
  const struct TALER_DenominationHashP *denom_h,
  struct TEH_DenominationKey **pdk)
{
  struct TEH_DenominationKey *dk;

  *pdk = NULL;
  dk = TEH_keys_denomination_by_hash_from_state (
    ksh,
    denom_h,
    NULL,
    NULL);
  if (NULL == dk)
  {
    SET_ERROR_WITH_FIELD (wc,
                          WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN,
                          denom_h);
    return false;
  }
  if (GNUNET_TIME_absolute_is_past (
        dk->meta.expire_withdraw.abs_time))
  {
    SET_ERROR_WITH_FIELD (wc,
                          WITHDRAW_ERROR_DENOMINATION_EXPIRED,
                          denom_h);
    return false;
  }
  if (GNUNET_TIME_absolute_is_future (
        dk->meta.start.abs_time))
  {
    GNUNET_break_op (0);
    SET_ERROR_WITH_FIELD (wc,
                          WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE,
                          denom_h);
    return false;
  }
  if (dk->recoup_possible)
  {
    SET_ERROR (wc,
               WITHDRAW_ERROR_DENOMINATION_REVOKED);
    return false;
  }
  /* In case of age withdraw, make sure that the denomination supports age restriction */
  if (wc->request.withdraw.age_proof_required)
  {
    if (0 == dk->denom_pub.age_mask.bits)
    {
      GNUNET_break_op (0);
      SET_ERROR_WITH_FIELD (wc,
                            WITHDRAW_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION,
                            denom_h);
      return false;
    }
  }
  *pdk = dk;
  return true;
}


/**
 * Check if the given array of hashes of denomination_keys
 * a) belong to valid denominations
 * b) those are marked as age restricted, if the request is age restricted
 * c) calculate the total amount of the denominations including fees
 * for withdraw.
 *
 * @param wc context of the age withdrawal to check keys for
 */
static void
phase_check_keys (
  struct WithdrawContext *wc)
{
  struct TEH_KeyStateHandle *ksh;
  bool is_cs_denom[wc->request.withdraw.num_coins];

  memset (is_cs_denom,
          0,
          sizeof(is_cs_denom));
  ksh = TEH_keys_get_state ();
  if (NULL == ksh)
  {
    GNUNET_break (0);
    SET_ERROR (wc,
               WITHDRAW_ERROR_KEYS_MISSING);
    return;
  }
  wc->request.withdraw.denom_serials =
    GNUNET_new_array (wc->request.withdraw.num_coins,
                      uint64_t);
  GNUNET_assert (GNUNET_OK ==
                 TALER_amount_set_zero (TEH_currency,
                                        &wc->request.amount));
  GNUNET_assert (GNUNET_OK ==
                 TALER_amount_set_zero (TEH_currency,
                                        &wc->request.fee));
  GNUNET_assert (GNUNET_OK ==
                 TALER_amount_set_zero (TEH_currency,
                                        &wc->request.withdraw.amount_with_fee));

  for (unsigned int i = 0; i < wc->request.withdraw.num_coins; i++)
  {
    struct TEH_DenominationKey *dk;

    if (! find_denomination (wc,
                             ksh,
                             &wc->request.denoms_h[i],
                             &dk))
      return;
    switch (dk->denom_pub.bsign_pub_key->cipher)
    {
    case GNUNET_CRYPTO_BSA_INVALID:
      /* This should never happen (memory corruption?) */
      GNUNET_assert (0);
    case GNUNET_CRYPTO_BSA_RSA:
      /* nothing to do here */
      break;
    case GNUNET_CRYPTO_BSA_CS:
      if (wc->request.withdraw.no_blinding_seed)
      {
        GNUNET_break_op (0);
        SET_ERROR (wc,
                   WITHDRAW_ERROR_BLINDING_SEED_REQUIRED);
        return;
      }
      wc->request.withdraw.num_cs_r_values++;
      is_cs_denom[i] = true;
      break;
    }

    /* Ensure the ciphers from the planchets match the denominations'. */
    if (wc->request.withdraw.age_proof_required)
    {
      for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++)
      {
        size_t off = k * wc->request.withdraw.num_coins;

        if (dk->denom_pub.bsign_pub_key->cipher !=
            wc->request.planchets[i + off].blinded_message->cipher)
        {
          GNUNET_break_op (0);
          SET_ERROR (wc,
                     WITHDRAW_ERROR_CIPHER_MISMATCH);
          return;
        }
      }
    }
    else
    {
      if (dk->denom_pub.bsign_pub_key->cipher !=
          wc->request.planchets[i].blinded_message->cipher)
      {
        GNUNET_break_op (0);
        SET_ERROR (wc,
                   WITHDRAW_ERROR_CIPHER_MISMATCH);
        return;
      }
    }

    /* Accumulate the values */
    if (0 > TALER_amount_add (&wc->request.amount,
                              &wc->request.amount,
                              &dk->meta.value))
    {
      GNUNET_break_op (0);
      SET_ERROR (wc,
                 WITHDRAW_ERROR_AMOUNT_OVERFLOW);
      return;
    }

    /* Accumulate the withdraw fees */
    if (0 > TALER_amount_add (&wc->request.fee,
                              &wc->request.fee,
                              &dk->meta.fees.withdraw))
    {
      GNUNET_break_op (0);
      SET_ERROR (wc,
                 WITHDRAW_ERROR_FEE_OVERFLOW);
      return;
    }
    wc->request.withdraw.denom_serials[i] = dk->meta.serial;
  }

  /* Save the hash of the batch of planchets */
  if (! wc->request.withdraw.age_proof_required)
  {
    TALER_wallet_blinded_planchets_hash (
      wc->request.withdraw.num_coins,
      wc->request.planchets,
      wc->request.denoms_h,
      &wc->request.withdraw.planchets_h);
  }
  else
  {
    struct GNUNET_HashContext *ctx;

    /**
     * The age-proof-required case is a bit more involved,
     * because we need to calculate and remember kappa hashes
     * for each batch of coins.
     */
    ctx = GNUNET_CRYPTO_hash_context_start ();
    GNUNET_assert (NULL != ctx);

    for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++)
    {
      size_t off = k * wc->request.withdraw.num_coins;

      TALER_wallet_blinded_planchets_hash (
        wc->request.withdraw.num_coins,
        &wc->request.planchets[off],
        wc->request.denoms_h,
        &wc->request.kappa_planchets_h[k]);
      GNUNET_CRYPTO_hash_context_read (
        ctx,
        &wc->request.kappa_planchets_h[k],
        sizeof(wc->request.kappa_planchets_h[k]));
    }
    GNUNET_CRYPTO_hash_context_finish (
      ctx,
      &wc->request.withdraw.planchets_h.hash);
  }

  /* Save the total amount including fees */
  if (0 >  TALER_amount_add (
        &wc->request.withdraw.amount_with_fee,
        &wc->request.amount,
        &wc->request.fee))
  {
    GNUNET_break_op (0);
    SET_ERROR (wc,
               WITHDRAW_ERROR_AMOUNT_PLUS_FEE_OVERFLOW);
    return;
  }

  /* Save the indices of CS denominations */
  if (0 < wc->request.withdraw.num_cs_r_values)
  {
    size_t j = 0;

    wc->request.cs_indices = GNUNET_new_array (
      wc->request.withdraw.num_cs_r_values,
      uint32_t);

    for (size_t i = 0; i < wc->request.withdraw.num_coins; i++)
    {
      if (is_cs_denom[i])
        wc->request.cs_indices[j++] = i;
    }
  }

  wc->phase++;
}


/**
 * Check that the client signature authorizing the withdrawal is valid.
 *
 * @param[in,out] wc request context to check
 */
static void
phase_check_reserve_signature (
  struct WithdrawContext *wc)
{
  TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++;
  if (GNUNET_OK !=
      TALER_wallet_withdraw_verify (
        &wc->request.amount,
        &wc->request.fee,
        &wc->request.withdraw.planchets_h,
        wc->request.withdraw.no_blinding_seed
        ? NULL
        : &wc->request.withdraw.blinding_seed,
        (wc->request.withdraw.age_proof_required)
        ? &TEH_age_restriction_config.mask
        : NULL,
        (wc->request.withdraw.age_proof_required)
        ? wc->request.withdraw.max_age
        : 0,
        &wc->request.withdraw.reserve_pub,
        &wc->request.withdraw.reserve_sig))
  {
    GNUNET_break_op (0);
    SET_ERROR (wc,
               WITHDRAW_ERROR_RESERVE_SIGNATURE_INVALID);
    return;
  }
  wc->phase++;
}


/**
 * Free data inside of @a wd, but not @a wd itself.
 *
 * @param[in] wd withdraw data to free
 */
static void
free_db_withdraw_data (struct TALER_EXCHANGEDB_Withdraw *wd)
{
  if (NULL != wd->denom_sigs)
  {
    for (unsigned int i = 0; i<wd->num_coins; i++)
      TALER_blinded_denom_sig_free (&wd->denom_sigs[i]);
    GNUNET_free (wd->denom_sigs);
  }
  GNUNET_free (wd->denom_serials);
  GNUNET_free (wd->cs_r_values);
}


/**
 * Cleanup routine for withdraw request.
 * The function is called upon completion of the request
 * that should clean up @a rh_ctx. Can be NULL.
 *
 * @param rc request context to clean up
 */
static void
clean_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;
  }
  GNUNET_free (wc->request.denoms_h);
  for (unsigned int i = 0; i<wc->request.num_planchets; i++)
    TALER_blinded_planchet_free (&wc->request.planchets[i]);
  GNUNET_free (wc->request.planchets);
  free_db_withdraw_data (&wc->request.withdraw);
  GNUNET_free (wc->request.cs_indices);
  if (wc->request.is_idempotent)
    free_db_withdraw_data (&wc->request.withdraw_idem);
  if ( (WITHDRAW_ERROR_LEGITIMIZATION_RESULT == wc->error.code) &&
       (NULL != wc->error.details.legitimization_result.response) )
  {
    MHD_destroy_response (wc->error.details.legitimization_result.response);
    wc->error.details.legitimization_result.response = NULL;
  }
  GNUNET_free (wc);
}


/**
 * Generates response for the withdraw request.
 *
 * @param wc withdraw operation context
 */
static void
phase_generate_reply_success (struct WithdrawContext *wc)
{
  struct TALER_EXCHANGEDB_Withdraw *db_obj;

  db_obj = wc->request.is_idempotent
    ? &wc->request.withdraw_idem
    : &wc->request.withdraw;

  if (wc->request.withdraw.age_proof_required)
  {
    struct TALER_ExchangePublicKeyP pub;
    struct TALER_ExchangeSignatureP sig;
    enum TALER_ErrorCode ec_confirmation_sign;

    ec_confirmation_sign =
      TALER_exchange_online_withdraw_age_confirmation_sign (
        &TEH_keys_exchange_sign_,
        &db_obj->planchets_h,
        db_obj->noreveal_index,
        &pub,
        &sig);
    if (TALER_EC_NONE != ec_confirmation_sign)
    {
      SET_ERROR_WITH_FIELD (wc,
                            WITHDRAW_ERROR_CONFIRMATION_SIGN,
                            ec_confirmation_sign);
      return;
    }

    finish_loop (wc,
                 TALER_MHD_REPLY_JSON_PACK (
                   wc->rc->connection,
                   MHD_HTTP_CREATED,
                   GNUNET_JSON_pack_uint64 ("noreveal_index",
                                            db_obj->noreveal_index),
                   GNUNET_JSON_pack_data_auto ("exchange_sig",
                                               &sig),
                   GNUNET_JSON_pack_data_auto ("exchange_pub",
                                               &pub)));
  }
  else /* not age restricted */
  {
    json_t *sigs;

    sigs = json_array ();
    GNUNET_assert (NULL != sigs);
    for (unsigned int i = 0; i<db_obj->num_coins; i++)
    {
      GNUNET_assert (
        0 ==
        json_array_append_new (
          sigs,
          GNUNET_JSON_PACK (
            TALER_JSON_pack_blinded_denom_sig (
              NULL,
              &db_obj->denom_sigs[i]))));
    }
    finish_loop (wc,
                 TALER_MHD_REPLY_JSON_PACK (
                   wc->rc->connection,
                   MHD_HTTP_OK,
                   GNUNET_JSON_pack_array_steal ("ev_sigs",
                                                 sigs)));
  }

  TEH_METRICS_withdraw_num_coins += db_obj->num_coins;
}


/**
 * Reports an error, potentially with details.
 * That is, it puts a error-type specific response into the MHD queue.
 * It will do a idempotency check first, if needed for the error type.
 *
 * @param wc withdraw context
 */
static void
phase_generate_reply_error (
  struct WithdrawContext *wc)
{
  GNUNET_assert (WITHDRAW_PHASE_GENERATE_REPLY_ERROR == wc->phase);
  if (IDEMPOTENCY_CHECK_REQUIRED (wc->error.code) &&
      withdraw_is_idempotent (wc))
  {
    return;
  }

  switch (wc->error.code)
  {
  case WITHDRAW_ERROR_NONE:
    break;
  case WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED:
    finish_loop (wc,
                 TALER_MHD_reply_with_error (
                   wc->rc->connection,
                   MHD_HTTP_BAD_REQUEST,
                   TALER_EC_GENERIC_PARAMETER_MALFORMED,
                   wc->error.details.request_parameter_malformed));
    return;
  case WITHDRAW_ERROR_KEYS_MISSING:
    finish_loop (wc,
                 TALER_MHD_reply_with_error (
                   wc->rc->connection,
                   MHD_HTTP_INTERNAL_SERVER_ERROR,
                   TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
                   NULL));
    return;
  case WITHDRAW_ERROR_DB_FETCH_FAILED:
    finish_loop (wc,
                 TALER_MHD_reply_with_error (
                   wc->rc->connection,
                   MHD_HTTP_INTERNAL_SERVER_ERROR,
                   TALER_EC_GENERIC_DB_FETCH_FAILED,
                   wc->error.details.db_fetch_context));
    return;
  case WITHDRAW_ERROR_DB_INVARIANT_FAILURE:
    finish_loop (wc,
                 TALER_MHD_reply_with_error (
                   wc->rc->connection,
                   MHD_HTTP_INTERNAL_SERVER_ERROR,
                   TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
                   NULL));
    return;
  case WITHDRAW_ERROR_RESERVE_UNKNOWN:
    finish_loop (wc,
                 TALER_MHD_reply_with_ec (
                   wc->rc->connection,
                   TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN,
                   NULL));
    return;
  case WITHDRAW_ERROR_DENOMINATION_SIGN:
    finish_loop (wc,
                 TALER_MHD_reply_with_ec (
                   wc->rc->connection,
                   wc->error.details.ec_denomination_sign,
                   NULL));
    return;
  case WITHDRAW_ERROR_KYC_REQUIRED:
    finish_loop (wc,
                 TEH_RESPONSE_reply_kyc_required (
                   wc->rc->connection,
                   &wc->h_normalized_payto,
                   &wc->kyc,
                   false));
    return;
  case WITHDRAW_ERROR_DENOMINATION_KEY_UNKNOWN:
    GNUNET_break_op (0);
    finish_loop (wc,
                 TEH_RESPONSE_reply_unknown_denom_pub_hash (
                   wc->rc->connection,
                   wc->error.details.denom_h));
    return;
  case WITHDRAW_ERROR_DENOMINATION_EXPIRED:
    GNUNET_break_op (0);
    finish_loop (wc,
                 TEH_RESPONSE_reply_expired_denom_pub_hash (
                   wc->rc->connection,
                   wc->error.details.denom_h,
                   TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED,
                   "WITHDRAW"));
    return;
  case WITHDRAW_ERROR_DENOMINATION_VALIDITY_IN_FUTURE:
    finish_loop (wc,
                 TEH_RESPONSE_reply_expired_denom_pub_hash (
                   wc->rc->connection,
                   wc->error.details.denom_h,
                   TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE,
                   "WITHDRAW"));
    return;
  case WITHDRAW_ERROR_DENOMINATION_REVOKED:
    GNUNET_break_op (0);
    finish_loop (wc,
                 TALER_MHD_reply_with_ec (
                   wc->rc->connection,
                   TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED,
                   NULL));
    return;
  case WITHDRAW_ERROR_CIPHER_MISMATCH:
    finish_loop (wc,
                 TALER_MHD_reply_with_ec (
                   wc->rc->connection,
                   TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH,
                   NULL));
    return;
  case WITHDRAW_ERROR_BLINDING_SEED_REQUIRED:
    finish_loop (wc,
                 TALER_MHD_reply_with_ec (
                   wc->rc->connection,
                   TALER_EC_GENERIC_PARAMETER_MISSING,
                   "blinding_seed"));
    return;
  case WITHDRAW_ERROR_CRYPTO_HELPER:
    finish_loop (wc,
                 TALER_MHD_reply_with_ec (
                   wc->rc->connection,
                   TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                   NULL));
    return;
  case WITHDRAW_ERROR_RESERVE_CIPHER_UNKNOWN:
    finish_loop (wc,
                 TALER_MHD_reply_with_ec (
                   wc->rc->connection,
                   TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH,
                   "cipher"));
    return;
  case WITHDRAW_ERROR_AGE_RESTRICTION_NOT_SUPPORTED_BY_DENOMINATION:
    {
      char msg[256];

      GNUNET_snprintf (msg,
                       sizeof(msg),
                       "denomination %s does not support age restriction",
                       GNUNET_h2s (&wc->error.details.denom_h->hash));
      finish_loop (wc,
                   TALER_MHD_reply_with_ec (
                     wc->rc->connection,
                     TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN,
                     msg));
      return;
    }
  case WITHDRAW_ERROR_MAXIMUM_AGE_TOO_LARGE:
    finish_loop (wc,
                 TALER_MHD_REPLY_JSON_PACK (
                   wc->rc->connection,
                   MHD_HTTP_CONFLICT,
                   TALER_MHD_PACK_EC (
                     TALER_EC_EXCHANGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE),
                   GNUNET_JSON_pack_uint64 (
                     "allowed_maximum_age",
                     wc->error.details.maximum_age_too_large.max_allowed),
                   GNUNET_JSON_pack_uint64 (
                     "reserve_birthday",
                     wc->error.details.maximum_age_too_large.birthday)));
    return;
  case WITHDRAW_ERROR_AGE_RESTRICTION_REQUIRED:
    finish_loop (wc,
                 TEH_RESPONSE_reply_reserve_age_restriction_required (
                   wc->rc->connection,
                   wc->error.details.age_restriction_required));
    return;
  case WITHDRAW_ERROR_AMOUNT_OVERFLOW:
    finish_loop (wc,
                 TALER_MHD_reply_with_error (
                   wc->rc->connection,
                   MHD_HTTP_BAD_REQUEST,
                   TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_OVERFLOW,
                   "amount"));
    return;
  case WITHDRAW_ERROR_FEE_OVERFLOW:
    finish_loop (wc,
                 TALER_MHD_reply_with_error (
                   wc->rc->connection,
                   MHD_HTTP_BAD_REQUEST,
                   TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_OVERFLOW,
                   "fee"));
    return;
  case WITHDRAW_ERROR_AMOUNT_PLUS_FEE_OVERFLOW:
    finish_loop (wc,
                 TALER_MHD_reply_with_error (
                   wc->rc->connection,
                   MHD_HTTP_INTERNAL_SERVER_ERROR,
                   TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW,
                   "amount+fee"));
    return;
  case WITHDRAW_ERROR_CONFIRMATION_SIGN:
    finish_loop (wc,
                 TALER_MHD_reply_with_ec (
                   wc->rc->connection,
                   wc->error.details.ec_confirmation_sign,
                   NULL));
    return;
  case WITHDRAW_ERROR_INSUFFICIENT_FUNDS:
    finish_loop (wc,
                 TEH_RESPONSE_reply_reserve_insufficient_balance (
                   wc->rc->connection,
                   TALER_EC_EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS,
                   &wc->error.details.insufficient_funds,
                   &wc->request.withdraw.amount_with_fee,
                   &wc->request.withdraw.reserve_pub));
    return;
  case WITHDRAW_ERROR_IDEMPOTENT_PLANCHET:
    finish_loop (wc,
                 TALER_MHD_reply_with_error (
                   wc->rc->connection,
                   MHD_HTTP_CONFLICT,
                   TALER_EC_EXCHANGE_WITHDRAW_IDEMPOTENT_PLANCHET,
                   NULL));
    return;
  case WITHDRAW_ERROR_NONCE_RESUSE:
    finish_loop (wc,
                 TALER_MHD_reply_with_error (
                   wc->rc->connection,
                   MHD_HTTP_CONFLICT,
                   TALER_EC_EXCHANGE_WITHDRAW_NONCE_REUSE,
                   "blinding_seed"));
    return;
  case WITHDRAW_ERROR_RESERVE_SIGNATURE_INVALID:
    finish_loop (wc,
                 TALER_MHD_reply_with_ec (
                   wc->rc->connection,
                   TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID,
                   NULL));
    return;
  case WITHDRAW_ERROR_LEGITIMIZATION_RESULT: {
      finish_loop (
        wc,
        MHD_queue_response (wc->rc->connection,
                            wc->error.details.legitimization_result.http_status,
                            wc->error.details.legitimization_result.response));
      return;
    }
  }
  GNUNET_break (0);
  finish_loop (wc,
               TALER_MHD_reply_with_error (
                 wc->rc->connection,
                 MHD_HTTP_INTERNAL_SERVER_ERROR,
                 TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                 "error phase without error"));
}


/**
 * Initializes the new context for the incoming withdraw request
 *
 * @param[in,out] wc withdraw request context
 * @param root json body of the request
 */
static void
withdraw_phase_parse (
  struct WithdrawContext *wc,
  const json_t *root)
{
  const json_t *j_denoms_h;
  const json_t *j_coin_evs;
  const char *cipher;
  bool no_max_age;
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_string ("cipher",
                             &cipher),
    GNUNET_JSON_spec_fixed_auto  ("reserve_pub",
                                  &wc->request.withdraw.reserve_pub),
    GNUNET_JSON_spec_array_const ("denoms_h",
                                  &j_denoms_h),
    GNUNET_JSON_spec_array_const ("coin_evs",
                                  &j_coin_evs),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_uint16 ("max_age",
                               &wc->request.withdraw.max_age),
      &no_max_age),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_fixed_auto ("blinding_seed",
                                   &wc->request.withdraw.blinding_seed),
      &wc->request.withdraw.no_blinding_seed),
    GNUNET_JSON_spec_fixed_auto ("reserve_sig",
                                 &wc->request.withdraw.reserve_sig),
    GNUNET_JSON_spec_end ()
  };
  enum GNUNET_GenericReturnValue res;

  res = TALER_MHD_parse_json_data (wc->rc->connection,
                                   root,
                                   spec);
  if (GNUNET_YES != res)
  {
    GNUNET_break_op (0);
    wc->phase = (GNUNET_SYSERR == res)
      ? WITHDRAW_PHASE_RETURN_NO
      : WITHDRAW_PHASE_RETURN_YES;
    return;
  }

  /* For now, we only support cipher "ED25519" for signatures by the reserve */
  if (0 != strcmp ("ED25519",
                   cipher))
  {
    GNUNET_break_op (0);
    SET_ERROR_WITH_DETAIL (wc,
                           WITHDRAW_ERROR_RESERVE_CIPHER_UNKNOWN,
                           reserve_cipher_unknown,
                           cipher);
    return;
  }

  wc->request.withdraw.age_proof_required = ! no_max_age;

  if (wc->request.withdraw.age_proof_required)
  {
    /* The age value MUST be on the beginning of an age group */
    if (wc->request.withdraw.max_age !=
        TALER_get_lowest_age (&TEH_age_restriction_config.mask,
                              wc->request.withdraw.max_age))
    {
      GNUNET_break_op (0);
      SET_ERROR_WITH_DETAIL (
        wc,
        WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED,
        request_parameter_malformed,
        "max_age must be the lower edge of an age group");
      return;
    }
  }

  /* validate array size */
  {
    size_t num_coins = json_array_size (j_denoms_h);
    size_t array_size = json_array_size (j_coin_evs);
    const char *error;

    GNUNET_static_assert (
      TALER_MAX_COINS < INT_MAX / TALER_CNC_KAPPA);

#define BAIL_IF(cond, msg) \
        if ((cond)) { \
          GNUNET_break_op (0); \
          error = (msg); break; \
        }

    do {
      BAIL_IF (0 == num_coins,
               "denoms_h must not be empty")

      /**
         * The wallet had committed to more than the maximum coins allowed, the
         * reserve has been charged, but now the user can not withdraw any money
         * from it.  Note that the user can't get their money back in this case!
         */
      BAIL_IF (num_coins > TALER_MAX_COINS,
               "maximum number of coins that can be withdrawn has been exceeded")

      BAIL_IF ((! wc->request.withdraw.age_proof_required) &&
               (num_coins != array_size),
               "denoms_h and coin_evs must be arrays of the same size")

      BAIL_IF (wc->request.withdraw.age_proof_required &&
               ((TALER_CNC_KAPPA * num_coins) != array_size),
               "coin_evs must be an array of length "
               TALER_CNC_KAPPA_STR
               "*len(denoms_h)")

      wc->request.withdraw.num_coins = num_coins;
      wc->request.num_planchets = array_size;
      error = NULL;

    } while (0);
#undef BAIL_IF

    if (NULL != error)
    {
      SET_ERROR_WITH_DETAIL (wc,
                             WITHDRAW_ERROR_REQUEST_PARAMETER_MALFORMED,
                             request_parameter_malformed,
                             error);
      return;
    }
  }
  /* extract the denomination hashes */
  {
    size_t idx;
    json_t *value;

    wc->request.denoms_h
      = GNUNET_new_array (wc->request.withdraw.num_coins,
                          struct TALER_DenominationHashP);

    json_array_foreach (j_denoms_h, idx, value) {
      struct GNUNET_JSON_Specification ispec[] = {
        GNUNET_JSON_spec_fixed_auto (NULL,
                                     &wc->request.denoms_h[idx]),
        GNUNET_JSON_spec_end ()
      };

      res = TALER_MHD_parse_json_data (wc->rc->connection,
                                       value,
                                       ispec);
      if (GNUNET_YES != res)
      {
        GNUNET_break_op (0);
        wc->phase = (GNUNET_SYSERR == res)
          ? WITHDRAW_PHASE_RETURN_NO
          : WITHDRAW_PHASE_RETURN_YES;
        return;
      }
    }
  }
  /* Parse the blinded coin envelopes */
  {
    json_t *j_cev;
    size_t idx;

    wc->request.planchets =
      GNUNET_new_array (wc->request.num_planchets,
                        struct  TALER_BlindedPlanchet);
    json_array_foreach (j_coin_evs, idx, j_cev)
    {
      /* Now parse the individual envelopes and calculate the hash of
       * the commitment along the way. */
      struct GNUNET_JSON_Specification kspec[] = {
        TALER_JSON_spec_blinded_planchet (NULL,
                                          &wc->request.planchets[idx]),
        GNUNET_JSON_spec_end ()
      };

      res = TALER_MHD_parse_json_data (wc->rc->connection,
                                       j_cev,
                                       kspec);
      if (GNUNET_OK != res)
      {
        GNUNET_break_op (0);
        wc->phase = (GNUNET_SYSERR == res)
          ? WITHDRAW_PHASE_RETURN_NO
          : WITHDRAW_PHASE_RETURN_YES;
        return;
      }

      /* Check for duplicate planchets. Technically a bug on
       * the client side that is harmless for us, but still
       * not allowed per protocol */
      for (size_t i = 0; i < idx; i++)
      {
        if (0 ==
            TALER_blinded_planchet_cmp (
              &wc->request.planchets[idx],
              &wc->request.planchets[i]))
        {
          GNUNET_break_op (0);
          SET_ERROR (wc,
                     WITHDRAW_ERROR_IDEMPOTENT_PLANCHET);
          return;
        }
      }       /* end duplicate check */
    }       /* json_array_foreach over j_coin_evs */
  }       /* scope of j_kappa_planchets, idx */
  wc->phase = WITHDRAW_PHASE_CHECK_KEYS;
}


MHD_RESULT
TEH_handler_withdraw (
  struct TEH_RequestContext *rc,
  const json_t *root,
  const char *const args[0])
{
  struct WithdrawContext *wc = rc->rh_ctx;

  (void) args;
  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 ();
  }
  while (true)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "withdrawal%s processing in phase %d\n",
                wc->request.withdraw.age_proof_required
                     ? " (with required age proof)"
                     : "",
                wc->phase);
    switch (wc->phase)
    {
    case WITHDRAW_PHASE_PARSE:
      withdraw_phase_parse (wc,
                            root);
      break;
    case WITHDRAW_PHASE_CHECK_KEYS:
      phase_check_keys (wc);
      break;
    case WITHDRAW_PHASE_CHECK_RESERVE_SIGNATURE:
      phase_check_reserve_signature (wc);
      break;
    case WITHDRAW_PHASE_RUN_LEGI_CHECK:
      phase_run_legi_check (wc);
      break;
    case WITHDRAW_PHASE_SUSPENDED:
      return MHD_YES;
    case WITHDRAW_PHASE_CHECK_KYC_RESULT:
      phase_check_kyc_result (wc);
      break;
    case WITHDRAW_PHASE_PREPARE_TRANSACTION:
      phase_prepare_transaction (wc);
      break;
    case WITHDRAW_PHASE_RUN_TRANSACTION:
      phase_run_transaction (wc);
      break;
    case WITHDRAW_PHASE_GENERATE_REPLY_SUCCESS:
      phase_generate_reply_success (wc);
      break;
    case WITHDRAW_PHASE_GENERATE_REPLY_ERROR:
      phase_generate_reply_error (wc);
      break;
    case WITHDRAW_PHASE_RETURN_YES:
      return MHD_YES;
    case WITHDRAW_PHASE_RETURN_NO:
      return MHD_NO;
    }
  }
}
