Task
The coin-reservation cap added in #505 bounds continuous
hold, not aggregate hold. Decide whether that is the behaviour we want, and either implement the
aggregate bound or record the decision to accept the current one.
Context
prune_reservations DELETEs the pending_transactions row when expires_at <= now
(crates/dig-wallet/src/sage/db.rs). submitted_at -- the anchor the cap is computed from -- lives on
that row and nowhere else. So once the cap fires and the row is pruned, the next re-push of the same
bundle re-INSERTs with submitted_at = now and gets a fresh MAX_RESERVATION_HOLD_MS.
The result is a sawtooth: hold for an hour, free, hold for another hour. A caller retrying tightly
therefore holds its inputs for all but a narrow window each hour, indefinitely. #502's acceptance bar
is met -- the coins genuinely do return to selection -- but "bounded total hold" is true only per row.
Found by the adversarial gate on PR #505 (R2) and independently by the correctness gate (finding 5),
which is why it is filed rather than argued about. PR #505 pins the behaviour with a test and states it
in the MAX_RESERVATION_HOLD_MS doc, so it is known and measured, not a surprise.
The fork, which is why this is a ticket and not a fix
Bounding aggregate hold means REMEMBERING that a given bundle already had its hour -- a tombstone, or
retaining the row past expiry with the reservation released. Either way the consequence is that the
wallet permanently declines to protect that bundle's inputs, even though the bundle may still be in
a mempool and may still land. That is the double-spend direction, and it is the direction
#348 and
#497 both deliberately closed.
So this is a genuine trade-off between the two failure modes, not a defect with an obvious fix, and it
was out of scope for #502's "smallest thing that works".
Worth weighing when deciding:
- How a bundle that has failed for an hour differs, if at all, from one pushed for the first time --
from the DB's point of view they are identical, and that is exactly the question.
- Whether a retention window (remember for N hours, then forget) is the honest middle, and what N is
anchored to. Chia block time is ~18.75 s, so an hour is already ~190 blocks.
- Whether the right surface is a bound at all rather than an operator-visible signal that a bundle has
been re-pushed past its cap -- a caller looping forever on a dead bundle is a bug worth reporting, not
only worth absorbing.
Scope
The deliverable is the DECISION plus whichever of these it selects:
- implement the aggregate bound (with the retention/GC story stated), or
- close this as accepted behaviour with the reasoning recorded on the ticket and the
SPEC.md §18.9a
clause worded to say the bound is per-row.
Either outcome closes it. Do not leave it open as a standing observation.
Evidence
Read against dig-node PR #505 head. prune_reservations' DELETE and the absence of submitted_at
from the upsert's DO UPDATE SET are the two facts the sawtooth follows from; both are directly
readable in crates/dig-wallet/src/sage/db.rs. Not fired against a live repushing caller.
Cross-reference
Parent behaviour this came out of: #502 ·
#505 ·
prior art on the failure direction: #348 ·
#497
Task
The coin-reservation cap added in #505 bounds continuous
hold, not aggregate hold. Decide whether that is the behaviour we want, and either implement the
aggregate bound or record the decision to accept the current one.
Context
prune_reservationsDELETEs thepending_transactionsrow whenexpires_at <= now(
crates/dig-wallet/src/sage/db.rs).submitted_at-- the anchor the cap is computed from -- lives onthat row and nowhere else. So once the cap fires and the row is pruned, the next re-push of the same
bundle re-INSERTs with
submitted_at = nowand gets a freshMAX_RESERVATION_HOLD_MS.The result is a sawtooth: hold for an hour, free, hold for another hour. A caller retrying tightly
therefore holds its inputs for all but a narrow window each hour, indefinitely. #502's acceptance bar
is met -- the coins genuinely do return to selection -- but "bounded total hold" is true only per row.
Found by the adversarial gate on PR #505 (R2) and independently by the correctness gate (finding 5),
which is why it is filed rather than argued about. PR #505 pins the behaviour with a test and states it
in the
MAX_RESERVATION_HOLD_MSdoc, so it is known and measured, not a surprise.The fork, which is why this is a ticket and not a fix
Bounding aggregate hold means REMEMBERING that a given bundle already had its hour -- a tombstone, or
retaining the row past expiry with the reservation released. Either way the consequence is that the
wallet permanently declines to protect that bundle's inputs, even though the bundle may still be in
a mempool and may still land. That is the double-spend direction, and it is the direction
#348 and
#497 both deliberately closed.
So this is a genuine trade-off between the two failure modes, not a defect with an obvious fix, and it
was out of scope for #502's "smallest thing that works".
Worth weighing when deciding:
from the DB's point of view they are identical, and that is exactly the question.
anchored to. Chia block time is ~18.75 s, so an hour is already ~190 blocks.
been re-pushed past its cap -- a caller looping forever on a dead bundle is a bug worth reporting, not
only worth absorbing.
Scope
The deliverable is the DECISION plus whichever of these it selects:
SPEC.md§18.9aclause worded to say the bound is per-row.
Either outcome closes it. Do not leave it open as a standing observation.
Evidence
Read against
dig-nodePR #505 head.prune_reservations' DELETE and the absence ofsubmitted_atfrom the upsert's
DO UPDATE SETare the two facts the sawtooth follows from; both are directlyreadable in
crates/dig-wallet/src/sage/db.rs. Not fired against a live repushing caller.Cross-reference
Parent behaviour this came out of: #502 ·
#505 ·
prior art on the failure direction: #348 ·
#497