exchange

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

taler-exchange-httpd_reveal-withdraw.c (21429B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2023,2025 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero 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 Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file taler-exchange-httpd_reveal-withdraw.c
     18  * @brief Handle /reveal-withdraw requests
     19  * @author Özgür Kesim
     20  */
     21 #include "taler/platform.h"
     22 #include <gnunet/gnunet_common.h>
     23 #include <gnunet/gnunet_util_lib.h>
     24 #include <jansson.h>
     25 #include <microhttpd.h>
     26 #include "taler-exchange-httpd_metrics.h"
     27 #include "taler/taler_error_codes.h"
     28 #include "taler/taler_exchangedb_plugin.h"
     29 #include "taler/taler_mhd_lib.h"
     30 #include "taler-exchange-httpd_mhd.h"
     31 #include "taler-exchange-httpd_reveal-withdraw.h"
     32 #include "taler-exchange-httpd_responses.h"
     33 #include "taler-exchange-httpd_keys.h"
     34 
     35 /**
     36  * State for an /reveal-withdraw operation.
     37  */
     38 struct WithdrawRevealContext
     39 {
     40 
     41   /**
     42    * Commitment for the withdraw operation, previously called by the
     43    * client.
     44    */
     45   struct TALER_HashBlindedPlanchetsP planchets_h;
     46 
     47   /**
     48    * TALER_CNC_KAPPA-1 secrets for disclosed coin batches.
     49    */
     50   struct TALER_RevealWithdrawMasterSeedsP disclosed_batch_seeds;
     51 
     52   /**
     53    * The data from the original withdraw.  Will be retrieved from
     54    * the DB via @a wch.
     55    */
     56   struct TALER_EXCHANGEDB_Withdraw withdraw;
     57 };
     58 
     59 
     60 /**
     61  * Parse the json body of an '/reveal-withdraw' request.  It extracts
     62  * the denomination hashes, blinded coins and disclosed coins and allocates
     63  * memory for those.
     64  *
     65  * @param connection The MHD connection to handle
     66  * @param j_disclosed_batch_seeds The n*(kappa-1) disclosed coins' private keys in JSON format, from which all other attributes (age restriction, blinding, nonce) will be derived from
     67  * @param[out] actx The context of the operation, only partially built at call time
     68  * @param[out] mhd_ret The result if a reply is queued for MHD
     69  * @return true on success, false on failure, with a reply already queued for MHD.
     70  */
     71 static enum GNUNET_GenericReturnValue
     72 parse_withdraw_reveal_json (
     73   struct MHD_Connection *connection,
     74   const json_t *j_disclosed_batch_seeds,
     75   struct WithdrawRevealContext *actx,
     76   MHD_RESULT *mhd_ret)
     77 {
     78   size_t num_entries;
     79   const char *error;
     80   struct GNUNET_JSON_Specification tuple[] = {
     81     GNUNET_JSON_spec_fixed (NULL,
     82                             &actx->disclosed_batch_seeds.tuple[0],
     83                             sizeof(actx->disclosed_batch_seeds.tuple[0])),
     84     GNUNET_JSON_spec_fixed (NULL,
     85                             &actx->disclosed_batch_seeds.tuple[1],
     86                             sizeof(actx->disclosed_batch_seeds.tuple[1])),
     87     GNUNET_JSON_spec_end ()
     88   };
     89   struct GNUNET_JSON_Specification spec[] = {
     90     TALER_JSON_spec_tuple_of (NULL,
     91                               tuple),
     92     GNUNET_JSON_spec_end ()
     93   };
     94 
     95   /**
     96    * Note that above, in tuple[], we have hard-wired
     97    * the size of TALER_CNC_KAPPA.
     98    * Let's make sure we keep this in sync.
     99    */
    100   _Static_assert ((TALER_CNC_KAPPA - 1) == 2);
    101 
    102   num_entries = json_array_size (j_disclosed_batch_seeds);   /* 0, if not an array */
    103   if (! json_is_array (j_disclosed_batch_seeds))
    104     error = "disclosed_batch_seeds must be an array";
    105   else if (num_entries == 0)
    106     error = "disclosed_batch_seeds must not be empty";
    107   else if (num_entries != TALER_CNC_KAPPA - 1)
    108     error =
    109       "disclosed_batch_seeds must be an array of size "
    110       TALER_CNC_KAPPA_MINUS_ONE_STR;
    111   else
    112     error = NULL;
    113 
    114   if ( (NULL != error) ||
    115        (GNUNET_OK !=
    116         GNUNET_JSON_parse (j_disclosed_batch_seeds,
    117                            spec,
    118                            &error,
    119                            NULL)) )
    120   {
    121     GNUNET_break_op (0);
    122     *mhd_ret = TALER_MHD_reply_with_ec (connection,
    123                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
    124                                         error);
    125     return GNUNET_SYSERR;
    126   }
    127 
    128   return GNUNET_OK;
    129 }
    130 
    131 
    132 /**
    133  * Check if the request belongs to an existing withdraw request.
    134  * If so, sets the withdraw object with the request data.
    135  * Otherwise, it queues an appropriate MHD response.
    136  *
    137  * @param connection The HTTP connection to the client
    138  * @param planchets_h Original commitment value sent with the withdraw request
    139  * @param[out] withdraw Data from the original withdraw request
    140  * @param[out] result In the error cases, a response will be queued with MHD and this will be the result.
    141  * @return #GNUNET_OK if the withdraw request has been found,
    142  *   #GNUNET_SYSERR if we did not find the request in the DB
    143  */
    144 static enum GNUNET_GenericReturnValue
    145 find_original_withdraw (
    146   struct MHD_Connection *connection,
    147   const struct TALER_HashBlindedPlanchetsP *planchets_h,
    148   struct TALER_EXCHANGEDB_Withdraw *withdraw,
    149   MHD_RESULT *result)
    150 {
    151   enum GNUNET_DB_QueryStatus qs;
    152 
    153   for (unsigned int try = 0; try < 3; try++)
    154   {
    155     qs = TEH_plugin->get_withdraw (TEH_plugin->cls,
    156                                    planchets_h,
    157                                    withdraw);
    158     switch (qs)
    159     {
    160     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    161       return GNUNET_OK; /* Only happy case */
    162     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    163       *result = TALER_MHD_reply_with_error (connection,
    164                                             MHD_HTTP_NOT_FOUND,
    165                                             TALER_EC_EXCHANGE_WITHDRAW_COMMITMENT_UNKNOWN,
    166                                             NULL);
    167       return GNUNET_SYSERR;
    168     case GNUNET_DB_STATUS_HARD_ERROR:
    169       *result = TALER_MHD_reply_with_ec (connection,
    170                                          TALER_EC_GENERIC_DB_FETCH_FAILED,
    171                                          "get_withdraw");
    172       return GNUNET_SYSERR;
    173     case GNUNET_DB_STATUS_SOFT_ERROR:
    174       break; /* try again */
    175     default:
    176       GNUNET_break (0);
    177       *result = TALER_MHD_reply_with_ec (connection,
    178                                          TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
    179                                          NULL);
    180       return GNUNET_SYSERR;
    181     }
    182   }
    183   /* after unsuccessful retries*/
    184   GNUNET_break (0);
    185   *result = TALER_MHD_reply_with_ec (connection,
    186                                      TALER_EC_GENERIC_DB_FETCH_FAILED,
    187                                      "get_withdraw");
    188   return GNUNET_SYSERR;
    189 }
    190 
    191 
    192 /**
    193  * @brief Derives an age-restricted planchet from a given secret and calculates the hash
    194  *
    195  * @param connection Connection to the client
    196  * @param denom_key The denomination key
    197  * @param secret The secret to a planchet
    198  * @param r_pub The public R-values from the exchange in case of a CS denomination; might be NULL
    199  * @param nonce The derived nonce needed for CS denomination
    200  * @param max_age The maximum age allowed
    201  * @param[out] bch Hashcode to write
    202  * @param[out] result On error, a HTTP-response will be queued and result set accordingly
    203  * @return #GNUNET_OK on success, #GNUNET_SYSERR otherwise, with an error message
    204  * written to the client and @e result set.
    205  */
    206 static enum GNUNET_GenericReturnValue
    207 calculate_blinded_hash (
    208   struct MHD_Connection *connection,
    209   struct TEH_DenominationKey *denom_key,
    210   const struct TALER_PlanchetMasterSecretP *secret,
    211   const struct GNUNET_CRYPTO_CSPublicRPairP *r_pub,
    212   union GNUNET_CRYPTO_BlindSessionNonce *nonce,
    213   uint8_t max_age,
    214   struct TALER_BlindedCoinHashP *bch,
    215   MHD_RESULT *result)
    216 {
    217   enum GNUNET_GenericReturnValue ret;
    218   struct TALER_AgeCommitmentHashP ach;
    219 
    220   /* calculate age commitment hash */
    221   {
    222     struct TALER_AgeCommitmentProof acp;
    223 
    224     TALER_age_restriction_from_secret (secret,
    225                                        &denom_key->denom_pub.age_mask,
    226                                        max_age,
    227                                        &acp);
    228     TALER_age_commitment_hash (&acp.commitment,
    229                                &ach);
    230     TALER_age_commitment_proof_free (&acp);
    231   }
    232 
    233   /* Next: calculate planchet */
    234   {
    235     struct TALER_CoinPubHashP c_hash;
    236     struct TALER_PlanchetDetail detail = {0};
    237     struct TALER_CoinSpendPrivateKeyP coin_priv;
    238     union GNUNET_CRYPTO_BlindingSecretP bks;
    239     struct GNUNET_CRYPTO_BlindingInputValues bi = {
    240       .cipher = denom_key->denom_pub.bsign_pub_key->cipher
    241     };
    242     struct TALER_ExchangeBlindingValues alg_values = {
    243       .blinding_inputs = &bi
    244     };
    245 
    246     if (GNUNET_CRYPTO_BSA_CS == bi.cipher)
    247     {
    248       GNUNET_assert (NULL != r_pub);
    249       GNUNET_assert (NULL != nonce);
    250       bi.details.cs_values = *r_pub;
    251     }
    252     TALER_planchet_blinding_secret_create (secret,
    253                                            &alg_values,
    254                                            &bks);
    255     TALER_planchet_setup_coin_priv (secret,
    256                                     &alg_values,
    257                                     &coin_priv);
    258     ret = TALER_planchet_prepare (&denom_key->denom_pub,
    259                                   &alg_values,
    260                                   &bks,
    261                                   nonce,
    262                                   &coin_priv,
    263                                   &ach,
    264                                   &c_hash,
    265                                   &detail);
    266     if (GNUNET_OK != ret)
    267     {
    268       GNUNET_break (0);
    269       *result = TALER_MHD_REPLY_JSON_PACK (connection,
    270                                            MHD_HTTP_INTERNAL_SERVER_ERROR,
    271                                            GNUNET_JSON_pack_string (
    272                                              "details",
    273                                              "failed to prepare planchet from base key"));
    274       return ret;
    275     }
    276 
    277     TALER_coin_ev_hash (&detail.blinded_planchet,
    278                         &denom_key->h_denom_pub,
    279                         bch);
    280     TALER_blinded_planchet_free (&detail.blinded_planchet);
    281   }
    282 
    283   return ret;
    284 }
    285 
    286 
    287 /**
    288  * @brief Checks the validity of the disclosed coins as follows:
    289  * - Derives and calculates the disclosed coins'
    290  *    - public keys,
    291  *    - nonces (if applicable),
    292  *    - age commitments,
    293  *    - blindings
    294  *    - blinded hashes
    295  * - Computes planchets_h with those calculated and the undisclosed hashes
    296  * - Compares planchets_h with the value from the original commitment
    297  * - Verifies that all public keys in indices larger than the age group
    298  *   corresponding to max_age are derived from the constant public key.
    299  *
    300  * The derivation of the blindings, (potential) nonces and age-commitment from
    301  * a coin's private keys is defined in
    302  * https://docs.taler.net/design-documents/024-age-restriction.html#withdraw
    303  *
    304  * @param con HTTP-connection to the client
    305  * @param wd Original withdraw request
    306  * @param disclosed_batch_seeds The secrets of the disclosed coins, (TALER_CNC_KAPPA - 1)*num_coins many
    307  * @param[out] result On error, a HTTP-response will be queued and result set accordingly
    308  * @return #GNUNET_OK on success, #GNUNET_SYSERR otherwise
    309  */
    310 static enum GNUNET_GenericReturnValue
    311 verify_commitment_and_max_age (
    312   struct MHD_Connection *con,
    313   const struct TALER_EXCHANGEDB_Withdraw *wd,
    314   const struct TALER_RevealWithdrawMasterSeedsP *disclosed_batch_seeds,
    315   MHD_RESULT *result)
    316 {
    317   enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
    318   struct GNUNET_HashContext *hash_context;
    319   struct TEH_KeyStateHandle *keys;
    320   struct TEH_DenominationKey *denom_keys[wd->num_coins];
    321   struct TALER_Amount total_amount;
    322   struct TALER_Amount total_fee;
    323   struct TALER_AgeMask mask;
    324   struct TALER_PlanchetMasterSecretP secrets[
    325     TALER_CNC_KAPPA - 1][wd->num_coins];
    326   bool is_cs[wd->num_coins];
    327   size_t cs_count = 0;
    328   uint8_t secrets_idx = 0;   /* first index into secrets */
    329 
    330   GNUNET_assert (wd->noreveal_index < TALER_CNC_KAPPA);
    331 
    332   GNUNET_assert (GNUNET_OK ==
    333                  TALER_amount_set_zero (TEH_currency,
    334                                         &total_amount));
    335   GNUNET_assert (GNUNET_OK ==
    336                  TALER_amount_set_zero (TEH_currency,
    337                                         &total_fee));
    338 
    339   memset (denom_keys,
    340           0,
    341           sizeof(denom_keys));
    342   memset (is_cs,
    343           0,
    344           sizeof(is_cs));
    345 
    346   /* We need the current keys in memory for the meta-data of the denominations */
    347   keys = TEH_keys_get_state ();
    348   if (NULL == keys)
    349   {
    350     *result = TALER_MHD_reply_with_ec (con,
    351                                        TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
    352                                        NULL);
    353     return GNUNET_SYSERR;
    354   }
    355 
    356   /* Find the denomination keys */
    357   for (size_t i = 0; i < wd->num_coins; i++)
    358   {
    359     denom_keys[i] =
    360       TEH_keys_denomination_by_serial_from_state (
    361         keys,
    362         wd->denom_serials[i]);
    363     if (NULL == denom_keys[i])
    364     {
    365       GNUNET_break_op (0);
    366       *result = TALER_MHD_reply_with_ec (con,
    367                                          TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
    368                                          NULL);
    369       return GNUNET_SYSERR;
    370     }
    371 
    372     /* Accumulate amount and fees */
    373     GNUNET_assert (0 <= TALER_amount_add (&total_amount,
    374                                           &total_amount,
    375                                           &denom_keys[i]->meta.value));
    376     GNUNET_assert (0 <= TALER_amount_add (&total_fee,
    377                                           &total_fee,
    378                                           &denom_keys[i]->meta.fees.withdraw));
    379 
    380     if (i == 0)
    381       mask = denom_keys[i]->meta.age_mask;
    382     GNUNET_assert (mask.bits == denom_keys[i]->meta.age_mask.bits);
    383 
    384     if (GNUNET_CRYPTO_BSA_CS ==
    385         denom_keys[i]->denom_pub.bsign_pub_key->cipher)
    386     {
    387       is_cs[i] = true;
    388       cs_count++;
    389     }
    390   }
    391 
    392   hash_context = GNUNET_CRYPTO_hash_context_start ();
    393 
    394   {
    395     uint32_t cs_indices[cs_count];
    396     union GNUNET_CRYPTO_BlindSessionNonce nonces[cs_count];
    397     size_t cs_idx = 0; /* [0...cs_count) */
    398 
    399     for (size_t i = 0; i < wd->num_coins; i++)
    400       if (is_cs[i])
    401         cs_indices[cs_idx++] = i;
    402 
    403     TALER_cs_derive_only_cs_blind_nonces_from_seed (&wd->blinding_seed,
    404                                                     false, /* not for melt */
    405                                                     cs_count,
    406                                                     cs_indices,
    407                                                     nonces);
    408 
    409     for (uint8_t gamma = 0; gamma<TALER_CNC_KAPPA; gamma++)
    410     {
    411       if (gamma == wd->noreveal_index)
    412       {
    413         /**
    414          * For the disclosed index, all we have to do is to accumulate the hash
    415          * of the selected coins
    416          */
    417         GNUNET_CRYPTO_hash_context_read (
    418           hash_context,
    419           &wd->selected_h,
    420           sizeof(wd->selected_h));
    421       }
    422       else
    423       {
    424         /**
    425          * For the non-disclosed index, we have to generate the planchets in
    426          * this batch and calculate their hash
    427          */
    428         struct GNUNET_HashContext *batch_ctx;
    429         struct TALER_BlindedCoinHashP batch_h;
    430         cs_idx = 0;
    431 
    432         batch_ctx = GNUNET_CRYPTO_hash_context_start ();
    433         GNUNET_assert (NULL != batch_ctx);
    434 
    435         /* Expand the secrets for a disclosed batch */
    436         GNUNET_assert (secrets_idx < (TALER_CNC_KAPPA - 1));
    437         TALER_withdraw_expand_secrets (
    438           wd->num_coins,
    439           &disclosed_batch_seeds->tuple[secrets_idx],
    440           secrets[secrets_idx]);
    441 
    442         /**
    443          * Now individually create each coin in this batch and calculate
    444          * its hash, and accumulate the hash of the batch with it
    445          */
    446         for (size_t coin_idx = 0; coin_idx < wd->num_coins; coin_idx++)
    447         {
    448           struct TALER_BlindedCoinHashP bch;
    449           struct GNUNET_CRYPTO_CSPublicRPairP *rp;
    450           union GNUNET_CRYPTO_BlindSessionNonce *np;
    451 
    452           if (is_cs[coin_idx])
    453           {
    454             GNUNET_assert (cs_idx < cs_count);
    455             np = &nonces[cs_idx];
    456             rp = &wd->cs_r_values[cs_idx];
    457             cs_idx++;
    458           }
    459           else
    460           {
    461             np = NULL;
    462             rp = NULL;
    463           }
    464           ret = calculate_blinded_hash (con,
    465                                         denom_keys[coin_idx],
    466                                         &secrets[secrets_idx][coin_idx],
    467                                         rp,
    468                                         np,
    469                                         wd->max_age,
    470                                         &bch,
    471                                         result);
    472           if (GNUNET_OK != ret)
    473           {
    474             GNUNET_CRYPTO_hash_context_abort (hash_context);
    475             return GNUNET_SYSERR;
    476           }
    477           /**
    478            * Continue the running hash of all coin hashes in the batch
    479            * with the calculated hash-value of the current, disclosed coin
    480            */
    481           GNUNET_CRYPTO_hash_context_read (batch_ctx,
    482                                            &bch,
    483                                            sizeof(bch));
    484         }
    485         /**
    486          * Finalize the hash of this batch and add it to
    487          * the total hash
    488          */
    489         GNUNET_CRYPTO_hash_context_finish (
    490           batch_ctx,
    491           &batch_h.hash);
    492         GNUNET_CRYPTO_hash_context_read (
    493           hash_context,
    494           &batch_h,
    495           sizeof(batch_h));
    496 
    497         secrets_idx++;
    498       }
    499     }
    500   }
    501 
    502   /* Finally, compare the calculated hash with the original wd */
    503   {
    504     struct TALER_HashBlindedPlanchetsP planchets_h;
    505 
    506     GNUNET_CRYPTO_hash_context_finish (
    507       hash_context,
    508       &planchets_h.hash);
    509 
    510     if (0 != GNUNET_CRYPTO_hash_cmp (
    511           &wd->planchets_h.hash,
    512           &planchets_h.hash))
    513     {
    514       GNUNET_break_op (0);
    515       *result = TALER_MHD_reply_with_ec (con,
    516                                          TALER_EC_EXCHANGE_WITHDRAW_REVEAL_INVALID_HASH,
    517                                          NULL);
    518       return GNUNET_SYSERR;
    519     }
    520 
    521   }
    522   return GNUNET_OK;
    523 }
    524 
    525 
    526 /**
    527  * @brief Send a response for "/reveal-withdraw"
    528  *
    529  * @param connection The http connection to the client to send the response to
    530  * @param commitment The data from the commitment with signatures
    531  * @return a MHD result code
    532  */
    533 static MHD_RESULT
    534 reply_withdraw_reveal_success (
    535   struct MHD_Connection *connection,
    536   const struct TALER_EXCHANGEDB_Withdraw *commitment)
    537 {
    538   json_t *list = json_array ();
    539 
    540   GNUNET_assert (NULL != list);
    541   for (size_t i = 0; i < commitment->num_coins; i++)
    542   {
    543     json_t *obj = GNUNET_JSON_PACK (
    544       TALER_JSON_pack_blinded_denom_sig (NULL,
    545                                          &commitment->denom_sigs[i]));
    546     GNUNET_assert (0 ==
    547                    json_array_append_new (list,
    548                                           obj));
    549   }
    550 
    551   return TALER_MHD_REPLY_JSON_PACK (
    552     connection,
    553     MHD_HTTP_OK,
    554     GNUNET_JSON_pack_array_steal ("ev_sigs",
    555                                   list));
    556 }
    557 
    558 
    559 MHD_RESULT
    560 TEH_handler_reveal_withdraw (
    561   struct TEH_RequestContext *rc,
    562   const json_t *root,
    563   const char *const args[0])
    564 {
    565   MHD_RESULT result = MHD_NO;
    566   enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
    567   struct WithdrawRevealContext actx = {0};
    568   const json_t *j_disclosed_batch_seeds;
    569   struct GNUNET_JSON_Specification spec[] = {
    570     GNUNET_JSON_spec_fixed_auto ("planchets_h",
    571                                  &actx.planchets_h),
    572     GNUNET_JSON_spec_array_const ("disclosed_batch_seeds",
    573                                   &j_disclosed_batch_seeds),
    574     GNUNET_JSON_spec_end ()
    575   };
    576 
    577   (void) args;
    578   /* Parse JSON body*/
    579   ret = TALER_MHD_parse_json_data (rc->connection,
    580                                    root,
    581                                    spec);
    582   if (GNUNET_OK != ret)
    583   {
    584     GNUNET_break_op (0);
    585     return (GNUNET_SYSERR == ret) ? MHD_NO : MHD_YES;
    586   }
    587 
    588   do {
    589     /* Extract denominations, blinded and disclosed coins */
    590     if (GNUNET_OK !=
    591         parse_withdraw_reveal_json (
    592           rc->connection,
    593           j_disclosed_batch_seeds,
    594           &actx,
    595           &result))
    596       break;
    597 
    598     /* Find original commitment */
    599     if (GNUNET_OK !=
    600         find_original_withdraw (
    601           rc->connection,
    602           &actx.planchets_h,
    603           &actx.withdraw,
    604           &result))
    605       break;
    606 
    607     /* Verify the computed planchets_h equals the committed one and that coins
    608      * have a maximum age group corresponding max_age (age-mask dependent) */
    609     if (GNUNET_OK !=
    610         verify_commitment_and_max_age (
    611           rc->connection,
    612           &actx.withdraw,
    613           &actx.disclosed_batch_seeds,
    614           &result))
    615       break;
    616 
    617     /* Finally, return the signatures */
    618     result = reply_withdraw_reveal_success (rc->connection,
    619                                             &actx.withdraw);
    620 
    621   } while (0);
    622 
    623   GNUNET_JSON_parse_free (spec);
    624   if (NULL != actx.withdraw.denom_sigs)
    625   {
    626     for (size_t i = 0; i<actx.withdraw.num_coins; i++)
    627       TALER_blinded_denom_sig_free (&actx.withdraw.denom_sigs[i]);
    628     GNUNET_free (actx.withdraw.denom_sigs);
    629   }
    630   GNUNET_free (actx.withdraw.cs_r_values);
    631   GNUNET_free (actx.withdraw.denom_pub_hashes);
    632   GNUNET_free (actx.withdraw.denom_serials);
    633   return result;
    634 }
    635 
    636 
    637 /* end of taler-exchange-httpd_reveal_withdraw.c */