exchange

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

exchange_api_post-withdraw_blinded.c (24387B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2023-2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12 
     13   You should have received a copy of the GNU General Public License along with
     14   TALER; see the file COPYING.  If not, see
     15   <http://www.gnu.org/licenses/>
     16 */
     17 /**
     18  * @file lib/exchange_api_post-withdraw.c
     19  * @brief Implementation of /withdraw requests
     20  * @author Özgür Kesim
     21  */
     22 #include "taler/platform.h"
     23 #include <gnunet/gnunet_common.h>
     24 #include <jansson.h>
     25 #include <microhttpd.h> /* just for HTTP status codes */
     26 #include <gnunet/gnunet_util_lib.h>
     27 #include <gnunet/gnunet_json_lib.h>
     28 #include <gnunet/gnunet_curl_lib.h>
     29 #include <sys/wait.h>
     30 #include "taler/taler_curl_lib.h"
     31 #include "taler/taler_error_codes.h"
     32 #include "taler/taler_json_lib.h"
     33 #include "taler/taler_exchange_service.h"
     34 #include "exchange_api_common.h"
     35 #include "exchange_api_handle.h"
     36 #include "taler/taler_signatures.h"
     37 #include "exchange_api_curl_defaults.h"
     38 #include "taler/taler_util.h"
     39 
     40 
     41 /**
     42  * A /withdraw request-handle for calls with pre-blinded planchets.
     43  * Returned by TALER_EXCHANGE_post_withdraw_blinded_create.
     44  */
     45 struct TALER_EXCHANGE_PostWithdrawBlindedHandle
     46 {
     47 
     48   /**
     49    * Reserve private key.
     50    */
     51   const struct TALER_ReservePrivateKeyP *reserve_priv;
     52 
     53   /**
     54    * Reserve public key, calculated
     55    */
     56   struct TALER_ReservePublicKeyP reserve_pub;
     57 
     58   /**
     59    * Signature of the reserve for the request, calculated after all
     60    * parameters for the coins are collected.
     61    */
     62   struct TALER_ReserveSignatureP reserve_sig;
     63 
     64   /*
     65    * The denomination keys of the exchange
     66    */
     67   struct TALER_EXCHANGE_Keys *keys;
     68 
     69   /**
     70    * The hash of all the planchets
     71    */
     72   struct TALER_HashBlindedPlanchetsP planchets_h;
     73 
     74   /**
     75    * Seed used for the derival of blinding factors for denominations
     76    * with Clause-Schnorr cipher.
     77    */
     78   const struct TALER_BlindingMasterSeedP *blinding_seed;
     79 
     80   /**
     81    * Total amount requested (without fee).
     82    */
     83   struct TALER_Amount amount;
     84 
     85   /**
     86    * Total withdraw fee
     87    */
     88   struct TALER_Amount fee;
     89 
     90   /**
     91    * Is this call for age-restricted coins, with age proof?
     92    */
     93   bool with_age_proof;
     94 
     95   /**
     96    * If @e with_age_proof is true or @max_age is > 0,
     97    * the age mask to use, extracted from the denominations.
     98    * MUST be the same for all denominations.
     99    */
    100   struct TALER_AgeMask age_mask;
    101 
    102   /**
    103    * The maximum age to commit to.  If @e with_age_proof
    104    * is true, the client will need to proof the correct setting
    105    * of age-restriction on the coins via an additional call
    106    * to /reveal-withdraw.
    107    */
    108   uint8_t max_age;
    109 
    110   /**
    111    * If @e with_age_proof is true, the hash of all the selected planchets
    112    */
    113   struct TALER_HashBlindedPlanchetsP selected_h;
    114 
    115   /**
    116    * Length of the either the @e blinded.input or
    117    * the @e blinded.with_age_proof_input array,
    118    * depending on @e with_age_proof.
    119    */
    120   size_t num_input;
    121 
    122   union
    123   {
    124     /**
    125      * The blinded planchet input candidates for age-restricted coins
    126      * for the call to /withdraw
    127      */
    128     const struct
    129     TALER_EXCHANGE_WithdrawBlindedAgeRestrictedCoinInput *with_age_proof_input;
    130 
    131     /**
    132      * The blinded planchet input for the call to /withdraw,
    133      * for age-unrestricted coins.
    134      */
    135     const struct TALER_EXCHANGE_WithdrawBlindedCoinInput *input;
    136 
    137   } blinded;
    138 
    139   /**
    140    * The url for this request.
    141    */
    142   char *request_url;
    143 
    144   /**
    145    * Context for curl.
    146    */
    147   struct GNUNET_CURL_Context *curl_ctx;
    148 
    149   /**
    150    * CURL handle for the request job.
    151    */
    152   struct GNUNET_CURL_Job *job;
    153 
    154   /**
    155    * Post Context
    156    */
    157   struct TALER_CURL_PostContext post_ctx;
    158 
    159   /**
    160    * Function to call with withdraw response results.
    161    */
    162   TALER_EXCHANGE_PostWithdrawBlindedCallback callback;
    163 
    164   /**
    165    * Closure for @e callback
    166    */
    167   void *callback_cls;
    168 };
    169 
    170 
    171 /**
    172  * We got a 200 OK response for the /withdraw operation.
    173  * Extract the signatures and return them to the caller.
    174  *
    175  * @param wbh operation handle
    176  * @param j_response reply from the exchange
    177  * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors
    178  */
    179 static enum GNUNET_GenericReturnValue
    180 withdraw_blinded_ok (
    181   struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh,
    182   const json_t *j_response)
    183 {
    184   struct TALER_EXCHANGE_PostWithdrawBlindedResponse response = {
    185     .hr.reply = j_response,
    186     .hr.http_status = MHD_HTTP_OK,
    187   };
    188   const json_t *j_sigs;
    189   struct GNUNET_JSON_Specification spec[] = {
    190     GNUNET_JSON_spec_array_const ("ev_sigs",
    191                                   &j_sigs),
    192     GNUNET_JSON_spec_end ()
    193   };
    194 
    195   if (GNUNET_OK !=
    196       GNUNET_JSON_parse (j_response,
    197                          spec,
    198                          NULL, NULL))
    199   {
    200     GNUNET_break_op (0);
    201     return GNUNET_SYSERR;
    202   }
    203 
    204   if (wbh->num_input != json_array_size (j_sigs))
    205   {
    206     /* Number of coins generated does not match our expectation */
    207     GNUNET_break_op (0);
    208     return GNUNET_SYSERR;
    209   }
    210 
    211   {
    212     struct TALER_BlindedDenominationSignature denoms_sig[wbh->num_input];
    213 
    214     memset (denoms_sig,
    215             0,
    216             sizeof(denoms_sig));
    217 
    218     /* Reconstruct the coins and unblind the signatures */
    219     {
    220       json_t *j_sig;
    221       size_t i;
    222 
    223       json_array_foreach (j_sigs, i, j_sig)
    224       {
    225         struct GNUNET_JSON_Specification ispec[] = {
    226           TALER_JSON_spec_blinded_denom_sig (NULL,
    227                                              &denoms_sig[i]),
    228           GNUNET_JSON_spec_end ()
    229         };
    230 
    231         if (GNUNET_OK !=
    232             GNUNET_JSON_parse (j_sig,
    233                                ispec,
    234                                NULL, NULL))
    235         {
    236           GNUNET_break_op (0);
    237           return GNUNET_SYSERR;
    238         }
    239       }
    240     }
    241 
    242     response.details.ok.num_sigs = wbh->num_input;
    243     response.details.ok.blinded_denom_sigs = denoms_sig;
    244     response.details.ok.planchets_h = wbh->planchets_h;
    245     wbh->callback (
    246       wbh->callback_cls,
    247       &response);
    248     /* Make sure the callback isn't called again */
    249     wbh->callback = NULL;
    250     /* Free resources */
    251     for (size_t i = 0; i < wbh->num_input; i++)
    252       TALER_blinded_denom_sig_free (&denoms_sig[i]);
    253   }
    254 
    255   return GNUNET_OK;
    256 }
    257 
    258 
    259 /**
    260  * We got a 201 CREATED response for the /withdraw operation.
    261  * Extract the noreveal_index and return it to the caller.
    262  *
    263  * @param wbh operation handle
    264  * @param j_response reply from the exchange
    265  * @return #GNUNET_OK on success, #GNUNET_SYSERR on errors
    266  */
    267 static enum GNUNET_GenericReturnValue
    268 withdraw_blinded_created (
    269   struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh,
    270   const json_t *j_response)
    271 {
    272   struct TALER_EXCHANGE_PostWithdrawBlindedResponse response = {
    273     .hr.reply = j_response,
    274     .hr.http_status = MHD_HTTP_CREATED,
    275     .details.created.planchets_h = wbh->planchets_h,
    276     .details.created.num_coins = wbh->num_input,
    277   };
    278   struct TALER_ExchangeSignatureP exchange_sig;
    279   struct GNUNET_JSON_Specification spec[] = {
    280     GNUNET_JSON_spec_uint8 ("noreveal_index",
    281                             &response.details.created.noreveal_index),
    282     GNUNET_JSON_spec_fixed_auto ("exchange_sig",
    283                                  &exchange_sig),
    284     GNUNET_JSON_spec_fixed_auto ("exchange_pub",
    285                                  &response.details.created.exchange_pub),
    286     GNUNET_JSON_spec_end ()
    287   };
    288 
    289   if (GNUNET_OK!=
    290       GNUNET_JSON_parse (j_response,
    291                          spec,
    292                          NULL, NULL))
    293   {
    294     GNUNET_break_op (0);
    295     return GNUNET_SYSERR;
    296   }
    297 
    298   if (GNUNET_OK !=
    299       TALER_exchange_online_withdraw_age_confirmation_verify (
    300         &wbh->planchets_h,
    301         response.details.created.noreveal_index,
    302         &response.details.created.exchange_pub,
    303         &exchange_sig))
    304   {
    305     GNUNET_break_op (0);
    306     return GNUNET_SYSERR;
    307 
    308   }
    309 
    310   wbh->callback (wbh->callback_cls,
    311                  &response);
    312   /* make sure the callback isn't called again */
    313   wbh->callback = NULL;
    314 
    315   return GNUNET_OK;
    316 }
    317 
    318 
    319 /**
    320  * Function called when we're done processing the
    321  * HTTP /withdraw request.
    322  *
    323  * @param cls the `struct TALER_EXCHANGE_PostWithdrawBlindedHandle`
    324  * @param response_code The HTTP response code
    325  * @param response response data
    326  */
    327 static void
    328 handle_withdraw_blinded_finished (
    329   void *cls,
    330   long response_code,
    331   const void *response)
    332 {
    333   struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh = cls;
    334   const json_t *j_response = response;
    335   struct TALER_EXCHANGE_PostWithdrawBlindedResponse wbr = {
    336     .hr.reply = j_response,
    337     .hr.http_status = (unsigned int) response_code
    338   };
    339 
    340   wbh->job = NULL;
    341   switch (response_code)
    342   {
    343   case 0:
    344     wbr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
    345     break;
    346   case MHD_HTTP_OK:
    347     {
    348       if (GNUNET_OK !=
    349           withdraw_blinded_ok (
    350             wbh,
    351             j_response))
    352       {
    353         GNUNET_break_op (0);
    354         wbr.hr.http_status = 0;
    355         wbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    356         break;
    357       }
    358       GNUNET_assert (NULL == wbh->callback);
    359       TALER_EXCHANGE_post_withdraw_blinded_cancel (wbh);
    360       return;
    361     }
    362   case MHD_HTTP_CREATED:
    363     if (GNUNET_OK !=
    364         withdraw_blinded_created (
    365           wbh,
    366           j_response))
    367     {
    368       GNUNET_break_op (0);
    369       wbr.hr.http_status = 0;
    370       wbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    371       break;
    372     }
    373     GNUNET_assert (NULL == wbh->callback);
    374     TALER_EXCHANGE_post_withdraw_blinded_cancel (wbh);
    375     return;
    376   case MHD_HTTP_BAD_REQUEST:
    377     /* This should never happen, either us or the exchange is buggy
    378        (or API version conflict); just pass JSON reply to the application */
    379     wbr.hr.ec = TALER_JSON_get_error_code (j_response);
    380     wbr.hr.hint = TALER_JSON_get_error_hint (j_response);
    381     break;
    382   case MHD_HTTP_FORBIDDEN:
    383     GNUNET_break_op (0);
    384     /* Nothing really to verify, exchange says one of the signatures is
    385        invalid; as we checked them, this should never happen, we
    386        should pass the JSON reply to the application */
    387     wbr.hr.ec = TALER_JSON_get_error_code (j_response);
    388     wbr.hr.hint = TALER_JSON_get_error_hint (j_response);
    389     break;
    390   case MHD_HTTP_NOT_FOUND:
    391     /* Nothing really to verify, the exchange basically just says
    392        that it doesn't know this reserve.  Can happen if we
    393        query before the wire transfer went through.
    394        We should simply pass the JSON reply to the application. */
    395     wbr.hr.ec = TALER_JSON_get_error_code (j_response);
    396     wbr.hr.hint = TALER_JSON_get_error_hint (j_response);
    397     break;
    398   case MHD_HTTP_CONFLICT:
    399     wbr.hr.ec = TALER_JSON_get_error_code (j_response);
    400     wbr.hr.hint = TALER_JSON_get_error_hint (j_response);
    401     if (TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS ==
    402         wbr.hr.ec)
    403     {
    404       struct GNUNET_JSON_Specification spec[] = {
    405         TALER_JSON_spec_amount_any (
    406           "balance",
    407           &wbr.details.conflict.details.generic_insufficient_funds.balance),
    408         TALER_JSON_spec_amount_any (
    409           "requested_amount",
    410           &wbr.details.conflict.details.generic_insufficient_funds.
    411           requested_amount),
    412         GNUNET_JSON_spec_end ()
    413       };
    414 
    415       if (GNUNET_OK !=
    416           GNUNET_JSON_parse (j_response,
    417                              spec,
    418                              NULL, NULL))
    419       {
    420         GNUNET_break_op (0);
    421         wbr.hr.http_status = 0;
    422         wbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    423         break;
    424       }
    425     }
    426     break;
    427   case MHD_HTTP_GONE:
    428     /* could happen if denomination was revoked */
    429     /* Note: one might want to check /keys for revocation
    430        signature here, alas tricky in case our /keys
    431        is outdated => left to clients */
    432     wbr.hr.ec = TALER_JSON_get_error_code (j_response);
    433     wbr.hr.hint = TALER_JSON_get_error_hint (j_response);
    434     break;
    435   case MHD_HTTP_PRECONDITION_FAILED:
    436     /* could happen if we were too early and the denomination
    437        is not yet available */
    438     /* Note: one might want to check the "Date" header to
    439        see if our clock is very far off */
    440     wbr.hr.ec = TALER_JSON_get_error_code (j_response);
    441     wbr.hr.hint = TALER_JSON_get_error_hint (j_response);
    442     break;
    443   case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
    444     wbr.hr.ec = TALER_JSON_get_error_code (j_response);
    445     wbr.hr.hint = TALER_JSON_get_error_hint (j_response);
    446     if (GNUNET_OK !=
    447         TALER_EXCHANGE_parse_451 (&wbr.details.unavailable_for_legal_reasons,
    448                                   j_response))
    449     {
    450       GNUNET_break_op (0);
    451       wbr.hr.http_status = 0;
    452       wbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    453       break;
    454     }
    455     break;
    456   case MHD_HTTP_INTERNAL_SERVER_ERROR:
    457     /* Server had an internal issue; we should retry, but this API
    458        leaves this to the application */
    459     wbr.hr.ec = TALER_JSON_get_error_code (j_response);
    460     wbr.hr.hint = TALER_JSON_get_error_hint (j_response);
    461     break;
    462   case MHD_HTTP_NOT_IMPLEMENTED:
    463     /* Server does not implement a feature (usually the cipher) */
    464     wbr.hr.ec = TALER_JSON_get_error_code (j_response);
    465     wbr.hr.hint = TALER_JSON_get_error_hint (j_response);
    466     break;
    467   case MHD_HTTP_BAD_GATEWAY:
    468     /* Server could not talk to another component, usually this
    469        indicates a problem with the secmod helper */
    470     wbr.hr.ec = TALER_JSON_get_error_code (j_response);
    471     wbr.hr.hint = TALER_JSON_get_error_hint (j_response);
    472     break;
    473   case MHD_HTTP_SERVICE_UNAVAILABLE:
    474     /* Server had an internal issue; we should retry, but this API
    475        leaves this to the application */
    476     wbr.hr.ec = TALER_JSON_get_error_code (j_response);
    477     wbr.hr.hint = TALER_JSON_get_error_hint (j_response);
    478     break;
    479   default:
    480     /* unexpected response code */
    481     GNUNET_break_op (0);
    482     wbr.hr.ec = TALER_JSON_get_error_code (j_response);
    483     wbr.hr.hint = TALER_JSON_get_error_hint (j_response);
    484     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    485                 "Unexpected response code %u/%d for exchange withdraw\n",
    486                 (unsigned int) response_code,
    487                 (int) wbr.hr.ec);
    488     break;
    489   }
    490   wbh->callback (wbh->callback_cls,
    491                  &wbr);
    492   TALER_EXCHANGE_post_withdraw_blinded_cancel (wbh);
    493 }
    494 
    495 
    496 struct TALER_EXCHANGE_PostWithdrawBlindedHandle *
    497 TALER_EXCHANGE_post_withdraw_blinded_create (
    498   struct GNUNET_CURL_Context *curl_ctx,
    499   struct TALER_EXCHANGE_Keys *keys,
    500   const char *exchange_url,
    501   const struct TALER_ReservePrivateKeyP *reserve_priv,
    502   const struct TALER_BlindingMasterSeedP *blinding_seed,
    503   size_t num_input,
    504   const struct TALER_EXCHANGE_WithdrawBlindedCoinInput *blinded_input)
    505 {
    506   struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh =
    507     GNUNET_new (struct TALER_EXCHANGE_PostWithdrawBlindedHandle);
    508 
    509   wbh->keys = TALER_EXCHANGE_keys_incref (keys);
    510   wbh->curl_ctx = curl_ctx;
    511   wbh->reserve_priv = reserve_priv;
    512   wbh->request_url = TALER_url_join (exchange_url,
    513                                      "withdraw",
    514                                      NULL);
    515   GNUNET_CRYPTO_eddsa_key_get_public (
    516     &wbh->reserve_priv->eddsa_priv,
    517     &wbh->reserve_pub.eddsa_pub);
    518   wbh->num_input = num_input;
    519   wbh->blinded.input = blinded_input;
    520   wbh->blinding_seed = blinding_seed;
    521 
    522   return wbh;
    523 }
    524 
    525 
    526 enum GNUNET_GenericReturnValue
    527 TALER_EXCHANGE_post_withdraw_blinded_set_options_ (
    528   struct TALER_EXCHANGE_PostWithdrawBlindedHandle *pwbh,
    529   unsigned int num_options,
    530   const struct TALER_EXCHANGE_PostWithdrawBlindedOptionValue options[])
    531 {
    532   for (unsigned int i = 0; i < num_options; i++)
    533   {
    534     const struct TALER_EXCHANGE_PostWithdrawBlindedOptionValue *opt =
    535       &options[i];
    536     switch (opt->option)
    537     {
    538     case TALER_EXCHANGE_POST_WITHDRAW_BLINDED_OPTION_END:
    539       return GNUNET_OK;
    540     case TALER_EXCHANGE_POST_WITHDRAW_BLINDED_OPTION_WITH_AGE_PROOF:
    541       pwbh->with_age_proof = true;
    542       pwbh->max_age = opt->details.with_age_proof.max_age;
    543       pwbh->blinded.with_age_proof_input = opt->details.with_age_proof.input;
    544       break;
    545     }
    546   }
    547   return GNUNET_OK;
    548 }
    549 
    550 
    551 enum TALER_ErrorCode
    552 TALER_EXCHANGE_post_withdraw_blinded_start (
    553   struct TALER_EXCHANGE_PostWithdrawBlindedHandle *pwbh,
    554   TALER_EXCHANGE_PostWithdrawBlindedCallback cb,
    555   TALER_EXCHANGE_POST_WITHDRAW_BLINDED_RESULT_CLOSURE *cb_cls)
    556 {
    557   json_t *j_denoms = NULL;
    558   json_t *j_planchets = NULL;
    559   json_t *j_request_body = NULL;
    560   CURL *curlh = NULL;
    561   struct GNUNET_HashContext *coins_hctx = NULL;
    562   struct TALER_BlindedCoinHashP bch;
    563 
    564   pwbh->callback = cb;
    565   pwbh->callback_cls = cb_cls;
    566 #define FAIL_IF(cond) \
    567         do { \
    568           if ((cond)) \
    569           { \
    570             GNUNET_break (! (cond)); \
    571             goto ERROR; \
    572           } \
    573         } while (0)
    574 
    575   GNUNET_assert (0 < pwbh->num_input);
    576 
    577   FAIL_IF (GNUNET_OK !=
    578            TALER_amount_set_zero (pwbh->keys->currency,
    579                                   &pwbh->amount));
    580   FAIL_IF (GNUNET_OK !=
    581            TALER_amount_set_zero (pwbh->keys->currency,
    582                                   &pwbh->fee));
    583 
    584   /* Accumulate total value with fees */
    585   for (size_t i = 0; i < pwbh->num_input; i++)
    586   {
    587     const struct TALER_EXCHANGE_DenomPublicKey *dpub =
    588       pwbh->with_age_proof ?
    589       pwbh->blinded.with_age_proof_input[i].denom_pub :
    590       pwbh->blinded.input[i].denom_pub;
    591 
    592     FAIL_IF (0 >
    593              TALER_amount_add (&pwbh->amount,
    594                                &pwbh->amount,
    595                                &dpub->value));
    596     FAIL_IF (0 >
    597              TALER_amount_add (&pwbh->fee,
    598                                &pwbh->fee,
    599                                &dpub->fees.withdraw));
    600 
    601     if (GNUNET_CRYPTO_BSA_CS ==
    602         dpub->key.bsign_pub_key->cipher)
    603       GNUNET_assert (NULL != pwbh->blinding_seed);
    604 
    605   }
    606 
    607   if (pwbh->with_age_proof || pwbh->max_age > 0)
    608   {
    609     pwbh->age_mask =
    610       pwbh->blinded.with_age_proof_input[0].denom_pub->key.age_mask;
    611 
    612     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    613                 "Attempting to withdraw from reserve %s with maximum age %d to proof\n",
    614                 TALER_B2S (&pwbh->reserve_pub),
    615                 pwbh->max_age);
    616   }
    617   else
    618   {
    619     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    620                 "Attempting to withdraw from reserve %s\n",
    621                 TALER_B2S (&pwbh->reserve_pub));
    622   }
    623 
    624   coins_hctx = GNUNET_CRYPTO_hash_context_start ();
    625   FAIL_IF (NULL == coins_hctx);
    626 
    627   j_denoms = json_array ();
    628   j_planchets = json_array ();
    629   FAIL_IF ((NULL == j_denoms) ||
    630            (NULL == j_planchets));
    631 
    632   for (size_t i  = 0; i< pwbh->num_input; i++)
    633   {
    634     /* Build the denomination array */
    635     const struct TALER_EXCHANGE_DenomPublicKey *denom_pub =
    636       pwbh->with_age_proof ?
    637       pwbh->blinded.with_age_proof_input[i].denom_pub :
    638       pwbh->blinded.input[i].denom_pub;
    639     const struct TALER_DenominationHashP *denom_h = &denom_pub->h_key;
    640     json_t *jdenom;
    641 
    642     /* The mask must be the same for all coins */
    643     FAIL_IF (pwbh->with_age_proof &&
    644              (pwbh->age_mask.bits != denom_pub->key.age_mask.bits));
    645 
    646     jdenom = GNUNET_JSON_from_data_auto (denom_h);
    647     FAIL_IF (NULL == jdenom);
    648     FAIL_IF (0 > json_array_append_new (j_denoms,
    649                                         jdenom));
    650   }
    651 
    652 
    653   /* Build the planchet array and calculate the hash over all planchets. */
    654   if (! pwbh->with_age_proof)
    655   {
    656     for (size_t i  = 0; i< pwbh->num_input; i++)
    657     {
    658       const struct TALER_PlanchetDetail *planchet =
    659         &pwbh->blinded.input[i].planchet_details;
    660       json_t *jc = GNUNET_JSON_PACK (
    661         TALER_JSON_pack_blinded_planchet (
    662           NULL,
    663           &planchet->blinded_planchet));
    664       FAIL_IF (NULL == jc);
    665       FAIL_IF (0 > json_array_append_new (j_planchets,
    666                                           jc));
    667 
    668       TALER_coin_ev_hash (&planchet->blinded_planchet,
    669                           &planchet->denom_pub_hash,
    670                           &bch);
    671 
    672       GNUNET_CRYPTO_hash_context_read (coins_hctx,
    673                                        &bch,
    674                                        sizeof(bch));
    675     }
    676   }
    677   else
    678   { /* Age restricted case with required age-proof. */
    679 
    680     /**
    681      * We collect the run of all coin candidates for the same γ index
    682      * first, then γ+1 etc.
    683      */
    684     for (size_t k = 0; k < TALER_CNC_KAPPA; k++)
    685     {
    686       struct GNUNET_HashContext *batch_ctx;
    687       struct TALER_BlindedCoinHashP batch_h;
    688 
    689       batch_ctx = GNUNET_CRYPTO_hash_context_start ();
    690       FAIL_IF (NULL == batch_ctx);
    691 
    692       for (size_t i  = 0; i< pwbh->num_input; i++)
    693       {
    694         const struct TALER_PlanchetDetail *planchet =
    695           &pwbh->blinded.with_age_proof_input[i].planchet_details[k];
    696         json_t *jc = GNUNET_JSON_PACK (
    697           TALER_JSON_pack_blinded_planchet (
    698             NULL,
    699             &planchet->blinded_planchet));
    700 
    701         FAIL_IF (NULL == jc);
    702         FAIL_IF (0 > json_array_append_new (
    703                    j_planchets,
    704                    jc));
    705 
    706         TALER_coin_ev_hash (
    707           &planchet->blinded_planchet,
    708           &planchet->denom_pub_hash,
    709           &bch);
    710 
    711         GNUNET_CRYPTO_hash_context_read (
    712           batch_ctx,
    713           &bch,
    714           sizeof(bch));
    715       }
    716 
    717       GNUNET_CRYPTO_hash_context_finish (
    718         batch_ctx,
    719         &batch_h.hash);
    720       GNUNET_CRYPTO_hash_context_read (
    721         coins_hctx,
    722         &batch_h,
    723         sizeof(batch_h));
    724     }
    725   }
    726 
    727   GNUNET_CRYPTO_hash_context_finish (
    728     coins_hctx,
    729     &pwbh->planchets_h.hash);
    730   coins_hctx = NULL;
    731 
    732   TALER_wallet_withdraw_sign (
    733     &pwbh->amount,
    734     &pwbh->fee,
    735     &pwbh->planchets_h,
    736     pwbh->blinding_seed,
    737     pwbh->with_age_proof ? &pwbh->age_mask: NULL,
    738     pwbh->with_age_proof ? pwbh->max_age : 0,
    739     pwbh->reserve_priv,
    740     &pwbh->reserve_sig);
    741 
    742   /* Initiate the POST-request */
    743   j_request_body = GNUNET_JSON_PACK (
    744     GNUNET_JSON_pack_string ("cipher",
    745                              "ED25519"),
    746     GNUNET_JSON_pack_data_auto ("reserve_pub",
    747                                 &pwbh->reserve_pub),
    748     GNUNET_JSON_pack_array_steal ("denoms_h",
    749                                   j_denoms),
    750     GNUNET_JSON_pack_array_steal ("coin_evs",
    751                                   j_planchets),
    752     GNUNET_JSON_pack_allow_null (
    753       pwbh->with_age_proof
    754       ? GNUNET_JSON_pack_uint64 ("max_age",
    755                                  pwbh->max_age)
    756       : GNUNET_JSON_pack_string ("max_age",
    757                                  NULL) ),
    758     GNUNET_JSON_pack_data_auto ("reserve_sig",
    759                                 &pwbh->reserve_sig));
    760   FAIL_IF (NULL == j_request_body);
    761 
    762   if (NULL != pwbh->blinding_seed)
    763   {
    764     json_t *j_seed = GNUNET_JSON_PACK (
    765       GNUNET_JSON_pack_data_auto ("blinding_seed",
    766                                   pwbh->blinding_seed));
    767     GNUNET_assert (NULL != j_seed);
    768     GNUNET_assert (0 ==
    769                    json_object_update_new (
    770                      j_request_body,
    771                      j_seed));
    772   }
    773 
    774   curlh = TALER_EXCHANGE_curl_easy_get_ (pwbh->request_url);
    775   FAIL_IF (NULL == curlh);
    776   FAIL_IF (GNUNET_OK !=
    777            TALER_curl_easy_post (
    778              &pwbh->post_ctx,
    779              curlh,
    780              j_request_body));
    781   json_decref (j_request_body);
    782   j_request_body = NULL;
    783 
    784   pwbh->job = GNUNET_CURL_job_add2 (
    785     pwbh->curl_ctx,
    786     curlh,
    787     pwbh->post_ctx.headers,
    788     &handle_withdraw_blinded_finished,
    789     pwbh);
    790   FAIL_IF (NULL == pwbh->job);
    791 
    792   return TALER_EC_NONE;
    793 
    794 ERROR:
    795   if (NULL != coins_hctx)
    796     GNUNET_CRYPTO_hash_context_abort (coins_hctx);
    797   if (NULL != j_denoms)
    798     json_decref (j_denoms);
    799   if (NULL != j_planchets)
    800     json_decref (j_planchets);
    801   if (NULL != j_request_body)
    802     json_decref (j_request_body);
    803   if (NULL != curlh)
    804     curl_easy_cleanup (curlh);
    805   return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
    806 #undef FAIL_IF
    807 }
    808 
    809 
    810 void
    811 TALER_EXCHANGE_post_withdraw_blinded_cancel (
    812   struct TALER_EXCHANGE_PostWithdrawBlindedHandle *pwbh)
    813 {
    814   if (NULL == pwbh)
    815     return;
    816   if (NULL != pwbh->job)
    817   {
    818     GNUNET_CURL_job_cancel (pwbh->job);
    819     pwbh->job = NULL;
    820   }
    821   GNUNET_free (pwbh->request_url);
    822   TALER_EXCHANGE_keys_decref (pwbh->keys);
    823   TALER_curl_easy_post_finished (&pwbh->post_ctx);
    824   GNUNET_free (pwbh);
    825 }
    826 
    827 
    828 /* exchange_api_post-withdraw_blinded.c */