taler-aggregator-benchmark.c (18408B)
1 /* 2 This file is part of TALER 3 (C) 2021, 2024 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it 6 under the terms of the GNU Affero 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 GNU 13 General Public License for more details. 14 15 You should have received a copy of the GNU General Public License 16 along with TALER; see the file COPYING. If not, 17 see <http://www.gnu.org/licenses/> 18 */ 19 /** 20 * @file benchmark/taler-aggregator-benchmark.c 21 * @brief Setup exchange database suitable for aggregator benchmarking 22 * @author Christian Grothoff 23 */ 24 #include "platform.h" 25 #include <jansson.h> 26 #include <gnunet/gnunet_util_lib.h> 27 #include <gnunet/gnunet_json_lib.h> 28 #include "taler/taler_util.h" 29 #include "taler/taler_signatures.h" 30 #include "exchangedb_lib.h" 31 #include "taler/taler_json_lib.h" 32 #include "taler/taler_error_codes.h" 33 #include "exchange-database/rollback.h" 34 #include "exchange-database/start.h" 35 #include "exchange-database/commit.h" 36 #include "exchange-database/preflight.h" 37 #include "exchange-database/insert_refund.h" 38 #include "exchange-database/do_deposit.h" 39 #include "exchange-database/insert_wire_fee.h" 40 #include "exchange-database/insert_denomination_info.h" 41 #include "exchange-database/ensure_coin_known.h" 42 43 /** 44 * Exit code. 45 */ 46 static int global_ret; 47 48 /** 49 * How many deposits we want to create per merchant. 50 */ 51 static unsigned int howmany_deposits = 1; 52 53 /** 54 * How many merchants do we want to setup. 55 */ 56 static unsigned int howmany_merchants = 1; 57 58 /** 59 * Probability of a refund, as in $NUMBER:100. 60 * Use 0 for no refunds. 61 */ 62 static unsigned int refund_rate = 0; 63 64 /** 65 * Currency used. 66 */ 67 static char *currency; 68 69 /** 70 * Configuration. 71 */ 72 static const struct GNUNET_CONFIGURATION_Handle *cfg; 73 74 /** 75 * Database context. 76 */ 77 static struct TALER_EXCHANGEDB_PostgresContext *pg; 78 79 /** 80 * Main task doing the work(). 81 */ 82 static struct GNUNET_SCHEDULER_Task *task; 83 84 /** 85 * Hash of the denomination. 86 */ 87 static struct TALER_DenominationHashP h_denom_pub; 88 89 /** 90 * "signature" to use for the coin(s). 91 */ 92 static struct TALER_DenominationSignature denom_sig; 93 94 /** 95 * Time range when deposits start. 96 */ 97 static struct GNUNET_TIME_Timestamp start; 98 99 /** 100 * Time range when deposits end. 101 */ 102 static struct GNUNET_TIME_Timestamp end; 103 104 105 /** 106 * Throw a weighted coin with @a probability. 107 * 108 * @return #GNUNET_OK with @a probability, 109 * #GNUNET_NO with 1 - @a probability 110 */ 111 static unsigned int 112 eval_probability (float probability) 113 { 114 uint64_t random; 115 float random_01; 116 117 if (probability <= 0.0f) 118 return GNUNET_NO; 119 random = GNUNET_CRYPTO_random_u64 (UINT64_MAX); 120 random_01 = (double) random / (double) UINT64_MAX; 121 return (random_01 <= probability) ? GNUNET_OK : GNUNET_NO; 122 } 123 124 125 /** 126 * Randomize data at pointer @a x 127 * 128 * @param x pointer to data to randomize 129 */ 130 #define RANDOMIZE(x) \ 131 GNUNET_CRYPTO_random_block (x, sizeof (*x)) 132 133 134 /** 135 * Initialize @a out with an amount given by @a val and 136 * @a frac using the main "currency". 137 * 138 * @param val value to set 139 * @param frac fraction to set 140 * @param[out] out where to write the amount 141 */ 142 static void 143 make_amount (unsigned int val, 144 unsigned int frac, 145 struct TALER_Amount *out) 146 { 147 GNUNET_assert (GNUNET_OK == 148 TALER_amount_set_zero (currency, 149 out)); 150 out->value = val; 151 out->fraction = frac; 152 } 153 154 155 /** 156 * Create random-ish timestamp. 157 * 158 * @return time stamp between start and end 159 */ 160 static struct GNUNET_TIME_Timestamp 161 random_time (void) 162 { 163 uint64_t delta; 164 struct GNUNET_TIME_Absolute ret; 165 166 delta = end.abs_time.abs_value_us - start.abs_time.abs_value_us; 167 delta = GNUNET_CRYPTO_random_u64 (delta); 168 ret.abs_value_us = start.abs_time.abs_value_us + delta; 169 return GNUNET_TIME_absolute_to_timestamp (ret); 170 } 171 172 173 /** 174 * Function run on shutdown. 175 * 176 * @param cls unused 177 */ 178 static void 179 do_shutdown (void *cls) 180 { 181 (void) cls; 182 if (NULL != pg) 183 { 184 TALER_EXCHANGEDB_disconnect (pg); 185 pg = NULL; 186 } 187 if (NULL != task) 188 { 189 GNUNET_SCHEDULER_cancel (task); 190 task = NULL; 191 } 192 TALER_denom_sig_free (&denom_sig); 193 } 194 195 196 struct Merchant 197 { 198 199 /** 200 * Public key of the merchant. Enables later identification 201 * of the merchant in case of a need to rollback transactions. 202 */ 203 struct TALER_MerchantPublicKeyP merchant_pub; 204 205 /** 206 * Hash of the (canonical) representation of @e wire, used 207 * to check the signature on the request. Generated by 208 * the exchange from the detailed wire data provided by the 209 * merchant. 210 */ 211 struct TALER_MerchantWireHashP h_wire; 212 213 /** 214 * Salt used when computing @e h_wire. 215 */ 216 struct TALER_WireSaltP wire_salt; 217 218 /** 219 * Account information for the merchant. 220 */ 221 struct TALER_FullPayto payto_uri; 222 223 }; 224 225 struct Deposit 226 { 227 228 /** 229 * Information about the coin that is being deposited. 230 */ 231 struct TALER_CoinPublicInfo coin; 232 233 /** 234 * Hash over the proposal data between merchant and customer 235 * (remains unknown to the Exchange). 236 */ 237 struct TALER_PrivateContractHashP h_contract_terms; 238 239 }; 240 241 242 /** 243 * Add a refund from @a m for @a d. 244 * 245 * @param m merchant granting the refund 246 * @param d deposit being refunded 247 * @return true on success 248 */ 249 static bool 250 add_refund (const struct Merchant *m, 251 const struct Deposit *d) 252 { 253 struct TALER_EXCHANGEDB_Refund r; 254 255 r.coin = d->coin; 256 r.details.merchant_pub = m->merchant_pub; 257 RANDOMIZE (&r.details.merchant_sig); 258 r.details.h_contract_terms = d->h_contract_terms; 259 r.details.rtransaction_id = 42; 260 make_amount (0, 5000000, &r.details.refund_amount); 261 make_amount (0, 5, &r.details.refund_fee); 262 if (0 >= 263 TALER_EXCHANGEDB_insert_refund (pg, 264 &r)) 265 { 266 GNUNET_break (0); 267 global_ret = EXIT_FAILURE; 268 GNUNET_SCHEDULER_shutdown (); 269 return false; 270 } 271 return true; 272 } 273 274 275 /** 276 * Add a (random-ish) deposit for merchant @a m. 277 * 278 * @param m merchant to receive the deposit 279 * @return true on success 280 */ 281 static bool 282 add_deposit (const struct Merchant *m) 283 { 284 struct Deposit d; 285 struct TALER_EXCHANGEDB_CoinDepositInformation deposit; 286 struct TALER_EXCHANGEDB_BatchDeposit bd = { 287 .cdis = &deposit, 288 .num_cdis = 1 289 }; 290 uint64_t known_coin_id; 291 struct TALER_DenominationHashP dph; 292 struct TALER_AgeCommitmentHashP agh; 293 294 RANDOMIZE (&d.coin.coin_pub); 295 d.coin.denom_pub_hash = h_denom_pub; 296 d.coin.denom_sig = denom_sig; 297 RANDOMIZE (&d.h_contract_terms); 298 d.coin.no_age_commitment = true; 299 memset (&d.coin.h_age_commitment, 300 0, 301 sizeof (d.coin.h_age_commitment)); 302 303 if (0 >= 304 TALER_EXCHANGEDB_ensure_coin_known (pg, 305 &d.coin, 306 &known_coin_id, 307 &dph, 308 &agh)) 309 { 310 GNUNET_break (0); 311 global_ret = EXIT_FAILURE; 312 GNUNET_SCHEDULER_shutdown (); 313 return false; 314 } 315 deposit.coin = d.coin; 316 RANDOMIZE (&deposit.csig); 317 bd.merchant_pub = m->merchant_pub; 318 bd.h_contract_terms = d.h_contract_terms; 319 bd.wire_salt = m->wire_salt; 320 bd.receiver_wire_account = m->payto_uri; 321 bd.wallet_timestamp = random_time (); 322 do { 323 bd.refund_deadline = random_time (); 324 bd.wire_deadline = random_time (); 325 } while (GNUNET_TIME_timestamp_cmp (bd.wire_deadline, 326 <, 327 bd.refund_deadline)); 328 329 make_amount (1, 330 0, 331 &deposit.amount_with_fee); 332 333 { 334 struct GNUNET_TIME_Timestamp now; 335 struct TALER_Amount deposit_fee; 336 struct TALER_Amount total; 337 bool balance_ok; 338 uint32_t bad_idx; 339 bool conflict; 340 341 GNUNET_assert (1 == bd.num_cdis); 342 make_amount (0, 343 0, 344 &deposit_fee); 345 now = random_time (); 346 if (0 >= 347 TALER_EXCHANGEDB_do_deposit (pg, 348 &bd, 349 &deposit_fee, 350 &now, 351 &total, 352 &balance_ok, 353 &bad_idx, 354 &conflict)) 355 { 356 GNUNET_break (0); 357 global_ret = EXIT_FAILURE; 358 GNUNET_SCHEDULER_shutdown (); 359 return false; 360 } 361 } 362 if (GNUNET_YES == 363 eval_probability (((float) refund_rate) / 100.0f)) 364 return add_refund (m, 365 &d); 366 return true; 367 } 368 369 370 /** 371 * Function to do the work. 372 * 373 * @param cls unused 374 */ 375 static void 376 work (void *cls) 377 { 378 struct Merchant m; 379 uint64_t rnd1; 380 uint64_t rnd2; 381 382 (void) cls; 383 task = NULL; 384 rnd1 = GNUNET_CRYPTO_random_u64 (UINT64_MAX); 385 rnd2 = GNUNET_CRYPTO_random_u64 (UINT64_MAX); 386 GNUNET_asprintf (&m.payto_uri.full_payto, 387 "payto://x-taler-bank/localhost:8082/account-%llX-%llX", 388 (unsigned long long) rnd1, 389 (unsigned long long) rnd2); 390 RANDOMIZE (&m.merchant_pub); 391 RANDOMIZE (&m.wire_salt); 392 TALER_merchant_wire_signature_hash (m.payto_uri, 393 &m.wire_salt, 394 &m.h_wire); 395 if (GNUNET_OK != 396 TALER_EXCHANGEDB_start (pg, 397 "aggregator-benchmark-fill")) 398 { 399 GNUNET_break (0); 400 global_ret = EXIT_FAILURE; 401 goto exit; 402 } 403 for (unsigned int i = 0; i<howmany_deposits; i++) 404 { 405 if (! add_deposit (&m)) 406 { 407 global_ret = EXIT_FAILURE; 408 goto exit; 409 } 410 } 411 if (0 <= 412 TALER_EXCHANGEDB_commit (pg)) 413 { 414 if (0 == --howmany_merchants) 415 { 416 goto exit; 417 } 418 } 419 else 420 { 421 /* Note: on persistent failures, this could 422 re-try forever; OK because this is a benchmark */ 423 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 424 "Failed to commit, will try again\n"); 425 } 426 GNUNET_free (m.payto_uri.full_payto); 427 task = GNUNET_SCHEDULER_add_now (&work, 428 NULL); 429 return; 430 exit: 431 GNUNET_SCHEDULER_shutdown (); 432 GNUNET_free (m.payto_uri.full_payto); 433 return; 434 } 435 436 437 /** 438 * Actual execution. 439 * 440 * @param cls unused 441 * @param args remaining command-line arguments 442 * @param cfgfile name of the configuration file used (for saving, can be NULL!) 443 * @param c configuration 444 */ 445 static void 446 run (void *cls, 447 char *const *args, 448 const char *cfgfile, 449 const struct GNUNET_CONFIGURATION_Handle *c) 450 { 451 struct TALER_EXCHANGEDB_DenominationKeyInformation issue; 452 453 (void) cls; 454 (void) args; 455 (void) cfgfile; 456 /* make sure everything 'ends' before the current time, 457 so that the aggregator will process everything without 458 need for time-travel */ 459 end = GNUNET_TIME_timestamp_get (); 460 start = GNUNET_TIME_absolute_to_timestamp ( 461 GNUNET_TIME_absolute_subtract (end.abs_time, 462 GNUNET_TIME_UNIT_MONTHS)); 463 cfg = c; 464 if (GNUNET_OK != 465 TALER_config_get_currency (cfg, 466 "exchange", 467 ¤cy)) 468 { 469 global_ret = EXIT_NOTCONFIGURED; 470 return; 471 } 472 pg = TALER_EXCHANGEDB_connect (cfg); 473 if (NULL == pg) 474 { 475 global_ret = EXIT_NOTCONFIGURED; 476 return; 477 } 478 if (GNUNET_SYSERR == 479 TALER_EXCHANGEDB_preflight (pg)) 480 { 481 global_ret = EXIT_FAILURE; 482 TALER_EXCHANGEDB_disconnect (pg); 483 return; 484 } 485 GNUNET_SCHEDULER_add_shutdown (&do_shutdown, 486 NULL); 487 memset (&issue, 488 0, 489 sizeof (issue)); 490 RANDOMIZE (&issue.signature); 491 issue.start 492 = start; 493 issue.expire_withdraw 494 = GNUNET_TIME_absolute_to_timestamp ( 495 GNUNET_TIME_absolute_add (start.abs_time, 496 GNUNET_TIME_UNIT_DAYS)); 497 issue.expire_deposit 498 = end; 499 issue.expire_legal 500 = GNUNET_TIME_absolute_to_timestamp ( 501 GNUNET_TIME_absolute_add (end.abs_time, 502 GNUNET_TIME_UNIT_YEARS)); 503 { 504 struct TALER_DenominationPrivateKey pk; 505 struct TALER_DenominationPublicKey denom_pub; 506 struct TALER_CoinPubHashP c_hash; 507 struct TALER_PlanchetDetail pd; 508 struct TALER_BlindedDenominationSignature bds; 509 struct TALER_PlanchetMasterSecretP ps; 510 struct TALER_CoinSpendPublicKeyP coin_pub; 511 struct TALER_AgeCommitmentHashP hac; 512 union GNUNET_CRYPTO_BlindingSecretP bks; 513 const struct TALER_ExchangeBlindingValues *alg_values; 514 515 RANDOMIZE (&coin_pub); 516 GNUNET_assert (GNUNET_OK == 517 TALER_denom_priv_create (&pk, 518 &denom_pub, 519 GNUNET_CRYPTO_BSA_RSA, 520 1024)); 521 alg_values = TALER_denom_ewv_rsa_singleton (); 522 denom_pub.age_mask = issue.age_mask; 523 TALER_denom_pub_hash (&denom_pub, 524 &h_denom_pub); 525 make_amount (2, 0, &issue.value); 526 make_amount (0, 5, &issue.fees.withdraw); 527 make_amount (0, 5, &issue.fees.deposit); 528 make_amount (0, 5, &issue.fees.refresh); 529 make_amount (0, 5, &issue.fees.refund); 530 issue.denom_hash = h_denom_pub; 531 if (0 >= 532 TALER_EXCHANGEDB_insert_denomination_info (pg, 533 &denom_pub, 534 &issue)) 535 { 536 GNUNET_break (0); 537 GNUNET_SCHEDULER_shutdown (); 538 global_ret = EXIT_FAILURE; 539 return; 540 } 541 542 TALER_planchet_blinding_secret_create (&ps, 543 TALER_denom_ewv_rsa_singleton (), 544 &bks); 545 546 { 547 struct GNUNET_HashCode seed; 548 struct TALER_AgeMask mask = { 549 .bits = 1 | (1 << 8) | (1 << 12) | (1 << 16) | (1 << 18) 550 }; 551 struct TALER_AgeCommitmentProof acp = {0}; 552 553 GNUNET_CRYPTO_random_block (&seed, 554 sizeof(seed)); 555 TALER_age_restriction_commit (&mask, 556 13, 557 &seed, 558 &acp); 559 TALER_age_commitment_hash (&acp.commitment, 560 &hac); 561 } 562 563 GNUNET_assert (GNUNET_OK == 564 TALER_denom_blind (&denom_pub, 565 &bks, 566 NULL, 567 &hac, 568 &coin_pub, 569 alg_values, 570 &c_hash, 571 &pd.blinded_planchet)); 572 GNUNET_assert (GNUNET_OK == 573 TALER_denom_sign_blinded (&bds, 574 &pk, 575 false, 576 &pd.blinded_planchet)); 577 TALER_blinded_planchet_free (&pd.blinded_planchet); 578 GNUNET_assert (GNUNET_OK == 579 TALER_denom_sig_unblind (&denom_sig, 580 &bds, 581 &bks, 582 &c_hash, 583 alg_values, 584 &denom_pub)); 585 TALER_blinded_denom_sig_free (&bds); 586 TALER_denom_pub_free (&denom_pub); 587 TALER_denom_priv_free (&pk); 588 } 589 590 { 591 struct TALER_WireFeeSet fees; 592 struct TALER_MasterSignatureP master_sig; 593 unsigned int year; 594 struct GNUNET_TIME_Timestamp ws; 595 struct GNUNET_TIME_Timestamp we; 596 597 year = GNUNET_TIME_get_current_year (); 598 for (unsigned int y = year - 1; y<year + 2; y++) 599 { 600 ws = GNUNET_TIME_absolute_to_timestamp (GNUNET_TIME_year_to_time (y - 1)); 601 we = GNUNET_TIME_absolute_to_timestamp (GNUNET_TIME_year_to_time (y)); 602 make_amount (0, 5, &fees.wire); 603 make_amount (0, 5, &fees.closing); 604 memset (&master_sig, 605 0, 606 sizeof (master_sig)); 607 if (0 > 608 TALER_TALER_EXCHANGEDB_insert_wire_fee (pg, 609 "x-taler-bank", 610 ws, 611 we, 612 &fees, 613 &master_sig)) 614 { 615 GNUNET_break (0); 616 GNUNET_SCHEDULER_shutdown (); 617 global_ret = EXIT_FAILURE; 618 return; 619 } 620 } 621 } 622 623 task = GNUNET_SCHEDULER_add_now (&work, 624 NULL); 625 } 626 627 628 /** 629 * The main function of the taler-aggregator-benchmark tool. 630 * 631 * @param argc number of arguments from the command line 632 * @param argv command line arguments 633 * @return 0 ok, non-zero on failure 634 */ 635 int 636 main (int argc, 637 char *const *argv) 638 { 639 struct GNUNET_GETOPT_CommandLineOption options[] = { 640 GNUNET_GETOPT_option_uint ('d', 641 "deposits", 642 "DN", 643 "How many deposits we should instantiate per merchant", 644 &howmany_deposits), 645 GNUNET_GETOPT_option_uint ('m', 646 "merchants", 647 "DM", 648 "How many merchants should we create", 649 &howmany_merchants), 650 GNUNET_GETOPT_option_uint ('r', 651 "refunds", 652 "RATE", 653 "Probability of refund per deposit (0-100)", 654 &refund_rate), 655 GNUNET_GETOPT_OPTION_END 656 }; 657 enum GNUNET_GenericReturnValue result; 658 659 unsetenv ("XDG_DATA_HOME"); 660 unsetenv ("XDG_CONFIG_HOME"); 661 if (0 >= 662 (result = GNUNET_PROGRAM_run ( 663 TALER_EXCHANGE_project_data (), 664 argc, 665 argv, 666 "taler-aggregator-benchmark", 667 "generate database to benchmark the aggregator", 668 options, 669 &run, 670 NULL))) 671 { 672 if (GNUNET_NO == result) 673 return EXIT_SUCCESS; 674 return EXIT_INVALIDARGUMENT; 675 } 676 return global_ret; 677 }