gnunet-svn
[Top][All Lists]
Advanced

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

[taler-exchange] branch master updated: skeleton for batch withdraw logi


From: gnunet
Subject: [taler-exchange] branch master updated: skeleton for batch withdraw logic (not finished)
Date: Sun, 01 May 2022 12:45:23 +0200

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

grothoff pushed a commit to branch master
in repository exchange.

The following commit(s) were added to refs/heads/master by this push:
     new 7718cd41 skeleton for batch withdraw logic (not finished)
7718cd41 is described below

commit 7718cd4153f3321f5f324a485d21a3b7fdb992d4
Author: Christian Grothoff <christian@grothoff.org>
AuthorDate: Sun May 1 12:45:12 2022 +0200

    skeleton for batch withdraw logic (not finished)
---
 ...raw.c => taler-exchange-httpd_batch-withdraw.c} | 603 ++++++++++++++-------
 ...raw.h => taler-exchange-httpd_batch-withdraw.h} |  27 +-
 src/exchange/taler-exchange-httpd_withdraw.c       |   4 +
 src/exchange/taler-exchange-httpd_withdraw.h       |   2 +-
 src/exchangedb/exchange-0001-part.sql              | 239 ++++++++
 src/exchangedb/plugin_exchangedb_postgres.c        | 143 +++++
 src/include/taler_exchangedb_plugin.h              |  53 ++
 7 files changed, 856 insertions(+), 215 deletions(-)

diff --git a/src/exchange/taler-exchange-httpd_withdraw.c 
b/src/exchange/taler-exchange-httpd_batch-withdraw.c
similarity index 50%
copy from src/exchange/taler-exchange-httpd_withdraw.c
copy to src/exchange/taler-exchange-httpd_batch-withdraw.c
index d5ecd338..a6302aad 100644
--- a/src/exchange/taler-exchange-httpd_withdraw.c
+++ b/src/exchange/taler-exchange-httpd_batch-withdraw.c
@@ -1,6 +1,6 @@
 /*
   This file is part of TALER
-  Copyright (C) 2014-2021 Taler Systems SA
+  Copyright (C) 2014-2022 Taler Systems SA
 
   TALER is free software; you can redistribute it and/or modify
   it under the terms of the GNU Affero General Public License as
@@ -17,8 +17,8 @@
   see <http://www.gnu.org/licenses/>
 */
 /**
- * @file taler-exchange-httpd_withdraw.c
- * @brief Handle /reserves/$RESERVE_PUB/withdraw requests
+ * @file taler-exchange-httpd_batch-withdraw.c
+ * @brief Handle /reserves/$RESERVE_PUB/batch-withdraw requests
  * @author Florian Dold
  * @author Benedikt Mueller
  * @author Christian Grothoff
@@ -28,11 +28,77 @@
 #include <jansson.h>
 #include "taler_json_lib.h"
 #include "taler_mhd_lib.h"
-#include "taler-exchange-httpd_withdraw.h"
+#include "taler-exchange-httpd_batch-withdraw.h"
 #include "taler-exchange-httpd_responses.h"
 #include "taler-exchange-httpd_keys.h"
 
 
+/**
+ * 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;
+
+};
+
+/**
+ * Context for #batch_withdraw_transaction.
+ */
+struct BatchWithdrawContext
+{
+
+  /**
+   * Public key of the reserv.
+   */
+  const struct TALER_ReservePublicKeyP *reserve_pub;
+
+  /**
+   * KYC status of the reserve used for the operation.
+   */
+  struct TALER_EXCHANGEDB_KycStatus kyc;
+
+  /**
+   * Array of @e planchets_length planchets we are processing.
+   */
+  struct PlanchetContext *planchets;
+
+  /**
+   * Total amount from all coins with fees.
+   */
+  struct TALER_Amount batch_total;
+
+  /**
+   * Length of the @e planchets array.
+   */
+  unsigned int planchets_length;
+
+};
+
+
 /**
  * Send reserve history information to client with the
  * message that we have insufficient funds for the
@@ -72,44 +138,6 @@ reply_withdraw_insufficient_funds (
 }
 
 
-/**
- * Context for #withdraw_transaction.
- */
-struct WithdrawContext
-{
-
-  /**
-   * 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;
-
-  /**
-   * KYC status for the operation.
-   */
-  struct TALER_EXCHANGEDB_KycStatus kyc;
-
-};
-
-
 /**
  * Function implementing withdraw transaction.  Runs the
  * transaction logic; IF it returns a non-error code, the transaction
@@ -121,47 +149,40 @@ struct WithdrawContext
  * Note that "wc->collectable.sig" is set before entering this function as we
  * signed before entering the transaction.
  *
- * @param cls a `struct WithdrawContext *`
+ * @param cls a `struct BatchWithdrawContext *`
  * @param connection MHD request which triggered the transaction
  * @param[out] mhd_ret set to MHD response status for @a connection,
  *             if transaction failed (!)
  * @return transaction status
  */
 static enum GNUNET_DB_QueryStatus
