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-09-03 21:49:35 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-03 21:49:35 +0000
commitea8fe94658bf20d2b669b619231238f12e6bd8fc (patch)
tree02b8f4485d3e4a4117a3c16751af24a702037ce9 /src
parent944f93f163cb560f095556834b6eb75bfde60c9c (diff)
parentca03f8e9416837acfac825f02a6c592e07e0c4df (diff)
merge: !8 fix/tunnel-retry into deploy/all-2026-08-24
Diffstat (limited to 'src')
-rw-r--r--src/btfrv.cpp16
-rw-r--r--src/btfrv.h19
-rw-r--r--src/btftunnel.cpp37
-rw-r--r--src/btftunnel.h25
-rw-r--r--src/main.cpp106
5 files changed, 182 insertions, 21 deletions
diff --git a/src/btfrv.cpp b/src/btfrv.cpp
index 8ab735c..7d705ef 100644
--- a/src/btfrv.cpp
+++ b/src/btfrv.cpp
@@ -377,17 +377,29 @@ bool RvServiceWaitPaired(RvSocket s, int nTimeoutSecs)
377} 377}
378 378
379RvSocket RvClientConnect(const char* relay_host, unsigned short port, 379RvSocket RvClientConnect(const char* relay_host, unsigned short port,
380 const unsigned char target_pubkey[32]) 380 const unsigned char target_pubkey[32],
381 RvClientFail* pFail)
381{ 382{
383 if (pFail) *pFail = RVC_FAIL_DIAL;
382 if (!RvInit()) return RV_INVALID; 384 if (!RvInit()) return RV_INVALID;
383 SOCKET s = ConnectTo(relay_host, port); 385 SOCKET s = ConnectTo(relay_host, port);
384 if (s == INVALID_SOCKET) return RV_INVALID; 386 if (s == INVALID_SOCKET) return RV_INVALID;
387 if (pFail) *pFail = RVC_FAIL_PAIR;
385 unsigned char hdr[33]; 388 unsigned char hdr[33];
386 hdr[0] = (unsigned char)'C'; 389 hdr[0] = (unsigned char)'C';
387 memcpy(hdr + 1, target_pubkey, 32); 390 memcpy(hdr + 1, target_pubkey, 32);
388 if (!WriteN(s, hdr, 33)) { CLOSESOCK(s); return RV_INVALID; } 391 if (!WriteN(s, hdr, 33)) { CLOSESOCK(s); return RV_INVALID; }
389 unsigned char paired = 0; 392 unsigned char paired = 0;
390 if (!ReadN(s, &paired, 1) || paired != 0x01) { CLOSESOCK(s); return RV_INVALID; } 393 if (!ReadN(s, &paired, 1)) { CLOSESOCK(s); return RV_INVALID; }
394 if (paired != 0x01)
395 {
396 // The relay answered and declined: HandleIncoming writes 0x00 and
397 // closes when no service is parked under this pubkey right now.
398 if (pFail) *pFail = RVC_FAIL_REFUSED;
399 CLOSESOCK(s);
400 return RV_INVALID;
401 }
402 if (pFail) *pFail = RVC_FAIL_NONE;
391 return (RvSocket)s; 403 return (RvSocket)s;
392} 404}
393 405
diff --git a/src/btfrv.h b/src/btfrv.h
index 9cd7476..1e85cb0 100644
--- a/src/btfrv.h
+++ b/src/btfrv.h
@@ -57,10 +57,25 @@ RvSocket RvServiceRegister(const char* relay_host, unsigned short port,
57bool RvServiceWaitPaired(RvSocket s, int nTimeoutSecs = 0); 57bool RvServiceWaitPaired(RvSocket s, int nTimeoutSecs = 0);
58 58
59// ---- client side (the node dialing a .btf address) ---- 59// ---- client side (the node dialing a .btf address) ----
60// Where a failed RvClientConnect stopped. RV_INVALID alone collapses three
61// different situations -- relay unreachable, relay reachable but nobody
62// registered, connection died mid-pairing -- and they point at different
63// components. The caller can only report the right one if this side says
64// which it saw.
65enum RvClientFail
66{
67 RVC_FAIL_NONE = 0,
68 RVC_FAIL_DIAL, // TCP connect to the relay itself failed
69 RVC_FAIL_REFUSED, // relay answered 0x00: no service registered under this pubkey
70 RVC_FAIL_PAIR, // relay connection died before pairing completed
71};
72
60// Connect through the relay to the service registered under `target_pubkey`. 73// Connect through the relay to the service registered under `target_pubkey`.
61// Returns the paired socket, or RV_INVALID on error. 74// Returns the paired socket, or RV_INVALID on error -- with `pFail`, if
75// given, naming the stage that failed.
62RvSocket RvClientConnect(const char* relay_host, unsigned short port, 76RvSocket RvClientConnect(const char* relay_host, unsigned short port,
63 const unsigned char target_pubkey[32]); 77 const unsigned char target_pubkey[32],
78 RvClientFail* pFail = 0);
64 79
65// Blocking read/write of exactly n bytes over a rendezvous socket. Return false 80// Blocking read/write of exactly n bytes over a rendezvous socket. Return false
66// on error/EOF. 81// on error/EOF.
diff --git a/src/btftunnel.cpp b/src/btftunnel.cpp
index 4acca1a..4058f6b 100644
--- a/src/btftunnel.cpp
+++ b/src/btftunnel.cpp
@@ -231,18 +231,47 @@ static btf_socket_t StartTunnel(RvSocket rv, const unsigned char shared[32])
231 return appEnd; 231 return appEnd;
232} 232}
233 233
234const char* TunnelFailText(TunnelFail f)
235{
236 switch (f)
237 {
238 case TUNNEL_FAIL_NONE: return "no failure";
239 case TUNNEL_FAIL_INIT: return "local socket/crypto init failed";
240 case TUNNEL_FAIL_DIAL: return "meeting node unreachable (tcp dial failed)";
241 case TUNNEL_FAIL_REFUSED: return "relay reachable, but no service registered there for this address";
242 case TUNNEL_FAIL_PAIR: return "relay connection died before pairing completed";
243 case TUNNEL_FAIL_HANDSHAKE: return "end-to-end handshake failed";
244 case TUNNEL_FAIL_LOCAL: return "local tunnel setup failed";
245 }
246 return "unknown failure";
247}
248
234btf_socket_t BtfClientTunnel(const char* meeting_host, unsigned short port, 249btf_socket_t BtfClientTunnel(const char* meeting_host, unsigned short port,
235 const unsigned char target_pubkey[32], 250 const unsigned char target_pubkey[32],
236 const unsigned char service_enc_pub[32]) 251 const unsigned char service_enc_pub[32],
252 TunnelFail* pFail)
237{ 253{
254 if (pFail) *pFail = TUNNEL_FAIL_INIT;
238 if (!TunnelInit()) return INVALID_SOCKET; 255 if (!TunnelInit()) return INVALID_SOCKET;
239 RvSocket rv = RvClientConnect(meeting_host, port, target_pubkey); 256 RvClientFail rvFail = RVC_FAIL_NONE;
240 if (rv == RV_INVALID) return INVALID_SOCKET; 257 RvSocket rv = RvClientConnect(meeting_host, port, target_pubkey, &rvFail);
258 if (rv == RV_INVALID)
259 {
260 if (pFail)
261 *pFail = rvFail == RVC_FAIL_DIAL ? TUNNEL_FAIL_DIAL
262 : rvFail == RVC_FAIL_REFUSED ? TUNNEL_FAIL_REFUSED
263 : TUNNEL_FAIL_PAIR;
264 return INVALID_SOCKET;
265 }
241 266
242 unsigned char eph_pub[32], shared[32]; 267 unsigned char eph_pub[32], shared[32];
268 if (pFail) *pFail = TUNNEL_FAIL_HANDSHAKE;
243 if (!ChanClientHandshake(service_enc_pub, eph_pub, shared)) { RvClose(rv); return INVALID_SOCKET; } 269 if (!ChanClientHandshake(service_enc_pub, eph_pub, shared)) { RvClose(rv); return INVALID_SOCKET; }
244 if (!RvWriteN(rv, eph_pub, 32)) { RvClose(rv); return INVALID_SOCKET; } 270 if (!RvWriteN(rv, eph_pub, 32)) { RvClose(rv); return INVALID_SOCKET; }
245 return StartTunnel(rv, shared); 271 if (pFail) *pFail = TUNNEL_FAIL_LOCAL;
272 btf_socket_t app = StartTunnel(rv, shared);
273 if (app != INVALID_SOCKET && pFail) *pFail = TUNNEL_FAIL_NONE;
274 return app;
246} 275}
247 276
248btf_socket_t BtfServiceWrap(RvSocket rv, const unsigned char my_enc_sk[32]) 277btf_socket_t BtfServiceWrap(RvSocket rv, const unsigned char my_enc_sk[32])
diff --git a/src/btftunnel.h b/src/btftunnel.h
index 24a7509..3cc87cd 100644
--- a/src/btftunnel.h
+++ b/src/btftunnel.h
@@ -36,12 +36,33 @@ namespace btf
36// Initialize (sockets + libsodium). Idempotent. 36// Initialize (sockets + libsodium). Idempotent.
37bool TunnelInit(); 37bool TunnelInit();
38 38
39// Which stage of BtfClientTunnel failed. INVALID_SOCKET alone says only "no
40// tunnel"; a caller that then guesses at causes in its log line sends whoever
41// reads it to the wrong component (issue #17: "relay unreachable or pool not
42// listening" blamed two healthy parts). The stage is what this side actually
43// knows -- report that.
44enum TunnelFail
45{
46 TUNNEL_FAIL_NONE = 0,
47 TUNNEL_FAIL_INIT, // local socket/crypto init failed
48 TUNNEL_FAIL_DIAL, // TCP connect to the meeting node failed
49 TUNNEL_FAIL_REFUSED, // relay reachable, but no service registered under the pubkey
50 TUNNEL_FAIL_PAIR, // relay connection died before pairing completed
51 TUNNEL_FAIL_HANDSHAKE, // end-to-end channel handshake failed
52 TUNNEL_FAIL_LOCAL, // local pump plumbing (socket pair / threads) failed
53};
54
55// Short description of a TunnelFail for log lines.
56const char* TunnelFailText(TunnelFail f);
57
39// Client: dial the service registered under `target_pubkey` at the meeting node 58// Client: dial the service registered under `target_pubkey` at the meeting node
40// (host:port), box to its static x25519 `service_enc_pub`, and return a local 59// (host:port), box to its static x25519 `service_enc_pub`, and return a local
41// plaintext SOCKET tunneled end-to-end to the service. INVALID_SOCKET on error. 60// plaintext SOCKET tunneled end-to-end to the service. INVALID_SOCKET on error
61// -- with `pFail`, if given, naming the stage that failed.
42btf_socket_t BtfClientTunnel(const char* meeting_host, unsigned short port, 62btf_socket_t BtfClientTunnel(const char* meeting_host, unsigned short port,
43 const unsigned char target_pubkey[32], 63 const unsigned char target_pubkey[32],
44 const unsigned char service_enc_pub[32]); 64 const unsigned char service_enc_pub[32],
65 TunnelFail* pFail = 0);
45 66
46// Service: wrap an already-paired rendezvous socket (RvServiceRegister followed 67// Service: wrap an already-paired rendezvous socket (RvServiceRegister followed
47// by RvServiceWaitPaired -- the pairing byte MUST already be consumed) with 68// by RvServiceWaitPaired -- the pairing byte MUST already be consumed) with
diff --git a/src/main.cpp b/src/main.cpp
index 681673c..6b8e0de 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -3970,6 +3970,14 @@ static void ParticipantHashWorker(ParticipantJobShared* sh, int idx, int nWorker
3970 RandomXDestroyMinerVM(vm); 3970 RandomXDestroyMinerVM(vm);
3971} 3971}
3972 3972
3973// Run one participant session: resolve the pool, open the tunnel, subscribe,
3974// hash, submit shares. Returns true only if the pool actually handed out work
3975// -- at least one well-formed mining.notify arrived. Everything else is false,
3976// including a tunnel that opened and was dropped before any job.
3977//
3978// The caller's reconnect backoff keys off this return value, so any path added
3979// here that leaves the session without work MUST return false. "The session
3980// ended tidily" is not the question being asked.
3973static bool PoolParticipantMiner() 3981static bool PoolParticipantMiner()
3974{ 3982{
3975 // ------------------------------------------------------------------ 3983 // ------------------------------------------------------------------
@@ -4047,6 +4055,33 @@ static bool PoolParticipantMiner()
4047 } 4055 }
4048 LogPrint("worker", "[worker] pool resolved to meeting node %s\n", meetingHostPort.c_str()); 4056 LogPrint("worker", "[worker] pool resolved to meeting node %s\n", meetingHostPort.c_str());
4049 4057
4058 // The descriptor names ONE meeting node, but the pool parks a
4059 // registration at every relay it knows (one accept thread per relay,
4060 // rpc.cpp:AcceptOneFn), so any seed relay can pair us. Insisting on the
4061 // advertised entry forever turns one bad path into a miner that idles
4062 // indefinitely while a working path exists (issue #17); after a few
4063 // consecutive tunnel failures, rotate the seed relays into the attempts
4064 // too. Only ONE participant session runs (StartMinerThreads), so these
4065 // statics are single-threaded.
4066 static const int TUNNEL_FAILOVER_AFTER = 3;
4067 static int nTunnelFails = 0; // consecutive tunnel failures, this pool
4068 static size_t iFailoverRelay = 0; // rotation cursor over vBtfMeetingRelays
4069 static std::string strFailPool; // pool the counters belong to
4070 if (strFailPool != strParticipantPool) {
4071 strFailPool = strParticipantPool;
4072 nTunnelFails = 0;
4073 iFailoverRelay = 0;
4074 }
4075 if (nTunnelFails >= TUNNEL_FAILOVER_AFTER && !vBtfMeetingRelays.empty()) {
4076 std::string alt = vBtfMeetingRelays[iFailoverRelay++ % vBtfMeetingRelays.size()];
4077 if (alt != meetingHostPort) {
4078 LogPrint("worker", "[worker] advertised meeting node %s failed %d"
4079 " times in a row -- trying seed relay %s instead\n",
4080 meetingHostPort.c_str(), nTunnelFails, alt.c_str());
4081 meetingHostPort = alt;
4082 }
4083 }
4084
4050 size_t colon = meetingHostPort.rfind(':'); 4085 size_t colon = meetingHostPort.rfind(':');
4051 if (colon == std::string::npos) { 4086 if (colon == std::string::npos) {
4052 LogPrint("worker", "[worker] bad meeting node format '%s'\n", meetingHostPort.c_str()); 4087 LogPrint("worker", "[worker] bad meeting node format '%s'\n", meetingHostPort.c_str());
@@ -4066,15 +4101,18 @@ static bool PoolParticipantMiner()
4066 // Step 2b: open encrypted tunnel to pool 4101 // Step 2b: open encrypted tunnel to pool
4067 // ------------------------------------------------------------------ 4102 // ------------------------------------------------------------------
4068 SetParticipantMiningStatus("opening tunnel"); 4103 SetParticipantMiningStatus("opening tunnel");
4104 btf::TunnelFail tunnelFail = btf::TUNNEL_FAIL_NONE;
4069 s = btf::BtfClientTunnel(host.c_str(), (unsigned short)port, 4105 s = btf::BtfClientTunnel(host.c_str(), (unsigned short)port,
4070 target_pubkey, service_enc_pub); 4106 target_pubkey, service_enc_pub, &tunnelFail);
4071 if (s == INVALID_SOCKET) { 4107 if (s == INVALID_SOCKET) {
4072 LogPrint("worker", "[worker] tunnel connect FAILED via %s" 4108 nTunnelFails++;
4073 " (relay unreachable or pool not listening)\n", 4109 LogPrint("worker", "[worker] tunnel connect FAILED via %s: %s\n",
4074 meetingHostPort.c_str()); 4110 meetingHostPort.c_str(), btf::TunnelFailText(tunnelFail));
4075 SetParticipantMiningStatus("tunnel connect failed"); 4111 SetParticipantMiningStatus(std::string("tunnel connect failed: ")
4112 + btf::TunnelFailText(tunnelFail));
4076 return false; 4113 return false;
4077 } 4114 }
4115 nTunnelFails = 0;
4078 LogPrint("worker", "[worker] tunnel open via %s\n", meetingHostPort.c_str()); 4116 LogPrint("worker", "[worker] tunnel open via %s\n", meetingHostPort.c_str());
4079 SetParticipantMiningStatus("tunnel open"); 4117 SetParticipantMiningStatus("tunnel open");
4080 } 4118 }
@@ -4253,6 +4291,7 @@ connected:
4253 int nBestHeightResolved = -1; // nBestHeight when we last resolved 4291 int nBestHeightResolved = -1; // nBestHeight when we last resolved
4254 double currentDifficulty = 1.0; // updated by mining.set_difficulty from pool 4292 double currentDifficulty = 1.0; // updated by mining.set_difficulty from pool
4255 bool haveJob = false; 4293 bool haveJob = false;
4294 bool gotAnyJob = false; // any job at all this session -- the return value
4256 bool gotSubscribeAck = false; 4295 bool gotSubscribeAck = false;
4257 bool gotAuthorizeAck = false; 4296 bool gotAuthorizeAck = false;
4258 int64 authWaitStart = 0; 4297 int64 authWaitStart = 0;
@@ -4369,8 +4408,9 @@ connected:
4369 jobId = newJobId; 4408 jobId = newJobId;
4370 memcpy(header, hdrBytes.data(), 80); 4409 memcpy(header, hdrBytes.data(), 80);
4371 memcpy(&target, tgtBytes.data(), 32); 4410 memcpy(&target, tgtBytes.data(), 32);
4372 haveJob = true; 4411 haveJob = true;
4373 nNonce = 1; 4412 gotAnyJob = true;
4413 nNonce = 1;
4374 { 4414 {
4375 std::lock_guard<std::mutex> lk(jobShared.mtx); 4415 std::lock_guard<std::mutex> lk(jobShared.mtx);
4376 memcpy(jobShared.header, header, 80); 4416 memcpy(jobShared.header, header, 80);
@@ -4562,7 +4602,11 @@ connected:
4562 (unsigned long long)hashesSinceSample, 4602 (unsigned long long)hashesSinceSample,
4563 (unsigned long long)sessionSharesSent, 4603 (unsigned long long)sessionSharesSent,
4564 (unsigned long long)sessionSharesAccepted); 4604 (unsigned long long)sessionSharesAccepted);
4565 return true; 4605 // Not "we got here", which is true of a tunnel the pool accepted and then
4606 // dropped without ever sending a job -- exactly the case the caller's
4607 // backoff exists for. Reaching the end of the session is not evidence the
4608 // session was worth anything; a job is.
4609 return gotAnyJob;
4566} 4610}
4567 4611
4568// --------------------------------------------------------------------------- 4612// ---------------------------------------------------------------------------
@@ -4759,11 +4803,51 @@ bool BitcoinMiner(int nThreadId)
4759 // Participant mode: connect to external pool instead of mining solo/operator 4803 // Participant mode: connect to external pool instead of mining solo/operator
4760 if (nMineMode == MINE_PARTICIPANT) 4804 if (nMineMode == MINE_PARTICIPANT)
4761 { 4805 {
4806 // An attempt that never got work backs off (10s doubling to 120s)
4807 // instead of hammering the same path every flat 10s, and a streak
4808 // lasting minutes is said at printf level: the identical once-per-10s
4809 // LogPrint line is how a worker idled all night unnoticed (issue #17).
4810 //
4811 // Only a session the pool actually sent a job to resets either. The
4812 // weaker test -- "the session returned" -- was gameable: a pool that
4813 // completes the tunnel and drops without ever sending work leaves by
4814 // the same tidy cleanup path a healthy one does, which would pin the
4815 // retry at 10s forever and defeat the backoff in exactly the scenario
4816 // it exists for.
4817 int nRetrySecs = 10;
4818 int64 nFailingSince = 0;
4762 while (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { 4819 while (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) {
4763 PoolParticipantMiner(); 4820 bool fGotWork = PoolParticipantMiner();
4764 if (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { 4821 if (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) {
4765 LogPrint("worker", "PoolParticipantMiner: reconnecting in 10s\n"); 4822 if (fGotWork) {
4766 for (int i = 0; i < 10 && !fShutdown; i++) Sleep(1000); 4823 nRetrySecs = 10;
4824 nFailingSince = 0;
4825 } else {
4826 if (nFailingSince == 0)
4827 nFailingSince = GetTime();
4828 int64 nMins = (GetTime() - nFailingSince) / 60;
4829 if (nMins >= 1)
4830 printf("[worker] connection to pool %s has been failing"
4831 " for %lld minutes\n",
4832 strParticipantPool.c_str(), (long long)nMins);
4833 }
4834 LogPrint("worker", "PoolParticipantMiner: reconnecting in %ds\n",
4835 nRetrySecs);
4836 // Poll everything the enclosing while polls, not just
4837 // fShutdown. Raising the ceiling to 120s made a
4838 // shutdown-only predicate expensive: after the operator
4839 // clears Generate or switches to Relay, the session -- and
4840 // the vfThreadRunning[3] slot that gates restarting it --
4841 // would stay held for up to two minutes. Measured 119s
4842 // before, 1s after.
4843 for (int i = 0; i < nRetrySecs && !fShutdown && fGenerateBitcoins
4844 && nMineMode == MINE_PARTICIPANT; i++)
4845 Sleep(1000);
4846 if (!fGotWork && nRetrySecs < 120) {
4847 nRetrySecs *= 2;
4848 if (nRetrySecs > 120)
4849 nRetrySecs = 120;
4850 }
4767 } 4851 }
4768 } 4852 }
4769 return true; 4853 return true;