gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-exchange] branch master updated (2d4ebd3f -> f969bd3c)


From: gnunet
Subject: [taler-exchange] branch master updated (2d4ebd3f -> f969bd3c)
Date: Mon, 03 Jul 2023 16:23:36 +0200

This is an automated email from the git hooks/post-receive script.

oec pushed a change to branch master
in repository exchange.

    from 2d4ebd3f unconfuse pay and deposit
     new 3ec14744 -fix typos for doxygen
     new 9130cda9 Merge branch 'master' into age-withdraw
     new 4833234d Merge branch 'master' into age-withdraw
     new a04425df [age-withdraw] WIP: change schema to use new support for 
array types, 11/n
     new 9f1f069c Merge branch 'age-withdraw' of 
ssh://git.kesim.org/taler/exchange into age-withdraw
     new 4a31a180 [æge-withdraw] WiP: towards new API
     new 80a1b8f5 Merge branch 'master' into age-withdraw
     new fb5bc18c Need libgnunetpq v4.0.0++
     new 98b51edf Merge branch 'master' into age-withdraw
     new 46188ae0 Merge branch 'master' into age-withdraw
     new 70bfe0ed Merge branch 'master' into age-withdraw
     new ddedf03a [age-withdraw] age-withdraw commit- and reveal-handlers  
implemented, 12/n
     new b87d1112 Merge branch 'master' into age-withdraw-merge
     new c5c3a44c -comment updates
     new 17001e44 -free spec
     new eeece1c9 -ruuid not needed
     new ee42b706 add exchange_do_age_withdraw.sql
     new ddd0e0af -fixed typos
     new 145310e2 added birthdate parser
     new f8536e8c -fix parameter in stored procedures
     new 89de1678 -fix typo
     new 2f21fa24 -rename variable
     new 1be14a34 [age-withdraw] added handlers for age-withdraw to router
     new d3d744a4 [testing] start an oauth2 server which returns static 
birthdates
     new e889179b [auditor] use the date for the report-directory
     new 537206e4 -update author
     new 9c3ddcbc added TALER_adult_age(struct TALER_AgeMask *mask)
     new 40629e89 [age-withdraw] added library function for age-withdraw
     new f969bd3c Merge branch 'master' into age-withdraw

The 29 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 configure.ac                                       |    4 +-
 contrib/gana                                       |    2 +-
 src/auditor/taler-auditor.in                       |    4 +-
 src/exchange/taler-exchange-httpd.c                |   52 +
 src/exchange/taler-exchange-httpd_age-withdraw.c   |  923 ++++++++++++++---
 .../taler-exchange-httpd_age-withdraw_reveal.c     |  933 +++++-------------
 .../taler-exchange-httpd_age-withdraw_reveal.h     |    4 +-
 src/exchange/taler-exchange-httpd_batch-withdraw.c |   36 +-
 src/exchange/taler-exchange-httpd_common_kyc.c     |   33 +-
 src/exchange/taler-exchange-httpd_csr.c            |   16 +-
 src/exchange/taler-exchange-httpd_extensions.c     |    2 +-
 src/exchange/taler-exchange-httpd_keys.c           |   92 +-
 src/exchange/taler-exchange-httpd_keys.h           |    4 +-
 .../taler-exchange-httpd_refreshes_reveal.c        |   10 +-
 src/exchange/taler-exchange-httpd_responses.c      |   14 +
 src/exchange/taler-exchange-httpd_responses.h      |   14 +
 src/exchange/taler-exchange-httpd_withdraw.c       |   11 +-
 src/exchangedb/0002-reserves.sql                   |    4 +-
 ...hdraw_commitments.sql => 0003-age_withdraw.sql} |   71 +-
 src/exchangedb/0003-age_withdraw_reveals.sql       |  152 ---
 src/exchangedb/Makefile.am                         |    3 +-
 src/exchangedb/exchange-0003.sql.in                |    3 +-
 src/exchangedb/exchange_do_age_withdraw.sql        |  177 ++++
 src/exchangedb/exchange_do_batch_withdraw.sql      |   44 +-
 .../exchange_do_batch_withdraw_insert.sql          |    7 +-
 .../exchange_do_insert_kyc_attributes.sql          |    2 +-
 ...pg_do_batch_withdraw.c => pg_do_age_withdraw.c} |   49 +-
 .../{pg_do_withdraw.h => pg_do_age_withdraw.h}     |   38 +-
 src/exchangedb/pg_do_batch_withdraw.c              |   13 +-
 src/exchangedb/pg_do_batch_withdraw.h              |    6 +
 ...t_age_withdraw_info.c => pg_get_age_withdraw.c} |   66 +-
 ...t_age_withdraw_info.h => pg_get_age_withdraw.h} |   14 +-
 src/exchangedb/pg_insert_age_withdraw_reveal.c     |  106 --
 src/exchangedb/pg_insert_age_withdraw_reveal.h     |   43 -
 src/exchangedb/pg_insert_records_by_table.c        |   90 +-
 src/exchangedb/pg_iterate_denominations.c          |    5 +-
 src/exchangedb/pg_lookup_records_by_table.c        |  118 +--
 src/exchangedb/pg_lookup_serial_by_table.c         |   21 +-
 src/exchangedb/plugin_exchangedb_postgres.c        |    9 +-
 src/exchangedb/procedures.sql.in                   |    1 +
 src/exchangedb/shard-0001.sql                      |    2 +-
 src/include/taler_crypto_lib.h                     |   35 +-
 src/include/taler_exchange_service.h               |  154 ++-
 src/include/taler_exchangedb_plugin.h              |  166 ++--
 src/include/taler_testing_lib.h                    |   17 +-
 src/include/taler_util.h                           |   60 ++
 src/lib/Makefile.am                                |    1 +
 src/lib/exchange_api_age_withdraw.c                | 1033 ++++++++++++++++++++
 src/lib/exchange_api_batch_withdraw.c              |   30 +-
 src/lib/exchange_api_batch_withdraw2.c             |   35 +-
 src/lib/exchange_api_common.c                      |    4 +-
 src/lib/exchange_api_csr_withdraw.c                |   27 +-
 src/lib/exchange_api_curl_defaults.h               |    1 -
 src/lib/exchange_api_handle.c                      |    2 +
 src/lib/exchange_api_melt.c                        |    1 +
 src/lib/exchange_api_refreshes_reveal.c            |    1 +
 src/lib/exchange_api_withdraw.c                    |   34 +-
 src/lib/exchange_api_withdraw2.c                   |   33 +-
 src/testing/testing_api_cmd_batch_withdraw.c       |   15 +-
 src/testing/testing_api_cmd_oauth.c                |   36 +-
 src/testing/testing_api_cmd_withdraw.c             |   15 +-
 src/util/age_restriction.c                         |   89 +-
 src/util/exchange_signatures.c                     |   28 +
 src/util/test_age_restriction.c                    |  169 +++-
 src/util/wallet_signatures.c                       |   35 +-
 65 files changed, 3468 insertions(+), 1751 deletions(-)
 rename src/exchangedb/{0003-age_withdraw_commitments.sql => 
0003-age_withdraw.sql} (52%)
 delete mode 100644 src/exchangedb/0003-age_withdraw_reveals.sql
 create mode 100644 src/exchangedb/exchange_do_age_withdraw.sql
 copy src/exchangedb/{pg_do_batch_withdraw.c => pg_do_age_withdraw.c} (54%)
 copy src/exchangedb/{pg_do_withdraw.h => pg_do_age_withdraw.h} (53%)
 rename src/exchangedb/{pg_get_age_withdraw_info.c => pg_get_age_withdraw.c} 
(54%)
 rename src/exchangedb/{pg_get_age_withdraw_info.h => pg_get_age_withdraw.h} 
(79%)
 delete mode 100644 src/exchangedb/pg_insert_age_withdraw_reveal.c
 delete mode 100644 src/exchangedb/pg_insert_age_withdraw_reveal.h
 create mode 100644 src/lib/exchange_api_age_withdraw.c

diff --git a/configure.ac b/configure.ac
index 96c368c6..793e31be 100644
--- a/configure.ac
+++ b/configure.ac
@@ -274,11 +274,11 @@ AS_CASE([$with_gnunet],
          CPPFLAGS="-I$with_gnunet/include ${CPPFLAGS}"])
 CPPFLAGS="${CPPFLAGS} ${POSTGRESQL_CPPFLAGS}"
 AC_CHECK_HEADERS([gnunet/gnunet_pq_lib.h],
- [AC_CHECK_LIB([gnunetpq], [GNUNET_PQ_result_spec_string], libgnunetpq=1)])
+ [AC_CHECK_LIB([gnunetpq], [GNUNET_PQ_result_spec_array_string], 
libgnunetpq=1)])
 AS_IF([test $libgnunetpq != 1],
   [AC_MSG_ERROR([[
 ***
-*** You need libgnunetpq to build this program.
+*** You need libgnunetpq version >= 4.0.0 to build this program.
 *** Make sure you have Postgres installed while
 *** building GNUnet (and that your GNUnet version
 *** is recent!)
diff --git a/contrib/gana b/contrib/gana
index 3e5591a7..31b74264 160000
--- a/contrib/gana
+++ b/contrib/gana
@@ -1 +1 @@
-Subproject commit 3e5591a7e3fd93ba46fc2b538c63f0c16336283d
+Subproject commit 31b74264e62c4a7f4a671033e214c43fa2f304c0
diff --git a/src/auditor/taler-auditor.in b/src/auditor/taler-auditor.in
index b00228fb..ab3d8d20 100644
--- a/src/auditor/taler-auditor.in
+++ b/src/auditor/taler-auditor.in
@@ -83,7 +83,9 @@ optcheck "$@"
 ARGS=("$@")
 ARGS=(${ARGS[@]/$INF})
 
-DIR=`mktemp -d reportXXXXXX`
+DATE=`date +%F_%H:%M:%S`
+DIR="report_$DATE"
+mkdir $DIR
 for n in aggregation coins deposits purses reserves
 do
   taler-helper-auditor-$n ${ARGS[*]} > ${DIR}/$n.json
diff --git a/src/exchange/taler-exchange-httpd.c 
b/src/exchange/taler-exchange-httpd.c
index 348967f7..25af862b 100644
--- a/src/exchange/taler-exchange-httpd.c
+++ b/src/exchange/taler-exchange-httpd.c
@@ -30,6 +30,8 @@
 #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_age-withdraw_reveal.h"
 #include "taler-exchange-httpd_aml-decision.h"
 #include "taler-exchange-httpd_auditors.h"
 #include "taler-exchange-httpd_batch-deposit.h"
@@ -571,6 +573,46 @@ handle_get_aml (struct TEH_RequestContext *rc,
 }
 
 
+/**
+ * Handle a "/age-withdraw/$ACH/reveal" POST request.  Parses the "ACH"
+ * hash of the commitment from a previous call to
+ * /reserves/$reserve_pub/age-withdraw
+ *
+ * @param rc request context
+ * @param root uploaded JSON data
+ * @param args array of additional options
+ * @return MHD result code
+ */
+static MHD_RESULT
+handle_post_age_withdraw (struct TEH_RequestContext *rc,
+                          const json_t *root,
+                          const char *const args[2])
+{
+  struct TALER_AgeWithdrawCommitmentHashP ach;
+
+  if (0 != strcmp ("reveal", args[1]))
+    return r404 (rc->connection,
+                 args[1]);
+
+  if (GNUNET_OK !=
+      GNUNET_STRINGS_string_to_data (args[0],
+                                     strlen (args[0]),
+                                     &ach,
+                                     sizeof (ach)))
+  {
+    GNUNET_break_op (0);
+    return TALER_MHD_reply_with_error (rc->connection,
+                                       MHD_HTTP_BAD_REQUEST,
+                                       TALER_EC_GENERIC_RESERVE_PUB_MALFORMED,
+                                       args[0]);
+  }
+
+  return TEH_handler_age_withdraw_reveal (rc,
+                                          &ach,
+                                          root);
+}
+
+
 /**
  * Signature of functions that handle operations on reserves.
  *
@@ -617,6 +659,10 @@ handle_post_reserves (struct TEH_RequestContext *rc,
       .op = "batch-withdraw",
       .handler = &TEH_handler_batch_withdraw
     },
+    {
+      .op = "age-withdraw",
+      .handler = &TEH_handler_age_withdraw
+    },
     {
       .op = "withdraw",
       .handler = &TEH_handler_withdraw
@@ -1454,6 +1500,12 @@ handle_mhd_request (void *cls,
       .handler.post = &handle_post_reserves,
       .nargs = 2
     },
+    {
+      .url = "age-withdraw",
+      .method = MHD_HTTP_METHOD_POST,
+      .handler.post = &handle_post_age_withdraw,
+      .nargs = 2
+    },
     {
       .url = "reserves-attest",
       .method = MHD_HTTP_METHOD_GET,
diff --git a/src/exchange/taler-exchange-httpd_age-withdraw.c 
b/src/exchange/taler-exchange-httpd_age-withdraw.c
index 0978421a..2f30ad8f 100644
--- a/src/exchange/taler-exchange-httpd_age-withdraw.c
+++ b/src/exchange/taler-exchange-httpd_age-withdraw.c
@@ -22,8 +22,13 @@
  * @author Özgür Kesim
  */
 #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"
@@ -31,6 +36,505 @@
 #include "taler-exchange-httpd_responses.h"
 #include "taler-exchange-httpd_keys.h"
 
+
+/**
+ * Context for #age_withdraw_transaction.
+ */
+struct AgeWithdrawContext
+{
+  /**
+   * KYC status for the operation.
+   */
+  struct TALER_EXCHANGEDB_KycStatus kyc;
+
+  /**
+   * Timestamp
+   */
+  struct GNUNET_TIME_Timestamp now;
+
+  /**
+   * Hash of the wire source URL, needed when kyc is needed.
+   */
+  struct TALER_PaytoHashP h_payto;
+
+  /**
+   * The data from the age-withdraw request, as we persist it
+   */
+  struct TALER_EXCHANGEDB_AgeWithdraw commitment;
+
+  /**
+   * Number of coins/denonations in the reveal
+   */
+  uint32_t num_coins;
+
+  /**
+   * kappa * #num_coins hashes of blinded coin planchets.
+   * FIXME[oec]: Make the [][] structure more explicit.
+   */
+  struct TALER_BlindedPlanchet *coin_evs;
+
+  /**
+   * #num_coins hashes of the denominations from which the coins are withdrawn.
+   * Those must support age restriction.
+   */
+  struct TALER_DenominationHashP *denom_hs;
+
+};
+
+/*
+ * @brief Free the resources within a AgeWithdrawContext
+ *
+ * @param awc the context to free
+ */
+static void
+free_age_withdraw_context_resources (struct AgeWithdrawContext *awc)
+{
+  GNUNET_free (awc->denom_hs);
+  GNUNET_free (awc->coin_evs);
+  GNUNET_free (awc->commitment.denom_serials);
+  /*
+   * Note:
+   * awc->commitment.denom_sigs and .h_coin_evs were stack allocated and
+   * .denom_pub_hashes is NULL for this context.
+   */
+}
+
+
+/**
+ * Parse the denominations and blinded coin data of an '/age-withdraw' request.
+ *
+ * @param connection The MHD connection to handle
+ * @param j_denoms_h Array of n hashes of the denominations for the 
withdrawal, in JSON format
+ * @param j_blinded_coin_evs Array of n arrays of kappa blinded envelopes of 
in JSON format for the coins.
+ * @param[out] aws The context of the operation, only partially built at call 
time
+ * @param[out] mhd_ret The result if a reply is queued for MHD
+ * @return true on success, false on failure, with a reply already queued for 
MHD
+ */
+static enum GNUNET_GenericReturnValue
+parse_age_withdraw_json (
+  struct MHD_Connection *connection,
+  const json_t *j_denoms_h,
+  const json_t *j_blinded_coin_evs,
+  struct AgeWithdrawContext *awc,
+  MHD_RESULT *mhd_ret)
+{
+  char buf[256] = {0};
+  const char *error = NULL;
+  unsigned int idx = 0;
+  json_t *value = NULL;
+
+
+  /* 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))
+  {
+    error = "max_age must be the lower edge of an age group";
+    goto EXIT;
+  }
+
+  /* Verify JSON-structure consistency */
+  {
+    uint32_t num_coins = json_array_size (j_denoms_h);
+
+    if (! json_is_array (j_denoms_h))
+      error = "denoms_h must be an array";
+    else if (! json_is_array (j_blinded_coin_evs))
+      error = "coin_evs must be an array";
+    else if (num_coins == 0)
+      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";
+
+    _Static_assert ((TALER_MAX_FRESH_COINS < INT_MAX / TALER_CNC_KAPPA),
+                    "TALER_MAX_FRESH_COINS too large");
+
+    if (NULL != error)
+      goto EXIT;
+
+    awc->num_coins =  num_coins;
+  }
+
+  /* Continue parsing the parts */
+
+  /* Parse denomination keys */
+  awc->denom_hs = GNUNET_new_array (awc->num_coins,
+                                    struct TALER_DenominationHashP);
+
+  json_array_foreach (j_denoms_h, idx, value) {
+    struct GNUNET_JSON_Specification spec[] = {
+      GNUNET_JSON_spec_fixed_auto (NULL, &awc->denom_hs[idx]),
+      GNUNET_JSON_spec_end ()
+    };
+
+    if (GNUNET_OK !=
+        GNUNET_JSON_parse (value, spec, NULL, NULL))
+    {
+      GNUNET_snprintf (buf,
+                       sizeof(buf),
+                       "couldn't parse entry no. %d in array denoms_h",
+                       idx + 1);
+      error = buf;
+      goto EXIT;
+    }
+  };
+
+  /* no overflow because
+   *   num_coins <= TALER_MAX_FRESH_COINS
+   * and
+   *    TALER_MAX_FRESH_COINS * TALER_CNC_KAPPA < INT_MAX
+   */
+  awc->coin_evs = GNUNET_new_array (awc->num_coins * TALER_CNC_KAPPA,
+                                    struct TALER_BlindedPlanchet);
+
+  /* Parse blinded envelopes. */
+  json_array_foreach (j_blinded_coin_evs, idx, value) {
+    const json_t *j_kappa_coin_evs;
+    struct GNUNET_JSON_Specification aspec[] = {
+      GNUNET_JSON_spec_array_const (NULL, &j_kappa_coin_evs),
+      GNUNET_JSON_spec_end ()
+    };
+
+    if (GNUNET_OK !=
+        GNUNET_JSON_parse (value, aspec, NULL, NULL))
+    {
+      GNUNET_snprintf (buf,
+                       sizeof(buf),
+                       "couldn't parse entry no. %d in array coin_evs",
+                       idx + 1);
+      error = buf;
+      goto EXIT;
+    }
+
+    if (TALER_CNC_KAPPA != json_array_size (j_kappa_coin_evs))
+    {
+      GNUNET_snprintf (buf,
+                       sizeof(buf),
+                       "array no. %d in coin_evs not of correct size",
+                       idx + 1);
+      error = buf;
+      goto EXIT;
+    }
+
+    /* Now parse the individual kappa envelopes */
+    {
+      size_t off = idx * TALER_CNC_KAPPA;
+      size_t kappa = 0;
+
+      json_array_foreach (j_kappa_coin_evs, kappa, value) {
+        struct GNUNET_JSON_Specification spec[] = {
+          GNUNET_JSON_spec_fixed_auto (NULL, &awc->coin_evs[off + kappa]),
+          GNUNET_JSON_spec_end ()
+        };
+
+        if (GNUNET_OK !=
+            GNUNET_JSON_parse (value, spec, NULL, NULL))
+        {
+          GNUNET_snprintf (buf,
+                           sizeof(buf),
+                           "couldn't parse array no. %d in coin_evs",
+                           idx + 1);
+          error = buf;
+          goto EXIT;
+        }
+
+        /* Check for duplicate planchets
+         * FIXME: is this needed?
+         */
+        for (unsigned int i = 0; i < off + kappa; i++)
+        {
+          if (0 == TALER_blinded_planchet_cmp (&awc->coin_evs[off + kappa],
+                                               &awc->coin_evs[i]))
+          {
+            error = "duplicate planchet";
+            goto EXIT;
+          }
+        }
+      }
+    }
+  }; /* json_array_foreach over j_blinded_coin_evs */
+
+  /* We successfully parsed denoms_h and blinded_coins_evs */
+  GNUNET_assert (NULL == error);
+
+  /* Finally, calculate the h_commitment from all blinded envelopes */
+  {
+    enum GNUNET_GenericReturnValue ret;
+    struct GNUNET_HashContext *hash_context;
+
+    hash_context = GNUNET_CRYPTO_hash_context_start ();
+
+    for (size_t c = 0;
+         c < TALER_CNC_KAPPA * awc->num_coins;
+         c++)
+    {
+      struct TALER_BlindedCoinHashP bch;
+
+      ret = TALER_coin_ev_hash (&awc->coin_evs[c],
+                                &awc->denom_hs[c],
+                                &bch);
+
+      GNUNET_assert (GNUNET_OK == ret);
+      GNUNET_CRYPTO_hash_context_read (hash_context,
+                                       &bch,
+                                       sizeof(bch));
+    }
+
+    GNUNET_CRYPTO_hash_context_finish (hash_context,
+                                       &awc->commitment.h_commitment.hash);
+  }
+
+
+EXIT:
+  if (NULL != error)
+  {
+    /* Note: resources are freed in caller */
+
+    *mhd_ret = TALER_MHD_reply_with_ec (
+      connection,
+      TALER_EC_GENERIC_PARAMETER_MALFORMED,
+      error);
+    return GNUNET_SYSERR;
+  }
+
+  return GNUNET_OK;
+}
+
+
+/**
+ * Check if the given denomination is still or already valid, has not been
+ * revoked and supports age restriction.
+ *
+ * @param connection HTTP-connection to the client
+ * @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] dk On success, will contain the denomination key details
+ * @param[out] result On failure, an MHD-response will be qeued and result 
will be set to accordingly
+ * @return true on success (denomination valid), false otherwise
+ */
+static bool
+denomination_is_valid (
+  struct MHD_Connection *connection,
+  struct TEH_KeyStateHandle *ksh,
+  const struct TALER_DenominationHashP *denom_h,
+  struct TEH_DenominationKey **pdk,
+  MHD_RESULT *result)
+{
+  struct TEH_DenominationKey *dk;
+  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 */
+    return false;
+  }
+
+  if (GNUNET_TIME_absolute_is_past (dk->meta.expire_withdraw.abs_time))
+  {
+    /* This denomination is past the expiration time for withdraws */
+    /* FIXME[oec]: add idempotency check */
+    *result = TEH_RESPONSE_reply_expired_denom_pub_hash (
+      connection,
+      denom_h,
+      TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED,
+      "age-withdraw_reveal");
+    return false;
+  }
+
+  if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time))
+  {
+    /* This denomination is not yet valid */
+    *result = TEH_RESPONSE_reply_expired_denom_pub_hash (
+      connection,
+      denom_h,
+      TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE,
+      "age-withdraw_reveal");
+    return false;
+  }
+
+  if (dk->recoup_possible)
+  {
+    /* This denomination has been revoked */
+    *result = TALER_MHD_reply_with_ec (
+      connection,
+      TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED,
+      NULL);
+    return false;
+  }
+
+  if (0 == dk->denom_pub.age_mask.bits)
+  {
+    /* This denomation does not support age restriction */
+    char msg[256] = {0};
+    GNUNET_snprintf (msg,
+                     sizeof(msg),
+                     "denomination %s does not support age restriction",
+                     GNUNET_h2s (&denom_h->hash));
+
+    *result = TALER_MHD_reply_with_ec (
+      connection,
+      TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN,
+      msg);
+    return false;
+  }
+
+  *pdk = dk;
+  return true;
+}
+
+
+/**
+ * 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 connection The HTTP connection to the client
+ * @param len The lengths of the array @a denoms_h
+ * @param denoms_h array of hashes of denomination public keys
+ * @param coin_evs array of blinded coin planchets
+ * @param[out] denom_serials On success, will be filled with the serial-id's 
of the denomination keys.  Caller must deallocate.
+ * @param[out] amount_with_fee On succes, will contain the committed amount 
including fees
+ * @param[out] result In the error cases, a response will be queued with MHD 
and this will be the result.
+ * @return GNUNET_OK if the denominations are valid and support age-restriction
+ *   GNUNET_SYSERR otherwise
+ */
+static enum GNUNET_GenericReturnValue
+are_denominations_valid (
+  struct MHD_Connection *connection,
+  uint32_t len,
+  const struct TALER_DenominationHashP *denom_hs,
+  const struct TALER_BlindedPlanchet *coin_evs,
+  uint64_t **denom_serials,
+  struct TALER_Amount *amount_with_fee,
+  MHD_RESULT *result)
+{
+  struct TALER_Amount total_amount;
+  struct TALER_Amount total_fee;
+  struct TEH_KeyStateHandle *ksh;
+  uint64_t *serials;
+
+  ksh = TEH_keys_get_state ();
+  if (NULL == ksh)
+  {
+    *result = TALER_MHD_reply_with_ec (connection,
+                                       TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
+                                       NULL);
+    return GNUNET_SYSERR;
+  }
+
+  *denom_serials =
+    serials = GNUNET_new_array (len, uint64_t);
+
+  TALER_amount_set_zero (TEH_currency, &total_amount);
+  TALER_amount_set_zero (TEH_currency, &total_fee);
+
+  for (uint32_t i = 0; i < len; i++)
+  {
+    struct TEH_DenominationKey *dk;
+    if (! denomination_is_valid (connection,
+                                 ksh,
+                                 &denom_hs[i],
+                                 &dk,
+                                 result))
+      /* FIXME[oec]: add idempotency check */
+      return GNUNET_SYSERR;
+
+    /* Ensure the ciphers from the planchets match the denominations' */
+    if (dk->denom_pub.cipher != coin_evs[i].cipher)
+    {
+      GNUNET_break_op (0);
+      *result = TALER_MHD_reply_with_ec (connection,
+                                         
TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH,
+                                         NULL);
+      return GNUNET_SYSERR;
+    }
+
+    /* Accumulate the values */
+    if (0 > TALER_amount_add (&total_amount,
+                              &total_amount,
+                              &dk->meta.value))
+    {
+      GNUNET_break_op (0);
+      *result = TALER_MHD_reply_with_error (connection,
+                                            MHD_HTTP_BAD_REQUEST,
+                                            
TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW,
+                                            "amount");
+      return GNUNET_SYSERR;
+    }
+
+    /* Accumulate the withdraw fees */
+    if (0 > TALER_amount_add (&total_fee,
+                              &total_fee,
+                              &dk->meta.fees.withdraw))
+    {
+      GNUNET_break_op (0);
+      *result = TALER_MHD_reply_with_error (connection,
+                                            MHD_HTTP_BAD_REQUEST,
+                                            
TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW,
+                                            "fee");
+      return GNUNET_SYSERR;
+    }
+
+    serials[i] = dk->meta.serial;
+  }
+
+  /* Save the total amount including fees */
+  GNUNET_assert (0 < TALER_amount_add (amount_with_fee,
+                                       &total_amount,
+                                       &total_fee));
+
+  return GNUNET_OK;
+}
+
+
+/**
+ * @brief Verify the signature of the request body with the reserve key
+ *
+ * @param connection the connection to the client
+ * @param commitment the age withdraw commitment
+ * @param mhd_ret the response to fill in the error case
+ * @return GNUNET_OK on success
+ */
+static enum GNUNET_GenericReturnValue
+verify_reserve_signature (
+  struct MHD_Connection *connection,
+  const struct TALER_EXCHANGEDB_AgeWithdraw *commitment,
+  enum MHD_Result *mhd_ret
+  )
+{
+
+  TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++;
+  if (GNUNET_OK !=
+      TALER_wallet_age_withdraw_verify (&commitment->h_commitment,
+                                        &commitment->amount_with_fee,
+                                        &TEH_age_restriction_config.mask,
+                                        commitment->max_age,
+                                        &commitment->reserve_pub,
+                                        &commitment->reserve_sig))
+  {
+    GNUNET_break_op (0);
+    *mhd_ret = TALER_MHD_reply_with_ec (connection,
+                                        
TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID,
+                                        NULL);
+    return GNUNET_SYSERR;
+  }
+
+  return GNUNET_OK;
+}
+
+
 /**
  * Send a response to a "age-withdraw" request.
  *
@@ -72,46 +576,62 @@ reply_age_withdraw_success (
 
 
 /**
- * Context for #age_withdraw_transaction.
+ * Check if the request is replayed and we already have an
+ * answer. If so, replay the existing answer and return the
+ * HTTP response.
+ *
+ * @param con connection to the client
+ * @param[in,out] awc parsed request data
+ * @param[out] mret HTTP status, set if we return true
+ * @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
  */
-struct AgeWithdrawContext
+static bool
+request_is_idempotent (struct MHD_Connection *con,
+                       struct AgeWithdrawContext *awc,
+                       MHD_RESULT *mret)
 {
-  /**
-   * KYC status for the operation.
-   */
-  struct TALER_EXCHANGEDB_KycStatus kyc;
+  enum GNUNET_DB_QueryStatus qs;
+  struct TALER_EXCHANGEDB_AgeWithdraw commitment;
 
-  /**
-   * Hash of the wire source URL, needed when kyc is needed.
-   */
-  struct TALER_PaytoHashP h_payto;
+  qs = TEH_plugin->get_age_withdraw (TEH_plugin->cls,
+                                     &awc->commitment.reserve_pub,
+                                     &awc->commitment.h_commitment,
+                                     &commitment);
+  if (0 > qs)
+  {
+    GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
+    if (GNUNET_DB_STATUS_HARD_ERROR == qs)
+      *mret = TALER_MHD_reply_with_ec (con,
+                                       TALER_EC_GENERIC_DB_FETCH_FAILED,
+                                       "get_age_withdraw");
+    return true; /* Well, kind-of.  At least we have set mret. */
+  }
 
-  /**
-   * The data from the age-withdraw request
-   */
-  struct TALER_EXCHANGEDB_AgeWithdrawCommitment commitment;
+  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+    return false;
 
-  /**
-   * Current time for the DB transaction.
-   */
-  struct GNUNET_TIME_Timestamp now;
-};
+  /* Generate idempotent reply */
+  TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_AGE_WITHDRAW]++;
+  *mret = reply_age_withdraw_success (con,
+                                      &commitment.h_commitment,
+                                      commitment.noreveal_index);
+  return true;
+}
 
 
 /**
- * Function called to iterate over KYC-relevant
- * transaction amounts for a particular time range.
- * Called within a database transaction, so must
+ * 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
+ * @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
  */
 static void
 age_withdraw_amount_cb (void *cls,
@@ -151,9 +671,6 @@ age_withdraw_amount_cb (void *cls,
  * IF it returns the soft error code, the function MAY be called again
  * to retry and MUST not queue a MHD response.
  *
- * Note that "awc->commitment.sig" is set before entering this function as we
- * signed before entering the transaction.
- *
  * @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,
@@ -167,20 +684,17 @@ age_withdraw_transaction (void *cls,
 {
   struct AgeWithdrawContext *awc = cls;
   enum GNUNET_DB_QueryStatus qs;
-  bool found = false;
-  bool balance_ok = false;
-  uint64_t ruuid;
 
-  awc->now = GNUNET_TIME_timestamp_get ();
   qs = TEH_plugin->reserves_get_origin (TEH_plugin->cls,
                                         &awc->commitment.reserve_pub,
                                         &awc->h_payto);
   if (qs < 0)
     return qs;
 
-  /* If no results, reserve was created by merge,
+  /* 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_ONE_RESULT == qs)
   {
     char *kyc_required;
@@ -199,17 +713,16 @@ age_withdraw_transaction (void *cls,
       if (GNUNET_DB_STATUS_HARD_ERROR == qs)
       {
         GNUNET_break (0);
-        *mhd_ret = TALER_MHD_reply_with_error (connection,
-                                               MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                               
TALER_EC_GENERIC_DB_FETCH_FAILED,
-                                               "kyc_test_required");
+        *mhd_ret = TALER_MHD_reply_with_ec (connection,
+                                            TALER_EC_GENERIC_DB_FETCH_FAILED,
+                                            "kyc_test_required");
       }
       return qs;
     }
 
     if (NULL != kyc_required)
     {
-      /* insert KYC requirement into DB! */
+      /* Mark result and return by inserting KYC requirement into DB! */
       awc->kyc.ok = false;
       return TEH_plugin->insert_kyc_requirement_for_account (
         TEH_plugin->cls,
@@ -220,37 +733,75 @@ age_withdraw_transaction (void *cls,
   }
 
   awc->kyc.ok = true;
-  qs = TEH_plugin->do_age_withdraw (TEH_plugin->cls,
-                                    &awc->commitment,
-                                    &found,
-                                    &balance_ok,
-                                    &ruuid);
-  if (0 > qs)
-  {
-    if (GNUNET_DB_STATUS_HARD_ERROR == qs)
-      *mhd_ret = TALER_MHD_reply_with_error (connection,
-                                             MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                             TALER_EC_GENERIC_DB_FETCH_FAILED,
-                                             "do_age_withdraw");
-    return qs;
-  }
-  else if (! found)
-  {
-    *mhd_ret = TALER_MHD_reply_with_error (connection,
-                                           MHD_HTTP_NOT_FOUND,
-                                           
TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN,
-                                           NULL);
-    return GNUNET_DB_STATUS_HARD_ERROR;
-  }
-  else if (! balance_ok)
+
+  /* KYC requirement fulfilled, do the age-withdraw transaction */
   {
-    TEH_plugin->rollback (TEH_plugin->cls);
-    *mhd_ret = TEH_RESPONSE_reply_reserve_insufficient_balance (
-      connection,
-      TALER_EC_EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS,
-      &awc->commitment.amount_with_fee,
-      &awc->commitment.reserve_pub);
-    return GNUNET_DB_STATUS_HARD_ERROR;
+    bool found = false;
+    bool balance_ok = false;
+    bool age_ok = false;
+    bool conflict = false;
+    uint16_t allowed_maximum_age = 0;
+
+    qs = TEH_plugin->do_age_withdraw (TEH_plugin->cls,
+                                      &awc->commitment,
+                                      awc->now,
+                                      &found,
+                                      &balance_ok,
+                                      &age_ok,
+                                      &allowed_maximum_age,
+                                      &conflict);
+    if (0 > qs)
+    {
+      if (GNUNET_DB_STATUS_HARD_ERROR == qs)
+        *mhd_ret = TALER_MHD_reply_with_ec (connection,
+                                            TALER_EC_GENERIC_DB_FETCH_FAILED,
+                                            "do_age_withdraw");
+      return qs;
+    }
+    else if (! found)
+    {
+      *mhd_ret = TALER_MHD_reply_with_ec (connection,
+                                          
TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN,
+                                          NULL);
+      return GNUNET_DB_STATUS_HARD_ERROR;
+    }
+    else if (! balance_ok)
+    {
+      TEH_plugin->rollback (TEH_plugin->cls);
+
+      *mhd_ret = TEH_RESPONSE_reply_reserve_insufficient_balance (
+        connection,
+        TALER_EC_EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS,
+        &awc->commitment.amount_with_fee,
+        &awc->commitment.reserve_pub);
+
+      return GNUNET_DB_STATUS_HARD_ERROR;
+    }
+    else if (! age_ok)
+    {
+      enum TALER_ErrorCode ec =
+        TALER_EC_EXCHANGE_AGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE;
+
+      *mhd_ret =
+        TALER_MHD_REPLY_JSON_PACK (
+          connection,
+          TALER_ErrorCode_get_http_status_safe (ec),
+          TALER_MHD_PACK_EC (ec),
+          GNUNET_JSON_pack_uint64 ("allowed_maximum_age",
+                                   allowed_maximum_age));
+
+      return GNUNET_DB_STATUS_HARD_ERROR;
+    }
+    else if (conflict)
+    {
+      /* do_age_withdraw signaled a conflict, so there MUST be an entry
+       * in the DB.  Put that into the response */
+      bool ok = request_is_idempotent (connection,
+                                       awc,
+                                       mhd_ret);
+      GNUNET_assert (ok);
+      return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
+    }
   }
 
   if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
@@ -260,48 +811,107 @@ age_withdraw_transaction (void *cls,
 
 
 /**
- * Check if the @a rc is replayed and we already have an
- * answer. If so, replay the existing answer and return the
- * HTTP response.
+ * @brief Sign the chosen blinded coins, debit the reserve and persist
+ * the commitment.
  *
- * @param rc request context
- * @param[in,out] awc parsed request data
- * @param[out] mret HTTP status, set if we return true
- * @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
+ * 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.
+ *
+ * Note that on success, there are two possible states:
+ *  1.) KYC is required (awc.kyc.ok == false) or
+ *  2.) age withdraw was successful.
+ *
+ * @param connection HTTP-connection to the client
+ * @param h_commitment Original commitment
+ * @param num_coins Number of coins
+ * @param coin_evs The Hashes of the undisclosed, blinded coins, @a num_coins 
many
+ * @param denom_keys The array of denomination keys, @a num_coins. Needed to 
detect Clause-Schnorr-based denominations
+ * @param[out] result On error, a HTTP-response will be queued and result set 
accordingly
+ * @return GNUNET_OK on success, GNUNET_SYSERR otherwise
  */
-static bool
-request_is_idempotent (struct TEH_RequestContext *rc,
-                       struct AgeWithdrawContext *awc,
-                       MHD_RESULT *mret)
+static enum GNUNET_GenericReturnValue
+sign_and_do_age_withdraw (
+  struct MHD_Connection *connection,
+  struct AgeWithdrawContext *awc,
+  MHD_RESULT *result)
 {
-  enum GNUNET_DB_QueryStatus qs;
-  struct TALER_EXCHANGEDB_AgeWithdrawCommitment commitment;
+  enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
+  struct TALER_BlindedCoinHashP h_coin_evs[awc->num_coins];
+  struct TALER_BlindedDenominationSignature denom_sigs[awc->num_coins];
+  uint8_t noreveal_index;
 
-  qs = TEH_plugin->get_age_withdraw_info (TEH_plugin->cls,
-                                          &awc->commitment.reserve_pub,
-                                          &awc->commitment.h_commitment,
-                                          &commitment);
-  if (0 > qs)
+  awc->now = GNUNET_TIME_timestamp_get ();
+
+  /* Pick the challenge */
+  awc->commitment.noreveal_index =
+    noreveal_index =
+      GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_STRONG,
+                                TALER_CNC_KAPPA);
+
+  /* Choose and sign the coins */
   {
-    GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
-    if (GNUNET_DB_STATUS_HARD_ERROR == qs)
-      *mret = TALER_MHD_reply_with_error (rc->connection,
-                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                          TALER_EC_GENERIC_DB_FETCH_FAILED,
-                                          "get_age_withdraw_info");
-    return true; /* well, kind-of */
+    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++)
+    {
+      csds[i].bp = &awc->coin_evs[TALER_CNC_KAPPA * i + noreveal_index];
+      csds[i].h_denom_pub = &awc->denom_hs[i];
+    }
+
+    ec = TEH_keys_denomination_batch_sign (csds,
+                                           awc->num_coins,
+                                           false,
+                                           denom_sigs);
+    if (TALER_EC_NONE != ec)
+    {
+      GNUNET_break (0);
+      *result = TALER_MHD_reply_with_ec (connection,
+                                         ec,
+                                         NULL);
+      return GNUNET_SYSERR;
+    }
   }
 
-  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
-    return false;
+  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+              "Signatures ready, starting DB interaction\n");
 
-  /* generate idempotent reply */
-  TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_AGE_WITHDRAW]++;
-  *mret = reply_age_withdraw_success (rc->connection,
-                                      &commitment.h_commitment,
-                                      commitment.noreveal_index);
-  return true;
+  /* 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],
+                        &awc->denom_hs[i],
+                        &h_coin_evs[i]);
+  }
+
+  /* Run the transaction */
+  awc->commitment.h_coin_evs = h_coin_evs;
+  awc->commitment.denom_sigs = denom_sigs;
+  ret = TEH_DB_run_transaction (connection,
+                                "run age withdraw",
+                                TEH_MT_REQUEST_AGE_WITHDRAW,
+                                result,
+                                &age_withdraw_transaction,
+                                awc);
+
+  if (GNUNET_OK != ret)
+  {
+    GNUNET_break (0);
+    *result = TALER_MHD_reply_with_error (connection,
+                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
+                                          
TALER_EC_GENERIC_UNEXPECTED_REQUEST_ERROR,
+                                          NULL);
+  }
+
+  /* Free resources */
+  awc->commitment.h_coin_evs = NULL;
+  awc->commitment.denom_sigs = NULL;
+  for (unsigned int i = 0; i<awc->num_coins; i++)
+    TALER_blinded_denom_sig_free (&denom_sigs[i]);
+  return ret;
 }
 
 
@@ -311,17 +921,18 @@ TEH_handler_age_withdraw (struct TEH_RequestContext *rc,
                           const json_t *root)
 {
   MHD_RESULT mhd_ret;
+  const json_t *j_denoms_h;
+  const json_t *j_blinded_coins_evs;
   struct AgeWithdrawContext awc = {0};
   struct GNUNET_JSON_Specification spec[] = {
-    GNUNET_JSON_spec_fixed_auto ("reserve_sig",
-                                 &awc.commitment.reserve_sig),
-    GNUNET_JSON_spec_fixed_auto ("h_commitment",
-                                 &awc.commitment.h_commitment),
-    TALER_JSON_spec_amount ("amount",
-                            TEH_currency,
-                            &awc.commitment.amount_with_fee),
+    GNUNET_JSON_spec_array_const ("denoms_h",
+                                  &j_denoms_h),
+    GNUNET_JSON_spec_array_const ("blinded_coins_evs",
+                                  &j_blinded_coins_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 ()
   };
 
@@ -340,53 +951,71 @@ TEH_handler_age_withdraw (struct TEH_RequestContext *rc,
   }
 
   do {
-    /* If request was made before successfully, return the previous answer */
-    if (request_is_idempotent (rc,
-                               &awc,
-                               &mhd_ret))
+    /* Note: If we break the statement here at any point,
+     * a response to the client MUST have been populated
+     * with an appropriate answer and mhd_ret MUST have
+     * been set accordingly.
+     */
+
+    /* Parse denoms_h and blinded_coins_evs, partially fill awc */
+    if (GNUNET_OK !=
+        parse_age_withdraw_json (rc->connection,
+                                 j_denoms_h,
+                                 j_blinded_coins_evs,
+                                 &awc,
+                                 &mhd_ret))
       break;
 
-    /* Verify the signature of the request body with the reserve key */
-    TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++;
+    /* Ensure validity of denoms and calculate amounts and fees */
     if (GNUNET_OK !=
-        TALER_wallet_age_withdraw_verify (&awc.commitment.h_commitment,
-                                          &awc.commitment.amount_with_fee,
-                                          awc.commitment.max_age,
-                                          &awc.commitment.reserve_pub,
-                                          &awc.commitment.reserve_sig))
-    {
-      GNUNET_break_op (0);
-      mhd_ret = TALER_MHD_reply_with_error (rc->connection,
-                                            MHD_HTTP_FORBIDDEN,
-                                            
TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID,
-                                            NULL);
+        are_denominations_valid (rc->connection,
+                                 awc.num_coins,
+                                 awc.denom_hs,
+                                 awc.coin_evs,
+                                 &awc.commitment.denom_serials,
+                                 &awc.commitment.amount_with_fee,
+                                 &mhd_ret))
       break;
-    }
 
-    /* Run the transaction */
+    /* Now that amount_with_fee is calculated, verify the signature of
+     * the request body with the reserve key.
+     */
     if (GNUNET_OK !=
-        TEH_DB_run_transaction (rc->connection,
-                                "run age withdraw",
-                                TEH_MT_REQUEST_AGE_WITHDRAW,
-                                &mhd_ret,
-                                &age_withdraw_transaction,
-                                &awc))
+        verify_reserve_signature (rc->connection,
+                                  &awc.commitment,
+                                  &mhd_ret))
       break;
 
-    /* Clean up and send back final response */
-    GNUNET_JSON_parse_free (spec);
+    /* Sign the chosen blinded coins, persist the commitment and
+     * charge the reserve.
+     * On error (like, insufficient funds), the client is notified.
+     * On conflict, the noreveal_index from the previous, existing
+     * commitment is returned to the client, returning success.
+     * Note that on success, there are two possible states:
+     *    KYC is required (awc.kyc.ok == false) or
+     *    age withdraw was successful.
+     */
+    if (GNUNET_OK !=
+        sign_and_do_age_withdraw (rc->connection,
+                                  &awc,
+                                  &mhd_ret))
+      break;
 
+    /* Send back final response, depending on the outcome of
+     * the DB-transaction */
     if (! awc.kyc.ok)
-      return TEH_RESPONSE_reply_kyc_required (rc->connection,
-                                              &awc.h_payto,
-                                              &awc.kyc);
+      mhd_ret = TEH_RESPONSE_reply_kyc_required (rc->connection,
+                                                 &awc.h_payto,
+                                                 &awc.kyc);
+    else
+      mhd_ret = reply_age_withdraw_success (rc->connection,
+                                            &awc.commitment.h_commitment,
+                                            awc.commitment.noreveal_index);
 
-    return reply_age_withdraw_success (rc->connection,
-                                       &awc.commitment.h_commitment,
-                                       awc.commitment.noreveal_index);
   } while(0);
 
   GNUNET_JSON_parse_free (spec);
+  free_age_withdraw_context_resources (&awc);
   return mhd_ret;
 
 }
diff --git a/src/exchange/taler-exchange-httpd_age-withdraw_reveal.c 
b/src/exchange/taler-exchange-httpd_age-withdraw_reveal.c
index d604632d..60f036f8 100644
--- a/src/exchange/taler-exchange-httpd_age-withdraw_reveal.c
+++ b/src/exchange/taler-exchange-httpd_age-withdraw_reveal.c
@@ -19,10 +19,12 @@
  * @author Özgür Kesim
  */
 #include "platform.h"
+#include <gnunet/gnunet_common.h>
 #include <gnunet/gnunet_util_lib.h>
 #include <jansson.h>
 #include <microhttpd.h>
 #include "taler-exchange-httpd_metrics.h"
+#include "taler_error_codes.h"
 #include "taler_exchangedb_plugin.h"
 #include "taler_mhd_lib.h"
 #include "taler-exchange-httpd_mhd.h"
@@ -50,94 +52,29 @@ struct AgeRevealContext
   struct TALER_ReservePublicKeyP reserve_pub;
 
   /**
-   * Number of coins/denonations in the reveal
+   * Number of coins to reveal.  MUST be equal to
+   * @e num_secrets/(kappa -1).
    */
   uint32_t num_coins;
 
   /**
-   * #num_coins hashes of the denominations from which the coins are withdrawn.
-   * Those must support age restriction.
+   * Number of secrets in the reveal.  MUST be a multiple of (kappa-1).
    */
-  struct TALER_DenominationHashP *denoms_h;
+  uint32_t num_secrets;
 
   /**
-   * #num_coins denomination keys, found in the system, according to denoms_h;
-   */
-  struct TEH_DenominationKey *denom_keys;
-
-  /**
-   * Total sum of all denominations' values
-   **/
-  struct TALER_Amount total_amount;
-
-  /**
-   * Total sum of all denominations' fees
-   */
-  struct TALER_Amount total_fee;
-
-  /**
-   * #num_coins hashes of blinded coin planchets.
-   */
-  struct TALER_BlindedPlanchet *coin_evs;
-
-  /**
-   * secrets for #num_coins*(kappa - 1) disclosed coins.
+   * @e num_secrets secrets for  disclosed coins.
    */
   struct TALER_PlanchetMasterSecretP *disclosed_coin_secrets;
 
   /**
    * The data from the original age-withdraw.  Will be retrieved from
-   * the DB via @a ach.
+   * the DB via @a ach and @a reserve_pub.
    */
-  struct TALER_EXCHANGEDB_AgeWithdrawCommitment commitment;
+  struct TALER_EXCHANGEDB_AgeWithdraw *commitment;
 };
 
 