-withdraw_transaction (void *cls,
-                      struct MHD_Connection *connection,
-                      MHD_RESULT *mhd_ret)
+batch_withdraw_transaction (void *cls,
+                            struct MHD_Connection *connection,
+                            MHD_RESULT *mhd_ret)
 {
-  struct WithdrawContext *wc = cls;
-  enum GNUNET_DB_QueryStatus qs;
-  bool found = false;
-  bool balance_ok = false;
+  struct BatchWithdrawContext *wc = cls;
   struct GNUNET_TIME_Timestamp now;
   uint64_t ruuid;
-  const struct TALER_CsNonce *nonce;
-  const struct TALER_BlindedPlanchet *bp;
+  enum GNUNET_DB_QueryStatus qs;
+  bool balance_ok = false;
+  bool found = false;
 
   now = GNUNET_TIME_timestamp_get ();
-  bp = &wc->blinded_planchet;
-  nonce =
-    (TALER_DENOMINATION_CS == bp->cipher)
-    ? &bp->details.cs_blinded_planchet.nonce
-    : NULL;
-  qs = TEH_plugin->do_withdraw (TEH_plugin->cls,
-                                nonce,
-                                &wc->collectable,
-                                now,
-                                &found,
-                                &balance_ok,
-                                &wc->kyc,
-                                &ruuid);
+  qs = TEH_plugin->do_batch_withdraw (TEH_plugin->cls,
+                                      now,
+                                      wc->reserve_pub,
+                                      &wc->batch_total,
+                                      &balance_ok,
+                                      &found,
+                                      &wc->kyc,
+                                      &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_withdraw");
+                                             "update_reserve_batch_withdraw");
     return qs;
   }
   if (! found)
@@ -174,6 +195,8 @@ withdraw_transaction (void *cls,
   }
   if (! balance_ok)
   {
+    /* FIXME: logic shared with normal withdraw
+       => refactor and move to new TEH_responses function! */
     struct TALER_EXCHANGEDB_ReserveHistory *rh;
     struct TALER_Amount balance;
 
@@ -209,7 +232,7 @@ withdraw_transaction (void *cls,
     *mhd_ret = reply_withdraw_insufficient_funds (
       connection,
       &balance,
-      &wc->collectable.amount_with_fee,
+      &wc->batch_total,
       rh);
     TEH_plugin->free_reserve_history (TEH_plugin->cls,
                                       rh);
@@ -264,7 +287,69 @@ withdraw_transaction (void *cls,
       return GNUNET_DB_STATUS_HARD_ERROR;
     }
   }
-  return qs;
+
+  /* Add information about each planchet in the batch */
+  for (unsigned int i = 0; i<wc->planchets_length; i++)
+  {
+    struct PlanchetContext *pc = &wc->planchets[i];
+    const struct TALER_BlindedPlanchet *bp = &pc->blinded_planchet;
+    const struct TALER_CsNonce *nonce;
+    bool denom_unknown = true;
+    bool conflict = true;
+    bool nonce_reuse = true;
+
+    nonce = (TALER_DENOMINATION_CS == bp->cipher)
+      ? &bp->details.cs_blinded_planchet.nonce
+      : NULL;
+    qs = TEH_plugin->do_batch_withdraw_insert (TEH_plugin->cls,
+                                               nonce,
+                                               &wc->collectable,
+                                               now,
+                                               ruuid,
+                                               &denom_unknown,
+                                               &conflict,
+                                               &nonce_reuse);
+    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_withdraw");
+      return qs;
+    }
+    if (denom_unknown)
+    {
+      GNUNET_break (0);
+      *mhd_ret = TALER_MHD_reply_with_error (connection,
+                                             MHD_HTTP_INTERNAL_SERVER_ERROR,
+                                             
TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
+                                             NULL);
+      return GNUNET_DB_STATUS_HARD_ERROR;
+    }
+    if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) ||
+         (conflict) )
+    {
+      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+                  "Idempotent coin in batch, not allowed. Aborting.\n");
+      *mhd_ret = TALER_MHD_reply_with_error (connection,
+                                             MHD_HTTP_CONFLICT,
+                                             
TALER_EC_EXCHANGE_BATCH_IDEMPOTENT_PLANCHET,
+                                             NULL);
+      return GNUNET_DB_STATUS_HARD_ERROR;
+    }
+    if (nonce_reuse)
+    {
+      GNUNET_break_op (0);
+      *mhd_ret = TALER_MHD_reply_with_error (connection,
+                                             MHD_HTTP_BAD_REQUEST,
+                                             
TALER_EC_EXCHANGE_WITHDRAW_NONCE_REUSE,
+                                             NULL);
+      return GNUNET_DB_STATUS_HARD_ERROR;
+    }
+  }
+
+  return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
 }
 
 
