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:
Diffstat (limited to 'src')
-rw-r--r--src/Makefile4
-rw-r--r--src/btfrv.cpp41
-rw-r--r--src/btftunnel.h13
-rw-r--r--src/db.cpp16
-rw-r--r--src/db.h23
-rw-r--r--src/headers_core.h1
-rw-r--r--src/main.cpp561
-rw-r--r--src/main.h14
-rw-r--r--src/main_gui.cpp64
-rw-r--r--src/makefile.mingw4
-rw-r--r--src/market.cpp264
-rw-r--r--src/market.h182
-rw-r--r--src/net.cpp226
-rw-r--r--src/net.h204
-rw-r--r--src/nostr.cpp122
-rw-r--r--src/nostr.h8
-rw-r--r--src/rpc.cpp963
-rw-r--r--src/selftest.cpp37
-rw-r--r--src/test_rvaccept.cpp180
-rw-r--r--src/util.h35
20 files changed, 1922 insertions, 1040 deletions
diff --git a/src/Makefile b/src/Makefile
index 511fe79..1166daf 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -52,14 +52,14 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \
52# binary. Nothing crashed here, and nothing would say so if it had. 52# binary. Nothing crashed here, and nothing would say so if it had.
53HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 53HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \
54 key.h bignum.h script.h db.h base58.h net.h nostr.h \ 54 key.h bignum.h script.h db.h base58.h net.h nostr.h \
55 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ 55 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \
56 selftest.h walletcmd.h compat.h sockcount.h proxy.h font_roboto.h 56 selftest.h walletcmd.h compat.h sockcount.h proxy.h font_roboto.h
57 57
58IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ 58IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
59 obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o 59 obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o
60 60
61NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ 61NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \
62 obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ 62 obj/sha.o obj/nostr.o obj/randomx_pow.o \
63 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ 63 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \
64 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o 64 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o
65 65
diff --git a/src/btfrv.cpp b/src/btfrv.cpp
index d4f079f..133eb89 100644
--- a/src/btfrv.cpp
+++ b/src/btfrv.cpp
@@ -102,31 +102,42 @@ static SOCKET ConnectTo(const char* host, unsigned short port)
102#else 102#else
103 struct addrinfo hints, *res = NULL; 103 struct addrinfo hints, *res = NULL;
104 memset(&hints, 0, sizeof(hints)); 104 memset(&hints, 0, sizeof(hints));
105 hints.ai_family = AF_INET; 105 hints.ai_family = AF_UNSPEC; // IPv4 or IPv6 (some relays are v6-only)
106 hints.ai_socktype = SOCK_STREAM; 106 hints.ai_socktype = SOCK_STREAM;
107 char portstr[16]; sprintf(portstr, "%u", (unsigned)port); 107 char portstr[16]; sprintf(portstr, "%u", (unsigned)port);
108 if (getaddrinfo(host, portstr, &hints, &res) != 0 || !res) return INVALID_SOCKET; 108 if (getaddrinfo(host, portstr, &hints, &res) != 0 || !res) return INVALID_SOCKET;
109 SOCKET s = BtfSocketTag(socket(res->ai_family, res->ai_socktype, res->ai_protocol), SOCK_RV_DIAL);
110 if (s == INVALID_SOCKET) { freeaddrinfo(res); return INVALID_SOCKET; }
111 109
112 // Apply a short timeout only for the connect() and initial handshake so an 110 // Try each resolved address until one connects.
113 // unresponsive relay doesn't block the thread. Once the tunnel is live the 111 SOCKET s = INVALID_SOCKET;
114 // timeout is cleared -- a forwarding pipe must block indefinitely on recv(). 112 for (struct addrinfo* rp = res; rp != NULL; rp = rp->ai_next)
113 {
114 s = BtfSocketTag(socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol), SOCK_RV_DIAL);
115 if (s == INVALID_SOCKET)
116 continue;
117
118 // Apply a short timeout only for the connect() and initial handshake so an
119 // unresponsive relay doesn't block the thread. Once the tunnel is live the
120 // timeout is cleared -- a forwarding pipe must block indefinitely on recv().
115#ifdef _WIN32 121#ifdef _WIN32
116 DWORD tvShort = 10000; 122 DWORD tvShort = 10000;
117 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tvShort, sizeof(tvShort)); 123 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tvShort, sizeof(tvShort));
118 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tvShort, sizeof(tvShort)); 124 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tvShort, sizeof(tvShort));
119#else 125#else
120 struct timeval tvShort; tvShort.tv_sec = 10; tvShort.tv_usec = 0; 126 struct timeval tvShort; tvShort.tv_sec = 10; tvShort.tv_usec = 0;
121 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tvShort, sizeof(tvShort)); 127 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char*)&tvShort, sizeof(tvShort));
122 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tvShort, sizeof(tvShort)); 128 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (char*)&tvShort, sizeof(tvShort));
123#endif 129#endif
124 130
125 if (connect(s, res->ai_addr, (int)res->ai_addrlen) != 0) 131 if (connect(s, rp->ai_addr, (int)rp->ai_addrlen) != 0)
126 { 132 {
127 CLOSESOCK(s); freeaddrinfo(res); return INVALID_SOCKET; 133 CLOSESOCK(s);
134 s = INVALID_SOCKET;
135 continue;
136 }
137 break;
128 } 138 }
129 freeaddrinfo(res); 139 freeaddrinfo(res);
140 if (s == INVALID_SOCKET) return INVALID_SOCKET;
130#endif 141#endif
131 142
132 // Clear the timeout -- the socket is now a live tunnel and must not 143 // Clear the timeout -- the socket is now a live tunnel and must not
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/db.cpp b/src/db.cpp
index 9f62964..405a3db 100644
--- a/src/db.cpp
+++ b/src/db.cpp
@@ -598,20 +598,12 @@ bool CTxDB::LoadBlockIndex()
598 598
599 599
600// 600//
601// CReviewDB 601// CReviewDB is gone with the "review" handler. Its write path was
602// read-whole-vector / append-one / write-whole-vector, driven straight from an
603// unauthenticated peer, which is quadratic disk on an unbounded record.
604// Existing reviews.dat files are simply left alone and never opened.
602// 605//
603 606
604bool CReviewDB::ReadReviews(uint256 hash, vector<CReview>& vReviews)
605{
606 vReviews.size(); // msvc workaround, just need to do anything with vReviews
607 return Read(make_pair(string("reviews"), hash), vReviews);
608}
609
610bool CReviewDB::WriteReviews(uint256 hash, const vector<CReview>& vReviews)
611{
612 return Write(make_pair(string("reviews"), hash), vReviews);
613}
614
615 607
616 608
617 609
diff --git a/src/db.h b/src/db.h
index 4fb5cfd..37a252f 100644
--- a/src/db.h
+++ b/src/db.h
@@ -8,8 +8,6 @@ class CTxIndex;
8class CDiskBlockIndex; 8class CDiskBlockIndex;
9class CDiskTxPos; 9class CDiskTxPos;
10class COutPoint; 10class COutPoint;
11class CUser;
12class CReview;
13class CAddress; 11class CAddress;
14class CWalletTx; 12class CWalletTx;
15 13
@@ -314,27 +312,6 @@ public:
314 312
315 313
316 314
317class CReviewDB : public CDB
318{
319public:
320 CReviewDB(const char* pszMode="r+", bool fTxn=false) : CDB("reviews.dat", pszMode, fTxn) { }
321private:
322 CReviewDB(const CReviewDB&);
323 void operator=(const CReviewDB&);
324public:
325 bool ReadUser(uint256 hash, CUser& user)
326 {
327 return Read(make_pair(string("user"), hash), user);
328 }
329
330 bool WriteUser(uint256 hash, const CUser& user)
331 {
332 return Write(make_pair(string("user"), hash), user);
333 }
334
335 bool ReadReviews(uint256 hash, vector<CReview>& vReviews);
336 bool WriteReviews(uint256 hash, const vector<CReview>& vReviews);
337};
338 315
339 316
340 317
diff --git a/src/headers_core.h b/src/headers_core.h
index 6c0c602..6a60b52 100644
--- a/src/headers_core.h
+++ b/src/headers_core.h
@@ -66,7 +66,6 @@ inline typename std::common_type<T1,T2>::type max(const T1& a, const T2& b)
66#include "nostr.h" 66#include "nostr.h"
67#include "randomx_pow.h" 67#include "randomx_pow.h"
68#include "main.h" 68#include "main.h"
69#include "market.h"
70 69
71// GUI callback -- implemented in gui.cpp (or as no-op in headless builds) 70// GUI callback -- implemented in gui.cpp (or as no-op in headless builds)
72void MainFrameRepaint(); 71void MainFrameRepaint();
diff --git a/src/main.cpp b/src/main.cpp
index f83164f..252911b 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -87,6 +87,8 @@ CKey keyUser;
87string strSetDataDir; 87string strSetDataDir;
88int nDropMessagesTest = 0; 88int nDropMessagesTest = 0;
89bool fSoloMineTest = false; // /solomine: mine without requiring a peer (local test) 89bool fSoloMineTest = false; // /solomine: mine without requiring a peer (local test)
90bool fPoolNoAnnounce = false; // /noannounce: operator serves miners but never publishes pool announcements
91bool fPoolMineBlind = false; // /poolmineblind: issue jobs even when no peer has reported a height
90 92
91// Settings 93// Settings
92int fGenerateBitcoins; 94int fGenerateBitcoins;
@@ -102,6 +104,9 @@ CAddress addrIncoming;
102int nMineMode = MINE_RELAY; 104int nMineMode = MINE_RELAY;
103bool fMineModeFromCommandLine = false; 105bool fMineModeFromCommandLine = false;
104string strParticipantPool; // participant mode: pool .btf address 106string strParticipantPool; // participant mode: pool .btf address
107string strPayoutAddress; // participant: /payoutaddr override (mine without a wallet)
108string strPayoutFile; // participant: /payoutfile with one address per line, rotated per reconnect
109int nHashThreads = 0; // participant: /hashthreads override (0 = cores-1)
105string strPoolName = "Bitflash Pool"; 110string strPoolName = "Bitflash Pool";
106string strPoolDashboardUrl; 111string strPoolDashboardUrl;
107string strPoolStatusFile; 112string strPoolStatusFile;
@@ -163,12 +168,32 @@ int MinerThreadCount()
163{ 168{
164 if (nMinerThreads > 0) 169 if (nMinerThreads > 0)
165 return nMinerThreads; 170 return nMinerThreads;
166 unsigned int nCores = std::thread::hardware_concurrency(); 171 unsigned int nCores = AvailableCoreCount();
167 if (nCores <= 1) 172 if (nCores <= 1)
168 return 1; 173 return 1;
169 return (int)(nCores - 1); 174 return (int)(nCores - 1);
170} 175}
171 176
177// Hash threads one participant session runs.
178//
179// Explicit /hashthreads=N is honored exactly, including 1 -- a miner who says
180// one means one. Automatic leaves a core to the node, the Stratum I/O loop and
181// everything else the process still has to do, for the same reason
182// MinerThreadCount() does.
183//
184// Lives here, next to that function, because the two are easy to confuse and
185// StartMinerThreads() needs to be able to tell them apart: only ONE session
186// runs in participant mode, and this is what sizes it.
187int ParticipantHashThreadCount()
188{
189 if (nHashThreads > 0)
190 return nHashThreads;
191 int nWorkers = (int)AvailableCoreCount();
192 if (nWorkers < 2)
193 nWorkers = 2;
194 return nWorkers - 1;
195}
196
172static void SecureClearBytes(vector<unsigned char>& vch); 197static void SecureClearBytes(vector<unsigned char>& vch);
173static void SecureClearKeyingMaterial(CKeyingMaterial& vch); 198static void SecureClearKeyingMaterial(CKeyingMaterial& vch);
174 199
@@ -1551,7 +1576,7 @@ int CMerkleTx::GetBlocksToMaturity() const
1551{ 1576{
1552 if (!IsCoinBase()) 1577 if (!IsCoinBase())
1553 return 0; 1578 return 0;
1554 return max(0, (COINBASE_MATURITY+20) - GetDepthInMainChain()); 1579 return max(0, COINBASE_SPENDABLE_DEPTH - GetDepthInMainChain());
1555} 1580}
1556 1581
1557 1582
@@ -2819,9 +2844,10 @@ bool AlreadyHave(CTxDB& txdb, const CInv& inv)
2819 { 2844 {
2820 case MSG_TX: return mapTransactions.count(inv.hash) || txdb.ContainsTx(inv.hash); 2845 case MSG_TX: return mapTransactions.count(inv.hash) || txdb.ContainsTx(inv.hash);
2821 case MSG_BLOCK: return mapBlockIndex.count(inv.hash) || mapOrphanBlocks.count(inv.hash); 2846 case MSG_BLOCK: return mapBlockIndex.count(inv.hash) || mapOrphanBlocks.count(inv.hash);
2822 case MSG_REVIEW: return true;
2823 case MSG_PRODUCT: return mapProducts.count(inv.hash);
2824 } 2847 }
2848 // MSG_REVIEW / MSG_PRODUCT / MSG_TABLE are the 2009 marketplace inventory
2849 // types. Nothing announces or serves them any more (see ProcessMessage),
2850 // so they fall through to the default and are never requested.
2825 // Don't know what it is, just say we already got one 2851 // Don't know what it is, just say we already got one
2826 return true; 2852 return true;
2827} 2853}
@@ -2864,11 +2890,13 @@ bool ProcessMessages(CNode* pfrom)
2864 if (LogAcceptsCategory("net")) printf("\n\nPROCESSMESSAGE SKIPPED %d BYTES\n\n", (int)(pstart - vRecv.begin())); 2890 if (LogAcceptsCategory("net")) printf("\n\nPROCESSMESSAGE SKIPPED %d BYTES\n\n", (int)(pstart - vRecv.begin()));
2865 vRecv.erase(vRecv.begin(), pstart); 2891 vRecv.erase(vRecv.begin(), pstart);
2866 2892
2867 // Read the header from a copy first. The old path consumed the header, 2893 // Read the header from a copy first, and do not consume it until the
2868 // noticed the payload was incomplete, inserted the header back at the 2894 // whole body is here. The old path consumed the header, noticed the
2869 // front of the receive buffer, then slept inside the shared message 2895 // payload was incomplete, inserted the header back at the front of the
2870 // thread. A peer could announce a large payload and drip bytes forever, 2896 // receive buffer, then slept inside the shared message thread. A peer
2871 // making every pass pay for an O(n) insert and a 100 ms stall. 2897 // could announce a large payload and drip bytes forever, making every
2898 // pass pay for an O(n) insert and a 100 ms stall -- a cheap way for one
2899 // peer to slow the thread that serves all of them. (upstream #109)
2872 CMessageHeader hdr; 2900 CMessageHeader hdr;
2873 CDataStream vHeader(vRecv.begin(), vRecv.begin() + sizeof(CMessageHeader), 2901 CDataStream vHeader(vRecv.begin(), vRecv.begin() + sizeof(CMessageHeader),
2874 vRecv.nType, vRecv.nVersion); 2902 vRecv.nType, vRecv.nVersion);
@@ -2878,7 +2906,10 @@ bool ProcessMessages(CNode* pfrom)
2878 if (LogAcceptsCategory("net")) printf("\n\nPROCESSMESSAGE: ERRORS IN HEADER %s\n\n\n", hdr.GetCommand().c_str()); 2906 if (LogAcceptsCategory("net")) printf("\n\nPROCESSMESSAGE: ERRORS IN HEADER %s\n\n\n", hdr.GetCommand().c_str());
2879 if (hdr.nMessageSize > MAX_PROTOCOL_MESSAGE_SIZE) 2907 if (hdr.nMessageSize > MAX_PROTOCOL_MESSAGE_SIZE)
2880 pfrom->fDisconnect = true; 2908 pfrom->fDisconnect = true;
2881 pfrom->nIncompleteMessageStart = 0; 2909 pfrom->nMsgWaitSince = 0;
2910 pfrom->nMsgWaitSize = 0;
2911 // The header was read from a copy, so it is still in vRecv. Skip it
2912 // explicitly or this loop never advances.
2882 vRecv.ignore(sizeof(CMessageHeader)); 2913 vRecv.ignore(sizeof(CMessageHeader));
2883 continue; 2914 continue;
2884 } 2915 }
@@ -2889,30 +2920,63 @@ bool ProcessMessages(CNode* pfrom)
2889 unsigned int nPayloadAvailable = vRecv.size() - sizeof(CMessageHeader); 2920 unsigned int nPayloadAvailable = vRecv.size() - sizeof(CMessageHeader);
2890 if (nMessageSize > nPayloadAvailable) 2921 if (nMessageSize > nPayloadAvailable)
2891 { 2922 {
2892 if (pfrom->nIncompleteMessageStart == 0 || 2923 // Wait for the rest of the message. The header stays in vRecv.
2893 pfrom->nIncompleteMessageSize != nMessageSize || 2924 //
2894 pfrom->strIncompleteMessageCommand != strCommand) 2925 // This is the mechanism the original note asked for: "need a
2926 // mechanism to give up waiting for overlong message size error".
2927 // Without one the wait is unbounded, and unbounded here means
2928 // permanent. The header stays at the front of vRecv and the loop
2929 // breaks; if the body never completes, every later pass re-reads
2930 // that same header and breaks in the same place. The peer is never
2931 // parsed again -- no blocks, no inv, not even its version message,
2932 // so it never reports a height either, which is why a wedged node
2933 // has peers and GetPeerMedianHeight() still returns -1.
2934 //
2935 // Nothing rescues it. A header could claim up to 256 MB and still
2936 // pass CMessageHeader::IsValid() -- now capped at
2937 // MAX_PROTOCOL_MESSAGE_SIZE -- and any byte that arrives
2938 // refreshes nLastRecv, so the inactivity timeout that drops a
2939 // silent peer never fires on one that is merely unreadable. The
2940 // socket stays ESTABLISHED and select() keeps offering it.
2941 // Measured on a wedged pool: this branch ran 2014 times in the
2942 // deaf window, against 607 in the healthy window before it.
2943 if (LogAcceptsCategory("net")) printf("MESSAGE-BREAK 2\n");
2944 //
2945 // Time the STALL, not the wait. A big message on a slow link is
2946 // still arriving, and dropping that peer would be a bug of our own
2947 // making; what must be caught is a buffer that has stopped growing
2948 // while we go on expecting more. So the clock restarts whenever
2949 // even one more byte has landed since the last look.
2950 //
2951 // Upstream #109 times this differently: it restarts the clock only
2952 // when the (size, command) pair changes, which drops a peer sending
2953 // one big message slowly but honestly. Progress is the better test,
2954 // so that half is kept as it was.
2955 int64 nNow = GetTime();
2956 if (pfrom->nMsgWaitSince == 0 || nPayloadAvailable > pfrom->nMsgWaitSize)
2895 { 2957 {
2896 pfrom->nIncompleteMessageStart = GetTime(); 2958 pfrom->nMsgWaitSince = nNow;
2897 pfrom->nIncompleteMessageSize = nMessageSize; 2959 pfrom->nMsgWaitSize = nPayloadAvailable;
2898 pfrom->strIncompleteMessageCommand = strCommand;
2899 } 2960 }
2900 else if (GetTime() - pfrom->nIncompleteMessageStart > 2961 else if (nNow - pfrom->nMsgWaitSince > BTF_MSG_WAIT_MAX_SECS)
2901 BTF_INCOMPLETE_MESSAGE_TIMEOUT_SECS)
2902 { 2962 {
2903 LogPrint("net", 2963 // Deliberately not behind a debug category. This is the node
2904 "disconnecting %s: incomplete %s message, %u of %u bytes after %d seconds\n", 2964 // going deaf to a peer, which is the thing nobody could see.
2905 pfrom->addr.ToString().c_str(), strCommand.c_str(), 2965 printf("peer %s announced a %u byte '%s' and has sent nothing more "
2906 nPayloadAvailable, nMessageSize, 2966 "for %d s (%d of %u payload bytes) -- dropping, this peer is unreadable\n",
2907 BTF_INCOMPLETE_MESSAGE_TIMEOUT_SECS); 2967 pfrom->addr.ToString().c_str(), nMessageSize,
2968 strCommand.c_str(), (int)(nNow - pfrom->nMsgWaitSince),
2969 (int)nPayloadAvailable, nMessageSize);
2908 pfrom->fDisconnect = true; 2970 pfrom->fDisconnect = true;
2909 } 2971 }
2972 // No re-insert, no Sleep(): the header was never consumed, so there
2973 // is nothing to put back, and sleeping here stalls every peer.
2910 break; 2974 break;
2911 } 2975 }
2912 pfrom->nIncompleteMessageStart = 0;
2913 pfrom->nIncompleteMessageSize = 0;
2914 pfrom->strIncompleteMessageCommand.clear();
2915 vRecv.ignore(sizeof(CMessageHeader)); 2976 vRecv.ignore(sizeof(CMessageHeader));
2977 // The body arrived, so this peer is readable again.
2978 pfrom->nMsgWaitSince = 0;
2979 pfrom->nMsgWaitSize = 0;
2916 2980
2917 // Copy message to its own buffer 2981 // Copy message to its own buffer
2918 CDataStream vMsg(vRecv.begin(), vRecv.begin() + nMessageSize, vRecv.nType, vRecv.nVersion); 2982 CDataStream vMsg(vRecv.begin(), vRecv.begin() + nMessageSize, vRecv.nType, vRecv.nVersion);
@@ -2941,7 +3005,6 @@ bool ProcessMessages(CNode* pfrom)
2941 3005
2942bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) 3006bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
2943{ 3007{
2944 static map<unsigned int, vector<unsigned char> > mapReuseKey;
2945 if (LogAcceptsCategory("net")) { printf("received: %-12s (%d bytes) ", strCommand.c_str(), (int)vRecv.size()); for (int i = 0; i < min(vRecv.size(), (unsigned int)25); i++) printf("%02x ", vRecv[i] & 0xff); printf("\n"); } 3008 if (LogAcceptsCategory("net")) { printf("received: %-12s (%d bytes) ", strCommand.c_str(), (int)vRecv.size()); for (int i = 0; i < min(vRecv.size(), (unsigned int)25); i++) printf("%02x ", vRecv[i] & 0xff); printf("\n"); }
2946 if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0) 3009 if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0)
2947 { 3010 {
@@ -3180,22 +3243,18 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
3180 } 3243 }
3181 3244
3182 3245
3183 else if (strCommand == "review") 3246 // "review" used to be handled here. It took a CReview off the wire from an
3184 { 3247 // unauthenticated peer, checked a signature the sender had made with a key
3185 CDataStream vMsg(vRecv); 3248 // of its own choosing -- which proves possession of that key and authorises
3186 CReview review; 3249 // nothing -- and then appended the message to reviews.dat by reading the
3187 vRecv >> review; 3250 // whole vector for the target hash, pushing one entry, and writing all of
3188 3251 // it back. That is quadratic disk for a stream an attacker controls, on a
3189 CInv inv(MSG_REVIEW, review.GetHash()); 3252 // record whose mapValue is an unbounded map<string,string> read straight
3190 pfrom->AddInventoryKnown(inv); 3253 // from the socket, followed by AddAtomsAndPropagate walking a link graph
3191 3254 // the same peer supplied, and finally RelayMessage forwarding it to every
3192 if (review.AcceptReview()) 3255 // other node. Unbounded remote disk growth with network amplification and
3193 { 3256 // no authorisation anywhere in the path. The marketplace it belonged to has
3194 // Relay the original message as-is in case it's a higher version than we know how to parse 3257 // not existed since 2009; the handler is gone rather than bounded.
3195 RelayMessage(inv, vMsg);
3196 mapAlreadyAskedFor.erase(inv);
3197 }
3198 }
3199 3258
3200 3259
3201 else if (strCommand == "block") 3260 else if (strCommand == "block")
@@ -3210,41 +3269,46 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
3210 pfrom->AddInventoryKnown(inv); 3269 pfrom->AddInventoryKnown(inv);
3211 3270
3212 if (ProcessBlock(pfrom, pblock.release())) 3271 if (ProcessBlock(pfrom, pblock.release()))
3272 {
3213 mapAlreadyAskedFor.erase(inv); 3273 mapAlreadyAskedFor.erase(inv);
3214 }
3215
3216 3274
3217 else if (strCommand == "checkorder" || strCommand == "submitorder") 3275 // A peer that just handed us a block is demonstrably at least at
3218 { 3276 // that block's height. Without this, nStartingHeight is a snapshot
3219 // Legacy Bitcoin 0.1 marketplace messages are not part of Bitflash's 3277 // taken once during the version handshake and never touched again,
3220 // supported protocol. They used to let a remote peer make this wallet 3278 // so on a long-lived connection it is the peer's height from
3221 // reserve a key or import/broadcast a wallet transaction. Keep the 3279 // however many hours ago it happened to connect. The pool's
3222 // command names explicit so old peers get a quiet no-op instead of a 3280 // behind-the-network check compares against the median of those
3223 // wallet side effect. 3281 // numbers, so as connections age it drifts toward "everyone is
3224 if (LogAcceptsCategory("net")) 3282 // behind us" and quietly stops firing -- the opposite of its job.
3225 printf("ProcessMessage(%s) : disabled legacy order message\n", strCommand.c_str()); 3283 map<uint256, CBlockIndex*>::iterator mi = mapBlockIndex.find(inv.hash);
3226 vRecv.clear(); 3284 if (mi != mapBlockIndex.end() && mi->second)
3285 pfrom->nStartingHeight = max(pfrom->nStartingHeight, mi->second->nHeight);
3286 }
3227 } 3287 }
3228 3288
3229 3289
3230 else if (strCommand == "reply") 3290 // "checkorder", "submitorder" and "reply" -- the 2009 buy-from-a-merchant
3231 { 3291 // exchange -- used to be handled here. All three are gone.
3232 uint256 hashReply; 3292 //
3233 vRecv >> hashReply; 3293 // "checkorder" called GenerateNewKey() for any peer that asked, which is
3234 3294 // AddKey() and a wallet.dat write, and handed the pubkey back. The comment
3235 CRequestTracker tracker; 3295 // said "keep giving the same key to the same ip until they use it", and on
3236 CRITICAL_BLOCK(pfrom->cs_mapRequests) 3296 // a 2009 network of routable IPs that held. It does not hold here: every
3237 { 3297 // inbound peer arrives through a rendezvous tunnel and is tagged with
3238 map<uint256, CRequestTracker>::iterator mi = pfrom->mapRequests.find(hashReply); 3298 // BtfMarkerAddr() over three random bytes (net.cpp), so addr.ip is freshly
3239 if (mi != pfrom->mapRequests.end()) 3299 // random per connection and the cache never hits. Reconnect in a loop and
3240 { 3300 // the node mints a key per connection -- unbounded wallet growth, unbounded
3241 tracker = (*mi).second; 3301 // growth of the static map that held them, and every one of those keys
3242 pfrom->mapRequests.erase(mi); 3302 // missing from every backup taken beforehand, which for this pre-BIP32
3243 } 3303 // wallet is how coins are lost.
3244 } 3304 //
3245 if (!tracker.IsNull()) 3305 // "submitorder" took a CWalletTx from an unauthenticated peer and, if it
3246 tracker.fn(tracker.param1, vRecv); 3306 // was merely a valid transaction, AddToWallet()'d it. Valid is not the same
3247 } 3307 // as ours: any peer could write chosen transactions into wallet.dat.
3308 //
3309 // "reply" dispatched to a CRequestTracker callback. Nothing in this tree
3310 // has ever called PushRequest, so mapRequests was permanently empty and the
3311 // handler could only ever be a parser for hostile input.
3248 3312
3249 3313
3250 else if (strCommand == "ping") 3314 else if (strCommand == "ping")
@@ -3502,6 +3566,102 @@ static bool StratumRecvLine(SOCKET s, std::string& buf,
3502// How long to wait for a mining.notify after authorize before giving up 3566// How long to wait for a mining.notify after authorize before giving up
3503static const int64 STUCK_NO_JOB_TIMEOUT_SECS = 120; 3567static const int64 STUCK_NO_JOB_TIMEOUT_SECS = 120;
3504 3568
3569// ------------------------------------------------------------------
3570// Multi-threaded pool worker. The original loop hashed a single nonce
3571// per blocking 50ms socket poll, capping one instance at ~20 H/s no
3572// matter how many cores the machine has. The socket and job bookkeeping
3573// stay on the calling thread; N hash threads grind nonce ranges on their
3574// own RandomX VMs (all sharing the one ~2 GB dataset) and drop found
3575// shares on a queue the I/O thread drains and submits.
3576// ------------------------------------------------------------------
3577struct ParticipantJobShared
3578{
3579 std::mutex mtx;
3580 unsigned char header[80];
3581 uint256 target;
3582 std::string jobId;
3583 uint64 jobSeq;
3584 bool haveJob;
3585 bool stop;
3586 ParticipantJobShared() : jobSeq(0), haveJob(false), stop(false)
3587 {
3588 memset(header, 0, sizeof(header));
3589 }
3590};
3591
3592struct ParticipantShareHit
3593{
3594 uint64 jobSeq;
3595 std::string jobId;
3596 unsigned int nonce;
3597};
3598
3599static void ParticipantHashWorker(ParticipantJobShared* sh, int idx, int nWorkers,
3600 std::vector<ParticipantShareHit>* hits,
3601 std::mutex* hitsMtx)
3602{
3603 void* vm = RandomXCreateMinerVM();
3604 if (!vm)
3605 {
3606 LogPrint("worker", "[worker] hash thread %d: RandomX VM creation failed\n", idx);
3607 return;
3608 }
3609
3610 unsigned char header[80];
3611 uint256 target;
3612 std::string jobId;
3613 uint64 mySeq = 0;
3614 bool active = false;
3615 unsigned int nNonce = 0;
3616 uint64 localHashes = 0;
3617
3618 while (!fShutdown)
3619 {
3620 {
3621 std::lock_guard<std::mutex> lk(sh->mtx);
3622 if (sh->stop)
3623 break;
3624 if (sh->haveJob && sh->jobSeq != mySeq)
3625 {
3626 memcpy(header, sh->header, 80);
3627 target = sh->target;
3628 jobId = sh->jobId;
3629 mySeq = sh->jobSeq;
3630 nNonce = (unsigned int)(idx + 1);
3631 active = true;
3632 }
3633 }
3634 if (!active)
3635 {
3636 Sleep(50);
3637 continue;
3638 }
3639
3640 memcpy(header + 76, &nNonce, 4);
3641 uint256 hash = RandomXHashWithVM(vm, header, 80);
3642 localHashes++;
3643 if ((localHashes & 0x3ff) == 0)
3644 gParticipantHashes.fetch_add(0x400);
3645
3646 if (hash <= target)
3647 {
3648 LogPrint("worker", "[worker] share found! nonce=%u job=%s (thread %d)\n",
3649 nNonce, jobId.c_str(), idx);
3650 std::lock_guard<std::mutex> lk(*hitsMtx);
3651 hits->push_back(ParticipantShareHit{mySeq, jobId, nNonce});
3652 }
3653
3654 unsigned int next = nNonce + (unsigned int)nWorkers;
3655 if (next < nNonce) // wrapped past 2^32: range exhausted, wait for next job
3656 active = false;
3657 else
3658 nNonce = next;
3659 }
3660
3661 gParticipantHashes.fetch_add(localHashes & 0x3ff);
3662 RandomXDestroyMinerVM(vm);
3663}
3664
3505static bool PoolParticipantMiner() 3665static bool PoolParticipantMiner()
3506{ 3666{
3507 // ------------------------------------------------------------------ 3667 // ------------------------------------------------------------------
@@ -3516,6 +3676,51 @@ static bool PoolParticipantMiner()
3516 LogPrint("worker", "[worker] connecting to pool %s\n", strParticipantPool.c_str()); 3676 LogPrint("worker", "[worker] connecting to pool %s\n", strParticipantPool.c_str());
3517 SetParticipantMiningStatus("resolving pool address"); 3677 SetParticipantMiningStatus("resolving pool address");
3518 3678
3679 // ------------------------------------------------------------------
3680 // Steps 1-2: connect to the pool. Two address forms:
3681 // tcp:HOST:PORT -- plain TCP straight to the pool's stratum port
3682 // (e.g. our own operator node on this machine),
3683 // no .btf resolution or rendezvous tunnel involved
3684 // <name>.btf -- resolve via Nostr, dial through a rendezvous relay
3685 // ------------------------------------------------------------------
3686 btf_socket_t s = INVALID_SOCKET;
3687 if (strParticipantPool.compare(0, 4, "tcp:") == 0)
3688 {
3689 std::string hp = strParticipantPool.substr(4);
3690 size_t colon = hp.rfind(':');
3691 if (colon == std::string::npos) {
3692 LogPrint("worker", "[worker] bad tcp pool address '%s' (want tcp:host:port)\n",
3693 strParticipantPool.c_str());
3694 SetParticipantMiningStatus("bad pool address");
3695 return false;
3696 }
3697 std::string host = hp.substr(0, colon);
3698 int port = atoi(hp.substr(colon + 1).c_str());
3699 unsigned long ip = inet_addr(host.c_str());
3700 if (port <= 0 || port > 65535 || ip == INADDR_NONE) {
3701 LogPrint("worker", "[worker] bad tcp pool endpoint '%s'\n", hp.c_str());
3702 SetParticipantMiningStatus("bad pool endpoint");
3703 return false;
3704 }
3705 SetParticipantMiningStatus("connecting to pool (direct tcp)");
3706 struct sockaddr_in sa;
3707 memset(&sa, 0, sizeof(sa));
3708 sa.sin_family = AF_INET;
3709 sa.sin_port = htons((unsigned short)port);
3710 sa.sin_addr.s_addr = ip;
3711 SOCKET cs = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
3712 if (cs == INVALID_SOCKET || connect(cs, (struct sockaddr*)&sa, sizeof(sa)) == SOCKET_ERROR) {
3713 LogPrint("worker", "[worker] direct connect to %s FAILED\n", hp.c_str());
3714 SetParticipantMiningStatus("pool connect failed");
3715 if (cs != INVALID_SOCKET) closesocket(cs);
3716 return false;
3717 }
3718 LogPrint("worker", "[worker] connected directly to pool %s\n", hp.c_str());
3719 s = (btf_socket_t)cs;
3720 goto connected;
3721 }
3722
3723 {
3519 unsigned char target_pubkey[32]; 3724 unsigned char target_pubkey[32];
3520 if (!btf::ParseAddress(strParticipantPool, target_pubkey)) { 3725 if (!btf::ParseAddress(strParticipantPool, target_pubkey)) {
3521 LogPrint("worker", "[worker] invalid .btf address: %s\n", strParticipantPool.c_str()); 3726 LogPrint("worker", "[worker] invalid .btf address: %s\n", strParticipantPool.c_str());
@@ -3550,11 +3755,11 @@ static bool PoolParticipantMiner()
3550 } 3755 }
3551 3756
3552 // ------------------------------------------------------------------ 3757 // ------------------------------------------------------------------
3553 // Step 2: open encrypted tunnel to pool 3758 // Step 2b: open encrypted tunnel to pool
3554 // ------------------------------------------------------------------ 3759 // ------------------------------------------------------------------
3555 SetParticipantMiningStatus("opening tunnel"); 3760 SetParticipantMiningStatus("opening tunnel");
3556 btf_socket_t s = btf::BtfClientTunnel(host.c_str(), (unsigned short)port, 3761 s = btf::BtfClientTunnel(host.c_str(), (unsigned short)port,
3557 target_pubkey, service_enc_pub); 3762 target_pubkey, service_enc_pub);
3558 if (s == INVALID_SOCKET) { 3763 if (s == INVALID_SOCKET) {
3559 LogPrint("worker", "[worker] tunnel connect FAILED via %s" 3764 LogPrint("worker", "[worker] tunnel connect FAILED via %s"
3560 " (relay unreachable or pool not listening)\n", 3765 " (relay unreachable or pool not listening)\n",
@@ -3564,11 +3769,48 @@ static bool PoolParticipantMiner()
3564 } 3769 }
3565 LogPrint("worker", "[worker] tunnel open via %s\n", meetingHostPort.c_str()); 3770 LogPrint("worker", "[worker] tunnel open via %s\n", meetingHostPort.c_str());
3566 SetParticipantMiningStatus("tunnel open"); 3771 SetParticipantMiningStatus("tunnel open");
3772 }
3773
3774connected:
3567 3775
3568 // ------------------------------------------------------------------ 3776 // ------------------------------------------------------------------
3569 // Step 3: determine our payout address (mining username) 3777 // Step 3: determine our payout address (mining username). /payoutaddr
3778 // wins -- it lets a machine mine to an address whose keys live
3779 // elsewhere, so no wallet file is needed on the mining machine.
3780 // /payoutfile rotates through a list of such addresses: one is picked
3781 // per (re)connection, and the session self-limits (see rotation break
3782 // in the main loop) so consecutive rounds land on fresh addresses.
3570 // ------------------------------------------------------------------ 3783 // ------------------------------------------------------------------
3571 std::string username; 3784 static std::vector<std::string> vPayoutAddrs;
3785 static size_t nPayoutRot = 0;
3786 if (!strPayoutFile.empty() && vPayoutAddrs.empty())
3787 {
3788 FILE* f = fopen(strPayoutFile.c_str(), "r");
3789 if (f)
3790 {
3791 char line[256];
3792 while (fgets(line, sizeof(line), f))
3793 {
3794 std::string a = line;
3795 while (!a.empty() && (a.back() == '\n' || a.back() == '\r' ||
3796 a.back() == ' ' || a.back() == '\t'))
3797 a.pop_back();
3798 if (!a.empty())
3799 vPayoutAddrs.push_back(a);
3800 }
3801 fclose(f);
3802 LogPrint("worker", "[worker] loaded %zu payout addresses from %s\n",
3803 vPayoutAddrs.size(), strPayoutFile.c_str());
3804 }
3805 else
3806 LogPrint("worker", "[worker] cannot open payout file %s\n",
3807 strPayoutFile.c_str());
3808 }
3809
3810 std::string username = strPayoutAddress;
3811 if (username.empty() && !vPayoutAddrs.empty())
3812 username = vPayoutAddrs[nPayoutRot++ % vPayoutAddrs.size()];
3813 if (username.empty())
3572 { 3814 {
3573 std::vector<unsigned char> vchPubKey; 3815 std::vector<unsigned char> vchPubKey;
3574 if (CWalletDB("r").ReadDefaultKey(vchPubKey)) 3816 if (CWalletDB("r").ReadDefaultKey(vchPubKey))
@@ -3606,16 +3848,14 @@ static bool PoolParticipantMiner()
3606 // ------------------------------------------------------------------ 3848 // ------------------------------------------------------------------
3607 if (!RandomXFastReady()) { 3849 if (!RandomXFastReady()) {
3608 SetParticipantMiningStatus("building RandomX dataset (~2GB, one-time)"); 3850 SetParticipantMiningStatus("building RandomX dataset (~2GB, one-time)");
3609 unsigned int nThreads = std::thread::hardware_concurrency(); 3851 // Fill with the cores we are permitted to use, not the ones the host
3852 // happens to have. More threads than the cpuset allows does not build
3853 // the dataset any faster -- the CPU budget is the same either way --
3854 // it just oversubscribes, and this runs alongside the pool node.
3855 unsigned int nThreads = AvailableCoreCount();
3610 RandomXInitDataset(nThreads > 0 ? (int)nThreads : 1); 3856 RandomXInitDataset(nThreads > 0 ? (int)nThreads : 1);
3611 } 3857 }
3612 void* rxvm = RandomXCreateMinerVM(); 3858 LogPrint("worker", "[worker] RandomX dataset ready (%s)\n",
3613 if (!rxvm) {
3614 LogPrint("worker", "[worker] failed to create RandomX VM\n");
3615 SetParticipantMiningStatus("RandomX init failed");
3616 closesocket(s); return false;
3617 }
3618 LogPrint("worker", "[worker] RandomX VM ready (%s)\n",
3619 RandomXFastReady() ? "fast 2GB" : "light 256MB"); 3859 RandomXFastReady() ? "fast 2GB" : "light 256MB");
3620 3860
3621 // ------------------------------------------------------------------ 3861 // ------------------------------------------------------------------
@@ -3633,7 +3873,6 @@ static bool PoolParticipantMiner()
3633 {"params",json::array()}})) { 3873 {"params",json::array()}})) {
3634 LogPrint("worker", "[worker] subscribe send FAILED -- pool dropped connection\n"); 3874 LogPrint("worker", "[worker] subscribe send FAILED -- pool dropped connection\n");
3635 SetParticipantMiningStatus("subscribe send failed"); 3875 SetParticipantMiningStatus("subscribe send failed");
3636 RandomXDestroyMinerVM(rxvm);
3637 closesocket(s); return false; 3876 closesocket(s); return false;
3638 } 3877 }
3639 3878
@@ -3643,11 +3882,31 @@ static bool PoolParticipantMiner()
3643 {"params",json::array({username,"x"})}})) { 3882 {"params",json::array({username,"x"})}})) {
3644 LogPrint("worker", "[worker] authorize send FAILED -- pool dropped connection\n"); 3883 LogPrint("worker", "[worker] authorize send FAILED -- pool dropped connection\n");
3645 SetParticipantMiningStatus("authorize send failed"); 3884 SetParticipantMiningStatus("authorize send failed");
3646 RandomXDestroyMinerVM(rxvm);
3647 closesocket(s); return false; 3885 closesocket(s); return false;
3648 } 3886 }
3649 3887
3650 // ------------------------------------------------------------------ 3888 // ------------------------------------------------------------------
3889 // Step 6: spawn hash threads (each with its own RandomX VM on the
3890 // shared dataset), then run the I/O loop below: receive pool messages,
3891 // publish jobs to the threads, submit the shares they find.
3892 // ------------------------------------------------------------------
3893 // One count, decided in one place (main.cpp:MinerThreadCount's neighbour),
3894 // so that what StartMinerThreads() reports at startup and what actually
3895 // hashes here can never drift apart. Explicit /hashthreads=N is honored
3896 // exactly, including 1; automatic leaves one core for node + I/O threads.
3897 int nWorkers = ParticipantHashThreadCount();
3898 ParticipantJobShared jobShared;
3899 std::vector<ParticipantShareHit> shareHits;
3900 std::mutex shareHitsMtx;
3901 std::vector<std::thread> hashThreads;
3902 for (int i = 0; i < nWorkers; i++)
3903 hashThreads.emplace_back(ParticipantHashWorker, &jobShared, i, nWorkers,
3904 &shareHits, &shareHitsMtx);
3905 LogPrint("worker", "[worker] spawned %d hash threads on shared RandomX dataset\n",
3906 nWorkers);
3907 const uint64 sessionStartHashes = gParticipantHashes.load();
3908
3909 // ------------------------------------------------------------------
3651 // Step 6: main loop -- receive messages, hash, submit shares 3910 // Step 6: main loop -- receive messages, hash, submit shares
3652 // ------------------------------------------------------------------ 3911 // ------------------------------------------------------------------
3653 std::string jobId; 3912 std::string jobId;
@@ -3664,6 +3923,8 @@ static bool PoolParticipantMiner()
3664 uint64 sessionSharesSent = 0; // this connection only 3923 uint64 sessionSharesSent = 0; // this connection only
3665 uint64 sessionSharesAccepted = 0; 3924 uint64 sessionSharesAccepted = 0;
3666 int64 sampleStart = GetTime(); 3925 int64 sampleStart = GetTime();
3926 int64 connStart = GetTime();
3927 const int64 PAYOUT_ROTATE_SECS = 300; // reconnect with next /payoutfile address
3667 3928
3668 SetParticipantMiningStatus("waiting for pool response"); 3929 SetParticipantMiningStatus("waiting for pool response");
3669 3930
@@ -3771,6 +4032,14 @@ static bool PoolParticipantMiner()
3771 memcpy(&target, tgtBytes.data(), 32); 4032 memcpy(&target, tgtBytes.data(), 32);
3772 haveJob = true; 4033 haveJob = true;
3773 nNonce = 1; 4034 nNonce = 1;
4035 {
4036 std::lock_guard<std::mutex> lk(jobShared.mtx);
4037 memcpy(jobShared.header, header, 80);
4038 jobShared.target = target;
4039 jobShared.jobId = jobId;
4040 jobShared.jobSeq++;
4041 jobShared.haveJob = true;
4042 }
3774 LogPrint("worker", "[pool->worker] new job %s received\n", 4043 LogPrint("worker", "[pool->worker] new job %s received\n",
3775 jobId.c_str()); 4044 jobId.c_str());
3776 SetParticipantMiningStatus("hashing job " + jobId); 4045 SetParticipantMiningStatus("hashing job " + jobId);
@@ -3826,46 +4095,71 @@ static bool PoolParticipantMiner()
3826 } 4095 }
3827 } 4096 }
3828 4097
4098 // -- Payout address rotation: end this session cleanly so the outer
4099 // loop reconnects and authorizes the next /payoutfile address --
4100 if (!vPayoutAddrs.empty() && GetTime() - connStart > PAYOUT_ROTATE_SECS)
4101 {
4102 LogPrint("worker", "[worker] payout rotation interval reached"
4103 " -- reconnecting with next address\n");
4104 break;
4105 }
4106
3829 if (!haveJob) { Sleep(100); continue; } 4107 if (!haveJob) { Sleep(100); continue; }
3830 4108
3831 // -- Hash one nonce -- 4109 // -- Hash rate accounting (hashing happens on the worker threads) --
3832 memcpy(header+76, &nNonce, 4); 4110 hashesSinceSample = gParticipantHashes.load() - sessionStartHashes;
3833 uint256 hash = RandomXHashWithVM(rxvm, header, 80); 4111 UpdateParticipantHashRate(hashesSinceSample, sampleStart);
3834 hashesSinceSample++;
3835 gParticipantHashes.fetch_add(1);
3836 if ((hashesSinceSample & 0x3ff) == 0)
3837 UpdateParticipantHashRate(hashesSinceSample, sampleStart);
3838
3839 if (hash <= target) {
3840 LogPrint("worker", "[worker] share found! nonce=%u job=%s difficulty=%.4f\n",
3841 nNonce, jobId.c_str(), currentDifficulty);
3842 SetParticipantMiningStatus("share found; submitting");
3843 int submitId = msgId++;
3844 pendingSubmitIds.insert(submitId);
3845 gParticipantSharesSent.fetch_add(1);
3846 sessionSharesSent++;
3847 json submit = {{"id",submitId},{"method","mining.submit"},
3848 {"params",json::array({username, jobId, ToHexStr(&nNonce,4)})}};
3849 if (!StratumSendLine(s, submit)) {
3850 LogPrint("worker", "[worker] share submit send FAILED"
3851 " -- pool connection lost\n");
3852 break;
3853 }
3854 }
3855 4112
3856 nNonce++; 4113 // -- Submit any shares the hash threads found --
3857 if (nNonce == 0) { 4114 bool submitFailed = false;
3858 LogPrint("worker", "[worker] nonce space exhausted for job %s" 4115 {
3859 " -- waiting for new job\n", jobId.c_str()); 4116 std::vector<ParticipantShareHit> pending;
3860 haveJob = false; 4117 {
4118 std::lock_guard<std::mutex> lk(shareHitsMtx);
4119 pending.swap(shareHits);
4120 }
4121 for (ParticipantShareHit& hit : pending)
4122 {
4123 if (hit.jobId != jobId)
4124 {
4125 LogPrint("worker", "[worker] dropping stale share for old job %s\n",
4126 hit.jobId.c_str());
4127 continue;
4128 }
4129 LogPrint("worker", "[worker] share found! nonce=%u job=%s difficulty=%.4f\n",
4130 hit.nonce, hit.jobId.c_str(), currentDifficulty);
4131 SetParticipantMiningStatus("share found; submitting");
4132 int submitId = msgId++;
4133 pendingSubmitIds.insert(submitId);
4134 gParticipantSharesSent.fetch_add(1);
4135 sessionSharesSent++;
4136 json submit = {{"id",submitId},{"method","mining.submit"},
4137 {"params",json::array({username, hit.jobId,
4138 ToHexStr(&hit.nonce,4)})}};
4139 if (!StratumSendLine(s, submit)) {
4140 LogPrint("worker", "[worker] share submit send FAILED"
4141 " -- pool connection lost\n");
4142 submitFailed = true;
4143 break;
4144 }
4145 }
3861 } 4146 }
4147 if (submitFailed)
4148 break;
3862 } 4149 }
3863 4150
3864 // ------------------------------------------------------------------ 4151 // ------------------------------------------------------------------
3865 // Cleanup 4152 // Cleanup: stop the hash threads, then drop the pool connection
3866 // ------------------------------------------------------------------ 4153 // ------------------------------------------------------------------
3867 RandomXDestroyMinerVM(rxvm); 4154 {
4155 std::lock_guard<std::mutex> lk(jobShared.mtx);
4156 jobShared.stop = true;
4157 jobShared.haveJob = false;
4158 }
4159 for (std::thread& t : hashThreads)
4160 t.join();
3868 closesocket(s); 4161 closesocket(s);
4162 hashesSinceSample = gParticipantHashes.load() - sessionStartHashes;
3869 UpdateParticipantHashRate(hashesSinceSample, sampleStart); 4163 UpdateParticipantHashRate(hashesSinceSample, sampleStart);
3870 SetParticipantMiningStatus("disconnected"); 4164 SetParticipantMiningStatus("disconnected");
3871 LogPrint("worker", "[worker] disconnected from pool" 4165 LogPrint("worker", "[worker] disconnected from pool"
@@ -4090,7 +4384,7 @@ bool BitcoinMiner(int nThreadId)
4090 // Build the fast (~2 GB dataset) RandomX mode once, up front, so we're 4384 // Build the fast (~2 GB dataset) RandomX mode once, up front, so we're
4091 // not stuck mining in the much slower light/cache-only mode forever. 4385 // not stuck mining in the much slower light/cache-only mode forever.
4092 if (!RandomXFastReady()) { 4386 if (!RandomXFastReady()) {
4093 unsigned int nThreads = std::thread::hardware_concurrency(); 4387 unsigned int nThreads = AvailableCoreCount();
4094 RandomXInitDataset(nThreads > 0 ? (int)nThreads : 1); 4388 RandomXInitDataset(nThreads > 0 ? (int)nThreads : 1);
4095 } 4389 }
4096 4390
@@ -4177,7 +4471,19 @@ bool BitcoinMiner(int nThreadId)
4177 vector<char> vfAlreadyAdded(mapTransactions.size()); 4471 vector<char> vfAlreadyAdded(mapTransactions.size());
4178 bool fFoundSomething = true; 4472 bool fFoundSomething = true;
4179 unsigned int nBlockSize = 0; 4473 unsigned int nBlockSize = 0;
4180 while (fFoundSomething && nBlockSize < MAX_SIZE/2) 4474 // Assembly must stop below the consensus cap that CheckBlock()
4475 // enforces, or we mine a block our own node then rejects.
4476 //
4477 // This used to read MAX_SIZE/2 (16 MB), which was consistent while
4478 // CheckBlock() also tested against MAX_SIZE (32 MB). Adding
4479 // MAX_BLOCK_SIZE inverted it: assembly could fill 16 MB and
4480 // validation would refuse anything over 1 MB, so a full mempool
4481 // meant solving a block and throwing it away -- with this pool at
4482 // ~90% of hashrate, that is the network stalling, not just us.
4483 //
4484 // Half the cap, keeping the original margin: the test runs before
4485 // a transaction is added, so the loop can overshoot by one tx.
4486 while (fFoundSomething && nBlockSize < MAX_BLOCK_SIZE/2)
4181 { 4487 {
4182 fFoundSomething = false; 4488 fFoundSomething = false;
4183 unsigned int n = 0; 4489 unsigned int n = 0;
@@ -4778,7 +5084,18 @@ bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew)
4778 { 5084 {
4779 string strError; 5085 string strError;
4780 if (nValue + nFeeRequired > GetBalance()) 5086 if (nValue + nFeeRequired > GetBalance())
4781 strError = strprintf("Error: This is an oversized transaction that requires a transaction fee of %s ", FormatMoney(nFeeRequired).c_str()); 5087 // Not an oversized transaction -- this branch is simply "the
5088 // spendable balance is too small". Saying otherwise (with a fee
5089 // of 0.00, because CreateTransaction failed before setting one)
5090 // sends anyone reading the log after a failed payout hunting a
5091 // transaction-size problem that does not exist.
5092 strError = strprintf("Error: insufficient spendable balance:"
5093 " need %s (plus fee %s) but only %s is spendable"
5094 " -- mined coin counts as zero until it is %d deep",
5095 FormatMoney(nValue).c_str(),
5096 FormatMoney(nFeeRequired).c_str(),
5097 FormatMoney(GetBalance()).c_str(),
5098 COINBASE_SPENDABLE_DEPTH);
4782 else 5099 else
4783 strError = "Error: Transaction creation failed "; 5100 strError = "Error: Transaction creation failed ";
4784 return error("SendMoney() : %s\n", strError.c_str()); 5101 return error("SendMoney() : %s\n", strError.c_str());
diff --git a/src/main.h b/src/main.h
index b389f1d..8bb536b 100644
--- a/src/main.h
+++ b/src/main.h
@@ -41,6 +41,12 @@ static const int64 MAX_MONEY = 21000000 * COIN;
41inline bool MoneyRange(int64 nValue) { return (nValue >= 0 && nValue <= MAX_MONEY); } 41inline bool MoneyRange(int64 nValue) { return (nValue >= 0 && nValue <= MAX_MONEY); }
42static const int64 CENT = 1000000; 42static const int64 CENT = 1000000;
43static const int COINBASE_MATURITY = 100; 43static const int COINBASE_MATURITY = 100;
44// Depth at which a coinbase we mined is actually usable. CMerkleTx::GetCredit
45// values a coinbase at zero until GetBlocksToMaturity() reaches 0, which is
46// this depth -- not COINBASE_MATURITY. Anything that plans to *spend* mined
47// coin must gate on this, or it will try to pay out of a balance the wallet
48// still reports as zero. Both rules live here so they cannot drift apart.
49static const int COINBASE_SPENDABLE_DEPTH = COINBASE_MATURITY + 20;
44 50
45// Bitflash difficulty floor. The PoW is RandomX (memory-hard), ~1000x slower 51// Bitflash difficulty floor. The PoW is RandomX (memory-hard), ~1000x slower
46// per hash than SHA-256d, so the target is much easier than Bitcoin's to let a 52// per hash than SHA-256d, so the target is much easier than Bitcoin's to let a
@@ -84,6 +90,11 @@ extern int nMineMode;
84// something a previous session wrote. 90// something a previous session wrote.
85extern bool fMineModeFromCommandLine; 91extern bool fMineModeFromCommandLine;
86extern string strParticipantPool; // participant: pool .btf address 92extern string strParticipantPool; // participant: pool .btf address
93extern string strPayoutAddress; // participant: /payoutaddr override (mine without a wallet)
94extern string strPayoutFile; // participant: /payoutfile address list, rotated per reconnect
95extern bool fPoolNoAnnounce; // /noannounce: suppress pool announcements
96extern bool fPoolMineBlind; // /poolmineblind: mine on even when deaf to every peer
97extern int nHashThreads; // participant: /hashthreads override (0 = cores-1)
87extern string strPoolName; // operator: announced pool name 98extern string strPoolName; // operator: announced pool name
88extern string strPoolDashboardUrl; // operator: optional dashboard URL 99extern string strPoolDashboardUrl; // operator: optional dashboard URL
89extern string strPoolStatusFile; // operator: optional JSON status output path 100extern string strPoolStatusFile; // operator: optional JSON status output path
@@ -96,6 +107,9 @@ extern bool fSoloMineTest; // /solomine: mine without requiring a peer (local te
96extern int nMinerThreads; 107extern int nMinerThreads;
97// Resolves the automatic case into a real count. 108// Resolves the automatic case into a real count.
98int MinerThreadCount(); 109int MinerThreadCount();
110// Hash threads one participant session runs; resolves /hashthreads. Not the
111// same number as MinerThreadCount(), and only ONE session ever runs.
112int ParticipantHashThreadCount();
99extern bool gPoolServerRunning; // GUI/startup state 113extern bool gPoolServerRunning; // GUI/startup state
100extern volatile bool gPoolRunning; // runtime pool server loop flag (rpc.cpp) 114extern volatile bool gPoolRunning; // runtime pool server loop flag (rpc.cpp)
101 115
diff --git a/src/main_gui.cpp b/src/main_gui.cpp
index bef8503..e941522 100644
--- a/src/main_gui.cpp
+++ b/src/main_gui.cpp
@@ -117,6 +117,9 @@ static void PrintUsage()
117 printf("Mining mode:\n"); 117 printf("Mining mode:\n");
118 printf(" /operator\n"); 118 printf(" /operator\n");
119 printf(" /participant=POOL_BTF_ADDRESS\n"); 119 printf(" /participant=POOL_BTF_ADDRESS\n");
120 printf(" /payoutaddr=BTF_ADDRESS (participant: pay shares to this address, no wallet needed)\n");
121 printf(" /payoutfile=FILE (participant: rotate payout addresses from file)\n");
122 printf(" /hashthreads=N (participant: mining threads, default cores-1)\n");
120 printf(" /stratumbridge=POOL_BTF_ADDRESS\n"); 123 printf(" /stratumbridge=POOL_BTF_ADDRESS\n");
121 printf(" /stratumbridgeport=N (default 3333; listen on 127.0.0.1)\n"); 124 printf(" /stratumbridgeport=N (default 3333; listen on 127.0.0.1)\n");
122 printf(" /solomine\n"); 125 printf(" /solomine\n");
@@ -207,7 +210,7 @@ static void ParseStartupArguments(int argc, char* argv[])
207 if (!strProcLimit.empty()) 210 if (!strProcLimit.empty())
208 { 211 {
209 int n = atoi(strProcLimit.c_str()); 212 int n = atoi(strProcLimit.c_str());
210 unsigned int nCores = std::thread::hardware_concurrency(); 213 unsigned int nCores = AvailableCoreCount();
211 if (n < 0) 214 if (n < 0)
212 fprintf(stderr, "Ignoring /genproclimit=%s: not a thread count\n", 215 fprintf(stderr, "Ignoring /genproclimit=%s: not a thread count\n",
213 strProcLimit.c_str()); 216 strProcLimit.c_str());
@@ -233,6 +236,18 @@ static void ParseStartupArguments(int argc, char* argv[])
233 fMineModeFromCommandLine = true; 236 fMineModeFromCommandLine = true;
234 } 237 }
235 238
239 string payoutAddr = argval2(argc, argv, "/payoutaddr", "-payoutaddr");
240 if (!payoutAddr.empty())
241 strPayoutAddress = payoutAddr;
242
243 string payoutFile = argval2(argc, argv, "/payoutfile", "-payoutfile");
244 if (!payoutFile.empty())
245 strPayoutFile = payoutFile;
246
247 string hashThreads = argval2(argc, argv, "/hashthreads", "-hashthreads");
248 if (!hashThreads.empty())
249 nHashThreads = atoi(hashThreads.c_str());
250
236 if (arg(argc,argv,"/stratumbridge") || arg(argc,argv,"-stratumbridge")) 251 if (arg(argc,argv,"/stratumbridge") || arg(argc,argv,"-stratumbridge"))
237 { 252 {
238 fStratumBridge = true; 253 fStratumBridge = true;
@@ -259,6 +274,12 @@ static void ParseStartupArguments(int argc, char* argv[])
259 if (!poolDash.empty()) 274 if (!poolDash.empty())
260 strPoolDashboardUrl = poolDash; 275 strPoolDashboardUrl = poolDash;
261 276
277 if (arg(argc,argv,"/noannounce") || arg(argc,argv,"-noannounce"))
278 fPoolNoAnnounce = true;
279
280 if (arg(argc,argv,"/poolmineblind") || arg(argc,argv,"-poolmineblind"))
281 fPoolMineBlind = true;
282
262 string poolFee = argval2(argc, argv, "/poolfee", "-poolfee"); 283 string poolFee = argval2(argc, argv, "/poolfee", "-poolfee");
263 if (!poolFee.empty()) 284 if (!poolFee.empty())
264 dPoolFeePercent = atof(poolFee.c_str()); 285 dPoolFeePercent = atof(poolFee.c_str());
@@ -278,8 +299,20 @@ static void ParseStartupArguments(int argc, char* argv[])
278 string rvRelay = argval2(argc, argv, "/rvrelay", "-rvrelay"); 299 string rvRelay = argval2(argc, argv, "/rvrelay", "-rvrelay");
279 if (!rvRelay.empty()) 300 if (!rvRelay.empty())
280 { 301 {
281 vBtfMeetingRelays.clear(); 302 if (!LooksLikeRelay(rvRelay))
282 vBtfMeetingRelays.push_back(rvRelay); 303 {
304 // A malformed pin must not replace the seed list: with the pin
305 // set, no discovered relay can ever mask the typo, and the pool
306 // would publish a garbage (or no) meeting node forever.
307 fprintf(stderr, "Ignoring /rvrelay=%s: expected host:port\n",
308 rvRelay.c_str());
309 }
310 else
311 {
312 vBtfMeetingRelays.clear();
313 vBtfMeetingRelays.push_back(rvRelay);
314 fBtfRelayPinned = true;
315 }
283 } 316 }
284 317
285 string announceRelay = argval2(argc, argv, "/announcerelay", "-announcerelay"); 318 string announceRelay = argval2(argc, argv, "/announcerelay", "-announcerelay");
@@ -702,6 +735,31 @@ int main(int argc, char* argv[])
702 735
703 ReacceptWalletTransactions(); 736 ReacceptWalletTransactions();
704 737
738 // /dumpaddresses=N:FILE -- generate N fresh wallet keys and write their
739 // addresses (one per line) to FILE, then exit. Keys stay in this wallet;
740 // the file alone is enough for a miner elsewhere to rotate payout
741 // addresses via /payoutfile. (No HD/xpub exists in this wallet format.)
742 string dumpSpec = argval2(argc, argv, "/dumpaddresses", "-dumpaddresses");
743 if (!dumpSpec.empty())
744 {
745 size_t colon = dumpSpec.rfind(':');
746 if (colon == string::npos) { fprintf(stderr, "usage: /dumpaddresses=N:FILE\n"); return 1; }
747 int n = atoi(dumpSpec.substr(0, colon).c_str());
748 string file = dumpSpec.substr(colon + 1);
749 FILE* f = fopen(file.c_str(), "w");
750 if (!f) { fprintf(stderr, "cannot open %s\n", file.c_str()); return 1; }
751 for (int i = 0; i < n; i++)
752 {
753 CKey key;
754 key.MakeNewKey();
755 if (!AddKey(key)) { fclose(f); fprintf(stderr, "AddKey failed at %d\n", i); return 1; }
756 fprintf(f, "%s\n", PubKeyToAddress(key.GetPubKey()).c_str());
757 }
758 fclose(f);
759 printf("wrote %d fresh wallet addresses to %s\n", n, file.c_str());
760 return 0;
761 }
762
705 if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; } 763 if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; }
706 764
707 if (nMineMode == MINE_OPERATOR) { 765 if (nMineMode == MINE_OPERATOR) {
diff --git a/src/makefile.mingw b/src/makefile.mingw
index f82247a..8b49cac 100644
--- a/src/makefile.mingw
+++ b/src/makefile.mingw
@@ -35,7 +35,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
35 obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o 35 obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o
36 36
37NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ 37NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \
38 obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ 38 obj/sha.o obj/nostr.o obj/randomx_pow.o \
39 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ 39 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \
40 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o 40 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o
41 41
@@ -50,7 +50,7 @@ GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o
50# binary. Nothing crashed here, and nothing would say so if it had. 50# binary. Nothing crashed here, and nothing would say so if it had.
51HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 51HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \
52 key.h bignum.h script.h db.h base58.h net.h nostr.h \ 52 key.h bignum.h script.h db.h base58.h net.h nostr.h \
53 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ 53 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \
54 selftest.h walletcmd.h sockcount.h proxy.h compat.h font_roboto.h 54 selftest.h walletcmd.h sockcount.h proxy.h compat.h font_roboto.h
55 55
56all: bitflash.exe 56all: bitflash.exe
diff --git a/src/market.cpp b/src/market.cpp
deleted file mode 100644
index e5d34f4..0000000
--- a/src/market.cpp
+++ /dev/null
@@ -1,264 +0,0 @@
1// Copyright (c) 2009 Satoshi Nakamoto
2// Distributed under the MIT/X11 software license, see the accompanying
3// file license.txt or http://www.opensource.org/licenses/mit-license.php.
4
5#include "headers.h"
6
7
8
9
10
11
12
13
14
15
16//
17// Global state variables
18//
19
20//// later figure out how these are persisted
21map<uint256, CProduct> mapMyProducts;
22
23
24
25
26map<uint256, CProduct> mapProducts;
27CCriticalSection cs_mapProducts;
28
29bool AdvertInsert(const CProduct& product)
30{
31 uint256 hash = product.GetHash();
32 bool fNew = false;
33 bool fUpdated = false;
34
35 CRITICAL_BLOCK(cs_mapProducts)
36 {
37 // Insert or find existing product
38 pair<map<uint256, CProduct>::iterator, bool> item = mapProducts.insert(make_pair(hash, product));
39 CProduct* pproduct = &(*(item.first)).second;
40 fNew = item.second;
41
42 // Update if newer
43 if (product.nSequence > pproduct->nSequence)
44 {
45 *pproduct = product;
46 fUpdated = true;
47 }
48 }
49
50 //if (fNew)
51 // NotifyProductAdded(hash);
52 //else if (fUpdated)
53 // NotifyProductUpdated(hash);
54
55 return (fNew || fUpdated);
56}
57
58void AdvertErase(const CProduct& product)
59{
60 uint256 hash = product.GetHash();
61 CRITICAL_BLOCK(cs_mapProducts)
62 mapProducts.erase(hash);
63 //NotifyProductDeleted(hash);
64}
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84template<typename T>
85unsigned int Union(T& v1, T& v2)
86{
87 // v1 = v1 union v2
88 // v1 and v2 must be sorted
89 // returns the number of elements added to v1
90
91 ///// need to check that this is equivalent, then delete this comment
92 //vector<unsigned short> vUnion(v1.size() + v2.size());
93 //vUnion.erase(set_union(v1.begin(), v1.end(),
94 // v2.begin(), v2.end(),
95 // vUnion.begin()),
96 // vUnion.end());
97
98 T vUnion;
99 vUnion.reserve(v1.size() + v2.size());
100 set_union(v1.begin(), v1.end(),
101 v2.begin(), v2.end(),
102 back_inserter(vUnion));
103 unsigned int nAdded = vUnion.size() - v1.size();
104 if (nAdded > 0)
105 v1 = vUnion;
106 return nAdded;
107}
108
109void CUser::AddAtom(unsigned short nAtom, bool fOrigin)
110{
111 // Ignore duplicates
112 if (binary_search(vAtomsIn.begin(), vAtomsIn.end(), nAtom) ||
113 find(vAtomsNew.begin(), vAtomsNew.end(), nAtom) != vAtomsNew.end())
114 return;
115
116 //// instead of zero atom, should change to free atom that propagates,
117 //// limited to lower than a certain value like 5 so conflicts quickly
118 // The zero atom never propagates,
119 // new atoms always propagate through the user that created them
120 if (nAtom == 0 || fOrigin)
121 {
122 vector<unsigned short> vTmp(1, nAtom);
123 Union(vAtomsIn, vTmp);
124 if (fOrigin)
125 vAtomsOut.push_back(nAtom);
126 return;
127 }
128
129 vAtomsNew.push_back(nAtom);
130
131 if (vAtomsNew.size() >= nFlowthroughRate || vAtomsOut.empty())
132 {
133 // Select atom to flow through to vAtomsOut
134 vAtomsOut.push_back(vAtomsNew[GetRand(vAtomsNew.size())]);
135
136 // Merge vAtomsNew into vAtomsIn
137 sort(vAtomsNew.begin(), vAtomsNew.end());
138 Union(vAtomsIn, vAtomsNew);
139 vAtomsNew.clear();
140 }
141}
142
143bool AddAtomsAndPropagate(uint256 hashUserStart, const vector<unsigned short>& vAtoms, bool fOrigin)
144{
145 CReviewDB reviewdb;
146 map<uint256, vector<unsigned short> > pmapPropagate[2];
147 pmapPropagate[0][hashUserStart] = vAtoms;
148
149 for (int side = 0; !pmapPropagate[side].empty(); side = 1 - side)
150 {
151 map<uint256, vector<unsigned short> >& mapFrom = pmapPropagate[side];
152 map<uint256, vector<unsigned short> >& mapTo = pmapPropagate[1 - side];
153
154 for (map<uint256, vector<unsigned short> >::iterator mi = mapFrom.begin(); mi != mapFrom.end(); ++mi)
155 {
156 const uint256& hashUser = (*mi).first;
157 const vector<unsigned short>& vReceived = (*mi).second;
158
159 ///// this would be a lot easier on the database if it put the new atom at the beginning of the list,
160 ///// so the change would be right next to the vector size.
161
162 // Read user
163 CUser user;
164 reviewdb.ReadUser(hashUser, user);
165 unsigned int nIn = user.vAtomsIn.size();
166 unsigned int nNew = user.vAtomsNew.size();
167 unsigned int nOut = user.vAtomsOut.size();
168
169 // Add atoms received
170 foreach(unsigned short nAtom, vReceived)
171 user.AddAtom(nAtom, fOrigin);
172 fOrigin = false;
173
174 // Don't bother writing to disk if no changes
175 if (user.vAtomsIn.size() == nIn && user.vAtomsNew.size() == nNew)
176 continue;
177
178 // Propagate
179 if (user.vAtomsOut.size() > nOut)
180 foreach(const uint256& hash, user.vLinksOut)
181 mapTo[hash].insert(mapTo[hash].end(), user.vAtomsOut.begin() + nOut, user.vAtomsOut.end());
182
183 // Write back
184 if (!reviewdb.WriteUser(hashUser, user))
185 return false;
186 }
187 mapFrom.clear();
188 }
189 return true;
190}
191
192
193
194
195
196
197bool CReview::AcceptReview()
198{
199 // Timestamp
200 nTime = GetTime();
201
202 // Check signature
203 if (!CKey::Verify(vchPubKeyFrom, GetSigHash(), vchSig))
204 return false;
205
206 CReviewDB reviewdb;
207
208 // Add review text to recipient
209 vector<CReview> vReviews;
210 reviewdb.ReadReviews(hashTo, vReviews);
211 vReviews.push_back(*this);
212 if (!reviewdb.WriteReviews(hashTo, vReviews))
213 return false;
214
215 // Add link from sender
216 CUser user;
217 uint256 hashFrom = Hash(vchPubKeyFrom.begin(), vchPubKeyFrom.end());
218 reviewdb.ReadUser(hashFrom, user);
219 user.vLinksOut.push_back(hashTo);
220 if (!reviewdb.WriteUser(hashFrom, user))
221 return false;
222
223 reviewdb.Close();
224
225 // Propagate atoms to recipient
226 vector<unsigned short> vZeroAtom(1, 0);
227 if (!AddAtomsAndPropagate(hashTo, user.vAtomsOut.size() ? user.vAtomsOut : vZeroAtom, false))
228 return false;
229
230 return true;
231}
232
233
234
235
236
237bool CProduct::CheckSignature()
238{
239 return (CKey::Verify(vchPubKeyFrom, GetSigHash(), vchSig));
240}
241
242bool CProduct::CheckProduct()
243{
244 if (!CheckSignature())
245 return false;
246
247 // Make sure it's a summary product
248 if (!mapDetails.empty() || !vOrderForm.empty())
249 return false;
250
251 // Look up seller's atom count
252 CReviewDB reviewdb("r");
253 CUser user;
254 reviewdb.ReadUser(GetUserHash(), user);
255 nAtoms = user.GetAtomCount();
256 reviewdb.Close();
257
258 ////// delme, this is now done by AdvertInsert
259 //// Store to memory
260 //CRITICAL_BLOCK(cs_mapProducts)
261 // mapProducts[GetHash()] = *this;
262
263 return true;
264}
diff --git a/src/market.h b/src/market.h
deleted file mode 100644
index b5c8b3b..0000000
--- a/src/market.h
+++ /dev/null
@@ -1,182 +0,0 @@
1// Copyright (c) 2009 Satoshi Nakamoto
2// Distributed under the MIT/X11 software license, see the accompanying
3// file license.txt or http://www.opensource.org/licenses/mit-license.php.
4
5class CUser;
6class CReview;
7class CProduct;
8
9static const unsigned int nFlowthroughRate = 2;
10
11
12
13
14bool AdvertInsert(const CProduct& product);
15void AdvertErase(const CProduct& product);
16bool AddAtomsAndPropagate(uint256 hashUserStart, const vector<unsigned short>& vAtoms, bool fOrigin);
17
18
19
20
21
22
23
24
25class CUser
26{
27public:
28 vector<unsigned short> vAtomsIn;
29 vector<unsigned short> vAtomsNew;
30 vector<unsigned short> vAtomsOut;
31 vector<uint256> vLinksOut;
32
33 CUser()
34 {
35 }
36
37 IMPLEMENT_SERIALIZE
38 (
39 if (!(nType & SER_GETHASH))
40 READWRITE(nVersion);
41 READWRITE(vAtomsIn);
42 READWRITE(vAtomsNew);
43 READWRITE(vAtomsOut);
44 READWRITE(vLinksOut);
45 )
46
47 void SetNull()
48 {
49 vAtomsIn.clear();
50 vAtomsNew.clear();
51 vAtomsOut.clear();
52 vLinksOut.clear();
53 }
54
55 uint256 GetHash() const { return SerializeHash(*this); }
56
57
58 int GetAtomCount() const
59 {
60 return (vAtomsIn.size() + vAtomsNew.size());
61 }
62
63 void AddAtom(unsigned short nAtom, bool fOrigin);
64};
65
66
67
68
69
70
71
72class CReview
73{
74public:
75 int nVersion;
76 uint256 hashTo;
77 map<string, string> mapValue;
78 vector<unsigned char> vchPubKeyFrom;
79 vector<unsigned char> vchSig;
80
81 // memory only
82 unsigned int nTime;
83 int nAtoms;
84
85
86 CReview()
87 {
88 nVersion = 1;
89 hashTo = 0;
90 nTime = 0;
91 nAtoms = 0;
92 }
93
94 IMPLEMENT_SERIALIZE
95 (
96 READWRITE(this->nVersion);
97 nVersion = this->nVersion;
98 if (!(nType & SER_DISK))
99 READWRITE(hashTo);
100 READWRITE(mapValue);
101 READWRITE(vchPubKeyFrom);
102 if (!(nType & SER_GETHASH))
103 READWRITE(vchSig);
104 )
105
106 uint256 GetHash() const { return SerializeHash(*this); }
107 uint256 GetSigHash() const { return SerializeHash(*this, SER_GETHASH|SER_SKIPSIG); }
108 uint256 GetUserHash() const { return Hash(vchPubKeyFrom.begin(), vchPubKeyFrom.end()); }
109
110
111 bool AcceptReview();
112};
113
114
115
116
117
118
119
120class CProduct
121{
122public:
123 int nVersion;
124 CAddress addr;
125 map<string, string> mapValue;
126 map<string, string> mapDetails;
127 vector<pair<string, string> > vOrderForm;
128 unsigned int nSequence;
129 vector<unsigned char> vchPubKeyFrom;
130 vector<unsigned char> vchSig;
131
132 // disk only
133 int nAtoms;
134
135 // memory only
136 set<unsigned int> setSources;
137
138 CProduct()
139 {
140 nVersion = 1;
141 nAtoms = 0;
142 nSequence = 0;
143 }
144
145 IMPLEMENT_SERIALIZE
146 (
147 READWRITE(this->nVersion);
148 nVersion = this->nVersion;
149 READWRITE(addr);
150 READWRITE(mapValue);
151 if (!(nType & SER_GETHASH))
152 {
153 READWRITE(mapDetails);
154 READWRITE(vOrderForm);
155 READWRITE(nSequence);
156 }
157 READWRITE(vchPubKeyFrom);
158 if (!(nType & SER_GETHASH))
159 READWRITE(vchSig);
160 if (nType & SER_DISK)
161 READWRITE(nAtoms);
162 )
163
164 uint256 GetHash() const { return SerializeHash(*this); }
165 uint256 GetSigHash() const { return SerializeHash(*this, SER_GETHASH|SER_SKIPSIG); }
166 uint256 GetUserHash() const { return Hash(vchPubKeyFrom.begin(), vchPubKeyFrom.end()); }
167
168
169 bool CheckSignature();
170 bool CheckProduct();
171};
172
173
174
175
176
177
178
179
180extern map<uint256, CProduct> mapProducts;
181extern CCriticalSection cs_mapProducts;
182extern map<uint256, CProduct> mapMyProducts;
diff --git a/src/net.cpp b/src/net.cpp
index c24547e..bb39122 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -464,28 +464,9 @@ bool GetMyExternalIP(unsigned int& ipRet)
464 464
465 465
466 466
467void AbandonRequests(void (*fn)(void*, CDataStream&), void* param1) 467// AbandonRequests() lived here, sweeping CNode::mapRequests for a callback a
468{ 468// closing dialog had registered. Nothing ever registered one and nothing ever
469 // If the dialog might get closed before the reply comes back, 469// called this, so it went with CRequestTracker.
470 // call this in the destructor so it doesn't get called after it's deleted.
471 CRITICAL_BLOCK(cs_vNodes)
472 {
473 foreach(CNode* pnode, vNodes)
474 {
475 CRITICAL_BLOCK(pnode->cs_mapRequests)
476 {
477 for (map<uint256, CRequestTracker>::iterator mi = pnode->mapRequests.begin(); mi != pnode->mapRequests.end();)
478 {
479 CRequestTracker& tracker = (*mi).second;
480 if (tracker.fn == fn && tracker.param1 == param1)
481 pnode->mapRequests.erase(mi++);
482 else
483 mi++;
484 }
485 }
486 }
487 }
488}
489 470
490 471
491 472
@@ -493,25 +474,10 @@ void AbandonRequests(void (*fn)(void*, CDataStream&), void* param1)
493 474
494 475
495 476
496// 477// The broadcast/subscribe channel system (AnySubscribed, CNode::IsSubscribed,
497// Subscription methods for the broadcast and subscription system. 478// Subscribe, CancelSubscribe) stood here. Its channels were MSG_TABLE and
498// Channel numbers are message numbers, i.e. MSG_TABLE and MSG_PRODUCT. 479// MSG_PRODUCT, there was never an inbound handler for "subscribe",
499// 480// "sub-cancel", "publish" or "pub-cancel", and the objects it carried are gone.
500// The subscription system uses a meet-in-the-middle strategy.
501// With 100,000 nodes, if senders broadcast to 1000 random nodes and receivers
502// subscribe to 1000 random nodes, 99.995% (1 - 0.99^1000) of messages will get through.
503//
504
505bool AnySubscribed(unsigned int nChannel)
506{
507 if (pnodeLocalHost->IsSubscribed(nChannel))
508 return true;
509 CRITICAL_BLOCK(cs_vNodes)
510 foreach(CNode* pnode, vNodes)
511 if (pnode->IsSubscribed(nChannel))
512 return true;
513 return false;
514}
515 481
516void CNode::PushGetBlocks(CBlockIndex* pindexBegin, uint256 hashEnd) 482void CNode::PushGetBlocks(CBlockIndex* pindexBegin, uint256 hashEnd)
517{ 483{
@@ -524,55 +490,6 @@ void CNode::PushGetBlocks(CBlockIndex* pindexBegin, uint256 hashEnd)
524 PushMessage("getblocks", CBlockLocator(pindexBegin), hashEnd); 490 PushMessage("getblocks", CBlockLocator(pindexBegin), hashEnd);
525} 491}
526 492
527bool CNode::IsSubscribed(unsigned int nChannel)
528{
529 if (nChannel >= vfSubscribe.size())
530 return false;
531 return vfSubscribe[nChannel];
532}
533
534void CNode::Subscribe(unsigned int nChannel, unsigned int nHops)
535{
536 if (nChannel >= vfSubscribe.size())
537 return;
538
539 if (!AnySubscribed(nChannel))
540 {
541 // Relay subscribe
542 CRITICAL_BLOCK(cs_vNodes)
543 foreach(CNode* pnode, vNodes)
544 if (pnode != this)
545 pnode->PushMessage("subscribe", nChannel, nHops);
546 }
547
548 vfSubscribe[nChannel] = true;
549}
550
551void CNode::CancelSubscribe(unsigned int nChannel)
552{
553 if (nChannel >= vfSubscribe.size())
554 return;
555
556 // Prevent from relaying cancel if wasn't subscribed
557 if (!vfSubscribe[nChannel])
558 return;
559 vfSubscribe[nChannel] = false;
560
561 if (!AnySubscribed(nChannel))
562 {
563 // Relay subscription cancel
564 CRITICAL_BLOCK(cs_vNodes)
565 foreach(CNode* pnode, vNodes)
566 if (pnode != this)
567 pnode->PushMessage("sub-cancel", nChannel);
568
569 // Clear memory, no longer subscribed
570 if (nChannel == MSG_PRODUCT)
571 CRITICAL_BLOCK(cs_mapProducts)
572 mapProducts.clear();
573 }
574}
575
576 493
577 494
578 495
@@ -1074,6 +991,18 @@ CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting
1074void ThreadBtfAccept(void* parg) 991void ThreadBtfAccept(void* parg)
1075{ 992{
1076 printf("ThreadBtfAccept started\n"); 993 printf("ThreadBtfAccept started\n");
994 // In operator mode the pool server registers this same node identity as a
995 // stratum service at the rendezvous relays, and a relay only holds ONE
996 // service registration per pubkey -- peer-accept and pool-accept would
997 // kick each other off forever, breaking both. The pool's registration is
998 // the one that must be stable (that's what miners resolve and dial), so
999 // the node skips inbound rendezvous here and relies on outbound .btf
1000 // connections for block sync.
1001 if (nMineMode == MINE_OPERATOR)
1002 {
1003 printf("ThreadBtfAccept: operator mode -- skipping peer rendezvous registration\n");
1004 return;
1005 }
1077 unsigned char pk[32]; 1006 unsigned char pk[32];
1078 unsigned char sk[32]; 1007 unsigned char sk[32];
1079 while (!BtfGetIdentity(pk, sk)) 1008 while (!BtfGetIdentity(pk, sk))
@@ -1223,17 +1152,6 @@ void CNode::Disconnect()
1223 BtfCloseSocket(hSocket); 1152 BtfCloseSocket(hSocket);
1224 hSocket = INVALID_SOCKET; 1153 hSocket = INVALID_SOCKET;
1225 1154
1226 // All of a nodes broadcasts and subscriptions are automatically torn down
1227 // when it goes down, so a node has to stay up to keep its broadcast going.
1228
1229 CRITICAL_BLOCK(cs_mapProducts)
1230 for (map<uint256, CProduct>::iterator mi = mapProducts.begin(); mi != mapProducts.end();)
1231 AdvertRemoveSource(this, MSG_PRODUCT, 0, (*(mi++)).second);
1232
1233 // Cancel subscriptions
1234 for (unsigned int nChannel = 0; nChannel < vfSubscribe.size(); nChannel++)
1235 if (vfSubscribe[nChannel])
1236 CancelSubscribe(nChannel);
1237} 1155}
1238 1156
1239 1157
@@ -1358,7 +1276,6 @@ void ThreadSocketHandler2(void* parg)
1358 bool fDelete = false; 1276 bool fDelete = false;
1359 TRY_CRITICAL_BLOCK(pnode->cs_vSend) 1277 TRY_CRITICAL_BLOCK(pnode->cs_vSend)
1360 TRY_CRITICAL_BLOCK(pnode->cs_vRecv) 1278 TRY_CRITICAL_BLOCK(pnode->cs_vRecv)
1361 TRY_CRITICAL_BLOCK(pnode->cs_mapRequests)
1362 TRY_CRITICAL_BLOCK(pnode->cs_inventory) 1279 TRY_CRITICAL_BLOCK(pnode->cs_inventory)
1363 fDelete = true; 1280 fDelete = true;
1364 if (fDelete) 1281 if (fDelete)
@@ -1466,6 +1383,27 @@ void ThreadSocketHandler2(void* parg)
1466 printf("dropping node %s: its socket is no longer valid\n", 1383 printf("dropping node %s: its socket is no longer valid\n",
1467 pnode->addr.ToString().c_str()); 1384 pnode->addr.ToString().c_str());
1468 pnode->fDisconnect = true; 1385 pnode->fDisconnect = true;
1386
1387 // Stop referring to it, or it goes into the next poll
1388 // set and fails it again.
1389 //
1390 // Marking fDisconnect is not enough: a node is only
1391 // removed from vNodes once it has drained or once
1392 // DISCONNECT_DRAIN_SECS has passed, so one dead
1393 // descriptor kept failing the wait for a full minute --
1394 // and a failed wait skips the service loop below
1395 // entirely, which means NO peer is read on that pass.
1396 // Measured on a wedged pool under the old select()
1397 // implementation: 1,027 consecutive passes over 61
1398 // seconds, one bad socket, the entire node deaf. poll()
1399 // reports a bad descriptor as POLLNVAL on that entry
1400 // rather than failing the whole call, so this path is
1401 // now the backstop rather than the common case.
1402 //
1403 // Not closed, only forgotten. getsockopt just told us
1404 // this descriptor is not ours any more; closing it now
1405 // is how an unrelated socket gets closed instead.
1406 pnode->hSocket = INVALID_SOCKET;
1469 } 1407 }
1470 } 1408 }
1471 } 1409 }
@@ -1710,6 +1648,13 @@ void ThreadMessageHandler(void* parg)
1710 } 1648 }
1711} 1649}
1712 1650
1651// When any peer last told us a height. A wedged node -- one that still has
1652// peers but has stopped hearing them -- is exactly a node where this stops
1653// advancing, and that is the one thing the height comparison itself cannot
1654// see: there is no height to compare.
1655static int64 nLastPeerHeightSeen = 0;
1656static int64 nPeerHeightFirstAsked = 0;
1657
1713int GetPeerMedianHeight() 1658int GetPeerMedianHeight()
1714{ 1659{
1715 vector<int> vHeights; 1660 vector<int> vHeights;
@@ -1721,10 +1666,26 @@ int GetPeerMedianHeight()
1721 if (vHeights.empty()) 1666 if (vHeights.empty())
1722 return -1; 1667 return -1;
1723 1668
1669 nLastPeerHeightSeen = GetTime();
1724 sort(vHeights.begin(), vHeights.end()); 1670 sort(vHeights.begin(), vHeights.end());
1725 return vHeights[vHeights.size() / 2]; 1671 return vHeights[vHeights.size() / 2];
1726} 1672}
1727 1673
1674// Seconds since any peer last reported a height.
1675//
1676// Before ever hearing one, measure from the first time somebody asked rather
1677// than from the epoch -- otherwise a node refuses jobs the instant it starts,
1678// before it has had a chance to complete a single handshake.
1679int64 GetPeerHeightAgeSecs()
1680{
1681 int64 nNow = GetTime();
1682 if (nPeerHeightFirstAsked == 0)
1683 nPeerHeightFirstAsked = nNow;
1684 if (nLastPeerHeightSeen == 0)
1685 return nNow - nPeerHeightFirstAsked;
1686 return nNow - nLastPeerHeightSeen;
1687}
1688
1728 1689
1729// Say out loud when we are behind the network. 1690// Say out loud when we are behind the network.
1730// 1691//
@@ -1861,7 +1822,21 @@ void ThreadBitcoinMiner(void* parg)
1861// Start the miners. Returns how many actually got off the ground. 1822// Start the miners. Returns how many actually got off the ground.
1862int StartMinerThreads() 1823int StartMinerThreads()
1863{ 1824{
1864 int nThreads = MinerThreadCount(); 1825 // MinerThreadCount() sizes the SOLO miner, where each thread hashes a
1826 // template of its own and more threads is simply more hashrate.
1827 //
1828 // Participant mode is not that shape. Every thread that enters
1829 // BitcoinMiner() takes the MINE_PARTICIPANT branch, and that branch is a
1830 // whole miner: its own Stratum session to the pool, its own payout address
1831 // from the rotation, and its own pool of /hashthreads worker threads. So
1832 // the two counts multiply instead of dividing, and the machine ends up
1833 // running MinerThreadCount() x nHashThreads hashing threads plus one pool
1834 // connection each. On a 12-core host with /hashthreads=2 that is 11
1835 // sessions and 22 hash threads -- roughly 2x the cores, which is not more
1836 // work done, only more contention -- and with /hashthreads unset it is 11
1837 // sessions of 11 workers. One session is the entirety of participant mode;
1838 // /hashthreads is what sizes it, and it can then mean what it says.
1839 int nThreads = (nMineMode == MINE_PARTICIPANT) ? 1 : MinerThreadCount();
1865 int nStarted = 0; 1840 int nStarted = 0;
1866 for (int i = 0; i < nThreads; i++) 1841 for (int i = 0; i < nThreads; i++)
1867 { 1842 {
@@ -1873,9 +1848,24 @@ int StartMinerThreads()
1873 // The dataset is one allocation shared by all of them; only the 2 MB 1848 // The dataset is one allocation shared by all of them; only the 2 MB
1874 // scratchpads multiply. Printed because "why is it using 2 GB" is the first 1849 // scratchpads multiply. Printed because "why is it using 2 GB" is the first
1875 // question anyone asks. 1850 // question anyone asks.
1876 printf("Mining with %d thread(s) of %u core(s) -- about %d MB " 1851 //
1877 "(~2080 MB shared RandomX dataset + ~2 MB per thread)\n", 1852 // Participant mode reports the workers that session will run, not the one
1878 nStarted, std::thread::hardware_concurrency(), 2080 + 2 * nStarted); 1853 // thread that runs it: the hash thread count is the number an operator
1854 // watches, and it is the number that was wrong here. It is otherwise only
1855 // logged under -debug, which is how 11 sessions ran unnoticed.
1856 if (nMineMode == MINE_PARTICIPANT)
1857 {
1858 int nWorkers = ParticipantHashThreadCount();
1859 printf("Mining for the pool with %d hash thread(s) of %u core(s) -- about %d MB "
1860 "(~2080 MB shared RandomX dataset + ~2 MB per thread)\n",
1861 nWorkers, AvailableCoreCount(), 2080 + 2 * nWorkers);
1862 }
1863 else
1864 {
1865 printf("Mining with %d thread(s) of %u core(s) -- about %d MB "
1866 "(~2080 MB shared RandomX dataset + ~2 MB per thread)\n",
1867 nStarted, AvailableCoreCount(), 2080 + 2 * nStarted);
1868 }
1879 return nStarted; 1869 return nStarted;
1880} 1870}
1881 1871
@@ -1950,11 +1940,29 @@ bool StartNode(string& strError)
1950 { 1940 {
1951 int nErr = WSAGetLastError(); 1941 int nErr = WSAGetLastError();
1952 if (nErr == WSAEADDRINUSE) 1942 if (nErr == WSAEADDRINUSE)
1953 strError = strprintf("Error: Unable to bind to port %s on this computer. The program is probably already running.", addrLocalHost.ToString().c_str()); 1943 {
1944 // Another local instance holds the default port. Fall back to an
1945 // ephemeral port: inbound direct-IP peering is optional, and .btf
1946 // rendezvous connectivity is unaffected by the local listen port.
1947 sockaddr.sin_port = htons(0);
1948 if (bind(hListenSocket, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) == SOCKET_ERROR)
1949 {
1950 strError = strprintf("Error: Unable to bind any port on this computer (bind returned error %d)", WSAGetLastError());
1951 printf("%s\n", strError.c_str());
1952 return false;
1953 }
1954 struct sockaddr_in bound;
1955 socklen_t nBoundLen = sizeof(bound);
1956 if (getsockname(hListenSocket, (struct sockaddr*)&bound, &nBoundLen) == 0)
1957 addrLocalHost.port = bound.sin_port;
1958 printf("default port in use by another instance; bound to ephemeral %s instead\n", addrLocalHost.ToString().c_str());
1959 }
1954 else 1960 else
1961 {
1955 strError = strprintf("Error: Unable to bind to port %s on this computer (bind returned error %d)", addrLocalHost.ToString().c_str(), nErr); 1962 strError = strprintf("Error: Unable to bind to port %s on this computer (bind returned error %d)", addrLocalHost.ToString().c_str(), nErr);
1956 printf("%s\n", strError.c_str()); 1963 printf("%s\n", strError.c_str());
1957 return false; 1964 return false;
1965 }
1958 } 1966 }
1959 printf("bound to addrLocalHost = %s\n\n", addrLocalHost.ToString().c_str()); 1967 printf("bound to addrLocalHost = %s\n\n", addrLocalHost.ToString().c_str());
1960 1968
diff --git a/src/net.h b/src/net.h
index feecf96..653a015 100644
--- a/src/net.h
+++ b/src/net.h
@@ -5,7 +5,6 @@
5class CMessageHeader; 5class CMessageHeader;
6class CAddress; 6class CAddress;
7class CInv; 7class CInv;
8class CRequestTracker;
9class CNode; 8class CNode;
10// Defined in main.h, which is included after this file. Only ever used here as 9// Defined in main.h, which is included after this file. Only ever used here as
11// a pointer, so the forward declaration is enough. 10// a pointer, so the forward declaration is enough.
@@ -14,7 +13,6 @@ class CBlockIndex;
14 13
15 14
16static const unsigned short DEFAULT_PORT = htons(8433); 15static const unsigned short DEFAULT_PORT = htons(8433);
17static const unsigned int PUBLISH_HOPS = 5;
18enum 16enum
19{ 17{
20 NODE_NETWORK = (1 << 0), 18 NODE_NETWORK = (1 << 0),
@@ -91,10 +89,9 @@ static const int BTF_HANDSHAKE_GRACE_SECS = 60;
91 89
92// A serialized object cannot be larger than MAX_SIZE, so no valid network 90// A serialized object cannot be larger than MAX_SIZE, so no valid network
93// message needs the old 256 MB header allowance. Keep the wire cap aligned with 91// message needs the old 256 MB header allowance. Keep the wire cap aligned with
94// what the deserializer can accept and bound how long one peer may sit on a 92// what the deserializer can accept. How long a peer may sit on a partial
95// partial payload after a complete header has arrived. 93// payload after a complete header has arrived is BTF_MSG_WAIT_MAX_SECS, below.
96static const unsigned int MAX_PROTOCOL_MESSAGE_SIZE = MAX_SIZE; 94static const unsigned int MAX_PROTOCOL_MESSAGE_SIZE = MAX_SIZE;
97static const int BTF_INCOMPLETE_MESSAGE_TIMEOUT_SECS = 2 * 60;
98 95
99// Ceiling on simultaneous connections. 96// Ceiling on simultaneous connections.
100// 97//
@@ -117,6 +114,42 @@ static const unsigned int MAX_CONNECTIONS = 125;
117// this long regardless of what is still buffered. 114// this long regardless of what is still buffered.
118static const int64 DISCONNECT_DRAIN_SECS = 60; 115static const int64 DISCONNECT_DRAIN_SECS = 60;
119 116
117// How long a pool may go without hearing a height from ANY peer before it
118// stops issuing jobs. Long enough that ordinary peer churn and a quiet
119// handshake window do not trip it; short enough that a deaf pool cannot build
120// more than a few blocks of private fork before it is stopped. Heights
121// 3966-4085 -- 120 blocks, 5890.50 BTF -- were mined in exactly this state.
122static const int64 BTF_NO_PEER_HEIGHT_MAX_SECS = 20 * 60;
123
124// How long a peer may leave this node waiting for the rest of a message it
125// announced in a header.
126//
127// ProcessMessages rewinds the header and breaks when the body has not all
128// arrived, which is correct for a message still in flight and unbounded for
129// one that never completes: the same header is re-read, the loop breaks
130// again, and that peer is never parsed again for the life of the process.
131// Satoshi left the note -- "need a mechanism to give up waiting for overlong
132// message size error" -- and this is it.
133//
134// It never self-corrects, because any byte that arrives refreshes nLastRecv,
135// so the inactivity timeout that would otherwise drop the peer never fires.
136// The socket stays healthy, select() keeps reporting it, and the node goes
137// silently deaf one peer at a time.
138//
139// A header may legitimately outrun its body by a round trip or two; two
140// minutes is far beyond that, and well inside BTF_RECV_TIMEOUT_SECS.
141static const int64 BTF_MSG_WAIT_MAX_SECS = 2 * 60;
142
143// Largest payload a peer may announce in a message header: MAX_PROTOCOL_MESSAGE_SIZE
144// is defined further up this file. This tree reached the same cap by the same
145// reasoning as upstream #109 -- a 256 MB allowance is eight times what the
146// deserializer will accept (serialize.h throws above MAX_SIZE), so the header
147// check admitted an allocation the parser would only reject later, after
148// BTF_MSG_WAIT_MAX_SECS of a stalled peer slot -- so only the note survives.
149//
150// Both definitions were textually clean and 59 lines apart, so the replay onto
151// upstream merged them without a conflict and only the compiler objected.
152
120// Send a ping after this long with nothing to say, so a peer running the 153// Send a ping after this long with nothing to say, so a peer running the
121// inactivity check above does not mistake a quiet node for a dead one. Must 154// inactivity check above does not mistake a quiet node for a dead one. Must
122// stay well under BTF_RECV_TIMEOUT_SECS. 155// stay well under BTF_RECV_TIMEOUT_SECS.
@@ -157,6 +190,7 @@ extern int nBestHeight;
157// has told us anything. Median rather than maximum so one peer claiming an 190// has told us anything. Median rather than maximum so one peer claiming an
158// absurd height cannot move it. 191// absurd height cannot move it.
159int GetPeerMedianHeight(); 192int GetPeerMedianHeight();
193int64 GetPeerHeightAgeSecs();
160 194
161// Lightweight .btf churn accounting. These counters are diagnostics only: they 195// Lightweight .btf churn accounting. These counters are diagnostics only: they
162// let an operator tell stale descriptors, relay pairing failures, and paired 196// let an operator tell stale descriptors, relay pairing failures, and paired
@@ -174,8 +208,6 @@ void BtfChurnNoteHandshakeTimeout(const string& strBtfAddr, const string& strMee
174void BtfPexCollect(std::vector<std::string>& vDescOut); 208void BtfPexCollect(std::vector<std::string>& vDescOut);
175// Verify descriptors a peer sent and remember the good ones. Returns how many. 209// Verify descriptors a peer sent and remember the good ones. Returns how many.
176int BtfPexAccept(const std::vector<std::string>& vDesc); 210int BtfPexAccept(const std::vector<std::string>& vDesc);
177void AbandonRequests(void (*fn)(void*, CDataStream&), void* param1);
178bool AnySubscribed(unsigned int nChannel);
179void ThreadBitcoinMiner(void* parg); 211void ThreadBitcoinMiner(void* parg);
180// Starts MinerThreadCount() miners; returns how many came up. 212// Starts MinerThreadCount() miners; returns how many came up.
181int StartMinerThreads(); 213int StartMinerThreads();
@@ -449,13 +481,19 @@ public:
449 481
450 482
451 483
484// MSG_REVIEW, MSG_PRODUCT and MSG_TABLE no longer have a handler, a producer
485// or a store -- the marketplace they belonged to is gone. They stay in the
486// enum, and their names stay in ppszTypeName below, because CInv serialises
487// `type` as a plain integer: dropping them would renumber nothing today but
488// would make it tempting to renumber later, and MSG_TX and MSG_BLOCK are wire
489// values shared with every other node. Reserved, never emitted, never served.
452enum 490enum
453{ 491{
454 MSG_TX = 1, 492 MSG_TX = 1,
455 MSG_BLOCK, 493 MSG_BLOCK,
456 MSG_REVIEW, 494 MSG_REVIEW, // reserved, dead
457 MSG_PRODUCT, 495 MSG_PRODUCT, // reserved, dead
458 MSG_TABLE, 496 MSG_TABLE, // reserved, dead
459}; 497};
460 498
461static const char* ppszTypeName[] = 499static const char* ppszTypeName[] =
@@ -542,23 +580,9 @@ public:
542 580
543 581
544 582
545class CRequestTracker 583// CRequestTracker and the PushRequest/"reply" round trip lived here. Nothing
546{ 584// in this tree ever called PushRequest, so the table was always empty and the
547public: 585// "reply" handler was pure inbound parser. Both removed.
548 void (*fn)(void*, CDataStream&);
549 void* param1;
550
551 explicit CRequestTracker(void (*fnIn)(void*, CDataStream&)=NULL, void* param1In=NULL)
552 {
553 fn = fnIn;
554 param1 = param1In;
555 }
556
557 bool IsNull()
558 {
559 return fn == NULL;
560 }
561};
562 586
563 587
564 588
@@ -604,9 +628,6 @@ protected:
604public: 628public:
605 int64 nReleaseTime; 629 int64 nReleaseTime;
606 int64 nDisconnectSince; 630 int64 nDisconnectSince;
607 map<uint256, CRequestTracker> mapRequests;
608 CCriticalSection cs_mapRequests;
609
610 // inventory based relay 631 // inventory based relay
611 set<CInv> setInventoryKnown; 632 set<CInv> setInventoryKnown;
612 set<CInv> setInventoryKnown2; 633 set<CInv> setInventoryKnown2;
@@ -614,9 +635,6 @@ public:
614 CCriticalSection cs_inventory; 635 CCriticalSection cs_inventory;
615 multimap<int64, CInv> mapAskFor; 636 multimap<int64, CInv> mapAskFor;
616 637
617 // publish and subscription
618 vector<char> vfSubscribe;
619
620 // Last "btfpeers" we accepted from this node, to rate-limit the exchange. 638 // Last "btfpeers" we accepted from this node, to rate-limit the exchange.
621 int64 nLastPexRecv; 639 int64 nLastPexRecv;
622 640
@@ -634,12 +652,17 @@ public:
634 int64 nLastSend; 652 int64 nLastSend;
635 int64 nLastRecv; 653 int64 nLastRecv;
636 int64 nLastSendEmpty; 654 int64 nLastSendEmpty;
637 int64 nIncompleteMessageStart;
638 unsigned int nIncompleteMessageSize;
639 string strIncompleteMessageCommand;
640 string strBtfAddr; 655 string strBtfAddr;
641 string strBtfMeeting; 656 string strBtfMeeting;
642 657
658 // When this peer's message loop last rewound a header without having made
659 // progress, or 0 when it is not waiting, plus how much was buffered then.
660 // The pair times a STALL rather than a duration, so a large message
661 // arriving slowly is never mistaken for one that is never arriving.
662 // See ProcessMessages.
663 int64 nMsgWaitSince;
664 unsigned int nMsgWaitSize;
665
643 // Height this peer announced in its version message, or -1 if it sent a 666 // Height this peer announced in its version message, or -1 if it sent a
644 // version message that predates the field. Knowing how far along everyone 667 // version message that predates the field. Knowing how far along everyone
645 // else is, is the only way a node can tell "in sync" from "not hearing". 668 // else is, is the only way a node can tell "in sync" from "not hearing".
@@ -669,13 +692,14 @@ public:
669 nLastSend = 0; 692 nLastSend = 0;
670 nLastRecv = 0; 693 nLastRecv = 0;
671 nLastSendEmpty = GetTime(); 694 nLastSendEmpty = GetTime();
672 nIncompleteMessageStart = 0;
673 nIncompleteMessageSize = 0;
674 strIncompleteMessageCommand.clear();
675 strBtfAddr.clear(); 695 strBtfAddr.clear();
676 strBtfMeeting.clear(); 696 strBtfMeeting.clear();
697 // Not waiting on anything yet. Left indeterminate, the first partial
698 // message this peer sends is measured against whatever was in this
699 // memory, and ProcessMessages drops it as unreadable on first contact.
700 nMsgWaitSince = 0;
701 nMsgWaitSize = 0;
677 nStartingHeight = -1; 702 nStartingHeight = -1;
678 vfSubscribe.assign(256, false);
679 703
680 // Push a version message 704 // Push a version message
681 /// when NTP implemented, change to just nTime = GetAdjustedTime() 705 /// when NTP implemented, change to just nTime = GetAdjustedTime()
@@ -703,6 +727,11 @@ public:
703 { 727 {
704 if (hSocket != INVALID_SOCKET) 728 if (hSocket != INVALID_SOCKET)
705 { 729 {
730 // Route through BtfCloseSocket so the accounting sees this close.
731 // A raw closesocket() here was invisible to sockcount, which is
732 // why the leak numbers never added up, and leaving the descriptor
733 // set afterwards is how a number that now belongs to somebody else
734 // gets closed a second time. (#103)
706 BtfCloseSocket(hSocket); 735 BtfCloseSocket(hSocket);
707 hSocket = INVALID_SOCKET; 736 hSocket = INVALID_SOCKET;
708 } 737 }
@@ -949,49 +978,6 @@ public:
949 } 978 }
950 979
951 980
952 void PushRequest(const char* pszCommand,
953 void (*fn)(void*, CDataStream&), void* param1)
954 {
955 uint256 hashReply;
956 RAND_bytes((unsigned char*)&hashReply, sizeof(hashReply));
957
958 CRITICAL_BLOCK(cs_mapRequests)
959 mapRequests[hashReply] = CRequestTracker(fn, param1);
960
961 PushMessage(pszCommand, hashReply);
962 }
963
964 template<typename T1>
965 void PushRequest(const char* pszCommand, const T1& a1,
966 void (*fn)(void*, CDataStream&), void* param1)
967 {
968 uint256 hashReply;
969 RAND_bytes((unsigned char*)&hashReply, sizeof(hashReply));
970
971 CRITICAL_BLOCK(cs_mapRequests)
972 mapRequests[hashReply] = CRequestTracker(fn, param1);
973
974 PushMessage(pszCommand, hashReply, a1);
975 }
976
977 template<typename T1, typename T2>
978 void PushRequest(const char* pszCommand, const T1& a1, const T2& a2,
979 void (*fn)(void*, CDataStream&), void* param1)
980 {
981 uint256 hashReply;
982 RAND_bytes((unsigned char*)&hashReply, sizeof(hashReply));
983
984 CRITICAL_BLOCK(cs_mapRequests)
985 mapRequests[hashReply] = CRequestTracker(fn, param1);
986
987 PushMessage(pszCommand, hashReply, a1, a2);
988 }
989
990
991
992 bool IsSubscribed(unsigned int nChannel);
993 void Subscribe(unsigned int nChannel, unsigned int nHops=0);
994 void CancelSubscribe(unsigned int nChannel);
995 void Disconnect(); 981 void Disconnect();
996}; 982};
997 983
@@ -1048,50 +1034,8 @@ inline void RelayMessage<>(const CInv& inv, const CDataStream& ss)
1048 1034
1049 1035
1050 1036
1051// 1037// The publish/subscribe advert templates that used to close this file
1052// Templates for the publish and subscription system. 1038// (AdvertStartPublish / AdvertStopPublish / AdvertRemoveSource) are gone along
1053// The object being published as T& obj needs to have: 1039// with the marketplace objects they carried. Nothing ever handled an inbound
1054// a set<unsigned int> setSources member 1040// "publish" or "pub-cancel", so the machinery only ever pushed; what it pushed
1055// specializations of AdvertInsert and AdvertErase 1041// was CProduct, which no longer exists.
1056// Currently implemented for CTable and CProduct.
1057//
1058
1059template<typename T>
1060void AdvertStartPublish(CNode* pfrom, unsigned int nChannel, unsigned int nHops, T& obj)
1061{
1062 // Add to sources
1063 obj.setSources.insert(pfrom->addr.ip);
1064
1065 if (!AdvertInsert(obj))
1066 return;
1067
1068 // Relay
1069 CRITICAL_BLOCK(cs_vNodes)
1070 foreach(CNode* pnode, vNodes)
1071 if (pnode != pfrom && (nHops < PUBLISH_HOPS || pnode->IsSubscribed(nChannel)))
1072 pnode->PushMessage("publish", nChannel, nHops, obj);
1073}
1074
1075template<typename T>
1076void AdvertStopPublish(CNode* pfrom, unsigned int nChannel, unsigned int nHops, T& obj)
1077{
1078 uint256 hash = obj.GetHash();
1079
1080 CRITICAL_BLOCK(cs_vNodes)
1081 foreach(CNode* pnode, vNodes)
1082 if (pnode != pfrom && (nHops < PUBLISH_HOPS || pnode->IsSubscribed(nChannel)))
1083 pnode->PushMessage("pub-cancel", nChannel, nHops, hash);
1084
1085 AdvertErase(obj);
1086}
1087
1088template<typename T>
1089void AdvertRemoveSource(CNode* pfrom, unsigned int nChannel, unsigned int nHops, T& obj)
1090{
1091 // Remove a source
1092 obj.setSources.erase(pfrom->addr.ip);
1093
1094 // If no longer supported by any sources, cancel it
1095 if (obj.setSources.empty())
1096 AdvertStopPublish(pfrom, nChannel, nHops, obj);
1097}
diff --git a/src/nostr.cpp b/src/nostr.cpp
index d286767..32cea18 100644
--- a/src/nostr.cpp
+++ b/src/nostr.cpp
@@ -6,6 +6,7 @@
6#include "headers.h" 6#include "headers.h"
7#include <openssl/ssl.h> 7#include <openssl/ssl.h>
8#include <openssl/err.h> 8#include <openssl/err.h>
9#include <openssl/x509.h>
9#include <openssl/x509v3.h> 10#include <openssl/x509v3.h>
10#ifdef _WIN32 11#ifdef _WIN32
11#include <wincrypt.h> 12#include <wincrypt.h>
@@ -82,9 +83,20 @@ static CCriticalSection cs_btfPeers;
82// independent relays. Kept only one Cloudflare-fronted relay (damus.io, the 83// independent relays. Kept only one Cloudflare-fronted relay (damus.io, the
83// most established) and filled the rest with relays on different hosts 84// most established) and filled the rest with relays on different hosts
84// (Hetzner, various independents) so no single upstream provider is a 85// (Hetzner, various independents) so no single upstream provider is a
85// single point of failure for peer discovery. 7 relays lands inside the 86// single point of failure for peer discovery. 8 relays lands inside the
86// commonly recommended 5-10 range for redundancy without keeping an 87// commonly recommended 5-10 range for redundancy without keeping an
87// excessive number of persistent sockets open per node. 88// excessive number of persistent sockets open per node.
89//
90// Four entries were removed after probing all twelve from two independent
91// networks (a residential line and the pool's VPS, over both v4 and v6):
92// relay.current.fyi, relay.nostr.bg -- no A record, and the AAAA for both
93// lands on the same parked host (2a04:c300:1000::381) serving a
94// certificate that fails verification. The domains are gone.
95// relay.nostr.band, nostr.fmt.wiz.biz -- TCP connects, TLS handshake never
96// completes, from both networks. Whether they are down or refusing
97// non-browser clients, this node cannot use them.
98// A dead relay is not free: each one holds a connect/retry slot and delays
99// discovery, so carrying it costs latency for no redundancy.
88const char* pszNostrRelays[] = { 100const char* pszNostrRelays[] = {
89 "wss://relay.damus.io", 101 "wss://relay.damus.io",
90 "wss://nos.lol", 102 "wss://nos.lol",
@@ -97,6 +109,8 @@ const char* pszNostrRelays[] = {
97 "wss://offchain.pub", 109 "wss://offchain.pub",
98 "wss://nostr.mom", 110 "wss://nostr.mom",
99 "wss://relay.primal.net", 111 "wss://relay.primal.net",
112 "wss://nostr.oxtr.dev",
113 "wss://relay.nos.social",
100}; 114};
101const int nNostrRelays = ARRAYLEN(pszNostrRelays); 115const int nNostrRelays = ARRAYLEN(pszNostrRelays);
102 116
@@ -110,6 +124,12 @@ vector<string> vBtfMeetingRelays = {
110 "31.44.4.249:8434", // New Jersey, US 124 "31.44.4.249:8434", // New Jersey, US
111 "90.156.222.107:8434", // Almaty, KZ 125 "90.156.222.107:8434", // Almaty, KZ
112}; 126};
127// True when the operator pinned the relay list with /rvrelay: discovered
128// relays must NOT be merged in (BtfAllRelays), otherwise one accept thread
129// per discovered relay is spawned and each one optimistically overwrites the
130// advertised meeting node (BtfSetActiveRelay) before registering -- a
131// discovered but unreachable relay then flaps the published .btf descriptor.
132bool fBtfRelayPinned = false;
113string strBtfActiveRelay; 133string strBtfActiveRelay;
114static CCriticalSection cs_activeRelay; 134static CCriticalSection cs_activeRelay;
115 135
@@ -144,7 +164,7 @@ static map<string, BtfPoolAnnouncement> g_discoveredPools;
144static CCriticalSection cs_discoveredPools; 164static CCriticalSection cs_discoveredPools;
145 165
146// Basic sanity: "host:port" with a numeric 1..65535 port and a plausible host. 166// Basic sanity: "host:port" with a numeric 1..65535 port and a plausible host.
147static bool LooksLikeRelay(const string& r) 167bool LooksLikeRelay(const string& r)
148{ 168{
149 size_t colon = r.rfind(':'); 169 size_t colon = r.rfind(':');
150 if (colon == string::npos || colon == 0 || colon + 1 >= r.size()) 170 if (colon == string::npos || colon == 0 || colon + 1 >= r.size())
@@ -172,6 +192,8 @@ static bool LooksLikeRelay(const string& r)
172vector<string> BtfAllRelays() 192vector<string> BtfAllRelays()
173{ 193{
174 vector<string> all = vBtfMeetingRelays; 194 vector<string> all = vBtfMeetingRelays;
195 if (fBtfRelayPinned)
196 return all;
175 CRITICAL_BLOCK(cs_discoveredRelays) 197 CRITICAL_BLOCK(cs_discoveredRelays)
176 foreach(const string& r, g_discoveredRelays) 198 foreach(const string& r, g_discoveredRelays)
177 if (find(all.begin(), all.end(), r) == all.end()) 199 if (find(all.begin(), all.end(), r) == all.end())
@@ -550,26 +572,52 @@ public:
550 { 572 {
551 sslctx = SSL_CTX_new(TLS_client_method()); 573 sslctx = SSL_CTX_new(TLS_client_method());
552 if (!sslctx) return false; 574 if (!sslctx) return false;
575
576 // SSL_CTX_new defaults to SSL_VERIFY_NONE with no trust store
577 // loaded, so until now every wss:// relay connection was accepted
578 // without looking at the certificate at all -- any on-path
579 // attacker could impersonate a relay. Our own events are signed,
580 // so a MITM cannot forge them, but it could drop the rendezvous
581 // registration (leaving the pool unreachable to miners) or answer
582 // subscriptions with whatever it liked.
553 SSL_CTX_set_verify(sslctx, SSL_VERIFY_PEER, NULL); 583 SSL_CTX_set_verify(sslctx, SSL_VERIFY_PEER, NULL);
584
585 // OpenSSL's default verify paths are usually empty on Windows, so
586 // the system ROOT store is loaded too -- without it verification
587 // fails closed there and every relay becomes unreachable.
554 bool fTrustLoaded = (SSL_CTX_set_default_verify_paths(sslctx) == 1); 588 bool fTrustLoaded = (SSL_CTX_set_default_verify_paths(sslctx) == 1);
555#ifdef _WIN32 589#ifdef _WIN32
556 fTrustLoaded = BtfLoadWindowsRootCertificates(sslctx) || fTrustLoaded; 590 fTrustLoaded = BtfLoadWindowsRootCertificates(sslctx) || fTrustLoaded;
557#endif 591#endif
558 if (!fTrustLoaded) 592 if (!fTrustLoaded)
559 return error("Nostr: failed to load TLS trust store"); 593 return error("Nostr: failed to load TLS trust store");
594
595 // Floor the protocol version. Verification says who the peer is,
596 // not how weak a cipher suite we will agree to; without this a
597 // relay offering nothing better still gets TLS 1.0.
598 SSL_CTX_set_min_proto_version(sslctx, TLS1_2_VERSION);
599
560 ssl = SSL_new(sslctx); 600 ssl = SSL_new(sslctx);
561 if (!ssl) return false; 601 if (!ssl) return false;
562 SSL_set_fd(ssl, (int)hSocket); 602 SSL_set_fd(ssl, (int)hSocket);
563 if (SSL_set_tlsext_host_name(ssl, host.c_str()) != 1) 603 if (SSL_set_tlsext_host_name(ssl, host.c_str()) != 1) // SNI
564 return error("Nostr: failed to set TLS SNI for %s", host.c_str()); 604 return error("Nostr: failed to set TLS SNI for %s", host.c_str());
605
606 // Verifying the chain is not enough on its own: without this the
607 // relay could present any certificate a trusted CA ever issued,
608 // for any name. SSL_set1_host binds the check to the host we asked
609 // for.
565 if (SSL_set1_host(ssl, host.c_str()) != 1) 610 if (SSL_set1_host(ssl, host.c_str()) != 1)
566 return error("Nostr: failed to set TLS hostname for %s", host.c_str()); 611 return error("Nostr: failed to set TLS hostname for %s", host.c_str());
612
567 if (SSL_connect(ssl) != 1) 613 if (SSL_connect(ssl) != 1)
614 {
615 long nVerify = SSL_get_verify_result(ssl);
616 if (nVerify != X509_V_OK)
617 return error("Nostr: certificate for %s rejected: %s",
618 host.c_str(), X509_verify_cert_error_string(nVerify));
568 return error("Nostr: TLS handshake with %s failed", host.c_str()); 619 return error("Nostr: TLS handshake with %s failed", host.c_str());
569 long nVerify = SSL_get_verify_result(ssl); 620 }
570 if (nVerify != X509_V_OK)
571 return error("Nostr: TLS certificate verification failed for %s: %s",
572 host.c_str(), X509_verify_cert_error_string(nVerify));
573 } 621 }
574 622
575 // WebSocket handshake (HTTP Upgrade) 623 // WebSocket handshake (HTTP Upgrade)
@@ -1017,9 +1065,14 @@ static void HandlePoolAnnouncement(const json& ev)
1017 1065
1018// Resolve a `.btf` address via a relay: fetch the descriptor published by that 1066// Resolve a `.btf` address via a relay: fetch the descriptor published by that
1019// address's key and verify it self-certifies under the decoded pubkey. 1067// address's key and verify it self-certifies under the decoded pubkey.
1068// fExchangeOk reports whether the relay actually finished the conversation
1069// (an EOSE arrived) as opposed to the socket dying mid-read. The caller pools
1070// this socket for reuse, and only a completed exchange leaves it in a state
1071// the next caller can trust.
1020static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, 1072static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1021 btf::Descriptor& out) 1073 btf::Descriptor& out, bool& fExchangeOk)
1022{ 1074{
1075 fExchangeOk = false;
1023 unsigned char pk[32]; 1076 unsigned char pk[32];
1024 if (!btf::ParseAddress(btfAddr, pk)) 1077 if (!btf::ParseAddress(btfAddr, pk))
1025 return false; 1078 return false;
@@ -1031,26 +1084,42 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1031 json req = json::array({ "REQ", "btf-resolve", filter }); 1084 json req = json::array({ "REQ", "btf-resolve", filter });
1032 if (!ws.SendText(req.dump())) 1085 if (!ws.SendText(req.dump()))
1033 return false; 1086 return false;
1087 bool fFound = false;
1034 for (;;) 1088 for (;;)
1035 { 1089 {
1036 string msg; 1090 string msg;
1037 if (!ws.RecvText(msg)) 1091 if (!ws.RecvText(msg))
1038 break; 1092 return fFound; // socket died mid-exchange; fExchangeOk stays false
1039 json j; 1093 json j;
1040 try { j = json::parse(msg); } catch (...) { continue; } 1094 try { j = json::parse(msg); } catch (...) { continue; }
1041 if (!j.is_array() || j.empty() || !j[0].is_string()) continue; 1095 if (!j.is_array() || j.empty() || !j[0].is_string()) continue;
1042 string t = j[0].get<string>(); 1096 string t = j[0].get<string>();
1097 // Every relay message names the subscription it belongs to. This socket
1098 // is pooled and reused, so a message left over from an earlier exchange
1099 // is the ordinary case here, not an exotic one -- and an unmatched EOSE
1100 // ends this query before it has begun, which is indistinguishable from
1101 // "that descriptor does not exist". Check the id.
1102 if (j.size() >= 2 && j[1].is_string() && j[1].get<string>() != "btf-resolve")
1103 continue;
1043 if (t == "EVENT" && j.size() >= 3) 1104 if (t == "EVENT" && j.size() >= 3)
1044 { 1105 {
1045 const json& ev = j[2]; 1106 const json& ev = j[2];
1046 if (ev.contains("content") && 1107 if (!fFound && ev.contains("content") &&
1047 btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out)) 1108 btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out))
1048 return true; 1109 fFound = true;
1110 // Deliberately keep reading to EOSE rather than returning here.
1111 // Returning early leaves the rest of the relay's answer queued on a
1112 // socket that goes straight back into the pool, and the next caller
1113 // reads our leftovers as its own -- one message out of step, for
1114 // the life of the process.
1049 } 1115 }
1050 else if (t == "EOSE") 1116 else if (t == "EOSE")
1117 {
1118 fExchangeOk = true; // relay said "that is all": socket is clean
1051 break; 1119 break;
1120 }
1052 } 1121 }
1053 return false; 1122 return fFound;
1054} 1123}
1055 1124
1056// Resolve a `.btf` address into its rendezvous coordinates by querying the 1125// Resolve a `.btf` address into its rendezvous coordinates by querying the
@@ -1069,12 +1138,22 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort,
1069 if (!lease.Connected()) 1138 if (!lease.Connected())
1070 continue; 1139 continue;
1071 btf::Descriptor d; 1140 btf::Descriptor d;
1072 if (!ResolveDescriptor(lease.Ws(), g_nostrKey.ctx, btfAddr, d)) 1141 bool fExchangeOk = false;
1142 if (!ResolveDescriptor(lease.Ws(), g_nostrKey.ctx, btfAddr, d, fExchangeOk))
1073 { 1143 {
1074 lease.fOk = true; // exchange completed, just no match 1144 // Only keep the pooled socket if the relay actually finished
1145 // the conversation. This used to say "exchange completed, just
1146 // no match" on every failure -- including a dead socket -- so
1147 // ~CRelayLease never closed it and fConnected was never
1148 // cleared. A relay connection that died was then pinned in the
1149 // pool, reported healthy, and reused for every later lookup,
1150 // which is why "no Nostr descriptor found" persisted for a
1151 // descriptor that was demonstrably published, and why only a
1152 // restart cleared it.
1153 lease.fOk = fExchangeOk;
1075 continue; 1154 continue;
1076 } 1155 }
1077 lease.fOk = true; 1156 lease.fOk = fExchangeOk;
1078 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") 1157 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending")
1079 continue; // descriptor predates the owner's relay config 1158 continue; // descriptor predates the owner's relay config
1080 if (!HexDecode(d.enc, enc_pub, 32)) 1159 if (!HexDecode(d.enc, enc_pub, 32))
@@ -1404,7 +1483,7 @@ void ThreadBtfPoolAnnouncer(void*)
1404 LogPrint("pool", "ThreadBtfPoolAnnouncer started\n"); 1483 LogPrint("pool", "ThreadBtfPoolAnnouncer started\n");
1405 while (!fShutdown) 1484 while (!fShutdown)
1406 { 1485 {
1407 if (nMineMode == MINE_OPERATOR && gPoolRunning) 1486 if (nMineMode == MINE_OPERATOR && gPoolRunning && !fPoolNoAnnounce)
1408 { 1487 {
1409 BtfPoolAnnouncement ann; 1488 BtfPoolAnnouncement ann;
1410 ann.btfAddress = BtfLocalAddress(); 1489 ann.btfAddress = BtfLocalAddress();
@@ -1736,14 +1815,17 @@ void ThreadNostrSeed(void* parg)
1736 PublishDescriptor(ws, key); 1815 PublishDescriptor(ws, key);
1737 Sleep(700); // let the relay store the replaceable event 1816 Sleep(700); // let the relay store the replaceable event
1738 btf::Descriptor d; 1817 btf::Descriptor d;
1739 if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d)) 1818 bool fExchangeOk = false;
1819 if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d, fExchangeOk))
1740 { 1820 {
1741 LogPrint("nostr", "Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n", 1821 LogPrint("nostr", "Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n",
1742 pszNostrRelays[i], d.meeting_node.c_str(), d.enc.substr(0, 16).c_str()); 1822 pszNostrRelays[i], d.meeting_node.c_str(), d.enc.substr(0, 16).c_str());
1743 lease.fOk = true; 1823 lease.fOk = fExchangeOk;
1744 break; 1824 break;
1745 } 1825 }
1746 lease.fOk = true; 1826 // Same rule as BtfResolve: a socket only goes back in the pool if
1827 // the relay finished the conversation on it.
1828 lease.fOk = fExchangeOk;
1747 } 1829 }
1748 CATCH_PRINT_EXCEPTION("btf self-test") 1830 CATCH_PRINT_EXCEPTION("btf self-test")
1749 } 1831 }
@@ -1775,7 +1857,7 @@ void ThreadNostrSeed(void* parg)
1775 } 1857 }
1776 CATCH_PRINT_EXCEPTION("ConnectDiscoveredBtfPeers") 1858 CATCH_PRINT_EXCEPTION("ConnectDiscoveredBtfPeers")
1777 1859
1778 if (nMineMode == MINE_OPERATOR && gPoolRunning) 1860 if (nMineMode == MINE_OPERATOR && gPoolRunning && !fPoolNoAnnounce)
1779 { 1861 {
1780 BtfPoolAnnouncement ann; 1862 BtfPoolAnnouncement ann;
1781 ann.btfAddress = BtfLocalAddress(); 1863 ann.btfAddress = BtfLocalAddress();
diff --git a/src/nostr.h b/src/nostr.h
index b5eb5e3..827b2f3 100644
--- a/src/nostr.h
+++ b/src/nostr.h
@@ -43,6 +43,14 @@ void BtfSetActiveRelay(const std::string& relay);
43// If set (via /announcerelay=host:port), this node announces that relay on 43// If set (via /announcerelay=host:port), this node announces that relay on
44// Nostr so other nodes discover it automatically -- no manual seed-list edit. 44// Nostr so other nodes discover it automatically -- no manual seed-list edit.
45extern std::string strBtfAnnounceRelay; 45extern std::string strBtfAnnounceRelay;
46// True when /rvrelay pinned the meeting relay list: BtfAllRelays then returns
47// ONLY vBtfMeetingRelays, never Nostr-discovered volunteer relays. Without
48// this, a pool that pins one public relay still spawns an accept thread per
49// discovered relay, and each thread's optimistic BtfSetActiveRelay makes the
50// published descriptor flap to relays the pool may not be reachable at.
51extern bool fBtfRelayPinned;
52// Basic sanity check on a "host:port" relay address (numeric port 1..65535).
53bool LooksLikeRelay(const std::string& r);
46// Every relay to try: curated seeds + relays discovered from Nostr announcements. 54// Every relay to try: curated seeds + relays discovered from Nostr announcements.
47std::vector<std::string> BtfAllRelays(); 55std::vector<std::string> BtfAllRelays();
48 56
diff --git a/src/rpc.cpp b/src/rpc.cpp
index 813f1d1..8af2fbe 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//
@@ -143,6 +143,75 @@ static bool RebuildJob()
143 return false; 143 return false;
144 } 144 }
145 145
146 // Don't issue jobs while we are behind the network: mining against a
147 // stale tip produces blocks the network will orphan, and payout
148 // entries that can never be paid (their coinbases never exist on the
149 // main chain). Require at least one peer, and require that no peer
150 // claims a longer chain than ours.
151 if (vNodes.empty()) {
152 LogPrint("pool", "RebuildJob: no peers yet -- not issuing jobs\n");
153 return false;
154 }
155 // This used to compare the tip's timestamp against 30 minutes, which
156 // was wrong in both directions and cost real money.
157 //
158 // It cannot tell "we are not synced" from "the network is slow". A
159 // pool mining its own fork keeps its tip timestamp fresh forever, so
160 // the check passed for the whole of heights 3966-4085 -- 120
161 // consecutive blocks, every one of them orphaned, 5890.50 BTF of
162 // payouts voided. Then, once the pool stopped finding blocks, the
163 // same check refused to issue jobs against a tip that was perfectly
164 // valid and was the network's own best, and the pool -- roughly 90%
165 // of the hashrate -- could not restart the chain it was the majority
166 // of. Stale tip, so no jobs; no jobs, so the tip stays stale.
167 //
168 // The height peers announce in their version message answers the
169 // question the age check was only guessing at. Median, so one peer
170 // claiming an absurd height cannot stall us, and one block of slack
171 // to match WarnIfBehind, because somebody is always mid-relay. A
172 // peer that predates the announcement reports -1 and is ignored; if
173 // nobody has told us anything we mine our best chain, which is what
174 // the node would do without a pool attached.
175 int nPeerHeight = GetPeerMedianHeight();
176 if (nPeerHeight >= 0 && nBestHeight < nPeerHeight - 1) {
177 LogPrint("pool", "RebuildJob: %d blocks behind the network "
178 "(height %d, peers report %d) -- not issuing jobs\n",
179 nPeerHeight - nBestHeight, nBestHeight, nPeerHeight);
180 return false;
181 }
182
183 // The comparison above is blind in the one case that matters most.
184 //
185 // When the node goes deaf -- peers still in vNodes, nothing arriving --
186 // no peer announces anything, GetPeerMedianHeight() returns -1, and the
187 // fallback ("nobody has told us anything, so mine our best chain") let
188 // the pool build a private fork for as long as the deafness lasted.
189 // That fallback is right for a node that has simply not been told yet
190 // and wrong for a node that has stopped being able to hear, and a
191 // height nobody sent cannot tell those apart. Time can.
192 //
193 // Deliberately not behind a debug category, and deliberately not a
194 // silent refusal: an operator whose pool has stopped issuing jobs needs
195 // the reason in the log. /poolmineblind is the escape hatch for a node
196 // that really is alone -- without one, a genuinely isolated pool that is
197 // the majority of the hashrate could never restart its own chain, which
198 // is the deadlock the tip-age check used to cause.
199 if (nPeerHeight < 0 && !fPoolMineBlind) {
200 int64 nAge = GetPeerHeightAgeSecs();
201 if (nAge >= BTF_NO_PEER_HEIGHT_MAX_SECS) {
202 static int64 nLastSaid = 0;
203 if (GetTime() - nLastSaid > 60) {
204 nLastSaid = GetTime();
205 printf("RebuildJob: no peer has reported a height for %d minutes "
206 "-- not issuing jobs (our height %d, %d peers connected). "
207 "This node is deaf; mining now would fork it. "
208 "Pass /poolmineblind to override.\n",
209 (int)(nAge / 60), nBestHeight, (int)vNodes.size());
210 }
211 return false;
212 }
213 }
214
146 CBlockIndex* pindexPrev = pindexBest; 215 CBlockIndex* pindexPrev = pindexBest;
147 unsigned int nBits = GetNextWorkRequired(pindexPrev); 216 unsigned int nBits = GetNextWorkRequired(pindexPrev);
148 217
@@ -194,7 +263,15 @@ static bool RebuildJob()
194 vector<char> vfAlreadyAdded(mapTransactions.size()); 263 vector<char> vfAlreadyAdded(mapTransactions.size());
195 bool fFoundSomething = true; 264 bool fFoundSomething = true;
196 unsigned int sz = 0; 265 unsigned int sz = 0;
197 while (fFoundSomething && sz < MAX_SIZE / 2) 266 // The cap is MAX_BLOCK_SIZE/2, not MAX_SIZE/2. MAX_SIZE is the
267 // 32 MB serialisation ceiling, so half of it is 16 MB -- this
268 // builder would happily assemble a template that its own
269 // CheckBlock() rejects at the 1 MB consensus cap. This is the
270 // pool's job builder, so a block over the cap is not one
271 // wasted solve, it is every miner on the pool working a
272 // template the network will reject. The inner break bounds the
273 // overshoot to a single transaction within a pass.
274 while (fFoundSomething && sz < MAX_BLOCK_SIZE / 2)
198 { 275 {
199 fFoundSomething = false; 276 fFoundSomething = false;
200 unsigned int n = 0; 277 unsigned int n = 0;
@@ -214,6 +291,7 @@ static bool RebuildJob()
214 sz += ::GetSerializeSize(tx, SER_NETWORK); 291 sz += ::GetSerializeSize(tx, SER_NETWORK);
215 vfAlreadyAdded[n] = true; 292 vfAlreadyAdded[n] = true;
216 fFoundSomething = true; 293 fFoundSomething = true;
294 if (sz > MAX_BLOCK_SIZE / 2) break;
217 } 295 }
218 } 296 }
219 } 297 }
@@ -251,14 +329,34 @@ static bool RebuildJob()
251// Payouts -- event loop thread only 329// Payouts -- event loop thread only
252// --------------------------------------------------------------------------- 330// ---------------------------------------------------------------------------
253struct PendingPayout { 331struct PendingPayout {
254 std::string id; 332 // The block that funds this payout, identified by hash and not by height
255 int blockHeight; 333 // alone. A reorg replaces the block at a height without changing the
256 int matureAtHeight; 334 // height, so a height-keyed obligation outlives the coinbase that was
335 // supposed to pay it: a 280-block reorg once left ~14.4k BTF of debt
336 // owed against blocks that no longer existed, which then drained the
337 // wallet paying fork-era miners out of real-chain coin.
338 //
339 // Upstream keys these on the string "block-<height>". That is the same
340 // bug this fork already paid for, and height keying additionally lets
341 // FindRecordedPoolPayout match the *previous* round's transaction after a
342 // reorg and silently skip a miner, so their `id` is deliberately dropped.
343 int blockHeight = 0;
344 // Must be explicitly zeroed: uint256's default constructor is empty
345 // (uint256.h:514), so leaving this unassigned gives every ledger entry
346 // loaded from an older file a garbage hash, which then matches no block
347 // and gets voided as "orphaned". That mistake voided a live payout.
348 uint256 blockHash = 0; // zero = entry predates hash keying
349 int matureAtHeight = 0;
257 std::map<std::string, int64> amounts; // address -> satoshis 350 std::map<std::string, int64> amounts; // address -> satoshis
351 // From upstream: which recipients already have a transaction. This is what
352 // lets an entry stay on disk until every address in `amounts` is paid,
353 // instead of being erased the moment the batch is handed off.
258 std::map<std::string, std::string> paidTxids; // address -> txid 354 std::map<std::string, std::string> paidTxids; // address -> txid
259 int64 nextAttempt; 355 int retries = 0; // exec-thread only: SendMoney attempts so far
260 356 int deferrals = 0; // exec-thread only: waits for spendable coin
261 PendingPayout() : blockHeight(0), matureAtHeight(0), nextAttempt(0) {} 357 // Upstream's `nextAttempt` is this field under another name; ours is kept
358 // so the retry/deferral policy above stays coherent with it.
359 int64 retryAfter = 0; // exec-thread only: unix time, 0 = ready now
262}; 360};
263static std::vector<PendingPayout> gPendingPayouts; 361static std::vector<PendingPayout> gPendingPayouts;
264static std::mutex gPayoutLedgerMutex; 362static std::mutex gPayoutLedgerMutex;
@@ -304,9 +402,25 @@ static std::string PoolRoundsFilePath()
304 return GetAppDir() + "/pool_rounds.json"; 402 return GetAppDir() + "/pool_rounds.json";
305} 403}
306 404
307static std::string PayoutIdForBlock(int blockHeight) 405// Identity of a payout round. Used as the pool-round proof id and, more
406// importantly, as the `pool_payout_id` written into wallet transaction
407// metadata and matched by FindRecordedPoolPayout().
408//
409// Derived from the block HASH, not from its height alone. Upstream uses
410// "block-<height>", which names two different blocks identically across a
411// reorg -- and because this id is what the crash-recovery probe matches on, a
412// height-keyed id lets that probe find the *previous* round's transaction and
413// conclude a miner has already been paid when they have not. The height stays
414// in the string for readability only.
415//
416// A zero hash means the entry predates hash keying; those keep the old form so
417// anything already recorded against them still matches.
418static std::string PayoutIdForBlock(int blockHeight, const uint256& blockHash)
308{ 419{
309 return strprintf("block-%d", blockHeight); 420 if (blockHash == 0)
421 return strprintf("block-%d", blockHeight);
422 return strprintf("block-%d-%s", blockHeight,
423 blockHash.ToString().substr(0, 16).c_str());
310} 424}
311 425
312static bool PayoutFullyPaid(const PendingPayout& pp) 426static bool PayoutFullyPaid(const PendingPayout& pp)
@@ -317,15 +431,22 @@ static bool PayoutFullyPaid(const PendingPayout& pp)
317 return true; 431 return true;
318} 432}
319 433
320static void SavePendingPayoutsLocked() 434// Serialise the ledger. Caller must hold gPayoutLedgerMutex.
435//
436// Split from the file write so the payout thread can hold the mutex just long
437// enough to read the structure and then do the I/O with it released. Upstream
438// writes the file inside the per-recipient loop with gPayoutLedgerMutex held,
439// while the event loop's FlushMaturePayouts blocks on that same mutex -- one
440// slow fsync per recipient, on the thread the miners' I/O shares.
441static std::string SerializePendingPayoutsLocked()
321{ 442{
322 nlohmann::json arr = nlohmann::json::array(); 443 nlohmann::json arr = nlohmann::json::array();
323 for (const auto& pp : gPendingPayouts) { 444 for (const auto& pp : gPendingPayouts) {
324 nlohmann::json obj; 445 nlohmann::json obj;
325 obj["id"] = pp.id; 446 obj["blockHeight"] = pp.blockHeight;
326 obj["blockHeight"] = pp.blockHeight; 447 obj["blockHash"] = pp.blockHash.ToString();
327 obj["matureAtHeight"] = pp.matureAtHeight; 448 obj["matureAtHeight"] = pp.matureAtHeight;
328 obj["nextAttempt"] = pp.nextAttempt; 449 obj["retryAfter"] = pp.retryAfter;
329 nlohmann::json amounts = nlohmann::json::object(); 450 nlohmann::json amounts = nlohmann::json::object();
330 for (const auto& kv : pp.amounts) 451 for (const auto& kv : pp.amounts)
331 amounts[kv.first] = kv.second; 452 amounts[kv.first] = kv.second;
@@ -336,24 +457,38 @@ static void SavePendingPayoutsLocked()
336 obj["paidTxids"] = paid; 457 obj["paidTxids"] = paid;
337 arr.push_back(obj); 458 arr.push_back(obj);
338 } 459 }
460 return arr.dump(2);
461}
462
463// Write the serialised ledger. Holds no lock; safe to call with the mutex
464// released. Atomic tmp+rename so a crash mid-write cannot truncate the record
465// of who is still owed money.
466static void WritePendingPayoutsFile(const std::string& s)
467{
339 std::string path = PayoutFilePath(); 468 std::string path = PayoutFilePath();
340 std::string tmp = path + ".tmp"; 469 std::string tmp = path + ".tmp";
341 FILE* f = fopen(tmp.c_str(), "w"); 470 FILE* f = fopen(tmp.c_str(), "w");
342 if (f) { 471 if (!f) {
343 std::string s = arr.dump(2);
344 fwrite(s.c_str(), 1, s.size(), f);
345 if (fclose(f) != 0) {
346 LogPrint("payout", "[payout] WARNING: could not close %s\n", tmp.c_str());
347 return;
348 }
349#ifdef _WIN32
350 remove(path.c_str());
351#endif
352 if (rename(tmp.c_str(), path.c_str()) != 0)
353 LogPrint("payout", "[payout] WARNING: could not replace %s\n", path.c_str());
354 } else {
355 LogPrint("payout", "[payout] WARNING: could not write %s\n", tmp.c_str()); 472 LogPrint("payout", "[payout] WARNING: could not write %s\n", tmp.c_str());
473 return;
474 }
475 fwrite(s.c_str(), 1, s.size(), f);
476 if (fclose(f) != 0) {
477 LogPrint("payout", "[payout] WARNING: could not close %s\n", tmp.c_str());
478 return;
356 } 479 }
480#ifdef _WIN32
481 remove(path.c_str());
482#endif
483 if (rename(tmp.c_str(), path.c_str()) != 0)
484 LogPrint("payout", "[payout] WARNING: could not replace %s\n", path.c_str());
485}
486
487// Convenience for the cold paths (startup, queueing) where holding the mutex
488// across the write costs nothing. Caller must hold gPayoutLedgerMutex.
489static void SavePendingPayoutsLocked()
490{
491 WritePendingPayoutsFile(SerializePendingPayoutsLocked());
357} 492}
358 493
359static void LoadPendingPayouts() 494static void LoadPendingPayouts()
@@ -372,17 +507,27 @@ static void LoadPendingPayouts()
372 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); 507 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
373 for (const auto& obj : arr) { 508 for (const auto& obj : arr) {
374 PendingPayout pp; 509 PendingPayout pp;
375 if (obj.contains("blockHeight"))
376 pp.blockHeight = obj["blockHeight"].get<int>();
377 pp.matureAtHeight = obj["matureAtHeight"].get<int>(); 510 pp.matureAtHeight = obj["matureAtHeight"].get<int>();
378 if (obj.contains("id") && obj["id"].is_string()) 511 // Entries written before payouts were keyed by hash carry neither
379 pp.id = obj["id"].get<std::string>(); 512 // field. Leave blockHash zero and they are grandfathered past the
380 if (pp.blockHeight == 0) 513 // orphan check rather than voided on a guess.
514 if (obj.contains("blockHash") && obj["blockHash"].is_string())
515 pp.blockHash.SetHex(obj["blockHash"].get<std::string>());
516 if (obj.contains("blockHeight") && obj["blockHeight"].is_number())
517 pp.blockHeight = obj["blockHeight"].get<int>();
518 // Upstream's tolerance for an entry with no usable height, kept
519 // because it costs nothing and an entry that cannot name its block
520 // is worse than one derived from its maturity. COINBASE_MATURITY
521 // and not COINBASE_SPENDABLE_DEPTH: this reverses whatever the
522 // writer of *that* file used, and only files old enough to lack
523 // blockHeight are affected -- they were written when maturity was
524 // still COINBASE_MATURITY.
525 if (pp.blockHeight == 0 && pp.matureAtHeight > COINBASE_MATURITY)
381 pp.blockHeight = pp.matureAtHeight - COINBASE_MATURITY; 526 pp.blockHeight = pp.matureAtHeight - COINBASE_MATURITY;
382 if (pp.id.empty()) 527 // Absent in every file written before this merge; default 0 means
383 pp.id = PayoutIdForBlock(pp.blockHeight); 528 // "ready now", which is the right answer for a restored entry.
384 if (obj.contains("nextAttempt")) 529 if (obj.contains("retryAfter") && obj["retryAfter"].is_number())
385 pp.nextAttempt = obj["nextAttempt"].get<int64>(); 530 pp.retryAfter = obj["retryAfter"].get<int64>();
386 for (auto it = obj["amounts"].begin(); it != obj["amounts"].end(); ++it) 531 for (auto it = obj["amounts"].begin(); it != obj["amounts"].end(); ++it)
387 pp.amounts[it.key()] = it.value().get<int64>(); 532 pp.amounts[it.key()] = it.value().get<int64>();
388 if (obj.contains("paidTxids") && obj["paidTxids"].is_object()) 533 if (obj.contains("paidTxids") && obj["paidTxids"].is_object())
@@ -398,6 +543,133 @@ static void LoadPendingPayouts()
398 } 543 }
399} 544}
400 545
546// A payment in flight: one record per (payout entry, address) pair, which is
547// the granularity SendMoney works at and therefore the granularity at which a
548// payment can be half-done when the process dies.
549//
550// This list is persisted. It used to be a memory-only queue, and matured
551// entries were erased from pending_payouts.json *before* being handed to it,
552// so any restart in between lost those payments with no record that anyone
553// was owed anything: a container restart on 2026-07-30 silently dropped
554// ~1,156 BTF that way. Writing the records before removing the ledger entry,
555// and reconciling against the wallet on startup, is what makes a restart
556// survivable.
557struct PayingRecord {
558 int blockHeight = 0;
559 uint256 blockHash = 0; // zero = predates hash keying, see PendingPayout
560 int matureAtHeight = 0;
561 std::string address;
562 int64 amount = 0;
563 int retries = 0;
564 int deferrals = 0;
565 int64 retryAfter = 0;
566 // Ask the wallet whether this payment already went out, once, before the
567 // first send attempt. Upstream asks on every attempt: FindRecordedPoolPayout
568 // is a linear scan of mapWallet under cs_mapWallet, per recipient, so with
569 // 2000 rotating payout addresses and a retry budget of 1440 that is a
570 // scan per address per minute for a day. The question it answers -- "did a
571 // previous process send this before it died?" -- can only change across a
572 // restart, and every restart re-sets this flag (ReconcilePayouts), so
573 // asking once per record is the whole of the answer.
574 //
575 // Not persisted: a record read back from disk is by definition a restart.
576 bool fProbeWallet = true;
577};
578static std::vector<PayingRecord> gPaying; // guarded by gPayoutExecMutex
579static std::mutex gPayoutExecMutex;
580static std::atomic<bool> gPayoutThreadRunning{false};
581
582static std::string PayingFilePath()
583{
584 return GetAppDir() + "/paying_payouts.json";
585}
586
587// Caller must hold gPayoutExecMutex.
588static void SavePayingLocked()
589{
590 nlohmann::json arr = nlohmann::json::array();
591 for (const auto& r : gPaying) {
592 nlohmann::json obj;
593 obj["blockHeight"] = r.blockHeight;
594 obj["blockHash"] = r.blockHash.ToString();
595 obj["matureAtHeight"] = r.matureAtHeight;
596 obj["address"] = r.address;
597 obj["amount"] = r.amount;
598 obj["retries"] = r.retries;
599 obj["deferrals"] = r.deferrals;
600 arr.push_back(obj);
601 }
602 std::string path = PayingFilePath();
603 // Write to a temporary file and rename, so a crash mid-write cannot leave
604 // a truncated list of who is still owed money.
605 std::string tmp = path + ".tmp";
606 FILE* f = fopen(tmp.c_str(), "w");
607 if (!f) {
608 LogPrint("payout", "[payout] WARNING: could not write %s\n", tmp.c_str());
609 return;
610 }
611 std::string s = arr.dump(2);
612 fwrite(s.c_str(), 1, s.size(), f);
613 // fclose() is where buffered data actually reaches the OS, so a failure
614 // here means the .tmp is short -- renaming it over the real file would
615 // destroy the good copy. Upstream's check, taken because a truncated
616 // list of who is owed money is exactly what this function exists to
617 // prevent. rename() on Windows will not replace an existing file, hence
618 // the remove() first.
619 if (fclose(f) != 0) {
620 LogPrint("payout", "[payout] WARNING: could not close %s\n", tmp.c_str());
621 return;
622 }
623#ifdef _WIN32
624 remove(path.c_str());
625#endif
626 if (rename(tmp.c_str(), path.c_str()) != 0)
627 LogPrint("payout", "[payout] WARNING: could not rename %s -> %s\n",
628 tmp.c_str(), path.c_str());
629}
630
631static void LoadPaying()
632{
633 std::string path = PayingFilePath();
634 FILE* f = fopen(path.c_str(), "r");
635 if (!f) return;
636 fseek(f, 0, SEEK_END);
637 long sz = ftell(f);
638 fseek(f, 0, SEEK_SET);
639 std::string s(sz, '\0');
640 fread(&s[0], 1, sz, f);
641 fclose(f);
642 try {
643 nlohmann::json arr = nlohmann::json::parse(s);
644 for (const auto& obj : arr) {
645 PayingRecord r;
646 r.address = obj["address"].get<std::string>();
647 r.amount = obj["amount"].get<int64>();
648 r.matureAtHeight = obj["matureAtHeight"].get<int>();
649 if (obj.contains("blockHash") && obj["blockHash"].is_string())
650 r.blockHash.SetHex(obj["blockHash"].get<std::string>());
651 if (obj.contains("blockHeight") && obj["blockHeight"].is_number())
652 r.blockHeight = obj["blockHeight"].get<int>();
653 if (obj.contains("retries") && obj["retries"].is_number())
654 r.retries = obj["retries"].get<int>();
655 if (obj.contains("deferrals") && obj["deferrals"].is_number())
656 r.deferrals = obj["deferrals"].get<int>();
657 gPaying.push_back(r);
658 }
659 LogPrint("payout", "[payout] loaded %zu in-flight payment(s) from disk\n",
660 gPaying.size());
661 } catch (const std::exception& e) {
662 LogPrint("payout", "[payout] WARNING: failed to parse %s: %s\n",
663 path.c_str(), e.what());
664 }
665}
666
667// ---------------------------------------------------------------------------
668// Pool round proofs (upstream cbdd06d). Disjoint from the in-flight ledger
669// above -- this is the operator's published record of how a round was split,
670// not an obligation to pay. Both sides added code at this offset; both are
671// kept.
672// ---------------------------------------------------------------------------
401static nlohmann::json PoolRoundToJson(const PoolRoundProof& r) 673static nlohmann::json PoolRoundToJson(const PoolRoundProof& r)
402{ 674{
403 nlohmann::json j; 675 nlohmann::json j;
@@ -529,6 +801,88 @@ static void LoadPoolRounds()
529 } 801 }
530} 802}
531 803
804// Is the block that funds this payout still on the main chain?
805//
806// A zero hash means the entry was written before payouts recorded one, so
807// there is nothing to check and we assume it is good -- voiding those would
808// mean refusing to pay miners on the strength of a missing field.
809static bool PayoutBlockStillOnChain(const uint256& blockHash)
810{
811 if (blockHash == 0)
812 return true;
813 CRITICAL_BLOCK(cs_main)
814 {
815 std::map<uint256, CBlockIndex*>::iterator mi = mapBlockIndex.find(blockHash);
816 if (mi == mapBlockIndex.end() || mi->second == NULL)
817 return false;
818 return mi->second->IsInMainChain();
819 }
820 return false;
821}
822
823static std::string PayoutComment(int blockHeight)
824{
825 return strprintf("Pool payout height %d", blockHeight);
826}
827
828// Startup reconciliation.
829//
830// This used to be the crash-recovery mechanism: it scanned the wallet with
831// WalletAlreadyPaid() and then dropped ledger entries that had already been
832// exploded into in-flight records. Neither job is right any more.
833//
834// The ledger is now authoritative and an entry survives until every recipient
835// has a txid recorded against it (upstream cbdd06d), so dropping ledger
836// entries here would reintroduce exactly the loss this merge is adopting that
837// model to prevent. And the wallet scan is now FindRecordedPoolPayout(), done
838// once per record on its first attempt, which is the same question asked in
839// the one place that can act on the answer. Running both would be two
840// authorities for the same debt.
841//
842// What is left is the useful part: the in-flight list restored from disk may
843// contain records the ledger already shows as paid (we died between the send
844// and the in-flight file being rewritten). The ledger wins.
845static void ReconcilePayouts()
846{
847 std::lock_guard<std::mutex> exec(gPayoutExecMutex);
848 std::lock_guard<std::mutex> ledger(gPayoutLedgerMutex);
849
850 size_t settled = 0;
851 int64 settledAmount = 0;
852 for (auto it = gPaying.begin(); it != gPaying.end(); ) {
853 bool paid = false;
854 for (const PendingPayout& pp : gPendingPayouts)
855 if (pp.blockHash == it->blockHash && pp.paidTxids.count(it->address)) {
856 paid = true;
857 break;
858 }
859 if (paid) {
860 settledAmount += it->amount;
861 settled++;
862 it = gPaying.erase(it);
863 } else {
864 // Every restored record gets one wallet probe before it is allowed
865 // to send: this is precisely the window where the wallet may hold
866 // a transaction the ledger has not recorded yet.
867 it->fProbeWallet = true;
868 ++it;
869 }
870 }
871 if (settled) {
872 LogPrint("payout", "[payout] %zu in-flight payment(s) totalling %s are"
873 " already recorded paid in the ledger -- not resending\n",
874 settled, FormatMoney(settledAmount).c_str());
875 }
876 SavePayingLocked();
877
878 if (!gPaying.empty()) {
879 int64 owed = 0;
880 for (const auto& r : gPaying) owed += r.amount;
881 LogPrint("payout", "[payout] resuming %zu in-flight payment(s) totalling %s\n",
882 gPaying.size(), FormatMoney(owed).c_str());
883 }
884}
885
532static void RecordPoolRound(const PoolRoundProof& round) 886static void RecordPoolRound(const PoolRoundProof& round)
533{ 887{
534 std::lock_guard<std::mutex> lk(gPoolRoundsMutex); 888 std::lock_guard<std::mutex> lk(gPoolRoundsMutex);
@@ -559,13 +913,13 @@ static void MarkPoolRoundPaid(const std::string& id,
559 } 913 }
560} 914}
561 915
562// Payouts ready to execute are moved here under gPayoutExecMutex. 916// Upstream's gPayoutExecQueue/gPayoutExecMutex/gPayoutThreadRunning are
563// A dedicated thread drains this list so SendMoney (which takes cs_main) 917// declared above with gPaying: the in-flight list IS the exec queue in this
564// never runs on the event loop thread and can't stall miner I/O. 918// tree, at per-recipient rather than per-round granularity, so a second queue
565static std::vector<PendingPayout> gPayoutExecQueue; 919// would be a second answer to "who is owed money". Only the dedupe set is new.
566static std::mutex gPayoutExecMutex; 920//
567static std::atomic<bool> gPayoutThreadRunning{false}; 921// Ids of rounds currently exploded into gPaying, so a flush cannot queue the
568 922// same round twice. Guarded by gPayoutExecMutex.
569static std::set<std::string> gQueuedPayouts; 923static std::set<std::string> gQueuedPayouts;
570 924
571static bool FindRecordedPoolPayout(const std::string& id, 925static bool FindRecordedPoolPayout(const std::string& id,
@@ -595,8 +949,13 @@ static bool FindRecordedPoolPayout(const std::string& id,
595 return false; 949 return false;
596} 950}
597 951
952// blockHash stays a uint256 and not upstream's display string: it is the key
953// this fork's payout ledger is built on, and a key that has been through
954// ToString() is a key that can be compared wrong. It is converted to text only
955// where the round proof needs text. The other three arguments are upstream's,
956// carried for the round-proof data.
598static void QueuePayouts(int blockHeight, 957static void QueuePayouts(int blockHeight,
599 const std::string& blockHash, 958 const uint256& blockHash,
600 const std::string& coinbaseTxid, 959 const std::string& coinbaseTxid,
601 int64 coinbaseValue, 960 int64 coinbaseValue,
602 const std::string& foundBy, 961 const std::string& foundBy,
@@ -604,11 +963,11 @@ static void QueuePayouts(int blockHeight,
604 uint64 total) 963 uint64 total)
605{ 964{
606 PoolRoundProof round; 965 PoolRoundProof round;
607 round.id = PayoutIdForBlock(blockHeight); 966 round.id = PayoutIdForBlock(blockHeight, blockHash);
608 round.blockHeight = blockHeight; 967 round.blockHeight = blockHeight;
609 round.matureAtHeight = blockHeight + COINBASE_MATURITY; 968 round.matureAtHeight = blockHeight + COINBASE_SPENDABLE_DEPTH;
610 round.createdAt = GetTime(); 969 round.createdAt = GetTime();
611 round.blockHash = blockHash; 970 round.blockHash = blockHash.ToString();
612 round.coinbaseTxid = coinbaseTxid; 971 round.coinbaseTxid = coinbaseTxid;
613 round.coinbaseValue = coinbaseValue; 972 round.coinbaseValue = coinbaseValue;
614 round.foundBy = foundBy; 973 round.foundBy = foundBy;
@@ -639,9 +998,21 @@ static void QueuePayouts(int blockHeight,
639 round.operatorReward = operatorCut; 998 round.operatorReward = operatorCut;
640 999
641 PendingPayout pp; 1000 PendingPayout pp;
642 pp.id = round.id;
643 pp.blockHeight = blockHeight; 1001 pp.blockHeight = blockHeight;
644 pp.matureAtHeight = blockHeight + COINBASE_MATURITY; 1002 pp.blockHash = blockHash;
1003 // COINBASE_SPENDABLE_DEPTH, not COINBASE_MATURITY. Paying at depth 100
1004 // means the block's own coinbase is still 20 blocks short of spendable, so
1005 // the payment has to come out of some *other* mature coin -- the pool runs
1006 // a permanent ~19-block float and every payout fails until unrelated coin
1007 // matures. The earlier "96% of 934 payments went through at depth 100"
1008 // measurement that justified the shorter wait was taken while this node was
1009 // alone on a private fork paying itself, so it says nothing about a pool
1010 // competing on the real chain. Waiting the extra 20 blocks makes each
1011 // payout funded by construction, and puts the maturity gate past the
1012 // reorg depth that orphaned 280 blocks of obligations. Upstream uses
1013 // COINBASE_MATURITY here; this is a considered disagreement, not an
1014 // oversight -- see the merge commit message.
1015 pp.matureAtHeight = blockHeight + COINBASE_SPENDABLE_DEPTH;
645 1016
646 int skipped = 0; 1017 int skipped = 0;
647 int64 burned = 0; // amount owed to miners with bad/empty addresses 1018 int64 burned = 0; // amount owed to miners with bad/empty addresses
@@ -689,37 +1060,222 @@ static void QueuePayouts(int blockHeight,
689 : ""); 1060 : "");
690} 1061}
691 1062
692// Called from event loop: copy matured entries to the exec queue (fast). 1063// Called from the event loop: explode matured ledger entries into per-recipient
693// The ledger entry stays on disk until every recipient has a txid recorded. 1064// in-flight records (fast; no wallet or disk work beyond one small write).
694// On restart, FindRecordedPoolPayout() checks wallet metadata before sending, 1065//
695// so a crash after SendMoney but before the ledger update does not double-pay. 1066// The ledger entry SURVIVES here. That is upstream's model and it is the
1067// crash-safe one: this fork used to erase the entry as soon as its payments
1068// were handed off, so a death in that window left the debt described by
1069// nothing at all -- a container restart on 2026-07-30 silently dropped
1070// ~1,156 BTF exactly that way. An entry is removed only once every recipient
1071// in it has a txid recorded (PayoutFullyPaid), which the payout thread does.
1072//
1073// gPaying is kept on top of that as the in-flight working set: it carries the
1074// per-recipient retry/deferral state that a per-round `nextAttempt` cannot
1075// express, and it is what the payout thread iterates. It is a cache of work
1076// derived from the ledger, never an independent record of debt -- if it were
1077// lost entirely the ledger would rebuild it on the next flush.
696static void FlushMaturePayouts() 1078static void FlushMaturePayouts()
697{ 1079{
1080 // Which funding blocks are still on the main chain? Asked with no payout
1081 // lock held: PayoutBlockStillOnChain takes cs_main, and cs_main is held by
1082 // ProcessBlock and SendMoney for long stretches. Holding the ledger mutex
1083 // across it would let the event loop park the payout thread behind a chain
1084 // lock it has no interest in. (It is not a deadlock either way -- nothing
1085 // takes the ledger mutex while holding cs_main -- but it is exactly the
1086 // kind of stall this merge is removing from the ledger path.)
1087 std::vector<uint256> orphaned;
1088 {
1089 std::vector<uint256> hashes;
1090 {
1091 std::lock_guard<std::mutex> ledger(gPayoutLedgerMutex);
1092 for (const PendingPayout& pp : gPendingPayouts)
1093 hashes.push_back(pp.blockHash);
1094 }
1095 for (const uint256& h : hashes)
1096 if (!PayoutBlockStillOnChain(h))
1097 orphaned.push_back(h);
1098 }
1099
698 std::vector<PendingPayout> ready; 1100 std::vector<PendingPayout> ready;
699 { 1101 {
700 std::lock_guard<std::mutex> ledger(gPayoutLedgerMutex); 1102 std::lock_guard<std::mutex> ledger(gPayoutLedgerMutex);
701 std::lock_guard<std::mutex> queue(gPayoutExecMutex); 1103
1104 // Void obligations whose funding block lost a reorg. The coin that was
1105 // meant to pay them does not exist, so paying anyway spends real-chain
1106 // coin the pool owes to somebody else.
1107 bool voided = false;
1108 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ) {
1109 if (std::find(orphaned.begin(), orphaned.end(), it->blockHash) == orphaned.end()) {
1110 ++it;
1111 continue;
1112 }
1113 int64 total = 0;
1114 for (auto& kv : it->amounts) total += kv.second;
1115 LogPrint("payout", "[payout] block %d (%s) was orphaned -- voiding %zu"
1116 " payment(s) totalling %s\n",
1117 it->blockHeight, it->blockHash.ToString().substr(0, 14).c_str(),
1118 it->amounts.size(), FormatMoney(total).c_str());
1119 it = gPendingPayouts.erase(it);
1120 voided = true;
1121 }
1122 if (voided) SavePendingPayoutsLocked();
1123
702 int64 now = GetTime(); 1124 int64 now = GetTime();
703 for (const PendingPayout& pp : gPendingPayouts) { 1125 for (const PendingPayout& pp : gPendingPayouts) {
704 if (nBestHeight < pp.matureAtHeight) 1126 if (nBestHeight < pp.matureAtHeight)
705 continue; 1127 continue;
706 if (pp.nextAttempt > now) 1128 if (pp.retryAfter > now)
707 continue; 1129 continue;
708 if (PayoutFullyPaid(pp)) 1130 if (PayoutFullyPaid(pp))
709 continue; 1131 continue;
710 if (gQueuedPayouts.count(pp.id))
711 continue;
712 LogPrint("payout", "[payout] block height %d matured -- queuing payout %s (%zu recipients)\n",
713 pp.matureAtHeight, pp.id.c_str(), pp.amounts.size());
714 ready.push_back(pp); 1132 ready.push_back(pp);
715 gQueuedPayouts.insert(pp.id);
716 } 1133 }
717 for (auto& pp : ready)
718 gPayoutExecQueue.push_back(pp);
719 } 1134 }
1135
1136 if (ready.empty())
1137 return;
1138
1139 std::lock_guard<std::mutex> queue(gPayoutExecMutex);
1140 size_t added = 0;
1141 for (const PendingPayout& pp : ready) {
1142 const std::string id = PayoutIdForBlock(pp.blockHeight, pp.blockHash);
1143 if (gQueuedPayouts.count(id))
1144 continue; // already exploded into gPaying and still in flight
1145 size_t n = 0;
1146 for (const auto& kv : pp.amounts) {
1147 // Upstream's per-recipient dedupe: a recipient already carrying a
1148 // txid is done, even though the entry as a whole is not.
1149 if (pp.paidTxids.count(kv.first))
1150 continue;
1151 PayingRecord r;
1152 r.blockHeight = pp.blockHeight;
1153 r.blockHash = pp.blockHash;
1154 r.matureAtHeight = pp.matureAtHeight;
1155 r.address = kv.first;
1156 r.amount = kv.second;
1157 gPaying.push_back(r);
1158 n++;
1159 }
1160 gQueuedPayouts.insert(id);
1161 added += n;
1162 LogPrint("payout", "[payout] block height %d matured -- queuing payout %s"
1163 " (%zu of %zu recipients still owed)\n",
1164 pp.matureAtHeight, id.c_str(), n, pp.amounts.size());
1165 }
1166 if (added) SavePayingLocked();
1167}
1168
1169// Identity of a payment, for finding its record again after SendMoney.
1170// amounts is a map keyed by address, so (block, address) is already unique.
1171static bool SamePayment(const PayingRecord& a, const PayingRecord& b)
1172{
1173 return a.matureAtHeight == b.matureAtHeight
1174 && a.blockHash == b.blockHash
1175 && a.address == b.address
1176 && a.amount == b.amount;
1177}
1178
1179// Settle a payment: it is either done or abandoned, either way it stops being
1180// in flight. Note this only retires the working record -- whether the miner is
1181// still OWED the money is the ledger's business, and the ledger is updated
1182// separately by RecordPayoutTxid().
1183static void ForgetPayment(const PayingRecord& r)
1184{
1185 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
1186 for (auto it = gPaying.begin(); it != gPaying.end(); ++it)
1187 if (SamePayment(*it, r)) { gPaying.erase(it); break; }
1188
1189 // When the last record for a round is gone the round is no longer in
1190 // flight, so a later flush may explode it again if the ledger still shows
1191 // recipients owed (a give-up, for instance, leaves the entry standing).
1192 bool stillInFlight = false;
1193 for (const auto& e : gPaying)
1194 if (e.blockHash == r.blockHash) { stillInFlight = true; break; }
1195 if (!stillInFlight)
1196 gQueuedPayouts.erase(PayoutIdForBlock(r.blockHeight, r.blockHash));
1197
1198 SavePayingLocked();
1199}
1200
1201// Persist an updated attempt count / backoff for a payment still owed.
1202static void ReschedulePayment(const PayingRecord& r)
1203{
1204 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
1205 for (auto& e : gPaying)
1206 if (SamePayment(e, r)) {
1207 e.retries = r.retries;
1208 e.deferrals = r.deferrals;
1209 e.retryAfter = r.retryAfter;
1210 // Carry the cleared probe flag back, or every retry re-scans the
1211 // whole wallet -- the cost this merge is specifically removing.
1212 e.fProbeWallet = r.fProbeWallet;
1213 break;
1214 }
1215 SavePayingLocked();
1216}
1217
1218// Record that a recipient has been paid, in the ledger, which is the only
1219// durable statement that the debt is settled.
1220//
1221// The file write happens with gPayoutLedgerMutex RELEASED. Upstream calls
1222// SavePendingPayoutsLocked() inside the per-recipient loop with the mutex
1223// held, so every recipient costs the event loop a full serialise+fsync it has
1224// to wait for in FlushMaturePayouts.
1225static void RecordPayoutTxid(const PayingRecord& r, const std::string& txid)
1226{
1227 std::string blob;
1228 bool complete = false;
1229 {
1230 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
1231 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ++it) {
1232 if (it->blockHash != r.blockHash)
1233 continue;
1234 it->paidTxids[r.address] = txid;
1235 it->retryAfter = 0;
1236 if (PayoutFullyPaid(*it)) {
1237 LogPrint("payout", "[payout] payout for block %d complete --"
1238 " removing from ledger\n", it->blockHeight);
1239 gPendingPayouts.erase(it);
1240 complete = true;
1241 }
1242 break;
1243 }
1244 blob = SerializePendingPayoutsLocked();
1245 }
1246 WritePendingPayoutsFile(blob);
1247 // The round proof records a txid per address, so this is per recipient and
1248 // not only on completion.
1249 MarkPoolRoundPaid(PayoutIdForBlock(r.blockHeight, r.blockHash), r.address, txid);
1250 if (complete)
1251 LogPrint("payout", "[payout] block %d fully settled\n", r.blockHeight);
1252}
1253
1254// Write a recipient off. Only reached after the full retry budget against a
1255// block that is still on the main chain with coin available to spend, i.e. a
1256// wallet fault rather than a maturity or reorg question.
1257//
1258// The amount is removed from the entry rather than marked paid, so the money
1259// is not reported as sent. Once nothing is left owed the entry retires.
1260static void ForfeitPayoutRecipient(const PayingRecord& r)
1261{
1262 std::string blob;
1263 {
1264 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
1265 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ++it) {
1266 if (it->blockHash != r.blockHash)
1267 continue;
1268 it->amounts.erase(r.address);
1269 if (it->amounts.empty() || PayoutFullyPaid(*it))
1270 gPendingPayouts.erase(it);
1271 break;
1272 }
1273 blob = SerializePendingPayoutsLocked();
1274 }
1275 WritePendingPayoutsFile(blob);
720} 1276}
721 1277
722// Payout execution thread: drains gPayoutExecQueue, calls SendMoney. 1278// Payout execution thread: works through gPaying, calls SendMoney.
723// Runs independently of the event loop so slow wallet operations 1279// Runs independently of the event loop so slow wallet operations
724// (SendMoney takes cs_main) never stall miner I/O. 1280// (SendMoney takes cs_main) never stall miner I/O.
725static void PayoutThreadFn(void*) 1281static void PayoutThreadFn(void*)
@@ -728,103 +1284,150 @@ static void PayoutThreadFn(void*)
728 LogPrint("payout", "[payout] payout thread started\n"); 1284 LogPrint("payout", "[payout] payout thread started\n");
729 1285
730 while (!fShutdown && gPoolRunning) { 1286 while (!fShutdown && gPoolRunning) {
731 std::vector<PendingPayout> batch; 1287 std::vector<PayingRecord> batch;
732 { 1288 {
733 std::lock_guard<std::mutex> lk(gPayoutExecMutex); 1289 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
734 batch.swap(gPayoutExecQueue); 1290 int64 now = GetTime();
1291 for (const auto& r : gPaying)
1292 if (r.retryAfter <= now)
1293 batch.push_back(r);
735 } 1294 }
736 1295
737 for (auto& ppQueued : batch) { 1296 for (PayingRecord r : batch) {
738 LogPrint("payout", "[payout] executing payments for block matured at %d" 1297 const std::string id = PayoutIdForBlock(r.blockHeight, r.blockHash);
739 " (%zu miners)\n", ppQueued.matureAtHeight, ppQueued.amounts.size()); 1298
740 bool fAbortBatch = false; 1299 // Re-check here and not only at maturity: a payment can sit in this
741 for (auto& kv : ppQueued.amounts) { 1300 // list for hours across retries, and the reorg that kills its
1301 // funding block can land at any point in that window. Upstream has
1302 // no equivalent -- it decides once, when the entry matures.
1303 if (!PayoutBlockStillOnChain(r.blockHash)) {
1304 LogPrint("payout", "[payout] block %d (%s) orphaned while queued"
1305 " -- dropping %s to %s\n",
1306 r.blockHeight, r.blockHash.ToString().substr(0, 14).c_str(),
1307 FormatMoney(r.amount).c_str(), r.address.c_str());
1308 ForgetPayment(r);
1309 continue;
1310 }
1311
1312 // Upstream's per-recipient dedupe, against the ledger rather than
1313 // against the batch: if a txid is already recorded for this
1314 // address, the money went out and nothing more is owed.
1315 {
742 bool alreadyPaid = false; 1316 bool alreadyPaid = false;
743 { 1317 {
744 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); 1318 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
745 for (const PendingPayout& pp : gPendingPayouts) 1319 for (const PendingPayout& pp : gPendingPayouts)
746 if (pp.id == ppQueued.id && pp.paidTxids.count(kv.first)) 1320 if (pp.blockHash == r.blockHash && pp.paidTxids.count(r.address)) {
747 alreadyPaid = true; 1321 alreadyPaid = true;
1322 break;
1323 }
748 } 1324 }
749 if (alreadyPaid) 1325 if (alreadyPaid) {
1326 ForgetPayment(r);
750 continue; 1327 continue;
751
752 uint160 h160;
753 if (!AddressToHash160(kv.first, h160)) {
754 // Should not happen -- bad addresses are filtered in QueuePayouts
755 LogPrint("payout", "[payout] SKIPPED bad address '%s'\n",
756 kv.first.c_str());
757 continue;
758 }
759 CScript sc;
760 sc << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG;
761 CWalletTx wtx;
762 wtx.mapValue["comment"] = strprintf("Pool payout height %d", ppQueued.matureAtHeight);
763 wtx.mapValue["pool_payout_id"] = ppQueued.id;
764 wtx.mapValue["pool_payout_address"] = kv.first;
765 wtx.mapValue["pool_payout_amount"] = strprintf("%lld", kv.second);
766
767 std::string txid;
768 if (FindRecordedPoolPayout(ppQueued.id, kv.first, kv.second, txid))
769 {
770 LogPrint("payout", "[payout] recovered recorded payout %s to %s"
771 " from wallet tx %s\n",
772 FormatMoney(kv.second).c_str(), kv.first.c_str(), txid.c_str());
773 MarkPoolRoundPaid(ppQueued.id, kv.first, txid);
774 }
775 else if (SendMoney(sc, kv.second, wtx))
776 {
777 txid = wtx.GetHash().GetHex();
778 LogPrint("payout", "[payout] paid %s to %s\n",
779 FormatMoney(kv.second).c_str(), kv.first.c_str());
780 MarkPoolRoundPaid(ppQueued.id, kv.first, txid);
781 }
782 else
783 {
784 LogPrint("payout", "[payout] FAILED to pay %s to %s"
785 " (insufficient funds or wallet locked?)\n",
786 FormatMoney(kv.second).c_str(), kv.first.c_str());
787 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
788 for (PendingPayout& pp : gPendingPayouts)
789 if (pp.id == ppQueued.id)
790 pp.nextAttempt = GetTime() + 60;
791 SavePendingPayoutsLocked();
792 fAbortBatch = true;
793 break;
794 } 1328 }
1329 }
795 1330
796 { 1331 uint160 h160;
797 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); 1332 if (!AddressToHash160(r.address, h160)) {
798 for (PendingPayout& pp : gPendingPayouts) { 1333 // Should not happen -- bad addresses are filtered in QueuePayouts
799 if (pp.id != ppQueued.id) 1334 LogPrint("payout", "[payout] SKIPPED bad address '%s'\n",
800 continue; 1335 r.address.c_str());
801 pp.paidTxids[kv.first] = txid; 1336 ForgetPayment(r);
802 pp.nextAttempt = 0; 1337 continue;
803 SavePendingPayoutsLocked(); 1338 }
804 break; 1339 CScript sc;
805 } 1340 sc << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG;
1341
1342 // Did a previous process send this and die before recording it?
1343 // Only asked once per record -- see PayingRecord::fProbeWallet.
1344 if (r.fProbeWallet) {
1345 std::string recovered;
1346 r.fProbeWallet = false;
1347 if (FindRecordedPoolPayout(id, r.address, r.amount, recovered)) {
1348 LogPrint("payout", "[payout] recovered recorded payout %s to %s"
1349 " from wallet tx %s -- not resending\n",
1350 FormatMoney(r.amount).c_str(), r.address.c_str(),
1351 recovered.c_str());
1352 RecordPayoutTxid(r, recovered);
1353 ForgetPayment(r);
1354 continue;
806 } 1355 }
807 } 1356 }
808 1357
809 { 1358 // Waiting for coin to mature is not a failure, so it must not
810 std::lock_guard<std::mutex> ledger(gPayoutLedgerMutex); 1359 // consume the retry budget. Burning a retry every 60s means a
811 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ) { 1360 // payment the pool fully intends to honour can hit GIVING UP
812 if (it->id == ppQueued.id && PayoutFullyPaid(*it)) { 1361 // 24h later purely because the subsidy funding it had not
813 LogPrint("payout", "[payout] payout %s complete -- removing from ledger\n", 1362 // matured yet -- the miner is then silently never paid.
814 it->id.c_str()); 1363 int64 nSpendable = GetBalance();
815 it = gPendingPayouts.erase(it); 1364 if (nSpendable < r.amount) {
816 SavePendingPayoutsLocked(); 1365 r.deferrals++;
817 } else { 1366 r.retryAfter = GetTime() + 60;
818 ++it; 1367 // Hourly, not every minute: a dry pool would otherwise
819 } 1368 // write one line per payment per minute forever.
820 } 1369 if (r.deferrals == 1 || r.deferrals % 60 == 0)
1370 LogPrint("payout", "[payout] deferring %s to %s -- only %s"
1371 " spendable (waiting %d min so far)\n",
1372 FormatMoney(r.amount).c_str(), r.address.c_str(),
1373 FormatMoney(nSpendable).c_str(), r.deferrals);
1374 ReschedulePayment(r);
1375 continue;
821 } 1376 }
822 { 1377
823 std::lock_guard<std::mutex> lk(gPayoutExecMutex); 1378 CWalletTx wtx;
824 gQueuedPayouts.erase(ppQueued.id); 1379 wtx.mapValue["comment"] = PayoutComment(r.blockHeight);
1380 // Upstream's structured metadata. This is what makes the wallet
1381 // probe above possible at all -- the old comment-only form could
1382 // not distinguish two rounds paying the same address the same
1383 // amount. Keyed by the hash-derived id, so a reorg cannot make one
1384 // round's transaction answer for another's.
1385 wtx.mapValue["pool_payout_id"] = id;
1386 wtx.mapValue["pool_payout_address"] = r.address;
1387 wtx.mapValue["pool_payout_amount"] = strprintf("%lld", r.amount);
1388
1389 if (SendMoney(sc, r.amount, wtx)) {
1390 // The wallet holds the transaction before this returns, so a
1391 // crash between here and the ledger write is recoverable: the
1392 // probe above finds it by id+address+amount and does not
1393 // resend. Ledger first, in-flight record second -- the debt is
1394 // described by at least one of them at every instant.
1395 RecordPayoutTxid(r, wtx.GetHash().GetHex());
1396 ForgetPayment(r);
1397 LogPrint("payout", "[payout] paid %s to %s (tx %s)\n",
1398 FormatMoney(r.amount).c_str(), r.address.c_str(),
1399 wtx.GetHash().ToString().substr(0, 12).c_str());
1400 } else if (r.retries < 1440) {
1401 // Transient failures (reorg, locked wallet) must not silently
1402 // burn the payment -- retry with backoff. Insufficient funds
1403 // never lands here; it defers above.
1404 r.retries++;
1405 r.retryAfter = GetTime() + 60;
1406 ReschedulePayment(r);
1407 LogPrint("payout", "[payout] FAILED to pay %s to %s"
1408 " (%s spendable at the time) -- retry %d/1440 in 60s\n",
1409 FormatMoney(r.amount).c_str(), r.address.c_str(),
1410 FormatMoney(nSpendable).c_str(), r.retries);
1411 } else {
1412 // Reaching here means 1440 attempts that were NOT
1413 // insufficient-funds waits (those defer instead) against a
1414 // block still on the main chain -- i.e. a real wallet fault,
1415 // not a maturity or reorg question.
1416 LogPrint("payout", "[payout] GIVING UP on %s to %s after %d"
1417 " retries -- block %d is still on-chain and %s was"
1418 " spendable, so this is a wallet fault, not maturity"
1419 " -- payment forfeited\n",
1420 FormatMoney(r.amount).c_str(), r.address.c_str(),
1421 r.retries, r.blockHeight,
1422 FormatMoney(nSpendable).c_str());
1423 // Write the debt off in the ledger as well as dropping the
1424 // in-flight record. Under the survives-until-fully-paid model
1425 // the entry would otherwise never complete, and every later
1426 // flush would explode it again -- a forfeit that silently
1427 // becomes an infinite retry.
1428 ForfeitPayoutRecipient(r);
1429 ForgetPayment(r);
825 } 1430 }
826 if (fAbortBatch)
827 continue;
828 } 1431 }
829 1432
830 Sleep(500); // check for new work every 500ms 1433 Sleep(500); // check for new work every 500ms
@@ -1044,6 +1647,45 @@ static void PushReady(btf_socket_t s)
1044 gReadyQueue.push_back(s); 1647 gReadyQueue.push_back(s);
1045} 1648}
1046 1649
1650// Loopback stratum listener: lets a miner running on the SAME machine reach
1651// this pool over plain TCP (127.0.0.1:8334), skipping the .btf rendezvous
1652// entirely. The rendezvous relay only allows one service registration per
1653// pubkey, so the pool service and the node's own peer-accept fight over the
1654// node identity at the relay -- loopback avoids that fight for local miners.
1655static void LocalAcceptFn(void*)
1656{
1657 SOCKET ls = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
1658 if (ls == INVALID_SOCKET)
1659 return;
1660 int yes = 1;
1661 setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, (char*)&yes, sizeof(yes));
1662 struct sockaddr_in addr;
1663 memset(&addr, 0, sizeof(addr));
1664 addr.sin_family = AF_INET;
1665 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
1666 addr.sin_port = htons(8334);
1667 if (bind(ls, (struct sockaddr*)&addr, sizeof(addr)) != 0 || listen(ls, 16) != 0)
1668 {
1669 LogPrint("pool", "[pool] loopback stratum bind/listen failed on 127.0.0.1:8334\n");
1670 closesocket(ls);
1671 return;
1672 }
1673 LogPrint("pool", "[pool] loopback stratum listening on 127.0.0.1:8334\n");
1674 while (!fShutdown && gPoolRunning)
1675 {
1676 SOCKET c = accept(ls, NULL, NULL);
1677 if (c == INVALID_SOCKET)
1678 {
1679 if (fShutdown || !gPoolRunning)
1680 break;
1681 continue;
1682 }
1683 LogPrint("pool", "[pool] miner arrived via loopback -- pushing to event loop\n");
1684 PushReady((btf_socket_t)c);
1685 }
1686 closesocket(ls);
1687}
1688
1047// --------------------------------------------------------------------------- 1689// ---------------------------------------------------------------------------
1048// I/O helpers -- event loop thread only 1690// I/O helpers -- event loop thread only
1049// --------------------------------------------------------------------------- 1691// ---------------------------------------------------------------------------
@@ -1480,7 +2122,11 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
1480 if (accepted) { 2122 if (accepted) {
1481 // Snapshot height before RebuildJob() (which increments it) 2123 // Snapshot height before RebuildJob() (which increments it)
1482 int foundHeight = gCurrentJob.height; 2124 int foundHeight = gCurrentJob.height;
1483 QueuePayouts(foundHeight, b.GetHash().GetHex(), b.vtx[0].GetHash().GetHex(), 2125 // b is the block we submitted, untouched by ProcessBlock (which
2126 // took a copy), so its hash is what the payout must be keyed on.
2127 // Passed as a uint256, not hex: the ledger key must stay a real
2128 // hash. The remaining arguments are upstream's round-proof data.
2129 QueuePayouts(foundHeight, b.GetHash(), b.vtx[0].GetHash().GetHex(),
1484 b.vtx[0].vout[0].nValue, m->address, 2130 b.vtx[0].vout[0].nValue, m->address,
1485 roundShareCount, roundShareTotal); 2131 roundShareCount, roundShareTotal);
1486 // Reset round shares 2132 // Reset round shares
@@ -1962,7 +2608,11 @@ static void AcceptOneFn(void* arg)
1962 LogPrint("pool", "[pool] accept thread started for relay %s\n", relay.c_str()); 2608 LogPrint("pool", "[pool] accept thread started for relay %s\n", relay.c_str());
1963 2609
1964 while (!fShutdown && gPoolRunning) { 2610 while (!fShutdown && gPoolRunning) {
1965 // Blocks until a miner connects to us at this relay 2611 // Advertise this relay as the pool's meeting node BEFORE registering,
2612 // so that miners which resolve our descriptor find a relay where the
2613 // pool (not the node's peer-accept) holds the registration.
2614 BtfSetActiveRelay(relay);
2615 // Returns as soon as the relay lists us -- registered, not yet paired.
1966 btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk); 2616 btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk);
1967 2617
1968 if (fShutdown || !gPoolRunning) { 2618 if (fShutdown || !gPoolRunning) {
@@ -1976,6 +2626,23 @@ static void AcceptOneFn(void* arg)
1976 continue; 2626 continue;
1977 } 2627 }
1978 2628
2629 // Block for a miner to actually arrive. RvServiceRegister used to do
2630 // this itself and returned with the relay's 0x01 pairing byte already
2631 // consumed; it no longer does. Without waiting here, BtfServiceWrap
2632 // reads that 0x01 as the first byte of the miner's 32-byte ephemeral
2633 // key -- every subsequent byte is shifted by one, so the X25519
2634 // handshake completes against a garbage shared secret (it has no
2635 // confirmation step to catch it), the tunnel is handed to the event
2636 // loop, and the miner's mining.subscribe never decrypts into a valid
2637 // line. The miner sees the pool close on it; the pool sees a peer that
2638 // never authorized.
2639 if (!btf::RvServiceWaitPaired(rv)) {
2640 btf::RvClose(rv);
2641 LogPrint("pool", "[pool] relay %s: dropped us before any miner dialed"
2642 " -- re-registering\n", relay.c_str());
2643 continue;
2644 }
2645
1979 // Echan handshake -- blocking but fast (local + relay round-trip) 2646 // Echan handshake -- blocking but fast (local + relay round-trip)
1980 btf_socket_t fd = btf::BtfServiceWrap(rv, sk); 2647 btf_socket_t fd = btf::BtfServiceWrap(rv, sk);
1981 if (fd == INVALID_SOCKET) { 2648 if (fd == INVALID_SOCKET) {
@@ -2062,8 +2729,14 @@ void ThreadRPCServer(void*)
2062 2729
2063 LoadPendingPayouts(); 2730 LoadPendingPayouts();
2064 LoadPoolRounds(); 2731 LoadPoolRounds();
2732 LoadPaying();
2733 // Must run before the payout thread starts, so nothing is resent that the
2734 // ledger already shows as sent, and every restored in-flight record is
2735 // armed for its one wallet probe before it is allowed to send.
2736 ReconcilePayouts();
2065 2737
2066 _beginthread(AcceptLoopFn, 0, NULL); 2738 _beginthread(AcceptLoopFn, 0, NULL);
2739 _beginthread(LocalAcceptFn, 0, NULL);
2067 _beginthread(ThreadBtfPoolAnnouncer, 0, NULL); 2740 _beginthread(ThreadBtfPoolAnnouncer, 0, NULL);
2068 _beginthread(PayoutThreadFn, 0, NULL); 2741 _beginthread(PayoutThreadFn, 0, NULL);
2069 2742
diff --git a/src/selftest.cpp b/src/selftest.cpp
index cca5335..eed6779 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -1109,6 +1109,13 @@ static int RunNetMessageSelfTest()
1109 int nFail = 0; 1109 int nFail = 0;
1110 try 1110 try
1111 { 1111 {
1112 // A peer that has not yet been kept waiting must not look like one that
1113 // has been waiting since the epoch. This is the whole bug: the stall
1114 // clock lives in CNode, so only the constructor can start it at zero.
1115 CNode opened(INVALID_SOCKET, CAddress("127.0.0.1"));
1116 nFail += Check(opened.nMsgWaitSince == 0 && opened.nMsgWaitSize == 0,
1117 "a new peer starts with its stall clock stopped") ? 0 : 1;
1118
1112 CNode complete(INVALID_SOCKET, CAddress("127.0.0.1")); 1119 CNode complete(INVALID_SOCKET, CAddress("127.0.0.1"));
1113 complete.nVersion = VERSION; 1120 complete.nVersion = VERSION;
1114 complete.vRecv << CMessageHeader("ping", 0); 1121 complete.vRecv << CMessageHeader("ping", 0);
@@ -1123,20 +1130,38 @@ static int RunNetMessageSelfTest()
1123 nFail += Check(ProcessMessages(&partial), "an incomplete message returns cleanly") ? 0 : 1; 1130 nFail += Check(ProcessMessages(&partial), "an incomplete message returns cleanly") ? 0 : 1;
1124 nFail += Check(partial.vRecv.size() == nPartialBefore, 1131 nFail += Check(partial.vRecv.size() == nPartialBefore,
1125 "an incomplete message keeps one header in the buffer") ? 0 : 1; 1132 "an incomplete message keeps one header in the buffer") ? 0 : 1;
1126 nFail += Check(partial.nIncompleteMessageStart != 0, 1133 nFail += Check(partial.nMsgWaitSince != 0,
1127 "an incomplete message starts a timeout clock") ? 0 : 1; 1134 "an incomplete message starts the stall clock") ? 0 : 1;
1135 nFail += Check(partial.nMsgWaitSize == 0,
1136 "the stall clock records how much payload had arrived") ? 0 : 1;
1128 nFail += Check(!partial.fDisconnect, 1137 nFail += Check(!partial.fDisconnect,
1129 "a fresh incomplete message does not disconnect immediately") ? 0 : 1; 1138 "a fresh incomplete message does not disconnect immediately") ? 0 : 1;
1130 1139
1140 // Still arriving, just slowly: more payload is here than when the clock
1141 // was started, so the clock restarts however old it was. Timing the
1142 // wait instead of the stall would drop this peer.
1143 CNode slow(INVALID_SOCKET, CAddress("127.0.0.1"));
1144 slow.nVersion = VERSION;
1145 slow.vRecv << CMessageHeader("block", 100);
1146 slow.vRecv.insert(slow.vRecv.end(), 50, '\0');
1147 slow.nMsgWaitSince = GetTime() - BTF_MSG_WAIT_MAX_SECS - 1;
1148 slow.nMsgWaitSize = 10;
1149 ProcessMessages(&slow);
1150 nFail += Check(!slow.fDisconnect,
1151 "a peer still sending payload is not dropped") ? 0 : 1;
1152 nFail += Check(slow.nMsgWaitSize == 50,
1153 "progress restarts the stall clock") ? 0 : 1;
1154
1155 // Nothing more has arrived since the clock started, and the clock is
1156 // older than the limit.
1131 CNode stale(INVALID_SOCKET, CAddress("127.0.0.1")); 1157 CNode stale(INVALID_SOCKET, CAddress("127.0.0.1"));
1132 stale.nVersion = VERSION; 1158 stale.nVersion = VERSION;
1133 stale.vRecv << CMessageHeader("block", 100); 1159 stale.vRecv << CMessageHeader("block", 100);
1134 stale.nIncompleteMessageStart = GetTime() - BTF_INCOMPLETE_MESSAGE_TIMEOUT_SECS - 1; 1160 stale.nMsgWaitSince = GetTime() - BTF_MSG_WAIT_MAX_SECS - 1;
1135 stale.nIncompleteMessageSize = 100; 1161 stale.nMsgWaitSize = 0;
1136 stale.strIncompleteMessageCommand = "block";
1137 ProcessMessages(&stale); 1162 ProcessMessages(&stale);
1138 nFail += Check(stale.fDisconnect, 1163 nFail += Check(stale.fDisconnect,
1139 "a stale incomplete message disconnects the peer") ? 0 : 1; 1164 "a stalled incomplete message disconnects the peer") ? 0 : 1;
1140 1165
1141 CNode oversized(INVALID_SOCKET, CAddress("127.0.0.1")); 1166 CNode oversized(INVALID_SOCKET, CAddress("127.0.0.1"));
1142 oversized.nVersion = VERSION; 1167 oversized.nVersion = VERSION;
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}
diff --git a/src/util.h b/src/util.h
index d95d3ae..d72518b 100644
--- a/src/util.h
+++ b/src/util.h
@@ -8,6 +8,13 @@
8#include <cxxabi.h> 8#include <cxxabi.h>
9#endif 9#endif
10 10
11// For AvailableCoreCount() below: sched_getaffinity on Linux, and
12// hardware_concurrency as the fallback everywhere else.
13#include <thread>
14#ifdef __linux__
15#include <sched.h>
16#endif
17
11#if defined(_MSC_VER) || defined(__BORLANDC__) 18#if defined(_MSC_VER) || defined(__BORLANDC__)
12typedef __int64 int64; 19typedef __int64 int64;
13typedef unsigned __int64 uint64; 20typedef unsigned __int64 uint64;
@@ -144,6 +151,34 @@ int64 GetTime();
144int64 GetAdjustedTime(); 151int64 GetAdjustedTime();
145void AddTimeData(unsigned int ip, int64 nTime); 152void AddTimeData(unsigned int ip, int64 nTime);
146 153
154// Cores this process is actually allowed to run on.
155//
156// std::thread::hardware_concurrency() reports the MACHINE's core count, which
157// is not the same number inside a container. With --cpuset-cpus 0-5 on a
158// 12-core host it still answers 12, so every caller that sized a thread pool
159// from it oversubscribed by 2x and then wondered why the box was thrashing:
160// the pool node started 11 miner threads on 6 permitted CPUs, and a burst
161// miner asked for 2 cores filled the ~2 GB RandomX dataset with 12 threads.
162//
163// sched_getaffinity reports what the cpuset permits, which is the number every
164// one of those callers meant. Falls back to hardware_concurrency where the
165// call is unavailable, and never returns 0 -- callers divide by this.
166inline unsigned int AvailableCoreCount()
167{
168#ifdef __linux__
169 cpu_set_t cpuset;
170 CPU_ZERO(&cpuset);
171 if (sched_getaffinity(0, sizeof(cpuset), &cpuset) == 0)
172 {
173 int nAllowed = CPU_COUNT(&cpuset);
174 if (nAllowed > 0)
175 return (unsigned int)nAllowed;
176 }
177#endif
178 unsigned int nCores = std::thread::hardware_concurrency();
179 return nCores > 0 ? nCores : 1;
180}
181
147 182
148 183
149 184