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_coin_history.c (17540B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2023 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify
      6   it under the terms of the GNU General Public License as
      7   published by the Free Software Foundation; either version 3, or
      8   (at your option) any later version.
      9 
     10   TALER is distributed in the hope that it will be useful, but
     11   WITHOUT ANY WARRANTY; without even the implied warranty of
     12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13   GNU General Public License for more details.
     14 
     15   You should have received a copy of the GNU General Public
     16   License along with TALER; see the file COPYING.  If not, see
     17   <http://www.gnu.org/licenses/>
     18 */
     19 /**
     20  * @file testing/testing_api_cmd_coin_history.c
     21  * @brief Implement the /coins/$COIN_PUB/history test command.
     22  * @author Christian Grothoff
     23  */
     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 "history" CMD.
     31  */
     32 struct HistoryState
     33 {
     34 
     35   /**
     36    * Public key of the coin being analyzed.
     37    */
     38   struct TALER_CoinSpendPublicKeyP coin_pub;
     39 
     40   /**
     41    * Label to the command which created the coin to check,
     42    * needed to resort the coin key.
     43    */
     44   const char *coin_reference;
     45 
     46   /**
     47    * Handle to the "coin history" operation.
     48    */
     49   struct TALER_EXCHANGE_GetCoinsHistoryHandle *rsh;
     50 
     51   /**
     52    * Expected coin balance.
     53    */
     54   const char *expected_balance;
     55 
     56   /**
     57    * Private key of the coin being analyzed.
     58    */
     59   const struct TALER_CoinSpendPrivateKeyP *coin_priv;
     60 
     61   /**
     62    * Interpreter state.
     63    */
     64   struct TALER_TESTING_Interpreter *is;
     65 
     66   /**
     67    * Expected HTTP response code.
     68    */
     69   unsigned int expected_response_code;
     70 
     71 };
     72 
     73 
     74 /**
     75  * Closure for analysis_cb().
     76  */
     77 struct AnalysisContext
     78 {
     79   /**
     80    * Coin public key we are looking at.
     81    */
     82   const struct TALER_CoinSpendPublicKeyP *coin_pub;
     83 
     84   /**
     85    * Length of the @e history array.
     86    */
     87   unsigned int history_length;
     88 
     89   /**
     90    * Array of history items to match.
     91    */
     92   const struct TALER_EXCHANGE_CoinHistoryEntry *history;
     93 
     94   /**
     95    * Array of @e history_length of matched entries.
     96    */
     97   bool *found;
     98 
     99   /**
    100    * Set to true if an entry could not be found.
    101    */
    102   bool failure;
    103 };
    104 
    105 
    106 /**
    107  * Compare @a h1 and @a h2.
    108  *
    109  * @param h1 a history entry
    110  * @param h2 a history entry
    111  * @return 0 if @a h1 and @a h2 are equal
    112  */
    113 static int
    114 history_entry_cmp (
    115   const struct TALER_EXCHANGE_CoinHistoryEntry *h1,
    116   const struct TALER_EXCHANGE_CoinHistoryEntry *h2)
    117 {
    118   if (h1->type != h2->type)
    119     return 1;
    120   if (0 != TALER_amount_cmp (&h1->amount,
    121                              &h2->amount))
    122   {
    123     GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    124                 "Amount mismatch (%s)\n",
    125                 TALER_amount2s (&h1->amount));
    126     return 1;
    127   }
    128   switch (h1->type)
    129   {
    130   case TALER_EXCHANGE_CTT_NONE:
    131     GNUNET_break (0);
    132     break;
    133   case TALER_EXCHANGE_CTT_DEPOSIT:
    134     if (0 != GNUNET_memcmp (&h1->details.deposit.h_contract_terms,
    135                             &h2->details.deposit.h_contract_terms))
    136       return 1;
    137     if (0 != GNUNET_memcmp (&h1->details.deposit.merchant_pub,
    138                             &h2->details.deposit.merchant_pub))
    139       return 1;
    140     if (0 != GNUNET_memcmp (&h1->details.deposit.h_wire,
    141                             &h2->details.deposit.h_wire))
    142       return 1;
    143     if (0 != GNUNET_memcmp (&h1->details.deposit.sig,
    144                             &h2->details.deposit.sig))
    145       return 1;
    146     return 0;
    147   case TALER_EXCHANGE_CTT_MELT:
    148     if (0 != GNUNET_memcmp (&h1->details.melt.h_age_commitment,
    149                             &h2->details.melt.h_age_commitment))
    150       return 1;
    151     /* Note: most other fields are not initialized
    152        in the trait as they are hard to extract from
    153        the API */
    154     return 0;
    155   case TALER_EXCHANGE_CTT_REFUND:
    156     if (0 != GNUNET_memcmp (&h1->details.refund.sig,
    157                             &h2->details.refund.sig))
    158       return 1;
    159     return 0;
    160   case TALER_EXCHANGE_CTT_RECOUP:
    161     if (0 != GNUNET_memcmp (&h1->details.recoup.coin_sig,
    162                             &h2->details.recoup.coin_sig))
    163       return 1;
    164     /* Note: exchange_sig, exchange_pub and timestamp are
    165        fundamentally not available in the initiating command */
    166     return 0;
    167   case TALER_EXCHANGE_CTT_RECOUP_REFRESH:
    168     if (0 != GNUNET_memcmp (&h1->details.recoup_refresh.coin_sig,
    169                             &h2->details.recoup_refresh.coin_sig))
    170       return 1;
    171     /* Note: exchange_sig, exchange_pub and timestamp are
    172        fundamentally not available in the initiating command */
    173     return 0;
    174   case TALER_EXCHANGE_CTT_OLD_COIN_RECOUP:
    175     if (0 != GNUNET_memcmp (&h1->details.old_coin_recoup.new_coin_pub,
    176                             &h2->details.old_coin_recoup.new_coin_pub))
    177       return 1;
    178     /* Note: exchange_sig, exchange_pub and timestamp are
    179        fundamentally not available in the initiating command */
    180     return 0;
    181   case TALER_EXCHANGE_CTT_PURSE_DEPOSIT:
    182     /* coin_sig is not initialized */
    183     if (0 != GNUNET_memcmp (&h1->details.purse_deposit.purse_pub,
    184                             &h2->details.purse_deposit.purse_pub))
    185     {
    186       GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    187                   "Purse public key mismatch\n");
    188       return 1;
    189     }
    190     if (0 != strcmp (h1->details.purse_deposit.exchange_base_url,
    191                      h2->details.purse_deposit.exchange_base_url))
    192     {
    193       GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    194                   "Exchange base URL mismatch (%s/%s)\n",
    195                   h1->details.purse_deposit.exchange_base_url,
    196                   h2->details.purse_deposit.exchange_base_url);
    197       GNUNET_break (0);
    198       return 1;
    199     }
    200     return 0;
    201   case TALER_EXCHANGE_CTT_PURSE_REFUND:
    202     /* NOTE: not supported yet (trait not returned) */
    203     return 0;
    204   case TALER_EXCHANGE_CTT_RESERVE_OPEN_DEPOSIT:
    205     /* NOTE: not supported yet (trait not returned) */
    206     if (0 != GNUNET_memcmp (&h1->details.reserve_open_deposit.coin_sig,
    207                             &h2->details.reserve_open_deposit.coin_sig))
    208       return 1;
    209     return 0;
    210   }
    211   GNUNET_assert (0);
    212   return -1;
    213 }
    214 
    215 
    216 /**
    217  * Check if @a cmd changed the coin, if so, find the
    218  * entry in our history and set the respective index in found
    219  * to true. If the entry is not found, set failure.
    220  *
    221  * @param cls our `struct AnalysisContext *`
    222  * @param cmd command to analyze for impact on history
    223  */
    224 static void
    225 analyze_command (void *cls,
    226                  const struct TALER_TESTING_Command *cmd)
    227 {
    228   struct AnalysisContext *ac = cls;
    229   const struct TALER_CoinSpendPublicKeyP *coin_pub = ac->coin_pub;
    230   const struct TALER_EXCHANGE_CoinHistoryEntry *history = ac->history;
    231   unsigned int history_length = ac->history_length;
    232   bool *found = ac->found;
    233 
    234   if (TALER_TESTING_cmd_is_batch (cmd))
    235   {
    236     struct TALER_TESTING_Command *cur;
    237     struct TALER_TESTING_Command *bcmd;
    238 
    239     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    240                 "Checking `%s' for history of coin `%s'\n",
    241                 cmd->label,
    242                 TALER_B2S (coin_pub));
    243     cur = TALER_TESTING_cmd_batch_get_current (cmd);
    244     if (GNUNET_OK !=
    245         TALER_TESTING_get_trait_batch_cmds (cmd,
    246                                             &bcmd))
    247     {
    248       GNUNET_break (0);
    249       ac->failure = true;
    250       return;
    251     }
    252     for (unsigned int i = 0; NULL != bcmd[i].label; i++)
    253     {
    254       struct TALER_TESTING_Command *step = &bcmd[i];
    255 
    256       analyze_command (ac,
    257                        step);
    258       if (ac->failure)
    259       {
    260         GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    261                     "Entry for batch step `%s' missing in coin history\n",
    262                     step->label);
    263         return;
    264       }
    265       if (step == cur)
    266         break; /* if *we* are in a batch, make sure not to analyze commands past 'now' */
    267     }
    268     return;
    269   }
    270 
    271   for (unsigned int j = 0; true; j++)
    272   {
    273     const struct TALER_CoinSpendPublicKeyP *rp;
    274     const struct TALER_EXCHANGE_CoinHistoryEntry *he;
    275     bool matched = false;
    276 
    277     if (GNUNET_OK !=
    278         TALER_TESTING_get_trait_coin_pub (cmd,
    279                                           j,
    280                                           &rp))
    281     {
    282       GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    283                   "Command `%s#%u' has no public key for a coin\n",
    284                   cmd->label,
    285                   j);
    286       break; /* command does nothing for coins */
    287     }
    288     if (0 !=
    289         GNUNET_memcmp (rp,
    290                        coin_pub))
    291     {
    292       GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    293                   "Command `%s#%u' is about another coin %s\n",
    294                   cmd->label,
    295                   j,
    296                   TALER_B2S (rp));
    297       continue; /* command affects some _other_ coin */
    298     }
    299     if (GNUNET_OK !=
    300         TALER_TESTING_get_trait_coin_history (cmd,
    301                                               j,
    302                                               &he))
    303     {
    304       /* NOTE: only for debugging... */
    305       if (0 == j)
    306         GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    307                     "Command `%s' has the coin_pub, but lacks coin history trait\n",
    308                     cmd->label);
    309       return; /* command does nothing for coins */
    310     }
    311     for (unsigned int i = 0; i<history_length; i++)
    312     {
    313       if (found[i])
    314         continue; /* already found, skip */
    315       if (0 ==
    316           history_entry_cmp (he,
    317                              &history[i]))
    318       {
    319         found[i] = true;
    320         matched = true;
    321         break;
    322       }
    323     }
    324     if (! matched)
    325     {
    326       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    327                   "Command `%s' coin history entry #%u not found\n",
    328                   cmd->label,
    329                   j);
    330       ac->failure = true;
    331       return;
    332     }
    333   }
    334 }
    335 
    336 
    337 /**
    338  * Check that the coin balance and HTTP response code are
    339  * both acceptable.
    340  *
    341  * @param cls closure.
    342  * @param rs HTTP response details
    343  */
    344 static void
    345 coin_history_cb (void *cls,
    346                  const struct TALER_EXCHANGE_GetCoinsHistoryResponse *rs)
    347 {
    348   struct HistoryState *ss = cls;
    349   struct TALER_TESTING_Interpreter *is = ss->is;
    350   struct TALER_Amount eb;
    351   unsigned int hlen;
    352 
    353   ss->rsh = NULL;
    354   if (ss->expected_response_code != rs->hr.http_status)
    355   {
    356     TALER_TESTING_unexpected_status (ss->is,
    357                                      rs->hr.http_status,
    358                                      ss->expected_response_code);
    359     return;
    360   }
    361   if (MHD_HTTP_OK != rs->hr.http_status)
    362   {
    363     TALER_TESTING_interpreter_next (is);
    364     return;
    365   }
    366   GNUNET_assert (GNUNET_OK ==
    367                  TALER_string_to_amount (ss->expected_balance,
    368                                          &eb));
    369 
    370   if (0 != TALER_amount_cmp (&eb,
    371                              &rs->details.ok.balance))
    372   {
    373     GNUNET_break (0);
    374     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    375                 "Unexpected balance for coin: %s\n",
    376                 TALER_amount_to_string (&rs->details.ok.balance));
    377     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    378                 "Expected balance of: %s\n",
    379                 TALER_amount_to_string (&eb));
    380     TALER_TESTING_interpreter_fail (ss->is);
    381     return;
    382   }
    383   hlen = json_array_size (rs->details.ok.history);
    384   {
    385     bool found[GNUNET_NZL (hlen)];
    386     struct TALER_EXCHANGE_CoinHistoryEntry rhist[GNUNET_NZL (hlen)];
    387     struct AnalysisContext ac = {
    388       .coin_pub = &ss->coin_pub,
    389       .history = rhist,
    390       .history_length = hlen,
    391       .found = found
    392     };
    393     const struct TALER_EXCHANGE_DenomPublicKey *dk;
    394     struct TALER_Amount total_in;
    395     struct TALER_Amount total_out;
    396     struct TALER_Amount hbal;
    397 
    398     dk = TALER_EXCHANGE_get_denomination_key_by_hash (
    399       TALER_TESTING_get_keys (is),
    400       &rs->details.ok.h_denom_pub);
    401     memset (found,
    402             0,
    403             sizeof (found));
    404     memset (rhist,
    405             0,
    406             sizeof (rhist));
    407     if (GNUNET_OK !=
    408         TALER_EXCHANGE_parse_coin_history (
    409           TALER_TESTING_get_keys (is),
    410           dk,
    411           rs->details.ok.history,
    412           &ss->coin_pub,
    413           &total_in,
    414           &total_out,
    415           hlen,
    416           rhist))
    417     {
    418       GNUNET_break (0);
    419       json_dumpf (rs->hr.reply,
    420                   stderr,
    421                   JSON_INDENT (2));
    422       TALER_TESTING_interpreter_fail (ss->is);
    423       return;
    424     }
    425     if (0 >
    426         TALER_amount_subtract (&hbal,
    427                                &total_in,
    428                                &total_out))
    429     {
    430       GNUNET_break (0);
    431       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    432                   "Coin credits: %s\n",
    433                   TALER_amount2s (&total_in));
    434       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    435                   "Coin debits: %s\n",
    436                   TALER_amount2s (&total_out));
    437       TALER_TESTING_interpreter_fail (ss->is);
    438       return;
    439     }
    440     if (0 != TALER_amount_cmp (&hbal,
    441                                &rs->details.ok.balance))
    442     {
    443       GNUNET_break (0);
    444       TALER_TESTING_interpreter_fail (ss->is);
    445       return;
    446     }
    447     (void) ac;
    448     TALER_TESTING_iterate (is,
    449                            true,
    450                            &analyze_command,
    451                            &ac);
    452     if (ac.failure)
    453     {
    454       json_dumpf (rs->hr.reply,
    455                   stderr,
    456                   JSON_INDENT (2));
    457       TALER_TESTING_interpreter_fail (ss->is);
    458       return;
    459     }
    460 #if 1
    461     for (unsigned int i = 0; i<hlen; i++)
    462     {
    463       if (found[i])
    464         continue;
    465       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    466                   "History entry at index %u of type %d not justified by command history\n",
    467                   i,
    468                   rs->details.ok.history[i].type);
    469       json_dumpf (rs->hr.reply,
    470                   stderr,
    471                   JSON_INDENT (2));
    472       TALER_TESTING_interpreter_fail (ss->is);
    473       return;
    474     }
    475 #endif
    476   }
    477   TALER_TESTING_interpreter_next (is);
    478 }
    479 
    480 
    481 /**
    482  * Run the command.
    483  *
    484  * @param cls closure.
    485  * @param cmd the command being executed.
    486  * @param is the interpreter state.
    487  */
    488 static void
    489 history_run (void *cls,
    490              const struct TALER_TESTING_Command *cmd,
    491              struct TALER_TESTING_Interpreter *is)
    492 {
    493   struct HistoryState *ss = cls;
    494   const struct TALER_TESTING_Command *create_coin;
    495   char *cref;
    496   unsigned int idx;
    497 
    498   ss->is = is;
    499   GNUNET_assert (
    500     GNUNET_OK ==
    501     TALER_TESTING_parse_coin_reference (
    502       ss->coin_reference,
    503       &cref,
    504       &idx));
    505   create_coin
    506     = TALER_TESTING_interpreter_lookup_command (is,
    507                                                 cref);
    508   GNUNET_free (cref);
    509   if (NULL == create_coin)
    510   {
    511     GNUNET_break (0);
    512     TALER_TESTING_interpreter_fail (is);
    513     return;
    514   }
    515   if (GNUNET_OK !=
    516       TALER_TESTING_get_trait_coin_priv (create_coin,
    517                                          idx,
    518                                          &ss->coin_priv))
    519   {
    520     GNUNET_break (0);
    521     TALER_LOG_ERROR ("Failed to find coin_priv for history query\n");
    522     TALER_TESTING_interpreter_fail (is);
    523     return;
    524   }
    525   GNUNET_CRYPTO_eddsa_key_get_public (&ss->coin_priv->eddsa_priv,
    526                                       &ss->coin_pub.eddsa_pub);
    527   ss->rsh = TALER_EXCHANGE_get_coins_history_create (
    528     TALER_TESTING_interpreter_get_context (is),
    529     TALER_TESTING_get_exchange_url (is),
    530     ss->coin_priv);
    531   GNUNET_assert (NULL != ss->rsh);
    532   GNUNET_assert (TALER_EC_NONE ==
    533                  TALER_EXCHANGE_get_coins_history_start (ss->rsh,
    534                                                          &coin_history_cb,
    535                                                          ss));
    536 }
    537 
    538 
    539 /**
    540  * Offer internal data from a "history" CMD, to other commands.
    541  *
    542  * @param cls closure.
    543  * @param[out] ret result.
    544  * @param trait name of the trait.
    545  * @param index index number of the object to offer.
    546  * @return #GNUNET_OK on success.
    547  */
    548 static enum GNUNET_GenericReturnValue
    549 history_traits (void *cls,
    550                 const void **ret,
    551                 const char *trait,
    552                 unsigned int index)
    553 {
    554   struct HistoryState *hs = cls;
    555   struct TALER_TESTING_Trait traits[] = {
    556     TALER_TESTING_make_trait_coin_pub (index,
    557                                        &hs->coin_pub),
    558     TALER_TESTING_trait_end ()
    559   };
    560 
    561   return TALER_TESTING_get_trait (traits,
    562                                   ret,
    563                                   trait,
    564                                   index);
    565 }
    566 
    567 
    568 /**
    569  * Cleanup the state from a "coin history" CMD, and possibly
    570  * cancel a pending operation thereof.
    571  *
    572  * @param cls closure.
    573  * @param cmd the command which is being cleaned up.
    574  */
    575 static void
    576 history_cleanup (void *cls,
    577                  const struct TALER_TESTING_Command *cmd)
    578 {
    579   struct HistoryState *ss = cls;
    580 
    581   if (NULL != ss->rsh)
    582   {
    583     TALER_TESTING_command_incomplete (ss->is,
    584                                       cmd->label);
    585     TALER_EXCHANGE_get_coins_history_cancel (ss->rsh);
    586     ss->rsh = NULL;
    587   }
    588   GNUNET_free (ss);
    589 }
    590 
    591 
    592 struct TALER_TESTING_Command
    593 TALER_TESTING_cmd_coin_history (const char *label,
    594                                 const char *coin_reference,
    595                                 const char *expected_balance,
    596                                 unsigned int expected_response_code)
    597 {
    598   struct HistoryState *ss;
    599 
    600   GNUNET_assert (NULL != coin_reference);
    601   ss = GNUNET_new (struct HistoryState);
    602   ss->coin_reference = coin_reference;
    603   ss->expected_balance = expected_balance;
    604   ss->expected_response_code = expected_response_code;
    605   {
    606     struct TALER_TESTING_Command cmd = {
    607       .cls = ss,
    608       .label = label,
    609       .run = &history_run,
    610       .cleanup = &history_cleanup,
    611       .traits = &history_traits
    612     };
    613 
    614     return cmd;
    615   }
    616 }