Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 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 exchangedb/test_recoup_refresh.c
18 : * @brief tests for the exchangedb functions whose primary table is
19 : * `recoup_refresh`
20 : * @author Christian Grothoff
21 : *
22 : * Covers #TALER_EXCHANGEDB_do_recoup_refresh() and, as far as it can be,
23 : * #TALER_EXCHANGEDB_iterate_recoup_refreshes_above_serial_id().
24 : *
25 : * `recoup_refresh` references `known_coins` and `refresh`, so each check
26 : * builds a melt first. do_recoup_refresh() moves the fresh coin's whole
27 : * remaining balance back to the old coin.
28 : *
29 : * The iterator cannot be checked beyond "does not invent an answer": its
30 : * statement joins `refresh_revealed_coins` and `refresh_commitments`, which
31 : * the schema no longer has (EDB-16 in bugs.txt, in-tree FIXME-#9828).
32 : */
33 : #include "test_common.h"
34 : #include "exchange-database/do_recoup_refresh.h"
35 : #include "exchange-database/do_refresh.h"
36 : #include "exchange-database/iterate_recoup_refreshes_above_serial_id.h"
37 :
38 :
39 : /**
40 : * Account the checks fund their reserves from.
41 : */
42 : static struct TDB_Account account;
43 :
44 :
45 : /**
46 : * Denomination the checks use.
47 : */
48 : static struct TDB_Denom denom;
49 :
50 :
51 : /**
52 : * Melt @a coin and return the row of the melt.
53 : *
54 : * @param pg the database context
55 : * @param coin coin to melt
56 : * @param seed seed for the commitment and signatures
57 : * @param amount how much to melt, e.g. "1"
58 : * @return row of the melt in `refresh`
59 : */
60 : static uint64_t
61 3 : melt (struct TALER_EXCHANGEDB_PostgresContext *pg,
62 : const struct TALER_CoinPublicInfo *coin,
63 : uint32_t seed,
64 : const char *amount)
65 : {
66 : struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS rf;
67 : struct TALER_BlindedDenominationSignature denom_sig;
68 3 : struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
69 : struct TALER_Amount coin_balance;
70 3 : uint64_t denom_serial = denom.serial;
71 : uint64_t refresh_id;
72 : uint32_t noreveal_index;
73 : bool found;
74 3 : bool zombie_required = false;
75 : bool nonce_reuse;
76 : bool balance_ok;
77 :
78 3 : memset (&rf,
79 : 0,
80 : sizeof (rf));
81 3 : rf.coin.coin_pub = coin->coin_pub;
82 3 : rf.coin.denom_pub_hash = coin->denom_pub_hash;
83 3 : rf.coin.no_age_commitment = coin->no_age_commitment;
84 3 : TDB_fill (&rf.coin_sig,
85 : sizeof (rf.coin_sig),
86 : seed);
87 3 : TDB_fill (&rf.rc,
88 : sizeof (rf.rc),
89 : seed);
90 3 : TDB_fill (&rf.refresh_seed,
91 : sizeof (rf.refresh_seed),
92 : seed);
93 3 : TDB_fill (&rf.planchets_h,
94 : sizeof (rf.planchets_h),
95 : seed);
96 3 : TDB_fill (&rf.selected_h,
97 : sizeof (rf.selected_h),
98 : seed + 1);
99 3 : rf.amount_with_fee = TDB_amount (amount);
100 3 : rf.num_coins = 1;
101 3 : rf.denom_serials = &denom_serial;
102 3 : TDB_blinded_denom_sig (seed,
103 : &denom_sig);
104 3 : rf.denom_sigs = &denom_sig;
105 3 : rf.noreveal_index = 0;
106 3 : rf.is_v27_refresh = true;
107 3 : rf.no_blinding_seed = true;
108 3 : GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
109 : TALER_EXCHANGEDB_do_refresh (pg,
110 : &rf,
111 : &now,
112 : &found,
113 : &noreveal_index,
114 : &zombie_required,
115 : &nonce_reuse,
116 : &balance_ok,
117 : &coin_balance));
118 3 : GNUNET_assert (balance_ok);
119 3 : TALER_blinded_denom_sig_free (&denom_sig);
120 : {
121 3 : struct GNUNET_PQ_QueryParam params[] = {
122 3 : GNUNET_PQ_query_param_auto_from_type (&rf.rc),
123 : GNUNET_PQ_query_param_end
124 : };
125 3 : struct GNUNET_PQ_ResultSpec rs[] = {
126 3 : GNUNET_PQ_result_spec_uint64 ("refresh_id",
127 : &refresh_id),
128 : GNUNET_PQ_result_spec_end
129 : };
130 :
131 3 : GNUNET_assert (GNUNET_OK ==
132 : GNUNET_PQ_prepare_anon (pg->conn,
133 : "SELECT refresh_id"
134 : " FROM refresh"
135 : " WHERE rc=$1;"));
136 3 : GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
137 : GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
138 : "",
139 : params,
140 : rs));
141 : }
142 3 : return refresh_id;
143 : }
144 :
145 :
146 : /**
147 : * Callback for
148 : * #TALER_EXCHANGEDB_iterate_recoup_refreshes_above_serial_id().
149 : *
150 : * @param cls a pointer to an `unsigned int` counter
151 : * @param rowid row of the recoup
152 : * @param timestamp when it happened
153 : * @param amount how much went back
154 : * @param old_coin_pub the old coin
155 : * @param old_denom_pub_hash denomination of the old coin
156 : * @param coin the fresh coin
157 : * @param denom_pub denomination of @a coin
158 : * @param coin_sig signature over the request
159 : * @param coin_blind blinding secret of the fresh coin
160 : * @return #GNUNET_OK
161 : */
162 : static enum GNUNET_GenericReturnValue
163 0 : recoup_refresh_cb (void *cls,
164 : uint64_t rowid,
165 : struct GNUNET_TIME_Timestamp timestamp,
166 : const struct TALER_Amount *amount,
167 : const struct TALER_CoinSpendPublicKeyP *old_coin_pub,
168 : const struct TALER_DenominationHashP *old_denom_pub_hash,
169 : const struct TALER_CoinPublicInfo *coin,
170 : const struct TALER_DenominationPublicKey *denom_pub,
171 : const struct TALER_CoinSpendSignatureP *coin_sig,
172 : const union GNUNET_CRYPTO_BlindingSecretP *coin_blind)
173 : {
174 0 : unsigned int *total = cls;
175 :
176 : (void) rowid;
177 : (void) timestamp;
178 : (void) amount;
179 : (void) old_coin_pub;
180 : (void) old_denom_pub_hash;
181 : (void) coin;
182 : (void) denom_pub;
183 : (void) coin_sig;
184 : (void) coin_blind;
185 0 : (*total)++;
186 0 : return GNUNET_OK;
187 : }
188 :
189 :
190 : /**
191 : * Outcome of a recoup-refresh request.
192 : */
193 : struct RecoupStatus
194 : {
195 : /**
196 : * Was the recoup accepted?
197 : */
198 : bool recoup_ok;
199 :
200 : /**
201 : * Did something go wrong inside the database?
202 : */
203 : bool internal_failure;
204 :
205 : /**
206 : * When the recoup happened.
207 : */
208 : struct GNUNET_TIME_Timestamp recoup_timestamp;
209 : };
210 :
211 :
212 : /**
213 : * Recoup a fresh coin back onto the coin it was refreshed from.
214 : *
215 : * @param pg the database context
216 : * @param old_coin_pub coin to credit
217 : * @param refresh_id melt that justifies the recoup
218 : * @param coin_pub fresh coin to drain
219 : * @param known_coin_id row of @a coin_pub
220 : * @param seed seed for the blinding secret and coin signature
221 : * @param[out] st set to the outcome
222 : * @return transaction status
223 : */
224 : static enum GNUNET_DB_QueryStatus
225 4 : run_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg,
226 : const struct TALER_CoinSpendPublicKeyP *old_coin_pub,
227 : uint64_t refresh_id,
228 : const struct TALER_CoinSpendPublicKeyP *coin_pub,
229 : uint64_t known_coin_id,
230 : uint32_t seed,
231 : struct RecoupStatus *st)
232 : {
233 : union GNUNET_CRYPTO_BlindingSecretP coin_bks;
234 : struct TALER_CoinSpendSignatureP coin_sig;
235 :
236 4 : TDB_fill (&coin_bks,
237 : sizeof (coin_bks),
238 : seed);
239 4 : TDB_fill (&coin_sig,
240 : sizeof (coin_sig),
241 : seed);
242 4 : memset (st,
243 : 0,
244 : sizeof (*st));
245 4 : st->recoup_timestamp = GNUNET_TIME_timestamp_get ();
246 4 : return TALER_EXCHANGEDB_do_recoup_refresh (pg,
247 : old_coin_pub,
248 : refresh_id,
249 : &coin_bks,
250 : coin_pub,
251 : known_coin_id,
252 : &coin_sig,
253 : &st->recoup_timestamp,
254 : &st->recoup_ok,
255 : &st->internal_failure);
256 : }
257 :
258 :
259 : /**
260 : * Recouping a coin the exchange does not know is an internal failure.
261 : *
262 : * @param pg the database context
263 : * @return 0 on success
264 : */
265 : static int
266 1 : check_unknown_coin (struct TALER_EXCHANGEDB_PostgresContext *pg)
267 : {
268 : struct TALER_ReservePublicKeyP reserve_pub;
269 : struct TALER_CoinPublicInfo old_coin;
270 : struct TALER_CoinSpendPublicKeyP coin_pub;
271 : struct RecoupStatus st;
272 : uint64_t refresh_id;
273 :
274 1 : TDB_denom (pg,
275 : 10,
276 : "5",
277 : "0.1",
278 : &denom);
279 1 : TDB_account (pg,
280 : 10,
281 : &account);
282 1 : TDB_reserve_in (pg,
283 : &account,
284 : 10,
285 : "10",
286 : &reserve_pub);
287 1 : TDB_coin (pg,
288 : &denom,
289 : 20,
290 : &old_coin,
291 : NULL);
292 1 : refresh_id = melt (pg,
293 : &old_coin,
294 : 1,
295 : "1");
296 1 : TDB_FILL (coin_pub,
297 : 99);
298 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
299 : run_recoup (pg,
300 : &old_coin.coin_pub,
301 : refresh_id,
302 : &coin_pub,
303 : 1,
304 : 99,
305 : &st),
306 : TDB_coin_free (&old_coin));
307 1 : TDB_coin_free (&old_coin);
308 1 : FAILIF (! st.internal_failure);
309 1 : FAILIF (st.recoup_ok);
310 1 : FAILIF (0 != TDB_count (pg,
311 : "FROM recoup_refresh"));
312 1 : return 0;
313 : }
314 :
315 :
316 : /**
317 : * A fresh coin with nothing left on it and no earlier recoup is refused.
318 : *
319 : * @param pg the database context
320 : * @return 0 on success
321 : */
322 : static int
323 1 : check_empty_coin (struct TALER_EXCHANGEDB_PostgresContext *pg)
324 : {
325 : struct TALER_CoinPublicInfo old_coin;
326 : struct TALER_CoinPublicInfo fresh;
327 : struct RecoupStatus st;
328 : uint64_t known_coin_id;
329 : uint64_t refresh_id;
330 : char *hex;
331 :
332 1 : TDB_coin (pg,
333 : &denom,
334 : 21,
335 : &old_coin,
336 : NULL);
337 1 : refresh_id = melt (pg,
338 : &old_coin,
339 : 2,
340 : "1");
341 1 : TDB_coin (pg,
342 : &denom,
343 : 22,
344 : &fresh,
345 : &known_coin_id);
346 1 : hex = TDB_hex (&fresh.coin_pub,
347 : sizeof (fresh.coin_pub));
348 1 : FAILIF_C (GNUNET_OK !=
349 : TDB_exec (pg,
350 : "UPDATE known_coins"
351 : " SET remaining=ROW(0,0)::taler_amount"
352 : " WHERE coin_pub=decode('%s','hex');",
353 : hex),
354 : GNUNET_free (hex);
355 : TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
356 1 : GNUNET_free (hex);
357 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
358 : run_recoup (pg,
359 : &old_coin.coin_pub,
360 : refresh_id,
361 : &fresh.coin_pub,
362 : known_coin_id,
363 : 22,
364 : &st),
365 : TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
366 1 : TDB_coin_free (&fresh);
367 1 : TDB_coin_free (&old_coin);
368 1 : FAILIF (st.internal_failure);
369 1 : FAILIF (st.recoup_ok);
370 1 : FAILIF (0 != TDB_count (pg,
371 : "FROM recoup_refresh"));
372 1 : return 0;
373 : }
374 :
375 :
376 : /**
377 : * A funded fresh coin is drained back onto the old coin.
378 : *
379 : * @param pg the database context
380 : * @return 0 on success
381 : */
382 : static int
383 1 : check_recoup_refresh (struct TALER_EXCHANGEDB_PostgresContext *pg)
384 : {
385 : struct TALER_CoinPublicInfo old_coin;
386 : struct TALER_CoinPublicInfo fresh;
387 : struct RecoupStatus st;
388 : uint64_t known_coin_id;
389 : uint64_t refresh_id;
390 : char *hex;
391 :
392 1 : TDB_coin (pg,
393 : &denom,
394 : 23,
395 : &old_coin,
396 : NULL);
397 1 : refresh_id = melt (pg,
398 : &old_coin,
399 : 3,
400 : "1");
401 1 : TDB_coin (pg,
402 : &denom,
403 : 24,
404 : &fresh,
405 : &known_coin_id);
406 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
407 : run_recoup (pg,
408 : &old_coin.coin_pub,
409 : refresh_id,
410 : &fresh.coin_pub,
411 : known_coin_id,
412 : 24,
413 : &st),
414 : TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
415 1 : FAILIF_C (st.internal_failure,
416 : TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
417 1 : FAILIF_C (! st.recoup_ok,
418 : TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
419 1 : FAILIF_C (1 != TDB_count (pg,
420 : "FROM recoup_refresh"),
421 : TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
422 :
423 : /* the fresh coin is empty */
424 1 : hex = TDB_hex (&fresh.coin_pub,
425 : sizeof (fresh.coin_pub));
426 1 : FAILIF_C (1 != TDB_count (pg,
427 : "FROM known_coins"
428 : " WHERE coin_pub=decode('%s','hex')"
429 : " AND remaining=ROW(0,0)::taler_amount",
430 : hex),
431 : GNUNET_free (hex);
432 : TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
433 1 : GNUNET_free (hex);
434 : /* the old coin got the EUR:5 back on top of the EUR:4 it had left */
435 1 : hex = TDB_hex (&old_coin.coin_pub,
436 : sizeof (old_coin.coin_pub));
437 1 : FAILIF_C (1 != TDB_count (pg,
438 : "FROM known_coins"
439 : " WHERE coin_pub=decode('%s','hex')"
440 : " AND remaining=ROW(9,0)::taler_amount",
441 : hex),
442 : GNUNET_free (hex);
443 : TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
444 1 : GNUNET_free (hex);
445 :
446 : /* recouping the same coin again finds the earlier recoup */
447 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
448 : run_recoup (pg,
449 : &old_coin.coin_pub,
450 : refresh_id,
451 : &fresh.coin_pub,
452 : known_coin_id,
453 : 24,
454 : &st),
455 : TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
456 1 : FAILIF_C (! st.recoup_ok,
457 : TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
458 1 : FAILIF_C (1 != TDB_count (pg,
459 : "FROM recoup_refresh"),
460 : TDB_coin_free (&fresh); TDB_coin_free (&old_coin));
461 1 : TDB_coin_free (&fresh);
462 1 : TDB_coin_free (&old_coin);
463 1 : return 0;
464 : }
465 :
466 :
467 : /**
468 : * The iterator must not report a recoup-refresh that is not there.
469 : *
470 : * This is all it can be checked for today: its statement joins
471 : * `refresh_revealed_coins` and `refresh_commitments`, tables the schema no
472 : * longer has, so it cannot return a row at all (EDB-16). The assertion
473 : * below holds both now and once that is repaired.
474 : *
475 : * @param pg the database context
476 : * @return 0 on success
477 : */
478 : static int
479 1 : check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
480 : {
481 1 : unsigned int total = 0;
482 :
483 1 : FAILIF (0 <
484 : TALER_EXCHANGEDB_iterate_recoup_refreshes_above_serial_id (
485 : pg,
486 : 1000,
487 : &recoup_refresh_cb,
488 : &total));
489 1 : FAILIF (0 != total);
490 1 : return 0;
491 : }
492 :
493 :
494 : /**
495 : * The checks to run, in order.
496 : */
497 : static const struct TDB_Test tests[] = {
498 : { "recoup-refresh-unknown-coin",
499 : &check_unknown_coin },
500 : { "recoup-refresh-empty-coin",
501 : &check_empty_coin },
502 : { "recoup-refresh-recoup",
503 : &check_recoup_refresh },
504 : { "recoup-refresh-iterate",
505 : &check_iterate },
506 : { NULL, NULL }
507 : };
508 :
509 :
510 : int
511 1 : main (int argc,
512 : char *const *argv)
513 : {
514 : int ret;
515 :
516 1 : ret = TDB_main (argc,
517 : argv,
518 : "test-recoup-refresh",
519 : "Tests for the exchangedb `recoup_refresh' table",
520 : tests);
521 1 : TDB_account_free (&account);
522 1 : TDB_denom_free (&denom);
523 1 : return ret;
524 : }
525 :
526 :
527 : /* end of test_recoup_refresh.c */
|