feat(reshare): pull the whole capsule after a read so a reader becomes a holder (#1576) - #108
Conversation
…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>
98d1afe to
5287caf
Compare
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
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
module_serve.rs:110-read_module_windowreads the ENTIRE module file into memory for every 4 MiB window request. A full capsule pull is ~total/chunkrequests, so one resharer pulling a 512 MiB capsule (dig-downloadDEFAULT_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.rsacquires the limiter only AFTERread_module_windowhas 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.module_serve.rs:77- same shape ondescribe_module: everydig.getModuleInforeads 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).module_reshare.rs:294-WarmRegistrydedupes 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 tomax_module_sizein dig-downloadtry_zeroed_blob, i.e. K x 512 MiB. Sincespawn_capsule_resharefires 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:44is non-vacuous both ways: it assertstestkitis ABSENT on the[dependencies]dig-download line (present atCargo.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 realverify_pinned_root(shared/chain_view.rs:65- a genuine tip comparison, not a permissive default);ChainAnchoredModuleVerifierholds 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]viadecode_id; no hex-string compare anywhere in the three modules. - 4 Empty rejected at
module_anchor.rs:153before any parse; unparseable at:167; refused at source indescribe_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 bypromote_into_cache, and that write is write-then-rename(module_reshare.rs:210-215).Errgivesdiscard_staging+Refused(PullFailed)with no cache touch (:331); the announce is after promotion (:348). - 6 No address is ever formatted:
dial_targetsorders viadial_candidatesand builds viacandidate_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 withmap_err(|_| ...)so no peer text propagates. - 8
the_workspace_carries_exactly_one_module_wire_cratereads the resolvedCargo.lock, asserts exactly onedig-rpc-protocoland that it is 0.5 - a re-pin of dig-node-service back to 0.3 (crates/dig-node-service/Cargo.toml:67) makesversions.len() == 2and fails. Non-vacuous. - 9
promote_into_cache:196-203compares againstverifier.admitted_digest()(module_anchor.rs:102, written ONLY on accept), never the peer-chosenmodule_hash; the no-digest case fails closed. - 10 dig-download 0.8.1
FileSink::truncateis real (sink.rs:308, shrink-only, never conjures an absent file); the trait default isErr(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 trustsdownload() == 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_addressall 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.
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>
|
Pushed 97914fa addressing all outstanding gates in one push (single-writer on this branch): Test-only (original ask): G1 (module_serve.rs G2 (module_serve.rs G3 (module_reshare.rs Security finding (loop-security CHANGES-REQUIRED) — P0 #1619 (found in the same review pass) — Gates: Blast radius: every Version stays 0.60.0 (unreleased minor). Not merging — leaving for the gate. |
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
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).
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>
|
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. |
Progress —
|
| 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
- 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.
Progress —
|
| 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.
Progress —
|
| 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>
728a39d to
a528892
Compare
Progress —
|
| 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'.
What this closes
The MVP content-replication flywheel (dig_ecosystem#1576, sub-family 4):
A resource read fetches only the bytes asked for. That makes the reader faster but the network no stronger: a
.digis served WHOLE (every retrieval key, with proofs), so a node holding one resource can serve nothing. This wires dig-download 0.8.1'sModuleDownloaderonto 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:
ModuleAnchorVerifier(the root of trust)crates/dig-node-core/src/seams/dig_peer/module_anchor.rsModuleTransport(the network)crates/dig-node-core/src/seams/dig_peer/module_transport.rscrates/dig-node-core/src/seams/dig_peer/module_reshare.rscrates/dig-node-core/src/seams/dig_peer/module_serve.rsFraming 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
AcceptAnyModuleAnchorin production.testkitis 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 iftestkitever appears on the production entry.2. The root comes from the CHAIN, never the serving peer.
module_reshare.rs:381resolve_chain_anchorresolves it throughAnchoredRootResolver::verify_pinned_root(coinset) before any peer is contacted, and the verifier is constructed from those 32 bytes.ChainAnchoredModuleVerifierholds[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 atmodule_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'srootargument — 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_idis the only way an id enters a comparison; every comparison is over[u8; 32].hex_case_does_not_change_the_verdictproves an upper-cased id still admits the genuine module;rejects_non_canonical_idsproves 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 declaressha256("")and it genuinely matches), so the verifier is the only check. Also rejected at the SOURCE:describe_modulerefuses to describe a 0-byte local file, so a truncated file is never advertised.5. Announce ONLY on
download()returningOk.module_reshare.rs:331— anErrdiscards staging and returnsRefused(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>.moduleIS 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_nothingasserts, with an announce spy, zero announces AND no file at the cache path.6.
dial_candidates, never string concatenation.module_transport.rsdial_targetsorders candidates viadig_download::dial_candidatesand builds sockets viacandidate_socket(parse anIpAddr, CONSTRUCT theSocketAddr).an_ipv6_candidate_is_dialable_and_bracketeduses the exact literal that blocked the read leg (::ffff:172.31.79.22, #1593);ipv6_is_dialed_before_ipv4_and_relay_only_is_lastproves 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 throughserve_log::SafeId. Nothing echoes a peer's answer upstream of the crate'sDisplaysanitization.8. No
ModuleInfoversion skew — asserted on the transitive lock. Cascade below.the_workspace_carries_exactly_one_module_wire_cratereads the resolvedCargo.lock, notCargo.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 failed —
cargo tree -i dig-rpc-protocol@0.3.1nameddig-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:201—promote_into_cachereads the artifact off disk and comparessha256againstverifier.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_itproves a post-finalize tamper is caught and nothing reaches the cache path;a_rejected_blob_never_becomes_the_promotion_referenceproves a rejected module leaves no digest behind;refuses_to_promote_an_artifact_no_gate_admittedproves an ungated artifact cannot be promoted merely because it exists.10.
Sink::truncate. No bespoke sink was written — the pull uses dig-download's ownFileSink, which implementstruncate(realset_len, shrink-only, never conjures an absent file) andread_at. So there is no default to inherit: the fail-closed default cannot be reached on this path. Verified by reading the crate atv0.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)
get_module_info+fetch_module_rangeclient methods; dig-rpc-protocol 0.3 -> 0.5dig-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.PeerRpcResponder—stream_module_rangeis 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 aOnceLockdefault; 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 (getModuleInfodescribes a capsule whose resourcesgetAvailability/fetchRangealready expose;fetchModuleRangeserves bytes of that same public, content-addressed.dig), neither mutates node state, and both are paced by the same FCFS outbound limiter asfetchRange— 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-nodeSPEC.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
rejection_reasonreturnsNoneto mean "this module IS anchored", and the first draft used?on theOption-returning helpers — so an unparseable blob, an absent section, or a non-canonical id each returnedNone, 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 explicitelse, and the module docs record why?is banned there.dig.fetchModuleRangeserved: local, but it is stream-routed before the JSON-RPC dispatch, sohandle_rpcanswered -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 advancingoffset(§6.2 machine-consumable).Gates
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:
capsule warm: whole capsule verified + cached; announced as a holder, and<cache>/modules/<store>/<root>.moduleon B is byte-identical to A's.find_providersreturns 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.find_providersnever returns B.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/*pathand the router fallback passed a hardcodedReadOrigin::Localinto the serve path, justified by a comment atcontent_serve.rs:664claiming the tier "only runs behind the LOCAL loopback plaintext read … never the peer wire" — the exact premise a prior round already refuted forrpc(), restated one file over. It is false:serve_content_plaintext's two production callers (server.rs'sstore_serve/fallback_serve) sit on the single flatRouterserved on every listener, andConfig::bind_addr()ishost.unwrap_or(127.0.0.1)with no loopback validation onparse_host_override.With
DIG_NODE_HOST=0.0.0.0, an unauthenticatedGET /s/<store>:<root>/index.html(no §21 token, no mTLS;Host: localhostclears the DNS-rebinding guard, which is not an origin check) reachedfetch_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 throughserve_resource→serve_miss→ContentServer::serve_content_plaintext(trait + impl) →peer_serve_plaintext→fetch_resource, plus the threemaybe_backfill_capsulesites at:432/:447/:509. The:664comment 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.mdrecords 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 ofserve_content_plaintext).maybe_backfill_capsule— 5 call sites: 2 indispatch.rs(already origin-threaded in round 2) + the 3 incontent_serve.rs(fixed here).fetch_resource— production callers swept; every remainingReadOrigin::Localliteral 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.rs— unchanged, as required.Tests (RED-verified first, on the genuine pre-fix code)
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: aPeerread 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 pairedLocalcontrol proves a started warm is observable at all. RED before the fix:expected warm-started == false … left: true.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 throughtower::ServiceExt::oneshotwith a forgedConnectInfo(a loopback-bound test server can never produce a non-loopback remote address), recording the label at the seam-5 boundary viaAppState::with_content_server. RED before the fix:got [Local, Local]for a203.0.113.7reader. 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 theserve_resourceread and theserve_missindex.htmlleg.Coherence
SPEC.mdgains 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 theDIG_NODE_BACKFILL_ON_MISSoff switch.DEVELOPMENT_LOG.mdrecords why the defect survived a full audit round.Gates
cargo test --workspace— 1096 tests, 27 suites, 0 failures.cargo clippy --workspace --all-targets --locked -- -D warningsclean.cargo fmt --checkclean. CI: all 12 checks green,Test + coveragePASS 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 inheritedwip: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).