Uh oh!
There was an error while loading. Please reload this page.
fix(dig-node): rooted /s read survives a broken lineage walk (#747, #841) - #79
Conversation
…) for verify_pinned_root WIP: read-root consume (#747/#841/#852 dig-node half). Bumps all digstore-* git-rev deps together (one repo, one workspace version) so the tree carries a single digstore-core 0.19.0; digstore-chain d5e52fb exposes the bounded, fail-closed verify_pinned_root the local /s serve path will consume. Co-Authored-By: Claude <noreply@anthropic.com>
797b9d5 to
4dc9bd3Compare…841) The local plaintext content-serve (`GET /s/<store>:<root>/<path>`) and the `dig.getContent` RPC pin every read to the store's chain-anchored root (#127). Both resolved that root by walking the full singleton lineage, which ABORTS on a single unparseable intermediate generation ("parse next store: missing child", #747) — leaving a perfectly valid pinned root permanently unreadable and breaking the local read tier. Rooted reads now anchor the pinned root with a graceful fallback: prefer the lineage walk when it succeeds (it also yields the owner puzzle hash, #486, with no extra chain call), but when the walk is broken fall back to digstore-chain's bounded, fail-closed `verify_pinned_root` — one launcher-hint query that reads only the current unspent generation, no per-generation walk. A valid pinned root stays readable; #127 anti-rollback is preserved (a stale/never-anchored root is still rejected with -32005 ROOT_NOT_ANCHORED). Rootless reads resolve + serve the chain-anchored tip and surface it as X-Dig-Root / X-Dig-Verified (#852 dig-node half); a rootless request has no candidate to bounded-verify and relies on the walk. Adds an additive `AnchoredRootResolver::verify_pinned_root` (default = walk-based tip equality, so test mocks are unchanged; `CoinsetResolver` overrides it with the bounded on-chain verify). SPEC §4.6/§14.4 updated. Owner is omitted on a rooted read only when the owner-carrying walk is broken. Closes #747 Closes #841 Co-Authored-By: Claude <noreply@anthropic.com>
13b9ad6 to
c25baa8Compare
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Independent correctness review (fresh context) — PASS
Read-root fix verified correct + #486 non-regressed + spec/tests aligned; all required checks green.
Acceptance — all met:
- #747/#841 — rooted
/sread whose lineage walk aborts ("parse next store: missing child") STILL serves via the boundedverify_pinned_rootfallback. Proven end-to-end byserve_content_plaintext_rooted_read_survives_a_broken_lineage_walk_747(lib.rs ~5320) and the RPC path by thedig.getContentbroken-walk tests (lib.rs ~4272/4306). Fallback is fail-closed:content_serve.rsanddispatch.rsonly serveSome(req)when the walk's tip matches OR the bounded verify returnsOk; a genuine walk tip-mismatch (Ok(Some(state)) != req) still rejects withROOT_NOT_ANCHORED(anti-rollback #127 preserved —serve_content_plaintext_rejects_a_non_anchored_root). - #852 node half — rootless request resolves the chain tip via the walk, serves it, emits
X-Dig-Root+X-Dig-Verified(server.rs:1305-1306). SPEC §4.6/§14.4 updated. - #486 non-regression — owner puzzle hash is computed from
anchored_statebefore the branch and surfaced on the intact-walk happy path (serve_content_plaintext_reports_the_resolver_owner_puzzle_hash_when_pin_enforced); omitted ONLY on the broken-walk fallback (asserted in the survival test). No happy-path regression. - Versioning — dig-node → 0.52.0, dig-node-core → 0.15.0, both minor, version-increment gate green; SPEC.md updated in-unit.
- digstore family — all digstore-* crates resolve to the single git-rev
d5e52fbin Cargo.lock; no split digstore-core instance. - Coverage/CI — Test+coverage, Clippy, Rustfmt, CodeQL, version-gate, all builds green on
c25baa8.
Non-gating observations (resolve without blocking):
lib.rsshows 5975/5790 churn but the real change is ~201 lines — the rest is a whole-file CRLF→LF normalization (base was CRLF, head LF). Consider a.gitattributes* text=eol=lfso it doesn't re-churn and bury future diffs.- This branch bundles the transport/STUN (§19.2) +
DIG_NETWORK_GENESIS/effective-network-label + genesis-field changes beyond the read-root title — all spec'd + tested, and part of the #1439 family, so in-scope, but the PR title undersells the blast radius. - dig-constants: the genesis correctly consumes
dig_constants::DIG_MAINNET. The derived network-label namespace (DIG_+ first 16 genesis hex) is a value the relay introducer / dig-gossip must agree on — confirm it is not independently re-derived in dig-relay/dig-gossip; if it is, it belongs in a shared crate/dig-constants. (Transport-lane follow-up, not a read-root blocker.)
Verdict: PASS. Merge unblocked from the correctness gate once threads are resolved.
MichaelTaylor3d
commented
Jul 21, 2026
MERGE BLOCKED — security gate CHANGES-REQUIRED (custody/anti-rollback, HIGH → CRITICAL for targeted stores). Correctness gate PASSED; the independent security gate (fresh Opus) found a CONFIRMED exploit. Finding: the graceful walk→ Safe + clear: healthy minted stores (walk succeeds, never reaches fallback), the rootless walk-anchored path, no-silent-downgrade, deps, secrets — all verified clean. The defect is solely the bounded verify's non-lineage-anchored launcher binding. Remediation (release-first): root-fix in digstore-chain — lineage-anchor |
…e 4c34f0be, #1473) Bump the digstore git-rev d5e52fb -> 4c34f0be (digstore v0.19.2, fix #1473) across all four crates that pin it (dig-node-core, dig-node-service, dig-runtime, dig-wallet) so the read-root anti-rollback pin (#747/#841/#852-node) runs the HARDENED verify_pinned_root. The prior rev anchored store identity on the attacker-controllable curried SingletonStruct.launcher_id, so a forged singleton currying launcher_id == store_id (hinted to store_id) could pass as chain-verified. 4c34f0be anchors identity on the unforgeable launcher coin (coin_id == store_id) via a bounded backward parent_coin_info coin-record walk, making PR#79's read-root pin SOUND. The public signature is unchanged, so the CoinsetResolver call site needed no code change. - Regression test: CoinsetResolver::verify_pinned_root fails closed (Err, never Ok) when the chain cannot anchor the pin; deep forge-rejection coverage lives in digstore's golden_read_proof.rs (documented in the test). - SPEC §4.2: document the launcher-anchored (unforgeable) identity model. - DEVELOPMENT_LOG: the lockstep-rev gotcha across the four manifests. Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
PASS — independent correctness gate (fresh context, read-only).
Traced the read-path anchoring change against the #747/#841/#852/#1439 acceptance criteria; the diff is sound and fail-closed at every branch.
Acceptance criteria — met:
- #747/#841 — the rooted
/s/dig.getContentpin now falls back to the BOUNDEDverify_pinned_root(one launcher-hint query) when the singleton-lineage walk aborts on an unparseable intermediate generation, so a valid pinned root stays readable instead of degrading the local tier. Covered end-to-end byserve_content_plaintext_rooted_read_survives_a_broken_lineage_walk_747(positive) andget_content_rooted_read_fails_closed_when_walk_broken_and_bounded_verify_rejects_747(negative). - #852 — rootless serve resolves the chain-anchored tip and surfaces
X-Dig-Root+X-Dig-Verified(the outcome-specific branded-error half already landed in main via #78); rootless path preserved. - #1439 (read-root half) — every served generation is anchored to the chain (walk tip equality, else bounded verify); the chain is the authority.
Fail-closed discipline — verified: both rooted branches (content_serve.rs:317-345, dispatch.rs:553-574) reject with ROOT_NOT_ANCHORED (-32005) on mismatch, and the Ok(None) | Err(_) fallback serves ONLY when verify_pinned_root returns Ok. The bounded verify delegates to the hardened launcher-coin-anchored digstore_chain::singleton::verify_pinned_root (digstore 4c34f0be / v0.19.2) and propagates Err as do-not-serve (coinset_resolver.rs:75-84). served_responseX-Dig-Verified is true only under the enforced pin, and bytes reach Served only after verify_and_decrypt against the pinned root — a verify failure becomes a clean miss, never served plaintext.
Deferral of deep forge coverage to digstore's golden test — acceptable: the node consumes verify_pinned_root unchanged; the impostor-launcher rejection is a digstore-layer crypto property. The node-layer test locks the wiring (unanchorable pin fails closed).
SPEC coherence (§4.2): SPEC.md + dig-node-core/SPEC.md accurately describe the walk-then-bounded-verify fallback and the fail-closed semantics.
readable-code (§2.5): small single-purpose branches, guard clauses, intent-revealing WHY comments, no dead code (old decide_pin(true,..)=>Unpinned branch removed cleanly).
Version/gate hygiene: 0.51.1 → 0.52.0 (minor, new read capability) + dig-node-core 0.15.0; digstore rev 4c34f0be uniform across all manifests + Cargo.lock (digstore-core 0.19.2); all required checks green (Test+coverage, Clippy, Rustfmt, CodeQL, version-increment).
No blocking or major findings. This is the correctness leg; a security change of this class also requires the independent adversarial loop-decider leg to clear before merge.
Uh oh!
There was an error while loading. Please reload this page.
Landed — read-root consume (#747 / #841 / #852 dig-node half)
Two commits:
chore(deps): bump all digstore-* git-rev deps → d5e52fb (workspace 0.19.0) so digstore-chain exposes the boundedverify_pinned_root. All digstore crates moved together (one repo/version → single digstore-core 0.19.0 in the tree).fix: rooted/sserve +dig.getContentanchor the pinned root with a graceful fallback — prefer the singleton-lineage walk (it also yields the #486 owner puzzle hash), but when the walk is broken (#747 "parse next store: missing child") fall back to the bounded, fail-closedverify_pinned_root(one launcher-hint query, no walk). A valid pinned root stays readable; fix(service): anchor identity + cache under the state dir, not $HOME #127 anti-rollback preserved (stale/never-anchored →-32005 ROOT_NOT_ANCHORED). Rootless reads resolve+serve the tip and surfaceX-Dig-Root/X-Dig-Verified(#852 node half; fully closes on read/DHT #1432 + #843 E2E). AdditiveAnchoredRootResolver::verify_pinned_root(default = walk equality;CoinsetResolveroverrides with the bounded on-chain verify). SPEC §4.6/§14.4 updated.Design note (deviation from the literal instruction, rationale): a pure replacement of the walk with
verify_pinned_rootwould drop the #486 owner header on every rooted read (the walk is what carries the owner). The graceful fallback honors the intent (a broken walk must not block a valid pinned root) with NO #486 regression.Blast radius:
content_serve.rsserve_plaintext +dispatch.rsgetContent (the two pin sites) + theAnchoredRootResolvertrait (additive) +CoinsetResolver. All 301 dig-node-core lib tests green; new #747 tests: end-to-end rooted serve over a broken walk, fail-closed fallback, bounded-vs-walk contract, default fallback.NOT in this branch — fetch transport shared NatRuntime (#1439)
BLOCKED (release-first) — see issue #1439: dig-download 0.4.0 needs dig-dht ^0.4 but dig-peer-selector 0.3.0 needs ^0.3; requires an unpublished dig-peer-selector 0.4.0. Tracked separately.
Local gates green: fmt, clippy (-D warnings), 301 lib tests. Do NOT merge — orchestrator gates + merges.