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_reserve_history.c
18 : * @brief tests for the exchangedb functions whose primary table is
19 : * `reserve_history`
20 : * @author Christian Grothoff
21 : *
22 : * Covers #TALER_EXCHANGEDB_get_reserve_history().
23 : *
24 : * `reserve_history` is an index: AFTER INSERT triggers on the tables that
25 : * change a reserve add one row each, naming the table and the row. The
26 : * function walks that index newest-first and looks each entry up in the
27 : * table it names. So the checks build a reserve with several kinds of
28 : * operation on it and read the history back, and they check the ETag
29 : * short-circuit -- which must not leave a transaction open.
30 : */
31 : #include "test_common.h"
32 : #include "exchange-database/do_recoup.h"
33 : #include "exchange-database/get_reserve_history.h"
34 : #include "exchange-database/insert_close_request.h"
35 : #include "exchange-database/start.h"
36 : #include "exchange-database/rollback.h"
37 :
38 :
39 : /**
40 : * Account the checks use.
41 : */
42 : static struct TDB_Account account;
43 :
44 :
45 : /**
46 : * Count the entries of @a rh, and how many of them have type @a type.
47 : *
48 : * @param rh history to walk
49 : * @param type operation to count
50 : * @param[out] matched set to the number of entries of that type
51 : * @return total number of entries
52 : */
53 : static unsigned int
54 4 : count_history (const struct TALER_EXCHANGEDB_ReserveHistory *rh,
55 : enum TALER_EXCHANGEDB_ReserveOperation type,
56 : unsigned int *matched)
57 : {
58 4 : unsigned int total = 0;
59 :
60 4 : *matched = 0;
61 4 : for (const struct TALER_EXCHANGEDB_ReserveHistory *p = rh;
62 11 : NULL != p;
63 7 : p = p->next)
64 : {
65 7 : total++;
66 7 : if (type == p->type)
67 4 : (*matched)++;
68 : }
69 4 : return total;
70 : }
71 :
72 :
73 : /**
74 : * An unknown reserve has no history at all.
75 : *
76 : * @param pg the database context
77 : * @return 0 on success
78 : */
79 : static int
80 1 : check_unknown_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg)
81 : {
82 : struct TALER_ReservePublicKeyP reserve_pub;
83 1 : struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
84 : struct TALER_Amount balance;
85 1 : uint64_t etag_out = 42;
86 :
87 1 : TDB_FILL (reserve_pub,
88 : 1);
89 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
90 : TALER_EXCHANGEDB_get_reserve_history (pg,
91 : &reserve_pub,
92 : 0,
93 : 0,
94 : &etag_out,
95 : &balance,
96 : &rh));
97 1 : FAILIF (NULL != rh);
98 1 : return 0;
99 : }
100 :
101 :
102 : /**
103 : * A reserve that exists but was never touched has no history either: the
104 : * `reserves` row alone does not create an index entry.
105 : *
106 : * @param pg the database context
107 : * @return 0 on success
108 : */
109 : static int
110 1 : check_untouched_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg)
111 : {
112 : struct TALER_ReservePublicKeyP reserve_pub;
113 1 : struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
114 : struct TALER_Amount balance;
115 1 : uint64_t etag_out = 42;
116 :
117 1 : TDB_reserve (pg,
118 : 2,
119 : "5",
120 : &reserve_pub);
121 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
122 : TALER_EXCHANGEDB_get_reserve_history (pg,
123 : &reserve_pub,
124 : 0,
125 : 0,
126 : &etag_out,
127 : &balance,
128 : &rh));
129 1 : FAILIF (NULL != rh);
130 1 : return 0;
131 : }
132 :
133 :
134 : /**
135 : * A bank transfer into the reserve shows up as one history entry.
136 : *
137 : * @param pg the database context
138 : * @return 0 on success
139 : */
140 : static int
141 1 : check_bank_transfer (struct TALER_EXCHANGEDB_PostgresContext *pg)
142 : {
143 : struct TALER_ReservePublicKeyP reserve_pub;
144 1 : struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
145 : struct TALER_Amount balance;
146 1 : struct TALER_Amount expect = TDB_amount ("10");
147 : unsigned int matched;
148 1 : uint64_t etag_out = 0;
149 :
150 1 : TDB_account (pg,
151 : 10,
152 : &account);
153 1 : TDB_reserve_in (pg,
154 : &account,
155 : 10,
156 : "10",
157 : &reserve_pub);
158 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
159 : TALER_EXCHANGEDB_get_reserve_history (pg,
160 : &reserve_pub,
161 : 0,
162 : 0,
163 : &etag_out,
164 : &balance,
165 : &rh));
166 1 : FAILIF_C (NULL == rh,
167 : TALER_EXCHANGEDB_free_reserve_history (rh));
168 1 : FAILIF_C (0 == etag_out,
169 : TALER_EXCHANGEDB_free_reserve_history (rh));
170 1 : FAILIF_C (0 != TALER_amount_cmp (&balance,
171 : &expect),
172 : TALER_EXCHANGEDB_free_reserve_history (rh));
173 1 : FAILIF_C (1 != count_history (rh,
174 : TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE,
175 : &matched),
176 : TALER_EXCHANGEDB_free_reserve_history (rh));
177 1 : FAILIF_C (1 != matched,
178 : TALER_EXCHANGEDB_free_reserve_history (rh));
179 1 : FAILIF_C (0 != TALER_amount_cmp (&rh->details.bank->amount,
180 : &expect),
181 : TALER_EXCHANGEDB_free_reserve_history (rh));
182 1 : FAILIF_C (0 != strcmp (rh->details.bank->sender_account_details.full_payto,
183 : account.payto.full_payto),
184 : TALER_EXCHANGEDB_free_reserve_history (rh));
185 1 : TALER_EXCHANGEDB_free_reserve_history (rh);
186 1 : return 0;
187 : }
188 :
189 :
190 : /**
191 : * A close request adds a second entry, and the offset filter hides the
192 : * older one.
193 : *
194 : * @param pg the database context
195 : * @return 0 on success
196 : */
197 : static int
198 1 : check_two_entries (struct TALER_EXCHANGEDB_PostgresContext *pg)
199 : {
200 : struct TALER_ReservePublicKeyP reserve_pub;
201 : struct TALER_ReserveSignatureP reserve_sig;
202 1 : struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
203 : struct TALER_Amount balance;
204 1 : struct TALER_Amount close_balance = TDB_amount ("10");
205 1 : struct TALER_Amount close_fee = TDB_amount ("0.5");
206 : unsigned int matched;
207 : uint64_t etag_first;
208 1 : uint64_t etag_out = 0;
209 : uint64_t first_off;
210 :
211 1 : TDB_fill (&reserve_pub,
212 : sizeof (reserve_pub),
213 : 10);
214 1 : TDB_FILL (reserve_sig,
215 : 10);
216 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
217 : TALER_EXCHANGEDB_get_reserve_history (pg,
218 : &reserve_pub,
219 : 0,
220 : 0,
221 : &etag_first,
222 : &balance,
223 : &rh));
224 1 : first_off = rh->history_offset;
225 1 : TALER_EXCHANGEDB_free_reserve_history (rh);
226 1 : rh = NULL;
227 :
228 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
229 : TALER_EXCHANGEDB_insert_close_request (pg,
230 : &reserve_pub,
231 : account.payto,
232 : &reserve_sig,
233 : GNUNET_TIME_timestamp_get (),
234 : &close_balance,
235 : &close_fee));
236 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
237 : TALER_EXCHANGEDB_get_reserve_history (pg,
238 : &reserve_pub,
239 : 0,
240 : 0,
241 : &etag_out,
242 : &balance,
243 : &rh));
244 1 : FAILIF_C (2 != count_history (rh,
245 : TALER_EXCHANGEDB_RO_CLOSE_REQUEST,
246 : &matched),
247 : TALER_EXCHANGEDB_free_reserve_history (rh));
248 1 : FAILIF_C (1 != matched,
249 : TALER_EXCHANGEDB_free_reserve_history (rh));
250 : /* The query runs newest-first but the rows are walked back to front, so
251 : the list that comes out is oldest-first. */
252 1 : FAILIF_C (TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE != rh->type,
253 : TALER_EXCHANGEDB_free_reserve_history (rh));
254 1 : FAILIF_C (TALER_EXCHANGEDB_RO_CLOSE_REQUEST != rh->next->type,
255 : TALER_EXCHANGEDB_free_reserve_history (rh));
256 1 : FAILIF_C (rh->history_offset >= rh->next->history_offset,
257 : TALER_EXCHANGEDB_free_reserve_history (rh));
258 1 : FAILIF_C (etag_out <= etag_first,
259 : TALER_EXCHANGEDB_free_reserve_history (rh));
260 1 : TALER_EXCHANGEDB_free_reserve_history (rh);
261 1 : rh = NULL;
262 :
263 : /* starting past the bank transfer leaves only the close request */
264 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
265 : TALER_EXCHANGEDB_get_reserve_history (pg,
266 : &reserve_pub,
267 : first_off,
268 : 0,
269 : &etag_out,
270 : &balance,
271 : &rh));
272 1 : FAILIF_C (1 != count_history (rh,
273 : TALER_EXCHANGEDB_RO_CLOSE_REQUEST,
274 : &matched),
275 : TALER_EXCHANGEDB_free_reserve_history (rh));
276 1 : FAILIF_C (1 != matched,
277 : TALER_EXCHANGEDB_free_reserve_history (rh));
278 1 : TALER_EXCHANGEDB_free_reserve_history (rh);
279 1 : rh = NULL;
280 :
281 : /* starting past everything leaves an empty history, but the reserve is
282 : still known and the ETag is still reported */
283 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
284 : TALER_EXCHANGEDB_get_reserve_history (pg,
285 : &reserve_pub,
286 : etag_out,
287 : 0,
288 : &etag_out,
289 : &balance,
290 : &rh));
291 1 : FAILIF_C (NULL != rh,
292 : TALER_EXCHANGEDB_free_reserve_history (rh));
293 1 : return 0;
294 : }
295 :
296 :
297 : /**
298 : * A recoup shows up as a RECOUP entry that names the coin, the amount
299 : * and the withdraw operation the coin came from.
300 : *
301 : * @param pg the database context
302 : * @return 0 on success
303 : */
304 : static int
305 1 : check_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg)
306 : {
307 : struct TDB_Denom denom;
308 : struct TALER_CoinPublicInfo coin;
309 : struct TALER_ReservePublicKeyP reserve_pub;
310 : struct TALER_HashBlindedPlanchetsP planchets_h;
311 : struct TALER_CoinSpendSignatureP coin_sig;
312 : union GNUNET_CRYPTO_BlindingSecretP coin_bks;
313 : struct GNUNET_TIME_Timestamp recoup_timestamp;
314 : struct TALER_Amount recoup_amount;
315 1 : struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
316 : struct TALER_Amount balance;
317 1 : struct TALER_Amount expect_value = TDB_amount ("5");
318 1 : struct TALER_Amount expect_balance = TDB_amount ("10");
319 1 : const struct TALER_EXCHANGEDB_Recoup *recoup = NULL;
320 1 : unsigned int matched = 0;
321 : uint64_t withdraw_id;
322 : uint64_t known_coin_id;
323 1 : uint64_t etag = 0;
324 : bool recoup_ok;
325 : bool internal_failure;
326 :
327 1 : TDB_denom (pg,
328 : 30,
329 : "5",
330 : "0.1",
331 : &denom);
332 1 : TDB_reserve_in (pg,
333 : &account,
334 : 30,
335 : "10",
336 : &reserve_pub);
337 1 : withdraw_id = TDB_withdraw (pg,
338 : &denom,
339 : &reserve_pub,
340 : 30,
341 : "5");
342 : /* TDB_withdraw() seeds the commitment with its seed */
343 1 : TDB_FILL (planchets_h,
344 : 30);
345 1 : TDB_coin (pg,
346 : &denom,
347 : 31,
348 : &coin,
349 : &known_coin_id);
350 1 : TDB_FILL (coin_sig,
351 : 31);
352 1 : TDB_FILL (coin_bks,
353 : 31);
354 1 : recoup_timestamp = GNUNET_TIME_timestamp_get ();
355 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
356 : TALER_EXCHANGEDB_do_recoup (pg,
357 : &reserve_pub,
358 : withdraw_id,
359 : 2,
360 : &coin_bks,
361 : &coin.coin_pub,
362 : known_coin_id,
363 : &coin_sig,
364 : &recoup_timestamp,
365 : &recoup_amount,
366 : &recoup_ok,
367 : &internal_failure),
368 : TDB_coin_free (&coin); TDB_denom_free (&denom));
369 1 : FAILIF_C (internal_failure || ! recoup_ok,
370 : TDB_coin_free (&coin); TDB_denom_free (&denom));
371 1 : FAILIF_C (0 != TALER_amount_cmp (&recoup_amount,
372 : &expect_value),
373 : TDB_coin_free (&coin); TDB_denom_free (&denom));
374 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
375 : TALER_EXCHANGEDB_get_reserve_history (pg,
376 : &reserve_pub,
377 : 0,
378 : 0,
379 : &etag,
380 : &balance,
381 : &rh),
382 : TDB_coin_free (&coin); TDB_denom_free (&denom));
383 : /* bank transfer in, withdraw, recoup */
384 1 : FAILIF_C (3 != count_history (rh,
385 : TALER_EXCHANGEDB_RO_RECOUP_COIN,
386 : &matched),
387 : TALER_EXCHANGEDB_free_reserve_history (rh);
388 : TDB_coin_free (&coin); TDB_denom_free (&denom));
389 1 : FAILIF_C (1 != matched,
390 : TALER_EXCHANGEDB_free_reserve_history (rh);
391 : TDB_coin_free (&coin); TDB_denom_free (&denom));
392 1 : for (const struct TALER_EXCHANGEDB_ReserveHistory *p = rh;
393 4 : NULL != p;
394 3 : p = p->next)
395 3 : if (TALER_EXCHANGEDB_RO_RECOUP_COIN == p->type)
396 1 : recoup = p->details.recoup;
397 1 : FAILIF_C (0 != GNUNET_memcmp (&recoup->coin.coin_pub,
398 : &coin.coin_pub),
399 : TALER_EXCHANGEDB_free_reserve_history (rh);
400 : TDB_coin_free (&coin); TDB_denom_free (&denom));
401 1 : FAILIF_C (0 != GNUNET_memcmp (&recoup->planchets_h,
402 : &planchets_h),
403 : TALER_EXCHANGEDB_free_reserve_history (rh);
404 : TDB_coin_free (&coin); TDB_denom_free (&denom));
405 1 : FAILIF_C (0 != GNUNET_memcmp (&recoup->reserve_pub,
406 : &reserve_pub),
407 : TALER_EXCHANGEDB_free_reserve_history (rh);
408 : TDB_coin_free (&coin); TDB_denom_free (&denom));
409 1 : FAILIF_C (0 != TALER_amount_cmp (&recoup->value,
410 : &expect_value),
411 : TALER_EXCHANGEDB_free_reserve_history (rh);
412 : TDB_coin_free (&coin); TDB_denom_free (&denom));
413 : /* 10 in, 5 withdrawn, 5 recouped */
414 1 : FAILIF_C (0 != TALER_amount_cmp (&balance,
415 : &expect_balance),
416 : TALER_EXCHANGEDB_free_reserve_history (rh);
417 : TDB_coin_free (&coin); TDB_denom_free (&denom));
418 1 : TALER_EXCHANGEDB_free_reserve_history (rh);
419 1 : TDB_coin_free (&coin);
420 1 : TDB_denom_free (&denom);
421 1 : return 0;
422 : }
423 :
424 :
425 : /**
426 : * Passing the current ETag short-circuits the lookup -- and must not leave
427 : * the transaction it opened behind.
428 : *
429 : * @param pg the database context
430 : * @return 0 on success
431 : */
432 : static int
433 1 : check_etag (struct TALER_EXCHANGEDB_PostgresContext *pg)
434 : {
435 : struct TALER_ReservePublicKeyP reserve_pub;
436 1 : struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL;
437 : struct TALER_Amount balance;
438 1 : uint64_t etag_out = 0;
439 1 : uint64_t etag_again = 0;
440 :
441 1 : TDB_fill (&reserve_pub,
442 : sizeof (reserve_pub),
443 : 10);
444 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
445 : TALER_EXCHANGEDB_get_reserve_history (pg,
446 : &reserve_pub,
447 : 0,
448 : 0,
449 : &etag_out,
450 : &balance,
451 : &rh));
452 1 : TALER_EXCHANGEDB_free_reserve_history (rh);
453 1 : rh = NULL;
454 :
455 : /* the client already has this version */
456 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
457 : TALER_EXCHANGEDB_get_reserve_history (pg,
458 : &reserve_pub,
459 : 0,
460 : etag_out,
461 : &etag_again,
462 : &balance,
463 : &rh));
464 1 : FAILIF (etag_again != etag_out);
465 1 : FAILIF (NULL != rh);
466 :
467 : /* The short-circuit above returns early. If it forgets to close the
468 : read-committed transaction it opened, the connection is parked in
469 : "idle in transaction" -- which the next TALER_EXCHANGEDB_start() would
470 : paper over, since its preflight rolls the stale transaction back and
471 : only logs it. So check the bookkeeping preflight itself looks at. */
472 1 : FAILIF (NULL != pg->transaction_name);
473 1 : FAILIF (GNUNET_OK !=
474 : TALER_EXCHANGEDB_start (pg,
475 : "test-reserve-history-etag"));
476 1 : FAILIF (NULL == pg->transaction_name);
477 1 : TALER_EXCHANGEDB_rollback (pg);
478 1 : FAILIF (NULL != pg->transaction_name);
479 1 : return 0;
480 : }
481 :
482 :
483 : /**
484 : * The checks to run, in order.
485 : */
486 : static const struct TDB_Test tests[] = {
487 : { "reserve-history-unknown-reserve",
488 : &check_unknown_reserve },
489 : { "reserve-history-untouched-reserve",
490 : &check_untouched_reserve },
491 : { "reserve-history-bank-transfer",
492 : &check_bank_transfer },
493 : { "reserve-history-two-entries",
494 : &check_two_entries },
495 : { "reserve-history-recoup",
496 : &check_recoup },
497 : { "reserve-history-etag",
498 : &check_etag },
499 : { NULL, NULL }
500 : };
501 :
502 :
503 : int
504 1 : main (int argc,
505 : char *const *argv)
506 : {
507 : int ret;
508 :
509 1 : ret = TDB_main (argc,
510 : argv,
511 : "test-reserve-history",
512 : "Tests for the exchangedb `reserve_history' table",
513 : tests);
514 1 : TDB_account_free (&account);
515 1 : return ret;
516 : }
517 :
518 :
519 : /* end of test_reserve_history.c */
|