Skip to content

Funding coins of a spend that never lands are withheld forever — a funded wallet reports Insufficient permanently #471

Description

@MichaelTaylor3d

Task

Funding coins reserved by a mirror spend that never lands are withheld from selection forever. On a modest operator wallet, repeated stuck records shrink the selectable set monotonically until a genuinely funded wallet reports Insufficient permanently.

This was the one place the mirror lifecycle did not self-heal. N was unbounded.

Note: this body was empty until now — the heredoc that created it failed on a disk-full error and I did not verify the result. The dispatch prompt was the only spec the implementing lane had. Restored for the record.

The mechanism

funding::committed_funding_coin_ids withheld the funding coins of every !is_terminal() record. Correct as far as it goes — a coin committed to a bundle that may sit in a mempool must not be reselected, which is the double-spend window dig-node#348 exists to close.

What was missing is anything that ages the record out. The resolver added in #457 promotes only positively, on observing the coin. A Submitted or Unresolved create whose coin never appears is therefore never settled, and its coins were never released.

Unlike the create-suppression bound, this did not lapse at the epoch rollover — that one is keyed on the bond's epoch and self-clears in ~1,008 passes (~7 days). This was keyed on the record's status alone, which nothing changed.

Reachable without an attacker: a hard kill between begin and any outcome, or a Submitted bundle evicted from a mempool without confirming.

A hypothesis this ticket carried, now REFUTED

The dispatch suggested Pending and Submitted differ in a way that might collapse the problem — a Pending bundle was never broadcast, so its coins were arguably never at risk.

That was wrong, and the lane reported the refutation rather than quietly working around it. SpendJournal::begin writes funding_coin_ids: Vec::new(); the ids appear only at submitted(). So a Pending record contributes zero coins to the committed set and always did. The stuck set is exactly the records that genuinely had a signed bundle handed to the network — so the window was necessary, with no free case to carve out.

Resolution — PR #475

A 20-minute window, N = 2 passes, built from two named constants rather than an invented number:

  • The chain-side figure is dig-wallet's own post-broadcast RESERVATION_TTL_MS (crates/dig-wallet/src/sage/rpc.rs:544) — 10 minutes, with its own recorded reasoning (~52 s blocks, ~12 chances to land, past the point where an unconfirmed bundle is likelier dropped than pending). It covers the same phase of the same lifecycle, so reusing it beats minting a second lifetime for one phase.
  • One further round is added, and that part is a judgement, stated as such in the code: the hold is re-evaluated only once per MIRROR_ROUND_LENGTH_MS (also 10 minutes), so a threshold equal to the poll interval would release a record on the first pass at which the resolver could even have observed its confirmation.

The record stays honest. Releasing the coins is not declaring the spend failed: the tests assert the record is still Submitted and still may_have_reached_the_network() after release. Unresolved means "this node signed and does not know what happened", and that remains true.

Both directions are pinned, which is what most fixes of this shape miss — a coin still inside the window is refused with Insufficient; the same coin two passes later is selectable. Same chain, same wallet, same audit record; only the observer's clock varies.

Mutation-proved: window removed → the release test fails while the in-flight control passes; window = 0 → both fail, including the control.

Follow-on routed to #469

The predicate moved to spend_audit.rs (it only ever read a &SpendLog), and funding::committed_funding_coin_ids is now production-dead — alive only via its own tests. Deleting it and folding those tests into the spend_audit ones belongs to the #461/#463 lane, which is rewriting that file.

Parent

Self-healing #464 · lifecycle #412 · epic https://github.com/DIG-Network/dig_ecosystem/issues/3166

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