@@ -281,9 +366,12 @@ withdraw_transaction (void *cls,
  */
 static bool
 check_request_idempotent (struct TEH_RequestContext *rc,
-                          struct WithdrawContext *wc,
+                          struct BatchWithdrawContext *wc,
                           MHD_RESULT *mret)
 {
+  /* FIXME: Not yet supported. Do we want to, or simply
+     generate an error in this case? */
+#if FIXME
   enum GNUNET_DB_QueryStatus qs;
 
   qs = TEH_plugin->get_withdraw_info (TEH_plugin->cls,
@@ -309,104 +397,185 @@ check_request_idempotent (struct TEH_RequestContext *rc,
                                        &wc->collectable.sig));
   TALER_blinded_denom_sig_free (&wc->collectable.sig);
   return true;
+#else
+  return false;
+#endif
 }
 
 
-MHD_RESULT
-TEH_handler_withdraw (struct TEH_RequestContext *rc,
-                      const struct TALER_ReservePublicKeyP *reserve_pub,
-                      const json_t *root)
+/**
+ * The request was parsed successfully. Prepare
+ * our side for the main DB transaction.
+ *
+ * @param rc request details
+ * @param wc storage for request processing
+ * @return MHD result for the @a rc
+ */
+static MHD_RESULT
+prepare_transaction (struct TEH_RequestContext *rc,
+                     struct BatchWithdrawContext *wc)
 {
-  struct WithdrawContext wc;
-  struct GNUNET_JSON_Specification spec[] = {
-    GNUNET_JSON_spec_fixed_auto ("reserve_sig",
-                                 &wc.collectable.reserve_sig),
-    GNUNET_JSON_spec_fixed_auto ("denom_pub_hash",
-                                 &wc.collectable.denom_pub_hash),
-    TALER_JSON_spec_blinded_planchet ("coin_ev",
-                                      &wc.blinded_planchet),
-    GNUNET_JSON_spec_end ()
-  };
-  enum TALER_ErrorCode ec;
-  struct TEH_DenominationKey *dk;
+  /* Note: We could check the reserve balance here,
+     just to be reasonably sure that the reserve has
+     a sufficient balance before doing the "expensive"
+     signatures... */
+  /* Sign before transaction! */
+  for (unsigned int i = 0; i<wc->planchets_length; i++)
+  {
+    struct PlanchetContext *pc = &wc->planchets[i];
+    enum TALER_ErrorCode ec;
+
+    ec = TEH_keys_denomination_sign_withdraw (
+      &pc->collectable.denom_pub_hash,
+      &pc->blinded_planchet,
+      &pc->collectable.sig);
+    if (TALER_EC_NONE != ec)
+    {
+      GNUNET_break (0);
+      return TALER_MHD_reply_with_ec (rc->connection,
+                                      ec,
+                                      NULL);
+    }
+  }
 
-  memset (&wc,
-          0,
-          sizeof (wc));
-  wc.collectable.reserve_pub = *reserve_pub;
+  /* run transaction */
+  {
+    MHD_RESULT mhd_ret;
 
+    if (GNUNET_OK !=
+        TEH_DB_run_transaction (rc->connection,
+                                "run batch withdraw",
+                                TEH_MT_REQUEST_WITHDRAW,
+                                &mhd_ret,
+                                &batch_withdraw_transaction,
+                                wc))
+    {
+      return mhd_ret;
+    }
+  }
+  /* return final positive response */
   {
-    enum GNUNET_GenericReturnValue res;
+    json_t *sigs;
 
-    res = TALER_MHD_parse_json_data (rc->connection,
-                                     root,
-                                     spec);
-    if (GNUNET_OK != res)
-      return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES;
+    sigs = json_array ();
+    GNUNET_assert (NULL != sigs);
+    for (unsigned int i = 0; i<wc->planchets_length; i++)
+    {
+      struct PlanchetContext *pc = &wc->planchets[i];
+
+      GNUNET_assert (
+        0 ==
+        json_array_append_new (
+          sigs,
+          GNUNET_JSON_PACK (
+            TALER_JSON_pack_blinded_denom_sig (
+              "ev_sig",
+              &pc->collectable.sig))));
+    }
+    return TALER_MHD_REPLY_JSON_PACK (
+      rc->connection,
+      MHD_HTTP_OK,
+      GNUNET_JSON_pack_array_steal ("ev_sigs",
+                                    sigs));
+  }
+}
+
+
+/**
+ * Continue processing the request @a rc by parsing the
+ * @a planchets and then running the transaction.
+ *
+ * @param rc request details
+ * @param wc storage for request processing
+ * @param planchets array of planchets to parse
+ * @return MHD result for the @a rc
+ */
+static MHD_RESULT
+parse_planchets (struct TEH_RequestContext *rc,
+                 struct BatchWithdrawContext *wc,
+                 const json_t *planchets)
+{
+  struct TEH_KeyStateHandle *ksh;
+  MHD_RESULT mret;
+
+  ksh = TEH_keys_get_state ();
+  if (NULL == ksh)
+  {
+    if (! check_request_idempotent (rc,
+                                    wc,
+                                    &mret))
+    {
+      return TALER_MHD_reply_with_error (rc->connection,
+                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
+                                         
TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
+                                         NULL);
+    }
+    return mret;
   }
+  for (unsigned int i = 0; i<wc->planchets_length; i++)
   {
-    MHD_RESULT mret;
-    struct TEH_KeyStateHandle *ksh;
+    struct PlanchetContext *pc = &wc->planchets[i];
+    struct GNUNET_JSON_Specification ispec[] = {
+      GNUNET_JSON_spec_fixed_auto ("reserve_sig",
+                                   &pc->collectable.reserve_sig),
+      GNUNET_JSON_spec_fixed_auto ("denom_pub_hash",
+                                   &pc->collectable.denom_pub_hash),
+      TALER_JSON_spec_blinded_planchet ("coin_ev",
+                                        &pc->blinded_planchet),
+      GNUNET_JSON_spec_end ()
+    };
+    struct TEH_DenominationKey *dk;
 
-    ksh = TEH_keys_get_state ();
-    if (NULL == ksh)
     {
-      if (! check_request_idempotent (rc,
-                                      &wc,
-                                      &mret))
-      {
-        GNUNET_JSON_parse_free (spec);
-        return TALER_MHD_reply_with_error (rc->connection,
-                                           MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                           
TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
-                                           NULL);
-      }
-      GNUNET_JSON_parse_free (spec);
-      return mret;
+      enum GNUNET_GenericReturnValue res;
+
+      res = TALER_MHD_parse_json_data (rc->connection,
+                                       json_array_get (planchets,
+                                                       i),
+                                       ispec);
+      if (GNUNET_OK != res)
+        return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES;
     }
+    pc->collectable.reserve_pub = *wc->reserve_pub;
     dk = TEH_keys_denomination_by_hash2 (ksh,
-                                         &wc.collectable.denom_pub_hash,
+                                         &pc->collectable.denom_pub_hash,
                                          NULL,
                                          NULL);
     if (NULL == dk)
     {
       if (! check_request_idempotent (rc,
-                                      &wc,
+                                      wc,
                                       &mret))
       {
-        GNUNET_JSON_parse_free (spec);
         return TEH_RESPONSE_reply_unknown_denom_pub_hash (
           rc->connection,
-          &wc.collectable.denom_pub_hash);
+          &pc->collectable.denom_pub_hash);
       }
-      GNUNET_JSON_parse_free (spec);
       return mret;
     }
     if (GNUNET_TIME_absolute_is_past (dk->meta.expire_withdraw.abs_time))
     {
       /* This denomination is past the expiration time for withdraws */
       if (! check_request_idempotent (rc,
-                                      &wc,
+                                      wc,
                                       &mret))
       {
         GNUNET_JSON_parse_free (spec);
         return TEH_RESPONSE_reply_expired_denom_pub_hash (
           rc->connection,
-          &wc.collectable.denom_pub_hash,
+          &wc->collectable.denom_pub_hash,
           TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED,
           "WITHDRAW");
       }
-      GNUNET_JSON_parse_free (spec);
       return mret;
     }
     if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time))
     {
       /* This denomination is not yet valid, no need to check
          for idempotency! */
-      GNUNET_JSON_parse_free (spec);
       return TEH_RESPONSE_reply_expired_denom_pub_hash (
         rc->connection,
-        &wc.collectable.denom_pub_hash,
+        &wc->collectable.denom_pub_hash,
         TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE,
         "WITHDRAW");
     }
@@ -414,120 +583,152 @@ TEH_handler_withdraw (struct TEH_RequestContext *rc,
     {
       /* This denomination has been revoked */
       if (! check_request_idempotent (rc,
-                                      &wc,
+                                      wc,
                                       &mret))
       {
-        GNUNET_JSON_parse_free (spec);
         return TEH_RESPONSE_reply_expired_denom_pub_hash (
           rc->connection,
-          &wc.collectable.denom_pub_hash,
+          &wc->collectable.denom_pub_hash,
           TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED,
           "WITHDRAW");
       }
-      GNUNET_JSON_parse_free (spec);
       return mret;
     }
-    if (dk->denom_pub.cipher != wc.blinded_planchet.cipher)
+    if (dk->denom_pub.cipher != wc->blinded_planchet.cipher)
     {
       /* denomination cipher and blinded planchet cipher not the same */
-      GNUNET_JSON_parse_free (spec);
       return TALER_MHD_reply_with_error (rc->connection,
                                          MHD_HTTP_BAD_REQUEST,
                                          
TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH,
                                          NULL);
     }
-  }
+    if (0 >
+        TALER_amount_add (&pc->collectable.amount_with_fee,
+                          &dk->meta.value,
+                          &dk->meta.fees.withdraw))
+    {
+      return TALER_MHD_reply_with_error (rc->connection,
+                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
+                                         
TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW,
+                                         NULL);
+    }
+    if (0 >
+        TALER_amount_add (&wc->batch_total,
+                          &wc->batch_total,
+                          pc->collectable.amount_with_fee))
+    {
+      return TALER_MHD_reply_with_error (rc->connection,
+                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
+                                         
TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW,
+                                         NULL);
+    }
 
-  if (0 >
-      TALER_amount_add (&wc.collectable.amount_with_fee,
-                        &dk->meta.value,
-                        &dk->meta.fees.withdraw))
-  {
-    GNUNET_JSON_parse_free (spec);
-    return TALER_MHD_reply_with_error (rc->connection,
-                                       MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                       
TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW,
-                                       NULL);
+    if (GNUNET_OK !=
+        TALER_coin_ev_hash (&pc->blinded_planchet,
+                            &pc->collectable.denom_pub_hash,
+                            &pc->collectable.h_coin_envelope))
+    {
+      GNUNET_break (0);
+      GNUNET_JSON_parse_free (spec);
+      return TALER_MHD_reply_with_error (rc->connection,
+                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
+                                         
TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
+                                         NULL);
+    }
+    TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++;
+    if (GNUNET_OK !=
+        TALER_wallet_withdraw_verify (&pc->collectable.denom_pub_hash,
+                                      &pc->collectable.amount_with_fee,
+                                      &pc->collectable.h_coin_envelope,
+                                      &pc->collectable.reserve_pub,
+                                      &pc->collectable.reserve_sig))
+    {
+      GNUNET_break_op (0);
+      GNUNET_JSON_parse_free (spec);
+      return TALER_MHD_reply_with_error (rc->connection,
+                                         MHD_HTTP_FORBIDDEN,
+                                         
TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID,
+                                         NULL);
+    }
   }
+  /* everything parsed */
+  return prepare_transaction (rc,
+                              wc);
+}
+
+
+MHD_RESULT
+TEH_handler_batch_withdraw (struct TEH_RequestContext *rc,
+                            const struct TALER_ReservePublicKeyP *reserve_pub,
+                            const json_t *root)
+{
+  struct BatchWithdrawContext wc;
+  json_t *planchets;
+  struct GNUNET_JSON_Specification spec[] = {
+    GNUNET_JSON_spec_json ("planchets",
+                           &planchets),
+    GNUNET_JSON_spec_end ()
+  };
+
+  memset (&wc,
+          0,
+          sizeof (wc));
+  TALER_amount_set_zero (TEH_currency,
+                         &wc.batch_total);
+  wc.reserve_pub = reserve_pub;
 
-  if (GNUNET_OK !=
-      TALER_coin_ev_hash (&wc.blinded_planchet,
-                          &wc.collectable.denom_pub_hash,
-                          &wc.collectable.h_coin_envelope))
   {
-    GNUNET_break (0);
-    GNUNET_JSON_parse_free (spec);
-    return TALER_MHD_reply_with_error (rc->connection,
-                                       MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                       
TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
-                                       NULL);
-  }
+    enum GNUNET_GenericReturnValue res;
 
-  TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++;
-  if (GNUNET_OK !=
-      TALER_wallet_withdraw_verify (&wc.collectable.denom_pub_hash,
-                                    &wc.collectable.amount_with_fee,
-                                    &wc.collectable.h_coin_envelope,
-                                    &wc.collectable.reserve_pub,
-                                    &wc.collectable.reserve_sig))
+    res = TALER_MHD_parse_json_data (rc->connection,
+                                     root,
+                                     spec);
+    if (GNUNET_OK != res)
+      return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES;
+  }
+  if ( (! json_is_array (planchets)) ||
+       (0 == json_array_size (planchets)) )
   {
-    GNUNET_break_op (0);
     GNUNET_JSON_parse_free (spec);
+    GNUNET_break_op (0);
     return TALER_MHD_reply_with_error (rc->connection,
-                                       MHD_HTTP_FORBIDDEN,
-                                       
TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID,
-                                       NULL);
+                                       MHD_HTTP_BAD_REQUEST,
+                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
+                                       "planchets");
   }
