feat(content): report peer-tier attachment on every serve and on /health (#1763) - #121
Conversation
A content read routed during the ~30 s cold-start window, before the P2P content
engine attaches, skips Tier 2 entirely and is answered by the public gateway. The
read succeeding is correct — refusing content for half a minute is worse — but
nothing on the response said the peer tier had never been consulted, so a fresh
node (or a test that started promptly) measured the gateway and recorded the
result as P2P. The defect is the silence, not the routing.
X-Dig-Source cannot carry this: a gateway serve because no peer HELD the resource
and a gateway serve because there was no peer tier YET are both 'rpc'. Add a
distinct signal that says only what it means:
* X-Dig-Peer-Tier: attached | unattached on every served response, captured once
at request entry so it describes the routing decision the read faced, applied
uniformly across whichever tier served the bytes, and never derived from
X-Dig-Source.
* GET /health -> peer_tier.attached, so a caller waits for the peer tier
deterministically instead of sleeping a guessed interval.
The name reports engine attachment and nothing else — not peer count, not
reachability, not verification. It deliberately does not repeat the overload that
makes X-Dig-Verified ambiguous (#1738).
SPEC.md states both surfaces normatively, plus the harness contract in the
integration-test launch surface (SS7.8). Bump 0.67.0 -> 0.68.0: additive header and
additive /health field, nothing removed or changed.
Closes #1763
Co-Authored-By: Claude <noreply@anthropic.com>
fd8a055 to
38da9a5
Compare
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES-REQUIRED (head read: 38da9a5ad1bb36ab1c92aab96346a91dd8f595da)
The design is right and option (c) is implemented coherently: peer_tier is captured ONCE at request entry before any tier runs (content_serve.rs:431), stamped uniformly in with_serve_metadata, and the three Served constructors are the only ones (verified: decrypt_local and peer_serve_plaintext each have exactly ONE caller, both routed through with_serve_metadata, so no Served escapes with the placeholder). The None -> Unattached mapping fails CLOSED. SPEC.md is updated in the same unit (§4.3 exposed set, new §4.6 paragraph, §6.1 health shape, §7.8 harness rule) and the §5.3 ladder is untouched. ServeProvenance is a genuine readability improvement over a sixth transposable positional. The two-arm core test serve_reports_peer_tier_attachment_independently_of_the_serving_tier is non-vacuous and does kill the source-derived impl from both directions — that one is real.
What blocks: the suite only ever asserts the unattached/false half at the two WIRE surfaces. PeerTier::Attached => "attached" and /health -> peer_tier.attached: true are asserted by no test anywhere in the repo (git grep '"attached"' finds exactly one hit, and it is the production json! line). This is the shape that has already shipped here: a suite covering one wire state stays green with the wire wrong, and X-Dig-Peer-Tier's value set is a cross-repo contract with dig-urn-resolver per SPEC §4.3. The remedy is 2–3 assertions, no design change. Details inline.
Non-gating notes posted separately and resolved by me.
Also confirmed for the orchestrator (not this repo, correctly deferred): superproject SYSTEM.md:107 is now stale on BOTH counts — it lists the provenance set as X-Dig-Verified / X-Dig-Root / X-Dig-Source and the health shape as {status, mode, version, cache}. docs.dig.net does not document the X-Dig-* set at all, so §4.3 triggers nothing there.
…(#1763) The new X-Dig-Peer-Tier / /health peer_tier signal had no test on its `attached`/`true` direction, so mutating either production value kept the whole workspace suite green: - `PeerTier::Attached => "attached"` — the only HTTP coverage exercised the `Unattached` arm, and every dig-node-service integration test builds state via `build_state`, which never attaches an engine, so the `Attached` arm was unreachable from that suite by construction. SPEC.md 4.3/4.6 make this header's value set a cross-repo contract with dig-urn-resolver. - `/health` `peer_tier.attached: true` — SPEC.md 6.1/7.8 tell every harness to poll this until true instead of sleeping, so a field stuck at `false` turns the documented wait into an infinite one with the same observable signature as the legitimate readiness window. - `x-dig-peer-tier` in `EXPOSED_DIG_HEADERS` — dropping it silently removes a browser-side resolver's read access to it. Adds a two-arm assertion on the real response builder, a two-arm assertion on the health field, a node-level `peer_tier()` assertion in the existing core attach/no-attach pair, and the missing CORS-exposure assertion. The one production change is a behaviour-preserving extraction of the health field into `peer_tier_status`, so both directions are assertable without attaching a peer network (attachment is settable only inside dig-node-core) and without widening any production visibility. Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
VERDICT: PASS — round 2, head read 090b99bf8e037e3ae255f8d21070655b9a9d8e11 (delta 38da9a5..090b99bf only; the rest of the diff was cleared in round 1).
Both gating findings are genuinely closed, and I falsified each new test rather than trusting the report:
- Finding 1 closed —
server.rs:1919 served_response_reports_both_peer_tier_wire_valuesasserts the header string for BOTH arms through the real response builder. Delete the.header("X-Dig-Peer-Tier", …)line and both arms getNone != Some(_)→ RED. Swap either literal, or pin both to one constant → RED. The control claim holds for the mutant class reachable at this layer:ServeSource::Localis constant across both arms, so aserved_responsemutant emittingif source == Rpc { "unattached" } else { "attached" }yieldsattachedtwice and fails theUnattachedarm. The source-derived mutant one layer UP (insideserve_content_plaintext) is not visible to this test and does not need to be — the core two-arm testserve_reports_peer_tier_attachment_independently_of_the_serving_tierkills it, from both directions. Non-vacuous:data.binis non-HTML so the builder reaches the header block unconditionally. - Finding 2 closed — the
peer_tier_statusextraction is behaviour-preserving: same keypeer_tier, same insert position (aftersync, before the return), byte-identicaljson!expression, and I checked all THREEstatus_fieldscallers (server.rs:520/health,:575the ws snapshot,:596the heartbeat) — every one keeps the identical field, so/ws/statusis unaffected.health_peer_tier_status_reports_both_directionspins both arms of the mapping; forcing either to a constant is RED. - Finding 3 folded in —
server.rs:2068now assertsEXPOSED_DIG_HEADERS.contains("x-dig-peer-tier"); removing the entry is RED.
No visibility was widened. peer_tier_status, served_response, ServeProvenance and status_fields are all private (no pub); the tests reach them via use super:: from the in-crate mod tests, which is the correct way to do this. StorePath was already pub in content.rs:109. Node::p2p_content() is still pub(crate) (download.rs:1316) and set_p2p_content was not touched — the implementer explicitly declined the visibility widening that a fully-composed health test would have needed, which is the right trade.
On the residual unasserted line — NOT a merge blocker; a follow-up at most. It is also strictly narrower than reported: health_reports_the_peer_tier_as_unattached_before_the_peer_network_starts drives real /health → status_fields → state.node.peer_tier() → peer_tier_status, so the composition IS covered end-to-end in the Unattached direction. What remains uncovered is the Attached direction of that one branch-free delegation, whose two operands are each pinned per-arm (Node::peer_tier() in the core test, peer_tier_status in the unit test) and whose only remaining mutant is substituting a literal for the accessor's result. Closing it would require making attachment settable from dig-node-service — trading real production encapsulation for one line of test reach, on a path the PR already has live-node evidence for (attached: false at +5 s/+10 s flipping true at ~+15 s). Accept as-is.
Gates: all 14 checks SUCCESS (0 non-success, 1 skipped release job), Test + coverage green so the 80% floor holds mechanically at the reported 83.28%. Zero unresolved review threads. Merge is unblocked from this gate's side; the orchestrator still owns the squash-merge, and the superproject SYSTEM.md:107 sweep (add X-Dig-Peer-Tier to the provenance list, refresh the /health shape) remains its action.
Closes DIG-Network/dig_ecosystem#1763.
The defect
dig-node opens its HTTP surface ~30 s before the P2P content engine attaches. A read routed in that window skips Tier 2 outright and is answered by the public gateway — and said nothing about it. A fresh node, or any test that started promptly, measured the gateway and recorded the result as P2P. That makes it a correctness bug about evidence, not routing.
The decision: (c) — serve, and expose readiness explicitly
Weighed against the alternatives:
/health→ read" harness contract (SPEC §7.8). Rejected.sleep 30from the harness.This is wire-visible, so it is stated normatively in
SPEC.mdrather than left implicit:X-Dig-Peer-Tier: attached | unattachedon every served response. Captured once at request entry, before any tier runs, so it describes the routing decision that read faced — not whether the engine happened to attach while the read was in flight. Applied uniformly across whichever tier served the bytes. MUST NOT be derived fromX-Dig-Source.GET /health→peer_tier: { attached }— the checkable wait condition.status: "ok"means live; it has never implied a usable peer tier, and now nothing has to infer that.X-Dig-Sourcecannot carry this on its own: a gateway serve because no peer held the resource and a gateway serve because there was no peer tier yet are bothrpc.On naming (#1738). That ticket's complaint is that
X-Dig-Verifiedreports enforcement while its name says verification. This header is deliberately narrow and named for exactly what it holds: whether the engine was attached. Not peer count, not reachability, not whether a fetch was attempted, and explicitly not verification. BothSPEC.mdand the enum doc say so, so a second implementation cannot widen it by accident.Blast radius checked
gitnexus is disabled in the loop (worktree hangs), so this was done with ripgrep + a direct read of every call site.
PlaintextOutcome::Servedgained a field. Constructed in 3 places (all incontent_serve.rs, all insideserve_content_plaintext's own tiers) and destructured in 3 (two arms inserver.rs, one core test). The compiler confirmed the radius: adding the field produced exactly one unmentioned-field error outside the files already being edited.with_serve_metadata— private tocontent_serve.rs, 3 call sites, all updated.served_response— private toserver.rs, 2 call sites. Its five positional provenance args were collapsed into aServeProvenancestruct rather than growing to a sixth transposable positional (it already carried#[allow(clippy::too_many_arguments)], now removed).Node::peer_tier()is additive;p2p_content()is untouched and stayspub(crate).EXPOSED_DIG_HEADERS10 → 11. This is a cross-repo contract (SPEC §4.3, mirrored in the superprojectSYSTEM.mdand consumed by dig-urn-resolver) — see "For the orchestrator" below.rpc.dig.net.Risk: LOW. Additive on both surfaces, nothing removed, renamed, or re-meant.
Tests — and why these fixtures
cold_start_gateway_serve_reports_the_peer_tier_as_unattached(dig-node-service, real HTTP). Not a mock of the window —build_stateis the same constructor the daemon uses and does not attach the engine, so the reader node is genuinely inside it. Its Tier-3 fallback is a second real dig-node answeringdig.getContentfrom its own cache, so the ciphertext, proof and chunk lengths come from the production serve path rather than being hand-built. It went RED on<header absent>after the200andx-dig-source: rpcassertions passed — which is what proves the fixture really is a cold-start gateway serve.serve_reports_peer_tier_attachment_independently_of_the_serving_tier(dig-node-core). The nearest wrong implementation ispeer_tier = if source == Rpc { Unattached } else { Attached }— indistinguishable from the real thing on any gateway-serve fixture. So this drives one capsule, locally seeded and chain-anchored, twice, varying only whether an engine is attached; both arms serveLocal. Arm 1 is Local-and-Unattached (that impl saysAttached) and arm 2 is Local-and-Attached, so it is satisfiable by neither the source-derived impl nor any constant.health_reports_the_peer_tier_as_unattached_before_the_peer_network_starts— pins the out-of-band half.Plus the existing real-provider peer-serve test now asserts
Attached, so the two reads #1763 calls indistinguishable assert opposite values.Load-bearing proof (committed first, reverted via file copy):
peer_tiercapture → constantAttachedattach=false); cold-start e2e RED/healthfield → constanttrueEvidence
Real cold start, real
dig-node runbinary, polling/health:status: okat +5 s with the peer tier down is exactly the window the ticket describes, now observable instead of inferred. The same live node confirms the header reaches the browser contract:Response-side: the two reads that were indistinguishable now assert opposite values —
rpc/unattachedon the real cold-start gateway serve,peer/attachedon the real provider fetch.Gates
cargo fmt --allclean ·cargo clippy --all-targets --locked -- -D warningsexit 0 ·cargo test --lockedfull workspace green (463 + 246 + all integration suites).Version
0.67.0 → 0.68.0(workspace),dig-node-core 0.24.0 → 0.25.0. Minor: additive response header + additive/healthfield, nothing removed or changed.For the orchestrator (superproject, not touched here)
SYSTEM.mdmirrors the exposed-X-Dig-*set as a cross-repo contract with dig-urn-resolver. It needsX-Dig-Peer-Tieradded to that list. I did not edit the superproject.Deliberately left undone
Servedoutcomes carry no peer-tier. A cold-start read that 502s or 404s is still silent about the tier.PlaintextOutcome's error variants have no such field anderror_response/not_foundare shared by unrelated paths, so that is real scope rather than a one-liner. Filed as a follow-up.