Skip to content

fix(dig-node): rooted /s read survives a broken lineage walk (#747, #841) - #79

Merged
MichaelTaylor3d merged 3 commits into
mainfrom
feat/1439-fetch-transport-shared-runtime
Jul 22, 2026
Merged

fix(dig-node): rooted /s read survives a broken lineage walk (#747, #841)#79
MichaelTaylor3d merged 3 commits into
mainfrom
feat/1439-fetch-transport-shared-runtime

Conversation

@MichaelTaylor3d

@MichaelTaylor3dMichaelTaylor3d commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Landed — read-root consume (#747 / #841 / #852 dig-node half)

Two commits:

  1. chore(deps): bump all digstore-* git-rev deps → d5e52fb (workspace 0.19.0) so digstore-chain exposes the bounded verify_pinned_root. All digstore crates moved together (one repo/version → single digstore-core 0.19.0 in the tree).
  2. fix: rooted /s serve + dig.getContent anchor 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-closed verify_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 surface X-Dig-Root/X-Dig-Verified (#852 node half; fully closes on read/DHT #1432 + #843 E2E). Additive AnchoredRootResolver::verify_pinned_root (default = walk equality; CoinsetResolver overrides 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_root would 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.rs serve_plaintext + dispatch.rs getContent (the two pin sites) + the AnchoredRootResolver trait (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.

…) 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>
@MichaelTaylor3d
MichaelTaylor3dforce-pushed the feat/1439-fetch-transport-shared-runtime branch from 797b9d5 to 4dc9bd3CompareJuly 21, 2026 21:45
@MichaelTaylor3dMichaelTaylor3d changed the title feat(dig-node): read-root /s serve via verify_pinned_root + fetch transport shared NatRuntime (#1439, #747, #841, #852)fix(dig-node): rooted /s read survives broken lineage walk via bounded verify_pinned_root (#747, #841, #852 node half)Jul 21, 2026
…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>
@MichaelTaylor3d
MichaelTaylor3dforce-pushed the feat/1439-fetch-transport-shared-runtime branch from 13b9ad6 to c25baa8CompareJuly 21, 2026 22:17
@MichaelTaylor3dMichaelTaylor3d changed the title fix(dig-node): rooted /s read survives broken lineage walk via bounded verify_pinned_root (#747, #841, #852 node half)fix(dig-node): rooted /s read survives a broken lineage walk (#747, #841)Jul 21, 2026

@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.

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 /s read whose lineage walk aborts ("parse next store: missing child") STILL serves via the bounded verify_pinned_root fallback. Proven end-to-end by serve_content_plaintext_rooted_read_survives_a_broken_lineage_walk_747 (lib.rs ~5320) and the RPC path by the dig.getContent broken-walk tests (lib.rs ~4272/4306). Fallback is fail-closed: content_serve.rs and dispatch.rs only serve Some(req) when the walk's tip matches OR the bounded verify returns Ok; a genuine walk tip-mismatch (Ok(Some(state)) != req) still rejects with ROOT_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_state before 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 d5e52fb in 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):

  1. lib.rs shows 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=lf so it doesn't re-churn and bury future diffs.
  2. 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.
  3. 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

Copy link
Copy Markdown
ContributorAuthor

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→verify_pinned_root fallback is fail-closed in structure but defers to a forgeable verify. digstore_chain::verify_pinned_root (d5e52fb, singleton.rs:1455-1507) anchors on unspent_coins_by_hint(store_id) (attacker-controllable memo) + reads launcher_id from the tip's CURRIED SingletonArgs (also attacker-controllable) — with NO check that the lineage descends from the unforgeable launcher coin (coin_id==store_id). The launcher_id == store_id guard compares the CURRIED value. Exploit: for any store whose genuine walk returns Ok(None)/Err (not-minted OR #747 broken-lineage — exactly the fallback's target set), an attacker creates a dust impostor singleton curried with launcher_id=store_id + root=R_evil + a store_id hint memo; the fallback returns Ok → node serves attacker content with X-Dig-Verified: true. Net REGRESSION: those stores were fail-closed before this PR.

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 verify_pinned_root to the unforgeable launcher coin (verify coin_record(store_id) spent + the unspent tip descends from its eve, not merely a curried launcher_id match). Interim for dig-node: keep the broken-walk path FAIL-CLOSED, or serve it as X-Dig-Verified: false (never overclaim chain-anchoring). Filing the digstore-chain fix + a decider on PR#79's shape (split safe read-root parts vs hold). Do not merge until resolved.

…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>

@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.

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.getContent pin now falls back to the BOUNDED verify_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 by serve_content_plaintext_rooted_read_survives_a_broken_lineage_walk_747 (positive) and get_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.

@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review July 22, 2026 01:39
@MichaelTaylor3d
MichaelTaylor3d merged commit c49c1cf into mainJul 22, 2026
13 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the feat/1439-fetch-transport-shared-runtime branch July 22, 2026 01:39
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.

1 participant

@MichaelTaylor3d