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_batch_deposits.c
18 : * @brief tests for the exchangedb functions whose primary table is
19 : * `batch_deposits`
20 : * @author Christian Grothoff
21 : *
22 : * Covers #TALER_EXCHANGEDB_do_deposit(),
23 : * #TALER_EXCHANGEDB_do_check_deposit_idempotent(),
24 : * #TALER_EXCHANGEDB_get_exists_deposit(),
25 : * #TALER_EXCHANGEDB_get_ready_deposit(),
26 : * #TALER_EXCHANGEDB_iterate_batch_deposits_missing_wire() and
27 : * #TALER_EXCHANGEDB_get_wire_hash_for_contract().
28 : *
29 : * `batch_deposits` references `wire_targets` and, through `coin_deposits`,
30 : * `known_coins`. A deposit is "ready" only once its wire deadline has
31 : * passed *and* its refund deadline is behind the aggregation shift
32 : * boundary, so the checks use deadlines in the past for that.
33 : */
34 : #include "test_common.h"
35 : #include "exchange-database/do_check_deposit_idempotent.h"
36 : #include "exchange-database/do_deposit.h"
37 : #include "exchange-database/get_exists_deposit.h"
38 : #include "exchange-database/get_ready_deposit.h"
39 : #include "exchange-database/get_wire_hash_for_contract.h"
40 : #include "exchange-database/iterate_batch_deposits_missing_wire.h"
41 : #include "exchange-database/rollback.h"
42 : #include "exchange-database/start.h"
43 :
44 :
45 : /**
46 : * Account the merchants of the checks are paid at.
47 : */
48 : static struct TDB_Account account;
49 :
50 :
51 : /**
52 : * Denomination the checks deposit.
53 : */
54 : static struct TDB_Denom denom;
55 :
56 :
57 : /**
58 : * Build a timestamp from a number of seconds since the epoch.
59 : *
60 : * @param secs seconds since the epoch
61 : * @return the timestamp
62 : */
63 : static struct GNUNET_TIME_Timestamp
64 7 : ts (uint64_t secs)
65 : {
66 7 : struct GNUNET_TIME_Absolute abs = {
67 7 : .abs_value_us = secs * 1000LLU * 1000LLU
68 : };
69 :
70 7 : return GNUNET_TIME_absolute_to_timestamp (abs);
71 : }
72 :
73 :
74 : /**
75 : * Closure for #missing_cb().
76 : */
77 : struct MissingContext
78 : {
79 : /**
80 : * How many deposits did the callback see?
81 : */
82 : unsigned int total;
83 :
84 : /**
85 : * Row of the first deposit seen.
86 : */
87 : uint64_t first_row;
88 :
89 : /**
90 : * Sum of the whole-unit parts of the amounts seen.
91 : */
92 : uint64_t value_sum;
93 :
94 : /**
95 : * Account of the last deposit seen.
96 : */
97 : struct TALER_FullPaytoHashP h_payto;
98 : };
99 :
100 :
101 : /**
102 : * Callback for #TALER_EXCHANGEDB_iterate_batch_deposits_missing_wire().
103 : *
104 : * @param cls a `struct MissingContext *`
105 : * @param batch_deposit_serial_id row of the deposit
106 : * @param total_amount how much is owed
107 : * @param wire_target_h_payto where it is owed
108 : * @param deadline when it was due
109 : */
110 : static void
111 1 : missing_cb (void *cls,
112 : uint64_t batch_deposit_serial_id,
113 : const struct TALER_Amount *total_amount,
114 : const struct TALER_FullPaytoHashP *wire_target_h_payto,
115 : struct GNUNET_TIME_Timestamp deadline)
116 : {
117 1 : struct MissingContext *ctx = cls;
118 :
119 : (void) deadline;
120 1 : if (0 == ctx->total++)
121 1 : ctx->first_row = batch_deposit_serial_id;
122 1 : ctx->value_sum += total_amount->value;
123 1 : ctx->h_payto = *wire_target_h_payto;
124 1 : }
125 :
126 :
127 : /**
128 : * Nothing is known while the table is empty.
129 : *
130 : * @param pg the database context
131 : * @return 0 on success
132 : */
133 : static int
134 1 : check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
135 : {
136 : struct TALER_MerchantPublicKeyP merchant_pub;
137 : struct TALER_PrivateContractHashP h_contract_terms;
138 : struct TALER_MerchantWireHashP h_wire;
139 : struct TALER_CoinSpendPublicKeyP coin_pub;
140 : struct TALER_Amount deposit_fee;
141 : struct GNUNET_TIME_Timestamp exchange_timestamp;
142 1 : struct TALER_FullPayto payto_uri = { NULL };
143 1 : struct MissingContext ctx = { 0 };
144 1 : char *metadata = NULL;
145 :
146 1 : TDB_FILL (merchant_pub,
147 : 1);
148 1 : TDB_FILL (h_contract_terms,
149 : 1);
150 1 : TDB_FILL (h_wire,
151 : 1);
152 1 : TDB_FILL (coin_pub,
153 : 1);
154 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
155 : TALER_EXCHANGEDB_get_wire_hash_for_contract (pg,
156 : &merchant_pub,
157 : &h_contract_terms,
158 : &h_wire));
159 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
160 : TALER_EXCHANGEDB_get_exists_deposit (pg,
161 : &h_contract_terms,
162 : &h_wire,
163 : &coin_pub,
164 : &merchant_pub,
165 : ts (1600000000),
166 : &deposit_fee,
167 : &exchange_timestamp));
168 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
169 : TALER_EXCHANGEDB_get_ready_deposit (pg,
170 : 0,
171 : UINT32_MAX,
172 : &merchant_pub,
173 : &payto_uri,
174 : &metadata));
175 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
176 : TALER_EXCHANGEDB_iterate_batch_deposits_missing_wire (pg,
177 : 0,
178 : &missing_cb,
179 : &ctx));
180 1 : FAILIF (0 != ctx.total);
181 1 : return 0;
182 : }
183 :
184 :
185 : /**
186 : * A deposit of more than the coin holds is refused.
187 : *
188 : * @param pg the database context
189 : * @return 0 on success
190 : */
191 : static int
192 1 : check_insufficient_balance (struct TALER_EXCHANGEDB_PostgresContext *pg)
193 : {
194 : struct TALER_CoinPublicInfo coin;
195 : struct TALER_EXCHANGEDB_CoinDepositInformation cdi;
196 : struct TALER_EXCHANGEDB_BatchDeposit bd;
197 : struct TALER_Amount fees[1];
198 : struct TALER_Amount total;
199 1 : struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
200 1 : uint32_t bad_balance_index = 42;
201 1 : bool balance_ok = true;
202 1 : bool in_conflict = true;
203 :
204 1 : TDB_denom (pg,
205 : 10,
206 : "5",
207 : "0.1",
208 : &denom);
209 1 : TDB_account (pg,
210 : 10,
211 : &account);
212 1 : TDB_coin (pg,
213 : &denom,
214 : 20,
215 : &coin,
216 : NULL);
217 1 : memset (&cdi,
218 : 0,
219 : sizeof (cdi));
220 1 : cdi.coin = coin;
221 1 : TDB_FILL (cdi.csig,
222 : 20);
223 1 : cdi.amount_with_fee = TDB_amount ("6");
224 1 : fees[0] = TDB_amount ("0.1");
225 1 : memset (&bd,
226 : 0,
227 : sizeof (bd));
228 1 : TDB_FILL (bd.merchant_pub,
229 : 20);
230 1 : TDB_FILL (bd.merchant_sig,
231 : 20);
232 1 : TDB_FILL (bd.h_contract_terms,
233 : 20);
234 1 : bd.no_wallet_data_hash = true;
235 1 : bd.wire_target_h_payto = account.h_full;
236 1 : TDB_FILL (bd.wire_salt,
237 : 20);
238 1 : bd.wallet_timestamp = now;
239 1 : bd.refund_deadline = now;
240 1 : bd.wire_deadline = now;
241 1 : bd.receiver_wire_account = account.payto;
242 1 : bd.cdis = &cdi;
243 1 : bd.num_cdis = 1;
244 : /* The batch row goes in before the per-coin balance is checked, so this
245 : is how the /deposit handler uses the function: inside a transaction it
246 : can roll back. */
247 1 : FAILIF_C (GNUNET_OK !=
248 : TALER_EXCHANGEDB_start (pg,
249 : "test-deposit-insufficient"),
250 : TDB_coin_free (&coin));
251 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
252 : TALER_EXCHANGEDB_do_deposit (pg,
253 : &bd,
254 : fees,
255 : &now,
256 : &total,
257 : &balance_ok,
258 : &bad_balance_index,
259 : &in_conflict),
260 : TALER_EXCHANGEDB_rollback (pg); TDB_coin_free (&coin));
261 1 : TALER_EXCHANGEDB_rollback (pg);
262 1 : TDB_coin_free (&coin);
263 1 : FAILIF (balance_ok);
264 1 : FAILIF (in_conflict);
265 1 : FAILIF (0 != bad_balance_index);
266 1 : FAILIF (0 != TDB_count (pg,
267 : "FROM batch_deposits"));
268 1 : return 0;
269 : }
270 :
271 :
272 : /**
273 : * A deposit within the coin's balance is recorded and can be found again.
274 : *
275 : * @param pg the database context
276 : * @return 0 on success
277 : */
278 : static int
279 1 : check_deposit (struct TALER_EXCHANGEDB_PostgresContext *pg)
280 : {
281 : struct TALER_CoinPublicInfo coin;
282 : struct TDB_Deposit dep;
283 : struct TALER_MerchantWireHashP h_wire;
284 : struct TALER_Amount deposit_fee;
285 1 : struct TALER_Amount expect_fee = TDB_amount ("0.1");
286 : struct GNUNET_TIME_Timestamp exchange_timestamp;
287 :
288 1 : TDB_coin (pg,
289 : &denom,
290 : 21,
291 : &coin,
292 : NULL);
293 1 : TDB_deposit (pg,
294 : &account,
295 : &coin,
296 : 21,
297 : "1",
298 : "0.1",
299 : ts (1600000000),
300 : ts (1600000000),
301 : &dep);
302 1 : FAILIF_C (1 != TDB_count (pg,
303 : "FROM batch_deposits"),
304 : TDB_coin_free (&coin));
305 1 : FAILIF_C (1 != TDB_count (pg,
306 : "FROM coin_deposits"),
307 : TDB_coin_free (&coin));
308 :
309 : /* the salted account hash comes back from the contract */
310 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
311 : TALER_EXCHANGEDB_get_wire_hash_for_contract (pg,
312 : &dep.merchant_pub,
313 : &dep.h_contract_terms,
314 : &h_wire),
315 : TDB_coin_free (&coin));
316 1 : FAILIF_C (0 != GNUNET_memcmp (&h_wire,
317 : &dep.h_wire),
318 : TDB_coin_free (&coin));
319 :
320 : /* ...and the deposit itself, with its fee */
321 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
322 : TALER_EXCHANGEDB_get_exists_deposit (pg,
323 : &dep.h_contract_terms,
324 : &dep.h_wire,
325 : &coin.coin_pub,
326 : &dep.merchant_pub,
327 : dep.refund_deadline,
328 : &deposit_fee,
329 : &exchange_timestamp),
330 : TDB_coin_free (&coin));
331 1 : FAILIF_C (0 != TALER_amount_cmp (&deposit_fee,
332 : &expect_fee),
333 : TDB_coin_free (&coin));
334 :
335 : /* a different refund deadline makes it a different deposit */
336 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
337 : TALER_EXCHANGEDB_get_exists_deposit (pg,
338 : &dep.h_contract_terms,
339 : &dep.h_wire,
340 : &coin.coin_pub,
341 : &dep.merchant_pub,
342 : ts (1700000000),
343 : &deposit_fee,
344 : &exchange_timestamp),
345 : TDB_coin_free (&coin));
346 : /* ...and so does a different account hash */
347 : {
348 : struct TALER_MerchantWireHashP other;
349 :
350 1 : TDB_FILL (other,
351 : 99);
352 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
353 : TALER_EXCHANGEDB_get_exists_deposit (pg,
354 : &dep.h_contract_terms,
355 : &other,
356 : &coin.coin_pub,
357 : &dep.merchant_pub,
358 : dep.refund_deadline,
359 : &deposit_fee,
360 : &exchange_timestamp),
361 : TDB_coin_free (&coin));
362 : }
363 1 : TDB_coin_free (&coin);
364 1 : return 0;
365 : }
366 :
367 :
368 : /**
369 : * A replayed deposit is recognised as idempotent; a different one is not.
370 : *
371 : * @param pg the database context
372 : * @return 0 on success
373 : */
374 : static int
375 1 : check_idempotent (struct TALER_EXCHANGEDB_PostgresContext *pg)
376 : {
377 : struct TALER_CoinPublicInfo coin;
378 : struct TALER_EXCHANGEDB_CoinDepositInformation cdi;
379 : struct TALER_EXCHANGEDB_BatchDeposit bd;
380 : struct GNUNET_TIME_Timestamp exchange_timestamp;
381 1 : bool is_idempotent = false;
382 :
383 1 : TDB_coin (pg,
384 : &denom,
385 : 21,
386 : &coin,
387 : NULL);
388 1 : memset (&cdi,
389 : 0,
390 : sizeof (cdi));
391 1 : cdi.coin = coin;
392 1 : TDB_FILL (cdi.csig,
393 : 21);
394 1 : cdi.amount_with_fee = TDB_amount ("1");
395 1 : memset (&bd,
396 : 0,
397 : sizeof (bd));
398 1 : TDB_FILL (bd.merchant_pub,
399 : 21);
400 1 : TDB_FILL (bd.merchant_sig,
401 : 21);
402 1 : TDB_FILL (bd.h_contract_terms,
403 : 21);
404 1 : bd.no_wallet_data_hash = true;
405 1 : bd.wire_target_h_payto = account.h_full;
406 1 : TDB_FILL (bd.wire_salt,
407 : 21);
408 1 : bd.wallet_timestamp = ts (1600000000);
409 1 : bd.refund_deadline = ts (1600000000);
410 1 : bd.wire_deadline = ts (1600000000);
411 1 : bd.receiver_wire_account = account.payto;
412 1 : bd.cdis = &cdi;
413 1 : bd.num_cdis = 1;
414 1 : exchange_timestamp = GNUNET_TIME_timestamp_get ();
415 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
416 : TALER_EXCHANGEDB_do_check_deposit_idempotent (
417 : pg,
418 : &bd,
419 : &exchange_timestamp,
420 : &is_idempotent),
421 : TDB_coin_free (&coin));
422 1 : FAILIF_C (! is_idempotent,
423 : TDB_coin_free (&coin));
424 :
425 : /* a different amount for the same contract is not the same request */
426 1 : cdi.amount_with_fee = TDB_amount ("2");
427 1 : is_idempotent = true;
428 1 : FAILIF_C (0 >
429 : TALER_EXCHANGEDB_do_check_deposit_idempotent (
430 : pg,
431 : &bd,
432 : &exchange_timestamp,
433 : &is_idempotent),
434 : TDB_coin_free (&coin));
435 1 : FAILIF_C (is_idempotent,
436 : TDB_coin_free (&coin));
437 :
438 : /* neither is a contract we never saw */
439 1 : cdi.amount_with_fee = TDB_amount ("1");
440 1 : TDB_FILL (bd.h_contract_terms,
441 : 98);
442 1 : is_idempotent = true;
443 1 : FAILIF_C (0 >
444 : TALER_EXCHANGEDB_do_check_deposit_idempotent (
445 : pg,
446 : &bd,
447 : &exchange_timestamp,
448 : &is_idempotent),
449 : TDB_coin_free (&coin));
450 1 : FAILIF_C (is_idempotent,
451 : TDB_coin_free (&coin));
452 1 : TDB_coin_free (&coin);
453 1 : return 0;
454 : }
455 :
456 :
457 : /**
458 : * A deposit whose deadlines have passed becomes ready for the aggregator,
459 : * and shows up as missing a wire transfer.
460 : *
461 : * @param pg the database context
462 : * @return 0 on success
463 : */
464 : static int
465 1 : check_ready (struct TALER_EXCHANGEDB_PostgresContext *pg)
466 : {
467 : struct TALER_MerchantPublicKeyP merchant_pub;
468 1 : struct TALER_FullPayto payto_uri = { NULL };
469 : struct MissingContext ctx;
470 1 : char *metadata = NULL;
471 :
472 : /* the deposit from check_deposit() has deadlines in 2020 */
473 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
474 : TALER_EXCHANGEDB_get_ready_deposit (pg,
475 : 0,
476 : UINT32_MAX,
477 : &merchant_pub,
478 : &payto_uri,
479 : &metadata));
480 1 : FAILIF_C (0 != strcmp (payto_uri.full_payto,
481 : account.payto.full_payto),
482 : GNUNET_free (payto_uri.full_payto); GNUNET_free (metadata));
483 1 : GNUNET_free (payto_uri.full_payto);
484 1 : GNUNET_free (metadata);
485 : /* no extra metadata was set */
486 1 : FAILIF (NULL != metadata);
487 :
488 : /* a shard range the merchant is not in has nothing ready */
489 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
490 : TALER_EXCHANGEDB_get_ready_deposit (pg,
491 : 0,
492 : 0,
493 : &merchant_pub,
494 : &payto_uri,
495 : &metadata));
496 :
497 1 : memset (&ctx,
498 : 0,
499 : sizeof (ctx));
500 1 : FAILIF (0 >=
501 : TALER_EXCHANGEDB_iterate_batch_deposits_missing_wire (pg,
502 : 0,
503 : &missing_cb,
504 : &ctx));
505 1 : FAILIF (1 != ctx.total);
506 1 : FAILIF (0 != GNUNET_memcmp (&ctx.h_payto,
507 : &account.h_full));
508 : /* the amount reported is the gross deposit, fee included */
509 1 : FAILIF (1 != ctx.value_sum);
510 :
511 : /* the serial bound excludes it again */
512 1 : memset (&ctx,
513 : 0,
514 : sizeof (ctx));
515 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
516 : TALER_EXCHANGEDB_iterate_batch_deposits_missing_wire (pg,
517 : 1000,
518 : &missing_cb,
519 : &ctx));
520 1 : FAILIF (0 != ctx.total);
521 1 : return 0;
522 : }
523 :
524 :
525 : /**
526 : * A deposit that is done or policy-blocked is not ready.
527 : *
528 : * @param pg the database context
529 : * @return 0 on success
530 : */
531 : static int
532 1 : check_not_ready (struct TALER_EXCHANGEDB_PostgresContext *pg)
533 : {
534 : struct TALER_MerchantPublicKeyP merchant_pub;
535 1 : struct TALER_FullPayto payto_uri = { NULL };
536 1 : char *metadata = NULL;
537 :
538 1 : FAILIF (GNUNET_OK !=
539 : TDB_exec (pg,
540 : "UPDATE batch_deposits SET done=TRUE;"));
541 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
542 : TALER_EXCHANGEDB_get_ready_deposit (pg,
543 : 0,
544 : UINT32_MAX,
545 : &merchant_pub,
546 : &payto_uri,
547 : &metadata));
548 1 : FAILIF (GNUNET_OK !=
549 : TDB_exec (pg,
550 : "UPDATE batch_deposits"
551 : " SET done=FALSE, policy_blocked=TRUE;"));
552 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
553 : TALER_EXCHANGEDB_get_ready_deposit (pg,
554 : 0,
555 : UINT32_MAX,
556 : &merchant_pub,
557 : &payto_uri,
558 : &metadata));
559 1 : FAILIF (GNUNET_OK !=
560 : TDB_exec (pg,
561 : "UPDATE batch_deposits SET policy_blocked=FALSE;"));
562 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
563 : TALER_EXCHANGEDB_get_ready_deposit (pg,
564 : 0,
565 : UINT32_MAX,
566 : &merchant_pub,
567 : &payto_uri,
568 : &metadata));
569 1 : GNUNET_free (payto_uri.full_payto);
570 1 : GNUNET_free (metadata);
571 1 : return 0;
572 : }
573 :
574 :
575 : /**
576 : * The checks to run, in order.
577 : */
578 : static const struct TDB_Test tests[] = {
579 : { "batch-deposits-empty",
580 : &check_empty },
581 : { "batch-deposits-insufficient-balance",
582 : &check_insufficient_balance },
583 : { "batch-deposits-deposit",
584 : &check_deposit },
585 : { "batch-deposits-idempotent",
586 : &check_idempotent },
587 : { "batch-deposits-ready",
588 : &check_ready },
589 : { "batch-deposits-not-ready",
590 : &check_not_ready },
591 : { NULL, NULL }
592 : };
593 :
594 :
595 : int
596 1 : main (int argc,
597 : char *const *argv)
598 : {
599 : int ret;
600 :
601 1 : ret = TDB_main (argc,
602 : argv,
603 : "test-batch-deposits",
604 : "Tests for the exchangedb `batch_deposits' table",
605 : tests);
606 1 : TDB_account_free (&account);
607 1 : TDB_denom_free (&denom);
608 1 : return ret;
609 : }
610 :
611 :
612 : /* end of test_batch_deposits.c */
|