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_regressions.c
18 : * @brief regression tests for individual exchangedb operations
19 : *
20 : * Each check in the #tests table pins down one previously broken behaviour.
21 : * The tests share one scratch database, which test_regressions.sh creates and
22 : * removes; every check must therefore use fresh keys rather than assume an
23 : * empty database.
24 : */
25 : #include "exchangedb_lib.h"
26 : #include "taler/taler_json_lib.h"
27 : #include "helper.h"
28 : #include "exchange-database/create_tables.h"
29 : #include "exchange-database/do_aggregate.h"
30 : #include "exchange-database/compute_shard.h"
31 : #include "exchange-database/start.h"
32 : #include "exchange-database/commit.h"
33 : #include "exchange-database/rollback.h"
34 : #include "exchange-database/do_reserve_open.h"
35 : #include "exchange-database/get_purse_deposit.h"
36 : #include "exchange-database/get_reserve_close_info.h"
37 : #include "exchange-database/begin_shard.h"
38 : #include "exchange-database/abort_shard.h"
39 : #include "exchange-database/update_shard_progress.h"
40 : #include "exchange-database/do_import_credits.h"
41 :
42 :
43 : /**
44 : * Currency we use, must match test_regressions.sh.
45 : */
46 : #define CURRENCY "EUR"
47 :
48 : /**
49 : * Report a failed expectation and return 1 from the calling check.
50 : */
51 : #define FAILIF(cond) \
52 : do { \
53 : if (! (cond)) break; \
54 : GNUNET_break (0); \
55 : fprintf (stderr, \
56 : "FAILED: %s at %s:%u\n", \
57 : # cond, __FILE__, __LINE__); \
58 : return 1; \
59 : } while (0)
60 :
61 :
62 : /**
63 : * Our database context.
64 : */
65 : static struct TALER_EXCHANGEDB_PostgresContext *pg;
66 :
67 : /**
68 : * Name of the single check to run, NULL to run all of them.
69 : */
70 : static char *only;
71 :
72 : /**
73 : * Return value of the process.
74 : */
75 : static int result;
76 :
77 :
78 : /**
79 : * Run @a sql on our database connection, outside of any transaction.
80 : *
81 : * @param sql statement(s) to run
82 : * @return #GNUNET_OK on success
83 : */
84 : static enum GNUNET_GenericReturnValue
85 15 : exec_sql (const char *sql)
86 : {
87 15 : struct GNUNET_PQ_ExecuteStatement es[] = {
88 15 : GNUNET_PQ_make_execute (sql),
89 : GNUNET_PQ_EXECUTE_STATEMENT_END
90 : };
91 :
92 15 : return GNUNET_PQ_exec_statements (pg->conn,
93 : es);
94 : }
95 :
96 :
97 : /**
98 : * Convert @a val to an amount in our currency.
99 : *
100 : * @param str amount without the currency prefix, e.g. "10.5"
101 : * @param[out] amount set to the parsed amount
102 : */
103 : static void
104 8 : parse_amount (const char *str,
105 : struct TALER_Amount *amount)
106 : {
107 : char *s;
108 :
109 8 : GNUNET_asprintf (&s,
110 : CURRENCY ":%s",
111 : str);
112 8 : GNUNET_assert (GNUNET_OK ==
113 : TALER_string_to_amount (s,
114 : amount));
115 8 : GNUNET_free (s);
116 8 : }
117 :
118 :
119 : /**
120 : * Build an INSERT that creates a reserve with a zero balance and no
121 : * `reserves_in` row, the way exchange_do_purse_merge() does.
122 : *
123 : * @param reserve_pub public key of the reserve to create
124 : * @return SQL statement, to be freed by the caller
125 : */
126 : static char *
127 4 : hex_insert_reserve (const struct TALER_ReservePublicKeyP *reserve_pub)
128 : {
129 : char hex[sizeof (*reserve_pub) * 2 + 1];
130 4 : const unsigned char *raw = (const unsigned char *) reserve_pub;
131 : char *sql;
132 :
133 132 : for (unsigned int i = 0; i<sizeof (*reserve_pub); i++)
134 128 : GNUNET_snprintf (&hex[i * 2],
135 : 3,
136 : "%02x",
137 128 : raw[i]);
138 4 : GNUNET_asprintf (&sql,
139 : "INSERT INTO reserves"
140 : " (reserve_pub,current_balance,expiration_date,gc_date)"
141 : " VALUES"
142 : " (decode('%s','hex')"
143 : " ,ROW(0,0)::taler_amount"
144 : " ,1770000000000000"
145 : " ,1780000000000000);",
146 : hex);
147 4 : return sql;
148 : }
149 :
150 :
151 : /**
152 : * Render @a data as a lowercase hex string for use in an SQL literal.
153 : *
154 : * @param data binary data
155 : * @param size number of bytes in @a data
156 : * @return hex string, to be freed by the caller
157 : */
158 : static char *
159 6 : to_hex (const void *data,
160 : size_t size)
161 : {
162 6 : const unsigned char *raw = data;
163 : char *hex;
164 :
165 6 : hex = GNUNET_malloc (size * 2 + 1);
166 198 : for (size_t i = 0; i<size; i++)
167 192 : GNUNET_snprintf (&hex[i * 2],
168 : 3,
169 : "%02x",
170 192 : raw[i]);
171 6 : return hex;
172 : }
173 :
174 :
175 : /**
176 : * E-2: a merchant may refund more than (deposit - deposit fee). The
177 : * aggregator used to charge the full deposit fee on top of such a refund,
178 : * making the amount to be wired out negative, and then abort() on the
179 : * GNUNET_assert() guarding the subtraction -- stopping every payout for the
180 : * shard, and doing so again on every restart.
181 : *
182 : * Deposit EUR:1.00 of a denomination with a EUR:0.10 deposit fee, refund
183 : * EUR:0.95 of it, and require the aggregation to come out at EUR:0.
184 : */
185 : static int
186 1 : check_aggregate_refund_below_deposit_fee (void)
187 : {
188 : struct TALER_MerchantPublicKeyP merchant_pub;
189 : struct TALER_FullPaytoHashP h_payto;
190 : struct TALER_NormalizedPaytoHashP h_norm;
191 : struct TALER_CoinSpendPublicKeyP coin_pub;
192 : struct TALER_WireTransferIdentifierRawP wtid;
193 : struct TALER_Amount total;
194 : struct TALER_Amount zero;
195 : char *sql;
196 : char *m_hex;
197 : char *p_hex;
198 : char *n_hex;
199 : char *c_hex;
200 : enum GNUNET_GenericReturnValue ok;
201 :
202 1 : parse_amount ("0",
203 : &zero);
204 1 : memset (&merchant_pub,
205 : 0x41,
206 : sizeof (merchant_pub));
207 1 : memset (&h_payto,
208 : 0x42,
209 : sizeof (h_payto));
210 1 : memset (&h_norm,
211 : 0x43,
212 : sizeof (h_norm));
213 1 : memset (&coin_pub,
214 : 0x44,
215 : sizeof (coin_pub));
216 1 : memset (&wtid,
217 : 0x45,
218 : sizeof (wtid));
219 1 : m_hex = to_hex (&merchant_pub,
220 : sizeof (merchant_pub));
221 1 : p_hex = to_hex (&h_payto,
222 : sizeof (h_payto));
223 1 : n_hex = to_hex (&h_norm,
224 : sizeof (h_norm));
225 1 : c_hex = to_hex (&coin_pub,
226 : sizeof (coin_pub));
227 1 : GNUNET_asprintf (
228 : &sql,
229 : "INSERT INTO kyc_targets (h_normalized_payto)"
230 : " VALUES (decode('%s','hex')) ON CONFLICT DO NOTHING;"
231 : "INSERT INTO wire_targets"
232 : " (wire_target_h_payto,payto_uri,h_normalized_payto)"
233 : " VALUES (decode('%s','hex'),'payto://x-taler-bank/h/e2',"
234 : " decode('%s','hex')) ON CONFLICT DO NOTHING;"
235 : "INSERT INTO denominations"
236 : " (denom_pub_hash,denom_type,age_mask,denom_pub,master_sig"
237 : " ,valid_from,expire_withdraw,expire_deposit,expire_legal"
238 : " ,coin,fee_withdraw,fee_deposit,fee_refresh,fee_refund)"
239 : " VALUES (decode(repeat('e2',64),'hex'),1,0,decode('00','hex')"
240 : " ,decode(repeat('00',64),'hex'),0,0,0,0"
241 : " ,ROW(1,0)::taler_amount,ROW(0,0)::taler_amount"
242 : " ,ROW(0,10000000)::taler_amount,ROW(0,0)::taler_amount"
243 : " ,ROW(0,0)::taler_amount);"
244 : "INSERT INTO known_coins"
245 : " (denominations_serial,coin_pub,denom_sig,remaining)"
246 : " VALUES ((SELECT denominations_serial FROM denominations"
247 : " WHERE denom_pub_hash=decode(repeat('e2',64),'hex'))"
248 : " ,decode('%s','hex'),decode('00','hex'),ROW(0,0)::taler_amount);"
249 : "INSERT INTO batch_deposits"
250 : " (shard,merchant_pub,wallet_timestamp,exchange_timestamp"
251 : " ,refund_deadline,wire_deadline,h_contract_terms,wire_salt"
252 : " ,wire_target_h_payto,policy_blocked,total_amount,merchant_sig"
253 : " ,done,total_without_fee)"
254 : " VALUES (%llu,decode('%s','hex'),0,0,1,1"
255 : " ,decode(repeat('e2',64),'hex'),decode(repeat('e2',16),'hex')"
256 : " ,decode('%s','hex'),FALSE,ROW(1,0)::taler_amount"
257 : " ,decode(repeat('00',64),'hex'),FALSE"
258 : " ,ROW(0,90000000)::taler_amount);"
259 : "INSERT INTO coin_deposits"
260 : " (batch_deposit_serial_id,coin_pub,coin_sig,amount_with_fee)"
261 : " VALUES ((SELECT batch_deposit_serial_id FROM batch_deposits"
262 : " WHERE merchant_pub=decode('%s','hex'))"
263 : " ,decode('%s','hex'),decode(repeat('e2',64),'hex')"
264 : " ,ROW(1,0)::taler_amount);"
265 : "INSERT INTO refunds"
266 : " (coin_pub,batch_deposit_serial_id,merchant_sig,rtransaction_id"
267 : " ,amount_with_fee)"
268 : " VALUES (decode('%s','hex')"
269 : " ,(SELECT batch_deposit_serial_id FROM batch_deposits"
270 : " WHERE merchant_pub=decode('%s','hex'))"
271 : " ,decode(repeat('00',64),'hex'),1"
272 : " ,ROW(0,95000000)::taler_amount);",
273 : n_hex,
274 : p_hex,
275 : n_hex,
276 : c_hex,
277 1 : (unsigned long long) TALER_EXCHANGEDB_compute_shard (&merchant_pub),
278 : m_hex,
279 : p_hex,
280 : m_hex,
281 : c_hex,
282 : c_hex,
283 : m_hex);
284 1 : ok = exec_sql (sql);
285 1 : GNUNET_free (sql);
286 1 : GNUNET_free (m_hex);
287 1 : GNUNET_free (p_hex);
288 1 : GNUNET_free (n_hex);
289 1 : GNUNET_free (c_hex);
290 1 : FAILIF (GNUNET_OK != ok);
291 :
292 : /* Before the fix this abort()ed inside TALER_EXCHANGEDB_do_aggregate(). */
293 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
294 : TALER_EXCHANGEDB_do_aggregate (pg,
295 : &h_payto,
296 : &merchant_pub,
297 : &wtid,
298 : &total));
299 1 : FAILIF (0 !=
300 : TALER_amount_cmp (&zero,
301 : &total));
302 1 : return 0;
303 : }
304 :
305 :
306 : /**
307 : * E-13: PostgreSQL accepts COMMIT on a transaction it has already aborted,
308 : * rolls it back and answers with the command tag ROLLBACK and no error at
309 : * all. TALER_EXCHANGEDB_commit() used to pass that straight through as
310 : * GNUNET_DB_STATUS_SUCCESS_NO_RESULTS, i.e. every caller was told the
311 : * transaction had committed while its writes were gone.
312 : */
313 : static int
314 1 : check_commit_detects_rolled_back_transaction (void)
315 : {
316 : /* A transaction that is aborted mid-way must NOT commit successfully. */
317 1 : FAILIF (GNUNET_OK !=
318 : TALER_EXCHANGEDB_start (pg,
319 : "test-e13-aborted"));
320 1 : FAILIF (GNUNET_OK !=
321 : exec_sql ("INSERT INTO kyc_targets (h_normalized_payto)"
322 : " VALUES (decode(repeat('13',32),'hex'));"));
323 : /* Provoke an error; from here on the transaction is doomed. */
324 1 : FAILIF (GNUNET_OK ==
325 : exec_sql ("SELECT 1/0;"));
326 : /* Before the fix this returned SUCCESS_NO_RESULTS (0). */
327 1 : FAILIF (0 <=
328 : TALER_EXCHANGEDB_commit (pg));
329 : /* ...and the row is indeed gone, so 'success' would have been a lie. */
330 1 : FAILIF (GNUNET_OK !=
331 : exec_sql ("DO $$ BEGIN"
332 : " IF EXISTS (SELECT FROM kyc_targets"
333 : " WHERE h_normalized_payto"
334 : " =decode(repeat('13',32),'hex'))"
335 : " THEN RAISE EXCEPTION 'row survived a rollback';"
336 : " END IF; END $$;"));
337 :
338 : /* A clean transaction must still commit and still return exactly 0. */
339 1 : FAILIF (GNUNET_OK !=
340 : TALER_EXCHANGEDB_start (pg,
341 : "test-e13-clean"));
342 1 : FAILIF (GNUNET_OK !=
343 : exec_sql ("INSERT INTO kyc_targets (h_normalized_payto)"
344 : " VALUES (decode(repeat('14',32),'hex'));"));
345 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
346 : TALER_EXCHANGEDB_commit (pg));
347 1 : FAILIF (GNUNET_OK !=
348 : exec_sql ("DO $$ BEGIN"
349 : " IF NOT EXISTS (SELECT FROM kyc_targets"
350 : " WHERE h_normalized_payto"
351 : " =decode(repeat('14',32),'hex'))"
352 : " THEN RAISE EXCEPTION 'committed row is missing';"
353 : " END IF; END $$;"));
354 1 : return 0;
355 : }
356 :
357 :
358 : /**
359 : * Helper for the reserve-open checks: create a reserve with the given
360 : * balance and run exchange_do_reserve_open() on it.
361 : *
362 : * @param reserve_pub reserve to create and open
363 : * @param desired_expiration expiration the client asks for
364 : * @param now current time to assume
365 : * @param min_purse_limit number of purses the client asks for
366 : * @param open_fee annual account fee of the exchange
367 : * @param[out] open_cost set to the cost the exchange computed
368 : * @return transaction status
369 : */
370 : static enum GNUNET_DB_QueryStatus
371 3 : try_reserve_open (const struct TALER_ReservePublicKeyP *reserve_pub,
372 : struct GNUNET_TIME_Timestamp desired_expiration,
373 : struct GNUNET_TIME_Timestamp now,
374 : uint32_t min_purse_limit,
375 : const struct TALER_Amount *open_fee,
376 : struct TALER_Amount *open_cost)
377 : {
378 : struct TALER_ReserveSignatureP reserve_sig;
379 : struct TALER_Amount zero;
380 : struct TALER_Amount balance;
381 : struct GNUNET_TIME_Timestamp final_expiration;
382 : bool no_funds;
383 : char *sql;
384 :
385 3 : memset (&reserve_sig,
386 : 0x51,
387 : sizeof (reserve_sig));
388 3 : parse_amount ("0",
389 : &zero);
390 3 : sql = hex_insert_reserve (reserve_pub);
391 3 : GNUNET_assert (GNUNET_OK ==
392 : exec_sql (sql));
393 3 : GNUNET_free (sql);
394 3 : return TALER_EXCHANGEDB_do_reserve_open (pg,
395 : reserve_pub,
396 : &zero,
397 : &zero,
398 : min_purse_limit,
399 : &reserve_sig,
400 : desired_expiration,
401 : now,
402 : open_fee,
403 : &no_funds,
404 : &balance,
405 : open_cost,
406 : &final_expiration);
407 : }
408 :
409 :
410 : /**
411 : * E-3: `{"reserve_expiration":"never"}` is a perfectly well-formed request:
412 : * GNUNET_JSON_spec_timestamp() turns it into GNUNET_TIME_UNIT_FOREVER_ABS and
413 : * qconv_abs_time() clamps that to INT64_MAX. The stored procedure then
414 : * overflowed INT8 computing the number of years, which SQLSTATE 22003 turns
415 : * into a hard error and the handler into an HTTP 500.
416 : */
417 : static int
418 1 : check_reserve_open_never_expires (void)
419 : {
420 : struct TALER_ReservePublicKeyP reserve_pub;
421 : struct TALER_Amount open_fee;
422 : struct TALER_Amount open_cost;
423 :
424 1 : memset (&reserve_pub,
425 : 0x33,
426 : sizeof (reserve_pub));
427 : /* No fractional part here; that is E-4's business. */
428 1 : parse_amount ("1",
429 : &open_fee);
430 : /* Before the fix: 'bigint out of range' -> HARD_ERROR. */
431 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
432 : try_reserve_open (&reserve_pub,
433 : GNUNET_TIME_UNIT_FOREVER_TS,
434 : GNUNET_TIME_timestamp_get (),
435 : 1,
436 : &open_fee,
437 : &open_cost));
438 1 : return 0;
439 : }
440 :
441 :
442 : /**
443 : * E-4: `my_years * in_open_fee.frac` was an INT4 multiplication that
444 : * overflowed *before* the division, so the author's own overflow guard could
445 : * never be reached: an exchange whose ACCOUNT_FEE has a fractional part
446 : * answered a far-future reserve-open request with an HTTP 500.
447 : *
448 : * The same block computed the new purse limit in INT4 from the client's
449 : * `purse_limit`, which overflows for any value near INT32_MAX.
450 : */
451 : static int
452 1 : check_reserve_open_int4_overflows (void)
453 : {
454 : struct TALER_ReservePublicKeyP reserve_pub;
455 : struct TALER_Amount open_fee;
456 : struct TALER_Amount open_cost;
457 : struct TALER_Amount expected;
458 : struct GNUNET_TIME_Timestamp now;
459 :
460 1 : parse_amount ("0.5",
461 : &open_fee);
462 1 : parse_amount ("25",
463 : &expected);
464 :
465 : /* 50 years at EUR:0.50/year: 50 * 50000000 does not fit into an INT4. */
466 1 : memset (&reserve_pub,
467 : 0x34,
468 : sizeof (reserve_pub));
469 1 : now = GNUNET_TIME_timestamp_get ();
470 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
471 : try_reserve_open (&reserve_pub,
472 : GNUNET_TIME_absolute_to_timestamp (
473 : GNUNET_TIME_absolute_add (
474 : now.abs_time,
475 : GNUNET_TIME_relative_multiply (
476 : GNUNET_TIME_UNIT_YEARS,
477 : 50))),
478 : now,
479 : 1,
480 : &open_fee,
481 : &open_cost));
482 1 : FAILIF (0 !=
483 : TALER_amount_cmp (&expected,
484 : &open_cost));
485 :
486 : /* An absurd purse_limit must be priced out of range, not overflow. */
487 1 : memset (&reserve_pub,
488 : 0x35,
489 : sizeof (reserve_pub));
490 1 : now = GNUNET_TIME_timestamp_get ();
491 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
492 : try_reserve_open (&reserve_pub,
493 : now,
494 : now,
495 : 2147483647,
496 : &open_fee,
497 : &open_cost));
498 1 : return 0;
499 : }
500 :
501 :
502 : /**
503 : * E-5: `known_coins.age_commitment_hash` is NULL for every coin without an
504 : * age commitment -- the common case -- but the result spec read it without
505 : * GNUNET_PQ_result_spec_allow_null(), so building the 409 conflict proof for
506 : * POST /purses/$PURSE_PUB/deposit failed with an HTTP 500 instead.
507 : */
508 : static int
509 1 : check_purse_deposit_without_age_commitment (void)
510 : {
511 : struct TALER_PurseContractPublicKeyP purse_pub;
512 : struct TALER_CoinSpendPublicKeyP coin_pub;
513 : struct TALER_Amount amount;
514 : struct TALER_DenominationHashP h_denom_pub;
515 : struct TALER_AgeCommitmentHashP hac;
516 : struct TALER_CoinSpendSignatureP coin_sig;
517 : bool no_age_commitment;
518 1 : char *partner_url = NULL;
519 : char *sql;
520 : char *p_hex;
521 : char *c_hex;
522 : enum GNUNET_GenericReturnValue ok;
523 :
524 1 : memset (&purse_pub,
525 : 0x55,
526 : sizeof (purse_pub));
527 1 : memset (&coin_pub,
528 : 0x56,
529 : sizeof (coin_pub));
530 1 : p_hex = to_hex (&purse_pub,
531 : sizeof (purse_pub));
532 1 : c_hex = to_hex (&coin_pub,
533 : sizeof (coin_pub));
534 1 : GNUNET_asprintf (
535 : &sql,
536 : "INSERT INTO denominations"
537 : " (denom_pub_hash,denom_type,age_mask,denom_pub,master_sig"
538 : " ,valid_from,expire_withdraw,expire_deposit,expire_legal"
539 : " ,coin,fee_withdraw,fee_deposit,fee_refresh,fee_refund)"
540 : " VALUES (decode(repeat('e5',64),'hex'),1,0,decode('00','hex')"
541 : " ,decode(repeat('00',64),'hex'),0,0,0,0"
542 : " ,ROW(1,0)::taler_amount,ROW(0,0)::taler_amount"
543 : " ,ROW(0,0)::taler_amount,ROW(0,0)::taler_amount"
544 : " ,ROW(0,0)::taler_amount);"
545 : /* age_commitment_hash deliberately left NULL */
546 : "INSERT INTO known_coins"
547 : " (denominations_serial,coin_pub,denom_sig,remaining)"
548 : " VALUES ((SELECT denominations_serial FROM denominations"
549 : " WHERE denom_pub_hash=decode(repeat('e5',64),'hex'))"
550 : " ,decode('%s','hex'),decode('00','hex')"
551 : " ,ROW(0,0)::taler_amount);"
552 : "INSERT INTO purse_deposits"
553 : " (purse_pub,coin_pub,amount_with_fee,coin_sig)"
554 : " VALUES (decode('%s','hex'),decode('%s','hex')"
555 : " ,ROW(1,0)::taler_amount,decode(repeat('e5',64),'hex'));",
556 : c_hex,
557 : p_hex,
558 : c_hex);
559 1 : ok = exec_sql (sql);
560 1 : GNUNET_free (sql);
561 1 : GNUNET_free (p_hex);
562 1 : GNUNET_free (c_hex);
563 1 : FAILIF (GNUNET_OK != ok);
564 :
565 : /* Before the fix: HARD_ERROR from the failed NULL extraction. */
566 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
567 : TALER_EXCHANGEDB_get_purse_deposit (pg,
568 : &purse_pub,
569 : &coin_pub,
570 : &amount,
571 : &h_denom_pub,
572 : &hac,
573 : &no_age_commitment,
574 : &coin_sig,
575 : &partner_url));
576 1 : GNUNET_free (partner_url);
577 1 : FAILIF (! no_age_commitment);
578 1 : return 0;
579 : }
580 :
581 :
582 : /**
583 : * E-6: a reserve funded by a purse merge has no `reserves_in` row, so the
584 : * LEFT JOIN in get_reserve_close_info() returns a NULL payto_uri. Without
585 : * allow_null the extraction failed and POST /reserves/$RP/close answered 500
586 : * instead of the 409 the handler already implements.
587 : */
588 : static int
589 1 : check_reserve_close_info_without_origin (void)
590 : {
591 : struct TALER_ReservePublicKeyP reserve_pub;
592 : struct TALER_Amount balance;
593 : struct TALER_FullPayto payto_uri;
594 : char *sql;
595 : enum GNUNET_GenericReturnValue ok;
596 :
597 1 : memset (&reserve_pub,
598 : 0x36,
599 : sizeof (reserve_pub));
600 1 : sql = hex_insert_reserve (&reserve_pub);
601 1 : ok = exec_sql (sql);
602 1 : GNUNET_free (sql);
603 1 : FAILIF (GNUNET_OK != ok);
604 :
605 : /* Before the fix: HARD_ERROR from the failed NULL extraction. */
606 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
607 : TALER_EXCHANGEDB_get_reserve_close_info (pg,
608 : &reserve_pub,
609 : &balance,
610 : &payto_uri));
611 1 : FAILIF (NULL != payto_uri.full_payto);
612 1 : return 0;
613 : }
614 :
615 :
616 : /**
617 : * Read back the bookkeeping of one work shard.
618 : *
619 : * @param job_name job the shard belongs to
620 : * @param start_row inclusive start row of the shard
621 : * @param end_row exclusive end row of the shard
622 : * @param[out] progress_row how far the shard has come
623 : * @param[out] completed whether the shard is done
624 : * @return transaction status code
625 : */
626 : static enum GNUNET_DB_QueryStatus
627 4 : get_shard_state (const char *job_name,
628 : uint64_t start_row,
629 : uint64_t end_row,
630 : uint64_t *progress_row,
631 : bool *completed)
632 : {
633 4 : struct GNUNET_PQ_QueryParam params[] = {
634 4 : GNUNET_PQ_query_param_string (job_name),
635 4 : GNUNET_PQ_query_param_uint64 (&start_row),
636 4 : GNUNET_PQ_query_param_uint64 (&end_row),
637 : GNUNET_PQ_query_param_end
638 : };
639 4 : struct GNUNET_PQ_ResultSpec rs[] = {
640 4 : GNUNET_PQ_result_spec_uint64 ("progress_row",
641 : progress_row),
642 4 : GNUNET_PQ_result_spec_bool ("completed",
643 : completed),
644 : GNUNET_PQ_result_spec_end
645 : };
646 :
647 4 : PREPARE (pg,
648 : "test_get_shard_state",
649 : "SELECT"
650 : " progress_row"
651 : ",completed"
652 : " FROM work_shards"
653 : " WHERE job_name=$1"
654 : " AND start_row=$2"
655 : " AND end_row=$3;");
656 4 : return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
657 : "test_get_shard_state",
658 : params,
659 : rs);
660 : }
661 :
662 :
663 : /**
664 : * A worker that gets part of the way through a shard and then stops used to
665 : * leave nothing behind: the shard was either untouched or completed, so the
666 : * next worker to pick it up redid all of it, and nothing it had imported was
667 : * visible until the whole shard was done. Check that the progress marker is
668 : * kept, that it survives releasing the shard, that it never moves backwards,
669 : * and that reaching the end of the shard is what completes it.
670 : */
671 : static int
672 1 : check_shard_progress_survives_abort (void)
673 : {
674 1 : const char *job = "test-shard-progress";
675 : uint64_t start;
676 : uint64_t end;
677 : uint64_t progress;
678 : uint64_t start2;
679 : uint64_t end2;
680 : uint64_t progress2;
681 : bool completed;
682 :
683 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
684 : TALER_EXCHANGEDB_begin_shard (pg,
685 : job,
686 : GNUNET_TIME_UNIT_HOURS,
687 : 1024,
688 : &start,
689 : &end,
690 : &progress));
691 : /* A fresh shard has nothing done yet. */
692 1 : FAILIF (progress != start);
693 :
694 : /* Get half way, then let go of the shard. */
695 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
696 : TALER_EXCHANGEDB_update_shard_progress (pg,
697 : job,
698 : start,
699 : end,
700 : start + 512,
701 : GNUNET_TIME_UNIT_HOURS));
702 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
703 : TALER_EXCHANGEDB_abort_shard (pg,
704 : job,
705 : start,
706 : end));
707 :
708 : /* The next worker gets the same shard back, but resumes in the middle. */
709 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
710 : TALER_EXCHANGEDB_begin_shard (pg,
711 : job,
712 : GNUNET_TIME_UNIT_HOURS,
713 : 1024,
714 : &start2,
715 : &end2,
716 : &progress2));
717 1 : FAILIF (start2 != start);
718 1 : FAILIF (end2 != end);
719 1 : FAILIF (progress2 != start + 512);
720 :
721 : /* A straggler reporting older progress must not rewind the shard. */
722 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
723 : TALER_EXCHANGEDB_update_shard_progress (pg,
724 : job,
725 : start,
726 : end,
727 : start + 1,
728 : GNUNET_TIME_UNIT_HOURS));
729 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
730 : get_shard_state (job,
731 : start,
732 : end,
733 : &progress,
734 : &completed));
735 1 : FAILIF (progress != start + 512);
736 1 : FAILIF (completed);
737 :
738 : /* Reaching the end completes the shard; there is no second statement for
739 : the caller to forget, or to crash before. */
740 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
741 : TALER_EXCHANGEDB_update_shard_progress (pg,
742 : job,
743 : start,
744 : end,
745 : end,
746 : GNUNET_TIME_UNIT_HOURS));
747 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
748 : get_shard_state (job,
749 : start,
750 : end,
751 : &progress,
752 : &completed));
753 1 : FAILIF (progress != end);
754 1 : FAILIF (! completed);
755 1 : return 0;
756 : }
757 :
758 :
759 : /**
760 : * Importing a batch of incoming wire transfers and recording how far the
761 : * shard has come is one statement, so a crash cannot land one without the
762 : * other. Check that a batch moves both, that importing it again is harmless
763 : * and reported as a duplicate, and that the shard completes as part of the
764 : * import rather than in a transaction of its own.
765 : */
766 : static int
767 1 : check_import_credits_advances_shard (void)
768 : {
769 1 : const char *job = "test-import-credits";
770 : struct TALER_ReservePublicKeyP reserve_pub;
771 : struct TALER_Amount balance;
772 : struct TALER_EXCHANGEDB_ReserveInInfo reserve;
773 : struct TALER_EXCHANGEDB_CreditBatch batch;
774 : enum GNUNET_DB_QueryStatus results[1];
775 : uint64_t start;
776 : uint64_t end;
777 : uint64_t progress;
778 : bool completed;
779 :
780 1 : memset (&reserve_pub,
781 : 0x51,
782 : sizeof (reserve_pub));
783 1 : parse_amount ("4.00",
784 : &balance);
785 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
786 : TALER_EXCHANGEDB_begin_shard (pg,
787 : job,
788 : GNUNET_TIME_UNIT_HOURS,
789 : 1024,
790 : &start,
791 : &end,
792 : &progress));
793 1 : reserve.reserve_pub = &reserve_pub;
794 1 : reserve.balance = &balance;
795 1 : reserve.execution_time = GNUNET_TIME_timestamp_get ();
796 : reserve.sender_account_details.full_payto
797 1 : = (char *) "payto://x-taler-bank/localhost/shard-test?receiver-name=Shard";
798 1 : reserve.wire_reference = start + 1;
799 1 : memset (&batch,
800 : 0,
801 : sizeof (batch));
802 1 : batch.exchange_account_name = "exchange-account-test";
803 1 : batch.reserves = &reserve;
804 1 : batch.reserves_length = 1;
805 1 : batch.job_name = job;
806 1 : batch.shard_start = start;
807 1 : batch.shard_end = end;
808 1 : batch.progress_row = start + 1;
809 1 : batch.lease = GNUNET_TIME_UNIT_HOURS;
810 :
811 1 : FAILIF (0 >
812 : TALER_EXCHANGEDB_do_import_credits (pg,
813 : &batch,
814 : results));
815 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != results[0]);
816 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
817 : get_shard_state (job,
818 : start,
819 : end,
820 : &progress,
821 : &completed));
822 : /* The transfer is committed and so is the fact that we consumed its row --
823 : long before the rest of the shard has been looked at. */
824 1 : FAILIF (progress != start + 1);
825 1 : FAILIF (completed);
826 :
827 : /* Re-importing the same batch is what happens whenever a shard is picked up
828 : twice. It has to be harmless. */
829 1 : batch.progress_row = end;
830 1 : FAILIF (0 >
831 : TALER_EXCHANGEDB_do_import_credits (pg,
832 : &batch,
833 : results));
834 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != results[0]);
835 1 : FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
836 : get_shard_state (job,
837 : start,
838 : end,
839 : &progress,
840 : &completed));
841 1 : FAILIF (progress != end);
842 1 : FAILIF (! completed);
843 1 : return 0;
844 : }
845 :
846 :
847 : /**
848 : * Statistics must return cumulative totals for ranges without their own
849 : * events, and age those events using the exchange process's clock.
850 : */
851 : static int
852 1 : check_statistics_time_travel (void)
853 : {
854 1 : struct TALER_EXCHANGEDB_PostgresContext *original_pg = pg;
855 1 : long long original_offset = GNUNET_TIME_get_offset ();
856 1 : const long long future = 60LL * 24 * 60 * 60 * 1000000;
857 1 : int ret = 1;
858 :
859 : /* The ordinary connection starts without time travel. */
860 1 : FAILIF (GNUNET_OK != exec_sql (
861 : "SET TIME ZONE 'Europe/Berlin';"
862 : "DO $$ BEGIN "
863 : " ASSERT exchange_now() = (CURRENT_TIMESTAMP(0) AT TIME ZONE 'UTC');"
864 : "END $$;"
865 : "INSERT INTO exchange_statistic_interval_meta"
866 : " (origin,slug,description,stype,ranges,precisions) VALUES"
867 : " ('test','clock-amount','test','amount',"
868 : " ARRAY[604800,2419200,31449600], ARRAY[86400,86400,86400]),"
869 : " ('test','clock-number','test','number',"
870 : " ARRAY[604800,2419200,31449600], ARRAY[86400,86400,86400]);"
871 : "CALL exchange_do_bump_amount_stat('clock-amount',"
872 : " decode(repeat('ab',32),'hex'),exchange_now(),(1,25000000)::taler_amount);"
873 : "CALL exchange_do_bump_number_stat('clock-number',"
874 : " decode(repeat('ab',32),'hex'),exchange_now(),1);"
875 : "DO $$ BEGIN "
876 : " ASSERT (SELECT count(*) = 3 AND bool_and(rvalue = (1,25000000)::taler_amount)"
877 : " FROM exchange_statistic_interval_amount_get('clock-amount',decode(repeat('ab',32),'hex')));"
878 : " ASSERT (SELECT count(*) = 3 AND bool_and(rvalue = 1)"
879 : " FROM exchange_statistic_interval_number_get('clock-number',decode(repeat('ab',32),'hex')));"
880 : "END $$;"));
881 :
882 1 : GNUNET_TIME_set_offset (future);
883 1 : pg = TALER_EXCHANGEDB_connect_admin (original_pg->cfg);
884 1 : if (NULL == pg)
885 0 : goto cleanup;
886 1 : if (GNUNET_OK != exec_sql (
887 : "DO $$ BEGIN "
888 : " ASSERT exchange_now() = (CURRENT_TIMESTAMP(0) AT TIME ZONE 'UTC') + INTERVAL '60 days';"
889 : "END $$;"
890 : "CALL exchange_do_bump_amount_stat('clock-amount',"
891 : " decode(repeat('ab',32),'hex'),exchange_now(),(0,75000000)::taler_amount);"
892 : "CALL exchange_do_bump_number_stat('clock-number',"
893 : " decode(repeat('ab',32),'hex'),exchange_now(),1);"
894 : "DO $$ BEGIN "
895 : " ASSERT (SELECT count(*) = 3 AND bool_and(rvalue = CASE WHEN range < 31449600"
896 : " THEN (0,75000000)::taler_amount ELSE (2,0)::taler_amount END)"
897 : " FROM exchange_statistic_interval_amount_get('clock-amount',decode(repeat('ab',32),'hex')));"
898 : " ASSERT (SELECT count(*) = 3 AND bool_and(rvalue = CASE WHEN range < 31449600 THEN 1 ELSE 2 END)"
899 : " FROM exchange_statistic_interval_number_get('clock-number',decode(repeat('ab',32),'hex')));"
900 : "END $$;"))
901 0 : goto cleanup;
902 :
903 : /* Deliberately break this connection. The reconnect callback must restore
904 : the offset as well as the ordinary database session settings. */
905 1 : fprintf (stderr,
906 : "Deliberately terminating statistics test connection to test reconnect\n");
907 1 : if (GNUNET_SYSERR != exec_sql ("SELECT pg_terminate_backend(pg_backend_pid())"))
908 0 : goto cleanup;
909 1 : GNUNET_PQ_reconnect_if_down (pg->conn);
910 1 : if (GNUNET_OK != exec_sql (
911 : "DO $$ BEGIN "
912 : " ASSERT exchange_now() = (CURRENT_TIMESTAMP(0) AT TIME ZONE 'UTC') + INTERVAL '60 days';"
913 : "END $$;"
914 : /* A plain SQL client with no setting falls back to ordinary UTC time. */
915 : "SET TIME ZONE 'America/New_York';"
916 : "RESET taler.timetravel_us;"
917 : "DO $$ BEGIN "
918 : " ASSERT exchange_now() = (CURRENT_TIMESTAMP(0) AT TIME ZONE 'UTC');"
919 : "END $$;"))
920 0 : goto cleanup;
921 1 : ret = 0;
922 1 : cleanup:
923 1 : if (NULL != pg)
924 1 : TALER_EXCHANGEDB_disconnect (pg);
925 1 : pg = original_pg;
926 1 : GNUNET_TIME_set_offset (original_offset);
927 1 : return ret;
928 : }
929 :
930 :
931 : /**
932 : * All checks we know about.
933 : */
934 : static const struct
935 : {
936 : const char *name;
937 : int (*fn)(void);
938 : } tests[] = {
939 : { "statistics-time-travel",
940 : &check_statistics_time_travel },
941 : { "aggregate-refund-below-deposit-fee",
942 : &check_aggregate_refund_below_deposit_fee },
943 : { "commit-detects-rolled-back-transaction",
944 : &check_commit_detects_rolled_back_transaction },
945 : { "reserve-open-never-expires",
946 : &check_reserve_open_never_expires },
947 : { "reserve-open-int4-overflows",
948 : &check_reserve_open_int4_overflows },
949 : { "purse-deposit-without-age-commitment",
950 : &check_purse_deposit_without_age_commitment },
951 : { "reserve-close-info-without-origin",
952 : &check_reserve_close_info_without_origin },
953 : { "shard-progress-survives-abort",
954 : &check_shard_progress_survives_abort },
955 : { "import-credits-advances-shard",
956 : &check_import_credits_advances_shard },
957 : { NULL, NULL }
958 : };
959 :
960 :
961 : /**
962 : * Main function that runs the checks.
963 : *
964 : * @param cls closure
965 : * @param args remaining command-line arguments
966 : * @param cfgfile name of the configuration file used
967 : * @param cfg configuration
968 : */
969 : static void
970 1 : run (void *cls,
971 : char *const *args,
972 : const char *cfgfile,
973 : const struct GNUNET_CONFIGURATION_Handle *cfg)
974 : {
975 1 : unsigned int ran = 0;
976 :
977 : (void) cls;
978 : (void) args;
979 : (void) cfgfile;
980 1 : pg = TALER_EXCHANGEDB_connect_admin (cfg);
981 1 : if (NULL == pg)
982 : {
983 0 : fprintf (stderr,
984 : "Failed to connect to the database\n");
985 0 : result = 77;
986 0 : return;
987 : }
988 1 : if (GNUNET_OK !=
989 1 : TALER_EXCHANGEDB_create_tables (pg,
990 : false,
991 : 0))
992 : {
993 0 : fprintf (stderr,
994 : "Failed to create the database schema\n");
995 0 : result = 77;
996 0 : goto cleanup;
997 : }
998 10 : for (unsigned int i = 0; NULL != tests[i].name; i++)
999 : {
1000 9 : if ( (NULL != only) &&
1001 0 : (0 != strcmp (only,
1002 0 : tests[i].name)) )
1003 0 : continue;
1004 9 : fprintf (stderr,
1005 : "Running check `%s'\n",
1006 9 : tests[i].name);
1007 9 : ran++;
1008 9 : if (0 != tests[i].fn ())
1009 : {
1010 0 : fprintf (stderr,
1011 : "Check `%s' FAILED\n",
1012 0 : tests[i].name);
1013 0 : result = 1;
1014 : }
1015 : }
1016 1 : if (0 == ran)
1017 : {
1018 0 : fprintf (stderr,
1019 : "No check matched `%s'\n",
1020 : only);
1021 0 : result = 1;
1022 : }
1023 1 : cleanup:
1024 1 : TALER_EXCHANGEDB_disconnect (pg);
1025 1 : pg = NULL;
1026 : }
1027 :
1028 :
1029 : int
1030 1 : main (int argc,
1031 : char *const *argv)
1032 : {
1033 1 : struct GNUNET_GETOPT_CommandLineOption options[] = {
1034 1 : GNUNET_GETOPT_option_string ('t',
1035 : "test",
1036 : "NAME",
1037 : "only run the check called NAME",
1038 : &only),
1039 : GNUNET_GETOPT_OPTION_END
1040 : };
1041 : enum GNUNET_GenericReturnValue ret;
1042 :
1043 1 : result = 0;
1044 1 : ret = GNUNET_PROGRAM_run (TALER_EXCHANGE_project_data (),
1045 : argc,
1046 : argv,
1047 : "test-regressions",
1048 : "Regression tests for the exchange database layer",
1049 : options,
1050 : &run,
1051 : NULL);
1052 1 : if (GNUNET_SYSERR == ret)
1053 0 : return 3;
1054 1 : if (GNUNET_NO == ret)
1055 0 : return 0;
1056 1 : return result;
1057 : }
1058 :
1059 :
1060 : /* end of test_regressions.c */
|