OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer commits / 9ae462a

udx: liveness — an idle peer is now told about, and it is the reference's wire
Gate D5, red since yesterday. Nothing detected a silently dead peer on an idle stream: DhtConnector's keepalive branch is unreachable (the connection is freed the instant it reaches CONNECTED), libudx's keepalive was never switched on, and switching it on is not liveness either — send_probe writes a bare heartbeat outside the packet and RTO machinery, and libudx's own TODO at that line says the packet that would fix it is not written. So it is fixed at OUR layer, in UdxTransport::_sweep_keepalive, with the wire form taken from the reference rather than invented: send a ZERO-LENGTH ENCRYPTED FRAME to any peer idle for 5s — which is what @hyperswarm/secret-stream's sendKeepAlive writes (`this.alloc(0)`) and hyperdht's connectionKeepAlive interval. Its receiver eats them ("assume the other side has it enabled also") and so does ours. A write of any kind counts, so a busy peer never sends one. kill a peer silent for 20s (four missed) is disconnected through the existing CMD_DISCONNECT path, which destroys the stream and therefore drives _on_stream_close -> on_mux_gone -> PEER_DOWN. Every link already existed; nothing drove the first. ⛔ THE KILL ONLY ARMS ONCE THE PEER HAS SENT A KEEPALIVE ITSELF. A build that predates this sends nothing while idle, and killing it after twenty seconds would make an idle connection a flag day — the exact thing swarmd-session went out of its way to avoid. Until a peer proves it speaks keepalives, its silence means what it always meant. ⛔ It required NoiseStream::decrypt to grow an answer it could not give. `{}` meant BOTH an authentication failure and a legitimately empty plaintext, so a keepalive was indistinguishable from an attack. The old signature stays for every other caller. fixed PEER-DOWN 17.8s on a testnet, 18.0s on the PUBLIC DHT mutated never — and the kill correctly stays DISARMED there, which is the compatibility guarantee working rather than a second failure No regressions in the path everything shares: 17 offline gates, three_peer_swarm and session_channel on a testnet, dht_godot_style, live_lookup, vectors 65/65, self_announce_live, all four swarmd gates, both public-DHT e2e gates (100 MB in 8.0s, 1112 bytes over the socket), and blender-bridge's session gate all three ways — old<->old, new<->new, old<->new — 9/9 each. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9ae462ae7a91620b575637174dc680e2b669a9bc
by plankatron <fleeky@gmail.com> · 2026-09-10T23:14:37.000Z

Changed files (19) +198 -51

