Skip to content

fix(dig-node): enforce chain-anchor uniformly across local, peer-serve, gateway paths (#1764, #1765) - #188

Merged
MichaelTaylor3d merged 3 commits into
mainfrom
fix/1764-anchor-enforcement-coherence
Aug 6, 2026
Merged

fix(dig-node): enforce chain-anchor uniformly across local, peer-serve, gateway paths (#1764, #1765)#188
MichaelTaylor3d merged 3 commits into
mainfrom
fix/1764-anchor-enforcement-coherence

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

Closes DIG-Network/dig_ecosystem#1764. Closes DIG-Network/dig_ecosystem#1765.

Unifies chain-anchor enforcement across all three content-serve paths so "chain-anchor verified" has ONE meaning. From the #1062 live demo: the same capsule was refused over local /s (502 … pinned root unverifiable) but served happily over peer-RPC dig.fetchRange (200 x-dig-source: peer), and a reachable gateway 404'd unanchored content that a peer would serve.

Root cause (single)

The read side (/s HTTP, dig.getContent, the reader whole-resource bind) all applied the #127 fail-closed anchor pin. The serve side — the dig.fetchRange peer-serve arm — applied no anchor gate: it validated request shape then served any range that Merkle-verified against the client-named root, delegating anchor authority entirely to the reader. That violated the SPEC invariant "the node MUST NOT serve content under a root it has not confirmed on-chain," which every other path honored — so a holder served unanchored/forged-generation bytes the local path refuses.

Fix (Policy A — unify on the existing strict fail-closed invariant)

  • Gate the peer-serve arm (#1764):dig.fetchRange now resolves the same anchor pin before serving. The fix(service): anchor identity + cache under the state dir, not $HOME #127 pin resolution the dig.getContent read arm used is factored into a shared resolve_enforced_pin helper and applied to both arms — one policy governs read and serve, no drift. dig.getContent behaviour is byte-identical (pure refactor there).
  • Uniform refusal of unanchored content (#1765): a store with no confirmed on-chain generation (Ok(None)) is refused with -32005 ROOT_NOT_ANCHORED uniformly on /s, dig.getContent, and dig.fetchRange. This removes the peer-serve-vs-gateway-fallback mutual-exclusivity — the serve side no longer disagrees with the read side, so the outcome is identical whether the gateway is reachable or not.
  • Provenance ⊥ verification:x-dig-source (local|peer|rpc) reports which tier served the bytes; it is orthogonal to x-dig-verified, which reports only whether the bytes were bound to the chain-anchored root. A peer- or gateway-served resource is still x-dig-verified: true because the reader re-binds to the anchor. x-dig-verified: false appears only under the DIG_NODE_PIN=off dev opt-out, now emitted consistently across legs.

Anti-rollback preserved (no #127/#2088 regression):Ok(Some(tip)) with a request root ≠ tip (a superseded/forged root — the real rollback attack) still hard-rejects -32005; Ok(None) cannot be a rollback; Err (chain unreachable) stays fail-closed. The serve arm gaining the gate only tightens. The generic dig-download crate is unchanged — the anchor policy lives in dig-node.

Tests (TDD)

  • dig.fetchRange fails closed for an unanchored store, for an unreachable chain, and for a superseded client root (rollback code).
  • Unanchored content is refused identically across dig.getContent (read) and dig.fetchRange (serve).
  • Uniform anchor refusal proven across the /s HTTP tier, peer, and gateway paths (content_serve.rs) — the #1765 face, gateway reachable vs unreachable now identical.
  • Existing fix(service): anchor identity + cache under the state dir, not $HOME #127/#2088 superseded-root + forged-latest_root regressions kept green.

Blast radius

crates/dig-node-core/src/seams/dig_rpc/dispatch.rs (dig.fetchRange + dig.getContent arms, the shared resolve_enforced_pin), crates/dig-node-core/src/lib.rs (pin resolution + tests), crates/dig-node-service/tests/content_serve.rs. Downstream x-dig-verified/x-dig-source consumers (extension/browser shields) read but do not set these headers — no cross-repo code change; the SPEC/docs/SYSTEM.md coherence sweep is a companion.

Version

root [workspace.package].version 0.99.3 → 0.99.4 (patch, fix: — tightens a serve path to the already-documented invariant; no wire/API/schema break; unanchored content that previously got a spurious 200 over peer-serve now correctly returns -32005). Cargo.lock regenerated. SPEC.md §4/§5 + the -32005 row + DEVELOPMENT_LOG updated.


Generated by Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
The peer-serve arm (dig.fetchRange) validated only request shape then served
any range that Merkle-verified against the CLIENT-named root, so a peer could
fetch bytes of a forged or superseded generation that the local /s and
dig.getContent read paths already refuse. Factor the read arm's #127 pin
resolution into a shared resolve_enforced_pin helper and apply it to BOTH the
dig.getContent read arm (pure refactor) and the dig.fetchRange serve arm (the
fix), so one policy governs read and serve with no drift.
Co-Authored-By: Claude <noreply@anthropic.com>
… paths (#1765)
Unit 2 of the anchor-enforcement-coherence work. Adds the remaining coverage +
coherence on top of the Unit 1 serve-arm gate (#1764):
- serve_content_plaintext_fails_closed_for_an_unanchored_store_1765: the local
/s HTTP tier refuses an unanchored store (Ok(None)) with ROOT_NOT_ANCHORED,
completing the uniform-refusal proof across all three serve paths.
- serve_content_plaintext_refuses_unanchored_identically_with_and_without_a_gateway_1765:
the #1765 face — an unanchored read fails closed identically whether or not a
gateway upstream is configured (the anchor gate precedes tier selection).
- cold_start_gateway_serve integration test now also asserts x-dig-verified:false
on the gateway leg under DIG_NODE_PIN=off, proving source is orthogonal to
verified (no code change needed — verified is threaded uniformly to all legs).
SPEC.md: anchor pin applies uniformly to dig.fetchRange (serve) and every read
leg; x-dig-source is orthogonal to x-dig-verified; -32005 row updated.
DEVELOPMENT_LOG.md: the fetchRange serve arm was the missing half of the invariant.
Version bump 0.99.3 -> 0.99.4 (patch, fix:).
Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d
MichaelTaylor3dforce-pushed the fix/1764-anchor-enforcement-coherence branch from 4ccae12 to 251820dCompareAugust 6, 2026 04:39
@MichaelTaylor3d
MichaelTaylor3d merged commit 3c47b1b into mainAug 6, 2026
15 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the fix/1764-anchor-enforcement-coherence branch August 6, 2026 05:05
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@claude