diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Makefile | 4 | ||||
| -rw-r--r-- | src/btfrv.cpp | 41 | ||||
| -rw-r--r-- | src/btftunnel.h | 13 | ||||
| -rw-r--r-- | src/db.cpp | 16 | ||||
| -rw-r--r-- | src/db.h | 23 | ||||
| -rw-r--r-- | src/headers_core.h | 1 | ||||
| -rw-r--r-- | src/main.cpp | 561 | ||||
| -rw-r--r-- | src/main.h | 14 | ||||
| -rw-r--r-- | src/main_gui.cpp | 64 | ||||
| -rw-r--r-- | src/makefile.mingw | 4 | ||||
| -rw-r--r-- | src/market.cpp | 264 | ||||
| -rw-r--r-- | src/market.h | 182 | ||||
| -rw-r--r-- | src/net.cpp | 226 | ||||
| -rw-r--r-- | src/net.h | 204 | ||||
| -rw-r--r-- | src/nostr.cpp | 122 | ||||
| -rw-r--r-- | src/nostr.h | 8 | ||||
| -rw-r--r-- | src/rpc.cpp | 963 | ||||
| -rw-r--r-- | src/selftest.cpp | 37 | ||||
| -rw-r--r-- | src/test_rvaccept.cpp | 180 | ||||
| -rw-r--r-- | src/util.h | 35 |
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. |
| 53 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ | 53 | HEADERS = 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 | ||
| 58 | IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ | 58 | IMGUI_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 | ||
| 61 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ | 61 | NODE_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). |
| 45 | btf_socket_t BtfServiceWrap(RvSocket rv, const unsigned char my_enc_sk[32]); | 50 | btf_socket_t BtfServiceWrap(RvSocket rv, const unsigned char my_enc_sk[32]); |
| 46 | 51 | ||
| @@ -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 | ||
| 604 | bool 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 | |||
| 610 | bool 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 | ||
| @@ -8,8 +8,6 @@ class CTxIndex; | |||
| 8 | class CDiskBlockIndex; | 8 | class CDiskBlockIndex; |
| 9 | class CDiskTxPos; | 9 | class CDiskTxPos; |
| 10 | class COutPoint; | 10 | class COutPoint; |
| 11 | class CUser; | ||
| 12 | class CReview; | ||
| 13 | class CAddress; | 11 | class CAddress; |
| 14 | class CWalletTx; | 12 | class CWalletTx; |
| 15 | 13 | ||
| @@ -314,27 +312,6 @@ public: | |||
| 314 | 312 | ||
| 315 | 313 | ||
| 316 | 314 | ||
| 317 | class CReviewDB : public CDB | ||
| 318 | { | ||
| 319 | public: | ||
| 320 | CReviewDB(const char* pszMode="r+", bool fTxn=false) : CDB("reviews.dat", pszMode, fTxn) { } | ||
| 321 | private: | ||
| 322 | CReviewDB(const CReviewDB&); | ||
| 323 | void operator=(const CReviewDB&); | ||
| 324 | public: | ||
| 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) |
| 72 | void MainFrameRepaint(); | 71 | void 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; | |||
| 87 | string strSetDataDir; | 87 | string strSetDataDir; |
| 88 | int nDropMessagesTest = 0; | 88 | int nDropMessagesTest = 0; |
| 89 | bool fSoloMineTest = false; // /solomine: mine without requiring a peer (local test) | 89 | bool fSoloMineTest = false; // /solomine: mine without requiring a peer (local test) |
| 90 | bool fPoolNoAnnounce = false; // /noannounce: operator serves miners but never publishes pool announcements | ||
| 91 | bool fPoolMineBlind = false; // /poolmineblind: issue jobs even when no peer has reported a height | ||
| 90 | 92 | ||
| 91 | // Settings | 93 | // Settings |
| 92 | int fGenerateBitcoins; | 94 | int fGenerateBitcoins; |
| @@ -102,6 +104,9 @@ CAddress addrIncoming; | |||
| 102 | int nMineMode = MINE_RELAY; | 104 | int nMineMode = MINE_RELAY; |
| 103 | bool fMineModeFromCommandLine = false; | 105 | bool fMineModeFromCommandLine = false; |
| 104 | string strParticipantPool; // participant mode: pool .btf address | 106 | string strParticipantPool; // participant mode: pool .btf address |
| 107 | string strPayoutAddress; // participant: /payoutaddr override (mine without a wallet) | ||
| 108 | string strPayoutFile; // participant: /payoutfile with one address per line, rotated per reconnect | ||
| 109 | int nHashThreads = 0; // participant: /hashthreads override (0 = cores-1) | ||
| 105 | string strPoolName = "Bitflash Pool"; | 110 | string strPoolName = "Bitflash Pool"; |
| 106 | string strPoolDashboardUrl; | 111 | string strPoolDashboardUrl; |
| 107 | string strPoolStatusFile; | 112 | string 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. | ||
| 187 | int 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 | |||
| 172 | static void SecureClearBytes(vector<unsigned char>& vch); | 197 | static void SecureClearBytes(vector<unsigned char>& vch); |
| 173 | static void SecureClearKeyingMaterial(CKeyingMaterial& vch); | 198 | static 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 | ||
| 2942 | bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) | 3006 | bool 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 |
| 3503 | static const int64 STUCK_NO_JOB_TIMEOUT_SECS = 120; | 3567 | static 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 | // ------------------------------------------------------------------ | ||
| 3577 | struct 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 | |||
| 3592 | struct ParticipantShareHit | ||
| 3593 | { | ||
| 3594 | uint64 jobSeq; | ||
| 3595 | std::string jobId; | ||
| 3596 | unsigned int nonce; | ||
| 3597 | }; | ||
| 3598 | |||
| 3599 | static 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 | |||
| 3505 | static bool PoolParticipantMiner() | 3665 | static 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 | |||
| 3774 | connected: | ||
| 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()); |
| @@ -41,6 +41,12 @@ static const int64 MAX_MONEY = 21000000 * COIN; | |||
| 41 | inline bool MoneyRange(int64 nValue) { return (nValue >= 0 && nValue <= MAX_MONEY); } | 41 | inline bool MoneyRange(int64 nValue) { return (nValue >= 0 && nValue <= MAX_MONEY); } |
| 42 | static const int64 CENT = 1000000; | 42 | static const int64 CENT = 1000000; |
| 43 | static const int COINBASE_MATURITY = 100; | 43 | static 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. | ||
| 49 | static 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. |
| 85 | extern bool fMineModeFromCommandLine; | 91 | extern bool fMineModeFromCommandLine; |
| 86 | extern string strParticipantPool; // participant: pool .btf address | 92 | extern string strParticipantPool; // participant: pool .btf address |
| 93 | extern string strPayoutAddress; // participant: /payoutaddr override (mine without a wallet) | ||
| 94 | extern string strPayoutFile; // participant: /payoutfile address list, rotated per reconnect | ||
| 95 | extern bool fPoolNoAnnounce; // /noannounce: suppress pool announcements | ||
| 96 | extern bool fPoolMineBlind; // /poolmineblind: mine on even when deaf to every peer | ||
| 97 | extern int nHashThreads; // participant: /hashthreads override (0 = cores-1) | ||
| 87 | extern string strPoolName; // operator: announced pool name | 98 | extern string strPoolName; // operator: announced pool name |
| 88 | extern string strPoolDashboardUrl; // operator: optional dashboard URL | 99 | extern string strPoolDashboardUrl; // operator: optional dashboard URL |
| 89 | extern string strPoolStatusFile; // operator: optional JSON status output path | 100 | extern string strPoolStatusFile; // operator: optional JSON status output path |
| @@ -96,6 +107,9 @@ extern bool fSoloMineTest; // /solomine: mine without requiring a peer (local te | |||
| 96 | extern int nMinerThreads; | 107 | extern int nMinerThreads; |
| 97 | // Resolves the automatic case into a real count. | 108 | // Resolves the automatic case into a real count. |
| 98 | int MinerThreadCount(); | 109 | int MinerThreadCount(); |
| 110 | // Hash threads one participant session runs; resolves /hashthreads. Not the | ||
| 111 | // same number as MinerThreadCount(), and only ONE session ever runs. | ||
| 112 | int ParticipantHashThreadCount(); | ||
| 99 | extern bool gPoolServerRunning; // GUI/startup state | 113 | extern bool gPoolServerRunning; // GUI/startup state |
| 100 | extern volatile bool gPoolRunning; // runtime pool server loop flag (rpc.cpp) | 114 | extern 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 | ||
| 37 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ | 37 | NODE_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. |
| 51 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ | 51 | HEADERS = 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 | ||
| 56 | all: bitflash.exe | 56 | all: 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 | ||
| 21 | map<uint256, CProduct> mapMyProducts; | ||
| 22 | |||
| 23 | |||
| 24 | |||
| 25 | |||
| 26 | map<uint256, CProduct> mapProducts; | ||
| 27 | CCriticalSection cs_mapProducts; | ||
| 28 | |||
| 29 | bool 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 | |||
| 58 | void 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 | |||
| 84 | template<typename T> | ||
| 85 | unsigned 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 | |||
| 109 | void 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 | |||
| 143 | bool 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 | |||
| 197 | bool 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 | |||
| 237 | bool CProduct::CheckSignature() | ||
| 238 | { | ||
| 239 | return (CKey::Verify(vchPubKeyFrom, GetSigHash(), vchSig)); | ||
| 240 | } | ||
| 241 | |||
| 242 | bool 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 | |||
| 5 | class CUser; | ||
| 6 | class CReview; | ||
| 7 | class CProduct; | ||
| 8 | |||
| 9 | static const unsigned int nFlowthroughRate = 2; | ||
| 10 | |||
| 11 | |||
| 12 | |||
| 13 | |||
| 14 | bool AdvertInsert(const CProduct& product); | ||
| 15 | void AdvertErase(const CProduct& product); | ||
| 16 | bool AddAtomsAndPropagate(uint256 hashUserStart, const vector<unsigned short>& vAtoms, bool fOrigin); | ||
| 17 | |||
| 18 | |||
| 19 | |||
| 20 | |||
| 21 | |||
| 22 | |||
| 23 | |||
| 24 | |||
| 25 | class CUser | ||
| 26 | { | ||
| 27 | public: | ||
| 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 | |||
| 72 | class CReview | ||
| 73 | { | ||
| 74 | public: | ||
| 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 | |||
| 120 | class CProduct | ||
| 121 | { | ||
| 122 | public: | ||
| 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 | |||
| 180 | extern map<uint256, CProduct> mapProducts; | ||
| 181 | extern CCriticalSection cs_mapProducts; | ||
| 182 | extern 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 | ||
| 467 | void 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 | |||
| 505 | bool 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 | ||
| 516 | void CNode::PushGetBlocks(CBlockIndex* pindexBegin, uint256 hashEnd) | 482 | void 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 | ||
| 527 | bool CNode::IsSubscribed(unsigned int nChannel) | ||
| 528 | { | ||
| 529 | if (nChannel >= vfSubscribe.size()) | ||
| 530 | return false; | ||
| 531 | return vfSubscribe[nChannel]; | ||
| 532 | } | ||
| 533 | |||
| 534 | void 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 | |||
| 551 | void 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 | |||
| 1074 | void ThreadBtfAccept(void* parg) | 991 | void 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. | ||
| 1655 | static int64 nLastPeerHeightSeen = 0; | ||
| 1656 | static int64 nPeerHeightFirstAsked = 0; | ||
| 1657 | |||
| 1713 | int GetPeerMedianHeight() | 1658 | int 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. | ||
| 1679 | int64 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. |
| 1862 | int StartMinerThreads() | 1823 | int 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 | ||
| @@ -5,7 +5,6 @@ | |||
| 5 | class CMessageHeader; | 5 | class CMessageHeader; |
| 6 | class CAddress; | 6 | class CAddress; |
| 7 | class CInv; | 7 | class CInv; |
| 8 | class CRequestTracker; | ||
| 9 | class CNode; | 8 | class 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 | ||
| 16 | static const unsigned short DEFAULT_PORT = htons(8433); | 15 | static const unsigned short DEFAULT_PORT = htons(8433); |
| 17 | static const unsigned int PUBLISH_HOPS = 5; | ||
| 18 | enum | 16 | enum |
| 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. |
| 96 | static const unsigned int MAX_PROTOCOL_MESSAGE_SIZE = MAX_SIZE; | 94 | static const unsigned int MAX_PROTOCOL_MESSAGE_SIZE = MAX_SIZE; |
| 97 | static 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. |
| 118 | static const int64 DISCONNECT_DRAIN_SECS = 60; | 115 | static 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. | ||
| 122 | static 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. | ||
| 141 | static 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. |
| 159 | int GetPeerMedianHeight(); | 192 | int GetPeerMedianHeight(); |
| 193 | int64 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 | |||
| 174 | void BtfPexCollect(std::vector<std::string>& vDescOut); | 208 | void 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. |
| 176 | int BtfPexAccept(const std::vector<std::string>& vDesc); | 210 | int BtfPexAccept(const std::vector<std::string>& vDesc); |
| 177 | void AbandonRequests(void (*fn)(void*, CDataStream&), void* param1); | ||
| 178 | bool AnySubscribed(unsigned int nChannel); | ||
| 179 | void ThreadBitcoinMiner(void* parg); | 211 | void ThreadBitcoinMiner(void* parg); |
| 180 | // Starts MinerThreadCount() miners; returns how many came up. | 212 | // Starts MinerThreadCount() miners; returns how many came up. |
| 181 | int StartMinerThreads(); | 213 | int 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. | ||
| 452 | enum | 490 | enum |
| 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 | ||
| 461 | static const char* ppszTypeName[] = | 499 | static const char* ppszTypeName[] = |
| @@ -542,23 +580,9 @@ public: | |||
| 542 | 580 | ||
| 543 | 581 | ||
| 544 | 582 | ||
| 545 | class 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 |
| 547 | public: | 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: | |||
| 604 | public: | 628 | public: |
| 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 | |||
| 1059 | template<typename T> | ||
| 1060 | void 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 | |||
| 1075 | template<typename T> | ||
| 1076 | void 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 | |||
| 1088 | template<typename T> | ||
| 1089 | void 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. | ||
| 88 | const char* pszNostrRelays[] = { | 100 | const 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 | }; |
| 101 | const int nNostrRelays = ARRAYLEN(pszNostrRelays); | 115 | const 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. | ||
| 132 | bool fBtfRelayPinned = false; | ||
| 113 | string strBtfActiveRelay; | 133 | string strBtfActiveRelay; |
| 114 | static CCriticalSection cs_activeRelay; | 134 | static CCriticalSection cs_activeRelay; |
| 115 | 135 | ||
| @@ -144,7 +164,7 @@ static map<string, BtfPoolAnnouncement> g_discoveredPools; | |||
| 144 | static CCriticalSection cs_discoveredPools; | 164 | static 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. |
| 147 | static bool LooksLikeRelay(const string& r) | 167 | bool 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) | |||
| 172 | vector<string> BtfAllRelays() | 192 | vector<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. | ||
| 1020 | static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, | 1072 | static 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. |
| 45 | extern std::string strBtfAnnounceRelay; | 45 | extern 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. | ||
| 51 | extern bool fBtfRelayPinned; | ||
| 52 | // Basic sanity check on a "host:port" relay address (numeric port 1..65535). | ||
| 53 | bool 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. |
| 47 | std::vector<std::string> BtfAllRelays(); | 55 | std::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 | // --------------------------------------------------------------------------- |
| 253 | struct PendingPayout { | 331 | struct 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 | }; |
| 263 | static std::vector<PendingPayout> gPendingPayouts; | 361 | static std::vector<PendingPayout> gPendingPayouts; |
| 264 | static std::mutex gPayoutLedgerMutex; | 362 | static 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 | ||
| 307 | static 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. | ||
| 418 | static 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 | ||
| 312 | static bool PayoutFullyPaid(const PendingPayout& pp) | 426 | static 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 | ||
| 320 | static 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. | ||
| 441 | static 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. | ||
| 466 | static 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. | ||
| 489 | static void SavePendingPayoutsLocked() | ||
| 490 | { | ||
| 491 | WritePendingPayoutsFile(SerializePendingPayoutsLocked()); | ||
| 357 | } | 492 | } |
| 358 | 493 | ||
| 359 | static void LoadPendingPayouts() | 494 | static 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. | ||
| 557 | struct 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 | }; | ||
| 578 | static std::vector<PayingRecord> gPaying; // guarded by gPayoutExecMutex | ||
| 579 | static std::mutex gPayoutExecMutex; | ||
| 580 | static std::atomic<bool> gPayoutThreadRunning{false}; | ||
| 581 | |||
| 582 | static std::string PayingFilePath() | ||
| 583 | { | ||
| 584 | return GetAppDir() + "/paying_payouts.json"; | ||
| 585 | } | ||
| 586 | |||
| 587 | // Caller must hold gPayoutExecMutex. | ||
| 588 | static 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 | |||
| 631 | static 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 | // --------------------------------------------------------------------------- | ||
| 401 | static nlohmann::json PoolRoundToJson(const PoolRoundProof& r) | 673 | static 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. | ||
| 809 | static 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 | |||
| 823 | static 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. | ||
| 845 | static 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 | |||
| 532 | static void RecordPoolRound(const PoolRoundProof& round) | 886 | static 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 |
| 565 | static std::vector<PendingPayout> gPayoutExecQueue; | 919 | // would be a second answer to "who is owed money". Only the dedupe set is new. |
| 566 | static std::mutex gPayoutExecMutex; | 920 | // |
| 567 | static 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. | |
| 569 | static std::set<std::string> gQueuedPayouts; | 923 | static std::set<std::string> gQueuedPayouts; |
| 570 | 924 | ||
| 571 | static bool FindRecordedPoolPayout(const std::string& id, | 925 | static 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. | ||
| 598 | static void QueuePayouts(int blockHeight, | 957 | static 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. | ||
| 696 | static void FlushMaturePayouts() | 1078 | static 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. | ||
| 1171 | static 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(). | ||
| 1183 | static 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. | ||
| 1202 | static 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. | ||
| 1225 | static 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. | ||
| 1260 | static 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. |
| 725 | static void PayoutThreadFn(void*) | 1281 | static 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. | ||
| 1655 | static 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 | |||
| 37 | using namespace btf; | ||
| 38 | |||
| 39 | static const char* HOST = "127.0.0.1"; | ||
| 40 | static unsigned short PORT = 18446; | ||
| 41 | static const char* MSG = "mining.subscribe stand-in payload"; | ||
| 42 | |||
| 43 | static 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 | |||
| 52 | static 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 | |||
| 64 | static 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 | |||
| 76 | static bool g_serviceEcho = false; | ||
| 77 | static std::string g_serviceSaw; | ||
| 78 | |||
| 79 | // The pool's AcceptOneFn, in both shapes. | ||
| 80 | static 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 | |||
| 115 | int 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 | } | ||
| @@ -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__) |
| 12 | typedef __int64 int64; | 19 | typedef __int64 int64; |
| 13 | typedef unsigned __int64 uint64; | 20 | typedef unsigned __int64 uint64; |
| @@ -144,6 +151,34 @@ int64 GetTime(); | |||
| 144 | int64 GetAdjustedTime(); | 151 | int64 GetAdjustedTime(); |
| 145 | void AddTimeData(unsigned int ip, int64 nTime); | 152 | void 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. | ||
| 166 | inline 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 | ||