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 (22853B)


      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     /* The age requirements might not have been met */
    400     wbr.hr.ec = TALER_JSON_get_error_code (j_response);
    401     wbr.hr.hint = TALER_JSON_get_error_hint (j_response);
    402     break;
    403   case MHD_HTTP_GONE:
    404     /* could happen if denomination was revoked */
    405     /* Note: one might want to check /keys for revocation
    406        signature here, alas tricky in case our /keys
    407        is outdated => left to clients */
    408     wbr.hr.ec = TALER_JSON_get_error_code (j_response);
    409     wbr.hr.hint = TALER_JSON_get_error_hint (j_response);
    410     break;
    411   case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
    412     /* only validate reply is well-formed */
    413     {
    414       struct GNUNET_JSON_Specification spec[] = {
    415         GNUNET_JSON_spec_fixed_auto (
    416           "h_payto",
    417           &wbr.details.unavailable_for_legal_reasons.h_payto),
    418         GNUNET_JSON_spec_uint64 (
    419           "requirement_row",
    420           &wbr.details.unavailable_for_legal_reasons.requirement_row),
    421         GNUNET_JSON_spec_end ()
    422       };
    423 
    424       if (GNUNET_OK !=
    425           GNUNET_JSON_parse (j_response,
    426                              spec,
    427                              NULL, NULL))
    428       {
    429         GNUNET_break_op (0);
    430         wbr.hr.http_status = 0;
    431         wbr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    432         break;
    433       }
    434       break;
    435     }
    436   case MHD_HTTP_INTERNAL_SERVER_ERROR:
    437     /* Server had an internal issue; we should retry, but this API
    438        leaves this to the application */
    439     wbr.hr.ec = TALER_JSON_get_error_code (j_response);
    440     wbr.hr.hint = TALER_JSON_get_error_hint (j_response);
    441     break;
    442   default:
    443     /* unexpected response code */
    444     GNUNET_break_op (0);
    445     wbr.hr.ec = TALER_JSON_get_error_code (j_response);
    446     wbr.hr.hint = TALER_JSON_get_error_hint (j_response);
    447     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    448                 "Unexpected response code %u/%d for exchange withdraw\n",
    449                 (unsigned int) response_code,
    450                 (int) wbr.hr.ec);
    451     break;
    452   }
    453   wbh->callback (wbh->callback_cls,
    454                  &wbr);
    455   TALER_EXCHANGE_post_withdraw_blinded_cancel (wbh);
    456 }
    457 
    458 
    459 struct TALER_EXCHANGE_PostWithdrawBlindedHandle *
    460 TALER_EXCHANGE_post_withdraw_blinded_create (
    461   struct GNUNET_CURL_Context *curl_ctx,
    462   struct TALER_EXCHANGE_Keys *keys,
    463   const char *exchange_url,
    464   const struct TALER_ReservePrivateKeyP *reserve_priv,
    465   const struct TALER_BlindingMasterSeedP *blinding_seed,
    466   size_t num_input,
    467   const struct TALER_EXCHANGE_WithdrawBlindedCoinInput *blinded_input)
    468 {
    469   struct TALER_EXCHANGE_PostWithdrawBlindedHandle *wbh =
    470     GNUNET_new (struct TALER_EXCHANGE_PostWithdrawBlindedHandle);
    471 
    472   wbh->keys = TALER_EXCHANGE_keys_incref (keys);
    473   wbh->curl_ctx = curl_ctx;
    474   wbh->reserve_priv = reserve_priv;
    475   wbh->request_url = TALER_url_join (exchange_url,
    476                                      "withdraw",
    477                                      NULL);
    478   GNUNET_CRYPTO_eddsa_key_get_public (
    479     &wbh->reserve_priv->eddsa_priv,
    480     &wbh->reserve_pub.eddsa_pub);
    481   wbh->num_input = num_input;
    482   wbh->blinded.input = blinded_input;
    483   wbh->blinding_seed = blinding_seed;
    484 
    485   return wbh;
    486 }
    487 
    488 
    489 enum GNUNET_GenericReturnValue
    490 TALER_EXCHANGE_post_withdraw_blinded_set_options_ (
    491   struct TALER_EXCHANGE_PostWithdrawBlindedHandle *pwbh,
    492   unsigned int num_options,
    493   const struct TALER_EXCHANGE_PostWithdrawBlindedOptionValue options[])
    494 {
    495   for (unsigned int i = 0; i < num_options; i++)
    496   {
    497     const struct TALER_EXCHANGE_PostWithdrawBlindedOptionValue *opt =
    498       &options[i];
    499     switch (opt->option)
    500     {
    501     case TALER_EXCHANGE_POST_WITHDRAW_BLINDED_OPTION_END:
    502       return GNUNET_OK;
    503     case TALER_EXCHANGE_POST_WITHDRAW_BLINDED_OPTION_WITH_AGE_PROOF:
    504       pwbh->with_age_proof = true;
    505       pwbh->max_age = opt->details.with_age_proof.max_age;
    506       pwbh->blinded.with_age_proof_input = opt->details.with_age_proof.input;
    507       break;
    508     }
    509   }
    510   return GNUNET_OK;
    511 }
    512 
    513 
    514 enum TALER_ErrorCode
    515 TALER_EXCHANGE_post_withdraw_blinded_start (
    516   struct TALER_EXCHANGE_PostWithdrawBlindedHandle *pwbh,
    517   TALER_EXCHANGE_PostWithdrawBlindedCallback cb,
    518   TALER_EXCHANGE_POST_WITHDRAW_BLINDED_RESULT_CLOSURE *cb_cls)
    519 {
    520   json_t *j_denoms = NULL;
    521   json_t *j_planchets = NULL;
    522   json_t *j_request_body = NULL;
    523   CURL *curlh = NULL;
    524   struct GNUNET_HashContext *coins_hctx = NULL;
    525   struct TALER_BlindedCoinHashP bch;
    526 
    527   pwbh->callback = cb;
    528   pwbh->callback_cls = cb_cls;
    529 #define FAIL_IF(cond) \
    530         do { \
    531           if ((cond)) \
    532           { \
    533             GNUNET_break (! (cond)); \
    534             goto ERROR; \
    535           } \
    536         } while (0)
    537 
    538   GNUNET_assert (0 < pwbh->num_input);
    539 
    540   FAIL_IF (GNUNET_OK !=
    541            TALER_amount_set_zero (pwbh->keys->currency,
    542                                   &pwbh->amount));
    543   FAIL_IF (GNUNET_OK !=
    544            TALER_amount_set_zero (pwbh->keys->currency,
    545                                   &pwbh->fee));
    546 
    547   /* Accumulate total value with fees */
    548   for (size_t i = 0; i < pwbh->num_input; i++)
    549   {
    550     const struct TALER_EXCHANGE_DenomPublicKey *dpub =
    551       pwbh->with_age_proof ?
    552       pwbh->blinded.with_age_proof_input[i].denom_pub :
    553       pwbh->blinded.input[i].denom_pub;
    554 
    555     FAIL_IF (0 >
    556              TALER_amount_add (&pwbh->amount,
    557                                &pwbh->amount,
    558                                &dpub->value));
    559     FAIL_IF (0 >
    560              TALER_amount_add (&pwbh->fee,
    561                                &pwbh->fee,
    562                                &dpub->fees.withdraw));
    563 
    564     if (GNUNET_CRYPTO_BSA_CS ==
    565         dpub->key.bsign_pub_key->cipher)
    566       GNUNET_assert (NULL != pwbh->blinding_seed);
    567 
    568   }
    569 
    570   if (pwbh->with_age_proof || pwbh->max_age > 0)
    571   {
    572     pwbh->age_mask =
    573       pwbh->blinded.with_age_proof_input[0].denom_pub->key.age_mask;
    574 
    575     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    576                 "Attempting to withdraw from reserve %s with maximum age %d to proof\n",
    577                 TALER_B2S (&pwbh->reserve_pub),
    578                 pwbh->max_age);
    579   }
    580   else
    581   {
    582     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    583                 "Attempting to withdraw from reserve %s\n",
    584                 TALER_B2S (&pwbh->reserve_pub));
    585   }
    586 
    587   coins_hctx = GNUNET_CRYPTO_hash_context_start ();
    588   FAIL_IF (NULL == coins_hctx);
    589 
    590   j_denoms = json_array ();
    591   j_planchets = json_array ();
    592   FAIL_IF ((NULL == j_denoms) ||
    593            (NULL == j_planchets));
    594 
    595   for (size_t i  = 0; i< pwbh->num_input; i++)
    596   {
    597     /* Build the denomination array */
    598     const struct TALER_EXCHANGE_DenomPublicKey *denom_pub =
    599       pwbh->with_age_proof ?
    600       pwbh->blinded.with_age_proof_input[i].denom_pub :
    601       pwbh->blinded.input[i].denom_pub;
    602     const struct TALER_DenominationHashP *denom_h = &denom_pub->h_key;
    603     json_t *jdenom;
    604 
    605     /* The mask must be the same for all coins */
    606     FAIL_IF (pwbh->with_age_proof &&
    607              (pwbh->age_mask.bits != denom_pub->key.age_mask.bits));
    608 
    609     jdenom = GNUNET_JSON_from_data_auto (denom_h);
    610     FAIL_IF (NULL == jdenom);
    611     FAIL_IF (0 > json_array_append_new (j_denoms,
    612                                         jdenom));
    613   }
    614 
    615 
    616   /* Build the planchet array and calculate the hash over all planchets. */
    617   if (! pwbh->with_age_proof)
    618   {
    619     for (size_t i  = 0; i< pwbh->num_input; i++)
    620     {
    621       const struct TALER_PlanchetDetail *planchet =
    622         &pwbh->blinded.input[i].planchet_details;
    623       json_t *jc = GNUNET_JSON_PACK (
    624         TALER_JSON_pack_blinded_planchet (
    625           NULL,
    626           &planchet->blinded_planchet));
    627       FAIL_IF (NULL == jc);
    628       FAIL_IF (0 > json_array_append_new (j_planchets,
    629                                           jc));
    630 
    631       TALER_coin_ev_hash (&planchet->blinded_planchet,
    632                           &planchet->denom_pub_hash,
    633                           &bch);
    634 
    635       GNUNET_CRYPTO_hash_context_read (coins_hctx,
    636                                        &bch,
    637                                        sizeof(bch));
    638     }
    639   }
    640   else
    641   { /* Age restricted case with required age-proof. */
    642 
    643     /**
    644      * We collect the run of all coin candidates for the same γ index
    645      * first, then γ+1 etc.
    646      */
    647     for (size_t k = 0; k < TALER_CNC_KAPPA; k++)
    648     {
    649       struct GNUNET_HashContext *batch_ctx;
    650       struct TALER_BlindedCoinHashP batch_h;
    651 
    652       batch_ctx = GNUNET_CRYPTO_hash_context_start ();
    653       FAIL_IF (NULL == batch_ctx);
    654 
    655       for (size_t i  = 0; i< pwbh->num_input; i++)
    656       {
    657         const struct TALER_PlanchetDetail *planchet =
    658           &pwbh->blinded.with_age_proof_input[i].planchet_details[k];
    659         json_t *jc = GNUNET_JSON_PACK (
    660           TALER_JSON_pack_blinded_planchet (
    661             NULL,
    662             &planchet->blinded_planchet));
    663 
    664         FAIL_IF (NULL == jc);
    665         FAIL_IF (0 > json_array_append_new (
    666                    j_planchets,
    667                    jc));
    668 
    669         TALER_coin_ev_hash (
    670           &planchet->blinded_planchet,
    671           &planchet->denom_pub_hash,
    672           &bch);
    673 
    674         GNUNET_CRYPTO_hash_context_read (
    675           batch_ctx,
    676           &bch,
    677           sizeof(bch));
    678       }
    679 
    680       GNUNET_CRYPTO_hash_context_finish (
    681         batch_ctx,
    682         &batch_h.hash);
    683       GNUNET_CRYPTO_hash_context_read (
    684         coins_hctx,
    685         &batch_h,
    686         sizeof(batch_h));
    687     }
    688   }
    689 
    690   GNUNET_CRYPTO_hash_context_finish (
    691     coins_hctx,
    692     &pwbh->planchets_h.hash);
    693   coins_hctx = NULL;
    694 
    695   TALER_wallet_withdraw_sign (
    696     &pwbh->amount,
    697     &pwbh->fee,
    698     &pwbh->planchets_h,
    699     pwbh->blinding_seed,
    700     pwbh->with_age_proof ? &pwbh->age_mask: NULL,
    701     pwbh->with_age_proof ? pwbh->max_age : 0,
    702     pwbh->reserve_priv,
    703     &pwbh->reserve_sig);
    704 
    705   /* Initiate the POST-request */
    706   j_request_body = GNUNET_JSON_PACK (
    707     GNUNET_JSON_pack_string ("cipher",
    708                              "ED25519"),
    709     GNUNET_JSON_pack_data_auto ("reserve_pub",
    710                                 &pwbh->reserve_pub),
    711     GNUNET_JSON_pack_array_steal ("denoms_h",
    712                                   j_denoms),
    713     GNUNET_JSON_pack_array_steal ("coin_evs",
    714                                   j_planchets),
    715     GNUNET_JSON_pack_allow_null (
    716       pwbh->with_age_proof
    717       ? GNUNET_JSON_pack_int64 ("max_age",
    718                                 pwbh->max_age)
    719       : GNUNET_JSON_pack_string ("max_age",
    720                                  NULL) ),
    721     GNUNET_JSON_pack_data_auto ("reserve_sig",
    722                                 &pwbh->reserve_sig));
    723   FAIL_IF (NULL == j_request_body);
    724 
    725   if (NULL != pwbh->blinding_seed)
    726   {
    727     json_t *j_seed = GNUNET_JSON_PACK (
    728       GNUNET_JSON_pack_data_auto ("blinding_seed",
    729                                   pwbh->blinding_seed));
    730     GNUNET_assert (NULL != j_seed);
    731     GNUNET_assert (0 ==
    732                    json_object_update_new (
    733                      j_request_body,
    734                      j_seed));
    735   }
    736 
    737   curlh = TALER_EXCHANGE_curl_easy_get_ (pwbh->request_url);
    738   FAIL_IF (NULL == curlh);
    739   FAIL_IF (GNUNET_OK !=
    740            TALER_curl_easy_post (
    741              &pwbh->post_ctx,
    742              curlh,
    743              j_request_body));
    744   json_decref (j_request_body);
    745   j_request_body = NULL;
    746 
    747   pwbh->job = GNUNET_CURL_job_add2 (
    748     pwbh->curl_ctx,
    749     curlh,
    750     pwbh->post_ctx.headers,
    751     &handle_withdraw_blinded_finished,
    752     pwbh);
    753   FAIL_IF (NULL == pwbh->job);
    754 
    755   return TALER_EC_NONE;
    756 
    757 ERROR:
    758   if (NULL != coins_hctx)
    759     GNUNET_CRYPTO_hash_context_abort (coins_hctx);
    760   if (NULL != j_denoms)
    761     json_decref (j_denoms);
    762   if (NULL != j_planchets)
    763     json_decref (j_planchets);
    764   if (NULL != j_request_body)
    765     json_decref (j_request_body);
    766   if (NULL != curlh)
    767     curl_easy_cleanup (curlh);
    768   return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
    769 #undef FAIL_IF
    770 }
    771 
    772 
    773 void
    774 TALER_EXCHANGE_post_withdraw_blinded_cancel (
    775   struct TALER_EXCHANGE_PostWithdrawBlindedHandle *pwbh)
    776 {
    777   if (NULL == pwbh)
    778     return;
    779   if (NULL != pwbh->job)
    780   {
    781     GNUNET_CURL_job_cancel (pwbh->job);
    782     pwbh->job = NULL;
    783   }
    784   GNUNET_free (pwbh->request_url);
    785   TALER_EXCHANGE_keys_decref (pwbh->keys);
    786   TALER_curl_easy_post_finished (&pwbh->post_ctx);
    787   GNUNET_free (pwbh);
    788 }
    789 
    790 
    791 /* exchange_api_post-withdraw_blinded.c */