Skip to content

fix(dig-node): serve tip-authoritative to close the Case-A §13 downgrade (#2211) - #192

Merged
MichaelTaylor3d merged 5 commits into
mainfrom
fix/2211-tip-authoritative-serve
Aug 6, 2026
Merged

fix(dig-node): serve tip-authoritative to close the Case-A §13 downgrade (#2211)#192
MichaelTaylor3d merged 5 commits into
mainfrom
fix/2211-tip-authoritative-serve

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

Refs DIG-Network/dig_ecosystem#2211 (interim; Case B stays open, blocked on #2203). Anti-rollback (NC-9 / #127) defense-in-depth.

The residual (from #2088)

The #2088 read path follows the §13 PublicManifest's per-path latest_root/sha256_latest to serve each path from the generation holding it; #184 chain-authenticates that latest_root is in the store's lineage (closing arbitrary substitution). But §13 is a forgeable additive section, not committed into the chain-anchored current_root (current_root = MerkleTree::from_leaves(merkle_leaves) commits only the tip generation's own leaves). So a forged §13 naming a genuine-but-superseded prior generation is honored → a downgrade served as X-Dig-Verified: true.

Two sub-cases: Case A — the path's current version is at the tip (its leaf IS committed by current_root); Case B — the current version lives in an older generation (no chain-anchored per-path fact to distinguish it). This PR closes Case A in dig-node; Case B requires the digstore #2203 change (commit §13 into current_root) and stays open on #2211.

Fix (Piece 2 — tip-authoritative serve)

The generation-resolution redirect no longer honors a §13 redirect for a path whose current bytes are committed by the tip's chain-anchored current_root: the node serves tip-first, bound purely via proof.root == tip (the chain-anchored tip), and consults the §13 redirect + expected_leafonly on a genuine tip miss (the tip capsule's constant-time decoy fall-through). A path present at the tip is served from the chain-anchored tip and the forged redirect is never reached; a legitimately-older-generation file misses at the tip and falls through to the (still §13-driven, Case-B) redirect exactly as #2088 intends.

Preserves: #2088 (older-generation-unchanged files still resolve via the fall-through), #1764/#1765 (tip serve runs the full anchor-gate / uniform enforcement; unanchored/superseded → -32005; X-Dig-SourceX-Dig-Verified), #2088 lineage-auth #184 (verify_lineage_root untouched, still gates the fall-through), and X-Dig-Generation for the fall-through case.

SPEC honesty correction (Piece 1)

SPEC.md §14.4a no longer overstates the guarantee — it now states the lineage cross-check binds served bytes to a genuine lineage generation, not the canonical/maximal one; a forged §13 naming a genuine-but-superseded prior generation is honored for a path whose current version lives in an older generation (a rollback bounded to owner-committed content, not arbitrary injection); full closure requires the per-path current-state commitment tracked in #2203.

Test (TDD, red → green)

lib.rs Case-A regression: the tip generation holds asset.js v2 (leaf committed by current_root); a §13 forged to a genuine prior-generation latest_root+sha256_latest (v1) must serve v2 (or a miss), never v1. It fails on main (forged redirect honored) and passes with this fix. The #2088-preserved + lineage-auth regressions stay green.

Blast radius

crates/dig-node-core/src/seams/content/content_serve.rs (the generation-resolution redirect reorder), crates/dig-node-core/src/lib.rs (the Case-A test + resolution), SPEC.md §14.4a, DEVELOPMENT_LOG.md. No wire/format change → no docs.dig.net / SYSTEM.md / canonical companion; §13 semantics unchanged.

Version

[workspace.package].version 0.99.7 → 0.99.8 (patch, fix: — a serve-path tightening, no wire/API change). Cargo.lock regenerated.


Generated by Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…ade (#2211)
The #2088 leaf binding + the #184 lineage cross-check bind a redirected serve
to a genuine lineage generation, but not the path's canonical one: §13 is
additive and NOT committed into the chain-anchored current_root, so a forged
tip §13 can redirect a tip-committed path at a genuine-but-superseded prior
generation and downgrade it.
Serve TIP-FIRST with no §13 leaf binding (bind purely by proof.root == tip),
and consult the §13 redirect + expected_leaf ONLY on a genuine tip miss. A
path the tip's current_root commits is served from the chain-anchored tip
(forged redirect never reached, Case A closed); an older-generation file
misses at the tip and falls through to the still-lineage-authenticated §13
redirect exactly as #2088 intends (Case B preserved). Case B rollback stays
open on #2211, blocked on the per-path current-state commitment (#2203).
Co-Authored-By: Claude <noreply@anthropic.com>
… root (#2211)
The tip-authoritative Case-A closure assumes a tip serve MISS means the path is
legitimately absent from the tip generation. That premise is unenforced: the
capsule anchor gate compares only the 32-byte CurrentRoot HEADER against the
chain, never recomputing the tree from MerkleNodes. A malicious holder can craft
a tip capsule whose header still names the genuine tip while its data is tampered
so a tip-committed path no longer folds to it -- the node admits + caches it, the
tip serve misses that path, and the forged §13 drives a redirect at a genuine-but-
superseded prior generation: the rollback Case A was meant to prevent.
Refuse a §13 redirect unless the tip capsule's own data folds to its committed
current_root (verify_module_root recomputes MerkleNodes root == committed
CurrentRoot, and that root == the chain-anchored tip). A tampered tip yields a
clean miss, never a downgrade. Placed at the §13-trust boundary so it covers a
tip capsule however it entered the cache.
Also restore the #2088 fall-through: the tip-first / §13-redirect two-pass now
DEFERS a non-Served tip outcome (a decoy miss OR an upstream error) while a
redirect candidate remains, so a tip-pass upstream error no longer pre-empts a
legitimate older-generation read.
Regression test: a tampered-tip capsule forced to miss must never serve the
rolled-back v1 (red without the gate: serves V1-OLD). The #2088 older-gen read,
Case-A honest downgrade, #184 out-of-lineage, and client-supplied-superseded
tests stay green. SPEC §14.4a corrected; DEVELOPMENT_LOG updated.
Co-Authored-By: Claude <noreply@anthropic.com>
Silences clippy::single_match on the _2211 tampered-capsule test; logic
unchanged (the gate + adversarial coverage are identical).
Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d
MichaelTaylor3d merged commit 4aa61bb into mainAug 6, 2026
15 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the fix/2211-tip-authoritative-serve branch August 6, 2026 12: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