Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2018-2022 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_deposit.c
21 : * @brief command for testing /batch-deposit.
22 : * @author Marcello Stanisci
23 : * @author Christian Grothoff
24 : */
25 : #include "platform.h"
26 : #include "taler_json_lib.h"
27 : #include <gnunet/gnunet_curl_lib.h>
28 : #include "taler_testing_lib.h"
29 : #include "taler_signatures.h"
30 : #include "backoff.h"
31 :
32 :
33 : /**
34 : * How often do we retry before giving up?
35 : */
36 : #define NUM_RETRIES 5
37 :
38 : /**
39 : * How long do we wait AT MOST when retrying?
40 : */
41 : #define MAX_BACKOFF GNUNET_TIME_relative_multiply ( \
42 : GNUNET_TIME_UNIT_MILLISECONDS, 100)
43 :
44 :
45 : /**
46 : * Information per coin in the batch.
47 : */
48 : struct Coin
49 : {
50 :
51 : /**
52 : * Amount to deposit.
53 : */
54 : struct TALER_Amount amount;
55 :
56 : /**
57 : * Deposit fee.
58 : */
59 : struct TALER_Amount deposit_fee;
60 :
61 : /**
62 : * Reference to any command that is able to provide a coin,
63 : * possibly using $LABEL#$INDEX notation.
64 : */
65 : char *coin_reference;
66 :
67 : /**
68 : * The command being referenced.
69 : */
70 : const struct TALER_TESTING_Command *coin_cmd;
71 :
72 : /**
73 : * Index of the coin at @e coin_cmd.
74 : */
75 : unsigned int coin_idx;
76 : };
77 :
78 :
79 : /**
80 : * State for a "batch deposit" CMD.
81 : */
82 : struct BatchDepositState
83 : {
84 :
85 : /**
86 : * Refund deadline. Zero for no refunds.
87 : */
88 : struct GNUNET_TIME_Timestamp refund_deadline;
89 :
90 : /**
91 : * Wire deadline.
92 : */
93 : struct GNUNET_TIME_Timestamp wire_deadline;
94 :
95 : /**
96 : * Timestamp of the /deposit operation in the wallet (contract signing time).
97 : */
98 : struct GNUNET_TIME_Timestamp wallet_timestamp;
99 :
100 : /**
101 : * How long do we wait until we retry?
102 : */
103 : struct GNUNET_TIME_Relative backoff;
104 :
105 : /**
106 : * When did the exchange receive the deposit?
107 : */
108 : struct GNUNET_TIME_Timestamp exchange_timestamp;
109 :
110 : /**
111 : * Signing key used by the exchange to sign the
112 : * deposit confirmation.
113 : */
114 : struct TALER_ExchangePublicKeyP exchange_pub;
115 :
116 : /**
117 : * Set (by the interpreter) to a fresh private key. This
118 : * key will be used to sign the deposit request.
119 : */
120 : struct TALER_MerchantPrivateKeyP merchant_priv;
121 :
122 : /**
123 : * Deposit handle while operation is running.
124 : */
125 : struct TALER_EXCHANGE_BatchDepositHandle *dh;
126 :
127 : /**
128 : * Array of coins to batch-deposit.
129 : */
130 : struct Coin *coins;
131 :
132 : /**
133 : * Wire details of who is depositing -- this would be merchant
134 : * wire details in a normal scenario.
135 : */
136 : json_t *wire_details;
137 :
138 : /**
139 : * JSON string describing what a proposal is about.
140 : */
141 : json_t *contract_terms;
142 :
143 : /**
144 : * Interpreter state.
145 : */
146 : struct TALER_TESTING_Interpreter *is;
147 :
148 : /**
149 : * Task scheduled to try later.
150 : */
151 : struct GNUNET_SCHEDULER_Task *retry_task;
152 :
153 : /**
154 : * Array of @e num_coins signatures from the exchange on the
155 : * deposit confirmation.
156 : */
157 : struct TALER_ExchangeSignatureP *exchange_sigs;
158 :
159 : /**
160 : * Reference to previous deposit operation.
161 : * Only present if we're supposed to replay the previous deposit.
162 : */
163 : const char *deposit_reference;
164 :
165 : /**
166 : * If @e coin_reference refers to an operation that generated
167 : * an array of coins, this value determines which coin to pick.
168 : */
169 : unsigned int num_coins;
170 :
171 : /**
172 : * Expected HTTP response code.
173 : */
174 : unsigned int expected_response_code;
175 :
176 : /**
177 : * Set to true if the /deposit succeeded
178 : * and we now can provide the resulting traits.
179 : */
180 : bool deposit_succeeded;
181 :
182 : };
183 :
184 :
185 : /**
186 : * Callback to analyze the /batch-deposit response, just used to check if the
187 : * response code is acceptable.
188 : *
189 : * @param cls closure.
190 : * @param dr deposit response details
191 : */
192 : static void
193 0 : batch_deposit_cb (void *cls,
194 : const struct TALER_EXCHANGE_BatchDepositResult *dr)
195 : {
196 0 : struct BatchDepositState *ds = cls;
197 :
198 0 : ds->dh = NULL;
199 0 : if (ds->expected_response_code != dr->hr.http_status)
200 : {
201 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
202 : "Unexpected response code %u to command %s in %s:%u\n",
203 : dr->hr.http_status,
204 : ds->is->commands[ds->is->ip].label,
205 : __FILE__,
206 : __LINE__);
207 0 : json_dumpf (dr->hr.reply,
208 : stderr,
209 : JSON_INDENT (2));
210 0 : TALER_TESTING_interpreter_fail (ds->is);
211 0 : return;
212 : }
213 0 : if (MHD_HTTP_OK == dr->hr.http_status)
214 : {
215 0 : if (ds->num_coins != dr->details.success.num_signatures)
216 : {
217 0 : GNUNET_break (0);
218 0 : TALER_TESTING_interpreter_fail (ds->is);
219 0 : return;
220 : }
221 0 : ds->deposit_succeeded = GNUNET_YES;
222 0 : ds->exchange_timestamp = dr->details.success.deposit_timestamp;
223 0 : ds->exchange_pub = *dr->details.success.exchange_pub;
224 0 : ds->exchange_sigs = GNUNET_memdup (dr->details.success.exchange_sigs,
225 : dr->details.success.num_signatures
226 : * sizeof (struct
227 : TALER_ExchangeSignatureP));
228 : }
229 0 : TALER_TESTING_interpreter_next (ds->is);
230 : }
231 :
232 :
233 : /**
234 : * Run the command.
235 : *
236 : * @param cls closure.
237 : * @param cmd the command to execute.
238 : * @param is the interpreter state.
239 : */
240 : static void
241 0 : batch_deposit_run (void *cls,
242 : const struct TALER_TESTING_Command *cmd,
243 : struct TALER_TESTING_Interpreter *is)
244 0 : {
245 0 : struct BatchDepositState *ds = cls;
246 : const struct TALER_EXCHANGE_DenomPublicKey *denom_pub;
247 : const struct TALER_DenominationSignature *denom_pub_sig;
248 : struct TALER_MerchantPublicKeyP merchant_pub;
249 : struct TALER_PrivateContractHashP h_contract_terms;
250 : enum TALER_ErrorCode ec;
251 : struct TALER_WireSaltP wire_salt;
252 : struct TALER_MerchantWireHashP h_wire;
253 : const char *payto_uri;
254 0 : struct TALER_EXCHANGE_CoinDepositDetail cdds[ds->num_coins];
255 : struct GNUNET_JSON_Specification spec[] = {
256 0 : GNUNET_JSON_spec_string ("payto_uri",
257 : &payto_uri),
258 0 : GNUNET_JSON_spec_fixed_auto ("salt",
259 : &wire_salt),
260 0 : GNUNET_JSON_spec_end ()
261 : };
262 :
263 : (void) cmd;
264 0 : memset (cdds,
265 : 0,
266 : sizeof (cdds));
267 0 : ds->is = is;
268 0 : GNUNET_assert (NULL != ds->wire_details);
269 0 : if (GNUNET_OK !=
270 0 : GNUNET_JSON_parse (ds->wire_details,
271 : spec,
272 : NULL, NULL))
273 : {
274 0 : json_dumpf (ds->wire_details,
275 : stderr,
276 : JSON_INDENT (2));
277 0 : GNUNET_break (0);
278 0 : TALER_TESTING_interpreter_fail (is);
279 0 : return;
280 : }
281 0 : if (GNUNET_OK !=
282 0 : TALER_JSON_contract_hash (ds->contract_terms,
283 : &h_contract_terms))
284 : {
285 0 : GNUNET_break (0);
286 0 : TALER_TESTING_interpreter_fail (is);
287 0 : return;
288 : }
289 0 : GNUNET_assert (GNUNET_OK ==
290 : TALER_JSON_merchant_wire_signature_hash (ds->wire_details,
291 : &h_wire));
292 0 : if (! GNUNET_TIME_absolute_is_zero (ds->refund_deadline.abs_time))
293 : {
294 : struct GNUNET_TIME_Relative refund_deadline;
295 :
296 : refund_deadline
297 0 : = GNUNET_TIME_absolute_get_remaining (ds->refund_deadline.abs_time);
298 : ds->wire_deadline
299 : =
300 0 : GNUNET_TIME_relative_to_timestamp (
301 : GNUNET_TIME_relative_multiply (refund_deadline,
302 : 2));
303 : }
304 : else
305 : {
306 0 : ds->refund_deadline = ds->wallet_timestamp;
307 0 : ds->wire_deadline = GNUNET_TIME_timestamp_get ();
308 : }
309 0 : GNUNET_CRYPTO_eddsa_key_get_public (&ds->merchant_priv.eddsa_priv,
310 : &merchant_pub.eddsa_pub);
311 :
312 0 : for (unsigned int i = 0; i<ds->num_coins; i++)
313 : {
314 0 : struct Coin *coin = &ds->coins[i];
315 0 : struct TALER_EXCHANGE_CoinDepositDetail *cdd = &cdds[i];
316 : const struct TALER_CoinSpendPrivateKeyP *coin_priv;
317 0 : const struct TALER_AgeCommitmentProof *age_commitment_proof = NULL;
318 :
319 0 : GNUNET_assert (NULL != coin->coin_reference);
320 0 : cdd->amount = coin->amount;
321 0 : coin->coin_cmd = TALER_TESTING_interpreter_lookup_command (
322 : is,
323 0 : coin->coin_reference);
324 0 : if (NULL == coin->coin_cmd)
325 : {
326 0 : GNUNET_break (0);
327 0 : TALER_TESTING_interpreter_fail (is);
328 0 : return;
329 : }
330 :
331 0 : if ( (GNUNET_OK !=
332 0 : TALER_TESTING_get_trait_coin_priv (coin->coin_cmd,
333 : coin->coin_idx,
334 0 : &coin_priv)) ||
335 : (GNUNET_OK !=
336 0 : TALER_TESTING_get_trait_age_commitment_proof (coin->coin_cmd,
337 : coin->coin_idx,
338 : &age_commitment_proof))
339 0 : ||
340 : (GNUNET_OK !=
341 0 : TALER_TESTING_get_trait_denom_pub (coin->coin_cmd,
342 : coin->coin_idx,
343 0 : &denom_pub)) ||
344 : (GNUNET_OK !=
345 0 : TALER_TESTING_get_trait_denom_sig (coin->coin_cmd,
346 : coin->coin_idx,
347 : &denom_pub_sig)) )
348 : {
349 0 : GNUNET_break (0);
350 0 : TALER_TESTING_interpreter_fail (is);
351 0 : return;
352 : }
353 0 : if (NULL != age_commitment_proof)
354 : {
355 0 : TALER_age_commitment_hash (&age_commitment_proof->commitment,
356 : &cdd->h_age_commitment);
357 : }
358 0 : coin->deposit_fee = denom_pub->fees.deposit;
359 0 : GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv->eddsa_priv,
360 : &cdd->coin_pub.eddsa_pub);
361 0 : cdd->denom_sig = *denom_pub_sig;
362 0 : cdd->h_denom_pub = denom_pub->h_key;
363 0 : TALER_wallet_deposit_sign (&coin->amount,
364 0 : &denom_pub->fees.deposit,
365 : &h_wire,
366 : &h_contract_terms,
367 0 : &cdd->h_age_commitment,
368 : NULL, /* FIXME #7270: add hash of extensions */
369 0 : &denom_pub->h_key,
370 : ds->wallet_timestamp,
371 : &merchant_pub,
372 : ds->refund_deadline,
373 : coin_priv,
374 : &cdd->coin_sig);
375 : }
376 :
377 0 : GNUNET_assert (NULL == ds->dh);
378 : {
379 0 : struct TALER_EXCHANGE_DepositContractDetail dcd = {
380 : .wire_deadline = ds->wire_deadline,
381 : .merchant_payto_uri = payto_uri,
382 : .wire_salt = wire_salt,
383 : .h_contract_terms = h_contract_terms,
384 : .extension_details = NULL /* FIXME #7270-OEC */,
385 : .timestamp = ds->wallet_timestamp,
386 : .merchant_pub = merchant_pub,
387 : .refund_deadline = ds->refund_deadline
388 : };
389 :
390 0 : ds->dh = TALER_EXCHANGE_batch_deposit (is->exchange,
391 : &dcd,
392 : ds->num_coins,
393 : cdds,
394 : &batch_deposit_cb,
395 : ds,
396 : &ec);
397 : }
398 0 : if (NULL == ds->dh)
399 : {
400 0 : GNUNET_break (0);
401 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
402 : "Could not create deposit with EC %d\n",
403 : (int) ec);
404 0 : TALER_TESTING_interpreter_fail (is);
405 0 : return;
406 : }
407 : }
408 :
409 :
410 : /**
411 : * Free the state of a "batch-deposit" CMD, and possibly cancel a
412 : * pending operation thereof.
413 : *
414 : * @param cls closure, must be a `struct BatchDepositState`.
415 : * @param cmd the command which is being cleaned up.
416 : */
417 : static void
418 0 : batch_deposit_cleanup (void *cls,
419 : const struct TALER_TESTING_Command *cmd)
420 : {
421 0 : struct BatchDepositState *ds = cls;
422 :
423 0 : if (NULL != ds->dh)
424 : {
425 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
426 : "Command %u (%s) did not complete\n",
427 : ds->is->ip,
428 : cmd->label);
429 0 : TALER_EXCHANGE_batch_deposit_cancel (ds->dh);
430 0 : ds->dh = NULL;
431 : }
432 0 : if (NULL != ds->retry_task)
433 : {
434 0 : GNUNET_SCHEDULER_cancel (ds->retry_task);
435 0 : ds->retry_task = NULL;
436 : }
437 0 : for (unsigned int i = 0; i<ds->num_coins; i++)
438 0 : GNUNET_free (ds->coins[i].coin_reference);
439 0 : GNUNET_free (ds->coins);
440 0 : GNUNET_free (ds->exchange_sigs);
441 0 : json_decref (ds->wire_details);
442 0 : json_decref (ds->contract_terms);
443 0 : GNUNET_free (ds);
444 0 : }
445 :
446 :
447 : /**
448 : * Offer internal data from a "batch-deposit" CMD, to other commands.
449 : *
450 : * @param cls closure.
451 : * @param[out] ret result.
452 : * @param trait name of the trait.
453 : * @param index index number of the object to offer.
454 : * @return #GNUNET_OK on success.
455 : */
456 : static enum GNUNET_GenericReturnValue
457 0 : batch_deposit_traits (void *cls,
458 : const void **ret,
459 : const char *trait,
460 : unsigned int index)
461 : {
462 0 : struct BatchDepositState *ds = cls;
463 0 : struct Coin *coin = &ds->coins[index];
464 : /* Will point to coin cmd internals. */
465 : const struct TALER_CoinSpendPrivateKeyP *coin_spent_priv;
466 : const struct TALER_AgeCommitmentProof *age_commitment_proof;
467 :
468 0 : if (index >= ds->num_coins)
469 : {
470 0 : GNUNET_break (0);
471 0 : return GNUNET_NO;
472 : }
473 0 : if (NULL == coin->coin_cmd)
474 : {
475 0 : GNUNET_break (0);
476 0 : TALER_TESTING_interpreter_fail (ds->is);
477 0 : return GNUNET_NO;
478 : }
479 0 : if ( (GNUNET_OK !=
480 0 : TALER_TESTING_get_trait_coin_priv (coin->coin_cmd,
481 : coin->coin_idx,
482 0 : &coin_spent_priv)) ||
483 : (GNUNET_OK !=
484 0 : TALER_TESTING_get_trait_age_commitment_proof (coin->coin_cmd,
485 : coin->coin_idx,
486 : &age_commitment_proof)) )
487 : {
488 0 : GNUNET_break (0);
489 0 : TALER_TESTING_interpreter_fail (ds->is);
490 0 : return GNUNET_NO;
491 : }
492 : {
493 : struct TALER_TESTING_Trait traits[] = {
494 : /* First two traits are only available if
495 : ds->traits is #GNUNET_YES */
496 0 : TALER_TESTING_make_trait_exchange_pub (index,
497 0 : &ds->exchange_pub),
498 0 : TALER_TESTING_make_trait_exchange_sig (index,
499 0 : &ds->exchange_sigs[index]),
500 : /* These traits are always available */
501 0 : TALER_TESTING_make_trait_wire_details (ds->wire_details),
502 0 : TALER_TESTING_make_trait_contract_terms (ds->contract_terms),
503 0 : TALER_TESTING_make_trait_merchant_priv (&ds->merchant_priv),
504 0 : TALER_TESTING_make_trait_age_commitment_proof (index,
505 : age_commitment_proof),
506 0 : TALER_TESTING_make_trait_coin_priv (index,
507 : coin_spent_priv),
508 0 : TALER_TESTING_make_trait_deposit_amount (index,
509 0 : &coin->amount),
510 0 : TALER_TESTING_make_trait_deposit_fee_amount (index,
511 0 : &coin->deposit_fee),
512 :
513 0 : TALER_TESTING_make_trait_timestamp (index,
514 0 : &ds->exchange_timestamp),
515 0 : TALER_TESTING_make_trait_wire_deadline (index,
516 0 : &ds->wire_deadline),
517 0 : TALER_TESTING_make_trait_refund_deadline (index,
518 0 : &ds->refund_deadline),
519 0 : TALER_TESTING_trait_end ()
520 : };
521 :
522 0 : return TALER_TESTING_get_trait ((ds->deposit_succeeded)
523 : ? traits
524 : : &traits[2],
525 : ret,
526 : trait,
527 : index);
528 : }
529 : }
530 :
531 :
532 : struct TALER_TESTING_Command
533 0 : TALER_TESTING_cmd_batch_deposit (const char *label,
534 : const char *target_account_payto,
535 : const char *contract_terms,
536 : struct GNUNET_TIME_Relative refund_deadline,
537 : unsigned int expected_response_code,
538 : ...)
539 : {
540 : struct BatchDepositState *ds;
541 : va_list ap;
542 0 : unsigned int num_coins = 0;
543 : const char *ref;
544 :
545 0 : va_start (ap,
546 : expected_response_code);
547 0 : while (NULL != (ref = va_arg (ap,
548 : const char *)))
549 : {
550 0 : GNUNET_assert (NULL != va_arg (ap,
551 : const char *));
552 0 : num_coins++;
553 : }
554 0 : va_end (ap);
555 :
556 0 : ds = GNUNET_new (struct BatchDepositState);
557 0 : ds->num_coins = num_coins;
558 0 : ds->coins = GNUNET_new_array (num_coins,
559 : struct Coin);
560 0 : num_coins = 0;
561 0 : va_start (ap,
562 : expected_response_code);
563 0 : while (NULL != (ref = va_arg (ap,
564 : const char *)))
565 : {
566 0 : struct Coin *coin = &ds->coins[num_coins++];
567 0 : const char *amount = va_arg (ap,
568 : const char *);
569 :
570 0 : GNUNET_assert (GNUNET_OK ==
571 : TALER_TESTING_parse_coin_reference (ref,
572 : &coin->coin_reference,
573 : &coin->coin_idx));
574 0 : GNUNET_assert (GNUNET_OK ==
575 : TALER_string_to_amount (amount,
576 : &coin->amount));
577 : }
578 0 : va_end (ap);
579 :
580 0 : ds->wire_details = TALER_TESTING_make_wire_details (target_account_payto);
581 0 : GNUNET_assert (NULL != ds->wire_details);
582 0 : ds->contract_terms = json_loads (contract_terms,
583 : JSON_REJECT_DUPLICATES,
584 : NULL);
585 0 : GNUNET_CRYPTO_eddsa_key_create (&ds->merchant_priv.eddsa_priv);
586 0 : if (NULL == ds->contract_terms)
587 : {
588 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
589 : "Failed to parse contract terms `%s' for CMD `%s'\n",
590 : contract_terms,
591 : label);
592 0 : GNUNET_assert (0);
593 : }
594 0 : ds->wallet_timestamp = GNUNET_TIME_timestamp_get ();
595 0 : GNUNET_assert (0 ==
596 : json_object_set_new (ds->contract_terms,
597 : "timestamp",
598 : GNUNET_JSON_from_timestamp (
599 : ds->wallet_timestamp)));
600 0 : if (! GNUNET_TIME_relative_is_zero (refund_deadline))
601 : {
602 0 : ds->refund_deadline = GNUNET_TIME_relative_to_timestamp (refund_deadline);
603 0 : GNUNET_assert (0 ==
604 : json_object_set_new (ds->contract_terms,
605 : "refund_deadline",
606 : GNUNET_JSON_from_timestamp (
607 : ds->refund_deadline)));
608 : }
609 0 : ds->expected_response_code = expected_response_code;
610 : {
611 0 : struct TALER_TESTING_Command cmd = {
612 : .cls = ds,
613 : .label = label,
614 : .run = &batch_deposit_run,
615 : .cleanup = &batch_deposit_cleanup,
616 : .traits = &batch_deposit_traits
617 : };
618 :
619 0 : return cmd;
620 : }
621 : }
622 :
623 :
624 : /* end of testing_api_cmd_batch_deposit.c */
|