Skip to content

feat(reshare): pull the whole capsule after a read so a reader becomes a holder (#1576) - #108

Merged
MichaelTaylor3d merged 5 commits into
mainfrom
feat/1576-module-reshare
Jul 26, 2026
Merged

feat(reshare): pull the whole capsule after a read so a reader becomes a holder (#1576)#108
MichaelTaylor3d merged 5 commits into
mainfrom
feat/1576-module-reshare

Conversation

@MichaelTaylor3d

@MichaelTaylor3d MichaelTaylor3d commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

What this closes

The MVP content-replication flywheel (dig_ecosystem#1576, sub-family 4):

install -> connect -> discover -> read -> CACHE THE WHOLE CAPSULE -> ANNOUNCE AS HOLDER
                          ^                                                  |
                          +--------------------------------------------------+

A resource read fetches only the bytes asked for. That makes the reader faster but the network no stronger: a .dig is served WHOLE (every retrieval key, with proofs), so a node holding one resource can serve nothing. This wires dig-download 0.8.1's ModuleDownloader onto read-completion, so a reader ends up holding — and announcing — the entire capsule.

The two production halves

dig-download's engine deliberately delegates 100% of the reshare guarantee to the consumer. This PR supplies both halves plus the serve leg:

Half File
ModuleAnchorVerifier (the root of trust) crates/dig-node-core/src/seams/dig_peer/module_anchor.rs
ModuleTransport (the network) crates/dig-node-core/src/seams/dig_peer/module_transport.rs
The reshare orchestration crates/dig-node-core/src/seams/dig_peer/module_reshare.rs
The serve leg + observability crates/dig-node-core/src/seams/dig_peer/module_serve.rs

Framing that shaped the design: every check the engine runs before the anchor gate compares attacker-chosen bytes against attacker-chosen hashes. Those prove self-consistency, never authenticity. Whatever the gate admits, this node then caches, SERVES, and ANNOUNCES itself an authoritative holder of — so a weak gate does not merely admit bad bytes locally, it makes an honest node a trusted-looking source of corrupt content network-wide.

The 10 obligations

1. Never AcceptAnyModuleAnchor in production. testkit is enabled ONLY on the dev-dependency edge (crates/dig-node-core/Cargo.toml); dev-dependency features do not propagate, so the binaries never see it. Because that protection is a manifest edit away from gone — and the edit would compile and pass every existing test — it is pinned by a test that reads the manifest: the_fail_open_anchor_verifier_is_not_reachable_from_a_production_build (crates/dig-node-core/tests/dependency_tree.rs) fails if testkit ever appears on the production entry.

2. The root comes from the CHAIN, never the serving peer. module_reshare.rs:381 resolve_chain_anchor resolves it through AnchoredRootResolver::verify_pinned_root (coinset) before any peer is contacted, and the verifier is constructed from those 32 bytes. ChainAnchoredModuleVerifier holds [u8; 32] and no resolver and no network handle, so there is no code path by which a peer answer could become the anchor — structural, not a convention. Gate at module_anchor.rs:179.

RED-proven by rejects_a_module_whose_root_is_the_one_the_serving_peer_offered: the peer serves a well-formed module and declares its root both in the module's committed section and in the pull's root argument — a fully self-consistent lie that every hash gate accepts — and it is still refused. The test also asserts the rejection reason names the chain anchor.

3. Decoded 32 bytes, never hex strings. decode_id is the only way an id enters a comparison; every comparison is over [u8; 32]. hex_case_does_not_change_the_verdict proves an upper-cased id still admits the genuine module; rejects_non_canonical_ids proves a wrong-length/non-hex id is refused rather than treated as a wildcard.

4. Unparseable and 0-byte blobs rejected explicitly. rejects_an_empty_blob, rejects_an_unparseable_blob. Both hash gates pass trivially for the empty module (the attacker declares sha256("") and it genuinely matches), so the verifier is the only check. Also rejected at the SOURCE: describe_module refuses to describe a 0-byte local file, so a truncated file is never advertised.

5. Announce ONLY on download() returning Ok. module_reshare.rs:331 — an Err discards staging and returns Refused(PullFailed) without touching the cache; the announce is at :348, reachable only after a successful promotion.

The load-bearing part is structural, not the if: this node's DHT provider records derive from CACHE INVENTORY, and a republish maintenance loop (peer.rs, run_maintenance) re-announces that inventory periodically. So a file at <cache>/modules/<store>/<root>.module IS the announcement — it would be advertised even with no explicit call. That is why the pull stages under <downloads> and only moves into the cache on success: there is no window in which a half-pulled capsule sits at the cache path, and a failed pull leaves no permanent claim. a_failed_pull_announces_nothing asserts, with an announce spy, zero announces AND no file at the cache path.

6. dial_candidates, never string concatenation. module_transport.rs dial_targets orders candidates via dig_download::dial_candidates and builds sockets via candidate_socket (parse an IpAddr, CONSTRUCT the SocketAddr). an_ipv6_candidate_is_dialable_and_bracketed uses the exact literal that blocked the read leg (::ffff:172.31.79.22, #1593); ipv6_is_dialed_before_ipv4_and_relay_only_is_last proves the §5.2 order and the relay-only fallback.

7. No peer-supplied text in errors. Reasons name the STEP in this node's own vocabulary ("getModuleInfo failed", "malformed module range frame"); ids reaching a log go through serve_log::SafeId. Nothing echoes a peer's answer upstream of the crate's Display sanitization.

8. No ModuleInfo version skew — asserted on the transitive lock. Cascade below. the_workspace_carries_exactly_one_module_wire_crate reads the resolved Cargo.lock, not Cargo.toml, because a caret dep can look correct while an intermediate consumer drags in an old major.

That test earned its keep twice. It first confirmed dig-download 0.8.0's lock held dig-rpc-protocol 0.3.1 AND 0.5.0. Then, after bumping dig-node-core to 0.5, it still failedcargo tree -i dig-rpc-protocol@0.3.1 named dig-node-service, this repo's own shell, whose pin nobody had thought to bump. A cross-repo cascade is not done until every crate in the consuming workspace has moved.

9. Re-hash the PROMOTED file before announcing. module_reshare.rs:201promote_into_cache reads the artifact off disk and compares sha256 against verifier.admitted_digest().

The reference is deliberately not the descriptor's module_hash: that is a value the serving peer chose. The anchor verifier is the only component that ever sees the fully-assembled, gate-passed bytes, so it records their digest (module_anchor.rs:102) and both sides of the comparison are this node's own. refuses_an_artifact_tampered_after_the_gate_admitted_it proves a post-finalize tamper is caught and nothing reaches the cache path; a_rejected_blob_never_becomes_the_promotion_reference proves a rejected module leaves no digest behind; refuses_to_promote_an_artifact_no_gate_admitted proves an ungated artifact cannot be promoted merely because it exists.

10. Sink::truncate. No bespoke sink was written — the pull uses dig-download's own FileSink, which implements truncate (real set_len, shrink-only, never conjures an absent file) and read_at. So there is no default to inherit: the fail-closed default cannot be reached on this path. Verified by reading the crate at v0.8.0 (src/sink.rs), whose own tests cover shrink-only and never-create. Adding a second store-backed sink would have been a second thing to keep correct for no gain.

The dig-peer cascade (release-first, both merged + live)

# Repo Change PR Version crates.io
1 dig-peer get_module_info + fetch_module_range client methods; dig-rpc-protocol 0.3 -> 0.5 #5 0.4.1 -> 0.5.0 (breaking) live
2 dig-download dig-peer 0.4.1 -> 0.5.0, collapsing the skew #17 0.8.0 -> 0.8.1 live
3 dig-node this PR 0.59.0 -> 0.60.0

dig-peer's two methods are proven over its real loopback mTLS harness, not a mock — the range test answers at a narrower frame granularity than the requested window, so a client that stopped after the first frame fails it. Both were RED first (no method named get_module_info found for struct DigPeer).

Blast radius checked

gitnexus is disabled in the loop (CLAUDE.md §2.0 temp override), so this is socraticode + ripgrep + direct reads.

Symbols edited (not added):

  • classify_request (peer.rs) — 1 production caller (serve_one_stream_from_with) + its own unit tests. Added a variant checked before the JSON-RPC arm; every pre-existing shape keeps its classification. Risk: LOW.
  • PeerRpcResponderstream_module_range is added with a fail-closed default ("not held"), so the existing implementors (FFI/base path + test stubs) need no change and cannot accidentally claim to serve modules. Risk: LOW.
  • NodeContent::new / for_dht — one added field with a OnceLock default; every existing caller compiles unchanged. Risk: LOW.
  • fetch_resource — appended a fire-and-forget call at the tail; the return value, error paths, and cache behaviour are untouched. Risk: LOW-MEDIUM (it is the read leg — mitigated by the warm being un-awaited, a no-op when unwired, and the full read-path suite staying green).
  • RpcDispatch::dispatch — two added match arms ahead of the _ fallback; no existing arm touched. Risk: LOW.

No symbol was renamed or deleted, so no call-graph-aware rename was required.

The one MEDIUM/HIGH-risk item, called out explicitly: widening the peer allowlist (peer_allowlist_is_byte_identical_to_the_pre_adoption_set). This is the #179 auth-bypass surface — the mTLS verifier accepts any well-formed self-signed leaf, so "authenticated" never means "authorized". The two additions are reads of content this node already serves at resource granularity (getModuleInfo describes a capsule whose resources getAvailability/fetchRange already expose; fetchModuleRange serves bytes of that same public, content-addressed .dig), neither mutates node state, and both are paced by the same FCFS outbound limiter as fetchRange — so the largest transfer the node serves cannot starve other peers. The guard fired on this change, which is the point: the set is listed literally so such an addition must be a reviewed edit, never an incidental consequence of a dependency bump.

Cross-repo sweep (§1.3b): dig-peer SPEC.md §3.5 (client contract + "the descriptor is not a trust anchor"), dig-node SPEC.md §21 (the full reshare contract), the shell's discovery catalogue, DEVELOPMENT_LOG.md. The dig-peer and dig-node specs state the same routing rule from both sides so they cannot drift.

Two real defects the tests caught

  1. A fail-OPEN inversion in the anchor gate. rejection_reason returns None to mean "this module IS anchored", and the first draft used ? on the Option-returning helpers — so an unparseable blob, an absent section, or a non-canonical id each returned None, i.e. ACCEPTED the module. The node would have cached, served, and announced itself a holder of a blob it could not parse. Four tests written before the implementation failed on it (rejects_an_unparseable_blob, rejects_a_short_root_section, rejects_a_module_with_no_committed_root, rejects_non_canonical_ids); every lookup now has an explicit else, and the module docs record why ? is banned there.
  2. A discovery document describing a method the node does not resolve. The catalogue marked dig.fetchModuleRange served: local, but it is stream-routed before the JSON-RPC dispatch, so handle_rpc answered -32601. Rather than exempt it, the dispatch now answers ONE frame per call — identical frame shape, so an agent can read a whole module through the plain request/response form by advancing offset (§6.2 machine-consumable).

Gates

cargo fmt --all                                                   clean
cargo clippy --workspace --all-targets --locked -- -D warnings    0 errors, 0 warnings
cargo test -p dig-node-core --lib                                 413 passed, 0 failed   (369 baseline)
cargo test --workspace --locked                                   ALL green, 0 failed

37 new tests on the reshare surface: anchor 13, reshare 8, serve 9, transport 4, dependency-tree 3 — plus 3 dispatch tests. Coverage in this repo's CI is measure-only (pre-existing, tracked separately); the new code is the most densely tested surface in the diff, so it raises the ratio rather than lowering it.

What an e2e must still prove

Unit + wire tests cannot prove the flywheel actually turns. The acceptance run (#1062 harness) needs three nodes:

  1. Node A holds a capsule. Node B (holding nothing) reads ONE resource from A -> DATA 200, merkle-verified.
  2. B's log shows capsule warm: whole capsule verified + cached; announced as a holder, and <cache>/modules/<store>/<root>.module on B is byte-identical to A's.
  3. Node C, which has never met A, discovers B via the DHT (find_providers returns B) and reads the same resource from B -> DATA 200, merkle-verified. That third node is the proof: it shows B became a genuinely authoritative holder, not merely a node with a file.
  4. Negative: point B at a holder serving a module for a different generation -> B's log shows the anchor refusal, no module at B's cache path, and C's find_providers never returns B.
  5. Confirm the warm did not slow leg 1 (compare read latency against the #836 baseline).

Closes dig_ecosystem#1576 (sub-family 4). Not to be merged until the full trio gate (reviewer + adversarial + security) has run — the reshare guarantee lives entirely in module_anchor.rs.


Round 3 — the /s/ serve door carried the same false premise (blast radius stated)

Finding closed. GET /s/*path and the router fallback passed a hardcoded ReadOrigin::Local into the serve path, justified by a comment at content_serve.rs:664 claiming the tier "only runs behind the LOCAL loopback plaintext read … never the peer wire" — the exact premise a prior round already refuted for rpc(), restated one file over. It is false: serve_content_plaintext's two production callers (server.rs's store_serve/fallback_serve) sit on the single flat Router served on every listener, and Config::bind_addr() is host.unwrap_or(127.0.0.1) with no loopback validation on parse_host_override.

With DIG_NODE_HOST=0.0.0.0, an unauthenticated GET /s/<store>:<root>/index.html (no §21 token, no mTLS; Host: localhost clears the DNS-rebinding guard, which is not an origin check) reached fetch_resource(&content, Local)spawn_capsule_reshare → whole-capsule pull → promote_into_cache → DHT holder-announce, for a capsule of the stranger's naming. This PR widened the door: pre-PR it reached only the §21-authenticated upstream sync, which can fail for want of authorization; post-PR it reached a peer-to-peer pull needing none.

The fix

One shared read_origin_for(&SocketAddr) (loopback ⇒ Local, else ⇒ Peer) is now the ONLY way a handler obtains a label; rpc() was refactored onto it. It is threaded as a parameter through serve_resourceserve_missContentServer::serve_content_plaintext (trait + impl) → peer_serve_plaintextfetch_resource, plus the three maybe_backfill_capsule sites at :432/:447/:509. The :664 comment is replaced by the derivation itself — a security label must be a parameter, never a claim written beside one, because such a claim actively suppresses the check a reviewer would otherwise perform (DEVELOPMENT_LOG.md records this).

Blast radius checked (gitnexus disabled per the standing override — done via ripgrep + direct call-graph read)

  • ContentServer::serve_content_plaintext — 2 production callers (server.rs:1209, :1263, both updated) + 10 core test call sites (all updated to pass an explicit origin).
  • peer_serve_plaintext — 1 caller (the tier-2 arm of serve_content_plaintext).
  • maybe_backfill_capsule — 5 call sites: 2 in dispatch.rs (already origin-threaded in round 2) + the 3 in content_serve.rs (fixed here).
  • fetch_resource — production callers swept; every remaining ReadOrigin::Local literal in the tree is now either the derivation itself (server.rs:646), an already-authorized loopback control call (control.rs:679), the FFI/in-process runtime (dig-runtime/src/lib.rs:113), or a test.
  • module_anchor.rsunchanged, as required.

Tests (RED-verified first, on the genuine pre-fix code)

  1. dig-node-core::serve_content_plaintext_starts_no_capsule_warm_for_a_peer_origin_read — one fixture driven twice, differing ONLY in the origin label: a Peer read is still served from the P2P tier (ServeSource::Peer, the very leg that fires the reshare, so "no warm" is a fact about the gate rather than about a read that never got far enough) yet starts no warm; the paired Local control proves a started warm is observable at all. RED before the fix: expected warm-started == false … left: true.
  2. dig-node-service::store_serve_labels_the_read_from_the_connection_not_the_endpoint + fallback_serve_labels_a_rerooted_read_from_the_connection — the REAL router, driven through tower::ServiceExt::oneshot with a forged ConnectInfo (a loopback-bound test server can never produce a non-loopback remote address), recording the label at the seam-5 boundary via AppState::with_content_server. RED before the fix: got [Local, Local] for a 203.0.113.7 reader. Recording the label rather than the outcome is deliberate: asserting "no warm" alone is satisfied identically by a guard at the wrong layer, so a relocated guard would keep such a test green; here the two arms differ only in the connection, so relocation changes the observable. The two records also cover BOTH the serve_resource read and the serve_miss index.html leg.

Coherence

SPEC.md gains normative §21.7 — Only the operator's own read may effect the network (MUST): the label is derived from the connection and nothing else, threaded as a parameter, fails closed on an unrecoverable remote address, cannot be forged by an IPv4-mapped IPv6 address, and does not override the DIG_NODE_BACKFILL_ON_MISS off switch. DEVELOPMENT_LOG.md records why the defect survived a full audit round.

Gates

cargo test --workspace1096 tests, 27 suites, 0 failures. cargo clippy --workspace --all-targets --locked -- -D warnings clean. cargo fmt --check clean. CI: all 12 checks green, Test + coverage PASS at 84.04% line coverage (82.64% region, 78.08% function) — above the 80% floor and up from the prior round's 84.01%. 0 unresolved review threads. Branch squashed to ONE conventional commit (the inherited wip: commit was redding commitlint).

No new risk is introduced by this change; the risk it REMOVES is HIGH (unauthenticated remote control over this node's bandwidth, disk, cache-eviction pressure, and DHT holder inventory).

…s a holder

Closes the MVP content-replication flywheel — install -> connect -> discover -> read -> CACHE
THE WHOLE CAPSULE -> ANNOUNCE AS HOLDER — so every read leaves the content more available than
it found it (dig_ecosystem#1576). A resource read fetches only the bytes asked for, which makes
the reader faster but the network no stronger: a `.dig` is served WHOLE, so a node holding one
resource can serve nothing.

Supplies the two production halves of dig-download 0.8.1's `ModuleDownloader`, serves both
module methods on the peer leg, and invokes the pull on read-completion.

The anchor verifier is the ONLY root of trust. Every check before it compares peer-supplied
bytes against peer-supplied hashes — self-consistency, not authenticity — and whatever it
admits, the node then caches, serves, and announces itself a holder of.

- `seams/dig_peer/module_anchor.rs` — `ChainAnchoredModuleVerifier`, built from a root resolved
  through `AnchoredRootResolver` BEFORE any peer is contacted. It holds 32 bytes and no network
  handle, so no peer answer can become the anchor. Compares decoded bytes (never hex text);
  rejects empty and unparseable blobs, since both of the engine's hash gates pass trivially for
  the empty module; rejects a module committing a different store or generation (a rollback
  primitive). Records the digest of the bytes it ADMITTED, so the promotion can be re-proven
  from outside the engine against a value no peer chose.
- `seams/dig_peer/module_transport.rs` — `NatModuleTransport` over `DigPeer`, resolving
  addresses through `dial_candidates`/`candidate_socket`: IPv6 first, sockets CONSTRUCTED from
  parsed IPs. The format-then-reparse round trip is invalid for every IPv6 literal and is what
  blocked the entire read leg. The live connected pool leads the DHT hints, and a DHT failure
  never discards a pool address.
- `seams/dig_peer/module_reshare.rs` — `CapsuleWarmer`. Stages OUTSIDE the cache, because the
  node's provider records derive from cache inventory: a file at the cache path IS its
  network-wide holder claim, and a republish loop would announce it even without an explicit
  call. Promotes only on the pull returning Ok, re-hashes the artifact against the admitted
  digest first, then announces via the node's one inventory-reconcile path.
- `seams/dig_peer/module_serve.rs` — serves both methods from local inventory. Chunk size
  scales with the module so the descriptor fits one control frame BY CONSTRUCTION, rather than
  a check that would reject an otherwise-servable capsule. Emits the #1595 serve-log outcome
  lines with sentinelled ids and no module bytes.
- `dig.getModuleInfo` + `dig.fetchModuleRange` in the dispatch and the shell's discovery
  catalogue; the peer surface routes the range method by METHOD NAME, since its response is a
  frame stream and a request shape cannot express that.

The warm is fire-and-forget: the read never waits and a failed warm never fails the read, since
a whole-capsule pull is orders of magnitude larger than the resource that revealed it. One warm
per generation at a time. A store-granularity read starts none — it does not name a generation.

`testkit` is deliberately NOT enabled on the production dig-download edge; it is what makes the
fail-OPEN `AcceptAnyModuleAnchor` nameable. `tests/dependency_tree.rs` fails if it ever appears
there, and asserts exactly ONE dig-rpc-protocol resolves. That test caught a real residual skew:
dig-node-service was still pinned to dig-rpc-protocol 0.3, so the workspace carried two
`ModuleInfo` majors even after the engine was bumped.

Deps: dig-rpc-protocol 0.3 -> 0.5, dig-download 0.7 -> 0.8.1, dig-peer 0.5 (new direct dep).

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d
MichaelTaylor3d force-pushed the feat/1576-module-reshare branch from 98d1afe to 5287caf Compare July 26, 2026 10:23
@MichaelTaylor3d MichaelTaylor3d changed the title feat(reshare): wire ModuleDownloader — a reader becomes a holder (#1576) feat(reshare): pull the whole capsule after a read so a reader becomes a holder (#1576) Jul 26, 2026
@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review July 26, 2026 10:26

@MichaelTaylor3d MichaelTaylor3d left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CHANGES-REQUIRED (correctness reviewer, fresh context)

The security core of this PR is genuinely good, and I verified all 10 acceptance obligations at the cited sites (detail below). What blocks merge is not the trust model - it is a resource-amplification defect the reshare leg newly introduces on the SERVE side, plus one missing bound on the pull side. Both are small, local fixes.

Gating

  1. module_serve.rs:110 - read_module_window reads the ENTIRE module file into memory for every 4 MiB window request. A full capsule pull is ~total/chunk requests, so one resharer pulling a 512 MiB capsule (dig-download DEFAULT_MAX_MODULE_SIZE) makes this holder perform ~512 full-file reads = ~256 GiB of disk I/O, with up to 512 MiB resident per in-flight request. The FCFS serve limiter cannot mitigate it: peer.rs acquires the limiter only AFTER read_module_window has already read + allocated the whole module, so a few-hundred-byte request buys the full allocation unpaced. Concurrency is unbounded across peers. Fix is ~10 lines: File::open + seek(SeekFrom::Start(offset)) + take(len).read_to_end.
  2. module_serve.rs:77 - same shape on describe_module: every dig.getModuleInfo reads the whole module AND SHA-256s it (plus up to 512 chunk hashes). Peer-reachable and, per this PR own allowlist note, "authenticated" does not mean "authorized" (#179) - so it is a cheap remote CPU/IO amplifier. Fix: hash streaming, and/or memoize the descriptor per (store, root, mtime).
  3. module_reshare.rs:294 - WarmRegistry dedupes per generation only; there is no global cap on concurrent warms. Reads across K distinct capsules start K concurrent pulls, each of which allocates up to max_module_size in dig-download try_zeroed_blob, i.e. K x 512 MiB. Since spawn_capsule_reshare fires automatically on every remote resource read, K is caller-influenced. Please add a semaphore (or a max-in-flight-warms config) - or state explicitly why the unbounded fan-out is acceptable.

Verified (obligations 1-10) - no findings

  • 1 tests/dependency_tree.rs:44 is non-vacuous both ways: it asserts testkit is ABSENT on the [dependencies] dig-download line (present at Cargo.toml:191) AND PRESENT on the dev entry (:265), so a manifest missing both fails. resolver = "2", so dev-dep features do not reach the binaries.
  • 2 Structurally sound. resolve_chain_anchor (module_reshare.rs:381) runs before any peer contact and goes through the real verify_pinned_root (shared/chain_view.rs:65 - a genuine tip comparison, not a permissive default); ChainAnchoredModuleVerifier holds only [u8;32] + a digest cell, no resolver and no network handle, so no peer answer can become the anchor.
  • 3 Every comparison is over [u8;32] via decode_id; no hex-string compare anywhere in the three modules.
  • 4 Empty rejected at module_anchor.rs:153 before any parse; unparseable at :167; refused at source in describe_module:79. The container-or-bare-blob fallback (:158) is a real parse, not a silent success.
  • 5 Correct and structural. Staging is under <downloads> (download.rs wire_capsule_reshare), the cache path is only ever written by promote_into_cache, and that write is write-then-rename (module_reshare.rs:210-215). Err gives discard_staging + Refused(PullFailed) with no cache touch (:331); the announce is after promotion (:348).
  • 6 No address is ever formatted: dial_targets orders via dial_candidates and builds via candidate_socket (module_transport.rs:117-124), relay-only appended last. The ::ffff:172.31.79.22 (#1593) literal is covered.
  • 7 Reasons are this node own step vocabulary; ids go through serve_log::SafeId; the transport maps peer errors with map_err(|_| ...) so no peer text propagates.
  • 8 the_workspace_carries_exactly_one_module_wire_crate reads the resolved Cargo.lock, asserts exactly one dig-rpc-protocol and that it is 0.5 - a re-pin of dig-node-service back to 0.3 (crates/dig-node-service/Cargo.toml:67) makes versions.len() == 2 and fails. Non-vacuous.
  • 9 promote_into_cache:196-203 compares against verifier.admitted_digest() (module_anchor.rs:102, written ONLY on accept), never the peer-chosen module_hash; the no-digest case fails closed.
  • 10 dig-download 0.8.1 FileSink::truncate is real (sink.rs:308, shrink-only, never conjures an absent file); the trait default is Err (sink.rs:67), so no fail-open default is reachable on this path.

Both self-caught defects are properly fixed

(a) The fail-OPEN inversion is genuinely closed: every lookup in rejection_reason uses an explicit let ... else { return Some(...) } - no ?-on-Option remains in the function. The four tests that caught it would each fail without it. The signature is still Option-shaped - see the inline note; non-gating. (b) The dig-node-service 0.3 pin is fixed and now lock-asserted.

Tests non-vacuous

  • rejects_a_module_whose_root_is_the_one_the_serving_peer_offered - a self-consistent lie (OTHER_ROOT in BOTH the committed section and the pull argument) plus a reason assertion on "chain-resolved root". Any wiring that took the anchor from the peer accepts this input; the test rejects it. Real.
  • refuses_an_artifact_tampered_after_the_gate_admitted_it - a post-gate on-disk tamper; fails for anything that trusts download() == Ok, and additionally asserts !cached.exists().
  • a_failed_pull_announces_nothing - announce SPY asserting exactly 0, plus absence of the cache dir; fails on any announce-before-promote wiring.
  • Transport: an_ipv6_candidate_is_dialable_and_bracketed, ipv6_is_dialed_before_ipv4_and_relay_only_is_last, a_failing_locator_does_not_lose_the_live_pool_address all assert real orderings, not mock symmetry.

Read-leg tail call - correct

download.rs step 7 fires spawn_capsule_reshare AFTER Ok(fetched) is assembled, un-awaited, and returns early when no warmer is installed (OnceLock empty on the FFI/base path). ContentId::Store starts no warm. Per-generation dedupe prevents a burst of resource reads from starting N pulls of the same module, and once the capsule is promoted the LOCAL serve tier answers, so fetch_resource (and therefore the warm) is not re-entered - no repeat-pull loop. The only unbounded axis is warms across DISTINCT capsules (gating item 3).

Housekeeping - all correct

Minor bump 0.59.0 -> 0.60.0 (new capability) with Cargo.toml + Cargo.lock in the same commit; dig-node-core 0.19.0 -> 0.20.0. SPEC section 21 covers the reshare contract, the announce-only-on-success rule, and 21.1 the 64 KiB control-frame ceiling that drives the chunk-size scaling. DEVELOPMENT_LOG.md updated. The four new modules read cleanly - small functions, guard-clause style, intent-revealing names, WHY-comments on every non-obvious rule, doc-comments on every public item; the promotion-ladder and dial-order module docs are exemplary. All 13 checks green, 0 unresolved threads at review time.

Coverage (non-gating): this repo measures coverage without a CI-enforced 80% floor (pre-existing, below the CLAUDE.md 2.3 bar). In practice the new code is well covered - 37 tests across anchor/reshare/serve/transport/dependency-tree plus 3 dispatch tests, covering the refusal and error branches rather than only happy paths. The real gaps are the outer wiring (wire_capsule_reshare, DhtInventoryAnnouncer, the stream_module_range frame loop), which the #1062 e2e is the right place to prove.

Re-review on push: I will re-verify each thread against the new head and resolve the ones addressed.

Comment thread crates/dig-node-core/src/seams/dig_peer/module_serve.rs Outdated
Comment thread crates/dig-node-core/src/seams/dig_peer/module_serve.rs Outdated
Comment thread crates/dig-node-core/src/seams/dig_peer/module_reshare.rs
Comment thread crates/dig-node-core/src/seams/dig_peer/module_reshare.rs
Comment thread crates/dig-node-core/src/seams/dig_peer/module_anchor.rs
Comment thread crates/dig-node-core/src/seams/dig_peer/module_transport.rs
Comment thread crates/dig-node-core/tests/dependency_tree.rs
Test-only + hardening pass on the #1576 reshare leg, folding in a P0 fetchRange
defect found in review of the same PR:

- warm() success-path test: all four pre-existing warm() tests were refusals
  (PullFailed/NoChainAnchor x2/bad id); WarmOutcome::Held was asserted nowhere.
  Add a_successful_pull_is_held_cached_and_announced_once driving the REAL
  ModuleDownloader/FileSink over dig-download's MockModuleTransport, asserting
  Held{bytes}, exactly one announce, byte-identical cache content, and staging
  discarded. Confirmed RED by sabotaging the success arm to always return
  Refused: only the new test failed.

- G1: read_module_window now seeks + reads only the requested window instead
  of `fs::read`-ing the whole module per fetchModuleRange window (avoids
  O(module_size) IO per window on a 512 MiB capsule).

- G2: describe_module memoizes the ModuleInfo descriptor keyed by
  (store, root, len, mtime), so an unchanged module is not re-read and
  re-hashed on every dig.getModuleInfo call.

- G3 + security: WarmRegistry now caps DISTINCT concurrent generations
  (skip-not-queue, default 4) on top of its existing per-generation dedup, and
  warm() short-circuits when the capsule is already held. spawn_capsule_reshare
  gains an explicit ReadOrigin (Local/Peer) threaded from every call site
  through handle_rpc/handle_rpc_json/RpcDispatch::dispatch and the peer-stream
  miss path, plus the backfill_on_miss_enabled() kill switch, so a REMOTE
  peer's fetchRange/getContent miss can never trigger a whole-capsule pull
  (unauthenticated amplification: attacker-shaped holder inventory + LRU
  eviction pressure on the operator's own content).

- P0 (#1619): dig.fetchRange's streaming loop (stream_range,
  stream_fetched_range) bounded ONLY by the resource's own `complete` flag,
  never by the caller's requested `length` — so dig-download's routine
  {offset:0, length:1} metadata probe (sent on every download) streamed the
  ENTIRE resource. Both loops now stop the instant the requested span is
  satisfied OR the resource ends, and request only the remaining span each
  iteration. Corrected two tests that pinned the old behaviour as a contract
  and added a real-wire regression test (tokio::io::duplex + read_framed) that
  a {offset:0, length:1} probe against a 10 KB resource yields exactly one
  frame; confirmed RED against the unfixed loop. SPEC.md now states the
  stream-level bound explicitly.

Blast radius checked: dig-node-core (module_reshare, module_serve, download,
peer, dispatch, content_serve, lib), dig-node-service (control, server, the
openrpc drift-guard test), dig-runtime (the FFI dispatch entry) — every
handle_rpc/handle_rpc_json/fetch_resource/miss_outcome call site in the
workspace was updated to thread ReadOrigin explicitly. cargo test --workspace,
clippy -D warnings, and fmt all green.

Closes #1619

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Pushed 97914fa addressing all outstanding gates in one push (single-writer on this branch):

Test-only (original ask): a_successful_pull_is_held_cached_and_announced_once — the missing success-path test for warm(). Confirmed RED by sabotaging the success arm to always return Refused: only this new test failed, the other 9 stayed green.

G1 (module_serve.rs read_module_window) — now File::open + seek + bounded read_exact of only the requested window, never fs::read-ing the whole module. New test with a module 3x MAX_MODULE_WINDOW proving a mid-file window reads byte-identical.

G2 (module_serve.rs describe_module) — memoized behind a (store, root) -> (len, mtime, ModuleInfo) map; a cache hit skips the read+hash entirely. New test proves the memo (not a recompute) answers an unchanged file by seeding a distinguishable sentinel and observing it returned. Also closes dig-node#109.

G3 (module_reshare.rs WarmRegistry) — added a max_concurrent cap (default 4), skip-not-queue, on top of the existing per-generation dedup. New test proves the (N+1)th distinct generation is skipped, not queued, and a freed slot re-admits.

Security finding (loop-security CHANGES-REQUIRED)spawn_capsule_reshare now requires ALL of: origin == ReadOrigin::Local (threaded explicitly from every call site — handle_rpc/handle_rpc_json/RpcDispatch::dispatch all gained an explicit origin parameter; peer.rs's NodeResponder::handle_json_rpc and its direct miss_outcome call both pass Peer; the loopback HTTP shell, control surface, and in-process FFI all pass Local), backfill_on_miss_enabled(), and (new) WarmOutcome::AlreadyHeld short-circuits warm() itself before claiming a registry slot when the cache path already exists. A remote peer's fetchRange/getContent miss can no longer trigger a whole-capsule pull, cache promotion, or DHT holder-announce.

P0 #1619 (found in the same review pass)dig.fetchRange's streaming loop (stream_range + stream_fetched_range) was bounded only by the resource's own complete flag, never by the caller's length — so dig-download's routine {offset:0, length:1} metadata probe (sent on every download) streamed the ENTIRE resource. Both loops now stop the instant the requested span is satisfied OR the resource ends. Corrected the two tests that pinned the old behaviour (a_fetch_through_serve_logs_the_real_frame_and_byte_counts, stream_range_paces_each_frame_under_a_tight_cap) plus a third I found by the same defect (an_inbound_fetch_range_logs_who_asked_for_what_and_what_was_served), added a clip-at-resource-end test, and added a REAL-WIRE regression test (tokio::io::duplex + read_framed, not a mocked/symmetric harness) proving a {offset:0,length:1} probe against a 10 KB resource yields exactly one frame — confirmed RED against the unfixed loop. SPEC.md now states the stream-level bound explicitly. Closes #1619.

Gates: cargo test --workspace (419 dig-node-core + full workspace, all green), cargo clippy --workspace --all-targets --locked -- -D warnings (clean), cargo fmt --check (clean).

Blast radius: every handle_rpc/handle_rpc_json/fetch_resource/miss_outcome/range_miss_envelope/content_miss_envelope call site in the workspace (dig-node-core, dig-node-service, dig-runtime) — all mechanically updated to thread ReadOrigin explicitly, verified by full workspace compile + test.

Version stays 0.60.0 (unreleased minor). Not merging — leaving for the gate.

@MichaelTaylor3d MichaelTaylor3d left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: CHANGES-REQUIRED. Recorded as a COMMENT review, not REQUEST_CHANGES, because the loop review token shares the PR author identity and GitHub rejects both self-approval and self-request-changes with HTTP 422. The three inline threads below are gating and must be addressed + resolved before merge.

CORRECTNESS GATE — CHANGES-REQUIRED (3 findings; G1/G2/G3 and the P0 corrections all VERIFIED)

Fresh-context correctness/completeness/test-quality/coherence review at 97914fa.
module_anchor.rs is byte-untouched since the prior round (git diff 5287caf..97914fa
lists 12 files, none of them the anchor) — the cleared trust argument stands.

Verified fixed (with how)

G1 — window-only read. module_serve.rs:158-176: File::open + metadata().len() +
seek(SeekFrom::Start(start)) + read_exact of a want-sized buffer. Edge cases traced by
hand: offset past EOF → start = offset.min(total) = total, want = ...min(total - start)
= 0 → empty Vec, and read_exact of a zero-length buffer is Ok (no error, no
wrap-around); straddling EOF → clamped by the same min(total - start); zero-length → same;
total - start can never underflow because start is clamped by min against total; a file
replaced under an OPEN handle keeps the handle inode so total and the read stay consistent,
and an in-place truncation makes read_exact fail → None → the module_unavailable_frame
path (fail-closed). reads_one_window_of_a_module_larger_than_the_window_cap (offset
MAX_MODULE_WINDOW*2+17 on a 12 MiB fixture) is the wrong-origin/off-by-one guard the small
fixtures could not be.

G2 — memoization. module_serve.rs:87-140. Key sufficiency: the path is
<cache>/modules/<store>/<root>.module, content-addressed by root, and the file is only
ever produced by write-then-rename (promote_into_cache), never edited in place — so a stale
hit would require a DIFFERENT module at the SAME root with the same byte length, i.e. a
SHA-256 collision. Coarse mtime granularity (FAT 2 s, ext3 1 s, Windows) therefore cannot
produce a stale descriptor here, because the root path component already pins the content.
an_unchanged_module_descriptor_is_served_from_the_memo is non-vacuous by construction
(sentinel-seeded memo; a recompute would recover the true hash and fail).

G3 — cross-generation cap. module_reshare.rs:106-127 SKIPS (return None), does not
queue — distinct_generations_are_capped_and_skip_rather_than_queue additionally asserts
!registry.is_warming("s:3"), so a queueing implementation would fail it. Logged at DEBUG
with generation + max_concurrent (:115-119). Released on EVERY exit path including panic
and a poisoned mutex, via impl Drop for WarmClaim (:144-152) using
unwrap_or_else(into_inner); the post-drop re-claim is asserted.

The warm() success-path test — NOT vacuous, and it is the real thing.
module_reshare.rs:717-797 drives the PRODUCTION ModuleDownloader + FileSink + the real
ChainAnchoredModuleVerifier + the real promote_into_cache; only the locator, transport
(dig-download own testkit::MockModuleTransport::serving(..., 8) — answering at an 8-byte
granularity NARROWER than any requested window, so a client that stopped after one frame would
fail), state store and announce are doubles. The fixture is a genuine .dig blob
(encode_blob with real SectionId::StoreId + CurrentRoot), so the anchor gate is genuinely
EXERCISED rather than bypassed. It asserts Held{bytes: module.len()}, spy.calls == 1, the
cache artifact byte-identical to module, and BOTH staging paths gone. Revert-check: making
the success arm Refused fails it; removing the announce fails it; skipping the promotion
fails it; staging inside the cache fails it. This closes the false-green hole the round was
opened for.

P0 #1619 — corrected to the bound, not softened. peer.rs:1281
requested_end = offset.saturating_add(length); :1296 asks for remaining (not the original
length) each iteration; :1359-1362 stops on complete || span_satisfied || this_len == 0;
identical shape in stream_fetched_range (:1505, :1509, :1523-1527). The two tests that
PINNED the overshoot are corrected to the bound with their comments rewritten to the new
premise (fetch-through log test 300/3 → 100/1; the serve-log test tail-sum/frames=2
chunk_lens[1]/frames=1), and the third (stream_range_paces_each_frame_under_a_tight_cap)
is HONESTLY rebased onto a RANGE_WINDOW+100 resource requested in full so it still observes
real pacing over 2 frames rather than being weakened. The new
a_one_byte_probe_gets_one_frame_not_the_whole_resource_over_the_real_wire is the strongest
test in the P0 half: a real tokio::io::duplex, read_framed to EOF, asserting exactly 1
frame AND complete == false — precisely the assertion that decouples "stream over" from
"resource exhausted" and forecloses a re-pin of the old premise. Swept the tree for the old
premise: the only surviving frames=3 (peer.rs:4248) is a log-forgery fixture string,
unrelated.

ReadOrigin sweep reads as a refactor, not noise. Five PRODUCTION call sites, each with a
one-line WHY: peer.rs:1173 and peer.rs:1387-1390 (→ Peer), content_serve.rs:652,
control.rs:678, server.rs:774, dig-runtime/src/lib.rs:109 (→ Local). I traced the one
label that looks wrong when read cold — content_serve.rs, whose log prose says "peer serve" —
and it is CORRECT: peer_serve_plaintext is reached only via serve_content_plaintext, whose
sole production callers are dig-node-service/src/server.rs:1202/:1256, the loopback /s
HTTP route. Everything else is test call sites. Threading is consistent; no site is
mislabelled.

SemVer. dig-node 0.60.0, dig-node-core 0.20.0. handle_rpc/handle_rpc_json/
RpcDispatch::dispatch are pub and changed incompatibly, but on a 0.x crate a MINOR bump
IS the Cargo breaking-change signal, so 0.19 → 0.20 is honest. SPEC.md:2973-2986 is
normative (MUST NOT/MUST), testable, states exactly the resolved ecosystem rule (holder MUST
NOT serve past the span; client clips defensively) and explicitly forbids re-introducing the
client-side REJECTION, so it does not contradict the dig-download SPEC.

Blocking (3)

Inline below. The first is the one that matters: a NEW security gate added in THIS round with
zero test coverage, in the exact shape (guard-not-exercised) this PR body says shipped green
nine times.

CI

Test + coverage is still PENDING at review time (run 30201165797) — coverage >=80% is
unverified, so the merge is check-blocked independently of these findings. Everything else is
green (Clippy, Rustfmt, CodeQL rust/actions/js, commitlint, version-increment, all three
installer builds).

Comment thread crates/dig-node-core/src/download.rs
Comment thread crates/dig-node-core/src/seams/dig_peer/module_serve.rs Outdated
Comment thread DEVELOPMENT_LOG.md
MichaelTaylor3d and others added 2 commits July 26, 2026 07:31
Fold in the second gating finding from the adversarial + security re-audit of
97914fa: the reshare leg's new ReadOrigin gate closed one door and left a
structurally identical pre-existing one open on the same wall.

- maybe_backfill_capsule now takes an explicit `origin: ReadOrigin` and
  early-returns unless Local (capsule_store.rs). Threaded from BOTH dispatch.rs
  call sites (the dig.fetchRange and dig.getContent miss paths) and from the
  three content_serve.rs call sites (always Local — confirmed only ever
  reachable from the loopback plaintext serve). Without this, an anonymous
  peer's `dig.fetchRange{length:1}` miss for content the victim lacks, under
  the DEFAULT MissMode::Redirect with any single provider present, reached the
  ungated call and triggered a whole-capsule pull -> cache promotion ->
  DHT holder-announce — no non-default config required, unlike the original
  finding. Regression tests use a Peer origin WITH A LIVE PROVIDER (the exact
  condition that makes the miss envelope Some and reaches the call), plus a
  Local-origin control proving the harness can observe a spawned backfill at
  all; both confirmed RED by deleting the origin check.

- ReadOrigin is now derived from the ACCEPTING CONNECTION's remote address
  (axum ConnectInfo<SocketAddr>, ip().is_loopback()), not assumed at the call
  site: an explicit DIG_NODE_HOST override replaces the loopback dual-bind
  with a non-loopback address, and the Host-header allowlist is a
  DNS-rebinding defense, not an origin one — a remote client sending
  `Host: localhost` to a non-loopback bind no longer forges Local. Every
  axum::serve/axum-server listener (localhost, ipv6, dig.local, HTTPS) now
  builds via into_make_service_with_connect_info; the two integration test
  harnesses that construct the router directly needed the same change.

- Tests that exercise spawn_capsule_reshare's OWN gate (not just the sibling
  above): a Peer-origin call and a backfill-disabled call, each independently
  confirmed RED by deleting its term, plus a Local-origin control proving the
  harness can observe a started warm at all. Uses a permanently-parked
  CapsuleWarmer (a resolver that never completes) so "did a warm start" is a
  stable fact rather than a race against a fast mock outcome.

- descriptor_memo (module_serve.rs) is now an lru::LruCache capped at 512
  entries (tens of MB ceiling regardless of how many distinct modules a peer
  has ever asked about), evicting least-recently-used. New test proves the
  cap actually evicts, confirmed RED by decoupling the cache's real capacity
  from the constant it fills against.

- DEVELOPMENT_LOG.md: the complete-means-resource-exhausted-not-span-satisfied
  lesson (#1619), the N-refusals-zero-successes test-design lesson (#1576),
  and the Windows FileStateStore filename-length sharp edge (#1639).

- SPEC.md + peer.rs doc comment: RANGE_WINDOW's 3 MiB exceeds dig-nat's real
  wire framing cap once base64 + per-frame verification metadata are counted
  (#1640 — decided cross-repo shape, not started here; dig-node's own
  independent copy of the framing defect is #1645, also not started here).
  The reshare flywheel is not claimed to work end-to-end at realistic capsule
  sizes until #1640 lands.

Gates: cargo test --workspace (all green, including the two integration test
files whose harnesses needed the ConnectInfo change), cargo clippy --workspace
--all-targets --locked -- -D warnings (clean), cargo fmt --check (clean).

Refs #1619, #1639, #1640, #1645

Co-Authored-By: Claude <noreply@anthropic.com>
Follow-up to 902138f's DEVELOPMENT_LOG.md entry — the review round asked for
the ticket number to be cited rather than left as "worth its own ticket" in a
test comment; #1639 already tracks it.

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Second-round findings addressed. HEAD is now `df18f6bf25abe65387a08edefa8cc6f5f2f3a5a7` (pushed in two commits: `902138f` for the five items, `df18f6b` citing #1639 in the dev-log entry the first push's follow-up flagged).

1. `maybe_backfill_capsule` origin threading (the sibling primitive) — now takes `origin: ReadOrigin` and refuses unless `Local` (`capsule_store.rs`), threaded from both `dispatch.rs` call sites (`dig.fetchRange`/`dig.getContent` miss paths) and all three `content_serve.rs` sites (confirmed local-only — `serve_content_plaintext` is called ONLY from `dig-node-service/server.rs`, never `peer.rs`). Regression tests use `Peer` origin with a live provider present (the exact condition that makes the miss envelope `Some` and reaches the call) plus a `Local`-origin control; both terms confirmed RED independently by deleting them.

2. `ReadOrigin` now derived from the accepting connection's remote address — `axum::extract::ConnectInfo`, `peer_addr.ip().is_loopback()`, not assumed at the call site. Every `axum::serve`/`axum-server` listener (localhost, ipv6, `dig.local`, HTTPS) now builds via `into_make_service_with_connect_info`; the two integration-test harnesses that constructed the router directly needed the identical change (both now fixed and green — 48 + 8 tests).

3. The gate itself is now exercised — `spawn_capsule_reshare` gets a `Peer`-origin test and a `backfill-disabled` test (each RED-confirmed by deleting its term) plus a `Local`-origin control proving the harness can observe a started warm at all. Uses a permanently-parked `CapsuleWarmer` (a resolver that never completes) so "did a warm start" is a stable fact, not a race against a fast mock outcome.

4. `descriptor_memo` bounded — now an `lru::LruCache` capped at 512 entries (tens of MB ceiling regardless of how many distinct modules a peer has asked about), evicting least-recently-used. New test proves the cap actually evicts (confirmed RED by decoupling the cache's real capacity from the constant it fills against).

5. `DEVELOPMENT_LOG.md` — added the `complete`-means-resource-exhausted-not-span-satisfied lesson (#1619), the N-refusals-zero-successes test-design lesson (#1576), and the Windows `FileStateStore` filename-length sharp edge citing #1639.

SPEC.md + `peer.rs` doc comment — `RANGE_WINDOW`'s 3 MiB constraint vs. `dig-nat`'s real framing cap is now stated explicitly (#1640, decided shape, not started here — dig-node's own copy of the framing defect is #1645, also not started here). The flywheel is not claimed to work end-to-end at realistic capsule sizes until #1640 lands.

Gates: `cargo test --workspace` — every crate green, including the two integration-test files the ConnectInfo change touched. `cargo clippy --workspace --all-targets --locked -- -D warnings` clean. `cargo fmt --check` clean.

CI on `902138f` (re-run since, unaffected by the doc-only follow-up): all checks green — Clippy, Rustfmt, CodeQL (actions/js/rust), version-increment, commitlint, all three platform builds, and `Test + coverage` PASSED at 84.01% line coverage (82.62% region, 78.08% function) — above the 80% floor.

0 unresolved review threads. Not merging — leaving for the gate, per the standing note this is the last round I'm addressing on this PR before a split.

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Progress — feat/1576-module-reshare

repo DIG-Network/dig-node
branch feat/1576-module-reshare
HEAD dbc591cb075df537464344bb3e93759d4a2296f4 (pushed)
written 2026-07-26T15:15:40Z

DONE
CRLF line-ending fix applied to download.rs and lib.rs (pure normalization, no content change, per coordinator's #1648 note). All prior rounds' fixes (peer-wire ReadOrigin gate, maybe_backfill_capsule origin threading, ConnectInfo-derived origin on the JSON-RPC rpc() handler, LRU-capped descriptor_memo, DEVELOPMENT_LOG.md entries) are committed and pushed as of df18f6b.

IN PROGRESS
Threading ReadOrigin through the /s/ plaintext-serve path (content_serve.rs + server.rs) is NOT STARTED. Tree currently compiles (no changes made yet beyond the CRLF fix) but the /s/ origin-gating work described in the coordinator's latest finding has not begun: store_serve/fallback_serve do not yet have ConnectInfo, serve_resource/serve_content_plaintext/peer_serve_plaintext/fetch_resource are NOT yet threaded with a derived origin for this path, the three content_serve.rs maybe_backfill_capsule call sites still hardcode ReadOrigin::Local, and the false comment at content_serve.rs ~line 664 ("this whole tier only runs behind serve_content_plaintext ... always Local") has NOT been replaced.

NEXT ACTION

  1. Add ConnectInfo(peer_addr): ConnectInfo to store_serve (server.rs ~line 1163) and fallback_serve (~line 1193), derive origin the same way rpc() does (peer_addr.ip().is_loopback() -> Local else Peer). 2) Thread that origin as a parameter through serve_resource -> Node::serve_content_plaintext -> peer_serve_plaintext -> engine.fetch_resource(&content, origin) in content_serve.rs, replacing the hardcoded ReadOrigin::Local at the ~432/443/501/664 call sites (delete the false comment at ~664, do not just add origin beside it). 3) Thread the same origin into the three maybe_backfill_capsule(store_hex, &root_hex, ...) calls in content_serve.rs (currently hardcoded ReadOrigin::Local). 4) Add ConnectInfo to server.rs's route wiring for /s/*path and the fallback route if not already covered by the shared router-level connect-info (check whether store_serve/fallback_serve are on the SAME axum Router already using into_make_service_with_connect_info from the rpc() fix -- if so ConnectInfo just needs adding as an extractor param to these two handler fns, no router change needed). 5) Add ONE test asserting a non-loopback-peer /s/ read starts no capsule warm, mirroring spawn_capsule_reshare_refuses_a_peer_origin, with a paired Local-origin control, both RED-verified by deleting the term. 6) Run cargo test --workspace + clippy --workspace --all-targets --locked -- -D warnings + cargo fmt --check, all must be green before push. 7) Verify no CRLF reintroduced (file should say plain 'Unicode text, UTF-8 text', not 'with CRLF line terminators') on any file touched.

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Progress — feat/1576-module-reshare

repo DIG-Network/dig-node
branch feat/1576-module-reshare
HEAD dbc591cb075df537464344bb3e93759d4a2296f4 (pushed)
written 2026-07-26T19:49:51Z

DONE
Took ownership of PR#108 final gating fix as sole dig-node writer. Reusing clean worktree C:/tmp/worktrees/dn-1576-reshare at PR head on feat/1576-module-reshare (verified clean, no partial edits).

IN PROGRESS
Reading content_serve.rs + server.rs at PR head to thread ConnectInfo-derived ReadOrigin through the /s/ plaintext-serve path. Nothing edited yet.

NEXT ACTION
Add ConnectInfo to store_serve (server.rs ~1163) and fallback_serve (~1193), derive loopback->Local else Peer, thread through serve_resource -> serve_content_plaintext -> peer_serve_plaintext -> fetch_resource and the three maybe_backfill_capsule sites in content_serve.rs; replace the false :664 comment with the derivation; add a RED-first non-loopback-/s/-read-starts-no-warm test with a Local control.

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Progress — feat/1576-module-reshare

repo DIG-Network/dig-node
branch feat/1576-module-reshare
HEAD 728a39dd66fea523673a7ac1b213d87321b3e157 (pushed)
written 2026-07-26T20:22:11Z

DONE
The /s/ ReadOrigin fix is implemented, committed, and pushed. store_serve + fallback_serve now take ConnectInfo and derive the label via one shared read_origin_for() (the same derivation rpc() uses); it is threaded as a parameter through serve_resource -> serve_miss -> serve_content_plaintext (trait + impl) -> peer_serve_plaintext -> fetch_resource, and into all three maybe_backfill_capsule sites. The false comment at content_serve.rs :664 is REPLACED by the derivation. Two RED-first tests: (1) dig-node-core serve_content_plaintext_starts_no_capsule_warm_for_a_peer_origin_read - a Peer-origin read is still SERVED from the P2P tier but starts no warm, with a Local control on the identical fixture proving a started warm is observable (RED evidence: warm-started was true before the fix); (2) dig-node-service store_serve_labels_the_read_from_the_connection_not_the_endpoint + fallback_serve_labels_a_rerooted_read_from_the_connection - the REAL router driven via tower oneshot with a forged ConnectInfo, recording the label at the seam-5 boundary so a relocated guard changes the observable (RED evidence: got [Local, Local] for a 203.0.113.7 reader). SPEC.md gains normative section 21.7; DEVELOPMENT_LOG.md records why the defect survived an audit round. clippy --workspace --all-targets --locked -D warnings clean; cargo fmt --check clean; target test files green.

IN PROGRESS
cargo test --workspace full run is in flight (the two touched suites already pass individually). Then re-poll gh pr checks 108 for the coverage number and resolve review threads.

NEXT ACTION
Read the workspace test result, then gh pr checks 108 --watch and report the actual coverage percentage; resolve all open review threads; hand back to the gate WITHOUT merging.

… endpoint

`GET /s/*path` and the router fallback passed a hardcoded `ReadOrigin::Local`
into the serve path, justified by a comment claiming the tier "only runs behind
the LOCAL loopback plaintext read". That premise is false: both production
callers sit on the single flat `Router` served on EVERY listener, and
`Config::bind_addr()` is `host.unwrap_or(127.0.0.1)` with no loopback validation
on the `DIG_NODE_HOST` override. With `DIG_NODE_HOST=0.0.0.0`, an
unauthenticated `GET /s/<store>:<root>/index.html` (no token, no mTLS, `Host:
localhost` clears the DNS-rebinding guard) reached `fetch_resource(.., Local)`
and so `spawn_capsule_reshare` — a whole-capsule pull, cache promotion, and DHT
holder-announce for a capsule of the STRANGER'S naming. The reshare leg widened
this: the same door previously reached only the §21-AUTHENTICATED upstream sync,
which can fail for want of authorization.

The label is now derived from the accepting connection's real remote address by
one shared `read_origin_for` (the derivation `rpc()` already used) and threaded
as a parameter through `serve_resource` -> `serve_miss` ->
`serve_content_plaintext` -> `peer_serve_plaintext` -> `fetch_resource`, plus the
three `maybe_backfill_capsule` sites. The false comment is replaced by the
derivation itself: a security label must be a parameter, never a claim beside one.

Tests (both RED-verified first): a `Peer`-origin `/s/` read is still SERVED from
the P2P tier but starts NO capsule warm, with a `Local` control over the
identical fixture proving a started warm is observable; and the real router,
driven through a forged `ConnectInfo`, labels a non-loopback reader `Peer` at
every read (`serve_resource` AND the `serve_miss` leg) while the loopback control
is `Local` — so a guard relocated to another layer changes the observable.

SPEC.md §21.7 states the rule normatively; DEVELOPMENT_LOG.md records why the
defect survived a full audit round. Also carries the CRLF line-ending
normalization for `download.rs`/`lib.rs` staged by the preceding lane.

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d
MichaelTaylor3d force-pushed the feat/1576-module-reshare branch from 728a39d to a528892 Compare July 26, 2026 20:27
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Progress — feat/1576-module-reshare

repo DIG-Network/dig-node
branch feat/1576-module-reshare
HEAD a5288929d8daf4d3f62ec3391e644f9247fd435f (pushed)
written 2026-07-26T20:52:11Z

DONE
DONE - the /s/ ReadOrigin gating fix is complete, squashed to ONE conventional commit, pushed, and fully green. store_serve + fallback_serve take ConnectInfo; one shared read_origin_for() (rpc() refactored onto it) is the only source of a label; threaded as a PARAMETER through serve_resource -> serve_miss -> serve_content_plaintext -> peer_serve_plaintext -> fetch_resource plus the three maybe_backfill_capsule sites. The false :664 comment is REPLACED by the derivation. module_anchor.rs untouched. RED evidence on the genuine pre-fix code: core test failed with 'expected warm-started == false ... left: true'; service tests failed with 'got [Local, Local]' for a 203.0.113.7 reader. GATES: cargo test --workspace 1096 tests / 27 suites / 0 failures; clippy --workspace --all-targets --locked -D warnings clean; cargo fmt --check clean; CI all 12 checks GREEN with Test + coverage PASS at 84.04% line coverage (82.64% region, 78.08% function), up from 84.01%; 0 unresolved review threads. SPEC.md gains normative 21.7; DEVELOPMENT_LOG.md records why the defect survived an audit round; PR body states the full blast radius.

IN PROGRESS
Nothing. Handing back to the gate.

NEXT ACTION
GATE DECISION ONLY - do not merge from this lane. The PR is green, squashed, and gate-ready at the head sha above. Blast radius and RED/GREEN evidence are in the PR body under 'Round 3'.

@MichaelTaylor3d
MichaelTaylor3d merged commit ce02fb3 into main Jul 26, 2026
14 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the feat/1576-module-reshare branch July 26, 2026 21:08
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