exchange

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

taler-exchange-closer.c (17523B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2016-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 /**
     18  * @file taler-exchange-closer.c
     19  * @brief Process that closes expired reserves
     20  * @author Christian Grothoff
     21  */
     22 #include "platform.h"
     23 #include <gnunet/gnunet_util_lib.h>
     24 #include <jansson.h>
     25 #include <pthread.h>
     26 #include "exchangedb_lib.h"
     27 #include "taler/taler_json_lib.h"
     28 #include "taler/taler_bank_service.h"
     29 #include "exchange-database/start.h"
     30 #include "exchange-database/preflight.h"
     31 #include "exchange-database/commit.h"
     32 #include "exchange-database/rollback.h"
     33 #include "exchange-database/get_wire_fee.h"
     34 #include "exchange-database/insert_reserve_closed.h"
     35 #include "exchange-database/iterate_unfinished_close_requests.h"
     36 #include "exchange-database/iterate_expired_reserves.h"
     37 #include "exchange-database/insert_prewire.h"
     38 #include "exchange-database/insert_prewire.h"
     39 
     40 
     41 /**
     42  * What is the smallest unit we support for wire transfers?
     43  * We will need to round down to a multiple of this amount.
     44  */
     45 static struct TALER_Amount currency_round_unit;
     46 
     47 /**
     48  * What is the base URL of this exchange?  Used in the
     49  * wire transfer subjects so that merchants and governments
     50  * can ask for the list of aggregated deposits.
     51  */
     52 static char *exchange_base_url;
     53 
     54 /**
     55  * The exchange's configuration.
     56  */
     57 static const struct GNUNET_CONFIGURATION_Handle *cfg;
     58 
     59 /**
     60  * Our database plugin.
     61  */
     62 static struct TALER_EXCHANGEDB_PostgresContext *pg;
     63 
     64 /**
     65  * Next task to run, if any.
     66  */
     67 static struct GNUNET_SCHEDULER_Task *task;
     68 
     69 /**
     70  * How long should we sleep when idle before trying to find more work?
     71  */
     72 static struct GNUNET_TIME_Relative closer_idle_sleep_interval;
     73 
     74 /**
     75  * Value to return from main(). 0 on success, non-zero
     76  * on serious errors.
     77  */
     78 static int global_ret;
     79 
     80 /**
     81  * #GNUNET_YES if we are in test mode and should exit when idle.
     82  */
     83 static int test_mode;
     84 
     85 
     86 /**
     87  * Main work function that finds and triggers transfers for reserves
     88  * closures.
     89  *
     90  * @param cls closure
     91  */
     92 static void
     93 run_reserve_closures (void *cls);
     94 
     95 
     96 /**
     97  * We're being aborted with CTRL-C (or SIGTERM). Shut down.
     98  *
     99  * @param cls closure
    100  */
    101 static void
    102 shutdown_task (void *cls)
    103 {
    104   (void) cls;
    105   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    106               "Running shutdown\n");
    107   if (NULL != task)
    108   {
    109     GNUNET_SCHEDULER_cancel (task);
    110     task = NULL;
    111   }
    112   TALER_EXCHANGEDB_disconnect (pg);
    113   pg = NULL;
    114   TALER_EXCHANGEDB_unload_accounts ();
    115   cfg = NULL;
    116 }
    117 
    118 
    119 /**
    120  * Parse the configuration for wirewatch.
    121  *
    122  * @return #GNUNET_OK on success
    123  */
    124 static enum GNUNET_GenericReturnValue
    125 parse_closer_config (void)
    126 {
    127   if (GNUNET_OK !=
    128       GNUNET_CONFIGURATION_get_value_string (cfg,
    129                                              "exchange",
    130                                              "BASE_URL",
    131                                              &exchange_base_url))
    132   {
    133     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
    134                                "exchange",
    135                                "BASE_URL");
    136     return GNUNET_SYSERR;
    137   }
    138   if (GNUNET_OK !=
    139       GNUNET_CONFIGURATION_get_value_time (cfg,
    140                                            "exchange",
    141                                            "CLOSER_IDLE_SLEEP_INTERVAL",
    142                                            &closer_idle_sleep_interval))
    143   {
    144     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
    145                                "exchange",
    146                                "CLOSER_IDLE_SLEEP_INTERVAL");
    147     return GNUNET_SYSERR;
    148   }
    149   if ( (GNUNET_OK !=
    150         TALER_config_get_amount (cfg,
    151                                  "exchange",
    152                                  "CURRENCY_ROUND_UNIT",
    153                                  &currency_round_unit)) ||
    154        (TALER_amount_is_zero (&currency_round_unit)) )
    155   {
    156     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    157                 "Need non-zero amount in section `exchange' under `CURRENCY_ROUND_UNIT'\n");
    158     return GNUNET_SYSERR;
    159   }
    160 
    161   if (NULL ==
    162       (pg = TALER_EXCHANGEDB_connect (cfg)))
    163   {
    164     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    165                 "Failed to initialize DB subsystem\n");
    166     return GNUNET_SYSERR;
    167   }
    168   if (GNUNET_OK !=
    169       TALER_EXCHANGEDB_load_accounts (cfg,
    170                                       TALER_EXCHANGEDB_ALO_DEBIT))
    171   {
    172     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    173                 "No wire accounts configured for debit!\n");
    174     TALER_EXCHANGEDB_disconnect (pg);
    175     pg = NULL;
    176     return GNUNET_SYSERR;
    177   }
    178   return GNUNET_OK;
    179 }
    180 
    181 
    182 /**
    183  * Perform a database commit. If it fails, print a warning.
    184  *
    185  * @return status of commit
    186  */
    187 static enum GNUNET_DB_QueryStatus
    188 commit_or_warn (void)
    189 {
    190   enum GNUNET_DB_QueryStatus qs;
    191 
    192   qs = TALER_EXCHANGEDB_commit (pg);
    193   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    194     return qs;
    195   GNUNET_log ((GNUNET_DB_STATUS_SOFT_ERROR == qs)
    196               ? GNUNET_ERROR_TYPE_INFO
    197               : GNUNET_ERROR_TYPE_ERROR,
    198               "Failed to commit database transaction!\n");
    199   return qs;
    200 }
    201 
    202 
    203 /**
    204  * Function called with details about expired reserves.
    205  * We trigger the reserve closure by inserting the respective
    206  * closing record and prewire instructions into the respective
    207  * tables.
    208  *
    209  * @param cls NULL
    210  * @param reserve_pub public key of the reserve
    211  * @param left amount left in the reserve
    212  * @param account_payto_uri information about the bank account that initially
    213  *        caused the reserve to be created
    214  * @param expiration_date when did the reserve expire
    215  * @param close_request_row row of request asking for
    216  *         closure, 0 for expired reserves
    217  * @return #GNUNET_OK on success (continue)
    218  *         #GNUNET_NO on non-fatal errors (try again)
    219  *         #GNUNET_SYSERR on fatal errors (abort)
    220  */
    221 static enum GNUNET_GenericReturnValue
    222 expired_reserve_cb (void *cls,
    223                     const struct TALER_ReservePublicKeyP *reserve_pub,
    224                     const struct TALER_Amount *left,
    225                     const struct TALER_FullPayto account_payto_uri,
    226                     struct GNUNET_TIME_Timestamp expiration_date,
    227                     uint64_t close_request_row)
    228 {
    229   struct GNUNET_TIME_Timestamp now;
    230   struct TALER_WireTransferIdentifierRawP wtid;
    231   struct TALER_Amount amount_without_fee;
    232   struct TALER_Amount closing_fee;
    233   struct TALER_WireFeeSet fees;
    234   enum TALER_AmountArithmeticResult ret;
    235   const struct TALER_EXCHANGEDB_AccountInfo *wa;
    236 
    237   (void) cls;
    238   /* NOTE: potential optimization: use custom SQL API to not
    239      fetch this: */
    240   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    241               "Processing reserve closure at %s\n",
    242               GNUNET_TIME_timestamp2s (expiration_date));
    243   now = GNUNET_TIME_timestamp_get ();
    244 
    245   /* lookup account we should use */
    246   wa = TALER_EXCHANGEDB_find_account_by_payto_uri (account_payto_uri);
    247   if (NULL == wa)
    248   {
    249     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    250                 "No wire account configured to deal with target URI `%s'\n",
    251                 account_payto_uri.full_payto);
    252     global_ret = EXIT_FAILURE;
    253     GNUNET_SCHEDULER_shutdown ();
    254     return GNUNET_SYSERR;
    255   }
    256 
    257   /* lookup `fees` from time of actual reserve expiration
    258      (we may be lagging behind!) */
    259   {
    260     struct GNUNET_TIME_Timestamp start_date;
    261     struct GNUNET_TIME_Timestamp end_date;
    262     struct TALER_MasterSignatureP master_sig;
    263     enum GNUNET_DB_QueryStatus qs;
    264     uint64_t rowid;
    265 
    266     qs = TALER_EXCHANGEDB_get_wire_fee (pg,
    267                                         wa->method,
    268                                         expiration_date,
    269                                         &rowid,
    270                                         &start_date,
    271                                         &end_date,
    272                                         &fees,
    273                                         &master_sig);
    274     switch (qs)
    275     {
    276     case GNUNET_DB_STATUS_HARD_ERROR:
    277       GNUNET_break (0);
    278       return GNUNET_SYSERR;
    279     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    280       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    281                   "Could not get wire fees for %s at %s. Aborting run.\n",
    282                   wa->method,
    283                   GNUNET_TIME_timestamp2s (expiration_date));
    284       return GNUNET_SYSERR;
    285     case GNUNET_DB_STATUS_SOFT_ERROR:
    286       return GNUNET_NO;
    287     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    288       /* continued below */
    289       break;
    290     }
    291   }
    292 
    293   /* calculate transfer amount */
    294   closing_fee = fees.closing;
    295   ret = TALER_amount_subtract (&amount_without_fee,
    296                                left,
    297                                &closing_fee);
    298   if ( (TALER_AAR_INVALID_NEGATIVE_RESULT == ret) ||
    299        (TALER_AAR_RESULT_ZERO == ret) )
    300   {
    301     /* Closing fee higher than or equal to remaining balance, close
    302        without wire transfer. */
    303     closing_fee = *left;
    304     GNUNET_assert (GNUNET_OK ==
    305                    TALER_amount_set_zero (left->currency,
    306                                           &amount_without_fee));
    307     ret = TALER_AAR_RESULT_ZERO;
    308   }
    309   /* round down to enable transfer */
    310   if (GNUNET_SYSERR ==
    311       TALER_amount_round_down (&amount_without_fee,
    312                                &currency_round_unit))
    313   {
    314     GNUNET_break (0);
    315     global_ret = EXIT_FAILURE;
    316     GNUNET_SCHEDULER_shutdown ();
    317     return GNUNET_SYSERR;
    318   }
    319   /* NOTE: sizeof (*reserve_pub) == sizeof (wtid) right now, but to
    320      be future-compatible, we use the memset + min construction */
    321   memset (&wtid,
    322           0,
    323           sizeof (wtid));
    324   GNUNET_memcpy (&wtid,
    325                  reserve_pub,
    326                  GNUNET_MIN (sizeof (wtid),
    327                              sizeof (*reserve_pub)));
    328 
    329   {
    330     enum GNUNET_DB_QueryStatus qs;
    331 
    332     qs = TALER_EXCHANGEDB_insert_reserve_closed (pg,
    333                                                  reserve_pub,
    334                                                  now,
    335                                                  account_payto_uri,
    336                                                  &wtid,
    337                                                  left,
    338                                                  &closing_fee,
    339                                                  close_request_row);
    340     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    341                 "Closing reserve %s over %s (%d, %d)\n",
    342                 TALER_B2S (reserve_pub),
    343                 TALER_amount2s (left),
    344                 (int) ret,
    345                 qs);
    346     /* Check for hard failure */
    347     if (GNUNET_DB_STATUS_HARD_ERROR == qs)
    348     {
    349       GNUNET_break (0);
    350       global_ret = EXIT_FAILURE;
    351       GNUNET_SCHEDULER_shutdown ();
    352       return GNUNET_SYSERR;
    353     }
    354     if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
    355     {
    356       /* Serialization failure or lost connection: the transaction is
    357          aborted, so everything below would run on a dead transaction (and
    358          the commit would be turned into a rollback that used to be reported
    359          as success).  Start over instead. */
    360       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    361                   "Got DB soft error closing reserve, starting over\n");
    362       return GNUNET_NO;
    363     }
    364   }
    365   if (TALER_amount_is_zero (&amount_without_fee))
    366   {
    367     /* Reserve balance was zero. */
    368     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    369                 "Reserve was virtually empty, moving on\n");
    370     return GNUNET_OK;
    371   }
    372 
    373   /* success, perform wire transfer */
    374   {
    375     void *buf;
    376     size_t buf_size;
    377     enum GNUNET_DB_QueryStatus qs;
    378 
    379     TALER_BANK_prepare_transfer (account_payto_uri,
    380                                  &amount_without_fee,
    381                                  exchange_base_url,
    382                                  &wtid,
    383                                  NULL, /* no extra meta data */
    384                                  &buf,
    385                                  &buf_size);
    386     /* Commit our intention to execute the wire transfer! */
    387     qs = TALER_EXCHANGEDB_insert_prewire (pg,
    388                                           wa->method,
    389                                           buf,
    390                                           buf_size);
    391     GNUNET_free (buf);
    392     switch (qs)
    393     {
    394     case GNUNET_DB_STATUS_HARD_ERROR:
    395       GNUNET_break (0);
    396       global_ret = EXIT_FAILURE;
    397       GNUNET_SCHEDULER_shutdown ();
    398       return GNUNET_SYSERR;
    399     case GNUNET_DB_STATUS_SOFT_ERROR:
    400       /* start again */
    401       return GNUNET_NO;
    402     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    403       GNUNET_break (0);
    404       global_ret = EXIT_FAILURE;
    405       GNUNET_SCHEDULER_shutdown ();
    406       return GNUNET_SYSERR;
    407     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    408       break;
    409     }
    410   }
    411   return GNUNET_OK;
    412 }
    413 
    414 
    415 /**
    416  * Main work function that finds and triggers transfers for reserves
    417  * closures.
    418  *
    419  * @param cls closure
    420  */
    421 static void
    422 run_reserve_closures (void *cls)
    423 {
    424   enum GNUNET_DB_QueryStatus qs;
    425   struct GNUNET_TIME_Timestamp now;
    426 
    427   (void) cls;
    428   task = NULL;
    429   if (GNUNET_SYSERR ==
    430       TALER_EXCHANGEDB_preflight (pg))
    431   {
    432     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    433                 "Failed to obtain database connection!\n");
    434     global_ret = EXIT_FAILURE;
    435     GNUNET_SCHEDULER_shutdown ();
    436     return;
    437   }
    438 
    439   if (GNUNET_OK !=
    440       TALER_EXCHANGEDB_start (pg,
    441                               "aggregator reserve closures"))
    442   {
    443     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    444                 "Failed to start database transaction!\n");
    445     global_ret = EXIT_FAILURE;
    446     GNUNET_SCHEDULER_shutdown ();
    447     return;
    448   }
    449   now = GNUNET_TIME_timestamp_get ();
    450   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    451               "Checking for reserves to close by date %s\n",
    452               GNUNET_TIME_timestamp2s (now));
    453   qs = TALER_EXCHANGEDB_iterate_unfinished_close_requests (pg,
    454                                                            &expired_reserve_cb,
    455                                                            NULL);
    456   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    457   {
    458     /* Try expired reserves as well */
    459     qs = TALER_EXCHANGEDB_iterate_expired_reserves (
    460       pg,
    461       now,
    462       &expired_reserve_cb,
    463       NULL);
    464   }
    465   switch (qs)
    466   {
    467   case GNUNET_DB_STATUS_HARD_ERROR:
    468     GNUNET_break (0);
    469     TALER_EXCHANGEDB_rollback (pg);
    470     global_ret = EXIT_FAILURE;
    471     GNUNET_SCHEDULER_shutdown ();
    472     return;
    473   case GNUNET_DB_STATUS_SOFT_ERROR:
    474     TALER_EXCHANGEDB_rollback (pg);
    475     GNUNET_assert (NULL == task);
    476     task = GNUNET_SCHEDULER_add_now (&run_reserve_closures,
    477                                      NULL);
    478     return;
    479   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    480     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    481                 "No more idle reserves to close, going to sleep.\n");
    482     TALER_EXCHANGEDB_rollback (pg);
    483     GNUNET_assert (NULL == task);
    484     if (GNUNET_YES == test_mode)
    485     {
    486       GNUNET_SCHEDULER_shutdown ();
    487       return;
    488     }
    489     task = GNUNET_SCHEDULER_add_delayed (closer_idle_sleep_interval,
    490                                          &run_reserve_closures,
    491                                          NULL);
    492     return;
    493   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    494     (void) commit_or_warn ();
    495     GNUNET_assert (NULL == task);
    496     task = GNUNET_SCHEDULER_add_now (&run_reserve_closures,
    497                                      NULL);
    498     return;
    499   }
    500 }
    501 
    502 
    503 /**
    504  * First task.  Parses the configuration and starts the
    505  * main loop of #run_reserve_closures(). Also schedules
    506  * the #shutdown_task() to clean up.
    507  *
    508  * @param cls closure, NULL
    509  * @param args remaining command-line arguments
    510  * @param cfgfile name of the configuration file used (for saving, can be NULL!)
    511  * @param c configuration
    512  */
    513 static void
    514 run (void *cls,
    515      char *const *args,
    516      const char *cfgfile,
    517      const struct GNUNET_CONFIGURATION_Handle *c)
    518 {
    519   (void) cls;
    520   (void) args;
    521   (void) cfgfile;
    522 
    523   cfg = c;
    524   if (GNUNET_OK != parse_closer_config ())
    525   {
    526     cfg = NULL;
    527     global_ret = EXIT_NOTCONFIGURED;
    528     return;
    529   }
    530   GNUNET_assert (NULL == task);
    531   task = GNUNET_SCHEDULER_add_now (&run_reserve_closures,
    532                                    NULL);
    533   GNUNET_SCHEDULER_add_shutdown (&shutdown_task,
    534                                  cls);
    535 }
    536 
    537 
    538 /**
    539  * The main function of the taler-exchange-closer.
    540  *
    541  * @param argc number of arguments from the command line
    542  * @param argv command line arguments
    543  * @return 0 ok, non-zero on error
    544  */
    545 int
    546 main (int argc,
    547       char *const *argv)
    548 {
    549   struct GNUNET_GETOPT_CommandLineOption options[] = {
    550     GNUNET_GETOPT_option_timetravel ('T',
    551                                      "timetravel"),
    552     GNUNET_GETOPT_option_flag ('t',
    553                                "test",
    554                                "run in test mode and exit when idle",
    555                                &test_mode),
    556     GNUNET_GETOPT_OPTION_END
    557   };
    558   enum GNUNET_GenericReturnValue ret;
    559 
    560   ret = GNUNET_PROGRAM_run (
    561     TALER_EXCHANGE_project_data (),
    562     argc, argv,
    563     "taler-exchange-closer",
    564     gettext_noop ("background process that closes expired reserves"),
    565     options,
    566     &run, NULL);
    567   if (GNUNET_SYSERR == ret)
    568     return EXIT_INVALIDARGUMENT;
    569   if (GNUNET_NO == ret)
    570     return EXIT_SUCCESS;
    571   return global_ret;
    572 }
    573 
    574 
    575 /* end of taler-exchange-closer.c */