diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-23 05:21:31 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-23 05:21:31 +0000 |
| commit | a9b4f8940907c2bf51e431172b628de89734a908 (patch) | |
| tree | ff0a0b19d7d7754b809e35c6eae43dae7a32e1e4 /src | |
| parent | 6d4c3f153cf44cefead39b21191dfb33491bfa5a (diff) | |
| parent | bc3de301d9ed0e5fb5e2034be2e066d77cf23804 (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/Makefile | 60 | ||||
| -rw-r--r-- | src/bignum.h | 4 | ||||
| -rw-r--r-- | src/btfrv.cpp | 9 | ||||
| -rw-r--r-- | src/db.cpp | 25 | ||||
| -rw-r--r-- | src/fuzz_net_message.cpp | 32 | ||||
| -rw-r--r-- | src/fuzz_script.cpp | 59 | ||||
| -rw-r--r-- | src/gui.cpp | 2 | ||||
| -rw-r--r-- | src/main.cpp | 250 | ||||
| -rw-r--r-- | src/main.h | 13 | ||||
| -rw-r--r-- | src/main_gui.cpp | 107 | ||||
| -rw-r--r-- | src/makefile.mingw | 15 | ||||
| -rw-r--r-- | src/net.cpp | 181 | ||||
| -rw-r--r-- | src/net.h | 15 | ||||
| -rw-r--r-- | src/nostr.cpp | 141 | ||||
| -rw-r--r-- | src/proxy.cpp | 377 | ||||
| -rw-r--r-- | src/proxy.h | 34 | ||||
| -rw-r--r-- | src/randomx_pow.cpp | 5 | ||||
| -rw-r--r-- | src/rc/bitcoin.ico | bin | 372526 -> 150897 bytes | |||
| -rw-r--r-- | src/readme.txt | 16 | ||||
| -rw-r--r-- | src/rpc.cpp | 1152 | ||||
| -rw-r--r-- | src/selftest.cpp | 332 | ||||
| -rw-r--r-- | src/sockcount.h | 1 | ||||
| -rw-r--r-- | src/util.cpp | 23 | ||||
| -rw-r--r-- | src/walletcmd.cpp | 2 |
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 | ||
| 4 | CXX ?= g++ | 4 | CXX ?= g++ |
| 5 | FUZZ_CXX ?= clang++ | ||
| 5 | 6 | ||
| 6 | SECP_INC ?= /usr/local/include | 7 | SECP_INC ?= /usr/local/include |
| 7 | SECP_LIB ?= /usr/local/lib | 8 | SECP_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 | ||
| 31 | CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ | 32 | CXXFLAGS = -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) |
| 37 | FUZZ_CXXFLAGS = $(filter-out -O2,$(CXXFLAGS)) -O1 -g -fno-omit-frame-pointer \ | ||
| 38 | -fsanitize=fuzzer-no-link,address,undefined | ||
| 39 | FUZZ_LDFLAGS = -fsanitize=fuzzer,address,undefined | ||
| 36 | 40 | ||
| 37 | LIBS = -L$(SECP_LIB) -lsecp256k1 \ | 41 | LIBS = -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 \ | |||
| 49 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ | 53 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ |
| 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 | ||
| 54 | IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ | 58 | IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ |
| 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 \ | |||
| 57 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ | 61 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ |
| 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 | ||
| 62 | GUI_OBJS = obj/gui.o obj/main_gui.o | 66 | GUI_OBJS = obj/gui.o obj/main_gui.o |
| 63 | 67 | ||
| @@ -117,6 +121,10 @@ NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \ | |||
| 117 | NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \ | 121 | NODE_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 | ||
| 124 | FUZZ_BASE_OBJS = $(patsubst obj/%,obj-fuzz/%,$(NODE_OBJS)) | ||
| 125 | FUZZ_NET_MESSAGE_OBJS = $(FUZZ_BASE_OBJS) obj-fuzz/fuzz_net_message.o | ||
| 126 | FUZZ_SCRIPT_OBJS = $(FUZZ_BASE_OBJS) obj-fuzz/fuzz_script.o | ||
| 127 | |||
| 120 | obj-node/%.o: %.cpp $(HEADERS) | 128 | obj-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 | ||
| 140 | obj-fuzz/%.o: %.cpp $(HEADERS) | ||
| 141 | @mkdir -p obj-fuzz | ||
| 142 | $(FUZZ_CXX) -c $(FUZZ_CXXFLAGS) -DBITFLASH_FUZZ -o $@ $< | ||
| 143 | |||
| 144 | obj-fuzz/sha.o: sha.cpp sha.h | ||
| 145 | @mkdir -p obj-fuzz | ||
| 146 | $(FUZZ_CXX) -c $(FUZZ_CXXFLAGS) -DBITFLASH_FUZZ -O1 -o $@ $< | ||
| 147 | |||
| 148 | obj-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 | |||
| 152 | obj-fuzz/fuzz_script.o: fuzz_script.cpp $(HEADERS) | ||
| 153 | @mkdir -p obj-fuzz | ||
| 154 | $(FUZZ_CXX) -c $(FUZZ_CXXFLAGS) -DBITFLASH_FUZZ -o $@ $< | ||
| 155 | |||
| 156 | fuzz-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 | |||
| 161 | fuzz-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 | |||
| 166 | fuzz-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 | |||
| 172 | fuzz-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 | |||
| 132 | clean: | 179 | clean: |
| 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 | ||
| 135 | test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h | 182 | test_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 | ||
| 93 | static SOCKET ConnectTo(const char* host, unsigned short port) | 96 | static 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. |
| @@ -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 | ||
| 1399 | static void SecureClearBytes(vector<unsigned char>& vch) | ||
| 1400 | { | ||
| 1401 | if (!vch.empty()) | ||
| 1402 | OPENSSL_cleanse(&vch[0], vch.size()); | ||
| 1403 | vch.clear(); | ||
| 1404 | } | ||
| 1405 | |||
| 1399 | bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet) | 1406 | bool 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. | ||
| 14 | map<string,string> mapAddressBook; | ||
| 15 | bool gPoolServerRunning = false; | ||
| 16 | void MainFrameRepaint() {} | ||
| 17 | |||
| 18 | extern "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. | ||
| 14 | map<string,string> mapAddressBook; | ||
| 15 | bool gPoolServerRunning = false; | ||
| 16 | void MainFrameRepaint() {} | ||
| 17 | |||
| 18 | extern "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 | |||
| 109 | int nHashThreads = 0; // participant: /hashthreads override (0 = cores-1) | 109 | int nHashThreads = 0; // participant: /hashthreads override (0 = cores-1) |
| 110 | string strPoolName = "Bitflash Pool"; | 110 | string strPoolName = "Bitflash Pool"; |
| 111 | string strPoolDashboardUrl; | 111 | string strPoolDashboardUrl; |
| 112 | string strPoolStatusFile; | ||
| 113 | string strPoolRoundsFile; | ||
| 112 | double dPoolFeePercent = 0.0; | 114 | double dPoolFeePercent = 0.0; |
| 115 | bool fStratumBridge = false; | ||
| 116 | int nStratumBridgePort = 3333; | ||
| 113 | 117 | ||
| 114 | static std::atomic<uint64> gParticipantSharesSent{0}; | 118 | static std::atomic<uint64> gParticipantSharesSent{0}; |
| 115 | static std::atomic<uint64> gParticipantSharesAccepted{0}; | 119 | static std::atomic<uint64> gParticipantSharesAccepted{0}; |
| @@ -190,6 +194,9 @@ int ParticipantHashThreadCount() | |||
| 190 | return nWorkers - 1; | 194 | return nWorkers - 1; |
| 191 | } | 195 | } |
| 192 | 196 | ||
| 197 | static void SecureClearBytes(vector<unsigned char>& vch); | ||
| 198 | static void SecureClearKeyingMaterial(CKeyingMaterial& vch); | ||
| 199 | |||
| 193 | bool AddKey(const CKey& key) | 200 | bool 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 | ||
| 239 | static void SecureClearBytes(vector<unsigned char>& vch) | ||
| 240 | { | ||
| 241 | if (!vch.empty()) | ||
| 242 | OPENSSL_cleanse(&vch[0], vch.size()); | ||
| 243 | vch.clear(); | ||
| 244 | } | ||
| 245 | |||
| 246 | static void SecureClearKeyingMaterial(CKeyingMaterial& vch) | ||
| 247 | { | ||
| 248 | if (!vch.empty()) | ||
| 249 | OPENSSL_cleanse(&vch[0], vch.size()); | ||
| 250 | vch.clear(); | ||
| 251 | } | ||
| 252 | |||
| 228 | void LockWallet() | 253 | void 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; | |||
| 625 | int nHDKeySchema = HD_SCHEMA_NONE; | 667 | int nHDKeySchema = HD_SCHEMA_NONE; |
| 626 | unsigned int nHDReceiveNext = 0; | 668 | unsigned int nHDReceiveNext = 0; |
| 627 | unsigned int nHDChangeNext = 0; | 669 | unsigned int nHDChangeNext = 0; |
| 628 | unsigned int nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL; | 670 | unsigned int nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH; |
| 629 | 671 | ||
| 630 | string HDKeySchemaName(int nSchema) | 672 | string 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 | |||
| 4178 | struct StratumBridgeClientCtx | ||
| 4179 | { | ||
| 4180 | SOCKET miner; | ||
| 4181 | std::string pool; | ||
| 4182 | }; | ||
| 4183 | |||
| 4184 | static 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 | |||
| 4197 | static 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 | |||
| 4227 | static 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 | |||
| 4285 | void 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 | |||
| 4131 | bool BitcoinMiner(int nThreadId) | 4357 | bool 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 |
| @@ -97,7 +97,11 @@ extern bool fPoolMineBlind; // /poolmineblind: mine on even when deaf to | |||
| 97 | extern int nHashThreads; // participant: /hashthreads override (0 = cores-1) | 97 | extern int nHashThreads; // participant: /hashthreads override (0 = cores-1) |
| 98 | extern string strPoolName; // operator: announced pool name | 98 | extern string strPoolName; // operator: announced pool name |
| 99 | extern string strPoolDashboardUrl; // operator: optional dashboard URL | 99 | extern string strPoolDashboardUrl; // operator: optional dashboard URL |
| 100 | extern string strPoolStatusFile; // operator: optional JSON status output path | ||
| 101 | extern string strPoolRoundsFile; // operator: optional public round proof path | ||
| 100 | extern double dPoolFeePercent; // operator: announced fee percent | 102 | extern double dPoolFeePercent; // operator: announced fee percent |
| 103 | extern bool fStratumBridge; // participant helper: local Stratum bridge | ||
| 104 | extern int nStratumBridgePort; // local bridge listen port, default 3333 | ||
| 101 | extern bool fSoloMineTest; // /solomine: mine without requiring a peer (local test) | 105 | extern 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. |
| 103 | extern int nMinerThreads; | 107 | extern int nMinerThreads; |
| @@ -226,9 +230,9 @@ static const unsigned int HD_BIP44_PURPOSE = 44; | |||
| 226 | static const unsigned int HD_BIP44_ACCOUNT = 0; | 230 | static const unsigned int HD_BIP44_ACCOUNT = 0; |
| 227 | static const unsigned int HD_BIP44_CHAIN_RECEIVE = 0; | 231 | static const unsigned int HD_BIP44_CHAIN_RECEIVE = 0; |
| 228 | static const unsigned int HD_BIP44_CHAIN_CHANGE = 1; | 232 | static 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") |
| 231 | static const unsigned int HD_BIP44_COIN_TYPE_BITFLASH_PROVISIONAL = 4346950; | 235 | static const unsigned int HD_BIP44_COIN_TYPE_BITFLASH = 4346950; |
| 232 | extern int nHDKeySchema; | 236 | extern int nHDKeySchema; |
| 233 | extern unsigned int nHDReceiveNext; // BIP44 external chain | 237 | extern unsigned int nHDReceiveNext; // BIP44 external chain |
| 234 | extern unsigned int nHDChangeNext; // BIP44 internal chain | 238 | extern 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. |
| 286 | bool BitcoinMiner(int nThreadId = 1); | 290 | bool BitcoinMiner(int nThreadId = 1); |
| 287 | void ThreadRPCServer(void* parg); // rpc.cpp -- .btf pool server | 291 | void ThreadRPCServer(void* parg); // rpc.cpp -- .btf pool server |
| 292 | void ThreadStratumBridge(void* parg); // main.cpp -- local Stratum -> .btf bridge | ||
| 288 | void GetParticipantMiningStats(uint64& sharesSent, uint64& sharesAccepted, double& hashRate); | 293 | void GetParticipantMiningStats(uint64& sharesSent, uint64& sharesAccepted, double& hashRate); |
| 289 | void SetParticipantMiningStatus(const std::string& status); | 294 | void SetParticipantMiningStatus(const std::string& status); |
| 290 | std::string GetParticipantMiningStatus(); | 295 | std::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.) | ||
| 443 | static volatile bool g_fHeadlessShutdownDone = false; | ||
| 444 | static 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 | |||
| 371 | int main(int argc, char* argv[]) | 463 | int 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. | ||
| 22 | CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ | 17 | CXXFLAGS = -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 \ | |||
| 42 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ | 37 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ |
| 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 | ||
| 47 | GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o | 42 | GUI_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 | |||
| 56 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ | 51 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ |
| 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 | ||
| 61 | all: bitflash.exe | 56 | all: 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 | ||
| 21 | void ThreadReconnectCachedBtfPeers(void* parg); | 24 | void ThreadReconnectCachedBtfPeers(void* parg); |
| 22 | void ThreadMessageHandler2(void* parg); | 25 | void ThreadMessageHandler2(void* parg); |
| 23 | void ThreadSocketHandler2(void* parg); | 26 | void ThreadSocketHandler2(void* parg); |
| 24 | 27 | ||
| 28 | #ifdef _WIN32 | ||
| 29 | typedef WSAPOLLFD BtfPollFd; | ||
| 30 | static int BtfPoll(BtfPollFd* pfd, unsigned int nfd, int nTimeoutMs) | ||
| 31 | { | ||
| 32 | return WSAPoll(pfd, nfd, nTimeoutMs); | ||
| 33 | } | ||
| 34 | #else | ||
| 35 | typedef struct pollfd BtfPollFd; | ||
| 36 | static 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 | ||
| 159 | static string SockAccountingText() | 176 | static 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 | ||
| 362 | bool GetMyExternalIP(unsigned int& ipRet) | 379 | bool 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 | { |
| @@ -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. |
| 111 | static const unsigned int MAX_CONNECTIONS = 125; | 108 | static 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. |
| 178 | extern int nPeersWatched; // peers in the last select() set | 175 | extern int nPeersWatched; // peers in the last poll() set |
| 179 | extern int64 nBlocksReceived; // blocks handed to ProcessBlock | 176 | extern int64 nBlocksReceived; // blocks handed to ProcessBlock |
| 180 | extern int64 nBlocksWithoutParent; // ...of those, how many arrived orphaned | 177 | extern 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 | ||
| 30 | using json = nlohmann::json; | 35 | using json = nlohmann::json; |
| 31 | 36 | ||
| @@ -468,6 +473,41 @@ static bool BuildSignedEvent(CNostrKey& key, int kind, const json& tags, | |||
| 468 | static const uint64 MAX_WS_FRAME = 1 << 20; // 1 MiB per frame | 473 | static const uint64 MAX_WS_FRAME = 1 << 20; // 1 MiB per frame |
| 469 | static const uint64 MAX_WS_MESSAGE = 4 << 20; // 4 MiB reassembled | 474 | static const uint64 MAX_WS_MESSAGE = 4 << 20; // 4 MiB reassembled |
| 470 | 475 | ||
| 476 | static 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 | ||
| 487 | static 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 | |||
| 471 | class CWebSocket | 511 | class CWebSocket |
| 472 | { | 512 | { |
| 473 | public: | 513 | public: |
| @@ -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 | |||
| 10 | static CCriticalSection cs_socks5; | ||
| 11 | static bool g_fSocks5Proxy = false; | ||
| 12 | static bool g_fTorProxy = false; | ||
| 13 | static std::string g_socks5Host; | ||
| 14 | static unsigned short g_socks5Port = 0; | ||
| 15 | static const char* DEFAULT_TOR_SOCKS5_PROXY = "127.0.0.1:9050"; | ||
| 16 | |||
| 17 | static 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 | |||
| 34 | static 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 | |||
| 48 | static 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 | |||
| 62 | static bool ValidProxyHostChar(char c) | ||
| 63 | { | ||
| 64 | return isalnum((unsigned char)c) || c == '.' || c == '-' || c == '_'; | ||
| 65 | } | ||
| 66 | |||
| 67 | static bool ValidProxyIpv6HostChar(char c) | ||
| 68 | { | ||
| 69 | return isxdigit((unsigned char)c) || c == ':' || c == '.'; | ||
| 70 | } | ||
| 71 | |||
| 72 | static 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 | |||
| 81 | bool 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 | |||
| 90 | static 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 | |||
| 119 | bool 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 | |||
| 199 | static 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 | |||
| 216 | bool BtfSetSocks5Proxy(const std::string& spec, std::string& errOut) | ||
| 217 | { | ||
| 218 | return SetSocks5Proxy(spec, false, errOut); | ||
| 219 | } | ||
| 220 | |||
| 221 | bool BtfEnableTorProxy(const std::string& spec, std::string& errOut) | ||
| 222 | { | ||
| 223 | return SetSocks5Proxy(spec.empty() ? DEFAULT_TOR_SOCKS5_PROXY : spec, true, errOut); | ||
| 224 | } | ||
| 225 | |||
| 226 | void 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 | |||
| 237 | bool BtfSocks5ProxyEnabled() | ||
| 238 | { | ||
| 239 | CRITICAL_BLOCK(cs_socks5) | ||
| 240 | return g_fSocks5Proxy; | ||
| 241 | return false; | ||
| 242 | } | ||
| 243 | |||
| 244 | bool BtfTorProxyEnabled() | ||
| 245 | { | ||
| 246 | CRITICAL_BLOCK(cs_socks5) | ||
| 247 | return g_fSocks5Proxy && g_fTorProxy; | ||
| 248 | return false; | ||
| 249 | } | ||
| 250 | |||
| 251 | std::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 | |||
| 264 | static 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 | |||
| 292 | static 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 | |||
| 344 | SOCKET 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 | |||
| 21 | bool BtfParseSocks5Proxy(const std::string& spec, std::string& hostOut, | ||
| 22 | unsigned short& portOut, std::string& errOut); | ||
| 23 | bool BtfSetSocks5Proxy(const std::string& spec, std::string& errOut); | ||
| 24 | bool BtfEnableTorProxy(const std::string& spec, std::string& errOut); | ||
| 25 | void BtfClearSocks5Proxy(); | ||
| 26 | bool BtfSocks5ProxyEnabled(); | ||
| 27 | bool BtfTorProxyEnabled(); | ||
| 28 | bool BtfIsTorOnionHost(const std::string& host); | ||
| 29 | std::string BtfSocks5ProxyName(); | ||
| 30 | |||
| 31 | SOCKET 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; | |||
| 19 | static randomx_dataset* g_dataset = NULL; | 19 | static randomx_dataset* g_dataset = NULL; |
| 20 | static randomx_vm* g_vmVerify = NULL; | 20 | static randomx_vm* g_vmVerify = NULL; |
| 21 | static CCriticalSection g_csVerify; | 21 | static CCriticalSection g_csVerify; |
| 22 | static std::mutex g_csInit; | ||
| 22 | static bool g_fInit = false; | 23 | static bool g_fInit = false; |
| 23 | static bool g_fFast = false; | 24 | static 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. | |||
| 42 | Command-line options (in addition to the original ones) | 42 | Command-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 | }; |
| 312 | static std::vector<PendingPayout> gPendingPayouts; | 361 | static std::vector<PendingPayout> gPendingPayouts; |
| 362 | static std::mutex gPayoutLedgerMutex; | ||
| 363 | |||
| 364 | struct 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 | }; | ||
| 389 | static std::vector<PoolRoundProof> gPoolRounds; | ||
| 390 | static std::mutex gPoolRoundsMutex; | ||
| 391 | static const size_t MAX_POOL_ROUNDS = 200; | ||
| 313 | 392 | ||
| 314 | static std::string PayoutFilePath() | 393 | static std::string PayoutFilePath() |
| 315 | { | 394 | { |
| 316 | return GetAppDir() + "/pending_payouts.json"; | 395 | return GetAppDir() + "/pending_payouts.json"; |
| 317 | } | 396 | } |
| 318 | 397 | ||
| 319 | static void SavePendingPayouts() | 398 | static 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. | ||
| 418 | static 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 | |||
| 426 | static 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. | ||
| 441 | static 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. | ||
| 466 | static 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. | ||
| 489 | static void SavePendingPayoutsLocked() | ||
| 490 | { | ||
| 491 | WritePendingPayoutsFile(SerializePendingPayoutsLocked()); | ||
| 342 | } | 492 | } |
| 343 | 493 | ||
| 344 | static void LoadPendingPayouts() | 494 | static 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 | }; |
| 400 | static std::vector<PayingRecord> gPaying; // guarded by gPayoutExecMutex | 578 | static std::vector<PayingRecord> gPaying; // guarded by gPayoutExecMutex |
| 401 | static std::mutex gPayoutExecMutex; | 579 | static 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 | // --------------------------------------------------------------------------- | ||
| 673 | static 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 | |||
| 716 | static 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 | |||
| 745 | static 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. |
| 509 | static 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). | ||
| 541 | static void ReconcilePayouts() | 845 | static 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 | ||
| 886 | static 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 | |||
| 902 | static 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. | ||
| 923 | static std::set<std::string> gQueuedPayouts; | ||
| 924 | |||
| 925 | static 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. | ||
| 590 | static void QueuePayouts(int blockHeight, | 957 | static 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. |
| 674 | static void FlushMaturePayouts() | 1078 | static 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(). | ||
| 742 | static void ForgetPayment(const PayingRecord& r) | 1183 | static 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. | ||
| 1225 | static void RecordPayoutTxid(const PayingRecord& r, const std::string& txid) | ||
| 1226 | { | ||
| 1227 | std::string blob; | ||
| 1228 | bool complete = false; | ||
| 1229 | { | ||
| 1230 | std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); | ||
| 1231 | for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ++it) { | ||
| 1232 | if (it->blockHash != r.blockHash) | ||
| 1233 | continue; | ||
| 1234 | it->paidTxids[r.address] = txid; | ||
| 1235 | it->retryAfter = 0; | ||
| 1236 | if (PayoutFullyPaid(*it)) { | ||
| 1237 | LogPrint("payout", "[payout] payout for block %d complete --" | ||
| 1238 | " removing from ledger\n", it->blockHeight); | ||
| 1239 | gPendingPayouts.erase(it); | ||
| 1240 | complete = true; | ||
| 1241 | } | ||
| 1242 | break; | ||
| 1243 | } | ||
| 1244 | blob = SerializePendingPayoutsLocked(); | ||
| 1245 | } | ||
| 1246 | WritePendingPayoutsFile(blob); | ||
| 1247 | // The round proof records a txid per address, so this is per recipient and | ||
| 1248 | // not only on completion. | ||
| 1249 | MarkPoolRoundPaid(PayoutIdForBlock(r.blockHeight, r.blockHash), r.address, txid); | ||
| 1250 | if (complete) | ||
| 1251 | LogPrint("payout", "[payout] block %d fully settled\n", r.blockHeight); | ||
| 1252 | } | ||
| 1253 | |||
| 1254 | // Write a recipient off. Only reached after the full retry budget against a | ||
| 1255 | // block that is still on the main chain with coin available to spend, i.e. a | ||
| 1256 | // wallet fault rather than a maturity or reorg question. | ||
| 1257 | // | ||
| 1258 | // The amount is removed from the entry rather than marked paid, so the money | ||
| 1259 | // is not reported as sent. Once nothing is left owed the entry retires. | ||
| 1260 | static void ForfeitPayoutRecipient(const PayingRecord& r) | ||
| 1261 | { | ||
| 1262 | std::string blob; | ||
| 1263 | { | ||
| 1264 | std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); | ||
| 1265 | for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ++it) { | ||
| 1266 | if (it->blockHash != r.blockHash) | ||
| 1267 | continue; | ||
| 1268 | it->amounts.erase(r.address); | ||
| 1269 | if (it->amounts.empty() || PayoutFullyPaid(*it)) | ||
| 1270 | gPendingPayouts.erase(it); | ||
| 1271 | break; | ||
| 1272 | } | ||
| 1273 | blob = SerializePendingPayoutsLocked(); | ||
| 1274 | } | ||
| 1275 | WritePendingPayoutsFile(blob); | ||
| 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; | |||
| 881 | static std::vector<PoolWorkerStatView> gWorkerStats; | 1450 | static std::vector<PoolWorkerStatView> gWorkerStats; |
| 882 | static std::vector<PendingPayoutView> gPayoutViews; | 1451 | static std::vector<PendingPayoutView> gPayoutViews; |
| 883 | static std::mutex gStatsMutex; | 1452 | static std::mutex gStatsMutex; |
| 1453 | static int64 gLastPoolStatusWrite = 0; | ||
| 884 | 1454 | ||
| 885 | void GetPoolOperatorStats(int& authorizedMiners, int& blocksFound, uint64& roundShares, double& totalHashRate) | 1455 | void 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 | ||
| 1476 | static std::string PoolStatusFilePath() | ||
| 1477 | { | ||
| 1478 | if (!strPoolStatusFile.empty()) | ||
| 1479 | return strPoolStatusFile; | ||
| 1480 | return GetAppDir() + "/pool_status.json"; | ||
| 1481 | } | ||
| 1482 | |||
| 1483 | static 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 | ||
| 2157 | int 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 | ||
| 15 | extern 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 | ||
| 1248 | static 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 | |||
| 1283 | static 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 | |||
| 1297 | static 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 | |||
| 1311 | static 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 | ||
| 1327 | struct 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 | |||
| 1343 | static 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 | |||
| 1474 | static 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 | |||
| 1244 | int RunSelfTest(const std::string& name) | 1556 | int 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) | |||
| 337 | bool ParseMoney(const char* pszIn, int64& nRet) | 337 | bool 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); |