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 "platform.h"
27 : #include "taler_exchange_service.h"
28 : #include "taler_json_lib.h"
29 : #include <microhttpd.h>
30 : #include <gnunet/gnunet_curl_lib.h>
31 : #include "taler_signatures.h"
32 : #include "taler_extensions.h"
33 : #include "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_WithdrawHandle *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_WithdrawResponse *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 :
258 : (void) cmd;
259 2 : ws->is = is;
260 : create_reserve
261 2 : = TALER_TESTING_interpreter_lookup_command (
262 : is,
263 : ws->reserve_reference);
264 :
265 2 : if (NULL == create_reserve)
266 : {
267 0 : GNUNET_break (0);
268 0 : TALER_TESTING_interpreter_fail (is);
269 0 : return;
270 : }
271 2 : if (GNUNET_OK !=
272 2 : TALER_TESTING_get_trait_reserve_priv (create_reserve,
273 : &rp))
274 : {
275 0 : GNUNET_break (0);
276 0 : TALER_TESTING_interpreter_fail (is);
277 0 : return;
278 : }
279 2 : if (NULL == ws->exchange_url)
280 : ws->exchange_url
281 2 : = GNUNET_strdup (TALER_TESTING_get_exchange_url (is));
282 2 : ws->reserve_priv = *rp;
283 2 : GNUNET_CRYPTO_eddsa_key_get_public (&ws->reserve_priv.eddsa_priv,
284 : &ws->reserve_pub.eddsa_pub);
285 : ws->reserve_payto_uri
286 2 : = TALER_reserve_make_payto (ws->exchange_url,
287 2 : &ws->reserve_pub);
288 :
289 :
290 2 : GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK,
291 2 : &ws->seed,
292 : sizeof(ws->seed));
293 :
294 : /**
295 : * This is the same expansion that happens inside the call to
296 : * TALER_EXCHANGE_withdraw. We save the expanded
297 : * secrets later per coin state.
298 : */
299 2 : TALER_withdraw_expand_secrets (ws->num_coins,
300 2 : &ws->seed,
301 : secrets);
302 :
303 2 : GNUNET_assert (ws->num_coins > 0);
304 2 : GNUNET_assert (GNUNET_OK ==
305 : TALER_amount_set_zero (
306 : ws->coins[0].amount.currency,
307 : &ws->reserve_history.amount));
308 2 : GNUNET_assert (GNUNET_OK ==
309 : TALER_amount_set_zero (
310 : ws->coins[0].amount.currency,
311 : &ws->reserve_history.details.withdraw.fee));
312 :
313 6 : for (unsigned int i = 0; i<ws->num_coins; i++)
314 : {
315 4 : struct CoinState *cs = &ws->coins[i];
316 : struct TALER_Amount amount;
317 :
318 :
319 4 : cs->secret = secrets[i];
320 :
321 4 : dpk = TALER_TESTING_find_pk (keys,
322 4 : &cs->amount,
323 : false); /* no age restriction */
324 4 : if (NULL == dpk)
325 : {
326 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
327 : "Failed to determine denomination key at %s\n",
328 : (NULL != cmd) ? cmd->label : "<retried command>");
329 0 : GNUNET_break (0);
330 0 : TALER_TESTING_interpreter_fail (is);
331 0 : return;
332 : }
333 : /* We copy the denomination key, as re-querying /keys
334 : * would free the old one. */
335 4 : cs->pk = TALER_EXCHANGE_copy_denomination_key (dpk);
336 :
337 4 : GNUNET_assert (GNUNET_OK ==
338 : TALER_amount_set_zero (
339 : cs->amount.currency,
340 : &amount));
341 4 : GNUNET_assert (0 <=
342 : TALER_amount_add (
343 : &amount,
344 : &cs->amount,
345 : &cs->pk->fees.withdraw));
346 4 : GNUNET_assert (0 <=
347 : TALER_amount_add (
348 : &ws->reserve_history.amount,
349 : &ws->reserve_history.amount,
350 : &amount));
351 4 : GNUNET_assert (0 <=
352 : TALER_amount_add (
353 : &ws->reserve_history.details.withdraw.fee,
354 : &ws->reserve_history.details.withdraw.fee,
355 : &cs->pk->fees.withdraw));
356 :
357 4 : denoms_pub[i] = *cs->pk;
358 4 : TALER_denom_pub_copy (&denoms_pub[i].key,
359 4 : &cs->pk->key);
360 : }
361 :
362 2 : ws->reserve_history.type = TALER_EXCHANGE_RTT_WITHDRAWAL;
363 :
364 2 : ws->wsh = TALER_EXCHANGE_withdraw (
365 : TALER_TESTING_interpreter_get_context (is),
366 : keys,
367 : TALER_TESTING_get_exchange_url (is),
368 : rp,
369 2 : ws->num_coins,
370 : denoms_pub,
371 2 : &ws->seed,
372 : 0,
373 : &batch_withdraw_cb,
374 : ws);
375 2 : if (NULL == ws->wsh)
376 : {
377 0 : GNUNET_break (0);
378 0 : TALER_TESTING_interpreter_fail (is);
379 0 : return;
380 : }
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_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 38 : batch_withdraw_traits (void *cls,
434 : const void **ret,
435 : const char *trait,
436 : unsigned int index)
437 : {
438 38 : struct BatchWithdrawState *ws = cls;
439 38 : 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 38 : TALER_TESTING_make_trait_reserve_history (index,
443 38 : &ws->reserve_history),
444 38 : TALER_TESTING_make_trait_coin_priv (index,
445 38 : &cs->details.coin_priv),
446 38 : TALER_TESTING_make_trait_coin_pub (index,
447 38 : &cs->details.coin_pub),
448 38 : TALER_TESTING_make_trait_planchet_secrets (index,
449 38 : &cs->secret),
450 38 : TALER_TESTING_make_trait_blinding_key (index,
451 38 : &cs->details.blinding_key),
452 38 : TALER_TESTING_make_trait_exchange_blinding_values (index,
453 38 : &cs->details.
454 : blinding_values),
455 38 : TALER_TESTING_make_trait_denom_pub (index,
456 38 : cs->pk),
457 38 : TALER_TESTING_make_trait_denom_sig (index,
458 38 : &cs->details.denom_sig),
459 38 : TALER_TESTING_make_trait_withdraw_seed (&ws->seed),
460 38 : TALER_TESTING_make_trait_withdraw_commitment (&ws->planchets_h),
461 38 : TALER_TESTING_make_trait_reserve_priv (&ws->reserve_priv),
462 38 : TALER_TESTING_make_trait_reserve_pub (&ws->reserve_pub),
463 38 : TALER_TESTING_make_trait_amounts (index,
464 38 : &cs->amount),
465 38 : TALER_TESTING_make_trait_legi_requirement_row (&ws->requirement_row),
466 38 : TALER_TESTING_make_trait_h_normalized_payto (&ws->h_payto),
467 38 : TALER_TESTING_make_trait_normalized_payto_uri (&ws->reserve_payto_uri),
468 38 : TALER_TESTING_make_trait_exchange_url (ws->exchange_url),
469 38 : TALER_TESTING_make_trait_age_commitment_proof (index,
470 38 : ws->age > 0 ?
471 : &cs->details.
472 : age_commitment_proof:
473 : NULL),
474 38 : TALER_TESTING_make_trait_h_age_commitment (index,
475 38 : ws->age > 0 ?
476 : &cs->details.h_age_commitment :
477 : NULL),
478 38 : TALER_TESTING_trait_end ()
479 : };
480 :
481 38 : if (index >= ws->num_coins)
482 0 : return GNUNET_NO;
483 38 : 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 */
|