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