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_deposits.c
18 : * @brief tests for the exchangedb functions whose primary table is
19 : * `reserves_open_deposits`
20 : * @author Christian Grothoff
21 : *
22 : * Covers #TALER_EXCHANGEDB_insert_reserve_open_deposit(), the only
23 : * exported function that touches the table. The rows are read back by the
24 : * coin history, which test_coin_history.c covers.
25 : *
26 : * The table's primary key is (coin_pub, coin_sig), which is what makes a
27 : * replayed request idempotent. On insufficient coin balance the row is
28 : * inserted first and the balance check fails afterwards, so the caller has
29 : * to roll back -- the check below does exactly that, since that is how the
30 : * /reserves/$RP/open handler uses the function.
31 : */
32 : #include "test_common.h"
33 : #include "exchange-database/commit.h"
34 : #include "exchange-database/get_known_coin.h"
35 : #include "exchange-database/insert_reserve_open_deposit.h"
36 : #include "exchange-database/rollback.h"
37 : #include "exchange-database/start.h"
38 :
39 :
40 : /**
41 : * Read the remaining balance of a coin.
42 : *
43 : * @param pg the database context
44 : * @param coin_pub coin to look up
45 : * @param[out] remaining set to the coin's remaining value
46 : */
47 : static void
48 4 : coin_remaining (struct TALER_EXCHANGEDB_PostgresContext *pg,
49 : const struct TALER_CoinSpendPublicKeyP *coin_pub,
50 : struct TALER_Amount *remaining)
51 : {
52 4 : struct GNUNET_PQ_QueryParam params[] = {
53 4 : GNUNET_PQ_query_param_auto_from_type (coin_pub),
54 : GNUNET_PQ_query_param_end
55 : };
56 4 : struct GNUNET_PQ_ResultSpec rs[] = {
57 4 : TALER_PQ_result_spec_amount ("remaining",
58 4 : pg->currency,
59 : remaining),
60 : GNUNET_PQ_result_spec_end
61 : };
62 :
63 4 : GNUNET_assert (GNUNET_OK ==
64 : GNUNET_PQ_prepare_anon (pg->conn,
65 : "SELECT remaining"
66 : " FROM known_coins"
67 : " WHERE coin_pub=$1;"));
68 4 : GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
69 : GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
70 : "",
71 : params,
72 : rs));
73 4 : }
74 :
75 :
76 : /**
77 : * Spend part of a coin on keeping a reserve open.
78 : *
79 : * @param pg the database context
80 : * @param coin the coin to spend from
81 : * @param known_coin_id row of @a coin
82 : * @param reserve_pub reserve being kept open
83 : * @param seed seed for the coin and reserve signatures
84 : * @param amount how much of the coin to spend, e.g. "1"
85 : * @param[out] insufficient_funds set if the coin does not have that much
86 : * @return transaction status
87 : */
88 : static enum GNUNET_DB_QueryStatus
89 5 : open_deposit (struct TALER_EXCHANGEDB_PostgresContext *pg,
90 : const struct TALER_CoinPublicInfo *coin,
91 : uint64_t known_coin_id,
92 : const struct TALER_ReservePublicKeyP *reserve_pub,
93 : uint32_t seed,
94 : const char *amount,
95 : bool *insufficient_funds)
96 : {
97 : struct TALER_CoinSpendSignatureP coin_sig;
98 : struct TALER_ReserveSignatureP reserve_sig;
99 5 : struct TALER_Amount a = TDB_amount (amount);
100 :
101 5 : TDB_fill (&coin_sig,
102 : sizeof (coin_sig),
103 : seed);
104 5 : TDB_fill (&reserve_sig,
105 : sizeof (reserve_sig),
106 : seed);
107 5 : return TALER_EXCHANGEDB_insert_reserve_open_deposit (pg,
108 : coin,
109 : &coin_sig,
110 : known_coin_id,
111 : &a,
112 : &reserve_sig,
113 : reserve_pub,
114 : insufficient_funds);
115 : }
116 :
117 :
118 : /**
119 : * Spending part of a coin records the deposit and debits the coin.
120 : *
121 : * @param pg the database context
122 : * @return 0 on success
123 : */
124 : static int
125 1 : check_deposit (struct TALER_EXCHANGEDB_PostgresContext *pg)
126 : {
127 : struct TDB_Denom denom;
128 : struct TALER_CoinPublicInfo coin;
129 : struct TALER_ReservePublicKeyP reserve_pub;
130 : struct TALER_Amount remaining;
131 1 : struct TALER_Amount expect = TDB_amount ("4");
132 : uint64_t known_coin_id;
133 1 : bool insufficient = true;
134 :
135 1 : FAILIF (0 != TDB_count (pg,
136 : "FROM reserves_open_deposits"));
137 1 : TDB_denom (pg,
138 : 10,
139 : "5",
140 : "0.1",
141 : &denom);
142 1 : TDB_coin (pg,
143 : &denom,
144 : 10,
145 : &coin,
146 : &known_coin_id);
147 1 : TDB_reserve (pg,
148 : 10,
149 : "0",
150 : &reserve_pub);
151 :
152 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
153 : open_deposit (pg,
154 : &coin,
155 : known_coin_id,
156 : &reserve_pub,
157 : 10,
158 : "1",
159 : &insufficient),
160 : TDB_coin_free (&coin); TDB_denom_free (&denom));
161 1 : FAILIF_C (insufficient,
162 : TDB_coin_free (&coin); TDB_denom_free (&denom));
163 1 : FAILIF_C (1 != TDB_count (pg,
164 : "FROM reserves_open_deposits"),
165 : TDB_coin_free (&coin); TDB_denom_free (&denom));
166 1 : coin_remaining (pg,
167 : &coin.coin_pub,
168 : &remaining);
169 1 : FAILIF_C (0 != TALER_amount_cmp (&remaining,
170 : &expect),
171 : TDB_coin_free (&coin); TDB_denom_free (&denom));
172 :
173 : /* a replay of the very same request is absorbed and does not debit the
174 : coin a second time */
175 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
176 : open_deposit (pg,
177 : &coin,
178 : known_coin_id,
179 : &reserve_pub,
180 : 10,
181 : "1",
182 : &insufficient),
183 : TDB_coin_free (&coin); TDB_denom_free (&denom));
184 1 : FAILIF_C (insufficient,
185 : TDB_coin_free (&coin); TDB_denom_free (&denom));
186 1 : FAILIF_C (1 != TDB_count (pg,
187 : "FROM reserves_open_deposits"),
188 : TDB_coin_free (&coin); TDB_denom_free (&denom));
189 1 : coin_remaining (pg,
190 : &coin.coin_pub,
191 : &remaining);
192 1 : FAILIF_C (0 != TALER_amount_cmp (&remaining,
193 : &expect),
194 : TDB_coin_free (&coin); TDB_denom_free (&denom));
195 1 : TDB_coin_free (&coin);
196 1 : TDB_denom_free (&denom);
197 1 : return 0;
198 : }
199 :
200 :
201 : /**
202 : * A second, larger deposit from the same coin is refused, and the caller
203 : * has to roll back to undo the row the function inserted first.
204 : *
205 : * @param pg the database context
206 : * @return 0 on success
207 : */
208 : static int
209 1 : check_insufficient_funds (struct TALER_EXCHANGEDB_PostgresContext *pg)
210 : {
211 : struct TDB_Denom denom;
212 : struct TALER_CoinPublicInfo coin;
213 : struct TALER_ReservePublicKeyP reserve_pub;
214 : struct TALER_Amount remaining;
215 1 : struct TALER_Amount expect = TDB_amount ("4");
216 : uint64_t known_coin_id;
217 1 : bool insufficient = false;
218 :
219 1 : TDB_denom (pg,
220 : 10,
221 : "5",
222 : "0.1",
223 : &denom);
224 1 : TDB_coin (pg,
225 : &denom,
226 : 10,
227 : &coin,
228 : &known_coin_id);
229 1 : TDB_reserve (pg,
230 : 10,
231 : "0",
232 : &reserve_pub);
233 :
234 1 : FAILIF_C (GNUNET_OK !=
235 : TALER_EXCHANGEDB_start (pg,
236 : "test-open-deposit-insufficient"),
237 : TDB_coin_free (&coin); TDB_denom_free (&denom));
238 : /* EUR:4 is left, so EUR:5 cannot be taken */
239 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
240 : open_deposit (pg,
241 : &coin,
242 : known_coin_id,
243 : &reserve_pub,
244 : 11,
245 : "5",
246 : &insufficient),
247 : TALER_EXCHANGEDB_rollback (pg);
248 : TDB_coin_free (&coin); TDB_denom_free (&denom));
249 1 : FAILIF_C (! insufficient,
250 : TALER_EXCHANGEDB_rollback (pg);
251 : TDB_coin_free (&coin); TDB_denom_free (&denom));
252 1 : TALER_EXCHANGEDB_rollback (pg);
253 : /* the rollback took the row with it */
254 1 : FAILIF_C (1 != TDB_count (pg,
255 : "FROM reserves_open_deposits"),
256 : TDB_coin_free (&coin); TDB_denom_free (&denom));
257 1 : coin_remaining (pg,
258 : &coin.coin_pub,
259 : &remaining);
260 1 : FAILIF_C (0 != TALER_amount_cmp (&remaining,
261 : &expect),
262 : TDB_coin_free (&coin); TDB_denom_free (&denom));
263 1 : TDB_coin_free (&coin);
264 1 : TDB_denom_free (&denom);
265 1 : return 0;
266 : }
267 :
268 :
269 : /**
270 : * Spending exactly what is left works and leaves the coin at zero.
271 : *
272 : * @param pg the database context
273 : * @return 0 on success
274 : */
275 : static int
276 1 : check_exact_balance (struct TALER_EXCHANGEDB_PostgresContext *pg)
277 : {
278 : struct TDB_Denom denom;
279 : struct TALER_CoinPublicInfo coin;
280 : struct TALER_ReservePublicKeyP reserve_pub;
281 : struct TALER_Amount remaining;
282 1 : struct TALER_Amount zero = TDB_amount ("0");
283 : uint64_t known_coin_id;
284 1 : bool insufficient = true;
285 :
286 1 : TDB_denom (pg,
287 : 10,
288 : "5",
289 : "0.1",
290 : &denom);
291 1 : TDB_coin (pg,
292 : &denom,
293 : 10,
294 : &coin,
295 : &known_coin_id);
296 1 : TDB_reserve (pg,
297 : 10,
298 : "0",
299 : &reserve_pub);
300 1 : FAILIF_C (GNUNET_OK !=
301 : TALER_EXCHANGEDB_start (pg,
302 : "test-open-deposit-exact"),
303 : TDB_coin_free (&coin); TDB_denom_free (&denom));
304 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
305 : open_deposit (pg,
306 : &coin,
307 : known_coin_id,
308 : &reserve_pub,
309 : 12,
310 : "4",
311 : &insufficient),
312 : TALER_EXCHANGEDB_rollback (pg);
313 : TDB_coin_free (&coin); TDB_denom_free (&denom));
314 1 : FAILIF_C (insufficient,
315 : TALER_EXCHANGEDB_rollback (pg);
316 : TDB_coin_free (&coin); TDB_denom_free (&denom));
317 1 : FAILIF_C (0 >
318 : TALER_EXCHANGEDB_commit (pg),
319 : TDB_coin_free (&coin); TDB_denom_free (&denom));
320 1 : coin_remaining (pg,
321 : &coin.coin_pub,
322 : &remaining);
323 1 : FAILIF_C (0 != TALER_amount_cmp (&remaining,
324 : &zero),
325 : TDB_coin_free (&coin); TDB_denom_free (&denom));
326 1 : FAILIF_C (2 != TDB_count (pg,
327 : "FROM reserves_open_deposits"),
328 : TDB_coin_free (&coin); TDB_denom_free (&denom));
329 : /* and now even the smallest further deposit is refused */
330 1 : insufficient = false;
331 1 : FAILIF_C (GNUNET_OK !=
332 : TALER_EXCHANGEDB_start (pg,
333 : "test-open-deposit-drained"),
334 : TDB_coin_free (&coin); TDB_denom_free (&denom));
335 1 : FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
336 : open_deposit (pg,
337 : &coin,
338 : known_coin_id,
339 : &reserve_pub,
340 : 13,
341 : "0.01",
342 : &insufficient),
343 : TALER_EXCHANGEDB_rollback (pg);
344 : TDB_coin_free (&coin); TDB_denom_free (&denom));
345 1 : FAILIF_C (! insufficient,
346 : TALER_EXCHANGEDB_rollback (pg);
347 : TDB_coin_free (&coin); TDB_denom_free (&denom));
348 1 : TALER_EXCHANGEDB_rollback (pg);
349 1 : TDB_coin_free (&coin);
350 1 : TDB_denom_free (&denom);
351 1 : return 0;
352 : }
353 :
354 :
355 : /**
356 : * The checks to run, in order.
357 : */
358 : static const struct TDB_Test tests[] = {
359 : { "reserves-open-deposits-deposit",
360 : &check_deposit },
361 : { "reserves-open-deposits-insufficient-funds",
362 : &check_insufficient_funds },
363 : { "reserves-open-deposits-exact-balance",
364 : &check_exact_balance },
365 : { NULL, NULL }
366 : };
367 :
368 :
369 : int
370 1 : main (int argc,
371 : char *const *argv)
372 : {
373 1 : return TDB_main (argc,
374 : argv,
375 : "test-reserves-open-deposits",
376 : "Tests for the exchangedb `reserves_open_deposits' table",
377 : tests);
378 : }
379 :
380 :
381 : /* end of test_reserves_open_deposits.c */
|