upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-08-23 05:21:31 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-08-23 05:21:31 +0000
commita9b4f8940907c2bf51e431172b628de89734a908 (patch)
treeff0a0b19d7d7754b809e35c6eae43dae7a32e1e4 /src
parent6d4c3f153cf44cefead39b21191dfb33491bfa5a (diff)
parentbc3de301d9ed0e5fb5e2034be2e066d77cf23804 (diff)
Merge upstream v1.2.18 into the fork
Merges upstream tag v1.2.18 (bc3de30) into our 24 commits. The merge base is a259215, upstream's own commit, whose src/ tree is byte-identical to 68de575 -- our commits were replayed onto a259215 first so that git had a real base to work from. That produced 28 conflict hunks in 9 files instead of the 21 conflict events / 51 hunks a direct cherry-pick onto the tag would have forced, most of which were artifacts of lifting our commits away from their own predecessors. No consensus change. main.cpp's GetBlockValue, GetNextWorkRequired, ConnectInputs, ConnectBlock, CheckBlock, AcceptBlock and AcceptTransaction are untouched by this merge; script.cpp, serialize.h and randomx_pow.h are identical on both sides. The frozen-chain oracle reproduces the pinned UTXO root 116a2b6b... byte for byte, and the merged binary loads the same 12,586-block chain to the same tip aac622f0a3b2f8... with all blocks re-verified and none rejected. Which side won, per file and per hunk. src/rpc.cpp -- 15 hunks. 1. RebuildJob transaction selection. THEIRS for the multi-pass loop and the nFees fix, OURS for the cap. Their vfAlreadyAdded/fFoundSomething loop lands dependent transactions that our single pass dropped deterministically, and their nFees is accumulated across the scan instead of being redeclared inside it -- ours passed a hardcoded 0 to GetBlockValue, so every pool block built here discarded its fees. But their bound is MAX_SIZE/2 = 16 MB, and MAX_BLOCK_SIZE is 1 MB, so their template can exceed what their own CheckBlock accepts. Cap kept at MAX_BLOCK_SIZE/2, with our inner break so a single pass cannot overshoot by more than one transaction. Our RebuildJob sync gating and deaf-node detection are kept; upstream has none. 2. struct PendingPayout. OURS for the key, THEIRS for paidTxids. Their id is the string "block-<height>", which is height keying -- the exact bug 44849e6 fixed after a 280-block reorg left ~14.4k BTF owed against blocks that no longer existed. Height keying also lets the crash-recovery probe match the previous round's transaction after a reorg and silently skip a miner, so their id is dropped entirely and blockHash stays the key. paidTxids is taken because it is what makes an entry survivable. Their nextAttempt and our retryAfter are the same concept under two names; ours is kept and theirs dropped. matureAtHeight gained an explicit = 0, which neither side had in our style of initialiser. 3./4. Ledger writer and loader. Mechanical follow-ons of 2. The loader keeps ours and takes their tolerance for an entry with no usable height, guarded so it cannot fire on a well-formed entry. One redundancy was removed rather than merged: their unguarded `if (obj.contains("blockHeight")) get<int>()` sat two lines above our is_number-guarded version, and an unguarded get<int>() on a malformed value throws out of the whole loop, losing every entry in the file. Verified against the live production pending_payouts.json (2 entries, keys amounts/blockHash/blockHeight/matureAtHeight): loads both. A file in the new shape, with retryAfter and paidTxids, also loads. 5. Insertion point after the loader. BOTH. Our PayingRecord/gPaying in-flight ledger and their PoolRoundProof/PoolRoundToJson are disjoint features that landed at the same offset. Both kept, in full. 6. Writer tail. THEIRS, copied into our writer. Checking fclose() matters here because fclose is where the buffered write actually reaches the OS: renaming a short .tmp over a good file is precisely the truncation the atomic write exists to prevent. The Windows remove()-before-rename() is required because rename() there will not replace an existing file. 7. Loader bodies. BOTH -- LoadPaying and LoadPoolRounds are different functions that happened to share a common tail. 8. Function block, 111 vs 36 lines. BOTH, with one deliberate deletion. PayoutBlockStillOnChain, PayoutComment, RecordPoolRound, MarkPoolRoundPaid all kept. WalletAlreadyPaid is DELETED and ReconcilePayouts rewritten, because our reconciliation and their FindRecordedPoolPayout are two answers to the same crash-recovery question and running both would give the same debt two authorities. Their wallet probe wins: it matches structured metadata rather than a comment string, so it can tell two rounds paying the same address the same amount apart. ReconcilePayouts keeps only the job that is still real -- dropping in-flight records the ledger already shows paid, and arming each restored record for its one wallet probe. It no longer touches gPendingPayouts at all; under the new model dropping ledger entries there would reintroduce the loss this merge adopts their model to prevent. Their gPayoutExecMutex/gPayoutThreadRunning re-declarations were dropped as duplicates of ours, and gPayoutExecQueue was not taken: gPaying is the exec queue in this tree, at finer granularity. 9. QueuePayouts signature. THEIRS for the round-proof arguments, OURS for the type of blockHash. It stays a uint256 and is converted to hex only where the round proof needs text -- a ledger key that has been through ToString() is a key that can be compared wrong. 10. Maturity policy. OURS. matureAtHeight = blockHeight + COINBASE_SPENDABLE_DEPTH (120), not + COINBASE_MATURITY (100). At depth 100 the block's own coinbase is still 20 blocks short of spendable, so the payment must come from unrelated mature coin and the pool runs a permanent float. Our own history contradicts itself here -- d6972a0 argued for COINBASE_MATURITY citing "96% of 934 payments", 44849e6 reversed it on the grounds that the measurement was taken while the node was alone on a private fork paying itself. HEAD's position is kept. This is a considered disagreement with upstream, not an oversight. The live production ledger confirms it: both entries have matureAtHeight - blockHeight = 120. 11. Comment above FlushMaturePayouts. Rewritten to describe the merged design. 12. FlushMaturePayouts, the central difference. THEIRS for the model. The ledger entry now survives on disk until every recipient has a txid, instead of being erased as soon as its payments were handed off -- the old ordering left a window where the debt was described by nothing at all, and a container restart on 2026-07-30 silently dropped ~1,156 BTF through it. Our gPaying is kept on top as the in-flight working set, because it carries the per-recipient retry and deferral state a per-round nextAttempt cannot express. It is explicitly a cache derived from the ledger, not a second record of debt: if it were lost the next flush would rebuild it. 13. PayoutThreadFn, 79 vs 70 lines. OURS for the loop and the reorg re-check, THEIRS for the per-recipient dedupe. The re-check on every attempt is load-bearing and upstream has no equivalent -- it decides once, at maturity, and a payment can sit through hours of retries during which the reorg that kills its funding block can land. Their paidTxids check is taken, against the ledger rather than against the batch. Our deferral policy is kept: waiting for coin is not a failure and must not consume the retry budget. Two fixes upstream does not have are folded in here rather than deferred to a follow-up commit: - FindRecordedPoolPayout runs once per record, on its first attempt, not on every attempt. It is a linear scan of mapWallet under cs_mapWallet, per recipient; upstream repeats it for every retry, which with 2000 rotating payout addresses and a budget of 1440 retries is a full wallet scan per address per minute for a day. The question it answers can only change across a restart, and every restart re-arms the flag. - The ledger file write no longer happens under gPayoutLedgerMutex in the per-recipient loop. SavePendingPayoutsLocked was split into a serialise step that needs the lock and a write step that does not, so the mutex is held only long enough to read the structure. The orphan scan in FlushMaturePayouts was likewise restructured so the ledger mutex is not held across PayoutBlockStillOnChain's cs_main acquisition. One case the merged model created and neither side handled: under survives-until-fully-paid, our GIVING UP path dropped the in-flight record but left the recipient owed in the ledger forever, so every later flush would explode it again -- a forfeit that silently becomes an infinite retry. ForfeitPayoutRecipient writes the amount off so the entry can retire. 14. QueuePayouts call site. Follow-on of 9. 15. ThreadRPCServer startup. BOTH. LoadPoolRounds added; LoadPaying and ReconcilePayouts stay, and stay before the payout thread starts. Crash safety, stated as the property that must hold: after kill -9 at any point, restart pays each recipient exactly once. Before SendMoney, the ledger has no txid and the payment is retried. Between SendMoney and the ledger write, the wallet holds the transaction and the first-attempt probe finds it. After the ledger write, paidTxids suppresses it. If the in-flight file is lost entirely, the ledger rebuilds it and the probe still covers the middle window. src/nostr.cpp -- 3 hunks. THEIRS as the base, with two things restored from ours. Theirs loads the Windows ROOT certificate store, which ours lacks and genuinely needs, since OpenSSL's default verify paths are usually empty there and our version would fail closed. But the plan's claim that theirs is a superset of our 06bceb4 is FALSE: upstream's nostr.cpp contains no min_proto_version, no TLS1_2, no SSL_OP_NO_*, and no set_options anywhere, so taking it wholesale would have removed the TLS-version floor outright. Restored. Second, their SSL_get_verify_result check sits after a successful handshake, where with SSL_VERIFY_PEER set it is nearly dead code -- a bad certificate aborts inside SSL_connect. The reporting belongs in the failure branch, which is where the merge put it; their post-handshake duplicate was dropped. Our relay-list and socket-pooling hunks auto-merged and were verified intact. src/net.cpp -- 1 hunk. THEIRS. Their guard, hSocket == INVALID_SOCKET || pnode->hSocket != hSocket, subsumes ours and additionally catches descriptor recycling, which ours did not; our version also referenced a pnode that only existed in the select() loop. Our other db34011 hunk, nulling pnode->hSocket in the poll-failure branch, auto-merged and was NOT re-applied -- plan section 7 Batch D says to re-apply it, and that instruction is stale. Two pieces of residue the merge left behind were cleaned up: a comment still describing select() semantics, and a pnode->hSocket != INVALID_SOCKET guard made redundant by a continue two lines above it. src/main.cpp -- 2 hunks. h1 BOTH, pure add/add adjacency between our ParticipantHashThreadCount and their SecureClear forward declarations. h2 OURS, and their handler discarded: 44004e3 no-ops checkorder/submitorder but keeps market.cpp and market.h, whereas 11766be deletes the marketplace outright including CRequestTracker, subscribe/publish and mapProducts. Ours is strictly more. src/main_gui.cpp -- 2 hunks. BOTH, additive on both sides. Our /payoutaddr, /payoutfile and /hashthreads alongside their /stratumbridge and /stratumbridgeport. src/btfrv.cpp -- 1 hunk, not predicted by the plan. False adjacency: our INVALID_SOCKET check and their #endif landed at the same offset. Ordering is load-bearing -- our check goes inside the #else arm, before the #endif, or the non-proxy build silently loses it. The BITFLASH_NODE_BUILD arm already returns early, so both paths now have the check. Makefile -- 2 hunks plus a hazard with no conflict marker. THEIRS for both dependency-fetch recipes: checkout --detach FETCH_HEAD followed by test "$(git rev-parse HEAD)" = "$(..._COMMIT)" asserts the pin actually landed, where ours only checked it out. Everything of ours that they lack is kept: the appimagetool 1.9.1 release pin with a sha256 check, where upstream fetches the mutable "continuous" tag and executes it undigested, and the move of both the tool and the AppDir out of /tmp, where any local user could pre-place an executable and have it run as whoever runs make. The hazard: both sides added a pin block in a different place, so git merged both, outside any conflict region. SECP256K1_COMMIT and RANDOMX_COMMIT ended up assigned twice, near L16 and near L44, and GNU Make takes the last := -- a naive resolution would have silently built against our old secp256k1 pin while appearing to adopt upstream's. The duplicate block was deleted by hand. Each of the four variables is now assigned exactly once, and make -n resolves SECP256K1_COMMIT to 7fecac74 with 6e2c8bc4 appearing nowhere. Also applied here, from plan phase 4: SECP256K1_COMMIT moved to upstream's 7fecac74aed8e1fd9078380d67dd04663705c989 so signature validation matches the binaries the rest of the network runs. secp256k1 was rebuilt from scratch against the new pin on the build host and the checked-out dependency commit verified. RANDOMX_COMMIT already matched at 1e9d4b2d and is unchanged. src/Makefile, src/makefile.mingw -- 1 hunk each, both the HEADERS list. Ours drops market.h, theirs adds proxy.h; the resolution does both. Taking theirs wholesale would have left the Makefiles referencing a header git had already deleted in our favour, silently, without a conflict. Files the plan predicted would conflict and did not: net.h, main.h and db.cpp auto-merged. Verified rather than assumed. net.h carries both our BTF_MSG_WAIT_MAX_SECS work, including the constructor fix from 2d84982 that starts nMsgWaitSince and nMsgWaitSize at 0, and their comment rewrite with extern int nPeersWatched. The dead members that fix removed are gone from every file. Deviations from MERGE-1.2.18-PLAN.md. - Section 6's mechanism was not used. Replayed onto the real merge base and merged, rather than cherry-picking onto the tag. Same result, 28 hunks instead of 51. - Section 2 calls 09ff575 a superset of our 06bceb4. It is not; upstream has no TLS-version floor at all. Ours restored on top of theirs. - Section 7 Batch D says to re-apply our db34011 line into their poll-failure branch. It auto-merged; re-applying would have duplicated it. - Section 2 says do not take the stratum bridge. It is taken, and left off. fStratumBridge defaults false in main.cpp, the thread is only spawned when /stratumbridge is passed, and the listener binds 127.0.0.1 only. Excising it would cost real divergence and future conflict for no gain while it is disabled. Its defects -- select()/FD_SET on a node that can hold more than 1024 descriptors, and an uncapped thread per connection -- are real only when it is enabled and are being reported upstream separately. - Section 7's staged batches were not run as separate gated commits. The conflict surface is interleaved within single functions, chiefly the payout ledger, and splitting it would have meant committing states that do not build. The merge is one commit, as briefed. - Batch F's two fixes are folded into hunk 13 rather than deferred, because they change the same lines the resolution rewrites. - The plan did not anticipate the btfrv.cpp conflict, the duplicate pin block, the market.h trap in the two Makefiles, or the give-up/infinite-retry interaction created by adopting upstream's ledger model.
Diffstat (limited to 'src')
-rw-r--r--src/Makefile60
-rw-r--r--src/bignum.h4
-rw-r--r--src/btfrv.cpp9
-rw-r--r--src/db.cpp25
-rw-r--r--src/fuzz_net_message.cpp32
-rw-r--r--src/fuzz_script.cpp59
-rw-r--r--src/gui.cpp2
-rw-r--r--src/main.cpp250
-rw-r--r--src/main.h13
-rw-r--r--src/main_gui.cpp107
-rw-r--r--src/makefile.mingw15
-rw-r--r--src/net.cpp181
-rw-r--r--src/net.h15
-rw-r--r--src/nostr.cpp141
-rw-r--r--src/proxy.cpp377
-rw-r--r--src/proxy.h34
-rw-r--r--src/randomx_pow.cpp5
-rw-r--r--src/rc/bitcoin.icobin372526 -> 150897 bytes
-rw-r--r--src/readme.txt16
-rw-r--r--src/rpc.cpp1152
-rw-r--r--src/selftest.cpp332
-rw-r--r--src/sockcount.h1
-rw-r--r--src/util.cpp23
-rw-r--r--src/walletcmd.cpp2
24 files changed, 2489 insertions, 366 deletions
diff --git a/src/Makefile b/src/Makefile
index 171afe0..1166daf 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -2,6 +2,7 @@
2# Called by: make linux (root Makefile) 2# Called by: make linux (root Makefile)
3 3
4CXX ?= g++ 4CXX ?= g++
5FUZZ_CXX ?= clang++
5 6
6SECP_INC ?= /usr/local/include 7SECP_INC ?= /usr/local/include
7SECP_LIB ?= /usr/local/lib 8SECP_LIB ?= /usr/local/lib
@@ -29,10 +30,13 @@ WARNFLAGS = -Wformat -Werror=format \
29 -Wno-narrowing -Wno-unused-result 30 -Wno-narrowing -Wno-unused-result
30 31
31CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ 32CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \
32 -DSECP256K1_STATIC \ 33 -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD \
33 -I$(SECP_INC) -I$(RANDOMX_DIR)/src \ 34 -I$(SECP_INC) -I$(RANDOMX_DIR)/src \
34 -I/usr/include/nlohmann \ 35 -I/usr/include/nlohmann \
35 -I$(IMGUI_DIR) 36 -I$(IMGUI_DIR)
37FUZZ_CXXFLAGS = $(filter-out -O2,$(CXXFLAGS)) -O1 -g -fno-omit-frame-pointer \
38 -fsanitize=fuzzer-no-link,address,undefined
39FUZZ_LDFLAGS = -fsanitize=fuzzer,address,undefined
36 40
37LIBS = -L$(SECP_LIB) -lsecp256k1 \ 41LIBS = -L$(SECP_LIB) -lsecp256k1 \
38 -L$(RANDOMX_DIR)/build -lrandomx \ 42 -L$(RANDOMX_DIR)/build -lrandomx \
@@ -49,7 +53,7 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \
49HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 53HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \
50 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 \
51 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ 55 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \
52 selftest.h walletcmd.h compat.h sockcount.h font_roboto.h 56 selftest.h walletcmd.h compat.h sockcount.h proxy.h font_roboto.h
53 57
54IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ 58IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
55 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
@@ -57,7 +61,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
57NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ 61NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \
58 obj/sha.o obj/nostr.o obj/randomx_pow.o \ 62 obj/sha.o obj/nostr.o obj/randomx_pow.o \
59 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 \
60 obj/selftest.o obj/bip32.o obj/walletcmd.o 64 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o
61 65
62GUI_OBJS = obj/gui.o obj/main_gui.o 66GUI_OBJS = obj/gui.o obj/main_gui.o
63 67
@@ -117,6 +121,10 @@ NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \
117NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \ 121NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \
118 obj-node/main_gui.o obj-node/headless.o 122 obj-node/main_gui.o obj-node/headless.o
119 123
124FUZZ_BASE_OBJS = $(patsubst obj/%,obj-fuzz/%,$(NODE_OBJS))
125FUZZ_NET_MESSAGE_OBJS = $(FUZZ_BASE_OBJS) obj-fuzz/fuzz_net_message.o
126FUZZ_SCRIPT_OBJS = $(FUZZ_BASE_OBJS) obj-fuzz/fuzz_script.o
127
120obj-node/%.o: %.cpp $(HEADERS) 128obj-node/%.o: %.cpp $(HEADERS)
121 @mkdir -p obj-node 129 @mkdir -p obj-node
122 $(CXX) -c $(CXXFLAGS) -DBITFLASH_NO_GUI -o $@ $< 130 $(CXX) -c $(CXXFLAGS) -DBITFLASH_NO_GUI -o $@ $<
@@ -129,8 +137,47 @@ bitflash-node: $(NODE_ONLY_OBJS)
129 $(CXX) $(CXXFLAGS) -o $@ $^ $(NODE_LIBS) 137 $(CXX) $(CXXFLAGS) -o $@ $^ $(NODE_LIBS)
130 @echo "Built: bitflash-node (headless, no libGL needed)" 138 @echo "Built: bitflash-node (headless, no libGL needed)"
131 139
140obj-fuzz/%.o: %.cpp $(HEADERS)
141 @mkdir -p obj-fuzz
142 $(FUZZ_CXX) -c $(FUZZ_CXXFLAGS) -DBITFLASH_FUZZ -o $@ $<
143
144obj-fuzz/sha.o: sha.cpp sha.h
145 @mkdir -p obj-fuzz
146 $(FUZZ_CXX) -c $(FUZZ_CXXFLAGS) -DBITFLASH_FUZZ -O1 -o $@ $<
147
148obj-fuzz/fuzz_net_message.o: fuzz_net_message.cpp $(HEADERS)
149 @mkdir -p obj-fuzz
150 $(FUZZ_CXX) -c $(FUZZ_CXXFLAGS) -DBITFLASH_FUZZ -o $@ $<
151
152obj-fuzz/fuzz_script.o: fuzz_script.cpp $(HEADERS)
153 @mkdir -p obj-fuzz
154 $(FUZZ_CXX) -c $(FUZZ_CXXFLAGS) -DBITFLASH_FUZZ -o $@ $<
155
156fuzz-net-message: $(FUZZ_NET_MESSAGE_OBJS)
157 $(FUZZ_CXX) $(filter-out -fsanitize=fuzzer-no-link%,$(FUZZ_CXXFLAGS)) \
158 $(FUZZ_LDFLAGS) -o $@ $^ $(NODE_LIBS)
159 @echo "Built: fuzz-net-message"
160
161fuzz-script: $(FUZZ_SCRIPT_OBJS)
162 $(FUZZ_CXX) $(filter-out -fsanitize=fuzzer-no-link%,$(FUZZ_CXXFLAGS)) \
163 $(FUZZ_LDFLAGS) -o $@ $^ $(NODE_LIBS)
164 @echo "Built: fuzz-script"
165
166fuzz-net-message-smoke: fuzz-net-message
167 @mkdir -p ../work/fuzz-corpus/net-message
168 @printf '\277\040\134\375ping\000\000\000\000\000\000\000\000\000\000\000\000' > ../work/fuzz-corpus/net-message/ping-header
169 @printf '\277\040\134\375version\000\000\000\000\000\000\000\000\000' > ../work/fuzz-corpus/net-message/version-header
170 ./fuzz-net-message -runs=128 ../work/fuzz-corpus/net-message
171
172fuzz-script-smoke: fuzz-script
173 @mkdir -p ../work/fuzz-corpus/script
174 @printf '\000\121' > ../work/fuzz-corpus/script/op-true
175 @printf '\001\166\251\024\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020\021\022\023\024\210\254' > ../work/fuzz-corpus/script/p2pkh-shape
176 @printf '\000\115\377\377' > ../work/fuzz-corpus/script/truncated-pushdata2
177 ./fuzz-script -runs=128 ../work/fuzz-corpus/script
178
132clean: 179clean:
133 rm -rf obj obj-node bitflash bitflash-node test_bip32 180 rm -rf obj obj-node obj-fuzz bitflash bitflash-node fuzz-net-message fuzz-script test_bip32
134 181
135test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h 182test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h
136 $(CXX) $(CXXFLAGS) -o $@ test_bip32.cpp bip32.cpp -lssl -lcrypto 183 $(CXX) $(CXXFLAGS) -o $@ test_bip32.cpp bip32.cpp -lssl -lcrypto
@@ -144,3 +191,8 @@ tests: test_bip32 bitflash-node
144 ./bitflash-node -selftest=wallet-encrypt 191 ./bitflash-node -selftest=wallet-encrypt
145 ./bitflash-node -selftest=net-message 192 ./bitflash-node -selftest=net-message
146 ./bitflash-node -selftest=consensus-limits 193 ./bitflash-node -selftest=consensus-limits
194 ./bitflash-node -selftest=pool-stratum
195 ./bitflash-node -selftest=parse-money
196 ./bitflash-node -selftest=socks5-proxy
197
198.PHONY: tests fuzz-net-message-smoke fuzz-script-smoke
diff --git a/src/bignum.h b/src/bignum.h
index 9da104b..6a295b2 100644
--- a/src/bignum.h
+++ b/src/bignum.h
@@ -155,7 +155,9 @@ public:
155 for (int i = 0; i < 8; i++) 155 for (int i = 0; i < 8; i++)
156 { 156 {
157 unsigned char c = (n >> 56) & 0xff; 157 unsigned char c = (n >> 56) & 0xff;
158 n <<= 8; 158 n = (int64)((uint64)n << 8); // shift in unsigned space; <<= on a
159 // signed int64 with the top byte set
160 // is overflow UB (fuzzer/UBSan finding)
159 if (fLeadingZeroes) 161 if (fLeadingZeroes)
160 { 162 {
161 if (c == 0) 163 if (c == 0)
diff --git a/src/btfrv.cpp b/src/btfrv.cpp
index 905717f..133eb89 100644
--- a/src/btfrv.cpp
+++ b/src/btfrv.cpp
@@ -5,6 +5,9 @@
5// Windows/Winsock implementation (Layer 6 will #ifdef this for Linux/BSD sockets). 5// Windows/Winsock implementation (Layer 6 will #ifdef this for Linux/BSD sockets).
6 6
7#include "btfrv.h" 7#include "btfrv.h"
8#ifdef BITFLASH_NODE_BUILD
9#include "proxy.h"
10#endif
8 11
9// Portable sockets: Winsock on Windows, BSD sockets on Linux/POSIX (for the 12// Portable sockets: Winsock on Windows, BSD sockets on Linux/POSIX (for the
10// headless relay `bitflashd` that runs on the VPS). 13// headless relay `bitflashd` that runs on the VPS).
@@ -92,6 +95,11 @@ static bool WriteN(SOCKET s, const void* buf, int n)
92 95
93static SOCKET ConnectTo(const char* host, unsigned short port) 96static SOCKET ConnectTo(const char* host, unsigned short port)
94{ 97{
98#ifdef BITFLASH_NODE_BUILD
99 SOCKET s = BtfConnectSocket(host, port, SOCK_RV_DIAL, 10);
100 if (s == INVALID_SOCKET)
101 return INVALID_SOCKET;
102#else
95 struct addrinfo hints, *res = NULL; 103 struct addrinfo hints, *res = NULL;
96 memset(&hints, 0, sizeof(hints)); 104 memset(&hints, 0, sizeof(hints));
97 hints.ai_family = AF_UNSPEC; // IPv4 or IPv6 (some relays are v6-only) 105 hints.ai_family = AF_UNSPEC; // IPv4 or IPv6 (some relays are v6-only)
@@ -130,6 +138,7 @@ static SOCKET ConnectTo(const char* host, unsigned short port)
130 } 138 }
131 freeaddrinfo(res); 139 freeaddrinfo(res);
132 if (s == INVALID_SOCKET) return INVALID_SOCKET; 140 if (s == INVALID_SOCKET) return INVALID_SOCKET;
141#endif
133 142
134 // 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
135 // time out when idle. recv() will block until data arrives or peer closes. 144 // time out when idle. recv() will block until data arrives or peer closes.
diff --git a/src/db.cpp b/src/db.cpp
index 6f9461b..405a3db 100644
--- a/src/db.cpp
+++ b/src/db.cpp
@@ -872,9 +872,9 @@ bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet)
872 } 872 }
873 if (!fHaveStoredHDCoinType) 873 if (!fHaveStoredHDCoinType)
874 { 874 {
875 nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; 875 nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH;
876 printf("LoadWallet: deterministic seed has no BIP44 coin type metadata; " 876 printf("LoadWallet: deterministic seed has no BIP44 coin type metadata; "
877 "using provisional Bitflash coin type %u\n", nHDCoinType); 877 "using Bitflash coin type %u\n", nHDCoinType);
878 } 878 }
879 } 879 }
880 else 880 else
@@ -883,7 +883,7 @@ bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet)
883 nHDNext = 0; 883 nHDNext = 0;
884 nHDReceiveNext = 0; 884 nHDReceiveNext = 0;
885 nHDChangeNext = 0; 885 nHDChangeNext = 0;
886 nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; 886 nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH;
887 } 887 }
888 888
889 // fGenerateBitcoins and nMineMode only mean anything together, and a 889 // fGenerateBitcoins and nMineMode only mean anything together, and a
@@ -1396,6 +1396,13 @@ static bool EncryptPrivateKeyForWallet(const CKeyingMaterial& vchMasterKey,
1396 return true; 1396 return true;
1397} 1397}
1398 1398
1399static void SecureClearBytes(vector<unsigned char>& vch)
1400{
1401 if (!vch.empty())
1402 OPENSSL_cleanse(&vch[0], vch.size());
1403 vch.clear();
1404}
1405
1399bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet) 1406bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet)
1400{ 1407{
1401 strBackupRet.clear(); 1408 strBackupRet.clear();
@@ -1415,6 +1422,7 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st
1415 map<vector<unsigned char>, CPrivKey> mapPlainKeys; 1422 map<vector<unsigned char>, CPrivKey> mapPlainKeys;
1416 vector<unsigned char> vchPlainHDMaster; 1423 vector<unsigned char> vchPlainHDMaster;
1417 vector<unsigned char> vchPlainHDChainCode; 1424 vector<unsigned char> vchPlainHDChainCode;
1425 bool fHadHDSeed = false;
1418 unsigned int nHDNextSnapshot = 0; 1426 unsigned int nHDNextSnapshot = 0;
1419 { 1427 {
1420 CRITICAL_BLOCK(cs_mapKeys) 1428 CRITICAL_BLOCK(cs_mapKeys)
@@ -1425,6 +1433,7 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st
1425 { 1433 {
1426 vchPlainHDMaster = vchHDMaster; 1434 vchPlainHDMaster = vchHDMaster;
1427 vchPlainHDChainCode = vchHDChainCode; 1435 vchPlainHDChainCode = vchHDChainCode;
1436 fHadHDSeed = !vchPlainHDMaster.empty() || !vchPlainHDChainCode.empty();
1428 nHDNextSnapshot = nHDNext; 1437 nHDNextSnapshot = nHDNext;
1429 } 1438 }
1430 } 1439 }
@@ -1475,11 +1484,13 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st
1475 1484
1476 vector<unsigned char> vchCryptedHDMasterNew; 1485 vector<unsigned char> vchCryptedHDMasterNew;
1477 vector<unsigned char> vchCryptedHDChainCodeNew; 1486 vector<unsigned char> vchCryptedHDChainCodeNew;
1478 if (!vchPlainHDMaster.empty() || !vchPlainHDChainCode.empty()) 1487 if (fHadHDSeed)
1479 { 1488 {
1480 if (vchPlainHDMaster.size() != 32 || vchPlainHDChainCode.size() != 32) 1489 if (vchPlainHDMaster.size() != 32 || vchPlainHDChainCode.size() != 32)
1481 { 1490 {
1482 strErrorRet = "wallet has an incomplete HD seed"; 1491 strErrorRet = "wallet has an incomplete HD seed";
1492 SecureClearBytes(vchPlainHDMaster);
1493 SecureClearBytes(vchPlainHDChainCode);
1483 return false; 1494 return false;
1484 } 1495 }
1485 if (!EncryptSecret(vchMasterKey, vchPlainHDMaster, 1496 if (!EncryptSecret(vchMasterKey, vchPlainHDMaster,
@@ -1488,8 +1499,12 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st
1488 WalletSecretIV("hdchaincode"), vchCryptedHDChainCodeNew)) 1499 WalletSecretIV("hdchaincode"), vchCryptedHDChainCodeNew))
1489 { 1500 {
1490 strErrorRet = "could not encrypt the HD seed"; 1501 strErrorRet = "could not encrypt the HD seed";
1502 SecureClearBytes(vchPlainHDMaster);
1503 SecureClearBytes(vchPlainHDChainCode);
1491 return false; 1504 return false;
1492 } 1505 }
1506 SecureClearBytes(vchPlainHDMaster);
1507 SecureClearBytes(vchPlainHDChainCode);
1493 } 1508 }
1494 1509
1495 vector<pair<int64, vector<unsigned char> > > vNewPool; 1510 vector<pair<int64, vector<unsigned char> > > vNewPool;
@@ -1498,7 +1513,7 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st
1498 for (int i = 0; i < KEYPOOL_SIZE; i++) 1513 for (int i = 0; i < KEYPOOL_SIZE; i++)
1499 { 1514 {
1500 CKey key; 1515 CKey key;
1501 if (!vchPlainHDMaster.empty()) 1516 if (fHadHDSeed)
1502 { 1517 {
1503 string strDeriveError; 1518 string strDeriveError;
1504 if (!DeriveHDKey(nHDNextNew, key, strDeriveError)) 1519 if (!DeriveHDKey(nHDNextNew, key, strDeriveError))
diff --git a/src/fuzz_net_message.cpp b/src/fuzz_net_message.cpp
new file mode 100644
index 0000000..1104d90
--- /dev/null
+++ b/src/fuzz_net_message.cpp
@@ -0,0 +1,32 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3//
4// libFuzzer harness for the P2P message envelope parser. It feeds arbitrary
5// receive-buffer bytes into ProcessMessages() through a dummy CNode, without
6// opening sockets or starting node threads.
7
8#include "headers_core.h"
9
10#include <stdint.h>
11#include <stddef.h>
12
13// Stand-ins normally supplied by the GUI/headless entry objects.
14map<string,string> mapAddressBook;
15bool gPoolServerRunning = false;
16void MainFrameRepaint() {}
17
18extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
19{
20 // Keep this first target focused on parser behavior, not multi-megabyte
21 // allocation pressure. The protocol size guards have their own selftest.
22 if (size > 4096)
23 return 0;
24
25 CNode node(INVALID_SOCKET, CAddress("127.0.0.1"));
26 node.nVersion = 0; // exercise pre-version gating plus the version parser
27 if (size > 0)
28 node.vRecv.write((const char*)data, (int)size);
29
30 ProcessMessages(&node);
31 return 0;
32}
diff --git a/src/fuzz_script.cpp b/src/fuzz_script.cpp
new file mode 100644
index 0000000..28c84f6
--- /dev/null
+++ b/src/fuzz_script.cpp
@@ -0,0 +1,59 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3//
4// libFuzzer harness for raw script parsing and evaluation. It keeps the
5// target deliberately small: arbitrary bytes become one CScript, then the
6// parser, standard-script extractors and EvalScript all see that same input.
7
8#include "headers_core.h"
9
10#include <stdint.h>
11#include <stddef.h>
12
13// Stand-ins normally supplied by the GUI/headless entry objects.
14map<string,string> mapAddressBook;
15bool gPoolServerRunning = false;
16void MainFrameRepaint() {}
17
18extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
19{
20 if (size < 1 || size > 4096)
21 return 0;
22
23 try
24 {
25 int nHashType = data[0];
26 const unsigned char* pbegin = data + 1;
27 const unsigned char* pend = data + size;
28 CScript script(pbegin, pend);
29
30 CScript::const_iterator pc = script.begin();
31 opcodetype opcode;
32 vector<unsigned char> vchPush;
33 while (pc < script.end())
34 {
35 if (!script.GetOp(pc, opcode, vchPush))
36 break;
37 }
38
39 script.GetSigOpCount();
40 script.ToString();
41
42 vector<unsigned char> vchPubKey;
43 ExtractPubKey(script, false, vchPubKey);
44 uint160 hash160;
45 ExtractHash160(script, hash160);
46
47 CTransaction tx;
48 tx.vin.push_back(CTxIn(COutPoint(), CScript()));
49 tx.vout.push_back(CTxOut(0, CScript()));
50
51 vector<vector<unsigned char> > stack;
52 EvalScript(script, tx, 0, nHashType, &stack);
53 }
54 catch (...)
55 {
56 }
57
58 return 0;
59}
diff --git a/src/gui.cpp b/src/gui.cpp
index 8f1d204..8dcb854 100644
--- a/src/gui.cpp
+++ b/src/gui.cpp
@@ -1250,7 +1250,7 @@ static void DrawWalletSafetyDialog()
1250 if (g_recoveryAudit.fHaveSeed) 1250 if (g_recoveryAudit.fHaveSeed)
1251 { 1251 {
1252 ImGui::Text("Derivation schema: %s", HDKeySchemaName(g_recoveryAudit.nSchema).c_str()); 1252 ImGui::Text("Derivation schema: %s", HDKeySchemaName(g_recoveryAudit.nSchema).c_str());
1253 ImGui::Text("BIP44 coin type: %u (provisional BITFLASH)", g_recoveryAudit.nCoinType); 1253 ImGui::Text("BIP44 coin type: %u (SLIP-0044 BITFLASH)", g_recoveryAudit.nCoinType);
1254 } 1254 }
1255 ImGui::Text("Phrase-backed spendable balance: %s BTF", 1255 ImGui::Text("Phrase-backed spendable balance: %s BTF",
1256 FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str()); 1256 FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str());
diff --git a/src/main.cpp b/src/main.cpp
index 77aba89..252911b 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -109,7 +109,11 @@ string strPayoutFile; // participant: /payoutfile with one addres
109int nHashThreads = 0; // participant: /hashthreads override (0 = cores-1) 109int nHashThreads = 0; // participant: /hashthreads override (0 = cores-1)
110string strPoolName = "Bitflash Pool"; 110string strPoolName = "Bitflash Pool";
111string strPoolDashboardUrl; 111string strPoolDashboardUrl;
112string strPoolStatusFile;
113string strPoolRoundsFile;
112double dPoolFeePercent = 0.0; 114double dPoolFeePercent = 0.0;
115bool fStratumBridge = false;
116int nStratumBridgePort = 3333;
113 117
114static std::atomic<uint64> gParticipantSharesSent{0}; 118static std::atomic<uint64> gParticipantSharesSent{0};
115static std::atomic<uint64> gParticipantSharesAccepted{0}; 119static std::atomic<uint64> gParticipantSharesAccepted{0};
@@ -190,6 +194,9 @@ int ParticipantHashThreadCount()
190 return nWorkers - 1; 194 return nWorkers - 1;
191} 195}
192 196
197static void SecureClearBytes(vector<unsigned char>& vch);
198static void SecureClearKeyingMaterial(CKeyingMaterial& vch);
199
193bool AddKey(const CKey& key) 200bool AddKey(const CKey& key)
194{ 201{
195 if (IsWalletEncrypted()) 202 if (IsWalletEncrypted())
@@ -199,9 +206,13 @@ bool AddKey(const CKey& key)
199 vector<unsigned char> vchCryptedSecret; 206 vector<unsigned char> vchCryptedSecret;
200 if (IsWalletLocked()) 207 if (IsWalletLocked())
201 return error("AddKey() : wallet is locked\n"); 208 return error("AddKey() : wallet is locked\n");
202 if (!EncryptSecret(vWalletMasterKey, vector<unsigned char>(vchPrivKey.begin(), vchPrivKey.end()), 209 vector<unsigned char> vchPlainSecret(vchPrivKey.begin(), vchPrivKey.end());
203 WalletKeyIV(key.GetPubKey()), vchCryptedSecret)) 210 if (!EncryptSecret(vWalletMasterKey, vchPlainSecret, WalletKeyIV(key.GetPubKey()), vchCryptedSecret))
211 {
212 SecureClearBytes(vchPlainSecret);
204 return error("AddKey() : encrypting key failed\n"); 213 return error("AddKey() : encrypting key failed\n");
214 }
215 SecureClearBytes(vchPlainSecret);
205 if (!AddCryptedKey(key.GetPubKey(), vchCryptedSecret)) 216 if (!AddCryptedKey(key.GetPubKey(), vchCryptedSecret))
206 return false; 217 return false;
207 return CWalletDB().WriteCryptedKey(key.GetPubKey(), vchCryptedSecret); 218 return CWalletDB().WriteCryptedKey(key.GetPubKey(), vchCryptedSecret);
@@ -225,17 +236,31 @@ bool IsWalletLocked()
225 return fWalletEncrypted && fWalletLocked; 236 return fWalletEncrypted && fWalletLocked;
226} 237}
227 238
239static void SecureClearBytes(vector<unsigned char>& vch)
240{
241 if (!vch.empty())
242 OPENSSL_cleanse(&vch[0], vch.size());
243 vch.clear();
244}
245
246static void SecureClearKeyingMaterial(CKeyingMaterial& vch)
247{
248 if (!vch.empty())
249 OPENSSL_cleanse(&vch[0], vch.size());
250 vch.clear();
251}
252
228void LockWallet() 253void LockWallet()
229{ 254{
230 if (!fWalletEncrypted) 255 if (!fWalletEncrypted)
231 return; 256 return;
232 CRITICAL_BLOCK(cs_mapKeys) 257 CRITICAL_BLOCK(cs_mapKeys)
233 { 258 {
234 vWalletMasterKey.clear(); 259 SecureClearKeyingMaterial(vWalletMasterKey);
235 if (!vchCryptedHDMaster.empty()) 260 if (!vchCryptedHDMaster.empty())
236 vchHDMaster.clear(); 261 SecureClearBytes(vchHDMaster);
237 if (!vchCryptedHDChainCode.empty()) 262 if (!vchCryptedHDChainCode.empty())
238 vchHDChainCode.clear(); 263 SecureClearBytes(vchHDChainCode);
239 fWalletLocked = true; 264 fWalletLocked = true;
240 } 265 }
241} 266}
@@ -260,14 +285,14 @@ bool DeriveWalletPassphraseKey(const string& strPassphrase,
260 chKey, chIV); 285 chKey, chIV);
261 if (n != 32) 286 if (n != 32)
262 { 287 {
263 memset(chKey, 0, sizeof(chKey)); 288 OPENSSL_cleanse(chKey, sizeof(chKey));
264 memset(chIV, 0, sizeof(chIV)); 289 OPENSSL_cleanse(chIV, sizeof(chIV));
265 return false; 290 return false;
266 } 291 }
267 vchKeyRet.assign(chKey, chKey + sizeof(chKey)); 292 vchKeyRet.assign(chKey, chKey + sizeof(chKey));
268 vchIVRet.assign(chIV, chIV + sizeof(chIV)); 293 vchIVRet.assign(chIV, chIV + sizeof(chIV));
269 memset(chKey, 0, sizeof(chKey)); 294 OPENSSL_cleanse(chKey, sizeof(chKey));
270 memset(chIV, 0, sizeof(chIV)); 295 OPENSSL_cleanse(chIV, sizeof(chIV));
271 return true; 296 return true;
272} 297}
273 298
@@ -328,6 +353,8 @@ bool DecryptSecret(const CKeyingMaterial& vchKey,
328 vchPlaintextRet.swap(vchOut); 353 vchPlaintextRet.swap(vchOut);
329 fOk = true; 354 fOk = true;
330 } 355 }
356 if (!fOk)
357 SecureClearBytes(vchOut);
331 EVP_CIPHER_CTX_free(ctx); 358 EVP_CIPHER_CTX_free(ctx);
332 return fOk; 359 return fOk;
333} 360}
@@ -403,6 +430,7 @@ bool GetWalletPrivKey(const vector<unsigned char>& vchPubKey,
403 } 430 }
404 431
405 CPrivKey vchPrivKey(vchPlaintext.begin(), vchPlaintext.end()); 432 CPrivKey vchPrivKey(vchPlaintext.begin(), vchPlaintext.end());
433 SecureClearBytes(vchPlaintext);
406 CKey key; 434 CKey key;
407 if (!key.SetPrivKey(vchPrivKey) || key.GetPubKey() != vchPubKey) 435 if (!key.SetPrivKey(vchPrivKey) || key.GetPubKey() != vchPubKey)
408 { 436 {
@@ -437,11 +465,20 @@ bool UnlockWallet(const string& strPassphrase, string& strErrorRet)
437 vector<unsigned char> vchMasterPlain; 465 vector<unsigned char> vchMasterPlain;
438 if (!DecryptSecret(vchPassKey, kMasterKey.vchCryptedKey, 466 if (!DecryptSecret(vchPassKey, kMasterKey.vchCryptedKey,
439 vchPassIV, vchMasterPlain)) 467 vchPassIV, vchMasterPlain))
468 {
469 SecureClearBytes(vchPassIV);
440 continue; 470 continue;
471 }
441 if (vchMasterPlain.size() != 32) 472 if (vchMasterPlain.size() != 32)
473 {
474 SecureClearBytes(vchMasterPlain);
475 SecureClearBytes(vchPassIV);
442 continue; 476 continue;
477 }
443 478
444 CKeyingMaterial vchCandidate(vchMasterPlain.begin(), vchMasterPlain.end()); 479 CKeyingMaterial vchCandidate(vchMasterPlain.begin(), vchMasterPlain.end());
480 SecureClearBytes(vchMasterPlain);
481 SecureClearBytes(vchPassIV);
445 bool fValid = mapCryptedKeys.empty(); 482 bool fValid = mapCryptedKeys.empty();
446 for (map<vector<unsigned char>, vector<unsigned char> >::iterator mci = mapCryptedKeys.begin(); 483 for (map<vector<unsigned char>, vector<unsigned char> >::iterator mci = mapCryptedKeys.begin();
447 mci != mapCryptedKeys.end(); ++mci) 484 mci != mapCryptedKeys.end(); ++mci)
@@ -450,6 +487,7 @@ bool UnlockWallet(const string& strPassphrase, string& strErrorRet)
450 if (!DecryptSecret(vchCandidate, (*mci).second, WalletKeyIV((*mci).first), vchPlaintext)) 487 if (!DecryptSecret(vchCandidate, (*mci).second, WalletKeyIV((*mci).first), vchPlaintext))
451 break; 488 break;
452 CPrivKey vchPrivKey(vchPlaintext.begin(), vchPlaintext.end()); 489 CPrivKey vchPrivKey(vchPlaintext.begin(), vchPlaintext.end());
490 SecureClearBytes(vchPlaintext);
453 CKey key; 491 CKey key;
454 if (key.SetPrivKey(vchPrivKey) && key.GetPubKey() == (*mci).first) 492 if (key.SetPrivKey(vchPrivKey) && key.GetPubKey() == (*mci).first)
455 fValid = true; 493 fValid = true;
@@ -465,10 +503,12 @@ bool UnlockWallet(const string& strPassphrase, string& strErrorRet)
465 WalletSecretIV("hdmaster"), vchPlainHDMaster) || 503 WalletSecretIV("hdmaster"), vchPlainHDMaster) ||
466 vchPlainHDMaster.size() != 32) 504 vchPlainHDMaster.size() != 32)
467 { 505 {
506 SecureClearBytes(vchPlainHDMaster);
468 strErrorRet = "could not decrypt HD master key"; 507 strErrorRet = "could not decrypt HD master key";
469 return false; 508 return false;
470 } 509 }
471 vchHDMaster = vchPlainHDMaster; 510 vchHDMaster = vchPlainHDMaster;
511 SecureClearBytes(vchPlainHDMaster);
472 } 512 }
473 if (!vchCryptedHDChainCode.empty()) 513 if (!vchCryptedHDChainCode.empty())
474 { 514 {
@@ -477,10 +517,12 @@ bool UnlockWallet(const string& strPassphrase, string& strErrorRet)
477 WalletSecretIV("hdchaincode"), vchPlainHDChainCode) || 517 WalletSecretIV("hdchaincode"), vchPlainHDChainCode) ||
478 vchPlainHDChainCode.size() != 32) 518 vchPlainHDChainCode.size() != 32)
479 { 519 {
520 SecureClearBytes(vchPlainHDChainCode);
480 strErrorRet = "could not decrypt HD chain code"; 521 strErrorRet = "could not decrypt HD chain code";
481 return false; 522 return false;
482 } 523 }
483 vchHDChainCode = vchPlainHDChainCode; 524 vchHDChainCode = vchPlainHDChainCode;
525 SecureClearBytes(vchPlainHDChainCode);
484 } 526 }
485 fWalletLocked = false; 527 fWalletLocked = false;
486 return true; 528 return true;
@@ -625,7 +667,7 @@ unsigned int nHDNext = 0;
625int nHDKeySchema = HD_SCHEMA_NONE; 667int nHDKeySchema = HD_SCHEMA_NONE;
626unsigned int nHDReceiveNext = 0; 668unsigned int nHDReceiveNext = 0;
627unsigned int nHDChangeNext = 0; 669unsigned int nHDChangeNext = 0;
628unsigned int nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; 670unsigned int nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH;
629 671
630string HDKeySchemaName(int nSchema) 672string HDKeySchemaName(int nSchema)
631{ 673{
@@ -781,7 +823,7 @@ bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet)
781 823
782 if (!CWalletDB().WriteHDMaster(master.privateKey, master.chainCode) || 824 if (!CWalletDB().WriteHDMaster(master.privateKey, master.chainCode) ||
783 !CWalletDB().WriteHDSchema(HD_SCHEMA_BIP44) || 825 !CWalletDB().WriteHDSchema(HD_SCHEMA_BIP44) ||
784 !CWalletDB().WriteHDCoinType(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL) || 826 !CWalletDB().WriteHDCoinType(HD_BIP44_COIN_TYPE_BITFLASH) ||
785 !CWalletDB().WriteHDNext(0) || 827 !CWalletDB().WriteHDNext(0) ||
786 !CWalletDB().WriteHDReceiveNext(1) || 828 !CWalletDB().WriteHDReceiveNext(1) ||
787 !CWalletDB().WriteHDChangeNext(0)) 829 !CWalletDB().WriteHDChangeNext(0))
@@ -794,7 +836,7 @@ bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet)
794 nHDKeySchema = HD_SCHEMA_BIP44; 836 nHDKeySchema = HD_SCHEMA_BIP44;
795 nHDReceiveNext = 1; 837 nHDReceiveNext = 1;
796 nHDChangeNext = 0; 838 nHDChangeNext = 0;
797 nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; 839 nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH;
798 840
799 // Index 0 is spoken for before it is derived, so a failure below cannot 841 // Index 0 is spoken for before it is derived, so a failure below cannot
800 // leave the pool free to hand it out as an ordinary key. The cost of 842 // leave the pool free to hand it out as an ordinary key. The cost of
@@ -4128,6 +4170,190 @@ connected:
4128 return true; 4170 return true;
4129} 4171}
4130 4172
4173// ---------------------------------------------------------------------------
4174// Local Stratum bridge -- lets external miners speak plain Stratum to
4175// 127.0.0.1 while Bitflash opens the .btf tunnel to the selected pool.
4176// ---------------------------------------------------------------------------
4177
4178struct StratumBridgeClientCtx
4179{
4180 SOCKET miner;
4181 std::string pool;
4182};
4183
4184static bool BridgeSendAll(SOCKET s, const char* p, int n)
4185{
4186 int off = 0;
4187 while (off < n)
4188 {
4189 int r = send(s, p + off, n - off, 0);
4190 if (r <= 0)
4191 return false;
4192 off += r;
4193 }
4194 return true;
4195}
4196
4197static void BridgePump(SOCKET a, SOCKET b)
4198{
4199 char buf[8192];
4200 while (!fShutdown)
4201 {
4202 fd_set rd;
4203 FD_ZERO(&rd);
4204 FD_SET(a, &rd);
4205 FD_SET(b, &rd);
4206 SOCKET maxfd = a > b ? a : b;
4207 struct timeval tv = {1, 0};
4208 int r = select((int)maxfd + 1, &rd, NULL, NULL, &tv);
4209 if (r <= 0)
4210 continue;
4211
4212 if (FD_ISSET(a, &rd))
4213 {
4214 int n = recv(a, buf, sizeof(buf), 0);
4215 if (n <= 0 || !BridgeSendAll(b, buf, n))
4216 break;
4217 }
4218 if (FD_ISSET(b, &rd))
4219 {
4220 int n = recv(b, buf, sizeof(buf), 0);
4221 if (n <= 0 || !BridgeSendAll(a, buf, n))
4222 break;
4223 }
4224 }
4225}
4226
4227static void ThreadStratumBridgeClient(void* arg)
4228{
4229 StratumBridgeClientCtx* ctx = (StratumBridgeClientCtx*)arg;
4230 SOCKET miner = ctx->miner;
4231 std::string pool = ctx->pool;
4232 delete ctx;
4233
4234 unsigned char target_pubkey[32];
4235 if (!btf::ParseAddress(pool, target_pubkey))
4236 {
4237 LogPrint("worker", "[bridge] invalid pool .btf address: %s\n", pool.c_str());
4238 BtfCloseSocket(miner);
4239 return;
4240 }
4241
4242 std::string meetingHostPort;
4243 unsigned char service_enc_pub[32];
4244 if (!BtfResolve(pool, meetingHostPort, service_enc_pub))
4245 {
4246 LogPrint("worker", "[bridge] failed to resolve pool %s\n", pool.c_str());
4247 BtfCloseSocket(miner);
4248 return;
4249 }
4250
4251 size_t colon = meetingHostPort.rfind(':');
4252 if (colon == std::string::npos)
4253 {
4254 LogPrint("worker", "[bridge] bad pool endpoint '%s'\n", meetingHostPort.c_str());
4255 BtfCloseSocket(miner);
4256 return;
4257 }
4258 std::string host = meetingHostPort.substr(0, colon);
4259 int port = atoi(meetingHostPort.substr(colon + 1).c_str());
4260 if (port <= 0 || port > 65535)
4261 {
4262 LogPrint("worker", "[bridge] bad pool port in '%s'\n", meetingHostPort.c_str());
4263 BtfCloseSocket(miner);
4264 return;
4265 }
4266
4267 SOCKET poolSock = btf::BtfClientTunnel(host.c_str(), (unsigned short)port,
4268 target_pubkey, service_enc_pub);
4269 if (poolSock == INVALID_SOCKET)
4270 {
4271 LogPrint("worker", "[bridge] tunnel connect failed for pool %s via %s\n",
4272 pool.c_str(), meetingHostPort.c_str());
4273 BtfCloseSocket(miner);
4274 return;
4275 }
4276
4277 LogPrint("worker", "[bridge] miner connected to pool %s via %s\n",
4278 pool.c_str(), meetingHostPort.c_str());
4279 BridgePump(miner, poolSock);
4280 BtfCloseSocket(poolSock);
4281 BtfCloseSocket(miner);
4282 LogPrint("worker", "[bridge] miner disconnected from pool %s\n", pool.c_str());
4283}
4284
4285void ThreadStratumBridge(void*)
4286{
4287 if (strParticipantPool.empty())
4288 {
4289 printf("Stratum bridge: no pool selected. Use /stratumbridge=POOL_BTF_ADDRESS\n");
4290 return;
4291 }
4292
4293 SOCKET listener = BtfSocketTag(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP),
4294 SOCK_STRATUM_BRIDGE);
4295 if (listener == INVALID_SOCKET)
4296 {
4297 printf("Stratum bridge: could not create listen socket\n");
4298 return;
4299 }
4300
4301 int yes = 1;
4302 setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, (char*)&yes, sizeof(yes));
4303
4304 sockaddr_in addr;
4305 memset(&addr, 0, sizeof(addr));
4306 addr.sin_family = AF_INET;
4307 addr.sin_addr.s_addr = htonl(0x7f000001);
4308 addr.sin_port = htons((unsigned short)nStratumBridgePort);
4309
4310 if (bind(listener, (sockaddr*)&addr, sizeof(addr)) != 0 ||
4311 listen(listener, 16) != 0)
4312 {
4313 printf("Stratum bridge: could not listen on 127.0.0.1:%d\n",
4314 nStratumBridgePort);
4315 BtfCloseSocket(listener);
4316 return;
4317 }
4318
4319 printf("Stratum bridge: listening on 127.0.0.1:%d for pool %s\n",
4320 nStratumBridgePort, strParticipantPool.c_str());
4321
4322 while (!fShutdown)
4323 {
4324 fd_set rd;
4325 FD_ZERO(&rd);
4326 FD_SET(listener, &rd);
4327 struct timeval tv = {1, 0};
4328 if (select((int)listener + 1, &rd, NULL, NULL, &tv) <= 0)
4329 continue;
4330
4331 sockaddr_in cli;
4332#ifdef _WIN32
4333 int len = sizeof(cli);
4334#else
4335 socklen_t len = sizeof(cli);
4336#endif
4337 SOCKET miner = BtfSocketTag(accept(listener, (sockaddr*)&cli, &len),
4338 SOCK_STRATUM_BRIDGE);
4339 if (miner == INVALID_SOCKET)
4340 continue;
4341
4342 StratumBridgeClientCtx* ctx = new StratumBridgeClientCtx();
4343 ctx->miner = miner;
4344 ctx->pool = strParticipantPool;
4345 if (_beginthread(ThreadStratumBridgeClient, 0, ctx) == (uintptr_t)-1)
4346 {
4347 delete ctx;
4348 BtfCloseSocket(miner);
4349 LogPrint("worker", "[bridge] failed to spawn client thread\n");
4350 }
4351 }
4352
4353 BtfCloseSocket(listener);
4354 printf("Stratum bridge: stopped\n");
4355}
4356
4131bool BitcoinMiner(int nThreadId) 4357bool BitcoinMiner(int nThreadId)
4132{ 4358{
4133 // Relay mode: never mines, just relays/syncs. (Shouldn't normally get here 4359 // Relay mode: never mines, just relays/syncs. (Shouldn't normally get here
diff --git a/src/main.h b/src/main.h
index 27f89ac..8bb536b 100644
--- a/src/main.h
+++ b/src/main.h
@@ -97,7 +97,11 @@ extern bool fPoolMineBlind; // /poolmineblind: mine on even when deaf to
97extern int nHashThreads; // participant: /hashthreads override (0 = cores-1) 97extern int nHashThreads; // participant: /hashthreads override (0 = cores-1)
98extern string strPoolName; // operator: announced pool name 98extern string strPoolName; // operator: announced pool name
99extern string strPoolDashboardUrl; // operator: optional dashboard URL 99extern string strPoolDashboardUrl; // operator: optional dashboard URL
100extern string strPoolStatusFile; // operator: optional JSON status output path
101extern string strPoolRoundsFile; // operator: optional public round proof path
100extern double dPoolFeePercent; // operator: announced fee percent 102extern double dPoolFeePercent; // operator: announced fee percent
103extern bool fStratumBridge; // participant helper: local Stratum bridge
104extern int nStratumBridgePort; // local bridge listen port, default 3333
101extern bool fSoloMineTest; // /solomine: mine without requiring a peer (local test) 105extern bool fSoloMineTest; // /solomine: mine without requiring a peer (local test)
102// Threads to hash with; 0 = decide from the hardware. Set by /genproclimit. 106// Threads to hash with; 0 = decide from the hardware. Set by /genproclimit.
103extern int nMinerThreads; 107extern int nMinerThreads;
@@ -226,9 +230,9 @@ static const unsigned int HD_BIP44_PURPOSE = 44;
226static const unsigned int HD_BIP44_ACCOUNT = 0; 230static const unsigned int HD_BIP44_ACCOUNT = 0;
227static const unsigned int HD_BIP44_CHAIN_RECEIVE = 0; 231static const unsigned int HD_BIP44_CHAIN_RECEIVE = 0;
228static const unsigned int HD_BIP44_CHAIN_CHANGE = 1; 232static const unsigned int HD_BIP44_CHAIN_CHANGE = 1;
229// Provisional until Bitflash has an official SLIP-0044 assignment. 233// Official SLIP-0044 assignment (registered 2026-08-07):
230// Proposed registry row: 4346950 | BITFLASH | Bitflash. 234// 4346950 | BITFLASH | Bitflash (4346950 == 0x425446 == "BTF")
231static const unsigned int HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL = 4346950; 235static const unsigned int HD_BIP44_COIN_TYPE_BITFLASH = 4346950;
232extern int nHDKeySchema; 236extern int nHDKeySchema;
233extern unsigned int nHDReceiveNext; // BIP44 external chain 237extern unsigned int nHDReceiveNext; // BIP44 external chain
234extern unsigned int nHDChangeNext; // BIP44 internal chain 238extern unsigned int nHDChangeNext; // BIP44 internal chain
@@ -285,6 +289,7 @@ void LimitOrphanBlocks(unsigned int nMaxOrphans);
285// distinguishable, which the dedup filter would otherwise collapse into one. 289// distinguishable, which the dedup filter would otherwise collapse into one.
286bool BitcoinMiner(int nThreadId = 1); 290bool BitcoinMiner(int nThreadId = 1);
287void ThreadRPCServer(void* parg); // rpc.cpp -- .btf pool server 291void ThreadRPCServer(void* parg); // rpc.cpp -- .btf pool server
292void ThreadStratumBridge(void* parg); // main.cpp -- local Stratum -> .btf bridge
288void GetParticipantMiningStats(uint64& sharesSent, uint64& sharesAccepted, double& hashRate); 293void GetParticipantMiningStats(uint64& sharesSent, uint64& sharesAccepted, double& hashRate);
289void SetParticipantMiningStatus(const std::string& status); 294void SetParticipantMiningStatus(const std::string& status);
290std::string GetParticipantMiningStatus(); 295std::string GetParticipantMiningStatus();
@@ -325,7 +330,7 @@ struct WalletRecoveryAudit
325 nDerivedKnown = 0; 330 nDerivedKnown = 0;
326 nReceiveNext = 0; 331 nReceiveNext = 0;
327 nChangeNext = 0; 332 nChangeNext = 0;
328 nCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; 333 nCoinType = HD_BIP44_COIN_TYPE_BITFLASH;
329 nRecoverableTx = 0; 334 nRecoverableTx = 0;
330 nLegacyTx = 0; 335 nLegacyTx = 0;
331 nRecoverableImmatureTx = 0; 336 nRecoverableImmatureTx = 0;
diff --git a/src/main_gui.cpp b/src/main_gui.cpp
index bee4b95..e941522 100644
--- a/src/main_gui.cpp
+++ b/src/main_gui.cpp
@@ -1,6 +1,7 @@
1// Bitflash entry point -- starts node threads then runs GUI (or headless). 1// Bitflash entry point -- starts node threads then runs GUI (or headless).
2 2
3#include "headers_core.h" 3#include "headers_core.h"
4#include "proxy.h"
4#include "selftest.h" 5#include "selftest.h"
5#include "walletcmd.h" 6#include "walletcmd.h"
6#include <thread> // hardware_concurrency, to sanity-check /genproclimit 7#include <thread> // hardware_concurrency, to sanity-check /genproclimit
@@ -110,7 +111,8 @@ static void PrintUsage()
110 printf(" /debug\n"); 111 printf(" /debug\n");
111 printf(" /gen\n"); 112 printf(" /gen\n");
112 printf(" /nogui or /daemon\n"); 113 printf(" /nogui or /daemon\n");
113 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, or wallet-encrypt\n"); 114 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt,\n");
115 printf(" net-message, consensus-limits, pool-stratum, parse-money, or socks5-proxy\n");
114 printf("\n"); 116 printf("\n");
115 printf("Mining mode:\n"); 117 printf("Mining mode:\n");
116 printf(" /operator\n"); 118 printf(" /operator\n");
@@ -118,6 +120,8 @@ static void PrintUsage()
118 printf(" /payoutaddr=BTF_ADDRESS (participant: pay shares to this address, no wallet needed)\n"); 120 printf(" /payoutaddr=BTF_ADDRESS (participant: pay shares to this address, no wallet needed)\n");
119 printf(" /payoutfile=FILE (participant: rotate payout addresses from file)\n"); 121 printf(" /payoutfile=FILE (participant: rotate payout addresses from file)\n");
120 printf(" /hashthreads=N (participant: mining threads, default cores-1)\n"); 122 printf(" /hashthreads=N (participant: mining threads, default cores-1)\n");
123 printf(" /stratumbridge=POOL_BTF_ADDRESS\n");
124 printf(" /stratumbridgeport=N (default 3333; listen on 127.0.0.1)\n");
121 printf(" /solomine\n"); 125 printf(" /solomine\n");
122 printf(" /genproclimit=N (mining threads; 0 or absent = every core but one)\n"); 126 printf(" /genproclimit=N (mining threads; 0 or absent = every core but one)\n");
123 printf(" /checkblocks=N (blocks re-verified at startup, default 288, 0 = all)\n"); 127 printf(" /checkblocks=N (blocks re-verified at startup, default 288, 0 = all)\n");
@@ -126,6 +130,8 @@ static void PrintUsage()
126 printf(" /poolname=NAME\n"); 130 printf(" /poolname=NAME\n");
127 printf(" /poolfee=PCT\n"); 131 printf(" /poolfee=PCT\n");
128 printf(" /pooldashboard=URL (alias: /pooldash=URL)\n"); 132 printf(" /pooldashboard=URL (alias: /pooldash=URL)\n");
133 printf(" /poolstatusfile=PATH (write pool status JSON for dashboards)\n");
134 printf(" /poolroundsfile=PATH (write public pool round proofs JSON)\n");
129 printf("\n"); 135 printf("\n");
130 printf(".btf and rendezvous:\n"); 136 printf(".btf and rendezvous:\n");
131 printf(" /connectbtf=PEER_BTF_ADDRESS\n"); 137 printf(" /connectbtf=PEER_BTF_ADDRESS\n");
@@ -134,6 +140,8 @@ static void PrintUsage()
134 printf("\n"); 140 printf("\n");
135 printf("Network:\n"); 141 printf("Network:\n");
136 printf(" /port=N (P2P listen port, default 8433)\n"); 142 printf(" /port=N (P2P listen port, default 8433)\n");
143 printf(" /socks=HOST:PORT (SOCKS5 proxy for outbound Nostr and .btf relay dials)\n");
144 printf(" /tor[=HOST:PORT] (Tor mode; default SOCKS5 proxy is 127.0.0.1:9050)\n");
137 printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n"); 145 printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n");
138 printf("\n"); 146 printf("\n");
139 printf("Wallet:\n"); 147 printf("Wallet:\n");
@@ -240,6 +248,22 @@ static void ParseStartupArguments(int argc, char* argv[])
240 if (!hashThreads.empty()) 248 if (!hashThreads.empty())
241 nHashThreads = atoi(hashThreads.c_str()); 249 nHashThreads = atoi(hashThreads.c_str());
242 250
251 if (arg(argc,argv,"/stratumbridge") || arg(argc,argv,"-stratumbridge"))
252 {
253 fStratumBridge = true;
254 string bridgePool = argval2(argc, argv, "/stratumbridge", "-stratumbridge");
255 if (!bridgePool.empty())
256 strParticipantPool = bridgePool;
257 }
258
259 string bridgePort = argval2(argc, argv, "/stratumbridgeport", "-stratumbridgeport");
260 if (!bridgePort.empty())
261 {
262 int nPort = atoi(bridgePort.c_str());
263 if (nPort > 0 && nPort <= 65535)
264 nStratumBridgePort = nPort;
265 }
266
243 string poolName = argval2(argc, argv, "/poolname", "-poolname"); 267 string poolName = argval2(argc, argv, "/poolname", "-poolname");
244 if (!poolName.empty()) 268 if (!poolName.empty())
245 strPoolName = poolName; 269 strPoolName = poolName;
@@ -260,6 +284,14 @@ static void ParseStartupArguments(int argc, char* argv[])
260 if (!poolFee.empty()) 284 if (!poolFee.empty())
261 dPoolFeePercent = atof(poolFee.c_str()); 285 dPoolFeePercent = atof(poolFee.c_str());
262 286
287 string poolStatusFile = argval2(argc, argv, "/poolstatusfile", "-poolstatusfile");
288 if (!poolStatusFile.empty())
289 strPoolStatusFile = poolStatusFile;
290
291 string poolRoundsFile = argval2(argc, argv, "/poolroundsfile", "-poolroundsfile");
292 if (!poolRoundsFile.empty())
293 strPoolRoundsFile = poolRoundsFile;
294
263 string btfConnect = argval2(argc, argv, "/connectbtf", "-connectbtf"); 295 string btfConnect = argval2(argc, argv, "/connectbtf", "-connectbtf");
264 if (!btfConnect.empty()) 296 if (!btfConnect.empty())
265 strBtfConnect = btfConnect; 297 strBtfConnect = btfConnect;
@@ -287,6 +319,36 @@ static void ParseStartupArguments(int argc, char* argv[])
287 if (!announceRelay.empty()) 319 if (!announceRelay.empty())
288 strBtfAnnounceRelay = announceRelay; 320 strBtfAnnounceRelay = announceRelay;
289 321
322 bool fTorMode = arg(argc, argv, "/tor") || arg(argc, argv, "-tor");
323 if (fTorMode)
324 {
325 string socksProxy = argval2(argc, argv, "/socks", "-socks");
326 if (!socksProxy.empty())
327 fprintf(stderr, "Warning: /tor takes precedence over /socks=%s\n",
328 socksProxy.c_str());
329
330 string torProxy = argval2(argc, argv, "/tor", "-tor");
331 string err;
332 if (!BtfEnableTorProxy(torProxy, err))
333 fprintf(stderr, "Ignoring /tor=%s: %s\n", torProxy.c_str(), err.c_str());
334 else
335 fprintf(stderr, "Tor mode enabled through SOCKS5 proxy %s\n",
336 BtfSocks5ProxyName().c_str());
337 }
338 else
339 {
340 string socksProxy = argval2(argc, argv, "/socks", "-socks");
341 if (!socksProxy.empty())
342 {
343 string err;
344 if (!BtfSetSocks5Proxy(socksProxy, err))
345 fprintf(stderr, "Ignoring /socks=%s: %s\n", socksProxy.c_str(), err.c_str());
346 else
347 fprintf(stderr, "SOCKS5 proxy enabled for outbound discovery/relay dials: %s\n",
348 BtfSocks5ProxyName().c_str());
349 }
350 }
351
290 // /btfseed=ADDRESS:ENCHEX -- extra bootstrap peers, repeatable. Useful for 352 // /btfseed=ADDRESS:ENCHEX -- extra bootstrap peers, repeatable. Useful for
291 // testing the seed path and for private networks that ship no compiled list. 353 // testing the seed path and for private networks that ship no compiled list.
292 for (int i = 1; i < argc; i++) 354 for (int i = 1; i < argc; i++)
@@ -368,6 +430,36 @@ static void FatalStartupError(bool fHeadless, const string& strWhat, const strin
368#endif 430#endif
369} 431}
370 432
433#ifdef _WIN32
434// Windows equivalent of the SIGINT/SIGTERM handler the headless loop installs on
435// POSIX. Without it a headless node on Windows had no way to reach the clean
436// StopNode()+DBFlush() path: Ctrl-C, closing the console window, a service stop
437// and OS shutdown all killed it mid-write, and a wallet.dat left unflushed by
438// Berkeley DB will not reopen. For CLOSE/LOGOFF/SHUTDOWN the OS terminates us
439// shortly after the handler returns, so we set fShutdown and then block until the
440// main loop signals the flush is done -- returning any sooner throws the flush
441// away, which is the whole bug. (A `taskkill /F` is still an uncatchable hard
442// kill; nothing can help there, and nothing should pretend to.)
443static volatile bool g_fHeadlessShutdownDone = false;
444static BOOL WINAPI HeadlessConsoleCtrlHandler(DWORD dwCtrlType)
445{
446 switch (dwCtrlType)
447 {
448 case CTRL_C_EVENT:
449 case CTRL_BREAK_EVENT:
450 case CTRL_CLOSE_EVENT:
451 case CTRL_LOGOFF_EVENT:
452 case CTRL_SHUTDOWN_EVENT:
453 fShutdown = true;
454 while (!g_fHeadlessShutdownDone)
455 Sleep(50);
456 return TRUE;
457 default:
458 return FALSE;
459 }
460}
461#endif
462
371int main(int argc, char* argv[]) 463int main(int argc, char* argv[])
372{ 464{
373 if (arg(argc,argv,"/help") || arg(argc,argv,"-help") || 465 if (arg(argc,argv,"/help") || arg(argc,argv,"-help") ||
@@ -676,6 +768,10 @@ int main(int argc, char* argv[])
676 if (_beginthread(ThreadRPCServer, 0, NULL) == (uintptr_t)-1) 768 if (_beginthread(ThreadRPCServer, 0, NULL) == (uintptr_t)-1)
677 printf("Error: _beginthread(ThreadRPCServer) failed\n"); 769 printf("Error: _beginthread(ThreadRPCServer) failed\n");
678 } 770 }
771 if (fStratumBridge) {
772 if (_beginthread(ThreadStratumBridge, 0, NULL) == (uintptr_t)-1)
773 printf("Error: _beginthread(ThreadStratumBridge) failed\n");
774 }
679 if (fGenerateBitcoins) 775 if (fGenerateBitcoins)
680 StartMinerThreads(); 776 StartMinerThreads();
681 777
@@ -688,7 +784,9 @@ int main(int argc, char* argv[])
688#endif 784#endif
689 785
690 if (fHeadless) { 786 if (fHeadless) {
691#ifndef _WIN32 787#ifdef _WIN32
788 SetConsoleCtrlHandler(HeadlessConsoleCtrlHandler, TRUE);
789#else
692 auto sig=[](int){fShutdown=true;}; 790 auto sig=[](int){fShutdown=true;};
693 signal(SIGINT,sig); signal(SIGTERM,sig); 791 signal(SIGINT,sig); signal(SIGTERM,sig);
694#endif 792#endif
@@ -700,6 +798,11 @@ int main(int argc, char* argv[])
700 // called here, which is why a wallet.dat copied elsewhere would not 798 // called here, which is why a wallet.dat copied elsewhere would not
701 // open -- issue #40. Not optional. 799 // open -- issue #40. Not optional.
702 DBFlush(true); 800 DBFlush(true);
801#ifdef _WIN32
802 // wallet.dat is safely flushed and closed now; let a pending console
803 // control handler (CLOSE/LOGOFF/SHUTDOWN) return so the OS can finish.
804 g_fHeadlessShutdownDone = true;
805#endif
703 return 0; 806 return 0;
704 } 807 }
705 808
diff --git a/src/makefile.mingw b/src/makefile.mingw
index 72f8bbf..8b49cac 100644
--- a/src/makefile.mingw
+++ b/src/makefile.mingw
@@ -14,14 +14,9 @@ WARNFLAGS = -Wformat -Werror=format \
14 -Wno-deprecated-declarations -Wno-write-strings \ 14 -Wno-deprecated-declarations -Wno-write-strings \
15 -Wno-narrowing -Wno-unused-result 15 -Wno-narrowing -Wno-unused-result
16 16
17# FD_SETSIZE has to be defined before winsock2.h and it has to be the same
18# number in every translation unit -- fd_set carries an array of that length,
19# so disagreeing about it means disagreeing about sizeof(fd_set). That is why
20# it lives here as a -D and not as a #define in a header. Windows defaults to
21# 64, which capped this node at 63 watchable peers no matter how many it had.
22CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ 17CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \
23 -DWIN32 -DNOPCH -DFD_SETSIZE=1024 \ 18 -DWIN32 -DNOPCH \
24 -DSECP256K1_STATIC \ 19 -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD \
25 -I$(DEPS)/include -I$(RANDOMX)/src \ 20 -I$(DEPS)/include -I$(RANDOMX)/src \
26 -I$(DEPS)/include/nlohmann \ 21 -I$(DEPS)/include/nlohmann \
27 -IC:/msys64/ucrt64/include \ 22 -IC:/msys64/ucrt64/include \
@@ -42,7 +37,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
42NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ 37NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \
43 obj/sha.o obj/nostr.o obj/randomx_pow.o \ 38 obj/sha.o obj/nostr.o obj/randomx_pow.o \
44 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 \
45 obj/selftest.o obj/bip32.o obj/walletcmd.o 40 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o
46 41
47GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o 42GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o
48 43
@@ -56,7 +51,7 @@ GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o
56HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 51HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \
57 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 \
58 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ 53 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \
59 selftest.h walletcmd.h sockcount.h compat.h font_roboto.h 54 selftest.h walletcmd.h sockcount.h proxy.h compat.h font_roboto.h
60 55
61all: bitflash.exe 56all: bitflash.exe
62 57
@@ -116,3 +111,5 @@ tests: test_bip32.exe bitflash.exe
116 ./bitflash.exe -selftest=wallet-encrypt 111 ./bitflash.exe -selftest=wallet-encrypt
117 ./bitflash.exe -selftest=net-message 112 ./bitflash.exe -selftest=net-message
118 ./bitflash.exe -selftest=consensus-limits 113 ./bitflash.exe -selftest=consensus-limits
114 ./bitflash.exe -selftest=pool-stratum
115 ./bitflash.exe -selftest=socks5-proxy
diff --git a/src/net.cpp b/src/net.cpp
index 34c608c..bb39122 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -13,15 +13,32 @@
13#include <thread> // hardware_concurrency, for the miner thread count 13#include <thread> // hardware_concurrency, for the miner thread count
14#ifdef _WIN32 14#ifdef _WIN32
15#include <winsock2.h> 15#include <winsock2.h>
16#else
17#include <poll.h>
16#endif 18#endif
17#include <openssl/rand.h> 19#include <openssl/rand.h>
18#include "btfaddr.h" 20#include "btfaddr.h"
19#include "btftunnel.h" 21#include "btftunnel.h"
22#include "proxy.h"
20 23
21void ThreadReconnectCachedBtfPeers(void* parg); 24void ThreadReconnectCachedBtfPeers(void* parg);
22void ThreadMessageHandler2(void* parg); 25void ThreadMessageHandler2(void* parg);
23void ThreadSocketHandler2(void* parg); 26void ThreadSocketHandler2(void* parg);
24 27
28#ifdef _WIN32
29typedef WSAPOLLFD BtfPollFd;
30static int BtfPoll(BtfPollFd* pfd, unsigned int nfd, int nTimeoutMs)
31{
32 return WSAPoll(pfd, nfd, nTimeoutMs);
33}
34#else
35typedef struct pollfd BtfPollFd;
36static int BtfPoll(BtfPollFd* pfd, unsigned int nfd, int nTimeoutMs)
37{
38 return poll(pfd, (nfds_t)nfd, nTimeoutMs);
39}
40#endif
41
25 42
26 43
27 44
@@ -153,7 +170,7 @@ static const char* pszSockSite[SOCK_SITES] = {
153 "external-ip probe", "listen socket", "inbound accept", 170 "external-ip probe", "listen socket", "inbound accept",
154 "rendezvous dial", "rendezvous listen", "rendezvous accept", 171 "rendezvous dial", "rendezvous listen", "rendezvous accept",
155 "loopback pair listener", "loopback pair app end", "loopback pair pump end", 172 "loopback pair listener", "loopback pair app end", "loopback pair pump end",
156 "nostr relay" 173 "nostr relay", "stratum bridge"
157}; 174};
158 175
159static string SockAccountingText() 176static string SockAccountingText()
@@ -274,8 +291,8 @@ string GetDiagnosticsText()
274 291
275 // The number that would have made the deafness obvious. Anything held but 292 // The number that would have made the deafness obvious. Anything held but
276 // not watched is a socket this node will never read again. 293 // not watched is a socket this node will never read again.
277 str += strprintf(" peers watched %d of %d that select() can hold%s\n", 294 str += strprintf(" peers watched %d of %d held by poll%s\n",
278 nPeersWatched, (int)FD_SETSIZE - 1, 295 nPeersWatched, nHeld,
279 nHeld > nPeersWatched ? " <-- NOT ALL PEERS ARE BEING READ" : ""); 296 nHeld > nPeersWatched ? " <-- NOT ALL PEERS ARE BEING READ" : "");
280 297
281 if (nBlocksReceived > 0) 298 if (nBlocksReceived > 0)
@@ -361,6 +378,9 @@ string GetDiagnosticsText()
361 378
362bool GetMyExternalIP(unsigned int& ipRet) 379bool GetMyExternalIP(unsigned int& ipRet)
363{ 380{
381 if (BtfSocks5ProxyEnabled())
382 return error("GetMyExternalIP() skipped while SOCKS5 proxy is enabled\n");
383
364 // Try several plain-text IP echo services in order. 384 // Try several plain-text IP echo services in order.
365 // Each returns just the IPv4 address as the first line of the HTTP body. 385 // Each returns just the IPv4 address as the first line of the HTTP body.
366 struct { const char* host; const char* path; } services[] = { 386 struct { const char* host; const char* path; } services[] = {
@@ -1127,7 +1147,7 @@ void CNode::Disconnect()
1127 1147
1128 // Invalidate before anything else can look at it. This used to close the 1148 // Invalidate before anything else can look at it. This used to close the
1129 // socket and leave the handle number sitting in the object, so ~CNode 1149 // socket and leave the handle number sitting in the object, so ~CNode
1130 // closed it a second time and the select loop kept arming it in the 1150 // closed it a second time and the socket loop kept arming it in the
1131 // meantime. See the note on ~CNode in net.h. 1151 // meantime. See the note on ~CNode in net.h.
1132 BtfCloseSocket(hSocket); 1152 BtfCloseSocket(hSocket);
1133 hSocket = INVALID_SOCKET; 1153 hSocket = INVALID_SOCKET;
@@ -1276,52 +1296,49 @@ void ThreadSocketHandler2(void* parg)
1276 // 1296 //
1277 // Find which sockets have data to receive 1297 // Find which sockets have data to receive
1278 // 1298 //
1279 struct timeval timeout; 1299 static const int nPollTimeoutMs = 50; // frequency to poll pnode->vSend
1280 timeout.tv_sec = 0; 1300
1281 timeout.tv_usec = 50000; // frequency to poll pnode->vSend 1301 struct NodePollResult
1282 1302 {
1283 fd_set fdsetRecv; 1303 CNode* pnode;
1284 fd_set fdsetSend; 1304 SOCKET hSocket;
1285 FD_ZERO(&fdsetRecv); 1305 bool fRecv;
1286 FD_ZERO(&fdsetSend); 1306 bool fSend;
1287 SOCKET hSocketMax = 0; 1307 bool fInvalid;
1288 FD_SET(hListenSocket, &fdsetRecv); 1308 NodePollResult(CNode* pnodeIn, SOCKET hSocketIn)
1289 hSocketMax = max(hSocketMax, hListenSocket); 1309 : pnode(pnodeIn), hSocket(hSocketIn), fRecv(false), fSend(false), fInvalid(false) {}
1290 // FD_SET drops silently once the set is full, so count what goes in 1310 };
1291 // and say so. A peer that never makes it into the set is never read, 1311
1292 // and that is indistinguishable from a peer with nothing to say -- 1312 vector<BtfPollFd> vPoll;
1293 // the failure this whole path used to hide. 1313 vector<NodePollResult> vPollNodes;
1294 unsigned int nWatched = 1; // the listen socket 1314
1315 BtfPollFd listenPoll;
1316 memset(&listenPoll, 0, sizeof(listenPoll));
1317 listenPoll.fd = hListenSocket;
1318 listenPoll.events = POLLIN;
1319 vPoll.push_back(listenPoll);
1320
1295 CRITICAL_BLOCK(cs_vNodes) 1321 CRITICAL_BLOCK(cs_vNodes)
1296 { 1322 {
1297 foreach(CNode* pnode, vNodes) 1323 foreach(CNode* pnode, vNodes)
1298 { 1324 {
1299 if (nWatched >= FD_SETSIZE)
1300 break;
1301 // A disconnected node stays in vNodes until its references go. 1325 // A disconnected node stays in vNodes until its references go.
1302 // Arming its closed handle asks select() to watch a number the 1326 // Do not watch a closed handle number: the OS may already have
1303 // operating system may already have given to somebody else. 1327 // handed it to a different connection.
1304 if (pnode->hSocket == INVALID_SOCKET) 1328 if (pnode->hSocket == INVALID_SOCKET)
1305 continue; 1329 continue;
1306 FD_SET(pnode->hSocket, &fdsetRecv); 1330
1307 nWatched++; 1331 BtfPollFd pfd;
1308 hSocketMax = max(hSocketMax, pnode->hSocket); 1332 memset(&pfd, 0, sizeof(pfd));
1333 pfd.fd = pnode->hSocket;
1334 pfd.events = POLLIN;
1309 TRY_CRITICAL_BLOCK(pnode->cs_vSend) 1335 TRY_CRITICAL_BLOCK(pnode->cs_vSend)
1310 if (!pnode->vSend.empty()) 1336 if (!pnode->vSend.empty())
1311 FD_SET(pnode->hSocket, &fdsetSend); 1337 pfd.events |= POLLOUT;
1312 } 1338 vPoll.push_back(pfd);
1313 if (vNodes.size() + 1 > (size_t)FD_SETSIZE) 1339 vPollNodes.push_back(NodePollResult(pnode, pnode->hSocket));
1314 {
1315 static int64 nLastWarned = 0;
1316 if (GetTime() - nLastWarned > 60)
1317 {
1318 nLastWarned = GetTime();
1319 printf("WARNING: %d peers but select() can only watch %d -- %d are not being read\n",
1320 (int)vNodes.size(), (int)FD_SETSIZE - 1,
1321 (int)vNodes.size() - (int)FD_SETSIZE + 1);
1322 }
1323 } 1340 }
1324 nPeersWatched = (int)nWatched - 1; // less the listen socket 1341 nPeersWatched = (int)vPollNodes.size();
1325 } 1342 }
1326 1343
1327 // Say the state of the node out loud now and then, so a log pulled off 1344 // Say the state of the node out loud now and then, so a log pulled off
@@ -1342,32 +1359,17 @@ void ThreadSocketHandler2(void* parg)
1342 } 1359 }
1343 1360
1344 vfThreadRunning[0] = false; 1361 vfThreadRunning[0] = false;
1345 int nSelect = select(hSocketMax + 1, &fdsetRecv, &fdsetSend, NULL, &timeout); 1362 int nPoll = BtfPoll(&vPoll[0], (unsigned int)vPoll.size(), nPollTimeoutMs);
1346 vfThreadRunning[0] = true; 1363 vfThreadRunning[0] = true;
1347 CheckForShutdown(0); 1364 CheckForShutdown(0);
1348 if (nSelect == SOCKET_ERROR) 1365 if (nPoll == SOCKET_ERROR)
1349 { 1366 {
1350 // This used to answer a failed select() by marking every
1351 // descriptor from 0 to hSocketMax ready, which is code written for
1352 // POSIX, where a descriptor is a small integer. A Windows SOCKET is
1353 // a kernel handle in the thousands, so the loop iterated over
1354 // numbers that are not sockets and told the rest of the function
1355 // that every peer had data. One bad handle in the set makes
1356 // select() fail on every pass, so a single closed socket put the
1357 // whole thread into that state permanently.
1358 //
1359 // Find the bad handle instead and let it be dropped.
1360 int nErr = WSAGetLastError(); 1367 int nErr = WSAGetLastError();
1361 LogPrint("net", "select failed: %d\n", nErr); 1368 LogPrint("net", "poll failed: %d\n", nErr);
1362 FD_ZERO(&fdsetRecv);
1363 FD_ZERO(&fdsetSend);
1364 CRITICAL_BLOCK(cs_vNodes) 1369 CRITICAL_BLOCK(cs_vNodes)
1365 { 1370 {
1366 foreach(CNode* pnode, vNodes) 1371 foreach(CNode* pnode, vNodes)
1367 { 1372 {
1368 // Already disconnected: its handle is gone on purpose, and
1369 // reporting it here would name the wrong node for a select()
1370 // failure it did not cause.
1371 if (pnode->hSocket == INVALID_SOCKET) 1373 if (pnode->hSocket == INVALID_SOCKET)
1372 continue; 1374 continue;
1373 int nType = 0; 1375 int nType = 0;
@@ -1376,24 +1378,27 @@ void ThreadSocketHandler2(void* parg)
1376#else 1378#else
1377 socklen_t nTypeLen = sizeof(nType); 1379 socklen_t nTypeLen = sizeof(nType);
1378#endif 1380#endif
1379 if (pnode->hSocket != INVALID_SOCKET && 1381 if (getsockopt(pnode->hSocket, SOL_SOCKET, SO_TYPE, (char*)&nType, &nTypeLen) != 0)
1380 getsockopt(pnode->hSocket, SOL_SOCKET, SO_TYPE, (char*)&nType, &nTypeLen) != 0)
1381 { 1382 {
1382 printf("dropping node %s: its socket is no longer valid\n", 1383 printf("dropping node %s: its socket is no longer valid\n",
1383 pnode->addr.ToString().c_str()); 1384 pnode->addr.ToString().c_str());
1384 pnode->fDisconnect = true; 1385 pnode->fDisconnect = true;
1385 1386
1386 // Stop referring to it, or it goes back into the next 1387 // Stop referring to it, or it goes into the next poll
1387 // select() set and fails it again. 1388 // set and fails it again.
1388 // 1389 //
1389 // Marking fDisconnect is not enough: a node is only 1390 // Marking fDisconnect is not enough: a node is only
1390 // removed from vNodes once it has drained or once 1391 // removed from vNodes once it has drained or once
1391 // DISCONNECT_DRAIN_SECS has passed, so one dead 1392 // DISCONNECT_DRAIN_SECS has passed, so one dead
1392 // descriptor kept failing select() for a full minute -- 1393 // descriptor kept failing the wait for a full minute --
1393 // and a failed select() zeroes both sets above, which 1394 // and a failed wait skips the service loop below
1394 // means NO peer is read on that pass. Measured on a 1395 // entirely, which means NO peer is read on that pass.
1395 // wedged pool: 1,027 consecutive passes over 61 1396 // Measured on a wedged pool under the old select()
1396 // seconds, one bad socket, the entire node deaf. 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.
1397 // 1402 //
1398 // Not closed, only forgotten. getsockopt just told us 1403 // Not closed, only forgotten. getsockopt just told us
1399 // this descriptor is not ours any more; closing it now 1404 // this descriptor is not ours any more; closing it now
@@ -1402,7 +1407,17 @@ void ThreadSocketHandler2(void* parg)
1402 } 1407 }
1403 } 1408 }
1404 } 1409 }
1405 Sleep(timeout.tv_usec/1000); 1410 Sleep(nPollTimeoutMs);
1411 }
1412 else
1413 {
1414 for (size_t i = 0; i < vPollNodes.size(); i++)
1415 {
1416 short revents = vPoll[i + 1].revents; // poll[0] is the listener
1417 vPollNodes[i].fRecv = (revents & (POLLIN | POLLERR | POLLHUP)) != 0;
1418 vPollNodes[i].fSend = (revents & POLLOUT) != 0;
1419 vPollNodes[i].fInvalid = (revents & POLLNVAL) != 0;
1420 }
1406 } 1421 }
1407 RandAddSeed(); 1422 RandAddSeed();
1408 1423
@@ -1418,7 +1433,7 @@ void ThreadSocketHandler2(void* parg)
1418 // 1433 //
1419 // Accept new connections 1434 // Accept new connections
1420 // 1435 //
1421 if (FD_ISSET(hListenSocket, &fdsetRecv)) 1436 if (nPoll != SOCKET_ERROR && (vPoll[0].revents & POLLIN))
1422 { 1437 {
1423 struct sockaddr_in sockaddr; 1438 struct sockaddr_in sockaddr;
1424#ifdef _WIN32 1439#ifdef _WIN32
@@ -1437,7 +1452,7 @@ void ThreadSocketHandler2(void* parg)
1437 { 1452 {
1438 // Refuse rather than accept a connection this node cannot 1453 // Refuse rather than accept a connection this node cannot
1439 // watch. Without this the listen socket kept taking peers -- 1454 // watch. Without this the listen socket kept taking peers --
1440 // it is always in the select set -- while everything past the 1455 // it is always watched -- while everything past the
1441 // limit sat open and unread, which fed on itself: the deafer 1456 // limit sat open and unread, which fed on itself: the deafer
1442 // the node got, the more connections it collected. 1457 // the node got, the more connections it collected.
1443 unsigned int nNodes = 0; 1458 unsigned int nNodes = 0;
@@ -1464,22 +1479,28 @@ void ThreadSocketHandler2(void* parg)
1464 // 1479 //
1465 // Service each socket 1480 // Service each socket
1466 // 1481 //
1467 vector<CNode*> vNodesCopy; 1482 for (size_t i = 0; i < vPollNodes.size(); i++)
1468 CRITICAL_BLOCK(cs_vNodes)
1469 vNodesCopy = vNodes;
1470 foreach(CNode* pnode, vNodesCopy)
1471 { 1483 {
1472 CheckForShutdown(0); 1484 CheckForShutdown(0);
1473 SOCKET hSocket = pnode->hSocket; 1485 NodePollResult& poll = vPollNodes[i];
1474 // Forgotten above because it was already invalid. FD_ISSET on it 1486 CNode* pnode = poll.pnode;
1475 // is meaningless and recv()/send() would be operating on -1. 1487 SOCKET hSocket = poll.hSocket;
1476 if (hSocket == INVALID_SOCKET) 1488
1489 if (hSocket == INVALID_SOCKET || pnode->hSocket != hSocket)
1477 continue; 1490 continue;
1478 1491
1492 if (poll.fInvalid)
1493 {
1494 LogPrint("net", "poll reported invalid socket for %s\n",
1495 pnode->addr.ToString().c_str());
1496 pnode->fDisconnect = true;
1497 continue;
1498 }
1499
1479 // 1500 //
1480 // Receive 1501 // Receive
1481 // 1502 //
1482 if (FD_ISSET(hSocket, &fdsetRecv)) 1503 if (poll.fRecv)
1483 { 1504 {
1484 TRY_CRITICAL_BLOCK(pnode->cs_vRecv) 1505 TRY_CRITICAL_BLOCK(pnode->cs_vRecv)
1485 { 1506 {
@@ -1519,7 +1540,7 @@ void ThreadSocketHandler2(void* parg)
1519 // 1540 //
1520 // Send 1541 // Send
1521 // 1542 //
1522 if (FD_ISSET(hSocket, &fdsetSend)) 1543 if (poll.fSend)
1523 { 1544 {
1524 TRY_CRITICAL_BLOCK(pnode->cs_vSend) 1545 TRY_CRITICAL_BLOCK(pnode->cs_vSend)
1525 { 1546 {
diff --git a/src/net.h b/src/net.h
index 3c8c409..653a015 100644
--- a/src/net.h
+++ b/src/net.h
@@ -95,19 +95,16 @@ static const unsigned int MAX_PROTOCOL_MESSAGE_SIZE = MAX_SIZE;
95 95
96// Ceiling on simultaneous connections. 96// Ceiling on simultaneous connections.
97// 97//
98// ThreadSocketHandler watches every peer through one select(), and select() 98// ThreadSocketHandler watches every peer in one socket loop. This used to be a
99// cannot watch more descriptors than FD_SETSIZE. On Windows FD_SET simply 99// select() loop, which made FD_SETSIZE the real limit and silently starved
100// stops adding once the set is full -- no error, no log -- so every socket 100// sockets past that bitmap. It is now poll()/WSAPoll(), so the ceiling below is
101// past the limit stays open and is never read again. Nothing capped vNodes, 101// a deliberate resource bound rather than a descriptor-set accident.
102// so a node accepted connections it had no way to service, and because new
103// entries go on the end of vNodes it was always the newest connection that
104// starved, including the ones dialed to fix connectivity.
105// 102//
106// Measured on two nodes before this ceiling existed: 277 and 90 CNode objects 103// Measured on two nodes before this ceiling existed: 277 and 90 CNode objects
107// against 93 and 90 live sockets, and a status bar reporting hundreds of 104// against 93 and 90 live sockets, and a status bar reporting hundreds of
108// peers for a node that was really talking to thirty. 105// peers for a node that was really talking to thirty.
109// 106//
110// 125 is Bitcoin's number and sits far below FD_SETSIZE on both platforms. 107// 125 is Bitcoin's number and remains a conservative public-node default.
111static const unsigned int MAX_CONNECTIONS = 125; 108static const unsigned int MAX_CONNECTIONS = 125;
112 109
113// A node marked for disconnect is normally held until its buffers drain, so 110// A node marked for disconnect is normally held until its buffers drain, so
@@ -175,7 +172,7 @@ static const int BTF_BEHIND_WARN_INTERVAL_SECS = 5 * 60;
175// 172//
176// These counters exist so it can. Cheap to keep, and they name the failures 173// These counters exist so it can. Cheap to keep, and they name the failures
177// that actually happened rather than the ones that sound impressive. 174// that actually happened rather than the ones that sound impressive.
178extern int nPeersWatched; // peers in the last select() set 175extern int nPeersWatched; // peers in the last poll() set
179extern int64 nBlocksReceived; // blocks handed to ProcessBlock 176extern int64 nBlocksReceived; // blocks handed to ProcessBlock
180extern int64 nBlocksWithoutParent; // ...of those, how many arrived orphaned 177extern int64 nBlocksWithoutParent; // ...of those, how many arrived orphaned
181 178
diff --git a/src/nostr.cpp b/src/nostr.cpp
index 3f43302..32cea18 100644
--- a/src/nostr.cpp
+++ b/src/nostr.cpp
@@ -7,6 +7,10 @@
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/x509.h>
10#include <openssl/x509v3.h>
11#ifdef _WIN32
12#include <wincrypt.h>
13#endif
10// We link libsecp256k1 statically (without this the header assumes DLL import). 14// We link libsecp256k1 statically (without this the header assumes DLL import).
11// Both makefiles also pass -DSECP256K1_STATIC; the guard keeps that from 15// Both makefiles also pass -DSECP256K1_STATIC; the guard keeps that from
12// warning while leaving this file correct if built without the flag. 16// warning while leaving this file correct if built without the flag.
@@ -26,6 +30,7 @@
26 30
27#include "btfaddr.h" 31#include "btfaddr.h"
28#include "btfchan.h" 32#include "btfchan.h"
33#include "proxy.h"
29 34
30using json = nlohmann::json; 35using json = nlohmann::json;
31 36
@@ -468,6 +473,41 @@ static bool BuildSignedEvent(CNostrKey& key, int kind, const json& tags,
468static const uint64 MAX_WS_FRAME = 1 << 20; // 1 MiB per frame 473static const uint64 MAX_WS_FRAME = 1 << 20; // 1 MiB per frame
469static const uint64 MAX_WS_MESSAGE = 4 << 20; // 4 MiB reassembled 474static const uint64 MAX_WS_MESSAGE = 4 << 20; // 4 MiB reassembled
470 475
476static int BtfConnectedNodeCount()
477{
478 int nNodes = 0;
479 CRITICAL_BLOCK(cs_vNodes)
480 {
481 nNodes = (int)vNodes.size();
482 }
483 return nNodes;
484}
485
486#ifdef _WIN32
487static bool BtfLoadWindowsRootCertificates(SSL_CTX* sslctx)
488{
489 HCERTSTORE hStore = CertOpenSystemStoreA(0, "ROOT");
490 if (!hStore)
491 return false;
492
493 bool fLoaded = false;
494 X509_STORE* x509store = SSL_CTX_get_cert_store(sslctx);
495 PCCERT_CONTEXT pCert = NULL;
496 while ((pCert = CertEnumCertificatesInStore(hStore, pCert)) != NULL)
497 {
498 const unsigned char* p = pCert->pbCertEncoded;
499 X509* x509 = d2i_X509(NULL, &p, pCert->cbCertEncoded);
500 if (!x509)
501 continue;
502 if (X509_STORE_add_cert(x509store, x509) == 1)
503 fLoaded = true;
504 X509_free(x509);
505 }
506 CertCloseStore(hStore, 0);
507 return fLoaded;
508}
509#endif
510
471class CWebSocket 511class CWebSocket
472{ 512{
473public: 513public:
@@ -523,53 +563,8 @@ public:
523 size_t colon = host.find(':'); 563 size_t colon = host.find(':');
524 if (colon != string::npos) { port = atoi(host.substr(colon + 1).c_str()); host = host.substr(0, colon); } 564 if (colon != string::npos) { port = atoi(host.substr(colon + 1).c_str()); host = host.substr(0, colon); }
525 565
526 // Resolve and connect (TCP). AF_UNSPEC so we get both A and AAAA 566 hSocket = BtfConnectSocket(host, (unsigned short)port, SOCK_NOSTR, timeoutSec);
527 // records back and try them in the order the resolver prefers, 567 if (hSocket == INVALID_SOCKET)
528 // rather than hard-failing whenever the IPv4 path to a relay is
529 // filtered/rate-limited (increasingly common for CDN-fronted
530 // relays) while a working IPv6 route exists right there in the
531 // same getaddrinfo() result.
532 struct addrinfo hints, *res = NULL, *rp = NULL;
533 memset(&hints, 0, sizeof(hints));
534 hints.ai_family = AF_UNSPEC;
535 hints.ai_socktype = SOCK_STREAM;
536 char portstr[16]; sprintf(portstr, "%d", port);
537 if (getaddrinfo(host.c_str(), portstr, &hints, &res) != 0 || !res)
538 return error("Nostr: getaddrinfo %s failed", host.c_str());
539
540 bool fConnected = false;
541 for (rp = res; rp != NULL; rp = rp->ai_next)
542 {
543 hSocket = BtfSocketTag(socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol), SOCK_NOSTR);
544 if (hSocket == INVALID_SOCKET)
545 continue;
546
547 // Socket receive/send timeout: Windows takes a DWORD of
548 // milliseconds; POSIX takes a struct timeval.
549#ifdef _WIN32
550 DWORD tv = timeoutSec * 1000;
551 setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv));
552 setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv));
553#else
554 struct timeval tv;
555 tv.tv_sec = timeoutSec;
556 tv.tv_usec = 0;
557 setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv));
558 setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv));
559#endif
560
561 if (connect(hSocket, rp->ai_addr, (int)rp->ai_addrlen) == 0)
562 {
563 fConnected = true;
564 break;
565 }
566
567 BtfCloseSocket(hSocket);
568 hSocket = INVALID_SOCKET;
569 }
570 freeaddrinfo(res);
571
572 if (!fConnected)
573 return error("Nostr: connect %s:%d failed", host.c_str(), port); 568 return error("Nostr: connect %s:%d failed", host.c_str(), port);
574 569
575 // TLS 570 // TLS
@@ -585,22 +580,35 @@ public:
585 // so a MITM cannot forge them, but it could drop the rendezvous 580 // so a MITM cannot forge them, but it could drop the rendezvous
586 // registration (leaving the pool unreachable to miners) or answer 581 // registration (leaving the pool unreachable to miners) or answer
587 // subscriptions with whatever it liked. 582 // subscriptions with whatever it liked.
588 if (!SSL_CTX_set_default_verify_paths(sslctx))
589 return error("Nostr: no system CA store available, refusing %s", host.c_str());
590 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.
588 bool fTrustLoaded = (SSL_CTX_set_default_verify_paths(sslctx) == 1);
589#ifdef _WIN32
590 fTrustLoaded = BtfLoadWindowsRootCertificates(sslctx) || fTrustLoaded;
591#endif
592 if (!fTrustLoaded)
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.
591 SSL_CTX_set_min_proto_version(sslctx, TLS1_2_VERSION); 598 SSL_CTX_set_min_proto_version(sslctx, TLS1_2_VERSION);
592 599
593 ssl = SSL_new(sslctx); 600 ssl = SSL_new(sslctx);
594 if (!ssl) return false; 601 if (!ssl) return false;
595 SSL_set_fd(ssl, (int)hSocket); 602 SSL_set_fd(ssl, (int)hSocket);
596 SSL_set_tlsext_host_name(ssl, host.c_str()); // SNI 603 if (SSL_set_tlsext_host_name(ssl, host.c_str()) != 1) // SNI
604 return error("Nostr: failed to set TLS SNI for %s", host.c_str());
597 605
598 // Verifying the chain is not enough on its own: without this the 606 // Verifying the chain is not enough on its own: without this the
599 // relay could present any certificate a trusted CA ever issued, 607 // relay could present any certificate a trusted CA ever issued,
600 // for any name. SSL_set1_host binds the check to the host we asked 608 // for any name. SSL_set1_host binds the check to the host we asked
601 // for. 609 // for.
602 if (!SSL_set1_host(ssl, host.c_str())) 610 if (SSL_set1_host(ssl, host.c_str()) != 1)
603 return error("Nostr: cannot pin verification to %s", host.c_str()); 611 return error("Nostr: failed to set TLS hostname for %s", host.c_str());
604 612
605 if (SSL_connect(ssl) != 1) 613 if (SSL_connect(ssl) != 1)
606 { 614 {
@@ -1071,6 +1079,7 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1071 json filter = json::object(); 1079 json filter = json::object();
1072 filter["authors"] = json::array({ HexEncode(pk, 32) }); 1080 filter["authors"] = json::array({ HexEncode(pk, 32) });
1073 filter["kinds"] = json::array({ BTF_DESC_KIND }); 1081 filter["kinds"] = json::array({ BTF_DESC_KIND });
1082 filter["#d"] = json::array({ BTF_DESC_DTAG });
1074 filter["limit"] = 1; 1083 filter["limit"] = 1;
1075 json req = json::array({ "REQ", "btf-resolve", filter }); 1084 json req = json::array({ "REQ", "btf-resolve", filter });
1076 if (!ws.SendText(req.dump())) 1085 if (!ws.SendText(req.dump()))
@@ -1207,6 +1216,7 @@ bool BtfResolveMany(const std::vector<std::string>& btfAddrs,
1207 json filter = json::object(); 1216 json filter = json::object();
1208 filter["authors"] = authors; 1217 filter["authors"] = authors;
1209 filter["kinds"] = json::array({ BTF_DESC_KIND }); 1218 filter["kinds"] = json::array({ BTF_DESC_KIND });
1219 filter["#d"] = json::array({ BTF_DESC_DTAG });
1210 filter["limit"] = (int)authors.size(); 1220 filter["limit"] = (int)authors.size();
1211 json req = json::array({ "REQ", "btf-resolve-many", filter }); 1221 json req = json::array({ "REQ", "btf-resolve-many", filter });
1212 if (!ws.SendText(req.dump())) 1222 if (!ws.SendText(req.dump()))
@@ -1617,8 +1627,9 @@ static void ConnectDiscoveredBtfPeers()
1617 // plus a random sample, not one or the other. 1627 // plus a random sample, not one or the other.
1618 int64 now = GetTime(); 1628 int64 now = GetTime();
1619 int nBudget = (int)BTF_DIALS_PER_PASS; 1629 int nBudget = (int)BTF_DIALS_PER_PASS;
1630 int nNodeCount = BtfConnectedNodeCount();
1620 { 1631 {
1621 int nRoom = (int)BTF_TARGET_CONN - (int)vNodes.size(); 1632 int nRoom = (int)BTF_TARGET_CONN - nNodeCount;
1622 if (nRoom < nBudget) nBudget = nRoom; 1633 if (nRoom < nBudget) nBudget = nRoom;
1623 } 1634 }
1624 if (nBudget < 0) nBudget = 0; 1635 if (nBudget < 0) nBudget = 0;
@@ -1632,7 +1643,6 @@ static void ConnectDiscoveredBtfPeers()
1632 // Reliability slice: walk the tiered (good -> untried -> bad) order. 1643 // Reliability slice: walk the tiered (good -> untried -> bad) order.
1633 for (const string& addr : peers) 1644 for (const string& addr : peers)
1634 { 1645 {
1635 if ((int)vNodes.size() >= (int)BTF_TARGET_CONN) break;
1636 if ((int)candidates.size() >= nSortedSlots) break; 1646 if ((int)candidates.size() >= nSortedSlots) break;
1637 map<string, int64>::iterator bi = g_btfPeerBackoff.find(addr); 1647 map<string, int64>::iterator bi = g_btfPeerBackoff.find(addr);
1638 if (bi != g_btfPeerBackoff.end() && now < bi->second) 1648 if (bi != g_btfPeerBackoff.end() && now < bi->second)
@@ -1657,7 +1667,7 @@ static void ConnectDiscoveredBtfPeers()
1657 int nWant = min(nRandomSlots, (int)pool.size()); 1667 int nWant = min(nRandomSlots, (int)pool.size());
1658 for (int i = 0; i < nWant; i++) 1668 for (int i = 0; i < nWant; i++)
1659 { 1669 {
1660 if ((int)vNodes.size() + (int)candidates.size() >= (int)BTF_TARGET_CONN) break; 1670 if (nNodeCount + (int)candidates.size() >= (int)BTF_TARGET_CONN) break;
1661 if ((int)candidates.size() >= nBudget) break; 1671 if ((int)candidates.size() >= nBudget) break;
1662 size_t j = i + (size_t)GetRand((uint64)(pool.size() - i)); 1672 size_t j = i + (size_t)GetRand((uint64)(pool.size() - i));
1663 std::swap(pool[i], pool[j]); 1673 std::swap(pool[i], pool[j]);
@@ -1712,12 +1722,10 @@ static void ConnectDiscoveredBtfPeers()
1712 // not the shared Nostr relays, so there's no thundering-herd effect here. 1722 // not the shared Nostr relays, so there's no thundering-herd effect here.
1713 // Each thread writes its result back via shared state guarded by cs_btfPeerState. 1723 // Each thread writes its result back via shared state guarded by cs_btfPeerState.
1714 struct DialCtx { string addr; BtfResolvedPeer rp; }; 1724 struct DialCtx { string addr; BtfResolvedPeer rp; };
1715 vector<DialCtx*> ctxs;
1716 for (const string& addr : toDial) 1725 for (const string& addr : toDial)
1717 { 1726 {
1718 DialCtx* ctx = new DialCtx{addr, resolved[addr]}; 1727 DialCtx* ctx = new DialCtx{addr, resolved[addr]};
1719 ctxs.push_back(ctx); 1728 uintptr_t tid = _beginthread([](void* arg) {
1720 _beginthread([](void* arg) {
1721 DialCtx* ctx = (DialCtx*)arg; 1729 DialCtx* ctx = (DialCtx*)arg;
1722 string addr = ctx->addr; 1730 string addr = ctx->addr;
1723 string meeting = ctx->rp.meetingHostPort; 1731 string meeting = ctx->rp.meetingHostPort;
@@ -1750,6 +1758,21 @@ static void ConnectDiscoveredBtfPeers()
1750 } 1758 }
1751 } 1759 }
1752 }, 0, ctx); 1760 }, 0, ctx);
1761 if (tid == (uintptr_t)-1)
1762 {
1763 LogPrint("nostr", "Nostr: failed to create .btf dial thread for %s\n", addr.c_str());
1764 CRITICAL_BLOCK(cs_btfPeerState)
1765 {
1766 int n = ++g_btfPeerFails[addr];
1767 int64 cap = (n >= BTF_CHRONIC_FAIL_THRESHOLD) ? BTF_BACKOFF_CAP_SECS_DEAD
1768 : BTF_BACKOFF_CAP_SECS;
1769 int64 secs = 30;
1770 for (int k = 1; k < n && secs < cap; k++) secs *= 2;
1771 if (secs > cap) secs = cap;
1772 g_btfPeerBackoff[addr] = GetTime() + secs;
1773 }
1774 delete ctx;
1775 }
1753 } 1776 }
1754 1777
1755 // Wait long enough for all parallel dials to finish (each has a 10s socket 1778 // Wait long enough for all parallel dials to finish (each has a 10s socket
@@ -1852,7 +1875,7 @@ void ThreadNostrSeed(void* parg)
1852 1875
1853 // Re-discover periodically. Sleep less aggressively when 1876 // Re-discover periodically. Sleep less aggressively when
1854 // we have no connections yet so we find peers faster on startup. 1877 // we have no connections yet so we find peers faster on startup.
1855 int nSleepSecs = vNodes.empty() ? 20 : 60; 1878 int nSleepSecs = (BtfConnectedNodeCount() == 0) ? 20 : 60;
1856 for (int i = 0; i < nSleepSecs && !fShutdown; i++) 1879 for (int i = 0; i < nSleepSecs && !fShutdown; i++)
1857 Sleep(1000); 1880 Sleep(1000);
1858 } 1881 }
diff --git a/src/proxy.cpp b/src/proxy.cpp
new file mode 100644
index 0000000..7d30911
--- /dev/null
+++ b/src/proxy.cpp
@@ -0,0 +1,377 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3
4#include "headers_core.h"
5#include "proxy.h"
6#include "sockcount.h"
7
8#include <errno.h>
9
10static CCriticalSection cs_socks5;
11static bool g_fSocks5Proxy = false;
12static bool g_fTorProxy = false;
13static std::string g_socks5Host;
14static unsigned short g_socks5Port = 0;
15static const char* DEFAULT_TOR_SOCKS5_PROXY = "127.0.0.1:9050";
16
17static void SetSocketTimeout(SOCKET s, int nTimeoutSecs)
18{
19 if (nTimeoutSecs <= 0)
20 return;
21#ifdef _WIN32
22 DWORD tv = (DWORD)nTimeoutSecs * 1000;
23 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
24 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv));
25#else
26 struct timeval tv;
27 tv.tv_sec = nTimeoutSecs;
28 tv.tv_usec = 0;
29 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
30 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
31#endif
32}
33
34static bool ReadN(SOCKET s, void* buf, int n)
35{
36 char* p = (char*)buf;
37 int off = 0;
38 while (off < n)
39 {
40 int r = recv(s, p + off, n - off, 0);
41 if (r <= 0)
42 return false;
43 off += r;
44 }
45 return true;
46}
47
48static bool WriteN(SOCKET s, const void* buf, int n)
49{
50 const char* p = (const char*)buf;
51 int off = 0;
52 while (off < n)
53 {
54 int r = send(s, p + off, n - off, 0);
55 if (r <= 0)
56 return false;
57 off += r;
58 }
59 return true;
60}
61
62static bool ValidProxyHostChar(char c)
63{
64 return isalnum((unsigned char)c) || c == '.' || c == '-' || c == '_';
65}
66
67static bool ValidProxyIpv6HostChar(char c)
68{
69 return isxdigit((unsigned char)c) || c == ':' || c == '.';
70}
71
72static std::string LowerProxyHost(std::string host)
73{
74 if (!host.empty() && host[host.size() - 1] == '.')
75 host.erase(host.size() - 1);
76 for (size_t i = 0; i < host.size(); i++)
77 host[i] = (char)tolower((unsigned char)host[i]);
78 return host;
79}
80
81bool BtfIsTorOnionHost(const std::string& host)
82{
83 std::string lower = LowerProxyHost(host);
84 static const char* suffix = ".onion";
85 size_t suffixLen = strlen(suffix);
86 return lower.size() > suffixLen &&
87 lower.compare(lower.size() - suffixLen, suffixLen, suffix) == 0;
88}
89
90static bool ParsePort(const std::string& port, unsigned short& portOut,
91 std::string& errOut)
92{
93 if (port.empty())
94 {
95 errOut = "port is empty";
96 return false;
97 }
98 for (size_t i = 0; i < port.size(); i++)
99 {
100 if (!isdigit((unsigned char)port[i]))
101 {
102 errOut = "port is not numeric";
103 return false;
104 }
105 }
106
107 errno = 0;
108 char* end = NULL;
109 long nPort = strtol(port.c_str(), &end, 10);
110 if (errno == ERANGE || end == NULL || *end != '\0' || nPort <= 0 || nPort > 65535)
111 {
112 errOut = "port is out of range";
113 return false;
114 }
115 portOut = (unsigned short)nPort;
116 return true;
117}
118
119bool BtfParseSocks5Proxy(const std::string& spec, std::string& hostOut,
120 unsigned short& portOut, std::string& errOut)
121{
122 hostOut.clear();
123 portOut = 0;
124 errOut.clear();
125
126 std::string host;
127 std::string port;
128 bool fBracketedIpv6 = false;
129 if (!spec.empty() && spec[0] == '[')
130 {
131 size_t close = spec.find(']');
132 if (close == std::string::npos || close == 1 || close + 1 >= spec.size() || spec[close + 1] != ':')
133 {
134 errOut = "expected [IPv6]:PORT";
135 return false;
136 }
137 host = spec.substr(1, close - 1);
138 port = spec.substr(close + 2);
139 fBracketedIpv6 = true;
140 }
141 else
142 {
143 size_t colon = spec.rfind(':');
144 if (colon == std::string::npos || colon == 0 || colon + 1 >= spec.size())
145 {
146 errOut = "expected HOST:PORT";
147 return false;
148 }
149 host = spec.substr(0, colon);
150 port = spec.substr(colon + 1);
151 if (host.find(':') != std::string::npos)
152 {
153 errOut = "IPv6 proxy hosts must use [ADDR]:PORT";
154 return false;
155 }
156 }
157
158 if (host.size() > 255)
159 {
160 errOut = "host is too long";
161 return false;
162 }
163 if (fBracketedIpv6)
164 {
165 bool fHasColon = false;
166 for (size_t i = 0; i < host.size(); i++)
167 {
168 fHasColon = fHasColon || host[i] == ':';
169 if (!ValidProxyIpv6HostChar(host[i]))
170 {
171 errOut = "IPv6 proxy host contains unsupported characters";
172 return false;
173 }
174 }
175 if (!fHasColon)
176 {
177 errOut = "bracketed proxy host is not IPv6";
178 return false;
179 }
180 }
181 else
182 {
183 for (size_t i = 0; i < host.size(); i++)
184 {
185 if (!ValidProxyHostChar(host[i]))
186 {
187 errOut = "host contains unsupported characters";
188 return false;
189 }
190 }
191 }
192 if (!ParsePort(port, portOut, errOut))
193 return false;
194
195 hostOut = host;
196 return true;
197}
198
199static bool SetSocks5Proxy(const std::string& spec, bool fTorMode,
200 std::string& errOut)
201{
202 std::string host;
203 unsigned short port = 0;
204 if (!BtfParseSocks5Proxy(spec, host, port, errOut))
205 return false;
206 CRITICAL_BLOCK(cs_socks5)
207 {
208 g_socks5Host = host;
209 g_socks5Port = port;
210 g_fSocks5Proxy = true;
211 g_fTorProxy = fTorMode;
212 }
213 return true;
214}
215
216bool BtfSetSocks5Proxy(const std::string& spec, std::string& errOut)
217{
218 return SetSocks5Proxy(spec, false, errOut);
219}
220
221bool BtfEnableTorProxy(const std::string& spec, std::string& errOut)
222{
223 return SetSocks5Proxy(spec.empty() ? DEFAULT_TOR_SOCKS5_PROXY : spec, true, errOut);
224}
225
226void BtfClearSocks5Proxy()
227{
228 CRITICAL_BLOCK(cs_socks5)
229 {
230 g_fSocks5Proxy = false;
231 g_fTorProxy = false;
232 g_socks5Host.clear();
233 g_socks5Port = 0;
234 }
235}
236
237bool BtfSocks5ProxyEnabled()
238{
239 CRITICAL_BLOCK(cs_socks5)
240 return g_fSocks5Proxy;
241 return false;
242}
243
244bool BtfTorProxyEnabled()
245{
246 CRITICAL_BLOCK(cs_socks5)
247 return g_fSocks5Proxy && g_fTorProxy;
248 return false;
249}
250
251std::string BtfSocks5ProxyName()
252{
253 CRITICAL_BLOCK(cs_socks5)
254 {
255 if (!g_fSocks5Proxy)
256 return "";
257 if (g_socks5Host.find(':') != std::string::npos)
258 return strprintf("[%s]:%u", g_socks5Host.c_str(), (unsigned)g_socks5Port);
259 return strprintf("%s:%u", g_socks5Host.c_str(), (unsigned)g_socks5Port);
260 }
261 return "";
262}
263
264static SOCKET ConnectDirect(const std::string& host, unsigned short port,
265 int nSockSite, int nTimeoutSecs)
266{
267 struct addrinfo hints, *res = NULL, *rp = NULL;
268 memset(&hints, 0, sizeof(hints));
269 hints.ai_family = AF_UNSPEC;
270 hints.ai_socktype = SOCK_STREAM;
271 char portstr[16];
272 sprintf(portstr, "%u", (unsigned)port);
273 if (getaddrinfo(host.c_str(), portstr, &hints, &res) != 0 || !res)
274 return INVALID_SOCKET;
275
276 SOCKET s = INVALID_SOCKET;
277 for (rp = res; rp != NULL; rp = rp->ai_next)
278 {
279 s = BtfSocketTag(socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol), nSockSite);
280 if (s == INVALID_SOCKET)
281 continue;
282 SetSocketTimeout(s, nTimeoutSecs);
283 if (connect(s, rp->ai_addr, (int)rp->ai_addrlen) == 0)
284 break;
285 BtfCloseSocket(s);
286 s = INVALID_SOCKET;
287 }
288 freeaddrinfo(res);
289 return s;
290}
291
292static bool Socks5Connect(SOCKET s, const std::string& destHost,
293 unsigned short destPort)
294{
295 if (destHost.empty() || destHost.size() > 255)
296 return false;
297
298 unsigned char hello[3] = { 0x05, 0x01, 0x00 }; // SOCKS5, one method, no auth
299 if (!WriteN(s, hello, sizeof(hello)))
300 return false;
301 unsigned char choice[2] = { 0, 0 };
302 if (!ReadN(s, choice, sizeof(choice)) || choice[0] != 0x05 || choice[1] != 0x00)
303 return false;
304
305 std::vector<unsigned char> req;
306 req.reserve(7 + destHost.size());
307 req.push_back(0x05); // version
308 req.push_back(0x01); // CONNECT
309 req.push_back(0x00); // reserved
310 req.push_back(0x03); // domain name; proxy resolves, avoiding local DNS leak
311 req.push_back((unsigned char)destHost.size());
312 req.insert(req.end(), destHost.begin(), destHost.end());
313 req.push_back((unsigned char)(destPort >> 8));
314 req.push_back((unsigned char)(destPort & 0xff));
315 if (!WriteN(s, &req[0], (int)req.size()))
316 return false;
317
318 unsigned char rep[4] = {0,0,0,0};
319 if (!ReadN(s, rep, sizeof(rep)) || rep[0] != 0x05)
320 return false;
321 if (rep[1] != 0x00)
322 {
323 error("SOCKS5 CONNECT to %s:%u failed with reply 0x%02x\n",
324 destHost.c_str(), (unsigned)destPort, (unsigned)rep[1]);
325 return false;
326 }
327 int nAddr = 0;
328 if (rep[3] == 0x01) nAddr = 4;
329 else if (rep[3] == 0x04) nAddr = 16;
330 else if (rep[3] == 0x03)
331 {
332 unsigned char len = 0;
333 if (!ReadN(s, &len, 1))
334 return false;
335 nAddr = len;
336 }
337 else
338 return false;
339
340 std::vector<unsigned char> discard(nAddr + 2);
341 return ReadN(s, &discard[0], (int)discard.size());
342}
343
344SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort,
345 int nSockSite, int nTimeoutSecs)
346{
347 std::string proxyHost;
348 unsigned short proxyPort = 0;
349 bool fProxy = false;
350 CRITICAL_BLOCK(cs_socks5)
351 {
352 fProxy = g_fSocks5Proxy;
353 proxyHost = g_socks5Host;
354 proxyPort = g_socks5Port;
355 }
356
357 if (!fProxy)
358 {
359 if (BtfIsTorOnionHost(destHost))
360 {
361 error("Refusing direct .onion connection to %s:%u; use /tor or /socks\n",
362 destHost.c_str(), (unsigned)destPort);
363 return INVALID_SOCKET;
364 }
365 return ConnectDirect(destHost, destPort, nSockSite, nTimeoutSecs);
366 }
367
368 SOCKET s = ConnectDirect(proxyHost, proxyPort, nSockSite, nTimeoutSecs);
369 if (s == INVALID_SOCKET)
370 return INVALID_SOCKET;
371 if (!Socks5Connect(s, destHost, destPort))
372 {
373 BtfCloseSocket(s);
374 return INVALID_SOCKET;
375 }
376 return s;
377}
diff --git a/src/proxy.h b/src/proxy.h
new file mode 100644
index 0000000..6429cf2
--- /dev/null
+++ b/src/proxy.h
@@ -0,0 +1,34 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3//
4// Optional SOCKS5 outbound proxy support. This is transport plumbing only:
5// no consensus, wallet, mining, or .btf identity data depends on it.
6
7#ifndef BITFLASH_PROXY_H
8#define BITFLASH_PROXY_H
9
10#ifdef _WIN32
11#ifndef WIN32_LEAN_AND_MEAN
12#define WIN32_LEAN_AND_MEAN 1
13#endif
14#include <winsock2.h>
15#else
16#include "compat.h"
17#endif
18
19#include <string>
20
21bool BtfParseSocks5Proxy(const std::string& spec, std::string& hostOut,
22 unsigned short& portOut, std::string& errOut);
23bool BtfSetSocks5Proxy(const std::string& spec, std::string& errOut);
24bool BtfEnableTorProxy(const std::string& spec, std::string& errOut);
25void BtfClearSocks5Proxy();
26bool BtfSocks5ProxyEnabled();
27bool BtfTorProxyEnabled();
28bool BtfIsTorOnionHost(const std::string& host);
29std::string BtfSocks5ProxyName();
30
31SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort,
32 int nSockSite, int nTimeoutSecs);
33
34#endif
diff --git a/src/randomx_pow.cpp b/src/randomx_pow.cpp
index d617ba2..1d058d4 100644
--- a/src/randomx_pow.cpp
+++ b/src/randomx_pow.cpp
@@ -19,6 +19,7 @@ static randomx_cache* g_cache = NULL;
19static randomx_dataset* g_dataset = NULL; 19static randomx_dataset* g_dataset = NULL;
20static randomx_vm* g_vmVerify = NULL; 20static randomx_vm* g_vmVerify = NULL;
21static CCriticalSection g_csVerify; 21static CCriticalSection g_csVerify;
22static std::mutex g_csInit;
22static bool g_fInit = false; 23static bool g_fInit = false;
23static bool g_fFast = false; 24static bool g_fFast = false;
24 25
@@ -28,6 +29,10 @@ bool RandomXInit()
28 if (g_fInit) 29 if (g_fInit)
29 return true; 30 return true;
30 31
32 std::lock_guard<std::mutex> lock(g_csInit);
33 if (g_fInit)
34 return true;
35
31 // Detect the best flags for this CPU (JIT, hardware AES, Argon2). 36 // Detect the best flags for this CPU (JIT, hardware AES, Argon2).
32 g_flags = randomx_get_flags(); 37 g_flags = randomx_get_flags();
33 38
diff --git a/src/rc/bitcoin.ico b/src/rc/bitcoin.ico
index b768077..33323de 100644
--- a/src/rc/bitcoin.ico
+++ b/src/rc/bitcoin.ico
Binary files differ
diff --git a/src/readme.txt b/src/readme.txt
index 6af1694..794fe20 100644
--- a/src/readme.txt
+++ b/src/readme.txt
@@ -42,14 +42,30 @@ The resulting executable is src/bitflash.exe.
42Command-line options (in addition to the original ones) 42Command-line options (in addition to the original ones)
43------------------------------------------------------- 43-------------------------------------------------------
44 /port=N listen on P2P port N (default 8433) 44 /port=N listen on P2P port N (default 8433)
45 /socks=HOST:PORT
46 route outbound Nostr and .btf rendezvous dials through a
47 SOCKS5 proxy, e.g. /socks=127.0.0.1:9050 for local Tor
48 or /socks=[::1]:9050 for an IPv6 loopback proxy
49 /tor[=HOST:PORT]
50 Tor mode for outbound Nostr and .btf rendezvous dials;
51 defaults to the local Tor SOCKS5 proxy at 127.0.0.1:9050
45 /solomine mine without requiring a connected peer (local testing) 52 /solomine mine without requiring a connected peer (local testing)
46 /operator operator mode (run pool server) 53 /operator operator mode (run pool server)
47 /participant=ADDR 54 /participant=ADDR
48 participant mode; mine to pool .btf address ADDR 55 participant mode; mine to pool .btf address ADDR
56 /stratumbridge=ADDR
57 listen on 127.0.0.1:3333 and bridge external Stratum miners
58 to pool .btf address ADDR
59 /stratumbridgeport=N
60 local Stratum bridge port (default 3333)
49 /poolname=NAME operator announcement pool name 61 /poolname=NAME operator announcement pool name
50 /poolfee=PCT operator announcement fee percent (example: 0.75) 62 /poolfee=PCT operator announcement fee percent (example: 0.75)
51 /pooldashboard=URL 63 /pooldashboard=URL
52 operator announcement dashboard URL 64 operator announcement dashboard URL
65 /poolstatusfile=PATH
66 write pool status JSON for dashboards
67 /poolroundsfile=PATH
68 write public pool round proofs JSON
53 /connectbtf=ADDR 69 /connectbtf=ADDR
54 keep an outbound .btf connection to ADDR 70 keep an outbound .btf connection to ADDR
55 /rvrelay=HOST:PORT 71 /rvrelay=HOST:PORT
diff --git a/src/rpc.cpp b/src/rpc.cpp
index 03d1f44..8af2fbe 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -236,35 +236,73 @@ static bool RebuildJob()
236 CBlock block; 236 CBlock block;
237 block.vtx.push_back(txNew); 237 block.vtx.push_back(txNew);
238 238
239 // Fill the block the same way BitcoinMiner() does, because this is the
240 // same job: a block that gets mined and submitted for real.
241 //
242 // It used to differ in two ways, and both were bugs.
243 //
244 // One pass over mapTransactions is not enough. That map is keyed by
245 // hash, so it has no dependency order, and a transaction spending the
246 // still-unconfirmed output of another (change from a recent send, most
247 // commonly) fails ConnectInputs whenever its parent happens to sit
248 // later in map order. A single pass then drops it -- and because map
249 // order does not change between rebuilds, every later rebuild drops it
250 // again at the same point. Deterministic exclusion, not bad luck: if
251 // pool operators mine most of the blocks, a transaction of that shape
252 // can stay unconfirmed for as long as its parent does.
253 //
254 // Diagnosis and fix both come from Vic-Nas, who found it in his fork
255 // of this code and described it exactly. The fork has since been
256 // deleted, so this carries it back rather than losing it.
257 int64 nFees = 0;
239 { 258 {
240 CTxDB txdb("r"); 259 CTxDB txdb("r");
241 CRITICAL_BLOCK(cs_mapTransactions) 260 CRITICAL_BLOCK(cs_mapTransactions)
242 { 261 {
243 map<uint256, CTxIndex> pool; 262 map<uint256, CTxIndex> pool;
263 vector<char> vfAlreadyAdded(mapTransactions.size());
264 bool fFoundSomething = true;
244 unsigned int sz = 0; 265 unsigned int sz = 0;
245 for (auto& kv : mapTransactions) { 266 // The cap is MAX_BLOCK_SIZE/2, not MAX_SIZE/2. MAX_SIZE is the
246 CTransaction& tx = kv.second; 267 // 32 MB serialisation ceiling, so half of it is 16 MB -- this
247 if (tx.IsCoinBase() || !tx.IsFinal()) continue; 268 // builder would happily assemble a template that its own
248 int64 nFees = 0; 269 // CheckBlock() rejects at the 1 MB consensus cap. This is the
249 map<uint256, CTxIndex> tmp(pool); 270 // pool's job builder, so a block over the cap is not one
250 if (!tx.ConnectInputs(txdb, tmp, CDiskTxPos(1,1,1), 0, nFees, 271 // wasted solve, it is every miner on the pool working a
251 false, true, tx.GetMinFee(block.vtx.size() < 100))) 272 // template the network will reject. The inner break bounds the
252 continue; 273 // overshoot to a single transaction within a pass.
253 pool = tmp; 274 while (fFoundSomething && sz < MAX_BLOCK_SIZE / 2)
254 block.vtx.push_back(tx); 275 {
255 sz += ::GetSerializeSize(tx, SER_NETWORK); 276 fFoundSomething = false;
256 // Must stay under the consensus cap CheckBlock() enforces. 277 unsigned int n = 0;
257 // This is the pool's job builder, so a block over the cap 278 for (map<uint256, CTransaction>::iterator mi = mapTransactions.begin();
258 // is not just one wasted solve -- it is every miner on the 279 mi != mapTransactions.end(); ++mi, ++n)
259 // pool working a template the network will reject. The 280 {
260 // transaction is already pushed above, so this can overshoot 281 if (vfAlreadyAdded[n])
261 // by one; half the cap leaves room for that. 282 continue;
262 if (sz > MAX_BLOCK_SIZE / 2) break; 283 CTransaction& tx = (*mi).second;
284 if (tx.IsCoinBase() || !tx.IsFinal()) continue;
285 map<uint256, CTxIndex> tmp(pool);
286 if (!tx.ConnectInputs(txdb, tmp, CDiskTxPos(1,1,1), 0, nFees,
287 false, true, tx.GetMinFee(block.vtx.size() < 100)))
288 continue;
289 swap(pool, tmp);
290 block.vtx.push_back(tx);
291 sz += ::GetSerializeSize(tx, SER_NETWORK);
292 vfAlreadyAdded[n] = true;
293 fFoundSomething = true;
294 if (sz > MAX_BLOCK_SIZE / 2) break;
295 }
263 } 296 }
264 } 297 }
265 } 298 }
266 299
267 block.vtx[0].vout[0].nValue = block.GetBlockValue(pindexPrev->nHeight + 1, 0); 300 // And the fees actually go to the coinbase now. nFees was declared
301 // inside the scan loop, so every transaction reset it and the total was
302 // thrown away, while this line passed a hardcoded 0. A pool operator
303 // collected fees into a block and paid none of them to itself. Solo
304 // mining has always passed nFees here; only this builder did not.
305 block.vtx[0].vout[0].nValue = block.GetBlockValue(pindexPrev->nHeight + 1, nFees);
268 block.hashPrevBlock = pindexPrev->GetBlockHash(); 306 block.hashPrevBlock = pindexPrev->GetBlockHash();
269 block.hashMerkleRoot = block.BuildMerkleTree(); 307 block.hashMerkleRoot = block.BuildMerkleTree();
270 block.nTime = max((unsigned int)(pindexPrev->GetMedianTimePast() + 1), 308 block.nTime = max((unsigned int)(pindexPrev->GetMedianTimePast() + 1),
@@ -297,26 +335,110 @@ struct PendingPayout {
297 // supposed to pay it: a 280-block reorg once left ~14.4k BTF of debt 335 // supposed to pay it: a 280-block reorg once left ~14.4k BTF of debt
298 // owed against blocks that no longer existed, which then drained the 336 // owed against blocks that no longer existed, which then drained the
299 // wallet paying fork-era miners out of real-chain coin. 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.
300 int blockHeight = 0; 343 int blockHeight = 0;
301 // Must be explicitly zeroed: uint256's default constructor is empty 344 // Must be explicitly zeroed: uint256's default constructor is empty
302 // (uint256.h:514), so leaving this unassigned gives every ledger entry 345 // (uint256.h:514), so leaving this unassigned gives every ledger entry
303 // loaded from an older file a garbage hash, which then matches no block 346 // loaded from an older file a garbage hash, which then matches no block
304 // and gets voided as "orphaned". That mistake voided a live payout. 347 // and gets voided as "orphaned". That mistake voided a live payout.
305 uint256 blockHash = 0; // zero = entry predates hash keying 348 uint256 blockHash = 0; // zero = entry predates hash keying
306 int matureAtHeight; 349 int matureAtHeight = 0;
307 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.
354 std::map<std::string, std::string> paidTxids; // address -> txid
308 int retries = 0; // exec-thread only: SendMoney attempts so far 355 int retries = 0; // exec-thread only: SendMoney attempts so far
309 int deferrals = 0; // exec-thread only: waits for spendable coin 356 int deferrals = 0; // exec-thread only: waits for spendable coin
357 // Upstream's `nextAttempt` is this field under another name; ours is kept
358 // so the retry/deferral policy above stays coherent with it.
310 int64 retryAfter = 0; // exec-thread only: unix time, 0 = ready now 359 int64 retryAfter = 0; // exec-thread only: unix time, 0 = ready now
311}; 360};
312static std::vector<PendingPayout> gPendingPayouts; 361static std::vector<PendingPayout> gPendingPayouts;
362static std::mutex gPayoutLedgerMutex;
363
364struct PoolRoundProof {
365 std::string id;
366 int blockHeight;
367 int matureAtHeight;
368 int64 createdAt;
369 std::string blockHash;
370 std::string coinbaseTxid;
371 std::string foundBy;
372 double feePercent;
373 uint64 totalShares;
374 int64 coinbaseValue;
375 int64 subsidy;
376 int64 minerReward;
377 int64 operatorReward;
378 int skippedDust;
379 int64 forfeitedBadAddress;
380 std::map<std::string, uint64> shares;
381 std::map<std::string, int64> payouts;
382 std::map<std::string, std::string> paidTxids;
383
384 PoolRoundProof()
385 : blockHeight(0), matureAtHeight(0), createdAt(0), feePercent(0.0),
386 totalShares(0), coinbaseValue(0), subsidy(0), minerReward(0), operatorReward(0),
387 skippedDust(0), forfeitedBadAddress(0) {}
388};
389static std::vector<PoolRoundProof> gPoolRounds;
390static std::mutex gPoolRoundsMutex;
391static const size_t MAX_POOL_ROUNDS = 200;
313 392
314static std::string PayoutFilePath() 393static std::string PayoutFilePath()
315{ 394{
316 return GetAppDir() + "/pending_payouts.json"; 395 return GetAppDir() + "/pending_payouts.json";
317} 396}
318 397
319static void SavePendingPayouts() 398static std::string PoolRoundsFilePath()
399{
400 if (!strPoolRoundsFile.empty())
401 return strPoolRoundsFile;
402 return GetAppDir() + "/pool_rounds.json";
403}
404
405// Identity of a payout round. Used as the pool-round proof id and, more
406// importantly, as the `pool_payout_id` written into wallet transaction
407// metadata and matched by FindRecordedPoolPayout().
408//
409// Derived from the block HASH, not from its height alone. Upstream uses
410// "block-<height>", which names two different blocks identically across a
411// reorg -- and because this id is what the crash-recovery probe matches on, a
412// height-keyed id lets that probe find the *previous* round's transaction and
413// conclude a miner has already been paid when they have not. The height stays
414// in the string for readability only.
415//
416// A zero hash means the entry predates hash keying; those keep the old form so
417// anything already recorded against them still matches.
418static std::string PayoutIdForBlock(int blockHeight, const uint256& blockHash)
419{
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());
424}
425
426static bool PayoutFullyPaid(const PendingPayout& pp)
427{
428 for (const auto& kv : pp.amounts)
429 if (pp.paidTxids.count(kv.first) == 0)
430 return false;
431 return true;
432}
433
434// Serialise the ledger. Caller must hold gPayoutLedgerMutex.
435//
436// Split from the file write so the payout thread can hold the mutex just long
437// enough to read the structure and then do the I/O with it released. Upstream
438// writes the file inside the per-recipient loop with gPayoutLedgerMutex held,
439// while the event loop's FlushMaturePayouts blocks on that same mutex -- one
440// slow fsync per recipient, on the thread the miners' I/O shares.
441static std::string SerializePendingPayoutsLocked()
320{ 442{
321 nlohmann::json arr = nlohmann::json::array(); 443 nlohmann::json arr = nlohmann::json::array();
322 for (const auto& pp : gPendingPayouts) { 444 for (const auto& pp : gPendingPayouts) {
@@ -324,21 +446,49 @@ static void SavePendingPayouts()
324 obj["blockHeight"] = pp.blockHeight; 446 obj["blockHeight"] = pp.blockHeight;
325 obj["blockHash"] = pp.blockHash.ToString(); 447 obj["blockHash"] = pp.blockHash.ToString();
326 obj["matureAtHeight"] = pp.matureAtHeight; 448 obj["matureAtHeight"] = pp.matureAtHeight;
449 obj["retryAfter"] = pp.retryAfter;
327 nlohmann::json amounts = nlohmann::json::object(); 450 nlohmann::json amounts = nlohmann::json::object();
328 for (const auto& kv : pp.amounts) 451 for (const auto& kv : pp.amounts)
329 amounts[kv.first] = kv.second; 452 amounts[kv.first] = kv.second;
330 obj["amounts"] = amounts; 453 obj["amounts"] = amounts;
454 nlohmann::json paid = nlohmann::json::object();
455 for (const auto& kv : pp.paidTxids)
456 paid[kv.first] = kv.second;
457 obj["paidTxids"] = paid;
331 arr.push_back(obj); 458 arr.push_back(obj);
332 } 459 }
460 return arr.dump(2);
461}
462
463// Write the serialised ledger. Holds no lock; safe to call with the mutex
464// released. Atomic tmp+rename so a crash mid-write cannot truncate the record
465// of who is still owed money.
466static void WritePendingPayoutsFile(const std::string& s)
467{
333 std::string path = PayoutFilePath(); 468 std::string path = PayoutFilePath();
334 FILE* f = fopen(path.c_str(), "w"); 469 std::string tmp = path + ".tmp";
335 if (f) { 470 FILE* f = fopen(tmp.c_str(), "w");
336 std::string s = arr.dump(2); 471 if (!f) {
337 fwrite(s.c_str(), 1, s.size(), f); 472 LogPrint("payout", "[payout] WARNING: could not write %s\n", tmp.c_str());
338 fclose(f); 473 return;
339 } else {
340 LogPrint("payout", "[payout] WARNING: could not write %s\n", path.c_str());
341 } 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;
479 }
480#ifdef _WIN32
481 remove(path.c_str());
482#endif
483 if (rename(tmp.c_str(), path.c_str()) != 0)
484 LogPrint("payout", "[payout] WARNING: could not replace %s\n", path.c_str());
485}
486
487// Convenience for the cold paths (startup, queueing) where holding the mutex
488// across the write costs nothing. Caller must hold gPayoutLedgerMutex.
489static void SavePendingPayoutsLocked()
490{
491 WritePendingPayoutsFile(SerializePendingPayoutsLocked());
342} 492}
343 493
344static void LoadPendingPayouts() 494static void LoadPendingPayouts()
@@ -354,6 +504,7 @@ static void LoadPendingPayouts()
354 fclose(f); 504 fclose(f);
355 try { 505 try {
356 nlohmann::json arr = nlohmann::json::parse(s); 506 nlohmann::json arr = nlohmann::json::parse(s);
507 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
357 for (const auto& obj : arr) { 508 for (const auto& obj : arr) {
358 PendingPayout pp; 509 PendingPayout pp;
359 pp.matureAtHeight = obj["matureAtHeight"].get<int>(); 510 pp.matureAtHeight = obj["matureAtHeight"].get<int>();
@@ -364,8 +515,24 @@ static void LoadPendingPayouts()
364 pp.blockHash.SetHex(obj["blockHash"].get<std::string>()); 515 pp.blockHash.SetHex(obj["blockHash"].get<std::string>());
365 if (obj.contains("blockHeight") && obj["blockHeight"].is_number()) 516 if (obj.contains("blockHeight") && obj["blockHeight"].is_number())
366 pp.blockHeight = obj["blockHeight"].get<int>(); 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)
526 pp.blockHeight = pp.matureAtHeight - COINBASE_MATURITY;
527 // Absent in every file written before this merge; default 0 means
528 // "ready now", which is the right answer for a restored entry.
529 if (obj.contains("retryAfter") && obj["retryAfter"].is_number())
530 pp.retryAfter = obj["retryAfter"].get<int64>();
367 for (auto it = obj["amounts"].begin(); it != obj["amounts"].end(); ++it) 531 for (auto it = obj["amounts"].begin(); it != obj["amounts"].end(); ++it)
368 pp.amounts[it.key()] = it.value().get<int64>(); 532 pp.amounts[it.key()] = it.value().get<int64>();
533 if (obj.contains("paidTxids") && obj["paidTxids"].is_object())
534 for (auto it = obj["paidTxids"].begin(); it != obj["paidTxids"].end(); ++it)
535 pp.paidTxids[it.key()] = it.value().get<std::string>();
369 gPendingPayouts.push_back(pp); 536 gPendingPayouts.push_back(pp);
370 } 537 }
371 LogPrint("payout", "[payout] loaded %zu pending payout(s) from disk\n", 538 LogPrint("payout", "[payout] loaded %zu pending payout(s) from disk\n",
@@ -396,6 +563,17 @@ struct PayingRecord {
396 int retries = 0; 563 int retries = 0;
397 int deferrals = 0; 564 int deferrals = 0;
398 int64 retryAfter = 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;
399}; 577};
400static std::vector<PayingRecord> gPaying; // guarded by gPayoutExecMutex 578static std::vector<PayingRecord> gPaying; // guarded by gPayoutExecMutex
401static std::mutex gPayoutExecMutex; 579static std::mutex gPayoutExecMutex;
@@ -432,8 +610,19 @@ static void SavePayingLocked()
432 } 610 }
433 std::string s = arr.dump(2); 611 std::string s = arr.dump(2);
434 fwrite(s.c_str(), 1, s.size(), f); 612 fwrite(s.c_str(), 1, s.size(), f);
435 fflush(f); 613 // fclose() is where buffered data actually reaches the OS, so a failure
436 fclose(f); 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
437 if (rename(tmp.c_str(), path.c_str()) != 0) 626 if (rename(tmp.c_str(), path.c_str()) != 0)
438 LogPrint("payout", "[payout] WARNING: could not rename %s -> %s\n", 627 LogPrint("payout", "[payout] WARNING: could not rename %s -> %s\n",
439 tmp.c_str(), path.c_str()); 628 tmp.c_str(), path.c_str());
@@ -475,6 +664,143 @@ static void LoadPaying()
475 } 664 }
476} 665}
477 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// ---------------------------------------------------------------------------
673static nlohmann::json PoolRoundToJson(const PoolRoundProof& r)
674{
675 nlohmann::json j;
676 j["id"] = r.id;
677 j["blockHeight"] = r.blockHeight;
678 j["matureAtHeight"] = r.matureAtHeight;
679 j["createdAt"] = r.createdAt;
680 j["blockHash"] = r.blockHash;
681 j["coinbaseTxid"] = r.coinbaseTxid;
682 j["foundBy"] = r.foundBy;
683 j["feePercent"] = r.feePercent;
684 j["totalShares"] = r.totalShares;
685 j["coinbaseValueSatoshis"] = r.coinbaseValue;
686 j["coinbaseValueBtf"] = FormatMoney(r.coinbaseValue);
687 j["subsidySatoshis"] = r.subsidy;
688 j["subsidyBtf"] = FormatMoney(r.subsidy);
689 j["minerRewardSatoshis"] = r.minerReward;
690 j["minerRewardBtf"] = FormatMoney(r.minerReward);
691 j["operatorRewardSatoshis"] = r.operatorReward;
692 j["operatorRewardBtf"] = FormatMoney(r.operatorReward);
693 j["skippedDust"] = r.skippedDust;
694 j["forfeitedBadAddressSatoshis"] = r.forfeitedBadAddress;
695 j["forfeitedBadAddressBtf"] = FormatMoney(r.forfeitedBadAddress);
696
697 j["shares"] = nlohmann::json::object();
698 for (const auto& kv : r.shares)
699 j["shares"][kv.first] = kv.second;
700
701 j["payouts"] = nlohmann::json::object();
702 for (const auto& kv : r.payouts) {
703 nlohmann::json p;
704 p["amountSatoshis"] = kv.second;
705 p["amountBtf"] = FormatMoney(kv.second);
706 std::map<std::string, std::string>::const_iterator tx = r.paidTxids.find(kv.first);
707 if (tx != r.paidTxids.end())
708 p["txid"] = tx->second;
709 else
710 p["txid"] = nullptr;
711 j["payouts"][kv.first] = p;
712 }
713 return j;
714}
715
716static void SavePoolRoundsLocked()
717{
718 nlohmann::json root;
719 root["schema"] = "bitflash-pool-rounds-1";
720 root["updatedAt"] = GetTime();
721 root["rounds"] = nlohmann::json::array();
722 for (const PoolRoundProof& r : gPoolRounds)
723 root["rounds"].push_back(PoolRoundToJson(r));
724
725 std::string path = PoolRoundsFilePath();
726 std::string tmp = path + ".tmp";
727 FILE* f = fopen(tmp.c_str(), "w");
728 if (!f) {
729 LogPrint("payout", "[payout] WARNING: could not write %s\n", tmp.c_str());
730 return;
731 }
732 std::string s = root.dump(2);
733 fwrite(s.c_str(), 1, s.size(), f);
734 if (fclose(f) != 0) {
735 LogPrint("payout", "[payout] WARNING: could not close %s\n", tmp.c_str());
736 return;
737 }
738#ifdef _WIN32
739 remove(path.c_str());
740#endif
741 if (rename(tmp.c_str(), path.c_str()) != 0)
742 LogPrint("payout", "[payout] WARNING: could not replace %s\n", path.c_str());
743}
744
745static void LoadPoolRounds()
746{
747 std::string path = PoolRoundsFilePath();
748 FILE* f = fopen(path.c_str(), "r");
749 if (!f) return;
750 fseek(f, 0, SEEK_END);
751 long sz = ftell(f);
752 fseek(f, 0, SEEK_SET);
753 std::string s(sz, '\0');
754 fread(&s[0], 1, sz, f);
755 fclose(f);
756
757 try {
758 nlohmann::json root = nlohmann::json::parse(s);
759 nlohmann::json arr = root.is_array() ? root : root["rounds"];
760 std::lock_guard<std::mutex> lk(gPoolRoundsMutex);
761 for (const auto& obj : arr) {
762 PoolRoundProof r;
763 if (obj.contains("id") && obj["id"].is_string()) r.id = obj["id"].get<std::string>();
764 if (obj.contains("blockHeight")) r.blockHeight = obj["blockHeight"].get<int>();
765 if (obj.contains("matureAtHeight")) r.matureAtHeight = obj["matureAtHeight"].get<int>();
766 if (obj.contains("createdAt")) r.createdAt = obj["createdAt"].get<int64>();
767 if (obj.contains("blockHash") && obj["blockHash"].is_string()) r.blockHash = obj["blockHash"].get<std::string>();
768 if (obj.contains("coinbaseTxid") && obj["coinbaseTxid"].is_string()) r.coinbaseTxid = obj["coinbaseTxid"].get<std::string>();
769 if (obj.contains("foundBy") && obj["foundBy"].is_string()) r.foundBy = obj["foundBy"].get<std::string>();
770 if (obj.contains("feePercent")) r.feePercent = obj["feePercent"].get<double>();
771 if (obj.contains("totalShares")) r.totalShares = obj["totalShares"].get<uint64>();
772 if (obj.contains("coinbaseValueSatoshis")) r.coinbaseValue = obj["coinbaseValueSatoshis"].get<int64>();
773 if (obj.contains("subsidySatoshis")) r.subsidy = obj["subsidySatoshis"].get<int64>();
774 if (obj.contains("minerRewardSatoshis")) r.minerReward = obj["minerRewardSatoshis"].get<int64>();
775 if (obj.contains("operatorRewardSatoshis")) r.operatorReward = obj["operatorRewardSatoshis"].get<int64>();
776 if (obj.contains("skippedDust")) r.skippedDust = obj["skippedDust"].get<int>();
777 if (obj.contains("forfeitedBadAddressSatoshis")) r.forfeitedBadAddress = obj["forfeitedBadAddressSatoshis"].get<int64>();
778 if (obj.contains("shares") && obj["shares"].is_object())
779 for (auto it = obj["shares"].begin(); it != obj["shares"].end(); ++it)
780 r.shares[it.key()] = it.value().get<uint64>();
781 if (obj.contains("payouts") && obj["payouts"].is_object())
782 for (auto it = obj["payouts"].begin(); it != obj["payouts"].end(); ++it) {
783 if (it.value().is_object()) {
784 r.payouts[it.key()] = it.value().value("amountSatoshis", (int64)0);
785 if (it.value().contains("txid") && it.value()["txid"].is_string())
786 r.paidTxids[it.key()] = it.value()["txid"].get<std::string>();
787 } else if (it.value().is_number_integer()) {
788 r.payouts[it.key()] = it.value().get<int64>();
789 }
790 }
791 if (!r.id.empty())
792 gPoolRounds.push_back(r);
793 }
794 while (gPoolRounds.size() > MAX_POOL_ROUNDS)
795 gPoolRounds.erase(gPoolRounds.begin());
796 LogPrint("payout", "[payout] loaded %zu pool round proof(s) from disk\n",
797 gPoolRounds.size());
798 } catch (const std::exception& e) {
799 LogPrint("payout", "[payout] WARNING: failed to parse %s: %s\n",
800 path.c_str(), e.what());
801 }
802}
803
478// Is the block that funds this payout still on the main chain? 804// Is the block that funds this payout still on the main chain?
479// 805//
480// A zero hash means the entry was written before payouts recorded one, so 806// A zero hash means the entry was written before payouts recorded one, so
@@ -499,85 +825,55 @@ static std::string PayoutComment(int blockHeight)
499 return strprintf("Pool payout height %d", blockHeight); 825 return strprintf("Pool payout height %d", blockHeight);
500} 826}
501 827
502// Did we already send this payment before dying? Looks for a wallet 828// Startup reconciliation.
503// transaction of ours carrying this payout's comment and paying exactly this
504// amount to this address.
505// 829//
506// Biased towards reporting "already paid" on any doubt: skipping a payment a 830// This used to be the crash-recovery mechanism: it scanned the wallet with
507// miner is owed is visible and recoverable from the log, while paying twice 831// WalletAlreadyPaid() and then dropped ledger entries that had already been
508// is neither. 832// exploded into in-flight records. Neither job is right any more.
509static bool WalletAlreadyPaid(const PayingRecord& r) 833//
510{ 834// The ledger is now authoritative and an entry survives until every recipient
511 uint160 h160; 835// has a txid recorded against it (upstream cbdd06d), so dropping ledger
512 if (!AddressToHash160(r.address, h160)) 836// entries here would reintroduce exactly the loss this merge is adopting that
513 return false; 837// model to prevent. And the wallet scan is now FindRecordedPoolPayout(), done
514 CScript want; 838// once per record on its first attempt, which is the same question asked in
515 want << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG; 839// the one place that can act on the answer. Running both would be two
516 const std::string comment = PayoutComment(r.blockHeight); 840// authorities for the same debt.
517 841//
518 CRITICAL_BLOCK(cs_mapWallet) 842// What is left is the useful part: the in-flight list restored from disk may
519 { 843// contain records the ledger already shows as paid (we died between the send
520 for (std::map<uint256, CWalletTx>::iterator it = mapWallet.begin(); 844// and the in-flight file being rewritten). The ledger wins.
521 it != mapWallet.end(); ++it) {
522 CWalletTx& wtx = it->second;
523 // The comment is written only by the payout path, so its presence
524 // already identifies the transaction as one of ours.
525 std::map<std::string, std::string>::const_iterator ci =
526 wtx.mapValue.find("comment");
527 if (ci == wtx.mapValue.end() || ci->second != comment)
528 continue;
529 foreach (const CTxOut& txout, wtx.vout)
530 if (txout.nValue == r.amount && txout.scriptPubKey == want)
531 return true;
532 }
533 }
534 return false;
535}
536
537// Startup reconciliation. Drops in-flight records the wallet shows were
538// already sent, and drops ledger entries that were already exploded into
539// records (which happens if we died between writing the records and rewriting
540// the ledger -- both files then describe the same debt).
541static void ReconcilePayouts() 845static void ReconcilePayouts()
542{ 846{
543 std::lock_guard<std::mutex> lk(gPayoutExecMutex); 847 std::lock_guard<std::mutex> exec(gPayoutExecMutex);
848 std::lock_guard<std::mutex> ledger(gPayoutLedgerMutex);
544 849
545 size_t settled = 0; 850 size_t settled = 0;
546 int64 settledAmount = 0; 851 int64 settledAmount = 0;
547 for (auto it = gPaying.begin(); it != gPaying.end(); ) { 852 for (auto it = gPaying.begin(); it != gPaying.end(); ) {
548 if (WalletAlreadyPaid(*it)) { 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) {
549 settledAmount += it->amount; 860 settledAmount += it->amount;
550 settled++; 861 settled++;
551 it = gPaying.erase(it); 862 it = gPaying.erase(it);
552 } else { 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;
553 ++it; 868 ++it;
554 } 869 }
555 } 870 }
556 if (settled) { 871 if (settled) {
557 LogPrint("payout", "[payout] %zu in-flight payment(s) totalling %s were" 872 LogPrint("payout", "[payout] %zu in-flight payment(s) totalling %s are"
558 " already sent before the last shutdown -- not resending\n", 873 " already recorded paid in the ledger -- not resending\n",
559 settled, FormatMoney(settledAmount).c_str()); 874 settled, FormatMoney(settledAmount).c_str());
560 SavePayingLocked();
561 }
562
563 size_t dropped = 0;
564 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ) {
565 bool exploded = false;
566 for (const auto& r : gPaying)
567 if (r.matureAtHeight == it->matureAtHeight && r.blockHash == it->blockHash) {
568 exploded = true;
569 break;
570 }
571 if (exploded) {
572 LogPrint("payout", "[payout] ledger entry for block %d is already"
573 " in flight -- dropping the duplicate\n", it->blockHeight);
574 it = gPendingPayouts.erase(it);
575 dropped++;
576 } else {
577 ++it;
578 }
579 } 875 }
580 if (dropped) SavePendingPayouts(); 876 SavePayingLocked();
581 877
582 if (!gPaying.empty()) { 878 if (!gPaying.empty()) {
583 int64 owed = 0; 879 int64 owed = 0;
@@ -587,12 +883,100 @@ static void ReconcilePayouts()
587 } 883 }
588} 884}
589 885
886static void RecordPoolRound(const PoolRoundProof& round)
887{
888 std::lock_guard<std::mutex> lk(gPoolRoundsMutex);
889 for (PoolRoundProof& existing : gPoolRounds) {
890 if (existing.id == round.id) {
891 existing = round;
892 SavePoolRoundsLocked();
893 return;
894 }
895 }
896 gPoolRounds.push_back(round);
897 while (gPoolRounds.size() > MAX_POOL_ROUNDS)
898 gPoolRounds.erase(gPoolRounds.begin());
899 SavePoolRoundsLocked();
900}
901
902static void MarkPoolRoundPaid(const std::string& id,
903 const std::string& address,
904 const std::string& txid)
905{
906 std::lock_guard<std::mutex> lk(gPoolRoundsMutex);
907 for (PoolRoundProof& r : gPoolRounds) {
908 if (r.id != id)
909 continue;
910 r.paidTxids[address] = txid;
911 SavePoolRoundsLocked();
912 return;
913 }
914}
915
916// Upstream's gPayoutExecQueue/gPayoutExecMutex/gPayoutThreadRunning are
917// declared above with gPaying: the in-flight list IS the exec queue in this
918// tree, at per-recipient rather than per-round granularity, so a second queue
919// would be a second answer to "who is owed money". Only the dedupe set is new.
920//
921// Ids of rounds currently exploded into gPaying, so a flush cannot queue the
922// same round twice. Guarded by gPayoutExecMutex.
923static std::set<std::string> gQueuedPayouts;
924
925static bool FindRecordedPoolPayout(const std::string& id,
926 const std::string& address,
927 int64 amount,
928 std::string& txidRet)
929{
930 CRITICAL_BLOCK(cs_mapWallet)
931 {
932 for (map<uint256, CWalletTx>::const_iterator it = mapWallet.begin();
933 it != mapWallet.end(); ++it)
934 {
935 const CWalletTx& wtx = it->second;
936 map<string, string>::const_iterator idIt = wtx.mapValue.find("pool_payout_id");
937 if (idIt == wtx.mapValue.end() || idIt->second != id)
938 continue;
939 map<string, string>::const_iterator addrIt = wtx.mapValue.find("pool_payout_address");
940 if (addrIt == wtx.mapValue.end() || addrIt->second != address)
941 continue;
942 map<string, string>::const_iterator amtIt = wtx.mapValue.find("pool_payout_amount");
943 if (amtIt == wtx.mapValue.end() || amtIt->second != strprintf("%lld", amount))
944 continue;
945 txidRet = wtx.GetHash().GetHex();
946 return true;
947 }
948 }
949 return false;
950}
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.
590static void QueuePayouts(int blockHeight, 957static void QueuePayouts(int blockHeight,
591 const uint256& blockHash, 958 const uint256& blockHash,
959 const std::string& coinbaseTxid,
960 int64 coinbaseValue,
961 const std::string& foundBy,
592 const std::map<std::string, uint64>& shareCount, 962 const std::map<std::string, uint64>& shareCount,
593 uint64 total) 963 uint64 total)
594{ 964{
965 PoolRoundProof round;
966 round.id = PayoutIdForBlock(blockHeight, blockHash);
967 round.blockHeight = blockHeight;
968 round.matureAtHeight = blockHeight + COINBASE_SPENDABLE_DEPTH;
969 round.createdAt = GetTime();
970 round.blockHash = blockHash.ToString();
971 round.coinbaseTxid = coinbaseTxid;
972 round.coinbaseValue = coinbaseValue;
973 round.foundBy = foundBy;
974 round.feePercent = dPoolFeePercent;
975 round.totalShares = total;
976 round.shares = shareCount;
977
595 if (total == 0) { 978 if (total == 0) {
979 RecordPoolRound(round);
596 LogPrint("payout", "[payout] block %d: zero shares recorded -- nothing to pay out\n", 980 LogPrint("payout", "[payout] block %d: zero shares recorded -- nothing to pay out\n",
597 blockHeight); 981 blockHeight);
598 return; 982 return;
@@ -608,6 +992,10 @@ static void QueuePayouts(int blockHeight,
608 int64 nSubsidy = 50 * COIN; 992 int64 nSubsidy = 50 * COIN;
609 nSubsidy >>= (blockHeight / 210000); // halving schedule 993 nSubsidy >>= (blockHeight / 210000); // halving schedule
610 int64 minerReward = (int64)((double)nSubsidy * (1.0 - feeFrac)); 994 int64 minerReward = (int64)((double)nSubsidy * (1.0 - feeFrac));
995 int64 operatorCut = nSubsidy - minerReward;
996 round.subsidy = nSubsidy;
997 round.minerReward = minerReward;
998 round.operatorReward = operatorCut;
611 999
612 PendingPayout pp; 1000 PendingPayout pp;
613 pp.blockHeight = blockHeight; 1001 pp.blockHeight = blockHeight;
@@ -621,7 +1009,9 @@ static void QueuePayouts(int blockHeight,
621 // alone on a private fork paying itself, so it says nothing about a pool 1009 // alone on a private fork paying itself, so it says nothing about a pool
622 // competing on the real chain. Waiting the extra 20 blocks makes each 1010 // competing on the real chain. Waiting the extra 20 blocks makes each
623 // payout funded by construction, and puts the maturity gate past the 1011 // payout funded by construction, and puts the maturity gate past the
624 // reorg depth that orphaned 280 blocks of obligations. 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.
625 pp.matureAtHeight = blockHeight + COINBASE_SPENDABLE_DEPTH; 1015 pp.matureAtHeight = blockHeight + COINBASE_SPENDABLE_DEPTH;
626 1016
627 int skipped = 0; 1017 int skipped = 0;
@@ -643,12 +1033,18 @@ static void QueuePayouts(int blockHeight,
643 continue; 1033 continue;
644 } 1034 }
645 pp.amounts[kv.first] = amount; 1035 pp.amounts[kv.first] = amount;
1036 round.payouts[kv.first] = amount;
646 } 1037 }
1038 round.skippedDust = skipped;
1039 round.forfeitedBadAddress = burned;
647 1040
648 gPendingPayouts.push_back(pp); 1041 {
649 SavePendingPayouts(); 1042 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
1043 gPendingPayouts.push_back(pp);
1044 SavePendingPayoutsLocked();
1045 }
1046 RecordPoolRound(round);
650 1047
651 int64 operatorCut = nSubsidy - minerReward;
652 LogPrint("payout", 1048 LogPrint("payout",
653 "[payout] block %d: subsidy %s BTF, miners get %s" 1049 "[payout] block %d: subsidy %s BTF, miners get %s"
654 " (fee %.2f%% = %s to operator)," 1050 " (fee %.2f%% = %s to operator),"
@@ -664,66 +1060,110 @@ static void QueuePayouts(int blockHeight,
664 : ""); 1060 : "");
665} 1061}
666 1062
667// Called from event loop: move matured entries into the in-flight list (fast). 1063// Called from the event loop: explode matured ledger entries into per-recipient
1064// in-flight records (fast; no wallet or disk work beyond one small write).
1065//
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.
668// 1072//
669// Ordering matters and is deliberate: the in-flight records are written and 1073// gPaying is kept on top of that as the in-flight working set: it carries the
670// fsync'd to their own file *before* the ledger entry is erased, so the debt 1074// per-recipient retry/deferral state that a per-round `nextAttempt` cannot
671// is always described by at least one of the two files. Dying in the window 1075// express, and it is what the payout thread iterates. It is a cache of work
672// where both describe it is handled at startup by ReconcilePayouts, which 1076// derived from the ledger, never an independent record of debt -- if it were
673// drops the ledger entry it can see is already in flight. 1077// lost entirely the ledger would rebuild it on the next flush.
674static void FlushMaturePayouts() 1078static void FlushMaturePayouts()
675{ 1079{
676 // Void obligations whose funding block lost a reorg. The coin that was 1080 // Which funding blocks are still on the main chain? Asked with no payout
677 // meant to pay them does not exist, so paying anyway spends real-chain 1081 // lock held: PayoutBlockStillOnChain takes cs_main, and cs_main is held by
678 // coin the pool owes to somebody else. 1082 // ProcessBlock and SendMoney for long stretches. Holding the ledger mutex
679 bool voided = false; 1083 // across it would let the event loop park the payout thread behind a chain
680 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ) { 1084 // lock it has no interest in. (It is not a deadlock either way -- nothing
681 if (PayoutBlockStillOnChain(it->blockHash)) { ++it; continue; } 1085 // takes the ledger mutex while holding cs_main -- but it is exactly the
682 int64 total = 0; 1086 // kind of stall this merge is removing from the ledger path.)
683 for (auto& kv : it->amounts) total += kv.second; 1087 std::vector<uint256> orphaned;
684 LogPrint("payout", "[payout] block %d (%s) was orphaned -- voiding %zu" 1088 {
685 " payment(s) totalling %s\n", 1089 std::vector<uint256> hashes;
686 it->blockHeight, it->blockHash.ToString().substr(0, 14).c_str(), 1090 {
687 it->amounts.size(), FormatMoney(total).c_str()); 1091 std::lock_guard<std::mutex> ledger(gPayoutLedgerMutex);
688 it = gPendingPayouts.erase(it); 1092 for (const PendingPayout& pp : gPendingPayouts)
689 voided = true; 1093 hashes.push_back(pp.blockHash);
1094 }
1095 for (const uint256& h : hashes)
1096 if (!PayoutBlockStillOnChain(h))
1097 orphaned.push_back(h);
690 } 1098 }
691 if (voided) SavePendingPayouts();
692 1099
693 std::vector<PendingPayout> ready; 1100 std::vector<PendingPayout> ready;
694 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ) { 1101 {
695 if (nBestHeight < it->matureAtHeight) { ++it; continue; } 1102 std::lock_guard<std::mutex> ledger(gPayoutLedgerMutex);
696 LogPrint("payout", "[payout] block height %d matured -- queuing %zu payments\n", 1103
697 it->matureAtHeight, it->amounts.size()); 1104 // Void obligations whose funding block lost a reorg. The coin that was
698 ready.push_back(*it); // copy first; erase after save 1105 // meant to pay them does not exist, so paying anyway spends real-chain
699 ++it; 1106 // coin the pool owes to somebody else.
700 } 1107 bool voided = false;
701 if (!ready.empty()) {
702 // Record the individual payments first, and only then drop the ledger
703 // entries that produced them.
704 {
705 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
706 for (const auto& pp : ready) {
707 for (const auto& kv : pp.amounts) {
708 PayingRecord r;
709 r.blockHeight = pp.blockHeight;
710 r.blockHash = pp.blockHash;
711 r.matureAtHeight = pp.matureAtHeight;
712 r.address = kv.first;
713 r.amount = kv.second;
714 gPaying.push_back(r);
715 }
716 }
717 SavePayingLocked();
718 }
719 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ) { 1108 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ) {
720 if (nBestHeight >= it->matureAtHeight) 1109 if (std::find(orphaned.begin(), orphaned.end(), it->blockHash) == orphaned.end()) {
721 it = gPendingPayouts.erase(it);
722 else
723 ++it; 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;
724 } 1121 }
725 SavePendingPayouts(); 1122 if (voided) SavePendingPayoutsLocked();
1123
1124 int64 now = GetTime();
1125 for (const PendingPayout& pp : gPendingPayouts) {
1126 if (nBestHeight < pp.matureAtHeight)
1127 continue;
1128 if (pp.retryAfter > now)
1129 continue;
1130 if (PayoutFullyPaid(pp))
1131 continue;
1132 ready.push_back(pp);
1133 }
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());
726 } 1165 }
1166 if (added) SavePayingLocked();
727} 1167}
728 1168
729// Identity of a payment, for finding its record again after SendMoney. 1169// Identity of a payment, for finding its record again after SendMoney.
@@ -737,13 +1177,24 @@ static bool SamePayment(const PayingRecord& a, const PayingRecord& b)
737} 1177}
738 1178
739// Settle a payment: it is either done or abandoned, either way it stops being 1179// Settle a payment: it is either done or abandoned, either way it stops being
740// owed. Removing the record and rewriting the file is what makes the payment 1180// in flight. Note this only retires the working record -- whether the miner is
741// not happen twice after a restart. 1181// still OWED the money is the ledger's business, and the ledger is updated
1182// separately by RecordPayoutTxid().
742static void ForgetPayment(const PayingRecord& r) 1183static void ForgetPayment(const PayingRecord& r)
743{ 1184{
744 std::lock_guard<std::mutex> lk(gPayoutExecMutex); 1185 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
745 for (auto it = gPaying.begin(); it != gPaying.end(); ++it) 1186 for (auto it = gPaying.begin(); it != gPaying.end(); ++it)
746 if (SamePayment(*it, r)) { gPaying.erase(it); break; } 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
747 SavePayingLocked(); 1198 SavePayingLocked();
748} 1199}
749 1200
@@ -753,14 +1204,77 @@ static void ReschedulePayment(const PayingRecord& r)
753 std::lock_guard<std::mutex> lk(gPayoutExecMutex); 1204 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
754 for (auto& e : gPaying) 1205 for (auto& e : gPaying)
755 if (SamePayment(e, r)) { 1206 if (SamePayment(e, r)) {
756 e.retries = r.retries; 1207 e.retries = r.retries;
757 e.deferrals = r.deferrals; 1208 e.deferrals = r.deferrals;
758 e.retryAfter = r.retryAfter; 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;
759 break; 1213 break;
760 } 1214 }
761 SavePayingLocked(); 1215 SavePayingLocked();
762} 1216}
763 1217
1218// Record that a recipient has been paid, in the ledger, which is the only
1219// durable statement that the debt is settled.
1220//
1221// The file write happens with gPayoutLedgerMutex RELEASED. Upstream calls
1222// SavePendingPayoutsLocked() inside the per-recipient loop with the mutex
1223// held, so every recipient costs the event loop a full serialise+fsync it has
1224// to wait for in FlushMaturePayouts.
1225static void RecordPayoutTxid(const PayingRecord& r, const std::string& txid)
1226{
1227 std::string blob;
1228 bool complete = false;
1229 {
1230 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
1231 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ++it) {
1232 if (it->blockHash != r.blockHash)
1233 continue;
1234 it->paidTxids[r.address] = txid;
1235 it->retryAfter = 0;
1236 if (PayoutFullyPaid(*it)) {
1237 LogPrint("payout", "[payout] payout for block %d complete --"
1238 " removing from ledger\n", it->blockHeight);
1239 gPendingPayouts.erase(it);
1240 complete = true;
1241 }
1242 break;
1243 }
1244 blob = SerializePendingPayoutsLocked();
1245 }
1246 WritePendingPayoutsFile(blob);
1247 // The round proof records a txid per address, so this is per recipient and
1248 // not only on completion.
1249 MarkPoolRoundPaid(PayoutIdForBlock(r.blockHeight, r.blockHash), r.address, txid);
1250 if (complete)
1251 LogPrint("payout", "[payout] block %d fully settled\n", r.blockHeight);
1252}
1253
1254// Write a recipient off. Only reached after the full retry budget against a
1255// block that is still on the main chain with coin available to spend, i.e. a
1256// wallet fault rather than a maturity or reorg question.
1257//
1258// The amount is removed from the entry rather than marked paid, so the money
1259// is not reported as sent. Once nothing is left owed the entry retires.
1260static void ForfeitPayoutRecipient(const PayingRecord& r)
1261{
1262 std::string blob;
1263 {
1264 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
1265 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ++it) {
1266 if (it->blockHash != r.blockHash)
1267 continue;
1268 it->amounts.erase(r.address);
1269 if (it->amounts.empty() || PayoutFullyPaid(*it))
1270 gPendingPayouts.erase(it);
1271 break;
1272 }
1273 blob = SerializePendingPayoutsLocked();
1274 }
1275 WritePendingPayoutsFile(blob);
1276}
1277
764// Payout execution thread: works through gPaying, calls SendMoney. 1278// Payout execution thread: works through gPaying, calls SendMoney.
765// Runs independently of the event loop so slow wallet operations 1279// Runs independently of the event loop so slow wallet operations
766// (SendMoney takes cs_main) never stall miner I/O. 1280// (SendMoney takes cs_main) never stall miner I/O.
@@ -780,9 +1294,12 @@ static void PayoutThreadFn(void*)
780 } 1294 }
781 1295
782 for (PayingRecord r : batch) { 1296 for (PayingRecord r : batch) {
1297 const std::string id = PayoutIdForBlock(r.blockHeight, r.blockHash);
1298
783 // Re-check here and not only at maturity: a payment can sit in this 1299 // Re-check here and not only at maturity: a payment can sit in this
784 // list for hours across retries, and the reorg that kills its 1300 // list for hours across retries, and the reorg that kills its
785 // funding block can land at any point in that window. 1301 // funding block can land at any point in that window. Upstream has
1302 // no equivalent -- it decides once, when the entry matures.
786 if (!PayoutBlockStillOnChain(r.blockHash)) { 1303 if (!PayoutBlockStillOnChain(r.blockHash)) {
787 LogPrint("payout", "[payout] block %d (%s) orphaned while queued" 1304 LogPrint("payout", "[payout] block %d (%s) orphaned while queued"
788 " -- dropping %s to %s\n", 1305 " -- dropping %s to %s\n",
@@ -792,6 +1309,25 @@ static void PayoutThreadFn(void*)
792 continue; 1309 continue;
793 } 1310 }
794 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 {
1316 bool alreadyPaid = false;
1317 {
1318 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
1319 for (const PendingPayout& pp : gPendingPayouts)
1320 if (pp.blockHash == r.blockHash && pp.paidTxids.count(r.address)) {
1321 alreadyPaid = true;
1322 break;
1323 }
1324 }
1325 if (alreadyPaid) {
1326 ForgetPayment(r);
1327 continue;
1328 }
1329 }
1330
795 uint160 h160; 1331 uint160 h160;
796 if (!AddressToHash160(r.address, h160)) { 1332 if (!AddressToHash160(r.address, h160)) {
797 // Should not happen -- bad addresses are filtered in QueuePayouts 1333 // Should not happen -- bad addresses are filtered in QueuePayouts
@@ -803,6 +1339,22 @@ static void PayoutThreadFn(void*)
803 CScript sc; 1339 CScript sc;
804 sc << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG; 1340 sc << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG;
805 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;
1355 }
1356 }
1357
806 // Waiting for coin to mature is not a failure, so it must not 1358 // Waiting for coin to mature is not a failure, so it must not
807 // consume the retry budget. Burning a retry every 60s means a 1359 // consume the retry budget. Burning a retry every 60s means a
808 // payment the pool fully intends to honour can hit GIVING UP 1360 // payment the pool fully intends to honour can hit GIVING UP
@@ -825,11 +1377,22 @@ static void PayoutThreadFn(void*)
825 1377
826 CWalletTx wtx; 1378 CWalletTx wtx;
827 wtx.mapValue["comment"] = PayoutComment(r.blockHeight); 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);
828 1388
829 if (SendMoney(sc, r.amount, wtx)) { 1389 if (SendMoney(sc, r.amount, wtx)) {
830 // The wallet holds the transaction before this returns, so a 1390 // The wallet holds the transaction before this returns, so a
831 // crash here is recoverable: ReconcilePayouts finds it by 1391 // crash between here and the ledger write is recoverable: the
832 // comment+address+amount and does not resend. 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());
833 ForgetPayment(r); 1396 ForgetPayment(r);
834 LogPrint("payout", "[payout] paid %s to %s (tx %s)\n", 1397 LogPrint("payout", "[payout] paid %s to %s (tx %s)\n",
835 FormatMoney(r.amount).c_str(), r.address.c_str(), 1398 FormatMoney(r.amount).c_str(), r.address.c_str(),
@@ -857,6 +1420,12 @@ static void PayoutThreadFn(void*)
857 FormatMoney(r.amount).c_str(), r.address.c_str(), 1420 FormatMoney(r.amount).c_str(), r.address.c_str(),
858 r.retries, r.blockHeight, 1421 r.retries, r.blockHeight,
859 FormatMoney(nSpendable).c_str()); 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);
860 ForgetPayment(r); 1429 ForgetPayment(r);
861 } 1430 }
862 } 1431 }
@@ -881,6 +1450,7 @@ static StatsSnapshot gStats;
881static std::vector<PoolWorkerStatView> gWorkerStats; 1450static std::vector<PoolWorkerStatView> gWorkerStats;
882static std::vector<PendingPayoutView> gPayoutViews; 1451static std::vector<PendingPayoutView> gPayoutViews;
883static std::mutex gStatsMutex; 1452static std::mutex gStatsMutex;
1453static int64 gLastPoolStatusWrite = 0;
884 1454
885void GetPoolOperatorStats(int& authorizedMiners, int& blocksFound, uint64& roundShares, double& totalHashRate) 1455void GetPoolOperatorStats(int& authorizedMiners, int& blocksFound, uint64& roundShares, double& totalHashRate)
886{ 1456{
@@ -903,6 +1473,107 @@ void GetPendingPayouts(std::vector<PendingPayoutView>& out)
903 out = gPayoutViews; 1473 out = gPayoutViews;
904} 1474}
905 1475
1476static std::string PoolStatusFilePath()
1477{
1478 if (!strPoolStatusFile.empty())
1479 return strPoolStatusFile;
1480 return GetAppDir() + "/pool_status.json";
1481}
1482
1483static void WritePoolStatusJsonLocked()
1484{
1485 nlohmann::json j;
1486 j["schema"] = "bitflash-pool-status-1";
1487 j["network"] = "bitflash";
1488 j["updatedAt"] = GetTime();
1489
1490 j["operator"] = {
1491 {"name", strPoolName},
1492 {"btfAddress", BtfLocalAddress()},
1493 {"feePercent", dPoolFeePercent},
1494 {"dashboardUrl", strPoolDashboardUrl},
1495 {"roundsFile", "pool_rounds.json"}
1496 };
1497
1498 j["node"] = {
1499 {"height", nBestHeight},
1500 {"poolRunning", (bool)gPoolRunning},
1501 {"mode", nMineMode}
1502 };
1503
1504 j["pool"] = {
1505 {"authorizedMiners", gStats.authorizedMiners},
1506 {"blocksFoundSession", gStats.blocksFound},
1507 {"roundShares", gStats.roundShares},
1508 {"hashRate", gStats.totalHashRate}
1509 };
1510
1511 j["workers"] = nlohmann::json::array();
1512 for (const PoolWorkerStatView& row : gWorkerStats) {
1513 j["workers"].push_back({
1514 {"address", row.address},
1515 {"worker", row.worker},
1516 {"totalShares", row.totalShares},
1517 {"roundShares", row.roundShares},
1518 {"lastSeen", row.lastSeen},
1519 {"hashRate", row.hashRate}
1520 });
1521 }
1522
1523 j["pendingPayouts"] = nlohmann::json::array();
1524 for (const PendingPayoutView& row : gPayoutViews) {
1525 j["pendingPayouts"].push_back({
1526 {"matureAtHeight", row.matureAtHeight},
1527 {"recipients", row.recipients},
1528 {"totalAmountSatoshis", row.totalAmount},
1529 {"totalAmountBtf", FormatMoney(row.totalAmount)}
1530 });
1531 }
1532
1533 j["recentRounds"] = nlohmann::json::array();
1534 {
1535 std::lock_guard<std::mutex> lk(gPoolRoundsMutex);
1536 size_t start = gPoolRounds.size() > 10 ? gPoolRounds.size() - 10 : 0;
1537 for (size_t i = start; i < gPoolRounds.size(); i++) {
1538 const PoolRoundProof& r = gPoolRounds[i];
1539 int unpaid = 0;
1540 for (const auto& kv : r.payouts)
1541 if (r.paidTxids.count(kv.first) == 0)
1542 unpaid++;
1543 j["recentRounds"].push_back({
1544 {"id", r.id},
1545 {"blockHeight", r.blockHeight},
1546 {"blockHash", r.blockHash},
1547 {"coinbaseTxid", r.coinbaseTxid},
1548 {"totalShares", r.totalShares},
1549 {"payouts", (int)r.payouts.size()},
1550 {"unpaidPayouts", unpaid},
1551 {"coinbaseValueBtf", FormatMoney(r.coinbaseValue)},
1552 {"minerRewardBtf", FormatMoney(r.minerReward)}
1553 });
1554 }
1555 }
1556
1557 std::string path = PoolStatusFilePath();
1558 std::string tmp = path + ".tmp";
1559 FILE* f = fopen(tmp.c_str(), "w");
1560 if (!f) {
1561 LogPrint("pool", "[pool] WARNING: could not write status file %s\n", tmp.c_str());
1562 return;
1563 }
1564 std::string s = j.dump(2);
1565 fwrite(s.c_str(), 1, s.size(), f);
1566 if (fclose(f) != 0) {
1567 LogPrint("pool", "[pool] WARNING: could not close status file %s\n", tmp.c_str());
1568 return;
1569 }
1570#ifdef _WIN32
1571 remove(path.c_str());
1572#endif
1573 if (rename(tmp.c_str(), path.c_str()) != 0)
1574 LogPrint("pool", "[pool] WARNING: could not replace status file %s\n", path.c_str());
1575}
1576
906// --------------------------------------------------------------------------- 1577// ---------------------------------------------------------------------------
907// Miner state -- event loop thread only 1578// Miner state -- event loop thread only
908// --------------------------------------------------------------------------- 1579// ---------------------------------------------------------------------------
@@ -1453,7 +2124,11 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
1453 int foundHeight = gCurrentJob.height; 2124 int foundHeight = gCurrentJob.height;
1454 // b is the block we submitted, untouched by ProcessBlock (which 2125 // b is the block we submitted, untouched by ProcessBlock (which
1455 // took a copy), so its hash is what the payout must be keyed on. 2126 // took a copy), so its hash is what the payout must be keyed on.
1456 QueuePayouts(foundHeight, b.GetHash(), roundShareCount, roundShareTotal); 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(),
2130 b.vtx[0].vout[0].nValue, m->address,
2131 roundShareCount, roundShareTotal);
1457 // Reset round shares 2132 // Reset round shares
1458 roundShareCount.clear(); 2133 roundShareCount.clear();
1459 roundShareTotal = 0; 2134 roundShareTotal = 0;
@@ -1479,6 +2154,147 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
1479 return true; 2154 return true;
1480} 2155}
1481 2156
2157int RunPoolStratumSelfTest()
2158{
2159 int nFail = 0;
2160 auto check = [&](bool ok, const char* msg) {
2161 fprintf(stdout, " %s %s\n", ok ? "ok" : "FAIL", msg);
2162 if (!ok) nFail++;
2163 };
2164
2165 fprintf(stdout, "pool-stratum self-test\n");
2166
2167 StratumJob savedJob = gCurrentJob;
2168 bool savedHaveJob = gHaveJob;
2169
2170 CTransaction coinbase;
2171 coinbase.vin.resize(1);
2172 coinbase.vin[0].prevout.SetNull();
2173 coinbase.vin[0].scriptSig << 1 << (CBigNum)1;
2174 coinbase.vout.resize(1);
2175 coinbase.vout[0].nValue = 50 * COIN;
2176
2177 CBlock block;
2178 block.vtx.push_back(coinbase);
2179 block.nVersion = 1;
2180 block.nBits = 0x1f0fffff;
2181 block.nTime = 1;
2182 block.nNonce = 1;
2183 block.hashMerkleRoot = block.BuildMerkleTree();
2184
2185 gCurrentJob.jobId = "selftest";
2186 gCurrentJob.block = block;
2187 gCurrentJob.target = 0; // accepted share, not a block candidate
2188 gCurrentJob.height = 1;
2189 gHaveJob = true;
2190
2191 Miner miner(INVALID_SOCKET);
2192 miner.authorised = true;
2193 miner.address = "selftest-address";
2194 miner.worker = "worker";
2195 miner.shareTarget = ~uint256(0); // every RandomX hash is a valid share
2196 miner.difficulty = 0.001;
2197 miner.vardiffWindowStart = GetTime();
2198
2199 std::map<std::string, uint64> roundShareCount;
2200 uint64 roundShareTotal = 0;
2201 int blocksFound = 0;
2202
2203 uint256 targetFromNotify;
2204 CBlock minerBlock = BuildMinerBlock(gCurrentJob, miner.extranonce2);
2205 json params = MakeNotifyParams(gCurrentJob, minerBlock, miner.shareTarget, true);
2206 std::vector<unsigned char> targetBytes = FromHex(params[3].get<std::string>());
2207 if (targetBytes.size() == 32)
2208 memcpy(&targetFromNotify, &targetBytes[0], 32);
2209 check(targetFromNotify == miner.shareTarget,
2210 "mining.notify exposes the same share target the pool validates");
2211
2212 json submit = {
2213 {"id", 7},
2214 {"method", "mining.submit"},
2215 {"params", json::array({miner.address, "selftest", ToHex(&minerBlock.nNonce, 4)})}
2216 };
2217 HandleLine(&miner, submit.dump(), roundShareCount, roundShareTotal, blocksFound);
2218
2219 check(roundShareTotal == 1, "fake low-difficulty share increments the round total");
2220 check(roundShareCount[miner.address] == 1, "fake share is credited to the submitting address");
2221 check(miner.sessionShares == 1 && miner.roundShares == 1 && miner.totalShares == 1,
2222 "miner share counters advance together");
2223 check(blocksFound == 0, "share below block target is not counted as a found block");
2224
2225 json stale = {
2226 {"id", 8},
2227 {"method", "mining.submit"},
2228 {"params", json::array({miner.address, "stale", ToHex(&minerBlock.nNonce, 4)})}
2229 };
2230 HandleLine(&miner, stale.dump(), roundShareCount, roundShareTotal, blocksFound);
2231 check(roundShareTotal == 1, "stale submit does not increment shares");
2232
2233 std::vector<PoolRoundProof> savedRounds = gPoolRounds;
2234 std::string savedRoundsFile = strPoolRoundsFile;
2235 std::string proofPath = "pool_rounds_selftest.json";
2236 remove(proofPath.c_str());
2237 remove((proofPath + ".tmp").c_str());
2238 gPoolRounds.clear();
2239 strPoolRoundsFile = proofPath;
2240
2241 PoolRoundProof proof;
2242 proof.id = "selftest-round";
2243 proof.blockHeight = 42;
2244 proof.matureAtHeight = 42 + COINBASE_MATURITY;
2245 proof.createdAt = GetTime();
2246 proof.blockHash = block.GetHash().GetHex();
2247 proof.coinbaseTxid = coinbase.GetHash().GetHex();
2248 proof.foundBy = miner.address;
2249 proof.feePercent = 1.25;
2250 proof.totalShares = 3;
2251 proof.coinbaseValue = 50 * COIN;
2252 proof.subsidy = 50 * COIN;
2253 proof.minerReward = 49 * COIN;
2254 proof.operatorReward = 1 * COIN;
2255 proof.shares[miner.address] = 3;
2256 proof.payouts[miner.address] = 49 * COIN;
2257 RecordPoolRound(proof);
2258 MarkPoolRoundPaid(proof.id, miner.address, "selftest-txid");
2259
2260 bool proofOk = false;
2261 FILE* pf = fopen(proofPath.c_str(), "r");
2262 if (pf) {
2263 fseek(pf, 0, SEEK_END);
2264 long sz = ftell(pf);
2265 fseek(pf, 0, SEEK_SET);
2266 std::string s(sz, '\0');
2267 fread(&s[0], 1, sz, pf);
2268 fclose(pf);
2269 try {
2270 json root = json::parse(s);
2271 proofOk =
2272 root.value("schema", "") == "bitflash-pool-rounds-1" &&
2273 root["rounds"].is_array() &&
2274 root["rounds"].size() == 1 &&
2275 root["rounds"][0].value("id", "") == proof.id &&
2276 root["rounds"][0]["shares"].value(miner.address, (uint64)0) == 3 &&
2277 root["rounds"][0]["payouts"][miner.address].value("txid", "") == "selftest-txid";
2278 } catch (...) {
2279 proofOk = false;
2280 }
2281 }
2282 check(proofOk, "pool round proof ledger is written and paid txid is recorded");
2283
2284 remove(proofPath.c_str());
2285 remove((proofPath + ".tmp").c_str());
2286 strPoolRoundsFile = savedRoundsFile;
2287 gPoolRounds = savedRounds;
2288
2289 gCurrentJob = savedJob;
2290 gHaveJob = savedHaveJob;
2291
2292 fprintf(stdout, "%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
2293 nFail, nFail == 1 ? "" : "s");
2294 fflush(stdout);
2295 return nFail == 0 ? 0 : 1;
2296}
2297
1482// --------------------------------------------------------------------------- 2298// ---------------------------------------------------------------------------
1483// Stats refresh -- called every 2s from event loop 2299// Stats refresh -- called every 2s from event loop
1484// --------------------------------------------------------------------------- 2300// ---------------------------------------------------------------------------
@@ -1512,16 +2328,27 @@ static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal)
1512 : a.lastSeen > b.lastSeen; 2328 : a.lastSeen > b.lastSeen;
1513 }); 2329 });
1514 2330
1515 // gPendingPayouts is event-loop-only data (no lock needed here since
1516 // RefreshStats is always called from the event loop thread).
1517 gPayoutViews.clear(); 2331 gPayoutViews.clear();
2332 std::lock_guard<std::mutex> ledger(gPayoutLedgerMutex);
1518 for (const PendingPayout& pp : gPendingPayouts) { 2333 for (const PendingPayout& pp : gPendingPayouts) {
1519 PendingPayoutView pv; 2334 PendingPayoutView pv;
1520 pv.matureAtHeight = pp.matureAtHeight; 2335 pv.matureAtHeight = pp.matureAtHeight;
1521 pv.recipients = (int)pp.amounts.size(); 2336 pv.recipients = 0;
1522 pv.totalAmount = 0; 2337 pv.totalAmount = 0;
1523 for (auto& kv : pp.amounts) pv.totalAmount += kv.second; 2338 for (auto& kv : pp.amounts) {
1524 gPayoutViews.push_back(pv); 2339 if (pp.paidTxids.count(kv.first))
2340 continue;
2341 pv.recipients++;
2342 pv.totalAmount += kv.second;
2343 }
2344 if (pv.recipients > 0)
2345 gPayoutViews.push_back(pv);
2346 }
2347
2348 int64 now = GetTime();
2349 if (now - gLastPoolStatusWrite >= 10) {
2350 WritePoolStatusJsonLocked();
2351 gLastPoolStatusWrite = now;
1525 } 2352 }
1526} 2353}
1527 2354
@@ -1901,10 +2728,11 @@ void ThreadRPCServer(void*)
1901 } 2728 }
1902 2729
1903 LoadPendingPayouts(); 2730 LoadPendingPayouts();
2731 LoadPoolRounds();
1904 LoadPaying(); 2732 LoadPaying();
1905 // Must run before the payout thread starts, so nothing is resent that the 2733 // Must run before the payout thread starts, so nothing is resent that the
1906 // wallet already shows as sent, and nothing is owed twice because we died 2734 // ledger already shows as sent, and every restored in-flight record is
1907 // between recording the payments and rewriting the ledger. 2735 // armed for its one wallet probe before it is allowed to send.
1908 ReconcilePayouts(); 2736 ReconcilePayouts();
1909 2737
1910 _beginthread(AcceptLoopFn, 0, NULL); 2738 _beginthread(AcceptLoopFn, 0, NULL);
diff --git a/src/selftest.cpp b/src/selftest.cpp
index d12c96b..eed6779 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -7,9 +7,13 @@
7 7
8#include "headers_core.h" 8#include "headers_core.h"
9#include "bip32.h" 9#include "bip32.h"
10#include "proxy.h"
10#include "selftest.h" 11#include "selftest.h"
12#include "sockcount.h"
11#include "walletcmd.h" 13#include "walletcmd.h"
12 14
15extern int RunPoolStratumSelfTest();
16
13// Test results go to the terminal, not to debug.log. 17// Test results go to the terminal, not to debug.log.
14// 18//
15// printf in this tree is OutputDebugStringF, which writes into the data 19// printf in this tree is OutputDebugStringF, which writes into the data
@@ -348,17 +352,17 @@ static int RunWalletHDSelfTest()
348 nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1; 352 nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1;
349 nFail += Check(nHDKeySchema == HD_SCHEMA_BIP44, 353 nFail += Check(nHDKeySchema == HD_SCHEMA_BIP44,
350 "a new recovery phrase records the BIP44 HD schema") ? 0 : 1; 354 "a new recovery phrase records the BIP44 HD schema") ? 0 : 1;
351 nFail += Check(nHDCoinType == HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, 355 nFail += Check(nHDCoinType == HD_BIP44_COIN_TYPE_BITFLASH,
352 "a new recovery phrase records the provisional BIP44 coin type") ? 0 : 1; 356 "a new recovery phrase records the SLIP-0044 BIP44 coin type") ? 0 : 1;
353 nFail += Check(nHDReceiveNext == 1 && nHDChangeNext == 0, 357 nFail += Check(nHDReceiveNext == 1 && nHDChangeNext == 0,
354 "BIP44 receive/change counters reserve the default receive key") ? 0 : 1; 358 "BIP44 receive/change counters reserve the default receive key") ? 0 : 1;
355 std::vector<unsigned int> vBIP44Path = HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, 359 std::vector<unsigned int> vBIP44Path = HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH,
356 HD_BIP44_ACCOUNT, 360 HD_BIP44_ACCOUNT,
357 HD_BIP44_CHAIN_RECEIVE, 361 HD_BIP44_CHAIN_RECEIVE,
358 0); 362 0);
359 nFail += Check(vBIP44Path.size() == 5 && 363 nFail += Check(vBIP44Path.size() == 5 &&
360 vBIP44Path[0] == (HD_BIP44_PURPOSE | bitflash::BIP32_HARDENED) && 364 vBIP44Path[0] == (HD_BIP44_PURPOSE | bitflash::BIP32_HARDENED) &&
361 vBIP44Path[1] == (HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL | bitflash::BIP32_HARDENED) && 365 vBIP44Path[1] == (HD_BIP44_COIN_TYPE_BITFLASH | bitflash::BIP32_HARDENED) &&
362 vBIP44Path[2] == (HD_BIP44_ACCOUNT | bitflash::BIP32_HARDENED) && 366 vBIP44Path[2] == (HD_BIP44_ACCOUNT | bitflash::BIP32_HARDENED) &&
363 vBIP44Path[3] == HD_BIP44_CHAIN_RECEIVE && 367 vBIP44Path[3] == HD_BIP44_CHAIN_RECEIVE &&
364 vBIP44Path[4] == 0, 368 vBIP44Path[4] == 0,
@@ -380,7 +384,7 @@ static int RunWalletHDSelfTest()
380 hdParent.chainCode = vchHDChainCode; 384 hdParent.chainCode = vchHDChainCode;
381 bitflash::BIP32PrivateNode hdBIP44ReceiveChild; 385 bitflash::BIP32PrivateNode hdBIP44ReceiveChild;
382 if (!bitflash::BIP32DerivePath(hdParent, 386 if (!bitflash::BIP32DerivePath(hdParent,
383 HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, 387 HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH,
384 HD_BIP44_ACCOUNT, 388 HD_BIP44_ACCOUNT,
385 HD_BIP44_CHAIN_RECEIVE, 389 HD_BIP44_CHAIN_RECEIVE,
386 0), 390 0),
@@ -568,7 +572,7 @@ static int RunWalletHDSelfTest()
568 nFail += Check(audit.fHaveSeed, "the recovery audit reports the phrase") ? 0 : 1; 572 nFail += Check(audit.fHaveSeed, "the recovery audit reports the phrase") ? 0 : 1;
569 nFail += Check(audit.nSchema == HD_SCHEMA_BIP44, 573 nFail += Check(audit.nSchema == HD_SCHEMA_BIP44,
570 "the recovery audit reports the derivation schema") ? 0 : 1; 574 "the recovery audit reports the derivation schema") ? 0 : 1;
571 nFail += Check(audit.nCoinType == HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL, 575 nFail += Check(audit.nCoinType == HD_BIP44_COIN_TYPE_BITFLASH,
572 "the recovery audit reports the BIP44 coin type") ? 0 : 1; 576 "the recovery audit reports the BIP44 coin type") ? 0 : 1;
573 nFail += Check(audit.nReceiveNext == nHDReceiveNext && audit.nChangeNext == 1, 577 nFail += Check(audit.nReceiveNext == nHDReceiveNext && audit.nChangeNext == 1,
574 "the recovery audit reports receive/change counters") ? 0 : 1; 578 "the recovery audit reports receive/change counters") ? 0 : 1;
@@ -1241,6 +1245,314 @@ static int RunConsensusLimitsSelfTest()
1241 return nFail == 0 ? 0 : 1; 1245 return nFail == 0 ? 0 : 1;
1242} 1246}
1243 1247
1248static int RunParseMoneySelfTest()
1249{
1250 printf("parse-money self-test\n");
1251 int nFail = 0;
1252 struct Case { const char* in; bool ok; int64 val; };
1253 Case cases[] = {
1254 {"0.5", true, 50000000LL},
1255 {"1", true, 100000000LL},
1256 {"1.23456789", true, 123456789LL},
1257 {"0.00000001", true, 1LL},
1258 {"10.00000000", true, 1000000000LL},
1259 {"1,000", true, 100000000000LL},
1260 {"1.5 ", true, 150000000LL},
1261 {"0.123456789", false, 0LL},
1262 {"1.2.3", false, 0LL},
1263 {"abc", false, 0LL},
1264 {"-1", false, 0LL},
1265 {"1e5", false, 0LL},
1266 };
1267 for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++)
1268 {
1269 int64 v = -1;
1270 bool r = ParseMoney(cases[i].in, v);
1271 bool pass = (r == cases[i].ok) && (!r || v == cases[i].val);
1272 char desc[160];
1273 snprintf(desc, sizeof(desc), "ParseMoney(\"%s\") %s and value matches",
1274 cases[i].in, cases[i].ok ? "accepts" : "rejects");
1275 nFail += Check(pass, desc) ? 0 : 1;
1276 }
1277 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
1278 nFail, nFail == 1 ? "" : "s");
1279 fflush(stdout);
1280 return nFail == 0 ? 0 : 1;
1281}
1282
1283static bool SelfTestReadN(SOCKET s, void* buf, int n)
1284{
1285 char* p = (char*)buf;
1286 int off = 0;
1287 while (off < n)
1288 {
1289 int r = recv(s, p + off, n - off, 0);
1290 if (r <= 0)
1291 return false;
1292 off += r;
1293 }
1294 return true;
1295}
1296
1297static bool SelfTestWriteN(SOCKET s, const void* buf, int n)
1298{
1299 const char* p = (const char*)buf;
1300 int off = 0;
1301 while (off < n)
1302 {
1303 int r = send(s, p + off, n - off, 0);
1304 if (r <= 0)
1305 return false;
1306 off += r;
1307 }
1308 return true;
1309}
1310
1311static void SelfTestSetSocketTimeout(SOCKET s, int nTimeoutSecs)
1312{
1313#ifdef _WIN32
1314 DWORD tv = (DWORD)nTimeoutSecs * 1000;
1315 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
1316 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv));
1317#else
1318 struct timeval tv;
1319 tv.tv_sec = nTimeoutSecs;
1320 tv.tv_usec = 0;
1321 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
1322 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
1323#endif
1324}
1325
1326#ifdef _WIN32
1327struct SelfTestWinsock
1328{
1329 bool fStarted;
1330 SelfTestWinsock() : fStarted(false)
1331 {
1332 WSADATA wsadata;
1333 fStarted = (WSAStartup(MAKEWORD(2,2), &wsadata) == 0);
1334 }
1335 ~SelfTestWinsock()
1336 {
1337 if (fStarted)
1338 WSACleanup();
1339 }
1340};
1341#endif
1342
1343static bool RunSocks5HandshakeProbe(std::string& errOut)
1344{
1345 errOut.clear();
1346#ifdef _WIN32
1347 SelfTestWinsock winsock;
1348 if (!winsock.fStarted)
1349 {
1350 errOut = "could not start Winsock";
1351 return false;
1352 }
1353#endif
1354
1355 SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
1356 if (listener == INVALID_SOCKET)
1357 {
1358 errOut = "could not create listener";
1359 return false;
1360 }
1361 SelfTestSetSocketTimeout(listener, 5);
1362
1363 struct sockaddr_in addr;
1364 memset(&addr, 0, sizeof(addr));
1365 addr.sin_family = AF_INET;
1366 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
1367 addr.sin_port = 0;
1368 if (bind(listener, (struct sockaddr*)&addr, sizeof(addr)) != 0 ||
1369 listen(listener, 1) != 0)
1370 {
1371 closesocket(listener);
1372 errOut = "could not bind/listen on loopback";
1373 return false;
1374 }
1375
1376#ifdef _WIN32
1377 int addrLen = sizeof(addr);
1378#else
1379 socklen_t addrLen = sizeof(addr);
1380#endif
1381 if (getsockname(listener, (struct sockaddr*)&addr, &addrLen) != 0)
1382 {
1383 closesocket(listener);
1384 errOut = "could not read listener port";
1385 return false;
1386 }
1387 unsigned short proxyPort = ntohs(addr.sin_port);
1388
1389 bool fServerOk = false;
1390 std::string serverErr;
1391 std::string requestedHost;
1392 unsigned short requestedPort = 0;
1393 std::thread server([&]() {
1394 SOCKET s = accept(listener, NULL, NULL);
1395 if (s == INVALID_SOCKET)
1396 {
1397 serverErr = "accept failed";
1398 return;
1399 }
1400 SelfTestSetSocketTimeout(s, 5);
1401
1402 unsigned char hello[3] = {0,0,0};
1403 if (!SelfTestReadN(s, hello, sizeof(hello)) ||
1404 hello[0] != 0x05 || hello[1] != 0x01 || hello[2] != 0x00)
1405 {
1406 serverErr = "bad SOCKS5 greeting";
1407 closesocket(s);
1408 return;
1409 }
1410 unsigned char choice[2] = {0x05, 0x00};
1411 if (!SelfTestWriteN(s, choice, sizeof(choice)))
1412 {
1413 serverErr = "could not write method choice";
1414 closesocket(s);
1415 return;
1416 }
1417
1418 unsigned char hdr[5] = {0,0,0,0,0};
1419 if (!SelfTestReadN(s, hdr, sizeof(hdr)) ||
1420 hdr[0] != 0x05 || hdr[1] != 0x01 || hdr[2] != 0x00 || hdr[3] != 0x03)
1421 {
1422 serverErr = "CONNECT did not use domain-name address type";
1423 closesocket(s);
1424 return;
1425 }
1426
1427 unsigned char len = hdr[4];
1428 std::vector<unsigned char> rest((size_t)len + 2);
1429 if (!SelfTestReadN(s, &rest[0], (int)rest.size()))
1430 {
1431 serverErr = "could not read CONNECT target";
1432 closesocket(s);
1433 return;
1434 }
1435 requestedHost.assign((const char*)&rest[0], (size_t)len);
1436 requestedPort = ((unsigned short)rest[len] << 8) | rest[len + 1];
1437
1438 unsigned char reply[10] = {0x05, 0x00, 0x00, 0x01, 0,0,0,0, 0,0};
1439 if (!SelfTestWriteN(s, reply, sizeof(reply)))
1440 {
1441 serverErr = "could not write CONNECT reply";
1442 closesocket(s);
1443 return;
1444 }
1445 fServerOk = true;
1446 closesocket(s);
1447 });
1448
1449 std::string err;
1450 BtfClearSocks5Proxy();
1451 bool fClientOk = BtfSetSocks5Proxy(strprintf("127.0.0.1:%u", (unsigned)proxyPort), err);
1452 SOCKET client = INVALID_SOCKET;
1453 if (fClientOk)
1454 client = BtfConnectSocket("relay.example", 443, SOCK_NOSTR, 5);
1455 if (client != INVALID_SOCKET)
1456 BtfCloseSocket(client);
1457 BtfClearSocks5Proxy();
1458 closesocket(listener);
1459 server.join();
1460
1461 if (!fClientOk)
1462 errOut = strprintf("could not enable proxy: %s", err.c_str());
1463 else if (client == INVALID_SOCKET)
1464 errOut = "client SOCKS5 connect failed";
1465 else if (!fServerOk)
1466 errOut = serverErr.empty() ? "mock server failed" : serverErr;
1467 else if (requestedHost != "relay.example" || requestedPort != 443)
1468 errOut = strprintf("unexpected CONNECT target %s:%u", requestedHost.c_str(), (unsigned)requestedPort);
1469 else
1470 return true;
1471 return false;
1472}
1473
1474static int RunSocks5ProxySelfTest()
1475{
1476 printf("socks5-proxy self-test\n");
1477 int nFail = 0;
1478 std::string host, err;
1479 unsigned short port = 0;
1480
1481 nFail += Check(BtfParseSocks5Proxy("127.0.0.1:9050", host, port, err) &&
1482 host == "127.0.0.1" && port == 9050,
1483 "parses local Tor proxy endpoint") ? 0 : 1;
1484 nFail += Check(BtfParseSocks5Proxy("localhost:9050", host, port, err) &&
1485 host == "localhost" && port == 9050,
1486 "parses hostname proxy endpoint") ? 0 : 1;
1487 nFail += Check(BtfParseSocks5Proxy("[::1]:9050", host, port, err) &&
1488 host == "::1" && port == 9050,
1489 "parses bracketed IPv6 proxy endpoint") ? 0 : 1;
1490 nFail += Check(!BtfParseSocks5Proxy("::1:9050", host, port, err),
1491 "rejects unbracketed IPv6 proxy endpoint") ? 0 : 1;
1492 nFail += Check(!BtfParseSocks5Proxy("localhost", host, port, err),
1493 "rejects missing port") ? 0 : 1;
1494 nFail += Check(!BtfParseSocks5Proxy("localhost:0", host, port, err),
1495 "rejects zero port") ? 0 : 1;
1496 nFail += Check(!BtfParseSocks5Proxy("localhost:70000", host, port, err),
1497 "rejects out-of-range port") ? 0 : 1;
1498 nFail += Check(!BtfParseSocks5Proxy("localhost:999999999999999999999999999999", host, port, err),
1499 "rejects overflowing port") ? 0 : 1;
1500 nFail += Check(!BtfParseSocks5Proxy("user:pass@localhost:9050", host, port, err),
1501 "rejects unsupported authenticated proxy syntax") ? 0 : 1;
1502 nFail += Check(BtfIsTorOnionHost("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion") &&
1503 BtfIsTorOnionHost("Relay.Example.ONION.") &&
1504 !BtfIsTorOnionHost("relay.example") &&
1505 !BtfIsTorOnionHost(".onion"),
1506 "detects .onion hosts without accepting lookalikes") ? 0 : 1;
1507
1508 BtfClearSocks5Proxy();
1509 nFail += Check(!BtfSocks5ProxyEnabled(), "starts disabled after clear") ? 0 : 1;
1510 nFail += Check(BtfSetSocks5Proxy("127.0.0.1:9050", err) &&
1511 BtfSocks5ProxyEnabled() &&
1512 BtfSocks5ProxyName() == "127.0.0.1:9050",
1513 "enables parsed SOCKS5 proxy") ? 0 : 1;
1514 BtfClearSocks5Proxy();
1515 nFail += Check(!BtfSocks5ProxyEnabled(), "clear disables proxy") ? 0 : 1;
1516
1517 nFail += Check(BtfSetSocks5Proxy("[::1]:9050", err) &&
1518 BtfSocks5ProxyEnabled() &&
1519 BtfSocks5ProxyName() == "[::1]:9050",
1520 "enables bracketed IPv6 proxy endpoint") ? 0 : 1;
1521 BtfClearSocks5Proxy();
1522
1523 nFail += Check(BtfEnableTorProxy("", err) &&
1524 BtfSocks5ProxyEnabled() &&
1525 BtfTorProxyEnabled() &&
1526 BtfSocks5ProxyName() == "127.0.0.1:9050",
1527 "enables Tor mode on the default local SOCKS5 endpoint") ? 0 : 1;
1528 BtfClearSocks5Proxy();
1529
1530 nFail += Check(BtfEnableTorProxy("[::1]:9050", err) &&
1531 BtfSocks5ProxyEnabled() &&
1532 BtfTorProxyEnabled() &&
1533 BtfSocks5ProxyName() == "[::1]:9050",
1534 "enables Tor mode on a custom IPv6 endpoint") ? 0 : 1;
1535 BtfClearSocks5Proxy();
1536
1537 nFail += Check(BtfSetSocks5Proxy("127.0.0.1:9051", err) &&
1538 BtfSocks5ProxyEnabled() &&
1539 !BtfTorProxyEnabled(),
1540 "plain SOCKS5 mode is distinct from Tor mode") ? 0 : 1;
1541 BtfClearSocks5Proxy();
1542
1543 std::string handshakeErr;
1544 bool fHandshakeOk = RunSocks5HandshakeProbe(handshakeErr);
1545 nFail += Check(fHandshakeOk, handshakeErr.empty() ?
1546 "completes SOCKS5 domain-name CONNECT handshake" :
1547 strprintf("completes SOCKS5 domain-name CONNECT handshake (%s)",
1548 handshakeErr.c_str()).c_str()) ? 0 : 1;
1549
1550 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
1551 nFail, nFail == 1 ? "" : "s");
1552 fflush(stdout);
1553 return nFail == 0 ? 0 : 1;
1554}
1555
1244int RunSelfTest(const std::string& name) 1556int RunSelfTest(const std::string& name)
1245{ 1557{
1246 AttachTerminal(); 1558 AttachTerminal();
@@ -1259,8 +1571,14 @@ int RunSelfTest(const std::string& name)
1259 return RunNetMessageSelfTest(); 1571 return RunNetMessageSelfTest();
1260 if (name == "consensus-limits") 1572 if (name == "consensus-limits")
1261 return RunConsensusLimitsSelfTest(); 1573 return RunConsensusLimitsSelfTest();
1574 if (name == "pool-stratum")
1575 return RunPoolStratumSelfTest();
1576 if (name == "parse-money")
1577 return RunParseMoneySelfTest();
1578 if (name == "socks5-proxy")
1579 return RunSocks5ProxySelfTest();
1262 1580
1263 printf("Unknown self-test '%s'\n", name.c_str()); 1581 printf("Unknown self-test '%s'\n", name.c_str());
1264 printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits\n"); 1582 printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy\n");
1265 return 1; 1583 return 1;
1266} 1584}
diff --git a/src/sockcount.h b/src/sockcount.h
index 96b6efb..5147f55 100644
--- a/src/sockcount.h
+++ b/src/sockcount.h
@@ -63,6 +63,7 @@ enum
63 SOCK_PAIR_APP, // loopback pair: the app end handed to CNode 63 SOCK_PAIR_APP, // loopback pair: the app end handed to CNode
64 SOCK_PAIR_PUMP, // loopback pair: the pump end 64 SOCK_PAIR_PUMP, // loopback pair: the pump end
65 SOCK_NOSTR, // outbound to a Nostr relay 65 SOCK_NOSTR, // outbound to a Nostr relay
66 SOCK_STRATUM_BRIDGE, // local Stratum bridge listener/miner sockets
66 SOCK_SITES 67 SOCK_SITES
67}; 68};
68 69
diff --git a/src/util.cpp b/src/util.cpp
index d52ca88..0cd0133 100644
--- a/src/util.cpp
+++ b/src/util.cpp
@@ -337,7 +337,7 @@ string FormatMoney(int64 n, bool fPlus)
337bool ParseMoney(const char* pszIn, int64& nRet) 337bool ParseMoney(const char* pszIn, int64& nRet)
338{ 338{
339 string strWhole; 339 string strWhole;
340 int64 nCents = 0; 340 int64 nUnits = 0; // fractional part, scaled to COIN (8 decimals)
341 const char* p = pszIn; 341 const char* p = pszIn;
342 while (isspace(*p)) 342 while (isspace(*p))
343 p++; 343 p++;
@@ -348,12 +348,16 @@ bool ParseMoney(const char* pszIn, int64& nRet)
348 if (*p == '.') 348 if (*p == '.')
349 { 349 {
350 p++; 350 p++;
351 if (!isdigit(p[0]) || !isdigit(p[1])) 351 int64 nMult = COIN / 10; // weight of the first decimal digit
352 while (isdigit(*p) && nMult > 0)
353 {
354 nUnits += nMult * (*p - '0');
355 nMult /= 10;
356 p++;
357 }
358 // reject more than 8 fractional digits rather than silently truncate
359 if (isdigit(*p))
352 return false; 360 return false;
353 nCents = atoi64(p);
354 if (nCents < 0 || nCents > 99)
355 return false;
356 p += 2;
357 break; 361 break;
358 } 362 }
359 if (isspace(*p)) 363 if (isspace(*p))
@@ -365,13 +369,12 @@ bool ParseMoney(const char* pszIn, int64& nRet)
365 for (; *p; p++) 369 for (; *p; p++)
366 if (!isspace(*p)) 370 if (!isspace(*p))
367 return false; 371 return false;
368 if (strWhole.size() > 17) 372 if (strWhole.size() > 10) // 10 whole digits * COIN stays within int64
369 return false; 373 return false;
370 int64 nWhole = atoi64(strWhole); 374 int64 nWhole = atoi64(strWhole);
371 int64 nValue = nWhole * 100 + nCents; 375 int64 nValue = nWhole * COIN + nUnits;
372 if (nValue / 100 != nWhole) 376 if (nValue < 0 || (nValue - nUnits) / COIN != nWhole)
373 return false; 377 return false;
374 nValue *= CENT;
375 nRet = nValue; 378 nRet = nValue;
376 return true; 379 return true;
377} 380}
diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp
index 06f35a5..0ea2251 100644
--- a/src/walletcmd.cpp
+++ b/src/walletcmd.cpp
@@ -354,7 +354,7 @@ int CmdRecoveryAudit()
354 if (audit.fHaveSeed) 354 if (audit.fHaveSeed)
355 { 355 {
356 printf(" derivation schema: %s\n", HDKeySchemaName(audit.nSchema).c_str()); 356 printf(" derivation schema: %s\n", HDKeySchemaName(audit.nSchema).c_str());
357 printf(" BIP44 coin type: %u (provisional BITFLASH)\n", audit.nCoinType); 357 printf(" BIP44 coin type: %u (SLIP-0044 BITFLASH)\n", audit.nCoinType);
358 printf(" derived keys known to this wallet: %u\n", audit.nDerivedKnown); 358 printf(" derived keys known to this wallet: %u\n", audit.nDerivedKnown);
359 printf(" receive/change counters: %u/%u\n", 359 printf(" receive/change counters: %u/%u\n",
360 audit.nReceiveNext, audit.nChangeNext); 360 audit.nReceiveNext, audit.nChangeNext);