testing_api_cmd_batch_withdraw.c (16317B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2018-2025 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it 6 under the terms of the GNU General Public License as published by 7 the Free Software Foundation; either version 3, or (at your 8 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 16 License along with TALER; see the file COPYING. If not, see 17 <http://www.gnu.org/licenses/> 18 */ 19 /** 20 * @file testing/testing_api_cmd_batch_withdraw.c 21 * @brief implements the batch withdraw command 22 * @author Christian Grothoff 23 * @author Marcello Stanisci 24 * @author Özgür Kesim 25 */ 26 #include "taler/platform.h" 27 #include "taler/taler_exchange_service.h" 28 #include "taler/taler_json_lib.h" 29 #include <microhttpd.h> 30 #include <gnunet/gnunet_curl_lib.h> 31 #include "taler/taler_signatures.h" 32 #include "taler/taler_extensions.h" 33 #include "taler/taler_testing_lib.h" 34 35 /** 36 * Information we track per withdrawn coin. 37 */ 38 struct CoinState 39 { 40 41 /** 42 * String describing the denomination value we should withdraw. 43 * A corresponding denomination key must exist in the exchange's 44 * offerings. Can be NULL if @e pk is set instead. 45 */ 46 struct TALER_Amount amount; 47 48 /** 49 * If @e amount is NULL, this specifies the denomination key to 50 * use. Otherwise, this will be set (by the interpreter) to the 51 * denomination PK matching @e amount. 52 */ 53 struct TALER_EXCHANGE_DenomPublicKey *pk; 54 55 /** 56 * Coin Details, as returned by the withdrawal operation 57 */ 58 struct TALER_EXCHANGE_WithdrawCoinPrivateDetails details; 59 60 /** 61 * Set (by the interpreter) to the exchange's signature over the 62 * coin's public key. 63 */ 64 struct TALER_BlindedDenominationSignature blinded_denom_sig; 65 66 /** 67 * Private key material of the coin, set by the interpreter. 68 */ 69 struct TALER_PlanchetMasterSecretP secret; 70 71 72 }; 73 74 75 /** 76 * State for a "batch withdraw" CMD. 77 */ 78 struct BatchWithdrawState 79 { 80 81 /** 82 * Which reserve should we withdraw from? 83 */ 84 const char *reserve_reference; 85 86 /** 87 * Exchange base URL. Only used as offered trait. 88 */ 89 char *exchange_url; 90 91 /** 92 * URI if the reserve we are withdrawing from. 93 */ 94 struct TALER_NormalizedPayto reserve_payto_uri; 95 96 /** 97 * Private key of the reserve we are withdrawing from. 98 */ 99 struct TALER_ReservePrivateKeyP reserve_priv; 100 101 /** 102 * Public key of the reserve we are withdrawing from. 103 */ 104 struct TALER_ReservePublicKeyP reserve_pub; 105 106 /** 107 * Interpreter state (during command). 108 */ 109 struct TALER_TESTING_Interpreter *is; 110 111 /** 112 * Withdraw handle (while operation is running). 113 */ 114 struct TALER_EXCHANGE_WithdrawHandle *wsh; 115 116 /** 117 * Array of coin states. 118 */ 119 struct CoinState *coins; 120 121 /** 122 * The seed from which the batch of seeds for the coins is derived 123 */ 124 struct TALER_WithdrawMasterSeedP seed; 125 126 127 /** 128 * Set to the KYC requirement payto hash *if* the exchange replied with a 129 * request for KYC. 130 */ 131 struct TALER_NormalizedPaytoHashP h_payto; 132 133 /** 134 * Set to the KYC requirement row *if* the exchange replied with 135 * a request for KYC. 136 */ 137 uint64_t requirement_row; 138 139 /** 140 * Length of the @e coins array. 141 */ 142 unsigned int num_coins; 143 144 /** 145 * An age > 0 signifies age restriction is applied. 146 * Same for all coins in the batch. 147 */ 148 uint8_t age; 149 150 /** 151 * Expected HTTP response code to the request. 152 */ 153 unsigned int expected_response_code; 154 155 156 /** 157 * Reserve history entry that corresponds to this withdrawal. 158 * Will be of type #TALER_EXCHANGE_RTT_WITHDRAWAL. 159 */ 160 struct TALER_EXCHANGE_ReserveHistoryEntry reserve_history; 161 162 /** 163 * The commitment of the call to withdraw, needed later for recoup. 164 */ 165 struct TALER_HashBlindedPlanchetsP planchets_h; 166 167 }; 168 169 170 /** 171 * "batch withdraw" operation callback; checks that the 172 * response code is expected and store the exchange signature 173 * in the state. 174 * 175 * @param cls closure. 176 * @param wr withdraw response details 177 */ 178 static void 179 batch_withdraw_cb (void *cls, 180 const struct 181 TALER_EXCHANGE_WithdrawResponse *wr) 182 { 183 struct BatchWithdrawState *ws = cls; 184 struct TALER_TESTING_Interpreter *is = ws->is; 185 186 ws->wsh = NULL; 187 if (ws->expected_response_code != wr->hr.http_status) 188 { 189 TALER_TESTING_unexpected_status_with_body (is, 190 wr->hr.http_status, 191 ws->expected_response_code, 192 wr->hr.reply); 193 return; 194 } 195 switch (wr->hr.http_status) 196 { 197 case MHD_HTTP_OK: 198 for (unsigned int i = 0; i<ws->num_coins; i++) 199 { 200 struct CoinState *cs = &ws->coins[i]; 201 202 cs->details = wr->details.ok.coin_details[i]; 203 TALER_denom_sig_copy (&cs->details.denom_sig, 204 &wr->details.ok.coin_details[i].denom_sig); 205 TALER_denom_ewv_copy (&cs->details.blinding_values, 206 &wr->details.ok.coin_details[i].blinding_values); 207 } 208 ws->planchets_h = wr->details.ok.planchets_h; 209 break; 210 case MHD_HTTP_FORBIDDEN: 211 /* nothing to check */ 212 break; 213 case MHD_HTTP_NOT_FOUND: 214 /* nothing to check */ 215 break; 216 case MHD_HTTP_CONFLICT: 217 /* FIXME[oec]: Check if age-requirement is the reason */ 218 break; 219 case MHD_HTTP_GONE: 220 /* theoretically could check that the key was actually */ 221 break; 222 case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: 223 /* nothing to check */ 224 ws->requirement_row 225 = wr->details.unavailable_for_legal_reasons.requirement_row; 226 ws->h_payto 227 = wr->details.unavailable_for_legal_reasons.h_payto; 228 break; 229 default: 230 /* Unsupported status code (by test harness) */ 231 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 232 "Batch withdraw test command does not support status code %u\n", 233 wr->hr.http_status); 234 GNUNET_break (0); 235 break; 236 } 237 TALER_TESTING_interpreter_next (is); 238 } 239 240 241 /** 242 * Run the command. 243 */ 244 static void 245 batch_withdraw_run (void *cls, 246 const struct TALER_TESTING_Command *cmd, 247 struct TALER_TESTING_Interpreter *is) 248 { 249 struct BatchWithdrawState *ws = cls; 250 struct TALER_EXCHANGE_Keys *keys = TALER_TESTING_get_keys (is); 251 const struct TALER_ReservePrivateKeyP *rp; 252 const struct TALER_TESTING_Command *create_reserve; 253 const struct TALER_EXCHANGE_DenomPublicKey *dpk; 254 struct TALER_EXCHANGE_DenomPublicKey denoms_pub[ws->num_coins]; 255 struct TALER_PlanchetMasterSecretP secrets[ws->num_coins]; 256 257 258 (void) cmd; 259 ws->is = is; 260 create_reserve 261 = TALER_TESTING_interpreter_lookup_command ( 262 is, 263 ws->reserve_reference); 264 265 if (NULL == create_reserve) 266 { 267 GNUNET_break (0); 268 TALER_TESTING_interpreter_fail (is); 269 return; 270 } 271 if (GNUNET_OK != 272 TALER_TESTING_get_trait_reserve_priv (create_reserve, 273 &rp)) 274 { 275 GNUNET_break (0); 276 TALER_TESTING_interpreter_fail (is); 277 return; 278 } 279 if (NULL == ws->exchange_url) 280 ws->exchange_url 281 = GNUNET_strdup (TALER_TESTING_get_exchange_url (is)); 282 ws->reserve_priv = *rp; 283 GNUNET_CRYPTO_eddsa_key_get_public (&ws->reserve_priv.eddsa_priv, 284 &ws->reserve_pub.eddsa_pub); 285 ws->reserve_payto_uri 286 = TALER_reserve_make_payto (ws->exchange_url, 287 &ws->reserve_pub); 288 289 290 GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, 291 &ws->seed, 292 sizeof(ws->seed)); 293 294 /** 295 * This is the same expansion that happens inside the call to 296 * TALER_EXCHANGE_withdraw. We save the expanded 297 * secrets later per coin state. 298 */ 299 TALER_withdraw_expand_secrets (ws->num_coins, 300 &ws->seed, 301 secrets); 302 303 GNUNET_assert (ws->num_coins > 0); 304 GNUNET_assert (GNUNET_OK == 305 TALER_amount_set_zero ( 306 ws->coins[0].amount.currency, 307 &ws->reserve_history.amount)); 308 GNUNET_assert (GNUNET_OK == 309 TALER_amount_set_zero ( 310 ws->coins[0].amount.currency, 311 &ws->reserve_history.details.withdraw.fee)); 312 313 for (unsigned int i = 0; i<ws->num_coins; i++) 314 { 315 struct CoinState *cs = &ws->coins[i]; 316 struct TALER_Amount amount; 317 318 319 cs->secret = secrets[i]; 320 321 dpk = TALER_TESTING_find_pk (keys, 322 &cs->amount, 323 false); /* no age restriction */ 324 if (NULL == dpk) 325 { 326 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 327 "Failed to determine denomination key at %s\n", 328 (NULL != cmd) ? cmd->label : "<retried command>"); 329 GNUNET_break (0); 330 TALER_TESTING_interpreter_fail (is); 331 return; 332 } 333 /* We copy the denomination key, as re-querying /keys 334 * would free the old one. */ 335 cs->pk = TALER_EXCHANGE_copy_denomination_key (dpk); 336 337 GNUNET_assert (GNUNET_OK == 338 TALER_amount_set_zero ( 339 cs->amount.currency, 340 &amount)); 341 GNUNET_assert (0 <= 342 TALER_amount_add ( 343 &amount, 344 &cs->amount, 345 &cs->pk->fees.withdraw)); 346 GNUNET_assert (0 <= 347 TALER_amount_add ( 348 &ws->reserve_history.amount, 349 &ws->reserve_history.amount, 350 &amount)); 351 GNUNET_assert (0 <= 352 TALER_amount_add ( 353 &ws->reserve_history.details.withdraw.fee, 354 &ws->reserve_history.details.withdraw.fee, 355 &cs->pk->fees.withdraw)); 356 357 denoms_pub[i] = *cs->pk; 358 TALER_denom_pub_copy (&denoms_pub[i].key, 359 &cs->pk->key); 360 } 361 362 ws->reserve_history.type = TALER_EXCHANGE_RTT_WITHDRAWAL; 363 364 ws->wsh = TALER_EXCHANGE_withdraw ( 365 TALER_TESTING_interpreter_get_context (is), 366 keys, 367 TALER_TESTING_get_exchange_url (is), 368 rp, 369 ws->num_coins, 370 denoms_pub, 371 &ws->seed, 372 0, 373 &batch_withdraw_cb, 374 ws); 375 if (NULL == ws->wsh) 376 { 377 GNUNET_break (0); 378 TALER_TESTING_interpreter_fail (is); 379 return; 380 } 381 } 382 383 384 /** 385 * Free the state of a "withdraw" CMD, and possibly cancel 386 * a pending operation thereof. 387 * 388 * @param cls closure. 389 * @param cmd the command being freed. 390 */ 391 static void 392 batch_withdraw_cleanup (void *cls, 393 const struct TALER_TESTING_Command *cmd) 394 { 395 struct BatchWithdrawState *ws = cls; 396 397 if (NULL != ws->wsh) 398 { 399 TALER_TESTING_command_incomplete (ws->is, 400 cmd->label); 401 TALER_EXCHANGE_withdraw_cancel (ws->wsh); 402 ws->wsh = NULL; 403 } 404 for (unsigned int i = 0; i<ws->num_coins; i++) 405 { 406 struct CoinState *cs = &ws->coins[i]; 407 TALER_denom_ewv_free (&cs->details.blinding_values); 408 TALER_denom_sig_free (&cs->details.denom_sig); 409 if (NULL != cs->pk) 410 { 411 TALER_EXCHANGE_destroy_denomination_key (cs->pk); 412 cs->pk = NULL; 413 } 414 } 415 GNUNET_free (ws->coins); 416 GNUNET_free (ws->exchange_url); 417 GNUNET_free (ws->reserve_payto_uri.normalized_payto); 418 GNUNET_free (ws); 419 } 420 421 422 /** 423 * Offer internal data to a "withdraw" CMD state to other 424 * commands. 425 * 426 * @param cls closure 427 * @param[out] ret result (could be anything) 428 * @param trait name of the trait 429 * @param index index number of the object to offer. 430 * @return #GNUNET_OK on success 431 */ 432 static enum GNUNET_GenericReturnValue 433 batch_withdraw_traits (void *cls, 434 const void **ret, 435 const char *trait, 436 unsigned int index) 437 { 438 struct BatchWithdrawState *ws = cls; 439 struct CoinState *cs = &ws->coins[index]; 440 struct TALER_TESTING_Trait traits[] = { 441 /* history entry MUST be first due to response code logic below! */ 442 TALER_TESTING_make_trait_reserve_history (index, 443 &ws->reserve_history), 444 TALER_TESTING_make_trait_coin_priv (index, 445 &cs->details.coin_priv), 446 TALER_TESTING_make_trait_coin_pub (index, 447 &cs->details.coin_pub), 448 TALER_TESTING_make_trait_planchet_secrets (index, 449 &cs->secret), 450 TALER_TESTING_make_trait_blinding_key (index, 451 &cs->details.blinding_key), 452 TALER_TESTING_make_trait_exchange_blinding_values (index, 453 &cs->details. 454 blinding_values), 455 TALER_TESTING_make_trait_denom_pub (index, 456 cs->pk), 457 TALER_TESTING_make_trait_denom_sig (index, 458 &cs->details.denom_sig), 459 TALER_TESTING_make_trait_withdraw_seed (&ws->seed), 460 TALER_TESTING_make_trait_withdraw_commitment (&ws->planchets_h), 461 TALER_TESTING_make_trait_reserve_priv (&ws->reserve_priv), 462 TALER_TESTING_make_trait_reserve_pub (&ws->reserve_pub), 463 TALER_TESTING_make_trait_amounts (index, 464 &cs->amount), 465 TALER_TESTING_make_trait_legi_requirement_row (&ws->requirement_row), 466 TALER_TESTING_make_trait_h_normalized_payto (&ws->h_payto), 467 TALER_TESTING_make_trait_normalized_payto_uri (&ws->reserve_payto_uri), 468 TALER_TESTING_make_trait_exchange_url (ws->exchange_url), 469 TALER_TESTING_make_trait_age_commitment_proof (index, 470 ws->age > 0 ? 471 &cs->details. 472 age_commitment_proof: 473 NULL), 474 TALER_TESTING_make_trait_h_age_commitment (index, 475 ws->age > 0 ? 476 &cs->details.h_age_commitment : 477 NULL), 478 TALER_TESTING_trait_end () 479 }; 480 481 if (index >= ws->num_coins) 482 return GNUNET_NO; 483 return TALER_TESTING_get_trait ((ws->expected_response_code == MHD_HTTP_OK) 484 ? &traits[0] /* we have reserve history */ 485 : &traits[1], /* skip reserve history */ 486 ret, 487 trait, 488 index); 489 } 490 491 492 struct TALER_TESTING_Command 493 TALER_TESTING_cmd_batch_withdraw ( 494 const char *label, 495 const char *reserve_reference, 496 unsigned int expected_response_code, 497 const char *amount, 498 ...) 499 { 500 struct BatchWithdrawState *ws; 501 unsigned int cnt; 502 va_list ap; 503 504 ws = GNUNET_new (struct BatchWithdrawState); 505 ws->reserve_reference = reserve_reference; 506 ws->expected_response_code = expected_response_code; 507 508 cnt = 1; 509 va_start (ap, 510 amount); 511 while (NULL != (va_arg (ap, 512 const char *))) 513 cnt++; 514 ws->num_coins = cnt; 515 ws->coins = GNUNET_new_array (cnt, 516 struct CoinState); 517 va_end (ap); 518 va_start (ap, 519 amount); 520 for (unsigned int i = 0; i<ws->num_coins; i++) 521 { 522 struct CoinState *cs = &ws->coins[i]; 523 524 if (GNUNET_OK != 525 TALER_string_to_amount (amount, 526 &cs->amount)) 527 { 528 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 529 "Failed to parse amount `%s' at %s\n", 530 amount, 531 label); 532 GNUNET_assert (0); 533 } 534 /* move on to next vararg! */ 535 amount = va_arg (ap, 536 const char *); 537 } 538 GNUNET_assert (NULL == amount); 539 va_end (ap); 540 541 { 542 struct TALER_TESTING_Command cmd = { 543 .cls = ws, 544 .label = label, 545 .run = &batch_withdraw_run, 546 .cleanup = &batch_withdraw_cleanup, 547 .traits = &batch_withdraw_traits 548 }; 549 550 return cmd; 551 } 552 } 553 554 555 /* end of testing_api_cmd_batch_withdraw.c */