exchange

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

testing_api_cmd_recoup.c (17247B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2018-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 <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file testing/testing_api_cmd_recoup.c
     18  * @brief Implement the /recoup-withdraw test command.
     19  * @author Marcello Stanisci
     20  * @author Christian Grothoff
     21  * @author Özgür Kesim
     22  */
     23 #include "platform.h"
     24 #include "taler/taler_json_lib.h"
     25 #include <gnunet/gnunet_curl_lib.h>
     26 #include "taler/taler_testing_lib.h"
     27 
     28 
     29 /**
     30  * State for a "recoup-withdraw" CMD.
     31  */
     32 struct RecoupState
     33 {
     34   /**
     35    * Expected HTTP status code.
     36    */
     37   unsigned int expected_response_code;
     38 
     39   /**
     40    * Label of the withdraw command the coins came from.
     41    */
     42   char *withdraw_reference;
     43 
     44   /**
     45    * Indices (in the withdraw command) of the coins to recoup.
     46    */
     47   unsigned int *indices;
     48 
     49   /**
     50    * Number of entries in @e indices, @e ches, @e rhes and @e coin_pubs.
     51    */
     52   size_t num_indices;
     53 
     54   /**
     55    * Amount we expect to be recouped per coin, invalid if
     56    * not specified.
     57    */
     58   struct TALER_Amount amount;
     59 
     60   /**
     61    * Interpreter state.
     62    */
     63   struct TALER_TESTING_Interpreter *is;
     64 
     65   /**
     66    * Handle to the request.
     67    */
     68   struct TALER_EXCHANGE_PostRecoupWithdrawHandle *ph;
     69 
     70   /**
     71    * Reserve that is credited.
     72    */
     73   struct TALER_ReservePublicKeyP reserve_pub;
     74 
     75   /**
     76    * Coin history entries we expect, one per recouped coin.
     77    */
     78   struct TALER_EXCHANGE_CoinHistoryEntry *ches;
     79 
     80   /**
     81    * Reserve history entries we expect, one per recouped coin.
     82    */
     83   struct TALER_EXCHANGE_ReserveHistoryEntry *rhes;
     84 
     85   /**
     86    * Public keys of the recouped coins.
     87    */
     88   struct TALER_CoinSpendPublicKeyP *coin_pubs;
     89 };
     90 
     91 
     92 /**
     93  * Check the response.
     94  *
     95  * @param cls closure, our `struct RecoupState`
     96  * @param rr response details
     97  */
     98 static void
     99 recoup_cb (void *cls,
    100            const struct TALER_EXCHANGE_PostRecoupWithdrawResponse *rr)
    101 {
    102   struct RecoupState *ps = cls;
    103   const struct TALER_EXCHANGE_HttpResponse *hr = &rr->hr;
    104   struct TALER_TESTING_Interpreter *is = ps->is;
    105 
    106   ps->ph = NULL;
    107   if (ps->expected_response_code != hr->http_status)
    108   {
    109     TALER_TESTING_unexpected_status (is,
    110                                      hr->http_status,
    111                                      ps->expected_response_code);
    112     return;
    113   }
    114   switch (hr->http_status)
    115   {
    116   case MHD_HTTP_OK:
    117     if (0 != GNUNET_memcmp (&rr->details.ok.reserve_pub,
    118                             &ps->reserve_pub))
    119     {
    120       GNUNET_break (0);
    121       TALER_TESTING_interpreter_fail (is);
    122       return;
    123     }
    124     if (rr->details.ok.num_recoups != ps->num_indices)
    125     {
    126       GNUNET_break (0);
    127       TALER_TESTING_interpreter_fail (is);
    128       return;
    129     }
    130     for (size_t i = 0; i < ps->num_indices; i++)
    131     {
    132       const struct TALER_RecoupedCoin *rc = &rr->details.ok.recoups[i];
    133 
    134       if (0 != GNUNET_memcmp (&rc->coin_pub,
    135                               &ps->coin_pubs[i]))
    136       {
    137         GNUNET_break (0);
    138         TALER_TESTING_interpreter_fail (is);
    139         return;
    140       }
    141       if ( (GNUNET_OK ==
    142             TALER_amount_is_valid (&ps->amount)) &&
    143            (0 != TALER_amount_cmp (&rc->amount,
    144                                    &ps->amount)) )
    145       {
    146         GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    147                     "Recouped %s for coin %u, expected %s\n",
    148                     TALER_amount2s (&rc->amount),
    149                     ps->indices[i],
    150                     TALER_amount2s (&ps->amount));
    151         GNUNET_break (0);
    152         TALER_TESTING_interpreter_fail (is);
    153         return;
    154       }
    155       ps->ches[i].amount = rc->amount;
    156       ps->rhes[i].amount = rc->amount;
    157       ps->rhes[i].type = TALER_EXCHANGE_RTT_RECOUP;
    158     }
    159     break;
    160   default:
    161     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    162                 "Recoup-withdraw failed as expected with HTTP status %u/%d\n",
    163                 hr->http_status,
    164                 (int) hr->ec);
    165     break;
    166   }
    167   TALER_TESTING_interpreter_next (is);
    168 }
    169 
    170 
    171 /**
    172  * Run the command.
    173  *
    174  * @param cls closure.
    175  * @param cmd the command to execute.
    176  * @param is the interpreter state.
    177  */
    178 static void
    179 recoup_run (void *cls,
    180             const struct TALER_TESTING_Command *cmd,
    181             struct TALER_TESTING_Interpreter *is)
    182 {
    183   struct RecoupState *ps = cls;
    184   const struct TALER_TESTING_Command *coin_cmd;
    185   const struct TALER_ReservePublicKeyP *reserve_pub;
    186   const struct TALER_WithdrawMasterSeedP *seed;
    187   const struct TALER_HashBlindedPlanchetsP *h_planchets;
    188   struct TALER_BlindingMasterSeedP blinding_seed;
    189   bool have_cs = false;
    190   size_t num_coins = 0;
    191 
    192   (void) cmd;
    193   ps->is = is;
    194   coin_cmd = TALER_TESTING_interpreter_lookup_command (is,
    195                                                        ps->withdraw_reference);
    196   if (NULL == coin_cmd)
    197   {
    198     GNUNET_break (0);
    199     TALER_TESTING_interpreter_fail (is);
    200     return;
    201   }
    202   if ( (GNUNET_OK !=
    203         TALER_TESTING_get_trait_reserve_pub (coin_cmd,
    204                                              &reserve_pub)) ||
    205        (GNUNET_OK !=
    206         TALER_TESTING_get_trait_withdraw_seed (coin_cmd,
    207                                                &seed)) ||
    208        (GNUNET_OK !=
    209         TALER_TESTING_get_trait_withdraw_commitment (coin_cmd,
    210                                                      &h_planchets)) )
    211   {
    212     GNUNET_break (0);
    213     TALER_TESTING_interpreter_fail (is);
    214     return;
    215   }
    216   ps->reserve_pub = *reserve_pub;
    217   /* count the coins of the withdraw operation */
    218   while (num_coins < TALER_MAX_COINS)
    219   {
    220     const struct TALER_EXCHANGE_DenomPublicKey *pk;
    221 
    222     if (GNUNET_OK !=
    223         TALER_TESTING_get_trait_denom_pub (coin_cmd,
    224                                            (unsigned int) num_coins,
    225                                            &pk))
    226       break;
    227     num_coins++;
    228   }
    229   if (0 == num_coins)
    230   {
    231     GNUNET_break (0);
    232     TALER_TESTING_interpreter_fail (is);
    233     return;
    234   }
    235   for (size_t i = 0; i < ps->num_indices; i++)
    236     if (ps->indices[i] >= num_coins)
    237     {
    238       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    239                   "Coin index %u out of range for command %s (%u coins)\n",
    240                   ps->indices[i],
    241                   ps->withdraw_reference,
    242                   (unsigned int) num_coins);
    243       GNUNET_break (0);
    244       TALER_TESTING_interpreter_fail (is);
    245       return;
    246     }
    247   /* the CS blinding seed is derived from the withdraw seed */
    248   TALER_cs_withdraw_seed_to_blinding_seed (seed,
    249                                            &blinding_seed);
    250   {
    251     struct TALER_PlanchetMasterSecretP secrets[num_coins];
    252     struct TALER_EXCHANGE_RecoupCoin coins[num_coins];
    253     size_t k = 0;
    254 
    255     TALER_withdraw_expand_secrets (num_coins,
    256                                    seed,
    257                                    secrets);
    258     memset (coins,
    259             0,
    260             sizeof (coins));
    261     for (size_t i = 0; i < num_coins; i++)
    262     {
    263       struct TALER_EXCHANGE_RecoupCoin *c = &coins[i];
    264       const struct TALER_AgeCommitmentHashP *hac = NULL;
    265       bool recoup = false;
    266 
    267       if ( (GNUNET_OK !=
    268             TALER_TESTING_get_trait_denom_pub (coin_cmd,
    269                                                (unsigned int) i,
    270                                                &c->pk)) ||
    271            (GNUNET_OK !=
    272             TALER_TESTING_get_trait_exchange_blinding_values (
    273               coin_cmd,
    274               (unsigned int) i,
    275               &c->exchange_vals)) )
    276       {
    277         GNUNET_break (0);
    278         TALER_TESTING_interpreter_fail (is);
    279         return;
    280       }
    281       /* only denominations with age restriction have a commitment */
    282       if ( (0 != c->pk->key.age_mask.bits) &&
    283            (GNUNET_OK ==
    284             TALER_TESTING_get_trait_h_age_commitment (coin_cmd,
    285                                                       (unsigned int) i,
    286                                                       &hac)) )
    287         c->h_age_commitment = hac; /* may still be NULL */
    288       /* commands with per-coin secrets (batch and age withdraw) expose
    289          them; the single withdraw derives them from the seed */
    290       if (GNUNET_OK !=
    291           TALER_TESTING_get_trait_planchet_secrets (coin_cmd,
    292                                                     (unsigned int) i,
    293                                                     &c->ps))
    294         c->ps = &secrets[i];
    295       if (GNUNET_CRYPTO_BSA_CS == c->pk->key.bsign_pub_key->cipher)
    296         have_cs = true;
    297       for (size_t j = 0; j < ps->num_indices; j++)
    298         if (ps->indices[j] == i)
    299           recoup = true;
    300       if (! recoup)
    301         continue;
    302       if (GNUNET_OK !=
    303           TALER_TESTING_get_trait_denom_sig (coin_cmd,
    304                                              (unsigned int) i,
    305                                              &c->denom_sig))
    306       {
    307         GNUNET_break (0);
    308         TALER_TESTING_interpreter_fail (is);
    309         return;
    310       }
    311       /* what the histories should show for this coin */
    312       {
    313         struct TALER_EXCHANGE_CoinHistoryEntry *che = &ps->ches[k];
    314         struct TALER_EXCHANGE_ReserveHistoryEntry *rhe = &ps->rhes[k];
    315         struct TALER_CoinSpendPrivateKeyP coin_priv;
    316         const struct TALER_ExchangeBlindingValues *vals = c->exchange_vals;
    317 
    318         GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    319                     "Trying to recoup coin %u of denomination '%s'\n",
    320                     (unsigned int) i,
    321                     TALER_B2S (&c->pk->h_key));
    322         TALER_planchet_setup_coin_priv (c->ps,
    323                                         vals,
    324                                         &coin_priv);
    325         GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv.eddsa_priv,
    326                                             &ps->coin_pubs[k].eddsa_pub);
    327         che->type = TALER_EXCHANGE_CTT_RECOUP;
    328         che->amount = ps->amount;
    329         che->details.recoup.reserve_pub = ps->reserve_pub;
    330         che->details.recoup.planchets_h = *h_planchets;
    331         che->details.recoup.coin_index = (uint32_t) i;
    332         TALER_planchet_blinding_secret_create (c->ps,
    333                                                vals,
    334                                                &che->details.recoup.coin_bks);
    335         /* exercise the documented contract: the request needs no
    336            exchange values for RSA denominations */
    337         if (GNUNET_CRYPTO_BSA_RSA == c->pk->key.bsign_pub_key->cipher)
    338           c->exchange_vals = NULL;
    339         TALER_wallet_recoup_sign (&c->pk->h_key,
    340                                   &che->details.recoup.coin_bks,
    341                                   &coin_priv,
    342                                   &che->details.recoup.coin_sig);
    343         rhe->amount = ps->amount;
    344         rhe->details.recoup_details.coin_pub = ps->coin_pubs[k];
    345         rhe->details.recoup_details.planchets_h = *h_planchets;
    346         k++;
    347       }
    348     }
    349     ps->ph = TALER_EXCHANGE_post_recoup_withdraw_create (
    350       TALER_TESTING_interpreter_get_context (is),
    351       TALER_TESTING_get_exchange_url (is),
    352       TALER_TESTING_get_keys (is),
    353       &ps->reserve_pub,
    354       h_planchets,
    355       have_cs ? &blinding_seed : NULL,
    356       num_coins,
    357       coins);
    358   }
    359   if (NULL == ps->ph)
    360   {
    361     GNUNET_break (0);
    362     TALER_TESTING_interpreter_fail (is);
    363     return;
    364   }
    365   GNUNET_assert (TALER_EC_NONE ==
    366                  TALER_EXCHANGE_post_recoup_withdraw_start (ps->ph,
    367                                                             &recoup_cb,
    368                                                             ps));
    369 }
    370 
    371 
    372 /**
    373  * Cleanup the state.
    374  *
    375  * @param cls closure, must be a `struct RecoupState`.
    376  * @param cmd the command which is being cleaned up.
    377  */
    378 static void
    379 recoup_cleanup (void *cls,
    380                 const struct TALER_TESTING_Command *cmd)
    381 {
    382   struct RecoupState *ps = cls;
    383 
    384   (void) cmd;
    385   if (NULL != ps->ph)
    386   {
    387     TALER_EXCHANGE_post_recoup_withdraw_cancel (ps->ph);
    388     ps->ph = NULL;
    389   }
    390   GNUNET_free (ps->withdraw_reference);
    391   GNUNET_free (ps->indices);
    392   GNUNET_free (ps->ches);
    393   GNUNET_free (ps->rhes);
    394   GNUNET_free (ps->coin_pubs);
    395   GNUNET_free (ps);
    396 }
    397 
    398 
    399 /**
    400  * Offer internal data to other commands: one coin history entry
    401  * and one reserve history entry per recouped coin.
    402  *
    403  * @param cls closure
    404  * @param[out] ret result (could be anything)
    405  * @param trait name of the trait
    406  * @param index index number of the object to extract.
    407  * @return #GNUNET_OK on success
    408  */
    409 static enum GNUNET_GenericReturnValue
    410 recoup_traits (void *cls,
    411                const void **ret,
    412                const char *trait,
    413                unsigned int index)
    414 {
    415   struct RecoupState *ps = cls;
    416 
    417   if (index >= ps->num_indices)
    418     return GNUNET_NO;
    419   if (TALER_EXCHANGE_RTT_RECOUP != ps->rhes[index].type)
    420     return GNUNET_SYSERR; /* request did not succeed: no traits */
    421   {
    422     struct TALER_TESTING_Trait traits[] = {
    423       TALER_TESTING_make_trait_reserve_pub (&ps->reserve_pub),
    424       TALER_TESTING_make_trait_reserve_history (index,
    425                                                 &ps->rhes[index]),
    426       TALER_TESTING_make_trait_coin_history (index,
    427                                              &ps->ches[index]),
    428       TALER_TESTING_make_trait_coin_pub (index,
    429                                          &ps->coin_pubs[index]),
    430       TALER_TESTING_trait_end ()
    431     };
    432 
    433     return TALER_TESTING_get_trait (traits,
    434                                     ret,
    435                                     trait,
    436                                     index);
    437   }
    438 }
    439 
    440 
    441 /**
    442  * Create the command state shared by both constructors.
    443  *
    444  * @param label the command label
    445  * @param expected_response_code expected HTTP status code
    446  * @param withdraw_reference label of the withdraw command
    447  * @param indices coins to recoup, becomes owned by the state
    448  * @param num_indices length of @a indices
    449  * @param amount expected amount per coin, NULL for none
    450  * @return the command
    451  */
    452 static struct TALER_TESTING_Command
    453 make_recoup_cmd (const char *label,
    454                  unsigned int expected_response_code,
    455                  const char *withdraw_reference,
    456                  unsigned int *indices,
    457                  size_t num_indices,
    458                  const char *amount)
    459 {
    460   struct RecoupState *ps;
    461 
    462   ps = GNUNET_new (struct RecoupState);
    463   ps->expected_response_code = expected_response_code;
    464   ps->withdraw_reference = GNUNET_strdup (withdraw_reference);
    465   ps->indices = indices;
    466   ps->num_indices = num_indices;
    467   ps->ches = GNUNET_new_array (num_indices,
    468                                struct TALER_EXCHANGE_CoinHistoryEntry);
    469   ps->rhes = GNUNET_new_array (num_indices,
    470                                struct TALER_EXCHANGE_ReserveHistoryEntry);
    471   ps->coin_pubs = GNUNET_new_array (num_indices,
    472                                     struct TALER_CoinSpendPublicKeyP);
    473   if ( (NULL != amount) &&
    474        (GNUNET_OK !=
    475         TALER_string_to_amount (amount,
    476                                 &ps->amount)) )
    477   {
    478     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    479                 "Failed to parse amount `%s' at %s\n",
    480                 amount,
    481                 label);
    482     GNUNET_assert (0);
    483   }
    484   {
    485     struct TALER_TESTING_Command cmd = {
    486       .cls = ps,
    487       .label = label,
    488       .run = &recoup_run,
    489       .cleanup = &recoup_cleanup,
    490       .traits = &recoup_traits
    491     };
    492 
    493     return cmd;
    494   }
    495 }
    496 
    497 
    498 struct TALER_TESTING_Command
    499 TALER_TESTING_cmd_recoup (const char *label,
    500                           unsigned int expected_response_code,
    501                           const char *coin_reference,
    502                           const char *amount)
    503 {
    504   char *cref;
    505   unsigned int idx;
    506   unsigned int *indices;
    507   struct TALER_TESTING_Command cmd;
    508 
    509   GNUNET_assert (GNUNET_OK ==
    510                  TALER_TESTING_parse_coin_reference (coin_reference,
    511                                                      &cref,
    512                                                      &idx));
    513   indices = GNUNET_new_array (1,
    514                               unsigned int);
    515   indices[0] = idx;
    516   cmd = make_recoup_cmd (label,
    517                          expected_response_code,
    518                          cref,
    519                          indices,
    520                          1,
    521                          amount);
    522   GNUNET_free (cref);
    523   return cmd;
    524 }
    525 
    526 
    527 struct TALER_TESTING_Command
    528 TALER_TESTING_cmd_recoup_batch (const char *label,
    529                                 unsigned int expected_response_code,
    530                                 const char *withdraw_reference,
    531                                 const char *indices,
    532                                 const char *amount)
    533 {
    534   unsigned int *idx;
    535   size_t num;
    536 
    537   idx = TALER_TESTING_parse_index_list (indices,
    538                                         &num);
    539   GNUNET_assert (NULL != idx);
    540   return make_recoup_cmd (label,
    541                           expected_response_code,
    542                           withdraw_reference,
    543                           idx,
    544                           num,
    545                           amount);
    546 }
    547 
    548 
    549 /* end of testing_api_cmd_recoup.c */