Skip to content

fix(sync): bind a synced capsule to its chain-anchored root before it lands (and is announced) - #138

Merged
MichaelTaylor3d merged 1 commit into
mainfrom
fix/1623-chain-anchored-verify-before-announce
Aug 2, 2026
Merged

fix(sync): bind a synced capsule to its chain-anchored root before it lands (and is announced)#138
MichaelTaylor3d merged 1 commit into
mainfrom
fix/1623-chain-anchored-verify-before-announce

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

What changed

Closes DIG-Network/dig_ecosystem#1623.

Whole-store sync landed a capsule to disk and the backfill then announced this node as a DHT holder of it — without ever binding the bytes to a chain-confirmed root. sync_module_from only ever checked the served bytes were non-empty (download_capsule trusted want_root; the clone_whole_store fallback verified emptiness only). Because landing a capsule is what makes this node a discoverable holder (SPEC §14.1), the node advertised itself as a holder of content no chain confirmed — poisoning holder reputation and multiplying unverified copies through the reshare flywheel (#1576), regardless of the downstream read-serve gate.

The synced module is now bound to the store's chain-anchored generation before it lands, using the same verification the #1576 reshare leg applies at its own seam:

  • New Node::verify_synced_capsule_is_chain_anchored — resolves the generation root through the chain (anchored_root_resolver, never from the serving peer), refuses a served root that is not the chain generation (a moved/forged head), then reuses ChainAnchoredModuleVerifier::for_generation + dig_download::ModuleAnchorVerifier::verify_module_anchor to re-hash the bytes against the anchor.
  • Called in sync_module_from immediately before write_atomic. An unverified capsule never reaches disk, and since the announce (refresh_dht_inventory) fires only on a fresh land inside cache_fetch_and_cache, no-land ⇒ no-announce by construction.
  • Fail-closed, matching #1576: no chain-confirmed generation, a chain-read failure, a moved head, or non-committing bytes all reject the sync (ModuleAnchor::UnavailableErr).

Design decision — reject, don't cache

Reject outright, matching the reshare leg (which stages outside the cache and promotes only on success). The gate is central — production reaches sync only through cache_fetch_and_cache — so every sync caller is covered by one verification shape, and an unverified module never touches the cache whose mere presence is an announcement. This is defense-in-depth: verify at the seam that admits the artifact, not reliance on the downstream serve gate that may not run before an announce.

Blast radius

sync_module_from (lib.rs) is the single funnel every sync caller reaches through cache_fetch_and_cache; the new gate is a pre-write_atomic guard on that one path. The announce (refresh_dht_inventory) and the local-serve verify (content_serve.rs, untouched — it already verifies before decrypt) are unaffected. Reuses ChainAnchoredModuleVerifier / ModuleAnchorVerifier unchanged.

Verification

  • New tests (mutation-verified):
    • a_capsule_whose_root_is_not_chain_confirmed_is_never_announced — installs an inventory-refresher spy; proves an unconfirmed capsule errs, does not land, and never announces. With the gate call removed the test FAILS for the right reason (Ok((93, "1010…")) — the capsule lands + announces); restoring it passes.
    • a_chain_confirmed_capsule_lands_and_is_announced — the fail-closed-not-fail-shut companion (a legitimate chain-confirmed capsule still lands + announces).
    • Pre-existing sync tests updated to build real chain-anchored .dig data sections (they had asserted the buggy "land unverified fixture bytes" behavior).
  • cargo fmt --all -- --check → exit 0 · cargo clippy -p dig-node-core --all-targets --all-features -D warnings → clean · all 14 sync-path tests pass. (9 pre-existing port-binding/dual-stack env tests fail identically on a clean tree — not from this diff; they pass in CI.)
  • SPEC.md §14.3 "Verify-before-announce invariant" + DEVELOPMENT_LOG.md entry added in the same unit of work.

Version

fix: (security bug fix, backwards-compatible) → patch bump [workspace.package].version 0.74.1 → 0.74.2.


Generated by Claude Code

… (#1623)

The whole-store sync (`sync_module_from` -> `cache_fetch_and_cache`) wrote a
downloaded module straight to the cache, and the mere existence of that file
makes this node a discoverable DHT holder (SPEC §14.1). The backfill then
announced the node as a holder WITHOUT ever binding the bytes to a
chain-confirmed root, so the node advertised itself as a holder of unverified
content — poisoning holder reputation and multiplying unverified copies through
the #1576 reshare flywheel.

Reuse the #1576 reshare leg's `ChainAnchoredModuleVerifier` at the sync seam:
resolve the store's chain-anchored root from the chain (never the serving peer),
re-hash the downloaded module against it, and refuse anything that is not the
store's confirmed generation BEFORE the write. An unverified capsule therefore
never lands and is never announced — matching the reshare leg, which rejects
outright rather than caching-but-not-announcing (one verification shape at every
seam that admits a capsule; defense in depth, never relying on a downstream gate).

The stale rationale on `sync_module_from` ("a tampered module fails the SERVE
gate, not this sync") no longer holds: the serve gate never runs for a peer that
only learns this node holds the capsule from its DHT announce.

The local-serve verify (content_serve) is untouched — it already verifies before
decrypt; this is about the announce/holder-reputation seam.

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d
MichaelTaylor3d merged commit fa370c5 into main Aug 2, 2026
15 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the fix/1623-chain-anchored-verify-before-announce branch August 2, 2026 08:28
Sign up for free to 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