Uh oh!
There was an error while loading. Please reload this page.
fix(dig-node): gate inbound-demand pull on XOR-proximity admission (anti-amplification) - #165
Merged
Merged
Conversation
Co-Authored-By: Claude <noreply@anthropic.com>
…nti-amplification) Wire the intended anti-amplification defence for the tier-1 inbound-demand pull (DIG_NODE_INBOUND_DEMAND_CACHE, #2014): admit a peer-driven capsule pull ONLY when the capsule key lies in this node's keyspace neighbourhood (XOR proximity to our own peer_id >= the keyspace midpoint), reusing relevance::xor_proximity and the same reference peer_id the tier-0 selector scores against. Binds even when the flag is on; fails closed without a known self-identity or a concrete (store, root). Default stays OFF. Read serving is unchanged — the gate governs only the demand-driven cache. Minor: root workspace 0.89.0->0.90.0, dig-node-core 0.38.0->0.39.0. Closes #2014 Co-Authored-By: Claude <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
MichaelTaylor3d added a commit
that referenced
this pull request
Aug 5, 2026
…back for the redirect (#2007) Compose the shipped redirect-on-miss (#165) + upstream removal (#1997) machinery with the three residual units of dig_ecosystem#2007: - Unit A: a per-requestor token-bucket limiter (`crate::rate_limit`, a byte-identical mirror of dig-wallet's #1957 primitive) in front of `miss_outcome`'s DHT lookup + proxy fetch, keyed by mTLS peer_id / connection IP / operator. Over-budget → -32009 CONTENT_MISS_RATE_LIMITED; a different requestor is unaffected. Redirect candidate set capped at MAX_REDIRECT_PROVIDERS (= dig-dht MAX_ADDRESSES_PER_RECORD). - Unit B: an explicit `params.proxy` (default OFF) routes a miss through the existing FetchThrough branch + identical chain-anchored merkle-verified fetch_resource; the origin!=Local reshare refusal stays intact (the middle node never becomes a holder). - Unit C: regression proving an unconfigured node (no upstream) redirects a miss and issues zero upstream HTTP, degrading old clients to a well-formed JSON-RPC error. Threads the requestor identity explicitly through dispatch + handle_rpc_as + handle_json_rpc(conn_key) so a peer JSON miss is keyed by the asking peer, not one shared bucket. SPEC §10.4 + the -32009 catalogue entry document the bounds, the proxy semantics, and the privacy note. Minor bump (additive `proxy` field + rate limit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmFawKNiEo1dowZ4mPM19j
MichaelTaylor3d added a commit
that referenced
this pull request
Aug 5, 2026
…back for the redirect (#2007) (#182) * feat(node): rate-limit the miss→DHT-lookup path + explicit proxy fallback for the redirect (#2007) Compose the shipped redirect-on-miss (#165) + upstream removal (#1997) machinery with the three residual units of dig_ecosystem#2007: - Unit A: a per-requestor token-bucket limiter (`crate::rate_limit`, a byte-identical mirror of dig-wallet's #1957 primitive) in front of `miss_outcome`'s DHT lookup + proxy fetch, keyed by mTLS peer_id / connection IP / operator. Over-budget → -32009 CONTENT_MISS_RATE_LIMITED; a different requestor is unaffected. Redirect candidate set capped at MAX_REDIRECT_PROVIDERS (= dig-dht MAX_ADDRESSES_PER_RECORD). - Unit B: an explicit `params.proxy` (default OFF) routes a miss through the existing FetchThrough branch + identical chain-anchored merkle-verified fetch_resource; the origin!=Local reshare refusal stays intact (the middle node never becomes a holder). - Unit C: regression proving an unconfigured node (no upstream) redirects a miss and issues zero upstream HTTP, degrading old clients to a well-formed JSON-RPC error. Threads the requestor identity explicitly through dispatch + handle_rpc_as + handle_json_rpc(conn_key) so a peer JSON miss is keyed by the asking peer, not one shared bucket. SPEC §10.4 + the -32009 catalogue entry document the bounds, the proxy semantics, and the privacy note. Minor bump (additive `proxy` field + rate limit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmFawKNiEo1dowZ4mPM19j * fix(node): bound getAvailability miss→DHT lookups per requestor (#2007) The dig.getAvailability not-held → find_providers enrichment was the largest DHT-amplification vector on the miss path and ran with NO per-requestor rate cap: one peer session could send a 512-item batch of distinct valid store ids, none held, and drive 512 independent DHT lookups from a ~50 KB request. The candidate cap bounds the RESPONSE, never the outbound LOOKUP work. Thread RequestorId into availability_batch/availability_answer from both entry points (the dispatch.rs JSON leg's in-scope requestor; the peer typed-frame leg's mTLS-verified conn_key as RequestorId::Peer) and gate each per-item find_providers behind the SAME per-requestor miss-lookup budget the single-item legs use — ONE token per not-held item that would trigger a lookup. When the bucket is exhausted the remaining items answer not-available without a lookup; the availability answer itself (held vs not-held from local inventory) is unchanged, only the best-effort providers hint is dropped. So the number of find_providers lookups one requestor can cause via getAvailability — across any batch size and any call rate — is bounded by its per-requestor token budget, identical to the single-item legs. Adds get_availability_enrichment_is_rate_limited_per_item_per_requestor (two actor, control-peer isolation, budget pinned from both sides, load-bearing against both once-per-batch and no-check mutations). SPEC §10.4 + rate_limit.rs module doc extended to include getAvailability in the bounded-path set. Version: workspace 0.99.0, dig-node-core 0.44.0 (exceed main's independent 0.98.0/0.43.0 so the version-increment gate passes post-rebase onto #179). Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes dig_ecosystem#2014. Follow-up from epic #1934 child 5 (#1990); the dependency (the XOR-proximity primitive) is now live post-#1934.
Why
The tier-1 inbound-demand pull (
DIG_NODE_INBOUND_DEMAND_CACHE) lets a remote peer's read drive this node to fetch + cache + DHT-announce a whole.digcapsule — a classic amplification primitive (a peer could make the node cache + advertise arbitrary attacker-chosen content). It was opt-in default-OFF precisely because the defence wasn't wired. This wires it: gate the pull on XOR-proximity (keyspace distance) so a peer can only drive caching of content NEAR this node'speer_id— ungameable (a peer can't move ourpeer_id).The default stays OFF — flipping it is a separate later security pass (explicitly out of scope, per #2014).
What changed
note_inbound_demand(lib.rs):if inbound_demand_cache_enabled() && self.inbound_demand_pull_admitted(store,root). Newinbound_demand_pull_admittedcomputesdig_dht::ContentId::capsule(store,root).to_key()and testsrelevance::in_keyspace_neighbourhood(capsule_key, peer_id).relevance.rs): newin_keyspace_neighbourhood+INBOUND_DEMAND_MIN_PROXIMITY, built on the existingxor_proximity(the SAME primitiverelevance()/the tier-0 selector use —xor_proximitypromoted topub(crate)).OnceLock<[u8;32]>node_peer_id+set_node_peer_id, installed at peer-network bring-up (peer.rs) fromidentity.peer_id()— the exact value tier-0'sNodeContext.peer_iduses (one shared reference). Kept OUT of the sharedspawn_capsule_backfillbody so only the inbound-demand path is gated (not the origin-gated fetch-side path).The proximity bar + justification (the security-sensitive number)
The tier-0 selector has no fixed proximity cutoff to reuse verbatim (it's a relevance/size greedy knapsack over the
xor_proximityprimary), so — per the ticket's fallback — the boolean is derived from the samexor_proximity+ same referencepeer_id, bar at the keyspace midpoint0.5:0.5admits the half of keyspace closer to ourpeer_idthan a random point (≈ shares the top keyspace bit). Kademlia routes near-key requests to near-key nodes, so genuine inbound demand sits well inside the neighbourhood — legitimate demand-caching always fires.peer_id(immovable). Landing a capsule key near it means grindingSHA-256(0x02‖store‖root)toward a fixed target — and because the pull is chain-anchored (backfill merkle-verifies against the CHIP-0035 anchored root), each grind candidate must be a REAL on-chain store, so grinding costs on-chain mints, not cheap hashing. Combined with default-OFF + single-flight + the byte-cap in the shared pull body.(store,root)→ no pull.0.5only guarantees "closer than random" (~2 on-chain mints per admitted key at the extreme); tightening toward a routing-aware k-closest / larger-prefix test needs a live network-size signal not available in this pure gate — deferred to the separate default-ON pass (a one-constant change toINBOUND_DEMAND_MIN_PROXIMITY).How verified (TDD)
keyspace_neighbourhood_admits_the_near_half_and_denies_the_far_half(relevance.rs).inbound_demand_pull_gated_on_keyspace_proximity— one store, two nodes with near/farpeer_id: near admits, far denies no pull.inbound_demand_pull_denied_without_a_known_self_identity— fail-closed.inbound_demandtests green (existing opt-in pull tests updated to set a nearnode_peer_id).cargo test -p dig-node-core --lib→ 634 passed, 9 failed (all the known socket-bind sandbox tests).cargo fmt0,clippy -D warningsclean,buildok.SPEC
SPEC.md §7.10dupdated — the "not yet wired" language replaced with the enforced XOR-proximity admission (bar, ungameability, fail-closed, default-OFF, the later-pass note).Version
root
[workspace.package].version0.89.0 → 0.90.0;dig-node-core0.38.0 → 0.39.0;Cargo.lockregenerated (dig-node-service=0.90.0,dig-node-core=0.39.0). Minor (additive gate, no public-API break).Generated by Claude Code