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 ¤cy_round_unit)) || 154 (TALER_amount_is_zero (¤cy_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 ¤cy_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 */