fix(read): resource read miss resolves the announced capsule holder (#1580)#86
Conversation
…#1580) A `/s` resource read miss builds a `ContentId::Resource` and locates providers by that exact key. But inventory is announced at STORE and CAPSULE (`store_id:root`) granularity ONLY — per-resource records are deliberately never announced (`dht::inventory_content_ids`). So the resource locate found nobody even when a holder was discoverable, Tier-2 peer fetch gave up, and the read dead-ended at the §21 whole-capsule backfill / public RPC → DATA 404 (the #836 read-leg blocker, #1062 e2e-proven on v0.56.1 where DISCOVER was already green). Add `CapsuleFallbackLocator`, wrapping the content engine's provider locator: on a `ContentId::Resource` lookup it ALSO queries the parent `ContentId::capsule(store_id, root)` — the granularity holders actually announce — and unions the holders (deduped by peer_id, resource-key hits first). A capsule holder serves every resource in it, so dig-download's next step (`dig.getAvailability` + `dig.fetchRange` for the resource) confirms and fetches from that peer. Non-resource lookups pass through unchanged; best-effort throughout. Regression: `resource_lookup_falls_back_to_the_announced_capsule_holder` (a resource lookup finds a capsule-only-announcing holder) fails without the fallback (0 found) and passes with it (1 found). Co-Authored-By: Claude <noreply@anthropic.com>
…mismatch (#1580) Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
CHANGES-REQUIRED — the fix design is correct but the Clippy gate is RED.
The CapsuleFallbackLocator logic is right: resource lookups query both the resource key and the parent ContentId::capsule(store,root), union deduped by peer_id (resource-first precedence), non-resource lookups pass through unchanged, wired at the NodeContent::for_dht seam. RED test is non-vacuous, SPEC §19.3 + DEVLOG updated, patch bump correct, no API change.
Blocker: cargo clippy --workspace --all-targets --locked -- -D warnings FAILS with 4x clippy::clone_on_copy on ContentId (which is Copy) at capsule_fallback.rs:100, 131, 185, 208 — all in the test module (.clone() on capsule/resource/content). Drop the .clone() calls (ContentId is Copy). Merge is blocked until Clippy is green.
ContentId implements Copy; .clone() is unnecessary on Copy types. Removed .clone() from four ContentId references: - Line 100: content.clone() -> *content - Line 131: capsule.clone() -> capsule - Line 185: resource.clone() -> resource - Line 208: capsule.clone() -> capsule Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Root cause (lead (a) confirmed; (b) ruled out)
The #836 read-leg DATA 404 (#1580, #1062 e2e-proven on v0.56.1 where CONNECT + DISCOVER were already green).
A
/sresource read miss (serve_content_plaintext→peer_serve_plaintext→download::miss_content_for) builds aContentId::Resource { store, root, retrieval_key }and hands it to dig-download, whose discover step (locate_and_confirm) locates providers by that EXACT resource key. But inventory is announced at STORE and CAPSULE (store_id:root) granularity ONLY — per-resource provider records are deliberately never announced (dht::inventory_content_ids, dht.rs:319: a capsule holder serves every resource in it, so per-resource records would be redundant and explode DHT write volume).Result:
find_providers(resource_id)returned nobody even though the capsule holder was discoverable → Tier-2 peer fetch gave up → the read fell through to the §21 whole-capsule backfill / public RPC and 404'd. The holder saw no inboundfetchRange.Lead (b) — path ordering — was checked and is NOT the fault:
serve_content_plaintextalready runs Tier-2 peer fetch (line 417) BEFORE Tier-3 RPC (line 438);maybe_backfill_capsuleis a background warm, not a preferred path.Fix
New
CapsuleFallbackLocator(crates/dig-node-core/src/seams/dig_peer/capsule_fallback.rs) wrapping the content engine's provider locator inNodeContent::for_dht. On aContentId::Resourcelookup it ALSO queries the parentContentId::capsule(store_id, root)— the announced granularity — and unions the holders (deduped bypeer_id, resource-key hits first). dig-download then confirms + fetches the specific resource from the capsule holder viadig.getAvailability+dig.fetchRange. Non-resource lookups pass through unchanged; best-effort (an error from either query never removes what the other found).No dig-download or dig-dht API change needed — the bridge lives entirely in dig-node.
Blast radius (socraticode + read; gitnexus disabled per §2.0 override)
NodeContent::for_dhtlocator wiring — the only construction site; the FFI/in-process path never attaches a content engine, so unaffected.download::miss_content_for/peer_serve_plaintextunchanged.Tests
resource_lookup_falls_back_to_the_announced_capsule_holderfinds 0 holders with a pass-through stub, 1 with the fallback.cargo test -p dig-node-core --lib download::(18) +capsule_fallback(3) green; fmt + clippy clean.Version
patch 0.56.1 → 0.56.2 (behaviour-preserving-for-existing-paths content-read fix).
Correctness tier
read / content-fetch (not custody). Verify-then-decrypt fail-closed path is untouched.
Closes #1580