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