-
-  /* Sign before transaction! */
-  ec = TEH_keys_denomination_sign_withdraw (
-    &wc.collectable.denom_pub_hash,
-    &wc.blinded_planchet,
-    &wc.collectable.sig);
-  if (TALER_EC_NONE != ec)
+  wc.planchets_length = json_array_size (planchets);
+  if (wc.planchets_length > TALER_MAX_FRESH_COINS)
   {
-    GNUNET_break (0);
     GNUNET_JSON_parse_free (spec);
-    return TALER_MHD_reply_with_ec (rc->connection,
-                                    ec,
-                                    NULL);
+    GNUNET_break_op (0);
+    return TALER_MHD_reply_with_error (rc->connection,
+                                       MHD_HTTP_BAD_REQUEST,
+                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
+                                       "too many planchets");
   }
-
-  /* run transaction */
   {
-    MHD_RESULT mhd_ret;
+    struct PlanchetContext splanchets[wc.planchets_length];
+    MHD_RESULT ret;
 
-    if (GNUNET_OK !=
-        TEH_DB_run_transaction (rc->connection,
-                                "run withdraw",
-                                TEH_MT_REQUEST_WITHDRAW,
-                                &mhd_ret,
-                                &withdraw_transaction,
-                                &wc))
+    memset (splanchets,
+            0,
+            sizeof (splanchets));
+    wc.planchets = splanchets;
+    ret = parse_planchets (rc,
+                           &wc,
+                           planchets);
+    /* Clean up */
+    for (unsigned int i = 0; i<wc.planchets_length; i++)
     {
-      /* Even if #withdraw_transaction() failed, it may have created a 
signature
-         (or we might have done it optimistically above). */
-      TALER_blinded_denom_sig_free (&wc.collectable.sig);
-      GNUNET_JSON_parse_free (spec);
-      return mhd_ret;
-    }
-  }
+      struct PlanchetContext *pc = &wc->planchets[i];
 
-  /* Clean up and send back final response */
-  GNUNET_JSON_parse_free (spec);
-
-  {
-    MHD_RESULT ret;
-
-    ret = TALER_MHD_REPLY_JSON_PACK (
-      rc->connection,
-      MHD_HTTP_OK,
-      TALER_JSON_pack_blinded_denom_sig ("ev_sig",
-                                         &wc.collectable.sig));
-    TALER_blinded_denom_sig_free (&wc.collectable.sig);
+      // FIXME: Free more of memory in pc!
+      TALER_blinded_denom_sig_free (&pc->collectable.sig);
+    }
+    GNUNET_JSON_parse_free (spec);
     return ret;
   }
 }
 
 
-/* end of taler-exchange-httpd_withdraw.c */
+/* end of taler-exchange-httpd_batch-withdraw.c */
diff --git a/src/exchange/taler-exchange-httpd_withdraw.h 
b/src/exchange/taler-exchange-httpd_batch-withdraw.h
similarity index 54%
copy from src/exchange/taler-exchange-httpd_withdraw.h
copy to src/exchange/taler-exchange-httpd_batch-withdraw.h
index b754e64f..dfc6e5ad 100644
--- a/src/exchange/taler-exchange-httpd_withdraw.h
+++ b/src/exchange/taler-exchange-httpd_batch-withdraw.h
@@ -1,6 +1,6 @@
 /*
   This file is part of TALER
-  Copyright (C) 2014-2021 Taler Systems SA
+  Copyright (C) 2014-2022 Taler Systems SA
 
   TALER is free software; you can redistribute it and/or modify it under the
   terms of the GNU Affero General Public License as published by the Free 
Software
@@ -14,25 +14,26 @@
   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */
 /**
- * @file taler-exchange-httpd_withdraw.h
- * @brief Handle /reserve/withdraw requests
+ * @file taler-exchange-httpd_batch-withdraw.h
+ * @brief Handle /reserve/batch-withdraw requests
  * @author Florian Dold
  * @author Benedikt Mueller
  * @author Christian Grothoff
  */
-#ifndef TALER_EXCHANGE_HTTPD_WITHDRAW_H
-#define TALER_EXCHANGE_HTTPD_WITHDRAW_H
+#ifndef TALER_EXCHANGE_HTTPD_BATCH_WITHDRAW_H
+#define TALER_EXCHANGE_HTTPD_BATCH_WITHDRAW_H
 
 #include <microhttpd.h>
 #include "taler-exchange-httpd.h"
 
 
 /**
- * Handle a "/reserves/$RESERVE_PUB/withdraw" request.  Parses the the 
requested "denom_pub" which
- * specifies the key/value of the coin to be withdrawn, and checks that the
- * signature "reserve_sig" makes this a valid withdrawal request from the
- * specified reserve.  If so, the envelope with the blinded coin "coin_ev" is
- * passed down to execute the withdrawal operation.
+ * Handle a "/reserves/$RESERVE_PUB/batch-withdraw" request.  Parses the batch 
of
+ * requested "denom_pub" which specifies the key/value of the coin to be
+ * withdrawn, and checks that the signature "reserve_sig" makes this a valid
+ * withdrawal request from the specified reserve.  If so, the envelope with
+ * the blinded coin "coin_ev" is passed down to execute the withdrawal
+ * operation.
  *
  * @param rc request context
  * @param root uploaded JSON data
@@ -40,8 +41,8 @@
  * @return MHD result code
   */
 MHD_RESULT
