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