Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2018-2025 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_withdraw.c
21 : * @brief implements the batch withdraw command
22 : * @author Christian Grothoff
23 : * @author Marcello Stanisci
24 : * @author Özgür Kesim
25 : */
26 : #include "taler/taler_json_lib.h"
27 : #include <microhttpd.h>
28 : #include <gnunet/gnunet_curl_lib.h>
29 : #include "taler/taler_signatures.h"
30 : #include "taler/taler_extensions.h"
31 : #include "taler/taler_testing_lib.h"
32 :
33 : /**
34 : * Information we track per withdrawn coin.
35 : */
36 : struct CoinState
37 : {
38 :
39 : /**
40 : * String describing the denomination value we should withdraw.
41 : * A corresponding denomination key must exist in the exchange's
42 : * offerings. Can be NULL if @e pk is set instead.
43 : */
44 : struct TALER_Amount amount;
45 :
46 : /**
47 : * If @e amount is NULL, this specifies the denomination key to
48 : * use. Otherwise, this will be set (by the interpreter) to the
49 : * denomination PK matching @e amount.
50 : */
51 : struct TALER_EXCHANGE_DenomPublicKey *pk;
52 :
53 : /**
54 : * Coin Details, as returned by the withdrawal operation
55 : */
56 : struct TALER_EXCHANGE_WithdrawCoinPrivateDetails details;
57 :
58 : /**
59 : * Set (by the interpreter) to the exchange's signature over the
60 : * coin's public key.
61 : */
62 : struct TALER_BlindedDenominationSignature blinded_denom_sig;
63 :
64 : /**
65 : * Private key material of the coin, set by the interpreter.
66 : */
67 : struct TALER_PlanchetMasterSecretP secret;
68 :
69 :
70 : };
71 :
72 :
73 : /**
74 : * State for a "batch withdraw" CMD.
75 : */
76 : struct BatchWithdrawState
77 : {
78 :
79 : /**
80 : * Which reserve should we withdraw from?
81 : */
82 : const char *reserve_reference;
83 :
84 : /**
85 : * Exchange base URL. Only used as offered trait.
86 : */
87 : char *exchange_url;
88 :
89 : /**
90 : * URI if the reserve we are withdrawing from.
91 : */
92 : struct TALER_NormalizedPayto reserve_payto_uri;
93 :
94 : /**
95 : * Private key of the reserve we are withdrawing from.
96 : */
97 : struct TALER_ReservePrivateKeyP reserve_priv;
98 :
99 : /**
100 : * Public key of the reserve we are withdrawing from.
101 : */
102 : struct TALER_ReservePublicKeyP reserve_pub;
103 :
104 : /**
105 : * Interpreter state (during command).
106 : */
107 : struct TALER_TESTING_Interpreter *is;
108 :
109 : /**
110 : * Withdraw handle (while operation is running).
111 : */
112 : struct TALER_EXCHANGE_PostWithdrawHandle *wsh;
113 :
114 : /**
115 : * Array of coin states.
116 : */
117 : struct CoinState *coins;
118 :
119 : /**
120 : * The seed from which the batch of seeds for the coins is derived
121 : */
122 : struct TALER_WithdrawMasterSeedP seed;
123 :
124 :
125 : /**
126 : * Set to the KYC requirement payto hash *if* the exchange replied with a
127 : * request for KYC.
128 : */
129 : struct TALER_NormalizedPaytoHashP h_payto;
130 :
131 : /**
132 : * Set to the KYC requirement row *if* the exchange replied with
133 : * a request for KYC.
134 : */
135 : uint64_t requirement_row;
136 :
137 : /**
138 : * Length of the @e coins array.
139 : */
140 : unsigned int num_coins;
141 :
142 : /**
143 : * An age > 0 signifies age restriction is applied.
144 : * Same for all coins in the batch.
145 : */
146 : uint8_t age;
147 :
148 : /**
149 : * Expected HTTP response code to the request.
150 : */
151 : unsigned int expected_response_code;
152 :
153 :
154 : /**
155 : * Reserve history entry that corresponds to this withdrawal.
156 : * Will be of type #TALER_EXCHANGE_RTT_WITHDRAWAL.
157 : */
158 : struct TALER_EXCHANGE_ReserveHistoryEntry reserve_history;
159 :
160 : /**
161 : * The commitment of the call to withdraw, needed later for recoup.
162 : */
163 : struct TALER_HashBlindedPlanchetsP planchets_h;
164 :
165 : };
166 :
167 :
168 : /**
169 : * "batch withdraw" operation callback; checks that the
170 : * response code is expected and store the exchange signature
171 : * in the state.
172 : *
173 : * @param cls closure.
174 : * @param wr withdraw response details
175 : */
176 : static void
177 2 : batch_withdraw_cb (void *cls,
178 : const struct
179 : TALER_EXCHANGE_PostWithdrawResponse *wr)
180 : {
181 2 : struct BatchWithdrawState *ws = cls;
182 2 : struct TALER_TESTING_Interpreter *is = ws->is;
183 :
184 2 : ws->wsh = NULL;
185 2 : if (ws->expected_response_code != wr->hr.http_status)
186 : {
187 0 : TALER_TESTING_unexpected_status_with_body (is,
188 : wr->hr.http_status,
189 : ws->expected_response_code,
190 : wr->hr.reply);
191 0 : return;
192 : }
193 2 : switch (wr->hr.http_status)
194 : {
195 2 : case MHD_HTTP_OK:
196 6 : for (unsigned int i = 0; i<ws->num_coins; i++)
197 : {
198 4 : struct CoinState *cs = &ws->coins[i];
199 :
200 4 : cs->details = wr->details.ok.coin_details[i];
201 4 : TALER_denom_sig_copy (&cs->details.denom_sig,
202 4 : &wr->details.ok.coin_details[i].denom_sig);
203 4 : TALER_denom_ewv_copy (&cs->details.blinding_values,
204 4 : &wr->details.ok.coin_details[i].blinding_values);
205 : }
206 2 : ws->planchets_h = wr->details.ok.planchets_h;
207 2 : break;
208 0 : case MHD_HTTP_FORBIDDEN:
209 : /* nothing to check */
210 0 : break;
211 0 : case MHD_HTTP_NOT_FOUND:
212 : /* nothing to check */
213 0 : break;
214 0 : case MHD_HTTP_CONFLICT:
215 : /* FIXME[oec]: Check if age-requirement is the reason */
216 0 : break;
217 0 : case MHD_HTTP_GONE:
218 : /* theoretically could check that the key was actually */
219 0 : break;
220 0 : case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
221 : /* nothing to check */
222 : ws->requirement_row
223 0 : = wr->details.unavailable_for_legal_reasons.requirement_row;
224 : ws->h_payto
225 0 : = wr->details.unavailable_for_legal_reasons.h_payto;
226 0 : break;
227 0 : default:
228 : /* Unsupported status code (by test harness) */
229 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
230 : "Batch withdraw test command does not support status code %u\n",
231 : wr->hr.http_status);
232 0 : GNUNET_break (0);
233 0 : break;
234 : }
235 2 : TALER_TESTING_interpreter_next (is);
236 : }
237 :
238 :
239 : /**
240 : * Run the command.
241 : */
242 : static void
243 2 : batch_withdraw_run (void *cls,
244 : const struct TALER_TESTING_Command *cmd,
245 : struct TALER_TESTING_Interpreter *is)
246 2 : {
247 2 : struct BatchWithdrawState *ws = cls;
248 2 : struct TALER_EXCHANGE_Keys *keys = TALER_TESTING_get_keys (is);
249 : const struct TALER_ReservePrivateKeyP *rp;
250 : const struct TALER_TESTING_Command *create_reserve;
251 : const struct TALER_EXCHANGE_DenomPublicKey *dpk;
252 2 : struct TALER_EXCHANGE_DenomPublicKey denoms_pub[ws->num_coins];
253 2 : struct TALER_PlanchetMasterSecretP secrets[ws->num_coins];
254 :
255 : (void) cmd;
256 2 : ws->is = is;
257 : create_reserve
258 2 : = TALER_TESTING_interpreter_lookup_command (
259 : is,
260 : ws->reserve_reference);
261 :
262 2 : if (NULL == create_reserve)
263 : {
264 0 : GNUNET_break (0);
265 0 : TALER_TESTING_interpreter_fail (is);
266 0 : return;
267 : }
268 2 : if (GNUNET_OK !=
269 2 : TALER_TESTING_get_trait_reserve_priv (create_reserve,
270 : &rp))
271 : {
272 0 : GNUNET_break (0);
273 0 : TALER_TESTING_interpreter_fail (is);
274 0 : return;
275 : }
276 2 : if (NULL == ws->exchange_url)
277 : ws->exchange_url
278 2 : = GNUNET_strdup (TALER_TESTING_get_exchange_url (is));
279 2 : ws->reserve_priv = *rp;
280 2 : GNUNET_CRYPTO_eddsa_key_get_public (&ws->reserve_priv.eddsa_priv,
281 : &ws->reserve_pub.eddsa_pub);
282 : ws->reserve_payto_uri
283 2 : = TALER_reserve_make_payto (ws->exchange_url,
284 2 : &ws->reserve_pub);
285 :
286 2 : GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK,
287 2 : &ws->seed,
288 : sizeof(ws->seed));
289 :
290 : /**
291 : * This is the same expansion that happens inside the call to
292 : * TALER_EXCHANGE_withdraw. We save the expanded
293 : * secrets later per coin state.
294 : */
295 2 : TALER_withdraw_expand_secrets (ws->num_coins,
296 2 : &ws->seed,
297 : secrets);
298 :
299 2 : GNUNET_assert (ws->num_coins > 0);
300 2 : GNUNET_assert (GNUNET_OK ==
301 : TALER_amount_set_zero (
302 : ws->coins[0].amount.currency,
303 : &ws->reserve_history.amount));
304 2 : GNUNET_assert (GNUNET_OK ==
305 : TALER_amount_set_zero (
306 : ws->coins[0].amount.currency,
307 : &ws->reserve_history.details.withdraw.fee));
308 :
309 6 : for (unsigned int i = 0; i<ws->num_coins; i++)
310 : {
311 4 : struct CoinState *cs = &ws->coins[i];
312 : struct TALER_Amount amount;
313 :
314 :
315 4 : cs->secret = secrets[i];
316 :
317 4 : dpk = TALER_TESTING_find_pk (keys,
318 4 : &cs->amount,
319 : false); /* no age restriction */
320 4 : if (NULL == dpk)
321 : {
322 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
323 : "Failed to determine denomination key at %s\n",
324 : (NULL != cmd) ? cmd->label : "<retried command>");
325 0 : GNUNET_break (0);
326 0 : TALER_TESTING_interpreter_fail (is);
327 0 : return;
328 : }
329 : /* We copy the denomination key, as re-querying /keys
330 : * would free the old one. */
331 4 : cs->pk = TALER_EXCHANGE_copy_denomination_key (dpk);
332 :
333 4 : GNUNET_assert (GNUNET_OK ==
334 : TALER_amount_set_zero (
335 : cs->amount.currency,
336 : &amount));
337 4 : GNUNET_assert (0 <=
338 : TALER_amount_add (
339 : &amount,
340 : &cs->amount,
341 : &cs->pk->fees.withdraw));
342 4 : GNUNET_assert (0 <=
343 : TALER_amount_add (
344 : &ws->reserve_history.amount,
345 : &ws->reserve_history.amount,
346 : &amount));
347 4 : GNUNET_assert (0 <=
348 : TALER_amount_add (
349 : &ws->reserve_history.details.withdraw.fee,
350 : &ws->reserve_history.details.withdraw.fee,
351 : &cs->pk->fees.withdraw));
352 :
353 4 : denoms_pub[i] = *cs->pk;
354 4 : TALER_denom_pub_copy (&denoms_pub[i].key,
355 4 : &cs->pk->key);
356 : }
357 :
358 2 : ws->reserve_history.type = TALER_EXCHANGE_RTT_WITHDRAWAL;
359 :
360 2 : ws->wsh = TALER_EXCHANGE_post_withdraw_create (
361 : TALER_TESTING_interpreter_get_context (is),
362 : TALER_TESTING_get_exchange_url (is),
363 : keys,
364 : rp,
365 2 : ws->num_coins,
366 : denoms_pub,
367 2 : &ws->seed,
368 : 0);
369 6 : for (unsigned int i = 0; i<ws->num_coins; i++)
370 4 : TALER_denom_pub_free (&denoms_pub[i].key);
371 2 : if (NULL == ws->wsh)
372 : {
373 0 : GNUNET_break (0);
374 0 : TALER_TESTING_interpreter_fail (is);
375 0 : return;
376 : }
377 2 : GNUNET_assert (TALER_EC_NONE ==
378 : TALER_EXCHANGE_post_withdraw_start (ws->wsh,
379 : &batch_withdraw_cb,
380 : ws));
381 : }
382 :
383 :
384 : /**
385 : * Free the state of a "withdraw" CMD, and possibly cancel
386 : * a pending operation thereof.
387 : *
388 : * @param cls closure.
389 : * @param cmd the command being freed.
390 : */
391 : static void
392 2 : batch_withdraw_cleanup (void *cls,
393 : const struct TALER_TESTING_Command *cmd)
394 : {
395 2 : struct BatchWithdrawState *ws = cls;
396 :
397 2 : if (NULL != ws->wsh)
398 : {
399 0 : TALER_TESTING_command_incomplete (ws->is,
400 : cmd->label);
401 0 : TALER_EXCHANGE_post_withdraw_cancel (ws->wsh);
402 0 : ws->wsh = NULL;
403 : }
404 6 : for (unsigned int i = 0; i<ws->num_coins; i++)
405 : {
406 4 : struct CoinState *cs = &ws->coins[i];
407 4 : TALER_denom_ewv_free (&cs->details.blinding_values);
408 4 : TALER_denom_sig_free (&cs->details.denom_sig);
409 4 : if (NULL != cs->pk)
410 : {
411 4 : TALER_EXCHANGE_destroy_denomination_key (cs->pk);
412 4 : cs->pk = NULL;
413 : }
414 : }
415 2 : GNUNET_free (ws->coins);
416 2 : GNUNET_free (ws->exchange_url);
417 2 : GNUNET_free (ws->reserve_payto_uri.normalized_payto);
418 2 : GNUNET_free (ws);
419 2 : }
420 :
421 :
422 : /**
423 : * Offer internal data to a "withdraw" CMD state to other
424 : * commands.
425 : *
426 : * @param cls closure
427 : * @param[out] ret result (could be anything)
428 : * @param trait name of the trait
429 : * @param index index number of the object to offer.
430 : * @return #GNUNET_OK on success
431 : */
432 : static enum GNUNET_GenericReturnValue
433 44 : batch_withdraw_traits (void *cls,
434 : const void **ret,
435 : const char *trait,
436 : unsigned int index)
437 : {
438 44 : struct BatchWithdrawState *ws = cls;
439 44 : struct CoinState *cs = &ws->coins[index];
440 : struct TALER_TESTING_Trait traits[] = {
441 : /* history entry MUST be first due to response code logic below! */
442 44 : TALER_TESTING_make_trait_reserve_history (index,
443 44 : &ws->reserve_history),
444 44 : TALER_TESTING_make_trait_coin_priv (index,
445 44 : &cs->details.coin_priv),
446 44 : TALER_TESTING_make_trait_coin_pub (index,
447 44 : &cs->details.coin_pub),
448 44 : TALER_TESTING_make_trait_planchet_secrets (index,
449 44 : &cs->secret),
450 44 : TALER_TESTING_make_trait_blinding_key (index,
451 44 : &cs->details.blinding_key),
452 44 : TALER_TESTING_make_trait_exchange_blinding_values (index,
453 44 : &cs->details.
454 : blinding_values),
455 44 : TALER_TESTING_make_trait_denom_pub (index,
456 44 : cs->pk),
457 44 : TALER_TESTING_make_trait_denom_sig (index,
458 44 : &cs->details.denom_sig),
459 44 : TALER_TESTING_make_trait_withdraw_seed (&ws->seed),
460 44 : TALER_TESTING_make_trait_withdraw_commitment (&ws->planchets_h),
461 44 : TALER_TESTING_make_trait_reserve_priv (&ws->reserve_priv),
462 44 : TALER_TESTING_make_trait_reserve_pub (&ws->reserve_pub),
463 44 : TALER_TESTING_make_trait_amounts (index,
464 44 : &cs->amount),
465 44 : TALER_TESTING_make_trait_legi_requirement_row (&ws->requirement_row),
466 44 : TALER_TESTING_make_trait_h_normalized_payto (&ws->h_payto),
467 44 : TALER_TESTING_make_trait_normalized_payto_uri (&ws->reserve_payto_uri),
468 44 : TALER_TESTING_make_trait_exchange_url (ws->exchange_url),
469 44 : TALER_TESTING_make_trait_age_commitment_proof (index,
470 44 : ws->age > 0 ?
471 : &cs->details.
472 : age_commitment_proof:
473 : NULL),
474 44 : TALER_TESTING_make_trait_h_age_commitment (index,
475 44 : ws->age > 0 ?
476 : &cs->details.h_age_commitment :
477 : NULL),
478 44 : TALER_TESTING_trait_end ()
479 : };
480 :
481 44 : if (index >= ws->num_coins)
482 0 : return GNUNET_NO;
483 44 : return TALER_TESTING_get_trait ((ws->expected_response_code == MHD_HTTP_OK)
484 : ? &traits[0] /* we have reserve history */
485 : : &traits[1], /* skip reserve history */
486 : ret,
487 : trait,
488 : index);
489 : }
490 :
491 :
492 : struct TALER_TESTING_Command
493 2 : TALER_TESTING_cmd_batch_withdraw (
494 : const char *label,
495 : const char *reserve_reference,
496 : unsigned int expected_response_code,
497 : const char *amount,
498 : ...)
499 : {
500 : struct BatchWithdrawState *ws;
501 : unsigned int cnt;
502 : va_list ap;
503 :
504 2 : ws = GNUNET_new (struct BatchWithdrawState);
505 2 : ws->reserve_reference = reserve_reference;
506 2 : ws->expected_response_code = expected_response_code;
507 :
508 2 : cnt = 1;
509 2 : va_start (ap,
510 : amount);
511 4 : while (NULL != (va_arg (ap,
512 : const char *)))
513 2 : cnt++;
514 2 : ws->num_coins = cnt;
515 2 : ws->coins = GNUNET_new_array (cnt,
516 : struct CoinState);
517 2 : va_end (ap);
518 2 : va_start (ap,
519 : amount);
520 6 : for (unsigned int i = 0; i<ws->num_coins; i++)
521 : {
522 4 : struct CoinState *cs = &ws->coins[i];
523 :
524 4 : if (GNUNET_OK !=
525 4 : TALER_string_to_amount (amount,
526 : &cs->amount))
527 : {
528 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
529 : "Failed to parse amount `%s' at %s\n",
530 : amount,
531 : label);
532 0 : GNUNET_assert (0);
533 : }
534 : /* move on to next vararg! */
535 4 : amount = va_arg (ap,
536 : const char *);
537 : }
538 2 : GNUNET_assert (NULL == amount);
539 2 : va_end (ap);
540 :
541 : {
542 2 : struct TALER_TESTING_Command cmd = {
543 : .cls = ws,
544 : .label = label,
545 : .run = &batch_withdraw_run,
546 : .cleanup = &batch_withdraw_cleanup,
547 : .traits = &batch_withdraw_traits
548 : };
549 :
550 2 : return cmd;
551 : }
552 : }
553 :
554 :
555 : /* end of testing_api_cmd_batch_withdraw.c */
|