commit ad2405b87647754aed6a26532ca457fcaea0ed2a
parent 40767e3e2a0cb9f36570bac55e92886f3cbc1009
Author: Christian Grothoff <christian@grothoff.org>
Date: Mon, 12 Oct 2026 17:29:31 +0200
do not accept deposits into purses that were already decided
Diffstat:
4 files changed, 125 insertions(+), 22 deletions(-)
diff --git a/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-deposit.c b/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-deposit.c
@@ -214,16 +214,8 @@ deposit_transaction (void *cls,
"do purse deposit");
return GNUNET_DB_STATUS_HARD_ERROR;
}
- if (! balance_ok)
- {
- *mhd_ret
- = TEH_RESPONSE_reply_coin_insufficient_funds (
- connection,
- TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS,
- &coin->cpi.denom_pub_hash,
- &coin->cpi.coin_pub);
- return GNUNET_DB_STATUS_HARD_ERROR;
- }
+ /* check too_late first: a deleted or decided purse also
+ reports balance_ok=false */
if (too_late)
{
TALER_EXCHANGEDB_rollback (TEH_pg);
@@ -234,6 +226,16 @@ deposit_transaction (void *cls,
NULL);
return GNUNET_DB_STATUS_HARD_ERROR;
}
+ if (! balance_ok)
+ {
+ *mhd_ret
+ = TEH_RESPONSE_reply_coin_insufficient_funds (
+ connection,
+ TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS,
+ &coin->cpi.denom_pub_hash,
+ &coin->cpi.coin_pub);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
if (conflict)
{
struct TALER_Amount amount;
diff --git a/src/exchangedb/do_purse_deposit.sql b/src/exchangedb/do_purse_deposit.sql
@@ -98,6 +98,19 @@ THEN
RETURN;
END IF;
+-- Check if purse was already decided (merged and paid, or expired
+-- and refunded), if so, abort and prevent deposit.
+PERFORM
+ FROM exchange.purse_decision
+ WHERE purse_pub = in_purse_pub;
+IF FOUND
+THEN
+ out_late=TRUE;
+ out_balance_ok=FALSE;
+ out_conflict=FALSE;
+ RETURN;
+END IF;
+
-- Debit the coin
-- Check and update balance of the coin.
diff --git a/src/exchangedb/test_purse_deposits.c b/src/exchangedb/test_purse_deposits.c
@@ -497,14 +497,10 @@ check_insufficient_balance (struct TALER_EXCHANGEDB_PostgresContext *pg)
/**
- * Once a merged purse has expired and been decided, the deposit that
- * would complete it is refused as too late.
- *
- * Note what this shows about the shape of the function: the "too late"
- * answer only comes out of the attempt to record the decision, which is
- * only reached for a purse that has been merged *and* is completed by this
- * deposit. A deposit into an expired purse that neither completes it nor
- * has been merged is accepted -- see EDBT-9 in bugs.txt.
+ * Once a purse has been decided, a new deposit into it is refused as too
+ * late, whether or not the purse was merged and whether or not the
+ * deposit would complete it. An idempotent replay of a deposit made
+ * before the decision still succeeds.
*
* @param pg the database context
* @return 0 on success
@@ -556,10 +552,8 @@ check_too_late (struct TALER_EXCHANGEDB_PostgresContext *pg)
22,
&coin,
NULL);
- /* Pay in the full target amount: the "too late" answer only comes out
- when the purse would be decided by this deposit, which is when the
- function tries to insert the `purse_decision` row that already exists.
- As with the balance check, the row is written first, so roll back. */
+ /* Pay in the full target amount. As with the balance check, the
+ `purse_deposits` row is written first, so roll back. */
FAILIF_C (GNUNET_OK !=
TALER_EXCHANGEDB_start (pg,
"test-purse-deposit-too-late"),
@@ -576,8 +570,100 @@ check_too_late (struct TALER_EXCHANGEDB_PostgresContext *pg)
TALER_EXCHANGEDB_rollback (pg);
TDB_coin_free (&coin);
FAILIF (! st.too_late);
+ FAILIF (st.balance_ok);
+ FAILIF (st.conflict);
FAILIF (1 != TDB_count (pg,
"FROM purse_deposits"));
+
+ /* A purse that was never merged and has been decided (refunded):
+ a deposit that would not complete it is refused as well, but a
+ replay of a deposit made before the decision is still idempotent.
+ All of this runs in one transaction that is rolled back, so the
+ later checks see the table unchanged. */
+ {
+ struct TDB_Purse purse2;
+ struct TALER_CoinPublicInfo coin2;
+ struct TALER_CoinPublicInfo coin3;
+ char *phex;
+
+ TDB_purse (pg,
+ 12,
+ "5",
+ ts (1600000000),
+ &purse2);
+ TDB_coin (pg,
+ &denom,
+ 23,
+ &coin2,
+ NULL);
+ TDB_coin (pg,
+ &denom,
+ 24,
+ &coin3,
+ NULL);
+ phex = TDB_hex (&purse2.purse_pub,
+ sizeof (purse2.purse_pub));
+ FAILIF_C (GNUNET_OK !=
+ TALER_EXCHANGEDB_start (pg,
+ "test-purse-deposit-decided"),
+ GNUNET_free (phex);
+ TDB_coin_free (&coin2); TDB_coin_free (&coin3));
+ FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ run_deposit (pg,
+ &purse2,
+ &coin2,
+ 23,
+ "1",
+ "0",
+ &st),
+ TALER_EXCHANGEDB_rollback (pg); GNUNET_free (phex);
+ TDB_coin_free (&coin2); TDB_coin_free (&coin3));
+ FAILIF_C ( (! st.balance_ok) || st.too_late || st.conflict,
+ TALER_EXCHANGEDB_rollback (pg); GNUNET_free (phex);
+ TDB_coin_free (&coin2); TDB_coin_free (&coin3));
+ FAILIF_C (GNUNET_OK !=
+ TDB_exec (pg,
+ "INSERT INTO purse_decision"
+ " (purse_pub,action_timestamp,refunded)"
+ " VALUES (decode('%s','hex'),1000000,TRUE);",
+ phex),
+ TALER_EXCHANGEDB_rollback (pg); GNUNET_free (phex);
+ TDB_coin_free (&coin2); TDB_coin_free (&coin3));
+ GNUNET_free (phex);
+ /* replay of the deposit made before the decision */
+ FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ run_deposit (pg,
+ &purse2,
+ &coin2,
+ 23,
+ "1",
+ "0",
+ &st),
+ TALER_EXCHANGEDB_rollback (pg);
+ TDB_coin_free (&coin2); TDB_coin_free (&coin3));
+ FAILIF_C ( (! st.balance_ok) || st.too_late || st.conflict,
+ TALER_EXCHANGEDB_rollback (pg);
+ TDB_coin_free (&coin2); TDB_coin_free (&coin3));
+ /* a new coin, not completing the purse */
+ FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ run_deposit (pg,
+ &purse2,
+ &coin3,
+ 24,
+ "1",
+ "0",
+ &st),
+ TALER_EXCHANGEDB_rollback (pg);
+ TDB_coin_free (&coin2); TDB_coin_free (&coin3));
+ TALER_EXCHANGEDB_rollback (pg);
+ TDB_coin_free (&coin2);
+ TDB_coin_free (&coin3);
+ FAILIF (! st.too_late);
+ FAILIF (st.balance_ok);
+ FAILIF (st.conflict);
+ FAILIF (1 != TDB_count (pg,
+ "FROM purse_deposits"));
+ }
return 0;
}
diff --git a/src/include/exchange-database/do_purse_deposit.h b/src/include/exchange-database/do_purse_deposit.h
@@ -47,6 +47,8 @@
* in this case, the return value will be
* #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT despite the failure
* @param[out] too_late set to true if it is too late to deposit into the purse
+ * (the purse was deleted or already decided); in this case
+ * @a balance_ok is false as well, and the caller must roll back
* @param[out] conflict set to true if the deposit failed due to a conflict (coin already spent,
* or deposited into this purse with a different amount)
* @return transaction status code