docs/SWARMD-DESIGN.md +29 -9
……@@ -131,7 +131,7 @@ content directory, which is also what makes a fetch resumable across restarts.
131131| D2 | a client that stops reading is dropped; survivors' streams stay intact |
132132| D3 | `SEND` past the high-water mark returns `writable=false`, `DRAIN` follows |
133133| D4 | two channels on one connection, neither starves the other |
134| D5 | `PEER_DOWN` on channel close, not on timeout — **written, RED, see below** |
134| D5 | `PEER_DOWN` when a peer dies — **PASS**, 18.0s on the public DHT |
135135| D6 | a 100 MB file crosses by hash and no frame exceeds the cap |
136136| D7 | `STATUS` reports `dht=down` when the DHT is down |
137137
……@@ -139,11 +139,10 @@ D1 and D7 are the gates #68 proved the suite lacked: every existing
139139cross-machine test passes `--bootstrap` at a local testnet, the one configuration
140140the products never use.
141141
142**D1, D2, D3, D4, D6 and D7 pass.** `test/test_peer_down.py` is **D5, and it is
143RED** — deliberately kept that way, because what it found is a real hole and not
144a gate bug.
142**All seven pass.** D5 was red for a day and what it found was a real hole; the
143trace below is kept because the shape recurs.
145144
146### 8.1 D5: nothing detects a silently dead peer on an idle stream
145### 8.1 D5: nothing detected a silently dead peer on an idle stream (FIXED)
147146
148147Kill one of two meshed daemons with SIGKILL and the survivor reports `peers=1`
149148for the whole window. Measured at 10s, 45s, 60s and 90s deadlines: never.
……@@ -177,10 +176,31 @@ instead of a third: propagating `DhtConnector::on_failed` to `disconnect_peer`
177176(dead code — the connector never reaches `on_failed` for an established peer),
178177and switching the libudx keepalive on (real, useful for NAT, and not liveness).
179178
180**What it needs:** an application-level ping at our layer — a small periodic
181message on a reserved channel, a timeout, and `disconnect_peer` when it lapses —
182so the close path that already exists gets driven. That is a component, not an
183edit, and it should be designed rather than patched in.
179**The fix**, in `UdxTransport::_sweep_keepalive`, at our layer because libudx's
180cannot be made to detect death without changing libudx:
181
182 * **send** — an idle peer gets a ZERO-LENGTH ENCRYPTED FRAME every
183 `PEER_KEEPALIVE_INTERVAL_MS` (5s, hyperdht's `connectionKeepAlive`). That is
184 the reference's wire form exactly: `@hyperswarm/secret-stream`'s
185 `sendKeepAlive` writes `this.alloc(0)`, and its receiver eats the result
186 (`if (plain.byteLength === 0 && this.keepAlive !== 0) return`). A write of any
187 kind counts, so a busy peer never sends one.
188 * **kill** — a peer silent for `PEER_DEAD_MS` (20s, four missed) is
189 disconnected through the existing CMD_DISCONNECT path, which destroys the
190 stream and therefore drives `_on_stream_close` → `on_mux_gone` → `PEER_DOWN`.
191
192⛔ **The kill only arms once the peer has sent a keepalive itself.** A build that
193predates this sends nothing while idle, and killing it after twenty seconds would
194make an idle connection a flag day. Until a peer proves it speaks keepalives, its
195silence means what it always meant.
196
197⛔ It also required `NoiseStream::decrypt` to grow an answer it could not give:
198`{}` meant BOTH an authentication failure and a legitimately empty plaintext, so
199a keepalive was indistinguishable from an attack.
200
201 fixed PEER-DOWN 17.8s on a testnet, 18.0s on the PUBLIC DHT
202 mutated never (the keepalive send removed — and note the kill correctly
203 stays disarmed, which is the compatibility guarantee working)
184204
185205## 9. Order
186206
src/noise_stream.cpp +19 -0
……@@ -40,6 +40,25 @@ std::vector<uint8_t> NoiseStream::encrypt(const uint8_t *data, size_t len) {
4040 return ct;
4141}
4242
43bool NoiseStream::decrypt(const uint8_t *data, size_t len, std::vector<uint8_t> &out) {
44 out.clear();
45 if (len < TAG_BYTES) return false;
46
47 std::vector<uint8_t> pt(len - TAG_BYTES);
48 unsigned long long pt_len;
49 unsigned char tag;
50 int ret = crypto_secretstream_xchacha20poly1305_pull(
51 &_rx_state,
52 pt.data(), &pt_len, &tag,
53 data, len,
54 nullptr, 0 // no additional data
55 );
56 if (ret != 0) return false; // Authentication failure.
57 pt.resize(static_cast<size_t>(pt_len));
58 out.swap(pt);
59 return true;
60}
61
4362std::vector<uint8_t> NoiseStream::decrypt(const uint8_t *data, size_t len) {
4463 if (len < TAG_BYTES) return {};
4564
src/noise_stream.h +12 -0
……@@ -34,6 +34,18 @@ public:
3434 // Decrypt ciphertext → plaintext. Returns empty on auth failure.
3535 std::vector<uint8_t> decrypt(const uint8_t *data, size_t len);
3636
37 // ⛔ THE SAME CALL, WITH THE ONE ANSWER THE OTHER CANNOT GIVE.
38 //
39 // decrypt() returns {} for an authentication failure AND for a frame whose
40 // plaintext is legitimately empty, and those must be told apart: an EMPTY
41 // authenticated frame is the keepalive @hyperswarm/secret-stream sends
42 // (`sendKeepAlive` writes `this.alloc(0)`), and the reference eats it on
43 // receipt rather than treating it as an error. Conflating them means a
44 // keepalive is indistinguishable from an attack.
45 //
46 // Returns false only on authentication failure.
47 bool decrypt(const uint8_t *data, size_t len, std::vector<uint8_t> &out);
48
3749 // Get the TX header (24 bytes, send to remote for their RX init).
3850 const uint8_t *tx_header() const { return _tx_header; }
3951
src/udx_transport.cpp +74 -2
……@@ -367,6 +367,7 @@ static void _async_cb(uv_async_t *handle) {
367367static void _tick_cb(uv_timer_t *handle) {
368368 UdxTransport *transport = static_cast<UdxTransport *>(handle->data);
369369 transport->_process_commands();
370 transport->_sweep_keepalive(uv_now(handle->loop));
370371}
371372
372373// ⛔ THE ONLY WAY TO WAKE THE LOOP, because the guard was previously written at
……@@ -778,11 +779,26 @@ void UdxTransport::_dispatch_incoming(PeerConnection *peer,
778779 // After noise_ready, ALL data is encrypted data — never check byte prefix.
779780 if (peer->encrypted && peer->noise_ready) {
780781 // Setup complete — decrypt data. No byte-prefix check (matches JS _setup=false).
781 auto pt = peer->noise->decrypt(buf_data, static_cast<size_t>(buf_len));
782 if (pt.empty()) {
782 std::vector<uint8_t> pt;
783 if (!peer->noise->decrypt(buf_data, static_cast<size_t>(buf_len), pt)) {
783784 return; // Auth failure — drop.
784785 }
785786
787 // ⛔ ANY AUTHENTICATED FRAME IS EVIDENCE THIS PEER IS ALIVE, whatever it
788 // carries. That is the signal _sweep_keepalive waits on, and it is
789 // stamped here rather than in the protomux handler so a frame for a
790 // channel nobody opened still counts.
791 peer->last_recv_ms = _loop_now_ms;
792
793 if (pt.empty()) {
794 // The keepalive. @hyperswarm/secret-stream eats these on receipt
795 // ("assume the other side has it enabled also") and so do we —
796 // forwarding an empty frame to protomux would decode a channel id of
797 // 0 and a type of 0 out of nothing at all.
798 peer->peer_keepalive = true;
799 return;
800 }
801
786802 // Route through protomux if available.
787803 if (peer->mux) {
788804 DHT_LOG("[UdxTransport] dispatch: routing %zu decrypted bytes to protomux for peer %d\n",
……@@ -1088,6 +1104,61 @@ int32_t UdxTransport::_next_peer_id() {
10881104
10891105// ---- Protomux helpers (UDX thread) ----
10901106
1107// ⛔ THE LIVENESS THAT NOTHING ELSE PROVIDES. hyper-games #75 PART 3 traced the
1108// hole through three layers; this is the fix, and it is deliberately at OUR
1109// layer rather than libudx's, because libudx's heartbeat cannot be made to
1110// detect death without changing libudx.
1111//
1112// TWO HALVES, and the second is conditional on purpose:
1113//
1114// send an idle peer gets a ZERO-LENGTH encrypted frame every
1115// PEER_KEEPALIVE_INTERVAL_MS. That is the reference's wire form
1116// exactly — @hyperswarm/secret-stream's sendKeepAlive writes
1117// `this.alloc(0)` — so a JS peer eats ours and we eat theirs. A write
1118// of any kind counts, so a busy peer never sends one.
1119//
1120// kill a peer that has sent us nothing for PEER_DEAD_MS is disconnected,
1121// which drives the close path that already exists: _on_stream_close ->
1122// on_mux_gone -> PEER_DOWN.
1123//
1124// ⛔ THE KILL ONLY ARMS ONCE THE PEER HAS SENT A KEEPALIVE ITSELF. A build that
1125// predates this sends nothing while idle, and killing it after twenty seconds
1126// would make an idle connection a flag day — the one thing the swarmd-session
1127// work went out of its way to avoid. Until a peer proves it speaks keepalives,
1128// its silence means what it meant before: nothing.
1129void UdxTransport::_sweep_keepalive(uint64_t now_ms) {
1130 _loop_now_ms = now_ms;
1131
1132 std::vector<int32_t> dead;
1133 for (auto &kv : _peers) {
1134 PeerConnection *peer = kv.second;
1135 if (!peer || !peer->connected || !peer->noise_ready) continue;
1136
1137 // A peer that has never been heard from starts its clock now rather than
1138 // at zero, or it is declared dead the moment it connects.
1139 if (peer->last_recv_ms == 0) peer->last_recv_ms = now_ms;
1140 if (peer->last_send_ms == 0) peer->last_send_ms = now_ms;
1141
1142 if (peer->peer_keepalive && now_ms - peer->last_recv_ms > PEER_DEAD_MS) {
1143 DHT_LOG("[UdxTransport] peer %d silent for %llums — disconnecting\n",
1144 peer->godot_peer_id,
1145 (unsigned long long)(now_ms - peer->last_recv_ms));
1146 dead.push_back(kv.first);
1147 continue;
1148 }
1149
1150 if (now_ms - peer->last_send_ms >= PEER_KEEPALIVE_INTERVAL_MS) {
1151 _send_encrypted(peer, nullptr, 0); // stamps last_send_ms
1152 }
1153 }
1154
1155 // Queued rather than done here: CMD_DISCONNECT already destroys the stream,
1156 // pushes the peer event and removes the peer — a tested path — and doing it
1157 // inline would mutate _peers while this loop walks it. It runs on the next
1158 // tick, one millisecond later.
1159 for (int32_t id : dead) disconnect_peer(id, true);
1160}
1161
10911162void UdxTransport::_send_encrypted(PeerConnection *peer, const uint8_t *data, size_t len) {
10921163 DHT_LOG("[UdxTransport] _send_encrypted: %zu bytes for peer %d (noise=%p noise_ready=%d)\n",
10931164 len, peer->godot_peer_id, (void *)peer->noise, peer->noise_ready);
……@@ -1109,6 +1180,7 @@ void UdxTransport::_send_encrypted(PeerConnection *peer, const uint8_t *data, si
11091180 // stream from one it was burying, so a bulk sender (the overlay's chunk pump)
11101181 // had nothing to throttle against.
11111182 const int rc = udx_stream_write(req, &peer->stream, &buf, 1, _on_write_ack);
1183 peer->last_send_ms = _loop_now_ms; // a write IS a keepalive; see _sweep_keepalive
11121184 peer->writable = rc > 0;
11131185 if (rc == 0) {
11141186 // Arm the drain edge exactly once per peer. udx calls on_drain when the
src/udx_transport.h +36 -0
……@@ -45,6 +45,21 @@ static inline int32_t pk_to_peer_id(const uint8_t *pk) {
4545}
4646
4747// Per-peer connection state managed by UdxTransport.
48// ⛔ LIVENESS, because nothing else provides it. hyper-games #74/#75 traced this
49// through three layers: DhtConnector's keepalive branch is unreachable (the
50// connection is freed the instant it reaches CONNECTED), and libudx's keepalive
51// sends a bare heartbeat OUTSIDE the packet and RTO machinery — its own TODO
52// says so — so an unanswered one never reaches close_stream(UV_ETIMEDOUT). An
53// idle stream to a killed process stayed "connected" for ever.
54//
55// The wire form is the reference's: @hyperswarm/secret-stream's `sendKeepAlive`
56// writes a ZERO-LENGTH encrypted frame, and the receiver eats it
57// (`if (plain.byteLength === 0 && this.keepAlive !== 0) return`).
58static constexpr uint64_t PEER_KEEPALIVE_INTERVAL_MS = 5000; // hyperdht connectionKeepAlive
59// Four consecutive keepalives missed. Long enough that a stalled loop or a
60// hiccup does not cost a healthy peer, short enough to be useful.
61static constexpr uint64_t PEER_DEAD_MS = 20000;
62
4863struct PeerConnection {
4964 udx_stream_t stream;
5065 uint32_t local_stream_id;
……@@ -72,6 +87,16 @@ struct PeerConnection {
7287 bool writable = true;
7388 bool drain_armed = false;
7489
90 // ---- liveness (see UdxTransport::_sweep_keepalive) ----
91 // When we last received an AUTHENTICATED frame from this peer, and when we
92 // last wrote to it. Both in the UDX loop's clock.
93 uint64_t last_recv_ms = 0;
94 uint64_t last_send_ms = 0;
95 // True once this peer has sent an empty keepalive frame. Until it does, its
96 // silence is not evidence of death — it may simply be a build that predates
97 // keepalives, and killing it would be a flag day for idle connections.
98 bool peer_keepalive = false;
99
75100 // Protomux multiplexer (created after Noise handshake completes).
76101 Protomux *mux = nullptr;
77102 // True once the godot-rpc channel has OPENED — the same instant on_mux_ready
……@@ -173,6 +198,11 @@ public:
173198 // Public because it's called from the libuv timer callback.
174199 void _process_commands();
175200
201 // UDX THREAD. Send a keepalive to any idle peer, and disconnect any peer that
202 // has stopped answering. Public for the same reason _process_commands is: the
203 // loop's timer callback is a free function.
204 void _sweep_keepalive(uint64_t now_ms);
205
176206public:
177207 // ---- UDX-thread execution seam -----------------------------------------
178208 //
……@@ -219,6 +249,10 @@ public:
219249 // happened, because a consumer polls from frame one while the thread is still
220250 // starting. Tasks queued early are safe; they are drained by the tick timer
221251 // once the loop is up.
252 // The UDX loop's clock, sampled each tick. Everything on that thread that
253 // needs "now" reads this rather than calling a clock of its own.
254 uint64_t _loop_now_ms = 0;
255
222256 std::atomic<bool> _async_ready{ false };
223257
224258 // Signal the loop. The ONLY way — see the comment on the definition.
……@@ -378,6 +412,8 @@ private:
378412 // Encrypt and send via UDX stream (used as Protomux write callback).
379413 void _send_encrypted(PeerConnection *peer, const uint8_t *data, size_t len);
380414
415
416
381417 // Route decrypted payload to Godot PacketQueue (used by godot-rpc channel).
382418 void _deliver_godot_packet(PeerConnection *peer, const uint8_t *data, size_t len);
383419
swarmd/README.md +3 -4
……@@ -94,10 +94,9 @@ a hundred million, and the gate fails at 256 KB.
9494`--storage <dir>` turns the content plane on. Without it the daemon refuses
9595content commands rather than accepting a fetch it has nowhere to put.
9696
97D7 passes in both directions; **D5 is written and RED**, and what it found is a
98real hole rather than a gate bug — nothing detects a silently dead peer on an
99idle stream. `../docs/SWARMD-DESIGN.md` §8.1 has the trace through all three
100layers. Run it with:
97All seven gates pass. D5 was red for a day and what it found was real — nothing
98detected a silently dead peer on an idle stream; `../docs/SWARMD-DESIGN.md` §8.1
99keeps the trace through all three layers and the fix. Run it with:
101100
102101 python3 -B test/test_peer_down.py --bootstrap host:port
103102
swarmd/test/test_peer_down.py +25 -36
……@@ -1,42 +1,31 @@
11#!/usr/bin/env python3
2"""Gate D5: a peer that goes away is reported on CLOSE, not on a timeout.
3
4⛔ THIS GATE IS RED, AND THE DEFECT IT FOUND IS REAL. Nothing detects a silently
5dead peer on an IDLE stream. Traced 2026-09-10:
6
7 * DhtConnector has a DHT_CONN_CONNECTED keepalive branch with a 30s timeout —
8 unreachable, because _finalize_connection frees the connection the instant it
9 reaches CONNECTED, "UDX stream takes over with its own keepalive".
10 * libudx's keepalive was never switched on. It is now, and it still is not
11 liveness: send_probe writes a bare UDX_HEADER_HEARTBEAT with
12 uv_udp_try_send, outside the packet and RTO machinery — libudx's own TODO at
13 that line says the packet that would fix this is not written. So no
14 retransmit, no UDX_MAX_RTO_TIMEOUTS, no close_stream(UV_ETIMEDOUT).
15 * Only a stream with real data outstanding dies. An idle one never does.
16
17Measured: kill one of two meshed daemons and the survivor reports peers=1 for
18the whole window — 10s, 45s, 60s, 90s, all the same. The fix is an
19application-level liveness ping with its own timeout and teardown, above the
20transport. It is not built. See docs/SWARMD-DESIGN.md §8.
21
2"""Gate D5: a peer that goes away is reported, and not on a wall clock.
223
234 python3 -B test/test_peer_down.py [--bootstrap host:port] [--seconds N]
245
25Two daemons meet on a topic; one is killed; the survivor's client must be told.
6Two daemons meet on a topic; one is KILLED; the survivor's client must be told.
267
27⛔ THE NUMBER THAT MAKES THIS A GATE IS THE DEADLINE, and it is not arbitrary.
28DHT_KEEPALIVE_TIMEOUT_MS is 30 seconds (src/dht_connector.h): if PEER_DOWN only
29ever arrived because a keepalive lapsed, it would still arrive — thirty seconds
30late, with the application meanwhile addressing a peer that is not there. So the
31assertion is that it lands WELL inside that, which a timeout-driven
32implementation cannot do.
8⛔ THE DEADLINE IS DERIVED FROM THE CONSTANT THAT GOVERNS IT, not picked.
9UdxTransport sends a zero-length encrypted keepalive to an idle peer every
10PEER_KEEPALIVE_INTERVAL_MS (5s) and disconnects one that has said nothing for
11PEER_DEAD_MS (20s) — so a correct implementation reports inside ~20 seconds and
12this allows 30. Set DOWN_DEADLINE_S to probe a different bound.
3313
3414⛔ SIGKILL, NOT SIGTERM. A clean shutdown is the easy case and not the one that
35hurts: a process that was killed never gets to say goodbye, and the stream close
36is the only signal there is.
15hurts: a process that was killed never gets to say goodbye.
3716
3817⛔ IT ASSERTS ITS OWN BASELINE. If the two daemons never meet, "no PEER-DOWN"
3918means nothing, so a swarm that never meshed exits INCONCLUSIVE rather than FAIL.
19
20⚠ WHAT THIS GATE COST, recorded because the shape recurs (hyper-games #75):
21nothing detected a dead peer at all, and three layers each looked like they did.
22DhtConnector's DHT_CONN_CONNECTED keepalive branch is unreachable — the
23connection is freed the instant it reaches CONNECTED. libudx's keepalive was
24never switched on, and switching it on is not liveness either: send_probe writes
25a bare heartbeat OUTSIDE the packet and RTO machinery, and libudx's own TODO at
26that line says the packet that would fix it is not written. The fix had to be at
27our layer, and its wire form is the reference's — @hyperswarm/secret-stream
28sends an empty encrypted frame and eats the ones it receives.
4029"""
4130
4231import os, re, signal, subprocess, sys, tempfile, time
……@@ -50,9 +39,9 @@ EXIT_PASS, EXIT_FAIL, EXIT_INCONCLUSIVE = 0, 1, 2
5039TOPIC = "d5" * 32
5140PROTOCOL = "swarmd-peerdown/v1"
5241
53# Comfortably inside DHT_KEEPALIVE_TIMEOUT_MS (30s). A timeout-driven report
54# cannot land here; a close-driven one lands almost immediately.
55DOWN_DEADLINE_S = int(os.environ.get("DOWN_DEADLINE_S", "10"))
42# PEER_DEAD_MS is 20s (src/udx_transport.h), so a working implementation reports
43# inside that. Thirty allows for the keepalive phase and a slow testnet.
44DOWN_DEADLINE_S = int(os.environ.get("DOWN_DEADLINE_S", "30"))
5645
5746
5847def arg(name, default=None):
……@@ -141,15 +130,15 @@ def main():
141130 elapsed = time.time() - killed_at
142131
143132 if not seen:
144 print("\nFAIL: A was not told its peer had gone within %ds. "
145 "DHT_KEEPALIVE_TIMEOUT_MS is 30s, so a report driven by a "
146 "timeout rather than by the close would look exactly like this."
133 print("\nFAIL: A was not told its peer had gone within %ds, and "
134 "PEER_DEAD_MS is 20s — so either the keepalive is not being "
135 "sent, or the sweep is not disconnecting on it."
147136 % DOWN_DEADLINE_S)
148137 print("--- A ---\n%s" % read(logs["watch-a"])[-1500:])
149138 return EXIT_FAIL
150139
151140 print("\nPASS: PEER-DOWN reached the client %.1fs after the peer was "
152 "killed (deadline %ds, keepalive timeout 30s)." % (elapsed, DOWN_DEADLINE_S))
141 "killed (PEER_DEAD_MS 20s, deadline %ds)." % (elapsed, DOWN_DEADLINE_S))
153142 if status and status.group(1) != "2":
154143 print(" ⚠ but STATUS reported dht_state=%s while the daemon's own log "
155144 "said dht=up — those disagree." % status.group(1))
test/test_ann_signable +0 -0
(no content changes — binary or rename)
test/test_bootstrap_ownership +0 -0
(no content changes — binary or rename)
test/test_find_peer_exhausted +0 -0
(no content changes — binary or rename)
test/test_handshake_pool +0 -0
(no content changes — binary or rename)
test/test_peer_topics +0 -0
(no content changes — binary or rename)
test/test_regression +0 -0
(no content changes — binary or rename)
test/test_relay_error_abort +0 -0
(no content changes — binary or rename)
test/test_self_announce +0 -0
(no content changes — binary or rename)
test/test_self_announce_live +0 -0
(no content changes — binary or rename)
test/test_session_channel +0 -0
(no content changes — binary or rename)
test/test_three_peer_swarm +0 -0
(no content changes — binary or rename)
test/test_unannounce_live +0 -0
(no content changes — binary or rename)