Line data Source code
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 5 : history_entry_cmp (
119 : const struct TALER_EXCHANGE_CoinHistoryEntry *h1,
120 : const struct TALER_EXCHANGE_CoinHistoryEntry *h2)
121 : {
122 5 : if (h1->type != h2->type)
123 0 : return 1;
124 5 : if (0 != TALER_amount_cmp (&h1->amount,
125 : &h2->amount))
126 : {
127 0 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
128 : "Amount mismatch (%s)\n",
129 : TALER_amount2s (&h1->amount));
130 0 : return 1;
131 : }
132 5 : switch (h1->type)
133 : {
134 0 : case TALER_EXCHANGE_CTT_NONE:
135 0 : GNUNET_break (0);
136 0 : break;
137 2 : case TALER_EXCHANGE_CTT_DEPOSIT:
138 2 : if (0 != GNUNET_memcmp (&h1->details.deposit.h_contract_terms,
139 : &h2->details.deposit.h_contract_terms))
140 0 : return 1;
141 2 : if (0 != GNUNET_memcmp (&h1->details.deposit.merchant_pub,
142 : &h2->details.deposit.merchant_pub))
143 0 : return 1;
144 2 : if (0 != GNUNET_memcmp (&h1->details.deposit.h_wire,
145 : &h2->details.deposit.h_wire))
146 0 : return 1;
147 2 : if (0 != GNUNET_memcmp (&h1->details.deposit.sig,
148 : &h2->details.deposit.sig))
149 0 : return 1;
150 2 : return 0;
151 0 : case TALER_EXCHANGE_CTT_MELT:
152 0 : if (0 != GNUNET_memcmp (&h1->details.melt.h_age_commitment,
153 : &h2->details.melt.h_age_commitment))
154 0 : 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 0 : return 0;
159 0 : case TALER_EXCHANGE_CTT_REFUND:
160 0 : if (0 != GNUNET_memcmp (&h1->details.refund.sig,
161 : &h2->details.refund.sig))
162 0 : return 1;
163 0 : return 0;
164 0 : case TALER_EXCHANGE_CTT_RECOUP:
165 0 : if (0 != GNUNET_memcmp (&h1->details.recoup.coin_sig,
166 : &h2->details.recoup.coin_sig))
167 0 : return 1;
168 : /* Note: exchange_sig, exchange_pub and timestamp are
169 : fundamentally not available in the initiating command */
170 0 : return 0;
171 0 : case TALER_EXCHANGE_CTT_RECOUP_REFRESH:
172 0 : if (0 != GNUNET_memcmp (&h1->details.recoup_refresh.coin_sig,
173 : &h2->details.recoup_refresh.coin_sig))
174 0 : return 1;
175 : /* Note: exchange_sig, exchange_pub and timestamp are
176 : fundamentally not available in the initiating command */
177 0 : return 0;
178 0 : case TALER_EXCHANGE_CTT_OLD_COIN_RECOUP:
179 0 : if (0 != GNUNET_memcmp (&h1->details.old_coin_recoup.new_coin_pub,
180 : &h2->details.old_coin_recoup.new_coin_pub))
181 0 : return 1;
182 : /* Note: exchange_sig, exchange_pub and timestamp are
183 : fundamentally not available in the initiating command */
184 0 : return 0;
185 3 : case TALER_EXCHANGE_CTT_PURSE_DEPOSIT:
186 : /* coin_sig is not initialized */
187 3 : if (0 != GNUNET_memcmp (&h1->details.purse_deposit.purse_pub,
188 : &h2->details.purse_deposit.purse_pub))
189 : {
190 0 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
191 : "Purse public key mismatch\n");
192 0 : return 1;
193 : }
194 3 : if (0 != strcmp (h1->details.purse_deposit.exchange_base_url,
195 3 : h2->details.purse_deposit.exchange_base_url))
196 : {
197 0 : 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 0 : GNUNET_break (0);
202 0 : return 1;
203 : }
204 3 : return 0;
205 0 : case TALER_EXCHANGE_CTT_PURSE_REFUND:
206 : /* NOTE: not supported yet (trait not returned) */
207 0 : return 0;
208 0 : case TALER_EXCHANGE_CTT_RESERVE_OPEN_DEPOSIT:
209 : /* NOTE: not supported yet (trait not returned) */
210 0 : if (0 != GNUNET_memcmp (&h1->details.reserve_open_deposit.coin_sig,
211 : &h2->details.reserve_open_deposit.coin_sig))
212 0 : return 1;
213 0 : return 0;
214 : }
215 0 : 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 464 : analyze_command (void *cls,
230 : const struct TALER_TESTING_Command *cmd)
231 : {
232 464 : struct AnalysisContext *ac = cls;
233 464 : const struct TALER_CoinSpendPublicKeyP *coin_pub = ac->coin_pub;
234 464 : const struct TALER_EXCHANGE_CoinHistoryEntry *history = ac->history;
235 464 : unsigned int history_length = ac->history_length;
236 464 : bool *found = ac->found;
237 :
238 464 : if (TALER_TESTING_cmd_is_batch (cmd))
239 : {
240 : struct TALER_TESTING_Command *cur;
241 : struct TALER_TESTING_Command *bcmd;
242 :
243 37 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
244 : "Checking `%s' for history of coin `%s'\n",
245 : cmd->label,
246 : TALER_B2S (coin_pub));
247 37 : cur = TALER_TESTING_cmd_batch_get_current (cmd);
248 37 : if (GNUNET_OK !=
249 37 : TALER_TESTING_get_trait_batch_cmds (cmd,
250 : &bcmd))
251 : {
252 0 : GNUNET_break (0);
253 0 : ac->failure = true;
254 0 : return;
255 : }
256 448 : for (unsigned int i = 0; NULL != bcmd[i].label; i++)
257 : {
258 415 : struct TALER_TESTING_Command *step = &bcmd[i];
259 :
260 415 : analyze_command (ac,
261 : step);
262 415 : if (ac->failure)
263 : {
264 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
265 : "Entry for batch step `%s' missing in coin history\n",
266 : step->label);
267 0 : return;
268 : }
269 415 : if (step == cur)
270 4 : break; /* if *we* are in a batch, make sure not to analyze commands past 'now' */
271 : }
272 37 : return;
273 : }
274 :
275 427 : for (unsigned int j = 0; true; j++)
276 147 : {
277 : const struct TALER_CoinSpendPublicKeyP *rp;
278 : const struct TALER_EXCHANGE_CoinHistoryEntry *he;
279 574 : bool matched = false;
280 :
281 574 : if (GNUNET_OK !=
282 574 : TALER_TESTING_get_trait_coin_pub (cmd,
283 : j,
284 : &rp))
285 : {
286 418 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
287 : "Command `%s#%u' has no public key for a coin\n",
288 : cmd->label,
289 : j);
290 418 : break; /* command does nothing for coins */
291 : }
292 298 : if (0 !=
293 156 : GNUNET_memcmp (rp,
294 : coin_pub))
295 : {
296 142 : 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 142 : continue; /* command affects some _other_ coin */
302 : }
303 14 : if (GNUNET_OK !=
304 14 : TALER_TESTING_get_trait_coin_history (cmd,
305 : j,
306 : &he))
307 : {
308 : /* NOTE: only for debugging... */
309 9 : if (0 == j)
310 9 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
311 : "Command `%s' has the coin_pub, but lacks coin history trait\n",
312 : cmd->label);
313 9 : return; /* command does nothing for coins */
314 : }
315 6 : for (unsigned int i = 0; i<history_length; i++)
316 : {
317 6 : if (found[i])
318 1 : continue; /* already found, skip */
319 5 : if (0 ==
320 5 : history_entry_cmp (he,
321 5 : &history[i]))
322 : {
323 5 : found[i] = true;
324 5 : matched = true;
325 5 : break;
326 : }
327 : }
328 5 : if (! matched)
329 : {
330 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
331 : "Command `%s' coin history entry #%u not found\n",
332 : cmd->label,
333 : j);
334 0 : ac->failure = true;
335 0 : 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 4 : coin_history_cb (struct HistoryState *ss,
350 : const struct TALER_EXCHANGE_GetCoinsHistoryResponse *rs)
351 : {
352 4 : struct TALER_TESTING_Interpreter *is = ss->is;
353 : struct TALER_Amount eb;
354 : unsigned int hlen;
355 :
356 4 : ss->rsh = NULL;
357 4 : if (ss->expected_response_code != rs->hr.http_status)
358 : {
359 0 : TALER_TESTING_unexpected_status (ss->is,
360 : rs->hr.http_status,
361 : ss->expected_response_code);
362 0 : return;
363 : }
364 4 : if (MHD_HTTP_OK != rs->hr.http_status)
365 : {
366 0 : TALER_TESTING_interpreter_next (is);
367 0 : return;
368 : }
369 4 : GNUNET_assert (GNUNET_OK ==
370 : TALER_string_to_amount (ss->expected_balance,
371 : &eb));
372 :
373 4 : if (0 != TALER_amount_cmp (&eb,
374 : &rs->details.ok.balance))
375 : {
376 0 : GNUNET_break (0);
377 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
378 : "Unexpected balance for coin: %s\n",
379 : TALER_amount_to_string (&rs->details.ok.balance));
380 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
381 : "Expected balance of: %s\n",
382 : TALER_amount_to_string (&eb));
383 0 : TALER_TESTING_interpreter_fail (ss->is);
384 0 : return;
385 : }
386 4 : hlen = json_array_size (rs->details.ok.history);
387 4 : {
388 4 : bool found[GNUNET_NZL (hlen)];
389 4 : struct TALER_EXCHANGE_CoinHistoryEntry rhist[GNUNET_NZL (hlen)];
390 4 : struct AnalysisContext ac = {
391 4 : .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 4 : dk = TALER_EXCHANGE_get_denomination_key_by_hash (
402 4 : TALER_TESTING_get_keys (is),
403 : &rs->details.ok.h_denom_pub);
404 4 : GNUNET_assert (NULL != dk);
405 4 : memset (found,
406 : 0,
407 4 : sizeof (found));
408 4 : memset (rhist,
409 : 0,
410 : sizeof (rhist));
411 4 : if (GNUNET_OK !=
412 4 : TALER_EXCHANGE_parse_coin_history (
413 4 : TALER_TESTING_get_keys (is),
414 : dk,
415 4 : rs->details.ok.history,
416 4 : &ss->coin_pub,
417 : &total_in,
418 : &total_out,
419 : hlen,
420 : rhist))
421 : {
422 0 : GNUNET_break (0);
423 0 : json_dumpf (rs->hr.reply,
424 : stderr,
425 : JSON_INDENT (2));
426 0 : TALER_TESTING_interpreter_fail (ss->is);
427 0 : return;
428 : }
429 4 : if (0 >
430 4 : TALER_amount_subtract (&hbal,
431 : &total_in,
432 : &total_out))
433 : {
434 0 : GNUNET_break (0);
435 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
436 : "Coin credits: %s\n",
437 : TALER_amount2s (&total_in));
438 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
439 : "Coin debits: %s\n",
440 : TALER_amount2s (&total_out));
441 0 : TALER_TESTING_interpreter_fail (ss->is);
442 0 : return;
443 : }
444 4 : if (0 != TALER_amount_cmp (&hbal,
445 : &rs->details.ok.balance))
446 : {
447 0 : GNUNET_break (0);
448 0 : TALER_TESTING_interpreter_fail (ss->is);
449 0 : return;
450 : }
451 : (void) ac;
452 4 : TALER_TESTING_iterate (is,
453 : true,
454 : &analyze_command,
455 : &ac);
456 4 : if (ac.failure)
457 : {
458 0 : json_dumpf (rs->hr.reply,
459 : stderr,
460 : JSON_INDENT (2));
461 0 : TALER_TESTING_interpreter_fail (ss->is);
462 0 : return;
463 : }
464 : #if 1
465 9 : for (unsigned int i = 0; i<hlen; i++)
466 : {
467 5 : if (found[i])
468 5 : continue;
469 0 : 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 0 : json_dumpf (rs->hr.reply,
474 : stderr,
475 : JSON_INDENT (2));
476 0 : TALER_TESTING_interpreter_fail (ss->is);
477 0 : return;
478 : }
479 : #endif
480 : }
481 4 : 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 4 : history_run (void *cls,
494 : const struct TALER_TESTING_Command *cmd,
495 : struct TALER_TESTING_Interpreter *is)
496 : {
497 4 : struct HistoryState *ss = cls;
498 : const struct TALER_TESTING_Command *create_coin;
499 : char *cref;
500 : unsigned int idx;
501 :
502 4 : ss->is = is;
503 4 : GNUNET_assert (
504 : GNUNET_OK ==
505 : TALER_TESTING_parse_coin_reference (
506 : ss->coin_reference,
507 : &cref,
508 : &idx));
509 : create_coin
510 4 : = TALER_TESTING_interpreter_lookup_command (is,
511 : cref);
512 4 : GNUNET_free (cref);
513 4 : if (NULL == create_coin)
514 : {
515 0 : GNUNET_break (0);
516 0 : TALER_TESTING_interpreter_fail (is);
517 0 : return;
518 : }
519 4 : if (GNUNET_OK !=
520 4 : TALER_TESTING_get_trait_coin_priv (create_coin,
521 : idx,
522 : &ss->coin_priv))
523 : {
524 0 : GNUNET_break (0);
525 0 : TALER_LOG_ERROR ("Failed to find coin_priv for history query\n");
526 0 : TALER_TESTING_interpreter_fail (is);
527 0 : return;
528 : }
529 4 : GNUNET_CRYPTO_eddsa_key_get_public (&ss->coin_priv->eddsa_priv,
530 : &ss->coin_pub.eddsa_pub);
531 4 : 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 4 : GNUNET_assert (NULL != ss->rsh);
536 4 : 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 13 : history_traits (void *cls,
554 : const void **ret,
555 : const char *trait,
556 : unsigned int index)
557 : {
558 13 : struct HistoryState *hs = cls;
559 : struct TALER_TESTING_Trait traits[] = {
560 13 : TALER_TESTING_make_trait_coin_pub (index,
561 13 : &hs->coin_pub),
562 13 : TALER_TESTING_trait_end ()
563 : };
564 :
565 13 : 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 4 : history_cleanup (void *cls,
581 : const struct TALER_TESTING_Command *cmd)
582 : {
583 4 : struct HistoryState *ss = cls;
584 :
585 4 : if (NULL != ss->rsh)
586 : {
587 0 : TALER_TESTING_command_incomplete (ss->is,
588 : cmd->label);
589 0 : TALER_EXCHANGE_get_coins_history_cancel (ss->rsh);
590 0 : ss->rsh = NULL;
591 : }
592 4 : GNUNET_free (ss);
593 4 : }
594 :
595 :
596 : struct TALER_TESTING_Command
597 4 : 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 4 : GNUNET_assert (NULL != coin_reference);
605 4 : ss = GNUNET_new (struct HistoryState);
606 4 : ss->coin_reference = coin_reference;
607 4 : ss->expected_balance = expected_balance;
608 4 : ss->expected_response_code = expected_response_code;
609 : {
610 4 : 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 4 : return cmd;
619 : }
620 : }
|