Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2018-2026 Taler Systems SA
4 :
5 : TALER is free software; you can redistribute it and/or modify it under the
6 : terms of the GNU General Public License as published by the Free Software
7 : Foundation; either version 3, or (at your option) any later version.
8 :
9 : TALER is distributed in the hope that it will be useful, but WITHOUT ANY
10 : WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11 : A PARTICULAR PURPOSE. See the GNU General Public License for more details.
12 :
13 : You should have received a copy of the GNU General Public License along with
14 : TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
15 : */
16 : /**
17 : * @file testing/testing_api_cmd_recoup.c
18 : * @brief Implement the /recoup-withdraw test command.
19 : * @author Marcello Stanisci
20 : * @author Christian Grothoff
21 : * @author Özgür Kesim
22 : */
23 : #include "platform.h"
24 : #include "taler/taler_json_lib.h"
25 : #include <gnunet/gnunet_curl_lib.h>
26 : #include "taler/taler_testing_lib.h"
27 :
28 :
29 : /**
30 : * State for a "recoup-withdraw" CMD.
31 : */
32 : struct RecoupState
33 : {
34 : /**
35 : * Expected HTTP status code.
36 : */
37 : unsigned int expected_response_code;
38 :
39 : /**
40 : * Label of the withdraw command the coins came from.
41 : */
42 : char *withdraw_reference;
43 :
44 : /**
45 : * Indices (in the withdraw command) of the coins to recoup.
46 : */
47 : unsigned int *indices;
48 :
49 : /**
50 : * Number of entries in @e indices, @e ches, @e rhes and @e coin_pubs.
51 : */
52 : size_t num_indices;
53 :
54 : /**
55 : * Amount we expect to be recouped per coin, invalid if
56 : * not specified.
57 : */
58 : struct TALER_Amount amount;
59 :
60 : /**
61 : * Interpreter state.
62 : */
63 : struct TALER_TESTING_Interpreter *is;
64 :
65 : /**
66 : * Handle to the request.
67 : */
68 : struct TALER_EXCHANGE_PostRecoupWithdrawHandle *ph;
69 :
70 : /**
71 : * Reserve that is credited.
72 : */
73 : struct TALER_ReservePublicKeyP reserve_pub;
74 :
75 : /**
76 : * Coin history entries we expect, one per recouped coin.
77 : */
78 : struct TALER_EXCHANGE_CoinHistoryEntry *ches;
79 :
80 : /**
81 : * Reserve history entries we expect, one per recouped coin.
82 : */
83 : struct TALER_EXCHANGE_ReserveHistoryEntry *rhes;
84 :
85 : /**
86 : * Public keys of the recouped coins.
87 : */
88 : struct TALER_CoinSpendPublicKeyP *coin_pubs;
89 : };
90 :
91 :
92 : /**
93 : * Check the response.
94 : *
95 : * @param cls closure, our `struct RecoupState`
96 : * @param rr response details
97 : */
98 : static void
99 22 : recoup_cb (void *cls,
100 : const struct TALER_EXCHANGE_PostRecoupWithdrawResponse *rr)
101 : {
102 22 : struct RecoupState *ps = cls;
103 22 : const struct TALER_EXCHANGE_HttpResponse *hr = &rr->hr;
104 22 : struct TALER_TESTING_Interpreter *is = ps->is;
105 :
106 22 : ps->ph = NULL;
107 22 : if (ps->expected_response_code != hr->http_status)
108 : {
109 0 : TALER_TESTING_unexpected_status (is,
110 : hr->http_status,
111 : ps->expected_response_code);
112 0 : return;
113 : }
114 22 : switch (hr->http_status)
115 : {
116 18 : case MHD_HTTP_OK:
117 18 : if (0 != GNUNET_memcmp (&rr->details.ok.reserve_pub,
118 : &ps->reserve_pub))
119 : {
120 0 : GNUNET_break (0);
121 0 : TALER_TESTING_interpreter_fail (is);
122 0 : return;
123 : }
124 18 : if (rr->details.ok.num_recoups != ps->num_indices)
125 : {
126 0 : GNUNET_break (0);
127 0 : TALER_TESTING_interpreter_fail (is);
128 0 : return;
129 : }
130 42 : for (size_t i = 0; i < ps->num_indices; i++)
131 : {
132 24 : const struct TALER_RecoupedCoin *rc = &rr->details.ok.recoups[i];
133 :
134 24 : if (0 != GNUNET_memcmp (&rc->coin_pub,
135 : &ps->coin_pubs[i]))
136 : {
137 0 : GNUNET_break (0);
138 0 : TALER_TESTING_interpreter_fail (is);
139 0 : return;
140 : }
141 24 : if ( (GNUNET_OK ==
142 48 : TALER_amount_is_valid (&ps->amount)) &&
143 24 : (0 != TALER_amount_cmp (&rc->amount,
144 24 : &ps->amount)) )
145 : {
146 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
147 : "Recouped %s for coin %u, expected %s\n",
148 : TALER_amount2s (&rc->amount),
149 : ps->indices[i],
150 : TALER_amount2s (&ps->amount));
151 0 : GNUNET_break (0);
152 0 : TALER_TESTING_interpreter_fail (is);
153 0 : return;
154 : }
155 24 : ps->ches[i].amount = rc->amount;
156 24 : ps->rhes[i].amount = rc->amount;
157 24 : ps->rhes[i].type = TALER_EXCHANGE_RTT_RECOUP;
158 : }
159 18 : break;
160 4 : default:
161 4 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
162 : "Recoup-withdraw failed as expected with HTTP status %u/%d\n",
163 : hr->http_status,
164 : (int) hr->ec);
165 4 : break;
166 : }
167 22 : TALER_TESTING_interpreter_next (is);
168 : }
169 :
170 :
171 : /**
172 : * Run the command.
173 : *
174 : * @param cls closure.
175 : * @param cmd the command to execute.
176 : * @param is the interpreter state.
177 : */
178 : static void
179 22 : recoup_run (void *cls,
180 : const struct TALER_TESTING_Command *cmd,
181 : struct TALER_TESTING_Interpreter *is)
182 : {
183 22 : struct RecoupState *ps = cls;
184 : const struct TALER_TESTING_Command *coin_cmd;
185 : const struct TALER_ReservePublicKeyP *reserve_pub;
186 : const struct TALER_WithdrawMasterSeedP *seed;
187 : const struct TALER_HashBlindedPlanchetsP *h_planchets;
188 : struct TALER_BlindingMasterSeedP blinding_seed;
189 22 : bool have_cs = false;
190 22 : size_t num_coins = 0;
191 :
192 : (void) cmd;
193 22 : ps->is = is;
194 22 : coin_cmd = TALER_TESTING_interpreter_lookup_command (is,
195 22 : ps->withdraw_reference);
196 22 : if (NULL == coin_cmd)
197 : {
198 0 : GNUNET_break (0);
199 0 : TALER_TESTING_interpreter_fail (is);
200 0 : return;
201 : }
202 22 : if ( (GNUNET_OK !=
203 22 : TALER_TESTING_get_trait_reserve_pub (coin_cmd,
204 22 : &reserve_pub)) ||
205 : (GNUNET_OK !=
206 22 : TALER_TESTING_get_trait_withdraw_seed (coin_cmd,
207 22 : &seed)) ||
208 : (GNUNET_OK !=
209 22 : TALER_TESTING_get_trait_withdraw_commitment (coin_cmd,
210 : &h_planchets)) )
211 : {
212 0 : GNUNET_break (0);
213 0 : TALER_TESTING_interpreter_fail (is);
214 0 : return;
215 : }
216 22 : ps->reserve_pub = *reserve_pub;
217 : /* count the coins of the withdraw operation */
218 56 : while (num_coins < TALER_MAX_COINS)
219 : {
220 : const struct TALER_EXCHANGE_DenomPublicKey *pk;
221 :
222 56 : if (GNUNET_OK !=
223 56 : TALER_TESTING_get_trait_denom_pub (coin_cmd,
224 : (unsigned int) num_coins,
225 : &pk))
226 22 : break;
227 34 : num_coins++;
228 : }
229 22 : if (0 == num_coins)
230 : {
231 0 : GNUNET_break (0);
232 0 : TALER_TESTING_interpreter_fail (is);
233 0 : return;
234 : }
235 50 : for (size_t i = 0; i < ps->num_indices; i++)
236 28 : if (ps->indices[i] >= num_coins)
237 : {
238 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
239 : "Coin index %u out of range for command %s (%u coins)\n",
240 : ps->indices[i],
241 : ps->withdraw_reference,
242 : (unsigned int) num_coins);
243 0 : GNUNET_break (0);
244 0 : TALER_TESTING_interpreter_fail (is);
245 0 : return;
246 : }
247 : /* the CS blinding seed is derived from the withdraw seed */
248 22 : TALER_cs_withdraw_seed_to_blinding_seed (seed,
249 : &blinding_seed);
250 22 : {
251 22 : struct TALER_PlanchetMasterSecretP secrets[num_coins];
252 22 : struct TALER_EXCHANGE_RecoupCoin coins[num_coins];
253 22 : size_t k = 0;
254 :
255 22 : TALER_withdraw_expand_secrets (num_coins,
256 : seed,
257 : secrets);
258 22 : memset (coins,
259 : 0,
260 : sizeof (coins));
261 56 : for (size_t i = 0; i < num_coins; i++)
262 : {
263 34 : struct TALER_EXCHANGE_RecoupCoin *c = &coins[i];
264 34 : const struct TALER_AgeCommitmentHashP *hac = NULL;
265 34 : bool recoup = false;
266 :
267 34 : if ( (GNUNET_OK !=
268 34 : TALER_TESTING_get_trait_denom_pub (coin_cmd,
269 : (unsigned int) i,
270 34 : &c->pk)) ||
271 : (GNUNET_OK !=
272 34 : TALER_TESTING_get_trait_exchange_blinding_values (
273 : coin_cmd,
274 : (unsigned int) i,
275 : &c->exchange_vals)) )
276 : {
277 0 : GNUNET_break (0);
278 0 : TALER_TESTING_interpreter_fail (is);
279 0 : return;
280 : }
281 : /* only denominations with age restriction have a commitment */
282 40 : if ( (0 != c->pk->key.age_mask.bits) &&
283 : (GNUNET_OK ==
284 6 : TALER_TESTING_get_trait_h_age_commitment (coin_cmd,
285 : (unsigned int) i,
286 : &hac)) )
287 6 : c->h_age_commitment = hac; /* may still be NULL */
288 : /* commands with per-coin secrets (batch and age withdraw) expose
289 : them; the single withdraw derives them from the seed */
290 34 : if (GNUNET_OK !=
291 34 : TALER_TESTING_get_trait_planchet_secrets (coin_cmd,
292 : (unsigned int) i,
293 : &c->ps))
294 16 : c->ps = &secrets[i];
295 34 : if (GNUNET_CRYPTO_BSA_CS == c->pk->key.bsign_pub_key->cipher)
296 17 : have_cs = true;
297 86 : for (size_t j = 0; j < ps->num_indices; j++)
298 52 : if (ps->indices[j] == i)
299 28 : recoup = true;
300 34 : if (! recoup)
301 6 : continue;
302 28 : if (GNUNET_OK !=
303 28 : TALER_TESTING_get_trait_denom_sig (coin_cmd,
304 : (unsigned int) i,
305 : &c->denom_sig))
306 : {
307 0 : GNUNET_break (0);
308 0 : TALER_TESTING_interpreter_fail (is);
309 0 : return;
310 : }
311 : /* what the histories should show for this coin */
312 : {
313 28 : struct TALER_EXCHANGE_CoinHistoryEntry *che = &ps->ches[k];
314 28 : struct TALER_EXCHANGE_ReserveHistoryEntry *rhe = &ps->rhes[k];
315 : struct TALER_CoinSpendPrivateKeyP coin_priv;
316 28 : const struct TALER_ExchangeBlindingValues *vals = c->exchange_vals;
317 :
318 28 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
319 : "Trying to recoup coin %u of denomination '%s'\n",
320 : (unsigned int) i,
321 : TALER_B2S (&c->pk->h_key));
322 28 : TALER_planchet_setup_coin_priv (c->ps,
323 : vals,
324 : &coin_priv);
325 28 : GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv.eddsa_priv,
326 28 : &ps->coin_pubs[k].eddsa_pub);
327 28 : che->type = TALER_EXCHANGE_CTT_RECOUP;
328 28 : che->amount = ps->amount;
329 28 : che->details.recoup.reserve_pub = ps->reserve_pub;
330 28 : che->details.recoup.planchets_h = *h_planchets;
331 28 : che->details.recoup.coin_index = (uint32_t) i;
332 28 : TALER_planchet_blinding_secret_create (c->ps,
333 : vals,
334 : &che->details.recoup.coin_bks);
335 : /* exercise the documented contract: the request needs no
336 : exchange values for RSA denominations */
337 28 : if (GNUNET_CRYPTO_BSA_RSA == c->pk->key.bsign_pub_key->cipher)
338 14 : c->exchange_vals = NULL;
339 28 : TALER_wallet_recoup_sign (&c->pk->h_key,
340 28 : &che->details.recoup.coin_bks,
341 : &coin_priv,
342 : &che->details.recoup.coin_sig);
343 28 : rhe->amount = ps->amount;
344 28 : rhe->details.recoup_details.coin_pub = ps->coin_pubs[k];
345 28 : rhe->details.recoup_details.planchets_h = *h_planchets;
346 28 : k++;
347 : }
348 : }
349 44 : ps->ph = TALER_EXCHANGE_post_recoup_withdraw_create (
350 : TALER_TESTING_interpreter_get_context (is),
351 : TALER_TESTING_get_exchange_url (is),
352 : TALER_TESTING_get_keys (is),
353 22 : &ps->reserve_pub,
354 : h_planchets,
355 : have_cs ? &blinding_seed : NULL,
356 : num_coins,
357 : coins);
358 : }
359 22 : if (NULL == ps->ph)
360 : {
361 0 : GNUNET_break (0);
362 0 : TALER_TESTING_interpreter_fail (is);
363 0 : return;
364 : }
365 22 : GNUNET_assert (TALER_EC_NONE ==
366 : TALER_EXCHANGE_post_recoup_withdraw_start (ps->ph,
367 : &recoup_cb,
368 : ps));
369 : }
370 :
371 :
372 : /**
373 : * Cleanup the state.
374 : *
375 : * @param cls closure, must be a `struct RecoupState`.
376 : * @param cmd the command which is being cleaned up.
377 : */
378 : static void
379 22 : recoup_cleanup (void *cls,
380 : const struct TALER_TESTING_Command *cmd)
381 : {
382 22 : struct RecoupState *ps = cls;
383 :
384 : (void) cmd;
385 22 : if (NULL != ps->ph)
386 : {
387 0 : TALER_EXCHANGE_post_recoup_withdraw_cancel (ps->ph);
388 0 : ps->ph = NULL;
389 : }
390 22 : GNUNET_free (ps->withdraw_reference);
391 22 : GNUNET_free (ps->indices);
392 22 : GNUNET_free (ps->ches);
393 22 : GNUNET_free (ps->rhes);
394 22 : GNUNET_free (ps->coin_pubs);
395 22 : GNUNET_free (ps);
396 22 : }
397 :
398 :
399 : /**
400 : * Offer internal data to other commands: one coin history entry
401 : * and one reserve history entry per recouped coin.
402 : *
403 : * @param cls closure
404 : * @param[out] ret result (could be anything)
405 : * @param trait name of the trait
406 : * @param index index number of the object to extract.
407 : * @return #GNUNET_OK on success
408 : */
409 : static enum GNUNET_GenericReturnValue
410 34 : recoup_traits (void *cls,
411 : const void **ret,
412 : const char *trait,
413 : unsigned int index)
414 : {
415 34 : struct RecoupState *ps = cls;
416 :
417 34 : if (index >= ps->num_indices)
418 8 : return GNUNET_NO;
419 26 : if (TALER_EXCHANGE_RTT_RECOUP != ps->rhes[index].type)
420 4 : return GNUNET_SYSERR; /* request did not succeed: no traits */
421 : {
422 : struct TALER_TESTING_Trait traits[] = {
423 22 : TALER_TESTING_make_trait_reserve_pub (&ps->reserve_pub),
424 22 : TALER_TESTING_make_trait_reserve_history (index,
425 22 : &ps->rhes[index]),
426 22 : TALER_TESTING_make_trait_coin_history (index,
427 22 : &ps->ches[index]),
428 22 : TALER_TESTING_make_trait_coin_pub (index,
429 22 : &ps->coin_pubs[index]),
430 22 : TALER_TESTING_trait_end ()
431 : };
432 :
433 22 : return TALER_TESTING_get_trait (traits,
434 : ret,
435 : trait,
436 : index);
437 : }
438 : }
439 :
440 :
441 : /**
442 : * Create the command state shared by both constructors.
443 : *
444 : * @param label the command label
445 : * @param expected_response_code expected HTTP status code
446 : * @param withdraw_reference label of the withdraw command
447 : * @param indices coins to recoup, becomes owned by the state
448 : * @param num_indices length of @a indices
449 : * @param amount expected amount per coin, NULL for none
450 : * @return the command
451 : */
452 : static struct TALER_TESTING_Command
453 22 : make_recoup_cmd (const char *label,
454 : unsigned int expected_response_code,
455 : const char *withdraw_reference,
456 : unsigned int *indices,
457 : size_t num_indices,
458 : const char *amount)
459 : {
460 : struct RecoupState *ps;
461 :
462 22 : ps = GNUNET_new (struct RecoupState);
463 22 : ps->expected_response_code = expected_response_code;
464 22 : ps->withdraw_reference = GNUNET_strdup (withdraw_reference);
465 22 : ps->indices = indices;
466 22 : ps->num_indices = num_indices;
467 22 : ps->ches = GNUNET_new_array (num_indices,
468 : struct TALER_EXCHANGE_CoinHistoryEntry);
469 22 : ps->rhes = GNUNET_new_array (num_indices,
470 : struct TALER_EXCHANGE_ReserveHistoryEntry);
471 22 : ps->coin_pubs = GNUNET_new_array (num_indices,
472 : struct TALER_CoinSpendPublicKeyP);
473 44 : if ( (NULL != amount) &&
474 : (GNUNET_OK !=
475 22 : TALER_string_to_amount (amount,
476 : &ps->amount)) )
477 : {
478 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
479 : "Failed to parse amount `%s' at %s\n",
480 : amount,
481 : label);
482 0 : GNUNET_assert (0);
483 : }
484 : {
485 22 : struct TALER_TESTING_Command cmd = {
486 : .cls = ps,
487 : .label = label,
488 : .run = &recoup_run,
489 : .cleanup = &recoup_cleanup,
490 : .traits = &recoup_traits
491 : };
492 :
493 22 : return cmd;
494 : }
495 : }
496 :
497 :
498 : struct TALER_TESTING_Command
499 16 : TALER_TESTING_cmd_recoup (const char *label,
500 : unsigned int expected_response_code,
501 : const char *coin_reference,
502 : const char *amount)
503 : {
504 : char *cref;
505 : unsigned int idx;
506 : unsigned int *indices;
507 : struct TALER_TESTING_Command cmd;
508 :
509 16 : GNUNET_assert (GNUNET_OK ==
510 : TALER_TESTING_parse_coin_reference (coin_reference,
511 : &cref,
512 : &idx));
513 16 : indices = GNUNET_new_array (1,
514 : unsigned int);
515 16 : indices[0] = idx;
516 16 : cmd = make_recoup_cmd (label,
517 : expected_response_code,
518 : cref,
519 : indices,
520 : 1,
521 : amount);
522 16 : GNUNET_free (cref);
523 16 : return cmd;
524 : }
525 :
526 :
527 : struct TALER_TESTING_Command
528 6 : TALER_TESTING_cmd_recoup_batch (const char *label,
529 : unsigned int expected_response_code,
530 : const char *withdraw_reference,
531 : const char *indices,
532 : const char *amount)
533 : {
534 : unsigned int *idx;
535 : size_t num;
536 :
537 6 : idx = TALER_TESTING_parse_index_list (indices,
538 : &num);
539 6 : GNUNET_assert (NULL != idx);
540 6 : return make_recoup_cmd (label,
541 : expected_response_code,
542 : withdraw_reference,
543 : idx,
544 : num,
545 : amount);
546 : }
547 :
548 :
549 : /* end of testing_api_cmd_recoup.c */
|