upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-08-09 02:43:44 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-08-19 22:48:26 +0000
commit82ac5bbc05a1de2be752e2f89110af1e424bc863 (patch)
tree4498fd222a5391130db055c66bc16526fecea4c7 /src
parent06bceb4d3e368bec47c4c2eaafdc1c1071625d30 (diff)
pool: wait to be paired before wrapping the rendezvous socket
Upstream #30 split rendezvous registration from waiting for a dial, so RvServiceRegister now returns as soon as the relay lists the service and no longer consumes the relay's 0x01 pairing byte. It updated its own caller in net.cpp and left this one behind. AcceptOneFn went straight from RvServiceRegister to BtfServiceWrap, so BtfServiceWrap read that 0x01 as the first byte of the miner's 32-byte ephemeral key and every byte after it was shifted by one. The X25519 handshake has no confirmation step, so it completed against a garbage shared secret, the tunnel was handed to the Stratum event loop, and nothing failed loudly: the pool logged a miner arriving and then dropped it as "never authorized", while the miner logged a pool that closed on it with gotSubscribeAck=0. Loopback miners were unaffected, which is exactly what made it look like a relay problem. Block in RvServiceWaitPaired before wrapping, and close and re-register if the relay drops us first. test_rvaccept.cpp covers both shapes against a real bitflashd on loopback: the pre-fix order establishes a tunnel that carries no bytes, the fixed order round-trips. Also corrects the btftunnel.h contract comment, which still described the pre-#30 behaviour and is what the old code was written against -- upstream's own test_relayproc.cpp, test_btftunnel.cpp and test_btfrv.cpp are still on that stale contract and fail the same way. (cherry picked from commit 3e3a36b2bf1e6ffb4861507089b743688619377f)
Diffstat (limited to 'src')
-rw-r--r--src/btftunnel.h13
-rw-r--r--src/rpc.cpp28
-rw-r--r--src/test_rvaccept.cpp180
3 files changed, 211 insertions, 10 deletions
diff --git a/src/btftunnel.h b/src/btftunnel.h
index 1fa4f01..24a7509 100644
--- a/src/btftunnel.h
+++ b/src/btftunnel.h
@@ -10,8 +10,12 @@
10// caller), then BtfClientTunnel connects through the relay, runs the echan 10// caller), then BtfClientTunnel connects through the relay, runs the echan
11// handshake, and returns a local SOCKET to hand a new outbound CNode. 11// handshake, and returns a local SOCKET to hand a new outbound CNode.
12// 12//
13// Service side: after RvServiceRegister pairs us with a client, BtfServiceWrap 13// Service side: RvServiceRegister only registers -- callers must then block in
14// completes the echan handshake and returns a local SOCKET for an inbound CNode. 14// RvServiceWaitPaired until a client is actually paired to them. BtfServiceWrap
15// completes the echan handshake on that paired socket and returns a local
16// SOCKET for an inbound CNode. Wrapping a registered-but-unpaired socket reads
17// the relay's 0x01 pairing byte as the first byte of the client's ephemeral
18// key, which silently corrupts the tunnel instead of failing.
15 19
16#ifndef BITFLASH_BTFTUNNEL_H 20#ifndef BITFLASH_BTFTUNNEL_H
17#define BITFLASH_BTFTUNNEL_H 21#define BITFLASH_BTFTUNNEL_H
@@ -39,8 +43,9 @@ btf_socket_t BtfClientTunnel(const char* meeting_host, unsigned short port,
39 const unsigned char target_pubkey[32], 43 const unsigned char target_pubkey[32],
40 const unsigned char service_enc_pub[32]); 44 const unsigned char service_enc_pub[32]);
41 45
42// Service: wrap an already-paired rendezvous socket (from RvServiceRegister) 46// Service: wrap an already-paired rendezvous socket (RvServiceRegister followed
43// with echan using my static x25519 secret, returning a local plaintext SOCKET 47// by RvServiceWaitPaired -- the pairing byte MUST already be consumed) with
48// echan using my static x25519 secret, returning a local plaintext SOCKET
44// for an inbound CNode. Consumes `rv` (closed when the tunnel tears down). 49// for an inbound CNode. Consumes `rv` (closed when the tunnel tears down).
45btf_socket_t BtfServiceWrap(RvSocket rv, const unsigned char my_enc_sk[32]); 50btf_socket_t BtfServiceWrap(RvSocket rv, const unsigned char my_enc_sk[32]);
46 51
diff --git a/src/rpc.cpp b/src/rpc.cpp
index 3dd05bb..21a8d77 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -6,8 +6,8 @@
6// Architecture: 6// Architecture:
7// PoolEventLoop -- single thread, owns all miner fds, all job/share/payout 7// PoolEventLoop -- single thread, owns all miner fds, all job/share/payout
8// state. No locks needed on any of that data. 8// state. No locks needed on any of that data.
9// AcceptOneFn -- one thread per relay. Blocks on RvServiceRegister + 9// AcceptOneFn -- one thread per relay. Registers, waits to be paired,
10// BtfServiceWrap, pushes finished socket to gReadyQueue. 10// wraps the tunnel, pushes finished socket to gReadyQueue.
11// AcceptLoopFn -- spawns AcceptOneFn threads, re-polls relay list every 11// AcceptLoopFn -- spawns AcceptOneFn threads, re-polls relay list every
12// 60s so dynamically discovered relays get picked up. 12// 60s so dynamically discovered relays get picked up.
13// 13//
@@ -1388,12 +1388,11 @@ static void AcceptOneFn(void* arg)
1388 LogPrint("pool", "[pool] accept thread started for relay %s\n", relay.c_str()); 1388 LogPrint("pool", "[pool] accept thread started for relay %s\n", relay.c_str());
1389 1389
1390 while (!fShutdown && gPoolRunning) { 1390 while (!fShutdown && gPoolRunning) {
1391 // Advertise this relay as the pool's meeting node BEFORE registering: 1391 // Advertise this relay as the pool's meeting node BEFORE registering,
1392 // RvServiceRegister blocks until a miner pairs, and miners can only 1392 // so that miners which resolve our descriptor find a relay where the
1393 // find us if the published descriptor already names a relay where the
1394 // pool (not the node's peer-accept) holds the registration. 1393 // pool (not the node's peer-accept) holds the registration.
1395 BtfSetActiveRelay(relay); 1394 BtfSetActiveRelay(relay);
1396 // Blocks until a miner connects to us at this relay 1395 // Returns as soon as the relay lists us -- registered, not yet paired.
1397 btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk); 1396 btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk);
1398 1397
1399 if (fShutdown || !gPoolRunning) { 1398 if (fShutdown || !gPoolRunning) {
@@ -1407,6 +1406,23 @@ static void AcceptOneFn(void* arg)
1407 continue; 1406 continue;
1408 } 1407 }
1409 1408
1409 // Block for a miner to actually arrive. RvServiceRegister used to do
1410 // this itself and returned with the relay's 0x01 pairing byte already
1411 // consumed; it no longer does. Without waiting here, BtfServiceWrap
1412 // reads that 0x01 as the first byte of the miner's 32-byte ephemeral
1413 // key -- every subsequent byte is shifted by one, so the X25519
1414 // handshake completes against a garbage shared secret (it has no
1415 // confirmation step to catch it), the tunnel is handed to the event
1416 // loop, and the miner's mining.subscribe never decrypts into a valid
1417 // line. The miner sees the pool close on it; the pool sees a peer that
1418 // never authorized.
1419 if (!btf::RvServiceWaitPaired(rv)) {
1420 btf::RvClose(rv);
1421 LogPrint("pool", "[pool] relay %s: dropped us before any miner dialed"
1422 " -- re-registering\n", relay.c_str());
1423 continue;
1424 }
1425
1410 // Echan handshake -- blocking but fast (local + relay round-trip) 1426 // Echan handshake -- blocking but fast (local + relay round-trip)
1411 btf_socket_t fd = btf::BtfServiceWrap(rv, sk); 1427 btf_socket_t fd = btf::BtfServiceWrap(rv, sk);
1412 if (fd == INVALID_SOCKET) { 1428 if (fd == INVALID_SOCKET) {
diff --git a/src/test_rvaccept.cpp b/src/test_rvaccept.cpp
new file mode 100644
index 0000000..afa9046
--- /dev/null
+++ b/src/test_rvaccept.cpp
@@ -0,0 +1,180 @@
1// Local regression test for the pool's rendezvous accept path.
2//
3// Reproduces the exact failure that took remote miners off the pool: since
4// upstream #30, RvServiceRegister returns as soon as the relay lists the
5// service and no longer consumes the relay's 0x01 pairing byte. A service that
6// goes straight from RvServiceRegister to BtfServiceWrap therefore reads that
7// 0x01 as the first byte of the client's 32-byte ephemeral key, shifting the
8// whole stream by one. X25519 has no confirmation step, so the handshake
9// "succeeds" against a garbage shared secret and the corruption only shows up
10// as a tunnel that never carries a valid byte.
11//
12// Build: g++ -std=gnu++14 -O2 -pthread test_rvaccept.cpp btftunnel.cpp btfrv.cpp \
13// btfchan.cpp -lsodium -o test_rvaccept
14//
15// Build: g++ -std=gnu++14 -O2 -pthread test_rvaccept.cpp btftunnel.cpp \
16// btfrv.cpp btfchan.cpp -lsodium -o test_rvaccept
17//
18// Run against a bitflashd relay already listening on 127.0.0.1:<port>.
19// test_rvaccept broken <port> -- the pre-fix pool code path, expected to FAIL
20// test_rvaccept fixed <port> -- with RvServiceWaitPaired, expected to PASS
21
22#include "btftunnel.h"
23#include "btfrv.h"
24#include "btfchan.h"
25
26#include <sys/socket.h>
27#include <sys/time.h>
28#include <unistd.h>
29#include <cstdio>
30#include <cstdlib>
31#include <cstring>
32#include <string>
33#include <vector>
34#include <thread>
35#include <chrono>
36
37using namespace btf;
38
39static const char* HOST = "127.0.0.1";
40static unsigned short PORT = 18446;
41static const char* MSG = "mining.subscribe stand-in payload";
42
43static void SetTimeout(int fd, int secs)
44{
45 struct timeval tv;
46 tv.tv_sec = secs;
47 tv.tv_usec = 0;
48 setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
49 setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
50}
51
52static bool RecvN(int fd, void* buf, int n)
53{
54 char* p = (char*)buf;
55 int off = 0;
56 while (off < n) {
57 int r = recv(fd, p + off, n - off, 0);
58 if (r <= 0) return false;
59 off += r;
60 }
61 return true;
62}
63
64static bool SendN(int fd, const void* buf, int n)
65{
66 const char* p = (const char*)buf;
67 int off = 0;
68 while (off < n) {
69 int r = send(fd, p + off, n - off, 0);
70 if (r <= 0) return false;
71 off += r;
72 }
73 return true;
74}
75
76static bool g_serviceEcho = false;
77static std::string g_serviceSaw;
78
79// The pool's AcceptOneFn, in both shapes.
80static void ServiceThread(bool fWaitPaired,
81 std::vector<unsigned char> id,
82 std::vector<unsigned char> enc_sk,
83 int len)
84{
85 RvSocket rv = RvServiceRegister(HOST, PORT, id.data());
86 if (rv == RV_INVALID) { printf(" service: RvServiceRegister failed\n"); return; }
87
88 if (fWaitPaired) {
89 if (!RvServiceWaitPaired(rv)) {
90 printf(" service: RvServiceWaitPaired failed\n");
91 RvClose(rv);
92 return;
93 }
94 printf(" service: paired (0x01 consumed by RvServiceWaitPaired)\n");
95 } else {
96 printf(" service: skipped RvServiceWaitPaired (pre-fix behaviour)\n");
97 }
98
99 btf_socket_t app = BtfServiceWrap(rv, enc_sk.data());
100 if (app == INVALID_SOCKET) { printf(" service: BtfServiceWrap failed\n"); return; }
101 printf(" service: BtfServiceWrap returned a tunnel\n");
102
103 SetTimeout(app, 5);
104 std::vector<char> buf(len);
105 if (!RecvN(app, buf.data(), len)) {
106 printf(" service: never received the client's plaintext\n");
107 close(app);
108 return;
109 }
110 g_serviceSaw.assign(buf.data(), len);
111 g_serviceEcho = SendN(app, buf.data(), len);
112 close(app);
113}
114
115int main(int argc, char* argv[])
116{
117 bool fWaitPaired = (argc >= 2 && strcmp(argv[1], "fixed") == 0);
118 if (argc >= 3) PORT = (unsigned short)atoi(argv[2]);
119
120 if (!TunnelInit()) { printf("TunnelInit failed\n"); return 2; }
121
122 unsigned char enc_pk[32], enc_sk[32];
123 ChanKeypair(enc_pk, enc_sk);
124
125 std::vector<unsigned char> id(32);
126 for (int i = 0; i < 32; i++) id[i] = (unsigned char)(i * 7 + 13);
127
128 const std::string msg(MSG);
129 const int len = (int)msg.size();
130
131 printf(" mode: %s relay: %s:%u\n", fWaitPaired ? "fixed" : "broken", HOST, PORT);
132
133 std::thread svc(ServiceThread, fWaitPaired, id,
134 std::vector<unsigned char>(enc_sk, enc_sk + 32), len);
135 std::this_thread::sleep_for(std::chrono::milliseconds(500));
136
137 int nFail = 0;
138
139 btf_socket_t app = BtfClientTunnel(HOST, PORT, id.data(), enc_pk);
140 if (app == INVALID_SOCKET) {
141 printf(" client: BtfClientTunnel failed\n");
142 nFail++;
143 } else {
144 printf(" client: tunnel established\n");
145 SetTimeout(app, 5);
146 if (!SendN(app, msg.data(), len)) { printf(" client: send failed\n"); nFail++; }
147 std::vector<char> echo(len);
148 if (!RecvN(app, echo.data(), len)) {
149 printf(" client: no echo came back\n");
150 nFail++;
151 } else if (std::string(echo.data(), len) != msg) {
152 printf(" client: echo did not match what was sent\n");
153 nFail++;
154 } else {
155 printf(" client: round-tripped the payload intact\n");
156 }
157 close(app);
158 }
159
160 svc.join();
161
162 if (g_serviceSaw != msg) {
163 printf(" service did not see the correct plaintext (saw %d bytes)\n",
164 (int)g_serviceSaw.size());
165 nFail++;
166 }
167 if (!g_serviceEcho) { printf(" service could not echo\n"); nFail++; }
168
169 bool fPassed = (nFail == 0);
170 printf(" RESULT: %s\n", fPassed ? "PASS -- tunnel carries plaintext end to end"
171 : "FAIL -- tunnel never carried a valid byte");
172
173 // A broken run is the expected outcome pre-fix, so exit 0 when the
174 // observed behaviour matches the expectation for the mode under test.
175 bool fAsExpected = (fWaitPaired ? fPassed : !fPassed);
176 printf(" EXPECTED %s -> %s\n",
177 fWaitPaired ? "PASS" : "FAIL",
178 fAsExpected ? "as expected" : "UNEXPECTED");
179 return fAsExpected ? 0 : 1;
180}