Repository navigation
feat(storage): answer get-or-closer requests (ADR-0020) - #249
mickvandijke wants to merge 11 commits into
Conversation
Kademlia's FIND_VALUE for chunks. A new get-or-closer request is answered with the chunk by a holder, and otherwise with the closer peers its FIND_NODE answer would hold, encoded and checked by saorsa-core. Native clients run their own iterative lookup with it. Peers that do not announce `get-or-closer/1` in their user agent get FIND_NODE plus a GET instead, so a new client reads at full speed from today's nodes. When the lookup finds nothing, the client falls back to today's read path, so the absence rule is unchanged. Measured on production: native p50 5.95 s to about 1 s, the whole file 3.7 times faster, at 2.5-3.3 chunk copies per read. Part of V2-1358. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR-0020. A node that holds the requested chunk answers `Found` with it. Otherwise it answers `Closer` with saorsa-core's encoding of the FIND_NODE answer it would give the authenticated requester, so clients check those peers exactly as they check FIND_NODE answers. A node with storage enabled announces `get-or-closer/1` in its user agent; one with storage off never answers chunk requests, so it does not. Browsers are not offered the request yet. Get-or-closer traffic gets its own rx and tx rows in the chunk traffic summary (group 4), and each answer is logged as `get_or_closer_rpc`. Builds against sibling saorsa-core and ant-protocol checkouts on the V2-1358 branch until those are released. Part of V2-1358. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A client built from the design read a 612 MB production file in 56 s, against 242 s for the current client, through the per-peer fallback that today's nodes take. In-process networks of upgraded nodes are read with get-or-closer requests only. Part of V2-1358. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the sibling-checkout paths with the pushed revisions, so the branch builds anywhere. Part of V2-1358. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Takes the rc-2026.10.1 release baseline: ant-protocol 4.0.0, saorsa-core 0.29.0 and saorsa-transport 0.38.0. Main no longer patches saorsa-core or saorsa-transport for copy-free sends, so the patch section now pins only the ADR-0020 branches of saorsa-core and ant-protocol, both merged onto that baseline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dirvine
left a comment
There was a problem hiding this comment.
Code approval at the reviewed head
No introduced blocking runtime defect found in this PR. Production responses use the size-capped encoder; authenticated requester identity is passed into the handler. Decoder use here is test-only.
Stack coordination remains required: saorsa-core#173 has a P2 decoder-size blocker. Refresh the core patch pin at Cargo.toml:240 after that fix and rerun integration tests; propagate the resulting node head into the client. This code approval is not approval to ship the current linked dependency set or a waiver of merge/release gates.
Reviewed commit: 79385bfddc04a26b5c0747b3355364c12b9a127b.
Review method: six substantive seats across the linked set, including a completed z-ai/glm-5.2 API review recovered from a worker that timed out after saving its response; coordinator source checks and local execution. Initial functional/security/test seats recommended approval. The coordinator probe, independent adjudicator, API seat and replacement concurrency/release seats corroborate the missing decoder bound. This is not unanimous approval. The API seat recommended client approval with concerns; the coordinator withholds it because the exact client pin still exposes the defect.
Local verification (macOS, rustc 1.96.0): core closer-peers 3 passed and DHT-manager regression 143 passed; protocol all-features 108 passed; node get-or-closer unit tests 3 passed and real-transport E2E 1 passed; client kad_read unit tests 10 passed and real-transport E2E 1 passed. Formatting passed in all four repositories. No production/testnet deployment or merge performed. Draft git pins were verified with cargo metadata --locked; this is not published dependency closure. ADR-0020 remains Proposed.
saorsa-core's decode_closer_peers now rejects payloads over the 64 KiB FIND_NODE limit before deserializing them. This node only encodes closer peers, and its encoder already kept within that limit, so its own behaviour is unchanged. The pin moves so that the client, which pins this node, takes the bounded decoder. Part of V2-1358. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review of the ADR-0020 stack: - decode_closer_peers rejects a payload over the 64 KiB FIND_NODE limit before parsing it (saorsa-core fd37775). The chunk message carrying it may be up to 5 MiB. - A peer whose content fails verification only drops out of the client's lookup (ant-client 7d9a248). Every queried peer is asked for the chunk, so ending the read on one bad answer would let any dishonest node on a lookup path fail reads. Today's read path, which runs when the lookup finds nothing, keeps its rule that bad content ends the read. - The trade-offs now list that every queried peer can send content. - Validation now covers both changes. Part of V2-1358. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks.
|
Browsers send the ADR-0020 request inside chunk_protocol. A node that holds the chunk answers Found, as over QUIC. One that does not answers with its browser find_node reply, whose peers carry the WebRTC Direct endpoints and owner-signed records a browser dials; native closer peers never cross the browser boundary. HELLO lists get_or_closer when storage is enabled, and the request is scheduled and bounded as a GET. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Browsers run the same read driver over WebRTC Direct once a node they reach lists get_or_closer. Also records the likely-holder gate the native client already uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rust 1.99's clippy rejects assert!(!x.is_empty()). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A node gives each source address one bulk response slot and 16 MiB in flight. Scheduling every get-or-closer as a GET made a lookup step that ends in a find_node answer reserve a full GET and wait behind chunk transfers to the same browser. Now only a node that holds the chunk takes the bulk slot and the GET reservation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR-0020: a chunk GET returns the chunk, or closer peers, in one request, as Kademlia's FIND_VALUE does. This PR adds the ADR and the node side.
Why. On production, a native chunk read spends 82% of its time finding a holder, because the closest-peers lookup runs to convergence before the GETs start. Asking each queried peer for the chunk as well as for closer peers cuts native p50 from 5.95 s to about 1 s; see the investigation.
Node.
GetOrCloserRequestwithFound.Closer, with saorsa-core's encoding of the FIND_NODE answer it would give the authenticated requester.get-or-closer/1in its user agent. One with storage off never answers chunk requests, so it does not announce it.get_or_closerwhen storage is enabled, and a browser sends the same request insidechunk_protocol:Found, as over QUIC.find_nodeanswer. Its peers carry the WebRTC Direct endpoints and owner-signed records a browser dials. NativeClosernever crosses the browser boundary: it holds QUIC addresses a browser cannot dial.find_nodeis, so a browser's lookup step never waits behind a transfer to the same browser. A node gives each source address one bulk slot and 16 MiB in flight.chunk_protocol, and both answers are existing browser response bodies.get_or_closer_rpc.Compatibility by design.
get_or_closer, so a new browser never sends it the request, which it could not decode. An old browser never sends it.Stack, merge in this order:
This PR pins saorsa-core and ant-protocol at those branches by git rev.
Linear issue
Closes V2-1358
Risk tier
Compatibility
GetOrCloserRequest, and its user agent gains the tokenget-or-closer/1. Thenode/prefix and the migration token are kept. Every existing message is unchanged.get_or_closercapability, andchunk_protocoladmitsGetOrCloserRequest. Its answers are existing response bodies.ChunkRequestContext::with_requesterandmigration_signal::user_agent(signal, serves_chunks)are crate-private.Semver impact
Test evidence
cargo test --lib --features test-utils: 1,269 passed. New tests:node/prefixget_or_closeronly with storageFoundcrosses to a browser;Closeris refused, and errors are redactedcargo test --all-features --test webrtc_direct_devnet: 7 passed, 1 ignored as before. New:get_or_closer_returns_the_chunk_or_the_find_node_answer. Over a real WebRTC Direct session, HELLO lists the capability, a held chunk comes backFound, and a missing one getsnodeswith a signed record for every peer.Real browser. ant-client's Chromium suite (
ant-core/browser-tests), run against seven nodes built from this branch: 2 of 2 passed. With a temporary trace on the download, the first record took the ordinary read, as a cold client does. The other three records came through get-or-closer: 3Found, 2nodesanswers, no plain GET.cargo test --test e2e --features test-utils test_get_or_closer_returns_chunk_or_closer_peers: passed. On a local test network, the holder answersFound, and a non-holder answersCloserwhose peers the client decodes as a FIND_NODE answer, without the requester.cargo clippy --all-features -- -D clippy::panic -D clippy::unwrap_used -D clippy::expect_used -D warnings: clean.cargo +1.99.0 clippy --all-targets --all-features -- -D warnings, as CI runs it: clean.Live. A client from the ant-client PR downloaded a 612 MB production file in 56 s, against 242 s for the current client, with identical bytes. All 150 chunks came through the new lookup, on the route for nodes without the token.
Browser reads on the dev testnet (2026-10-12): the kad fleet's nodes were swapped in place to this branch, keeping their data, and browser downloads were A/B'd. The new browser read did not help: fra1 is about a fifth slower at p50, and syd1 is unchanged. See ant-client#221 for the table.
Dev testnet comparison against main, matched (2026-10-09, native clients). Two 60-node fleets on the same layout:
ant-testnet/state/comparisons/v2-1358-kad-vs-main-matched-60m-1009/README.md.New dependency
none
ADR
ADR-0020: Kademlia-style chunk GET: return the chunk or closer peers, added in this PR.
Mitigation / rollback
The node is passive until clients send the request. To stop clients using it, drop the user-agent token; they then fall back to FIND_NODE plus GET. Otherwise revert.
🤖 Generated with Claude Code