testing_api_cmd_batch_withdraw.c (16562B)
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/taler_json_lib.h" 27 #include <microhttpd.h> 28 #include <gnunet/gnunet_curl_lib.h> 29 #include "taler/taler_signatures.h" 30 #include "taler/taler_extensions.h" 31 #include "taler/taler_testing_lib.h" 32 33 /** 34 * Information we track per withdrawn coin. 35 */ 36 struct CoinState 37 { 38 39 /** 40 * String describing the denomination value we should withdraw. 41 * A corresponding denomination key must exist in the exchange's 42 * offerings. Can be NULL if @e pk is set instead. 43 */ 44 struct TALER_Amount amount; 45 46 /** 47 * If @e amount is NULL, this specifies the denomination key to 48 * use. Otherwise, this will be set (by the interpreter) to the 49 * denomination PK matching @e amount. 50 */ 51 struct TALER_EXCHANGE_DenomPublicKey *pk; 52 53 /** 54 * Coin Details, as returned by the withdrawal operation 55 */ 56 struct TALER_EXCHANGE_WithdrawCoinPrivateDetails details; 57 58 /** 59 * Set (by the interpreter) to the exchange's signature over the 60 * coin's public key. 61 */ 62 struct TALER_BlindedDenominationSignature blinded_denom_sig; 63 64 /** 65 * Private key material of the coin, set by the interpreter. 66 */ 67 struct TALER_PlanchetMasterSecretP secret; 68 69 70 }; 71 72 73 /** 74 * State for a "batch withdraw" CMD. 75 */ 76 struct BatchWithdrawState 77 { 78 79 /** 80 * Which reserve should we withdraw from? 81 */ 82 const char *reserve_reference; 83 84 /** 85 * Exchange base URL. Only used as offered trait. 86 */ 87 char *exchange_url; 88 89 /** 90 * URI if the reserve we are withdrawing from. 91 */ 92 struct TALER_NormalizedPayto reserve_payto_uri; 93 94 /** 95 * Private key of the reserve we are withdrawing from. 96 */ 97 struct TALER_ReservePrivateKeyP reserve_priv; 98 99 /** 100 * Public key of the reserve we are withdrawing from. 101 */ 102 struct TALER_ReservePublicKeyP reserve_pub; 103 104 /** 105 * Interpreter state (during command). 106 */ 107 struct TALER_TESTING_Interpreter *is; 108 109 /** 110 * Withdraw handle (while operation is running). 111 */ 112 struct TALER_EXCHANGE_PostWithdrawHandle *wsh; 113 114 /** 115 * Array of coin states. 116 */ 117 struct CoinState *coins; 118 119 /** 120 * The seed from which the batch of seeds for the coins is derived 121 */ 122 struct TALER_WithdrawMasterSeedP seed; 123 124 125 /** 126 * Set to the KYC requirement payto hash *if* the exchange replied with a 127 * request for KYC. 128 */ 129 struct TALER_NormalizedPaytoHashP h_payto; 130 131 /** 132 * Set to the KYC requirement row *if* the exchange replied with 133 * a request for KYC. 134 */ 135 uint64_t requirement_row; 136 137 /** 138 * Length of the @e coins array. 139 */ 140 unsigned int num_coins; 141 142 /** 143 * An age > 0 signifies age restriction is applied. 144 * Same for all coins in the batch. 145 */ 146 uint8_t age; 147 148 /** 149 * Expected HTTP response code to the request. 150 */ 151 unsigned int expected_response_code; 152 153 154 /** 155 * Reserve history entry that corresponds to this withdrawal. 156 * Will be of type #TALER_EXCHANGE_RTT_WITHDRAWAL. 157 */ 158 struct TALER_EXCHANGE_ReserveHistoryEntry reserve_history; 159 160 /** 161 * The commitment of the call to withdraw, needed later for recoup. 162 */ 163 struct TALER_HashBlindedPlanchetsP planchets_h; 164 165 }; 166 167 168 /** 169 * "batch withdraw" operation callback; checks that the 170 * response code is expected and store the exchange signature 171 * in the state. 172 * 173 * @param cls closure. 174 * @param wr withdraw response details 175 */ 176 static void 177 batch_withdraw_cb (void *cls, 178 const struct 179 TALER_EXCHANGE_PostWithdrawResponse *wr) 180 { 181 struct BatchWithdrawState *ws = cls; 182 struct TALER_TESTING_Interpreter *is = ws->is; 183 184 ws->wsh = NULL; 185 if (ws->expected_response_code != wr->hr.http_status) 186 { 187 TALER_TESTING_unexpected_status_with_body (is, 188 wr->hr.http_status, 189 ws->expected_response_code, 190 wr->hr.reply); 191 return; 192 } 193 switch (wr->hr.http_status) 194 { 195 case MHD_HTTP_OK: 196 for (unsigned int i = 0; i<ws->num_coins; i++) 197 { 198 struct CoinState *cs = &ws->coins[i]; 199 200 cs->details = wr->details.ok.coin_details[i]; 201 TALER_denom_sig_copy (&cs->details.denom_sig, 202 &wr->details.ok.coin_details[i].denom_sig); 203 TALER_denom_ewv_copy (&cs->details.blinding_values, 204 &wr->details.ok.coin_details[i].blinding_values); 205 } 206 ws->planchets_h = wr->details.ok.planchets_h; 207 break; 208 case MHD_HTTP_FORBIDDEN: 209 /* nothing to check */ 210 break; 211 case MHD_HTTP_NOT_FOUND: 212 /* nothing to check */ 213 break; 214 case MHD_HTTP_CONFLICT: 215 /* FIXME[oec]: Check if age-requirement is the reason */ 216 break; 217 case MHD_HTTP_GONE: 218 /* theoretically could check that the key was actually */ 219 break; 220 case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: 221 /* nothing to check */ 222 ws->requirement_row 223 = wr->details.unavailable_for_legal_reasons.requirement_row; 224 ws->h_payto 225 = wr->details.unavailable_for_legal_reasons.h_payto; 226 break; 227 default: 228 /* Unsupported status code (by test harness) */ 229 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 230 "Batch withdraw test command does not support status code %u\n", 231 wr->hr.http_status); 232 GNUNET_break (0); 233 break; 234 } 235 TALER_TESTING_interpreter_next (is); 236 } 237 238 239 /** 240 * Run the command. 241 */ 242 static void 243 batch_withdraw_run (void *cls, 244 const struct TALER_TESTING_Command *cmd, 245 struct TALER_TESTING_Interpreter *is) 246 { 247 struct BatchWithdrawState *ws = cls; 248 struct TALER_EXCHANGE_Keys *keys = TALER_TESTING_get_keys (is); 249 const struct TALER_ReservePrivateKeyP *rp; 250 const struct TALER_TESTING_Command *create_reserve; 251 const struct TALER_EXCHANGE_DenomPublicKey *dpk; 252 struct TALER_EXCHANGE_DenomPublicKey denoms_pub[ws->num_coins]; 253 struct TALER_PlanchetMasterSecretP secrets[ws->num_coins]; 254 255 (void) cmd; 256 ws->is = is; 257 create_reserve 258 = TALER_TESTING_interpreter_lookup_command ( 259 is, 260 ws->reserve_reference); 261 262 if (NULL == create_reserve) 263 { 264 GNUNET_break (0); 265 TALER_TESTING_interpreter_fail (is); 266 return; 267 } 268 if (GNUNET_OK != 269 TALER_TESTING_get_trait_reserve_priv (create_reserve, 270 &rp)) 271 { 272 GNUNET_break (0); 273 TALER_TESTING_interpreter_fail (is); 274 return; 275 } 276 if (NULL == ws->exchange_url) 277 ws->exchange_url 278 = GNUNET_strdup (TALER_TESTING_get_exchange_url (is)); 279 ws->reserve_priv = *rp; 280 GNUNET_CRYPTO_eddsa_key_get_public (&ws->reserve_priv.eddsa_priv, 281 &ws->reserve_pub.eddsa_pub); 282 ws->reserve_payto_uri 283 = TALER_reserve_make_payto (ws->exchange_url, 284 &ws->reserve_pub); 285 286 GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, 287 &ws->seed, 288 sizeof(ws->seed)); 289 290 /** 291 * This is the same expansion that happens inside the call to 292 * TALER_EXCHANGE_withdraw. We save the expanded 293 * secrets later per coin state. 294 */ 295 TALER_withdraw_expand_secrets (ws->num_coins, 296 &ws->seed, 297 secrets); 298 299 GNUNET_assert (ws->num_coins > 0); 300 GNUNET_assert (GNUNET_OK == 301 TALER_amount_set_zero ( 302 ws->coins[0].amount.currency, 303 &ws->reserve_history.amount)); 304 GNUNET_assert (GNUNET_OK == 305 TALER_amount_set_zero ( 306 ws->coins[0].amount.currency, 307 &ws->reserve_history.details.withdraw.fee)); 308 309 for (unsigned int i = 0; i<ws->num_coins; i++) 310 { 311 struct CoinState *cs = &ws->coins[i]; 312 struct TALER_Amount amount; 313 314 315 cs->secret = secrets[i]; 316 317 dpk = TALER_TESTING_find_pk (keys, 318 &cs->amount, 319 false); /* no age restriction */ 320 if (NULL == dpk) 321 { 322 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 323 "Failed to determine denomination key at %s\n", 324 (NULL != cmd) ? cmd->label : "<retried command>"); 325 GNUNET_break (0); 326 TALER_TESTING_interpreter_fail (is); 327 return; 328 } 329 /* We copy the denomination key, as re-querying /keys 330 * would free the old one. */ 331 cs->pk = TALER_EXCHANGE_copy_denomination_key (dpk); 332 333 GNUNET_assert (GNUNET_OK == 334 TALER_amount_set_zero ( 335 cs->amount.currency, 336 &amount)); 337 GNUNET_assert (0 <= 338 TALER_amount_add ( 339 &amount, 340 &cs->amount, 341 &cs->pk->fees.withdraw)); 342 GNUNET_assert (0 <= 343 TALER_amount_add ( 344 &ws->reserve_history.amount, 345 &ws->reserve_history.amount, 346 &amount)); 347 GNUNET_assert (0 <= 348 TALER_amount_add ( 349 &ws->reserve_history.details.withdraw.fee, 350 &ws->reserve_history.details.withdraw.fee, 351 &cs->pk->fees.withdraw)); 352 353 denoms_pub[i] = *cs->pk; 354 TALER_denom_pub_copy (&denoms_pub[i].key, 355 &cs->pk->key); 356 } 357 358 ws->reserve_history.type = TALER_EXCHANGE_RTT_WITHDRAWAL; 359 360 ws->wsh = TALER_EXCHANGE_post_withdraw_create ( 361 TALER_TESTING_interpreter_get_context (is), 362 TALER_TESTING_get_exchange_url (is), 363 keys, 364 rp, 365 ws->num_coins, 366 denoms_pub, 367 &ws->seed, 368 0); 369 for (unsigned int i = 0; i<ws->num_coins; i++) 370 TALER_denom_pub_free (&denoms_pub[i].key); 371 if (NULL == ws->wsh) 372 { 373 GNUNET_break (0); 374 TALER_TESTING_interpreter_fail (is); 375 return; 376 } 377 GNUNET_assert (TALER_EC_NONE == 378 TALER_EXCHANGE_post_withdraw_start (ws->wsh, 379 &batch_withdraw_cb, 380 ws)); 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_post_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 */