Task
Coin reservation is gated on an untrusted accepted, and the under-claim direction FAILS OPEN into the
same double-select window this family exists to close.
Verified read-only against origin/main @ de75ecf (v0.149.0), both directions confirmed exactly as
originally filed:
| the source says |
behaviour |
consequence |
| under-claims — denies relaying what it actually relayed |
FAIL-OPEN: nothing reserved, rpc.rs:1978 only warn!s |
coins stay reselectable, so a second send inside the confirmation window may reselect the same inputs |
| over-claims |
FAIL-CLOSED for 10 min (RESERVATION_TTL_MS = 10 * 60 * 1000, rpc.rs:514) |
a legitimate coin is held for the full TTL over a spend that may not exist |
Producer: crates/dig-wallet/src/sage/rpc.rs:1974.
Why this belongs to this epic
This is the double-select window, arriving through a different door. dig-account #40 closed the
in-process race, and dig-node #344 is serving cross-process reservations — but both assume the
post-broadcast reservation is actually written. When the source under-claims, it is not, and the coin
returns to the selectable set while a bundle carrying it may be in flight.
NC-12 is the reason this is not a corner case: accepted comes from a dialled peer, and every
dialled peer is untrusted. A peer that denies relaying what it relayed is not an exotic failure — it is
the untrusted-input case the contract already assumes exists.
The asymmetry is the finding
The two directions fail opposite ways, and the cheap-to-lie direction is the unsafe one. A source
that wants a coin reselected simply says "not accepted." A source that wants a coin locked must
over-claim, and gets only a bounded 10-minute hold.
Fix direction: treat a post-transmit transport failure as POSSIBLY-IN-FLIGHT — reserve, and let the
TTL retire it — rather than treating an unconfirmed relay as a non-event. That is the same discipline
dig-account settled on: anything other than a definitive rejection holds to TTL, because the outcome is
unknown and re-selecting could double-spend.
Do not shorten the TTL to compensate. That trades a double-select for a lockout, and a lockout is the
worse failure — a gate measured exactly that on dig-account (available=4000000 selectable=0, renewable
indefinitely).
Scope
crates/dig-wallet/src/sage/rpc.rs. dig-node has live lanes (PR #344 serving reservations, #237) —
work in your own worktree off origin/main, and coordinate with #344 rather than colliding: it is the
lane that owns the reservation surface right now, and this may be cheapest folded into it.
Evidence
- Reproduce the under-claim fail-open first: a source that denies relaying what it relayed must be
shown leaving the coin reselectable — red for the right reason, then fix.
- Prove both directions after. A fix that reserves on every failure and never releases has traded one
defect for the worse one; pin the bound from both sides.
warn! is not a guard. If the remedy keeps a log line, the log must not be the only difference
between the two paths.
- Read passed-counts, never
ok. Mutations assert their patch applied; parse the test result:
line, never stderr. One cargo process at a time.
- Never print a seed, mnemonic or private key. Construct the race in tests; do not spend mainnet funds.
Provenance
Filed during the pre-merge audit of dig-node PR #295 (which PASSED — this was explicitly non-gating),
carried as a folded finding on dig-node#297, and re-verified still-real on 2026-08-25 when that ticket
was closed as obsolete. Three of the four findings folded there are now obsolete; this is the one that
survived, so it is filed on its own rather than dying with its host.
Parent: https://github.com/DIG-Network/dig_ecosystem/issues/3127
Related: DIG-Network/dig-node PR #344 (cross-process reservation), DIG-Network/dig-account #40 (the
in-process race, closed)
Task
Coin reservation is gated on an untrusted
accepted, and the under-claim direction FAILS OPEN into thesame double-select window this family exists to close.
Verified read-only against
origin/main@de75ecf(v0.149.0), both directions confirmed exactly asoriginally filed:
rpc.rs:1978onlywarn!sRESERVATION_TTL_MS = 10 * 60 * 1000,rpc.rs:514)Producer:
crates/dig-wallet/src/sage/rpc.rs:1974.Why this belongs to this epic
This is the double-select window, arriving through a different door. dig-account #40 closed the
in-process race, and dig-node #344 is serving cross-process reservations — but both assume the
post-broadcast reservation is actually written. When the source under-claims, it is not, and the coin
returns to the selectable set while a bundle carrying it may be in flight.
NC-12 is the reason this is not a corner case:
acceptedcomes from a dialled peer, and everydialled peer is untrusted. A peer that denies relaying what it relayed is not an exotic failure — it is
the untrusted-input case the contract already assumes exists.
The asymmetry is the finding
The two directions fail opposite ways, and the cheap-to-lie direction is the unsafe one. A source
that wants a coin reselected simply says "not accepted." A source that wants a coin locked must
over-claim, and gets only a bounded 10-minute hold.
Fix direction: treat a post-transmit transport failure as POSSIBLY-IN-FLIGHT — reserve, and let the
TTL retire it — rather than treating an unconfirmed relay as a non-event. That is the same discipline
dig-account settled on: anything other than a definitive rejection holds to TTL, because the outcome is
unknown and re-selecting could double-spend.
Do not shorten the TTL to compensate. That trades a double-select for a lockout, and a lockout is the
worse failure — a gate measured exactly that on dig-account (
available=4000000 selectable=0, renewableindefinitely).
Scope
crates/dig-wallet/src/sage/rpc.rs. dig-node has live lanes (PR #344 serving reservations, #237) —work in your own worktree off
origin/main, and coordinate with #344 rather than colliding: it is thelane that owns the reservation surface right now, and this may be cheapest folded into it.
Evidence
shown leaving the coin reselectable — red for the right reason, then fix.
defect for the worse one; pin the bound from both sides.
warn!is not a guard. If the remedy keeps a log line, the log must not be the only differencebetween the two paths.
ok. Mutations assert their patch applied; parse thetest result:line, never stderr. One cargo process at a time.
Provenance
Filed during the pre-merge audit of dig-node PR #295 (which PASSED — this was explicitly non-gating),
carried as a folded finding on dig-node#297, and re-verified still-real on 2026-08-25 when that ticket
was closed as obsolete. Three of the four findings folded there are now obsolete; this is the one that
survived, so it is filed on its own rather than dying with its host.
Parent: https://github.com/DIG-Network/dig_ecosystem/issues/3127
Related:
DIG-Network/dig-nodePR #344 (cross-process reservation),DIG-Network/dig-account#40 (thein-process race, closed)