testing_api_cmd_coin_history.c (17714B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2023 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify 6 it under the terms of the GNU General Public License as 7 published by the Free Software Foundation; either version 3, or 8 (at your 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 13 GNU 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_coin_history.c 21 * @brief Implement the /coins/$COIN_PUB/history test command. 22 * @author Christian Grothoff 23 */ 24 #include "taler/taler_json_lib.h" 25 #include <gnunet/gnunet_curl_lib.h> 26 struct HistoryState; 27 #define TALER_EXCHANGE_GET_COINS_HISTORY_RESULT_CLOSURE \ 28 struct HistoryState 29 #include "taler/exchange/get-coins-COIN_PUB-history.h" 30 #include "taler/taler_testing_lib.h" 31 32 33 /** 34 * State for a "history" CMD. 35 */ 36 struct HistoryState 37 { 38 39 /** 40 * Public key of the coin being analyzed. 41 */ 42 struct TALER_CoinSpendPublicKeyP coin_pub; 43 44 /** 45 * Label to the command which created the coin to check, 46 * needed to resort the coin key. 47 */ 48 const char *coin_reference; 49 50 /** 51 * Handle to the "coin history" operation. 52 */ 53 struct TALER_EXCHANGE_GetCoinsHistoryHandle *rsh; 54 55 /** 56 * Expected coin balance. 57 */ 58 const char *expected_balance; 59 60 /** 61 * Private key of the coin being analyzed. 62 */ 63 const struct TALER_CoinSpendPrivateKeyP *coin_priv; 64 65 /** 66 * Interpreter state. 67 */ 68 struct TALER_TESTING_Interpreter *is; 69 70 /** 71 * Expected HTTP response code. 72 */ 73 unsigned int expected_response_code; 74 75 }; 76 77 78 /** 79 * Closure for analysis_cb(). 80 */ 81 struct AnalysisContext 82 { 83 /** 84 * Coin public key we are looking at. 85 */ 86 const struct TALER_CoinSpendPublicKeyP *coin_pub; 87 88 /** 89 * Length of the @e history array. 90 */ 91 unsigned int history_length; 92 93 /** 94 * Array of history items to match. 95 */ 96 const struct TALER_EXCHANGE_CoinHistoryEntry *history; 97 98 /** 99 * Array of @e history_length of matched entries. 100 */ 101 bool *found; 102 103 /** 104 * Set to true if an entry could not be found. 105 */ 106 bool failure; 107 }; 108 109 110 /** 111 * Compare @a h1 and @a h2. 112 * 113 * @param h1 a history entry 114 * @param h2 a history entry 115 * @return 0 if @a h1 and @a h2 are equal 116 */ 117 static int 118 history_entry_cmp ( 119 const struct TALER_EXCHANGE_CoinHistoryEntry *h1, 120 const struct TALER_EXCHANGE_CoinHistoryEntry *h2) 121 { 122 if (h1->type != h2->type) 123 return 1; 124 if (0 != TALER_amount_cmp (&h1->amount, 125 &h2->amount)) 126 { 127 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 128 "Amount mismatch (%s)\n", 129 TALER_amount2s (&h1->amount)); 130 return 1; 131 } 132 switch (h1->type) 133 { 134 case TALER_EXCHANGE_CTT_NONE: 135 GNUNET_break (0); 136 break; 137 case TALER_EXCHANGE_CTT_DEPOSIT: 138 if (0 != GNUNET_memcmp (&h1->details.deposit.h_contract_terms, 139 &h2->details.deposit.h_contract_terms)) 140 return 1; 141 if (0 != GNUNET_memcmp (&h1->details.deposit.merchant_pub, 142 &h2->details.deposit.merchant_pub)) 143 return 1; 144 if (0 != GNUNET_memcmp (&h1->details.deposit.h_wire, 145 &h2->details.deposit.h_wire)) 146 return 1; 147 if (0 != GNUNET_memcmp (&h1->details.deposit.sig, 148 &h2->details.deposit.sig)) 149 return 1; 150 return 0; 151 case TALER_EXCHANGE_CTT_MELT: 152 if (0 != GNUNET_memcmp (&h1->details.melt.h_age_commitment, 153 &h2->details.melt.h_age_commitment)) 154 return 1; 155 /* Note: most other fields are not initialized 156 in the trait as they are hard to extract from 157 the API */ 158 return 0; 159 case TALER_EXCHANGE_CTT_REFUND: 160 if (0 != GNUNET_memcmp (&h1->details.refund.sig, 161 &h2->details.refund.sig)) 162 return 1; 163 return 0; 164 case TALER_EXCHANGE_CTT_RECOUP: 165 if (0 != GNUNET_memcmp (&h1->details.recoup.coin_sig, 166 &h2->details.recoup.coin_sig)) 167 return 1; 168 /* Note: exchange_sig, exchange_pub and timestamp are 169 fundamentally not available in the initiating command */ 170 return 0; 171 case TALER_EXCHANGE_CTT_RECOUP_REFRESH: 172 if (0 != GNUNET_memcmp (&h1->details.recoup_refresh.coin_sig, 173 &h2->details.recoup_refresh.coin_sig)) 174 return 1; 175 /* Note: exchange_sig, exchange_pub and timestamp are 176 fundamentally not available in the initiating command */ 177 return 0; 178 case TALER_EXCHANGE_CTT_OLD_COIN_RECOUP: 179 if (0 != GNUNET_memcmp (&h1->details.old_coin_recoup.new_coin_pub, 180 &h2->details.old_coin_recoup.new_coin_pub)) 181 return 1; 182 /* Note: exchange_sig, exchange_pub and timestamp are 183 fundamentally not available in the initiating command */ 184 return 0; 185 case TALER_EXCHANGE_CTT_PURSE_DEPOSIT: 186 /* coin_sig is not initialized */ 187 if (0 != GNUNET_memcmp (&h1->details.purse_deposit.purse_pub, 188 &h2->details.purse_deposit.purse_pub)) 189 { 190 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 191 "Purse public key mismatch\n"); 192 return 1; 193 } 194 if (0 != strcmp (h1->details.purse_deposit.exchange_base_url, 195 h2->details.purse_deposit.exchange_base_url)) 196 { 197 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 198 "Exchange base URL mismatch (%s/%s)\n", 199 h1->details.purse_deposit.exchange_base_url, 200 h2->details.purse_deposit.exchange_base_url); 201 GNUNET_break (0); 202 return 1; 203 } 204 return 0; 205 case TALER_EXCHANGE_CTT_PURSE_REFUND: 206 /* NOTE: not supported yet (trait not returned) */ 207 return 0; 208 case TALER_EXCHANGE_CTT_RESERVE_OPEN_DEPOSIT: 209 /* NOTE: not supported yet (trait not returned) */ 210 if (0 != GNUNET_memcmp (&h1->details.reserve_open_deposit.coin_sig, 211 &h2->details.reserve_open_deposit.coin_sig)) 212 return 1; 213 return 0; 214 } 215 GNUNET_assert (0); 216 return -1; 217 } 218 219 220 /** 221 * Check if @a cmd changed the coin, if so, find the 222 * entry in our history and set the respective index in found 223 * to true. If the entry is not found, set failure. 224 * 225 * @param cls our `struct AnalysisContext *` 226 * @param cmd command to analyze for impact on history 227 */ 228 static void 229 analyze_command (void *cls, 230 const struct TALER_TESTING_Command *cmd) 231 { 232 struct AnalysisContext *ac = cls; 233 const struct TALER_CoinSpendPublicKeyP *coin_pub = ac->coin_pub; 234 const struct TALER_EXCHANGE_CoinHistoryEntry *history = ac->history; 235 unsigned int history_length = ac->history_length; 236 bool *found = ac->found; 237 238 if (TALER_TESTING_cmd_is_batch (cmd)) 239 { 240 struct TALER_TESTING_Command *cur; 241 struct TALER_TESTING_Command *bcmd; 242 243 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 244 "Checking `%s' for history of coin `%s'\n", 245 cmd->label, 246 TALER_B2S (coin_pub)); 247 cur = TALER_TESTING_cmd_batch_get_current (cmd); 248 if (GNUNET_OK != 249 TALER_TESTING_get_trait_batch_cmds (cmd, 250 &bcmd)) 251 { 252 GNUNET_break (0); 253 ac->failure = true; 254 return; 255 } 256 for (unsigned int i = 0; NULL != bcmd[i].label; i++) 257 { 258 struct TALER_TESTING_Command *step = &bcmd[i]; 259 260 analyze_command (ac, 261 step); 262 if (ac->failure) 263 { 264 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 265 "Entry for batch step `%s' missing in coin history\n", 266 step->label); 267 return; 268 } 269 if (step == cur) 270 break; /* if *we* are in a batch, make sure not to analyze commands past 'now' */ 271 } 272 return; 273 } 274 275 for (unsigned int j = 0; true; j++) 276 { 277 const struct TALER_CoinSpendPublicKeyP *rp; 278 const struct TALER_EXCHANGE_CoinHistoryEntry *he; 279 bool matched = false; 280 281 if (GNUNET_OK != 282 TALER_TESTING_get_trait_coin_pub (cmd, 283 j, 284 &rp)) 285 { 286 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 287 "Command `%s#%u' has no public key for a coin\n", 288 cmd->label, 289 j); 290 break; /* command does nothing for coins */ 291 } 292 if (0 != 293 GNUNET_memcmp (rp, 294 coin_pub)) 295 { 296 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 297 "Command `%s#%u' is about another coin %s\n", 298 cmd->label, 299 j, 300 TALER_B2S (rp)); 301 continue; /* command affects some _other_ coin */ 302 } 303 if (GNUNET_OK != 304 TALER_TESTING_get_trait_coin_history (cmd, 305 j, 306 &he)) 307 { 308 /* NOTE: only for debugging... */ 309 if (0 == j) 310 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 311 "Command `%s' has the coin_pub, but lacks coin history trait\n", 312 cmd->label); 313 return; /* command does nothing for coins */ 314 } 315 for (unsigned int i = 0; i<history_length; i++) 316 { 317 if (found[i]) 318 continue; /* already found, skip */ 319 if (0 == 320 history_entry_cmp (he, 321 &history[i])) 322 { 323 found[i] = true; 324 matched = true; 325 break; 326 } 327 } 328 if (! matched) 329 { 330 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 331 "Command `%s' coin history entry #%u not found\n", 332 cmd->label, 333 j); 334 ac->failure = true; 335 return; 336 } 337 } 338 } 339 340 341 /** 342 * Check that the coin balance and HTTP response code are 343 * both acceptable. 344 * 345 * @param ss closure. 346 * @param rs HTTP response details 347 */ 348 static void 349 coin_history_cb (struct HistoryState *ss, 350 const struct TALER_EXCHANGE_GetCoinsHistoryResponse *rs) 351 { 352 struct TALER_TESTING_Interpreter *is = ss->is; 353 struct TALER_Amount eb; 354 unsigned int hlen; 355 356 ss->rsh = NULL; 357 if (ss->expected_response_code != rs->hr.http_status) 358 { 359 TALER_TESTING_unexpected_status (ss->is, 360 rs->hr.http_status, 361 ss->expected_response_code); 362 return; 363 } 364 if (MHD_HTTP_OK != rs->hr.http_status) 365 { 366 TALER_TESTING_interpreter_next (is); 367 return; 368 } 369 GNUNET_assert (GNUNET_OK == 370 TALER_string_to_amount (ss->expected_balance, 371 &eb)); 372 373 if (0 != TALER_amount_cmp (&eb, 374 &rs->details.ok.balance)) 375 { 376 GNUNET_break (0); 377 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 378 "Unexpected balance for coin: %s\n", 379 TALER_amount_to_string (&rs->details.ok.balance)); 380 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 381 "Expected balance of: %s\n", 382 TALER_amount_to_string (&eb)); 383 TALER_TESTING_interpreter_fail (ss->is); 384 return; 385 } 386 hlen = json_array_size (rs->details.ok.history); 387 { 388 bool found[GNUNET_NZL (hlen)]; 389 struct TALER_EXCHANGE_CoinHistoryEntry rhist[GNUNET_NZL (hlen)]; 390 struct AnalysisContext ac = { 391 .coin_pub = &ss->coin_pub, 392 .history = rhist, 393 .history_length = hlen, 394 .found = found 395 }; 396 const struct TALER_EXCHANGE_DenomPublicKey *dk; 397 struct TALER_Amount total_in; 398 struct TALER_Amount total_out; 399 struct TALER_Amount hbal; 400 401 dk = TALER_EXCHANGE_get_denomination_key_by_hash ( 402 TALER_TESTING_get_keys (is), 403 &rs->details.ok.h_denom_pub); 404 GNUNET_assert (NULL != dk); 405 memset (found, 406 0, 407 sizeof (found)); 408 memset (rhist, 409 0, 410 sizeof (rhist)); 411 if (GNUNET_OK != 412 TALER_EXCHANGE_parse_coin_history ( 413 TALER_TESTING_get_keys (is), 414 dk, 415 rs->details.ok.history, 416 &ss->coin_pub, 417 &total_in, 418 &total_out, 419 hlen, 420 rhist)) 421 { 422 GNUNET_break (0); 423 json_dumpf (rs->hr.reply, 424 stderr, 425 JSON_INDENT (2)); 426 TALER_TESTING_interpreter_fail (ss->is); 427 return; 428 } 429 if (0 > 430 TALER_amount_subtract (&hbal, 431 &total_in, 432 &total_out)) 433 { 434 GNUNET_break (0); 435 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 436 "Coin credits: %s\n", 437 TALER_amount2s (&total_in)); 438 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 439 "Coin debits: %s\n", 440 TALER_amount2s (&total_out)); 441 TALER_TESTING_interpreter_fail (ss->is); 442 return; 443 } 444 if (0 != TALER_amount_cmp (&hbal, 445 &rs->details.ok.balance)) 446 { 447 GNUNET_break (0); 448 TALER_TESTING_interpreter_fail (ss->is); 449 return; 450 } 451 (void) ac; 452 TALER_TESTING_iterate (is, 453 true, 454 &analyze_command, 455 &ac); 456 if (ac.failure) 457 { 458 json_dumpf (rs->hr.reply, 459 stderr, 460 JSON_INDENT (2)); 461 TALER_TESTING_interpreter_fail (ss->is); 462 return; 463 } 464 #if 1 465 for (unsigned int i = 0; i<hlen; i++) 466 { 467 if (found[i]) 468 continue; 469 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 470 "History entry at index %u of type %d not justified by command history\n", 471 i, 472 rs->details.ok.history[i].type); 473 json_dumpf (rs->hr.reply, 474 stderr, 475 JSON_INDENT (2)); 476 TALER_TESTING_interpreter_fail (ss->is); 477 return; 478 } 479 #endif 480 } 481 TALER_TESTING_interpreter_next (is); 482 } 483 484 485 /** 486 * Run the command. 487 * 488 * @param cls closure. 489 * @param cmd the command being executed. 490 * @param is the interpreter state. 491 */ 492 static void 493 history_run (void *cls, 494 const struct TALER_TESTING_Command *cmd, 495 struct TALER_TESTING_Interpreter *is) 496 { 497 struct HistoryState *ss = cls; 498 const struct TALER_TESTING_Command *create_coin; 499 char *cref; 500 unsigned int idx; 501 502 ss->is = is; 503 GNUNET_assert ( 504 GNUNET_OK == 505 TALER_TESTING_parse_coin_reference ( 506 ss->coin_reference, 507 &cref, 508 &idx)); 509 create_coin 510 = TALER_TESTING_interpreter_lookup_command (is, 511 cref); 512 GNUNET_free (cref); 513 if (NULL == create_coin) 514 { 515 GNUNET_break (0); 516 TALER_TESTING_interpreter_fail (is); 517 return; 518 } 519 if (GNUNET_OK != 520 TALER_TESTING_get_trait_coin_priv (create_coin, 521 idx, 522 &ss->coin_priv)) 523 { 524 GNUNET_break (0); 525 TALER_LOG_ERROR ("Failed to find coin_priv for history query\n"); 526 TALER_TESTING_interpreter_fail (is); 527 return; 528 } 529 GNUNET_CRYPTO_eddsa_key_get_public (&ss->coin_priv->eddsa_priv, 530 &ss->coin_pub.eddsa_pub); 531 ss->rsh = TALER_EXCHANGE_get_coins_history_create ( 532 TALER_TESTING_interpreter_get_context (is), 533 TALER_TESTING_get_exchange_url (is), 534 ss->coin_priv); 535 GNUNET_assert (NULL != ss->rsh); 536 GNUNET_assert (TALER_EC_NONE == 537 TALER_EXCHANGE_get_coins_history_start (ss->rsh, 538 &coin_history_cb, 539 ss)); 540 } 541 542 543 /** 544 * Offer internal data from a "history" CMD, to other commands. 545 * 546 * @param cls closure. 547 * @param[out] ret result. 548 * @param trait name of the trait. 549 * @param index index number of the object to offer. 550 * @return #GNUNET_OK on success. 551 */ 552 static enum GNUNET_GenericReturnValue 553 history_traits (void *cls, 554 const void **ret, 555 const char *trait, 556 unsigned int index) 557 { 558 struct HistoryState *hs = cls; 559 struct TALER_TESTING_Trait traits[] = { 560 TALER_TESTING_make_trait_coin_pub (index, 561 &hs->coin_pub), 562 TALER_TESTING_trait_end () 563 }; 564 565 return TALER_TESTING_get_trait (traits, 566 ret, 567 trait, 568 index); 569 } 570 571 572 /** 573 * Cleanup the state from a "coin history" CMD, and possibly 574 * cancel a pending operation thereof. 575 * 576 * @param cls closure. 577 * @param cmd the command which is being cleaned up. 578 */ 579 static void 580 history_cleanup (void *cls, 581 const struct TALER_TESTING_Command *cmd) 582 { 583 struct HistoryState *ss = cls; 584 585 if (NULL != ss->rsh) 586 { 587 TALER_TESTING_command_incomplete (ss->is, 588 cmd->label); 589 TALER_EXCHANGE_get_coins_history_cancel (ss->rsh); 590 ss->rsh = NULL; 591 } 592 GNUNET_free (ss); 593 } 594 595 596 struct TALER_TESTING_Command 597 TALER_TESTING_cmd_coin_history (const char *label, 598 const char *coin_reference, 599 const char *expected_balance, 600 unsigned int expected_response_code) 601 { 602 struct HistoryState *ss; 603 604 GNUNET_assert (NULL != coin_reference); 605 ss = GNUNET_new (struct HistoryState); 606 ss->coin_reference = coin_reference; 607 ss->expected_balance = expected_balance; 608 ss->expected_response_code = expected_response_code; 609 { 610 struct TALER_TESTING_Command cmd = { 611 .cls = ss, 612 .label = label, 613 .run = &history_run, 614 .cleanup = &history_cleanup, 615 .traits = &history_traits 616 }; 617 618 return cmd; 619 } 620 }