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 : /**
125 : * Set to the KYC requirement payto hash *if* the exchange replied with a
126 : * request for KYC.
127 : */
128 : struct TALER_NormalizedPaytoHashP h_payto;
129 :
130 : /**
131 : * Set to the KYC requirement row *if* the exchange replied with
132 : * a request for KYC.
133 : */
134 : uint64_t requirement_row;
135 :
136 : /**
137 : * Length of the @e coins array.
138 : */
139 : unsigned int num_coins;
140 :
141 : /**
142 : * An age > 0 signifies age restriction is applied.
143 : * Same for all coins in the batch.
144 : */
145 : uint8_t age;
146 :
147 : /**
148 : * Expected HTTP response code to the request.
149 : */
150 : unsigned int expected_response_code;
151 :
152 :
153 : /**
154 : * Reserve history entry that corresponds to this withdrawal.
155 : * Will be of type #TALER_EXCHANGE_RTT_WITHDRAWAL.
156 : */
157 : struct TALER_EXCHANGE_ReserveHistoryEntry reserve_history;
158 :
159 : /**
160 : * The commitment of the call to withdraw, needed later for recoup.
161 : */
162 : struct TALER_HashBlindedPlanchetsP planchets_h;
163 :
164 : };
165 :
166 :
167 : /**
168 : * "batch withdraw" operation callback; checks that the
169 : * response code is expected and store the exchange signature
170 : * in the state.
171 : *
172 : * @param cls closure.
173 : * @param wr withdraw response details
174 : */
175 : static void
176 2 : batch_withdraw_cb (void *cls,
177 : const struct
178 : TALER_EXCHANGE_PostWithdrawResponse *wr)
179 : {
180 2 : struct BatchWithdrawState *ws = cls;
181 2 : struct TALER_TESTING_Interpreter *is = ws->is;
182 :
183 2 : ws->wsh = NULL;
184 2 : if (ws->expected_response_code != wr->hr.http_status)
185 : {
186 0 : TALER_TESTING_unexpected_status_with_body (is,
187 : wr->hr.http_status,
188 : ws->expected_response_code,
189 : wr->hr.reply);
190 0 : return;
191 : }
192 2 : switch (wr->hr.http_status)
193 : {
194 2 : case MHD_HTTP_OK:
195 6 : for (unsigned int i = 0; i<ws->num_coins; i++)
196 : {
197 4 : struct CoinState *cs = &ws->coins[i];
198 :
199 4 : cs->details = wr->details.ok.coin_details[i];
200 4 : TALER_denom_sig_copy (&cs->details.denom_sig,
201 4 : &wr->details.ok.coin_details[i].denom_sig);
202 4 : TALER_denom_ewv_copy (&cs->details.blinding_values,
203 4 : &wr->details.ok.coin_details[i].blinding_values);
204 : }
205 2 : ws->planchets_h = wr->details.ok.planchets_h;
206 2 : break;
207 0 : case MHD_HTTP_FORBIDDEN:
208 : /* nothing to check */
209 0 : break;
210 0 : case MHD_HTTP_NOT_FOUND:
211 : /* nothing to check */
212 0 : break;
213 0 : case MHD_HTTP_CONFLICT:
214 : /* FIXME[oec]: Check if age-requirement is the reason */
215 0 : break;
216 0 : case MHD_HTTP_GONE:
217 : /* theoretically could check that the key was actually */
218 0 : break;
219 0 : case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
220 : /* nothing to check */
221 : ws->requirement_row
222 0 : = wr->details.unavailable_for_legal_reasons.requirement_row;
223 : ws->h_payto
224 0 : = wr->details.unavailable_for_legal_reasons.h_payto;
225 0 : break;
226 0 : default:
227 : /* Unsupported status code (by test harness) */
228 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
229 : "Batch withdraw test command does not support status code %u\n",
230 : wr->hr.http_status);
231 0 : GNUNET_break (0);
232 0 : break;
233 : }
234 2 : TALER_TESTING_interpreter_next (is);
235 : }
236 :
237 :
238 : /**
239 : * Run the command.
240 : */
241 : static void
242 2 : batch_withdraw_run (void *cls,
243 : const struct TALER_TESTING_Command *cmd,
244 : struct TALER_TESTING_Interpreter *is)
245 2 : {
246 2 : struct BatchWithdrawState *ws = cls;
247 2 : struct TALER_EXCHANGE_Keys *keys = TALER_TESTING_get_keys (is);
248 : const struct TALER_ReservePrivateKeyP *rp;
249 : const struct TALER_TESTING_Command *create_reserve;
250 : const struct TALER_EXCHANGE_DenomPublicKey *dpk;
251 2 : struct TALER_EXCHANGE_DenomPublicKey denoms_pub[ws->num_coins];
252 2 : struct TALER_PlanchetMasterSecretP secrets[ws->num_coins];
253 :
254 : (void) cmd;
255 2 : ws->is = is;
256 : create_reserve
257 2 : = TALER_TESTING_interpreter_lookup_command (
258 : is,
259 : ws->reserve_reference);
260 :
261 2 : if (NULL == create_reserve)
262 : {
263 0 : GNUNET_break (0);
264 0 : TALER_TESTING_interpreter_fail (is);
265 0 : return;
266 : }
267 2 : if (GNUNET_OK !=
268 2 : TALER_TESTING_get_trait_reserve_priv (create_reserve,
269 : &rp))
270 : {
271 0 : GNUNET_break (0);
272 0 : TALER_TESTING_interpreter_fail (is);
273 0 : return;
274 : }
275 2 : if (NULL == ws->exchange_url)
276 : ws->exchange_url
277 2 : = GNUNET_strdup (TALER_TESTING_get_exchange_url (is));
278 2 : ws->reserve_priv = *rp;
279 2 : GNUNET_CRYPTO_eddsa_key_get_public (&ws->reserve_priv.eddsa_priv,
280 : &ws->reserve_pub.eddsa_pub);
281 : ws->reserve_payto_uri
282 2 : = TALER_reserve_make_payto (ws->exchange_url,
283 2 : &ws->reserve_pub);
284 :
285 2 : GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK,
286 2 : &ws->seed,
287 : sizeof(ws->seed));
288 :
289 : /**
290 : * This is the same expansion that happens inside the call to
291 : * TALER_EXCHANGE_withdraw. We save the expanded
292 : * secrets later per coin state.
293 : */
294 2 : TALER_withdraw_expand_secrets (ws->num_coins,
295 2 : &ws->seed,
296 : secrets);
297 :
298 2 : GNUNET_assert (ws->num_coins > 0);
299 2 : GNUNET_assert (GNUNET_OK ==
300 : TALER_amount_set_zero (
301 : ws->coins[0].amount.currency,
302 : &ws->reserve_history.amount));
303 2 : GNUNET_assert (GNUNET_OK ==
304 : TALER_amount_set_zero (
305 : ws->coins[0].amount.currency,
306 : &ws->reserve_history.details.withdraw.fee));
307 :
308 6 : for (unsigned int i = 0; i<ws->num_coins; i++)
309 : {
310 4 : struct CoinState *cs = &ws->coins[i];
311 : struct TALER_Amount amount;
312 :
313 :
314 4 : cs->secret = secrets[i];
315 :
316 4 : dpk = TALER_TESTING_find_pk (keys,
317 4 : &cs->amount,
318 : false); /* no age restriction */
319 4 : if (NULL == dpk)
320 : {
321 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
322 : "Failed to determine denomination key at %s\n",
323 : (NULL != cmd) ? cmd->label : "<retried command>");
324 0 : GNUNET_break (0);
325 0 : TALER_TESTING_interpreter_fail (is);
326 0 : return;
327 : }
328 : /* We copy the denomination key, as re-querying /keys
329 : * would free the old one. */
330 4 : cs->pk = TALER_EXCHANGE_copy_denomination_key (dpk);
331 :
332 4 : GNUNET_assert (GNUNET_OK ==
333 : TALER_amount_set_zero (
334 : cs->amount.currency,
335 : &amount));
336 4 : GNUNET_assert (0 <=
337 : TALER_amount_add (
338 : &amount,
339 : &cs->amount,
340 : &cs->pk->fees.withdraw));
341 4 : GNUNET_assert (0 <=
342 : TALER_amount_add (
343 : &ws->reserve_history.amount,
344 : &ws->reserve_history.amount,
345 : &amount));
346 4 : GNUNET_assert (0 <=
347 : TALER_amount_add (
348 : &ws->reserve_history.details.withdraw.fee,
349 : &ws->reserve_history.details.withdraw.fee,
350 : &cs->pk->fees.withdraw));
351 :
352 4 : denoms_pub[i] = *cs->pk;
353 4 : TALER_denom_pub_copy (&denoms_pub[i].key,
354 4 : &cs->pk->key);
355 : }
356 :
357 2 : ws->reserve_history.type = TALER_EXCHANGE_RTT_WITHDRAWAL;
358 :
359 2 : ws->wsh = TALER_EXCHANGE_post_withdraw_create (
360 : TALER_TESTING_interpreter_get_context (is),
361 : TALER_TESTING_get_exchange_url (is),
362 : keys,
363 : rp,
364 2 : ws->num_coins,
365 : denoms_pub,
366 2 : &ws->seed,
367 : 0);
368 6 : for (unsigned int i = 0; i<ws->num_coins; i++)
369 4 : TALER_denom_pub_free (&denoms_pub[i].key);
370 2 : if (NULL == ws->wsh)
371 : {
372 0 : GNUNET_break (0);
373 0 : TALER_TESTING_interpreter_fail (is);
374 0 : return;
375 : }
376 2 : GNUNET_assert (TALER_EC_NONE ==
377 : TALER_EXCHANGE_post_withdraw_start (ws->wsh,
378 : &batch_withdraw_cb,
379 : ws));
380 : }
381 :
382 :
383 : /**
384 : * Free the state of a "withdraw" CMD, and possibly cancel
385 : * a pending operation thereof.
386 : *
387 : * @param cls closure.
388 : * @param cmd the command being freed.
389 : */
390 : static void
391 2 : batch_withdraw_cleanup (void *cls,
392 : const struct TALER_TESTING_Command *cmd)
393 : {
394 2 : struct BatchWithdrawState *ws = cls;
395 :
396 2 : if (NULL != ws->wsh)
397 : {
398 0 : TALER_TESTING_command_incomplete (ws->is,
399 : cmd->label);
400 0 : TALER_EXCHANGE_post_withdraw_cancel (ws->wsh);
401 0 : ws->wsh = NULL;
402 : }
403 6 : for (unsigned int i = 0; i<ws->num_coins; i++)
404 : {
405 4 : struct CoinState *cs = &ws->coins[i];
406 4 : TALER_denom_ewv_free (&cs->details.blinding_values);
407 4 : TALER_denom_sig_free (&cs->details.denom_sig);
408 4 : if (NULL != cs->pk)
409 : {
410 4 : TALER_EXCHANGE_destroy_denomination_key (cs->pk);
411 4 : cs->pk = NULL;
412 : }
413 : }
414 2 : GNUNET_free (ws->coins);
415 2 : GNUNET_free (ws->exchange_url);
416 2 : GNUNET_free (ws->reserve_payto_uri.normalized_payto);
417 2 : GNUNET_free (ws);
418 2 : }
419 :
420 :
421 : /**
422 : * Offer internal data to a "withdraw" CMD state to other
423 : * commands.
424 : *
425 : * @param cls closure
426 : * @param[out] ret result (could be anything)
427 : * @param trait name of the trait
428 : * @param index index number of the object to offer.
429 : * @return #GNUNET_OK on success
430 : */
431 : static enum GNUNET_GenericReturnValue
432 44 : batch_withdraw_traits (void *cls,
433 : const void **ret,
434 : const char *trait,
435 : unsigned int index)
436 : {
437 44 : struct BatchWithdrawState *ws = cls;
438 44 : 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 44 : TALER_TESTING_make_trait_reserve_history (index,
442 44 : &ws->reserve_history),
443 44 : TALER_TESTING_make_trait_coin_priv (index,
444 44 : &cs->details.coin_priv),
445 44 : TALER_TESTING_make_trait_coin_pub (index,
446 44 : &cs->details.coin_pub),
447 44 : TALER_TESTING_make_trait_planchet_secrets (index,
448 44 : &cs->secret),
449 44 : TALER_TESTING_make_trait_blinding_key (index,
450 44 : &cs->details.blinding_key),
451 44 : TALER_TESTING_make_trait_exchange_blinding_values (index,
452 44 : &cs->details.
453 : blinding_values),
454 44 : TALER_TESTING_make_trait_denom_pub (index,
455 44 : cs->pk),
456 44 : TALER_TESTING_make_trait_denom_sig (index,
457 44 : &cs->details.denom_sig),
458 44 : TALER_TESTING_make_trait_withdraw_seed (&ws->seed),
459 44 : TALER_TESTING_make_trait_withdraw_commitment (&ws->planchets_h),
460 44 : TALER_TESTING_make_trait_reserve_priv (&ws->reserve_priv),
461 44 : TALER_TESTING_make_trait_reserve_pub (&ws->reserve_pub),
462 44 : TALER_TESTING_make_trait_amounts (index,
463 44 : &cs->amount),
464 44 : TALER_TESTING_make_trait_legi_requirement_row (&ws->requirement_row),
465 44 : TALER_TESTING_make_trait_h_normalized_payto (&ws->h_payto),
466 44 : TALER_TESTING_make_trait_normalized_payto_uri (&ws->reserve_payto_uri),
467 44 : TALER_TESTING_make_trait_exchange_url (ws->exchange_url),
468 44 : TALER_TESTING_make_trait_age_commitment_proof (index,
469 44 : ws->age > 0 ?
470 : &cs->details.
471 : age_commitment_proof:
472 : NULL),
473 44 : TALER_TESTING_make_trait_h_age_commitment (index,
474 44 : ws->age > 0 ?
475 : &cs->details.h_age_commitment :
476 : NULL),
477 44 : TALER_TESTING_trait_end ()
478 : };
479 :
480 44 : if (index >= ws->num_coins)
481 0 : return GNUNET_NO;
482 44 : return TALER_TESTING_get_trait ((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 : struct TALER_TESTING_Command
492 2 : TALER_TESTING_cmd_batch_withdraw (
493 : const char *label,
494 : const char *reserve_reference,
495 : unsigned int expected_response_code,
496 : const char *amount,
497 : ...)
498 : {
499 : struct BatchWithdrawState *ws;
500 : unsigned int cnt;
501 : va_list ap;
502 :
503 2 : ws = GNUNET_new (struct BatchWithdrawState);
504 2 : ws->reserve_reference = reserve_reference;
505 2 : ws->expected_response_code = expected_response_code;
506 :
507 2 : cnt = 1;
508 2 : va_start (ap,
509 : amount);
510 4 : while (NULL != (va_arg (ap,
511 : const char *)))
512 2 : cnt++;
513 2 : ws->num_coins = cnt;
514 2 : ws->coins = GNUNET_new_array (cnt,
515 : struct CoinState);
516 2 : va_end (ap);
517 2 : va_start (ap,
518 : amount);
519 6 : for (unsigned int i = 0; i<ws->num_coins; i++)
520 : {
521 4 : struct CoinState *cs = &ws->coins[i];
522 :
523 4 : if (GNUNET_OK !=
524 4 : TALER_string_to_amount (amount,
525 : &cs->amount))
526 : {
527 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
528 : "Failed to parse amount `%s' at %s\n",
529 : amount,
530 : label);
531 0 : GNUNET_assert (0);
532 : }
533 : /* move on to next vararg! */
534 4 : amount = va_arg (ap,
535 : const char *);
536 : }
537 2 : GNUNET_assert (NULL == amount);
538 2 : va_end (ap);
539 :
540 : {
541 2 : struct TALER_TESTING_Command cmd = {
542 : .cls = ws,
543 : .label = label,
544 : .run = &batch_withdraw_run,
545 : .cleanup = &batch_withdraw_cleanup,
546 : .traits = &batch_withdraw_traits
547 : };
548 :
549 2 : return cmd;
550 : }
551 : }
552 :
553 :
554 : /* end of testing_api_cmd_batch_withdraw.c */
|