-/**
- * Information per planchet in the batch.
- */
-struct PlanchetContext
-{
-
-  /**
-   * Hash of the (blinded) message to be signed by the Exchange.
-   */
-  struct TALER_BlindedCoinHashP h_coin_envelope;
-
-  /**
-   * 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;
-
-};
-
-/**
- * Helper function to free resources in the context
- */
-void
-age_reveal_context_free (struct AgeRevealContext *actx)
-{
-  GNUNET_free (actx->denoms_h);
-  GNUNET_free (actx->denom_keys);
-  GNUNET_free (actx->coin_evs);
-  GNUNET_free (actx->disclosed_coin_secrets);
-}
-
-
 /**
  * Parse the json body of an '/age-withdraw/$ACH/reveal' request.  It extracts
  * the denomination hashes, blinded coins and disclosed coins and allocates
@@ -154,50 +91,40 @@ age_reveal_context_free (struct AgeRevealContext *actx)
 static enum GNUNET_GenericReturnValue
 parse_age_withdraw_reveal_json (
   struct MHD_Connection *connection,
-  const json_t *j_denoms_h,
-  const json_t *j_coin_evs,
   const json_t *j_disclosed_coin_secrets,
   struct AgeRevealContext *actx,
   MHD_RESULT *mhd_ret)
 {
   enum GNUNET_GenericReturnValue result = GNUNET_SYSERR;
+  size_t num_entries;
 
   /* Verify JSON-structure consistency */
   {
     const char *error = NULL;
 
-    actx->num_coins = json_array_size (j_denoms_h); /* 0, if j_denoms_h is not 
an array */
+    num_entries = json_array_size (j_disclosed_coin_secrets); /* 0, if not an 
array */
 
-    if (! json_is_array (j_denoms_h))
-      error = "denoms_h must be an array";
-    else if (! json_is_array (j_coin_evs))
-      error = "coin_evs must be an array";
-    else if (! json_is_array (j_disclosed_coin_secrets))
+    if (! json_is_array (j_disclosed_coin_secrets))
       error = "disclosed_coin_secrets must be an array";
-    else if (actx->num_coins == 0)
-      error = "denoms_h must not be empty";
-    else if (actx->num_coins != json_array_size (j_coin_evs))
-      error = "denoms_h and coins_evs must be arrays of the same size";
-    else if (actx->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!
-       **/
+    else if (num_entries == 0)
+      error = "disclosed_coin_secrets must not be empty";
+    else if (num_entries > TALER_MAX_FRESH_COINS * (TALER_CNC_KAPPA - 1))
       error = "maximum number of coins that can be withdrawn has been 
exceeded";
-    else if (actx->num_coins * (TALER_CNC_KAPPA - 1)
-             != json_array_size (j_disclosed_coin_secrets))
-      error = "the size of array disclosed_coin_secrets must be "
-              TALER_CNC_KAPPA_MINUS_ONE_STR " times the size of denoms_h";
+    else if (0 != num_entries % (TALER_CNC_KAPPA - 1))
+      error = "the size of disclosed_coin_secrets must be a multiple of "
+              TALER_CNC_KAPPA_MINUS_ONE_STR;
 
     if (NULL != error)
     {
-      *mhd_ret = TALER_MHD_reply_with_error (connection,
-                                             MHD_HTTP_BAD_REQUEST,
-                                             
TALER_EC_GENERIC_PARAMETER_MALFORMED,
-                                             error);
+      *mhd_ret = TALER_MHD_reply_with_ec (connection,
+                                          TALER_EC_GENERIC_PARAMETER_MALFORMED,
+                                          error);
       return GNUNET_SYSERR;
     }
+
+    actx->num_secrets = num_entries;
+    actx->num_coins = num_entries / (TALER_CNC_KAPPA - 1);
+
   }
 
   /* Continue parsing the parts */
@@ -205,78 +132,10 @@ parse_age_withdraw_reveal_json (
     unsigned int idx = 0;
     json_t *value = NULL;
 
-    /* Parse denomination keys */
-    actx->denoms_h = GNUNET_new_array (actx->num_coins,
-                                       struct TALER_DenominationHashP);
-
-    json_array_foreach (j_denoms_h, idx, value) {
-      struct GNUNET_JSON_Specification spec[] = {
-        GNUNET_JSON_spec_fixed_auto (NULL, &actx->denoms_h[idx]),
-        GNUNET_JSON_spec_end ()
-      };
-
-      if (GNUNET_OK !=
-          GNUNET_JSON_parse (value, spec, NULL, NULL))
-      {
-        char msg[256] = {0};
-        GNUNET_snprintf (msg,
-                         sizeof(msg),
-                         "couldn't parse entry no. %d in array denoms_h",
-                         idx + 1);
-        *mhd_ret = TALER_MHD_reply_with_error (connection,
-                                               MHD_HTTP_BAD_REQUEST,
-                                               
TALER_EC_GENERIC_PARAMETER_MALFORMED,
-                                               msg);
-        goto EXIT;
-      }
-    };
-
-    /* Parse blinded envelopes */
-    actx->coin_evs = GNUNET_new_array (actx->num_coins,
-                                       struct TALER_BlindedPlanchet);
-
-    json_array_foreach (j_coin_evs, idx, value) {
-      struct GNUNET_JSON_Specification spec[] = {
-        TALER_JSON_spec_blinded_planchet (NULL, &actx->coin_evs[idx]),
-        GNUNET_JSON_spec_end ()
-      };
-
-      if (GNUNET_OK !=
-          GNUNET_JSON_parse (value, spec, NULL, NULL))
-      {
-        char msg[256] = {0};
-        GNUNET_snprintf (msg,
-                         sizeof(msg),
-                         "couldn't parse entry no. %d in array coin_evs",
-                         idx + 1);
-        *mhd_ret = TALER_MHD_reply_with_error (connection,
-                                               MHD_HTTP_BAD_REQUEST,
-                                               
TALER_EC_GENERIC_PARAMETER_MALFORMED,
-                                               msg);
-        goto EXIT;
-      }
-
-      /* Check for duplicate planchets */
-      for (unsigned int i = 0; i < idx; i++)
-      {
-        if (0 == TALER_blinded_planchet_cmp (&actx->coin_evs[idx],
-                                             &actx->coin_evs[i]))
-        {
-          GNUNET_break_op (0);
-          *mhd_ret = TALER_MHD_reply_with_error (connection,
-                                                 MHD_HTTP_BAD_REQUEST,
-                                                 
TALER_EC_GENERIC_PARAMETER_MALFORMED,
-                                                 "duplicate planchet");
-          goto EXIT;
-        }
-
-      }
-    };
-
     /* Parse diclosed keys */
-    actx->disclosed_coin_secrets = GNUNET_new_array (
-      actx->num_coins * (TALER_CNC_KAPPA - 1),
-      struct TALER_PlanchetMasterSecretP);
+    actx->disclosed_coin_secrets =
+      GNUNET_new_array (num_entries,
+                        struct TALER_PlanchetMasterSecretP);
 
     json_array_foreach (j_disclosed_coin_secrets, idx, value) {
       struct GNUNET_JSON_Specification spec[] = {
@@ -292,18 +151,15 @@ parse_age_withdraw_reveal_json (
                          sizeof(msg),
                          "couldn't parse entry no. %d in array 
disclosed_coin_secrets",
                          idx + 1);
-        *mhd_ret = TALER_MHD_reply_with_error (connection,
-                                               MHD_HTTP_BAD_REQUEST,
-                                               
TALER_EC_GENERIC_PARAMETER_MALFORMED,
-                                               msg);
+        *mhd_ret = TALER_MHD_reply_with_ec (connection,
+                                            
TALER_EC_GENERIC_PARAMETER_MALFORMED,
+                                            msg);
         goto EXIT;
       }
     };
   }
 
   result = GNUNET_OK;
-  *mhd_ret = MHD_YES;
-
 
 EXIT:
   return result;
@@ -328,256 +184,181 @@ find_original_commitment (
   struct MHD_Connection *connection,
   const struct TALER_AgeWithdrawCommitmentHashP *h_commitment,
   const struct TALER_ReservePublicKeyP *reserve_pub,
-  struct TALER_EXCHANGEDB_AgeWithdrawCommitment *commitment,
+  struct TALER_EXCHANGEDB_AgeWithdraw **commitment,
   MHD_RESULT *result)
 {
   enum GNUNET_DB_QueryStatus qs;
 
-  qs = TEH_plugin->get_age_withdraw_info (TEH_plugin->cls,
-                                          reserve_pub,
-                                          h_commitment,
-                                          commitment);
-  switch (qs)
+  for (unsigned int try = 0; try < 3; try++)
   {
-  case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
-    return GNUNET_OK; /* Only happy case */
+    qs = TEH_plugin->get_age_withdraw (TEH_plugin->cls,
+                                       reserve_pub,
+                                       h_commitment,
+                                       *commitment);
+    switch (qs)
+    {
+    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+      return GNUNET_OK; /* Only happy case */
 
-  case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
-    *result = TALER_MHD_reply_with_error (connection,
-                                          MHD_HTTP_NOT_FOUND,
-                                          
TALER_EC_EXCHANGE_AGE_WITHDRAW_COMMITMENT_UNKNOWN,
-                                          NULL);
-    break;
+    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+      *result = TALER_MHD_reply_with_error (connection,
+                                            MHD_HTTP_NOT_FOUND,
+                                            
TALER_EC_EXCHANGE_AGE_WITHDRAW_COMMITMENT_UNKNOWN,
+                                            NULL);
+      return GNUNET_SYSERR;
 
-  case GNUNET_DB_STATUS_HARD_ERROR:
-    *result = TALER_MHD_reply_with_error (connection,
-                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                          TALER_EC_GENERIC_DB_FETCH_FAILED,
-                                          "get_age_withdraw_info");
-    break;
-
-  case GNUNET_DB_STATUS_SOFT_ERROR:
-  /* FIXME oec: Do we queue a result in this case or retry? */
-  default:
-    GNUNET_break (0);
-    *result = TALER_MHD_reply_with_error (connection,
-                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                          
TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
-                                          NULL);
-  }
+    case GNUNET_DB_STATUS_HARD_ERROR:
+      *result = TALER_MHD_reply_with_ec (connection,
+                                         TALER_EC_GENERIC_DB_FETCH_FAILED,
+                                         "get_age_withdraw_info");
+      return GNUNET_SYSERR;
 
+    case GNUNET_DB_STATUS_SOFT_ERROR:
+      break; /* try again */
+    default:
+      GNUNET_break (0);
+      *result = TALER_MHD_reply_with_ec (connection,
+                                         
TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
+                                         NULL);
+      return GNUNET_SYSERR;
+    }
+  }
+  /* after unsuccessfull retries*/
+  *result = TALER_MHD_reply_with_ec (connection,
+                                     TALER_EC_GENERIC_DB_FETCH_FAILED,
+                                     "get_age_withdraw_info");
   return GNUNET_SYSERR;
 }
 
 
 /**
- * Check if the given denomination is still or already valid, has not been
- * revoked and supports age restriction.
+ * @brief Derives a age-restricted planchet from a given secret and calculates 
the hash
  *
- * @param connection HTTP-connection to the client
- * @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] dks On success, will contain the denomination key details
- * @param[out] result On failure, an MHD-response will be qeued and result 
will be set to accordingly
- * @return true on success (denomination valid), false otherwise
+ * @param connection Connection to the client
+ * @param ksh The denomination keys in memory
+ * @param secret The secret to a planchet
+ * @param denom_pub_h The hash of the denomination for the planchet
+ * @param max_age The maximum age allowed
+ * @param[out] hc Hashcode to write
+ * @param[out] result On error, a HTTP-response will be queued and result set 
accordingly
+ * @return GNUNET_OK on success, GNUNET_SYSERR otherwise, with an error message
+ * written to the client and @e result set.
  */
-static bool
-denomination_is_valid (
+static enum GNUNET_GenericReturnValue
+calculate_blinded_hash (
   struct MHD_Connection *connection,
-  struct TEH_KeyStateHandle *ksh,
-  const struct TALER_DenominationHashP *denom_h,
-  struct TEH_DenominationKey *dks,
+  const struct TEH_KeyStateHandle *keys,
+  const struct TALER_PlanchetMasterSecretP *secret,
+  const struct TALER_DenominationHashP *denom_pub_h,
+  uint8_t max_age,
+  struct TALER_BlindedCoinHashP *bch,
   MHD_RESULT *result)
 {
-  dks = TEH_keys_denomination_by_hash2 (ksh,
-                                        denom_h,
-                                        connection,
-                                        result);
-  if (NULL == dks)
-  {
-    /* The denomination doesn't exist */
-    GNUNET_assert (result != NULL);
-    /* Note: a HTTP-response has been queued and result has been set by
-     * TEH_keys_denominations_by_hash2 */
-    return false;
-  }
-
-  if (GNUNET_TIME_absolute_is_past (dks->meta.expire_withdraw.abs_time))
-  {
-    /* This denomination is past the expiration time for withdraws */
-    *result = TEH_RESPONSE_reply_expired_denom_pub_hash (
-      connection,
-      denom_h,
-      TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED,
-      "age-withdraw_reveal");
-    return false;
-  }
-
-  if (GNUNET_TIME_absolute_is_future (dks->meta.start.abs_time))
-  {
-    /* This denomination is not yet valid */
-    *result = TEH_RESPONSE_reply_expired_denom_pub_hash (
-      connection,
-      denom_h,
-      TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE,
-      "age-withdraw_reveal");
-    return false;
-  }
-
-  if (dks->recoup_possible)
-  {
-    /* This denomination has been revoked */
-    *result = TALER_MHD_reply_with_error (
-      connection,
-      MHD_HTTP_GONE,
-      TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED,
-      NULL);
-    return false;
-  }
+  enum GNUNET_GenericReturnValue ret;
+  struct TEH_DenominationKey *denom_key;
+  struct TALER_AgeCommitmentHash ach;
+
+  /* First, retrieve denomination details */
+  denom_key = TEH_keys_denomination_by_hash_from_state (keys,
+                                                        denom_pub_h,
+                                                        connection,
+                                                        result);
+  if (NULL == denom_key)
+    return GNUNET_SYSERR;
 
-  if (0 == dks->denom_pub.age_mask.bits)
+  /* calculate age commitment hash */
   {
-    /* This denomation does not support age restriction */
-    char msg[256] = {0};
-    GNUNET_snprintf (msg,
-                     sizeof(msg),
-                     "denomination %s does not support age restriction",
-                     GNUNET_h2s (&denom_h->hash));
-
-    *result = TALER_MHD_reply_with_error (
-      connection,
-      MHD_HTTP_BAD_REQUEST,
-      TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN,
-      msg);
-    return false;
-  }
+    struct TALER_AgeCommitmentProof acp;
+    ret = TALER_age_restriction_from_secret (secret,
+                                             &denom_key->denom_pub.age_mask,
+                                             max_age,
+                                             &acp);
 
-  return true;
-}
-
-
-/**
- * Check if the given array of hashes of denomination_keys a) belong
- * to valid denominations and b) those are marked as age restricted.
- *
- * @param connection The HTTP connection to the client
- * @param len The lengths of the array @a denoms_h
- * @param denoms_h array of hashes of denomination public keys
- * @param coin_evs array of blinded coin planchets
- * @param[out] dks On success, will be filled with the denomination keys.  
Caller must deallocate.
- * @param amount_with_fee The committed amount including fees
- * @param[out] total_amount On success, will contain the total sum of all 
denominations
- * @param[out] total_fee On success, will contain the total sum of all fees
- * @param[out] result In the error cases, a response will be queued with MHD 
and this will be the result.
- * @return GNUNET_OK if the denominations are valid and support age-restriction
- *   GNUNET_SYSERR otherwise
- */
-static enum GNUNET_GenericReturnValue
-are_denominations_valid (
-  struct MHD_Connection *connection,
-  uint32_t len,
-  const struct TALER_DenominationHashP *denoms_h,
-  const struct TALER_BlindedPlanchet *coin_evs,
-  struct TEH_DenominationKey **dks,
-  const struct TALER_Amount *amount_with_fee,
-  struct TALER_Amount *total_amount,
-  struct TALER_Amount *total_fee,
-  MHD_RESULT *result)
-{
-  struct TEH_KeyStateHandle *ksh;
-
-  GNUNET_assert (*dks == NULL);
+    if (GNUNET_OK != ret)
+    {
+      GNUNET_break (0);
+      *result = TALER_MHD_reply_with_ec (connection,
+                                         
TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, /* FIXME[oec]: better error code */
+                                         "derivation of age restriction 
failed");
+      return ret;
+    }
 
-  ksh = TEH_keys_get_state ();
-  if (NULL == ksh)
-  {
-    *result = TALER_MHD_reply_with_error (connection,
-                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                          
TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
-                                          NULL);
-    return GNUNET_SYSERR;
+    TALER_age_commitment_hash (&acp.commitment, &ach);
   }
 
-  *dks = GNUNET_new_array (len, struct TEH_DenominationKey);
-  TALER_amount_set_zero (TEH_currency, total_amount);
-  TALER_amount_set_zero (TEH_currency, total_fee);
-
-  for (uint32_t i = 0; i < len; i++)
+  /* Next: calculate planchet */
   {
-    if (! denomination_is_valid (connection,
-                                 ksh,
-                                 &denoms_h[i],
-                                 dks[i],
-                                 result))
-      return GNUNET_SYSERR;
+    struct TALER_CoinPubHashP c_hash;
+    struct TALER_PlanchetDetail detail;
+    struct TALER_CoinSpendPrivateKeyP coin_priv;
+    union TALER_DenominationBlindingKeyP bks;
+    struct TALER_ExchangeWithdrawValues alg_values = {
+      .cipher = denom_key->denom_pub.cipher,
+    };
 
-    /* Ensure the ciphers from the planchets match the denominations' */
-    if (dks[i]->denom_pub.cipher != coin_evs[i].cipher)
+    if (TALER_DENOMINATION_CS == alg_values.cipher)
     {
-      GNUNET_break_op (0);
-      *result = TALER_MHD_reply_with_error (connection,
-                                            MHD_HTTP_BAD_REQUEST,
-                                            
TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH,
-                                            NULL);
-      return GNUNET_SYSERR;
-    }
+      struct TALER_CsNonce nonce;
 
-    /* Accumulate the values */
-    if (0 > TALER_amount_add (
-          total_amount,
-          total_amount,
-          &dks[i]->meta.value))
-    {
-      GNUNET_break_op (0);
-      *result = TALER_MHD_reply_with_error (connection,
-                                            MHD_HTTP_BAD_REQUEST,
-                                            
TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW,
-                                            "amount");
-      return GNUNET_SYSERR;
-    }
+      TALER_cs_withdraw_nonce_derive (
+        secret,
+        &nonce);
 
-    /* Accumulate the withdraw fees */
-    if (0 > TALER_amount_add (
-          total_fee,
-          total_fee,
-          &dks[i]->meta.fees.withdraw))
-    {
-      GNUNET_break_op (0);
-      *result = TALER_MHD_reply_with_error (connection,
-                                            MHD_HTTP_BAD_REQUEST,
-                                            
TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW,
-                                            "fee");
-      return GNUNET_SYSERR;
+      {
+        enum TALER_ErrorCode ec;
+        struct TEH_CsDeriveData cdd = {
+          .h_denom_pub = &denom_key->h_denom_pub,
+          .nonce = &nonce,
+        };
+
+        ec = TEH_keys_denomination_cs_r_pub (&cdd,
+                                             false,
+                                             &alg_values.details.
+                                             cs_values);
+        /* FIXME Handle error? */
+        GNUNET_assert (TALER_EC_NONE == ec);
+      }
     }
-  }
 
-  /* Compare the committed amount against the totals */
-  {
-    struct TALER_Amount sum;
-    TALER_amount_set_zero (TEH_currency, &sum);
+    TALER_planchet_blinding_secret_create (secret,
+                                           &alg_values,
+                                           &bks);
+
+    TALER_planchet_setup_coin_priv (secret,
+                                    &alg_values,
+                                    &coin_priv);
 
-    GNUNET_assert (0 < TALER_amount_add (
-                     &sum,
-                     total_amount,
-                     total_fee));
+    ret = TALER_planchet_prepare (&denom_key->denom_pub,
+                                  &alg_values,
+                                  &bks,
+                                  &coin_priv,
+                                  &ach,
+                                  &c_hash,
+                                  &detail);
 
-    if (0 != TALER_amount_cmp (&sum, amount_with_fee))
+    if (GNUNET_OK != ret)
     {
-      GNUNET_break_op (0);
-      *result = TALER_MHD_reply_with_error (connection,
-                                            MHD_HTTP_BAD_REQUEST,
-                                            
TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_INCORRECT,
-                                            NULL);
-      return GNUNET_SYSERR;
+      GNUNET_break (0);
+      *result = TALER_MHD_reply_json_pack (connection,
+                                           MHD_HTTP_INTERNAL_SERVER_ERROR,
+                                           "{ss}",
+                                           "details",
+                                           "failed to prepare planchet from 
base key");
+      return ret;
     }
+
+    ret = TALER_coin_ev_hash (&detail.blinded_planchet,
+                              &denom_key->h_denom_pub,
+                              bch);
+    GNUNET_assert (GNUNET_OK == ret);
   }
 
-  return GNUNET_OK;
+  return GNUNET_SYSERR;
 }
 
 
 /**
- * Checks the validity of the disclosed coins as follows:
+ * @brief Checks the validity of the disclosed coins as follows:
  * - Derives and calculates the disclosed coins'
  *    - public keys,
  *    - nonces (if applicable),
@@ -594,163 +375,83 @@ are_denominations_valid (
  * https://docs.taler.net/design-documents/024-age-restriction.html#withdraw
  *
  * @param connection HTTP-connection to the client
- * @param h_commitment_orig Original commitment
- * @param max_age Maximum age allowed for the age restriction
- * @param noreveal_idx Index that was given to the client in response to the 
age-withdraw request
- * @param num_coins Number of coins
- * @param coin_evs The blindet planchets of the undisclosed coins, @a 
num_coins many
- * @param denom_keys The array of denomination keys, @a num_coins. Needed to 
detect Clause-Schnorr-based denominations
+ * @param commitment Original commitment
  * @param disclosed_coin_secrets The secrets of the disclosed coins, 
(TALER_CNC_KAPPA - 1)*num_coins many
+ * @param num_coins number of coins to reveal via @a disclosed_coin_secrets
  * @param[out] result On error, a HTTP-response will be queued and result set 
accordingly
  * @return GNUNET_OK on success, GNUNET_SYSERR otherwise
  */
 static enum GNUNET_GenericReturnValue
 verify_commitment_and_max_age (
   struct MHD_Connection *connection,
-  const struct TALER_AgeWithdrawCommitmentHashP *h_commitment_orig,
-  const uint32_t max_age,
-  const uint32_t noreveal_idx,
-  const uint32_t num_coins,
-  const struct TALER_BlindedPlanchet *coin_evs,
-  const struct TEH_DenominationKey *denom_keys,
+  const struct TALER_EXCHANGEDB_AgeWithdraw *commitment,
   const struct TALER_PlanchetMasterSecretP *disclosed_coin_secrets,
+  uint32_t num_coins,
   MHD_RESULT *result)
 {
   enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
   struct GNUNET_HashContext *hash_context;
+  struct TEH_KeyStateHandle *keys;
+
+  if (num_coins != commitment->num_coins)
+  {
+    GNUNET_break_op (0);
+    *result = TALER_MHD_reply_with_error (connection,
+                                          MHD_HTTP_BAD_REQUEST,
+                                          TALER_EC_GENERIC_PARAMETER_MALFORMED,
+                                          "#coins");
+    return GNUNET_SYSERR;
+  }
+
+  /* We need the current keys in memory for the meta-data of the denominations 
*/
+  keys = TEH_keys_get_state ();
+  if (NULL == keys)
+  {
+    *result = TALER_MHD_reply_with_ec (connection,
+                                       TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
+                                       NULL);
+    return GNUNET_SYSERR;
+  }
 
   hash_context = GNUNET_CRYPTO_hash_context_start ();
 
-  for (size_t c = 0; c < num_coins; c++)
+  for (size_t coin_idx = 0; coin_idx < num_coins; coin_idx++)
   {
-    size_t k = 0; /* either 0 or 1, to index into coin_evs */
+    size_t i = 0; /* either 0 or 1, to index into coin_evs */
 
-    for (size_t idx = 0; idx<TALER_CNC_KAPPA; idx++)
+    for (size_t gamma = 0; gamma<TALER_CNC_KAPPA; gamma++)
     {
-      if (idx == (size_t) noreveal_idx)
+      if (gamma == (size_t) commitment->noreveal_index)
       {
         GNUNET_CRYPTO_hash_context_read (hash_context,
-                                         &coin_evs[c],
-                                         sizeof(coin_evs[c]));
+                                         &commitment->h_coin_evs[coin_idx],
+                                         
sizeof(commitment->h_coin_evs[coin_idx]));
       }
       else
       {
-        /* FIXME[oec] Refactor this block out into its own function */
-
-        size_t j = (TALER_CNC_KAPPA - 1) * c + k; /* Index into 
disclosed_coin_secrets[] */
+        /* j is the index into disclosed_coin_secrets[] */
+        size_t j = (TALER_CNC_KAPPA - 1) * coin_idx + i;
         const struct TALER_PlanchetMasterSecretP *secret;
-        struct TALER_AgeCommitmentHash ach;
         struct TALER_BlindedCoinHashP bch;
 
-        GNUNET_assert (k<2);
+        GNUNET_assert (i<2);
         GNUNET_assert ((TALER_CNC_KAPPA - 1) * num_coins  > j);
 
         secret = &disclosed_coin_secrets[j];
-        k++;
+        i++;
 
-        /* First: calculate age commitment hash */
-        {
-          struct TALER_AgeCommitmentProof acp;
-          ret = TALER_age_restriction_from_secret (
-            secret,
-            &denom_keys[c].denom_pub.age_mask,
-            max_age,
-            &acp);
-
-          if (GNUNET_OK != ret)
-          {
-            GNUNET_break (0);
-            *result = TALER_MHD_reply_json_pack (connection,
-                                                 
MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                                 "{sssi}",
-                                                 "failed to derive age 
restriction from base key",
-                                                 "index",
-                                                 j);
-            return ret;
-          }
-
-          TALER_age_commitment_hash (&acp.commitment, &ach);
-        }
+        ret = calculate_blinded_hash (connection,
+                                      keys,
+                                      secret,
+                                      &commitment->denom_pub_hashes[coin_idx],
+                                      commitment->max_age,
+                                      &bch,
+                                      result);
 
-        /* Next: calculate planchet */
+        if (GNUNET_OK != ret)
         {
-          struct TALER_CoinPubHashP c_hash;
-          struct TALER_PlanchetDetail detail;
-          struct TALER_CoinSpendPrivateKeyP coin_priv;
-          union TALER_DenominationBlindingKeyP bks;
-          struct TALER_ExchangeWithdrawValues alg_values = {
-            .cipher = denom_keys[c].denom_pub.cipher,
-          };
-
-          if (TALER_DENOMINATION_CS == alg_values.cipher)
-          {
-            struct TALER_CsNonce nonce;
-
-            TALER_cs_withdraw_nonce_derive (
-              secret,
-              &nonce);
-
-            {
-              enum TALER_ErrorCode ec;
-              struct TEH_CsDeriveData cdd = {
-                .h_denom_pub = &denom_keys[c].h_denom_pub,
-                .nonce = &nonce,
-              };
-
-              ec = TEH_keys_denomination_cs_r_pub (&cdd,
-                                                   false,
-                                                   &alg_values.details.
-                                                   cs_values);
-              /* FIXME Handle error? */
-              GNUNET_assert (TALER_EC_NONE == ec);
-            }
-          }
-
-          TALER_planchet_blinding_secret_create (secret,
-                                                 &alg_values,
-                                                 &bks);
-
-          TALER_planchet_setup_coin_priv (secret,
-                                          &alg_values,
-                                          &coin_priv);
-
-          ret = TALER_planchet_prepare (&denom_keys[c].denom_pub,
-                                        &alg_values,
-                                        &bks,
-                                        &coin_priv,
-                                        &ach,
-                                        &c_hash,
-                                        &detail);
-
-          if (GNUNET_OK != ret)
-          {
-            GNUNET_break (0);
-            *result = TALER_MHD_reply_json_pack (connection,
-                                                 
MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                                 "{sssi}",
-                                                 "details",
-                                                 "failed to prepare planchet 
from base key",
-                                                 "index",
-                                                 j);
-            return ret;
-          }
-
-          ret = TALER_coin_ev_hash (&detail.blinded_planchet,
-                                    &denom_keys[c].h_denom_pub,
-                                    &bch);
-          if (GNUNET_OK != ret)
-          {
-            GNUNET_break (0);
-            *result = TALER_MHD_reply_json_pack (connection,
-                                                 
MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                                 "{sssi}",
-                                                 "details",
-                                                 "failed to hash planchet from 
base key",
-                                                 "index",
-                                                 j);
-            return ret;
-          }
-
+          GNUNET_CRYPTO_hash_context_abort (hash_context);
+          return GNUNET_SYSERR;
         }
 
         /* Continue the running hash of all coin hashes with the calculated
@@ -768,7 +469,7 @@ verify_commitment_and_max_age (
     GNUNET_CRYPTO_hash_context_finish (hash_context,
                                        &calc_hash);
 
-    if (0 != GNUNET_CRYPTO_hash_cmp (&h_commitment_orig->hash,
+    if (0 != GNUNET_CRYPTO_hash_cmp (&commitment->h_commitment.hash,
                                      &calc_hash))
     {
       GNUNET_break_op (0);
@@ -788,26 +489,22 @@ verify_commitment_and_max_age (
  * @brief Send a response for "/age-withdraw/$RCH/reveal"
  *
  * @param connection The http connection to the client to send the response to
- * @param num_coins Number of new coins with age restriction for which we 
reveal data
- * @param awrcs array of @a num_coins signatures revealed
+ * @param commitment The data from the commitment with signatures
  * @return a MHD result code
  */
 static MHD_RESULT
 reply_age_withdraw_reveal_success (
   struct MHD_Connection *connection,
-  unsigned int num_coins,
-  const struct TALER_EXCHANGEDB_AgeWithdrawRevealedCoin *awrcs)
+  const struct TALER_EXCHANGEDB_AgeWithdraw *commitment)
 {
   json_t *list = json_array ();
   GNUNET_assert (NULL != list);
 
-  for (unsigned int index = 0;
-       index < num_coins;
-       index++)
+  for (unsigned int i = 0; i < commitment->num_coins; i++)
   {
     json_t *obj = GNUNET_JSON_PACK (
       TALER_JSON_pack_blinded_denom_sig ("ev_sig",
-                                         &awrcs[index].coin_sig));
+                                         &commitment->denom_sigs[i]));
     GNUNET_assert (0 ==
                    json_array_append_new (list,
                                           obj));
@@ -821,160 +518,6 @@ reply_age_withdraw_reveal_success (
 }
 
 
-/**
- * @brief Signs and persists the undisclosed coins
- *
- * @param connection HTTP-connection to the client
- * @param h_commitment Original commitment
- * @param num_coins Number of coins
- * @param coin_evs The Hashes of the undisclosed, blinded coins, @a num_coins 
many
- * @param denom_keys The array of denomination keys, @a num_coins. Needed to 
detect Clause-Schnorr-based denominations
- * @param[out] result On error, a HTTP-response will be queued and result set 
accordingly
- * @return GNUNET_OK on success, GNUNET_SYSERR otherwise
- */
-static enum GNUNET_GenericReturnValue
-sign_and_finalize_age_withdraw (
-  struct MHD_Connection *connection,
-  const struct TALER_AgeWithdrawCommitmentHashP *h_commitment,
-  const uint32_t num_coins,
-  const struct TALER_BlindedPlanchet *coin_evs,
-  const struct TEH_DenominationKey *denom_keys,
-  MHD_RESULT *result)
-{
-  enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
-  struct TEH_CoinSignData csds[num_coins];
-  struct TALER_BlindedDenominationSignature bds[num_coins];
-  struct TALER_EXCHANGEDB_AgeWithdrawRevealedCoin awrcs[num_coins];
-  enum GNUNET_DB_QueryStatus qs;
-
-  for (uint32_t i = 0; i<num_coins; i++)
-  {
-    csds[i].h_denom_pub = &denom_keys[i].h_denom_pub;
-    csds[i].bp = &coin_evs[i];
-  }
-
-  /* Sign the the blinded coins first */
-  {
-    enum TALER_ErrorCode ec;
-    ec = TEH_keys_denomination_batch_sign (csds,
-                                           num_coins,
-                                           false,
-                                           bds);
-    if (TALER_EC_NONE != ec)
-    {
-      GNUNET_break (0);
-      *result = TALER_MHD_reply_with_ec (connection,
-                                         ec,
-                                         NULL);
-      return GNUNET_SYSERR;
-    }
-  }
-
-  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
-              "Signatures ready, starting DB interaction\n");
-
-  /* Prepare the data for insertion */
-  for (uint32_t i = 0; i<num_coins; i++)
-  {
-    TALER_coin_ev_hash (&coin_evs[i],
-                        csds[i].h_denom_pub,
-                        &awrcs[i].h_coin_ev);
-    awrcs[i].h_denom_pub = *csds[i].h_denom_pub;
-    awrcs[i].coin_sig = bds[i];
-  }
-
-  /* Persist operation result in DB, transactionally */
-  for (unsigned int r = 0; r < MAX_TRANSACTION_COMMIT_RETRIES; r++)
-  {
-    bool changed = false;
-
-    /* Transaction start */
-    if (GNUNET_OK !=
-        TEH_plugin->start (TEH_plugin->cls,
-                           "insert_age_withdraw_reveal batch"))
-    {
-      GNUNET_break (0);
-      ret = TALER_MHD_reply_with_error (connection,
-                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                        TALER_EC_GENERIC_DB_START_FAILED,
-                                        NULL);
-      goto cleanup;
-    }
-
-    qs = TEH_plugin->insert_age_withdraw_reveal (TEH_plugin->cls,
-                                                 h_commitment,
-                                                 num_coins,
-                                                 awrcs);
-
-    if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
-    {
-      TEH_plugin->rollback (TEH_plugin->cls);
-      continue;
-    }
-    else if (GNUNET_DB_STATUS_HARD_ERROR == qs)
-    {
-      GNUNET_break (0);
-      TEH_plugin->rollback (TEH_plugin->cls);
-      ret = TALER_MHD_reply_with_error (connection,
-                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                        TALER_EC_GENERIC_DB_STORE_FAILED,
-                                        "insert_age_withdraw_reveal");
-      goto cleanup;
-    }
-
-    changed = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs);
-
-    /* Commit the transaction */
-    qs = TEH_plugin->commit (TEH_plugin->cls);
-    if (qs >= 0)
-    {
-      if (changed)
-        TEH_METRICS_num_success[TEH_MT_SUCCESS_AGE_WITHDRAW_REVEAL]++;
-
-      break; /* success */
-
-    }
-    else if (GNUNET_DB_STATUS_HARD_ERROR == qs)
-    {
-      GNUNET_break (0);
-      TEH_plugin->rollback (TEH_plugin->cls);
-      ret = TALER_MHD_reply_with_error (connection,
-                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                        TALER_EC_GENERIC_DB_COMMIT_FAILED,
-                                        NULL);
-      goto cleanup;
-    }
-    else
-    {
-      TEH_plugin->rollback (TEH_plugin->cls);
-    }
-  } /* end of retry */
-
-  if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
-  {
-    GNUNET_break (0);
-    TEH_plugin->rollback (TEH_plugin->cls);
-    ret = TALER_MHD_reply_with_error (connection,
-                                      MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                      TALER_EC_GENERIC_DB_SOFT_FAILURE,
-                                      NULL);
-    goto cleanup;
-  }
-
-  /* Generate final (positive) response */
-  ret = reply_age_withdraw_reveal_success (connection,
-                                           num_coins,
-                                           awrcs);
-cleanup:
-  GNUNET_break (GNUNET_OK != ret);
-
-  /* Free resources */
-  for (unsigned int i = 0; i<num_coins; i++)
-    TALER_blinded_denom_sig_free (&awrcs[i].coin_sig);
-  return ret;
-}
-
-
 MHD_RESULT
 TEH_handler_age_withdraw_reveal (
   struct TEH_RequestContext *rc,
@@ -984,16 +527,10 @@ TEH_handler_age_withdraw_reveal (
   MHD_RESULT result = MHD_NO;
   enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
   struct AgeRevealContext actx = {0};
-  const json_t *j_denoms_h;
-  const json_t *j_coin_evs;
   const json_t *j_disclosed_coin_secrets;
   struct GNUNET_JSON_Specification spec[] = {
     GNUNET_JSON_spec_fixed_auto ("reserve_pub",
                                  &actx.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_array_const ("disclosed_coin_secrets",
                                   &j_disclosed_coin_secrets),
     GNUNET_JSON_spec_end ()
@@ -1017,8 +554,6 @@ TEH_handler_age_withdraw_reveal (
     if (GNUNET_OK !=
         parse_age_withdraw_reveal_json (
           rc->connection,
-          j_denoms_h,
-          j_coin_evs,
           j_disclosed_coin_secrets,
           &actx,
           &result))
@@ -1034,49 +569,25 @@ TEH_handler_age_withdraw_reveal (
           &result))
       break;
 
-    /* Ensure validity of denoms and the sum of amounts and fees */
-    if (GNUNET_OK !=
-        are_denominations_valid (
-          rc->connection,
-          actx.num_coins,
-          actx.denoms_h,
-          actx.coin_evs,
-          &actx.denom_keys,
-          &actx.commitment.amount_with_fee,
-          &actx.total_amount,
-          &actx.total_fee,
-          &result))
-      break;
-
     /* Verify the computed h_commitment equals the committed one and that coins
      * have a maximum age group corresponding max_age (age-mask dependent) */
     if (GNUNET_OK !=
         verify_commitment_and_max_age (
           rc->connection,
-          &actx.commitment.h_commitment,
-          actx.commitment.max_age,
-          actx.commitment.noreveal_index,
-          actx.num_coins,
-          actx.coin_evs,
-          actx.denom_keys,
+          actx.commitment,
           actx.disclosed_coin_secrets,
-          &result))
-      break;
-
-    /* Finally, sign and persist the coins */
-    if (GNUNET_OK !=
-        sign_and_finalize_age_withdraw (
-          rc->connection,
-          &actx.commitment.h_commitment,
           actx.num_coins,
-          actx.coin_evs,
-          actx.denom_keys,
           &result))
       break;
 
+    /* Finally, return the signatures */
+    result = reply_age_withdraw_reveal_success (rc->connection,
+                                                actx.commitment);
+
   } while(0);
 
-  age_reveal_context_free (&actx);
+  GNUNET_JSON_parse_free (spec);
+  GNUNET_free (actx.disclosed_coin_secrets);
   return result;
 }
 
diff --git a/src/exchange/taler-exchange-httpd_age-withdraw_reveal.h 
b/src/exchange/taler-exchange-httpd_age-withdraw_reveal.h
index 73ebc6fb..f7b813fe 100644
--- a/src/exchange/taler-exchange-httpd_age-withdraw_reveal.h
+++ b/src/exchange/taler-exchange-httpd_age-withdraw_reveal.h
@@ -18,8 +18,8 @@
  * @brief Handle /age-withdraw/$ACH/reveal requests
  * @author Özgür Kesim
  */
-#ifndef TALER_EXCHANGE_HTTPD_AGE_WITHDRAW_H
-#define TALER_EXCHANGE_HTTPD_AGE_WITHDRAW_H
+#ifndef TALER_EXCHANGE_HTTPD_AGE_WITHDRAW_REVEAL_H
+#define TALER_EXCHANGE_HTTPD_AGE_WITHDRAW_REVEAL_H
 
 #include <microhttpd.h>
 #include "taler-exchange-httpd.h"
diff --git a/src/exchange/taler-exchange-httpd_batch-withdraw.c 
b/src/exchange/taler-exchange-httpd_batch-withdraw.c
index 9d7d64cb..270ee0ca 100644
--- a/src/exchange/taler-exchange-httpd_batch-withdraw.c
+++ b/src/exchange/taler-exchange-httpd_batch-withdraw.c
@@ -26,12 +26,14 @@
 #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_responses.h"
 #include "taler-exchange-httpd_keys.h"
+#include "taler_util.h"
 
 
 /**
@@ -306,8 +308,10 @@ batch_withdraw_transaction (void *cls,
   struct BatchWithdrawContext *wc = cls;
   uint64_t ruuid;
   enum GNUNET_DB_QueryStatus qs;
-  bool balance_ok = false;
   bool found = false;
+  bool balance_ok = false;
+  bool age_ok = false;
+  uint16_t allowed_maximum_age = 0;
   char *kyc_required;
   struct TALER_PaytoHashP reserve_h_payto;
 
@@ -466,12 +470,16 @@ batch_withdraw_transaction (void *cls,
     return qs;
   }
   wc->kyc.ok = true;
+
   qs = TEH_plugin->do_batch_withdraw (TEH_plugin->cls,
                                       wc->now,
                                       wc->reserve_pub,
                                       &wc->batch_total,
+                                      TEH_age_restriction_enabled,
                                       &found,
                                       &balance_ok,
+                                      &age_ok,
+                                      &allowed_maximum_age,
                                       &ruuid);
   if (0 > qs)
   {
@@ -493,6 +501,22 @@ batch_withdraw_transaction (void *cls,
                                            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);
+
+    TEH_plugin->rollback (TEH_plugin->cls);
+    *mhd_ret = TEH_RESPONSE_reply_reserve_age_restriction_required (
+      connection,
+      lowest_age);
+    return GNUNET_DB_STATUS_HARD_ERROR;
+  }
+
   if (! balance_ok)
   {
     TEH_plugin->rollback (TEH_plugin->cls);
@@ -717,10 +741,12 @@ parse_planchets (const struct TEH_RequestContext *rc,
     struct PlanchetContext *pc = &wc->planchets[i];
     struct TEH_DenominationKey *dk;
 
-    dk = TEH_keys_denomination_by_hash2 (ksh,
-                                         &pc->collectable.denom_pub_hash,
-                                         NULL,
-                                         NULL);
+    dk = TEH_keys_denomination_by_hash_from_state (
+      ksh,
+      &pc->collectable.denom_pub_hash,
+      NULL,
+      NULL);
+
     if (NULL == dk)
     {
       if (! check_request_idempotent (wc,
diff --git a/src/exchange/taler-exchange-httpd_common_kyc.c 
b/src/exchange/taler-exchange-httpd_common_kyc.c
index b6585ed5..bb3ca479 100644
--- a/src/exchange/taler-exchange-httpd_common_kyc.c
+++ b/src/exchange/taler-exchange-httpd_common_kyc.c
@@ -19,9 +19,12 @@
  * @author Christian Grothoff
  */
 #include "platform.h"
+#include "taler-exchange-httpd.h"
 #include "taler-exchange-httpd_common_kyc.h"
 #include "taler_attributes.h"
+#include "taler_error_codes.h"
 #include "taler_exchangedb_plugin.h"
+#include <gnunet/gnunet_common.h>
 
 struct TEH_KycAmlTrigger
 {
@@ -114,7 +117,7 @@ kyc_aml_finished (void *cls,
   size_t eas;
   void *ea;
   const char *birthdate;
-  unsigned int birthday;
+  unsigned int birthday = 0;
   struct GNUNET_ShortHashCode kyc_prox;
   struct GNUNET_AsyncScopeSave old_scope;
 
@@ -125,9 +128,29 @@ kyc_aml_finished (void *cls,
                                        &kyc_prox);
   birthdate = json_string_value (json_object_get (kat->attributes,
                                                   TALER_ATTRIBUTE_BIRTHDATE));
-  birthday = 0; (void) birthdate;  // FIXME-Oec: calculate birthday here...
-  // Convert 'birthdate' to time after 1970, then compute days.
-  // Then compare against max age-restriction, and if before, set to 0.
+
+  if (TEH_age_restriction_enabled)
+  {
+    enum GNUNET_GenericReturnValue ret;
+
+    ret = TALER_parse_coarse_date (birthdate,
+                                   &TEH_age_restriction_config.mask,
+                                   &birthday);
+
+    if (GNUNET_OK != ret)
+    {
+      GNUNET_break (0);
+      if (NULL != kat->response)
+        MHD_destroy_response (kat->response);
+      kat->http_status = MHD_HTTP_BAD_REQUEST;
+      kat->response = TALER_MHD_make_error (
+        TALER_EC_GENERIC_PARAMETER_MALFORMED,
+        TALER_ATTRIBUTE_BIRTHDATE);
+
+      /* FIXME-Christian: shouldn't we return in the error case? */
+    }
+  }
+
   TALER_CRYPTO_kyc_attributes_encrypt (&TEH_attribute_key,
                                        kat->attributes,
                                        &ea,
@@ -159,6 +182,8 @@ kyc_aml_finished (void *cls,
     kat->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
     kat->response = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED,
                                           "do_insert_kyc_attributes");
+
+    /* FIXME-Christian: shouldn't we return in the error case? */
   }
   /* Finally, return result to main handler */
   kat->cb (kat->cb_cls,
diff --git a/src/exchange/taler-exchange-httpd_csr.c 
b/src/exchange/taler-exchange-httpd_csr.c
index 3ceb319c..64892d36 100644
--- a/src/exchange/taler-exchange-httpd_csr.c
+++ b/src/exchange/taler-exchange-httpd_csr.c
@@ -127,10 +127,10 @@ TEH_handler_csr_melt (struct TEH_RequestContext *rc,
                                                
TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
                                                NULL);
           }
-          dk = TEH_keys_denomination_by_hash2 (ksh,
-                                               denom_pub_hash,
-                                               NULL,
-                                               NULL);
+          dk = TEH_keys_denomination_by_hash_from_state (ksh,
+                                                         denom_pub_hash,
+                                                         NULL,
+                                                         NULL);
           if (NULL == dk)
           {
             return TEH_RESPONSE_reply_unknown_denom_pub_hash (
@@ -262,10 +262,10 @@ TEH_handler_csr_withdraw (struct TEH_RequestContext *rc,
                                          
TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
                                          NULL);
     }
-    dk = TEH_keys_denomination_by_hash2 (ksh,
-                                         &denom_pub_hash,
-                                         NULL,
-                                         NULL);
+    dk = TEH_keys_denomination_by_hash_from_state (ksh,
+                                                   &denom_pub_hash,
+                                                   NULL,
+                                                   NULL);
     if (NULL == dk)
     {
       return TEH_RESPONSE_reply_unknown_denom_pub_hash (
diff --git a/src/exchange/taler-exchange-httpd_extensions.c 
b/src/exchange/taler-exchange-httpd_extensions.c
index 48c3f4a9..68cc2da5 100644
--- a/src/exchange/taler-exchange-httpd_extensions.c
+++ b/src/exchange/taler-exchange-httpd_extensions.c
@@ -148,7 +148,7 @@ extension_update_event_cb (void *cls,
     TEH_age_restriction_enabled = false;
     if (NULL != conf)
     {
-      TEH_age_restriction_enabled = true;
+      TEH_age_restriction_enabled = extension->enabled;
       TEH_age_restriction_config = *conf;
       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                   "[age restriction] DB event has changed the config to %s 
with mask: %s\n",
diff --git a/src/exchange/taler-exchange-httpd_keys.c 
b/src/exchange/taler-exchange-httpd_keys.c
index b39093ec..e53f2732 100644
--- a/src/exchange/taler-exchange-httpd_keys.c
+++ b/src/exchange/taler-exchange-httpd_keys.c
@@ -1364,7 +1364,6 @@ denomination_info_cb (
                                        &dk->h_denom_pub.hash,
                                        dk,
                                        
GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY));
-
 }
 
 
@@ -1753,7 +1752,7 @@ wallet_threshold_cb (void *cls,
  *
  * @param[in,out] ksh key state handle we build @a krd for
  * @param[in] denom_keys_hash hash over all the denomination keys in @a denoms
- * @param last_cpd timestamp to use
+ * @param last_cherry_pick_date timestamp to use
  * @param signkeys list of sign keys to return
  * @param recoup list of revoked keys to return
  * @param denoms list of denominations to return
@@ -1764,7 +1763,7 @@ wallet_threshold_cb (void *cls,
 static enum GNUNET_GenericReturnValue
 create_krd (struct TEH_KeyStateHandle *ksh,
             const struct GNUNET_HashCode *denom_keys_hash,
-            struct GNUNET_TIME_Timestamp last_cpd,
+            struct GNUNET_TIME_Timestamp last_cherry_pick_date,
             json_t *signkeys,
             json_t *recoup,
             json_t *denoms,
@@ -1778,7 +1777,8 @@ create_krd (struct TEH_KeyStateHandle *ksh,
   struct TALER_ExchangeSignatureP grouped_exchange_sig;
   json_t *keys;
 
-  GNUNET_assert (! GNUNET_TIME_absolute_is_zero (last_cpd.abs_time));
+  GNUNET_assert (! GNUNET_TIME_absolute_is_zero (
+                   last_cherry_pick_date.abs_time));
   GNUNET_assert (NULL != signkeys);
   GNUNET_assert (NULL != recoup);
   GNUNET_assert (NULL != denoms);
@@ -1788,7 +1788,7 @@ create_krd (struct TEH_KeyStateHandle *ksh,
   GNUNET_assert (NULL != TEH_currency);
   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
               "Creating /keys at cherry pick date %s\n",
-              GNUNET_TIME_timestamp2s (last_cpd));
+              GNUNET_TIME_timestamp2s (last_cherry_pick_date));
 
   /* Sign hash over denomination keys */
   {
@@ -1799,7 +1799,7 @@ create_krd (struct TEH_KeyStateHandle *ksh,
            TALER_exchange_online_key_set_sign (
              &TEH_keys_exchange_sign2_,
              ksh,
-             last_cpd,
+             last_cherry_pick_date,
              denom_keys_hash,
              &exchange_pub,
              &exchange_sig)))
@@ -1820,7 +1820,7 @@ create_krd (struct TEH_KeyStateHandle *ksh,
            TALER_exchange_online_key_set_sign (
              &TEH_keys_exchange_sign2_,
              ksh,
-             last_cpd,
+             last_cherry_pick_date,
              h_grouped,
              &grouped_exchange_pub,
              &grouped_exchange_sig)))
@@ -1876,7 +1876,7 @@ create_krd (struct TEH_KeyStateHandle *ksh,
     GNUNET_JSON_pack_array_incref ("global_fees",
                                    ksh->global_fees),
     GNUNET_JSON_pack_timestamp ("list_issue_date",
-                                last_cpd),
+                                last_cherry_pick_date),
     GNUNET_JSON_pack_data_auto ("eddsa_pub",
                                 &exchange_pub),
     GNUNET_JSON_pack_data_auto ("eddsa_sig",
@@ -2034,7 +2034,7 @@ create_krd (struct TEH_KeyStateHandle *ksh,
                                            etag));
     krd.etag = GNUNET_strdup (etag);
   }
-  krd.cherry_pick_date = last_cpd;
+  krd.cherry_pick_date = last_cherry_pick_date;
   GNUNET_array_append (ksh->krd_array,
                        ksh->krd_array_length,
                        krd);
@@ -2054,14 +2054,17 @@ create_krd (struct TEH_KeyStateHandle *ksh,
 static enum GNUNET_GenericReturnValue
 finish_keys_response (struct TEH_KeyStateHandle *ksh)
 {
+  enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
   json_t *recoup;
   struct SignKeyCtx sctx;
   json_t *denoms = NULL;
   json_t *grouped_denominations = NULL;
-  struct GNUNET_TIME_Timestamp last_cpd;
+  struct GNUNET_TIME_Timestamp last_cherry_pick_date;
   struct GNUNET_CONTAINER_Heap *heap;
   struct GNUNET_HashContext *hash_context = NULL;
   struct GNUNET_HashCode grouped_hash_xor = {0};
+  /* Remember if we have any denomination with age restriction */
+  bool has_age_restricted_denomination = false;
 
   sctx.signkeys = json_array ();
   GNUNET_assert (NULL != sctx.signkeys);
@@ -2094,7 +2097,7 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh)
   grouped_denominations = json_array ();
   GNUNET_assert (NULL != grouped_denominations);
 
-  last_cpd = GNUNET_TIME_UNIT_ZERO_TS;
+  last_cherry_pick_date = GNUNET_TIME_UNIT_ZERO_TS;
 
   // FIXME: This block contains the implementation of the DEPRECATED
   // "denom_pubs" array along with the new grouped "denominations".
@@ -2124,13 +2127,13 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh)
     /* heap = min heap, sorted by start time */
     while (NULL != (dk = GNUNET_CONTAINER_heap_remove_root (heap)))
     {
-      if (GNUNET_TIME_timestamp_cmp (last_cpd,
+      if (GNUNET_TIME_timestamp_cmp (last_cherry_pick_date,
                                      !=,
                                      dk->meta.start) &&
-          (! GNUNET_TIME_absolute_is_zero (last_cpd.abs_time)) )
+          (! GNUNET_TIME_absolute_is_zero (last_cherry_pick_date.abs_time)) )
       {
         /*
-         * This is not the first entry in the heap (because last_cpd !=
+         * This is not the first entry in the heap (because 
last_cherry_pick_date !=
          * GNUNET_TIME_UNIT_ZERO_TS) and the previous entry had a different
          * start time.  Therefore, we create a new entry in ksh.
          */
@@ -2143,7 +2146,7 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh)
         if (GNUNET_OK !=
             create_krd (ksh,
                         &hc,
-                        last_cpd,
+                        last_cherry_pick_date,
                         sctx.signkeys,
                         recoup,
                         denoms,
@@ -2152,21 +2155,17 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh)
         {
           GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                       "Failed to generate key response data for %s\n",
-                      GNUNET_TIME_timestamp2s (last_cpd));
+                      GNUNET_TIME_timestamp2s (last_cherry_pick_date));
           GNUNET_CRYPTO_hash_context_abort (hash_context);
           /* drain heap before destroying it */
           while (NULL != (dk = GNUNET_CONTAINER_heap_remove_root (heap)))
             /* intentionally empty */;
           GNUNET_CONTAINER_heap_destroy (heap);
-          json_decref (denoms);
-          json_decref (grouped_denominations);
-          json_decref (sctx.signkeys);
-          json_decref (recoup);
-          return GNUNET_SYSERR;
+          goto CLEANUP;
         }
       }
 
-      last_cpd = dk->meta.start;
+      last_cherry_pick_date = dk->meta.start;
 
       {
         json_t *denom;
@@ -2264,7 +2263,11 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh)
             int r = json_object_set_new (group->json,
                                          "age_mask",
                                          json_integer (meta.age_mask.bits));
+
             GNUNET_assert (0 == r);
+
+            /* Remember that we have found at least _one_ age restricted 
denomination */
+            has_age_restricted_denomination = true;
           }
 
           /* Create a new array for the denominations in this group */
@@ -2386,7 +2389,7 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh)
   }
 
   GNUNET_CONTAINER_heap_destroy (heap);
-  if (! GNUNET_TIME_absolute_is_zero (last_cpd.abs_time))
+  if (! GNUNET_TIME_absolute_is_zero (last_cherry_pick_date.abs_time))
   {
     struct GNUNET_HashCode hc;
 
@@ -2395,7 +2398,7 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh)
     if (GNUNET_OK !=
         create_krd (ksh,
                     &hc,
-                    last_cpd,
+                    last_cherry_pick_date,
                     sctx.signkeys,
                     recoup,
                     denoms,
@@ -2404,14 +2407,25 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh)
     {
       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                   "Failed to generate key response data for %s\n",
-                  GNUNET_TIME_timestamp2s (last_cpd));
-      json_decref (denoms);
-      json_decref (grouped_denominations);
-      json_decref (sctx.signkeys);
-      json_decref (recoup);
-      return GNUNET_SYSERR;
+                  GNUNET_TIME_timestamp2s (last_cherry_pick_date));
+      goto CLEANUP;
     }
     ksh->management_only = false;
+
+    /* Sanity check:  Make sure that age restriction is enabled IFF at least
+     * one age restricted denomination exist */
+    if (! has_age_restricted_denomination && TEH_age_restriction_enabled)
+    {
+      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                  "Age restriction is enabled, but NO denominations with age 
restriction found!\n");
+      goto CLEANUP;
+    }
+    else if (has_age_restricted_denomination && ! TEH_age_restriction_enabled)
+    {
+      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                  "Age restriction is NOT enabled, but denominations with age 
restriction found!\n");
+      goto CLEANUP;
+    }
   }
   else
   {
@@ -2419,11 +2433,15 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh)
                 "No denomination keys available. Refusing to generate /keys 
response.\n");
     GNUNET_CRYPTO_hash_context_abort (hash_context);
   }
+
+  ret = GNUNET_OK;
+
+CLEANUP:
   json_decref (grouped_denominations);
   json_decref (sctx.signkeys);
   json_decref (recoup);
   json_decref (denoms);
-  return GNUNET_OK;
+  return ret;
 }
 
 
@@ -2733,16 +2751,16 @@ TEH_keys_denomination_by_hash (
     return NULL;
   }
 
-  return TEH_keys_denomination_by_hash2 (ksh,
-                                         h_denom_pub,
-                                         conn,
-                                         mret);
+  return TEH_keys_denomination_by_hash_from_state (ksh,
+                                                   h_denom_pub,
+                                                   conn,
+                                                   mret);
 }
 
 
 struct TEH_DenominationKey *
-TEH_keys_denomination_by_hash2 (
-  struct TEH_KeyStateHandle *ksh,
+TEH_keys_denomination_by_hash_from_state (
+  const struct TEH_KeyStateHandle *ksh,
   const struct TALER_DenominationHashP *h_denom_pub,
   struct MHD_Connection *conn,
   MHD_RESULT *mret)
diff --git a/src/exchange/taler-exchange-httpd_keys.h 
b/src/exchange/taler-exchange-httpd_keys.h
index 8758afb7..d4a57812 100644
--- a/src/exchange/taler-exchange-httpd_keys.h
+++ b/src/exchange/taler-exchange-httpd_keys.h
@@ -234,8 +234,8 @@ TEH_keys_denomination_by_hash (
  *         or NULL if @a h_denom_pub could not be found
  */
 struct TEH_DenominationKey *
-TEH_keys_denomination_by_hash2 (
-  struct TEH_KeyStateHandle *ksh,
+TEH_keys_denomination_by_hash_from_state (
+  const struct TEH_KeyStateHandle *ksh,
   const struct TALER_DenominationHashP *h_denom_pub,
   struct MHD_Connection *conn,
   MHD_RESULT *mret);
diff --git a/src/exchange/taler-exchange-httpd_refreshes_reveal.c 
b/src/exchange/taler-exchange-httpd_refreshes_reveal.c
index 47926a74..4fb16407 100644
--- a/src/exchange/taler-exchange-httpd_refreshes_reveal.c
+++ b/src/exchange/taler-exchange-httpd_refreshes_reveal.c
@@ -511,7 +511,7 @@ resolve_refreshes_reveal_denominations (
     }
   }
 
-  old_dk = TEH_keys_denomination_by_hash2 (
+  old_dk = TEH_keys_denomination_by_hash_from_state (
     ksh,
     &rctx->melt.session.coin.denom_pub_hash,
     connection,
@@ -536,10 +536,10 @@ resolve_refreshes_reveal_denominations (
                                       -1);
     if (GNUNET_OK != res)
       return (GNUNET_NO == res) ? MHD_YES : MHD_NO;
-    dks[i] = TEH_keys_denomination_by_hash2 (ksh,
-                                             &rrcs[i].h_denom_pub,
-                                             connection,
-                                             &ret);
+    dks[i] = TEH_keys_denomination_by_hash_from_state (ksh,
+                                                       &rrcs[i].h_denom_pub,
+                                                       connection,
+                                                       &ret);
     if (NULL == dks[i])
       return ret;
     if ( (TALER_DENOMINATION_CS == dks[i]->denom_pub.cipher) &&
diff --git a/src/exchange/taler-exchange-httpd_responses.c 
b/src/exchange/taler-exchange-httpd_responses.c
index 835a4771..d1636a0a 100644
--- a/src/exchange/taler-exchange-httpd_responses.c
+++ b/src/exchange/taler-exchange-httpd_responses.c
@@ -1084,6 +1084,20 @@ TEH_RESPONSE_reply_reserve_insufficient_balance (
 }
 
 
+MHD_RESULT
+TEH_RESPONSE_reply_reserve_age_restriction_required (
+  struct MHD_Connection *connection,
+  uint16_t maximum_allowed_age)
+{
+  return TALER_MHD_REPLY_JSON_PACK (
+    connection,
+    MHD_HTTP_BAD_REQUEST,
+    TALER_JSON_pack_ec (TALER_EC_EXCHANGE_RESERVES_AGE_RESTRICTION_REQUIRED),
+    GNUNET_JSON_pack_uint64 ("maximum_allowed_age",
+                             maximum_allowed_age));
+}
+
+
 MHD_RESULT
 TEH_RESPONSE_reply_purse_created (
   struct MHD_Connection *connection,
diff --git a/src/exchange/taler-exchange-httpd_responses.h 
b/src/exchange/taler-exchange-httpd_responses.h
index 0db6968f..a57fa495 100644
--- a/src/exchange/taler-exchange-httpd_responses.h
+++ b/src/exchange/taler-exchange-httpd_responses.h
@@ -75,6 +75,20 @@ TEH_RESPONSE_reply_reserve_insufficient_balance (
   const struct TALER_Amount *balance_required,
   const struct TALER_ReservePublicKeyP *reserve_pub);
 
+/**
+ * Return error message indicating that a reserve requires age
+ * restriction to be set during withdraw, that is: the age-withdraw
+ * protocol MUST be used with commitment to an admissible age.
+ *
+ * @param connection connection to the client
+ * @param maximum_allowed_age the balance required for the operation
+ * @return MHD result code
+ */
+MHD_RESULT
+TEH_RESPONSE_reply_reserve_age_restriction_required (
+  struct MHD_Connection *connection,
+  uint16_t maximum_allowed_age);
+
 
 /**
  * Send information that a KYC check must be
diff --git a/src/exchange/taler-exchange-httpd_withdraw.c 
b/src/exchange/taler-exchange-httpd_withdraw.c
index 28addba4..9c8a405c 100644
--- a/src/exchange/taler-exchange-httpd_withdraw.c
+++ b/src/exchange/taler-exchange-httpd_withdraw.c
@@ -488,10 +488,13 @@ TEH_handler_withdraw (struct TEH_RequestContext *rc,
       GNUNET_JSON_parse_free (spec);
       return mret;
     }
-    dk = TEH_keys_denomination_by_hash2 (ksh,
-                                         &wc.collectable.denom_pub_hash,
-                                         NULL,
-                                         NULL);
+
+    dk = TEH_keys_denomination_by_hash_from_state (
+      ksh,
+      &wc.collectable.denom_pub_hash,
+      NULL,
+      NULL);
+
     if (NULL == dk)
     {
       if (! check_request_idempotent (rc,
diff --git a/src/exchangedb/0002-reserves.sql b/src/exchangedb/0002-reserves.sql
index df5b6c3d..3d345afd 100644
--- a/src/exchangedb/0002-reserves.sql
+++ b/src/exchangedb/0002-reserves.sql
@@ -31,7 +31,7 @@ BEGIN
       ',current_balance_frac INT4 NOT NULL DEFAULT(0)'
       ',purses_active INT8 NOT NULL DEFAULT(0)'
       ',purses_allowed INT8 NOT NULL DEFAULT(0)'
-      ',max_age INT4 NOT NULL DEFAULT(0)'
+      ',birthday INT4 NOT NULL DEFAULT(0)'
       ',expiration_date INT8 NOT NULL'
       ',gc_date INT8 NOT NULL'
     ') %s ;'
@@ -82,7 +82,7 @@ BEGIN
   );
   PERFORM comment_partitioned_column(
      'Birthday of the user in days after 1970, or 0 if user is an adult and is 
not subject to age restrictions'
-    ,'max_age'
+    ,'birthday'
     ,table_name
     ,partition_suffix
   );
diff --git a/src/exchangedb/0003-age_withdraw_commitments.sql 
b/src/exchangedb/0003-age_withdraw.sql
similarity index 52%
rename from src/exchangedb/0003-age_withdraw_commitments.sql
rename to src/exchangedb/0003-age_withdraw.sql
index d74a697c..1d296b05 100644
--- a/src/exchangedb/0003-age_withdraw_commitments.sql
+++ b/src/exchangedb/0003-age_withdraw.sql
@@ -1,6 +1,6 @@
 --
 -- This file is part of TALER
--- Copyright (C) 2022 Taler Systems SA
+-- Copyright (C) 2023 Taler Systems SA
 --
 -- TALER is free software; you can redistribute it and/or modify it under the
 -- terms of the GNU General Public License as published by the Free Software
@@ -13,33 +13,36 @@
 -- You should have received a copy of the GNU General Public License along with
 -- TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 --
+-- @author Özgür Kesim
 
-CREATE FUNCTION create_table_age_withdraw_commitments(
+CREATE FUNCTION create_table_age_withdraw(
   IN partition_suffix VARCHAR DEFAULT NULL
 )
 RETURNS VOID
 LANGUAGE plpgsql
 AS $$
 DECLARE
-  table_name VARCHAR DEFAULT 'age_withdraw_commitments';
+  table_name VARCHAR DEFAULT 'age_withdraw';
 BEGIN
   PERFORM create_partitioned_table(
     'CREATE TABLE %I'
-      '(age_withdraw_commitment_id BIGINT GENERATED BY DEFAULT AS IDENTITY'
-      ',h_commitment BYTEA CHECK (LENGTH(h_commitment)=64)'
-      ',amount_with_fee_val INT8 NOT NULL'
-      ',amount_with_fee_frac INT4 NOT NULL'
-      ',max_age INT2 NOT NULL'
-      ',reserve_pub BYTEA CHECK (LENGTH(reserve_pub)=32)'
-      ',reserve_sig BYTEA NOT NULL CHECK (LENGTH(reserve_sig)=64)'
-      ',noreveal_index INT4 NOT NULL'
+      '(age_withdraw_id BIGINT GENERATED BY DEFAULT AS IDENTITY'
+      ',h_commitment BYTEA NOT NULL CONSTRAINT h_commitment_length 
CHECK(LENGTH(h_commitment)=64)'
+      ',max_age SMALLINT NOT NULL CONSTRAINT max_age_positive 
CHECK(max_age>=0)'
+      ',reserve_pub BYTEA NOT NULL CONSTRAINT reserve_pub_length 
CHECK(LENGTH(reserve_pub)=32)'
+      ',reserve_sig BYTEA NOT NULL CONSTRAINT reserve_sig_length 
CHECK(LENGTH(reserve_sig)=64)'
+      ',noreveal_index SMALLINT NOT NULL CONSTRAINT noreveal_index_positive 
CHECK(noreveal_index>=0)'
+      ',h_blind_evs BYTEA[] NOT NULL CONSTRAINT h_blind_evs_length 
CHECK(cardinality(h_blind_evs)=cardinality(denom_serials))'
+      ',denom_serials INT8[] NOT NULL CONSTRAINT denom_serials_array_length 
CHECK(cardinality(denom_serials)=cardinality(denom_sigs))'
+      ',denom_sigs BYTEA[] NOT NULL CONSTRAINT denom_sigs_array_length 
CHECK(cardinality(denom_sigs)=cardinality(denom_serials))'
     ') %s ;'
     ,table_name
     ,'PARTITION BY HASH (reserve_pub)'
     ,partition_suffix
   );
   PERFORM comment_partitioned_table(
-     'Commitments made when withdrawing coins with age restriction and the 
gamma value chosen by the exchange.'
+     'Commitments made when withdrawing coins with age restriction and the 
gamma value chosen by the exchange. '
+     'It also contains the blindly signed coins, their signatures and 
denominations.'
     ,table_name
     ,partition_suffix
   );
@@ -68,23 +71,41 @@ BEGIN
     ,partition_suffix
   );
   PERFORM comment_partitioned_column(
-     'Signature of the reserve''s private key over the withdraw-age request'
+     'Signature of the reserve''s private key over the age-withdraw request'
     ,'reserve_sig'
     ,table_name
     ,partition_suffix
   );
+  PERFORM comment_partitioned_column(
+     'Array of references to the denominations'
+    ,'denom_serials'
+    ,table_name
+    ,partition_suffix
+  );
+  PERFORM comment_partitioned_column(
+     'Array of the blinded envelopes of the chosen fresh coins, with value as 
given by the denomination in the corresponding slot in denom_serials'
+    ,'h_blind_evs'
+    ,table_name
+    ,partition_suffix
+  );
+  PERFORM comment_partitioned_column(
+     'Array of signatures over each blinded envelope'
+    ,'denom_sigs'
+    ,table_name
+    ,partition_suffix
+  );
 END
 $$;
 
 
-CREATE FUNCTION constrain_table_age_withdraw_commitments(
+CREATE FUNCTION constrain_table_age_withdraw(
   IN partition_suffix VARCHAR
 )
 RETURNS void
 LANGUAGE plpgsql
 AS $$
 DECLARE
-  table_name VARCHAR DEFAULT 'age_withdraw_commitments';
+  table_name VARCHAR DEFAULT 'age_withdraw';
 BEGIN
   table_name = concat_ws('_', table_name, partition_suffix);
   EXECUTE FORMAT (
@@ -98,37 +119,37 @@ BEGIN
   );
   EXECUTE FORMAT (
     'ALTER TABLE ' || table_name ||
-    ' ADD CONSTRAINT ' || table_name || '_age_withdraw_commitment_id_key'
-    ' UNIQUE (age_withdraw_commitment_id);'
+    ' ADD CONSTRAINT ' || table_name || '_age_withdraw_id_key'
+    ' UNIQUE (age_withdraw_id);'
   );
 END
 $$;
 
 
-CREATE FUNCTION foreign_table_age_withdraw_commitments()
+CREATE FUNCTION foreign_table_age_withdraw()
 RETURNS void
 LANGUAGE plpgsql
 AS $$
 DECLARE
-  table_name VARCHAR DEFAULT 'age_withdraw_commitments';
+  table_name VARCHAR DEFAULT 'age_withdraw';
 BEGIN
   EXECUTE FORMAT (
     'ALTER TABLE ' || table_name ||
     ' ADD CONSTRAINT ' || table_name || '_foreign_reserve_pub'
     ' FOREIGN KEY (reserve_pub)'
-    ' REFERENCES reserves(reserve_pub) ON DELETE CASCADE;'
+    ' REFERENCES reserves(reserve_pub);' -- ON DELETE CASCADE;'
   );
 END
 $$;
 
 
-INSERT INTO exchange_tables 
+INSERT INTO exchange_tables
   (name
   ,version
   ,action
   ,partitioned
   ,by_range)
-VALUES 
-  ('age_withdraw_commitments', 'exchange-0003', 'create',   TRUE ,FALSE),
-  ('age_withdraw_commitments', 'exchange-0003', 'constrain',TRUE ,FALSE),
-  ('age_withdraw_commitments', 'exchange-0003', 'foreign',  TRUE ,FALSE);
+VALUES
+  ('age_withdraw', 'exchange-0003', 'create',   TRUE ,FALSE),
+  ('age_withdraw', 'exchange-0003', 'constrain',TRUE ,FALSE),
+  ('age_withdraw', 'exchange-0003', 'foreign',  TRUE ,FALSE);
diff --git a/src/exchangedb/0003-age_withdraw_reveals.sql 
b/src/exchangedb/0003-age_withdraw_reveals.sql
deleted file mode 100644
index 1c55fb19..00000000
--- a/src/exchangedb/0003-age_withdraw_reveals.sql
+++ /dev/null
@@ -1,152 +0,0 @@
---
--- This file is part of TALER
--- Copyright (C) 2022 Taler Systems SA
---
--- TALER is free software; you can redistribute it and/or modify it under the
--- terms of the GNU General Public License as published by the Free Software
--- Foundation; either version 3, or (at your option) any later version.
---
--- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
--- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
--- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
---
--- You should have received a copy of the GNU General Public License along with
--- TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
---
-
-CREATE FUNCTION create_table_age_withdraw_revealed_coins(
-  IN partition_suffix VARCHAR DEFAULT NULL
-)
-RETURNS VOID
-LANGUAGE plpgsql
-AS $$
-DECLARE
-  table_name VARCHAR DEFAULT 'age_withdraw_revealed_coins';
-BEGIN
-  PERFORM create_partitioned_table(
-    'CREATE TABLE %I'
-      '(age_withdraw_revealed_coins_id BIGINT GENERATED BY DEFAULT AS 
IDENTITY' -- UNIQUE
-      ',h_commitment BYTEA NOT NULL CHECK (LENGTH(h_commitment)=64)'
-      ',freshcoin_index INT4 NOT NULL'
-      ',denominations_serial INT8 NOT NULL'
-      ',coin_ev BYTEA NOT NULL'
-      ',h_coin_ev BYTEA CHECK (LENGTH(h_coin_ev)=64)'
-      ',ev_sig BYTEA NOT NULL'
-    ') %s ;'
-    ,table_name
-    ,'PARTITION BY HASH (h_commitment)'
-    ,partition_suffix
-  );
-  PERFORM comment_partitioned_table(
-     'Reveal of proofs of the correct age restriction after the commitment 
when withdrawing coins with age restriction'
-    ,table_name
-    ,partition_suffix
-  );
-  PERFORM comment_partitioned_column(
-     'Foreign key reference to the corresponding commitment'
-    ,'h_commitment'
-    ,table_name
-    ,partition_suffix
-  );
-  PERFORM comment_partitioned_column(
-     'Index of the coin in the withdraw-age request, which is implicitly a 
batch request'
-    ,'freshcoin_index'
-    ,table_name
-    ,partition_suffix
-  );
-  PERFORM comment_partitioned_column(
-     'Foreign key reference to the denominations'
-    ,'denominations_serial'
-    ,table_name
-    ,partition_suffix
-  );
-  PERFORM comment_partitioned_column(
-     'Envelope of the new coin to be signed'
-    ,'coin_ev'
-    ,table_name
-    ,partition_suffix
-  );
-  PERFORM comment_partitioned_column(
-     'Hash of the envelope of the new coin to be signed (for lookups)'
-    ,'h_coin_ev'
-    ,table_name
-    ,partition_suffix
-  );
-  PERFORM comment_partitioned_column(
-     'Exchange signature over the envelope'
-    ,'ev_sig'
-    ,table_name
-    ,partition_suffix
-  );
-END
-$$;
-
-CREATE FUNCTION constrain_table_age_withdraw_revealed_coins(
-  IN partition_suffix VARCHAR
-)
-RETURNS void
-LANGUAGE plpgsql
-AS $$
-DECLARE
-  table_name VARCHAR DEFAULT 'age_withdraw_revealed_coins';
-BEGIN
-  table_name = concat_ws('_', table_name, partition_suffix);
-
-  EXECUTE FORMAT (
-    'ALTER TABLE ' || table_name ||
-    ' ADD CONSTRAINT ' || table_name || '_age_withdraw_revealed_coins_id_key'
-    ' UNIQUE (age_withdraw_revealed_coins_id);'
-  );
-  EXECUTE FORMAT (
-    'ALTER TABLE ' || table_name ||
-    ' ADD CONSTRAINT ' || table_name || 
'_freshcoin_index_and_h_commitment_uniqueness'
-    ' UNIQUE (freshcoin_index, h_commitment);'
-  );
-END
-$$;
-
-CREATE FUNCTION foreign_table_age_withdraw_revealed_coins()
-RETURNS void
-LANGUAGE plpgsql
-AS $$
-DECLARE
-  table_name VARCHAR DEFAULT 'age_withdraw_revealed_coins';
-BEGIN
-  EXECUTE FORMAT (
-    'ALTER TABLE ' || table_name ||
-    ' ADD CONSTRAINT ' || table_name || '_foreign_h_commitment'
-    ' FOREIGN KEY (h_commitment)'
-    ' REFERENCES age_withdraw_commitments (h_commitment) ON DELETE CASCADE;'
-  );
-  EXECUTE FORMAT (
-    'ALTER TABLE ' || table_name ||
-    ' ADD CONSTRAINT ' || table_name || '_foreign_denominations_serial'
-    ' FOREIGN KEY (denominations_serial) '
-    ' REFERENCES denominations (denominations_serial) ON DELETE CASCADE;'
-  );
-END
-$$;
-
-
-INSERT INTO exchange_tables
-    (name
-    ,version
-    ,action
-    ,partitioned
-    ,by_range)
-  VALUES
-    ('age_withdraw_revealed_coins'
-    ,'exchange-0003'
-    ,'create'
-    ,TRUE
-    ,FALSE),
-    ('age_withdraw_revealed_coins'
-    ,'exchange-0003'
-    ,'constrain'
-    ,TRUE
-    ,FALSE),
-    ('age_withdraw_revealed_coins'
-    ,'exchange-0003'
-    ,'foreign'
-    ,TRUE
-    ,FALSE);
diff --git a/src/exchangedb/Makefile.am b/src/exchangedb/Makefile.am
index c2e87e5d..6d89decd 100644
--- a/src/exchangedb/Makefile.am
+++ b/src/exchangedb/Makefile.am
@@ -128,7 +128,8 @@ libtaler_plugin_exchangedb_postgres_la_SOURCES = \
   pg_drain_kyc_alert.h pg_drain_kyc_alert.c \
   pg_reserves_in_insert.h pg_reserves_in_insert.c \
   pg_get_withdraw_info.h pg_get_withdraw_info.c \
-  pg_get_age_withdraw_info.c pg_get_age_withdraw_info.h \
+  pg_do_age_withdraw.h pg_do_age_withdraw.c \
+  pg_get_age_withdraw.h pg_get_age_withdraw.c \
   pg_batch_ensure_coin_known.h pg_batch_ensure_coin_known.c \
   pg_do_batch_withdraw.h pg_do_batch_withdraw.c \
   pg_get_policy_details.h pg_get_policy_details.c \
diff --git a/src/exchangedb/exchange-0003.sql.in 
b/src/exchangedb/exchange-0003.sql.in
index 01733ea2..cd16bfbd 100644
--- a/src/exchangedb/exchange-0003.sql.in
+++ b/src/exchangedb/exchange-0003.sql.in
@@ -25,8 +25,7 @@ SET search_path TO exchange;
 #include "0003-aml_status.sql"
 #include "0003-aml_staff.sql"
 #include "0003-aml_history.sql"
-#include "0003-age_withdraw_commitments.sql"
-#include "0003-age_withdraw_reveals.sql"
+#include "0003-age_withdraw.sql"
 
 
 COMMIT;
diff --git a/src/exchangedb/exchange_do_age_withdraw.sql 
b/src/exchangedb/exchange_do_age_withdraw.sql
new file mode 100644
index 00000000..d6ae118a
--- /dev/null
+++ b/src/exchangedb/exchange_do_age_withdraw.sql
@@ -0,0 +1,177 @@
+--
+-- 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 General Public License as published by the Free Software
+-- Foundation; either version 3, or (at your option) any later version.
+--
+-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+-- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License along with
+-- TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+--
+-- @author Özgür Kesim
+
+CREATE OR REPLACE FUNCTION exchange_do_age_withdraw(
+  IN amount_val INT8,
+  IN amount_frac INT4,
+  IN rpub BYTEA,
+  IN rsig BYTEA,
+  IN now INT8,
+  IN min_reserve_gc INT8,
+  IN h_commitment BYTEA,
+  IN maximum_age_committed INT2, -- in years ϵ [0,1..)
+  IN noreveal_index INT2,
+  IN blinded_evs BYTEA[],
+  IN denom_serials INT8[],
+  IN denom_sigs BYTEA[],
+  OUT reserve_found BOOLEAN,
+  OUT balance_ok BOOLEAN,
+  OUT age_ok BOOLEAN,
+  OUT required_age INT2, -- in years ϵ [0,1..)
+  OUT conflict BOOLEAN)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+  reserve_gc INT8;
+  reserve_val INT8;
+  reserve_frac INT4;
+  reserve_birthday INT4;
+  not_before date;
+  earliest_date date;
+BEGIN
+-- Shards: reserves by reserve_pub (SELECT)
+--         reserves_out (INSERT, with CONFLICT detection) by wih
+--         reserves by reserve_pub (UPDATE)
+--         reserves_in by reserve_pub (SELECT)
+--         wire_targets by wire_target_h_payto
+
+SELECT
+   current_balance_val
+  ,current_balance_frac
+  ,gc_date
+  ,birthday
+ INTO
+   reserve_val
+  ,reserve_frac
+  ,reserve_gc
+  ,reserve_birthday
+  FROM exchange.reserves
+ WHERE reserves.reserve_pub=rpub;
+
+IF NOT FOUND
+THEN
+  -- reserve unknown
+  reserve_found=FALSE;
+  balance_ok=FALSE;
+  age_ok=FALSE;
+  required_age=0;
+  conflict=FALSE;
+  RETURN;
+END IF;
+
+
+-- Check age requirements
+IF ((maximum_age_committed = 0) OR (reserve_birthday = 0))
+THEN
+  -- No commitment to a non-zero age was provided or the reserve is marked as
+  -- having no age restriction. We can simply pass.
+  age_ok = OK;
+ELSE 
+  not_before=date '1970-01-01' + reserve_birthday;
+  earliest_date = current_date - make_interval(maximum_age_committed);
+  --
+  -- 1970-01-01 + birthday == not_before                 now
+  --     |                     |                          |
+  -- <.......not allowed......>[<.....allowed range......>]
+  --     |                     |                          |
+  -- ____*_____________________*_________*________________*  timeline
+  --                                     |
+  --                            earliest_date ==
+  --                                now - maximum_age_committed*year
+  --
+  IF (earliest_date < not_before)
+  THEN
+    reserve_found = TRUE;
+    balance_ok = FALSE;
+    age_ok = FALSE;
+    required_age = extract(year from age(not_before, current_date)) + 1;
+    RETURN;
+  END IF;
+END IF;
+
+-- Check reserve balance is sufficient.
+IF (reserve_val > amount_val)
+THEN
+  IF (reserve_frac >= amount_frac)
+  THEN
+    reserve_val=reserve_val - amount_val;
+    reserve_frac=reserve_frac - amount_frac;
+  ELSE
+    reserve_val=reserve_val - amount_val - 1;
+    reserve_frac=reserve_frac + 100000000 - amount_frac;
+  END IF;
+ELSE
+  IF (reserve_val = amount_val) AND (reserve_frac >= amount_frac)
+  THEN
+    reserve_val=0;
+    reserve_frac=reserve_frac - amount_frac;
+  ELSE
+    balance_ok=FALSE;
+    RETURN;
+  END IF;
+END IF;
+
+-- Calculate new expiration dates.
+min_reserve_gc=GREATEST(min_reserve_gc,reserve_gc);
+
+-- Update reserve balance.
+UPDATE reserves SET
+  gc_date=min_reserve_gc
+ ,current_balance_val=reserve_val
+ ,current_balance_frac=reserve_frac
+WHERE
+  reserves.reserve_pub=rpub;
+
+reserve_found=TRUE;
+balance_ok=TRUE;
+
+-- Write the commitment into the age-withdraw table
+INSERT INTO exchange.age_withdraw
+  (h_commitment
+  ,max_age
+  ,reserve_pub
+  ,reserve_sig
+  ,noreveal_index
+  ,denomination_serials
+  ,h_blind_evs
+  ,denom_sigs)
+VALUES
+  (h_commitment
+  ,maximum_age_committed
+  ,rpub
+  ,rsig
+  ,noreveal_index
+  ,denom_serials
+  ,blinded_evs
+  ,denom_sigs)
+ON CONFLICT DO NOTHING;
+
+IF NOT FOUND
+THEN
+  -- Signal a conflict so that the caller
+  -- can fetch the actual data from the DB.
+  conflict=TRUE;
+  RETURN;
+ELSE
+  conflict=FALSE;
+END IF;
+
+END $$;
+
+COMMENT ON FUNCTION exchange_do_age_withdraw(INT8, INT4, BYTEA, BYTEA, INT8, 
INT8, BYTEA, INT2, INT2, BYTEA[], INT8[], BYTEA[])
+  IS 'Checks whether the reserve has sufficient balance for an age-withdraw 
operation (or the request is repeated and was previously approved) and that age 
requirements are met. If so updates the database with the result. Includes 
storing the blinded planchets and denomination signatures, or signaling 
conflict';
+
diff --git a/src/exchangedb/exchange_do_batch_withdraw.sql 
b/src/exchangedb/exchange_do_batch_withdraw.sql
index fedb7e91..53e90844 100644
--- a/src/exchangedb/exchange_do_batch_withdraw.sql
+++ b/src/exchangedb/exchange_do_batch_withdraw.sql
@@ -1,6 +1,6 @@
 --
 -- This file is part of TALER
--- Copyright (C) 2014--2022 Taler Systems SA
+-- Copyright (C) 2014--2023 Taler Systems SA
 --
 -- TALER is free software; you can redistribute it and/or modify it under the
 -- terms of the GNU General Public License as published by the Free Software
@@ -13,6 +13,8 @@
 -- You should have received a copy of the GNU General Public License along with
 -- TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 --
+-- @author Christian Grothoff
+-- @author Özgür Kesim
 
 CREATE OR REPLACE FUNCTION exchange_do_batch_withdraw(
   IN amount_val INT8,
@@ -20,17 +22,20 @@ CREATE OR REPLACE FUNCTION exchange_do_batch_withdraw(
   IN rpub BYTEA,
   IN now INT8,
   IN min_reserve_gc INT8,
+  IN do_age_check BOOLEAN,
   OUT reserve_found BOOLEAN,
   OUT balance_ok BOOLEAN,
+  OUT age_ok BOOLEAN,
+  OUT allowed_maximum_age INT2, -- in years
   OUT ruuid INT8)
 LANGUAGE plpgsql
 AS $$
 DECLARE
   reserve_gc INT8;
-DECLARE
   reserve_val INT8;
-DECLARE
   reserve_frac INT4;
+  reserve_birthday INT4;
+  not_before date;
 BEGIN
 -- Shards: reserves by reserve_pub (SELECT)
 --         reserves_out (INSERT, with CONFLICT detection) by wih
@@ -38,15 +43,18 @@ BEGIN
 --         reserves_in by reserve_pub (SELECT)
 --         wire_targets by wire_target_h_payto
 
+
 SELECT
    current_balance_val
   ,current_balance_frac
   ,gc_date
+  ,birthday
   ,reserve_uuid
  INTO
    reserve_val
   ,reserve_frac
   ,reserve_gc
+  ,reserve_birthday
   ,ruuid
   FROM exchange.reserves
  WHERE reserves.reserve_pub=rpub;
@@ -56,10 +64,33 @@ THEN
   -- reserve unknown
   reserve_found=FALSE;
   balance_ok=FALSE;
+  age_ok=FALSE;
+  allowed_maximum_age=0;
   ruuid=2;
   RETURN;
 END IF;
 
+
+-- Check if age requirements are present
+IF ((NOT do_age_check) OR (reserve_birthday = 0))
+THEN
+  age_ok = TRUE;
+  allowed_maximum_age = -1;
+ELSE
+  -- Age requirements are formally not met:  The exchange is setup to support
+  -- age restrictions (do_age_check == TRUE) and the reserve has a
+  -- birthday set (reserve_birthday != 0), but the client called the
+  -- batch-withdraw endpoint instead of the age-withdraw endpoint, which it
+  -- should have.
+  not_before=date '1970-01-01' + reserve_birthday;
+  allowed_maximum_age = extract(year from age(current_date, not_before));
+
+  reserve_found=TRUE;
+  balance_ok=FALSE;
+  age_ok = FALSE;
+  RETURN;
+END IF;
+
 -- Check reserve balance is sufficient.
 IF (reserve_val > amount_val)
 THEN
@@ -77,7 +108,6 @@ ELSE
     reserve_val=0;
     reserve_frac=reserve_frac - amount_frac;
   ELSE
-    reserve_found=TRUE;
     balance_ok=FALSE;
     RETURN;
   END IF;
@@ -99,8 +129,6 @@ balance_ok=TRUE;
 
 END $$;
 
-COMMENT ON FUNCTION exchange_do_batch_withdraw(INT8, INT4, BYTEA, INT8, INT8)
-  IS 'Checks whether the reserve has sufficient balance for a withdraw 
operation (or the request is repeated and was previously approved) and if so 
updates the database with the result. Excludes storing the planchets.';
-
-
+COMMENT ON FUNCTION exchange_do_batch_withdraw(INT8, INT4, BYTEA, INT8, INT8, 
BOOLEAN)
+  IS 'Checks whether the reserve has sufficient balance for a withdraw 
operation (or the request is repeated and was previously approved) and that age 
requirements are formally met. If so updates the database with the result. 
Excludes storing the planchets.';
 
diff --git a/src/exchangedb/exchange_do_batch_withdraw_insert.sql 
b/src/exchangedb/exchange_do_batch_withdraw_insert.sql
index 98db840f..b580f6fb 100644
--- a/src/exchangedb/exchange_do_batch_withdraw_insert.sql
+++ b/src/exchangedb/exchange_do_batch_withdraw_insert.sql
@@ -14,14 +14,11 @@
 -- TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 --
 
-
-
-
 CREATE OR REPLACE FUNCTION exchange_do_batch_withdraw_insert(
   IN cs_nonce BYTEA,
   IN amount_val INT8,
   IN amount_frac INT4,
-  IN h_denom_pub BYTEA,
+  IN h_denom_pub BYTEA, -- FIXME: denom_serials should really be a parameter 
to this FUNCTION.
   IN ruuid INT8,
   IN reserve_sig BYTEA,
   IN h_coin_envelope BYTEA,
@@ -45,6 +42,8 @@ out_denom_unknown=TRUE;
 out_conflict=TRUE;
 out_nonce_reuse=TRUE;
 
+-- FIXME: denom_serials should really be a parameter to this FUNCTION.
+
 SELECT denominations_serial
   INTO denom_serial
   FROM exchange.denominations
diff --git a/src/exchangedb/exchange_do_insert_kyc_attributes.sql 
b/src/exchangedb/exchange_do_insert_kyc_attributes.sql
index 5bb199bb..ae6a6575 100644
--- a/src/exchangedb/exchange_do_insert_kyc_attributes.sql
+++ b/src/exchangedb/exchange_do_insert_kyc_attributes.sql
@@ -61,7 +61,7 @@ UPDATE exchange.legitimization_processes
 out_ok = FOUND;
 
 -- FIXME-Oec: update exchange reserve table to store in_birthday here!
--- UPDATE exchange.reserves SET max_age=in_birthday WHERE reserve_pub=X;
+-- UPDATE exchange.reserves SET birthday=in_birthday WHERE reserve_pub=X;
 
 IF in_require_aml
 THEN
diff --git a/src/exchangedb/pg_do_batch_withdraw.c 
b/src/exchangedb/pg_do_age_withdraw.c
similarity index 54%
copy from src/exchangedb/pg_do_batch_withdraw.c
copy to src/exchangedb/pg_do_age_withdraw.c
index 3dee20d2..8a93ef8d 100644
--- a/src/exchangedb/pg_do_batch_withdraw.c
+++ b/src/exchangedb/pg_do_age_withdraw.c
@@ -1,6 +1,6 @@
 /*
    This file is part of TALER
-   Copyright (C) 2022 Taler Systems SA
+   Copyright (C) 2023 Taler Systems SA
 
    TALER is free software; you can redistribute it and/or modify it under the
    terms of the GNU General Public License as published by the Free Software
@@ -16,33 +16,51 @@
 /**
  * @file exchangedb/pg_do_batch_withdraw.c
  * @brief Implementation of the do_batch_withdraw function for Postgres
- * @author Christian Grothoff
+ * @author Özgür Kesim
  */
 #include "platform.h"
 #include "taler_error_codes.h"
 #include "taler_dbevents.h"
+#include "taler_exchangedb_plugin.h"
+#include "taler_pq_lib.h"
 #include "taler_pq_lib.h"
 #include "pg_do_batch_withdraw.h"
 #include "pg_helper.h"
+#include <gnunet/gnunet_time_lib.h>
 
 
 enum GNUNET_DB_QueryStatus
-TEH_PG_do_batch_withdraw (
+TEH_PG_do_age_withdraw (
   void *cls,
+  const struct TALER_EXCHANGEDB_AgeWithdraw *commitment,
   struct GNUNET_TIME_Timestamp now,
-  const struct TALER_ReservePublicKeyP *reserve_pub,
-  const struct TALER_Amount *amount,
   bool *found,
   bool *balance_ok,
+  bool *age_ok,
+  uint16_t *required_age,
+  bool *conflict,
   uint64_t *ruuid)
 {
   struct PostgresClosure *pg = cls;
   struct GNUNET_TIME_Timestamp gc;
   struct GNUNET_PQ_QueryParam params[] = {
-    TALER_PQ_query_param_amount (amount),
-    GNUNET_PQ_query_param_auto_from_type (reserve_pub),
+    TALER_PQ_query_param_amount (&commitment->amount_with_fee),
+    GNUNET_PQ_query_param_auto_from_type (&commitment->reserve_pub),
+    GNUNET_PQ_query_param_auto_from_type (&commitment->reserve_sig),
     GNUNET_PQ_query_param_timestamp (&now),
     GNUNET_PQ_query_param_timestamp (&gc),
+    GNUNET_PQ_query_param_auto_from_type (&commitment->h_commitment),
+    GNUNET_PQ_query_param_uint16 (&commitment->max_age),
+    GNUNET_PQ_query_param_uint16 (&commitment->noreveal_index),
+    GNUNET_PQ_query_param_array_auto_from_type (commitment->num_coins,
+                                                commitment->h_coin_evs,
+                                                pg->conn),
+    GNUNET_PQ_query_param_array_uint64 (commitment->num_coins,
+                                        commitment->denom_serials,
+                                        pg->conn),
+    GNUNET_PQ_query_param_array_auto_from_type (commitment->num_coins,
+                                                commitment->denom_sigs,
+                                                pg->conn),
     GNUNET_PQ_query_param_end
   };
   struct GNUNET_PQ_ResultSpec rs[] = {
@@ -50,6 +68,12 @@ TEH_PG_do_batch_withdraw (
                                 found),
     GNUNET_PQ_result_spec_bool ("balance_ok",
                                 balance_ok),
+    GNUNET_PQ_result_spec_bool ("age_ok",
+                                age_ok),
+    GNUNET_PQ_result_spec_uint16 ("required_age",
+                                  required_age),
+    GNUNET_PQ_result_spec_bool ("conflict",
+                                conflict),
     GNUNET_PQ_result_spec_uint64 ("ruuid",
                                   ruuid),
     GNUNET_PQ_result_spec_end
@@ -60,18 +84,21 @@ TEH_PG_do_batch_withdraw (
                               pg->legal_reserve_expiration_time));
 
 
-  /* Used in #postgres_do_batch_withdraw() to
+  /* Used in #postgres_do_age_withdraw() to
         update the reserve balance and check its status */
   PREPARE (pg,
-           "call_batch_withdraw",
+           "call_age_withdraw",
            "SELECT "
            " reserve_found"
            ",balance_ok"
+           ",age_ok"
+           ",required_age"
+           ",conflict"
            ",ruuid"
            " FROM exchange_do_batch_withdraw"
-           " ($1,$2,$3,$4,$5);");
+           " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12);");
   return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
-                                                   "call_batch_withdraw",
+                                                   "call_age_withdraw",
                                                    params,
                                                    rs);
 }
diff --git a/src/exchangedb/pg_do_withdraw.h 
b/src/exchangedb/pg_do_age_withdraw.h
similarity index 53%
copy from src/exchangedb/pg_do_withdraw.h
copy to src/exchangedb/pg_do_age_withdraw.h
index 406785c4..8f42bfb5 100644
--- a/src/exchangedb/pg_do_withdraw.h
+++ b/src/exchangedb/pg_do_age_withdraw.h
@@ -1,6 +1,6 @@
 /*
    This file is part of TALER
-   Copyright (C) 2022 Taler Systems SA
+   Copyright (C) 2023 Taler Systems SA
 
    TALER is free software; you can redistribute it and/or modify it under the
    terms of the GNU General Public License as published by the Free Software
@@ -14,40 +14,40 @@
    TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 /**
- * @file exchangedb/pg_do_withdraw.h
- * @brief implementation of the do_withdraw function for Postgres
- * @author Christian Grothoff
+ * @file exchangedb/pg_do_age_withdraw.h
+ * @brief implementation of the do_age_withdraw function for Postgres
+ * @author Özgür Kesim
  */
-#ifndef PG_DO_WITHDRAW_H
-#define PG_DO_WITHDRAW_H
+#ifndef PG_DO_AGE_WITHDRAW_H
+#define PG_DO_AGE_WITHDRAW_H
 
 #include "taler_util.h"
 #include "taler_json_lib.h"
 #include "taler_exchangedb_plugin.h"
-
 /**
- * Perform withdraw operation, checking for sufficient balance
- * and possibly persisting the withdrawal details.
+ * Perform reserve update as part of an age-withdraw operation, checking for
+ * sufficient balance and fulfillment of age requirements. Finally persisting
+ * the withdrawal details.
  *
  * @param cls the `struct PostgresClosure` with the plugin-specific state
- * @param nonce client-contributed input for CS denominations that must be 
checked for idempotency, or NULL for non-CS withdrawals
- * @param[in,out] collectable corresponding collectable coin (blind signature) 
if a coin is found; possibly updated if a (different) signature exists already
+ * @param commitment the commitment with all parameters
  * @param now current time (rounded)
  * @param[out] found set to true if the reserve was found
  * @param[out] balance_ok set to true if the balance was sufficient
- * @param[out] nonce_ok set to false if the nonce was reused
- * @param[out] ruuid set to the reserve's UUID (reserves table row)
+ * @param[out] age_ok set to true if no age requirements are present on the 
reserve
+ * @param[out] required_age if @e age_ok is false, set to the maximum allowed 
age when withdrawing from this reserve
+ * @param[out] conflict set to true if there already is an entry in the 
database for the given pair (h_commitment, reserve_pub)
  * @return query execution status
  */
 enum GNUNET_DB_QueryStatus
-TEH_PG_do_withdraw (
+TEH_PG_do_age_withdraw (
   void *cls,
-  const struct TALER_CsNonce *nonce,
-  const struct TALER_EXCHANGEDB_CollectableBlindcoin *collectable,
-  struct GNUNET_TIME_Timestamp now,
+  const struct TALER_EXCHANGEDB_AgeWithdraw *commitment,
+  const struct GNUNET_TIME_Timestamp now,
   bool *found,
   bool *balance_ok,
-  bool *nonce_ok,
-  uint64_t *ruuid);
+  bool *age_ok,
+  uint16_t *required_age,
+  bool *conflict);
 
 #endif
diff --git a/src/exchangedb/pg_do_batch_withdraw.c 
b/src/exchangedb/pg_do_batch_withdraw.c
index 3dee20d2..45b1d322 100644
--- a/src/exchangedb/pg_do_batch_withdraw.c
+++ b/src/exchangedb/pg_do_batch_withdraw.c
@@ -17,6 +17,7 @@
  * @file exchangedb/pg_do_batch_withdraw.c
  * @brief Implementation of the do_batch_withdraw function for Postgres
  * @author Christian Grothoff
+ * @author Özgür Kesim
  */
 #include "platform.h"
 #include "taler_error_codes.h"
@@ -32,8 +33,11 @@ TEH_PG_do_batch_withdraw (
   struct GNUNET_TIME_Timestamp now,
   const struct TALER_ReservePublicKeyP *reserve_pub,
   const struct TALER_Amount *amount,
+  bool do_age_check,
   bool *found,
   bool *balance_ok,
+  bool *age_ok,
+  uint16_t *allowed_maximum_age,
   uint64_t *ruuid)
 {
   struct PostgresClosure *pg = cls;
@@ -43,6 +47,7 @@ TEH_PG_do_batch_withdraw (
     GNUNET_PQ_query_param_auto_from_type (reserve_pub),
     GNUNET_PQ_query_param_timestamp (&now),
     GNUNET_PQ_query_param_timestamp (&gc),
+    GNUNET_PQ_query_param_bool (do_age_check),
     GNUNET_PQ_query_param_end
   };
   struct GNUNET_PQ_ResultSpec rs[] = {
@@ -50,6 +55,10 @@ TEH_PG_do_batch_withdraw (
                                 found),
     GNUNET_PQ_result_spec_bool ("balance_ok",
                                 balance_ok),
+    GNUNET_PQ_result_spec_bool ("age_ok",
+                                age_ok),
+    GNUNET_PQ_result_spec_uint16 ("allowed_maximum_age",
+                                  allowed_maximum_age),
     GNUNET_PQ_result_spec_uint64 ("ruuid",
                                   ruuid),
     GNUNET_PQ_result_spec_end
@@ -67,9 +76,11 @@ TEH_PG_do_batch_withdraw (
            "SELECT "
            " reserve_found"
            ",balance_ok"
+           ",age_ok"
+           ",allowed_maximum_age"
            ",ruuid"
            " FROM exchange_do_batch_withdraw"
-           " ($1,$2,$3,$4,$5);");
+           " ($1,$2,$3,$4,$5,$6);");
   return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
                                                    "call_batch_withdraw",
                                                    params,
diff --git a/src/exchangedb/pg_do_batch_withdraw.h 
b/src/exchangedb/pg_do_batch_withdraw.h
index ee4bf293..d0b86574 100644
--- a/src/exchangedb/pg_do_batch_withdraw.h
+++ b/src/exchangedb/pg_do_batch_withdraw.h
@@ -33,8 +33,11 @@
  * @param now current time (rounded)
  * @param reserve_pub public key of the reserve to debit
  * @param amount total amount to withdraw
+ * @param age_check_required if true, fail if age requirements are set on the 
reserve
  * @param[out] found set to true if the reserve was found
  * @param[out] balance_ok set to true if the balance was sufficient
+ * @param[out] age_ok set to true if no age requirements are present on the 
reserve
+ * @param[out] allowed_maximum_age if @e age_ok is false, set to the maximum 
allowed age when withdrawing from this reserve (client needs to call 
age-withdraw)
  * @param[out] ruuid set to the reserve's UUID (reserves table row)
  * @return query execution status
  */
@@ -44,8 +47,11 @@ TEH_PG_do_batch_withdraw (
   struct GNUNET_TIME_Timestamp now,
   const struct TALER_ReservePublicKeyP *reserve_pub,
   const struct TALER_Amount *amount,
+  bool age_check_required,
   bool *found,
   bool *balance_ok,
+  bool *age_ok,
+  uint16_t *allowed_maximum_age,
   uint64_t *ruuid);
 
 #endif
diff --git a/src/exchangedb/pg_get_age_withdraw_info.c 
b/src/exchangedb/pg_get_age_withdraw.c
similarity index 54%
rename from src/exchangedb/pg_get_age_withdraw_info.c
rename to src/exchangedb/pg_get_age_withdraw.c
index f4a68b37..5e123ca9 100644
--- a/src/exchangedb/pg_get_age_withdraw_info.c
+++ b/src/exchangedb/pg_get_age_withdraw.c
@@ -14,24 +14,24 @@
    TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 /**
- * @file exchangedb/pg_get_age_withdraw_info.c
- * @brief Implementation of the get_age_withdraw_info function for Postgres
+ * @file exchangedb/pg_get_age_withdraw.c
+ * @brief Implementation of the get_age_withdraw function for Postgres
  * @author Özgür Kesim
  */
 #include "platform.h"
 #include "taler_error_codes.h"
 #include "taler_dbevents.h"
 #include "taler_pq_lib.h"
-#include "pg_get_age_withdraw_info.h"
+#include "pg_get_age_withdraw.h"
 #include "pg_helper.h"
 
 
 enum GNUNET_DB_QueryStatus
-TEH_PG_get_age_withdraw_info (
+TEH_PG_get_age_withdraw (
   void *cls,
   const struct TALER_ReservePublicKeyP *reserve_pub,
   const struct TALER_AgeWithdrawCommitmentHashP *ach,
-  struct TALER_EXCHANGEDB_AgeWithdrawCommitment *awc)
+  struct TALER_EXCHANGEDB_AgeWithdraw *aw)
 {
   struct PostgresClosure *pg = cls;
   struct GNUNET_PQ_QueryParam params[] = {
@@ -41,26 +41,44 @@ TEH_PG_get_age_withdraw_info (
   };
   struct GNUNET_PQ_ResultSpec rs[] = {
     GNUNET_PQ_result_spec_auto_from_type ("h_commitment",
-                                          &awc->h_commitment),
+                                          &aw->h_commitment),
     GNUNET_PQ_result_spec_auto_from_type ("reserve_sig",
-                                          &awc->reserve_sig),
+                                          &aw->reserve_sig),
     GNUNET_PQ_result_spec_auto_from_type ("reserve_pub",
-                                          &awc->reserve_pub),
+                                          &aw->reserve_pub),
     GNUNET_PQ_result_spec_uint16 ("max_age",
-                                  &awc->max_age),
+                                  &aw->max_age),
     TALER_PQ_RESULT_SPEC_AMOUNT ("amount_with_fee",
-                                 &awc->amount_with_fee),
-    GNUNET_PQ_result_spec_uint32 ("noreveal_index",
-                                  &awc->noreveal_index),
+                                 &aw->amount_with_fee),
+    GNUNET_PQ_result_spec_uint16 ("noreveal_index",
+                                  &aw->noreveal_index),
+    GNUNET_PQ_result_spec_array_fixed_size (
+      pg->conn,
+      "h_coin_evs",
+      sizeof(struct TALER_BlindedPlanchet),
+      &aw->num_coins,
+      (void **) &aw->h_coin_evs),
+    GNUNET_PQ_result_spec_array_fixed_size (
+      pg->conn,
+      "denom_sigs",
+      sizeof(struct TALER_DenominationSignature),
+      NULL,
+      (void **) &aw->denom_sigs),
+    GNUNET_PQ_result_spec_array_fixed_size (
+      pg->conn,
+      "denom_pub_hashes",
+      sizeof(struct TALER_DenominationHashP),
+      NULL,
+      (void **) &aw->denom_pub_hashes),
     GNUNET_PQ_result_spec_end
   };
 
-  /* Used in #postgres_get_age_withdraw_info() to
-     locate the response for a /reserve/$RESERVE_PUB/age-withdraw request using
-     the hash of the blinded message.  Used to make sure
-     /reserve/$RESERVE_PUB/age-withdraw requests are idempotent. */
+  /* Used in #postgres_get_age_withdraw() to
+     locate the response for a /reserve/$RESERVE_PUB/age-withdraw request
+     using the hash of the blinded message.  Also needed to ensure
+     idempotency of /reserve/$RESERVE_PUB/age-withdraw requests. */
   PREPARE (pg,
-           "get_age_withdraw_info",
+           "get_age_withdraw",
            "SELECT"
            " h_commitment"
            ",reserve_sig"
@@ -69,10 +87,20 @@ TEH_PG_get_age_withdraw_info (
            ",amount_with_fee_val"
            ",amount_with_fee_frac"
            ",noreveal_index"
-           " FROM age_withdraw_commitments"
+           ",h_coin_evs"
+           ",denom_sigs"
+           ",ARRAY("
+           "  SELECT denominations.denom_pub_hash FROM ("
+           "    SELECT UNNEST(denomination_serials) AS id,"
+           "           generate_subscripts(denominations_serials, 1) AS nr" /* 
for order */
+           "  ) AS denoms"
+           "  LEFT JOIN denominations ON 
denominations.denominations_serial=denoms.id"
+           ") AS denom_pub_hashes"
+           " FROM age_withdraw"
            " WHERE reserve_pub=$1 and h_commitment=$2;");
+
   return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
-                                                   "get_age_withdraw_info",
+                                                   "get_age_withdraw",
                                                    params,
                                                    rs);
 }
diff --git a/src/exchangedb/pg_get_age_withdraw_info.h 
b/src/exchangedb/pg_get_age_withdraw.h
similarity index 79%
rename from src/exchangedb/pg_get_age_withdraw_info.h
rename to src/exchangedb/pg_get_age_withdraw.h
index 0844b1a1..2257aa43 100644
--- a/src/exchangedb/pg_get_age_withdraw_info.h
+++ b/src/exchangedb/pg_get_age_withdraw.h
@@ -14,12 +14,12 @@
    TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 /**
- * @file exchangedb/pg_get_age_withdraw_info.h
- * @brief implementation of the get_age_withdraw_info function for Postgres
+ * @file exchangedb/pg_get_age_withdraw.h
+ * @brief implementation of the get_age_withdraw function for Postgres
  * @author Özgür KESIM
  */
-#ifndef PG_GET_AGE_WITHDRAW_INFO_H
-#define PG_GET_AGE_WITHDRAW_INFO_H
+#ifndef PG_GET_AGE_WITHDRAW_H
+#define PG_GET_AGE_WITHDRAW_H
 
 #include "taler_util.h"
 #include "taler_json_lib.h"
@@ -33,13 +33,13 @@
    * @param cls the @e cls of this struct with the plugin-specific state
    * @param reserve_pub public key of the reserve for which the age-withdraw 
request is made
    * @param ach hash that uniquely identifies the age-withdraw operation
-   * @param[out] awc corresponding details of the previous age-withdraw 
request if an entry was found
+   * @param[out] aw corresponding details of the previous age-withdraw request 
if an entry was found
    * @return statement execution status
    */
 enum GNUNET_DB_QueryStatus
-TEH_PG_get_age_withdraw_info (
+TEH_PG_get_age_withdraw (
   void *cls,
   const struct TALER_ReservePublicKeyP *reserve_pub,
   const struct TALER_AgeWithdrawCommitmentHashP *ach,
-  struct TALER_EXCHANGEDB_AgeWithdrawCommitment *awc);
+  struct TALER_EXCHANGEDB_AgeWithdraw *aw);
 #endif
diff --git a/src/exchangedb/pg_insert_age_withdraw_reveal.c 
b/src/exchangedb/pg_insert_age_withdraw_reveal.c
deleted file mode 100644
index ebba7ebb..00000000
--- a/src/exchangedb/pg_insert_age_withdraw_reveal.c
+++ /dev/null
@@ -1,106 +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 General Public License as published by the Free Software
-   Foundation; either version 3, or (at your option) any later version.
-
-   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
-   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
-   A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
-   You should have received a copy of the GNU General Public License along with
-   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-/**
- * @file exchangedb/pg_insert_age_withdraw_reveal.c
- * @brief Implementation of the insert_age_withdraw_reveal function for 
Postgres
- * @author Özgür Kesim
- */
-#include "platform.h"
-#include "taler_error_codes.h"
-#include "taler_dbevents.h"
-#include "taler_pq_lib.h"
-#include "pg_insert_refresh_reveal.h"
-#include "pg_helper.h"
-
-
-enum GNUNET_DB_QueryStatus
-TEH_PG_insert_age_withdraw_reveal (
-  void *cls,
-  /*TODO:oec*/
-  )
-{
-  struct PostgresClosure *pg = cls;
-
-  if (TALER_CNC_KAPPA != num_tprivs + 1)
-  {
-    GNUNET_break (0);
-    return GNUNET_DB_STATUS_HARD_ERROR;
-  }
-  /* TODO */
-#if 0
-  PREPARE (pg,
-           "insert_age_withdraw_revealed_coin",
-           "INSERT INTO age_withdraw_reveals "
-           "(h_commitment "
-           ",freshcoin_index "
-           ",denominations_serial "
-           ",h_coin_ev "
-           ",ev_sig"
-           ") SELECT $1, $2, $3, "
-           "         denominations_serial, $5, $6, $7, $8"
-           "    FROM denominations"
-           "   WHERE denom_pub_hash=$4"
-
-           " ON CONFLICT DO NOTHING;");
-  for (uint32_t i = 0; i<num_rrcs; i++)
-  {
-    const struct TALER_EXCHANGEDB_RefreshRevealedCoin *rrc = &rrcs[i];
-    struct GNUNET_PQ_QueryParam params[] = {
-      GNUNET_PQ_query_param_uint64 (&melt_serial_id),
-      GNUNET_PQ_query_param_uint32 (&i),
-      GNUNET_PQ_query_param_auto_from_type (&rrc->orig_coin_link_sig),
-      GNUNET_PQ_query_param_auto_from_type (&rrc->h_denom_pub),
-      TALER_PQ_query_param_blinded_planchet (&rrc->blinded_planchet),
-      TALER_PQ_query_param_exchange_withdraw_values (&rrc->exchange_vals),
-      GNUNET_PQ_query_param_auto_from_type (&rrc->coin_envelope_hash),
-      TALER_PQ_query_param_blinded_denom_sig (&rrc->coin_sig),
-      GNUNET_PQ_query_param_end
-    };
-    enum GNUNET_DB_QueryStatus qs;
-
-    qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
-                                             "insert_refresh_revealed_coin",
-                                             params);
-    if (0 > qs)
-      return qs;
-  }
-
-  {
-    struct GNUNET_PQ_QueryParam params[] = {
-      GNUNET_PQ_query_param_uint64 (&melt_serial_id),
-      GNUNET_PQ_query_param_auto_from_type (tp),
-      GNUNET_PQ_query_param_fixed_size (
-        tprivs,
-        num_tprivs * sizeof (struct TALER_TransferPrivateKeyP)),
-      GNUNET_PQ_query_param_end
-    };
-
-    /* Used in #postgres_insert_refresh_reveal() to store the transfer
-   keys we learned */
-    PREPARE (pg,
-             "insert_refresh_transfer_keys",
-             "INSERT INTO refresh_transfer_keys "
-             "(melt_serial_id"
-             ",transfer_pub"
-             ",transfer_privs"
-             ") VALUES ($1, $2, $3)"
-             " ON CONFLICT DO NOTHING;");
-    return GNUNET_PQ_eval_prepared_non_select (pg->conn,
-                                               "insert_refresh_transfer_keys",
-                                               params);
-  }
-#endif
-}
diff --git a/src/exchangedb/pg_insert_age_withdraw_reveal.h 
b/src/exchangedb/pg_insert_age_withdraw_reveal.h
deleted file mode 100644
index 1ce05d59..00000000
--- a/src/exchangedb/pg_insert_age_withdraw_reveal.h
+++ /dev/null
@@ -1,43 +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 General Public License as published by the Free Software
-   Foundation; either version 3, or (at your option) any later version.
-
-   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
-   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
-   A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
-   You should have received a copy of the GNU General Public License along with
-   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-/**
- * @file exchangedb/pg_insert_age_withdraw_reveal.h
- * @brief implementation of the insert_age_withdraw_reveal function for 
Postgres
- * @author Özgür Kesim
- */
-#ifndef PG_INSERT_AGE_WITHDRAW_REVEAL_H
-#define PG_INSERT_AGE_WITHDRAW_REVEAL_H
-
-#include "taler_util.h"
-#include "taler_json_lib.h"
-#include "taler_exchangedb_plugin.h"
-
-/**
- * @brief Store in the database which coin(s) the wallet wanted to create in a
- * given age-withdraw operation and all of the other information we learned or
- * created in the /age-withdraw/reveal step.
- *
- * @param cls the @e cls of this struct with the plugin-specific state
- * TODO:oec
- * @return query status for the transaction
- */
-enum GNUNET_DB_QueryStatus
-TEH_PG_insert_refresh_reveal (
-  void *cls,
-  /* TODO: oec */
-  );
-
-#endif
diff --git a/src/exchangedb/pg_insert_records_by_table.c 
b/src/exchangedb/pg_insert_records_by_table.c
index 9baaf3b1..a8b71759 100644
--- a/src/exchangedb/pg_insert_records_by_table.c
+++ b/src/exchangedb/pg_insert_records_by_table.c
@@ -2057,38 +2057,37 @@ irbt_cb_table_purse_deletion (struct PostgresClosure 
*pg,
 
 
 /**
- * Function called with age_withdraw_commitments records to insert into table.
+ * Function called with age_withdraw records to insert into table.
  *
  * @param pg plugin context
  * @param td record to insert
  */
 static enum GNUNET_DB_QueryStatus
-irbt_cb_table_age_withdraw_commitments (struct PostgresClosure *pg,
-                                        const struct
-                                        TALER_EXCHANGEDB_TableData *td)
+irbt_cb_table_age_withdraw (struct PostgresClosure *pg,
+                            const struct
+                            TALER_EXCHANGEDB_TableData *td)
 {
   struct GNUNET_PQ_QueryParam params[] = {
     GNUNET_PQ_query_param_uint64 (&td->serial),
     GNUNET_PQ_query_param_auto_from_type (
-      &td->details.age_withdraw_commitments.h_commitment),
+      &td->details.age_withdraw.h_commitment),
     TALER_PQ_query_param_amount (
-      &td->details.age_withdraw_commitments.amount_with_fee),
+      &td->details.age_withdraw.amount_with_fee),
     GNUNET_PQ_query_param_uint16 (
-      &td->details.age_withdraw_commitments.max_age),
+      &td->details.age_withdraw.max_age),
     GNUNET_PQ_query_param_auto_from_type (
-      &td->details.age_withdraw_commitments.reserve_pub),
+      &td->details.age_withdraw.reserve_pub),
     GNUNET_PQ_query_param_auto_from_type (
-      &td->details.age_withdraw_commitments.reserve_sig),
+      &td->details.age_withdraw.reserve_sig),
     GNUNET_PQ_query_param_uint32 (
-      &td->details.age_withdraw_commitments.noreveal_index),
-    GNUNET_PQ_query_param_absolute_time (
-      &td->details.age_withdraw_commitments.timestamp),
+      &td->details.age_withdraw.noreveal_index),
+    /* TODO: other fields, too! */
     GNUNET_PQ_query_param_end
   };
 
   PREPARE (pg,
-           "insert_into_table_age_withdraw_commitments",
-           "INSERT INTO age_withdraw_commitments"
+           "insert_into_table_age_withdraw",
+           "INSERT INTO age_withdraw"
            "(age_withdraw_commitment_id"
            ",h_commitment"
            ",amount_with_fee_val"
@@ -2097,64 +2096,10 @@ irbt_cb_table_age_withdraw_commitments (struct 
PostgresClosure *pg,
            ",reserve_pub"
            ",reserve_sig"
            ",noreveal_index"
-           ",timestamp"
            ") VALUES "
            "($1, $2, $3, $4, $5, $6, $7, $8, $9);");
   return GNUNET_PQ_eval_prepared_non_select (pg->conn,
-                                             
"insert_into_table_age_withdraw_commitments",
-                                             params);
-}
-
-
-/**
- * Function called with age_withdraw_revealed_coins records to insert into 
table.
- *
- * @param pg plugin context
- * @param td record to insert
- */
-static enum GNUNET_DB_QueryStatus
-irbt_cb_table_age_withdraw_revealed_coins (struct PostgresClosure *pg,
-                                           const struct
-                                           TALER_EXCHANGEDB_TableData *td)
-{
-  struct GNUNET_HashCode h_coin_ev;
-  struct GNUNET_PQ_QueryParam params[] = {
-    GNUNET_PQ_query_param_uint64 (&td->serial),
-    GNUNET_PQ_query_param_auto_from_type (
-      &td->details.age_withdraw_revealed_coins.h_commitment),
-    GNUNET_PQ_query_param_uint32 (
-      &td->details.age_withdraw_revealed_coins.freshcoin_index),
-    GNUNET_PQ_query_param_uint64 (
-      &td->details.age_withdraw_revealed_coins.denominations_serial),
-    GNUNET_PQ_query_param_fixed_size (
-      td->details.age_withdraw_revealed_coins.coin_ev,
-      td->details.age_withdraw_revealed_coins.coin_ev_size),
-    GNUNET_PQ_query_param_auto_from_type (&h_coin_ev),
-    TALER_PQ_query_param_blinded_denom_sig (
-      &td->details.age_withdraw_revealed_coins.ev_sig),
-    GNUNET_PQ_query_param_end
-  };
-
-  PREPARE (pg,
-           "insert_into_table_age_withdraw_revealed_coins",
-           "INSERT INTO age_withdraw_revealed_coins"
-           "(age_withdraw_revealed_coins_id"
-           ",h_commitment"
-           ",freshcoin_index"
-           ",denominations_serial"
-           ",coin_ev"
-           ",h_coin_ev"
-           ",ev_sig"
-           ",ewv"
-           ") VALUES "
-           "($1, $2, $3, $4, $5, $6, $7, $8);");
-
-  GNUNET_CRYPTO_hash (td->details.age_withdraw_revealed_coins.coin_ev,
-                      td->details.age_withdraw_revealed_coins.coin_ev_size,
-                      &h_coin_ev);
-
-  return GNUNET_PQ_eval_prepared_non_select (pg->conn,
-                                             
"insert_into_table_age_withdraw_revealed_coins",
+                                             "insert_into_table_age_withdraw",
                                              params);
 }
 
@@ -2306,11 +2251,8 @@ TEH_PG_insert_records_by_table (void *cls,
   case TALER_EXCHANGEDB_RT_PURSE_DELETION:
     rh = &irbt_cb_table_purse_deletion;
     break;
-  case TALER_EXCHANGEDB_RT_WITHDRAW_AGE_COMMITMENTS:
-    rh = &irbt_cb_table_age_withdraw_commitments;
-    break;
-  case TALER_EXCHANGEDB_RT_WITHDRAW_AGE_REVEALED_COINS:
-    rh = &irbt_cb_table_age_withdraw_revealed_coins;
+  case TALER_EXCHANGEDB_RT_AGE_WITHDRAW:
+    rh = &irbt_cb_table_age_withdraw;
     break;
   }
   if (NULL == rh)
diff --git a/src/exchangedb/pg_iterate_denominations.c 
b/src/exchangedb/pg_iterate_denominations.c
index a3825768..1549ed77 100644
--- a/src/exchangedb/pg_iterate_denominations.c
+++ b/src/exchangedb/pg_iterate_denominations.c
@@ -72,6 +72,8 @@ dominations_cb_helper (void *cls,
     struct TALER_DenominationHashP h_denom_pub = {0};
     bool revoked;
     struct GNUNET_PQ_ResultSpec rs[] = {
+      GNUNET_PQ_result_spec_uint64 ("denominations_serial",
+                                    &meta.serial),
       GNUNET_PQ_result_spec_auto_from_type ("master_sig",
                                             &master_sig),
       GNUNET_PQ_result_spec_bool ("revoked",
@@ -145,7 +147,8 @@ TEH_PG_iterate_denominations (void *cls,
   PREPARE (pg,
            "select_denominations",
            "SELECT"
-           " denominations.master_sig"
+           " denominations_serial"
+           ",denominations.master_sig"
            ",denom_revocations_serial_id IS NOT NULL AS revoked"
            ",valid_from"
            ",expire_withdraw"
diff --git a/src/exchangedb/pg_lookup_records_by_table.c 
b/src/exchangedb/pg_lookup_records_by_table.c
index 3fcad58c..7862335a 100644
--- a/src/exchangedb/pg_lookup_records_by_table.c
+++ b/src/exchangedb/pg_lookup_records_by_table.c
@@ -2762,109 +2762,48 @@ lrbt_cb_table_purse_deletion (void *cls,
 
 
 /**
- * Function called with age_withdraw_commitments table entries.
+ * Function called with age_withdraw table entries.
  *
  * @param cls closure
  * @param result the postgres result
  * @param num_results the number of results in @a result
  */
 static void
-lrbt_cb_table_age_withdraw_commitments (void *cls,
-                                        PGresult *result,
-                                        unsigned int num_results)
+lrbt_cb_table_age_withdraw (void *cls,
+                            PGresult *result,
+                            unsigned int num_results)
 {
   struct LookupRecordsByTableContext *ctx = cls;
   struct PostgresClosure *pg = ctx->pg;
   struct TALER_EXCHANGEDB_TableData td = {
-    .table = TALER_EXCHANGEDB_RT_WITHDRAW_AGE_COMMITMENTS
+    .table = TALER_EXCHANGEDB_RT_AGE_WITHDRAW
   };
 
   for (unsigned int i = 0; i<num_results; i++)
   {
     struct GNUNET_PQ_ResultSpec rs[] = {
       GNUNET_PQ_result_spec_uint64 (
-        "age_withdraw_commitment_id",
+        "age_withdraw_id",
         &td.serial),
       GNUNET_PQ_result_spec_auto_from_type (
         "h_commitment",
-        &td.details.age_withdraw_commitments.h_commitment),
+        &td.details.age_withdraw.h_commitment),
       GNUNET_PQ_result_spec_uint16 (
         "max_age",
-        &td.details.age_withdraw_commitments.max_age),
+        &td.details.age_withdraw.max_age),
       TALER_PQ_RESULT_SPEC_AMOUNT (
         "amount_with_fee",
-        &td.details.age_withdraw_commitments.amount_with_fee),
+        &td.details.age_withdraw.amount_with_fee),
       GNUNET_PQ_result_spec_auto_from_type (
         "reserve_pub",
-        &td.details.age_withdraw_commitments.reserve_pub),
+        &td.details.age_withdraw.reserve_pub),
       GNUNET_PQ_result_spec_auto_from_type (
         "reserve_sig",
-        &td.details.age_withdraw_commitments.reserve_sig),
+        &td.details.age_withdraw.reserve_sig),
       GNUNET_PQ_result_spec_uint32 (
         "noreveal_index",
-        &td.details.age_withdraw_commitments.noreveal_index),
-      GNUNET_PQ_result_spec_end
-    };
-
-    if (GNUNET_OK !=
-        GNUNET_PQ_extract_result (result,
-                                  rs,
-                                  i))
-    {
-      GNUNET_break (0);
-      ctx->error = true;
-      return;
-    }
-    ctx->cb (ctx->cb_cls,
-             &td);
-    GNUNET_PQ_cleanup_result (rs);
-  }
-}
-
-
-/**
- * Function called with age_withdraw_revealed_coins table entries.
- *
- * @param cls closure
- * @param result the postgres result
- * @param num_results the number of results in @a result
- */
-static void
-lrbt_cb_table_age_withdraw_revealed_coins (void *cls,
-                                           PGresult *result,
-                                           unsigned int num_results)
-{
-  struct LookupRecordsByTableContext *ctx = cls;
-  struct TALER_EXCHANGEDB_TableData td = {
-    .table = TALER_EXCHANGEDB_RT_WITHDRAW_AGE_REVEALED_COINS
-  };
-
-  for (unsigned int i = 0; i<num_results; i++)
-  {
-    struct GNUNET_PQ_ResultSpec rs[] = {
-      GNUNET_PQ_result_spec_uint64 (
-        "age_withdraw_revealed_coins_id",
-        &td.serial),
-      GNUNET_PQ_result_spec_auto_from_type (
-        "h_commitment",
-        &td.details.age_withdraw_revealed_coins.h_commitment),
-      GNUNET_PQ_result_spec_uint32 (
-        "freshcoin_index",
-        &td.details.age_withdraw_revealed_coins.freshcoin_index),
-      GNUNET_PQ_result_spec_uint64 (
-        "denominations_serial",
-        &td.details.age_withdraw_revealed_coins.denominations_serial),
-      /* Note: h_coin_ev is recalculated */
-      GNUNET_PQ_result_spec_variable_size (
-        "coin_ev",
-        (void **) &td.details.age_withdraw_revealed_coins.coin_ev,
-        &td.details.age_withdraw_revealed_coins.coin_ev_size),
-      TALER_PQ_result_spec_blinded_denom_sig (
-        "ev_sig",
-        &td.details.refresh_revealed_coins.ev_sig),
-      TALER_PQ_result_spec_exchange_withdraw_values (
-        "ewv",
-        &td.details.refresh_revealed_coins.ewv),
+        &td.details.age_withdraw.noreveal_index),
+      /* TODO[oec]: more fields! */
       GNUNET_PQ_result_spec_end
     };
 
@@ -3591,10 +3530,10 @@ TEH_PG_lookup_records_by_table (void *cls,
               " ORDER BY purse_deletion_serial_id ASC;");
     rh = &lrbt_cb_table_purse_deletion;
     break;
-  case TALER_EXCHANGEDB_RT_WITHDRAW_AGE_COMMITMENTS:
-    XPREPARE ("select_above_serial_by_table_age_withdraw_commitments",
+  case TALER_EXCHANGEDB_RT_AGE_WITHDRAW:
+    XPREPARE ("select_above_serial_by_table_age_withdraw",
               "SELECT"
-              " age_withdraw_commitment_id"
+              " age_withdraw_id"
               ",h_commitment"
               ",amount_with_fee_val"
               ",amount_with_fee_frac"
@@ -3602,26 +3541,11 @@ TEH_PG_lookup_records_by_table (void *cls,
               ",reserve_pub"
               ",reserve_sig"
               ",noreveal_index"
-              " FROM age_withdraw_commitments"
-              " WHERE age_withdraw_commitment_id > $1"
-              " ORDER BY age_withdraw_commitment_id ASC;");
-    rh = &lrbt_cb_table_age_withdraw_commitments;
-    break;
-  case TALER_EXCHANGEDB_RT_WITHDRAW_AGE_REVEALED_COINS:
-    XPREPARE ("select_above_serial_by_table_age_withdraw_revealed_coins",
-              "SELECT"
-              " age_withdraw_revealed_coins_serial_id"
-              ",h_commitment"
-              ",freshcoin_index"
-              ",denominations_serial"
-              ",coin_ev"
-              ",h_coin_ev"
-              ",ev_sig"
-              ",ewv"
-              " FROM age_withdraw_revealed_coins"
-              " WHERE age_withdraw_revealed_coins_serial_id > $1"
-              " ORDER BY age_withdraw_revealed_coins_serial_id ASC;");
-    rh = &lrbt_cb_table_age_withdraw_revealed_coins;
+              " FROM age_withdraw"
+              " WHERE age_withdraw_id > $1"
+              " ORDER BY age_withdraw_id ASC;");
+    /* TODO[oec]: MORE FIELDS! */
+    rh = &lrbt_cb_table_age_withdraw;
     break;
   }
   if (NULL == rh)
diff --git a/src/exchangedb/pg_lookup_serial_by_table.c 
b/src/exchangedb/pg_lookup_serial_by_table.c
index 2e3b4130..0bf0b971 100644
--- a/src/exchangedb/pg_lookup_serial_by_table.c
+++ b/src/exchangedb/pg_lookup_serial_by_table.c
@@ -426,23 +426,14 @@ TEH_PG_lookup_serial_by_table (void *cls,
               " LIMIT 1;");
     statement = "select_serial_by_table_purse_deletion";
     break;
-  case TALER_EXCHANGEDB_RT_WITHDRAW_AGE_COMMITMENTS:
-    XPREPARE ("select_serial_by_table_age_withdraw_commitments",
+  case TALER_EXCHANGEDB_RT_AGE_WITHDRAW:
+    XPREPARE ("select_serial_by_table_age_withdraw",
               "SELECT"
-              " age_withdraw_commitment_id AS serial"
-              " FROM age_withdraw_commitments"
-              " ORDER BY age_withdraw_commitment_id DESC"
+              " age_withdraw_id AS serial"
+              " FROM age_withdraw"
+              " ORDER BY age_withdraw_id DESC"
               " LIMIT 1;");
-    statement = "select_serial_by_table_age_withdraw_commitments";
-    break;
-  case TALER_EXCHANGEDB_RT_WITHDRAW_AGE_REVEALED_COINS:
-    XPREPARE ("select_serial_by_table_age_withdraw_revealed_coins",
-              "SELECT"
-              " age_withdraw_revealed_coins_id AS serial"
-              " FROM age_withdraw_revealed_coins"
-              " ORDER BY age_withdraw_revealed_coins_id DESC"
-              " LIMIT 1;");
-    statement = "select_serial_by_table_age_withdraw_revealed_coins";
+    statement = "select_serial_by_table_age_withdraw";
     break;
   }
   if (NULL == statement)
diff --git a/src/exchangedb/plugin_exchangedb_postgres.c 
b/src/exchangedb/plugin_exchangedb_postgres.c
index 00648419..6938ee0a 100644
--- a/src/exchangedb/plugin_exchangedb_postgres.c
+++ b/src/exchangedb/plugin_exchangedb_postgres.c
@@ -117,8 +117,9 @@
 #include "pg_drain_kyc_alert.h"
 #include "pg_reserves_in_insert.h"
 #include "pg_get_withdraw_info.h"
-#include "pg_get_age_withdraw_info.h"
+#include "pg_get_age_withdraw.h"
 #include "pg_do_batch_withdraw.h"
+#include "pg_do_age_withdraw.h"
 #include "pg_get_policy_details.h"
 #include "pg_persist_policy_details.h"
 #include "pg_do_deposit.h"
@@ -581,8 +582,10 @@ libtaler_plugin_exchangedb_postgres_init (void *cls)
     = &TEH_PG_get_withdraw_info;
   plugin->do_batch_withdraw
     = &TEH_PG_do_batch_withdraw;
-  plugin->get_age_withdraw_info
-    = &TEH_PG_get_age_withdraw_info;
+  plugin->do_age_withdraw
+    = &TEH_PG_do_age_withdraw;
+  plugin->get_age_withdraw
+    = &TEH_PG_get_age_withdraw;
   plugin->get_policy_details
     = &TEH_PG_get_policy_details;
   plugin->persist_policy_details
diff --git a/src/exchangedb/procedures.sql.in b/src/exchangedb/procedures.sql.in
index c9277ea6..7166540f 100644
--- a/src/exchangedb/procedures.sql.in
+++ b/src/exchangedb/procedures.sql.in
@@ -21,6 +21,7 @@ SET search_path TO exchange;
 #include "exchange_do_withdraw.sql"
 #include "exchange_do_batch_withdraw.sql"
 #include "exchange_do_batch_withdraw_insert.sql"
+#include "exchange_do_age_withdraw.sql"
 #include "exchange_do_recoup_by_reserve.sql"
 #include "exchange_do_deposit.sql"
 #include "exchange_do_melt.sql"
diff --git a/src/exchangedb/shard-0001.sql b/src/exchangedb/shard-0001.sql
index 89c79f17..03066443 100644
--- a/src/exchangedb/shard-0001.sql
+++ b/src/exchangedb/shard-0001.sql
@@ -160,7 +160,7 @@ BEGIN
       ',current_balance_frac INT4 NOT NULL DEFAULT(0)'
       ',purses_active INT8 NOT NULL DEFAULT(0)'
       ',purses_allowed INT8 NOT NULL DEFAULT(0)'
-      ',max_age INT4 NOT NULL DEFAULT(120)'
+      ',birthdate INT4 NOT NULL DEFAULT(0)' -- 0 means: no age restriction.
       ',expiration_date INT8 NOT NULL'
       ',gc_date INT8 NOT NULL'
     ') %s ;'
diff --git a/src/include/taler_crypto_lib.h b/src/include/taler_crypto_lib.h
index ea53c2fc..3ad441cb 100644
--- a/src/include/taler_crypto_lib.h
+++ b/src/include/taler_crypto_lib.h
@@ -439,7 +439,9 @@ struct TALER_AgeCommitmentPublicKeyP
 
 
 /*
- * @brief Hash to represent the commitment to n*kappa blinded keys during a 
age-withdrawal.
+ * @brief Hash to represent the commitment to n*kappa blinded keys during a
+ * age-withdrawal. It is the running SHA512 hash over the hashes of the blinded
+ * envelopes of n*kappa coins.
  */
 struct TALER_AgeWithdrawCommitmentHashP
 {
@@ -3726,9 +3728,10 @@ TALER_wallet_withdraw_verify (
 /**
  * Sign age-withdraw request.
  *
- * @param h_commitment hash all n*kappa blinded coins in the commitment for 
the age-withdraw
+ * @param h_commitment hash over all n*kappa blinded coins in the commitment 
for the age-withdraw
  * @param amount_with_fee amount to debit the reserve for
- * @param max_age_group maximum age group that the withdrawn coins must be 
restricted to
+ * @param mask the mask that defines the age groups
+ * @param max_age maximum age from which the age group is derived, that the 
withdrawn coins must be restricted to.
  * @param reserve_priv private key to sign with
  * @param[out] reserve_sig resulting signature
  */
@@ -3736,7 +3739,8 @@ void
 TALER_wallet_age_withdraw_sign (
   const struct TALER_AgeWithdrawCommitmentHashP *h_commitment,
   const struct TALER_Amount *amount_with_fee,
-  uint32_t max_age_group,
+  const struct TALER_AgeMask *mask,
+  uint8_t max_age,
   const struct TALER_ReservePrivateKeyP *reserve_priv,
   struct TALER_ReserveSignatureP *reserve_sig);
 
@@ -3745,7 +3749,8 @@ TALER_wallet_age_withdraw_sign (
  *
  * @param h_commitment hash all n*kappa blinded coins in the commitment for 
the age-withdraw
  * @param amount_with_fee amount to debit the reserve for
- * @param max_age_group maximum age group that the withdrawn coins must be 
restricted to
+ * @param mask the mask that defines the age groups
+ * @param max_age maximum age from which the age group is derived, that the 
withdrawn coins must be restricted to.
  * @param reserve_pub public key of the reserve
  * @param reserve_sig resulting signature
  * @return #GNUNET_OK if the signature is valid
@@ -3754,11 +3759,11 @@ enum GNUNET_GenericReturnValue
 TALER_wallet_age_withdraw_verify (
   const struct TALER_AgeWithdrawCommitmentHashP *h_commitment,
   const struct TALER_Amount *amount_with_fee,
-  uint32_t max_age_group,
+  const struct TALER_AgeMask *mask,
+  uint8_t max_age,
   const struct TALER_ReservePublicKeyP *reserve_pub,
   const struct TALER_ReserveSignatureP *reserve_sig);
 
-
 /**
  * Verify exchange melt confirmation.
  *
@@ -4867,6 +4872,22 @@ TALER_exchange_online_age_withdraw_confirmation_sign (
   struct TALER_ExchangeSignatureP *sig);
 
 
+/**
+ * Verfiy an exchange age-withdraw confirmation
+ *
+ * @param h_commitment Commitment over all n*kappa coin candidates from the 
original request to age-withdraw
+ * @param noreveal_index The index returned by the exchange
+ * @param exchange_pub The public key used for signing
+ * @param exchange_sig The signature from the exchange
+ */
+enum GNUNET_GenericReturnValue
+TALER_exchange_online_age_withdraw_confirmation_verify (
+  const struct TALER_AgeWithdrawCommitmentHashP *h_commitment,
+  uint32_t noreveal_index,
+  const struct TALER_ExchangePublicKeyP *exchange_pub,
+  const struct TALER_ExchangeSignatureP *exchange_sig);
+
+
 /* ********************* offline signing ************************** */
 
 
diff --git a/src/include/taler_exchange_service.h 
b/src/include/taler_exchange_service.h
index bc6a230b..d4efc15d 100644
--- a/src/include/taler_exchange_service.h
+++ b/src/include/taler_exchange_service.h
@@ -18,6 +18,7 @@
  * @brief C interface of libtalerexchange, a C library to use exchange's HTTP 
API
  * @author Sree Harsha Totakura <sreeharsha@totakura.in>
  * @author Christian Grothoff
+ * @author Özgür Kesim
  */
 #ifndef _TALER_EXCHANGE_SERVICE_H
 #define _TALER_EXCHANGE_SERVICE_H
@@ -1618,7 +1619,8 @@ typedef void
 /**
  * Get a CS R using a /csr-withdraw request.
  *
- * @param exchange the exchange handle; the exchange must be ready to operate
+ * @param curl_ctx The curl context to use for the requests
+ * @param exchange_url Base-URL to the excnange
  * @param pk Which denomination key is the /csr request for
  * @param nonce client nonce for the request
  * @param res_cb the callback to call when the final result for this request 
is available
@@ -1629,7 +1631,8 @@ typedef void
  */
 struct TALER_EXCHANGE_CsRWithdrawHandle *
 TALER_EXCHANGE_csr_withdraw (
-  struct TALER_EXCHANGE_Handle *exchange,
+  struct GNUNET_CURL_Context *curl_ctx,
+  const char *exchange_url,
   const struct TALER_EXCHANGE_DenomPublicKey *pk,
   const struct TALER_CsNonce *nonce,
   TALER_EXCHANGE_CsRWithdrawCallback res_cb,
@@ -2448,7 +2451,9 @@ typedef void
  * disk before calling, and be ready to repeat the request with the
  * same arguments in case of failures.
  *
- * @param exchange the exchange handle; the exchange must be ready to operate
+ * @param curl_ctx The curl context to use
+ * @param exchange_url The base-URL of the exchange
+ * @param keys The /keys material from the exchange
  * @param reserve_priv private key of the reserve to withdraw from
  * @param wci inputs that determine the planchet
  * @param res_cb the callback to call when the final result for this request 
is available
@@ -2459,7 +2464,9 @@ typedef void
  */
 struct TALER_EXCHANGE_WithdrawHandle *
 TALER_EXCHANGE_withdraw (
-  struct TALER_EXCHANGE_Handle *exchange,
+  struct GNUNET_CURL_Context *curl_ctx,
+  const char *exchange_url,
+  struct TALER_EXCHANGE_Keys *keys,
   const struct TALER_ReservePrivateKeyP *reserve_priv,
   const struct TALER_EXCHANGE_WithdrawCoinInput *wci,
   TALER_EXCHANGE_WithdrawCallback res_cb,
@@ -2575,7 +2582,9 @@ typedef void
  * disk before calling, and be ready to repeat the request with the
  * same arguments in case of failures.
  *
- * @param exchange the exchange handle; the exchange must be ready to operate
+ * @param curl_ctx The curl context to use
+ * @param exchange_url The base-URL of the exchange
+ * @param keys The /keys material from the exchange
  * @param reserve_priv private key of the reserve to withdraw from
  * @param wcis inputs that determine the planchets
  * @param wci_length number of entries in @a wcis
@@ -2587,7 +2596,9 @@ typedef void
  */
 struct TALER_EXCHANGE_BatchWithdrawHandle *
 TALER_EXCHANGE_batch_withdraw (
-  struct TALER_EXCHANGE_Handle *exchange,
+  struct GNUNET_CURL_Context *curl_ctx,
+  const char *exchange_url,
+  const struct TALER_EXCHANGE_Keys *keys,
   const struct TALER_ReservePrivateKeyP *reserve_priv,
   const struct TALER_EXCHANGE_WithdrawCoinInput *wcis,
   unsigned int wci_length,
@@ -2668,7 +2679,9 @@ struct TALER_EXCHANGE_Withdraw2Handle;
  * disk before calling, and be ready to repeat the request with the
  * same arguments in case of failures.
  *
- * @param exchange the exchange handle; the exchange must be ready to operate
+ * @param curl_ctx The curl-context to use
+ * @param exchange_url The base-URL of the exchange
+ * @param keys The /keys material from the exchange
  * @param pd planchet details of the planchet to withdraw
  * @param reserve_priv private key of the reserve to withdraw from
  * @param res_cb the callback to call when the final result for this request 
is available
@@ -2679,7 +2692,9 @@ struct TALER_EXCHANGE_Withdraw2Handle;
  */
 struct TALER_EXCHANGE_Withdraw2Handle *
 TALER_EXCHANGE_withdraw2 (
-  struct TALER_EXCHANGE_Handle *exchange,
+  struct GNUNET_CURL_Context *curl_ctx,
+  const char *exchange_url,
+  struct TALER_EXCHANGE_Keys *keys,
   const struct TALER_PlanchetDetail *pd,
   const struct TALER_ReservePrivateKeyP *reserve_priv,
   TALER_EXCHANGE_Withdraw2Callback res_cb,
@@ -2765,7 +2780,9 @@ struct TALER_EXCHANGE_BatchWithdraw2Handle;
  * disk before calling, and be ready to repeat the request with the
  * same arguments in case of failures.
  *
- * @param exchange the exchange handle; the exchange must be ready to operate
+ * @param curl_ctx The curl context to use
+ * @param exchange_url The base-URL of the exchange
+ * @param keys The /keys material from the exchange
  * @param pds array of planchet details of the planchet to withdraw
  * @param pds_length number of entries in the @a pds array
  * @param reserve_priv private key of the reserve to withdraw from
@@ -2777,7 +2794,9 @@ struct TALER_EXCHANGE_BatchWithdraw2Handle;
  */
 struct TALER_EXCHANGE_BatchWithdraw2Handle *
 TALER_EXCHANGE_batch_withdraw2 (
-  struct TALER_EXCHANGE_Handle *exchange,
+  struct GNUNET_CURL_Context *curl_ctx,
+  const char *exchange_url,
+  const struct TALER_EXCHANGE_Keys *keys,
   const struct TALER_ReservePrivateKeyP *reserve_priv,
   const struct TALER_PlanchetDetail *pds,
   unsigned int pds_length,
@@ -2796,6 +2815,119 @@ TALER_EXCHANGE_batch_withdraw2_cancel (
   struct TALER_EXCHANGE_BatchWithdraw2Handle *wh);
 
 
+/* ********************* /reserve/$RESERVE_PUB/age-withdraw *************** */
+
+/**
+ * @brief Information needed to withdraw age restricted coins.
+ */
+struct TALER_EXCHANGE_AgeWithdrawCoinInput
+{
+  /* The master secret from which we derive all other relevant values for
+   * the coin: private key, nonces (if applicable) and age restriction
+   */
+  const struct TALER_PlanchetMasterSecretP secret[TALER_CNC_KAPPA];
+
+  /* The denomination of the coin.  Must support age restriction, i.e
+   * its .keys.age_mask MUST not be 0 */
+  const struct TALER_EXCHANGE_DenomPublicKey *denom_pub;
+};
+
+/**
+ * @brief A handle to a /reserves/$RESERVE_PUB/age-withdraw request
+ */
+struct TALER_EXCHANGE_AgeWithdrawHandle;
+
+/**
+ * @brief Details about the response for a age withdraw request.
+ */
+struct TALER_EXCHANGE_AgeWithdrawResponse
+{
+  /**
+   * HTTP response data.
+   */
+  struct TALER_EXCHANGE_HttpResponse hr;
+
+  /**
+   * Details about the response
+   */
+  union
+  {
+    /**
+     * Details if the status is #MHD_HTTP_OK.
+     */
+    struct
+    {
+      /**
+       * Index that should not be revealed during the age-withdraw reveal 
phase.
+       * The struct TALER_PlanchetMasterSecretP * from the request
+       * with this index are the ones to keep.
+       */
+      uint8_t noreveal_index;
+
+      /**
+       * Signature of the exchange over the origina TALER_AgeWithdrawRequestPS
+       */
+      struct TALER_ExchangeSignatureP exchange_sig;
+
+      /**
+       * Key used by the exchange for @e exchange_sig
+       */
+      struct TALER_ExchangePublicKeyP exchange_pub;
+
+    } ok;
+    /* FIXME[oec]: error cases */
+  } details;
+};
+
+typedef void
+(*TALER_EXCHANGE_AgeWithdrawCallback)(
+  void *cls,
+  const struct TALER_EXCHANGE_AgeWithdrawResponse *awr);
+
+/**
+ * Submit an age-withdraw request to the exchange and get the exchange's
+ * response.
+ *
+ * This API is typically used by a wallet.  Note that to ensure that
+ * no money is lost in case of hardware failures, the provided
+ * argument @a rd should be committed to persistent storage
+ * prior to calling this function.
+ *
+ * @param curl_ctx The curl context
+ * @param exchange_url The base url of the exchange
+ * @parm keys The denomination keys from the exchange
+ * @param reserve_priv The pivate key to the reserve
+ * @param coin_inputs The input for the coins to withdraw
+ * @param num_coins The number of elements in @e coin_inputs
+ * @param max_age The maximum age we commit to.
+ * @param res_cb A callback for the result, maybe NULL
+ * @param res_cb_cls A closure for @e res_cb, maybe NULL
+ * @return a handle for this request; NULL if the argument was invalid.
+ *         In this case, the callback will not be called.
+ */
+struct TALER_EXCHANGE_AgeWithdrawHandle *
+TALER_EXCHANGE_age_withdraw (
+  struct GNUNET_CURL_Context *curl_ctx,
+  const char *exchange_url,
+  struct TALER_EXCHANGE_Keys *keys,
+  const struct TALER_ReservePrivateKeyP *reserve_priv,
+  const struct TALER_EXCHANGE_AgeWithdrawCoinInput *coin_inputs,
+  size_t num_coins,
+  uint8_t max_age,
+  TALER_EXCHANGE_AgeWithdrawCallback res_cb,
+  void *res_cb_cls);
+
+/**
+ * Cancel a age-withdraw request.  This function cannot be used
+ * on a request handle if a response is already served for it.
+ *
+ * @param awh the age-withdraw handle
+ */
+void
+TALER_EXCHANGE_age_withdraw_cancel (
+  struct TALER_EXCHANGE_AgeWithdrawHandle *awh);
+
+
 /* ********************* /refresh/melt+reveal ***************************** */
 
 
@@ -3565,7 +3697,7 @@ TALER_EXCHANGE_verify_coin_history (
  */
 enum GNUNET_GenericReturnValue
 TALER_EXCHANGE_parse_reserve_history (
-  struct TALER_EXCHANGE_Keys *keys,
+  const struct TALER_EXCHANGE_Keys *keys,
   const json_t *history,
   const struct TALER_ReservePublicKeyP *reserve_pub,
   const char *currency,
diff --git a/src/include/taler_exchangedb_plugin.h 
b/src/include/taler_exchangedb_plugin.h
index 5404f0b1..1b1a657c 100644
--- a/src/include/taler_exchangedb_plugin.h
+++ b/src/include/taler_exchangedb_plugin.h
@@ -301,8 +301,7 @@ enum TALER_EXCHANGEDB_ReplicatedTable
   TALER_EXCHANGEDB_RT_AML_HISTORY,
   TALER_EXCHANGEDB_RT_KYC_ATTRIBUTES,
   TALER_EXCHANGEDB_RT_PURSE_DELETION,
-  TALER_EXCHANGEDB_RT_WITHDRAW_AGE_COMMITMENTS,
-  TALER_EXCHANGEDB_RT_WITHDRAW_AGE_REVEALED_COINS,
+  TALER_EXCHANGEDB_RT_AGE_WITHDRAW,
 };
 
 
@@ -773,22 +772,14 @@ struct TALER_EXCHANGEDB_TableData
       struct TALER_AgeWithdrawCommitmentHashP h_commitment;
       struct TALER_Amount amount_with_fee;
       uint16_t max_age;
+      uint32_t noreveal_index;
       struct TALER_ReservePublicKeyP reserve_pub;
       struct TALER_ReserveSignatureP reserve_sig;
-      uint32_t noreveal_index;
-      struct GNUNET_TIME_Absolute timestamp;
-    } age_withdraw_commitments;
-
-    struct
-    {
-      struct TALER_AgeWithdrawCommitmentHashP h_commitment;
-      uint32_t freshcoin_index;
-      uint64_t denominations_serial;
-      void *coin_ev;
-      size_t coin_ev_size;
-      // h_coin_ev omitted, to be recomputed!
-      struct TALER_BlindedDenominationSignature ev_sig;
-    } age_withdraw_revealed_coins;
+      uint64_t num_coins;
+      uint64_t *denominations_serials;
+      void *h_blind_evs;
+      struct TALER_BlindedDenominationSignature denom_sigs;
+    } age_withdraw;
 
   } details;
 
@@ -948,6 +939,13 @@ struct TALER_EXCHANGEDB_Reserve
  */
 struct TALER_EXCHANGEDB_DenominationKeyMetaData
 {
+  /**
+   * Serial of the denomination key as in the DB.
+   * Can be used in calls to stored procedures in order to spare
+   * additional lookups.
+   */
+  uint64_t serial;
+
   /**
    * Start time of the validity period for this key.
    */
@@ -1182,11 +1180,11 @@ struct TALER_EXCHANGEDB_CollectableBlindcoin
 
 
 /**
- * @brief Information we keep for an age-withdraw commitment
+ * @brief Information we keep for an age-withdraw request
  * to reproduce the /age-withdraw operation if needed, and to have proof
  * that a reserve was drained by this amount.
  */
-struct TALER_EXCHANGEDB_AgeWithdrawCommitment
+struct TALER_EXCHANGEDB_AgeWithdraw
 {
   /**
    * Total amount (with fee) committed to withdraw
@@ -1194,7 +1192,7 @@ struct TALER_EXCHANGEDB_AgeWithdrawCommitment
   struct TALER_Amount amount_with_fee;
 
   /**
-   * Maximum age that the coins are restricted to.
+   * Maximum age (in years) that the coins are restricted to.
    */
   uint16_t max_age;
 
@@ -1208,7 +1206,7 @@ struct TALER_EXCHANGEDB_AgeWithdrawCommitment
    * revealed during cut and choose.  This value applies to all n coins in the
    * commitment.
    */
-  uint32_t noreveal_index;
+  uint16_t noreveal_index;
 
   /**
    * Public key of the reserve that was drained.
@@ -1217,15 +1215,40 @@ struct TALER_EXCHANGEDB_AgeWithdrawCommitment
 
   /**
    * Signature confirming the age withdrawal commitment, matching @e
-   * reserve_pub, @e maximum_age_group and @e h_commitment and @e
-   * total_amount_with_fee.
+   * reserve_pub, @e max_age and @e h_commitment and @e amount_with_fee.
    */
   struct TALER_ReserveSignatureP reserve_sig;
 
   /**
-   * The exchange's signature of the response.
+   * Number of coins to be withdrawn.
    */
-  struct TALER_ExchangeSignatureP sig;
+  size_t num_coins;
+
+  /**
+   * Array of @a num_coins blinded coins.  These are the chosen coins
+   * (according to @a noreveal_index) from the request, which contained
+   * kappa*num_coins blinded coins.
+   */
+  struct TALER_BlindedCoinHashP *h_coin_evs;
+
+  /**
+   * Array of @a num_coins denomination signatures of the blinded coins @a
+   * h_coin_evs.
+   */
+  struct TALER_BlindedDenominationSignature *denom_sigs;
+
+  /**
+   * Array of @a num_coins serial id's of the denominations, corresponding to
+   * the coins in @a h_coin_evs.
+   */
+  uint64_t *denom_serials;
+
+  /**
+   * [out]-Array of @a num_coins hashes of the public keys of the denominations
+   * identified by @e denom_serials.  This field is set when calling
+   * get_age_withdraw
+   */
+  struct TALER_DenominationHashP *denom_pub_hashes;
 };
 
 
@@ -2751,28 +2774,6 @@ struct TALER_EXCHANGEDB_CsRevealFreshCoinData
   uint32_t coin_off;
 };
 
-/**
- * Information about a coin that was revealed to the exchange
- * during reveal.
- */
-struct TALER_EXCHANGEDB_AgeWithdrawRevealedCoin
-{
-  /**
-   * Hash of the public denomination key of the coin.
-   */
-  struct TALER_DenominationHashP h_denom_pub;
-
-  /**
-   * Signature generated by the exchange over the coin (in blinded format).
-   */
-  struct TALER_BlindedDenominationSignature coin_sig;
-
-  /**
-   * Blinded hash of the new coin
-   */
-  struct TALER_BlindedCoinHashP h_coin_ev;
-};
-
 
 /**
  * Generic KYC status for some operation.
@@ -3760,6 +3761,15 @@ struct TALER_EXCHANGEDB_Plugin
     uint64_t *ruuid);
 
 
+  /**
+   * FIXME: merge do_batch_withdraw and do_batch_withdraw_insert into one API,
+   * which takes as input (among others)
+   *   - denom_serial[]
+   *   - blinded_coin_evs[]
+   *   - denom_sigs[]
+   * The implementation should persist the data as _arrays_ in the DB.
+   */
+
   /**
    * Perform reserve update as part of a batch withdraw operation, checking
    * for sufficient balance. Persisting the withdrawal details is done
@@ -3769,8 +3779,11 @@ struct TALER_EXCHANGEDB_Plugin
    * @param now current time (rounded)
    * @param reserve_pub public key of the reserve to debit
    * @param amount total amount to withdraw
+   * @param do_age_check if set, the batch-withdrawal can only succeed when 
the reserve has no age restriction (birthday) set.
    * @param[out] found set to true if the reserve was found
    * @param[out] balance_ok set to true if the balance was sufficient
+   * @param[out] age_ok set to true if no age requirements were defined on the 
reserve or @e do_age_check was false
+   * @param[out] allowed_maximum_age when @e age_ok is false, set to the 
allowed maximum age for withdrawal from the reserve.  The client MUST then use 
the age-withdraw endpoint
    * @param[out] ruuid set to the reserve's UUID (reserves table row)
    * @return query execution status
    */
@@ -3780,8 +3793,11 @@ struct TALER_EXCHANGEDB_Plugin
     struct GNUNET_TIME_Timestamp now,
     const struct TALER_ReservePublicKeyP *reserve_pub,
     const struct TALER_Amount *amount,
+    bool do_age_check,
     bool *found,
     bool *balance_ok,
+    bool *age_ok,
+    uint16_t *allowed_maximum_age,
     uint64_t *ruuid);
 
 
@@ -3811,72 +3827,46 @@ struct TALER_EXCHANGEDB_Plugin
     bool *nonce_reuse);
 
   /**
-   * Locate the response for a age-withdraw request under a hash that uniquely
-   * identifies the age-withdraw operation.  Used to ensure idempotency of the
-   * request.
+   * Locate the response for a age-withdraw request under a hash of the
+   * commitment and reserve_pub that uniquely identifies the age-withdraw
+   * operation.  Used to ensure idempotency of the request.
    *
    * @param cls the @e cls of this struct with the plugin-specific state
    * @param reserve_pub public key of the reserve for which the age-withdraw 
request is made
    * @param ach hash that uniquely identifies the age-withdraw operation
-   * @param[out] awc corresponding details of the previous age-withdraw 
request if an entry was found
+   * @param[out] aw corresponding details of the previous age-withdraw request 
if an entry was found
    * @return statement execution status
    */
   enum GNUNET_DB_QueryStatus
-  (*get_age_withdraw_info)(
+  (*get_age_withdraw)(
     void *cls,
     const struct TALER_ReservePublicKeyP *reserve_pub,
     const struct TALER_AgeWithdrawCommitmentHashP *ach,
-    struct TALER_EXCHANGEDB_AgeWithdrawCommitment *awc);
+    struct TALER_EXCHANGEDB_AgeWithdraw *aw);
 
   /**
-   * Perform an age-withdraw operation, checking for sufficient balance
-   * and possibly persisting the withdrawal details.
+   * Perform an age-withdraw operation, checking for sufficient balance and
+   * fulfillment of age requirements and possibly persisting the withdrawal
+   * details.
    *
    * @param cls the `struct PostgresClosure` with the plugin-specific state
    * @param commitment corresponding commitment for the age-withdraw
    * @param[out] found set to true if the reserve was found
    * @param[out] balance_ok set to true if the balance was sufficient
-   * @param[out] ruuid set to the reserve's UUID (reserves table row)
+   * @param[out] age_ok set to true if age requirements were met
+   * @param[out] allowed_maximum_age if @e age_ok is FALSE, this is set to the 
allowed maximum age
    * @return query execution status
    */
   enum GNUNET_DB_QueryStatus
   (*do_age_withdraw)(
     void *cls,
-    const struct TALER_EXCHANGEDB_AgeWithdrawCommitment *commitment,
+    const struct TALER_EXCHANGEDB_AgeWithdraw *commitment,
+    struct GNUNET_TIME_Timestamp now,
     bool *found,
     bool *balance_ok,
-    uint64_t *ruuid);
-
-  /**
-   * Store in the database which coin(s) the wallet wanted to withdraw with
-   * age restriction enabled in a given age-withdraw operation and the relevant
-   * information we learned or created in the reveal steop
-   *
-   * @param cls The `struct PostgresClosure` with the plugin-specific state
-   * @param h_commitment The hash of the original age-withdraw commitment, 
which is a key into the age_withdraw_commitments table
-   * @param num_awrcs Number of coins to generate, size of the @a coin_evs 
array
-   * @param awrcs Array of @a num_awrcs information about coins to be created
-   * @return query execution status
-   */
-  enum GNUNET_DB_QueryStatus
-  (*insert_age_withdraw_reveal)(
-    void *cls,
-    const struct TALER_AgeWithdrawCommitmentHashP *h_commitment,
-    uint32_t num_awrcs,
-    const struct TALER_EXCHANGEDB_AgeWithdrawRevealedCoin *awrcs);
-
-  /**
-   * Lookup in the database for the fresh coins with age-restriction that
-   * we created in the given age-withdraw operation.
-   *
-   * TODO: oec
-   */
-  enum GNUNET_DB_QueryStatus
-  (*get_age_withdraw_reveal)(
-    void *cls,
-    uint64_t h_commitment
-    /* TODO: oec */
-    );
+    bool *age_ok,
+    uint16_t *allowed_maximum_age,
+    bool *conflict);
 
   /**
    * Retrieve the details to a policy given by its hash_code
diff --git a/src/include/taler_testing_lib.h b/src/include/taler_testing_lib.h
index 2ef7ef60..8950e71b 100644
--- a/src/include/taler_testing_lib.h
+++ b/src/include/taler_testing_lib.h
@@ -2243,14 +2243,25 @@ TALER_TESTING_cmd_proof_kyc_oauth2 (
 
 /**
  * Starts a fake OAuth 2.0 service on @a port for testing
- * KYC processes.
+ * KYC processes which also provides a @a birthdate in a response
  *
  * @param label command label
  * @param port the TCP port to listen on
  */
 struct TALER_TESTING_Command
-TALER_TESTING_cmd_oauth (const char *label,
-                         uint16_t port);
+TALER_TESTING_cmd_oauth_with_birthdate (const char *label,
+                                        const char *birthdate,
+                                        uint16_t port);
+
+/**
+ * Starts a fake OAuth 2.0 service on @a port for testing
+ * KYC processes.
+ *
+ * @param label command label
+ * @param port the TCP port to listen on
+ */
+#define TALER_TESTING_cmd_oauth(label, port) \
+  TALER_TESTING_cmd_oauth_with_birthdate ((label), NULL, (port))
 
 
 /* ****************** P2P payment commands ****************** */
diff --git a/src/include/taler_util.h b/src/include/taler_util.h
index 1de264c1..e0473bff 100644
--- a/src/include/taler_util.h
+++ b/src/include/taler_util.h
@@ -21,6 +21,7 @@
 #ifndef TALER_UTIL_H
 #define TALER_UTIL_H
 
+#include <gnunet/gnunet_common.h>
 #define __TALER_UTIL_LIB_H_INSIDE__
 
 #include <gnunet/gnunet_util_lib.h>
@@ -510,6 +511,33 @@ char *strchrnul (const char *s, int c);
 
 #endif
 
+/**
+ * @brief Parses a date information into days after 1970-01-01 (or 0)
+ *
+ * The input MUST be of the form
+ *
+ *   1) YYYY-MM-DD, representing a valid date
+ *   2) YYYY-MM-00, representing a valid month in a particular year
+ *   3) YYYY-00-00, representing a valid year.
+ *
+ * In the cases 2) and 3) the out parameter is set to the beginning of the
+ * time, f.e. 1950-00-00 == 1950-01-01 and 1888-03-00 == 1888-03-01
+ *
+ * The output will set to the number of days after 1970-01-01 or 0, if the 
input
+ * represents a date belonging to the largest allowed age group.
+ *
+ * @param in Input string representation of the date
+ * @param mask Age mask
+ * @param[out] out Where to write the result
+ * @return GNUNET_OK on success, GNUNET_SYSERR otherwise
+ */
+enum GNUNET_GenericReturnValue
+TALER_parse_coarse_date (
+  const char *in,
+  const struct TALER_AgeMask *mask,
+  uint32_t *out);
+
+
 /**
  * @brief Parses a string as a list of age groups.
  *
@@ -558,6 +586,18 @@ char *
 TALER_age_mask_to_string (
   const struct TALER_AgeMask *mask);
 
+/*
+ * @brief returns the age group of a given age for a given age mask
+ *
+ * @param mask Age mask
+ * @param age The given age
+ * @return age group
+ */
+uint8_t
+TALER_get_age_group (
+  const struct TALER_AgeMask *mask,
+  uint8_t age);
+
 /**
  * @brief Parses a JSON object { "age_groups": "a:b:...y:z" }.
  *
@@ -570,6 +610,26 @@ TALER_JSON_parse_age_groups (const json_t *root,
                              struct TALER_AgeMask *mask);
 
 
+/* @brief Return the lowest age in the corresponding group for a given age
+ * according the given age mask.
+ *
+ * @param[IN] mask age mask
+ * @param[IN] age age to check
+ * @return lowest age in corresponding age group
+ */
+uint8_t
+TALER_get_lowest_age (
+  const struct TALER_AgeMask *mask,
+  uint8_t age);
+
+/* @brief Get the lowest age for the largest age group
+ *
+ * @param mask the age mask
+ * @return lowest age for the largest age group
+ */
+#define TALER_adult_age(mask) \
+  sizeof((mask)->bits) * 8 - __builtin_clz ((mask)->bits) - 1
+
 /**
  * Handle to an external process that will assist
  * with some JSON-to-JSON conversion.
diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am
index 53190bc5..762a3f80 100644
--- a/src/lib/Makefile.am
+++ b/src/lib/Makefile.am
@@ -22,6 +22,7 @@ libtalerexchange_la_LDFLAGS = \
   -no-undefined
 libtalerexchange_la_SOURCES = \
   exchange_api_add_aml_decision.c \
+  exchange_api_age_withdraw.c \
   exchange_api_auditor_add_denomination.c \
   exchange_api_batch_deposit.c \
   exchange_api_batch_withdraw.c \
diff --git a/src/lib/exchange_api_age_withdraw.c 
b/src/lib/exchange_api_age_withdraw.c
new file mode 100644
index 00000000..4e146c15
--- /dev/null
+++ b/src/lib/exchange_api_age_withdraw.c
@@ -0,0 +1,1033 @@
+/*
+  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 General Public License as published by the Free Software
+  Foundation; either version 3, or (at your option) any later version.
+
+  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along with
+  TALER; see the file COPYING.  If not, see
+  <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/exchange_api_age_withdraw.c
+ * @brief Implementation of /reserves/$RESERVE_PUB/age-withdraw requests
+ * @author Özgür Kesim
+ */
+
+#include "platform.h"
+#include <gnunet/gnunet_common.h>
+#include <jansson.h>
+#include <microhttpd.h> /* just for HTTP status codes */
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_curl_lib.h"
+#include "taler_json_lib.h"
+#include "taler_exchange_service.h"
+#include "exchange_api_common.h"
+#include "exchange_api_handle.h"
+#include "taler_signatures.h"
+#include "exchange_api_curl_defaults.h"
+
+struct CoinCandidate
+{
+  /**
+   * Master key material for the coin candidates.
+   */
+  struct TALER_PlanchetMasterSecretP secret;
+
+  /**
+   * Age commitment for the coin candidates, calculated from the @e ps and a
+   * given maximum age
+   */
+  struct TALER_AgeCommitmentProof age_commitment_proof;
+
+  /**
+   * Age commitment for the coin.
+   */
+  struct TALER_AgeCommitmentHash h_age_commitment;
+
+  /**
+   *  blinding secret
+   */
+  union TALER_DenominationBlindingKeyP blinding_key;
+
+  /**
+   * Private key of the coin we are withdrawing.
+   */
+  struct TALER_CoinSpendPrivateKeyP coin_priv;
+
+  /**
+   * Details of the planchet.
+   */
+  struct TALER_PlanchetDetail planchet_detail;
+
+  /**
+   * Values of the @cipher selected
+   */
+  struct TALER_ExchangeWithdrawValues alg_values;
+
+  /**
+   * Hash of the public key of the coin we are signing.
+   */
+  struct TALER_CoinPubHashP h_coin_pub;
+
+  /* Blinded hash of the coin */
+  struct TALER_BlindedCoinHashP blinded_coin_h;
+
+  /**
+   * The following fields are needed as closure for the call to /csr-withdrwaw
+   * per coin-candidate.
+   */
+
+  /* Denomination information, needed for CS coins for the step after 
/csr-withdraw */
+  struct TALER_EXCHANGE_DenomPublicKey *denom_pub;
+
+  /**
+   * Handler for the CS R request (only used for TALER_DENOMINATION_CS 
denominations)
+   */
+  struct TALER_EXCHANGE_CsRWithdrawHandle *csr_withdraw_handle;
+
+  /* Needed in the closure for csr-withdraw calls */
+  struct TALER_EXCHANGE_AgeWithdrawHandle *age_withdraw_handle;
+
+};
+
+
+/**
+ * Data we keep per coin in the batch.
+ */
+struct CoinData
+{
+
+  /**
+   * Denomination key we are withdrawing.
+   */
+  struct TALER_EXCHANGE_DenomPublicKey denom_pub;
+
+  /**
+   * The Candidates for the coin
+   */
+  struct CoinCandidate coin_candidates[TALER_CNC_KAPPA];
+
+};
+
+
+/**
+ * @brief A /reserves/$RESERVE_PUB/age-withdraw request-handle
+ */
+struct TALER_EXCHANGE_AgeWithdrawHandle
+{
+
+  /**
+   * Reserve private key.
+   */
+  const struct TALER_ReservePrivateKeyP *reserve_priv;
+
+  /**
+   * Reserve public key, calculated
+   */
+  struct TALER_ReservePublicKeyP reserve_pub;
+
+  /**
+   * Signature of the reserve for the request, calculated after all
+   * parameters for the coins are collected.
+   */
+  struct TALER_ReserveSignatureP reserve_sig;
+
+  /*
+   * The denomination keys of the exchange
+   */
+  struct TALER_EXCHANGE_Keys *keys;
+
+  /**
+   * The base-URL of the exchange.
+   */
+  const char *exchange_url;
+
+  /**
+   * The age mask, extacted from the denominations.
+   * MUST be the same for all denominations
+   *
+   */
+  struct TALER_AgeMask age_mask;
+
+  /**
+   * Maximum age to commit to.
+   */
+  uint8_t max_age;
+
+  /**
+   * Length of the @e coin_data Array
+   */
+  size_t num_coins;
+
+  /**
+   * Array of per-coin data
+   */
+  struct CoinData *coin_data;
+
+  /**
+   * The commitment calculated as SHA512 hash over all blinded_coin_h
+   */
+  struct TALER_AgeWithdrawCommitmentHashP h_commitment;
+
+  /**
+   * Total amount requested (value plus withdraw fee).
+   */
+  struct TALER_Amount amount_with_fee;
+
+  /**
+   * Number of /csr-withdraw requests still pending.
+   */
+  unsigned int csr_pending;
+
+  /**
+   * The url for this request.
+   */
+  char *request_url;
+
+  /**
+   * Context for curl.
+   */
+  struct GNUNET_CURL_Context *curl_ctx;
+
+  /**
+   * CURL handle for the request job.
+   */
+  struct GNUNET_CURL_Job *job;
+
+  /**
+   * Post Context
+   */
+  struct TALER_CURL_PostContext post_ctx;
+
+  /**
+   * Function to call with age-withdraw response results.
+   */
+  TALER_EXCHANGE_AgeWithdrawCallback callback;
+
+  /**
+   * Closure for @e age_withdraw_cb
+   */
+  void *callback_cls;
+
+};
+
+/**
+ * We got a 200 OK response for the /reserves/$RESERVE_PUB/age-withdraw 
operation.
+ * Extract the noreveal_index and return it to the caller.
+ *
+ * @param awh operation handle
+ * @param j_response reply from the exchange
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors
+ */
+static enum GNUNET_GenericReturnValue
+reserve_age_withdraw_ok (
+  struct TALER_EXCHANGE_AgeWithdrawHandle *awh,
+  const json_t *j_response)
+{
+  struct TALER_EXCHANGE_AgeWithdrawResponse response = {
+    .hr.reply = j_response,
+    .hr.http_status = MHD_HTTP_OK
+  };
+  struct GNUNET_JSON_Specification spec[] = {
+    GNUNET_JSON_spec_uint8 ("noreaveal_index",
+                            &response.details.ok.noreveal_index),
+    GNUNET_JSON_spec_fixed_auto ("exchange_sig",
+                                 &response.details.ok.exchange_sig),
+    GNUNET_JSON_spec_fixed_auto ("exchange_pub",
+                                 &response.details.ok.exchange_pub)
+  };
+
+  if (GNUNET_OK!=
+      GNUNET_JSON_parse (j_response,
+                         spec,
+                         NULL, NULL))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+
+  if (GNUNET_OK !=
+      TALER_exchange_online_age_withdraw_confirmation_verify (
+        &awh->h_commitment,
+        response.details.ok.noreveal_index,
+        &response.details.ok.exchange_pub,
+        &response.details.ok.exchange_sig))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+
+  }
+  awh->callback (awh->callback_cls,
+                 &response);
+  /* make sure the callback isn't called again */
+  awh->callback = NULL;
+
+  return GNUNET_OK;
+}
+
+
+/**
+ * FIXME: This function should be common to batch- and age-withdraw
+ *
+ * We got a 409 CONFLICT response for the /reserves/$RESERVE_PUB/age-withdraw 
operation.
+ * Check the signatures on the batch withdraw transactions in the provided
+ * history and that the balances add up.  We don't do anything directly
+ * with the information, as the JSON will be returned to the application.
+ * However, our job is ensuring that the exchange followed the protocol, and
+ * this in particular means checking all of the signatures in the history.
+ *
+ * @param keys The denomination keys from the exchange
+ * @param reserve_pub The reserve's public key
+ * @param requested_amount The requested amount
+ * @param json reply from the exchange
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors
+ */
+static enum GNUNET_GenericReturnValue
+reserve_age_withdraw_payment_required (
+  struct TALER_EXCHANGE_Keys *keys,
+  const struct TALER_ReservePublicKeyP *reserve_pub,
+  const struct TALER_Amount *requested_amount,
+  const json_t *json)
+{
+  struct TALER_Amount balance;
+  struct TALER_Amount total_in_from_history;
+  struct TALER_Amount total_out_from_history;
+  json_t *history;
+  size_t len;
+  struct GNUNET_JSON_Specification spec[] = {
+    TALER_JSON_spec_amount_any ("balance",
+                                &balance),
+    GNUNET_JSON_spec_end ()
+  };
+
+  if (GNUNET_OK !=
+      GNUNET_JSON_parse (json,
+                         spec,
+                         NULL, NULL))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  history = json_object_get (json,
+                             "history");
+  if (NULL == history)
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+
+  /* go over transaction history and compute
+     total incoming and outgoing amounts */
+  len = json_array_size (history);
+  {
+    struct TALER_EXCHANGE_ReserveHistoryEntry *rhistory;
+
+    /* Use heap allocation as "len" may be very big and thus this may
+       not fit on the stack. Use "GNUNET_malloc_large" as a malicious
+       exchange may theoretically try to crash us by giving a history
+       that does not fit into our memory. */
+    rhistory = GNUNET_malloc_large (
+      sizeof (struct TALER_EXCHANGE_ReserveHistoryEntry)
+      * len);
+    if (NULL == rhistory)
+    {
+      GNUNET_break (0);
+      return GNUNET_SYSERR;
+    }
+
+    if (GNUNET_OK !=
+        TALER_EXCHANGE_parse_reserve_history (
+          keys,
+          history,
+          reserve_pub,
+          balance.currency,
+          &total_in_from_history,
+          &total_out_from_history,
+          len,
+          rhistory))
+    {
+      GNUNET_break_op (0);
+      TALER_EXCHANGE_free_reserve_history (rhistory,
+                                           len);
+      return GNUNET_SYSERR;
+    }
+    TALER_EXCHANGE_free_reserve_history (rhistory,
+                                         len);
+  }
+
+  /* Check that funds were really insufficient */
+  if (0 >= TALER_amount_cmp (requested_amount,
+                             &balance))
+  {
+    /* Requested amount is smaller or equal to reported balance,
+       so this should not have failed. */
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  return GNUNET_OK;
+}
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /reserves/$RESERVE_PUB/age-withdraw request.
+ *
+ * @param cls the `struct TALER_EXCHANGE_AgeWithdrawHandle`
+ * @param aw2r response data
+ */
+static void
+handle_reserve_age_withdraw_finished (
+  void *cls,
+  long response_code,
+  const void *response)
+{
+  struct TALER_EXCHANGE_AgeWithdrawHandle *awh = cls;
+  const json_t *j_response = response;
+  struct TALER_EXCHANGE_AgeWithdrawResponse awr = {
+    .hr.reply = j_response,
+    .hr.http_status = (unsigned int) response_code
+  };
+
+  awh->job = NULL;
+  switch (response_code)
+  {
+  case 0:
+    awr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    break;
+  case MHD_HTTP_OK:
+    if (GNUNET_OK !=
+        reserve_age_withdraw_ok (awh,
+                                 j_response))
+    {
+      GNUNET_break_op (0);
+      awr.hr.http_status = 0;
+      awr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+      break;
+    }
+    GNUNET_assert (NULL == awh->callback);
+    TALER_EXCHANGE_age_withdraw_cancel (awh);
+    return;
+  case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
+    /* only validate reply is well-formed */
+    {
+      uint64_t ptu;
+      struct GNUNET_JSON_Specification spec[] = {
+        GNUNET_JSON_spec_uint64 ("legitimization_uuid",
+                                 &ptu),
+        GNUNET_JSON_spec_end ()
+      };
+
+      if (GNUNET_OK !=
+          GNUNET_JSON_parse (j_response,
+                             spec,
+                             NULL, NULL))
+      {
+        GNUNET_break_op (0);
+        awr.hr.http_status = 0;
+        awr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+        break;
+      }
+    }
+  case MHD_HTTP_BAD_REQUEST:
+    /* This should never happen, either us or the exchange is buggy
+       (or API version conflict); just pass JSON reply to the application */
+    awr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    break;
+  case MHD_HTTP_FORBIDDEN:
+    GNUNET_break_op (0);
+    /* Nothing really to verify, exchange says one of the signatures is
+       invalid; as we checked them, this should never happen, we
+       should pass the JSON reply to the application */
+    awr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    break;
+  case MHD_HTTP_NOT_FOUND:
+    /* Nothing really to verify, the exchange basically just says
+       that it doesn't know this reserve.  Can happen if we
+       query before the wire transfer went through.
+       We should simply pass the JSON reply to the application. */
+    awr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    break;
+  case MHD_HTTP_CONFLICT:
+    /* The exchange says that the reserve has insufficient funds;
+       check the signatures in the history... */
+    if (GNUNET_OK !=
+        reserve_age_withdraw_payment_required (awh->keys,
+                                               &awh->reserve_pub,
+                                               &awh->amount_with_fee,
+                                               j_response))
+    {
+      GNUNET_break_op (0);
+      awr.hr.http_status = 0;
+      awr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+    }
+    else
+    {
+      awr.hr.ec = TALER_JSON_get_error_code (j_response);
+      awr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    }
+    break;
+  case MHD_HTTP_GONE:
+    /* could happen if denomination was revoked */
+    /* Note: one might want to check /keys for revocation
+       signature here, alas tricky in case our /keys
+       is outdated => left to clients */
+    awr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    break;
+  case MHD_HTTP_INTERNAL_SERVER_ERROR:
+    /* Server had an internal issue; we should retry, but this API
+       leaves this to the application */
+    awr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    break;
+  default:
+    /* unexpected response code */
+    GNUNET_break_op (0);
+    awr.hr.ec = TALER_JSON_get_error_code (j_response);
+    awr.hr.hint = TALER_JSON_get_error_hint (j_response);
+    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                "Unexpected response code %u/%d for exchange age-withdraw\n",
+                (unsigned int) response_code,
+                (int) awr.hr.ec);
+    break;
+  }
+  awh->callback (awh->callback_cls,
+                 &awr);
+  TALER_EXCHANGE_age_withdraw_cancel (awh);
+}
+
+
+/**
+ * Runs the actual age-withdraw operation. If there were CS-denominations
+ * involved, started once the all calls to /csr-withdraw are done.
+ *
+ * @param[in,out] awh age withdraw handler
+ */
+static void
+perform_protocol (
+  struct TALER_EXCHANGE_AgeWithdrawHandle *awh)
+{
+#define FAIL_IF(cond) \
+  do { \
+    if ((cond)) \
+    { \
+      GNUNET_break (! (cond)); \
+      goto ERROR; \
+    } \
+  } while(0)
+
+  struct GNUNET_HashContext *coins_hctx;
+  json_t *j_denoms = NULL;
+  json_t *j_array_candidates = NULL;
+  json_t *j_request_body = NULL;
+  CURL *curlh = NULL;
+
+
+  GNUNET_assert (0 == awh->csr_pending);
+
+  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+              "Attempting to age-withdraw from reserve %s with maximum age 
%d\n",
+              TALER_B2S (&awh->reserve_pub),
+              awh->max_age);
+
+  coins_hctx = GNUNET_CRYPTO_hash_context_start ();
+  FAIL_IF (NULL == coins_hctx);
+
+
+  j_denoms = json_array ();
+  j_array_candidates = json_array ();
+  FAIL_IF ((NULL == j_denoms) ||
+           (NULL == j_array_candidates));
+
+  for (size_t i  = 0; i< awh->num_coins; i++)
+  {
+    /* Build the denomination array */
+    {
+      struct TALER_EXCHANGE_DenomPublicKey *denom =
+        &awh->coin_data[i].denom_pub;
+      json_t *jdenom = GNUNET_JSON_PACK (
+        TALER_JSON_pack_denom_pub (NULL,
+                                   &denom->key));
+
+      FAIL_IF (NULL == jdenom);
+      FAIL_IF (0 < json_array_append_new (j_denoms,
+                                          jdenom));
+
+      /* Build the candidate array */
+      {
+        const struct CoinCandidate *can = awh->coin_data[i].coin_candidates;
+        json_t *j_can = json_array ();
+        FAIL_IF (NULL == j_can);
+
+        for (size_t k = 0; k < TALER_CNC_KAPPA; k++)
+        {
+          json_t *jc = GNUNET_JSON_PACK (
+            TALER_JSON_pack_blinded_planchet (
+              NULL,
+              &can->planchet_detail.blinded_planchet));
+
+          FAIL_IF (NULL == jc);
+          FAIL_IF (0 < json_array_append_new (j_can,
+                                              jc));
+
+          GNUNET_CRYPTO_hash_context_read (coins_hctx,
+                                           &can->blinded_coin_h,
+                                           sizeof(can->blinded_coin_h));
+        }
+      }
+    }
+  }
+
+  /* Sign the request */
+  {
+    struct TALER_AgeWithdrawCommitmentHashP coins_commitment_h;
+
+    GNUNET_CRYPTO_hash_context_finish (coins_hctx,
+                                       &coins_commitment_h.hash);
+
+    TALER_wallet_age_withdraw_sign (&coins_commitment_h,
+                                    &awh->amount_with_fee,
+                                    &awh->age_mask,
+                                    awh->max_age,
+                                    awh->reserve_priv,
+                                    &awh->reserve_sig);
+  }
+
+  /* Initiate the POST-request */
+  j_request_body = GNUNET_JSON_PACK (
+    GNUNET_JSON_pack_array_steal ("denoms_h", j_denoms),
+    GNUNET_JSON_pack_array_steal ("blinded_coin_evs", j_array_candidates),
+    GNUNET_JSON_pack_uint64 ("max_age", awh->max_age),
+    GNUNET_JSON_pack_data_auto ("reserve_sig", &awh->reserve_sig));
+  FAIL_IF (NULL == j_request_body);
+
+  curlh = TALER_EXCHANGE_curl_easy_get_ (awh->request_url);
+  FAIL_IF (NULL == curlh);
+  FAIL_IF (GNUNET_OK !=
+           TALER_curl_easy_post (&awh->post_ctx,
+                                 curlh,
+                                 j_request_body));
+  json_decref (j_request_body);
+  j_request_body = NULL;
+
+  awh->job = GNUNET_CURL_job_add2 (awh->curl_ctx,
+                                   curlh,
+                                   awh->post_ctx.headers,
+                                   &handle_reserve_age_withdraw_finished,
+                                   awh);
+  FAIL_IF (NULL == awh->job);
+
+  /* No errors, return */
+  return;
+
+ERROR:
+  if (NULL != j_denoms)
+    json_decref (j_denoms);
+  if (NULL != j_array_candidates)
+    json_decref (j_array_candidates);
+  if (NULL != j_request_body)
+    json_decref (j_request_body);
+  if (NULL != curlh)
+    curl_easy_cleanup (curlh);
+  TALER_EXCHANGE_age_withdraw_cancel (awh);
+  return;
+#undef FAIL_IF
+}
+
+
+/**
+ * Prepares the request URL for the age-withdraw request
+ *
+ * @param awh The handler
+ */
+static
+enum GNUNET_GenericReturnValue
+prepare_url (
+  struct TALER_EXCHANGE_AgeWithdrawHandle *awh,
+  const char *exchange_url)
+{
+  char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32];
+  char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2];
+  char *end;
+
+  end = GNUNET_STRINGS_data_to_string (
+    &awh->reserve_pub,
+    sizeof (awh->reserve_pub),
+    pub_str,
+    sizeof (pub_str));
+  *end = '\0';
+  GNUNET_snprintf (arg_str,
+                   sizeof (arg_str),
+                   "reserves/%s/age-withdraw",
+                   pub_str);
+
+  awh->request_url = TALER_url_join (exchange_url,
+                                     arg_str,
+                                     NULL);
+  if (NULL == awh->request_url)
+  {
+    GNUNET_break (0);
+    TALER_EXCHANGE_age_withdraw_cancel (awh);
+    return GNUNET_SYSERR;
+  }
+
+  return GNUNET_OK;
+}
+
+
+/**
+ * @brief Function called when CSR withdraw retrieval is finished
+ *
+ * @param cls the `struct CoinCandidate *`
+ * @param csrr replies from the /csr-withdraw request
+ */
+static void
+csr_withdraw_done (
+  void *cls,
+  const struct TALER_EXCHANGE_CsRWithdrawResponse *csrr)
+{
+  struct CoinCandidate *can = cls;
+  struct TALER_EXCHANGE_AgeWithdrawHandle *awh = can->age_withdraw_handle;
+  struct TALER_EXCHANGE_AgeWithdrawResponse awr = { .hr = csrr->hr };
+
+  can->csr_withdraw_handle = NULL;
+
+  switch (csrr->hr.http_status)
+  {
+  case MHD_HTTP_OK:
+    {
+      bool success = true;
+      /* Complete the initialization of the coin with CS denomination */
+      can->alg_values = csrr->details.ok.alg_values;
+      TALER_planchet_setup_coin_priv (&can->secret,
+                                      &can->alg_values,
+                                      &can->coin_priv);
+      TALER_planchet_blinding_secret_create (&can->secret,
+                                             &can->alg_values,
+                                             &can->blinding_key);
+      /* This initializes the 2nd half of the
+         can->planchet_detail.blinded_planchet! */
+      if (GNUNET_OK !=
+          TALER_planchet_prepare (&can->denom_pub->key,
+                                  &can->alg_values,
+                                  &can->blinding_key,
+                                  &can->coin_priv,
+                                  &can->h_age_commitment,
+                                  &can->h_coin_pub,
+                                  &can->planchet_detail))
+      {
+        GNUNET_break (0);
+        success = false;
+        TALER_EXCHANGE_age_withdraw_cancel (awh);
+      }
+
+      if (GNUNET_OK !=
+          TALER_coin_ev_hash (&can->planchet_detail.blinded_planchet,
+                              &can->planchet_detail.denom_pub_hash,
+                              &can->blinded_coin_h))
+      {
+        GNUNET_break (0);
+        success = false;
+        TALER_EXCHANGE_age_withdraw_cancel (awh);
+      }
+
+      awh->csr_pending--;
+
+      /* No more pending requests to /csr-withdraw, we can now perform the
+       * actual age-withdraw operation */
+      if (0 == awh->csr_pending && success)
+        perform_protocol (awh);
+      return;
+    }
+  default:
+    break;
+  }
+
+  awh->callback (awh->callback_cls,
+                 &awr);
+  TALER_EXCHANGE_age_withdraw_cancel (awh);
+}
+
+
+/**
+ * @brief Prepare the coins for the call to age-withdraw and calculates
+ * the total amount with fees.
+ *
+ * For denomination with CS as cipher, initiates the preflight to retrieve the
+ * csr-parameter via /csr-withdraw.
+ *
+ * @param awh The handler to the age-withdraw
+ * @param coin_inputs The input for the individial coin(-candidates)
+ * @param num_coins The number of coins in @e coin_inputs
+ *
+ * @return GNUNET_OK on success, GNUNET_SYSERR on failure
+ */
+static
+enum GNUNET_GenericReturnValue
+prepare_coins (
+  struct TALER_EXCHANGE_AgeWithdrawHandle *awh,
+  const struct TALER_EXCHANGE_AgeWithdrawCoinInput *coin_inputs,
+  size_t num_coins)
+{
+
+  if (GNUNET_OK != TALER_amount_set_zero (
+        awh->keys->currency,
+        &awh->amount_with_fee))
+  {
+    GNUNET_break (0);
+    return GNUNET_SYSERR;
+  }
+
+  awh->coin_data = GNUNET_new_array (awh->num_coins,
+                                     struct CoinData);
+
+  GNUNET_assert (0 < num_coins);
+  awh->age_mask = coin_inputs[0].denom_pub->key.age_mask;
+
+  for (size_t i = 0; i < num_coins; i++)
+  {
+    struct CoinData *cd = &awh->coin_data[i];
+    const struct TALER_EXCHANGE_AgeWithdrawCoinInput *input = &coin_inputs[i];
+    cd->denom_pub = *input->denom_pub;
+
+    /* The mask must be the same for all coins */
+    if (awh->age_mask.bits != input->denom_pub->key.age_mask.bits)
+    {
+      GNUNET_break (0);
+      TALER_EXCHANGE_age_withdraw_cancel (awh);
+      return GNUNET_SYSERR;
+    }
+
+    TALER_denom_pub_deep_copy (&cd->denom_pub.key,
+                               &input->denom_pub->key);
+
+    /* Accumulate total value with fees */
+    {
+      struct TALER_Amount coin_total;
+
+      if (0 >
+          TALER_amount_add (&coin_total,
+                            &cd->denom_pub.fees.withdraw,
+                            &cd->denom_pub.value))
+      {
+        GNUNET_break (0);
+        TALER_EXCHANGE_age_withdraw_cancel (awh);
+        return GNUNET_SYSERR;
+      }
+
+      if (0 >
+          TALER_amount_add (&awh->amount_with_fee,
+                            &awh->amount_with_fee,
+                            &coin_total))
+      {
+        /* Overflow here? Very strange, our CPU must be fried... */
+        GNUNET_break (0);
+        TALER_EXCHANGE_age_withdraw_cancel (awh);
+        return GNUNET_SYSERR;
+      }
+    }
+
+    for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++)
+    {
+      struct CoinCandidate *can = &cd->coin_candidates[k];
+
+      can->secret = input->secret[k];
+
+      /* Derive the age restriction from the given secret and
+       * the maximum age */
+      GNUNET_assert (GNUNET_OK ==
+                     TALER_age_restriction_from_secret (
+                       &can->secret,
+                       &input->denom_pub->key.age_mask,
+                       awh->max_age,
+                       &can->age_commitment_proof));
+      TALER_age_commitment_hash (&can->age_commitment_proof.commitment,
+                                 &can->h_age_commitment);
+
+      switch (input->denom_pub->key.cipher)
+      {
+      case TALER_DENOMINATION_RSA:
+        {
+          can->alg_values.cipher = TALER_DENOMINATION_RSA;
+          TALER_planchet_setup_coin_priv (&can->secret,
+                                          &can->alg_values,
+                                          &can->coin_priv);
+          TALER_planchet_blinding_secret_create (&can->secret,
+                                                 &can->alg_values,
+                                                 &can->blinding_key);
+          if (GNUNET_OK !=
+              TALER_planchet_prepare (&cd->denom_pub.key,
+                                      &can->alg_values,
+                                      &can->blinding_key,
+                                      &can->coin_priv,
+                                      &can->h_age_commitment,
+                                      &can->h_coin_pub,
+                                      &can->planchet_detail))
+          {
+            GNUNET_break (0);
+            TALER_EXCHANGE_age_withdraw_cancel (awh);
+            return GNUNET_SYSERR;
+          }
+          if (GNUNET_OK !=
+              TALER_coin_ev_hash (&can->planchet_detail.blinded_planchet,
+                                  &can->planchet_detail.denom_pub_hash,
+                                  &can->blinded_coin_h))
+          {
+            GNUNET_break (0);
+            TALER_EXCHANGE_age_withdraw_cancel (awh);
+            return GNUNET_SYSERR;
+          }
+          break;
+        }
+      case TALER_DENOMINATION_CS:
+        {
+          /**
+           * Save the handler and the denomination for the callback
+           * after the call to csr-withdraw */
+          can->age_withdraw_handle = awh;
+          can->denom_pub = &cd->denom_pub;
+
+          TALER_cs_withdraw_nonce_derive (
+            &can->secret,
+            &can->planchet_detail
+            .blinded_planchet
+            .details
+            .cs_blinded_planchet
+            .nonce);
+
+          /* Note that we only initialize the first half
+             of the blinded_planchet here; the other part
+             will be done after the /csr-withdraw request! */
+          can->planchet_detail.blinded_planchet.cipher = TALER_DENOMINATION_CS;
+          can->csr_withdraw_handle = NULL;
+          TALER_EXCHANGE_csr_withdraw (
+            awh->curl_ctx,
+            awh->exchange_url,
+            &cd->denom_pub,
+            &can->planchet_detail
+            .blinded_planchet
+            .details
+            .cs_blinded_planchet
+            .nonce,
+            &csr_withdraw_done,
+            &can);
+          if (NULL == can->csr_withdraw_handle)
+          {
+            GNUNET_break (0);
+            TALER_EXCHANGE_age_withdraw_cancel (awh);
+            return GNUNET_SYSERR;
+          }
+
+          awh->csr_pending++;
+          break;
+        }
+      default:
+        GNUNET_break (0);
+        TALER_EXCHANGE_age_withdraw_cancel (awh);
+        return GNUNET_SYSERR;
+      }
+    }
+  }
+  return GNUNET_OK;
+};
+
+struct TALER_EXCHANGE_AgeWithdrawHandle *
+TALER_EXCHANGE_age_withdraw (
+  struct GNUNET_CURL_Context *curl_ctx,
+  const char *exchange_url,
+  struct TALER_EXCHANGE_Keys *keys,
+  const struct TALER_ReservePrivateKeyP *reserve_priv,
+  const struct TALER_EXCHANGE_AgeWithdrawCoinInput *coin_inputs,
+  size_t num_coins,
+  uint8_t max_age,
+  TALER_EXCHANGE_AgeWithdrawCallback res_cb,
+  void *res_cb_cls)
+{
+  struct TALER_EXCHANGE_AgeWithdrawHandle *awh;
+
+  awh = GNUNET_new (struct TALER_EXCHANGE_AgeWithdrawHandle);
+  awh->exchange_url = exchange_url;
+  awh->keys = TALER_EXCHANGE_keys_incref (keys);
+  awh->curl_ctx = curl_ctx;
+  awh->reserve_priv = reserve_priv;
+  awh->callback = res_cb;
+  awh->callback_cls = res_cb_cls;
+  awh->num_coins = num_coins;
+  awh->max_age = max_age;
+
+  GNUNET_CRYPTO_eddsa_key_get_public (&awh->reserve_priv->eddsa_priv,
+                                      &awh->reserve_pub.eddsa_pub);
+
+
+  if (GNUNET_OK != prepare_url (awh,
+                                exchange_url))
+    return NULL;
+
+  if (GNUNET_OK != prepare_coins (awh,
+                                  coin_inputs,
+                                  num_coins))
+    return NULL;
+
+  /* If there were no CS denominations, we can now perform the actual
+   * age-withdraw protocol.  Otherwise, there are calls to /csr-withdraw
+   * in flight and once they finish, the age-withdraw-protocol will be
+   * called from within the csr_withdraw_done-function.
+   */
+  if (0 == awh->csr_pending)
+    perform_protocol (awh);
+
+  return awh;
+}
+
+
+void
+TALER_EXCHANGE_age_withdraw_cancel (
+  struct TALER_EXCHANGE_AgeWithdrawHandle *awh)
+{
+  /* Cleanup coin data */
+  for (unsigned int i = 0; i<awh->num_coins; i++)
+  {
+    struct CoinData *cd = &awh->coin_data[i];
+
+    for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++)
+    {
+      struct CoinCandidate *can = &cd->coin_candidates[k];
+
+      if (NULL != can->csr_withdraw_handle)
+      {
+        TALER_EXCHANGE_csr_withdraw_cancel (can->csr_withdraw_handle);
+        can->csr_withdraw_handle = NULL;
+      }
+      TALER_blinded_planchet_free (&can->planchet_detail.blinded_planchet);
+    }
+    TALER_denom_pub_free (&cd->denom_pub.key);
+  }
+  GNUNET_free (awh->coin_data);
+
+  /* Cleanup CURL job data */
+  if (NULL != awh->job)
+  {
+    GNUNET_CURL_job_cancel (awh->job);
+    awh->job = NULL;
+  }
+  TALER_curl_easy_post_finished (&awh->post_ctx);
+  TALER_EXCHANGE_keys_decref (awh->keys);
+  GNUNET_free (awh->request_url);
+  GNUNET_free (awh);
+
+}
+
+
+/* exchange_api_age_withdraw.c */
diff --git a/src/lib/exchange_api_batch_withdraw.c 
b/src/lib/exchange_api_batch_withdraw.c
index 4817ae40..07ad37f6 100644
--- a/src/lib/exchange_api_batch_withdraw.c
+++ b/src/lib/exchange_api_batch_withdraw.c
@@ -97,9 +97,20 @@ struct TALER_EXCHANGE_BatchWithdrawHandle
 {
 
   /**
-   * The connection to exchange this request handle will use
+   * The curl context to use
    */
-  struct TALER_EXCHANGE_Handle *exchange;
+  struct GNUNET_CURL_Context *curl_ctx;
+
+  /**
+   * The base URL to the exchange
+   */
+  const char *exchange_url;
+
+  /**
+   * The /keys information from the exchange
+   */
+  const struct TALER_EXCHANGE_Keys *keys;
+
 
   /**
    * Handle for the actual (internal) batch withdraw operation.
@@ -255,7 +266,9 @@ phase_two (struct TALER_EXCHANGE_BatchWithdrawHandle *wh)
     pds[i] = cd->pd;
   }
   wh->wh2 = TALER_EXCHANGE_batch_withdraw2 (
-    wh->exchange,
+    wh->curl_ctx,
+    wh->exchange_url,
+    wh->keys,
     wh->reserve_priv,
     pds,
     wh->num_coins,
@@ -322,7 +335,9 @@ withdraw_cs_stage_two_callback (
 
 struct TALER_EXCHANGE_BatchWithdrawHandle *
 TALER_EXCHANGE_batch_withdraw (
-  struct TALER_EXCHANGE_Handle *exchange,
+  struct GNUNET_CURL_Context *curl_ctx,
+  const char *exchange_url,
+  const struct TALER_EXCHANGE_Keys *keys,
   const struct TALER_ReservePrivateKeyP *reserve_priv,
   const struct TALER_EXCHANGE_WithdrawCoinInput *wcis,
   unsigned int wci_length,
@@ -332,7 +347,9 @@ TALER_EXCHANGE_batch_withdraw (
   struct TALER_EXCHANGE_BatchWithdrawHandle *wh;
 
   wh = GNUNET_new (struct TALER_EXCHANGE_BatchWithdrawHandle);
-  wh->exchange = exchange;
+  wh->curl_ctx = curl_ctx;
+  wh->exchange_url = exchange_url;
+  wh->keys = keys;
   wh->cb = res_cb;
   wh->cb_cls = res_cb_cls;
   wh->reserve_priv = reserve_priv;
@@ -386,7 +403,8 @@ TALER_EXCHANGE_batch_withdraw (
            will be done after the /csr-withdraw request! */
         cd->pd.blinded_planchet.cipher = TALER_DENOMINATION_CS;
         cd->csrh = TALER_EXCHANGE_csr_withdraw (
-          exchange,
+          curl_ctx,
+          exchange_url,
           &cd->pk,
           &cd->pd.blinded_planchet.details.cs_blinded_planchet.nonce,
           &withdraw_cs_stage_two_callback,
diff --git a/src/lib/exchange_api_batch_withdraw2.c 
b/src/lib/exchange_api_batch_withdraw2.c
index 6dd421ce..a855fc57 100644
--- a/src/lib/exchange_api_batch_withdraw2.c
+++ b/src/lib/exchange_api_batch_withdraw2.c
@@ -39,14 +39,14 @@ struct TALER_EXCHANGE_BatchWithdraw2Handle
 {
 
   /**
-   * The connection to exchange this request handle will use
+   * The url for this request.
    */
-  struct TALER_EXCHANGE_Handle *exchange;
+  char *url;
 
   /**
-   * The url for this request.
+   * The /keys material from the exchange
    */
-  char *url;
+  const struct TALER_EXCHANGE_Keys *keys;
 
   /**
    * Handle for the request.
@@ -219,7 +219,7 @@ reserve_batch_withdraw_payment_required (
 
     if (GNUNET_OK !=
         TALER_EXCHANGE_parse_reserve_history (
-          TALER_EXCHANGE_get_keys (wh->exchange),
+          wh->keys,
           history,
           &wh->reserve_pub,
           balance.currency,
@@ -387,7 +387,9 @@ handle_reserve_batch_withdraw_finished (void *cls,
 
 struct TALER_EXCHANGE_BatchWithdraw2Handle *
 TALER_EXCHANGE_batch_withdraw2 (
-  struct TALER_EXCHANGE_Handle *exchange,
+  struct GNUNET_CURL_Context *curl_ctx,
+  const char *exchange_url,
+  const struct TALER_EXCHANGE_Keys *keys,
   const struct TALER_ReservePrivateKeyP *reserve_priv,
   const struct TALER_PlanchetDetail *pds,
   unsigned int pds_length,
@@ -395,21 +397,15 @@ TALER_EXCHANGE_batch_withdraw2 (
   void *res_cb_cls)
 {
   struct TALER_EXCHANGE_BatchWithdraw2Handle *wh;
-  const struct TALER_EXCHANGE_Keys *keys;
   const struct TALER_EXCHANGE_DenomPublicKey *dk;
   struct TALER_ReserveSignatureP reserve_sig;
   char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32];
   struct TALER_BlindedCoinHashP bch;
   json_t *jc;
 
-  keys = TALER_EXCHANGE_get_keys (exchange);
-  if (NULL == keys)
-  {
-    GNUNET_break (0);
-    return NULL;
-  }
+  GNUNET_assert (NULL != keys);
   wh = GNUNET_new (struct TALER_EXCHANGE_BatchWithdraw2Handle);
-  wh->exchange = exchange;
+  wh->keys = keys;
   wh->cb = res_cb;
   wh->cb_cls = res_cb_cls;
   wh->num_coins = pds_length;
@@ -430,14 +426,15 @@ TALER_EXCHANGE_batch_withdraw2 (
     *end = '\0';
     GNUNET_snprintf (arg_str,
                      sizeof (arg_str),
-                     "/reserves/%s/batch-withdraw",
+                     "reserves/%s/batch-withdraw",
                      pub_str);
   }
   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
               "Attempting to batch-withdraw from reserve %s\n",
               TALER_B2S (&wh->reserve_pub));
-  wh->url = TEAH_path_to_url (exchange,
-                              arg_str);
+  wh->url = TALER_url_join (exchange_url,
+                            arg_str,
+                            NULL);
   if (NULL == wh->url)
   {
     GNUNET_break (0);
@@ -513,13 +510,11 @@ TALER_EXCHANGE_batch_withdraw2 (
   }
   {
     CURL *eh;
-    struct GNUNET_CURL_Context *ctx;
     json_t *req;
 
     req = GNUNET_JSON_PACK (
       GNUNET_JSON_pack_array_steal ("planchets",
                                     jc));
-    ctx = TEAH_handle_to_context (exchange);
     eh = TALER_EXCHANGE_curl_easy_get_ (wh->url);
     if ( (NULL == eh) ||
          (GNUNET_OK !=
@@ -535,7 +530,7 @@ TALER_EXCHANGE_batch_withdraw2 (
       return NULL;
     }
     json_decref (req);
-    wh->job = GNUNET_CURL_job_add2 (ctx,
+    wh->job = GNUNET_CURL_job_add2 (curl_ctx,
                                     eh,
                                     wh->post_ctx.headers,
                                     &handle_reserve_batch_withdraw_finished,
diff --git a/src/lib/exchange_api_common.c b/src/lib/exchange_api_common.c
index f43673d7..77a5cf81 100644
--- a/src/lib/exchange_api_common.c
+++ b/src/lib/exchange_api_common.c
@@ -36,7 +36,7 @@ struct HistoryParseContext
   /**
    * Keys of the exchange we use.
    */
-  struct TALER_EXCHANGE_Keys *keys;
+  const struct TALER_EXCHANGE_Keys *keys;
 
   /**
    * Our reserve public key.
@@ -647,7 +647,7 @@ parse_close (struct TALER_EXCHANGE_ReserveHistoryEntry *rh,
 
 enum GNUNET_GenericReturnValue
 TALER_EXCHANGE_parse_reserve_history (
-  struct TALER_EXCHANGE_Keys *keys,
+  const struct TALER_EXCHANGE_Keys *keys,
   const json_t *history,
   const struct TALER_ReservePublicKeyP *reserve_pub,
   const char *currency,
diff --git a/src/lib/exchange_api_csr_withdraw.c 
b/src/lib/exchange_api_csr_withdraw.c
index fca3fff8..4c1d83a9 100644
--- a/src/lib/exchange_api_csr_withdraw.c
+++ b/src/lib/exchange_api_csr_withdraw.c
@@ -38,11 +38,6 @@
  */
 struct TALER_EXCHANGE_CsRWithdrawHandle
 {
-  /**
-   * The connection to exchange this request handle will use
-   */
-  struct TALER_EXCHANGE_Handle *exchange;
-
   /**
    * Function to call with the result.
    */
@@ -204,11 +199,13 @@ handle_csr_finished (void *cls,
 
 
 struct TALER_EXCHANGE_CsRWithdrawHandle *
-TALER_EXCHANGE_csr_withdraw (struct TALER_EXCHANGE_Handle *exchange,
-                             const struct TALER_EXCHANGE_DenomPublicKey *pk,
-                             const struct TALER_CsNonce *nonce,
-                             TALER_EXCHANGE_CsRWithdrawCallback res_cb,
-                             void *res_cb_cls)
+TALER_EXCHANGE_csr_withdraw (
+  struct GNUNET_CURL_Context *curl_ctx,
+  const char *exchange_url,
+  const struct TALER_EXCHANGE_DenomPublicKey *pk,
+  const struct TALER_CsNonce *nonce,
+  TALER_EXCHANGE_CsRWithdrawCallback res_cb,
+  void *res_cb_cls)
 {
   struct TALER_EXCHANGE_CsRWithdrawHandle *csrh;
 
@@ -218,11 +215,11 @@ TALER_EXCHANGE_csr_withdraw (struct TALER_EXCHANGE_Handle 
*exchange,
     return NULL;
   }
   csrh = GNUNET_new (struct TALER_EXCHANGE_CsRWithdrawHandle);
-  csrh->exchange = exchange;
   csrh->cb = res_cb;
   csrh->cb_cls = res_cb_cls;
-  csrh->url = TEAH_path_to_url (exchange,
-                                "/csr-withdraw");
+  csrh->url = TALER_url_join (exchange_url,
+                              "csr-withdraw",
+                              NULL);
   if (NULL == csrh->url)
   {
     GNUNET_free (csrh);
@@ -231,7 +228,6 @@ TALER_EXCHANGE_csr_withdraw (struct TALER_EXCHANGE_Handle 
*exchange,
 
   {
     CURL *eh;
-    struct GNUNET_CURL_Context *ctx;
     json_t *req;
 
     req = GNUNET_JSON_PACK (
@@ -242,7 +238,6 @@ TALER_EXCHANGE_csr_withdraw (struct TALER_EXCHANGE_Handle 
*exchange,
                                      &pk->h_key,
                                      sizeof(struct TALER_DenominationHashP)));
     GNUNET_assert (NULL != req);
-    ctx = TEAH_handle_to_context (exchange);
     eh = TALER_EXCHANGE_curl_easy_get_ (csrh->url);
     if ( (NULL == eh) ||
          (GNUNET_OK !=
@@ -259,7 +254,7 @@ TALER_EXCHANGE_csr_withdraw (struct TALER_EXCHANGE_Handle 
*exchange,
       return NULL;
     }
     json_decref (req);
-    csrh->job = GNUNET_CURL_job_add2 (ctx,
+    csrh->job = GNUNET_CURL_job_add2 (curl_ctx,
                                       eh,
                                       csrh->post_ctx.headers,
                                       &handle_csr_finished,
diff --git a/src/lib/exchange_api_curl_defaults.h 
b/src/lib/exchange_api_curl_defaults.h
index 009d72ab..c4ba04fc 100644
--- a/src/lib/exchange_api_curl_defaults.h
+++ b/src/lib/exchange_api_curl_defaults.h
@@ -25,7 +25,6 @@
 #define _TALER_CURL_DEFAULTS_H
 
 
-#include "platform.h"
 #include <gnunet/gnunet_curl_lib.h>
 
 
diff --git a/src/lib/exchange_api_handle.c b/src/lib/exchange_api_handle.c
index fe73f050..0bb3c208 100644
--- a/src/lib/exchange_api_handle.c
+++ b/src/lib/exchange_api_handle.c
@@ -1229,6 +1229,8 @@ TALER_EXCHANGE_check_keys_current (struct 
TALER_EXCHANGE_Handle *exchange,
   bool force_download = 0 != (flags & TALER_EXCHANGE_CKF_FORCE_DOWNLOAD);
   bool pull_all_keys = 0 != (flags & TALER_EXCHANGE_CKF_PULL_ALL_KEYS);
 
+  GNUNET_assert (NULL != exchange);
+
   if ( (NULL != cb) &&
        ( (exchange->cert_cb != cb) ||
          (exchange->cert_cb_cls != cb_cls) ) )
diff --git a/src/lib/exchange_api_melt.c b/src/lib/exchange_api_melt.c
index 1da4184d..f19f98c5 100644
--- a/src/lib/exchange_api_melt.c
+++ b/src/lib/exchange_api_melt.c
@@ -504,6 +504,7 @@ csr_cb (void *cls,
 }
 
 
+/* FIXME: refactor this to use struct TALER_EXCHANGE_Handle */
 struct TALER_EXCHANGE_MeltHandle *
 TALER_EXCHANGE_melt (
   struct GNUNET_CURL_Context *ctx,
diff --git a/src/lib/exchange_api_refreshes_reveal.c 
b/src/lib/exchange_api_refreshes_reveal.c
index 9b6f9b19..e698a297 100644
--- a/src/lib/exchange_api_refreshes_reveal.c
+++ b/src/lib/exchange_api_refreshes_reveal.c
@@ -303,6 +303,7 @@ handle_refresh_reveal_finished (void *cls,
 }
 
 
+/* FIXME: refactor this to use struct TALER_EXCHANGE_Handle */
 struct TALER_EXCHANGE_RefreshesRevealHandle *
 TALER_EXCHANGE_refreshes_reveal (
   struct GNUNET_CURL_Context *ctx,
diff --git a/src/lib/exchange_api_withdraw.c b/src/lib/exchange_api_withdraw.c
index 2a3c095a..87218989 100644
--- a/src/lib/exchange_api_withdraw.c
+++ b/src/lib/exchange_api_withdraw.c
@@ -39,9 +39,19 @@ struct TALER_EXCHANGE_WithdrawHandle
 {
 
   /**
-   * The connection to exchange this request handle will use
+   * The curl context to use
    */
-  struct TALER_EXCHANGE_Handle *exchange;
+  struct GNUNET_CURL_Context *curl_ctx;
+
+  /**
+   * The base-URL to the exchange
+   */
+  const char *exchange_url;
+
+  /**
+   * The /keys material from the exchange
+   */
+  struct TALER_EXCHANGE_Keys *keys;
 
   /**
    * Handle for the actual (internal) withdraw operation.
@@ -232,7 +242,9 @@ withdraw_cs_stage_two_callback (
       GNUNET_break (0);
       break;
     }
-    wh->wh2 = TALER_EXCHANGE_withdraw2 (wh->exchange,
+    wh->wh2 = TALER_EXCHANGE_withdraw2 (wh->curl_ctx,
+                                        wh->exchange_url,
+                                        wh->keys,
                                         &wh->pd,
                                         wh->reserve_priv,
                                         &handle_reserve_withdraw_finished,
@@ -249,7 +261,9 @@ withdraw_cs_stage_two_callback (
 
 struct TALER_EXCHANGE_WithdrawHandle *
 TALER_EXCHANGE_withdraw (
-  struct TALER_EXCHANGE_Handle *exchange,
+  struct GNUNET_CURL_Context *curl_ctx,
+  const char *exchange_url,
+  struct TALER_EXCHANGE_Keys *keys,
   const struct TALER_ReservePrivateKeyP *reserve_priv,
   const struct TALER_EXCHANGE_WithdrawCoinInput *wci,
   TALER_EXCHANGE_WithdrawCallback res_cb,
@@ -258,7 +272,9 @@ TALER_EXCHANGE_withdraw (
   struct TALER_EXCHANGE_WithdrawHandle *wh;
 
   wh = GNUNET_new (struct TALER_EXCHANGE_WithdrawHandle);
-  wh->exchange = exchange;
+  wh->keys = TALER_EXCHANGE_keys_incref (keys);
+  wh->exchange_url = exchange_url;
+  wh->curl_ctx = curl_ctx;
   wh->cb = res_cb;
   wh->cb_cls = res_cb_cls;
   wh->reserve_priv = reserve_priv;
@@ -292,7 +308,9 @@ TALER_EXCHANGE_withdraw (
         GNUNET_free (wh);
         return NULL;
       }
-      wh->wh2 = TALER_EXCHANGE_withdraw2 (exchange,
+      wh->wh2 = TALER_EXCHANGE_withdraw2 (curl_ctx,
+                                          exchange_url,
+                                          keys,
                                           &wh->pd,
                                           wh->reserve_priv,
                                           &handle_reserve_withdraw_finished,
@@ -309,7 +327,8 @@ TALER_EXCHANGE_withdraw (
          will be done after the /csr-withdraw request! */
       wh->pd.blinded_planchet.cipher = TALER_DENOMINATION_CS;
       wh->csrh = TALER_EXCHANGE_csr_withdraw (
-        exchange,
+        curl_ctx,
+        exchange_url,
         &wh->pk,
         &wh->pd.blinded_planchet.details.cs_blinded_planchet.nonce,
         &withdraw_cs_stage_two_callback,
@@ -339,6 +358,7 @@ TALER_EXCHANGE_withdraw_cancel (struct 
TALER_EXCHANGE_WithdrawHandle *wh)
     TALER_EXCHANGE_withdraw2_cancel (wh->wh2);
     wh->wh2 = NULL;
   }
+  TALER_EXCHANGE_keys_decref (wh->keys);
   TALER_denom_pub_free (&wh->pk.key);
   GNUNET_free (wh);
 }
diff --git a/src/lib/exchange_api_withdraw2.c b/src/lib/exchange_api_withdraw2.c
index daac429c..11028ed5 100644
--- a/src/lib/exchange_api_withdraw2.c
+++ b/src/lib/exchange_api_withdraw2.c
@@ -39,9 +39,9 @@ struct TALER_EXCHANGE_Withdraw2Handle
 {
 
   /**
-   * The connection to exchange this request handle will use
+   * The /keys material from the exchange
    */
-  struct TALER_EXCHANGE_Handle *exchange;
+  struct TALER_EXCHANGE_Keys *keys;
 
   /**
    * The url for this request.
@@ -192,8 +192,7 @@ reserve_withdraw_payment_required (
     }
 
     if (GNUNET_OK !=
-        TALER_EXCHANGE_parse_reserve_history (TALER_EXCHANGE_get_keys (
-                                                wh->exchange),
+        TALER_EXCHANGE_parse_reserve_history (wh->keys,
                                               history,
                                               &wh->reserve_pub,
                                               balance.currency,
@@ -361,25 +360,21 @@ handle_reserve_withdraw_finished (void *cls,
 
 struct TALER_EXCHANGE_Withdraw2Handle *
 TALER_EXCHANGE_withdraw2 (
-  struct TALER_EXCHANGE_Handle *exchange,
+  struct GNUNET_CURL_Context *curl_ctx,
+  const char *exchange_url,
+  struct TALER_EXCHANGE_Keys *keys,
   const struct TALER_PlanchetDetail *pd,
   const struct TALER_ReservePrivateKeyP *reserve_priv,
   TALER_EXCHANGE_Withdraw2Callback res_cb,
   void *res_cb_cls)
 {
   struct TALER_EXCHANGE_Withdraw2Handle *wh;
-  const struct TALER_EXCHANGE_Keys *keys;
   const struct TALER_EXCHANGE_DenomPublicKey *dk;
   struct TALER_ReserveSignatureP reserve_sig;
   char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32];
   struct TALER_BlindedCoinHashP bch;
 
-  keys = TALER_EXCHANGE_get_keys (exchange);
-  if (NULL == keys)
-  {
-    GNUNET_break (0);
-    return NULL;
-  }
+  GNUNET_assert (NULL != keys);
   dk = TALER_EXCHANGE_get_denomination_key_by_hash (keys,
                                                     &pd->denom_pub_hash);
   if (NULL == dk)
@@ -388,7 +383,7 @@ TALER_EXCHANGE_withdraw2 (
     return NULL;
   }
   wh = GNUNET_new (struct TALER_EXCHANGE_Withdraw2Handle);
-  wh->exchange = exchange;
+  wh->keys = TALER_EXCHANGE_keys_incref (keys);
   wh->cb = res_cb;
   wh->cb_cls = res_cb_cls;
   /* Compute how much we expected to charge to the reserve */
@@ -418,7 +413,7 @@ TALER_EXCHANGE_withdraw2 (
     *end = '\0';
     GNUNET_snprintf (arg_str,
                      sizeof (arg_str),
-                     "/reserves/%s/withdraw",
+                     "reserves/%s/withdraw",
                      pub_str);
   }
 
@@ -448,8 +443,9 @@ TALER_EXCHANGE_withdraw2 (
     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                 "Attempting to withdraw from reserve %s\n",
                 TALER_B2S (&wh->reserve_pub));
-    wh->url = TEAH_path_to_url (exchange,
-                                arg_str);
+    wh->url = TALER_url_join (exchange_url,
+                              arg_str,
+                              NULL);
     if (NULL == wh->url)
     {
       json_decref (withdraw_obj);
@@ -458,9 +454,7 @@ TALER_EXCHANGE_withdraw2 (
     }
     {
       CURL *eh;
-      struct GNUNET_CURL_Context *ctx;
 
-      ctx = TEAH_handle_to_context (exchange);
       eh = TALER_EXCHANGE_curl_easy_get_ (wh->url);
       if ( (NULL == eh) ||
            (GNUNET_OK !=
@@ -477,7 +471,7 @@ TALER_EXCHANGE_withdraw2 (
         return NULL;
       }
       json_decref (withdraw_obj);
-      wh->job = GNUNET_CURL_job_add2 (ctx,
+      wh->job = GNUNET_CURL_job_add2 (curl_ctx,
                                       eh,
                                       wh->post_ctx.headers,
                                       &handle_reserve_withdraw_finished,
@@ -498,5 +492,6 @@ TALER_EXCHANGE_withdraw2_cancel (struct 
TALER_EXCHANGE_Withdraw2Handle *wh)
   }
   GNUNET_free (wh->url);
   TALER_curl_easy_post_finished (&wh->post_ctx);
+  TALER_EXCHANGE_keys_decref (wh->keys);
   GNUNET_free (wh);
 }
diff --git a/src/testing/testing_api_cmd_batch_withdraw.c 
b/src/testing/testing_api_cmd_batch_withdraw.c
index e0b8285a..9283f032 100644
--- a/src/testing/testing_api_cmd_batch_withdraw.c
+++ b/src/testing/testing_api_cmd_batch_withdraw.c
@@ -321,12 +321,15 @@ batch_withdraw_run (void *cls,
     wci->ps = &cs->ps;
     wci->ach = cs->h_age_commitment;
   }
-  ws->wsh = TALER_EXCHANGE_batch_withdraw (exchange,
-                                           rp,
-                                           wcis,
-                                           ws->num_coins,
-                                           &reserve_batch_withdraw_cb,
-                                           ws);
+  ws->wsh = TALER_EXCHANGE_batch_withdraw (
+    TALER_TESTING_interpreter_get_context (is),
+    TALER_TESTING_get_exchange_url (is),
+    TALER_TESTING_get_keys (is),
+    rp,
+    wcis,
+    ws->num_coins,
+    &reserve_batch_withdraw_cb,
+    ws);
   if (NULL == ws->wsh)
   {
     GNUNET_break (0);
diff --git a/src/testing/testing_api_cmd_oauth.c 
b/src/testing/testing_api_cmd_oauth.c
index 0bcf2f68..fcf7e843 100644
--- a/src/testing/testing_api_cmd_oauth.c
+++ b/src/testing/testing_api_cmd_oauth.c
@@ -39,6 +39,11 @@ struct OAuthState
    */
   struct MHD_Daemon *mhd;
 
+  /**
+   * Birthdate that the oauth server should return in a response, may be NULL
+   */
+  const char *birthdate;
+
   /**
    * Port to listen on.
    */
@@ -172,28 +177,33 @@ handler_cb (void *cls,
             void **con_cls)
 {
   struct RequestCtx *rc = *con_cls;
+  struct OAuthState *oas = cls;
   unsigned int hc;
   json_t *body;
 
-  (void) cls;
   (void) version;
   if (0 == strcasecmp (method,
                        MHD_HTTP_METHOD_GET))
   {
+    json_t *data =
+      GNUNET_JSON_PACK (
+        GNUNET_JSON_pack_string ("id",
+                                 "XXXID12345678"),
+        GNUNET_JSON_pack_string ("first_name",
+                                 "Bob"),
+        GNUNET_JSON_pack_string ("last_name",
+                                 "Builder"));
+    if (NULL != oas->birthdate)
+      json_object_set_new (data,
+                           "birthdate",
+                           json_string_nocheck (oas->birthdate));
+
     body = GNUNET_JSON_PACK (
       GNUNET_JSON_pack_string (
         "status",
         "success"),
       GNUNET_JSON_pack_object_steal (
-        "data",
-        GNUNET_JSON_PACK (
-          GNUNET_JSON_pack_string ("id",
-                                   "XXXID12345678"),
-          GNUNET_JSON_pack_string ("first_name",
-                                   "Bob"),
-          GNUNET_JSON_pack_string ("last_name",
-                                   "Builder")
-          )));
+        "data", data));
     return TALER_MHD_reply_json_steal (connection,
                                        body,
                                        MHD_HTTP_OK);
@@ -368,13 +378,15 @@ oauth_cleanup (void *cls,
 
 
 struct TALER_TESTING_Command
-TALER_TESTING_cmd_oauth (const char *label,
-                         uint16_t port)
+TALER_TESTING_cmd_oauth_with_birthdate (const char *label,
+                                        const char *birthdate,
+                                        uint16_t port)
 {
   struct OAuthState *oas;
 
   oas = GNUNET_new (struct OAuthState);
   oas->port = port;
+  oas->birthdate = birthdate;
   {
     struct TALER_TESTING_Command cmd = {
       .cls = oas,
diff --git a/src/testing/testing_api_cmd_withdraw.c 
b/src/testing/testing_api_cmd_withdraw.c
index 8188eeae..a6315f91 100644
--- a/src/testing/testing_api_cmd_withdraw.c
+++ b/src/testing/testing_api_cmd_withdraw.c
@@ -410,7 +410,7 @@ withdraw_run (void *cls,
 
   if (NULL == ws->pk)
   {
-    dpk = TALER_TESTING_find_pk (TALER_EXCHANGE_get_keys (exchange),
+    dpk = TALER_TESTING_find_pk (TALER_TESTING_get_keys (is),
                                  &ws->amount,
                                  ws->age > 0);
     if (NULL == dpk)
@@ -443,11 +443,14 @@ withdraw_run (void *cls,
       .ps = &ws->ps,
       .ach = ws->h_age_commitment
     };
-    ws->wsh = TALER_EXCHANGE_withdraw (exchange,
-                                       rp,
-                                       &wci,
-                                       &reserve_withdraw_cb,
-                                       ws);
+    ws->wsh = TALER_EXCHANGE_withdraw (
+      TALER_TESTING_interpreter_get_context (is),
+      TALER_TESTING_get_exchange_url (is),
+      TALER_TESTING_get_keys (is),
+      rp,
+      &wci,
+      &reserve_withdraw_cb,
+      ws);
   }
   if (NULL == ws->wsh)
   {
diff --git a/src/util/age_restriction.c b/src/util/age_restriction.c
index f0d99fe6..6f070229 100644
--- a/src/util/age_restriction.c
+++ b/src/util/age_restriction.c
@@ -23,6 +23,7 @@
 #include "taler_signatures.h"
 #include <gnunet/gnunet_json_lib.h>
 #include <gcrypt.h>
+#include <stdint.h>
 
 struct
 #ifndef AGE_RESTRICTION_WITH_ECDSA
@@ -76,7 +77,7 @@ TALER_age_commitment_hash (
  * defined by the given mask.
  */
 uint8_t
-get_age_group (
+TALER_get_age_group (
   const struct TALER_AgeMask *mask,
   uint8_t age)
 {
@@ -95,6 +96,27 @@ get_age_group (
 }
 
 
+uint8_t
+TALER_get_lowest_age (
+  const struct TALER_AgeMask *mask,
+  uint8_t age)
+{
+  uint32_t m = mask->bits;
+  uint8_t group = TALER_get_age_group (mask, age);
+  uint8_t lowest = 0;
+
+  while (group > 0)
+  {
+    m = m >> 1;
+    if (m & 1)
+      group--;
+    lowest++;
+  }
+
+  return lowest;
+}
+
+
 #ifdef AGE_RESTRICTION_WITH_ECDSA
 /* @brief Helper function to generate a ECDSA private key
  *
@@ -150,7 +172,7 @@ TALER_age_restriction_commit (
   GNUNET_assert (mask->bits & 1); /* first bit must have been set */
 
   num_pub = __builtin_popcount (mask->bits) - 1;
-  num_priv = get_age_group (mask, age);
+  num_priv = TALER_get_age_group (mask, age);
 
   GNUNET_assert (31 > num_priv);
   GNUNET_assert (num_priv <= num_pub);
@@ -335,8 +357,8 @@ TALER_age_commitment_attest (
   GNUNET_assert (NULL != attest);
   GNUNET_assert (NULL != cp);
 
-  group = get_age_group (&cp->commitment.mask,
-                         age);
+  group = TALER_get_age_group (&cp->commitment.mask,
+                               age);
 
   GNUNET_assert (group < 32);
 
@@ -386,8 +408,8 @@ TALER_age_commitment_verify (
   GNUNET_assert (NULL != attest);
   GNUNET_assert (NULL != comm);
 
-  group = get_age_group (&comm->mask,
-                         age);
+  group = TALER_get_age_group (&comm->mask,
+                               age);
 
   GNUNET_assert (group < 32);
 
@@ -604,7 +626,7 @@ TALER_age_restriction_from_secret (
   GNUNET_assert (mask->bits & 1); /* fist bit must have been set */
 
   num_pub = __builtin_popcount (mask->bits) - 1;
-  num_priv = get_age_group (mask, max_age);
+  num_priv = TALER_get_age_group (mask, max_age);
 
   GNUNET_assert (31 > num_priv);
   GNUNET_assert (num_priv <= num_pub);
@@ -689,4 +711,57 @@ TALER_age_restriction_from_secret (
 }
 
 
+enum GNUNET_GenericReturnValue
+TALER_parse_coarse_date (
+  const char *in,
+  const struct TALER_AgeMask *mask,
+  uint32_t *out)
+{
+  struct tm date = {0};
+  struct tm limit = {0};
+  time_t seconds;
+
+  if (NULL == in)
+  {
+    /* FIXME[oec]: correct behaviour? */
+    *out = 0;
+    return GNUNET_OK;
+  }
+
+  GNUNET_assert (NULL !=mask);
+  GNUNET_assert (NULL !=out);
+
+  if (NULL == strptime (in, "%Y-%0m-%0d", &date))
+  {
+    if (NULL == strptime (in, "%Y-%0m-00", &date))
+      if (NULL == strptime (in, "%Y-00-00", &date))
+        return GNUNET_SYSERR;
+
+    /* turns out that the day is off by one in the last two cases */
+    date.tm_mday += 1;
+  }
+
+  seconds = mktime (&date);
+  if (-1 == seconds)
+    return GNUNET_SYSERR;
+
+  /* calculate the limit date for the largest age group */
+  localtime_r (&(time_t){time (NULL)}, &limit);
+  limit.tm_year -= TALER_adult_age (mask);
+  GNUNET_assert (-1 != mktime (&limit));
+
+  if ((limit.tm_year < date.tm_year)
+      || ((limit.tm_year == date.tm_year)
+          && (limit.tm_mon < date.tm_mon))
+      || ((limit.tm_year == date.tm_year)
+          && (limit.tm_mon == date.tm_mon)
+          && (limit.tm_mday < date.tm_mday)))
+    *out = seconds / 60 / 60 / 24;
+  else
+    *out = 0;
+
+  return GNUNET_OK;
+}
+
+
 /* end util/age_restriction.c */
diff --git a/src/util/exchange_signatures.c b/src/util/exchange_signatures.c
index 6f8ebdaf..3aa464aa 100644
--- a/src/util/exchange_signatures.c
+++ b/src/util/exchange_signatures.c
@@ -413,6 +413,34 @@ TALER_exchange_online_age_withdraw_confirmation_sign (
 }
 
 
+enum GNUNET_GenericReturnValue
+TALER_exchange_online_age_withdraw_confirmation_verify (
+  const struct TALER_AgeWithdrawCommitmentHashP *h_commitment,
+  uint32_t noreveal_index,
+  const struct TALER_ExchangePublicKeyP *exchange_pub,
+  const struct TALER_ExchangeSignatureP *exchange_sig)
+{
+  struct TALER_AgeWithdrawConfirmationPS confirm = {
+    .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_AGE_WITHDRAW),
+    .purpose.size = htonl (sizeof (confirm)),
+    .h_commitment = *h_commitment,
+    .noreveal_index = htonl (noreveal_index)
+  };
+
+  if (GNUNET_OK !=
+      GNUNET_CRYPTO_eddsa_verify (
+        TALER_SIGNATURE_EXCHANGE_CONFIRM_AGE_WITHDRAW,
+        &confirm,
+        &exchange_sig->eddsa_signature,
+        &exchange_pub->eddsa_pub))
+  {
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
+  return GNUNET_OK;
+}
+
+
 /* TODO:oec: add signature for age-withdraw, age-reveal */
 
 
diff --git a/src/util/test_age_restriction.c b/src/util/test_age_restriction.c
index 77717616..53cacc6d 100644
--- a/src/util/test_age_restriction.c
+++ b/src/util/test_age_restriction.c
@@ -21,11 +21,7 @@
  */
 #include "platform.h"
 #include "taler_util.h"
-
-extern uint8_t
-get_age_group (
-  const struct TALER_AgeMask *mask,
-  uint8_t age);
+#include <gnunet/gnunet_common.h>
 
 /**
  * Encodes the age mask into a string, like "8:10:12:14:16:18:21"
@@ -113,10 +109,10 @@ test_groups (void)
 
     for (uint8_t i = 0; i < 32; i++)
     {
-      uint8_t r = get_age_group (&mask, i);
+      uint8_t r = TALER_get_age_group (&mask, i);
       char *m = age_mask_to_string (&mask);
 
-      printf ("get_age_group(%s, %2d) = %d vs %d (exp)\n",
+      printf ("TALER_get_age_group(%s, %2d) = %d vs %d (exp)\n",
               m,
               i,
               r,
@@ -133,6 +129,153 @@ test_groups (void)
 }
 
 
+enum GNUNET_GenericReturnValue
+test_dates (void)
+{
+  struct TALER_AgeMask mask = {
+    .bits = 1 | 1 << 5 | 1 << 9 | 1 << 13 | 1 << 17 | 1 << 21
+  };
+
+  struct
+  {
+    char *date;
+    uint32_t expected;
+    enum GNUNET_GenericReturnValue ret;
+  }
+  test [] = {
+    {.date = "abcd-00-00", .expected = 0, .ret = GNUNET_SYSERR},
+    {.date = "1900-00-01", .expected = 0, .ret = GNUNET_SYSERR},
+    {.date = "19000001",   .expected = 0, .ret = GNUNET_SYSERR},
+    {.date = "2001-33-05", .expected = 0, .ret = GNUNET_SYSERR},
+    {.date = "2001-33-35", .expected = 0, .ret = GNUNET_SYSERR},
+
+    {.date = "1900-00-00", .expected = 0, .ret = GNUNET_OK},
+    {.date = "2001-00-00", .expected = 0, .ret = GNUNET_OK},
+    {.date = "2001-03-00", .expected = 0, .ret = GNUNET_OK},
+    {.date = "2001-03-05", .expected = 0, .ret = GNUNET_OK},
+
+    /* These dates should be far enough for the near future so that
+     * the expected values are correct. Will need adjustment in 2044 :) */
+    {.date = "2023-06-26", .expected = 19533, .ret = GNUNET_OK },
+    {.date = "2023-06-01", .expected = 19508, .ret = GNUNET_OK },
+    {.date = "2023-06-00", .expected = 19508, .ret = GNUNET_OK },
+    {.date = "2023-01-01", .expected = 19357, .ret = GNUNET_OK },
+    {.date = "2023-00-00", .expected = 19357, .ret = GNUNET_OK },
+  };
+
+  for (uint8_t t = 0; t < sizeof(test) / sizeof(test[0]); t++)
+  {
+    uint32_t d;
+    enum GNUNET_GenericReturnValue ret;
+
+    ret = TALER_parse_coarse_date (test[t].date,
+                                   &mask,
+                                   &d);
+    if (ret != test[t].ret)
+    {
+      printf (
+        "dates[%d] for date `%s` expected parser to return: %d, got: %d\n",
+        t, test[t].date, test[t].ret, ret);
+      return GNUNET_SYSERR;
+    }
+
+    if (ret == GNUNET_SYSERR)
+      continue;
+
+    if (d != test[t].expected)
+    {
+      printf (
+        "dates[%d] for date `%s` expected value %d, but got %d\n",
+        t, test[t].date, test[t].expected, d);
+      return GNUNET_SYSERR;
+    }
+
+    printf ("dates[%d] for date `%s` got expected value %d\n",
+            t, test[t].date, d);
+  }
+
+  printf ("done with dates\n");
+
+  return GNUNET_OK;
+}
+
+
+enum GNUNET_GenericReturnValue
+test_lowest (void)
+{
+  struct TALER_AgeMask mask = {
+    .bits = 1 | 1 << 5 | 1 << 9 | 1 << 13 | 1 << 17 | 1 << 21
+  };
+
+  struct { uint8_t age; uint8_t expected; }
+  test [] = {
+    {.age = 1, .expected = 0 },
+    {.age = 2, .expected = 0 },
+    {.age = 3, .expected = 0 },
+    {.age = 4, .expected = 0 },
+    {.age = 5, .expected = 5 },
+    {.age = 6, .expected = 5 },
+    {.age = 7, .expected = 5 },
+    {.age = 8, .expected = 5 },
+    {.age = 9, .expected = 9 },
+    {.age = 10, .expected = 9 },
+    {.age = 11, .expected = 9 },
+    {.age = 12, .expected = 9 },
+    {.age = 13, .expected = 13 },
+    {.age = 14, .expected = 13 },
+    {.age = 15, .expected = 13 },
+    {.age = 16, .expected = 13 },
+    {.age = 17, .expected = 17 },
+    {.age = 18, .expected = 17 },
+    {.age = 19, .expected = 17 },
+    {.age = 20, .expected = 17 },
+    {.age = 21, .expected = 21 },
+    {.age = 22, .expected = 21 },
+  };
+
+  for (uint8_t n = 0; n < sizeof(test) / sizeof(test[0]); n++)
+  {
+    uint8_t l = TALER_get_lowest_age (&mask, test[n].age);
+    printf ("lowest[%d] for age %d, expected lowest: %d, got: %d\n",
+            n, test[n].age, test[n].expected, l);
+    if (test[n].expected != l)
+      return GNUNET_SYSERR;
+  }
+
+  return GNUNET_OK;
+}
+
+
+enum GNUNET_GenericReturnValue
+test_adult (void)
+{
+  struct { struct TALER_AgeMask mask; uint8_t expected; }
+  test[] = {
+    { .mask = {.bits = 1 | 1 << 2},
+      .expected = 2 },
+    { .mask = {.bits = 1 | 1 << 2 | 1 << 3},
+      .expected = 3 },
+    { .mask = {.bits = 1 | 1 << 3},
+      .expected = 3 },
+    { .mask = {.bits = 1 | 1 << 22},
+      .expected = 22 },
+    { .mask = {.bits = 1 | 1 << 10 | 1 << 16 | 1 << 22},
+      .expected = 22 },
+  };
+  for (uint8_t n = 0; n < sizeof(test) / sizeof(test[0]); n++)
+  {
+    uint8_t l = TALER_adult_age (&test[n].mask);
+    printf ("adult[%d] for mask %s, expected: %d, got: %d\n",
+            n, TALER_age_mask_to_string (&test[n].mask), test[n].expected, l);
+    if (test[n].expected != l)
+      return GNUNET_SYSERR;
+  }
+  printf ("done with adult\n");
+
+  return GNUNET_OK;
+}
+
+
 static struct TALER_AgeMask age_mask = {
   .bits = 1 | 1 << 8 | 1 << 10 | 1 << 12 | 1 << 14 | 1 << 16 | 1 << 18 | 1 << 
21
 };
@@ -146,7 +289,7 @@ test_attestation (void)
     enum GNUNET_GenericReturnValue ret;
     struct TALER_AgeCommitmentProof acp[3] = {0};
     struct TALER_AgeAttestation at = {0};
-    uint8_t age_group = get_age_group (&age_mask, age);
+    uint8_t age_group = TALER_get_age_group (&age_mask, age);
     struct GNUNET_HashCode seed;
 
 
@@ -183,7 +326,7 @@ test_attestation (void)
     {
       for (uint8_t min = 0; min < 22; min++)
       {
-        uint8_t min_group = get_age_group (&age_mask, min);
+        uint8_t min_group = TALER_get_age_group (&age_mask, min);
 
         ret = TALER_age_commitment_attest (&acp[i],
                                            min,
@@ -259,11 +402,17 @@ main (int argc,
                     NULL);
   if (GNUNET_OK != test_groups ())
     return 1;
+  if (GNUNET_OK != test_lowest ())
+    return 2;
   if (GNUNET_OK != test_attestation ())
   {
     GNUNET_break (0);
-    return 2;
+    return 3;
   }
+  if (GNUNET_OK != test_dates ())
+    return 4;
+  if (GNUNET_OK != test_adult ())
+    return 5;
   return 0;
 }
 
diff --git a/src/util/wallet_signatures.c b/src/util/wallet_signatures.c
index 221865e7..823641ed 100644
--- a/src/util/wallet_signatures.c
+++ b/src/util/wallet_signatures.c
@@ -1,6 +1,6 @@
 /*
   This file is part of TALER
-  Copyright (C) 2021, 2022 Taler Systems SA
+  Copyright (C) 2021-2023 Taler Systems SA
 
   TALER is free software; you can redistribute it and/or modify it under the
   terms of the GNU General Public License as published by the Free Software
@@ -17,10 +17,12 @@
  * @file wallet_signatures.c
  * @brief Utility functions for Taler wallet signatures
  * @author Christian Grothoff
+ * @author Özgür Kesim
  */
 #include "platform.h"
 #include "taler_util.h"
 #include "taler_signatures.h"
+#include <gnunet/gnunet_common.h>
 
 
 GNUNET_NETWORK_STRUCT_BEGIN
@@ -620,9 +622,9 @@ struct TALER_AgeWithdrawRequestPS
   struct GNUNET_CRYPTO_EccSignaturePurpose purpose;
 
   /**
-   * Hash of the commitment of n*kappa coins
+   * The reserve's public key
    */
-  struct TALER_AgeWithdrawCommitmentHashP h_commitment GNUNET_PACKED;
+  struct TALER_ReservePublicKeyP reserve_pub;
 
   /**
    * Value of the coin being exchanged (matching the denomination key)
@@ -633,10 +635,20 @@ struct TALER_AgeWithdrawRequestPS
    */
   struct TALER_AmountNBO amount_with_fee;
 
+  /**
+   * Running SHA512 hash of the commitment of n*kappa coins
+   */
+  struct TALER_AgeWithdrawCommitmentHashP h_commitment;
+
+  /**
+   * The mask that defines the age groups.  MUST be the same for all 
denominations.
+   */
+  struct TALER_AgeMask mask;
+
   /**
    * Maximum age group that the coins are going to be restricted to.
    */
-  uint32_t max_age_group;
+  uint8_t max_age_group;
 };
 
 
@@ -646,7 +658,8 @@ void
 TALER_wallet_age_withdraw_sign (
   const struct TALER_AgeWithdrawCommitmentHashP *h_commitment,
   const struct TALER_Amount *amount_with_fee,
-  uint32_t max_age_group,
+  const struct TALER_AgeMask *mask,
+  uint8_t max_age,
   const struct TALER_ReservePrivateKeyP *reserve_priv,
   struct TALER_ReserveSignatureP *reserve_sig)
 {
@@ -654,9 +667,12 @@ TALER_wallet_age_withdraw_sign (
     .purpose.size = htonl (sizeof (req)),
     .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_RESERVE_AGE_WITHDRAW),
     .h_commitment = *h_commitment,
-    .max_age_group = max_age_group
+    .mask = *mask,
+    .max_age_group = TALER_get_age_group (mask, max_age)
   };
 
+  GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv,
+                                      &req.reserve_pub.eddsa_pub);
   TALER_amount_hton (&req.amount_with_fee,
                      amount_with_fee);
   GNUNET_CRYPTO_eddsa_sign (&reserve_priv->eddsa_priv,
@@ -669,15 +685,18 @@ enum GNUNET_GenericReturnValue
 TALER_wallet_age_withdraw_verify (
   const struct TALER_AgeWithdrawCommitmentHashP *h_commitment,
   const struct TALER_Amount *amount_with_fee,
-  uint32_t max_age_group,
+  const struct TALER_AgeMask *mask,
+  uint8_t max_age,
   const struct TALER_ReservePublicKeyP *reserve_pub,
   const struct TALER_ReserveSignatureP *reserve_sig)
 {
   struct TALER_AgeWithdrawRequestPS awsrd = {
     .purpose.size = htonl (sizeof (awsrd)),
     .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_RESERVE_AGE_WITHDRAW),
+    .reserve_pub = *reserve_pub,
     .h_commitment = *h_commitment,
-    .max_age_group = max_age_group
+    .mask = *mask,
+    .max_age_group = TALER_get_age_group (mask, max_age)
   };
 
   TALER_amount_hton (&awsrd.amount_with_fee,

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]