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