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.c
18 : * @brief tests for the exchangedb functions whose primary table is `recoup`
19 : * @author Christian Grothoff
20 : *
21 : * Covers #TALER_EXCHANGEDB_do_recoup() and
22 : * #TALER_EXCHANGEDB_iterate_recoups_above_serial_id().
23 : *
24 : * `recoup` references `known_coins` and `withdraw`, so a full chain is
25 : * built for each check: a funded reserve, a withdraw from it and a coin.
26 : * do_recoup() moves the coin's whole remaining balance back to the reserve,
27 : * which is why the checks look at both sides afterwards.
28 : */
29 : #include "test_common.h"
30 : #include "exchange-database/do_recoup.h"
31 : #include "exchange-database/get_reserve.h"
32 : #include "exchange-database/iterate_recoups_above_serial_id.h"
33 :
34 :
35 : /**
36 : * Account the checks fund their reserves from.
37 : */
38 : static struct TDB_Account account;
39 :
40 :
41 : /**
42 : * Denomination the checks use.
43 : */
44 : static struct TDB_Denom denom;
45 :
46 :
47 : /**
48 : * Closure for #recoup_cb().
49 : */
50 : struct RecoupContext
51 : {
52 : /**
53 : * How many rows did the callback see?
54 : */
55 : unsigned int total;
56 :
57 : /**
58 : * Stop after this many rows; 0 for no limit.
59 : */
60 : unsigned int stop_after;
61 :
62 : /**
63 : * Coin we are looking for, NULL to match nothing.
64 : */
65 : const struct TALER_CoinSpendPublicKeyP *coin_pub;
66 :
67 : /**
68 : * How many times did we see it?
69 : */
70 : unsigned int matched;
71 :
72 : /**
73 : * Amount reported for it.
74 : */
75 : struct TALER_Amount amount;
76 :
77 : /**
78 : * Reserve reported for it.
79 : */
80 : struct TALER_ReservePublicKeyP reserve_pub;
81 :
82 : /**
83 : * Blinding secret reported for it.
84 : */
85 : union GNUNET_CRYPTO_BlindingSecretP coin_blind;
86 : };
87 :
88 :
89 : /**
90 : * Callback for #TALER_EXCHANGEDB_iterate_recoups_above_serial_id().
91 : *
92 : * @param cls a `struct RecoupContext *`
93 : * @param rowid row of the recoup
94 : * @param timestamp when the recoup was requested
95 : * @param amount how much went back to the reserve
96 : * @param reserve_pub the reserve that was credited
97 : * @param coin the coin that was recouped
98 : * @param denom_pub denomination of @a coin
99 : * @param coin_sig signature over the request
100 : * @param coin_blind blinding secret of the coin
101 : * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
102 : */
103 : static enum GNUNET_GenericReturnValue
104 3 : recoup_cb (void *cls,
105 : uint64_t rowid,
106 : struct GNUNET_TIME_Timestamp timestamp,
107 : const struct TALER_Amount *amount,
108 : const struct TALER_ReservePublicKeyP *reserve_pub,
109 : const struct TALER_CoinPublicInfo *coin,
110 : const struct TALER_DenominationPublicKey *denom_pub,
111 : const struct TALER_CoinSpendSignatureP *coin_sig,
112 : const union GNUNET_CRYPTO_BlindingSecretP *coin_blind)
113 : {
114 3 : struct RecoupContext *ctx = cls;
115 :
116 : (void) rowid;
117 : (void) timestamp;
118 : (void) denom_pub;
119 : (void) coin_sig;
120 3 : ctx->total++;
121 3 : if ( (NULL != ctx->coin_pub) &&
122 1 : (0 == GNUNET_memcmp (&coin->coin_pub,
123 : ctx->coin_pub)) )
124 : {
125 1 : ctx->matched++;
126 1 : ctx->amount = *amount;
127 1 : ctx->reserve_pub = *reserve_pub;
128 1 : ctx->coin_blind = *coin_blind;
129 : }
130 3 : if ( (0 != ctx->stop_after) &&
131 1 : (ctx->total >= ctx->stop_after) )
132 1 : return GNUNET_SYSERR;
133 2 : return GNUNET_OK;
134 : }
135 :
136 :
137 : /**
138 : * Outcome of a recoup request.
139 : */
140 : struct RecoupStatus
141 : {
142 : /**
143 : * Was the recoup accepted?
144 : */
145 : bool recoup_ok;
146 :
147 : /**
148 : * Did something go wrong inside the database?
149 : */
150 : bool internal_failure;
151 :
152 : /**
153 : * When the recoup happened.
154 : */
155 : struct GNUNET_TIME_Timestamp recoup_timestamp;
156 : };
157 :
158 :
159 : /**
160 : * Recoup a coin into a reserve.
161 : *
162 : * @param pg the database context
163 : * @param reserve_pub reserve to credit
164 : * @param withdraw_id withdraw that justifies the recoup
165 : * @param coin_pub coin to recoup
166 : * @param known_coin_id row of @a coin_pub
167 : * @param seed seed for the blinding secret and coin signature
168 : * @param[out] st set to the outcome
169 : * @return transaction status
170 : */
171 : static enum GNUNET_DB_QueryStatus
172 4 : run_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg,
173 : const struct TALER_ReservePublicKeyP *reserve_pub,
174 : uint64_t withdraw_id,
175 : const struct TALER_CoinSpendPublicKeyP *coin_pub,
176 : uint64_t known_coin_id,
177 : uint32_t seed,
178 : struct RecoupStatus *st)
179 : {
180 : union GNUNET_CRYPTO_BlindingSecretP coin_bks;
181 : struct TALER_CoinSpendSignatureP coin_sig;
182 :
183 4 : TDB_fill (&coin_bks,
184 : sizeof (coin_bks),
185 : seed);
186 4 : TDB_fill (&coin_sig,
187 : sizeof (coin_sig),
188 : seed);
189 4 : memset (st,
190 : 0,
191 : sizeof (*st));
192 4 : st->recoup_timestamp = GNUNET_TIME_timestamp_get ();
193 4 : return TALER_EXCHANGEDB_do_recoup (pg,
194 : reserve_pub,
195 : withdraw_id,
196 : &coin_bks,
197 : coin_pub,
198 : known_coin_id,
199 : &coin_sig,
200 : &st->recoup_timestamp,
201 : &st->recoup_ok,
202 : &st->internal_failure);
203 : }
204 :
205 :
206 : /**
207 : * Recouping a coin the exchange does not know is an internal failure.
208 : *
209 : * @param pg the database context
210 : * @return 0 on success
211 : */
212 : static int
213 1 : check_unknown_coin (struct TALER_EXCHANGEDB_PostgresContext *pg)
214 : {
215 : struct TALER_ReservePublicKeyP reserve_pub;
216 : struct TALER_CoinSpendPublicKeyP coin_pub;
217 1 : struct RecoupContext ctx = { 0 };
218 : struct RecoupStatus st;
219 : uint64_t withdraw_id;
220 :
221 1 : TDB_denom (pg,
222 : 10,
223 : "5",
224 : "0.1",
225 : &denom);
226 1 : TDB_account (pg,
227 : 10,
228 : &account);
229 1 : TDB_reserve_in (pg,
230 : &account,
231 : 10,
232 : "10",
233 : &reserve_pub);
234 1 : withdraw_id = TDB_withdraw (pg,
235 : &denom,
236 : &reserve_pub,
237 : 10,
238 : "5");
239 1 : TDB_FILL (coin_pub,
240 : 99);
241 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
242 : run_recoup (pg,
243 : &reserve_pub,
244 : withdraw_id,
245 : &coin_pub,
246 : 1,
247 : 99,
248 : &st));
249 1 : FAILIF (! st.internal_failure);
250 1 : FAILIF (st.recoup_ok);
251 1 : FAILIF (0 != TDB_count (pg,
252 : "FROM recoup"));
253 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
254 : TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
255 : 0,
256 : &recoup_cb,
257 : &ctx));
258 1 : FAILIF (0 != ctx.total);
259 1 : return 0;
260 : }
261 :
262 :
263 : /**
264 : * A coin that is already empty and was never recouped is refused.
265 : *
266 : * @param pg the database context
267 : * @return 0 on success
268 : */
269 : static int
270 1 : check_empty_coin (struct TALER_EXCHANGEDB_PostgresContext *pg)
271 : {
272 : struct TALER_ReservePublicKeyP reserve_pub;
273 : struct TALER_CoinPublicInfo coin;
274 : struct RecoupStatus st;
275 : uint64_t known_coin_id;
276 : uint64_t withdraw_id;
277 : char *hex;
278 :
279 1 : TDB_reserve_in (pg,
280 : &account,
281 : 11,
282 : "10",
283 : &reserve_pub);
284 1 : withdraw_id = TDB_withdraw (pg,
285 : &denom,
286 : &reserve_pub,
287 : 11,
288 : "5");
289 1 : TDB_coin (pg,
290 : &denom,
291 : 20,
292 : &coin,
293 : &known_coin_id);
294 1 : hex = TDB_hex (&coin.coin_pub,
295 : sizeof (coin.coin_pub));
296 1 : FAILIF_C (GNUNET_OK !=
297 : TDB_exec (pg,
298 : "UPDATE known_coins"
299 : " SET remaining=ROW(0,0)::taler_amount"
300 : " WHERE coin_pub=decode('%s','hex');",
301 : hex),
302 : GNUNET_free (hex); TDB_coin_free (&coin));
303 1 : GNUNET_free (hex);
304 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
305 : run_recoup (pg,
306 : &reserve_pub,
307 : withdraw_id,
308 : &coin.coin_pub,
309 : known_coin_id,
310 : 20,
311 : &st),
312 : TDB_coin_free (&coin));
313 1 : TDB_coin_free (&coin);
314 1 : FAILIF (st.internal_failure);
315 : /* nothing to give back, and no earlier recoup to report */
316 1 : FAILIF (st.recoup_ok);
317 1 : FAILIF (0 != TDB_count (pg,
318 : "FROM recoup"));
319 1 : return 0;
320 : }
321 :
322 :
323 : /**
324 : * A funded coin is drained and the reserve is credited.
325 : *
326 : * @param pg the database context
327 : * @return 0 on success
328 : */
329 : static int
330 1 : check_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg)
331 : {
332 : struct TALER_ReservePublicKeyP reserve_pub;
333 : struct TALER_CoinPublicInfo coin;
334 : struct TALER_EXCHANGEDB_Reserve reserve;
335 : struct RecoupStatus st;
336 : struct RecoupContext ctx;
337 1 : struct TALER_Amount expect_coin = TDB_amount ("5");
338 1 : struct TALER_Amount expect_reserve = TDB_amount ("10");
339 : union GNUNET_CRYPTO_BlindingSecretP expect_bks;
340 : uint64_t known_coin_id;
341 : uint64_t withdraw_id;
342 : char *hex;
343 :
344 1 : TDB_reserve_in (pg,
345 : &account,
346 : 12,
347 : "10",
348 : &reserve_pub);
349 1 : withdraw_id = TDB_withdraw (pg,
350 : &denom,
351 : &reserve_pub,
352 : 12,
353 : "5");
354 1 : TDB_coin (pg,
355 : &denom,
356 : 21,
357 : &coin,
358 : &known_coin_id);
359 : /* EUR:10 came in and EUR:5 was withdrawn, so EUR:5 is left */
360 1 : memset (&reserve,
361 : 0,
362 : sizeof (reserve));
363 1 : reserve.pub = reserve_pub;
364 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
365 : TALER_EXCHANGEDB_get_reserve (pg,
366 : &reserve),
367 : TDB_coin_free (&coin));
368 :
369 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
370 : run_recoup (pg,
371 : &reserve_pub,
372 : withdraw_id,
373 : &coin.coin_pub,
374 : known_coin_id,
375 : 21,
376 : &st),
377 : TDB_coin_free (&coin));
378 1 : FAILIF_C (st.internal_failure,
379 : TDB_coin_free (&coin));
380 1 : FAILIF_C (! st.recoup_ok,
381 : TDB_coin_free (&coin));
382 1 : FAILIF_C (1 != TDB_count (pg,
383 : "FROM recoup"),
384 : TDB_coin_free (&coin));
385 :
386 : /* the coin is empty... */
387 1 : hex = TDB_hex (&coin.coin_pub,
388 : sizeof (coin.coin_pub));
389 1 : FAILIF_C (1 != TDB_count (pg,
390 : "FROM known_coins"
391 : " WHERE coin_pub=decode('%s','hex')"
392 : " AND remaining=ROW(0,0)::taler_amount",
393 : hex),
394 : GNUNET_free (hex); TDB_coin_free (&coin));
395 1 : GNUNET_free (hex);
396 : /* ...and the reserve got the money */
397 1 : memset (&reserve,
398 : 0,
399 : sizeof (reserve));
400 1 : reserve.pub = reserve_pub;
401 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
402 : TALER_EXCHANGEDB_get_reserve (pg,
403 : &reserve),
404 : TDB_coin_free (&coin));
405 1 : FAILIF_C (0 != TALER_amount_cmp (&reserve.balance,
406 : &expect_reserve),
407 : TDB_coin_free (&coin));
408 :
409 : /* the recoup is reported with everything it was made with */
410 1 : TDB_FILL (expect_bks,
411 : 21);
412 1 : memset (&ctx,
413 : 0,
414 : sizeof (ctx));
415 1 : ctx.coin_pub = &coin.coin_pub;
416 1 : FAILIF_C (0 >=
417 : TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
418 : 0,
419 : &recoup_cb,
420 : &ctx),
421 : TDB_coin_free (&coin));
422 1 : FAILIF_C (1 != ctx.matched,
423 : TDB_coin_free (&coin));
424 1 : FAILIF_C (0 != TALER_amount_cmp (&ctx.amount,
425 : &expect_coin),
426 : TDB_coin_free (&coin));
427 1 : FAILIF_C (0 != GNUNET_memcmp (&ctx.reserve_pub,
428 : &reserve_pub),
429 : TDB_coin_free (&coin));
430 1 : FAILIF_C (0 != GNUNET_memcmp (&ctx.coin_blind,
431 : &expect_bks),
432 : TDB_coin_free (&coin));
433 :
434 : /* recouping the same coin again finds the earlier recoup */
435 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
436 : run_recoup (pg,
437 : &reserve_pub,
438 : withdraw_id,
439 : &coin.coin_pub,
440 : known_coin_id,
441 : 21,
442 : &st),
443 : TDB_coin_free (&coin));
444 1 : FAILIF_C (! st.recoup_ok,
445 : TDB_coin_free (&coin));
446 1 : FAILIF_C (st.internal_failure,
447 : TDB_coin_free (&coin));
448 1 : FAILIF_C (1 != TDB_count (pg,
449 : "FROM recoup"),
450 : TDB_coin_free (&coin));
451 1 : TDB_coin_free (&coin);
452 1 : return 0;
453 : }
454 :
455 :
456 : /**
457 : * The iterator's serial bound and abort return behave as documented.
458 : *
459 : * @param pg the database context
460 : * @return 0 on success
461 : */
462 : static int
463 1 : check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
464 : {
465 : struct RecoupContext ctx;
466 :
467 1 : memset (&ctx,
468 : 0,
469 : sizeof (ctx));
470 1 : FAILIF (1 !=
471 : TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
472 : 0,
473 : &recoup_cb,
474 : &ctx));
475 1 : memset (&ctx,
476 : 0,
477 : sizeof (ctx));
478 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
479 : TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
480 : 1000,
481 : &recoup_cb,
482 : &ctx));
483 1 : FAILIF (0 != ctx.total);
484 1 : memset (&ctx,
485 : 0,
486 : sizeof (ctx));
487 1 : ctx.stop_after = 1;
488 1 : FAILIF (1 !=
489 : TALER_EXCHANGEDB_iterate_recoups_above_serial_id (pg,
490 : 0,
491 : &recoup_cb,
492 : &ctx));
493 1 : FAILIF (1 != ctx.total);
494 1 : return 0;
495 : }
496 :
497 :
498 : /**
499 : * The checks to run, in order.
500 : */
501 : static const struct TDB_Test tests[] = {
502 : { "recoup-unknown-coin",
503 : &check_unknown_coin },
504 : { "recoup-empty-coin",
505 : &check_empty_coin },
506 : { "recoup-recoup",
507 : &check_recoup },
508 : { "recoup-iterate",
509 : &check_iterate },
510 : { NULL, NULL }
511 : };
512 :
513 :
514 : int
515 1 : main (int argc,
516 : char *const *argv)
517 : {
518 : int ret;
519 :
520 1 : ret = TDB_main (argc,
521 : argv,
522 : "test-recoup",
523 : "Tests for the exchangedb `recoup' table",
524 : tests);
525 1 : TDB_account_free (&account);
526 1 : TDB_denom_free (&denom);
527 1 : return ret;
528 : }
529 :
530 :
531 : /* end of test_recoup.c */
|