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_purses_deposit.c (16682B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2022 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_purses_deposit.c
     18  * @brief Handle /purses/$PID/deposit requests; parses the POST and JSON and
     19  *        verifies the coin signature before handing things off
     20  *        to the database.
     21  * @author Christian Grothoff
     22  */
     23 #include "taler/platform.h"
     24 #include <gnunet/gnunet_util_lib.h>
     25 #include <gnunet/gnunet_json_lib.h>
     26 #include <jansson.h>
     27 #include <microhttpd.h>
     28 #include "taler/taler_dbevents.h"
     29 #include "taler/taler_json_lib.h"
     30 #include "taler/taler_mhd_lib.h"
     31 #include "taler-exchange-httpd_common_deposit.h"
     32 #include "taler-exchange-httpd_purses_deposit.h"
     33 #include "taler-exchange-httpd_responses.h"
     34 #include "taler/taler_exchangedb_lib.h"
     35 #include "taler-exchange-httpd_keys.h"
     36 
     37 
     38 /**
     39  * Closure for #deposit_transaction.
     40  */
     41 struct PurseDepositContext
     42 {
     43   /**
     44    * Public key of the purse we are creating.
     45    */
     46   const struct TALER_PurseContractPublicKeyP *purse_pub;
     47 
     48   /**
     49    * Total amount to be put into the purse.
     50    */
     51   struct TALER_Amount amount;
     52 
     53   /**
     54    * Total actually deposited by all the coins.
     55    */
     56   struct TALER_Amount deposit_total;
     57 
     58   /**
     59    * When should the purse expire.
     60    */
     61   struct GNUNET_TIME_Timestamp purse_expiration;
     62 
     63   /**
     64    * Hash of the contract (needed for signing).
     65    */
     66   struct TALER_PrivateContractHashP h_contract_terms;
     67 
     68   /**
     69    * Our current time.
     70    */
     71   struct GNUNET_TIME_Timestamp exchange_timestamp;
     72 
     73   /**
     74    * Array of coins being deposited.
     75    */
     76   struct TEH_PurseDepositedCoin *coins;
     77 
     78   /**
     79    * Length of the @e coins array.
     80    */
     81   unsigned int num_coins;
     82 
     83   /**
     84    * Minimum age for deposits into this purse.
     85    */
     86   uint32_t min_age;
     87 };
     88 
     89 
     90 /**
     91  * Send confirmation of purse creation success to client.
     92  *
     93  * @param connection connection to the client
     94  * @param pcc details about the request that succeeded
     95  * @return MHD result code
     96  */
     97 static MHD_RESULT
     98 reply_deposit_success (struct MHD_Connection *connection,
     99                        const struct PurseDepositContext *pcc)
    100 {
    101   struct TALER_ExchangePublicKeyP pub;
    102   struct TALER_ExchangeSignatureP sig;
    103   enum TALER_ErrorCode ec;
    104 
    105   if (TALER_EC_NONE !=
    106       (ec = TALER_exchange_online_purse_created_sign (
    107          &TEH_keys_exchange_sign_,
    108          pcc->exchange_timestamp,
    109          pcc->purse_expiration,
    110          &pcc->amount,
    111          &pcc->deposit_total,
    112          pcc->purse_pub,
    113          &pcc->h_contract_terms,
    114          &pub,
    115          &sig)))
    116   {
    117     GNUNET_break (0);
    118     return TALER_MHD_reply_with_ec (connection,
    119                                     ec,
    120                                     NULL);
    121   }
    122   return TALER_MHD_REPLY_JSON_PACK (
    123     connection,
    124     MHD_HTTP_OK,
    125     TALER_JSON_pack_amount ("total_deposited",
    126                             &pcc->deposit_total),
    127     TALER_JSON_pack_amount ("purse_value_after_fees",
    128                             &pcc->amount),
    129     GNUNET_JSON_pack_timestamp ("exchange_timestamp",
    130                                 pcc->exchange_timestamp),
    131     GNUNET_JSON_pack_timestamp ("purse_expiration",
    132                                 pcc->purse_expiration),
    133     GNUNET_JSON_pack_data_auto ("h_contract_terms",
    134                                 &pcc->h_contract_terms),
    135     GNUNET_JSON_pack_data_auto ("exchange_sig",
    136                                 &sig),
    137     GNUNET_JSON_pack_data_auto ("exchange_pub",
    138                                 &pub));
    139 }
    140 
    141 
    142 /**
    143  * Execute database transaction for /purses/$PID/deposit.  Runs the transaction
    144  * logic; IF it returns a non-error code, the transaction logic MUST NOT queue
    145  * a MHD response.  IF it returns an hard error, the transaction logic MUST
    146  * queue a MHD response and set @a mhd_ret.  IF it returns the soft error
    147  * code, the function MAY be called again to retry and MUST not queue a MHD
    148  * response.
    149  *
    150  * @param cls a `struct PurseDepositContext`
    151  * @param connection MHD request context
    152  * @param[out] mhd_ret set to MHD status on error
    153  * @return transaction status
    154  */
    155 static enum GNUNET_DB_QueryStatus
    156 deposit_transaction (void *cls,
    157                      struct MHD_Connection *connection,
    158                      MHD_RESULT *mhd_ret)
    159 {
    160   struct PurseDepositContext *pcc = cls;
    161   enum GNUNET_DB_QueryStatus qs;
    162 
    163   qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS;
    164   for (unsigned int i = 0; i<pcc->num_coins; i++)
    165   {
    166     struct TEH_PurseDepositedCoin *coin = &pcc->coins[i];
    167     bool balance_ok = false;
    168     bool conflict = true;
    169     bool too_late = true;
    170 
    171     qs = TEH_make_coin_known (&coin->cpi,
    172                               connection,
    173                               &coin->known_coin_id,
    174                               mhd_ret);
    175     if (qs < 0)
    176       return qs;
    177     qs = TEH_plugin->do_purse_deposit (TEH_plugin->cls,
    178                                        pcc->purse_pub,
    179                                        &coin->cpi.coin_pub,
    180                                        &coin->amount,
    181                                        &coin->coin_sig,
    182                                        &coin->amount_minus_fee,
    183                                        &balance_ok,
    184                                        &too_late,
    185                                        &conflict);
    186     if (qs <= 0)
    187     {
    188       if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
    189         return qs;
    190       GNUNET_break (0 != qs);
    191       TALER_LOG_WARNING (
    192         "Failed to store purse deposit information in database\n");
    193       *mhd_ret = TALER_MHD_reply_with_error (connection,
    194                                              MHD_HTTP_INTERNAL_SERVER_ERROR,
    195                                              TALER_EC_GENERIC_DB_STORE_FAILED,
    196                                              "do purse deposit");
    197       return GNUNET_DB_STATUS_HARD_ERROR;
    198     }
    199     if (! balance_ok)
    200     {
    201       *mhd_ret
    202         = TEH_RESPONSE_reply_coin_insufficient_funds (
    203             connection,
    204             TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS,
    205             &coin->cpi.denom_pub_hash,
    206             &coin->cpi.coin_pub);
    207       return GNUNET_DB_STATUS_HARD_ERROR;
    208     }
    209     if (too_late)
    210     {
    211       TEH_plugin->rollback (TEH_plugin->cls);
    212       *mhd_ret
    213         = TALER_MHD_reply_with_ec (
    214             connection,
    215             TALER_EC_EXCHANGE_PURSE_DEPOSIT_DECIDED_ALREADY,
    216             NULL);
    217       return GNUNET_DB_STATUS_HARD_ERROR;
    218     }
    219     if (conflict)
    220     {
    221       struct TALER_Amount amount;
    222       struct TALER_CoinSpendPublicKeyP coin_pub;
    223       struct TALER_CoinSpendSignatureP coin_sig;
    224       struct TALER_DenominationHashP h_denom_pub;
    225       struct TALER_AgeCommitmentHashP phac;
    226       char *partner_url = NULL;
    227 
    228       TEH_plugin->rollback (TEH_plugin->cls);
    229       qs = TEH_plugin->get_purse_deposit (TEH_plugin->cls,
    230                                           pcc->purse_pub,
    231                                           &coin->cpi.coin_pub,
    232                                           &amount,
    233                                           &h_denom_pub,
    234                                           &phac,
    235                                           &coin_sig,
    236                                           &partner_url);
    237       if (qs < 0)
    238       {
    239         GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
    240         TALER_LOG_WARNING (
    241           "Failed to fetch purse deposit information from database\n");
    242         *mhd_ret = TALER_MHD_reply_with_error (connection,
    243                                                MHD_HTTP_INTERNAL_SERVER_ERROR,
    244                                                TALER_EC_GENERIC_DB_FETCH_FAILED,
    245                                                "get purse deposit");
    246         return GNUNET_DB_STATUS_HARD_ERROR;
    247       }
    248 
    249       *mhd_ret
    250         = TALER_MHD_REPLY_JSON_PACK (
    251             connection,
    252             MHD_HTTP_CONFLICT,
    253             TALER_JSON_pack_ec (
    254               TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA),
    255             GNUNET_JSON_pack_data_auto ("coin_pub",
    256                                         &coin_pub),
    257             GNUNET_JSON_pack_data_auto ("h_denom_pub",
    258                                         &h_denom_pub),
    259             GNUNET_JSON_pack_data_auto ("h_age_commitment",
    260                                         &phac),
    261             GNUNET_JSON_pack_data_auto ("coin_sig",
    262                                         &coin_sig),
    263             GNUNET_JSON_pack_allow_null (
    264               GNUNET_JSON_pack_string ("partner_url",
    265                                        partner_url)),
    266             TALER_JSON_pack_amount ("amount",
    267                                     &amount));
    268       GNUNET_free (partner_url);
    269       return GNUNET_DB_STATUS_HARD_ERROR;
    270     }
    271   }
    272   return qs;
    273 }
    274 
    275 
    276 /**
    277  * Parse a coin and check signature of the coin and the denomination
    278  * signature over the coin.
    279  *
    280  * @param[in,out] connection our HTTP connection
    281  * @param[in,out] pcc request context
    282  * @param[out] coin coin to initialize
    283  * @param jcoin coin to parse
    284  * @return #GNUNET_OK on success, #GNUNET_NO if an error was returned,
    285  *         #GNUNET_SYSERR on failure and no error could be returned
    286  */
    287 static enum GNUNET_GenericReturnValue
    288 parse_coin (struct MHD_Connection *connection,
    289             struct PurseDepositContext *pcc,
    290             struct TEH_PurseDepositedCoin *coin,
    291             const json_t *jcoin)
    292 {
    293   enum GNUNET_GenericReturnValue iret;
    294 
    295   if (GNUNET_OK !=
    296       (iret = TEH_common_purse_deposit_parse_coin (connection,
    297                                                    coin,
    298                                                    jcoin)))
    299     return iret;
    300   if (GNUNET_OK !=
    301       (iret = TEH_common_deposit_check_purse_deposit (
    302          connection,
    303          coin,
    304          pcc->purse_pub,
    305          pcc->min_age)))
    306     return iret;
    307   if (0 >
    308       TALER_amount_add (&pcc->deposit_total,
    309                         &pcc->deposit_total,
    310                         &coin->amount_minus_fee))
    311   {
    312     GNUNET_break (0);
    313     return TALER_MHD_reply_with_error (connection,
    314                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
    315                                        TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT,
    316                                        "total deposit contribution");
    317   }
    318   return GNUNET_OK;
    319 }
    320 
    321 
    322 MHD_RESULT
    323 TEH_handler_purses_deposit (
    324   struct TEH_RequestContext *rc,
    325   const struct TALER_PurseContractPublicKeyP *purse_pub,
    326   const json_t *root)
    327 {
    328   struct MHD_Connection *connection = rc->connection;
    329   struct PurseDepositContext pcc = {
    330     .purse_pub = purse_pub,
    331     .exchange_timestamp = GNUNET_TIME_timestamp_get ()
    332   };
    333   const json_t *deposits;
    334   json_t *deposit;
    335   unsigned int idx;
    336   struct GNUNET_JSON_Specification spec[] = {
    337     GNUNET_JSON_spec_array_const ("deposits",
    338                                   &deposits),
    339     GNUNET_JSON_spec_end ()
    340   };
    341 
    342   {
    343     enum GNUNET_GenericReturnValue res;
    344 
    345     res = TALER_MHD_parse_json_data (connection,
    346                                      root,
    347                                      spec);
    348     if (GNUNET_SYSERR == res)
    349     {
    350       GNUNET_break (0);
    351       return MHD_NO; /* hard failure */
    352     }
    353     if (GNUNET_NO == res)
    354     {
    355       GNUNET_break_op (0);
    356       return MHD_YES; /* failure */
    357     }
    358   }
    359   GNUNET_assert (GNUNET_OK ==
    360                  TALER_amount_set_zero (TEH_currency,
    361                                         &pcc.deposit_total));
    362   pcc.num_coins = (unsigned int) json_array_size (deposits);
    363   if ( (0 == pcc.num_coins) ||
    364        (((size_t) pcc.num_coins) != json_array_size (deposits)) ||
    365        (pcc.num_coins > TALER_MAX_COINS) )
    366   {
    367     GNUNET_break_op (0);
    368     return TALER_MHD_reply_with_error (connection,
    369                                        MHD_HTTP_BAD_REQUEST,
    370                                        TALER_EC_GENERIC_PARAMETER_MALFORMED,
    371                                        "deposits");
    372   }
    373 
    374   {
    375     enum GNUNET_DB_QueryStatus qs;
    376     struct GNUNET_TIME_Timestamp create_timestamp;
    377     struct GNUNET_TIME_Timestamp merge_timestamp;
    378     bool was_deleted;
    379     bool was_refunded;
    380 
    381     qs = TEH_plugin->select_purse (
    382       TEH_plugin->cls,
    383       pcc.purse_pub,
    384       &create_timestamp,
    385       &pcc.purse_expiration,
    386       &pcc.amount,
    387       &pcc.deposit_total,
    388       &pcc.h_contract_terms,
    389       &merge_timestamp,
    390       &was_deleted,
    391       &was_refunded);
    392     switch (qs)
    393     {
    394     case GNUNET_DB_STATUS_HARD_ERROR:
    395       GNUNET_break (0);
    396       return TALER_MHD_reply_with_error (connection,
    397                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
    398                                          TALER_EC_GENERIC_DB_FETCH_FAILED,
    399                                          "select purse");
    400     case GNUNET_DB_STATUS_SOFT_ERROR:
    401       GNUNET_break (0);
    402       return TALER_MHD_reply_with_error (connection,
    403                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
    404                                          TALER_EC_GENERIC_DB_FETCH_FAILED,
    405                                          "select purse");
    406     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    407       return TALER_MHD_reply_with_error (connection,
    408                                          MHD_HTTP_NOT_FOUND,
    409                                          TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN,
    410                                          NULL);
    411     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    412       break; /* handled below */
    413     }
    414     if (was_refunded ||
    415         was_deleted)
    416     {
    417       return TALER_MHD_reply_with_error (
    418         connection,
    419         MHD_HTTP_GONE,
    420         was_deleted
    421         ? TALER_EC_EXCHANGE_GENERIC_PURSE_DELETED
    422         : TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED,
    423         GNUNET_TIME_timestamp2s (pcc.purse_expiration));
    424     }
    425   }
    426 
    427   /* parse deposits */
    428   pcc.coins = GNUNET_new_array (pcc.num_coins,
    429                                 struct TEH_PurseDepositedCoin);
    430   json_array_foreach (deposits, idx, deposit)
    431   {
    432     enum GNUNET_GenericReturnValue res;
    433     struct TEH_PurseDepositedCoin *coin = &pcc.coins[idx];
    434 
    435     res = parse_coin (connection,
    436                       &pcc,
    437                       coin,
    438                       deposit);
    439     if (GNUNET_OK != res)
    440     {
    441       for (unsigned int i = 0; i<idx; i++)
    442         TEH_common_purse_deposit_free_coin (&pcc.coins[i]);
    443       GNUNET_free (pcc.coins);
    444       return (GNUNET_NO == res) ? MHD_YES : MHD_NO;
    445     }
    446   }
    447 
    448   if (GNUNET_SYSERR ==
    449       TEH_plugin->preflight (TEH_plugin->cls))
    450   {
    451     GNUNET_break (0);
    452     for (unsigned int i = 0; i<pcc.num_coins; i++)
    453       TEH_common_purse_deposit_free_coin (&pcc.coins[i]);
    454     GNUNET_free (pcc.coins);
    455     return TALER_MHD_reply_with_error (connection,
    456                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
    457                                        TALER_EC_GENERIC_DB_START_FAILED,
    458                                        "preflight failure");
    459   }
    460 
    461   /* execute transaction */
    462   {
    463     MHD_RESULT mhd_ret;
    464 
    465     if (GNUNET_OK !=
    466         TEH_DB_run_transaction (connection,
    467                                 "execute purse deposit",
    468                                 TEH_MT_REQUEST_PURSE_DEPOSIT,
    469                                 &mhd_ret,
    470                                 &deposit_transaction,
    471                                 &pcc))
    472     {
    473       for (unsigned int i = 0; i<pcc.num_coins; i++)
    474         TEH_common_purse_deposit_free_coin (&pcc.coins[i]);
    475       GNUNET_free (pcc.coins);
    476       return mhd_ret;
    477     }
    478   }
    479   {
    480     struct TALER_PurseEventP rep = {
    481       .header.size = htons (sizeof (rep)),
    482       .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_DEPOSITED),
    483       .purse_pub = *pcc.purse_pub
    484     };
    485 
    486     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    487                 "Notifying about purse deposit %s\n",
    488                 TALER_B2S (pcc.purse_pub));
    489     TEH_plugin->event_notify (TEH_plugin->cls,
    490                               &rep.header,
    491                               NULL,
    492                               0);
    493   }
    494 
    495   /* generate regular response */
    496   {
    497     MHD_RESULT res;
    498 
    499     res = reply_deposit_success (connection,
    500                                  &pcc);
    501     for (unsigned int i = 0; i<pcc.num_coins; i++)
    502       TEH_common_purse_deposit_free_coin (&pcc.coins[i]);
    503     GNUNET_free (pcc.coins);
    504     return res;
    505   }
    506 }
    507 
    508 
    509 /* end of taler-exchange-httpd_purses_deposit.c */