testing_api_cmd_batch_withdraw.c (16632B)
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_PostWithdrawHandle *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_PostWithdrawResponse *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 (void) cmd; 258 ws->is = is; 259 create_reserve 260 = TALER_TESTING_interpreter_lookup_command ( 261 is, 262 ws->reserve_reference); 263 264 if (NULL == create_reserve) 265 { 266 GNUNET_break (0); 267 TALER_TESTING_interpreter_fail (is); 268 return; 269 } 270 if (GNUNET_OK != 271 TALER_TESTING_get_trait_reserve_priv (create_reserve, 272 &rp)) 273 { 274 GNUNET_break (0); 275 TALER_TESTING_interpreter_fail (is); 276 return; 277 } 278 if (NULL == ws->exchange_url) 279 ws->exchange_url 280 = GNUNET_strdup (TALER_TESTING_get_exchange_url (is)); 281 ws->reserve_priv = *rp; 282 GNUNET_CRYPTO_eddsa_key_get_public (&ws->reserve_priv.eddsa_priv, 283 &ws->reserve_pub.eddsa_pub); 284 ws->reserve_payto_uri 285 = TALER_reserve_make_payto (ws->exchange_url, 286 &ws->reserve_pub); 287 288 GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, 289 &ws->seed, 290 sizeof(ws->seed)); 291 292 /** 293 * This is the same expansion that happens inside the call to 294 * TALER_EXCHANGE_withdraw. We save the expanded 295 * secrets later per coin state. 296 */ 297 TALER_withdraw_expand_secrets (ws->num_coins, 298 &ws->seed, 299 secrets); 300 301 GNUNET_assert (ws->num_coins > 0); 302 GNUNET_assert (GNUNET_OK == 303 TALER_amount_set_zero ( 304 ws->coins[0].amount.currency, 305 &ws->reserve_history.amount)); 306 GNUNET_assert (GNUNET_OK == 307 TALER_amount_set_zero ( 308 ws->coins[0].amount.currency, 309 &ws->reserve_history.details.withdraw.fee)); 310 311 for (unsigned int i = 0; i<ws->num_coins; i++) 312 { 313 struct CoinState *cs = &ws->coins[i]; 314 struct TALER_Amount amount; 315 316 317 cs->secret = secrets[i]; 318 319 dpk = TALER_TESTING_find_pk (keys, 320 &cs->amount, 321 false); /* no age restriction */ 322 if (NULL == dpk) 323 { 324 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 325 "Failed to determine denomination key at %s\n", 326 (NULL != cmd) ? cmd->label : "<retried command>"); 327 GNUNET_break (0); 328 TALER_TESTING_interpreter_fail (is); 329 return; 330 } 331 /* We copy the denomination key, as re-querying /keys 332 * would free the old one. */ 333 cs->pk = TALER_EXCHANGE_copy_denomination_key (dpk); 334 335 GNUNET_assert (GNUNET_OK == 336 TALER_amount_set_zero ( 337 cs->amount.currency, 338 &amount)); 339 GNUNET_assert (0 <= 340 TALER_amount_add ( 341 &amount, 342 &cs->amount, 343 &cs->pk->fees.withdraw)); 344 GNUNET_assert (0 <= 345 TALER_amount_add ( 346 &ws->reserve_history.amount, 347 &ws->reserve_history.amount, 348 &amount)); 349 GNUNET_assert (0 <= 350 TALER_amount_add ( 351 &ws->reserve_history.details.withdraw.fee, 352 &ws->reserve_history.details.withdraw.fee, 353 &cs->pk->fees.withdraw)); 354 355 denoms_pub[i] = *cs->pk; 356 TALER_denom_pub_copy (&denoms_pub[i].key, 357 &cs->pk->key); 358 } 359 360 ws->reserve_history.type = TALER_EXCHANGE_RTT_WITHDRAWAL; 361 362 ws->wsh = TALER_EXCHANGE_post_withdraw_create ( 363 TALER_TESTING_interpreter_get_context (is), 364 TALER_TESTING_get_exchange_url (is), 365 keys, 366 rp, 367 ws->num_coins, 368 denoms_pub, 369 &ws->seed, 370 0); 371 for (unsigned int i = 0; i<ws->num_coins; i++) 372 TALER_denom_pub_free (&denoms_pub[i].key); 373 if (NULL == ws->wsh) 374 { 375 GNUNET_break (0); 376 TALER_TESTING_interpreter_fail (is); 377 return; 378 } 379 GNUNET_assert (TALER_EC_NONE == 380 TALER_EXCHANGE_post_withdraw_start (ws->wsh, 381 &batch_withdraw_cb, 382 ws)); 383 } 384 385 386 /** 387 * Free the state of a "withdraw" CMD, and possibly cancel 388 * a pending operation thereof. 389 * 390 * @param cls closure. 391 * @param cmd the command being freed. 392 */ 393 static void 394 batch_withdraw_cleanup (void *cls, 395 const struct TALER_TESTING_Command *cmd) 396 { 397 struct BatchWithdrawState *ws = cls; 398 399 if (NULL != ws->wsh) 400 { 401 TALER_TESTING_command_incomplete (ws->is, 402 cmd->label); 403 TALER_EXCHANGE_post_withdraw_cancel (ws->wsh); 404 ws->wsh = NULL; 405 } 406 for (unsigned int i = 0; i<ws->num_coins; i++) 407 { 408 struct CoinState *cs = &ws->coins[i]; 409 TALER_denom_ewv_free (&cs->details.blinding_values); 410 TALER_denom_sig_free (&cs->details.denom_sig); 411 if (NULL != cs->pk) 412 { 413 TALER_EXCHANGE_destroy_denomination_key (cs->pk); 414 cs->pk = NULL; 415 } 416 } 417 GNUNET_free (ws->coins); 418 GNUNET_free (ws->exchange_url); 419 GNUNET_free (ws->reserve_payto_uri.normalized_payto); 420 GNUNET_free (ws); 421 } 422 423 424 /** 425 * Offer internal data to a "withdraw" CMD state to other 426 * commands. 427 * 428 * @param cls closure 429 * @param[out] ret result (could be anything) 430 * @param trait name of the trait 431 * @param index index number of the object to offer. 432 * @return #GNUNET_OK on success 433 */ 434 static enum GNUNET_GenericReturnValue 435 batch_withdraw_traits (void *cls, 436 const void **ret, 437 const char *trait, 438 unsigned int index) 439 { 440 struct BatchWithdrawState *ws = cls; 441 struct CoinState *cs = &ws->coins[index]; 442 struct TALER_TESTING_Trait traits[] = { 443 /* history entry MUST be first due to response code logic below! */ 444 TALER_TESTING_make_trait_reserve_history (index, 445 &ws->reserve_history), 446 TALER_TESTING_make_trait_coin_priv (index, 447 &cs->details.coin_priv), 448 TALER_TESTING_make_trait_coin_pub (index, 449 &cs->details.coin_pub), 450 TALER_TESTING_make_trait_planchet_secrets (index, 451 &cs->secret), 452 TALER_TESTING_make_trait_blinding_key (index, 453 &cs->details.blinding_key), 454 TALER_TESTING_make_trait_exchange_blinding_values (index, 455 &cs->details. 456 blinding_values), 457 TALER_TESTING_make_trait_denom_pub (index, 458 cs->pk), 459 TALER_TESTING_make_trait_denom_sig (index, 460 &cs->details.denom_sig), 461 TALER_TESTING_make_trait_withdraw_seed (&ws->seed), 462 TALER_TESTING_make_trait_withdraw_commitment (&ws->planchets_h), 463 TALER_TESTING_make_trait_reserve_priv (&ws->reserve_priv), 464 TALER_TESTING_make_trait_reserve_pub (&ws->reserve_pub), 465 TALER_TESTING_make_trait_amounts (index, 466 &cs->amount), 467 TALER_TESTING_make_trait_legi_requirement_row (&ws->requirement_row), 468 TALER_TESTING_make_trait_h_normalized_payto (&ws->h_payto), 469 TALER_TESTING_make_trait_normalized_payto_uri (&ws->reserve_payto_uri), 470 TALER_TESTING_make_trait_exchange_url (ws->exchange_url), 471 TALER_TESTING_make_trait_age_commitment_proof (index, 472 ws->age > 0 ? 473 &cs->details. 474 age_commitment_proof: 475 NULL), 476 TALER_TESTING_make_trait_h_age_commitment (index, 477 ws->age > 0 ? 478 &cs->details.h_age_commitment : 479 NULL), 480 TALER_TESTING_trait_end () 481 }; 482 483 if (index >= ws->num_coins) 484 return GNUNET_NO; 485 return TALER_TESTING_get_trait ((ws->expected_response_code == MHD_HTTP_OK) 486 ? &traits[0] /* we have reserve history */ 487 : &traits[1], /* skip reserve history */ 488 ret, 489 trait, 490 index); 491 } 492 493 494 struct TALER_TESTING_Command 495 TALER_TESTING_cmd_batch_withdraw ( 496 const char *label, 497 const char *reserve_reference, 498 unsigned int expected_response_code, 499 const char *amount, 500 ...) 501 { 502 struct BatchWithdrawState *ws; 503 unsigned int cnt; 504 va_list ap; 505 506 ws = GNUNET_new (struct BatchWithdrawState); 507 ws->reserve_reference = reserve_reference; 508 ws->expected_response_code = expected_response_code; 509 510 cnt = 1; 511 va_start (ap, 512 amount); 513 while (NULL != (va_arg (ap, 514 const char *))) 515 cnt++; 516 ws->num_coins = cnt; 517 ws->coins = GNUNET_new_array (cnt, 518 struct CoinState); 519 va_end (ap); 520 va_start (ap, 521 amount); 522 for (unsigned int i = 0; i<ws->num_coins; i++) 523 { 524 struct CoinState *cs = &ws->coins[i]; 525 526 if (GNUNET_OK != 527 TALER_string_to_amount (amount, 528 &cs->amount)) 529 { 530 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 531 "Failed to parse amount `%s' at %s\n", 532 amount, 533 label); 534 GNUNET_assert (0); 535 } 536 /* move on to next vararg! */ 537 amount = va_arg (ap, 538 const char *); 539 } 540 GNUNET_assert (NULL == amount); 541 va_end (ap); 542 543 { 544 struct TALER_TESTING_Command cmd = { 545 .cls = ws, 546 .label = label, 547 .run = &batch_withdraw_run, 548 .cleanup = &batch_withdraw_cleanup, 549 .traits = &batch_withdraw_traits 550 }; 551 552 return cmd; 553 } 554 } 555 556 557 /* end of testing_api_cmd_batch_withdraw.c */