Skip to content

A repushed bundle re-arms its reservation from now, so a retrying caller can hold coins indefinitely #502

Description

@MichaelTaylor3d

Task

reserve_spend re-arms a coin reservation's expiry from now on every push of the same bundle, with
no cap on total hold. A caller that re-pushes the same signed bundle more often than every 600 s holds
those coins indefinitely, which is the lockout failure the reservation design explicitly names as
the worse of the two — measured on dig-account as available=4000000 selectable=0, renewable
indefinitely.

Context

crates/dig-wallet/src/sage/db.rs:2765-2773:

INSERT INTO pending_transactions (...) VALUES (...)
ON CONFLICT(transaction_id) DO UPDATE SET
   expires_at = excluded.expires_at,
   attempts = pending_transactions.attempts + 1

expires_at is supplied by crates/dig-wallet/src/sage/rpc.rs's reserve_pushed_bundle as
now + RESERVATION_TTL_MS, so each repush pushes the deadline a further 600 s into the future.
attempts is incremented and never read as a cap.

Nothing in this tree exercises it today, which is why this is not a live defect and not gating:
push_signed_bundle has exactly one production caller, crates/dig-node-service/src/control.rs:2877
(control.wallet.broadcast), and there is no automated resend behind it — the mirror-coin, tipping and
collateral spend paths use their own broadcaster and never reach this guard. So the bound currently
holds because of the caller set, not because of the design, and an external caller (dig-app, an
automation, a retrying UI) is outside that accident.

Why it matters more after #497. That PR (closing
#460) moves the COMMON peer-local refusals —
DOUBLE_SPEND, MEMPOOL_CONFLICT, ALREADY_INCLUDING_TRANSACTION — from the free branch onto the
hold branch, which is correct for #460 and is also what enlarges the exposure here. Worst honest case:
one input of a multi-input bundle is spent by someone else, so every node answers DOUBLE_SPEND
forever; before #497 the bundle's other, still-good inputs freed immediately, and after it they are
held and re-armed by every retry.

Found by the adversarial gate on #497 (https://github.com/DIG-Network/dig-node/pull/497#issuecomment-5504576879,
finding F3), which rated it MEDIUM and non-gating and recommended filing rather than widening #460.

Scope

Assess first, then fix the smallest thing that works. Two candidate shapes, both stated by the gate:

  • cap the total hold at submitted_at + RESERVATION_TTL_MS rather than extending from now; or
  • decline to extend when every observed refusal for that bundle was peer-local.

Read RESERVATION_TTL_MS's own doc before changing anything: the TTL must not be shortened, and
this ticket is not licence to do so. The right outcome bounds total hold without making a genuine
in-flight bundle's inputs reselectable earlier than 600 s after its LAST honest transmission — those
two goals are in tension and the resolution is the deliverable.

Note the deliberate asymmetry that must survive: coin_reservations uses
ON CONFLICT(coin_id) DO NOTHING (db.rs:2788) so a coin backs only ONE in-flight bundle and the
FIRST claim wins. A fix must not turn that into a path where a second bundle can displace or shorten
another's claim.

Evidence

Every claim above is a direct read of dig-node@41dbca5; gitnexus's index for this repo is ~300
commits stale and returns a false-safe impactedCount: 0, so it was not used. Not fired against a live
repushing caller.

Acceptance

A caller that re-pushes the same bundle indefinitely cannot hold its inputs beyond a bounded total,
proven by a test that repushes past the point where a single TTL would have expired and asserts the
coins return to selection — not merely that one push reserves.

Cross-reference: #460 ·
#348 ·
#497

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:businessa person can DO something new, or money moves, or a shipped surface stops lying to them

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions