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