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_reserves_open_requests.c
18 : * @brief tests for the exchangedb functions whose primary table is
19 : * `reserves_open_requests`
20 : * @author Christian Grothoff
21 : *
22 : * Covers #TALER_EXCHANGEDB_do_reserve_open() and
23 : * #TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id().
24 : *
25 : * The table references `reserves`, which TDB_reserve() creates. The
26 : * interesting cases of do_reserve_open() are the ones where it declines to
27 : * do the work: an unknown reserve, a payment that does not cover the cost,
28 : * a reserve that cannot pay its share, and a request that asks for nothing
29 : * that is not already granted. Only the success path writes the table.
30 : */
31 : #include "test_common.h"
32 : #include "exchange-database/do_reserve_open.h"
33 : #include "exchange-database/get_reserve.h"
34 : #include "exchange-database/iterate_reserve_open_above_serial_id.h"
35 :
36 :
37 : /**
38 : * A year in microseconds, as exchange_do_reserve_open() counts it.
39 : */
40 : #define YEAR_US 31536000000000LLU
41 :
42 :
43 : /**
44 : * Closure for #open_cb().
45 : */
46 : struct OpenContext
47 : {
48 : /**
49 : * How many rows did the callback see?
50 : */
51 : unsigned int total;
52 :
53 : /**
54 : * Stop after this many rows; 0 for no limit.
55 : */
56 : unsigned int stop_after;
57 :
58 : /**
59 : * Reserve we are looking for, NULL to match nothing.
60 : */
61 : const struct TALER_ReservePublicKeyP *reserve_pub;
62 :
63 : /**
64 : * How many times did we see @e reserve_pub?
65 : */
66 : unsigned int matched;
67 :
68 : /**
69 : * Reserve payment reported for @e reserve_pub.
70 : */
71 : struct TALER_Amount reserve_payment;
72 :
73 : /**
74 : * Purse limit reported for @e reserve_pub.
75 : */
76 : uint32_t purse_limit;
77 :
78 : /**
79 : * Signature reported for @e reserve_pub.
80 : */
81 : struct TALER_ReserveSignatureP reserve_sig;
82 :
83 : /**
84 : * Expiration reported for @e reserve_pub.
85 : */
86 : struct GNUNET_TIME_Timestamp expiration;
87 :
88 : /**
89 : * Request timestamp reported for @e reserve_pub.
90 : */
91 : struct GNUNET_TIME_Timestamp request_timestamp;
92 : };
93 :
94 :
95 : /**
96 : * Callback for #TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id().
97 : *
98 : * @param cls a `struct OpenContext *`
99 : * @param rowid row of the request
100 : * @param reserve_payment how much the reserve itself paid
101 : * @param request_timestamp when the request was made
102 : * @param reserve_expiration until when the reserve is now open
103 : * @param purse_limit how many purses were asked for
104 : * @param reserve_pub the reserve in question
105 : * @param reserve_sig signature affirming the request
106 : * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
107 : */
108 : static enum GNUNET_GenericReturnValue
109 4 : open_cb (void *cls,
110 : uint64_t rowid,
111 : const struct TALER_Amount *reserve_payment,
112 : struct GNUNET_TIME_Timestamp request_timestamp,
113 : struct GNUNET_TIME_Timestamp reserve_expiration,
114 : uint32_t purse_limit,
115 : const struct TALER_ReservePublicKeyP *reserve_pub,
116 : const struct TALER_ReserveSignatureP *reserve_sig)
117 : {
118 4 : struct OpenContext *ctx = cls;
119 :
120 : (void) rowid;
121 4 : ctx->total++;
122 4 : if ( (NULL != ctx->reserve_pub) &&
123 1 : (0 == GNUNET_memcmp (reserve_pub,
124 : ctx->reserve_pub)) )
125 : {
126 1 : ctx->matched++;
127 1 : ctx->reserve_payment = *reserve_payment;
128 1 : ctx->purse_limit = purse_limit;
129 1 : ctx->reserve_sig = *reserve_sig;
130 1 : ctx->expiration = reserve_expiration;
131 1 : ctx->request_timestamp = request_timestamp;
132 : }
133 4 : if ( (0 != ctx->stop_after) &&
134 1 : (ctx->total >= ctx->stop_after) )
135 1 : return GNUNET_SYSERR;
136 3 : return GNUNET_OK;
137 : }
138 :
139 :
140 : /**
141 : * Ask to keep a reserve open.
142 : *
143 : * @param pg the database context
144 : * @param reserve_pub reserve to open
145 : * @param seed seed for the reserve signature
146 : * @param total_paid total the client is paying, e.g. "1"
147 : * @param reserve_payment share of that taken from the reserve, e.g. "1"
148 : * @param purse_limit how many purses the client wants
149 : * @param desired_expiration until when the client wants the reserve open
150 : * @param now when the client made the request
151 : * @param open_fee annual fee, e.g. "1"
152 : * @param[out] no_funds set if the reserve cannot pay its share
153 : * @param[out] open_cost set to what the operation costs
154 : * @param[out] final_expiration set to the expiration that was granted
155 : * @return transaction status
156 : */
157 : static enum GNUNET_DB_QueryStatus
158 7 : open_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg,
159 : const struct TALER_ReservePublicKeyP *reserve_pub,
160 : uint32_t seed,
161 : const char *total_paid,
162 : const char *reserve_payment,
163 : uint32_t purse_limit,
164 : struct GNUNET_TIME_Timestamp desired_expiration,
165 : struct GNUNET_TIME_Timestamp now,
166 : const char *open_fee,
167 : bool *no_funds,
168 : struct TALER_Amount *open_cost,
169 : struct GNUNET_TIME_Timestamp *final_expiration)
170 : {
171 : struct TALER_ReserveSignatureP reserve_sig;
172 7 : struct TALER_Amount paid = TDB_amount (total_paid);
173 7 : struct TALER_Amount payment = TDB_amount (reserve_payment);
174 7 : struct TALER_Amount fee = TDB_amount (open_fee);
175 : struct TALER_Amount balance;
176 :
177 7 : TDB_fill (&reserve_sig,
178 : sizeof (reserve_sig),
179 : seed);
180 7 : return TALER_EXCHANGEDB_do_reserve_open (pg,
181 : reserve_pub,
182 : &paid,
183 : &payment,
184 : purse_limit,
185 : &reserve_sig,
186 : desired_expiration,
187 : now,
188 : &fee,
189 : no_funds,
190 : &balance,
191 : open_cost,
192 : final_expiration);
193 : }
194 :
195 :
196 : /**
197 : * Build a timestamp from a number of seconds since the epoch.
198 : *
199 : * @param secs seconds since the epoch
200 : * @return the timestamp
201 : */
202 : static struct GNUNET_TIME_Timestamp
203 12 : ts (uint64_t secs)
204 : {
205 12 : struct GNUNET_TIME_Absolute abs = {
206 12 : .abs_value_us = secs * 1000LLU * 1000LLU
207 : };
208 :
209 12 : return GNUNET_TIME_absolute_to_timestamp (abs);
210 : }
211 :
212 :
213 : /**
214 : * A reserve that does not exist cannot be opened, and nothing is on file.
215 : *
216 : * @param pg the database context
217 : * @return 0 on success
218 : */
219 : static int
220 1 : check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
221 : {
222 : struct TALER_ReservePublicKeyP reserve_pub;
223 : struct TALER_Amount open_cost;
224 : struct GNUNET_TIME_Timestamp final_expiration;
225 1 : struct OpenContext ctx = { 0 };
226 1 : bool no_funds = false;
227 :
228 1 : TDB_FILL (reserve_pub,
229 : 1);
230 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
231 : open_reserve (pg,
232 : &reserve_pub,
233 : 1,
234 : "1",
235 : "1",
236 : 1,
237 : ts (2000000000),
238 : ts (1600000000),
239 : "1",
240 : &no_funds,
241 : &open_cost,
242 : &final_expiration));
243 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
244 : TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id (
245 : pg,
246 : 0,
247 : &open_cb,
248 : &ctx));
249 1 : FAILIF (0 != ctx.total);
250 1 : FAILIF (0 != TDB_count (pg,
251 : "FROM reserves_open_requests"));
252 1 : return 0;
253 : }
254 :
255 :
256 : /**
257 : * Opening a reserve charges it, extends it and records the request.
258 : *
259 : * @param pg the database context
260 : * @return 0 on success
261 : */
262 : static int
263 1 : check_open (struct TALER_EXCHANGEDB_PostgresContext *pg)
264 : {
265 : struct TALER_ReservePublicKeyP reserve_pub;
266 : struct TALER_EXCHANGEDB_Reserve reserve;
267 : struct TALER_Amount open_cost;
268 1 : struct TALER_Amount expect_cost = TDB_amount ("1");
269 1 : struct TALER_Amount expect_balance = TDB_amount ("9");
270 : struct TALER_ReserveSignatureP expect_sig;
271 : struct GNUNET_TIME_Timestamp final_expiration;
272 1 : struct GNUNET_TIME_Timestamp now = ts (1600000000);
273 : struct OpenContext ctx;
274 1 : bool no_funds = true;
275 :
276 1 : TDB_reserve (pg,
277 : 10,
278 : "10",
279 : &reserve_pub);
280 : /* TDB_reserve() gives the reserve an expiration far in the future, which
281 : would leave nothing for this call to do; pull it back to `now'. */
282 : {
283 1 : char *hex = TDB_hex (&reserve_pub,
284 : sizeof (reserve_pub));
285 :
286 1 : FAILIF_C (GNUNET_OK !=
287 : TDB_exec (pg,
288 : "UPDATE reserves"
289 : " SET expiration_date=%llu"
290 : " WHERE reserve_pub=decode('%s','hex');",
291 : (unsigned long long) now.abs_time.abs_value_us,
292 : hex),
293 : GNUNET_free (hex));
294 1 : GNUNET_free (hex);
295 : }
296 :
297 : /* one year of extension at an annual fee of EUR:1 */
298 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
299 : open_reserve (pg,
300 : &reserve_pub,
301 : 10,
302 : "1",
303 : "1",
304 : 1,
305 : ts (1600000000 + YEAR_US / 1000000),
306 : now,
307 : "1",
308 : &no_funds,
309 : &open_cost,
310 : &final_expiration));
311 1 : FAILIF (no_funds);
312 1 : FAILIF (0 != TALER_amount_cmp (&open_cost,
313 : &expect_cost));
314 1 : FAILIF (final_expiration.abs_time.abs_value_us !=
315 : now.abs_time.abs_value_us + YEAR_US);
316 :
317 : /* the reserve paid for it */
318 1 : memset (&reserve,
319 : 0,
320 : sizeof (reserve));
321 1 : reserve.pub = reserve_pub;
322 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
323 : TALER_EXCHANGEDB_get_reserve (pg,
324 : &reserve));
325 1 : FAILIF (0 != TALER_amount_cmp (&reserve.balance,
326 : &expect_balance));
327 1 : FAILIF (GNUNET_TIME_timestamp_cmp (reserve.expiry,
328 : !=,
329 : final_expiration));
330 :
331 : /* ...and the request itself was recorded */
332 1 : TDB_FILL (expect_sig,
333 : 10);
334 1 : memset (&ctx,
335 : 0,
336 : sizeof (ctx));
337 1 : ctx.reserve_pub = &reserve_pub;
338 1 : FAILIF (0 >=
339 : TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id (
340 : pg,
341 : 0,
342 : &open_cb,
343 : &ctx));
344 1 : FAILIF (1 != ctx.matched);
345 1 : FAILIF (0 != GNUNET_memcmp (&ctx.reserve_sig,
346 : &expect_sig));
347 1 : FAILIF (0 != TALER_amount_cmp (&ctx.reserve_payment,
348 : &expect_cost));
349 1 : FAILIF (1 != ctx.purse_limit);
350 1 : FAILIF (GNUNET_TIME_timestamp_cmp (ctx.expiration,
351 : !=,
352 : final_expiration));
353 1 : FAILIF (GNUNET_TIME_timestamp_cmp (ctx.request_timestamp,
354 : !=,
355 : now));
356 :
357 : /* a retry of the very same request is absorbed */
358 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
359 : open_reserve (pg,
360 : &reserve_pub,
361 : 10,
362 : "1",
363 : "1",
364 : 1,
365 : ts (1600000000 + YEAR_US / 1000000),
366 : now,
367 : "1",
368 : &no_funds,
369 : &open_cost,
370 : &final_expiration));
371 1 : FAILIF (1 != TDB_count (pg,
372 : "FROM reserves_open_requests"));
373 1 : return 0;
374 : }
375 :
376 :
377 : /**
378 : * A request that asks for nothing beyond what the reserve already has
379 : * costs nothing and is not recorded.
380 : *
381 : * @param pg the database context
382 : * @return 0 on success
383 : */
384 : static int
385 1 : check_nothing_to_do (struct TALER_EXCHANGEDB_PostgresContext *pg)
386 : {
387 : struct TALER_ReservePublicKeyP reserve_pub;
388 : struct TALER_Amount open_cost;
389 1 : struct TALER_Amount zero = TDB_amount ("0");
390 : struct GNUNET_TIME_Timestamp final_expiration;
391 1 : bool no_funds = true;
392 : uint64_t before;
393 :
394 1 : TDB_reserve (pg,
395 : 11,
396 : "10",
397 : &reserve_pub);
398 1 : before = TDB_count (pg,
399 : "FROM reserves_open_requests");
400 : /* The reserve already runs to 2096 and no additional purses are asked
401 : for, so there is nothing left to grant. Note that asking for even one
402 : purse would *not* be a no-op: a fresh reserve has purses_allowed=0. */
403 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
404 : open_reserve (pg,
405 : &reserve_pub,
406 : 11,
407 : "1",
408 : "1",
409 : 0,
410 : ts (1600000000),
411 : ts (1600000000),
412 : "1",
413 : &no_funds,
414 : &open_cost,
415 : &final_expiration));
416 1 : FAILIF (no_funds);
417 1 : FAILIF (0 != TALER_amount_cmp (&open_cost,
418 : &zero));
419 1 : FAILIF (before != TDB_count (pg,
420 : "FROM reserves_open_requests"));
421 1 : return 0;
422 : }
423 :
424 :
425 : /**
426 : * Too little paid, or too little in the reserve, both decline the work.
427 : *
428 : * @param pg the database context
429 : * @return 0 on success
430 : */
431 : static int
432 1 : check_insufficient (struct TALER_EXCHANGEDB_PostgresContext *pg)
433 : {
434 : struct TALER_ReservePublicKeyP reserve_pub;
435 : struct TALER_Amount open_cost;
436 1 : struct TALER_Amount expect_cost = TDB_amount ("2");
437 : struct GNUNET_TIME_Timestamp final_expiration;
438 1 : struct GNUNET_TIME_Timestamp now = ts (1600000000);
439 1 : bool no_funds = false;
440 : uint64_t before;
441 :
442 1 : TDB_reserve (pg,
443 : 12,
444 : "1",
445 : &reserve_pub);
446 : {
447 1 : char *hex = TDB_hex (&reserve_pub,
448 : sizeof (reserve_pub));
449 :
450 1 : FAILIF_C (GNUNET_OK !=
451 : TDB_exec (pg,
452 : "UPDATE reserves"
453 : " SET expiration_date=%llu"
454 : " WHERE reserve_pub=decode('%s','hex');",
455 : (unsigned long long) now.abs_time.abs_value_us,
456 : hex),
457 : GNUNET_free (hex));
458 1 : GNUNET_free (hex);
459 : }
460 1 : before = TDB_count (pg,
461 : "FROM reserves_open_requests");
462 :
463 : /* two years at EUR:1 a year, but only EUR:1 offered */
464 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
465 : open_reserve (pg,
466 : &reserve_pub,
467 : 12,
468 : "1",
469 : "1",
470 : 1,
471 : ts (1600000000 + 2 * (YEAR_US / 1000000)),
472 : now,
473 : "1",
474 : &no_funds,
475 : &open_cost,
476 : &final_expiration));
477 1 : FAILIF (no_funds);
478 1 : FAILIF (0 != TALER_amount_cmp (&open_cost,
479 : &expect_cost));
480 1 : FAILIF (before != TDB_count (pg,
481 : "FROM reserves_open_requests"));
482 :
483 : /* enough offered, but the reserve holds only EUR:1 of the EUR:2 */
484 1 : no_funds = false;
485 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
486 : open_reserve (pg,
487 : &reserve_pub,
488 : 12,
489 : "2",
490 : "2",
491 : 1,
492 : ts (1600000000 + 2 * (YEAR_US / 1000000)),
493 : now,
494 : "1",
495 : &no_funds,
496 : &open_cost,
497 : &final_expiration));
498 1 : FAILIF (! no_funds);
499 1 : FAILIF (before != TDB_count (pg,
500 : "FROM reserves_open_requests"));
501 1 : return 0;
502 : }
503 :
504 :
505 : /**
506 : * The iterator's serial bound and its abort return behave as documented.
507 : *
508 : * @param pg the database context
509 : * @return 0 on success
510 : */
511 : static int
512 1 : check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
513 : {
514 : struct TALER_ReservePublicKeyP reserve_pub;
515 : struct TALER_Amount open_cost;
516 : struct GNUNET_TIME_Timestamp final_expiration;
517 1 : struct GNUNET_TIME_Timestamp now = ts (1600000000);
518 : struct OpenContext ctx;
519 1 : bool no_funds = true;
520 :
521 : /* a second recorded request, from another reserve */
522 1 : TDB_reserve (pg,
523 : 13,
524 : "10",
525 : &reserve_pub);
526 : {
527 1 : char *hex = TDB_hex (&reserve_pub,
528 : sizeof (reserve_pub));
529 :
530 1 : FAILIF_C (GNUNET_OK !=
531 : TDB_exec (pg,
532 : "UPDATE reserves"
533 : " SET expiration_date=%llu"
534 : " WHERE reserve_pub=decode('%s','hex');",
535 : (unsigned long long) now.abs_time.abs_value_us,
536 : hex),
537 : GNUNET_free (hex));
538 1 : GNUNET_free (hex);
539 : }
540 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
541 : open_reserve (pg,
542 : &reserve_pub,
543 : 13,
544 : "1",
545 : "1",
546 : 1,
547 : ts (1600000000 + YEAR_US / 1000000),
548 : now,
549 : "1",
550 : &no_funds,
551 : &open_cost,
552 : &final_expiration));
553 1 : FAILIF (2 != TDB_count (pg,
554 : "FROM reserves_open_requests"));
555 :
556 1 : memset (&ctx,
557 : 0,
558 : sizeof (ctx));
559 1 : FAILIF (2 !=
560 : TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id (
561 : pg,
562 : 0,
563 : &open_cb,
564 : &ctx));
565 1 : memset (&ctx,
566 : 0,
567 : sizeof (ctx));
568 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
569 : TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id (
570 : pg,
571 : 1000,
572 : &open_cb,
573 : &ctx));
574 1 : FAILIF (0 != ctx.total);
575 :
576 1 : memset (&ctx,
577 : 0,
578 : sizeof (ctx));
579 1 : ctx.stop_after = 1;
580 1 : FAILIF (2 !=
581 : TALER_EXCHANGEDB_iterate_reserve_open_above_serial_id (
582 : pg,
583 : 0,
584 : &open_cb,
585 : &ctx));
586 1 : FAILIF (1 != ctx.total);
587 1 : return 0;
588 : }
589 :
590 :
591 : /**
592 : * The checks to run, in order.
593 : */
594 : static const struct TDB_Test tests[] = {
595 : { "reserves-open-requests-empty",
596 : &check_empty },
597 : { "reserves-open-requests-open",
598 : &check_open },
599 : { "reserves-open-requests-nothing-to-do",
600 : &check_nothing_to_do },
601 : { "reserves-open-requests-insufficient",
602 : &check_insufficient },
603 : { "reserves-open-requests-iterate",
604 : &check_iterate },
605 : { NULL, NULL }
606 : };
607 :
608 :
609 : int
610 1 : main (int argc,
611 : char *const *argv)
612 : {
613 1 : return TDB_main (argc,
614 : argv,
615 : "test-reserves-open-requests",
616 : "Tests for the exchangedb `reserves_open_requests' table",
617 : tests);
618 : }
619 :
620 :
621 : /* end of test_reserves_open_requests.c */
|