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