/* This file is part of TALER Copyright (C) 2022 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with TALER; see the file COPYING. If not, see */ /** * @file taler-exchange-drain.c * @brief Process that drains exchange profits from the escrow account * and puts them into some regular account of the exchange. * @author Christian Grothoff */ #include "platform.h" #include #include #include #include "taler_exchangedb_lib.h" #include "taler_exchangedb_plugin.h" #include "taler_json_lib.h" #include "taler_bank_service.h" /** * The exchange's configuration. */ static const struct GNUNET_CONFIGURATION_Handle *cfg; /** * Our database plugin. */ static struct TALER_EXCHANGEDB_Plugin *db_plugin; /** * Our master public key. */ static struct TALER_MasterPublicKeyP master_pub; /** * Next task to run, if any. */ static struct GNUNET_SCHEDULER_Task *task; /** * Base URL of this exchange. */ static char *exchange_base_url; /** * Value to return from main(). 0 on success, non-zero on errors. */ static int global_ret; /** * We're being aborted with CTRL-C (or SIGTERM). Shut down. * * @param cls closure */ static void shutdown_task (void *cls) { (void) cls; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Running shutdown\n"); if (NULL != task) { GNUNET_SCHEDULER_cancel (task); task = NULL; } db_plugin->rollback (db_plugin->cls); /* just in case */ TALER_EXCHANGEDB_plugin_unload (db_plugin); db_plugin = NULL; TALER_EXCHANGEDB_unload_accounts (); cfg = NULL; } /** * Parse the configuration for drain. * * @return #GNUNET_OK on success */ static enum GNUNET_GenericReturnValue parse_drain_config (void) { if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg, "exchange", "BASE_URL", &exchange_base_url)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "exchange", "BASE_URL"); return GNUNET_SYSERR; } { char *master_public_key_str; if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg, "exchange", "MASTER_PUBLIC_KEY", &master_public_key_str)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "exchange", "MASTER_PUBLIC_KEY"); return GNUNET_SYSERR; } if (GNUNET_OK != GNUNET_CRYPTO_eddsa_public_key_from_string (master_public_key_str, strlen ( master_public_key_str), &master_pub.eddsa_pub)) { GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, "exchange", "MASTER_PUBLIC_KEY", "invalid base32 encoding for a master public key"); GNUNET_free (master_public_key_str); return GNUNET_SYSERR; } GNUNET_free (master_public_key_str); } if (NULL == (db_plugin = TALER_EXCHANGEDB_plugin_load (cfg))) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to initialize DB subsystem\n"); return GNUNET_SYSERR; } if (GNUNET_OK != TALER_EXCHANGEDB_load_accounts (cfg, TALER_EXCHANGEDB_ALO_DEBIT | TALER_EXCHANGEDB_ALO_AUTHDATA)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "No wire accounts configured for debit!\n"); TALER_EXCHANGEDB_plugin_unload (db_plugin); db_plugin = NULL; return GNUNET_SYSERR; } return GNUNET_OK; } /** * Perform a database commit. If it fails, print a warning. * * @return status of commit */ static enum GNUNET_DB_QueryStatus commit_or_warn (void) { enum GNUNET_DB_QueryStatus qs; qs = db_plugin->commit (db_plugin->cls); if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) return qs; GNUNET_log ((GNUNET_DB_STATUS_SOFT_ERROR == qs) ? GNUNET_ERROR_TYPE_INFO : GNUNET_ERROR_TYPE_ERROR, "Failed to commit database transaction!\n"); return qs; } /** * Execute a wire drain. * * @param cls NULL */ static void run_drain (void *cls) { enum GNUNET_DB_QueryStatus qs; uint64_t serial; struct TALER_WireTransferIdentifierRawP wtid; char *account_section; char *payto_uri; struct GNUNET_TIME_Timestamp request_timestamp; struct TALER_Amount amount; struct TALER_MasterSignatureP master_sig; (void) cls; task = NULL; if (GNUNET_OK != db_plugin->start (db_plugin->cls, "run drain")) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to start database transaction!\n"); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; } qs = db_plugin->profit_drains_get_pending (db_plugin->cls, &serial, &wtid, &account_section, &payto_uri, &request_timestamp, &amount, &master_sig); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: db_plugin->rollback (db_plugin->cls); GNUNET_break (0); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; case GNUNET_DB_STATUS_SOFT_ERROR: db_plugin->rollback (db_plugin->cls); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Serialization failure on simple SELECT!?\n"); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: /* no profit drains, finished */ db_plugin->rollback (db_plugin->cls); GNUNET_assert (NULL == task); GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, "No profit drains pending. Exiting.\n"); GNUNET_SCHEDULER_shutdown (); return; default: /* continued below */ break; } /* Check signature (again, this is a critical operation!) */ if (GNUNET_OK != TALER_exchange_offline_profit_drain_verify ( &wtid, request_timestamp, &amount, account_section, payto_uri, &master_pub, &master_sig)) { GNUNET_break (0); global_ret = EXIT_FAILURE; db_plugin->rollback (db_plugin->cls); GNUNET_assert (NULL == task); GNUNET_SCHEDULER_shutdown (); return; } /* Display data for manual human check */ fprintf (stdout, "Critical operation. MANUAL CHECK REQUIRED.\n"); fprintf (stdout, "We will wire %s to `%s'\n based on instructions from %s.\n", TALER_amount2s (&amount), payto_uri, GNUNET_TIME_timestamp2s (request_timestamp)); fprintf (stdout, "Press ENTER to confirm, CTRL-D to abort.\n"); while (1) { int key; key = getchar (); if (EOF == key) { fprintf (stdout, "Transfer aborted.\n" "Re-run 'taler-exchange-drain' to try it again.\n" "Contact Taler Systems SA to cancel it for good.\n" "Exiting.\n"); db_plugin->rollback (db_plugin->cls); GNUNET_assert (NULL == task); GNUNET_SCHEDULER_shutdown (); global_ret = EXIT_FAILURE; return; } if ('\n' == key) break; } /* Note: account_section ignored for now, we might want to use it here in the future... */ (void) account_section; { char *method; void *buf; size_t buf_size; TALER_BANK_prepare_transfer (payto_uri, &amount, exchange_base_url, &wtid, &buf, &buf_size); method = TALER_payto_get_method (payto_uri); qs = db_plugin->wire_prepare_data_insert (db_plugin->cls, method, buf, buf_size); GNUNET_free (method); GNUNET_free (buf); } qs = db_plugin->profit_drains_set_finished (db_plugin->cls, serial); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: db_plugin->rollback (db_plugin->cls); GNUNET_break (0); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; case GNUNET_DB_STATUS_SOFT_ERROR: db_plugin->rollback (db_plugin->cls); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed: database serialization issue\n"); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: db_plugin->rollback (db_plugin->cls); GNUNET_assert (NULL == task); GNUNET_break (0); GNUNET_SCHEDULER_shutdown (); return; default: /* continued below */ break; } /* commit transaction + report success + exit */ if (0 >= commit_or_warn ()) GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, "Profit drain triggered. Exiting.\n"); GNUNET_SCHEDULER_shutdown (); } /** * First task. * * @param cls closure, NULL * @param args remaining command-line arguments * @param cfgfile name of the configuration file used (for saving, can be NULL!) * @param c configuration */ static void run (void *cls, char *const *args, const char *cfgfile, const struct GNUNET_CONFIGURATION_Handle *c) { (void) cls; (void) args; (void) cfgfile; cfg = c; if (GNUNET_OK != parse_drain_config ()) { cfg = NULL; global_ret = EXIT_NOTCONFIGURED; return; } if (GNUNET_SYSERR == db_plugin->preflight (db_plugin->cls)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to obtain database connection!\n"); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; } GNUNET_assert (NULL == task); task = GNUNET_SCHEDULER_add_now (&run_drain, NULL); GNUNET_SCHEDULER_add_shutdown (&shutdown_task, cls); } /** * The main function of the taler-exchange-drain. * * @param argc number of arguments from the command line * @param argv command line arguments * @return 0 ok, 1 on error */ int main (int argc, char *const *argv) { struct GNUNET_GETOPT_CommandLineOption options[] = { GNUNET_GETOPT_option_version (VERSION "-" VCS_VERSION), GNUNET_GETOPT_OPTION_END }; enum GNUNET_GenericReturnValue ret; if (GNUNET_OK != GNUNET_STRINGS_get_utf8_args (argc, argv, &argc, &argv)) return EXIT_INVALIDARGUMENT; TALER_OS_init (); ret = GNUNET_PROGRAM_run ( argc, argv, "taler-exchange-drain", gettext_noop ( "process that executes a single profit drain"), options, &run, NULL); GNUNET_free_nz ((void *) argv); if (GNUNET_SYSERR == ret) return EXIT_INVALIDARGUMENT; if (GNUNET_NO == ret) return EXIT_SUCCESS; return global_ret; } /* end of taler-exchange-drain.c */