test_reserves_open_deposits.c (12917B)
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 coin_remaining (struct TALER_EXCHANGEDB_PostgresContext *pg, 49 const struct TALER_CoinSpendPublicKeyP *coin_pub, 50 struct TALER_Amount *remaining) 51 { 52 struct GNUNET_PQ_QueryParam params[] = { 53 GNUNET_PQ_query_param_auto_from_type (coin_pub), 54 GNUNET_PQ_query_param_end 55 }; 56 struct GNUNET_PQ_ResultSpec rs[] = { 57 TALER_PQ_result_spec_amount ("remaining", 58 pg->currency, 59 remaining), 60 GNUNET_PQ_result_spec_end 61 }; 62 63 GNUNET_assert (GNUNET_OK == 64 GNUNET_PQ_prepare_anon (pg->conn, 65 "SELECT remaining" 66 " FROM known_coins" 67 " WHERE coin_pub=$1;")); 68 GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == 69 GNUNET_PQ_eval_prepared_singleton_select (pg->conn, 70 "", 71 params, 72 rs)); 73 } 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 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 struct TALER_Amount a = TDB_amount (amount); 100 101 TDB_fill (&coin_sig, 102 sizeof (coin_sig), 103 seed); 104 TDB_fill (&reserve_sig, 105 sizeof (reserve_sig), 106 seed); 107 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 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 struct TALER_Amount expect = TDB_amount ("4"); 132 uint64_t known_coin_id; 133 bool insufficient = true; 134 135 FAILIF (0 != TDB_count (pg, 136 "FROM reserves_open_deposits")); 137 TDB_denom (pg, 138 10, 139 "5", 140 "0.1", 141 &denom); 142 TDB_coin (pg, 143 &denom, 144 10, 145 &coin, 146 &known_coin_id); 147 TDB_reserve (pg, 148 10, 149 "0", 150 &reserve_pub); 151 152 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 FAILIF_C (insufficient, 162 TDB_coin_free (&coin); TDB_denom_free (&denom)); 163 FAILIF_C (1 != TDB_count (pg, 164 "FROM reserves_open_deposits"), 165 TDB_coin_free (&coin); TDB_denom_free (&denom)); 166 coin_remaining (pg, 167 &coin.coin_pub, 168 &remaining); 169 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 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 FAILIF_C (insufficient, 185 TDB_coin_free (&coin); TDB_denom_free (&denom)); 186 FAILIF_C (1 != TDB_count (pg, 187 "FROM reserves_open_deposits"), 188 TDB_coin_free (&coin); TDB_denom_free (&denom)); 189 coin_remaining (pg, 190 &coin.coin_pub, 191 &remaining); 192 FAILIF_C (0 != TALER_amount_cmp (&remaining, 193 &expect), 194 TDB_coin_free (&coin); TDB_denom_free (&denom)); 195 TDB_coin_free (&coin); 196 TDB_denom_free (&denom); 197 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 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 struct TALER_Amount expect = TDB_amount ("4"); 216 uint64_t known_coin_id; 217 bool insufficient = false; 218 219 TDB_denom (pg, 220 10, 221 "5", 222 "0.1", 223 &denom); 224 TDB_coin (pg, 225 &denom, 226 10, 227 &coin, 228 &known_coin_id); 229 TDB_reserve (pg, 230 10, 231 "0", 232 &reserve_pub); 233 234 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 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 FAILIF_C (! insufficient, 250 TALER_EXCHANGEDB_rollback (pg); 251 TDB_coin_free (&coin); TDB_denom_free (&denom)); 252 TALER_EXCHANGEDB_rollback (pg); 253 /* the rollback took the row with it */ 254 FAILIF_C (1 != TDB_count (pg, 255 "FROM reserves_open_deposits"), 256 TDB_coin_free (&coin); TDB_denom_free (&denom)); 257 coin_remaining (pg, 258 &coin.coin_pub, 259 &remaining); 260 FAILIF_C (0 != TALER_amount_cmp (&remaining, 261 &expect), 262 TDB_coin_free (&coin); TDB_denom_free (&denom)); 263 TDB_coin_free (&coin); 264 TDB_denom_free (&denom); 265 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 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 struct TALER_Amount zero = TDB_amount ("0"); 283 uint64_t known_coin_id; 284 bool insufficient = true; 285 286 TDB_denom (pg, 287 10, 288 "5", 289 "0.1", 290 &denom); 291 TDB_coin (pg, 292 &denom, 293 10, 294 &coin, 295 &known_coin_id); 296 TDB_reserve (pg, 297 10, 298 "0", 299 &reserve_pub); 300 FAILIF_C (GNUNET_OK != 301 TALER_EXCHANGEDB_start (pg, 302 "test-open-deposit-exact"), 303 TDB_coin_free (&coin); TDB_denom_free (&denom)); 304 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 FAILIF_C (insufficient, 315 TALER_EXCHANGEDB_rollback (pg); 316 TDB_coin_free (&coin); TDB_denom_free (&denom)); 317 FAILIF_C (0 > 318 TALER_EXCHANGEDB_commit (pg), 319 TDB_coin_free (&coin); TDB_denom_free (&denom)); 320 coin_remaining (pg, 321 &coin.coin_pub, 322 &remaining); 323 FAILIF_C (0 != TALER_amount_cmp (&remaining, 324 &zero), 325 TDB_coin_free (&coin); TDB_denom_free (&denom)); 326 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 insufficient = false; 331 FAILIF_C (GNUNET_OK != 332 TALER_EXCHANGEDB_start (pg, 333 "test-open-deposit-drained"), 334 TDB_coin_free (&coin); TDB_denom_free (&denom)); 335 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 FAILIF_C (! insufficient, 346 TALER_EXCHANGEDB_rollback (pg); 347 TDB_coin_free (&coin); TDB_denom_free (&denom)); 348 TALER_EXCHANGEDB_rollback (pg); 349 TDB_coin_free (&coin); 350 TDB_denom_free (&denom); 351 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 main (int argc, 371 char *const *argv) 372 { 373 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 */