Skip to content

sec(reservation): gated on an untrusted 'accepted' — the under-claim direction fails OPEN into the double-select window #348

Description

@MichaelTaylor3d

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions