Skip to content
4 changes: 2 additions & 2 deletions 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.247.0"
version = "0.247.1"

# 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
2 changes: 1 addition & 1 deletion crates/dig-wallet/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dig-wallet"
version = "0.47.0"
version = "0.48.0"
edition = "2021"
license = "GPL-2.0-only"
description = "DIG Browser built-in Chia wallet sidecar: a local axum server (using digstore-chain + chia-wallet-sdk over coinset.org) that serves a Sage-mirroring wallet UI. Native Rust so BLS signing works; the browser opens it at 127.0.0.1."
Expand Down
76 changes: 35 additions & 41 deletions crates/dig-wallet/src/sage/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1035,7 +1035,7 @@ impl WalletBackend {
/// `synced: true` on that basis told a client a stale balance was settled, which is the
/// money-adjacent falsehood dig_ecosystem#2869 exists to remove.
///
/// It reuses [`super::sync_supervisor::is_following`] — the SAME predicate
/// It reuses [`super::sync_supervisor::FollowingEvidence`] — the SAME evidence
/// `control.wallet.syncStatus` reports its phase from — so a client cannot be told `synced` by
/// one endpoint and `syncing` by the other about the same moment.
///
Expand All @@ -1046,12 +1046,16 @@ impl WalletBackend {
/// `db.is_synced()` directly and so could, and on a live node did, report `synced: true`
/// about the same replica this method was calling stale (dig-node#293).
///
/// It narrows that predicate on BOTH of its unobservable arms, and only here. `is_following`
/// answers `true` whenever EITHER height is missing, because on a status endpoint an absent
/// measurement is not an accusation against the replica. On a money read it is the opposite:
/// currency is a claim, and nothing that was never measured can establish one. Either arm left
/// unnarrowed leaves `synced: true` resting on the latched `initial_sync_complete` this method
/// exists to stop trusting.
/// BOTH unobservable arms withhold the claim, and that is now a property of the evidence type
/// itself rather than a narrowing applied at this call site. `FollowingEvidence::measure`
/// yields `None` whenever EITHER height is missing: currency is a claim, and nothing that was
/// never measured can establish one. Either arm left permissive would leave `synced: true`
/// resting on the latched `initial_sync_complete` this method exists to stop trusting.
///
/// This method used to hold that narrowing alone, in two `let ... else { return false }`
/// guards duplicating exactly those arms. dig-node#495 moved the rule into the constructor and
/// this call site simply asks for the evidence — one narrowing, in one place, for both
/// endpoints.
///
/// - **No PEER height** — there is no second opinion to compare the replica against.
/// - **No REPLICA height** — there is no figure to compare, and `synced: true` would then be
Expand All @@ -1065,35 +1069,24 @@ impl WalletBackend {
/// Either way the figure is still SERVED, with whatever `peak_height` is actually known,
/// labelled stale — never withheld.
///
/// `is_following` itself is deliberately left ALONE. Its permissive `_ => true` is correct for
/// the sync-phase reporting it was written for, where an unmeasured tier must not be spent as
/// evidence against a replica; narrowing it there would change a phase machine owned by another
/// family. The narrowing is a property of the MONEY read, so it lives at this call site.
///
/// That placement has a LIMIT, and it is stated here rather than left to be rediscovered:
/// this method is the single gate for every read served by [`WalletBackend`] — each one either
/// passes through here or writes the literal `false` — but it is NOT the only producer of a
/// `synced` claim in the crate. [`super::sync_supervisor::SyncHandle::status`] reaches
/// `SyncPhase::Synced` through its own `is_following` call and pairs it with the replica's raw
/// `peak_height`, so `control.wallet.sync-status` can still emit
/// `{phase: "synced", peak_height: null}` — the exact pairing this gate abolishes on the money
/// reads. That path is deliberately out of scope: it is a status endpoint rather than a
/// currency claim, and the phase machine belongs to another family (dig_ecosystem#2761).
///
/// The asymmetry worth carrying into that work: `is_following`'s own doc justifies its
/// permissive arm ENTIRELY in terms of an unmeasured PEER tier, and offers no justification at
/// all for the unmeasured-REPLICA arm. Those are different things. An absent peer height is a
/// missing second opinion, which is fairly read as no accusation; an absent replica height is
/// the subject of the claim having no measurement whatsoever, which supports no verdict in
/// either direction.
/// The structural agreement claimed above is now TRUE, and it was FALSE when first written —
/// which is the point of the convergence (dig-node#495). This method is the single gate for
/// every read served by [`WalletBackend`], but it was never the only producer of a `synced`
/// claim in the crate: [`super::sync_supervisor::SyncHandle::status`] reached
/// `SyncPhase::Synced` through a predicate permissive on both unmeasured arms, and paired it
/// with the replica's separately-read `peak_height`, so `control.wallet.syncStatus` could emit
/// `{phase: "synced", peak_height: null}` — the exact pairing this gate abolished on the money
/// reads while the status endpoint kept producing it.
///
/// Both endpoints now derive `synced` from the same [`super::sync_supervisor::FollowingEvidence`],
/// and on that path the reported height comes FROM the evidence, so the pairing is
/// unrepresentable rather than merely unlikely.
async fn replica_answer_is_current(&self, peak_height: Option<u32>) -> bool {
let Some(replica_peak) = peak_height else {
return false;
};
let Some(peer_peak) = self.chain_peer_tier().await.peak_height else {
return false;
};
super::sync_supervisor::is_following(Some(replica_peak), Some(peer_peak))
super::sync_supervisor::FollowingEvidence::measure(
peak_height,
self.chain_peer_tier().await.peak_height,
)
.is_some()
}

/// The chain-sync supervisor's handle, if one is running.
Expand Down Expand Up @@ -7551,10 +7544,11 @@ mod tests {
/// chain peer that has announced a height serves its figure labelled stale.
///
/// This is the state a freshly-started node sits in, and the one a node with no reachable chain
/// peer sits in indefinitely. [`super::sync_supervisor::is_following`] answers `true` there by
/// design (an absent second opinion is not an accusation on a status endpoint), so a money read
/// delegating to it unnarrowed pairs `synced: true` with an arbitrarily old `peak_height` — the
/// stale-presented-as-current claim this PR exists to remove.
/// peer sits in indefinitely. The predicate this gate once delegated to answered `true` there
/// by design (an absent second opinion was read as no accusation), so a money read delegating
/// to it unnarrowed pairs `synced: true` with an arbitrarily old `peak_height`, the
/// stale-presented-as-current claim this PR exists to remove. Since dig-node#495
/// [`super::sync_supervisor::FollowingEvidence::measure`] withholds the evidence itself.
///
/// FIXTURE DESIGN — `peak_height: None` is what makes the tier unobservable, and it is the only
/// axis varied from [`a_replica_level_with_its_peers_still_reports_synced`], which stays green
Expand Down Expand Up @@ -7598,8 +7592,8 @@ mod tests {
/// **Proves:** an UNKNOWN REPLICA height is never reported as current either — the other
/// `None` arm of the same predicate, on the endpoint where it actually reaches production.
///
/// [`super::sync_supervisor::is_following`] answers `true` when EITHER side is `None`, and
/// [`WalletBackend::replica_answer_is_current`] narrowed only the peer side. `chain_peak`
/// The predicate this gate once delegated to answered `true` when EITHER side was `None`, and
/// an earlier [`WalletBackend::replica_answer_is_current`] narrowed only the peer side. `chain_peak`
/// escapes the remaining arm by construction — it calls the gate inside `if let Some(peak)`,
/// so it can never hand it a `None` replica. The balance and coin reads do not: they read
/// `sync_state().peak_height` as an `Option` and pass it straight through. So the money reads,
Expand Down
8 changes: 4 additions & 4 deletions crates/dig-wallet/src/sage/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1320,7 +1320,7 @@ mod tests {
use super::*;
use crate::sage::db::WalletDb;
use crate::sage::sync_supervisor::{
is_following, StallVerdict, StallWatch, SESSION_MAX_LIFETIME, STALL_AFTER,
FollowingEvidence, StallVerdict, StallWatch, SESSION_MAX_LIFETIME, STALL_AFTER,
};

fn coin(parent: u8, ph: u8, amount: u64) -> Coin {
Expand Down Expand Up @@ -3341,7 +3341,7 @@ mod tests {
///
/// This is the test the whole change exists for. An accepted `u32::MAX` does not merely
/// misreport the peak — it permanently DISABLES both of them, and silently:
/// [`is_following`]'s `peers.saturating_sub(replica)` is `0` for ever, so the phase reports
/// [`FollowingEvidence`]'s `peers.saturating_sub(replica)` is `0` for ever, so the phase reports
/// `Synced` however far behind the replica really is; and [`StallWatch`]'s
/// `behind = peers > replica` is false for ever, so the stall clock never starts.
///
Expand All @@ -3367,7 +3367,7 @@ mod tests {
let peers = Some(anchor + 50);

assert!(
!is_following(replica, peers),
FollowingEvidence::measure(replica, peers).is_none(),
"the phase must still be able to see a replica {replica:?} behind peers {peers:?}"
);

Expand Down Expand Up @@ -3611,7 +3611,7 @@ mod tests {
let peers = Some(anchor + 50);

assert!(
!is_following(replica, peers),
FollowingEvidence::measure(replica, peers).is_none(),
"the phase must still see a replica {replica:?} behind peers {peers:?}"
);

Expand Down
Loading
Loading