-TEH_handler_withdraw (struct TEH_RequestContext *rc,
-                      const struct TALER_ReservePublicKeyP *reserve_pub,
-                      const json_t *root);
+TEH_handler_batch_withdraw (struct TEH_RequestContext *rc,
+                            const struct TALER_ReservePublicKeyP *reserve_pub,
+                            const json_t *root);
 
 #endif
diff --git a/src/exchange/taler-exchange-httpd_withdraw.c 
b/src/exchange/taler-exchange-httpd_withdraw.c
index d5ecd338..0c68b6d4 100644
--- a/src/exchange/taler-exchange-httpd_withdraw.c
+++ b/src/exchange/taler-exchange-httpd_withdraw.c
@@ -147,6 +147,10 @@ withdraw_transaction (void *cls,
     (TALER_DENOMINATION_CS == bp->cipher)
     ? &bp->details.cs_blinded_planchet.nonce
     : NULL;
+  // FIXME: what error is returned on nonce reuse?
+  // Should expand function to return this error
+  // specifically, and then we should return a
+  // TALER_EC_EXCHANGE_WITHDRAW_NONCE_REUSE,
   qs = TEH_plugin->do_withdraw (TEH_plugin->cls,
                                 nonce,
                                 &wc->collectable,
diff --git a/src/exchange/taler-exchange-httpd_withdraw.h 
b/src/exchange/taler-exchange-httpd_withdraw.h
index b754e64f..2ec76bb9 100644
--- a/src/exchange/taler-exchange-httpd_withdraw.h
+++ b/src/exchange/taler-exchange-httpd_withdraw.h
@@ -28,7 +28,7 @@
 
 
 /**
- * Handle a "/reserves/$RESERVE_PUB/withdraw" request.  Parses the the 
requested "denom_pub" which
+ * Handle a "/reserves/$RESERVE_PUB/withdraw" request.  Parses the requested 
"denom_pub" which
  * specifies the key/value of the coin to be withdrawn, and checks that the
  * signature "reserve_sig" makes this a valid withdrawal request from the
  * specified reserve.  If so, the envelope with the blinded coin "coin_ev" is
diff --git a/src/exchangedb/exchange-0001-part.sql 
b/src/exchangedb/exchange-0001-part.sql
index 9ca66cd4..56f1df29 100644
--- a/src/exchangedb/exchange-0001-part.sql
+++ b/src/exchangedb/exchange-0001-part.sql
@@ -1509,6 +1509,245 @@ COMMENT ON FUNCTION exchange_do_withdraw(BYTEA, INT8, 
INT4, BYTEA, BYTEA, BYTEA,
 
 
 
+
+CREATE OR REPLACE FUNCTION exchange_do_batch_withdraw(
+  IN amount_val INT8,
+  IN amount_frac INT4,
+  IN rpub BYTEA,
+  IN now INT8,
+  IN min_reserve_gc INT8,
+  OUT reserve_found BOOLEAN,
+  OUT balance_ok BOOLEAN,
+  OUT kycok BOOLEAN,
+  OUT account_uuid INT8,
+  OUT ruuid INT8)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+  reserve_gc INT8;
+DECLARE
+  reserve_val INT8;
+DECLARE
+  reserve_frac INT4;
+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
+  ,reserve_uuid
+ INTO
+   reserve_val
+  ,reserve_frac
+  ,reserve_gc
+  ,ruuid
+  FROM reserves
+ WHERE reserves.reserve_pub=rpub;
+
+IF NOT FOUND
+THEN
+  -- reserve unknown
+  reserve_found=FALSE;
+  balance_ok=FALSE;
+  kycok=FALSE;
+  account_uuid=0;
+  ruuid=2;
+  RETURN;
+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
+    reserve_found=TRUE;
+    balance_ok=FALSE;
+    kycok=FALSE; -- we do not really know or care
+    account_uuid=0;
+    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;
+
+
+-- Obtain KYC status based on the last wire transfer into
+-- this reserve. FIXME: likely not adequate for reserves that got P2P 
transfers!
+-- SELECT
+--    kyc_ok
+--   ,wire_target_serial_id
+--   INTO
+--    kycok
+--   ,account_uuid
+--   FROM reserves_in
+--   JOIN wire_targets ON (wire_source_h_payto = wire_target_h_payto)
+--  WHERE reserve_pub=rpub
+--  LIMIT 1; -- limit 1 should not be required (without p2p transfers)
+
+WITH reserves_in AS materialized (
+  SELECT wire_source_h_payto
+  FROM reserves_in WHERE
+  reserve_pub=rpub
+)
+SELECT
+  kyc_ok
+  ,wire_target_serial_id
+INTO
+  kycok
+  ,account_uuid
+FROM wire_targets
+  WHERE wire_target_h_payto = (
+    SELECT wire_source_h_payto
+      FROM reserves_in
+  );
+
+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.';
+
+
+
+
+
+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 ruuid INT8,
+  IN reserve_sig BYTEA,
+  IN h_coin_envelope BYTEA,
+  IN denom_sig BYTEA,
+  IN now INT8,
+  OUT out_denom_unknown BOOLEAN,
+  OUT out_nonce_reuse BOOLEAN,
+  OUT out_conflict BOOLEAN)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+  denom_serial INT8;
+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
+
+out_denom_unknown=TRUE;
+out_conflict=TRUE;
+out_nonce_reuse=TRUE;
+
+SELECT denominations_serial
+  INTO denom_serial
+  FROM denominations
+ WHERE denom_pub_hash=h_denom_pub;
+
+IF NOT FOUND
+THEN
+  -- denomination unknown, should be impossible!
+  out_denom_unknown=TRUE;
+  ASSERT false, 'denomination unknown';
+  RETURN;
+END IF;
+out_denom_unknown=FALSE;
+
+INSERT INTO reserves_out
+  (h_blind_ev
+  ,denominations_serial
+  ,denom_sig
+  ,reserve_uuid
+  ,reserve_sig
+  ,execution_date
+  ,amount_with_fee_val
+  ,amount_with_fee_frac)
+VALUES
+  (h_coin_envelope
+  ,denom_serial
+  ,denom_sig
+  ,ruuid
+  ,reserve_sig
+  ,now
+  ,amount_val
+  ,amount_frac)
+ON CONFLICT DO NOTHING;
+
+IF NOT FOUND
+THEN
+  out_conflict=TRUE;
+  RETURN;
+END IF;
+out_conflict=FALSE;
+
+-- Special actions needed for a CS withdraw?
+out_nonce_reuse=FALSE;
+IF NOT NULL cs_nonce
+THEN
+  -- Cache CS signature to prevent replays in the future
+  -- (and check if cached signature exists at the same time).
+  INSERT INTO cs_nonce_locks
+    (nonce
+    ,max_denomination_serial
+    ,op_hash)
+  VALUES
+    (cs_nonce
+    ,denom_serial
+    ,h_coin_envelope)
+  ON CONFLICT DO NOTHING;
+
+  IF NOT FOUND
+  THEN
+    -- See if the existing entry is identical.
+    SELECT 1
+      FROM cs_nonce_locks
+     WHERE nonce=cs_nonce
+       AND op_hash=h_coin_envelope;
+    IF NOT FOUND
+    THEN
+      out_nonce_reuse=TRUE;
+      ASSERT false, 'nonce reuse attempted by client';
+      RETURN;
+    END IF;
+  END IF;
+END IF;
+
+END $$;
+
+COMMENT ON FUNCTION exchange_do_batch_withdraw_insert(BYTEA, INT8, INT4, 
BYTEA, INT8, BYTEA, BYTEA, BYTEA, INT8)
+  IS 'Stores information about a planchet for a batch withdraw operation. 
Checks if the planchet already exists, and in that case indicates a conflict';
+
+
+
+
 CREATE OR REPLACE FUNCTION exchange_do_withdraw_limit_check(
   IN ruuid INT8,
   IN start_time INT8,
diff --git a/src/exchangedb/plugin_exchangedb_postgres.c 
b/src/exchangedb/plugin_exchangedb_postgres.c
index 238322c9..8d29581d 100644
--- a/src/exchangedb/plugin_exchangedb_postgres.c
+++ b/src/exchangedb/plugin_exchangedb_postgres.c
@@ -797,6 +797,31 @@ prepare_statements (struct PostgresClosure *pg)
       " FROM exchange_do_withdraw"
       " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10);",
       10),
+    /* Used in #postgres_do_batch_withdraw() to
+       update the reserve balance and check its status */
+    GNUNET_PQ_make_prepare (
+      "call_batch_withdraw",
+      "SELECT "
+      " reserve_found"
+      ",balance_ok"
+      ",kycok AS kyc_ok"
+      ",account_uuid AS payment_target_uuid"
+      ",ruuid"
+      " FROM exchange_do_batch_withdraw"
+      " ($1,$2,$3,$4,$5);",
+      5),
+    /* Used in #postgres_do_batch_withdraw_insert() to store
+       the signature of a blinded coin with the blinded coin's
+       details. */
+    GNUNET_PQ_make_prepare (
+      "call_batch_withdraw_insert",
+      "SELECT "
+      " out_denom_unknown AS denom_unknown"
+      ",out_conflict AS conflict"
+      ",out_nonce_reuse AS nonce_reuse"
+      " FROM exchange_do_batch_withdraw_insert"
+      " ($1,$2,$3,$4,$5,$6,$7,$8,$9);",
+      9),
     /* Used in #postgres_do_withdraw_limit_check() to check
        if the withdrawals remain below the limit under which
        KYC is not required. */
@@ -5243,6 +5268,122 @@ postgres_do_withdraw (
 }
 
 
+/**
+ * Perform reserve update as part of a batch withdraw operation, checking
+ * for sufficient balance. Persisting the withdrawal details is done
+ * separately!
+ *
+ * @param cls the `struct PostgresClosure` with the plugin-specific state
+ * @param now current time (rounded)
+ * @param reserve_pub public key of the reserve to debit
+ * @param amount total amount to 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] kyc set to the KYC status of the reserve
+ * @param[out] ruuid set to the reserve's UUID (reserves table row)
+ * @return query execution status
+ */
+static enum GNUNET_DB_QueryStatus
+postgres_do_batch_withdraw (
+  void *cls,
+  struct GNUNET_TIME_Timestamp now,
+  const struct TALER_ReservePublicKeyP *reserve_pub,
+  const struct TALER_Amount *amount,
+  bool *found,
+  bool *balance_ok,
+  struct TALER_EXCHANGEDB_KycStatus *kyc,
+  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),
+    GNUNET_PQ_query_param_timestamp (&now),
+    GNUNET_PQ_query_param_timestamp (&gc),
+    GNUNET_PQ_query_param_end
+  };
+  struct GNUNET_PQ_ResultSpec rs[] = {
+    GNUNET_PQ_result_spec_bool ("reserve_found",
+                                found),
+    GNUNET_PQ_result_spec_bool ("balance_ok",
+                                balance_ok),
+    GNUNET_PQ_result_spec_bool ("kyc_ok",
+                                &kyc->ok),
+    GNUNET_PQ_result_spec_uint64 ("payment_target_uuid",
+                                  &kyc->payment_target_uuid),
+    GNUNET_PQ_result_spec_uint64 ("ruuid",
+                                  ruuid),
+    GNUNET_PQ_result_spec_end
+  };
+
+  gc = GNUNET_TIME_absolute_to_timestamp (
+    GNUNET_TIME_absolute_add (now.abs_time,
+                              pg->legal_reserve_expiration_time));
+  kyc->type = TALER_EXCHANGEDB_KYC_WITHDRAW;
+  return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+                                                   "call_batch_withdraw",
+                                                   params,
+                                                   rs);
+}
+
+
+/**
+ * Perform insert as part of a batch withdraw operation, and 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 collectable corresponding collectable coin (blind signature)
+ * @param now current time (rounded)
+ * @param ruuid reserve UUID
+ * @param[out] denom_unknown set if the denomination is unknown in the DB
+ * @param[out] conflict if the envelope was already in the DB
+ * @param[out] nonce_reuse if @a nonce was non-NULL and reused
+ * @return query execution status
+ */
+static enum GNUNET_DB_QueryStatus
+postgres_do_batch_withdraw_insert (
+  void *cls,
+  const struct TALER_CsNonce *nonce,
+  const struct TALER_EXCHANGEDB_CollectableBlindcoin *collectable,
+  struct GNUNET_TIME_Timestamp now,
+  uint64_t ruuid,
+  bool *denom_unknown,
+  bool *conflict,
+  bool *nonce_reuse)
+{
+  struct PostgresClosure *pg = cls;
+  struct GNUNET_PQ_QueryParam params[] = {
+    NULL == nonce
+    ? GNUNET_PQ_query_param_null ()
+    : GNUNET_PQ_query_param_auto_from_type (nonce),
+    TALER_PQ_query_param_amount (&collectable->amount_with_fee),
+    GNUNET_PQ_query_param_auto_from_type (&collectable->denom_pub_hash),
+    GNUNET_PQ_query_param_uint64 (&ruuid),
+    GNUNET_PQ_query_param_auto_from_type (&collectable->reserve_sig),
+    GNUNET_PQ_query_param_auto_from_type (&collectable->h_coin_envelope),
+    TALER_PQ_query_param_blinded_denom_sig (&collectable->sig),
+    GNUNET_PQ_query_param_timestamp (&now),
+    GNUNET_PQ_query_param_end
+  };
+  struct GNUNET_PQ_ResultSpec rs[] = {
+    GNUNET_PQ_result_spec_bool ("denom_unknown",
+                                denom_unknown),
+    GNUNET_PQ_result_spec_bool ("conflict",
+                                conflict),
+    GNUNET_PQ_result_spec_bool ("nonce_reuse",
+                                nonce_reuse),
+    GNUNET_PQ_result_spec_end
+  };
+
+  return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+                                                   
"call_batch_withdraw_insert",
+                                                   params,
+                                                   rs);
+}
+
+
 /**
  * Check that reserve remains below threshold for KYC
  * checks after withdraw operation.
@@ -13931,6 +14072,8 @@ libtaler_plugin_exchangedb_postgres_init (void *cls)
   plugin->reserves_in_insert = &postgres_reserves_in_insert;
   plugin->get_withdraw_info = &postgres_get_withdraw_info;
   plugin->do_withdraw = &postgres_do_withdraw;
+  plugin->do_batch_withdraw = &postgres_do_batch_withdraw;
+  plugin->do_batch_withdraw_insert = &postgres_do_batch_withdraw_insert;
   plugin->do_withdraw_limit_check = &postgres_do_withdraw_limit_check;
   plugin->do_deposit = &postgres_do_deposit;
   plugin->do_melt = &postgres_do_melt;
diff --git a/src/include/taler_exchangedb_plugin.h 
b/src/include/taler_exchangedb_plugin.h
index b347ac56..9cdbb944 100644
--- a/src/include/taler_exchangedb_plugin.h
+++ b/src/include/taler_exchangedb_plugin.h
@@ -2677,6 +2677,59 @@ struct TALER_EXCHANGEDB_Plugin
     uint64_t *ruuid);
 
 
+  /**
+   * Perform reserve update as part of a batch withdraw operation, checking
+   * for sufficient balance. Persisting the withdrawal details is done
+   * separately!
+   *
+   * @param cls the `struct PostgresClosure` with the plugin-specific state
+   * @param now current time (rounded)
+   * @param reserve_pub public key of the reserve to debit
+   * @param amount total amount to 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] kyc set to the KYC status of the reserve
+   * @param[out] ruuid set to the reserve's UUID (reserves table row)
+   * @return query execution status
+   */
+  enum GNUNET_DB_QueryStatus
+  (*do_batch_withdraw)(
+    void *cls,
+    struct GNUNET_TIME_Timestamp now,
+    const struct TALER_ReservePublicKeyP *reserve_pub,
+    const struct TALER_Amount *amount,
+    bool *found,
+    bool *balance_ok,
+    struct TALER_EXCHANGEDB_KycStatus *kyc_ok,
+    uint64_t *ruuid);
+
+
+  /**
+   * Perform insert as part of a batch withdraw operation, and 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 collectable corresponding collectable coin (blind signature)
+   * @param now current time (rounded)
+   * @param ruuid reserve UUID
+   * @param[out] denom_unknown set if the denomination is unknown in the DB
+   * @param[out] conflict if the envelope was already in the DB
+   * @param[out] nonce_reuse if @a nonce was non-NULL and reused
+   * @return query execution status
+   */
+  enum GNUNET_DB_QueryStatus
+  (*do_batch_withdraw_insert)(
+    void *cls,
+    const struct TALER_CsNonce *nonce,
+    const struct TALER_EXCHANGEDB_CollectableBlindcoin *collectable,
+    struct GNUNET_TIME_Timestamp now,
+    uint64_t ruuid,
+    bool *denom_unknown,
+    bool *conflict,
+    bool *nonce_reuse);
+
+
   /**
    * Check that reserve remains below threshold for KYC
    * checks after withdraw operation.

-- 
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]