Uh oh!
There was an error while loading. Please reload this page.
fix(read): merge same-peer address hints so a reachable pool address is not shadowed (#1590, #836) - #97
Conversation
…is not shadowed (#1590, #836) The download locator unions the raw DHT discovery source with a PoolProviderLocator over the live connected pool. Both can name the SAME authenticated peer_id with DIFFERENT addresses: the DHT carries an untrusted, often stale/relayed-net advertised hint, while the pool carries the connection-verified reachable address. UnionLocator deduped by peer_id keeping the first-seen record whole and discarding later same-peer records, so the DHT's UNREACHABLE hint (seen first) shadowed the pool's REACHABLE address for the same holder. The confirm/fetch then dialed only the unreachable address, every dial was refused, dig-download reported `no providers located for the resource`, and the read fell through to §21 upstream -> DATA 404 despite a connected, dialable holder (arbiter e2e c095436, run e2e-836-arb-20260725-094734). UnionLocator now MERGES the (untrusted, MAX_ADDRS_PER_PROVIDER-capped) address hints of same-peer records across sources instead of dropping the later record. peer_id remains the authenticated identity (SPKI-pinned at connect); addresses are only reach hints, so unioning them is safe, and the reachable pool address survives so a fetchRange reaches the holder. First-seen record ORDER is preserved (dig-dht stays authoritative), and this is a no-op on the DISCOVERY union ([dht, empty, empty]) so redirect hints stay announced-holder granularity. Regression tests: - download.rs: resource_fetch_uses_the_reachable_pool_address_when_the_dht_hint_is_unreachable drives the real locator composition with an address-aware transport (fails a dial unless the record carries the reachable address); pre-fix it reproduces the exact `no providers located for ContentId::Resource` error. - union_locator.rs: same_peer_across_sources_merges_address_hints pins the merge. Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Correctness review — PASS (fresh independent context)
Verified the #836 read-leg DATA fix (address-shadowing in UnionLocator). All confirm points hold:
- Merge is correct (
union_locator.rs:76-100): same-peer_idrecords now UNION their address hints (existing.addresses.extend(record.addresses)) into the first-seen record instead of drop-later.peer_idstays the sole authenticated identity (SPKI-pinned at connect); only untrusted reach hints merge. The reachable pool address survives alongside the DHT hint. - Cap respected (
sanitize_address_hintsre-run after every extend,MAX_ADDRS_PER_PROVIDER = 8): merging can never grow a record past the #1490/#94 dial-amplification bound, even across many sources. No unbounded growth. - Discovery/redirect untouched:
download.rs:549-556composes a SEPARATE outerUnionLocator([self.locator, PoolProviderLocator])for the download path only; the discovery/redirect leg keeps the rawself.locator=SelfExcludingLocator(CapsuleFallbackLocator(UnionLocator([dht, empty, empty]))). The inner discovery union has no cross-source dups, so the merge is a genuine no-op there — redirect hints stay announced-holder granularity, unpolluted. #96 self-exclusion (both defenses), #1584, #1580 CapsuleFallback, verify-then-decrypt all unchanged. - Tests non-vacuous + RED/GREEN:
AddressAwareTransport(download.rs:1541) fails a dial unless the record carries the reachable addr — an address-only mock, so pre-fix the pool record is dropped →is_reachablefalse →fetch_resourceErr (the exact e2eno providers located for ContentId::Resourcepath); post-fix both addrs present → fetchRange served + merkle-verified.same_peer_across_sources_merges_address_hintsasserts one record, both hints survive. Both drive the real composition; helpers (anchored_mock_content,on_pool_event,mock_peer_hex) are real pre-existing testkit. - Meta: patch bump correct (0.58.2→0.58.3 / core 0.18.1→0.18.2, behaviour-fix); DEVELOPMENT_LOG records the address-shadowing gotcha + the merge-not-drop lesson; readable-code clean (intent-revealing match, WHY-comments citing #1590/#836/#1490).
dig-constants: no shared constant introduced or shadowed — MAX_ADDRS_PER_PROVIDER is a local dial-amplification bound (not a cross-repo value); addresses are runtime reach hints, not canon. Nothing belongs in / should consume dig-constants here.
Non-blocking note (resolving myself): the fix's real-world efficacy depends on the NatRangeTransport dialing BEYOND the first address in a record (DHT hint is ordered first, reachable pool addr second) — that behaviour lives in dig-nat, out of this diff, and is validated by the arbiter e2e (c095436). No action needed.
Blast radius: only union_locator.rsfind_providers (merge logic) + additive tests + version/log. detect_changes scope matches. Gates: Rustfmt/Clippy/CodeQL/version/commitlint green; Test+coverage + build jobs pending — merge gated on those going green (coverage ≥80%).
…older (#836, #1590) The download union merged same-peer address hints but kept the pool source SECOND, so the reachable :9444 was appended AFTER the stale/unreachable DHT hint. The real transport dials a SINGLE address — provider.best_address(), the first dialable candidate in list order (dig-dht record.rs) — so it still dialed the leading unreachable DHT address and the read 404'd despite a connected, dialable holder. Put PoolProviderLocator FIRST in the DOWNLOAD union (NodeContent::new): a live connection-verified pool address is strictly better than an untrusted advertised DHT hint, so it now leads the list and best_address() selects the :9444 that actually connects. Orders ONLY the download union; the discovery leg (self.locator, find_providers/redirect), #1584 self-exclusion, #1580 capsule-fallback, and verify-then-decrypt fail-closed are untouched. Rewrite AddressAwareTransport::is_reachable to model best_address() (the first dialable candidate) instead of .any() — the .any() model was the false-green trap: it passed whenever the reachable address appeared anywhere in the list, even when best_address() was the unreachable one. The test is now RED under the old append-order and GREEN once the pool leads. Strengthen the union merge test to assert first-seen order leads. Co-Authored-By: Claude <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
…(#836) (#100) * fix(read): bypass getAvailability confirm for connected-pool holders (#836) The #836 read-leg DATA miss survives the #97 address fix at the next layer: dig-download's `locate_and_confirm` keeps ONLY providers whose `dig.getAvailability` answer is `available`, dropping every other provider BEFORE any `dig.fetchRange`. On a relayed/isolated net a holder the reader is already CONNECTED to (offered by `PoolProviderLocator`) can answer availability=not-available as a FALSE NEGATIVE (cache-inventory lag, a resource-vs-capsule granularity quirk, or a transient probe failure), so the confirm drops it -> `providers.is_empty()` -> `DownloadError::NotFound` ("no providers located for ContentId::Resource {...}") -> ZERO fetchRange -> §21 upstream backfill -> 404. This is the exact e2e symptom: the availability probe dials :9444, then no fetchRange. Ground truth (ends the "key mismatch" misdiagnosis): the logged `ea12da62` IS the RESOURCE key (`ContentId::to_key()` hashes store+root+rk), and `befbbaf9` is that resource's raw retrieval_key -- same resource, no key mismatch. The locate chain (CapsuleFallback bridge + pool union + self-exclusion) resolves the holder in-process; the drop is downstream in the CONFIRM. Fix: `PoolConfirmTransport` wraps the range transport and short-circuits `query_availability` to available=true for any provider whose peer_id is in the connected pool -- a live, connection-verified holder is confirmed by the connection itself, and the whole-resource merkle verify (not the self-reported availability flag) is the real integrity gate. A DHT-only provider still goes through the real confirm; a connected non-holder fails its ranges and is dropped there (bounded, safe). Preserves #96/#97/#1584/#1580 + verify-then-decrypt fail-closed; the discovery/redirect leg is untouched. Faithful regression test `connected_pool_holder_is_fetched_even_when_it_answers_availability_false`: a connected, reachable holder that answers availability=false but WOULD serve bytes -- RED pre-fix (no fetchRange, "no providers located"), GREEN post-fix. Every prior read-leg mock answered availability=true, so none could reproduce the confirm-gate drop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SaxTyBhLj2Ry1eVQBLLotN * style(read): cargo fmt PoolConfirmTransport construction (#836) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…rce fills the cap union_locator merges the address hints of the same peer_id named by multiple sources (#836/#97), then caps the combined set at MAX_ADDRS_PER_PROVIDER. But the cap ran AFTER the merge and kept the FIRST MAX, so an earlier source that had already filled the cap with unreachable hints (a stale DHT record) silently dropped a later source's novel REACHABLE hint (the live pool connection) for the same peer (#1620). The node then dialed only stale hints and missed a dialable holder. Masked in production only by wiring the reachable pool source first — any reorder reintroduced it. merge_address_hints now reserves the cap's slots for the later source's NOVEL hints first, then leads with as many of the earlier source's hints as still fit — so the earlier HEAD keeps its lead position (#836: best_address breaks ties by list order) while only the earlier SURPLUS TAIL is displaced, never a later reachable hint. When everything fits, the order is unchanged (earlier-then-later). The shared order- preserving dedup is factored into dedup_hints; sanitize_address_hints (first-seen ingest) dedups-then-caps through it. Regression test: an earlier source filling the cap + a later source's one reachable hint -> the reachable hint survives and the earlier head still leads. The existing #836 merge/order tests stay green. union_locator suite 6/6; fmt + clippy -D warnings clean. Closes #1620 Co-Authored-By: Claude <noreply@anthropic.com>
…rce fills the cap union_locator merges the address hints of the same peer_id named by multiple sources (#836/#97), then caps the combined set at MAX_ADDRS_PER_PROVIDER. But the cap ran AFTER the merge and kept the FIRST MAX, so an earlier source that had already filled the cap with unreachable hints (a stale DHT record) silently dropped a later source's novel REACHABLE hint (the live pool connection) for the same peer (#1620). The node then dialed only stale hints and missed a dialable holder. Masked in production only by wiring the reachable pool source first — any reorder reintroduced it. merge_address_hints now reserves the cap's slots for the later source's NOVEL hints first, then leads with as many of the earlier source's hints as still fit — so the earlier HEAD keeps its lead position (#836: best_address breaks ties by list order) while only the earlier SURPLUS TAIL is displaced, never a later reachable hint. When everything fits, the order is unchanged (earlier-then-later). The shared order- preserving dedup is factored into dedup_hints; sanitize_address_hints (first-seen ingest) dedups-then-caps through it. Regression test: an earlier source filling the cap + a later source's one reachable hint -> the reachable hint survives and the earlier head still leads. The existing #836 merge/order tests stay green. union_locator suite 6/6; fmt + clippy -D warnings clean. Closes #1620 Co-Authored-By: Claude <noreply@anthropic.com>
…rce fills the cap (#130) union_locator merges the address hints of the same peer_id named by multiple sources (#836/#97), then caps the combined set at MAX_ADDRS_PER_PROVIDER. But the cap ran AFTER the merge and kept the FIRST MAX, so an earlier source that had already filled the cap with unreachable hints (a stale DHT record) silently dropped a later source's novel REACHABLE hint (the live pool connection) for the same peer (#1620). The node then dialed only stale hints and missed a dialable holder. Masked in production only by wiring the reachable pool source first — any reorder reintroduced it. merge_address_hints now reserves the cap's slots for the later source's NOVEL hints first, then leads with as many of the earlier source's hints as still fit — so the earlier HEAD keeps its lead position (#836: best_address breaks ties by list order) while only the earlier SURPLUS TAIL is displaced, never a later reachable hint. When everything fits, the order is unchanged (earlier-then-later). The shared order- preserving dedup is factored into dedup_hints; sanitize_address_hints (first-seen ingest) dedups-then-caps through it. Regression test: an earlier source filling the cap + a later source's one reachable hint -> the reachable hint survives and the earlier head still leads. The existing #836 merge/order tests stay green. union_locator suite 6/6; fmt + clippy -D warnings clean. Closes #1620 Co-authored-by: Claude <noreply@anthropic.com>
Root cause (#1590/#836 read-leg DATA blocker)
Provider-source ADDRESS shadowing in the download locator — NOT the resource→capsule granularity gap the ticket hypothesised.
The download locator is
SelfExcludingLocator(UnionLocator([raw_dht_discovery, PoolProviderLocator])). Both sources can name the SAME authenticatedpeer_idwith DIFFERENT addresses:172.31.44.121:<ephemeral>in the e2e, refused);172.31.29.67:9444).UnionLocator(crates/dig-node-core/src/seams/dig_peer/union_locator.rs) deduped bypeer_idkeeping the FIRST-SEEN record whole and discarding every later same-peer record. So the DHT's unreachable hint (seen first) SHADOWED the pool's reachable address for the same holder. The confirm step (dig.getAvailability) then dialed only the unreachable address, every dial refused, dig-download'slocate_and_confirmreturned empty →NotFound→no providers located for ContentId::Resource {…}, and the read fell through to §21 upstream rpc.dig.net → 400 → 404. Holder inboundfetchRange= 0.Evidence (arbiter e2e c095436):
fetch_resource: located providers before download … located=1 connected_pool=1, the pool locator logs it is offering the holder at:9444, yet the fetch dials172.31.44.121:<ephemeral>(the DHT hint, refused) and never172.31.29.67:9444.Fix
UnionLocatornow MERGES the (untrusted,MAX_ADDRS_PER_PROVIDER-capped) address hints of same-peer_idrecords across sources instead of dropping the later record.peer_idstays the authenticated identity (SPKI-pinned at connect); addresses are only reach hints, so unioning them is safe, and the reachable pool address survives so afetchRangereaches the holder.Why the DISCOVERY locator stays unpolluted: the merge is a no-op on the discovery union
[dht, EmptyLocator, EmptyLocator](no same-peer cross-source dups), andNodeContent::find_providers/ redirect-on-miss useself.locator(the raw discovery locator) unchanged — redirect hints stay capsule/announced-holder granularity. #96 self-exclusion (both defenses) and #1584 discovery leg are untouched; verify-then-decrypt fail-closed is unchanged.Tests (TDD red → green)
download.rs::resource_fetch_uses_the_reachable_pool_address_when_the_dht_hint_is_unreachable— drives the REAL locator composition (NodeContent::new) with an address-aware transport that fails a dial unless the record carries the reachable address, and asserts afetchRangereaches the holder over its reachable pool address + merkle-verified bytes. Pre-fix it reproduces the exactcontent not found: no providers located for ContentId::Resource {…}error.union_locator.rs::same_peer_across_sources_merges_address_hints— pins the cross-source address merge.Verification
cargo test -p dig-node-core --lib: 341 passed, 0 failed (incl. all fix(read): never self-dial on the fetch path; self-exclude the download locator (#836, #92) #96 self-exclusion + #1590 pool tests).cargo clippy -p dig-node-core --all-targets -- -D warnings: clean.cargo fmt: clean.Blast radius
UnionLocator::find_providers— callers: the download locator (NodeContent::new) and the discovery union (NodeContent::for_dht). The discovery union has no same-peer cross-source dups → behaviour unchanged there. Only the download/fetch path changes (reachable address now survives). Version bumped 0.58.2 → 0.58.3 (patch), dig-node-core 0.18.1 → 0.18.2.Resume step
Re-run
runlegs-read-arbiter.shon cached c095436 + this fix — it should now issue afetchRangeto the holder at:9444→DATA_MERKLE_VERIFIED.Do NOT merge. Does not close #836/#1586/#1572/#1062/#1590.