Skip to content

feat(node): store-melt P2P propagation — verify-then-delete + convergent epidemic (#1316) - #148

Open
MichaelTaylor3d wants to merge 3 commits into
mainfrom
feat/store-melt-propagation
Open

feat(node): store-melt P2P propagation — verify-then-delete + convergent epidemic (#1316)#148
MichaelTaylor3d wants to merge 3 commits into
mainfrom
feat/store-melt-propagation

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

What changed

Implements the dig-node legs of DIG-Network/dig_ecosystem#1316 (design locked in that issue; the dig-gossip STORE_MELTED=221 wire piece already merged). Melting a store now propagates the deletion across the P2P network so peers stop hosting a melted store's content — on-chain-verified and convergent.

  • Piece feat(dig-runtime): wallet-only start + native read-crypto FFI #4 (melting/holder node): a holder tick resolves each held store's chain state; on a confirmed melt it deletes all held .dig generations (via the audited cache_remove_cached), broadcasts a signed StoreMeltedAnnounce, and tombstones the store.
  • Piece feat: throttle outgoing bandwidth and redirect saturated serves to peers #3 (receiving node): a new ingest loop off a 2nd inbound_receiver()held-check first (O(1), before any chain call) → NC-9 on-chain verify → delete-if-melted → rebroadcast exactly once via a per-store tombstone CAS. Never-held / already-tombstoned ⇒ ignored, no re-storm.
  • New additive module crates/dig-node-core/src/seams/dig_peer/store_melted.rs (pure decide_melt gate + TombstoneSet + seams + loops + 10 tests); peer.rs::bring_up_dht spawns the two loops sharing one TombstoneSet; SPEC.md §14.5. Purely additive — no existing signature/struct changed.

Security model

NC-9 (verify gates delete) — fail-CLOSED: confirm_melt maps Ok(None)⇒Melted / Ok(Some)⇒Live (never delete) / Err⇒Unknown (never delete). The held-check precedes every chain call (a store is only verified if the node holds it — this bounds verify-cost DoS and anchors the Ok(None) semantics). Signature is attribution-only, never the delete gate. §5.4-exempt (public all-peers broadcast, signed+mTLS, not recipient-sealed). §5.1-clean (deletes cached .dig; the on-chain anchor is permanent).

⚠️ Deviations from the locked plan — flagged for the gates

  1. NC-9 seam substitution (the key review target). The plan named ChainSource::resolve_singleton_lineage, which does not exist on b04dd41. The implementer used AnchoredRootResolver::anchored_root(&store_id) -> Result<Option<Bytes32>, String> (the existing chain-watch authority) with Ok(None)⇒Melted. The decider explicitly warned against treating a bare anchored_root()==Ok(None) as melt (it may be transient — "no confirmed generation yet" — rather than "singleton closed"), which if true would false-positive into wrongful deletion of a live store. The 8 tests use a spy chain, so they do not prove the real anchored_root Ok(None) semantics equal "melted." This must be verified before merge.
  2. melt_height is a 0 advisory placeholder (receivers verify on-chain, never trust it) — follow-up.
  3. Inbound signature not verified in the production loop (attribution-only; the DoS guard is the held-check + tombstone) — follow-up.

Verification

All 8 adversarial tests + 2 core green (cargo test -p dig-node-core --lib store_melted → 10 passed): live-store-never-deleted, chain-error-fail-closed, genuine-melt-deletes-all-and-rebroadcasts-once, never-held-skips-chain, already-tombstoned-inert, unheld-flood-no-chain-reads, convergence-terminates-at-holder-count, holder-tick-fail-closed-on-transient-error. cargo fmt --check → 0 · cargo clippy -p dig-node-core --all-targets --all-features -- -D warnings → 0 · full cargo test -p dig-node-core → 527 passed (the only 9 failures are the known #1961 local-only port-bind env tests).

Version

feat (new capability, backwards-compatible) → [workspace.package].version 0.75.6 → 0.76.0. Cross-repo coherence (SYSTEM.md, canonical, normative-contract NC-9, docs.dig.net) is the orchestrator's to commit on land.


Generated by Claude Code

MichaelTaylor3d and others added 3 commits August 2, 2026 17:45
First red stub for store-melt P2P propagation: the pure decide_melt
gate (only a held, on-chain-confirmed melt deletes) and the set-once
TombstoneSet that bounds each node to one broadcast per store. The 8
adversarial actuator tests + the receive/holder wiring follow.

Refs #1316
Co-Authored-By: Claude <noreply@anthropic.com>
Piece #3 (receiver) + #4 (holder) of store-melt propagation, over
spy-testable seams (MeltChain/MeltCache/MeltBroadcast) with the
fail-closed NC-9 gate: only a held, on-chain-confirmed melt deletes;
Err/Unknown never deletes; held-check precedes any chain read; the
TombstoneSet CAS bounds each node to one rebroadcast so the epidemic
terminates. All 8 adversarial tests green.

Refs #1316
Co-Authored-By: Claude <noreply@anthropic.com>
…PEC (#1316)

Spawn the store-melt receive ingest (opcode 221) beside holdings ingest
off a second inbound receiver, and a holder watch loop that deletes +
announces this node's own on-chain-melted stores — both sharing one
tombstone. Bump workspace 0.75.6 -> 0.76.0 (feat/minor) + Cargo.lock.
SPEC.md §14.5 documents the receive->verify->delete->rebroadcast
contract, the NC-9 fail-closed gate, and the §5.4 public-broadcast
exemption.

Refs #1316
Co-Authored-By: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor Author

🔴 Review gate: CHANGES-REQUIRED — the melt signal is unsound (do not merge)

The production MeltChain impl derives melt from AnchoredRootResolver::anchored_root() == Ok(None), which is doubly wrong (code-traced):

  1. A genuine melt never yields Ok(None) — it yields Err. coinset_resolver.rs:60-66 maps only "not minted"/"unspent" error strings to Ok(None); a real melt (tip singleton spent with no datastore child) resolves to Err("singleton spend did not yield a store") (singleton.rs:887-889, confirmed by owner_melts_store_on_simulator) → store_melted.rs:331 Err ⇒ Unknown ⇒ Ignore. So the feature never deletes on a real melt — it is inert against the exact event it exists to propagate.
  2. Ok(None) is the codebase's fail-closed sentinel (chain_view.rs:20-27: "not minted / no confirmed generation yet, treated as fail-closed"). For a held store it can arise transiently (external-indexer inconsistency, launcher-spend reorg), so Ok(None)⇒delete is also a wrongful-delete-of-live-data vector — exactly the transient false-positive flagged pre-merge.
  3. SPEC.md §14.5 (added in this PR) contradicts the code — it states "the melt authority is the lineage walk, NEVER a bare anchored_root() == Ok(None)," while the impl is that bare check.
  4. The 8 adversarial tests all drive a scripted ChainSpy, so none exercise the real resolver→MeltStatus mapping; findings feat: serve dig.getManifest locally from the embedded public manifest #1/ci: add PR quality gates (fmt/clippy/test/build) [#230] #2 pass the suite.

Sound as-is (keep on re-spin): decide_melt fail-closes on Live/Unknown; the receiver held-check precedes any chain call (DoS guard); the tombstone CAS gives race-free single-broadcast termination; deletion uses the audited cache_remove_cached; blast radius additive; 0.76.0 bump correct.

Required fix (direction): the MeltChain production impl must derive Melted from a positive lineage observation of a closed/melted singleton (the melt-spend terminal state), NOT from Ok(None); Ok(None) (never-minted/launcher-unspent) must map to Unknown/no-delete like the rest of the codebase. This needs a first-class resolver signal that distinguishes melted from never-minted from unreachable (the Result<Option<_>> tri-state can't — melt is currently indistinguishable from a generic Err). Add a test running a real melted-simulator lineage through the production resolver asserting Melted (and a never-minted lineage asserting no delete).

Awaiting the loop-security + adversarial gates, then this re-spins with a proper melt-detection primitive and re-runs all three gates. The green CI + green unit tests are not sufficient assurance for an irreversible network-triggered delete.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Triple gate: all three independently blocked — re-implementing the melt signal (converting to draft)

Three fresh, independent adversarial contexts reached the same verdict:

  • loop-reviewer: CHANGES-REQUIRED
  • loop-security: CHANGES-REQUIRED (+ a concrete correlated mass-deletion exploit)
  • adversarial loop-decider: REFUTED

Unanimous root cause: the production MeltChain impl derives melt from anchored_root() == Ok(None), which is inverted-broken:

  • Ok(None) = launcher-unspent / never-minted (coinset_resolver.rs:55-66digstore-chain/singleton.rs:851-853), reachable transiently for a live/nascent held store (launcher-spend reorg, or a stale/hostile shared coinset.org snapshot reporting spent=false) → wrongfully deletes live data + broadcasts a false melt network-wide, correlated across nodes.
  • A genuine meltErr("singleton spend did not yield a store") (singleton.rs:889) → Unknown → Ignore, so the feature never fires on a real melt.
  • The sibling chainwatch.rs:89 correctly treats the same Ok(None) as Skip(NoConfirmedGeneration) (benign retry).
  • All 8 tests drive a scripted ChainSpy, so none exercise the real resolver→MeltStatus mapping — the exact broken link.

Sound and unchanged on the re-spin (all three cleared these): decide_melt fail-closes on Live/Unknown; the receiver held-check precedes any chain call (DoS guard); the tombstone CAS gives race-free single-broadcast termination; deletion uses the audited path-contained cache_remove_cached; blast radius additive; §5.4/§5.1 correct.

Required fix: the melt gate must key on a melt-SPECIFIC positive confirmation — the singleton lineage terminated in an owner-authorized melt spend (launcher spent + terminal child yielding no datastore output) — NOT anchored_root() == Ok(None) (which must map to no-delete like everywhere else). Plus a test running a real melted lineage through the production resolver (what the spy tests couldn't). A fix-design decider is determining whether this is achievable dig-node-only or needs a first-class Melted verdict in digstore-chain (a dig-store change, release-first). This PR is draft until re-implemented and re-run through all three gates.

This is the triple gate doing exactly its job: green CI and green unit tests were not assurance for a network-triggered irreversible delete.


Generated by Claude Code

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