testing_api_cmd_batch_withdraw.c (16357B)
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 * Set to the KYC requirement payto hash *if* the exchange replied with a 125 * request for KYC. 126 */ 127 struct TALER_NormalizedPaytoHashP h_payto; 128 129 /** 130 * Set to the KYC requirement row *if* the exchange replied with 131 * a request for KYC. 132 */ 133 uint64_t requirement_row; 134 135 /** 136 * Length of the @e coins array. 137 */ 138 unsigned int num_coins; 139 140 /** 141 * An age > 0 signifies age restriction is applied. 142 * Same for all coins in the batch. 143 */ 144 uint8_t age; 145 146 /** 147 * Expected HTTP response code to the request. 148 */ 149 unsigned int expected_response_code; 150 151 /** 152 * Reserve history entry that corresponds to this withdrawal. 153 * Will be of type #TALER_EXCHANGE_RTT_WITHDRAWAL. 154 */ 155 struct TALER_EXCHANGE_ReserveHistoryEntry reserve_history; 156 157 /** 158 * The commitment of the call to withdraw, needed later for recoup. 159 */ 160 struct TALER_HashBlindedPlanchetsP planchets_h; 161 162 }; 163 164 165 /** 166 * "batch withdraw" operation callback; checks that the 167 * response code is expected and store the exchange signature 168 * in the state. 169 * 170 * @param cls closure. 171 * @param wr withdraw response details 172 */ 173 static void 174 batch_withdraw_cb (void *cls, 175 const struct 176 TALER_EXCHANGE_PostWithdrawResponse *wr) 177 { 178 struct BatchWithdrawState *ws = cls; 179 struct TALER_TESTING_Interpreter *is = ws->is; 180 181 ws->wsh = NULL; 182 if (ws->expected_response_code != wr->hr.http_status) 183 { 184 TALER_TESTING_unexpected_status_with_body (is, 185 wr->hr.http_status, 186 ws->expected_response_code, 187 wr->hr.reply); 188 return; 189 } 190 switch (wr->hr.http_status) 191 { 192 case MHD_HTTP_OK: 193 for (unsigned int i = 0; i<ws->num_coins; i++) 194 { 195 struct CoinState *cs = &ws->coins[i]; 196 197 cs->details = wr->details.ok.coin_details[i]; 198 TALER_denom_sig_copy (&cs->details.denom_sig, 199 &wr->details.ok.coin_details[i].denom_sig); 200 TALER_denom_ewv_copy (&cs->details.blinding_values, 201 &wr->details.ok.coin_details[i].blinding_values); 202 } 203 ws->planchets_h = wr->details.ok.planchets_h; 204 break; 205 case MHD_HTTP_FORBIDDEN: 206 /* nothing to check */ 207 break; 208 case MHD_HTTP_NOT_FOUND: 209 /* nothing to check */ 210 break; 211 case MHD_HTTP_CONFLICT: 212 /* FIXME[oec]: Check if age-requirement is the reason */ 213 break; 214 case MHD_HTTP_GONE: 215 /* theoretically could check that the key was actually */ 216 break; 217 case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: 218 /* nothing to check */ 219 ws->requirement_row 220 = wr->details.unavailable_for_legal_reasons.requirement_row; 221 ws->h_payto 222 = wr->details.unavailable_for_legal_reasons.h_payto; 223 break; 224 default: 225 /* Unsupported status code (by test harness) */ 226 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 227 "Batch withdraw test command does not support status code %u\n", 228 wr->hr.http_status); 229 GNUNET_break (0); 230 break; 231 } 232 TALER_TESTING_interpreter_next (is); 233 } 234 235 236 /** 237 * Run the command. 238 */ 239 static void 240 batch_withdraw_run (void *cls, 241 const struct TALER_TESTING_Command *cmd, 242 struct TALER_TESTING_Interpreter *is) 243 { 244 struct BatchWithdrawState *ws = cls; 245 struct TALER_EXCHANGE_Keys *keys = TALER_TESTING_get_keys (is); 246 const struct TALER_ReservePrivateKeyP *rp; 247 const struct TALER_TESTING_Command *create_reserve; 248 const struct TALER_EXCHANGE_DenomPublicKey *dpk; 249 struct TALER_EXCHANGE_DenomPublicKey denoms_pub[ws->num_coins]; 250 struct TALER_PlanchetMasterSecretP secrets[ws->num_coins]; 251 252 (void) cmd; 253 ws->is = is; 254 create_reserve 255 = TALER_TESTING_interpreter_lookup_command ( 256 is, 257 ws->reserve_reference); 258 259 if (NULL == create_reserve) 260 { 261 GNUNET_break (0); 262 TALER_TESTING_interpreter_fail (is); 263 return; 264 } 265 if (GNUNET_OK != 266 TALER_TESTING_get_trait_reserve_priv (create_reserve, 267 &rp)) 268 { 269 GNUNET_break (0); 270 TALER_TESTING_interpreter_fail (is); 271 return; 272 } 273 if (NULL == ws->exchange_url) 274 ws->exchange_url 275 = GNUNET_strdup (TALER_TESTING_get_exchange_url (is)); 276 ws->reserve_priv = *rp; 277 GNUNET_CRYPTO_eddsa_key_get_public (&ws->reserve_priv.eddsa_priv, 278 &ws->reserve_pub.eddsa_pub); 279 ws->reserve_payto_uri 280 = TALER_reserve_make_payto (ws->exchange_url, 281 &ws->reserve_pub); 282 283 GNUNET_CRYPTO_random_block (&ws->seed, 284 sizeof(ws->seed)); 285 286 /** 287 * This is the same expansion that happens inside the call to 288 * TALER_EXCHANGE_withdraw. We save the expanded 289 * secrets later per coin state. 290 */ 291 TALER_withdraw_expand_secrets (ws->num_coins, 292 &ws->seed, 293 secrets); 294 295 GNUNET_assert (ws->num_coins > 0); 296 GNUNET_assert (GNUNET_OK == 297 TALER_amount_set_zero ( 298 ws->coins[0].amount.currency, 299 &ws->reserve_history.amount)); 300 GNUNET_assert (GNUNET_OK == 301 TALER_amount_set_zero ( 302 ws->coins[0].amount.currency, 303 &ws->reserve_history.details.withdraw.fee)); 304 305 for (unsigned int i = 0; i<ws->num_coins; i++) 306 { 307 struct CoinState *cs = &ws->coins[i]; 308 struct TALER_Amount amount; 309 310 cs->secret = secrets[i]; 311 dpk = TALER_TESTING_find_pk (keys, 312 &cs->amount, 313 false); /* no age restriction */ 314 if (NULL == dpk) 315 { 316 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 317 "Failed to determine denomination key at %s\n", 318 (NULL != cmd) ? cmd->label : "<retried command>"); 319 GNUNET_break (0); 320 TALER_TESTING_interpreter_fail (is); 321 return; 322 } 323 /* We copy the denomination key, as re-querying /keys 324 * would free the old one. */ 325 cs->pk = TALER_EXCHANGE_copy_denomination_key (dpk); 326 327 GNUNET_assert (GNUNET_OK == 328 TALER_amount_set_zero ( 329 cs->amount.currency, 330 &amount)); 331 GNUNET_assert (0 <= 332 TALER_amount_add ( 333 &amount, 334 &cs->amount, 335 &cs->pk->fees.withdraw)); 336 GNUNET_assert (0 <= 337 TALER_amount_add ( 338 &ws->reserve_history.amount, 339 &ws->reserve_history.amount, 340 &amount)); 341 GNUNET_assert (0 <= 342 TALER_amount_add ( 343 &ws->reserve_history.details.withdraw.fee, 344 &ws->reserve_history.details.withdraw.fee, 345 &cs->pk->fees.withdraw)); 346 ws->reserve_history.details.withdraw.age_restricted = (0 != ws->age); 347 ws->reserve_history.details.withdraw.max_age = ws->age; 348 denoms_pub[i] = *cs->pk; 349 TALER_denom_pub_copy (&denoms_pub[i].key, 350 &cs->pk->key); 351 } 352 ws->reserve_history.details.withdraw.num_coins = ws->num_coins; 353 ws->reserve_history.type = TALER_EXCHANGE_RTT_WITHDRAWAL; 354 355 ws->wsh = TALER_EXCHANGE_post_withdraw_create ( 356 TALER_TESTING_interpreter_get_context (is), 357 TALER_TESTING_get_exchange_url (is), 358 keys, 359 rp, 360 ws->num_coins, 361 denoms_pub, 362 &ws->seed, 363 0); 364 for (unsigned int i = 0; i < ws->num_coins; i++) 365 TALER_denom_pub_free (&denoms_pub[i].key); 366 if (NULL == ws->wsh) 367 { 368 GNUNET_break (0); 369 TALER_TESTING_interpreter_fail (is); 370 return; 371 } 372 GNUNET_assert (TALER_EC_NONE == 373 TALER_EXCHANGE_post_withdraw_start (ws->wsh, 374 &batch_withdraw_cb, 375 ws)); 376 } 377 378 379 /** 380 * Free the state of a "withdraw" CMD, and possibly cancel 381 * a pending operation thereof. 382 * 383 * @param cls closure. 384 * @param cmd the command being freed. 385 */ 386 static void 387 batch_withdraw_cleanup (void *cls, 388 const struct TALER_TESTING_Command *cmd) 389 { 390 struct BatchWithdrawState *ws = cls; 391 392 if (NULL != ws->wsh) 393 { 394 TALER_TESTING_command_incomplete (ws->is, 395 cmd->label); 396 TALER_EXCHANGE_post_withdraw_cancel (ws->wsh); 397 ws->wsh = NULL; 398 } 399 for (unsigned int i = 0; i<ws->num_coins; i++) 400 { 401 struct CoinState *cs = &ws->coins[i]; 402 TALER_denom_ewv_free (&cs->details.blinding_values); 403 TALER_denom_sig_free (&cs->details.denom_sig); 404 if (NULL != cs->pk) 405 { 406 TALER_EXCHANGE_destroy_denomination_key (cs->pk); 407 cs->pk = NULL; 408 } 409 } 410 GNUNET_free (ws->coins); 411 GNUNET_free (ws->exchange_url); 412 GNUNET_free (ws->reserve_payto_uri.normalized_payto); 413 GNUNET_free (ws); 414 } 415 416 417 /** 418 * Offer internal data to a "withdraw" CMD state to other 419 * commands. 420 * 421 * @param cls closure 422 * @param[out] ret result (could be anything) 423 * @param trait name of the trait 424 * @param index index number of the object to offer. 425 * @return #GNUNET_OK on success 426 */ 427 static enum GNUNET_GenericReturnValue 428 batch_withdraw_traits (void *cls, 429 const void **ret, 430 const char *trait, 431 unsigned int index) 432 { 433 struct BatchWithdrawState *ws = cls; 434 435 if (index >= ws->num_coins) 436 return GNUNET_NO; 437 { 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 (0, /* only 1 trait, not per coin! */ 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 ( 469 index, 470 ws->age > 0 471 ? &cs->details.age_commitment_proof 472 : NULL), 473 TALER_TESTING_make_trait_h_age_commitment ( 474 index, 475 ws->age > 0 476 ? &cs->details.h_age_commitment 477 : NULL), 478 TALER_TESTING_trait_end () 479 }; 480 481 return TALER_TESTING_get_trait ( 482 (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 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 */