Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ edition = "2021"
# the ROOT manifest (`[workspace.package].version`), so it MUST be set here for a
# release to fire (§3.6). The library crates (dig-node-core/dig-runtime/dig-wallet)
# keep their own independent versions — only the released binary tracks the workspace version.
version = "0.198.0"
version = "0.206.0"

# Release hardening, matching digstore: keep integer-overflow checks ON in release.
# The node parses untrusted serialized input and does offset/length arithmetic over
Expand Down
20 changes: 20 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -8445,6 +8445,26 @@ A pass runs: at start-up (once the wallet and a chain source are available), on
the plan's create set until that entry resolves. The audit record is the in-flight ledger; the
disk and the chain remain the only steady-state truths.

**A funding-coin reservation is a BOUNDED hold.** The audit record also reserves the funding
coins a non-terminal entry consumed, so a second create in the same confirmation window cannot
re-select them. That reservation MUST expire: it holds a coin for
`FUNDING_RESERVATION_WINDOW_MS` = `2 x MIRROR_ROUND_LENGTH_MS` (20 minutes) measured from the
entry's LAST revision, after which the coin returns to the selectable set. An unbounded hold is a
lockout — a spend that never lands would strand its inputs forever and a genuinely funded
operator wallet would report `Insufficient` permanently. The window is derived: the chain-side
figure is the wallet's own post-broadcast reservation lifetime (10 minutes, roughly a dozen Chia
blocks), and one further round is added because this hold is re-evaluated only once per
`MIRROR_ROUND_LENGTH_MS`, so a threshold equal to the poll interval would release an entry on the
first pass at which its confirmation could even have been observed. A record whose `updated_ms`
is in the FUTURE keeps its hold.

**Expiry MUST NOT change the record.** The entry stays exactly the `submitted` or `unresolved` it
was, and stays resolvable by step 7 and by §23.5's reconcile indefinitely. Releasing a coin is
not a claim that the spend failed: `unresolved` means "this node signed and does not know what
happened", which remains true afterwards. Writing a `failed` entry to settle the bookkeeping is
forbidden, for the same reason a `confirmed` entry carries its height and coin id inside the
variant.

7. **Resolves spends an EARLIER pass broadcast.** A mirror spend is broadcast in one pass and
confirms during a later one, so the outcome MUST be recorded by an id-keyed resolution over the
audit record rather than by the handle that opened it. Before the observation of step 2 is
Expand Down
10 changes: 9 additions & 1 deletion crates/dig-node-service/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2812,8 +2812,16 @@ fn spawn_mirror_passes(
// wallet selector's reservation prune (dig_ecosystem#2763), which the chain cannot
// offer: a broadcast coin stays unspent in the chain's view for the entire confirmation
// window, and this loop runs inside it. An `Err` defers creates and never reclaims.
let committed = crate::mirror::funding::committed_funding_coin_ids(
//
// BOUNDED, not merely computed (dig-node#471). The reservation is a time box read from
// the audit record, so a spend that never lands releases its coins after
// `FUNDING_RESERVATION_WINDOW_MS` instead of withholding them forever. The record itself
// is untouched and stays chaseable; only the hold lapses.
//
// One clock reading for the whole pass, alongside the one disk and one balance reading.
let committed = crate::spend_audit::committed_funding_coin_ids(
&crate::spend_audit::SpendLog::in_state_dir(),
lifecycle::now_unix_ms(),
)
.map_err(|e| crate::mirror::runner::PassError::Wallet(e.to_string()));

Expand Down
Loading
Loading