Repository navigation
Conversation
This was referenced Oct 30, 2026
Draft
grumbach
marked this pull request as ready for review
November 2, 2026 19:27
2 of 7 tasks
grumbach
marked this pull request as draft
November 5, 2026 04:12
Every client, native and browser, now takes part in a BitTorrent-style swarm for each file it reads, always on. Readers meet through the file's rendezvous nodes (ADR-0026 in ant-node), share records over WebRTC data channels, and fall back to the nodes, which stay the last seed. - `ant-core/src/swarm/`: rooms, links and the wire between clients, on webrtc-rs natively and the browser's `RTCPeerConnection` in WASM. - **Rooms.** A room is keyed by the root DataMap address, at most 4 at a time, joined only for a valid root map. A read keeps its room in use for as long as it runs, however long the nodes take, a room joined while it runs included; joining past 4 leaves the room unread longest once no read has used it for 30 s, and otherwise waits (2 joins at most) to be made at a later read of the file, so reading more files at once cannot make every read a new membership, nor push out a long download's room. Each room has a membership number and a random per-room name. Its rendezvous nodes are the closest of the 8 nodes nearest its key that host rooms: one lookup serves both places for 30 s, and a node whose session says it hosts no rooms, or that never let this client join, is passed over for 10 minutes, so readers still meet while some of the closest nodes run an older release. Offers go through both rendezvous nodes, which are always distinct, counted per node, without doubling up while a connection is being made. Every negotiation step is bounded and ends with the room; a native client announces at a node at most once a second, and a room starts at most 8 answers in 20 s, so a node handing out offers, well formed or not, costs a bounded amount however fast it replies. In a browser a membership lives on the session to its node: once that session has closed, an answer or a leave for that node fails at once, and the next announce connects again (in the swarm's discovery turn), joins anew and drops the offers made to the old membership. A node's STUN servers are used only at an IP address the client reached that node on. Each room has a tag derived from its secret: its links' data channels are labelled `ant-swarm-<tag>`, and an answerer refuses a channel labelled for another room. Another member's offer or answer is applied with only the candidates a receiver may try (ant-protocol's `constrain_swarm_sdp`): local addresses and mDNS names only from a node this client reached at a local address itself, loopback ones only over loopback. Every swarm reply is checked before it is used. An offer waits for its answer as long as the protocol's signal lifetime and both trips need, checked at compile time against the slowest answer any client gives; making a round's offers and each local WebRTC step are bounded so the announces keep their pace, and a link this client answered waits for the offerer to collect and apply the answer before it must open. - **Links.** A link opens only after the peer proves it knows the room's secret and holds the key it names itself by. The secret is derived from the root map, and the proof is bound to both ends' DTLS fingerprints as each SDP is applied (only their standard lines remain, see `constrain_swarm_sdp`), so a node knowing only a private file's room key cannot join or sit in the middle. A message before the channel reports itself open opens the link and is read first (webrtc-rs reports the opening from a task of its own), so a peer's first message is never lost. Each membership has its own ML-DSA-65 key; a peer's name in the room comes from it and is taken only once its signature over the link verifies, so no member can pose as another. Duplicates settle on one link per peer, one per introduced member: each end sends its own proof before reading the peer's, and the end closing a duplicate keeps its connection 2 s more, still counted, so both ends settle the same way and stop offering again. Each answer is bounded. - **Reads.** Records the swarm holds are used first. Peers complement the nodes and never hold a read up: a read asks at most 2 peers that announced the record, counted across all its asks from the first that found one, one record per link at a time with at most one more read waiting behind it, within strict deadlines and at most 8 at once, and gives them the read's hedge delay (3 s in a browser, 1 s natively); past that a peer keeps the read while it is on pace to finish within the read's patience (about as long as the nodes take a record now, read-ahead a reader waits on included, at least twice the hedge delay, at most 12 s), then the nodes are read beside it, at once if it fails; the first record that checks against its address wins, and a node read that ends without one (refused, busy or missing) leaves a peer still sending to finish within its deadline (the node limiter still learns of the nodes' miss). A link settled away mid-read hands the read to its twin. In a browser, a read still waiting for its turn at the nodes, with no GET sent, takes its record from a linked reader that has it meanwhile, while it keeps its place in that queue. A browser download asks for its records in an order of its own, as BitTorrent picks pieces, so readers starting together hold different records early on. A hidden browser page reads from nodes. - **Serving.** A store of 256 MiB (native) or 64 MiB (browser) holds every record at a minimum charge. Uploads are limited per link and in all (4, and 1 while any of the client's own reads runs, whichever download API started it, so serving never takes the CPU and line its own download needs), with transfer numbers per ask, and sends take turns in order and yield through a posted message in browsers. - **Integration.** File and data downloads, browser readers, streams and read-ahead join the file's room as soon as they have its root map, and seed what they fetch; joining returns at once, and the other readers are found beside the reads from the nodes. A room joined by a read asks the nodes for nothing until that read's first record is in (30 s at most), so discovery never competes with a download's first record; one joined on request (`join_swarm`) starts at once. A browser reader joins again on a read if reading other files has left the room meanwhile. Records already held are announced to links opened before the room learnt its file's records. Raw `chunk_get`, `chunk_exists` and DataMap records (a shrunk map's wrappers included) neither ask peers nor share. Clients on one native node identity share one swarm. A browser client takes part only on a network adapter that says which nodes host rooms (`BrowserNetwork::supports_swarm`, false by default; the client's own adapter says true), since a node that predates rooms closes the channel a swarm request comes on. - **What a page sees.** `Client::join_swarm`, `swarm_stats` and `swarm_room_stats`, and the browser's `joinSwarm`, `swarmStats` and `swarmRoomStats`, expose it; `swarm_stats().discovery` counts what finding peers cost (lookups, announces and their failures by cause, signalling bytes, offers, answers, channels and links). A browser reports `Swarm room <tag>` when it opens the file, and counts are kept per room, with each open link named by this end's DTLS fingerprint, so a page can tell which file each link and record belongs to, and which of its connections are proven links. A downloaded chunk's progress line, which names the node that served it or `cache`, says `from a reader` when another reader sent it (who served a chunk is a `ChunkServer`). - **Tests.** Unit tests over an in-memory WebRTC stand-in and over webrtc-rs on loopback; timing tests, on a paused clock, that a stalled, failed or slow peer delays a read by the hedge delay, or the patience for one on pace, at most, that a peer beating busy nodes keeps the read, that nodes that fail leave a peer still sending to finish and that the miss is still reported, that only a room gone unread makes way for another, that a room whose read still runs never does, and that a queued read keeps its place while a peer tries, and asks no peer twice; a test that a relayed SDP gives webrtc-rs's own parser no fingerprint or candidate but the sender's allowed ones; a test that a room starts a bounded number of answers; a test that a client still reading sends one record at a time; a test that readers pick the same closest nodes that host rooms; an end-to-end test over webrtc-rs on a devnet (whose test node now hands the rendezvous each request's source address, as a real node does); WASM tests that opening a private file, or starting its download, joins its room at once without waiting for it; and a real Chromium spec that checks sharing on swarm nodes and silence on older ones.
Patch ant-protocol, saorsa-core, saorsa-transport and ant-node to their feat/peer-swarm branches, which this change needs: the swarm messages (WithAutonomi/ant-protocol#50), the send on one named channel (WithAutonomi/saorsa-core#183), the plain STUN answers (WithAutonomi/saorsa-transport#231) and the rendezvous on every node (WithAutonomi/ant-node#262). Once those merge, these point at their main branches, or at the released versions at the release train. Cargo.lock starts from main's lock and differs from it only where those branches need it: the swarm's WebRTC and STUN crates, saorsa-transport's main dropping its CLI (and its clap and reqwest dependencies), and ant-node's main adding winapi-util on Windows. iana-time-zone keeps the windows-core 0.62.2 main's lock has.
cargo tree percent-encodes the slash in a git branch's name and cargo metadata does not, so the notices generator matched no package on such a branch (this PR's dependency branches are feat/peer-swarm): it now compares both decoded. saorsa-transport's main pulls granit-parser 2.0.0, which ships its licence texts as 1.3.0 did; its entry names the new version.
Rust 1.99, which CI's stable toolchain runs, deprecates the atomics' fetch_update in favour of try_update, which older toolchains lack. The swarm's four uses go through a compare-exchange loop of its own that does the same on both.
On a slow runner (CI's macOS) the second reader's reads could start before the first's opening messages, which say what it holds, had arrived: each read then went to the nodes at once, as a read must, and the test found no record from the other reader. The test now waits until the second reader has heard that the first holds every record, through Client::swarm_could_serve, which only the test-utils feature builds.
…odes on CI The second-reader test shared three 2.7 MiB records and failed on both CI runners with no record from the first reader. A read gives a linked reader that offers a record a 1 s head start, then keeps it while it is on pace to finish within about 2 s; past that the nodes serve it. In a debug build the link's DTLS encrypts in unoptimised Rust (RustCrypto aes-gcm) while the nodes' QUIC uses aws-lc-rs native code: on a laptop a 2.7 MiB record took the link about 1.3 s and the nodes about 1.0 s, and on the slower runners the nodes won every record. Waiting for the other reader's offer, added before, stays: a read that starts before it still goes to the nodes, as it should. The test now shares three 64 KiB records, each still several 16 KiB pieces on the link, and checks that every record from a peer came whole. On a laptop the second reader's whole download then took 0.14 s with two records from the first. Records of the largest size stay covered by the wire assembly unit test and by the real Chromium swarm job, which shares 2 MiB records over the browser's link.
A client sends other readers one record at a time while its own reads run, since a record sent costs the CPU and line its own download needs. Only reads of a file's data records counted, so opening a file's map, a raw read or an existence check let a client seeding another file keep four sends going, and slowed the file it was opening. Every fetch from the nodes now counts (Swarm::fetching); only a read of a file's data record also keeps the file's room as read, as before. The new test fails if a fetch that shares nothing stops holding sends to one.
The suite waits on Client::swarm_could_serve, which only test-utils builds have, so 'cargo test --all' failed to compile it. It now requires native and test-utils, as e2e_native_payment does; CI already passes test-utils.
chunk_get_from_close_group and chunk_get_from_closest_peer_group ask the nodes directly, so a client seeding a file kept four sends to other readers going while one ran. They now hold sends to one for their whole run, lookup included, as every other fetch does. e2e_swarm checks it through the public entry point: once a close-group read has taken its first step the client counts one fetch running, and none after it ends (Client::swarm_fetches_running, test-utils only). It fails without the fix.
Where a browser makes connections take turns to gather ICE (every engine but Chromium), a read's connection may wait behind at most one of the swarm's gatherings. A discovery dial let the swarm's discovery turn go once it had said hello, while its ICE turn stayed held until its gathering ended (up to 1 s). The next discovery dial, seeing no read waiting yet, queued for the ICE turn behind it, and a read arriving then queued behind both: two turns, close to 2 s. Discovery dials (a room's lookup, its seed and its announce connections) now wait until the ICE turn is free and no read waits for it (ice_gathering_turn_idle, which replaces reads_not_waiting_to_gather), so one never queues for the turn and a read waits behind one discovery gathering at most. The seed loop waits before each seed's dial. The new WASM test makes two discovery dials whose gatherings never finish, then a read's dial 50 ms later: the read connects within one turn and before the second discovery dial. With the old condition it waited 2 s.
A page counts and draws the peers it shares with by their links, and two readers can hold two links to each other for a moment while the client settles on one (met through both rendezvous nodes, or offering to each other at once). A page then counted one person twice and drew them twice. RoomStats gains open_link_peers: for each open link, its fingerprint (as in open_links) and a name for the peer at the other end, derived from the name the peer uses in that room alone, so two links to one peer share it and the same client in another room has an unrelated one. Exported as OpenLinkPeer. The new test opens two links from one member and one from another: the first two name one peer, the third another, and the name is not the member's own name in the room. It fails if links are named apart.
… read A fifth file's room joined while every room was still read waits for one to go unread. The waiting join was retried only when a record of the file came in from the nodes, so if the nodes failed the file's reads it never joined, even once a room had gone unread, and the reads could not reach another reader who held the file; joining the file again did nothing either. A join of a file whose join waits now tries it again at once, and so does the start of a read of one of the file's records, before any record is in. Both take the waiting join off the list and put it back if still no room makes way, as the retry after a record already did (Swarm::admit_waiting). The new test fills the four rooms, makes a fifth wait, then lets the rooms go unread: joining the fifth again makes it, and a sixth waiting join is made when a read of its record starts. It fails with either retry removed.
Swarm::fetches_running is read only by Client::swarm_fetches_running, a test-utils accessor, but it was also built for unit tests, where nothing reads it: CI's unit tests, built without test-utils and with warnings denied, failed on dead code. It is now built with test-utils alone.
A native file download retries a record still missing after the immediate retry 15 s and then 45 s later. A read whose other records came quickly, from other readers or in a short batch, so spent its last tries in a minute or two of an overload, while a read with more records to fetch spread them over its own longer rounds. In a lab rollout with half the nodes on the old release and the network overloaded, swarm readers gave up first for this reason. Those two rounds now wait at least as long as the batch's longest round so far took, up to 5 minutes, on a clock the caller passes as it passes the timer. The first pass and the immediate retry, which catches a record missing only for a moment, are unchanged, and so are browser reads. Waiting sends nothing, so it adds no load to busy nodes.
… are `room_of`'s doc said a public file's room is its address. The room is the address of the file's root DataMap, which a reader resolves a shrunk map to first, so a public file published with a shrunk map has a room other than its address; the doc now says so, as ADR-0026 does, and the test of a root map's room is named for what it checks. The browser tests' README said the second page must take every data record from the first. `swarm.spec.js` requires at least one: a link takes one record asked and one waiting at once, and further reads go to the nodes, which a busy machine may also let win. The README now says that.
A browser download reports per chunk the GETs it cost (main's WithAutonomi#249). The count was raised when a GET's future started, before the GET waited for its turn under the browser's read budget. A GET still waiting there is dropped, unsent, when a linked reader sends the record meanwhile, and the chunk then reported a GET that never left; a download that joined the fetch was charged the same. A GET now counts once it is sent, or once it ends without being sent (it failed first, which main counts as a try that cost the chunk), and not when it is dropped while it still waits. A unit test covers the three cases.
A browser download asked for a file's records in an order picked at random for that download, so readers of one file starting together hold different records early on. Two downloads of a file in the same client then asked in different orders as well: they no longer overlapped record by record, a record one had cached could be reported from the cache to the other, and they shared fewer fetches. Main's test that a joined fetch charges both downloads its GETs could then fail intermittently. The order is now picked once per client, from a seed its swarm draws at random when made: readers in different clients still ask in different orders, while one client's downloads of a file ask in the same one. A unit test checks that one client's order is fixed and a permutation, and that another client's differs.
A browser download reports per chunk who served it. A record the client's own swarm store held already came back through the same path as one a linked reader sent, so it was reported `from a reader`. That happens after a range read: read-ahead keeps the records it fetches for the file's room and out of the client's chunk cache, so a later download of the file finds them only in the swarm's store, with no reader involved. The read's pace now notes when the swarm answered from its store, and such a record is reported `from cache`, as one the chunk cache held is. Like a reader's record, it still does not move the node limiter: the nodes were not asked. A WASM test reads a file through a reader, then downloads it in the same client, and fails with the old labelling.
ant-node's feat/peer-swarm branch moved from e5e86480 to 008a76d6, which only says in ADR-0026 that a client's downloads of one file share their record order. Cargo.lock follows; iana-time-zone keeps the windows-core 0.62.2 main's lock has.
grumbach
force-pushed
the
feat/peer-swarm
branch
from
November 5, 2026 05:46
8541ad0 to
e44ea44
Compare
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 free
to 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.
Every client, native and browser, shares the records of the files it reads with the other readers of each file, BitTorrent style: it joins the file's room at the nodes, links with other readers over WebRTC data channels, and races a peer that has a record against the nodes, which stay the last seed. Peers only ever add to the node path; every record is checked against its address. The branch is rebased onto main as linear commits: main's ADR-0020 lookup runs inside the node read the swarm races, so a reader that sends a record first spares the nodes the lookup too, and main's per-chunk download progress (who served a chunk and the GETs it cost) now tells a node, the cache and another reader apart (
from a reader). The commits after the feature point the four dependencies at their matching branches (the lock starting from main's), let the notices generator match git dependencies on a branch whose name has a slash, avoid the atomics'fetch_update, which Rust 1.99 deprecates, makee2e_swarmwait for what the other reader holds and share records small enough that a debug build's link beats the nodes on slow runners, admit new sends to other readers only one at a time while any fetch of the client's runs, builde2e_swarmonly withtest-utils, count close-group reads as fetches, let a browser's discovery dial only once the page's ICE gathering turn is idle, name the peer each open link reaches for pages, make a waiting room join when its file is joined again or read, build the running-fetch count only withtest-utils, space a native file download's last two retry rounds by its slowest round so far (up to 5 minutes), correct two docs, count a chunk's GETs once sent rather than while they wait for their turn, ask for a file's records in one order per client (so one client's downloads of a file keep sharing fetches), report a record the swarm already held as from the cache, and lock ant-node at its branch's head.One change across six repositories. Merge order:
This PR depends on: protocol, transport, core and node, as git branch dependencies on their
feat/peer-swarmbranches. Once those merge, the dependency commit moves to theirmainbranches (or the released versions at the release train).Linear issue
Closes V2-1892
Risk tier
Proposed; a human confirms at review.
Compatibility
ant-swarm-<room tag>.Client::join_swarm,swarm_stats,swarm_room_stats; the browser'sjoinSwarm,swarmStats,swarmRoomStats;BrowserNetwork::supports_swarm(default false) andconnect_for_swarm(default no-op).Semver impact
Test evidence
e44ea44(rebased onto main26199fe):cargo test -p ant-core --lib989 passed;cargo test -p ant-core --features test-utils --test e2e_swarm4 passed (a second reader served records by the first over real webrtc-rs on an in-process devnet, the nodes asked for every record it did not send; a reader whose peers left served by the nodes; readers of different files not meeting; a close-group read counting as a fetch while it runs). CI's Browser WASM job ate44ea44: 261 of 261 WASM tests (wasm-tests/*.test.mjs). Locally, beforee44ea44, which changes only Cargo.lock's ant-node revision to008a76d6(ADR-0026's text only): the same 261, and main's large-download tests 15 times in a row. CI's end-to-end jobs ate44ea44, on Linux and on macOS, withtest-utils:e2e_chunk13,e2e_data7,e2e_file10,e2e_manifest7,e2e_swarm4,e2e_payment8,e2e_native_payment4,e2e_security10,e2e_cost_estimate7 ande2e_adr00043 passed, ande2e_merkle8. fmt, clippy (all targets and features,wasm32browser-wasm,-D warnings, Rust 1.99) andRUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps: clean.cache(the chunk cache or the swarm's own store held it) ora reader, and the GETs it cost; a GET counts once sent, or once it ends unsent, never while it still waits for its turn (unit test); one client asks for a file's records in one order, fixed and a permutation, another client's differing (unit test), so two downloads in one client keep sharing fetches; a download after a range read in the same client reports the records its swarm held asfrom cache(WASM test, failing with the old labelling).e2e_swarm, and the browser job also runsswarm.spec.jsin real Chromium against the ant-node this client is built with (REQUIRE_SWARM), beside the existing run against the pinned older node.008a76d6, the swarm client ate44ea44and, as the released client, client main26199fe):039ce7c5, the swarm client at40258e4and, as the released client, client main42b68ff, which reads through ADR-0020's get-or-closer lookup; the later commits change only docs, a test's name and locks):e61446b8, every reader on the released client, three runs), finished 2 of 48 downloads, the nodes sending 22.4 to 25.0 GiB a run (7 of 64 and 8.6 to 16.9 GiB before the merges): the harder collapse comes with the merged mains, ADR-0020's read among them, and the mix with the swarm client finished more (12 of 48 against 2 of 48, p = 0.007) with the nodes sending 22% fewer bytes (18.5 against 23.7 GiB a run on average); its released readers did as well as today's (1 of 24 against 2 of 48). The old nodes logged no panic and no ERROR line, and 31 to 325 decode warnings a run beside these PRs' nodes, the swarm client's room requests.ant file downloadCLI joins the same rooms as the pages (25 s beside four pages, which took 144 MiB from each other). No dev testnet run yet.ant file downloadon its own address with a fifth of a vCPU, one 40 MiB file, a fresh network for every run), the swarm client ata5cb5a2(the client's later commits space a file download's last retry rounds after slow rounds,19715a9, in the rollout runs below, update Cargo.lock: the native devnet's ant-node and a Windows-only dependency resolution, and merge main) against the released one (client main, no swarm), alternating:ae8e997, without the waiting room join on read) had been slower in its first two 8-reader pairs (73 and 78 s against 47 and 55 s, the nodes sending 16 to 25% more) and mixed in the next four.a5cb5a2, or where marked at19715a9, the client's commit that spaces a file download's last two retry rounds by its slowest round so far):19715a9: 16 of 16 swarm-client and 14 of 16 released-client downloads finished (both failures could not fetch the file's DataMap), median 191 s against 205 s.a5cb5a2the swarm client did worse than the released one. Beside these PRs' nodes 4 of 40 swarm-client and 13 of 40 released-client downloads finished (p = 0.03); past the first reader of each client, 0 of 35 against 10 of 35 (p = 0.001), the ten in two runs, at 944 to 1198 s, each time after every swarm reader there had given up (at 618 to 940 s). Beside main's nodes 7 of 64 finished (every reader beside these PRs' nodes, 17 of 80: p = 0.1; their released readers, 13 of 40: p = 0.01), the nodes sending 9.1 to 16.3 GiB against 8.6 to 16.9 GiB a run. Both clients give up on a record after the same four tries. The swarm readers had most records from each other, so their rounds held only the records none of them had, took little time and put all four tries in the worst of the overload: they gave up first (median 729 s against 1014 s), and some released readers finished after every swarm reader there had stopped. Every failed swarm reader lacked a record no other swarm reader held. Five more runs of this mix at19715a9, after the others: the swarm client finished 36 of 40 against 23 of 40 for the released client in the same runs (p = 0.002), past the first reader 33 of 35 against 21 of 35 (p = 0.001); two of its four failures were the first swarm reader, with no reader ahead of it, and the four gave up at 707 to 1015 s. The released readers did no worse beside it (23 of 40 against 13 of 40 besidea5cb5a2, a later batch, p = 0.04), the nodes sending 5.3 to 14.3 GiB a run. Load varied from run to run: one of the five never overloaded and all 16 finished (without it, 28 of 32 against 15 of 32). The readers also met later than with every node upgraded (the first record from another reader at a median of 167 s against 116 s, ata5cb5a2): a node on the old release cannot read a room request and leaves it unanswered, and a joining reader passes such a node over after about 60 s, then waits 10 s before trying the next, twice as long after each further node that never answers (up to 5 minutes). So with19715a9, while half the nodes run the old release and the network is overloaded like this, a reader on the swarm client gave up less often than one on the released client.19715a9: no panic or ERROR line, 0 to 279 such warnings a run.e44ea44: every build, test and lint job passes, among them the end-to-end and Merkle end-to-end jobs on Linux and macOS, Browser WASM and both real-Chromium jobs (the pinned older node and the swarm's); Security Audit fails. It fails only on RUSTSEC-2026-0387 (crypto-bigint0.5.5, throughelliptic-curve0.13), whichcargo auditreports on this repository's main lockfile too, with the same dependents; its fixes wait on alloy and webrtc-rs releases.linear-linkandpr-template, which check this description, failed on its previous version, which named no Linear issue; this version passes both when their script is run on it locally.New dependency
webrtc0.14 andrustls0.23 become direct, optional dependencies of the native build (both were already in the tree through saorsa-transport), and a few moreweb-sysfeatures.ADR
https://github.com/grumbach/ant-node/blob/feat/peer-swarm/docs/adr/ADR-0026-clients-share-records-nodes-introduce-them.md (new in the ant-node PR, status Proposed)
Mitigation / rollback
It need not wait for the nodes: in the lab, with half the nodes still on the old release and the network overloaded, the swarm client finished more downloads than the released one (Rollout, under Test evidence), though its readers meet later beside old nodes. Revert: reads go to the nodes alone, as today; nodes with rooms then hold nothing for this client.