From 82ac5bbc05a1de2be752e2f89110af1e424bc863 Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Sun, 9 Aug 2026 02:43:44 +0000 Subject: 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) --- src/test_rvaccept.cpp | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 src/test_rvaccept.cpp (limited to 'src/test_rvaccept.cpp') 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 @@ +// Local regression test for the pool's rendezvous accept path. +// +// Reproduces the exact failure that took remote miners off the pool: since +// upstream #30, RvServiceRegister returns as soon as the relay lists the +// service and no longer consumes the relay's 0x01 pairing byte. A service that +// goes straight from RvServiceRegister to BtfServiceWrap therefore reads that +// 0x01 as the first byte of the client's 32-byte ephemeral key, shifting the +// whole stream by one. X25519 has no confirmation step, so the handshake +// "succeeds" against a garbage shared secret and the corruption only shows up +// as a tunnel that never carries a valid byte. +// +// Build: g++ -std=gnu++14 -O2 -pthread test_rvaccept.cpp btftunnel.cpp btfrv.cpp \ +// btfchan.cpp -lsodium -o test_rvaccept +// +// Build: g++ -std=gnu++14 -O2 -pthread test_rvaccept.cpp btftunnel.cpp \ +// btfrv.cpp btfchan.cpp -lsodium -o test_rvaccept +// +// Run against a bitflashd relay already listening on 127.0.0.1:. +// test_rvaccept broken -- the pre-fix pool code path, expected to FAIL +// test_rvaccept fixed -- with RvServiceWaitPaired, expected to PASS + +#include "btftunnel.h" +#include "btfrv.h" +#include "btfchan.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace btf; + +static const char* HOST = "127.0.0.1"; +static unsigned short PORT = 18446; +static const char* MSG = "mining.subscribe stand-in payload"; + +static void SetTimeout(int fd, int secs) +{ + struct timeval tv; + tv.tv_sec = secs; + tv.tv_usec = 0; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); +} + +static bool RecvN(int fd, void* buf, int n) +{ + char* p = (char*)buf; + int off = 0; + while (off < n) { + int r = recv(fd, p + off, n - off, 0); + if (r <= 0) return false; + off += r; + } + return true; +} + +static bool SendN(int fd, const void* buf, int n) +{ + const char* p = (const char*)buf; + int off = 0; + while (off < n) { + int r = send(fd, p + off, n - off, 0); + if (r <= 0) return false; + off += r; + } + return true; +} + +static bool g_serviceEcho = false; +static std::string g_serviceSaw; + +// The pool's AcceptOneFn, in both shapes. +static void ServiceThread(bool fWaitPaired, + std::vector id, + std::vector enc_sk, + int len) +{ + RvSocket rv = RvServiceRegister(HOST, PORT, id.data()); + if (rv == RV_INVALID) { printf(" service: RvServiceRegister failed\n"); return; } + + if (fWaitPaired) { + if (!RvServiceWaitPaired(rv)) { + printf(" service: RvServiceWaitPaired failed\n"); + RvClose(rv); + return; + } + printf(" service: paired (0x01 consumed by RvServiceWaitPaired)\n"); + } else { + printf(" service: skipped RvServiceWaitPaired (pre-fix behaviour)\n"); + } + + btf_socket_t app = BtfServiceWrap(rv, enc_sk.data()); + if (app == INVALID_SOCKET) { printf(" service: BtfServiceWrap failed\n"); return; } + printf(" service: BtfServiceWrap returned a tunnel\n"); + + SetTimeout(app, 5); + std::vector buf(len); + if (!RecvN(app, buf.data(), len)) { + printf(" service: never received the client's plaintext\n"); + close(app); + return; + } + g_serviceSaw.assign(buf.data(), len); + g_serviceEcho = SendN(app, buf.data(), len); + close(app); +} + +int main(int argc, char* argv[]) +{ + bool fWaitPaired = (argc >= 2 && strcmp(argv[1], "fixed") == 0); + if (argc >= 3) PORT = (unsigned short)atoi(argv[2]); + + if (!TunnelInit()) { printf("TunnelInit failed\n"); return 2; } + + unsigned char enc_pk[32], enc_sk[32]; + ChanKeypair(enc_pk, enc_sk); + + std::vector id(32); + for (int i = 0; i < 32; i++) id[i] = (unsigned char)(i * 7 + 13); + + const std::string msg(MSG); + const int len = (int)msg.size(); + + printf(" mode: %s relay: %s:%u\n", fWaitPaired ? "fixed" : "broken", HOST, PORT); + + std::thread svc(ServiceThread, fWaitPaired, id, + std::vector(enc_sk, enc_sk + 32), len); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + int nFail = 0; + + btf_socket_t app = BtfClientTunnel(HOST, PORT, id.data(), enc_pk); + if (app == INVALID_SOCKET) { + printf(" client: BtfClientTunnel failed\n"); + nFail++; + } else { + printf(" client: tunnel established\n"); + SetTimeout(app, 5); + if (!SendN(app, msg.data(), len)) { printf(" client: send failed\n"); nFail++; } + std::vector echo(len); + if (!RecvN(app, echo.data(), len)) { + printf(" client: no echo came back\n"); + nFail++; + } else if (std::string(echo.data(), len) != msg) { + printf(" client: echo did not match what was sent\n"); + nFail++; + } else { + printf(" client: round-tripped the payload intact\n"); + } + close(app); + } + + svc.join(); + + if (g_serviceSaw != msg) { + printf(" service did not see the correct plaintext (saw %d bytes)\n", + (int)g_serviceSaw.size()); + nFail++; + } + if (!g_serviceEcho) { printf(" service could not echo\n"); nFail++; } + + bool fPassed = (nFail == 0); + printf(" RESULT: %s\n", fPassed ? "PASS -- tunnel carries plaintext end to end" + : "FAIL -- tunnel never carried a valid byte"); + + // A broken run is the expected outcome pre-fix, so exit 0 when the + // observed behaviour matches the expectation for the mode under test. + bool fAsExpected = (fWaitPaired ? fPassed : !fPassed); + printf(" EXPECTED %s -> %s\n", + fWaitPaired ? "PASS" : "FAIL", + fAsExpected ? "as expected" : "UNEXPECTED"); + return fAsExpected ? 0 : 1; +} -- cgit v1.2.3