Skip to content

fix(wallet): report an honest phase when the node has no addresses to watch - #212

Merged
MichaelTaylor3d merged 7 commits into
mainfrom
fix/2609-sync-phase-honest
Aug 10, 2026
Merged

fix(wallet): report an honest phase when the node has no addresses to watch#212
MichaelTaylor3d merged 7 commits into
mainfrom
fix/2609-sync-phase-honest

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

Closes DIG-Network/dig_ecosystem#2609

The defect, and which of the three candidates it actually is

Cause 2, confirmed in code and on the machine. The phase could never settle on a default install.

Supervisor::run computes the subscription set, then:

let nothing_subscribed = puzzle_hashes.is_empty();if nothing_subscribed {// ... catch_up is NEVER CALLED}elseifletErr(e) = session.catch_up(...)

catch_up is the only caller of complete_catch_up, which is the only writer of
initial_sync_complete. So with zero puzzle hashes the flag can never latch — while
run_update_loop's NewPeakWallet arm keeps advancing sync_state.peak_height with the chain
for an authoritative peer. The old ladder read initial_sync_complete == false as Syncing.

That is exactly the measured shape: replica at the tip, advancing, phase syncing forever.

Candidate 1 (a missing terminal is_finished) is excluded: the terminal response is never awaited
because the catch-up never starts. Candidate 3 is excluded: the phase already consults live peer
state, not initial_sync_complete alone.

The fix

A fourth phase, no_addresses_to_watch, and a watched_addresses field that reports the reason.

The phase is NOT flipped on an empty set.initial_sync_complete stays false, because latching
it would flip §18.7 routing to Source::Db and read a funded wallet as empty. The new variant says
"the chain replica is current AND no wallet-scoped claim is being made" — which synced cannot say.

Three facts are required, and each excludes a different lie:

FactWhat it excludes
a peer attached right nowan offline replica reporting as current
that peer MAY WRITEa refused writer — its set is forced empty too, and its replica is falling behind
a MEASURED-zero watched setan unmeasured 0 default announcing itself as a fact

The trust condition is the load-bearing one. nothing_subscribed is true both when custody is
empty (benign) and when corroboration refused the writer (not benign — the replica is deliberately
not being written). Keying the phase on it would have told a user everything was fine while their
node silently stopped following the chain. watched is Option<u32>, not u32, so
"not resolved yet" cannot be spelled the same way as "resolved, and empty".

Verification

Red first, on the assertion rather than a compile error — the API was added with the ladder left
unchanged, so the new tests failed left: Syncing, right: NoAddressesToWatch. The three guard
tests passed while red (they expect Syncing), so they genuinely constrain an over-broad fix.

Six new tests: the default-install case, the refused-writer anti-conflation guard, the
unmeasured-set guard, fact-clearing on disconnect, an enrolled wallet mid-catch-up, and an
end-to-end run through the real supervisor loop that fails if the ladder is fixed but the wiring
is forgotten.

cargo test -p dig-wallet --lib: 452 passed, 0 failed. Every #2568 quorum test still passes.

Real-machine before/after with the installed binary is posted on the issue.

Blast radius

control.wallet.syncStatus gains a token and a field — both additive. The method is not in
dig-node-control-interface, so no release-first cross-repo change is needed. dig-app must learn
the new token; that surface is tracked separately.

Version

0.113.0 (minor — additive capability). 0.112.0 is claimed by the open #210 branch.

@MichaelTaylor3d

Copy link
Copy Markdown
ContributorAuthor

Real-machine verification — measured, not inferred

Two nodes, one machine, the same chain, seconds apart. Both have no wallet enrolled (empty
custody) and an authoritative Chia peer, and both are following the tip.

BEFORE — installed dign 0.111.0:

{"phase":"syncing","peak_height":9131612,"chia_peer_count":1}

AFTER — this branch, 0.113.0:

{"phase":"no_addresses_to_watch","peak_height":9131611,"chia_peer_count":1,"watched_addresses":0}

The peak is genuinely advancing under the new phase, so this is a live replica and not a latched
snapshot — three samples: 9131604 -> 9131607 -> 9131611.

The invariant that must NOT move, checked directly in the DB

peak=9131611 initial_sync_complete=0

initial_sync_complete is still 0. The phase settled WITHOUT latching it — which is the whole
point. Latching it would flip §18.7 routing to Source::Db and make a funded wallet read as empty.
no_addresses_to_watch says "the chain replica is current"; it deliberately does not say "serve
wallet reads from the local DB".

The anti-conflation guard, caught working on real hardware

The first run of the new binary reported:

{"phase":"syncing","peak_height":null,"chia_peer_count":1,"watched_addresses":0}

An empty watched set (watched_addresses: 0) and still syncing — because that peer was
DISCOVERED and had not been corroborated, so it may not write and peak_height was null. That is
the case the guard exists for: a replica that is genuinely falling behind, whose subscription set is
also empty. Had the phase keyed on "subscribed nothing", it would have told that user everything was
fine. It settled only once the peer was authoritative.

Live node undisturbed

The installed service was never stopped and its binaries were never swapped — the new build ran
standalone on its own port and state dir. dign 0.111.0 still installed, service Running,
still syncing at 9131612.

Fix the wallet sync phase never settling on a default install.
An empty puzzle-hash set never runs a catch-up, so initial_sync_complete cannot latch while the peak advances. Add SyncPhase::NoAddressesToWatch, gated on a writing peer AND a measured-zero watched set.

@MichaelTaylor3dMichaelTaylor3d left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CHANGES-REQUIRED — independent correctness gate, read at head 8c7c98f3e5049c2bf9eff36d58d005a96afbe6dd.

The fix itself is right and I verified it by mutation in my own worktree, not by reading:

proberesult
drop the session_may_write condition from the laddera_refused_writer_is_not_reported_as_nothing_to_watchFAILS — real guard
delete the handle.set_subscription(...) wiring line in Supervisor::runa_default_install_with_no_wallet_settles_on_nothing_to_watchFAILS — real wiring proof
replace observed.watched == Some(0) with observed.watched.unwrap_or(0) == 0all 30 supervisor tests stay GREEN — see finding 1

#2568 is untouched: no line of this diff reaches quorum corroboration, common_height(), Verdict::Insufficient or the by-name coin confirmation, and all of a_split_quorum_writes_nothing, an_unreachable_quorum_refuses_rather_than_defaulting_to_allow, a_single_lying_writer_cannot_move_the_replica, the_writer_is_examined_at_a_height_it_did_not_choose pass unchanged. The three-fact gate is sound for every state I could construct — Resubscribe clears the facts via set_connected(0) before reconnecting, the catch-up-error path clears them too, and no path sets peers=1 while carrying a previous session's facts.

Two findings block, both inline. Also note for the orchestrator: mergeStateStatus=DIRTY and all five required contexts are ABSENT on 8c7c98f (check-merge-preconditions.sh → BLOCKED) — rebase and re-run before any merge, independent of this verdict.

Non-gating, resolved by me: the sibling dig-app surface is filed as dig_ecosystem#2617; the PR body should cite it by number rather than "tracked separately".

Comment threadcrates/dig-wallet/src/sage/sync_supervisor/tests.rs
Comment threadSPEC.md Outdated
Comment threadSPEC.md Outdated
…eason field
Bump the workspace version to 0.113.0 (0.112.0 is claimed by the open #210 branch).
@MichaelTaylor3d
MichaelTaylor3dforce-pushed the fix/2609-sync-phase-honest branch from 8c7c98f to 6c28557CompareAugust 10, 2026 21:52
Co-authored-by: MichaelTaylor3d <5665004+MichaelTaylor3d@users.noreply.github.com>
Review measured that replacing == Some(0) with unwrap_or(0) == 0 left the whole suite green: the only unmeasured test also left may_write false, so the trust condition rejected it a step earlier. Split set_subscription into set_trust + set_watched so the real in-between state (trust settled, set not yet resolved) is reachable, and add a test for it. The mutation now fails that test.
@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review August 10, 2026 23:38
@MichaelTaylor3d
MichaelTaylor3d merged commit 1430086 into mainAug 10, 2026
15 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the fix/2609-sync-phase-honest branch August 10, 2026 23:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@MichaelTaylor3d