From 6ca1a1d6799474f8ca977b93ab630af7ace879c2 Mon Sep 17 00:00:00 2001 From: Victorio Nascimento <155357371+Vic-Nas@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:45:28 -0400 Subject: Add Linux GUI installer via Wine (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Linux GUI installer via Wine * Fix launcher creation and add BTF_RELAY env var support * Pool server, Linux GUI, unified binary, README update * Root Makefile: make linux / make windows, cleaner README * Remove headless.cpp * Fix XPM color count mismatch causing toolbar image errors on Linux * XPM icons: same code on Windows and Linux, no ifdefs around image loading * Fix GTK icon crash and Linux printf format * btfrv: 10s socket timeout on relay connects * Faster peer discovery: more dials per pass, shorter backoff and sleep * Parallel btf peer connections * Fix GetMyExternalIP - was disabled, now uses modern IP echo services * Fix tunnel disconnect on idle and restore external IP detection * Filter stale Nostr descriptors * Sort peers by freshness, fresh first * Pool: add .btf Stratum listener — miners connect via rendezvous, no port forwarding needed * btf pool: self-replacing listeners, one per relay, no queuing between miners * Mining modes: solo/operator/participant — pool server on-demand, participant Stratum client * Status bar: show external IP and .btf address * README: clean rewrite, concept-first, minimal * make linux produces AppImage * make linux produces AppImage * Pool dashboard: connect instructions, tabs, share bars * Perf fixes: GetMyExternalIP threaded, btf listener counted * Perf fixes: GetMyExternalIP threaded, btf listener counted * Fix pool payouts: defer until coinbase maturity * Pool: clean mode switching, IP warning * Pool: clean mode switching * Fix gPoolRunning: remove static * AppImage: use real icon from bitcoin.ico * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Windows build: use mingw32-make * Windows build: simpler make invocation * Add deps-windows target * Windows build fixes * Windows build fixes * Windows makefile: fix obj dir * Windows: output to repo root * Windows: bundle DLLs into zip * Replace wxWidgets with ImGui+GLFW: same build on Linux and Windows * Fix %I64d that slipped back through rebase (main.h, net.h) * MinGW: add _mkdir shim for non-MSVC Windows; fix %I64d * Fix gPoolRunning extern outside ifndef _WIN32 * Remove -lboost_system: boost is header-only in this codebase * Status bar: copy buttons for IP and .btf address * Windows: add app icon via windres, strip wx stuff from rc file * Clean GUI: single Options menu item, bigger font, better spacing, cleaner options dialog; update README * GUI: embed Roboto font at 16px for clear readability on all platforms * Windows: windres depends on obj dir target * Fix balance hidden off-screen: put on own line below address bar * Windows: inline mkdir in every rule, remove dir target that confused make * Windows: move all target to top so it's the default rule * Fix pool server not starting: always start when operator selected, re-sync dialog state on open * Write debug.log to data directory (AppData/Roaming/Bitflash on Windows) * Participant miner: use wallet address as username so payouts go to wallet * Add .gitattributes to lock LF line endings, prevent CRLF drift * Refactor pool mining to btf-only transport * Persist pool settings and startup wiring * Prune stale peers and expose pool queries * Fix build regressions and add CLI mode/arg parity * Fix rpc miner stats declaration order * Centralize pool runtime extern declarations * Separate operator/participant dashboards and restore peer stats * Add live telemetry strip and clearer per-mode runtime metrics * Reduce mining dashboard clutter and remove manual pool entry * Count only authorized pool miners * Seed initial pool job on startup * Use a real share target for pool work * Switch pool accounting to round-based shares * Fix nostr pool announcer stats call sites * Fix pool stats definition order * Improve pool worker status display * Add mutex include for participant status * Replace ImClamp in dashboard layout * Clarify participant work status * Log participant status transitions * Reduce peer discovery log noise * Add participant status badge * Track participant auth acknowledgements * Log and validate pool job delivery --------- Co-authored-by: Bitflash Pool Dev --- src/Makefile | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/Makefile (limited to 'src/Makefile') diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..3280fd3 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,75 @@ +# Bitflash Linux build — ImGui + GLFW + OpenGL3 +# Called by: make linux (root Makefile) + +CXX ?= g++ + +SECP_INC ?= /usr/local/include +SECP_LIB ?= /usr/local/lib +RANDOMX_DIR ?= $(HOME)/RandomX + +IMGUI_DIR = imgui + +CXXFLAGS = -std=gnu++14 -O2 -w -fpermissive -pthread \ + -DSECP256K1_STATIC \ + -I$(SECP_INC) -I$(RANDOMX_DIR)/src \ + -I/usr/include/nlohmann \ + -I$(IMGUI_DIR) + +LIBS = -L$(SECP_LIB) -lsecp256k1 \ + -L$(RANDOMX_DIR)/build -lrandomx \ + -lsodium -lssl -lcrypto -ldb_cxx -lpthread \ + -lglfw -lGL + +HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ + key.h bignum.h script.h db.h base58.h net.h irc.h nostr.h \ + randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h compat.h + +IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ + obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o + +NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ + obj/market.o obj/sha.o obj/irc.o obj/nostr.o obj/randomx_pow.o \ + obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o + +GUI_OBJS = obj/gui.o obj/main_gui.o + +all: bitflash + +obj/%.o: %.cpp $(HEADERS) + @mkdir -p obj + $(CXX) -c $(CXXFLAGS) -o $@ $< + +obj/sha.o: sha.cpp sha.h + @mkdir -p obj + $(CXX) -c $(CXXFLAGS) -O3 -o $@ $< + +obj/imgui.o: $(IMGUI_DIR)/imgui.cpp + @mkdir -p obj + $(CXX) -c $(CXXFLAGS) -o $@ $< + +obj/imgui_draw.o: $(IMGUI_DIR)/imgui_draw.cpp + @mkdir -p obj + $(CXX) -c $(CXXFLAGS) -o $@ $< + +obj/imgui_tables.o: $(IMGUI_DIR)/imgui_tables.cpp + @mkdir -p obj + $(CXX) -c $(CXXFLAGS) -o $@ $< + +obj/imgui_widgets.o: $(IMGUI_DIR)/imgui_widgets.cpp + @mkdir -p obj + $(CXX) -c $(CXXFLAGS) -o $@ $< + +obj/imgui_impl_glfw.o: $(IMGUI_DIR)/imgui_impl_glfw.cpp + @mkdir -p obj + $(CXX) -c $(CXXFLAGS) -o $@ $< + +obj/imgui_impl_opengl3.o: $(IMGUI_DIR)/imgui_impl_opengl3.cpp + @mkdir -p obj + $(CXX) -c $(CXXFLAGS) -o $@ $< + +bitflash: $(IMGUI_OBJS) $(NODE_OBJS) $(GUI_OBJS) + $(CXX) $(CXXFLAGS) -o $@ $^ $(LIBS) + @echo "Built: bitflash" + +clean: + rm -rf obj bitflash -- cgit v1.2.3 From 663784eae9b04ec71e3e1eebf445910e5a8add42 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Tue, 28 Jul 2026 12:55:04 -0300 Subject: Merge PR #6 (large refactor), preserving main's fixes (#9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Linux GUI installer via Wine * Fix launcher creation and add BTF_RELAY env var support * Pool server, Linux GUI, unified binary, README update * Root Makefile: make linux / make windows, cleaner README * Remove headless.cpp * Fix XPM color count mismatch causing toolbar image errors on Linux * XPM icons: same code on Windows and Linux, no ifdefs around image loading * Fix GTK icon crash and Linux printf format * btfrv: 10s socket timeout on relay connects * Faster peer discovery: more dials per pass, shorter backoff and sleep * Parallel btf peer connections * Fix GetMyExternalIP - was disabled, now uses modern IP echo services * Fix tunnel disconnect on idle and restore external IP detection * Filter stale Nostr descriptors * Sort peers by freshness, fresh first * Pool: add .btf Stratum listener — miners connect via rendezvous, no port forwarding needed * btf pool: self-replacing listeners, one per relay, no queuing between miners * Mining modes: solo/operator/participant — pool server on-demand, participant Stratum client * Status bar: show external IP and .btf address * README: clean rewrite, concept-first, minimal * make linux produces AppImage * make linux produces AppImage * Pool dashboard: connect instructions, tabs, share bars * Perf fixes: GetMyExternalIP threaded, btf listener counted * Perf fixes: GetMyExternalIP threaded, btf listener counted * Fix pool payouts: defer until coinbase maturity * Pool: clean mode switching, IP warning * Pool: clean mode switching * Fix gPoolRunning: remove static * AppImage: use real icon from bitcoin.ico * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Windows build: use mingw32-make * Windows build: simpler make invocation * Add deps-windows target * Windows build fixes * Windows build fixes * Windows makefile: fix obj dir * Windows: output to repo root * Windows: bundle DLLs into zip * Replace wxWidgets with ImGui+GLFW: same build on Linux and Windows * Fix %I64d that slipped back through rebase (main.h, net.h) * MinGW: add _mkdir shim for non-MSVC Windows; fix %I64d * Fix gPoolRunning extern outside ifndef _WIN32 * Remove -lboost_system: boost is header-only in this codebase * Status bar: copy buttons for IP and .btf address * Windows: add app icon via windres, strip wx stuff from rc file * Clean GUI: single Options menu item, bigger font, better spacing, cleaner options dialog; update README * GUI: embed Roboto font at 16px for clear readability on all platforms * Windows: windres depends on obj dir target * Fix balance hidden off-screen: put on own line below address bar * Windows: inline mkdir in every rule, remove dir target that confused make * Windows: move all target to top so it's the default rule * Fix pool server not starting: always start when operator selected, re-sync dialog state on open * Write debug.log to data directory (AppData/Roaming/Bitflash on Windows) * Participant miner: use wallet address as username so payouts go to wallet * Add .gitattributes to lock LF line endings, prevent CRLF drift * Refactor pool mining to btf-only transport * Persist pool settings and startup wiring * Prune stale peers and expose pool queries * Fix build regressions and add CLI mode/arg parity * Fix rpc miner stats declaration order * Centralize pool runtime extern declarations * Separate operator/participant dashboards and restore peer stats * Add live telemetry strip and clearer per-mode runtime metrics * Reduce mining dashboard clutter and remove manual pool entry * Count only authorized pool miners * Seed initial pool job on startup * Use a real share target for pool work * Switch pool accounting to round-based shares * Fix nostr pool announcer stats call sites * Fix pool stats definition order * Improve pool worker status display * Add mutex include for participant status * Replace ImClamp in dashboard layout * Clarify participant work status * Log participant status transitions * Reduce peer discovery log noise * Add participant status badge * Track participant auth acknowledgements * Log and validate pool job delivery * Stop persisting mining mode/pool settings across restarts LoadWallet() runs after CLI flags are parsed in main_gui.cpp, and was restoring nMineMode/strParticipantPool/strPoolName/strPoolDashboardUrl/ dPoolFeePercent/fGenerateBitcoins from wallet.dat every time, silently overwriting whatever was just set on the command line or in the current Options session -- no error, no log line. This is why pool name/address could show correctly in the UI but stay stale in the logs (UI held the in-memory value just set; logs eventually reflected whatever LoadWallet clobbered it back to), and why mode changed behavior across restarts. Mode/pool config is now decided fresh every launch: CLI flags if given, otherwise the compiled-in default. Nothing is written back to wallet.dat for these settings anymore, so there's nothing left to go stale or fight over load order. * Add explicit Relay mode as the new startup default 4th mining mode: node runs/syncs, never mines, no pool server -- an explicit option instead of an implicit side-effect of 'Solo + never click Start Mining'. Now the compiled-in default (was Solo). - main.h: MINE_RELAY = 0, renumbered other modes (nothing compares nMineMode against raw integers elsewhere, verified). - main.cpp: default nMineMode = MINE_RELAY; BitcoinMiner() early-returns for Relay, mirroring the existing Participant-mode guard. - gui.cpp: added Relay radio button, status label, and telemetry-strip text. Start/Stop Mining button is hidden for Relay (meaningless -- it never mines by definition); OK forces fGenerateBitcoins off if you switch into Relay while previously mining. * Announce pool changes immediately instead of waiting up to 60s ThreadBtfPoolAnnouncer re-read strPoolName/dPoolFeePercent/etc live, but only once per iteration of its 60s sleep loop, so a pool name/fee change in Options could take up to a minute to reach other nodes. - nostr.h/.cpp: add gAnnounceNow flag; the announcer's sleep loop now wakes early and publishes immediately when it's set, then clears it. - gui.cpp: Options OK handler sets gAnnounceNow when operator pool settings actually changed (name/dashboard/fee/mode), so a saved change is announced right away instead of on the next tick. * Give each pool miner a distinct extranonce2 so jobs no longer overlap mining.notify previously broadcast one identical 80-byte header (baked from a single server-side extraNonce) to every connected miner, so any two miners on the same job searched the exact same nNonce space instead of disjoint ranges. Each Miner connection is now assigned a unique extranonce2 on connect. BuildMinerBlock() takes the shared job template and appends that miner's extranonce2 to the coinbase scriptSig, recomputing the merkle root, so every miner gets its own header variant of the same job. mining.submit validates and, on a full block, assembles the block using that same miner-specific variant (via m->extranonce2), not the shared template. No client-side protocol change needed: main.cpp's participant miner already just takes whatever header bytes mining.notify hands it and submits (jobId, nonce) back, so it works unmodified against per-miner headers. Still simplified vs standard Stratum (no coinb1/coinb2/merkle-branch wire split, no mining.extranonce.subscribe reply payload) since this protocol was already a custom simplification pre-existing this fix. Syntax-checked with g++ -fsyntax-only (full link needs secp256k1/ RandomX/GLFW not present in this sandbox). * Fix debug.log never being written on the real Windows build printf-to-debug.log routing was gated on: #if defined(_WINDOWS) || !defined(_WIN32) _WINDOWS is only ever defined by the legacy, unused MSVC makefile.vc. The actual Windows build (makefile.mingw) never defines it, and _WIN32 IS defined there (mingw builtin), so on real Windows this condition was always false || false = false. printf() fell through to plain libc printf (stdout only), so debug.log was never created, and output was only visible if the process happened to be run attached to a console. OutputDebugStringF() was already platform-safe internally (always writes debug.log; only echoes to console when not on Windows), so the outer condition was pure dead weight. Made the routing unconditional. * Reduce base UI font size one more step (16px -> 14px) * Remove Start/Stop Mining button; mining state follows selected mode The button let a user configure a pool then click a separate toggle, creating states where the button's label/action didn't match what the selected mode actually needed. OK now starts/stops the miner thread itself based on nMineMode: Relay clears fGenerateBitcoins, the other three modes set it and spawn ThreadBitcoinMiner if it isn't already running (guarded by vfThreadRunning[3]). * Fix participant miner stuck waiting for notify after buffered multi-line recv Root cause of the operator/participant pairing successfully but no work ever flowing: PoolParticipantMiner()'s receive loop only pulled one line out of the socket per select() wakeup. The server sends the authorize reply, mining.set_difficulty, and mining.notify back-to-back on the same connection, and these often land in a single TCP segment — one recv() inside StratumRecvLine() buffers all three lines into 'buf', but only the first was extracted and processed. The remaining lines (frequently mining.notify itself) sat in 'buf' indefinitely, because select() only reports readability of the OS socket, not whether our own buffer still holds unprocessed complete lines — and the server had nothing further to send once the initial burst was over, so select() never fired again. Fix: drain any complete line already sitting in 'buf' first; only fall back to select()+recv() when 'buf' has no complete line left. * Fix mining.submit rejecting every share due to wrong param-count check Server required params.size() >= 4 for mining.submit, but the client only ever sends 3: [username, jobId, nonceHex] (indices 0-2, matching what the handler actually reads: p[1]=jobId, p[2]=nonceHex). Every submission was bounced with 'bad params' before its nonce was ever checked against the job, producing an endless share-sent/share-rejected loop with no other visible cause. Likely a leftover from the standard 5-field Stratum mining.submit shape, which this simplified 3-field protocol never matched. Threshold corrected to size() >= 3. * Fix mining.submit rejecting legitimate shares that weren't also winning blocks Pool share difficulty is intentionally ~256x easier than full network difficulty (MakeShareTarget() shifts the block target left 8 bits), so miners are expected to submit many shares per actual block found. But the handler replied to the miner based on ProcessBlock()/CheckBlock() success — validation against the much harder network target — rather than on whether the share met pool difficulty. Every legitimate share that wasn't also a winning block (the vast majority, by design) failed CheckBlock()'s nBits check and got reported to the miner as rejected. Now: reply(true) as soon as a submission clears pool-share difficulty. Only if it ALSO clears full network difficulty (job.target, not job.shareTarget) does it get submitted as a block — with no second reply sent, since the share was already acknowledged. * Fix dead-peer dial priority: freshness sort was undone by full shuffle ConnectDiscoveredBtfPeers sorted candidates newest-first, then immediately shuffled the whole list, making the freshness sort a no-op — every pass was fully random. Meanwhile g_btfPeerFails was tracked but never used to prioritize, so a peer that had failed 20 times in a row got equal footing with an untried or previously-successful peer; the only effect was backoff, capped at 15 minutes, so dead descriptors cycled back into the dial pool constantly. - Track successful connections (g_btfPeerEverConnected) and use it, plus fail history, to order candidates in tiers: known-good, then untried, then known-bad (worst offenders last). Freshness order is preserved within each tier instead of being discarded. - Raise the backoff cap for chronic failures (6+ in a row) from 15 minutes to 6 hours, so descriptors that are clearly dead stop consuming dial budget every pass while still getting retried eventually. * Batch-resolve .btf descriptors to stop hammering public relays during dial passes Root cause of 'stuck at 1 peer' despite good peer discovery: each parallel dial thread in ConnectDiscoveredBtfPeers called ConnectNodeBtf, which itself walked all configured relays (relay.damus.io, nos.lol, relay.nostr.band, relay.snort.social) to resolve that one peer's descriptor. With up to BTF_DIALS_PER_PASS (12) threads dialing at once, a single pass could fire up to 12 * nNostrRelays near-simultaneous fresh connections at the same handful of public relays -- easily enough to trip rate limits or get handshakes refused (exactly what the logs showed: 'handshake refused by relay.damus.io', repeated connect failures to relay.nostr.band / relay.snort.social). Peers that were genuinely reachable looked dead simply because their descriptor couldn't be fetched under that load, and then got penalized as known-bad by the dial-priority tiering from the previous fix. - Add BtfResolveMany: resolves every still-unresolved candidate's descriptor in a single Nostr REQ per relay (multiple authors in one filter), instead of one relay connection per candidate. A 12-candidate pass now costs at most a few relay round-trips total instead of dozens. - Add ConnectNodeBtfResolved / ConnectNodeBtfTail (net.cpp): given an already-resolved descriptor, dial straight to the peer's rendezvous node with zero further relay contact. ConnectDiscoveredBtfPeers now batch- resolves all candidates up front, then fans out tunnel-only connect threads in parallel -- no thundering herd against the relays. - Candidates that fail to resolve are recorded as a failure immediately (same backoff/fail-count bookkeeping as before) instead of spawning a thread with nothing to dial. * Stop reconnecting to relays constantly: pool one persistent connection per relay Real root cause of the ongoing 'handshake refused' / 'connect failed' spam and peer count stuck at 0-1: every relay-touching operation (startup self- test, per-pass seeding, batch descriptor resolve, pool announce, pool query) opened its own brand-new CWebSocket every single time it ran. Two independent threads (ThreadNostrSeed and ThreadBtfPoolAnnouncer) each reconnect from scratch on their own ~60s cycle, so a node could fire well over a dozen fresh TCP+TLS+WS handshakes at the same 4 public relays inside any given minute. Public relays -- relay.damus.io especially -- rate-limit new connections per source IP fairly aggressively, so this node was effectively DoS-ing itself: peers were genuinely reachable, but their descriptors couldn't be fetched because the relay had already started refusing this node's own repeated reconnects. - Add a small per-relay connection pool (g_relayPool) and an RAII CRelayLease that reuses a relay's live socket across calls instead of reconnecting every time, only reconnecting when a socket actually drops or was never established. - Route every relay call site through it: the startup self-test, the self-test loop, SeedFromRelay, BtfResolve, BtfResolveMany, BtfPublishPoolAnnouncement, BtfQueryPoolAnnouncement. - Only log 'connected to ' on an actual fresh (re)connect, not on every pass that's just reusing an already-live socket, so the log reflects what's really happening instead of implying constant churn. This cuts fresh relay connections from several per minute down to essentially one per relay for the life of the process (until a relay actually drops the connection), which should make the rate-limit-driven refusals go away and let descriptor resolution -- and therefore dialing -- actually succeed at the rate the peer list implies it should. * Fix wallet crash on rejected send; remove legacy IP-based peer discovery - SendMoney() no longer throws when a committed tx is rejected by AcceptTransaction() (e.g. raced input already spent). It now unwinds the wallet state via RollbackTransactionSpent() and returns false, which the GUI already handles as 'Transaction failed.' - Removed the dead IP-based peer discovery/connect path now that discovery is entirely Nostr/.btf: CAddrDB/addr.dat, mapAddresses, AddAddress, ConnectNode(CAddress), FindNode(CAddress), ConnectSocket (SOCKS4 proxy helper), addrProxy, ThreadOpenConnections/2 class-C dialer and its thread spawn, the addr/getaddr P2P handlers, and the dead vAddrToSend/setAddrKnown fields on CNode. - Deleted irc.cpp/irc.h (already-disabled legacy seed code) and scrubbed references from headers_core.h and all four makefiles. * Raise peer connection target and add dial diversity; fix IPv4-only relay connect - BTF_TARGET_CONN: 8 -> 24. With ~100 known .btf peers, average degree needs only to exceed ln(N)~=4.6 for the network to stay connected with overwhelming probability (Erdos-Renyi); 24 is a healthy margin. - ConnectDiscoveredBtfPeers now splits each pass's dial budget ~70/30 between the existing reliability-tiered order and a random sample from the full fresh peer pool. Pure reliability bias means every node converges on the same 'known good' hub set, which correlates peer sets across the network -- a relay outage then drops the same edges everywhere at once instead of independent ones. The random slice keeps a floor of uncorrelated edges. - CWebSocket::Connect (relay websocket) now resolves with AF_UNSPEC and tries every returned address (IPv6 or IPv4) in turn instead of hard-failing on AF_INET-only resolution, which was one plausible cause of the relay.nostr.band:443 connect failures (CDN-fronted relays increasingly rate-limit/filter the v4 path). * Remove accidentally committed build binary; ignore src/bitflash * Fix pool participants getting stuck after the first job JobWatcherFn only rebuilt/broadcast a new Stratum job when a new block arrived on this node or no job existed yet. Once a job was built after startup, gHaveJob stayed true forever unless *this* node found the block, so with no new block (slow interval, or this node just not seeing one) every connected miner sat hashing the exact same job -- stale timestamp, stale mempool snapshot -- indefinitely. From the miner's side that's indistinguishable from being stuck: it worked at connect (gets the one-time job sent on authorize) and then nothing ever arrives again. Added a 60s periodic refresh so the job (and therefore mining.notify) gets rebroadcast on a timer too, same as a normal stratum pool, not only on new-block/no-job events. * Diversify Nostr discovery relay list Went from 4 relays to 7, and reduced Cloudflare concentration: previously 2 of 4 (damus.io, snort.social) sat behind the same CDN, so a Cloudflare- side issue could take out half the list even though they're nominally independent relays. Kept damus.io (most established) as the one CF-fronted entry and filled the rest with different hosts (nos.lol, nostr.band on Hetzner, nostr21.com, offchain.pub, nostr.mom, primal.net) so peer discovery doesn't have a single shared upstream as a point of failure. 7 relays is within the commonly recommended 5-10 range for redundancy. * Fix data race in RebuildJob: chain-state reads weren't holding cs_main pindexBest, nBestHeight, GetNextWorkRequired() and GetMedianTimePast() were all read without cs_main. A block being accepted concurrently by the P2P thread could interleave with this: pindexPrev captured from the old best block, but nBestHeight already bumped to the new one by the time job.height is computed, or pindexBest reassigned mid-read. That mismatch would make QueuePayouts() schedule a payout for the wrong maturity height for whatever block gets found against that job. Wrapped the whole chain-state snapshot in one CRITICAL_BLOCK(cs_main), consistent with how the rest of the codebase touches this state. * add operator fee * Add categorized, burst-collapsing logging for pool/worker/payout flow debug.log was growing unboundedly because every printf() call wrote unconditionally, so loops that log once per item (discovered peers, connect attempts) produced a near-identical line per address with no cap. Add LogPrint(category, ...): a drop-in printf replacement that tags each call with a subsystem (pool/worker/payout/net/nostr) and collapses repeated calls from the same call site into one line per 3s window plus a '(xN similar in 3s)' summary, so log growth is bounded by elapsed time instead of event count. Wire it through the whole pool -> work -> payout pipeline (rpc.cpp operator side, main.cpp participant/worker side, net.cpp/nostr.cpp peer & relay discovery), and along the way fix several previously *silent* failure points that produced exactly the reported symptom (worker stuck asking for work, nothing logged): - Relay listener registration failing: no log at all, so a pool with every relay down looked identical to a healthy idle pool. - No relays configured: silent infinite 5s retry loop. - Initial job seed failing at pool startup: silent. - JobWatcher: added a stuck-job watchdog that, after 15s with no job available, logs a specific diagnosis (no chain tip yet vs. RebuildJob() failing) instead of retrying forever in silence. - QueuePayouts(): a block found with zero recorded shares queued no payout and logged nothing. - FlushMaturePayouts(): an unparseable payout address was skipped with no log. - Participant miner: the 'waiting for pool notify' stall was only ever shown as a transient GUI status string, never written to debug.log. Now logs on a 15s cadence while stalled, and after 120s proactively drops the tunnel so the existing reconnect-in-10s loop gets a fresh attempt instead of hanging indefinitely. - Participant miner: share rejections were only a transient GUI status string; now logged with the pool's rejection reason. All edited files verified with g++ -fsyntax-only (gnu++14). * Fix two bugs causing worker stuck + log bloat 1. Worker stuck asking for work (main.cpp): The participant miner checked msg["result"].is_boolean() to detect the mining.subscribe acknowledgement (id==1), but the pool sends an array ([[["mining.notify",sid],sid,4]]) as per Stratum spec, not a boolean. is_boolean() always returned false, so gotSubscribeAck was never set true. This didn't directly block authorize (id==2 path is independent), but it did mean the status never advanced through "subscribed", leaving the progress logging misleading. More importantly, the same wrong assumption meant any pool that also gated job dispatch on a prior subscribe confirmation would leave the worker stuck forever with no diagnosis. Fix: accept any non-null result for id==1 (the array is the session/extranonce info; its contents don't matter for our use case). 2. Log bloat from ProcessMessages (main.cpp): ProcessMessages() used a bare printf() which fired unconditionally for every P2P message received -- the bulk of debug.log was this one line repeating thousands of times. Convert to LogPrint("net", ...) so it is gated behind fDebug (off by default) and burst-collapsed by the dedup introduced in the previous commit. * Fix log bloat, em-dashes, dead code, status bar, Connected/Detected peers Log bloat: - Gate all per-message/per-block/per-loop printf in main.cpp, net.h, net.cpp behind LogAcceptsCategory("net") -- only visible with -debug. This includes: received:/sending: hex dumps, got inventory, askfor, getblocks, getdata, received block, AddToWallet, RelayWalletTransactions, ProcessMessage FAILED, mining loop N-transactions line, retarget block, socket recv/send errors. AddToBlockIndex new best and ProcessBlock ACCEPTED intentionally left always-on (one line per block). - PoolParticipantMiner: status -> now LogPrint(worker) not printf. Em-dashes: - Replace all UTF-8 em-dashes (0xE2 0x80 0x94) with ASCII '--' in every source file (rpc.cpp, gui.cpp, main.cpp, main_gui.cpp, net.cpp, nostr.cpp, util.cpp, btfrv.cpp, db.cpp, db.h, main.h, headers.h, headers_core.h, util.h). 40 occurrences across 14 files. Dead code: - Remove unused gJobSeq atomic from rpc.cpp (written, never read). - Fix minerReward: was computed but payout loop used blockReward directly (operator fee was advertised but never deducted). Now payout correctly uses minerReward = blockReward * (1 - feeFrac). Status bar / peers: - Add GetDiscoveredPeerCount() to nostr.cpp/nostr.h (reads g_btfPeers size under cs_btfPeers). - Replace scrollable inline status text with DrawStatusBar(): a separate ImGui overlay window pinned to bottom of screen (NoScrollbar, NoBringToDisplayFront). Labels changed from 'Peers N' to 'Connected peers: N Detected peers: N'. * Rewrite gui.cpp: proper fixed status bar, scrollable content child Layout architecture: - DrawMainWindow() covers the full display width, height minus STATUS_BAR_H (22px). NoScrollbar + NoScrollWithMouse on the outer window so the chrome (menu, address/balance strip, telemetry strip, separators) never scrolls. - A BeginChild("##scroll") fills the remaining height after the fixed chrome, sized to GetContentRegionAvail().y -- which is already the space between the cursor and the bottom of the outer window. All mode-specific content (transaction table, participant pool list, operator workers/payouts) lives inside this child. Content that overflows gets a scrollbar; nothing bleeds past the outer window edge. - DrawStatusBar() is a separate overlay window pinned to the bottom 22px, drawn last so it is always on top. NoBringToDisplayFront is NOT used here -- drawing last is sufficient and avoids interaction issues with dialogs. - Status bar background is fully opaque (not alpha-blended) so it never shows content behind it. Labels and content: - 'Peers N' replaced with 'Connected peers: N Detected peers: N' using GetDiscoveredPeerCount(). - Mining indicator: '* Mode' instead of the 'filled circle' Unicode glyph that renders as boxes on some Linux font configs. - FmtAge used consistently; 'blocks deep' shortened to 'N deep' in tx list. - Options dialog: em-dashes removed from RadioButton labels (ASCII '--'). - Removed usedY dead variable (GetCursorPosY() result was unused). * Fix blocking bugs, dead code, pool/participant flow correctness Bugs fixed: StratumRecvLine blocking (main.cpp): The participant miner's recv loop called recv() with no timeout after select() said the socket was readable. If the server sent a partial line (split across two TCP segments), the second recv() would block indefinitely waiting for the rest -- the classic 'stuck asking for work' hang even though the pool was alive and had sent data. Fix: single non-blocking recv() pass (MSG_DONTWAIT on Linux, FIONREAD+recv on Windows). If no complete line is in the buffer yet, return false; the outer select() loop retries when more data arrives. BroadcastJob holding mutex during send() (rpc.cpp): BroadcastJob() held gMinersMutex across all SendLine() calls. send() can block if a slow miner's TCP receive buffer is full. With the mutex held, MinerThreadFn could not acquire it to remove a disconnecting miner, causing a deadlock-adjacent stall: broadcast blocked on slow miner, disconnect blocked on broadcast, new jobs not delivered. Fix: snapshot {fd, authorised, address, extranonce2} under the mutex, release it, then send to each fd outside the lock. Dead code removed (rpc.cpp): - gRpcUser, gRpcPassword: defined, never read or set by anything. - dHashesPerSec, nHPSTimerStart: leftover Bitcoin 0.1 stubs, unused. - IsInitialBlockDownload(): stub that returned pindexBest==NULL, never called anywhere in this codebase. - struct Share: declared with three fields, never instantiated. - Duplicate comment block before payout section. Other cleanup (main.cpp): - STUCK_NO_JOB_TIMEOUT_SECS moved from inside the loop body to file scope (was 'static const' inside a loop -- correct but confusing). - pblock->print() in solo miner gated behind LogAcceptsCategory("net") -- was unconditionally dumping a full block to stdout on every solve. - ERROR in BitcoinMiner printf converted to LogPrint("net"). * Pool/participant: fix recv blocking, BroadcastJob mutex, listener spawn, escaped em-dash main.cpp -- StratumRecvLine rewrite: The previous version called recv() in a while loop -- if select() said the socket was readable but only a partial line had arrived, the second recv() call would block indefinitely waiting for the rest of the line. This was the root cause of the recurring stuck-worker bug. Rewrite: single non-blocking recv (MSG_DONTWAIT on Linux, FIONREAD+recv on Windows). If no complete newline is in the buffer yet, return false immediately; the outer 50ms select() loop retries when more data arrives. This means the hash loop always stays responsive regardless of network fragmentation. rpc.cpp -- BroadcastJob mutex held across send(): BroadcastJob held gMinersMutex for the entire duration of sending jobs to all connected miners. send() can block if a slow miner's TCP buffer is full. With the mutex held, MinerThreadFn could not acquire it to clean up a disconnecting miner, stalling the whole broadcast. Fix: snapshot {fd, authorised, address, extranonce2} under the mutex, release it, then send to each fd without holding any lock. rpc.cpp -- LaunchThread failure not handled in BtfStratumAcceptOneFn: The replacement listener thread was launched via LaunchThread() which wraps _beginthread() but discarded the return value. If _beginthread fails (thread limit, OOM), the listener count stayed at 1 with no thread behind it -- that relay would get no new listener until the 30s outer loop in BtfStratumAcceptFn fired. Fix: call _beginthread() directly, check the return value, decrement and log on failure. rpc.cpp -- escaped em-dash in log string: BtfStratumAcceptFn had a literal \xe2\x80\x94 in a LogPrint format string that survived the em-dash sweep (it was source-encoded, not a UTF-8 byte in the file). Replaced with '--'. * Rewrite pool server: single event loop, no per-miner threads Replace the old architecture (one thread per connected miner + separate JobWatcher thread + BtfStratumAcceptFn thread with listener count tracking) with a clean two-layer design: Accept layer (one thread per relay, AcceptOneFn): Each relay gets exactly one persistent thread. It loops: calls blocking RvServiceRegister (waits for a miner to knock), then BtfServiceWrap (echan handshake), then pushes the finished plain socket to a small lock-protected queue, then loops immediately to accept the next miner. No listener count tracking. No replacement thread spawning. No races around spawn failure. The relay always has exactly one pending accept. Event loop (PoolEventLoop, runs on the ThreadRPCServer thread): Single thread owns all connected miner sockets. Each tick: 1. Drain the accept queue -- pick up any newly-connected miners. 2. Check for a new block -- rebuild job and broadcast if so. 3. No-job watchdog -- log and retry if job is missing too long. 4. Periodic job refresh -- restamp nTime every 60s. 5. select() over all miner fds with 50ms timeout. 6. For each readable fd: non-blocking RecvLine, drain all complete lines, dispatch each to HandleLine, collect dead fds. 7. Refresh GUI stats snapshot every 2s. Because the event loop is the only writer to gMiners, no mutex is needed on the miner list. The only mutexes remaining are: - gStatsMutex: guards the GUI-visible snapshot (StatsSnapshot, gWorkerStats, gPayoutViews) written by the event loop and read by GUI threads via GetPoolOperatorStats/GetPoolWorkerStats/GetPendingPayouts. - gReadyMutex: guards the accept queue between AcceptOneFn threads and the event loop. Other improvements in this rewrite: - gPendingPayouts is event-loop-only (no mutex needed). - Round share tracking lives in the event loop (roundShareCount map + roundShareTotal counter) -- zeroed on block found, no per-miner stat mutex needed mid-round. - QueuePayouts takes the share snapshot as a parameter instead of reaching into gMinerStats under a separate mutex. - BroadcastJob no longer snapshots miners to avoid a mutex -- it runs directly in the event loop with exclusive access to gMiners. - AcceptOneFn backs off 30s on relay registration failure instead of returning -- one relay going down doesn't kill the accept thread. - No goto. No per-thread dynamic allocation for listener contexts on each accepted connection. * Thorough logging and correctness fixes across the full pool/participant flow Every failure point in the operator->miner->payout chain now logs exactly what broke, with enough context to diagnose without a debugger. Operator side (rpc.cpp): RebuildJob: logs 'no chain tip (pindexBest is null)' on failure instead of silently returning false. HandleLine: - JSON parse errors logged with the raw line (first 80 chars) - subscribe: logs fd and session id - authorize: logs address, worker, and whether the address is valid - submit: logs stale-job case with submitted vs current job id; logs high-hash with actual hash and target values; logs valid shares with nonce and running session count; logs BLOCK CANDIDATE separately from BLOCK ACCEPTED/REJECTED so a ProcessBlock failure is unambiguous - unknown method logged (reveals miner software quirks) - reply() logs send failures with method name QueuePayouts: logs total payout amount, miner count, fee applied, and how many miners were below dust threshold. FlushMaturePayouts: logs 'insufficient funds or wallet locked?' on SendMoney failure instead of just 'FAILED'. BroadcastJob: logs job id and miner count on every broadcast. Event loop: logs when each new miner socket enters the loop; logs disconnect with session and round share counts. No-job watchdog: RebuildJob retried every 5s (not every 50ms tick), eliminating cs_main hammering when there is no chain tip. AcceptLoopFn: re-polls BtfAllRelays every 60s so relays discovered after startup are picked up; logs each new relay thread spawned. AcceptOneFn: logs relay name on start/stop; logs BtfServiceWrap (echan handshake) failure separately from RvServiceRegister failure. RecvLine: disconnected out-param distinguishes 'socket closed' from 'no complete line yet', preventing incorrect disconnection on partial recv. Participant side (main.cpp): StratumRecvLine: same disconnected out-param fix as RecvLine in rpc.cpp. Previously a false return could mean either 'disconnect' or 'no data yet', causing spurious reconnects when a partial line arrived. PoolParticipantMiner rewritten with step-by-step logging: Step 1 resolve: logs specific failure (no descriptor vs bad format) Step 2 tunnel: logs relay and failure reason Step 3 address: warns explicitly if payout address cannot be determined Step 4 subscribe/authorize: logs each send and each response Step 5 RandomX: logs VM mode Step 6 main loop: - malformed mining.notify (wrong param count or short header/target) logged with sizes instead of silently ignored - authorize rejected: logs reason, does not disconnect (some pools reject bad addresses but still send work) - share accepted/rejected: logs share id and rejection reason - unknown server method logged - pool disconnect logged with handshake state at that moment (gotSubscribeAck, gotAuthorizeAck, haveJob) so you know exactly how far the connection got - nonce space exhausted logged (was silent before) - on exit: logs total hashes, shares sent, shares accepted * Fix build error: ImGuiWindowFlags_NoBringToDisplayFront -> NoBringToFrontOnFocus NoBringToDisplayFront does not exist in this version of ImGui (v1.89.x). The correct flag is ImGuiWindowFlags_NoBringToFrontOnFocus (bit 13). Used in two places: ##statusbar and ##main windows. Also remove dead variable usedY (GetCursorPosY() result assigned but never read) and replace the confusing 'ImGuiWindowFlags_NoScrollbar * 0' expression (evaluates to 0 but looks like a mistake) with a plain 0 in the BeginChild call. * Remove duplicate status logging from SetParticipantMiningStatus Every transition in PoolParticipantMiner already has a direct LogPrint with full context (addresses, job ids, nonces, error reasons). The LogPrint inside SetParticipantMiningStatus was firing on top of those, producing redundant 'PoolParticipantMiner: status -> ...' lines after every meaningful log entry. SetParticipantMiningStatus now just updates the GUI string silently. * Implement vardiff: stable share rate instead of fixed difficulty Previously the pool set shareTarget = blockTarget >> 8 for every miner forever. This meant at low network difficulty a fast miner found shares every few nonces (dozens per second), while at high difficulty a slow miner might go hours without one -- both are bad for fair payout accounting and pool stability. Pool (rpc.cpp): Per-miner difficulty that adjusts to target one share every 15 seconds. After every 5 shares, check actual seconds/share vs target. If the ratio is off by more than 50% (VARDIFF_RETARGET_RATIO), recompute: new_difficulty = current_difficulty / (actual_secs_per_share / target) i.e. shares coming too fast -> difficulty goes up; too slow -> down. Only apply if the change exceeds 10% to avoid jitter. Difficulty is bounded to [0.001, 1e12]. Each miner gets their own shareTarget derived from their difficulty via ShareTargetFromDifficulty(), which maps Stratum difficulty to a uint256 target using the Bitcoin difficulty-1 base (0x1d00ffff compact). New difficulty is sent to the miner via mining.set_difficulty. Participant (main.cpp): Apply the received difficulty value from mining.set_difficulty. Previously the handler was a no-op ('acknowledge only'); now it parses the difficulty double and logs the change. The share target used for comparison comes from the pool's mining.notify p[3] field, which the pool updates after each vardiff adjustment -- this is the standard approach and matches how cpuminer, xmrig etc. handle it. With vardiff, share rate is roughly constant regardless of miner speed or network difficulty. Payouts are proportional to shares submitted, so each miner's payout accurately reflects their CPU contribution. * Fix share target mismatch: pool rejected shares it should have accepted Root cause: the pool put job.shareTarget (from blockTarget<<8, a fixed per-job value) into mining.notify p[3], but checked m->shareTarget (from ShareTargetFromDifficulty, the vardiff value) on submit. With difficulty=1.0, ShareTargetFromDifficulty gives a much HARDER target than blockTarget<<8, so shares that the participant thought were valid (passed the easier notify target) were rejected by the pool (failed the harder difficulty-1 target). Fix: MakeNotifyParams now takes the per-miner shareTarget as an explicit parameter. SendJob passes m->shareTarget. Both sides -- the participant reading p[3] and HandleLine checking m->shareTarget on submit -- now use exactly the same value. Single source of truth. Cleanup: - Remove shareTarget field from StratumJob (was only set, never read after the MakeNotifyParams call was fixed). - Remove MakeShareTarget() function (was only used to populate that now-dead field). - Participant set_difficulty log: 'initial' vs 'updated' instead of 'unchanged' (first connect sends 1.0 which matches the default 1.0, so it was misleadingly logged as 'unchanged'). * Fix double broadcast, correct block reward calculation, improve payout log Double broadcast (event loop vs HandleLine): When a pool miner finds a block, HandleLine calls RebuildJob() + BroadcastJob(true) immediately. The event loop then detects nBestHeight changed (step 2) and was calling RebuildJob() + BroadcastJob(true) again -- miners received duplicate job broadcasts. Fix: step 2 now checks gHaveJob before rebuilding. If HandleLine already built and broadcast a job for this height (gHaveJob=true), step 2 just logs 'job already broadcast by submit handler' and skips. If the block came from an external peer (not our miner), gHaveJob is false and step 2 handles it as before. Block reward calculation in QueuePayouts: Was calling CBlock().GetBlockValue(0) on an empty block object. GetBlockValue uses nBestHeight globally, which by the time QueuePayouts runs has already been incremented to the height AFTER the found block. Fix: compute subsidy directly as '50 COIN >> (blockHeight / 210000)' using the actual found height. At current scale this makes no difference (same subsidy for thousands of blocks) but is now correct by construction. Payout log now shows: - Full block subsidy - Miner share after fee - Operator fee amount in BTF (not just percentage) - Number of recipients, maturity height, dust-filtered count Example: '[payout] block 2096: subsidy 50.00 BTF, miners get 49.00 (fee 2.00% = 1.00 to operator), 3 recipients, matures height 2196' * Fix 6 bugs found in full flow re-read Bug 1 -- Double broadcast (re-applied fix that was lost): When HandleLine accepted a block it called RebuildJob()+BroadcastJob(true). The event loop then detected nBestHeight changed and did it again. Miners received two back-to-back mining.notify clean=true, wasting their work. Fix: step 2 of event loop checks gHaveJob before rebuilding. If HandleLine already built a fresh job (gHaveJob=true), step 2 just logs and skips. Bug 3 -- Disconnect log showed global totals, not session totals: gParticipantSharesSent/Accepted accumulate since node start, not since the last connect. Added sessionSharesSent/sessionSharesAccepted local counters that reset each time PoolParticipantMiner() is called. Bug 4 -- Dead function ParticipantShareTargetFromDiff (main.cpp): Defined but never called anywhere. Removed. Bug 5 -- Invalid-address miner shares silently burned: If a miner authorized with an invalid address, their share of the block reward was neither paid out nor logged -- it just vanished. QueuePayouts now validates addresses up front, logs the forfeited amount explicitly ('X BTF forfeited (bad address)'), and excludes those entries so the payout log is accurate. Bug 6 -- SendMoney called from event loop (stalls miner I/O): FlushMaturePayouts called SendMoney which takes cs_main. If the P2P thread held cs_main (processing a block), the event loop would stall for seconds, causing all connected miners to time out waiting for responses. Fix: FlushMaturePayouts now only moves matured entries to gPayoutExecQueue (fast, no locks except a brief mutex). A new PayoutThreadFn thread drains the queue and calls SendMoney independently, so the event loop is never blocked by wallet I/O. Bug 7 -- Dead goto + dead HandleLine return-false path: HandleLine always returns true; the goto next_miner path was never reached. Removed goto and simplified the select drain loop. * Fix share-target scale mismatch and dead pool hashrate reporting - ShareTargetFromDifficulty() based Stratum difficulty 1 on Bitcoin's SHA256 diff-1 constant (0x1d00ffff, ~2^32 hashes/share) instead of this chain's actual PoW floor (bnProofOfWorkLimit = ~uint256(0)>>12, ~2^12 hashes/share). That's a ~2^20 (~1,048,576x) mismatch: at the supposedly-easy difficulty of 1.0, workers were being asked for shares roughly a million times harder than the chain's own easiest target. Vardiff never gets a chance to correct this because it only retargets after real shares come in, and none were coming in. Fixed by basing the diff-1 reference on bnProofOfWorkLimit instead. - Pool hashrate was never computed anywhere despite the data (per-miner difficulty + share timing) being available: ThreadBtfPoolAnnouncer in nostr.cpp hardcoded ann.hashRate = 0.0, PoolWorkerStatView had no hashrate field, and the GUI workers table had no column for it. Added an EWMA hashrate estimate per miner (updated on each accepted share from expected-hashes-at-difficulty / time-since-last-share), threaded it through GetPoolOperatorStats/GetPoolWorkerStats, the pool announcement payload, and the GUI (new Hashrate column + totals in the telemetry line). Verified: full clean build via src/Makefile (secp256k1 + RandomX deps built from source, g++ 13, all four touched files compile and link). * Remove stale wxWidgets-era build files and orphaned assets These predate the ImGui/GLFW migration (83af065) and are dead weight: - src/makefile.linux, build-linux.sh: superseded by src/Makefile + root Makefile (make linux). The old ones reference obj/uibase.o and obj/ui.o, which don't exist -- they never worked post-migration. - src/makefile.vc: MSVC build for the old wx UI (obj\ui.obj, obj\uibase.obj, obj\irc.obj) -- irc.cpp doesn't even exist anymore, and nothing invokes this makefile. Windows builds go through makefile.mingw via the root Makefile's target. - src/ui.rc, src/uiproject.fbp: wxFormBuilder project/resource files for the old wx UI. The current Windows resource file is bitflash.rc. - src/rc/{addressbook,send}*.bmp, check.ico, bitcoin_bc_original.ico: icons used only by the old wx UI. bitcoin.ico (used by bitflash.rc) is kept. Verified nothing includes uibase.h/ui.h/irc.h anywhere, and did a full clean rebuild afterward (secp256k1 + RandomX from source, src/Makefile) to confirm removal doesn't break the real build. * Mine with RandomX fast (2GB dataset) mode instead of light mode forever RandomXInitDataset() was fully implemented and ready to switch mining into fast mode, but nothing ever called it -- not in PoolParticipantMiner() (pool worker), not in the solo BitcoinMiner() loop. RandomXCreateMinerVM() falls back to light mode whenever the dataset isn't built, so every miner, participant or solo, has always been hashing in light/cache-only mode. Light mode exists for cheap single-hash verification (which is why the pool's own share verification correctly stays on it) -- it recomputes dataset items on the fly for every hash, which is dramatically slower than fast mode's direct dataset lookups. Combined with the share-target scale bug fixed in e87a905, this compounds into exactly the symptom reported: a participant sitting there hashing for many minutes per share instead of the vardiff-targeted ~15s, even after the target fix landed. Fix: build the ~2GB dataset once (multi-threaded, sized to hardware_concurrency) before mining starts, in both the participant and solo paths, so RandomXCreateMinerVM() actually gets to use fast mode. Verified: full clean rebuild via src/Makefile. * Add vardiff stall watchdog: force difficulty down on zero shares, not just share count ShareTargetFromDifficulty already fixed the diff-1 scale mismatch (chain floor vs Bitcoin's SHA256 diff-1), so difficulty 1 correctly means ~4096 expected hashes on this chain, not ~2^32. That part was already correct. But the normal vardiff retarget in HandleLine only runs after VARDIFF_SHARES_PER_CHECK (5) shares arrive. A miner that never finds even one share -- e.g. stuck in RandomX light/cache mode instead of the fast 2GB dataset, or just slower hardware than the target assumes -- never reaches that check, so it can sit at its initial/current difficulty indefinitely with vardiff having no samples to act on. Add a time-based fallback in PoolEventLoop: if a miner's vardiff window has been open for VARDIFF_STALL_SECS (90s, 6x the 15s target) with zero shares, force difficulty down by VARDIFF_STALL_CUT (4x) regardless of share count, and re-notify via mining.set_difficulty. Once a miner is already at VARDIFF_MIN, log it plainly as a hashrate/connectivity issue instead of a difficulty issue, so it's not confused with the same-looking but different problem this was actually fixed for before. * Fix PrintException rethrow killing the whole node on any transient exception The uploaded participant log shows the real bug behind the mining stalls: the node process itself was dying and restarting from scratch every minute or two ('UNKNOWN EXCEPTION ... in ThreadMessageHandler()' / 'ThreadSocketHandler()'), wiping the RandomX dataset/cache and losing all progress toward a share -- not a difficulty problem. Every background thread (ThreadMessageHandler, ThreadSocketHandler, ThreadBtfConnect, the Nostr helpers) wraps its real work in a 'try { Foo2() } catch(...) { log, sleep 5s, loop again }' self-healing retry pattern specifically so one bad exception doesn't take the whole node down. But PrintException(), called from inside every one of those catch blocks, ended with 'if (!fShutdown) throw;' -- re-throwing the just- caught exception during ordinary operation. Nothing above the retry loop catches it, so it escapes the thread and std::terminate()s the entire process on the very first transient error (a flaky relay connect, a malformed message, whatever). The comment's intent (suppress noise while already shutting down) was inverted from what the surrounding retry loops actually need. Removed the rethrow. Threads now log the exception and let their own retry loop handle it, as designed. * Push a fresh job immediately after any difficulty change, not just on next broadcast mining.set_difficulty alone doesn't change what the worker actually hashes against -- the participant only reads its share target from mining.notify's params[3] (see main.cpp's mining.notify handler); the set_difficulty message just updates a locally-logged currentDifficulty value. So both the normal vardiff retarget (in HandleLine, on 5 shares) and the new stall watchdog were updating m->shareTarget and telling the worker about the new difficulty, but the worker kept hashing against its old target until the next natural job broadcast -- a new block, or the 60s trickle refresh. That could delay any difficulty correction by up to a minute for no reason. Call SendJob(m, false) right after each set_difficulty send so the updated shareTarget reaches the worker in the very next tick instead of waiting on the next unrelated broadcast. * Build RandomX dataset before stratum handshake, not after Worker was sending mining.subscribe/authorize, then blocking for the duration of the synchronous ~2GB RandomX dataset build, and only then reading the socket. The pool's replies (subscribe ack, authorize ack, set_difficulty, first job) sat unread in the OS receive buffer for that entire window -- nothing was lost, but with the build taking anywhere from seconds to a couple minutes depending on core count, the worker looked and logged as permanently stuck on 'waiting for pool response' when it just wasn't listening yet. Move the dataset build ahead of the subscribe/authorize sends so the socket is actively being read the moment the pool replies. Also add missing RandomXDestroyMinerVM cleanup on the two early-return paths that now happen after VM creation. * Reset round shares on externally-found blocks, not just our own Round shares were only cleared in HandleLine() after our own miner's block got accepted. When the chain height advanced because someone else found the block, the main loop rebuilt the job but left roundShareCount/roundShareTotal/Miner::roundShares untouched, so they kept accumulating across rounds instead of resetting. * Show pool hashrate in participant view; show immature coinbase value with maturity countdown * Persist pending payouts to disk -- survive operator restarts * Fix log escapes, stall timeout 90->45s, save-before-exec ordering, Immature tx status * Sort transactions by nTimeReceived not chain median time -- new txs always on top * Sort txs by chain depth not nTimeReceived -- survives resync, unconfirmed always first * Fix orphan block false-positive payout; meaningful tx status instead of raw depth * Show Orphaned status in red for orphaned coinbases; distinguish from genuinely unconfirmed --------- Co-authored-by: Vic-Nas Co-authored-by: Bitflash Pool Dev Co-authored-by: Victorio Nascimento <155357371+Vic-Nas@users.noreply.github.com> Co-authored-by: Claude --- .gitignore | 1 + Bitflash-1.1.0-windows/glfw3.dll | Bin 0 -> 263756 bytes Bitflash-1.1.0-windows/libcrypto-3-x64.dll | Bin 0 -> 5495100 bytes Bitflash-1.1.0-windows/libdb_cxx-6.2.dll | Bin 0 -> 1666981 bytes Bitflash-1.1.0-windows/libgcc_s_seh-1.dll | Bin 0 -> 153412 bytes Bitflash-1.1.0-windows/libsodium-26.dll | Bin 0 -> 451415 bytes Bitflash-1.1.0-windows/libssl-3-x64.dll | Bin 0 -> 996164 bytes Bitflash-1.1.0-windows/libstdc++-6.dll | Bin 0 -> 2667044 bytes Bitflash-1.1.0-windows/libwinpthread-1.dll | Bin 0 -> 64703 bytes build-linux.sh | 72 - install-bitflash-wine.sh | 211 - src/Makefile | 4 +- src/btfrv.cpp | 4 +- src/db.cpp | 97 +- src/db.h | 15 +- src/gui.cpp | 701 +- src/headers.h | 2 +- src/headers_core.h | 5 +- src/irc.cpp | 271 - src/irc.h | 7 - src/main.cpp | 631 +- src/main.h | 23 +- src/main_gui.cpp | 4 +- src/makefile.linux | 55 - src/makefile.mingw | 4 +- src/makefile.vc | 77 - src/net.cpp | 391 +- src/net.h | 31 +- src/nostr.cpp | 518 +- src/nostr.h | 26 + src/rc/addressbook16.bmp | Bin 1334 -> 0 bytes src/rc/addressbook16mask.bmp | Bin 126 -> 0 bytes src/rc/addressbook20.bmp | Bin 1478 -> 0 bytes src/rc/addressbook20mask.bmp | Bin 142 -> 0 bytes src/rc/bitcoin_bc_original.ico | Bin 22486 -> 0 bytes src/rc/check.ico | Bin 766 -> 0 bytes src/rc/send16.bmp | Bin 1334 -> 0 bytes src/rc/send16mask.bmp | Bin 126 -> 0 bytes src/rc/send16masknoshadow.bmp | Bin 126 -> 0 bytes src/rc/send20.bmp | Bin 1478 -> 0 bytes src/rc/send20mask.bmp | Bin 142 -> 0 bytes src/rpc.cpp | 1544 ++-- src/ui.rc | 14 - src/uiproject.fbp | 11825 --------------------------- src/util.cpp | 108 +- src/util.h | 42 +- 46 files changed, 2648 insertions(+), 14035 deletions(-) create mode 100644 Bitflash-1.1.0-windows/glfw3.dll create mode 100644 Bitflash-1.1.0-windows/libcrypto-3-x64.dll create mode 100644 Bitflash-1.1.0-windows/libdb_cxx-6.2.dll create mode 100644 Bitflash-1.1.0-windows/libgcc_s_seh-1.dll create mode 100644 Bitflash-1.1.0-windows/libsodium-26.dll create mode 100644 Bitflash-1.1.0-windows/libssl-3-x64.dll create mode 100644 Bitflash-1.1.0-windows/libstdc++-6.dll create mode 100644 Bitflash-1.1.0-windows/libwinpthread-1.dll delete mode 100644 build-linux.sh delete mode 100644 install-bitflash-wine.sh delete mode 100644 src/irc.cpp delete mode 100644 src/irc.h delete mode 100644 src/makefile.linux delete mode 100644 src/makefile.vc delete mode 100644 src/rc/addressbook16.bmp delete mode 100644 src/rc/addressbook16mask.bmp delete mode 100644 src/rc/addressbook20.bmp delete mode 100644 src/rc/addressbook20mask.bmp delete mode 100644 src/rc/bitcoin_bc_original.ico delete mode 100644 src/rc/check.ico delete mode 100644 src/rc/send16.bmp delete mode 100644 src/rc/send16mask.bmp delete mode 100644 src/rc/send16masknoshadow.bmp delete mode 100644 src/rc/send20.bmp delete mode 100644 src/rc/send20mask.bmp delete mode 100644 src/ui.rc delete mode 100644 src/uiproject.fbp (limited to 'src/Makefile') diff --git a/.gitignore b/.gitignore index 29ebcff..a734faf 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ dist/ # Editor/OS cruft .DS_Store Thumbs.db +src/bitflash diff --git a/Bitflash-1.1.0-windows/glfw3.dll b/Bitflash-1.1.0-windows/glfw3.dll new file mode 100644 index 0000000..f988a2e Binary files /dev/null and b/Bitflash-1.1.0-windows/glfw3.dll differ diff --git a/Bitflash-1.1.0-windows/libcrypto-3-x64.dll b/Bitflash-1.1.0-windows/libcrypto-3-x64.dll new file mode 100644 index 0000000..76250f0 Binary files /dev/null and b/Bitflash-1.1.0-windows/libcrypto-3-x64.dll differ diff --git a/Bitflash-1.1.0-windows/libdb_cxx-6.2.dll b/Bitflash-1.1.0-windows/libdb_cxx-6.2.dll new file mode 100644 index 0000000..c4279f6 Binary files /dev/null and b/Bitflash-1.1.0-windows/libdb_cxx-6.2.dll differ diff --git a/Bitflash-1.1.0-windows/libgcc_s_seh-1.dll b/Bitflash-1.1.0-windows/libgcc_s_seh-1.dll new file mode 100644 index 0000000..55f4ad1 Binary files /dev/null and b/Bitflash-1.1.0-windows/libgcc_s_seh-1.dll differ diff --git a/Bitflash-1.1.0-windows/libsodium-26.dll b/Bitflash-1.1.0-windows/libsodium-26.dll new file mode 100644 index 0000000..add05ab Binary files /dev/null and b/Bitflash-1.1.0-windows/libsodium-26.dll differ diff --git a/Bitflash-1.1.0-windows/libssl-3-x64.dll b/Bitflash-1.1.0-windows/libssl-3-x64.dll new file mode 100644 index 0000000..cbabbbc Binary files /dev/null and b/Bitflash-1.1.0-windows/libssl-3-x64.dll differ diff --git a/Bitflash-1.1.0-windows/libstdc++-6.dll b/Bitflash-1.1.0-windows/libstdc++-6.dll new file mode 100644 index 0000000..bd3fc0f Binary files /dev/null and b/Bitflash-1.1.0-windows/libstdc++-6.dll differ diff --git a/Bitflash-1.1.0-windows/libwinpthread-1.dll b/Bitflash-1.1.0-windows/libwinpthread-1.dll new file mode 100644 index 0000000..32d8a01 Binary files /dev/null and b/Bitflash-1.1.0-windows/libwinpthread-1.dll differ diff --git a/build-linux.sh b/build-linux.sh deleted file mode 100644 index 4c3844d..0000000 --- a/build-linux.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env bash -# Bitflash headless node/miner -- one-shot Linux build (Debian/Ubuntu). -# Builds the dependencies (libsecp256k1 with schnorrsig, RandomX) and then the -# node itself. Run from the project root (the directory that contains src/): -# -# bash build-linux.sh -# -# Result: src/bitflash-node . Run it with: ./src/bitflash-node -gen - -set -e -ROOT="$(cd "$(dirname "$0")" && pwd)" -cd "$ROOT" - -# Use sudo only when not already root (a root login often has no sudo installed). -SUDO="" -if [ "$(id -u)" -ne 0 ]; then SUDO="sudo"; fi - -echo "==> installing system packages" -if command -v apt-get >/dev/null 2>&1; then - $SUDO apt-get update - $SUDO apt-get install -y build-essential cmake git pkg-config autoconf libtool \ - libssl-dev libdb5.3++-dev libsodium-dev nlohmann-json3-dev libboost-dev \ - libboost-system-dev libwxgtk3.2-dev -else - echo "!! non-apt system: install manually -> g++ cmake git autoconf libtool" - echo " libssl-dev libdb++-dev libsodium-dev nlohmann-json (headers)" -fi - -# ---- libsecp256k1 (Schnorr/BIP340 + extrakeys), static, installed to /usr/local -if [ ! -f /usr/local/lib/libsecp256k1.a ]; then - echo "==> building libsecp256k1" - rm -rf "$ROOT/secp256k1-build" - git clone --depth 1 https://github.com/bitcoin-core/secp256k1 "$ROOT/secp256k1-build" - cd "$ROOT/secp256k1-build" - ./autogen.sh - ./configure --enable-module-schnorrsig --enable-module-extrakeys \ - --disable-shared --with-pic --disable-benchmark --disable-tests - make -j"$(nproc)" - $SUDO make install - $SUDO ldconfig - cd "$ROOT" -else - echo "==> libsecp256k1 already installed, skipping" -fi - -# ---- RandomX (memory-hard CPU PoW), static lib -if [ ! -f "$HOME/RandomX/build/librandomx.a" ]; then - echo "==> building RandomX" - rm -rf "$HOME/RandomX" - git clone --depth 1 https://github.com/tevador/RandomX "$HOME/RandomX" - mkdir -p "$HOME/RandomX/build" - cd "$HOME/RandomX/build" - cmake .. -DCMAKE_BUILD_TYPE=Release - make -j"$(nproc)" randomx - cd "$ROOT" -else - echo "==> RandomX already built, skipping" -fi - -# ---- build -echo "==> building bitflash" -cd "$ROOT/src" -make -f makefile.linux RANDOMX_DIR="$HOME/RandomX" -j"$(nproc)" - -echo -echo "===================================================================" -echo " DONE. $ROOT/src/bitflash" -echo -echo " GUI: ./src/bitflash" -echo " Daemon mode: ./src/bitflash /nogui /gen" -echo " Pool server starts automatically on Stratum :3333, stats :19012" -echo "===================================================================" diff --git a/install-bitflash-wine.sh b/install-bitflash-wine.sh deleted file mode 100644 index 91fef59..0000000 --- a/install-bitflash-wine.sh +++ /dev/null @@ -1,211 +0,0 @@ -#!/usr/bin/env bash -# Bitflash GUI installer for Linux (via Wine) — hardened -set -euo pipefail - -INSTALL_DIR="$HOME/.btf-wine" -WINE_DATA="$HOME/.wine/drive_c/users/$USER/AppData/Roaming/Bitflash" -LINUX_DATA="$HOME/.bitflash" -BIN_DIR="$HOME/.local/bin" -REPO="Bitflash-sh/bitflash" - -echo "==> Bitflash GUI installer" -echo - -# ---- Safety: don't run as root (breaks Wine + pollutes /root) -if [ "$(id -u)" -eq 0 ]; then - echo "ERROR: do not run this as root. Run as your normal user (it will call sudo only when needed)." >&2 - exit 1 -fi - -need() { command -v "$1" >/dev/null 2>&1; } - -# ---- Wine -echo "==> Checking Wine..." -if ! need wine; then - echo " Installing Wine..." - if ! need apt-get; then - echo "ERROR: apt-get not found. Install 'wine' manually, then re-run." >&2 - exit 1 - fi - sudo apt-get update -qq - sudo apt-get install -y wine -fi -echo " Wine: $(wine --version)" - -# ---- db-util (db_dump / db_load for wallet conversion) -echo "==> Checking db-util..." -if ! need db_dump; then - echo " Installing db-util..." - sudo apt-get install -y db-util -fi -echo " db_dump: $(command -v db_dump)" - -# ---- unzip + sha256sum must exist -for tool in unzip sha256sum curl; do - if ! need "$tool"; then - echo " Installing $tool..." - sudo apt-get install -y "$tool" - fi -done - -# ---- Resolve latest release (single API call) -echo "==> Fetching latest release metadata from GitHub..." -API_JSON=$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest") - -RELEASE_URL=$(printf '%s\n' "$API_JSON" | grep '"browser_download_url"' \ - | grep -iE 'win|windows|gui' | head -1 | cut -d '"' -f 4 || true) -[ -z "$RELEASE_URL" ] && RELEASE_URL=$(printf '%s\n' "$API_JSON" | grep '"browser_download_url"' \ - | grep -iE '\.zip"?$' | head -1 | cut -d '"' -f 4 || true) - -if [ -z "$RELEASE_URL" ]; then - echo "ERROR: no downloadable release asset found at https://github.com/$REPO/releases" >&2 - exit 1 -fi -ASSET_NAME=$(basename "$RELEASE_URL") -echo " Asset: $ASSET_NAME" - -# checksum manifest published alongside the release (SHA256SUMS) -SUMS_URL=$(printf '%s\n' "$API_JSON" | grep '"browser_download_url"' \ - | grep -iE 'sha256sums|checksums|\.sha256"?$' | head -1 | cut -d '"' -f 4 || true) - -# ---- Download the release -TMPZIP=$(mktemp /tmp/bitflash-XXXXXX.zip) -cleanup() { rm -f "$TMPZIP" "${TMPSUMS:-}"; } -trap cleanup EXIT -echo "==> Downloading release..." -curl -fL --retry 3 --retry-delay 2 -o "$TMPZIP" "$RELEASE_URL" -echo " Downloaded: $(du -h "$TMPZIP" | cut -f1)" - -# ---- MANDATORY integrity check -echo "==> Verifying integrity..." -GOT=$(sha256sum "$TMPZIP" | awk '{print $1}') -if [ -n "${EXPECTED_SHA256:-}" ]; then - # Strongest: user pinned a known-good hash out-of-band. - if [ "$GOT" != "$EXPECTED_SHA256" ]; then - echo "ERROR: checksum mismatch. expected $EXPECTED_SHA256, got $GOT" >&2 - exit 1 - fi - echo " OK (matches pinned EXPECTED_SHA256)" -elif [ -n "$SUMS_URL" ]; then - TMPSUMS=$(mktemp /tmp/bitflash-sums-XXXXXX) - curl -fsSL --retry 3 -o "$TMPSUMS" "$SUMS_URL" - WANT=$(grep -iF "$ASSET_NAME" "$TMPSUMS" | awk '{print $1}' | head -1 || true) - if [ -z "$WANT" ]; then - echo "ERROR: no checksum for $ASSET_NAME in published SHA256SUMS." >&2 - exit 1 - fi - if [ "$GOT" != "$WANT" ]; then - echo "ERROR: checksum mismatch. published $WANT, got $GOT" >&2 - exit 1 - fi - echo " OK (matches published SHA256SUMS)" -else - echo "ERROR: no checksum available for this release." >&2 - echo " Publish a SHA256SUMS asset with the release, or re-run with" >&2 - echo " EXPECTED_SHA256= to verify against a known-good value." >&2 - echo " (To bypass at your own risk: ALLOW_UNVERIFIED=1)" >&2 - if [ "${ALLOW_UNVERIFIED:-0}" != "1" ]; then - exit 1 - fi - echo " WARNING: proceeding UNVERIFIED (ALLOW_UNVERIFIED=1). sha256=$GOT" -fi - -# ---- Extract -echo "==> Extracting to $INSTALL_DIR..." -rm -rf "$INSTALL_DIR" -mkdir -p "$INSTALL_DIR" -unzip -j -o "$TMPZIP" -d "$INSTALL_DIR" >/dev/null - -# ---- Rename backslash-mangled entries (Windows zip paths) -count=0 -for f in "$INSTALL_DIR"/*; do - [ -f "$f" ] || continue - base=$(basename "$f") - newname=$(printf '%s' "$base" | sed 's/.*\\//') - if [ "$newname" != "$base" ]; then - mv "$f" "$INSTALL_DIR/$newname" - count=$((count+1)) - fi -done -echo " Renamed $count files; total: $(ls "$INSTALL_DIR" | wc -l)" - -# Locate the Bitflash exe case-insensitively (zip may ship Bitflash.exe or bitflash.exe) -EXE=$(find "$INSTALL_DIR" -maxdepth 1 -iname 'bitflash.exe' | head -1) -if [ -z "$EXE" ]; then - echo "ERROR: bitflash exe not found after extract. Contents:" >&2 - ls "$INSTALL_DIR" >&2 - exit 1 -fi -echo " Found $(basename "$EXE")" - -# ---- Init Wine data dir -echo "==> Initializing Wine data directory..." -WINEDLLOVERRIDES="libdb_cxx-6.2=n" wine "$EXE" & -WINE_PID=$! -created=0 -for i in $(seq 1 30); do - sleep 1 - if [ -f "$WINE_DATA/wallet.dat" ]; then - echo " wallet.dat created after ${i}s" - created=1 - break - fi -done -kill "$WINE_PID" 2>/dev/null || true -wait "$WINE_PID" 2>/dev/null || true -sleep 1 -mkdir -p "$WINE_DATA" -[ "$created" -eq 1 ] || echo " NOTE: Wine did not create wallet.dat in 30s; continuing." - -# ---- Optional wallet import (with backup) -echo -if [ -f "$LINUX_DATA/wallet.dat" ]; then - echo "==> Found existing Linux wallet at $LINUX_DATA/wallet.dat" - read -r -p " Import it into the GUI wallet? [Y/n] " ans - ans=${ans:-Y} - if [[ "$ans" =~ ^[Yy] ]]; then - if [ -f "$WINE_DATA/wallet.dat" ]; then - BK="$WINE_DATA/wallet.dat.bak.$(date +%Y%m%d%H%M%S)" - cp "$WINE_DATA/wallet.dat" "$BK" - echo " Backed up existing GUI wallet -> $BK" - fi - DUMP=$(mktemp); CONVERTED=$(mktemp); rm -f "$CONVERTED" - echo " Converting Linux wallet format..." - db_dump -p "$LINUX_DATA/wallet.dat" > "$DUMP" - db_load -f "$DUMP" "$CONVERTED" - cp "$CONVERTED" "$WINE_DATA/wallet.dat" - rm -f "$DUMP" "$CONVERTED" - echo " Wallet imported." - else - echo " Skipped. Re-run to import later." - fi -else - echo "==> No Linux wallet found, starting fresh." -fi - -# ---- Launcher -echo "==> Creating 'bitflash' command..." -mkdir -p "$BIN_DIR" -LAUNCHER_FILE="$BIN_DIR/bitflash" -cat > "$LAUNCHER_FILE" </dev/null; then - echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc" - echo " Added ~/.local/bin to PATH (restart shell or: source ~/.bashrc)" -fi - -echo -echo "==================================================================" -echo " DONE." -echo " Launch: bitflash" -echo " Mine: bitflash -gen" -echo " Relay: bitflash -announcerelay=YOUR_HOST:8434" -echo " Data: $WINE_DATA" -echo "==================================================================" diff --git a/src/Makefile b/src/Makefile index 3280fd3..c0b6e1d 100644 --- a/src/Makefile +++ b/src/Makefile @@ -21,14 +21,14 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \ -lglfw -lGL HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ - key.h bignum.h script.h db.h base58.h net.h irc.h nostr.h \ + key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h compat.h IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ - obj/market.o obj/sha.o obj/irc.o obj/nostr.o obj/randomx_pow.o \ + obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o GUI_OBJS = obj/gui.o obj/main_gui.o diff --git a/src/btfrv.cpp b/src/btfrv.cpp index 812d077..0d02ca8 100644 --- a/src/btfrv.cpp +++ b/src/btfrv.cpp @@ -101,7 +101,7 @@ static SOCKET ConnectTo(const char* host, unsigned short port) // Apply a short timeout only for the connect() and initial handshake so an // unresponsive relay doesn't block the thread. Once the tunnel is live the - // timeout is cleared — a forwarding pipe must block indefinitely on recv(). + // timeout is cleared -- a forwarding pipe must block indefinitely on recv(). #ifdef _WIN32 DWORD tvShort = 10000; setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tvShort, sizeof(tvShort)); @@ -118,7 +118,7 @@ static SOCKET ConnectTo(const char* host, unsigned short port) } freeaddrinfo(res); - // Clear the timeout — the socket is now a live tunnel and must not + // Clear the timeout -- the socket is now a live tunnel and must not // time out when idle. recv() will block until data arrives or peer closes. #ifdef _WIN32 DWORD tvOff = 0; diff --git a/src/db.cpp b/src/db.cpp index de71a57..199fa44 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -392,77 +392,8 @@ bool CTxDB::LoadBlockIndex() -// -// CAddrDB -// - -bool CAddrDB::WriteAddress(const CAddress& addr) -{ - return Write(make_pair(string("addr"), addr.GetKey()), addr); -} - -bool CAddrDB::LoadAddresses() -{ - CRITICAL_BLOCK(cs_mapAddresses) - { - // Load user provided addresses - CAutoFile filein = fopen("addr.txt", "rt"); - if (filein) - { - try - { - char psz[1000]; - while (fgets(psz, sizeof(psz), filein)) - { - CAddress addr(psz, NODE_NETWORK); - if (addr.ip != 0) - AddAddress(*this, addr); - } - } - catch (...) { } - } - - // Get cursor - Dbc* pcursor = GetCursor(); - if (!pcursor) - return false; - - loop - { - // Read next record - CDataStream ssKey; - CDataStream ssValue; - int ret = ReadAtCursor(pcursor, ssKey, ssValue); - if (ret == DB_NOTFOUND) - break; - else if (ret != 0) - return false; - - // Unserialize - string strType; - ssKey >> strType; - if (strType == "addr") - { - CAddress addr; - ssValue >> addr; - mapAddresses.insert(make_pair(addr.GetKey(), addr)); - } - } - - //// debug print - printf("mapAddresses:\n"); - foreach(const PAIRTYPE(vector, CAddress)& item, mapAddresses) - item.second.print(); - printf("-----\n"); - } - - return true; -} - -bool LoadAddresses() -{ - return CAddrDB("cr+").LoadAddresses(); -} +// Legacy IP-based peer address database (CAddrDB / addr.dat) removed -- +// peer discovery is entirely Nostr/.btf-based now. @@ -563,24 +494,28 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) { string strKey; ssKey >> strKey; - if (strKey == "fGenerateBitcoins") ssValue >> fGenerateBitcoins; if (strKey == "nTransactionFee") ssValue >> nTransactionFee; if (strKey == "addrIncoming") ssValue >> addrIncoming; - if (strKey == "nMineMode") ssValue >> nMineMode; - if (strKey == "strParticipantPool") ssValue >> strParticipantPool; - if (strKey == "strPoolName") ssValue >> strPoolName; - if (strKey == "strPoolDashboardUrl") ssValue >> strPoolDashboardUrl; - if (strKey == "dPoolFeePercent") ssValue >> dPoolFeePercent; + // Mining-mode/pool settings (nMineMode, strParticipantPool, strPoolName, + // strPoolDashboardUrl, dPoolFeePercent, fGenerateBitcoins) are intentionally + // NOT restored here. They used to be, and LoadWallet() runs after CLI flags + // are parsed in main_gui.cpp, so a saved value would silently overwrite + // whatever was just requested on the command line or in a previous Options + // session -- no error, no log line. That's what caused pool name/address to + // show correctly in the UI but stay stale in the logs, and mode to change + // behavior across restarts. Mode/pool config is decided fresh every launch + // instead: CLI flags if given, otherwise the compiled-in default. Nothing to + // go stale, nothing to fight over load order. } } } - printf("fGenerateBitcoins = %d\n", fGenerateBitcoins); printf("nTransactionFee = %lld\n", nTransactionFee); printf("addrIncoming = %s\n", addrIncoming.ToString().c_str()); - printf("nMineMode = %d, strParticipantPool = %s\n", nMineMode, strParticipantPool.c_str()); - printf("strPoolName = %s, dPoolFeePercent = %.2f\n", strPoolName.c_str(), dPoolFeePercent); - printf("strPoolDashboardUrl = %s\n", strPoolDashboardUrl.c_str()); + printf("nMineMode = %d, strParticipantPool = %s, fGenerateBitcoins = %d (from CLI/default, not wallet.dat)\n", + nMineMode, strParticipantPool.c_str(), fGenerateBitcoins); + printf("strPoolName = %s, dPoolFeePercent = %.2f, strPoolDashboardUrl = %s (from CLI/default, not wallet.dat)\n", + strPoolName.c_str(), dPoolFeePercent, strPoolDashboardUrl.c_str()); return true; } diff --git a/src/db.h b/src/db.h index feaa45e..6dc3a4f 100644 --- a/src/db.h +++ b/src/db.h @@ -316,19 +316,8 @@ private: -class CAddrDB : public CDB -{ -public: - CAddrDB(const char* pszMode="r+", bool fTxn=false) : CDB("addr.dat", pszMode, fTxn) { } -private: - CAddrDB(const CAddrDB&); - void operator=(const CAddrDB&); -public: - bool WriteAddress(const CAddress& addr); - bool LoadAddresses(); -}; - -bool LoadAddresses(); +// Legacy IP-based peer address database (CAddrDB / addr.dat) removed -- +// peer discovery is entirely Nostr/.btf-based now. diff --git a/src/gui.cpp b/src/gui.cpp index 2210296..a1a0698 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -1,4 +1,4 @@ -// Bitflash GUI — ImGui + GLFW + OpenGL3 +// Bitflash GUI -- ImGui + GLFW + OpenGL3 // Same source on Linux and Windows. No platform ifdefs. #include "imgui/imgui.h" @@ -25,6 +25,7 @@ extern std::string strParticipantPool; extern CAddress addrLocalHost; void MainFrameRepaint(); +int GetDiscoveredPeerCount(); string DateTimeStr(int64 nTime); bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew); int64 GetBalance(); @@ -34,15 +35,20 @@ void ThreadBitcoinMiner(void*); void ThreadRPCServer(void*); // --------------------------------------------------------------------------- -// State +// Layout constants +// --------------------------------------------------------------------------- +static const float STATUS_BAR_H = 22.0f; + +// --------------------------------------------------------------------------- +// GUI state // --------------------------------------------------------------------------- static std::string g_myAddress; -static int64 g_balance = 0; +static int64 g_balance = 0; static int64 g_lastWalletRefresh = 0; -static bool g_showSend = false; -static bool g_showOptions = false; -static bool g_showAbout = false; -static bool g_needRefresh = true; +static bool g_showSend = false; +static bool g_showOptions = false; +static bool g_showAbout = false; +static bool g_needRefresh = true; static char g_sendAddr[128] = {}; static char g_sendAmount[32] = {}; @@ -73,36 +79,34 @@ static std::string FmtMoney(int64 n) static std::string FmtAge(int64 createdAt) { int64 age = GetTime() - createdAt; - if (age < 0) - age = 0; - if (age < 60) - return strprintf("%llds", (long long)age); - if (age < 3600) - return strprintf("%lldm", (long long)(age / 60)); - return strprintf("%lldh", (long long)(age / 3600)); + if (age < 0) age = 0; + if (age < 60) return strprintf("%llds", (long long)age); + if (age < 3600) return strprintf("%lldm", (long long)(age / 60)); + return strprintf("%lldh", (long long)(age / 3600)); } -static ImVec4 ParticipantStatusColor(const std::string& status) +static ImVec4 StatusColor(const std::string& s) { - if (status.find("failed") != std::string::npos || - status.find("rejected") != std::string::npos || - status.find("stopped") != std::string::npos) - return ImVec4(1.0f, 0.38f, 0.38f, 1.0f); - if (status.find("hashing") != std::string::npos || - status.find("accepted") != std::string::npos || - status.find("connected") != std::string::npos) - return ImVec4(0.35f, 1.0f, 0.45f, 1.0f); - if (status.find("asking") != std::string::npos || - status.find("authoriz") != std::string::npos || - status.find("subscrib") != std::string::npos) - return ImVec4(1.0f, 0.75f, 0.25f, 1.0f); - return ImVec4(0.55f, 0.80f, 1.0f, 1.0f); + if (s.find("failed") != std::string::npos || + s.find("rejected") != std::string::npos || + s.find("stopped") != std::string::npos) + return ImVec4(1.0f, 0.38f, 0.38f, 1.0f); // red + if (s.find("hashing") != std::string::npos || + s.find("accepted") != std::string::npos || + s.find("connected")!= std::string::npos) + return ImVec4(0.35f, 1.0f, 0.45f, 1.0f); // green + if (s.find("asking") != std::string::npos || + s.find("authoriz") != std::string::npos || + s.find("subscrib") != std::string::npos) + return ImVec4(1.0f, 0.75f, 0.25f, 1.0f); // amber + return ImVec4(0.55f, 0.80f, 1.0f, 1.0f); // blue-grey } static std::string PoolLabel(const BtfPoolAnnouncement& ann) { std::ostringstream o; - o << ann.poolName << " - " << std::fixed << std::setprecision(2) << ann.feePercent << "%"; + o << ann.poolName << " - " + << std::fixed << std::setprecision(2) << ann.feePercent << "%"; if (ann.connectedMiners > 0) o << " - " << ann.connectedMiners << " miners"; return o.str(); @@ -118,19 +122,47 @@ static void RefreshWallet() g_txRows.clear(); TRY_CRITICAL_BLOCK(cs_mapWallet) { - std::vector> vs; - for (auto& kv : mapWallet) vs.push_back({kv.second.GetTxTime(), kv.first}); - std::sort(vs.rbegin(), vs.rend()); + // Sort: unconfirmed first (depth 0), then by depth ascending (least + // deep = most recent). We can't use nTimeReceived because a node + // resync re-stamps all txs with the same time. Depth is always correct. + std::vector> vs; + for (auto& kv : mapWallet) + vs.push_back({kv.second.GetDepthInMainChain(), kv.second.GetHash()}); + std::sort(vs.begin(), vs.end(), + [](const std::pair& a, const std::pair& b) { + return a.first < b.first; // 0 (unconfirmed) first, then shallowest + }); for (auto& sv : vs) { auto mi = mapWallet.find(sv.second); if (mi == mapWallet.end()) continue; CWalletTx& wtx = mi->second; - int64 net = wtx.GetCredit() - wtx.GetDebit(); - int depth = wtx.GetDepthInMainChain(); + int depth = wtx.GetDepthInMainChain(); + int toMature = wtx.IsCoinBase() ? wtx.GetBlocksToMaturity() : 0; + // An orphaned coinbase has hashBlock set but depth=0 (block not in main chain). + // A genuinely unconfirmed tx has hashBlock==0. + bool isOrphaned = wtx.IsCoinBase() && wtx.hashBlock != 0 && depth == 0; + int64 net; std::string desc; - if (wtx.IsCoinBase()) desc = depth<1 ? "Generated (unconfirmed)" : "Generated"; - else if (wtx.GetDebit() > 0) desc = "Sent"; - else desc = "Received"; + if (wtx.IsCoinBase()) { + if (isOrphaned) { + net = 0; + desc = "Generated (orphaned -- block not in main chain)"; + } else if (toMature > 0) { + net = wtx.GetValueOut(); + char buf[64]; + snprintf(buf, sizeof(buf), "Generated (matures in %d blocks)", toMature); + desc = buf; + } else { + net = wtx.GetCredit() - wtx.GetDebit(); + desc = depth < 1 ? "Generated (unconfirmed)" : "Generated"; + } + } else if (wtx.GetDebit() > 0) { + net = wtx.GetCredit() - wtx.GetDebit(); + desc = "Sent"; + } else { + net = wtx.GetCredit() - wtx.GetDebit(); + desc = "Received"; + } g_txRows.push_back({DateTimeStr(wtx.GetTxTime()), desc, net, depth}); } } @@ -148,19 +180,104 @@ string DateTimeStr(int64 nTime) return buf; } +// --------------------------------------------------------------------------- +// Status bar -- separate fullscreen-width overlay pinned to bottom edge. +// Drawn last so it always sits on top of everything else. +// --------------------------------------------------------------------------- +static void DrawStatusBar() +{ + ImGuiIO& io = ImGui::GetIO(); + ImGui::SetNextWindowPos(ImVec2(0.0f, io.DisplaySize.y - STATUS_BAR_H)); + ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, STATUS_BAR_H)); + ImGui::SetNextWindowBgAlpha(1.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 3.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.13f, 0.13f, 0.13f, 1.0f)); + ImGui::Begin("##statusbar", nullptr, + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse| + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoBringToFrontOnFocus); + ImGui::PopStyleVar(3); + ImGui::PopStyleColor(); + + int connected = (int)vNodes.size(); + int discovered = GetDiscoveredPeerCount(); + + if (nMineMode == MINE_PARTICIPANT) { + uint64 sent = 0, accepted = 0; + double hashRate = 0.0; + std::string st = GetParticipantMiningStatus(); + GetParticipantMiningStats(sent, accepted, hashRate); + if (sent > 0 || accepted > 0 || hashRate > 0.0) + ImGui::TextDisabled( + "Connected peers: %d Detected peers: %d Height: %d" + " Shares: %llu sent / %llu accepted %.2f H/s", + connected, discovered, nBestHeight, + (unsigned long long)sent, (unsigned long long)accepted, hashRate); + else if (!st.empty()) + ImGui::TextDisabled( + "Connected peers: %d Detected peers: %d Height: %d %s", + connected, discovered, nBestHeight, st.c_str()); + else + ImGui::TextDisabled( + "Connected peers: %d Detected peers: %d Height: %d", + connected, discovered, nBestHeight); + } else if (nMineMode == MINE_OPERATOR) { + int miners = 0, blocksFound = 0; + uint64 roundShares = 0; + double totalHashRate = 0.0; + GetPoolOperatorStats(miners, blocksFound, roundShares, totalHashRate); + ImGui::TextDisabled( + "Connected peers: %d Detected peers: %d Height: %d" + " Miners: %d Round shares: %llu Blocks found: %d %.2f H/s", + connected, discovered, nBestHeight, + miners, (unsigned long long)roundShares, blocksFound, totalHashRate); + } else { + ImGui::TextDisabled( + "Connected peers: %d Detected peers: %d Height: %d", + connected, discovered, nBestHeight); + } + + ImGui::End(); +} + // --------------------------------------------------------------------------- // Main window +// +// Layout: +// [menu bar] -- ImGui built-in menu +// [header strip] -- address, balance, send button, mine status +// [separator] +// [telemetry strip] -- mode-specific live stats (never scrolls) +// [separator] +// [scrollable content child] -- transactions + mode panels +// +// The scrollable child ends at the top of the status bar so content never +// hides behind it. The child is explicitly sized to fill the remaining space. // --------------------------------------------------------------------------- static void DrawMainWindow() { ImGuiIO& io = ImGui::GetIO(); - ImGui::SetNextWindowPos(ImVec2(0,0)); - ImGui::SetNextWindowSize(io.DisplaySize); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12,12)); + + // Full display height minus the status bar at the bottom. + const float winH = io.DisplaySize.y - STATUS_BAR_H; + + ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f)); + ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, winH)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 12.0f)); ImGui::Begin("##main", nullptr, - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_MenuBar | - ImGuiWindowFlags_NoFocusOnAppearing); + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_MenuBar | + ImGuiWindowFlags_NoBringToFrontOnFocus); ImGui::SetWindowFontScale(0.92f); ImGui::PopStyleVar(); @@ -170,179 +287,209 @@ static void DrawMainWindow() if (ImGui::MenuItem("Exit")) glfwSetWindowShouldClose(glfwGetCurrentContext(), true); ImGui::EndMenu(); } - if (ImGui::MenuItem("Options")) g_showOptions = true; - if (ImGui::MenuItem("About")) g_showAbout = true; + if (ImGui::MenuItem("Options")) g_showOptions = true; + if (ImGui::MenuItem("About")) g_showAbout = true; ImGui::EndMenuBar(); } - // ---- Address bar ---- + // ---- Header strip (never scrolls) ---- ImGui::Spacing(); ImGui::TextDisabled("Address:"); ImGui::SameLine(); - ImGui::SetNextItemWidth(-80); - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.14f,0.14f,0.14f,1)); + ImGui::SetNextItemWidth(-80.0f); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.14f, 0.14f, 0.14f, 1.0f)); ImGui::InputText("##addr", (char*)g_myAddress.c_str(), g_myAddress.size()+1, ImGuiInputTextFlags_ReadOnly); ImGui::PopStyleColor(); ImGui::SameLine(); if (ImGui::Button("Copy##a")) ImGui::SetClipboardText(g_myAddress.c_str()); - // ---- Balance on its own line ---- ImGui::TextDisabled("Balance:"); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.35f,1.0f,0.35f,1.0f)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.35f, 1.0f, 0.35f, 1.0f)); ImGui::Text("%s BTF", FmtMoney(g_balance).c_str()); ImGui::PopStyleColor(); - // ---- Toolbar ---- ImGui::Spacing(); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f,0.40f,0.15f,1)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.20f,0.55f,0.20f,1)); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.40f, 0.15f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.20f, 0.55f, 0.20f, 1.0f)); if (ImGui::Button(" Send Coins ")) g_showSend = true; ImGui::PopStyleColor(2); - // Mining status indicator - ImGui::SameLine(0, 20); + ImGui::SameLine(0.0f, 20.0f); if (fGenerateBitcoins) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f,0.6f,0.0f,1.0f)); - const char* modeLabel = nMineMode==MINE_OPERATOR ? "Mining (operator)" : - nMineMode==MINE_PARTICIPANT ? "Mining (pool)" : "Mining"; - ImGui::Text("⬤ %s", modeLabel); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.6f, 0.0f, 1.0f)); + const char* modeLabel = + nMineMode == MINE_OPERATOR ? "Mining (operator)" : + nMineMode == MINE_PARTICIPANT ? "Mining (pool)" : + nMineMode == MINE_RELAY ? "Relay" : "Mining"; + ImGui::Text("* %s", modeLabel); ImGui::PopStyleColor(); if (nMineMode == MINE_PARTICIPANT) { - std::string mineStatus = GetParticipantMiningStatus(); - if (!mineStatus.empty()) { - ImGui::SameLine(0, 12); - ImGui::PushStyleColor(ImGuiCol_Text, ParticipantStatusColor(mineStatus)); - ImGui::Text("[%s]", mineStatus.c_str()); + std::string st = GetParticipantMiningStatus(); + if (!st.empty()) { + ImGui::SameLine(0.0f, 12.0f); + ImGui::PushStyleColor(ImGuiCol_Text, StatusColor(st)); + ImGui::Text("[%s]", st.c_str()); ImGui::PopStyleColor(); } } } else { - ImGui::TextDisabled("○ Not mining"); + ImGui::TextDisabled("Not mining"); } ImGui::Spacing(); ImGui::Separator(); - ImGui::Spacing(); - // ---- Live telemetry strip ---- + // ---- Telemetry strip (never scrolls) ---- { - const char* modeName = nMineMode==MINE_OPERATOR ? "Operator" : - nMineMode==MINE_PARTICIPANT ? "Participant" : "Solo"; - int64 age = g_lastWalletRefresh > 0 ? (GetTime() - g_lastWalletRefresh) : -1; - if (age < 0) age = 0; + ImGui::Spacing(); + const char* modeName = + nMineMode == MINE_OPERATOR ? "Operator" : + nMineMode == MINE_PARTICIPANT ? "Participant" : + nMineMode == MINE_RELAY ? "Relay" : "Solo"; + int64 age = g_lastWalletRefresh > 0 ? GetTime() - g_lastWalletRefresh : 0; ImGui::Text("Mode: %s", modeName); - ImGui::SameLine(0, 16); - ImGui::Text("Wallet refresh: %llds ago", (long long)age); + ImGui::SameLine(0.0f, 16.0f); + ImGui::TextDisabled("Wallet refreshed %llds ago", (long long)age); if (nMineMode == MINE_PARTICIPANT) { uint64 sent = 0, accepted = 0; double hashRate = 0.0; - std::string mineStatus = GetParticipantMiningStatus(); + std::string st = GetParticipantMiningStatus(); GetParticipantMiningStats(sent, accepted, hashRate); if (sent > 0 || accepted > 0 || hashRate > 0.0) { - double acceptPct = sent ? (100.0 * (double)accepted / (double)sent) : 0.0; - ImGui::TextDisabled("Your mining: sent %llu, accepted %llu (%.1f%%), %.2f H/s", - (unsigned long long)sent, - (unsigned long long)accepted, - acceptPct, - hashRate); - } else if (!mineStatus.empty()) { - ImGui::TextDisabled("Your mining: %s", mineStatus.c_str()); + double pct = sent ? (100.0 * (double)accepted / (double)sent) : 0.0; + ImGui::TextDisabled( + "Sent %llu shares, accepted %llu (%.1f%%), %.2f H/s", + (unsigned long long)sent, (unsigned long long)accepted, pct, hashRate); + } else if (!st.empty()) { + ImGui::TextDisabled("Status: %s", st.c_str()); } else { - ImGui::TextDisabled("Your mining: asking pool for work"); + ImGui::TextDisabled("Waiting for pool work..."); } } else if (nMineMode == MINE_OPERATOR) { int miners = 0, blocksFound = 0; uint64 roundShares = 0; - GetPoolOperatorStats(miners, blocksFound, roundShares); + double totalHashRate = 0.0; + GetPoolOperatorStats(miners, blocksFound, roundShares, totalHashRate); if (miners > 0 || blocksFound > 0 || roundShares > 0) - ImGui::TextDisabled("Pool runtime: %d authorized miners, %llu round shares, %d blocks found", miners, (unsigned long long)roundShares, blocksFound); + ImGui::TextDisabled( + "Authorized miners: %d Round shares: %llu Blocks found: %d %.2f H/s", + miners, (unsigned long long)roundShares, blocksFound, totalHashRate); else - ImGui::TextDisabled("Pool runtime: waiting for miners"); + ImGui::TextDisabled("Pool server running, waiting for miners"); + } else if (nMineMode == MINE_RELAY) { + ImGui::TextDisabled("Relay mode: syncing, not mining"); } else { - ImGui::TextDisabled("Solo runtime: local wallet mining telemetry"); + ImGui::TextDisabled("Solo mining"); } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); } + // ---- Scrollable content child ---- + // Fills all remaining height in the window (cursor Y to bottom of window). + // Content that overflows scrolls; nothing leaks past the window edge. + // Size the scrollable child to fill exactly the remaining window height. + // GetContentRegionAvail() already accounts for padding/borders. + const float availH = ImGui::GetContentRegionAvail().y; + ImGui::BeginChild("##scroll", ImVec2(0.0f, availH), false, 0); + // ---- Transaction list ---- - float listHeight = io.DisplaySize.y * 0.26f; - if (listHeight < 190.0f) listHeight = 190.0f; - if (listHeight > 260.0f) listHeight = 260.0f; + float txH = io.DisplaySize.y * 0.26f; + if (txH < 180.0f) txH = 180.0f; + if (txH > 260.0f) txH = 260.0f; + if (ImGui::BeginTable("txlist", 4, - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | - ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp, - ImVec2(0, listHeight))) + ImGuiTableFlags_Borders | + ImGuiTableFlags_RowBg | + ImGuiTableFlags_ScrollY | + ImGuiTableFlags_SizingStretchProp, + ImVec2(0.0f, txH))) { ImGui::TableSetupScrollFreeze(0, 1); - ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 110); - ImGui::TableSetupColumn("Date", ImGuiTableColumnFlags_WidthFixed, 130); + ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 110.0f); + ImGui::TableSetupColumn("Date", ImGuiTableColumnFlags_WidthFixed, 130.0f); ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Amount", ImGuiTableColumnFlags_WidthFixed, 140); + ImGui::TableSetupColumn("Amount", ImGuiTableColumnFlags_WidthFixed, 140.0f); ImGui::TableHeadersRow(); for (auto& row : g_txRows) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - if (row.depth < 1) - ImGui::TextColored(ImVec4(1,0.6f,0,1), "Unconfirmed"); + bool isImmatureCoinbase = (row.desc.find("matures in") != std::string::npos); + bool isOrphaned = (row.desc.find("orphaned") != std::string::npos); + if (isOrphaned) + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Orphaned"); + else if (row.depth < 1) + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Unconfirmed"); + else if (isImmatureCoinbase) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), "Immature"); + else if (row.depth < 6) + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f), "%d confirm%s", + row.depth, row.depth == 1 ? "" : "s"); else - ImGui::Text("%d blocks deep", row.depth); + ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.0f), "Confirmed"); ImGui::TableSetColumnIndex(1); ImGui::TextDisabled("%s", row.date.c_str()); ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(row.desc.c_str()); ImGui::TableSetColumnIndex(3); if (row.amount >= 0) - ImGui::TextColored(ImVec4(0.35f,1,0.35f,1), "+%s BTF", FmtMoney(row.amount).c_str()); + ImGui::TextColored(ImVec4(0.35f, 1.0f, 0.35f, 1.0f), + "+%s BTF", FmtMoney(row.amount).c_str()); else - ImGui::TextColored(ImVec4(1,0.4f,0.4f,1), "%s BTF", FmtMoney(row.amount).c_str()); + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), + "%s BTF", FmtMoney(row.amount).c_str()); } ImGui::EndTable(); } + // ---- Mode panels ---- std::vector pools; BtfGetPoolAnnouncements(pools); if (nMineMode == MINE_PARTICIPANT) { ImGui::Spacing(); ImGui::SeparatorText("Participant Mining"); - ImGui::Text("Selected pool:"); + + ImGui::TextDisabled("Selected pool:"); + ImGui::SameLine(); if (strParticipantPool.empty()) - ImGui::TextDisabled("No pool selected yet"); + ImGui::TextDisabled("none"); else ImGui::TextWrapped("%s", strParticipantPool.c_str()); - const BtfPoolAnnouncement* selected = NULL; + const BtfPoolAnnouncement* sel = nullptr; for (size_t i = 0; i < pools.size(); i++) - if (pools[i].btfAddress == strParticipantPool) { selected = &pools[i]; break; } - - if (selected) { - ImGui::Text("Pool name: %s", selected->poolName.c_str()); - ImGui::Text("Fee: %.2f%%", selected->feePercent); - ImGui::Text("Live stats: %d miners, %d blocks", selected->connectedMiners, selected->blocksFound); - if (!selected->dashboardUrl.empty()) - ImGui::TextWrapped("Dashboard: %s", selected->dashboardUrl.c_str()); + if (pools[i].btfAddress == strParticipantPool) { sel = &pools[i]; break; } + + if (sel) { + ImGui::Text("Pool: %s Fee: %.2f%% Miners: %d Blocks: %d %.2f H/s", + sel->poolName.c_str(), sel->feePercent, + sel->connectedMiners, sel->blocksFound, sel->hashRate); + if (!sel->dashboardUrl.empty()) + ImGui::TextDisabled("Dashboard: %s", sel->dashboardUrl.c_str()); } else if (!strParticipantPool.empty()) { - ImGui::TextDisabled("No fresh announcement found for this pool yet"); + ImGui::TextDisabled("No live announcement for this pool yet."); } if (!pools.empty()) { ImGui::Spacing(); ImGui::SeparatorText("Live Pools"); if (ImGui::BeginTable("livepools", 5, - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + ImGuiTableFlags_Borders | + ImGuiTableFlags_RowBg | + ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn("Pool", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Fee", ImGuiTableColumnFlags_WidthFixed, 70); - ImGui::TableSetupColumn("Stats", ImGuiTableColumnFlags_WidthFixed, 160); + ImGui::TableSetupColumn("Pool", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Fee", ImGuiTableColumnFlags_WidthFixed, 60.0f); + ImGui::TableSetupColumn("Stats", ImGuiTableColumnFlags_WidthFixed, 150.0f); ImGui::TableSetupColumn("Dashboard", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Use", ImGuiTableColumnFlags_WidthFixed, 60); + ImGui::TableSetupColumn("Use", ImGuiTableColumnFlags_WidthFixed, 50.0f); ImGui::TableHeadersRow(); for (size_t i = 0; i < pools.size(); i++) { const BtfPoolAnnouncement& ann = pools[i]; @@ -353,15 +500,19 @@ static void DrawMainWindow() ImGui::TableSetColumnIndex(1); ImGui::Text("%.2f%%", ann.feePercent); ImGui::TableSetColumnIndex(2); - ImGui::Text("%d miners, %d blocks, %s old", ann.connectedMiners, ann.blocksFound, FmtAge(ann.createdAt).c_str()); + ImGui::Text("%d miners %d blocks %.2f H/s %s", + ann.connectedMiners, ann.blocksFound, ann.hashRate, + FmtAge(ann.createdAt).c_str()); ImGui::TableSetColumnIndex(3); if (!ann.dashboardUrl.empty()) ImGui::TextWrapped("%s", ann.dashboardUrl.c_str()); else ImGui::TextDisabled("-"); ImGui::TableSetColumnIndex(4); - if (ImGui::SmallButton((std::string("Use##") + ann.btfAddress).c_str())) { - strncpy(g_participantPool, ann.btfAddress.c_str(), sizeof(g_participantPool)-1); + std::string btnId = "Use##" + ann.btfAddress; + if (ImGui::SmallButton(btnId.c_str())) { + strncpy(g_participantPool, ann.btfAddress.c_str(), + sizeof(g_participantPool)-1); strParticipantPool = ann.btfAddress; CWalletDB().WriteSetting("strParticipantPool", strParticipantPool); g_needRefresh = true; @@ -375,55 +526,60 @@ static void DrawMainWindow() if (nMineMode == MINE_OPERATOR) { ImGui::Spacing(); ImGui::SeparatorText("Operator Overview"); - ImGui::Text("Pool .btf address:"); + std::string opAddr = BtfLocalAddress(); + ImGui::TextDisabled("Pool .btf address:"); + ImGui::SameLine(); if (opAddr.empty()) - ImGui::TextDisabled("Identity not ready yet"); + ImGui::TextDisabled("identity not ready"); else ImGui::TextWrapped("%s", opAddr.c_str()); + // Pending payouts std::vector owed; GetPendingPayouts(owed); ImGui::Spacing(); - ImGui::SeparatorText("Pending Pool Payouts (you owe miners)"); + ImGui::SeparatorText("Pending Payouts (owed to miners)"); if (owed.empty()) { - ImGui::TextDisabled("No pending pool payouts"); + ImGui::TextDisabled("No pending payouts"); } else if (ImGui::BeginTable("owedpayouts", 4, - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + ImGuiTableFlags_Borders | + ImGuiTableFlags_RowBg | + ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn("Matures At", ImGuiTableColumnFlags_WidthFixed, 100); - ImGui::TableSetupColumn("Blocks Left", ImGuiTableColumnFlags_WidthFixed, 90); - ImGui::TableSetupColumn("Recipients", ImGuiTableColumnFlags_WidthFixed, 90); - ImGui::TableSetupColumn("Total", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Matures At", ImGuiTableColumnFlags_WidthFixed, 90.0f); + ImGui::TableSetupColumn("Blocks left", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableSetupColumn("Recipients", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableSetupColumn("Total", ImGuiTableColumnFlags_WidthStretch); ImGui::TableHeadersRow(); for (const PendingPayoutView& row : owed) { - int remaining = std::max(0, row.matureAtHeight - nBestHeight); + int left = std::max(0, row.matureAtHeight - nBestHeight); ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("%d", row.matureAtHeight); - ImGui::TableSetColumnIndex(1); - ImGui::Text("%d", remaining); - ImGui::TableSetColumnIndex(2); - ImGui::Text("%d", row.recipients); - ImGui::TableSetColumnIndex(3); - ImGui::Text("%s BTF", FmtMoney(row.totalAmount).c_str()); + ImGui::TableSetColumnIndex(0); ImGui::Text("%d", row.matureAtHeight); + ImGui::TableSetColumnIndex(1); ImGui::Text("%d", left); + ImGui::TableSetColumnIndex(2); ImGui::Text("%d", row.recipients); + ImGui::TableSetColumnIndex(3); ImGui::Text("%s BTF", FmtMoney(row.totalAmount).c_str()); } ImGui::EndTable(); } + // Workers std::vector workers; GetPoolWorkerStats(workers); ImGui::Spacing(); ImGui::SeparatorText("Current Round Workers"); if (workers.empty()) { - ImGui::TextDisabled("No active workers in this round"); - } else if (ImGui::BeginTable("workers", 4, - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + ImGui::TextDisabled("No active workers this round"); + } else if (ImGui::BeginTable("workers", 5, + ImGuiTableFlags_Borders | + ImGuiTableFlags_RowBg | + ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn("Payout Addr", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Worker", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Round Shares", ImGuiTableColumnFlags_WidthFixed, 90); - ImGui::TableSetupColumn("Last Seen", ImGuiTableColumnFlags_WidthFixed, 100); + ImGui::TableSetupColumn("Payout Address", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Worker", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Hashrate", ImGuiTableColumnFlags_WidthFixed, 90.0f); + ImGui::TableSetupColumn("Round shares", ImGuiTableColumnFlags_WidthFixed, 90.0f); + ImGui::TableSetupColumn("Last seen", ImGuiTableColumnFlags_WidthFixed, 90.0f); ImGui::TableHeadersRow(); for (const PoolWorkerStatView& row : workers) { ImGui::TableNextRow(); @@ -432,53 +588,19 @@ static void DrawMainWindow() ImGui::TableSetColumnIndex(1); ImGui::TextDisabled("%s", row.worker.empty() ? "worker" : row.worker.c_str()); ImGui::TableSetColumnIndex(2); - ImGui::Text("%llu", (unsigned long long)row.roundShares); + ImGui::Text("%.2f H/s", row.hashRate); ImGui::TableSetColumnIndex(3); - ImGui::TextDisabled("%s", FmtAge(row.lastSeen).c_str()); + ImGui::Text("%llu", (unsigned long long)row.roundShares); + ImGui::TableSetColumnIndex(4); + ImGui::TextDisabled("%s ago", FmtAge(row.lastSeen).c_str()); } ImGui::EndTable(); } } - // ---- Status bar ---- - { - ImGui::Spacing(); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4,2)); - - int peers = (int)vNodes.size(); - if (nMineMode == MINE_PARTICIPANT) { - uint64 sent = 0, accepted = 0; - double hashRate = 0.0; - std::string mineStatus = GetParticipantMiningStatus(); - GetParticipantMiningStats(sent, accepted, hashRate); - if (sent > 0 || accepted > 0 || hashRate > 0.0) - ImGui::TextDisabled("Peers %d height %d shares sent %llu accepted %llu hash rate %.2f H/s", - peers, - nBestHeight, - (unsigned long long)sent, - (unsigned long long)accepted, - hashRate); - else if (!mineStatus.empty()) - ImGui::TextDisabled("Peers %d height %d %s", peers, nBestHeight, mineStatus.c_str()); - else - ImGui::TextDisabled("Peers %d height %d asking pool for work", peers, nBestHeight); - } else if (nMineMode == MINE_OPERATOR) { - int miners = 0, blocksFound = 0; - uint64 roundShares = 0; - GetPoolOperatorStats(miners, blocksFound, roundShares); - if (miners > 0 || blocksFound > 0 || roundShares > 0) - ImGui::TextDisabled("Peers %d height %d authorized miners %d round shares %llu blocks found %d", - peers, nBestHeight, miners, (unsigned long long)roundShares, blocksFound); - else - ImGui::TextDisabled("Peers %d height %d waiting for miners", peers, nBestHeight); - } else { - ImGui::TextDisabled("Peers %d height %d", peers, nBestHeight); - } - ImGui::PopStyleVar(); - } - + ImGui::EndChild(); // ##scroll ImGui::SetWindowFontScale(1.0f); - ImGui::End(); + ImGui::End(); // ##main } // --------------------------------------------------------------------------- @@ -487,42 +609,53 @@ static void DrawMainWindow() static void DrawSendDialog() { if (!g_showSend) return; - ImGui::SetNextWindowSize(ImVec2(500, 180), ImGuiCond_Always); - ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f)); + ImGui::SetNextWindowSize(ImVec2(500.0f, 180.0f), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), + ImGuiCond_Always, ImVec2(0.5f, 0.5f)); if (ImGui::Begin("Send Coins", &g_showSend, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) { ImGui::Text("Recipient address:"); - ImGui::SetNextItemWidth(-1); + ImGui::SetNextItemWidth(-1.0f); ImGui::InputText("##sa", g_sendAddr, sizeof(g_sendAddr)); ImGui::Text("Amount (BTF):"); - ImGui::SetNextItemWidth(180); + ImGui::SetNextItemWidth(180.0f); ImGui::InputText("##sm", g_sendAmount, sizeof(g_sendAmount)); if (!g_sendStatus.empty()) { ImGui::Spacing(); - ImGui::TextColored(ImVec4(1,.4f,.4f,1), "%s", g_sendStatus.c_str()); + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), + "%s", g_sendStatus.c_str()); } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - if (ImGui::Button("Send", ImVec2(90,0))) { + if (ImGui::Button("Send", ImVec2(90.0f, 0.0f))) { uint160 h; int64 nv = 0; - if (!AddressToHash160(g_sendAddr, h)) g_sendStatus = "Invalid address."; - else if (!ParseMoney(g_sendAmount,nv)||nv<=0) g_sendStatus = "Invalid amount."; + if (!AddressToHash160(g_sendAddr, h)) + g_sendStatus = "Invalid address."; + else if (!ParseMoney(g_sendAmount, nv) || nv <= 0) + g_sendStatus = "Invalid amount."; else { - CScript s; s << OP_DUP << OP_HASH160 << h << OP_EQUALVERIFY << OP_CHECKSIG; + CScript sc; + sc << OP_DUP << OP_HASH160 << h << OP_EQUALVERIFY << OP_CHECKSIG; CWalletTx wtx; - if (SendMoney(s, nv, wtx)) { - g_showSend=false; g_sendStatus=""; - memset(g_sendAddr,0,sizeof(g_sendAddr)); - memset(g_sendAmount,0,sizeof(g_sendAmount)); + if (SendMoney(sc, nv, wtx)) { + g_showSend = false; + g_sendStatus = ""; + memset(g_sendAddr, 0, sizeof(g_sendAddr)); + memset(g_sendAmount, 0, sizeof(g_sendAmount)); g_needRefresh = true; - } else g_sendStatus = "Transaction failed."; + } else { + g_sendStatus = "Transaction failed."; + } } } ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(90,0))) { g_showSend=false; g_sendStatus=""; } + if (ImGui::Button("Cancel", ImVec2(90.0f, 0.0f))) { + g_showSend = false; + g_sendStatus = ""; + } } ImGui::End(); } @@ -534,7 +667,6 @@ static void DrawOptionsDialog() { if (!g_showOptions) return; - // Always sync from current globals when dialog opens static bool wasOpen = false; if (!wasOpen) { g_mineRadio = nMineMode; @@ -544,42 +676,46 @@ static void DrawOptionsDialog() strncpy(g_poolDash, strPoolDashboardUrl.c_str(), sizeof(g_poolDash)-1); } wasOpen = true; - ImGui::SetNextWindowSize(ImVec2(500, 310), ImGuiCond_Always); - ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f)); + + ImGui::SetNextWindowSize(ImVec2(500.0f, 335.0f), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), + ImGuiCond_Always, ImVec2(0.5f, 0.5f)); if (ImGui::Begin("Options", &g_showOptions, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) { // Transaction fee ImGui::SeparatorText("Transaction Fee"); static char feeStr[32] = {}; - if (feeStr[0]=='\0') snprintf(feeStr,sizeof(feeStr),"%s",FmtMoney(nTransactionFee).c_str()); - ImGui::SetNextItemWidth(180); + if (feeStr[0] == '\0') + snprintf(feeStr, sizeof(feeStr), "%s", FmtMoney(nTransactionFee).c_str()); + ImGui::SetNextItemWidth(180.0f); ImGui::InputText("BTF per transaction##fee", feeStr, sizeof(feeStr)); - // Mining + // Mining mode ImGui::SeparatorText("Mining Mode"); - ImGui::RadioButton("Solo — rewards go to your wallet", &g_mineRadio, MINE_SOLO); - ImGui::RadioButton("Operator — run a pool for other miners", &g_mineRadio, MINE_OPERATOR); - ImGui::RadioButton("Participant — mine to someone's pool", &g_mineRadio, MINE_PARTICIPANT); + ImGui::RadioButton("Relay -- just run the node, don't mine", &g_mineRadio, MINE_RELAY); + ImGui::RadioButton("Solo -- rewards go to your wallet", &g_mineRadio, MINE_SOLO); + ImGui::RadioButton("Operator -- run a pool for other miners", &g_mineRadio, MINE_OPERATOR); + ImGui::RadioButton("Participant -- mine to someone's pool", &g_mineRadio, MINE_PARTICIPANT); if (g_mineRadio == MINE_OPERATOR) { ImGui::Spacing(); ImGui::SeparatorText("Pool Announcement"); ImGui::Text("Pool name:"); - ImGui::SetNextItemWidth(-1); + ImGui::SetNextItemWidth(-1.0f); ImGui::InputText("##poolname", g_poolName, sizeof(g_poolName)); ImGui::Text("Fee %%:"); - ImGui::SetNextItemWidth(120); + ImGui::SetNextItemWidth(120.0f); ImGui::InputText("##poolfee", g_poolFee, sizeof(g_poolFee)); ImGui::Text("Dashboard URL:"); - ImGui::SetNextItemWidth(-1); + ImGui::SetNextItemWidth(-1.0f); ImGui::InputText("##pooldash", g_poolDash, sizeof(g_poolDash)); } if (g_mineRadio == MINE_PARTICIPANT) { ImGui::Spacing(); ImGui::SeparatorText("Pool Selection"); - ImGui::TextDisabled("Pick from live discovered pools. Manual entry is disabled here."); + ImGui::TextDisabled("Pick from live discovered pools."); std::vector pools; BtfGetPoolAnnouncements(pools); @@ -591,46 +727,49 @@ static void DrawOptionsDialog() break; } } - if (ImGui::BeginCombo("Discovered pools", current.c_str())) { + if (ImGui::BeginCombo("Discovered pools##combo", current.c_str())) { for (size_t i = 0; i < pools.size(); i++) { const BtfPoolAnnouncement& ann = pools[i]; - std::string label = PoolLabel(ann); - bool selected = ann.btfAddress == g_participantPool; - if (ImGui::Selectable(label.c_str(), selected)) { - strncpy(g_participantPool, ann.btfAddress.c_str(), sizeof(g_participantPool)-1); - } + bool selected = (ann.btfAddress == g_participantPool); + if (ImGui::Selectable(PoolLabel(ann).c_str(), selected)) + strncpy(g_participantPool, ann.btfAddress.c_str(), + sizeof(g_participantPool)-1); } ImGui::EndCombo(); } } else { - ImGui::TextDisabled("No live pools discovered yet"); + ImGui::TextDisabled("No live pools discovered yet."); } } - ImGui::Spacing(); - const char* genLabel = fGenerateBitcoins ? "Stop Mining" : "Start Mining"; - if (ImGui::Button(genLabel, ImVec2(130,0))) { - fGenerateBitcoins = fGenerateBitcoins ? 0 : 1; - if (fGenerateBitcoins) _beginthread(ThreadBitcoinMiner, 0, NULL); - CWalletDB().WriteSetting("fGenerateBitcoins", fGenerateBitcoins); - } - ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - if (ImGui::Button("OK", ImVec2(90,0))) { - int64 fee=0; - if (ParseMoney(feeStr,fee)) { nTransactionFee=fee; CWalletDB().WriteSetting("nTransactionFee",nTransactionFee); } - - bool changed = (g_mineRadio != nMineMode); - nMineMode = g_mineRadio; - strParticipantPool = g_participantPool; - strPoolName = g_poolName[0] ? g_poolName : "Bitflash Pool"; - strPoolDashboardUrl = g_poolDash; - dPoolFeePercent = atof(g_poolFee); - CWalletDB().WriteSetting("nMineMode", nMineMode); - CWalletDB().WriteSetting("strParticipantPool", strParticipantPool); - CWalletDB().WriteSetting("strPoolName", strPoolName); - CWalletDB().WriteSetting("strPoolDashboardUrl", strPoolDashboardUrl); - CWalletDB().WriteSetting("dPoolFeePercent", dPoolFeePercent); + if (ImGui::Button("OK", ImVec2(90.0f, 0.0f))) { + int64 fee = 0; + if (ParseMoney(feeStr, fee)) { + nTransactionFee = fee; + CWalletDB().WriteSetting("nTransactionFee", nTransactionFee); + } + + bool poolChanged = + nMineMode != g_mineRadio || + strPoolName != (g_poolName[0] ? g_poolName : "Bitflash Pool") || + strPoolDashboardUrl != g_poolDash || + dPoolFeePercent != atof(g_poolFee); + + nMineMode = g_mineRadio; + strParticipantPool = g_participantPool; + strPoolName = g_poolName[0] ? g_poolName : "Bitflash Pool"; + strPoolDashboardUrl = g_poolDash; + dPoolFeePercent = atof(g_poolFee); + + if (nMineMode == MINE_OPERATOR && poolChanged) gAnnounceNow = true; + + if (nMineMode == MINE_RELAY) { + fGenerateBitcoins = 0; + } else { + fGenerateBitcoins = 1; + if (!vfThreadRunning[3]) _beginthread(ThreadBitcoinMiner, 0, NULL); + } if (nMineMode == MINE_OPERATOR) { if (!gPoolServerRunning) { @@ -645,7 +784,7 @@ static void DrawOptionsDialog() g_showOptions = false; } ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(90,0))) g_showOptions = false; + if (ImGui::Button("Cancel", ImVec2(90.0f, 0.0f))) g_showOptions = false; } if (!g_showOptions) wasOpen = false; ImGui::End(); @@ -657,18 +796,21 @@ static void DrawOptionsDialog() static void DrawAboutDialog() { if (!g_showAbout) return; - ImGui::SetNextWindowSize(ImVec2(380, 170), ImGuiCond_Always); - ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f)); + ImGui::SetNextWindowSize(ImVec2(380.0f, 170.0f), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), + ImGuiCond_Always, ImVec2(0.5f, 0.5f)); if (ImGui::Begin("About Bitflash", &g_showAbout, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) { - ImGui::TextColored(ImVec4(1,.6f,0,1), "Bitflash BTF v1.1.0"); + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Bitflash BTF v1.1.0"); ImGui::Spacing(); - ImGui::TextWrapped("CPU-only cryptocurrency. RandomX proof of work. Anonymous .btf addressing over Nostr. No premine."); + ImGui::TextWrapped( + "CPU-only cryptocurrency. RandomX proof of work. " + "Anonymous .btf addressing over Nostr. No premine."); ImGui::Spacing(); ImGui::TextDisabled("Based on Bitcoin 0.1.0 (Satoshi Nakamoto, 2009)"); ImGui::Spacing(); - if (ImGui::Button("OK", ImVec2(90,0))) g_showAbout = false; + if (ImGui::Button("OK", ImVec2(90.0f, 0.0f))) g_showAbout = false; } ImGui::End(); } @@ -705,34 +847,33 @@ int RunGUI(int argc, char* argv[]) ImGuiIO& io = ImGui::GetIO(); io.IniFilename = nullptr; - // Load Roboto at 16px — clean, readable on all platforms io.Fonts->AddFontFromMemoryCompressedTTF( - RobotoMedium_compressed_data, RobotoMedium_compressed_size, 16.0f); + RobotoMedium_compressed_data, RobotoMedium_compressed_size, 14.0f); ImGui::StyleColorsDark(); - ImGuiStyle& s = ImGui::GetStyle(); - s.WindowRounding = s.FrameRounding = s.PopupRounding = 4.0f; - s.ItemSpacing = ImVec2(10, 7); - s.FramePadding = ImVec2(8, 5); - s.WindowPadding = ImVec2(12, 12); + ImGuiStyle& st = ImGui::GetStyle(); + st.WindowRounding = st.FrameRounding = st.PopupRounding = 4.0f; + st.ItemSpacing = ImVec2(10.0f, 7.0f); + st.FramePadding = ImVec2(8.0f, 5.0f); + st.WindowPadding = ImVec2(12.0f, 12.0f); // Orange accent - s.Colors[ImGuiCol_TitleBgActive] = ImVec4(.80f,.45f,.00f,1); - s.Colors[ImGuiCol_CheckMark] = ImVec4(1.0f,.60f,.00f,1); - s.Colors[ImGuiCol_SliderGrab] = ImVec4(.80f,.45f,.00f,1); - s.Colors[ImGuiCol_Button] = ImVec4(.25f,.25f,.25f,1); - s.Colors[ImGuiCol_ButtonHovered] = ImVec4(.80f,.45f,.00f,1); - s.Colors[ImGuiCol_ButtonActive] = ImVec4(.60f,.30f,.00f,1); - s.Colors[ImGuiCol_Header] = ImVec4(.80f,.45f,.00f,.40f); - s.Colors[ImGuiCol_HeaderHovered] = ImVec4(.80f,.45f,.20f,.80f); - s.Colors[ImGuiCol_Tab] = ImVec4(.20f,.20f,.20f,1); - s.Colors[ImGuiCol_TabHovered] = ImVec4(.80f,.45f,.00f,1); - s.Colors[ImGuiCol_TabActive] = ImVec4(.80f,.45f,.00f,1); - s.Colors[ImGuiCol_SeparatorHovered]=ImVec4(.80f,.45f,.00f,1); - s.Colors[ImGuiCol_FrameBg] = ImVec4(.14f,.14f,.14f,1); - s.Colors[ImGuiCol_FrameBgHovered] = ImVec4(.20f,.20f,.20f,1); - s.Colors[ImGuiCol_TableHeaderBg] = ImVec4(.18f,.18f,.18f,1); - s.Colors[ImGuiCol_TableRowBg] = ImVec4(.11f,.11f,.11f,1); - s.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(.14f,.14f,.14f,1); + st.Colors[ImGuiCol_TitleBgActive] = ImVec4(.80f, .45f, .00f, 1.0f); + st.Colors[ImGuiCol_CheckMark] = ImVec4(1.0f, .60f, .00f, 1.0f); + st.Colors[ImGuiCol_SliderGrab] = ImVec4(.80f, .45f, .00f, 1.0f); + st.Colors[ImGuiCol_Button] = ImVec4(.25f, .25f, .25f, 1.0f); + st.Colors[ImGuiCol_ButtonHovered] = ImVec4(.80f, .45f, .00f, 1.0f); + st.Colors[ImGuiCol_ButtonActive] = ImVec4(.60f, .30f, .00f, 1.0f); + st.Colors[ImGuiCol_Header] = ImVec4(.80f, .45f, .00f, .40f); + st.Colors[ImGuiCol_HeaderHovered] = ImVec4(.80f, .45f, .20f, .80f); + st.Colors[ImGuiCol_Tab] = ImVec4(.20f, .20f, .20f, 1.0f); + st.Colors[ImGuiCol_TabHovered] = ImVec4(.80f, .45f, .00f, 1.0f); + st.Colors[ImGuiCol_TabActive] = ImVec4(.80f, .45f, .00f, 1.0f); + st.Colors[ImGuiCol_SeparatorHovered]= ImVec4(.80f, .45f, .00f, 1.0f); + st.Colors[ImGuiCol_FrameBg] = ImVec4(.14f, .14f, .14f, 1.0f); + st.Colors[ImGuiCol_FrameBgHovered] = ImVec4(.20f, .20f, .20f, 1.0f); + st.Colors[ImGuiCol_TableHeaderBg] = ImVec4(.18f, .18f, .18f, 1.0f); + st.Colors[ImGuiCol_TableRowBg] = ImVec4(.11f, .11f, .11f, 1.0f); + st.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(.14f, .14f, .14f, 1.0f); ImGui_ImplGlfw_InitForOpenGL(window, true); ImGui_ImplOpenGL3_Init("#version 330"); @@ -744,22 +885,28 @@ int RunGUI(int argc, char* argv[]) while (!glfwWindowShouldClose(window) && !fShutdown) { glfwPollEvents(); - if (g_needRefresh || (frame % 120 == 0)) { RefreshWallet(); g_needRefresh = false; } + if (g_needRefresh || (frame % 120 == 0)) { + RefreshWallet(); + g_needRefresh = false; + } frame++; ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); + // Draw order matters: status bar last so it is always on top. DrawMainWindow(); DrawSendDialog(); DrawOptionsDialog(); DrawAboutDialog(); + DrawStatusBar(); ImGui::Render(); - int w, h; glfwGetFramebufferSize(window, &w, &h); + int w, h; + glfwGetFramebufferSize(window, &w, &h); glViewport(0, 0, w, h); - glClearColor(.08f,.08f,.08f,1); + glClearColor(.08f, .08f, .08f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); glfwSwapBuffers(window); diff --git a/src/headers.h b/src/headers.h index bb252c7..367e9b0 100644 --- a/src/headers.h +++ b/src/headers.h @@ -1,4 +1,4 @@ -// Bitflash — main include file. +// Bitflash -- main include file. // Includes core node headers. GUI is handled separately in gui.cpp. #pragma once #include "headers_core.h" diff --git a/src/headers_core.h b/src/headers_core.h index 089b67f..6c0c602 100644 --- a/src/headers_core.h +++ b/src/headers_core.h @@ -1,4 +1,4 @@ -// Core node headers — no GUI framework dependency. +// Core node headers -- no GUI framework dependency. // Included by gui.cpp and all non-GUI source files. #ifdef _MSC_VER @@ -63,12 +63,11 @@ inline typename std::common_type::type max(const T1& a, const T2& b) #include "script.h" #include "db.h" #include "net.h" -#include "irc.h" #include "nostr.h" #include "randomx_pow.h" #include "main.h" #include "market.h" -// GUI callback — implemented in gui.cpp (or as no-op in headless builds) +// GUI callback -- implemented in gui.cpp (or as no-op in headless builds) void MainFrameRepaint(); string DateTimeStr(int64 nTime); diff --git a/src/irc.cpp b/src/irc.cpp deleted file mode 100644 index ff82c66..0000000 --- a/src/irc.cpp +++ /dev/null @@ -1,271 +0,0 @@ -// Copyright (c) 2009 Satoshi Nakamoto -// Distributed under the MIT/X11 software license, see the accompanying -// file license.txt or http://www.opensource.org/licenses/mit-license.php. - -#include "headers.h" - - - - -#pragma pack(1) -struct ircaddr -{ - int ip; - short port; -}; - -string EncodeAddress(const CAddress& addr) -{ - struct ircaddr tmp; - tmp.ip = addr.ip; - tmp.port = addr.port; - - vector vch(UBEGIN(tmp), UEND(tmp)); - return string("u") + EncodeBase58Check(vch); -} - -bool DecodeAddress(string str, CAddress& addr) -{ - vector vch; - if (!DecodeBase58Check(str.substr(1), vch)) - return false; - - struct ircaddr tmp; - if (vch.size() != sizeof(tmp)) - return false; - memcpy(&tmp, &vch[0], sizeof(tmp)); - - addr = CAddress(tmp.ip, tmp.port); - return true; -} - - - - - - -static bool Send(SOCKET hSocket, const char* pszSend) -{ - if (strstr(pszSend, "PONG") != pszSend) - printf("SENDING: %s\n", pszSend); - const char* psz = pszSend; - const char* pszEnd = psz + strlen(psz); - while (psz < pszEnd) - { - int ret = send(hSocket, psz, pszEnd - psz, 0); - if (ret < 0) - return false; - psz += ret; - } - return true; -} - -bool RecvLine(SOCKET hSocket, string& strLine) -{ - strLine = ""; - loop - { - char c; - int nBytes = recv(hSocket, &c, 1, 0); - if (nBytes > 0) - { - if (c == '\n') - continue; - if (c == '\r') - return true; - strLine += c; - } - else if (nBytes <= 0) - { - if (!strLine.empty()) - return true; - // socket closed - printf("IRC socket closed\n"); - return false; - } - else - { - // socket error - int nErr = WSAGetLastError(); - if (nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS) - { - printf("IRC recv failed: %d\n", nErr); - return false; - } - } - } -} - -bool RecvLineIRC(SOCKET hSocket, string& strLine) -{ - loop - { - bool fRet = RecvLine(hSocket, strLine); - if (fRet) - { - if (fShutdown) - return false; - vector vWords; - ParseString(strLine, ' ', vWords); - if (vWords[0] == "PING") - { - strLine[1] = 'O'; - strLine += '\r'; - Send(hSocket, strLine.c_str()); - continue; - } - } - return fRet; - } -} - -bool RecvUntil(SOCKET hSocket, const char* psz1, const char* psz2=NULL, const char* psz3=NULL) -{ - loop - { - string strLine; - if (!RecvLineIRC(hSocket, strLine)) - return false; - printf("IRC %s\n", strLine.c_str()); - if (psz1 && strLine.find(psz1) != -1) - return true; - if (psz2 && strLine.find(psz2) != -1) - return true; - if (psz3 && strLine.find(psz3) != -1) - return true; - } -} - - - - -bool fRestartIRCSeed = false; - -void ThreadIRCSeed(void* parg) -{ - // IRC peer discovery disabled: the 2009 IRC network no longer exists (and - // what answers today crashes the parser). Replaced by Nostr relay - // discovery. - printf("ThreadIRCSeed: IRC peer discovery disabled (using Nostr)\n"); - return; - - loop - { - struct hostent* phostent = gethostbyname("chat.freenode.net"); - CAddress addrConnect(*(u_long*)phostent->h_addr_list[0], htons(6667)); - - SOCKET hSocket; - if (!ConnectSocket(addrConnect, hSocket)) - { - printf("IRC connect failed\n"); - return; - } - - if (!RecvUntil(hSocket, "Found your hostname", "using your IP address instead", "Couldn't look up your hostname")) - { - closesocket(hSocket); - return; - } - - string strMyName = EncodeAddress(addrLocalHost); - - if (!addrLocalHost.IsRoutable()) - strMyName = strprintf("x%u", GetRand(1000000000)); - - Send(hSocket, strprintf("NICK %s\r", strMyName.c_str()).c_str()); - Send(hSocket, strprintf("USER %s 8 * : %s\r", strMyName.c_str(), strMyName.c_str()).c_str()); - - if (!RecvUntil(hSocket, " 004 ")) - { - closesocket(hSocket); - return; - } - Sleep(500); - - Send(hSocket, "JOIN #bitcoin\r"); - Send(hSocket, "WHO #bitcoin\r"); - - while (!fRestartIRCSeed) - { - string strLine; - if (fShutdown || !RecvLineIRC(hSocket, strLine)) - { - closesocket(hSocket); - return; - } - if (strLine.empty() || strLine[0] != ':') - continue; - printf("IRC %s\n", strLine.c_str()); - - vector vWords; - ParseString(strLine, ' ', vWords); - if (vWords.size() < 2) - continue; - - char pszName[10000]; - pszName[0] = '\0'; - - if (vWords[1] == "352" && vWords.size() >= 8) - { - // index 7 is limited to 16 characters - // could get full length name at index 10, but would be different from join messages - strcpy(pszName, vWords[7].c_str()); - printf("GOT WHO: [%s] ", pszName); - } - - if (vWords[1] == "JOIN") - { - // :username!username@50000007.F000000B.90000002.IP JOIN :#channelname - strcpy(pszName, vWords[0].c_str() + 1); - if (strchr(pszName, '!')) - *strchr(pszName, '!') = '\0'; - printf("GOT JOIN: [%s] ", pszName); - } - - if (pszName[0] == 'u') - { - CAddress addr; - if (DecodeAddress(pszName, addr)) - { - CAddrDB addrdb; - if (AddAddress(addrdb, addr)) - printf("new "); - addr.print(); - } - else - { - printf("decode failed\n"); - } - } - } - - fRestartIRCSeed = false; - closesocket(hSocket); - } -} - - - - - - - - - - -#ifdef TEST -int main(int argc, char *argv[]) -{ - WSADATA wsadata; - if (WSAStartup(MAKEWORD(2,2), &wsadata) != NO_ERROR) - { - printf("Error at WSAStartup()\n"); - return false; - } - - ThreadIRCSeed(NULL); - - WSACleanup(); - return 0; -} -#endif diff --git a/src/irc.h b/src/irc.h deleted file mode 100644 index 1ae4ffc..0000000 --- a/src/irc.h +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) 2009 Satoshi Nakamoto -// Distributed under the MIT/X11 software license, see the accompanying -// file license.txt or http://www.opensource.org/licenses/mit-license.php. - -extern bool RecvLine(SOCKET hSocket, string& strLine); -extern void ThreadIRCSeed(void* parg); -extern bool fRestartIRCSeed; diff --git a/src/main.cpp b/src/main.cpp index f3de4d3..aff7bab 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,6 +13,7 @@ #include "sha.h" #include #include +#include #include "btfaddr.h" #include "btftunnel.h" #ifndef _WIN32 @@ -73,7 +74,7 @@ bool fSoloMineTest = false; // /solomine: mine without requiring a peer (local t int fGenerateBitcoins; int64 nTransactionFee = 0; CAddress addrIncoming; -int nMineMode = MINE_SOLO; +int nMineMode = MINE_RELAY; string strParticipantPool; // participant mode: pool .btf address string strPoolName = "Bitflash Pool"; string strPoolDashboardUrl; @@ -88,11 +89,10 @@ static std::string gParticipantStatus; void SetParticipantMiningStatus(const std::string& status) { + // Updates the GUI status string only. Logging is done at each call site + // with full context; duplicating it here just adds noise to the log. std::lock_guard lk(gParticipantStatusMutex); - if (gParticipantStatus == status) - return; gParticipantStatus = status; - printf("PoolParticipantMiner: status -> %s\n", status.c_str()); } std::string GetParticipantMiningStatus() @@ -159,7 +159,7 @@ bool AddToWallet(const CWalletTx& wtxIn) wtx.nTimeReceived = GetAdjustedTime(); //// debug print - printf("AddToWallet %s %s\n", wtxIn.GetHash().ToString().substr(0,6).c_str(), fInsertedNew ? "new" : "update"); + if (LogAcceptsCategory("net")) printf("AddToWallet %s %s\n", wtxIn.GetHash().ToString().substr(0,6).c_str(), fInsertedNew ? "new" : "update"); if (!fInsertedNew) { @@ -372,7 +372,7 @@ int CMerkleTx::SetMerkleBranch(const CBlock* pblock) { vMerkleBranch.clear(); nIndex = -1; - printf("ERROR: SetMerkleBranch() : couldn't find tx in block\n"); + if (LogAcceptsCategory("net")) printf("ERROR: SetMerkleBranch() : couldn't find tx in block\n"); return 0; } @@ -433,7 +433,7 @@ void CWalletTx::AddSupportingTransactions(CTxDB& txdb) } else { - printf("ERROR: AddSupportingTransactions() : unsupported transaction\n"); + if (LogAcceptsCategory("net")) printf("ERROR: AddSupportingTransactions() : unsupported transaction\n"); continue; } @@ -519,7 +519,7 @@ bool CTransaction::AcceptTransaction(CTxDB& txdb, bool fCheckInputs, bool* pfMis { if (ptxOld) { - printf("mapTransaction.erase(%s) replacing with new version\n", ptxOld->GetHash().ToString().c_str()); + if (LogAcceptsCategory("net")) printf("mapTransaction.erase(%s) replacing with new version\n", ptxOld->GetHash().ToString().c_str()); mapTransactions.erase(ptxOld->GetHash()); } AddToMemoryPool(); @@ -530,7 +530,7 @@ bool CTransaction::AcceptTransaction(CTxDB& txdb, bool fCheckInputs, bool* pfMis if (ptxOld) EraseFromWallet(ptxOld->GetHash()); - printf("AcceptTransaction(): accepted %s\n", hash.ToString().substr(0,6).c_str()); + if (LogAcceptsCategory("net")) printf("AcceptTransaction(): accepted %s\n", hash.ToString().substr(0,6).c_str()); return true; } @@ -669,7 +669,7 @@ void CWalletTx::RelayWalletTransaction(CTxDB& txdb) uint256 hash = GetHash(); if (!txdb.ContainsTx(hash)) { - printf("Relaying wtx %s\n", hash.ToString().substr(0,6).c_str()); + if (LogAcceptsCategory("net")) printf("Relaying wtx %s\n", hash.ToString().substr(0,6).c_str()); RelayMessage(CInv(MSG_TX, hash), (CTransaction)*this); } } @@ -683,7 +683,7 @@ void RelayWalletTransactions() nLastTime = GetTime(); // Rebroadcast any of our txes that aren't in a block yet - printf("RelayWalletTransactions()\n"); + if (LogAcceptsCategory("net")) printf("RelayWalletTransactions()\n"); CTxDB txdb("r"); CRITICAL_BLOCK(cs_mapWallet) { @@ -764,7 +764,7 @@ unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast) // Limit adjustment step unsigned int nActualTimespan = pindexLast->nTime - pindexFirst->nTime; - printf(" nActualTimespan = %d before bounds\n", nActualTimespan); + if (LogAcceptsCategory("net")) printf(" nActualTimespan = %d before bounds\n", nActualTimespan); if (nActualTimespan < nTargetTimespan/4) nActualTimespan = nTargetTimespan/4; if (nActualTimespan > nTargetTimespan*4) @@ -780,10 +780,7 @@ unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast) bnNew = bnProofOfWorkLimit; /// debug print - printf("\n\n\nGetNextWorkRequired RETARGET *****\n"); - printf("nTargetTimespan = %d nActualTimespan = %d\n", nTargetTimespan, nActualTimespan); - printf("Before: %08x %s\n", pindexLast->nBits, CBigNum().SetCompact(pindexLast->nBits).getuint256().ToString().c_str()); - printf("After: %08x %s\n", bnNew.GetCompact(), bnNew.getuint256().ToString().c_str()); + if (LogAcceptsCategory("net")) { printf("\n\n\nGetNextWorkRequired RETARGET *****\n"); printf("nTargetTimespan = %d nActualTimespan = %d\n", nTargetTimespan, nActualTimespan); printf("Before: %08x %s\n", pindexLast->nBits, CBigNum().SetCompact(pindexLast->nBits).getuint256().ToString().c_str()); printf("After: %08x %s\n", bnNew.GetCompact(), bnNew.getuint256().ToString().c_str()); } return bnNew.GetCompact(); } @@ -1037,7 +1034,7 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex) bool Reorganize(CTxDB& txdb, CBlockIndex* pindexNew) { - printf("*** REORGANIZE ***\n"); + LogPrint("net", "*** REORGANIZE ***\n"); // Find the fork CBlockIndex* pfork = pindexBest; @@ -1712,7 +1709,7 @@ bool ProcessMessages(CNode* pfrom) CDataStream& vRecv = pfrom->vRecv; if (vRecv.empty()) return true; - printf("ProcessMessages(%d bytes)\n", vRecv.size()); + LogPrint("net", "ProcessMessages(%d bytes)\n", vRecv.size()); // // Message format @@ -1730,13 +1727,13 @@ bool ProcessMessages(CNode* pfrom) { if (vRecv.size() > sizeof(CMessageHeader)) { - printf("\n\nPROCESSMESSAGE MESSAGESTART NOT FOUND\n\n"); + if (LogAcceptsCategory("net")) printf("\n\nPROCESSMESSAGE MESSAGESTART NOT FOUND\n\n"); vRecv.erase(vRecv.begin(), vRecv.end() - sizeof(CMessageHeader)); } break; } if (pstart - vRecv.begin() > 0) - printf("\n\nPROCESSMESSAGE SKIPPED %d BYTES\n\n", pstart - vRecv.begin()); + if (LogAcceptsCategory("net")) printf("\n\nPROCESSMESSAGE SKIPPED %d BYTES\n\n", pstart - vRecv.begin()); vRecv.erase(vRecv.begin(), pstart); // Read header @@ -1744,7 +1741,7 @@ bool ProcessMessages(CNode* pfrom) vRecv >> hdr; if (!hdr.IsValid()) { - printf("\n\nPROCESSMESSAGE: ERRORS IN HEADER %s\n\n\n", hdr.GetCommand().c_str()); + if (LogAcceptsCategory("net")) printf("\n\nPROCESSMESSAGE: ERRORS IN HEADER %s\n\n\n", hdr.GetCommand().c_str()); continue; } string strCommand = hdr.GetCommand(); @@ -1755,7 +1752,7 @@ bool ProcessMessages(CNode* pfrom) { // Rewind and wait for rest of message ///// need a mechanism to give up waiting for overlong message size error - printf("MESSAGE-BREAK 2\n"); + if (LogAcceptsCategory("net")) printf("MESSAGE-BREAK 2\n"); vRecv.insert(vRecv.begin(), BEGIN(hdr), END(hdr)); Sleep(100); break; @@ -1776,7 +1773,7 @@ bool ProcessMessages(CNode* pfrom) } CATCH_PRINT_EXCEPTION("ProcessMessage()") if (!fRet) - printf("ProcessMessage(%s, %d bytes) from %s to %s FAILED\n", strCommand.c_str(), nMessageSize, pfrom->addr.ToString().c_str(), addrLocalHost.ToString().c_str()); + if (LogAcceptsCategory("net")) printf("ProcessMessage(%s, %d bytes) from %s to %s FAILED\n", strCommand.c_str(), nMessageSize, pfrom->addr.ToString().c_str(), addrLocalHost.ToString().c_str()); } vRecv.Compact(); @@ -1789,13 +1786,10 @@ bool ProcessMessages(CNode* pfrom) bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) { static map > mapReuseKey; - printf("received: %-12s (%d bytes) ", strCommand.c_str(), vRecv.size()); - for (int i = 0; i < min(vRecv.size(), (unsigned int)25); i++) - printf("%02x ", vRecv[i] & 0xff); - printf("\n"); + if (LogAcceptsCategory("net")) { printf("received: %-12s (%d bytes) ", strCommand.c_str(), vRecv.size()); for (int i = 0; i < min(vRecv.size(), (unsigned int)25); i++) printf("%02x ", vRecv[i] & 0xff); printf("\n"); } if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0) { - printf("dropmessages DROPPING RECV MESSAGE\n"); + if (LogAcceptsCategory("net")) printf("dropmessages DROPPING RECV MESSAGE\n"); return true; } @@ -1833,7 +1827,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) pfrom->PushMessage("getblocks", CBlockLocator(pindexBest), uint256(0)); } - printf("version addrMe = %s\n", addrMe.ToString().c_str()); + if (LogAcceptsCategory("net")) printf("version addrMe = %s\n", addrMe.ToString().c_str()); } @@ -1844,30 +1838,6 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) } - else if (strCommand == "addr") - { - vector vAddr; - vRecv >> vAddr; - - // Store the new addresses - CAddrDB addrdb; - foreach(const CAddress& addr, vAddr) - { - if (fShutdown) - return true; - if (AddAddress(addrdb, addr)) - { - // Put on lists to send to other nodes - pfrom->setAddrKnown.insert(addr); - CRITICAL_BLOCK(cs_vNodes) - foreach(CNode* pnode, vNodes) - if (!pnode->setAddrKnown.count(addr)) - pnode->vAddrToSend.push_back(addr); - } - } - } - - else if (strCommand == "inv") { vector vInv; @@ -1881,7 +1851,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) pfrom->AddInventoryKnown(inv); bool fAlreadyHave = AlreadyHave(txdb, inv); - printf(" got inventory: %s %s\n", inv.ToString().c_str(), fAlreadyHave ? "have" : "new"); + if (LogAcceptsCategory("net")) printf(" got inventory: %s %s\n", inv.ToString().c_str(), fAlreadyHave ? "have" : "new"); if (!fAlreadyHave) pfrom->AskFor(inv); @@ -1900,7 +1870,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) { if (fShutdown) return true; - printf("received getdata for: %s\n", inv.ToString().c_str()); + if (LogAcceptsCategory("net")) printf("received getdata for: %s\n", inv.ToString().c_str()); if (inv.type == MSG_BLOCK) { @@ -1940,12 +1910,12 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) // Send the rest of the chain if (pindex) pindex = pindex->pnext; - printf("getblocks %d to %s\n", (pindex ? pindex->nHeight : -1), hashStop.ToString().substr(0,14).c_str()); + if (LogAcceptsCategory("net")) printf("getblocks %d to %s\n", (pindex ? pindex->nHeight : -1), hashStop.ToString().substr(0,14).c_str()); for (; pindex; pindex = pindex->pnext) { if (pindex->GetBlockHash() == hashStop) { - printf(" getblocks stopping at %d %s\n", pindex->nHeight, pindex->GetBlockHash().ToString().substr(0,14).c_str()); + if (LogAcceptsCategory("net")) printf(" getblocks stopping at %d %s\n", pindex->nHeight, pindex->GetBlockHash().ToString().substr(0,14).c_str()); break; } @@ -1997,7 +1967,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) if (tx.AcceptTransaction(true)) { - printf(" accepted orphan tx %s\n", inv.hash.ToString().substr(0,6).c_str()); + if (LogAcceptsCategory("net")) printf(" accepted orphan tx %s\n", inv.hash.ToString().substr(0,6).c_str()); AddToWalletIfMine(tx, NULL); RelayMessage(inv, vMsg); mapAlreadyAskedFor.erase(inv); @@ -2011,7 +1981,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) } else if (fMissingInputs) { - printf("storing orphan tx %s\n", inv.hash.ToString().substr(0,6).c_str()); + if (LogAcceptsCategory("net")) printf("storing orphan tx %s\n", inv.hash.ToString().substr(0,6).c_str()); AddOrphanTx(vMsg); } } @@ -2041,7 +2011,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) vRecv >> *pblock; //// debug print - printf("received block:\n"); pblock->print(); + if (LogAcceptsCategory("net")) { printf("received block:\n"); pblock->print(); } CInv inv(MSG_BLOCK, pblock->GetHash()); pfrom->AddInventoryKnown(inv); @@ -2051,25 +2021,6 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) } - else if (strCommand == "getaddr") - { - pfrom->vAddrToSend.clear(); - //// need to expand the time range if not enough found - int64 nSince = GetAdjustedTime() - 60 * 60; // in the last hour - CRITICAL_BLOCK(cs_mapAddresses) - { - foreach(const PAIRTYPE(vector, CAddress)& item, mapAddresses) - { - if (fShutdown) - return true; - const CAddress& addr = item.second; - if (addr.nTime > nSince) - pfrom->vAddrToSend.push_back(addr); - } - } - } - - else if (strCommand == "checkorder") { uint256 hashReply; @@ -2134,12 +2085,12 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) else { // Ignore unknown commands for extensibility - printf("ProcessMessage(%s) : Ignored unknown message\n", strCommand.c_str()); + if (LogAcceptsCategory("net")) printf("ProcessMessage(%s) : Ignored unknown message\n", strCommand.c_str()); } if (!vRecv.empty()) - printf("ProcessMessage(%s) : %d extra bytes\n", strCommand.c_str(), vRecv.size()); + if (LogAcceptsCategory("net")) printf("ProcessMessage(%s) : %d extra bytes\n", strCommand.c_str(), vRecv.size()); return true; } @@ -2162,19 +2113,6 @@ bool SendMessages(CNode* pto) return true; - // - // Message: addr - // - vector vAddrToSend; - vAddrToSend.reserve(pto->vAddrToSend.size()); - foreach(const CAddress& addr, pto->vAddrToSend) - if (!pto->setAddrKnown.count(addr)) - vAddrToSend.push_back(addr); - pto->vAddrToSend.clear(); - if (!vAddrToSend.empty()) - pto->PushMessage("addr", vAddrToSend); - - // // Message: inventory // @@ -2204,7 +2142,7 @@ bool SendMessages(CNode* pto) while (!pto->mapAskFor.empty() && (*pto->mapAskFor.begin()).first <= nNow) { const CInv& inv = (*pto->mapAskFor.begin()).second; - printf("sending getdata: %s\n", inv.ToString().c_str()); + if (LogAcceptsCategory("net")) printf("sending getdata: %s\n", inv.ToString().c_str()); if (!AlreadyHave(txdb, inv)) vAskFor.push_back(inv); pto->mapAskFor.erase(pto->mapAskFor.begin()); @@ -2280,13 +2218,21 @@ void BlockSHA256(const void* pin, unsigned int nBlocks, void* pout) // --------------------------------------------------------------------------- -// Participant pool miner — connects to an external Stratum server, receives +// Participant pool miner -- connects to an external Stratum server, receives // RandomX jobs, hashes with the local CPU, and submits shares. // Called from BitcoinMiner() when nMineMode == MINE_PARTICIPANT. // --------------------------------------------------------------------------- using json = nlohmann::json; + +// --------------------------------------------------------------------------- +// Participant miner -- connects to a pool over a .btf tunnel, runs Stratum, +// hashes with RandomX, submits shares. Called from BitcoinMiner(). +// --------------------------------------------------------------------------- + +using json = nlohmann::json; + static std::string ToHexStr(const void* p, size_t n) { const unsigned char* b = (const unsigned char*)p; @@ -2300,10 +2246,10 @@ static std::vector FromHexStr(const std::string& s) { std::vector v; for (size_t i = 0; i+1 < s.size(); i += 2) { - auto h=[](char c)->int{ - if(c>='0'&&c<='9')return c-'0'; - if(c>='a'&&c<='f')return c-'a'+10; - if(c>='A'&&c<='F')return c-'A'+10; + auto h = [](char c)->int{ + if(c>='0'&&c<='9') return c-'0'; + if(c>='a'&&c<='f') return c-'a'+10; + if(c>='A'&&c<='F') return c-'A'+10; return 0; }; v.push_back((unsigned char)((h(s[i])<<4)|h(s[i+1]))); @@ -2313,14 +2259,13 @@ static std::vector FromHexStr(const std::string& s) static void UpdateParticipantHashRate(uint64 hashesSinceSample, int64 sampleStart) { - int64 now = GetTime(); - int64 elapsed = now - sampleStart; - if (elapsed <= 0) - elapsed = 1; - double rate = (double)hashesSinceSample / (double)elapsed; - gParticipantHashRateX1000.store((uint64)(rate * 1000.0)); + int64 elapsed = GetTime() - sampleStart; + if (elapsed <= 0) elapsed = 1; + gParticipantHashRateX1000.store( + (uint64)((double)hashesSinceSample / (double)elapsed * 1000.0)); } +// Blocking send of a JSON line to the pool. static bool StratumSendLine(SOCKET s, const json& j) { std::string line = j.dump() + "\n"; @@ -2333,38 +2278,59 @@ static bool StratumSendLine(SOCKET s, const json& j) return true; } -static bool StratumRecvLine(SOCKET s, std::string& buf, std::string& line) +// Non-blocking recv into carry buffer. Returns true + fills line when a +// complete newline-terminated message is ready. Returns false in two cases: +// disconnected=true -- socket closed by peer; caller must stop +// disconnected=false -- no complete line yet; caller should select() and retry +static bool StratumRecvLine(SOCKET s, std::string& buf, + std::string& line, bool& disconnected) { - while (true) { - size_t pos = buf.find('\n'); - if (pos != std::string::npos) { - line = buf.substr(0, pos); - if (!line.empty() && line.back()=='\r') line.pop_back(); - buf = buf.substr(pos+1); - return true; - } - char tmp[4096]; - int r = recv(s, tmp, sizeof(tmp)-1, 0); - if (r <= 0) return false; - tmp[r] = 0; - buf += tmp; + disconnected = false; + char tmp[4096]; +#ifdef _WIN32 + u_long avail = 0; + ioctlsocket(s, FIONREAD, &avail); + if (avail > 0) { + int r = recv(s, tmp, (int)std::min((u_long)(sizeof(tmp)-1), avail), 0); + if (r > 0) { tmp[r] = 0; buf += tmp; } + else if (r == 0) { disconnected = true; return false; } + } +#else + { + int r = recv(s, tmp, sizeof(tmp)-1, MSG_DONTWAIT); + if (r > 0) { tmp[r] = 0; buf += tmp; } + else if (r == 0) { disconnected = true; return false; } + // r < 0: EAGAIN -- no data right now } +#endif + size_t pos = buf.find('\n'); + if (pos == std::string::npos) return false; + line = buf.substr(0, pos); + if (!line.empty() && line.back() == '\r') line.pop_back(); + buf = buf.substr(pos + 1); + return true; } +// How long to wait for a mining.notify after authorize before giving up +static const int64 STUCK_NO_JOB_TIMEOUT_SECS = 120; + static bool PoolParticipantMiner() { - printf("PoolParticipantMiner: connecting to %s\n", strParticipantPool.c_str()); - SetParticipantMiningStatus("resolving pool address"); - + // ------------------------------------------------------------------ + // Step 1: resolve .btf address -> meeting node + // ------------------------------------------------------------------ if (strParticipantPool.empty()) { - printf("PoolParticipantMiner: no pool selected\n"); + LogPrint("worker", "[worker] no pool selected\n"); SetParticipantMiningStatus("no pool selected"); return false; } + LogPrint("worker", "[worker] connecting to pool %s\n", strParticipantPool.c_str()); + SetParticipantMiningStatus("resolving pool address"); + unsigned char target_pubkey[32]; if (!btf::ParseAddress(strParticipantPool, target_pubkey)) { - printf("PoolParticipantMiner: invalid .btf address %s\n", strParticipantPool.c_str()); + LogPrint("worker", "[worker] invalid .btf address: %s\n", strParticipantPool.c_str()); SetParticipantMiningStatus("invalid pool address"); return false; } @@ -2372,35 +2338,48 @@ static bool PoolParticipantMiner() std::string meetingHostPort; unsigned char service_enc_pub[32]; if (!BtfResolve(strParticipantPool, meetingHostPort, service_enc_pub)) { - printf("PoolParticipantMiner: failed to resolve %s\n", strParticipantPool.c_str()); + LogPrint("worker", "[worker] failed to resolve pool %s" + " (no Nostr descriptor found -- pool may be offline or relays down)\n", + strParticipantPool.c_str()); SetParticipantMiningStatus("failed to resolve pool"); return false; } + LogPrint("worker", "[worker] pool resolved to meeting node %s\n", meetingHostPort.c_str()); size_t colon = meetingHostPort.rfind(':'); if (colon == std::string::npos) { - printf("PoolParticipantMiner: bad meeting node %s\n", meetingHostPort.c_str()); + LogPrint("worker", "[worker] bad meeting node format '%s'\n", meetingHostPort.c_str()); SetParticipantMiningStatus("bad pool endpoint"); return false; } std::string host = meetingHostPort.substr(0, colon); int port = atoi(meetingHostPort.substr(colon + 1).c_str()); if (port <= 0 || port > 65535) { - printf("PoolParticipantMiner: bad meeting port %d\n", port); + LogPrint("worker", "[worker] bad meeting port %d in '%s'\n", + port, meetingHostPort.c_str()); SetParticipantMiningStatus("bad pool port"); return false; } - btf_socket_t s = btf::BtfClientTunnel(host.c_str(), (unsigned short)port, target_pubkey, service_enc_pub); + // ------------------------------------------------------------------ + // Step 2: open encrypted tunnel to pool + // ------------------------------------------------------------------ + SetParticipantMiningStatus("opening tunnel"); + btf_socket_t s = btf::BtfClientTunnel(host.c_str(), (unsigned short)port, + target_pubkey, service_enc_pub); if (s == INVALID_SOCKET) { - printf("PoolParticipantMiner: tunnel connect failed via %s\n", meetingHostPort.c_str()); + LogPrint("worker", "[worker] tunnel connect FAILED via %s" + " (relay unreachable or pool not listening)\n", + meetingHostPort.c_str()); SetParticipantMiningStatus("tunnel connect failed"); return false; } - printf("PoolParticipantMiner: connected via %s\n", meetingHostPort.c_str()); - SetParticipantMiningStatus("tunnel connected"); + LogPrint("worker", "[worker] tunnel open via %s\n", meetingHostPort.c_str()); + SetParticipantMiningStatus("tunnel open"); - // Use our wallet address as mining username so payouts go to our wallet + // ------------------------------------------------------------------ + // Step 3: determine our payout address (mining username) + // ------------------------------------------------------------------ std::string username; { std::vector vchPubKey; @@ -2408,102 +2387,260 @@ static bool PoolParticipantMiner() username = PubKeyToAddress(vchPubKey); } if (username.empty()) username = BtfLocalAddress(); - if (username.empty()) username = "unknown"; - + if (username.empty()) { + LogPrint("worker", "[worker] WARNING: could not determine wallet address" + " -- shares will not pay out\n"); + username = "unknown"; + } + LogPrint("worker", "[worker] payout address: %s\n", username.c_str()); + + // ------------------------------------------------------------------ + // Step 4: build the fast (~2 GB dataset) RandomX mode if not already + // built, then start our VM. Without this, RandomXCreateMinerVM() falls + // back to "light" mode (cache only) forever, which is dramatically + // slower per hash than fast mode -- it recomputes dataset items on the + // fly for every single hash instead of a direct memory lookup. That + // alone can turn a "few seconds per share" pool into "many minutes per + // share" regardless of how easy the share target is. + // + // This MUST happen before we send mining.subscribe/authorize, not + // after: it's a synchronous, single-threaded-per-call build that can + // take anywhere from several seconds to a couple of minutes depending + // on core count. If it runs after the handshake messages are sent, + // the pool's replies (subscribe ack, authorize ack, set_difficulty, + // and the first job) all land in the OS socket buffer while we're + // not reading -- nothing is lost, but we sit there looking (and + // logging) like we're "stuck waiting for pool response" for the + // entire build duration, when really we're just not listening yet. + // Building the dataset first means that by the time we say + // "subscribing", we're genuinely ready to read the reply the instant + // it arrives. + // ------------------------------------------------------------------ + if (!RandomXFastReady()) { + SetParticipantMiningStatus("building RandomX dataset (~2GB, one-time)"); + unsigned int nThreads = std::thread::hardware_concurrency(); + RandomXInitDataset(nThreads > 0 ? (int)nThreads : 1); + } + void* rxvm = RandomXCreateMinerVM(); + if (!rxvm) { + LogPrint("worker", "[worker] failed to create RandomX VM\n"); + SetParticipantMiningStatus("RandomX init failed"); + closesocket(s); return false; + } + LogPrint("worker", "[worker] RandomX VM ready (%s)\n", + RandomXFastReady() ? "fast 2GB" : "light 256MB"); + + // ------------------------------------------------------------------ + // Step 5: Stratum subscribe + authorize. The socket is read from + // immediately after this, in the main loop below, so nothing sits + // unread behind a blocking dataset build anymore. + // ------------------------------------------------------------------ std::string buf; int msgId = 1; std::set pendingSubmitIds; - // Subscribe SetParticipantMiningStatus("subscribing"); - if (!StratumSendLine(s, json{{"id",msgId++},{"method","mining.subscribe"},{"params",json::array()}})) - { SetParticipantMiningStatus("subscribe failed"); closesocket(s); return false; } + LogPrint("worker", "[worker->pool] sending mining.subscribe\n"); + if (!StratumSendLine(s, json{{"id",msgId++},{"method","mining.subscribe"}, + {"params",json::array()}})) { + LogPrint("worker", "[worker] subscribe send FAILED -- pool dropped connection\n"); + SetParticipantMiningStatus("subscribe send failed"); + RandomXDestroyMinerVM(rxvm); + closesocket(s); return false; + } - // Authorize SetParticipantMiningStatus("authorizing"); + LogPrint("worker", "[worker->pool] sending mining.authorize as %s\n", username.c_str()); if (!StratumSendLine(s, json{{"id",msgId++},{"method","mining.authorize"}, - {"params",json::array({username,"x"})}})) - { SetParticipantMiningStatus("authorize failed"); closesocket(s); return false; } + {"params",json::array({username,"x"})}})) { + LogPrint("worker", "[worker] authorize send FAILED -- pool dropped connection\n"); + SetParticipantMiningStatus("authorize send failed"); + RandomXDestroyMinerVM(rxvm); + closesocket(s); return false; + } - // Job state + // ------------------------------------------------------------------ + // Step 6: main loop -- receive messages, hash, submit shares + // ------------------------------------------------------------------ std::string jobId; - unsigned char header[80]; + unsigned char header[80] = {}; uint256 target; - bool haveJob = false; + double currentDifficulty = 1.0; // updated by mining.set_difficulty from pool + bool haveJob = false; bool gotSubscribeAck = false; bool gotAuthorizeAck = false; - int64 authWaitStart = 0; - unsigned int nNonce = 1; - uint64 hashesSinceSample = 0; - int64 sampleStart = GetTime(); + int64 authWaitStart = 0; + int64 lastStuckLog = 0; + unsigned int nNonce = 1; + uint64 hashesSinceSample = 0; + uint64 sessionSharesSent = 0; // this connection only + uint64 sessionSharesAccepted = 0; + int64 sampleStart = GetTime(); - void* rxvm = RandomXCreateMinerVM(); - if (!rxvm) { SetParticipantMiningStatus("failed to start RandomX miner"); closesocket(s); return false; } - printf("PoolParticipantMiner: RandomX VM ready (%s)\n", - RandomXFastReady() ? "fast 2GB" : "light 256MB"); - SetParticipantMiningStatus("authorized; asking pool for work"); + SetParticipantMiningStatus("waiting for pool response"); while (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { - // Non-blocking receive check + // -- Receive: check buffer first, then select -- { - fd_set fds; FD_ZERO(&fds); FD_SET(s, &fds); - struct timeval tv={0,50000}; // 50ms - if (select((int)s+1, &fds, NULL, NULL, &tv) > 0) { + bool haveBuffered = buf.find('\n') != std::string::npos; + bool readable = false; + if (!haveBuffered) { + fd_set fds; FD_ZERO(&fds); FD_SET(s, &fds); + struct timeval tv = {0, 50000}; // 50ms + readable = select((int)s+1, &fds, NULL, NULL, &tv) > 0; + } + + if (haveBuffered || readable) { std::string line; - if (!StratumRecvLine(s, buf, line)) break; - try { - json msg = json::parse(line); - if (msg.contains("id") && msg["id"].is_number_integer() && msg.contains("result")) { - int id = msg["id"].get(); - if (id == 1 && msg["result"].is_boolean() && msg["result"].get()) { - gotSubscribeAck = true; - SetParticipantMiningStatus("subscribed; authorizing"); - } else if (id == 2 && msg["result"].is_boolean()) { - gotAuthorizeAck = msg["result"].get(); - authWaitStart = GetTime(); - SetParticipantMiningStatus(gotAuthorizeAck ? "authorized; asking pool for work" : "authorization rejected"); + bool disc = false; + while (StratumRecvLine(s, buf, line, disc)) { + json msg; + try { msg = json::parse(line); } catch (...) { + LogPrint("worker", "[pool->worker] JSON parse error: %.80s\n", + line.c_str()); + continue; + } + + // -- Response to our subscribe (id=1) -- + if (msg.contains("id") && msg["id"].is_number_integer() + && msg["id"].get() == 1 + && msg.contains("result") && !msg["result"].is_null()) { + gotSubscribeAck = true; + LogPrint("worker", "[pool->worker] subscribe ack received\n"); + SetParticipantMiningStatus("subscribed; waiting for authorize ack"); + } + + // -- Response to our authorize (id=2) -- + else if (msg.contains("id") && msg["id"].is_number_integer() + && msg["id"].get() == 2 + && msg.contains("result")) { + bool ok = msg["result"].is_boolean() && msg["result"].get(); + authWaitStart = GetTime(); + if (ok) { + gotAuthorizeAck = true; + LogPrint("worker", "[pool->worker] authorize OK -- waiting for job\n"); + SetParticipantMiningStatus("authorized; waiting for job"); + } else { + std::string reason = msg.value("error", json("unspecified")).is_string() + ? msg["error"].get() : "unspecified"; + LogPrint("worker", "[pool->worker] authorize REJECTED: %s\n", + reason.c_str()); + SetParticipantMiningStatus("authorization rejected"); + // Don't break -- pool might still send a job (some pools + // reject bad addresses but still send work) } - if (pendingSubmitIds.erase(id) > 0) { - if (msg["result"].is_boolean() && msg["result"].get()) { + } + + // -- Share submission response -- + else if (msg.contains("id") && msg["id"].is_number_integer()) { + int rid = msg["id"].get(); + if (pendingSubmitIds.erase(rid) > 0) { + bool accepted = msg.contains("result") + && msg["result"].is_boolean() + && msg["result"].get(); + if (accepted) { gParticipantSharesAccepted.fetch_add(1); + sessionSharesAccepted++; + LogPrint("worker", "[pool->worker] share id=%d ACCEPTED\n", rid); SetParticipantMiningStatus("share accepted"); } else { - SetParticipantMiningStatus("share rejected"); + std::string reason = (msg.contains("error") + && msg["error"].is_string()) + ? msg["error"].get() : "unspecified"; + LogPrint("worker", "[pool->worker] share id=%d REJECTED: %s\n", + rid, reason.c_str()); + SetParticipantMiningStatus("share rejected: " + reason); } } } - std::string method = msg.value("method",""); - if (method == "mining.notify") { - auto& p = msg["params"]; - jobId = p[0].get(); - // p[1] = full 80-byte header hex - auto hdrBytes = FromHexStr(p[1].get()); - if (hdrBytes.size() >= 80) - memcpy(header, hdrBytes.data(), 80); - // p[3] = share target hex (little-endian uint256) - auto tgtBytes = FromHexStr(p[3].get()); - if (tgtBytes.size() >= 32) - memcpy(&target, tgtBytes.data(), 32); - haveJob = true; - nNonce = 1; - SetParticipantMiningStatus("job received; hashing"); - printf("PoolParticipantMiner: new job %s\n", jobId.c_str()); - } else if (method == "mining.set_difficulty") { - // acknowledged, we use the target from notify + + // -- Server notification -- + else { + std::string method = msg.value("method", ""); + + if (method == "mining.notify") { + auto& p = msg["params"]; + if (!p.is_array() || p.size() < 4) { + LogPrint("worker", "[pool->worker] malformed mining.notify" + " (params size=%zu)\n", p.is_array() ? p.size() : 0); + } else { + std::string newJobId = p[0].get(); + auto hdrBytes = FromHexStr(p[1].get()); + auto tgtBytes = FromHexStr(p[3].get()); + + if (hdrBytes.size() < 80) { + LogPrint("worker", "[pool->worker] mining.notify:" + " header too short (%zu bytes)\n", + hdrBytes.size()); + } else if (tgtBytes.size() < 32) { + LogPrint("worker", "[pool->worker] mining.notify:" + " target too short (%zu bytes)\n", + tgtBytes.size()); + } else { + jobId = newJobId; + memcpy(header, hdrBytes.data(), 80); + memcpy(&target, tgtBytes.data(), 32); + haveJob = true; + nNonce = 1; + LogPrint("worker", "[pool->worker] new job %s received\n", + jobId.c_str()); + SetParticipantMiningStatus("hashing job " + jobId); + } + } + } else if (method == "mining.set_difficulty") { + // Pool adjusts our share difficulty via vardiff. + // Apply it immediately so we start hashing at the new target. + if (msg.contains("params") && msg["params"].is_array() + && msg["params"].size() > 0 + && msg["params"][0].is_number()) { + double newDiff = msg["params"][0].get(); + if (newDiff > 0.0) { + bool changed = (newDiff != currentDifficulty); + currentDifficulty = newDiff; + LogPrint("worker", "[pool->worker] set_difficulty %.6f%s\n", + newDiff, changed ? " (updated)" : " (initial)"); + } + } else { + LogPrint("worker", "[pool->worker] set_difficulty" + " received (no valid params)\n"); + } + } else if (!method.empty()) { + LogPrint("worker", "[pool->worker] unknown server method: %s\n", + method.c_str()); + } } - } catch(...) {} + } + + if (disc) { + LogPrint("worker", "[worker] pool closed the connection" + " (gotSubscribeAck=%d gotAuthorizeAck=%d haveJob=%d)\n", + gotSubscribeAck, gotAuthorizeAck, haveJob); + break; + } } } - if (gotAuthorizeAck && !haveJob && authWaitStart > 0 && GetTime() - authWaitStart > 10) { - SetParticipantMiningStatus("authorized; waiting for pool notify"); - authWaitStart = 0; + // -- Stuck watchdog: authorized but no job arriving -- + if (gotAuthorizeAck && !haveJob && authWaitStart > 0) { + int64 waited = GetTime() - authWaitStart; + if (waited > 10 && GetTime() - lastStuckLog >= 15) { + LogPrint("worker", "[worker] authorized but no job after %llds" + " -- pool tunnel is live, waiting on mining.notify\n", + (long long)waited); + lastStuckLog = GetTime(); + } + if (waited > STUCK_NO_JOB_TIMEOUT_SECS) { + LogPrint("worker", "[worker] no job received after %llds -- disconnecting" + " and reconnecting\n", (long long)STUCK_NO_JOB_TIMEOUT_SECS); + SetParticipantMiningStatus("timeout waiting for job; reconnecting"); + break; + } } if (!haveJob) { Sleep(100); continue; } - // Hash one nonce + // -- Hash one nonce -- memcpy(header+76, &nNonce, 4); uint256 hash = RandomXHashWithVM(rxvm, header, 80); hashesSinceSample++; @@ -2512,38 +2649,59 @@ static bool PoolParticipantMiner() UpdateParticipantHashRate(hashesSinceSample, sampleStart); if (hash <= target) { - printf("PoolParticipantMiner: share found! nonce=%u\n", nNonce); + LogPrint("worker", "[worker] share found! nonce=%u job=%s difficulty=%.4f\n", + nNonce, jobId.c_str(), currentDifficulty); SetParticipantMiningStatus("share found; submitting"); int submitId = msgId++; pendingSubmitIds.insert(submitId); gParticipantSharesSent.fetch_add(1); + sessionSharesSent++; json submit = {{"id",submitId},{"method","mining.submit"}, - {"params",json::array({username, jobId, - ToHexStr(&nNonce,4)})}}; - if (!StratumSendLine(s, submit)) break; + {"params",json::array({username, jobId, ToHexStr(&nNonce,4)})}}; + if (!StratumSendLine(s, submit)) { + LogPrint("worker", "[worker] share submit send FAILED" + " -- pool connection lost\n"); + break; + } } nNonce++; - if (nNonce == 0) { haveJob = false; } // exhausted, wait for new job + if (nNonce == 0) { + LogPrint("worker", "[worker] nonce space exhausted for job %s" + " -- waiting for new job\n", jobId.c_str()); + haveJob = false; + } } + // ------------------------------------------------------------------ + // Cleanup + // ------------------------------------------------------------------ RandomXDestroyMinerVM(rxvm); closesocket(s); UpdateParticipantHashRate(hashesSinceSample, sampleStart); - SetParticipantMiningStatus("participant miner stopped"); - printf("PoolParticipantMiner: disconnected\n"); + SetParticipantMiningStatus("disconnected"); + LogPrint("worker", "[worker] disconnected from pool" + " (hashes this session: %llu, shares sent: %llu, accepted: %llu)\n", + (unsigned long long)hashesSinceSample, + (unsigned long long)sessionSharesSent, + (unsigned long long)sessionSharesAccepted); return true; } bool BitcoinMiner() { + // Relay mode: never mines, just relays/syncs. (Shouldn't normally get here + // since the GUI hides Start Mining for Relay, but guard it regardless.) + if (nMineMode == MINE_RELAY) + return true; + // Participant mode: connect to external pool instead of mining solo/operator if (nMineMode == MINE_PARTICIPANT) { while (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { PoolParticipantMiner(); if (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { - printf("PoolParticipantMiner: reconnecting in 10s\n"); + LogPrint("worker", "PoolParticipantMiner: reconnecting in 10s\n"); for (int i = 0; i < 10 && !fShutdown; i++) Sleep(1000); } } @@ -2554,9 +2712,16 @@ bool BitcoinMiner() if (nMineMode == MINE_OPERATOR) return true; - printf("BitcoinMiner started\n"); + LogPrint("net", "BitcoinMiner started\n"); SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_LOWEST); + // Build the fast (~2 GB dataset) RandomX mode once, up front, so we're + // not stuck mining in the much slower light/cache-only mode forever. + if (!RandomXFastReady()) { + unsigned int nThreads = std::thread::hardware_concurrency(); + RandomXInitDataset(nThreads > 0 ? (int)nThreads : 1); + } + CKey key; key.MakeNewKey(); CBigNum bnExtraNonce = 0; @@ -2639,7 +2804,7 @@ bool BitcoinMiner() } pblock->nBits = nBits; pblock->vtx[0].vout[0].nValue = pblock->GetBlockValue(nFees); - printf("\n\nRunning BitcoinMiner with %d transactions in block\n", pblock->vtx.size()); + if (LogAcceptsCategory("net")) printf("\n\nRunning BitcoinMiner with %d transactions in block\n", pblock->vtx.size()); // @@ -2658,11 +2823,11 @@ bool BitcoinMiner() void* rxvm = RandomXCreateMinerVM(); if (!rxvm) { - printf("BitcoinMiner: failed to create RandomX VM\n"); + LogPrint("net", "BitcoinMiner: failed to create RandomX VM\n"); Sleep(1000); continue; } - printf("BitcoinMiner: mining with RandomX (%s)\n", + LogPrint("net", "BitcoinMiner: mining with RandomX (%s)\n", RandomXFastReady() ? "fast 2GB" : "light 256MB"); unsigned int nStart = GetTime(); @@ -2675,9 +2840,9 @@ bool BitcoinMiner() if (hash <= hashTarget) { //// debug print - printf("BitcoinMiner:\n"); - printf("RandomX proof-of-work found \n powhash: %s \ntarget: %s\n", hash.GetHex().c_str(), hashTarget.GetHex().c_str()); - pblock->print(); + if (LogAcceptsCategory("net")) printf("BitcoinMiner:\n"); + if (LogAcceptsCategory("net")) printf("RandomX proof-of-work found \n powhash: %s \ntarget: %s\n", hash.GetHex().c_str(), hashTarget.GetHex().c_str()); + if (LogAcceptsCategory("net")) pblock->print(); SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_NORMAL); CRITICAL_BLOCK(cs_main) @@ -2692,7 +2857,7 @@ bool BitcoinMiner() // Process this block the same as if we had received it from another node if (!ProcessBlock(NULL, pblock.release())) - printf("ERROR in BitcoinMiner, ProcessBlock, block not accepted\n"); + LogPrint("net", "ERROR in BitcoinMiner, ProcessBlock, block not accepted\n"); } SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_LOWEST); @@ -2860,11 +3025,11 @@ bool SelectCoins(int64 nTargetValue, set& setCoinsRet) setCoinsRet.insert(vValue[i].second); //// debug print - printf("SelectCoins() best subset: "); + if (LogAcceptsCategory("net")) printf("SelectCoins() best subset: "); for (int i = 0; i < vValue.size(); i++) if (vfBest[i]) - printf("%s ", FormatMoney(vValue[i].first).c_str()); - printf("total %s\n", FormatMoney(nBest).c_str()); + if (LogAcceptsCategory("net")) printf("%s ", FormatMoney(vValue[i].first).c_str()); + if (LogAcceptsCategory("net")) printf("total %s\n", FormatMoney(nBest).c_str()); } return true; @@ -2953,6 +3118,34 @@ bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew, in return true; } +// Undo what CommitTransactionSpent did: unmark the inputs it marked spent +// and erase the half-committed wallet entry. Used when a transaction is +// accepted into the wallet but then rejected by the mempool/chain, so we +// don't end up with a wallet that thinks money is spent that never left. +void RollbackTransactionSpent(const CWalletTx& wtxNew) +{ + CRITICAL_BLOCK(cs_main) + CRITICAL_BLOCK(cs_mapWallet) + { + set setCoins; + foreach(const CTxIn& txin, wtxNew.vin) + { + map::iterator mi = mapWallet.find(txin.prevout.hash); + if (mi != mapWallet.end()) + setCoins.insert(&mi->second); + } + foreach(CWalletTx* pcoin, setCoins) + { + pcoin->fSpent = false; + pcoin->WriteToDisk(); + vWalletUpdated.push_back(make_pair(pcoin->GetHash(), false)); + } + + EraseFromWallet(wtxNew.GetHash()); + } + MainFrameRepaint(); +} + // Call after CreateTransaction unless you want to abort bool CommitTransactionSpent(const CWalletTx& wtxNew) { @@ -3003,13 +3196,17 @@ bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew) return error("SendMoney() : Error finalizing transaction"); } - printf("SendMoney: %s\n", wtxNew.GetHash().ToString().substr(0,6).c_str()); + if (LogAcceptsCategory("net")) printf("SendMoney: %s\n", wtxNew.GetHash().ToString().substr(0,6).c_str()); // Broadcast if (!wtxNew.AcceptTransaction()) { - // This must not fail. The transaction has already been signed and recorded. - throw runtime_error("SendMoney() : wtxNew.AcceptTransaction() failed\n"); + // The transaction was already signed and committed to the wallet + // above, but the chain/mempool just rejected it (e.g. a raced + // input already spent by another transaction). Unwind the + // wallet state we just wrote instead of crashing the app. + RollbackTransactionSpent(wtxNew); + return error("SendMoney() : wtxNew.AcceptTransaction() failed"); } wtxNew.RelayWalletTransaction(); } diff --git a/src/main.h b/src/main.h index 3d7d6dd..8bb8609 100644 --- a/src/main.h +++ b/src/main.h @@ -48,13 +48,17 @@ extern CBlockIndex* pindexBest; extern unsigned int nTransactionsUpdated; extern string strSetDataDir; extern int nDropMessagesTest; -// Mining mode — persisted in wallet.dat -// MINE_SOLO (0): mine to own wallet, no pool server -// MINE_OPERATOR (1): run pool server, mine to own wallet, distribute to miners -// MINE_PARTICIPANT (2): connect to external pool, submit shares, receive payouts -#define MINE_SOLO 0 -#define MINE_OPERATOR 1 -#define MINE_PARTICIPANT 2 +// Mining mode -- persisted in wallet.dat +// Mining mode -- decided fresh every launch (CLI flags, else default). Never +// restored from wallet.dat; see LoadWallet() in db.cpp. +// MINE_RELAY (0): default. Node runs/syncs, never mines, no pool server. +// MINE_SOLO (1): mine to own wallet, no pool server +// MINE_OPERATOR (2): run pool server, mine to own wallet, distribute to miners +// MINE_PARTICIPANT (3): connect to external pool, submit shares, receive payouts +#define MINE_RELAY 0 +#define MINE_SOLO 1 +#define MINE_OPERATOR 2 +#define MINE_PARTICIPANT 3 extern int nMineMode; extern string strParticipantPool; // participant: pool .btf address extern string strPoolName; // operator: announced pool name @@ -78,6 +82,7 @@ struct PoolWorkerStatView uint64 totalShares; uint64 roundShares; int64 lastSeen; + double hashRate; // estimated H/s, derived from share difficulty + submit rate }; // Settings @@ -102,11 +107,11 @@ void RelayWalletTransactions(); bool LoadBlockIndex(bool fAllowNew=true); void PrintBlockTree(); bool BitcoinMiner(); -void ThreadRPCServer(void* parg); // rpc.cpp — .btf pool server +void ThreadRPCServer(void* parg); // rpc.cpp -- .btf pool server void GetParticipantMiningStats(uint64& sharesSent, uint64& sharesAccepted, double& hashRate); void SetParticipantMiningStatus(const std::string& status); std::string GetParticipantMiningStatus(); -void GetPoolOperatorStats(int& authorizedMiners, int& blocksFound, uint64& roundShares); +void GetPoolOperatorStats(int& authorizedMiners, int& blocksFound, uint64& roundShares, double& totalHashRate); void GetPoolWorkerStats(std::vector& out); void GetPendingPayouts(std::vector& out); bool ProcessMessages(CNode* pfrom); diff --git a/src/main_gui.cpp b/src/main_gui.cpp index b7377da..842b0d5 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -1,4 +1,4 @@ -// Bitflash entry point — starts node threads then runs GUI (or headless). +// Bitflash entry point -- starts node threads then runs GUI (or headless). #include "headers_core.h" #ifndef _WIN32 @@ -126,8 +126,6 @@ int main(int argc, char* argv[]) ParseStartupArguments(argc, argv); - printf("Loading addresses...\n"); - if (!LoadAddresses()) printf("Warning: error loading addr.dat\n"); printf("Loading block index...\n"); string strErrors; if (!LoadBlockIndex()) { fprintf(stderr,"LoadBlockIndex failed\n"); return 1; } diff --git a/src/makefile.linux b/src/makefile.linux deleted file mode 100644 index 9e63f7d..0000000 --- a/src/makefile.linux +++ /dev/null @@ -1,55 +0,0 @@ -# Bitflash Linux build — GUI wallet + pool server + daemon mode. -# Requires: bash build-linux.sh (installs deps, builds secp256k1 + RandomX) -# Result: src/bitflash -# -# ./bitflash — GUI wallet -# ./bitflash /nogui /gen — headless daemon (same binary, no window) - -CXX ?= g++ - -SECP_INC ?= /usr/local/include -SECP_LIB ?= /usr/local/lib -RANDOMX_DIR ?= $(HOME)/RandomX - -WX_CFLAGS := $(shell wx-config --cxxflags) -WX_LIBS := $(shell wx-config --libs) - -CXXFLAGS = -std=gnu++14 -O2 -w -fpermissive -pthread \ - -DSECP256K1_STATIC \ - -I$(SECP_INC) -I$(RANDOMX_DIR)/src \ - -I/usr/include/nlohmann \ - $(WX_CFLAGS) - -LIBS = -L$(SECP_LIB) -lsecp256k1 \ - -L$(RANDOMX_DIR)/build -lrandomx \ - -lsodium -lssl -lcrypto -ldb_cxx -lpthread \ - -lboost_system \ - $(WX_LIBS) - -HEADERS = headers.h util.h main.h serialize.h uint256.h key.h bignum.h script.h \ - db.h base58.h net.h irc.h nostr.h randomx_pow.h btfaddr.h btfchan.h \ - btfrv.h btftunnel.h market.h compat.h uibase.h ui.h - -OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o obj/market.o \ - obj/sha.o obj/irc.o obj/nostr.o obj/randomx_pow.o \ - obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o \ - obj/rpc.o obj/uibase.o obj/ui.o - -all: bitflash - -obj/%.o: %.cpp $(HEADERS) - @mkdir -p obj - $(CXX) -c $(CXXFLAGS) -o $@ $< - -obj/sha.o: sha.cpp sha.h - @mkdir -p obj - $(CXX) -c $(CXXFLAGS) -O3 -o $@ $< - -bitflash: $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $(OBJS) $(LIBS) - @echo "Built: bitflash" - @echo " GUI: ./bitflash" - @echo " Daemon: ./bitflash /nogui /gen" - -clean: - rm -rf obj bitflash diff --git a/src/makefile.mingw b/src/makefile.mingw index d381653..7d35d92 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -28,13 +28,13 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ - obj/market.o obj/sha.o obj/irc.o obj/nostr.o obj/randomx_pow.o \ + obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ - key.h bignum.h script.h db.h base58.h net.h irc.h nostr.h \ + key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h all: bitflash.exe diff --git a/src/makefile.vc b/src/makefile.vc deleted file mode 100644 index 60dca78..0000000 --- a/src/makefile.vc +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) 2009 Satoshi Nakamoto -# Distributed under the MIT/X11 software license, see the accompanying -# file license.txt or http://www.opensource.org/licenses/mit-license.php. - - -!IF "$(BUILD)" != "debug" && "$(BUILD)" != "release" -BUILD=debug -!ENDIF -!IF "$(BUILD)" == "debug" -D=d -DEBUGFLAGS=/Zi /Od /D__WXDEBUG__ -!ENDIF - - - -INCLUDEPATHS=/I"/boost" /I"/DB/build_windows" /I"/OpenSSL/include" /I"/wxWidgets/lib/vc_lib/mswd" /I"/wxWidgets/include" -LIBPATHS=/LIBPATH:"/DB/build_windows/$(BUILD)" /LIBPATH:"/OpenSSL/out" /LIBPATH:"/wxWidgets/lib/vc_lib" -LIBS= \ - libdb47s$(D).lib \ - libeay32.lib \ - wxmsw28$(D)_richtext.lib wxmsw28$(D)_html.lib wxmsw28$(D)_core.lib wxbase28$(D).lib wxtiff$(D).lib wxjpeg$(D).lib wxpng$(D).lib wxzlib$(D).lib wxregex$(D).lib wxexpat$(D).lib \ - kernel32.lib user32.lib gdi32.lib comdlg32.lib winspool.lib winmm.lib shell32.lib comctl32.lib ole32.lib oleaut32.lib uuid.lib rpcrt4.lib advapi32.lib ws2_32.lib -WXDEFS=/DWIN32 /D__WXMSW__ /D_WINDOWS /DNOPCH -CFLAGS=/c /nologo /Ob0 /MD$(D) /EHsc /GR /Zm300 /YX /Fpobj/headers.pch $(DEBUGFLAGS) $(WXDEFS) $(INCLUDEPATHS) -HEADERS=headers.h util.h main.h serialize.h uint256.h key.h bignum.h script.h db.h base58.h - - - -all: bitcoin.exe - - -obj\util.obj: util.cpp $(HEADERS) - cl $(CFLAGS) /Fo$@ %s - -obj\script.obj: script.cpp $(HEADERS) - cl $(CFLAGS) /Fo$@ %s - -obj\db.obj: db.cpp $(HEADERS) market.h - cl $(CFLAGS) /Fo$@ %s - -obj\net.obj: net.cpp $(HEADERS) net.h - cl $(CFLAGS) /Fo$@ %s - -obj\main.obj: main.cpp $(HEADERS) net.h market.h - cl $(CFLAGS) /Fo$@ %s - -obj\market.obj: market.cpp $(HEADERS) market.h - cl $(CFLAGS) /Fo$@ %s - -obj\ui.obj: ui.cpp $(HEADERS) net.h uibase.h ui.h market.h - cl $(CFLAGS) /Fo$@ %s - -obj\uibase.obj: uibase.cpp uibase.h - cl $(CFLAGS) /Fo$@ %s - -obj\sha.obj: sha.cpp sha.h - cl $(CFLAGS) /O2 /Fo$@ %s - -obj\irc.obj: irc.cpp $(HEADERS) - cl $(CFLAGS) /Fo$@ %s - -obj\ui.res: ui.rc rc/bitcoin.ico rc/check.ico rc/send16.bmp rc/send16mask.bmp rc/send16masknoshadow.bmp rc/send20.bmp rc/send20mask.bmp rc/addressbook16.bmp rc/addressbook16mask.bmp rc/addressbook20.bmp rc/addressbook20mask.bmp - rc $(INCLUDEPATHS) $(WXDEFS) /Fo$@ %s - - - -OBJS=obj\util.obj obj\script.obj obj\db.obj obj\net.obj obj\main.obj obj\market.obj \ - obj\ui.obj obj\uibase.obj obj\sha.obj obj\irc.obj obj\ui.res - -bitcoin.exe: $(OBJS) - -kill /f bitcoin.exe & sleep 1 - link /nologo /DEBUG /SUBSYSTEM:WINDOWS /OUT:$@ $(LIBPATHS) $** $(LIBS) - -clean: - -del /Q obj\* - -del *.ilk - -del *.pdb diff --git a/src/net.cpp b/src/net.cpp index bdfb9c3..ab07f94 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -12,7 +12,6 @@ void ThreadMessageHandler2(void* parg); void ThreadSocketHandler2(void* parg); -void ThreadOpenConnections2(void* parg); @@ -32,8 +31,6 @@ bool fShutdown = false; array vfThreadRunning; vector vNodes; CCriticalSection cs_vNodes; -map, CAddress> mapAddresses; -CCriticalSection cs_mapAddresses; map mapRelay; deque > vRelayExpiration; CCriticalSection cs_mapRelay; @@ -41,61 +38,6 @@ map mapAlreadyAskedFor; string strBtfConnect; // .btf peer to keep connected to (from /connectbtf) - -CAddress addrProxy; - -bool ConnectSocket(const CAddress& addrConnect, SOCKET& hSocketRet) -{ - hSocketRet = INVALID_SOCKET; - - SOCKET hSocket = socket(AF_INET, SOCK_STREAM, 0); - if (hSocket == INVALID_SOCKET) - return false; - - bool fRoutable = !(addrConnect.GetByte(3) == 10 || (addrConnect.GetByte(3) == 192 && addrConnect.GetByte(2) == 168)); - bool fProxy = (addrProxy.ip && fRoutable); - struct sockaddr_in sockaddr = (fProxy ? addrProxy.GetSockAddr() : addrConnect.GetSockAddr()); - - if (connect(hSocket, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) == SOCKET_ERROR) - { - closesocket(hSocket); - return false; - } - - if (fProxy) - { - printf("Proxy connecting to %s\n", addrConnect.ToString().c_str()); - char pszSocks4IP[] = "\4\1\0\0\0\0\0\0user"; - memcpy(pszSocks4IP + 2, &addrConnect.port, 2); - memcpy(pszSocks4IP + 4, &addrConnect.ip, 4); - char* pszSocks4 = pszSocks4IP; - int nSize = sizeof(pszSocks4IP); - - int ret = send(hSocket, pszSocks4, nSize, 0); - if (ret != nSize) - { - closesocket(hSocket); - return error("Error sending to proxy\n"); - } - char pchRet[8]; - if (recv(hSocket, pchRet, 8, 0) != 8) - { - closesocket(hSocket); - return error("Error reading proxy response\n"); - } - if (pchRet[1] != 0x5a) - { - closesocket(hSocket); - return error("Proxy returned error %d\n", pchRet[1]); - } - printf("Proxy connection established %s\n", addrConnect.ToString().c_str()); - } - - hSocketRet = hSocket; - return true; -} - - bool GetMyExternalIP(unsigned int& ipRet) { // Try several plain-text IP echo services in order. @@ -178,38 +120,6 @@ bool GetMyExternalIP(unsigned int& ipRet) -bool AddAddress(CAddrDB& addrdb, const CAddress& addr) -{ - if (!addr.IsRoutable()) - return false; - if (addr.ip == addrLocalHost.ip) - return false; - CRITICAL_BLOCK(cs_mapAddresses) - { - map, CAddress>::iterator it = mapAddresses.find(addr.GetKey()); - if (it == mapAddresses.end()) - { - // New address - mapAddresses.insert(make_pair(addr.GetKey(), addr)); - addrdb.WriteAddress(addr); - return true; - } - else - { - CAddress& addrFound = (*it).second; - if ((addrFound.nServices | addr.nServices) != addrFound.nServices) - { - // Services have been added - addrFound.nServices |= addr.nServices; - addrdb.WriteAddress(addrFound); - return true; - } - } - } - return false; -} - - @@ -330,64 +240,6 @@ CNode* FindNode(unsigned int ip) return NULL; } -CNode* FindNode(CAddress addr) -{ - CRITICAL_BLOCK(cs_vNodes) - { - foreach(CNode* pnode, vNodes) - if (pnode->addr == addr) - return (pnode); - } - return NULL; -} - -CNode* ConnectNode(CAddress addrConnect, int64 nTimeout) -{ - if (addrConnect.ip == addrLocalHost.ip) - return NULL; - - // Look for an existing connection - CNode* pnode = FindNode(addrConnect.ip); - if (pnode) - { - if (nTimeout != 0) - pnode->AddRef(nTimeout); - else - pnode->AddRef(); - return pnode; - } - - if (fDebug) - printf("trying %s\n", addrConnect.ToString().c_str()); - - // Connect - SOCKET hSocket; - if (ConnectSocket(addrConnect, hSocket)) - { - if (fDebug) - printf("connected %s\n", addrConnect.ToString().c_str()); - - // Add node - CNode* pnode = new CNode(hSocket, addrConnect, false); - if (nTimeout != 0) - pnode->AddRef(nTimeout); - else - pnode->AddRef(); - CRITICAL_BLOCK(cs_vNodes) - vNodes.push_back(pnode); - - CRITICAL_BLOCK(cs_mapAddresses) - mapAddresses[addrConnect.GetKey()].nLastFailed = 0; - return pnode; - } - else - { - CRITICAL_BLOCK(cs_mapAddresses) - mapAddresses[addrConnect.GetKey()].nLastFailed = GetTime(); - return NULL; - } -} - // Synthetic marker address for a tunneled `.btf` peer. The real endpoint is // unknown by design (the tunnel hides it), so tag the CNode with a non-routable // 10.x.x.x address (fails IsRoutable, so it is never gossiped as a real peer) @@ -401,20 +253,15 @@ static CAddress BtfMarkerAddr(const unsigned char b5[5]) return CAddress(ip, port, nLocalServices); } -// Connect to a peer by its `.btf` address: resolve the self-certified -// descriptor over Nostr, then tunnel through its meeting node with the -// end-to-end channel. Neither side ever learns the other's IP. -CNode* ConnectNodeBtf(const string& strBtfAddr) +// Shared tail: given a peer's pubkey and already-resolved rendezvous +// coordinates, open the tunnel and register the CNode. Used by both +// ConnectNodeBtf (which resolves the descriptor itself) and +// ConnectNodeBtfResolved (which takes an already-resolved descriptor, so it +// makes no Nostr relay calls at all -- see nostr.cpp's BtfResolveMany for why +// that matters when dialing many candidates in parallel). +static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char pk[32], + const string& strMeeting, const unsigned char enc_pub[32]) { - unsigned char pk[32]; - if (!btf::ParseAddress(strBtfAddr, pk)) - { - printf("ConnectNodeBtf: invalid address %s\n", strBtfAddr.c_str()); - return NULL; - } - if (BtfLocalAddress() == strBtfAddr) - return NULL; // ourselves - CAddress addr = BtfMarkerAddr(pk); CNode* pnode = FindNode(addr.ip); if (pnode) @@ -423,17 +270,6 @@ CNode* ConnectNodeBtf(const string& strBtfAddr) return pnode; } - if (fDebug) - printf("trying %s\n", strBtfAddr.c_str()); - - string strMeeting; - unsigned char enc_pub[32]; - if (!BtfResolve(strBtfAddr, strMeeting, enc_pub)) - { - if (fDebug) - printf("ConnectNodeBtf: could not resolve a descriptor for %s\n", strBtfAddr.c_str()); - return NULL; - } size_t colon = strMeeting.rfind(':'); if (colon == string::npos) return NULL; @@ -446,12 +282,12 @@ CNode* ConnectNodeBtf(const string& strBtfAddr) if (hSocket == INVALID_SOCKET) { if (fDebug) - printf("ConnectNodeBtf: tunnel to %s via %s failed\n", strBtfAddr.c_str(), strMeeting.c_str()); + LogPrint("net", "ConnectNodeBtf: tunnel to %s via %s failed\n", strBtfAddr.c_str(), strMeeting.c_str()); return NULL; } if (fDebug) - printf("connected %s via rendezvous %s\n", strBtfAddr.c_str(), strMeeting.c_str()); + LogPrint("net", "connected %s via rendezvous %s\n", strBtfAddr.c_str(), strMeeting.c_str()); // Add node pnode = new CNode(hSocket, addr, false); @@ -461,6 +297,53 @@ CNode* ConnectNodeBtf(const string& strBtfAddr) return pnode; } +// Connect to a peer by its `.btf` address: resolve the self-certified +// descriptor over Nostr, then tunnel through its meeting node with the +// end-to-end channel. Neither side ever learns the other's IP. +CNode* ConnectNodeBtf(const string& strBtfAddr) +{ + unsigned char pk[32]; + if (!btf::ParseAddress(strBtfAddr, pk)) + { + LogPrint("net", "ConnectNodeBtf: invalid address %s\n", strBtfAddr.c_str()); + return NULL; + } + if (BtfLocalAddress() == strBtfAddr) + return NULL; // ourselves + + if (fDebug) + LogPrint("net", "trying %s\n", strBtfAddr.c_str()); + + string strMeeting; + unsigned char enc_pub[32]; + if (!BtfResolve(strBtfAddr, strMeeting, enc_pub)) + { + if (fDebug) + LogPrint("net", "ConnectNodeBtf: could not resolve a descriptor for %s\n", strBtfAddr.c_str()); + return NULL; + } + return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, enc_pub); +} + +// Same as ConnectNodeBtf, but skips the Nostr resolve step because the caller +// already resolved the descriptor (e.g. via BtfResolveMany, batching many +// addresses into one relay round-trip instead of one relay round-trip per +// address). Makes zero relay connections -- only talks to the peer's +// rendezvous meeting node. +CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, + const unsigned char enc_pub[32]) +{ + unsigned char pk[32]; + if (!btf::ParseAddress(strBtfAddr, pk)) + { + LogPrint("net", "ConnectNodeBtf: invalid address %s\n", strBtfAddr.c_str()); + return NULL; + } + if (BtfLocalAddress() == strBtfAddr) + return NULL; // ourselves + return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, enc_pub); +} + // Anonymous inbound listener (this node's `.btf` hidden service). Registers at // the meeting relay and blocks until a client dials our address; each pairing // becomes a normal inbound CNode riding the end-to-end channel, then we @@ -535,7 +418,7 @@ void ThreadBtfAccept(void* parg) RAND_bytes(rnd, sizeof(rnd)); CAddress addr = BtfMarkerAddr(rnd); - printf("accepted .btf connection via rendezvous %s\n", strMeeting.c_str()); + LogPrint("net", "accepted .btf connection via rendezvous %s\n", strMeeting.c_str()); CNode* pnode = new CNode(hSocket, addr, true); pnode->AddRef(); CRITICAL_BLOCK(cs_vNodes) @@ -574,7 +457,7 @@ void ThreadBtfConnect(void* parg) void CNode::Disconnect() { - printf("disconnecting node %s\n", addr.ToString().c_str()); + LogPrint("net", "disconnecting node %s\n", addr.ToString().c_str()); closesocket(hSocket); @@ -653,7 +536,7 @@ void ThreadSocketHandler2(void* parg) if (pnodeExtra->GetRefCount() <= (pnodeExtra->fNetworkNode ? 1 : 0)) { - printf("(%d nodes) disconnecting duplicate: %s\n", vNodes.size(), pnodeExtra->addr.ToString().c_str()); + LogPrint("net", "(%d nodes) disconnecting duplicate: %s\n", vNodes.size(), pnodeExtra->addr.ToString().c_str()); if (pnodeExtra->fNetworkNode && !pnode->fNetworkNode) { pnode->AddRef(); @@ -745,7 +628,7 @@ void ThreadSocketHandler2(void* parg) if (nSelect == SOCKET_ERROR) { int nErr = WSAGetLastError(); - printf("select failed: %d\n", nErr); + LogPrint("net", "select failed: %d\n", nErr); for (int i = 0; i <= hSocketMax; i++) { FD_SET(i, &fdsetRecv); @@ -784,7 +667,7 @@ void ThreadSocketHandler2(void* parg) } else { - printf("accepted connection from %s\n", addr.ToString().c_str()); + LogPrint("net", "accepted connection from %s\n", addr.ToString().c_str()); CNode* pnode = new CNode(hSocket, addr, true); pnode->AddRef(); CRITICAL_BLOCK(cs_vNodes) @@ -823,7 +706,7 @@ void ThreadSocketHandler2(void* parg) { // socket closed gracefully if (!pnode->fDisconnect) - printf("recv: socket closed\n"); + LogPrint("net", "recv: socket closed\n"); pnode->fDisconnect = true; } else if (nBytes < 0) @@ -833,7 +716,7 @@ void ThreadSocketHandler2(void* parg) if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS) { if (!pnode->fDisconnect) - printf("recv failed: %d\n", nErr); + LogPrint("net", "recv failed: %d\n", nErr); pnode->fDisconnect = true; } } @@ -862,7 +745,7 @@ void ThreadSocketHandler2(void* parg) } else { - printf("send error %d\n", nBytes); + LogPrint("net", "send error %d\n", nBytes); if (pnode->ReadyToDisconnect()) pnode->vSend.clear(); } @@ -885,141 +768,6 @@ void ThreadSocketHandler2(void* parg) -void ThreadOpenConnections(void* parg) -{ - IMPLEMENT_RANDOMIZE_STACK(ThreadOpenConnections(parg)); - - loop - { - vfThreadRunning[1] = true; - CheckForShutdown(1); - try - { - ThreadOpenConnections2(parg); - } - CATCH_PRINT_EXCEPTION("ThreadOpenConnections()") - vfThreadRunning[1] = false; - Sleep(5000); - } -} - -void ThreadOpenConnections2(void* parg) -{ - printf("ThreadOpenConnections started\n"); - - // Initiate network connections - const int nMaxConnections = 15; - loop - { - // Wait - vfThreadRunning[1] = false; - Sleep(500); - while (vNodes.size() >= nMaxConnections || vNodes.size() >= mapAddresses.size()) - { - CheckForShutdown(1); - Sleep(2000); - } - vfThreadRunning[1] = true; - CheckForShutdown(1); - - - // Make a list of unique class C's - unsigned char pchIPCMask[4] = { 0xff, 0xff, 0xff, 0x00 }; - unsigned int nIPCMask = *(unsigned int*)pchIPCMask; - vector vIPC; - CRITICAL_BLOCK(cs_mapAddresses) - { - vIPC.reserve(mapAddresses.size()); - unsigned int nPrev = 0; - foreach(const PAIRTYPE(vector, CAddress)& item, mapAddresses) - { - const CAddress& addr = item.second; - if (!addr.IsIPv4()) - continue; - - // Taking advantage of mapAddresses being in sorted order, - // with IPs of the same class C grouped together. - unsigned int ipC = addr.ip & nIPCMask; - if (ipC != nPrev) - vIPC.push_back(nPrev = ipC); - } - } - - // - // The IP selection process is designed to limit vulnerability to address flooding. - // Any class C (a.b.c.?) has an equal chance of being chosen, then an IP is - // chosen within the class C. An attacker may be able to allocate many IPs, but - // they would normally be concentrated in blocks of class C's. They can hog the - // attention within their class C, but not the whole IP address space overall. - // A lone node in a class C will get as much attention as someone holding all 255 - // IPs in another class C. - // - bool fSuccess = false; - int nLimit = vIPC.size(); - while (!fSuccess && nLimit-- > 0) - { - // Choose a random class C - unsigned int ipC = vIPC[GetRand(vIPC.size())]; - - // Organize all addresses in the class C by IP - map > mapIP; - CRITICAL_BLOCK(cs_mapAddresses) - { - unsigned int nDelay = ((30 * 60) << vNodes.size()); - if (nDelay > 8 * 60 * 60) - nDelay = 8 * 60 * 60; - for (map, CAddress>::iterator mi = mapAddresses.lower_bound(CAddress(ipC, 0).GetKey()); - mi != mapAddresses.upper_bound(CAddress(ipC | ~nIPCMask, 0xffff).GetKey()); - ++mi) - { - const CAddress& addr = (*mi).second; - unsigned int nRandomizer = (addr.nLastFailed * addr.ip * 7777U) % 20000; - if (GetTime() - addr.nLastFailed > nDelay * nRandomizer / 10000) - mapIP[addr.ip].push_back(addr); - } - } - if (mapIP.empty()) - break; - - // Choose a random IP in the class C - map >::iterator mi = mapIP.begin(); - advance(mi, GetRand(mapIP.size())); - - // Once we've chosen an IP, we'll try every given port before moving on - foreach(const CAddress& addrConnect, (*mi).second) - { - if (addrConnect.ip == addrLocalHost.ip || !addrConnect.IsIPv4() || FindNode(addrConnect.ip)) - continue; - - CNode* pnode = ConnectNode(addrConnect); - if (!pnode) - continue; - pnode->fNetworkNode = true; - - if (addrLocalHost.IsRoutable()) - { - // Advertise our address - vector vAddrToSend; - vAddrToSend.push_back(addrLocalHost); - pnode->PushMessage("addr", vAddrToSend); - } - - // Get as many addresses as we can - pnode->PushMessage("getaddr"); - - ////// should the one on the receiving end do this too? - // Subscribe our local subscription list - const unsigned int nHops = 0; - for (unsigned int nChannel = 0; nChannel < pnodeLocalHost->vfSubscribe.size(); nChannel++) - if (pnodeLocalHost->vfSubscribe[nChannel]) - pnode->PushMessage("subscribe", nChannel, nHops); - - fSuccess = true; - break; - } - } - } -} @@ -1187,7 +935,7 @@ bool StartNode(string& strError) return false; } - // Get our external IP in a background thread — up to 4 HTTP requests with + // Get our external IP in a background thread -- up to 4 HTTP requests with // 5s timeouts each could otherwise delay startup by 20s on first run. // addrIncoming from a previous session is used immediately as a fallback. if (addrIncoming.ip) @@ -1228,13 +976,6 @@ bool StartNode(string& strError) return false; } - if (_beginthread(ThreadOpenConnections, 0, NULL) == -1) - { - strError = "Error: _beginthread(ThreadOpenConnections) failed"; - printf("%s\n", strError.c_str()); - return false; - } - if (_beginthread(ThreadMessageHandler, 0, NULL) == -1) { strError = "Error: _beginthread(ThreadMessageHandler) failed"; diff --git a/src/net.h b/src/net.h index e732eb3..f028c3b 100644 --- a/src/net.h +++ b/src/net.h @@ -22,12 +22,10 @@ enum -bool ConnectSocket(const CAddress& addrConnect, SOCKET& hSocketRet); bool GetMyExternalIP(unsigned int& ipRet); -bool AddAddress(CAddrDB& addrdb, const CAddress& addr); -CNode* FindNode(unsigned int ip); -CNode* ConnectNode(CAddress addrConnect, int64 nTimeout=0); CNode* ConnectNodeBtf(const string& strBtfAddr); +CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, + const unsigned char enc_pub[32]); void ThreadBtfAccept(void* parg); void ThreadBtfConnect(void* parg); extern string strBtfConnect; @@ -120,7 +118,7 @@ public: // Message size if (nMessageSize > 0x10000000) { - printf("CMessageHeader::IsValid() : nMessageSize too large %u\n", nMessageSize); + if (LogAcceptsCategory("net")) printf("CMessageHeader::IsValid() : nMessageSize too large %u\n", nMessageSize); return false; } @@ -294,7 +292,7 @@ public: void print() const { - printf("CAddress(%s)\n", ToString().c_str()); + if (LogAcceptsCategory("net")) printf("CAddress(%s)\n", ToString().c_str()); } }; @@ -387,7 +385,7 @@ public: void print() const { - printf("CInv(%s)\n", ToString().c_str()); + if (LogAcceptsCategory("net")) printf("CInv(%s)\n", ToString().c_str()); } }; @@ -426,13 +424,10 @@ extern bool fShutdown; extern array vfThreadRunning; extern vector vNodes; extern CCriticalSection cs_vNodes; -extern map, CAddress> mapAddresses; -extern CCriticalSection cs_mapAddresses; extern map mapRelay; extern deque > vRelayExpiration; extern CCriticalSection cs_mapRelay; extern map mapAlreadyAskedFor; -extern CAddress addrProxy; @@ -462,10 +457,6 @@ public: map mapRequests; CCriticalSection cs_mapRequests; - // flood - vector vAddrToSend; - set setAddrKnown; - // inventory based relay set setInventoryKnown; set setInventoryKnown2; @@ -555,7 +546,7 @@ public: // We're using mapAskFor as a priority queue, // the key is the earliest time the request can be sent int64& nRequestTime = mapAlreadyAskedFor[inv]; - printf("askfor %s %lld\n", inv.ToString().c_str(), nRequestTime); + if (LogAcceptsCategory("net")) printf("askfor %s %lld\n", inv.ToString().c_str(), nRequestTime); // Make sure not to reuse time indexes to keep things in the same order int64 nNow = (GetTime() - 1) * 1000000; @@ -576,7 +567,7 @@ public: AbortMessage(); nPushPos = vSend.size(); vSend << CMessageHeader(pszCommand, 0); - printf("sending: %-12s ", pszCommand); + if (LogAcceptsCategory("net")) printf("sending: %-12s ", pszCommand); } void AbortMessage() @@ -586,7 +577,7 @@ public: vSend.resize(nPushPos); nPushPos = -1; LeaveCriticalSection(&cs_vSend); - printf("(aborted)\n"); + if (LogAcceptsCategory("net")) printf("(aborted)\n"); } void EndMessage() @@ -594,7 +585,7 @@ public: extern int nDropMessagesTest; if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0) { - printf("dropmessages DROPPING SEND MESSAGE\n"); + if (LogAcceptsCategory("net")) printf("dropmessages DROPPING SEND MESSAGE\n"); AbortMessage(); return; } @@ -606,10 +597,10 @@ public: unsigned int nSize = vSend.size() - nPushPos - sizeof(CMessageHeader); memcpy((char*)&vSend[nPushPos] + offsetof(CMessageHeader, nMessageSize), &nSize, sizeof(nSize)); - printf("(%d bytes) ", nSize); + if (LogAcceptsCategory("net")) printf("(%d bytes) ", nSize); //for (int i = nPushPos+sizeof(CMessageHeader); i < min(vSend.size(), nPushPos+sizeof(CMessageHeader)+20U); i++) // printf("%02x ", vSend[i] & 0xff); - printf("\n"); + if (LogAcceptsCategory("net")) printf("\n"); nPushPos = -1; LeaveCriticalSection(&cs_vSend); diff --git a/src/nostr.cpp b/src/nostr.cpp index c2ccc4c..009edd1 100644 --- a/src/nostr.cpp +++ b/src/nostr.cpp @@ -45,7 +45,12 @@ static const int64 BTF_POOL_MAX_AGE = 3 * 60; // seconds // Anonymous auto-discovery: how many peers to keep connected via .btf rendezvous // before we stop dialing more, and how many new dials to attempt per cycle. -static const unsigned int BTF_TARGET_CONN = 8; +// Target outbound peer count. With ~100 known .btf peers on the network, +// an Erdos-Renyi random graph stays connected with overwhelming probability +// once average degree exceeds ln(N) (~4.6 here); 24 is ~5x past that +// threshold, so this is plenty *provided* selection isn't fully correlated +// across nodes -- see the diversity slice in ConnectDiscoveredBtfPeers. +static const unsigned int BTF_TARGET_CONN = 24; static const int BTF_DIALS_PER_PASS = 12; // try more peers per Nostr cycle // Nostr descriptor freshness cutoff: skip peers whose descriptor hasn't been @@ -58,11 +63,27 @@ static map g_btfPeers; static CCriticalSection cs_btfPeers; // Public discovery relays. Tunable. +// +// Picked for: free/public (no NIP-42 auth or payment wall), track record of +// high uptime, and *infrastructure* diversity, not just different hostnames. +// relay.damus.io, relay.snort.social and relay.primal.net are all fronted by +// Cloudflare -- different operators, same upstream, so a Cloudflare-side +// incident (or a Cloudflare IP range getting rate-limited/blocked on some +// network) can take out several of them at once even though they look like +// independent relays. Kept only one Cloudflare-fronted relay (damus.io, the +// most established) and filled the rest with relays on different hosts +// (Hetzner, various independents) so no single upstream provider is a +// single point of failure for peer discovery. 7 relays lands inside the +// commonly recommended 5-10 range for redundancy without keeping an +// excessive number of persistent sockets open per node. const char* pszNostrRelays[] = { "wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band", - "wss://relay.snort.social", + "wss://nostr21.com", + "wss://offchain.pub", + "wss://nostr.mom", + "wss://relay.primal.net", }; const int nNostrRelays = ARRAYLEN(pszNostrRelays); @@ -317,6 +338,13 @@ std::string BtfLocalAddress() return g_nostrKey.BtfAddress(); } +int GetDiscoveredPeerCount() +{ + int n = 0; + CRITICAL_BLOCK(cs_btfPeers) { n = (int)g_btfPeers.size(); } + return n; +} + void BtfGetPoolAnnouncements(std::vector& out) { out.clear(); @@ -425,38 +453,55 @@ public: size_t colon = host.find(':'); if (colon != string::npos) { port = atoi(host.substr(colon + 1).c_str()); host = host.substr(0, colon); } - // Resolve and connect (TCP) - struct addrinfo hints, *res = NULL; + // Resolve and connect (TCP). AF_UNSPEC so we get both A and AAAA + // records back and try them in the order the resolver prefers, + // rather than hard-failing whenever the IPv4 path to a relay is + // filtered/rate-limited (increasingly common for CDN-fronted + // relays) while a working IPv6 route exists right there in the + // same getaddrinfo() result. + struct addrinfo hints, *res = NULL, *rp = NULL; memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_INET; + hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; char portstr[16]; sprintf(portstr, "%d", port); if (getaddrinfo(host.c_str(), portstr, &hints, &res) != 0 || !res) return error("Nostr: getaddrinfo %s failed", host.c_str()); - hSocket = socket(res->ai_family, res->ai_socktype, res->ai_protocol); - if (hSocket == INVALID_SOCKET) { freeaddrinfo(res); return false; } - // Socket receive/send timeout: Windows takes a DWORD of milliseconds; - // POSIX takes a struct timeval. + bool fConnected = false; + for (rp = res; rp != NULL; rp = rp->ai_next) + { + hSocket = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (hSocket == INVALID_SOCKET) + continue; + + // Socket receive/send timeout: Windows takes a DWORD of + // milliseconds; POSIX takes a struct timeval. #ifdef _WIN32 - DWORD tv = timeoutSec * 1000; - setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); - setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)); + DWORD tv = timeoutSec * 1000; + setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); + setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)); #else - struct timeval tv; - tv.tv_sec = timeoutSec; - tv.tv_usec = 0; - setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); - setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)); + struct timeval tv; + tv.tv_sec = timeoutSec; + tv.tv_usec = 0; + setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); + setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)); #endif - if (connect(hSocket, res->ai_addr, (int)res->ai_addrlen) != 0) - { - freeaddrinfo(res); - return error("Nostr: connect %s:%d failed", host.c_str(), port); + if (connect(hSocket, rp->ai_addr, (int)rp->ai_addrlen) == 0) + { + fConnected = true; + break; + } + + closesocket(hSocket); + hSocket = INVALID_SOCKET; } freeaddrinfo(res); + if (!fConnected) + return error("Nostr: connect %s:%d failed", host.c_str(), port); + // TLS if (fTls) { @@ -598,6 +643,88 @@ private: }; +// Persistent relay connections. Public relays (relay.damus.io in particular) +// rate-limit new connections per source IP fairly aggressively. Before this, +// every single operation -- self-test, per-pass seeding, batch descriptor +// resolve, pool announce, pool query -- opened its own fresh CWebSocket, +// meaning a single node could fire a dozen-plus brand-new TCP+TLS+WS +// handshakes at the same 4 relays within any given minute (ThreadNostrSeed's +// own loop plus the separate ThreadBtfPoolAnnouncer thread, each reconnecting +// from scratch every cycle). That's exactly what was showing up as constant +// "handshake refused by relay.damus.io" and "connect ... failed" -- not +// necessarily dead relays, just this node getting rate-limited by its own +// reconnect churn. +// +// Now every relay-touching call reuses one long-lived socket per relay via +// this pool, reconnecting only when a socket actually drops. That cuts fresh +// connections from several per minute down to essentially one per relay for +// the life of the process. +struct RelayConn +{ + CWebSocket ws; + CCriticalSection cs; // serializes use of this relay's socket + bool fConnected; + RelayConn() : fConnected(false) {} +}; +static map g_relayPool; +static CCriticalSection cs_relayPool; + +static RelayConn* GetRelayConn(const string& relay) +{ + CRITICAL_BLOCK(cs_relayPool) + { + map::iterator it = g_relayPool.find(relay); + if (it != g_relayPool.end()) + return it->second; + RelayConn* rc = new RelayConn(); + g_relayPool[relay] = rc; + return rc; + } + return NULL; // unreachable +} + +// RAII lease on a relay's pooled connection: connects (if not already +// connected) on construction, and on destruction either keeps the socket +// alive for the next caller (if the exchange succeeded) or closes it so the +// next caller reconnects instead of reusing a socket that may be wedged. +// Caller must set fOk = true once its exchange with the relay completed +// cleanly. +class CRelayLease +{ +public: + RelayConn* rc; + bool fOk; + bool fJustConnected; // true if this lease had to (re)connect, false if reusing a live socket + + CRelayLease(const string& relay, int timeoutSec) : rc(NULL), fOk(false), fJustConnected(false) + { + rc = GetRelayConn(relay); + rc->cs.Enter(); + if (!rc->fConnected) + { + rc->ws.Close(); + rc->fConnected = rc->ws.Connect(relay, timeoutSec); + fJustConnected = rc->fConnected; + } + } + ~CRelayLease() + { + if (!fOk) + { + rc->ws.Close(); + rc->fConnected = false; + } + rc->cs.Leave(); + } + bool Connected() const { return rc->fConnected; } + CWebSocket& Ws() { return rc->ws; } + +private: + CRelayLease(const CRelayLease&); + CRelayLease& operator=(const CRelayLease&); +}; + + // Publish this node's self-certifying `.btf` descriptor: "reach me (this key) // via , encrypting to my x25519 key ". Signed by the node // key so only the address's owner can publish it (no hijacking). @@ -673,11 +800,11 @@ bool BtfPublishPoolAnnouncement(const BtfPoolAnnouncement& ann) { try { - CWebSocket ws; - if (!ws.Connect(pszNostrRelays[i], 10)) + CRelayLease lease(pszNostrRelays[i], 10); + if (!lease.Connected()) continue; - if (PublishPoolAnnouncementOnRelay(ws, g_nostrKey, ann)) - fPublished = true; + if (PublishPoolAnnouncementOnRelay(lease.Ws(), g_nostrKey, ann)) + fPublished = lease.fOk = true; } CATCH_PRINT_EXCEPTION("BtfPublishPoolAnnouncement") } @@ -699,9 +826,10 @@ bool BtfQueryPoolAnnouncement(const std::string& poolBtfAddr, BtfPoolAnnouncemen { try { - CWebSocket ws; - if (!ws.Connect(pszNostrRelays[i], 10)) + CRelayLease lease(pszNostrRelays[i], 10); + if (!lease.Connected()) continue; + CWebSocket& ws = lease.Ws(); json filter = json::object(); filter["authors"] = json::array({ HexEncode(pk, 32) }); @@ -756,6 +884,7 @@ bool BtfQueryPoolAnnouncement(const std::string& poolBtfAddr, BtfPoolAnnouncemen else if (type == "EOSE") break; } + lease.fOk = true; } CATCH_PRINT_EXCEPTION("BtfQueryPoolAnnouncement") } @@ -855,12 +984,16 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, { try { - CWebSocket ws; - if (!ws.Connect(pszNostrRelays[i], 10)) + CRelayLease lease(pszNostrRelays[i], 10); + if (!lease.Connected()) continue; btf::Descriptor d; - if (!ResolveDescriptor(ws, g_nostrKey.ctx, btfAddr, d)) + if (!ResolveDescriptor(lease.Ws(), g_nostrKey.ctx, btfAddr, d)) + { + lease.fOk = true; // exchange completed, just no match continue; + } + lease.fOk = true; if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") continue; // descriptor predates the owner's relay config if (!HexDecode(d.enc, enc_pub, 32)) @@ -873,14 +1006,115 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, return false; } +// Resolve MANY .btf addresses in one pass. A single relay connection carries +// a REQ with every still-unresolved address's pubkey in "authors", instead of +// opening one relay connection per address. This matters because callers like +// ConnectDiscoveredBtfPeers dial many candidates in parallel; without +// batching, each of those dial threads independently walks the same handful +// of public relays on its own, so a pass of N candidates can fire up to +// N * nNostrRelays near-simultaneous connections at the same few relays -- +// enough to get rate-limited or have handshakes refused, which then looks +// like "dead peers" even though the descriptors are perfectly resolvable. +bool BtfResolveMany(const std::vector& btfAddrs, + std::map& out) +{ + if (!EnsureNostrKey() || btfAddrs.empty()) + return false; + + map hexToAddr; // pubkey hex -> .btf addr, for matching events back + set pending; + for (const string& a : btfAddrs) + { + unsigned char pk[32]; + if (!btf::ParseAddress(a, pk)) + continue; + hexToAddr[HexEncode(pk, 32)] = a; + pending.insert(a); + } + if (pending.empty()) + return false; + + for (int i = 0; i < nNostrRelays && !fShutdown && !pending.empty(); i++) + { + try + { + CRelayLease lease(pszNostrRelays[i], 10); + if (!lease.Connected()) + continue; + CWebSocket& ws = lease.Ws(); + + json authors = json::array(); + for (auto& kv : hexToAddr) + if (pending.count(kv.second)) + authors.push_back(kv.first); + if (authors.empty()) + { + lease.fOk = true; + break; + } + + json filter = json::object(); + filter["authors"] = authors; + filter["kinds"] = json::array({ BTF_DESC_KIND }); + filter["limit"] = (int)authors.size(); + json req = json::array({ "REQ", "btf-resolve-many", filter }); + if (!ws.SendText(req.dump())) + continue; + + for (;;) + { + string msg; + if (!ws.RecvText(msg)) + break; + json j; + try { j = json::parse(msg); } catch (...) { continue; } + if (!j.is_array() || j.empty()) continue; + string t = j[0].get(); + if (t == "EVENT" && j.size() >= 3) + { + const json& ev = j[2]; + if (!ev.contains("pubkey") || !ev.contains("content")) + continue; + string hexpk = ev["pubkey"].get(); + auto ai = hexToAddr.find(hexpk); + if (ai == hexToAddr.end() || !pending.count(ai->second)) + continue; // not one we're waiting on (or already resolved) + + unsigned char pk[32]; + if (!HexDecode(hexpk, pk, 32)) + continue; + btf::Descriptor d; + if (!btf::VerifyDescriptor(g_nostrKey.ctx, ev["content"].get(), pk, d)) + continue; + if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") + continue; + BtfResolvedPeer rp; + if (!HexDecode(d.enc, rp.enc_pub, 32)) + continue; + rp.meetingHostPort = d.meeting_node; + out[ai->second] = rp; + pending.erase(ai->second); + } + else if (t == "EOSE") + break; + } + lease.fOk = true; + } + CATCH_PRINT_EXCEPTION("BtfResolveMany") + } + return !out.empty(); +} + // Connect to a relay: publish our .btf descriptor and collect relay data. static bool SeedFromRelay(CNostrKey& key, const string& relay) { - CWebSocket ws; - if (!ws.Connect(relay, 10)) + CRelayLease lease(relay, 10); + if (!lease.Connected()) return false; - printf("Nostr: connected to %s\n", relay.c_str()); + CWebSocket& ws = lease.Ws(); + if (lease.fJustConnected) + LogPrint("nostr", "Nostr: connected to %s\n", relay.c_str()); // Publish our self-certifying .btf descriptor (rendezvous discovery) PublishDescriptor(ws, key); @@ -930,7 +1164,7 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay) else if (createdAt > it->second) it->second = createdAt; // refresh } if (fNew && fDebug) - printf("Nostr: discovered .btf peer %s\n", peerAddr.c_str()); + LogPrint("nostr", "Nostr: discovered .btf peer %s\n", peerAddr.c_str()); if (++nPeers > 500) break; } else if (type == "EOSE") @@ -974,7 +1208,7 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay) if (g_discoveredRelays.size() < BTF_MAX_DISCOVERED_RELAYS) fNew = g_discoveredRelays.insert(r).second; if (fNew && fDebug) - printf("Nostr: discovered relay %s\n", r.c_str()); + LogPrint("nostr", "Nostr: discovered relay %s\n", r.c_str()); } else if (type == "EOSE") break; @@ -1009,12 +1243,15 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay) } } } + lease.fOk = true; return true; } +volatile bool gAnnounceNow = false; + void ThreadBtfPoolAnnouncer(void*) { - printf("ThreadBtfPoolAnnouncer started\n"); + LogPrint("pool", "ThreadBtfPoolAnnouncer started\n"); while (!fShutdown) { if (nMineMode == MINE_OPERATOR && gPoolRunning) @@ -1025,15 +1262,17 @@ void ThreadBtfPoolAnnouncer(void*) ann.dashboardUrl = strPoolDashboardUrl; ann.feePercent = dPoolFeePercent; uint64 roundShares = 0; - GetPoolOperatorStats(ann.connectedMiners, ann.blocksFound, roundShares); - ann.hashRate = 0.0; + double totalHashRate = 0.0; + GetPoolOperatorStats(ann.connectedMiners, ann.blocksFound, roundShares, totalHashRate); + ann.hashRate = totalHashRate; ann.createdAt = GetTime(); if (!ann.btfAddress.empty() && !ann.poolName.empty()) BtfPublishPoolAnnouncement(ann); } - for (int i = 0; i < 60 && !fShutdown; i++) + for (int i = 0; i < 60 && !fShutdown && !gAnnounceNow; i++) Sleep(1000); + gAnnounceNow = false; } } @@ -1042,10 +1281,29 @@ void ThreadBtfPoolAnnouncer(void*) // dead/stale descriptors (e.g. abandoned test identities that linger on the // relays) from consuming the whole per-pass dial budget and starving the live // peers. Backoff grows on repeated failures, capped. +// +// g_btfPeerFails also doubles as the priority signal for dial ordering: a peer +// we've connected to before (present in g_btfPeerEverConnected) is known-good +// and goes first; a peer we've never tried goes next; a peer with a growing +// fail count is known-bad and goes last, worst offenders furthest back. This +// replaces the old behavior where the whole candidate list was shuffled right +// after being sorted by freshness, which made the freshness sort a no-op and +// gave dead peers equal footing with everyone else. static map g_btfPeerBackoff; // addr -> next-try time static map g_btfPeerFails; // addr -> consecutive failures +static set g_btfPeerEverConnected; // addr -> has succeeded at least once static CCriticalSection cs_btfPeerState; +// Backoff cap for repeatedly-dead peers. The old 15-minute cap meant a +// descriptor that has failed many times in a row cycles back into the dial +// pool just as often as a peer we've never tried, wasting dial budget on +// known-dead addresses every pass. Once a peer has failed enough times to +// be "chronic", stretch the cap much further so it consumes budget far less +// often while still getting retried eventually in case it comes back online. +static const int64 BTF_BACKOFF_CAP_SECS = 15 * 60; // 15 min: initial ramp cap +static const int64 BTF_BACKOFF_CAP_SECS_DEAD = 6 * 60 * 60; // 6 hr: cap for chronic failures +static const int BTF_CHRONIC_FAIL_THRESHOLD = 6; // fails before "chronic" cap kicks in + // Dial the .btf peers discovered on the relays, up to a target connection count. // Connections are attempted in parallel so a single slow/dead relay doesn't // stall the entire pass. Each candidate gets its own thread; all threads start @@ -1060,23 +1318,18 @@ static void ConnectDiscoveredBtfPeers() for (auto& kv : g_btfPeers) peersWithAge.push_back({kv.second, kv.first}); } - // Sort newest descriptor first — recently-active nodes are much more - // likely to be online right now. Stale ones stay in the list as fallback. - sort(peersWithAge.begin(), peersWithAge.end(), - [](const pair& a, const pair& b){ return a.first > b.first; }); - int64 cutoff = GetTime() - BTF_PEER_MAX_AGE; int nFresh = 0; - vector peers; + vector> fresh; // (createdAt, addr), newest first for (auto& p : peersWithAge) { if (p.first < cutoff) continue; - peers.push_back(p.second); + fresh.push_back(p); nFresh++; } - if (!peers.empty()) - printf("Nostr: %d fresh peers, %d stale (of %d total)\n", nFresh, nTotal-nFresh, nTotal); + if (!fresh.empty()) + LogPrint("nostr", "Nostr: %d fresh peers, %d stale (of %d total)\n", nFresh, nTotal-nFresh, nTotal); // Drop stale peers so we stop retrying dead descriptors until they refresh. { @@ -1092,42 +1345,164 @@ static void ConnectDiscoveredBtfPeers() } } - // Shuffle so we don't always retry the same dead peers at the front. - for (size_t i = peers.size(); i > 1; i--) - swap(peers[i - 1], peers[(size_t)GetRand(i)]); + // Sort newest descriptor first -- recently-active nodes are much more + // likely to be online right now. + sort(fresh.begin(), fresh.end(), + [](const pair& a, const pair& b){ return a.first > b.first; }); + + // Reorder into tiers: known-good (succeeded before) first, untried next, + // known-bad (has a fail count) last -- worst offenders furthest back. + // Freshness order is preserved *within* each tier, so recency still + // matters, but a peer we know is dead no longer gets equal footing with + // a peer we've never tried or one that's worked before. + vector tierGood, tierUntried, tierBad; + CRITICAL_BLOCK(cs_btfPeerState) + { + for (auto& p : fresh) + { + const string& addr = p.second; + if (g_btfPeerEverConnected.count(addr)) + tierGood.push_back(addr); + else if (g_btfPeerFails.find(addr) == g_btfPeerFails.end()) + tierUntried.push_back(addr); + else + tierBad.push_back(addr); + } + // Within known-bad, put fewer-fails peers ahead of chronic offenders. + sort(tierBad.begin(), tierBad.end(), + [](const string& a, const string& b){ return g_btfPeerFails[a] < g_btfPeerFails[b]; }); + } + + vector peers; + peers.reserve(fresh.size()); + peers.insert(peers.end(), tierGood.begin(), tierGood.end()); + peers.insert(peers.end(), tierUntried.begin(), tierUntried.end()); + peers.insert(peers.end(), tierBad.begin(), tierBad.end()); // Pick candidates for this pass (skip those in backoff, cap at budget). + // + // The tiered order above is deliberately biased toward peers we know + // are good -- that's the right default, since dialing untested/bad + // peers wastes attempts. But if every node on the network applies the + // same bias, everyone converges on the same small set of "known good" + // peers as neighbors: that correlates peer sets across the network, + // which is exactly what standard random-graph connectivity results + // assume you avoid. If that shared hub set has a bad day (e.g. one + // relay outage), many nodes lose the same edges at once instead of + // losing independent, uncorrelated edges. + // + // So: fill most of the pass from the reliable/tiered order, but reserve + // a slice for peers picked at random from the *entire* fresh pool + // (not just the front of the sorted list). Same pattern Bitcoin's own + // peer manager and Kademlia-style DHTs use -- sticky reliable peers + // plus a random sample, not one or the other. int64 now = GetTime(); + int nBudget = (int)BTF_DIALS_PER_PASS; + { + int nRoom = (int)BTF_TARGET_CONN - (int)vNodes.size(); + if (nRoom < nBudget) nBudget = nRoom; + } + if (nBudget < 0) nBudget = 0; + int nRandomSlots = (nBudget > 0) ? max(1, nBudget * 3 / 10) : 0; + int nSortedSlots = nBudget - nRandomSlots; + vector candidates; + set setChosen; CRITICAL_BLOCK(cs_btfPeerState) { + // Reliability slice: walk the tiered (good -> untried -> bad) order. for (const string& addr : peers) { if ((int)vNodes.size() >= (int)BTF_TARGET_CONN) break; - if ((int)candidates.size() >= BTF_DIALS_PER_PASS) break; + if ((int)candidates.size() >= nSortedSlots) break; map::iterator bi = g_btfPeerBackoff.find(addr); if (bi != g_btfPeerBackoff.end() && now < bi->second) continue; candidates.push_back(addr); + setChosen.insert(addr); + } + + // Diversity slice: random sample from the full fresh pool, so the + // dial isn't limited to whatever the tiered sort put up front. + vector pool; + for (auto& p : fresh) + { + const string& addr = p.second; + if (setChosen.count(addr)) + continue; + map::iterator bi = g_btfPeerBackoff.find(addr); + if (bi != g_btfPeerBackoff.end() && now < bi->second) + continue; + pool.push_back(addr); + } + int nWant = min(nRandomSlots, (int)pool.size()); + for (int i = 0; i < nWant; i++) + { + if ((int)vNodes.size() + (int)candidates.size() >= (int)BTF_TARGET_CONN) break; + if ((int)candidates.size() >= nBudget) break; + size_t j = i + (size_t)GetRand((uint64)(pool.size() - i)); + std::swap(pool[i], pool[j]); + candidates.push_back(pool[i]); + setChosen.insert(pool[i]); } } if (candidates.empty()) return; - // Launch one thread per candidate — all connect attempts run in parallel. + // Resolve every candidate's descriptor in one batched pass (a handful of + // relay round-trips total, one REQ per relay covering all candidates) + // instead of each dial thread independently walking the relay list on + // its own. That per-thread approach meant a 12-candidate pass could fire + // up to 12 * nNostrRelays near-simultaneous connections at the same + // handful of public relays -- enough to trip rate limits or get + // handshakes refused, which then looks exactly like "dead peers" even + // though the descriptors were perfectly resolvable. + map resolved; + BtfResolveMany(candidates, resolved); + + // Candidates whose descriptor didn't resolve this pass are recorded as a + // failure right away (no point spawning a tunnel thread with nothing to + // dial), same bookkeeping as a failed connect attempt. + vector toDial; + CRITICAL_BLOCK(cs_btfPeerState) + { + for (const string& addr : candidates) + { + if (resolved.count(addr)) + { + toDial.push_back(addr); + continue; + } + int n = ++g_btfPeerFails[addr]; + int64 cap = (n >= BTF_CHRONIC_FAIL_THRESHOLD) ? BTF_BACKOFF_CAP_SECS_DEAD + : BTF_BACKOFF_CAP_SECS; + int64 secs = 30; + for (int k = 1; k < n && secs < cap; k++) secs *= 2; + if (secs > cap) secs = cap; + g_btfPeerBackoff[addr] = GetTime() + secs; + } + } + if (toDial.empty()) return; + + // Launch one thread per resolved candidate -- all connect attempts run in + // parallel, but purely against each peer's own rendezvous meeting node, + // not the shared Nostr relays, so there's no thundering-herd effect here. // Each thread writes its result back via shared state guarded by cs_btfPeerState. - struct DialCtx { string addr; }; + struct DialCtx { string addr; BtfResolvedPeer rp; }; vector ctxs; - for (const string& addr : candidates) + for (const string& addr : toDial) { - DialCtx* ctx = new DialCtx{addr}; + DialCtx* ctx = new DialCtx{addr, resolved[addr]}; ctxs.push_back(ctx); _beginthread([](void* arg) { DialCtx* ctx = (DialCtx*)arg; string addr = ctx->addr; + string meeting = ctx->rp.meetingHostPort; + unsigned char enc_pub[32]; + memcpy(enc_pub, ctx->rp.enc_pub, 32); delete ctx; - CNode* pnode = ConnectNodeBtf(addr); + CNode* pnode = ConnectNodeBtfResolved(addr, meeting, enc_pub); CRITICAL_BLOCK(cs_btfPeerState) { if (pnode) @@ -1138,13 +1513,16 @@ static void ConnectDiscoveredBtfPeers() pnode->Release(); g_btfPeerBackoff.erase(addr); g_btfPeerFails.erase(addr); + g_btfPeerEverConnected.insert(addr); } else { int n = ++g_btfPeerFails[addr]; + int64 cap = (n >= BTF_CHRONIC_FAIL_THRESHOLD) ? BTF_BACKOFF_CAP_SECS_DEAD + : BTF_BACKOFF_CAP_SECS; int64 secs = 30; - for (int k = 1; k < n && secs < 15*60; k++) secs *= 2; - if (secs > 15*60) secs = 15*60; + for (int k = 1; k < n && secs < cap; k++) secs *= 2; + if (secs > cap) secs = cap; g_btfPeerBackoff[addr] = GetTime() + secs; } } @@ -1160,16 +1538,16 @@ static void ConnectDiscoveredBtfPeers() void ThreadNostrSeed(void* parg) { - printf("ThreadNostrSeed started\n"); + LogPrint("nostr", "ThreadNostrSeed started\n"); if (!EnsureNostrKey()) { - printf("Nostr: failed to initialize secp256k1 key\n"); + LogPrint("nostr", "Nostr: failed to initialize secp256k1 key\n"); return; } CNostrKey& key = g_nostrKey; - printf("Nostr: node pubkey = %s\n", key.PubKeyHex().c_str()); - printf("Nostr: node .btf address = %s\n", key.BtfAddress().c_str()); + LogPrint("nostr", "Nostr: node pubkey = %s\n", key.PubKeyHex().c_str()); + LogPrint("nostr", "Nostr: node .btf address = %s\n", key.BtfAddress().c_str()); // One-time self-test of the rendezvous: publish our descriptor to a relay, // then resolve our OWN .btf address back and verify it self-certifies. Proves @@ -1178,17 +1556,20 @@ void ThreadNostrSeed(void* parg) { try { - CWebSocket ws; - if (!ws.Connect(pszNostrRelays[i], 10)) continue; + CRelayLease lease(pszNostrRelays[i], 10); + if (!lease.Connected()) continue; + CWebSocket& ws = lease.Ws(); PublishDescriptor(ws, key); Sleep(700); // let the relay store the replaceable event btf::Descriptor d; if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d)) { - printf("Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n", + LogPrint("nostr", "Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n", pszNostrRelays[i], d.meeting_node.c_str(), d.enc.substr(0, 16).c_str()); + lease.fOk = true; break; } + lease.fOk = true; } CATCH_PRINT_EXCEPTION("btf self-test") } @@ -1223,8 +1604,9 @@ void ThreadNostrSeed(void* parg) ann.dashboardUrl = strPoolDashboardUrl; ann.feePercent = dPoolFeePercent; uint64 roundShares = 0; - GetPoolOperatorStats(ann.connectedMiners, ann.blocksFound, roundShares); - ann.hashRate = 0.0; + double totalHashRate = 0.0; + GetPoolOperatorStats(ann.connectedMiners, ann.blocksFound, roundShares, totalHashRate); + ann.hashRate = totalHashRate; ann.createdAt = GetTime(); if (!ann.btfAddress.empty() && !ann.poolName.empty()) BtfPublishPoolAnnouncement(ann); diff --git a/src/nostr.h b/src/nostr.h index 63fc095..37b3b05 100644 --- a/src/nostr.h +++ b/src/nostr.h @@ -12,11 +12,17 @@ #include #include +#include // Thread entry point (signature compatible with _beginthread) void ThreadNostrSeed(void* parg); void ThreadBtfPoolAnnouncer(void* parg); +// Set to true to make ThreadBtfPoolAnnouncer publish immediately instead of +// waiting for its ~60s sleep loop (e.g. right after the operator changes pool +// name/fee/dashboard URL in Options). The announcer clears it after publishing. +extern volatile bool gAnnounceNow; + // Public relays used for discovery. Tunable. extern const char* pszNostrRelays[]; extern const int nNostrRelays; @@ -56,6 +62,9 @@ struct BtfPoolAnnouncement } }; +// Number of .btf peers discovered from Nostr relays (includes not-yet-connected). +int GetDiscoveredPeerCount(); + // Current live pool announcements discovered from Nostr relays. void BtfGetPoolAnnouncements(std::vector& out); @@ -78,4 +87,21 @@ std::string BtfLocalAddress(); bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, unsigned char enc_pub[32]); +// Result of resolving one .btf address via BtfResolveMany. +struct BtfResolvedPeer +{ + std::string meetingHostPort; + unsigned char enc_pub[32]; +}; + +// Resolve many .btf addresses at once. Batches all still-unresolved addresses +// into a single Nostr REQ per relay (multiple "authors"), instead of opening +// one relay connection per address like BtfResolve does. Use this whenever +// resolving more than a handful of addresses in the same pass -- e.g. before +// dialing several discovered peers in parallel -- to avoid hammering the +// same few public relays with a burst of simultaneous connections. Returns +// true if at least one address resolved; check `out` for which ones. +bool BtfResolveMany(const std::vector& btfAddrs, + std::map& out); + #endif diff --git a/src/rc/addressbook16.bmp b/src/rc/addressbook16.bmp deleted file mode 100644 index c557691..0000000 Binary files a/src/rc/addressbook16.bmp and /dev/null differ diff --git a/src/rc/addressbook16mask.bmp b/src/rc/addressbook16mask.bmp deleted file mode 100644 index d3a478d..0000000 Binary files a/src/rc/addressbook16mask.bmp and /dev/null differ diff --git a/src/rc/addressbook20.bmp b/src/rc/addressbook20.bmp deleted file mode 100644 index 2b33b22..0000000 Binary files a/src/rc/addressbook20.bmp and /dev/null differ diff --git a/src/rc/addressbook20mask.bmp b/src/rc/addressbook20mask.bmp deleted file mode 100644 index 56ce612..0000000 Binary files a/src/rc/addressbook20mask.bmp and /dev/null differ diff --git a/src/rc/bitcoin_bc_original.ico b/src/rc/bitcoin_bc_original.ico deleted file mode 100644 index 88cc240..0000000 Binary files a/src/rc/bitcoin_bc_original.ico and /dev/null differ diff --git a/src/rc/check.ico b/src/rc/check.ico deleted file mode 100644 index 0c4e6e8..0000000 Binary files a/src/rc/check.ico and /dev/null differ diff --git a/src/rc/send16.bmp b/src/rc/send16.bmp deleted file mode 100644 index 676b5c4..0000000 Binary files a/src/rc/send16.bmp and /dev/null differ diff --git a/src/rc/send16mask.bmp b/src/rc/send16mask.bmp deleted file mode 100644 index 06c747f..0000000 Binary files a/src/rc/send16mask.bmp and /dev/null differ diff --git a/src/rc/send16masknoshadow.bmp b/src/rc/send16masknoshadow.bmp deleted file mode 100644 index faf24e0..0000000 Binary files a/src/rc/send16masknoshadow.bmp and /dev/null differ diff --git a/src/rc/send20.bmp b/src/rc/send20.bmp deleted file mode 100644 index 2b90422..0000000 Binary files a/src/rc/send20.bmp and /dev/null differ diff --git a/src/rc/send20mask.bmp b/src/rc/send20mask.bmp deleted file mode 100644 index f124d0d..0000000 Binary files a/src/rc/send20mask.bmp and /dev/null differ diff --git a/src/rpc.cpp b/src/rpc.cpp index 99d182e..1b529e8 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -1,14 +1,19 @@ // Copyright (c) 2009 Satoshi Nakamoto / Copyright (c) 2026 Bitflash developers // Distributed under the MIT/X11 software license. // -// Bitflash pool server — .btf-only pool listener. -// Miners connect through rendezvous tunnels; no raw IP:port endpoints remain. -// Pending payouts are held until maturity and shown in the GUI. +// Bitflash pool server -- .btf Stratum pool, operator side. // -// Compiles on both Windows (mingw) and Linux unchanged. -// Launched from ui.cpp (GUI) and headless.cpp (node) via ThreadRPCServer(). +// Architecture: +// PoolEventLoop -- single thread, owns all miner fds, all job/share/payout +// state. No locks needed on any of that data. +// AcceptOneFn -- one thread per relay. Blocks on RvServiceRegister + +// BtfServiceWrap, pushes finished socket to gReadyQueue. +// AcceptLoopFn -- spawns AcceptOneFn threads, re-polls relay list every +// 60s so dynamically discovered relays get picked up. +// +// GUI-visible state is snapshotted under gStatsMutex every 2s. +// Accept queue handoff uses gReadyMutex (held for microseconds). -// nlohmann must be included before headers.h because util.h redefines snprintf #pragma push_macro("snprintf") #undef snprintf #include @@ -20,8 +25,6 @@ #endif #include "btftunnel.h" -// POSIX socket headers — already provided by compat.h on Windows, -// and by the OS directly on Linux. #ifndef _WIN32 #include #include @@ -33,14 +36,15 @@ #include #include #include +#include #include +#include +#include #include #include -#include using json = nlohmann::json; -// Cross-platform socket close / send flags #ifdef _WIN32 #define sock_close(s) closesocket(s) #define SEND_FLAGS 0 @@ -49,660 +53,1260 @@ using json = nlohmann::json; #define SEND_FLAGS MSG_NOSIGNAL #endif -// Cross-platform thread launch: wraps _beginthread (compat.h maps it to -// pthread_create+detach on Linux, native _beginthread on Windows). -// fn must be void(*)(void*). -static void LaunchThread(void (*fn)(void*), void* arg) -{ - _beginthread(fn, 0, arg); -} - -// --------------------------------------------------------------------------- -// Config — set by headless.cpp / ui.cpp before threads launch -// --------------------------------------------------------------------------- -std::string gRpcUser; -std::string gRpcPassword; - -// Pool server running flag — set false to stop server cleanly (all loops check it) volatile bool gPoolRunning = false; -// --------------------------------------------------------------------------- -// Stubs for symbols absent from Bitflash 0.1.0 -// --------------------------------------------------------------------------- -double dHashesPerSec = 0.0; -int64 nHPSTimerStart = 0; - -static bool IsInitialBlockDownload() -{ - return (pindexBest == NULL || nBestHeight < 1); -} - // --------------------------------------------------------------------------- // Hex helpers // --------------------------------------------------------------------------- static std::string ToHex(const void* p, size_t n) { const unsigned char* b = (const unsigned char*)p; - std::string s; s.reserve(n*2); + std::string s; s.reserve(n * 2); static const char* H = "0123456789abcdef"; - for (size_t i = 0; i < n; i++) { s += H[b[i]>>4]; s += H[b[i]&0xf]; } + for (size_t i = 0; i < n; i++) { s += H[b[i] >> 4]; s += H[b[i] & 0xf]; } return s; } + static std::vector FromHex(const std::string& s) { std::vector v; - for (size_t i = 0; i+1 < s.size(); i += 2) { - auto h=[](char c)->int{ - if(c>='0'&&c<='9') return c-'0'; - if(c>='a'&&c<='f') return c-'a'+10; - if(c>='A'&&c<='F') return c-'A'+10; + for (size_t i = 0; i + 1 < s.size(); i += 2) { + auto h = [](char c) -> int { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; return 0; }; - v.push_back((unsigned char)((h(s[i])<<4)|h(s[i+1]))); + v.push_back((unsigned char)((h(s[i]) << 4) | h(s[i + 1]))); } return v; } // --------------------------------------------------------------------------- -// PPLNS share tracking -// --------------------------------------------------------------------------- -struct Share { - std::string minerAddress; - int64 timestamp; - uint64 difficulty; -}; - -struct MinerStats { - std::string address; - std::string worker; - uint64 totalShares = 0; - uint64 roundShares = 0; - int64 lastSeen = 0; -}; -static std::mutex gMinerStatsMutex; -static std::map gMinerStats; - -struct FoundBlock { - int height; - int64 timestamp; - std::string minerAddress; - uint256 hash; -}; -static std::mutex gBlocksMutex; -static std::deque gFoundBlocks; - -struct Miner; -static std::mutex gMinersMutex; -static std::vector gMiners; - -static std::string MinerKey(const std::string& address, const std::string& worker) -{ - return address + "\n" + worker; -} - -void GetPoolWorkerStats(std::vector& out) -{ - out.clear(); - int64 now = GetTime(); - std::lock_guard lk(gMinerStatsMutex); - for (std::map::const_iterator it = gMinerStats.begin(); it != gMinerStats.end(); ++it) - { - const MinerStats& ms = it->second; - if (ms.lastSeen <= 0) - continue; - if (now - ms.lastSeen > 15 * 60) - continue; - PoolWorkerStatView view; - view.address = ms.address; - view.worker = ms.worker; - view.totalShares = ms.totalShares; - view.roundShares = ms.roundShares; - view.lastSeen = ms.lastSeen; - out.push_back(view); - } - std::sort(out.begin(), out.end(), [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) { - if (a.roundShares != b.roundShares) return a.roundShares > b.roundShares; - if (a.totalShares != b.totalShares) return a.totalShares > b.totalShares; - return a.lastSeen > b.lastSeen; - }); -} - -// --------------------------------------------------------------------------- -// Current mining job +// Job -- written and read exclusively by the event loop thread // --------------------------------------------------------------------------- struct StratumJob { std::string jobId; CBlock block; - uint256 target; - uint256 shareTarget; + uint256 target; // full block target; share targets are per-miner via vardiff int height; }; +static StratumJob gCurrentJob; +static bool gHaveJob = false; + +// Compute a share target from a Stratum difficulty value. +// Stratum difficulty 1 must correspond to THIS chain's easiest allowed target +// (bnProofOfWorkLimit = ~uint256(0) >> 12, compact 0x1f0fffff), not Bitcoin's +// SHA256 diff-1 target (0x1d00ffff). Those differ by about 2^20 (~1e6x): +// Bitcoin's diff-1 needs ~2^32 hashes/share on average, while this chain's PoW +// floor needs only ~2^12. Using the Bitcoin constant made every miner's +// "difficulty 1" about a million times harder than the chain's own floor, so a +// worker could hash for minutes without ever finding a share -- and vardiff +// never gets the samples it needs to correct course, because no share ever +// comes in for it to measure. Base off the actual chain limit instead so +// difficulty 1 is genuinely easy and vardiff has real data to retarget from. +// Higher difficulty = smaller (harder) target. +static uint256 ShareTargetFromDifficulty(double difficulty) +{ + if (difficulty <= 0.0) difficulty = 1.0; + static const uint256 diff1 = CBigNum(bnProofOfWorkLimit).getuint256(); + // shareTarget = diff1 / difficulty + CBigNum bn; bn.setuint256(diff1); + // Multiply by 2^16 to keep precision, divide, shift back + bn <<= 16; + bn /= (uint64)(difficulty * 65536.0); + uint256 t = bn.getuint256(); + // Cap at all-ones (maximum, easiest possible) + if (t < diff1 && difficulty < 1.0) t = ~uint256(0); + return t; +} -static std::mutex gJobMutex; -static StratumJob gCurrentJob; -static bool gHaveJob = false; -static std::atomic gJobSeq{0}; - -static uint256 MakeShareTarget(const uint256& blockTarget) +// Expected number of hashes to find one share at a given Stratum difficulty, +// on this chain's scale (difficulty 1 == bnProofOfWorkLimit, which needs on +// average 2^12 = 4096 hashes). Used to turn "shares per second" into H/s. +static const double SHARE_HASHES_AT_DIFF_1 = 4096.0; +static double ExpectedHashesForDifficulty(double difficulty) { - uint256 shareTarget = blockTarget; - shareTarget <<= 8; - if (shareTarget < blockTarget) - shareTarget = ~uint256(0); - return shareTarget; + if (difficulty <= 0.0) difficulty = 1.0; + return difficulty * SHARE_HASHES_AT_DIFF_1; } +// Rebuild the current job from the best chain tip. +// Must be called from the event loop thread (or startup, before the loop). +// Returns true on success. Logs the specific failure reason on false. static bool RebuildJob() { - if (!pindexBest) return false; + CRITICAL_BLOCK(cs_main) + { + if (!pindexBest) { + LogPrint("pool", "RebuildJob: no chain tip (pindexBest is null)\n"); + return false; + } - CBlockIndex* pindexPrev = pindexBest; - unsigned int nBits = GetNextWorkRequired(pindexPrev); + CBlockIndex* pindexPrev = pindexBest; + unsigned int nBits = GetNextWorkRequired(pindexPrev); - CKey key; - key.MakeNewKey(); - static std::atomic sExtra{0}; - uint32_t extraNonce = ++sExtra; + CKey key; key.MakeNewKey(); + static std::atomic sExtra{0}; + uint32_t extraNonce = ++sExtra; - CTransaction txNew; - txNew.vin.resize(1); - txNew.vin[0].prevout.SetNull(); - txNew.vin[0].scriptSig << nBits << (CBigNum)extraNonce; - txNew.vout.resize(1); - // Always pay coinbase to a fresh wallet key. DoPayouts() distributes - // shares to miners via SendMoney() after the block is accepted. - txNew.vout[0].scriptPubKey << key.GetPubKey() << OP_CHECKSIG; + CTransaction txNew; + txNew.vin.resize(1); + txNew.vin[0].prevout.SetNull(); + txNew.vin[0].scriptSig << nBits << (CBigNum)extraNonce; + txNew.vout.resize(1); + txNew.vout[0].scriptPubKey << key.GetPubKey() << OP_CHECKSIG; - CBlock block; - block.vtx.push_back(txNew); + CBlock block; + block.vtx.push_back(txNew); - { - CTxDB txdb("r"); - CRITICAL_BLOCK(cs_mapTransactions) { - map pool; - unsigned int sz = 0; - for (auto& kv : mapTransactions) { - CTransaction& tx = kv.second; - if (tx.IsCoinBase() || !tx.IsFinal()) continue; - int64 nFees = 0; - int64 nMinFee = tx.GetMinFee(block.vtx.size() < 100); - map tmp(pool); - if (!tx.ConnectInputs(txdb, tmp, CDiskTxPos(1,1,1), 0, nFees, false, true, nMinFee)) continue; - pool = tmp; - block.vtx.push_back(tx); - sz += ::GetSerializeSize(tx, SER_NETWORK); - if (sz > MAX_SIZE/2) break; + CTxDB txdb("r"); + CRITICAL_BLOCK(cs_mapTransactions) + { + map pool; + unsigned int sz = 0; + for (auto& kv : mapTransactions) { + CTransaction& tx = kv.second; + if (tx.IsCoinBase() || !tx.IsFinal()) continue; + int64 nFees = 0; + map tmp(pool); + if (!tx.ConnectInputs(txdb, tmp, CDiskTxPos(1,1,1), 0, nFees, + false, true, tx.GetMinFee(block.vtx.size() < 100))) + continue; + pool = tmp; + block.vtx.push_back(tx); + sz += ::GetSerializeSize(tx, SER_NETWORK); + if (sz > MAX_SIZE / 2) break; + } } } + + block.vtx[0].vout[0].nValue = block.GetBlockValue(0); + block.hashPrevBlock = pindexPrev->GetBlockHash(); + block.hashMerkleRoot = block.BuildMerkleTree(); + block.nTime = max((unsigned int)(pindexPrev->GetMedianTimePast() + 1), + (unsigned int)GetAdjustedTime()); + block.nBits = nBits; + block.nNonce = 1; + AddKey(key); + + StratumJob job; + job.jobId = ToHex(&extraNonce, 4); + job.block = block; + job.target = CBigNum().SetCompact(nBits).getuint256(); + job.height = nBestHeight + 1; + + gCurrentJob = job; + gHaveJob = true; + return true; } - block.vtx[0].vout[0].nValue = block.GetBlockValue(0); - block.hashPrevBlock = pindexPrev->GetBlockHash(); - block.hashMerkleRoot = block.BuildMerkleTree(); - block.nTime = max((unsigned int)(pindexPrev->GetMedianTimePast()+1), - (unsigned int)GetAdjustedTime()); - block.nBits = nBits; - block.nNonce = 1; - - AddKey(key); - - StratumJob job; - job.jobId = ToHex(&extraNonce, 4); - job.block = block; - job.target = CBigNum().SetCompact(nBits).getuint256(); - job.shareTarget = MakeShareTarget(job.target); - job.height = nBestHeight + 1; - - std::lock_guard lk(gJobMutex); - gCurrentJob = job; - gHaveJob = true; - ++gJobSeq; - return true; + return false; } // --------------------------------------------------------------------------- -// PPLNS payout -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// Deferred payouts — coinbase outputs require COINBASE_MATURITY (100) blocks -// before they can be spent. We store the share snapshot when a block is found -// and execute the actual SendMoney() calls once the coinbase matures. +// Payouts -- event loop thread only // --------------------------------------------------------------------------- struct PendingPayout { - int matureAtHeight; // pay out when nBestHeight >= this - std::map amounts; // addr -> BTF amount + int matureAtHeight; + std::map amounts; // address -> satoshis }; +static std::vector gPendingPayouts; -static std::mutex gPayoutMutex; -static std::vector gPendingPayouts; +static std::string PayoutFilePath() +{ + return GetAppDir() + "/pending_payouts.json"; +} -void GetPendingPayouts(std::vector& out) +static void SavePendingPayouts() { - out.clear(); - std::lock_guard lk(gPayoutMutex); - for (const PendingPayout& p : gPendingPayouts) - { - PendingPayoutView view; - view.matureAtHeight = p.matureAtHeight; - view.recipients = (int)p.amounts.size(); - view.totalAmount = 0; - for (const std::map::value_type& kv : p.amounts) - view.totalAmount += kv.second; - out.push_back(view); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& pp : gPendingPayouts) { + nlohmann::json obj; + obj["matureAtHeight"] = pp.matureAtHeight; + nlohmann::json amounts = nlohmann::json::object(); + for (const auto& kv : pp.amounts) + amounts[kv.first] = kv.second; + obj["amounts"] = amounts; + arr.push_back(obj); + } + std::string path = PayoutFilePath(); + FILE* f = fopen(path.c_str(), "w"); + if (f) { + std::string s = arr.dump(2); + fwrite(s.c_str(), 1, s.size(), f); + fclose(f); + } else { + LogPrint("payout", "[payout] WARNING: could not write %s\n", path.c_str()); } } -// Called when a block is found: snapshot shares, schedule payout for maturity. -static void QueuePayouts(int blockHeight) +static void LoadPendingPayouts() { - if (nMineMode != MINE_OPERATOR) return; - - std::map shareCount; - uint64 total = 0; - { - std::lock_guard lk(gMinerStatsMutex); - for (std::map::iterator it = gMinerStats.begin(); it != gMinerStats.end(); ++it) - { - MinerStats& ms = it->second; - if (ms.roundShares == 0) - continue; - if (ms.address.empty()) - continue; - shareCount[ms.address] += ms.roundShares; - total += ms.roundShares; - ms.roundShares = 0; + std::string path = PayoutFilePath(); + FILE* f = fopen(path.c_str(), "r"); + if (!f) return; // no file yet -- first run, nothing to load + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + std::string s(sz, '\0'); + fread(&s[0], 1, sz, f); + fclose(f); + try { + nlohmann::json arr = nlohmann::json::parse(s); + for (const auto& obj : arr) { + PendingPayout pp; + pp.matureAtHeight = obj["matureAtHeight"].get(); + for (auto it = obj["amounts"].begin(); it != obj["amounts"].end(); ++it) + pp.amounts[it.key()] = it.value().get(); + gPendingPayouts.push_back(pp); } + LogPrint("payout", "[payout] loaded %zu pending payout(s) from disk\n", + gPendingPayouts.size()); + } catch (const std::exception& e) { + LogPrint("payout", "[payout] WARNING: failed to parse %s: %s\n", + path.c_str(), e.what()); + } +} + +// Payouts ready to execute are moved here under gPayoutExecMutex. +// A dedicated thread drains this list so SendMoney (which takes cs_main) +// never runs on the event loop thread and can't stall miner I/O. +static std::vector gPayoutExecQueue; +static std::mutex gPayoutExecMutex; +static std::atomic gPayoutThreadRunning{false}; + +static void QueuePayouts(int blockHeight, + const std::map& shareCount, + uint64 total) +{ + if (total == 0) { + LogPrint("payout", "[payout] block %d: zero shares recorded -- nothing to pay out\n", + blockHeight); + return; } - if (total == 0) return; - int64 blockReward = CBlock().GetBlockValue(0); + double feeFrac = dPoolFeePercent / 100.0; + if (feeFrac < 0.0) feeFrac = 0.0; + if (feeFrac > 1.0) feeFrac = 1.0; + + // Compute the block subsidy at the height that was just found. + // We do not include tx fees in miner payout -- those stay in the operator + // wallet as part of the coinbase, which is the operator's cut on top of feeFrac. + int64 nSubsidy = 50 * COIN; + nSubsidy >>= (blockHeight / 210000); // halving schedule + int64 minerReward = (int64)((double)nSubsidy * (1.0 - feeFrac)); + PendingPayout pp; pp.matureAtHeight = blockHeight + COINBASE_MATURITY; + + int skipped = 0; + int64 burned = 0; // amount owed to miners with bad/empty addresses for (auto& kv : shareCount) { - int64 amount = (int64)(((double)kv.second / (double)total) * (double)blockReward); - if (amount >= CENT) - pp.amounts[kv.first] = amount; + int64 amount = (int64)(((double)kv.second / (double)total) * (double)minerReward); + if (amount < CENT) { + skipped++; + continue; + } + uint160 addrCheck; + if (kv.first.empty() || !AddressToHash160(kv.first, addrCheck)) { + // Address was invalid at authorize time (warned then). Their share + // of the reward cannot be paid -- log the amount so it's visible. + burned += amount; + LogPrint("payout", "[payout] WARNING: %s owed to miner with invalid" + " address '%s' -- cannot pay out, amount forfeited\n", + FormatMoney(amount).c_str(), kv.first.c_str()); + continue; + } + pp.amounts[kv.first] = amount; } - { std::lock_guard lk(gPayoutMutex); gPendingPayouts.push_back(pp); } - - printf("Pool: block %d payout queued for height %d (%zu miners)\n", - blockHeight, pp.matureAtHeight, pp.amounts.size()); + gPendingPayouts.push_back(pp); + SavePendingPayouts(); + + int64 operatorCut = nSubsidy - minerReward; + LogPrint("payout", + "[payout] block %d: subsidy %s BTF, miners get %s" + " (fee %.2f%% = %s to operator)," + " %zu recipients, matures height %d," + " %d below dust%s\n", + blockHeight, + FormatMoney(nSubsidy).c_str(), + FormatMoney(minerReward).c_str(), + dPoolFeePercent, + FormatMoney(operatorCut).c_str(), + pp.amounts.size(), pp.matureAtHeight, skipped, + burned > 0 ? (", " + FormatMoney(burned) + " forfeited (bad address)").c_str() + : ""); } -// Called from JobWatcher every new block: execute any matured payouts. +// Called from event loop: move matured entries to the exec queue (fast). +// Save BEFORE moving to exec queue: if we crash between the save and SendMoney, +// LoadPendingPayouts re-queues them from the exec-queue section of the file. static void FlushMaturePayouts() { - if (nMineMode != MINE_OPERATOR) return; - - std::lock_guard lk(gPayoutMutex); + std::vector ready; for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ) { if (nBestHeight < it->matureAtHeight) { ++it; continue; } - - printf("Pool: executing payout for block matured at height %d\n", it->matureAtHeight); - for (auto& kv : it->amounts) { - uint160 hash160; - if (!AddressToHash160(kv.first, hash160)) continue; - CScript scriptPubKey; - scriptPubKey << OP_DUP << OP_HASH160 << hash160 << OP_EQUALVERIFY << OP_CHECKSIG; - CWalletTx wtx; - wtx.mapValue["comment"] = strprintf("Pool payout height %d", it->matureAtHeight); - if (SendMoney(scriptPubKey, kv.second, wtx)) - printf("Pool: paid %s to %s\n", FormatMoney(kv.second).c_str(), kv.first.c_str()); + LogPrint("payout", "[payout] block height %d matured -- queuing %zu payments\n", + it->matureAtHeight, it->amounts.size()); + ready.push_back(*it); // copy first; erase after save + ++it; + } + if (!ready.empty()) { + // Erase from pending then save before handing off to exec thread, + // so the file never contains an entry that is already being sent. + for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ) { + if (nBestHeight >= it->matureAtHeight) + it = gPendingPayouts.erase(it); else - printf("Pool: payout FAILED for %s (will not retry)\n", kv.first.c_str()); + ++it; + } + SavePendingPayouts(); + std::lock_guard lk(gPayoutExecMutex); + for (auto& pp : ready) + gPayoutExecQueue.push_back(std::move(pp)); + } +} + +// Payout execution thread: drains gPayoutExecQueue, calls SendMoney. +// Runs independently of the event loop so slow wallet operations +// (SendMoney takes cs_main) never stall miner I/O. +static void PayoutThreadFn(void*) +{ + gPayoutThreadRunning = true; + LogPrint("payout", "[payout] payout thread started\n"); + + while (!fShutdown && gPoolRunning) { + std::vector batch; + { + std::lock_guard lk(gPayoutExecMutex); + batch.swap(gPayoutExecQueue); + } + + for (auto& pp : batch) { + LogPrint("payout", "[payout] executing payments for block matured at %d" + " (%zu miners)\n", pp.matureAtHeight, pp.amounts.size()); + for (auto& kv : pp.amounts) { + uint160 h160; + if (!AddressToHash160(kv.first, h160)) { + // Should not happen -- bad addresses are filtered in QueuePayouts + LogPrint("payout", "[payout] SKIPPED bad address '%s'\n", + kv.first.c_str()); + continue; + } + CScript sc; + sc << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG; + CWalletTx wtx; + wtx.mapValue["comment"] = strprintf("Pool payout height %d", + pp.matureAtHeight); + if (SendMoney(sc, kv.second, wtx)) + LogPrint("payout", "[payout] paid %s to %s\n", + FormatMoney(kv.second).c_str(), kv.first.c_str()); + else + LogPrint("payout", "[payout] FAILED to pay %s to %s" + " (insufficient funds or wallet locked?)\n", + FormatMoney(kv.second).c_str(), kv.first.c_str()); + } } - it = gPendingPayouts.erase(it); + + Sleep(500); // check for new work every 500ms } + + gPayoutThreadRunning = false; + LogPrint("payout", "[payout] payout thread stopped\n"); } // --------------------------------------------------------------------------- -// Stratum protocol +// GUI stats snapshot -- event loop writes, GUI threads read // --------------------------------------------------------------------------- -static json MakeNotifyParams(const StratumJob& job, bool clean) +struct StatsSnapshot { + int authorizedMiners = 0; + int blocksFound = 0; + uint64 roundShares = 0; + double totalHashRate = 0.0; +}; +static StatsSnapshot gStats; +static std::vector gWorkerStats; +static std::vector gPayoutViews; +static std::mutex gStatsMutex; + +void GetPoolOperatorStats(int& authorizedMiners, int& blocksFound, uint64& roundShares, double& totalHashRate) { - CBlock& b = const_cast(job.block); - unsigned char hdr[80]; - memcpy(hdr, &b.nVersion, 4); - memcpy(hdr+4, &b.hashPrevBlock, 32); - memcpy(hdr+36, &b.hashMerkleRoot, 32); - memcpy(hdr+68, &b.nTime, 4); - memcpy(hdr+72, &b.nBits, 4); - memcpy(hdr+76, &b.nNonce, 4); - unsigned char shareTgt[32]; memcpy(shareTgt, &job.shareTarget, 32); - return json::array({job.jobId, ToHex(hdr,80), b.hashPrevBlock.GetHex(), ToHex(shareTgt,32), clean}); + std::lock_guard lk(gStatsMutex); + authorizedMiners = gStats.authorizedMiners; + blocksFound = gStats.blocksFound; + roundShares = gStats.roundShares; + totalHashRate = gStats.totalHashRate; +} + +void GetPoolWorkerStats(std::vector& out) +{ + std::lock_guard lk(gStatsMutex); + out = gWorkerStats; +} + +void GetPendingPayouts(std::vector& out) +{ + std::lock_guard lk(gStatsMutex); + out = gPayoutViews; } +// --------------------------------------------------------------------------- +// Miner state -- event loop thread only +// --------------------------------------------------------------------------- +static std::atomic gNextExtranonce2{1}; + +// Vardiff target: aim for one share every VARDIFF_TARGET_SECS seconds per miner. +// After VARDIFF_SHARES_PER_CHECK shares, recalculate and adjust if rate is off +// by more than 50%. Shares are the stable unit; difficulty floats to match. +static const int64 VARDIFF_TARGET_SECS = 15; // one share every 15s +static const uint64 VARDIFF_SHARES_PER_CHECK = 5; // re-evaluate every 5 shares +static const double VARDIFF_RETARGET_RATIO = 1.5; // adjust if >50% off target +static const double VARDIFF_MIN = 0.001; +static const double VARDIFF_MAX = 1e12; +// If a miner has gone this long since its vardiff window opened with ZERO +// shares, the normal vardiff retarget (which only fires after +// VARDIFF_SHARES_PER_CHECK shares) can never trigger -- it has no samples to +// act on. This is the residual gap the ShareTargetFromDifficulty fix above +// doesn't cover: even with a correctly-easy diff-1 target, a miner stuck in +// RandomX light mode (or otherwise much slower than expected) can go +// indefinitely without a single share, and difficulty would never drop to +// meet it. Force a difficulty cut on a timeout instead of waiting for shares +// that will never come. +static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance +static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much + struct Miner { - SOCKET fd; - bool authorised; - std::string address; - std::string worker; - std::string readBuf; - int64 connectTime; - uint64 sessionShares; - Miner(SOCKET f) : fd(f), authorised(false), connectTime(GetTime()), sessionShares(0) {} + btf_socket_t fd; + bool authorised = false; + std::string address; + std::string worker; + std::string readBuf; + int64 connectTime; + uint64 sessionShares = 0; + uint64 roundShares = 0; + uint64 totalShares = 0; + int64 lastSeen = 0; + uint32_t extranonce2; + + // Vardiff state + double difficulty = 1.0; // current share difficulty sent to this miner + uint256 shareTarget; // derived from difficulty; what we check against + int64 vardiffWindowStart = 0; // when the current vardiff window opened + uint64 vardiffShares = 0; // shares found in current window + + // Hashrate estimate -- EWMA of (expected hashes per share at current + // difficulty) / (seconds since the previous share). We had all the data + // to compute this (difficulty + share timing) but never actually did, + // so pool/GUI hashrate always showed as zero/missing. + int64 lastShareTime = 0; // GetTime() of the previous accepted share + double hashRate = 0.0; // smoothed H/s estimate + + explicit Miner(btf_socket_t f) + : fd(f), connectTime(GetTime()), + extranonce2(gNextExtranonce2.fetch_add(1)) {} }; -void GetPoolOperatorStats(int& authorizedMiners, int& blocksFound, uint64& roundShares) +static std::vector gMiners; + +struct FoundBlock { int height; int64 ts; std::string addr; }; +static std::deque gFoundBlocks; + +// --------------------------------------------------------------------------- +// Accept queue -- accept threads push; event loop pops at top of each tick +// --------------------------------------------------------------------------- +static std::vector gReadyQueue; +static std::mutex gReadyMutex; + +static void PushReady(btf_socket_t s) { - authorizedMiners = 0; - blocksFound = 0; - roundShares = 0; - { - std::lock_guard lk(gMinersMutex); - for (Miner* m : gMiners) - if (m->authorised) - authorizedMiners++; + std::lock_guard lk(gReadyMutex); + gReadyQueue.push_back(s); +} + +// --------------------------------------------------------------------------- +// I/O helpers -- event loop thread only +// --------------------------------------------------------------------------- + +// Blocking send of a complete JSON line. Called only from the event loop so +// no other thread is writing to the same fd concurrently. +static bool SendLine(btf_socket_t fd, const json& j) +{ + std::string s = j.dump() + "\n"; + int off = 0, total = (int)s.size(); + while (off < total) { + int r = send(fd, s.c_str() + off, total - off, SEND_FLAGS); + if (r <= 0) return false; + off += r; } - { - std::lock_guard lk(gBlocksMutex); - blocksFound = (int)gFoundBlocks.size(); + return true; +} + +// Non-blocking recv: pull whatever bytes are available now into buf, then +// return one complete line if one is ready. Three outcomes: +// true -- line is filled; buf may still contain more lines +// false, errno EAGAIN/EWOULDBLOCK (or FIONREAD==0) -- no data right now +// false, r==0 -- clean disconnect; caller must treat fd as dead +// Caller distinguishes disconnect from "not yet" by checking whether +// line.empty() after a false return (line is "" on both paths, so we +// use a separate out-param for the disconnect signal). +static bool RecvLine(btf_socket_t fd, std::string& buf, + std::string& line, bool& disconnected) +{ + disconnected = false; + char tmp[4096]; + +#ifdef _WIN32 + u_long avail = 0; + ioctlsocket(fd, FIONREAD, &avail); + if (avail > 0) { + int r = recv(fd, tmp, (int)std::min((u_long)(sizeof(tmp)-1), avail), 0); + if (r > 0) { tmp[r] = 0; buf += tmp; } + else if (r == 0){ disconnected = true; return false; } + // r < 0: WSAEWOULDBLOCK -- fall through with what we have } +#else { - std::lock_guard lk(gMinerStatsMutex); - for (std::map::const_iterator it = gMinerStats.begin(); it != gMinerStats.end(); ++it) - roundShares += it->second.roundShares; + int r = recv(fd, tmp, sizeof(tmp) - 1, MSG_DONTWAIT); + if (r > 0) { tmp[r] = 0; buf += tmp; } + else if (r == 0) { disconnected = true; return false; } + // r < 0: EAGAIN/EWOULDBLOCK -- fall through with what we have } +#endif + + size_t pos = buf.find('\n'); + if (pos == std::string::npos) return false; // no complete line yet + + line = buf.substr(0, pos); + if (!line.empty() && line.back() == '\r') line.pop_back(); + buf = buf.substr(pos + 1); + return true; } -static bool SendLine(Miner* m, const json& j) +// --------------------------------------------------------------------------- +// Job helpers +// --------------------------------------------------------------------------- +static CBlock BuildMinerBlock(const StratumJob& job, uint32_t extranonce2) { - std::string s = j.dump() + "\n"; - int off = 0; - while (off < (int)s.size()) { - int n = send(m->fd, s.c_str() + off, (int)s.size() - off, SEND_FLAGS); - if (n <= 0) - return false; - off += n; - } - return true; + CBlock blk = job.block; + blk.vtx[0].vin[0].scriptSig << (CBigNum)extranonce2; + blk.hashMerkleRoot = blk.BuildMerkleTree(); + return blk; +} + +// shareTarget is per-miner (set by vardiff) and passed explicitly so the +// value in p[3] of mining.notify always matches what HandleLine checks on submit. +// This is the single source of truth for share difficulty. +static json MakeNotifyParams(const StratumJob& job, const CBlock& b, + const uint256& shareTarget, bool clean) +{ + unsigned char hdr[80]; + memcpy(hdr, &b.nVersion, 4); + memcpy(hdr+4, &b.hashPrevBlock, 32); + memcpy(hdr+36, &b.hashMerkleRoot, 32); + memcpy(hdr+68, &b.nTime, 4); + memcpy(hdr+72, &b.nBits, 4); + memcpy(hdr+76, &b.nNonce, 4); + unsigned char tgt[32]; memcpy(tgt, &shareTarget, 32); + return json::array({job.jobId, ToHex(hdr, 80), + b.hashPrevBlock.GetHex(), ToHex(tgt, 32), clean}); +} + +static void SendJob(Miner* m, bool clean) +{ + if (!gHaveJob) return; + CBlock mb = BuildMinerBlock(gCurrentJob, m->extranonce2); + // Use this miner's own shareTarget (set by vardiff) in the notify params. + // The participant miner reads p[3] as its share target, and HandleLine + // checks m->shareTarget on submit -- both sides now use the same value. + json n = {{"id",nullptr},{"method","mining.notify"}, + {"params", MakeNotifyParams(gCurrentJob, mb, m->shareTarget, clean)}}; + if (!SendLine(m->fd, n)) + LogPrint("worker", "[pool->worker] job send FAILED for %s -- miner will be dropped\n", + m->address.empty() ? "(not yet authorized)" : m->address.c_str()); } static void BroadcastJob(bool clean) { - StratumJob job; - { std::lock_guard lk(gJobMutex); if(!gHaveJob) return; job=gCurrentJob; } - json n = {{"id",nullptr},{"method","mining.notify"},{"params",MakeNotifyParams(job,clean)}}; - std::lock_guard lk(gMinersMutex); - for (Miner* m : gMiners) - if (m->authorised && !SendLine(m, n)) - printf("Stratum: failed to broadcast job to %s\n", m->address.c_str()); + if (!gHaveJob) return; + int sent = 0; + for (Miner* m : gMiners) { + if (!m->authorised) continue; + SendJob(m, clean); + sent++; + } + LogPrint("pool", "[pool] job %s broadcast to %d miner(s)\n", + gCurrentJob.jobId.c_str(), sent); } -static void HandleLine(Miner* m, const std::string& line) +// --------------------------------------------------------------------------- +// Stratum message handler -- called for each complete line from a miner +// Returns false if the miner should be disconnected immediately. +// --------------------------------------------------------------------------- +static bool HandleLine(Miner* m, const std::string& rawLine, + std::map& roundShareCount, + uint64& roundShareTotal, + int& blocksFoundThisSession) { json req; - try { req = json::parse(line); } catch(...){ return; } + try { req = json::parse(rawLine); } + catch (...) { + LogPrint("worker", "[worker] JSON parse error from %s: %.80s\n", + m->address.empty() ? "(unauth)" : m->address.c_str(), + rawLine.c_str()); + return true; // don't disconnect on bad JSON, just ignore the line + } - json id = req.value("id", json(nullptr)); - std::string method = req.value("method",""); + json id = req.value("id", json(nullptr)); + std::string method = req.value("method", ""); - auto reply = [&](json result, json error=nullptr){ - if (!SendLine(m, json{{"id",id},{"result",result},{"error",error}})) - printf("Stratum: failed to reply to miner %s\n", m->address.c_str()); + // Reply helper: always sends result+error pair as Stratum requires + auto reply = [&](json result, std::string error = "") { + json err = error.empty() ? json(nullptr) : json(error); + if (!SendLine(m->fd, json{{"id",id},{"result",result},{"error",err}})) + LogPrint("worker", "[pool->worker] reply send FAILED for %s (method=%s)\n", + m->address.empty() ? "(unauth)" : m->address.c_str(), + method.c_str()); }; + // ------------------------------------------------------------------ if (method == "mining.subscribe") { std::string sid = ToHex(&m->fd, 4); - printf("Stratum: miner subscribed %s\n", sid.c_str()); + LogPrint("worker", "[worker->pool] subscribe from fd=%d sid=%s\n", + (int)m->fd, sid.c_str()); reply(json::array({json::array({json::array({"mining.notify",sid})}),sid,4})); - return; + return true; } + // ------------------------------------------------------------------ if (method == "mining.authorize") { auto& p = req["params"]; - m->address = (p.size()>0 && p[0].is_string()) ? p[0].get() : ""; - m->worker = (p.size()>1 && p[1].is_string()) ? p[1].get() : "worker"; - m->authorised = true; - printf("Stratum: miner authorized address=%s worker=%s\n", m->address.c_str(), m->worker.c_str()); - reply(true); + m->address = (p.size() > 0 && p[0].is_string()) ? p[0].get() : ""; + m->worker = (p.size() > 1 && p[1].is_string()) ? p[1].get() : "worker"; + m->lastSeen = GetTime(); - uint160 h; bool valid = AddressToHash160(m->address, h); - if (!valid) printf("Stratum: miner %s invalid address '%s' — shares won't pay out\n", - m->worker.c_str(), m->address.c_str()); + // Validate address before authorising + uint160 h160; bool validAddr = AddressToHash160(m->address, h160); - { std::lock_guard lk(gMinerStatsMutex); - auto& ms = gMinerStats[m->address]; - ms.lastSeen = GetTime(); ms.worker = m->worker; } + m->authorised = true; // authorise regardless -- shares just won't pay out + LogPrint("worker", "[worker->pool] authorize address=%s worker=%s addr_valid=%s\n", + m->address.c_str(), m->worker.c_str(), validAddr ? "yes" : "NO"); - if (!SendLine(m, json{{"id",nullptr},{"method","mining.set_difficulty"},{"params",json::array({1})}})) - printf("Stratum: failed to send difficulty to %s\n", m->address.c_str()); + if (!validAddr) + LogPrint("worker", "[pool] WARNING: address '%s' is invalid -- " + "miner will hash but shares cannot be paid out\n", + m->address.c_str()); - StratumJob job; bool have; - { std::lock_guard lk(gJobMutex); have=gHaveJob; if(have) job=gCurrentJob; } - if (have) { - printf("Stratum: sending job %s to %s\n", job.jobId.c_str(), m->address.c_str()); - if (!SendLine(m, json{{"id",nullptr},{"method","mining.notify"},{"params",MakeNotifyParams(job,true)}})) - printf("Stratum: failed to send job %s to %s\n", job.jobId.c_str(), m->address.c_str()); + reply(true); + + // Initialize vardiff state and send initial difficulty + m->difficulty = 1.0; + m->shareTarget = ShareTargetFromDifficulty(m->difficulty); + m->vardiffWindowStart = GetTime(); + m->vardiffShares = 0; + LogPrint("worker", "[pool->worker] initial difficulty=%.4f for %s\n", + m->difficulty, m->address.c_str()); + if (!SendLine(m->fd, json{{"id",nullptr},{"method","mining.set_difficulty"}, + {"params",json::array({m->difficulty})}})) + LogPrint("worker", "[pool->worker] set_difficulty send FAILED for %s\n", + m->address.c_str()); + + if (gHaveJob) { + LogPrint("worker", "[pool->worker] sending job %s to %s\n", + gCurrentJob.jobId.c_str(), m->address.c_str()); + SendJob(m, true); } else { - printf("Stratum: no job available for %s\n", m->address.c_str()); + LogPrint("worker", "[pool->worker] no job available yet for %s" + " -- will be sent one as soon as the pool builds it\n", + m->address.c_str()); } - return; + return true; } - if (method == "mining.extranonce.subscribe") { reply(true); return; } + // ------------------------------------------------------------------ + if (method == "mining.extranonce.subscribe") { reply(true); return true; } + if (method == "mining.get_transactions") { reply(json::array()); return true; } + // ------------------------------------------------------------------ if (method == "mining.submit") { - if (!m->authorised) { reply(false,"not authorised"); return; } + if (!m->authorised) { + LogPrint("worker", "[worker->pool] submit from unauthorized miner (fd=%d)\n", + (int)m->fd); + reply(false, "not authorised"); + return true; + } auto& p = req["params"]; - if (p.size() < 4) { reply(false,"bad params"); return; } + if (p.size() < 3) { + LogPrint("worker", "[worker->pool] submit bad params from %s (size=%zu)\n", + m->address.c_str(), p.size()); + reply(false, "bad params"); + return true; + } - std::string jobId = p[1].get(); - std::string nonceHex = p[2].get(); + std::string submitJobId = p[1].get(); + std::string nonceHex = p[2].get(); - StratumJob job; - { std::lock_guard lk(gJobMutex); - if (!gHaveJob || gCurrentJob.jobId != jobId) { reply(false,"stale job"); return; } - job = gCurrentJob; } + if (!gHaveJob) { + LogPrint("worker", "[worker->pool] submit from %s but pool has no current job\n", + m->address.c_str()); + reply(false, "stale job"); + return true; + } + if (gCurrentJob.jobId != submitJobId) { + LogPrint("worker", "[worker->pool] submit stale job from %s" + " (submitted=%s current=%s)\n", + m->address.c_str(), submitJobId.c_str(), gCurrentJob.jobId.c_str()); + reply(false, "stale job"); + return true; + } auto nb = FromHex(nonceHex); - if (nb.size()<4) { reply(false,"bad nonce"); return; } + if (nb.size() < 4) { + LogPrint("worker", "[worker->pool] submit bad nonce from %s: '%s'\n", + m->address.c_str(), nonceHex.c_str()); + reply(false, "bad nonce"); + return true; + } unsigned int nNonce; memcpy(&nNonce, nb.data(), 4); - CBlock& b = job.block; + // Rebuild this miner's exact block (unique merkle via extranonce2) + CBlock b = BuildMinerBlock(gCurrentJob, m->extranonce2); b.nNonce = nNonce; unsigned char hdr[80]; - memcpy(hdr, &b.nVersion, 4); - memcpy(hdr+4, &b.hashPrevBlock, 32); - memcpy(hdr+36, &b.hashMerkleRoot,32); - memcpy(hdr+68, &b.nTime, 4); - memcpy(hdr+72, &b.nBits, 4); - memcpy(hdr+76, &b.nNonce, 4); + memcpy(hdr, &b.nVersion, 4); + memcpy(hdr+4, &b.hashPrevBlock, 32); + memcpy(hdr+36, &b.hashMerkleRoot, 32); + memcpy(hdr+68, &b.nTime, 4); + memcpy(hdr+72, &b.nBits, 4); + memcpy(hdr+76, &b.nNonce, 4); uint256 powHash = RandomXPoWHash(hdr, 80); - // Record share in the current round. - std::string key = MinerKey(m->address, m->worker); - { std::lock_guard lk(gMinerStatsMutex); - MinerStats& ms = gMinerStats[key]; - ms.address = m->address; - ms.worker = m->worker; - ms.roundShares++; - ms.totalShares++; - ms.lastSeen = GetTime(); } + // Check against this miner's personal share target (set by vardiff) + if (powHash > m->shareTarget) { + LogPrint("worker", "[worker->pool] high-hash from %s" + " nonce=%u (difficulty=%.4f)\n", + m->address.c_str(), nNonce, m->difficulty); + reply(false, "high-hash"); + return true; + } + + // Valid share -- record it m->sessionShares++; + m->roundShares++; + m->totalShares++; + m->vardiffShares++; + int64 shareTime = GetTime(); + m->lastSeen = shareTime; + roundShareCount[m->address]++; + roundShareTotal++; + + // Update hashrate estimate: expected hashes for this share divided by + // the time since the last one, smoothed with an EWMA so a single fast + // or slow share doesn't make the number jump around. + if (m->lastShareTime > 0) { + int64 dt = shareTime - m->lastShareTime; + if (dt <= 0) dt = 1; + double inst = ExpectedHashesForDifficulty(m->difficulty) / (double)dt; + const double alpha = 0.25; + m->hashRate = (m->hashRate <= 0.0) ? inst + : alpha * inst + (1.0 - alpha) * m->hashRate; + } + m->lastShareTime = shareTime; - if (powHash > job.shareTarget) { reply(true); return; } // valid share, not a full block + LogPrint("worker", "[worker->pool] valid share from %s" + " nonce=%u job=%s difficulty=%.4f (session: %llu)\n", + m->address.c_str(), nNonce, + gCurrentJob.jobId.c_str(), m->difficulty, + (unsigned long long)m->sessionShares); + reply(true); + + // Vardiff: after every VARDIFF_SHARES_PER_CHECK shares, check rate + if (m->vardiffShares >= VARDIFF_SHARES_PER_CHECK) { + int64 elapsed = GetTime() - m->vardiffWindowStart; + if (elapsed <= 0) elapsed = 1; + double actualSecsPerShare = (double)elapsed / (double)m->vardiffShares; + double ratio = actualSecsPerShare / (double)VARDIFF_TARGET_SECS; + // ratio < 1 means shares arriving faster than target (too easy) + // ratio > 1 means shares arriving slower than target (too hard) + if (ratio < (1.0 / VARDIFF_RETARGET_RATIO) || + ratio > VARDIFF_RETARGET_RATIO) { + // New difficulty scales inversely with actual share rate + double newDiff = m->difficulty / ratio; + if (newDiff < VARDIFF_MIN) newDiff = VARDIFF_MIN; + if (newDiff > VARDIFF_MAX) newDiff = VARDIFF_MAX; + // Only apply if the change is meaningful (>10%) + if (newDiff / m->difficulty > 1.1 || m->difficulty / newDiff > 1.1) { + m->difficulty = newDiff; + m->shareTarget = ShareTargetFromDifficulty(m->difficulty); + LogPrint("worker", "[pool] vardiff: %s new difficulty=%.4f" + " (%.1fs/share actual vs %ds target)\n", + m->address.c_str(), m->difficulty, + actualSecsPerShare, (int)VARDIFF_TARGET_SECS); + SendLine(m->fd, json{{"id",nullptr}, + {"method","mining.set_difficulty"}, + {"params",json::array({m->difficulty})}}); + // set_difficulty alone doesn't change what the worker + // actually hashes against -- it only reads its share + // target from mining.notify params[3]. Without this, the + // new difficulty wouldn't take effect until the next + // natural job broadcast (new block, or the 60s trickle + // refresh), so push a fresh notify for this miner now. + SendJob(m, false); + } + } + // Reset window + m->vardiffWindowStart = GetTime(); + m->vardiffShares = 0; + } + + if (powHash > gCurrentJob.target) return true; // valid share, not a block + + // Full difficulty met -- submit block to the chain + LogPrint("pool", "[pool] BLOCK CANDIDATE from %s height=%d nNonce=%u -- submitting\n", + m->address.c_str(), gCurrentJob.height, nNonce); - // Full block CBlock* pblock = new CBlock(b); bool accepted = false; CRITICAL_BLOCK(cs_main) { + int heightBefore = nBestHeight; if (ProcessBlock(NULL, pblock)) { - accepted = true; - printf("Pool: BLOCK FOUND by %s height=%d nNonce=%u\n", - m->address.c_str(), job.height, nNonce); - FoundBlock fb{job.height, GetTime(), m->address, pblock->GetHash()}; - { std::lock_guard lk(gBlocksMutex); - gFoundBlocks.push_front(fb); - if (gFoundBlocks.size()>50) gFoundBlocks.pop_back(); } - { std::lock_guard lk(gJobMutex); gHaveJob=false; } + // ProcessBlock returns true for orphans too -- verify the chain + // actually advanced. If nBestHeight didn't increase, the block + // was shelved as an orphan (pool was behind the chain tip) and + // no coinbase was added to the wallet. Do not pay out. + if (nBestHeight > heightBefore) { + accepted = true; + LogPrint("pool", "[pool] BLOCK ACCEPTED height=%d by %s\n", + gCurrentJob.height, m->address.c_str()); + gFoundBlocks.push_front({gCurrentJob.height, GetTime(), m->address}); + if (gFoundBlocks.size() > 50) gFoundBlocks.pop_back(); + blocksFoundThisSession++; + gHaveJob = false; // invalidate job immediately + } else { + LogPrint("pool", "[pool] BLOCK ORPHANED height=%d by %s" + " -- pool was behind chain tip, no payout\n", + gCurrentJob.height, m->address.c_str()); + gHaveJob = false; // job is stale regardless + } } else { + LogPrint("pool", "[pool] BLOCK REJECTED height=%d (ProcessBlock returned false)\n", + gCurrentJob.height); delete pblock; } } - reply(accepted); - if (accepted) { QueuePayouts(job.height); RebuildJob(); BroadcastJob(true); } - return; + + if (accepted) { + // Snapshot height before RebuildJob() (which increments it) + int foundHeight = gCurrentJob.height; + QueuePayouts(foundHeight, roundShareCount, roundShareTotal); + // Reset round shares + roundShareCount.clear(); + roundShareTotal = 0; + for (Miner* mi : gMiners) mi->roundShares = 0; + // Build and broadcast new job immediately + if (RebuildJob()) { + LogPrint("pool", "[pool] new job %s built after block %d\n", + gCurrentJob.jobId.c_str(), foundHeight); + BroadcastJob(true); + } else { + LogPrint("pool", "[pool] WARNING: RebuildJob() failed after block %d" + " -- miners will wait\n", foundHeight); + } + } + return true; } - if (method == "mining.get_transactions") { reply(json::array()); return; } + // Unknown method -- log it (can reveal miner software quirks) + LogPrint("worker", "[worker->pool] unknown method '%s' from %s\n", + method.c_str(), + m->address.empty() ? "(unauth)" : m->address.c_str()); reply(nullptr, "unknown method"); + return true; } -static void MinerThreadFn(void* arg) +// --------------------------------------------------------------------------- +// Stats refresh -- called every 2s from event loop +// --------------------------------------------------------------------------- +static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal) { - Miner* m = (Miner*)arg; - while (!fShutdown && gPoolRunning) { - fd_set fds; FD_ZERO(&fds); FD_SET(m->fd, &fds); - struct timeval tv={1,0}; - if (select((int)m->fd+1, &fds, NULL, NULL, &tv) <= 0) continue; - char buf[4096]; - int n = recv(m->fd, buf, sizeof(buf)-1, 0); - if (n <= 0) break; - buf[n]=0; m->readBuf += buf; - size_t pos; - while ((pos = m->readBuf.find('\n')) != std::string::npos) { - std::string line = m->readBuf.substr(0, pos); - m->readBuf = m->readBuf.substr(pos+1); - if (!line.empty() && line.back()=='\r') line.pop_back(); - if (!line.empty()) HandleLine(m, line); - } + std::lock_guard lk(gStatsMutex); + + gStats.authorizedMiners = 0; + gStats.blocksFound = blocksFoundThisSession; + gStats.roundShares = roundShareTotal; + gStats.totalHashRate = 0.0; + for (Miner* m : gMiners) + if (m->authorised) gStats.authorizedMiners++; + + gWorkerStats.clear(); + for (Miner* m : gMiners) { + if (!m->authorised || m->lastSeen <= 0) continue; + PoolWorkerStatView v; + v.address = m->address; + v.worker = m->worker; + v.roundShares = m->roundShares; + v.totalShares = m->totalShares; + v.lastSeen = m->lastSeen; + v.hashRate = m->hashRate; + gWorkerStats.push_back(v); + gStats.totalHashRate += m->hashRate; + } + std::sort(gWorkerStats.begin(), gWorkerStats.end(), + [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) { + return a.roundShares != b.roundShares ? a.roundShares > b.roundShares + : a.lastSeen > b.lastSeen; + }); + + // gPendingPayouts is event-loop-only data (no lock needed here since + // RefreshStats is always called from the event loop thread). + gPayoutViews.clear(); + for (const PendingPayout& pp : gPendingPayouts) { + PendingPayoutView pv; + pv.matureAtHeight = pp.matureAtHeight; + pv.recipients = (int)pp.amounts.size(); + pv.totalAmount = 0; + for (auto& kv : pp.amounts) pv.totalAmount += kv.second; + gPayoutViews.push_back(pv); } - sock_close(m->fd); - printf("Stratum: miner %s disconnected (%llu shares)\n", - m->worker.empty()?"unknown":m->worker.c_str(), - (unsigned long long)m->sessionShares); - { std::lock_guard lk(gMinersMutex); - auto it = std::find(gMiners.begin(), gMiners.end(), m); - if (it != gMiners.end()) gMiners.erase(it); } - delete m; } -static void JobWatcherFn(void*) +// --------------------------------------------------------------------------- +// Event loop +// --------------------------------------------------------------------------- +static void PoolEventLoop() { - int lastHeight = -1; + int lastHeight = -1; + int64 lastRefresh = GetTime(); + int64 lastStatsRefresh = 0; + int blocksFoundThisSession = 0; + int64 noJobSince = 0; // 0 = have a job + bool warnedNoJob = false; + int64 lastNoJobRetry = 0; // throttle RebuildJob calls when no tip + + std::map roundShareCount; + uint64 roundShareTotal = 0; + + static const int64 JOB_REFRESH_SECS = 60; // re-stamp nTime periodically + static const int64 NO_JOB_WARN_SECS = 15; // warn after this long with no job + static const int64 NO_JOB_RETRY_SECS = 5; // how often to retry RebuildJob + while (!fShutdown && gPoolRunning) { - Sleep(500); - bool newBlock = false; - CRITICAL_BLOCK(cs_main) - { if (nBestHeight != lastHeight && pindexBest) { lastHeight = nBestHeight; newBlock = true; } } - if (newBlock) { - FlushMaturePayouts(); - if (RebuildJob()) { printf("Stratum: new job height=%d\n", lastHeight+1); BroadcastJob(true); } - } else if (!gHaveJob) { + + // 1. Drain accept queue + { + std::lock_guard lk(gReadyMutex); + for (btf_socket_t fd : gReadyQueue) { + gMiners.push_back(new Miner(fd)); + LogPrint("worker", "[pool] miner fd=%d entered event loop" + " -- awaiting subscribe/authorize\n", (int)fd); + } + gReadyQueue.clear(); + } + + // 2. Detect new block. + // When our own miner found the block, HandleLine already called + // RebuildJob() + BroadcastJob(true) and set gHaveJob=true. + // We still flush mature payouts and update bookkeeping, but skip + // rebuilding if a fresh job already exists for this height. + { + bool newBlock = false; + CRITICAL_BLOCK(cs_main) + { + if (pindexBest && nBestHeight != lastHeight) { + lastHeight = nBestHeight; + newBlock = true; + } + } + if (newBlock) { + FlushMaturePayouts(); + if (!gHaveJob) { + // External block (not found by our miners), or HandleLine's + // RebuildJob failed. Either way this round is over with no + // payout from us, so clear round shares before starting the + // next one -- otherwise they'd keep accumulating across + // rounds that our own miners didn't win. + roundShareCount.clear(); + roundShareTotal = 0; + for (Miner* mi : gMiners) mi->roundShares = 0; + + // Build and broadcast now. + if (RebuildJob()) { + LogPrint("pool", "[pool] new block height=%d -- job %s ready," + " %zu miner(s) connected\n", + lastHeight, gCurrentJob.jobId.c_str(), gMiners.size()); + BroadcastJob(true); + } else { + LogPrint("pool", "[pool] new block height=%d but RebuildJob() failed" + " -- miners will wait\n", lastHeight); + } + } else { + // HandleLine already built and broadcast for this height. + LogPrint("pool", "[pool] new block height=%d -- job already" + " broadcast by submit handler\n", lastHeight); + } + lastRefresh = GetTime(); + noJobSince = 0; + warnedNoJob = false; + } + } + + // 3. No-job watchdog (throttled: only retry every NO_JOB_RETRY_SECS) + if (!gHaveJob) { + int64 now = GetTime(); + if (noJobSince == 0) noJobSince = now; + + if (now - lastNoJobRetry >= NO_JOB_RETRY_SECS) { + lastNoJobRetry = now; + if (RebuildJob()) { + LogPrint("pool", "[pool] job built after %llds without one\n", + (long long)(now - noJobSince)); + BroadcastJob(false); + noJobSince = 0; + warnedNoJob = false; + lastRefresh = now; + } else if (!warnedNoJob && now - noJobSince >= NO_JOB_WARN_SECS) { + LogPrint("pool", "[pool] STUCK: no job for %llds -- %s." + " Miners are waiting. Will keep retrying every %llds.\n", + (long long)(now - noJobSince), + pindexBest ? "RebuildJob() keeps failing (check above for why)" + : "no chain tip yet (node still syncing)", + (long long)NO_JOB_RETRY_SECS); + warnedNoJob = true; + } + } + } else if (GetTime() - lastRefresh >= JOB_REFRESH_SECS) { + // Periodic nTime refresh -- stale timestamps cause valid hashes to be rejected if (RebuildJob()) BroadcastJob(false); + lastRefresh = GetTime(); + } + + // 3.5. Vardiff stall watchdog -- runs independently of share count. + // The normal vardiff check in HandleLine only fires once + // VARDIFF_SHARES_PER_CHECK shares have arrived; a miner that never + // finds a single share never reaches that check, so it can be stuck + // at its initial (or last) difficulty forever. Catch that here. + for (Miner* m : gMiners) { + if (!m->authorised || m->vardiffShares != 0 || m->vardiffWindowStart <= 0) + continue; + int64 stalled = GetTime() - m->vardiffWindowStart; + if (stalled < VARDIFF_STALL_SECS) continue; + + double newDiff = m->difficulty / VARDIFF_STALL_CUT; + if (newDiff < VARDIFF_MIN) newDiff = VARDIFF_MIN; + + if (newDiff < m->difficulty) { + m->difficulty = newDiff; + m->shareTarget = ShareTargetFromDifficulty(m->difficulty); + m->vardiffWindowStart = GetTime(); + LogPrint("worker", "[pool] vardiff STALL: %s sent zero shares in" + " %llds -- forcing difficulty down to %.6f (no share data" + " to retarget from normally)\n", + m->address.c_str(), (long long)stalled, m->difficulty); + SendLine(m->fd, json{{"id",nullptr}, + {"method","mining.set_difficulty"}, + {"params",json::array({m->difficulty})}}); + // Same reasoning as the normal vardiff retarget above: the + // worker only reads its share target from mining.notify, so + // push a fresh job now instead of waiting on the next + // natural broadcast. + SendJob(m, false); + } else { + // Already at the difficulty floor -- this miner's problem isn't + // difficulty, it's something else (slow hashing, bad connection, + // stuck RandomX light mode, etc). Log once per stall window so + // it's visible without spamming. + LogPrint("worker", "[pool] vardiff STALL: %s still at zero shares" + " after %llds at floor difficulty %.6f -- likely a" + " hashrate/connectivity problem, not a difficulty problem\n", + m->address.c_str(), (long long)stalled, m->difficulty); + m->vardiffWindowStart = GetTime(); // avoid re-logging every tick + } + } + + // 4. Select over miner sockets + if (!gMiners.empty()) { + fd_set fds; FD_ZERO(&fds); + int maxfd = 0; + for (Miner* m : gMiners) { + FD_SET(m->fd, &fds); + if ((int)m->fd > maxfd) maxfd = (int)m->fd; + } + struct timeval tv = {0, 50000}; // 50ms + if (select(maxfd + 1, &fds, NULL, NULL, &tv) > 0) { + std::vector dead; + for (Miner* m : gMiners) { + if (!FD_ISSET(m->fd, &fds)) continue; + std::string line; + bool disc = false; + // Drain all complete lines buffered for this miner. + // HandleLine always returns true; disc signals clean disconnect. + while (RecvLine(m->fd, m->readBuf, line, disc)) + HandleLine(m, line, roundShareCount, + roundShareTotal, blocksFoundThisSession); + if (disc) dead.push_back(m); + } + for (Miner* m : dead) { + LogPrint("worker", "[pool] miner %s disconnected" + " (session shares: %llu, round shares: %llu)\n", + m->worker.empty() ? "(never authorized)" : m->worker.c_str(), + (unsigned long long)m->sessionShares, + (unsigned long long)m->roundShares); + sock_close(m->fd); + gMiners.erase(std::find(gMiners.begin(), gMiners.end(), m)); + delete m; + } + } + } else { + Sleep(50); + } + + // 5. Refresh GUI stats every 2s + if (GetTime() - lastStatsRefresh >= 2) { + RefreshStats(blocksFoundThisSession, roundShareTotal); + lastStatsRefresh = GetTime(); } } + + for (Miner* m : gMiners) { sock_close(m->fd); delete m; } + gMiners.clear(); } // --------------------------------------------------------------------------- -// Main Stratum accept loop — entry point called from ui.cpp and headless.cpp -// --------------------------------------------------------------------------- -// .btf Stratum listener — miners connect via rendezvous relay using the -// node's .btf address instead of a raw IP:port. Same Stratum protocol, -// same MinerThreadFn handler, works through CGNAT with no port forwarding. -// -// Keeps one listener registered per relay at all times. When a miner -// connects, a replacement listener is spawned immediately so the next -// miner doesn't have to wait. +// Accept threads // --------------------------------------------------------------------------- +struct AcceptCtx { unsigned char pk[32]; unsigned char sk[32]; std::string relay; }; -struct BtfAcceptCtx { unsigned char pk[32]; unsigned char sk[32]; string relay; }; - -// Track active listener count per relay to avoid pile-up when relay is slow -static std::map gBtfListenerCount; -static std::mutex gBtfListenerMutex; - -static void BtfStratumAcceptOneFn(void* arg) +// One persistent thread per relay. Loops: register -> wrap -> push -> repeat. +static void AcceptOneFn(void* arg) { - BtfAcceptCtx* ctx = (BtfAcceptCtx*)arg; + AcceptCtx* ctx = (AcceptCtx*)arg; unsigned char pk[32], sk[32]; memcpy(pk, ctx->pk, 32); memcpy(sk, ctx->sk, 32); - string relay = ctx->relay; + std::string relay = ctx->relay; delete ctx; - auto decrement = [&]() { - std::lock_guard lk(gBtfListenerMutex); - gBtfListenerCount[relay]--; - }; - size_t colon = relay.rfind(':'); - if (colon == string::npos) { decrement(); return; } - string strHost = relay.substr(0, colon); - int nPort = atoi(relay.substr(colon+1).c_str()); - - btf::RvSocket rv = btf::RvServiceRegister(strHost.c_str(), (unsigned short)nPort, pk); - if (fShutdown || !gPoolRunning) { if (rv != btf::RV_INVALID) btf::RvClose(rv); decrement(); return; } - if (rv == btf::RV_INVALID) { decrement(); return; } - - // Paired: spawn replacement (count transfers to new thread) - if (!fShutdown && gPoolRunning) { - BtfAcceptCtx* next = new BtfAcceptCtx(); - memcpy(next->pk, pk, 32); memcpy(next->sk, sk, 32); - next->relay = relay; - LaunchThread(BtfStratumAcceptOneFn, next); - } else { - decrement(); + if (colon == std::string::npos) { + LogPrint("pool", "[pool] AcceptOneFn: bad relay address '%s'\n", relay.c_str()); + return; } + std::string host = relay.substr(0, colon); + int port = atoi(relay.substr(colon + 1).c_str()); + + LogPrint("pool", "[pool] accept thread started for relay %s\n", relay.c_str()); + + while (!fShutdown && gPoolRunning) { + // Blocks until a miner connects to us at this relay + btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk); - btf_socket_t hSocket = btf::BtfServiceWrap(rv, sk); - if (hSocket == INVALID_SOCKET) return; + if (fShutdown || !gPoolRunning) { + if (rv != btf::RV_INVALID) btf::RvClose(rv); + break; + } + if (rv == btf::RV_INVALID) { + LogPrint("pool", "[pool] relay %s: RvServiceRegister failed" + " -- backing off 30s before retry\n", relay.c_str()); + for (int i = 0; i < 30 && !fShutdown && gPoolRunning; i++) Sleep(1000); + continue; + } - printf("Stratum/.btf: miner connected via %s\n", relay.c_str()); - Miner* m = new Miner(hSocket); - { std::lock_guard lk(gMinersMutex); gMiners.push_back(m); } - LaunchThread(MinerThreadFn, m); + // Echan handshake -- blocking but fast (local + relay round-trip) + btf_socket_t fd = btf::BtfServiceWrap(rv, sk); + if (fd == INVALID_SOCKET) { + LogPrint("pool", "[pool] relay %s: BtfServiceWrap failed (echan handshake)" + " -- miner dropped, will accept next\n", relay.c_str()); + continue; + } + + LogPrint("worker", "[pool] miner arrived via relay %s -- pushing to event loop\n", + relay.c_str()); + PushReady(fd); + // Loop immediately: register again so the next miner can connect + } + + LogPrint("pool", "[pool] accept thread stopped for relay %s\n", relay.c_str()); } -static void BtfStratumAcceptFn(void*) +// Spawns one AcceptOneFn per relay. Re-polls relay list every 60s to pick up +// newly discovered relays. Uses a set to avoid spawning duplicate threads. +static void AcceptLoopFn(void*) { - printf("Stratum/.btf: starting \xe2\x80\x94 miners can connect via %s\n", - BtfLocalAddress().c_str()); + LogPrint("pool", "[pool] AcceptLoopFn started -- miners can connect via %s\n", + BtfLocalAddress().c_str()); + // Wait for our identity to be ready unsigned char pk[32], sk[32]; + int waitSecs = 0; while (!BtfGetIdentity(pk, sk)) { if (fShutdown || !gPoolRunning) return; + if (waitSecs == 0) + LogPrint("pool", "[pool] waiting for .btf identity...\n"); + waitSecs++; Sleep(2000); } + LogPrint("pool", "[pool] .btf identity ready after %ds\n", waitSecs * 2); + + std::set spawned; // relays we've already started a thread for while (!fShutdown && gPoolRunning) { vector relays = BtfAllRelays(); - if (relays.empty()) { Sleep(5000); continue; } - - // Spawn one listener per relay, only if none already active - for (const string& relay : relays) { - std::lock_guard lk(gBtfListenerMutex); - if (gBtfListenerCount[relay] == 0) { - gBtfListenerCount[relay]++; - BtfAcceptCtx* ctx = new BtfAcceptCtx(); + if (relays.empty()) { + LogPrint("pool", "[pool] no relays available -- miners cannot connect yet\n"); + } else { + for (const string& relay : relays) { + if (spawned.count(relay)) continue; // already running + AcceptCtx* ctx = new AcceptCtx(); memcpy(ctx->pk, pk, 32); memcpy(ctx->sk, sk, 32); ctx->relay = relay; - LaunchThread(BtfStratumAcceptOneFn, ctx); + uintptr_t tid = _beginthread(AcceptOneFn, 0, ctx); + if (tid == (uintptr_t)-1) { + delete ctx; + LogPrint("pool", "[pool] failed to spawn accept thread for %s\n", + relay.c_str()); + } else { + spawned.insert(relay); + LogPrint("pool", "[pool] accept thread spawned for relay %s" + " (%zu total)\n", relay.c_str(), spawned.size()); + } } } - for (int i = 0; i < 30 && !fShutdown && gPoolRunning; i++) Sleep(1000); + // Re-poll every 60s -- newly discovered relays get picked up + for (int i = 0; i < 60 && !fShutdown && gPoolRunning; i++) Sleep(1000); } } + +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- void ThreadRPCServer(void*) { gPoolRunning = true; - printf("Bitflash pool server starting\n"); - printf(" Stratum .btf: %s (no port forwarding needed)\n", - BtfLocalAddress().c_str()); - printf(" Mode: operator\n"); - if (RebuildJob()) - printf("Stratum: initial job seeded height=%d\n", nBestHeight + 1); + LogPrint("pool", "[pool] ============================================\n"); + LogPrint("pool", "[pool] Pool server starting\n"); + LogPrint("pool", "[pool] .btf address: %s\n", BtfLocalAddress().c_str()); + LogPrint("pool", "[pool] fee: %.2f%%\n", dPoolFeePercent); + + if (RebuildJob()) { + LogPrint("pool", "[pool] initial job %s seeded at height %d\n", + gCurrentJob.jobId.c_str(), nBestHeight + 1); + } else { + LogPrint("pool", "[pool] no chain tip yet -- job will build on first block\n"); + } + + LoadPendingPayouts(); + + _beginthread(AcceptLoopFn, 0, NULL); + _beginthread(ThreadBtfPoolAnnouncer, 0, NULL); + _beginthread(PayoutThreadFn, 0, NULL); - LaunchThread(JobWatcherFn, NULL); - LaunchThread(BtfStratumAcceptFn, NULL); - LaunchThread(ThreadBtfPoolAnnouncer, NULL); + PoolEventLoop(); // blocks until gPoolRunning = false or fShutdown - while (!fShutdown && gPoolRunning) - Sleep(1000); - printf("Pool server stopped\n"); + LogPrint("pool", "[pool] pool server stopped\n"); + LogPrint("pool", "[pool] ============================================\n"); + gPoolRunning = false; } diff --git a/src/ui.rc b/src/ui.rc deleted file mode 100644 index ec8b142..0000000 --- a/src/ui.rc +++ /dev/null @@ -1,14 +0,0 @@ -bitcoin ICON "rc/bitcoin.ico" - -#include "wx/msw/wx.rc" - -check ICON "rc/check.ico" -send16 BITMAP "rc/send16.bmp" -send16mask BITMAP "rc/send16mask.bmp" -send16masknoshadow BITMAP "rc/send16masknoshadow.bmp" -send20 BITMAP "rc/send20.bmp" -send20mask BITMAP "rc/send20mask.bmp" -addressbook16 BITMAP "rc/addressbook16.bmp" -addressbook16mask BITMAP "rc/addressbook16mask.bmp" -addressbook20 BITMAP "rc/addressbook20.bmp" -addressbook20mask BITMAP "rc/addressbook20mask.bmp" diff --git a/src/uiproject.fbp b/src/uiproject.fbp deleted file mode 100644 index 57a2950..0000000 --- a/src/uiproject.fbp +++ /dev/null @@ -1,11825 +0,0 @@ - - - - - - C++ - 1 - UTF-8 - connect - uibase - 1000 - none - 0 - - - . - - 1 - 0 - 0 - - wxSYS_COLOUR_BTNFACE - - - 1 - - - - 0 - wxID_MAINFRAME - - - CMainFrameBase - - 705,484 - wxDEFAULT_FRAME_STYLE|wxRESIZE_BORDER - - Bitcoin - - - - wxTAB_TRAVERSAL - 1 - - - - OnClose - - - - - OnIdle - - - - - - - - - - - - OnMouseEvents - - OnPaint - - - - - - - - 240,240,240 - - 1 - - - 0 - wxID_ANY - MyMenuBar - - - m_menubar - protected - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - &File - m_menuFile - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - E&xit - m_menuFileExit - none - - - OnMenuFileExit - - - - - &Options - m_menuOptions - public - - - 0 - 1 - - wxID_OPTIONSGENERATEBITCOINS - wxITEM_CHECK - &Generate Coins - m_menuOptionsGenerateBitcoins - none - - - OnMenuOptionsGenerate - - - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - &Options... - m_menuOptionsOptions - none - - - OnMenuOptionsOptions - - - - - &Help - m_menuHelp - protected - - - 0 - 1 - - wxID_ANY - wxITEM_NORMAL - &About... - m_menuHelpAbout - none - - - OnMenuHelpAbout - - - - - - - 20,20 - - 1 - - ,90,90,-1,70,0 - 0 - wxID_ANY - - - - m_toolBar - 1 - protected - - 1 - -1,-1 - wxTB_FLAT|wxTB_HORZ_TEXT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - send20; Load From Resource - wxID_BUTTONSEND - wxITEM_NORMAL - &Send Coins - m_tool1 - - - - OnButtonSend - - - - - - addressbook20; Load From Resource - wxID_BUTTONRECEIVE - wxITEM_NORMAL - &Address Book - m_tool2 - - - - OnButtonAddressBook - - - - - - - 240,240,240 - - 1 - - 1 - - 0 - wxID_ANY - - - m_statusBar - protected - - - wxST_SIZEGRIP - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer2 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - 2 - protected - 0 - - - - 5 - wxEXPAND|wxRIGHT|wxLEFT - 0 - - - bSizer85 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Your Bitcoin Address: - - - m_staticText32 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - wxSYS_COLOUR_MENU - - 1 - - - 0 - wxID_TEXTCTRLADDRESS - -1,-1 - 0 - -1,-1 - m_textCtrlAddress - protected - - 250,-1 - wxTE_READONLY - - - - - - - - - - OnKeyDown - - - - - - - - - - - OnMouseEventsAddress - - - - - - OnSetFocusAddress - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONCOPY - &Copy to Clipboard - - - m_buttonCopy - protected - - - wxBU_EXACTFIT - - - - - - OnButtonCopy - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxRIGHT - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONCHANGE - C&hange... - - - m_button91 - protected - - - - - - - - - OnButtonChange - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - 0 - protected - 0 - - - - - - 5 - wxEXPAND - 0 - - - bSizer3 - wxHORIZONTAL - none - - 5 - wxEXPAND|wxALIGN_BOTTOM|wxALL - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_panel14 - protected - - - - - - - wxTAB_TRAVERSAL - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer66 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Balance: - - - m_staticText41 - protected - - -1,15 - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - 240,240,240 - - 1 - - ,90,90,8,70,0 - 0 - wxID_ANY - - - - m_staticTextBalance - protected - - 120,15 - wxALIGN_RIGHT|wxST_NO_AUTORESIZE - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - 0 - protected - 0 - - - - 5 - wxALIGN_BOTTOM|wxTOP|wxRIGHT|wxLEFT - 0 - - - " All" " Sent" " Received" " In Progress" - - 1 - - - 1 - wxID_ANY - - - m_choiceFilter - protected - - 0 - 110,-1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 1 - - - - - 1 - - - 0 - wxID_ANY - - - m_notebook - protected - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - All Transactions - 0 - - - - 1 - - - 0 - wxID_ANY - - - m_panel7 - protected - - - - - - - wxTAB_TRAVERSAL - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer157 - wxVERTICAL - none - - 5 - wxEXPAND|wxALL - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_listCtrl - public - - - wxLC_NO_SORT_HEADER|wxLC_REPORT|wxLC_SORT_DESCENDING - - - - - wxALWAYS_SHOW_SB - - - - - - - - - - - - - - - OnListColBeginDrag - - - - - - - - - OnListItemActivatedAllTransactions - - - - - - - - - - - - - OnPaintListCtrl - - - - - - - - - - - - - - - 5 - wxEXPAND - 1 - - - bSizer_TabsForFutureUse - wxVERTICAL - none - - 5 - wxEXPAND | wxALL - 1 - - - - 1 - - - 1 - wxID_ANY - - - m_panel9 - protected - - - - - - - wxTAB_TRAVERSAL - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer159 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_listCtrlEscrows - public - - - wxLC_NO_SORT_HEADER|wxLC_REPORT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - - - 1 - - - 1 - wxID_ANY - - - m_panel8 - protected - - - - - - - wxTAB_TRAVERSAL - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer158 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_listCtrlOrdersSent - public - - - wxLC_NO_SORT_HEADER|wxLC_REPORT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - OnListItemActivatedOrdersSent - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - - - 1 - - - 1 - wxID_ANY - - - m_panel10 - protected - - - - - - - wxTAB_TRAVERSAL - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer160 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_listCtrlProductsSent - public - - - wxLC_NO_SORT_HEADER|wxLC_REPORT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - OnListItemActivatedProductsSent - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND | wxALL - 1 - - - - 1 - - - 1 - wxID_ANY - - - m_panel11 - protected - - - - - - - wxTAB_TRAVERSAL - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer161 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_listCtrlOrdersReceived - public - - - wxLC_NO_SORT_HEADER|wxLC_REPORT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - OnListItemActivatedOrdersReceived - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CTxDetailsDialogBase - - 620,450 - wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER - - Transaction Details - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer64 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - - bSizer66 - wxVERTICAL - none - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_htmlWin - protected - - - wxHW_SCROLLBAR_AUTO - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_RIGHT - 0 - - - bSizer65 - wxVERTICAL - none - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_OK - OK - - - m_buttonOK - protected - - 85,25 - - - - - - - OnButtonOK - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - COptionsDialogBase - - 500,261 - wxDEFAULT_DIALOG_STYLE - - Options - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer55 - wxVERTICAL - none - - 5 - wxEXPAND|wxLEFT - 1 - - - bSizer57 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - 20 - protected - 0 - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - - - 1 - - - 0 - wxID_ANY - Optional transaction fee you give to the nodes that process your transactions. - - - m_staticText32 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer56 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Transaction fee: - - - m_staticText31 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_TRANSACTIONFEE - - 0 - - m_textCtrlTransactionFee - protected - - 70,-1 - - - - - - - - - - - - - OnKillFocusTransactionFee - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_RIGHT - 0 - - - bSizer58 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_OK - OK - - - m_buttonOK - protected - - 85,25 - - - - - - - OnButtonOK - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - protected - - -1,-1 - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CAboutDialogBase - - 507,298 - wxDEFAULT_DIALOG_STYLE - - About Bitcoin - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer60 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - - bSizer62 - wxHORIZONTAL - none - - 5 - wxEXPAND - 0 - - 0 - protected - 60 - - - - 5 - wxEXPAND - 1 - - - bSizer63 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - 50 - protected - 0 - - - - 5 - wxEXPAND - 0 - - - bSizer64 - wxHORIZONTAL - none - - 5 - wxALIGN_BOTTOM|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - Tahoma,90,92,10,74,0 - 0 - wxID_ANY - Bitcoin - - - m_staticText40 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_BOTTOM|wxTOP|wxBOTTOM|wxRIGHT - 0 - - - - 1 - - Tahoma,90,90,10,74,0 - 0 - wxID_ANY - version - - - m_staticTextVersion - public - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - 4 - protected - 0 - - - - 5 - wxALL - 0 - - - - 1 - - - 0 - wxID_ANY - Copyright © 2009 Satoshi Nakamoto. This is experimental software. Do not rely on it for actual financial transactions. Distributed under the MIT/X11 software license, see the accompanying file license.txt or http://www.opensource.org/licenses/mit-license.php. This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit (http://www.openssl.org/) and cryptographic software written by Eric Young (eay@cryptsoft.com). - - - m_staticTextMain - protected - - - - - - - - - 400 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - - - - - 5 - wxALIGN_RIGHT|wxEXPAND - 0 - - - bSizer61 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_OK - OK - - - m_buttonOK - protected - - 85,25 - - - - - - - OnButtonOK - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CSendDialogBase - - 675,338 - wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER - - Send Coins - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer21 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - 5 - protected - 0 - - - - 5 - wxEXPAND|wxLEFT - 0 - - 2 - wxBOTH - 1 - - 0 - - fgSizer1 - wxFLEX_GROWMODE_SPECIFIED - none - 3 - 0 - - 5 - wxEXPAND - 0 - - 0 - protected - 0 - - - - 5 - wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Enter the recipient's IP address (e.g. 123.45.6.7) for online transfer with comments and confirmation, or bitcoin address (e.g. 1NS17iag9jJgTHD1VXjvLCEnZuQ3rJED9L) if recipient is not online. - - - m_staticText14 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND|wxLEFT - 1 - - 70,-1 - bSizer47 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - check; Load From Icon Resource [-1; -1] - - 1 - - - 0 - wxID_ANY - - - m_bitmapCheckMark - protected - - 16,16 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Pay &To: - - - m_staticText36 - protected - - -1,-1 - wxALIGN_RIGHT - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND|wxRIGHT - 1 - - - bSizer19 - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 1 - - - - 1 - - - 0 - wxID_TEXTCTRLPAYTO - - 0 - - m_textCtrlAddress - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - OnTextAddress - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONPASTE - &Paste - - - m_buttonPaste - protected - - -1,-1 - wxBU_EXACTFIT - - - - - - OnButtonPaste - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONADDRESSBOOK - Address &Book... - - - m_buttonAddress - protected - - - - - - - - - OnButtonAddressBook - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT|wxALIGN_RIGHT - 0 - - - - 1 - - - 0 - wxID_ANY - &Amount: - - - m_staticText19 - protected - - -1,-1 - wxALIGN_RIGHT - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - - - 1 - - ,90,90,-1,70,0 - 0 - wxID_TEXTCTRLAMOUNT - - 20 - - m_textCtrlAmount - protected - - 145,-1 - - - - - - - - - - - OnKeyDown - - OnKillFocusAmount - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - T&ransfer: - - - m_staticText20 - protected - - -1,-1 - wxALIGN_RIGHT - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALL - 0 - - - " Standard" - - 1 - - - 0 - wxID_CHOICETRANSFERTYPE - - - m_choiceTransferType - protected - - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer672 - wxHORIZONTAL - none - - 5 - wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT - 1 - - - bSizer681 - wxVERTICAL - none - - 5 - wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - &From: - - - m_staticTextFrom - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxLEFT|wxEXPAND - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - - m_textCtrlFrom - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 1 - - - bSizer67 - wxHORIZONTAL - none - - 5 - wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT - 1 - - - bSizer68 - wxVERTICAL - none - - 5 - wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - &Message: - - - m_staticTextMessage - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND|wxLEFT - 1 - - - - 1 - - - 0 - wxID_ANY - - 0 - - m_textCtrlMessage - protected - - - wxTE_MULTILINE - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer23 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 0 - - - - 0 - 1 - - ,90,90,-1,70,0 - 0 - wxID_BUTTONSEND - &Send - - 85,25 - m_buttonSend - protected - - -1,-1 - - - - - - - OnButtonSend - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - protected - - -1,-1 - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CSendingDialogBase - - 442,151 - wxDEFAULT_DIALOG_STYLE - - Sending... - - - - - - - - OnClose - - - - - - - - - - - - - - - - - - - - OnPaint - - - - - - - - - bSizer68 - wxVERTICAL - none - - 8 - wxALIGN_CENTER_VERTICAL|wxEXPAND|wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - - - - m_staticTextSending - protected - - -1,14 - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 10 - wxEXPAND|wxRIGHT|wxLEFT - 1 - - wxSYS_COLOUR_BTNFACE - - 1 - - - 0 - wxID_ANY - - 0 - - m_textCtrlStatus - protected - - - wxTE_CENTRE|wxTE_MULTILINE|wxTE_NO_VSCROLL|wxTE_READONLY - - - Connecting... - - - wxNO_BORDER - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer69 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 0 - - - - 0 - 0 - - - 0 - wxID_ANY - OK - - 85,25 - m_buttonOK - protected - - - - - - - - - OnButtonOK - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - protected - - -1,-1 - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CYourAddressDialogBase - - 610,390 - wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER - - Your Bitcoin Address - - - - - - - - OnClose - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer68 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - 5 - protected - 0 - - - - 5 - wxALL - 0 - - - - 1 - - - 0 - wxID_ANY - These are your Bitcoin addresses for receiving payments. You may want to give a different one to each sender so you can keep track of who is paying you. - - - m_staticText45 - protected - - - - - - - - - 600 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_LISTCTRL - - - m_listCtrl - protected - - - wxLC_NO_SORT_HEADER|wxLC_REPORT|wxLC_SORT_ASCENDING - - - - - - - - - - - - - - - - - - - - - - - - - - - OnListEndLabelEdit - - OnListItemActivated - - - - - OnListItemSelected - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer69 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONRENAME - &Rename... - - 85,25 - m_buttonRename - protected - - - - - - - - - OnButtonRename - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONNEW - &New Address... - - 110,25 - m_buttonNew - protected - - -1,-1 - - - - - - - OnButtonNew - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONCOPY - &Copy to Clipboard - - 120,25 - m_buttonCopy - protected - - -1,-1 - - - - - - - OnButtonCopy - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_OK - OK - - 85,25 - m_buttonOK - protected - - - - - - - - - OnButtonOK - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - protected - - -1,-1 - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CAddressBookDialogBase - - 610,390 - wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER - - Address Book - - - - - - - - OnClose - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer68 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - 5 - protected - 0 - - - - 5 - wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 1 - wxID_ANY - Bitcoin Address - - - m_staticText55 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_LISTCTRL - - - m_listCtrl - protected - - - wxLC_NO_SORT_HEADER|wxLC_REPORT|wxLC_SORT_ASCENDING - - - - - - - - - - - - - - - - - - - - - - - - - - - OnListEndLabelEdit - - OnListItemActivated - - - - - OnListItemSelected - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - - bSizer69 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONEDIT - &Edit... - - 85,25 - m_buttonEdit - protected - - - - - - - - - OnButtonEdit - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONNEW - &New Address... - - 110,25 - m_buttonNew - protected - - - - - - - - - OnButtonNew - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONDELETE - &Delete - - 85,25 - m_buttonDelete - protected - - - - - - - - - OnButtonDelete - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_OK - OK - - 85,25 - m_buttonOK - protected - - -1,-1 - - - - - - - OnButtonOK - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - public - - -1,-1 - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CProductsDialogBase - - 708,535 - wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER - - Marketplace - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer22 - wxVERTICAL - none - - 5 - wxEXPAND|wxTOP|wxBOTTOM|wxRIGHT - 0 - - - bSizer23 - wxHORIZONTAL - none - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - "(Any Category)" - - 1 - - - 0 - wxID_ANY - - - m_comboBoxCategory - protected - - 150,-1 - - - - (Any Category) - - - - - OnCombobox - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 1 - - - - 1 - - - 0 - wxID_ANY - - 0 - - m_textCtrlSearch - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 0 - 1 - - - 0 - wxID_ANY - &Search - - - m_buttonSearch - protected - - - - - - - - - OnButtonSearch - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_listCtrl - protected - - - wxLC_NO_SORT_HEADER|wxLC_REPORT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - OnListItemActivated - - - - - - - - - - - - - - - - - - - - - - - - - wxSYS_COLOUR_MENU - - - 1 - - - - 0 - wxID_ANY - - - CEditProductDialogBase - - 660,640 - wxDEFAULT_FRAME_STYLE|wxRESIZE_BORDER - - Edit Product - - - - wxTAB_TRAVERSAL - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer20 - wxVERTICAL - none - - 5 - wxEXPAND|wxALL - 1 - - wxSYS_COLOUR_WINDOW - - 1 - - - 0 - wxID_ANY - - - m_scrolledWindow - protected - - 5 - 5 - - - - - - wxHSCROLL|wxTAB_TRAVERSAL|wxVSCROLL - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer21 - wxVERTICAL - none - - 5 - wxEXPAND|wxTOP|wxRIGHT|wxLEFT - 0 - - 2 - wxBOTH - 1 - - 0 - - fgSizer8 - wxFLEX_GROWMODE_SPECIFIED - none - 0 - 0 - - 5 - wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Category - - - m_staticText106 - protected - - - wxALIGN_RIGHT - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - - 1 - - - 0 - wxID_ANY - - 180,-1 - m_comboBoxCategory - protected - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Title - - - m_staticText108 - protected - - - wxALIGN_RIGHT - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - 0 - - m_textCtrlTitle - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Price - - - m_staticText107 - protected - - - wxALIGN_RIGHT - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 105,-1 - m_textCtrlPrice - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Page 1: Description - - - m_staticText22 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - -1,170 - m_textCtrlDescription - protected - - - wxTE_MULTILINE - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Page 2: Order Form - - - m_staticText23 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND|wxALL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - -1,120 - m_textCtrlInstructions - protected - - - wxTE_MULTILINE - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 0 - - 3 - wxBOTH - 1 - - 0 - - fgSizer5 - wxFLEX_GROWMODE_SPECIFIED - public - 0 - 0 - - 5 - wxALIGN_CENTER_VERTICAL|wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Label - - - m_staticText24 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Comma separated list of choices, or leave blank for text field - - - m_staticText25 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel0 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField0 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxRIGHT|wxLEFT|wxALIGN_CENTER_VERTICAL - 0 - - - - 0 - 1 - - - 0 - wxID_DEL0 - Delete - - -1,-1 - m_buttonDel0 - protected - - 60,20 - - - - - - - OnButtonDel0 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel1 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField1 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL1 - Delete - - -1,-1 - m_buttonDel1 - protected - - 60,20 - - - - - - - OnButtonDel1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel2 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField2 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL2 - Delete - - -1,-1 - m_buttonDel2 - protected - - 60,20 - - - - - - - OnButtonDel2 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel3 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField3 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL3 - Delete - - -1,-1 - m_buttonDel3 - protected - - 60,20 - - - - - - - OnButtonDel3 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel4 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField4 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL4 - Delete - - -1,-1 - m_buttonDel4 - protected - - 60,20 - - - - - - - OnButtonDel4 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel5 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField5 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL5 - Delete - - -1,-1 - m_buttonDel5 - protected - - 60,20 - - - - - - - OnButtonDel5 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel6 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField6 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL6 - Delete - - -1,-1 - m_buttonDel6 - protected - - 60,20 - - - - - - - OnButtonDel6 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel7 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField7 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL7 - Delete - - -1,-1 - m_buttonDel7 - protected - - 60,20 - - - - - - - OnButtonDel7 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel8 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField8 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL8 - Delete - - -1,-1 - m_buttonDel8 - protected - - 60,20 - - - - - - - OnButtonDel8 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel9 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField9 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL9 - Delete - - -1,-1 - m_buttonDel9 - protected - - 60,20 - - - - - - - OnButtonDel9 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel10 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField10 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL10 - Delete - - -1,-1 - m_buttonDel10 - protected - - 60,20 - - - - - - - OnButtonDel10 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel11 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField11 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL11 - Delete - - -1,-1 - m_buttonDel11 - protected - - 60,20 - - - - - - - OnButtonDel11 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel12 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField12 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL12 - Delete - - -1,-1 - m_buttonDel12 - protected - - 60,20 - - - - - - - OnButtonDel12 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel13 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField13 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL13 - Delete - - -1,-1 - m_buttonDel13 - protected - - 60,20 - - - - - - - OnButtonDel13 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel14 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField14 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL14 - Delete - - -1,-1 - m_buttonDel14 - protected - - 60,20 - - - - - - - OnButtonDel14 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel15 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField15 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL15 - Delete - - -1,-1 - m_buttonDel15 - protected - - 60,20 - - - - - - - OnButtonDel15 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel16 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField16 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL16 - Delete - - -1,-1 - m_buttonDel16 - protected - - 60,20 - - - - - - - OnButtonDel16 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel17 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField17 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL17 - Delete - - -1,-1 - m_buttonDel17 - protected - - 60,20 - - - - - - - OnButtonDel17 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel18 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField18 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL18 - Delete - - -1,-1 - m_buttonDel18 - protected - - 60,20 - - - - - - - OnButtonDel18 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - - 0 - 150,-1 - m_textCtrlLabel19 - protected - - - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_VERTICAL - 0 - - - - 1 - - - 0 - wxID_ANY - -1,-1 - 0 - -1,-1 - m_textCtrlField19 - protected - - -1,-1 - - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT - 0 - - - - 0 - 1 - - - 0 - wxID_DEL19 - Delete - - -1,-1 - m_buttonDel19 - protected - - 60,20 - - - - - - - OnButtonDel19 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_HORIZONTAL - 0 - - - bSizer25 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_ANY - &Add Field - - - m_buttonAddField - protected - - - - - - - - - OnButtonAddField - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_RIGHT - 0 - - - bSizer26 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONSEND - &Send - - 85,25 - m_buttonOK - protected - - - - - - - - - OnButtonSend - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONPREVIEW - &Preview - - 85,25 - m_buttonPreview - protected - - - - - - - - - OnButtonPreview - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - protected - - - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - wxSYS_COLOUR_MENU - - - 1 - - - - 0 - wxID_ANY - - - CViewProductDialogBase - - 630,520 - wxDEFAULT_FRAME_STYLE|wxRESIZE_BORDER - - Order Form - - - - wxTAB_TRAVERSAL - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer20 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - - bSizer116 - wxHORIZONTAL - none - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 1 - wxID_ANY - - - m_htmlWinReviews - protected - - - wxHW_SCROLLBAR_AUTO - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND|wxALL - 1 - - wxSYS_COLOUR_WINDOW - - 1 - - - 0 - wxID_ANY - - - m_scrolledWindow - protected - - 5 - 5 - - - - - - wxHSCROLL|wxTAB_TRAVERSAL|wxVSCROLL - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer21 - wxVERTICAL - none - - 5 - wxEXPAND - 0 - - - - 1 - - - 0 - wxID_ANY - - -1,-1 - m_richTextHeading - protected - - -1,50 - wxTE_READONLY - - - - - wxNO_BORDER - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND - 0 - - - - 1 - - - 0 - wxID_ANY - Order Form instructions here multiple lines 1 2 3 4 5 6 - - - m_staticTextInstructions - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_CENTER_HORIZONTAL - 0 - - - bSizer25 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONSAMPLE - &Submit - - -1,-1 - m_buttonSubmitForm - protected - - - - - - - - - OnButtonSubmitForm - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL2 - Cancel - - - m_buttonCancelForm - protected - - - - - - - - - OnButtonCancelForm - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_RIGHT - 0 - - - bSizer26 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - - - 0 - 0 - - - 0 - wxID_BUTTONBACK - < &Back - - 85,25 - m_buttonBack - protected - - - - - - - - - OnButtonBack - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_BUTTONNEXT - &Next > - - 85,25 - m_buttonNext - protected - - - - - - - - - OnButtonNext - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - protected - - - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - wxSYS_COLOUR_MENU - - - 1 - - - - 0 - wxID_ANY - - - CViewOrderDialogBase - - 630,520 - wxDEFAULT_FRAME_STYLE|wxRESIZE_BORDER - - View Order - - - - wxTAB_TRAVERSAL - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer20 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - - bSizer116 - wxHORIZONTAL - none - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_htmlWin - protected - - - wxHW_SCROLLBAR_AUTO - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_RIGHT - 0 - - - bSizer26 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_OK - OK - - 85,25 - m_buttonOK - protected - - - - - - - - - OnButtonOK - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - wxSYS_COLOUR_MENU - - - 1 - - - - 0 - wxID_ANY - - - CEditReviewDialogBase - - 630,440 - wxDEFAULT_FRAME_STYLE|wxRESIZE_BORDER - - Enter Review - - - - wxTAB_TRAVERSAL - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer112 - wxVERTICAL - none - - 5 - - 0 - - 3 - protected - 0 - - - - 5 - wxALL|wxEXPAND - 0 - - - - 1 - - - 0 - wxID_ANY - - - - m_staticTextSeller - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - - 0 - - 3 - protected - 0 - - - - 5 - wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Rating - - - m_staticText110 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - " 1 star" " 2 stars" " 3 stars" " 4 stars" " 5 stars" - - 1 - - - 0 - wxID_ANY - - - m_choiceStars - protected - - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - Review - - - m_staticText43 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND - 1 - - - - 1 - - - 0 - wxID_ANY - - 0 - - m_textCtrlReview - protected - - - wxTE_MULTILINE - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALIGN_RIGHT - 0 - - - bSizer113 - wxHORIZONTAL - none - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_SUBMIT - &Submit - - 85,25 - m_buttonSubmit - protected - - - - - - - - - OnButtonSubmit - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - protected - - - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - wxSYS_COLOUR_BTNFACE - - - 1 - - - - 0 - wxID_ANY - - - CPokerLobbyDialogBase - - 586,457 - wxDEFAULT_FRAME_STYLE - - Poker Lobby - - - - wxTAB_TRAVERSAL - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer156 - wxHORIZONTAL - none - - 5 - wxEXPAND|wxTOP|wxBOTTOM|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - - 130,-1 - m_treeCtrl - protected - - - wxTR_HAS_BUTTONS|wxTR_HIDE_ROOT|wxTR_LINES_AT_ROOT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - OnTreeSelChanged - - - - - - - - 5 - wxEXPAND - 1 - - - bSizer172 - wxVERTICAL - none - - 5 - wxEXPAND|wxALL - 1 - - - - 1 - - - 0 - wxID_ANY - - - m_listCtrl - protected - - - wxLC_NO_SORT_HEADER|wxLC_REPORT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - OnListItemActivated - - - - - OnListItemSelected - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_OPENNEWTABLE - &Open New Table - - - m_buttonNewTable - protected - - - - - - - - - OnButtonNewTable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CPokerDialogBase - - 806,550 - wxDEFAULT_FRAME_STYLE|wxFRAME_NO_TASKBAR - - Poker - - - - wxFULL_REPAINT_ON_RESIZE|wxTAB_TRAVERSAL - 1 - - - - OnClose - - - - - - - - - - - - - - - - - OnMouseEvents - - OnPaint - - - - - OnSize - - - - bSizer174 - wxVERTICAL - none - - 5 - wxALL - 0 - - - 0 - - 1 - - - 0 - wxID_ANY - Deal Me Out - - - m_checkSitOut - public - - - - - - - - - - OnCheckSitOut - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_DEALHAND - &Deal Hand - - - m_buttonDealHand - protected - - 150,25 - - - - - - - OnButtonDealHand - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_FOLD - &Fold - - - m_buttonFold - protected - - 80,25 - - - - - - - OnButtonFold - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CALL - &Call - - - m_buttonCall - protected - - 80,25 - - - - - - - OnButtonCall - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_RAISE - &Raise - - - m_buttonRaise - protected - - 80,25 - - - - - - - OnButtonRaise - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_LEAVETABLE - &Leave Table - - - m_buttonLeaveTable - protected - - 90,25 - - - - - - - OnButtonLeaveTable - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 1 - - - 0 - wxID_DITCHPLAYER - - 0 - - m_textDitchPlayer - protected - - 45,-1 - wxTE_PROCESS_ENTER - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - OnDitchPlayer - - - - - - - 5 - wxALL - 0 - - - 0 - - 1 - - - 0 - wxID_ANY - FOLD - - - m_checkPreFold - public - - 100,-1 - - - - - - - - OnCheckPreFold - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - 0 - - 1 - - - 0 - wxID_ANY - CALL - - - m_checkPreCall - public - - 100,-1 - - - - - - - - OnCheckPreCall - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - 0 - - 1 - - - 0 - wxID_ANY - CALL ANY - - - m_checkPreCallAny - public - - 100,-1 - - - - - - - - OnCheckPreCallAny - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - 0 - - 1 - - - 0 - wxID_ANY - RAISE - - - m_checkPreRaise - public - - 100,-1 - - - - - - - - OnCheckPreRaise - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - 0 - - 1 - - - 0 - wxID_ANY - RAISE ANY - - - m_checkPreRaiseAny - public - - 100,-1 - - - - - - - - OnCheckPreRaiseAny - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - 1 - - 0 - wxID_ANY - - - m_statusBar - public - - - wxST_SIZEGRIP - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - 0 - wxID_ANY - - - CGetTextFromUserDialogBase - - 403,138 - wxDEFAULT_DIALOG_STYLE - - - - - - - - - - OnClose - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bSizer79 - wxVERTICAL - none - - 10 - wxEXPAND|wxALL - 1 - - - bSizer81 - wxVERTICAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 0 - wxID_ANY - - - - m_staticTextMessage1 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_HORIZONTAL - 0 - - - - 1 - - - 0 - wxID_TEXTCTRL - - 0 - - m_textCtrl1 - protected - - - wxTE_PROCESS_ENTER - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxTOP|wxRIGHT|wxLEFT - 0 - - - - 1 - - - 1 - wxID_ANY - - - - m_staticTextMessage2 - protected - - - - - - - - - -1 - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL|wxEXPAND|wxALIGN_CENTER_HORIZONTAL - 0 - - - - 1 - - - 1 - wxID_TEXTCTRL - - 0 - - m_textCtrl2 - protected - - - wxTE_PROCESS_ENTER - - - - - - - - - - OnKeyDown - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - - - 5 - wxEXPAND - 0 - - - bSizer80 - wxHORIZONTAL - none - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_OK - OK - - 85,25 - m_buttonOK - protected - - -1,-1 - - - - - - - OnButtonOK - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 - wxALL - 0 - - - - 0 - 1 - - - 0 - wxID_CANCEL - Cancel - - 85,25 - m_buttonCancel - protected - - - - - - - - - OnButtonCancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/util.cpp b/src/util.cpp index 7f56530..68bd18e 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -8,6 +8,99 @@ bool fDebug = false; +// --------------------------------------------------------------------------- +// Categorized, burst-collapsing logging -- see the LogPrint doc comment in +// util.h for the problem this solves. +// --------------------------------------------------------------------------- + +// Categories that explain the pool/worker/payout pipeline are on unconditionally: +// silence there is exactly the "worker stuck, nothing then happens" complaint +// this exists to fix, so it can't be opted out of by forgetting -debug. +// "net" and "nostr" cover the per-address connection/discovery chatter that +// used to dominate debug.log; they're gated behind fDebug like the printf() +// calls they replace, so default output stays quiet unless requested. +static bool LogCategoryDefaultOn(const char* category) +{ + return strcmp(category, "pool") == 0 || + strcmp(category, "worker") == 0 || + strcmp(category, "payout") == 0; +} + +bool LogAcceptsCategory(const char* category) +{ + if (LogCategoryDefaultOn(category)) + return true; + return fDebug; +} + +// Only collapse bursts from the exact same call site within this window; +// a different LogPrint() call (different file/line) is never merged with +// this one, so unrelated messages never get mixed into one summary. +static const int64 DEDUP_WINDOW_SECS = 3; + +struct LogDedupState { + int64 windowStart; // GetTime() when the current window opened + int suppressed; // calls swallowed so far this window + std::string lastMsg; // most recent message text, for the summary line +}; + +static std::map g_logDedup; +static CCriticalSection cs_logDedup; + +void LogPrintDedup(const char* category, const char* file, int line, const std::string& msgIn) +{ + // Call sites follow the existing printf() convention of ending their + // format string with \n; strip it here so the single \n this function + // adds below doesn't turn into a blank line. + std::string msg = msgIn; + while (!msg.empty() && (msg.back() == '\n' || msg.back() == '\r')) + msg.pop_back(); + + char key[512]; + my_snprintf(key, sizeof(key), "%s|%s:%d", category, file, line); + + int64 now = GetTime(); + bool fEmitNow = false; + int suppressedToFlush = 0; + std::string flushedMsg; + + CRITICAL_BLOCK(cs_logDedup) + { + std::map::iterator it = g_logDedup.find(key); + if (it == g_logDedup.end()) + { + // First time this call site has fired (or its last window is long + // gone) -- print it directly and open a fresh window. + g_logDedup[key] = LogDedupState{now, 0, msg}; + fEmitNow = true; + } + else if (now - it->second.windowStart >= DEDUP_WINDOW_SECS) + { + // Window rolled over: flush what was suppressed during it (if + // anything), then start a new window with this call printed. + suppressedToFlush = it->second.suppressed; + flushedMsg = it->second.lastMsg; + it->second.windowStart = now; + it->second.suppressed = 0; + it->second.lastMsg = msg; + fEmitNow = true; + } + else + { + // Still inside the window and already printed one line for it: + // count this occurrence instead of writing another near-duplicate. + it->second.suppressed++; + it->second.lastMsg = msg; + } + } + + if (suppressedToFlush > 0) + OutputDebugStringF("[%s] (%d similar in %llds, last: %s)\n", + category, suppressedToFlush, (long long)DEDUP_WINDOW_SECS, flushedMsg.c_str()); + if (fEmitNow) + OutputDebugStringF("[%s] %s\n", category, msg.c_str()); +} + @@ -200,11 +293,16 @@ void PrintException(std::exception* pex, const char* pszThread) snprintf(pszMessage, sizeof(pszMessage), "UNKNOWN EXCEPTION \n%s in %s \n", pszModule, pszThread); printf("\n\n************************\n%s", pszMessage); - // Suppress the dialog and rethrow during shutdown — threads unwinding on - // exit cause spurious "UNKNOWN EXCEPTION" popups that aren't real errors. - // Show error in GUI via repaint signal — GUI polls g_errorMessage - if (!fShutdown) - throw; + // Do NOT rethrow here. Every caller of PrintException (via + // CATCH_PRINT_EXCEPTION) is a thread wrapped in its own + // "try { ThreadFoo2() } catch(...) { log, sleep, loop again }" self-healing + // retry loop -- that's the whole point of the wrapper. Rethrowing from + // inside that catch block escapes the retry loop entirely (nothing above + // it in the thread catches it) and takes down the whole process with + // std::terminate() on the very first transient error -- a flaky relay + // connection, a malformed message, anything -- instead of letting the + // thread log it, sleep 5s, and try again like it's designed to. + // Show error in GUI via repaint signal -- GUI polls g_errorMessage } diff --git a/src/util.h b/src/util.h index c53293a..641b838 100644 --- a/src/util.h +++ b/src/util.h @@ -26,11 +26,14 @@ typedef unsigned long long uint64; #define UEND(a) ((unsigned char*)&((&(a))[1])) #define ARRAYLEN(array) (sizeof(array)/sizeof((array)[0])) -// Route printf to debug.log (and the console) on the GUI Windows build and on -// the headless Linux build alike. -#if defined(_WINDOWS) || !defined(_WIN32) +// Route printf to debug.log (and the console on non-Windows) everywhere. +// (Previously gated on `defined(_WINDOWS) || !defined(_WIN32)` -- but +// _WINDOWS is only ever defined by the legacy, unused makefile.vc build; +// the real Windows build (makefile.mingw) never defines it, so on actual +// Windows this condition was always false and debug.log was never written. +// OutputDebugStringF() is already platform-safe internally: it always +// writes debug.log, and only echoes to the console when not on Windows.) #define printf OutputDebugStringF -#endif #ifdef snprintf #undef snprintf @@ -70,6 +73,35 @@ inline T& REF(const T& val) extern bool fDebug; +// --------------------------------------------------------------------------- +// Categorized, burst-collapsing logging +// +// Plain printf() (routed to OutputDebugStringF below) writes every call +// unfiltered to debug.log. That's fine for one-off events, but a loop that +// logs once per item -- once per discovered peer, once per connect attempt, +// once per share -- turns debug.log into a wall of near-identical lines that +// differ only by an address, while the line that actually explains a stuck +// flow ("Stratum: no job available for ...") scrolls off screen under the +// noise. LogPrint() is a drop-in replacement for printf() that fixes both +// problems without touching call sites' formatting: +// +// 1. Category tag: every call names the subsystem it belongs to ("pool", +// "worker", "payout", "net", "nostr", ...). LogAcceptsCategory() +// controls what's written (see util.cpp); the pool/worker/payout flow +// the person actually needs visibility into is on by default. +// 2. Burst collapse: repeated LogPrint() calls from the *same source line* +// only emit one line per DEDUP_WINDOW_SECS; if more calls land inside +// that window they're counted instead of reprinted, and the count is +// flushed as a single "(xN similar in Ws)" line the moment the window +// rolls over. A loop discovering 500 peers by address now produces a +// small, bounded number of lines instead of 500 -- log growth is capped +// by elapsed time, not by how many similar events happen to fire. +// --------------------------------------------------------------------------- +bool LogAcceptsCategory(const char* category); +void LogPrintDedup(const char* category, const char* file, int line, const std::string& msg); +#define LogPrint(category, ...) \ + do { if (LogAcceptsCategory(category)) LogPrintDedup((category), __FILE__, __LINE__, strprintf(__VA_ARGS__)); } while (0) + void RandAddSeed(bool fPerfmon=false); int my_snprintf(char* buffer, size_t limit, const char* format, ...); string strprintf(const char* format, ...); @@ -231,7 +263,7 @@ void PrintHex(const T pbegin, const T pend, const char* pszFormat="%s", bool fSp -// Forward declaration — defined in main.cpp +// Forward declaration -- defined in main.cpp string GetAppDir(); inline int OutputDebugStringF(const char* pszFormat, ...) -- cgit v1.2.3 From c404e1c7f5d3f04d07e7e5b35a7a8ffcc998d855 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Wed, 29 Jul 2026 16:33:02 -0300 Subject: build: headless Linux node that does not need libGL (#37) The GUI build requires libGL at load time even when started with -nogui. The dynamic loader resolves every dependency before main() runs, so the flag arrives far too late -- on a clean server the binary dies with error while loading shared libraries: libGL.so.1: cannot open shared object file and nothing suggests it would otherwise have worked. A server is where a node most naturally lives, so this is the deployment that mattered most and the one that failed. The AppImage additionally needs FUSE, which minimal servers lack. Bundling libGL into the AppImage would be the wrong fix: GL has to come from the host's driver stack, and shipping our own breaks machines that have a real GPU. So: a second binary, built from the same node objects without ImGui, GLFW or OpenGL. `make linux-node` produces bitflash-node--x86_64, 2.5 MB stripped, with no graphics or FUSE dependency at all. `make linux` now builds it alongside the AppImage. Anyone wanting a window keeps using the AppImage. Two small moves were needed. MainFrameRepaint is called from main.cpp and net.cpp but defined in gui.cpp, so headless.cpp provides an empty one rather than putting ifdefs at six call sites. DateTimeStr also lived in gui.cpp while main.cpp logs block times with it -- nothing about formatting a date is GUI work, so it moved to util.cpp and both builds share one definition. Verified on Debian 13: ldd bitflash-node | grep -iE 'libGL|glfw|X11' -> nothing ldd bitflash | grep -iE 'libGL|glfw' -> libglfw.so.3, libGL.so.1, libGLdispatch.so.0 and at runtime, from an empty datadir: btfseed: reached ygffz37...btf via 90.156.222.107:8434 rendezvous: registered at 90.156.222.107:8434, waiting for a dial self-resolve OK (meeting_node=90.156.222.107:8434) btfpeers: kept 15 of 15 descriptor(s) first block at ~30s, 3182 blocks accepted, synced to the tip The GUI builds on Linux and Windows were rebuilt and are unaffected. --- Makefile | 12 ++++++++++++ src/Makefile | 32 +++++++++++++++++++++++++++++++- src/gui.cpp | 10 ++-------- src/headless.cpp | 20 ++++++++++++++++++++ src/main_gui.cpp | 13 +++++++++++++ src/util.cpp | 12 ++++++++++++ 6 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 src/headless.cpp (limited to 'src/Makefile') diff --git a/Makefile b/Makefile index 9cfef2f..b5bfa9a 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,18 @@ linux: deps-linux $(MAKE) -C src -f Makefile -j$(NPROC) strip src/bitflash $(MAKE) -f Makefile appimage + $(MAKE) -f Makefile linux-node + +# Headless node, no ImGui/GLFW/OpenGL. The GUI build needs libGL present at load +# time even when started with -nogui -- the dynamic loader resolves everything +# before main() runs, so the flag comes far too late. On a clean server, which is +# where a node most naturally lives, that is a hard failure. This binary has no +# such dependency and needs no FUSE either, so it drops onto a VPS and runs. +linux-node: deps-linux + $(MAKE) -C src -f Makefile bitflash-node -j$(NPROC) + strip src/bitflash-node + cp src/bitflash-node bitflash-node-$(VERSION)-x86_64 + @echo "Built: bitflash-node-$(VERSION)-x86_64" deps-linux: deps-apt deps-secp256k1 deps-randomx diff --git a/src/Makefile b/src/Makefile index c0b6e1d..9846d8e 100644 --- a/src/Makefile +++ b/src/Makefile @@ -71,5 +71,35 @@ bitflash: $(IMGUI_OBJS) $(NODE_OBJS) $(GUI_OBJS) $(CXX) $(CXXFLAGS) -o $@ $^ $(LIBS) @echo "Built: bitflash" +# ---- headless node --------------------------------------------------------- +# +# Same node, built without ImGui, GLFW or OpenGL. Linking the GUI made the +# binary require libGL at load time even when started with -nogui, because the +# dynamic loader resolves everything before main() runs -- so on a clean server, +# which is where a node most naturally lives, it failed to start with an error +# about shared libraries and no hint that it would otherwise work. +# +# Objects go to obj-node/ because main_gui.cpp is compiled twice, with and +# without -DBITFLASH_NO_GUI, and the two must not overwrite each other. + +NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \ + -L$(RANDOMX_DIR)/build -lrandomx \ + -lsodium -lssl -lcrypto -ldb_cxx -lpthread + +NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \ + obj-node/main_gui.o obj-node/headless.o + +obj-node/%.o: %.cpp $(HEADERS) + @mkdir -p obj-node + $(CXX) -c $(CXXFLAGS) -DBITFLASH_NO_GUI -o $@ $< + +obj-node/sha.o: sha.cpp sha.h + @mkdir -p obj-node + $(CXX) -c $(CXXFLAGS) -DBITFLASH_NO_GUI -O3 -o $@ $< + +bitflash-node: $(NODE_ONLY_OBJS) + $(CXX) $(CXXFLAGS) -o $@ $^ $(NODE_LIBS) + @echo "Built: bitflash-node (headless, no libGL needed)" + clean: - rm -rf obj bitflash + rm -rf obj obj-node bitflash bitflash-node diff --git a/src/gui.cpp b/src/gui.cpp index a1a0698..fc6f1e1 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -171,14 +171,8 @@ static void RefreshWallet() void MainFrameRepaint() { g_needRefresh = true; } -string DateTimeStr(int64 nTime) -{ - time_t t = (time_t)nTime; - struct tm* p = localtime(&t); - if (!p) return ""; - char buf[32]; strftime(buf, sizeof(buf), "%m/%d/%y %H:%M", p); - return buf; -} +// DateTimeStr moved to util.cpp: main.cpp logs block times with it, so it is +// needed by builds that have no GUI at all. // --------------------------------------------------------------------------- // Status bar -- separate fullscreen-width overlay pinned to bottom edge. diff --git a/src/headless.cpp b/src/headless.cpp new file mode 100644 index 0000000..3372936 --- /dev/null +++ b/src/headless.cpp @@ -0,0 +1,20 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// Stand-ins for the handful of symbols the node core expects the GUI to +// provide, so a headless binary can be linked without ImGui, GLFW or OpenGL. +// +// This exists because linking the GUI made the node need libGL at load time +// even when started with -nogui: the dynamic loader resolves everything before +// main() runs, so the flag came far too late. On a clean server -- which is +// where a node most naturally lives -- that is a hard failure with an error +// message about shared libraries and no hint that the program would otherwise +// have worked fine. + +#include "headers_core.h" + +// The GUI uses this to mark itself dirty. With no window to repaint there is +// nothing to do, and the call sites in main.cpp and net.cpp stay unchanged. +void MainFrameRepaint() +{ +} diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 9f7e34e..99e63d4 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -5,7 +5,11 @@ #include #endif +// BITFLASH_NO_GUI builds the same entry point without ImGui, GLFW or OpenGL, +// so the binary does not need libGL present just to start. See headless.cpp. +#ifndef BITFLASH_NO_GUI int RunGUI(int argc, char* argv[]); +#endif // Global definitions (were in ui.cpp, now here) map mapAddressBook; @@ -186,8 +190,13 @@ int main(int argc, char* argv[]) if (_beginthread(ThreadBitcoinMiner, 0, NULL) == (uintptr_t)-1) printf("Error: _beginthread(ThreadBitcoinMiner) failed\n"); +#ifdef BITFLASH_NO_GUI + // Nothing else this binary can do; /nogui is accepted and redundant. + bool fHeadless = true; +#else bool fHeadless = arg(argc,argv,"/nogui") || arg(argc,argv,"-nogui") || arg(argc,argv,"/daemon") || arg(argc,argv,"-daemon"); +#endif if (fHeadless) { #ifndef _WIN32 @@ -200,8 +209,12 @@ int main(int argc, char* argv[]) return 0; } +#ifdef BITFLASH_NO_GUI + return 0; // unreachable: fHeadless is always true in this build +#else int ret = RunGUI(argc, argv); fShutdown = true; StopNode(); return ret; +#endif } diff --git a/src/util.cpp b/src/util.cpp index 68bd18e..e4e314f 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -490,3 +490,15 @@ void AddTimeData(unsigned int ip, int64 nTime) printf("| nTimeOffset = %+lld (%+lld minutes)\n", nTimeOffset, nTimeOffset/60); } } + +// Formats a block or transaction timestamp for logs and for the wallet view. +// It lived in gui.cpp, but main.cpp logs block times with it, so a headless +// build needed it too -- and nothing about formatting a date is GUI work. +string DateTimeStr(int64 nTime) +{ + time_t t = (time_t)nTime; + struct tm* p = localtime(&t); + if (!p) return ""; + char buf[32]; strftime(buf, sizeof(buf), "%m/%d/%y %H:%M", p); + return buf; +} -- cgit v1.2.3 From 2ed9aed2ea9e302b3b36b37806ec03f2cccaaed1 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Thu, 30 Jul 2026 10:38:12 -0300 Subject: build: check format strings, which nothing ever did (#44) The segfault in issue #5 was a format string: `%I64d` is MSVC-only, glibc parses it as the GNU `I` flag with a width of 64 and an `int` conversion, the vararg run desynchronises, and the following `%s` dereferences an integer. It killed the node mid-sync at the first block carrying a real transaction, deterministically. GCC can catch this. It never got the chance, for two reasons that had to be fixed together: - `-w` sat in both makefiles and suppressed every diagnostic. It cannot be narrowed; it wins over any -W or -Werror= that follows, so it had to go. The four -Wno- entries replacing it are the categories this 2009-era tree actually produces (measured: 248 deprecated-declarations, 157 write-strings, 18 narrowing, 1 unused-result), silenced by name instead of wholesale. - Even with -Wformat on, nothing was checked. GCC only inspects format strings for functions it knows are printf-like, and every format string here goes through strprintf, error, my_snprintf, or OutputDebugStringF -- `printf` itself is #defined to the last of those. None carried a format attribute, so the entire tree was invisible to the check. They are annotated now. Annotating surfaced 77 warnings across 17 distinct sites, all fixed here. Most were size_t or int64 passed to %d, harmless on x86-64 but wrong. Two were not cosmetic: - CBlockIndex::ToString printed pprev/pnext with %08x, truncating both pointers to 32 bits, so the two fields that line exists to show were unreliable on every 64-bit build. - CInv::GetCommand threw "type=% unknown type", where the % ate the space as a flag and the u as its conversion. The message read "type= 3nknown type". PRId64 is now one spelling for every target. Its MSVCRT branch was what produced "I64d" in the first place, it is pointless against UCRT which takes the C99 forms, the rest of the tree already prints int64 with %lld on Windows, and its only user (i64tostr) is dead code. Collapsing it lets both builds share the gnu_printf archetype -- ms_printf would reject %zu, which this tree uses and UCRT supports, while waving through the %I64d that crashes. Verified on both platforms: clean build, and reintroducing the issue #5 specifier now fails compilation rather than shipping. main.h:410:50: error: unknown conversion type character 'I' in format main.h:410:70: error: format '%s' expects argument of type 'char*', but argument 3 has type 'long long int' That second line is the crash itself, caught at build time. Runtime checked on both: node syncs past the block from issue #5, and the repaired log lines read correctly -- before: CTxOut(nValue= 50.%08I64d, scriptPubKey=(null)) after: CTxOut(nValue=50.00000000, scriptPubKey=0xE1C7EF3E706002) Four '_WIN32_WINNT redefined' warnings remain on Windows. Silencing them means letting a system header pick the Windows API level instead of this tree, which is a separate decision from format checking. --- src/Makefile | 21 ++++++++++++++++++++- src/btfaddr.cpp | 3 +++ src/main.cpp | 16 ++++++++-------- src/main.h | 14 ++++++++------ src/makefile.mingw | 9 ++++++++- src/net.cpp | 2 +- src/net.h | 4 +++- src/nostr.cpp | 6 +++++- src/util.cpp | 4 ++-- src/util.h | 44 ++++++++++++++++++++++++++++++++++---------- 10 files changed, 92 insertions(+), 31 deletions(-) (limited to 'src/Makefile') diff --git a/src/Makefile b/src/Makefile index 9846d8e..f0c6706 100644 --- a/src/Makefile +++ b/src/Makefile @@ -9,7 +9,26 @@ RANDOMX_DIR ?= $(HOME)/RandomX IMGUI_DIR = imgui -CXXFLAGS = -std=gnu++14 -O2 -w -fpermissive -pthread \ +# -w used to sit here and suppressed every diagnostic in the build, including +# the one that mattered: `%I64d` is MSVC-only, glibc mis-parses it as the GNU +# `I` flag with a width of 64 and an `int` conversion, and the resulting vararg +# desynchronisation segfaulted the node while logging a received block (issue +# #5, deterministic at height 1062 -- the first block carrying a real +# transaction). GCC had been reporting it all along: +# +# warning: format '%d' expects argument of type 'int', +# but argument 4 has type 'int64_t' +# +# -w cannot be narrowed: it wins over any -W or -Werror= that follows it, so it +# has to go. The four -Wno- entries below are the categories this 2009-era tree +# genuinely produces (measured: 248 deprecated-declarations, 157 write-strings, +# 18 narrowing, 1 unused-result); silencing them by name instead of silencing +# everything keeps the build quiet while leaving format errors fatal. +WARNFLAGS = -Wformat -Werror=format \ + -Wno-deprecated-declarations -Wno-write-strings \ + -Wno-narrowing -Wno-unused-result + +CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ -DSECP256K1_STATIC \ -I$(SECP_INC) -I$(RANDOMX_DIR)/src \ -I/usr/include/nlohmann \ diff --git a/src/btfaddr.cpp b/src/btfaddr.cpp index 55bb6c5..6edcc91 100644 --- a/src/btfaddr.cpp +++ b/src/btfaddr.cpp @@ -7,7 +7,10 @@ #include "btfaddr.h" #include +// See the note in nostr.cpp: the makefiles pass -DSECP256K1_STATIC too. +#ifndef SECP256K1_STATIC #define SECP256K1_STATIC +#endif #include #include #include diff --git a/src/main.cpp b/src/main.cpp index bb317d2..c882b0b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -898,7 +898,7 @@ bool CTransaction::ConnectInputs(CTxDB& txdb, map& mapTestPoo } if (prevout.n >= txPrev.vout.size() || prevout.n >= txindex.vSpent.size()) - return error("ConnectInputs() : %s prevout.n out of range %d %d %d", GetHash().ToString().substr(0,6).c_str(), prevout.n, txPrev.vout.size(), txindex.vSpent.size()); + return error("ConnectInputs() : %s prevout.n out of range %d %d %d", GetHash().ToString().substr(0,6).c_str(), prevout.n, (int)txPrev.vout.size(), (int)txindex.vSpent.size()); // If prev is coinbase, check that it's matured if (txPrev.IsCoinBase()) @@ -1690,14 +1690,14 @@ void PrintBlockTree() pindex->nBlockPos, block.GetHash().ToString().substr(0,14).c_str(), DateTimeStr(block.nTime).c_str(), - block.vtx.size()); + (int)block.vtx.size()); CRITICAL_BLOCK(cs_mapWallet) { if (mapWallet.count(block.vtx[0].GetHash())) { CWalletTx& wtx = mapWallet[block.vtx[0].GetHash()]; - printf(" mine: %d %d %d", wtx.GetDepthInMainChain(), wtx.GetBlocksToMaturity(), wtx.GetCredit()); + printf(" mine: %d %d %lld", wtx.GetDepthInMainChain(), wtx.GetBlocksToMaturity(), wtx.GetCredit()); } } printf("\n"); @@ -1759,7 +1759,7 @@ bool ProcessMessages(CNode* pfrom) CDataStream& vRecv = pfrom->vRecv; if (vRecv.empty()) return true; - LogPrint("net", "ProcessMessages(%d bytes)\n", vRecv.size()); + LogPrint("net", "ProcessMessages(%d bytes)\n", (int)vRecv.size()); // // Message format @@ -1783,7 +1783,7 @@ bool ProcessMessages(CNode* pfrom) break; } if (pstart - vRecv.begin() > 0) - if (LogAcceptsCategory("net")) printf("\n\nPROCESSMESSAGE SKIPPED %d BYTES\n\n", pstart - vRecv.begin()); + if (LogAcceptsCategory("net")) printf("\n\nPROCESSMESSAGE SKIPPED %d BYTES\n\n", (int)(pstart - vRecv.begin())); vRecv.erase(vRecv.begin(), pstart); // Read header @@ -1836,7 +1836,7 @@ bool ProcessMessages(CNode* pfrom) bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) { static map > mapReuseKey; - if (LogAcceptsCategory("net")) { printf("received: %-12s (%d bytes) ", strCommand.c_str(), vRecv.size()); for (int i = 0; i < min(vRecv.size(), (unsigned int)25); i++) printf("%02x ", vRecv[i] & 0xff); printf("\n"); } + if (LogAcceptsCategory("net")) { printf("received: %-12s (%d bytes) ", strCommand.c_str(), (int)vRecv.size()); for (int i = 0; i < min(vRecv.size(), (unsigned int)25); i++) printf("%02x ", vRecv[i] & 0xff); printf("\n"); } if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0) { if (LogAcceptsCategory("net")) printf("dropmessages DROPPING RECV MESSAGE\n"); @@ -2171,7 +2171,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) if (!vRecv.empty()) - if (LogAcceptsCategory("net")) printf("ProcessMessage(%s) : %d extra bytes\n", strCommand.c_str(), vRecv.size()); + if (LogAcceptsCategory("net")) printf("ProcessMessage(%s) : %d extra bytes\n", strCommand.c_str(), (int)vRecv.size()); return true; } @@ -2885,7 +2885,7 @@ bool BitcoinMiner() } pblock->nBits = nBits; pblock->vtx[0].vout[0].nValue = pblock->GetBlockValue(nBestHeight + 1, nFees); - if (LogAcceptsCategory("net")) printf("\n\nRunning BitcoinMiner with %d transactions in block\n", pblock->vtx.size()); + if (LogAcceptsCategory("net")) printf("\n\nRunning BitcoinMiner with %d transactions in block\n", (int)pblock->vtx.size()); // diff --git a/src/main.h b/src/main.h index fde3a4e..7e5bdf2 100644 --- a/src/main.h +++ b/src/main.h @@ -650,11 +650,11 @@ public: string ToString() const { string str; - str += strprintf("CTransaction(hash=%s, ver=%d, vin.size=%d, vout.size=%d, nLockTime=%d)\n", + str += strprintf("CTransaction(hash=%s, ver=%d, vin.size=%d, vout.size=%d, nLockTime=%u)\n", GetHash().ToString().substr(0,6).c_str(), nVersion, - vin.size(), - vout.size(), + (int)vin.size(), + (int)vout.size(), nLockTime); for (int i = 0; i < vin.size(); i++) str += " " + vin[i].ToString() + "\n"; @@ -1081,7 +1081,7 @@ public: hashPrevBlock.ToString().substr(0,14).c_str(), hashMerkleRoot.ToString().substr(0,6).c_str(), nTime, nBits, nNonce, - vtx.size()); + (int)vtx.size()); for (int i = 0; i < vtx.size(); i++) { printf(" "); @@ -1223,8 +1223,10 @@ public: string ToString() const { - return strprintf("CBlockIndex(nprev=%08x, pnext=%08x, nFile=%d, nBlockPos=%-6d nHeight=%d, merkle=%s, hashBlock=%s)", - pprev, pnext, nFile, nBlockPos, nHeight, + // pprev/pnext are pointers: %08x truncated them to 32 bits on a 64-bit + // build, so the two fields this line exists to show were unreliable. + return strprintf("CBlockIndex(nprev=%p, pnext=%p, nFile=%d, nBlockPos=%-6d nHeight=%d, merkle=%s, hashBlock=%s)", + (void*)pprev, (void*)pnext, nFile, nBlockPos, nHeight, hashMerkleRoot.ToString().substr(0,6).c_str(), GetBlockHash().ToString().substr(0,14).c_str()); } diff --git a/src/makefile.mingw b/src/makefile.mingw index 7d35d92..01f7702 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -7,7 +7,14 @@ DEPS = ../deps RANDOMX = ../RandomX IMGUI_DIR = imgui -CXXFLAGS = -std=gnu++14 -O2 -w -fpermissive -Wno-deprecated-declarations -pthread \ +# See the long note in Makefile: -w hid the format bug that segfaulted the node +# on glibc (issue #5), and -w cannot be narrowed, so format diagnostics are now +# fatal and only the categories this tree actually produces are silenced. +WARNFLAGS = -Wformat -Werror=format \ + -Wno-deprecated-declarations -Wno-write-strings \ + -Wno-narrowing -Wno-unused-result + +CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ -DWIN32 -DNOPCH \ -DSECP256K1_STATIC \ -I$(DEPS)/include -I$(RANDOMX)/src \ diff --git a/src/net.cpp b/src/net.cpp index 08a3348..395092d 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -920,7 +920,7 @@ void ThreadSocketHandler2(void* parg) if (pnodeExtra->GetRefCount() <= (pnodeExtra->fNetworkNode ? 1 : 0)) { - LogPrint("net", "(%d nodes) disconnecting duplicate: %s\n", vNodes.size(), pnodeExtra->addr.ToString().c_str()); + LogPrint("net", "(%d nodes) disconnecting duplicate: %s\n", (int)vNodes.size(), pnodeExtra->addr.ToString().c_str()); if (pnodeExtra->fNetworkNode && !pnode->fNetworkNode) { pnode->AddRef(); diff --git a/src/net.h b/src/net.h index d13de61..364fbfb 100644 --- a/src/net.h +++ b/src/net.h @@ -392,7 +392,9 @@ public: const char* GetCommand() const { if (!IsKnownType()) - throw std::out_of_range(strprintf("CInv::GetCommand() : type=% unknown type", type)); + // Was "type=% unknown type": the % swallowed the space as a flag and + // the u as the conversion, so this read "type= 3nknown type". + throw std::out_of_range(strprintf("CInv::GetCommand() : type=%d unknown type", type)); return ppszTypeName[type]; } diff --git a/src/nostr.cpp b/src/nostr.cpp index 482a835..088174a 100644 --- a/src/nostr.cpp +++ b/src/nostr.cpp @@ -6,8 +6,12 @@ #include "headers.h" #include #include -// We link libsecp256k1 statically (without this the header assumes DLL import) +// We link libsecp256k1 statically (without this the header assumes DLL import). +// Both makefiles also pass -DSECP256K1_STATIC; the guard keeps that from +// warning while leaving this file correct if built without the flag. +#ifndef SECP256K1_STATIC #define SECP256K1_STATIC +#endif #include #include #include diff --git a/src/util.cpp b/src/util.cpp index e4e314f..d52ca88 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -180,7 +180,7 @@ void RandAddSeed(bool fPerfmon) RAND_add(&hash, sizeof(hash), min(nSize/500.0, (double)sizeof(hash))); hash = 0; memset(pdata, 0, nSize); - printf("RandAddSeed() got %d bytes of performance data\n", nSize); + printf("RandAddSeed() got %lu bytes of performance data\n", nSize); } } #else @@ -472,7 +472,7 @@ void AddTimeData(unsigned int ip, int64 nTime) if (vTimeOffsets.empty()) vTimeOffsets.push_back(0); vTimeOffsets.push_back(nOffsetSample); - printf("Added time data, samples %d, ip %08x, offset %+lld (%+lld minutes)\n", vTimeOffsets.size(), ip, vTimeOffsets.back(), vTimeOffsets.back()/60); + printf("Added time data, samples %d, ip %08x, offset %+lld (%+lld minutes)\n", (int)vTimeOffsets.size(), ip, vTimeOffsets.back(), vTimeOffsets.back()/60); if (vTimeOffsets.size() >= 5 && vTimeOffsets.size() % 2 == 1) { sort(vTimeOffsets.begin(), vTimeOffsets.end()); diff --git a/src/util.h b/src/util.h index b583248..797ed27 100644 --- a/src/util.h +++ b/src/util.h @@ -14,7 +14,9 @@ typedef unsigned long long uint64; #define for if (false) ; else for #endif -#ifndef _MSC_VER +// MinGW already provides __forceinline for MSVC compatibility, so defining it +// unconditionally warned ten times per build once -w was removed. +#if !defined(_MSC_VER) && !defined(__forceinline) #define __forceinline inline #endif @@ -40,17 +42,17 @@ typedef unsigned long long uint64; #endif #define snprintf my_snprintf +// One definition for every target. The MSVCRT branch that used to sit here +// produced "I64d", which is the specifier that segfaults on glibc (issue #5) -- +// and it was pointless besides: this build targets UCRT, which accepts the C99 +// forms, and the rest of the tree already prints int64 with %lld on Windows. +// Keeping one spelling everywhere is also what lets the format attributes below +// use a single archetype, so a bad specifier fails the build on both platforms. #ifndef PRId64 -#if defined(_MSC_VER) || defined(__BORLANDC__) || defined(__MSVCRT__) -#define PRId64 "I64d" -#define PRIu64 "I64u" -#define PRIx64 "I64x" -#else #define PRId64 "lld" #define PRIu64 "llu" #define PRIx64 "llx" #endif -#endif // This is needed because the foreach macro can't get over the comma in pair #define PAIRTYPE(t1, t2) pair @@ -102,10 +104,30 @@ void LogPrintDedup(const char* category, const char* file, int line, const std:: #define LogPrint(category, ...) \ do { if (LogAcceptsCategory(category)) LogPrintDedup((category), __FILE__, __LINE__, strprintf(__VA_ARGS__)); } while (0) +// Every format string in this project goes through one of the four functions +// below -- `printf` itself is remapped to OutputDebugStringF just above, and +// snprintf to my_snprintf. None of them carried a format attribute, and GCC +// only checks format strings for functions it knows are printf-like, so +// -Wformat had nothing to inspect and the whole tree was unchecked. That is +// how `%I64d` (MSVC-only, and a segfault on glibc) survived in +// CTxOut::ToString until it crashed a node mid-sync -- see issue #5. +// +// gnu_printf on both platforms, deliberately. MinGW's other option, ms_printf, +// models the legacy msvcrt: it rejects %zu, which this tree uses and UCRT +// supports, and it accepts %I64d, which is the specifier that crashes on glibc. +// It would give false positives where the code is right and stay silent where it +// is dangerous. gnu_printf describes UCRT's C99 behaviour accurately, so one +// archetype covers both builds and a bad specifier fails either one. +#if defined(__GNUC__) +#define BF_FORMAT(fmt, args) __attribute__((format(gnu_printf, fmt, args))) +#else +#define BF_FORMAT(fmt, args) +#endif + void RandAddSeed(bool fPerfmon=false); -int my_snprintf(char* buffer, size_t limit, const char* format, ...); -string strprintf(const char* format, ...); -bool error(const char* format, ...); +int my_snprintf(char* buffer, size_t limit, const char* format, ...) BF_FORMAT(3, 4); +string strprintf(const char* format, ...) BF_FORMAT(1, 2); +bool error(const char* format, ...) BF_FORMAT(1, 2); void PrintException(std::exception* pex, const char* pszThread); void ParseString(const string& str, char c, vector& v); string FormatMoney(int64 n, bool fPlus=false); @@ -266,6 +288,8 @@ void PrintHex(const T pbegin, const T pend, const char* pszFormat="%s", bool fSp // Forward declaration -- defined in main.cpp string GetAppDir(); +inline int OutputDebugStringF(const char* pszFormat, ...) BF_FORMAT(1, 2); + inline int OutputDebugStringF(const char* pszFormat, ...) { #if 1 // debug.log always enabled -- cgit v1.2.3 From 64b9507846846e468ebba7e00fb59971edceca3b Mon Sep 17 00:00:00 2001 From: mtg0001 <106211915+mtg0001@users.noreply.github.com> Date: Sat, 1 Aug 2026 12:23:28 -0300 Subject: Add BIP39 and BIP32 foundation tests (#83) Co-authored-by: Gilcimar Duarte --- Makefile | 6 +- src/Makefile | 8 +- src/bip32.cpp | 366 +++++++++ src/bip32.h | 53 ++ src/bip39_english.h | 2062 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/makefile.mingw | 9 +- src/test_bip32.cpp | 130 ++++ 7 files changed, 2631 insertions(+), 3 deletions(-) create mode 100644 src/bip32.cpp create mode 100644 src/bip32.h create mode 100644 src/bip39_english.h create mode 100644 src/test_bip32.cpp (limited to 'src/Makefile') diff --git a/Makefile b/Makefile index 970114d..d436885 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ # # make linux build Bitflash-*-x86_64.AppImage # make windows build Bitflash-*-windows.zip (from MSYS2 UCRT64) +# make tests build and run standalone unit tests # make clean remove build artifacts ROOT := $(shell pwd) @@ -156,5 +157,8 @@ clean: rm -f Bitflash-*.AppImage Bitflash-*.zip rm -rf Bitflash-*-windows +tests: + $(MAKE) -C src -f Makefile tests + .PHONY: linux windows clean appimage \ - deps-linux deps-windows deps-apt deps-secp256k1 deps-randomx + tests deps-linux deps-windows deps-apt deps-secp256k1 deps-randomx diff --git a/src/Makefile b/src/Makefile index f0c6706..4e104c0 100644 --- a/src/Makefile +++ b/src/Makefile @@ -121,4 +121,10 @@ bitflash-node: $(NODE_ONLY_OBJS) @echo "Built: bitflash-node (headless, no libGL needed)" clean: - rm -rf obj obj-node bitflash bitflash-node + rm -rf obj obj-node bitflash bitflash-node test_bip32 + +test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h + $(CXX) $(CXXFLAGS) -o $@ test_bip32.cpp bip32.cpp -lssl -lcrypto + +tests: test_bip32 + ./test_bip32 diff --git a/src/bip32.cpp b/src/bip32.cpp new file mode 100644 index 0000000..6423974 --- /dev/null +++ b/src/bip32.cpp @@ -0,0 +1,366 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. + +#include "bip32.h" +#include "bip39_english.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace bitflash +{ + +static bool IsSupportedEntropySize(size_t n) +{ + return n == 16 || n == 20 || n == 24 || n == 28 || n == 32; +} + +static bool GetBit(const unsigned char* data, size_t bit) +{ + return (data[bit / 8] >> (7 - (bit % 8))) & 1; +} + +static void AppendBits(std::vector& out, const unsigned char* data, + size_t nBits) +{ + for (size_t i = 0; i < nBits; i++) + out.push_back(GetBit(data, i)); +} + +static std::string NormalizeMnemonic(const std::string& in) +{ + std::string out; + bool inSpace = true; + for (unsigned char c : in) + { + if (std::isspace(c)) + { + if (!inSpace) + out.push_back(' '); + inSpace = true; + } + else + { + out.push_back((char)std::tolower(c)); + inSpace = false; + } + } + if (!out.empty() && out.back() == ' ') + out.pop_back(); + return out; +} + +static std::vector SplitWords(const std::string& phrase) +{ + std::vector words; + std::istringstream in(phrase); + std::string word; + while (in >> word) + words.push_back(word); + return words; +} + +static const std::map& WordIndex() +{ + static std::map m; + if (m.empty()) + { + for (int i = 0; i < 2048; i++) + m[BIP39_ENGLISH_WORDS[i]] = i; + } + return m; +} + +static bool IsValidPrivateKey(const std::vector& key) +{ + if (key.size() != 32) + return false; + bool fNonZero = false; + for (unsigned char c : key) + fNonZero = fNonZero || c != 0; + if (!fNonZero) + return false; + + bool ok = false; + BIGNUM* bn = BN_bin2bn(&key[0], 32, NULL); + BIGNUM* order = BN_new(); + EC_GROUP* group = EC_GROUP_new_by_curve_name(NID_secp256k1); + if (bn && order && group && EC_GROUP_get_order(group, order, NULL)) + ok = BN_cmp(bn, order) < 0; + BN_free(bn); + BN_free(order); + EC_GROUP_free(group); + return ok; +} + +static bool HmacSha512(const unsigned char* key, int keyLen, + const unsigned char* data, size_t dataLen, + unsigned char out[64]) +{ + unsigned int len = 64; + return HMAC(EVP_sha512(), key, keyLen, data, dataLen, out, &len) != NULL && + len == 64; +} + +bool BIP39EntropyToMnemonic(const std::vector& entropy, + std::string& mnemonicOut, + std::string& errorOut) +{ + mnemonicOut.clear(); + errorOut.clear(); + if (!IsSupportedEntropySize(entropy.size())) + { + errorOut = "entropy must be 128, 160, 192, 224, or 256 bits"; + return false; + } + + unsigned char checksum[SHA256_DIGEST_LENGTH]; + SHA256(&entropy[0], entropy.size(), checksum); + + const size_t entropyBits = entropy.size() * 8; + const size_t checksumBits = entropyBits / 32; + + std::vector bits; + bits.reserve(entropyBits + checksumBits); + AppendBits(bits, &entropy[0], entropyBits); + AppendBits(bits, checksum, checksumBits); + + const size_t words = bits.size() / 11; + for (size_t i = 0; i < words; i++) + { + int idx = 0; + for (int j = 0; j < 11; j++) + idx = (idx << 1) | (bits[i * 11 + j] ? 1 : 0); + if (i != 0) + mnemonicOut += " "; + mnemonicOut += BIP39_ENGLISH_WORDS[idx]; + } + return true; +} + +bool BIP39GenerateMnemonic(std::string& mnemonicOut, + std::string& errorOut, + unsigned int entropyBytes) +{ + mnemonicOut.clear(); + errorOut.clear(); + if (!IsSupportedEntropySize(entropyBytes)) + { + errorOut = "entropy must be 16, 20, 24, 28, or 32 bytes"; + return false; + } + + std::vector entropy(entropyBytes); + if (RAND_bytes(&entropy[0], entropy.size()) != 1) + { + errorOut = "RAND_bytes failed"; + return false; + } + bool ok = BIP39EntropyToMnemonic(entropy, mnemonicOut, errorOut); + OPENSSL_cleanse(&entropy[0], entropy.size()); + return ok; +} + +bool BIP39ValidateMnemonic(const std::string& mnemonic, + std::string& normalizedOut, + std::string& errorOut) +{ + normalizedOut = NormalizeMnemonic(mnemonic); + errorOut.clear(); + std::vector words = SplitWords(normalizedOut); + const size_t nWords = words.size(); + if (nWords != 12 && nWords != 15 && nWords != 18 && nWords != 21 && nWords != 24) + { + errorOut = "mnemonic must contain 12, 15, 18, 21, or 24 words"; + return false; + } + + const std::map& index = WordIndex(); + std::vector bits; + bits.reserve(nWords * 11); + for (const std::string& word : words) + { + std::map::const_iterator mi = index.find(word); + if (mi == index.end()) + { + errorOut = "mnemonic contains a word outside the BIP39 English list"; + return false; + } + int value = mi->second; + for (int i = 10; i >= 0; i--) + bits.push_back(((value >> i) & 1) != 0); + } + + const size_t totalBits = bits.size(); + const size_t entropyBits = totalBits * 32 / 33; + const size_t checksumBits = totalBits - entropyBits; + std::vector entropy(entropyBits / 8, 0); + for (size_t i = 0; i < entropyBits; i++) + if (bits[i]) + entropy[i / 8] |= (unsigned char)(1 << (7 - (i % 8))); + + unsigned char checksum[SHA256_DIGEST_LENGTH]; + SHA256(&entropy[0], entropy.size(), checksum); + for (size_t i = 0; i < checksumBits; i++) + { + if (bits[entropyBits + i] != GetBit(checksum, i)) + { + errorOut = "mnemonic checksum failed"; + return false; + } + } + return true; +} + +bool BIP39MnemonicToSeed(const std::string& mnemonic, + const std::string& passphrase, + std::vector& seedOut, + std::string& errorOut) +{ + seedOut.assign(64, 0); + std::string normalized; + if (!BIP39ValidateMnemonic(mnemonic, normalized, errorOut)) + { + seedOut.clear(); + return false; + } + + const std::string salt = "mnemonic" + passphrase; + if (!PKCS5_PBKDF2_HMAC(normalized.c_str(), normalized.size(), + (const unsigned char*)salt.data(), salt.size(), + 2048, EVP_sha512(), 64, &seedOut[0])) + { + seedOut.clear(); + errorOut = "PBKDF2-HMAC-SHA512 failed"; + return false; + } + return true; +} + +bool BIP32MasterFromSeed(const std::vector& seed, + BIP32PrivateNode& nodeOut, + std::string& errorOut) +{ + nodeOut.privateKey.clear(); + nodeOut.chainCode.clear(); + errorOut.clear(); + if (seed.size() < 16 || seed.size() > 64) + { + errorOut = "BIP32 seed must be between 128 and 512 bits"; + return false; + } + + unsigned char I[64]; + static const unsigned char key[] = "Bitcoin seed"; + if (!HmacSha512(key, sizeof(key) - 1, &seed[0], seed.size(), I)) + { + errorOut = "HMAC-SHA512 failed"; + return false; + } + + nodeOut.privateKey.assign(I, I + 32); + nodeOut.chainCode.assign(I + 32, I + 64); + OPENSSL_cleanse(I, sizeof(I)); + + if (!IsValidPrivateKey(nodeOut.privateKey)) + { + nodeOut.privateKey.clear(); + nodeOut.chainCode.clear(); + errorOut = "BIP32 master key is invalid"; + return false; + } + return true; +} + +bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, + unsigned int childIndex, + BIP32PrivateNode& childOut, + std::string& errorOut) +{ + childOut.privateKey.clear(); + childOut.chainCode.clear(); + errorOut.clear(); + if (!IsValidPrivateKey(parent.privateKey) || parent.chainCode.size() != 32) + { + errorOut = "parent private key or chain code is invalid"; + return false; + } + if (childIndex >= 0x80000000U) + { + errorOut = "child index must be non-hardened; hardening is applied here"; + return false; + } + + const unsigned int hardened = childIndex | 0x80000000U; + unsigned char data[37]; + data[0] = 0x00; + memcpy(data + 1, &parent.privateKey[0], 32); + data[33] = (unsigned char)((hardened >> 24) & 0xff); + data[34] = (unsigned char)((hardened >> 16) & 0xff); + data[35] = (unsigned char)((hardened >> 8) & 0xff); + data[36] = (unsigned char)(hardened & 0xff); + + unsigned char I[64]; + if (!HmacSha512(&parent.chainCode[0], 32, data, sizeof(data), I)) + { + OPENSSL_cleanse(data, sizeof(data)); + errorOut = "HMAC-SHA512 failed"; + return false; + } + OPENSSL_cleanse(data, sizeof(data)); + + std::vector IL(I, I + 32); + childOut.chainCode.assign(I + 32, I + 64); + OPENSSL_cleanse(I, sizeof(I)); + + BIGNUM* bnIL = BN_bin2bn(&IL[0], 32, NULL); + BIGNUM* bnParent = BN_bin2bn(&parent.privateKey[0], 32, NULL); + BIGNUM* bnOrder = BN_new(); + BIGNUM* bnChild = BN_new(); + BN_CTX* ctx = BN_CTX_new(); + EC_GROUP* group = EC_GROUP_new_by_curve_name(NID_secp256k1); + + bool ok = false; + if (bnIL && bnParent && bnOrder && bnChild && ctx && group && + EC_GROUP_get_order(group, bnOrder, NULL) && + BN_cmp(bnIL, bnOrder) < 0 && + BN_mod_add(bnChild, bnIL, bnParent, bnOrder, ctx) && + !BN_is_zero(bnChild)) + { + childOut.privateKey.assign(32, 0); + int n = BN_num_bytes(bnChild); + BN_bn2bin(bnChild, &childOut.privateKey[32 - n]); + ok = true; + } + + BN_free(bnIL); + BN_free(bnParent); + BN_free(bnOrder); + BN_free(bnChild); + BN_CTX_free(ctx); + EC_GROUP_free(group); + OPENSSL_cleanse(&IL[0], IL.size()); + + if (!ok) + { + childOut.privateKey.clear(); + childOut.chainCode.clear(); + errorOut = "BIP32 child derivation produced an invalid key"; + return false; + } + return true; +} + +} // namespace bitflash diff --git a/src/bip32.h b/src/bip32.h new file mode 100644 index 0000000..a029444 --- /dev/null +++ b/src/bip32.h @@ -0,0 +1,53 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// BIP39/BIP32 primitives for deterministic wallet work. +// +// This module is deliberately independent from wallet.dat and GUI code. It is +// the auditable foundation: mnemonic generation/validation, seed derivation, +// and hardened private child derivation over secp256k1. + +#ifndef BITFLASH_BIP32_H +#define BITFLASH_BIP32_H + +#include +#include + +namespace bitflash +{ + +struct BIP32PrivateNode +{ + std::vector privateKey; // 32-byte scalar + std::vector chainCode; // 32 bytes +}; + +bool BIP39EntropyToMnemonic(const std::vector& entropy, + std::string& mnemonicOut, + std::string& errorOut); + +bool BIP39GenerateMnemonic(std::string& mnemonicOut, + std::string& errorOut, + unsigned int entropyBytes = 16); + +bool BIP39ValidateMnemonic(const std::string& mnemonic, + std::string& normalizedOut, + std::string& errorOut); + +bool BIP39MnemonicToSeed(const std::string& mnemonic, + const std::string& passphrase, + std::vector& seedOut, + std::string& errorOut); + +bool BIP32MasterFromSeed(const std::vector& seed, + BIP32PrivateNode& nodeOut, + std::string& errorOut); + +bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, + unsigned int childIndex, + BIP32PrivateNode& childOut, + std::string& errorOut); + +} // namespace bitflash + +#endif diff --git a/src/bip39_english.h b/src/bip39_english.h new file mode 100644 index 0000000..04b4ed2 --- /dev/null +++ b/src/bip39_english.h @@ -0,0 +1,2062 @@ +// BIP39 English Wordlist + +#ifndef BITFLASH_BIP39_ENGLISH_H +#define BITFLASH_BIP39_ENGLISH_H + +namespace bitflash +{ + +static const char* BIP39_ENGLISH_WORDS[2048] = { + "abandon", + "ability", + "able", + "about", + "above", + "absent", + "absorb", + "abstract", + "absurd", + "abuse", + "access", + "accident", + "account", + "accuse", + "achieve", + "acid", + "acoustic", + "acquire", + "across", + "act", + "action", + "actor", + "actress", + "actual", + "adapt", + "add", + "addict", + "address", + "adjust", + "admit", + "adult", + "advance", + "advice", + "aerobic", + "affair", + "afford", + "afraid", + "again", + "age", + "agent", + "agree", + "ahead", + "aim", + "air", + "airport", + "aisle", + "alarm", + "album", + "alcohol", + "alert", + "alien", + "all", + "alley", + "allow", + "almost", + "alone", + "alpha", + "already", + "also", + "alter", + "always", + "amateur", + "amazing", + "among", + "amount", + "amused", + "analyst", + "anchor", + "ancient", + "anger", + "angle", + "angry", + "animal", + "ankle", + "announce", + "annual", + "another", + "answer", + "antenna", + "antique", + "anxiety", + "any", + "apart", + "apology", + "appear", + "apple", + "approve", + "april", + "arch", + "arctic", + "area", + "arena", + "argue", + "arm", + "armed", + "armor", + "army", + "around", + "arrange", + "arrest", + "arrive", + "arrow", + "art", + "artefact", + "artist", + "artwork", + "ask", + "aspect", + "assault", + "asset", + "assist", + "assume", + "asthma", + "athlete", + "atom", + "attack", + "attend", + "attitude", + "attract", + "auction", + "audit", + "august", + "aunt", + "author", + "auto", + "autumn", + "average", + "avocado", + "avoid", + "awake", + "aware", + "away", + "awesome", + "awful", + "awkward", + "axis", + "baby", + "bachelor", + "bacon", + "badge", + "bag", + "balance", + "balcony", + "ball", + "bamboo", + "banana", + "banner", + "bar", + "barely", + "bargain", + "barrel", + "base", + "basic", + "basket", + "battle", + "beach", + "bean", + "beauty", + "because", + "become", + "beef", + "before", + "begin", + "behave", + "behind", + "believe", + "below", + "belt", + "bench", + "benefit", + "best", + "betray", + "better", + "between", + "beyond", + "bicycle", + "bid", + "bike", + "bind", + "biology", + "bird", + "birth", + "bitter", + "black", + "blade", + "blame", + "blanket", + "blast", + "bleak", + "bless", + "blind", + "blood", + "blossom", + "blouse", + "blue", + "blur", + "blush", + "board", + "boat", + "body", + "boil", + "bomb", + "bone", + "bonus", + "book", + "boost", + "border", + "boring", + "borrow", + "boss", + "bottom", + "bounce", + "box", + "boy", + "bracket", + "brain", + "brand", + "brass", + "brave", + "bread", + "breeze", + "brick", + "bridge", + "brief", + "bright", + "bring", + "brisk", + "broccoli", + "broken", + "bronze", + "broom", + "brother", + "brown", + "brush", + "bubble", + "buddy", + "budget", + "buffalo", + "build", + "bulb", + "bulk", + "bullet", + "bundle", + "bunker", + "burden", + "burger", + "burst", + "bus", + "business", + "busy", + "butter", + "buyer", + "buzz", + "cabbage", + "cabin", + "cable", + "cactus", + "cage", + "cake", + "call", + "calm", + "camera", + "camp", + "can", + "canal", + "cancel", + "candy", + "cannon", + "canoe", + "canvas", + "canyon", + "capable", + "capital", + "captain", + "car", + "carbon", + "card", + "cargo", + "carpet", + "carry", + "cart", + "case", + "cash", + "casino", + "castle", + "casual", + "cat", + "catalog", + "catch", + "category", + "cattle", + "caught", + "cause", + "caution", + "cave", + "ceiling", + "celery", + "cement", + "census", + "century", + "cereal", + "certain", + "chair", + "chalk", + "champion", + "change", + "chaos", + "chapter", + "charge", + "chase", + "chat", + "cheap", + "check", + "cheese", + "chef", + "cherry", + "chest", + "chicken", + "chief", + "child", + "chimney", + "choice", + "choose", + "chronic", + "chuckle", + "chunk", + "churn", + "cigar", + "cinnamon", + "circle", + "citizen", + "city", + "civil", + "claim", + "clap", + "clarify", + "claw", + "clay", + "clean", + "clerk", + "clever", + "click", + "client", + "cliff", + "climb", + "clinic", + "clip", + "clock", + "clog", + "close", + "cloth", + "cloud", + "clown", + "club", + "clump", + "cluster", + "clutch", + "coach", + "coast", + "coconut", + "code", + "coffee", + "coil", + "coin", + "collect", + "color", + "column", + "combine", + "come", + "comfort", + "comic", + "common", + "company", + "concert", + "conduct", + "confirm", + "congress", + "connect", + "consider", + "control", + "convince", + "cook", + "cool", + "copper", + "copy", + "coral", + "core", + "corn", + "correct", + "cost", + "cotton", + "couch", + "country", + "couple", + "course", + "cousin", + "cover", + "coyote", + "crack", + "cradle", + "craft", + "cram", + "crane", + "crash", + "crater", + "crawl", + "crazy", + "cream", + "credit", + "creek", + "crew", + "cricket", + "crime", + "crisp", + "critic", + "crop", + "cross", + "crouch", + "crowd", + "crucial", + "cruel", + "cruise", + "crumble", + "crunch", + "crush", + "cry", + "crystal", + "cube", + "culture", + "cup", + "cupboard", + "curious", + "current", + "curtain", + "curve", + "cushion", + "custom", + "cute", + "cycle", + "dad", + "damage", + "damp", + "dance", + "danger", + "daring", + "dash", + "daughter", + "dawn", + "day", + "deal", + "debate", + "debris", + "decade", + "december", + "decide", + "decline", + "decorate", + "decrease", + "deer", + "defense", + "define", + "defy", + "degree", + "delay", + "deliver", + "demand", + "demise", + "denial", + "dentist", + "deny", + "depart", + "depend", + "deposit", + "depth", + "deputy", + "derive", + "describe", + "desert", + "design", + "desk", + "despair", + "destroy", + "detail", + "detect", + "develop", + "device", + "devote", + "diagram", + "dial", + "diamond", + "diary", + "dice", + "diesel", + "diet", + "differ", + "digital", + "dignity", + "dilemma", + "dinner", + "dinosaur", + "direct", + "dirt", + "disagree", + "discover", + "disease", + "dish", + "dismiss", + "disorder", + "display", + "distance", + "divert", + "divide", + "divorce", + "dizzy", + "doctor", + "document", + "dog", + "doll", + "dolphin", + "domain", + "donate", + "donkey", + "donor", + "door", + "dose", + "double", + "dove", + "draft", + "dragon", + "drama", + "drastic", + "draw", + "dream", + "dress", + "drift", + "drill", + "drink", + "drip", + "drive", + "drop", + "drum", + "dry", + "duck", + "dumb", + "dune", + "during", + "dust", + "dutch", + "duty", + "dwarf", + "dynamic", + "eager", + "eagle", + "early", + "earn", + "earth", + "easily", + "east", + "easy", + "echo", + "ecology", + "economy", + "edge", + "edit", + "educate", + "effort", + "egg", + "eight", + "either", + "elbow", + "elder", + "electric", + "elegant", + "element", + "elephant", + "elevator", + "elite", + "else", + "embark", + "embody", + "embrace", + "emerge", + "emotion", + "employ", + "empower", + "empty", + "enable", + "enact", + "end", + "endless", + "endorse", + "enemy", + "energy", + "enforce", + "engage", + "engine", + "enhance", + "enjoy", + "enlist", + "enough", + "enrich", + "enroll", + "ensure", + "enter", + "entire", + "entry", + "envelope", + "episode", + "equal", + "equip", + "era", + "erase", + "erode", + "erosion", + "error", + "erupt", + "escape", + "essay", + "essence", + "estate", + "eternal", + "ethics", + "evidence", + "evil", + "evoke", + "evolve", + "exact", + "example", + "excess", + "exchange", + "excite", + "exclude", + "excuse", + "execute", + "exercise", + "exhaust", + "exhibit", + "exile", + "exist", + "exit", + "exotic", + "expand", + "expect", + "expire", + "explain", + "expose", + "express", + "extend", + "extra", + "eye", + "eyebrow", + "fabric", + "face", + "faculty", + "fade", + "faint", + "faith", + "fall", + "false", + "fame", + "family", + "famous", + "fan", + "fancy", + "fantasy", + "farm", + "fashion", + "fat", + "fatal", + "father", + "fatigue", + "fault", + "favorite", + "feature", + "february", + "federal", + "fee", + "feed", + "feel", + "female", + "fence", + "festival", + "fetch", + "fever", + "few", + "fiber", + "fiction", + "field", + "figure", + "file", + "film", + "filter", + "final", + "find", + "fine", + "finger", + "finish", + "fire", + "firm", + "first", + "fiscal", + "fish", + "fit", + "fitness", + "fix", + "flag", + "flame", + "flash", + "flat", + "flavor", + "flee", + "flight", + "flip", + "float", + "flock", + "floor", + "flower", + "fluid", + "flush", + "fly", + "foam", + "focus", + "fog", + "foil", + "fold", + "follow", + "food", + "foot", + "force", + "forest", + "forget", + "fork", + "fortune", + "forum", + "forward", + "fossil", + "foster", + "found", + "fox", + "fragile", + "frame", + "frequent", + "fresh", + "friend", + "fringe", + "frog", + "front", + "frost", + "frown", + "frozen", + "fruit", + "fuel", + "fun", + "funny", + "furnace", + "fury", + "future", + "gadget", + "gain", + "galaxy", + "gallery", + "game", + "gap", + "garage", + "garbage", + "garden", + "garlic", + "garment", + "gas", + "gasp", + "gate", + "gather", + "gauge", + "gaze", + "general", + "genius", + "genre", + "gentle", + "genuine", + "gesture", + "ghost", + "giant", + "gift", + "giggle", + "ginger", + "giraffe", + "girl", + "give", + "glad", + "glance", + "glare", + "glass", + "glide", + "glimpse", + "globe", + "gloom", + "glory", + "glove", + "glow", + "glue", + "goat", + "goddess", + "gold", + "good", + "goose", + "gorilla", + "gospel", + "gossip", + "govern", + "gown", + "grab", + "grace", + "grain", + "grant", + "grape", + "grass", + "gravity", + "great", + "green", + "grid", + "grief", + "grit", + "grocery", + "group", + "grow", + "grunt", + "guard", + "guess", + "guide", + "guilt", + "guitar", + "gun", + "gym", + "habit", + "hair", + "half", + "hammer", + "hamster", + "hand", + "happy", + "harbor", + "hard", + "harsh", + "harvest", + "hat", + "have", + "hawk", + "hazard", + "head", + "health", + "heart", + "heavy", + "hedgehog", + "height", + "hello", + "helmet", + "help", + "hen", + "hero", + "hidden", + "high", + "hill", + "hint", + "hip", + "hire", + "history", + "hobby", + "hockey", + "hold", + "hole", + "holiday", + "hollow", + "home", + "honey", + "hood", + "hope", + "horn", + "horror", + "horse", + "hospital", + "host", + "hotel", + "hour", + "hover", + "hub", + "huge", + "human", + "humble", + "humor", + "hundred", + "hungry", + "hunt", + "hurdle", + "hurry", + "hurt", + "husband", + "hybrid", + "ice", + "icon", + "idea", + "identify", + "idle", + "ignore", + "ill", + "illegal", + "illness", + "image", + "imitate", + "immense", + "immune", + "impact", + "impose", + "improve", + "impulse", + "inch", + "include", + "income", + "increase", + "index", + "indicate", + "indoor", + "industry", + "infant", + "inflict", + "inform", + "inhale", + "inherit", + "initial", + "inject", + "injury", + "inmate", + "inner", + "innocent", + "input", + "inquiry", + "insane", + "insect", + "inside", + "inspire", + "install", + "intact", + "interest", + "into", + "invest", + "invite", + "involve", + "iron", + "island", + "isolate", + "issue", + "item", + "ivory", + "jacket", + "jaguar", + "jar", + "jazz", + "jealous", + "jeans", + "jelly", + "jewel", + "job", + "join", + "joke", + "journey", + "joy", + "judge", + "juice", + "jump", + "jungle", + "junior", + "junk", + "just", + "kangaroo", + "keen", + "keep", + "ketchup", + "key", + "kick", + "kid", + "kidney", + "kind", + "kingdom", + "kiss", + "kit", + "kitchen", + "kite", + "kitten", + "kiwi", + "knee", + "knife", + "knock", + "know", + "lab", + "label", + "labor", + "ladder", + "lady", + "lake", + "lamp", + "language", + "laptop", + "large", + "later", + "latin", + "laugh", + "laundry", + "lava", + "law", + "lawn", + "lawsuit", + "layer", + "lazy", + "leader", + "leaf", + "learn", + "leave", + "lecture", + "left", + "leg", + "legal", + "legend", + "leisure", + "lemon", + "lend", + "length", + "lens", + "leopard", + "lesson", + "letter", + "level", + "liar", + "liberty", + "library", + "license", + "life", + "lift", + "light", + "like", + "limb", + "limit", + "link", + "lion", + "liquid", + "list", + "little", + "live", + "lizard", + "load", + "loan", + "lobster", + "local", + "lock", + "logic", + "lonely", + "long", + "loop", + "lottery", + "loud", + "lounge", + "love", + "loyal", + "lucky", + "luggage", + "lumber", + "lunar", + "lunch", + "luxury", + "lyrics", + "machine", + "mad", + "magic", + "magnet", + "maid", + "mail", + "main", + "major", + "make", + "mammal", + "man", + "manage", + "mandate", + "mango", + "mansion", + "manual", + "maple", + "marble", + "march", + "margin", + "marine", + "market", + "marriage", + "mask", + "mass", + "master", + "match", + "material", + "math", + "matrix", + "matter", + "maximum", + "maze", + "meadow", + "mean", + "measure", + "meat", + "mechanic", + "medal", + "media", + "melody", + "melt", + "member", + "memory", + "mention", + "menu", + "mercy", + "merge", + "merit", + "merry", + "mesh", + "message", + "metal", + "method", + "middle", + "midnight", + "milk", + "million", + "mimic", + "mind", + "minimum", + "minor", + "minute", + "miracle", + "mirror", + "misery", + "miss", + "mistake", + "mix", + "mixed", + "mixture", + "mobile", + "model", + "modify", + "mom", + "moment", + "monitor", + "monkey", + "monster", + "month", + "moon", + "moral", + "more", + "morning", + "mosquito", + "mother", + "motion", + "motor", + "mountain", + "mouse", + "move", + "movie", + "much", + "muffin", + "mule", + "multiply", + "muscle", + "museum", + "mushroom", + "music", + "must", + "mutual", + "myself", + "mystery", + "myth", + "naive", + "name", + "napkin", + "narrow", + "nasty", + "nation", + "nature", + "near", + "neck", + "need", + "negative", + "neglect", + "neither", + "nephew", + "nerve", + "nest", + "net", + "network", + "neutral", + "never", + "news", + "next", + "nice", + "night", + "noble", + "noise", + "nominee", + "noodle", + "normal", + "north", + "nose", + "notable", + "note", + "nothing", + "notice", + "novel", + "now", + "nuclear", + "number", + "nurse", + "nut", + "oak", + "obey", + "object", + "oblige", + "obscure", + "observe", + "obtain", + "obvious", + "occur", + "ocean", + "october", + "odor", + "off", + "offer", + "office", + "often", + "oil", + "okay", + "old", + "olive", + "olympic", + "omit", + "once", + "one", + "onion", + "online", + "only", + "open", + "opera", + "opinion", + "oppose", + "option", + "orange", + "orbit", + "orchard", + "order", + "ordinary", + "organ", + "orient", + "original", + "orphan", + "ostrich", + "other", + "outdoor", + "outer", + "output", + "outside", + "oval", + "oven", + "over", + "own", + "owner", + "oxygen", + "oyster", + "ozone", + "pact", + "paddle", + "page", + "pair", + "palace", + "palm", + "panda", + "panel", + "panic", + "panther", + "paper", + "parade", + "parent", + "park", + "parrot", + "party", + "pass", + "patch", + "path", + "patient", + "patrol", + "pattern", + "pause", + "pave", + "payment", + "peace", + "peanut", + "pear", + "peasant", + "pelican", + "pen", + "penalty", + "pencil", + "people", + "pepper", + "perfect", + "permit", + "person", + "pet", + "phone", + "photo", + "phrase", + "physical", + "piano", + "picnic", + "picture", + "piece", + "pig", + "pigeon", + "pill", + "pilot", + "pink", + "pioneer", + "pipe", + "pistol", + "pitch", + "pizza", + "place", + "planet", + "plastic", + "plate", + "play", + "please", + "pledge", + "pluck", + "plug", + "plunge", + "poem", + "poet", + "point", + "polar", + "pole", + "police", + "pond", + "pony", + "pool", + "popular", + "portion", + "position", + "possible", + "post", + "potato", + "pottery", + "poverty", + "powder", + "power", + "practice", + "praise", + "predict", + "prefer", + "prepare", + "present", + "pretty", + "prevent", + "price", + "pride", + "primary", + "print", + "priority", + "prison", + "private", + "prize", + "problem", + "process", + "produce", + "profit", + "program", + "project", + "promote", + "proof", + "property", + "prosper", + "protect", + "proud", + "provide", + "public", + "pudding", + "pull", + "pulp", + "pulse", + "pumpkin", + "punch", + "pupil", + "puppy", + "purchase", + "purity", + "purpose", + "purse", + "push", + "put", + "puzzle", + "pyramid", + "quality", + "quantum", + "quarter", + "question", + "quick", + "quit", + "quiz", + "quote", + "rabbit", + "raccoon", + "race", + "rack", + "radar", + "radio", + "rail", + "rain", + "raise", + "rally", + "ramp", + "ranch", + "random", + "range", + "rapid", + "rare", + "rate", + "rather", + "raven", + "raw", + "razor", + "ready", + "real", + "reason", + "rebel", + "rebuild", + "recall", + "receive", + "recipe", + "record", + "recycle", + "reduce", + "reflect", + "reform", + "refuse", + "region", + "regret", + "regular", + "reject", + "relax", + "release", + "relief", + "rely", + "remain", + "remember", + "remind", + "remove", + "render", + "renew", + "rent", + "reopen", + "repair", + "repeat", + "replace", + "report", + "require", + "rescue", + "resemble", + "resist", + "resource", + "response", + "result", + "retire", + "retreat", + "return", + "reunion", + "reveal", + "review", + "reward", + "rhythm", + "rib", + "ribbon", + "rice", + "rich", + "ride", + "ridge", + "rifle", + "right", + "rigid", + "ring", + "riot", + "ripple", + "risk", + "ritual", + "rival", + "river", + "road", + "roast", + "robot", + "robust", + "rocket", + "romance", + "roof", + "rookie", + "room", + "rose", + "rotate", + "rough", + "round", + "route", + "royal", + "rubber", + "rude", + "rug", + "rule", + "run", + "runway", + "rural", + "sad", + "saddle", + "sadness", + "safe", + "sail", + "salad", + "salmon", + "salon", + "salt", + "salute", + "same", + "sample", + "sand", + "satisfy", + "satoshi", + "sauce", + "sausage", + "save", + "say", + "scale", + "scan", + "scare", + "scatter", + "scene", + "scheme", + "school", + "science", + "scissors", + "scorpion", + "scout", + "scrap", + "screen", + "script", + "scrub", + "sea", + "search", + "season", + "seat", + "second", + "secret", + "section", + "security", + "seed", + "seek", + "segment", + "select", + "sell", + "seminar", + "senior", + "sense", + "sentence", + "series", + "service", + "session", + "settle", + "setup", + "seven", + "shadow", + "shaft", + "shallow", + "share", + "shed", + "shell", + "sheriff", + "shield", + "shift", + "shine", + "ship", + "shiver", + "shock", + "shoe", + "shoot", + "shop", + "short", + "shoulder", + "shove", + "shrimp", + "shrug", + "shuffle", + "shy", + "sibling", + "sick", + "side", + "siege", + "sight", + "sign", + "silent", + "silk", + "silly", + "silver", + "similar", + "simple", + "since", + "sing", + "siren", + "sister", + "situate", + "six", + "size", + "skate", + "sketch", + "ski", + "skill", + "skin", + "skirt", + "skull", + "slab", + "slam", + "sleep", + "slender", + "slice", + "slide", + "slight", + "slim", + "slogan", + "slot", + "slow", + "slush", + "small", + "smart", + "smile", + "smoke", + "smooth", + "snack", + "snake", + "snap", + "sniff", + "snow", + "soap", + "soccer", + "social", + "sock", + "soda", + "soft", + "solar", + "soldier", + "solid", + "solution", + "solve", + "someone", + "song", + "soon", + "sorry", + "sort", + "soul", + "sound", + "soup", + "source", + "south", + "space", + "spare", + "spatial", + "spawn", + "speak", + "special", + "speed", + "spell", + "spend", + "sphere", + "spice", + "spider", + "spike", + "spin", + "spirit", + "split", + "spoil", + "sponsor", + "spoon", + "sport", + "spot", + "spray", + "spread", + "spring", + "spy", + "square", + "squeeze", + "squirrel", + "stable", + "stadium", + "staff", + "stage", + "stairs", + "stamp", + "stand", + "start", + "state", + "stay", + "steak", + "steel", + "stem", + "step", + "stereo", + "stick", + "still", + "sting", + "stock", + "stomach", + "stone", + "stool", + "story", + "stove", + "strategy", + "street", + "strike", + "strong", + "struggle", + "student", + "stuff", + "stumble", + "style", + "subject", + "submit", + "subway", + "success", + "such", + "sudden", + "suffer", + "sugar", + "suggest", + "suit", + "summer", + "sun", + "sunny", + "sunset", + "super", + "supply", + "supreme", + "sure", + "surface", + "surge", + "surprise", + "surround", + "survey", + "suspect", + "sustain", + "swallow", + "swamp", + "swap", + "swarm", + "swear", + "sweet", + "swift", + "swim", + "swing", + "switch", + "sword", + "symbol", + "symptom", + "syrup", + "system", + "table", + "tackle", + "tag", + "tail", + "talent", + "talk", + "tank", + "tape", + "target", + "task", + "taste", + "tattoo", + "taxi", + "teach", + "team", + "tell", + "ten", + "tenant", + "tennis", + "tent", + "term", + "test", + "text", + "thank", + "that", + "theme", + "then", + "theory", + "there", + "they", + "thing", + "this", + "thought", + "three", + "thrive", + "throw", + "thumb", + "thunder", + "ticket", + "tide", + "tiger", + "tilt", + "timber", + "time", + "tiny", + "tip", + "tired", + "tissue", + "title", + "toast", + "tobacco", + "today", + "toddler", + "toe", + "together", + "toilet", + "token", + "tomato", + "tomorrow", + "tone", + "tongue", + "tonight", + "tool", + "tooth", + "top", + "topic", + "topple", + "torch", + "tornado", + "tortoise", + "toss", + "total", + "tourist", + "toward", + "tower", + "town", + "toy", + "track", + "trade", + "traffic", + "tragic", + "train", + "transfer", + "trap", + "trash", + "travel", + "tray", + "treat", + "tree", + "trend", + "trial", + "tribe", + "trick", + "trigger", + "trim", + "trip", + "trophy", + "trouble", + "truck", + "true", + "truly", + "trumpet", + "trust", + "truth", + "try", + "tube", + "tuition", + "tumble", + "tuna", + "tunnel", + "turkey", + "turn", + "turtle", + "twelve", + "twenty", + "twice", + "twin", + "twist", + "two", + "type", + "typical", + "ugly", + "umbrella", + "unable", + "unaware", + "uncle", + "uncover", + "under", + "undo", + "unfair", + "unfold", + "unhappy", + "uniform", + "unique", + "unit", + "universe", + "unknown", + "unlock", + "until", + "unusual", + "unveil", + "update", + "upgrade", + "uphold", + "upon", + "upper", + "upset", + "urban", + "urge", + "usage", + "use", + "used", + "useful", + "useless", + "usual", + "utility", + "vacant", + "vacuum", + "vague", + "valid", + "valley", + "valve", + "van", + "vanish", + "vapor", + "various", + "vast", + "vault", + "vehicle", + "velvet", + "vendor", + "venture", + "venue", + "verb", + "verify", + "version", + "very", + "vessel", + "veteran", + "viable", + "vibrant", + "vicious", + "victory", + "video", + "view", + "village", + "vintage", + "violin", + "virtual", + "virus", + "visa", + "visit", + "visual", + "vital", + "vivid", + "vocal", + "voice", + "void", + "volcano", + "volume", + "vote", + "voyage", + "wage", + "wagon", + "wait", + "walk", + "wall", + "walnut", + "want", + "warfare", + "warm", + "warrior", + "wash", + "wasp", + "waste", + "water", + "wave", + "way", + "wealth", + "weapon", + "wear", + "weasel", + "weather", + "web", + "wedding", + "weekend", + "weird", + "welcome", + "west", + "wet", + "whale", + "what", + "wheat", + "wheel", + "when", + "where", + "whip", + "whisper", + "wide", + "width", + "wife", + "wild", + "will", + "win", + "window", + "wine", + "wing", + "wink", + "winner", + "winter", + "wire", + "wisdom", + "wise", + "wish", + "witness", + "wolf", + "woman", + "wonder", + "wood", + "wool", + "word", + "work", + "world", + "worry", + "worth", + "wrap", + "wreck", + "wrestle", + "wrist", + "write", + "wrong", + "yard", + "year", + "yellow", + "you", + "young", + "youth", + "zebra", + "zero", + "zone", + "zoo", +}; + +} // namespace bitflash + +#endif diff --git a/src/makefile.mingw b/src/makefile.mingw index ae533ef..e4dee59 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -92,4 +92,11 @@ bitflash.exe: $(IMGUI_OBJS) $(NODE_OBJS) $(GUI_OBJS) @echo "Built: bitflash.exe" clean: - rm -rf obj bitflash.exe + rm -rf obj bitflash.exe test_bip32.exe + +test_bip32.exe: test_bip32.cpp bip32.cpp bip32.h bip39_english.h + $(CXX) $(CXXFLAGS) -o $@ test_bip32.cpp bip32.cpp -lssl -lcrypto -lcrypt32 + @echo "Built: test_bip32.exe" + +tests: test_bip32.exe + ./test_bip32.exe diff --git a/src/test_bip32.cpp b/src/test_bip32.cpp new file mode 100644 index 0000000..610a651 --- /dev/null +++ b/src/test_bip32.cpp @@ -0,0 +1,130 @@ +// Standalone tests for BIP39/BIP32 primitives. +// +// Build: +// g++ -std=gnu++14 test_bip32.cpp bip32.cpp -lssl -lcrypto -o test_bip32 + +#include "bip32.h" + +#include +#include +#include +#include + +using namespace bitflash; + +static int g_fail = 0; + +#define CHECK(cond, name) do { \ + if (cond) { printf(" ok %s\n", name); } \ + else { printf(" FAIL %s\n", name); g_fail++; } \ +} while (0) + +static int HexDigit(char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +static std::vector FromHex(const std::string& s) +{ + std::vector out; + if (s.size() % 2 != 0) + return out; + out.reserve(s.size() / 2); + for (size_t i = 0; i < s.size(); i += 2) + { + int hi = HexDigit(s[i]); + int lo = HexDigit(s[i + 1]); + if (hi < 0 || lo < 0) + return std::vector(); + out.push_back((unsigned char)((hi << 4) | lo)); + } + return out; +} + +static std::string ToHex(const std::vector& v) +{ + static const char* hex = "0123456789abcdef"; + std::string out; + out.reserve(v.size() * 2); + for (unsigned char c : v) + { + out.push_back(hex[c >> 4]); + out.push_back(hex[c & 0x0f]); + } + return out; +} + +int main() +{ + std::string err; + + printf("bip39_entropy_to_mnemonic\n"); + { + std::vector entropy(16, 0); + std::string mnemonic; + CHECK(BIP39EntropyToMnemonic(entropy, mnemonic, err), "zero entropy encodes"); + CHECK(mnemonic == + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + "matches BIP39 vector"); + } + + printf("bip39_validation\n"); + { + std::string normalized; + CHECK(BIP39ValidateMnemonic( + " Abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about ", + normalized, err), + "valid phrase normalizes"); + CHECK(normalized == + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + "normalized text matches"); + CHECK(!BIP39ValidateMnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon", + normalized, err), + "bad checksum rejected"); + } + + printf("bip39_seed\n"); + { + std::vector seed; + const std::string mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + CHECK(BIP39MnemonicToSeed(mnemonic, "TREZOR", seed, err), "mnemonic derives seed"); + CHECK(ToHex(seed) == + "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e5349553" + "1f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04", + "matches BIP39 seed vector"); + } + + printf("bip32_master\n"); + { + std::vector seed = FromHex("000102030405060708090a0b0c0d0e0f"); + BIP32PrivateNode master; + CHECK(BIP32MasterFromSeed(seed, master, err), "master key derives"); + CHECK(ToHex(master.privateKey) == + "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35", + "matches BIP32 master private key"); + CHECK(ToHex(master.chainCode) == + "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508", + "matches BIP32 master chain code"); + + BIP32PrivateNode childA, childB; + CHECK(BIP32DeriveHardenedChild(master, 0, childA, err), "hardened child derives"); + CHECK(BIP32DeriveHardenedChild(master, 0, childB, err), "hardened child re-derives"); + CHECK(childA.privateKey == childB.privateKey && + childA.chainCode == childB.chainCode, + "same child path is deterministic"); + CHECK(childA.privateKey != master.privateKey && + childA.chainCode != master.chainCode, + "child differs from parent"); + CHECK(!BIP32DeriveHardenedChild(master, 0x80000000U, childA, err), + "pre-hardened index rejected"); + } + + printf("\n%s (%d failures)\n", + g_fail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", g_fail); + return g_fail == 0 ? 0 : 1; +} -- cgit v1.2.3 From a2726cad9e9e11c73ae341ec627a696cdeb63edc Mon Sep 17 00:00:00 2001 From: mtg0001 <106211915+mtg0001@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:15:15 -0300 Subject: Add wallet keypool self-test (#91) Co-authored-by: Gilcimar Duarte --- src/Makefile | 9 +- src/main.cpp | 5 + src/main_gui.cpp | 6 ++ src/makefile.mingw | 9 +- src/selftest.cpp | 275 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/selftest.h | 11 +++ 6 files changed, 309 insertions(+), 6 deletions(-) create mode 100644 src/selftest.cpp create mode 100644 src/selftest.h (limited to 'src/Makefile') diff --git a/src/Makefile b/src/Makefile index 4e104c0..248e62c 100644 --- a/src/Makefile +++ b/src/Makefile @@ -41,14 +41,16 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \ HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ - randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h compat.h + randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ + selftest.h compat.h IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ - obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o + obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ + obj/selftest.o GUI_OBJS = obj/gui.o obj/main_gui.o @@ -126,5 +128,6 @@ clean: test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h $(CXX) $(CXXFLAGS) -o $@ test_bip32.cpp bip32.cpp -lssl -lcrypto -tests: test_bip32 +tests: test_bip32 bitflash-node ./test_bip32 + ./bitflash-node /selftest=wallet-keypool diff --git a/src/main.cpp b/src/main.cpp index 68ae44c..82a98d2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -217,6 +217,11 @@ vector GetKeyFromPool() vector vchPubKey = mi->second; mapKeyPool.erase(mi); CWalletDB().ErasePool(nIndex); + // Leave the wallet with a full backup window after each draw too, + // not only immediately before a draw. If topping up fails, the key + // already chosen is still safe to use -- its private half was + // written before it entered the pool. + TopUpKeyPool(); return vchPubKey; } } diff --git a/src/main_gui.cpp b/src/main_gui.cpp index c9e27a9..2bcdb83 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -1,6 +1,7 @@ // Bitflash entry point -- starts node threads then runs GUI (or headless). #include "headers_core.h" +#include "selftest.h" #include // hardware_concurrency, to sanity-check /genproclimit #ifndef _WIN32 #include @@ -48,6 +49,7 @@ static void PrintUsage() printf(" /debug\n"); printf(" /gen\n"); printf(" /nogui or /daemon\n"); + printf(" /selftest=wallet-keypool\n"); printf("\n"); printf("Mining mode:\n"); printf(" /operator\n"); @@ -219,6 +221,10 @@ int main(int argc, char* argv[]) ParseStartupArguments(argc, argv); + string strSelfTest = argval2(argc, argv, "/selftest", "-selftest"); + if (!strSelfTest.empty()) + return RunSelfTest(strSelfTest); + // Berkeley DB reports failure by throwing, and CDB's constructor lets it // through. Nothing on this path caught anything, so an unreadable // blkindex.dat or wallet.dat -- one written by another platform's Berkeley diff --git a/src/makefile.mingw b/src/makefile.mingw index e4dee59..c263f4e 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -41,13 +41,15 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ - obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o + obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ + obj/selftest.o GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ - randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h + randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ + selftest.h all: bitflash.exe @@ -98,5 +100,6 @@ test_bip32.exe: test_bip32.cpp bip32.cpp bip32.h bip39_english.h $(CXX) $(CXXFLAGS) -o $@ test_bip32.cpp bip32.cpp -lssl -lcrypto -lcrypt32 @echo "Built: test_bip32.exe" -tests: test_bip32.exe +tests: test_bip32.exe bitflash.exe ./test_bip32.exe + ./bitflash.exe /selftest=wallet-keypool diff --git a/src/selftest.cpp b/src/selftest.cpp new file mode 100644 index 0000000..d649b23 --- /dev/null +++ b/src/selftest.cpp @@ -0,0 +1,275 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// Integration self-tests that need the real wallet/database code linked into +// the node. These are intentionally run behind an explicit command-line flag +// and against a temporary data directory. + +#include "headers_core.h" +#include "selftest.h" + +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#endif + +static std::string HexStrLocal(const std::vector& v) +{ + static const char* hex = "0123456789abcdef"; + std::string out; + out.reserve(v.size() * 2); + for (unsigned char c : v) + { + out.push_back(hex[c >> 4]); + out.push_back(hex[c & 0x0f]); + } + return out; +} + +static bool MakeTempDir(std::string& out) +{ +#ifdef _WIN32 + char base[MAX_PATH + 1] = {0}; + if (!GetTempPathA(MAX_PATH, base)) + return false; + for (int i = 0; i < 100; i++) + { + std::string path = strprintf("%sbitflash-selftest-%lu-%lld-%d", + base, (unsigned long)GetCurrentProcessId(), (long long)GetTime(), i); + if (CreateDirectoryA(path.c_str(), NULL)) + { + out = path; + return true; + } + } + return false; +#else + char tmpl[256]; + snprintf(tmpl, sizeof(tmpl), "/tmp/bitflash-selftest-%ld-XXXXXX", (long)getpid()); + char* p = mkdtemp(tmpl); + if (!p) + return false; + out = p; + return true; +#endif +} + +static bool GetCurrentDir(std::string& out) +{ +#ifdef _WIN32 + char buf[MAX_PATH + 1] = {0}; + DWORD n = GetCurrentDirectoryA(MAX_PATH, buf); + if (n == 0 || n > MAX_PATH) + return false; + out = buf; + return true; +#else + char buf[4096]; + if (!getcwd(buf, sizeof(buf))) + return false; + out = buf; + return true; +#endif +} + +static bool SetCurrentDir(const std::string& path) +{ +#ifdef _WIN32 + return SetCurrentDirectoryA(path.c_str()) != 0; +#else + return chdir(path.c_str()) == 0; +#endif +} + +static void RemoveTree(const std::string& path) +{ +#ifdef _WIN32 + DWORD attr = GetFileAttributesA(path.c_str()); + if (attr == INVALID_FILE_ATTRIBUTES) + return; + if (!(attr & FILE_ATTRIBUTE_DIRECTORY)) + { + DeleteFileA(path.c_str()); + return; + } + + WIN32_FIND_DATAA findData; + std::string pattern = path + "\\*"; + HANDLE hFind = FindFirstFileA(pattern.c_str(), &findData); + if (hFind != INVALID_HANDLE_VALUE) + { + do + { + std::string name = findData.cFileName; + if (name == "." || name == "..") + continue; + RemoveTree(path + "\\" + name); + } + while (FindNextFileA(hFind, &findData)); + FindClose(hFind); + } + RemoveDirectoryA(path.c_str()); +#else + struct stat st; + if (lstat(path.c_str(), &st) != 0) + return; + if (!S_ISDIR(st.st_mode)) + { + unlink(path.c_str()); + return; + } + + DIR* dir = opendir(path.c_str()); + if (dir) + { + struct dirent* ent; + while ((ent = readdir(dir)) != NULL) + { + std::string name = ent->d_name; + if (name == "." || name == "..") + continue; + RemoveTree(path + "/" + name); + } + closedir(dir); + } + rmdir(path.c_str()); +#endif +} + +static bool WalletHasPrivateKey(const std::vector& vchPubKey) +{ + CPrivKey priv; + return CWalletDB("r").ReadKey(vchPubKey, priv) && !priv.empty(); +} + +static bool Check(bool condition, const char* message) +{ + if (condition) + { + printf(" ok %s\n", message); + return true; + } + printf(" FAIL %s\n", message); + return false; +} + +static int RunWalletKeyPoolSelfTest() +{ + printf("wallet-keypool self-test\n"); + + std::string tmp; + std::string cwd; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) + { + printf(" FAIL could not move into the temporary data directory\n"); + RemoveTree(tmp); + return 1; + } + + int nFail = 0; + strSetDataDir = tmp; + printf(" temp datadir: %s\n", tmp.c_str()); + + try + { + if (!LoadWallet()) + throw std::runtime_error("LoadWallet failed"); + + TopUpKeyPool(); + int nInitialPoolSize = 0; + CRITICAL_BLOCK(cs_keyPool) + nInitialPoolSize = (int)mapKeyPool.size(); + nFail += Check(nInitialPoolSize == KEYPOOL_SIZE, "TopUpKeyPool fills the pool") ? 0 : 1; + + bool fPooledKeysStored = true; + CRITICAL_BLOCK(cs_keyPool) + { + for (map >::const_iterator mi = mapKeyPool.begin(); + mi != mapKeyPool.end(); ++mi) + if (!WalletHasPrivateKey(mi->second)) + fPooledKeysStored = false; + } + nFail += Check(fPooledKeysStored, "every pooled public key has a stored private key") ? 0 : 1; + + std::vector > drawn; + std::mutex drawnMutex; + const int nThreads = 8; + const int nDrawsPerThread = 32; + std::vector threads; + + for (int t = 0; t < nThreads; t++) + { + threads.emplace_back([&drawn, &drawnMutex, nDrawsPerThread]() { + for (int i = 0; i < nDrawsPerThread; i++) + { + std::vector key = GetKeyFromPool(); + std::lock_guard lock(drawnMutex); + drawn.push_back(key); + } + }); + } + for (std::thread& t : threads) + t.join(); + + nFail += Check((int)drawn.size() == nThreads * nDrawsPerThread, + "all concurrent draws returned a key") ? 0 : 1; + + std::set seen; + bool fUnique = true; + bool fAllStored = true; + for (const std::vector& key : drawn) + { + if (!seen.insert(HexStrLocal(key)).second) + fUnique = false; + if (!WalletHasPrivateKey(key)) + fAllStored = false; + } + + nFail += Check(fUnique, "concurrent draws never return the same key") ? 0 : 1; + nFail += Check(fAllStored, "each drawn key was already stored in wallet.dat") ? 0 : 1; + int nPoolSize = 0; + CRITICAL_BLOCK(cs_keyPool) + nPoolSize = (int)mapKeyPool.size(); + nFail += Check(nPoolSize == KEYPOOL_SIZE, "drawing leaves the key pool topped up") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + DBFlush(true); + SetCurrentDir(cwd); + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + return nFail == 0 ? 0 : 1; +} + +int RunSelfTest(const std::string& name) +{ + if (name == "wallet-keypool") + return RunWalletKeyPoolSelfTest(); + + printf("Unknown self-test '%s'\n", name.c_str()); + printf("Known self-tests: wallet-keypool\n"); + return 1; +} diff --git a/src/selftest.h b/src/selftest.h new file mode 100644 index 0000000..d3699c6 --- /dev/null +++ b/src/selftest.h @@ -0,0 +1,11 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. + +#ifndef BITFLASH_SELFTEST_H +#define BITFLASH_SELFTEST_H + +#include + +int RunSelfTest(const std::string& name); + +#endif -- cgit v1.2.3 From 593cf346dbce0a8ce46a13a73a4f78ddd044ab52 Mon Sep 17 00:00:00 2001 From: mtg0001 <106211915+mtg0001@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:45:54 -0300 Subject: Avoid MSYS2 self-test argument conversion (#94) Co-authored-by: Gilcimar Duarte --- src/Makefile | 2 +- src/makefile.mingw | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/Makefile') diff --git a/src/Makefile b/src/Makefile index 248e62c..d4b1837 100644 --- a/src/Makefile +++ b/src/Makefile @@ -130,4 +130,4 @@ test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h tests: test_bip32 bitflash-node ./test_bip32 - ./bitflash-node /selftest=wallet-keypool + ./bitflash-node -selftest=wallet-keypool diff --git a/src/makefile.mingw b/src/makefile.mingw index c263f4e..4f297d9 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -102,4 +102,4 @@ test_bip32.exe: test_bip32.cpp bip32.cpp bip32.h bip39_english.h tests: test_bip32.exe bitflash.exe ./test_bip32.exe - ./bitflash.exe /selftest=wallet-keypool + ./bitflash.exe -selftest=wallet-keypool -- cgit v1.2.3 From ce59d8c72c667ce638bd12f37db26d8b3408a5d2 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Sat, 1 Aug 2026 17:53:17 -0300 Subject: wallet: derive the key pool from a seed, so a phrase could bring it back (#93) Adds BIP32 derivation behind the wallet key pool, with no user-facing way to create a seed yet. wallet.dat gains hdmaster/hdchaincode/hdnext; the mnemonic is never stored. TopUpKeyPool derives m/n' when a seed exists and falls back to a random key, with a log line, if derivation fails. New -selftest=wallet-hd, wired into make tests on both platforms. --- src/Makefile | 3 +- src/db.cpp | 12 +++++ src/db.h | 27 ++++++++++ src/key.h | 34 ++++++++++++ src/main.cpp | 112 +++++++++++++++++++++++++++++++++++++++- src/main.h | 28 ++++++++++ src/main_gui.cpp | 18 +++++++ src/makefile.mingw | 3 +- src/selftest.cpp | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 9 files changed, 382 insertions(+), 4 deletions(-) (limited to 'src/Makefile') diff --git a/src/Makefile b/src/Makefile index d4b1837..bbdc9e2 100644 --- a/src/Makefile +++ b/src/Makefile @@ -50,7 +50,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ - obj/selftest.o + obj/selftest.o obj/bip32.o GUI_OBJS = obj/gui.o obj/main_gui.o @@ -131,3 +131,4 @@ test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h tests: test_bip32 bitflash-node ./test_bip32 ./bitflash-node -selftest=wallet-keypool + ./bitflash-node -selftest=wallet-hd diff --git a/src/db.cpp b/src/db.cpp index d482c5a..294920b 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -639,6 +639,18 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) { ssValue >> vchDefaultKeyRet; } + else if (strType == "hdmaster") + { + ssValue >> vchHDMaster; + } + else if (strType == "hdchaincode") + { + ssValue >> vchHDChainCode; + } + else if (strType == "hdnext") + { + ssValue >> nHDNext; + } else if (strType == "pool") { int64 nIndex; diff --git a/src/db.h b/src/db.h index f51047e..01b8583 100644 --- a/src/db.h +++ b/src/db.h @@ -386,6 +386,33 @@ public: return Write(string("defaultkey"), vchPubKey); } + // --- Deterministic (BIP32) seed --------------------------------------- + // + // Stored as the master key and chain code rather than the mnemonic: the + // twelve words are the user's to write down, and keeping a copy of them in + // the file they are meant to protect defeats the point of having them. + // + // "hdnext" is the next child index to derive, so the sequence a restore + // reproduces is the same one this wallet handed out. + bool ReadHDMaster(vector& vchMasterRet, vector& vchChainCodeRet) + { + vchMasterRet.clear(); + vchChainCodeRet.clear(); + return Read(string("hdmaster"), vchMasterRet) + && Read(string("hdchaincode"), vchChainCodeRet); + } + + bool WriteHDMaster(const vector& vchMaster, const vector& vchChainCode) + { + return Write(string("hdmaster"), vchMaster) + && Write(string("hdchaincode"), vchChainCode); + } + + bool WriteHDNext(unsigned int nNext) + { + return Write(string("hdnext"), nNext); + } + // Keys generated ahead of time and not yet handed out. The private key of // each is already stored under its own "key" record by AddKey; a "pool" // record only says that this one is still unspoken for. diff --git a/src/key.h b/src/key.h index 26e1ae2..6cc2a24 100644 --- a/src/key.h +++ b/src/key.h @@ -86,6 +86,40 @@ public: return true; } + // Load a raw 32-byte scalar, the form BIP32 derivation produces. + // + // SetPrivKey above takes DER and cannot be used for this: handing it a + // scalar makes d2i_ECPrivateKey fail, and if the caller ignores the return + // value -- as one attempt at this did -- the key is left with no material + // at all and the address it produces has no private half. Coins paid there + // are gone. Hence a separate entry point rather than a looser SetPrivKey. + // + // The public half is computed here rather than read from anywhere: + // pub = G * secret. + bool SetSecret(const std::vector& vchSecret) + { + if (vchSecret.size() != 32) + return false; + + bool fOk = false; + BIGNUM* bn = BN_bin2bn(&vchSecret[0], 32, NULL); + EC_POINT* pubkey = NULL; + if (bn && EC_KEY_set_private_key(pkey, bn)) + { + const EC_GROUP* group = EC_KEY_get0_group(pkey); + pubkey = EC_POINT_new(group); + if (pubkey && + EC_POINT_mul(group, pubkey, bn, NULL, NULL, NULL) && + EC_KEY_set_public_key(pkey, pubkey)) + fOk = true; + } + if (pubkey) + EC_POINT_free(pubkey); + if (bn) + BN_clear_free(bn); // it held the secret + return fOk; + } + CPrivKey GetPrivKey() const { unsigned int nSize = i2d_ECPrivateKey(pkey, NULL); diff --git a/src/main.cpp b/src/main.cpp index 82a98d2..e5754e6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,6 +14,7 @@ #include #include #include +#include "bip32.h" #include "btfaddr.h" #include "btftunnel.h" #ifndef _WIN32 @@ -178,6 +179,66 @@ vector GenerateNewKey() CCriticalSection cs_keyPool; map > mapKeyPool; +vector vchHDMaster; +vector vchHDChainCode; +unsigned int nHDNext = 0; + +bool DeriveHDKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet) +{ + if (!HaveHDSeed()) + { + strErrorRet = "no seed"; + return false; + } + + bitflash::BIP32PrivateNode parent; + parent.privateKey = vchHDMaster; + parent.chainCode = vchHDChainCode; + + bitflash::BIP32PrivateNode child; + if (!bitflash::BIP32DeriveHardenedChild(parent, nIndex, child, strErrorRet)) + return false; + + if (!keyRet.SetSecret(child.privateKey)) + { + strErrorRet = "derived scalar is not a usable key"; + return false; + } + return true; +} + +bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet) +{ + string strNormalized; + if (!bitflash::BIP39ValidateMnemonic(strMnemonic, strNormalized, strErrorRet)) + return false; + + vector vchSeed; + if (!bitflash::BIP39MnemonicToSeed(strNormalized, "", vchSeed, strErrorRet)) + return false; + + bitflash::BIP32PrivateNode master; + if (!bitflash::BIP32MasterFromSeed(vchSeed, master, strErrorRet)) + return false; + + CRITICAL_BLOCK(cs_keyPool) + { + // Written before the globals change, so a failed write leaves the + // wallet on the seed it already had rather than on one that exists + // only in memory. + if (!CWalletDB().WriteHDMaster(master.privateKey, master.chainCode) || + !CWalletDB().WriteHDNext(0)) + { + strErrorRet = "could not write the seed to wallet.dat"; + return false; + } + vchHDMaster = master.privateKey; + vchHDChainCode = master.chainCode; + nHDNext = 0; + } + return true; +} + void TopUpKeyPool() { CRITICAL_BLOCK(cs_keyPool) @@ -191,7 +252,30 @@ void TopUpKeyPool() nIndex = (--mapKeyPool.end())->first + 1; CKey key; - key.MakeNewKey(); + if (HaveHDSeed()) + { + // Derived, so a recovery phrase can reproduce this exact key. + // A derivation that fails is not a reason to stop filling the + // pool -- fall back to a random key, which is what a wallet + // without a seed uses anyway. It will not come back from the + // phrase, and that is better than a node that cannot mine. + string strError; + if (DeriveHDKey(nHDNext, key, strError)) + { + nHDNext++; + CWalletDB().WriteHDNext(nHDNext); + } + else + { + printf("TopUpKeyPool() : derivation at index %u failed (%s), " + "falling back to a random key\n", nHDNext, strError.c_str()); + key.MakeNewKey(); + } + } + else + { + key.MakeNewKey(); + } // AddKey writes the private key. Order matters: the key is in // wallet.dat before anything can point at it, so a crash between // the two writes costs an unused key, never a usable one. @@ -310,6 +394,32 @@ bool AddToWalletIfMine(const CTransaction& tx, const CBlock* pblock) return true; } +bool CanScanWalletTransactions(string& strErrorRet) +{ + strErrorRet.clear(); + + if (pindexGenesisBlock == NULL || pindexBest == NULL || hashBestChain == 0 || nBestHeight < 0) + { + strErrorRet = "The block index is not loaded, so the wallet scan cannot " + "prove whether imported or restored keys own coins."; + return false; + } + + if (nBestHeight == 0) + { + strErrorRet = "The local chain is only at height 0. A wallet scan would " + "inspect only the genesis block and could incorrectly " + "report zero transactions. Start the node and let it sync " + "before scanning. If this happened after copying a data " + "directory, keep wallet.dat safe and rebuild or re-download " + "the block data instead of trusting the copied database/ " + "environment."; + return false; + } + + return true; +} + int ScanForWalletTransactions(CBlockIndex* pindexStart) { // Walk the chain looking for transactions that belong to keys this wallet diff --git a/src/main.h b/src/main.h index 8d9b7e6..a04a4e3 100644 --- a/src/main.h +++ b/src/main.h @@ -141,6 +141,33 @@ static const int KEYPOOL_SIZE = 100; extern CCriticalSection cs_keyPool; extern map > mapKeyPool; +// --- Deterministic wallet (BIP32) ----------------------------------------- +// +// When a seed is present the key pool is derived from it -- m/0'/n, hardened, +// n increasing -- instead of being made of random keys. That is what lets a +// recovery phrase bring a wallet back: the same twelve words reproduce the same +// keys in the same order. +// +// A wallet without a seed keeps working exactly as before. Nothing here +// migrates an existing wallet on its own: the seed is created only when the +// user asks for a phrase and confirms they have written it down, because a +// phrase generated silently is a backup nobody has. +// +// Empty until a seed exists. +extern vector vchHDMaster; // 32-byte master private key (IL) +extern vector vchHDChainCode; // 32 bytes (IR) +extern unsigned int nHDNext; // next child index to derive +inline bool HaveHDSeed() { return vchHDMaster.size() == 32 && vchHDChainCode.size() == 32; } + +// Install a seed derived from a mnemonic, replacing any existing one, and reset +// the derivation counter. Writes to wallet.dat. Returns false and leaves the +// wallet untouched if the phrase is not valid. +bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet); + +// Derive the child at nIndex and return it as a key. Used by the key pool and +// by restore, which needs to run ahead of the pool. +bool DeriveHDKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet); + // Fill the pool back up to KEYPOOL_SIZE. Every key it creates is written to // wallet.dat before it is offered to anybody. void TopUpKeyPool(); @@ -156,6 +183,7 @@ void ReacceptWalletTransactions(); // a key this wallet holds. Returns how many were added or updated. Needed by // anything that puts a key into the wallet after the fact -- an import, a // restored backup, and later a recovery phrase. +bool CanScanWalletTransactions(string& strErrorRet); int ScanForWalletTransactions(CBlockIndex* pindexStart); // Returns how many wallet transactions the chain corrected. int RescanSpentFlags(); diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 2bcdb83..aa9c23d 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -323,6 +323,16 @@ int main(int argc, char* argv[]) // import having failed. if (nAdded > 0) { + string strScanError; + if (!CanScanWalletTransactions(strScanError)) + { + fprintf(stderr, "Cannot scan for imported coins: %s\n", + strScanError.c_str()); + fprintf(stderr, "The key import was written, but the wallet " + "balance was not proven against the chain.\n"); + DBFlush(true); + return 1; + } printf("Looking through the chain for coins belonging to the imported key(s)...\n"); int nFound = ScanForWalletTransactions(pindexGenesisBlock); printf("Found %d transaction(s). Start the node normally to see the balance.\n", nFound); @@ -339,6 +349,14 @@ int main(int argc, char* argv[]) // balance is wrong for this reason. if (arg(argc,argv,"/rescan") || arg(argc,argv,"-rescan")) { + string strScanError; + if (!CanScanWalletTransactions(strScanError)) + { + fprintf(stderr, "Cannot rescan wallet transactions: %s\n", + strScanError.c_str()); + DBFlush(true); + return 1; + } printf("Rescanning the chain for this wallet's transactions...\n"); int nFound = ScanForWalletTransactions(pindexGenesisBlock); printf("Rescan done: %d transaction(s) added or updated.\n", nFound); diff --git a/src/makefile.mingw b/src/makefile.mingw index 4f297d9..07ae034 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -42,7 +42,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ - obj/selftest.o + obj/selftest.o obj/bip32.o GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o @@ -103,3 +103,4 @@ test_bip32.exe: test_bip32.cpp bip32.cpp bip32.h bip39_english.h tests: test_bip32.exe bitflash.exe ./test_bip32.exe ./bitflash.exe -selftest=wallet-keypool + ./bitflash.exe -selftest=wallet-hd diff --git a/src/selftest.cpp b/src/selftest.cpp index abe1d15..6792ae7 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -314,14 +314,161 @@ static void AttachTerminal() #endif } +static int RunWalletHDSelfTest() +{ + fflush(stdout); + printf("wallet-hd self-test\n"); + + std::string tmp; + std::string cwd; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) + { + printf(" FAIL could not move into the temporary data directory\n"); + RemoveTree(tmp); + return 1; + } + + int nFail = 0; + strSetDataDir = tmp; + printf(" temp datadir: %s\n", tmp.c_str()); + + // Test-vector phrases, so nothing here depends on randomness. + const std::string strPhraseA = + "abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon about"; + const std::string strPhraseB = + "legal winner thank year wave sausage worth useful " + "legal winner thank yellow"; + + try + { + if (!LoadWallet()) + throw std::runtime_error("LoadWallet failed"); + + std::string strError; + nFail += Check(SetHDSeedFromMnemonic(strPhraseA, strError), + "a valid phrase installs a seed") ? 0 : 1; + nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1; + + std::vector vchBefore = vchHDMaster; + nFail += Check(!SetHDSeedFromMnemonic("not a mnemonic at all", strError), + "an invalid phrase is refused") ? 0 : 1; + nFail += Check(vchHDMaster == vchBefore, + "a refused phrase leaves the existing seed alone") ? 0 : 1; + + // The property the whole feature exists for: the same words give back + // the same keys, in the same order. + std::vector first; + for (unsigned int i = 0; i < 5; i++) + { + CKey key; + if (!DeriveHDKey(i, key, strError)) + throw std::runtime_error("derivation failed: " + strError); + first.push_back(HexStrLocal(key.GetPubKey())); + } + + SetHDSeedFromMnemonic(strPhraseA, strError); + std::vector again; + for (unsigned int i = 0; i < 5; i++) + { + CKey key; + if (!DeriveHDKey(i, key, strError)) + throw std::runtime_error("derivation failed: " + strError); + again.push_back(HexStrLocal(key.GetPubKey())); + } + nFail += Check(first == again, + "the same phrase derives the same keys in the same order") ? 0 : 1; + + SetHDSeedFromMnemonic(strPhraseB, strError); + std::vector other; + for (unsigned int i = 0; i < 5; i++) + { + CKey key; + if (!DeriveHDKey(i, key, strError)) + throw std::runtime_error("derivation failed: " + strError); + other.push_back(HexStrLocal(key.GetPubKey())); + } + nFail += Check(first != other, "a different phrase derives different keys") ? 0 : 1; + + std::set distinct(first.begin(), first.end()); + nFail += Check(distinct.size() == first.size(), + "consecutive indices give distinct keys") ? 0 : 1; + + // With a seed installed the pool must be derived from it, and the + // counter must move exactly once per key. + SetHDSeedFromMnemonic(strPhraseA, strError); + CRITICAL_BLOCK(cs_keyPool) + mapKeyPool.clear(); + unsigned int nNextBefore = nHDNext; + TopUpKeyPool(); + + int nPool = 0; + CRITICAL_BLOCK(cs_keyPool) + nPool = (int)mapKeyPool.size(); + nFail += Check(nPool == KEYPOOL_SIZE, "the derived pool fills") ? 0 : 1; + nFail += Check(nHDNext == nNextBefore + (unsigned int)KEYPOOL_SIZE, + "the derivation counter advances once per pooled key") ? 0 : 1; + + std::set derived; + for (unsigned int i = nNextBefore; i < nHDNext; i++) + { + CKey key; + if (!DeriveHDKey(i, key, strError)) + throw std::runtime_error("derivation failed: " + strError); + derived.insert(HexStrLocal(key.GetPubKey())); + } + + bool fStored = true; + bool fFromSeed = true; + CRITICAL_BLOCK(cs_keyPool) + { + for (map >::const_iterator mi = mapKeyPool.begin(); + mi != mapKeyPool.end(); ++mi) + { + if (!WalletHasPrivateKey(mi->second)) + fStored = false; + if (!derived.count(HexStrLocal(mi->second))) + fFromSeed = false; + } + } + nFail += Check(fStored, "every derived pooled key is stored in wallet.dat") ? 0 : 1; + nFail += Check(fFromSeed, "every pooled key came from the seed, not from chance") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + DBFlush(true); + SetCurrentDir(cwd); + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + int RunSelfTest(const std::string& name) { AttachTerminal(); if (name == "wallet-keypool") return RunWalletKeyPoolSelfTest(); + if (name == "wallet-hd") + return RunWalletHDSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - printf("Known self-tests: wallet-keypool\n"); + printf("Known self-tests: wallet-keypool, wallet-hd\n"); return 1; } -- cgit v1.2.3 From 7de7ef89d3365f8f9c2eb2b6ed0193c3033e221c Mon Sep 17 00:00:00 2001 From: Bitflash Date: Sat, 1 Aug 2026 17:57:07 -0300 Subject: wallet: create and restore a recovery phrase (#96) Adds -newphrase, -restorephrase, -restoredepth, -newaddress and -showderived, plus the window buttons that reach the same code. Restore derives forward in batches of 100 and rescans after each, stopping when a whole batch turns up nothing; the phrase is shown on the terminal only, never in debug.log. Proved end to end on a live wallet: paid a derived address, deleted wallet.dat, restored from the twelve words and recovered the coin. Coins on the pre-seed default address do not come back, and the command says so. --- src/Makefile | 4 +- src/gui.cpp | 258 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main_gui.cpp | 38 ++++++++ src/makefile.mingw | 4 +- src/selftest.cpp | 30 +------ src/util.h | 27 ++++++ src/walletcmd.cpp | 237 ++++++++++++++++++++++++++++++++++++++++++++++++ src/walletcmd.h | 43 +++++++++ 8 files changed, 610 insertions(+), 31 deletions(-) create mode 100644 src/walletcmd.cpp create mode 100644 src/walletcmd.h (limited to 'src/Makefile') diff --git a/src/Makefile b/src/Makefile index bbdc9e2..3eaf2ba 100644 --- a/src/Makefile +++ b/src/Makefile @@ -42,7 +42,7 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \ HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ - selftest.h compat.h + selftest.h walletcmd.h compat.h IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o @@ -50,7 +50,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ - obj/selftest.o obj/bip32.o + obj/selftest.o obj/bip32.o obj/walletcmd.o GUI_OBJS = obj/gui.o obj/main_gui.o diff --git a/src/gui.cpp b/src/gui.cpp index 51b5d5f..e800244 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -12,6 +12,11 @@ #include #include #include +#include +#include +#include +#include "bip32.h" +#include "walletcmd.h" #include "font_roboto.h" // --------------------------------------------------------------------------- @@ -910,6 +915,220 @@ static void DrawOptionsDialog() // --------------------------------------------------------------------------- // Wallet safety dialog // --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// Recovery phrase +// +// The phrase is shown once, and the seed is installed only after the user says +// they have written it down. Generating it and storing it in the same breath +// would hand somebody a wallet whose only backup is twelve words they may have +// closed the window on -- which is worse than no phrase at all, because they +// would believe they had one. +// +// Nothing here writes the words to disk or to debug.log. They live in one +// std::string that is cleared the moment the dialog closes. +// --------------------------------------------------------------------------- +static bool g_showCreatePhrase = false; +static bool g_showRestorePhrase = false; +static std::string g_pendingMnemonic; +static bool g_phraseWrittenDown = false; +static std::string g_phraseStatus; + +static char g_restoreInput[600] = {}; +static std::string g_restoreStatus; +static std::atomic g_restoreRunning(false); +static std::atomic g_restoreDerived(0); +static std::atomic g_restoreRecovered(0); +static std::string g_restoreResult; +static std::mutex g_restoreResultMutex; + +static void RestoreProgress(void*, int nDerived, int nRecovered) +{ + g_restoreDerived.store(nDerived); + g_restoreRecovered.store(nRecovered); +} + +static void RestoreThread(std::string strPhrase) +{ + std::string strError; + int nRecovered = 0, nDerived = 0; + bool fOk = RestoreFromPhrase(strPhrase, 0, RestoreProgress, NULL, + strError, nRecovered, nDerived); + { + std::lock_guard lock(g_restoreResultMutex); + if (!fOk) + g_restoreResult = "Nothing was changed. " + strError; + else if (nRecovered > 0) + g_restoreResult = strprintf( + "Restored %d transaction(s) across %d derived addresses. " + "Restart the node to see the balance.", nRecovered, nDerived); + else + g_restoreResult = strprintf( + "No transactions found for the first %d addresses of that phrase. " + "Check the words and their order.", nDerived); + } + g_restoreRunning.store(false); + g_needRefresh = true; +} + +static void DrawCreatePhraseDialog() +{ + if (!g_showCreatePhrase) return; + + ImGui::SetNextWindowSize(ImVec2(640.0f, 340.0f), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), + ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + if (ImGui::Begin("Recovery Phrase", &g_showCreatePhrase, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) + { + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), + "Write these twelve words down, in order, on paper."); + ImGui::TextWrapped( + "They are the only thing that can rebuild this wallet if the file is lost. " + "Anyone who reads them can spend your coins. They are not stored anywhere, " + "and this is the only time they will be shown."); + ImGui::Spacing(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.10f, 0.10f, 0.10f, 1.0f)); + ImGui::BeginChild("##phrase", ImVec2(0.0f, 70.0f), true); + ImGui::PushTextWrapPos(0.0f); + ImGui::TextColored(ImVec4(0.55f, 1.0f, 0.6f, 1.0f), "%s", g_pendingMnemonic.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndChild(); + ImGui::PopStyleColor(); + + ImGui::Spacing(); + ImGui::TextWrapped( + "Keys already in this wallet are NOT covered by the phrase -- they existed " + "before it did. Keep your file backups as well."); + ImGui::Spacing(); + + ImGui::Checkbox("I have written these words down", &g_phraseWrittenDown); + ImGui::Spacing(); + + if (!g_phraseWrittenDown) + ImGui::BeginDisabled(); + if (ImGui::Button("Finish", ImVec2(120.0f, 0.0f))) + { + std::string strError; + if (SetHDSeedFromMnemonic(g_pendingMnemonic, strError)) + { + TopUpKeyPool(); + g_phraseStatus = "Recovery phrase created. New addresses come from it."; + } + else + { + g_phraseStatus = "Could not install the seed: " + strError; + } + g_pendingMnemonic.clear(); + g_phraseWrittenDown = false; + g_showCreatePhrase = false; + g_needRefresh = true; + } + if (!g_phraseWrittenDown) + ImGui::EndDisabled(); + + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120.0f, 0.0f))) + { + // Nothing was installed, so there is nothing to undo. The words go + // away with the string. + g_pendingMnemonic.clear(); + g_phraseWrittenDown = false; + g_showCreatePhrase = false; + g_phraseStatus = "Cancelled. No recovery phrase was created."; + } + } + ImGui::End(); + + // Closing with the window X is the same as cancelling. + if (!g_showCreatePhrase && !g_pendingMnemonic.empty()) + { + g_pendingMnemonic.clear(); + g_phraseWrittenDown = false; + g_phraseStatus = "Cancelled. No recovery phrase was created."; + } +} + +static void DrawRestorePhraseDialog() +{ + if (!g_showRestorePhrase) return; + + ImGui::SetNextWindowSize(ImVec2(640.0f, 330.0f), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), + ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + if (ImGui::Begin("Restore From Phrase", &g_showRestorePhrase, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) + { + ImGui::TextWrapped( + "Type the twelve words, in order, separated by spaces. The node will " + "derive the addresses they describe and search the chain for their coins."); + ImGui::Spacing(); + + if (HaveHDSeed()) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); + ImGui::TextWrapped( + "This wallet already has a recovery phrase. Restoring replaces it: " + "coins on addresses from the current phrase would no longer come back " + "from the words you wrote down for it. Back up wallet.dat first."); + ImGui::PopStyleColor(); + ImGui::Spacing(); + } + + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputTextMultiline("##phrase-in", g_restoreInput, sizeof(g_restoreInput), + ImVec2(0.0f, 60.0f)); + + ImGui::Spacing(); + bool fBusy = g_restoreRunning.load(); + if (fBusy) + ImGui::BeginDisabled(); + if (ImGui::Button("Restore", ImVec2(140.0f, 0.0f))) + { + std::string strWhy; + if (!CanScanWalletTransactions(strWhy)) + { + std::lock_guard lock(g_restoreResultMutex); + g_restoreResult = strWhy; + } + else + { + { + std::lock_guard lock(g_restoreResultMutex); + g_restoreResult.clear(); + } + g_restoreDerived.store(0); + g_restoreRecovered.store(0); + g_restoreRunning.store(true); + // On its own thread: a restore walks the whole chain once per + // batch and would otherwise freeze the window for as long as it + // takes. + std::thread(RestoreThread, std::string(g_restoreInput)).detach(); + } + } + if (fBusy) + ImGui::EndDisabled(); + + ImGui::SameLine(); + if (ImGui::Button("Close", ImVec2(100.0f, 0.0f)) && !fBusy) + g_showRestorePhrase = false; + + ImGui::Spacing(); + if (fBusy) + { + ImGui::Text("Searching: %d addresses checked, %d transaction(s) so far", + g_restoreDerived.load(), g_restoreRecovered.load()); + } + else + { + std::lock_guard lock(g_restoreResultMutex); + if (!g_restoreResult.empty()) + ImGui::TextWrapped("%s", g_restoreResult.c_str()); + } + } + ImGui::End(); +} + static void DrawWalletSafetyDialog() { if (!g_showWalletSafety) return; @@ -993,6 +1212,43 @@ static void DrawWalletSafetyDialog() ImGui::PopStyleColor(); } + ImGui::Spacing(); + ImGui::SeparatorText("Recovery Phrase"); + if (HaveHDSeed()) + ImGui::TextColored(ImVec4(0.55f, 1.0f, 0.6f, 1.0f), + "This wallet has a recovery phrase."); + else + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), + "No recovery phrase. Only a file backup can rebuild this wallet."); + + if (!HaveHDSeed()) + { + if (ImGui::Button("Create recovery phrase", ImVec2(200.0f, 0.0f))) + { + std::string strError; + if (bitflash::BIP39GenerateMnemonic(g_pendingMnemonic, strError)) + { + g_phraseWrittenDown = false; + g_phraseStatus.clear(); + g_showCreatePhrase = true; + } + else + { + g_phraseStatus = "Could not generate a phrase: " + strError; + } + } + ImGui::SameLine(); + } + if (ImGui::Button("Restore from phrase", ImVec2(190.0f, 0.0f))) + g_showRestorePhrase = true; + + if (!g_phraseStatus.empty()) + ImGui::TextWrapped("%s", g_phraseStatus.c_str()); + + ImGui::TextWrapped( + "A phrase covers addresses created after it. Keys that already existed " + "are not derived from it, so keep the file backups above as well."); + ImGui::Spacing(); ImGui::SeparatorText("Key Pool"); int nPool = KeyPoolCount(); @@ -1157,6 +1413,8 @@ int RunGUI(int argc, char* argv[]) DrawSendDialog(); DrawOptionsDialog(); DrawWalletSafetyDialog(); + DrawCreatePhraseDialog(); + DrawRestorePhraseDialog(); DrawDiagnosticsDialog(); DrawAboutDialog(); DrawStatusBar(); diff --git a/src/main_gui.cpp b/src/main_gui.cpp index aa9c23d..c9e7dee 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -2,6 +2,7 @@ #include "headers_core.h" #include "selftest.h" +#include "walletcmd.h" #include // hardware_concurrency, to sanity-check /genproclimit #ifndef _WIN32 #include @@ -78,6 +79,11 @@ static void PrintUsage() printf(" /dumpwallet=FILE (export private keys as text -- readable by\n"); printf(" anyone, so guard it like cash)\n"); printf(" /importwallet=FILE (load keys from such a file back in)\n"); + printf(" /newaddress (print the next receiving address, then exit)\n"); + printf(" /newphrase (create a twelve-word recovery phrase for a\n"); + printf(" wallet that has none, show it once, exit)\n"); + printf(" /restorephrase=\"WORDS\" (rebuild this wallet from a phrase and scan\n"); + printf(" the chain for its coins, then exit)\n"); printf(" /rescan (walk the chain for coins this wallet owns\n"); printf(" but never recorded, then exit)\n"); printf("\n"); @@ -347,6 +353,38 @@ int main(int argc, char* argv[]) // /rescan -- walk the chain and pick up anything the wallet's keys own but // the wallet never recorded. Cheap to offer and the only recourse when a // balance is wrong for this reason. + string strShow = argval2(argc, argv, "/showderived", "-showderived"); + if (!strShow.empty()) + { + int nRet = CmdShowDerived(atoi(strShow.c_str())); + DBFlush(true); + return nRet; + } + + if (arg(argc,argv,"/newaddress") || arg(argc,argv,"-newaddress")) + { + int nRet = CmdNewAddress(); + DBFlush(true); + return nRet; + } + + if (arg(argc,argv,"/newphrase") || arg(argc,argv,"-newphrase")) + { + int nRet = CmdNewPhrase(); + DBFlush(true); + return nRet; + } + + string strRestore = argval2(argc, argv, "/restorephrase", "-restorephrase"); + if (!strRestore.empty()) + { + string strDepth = argval2(argc, argv, "/restoredepth", "-restoredepth"); + int nMinDepth = strDepth.empty() ? 0 : atoi(strDepth.c_str()); + int nRet = CmdRestorePhrase(strRestore, nMinDepth); + DBFlush(true); + return nRet; + } + if (arg(argc,argv,"/rescan") || arg(argc,argv,"-rescan")) { string strScanError; diff --git a/src/makefile.mingw b/src/makefile.mingw index 07ae034..94d2146 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -42,14 +42,14 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ - obj/selftest.o obj/bip32.o + obj/selftest.o obj/bip32.o obj/walletcmd.o GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ - selftest.h + selftest.h walletcmd.h all: bitflash.exe diff --git a/src/selftest.cpp b/src/selftest.cpp index 6792ae7..b733c60 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -286,33 +286,9 @@ static int RunWalletKeyPoolSelfTest() return nFail == 0 ? 0 : 1; } -// A -mwindows binary starts with no console attached, so stdout goes nowhere -// even when it was launched from a terminal. Borrow the caller's console for -// the duration of the self-test. Nothing to do on Linux, where stdout is -// already connected. -static void AttachTerminal() -{ -#ifdef _WIN32 - // Only when there is nowhere for stdout to go. If the caller redirected it - // to a file or a pipe -- which is what `make tests > log` and any CI does -- - // that handle is inherited and already works, and reopening it on CONOUT$ - // would take the output away from the file and put it on the screen. That - // was the first version of this fix, and it broke the one case that - // mattered. - HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); - if (hOut != NULL && hOut != INVALID_HANDLE_VALUE) - return; - - if (AttachConsole(ATTACH_PARENT_PROCESS)) - { - // Return values ignored on purpose: if the reopen fails there is - // nowhere left to report it, and the exit status still carries the - // result. - freopen("CONOUT$", "w", stdout); - freopen("CONOUT$", "w", stderr); - } -#endif -} +// AttachTerminal() moved to util.h: the wallet phrase commands need it too, +// and two copies of a console-attach that must get its one condition right is +// one copy too many. static int RunWalletHDSelfTest() { diff --git a/src/util.h b/src/util.h index 25ef118..d95d3ae 100644 --- a/src/util.h +++ b/src/util.h @@ -293,6 +293,33 @@ void PrintHex(const T pbegin, const T pend, const char* pszFormat="%s", bool fSp // Forward declaration -- defined in main.cpp string GetAppDir(); +// Give a command-line operation somewhere to print. +// +// The Windows binary is linked -mwindows and starts with no console, so stdout +// goes nowhere even when it was launched from a terminal. Commands that report +// to the person who typed them -- the self-tests, the wallet phrase commands -- +// call this first. +// +// Only when there is nowhere for stdout to go already: if the caller redirected +// it to a file or a pipe, that handle is inherited and works, and reopening it +// on CONOUT$ would take the output away from the file and put it on the screen. +inline void AttachTerminal() +{ +#ifdef _WIN32 + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + if (hOut != NULL && hOut != INVALID_HANDLE_VALUE) + return; + if (AttachConsole(ATTACH_PARENT_PROCESS)) + { + // Return values ignored on purpose: if the reopen fails there is + // nowhere left to report it, and the exit status still carries the + // result. + freopen("CONOUT$", "w", stdout); + freopen("CONOUT$", "w", stderr); + } +#endif +} + inline int OutputDebugStringF(const char* pszFormat, ...) BF_FORMAT(1, 2); inline int OutputDebugStringF(const char* pszFormat, ...) diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp new file mode 100644 index 0000000..94e9d72 --- /dev/null +++ b/src/walletcmd.cpp @@ -0,0 +1,237 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// Recovery-phrase commands, from the command line. +// +// The phrase reaches the person who typed the command and nowhere else. In this +// tree printf is OutputDebugStringF, which writes into the data directory's +// debug.log -- the file we routinely ask users to attach to issues. A wallet's +// twelve words must never go there, so this file takes the real printf and +// AttachTerminal() gives it somewhere to land on Windows. + +#include "headers_core.h" +#include "bip32.h" +#include "walletcmd.h" + +#undef printf + +// How far ahead to look for used addresses when restoring. +// +// A wallet is restored from a phrase alone, so the node has no idea how many +// addresses the old one handed out. It derives forward in batches and stops +// once a whole batch turns up unused. A fixed number would be simpler and wrong +// for exactly this network: a miner burns an address per block, so a node that +// mined for a week is thousands of keys deep, and stopping at a few hundred +// would restore a wallet that looks empty. +static const int RESTORE_BATCH = 100; +static const int RESTORE_MAX = 10000; + +int CmdNewPhrase() +{ + AttachTerminal(); + + if (HaveHDSeed()) + { + printf("This wallet already has a recovery phrase.\n"); + printf("Creating another would replace it, and coins on addresses derived from\n"); + printf("the current one would no longer come back from the phrase you write down.\n"); + printf("Nothing was changed.\n"); + return 1; + } + + std::string strMnemonic, strError; + if (!bitflash::BIP39GenerateMnemonic(strMnemonic, strError)) + { + fprintf(stderr, "Could not generate a phrase: %s\n", strError.c_str()); + return 1; + } + + if (!SetHDSeedFromMnemonic(strMnemonic, strError)) + { + fprintf(stderr, "Could not install the seed: %s\n", strError.c_str()); + return 1; + } + + // Fill the pool from the new seed straight away, so the addresses this + // wallet hands out next are ones the phrase can reproduce. + TopUpKeyPool(); + + printf("\n"); + printf("Write these twelve words down, in order, on paper.\n"); + printf("They are the only thing that can rebuild this wallet if the file is lost.\n"); + printf("Anyone who reads them can spend your coins. They are not stored anywhere,\n"); + printf("and this is the only time they will be shown.\n"); + printf("\n"); + printf(" %s\n", strMnemonic.c_str()); + printf("\n"); + printf("Keys already in this wallet are NOT covered by the phrase -- they existed\n"); + printf("before it did. Keep your file backups as well.\n"); + fflush(stdout); + return 0; +} + +bool RestoreFromPhrase(const std::string& strMnemonic, + int nMinDepth, + RestoreProgressFn fnProgress, + void* pArg, + std::string& strErrorRet, + int& nRecoveredRet, + int& nDerivedRet) +{ + strErrorRet.clear(); + nRecoveredRet = 0; + nDerivedRet = 0; + + // Refuse before touching the wallet, not after. + // + // Without this, a restore on a node that has not synced derives the right + // keys, scans a chain that is not there, and reports "no transactions were + // found" -- which reads as "your coins are gone" to the one person least + // able to argue with it. Measured while testing: a copied data directory + // whose Berkeley DB environment failed to load left the node at height 0, + // the scan walked one block, and the message said nothing was found across + // 600 addresses. + if (!CanScanWalletTransactions(strErrorRet)) + return false; + + if (!SetHDSeedFromMnemonic(strMnemonic, strErrorRet)) + return false; + + // Derive forward in batches, scanning after each, until a whole batch turns + // up nothing. Every derived key is written to the wallet before the scan, + // because the scan asks the wallet what belongs to it. + int nTotalDerived = 0; + while (nTotalDerived < RESTORE_MAX) + { + // Count wallet transactions, not scan hits. + // + // ScanForWalletTransactions reports what it added *or updated*, and it + // walks the whole chain every time, so a transaction found in the first + // batch is reported again by every batch after it. Testing this against + // a real payment produced exactly +1 per batch and a loop that ran to + // the ceiling, deriving ten thousand addresses to rediscover one coin. + size_t nWalletBefore = 0; + CRITICAL_BLOCK(cs_mapWallet) + nWalletBefore = mapWallet.size(); + + std::string strDeriveError; + for (int i = 0; i < RESTORE_BATCH; i++) + { + CKey key; + if (!DeriveHDKey(nHDNext, key, strDeriveError)) + break; + if (!AddKey(key)) + break; + nHDNext++; + nTotalDerived++; + } + CWalletDB().WriteHDNext(nHDNext); + + ScanForWalletTransactions(pindexGenesisBlock); + + size_t nWalletAfter = 0; + CRITICAL_BLOCK(cs_mapWallet) + nWalletAfter = mapWallet.size(); + + if (fnProgress) + fnProgress(pArg, nTotalDerived, (int)nWalletAfter); + + // A whole batch with nothing in it means far enough -- unless the + // caller asked to look deeper anyway. + if (nWalletAfter == nWalletBefore && nTotalDerived >= nMinDepth) + break; + } + + // The pool is rebuilt from where derivation stopped, so the next address + // this wallet hands out is one the phrase can reproduce as well. + CRITICAL_BLOCK(cs_keyPool) + mapKeyPool.clear(); + TopUpKeyPool(); + + CRITICAL_BLOCK(cs_mapWallet) + nRecoveredRet = (int)mapWallet.size(); + nDerivedRet = nTotalDerived; + return true; +} + +static void PrintRestoreProgress(void*, int nDerived, int nRecovered) +{ + printf(" %d addresses checked, %d transaction(s) recovered\n", nDerived, nRecovered); + fflush(stdout); +} + +int CmdRestorePhrase(const std::string& strMnemonic, int nMinDepth) +{ + AttachTerminal(); + + printf("Checking the phrase and the chain.\n"); + fflush(stdout); + + std::string strError; + int nRecovered = 0, nDerived = 0; + if (!RestoreFromPhrase(strMnemonic, nMinDepth, PrintRestoreProgress, NULL, + strError, nRecovered, nDerived)) + { + fprintf(stderr, "Cannot restore: %s\n", strError.c_str()); + fprintf(stderr, "Nothing was changed.\n"); + return 1; + } + + printf("\n"); + if (nRecovered > 0) + printf("Restored: %d transaction(s) across %d derived addresses.\n", + nRecovered, nDerived); + else + printf("No transactions were found for the first %d addresses of that phrase.\n" + "If you expected coins, check the words and the order.\n", nDerived); + printf("Start the node normally to see the balance.\n"); + fflush(stdout); + return 0; +} + +int CmdNewAddress() +{ + AttachTerminal(); + + std::vector vchPubKey = GetKeyFromPool(); + if (vchPubKey.empty()) + { + fprintf(stderr, "Could not get a key from the wallet.\n"); + return 1; + } + + printf("%s\n", PubKeyToAddress(vchPubKey).c_str()); + if (!HaveHDSeed()) + printf("(this wallet has no recovery phrase, so only a file backup covers this address)\n"); + fflush(stdout); + return 0; +} + +// Diagnostic: print the addresses a phrase produces, so "the restore found +// nothing" can be told apart from "that address was never derived". +int CmdShowDerived(int nCount) +{ + AttachTerminal(); + + if (!HaveHDSeed()) + { + fprintf(stderr, "This wallet has no recovery phrase installed.\n"); + return 1; + } + if (nCount <= 0) + nCount = 20; + + std::string strError; + for (int i = 0; i < nCount; i++) + { + CKey key; + if (!DeriveHDKey((unsigned int)i, key, strError)) + { + fprintf(stderr, "derivation failed at %d: %s\n", i, strError.c_str()); + return 1; + } + printf("%6d %s\n", i, PubKeyToAddress(key.GetPubKey()).c_str()); + } + fflush(stdout); + return 0; +} diff --git a/src/walletcmd.h b/src/walletcmd.h new file mode 100644 index 0000000..89834d8 --- /dev/null +++ b/src/walletcmd.h @@ -0,0 +1,43 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// Recovery-phrase commands. Both run against a loaded wallet and then exit; +// neither starts the node. + +#ifndef BITFLASH_WALLETCMD_H +#define BITFLASH_WALLETCMD_H + +#include + +// Create a phrase for a wallet that does not have one, install the seed, and +// show the words once. Refuses if a phrase already exists. Returns 0 on success. +int CmdNewPhrase(); + +// Install the seed a phrase describes, derive forward looking for used +// addresses, and rescan the chain so their coins appear. Returns 0 on success. +int CmdRestorePhrase(const std::string& strMnemonic, int nMinDepth = 0); + +// Called once per batch with how many addresses have been derived so far and +// how many transactions the wallet now holds. +typedef void (*RestoreProgressFn)(void* pArg, int nDerived, int nRecovered); + +// The restore itself, shared by the command and the GUI so the two cannot +// drift apart. Returns false without touching the wallet if the chain is not +// loaded or the phrase is not valid. +bool RestoreFromPhrase(const std::string& strMnemonic, + int nMinDepth, + RestoreProgressFn fnProgress, + void* pArg, + std::string& strErrorRet, + int& nRecoveredRet, + int& nDerivedRet); + +// Take the next address from the key pool and print it. With a recovery phrase +// installed the address is derived, so the phrase can bring back whatever is +// paid to it. +int CmdNewAddress(); + +// Diagnostic: print the first nCount addresses this wallet's phrase derives. +int CmdShowDerived(int nCount); + +#endif -- cgit v1.2.3 From 09007b4a0321c594083fe1b5e67edbc8b257844b Mon Sep 17 00:00:00 2001 From: Bitflash Date: Sat, 1 Aug 2026 21:27:09 -0300 Subject: build: list every header, so editing one rebuilds what includes it (#99) sockcount.h was in neither makefile's HEADERS, so editing it rebuilt only the objects whose .cpp had also changed and the rest kept the old struct layout -- one object indexing a shared function-local static by the old layout and another by the new one, in the same binary. compat.h and font_roboto.h were missing from one list each. Verified with a full clean rebuild: no duplicate-section warning. --- src/Makefile | 9 ++++++++- src/makefile.mingw | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) (limited to 'src/Makefile') diff --git a/src/Makefile b/src/Makefile index 3eaf2ba..5ded7d6 100644 --- a/src/Makefile +++ b/src/Makefile @@ -39,10 +39,17 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \ -lsodium -lssl -lcrypto -ldb_cxx -lpthread \ -lglfw -lGL +# Every header a .cpp here includes has to be listed, not just the interesting +# ones. sockcount.h was missing, and it declares a struct that lives in a +# function-local static shared across translation units: editing it rebuilt +# some objects and not others, and the linker said so in a warning easy to walk +# past -- "duplicate section ... has different size". That is one object +# indexing a struct by the old layout and another by the new one, in the same +# binary. Nothing crashed here, and nothing would say so if it had. HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ - selftest.h walletcmd.h compat.h + selftest.h walletcmd.h compat.h sockcount.h font_roboto.h IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o diff --git a/src/makefile.mingw b/src/makefile.mingw index 94d2146..5be3704 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -46,10 +46,17 @@ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o +# Every header a .cpp here includes has to be listed, not just the interesting +# ones. sockcount.h was missing, and it declares a struct that lives in a +# function-local static shared across translation units: editing it rebuilt +# some objects and not others, and the linker said so in a warning easy to walk +# past -- "duplicate section ... has different size". That is one object +# indexing a struct by the old layout and another by the new one, in the same +# binary. Nothing crashed here, and nothing would say so if it had. HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ - selftest.h walletcmd.h + selftest.h walletcmd.h sockcount.h compat.h font_roboto.h all: bitflash.exe -- cgit v1.2.3 From 89f6d045c8e3dacbfbb369a3ac89c0c33de22b4c Mon Sep 17 00:00:00 2001 From: mtg0001 <106211915+mtg0001@users.noreply.github.com> Date: Sun, 2 Aug 2026 10:56:50 -0300 Subject: Keep HD change recoverable and bound message stalls (#109) Change from an HD wallet now goes to a newly derived phrase-backed key instead of reusing a spent input's key. Reusing an input key silently moved value back to a key the phrase cannot reproduce; if derivation fails the transaction is refused rather than creating unrecoverable change. Legacy wallets with no seed keep the 0.1.0 behaviour. Restore also scans past the pre-generated key-pool window before treating an empty batch as the end. The message parser no longer consumes a header it cannot complete: it reads from a copy, so the O(n) reinsert and the 100 ms sleep inside the shared message thread are gone, and a peer that sits on an incomplete payload is disconnected after two minutes. The wire cap drops from 256 MB to MAX_SIZE. New net-message self-test covers complete, incomplete, stale and oversized headers. Verified here: clean rebuild, all self-tests pass, lock order checked (no path takes cs_keyPool before cs_mapWallet), and a node built from this branch synced from genesis to the chain tip on the live network with zero rejections. --- src/Makefile | 1 + src/main.cpp | 104 +++++++++++++++++++++++++++++++++++++++++++++-------- src/makefile.mingw | 1 + src/net.h | 15 +++++++- src/selftest.cpp | 65 ++++++++++++++++++++++++++++++++- src/walletcmd.cpp | 7 +++- 6 files changed, 176 insertions(+), 17 deletions(-) (limited to 'src/Makefile') diff --git a/src/Makefile b/src/Makefile index 5ded7d6..88dc08f 100644 --- a/src/Makefile +++ b/src/Makefile @@ -139,3 +139,4 @@ tests: test_bip32 bitflash-node ./test_bip32 ./bitflash-node -selftest=wallet-keypool ./bitflash-node -selftest=wallet-hd + ./bitflash-node -selftest=net-message diff --git a/src/main.cpp b/src/main.cpp index 9082bd5..9bb3c24 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -181,6 +181,39 @@ static bool AddKeyIfMissing(const CKey& key) return AddKey(key); } +static bool DeriveNextHDWalletKey(vector& vchPubKeyRet, string& strErrorRet) +{ + vchPubKeyRet.clear(); + CRITICAL_BLOCK(cs_keyPool) + { + if (!HaveHDSeed()) + { + strErrorRet = "no seed"; + return false; + } + + CKey key; + if (!DeriveHDKey(nHDNext, key, strErrorRet)) + return false; + if (!AddKeyIfMissing(key)) + { + strErrorRet = "could not write the derived key to wallet.dat"; + return false; + } + + unsigned int nNext = nHDNext + 1; + if (!CWalletDB().WriteHDNext(nNext)) + { + strErrorRet = "could not advance the derivation counter"; + return false; + } + + nHDNext = nNext; + vchPubKeyRet = key.GetPubKey(); + } + return true; +} + vector GenerateNewKey() { CKey key; @@ -2310,27 +2343,55 @@ bool ProcessMessages(CNode* pfrom) if (LogAcceptsCategory("net")) printf("\n\nPROCESSMESSAGE SKIPPED %d BYTES\n\n", (int)(pstart - vRecv.begin())); vRecv.erase(vRecv.begin(), pstart); - // Read header + // Read the header from a copy first. The old path consumed the header, + // noticed the payload was incomplete, inserted the header back at the + // front of the receive buffer, then slept inside the shared message + // thread. A peer could announce a large payload and drip bytes forever, + // making every pass pay for an O(n) insert and a 100 ms stall. CMessageHeader hdr; - vRecv >> hdr; + CDataStream vHeader(vRecv.begin(), vRecv.begin() + sizeof(CMessageHeader), + vRecv.nType, vRecv.nVersion); + vHeader >> hdr; if (!hdr.IsValid()) { if (LogAcceptsCategory("net")) printf("\n\nPROCESSMESSAGE: ERRORS IN HEADER %s\n\n\n", hdr.GetCommand().c_str()); + if (hdr.nMessageSize > MAX_PROTOCOL_MESSAGE_SIZE) + pfrom->fDisconnect = true; + pfrom->nIncompleteMessageStart = 0; + vRecv.ignore(sizeof(CMessageHeader)); continue; } string strCommand = hdr.GetCommand(); // Message size unsigned int nMessageSize = hdr.nMessageSize; - if (nMessageSize > vRecv.size()) + unsigned int nPayloadAvailable = vRecv.size() - sizeof(CMessageHeader); + if (nMessageSize > nPayloadAvailable) { - // Rewind and wait for rest of message - ///// need a mechanism to give up waiting for overlong message size error - if (LogAcceptsCategory("net")) printf("MESSAGE-BREAK 2\n"); - vRecv.insert(vRecv.begin(), BEGIN(hdr), END(hdr)); - Sleep(100); + if (pfrom->nIncompleteMessageStart == 0 || + pfrom->nIncompleteMessageSize != nMessageSize || + pfrom->strIncompleteMessageCommand != strCommand) + { + pfrom->nIncompleteMessageStart = GetTime(); + pfrom->nIncompleteMessageSize = nMessageSize; + pfrom->strIncompleteMessageCommand = strCommand; + } + else if (GetTime() - pfrom->nIncompleteMessageStart > + BTF_INCOMPLETE_MESSAGE_TIMEOUT_SECS) + { + LogPrint("net", + "disconnecting %s: incomplete %s message, %u of %u bytes after %d seconds\n", + pfrom->addr.ToString().c_str(), strCommand.c_str(), + nPayloadAvailable, nMessageSize, + BTF_INCOMPLETE_MESSAGE_TIMEOUT_SECS); + pfrom->fDisconnect = true; + } break; } + pfrom->nIncompleteMessageStart = 0; + pfrom->nIncompleteMessageSize = 0; + pfrom->strIncompleteMessageCommand.clear(); + vRecv.ignore(sizeof(CMessageHeader)); // Copy message to its own buffer CDataStream vMsg(vRecv.begin(), vRecv.begin() + nMessageSize, vRecv.nType, vRecv.nVersion); @@ -3774,13 +3835,28 @@ bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew, in // Fill vout[1] back to self with any change if (nValueIn > nTotalValue) { - // Use the same key as one of the coins vector vchPubKey; - CTransaction& txFirst = *(*setCoins.begin()); - foreach(const CTxOut& txout, txFirst.vout) - if (txout.IsMine()) - if (ExtractPubKey(txout.scriptPubKey, true, vchPubKey)) - break; + if (HaveHDSeed()) + { + string strError; + if (!DeriveNextHDWalletKey(vchPubKey, strError)) + { + printf("CreateTransaction() : could not derive a phrase-backed change key: %s\n", + strError.c_str()); + return false; + } + } + else + { + // Legacy wallets have no deterministic seed. Preserve + // the 0.1.0 behaviour for them: return change to a key + // already proven to belong to this wallet. + CTransaction& txFirst = *(*setCoins.begin()); + foreach(const CTxOut& txout, txFirst.vout) + if (txout.IsMine()) + if (ExtractPubKey(txout.scriptPubKey, true, vchPubKey)) + break; + } if (vchPubKey.empty()) return false; diff --git a/src/makefile.mingw b/src/makefile.mingw index 5be3704..5025acb 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -111,3 +111,4 @@ tests: test_bip32.exe bitflash.exe ./test_bip32.exe ./bitflash.exe -selftest=wallet-keypool ./bitflash.exe -selftest=wallet-hd + ./bitflash.exe -selftest=net-message diff --git a/src/net.h b/src/net.h index 1c0c24e..37f335e 100644 --- a/src/net.h +++ b/src/net.h @@ -67,6 +67,13 @@ static const int BTF_RECV_TIMEOUT_SECS = 30 * 60; static const int BTF_SEND_STALL_SECS = 10 * 60; static const int BTF_HANDSHAKE_GRACE_SECS = 60; +// A serialized object cannot be larger than MAX_SIZE, so no valid network +// message needs the old 256 MB header allowance. Keep the wire cap aligned with +// what the deserializer can accept and bound how long one peer may sit on a +// partial payload after a complete header has arrived. +static const unsigned int MAX_PROTOCOL_MESSAGE_SIZE = MAX_SIZE; +static const int BTF_INCOMPLETE_MESSAGE_TIMEOUT_SECS = 2 * 60; + // Ceiling on simultaneous connections. // // ThreadSocketHandler watches every peer through one select(), and select() @@ -225,7 +232,7 @@ public: } // Message size - if (nMessageSize > 0x10000000) + if (nMessageSize > MAX_PROTOCOL_MESSAGE_SIZE) { if (LogAcceptsCategory("net")) printf("CMessageHeader::IsValid() : nMessageSize too large %u\n", nMessageSize); return false; @@ -596,6 +603,9 @@ public: int64 nLastSend; int64 nLastRecv; int64 nLastSendEmpty; + int64 nIncompleteMessageStart; + unsigned int nIncompleteMessageSize; + string strIncompleteMessageCommand; // Height this peer announced in its version message, or -1 if it sent a // version message that predates the field. Knowing how far along everyone @@ -626,6 +636,9 @@ public: nLastSend = 0; nLastRecv = 0; nLastSendEmpty = GetTime(); + nIncompleteMessageStart = 0; + nIncompleteMessageSize = 0; + strIncompleteMessageCommand.clear(); nStartingHeight = -1; vfSubscribe.assign(256, false); diff --git a/src/selftest.cpp b/src/selftest.cpp index 88e9863..c04adf4 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -470,6 +470,67 @@ static int RunWalletHDSelfTest() return nFail == 0 ? 0 : 1; } +static int RunNetMessageSelfTest() +{ + fflush(stdout); + printf("net-message self-test\n"); + + int nFail = 0; + try + { + CNode complete(INVALID_SOCKET, CAddress("127.0.0.1")); + complete.nVersion = VERSION; + complete.vRecv << CMessageHeader("ping", 0); + nFail += Check(ProcessMessages(&complete), "a complete empty message processes") ? 0 : 1; + nFail += Check(complete.vRecv.empty(), "a complete message is consumed") ? 0 : 1; + nFail += Check(!complete.fDisconnect, "a valid ping does not disconnect the peer") ? 0 : 1; + + CNode partial(INVALID_SOCKET, CAddress("127.0.0.1")); + partial.nVersion = VERSION; + partial.vRecv << CMessageHeader("block", 100); + unsigned int nPartialBefore = partial.vRecv.size(); + nFail += Check(ProcessMessages(&partial), "an incomplete message returns cleanly") ? 0 : 1; + nFail += Check(partial.vRecv.size() == nPartialBefore, + "an incomplete message keeps one header in the buffer") ? 0 : 1; + nFail += Check(partial.nIncompleteMessageStart != 0, + "an incomplete message starts a timeout clock") ? 0 : 1; + nFail += Check(!partial.fDisconnect, + "a fresh incomplete message does not disconnect immediately") ? 0 : 1; + + CNode stale(INVALID_SOCKET, CAddress("127.0.0.1")); + stale.nVersion = VERSION; + stale.vRecv << CMessageHeader("block", 100); + stale.nIncompleteMessageStart = GetTime() - BTF_INCOMPLETE_MESSAGE_TIMEOUT_SECS - 1; + stale.nIncompleteMessageSize = 100; + stale.strIncompleteMessageCommand = "block"; + ProcessMessages(&stale); + nFail += Check(stale.fDisconnect, + "a stale incomplete message disconnects the peer") ? 0 : 1; + + CNode oversized(INVALID_SOCKET, CAddress("127.0.0.1")); + oversized.nVersion = VERSION; + oversized.vRecv << CMessageHeader("block", MAX_PROTOCOL_MESSAGE_SIZE + 1); + ProcessMessages(&oversized); + nFail += Check(oversized.fDisconnect, + "an oversized message header disconnects the peer") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + int RunSelfTest(const std::string& name) { AttachTerminal(); @@ -478,8 +539,10 @@ int RunSelfTest(const std::string& name) return RunWalletKeyPoolSelfTest(); if (name == "wallet-hd") return RunWalletHDSelfTest(); + if (name == "net-message") + return RunNetMessageSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - printf("Known self-tests: wallet-keypool, wallet-hd\n"); + printf("Known self-tests: wallet-keypool, wallet-hd, net-message\n"); return 1; } diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 8eb3592..fae2095 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -25,6 +25,10 @@ // would restore a wallet that looks empty. static const int RESTORE_BATCH = 100; static const int RESTORE_MAX = 10000; +// The wallet may have a full unused key pool in front of a change address +// created while spending pre-phrase coins. Looking only one empty batch ahead +// can stop just before that change output. +static const int RESTORE_MIN_SCAN = KEYPOOL_SIZE + RESTORE_BATCH; int CmdNewPhrase() { @@ -105,6 +109,7 @@ bool RestoreFromPhrase(const std::string& strMnemonic, // up nothing. Every derived key is written to the wallet before the scan, // because the scan asks the wallet what belongs to it. int nTotalDerived = (int)nHDNext; + int nStopDepth = max(nMinDepth, RESTORE_MIN_SCAN); while (nTotalDerived < RESTORE_MAX) { // Count wallet transactions, not scan hits. @@ -146,7 +151,7 @@ bool RestoreFromPhrase(const std::string& strMnemonic, // A whole batch with nothing in it means far enough -- unless the // caller asked to look deeper anyway. - if (nWalletAfter == nWalletBefore && nTotalDerived >= nMinDepth) + if (nWalletAfter == nWalletBefore && nTotalDerived >= nStopDepth) break; } -- cgit v1.2.3 From fe04a2045c3080797c6f93bf7cd2e12192b8f68f Mon Sep 17 00:00:00 2001 From: Bitflash Date: Sun, 2 Aug 2026 11:04:00 -0300 Subject: Add consensus block size limit (#111, from #110) CheckBlock() validated block size against MAX_SIZE, the serializer's generic 32 MB guard rather than a block rule; MAX_BLOCK_SIZE did not exist in this tree at all, because Bitcoin's 1 MB cap arrived in 0.3.x, after the 0.1.0 this fork came from. A miner could produce a 32 MB block and every node would download, verify and store it. Original work by mtg0001 in #110, rebased over #109 with the self-test conflict resolved, plus a comment explaining why the inherited count-against-bytes test stays. Measured before choosing the number: 5709 blocks read from blk0001.dat give a largest block of 34,290 bytes and a median of 216, so 1 MB leaves 29x of headroom and rejects nothing that exists. A node built from this branch then synced genesis to 5706 on the live network with zero size-limit rejections and zero CheckBlock errors -- the same test that caught BIP30 rejecting a real block at height 859. --- src/Makefile | 1 + src/main.cpp | 2 +- src/main.h | 21 ++++++++++++++++++ src/makefile.mingw | 1 + src/selftest.cpp | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 85 insertions(+), 2 deletions(-) (limited to 'src/Makefile') diff --git a/src/Makefile b/src/Makefile index 88dc08f..82e7d06 100644 --- a/src/Makefile +++ b/src/Makefile @@ -140,3 +140,4 @@ tests: test_bip32 bitflash-node ./bitflash-node -selftest=wallet-keypool ./bitflash-node -selftest=wallet-hd ./bitflash-node -selftest=net-message + ./bitflash-node -selftest=consensus-limits diff --git a/src/main.cpp b/src/main.cpp index 9bb3c24..1b09a1a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1719,7 +1719,7 @@ bool CBlock::CheckBlock() const // that can be verified before saving an orphan block. // Size limits - if (vtx.empty() || vtx.size() > MAX_SIZE || ::GetSerializeSize(*this, SER_DISK) > MAX_SIZE) + if (!CheckSizeLimits()) return error("CheckBlock() : size limits failed"); // Check timestamp diff --git a/src/main.h b/src/main.h index 1b0be59..768f1c7 100644 --- a/src/main.h +++ b/src/main.h @@ -29,6 +29,10 @@ static const unsigned int MAX_ORPHAN_BLOCKS = 200; // so a block within the size limit could still hold millions of them and take // the network minutes of CPU to reject. Same value Bitcoin settled on. static const unsigned int MAX_BLOCK_SIGOPS = 20000; +// Consensus block-size ceiling. MAX_SIZE is the serializer's generic safety +// limit (32 MB), not a block rule. Bitcoin 0.1.0 predated the later 1 MB block +// cap, so this tree inherited no real block-size consensus limit. +static const unsigned int MAX_BLOCK_SIZE = 1000000; static const int64 COIN = 100000000; // Total Bitflash emission: identical to Bitcoin (21 million). // MAX_MONEY guards against the value overflow bug (CVE-2010-5139), which in @@ -1053,6 +1057,23 @@ public: return RandomXPoWHash((const void*)BEGIN(nVersion), END(nNonce) - BEGIN(nVersion)); } + bool CheckSizeLimits() const + { + // The middle test compares a transaction *count* against a *byte* + // limit. It is inherited from 0.1.0, where the same line compared + // vtx.size() against MAX_SIZE, and it is inert: the smallest possible + // transaction serializes to more than sixty bytes, so a block holding + // over a million of them cannot also weigh under a megabyte. + // + // Kept rather than removed. This is consensus code, and a rewrite that + // reads better is still a rewrite that can change what the network + // accepts. Written down so the next reader does not have to derive it, + // or "fix" it into something that does. + return (!vtx.empty() && + vtx.size() <= MAX_BLOCK_SIZE && + ::GetSerializeSize(*this, SER_DISK) <= MAX_BLOCK_SIZE); + } + uint256 BuildMerkleTree() const { diff --git a/src/makefile.mingw b/src/makefile.mingw index 5025acb..9bbfef1 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -112,3 +112,4 @@ tests: test_bip32.exe bitflash.exe ./bitflash.exe -selftest=wallet-keypool ./bitflash.exe -selftest=wallet-hd ./bitflash.exe -selftest=net-message + ./bitflash.exe -selftest=consensus-limits diff --git a/src/selftest.cpp b/src/selftest.cpp index c04adf4..67f96f8 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -531,6 +531,64 @@ static int RunNetMessageSelfTest() return nFail == 0 ? 0 : 1; } +static CBlock MakeSizedConsensusBlock(size_t nScriptBytes) +{ + CTransaction tx; + tx.vin.push_back(CTxIn(COutPoint(), CScript() << 1 << 1)); + tx.vout.push_back(CTxOut(0, CScript())); + tx.vout[0].scriptPubKey.insert(tx.vout[0].scriptPubKey.end(), nScriptBytes, 0); + + CBlock block; + block.vtx.push_back(tx); + block.hashMerkleRoot = block.BuildMerkleTree(); + block.nVersion = 1; + block.nTime = GetAdjustedTime(); + block.nBits = bnProofOfWorkLimit.GetCompact(); + block.nNonce = 0; + return block; +} + +static int RunConsensusLimitsSelfTest() +{ + fflush(stdout); + printf("consensus-limits self-test\n"); + + int nFail = 0; + try + { + nFail += Check(MAX_BLOCK_SIZE == 1000000, + "the consensus block-size cap is 1 MB") ? 0 : 1; + nFail += Check(MAX_BLOCK_SIZE < MAX_SIZE, + "the block cap is tighter than the serializer cap") ? 0 : 1; + + CBlock small = MakeSizedConsensusBlock(100); + nFail += Check(small.CheckSizeLimits(), + "a small block is within the consensus size limit") ? 0 : 1; + + CBlock oversized = MakeSizedConsensusBlock(MAX_BLOCK_SIZE); + unsigned int nSerialized = ::GetSerializeSize(oversized, SER_DISK); + nFail += Check(nSerialized > MAX_BLOCK_SIZE && nSerialized <= MAX_SIZE, + "the test block sits between 1 MB and the old 32 MB cap") ? 0 : 1; + nFail += Check(!oversized.CheckSizeLimits(), + "a block above 1 MB is outside the consensus size limit") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + int RunSelfTest(const std::string& name) { AttachTerminal(); @@ -541,8 +599,10 @@ int RunSelfTest(const std::string& name) return RunWalletHDSelfTest(); if (name == "net-message") return RunNetMessageSelfTest(); + if (name == "consensus-limits") + return RunConsensusLimitsSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - printf("Known self-tests: wallet-keypool, wallet-hd, net-message\n"); + printf("Known self-tests: wallet-keypool, wallet-hd, net-message, consensus-limits\n"); return 1; } -- cgit v1.2.3 From 1db5f372c36eb701602a70a358de2344726ed734 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Tue, 4 Aug 2026 10:01:36 -0300 Subject: wallet encryption: the stack from #130–#133, with the two blockers fixed (#134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wallet: refuse future encrypted wallet formats * wallet: add encrypted key unlock core * wallet: add encryptwallet rewrite command * wallet: read passphrases from stdin or files * wallet: keep the derivation across encryption, and take the logs with it Two things stopped the encryption stack from being safe to merge. Both were found against a wallet holding real coin, not in the self-tests. 1. Encrypting turned a BIP44 wallet into a legacy one. LoadWallet() ends by resetting the schema and the derivation counters when no seed is in memory. That was written when "no seed after loading" could only mean the wallet had none. An encrypted wallet has a seed -- it just has not been decrypted yet -- so the reset threw away metadata that had been read correctly a moment earlier. Unlocking later restores the seed and nothing restores the rest. Measured on the wallet from the #124 proof: after encrypting, index 1 moved off BAtgSgra2X8Dqo5PJzPCLJhPwrrC2thYVJ, the address actually holding 1.00 BTF, and the audit went from "1.00 covered by the phrase" to "0.00 covered, 1.00 wallet.dat-only". Encrypt your wallet and the program told you your recovery phrase no longer covered your money. Now the condition also accepts a wallet whose seed is present but encrypted. Same wallet, before and after: schema bip44, same four derived addresses, 1.00 BTF still reported as covered. 2. The plaintext keys stayed behind in the Berkeley DB logs. wallet.dat comes out clean -- verified by scanning the raw file for the 118 bytes of a known private key. database/log.0000000002 did not: it held the whole key, and still did after a full node restart, because every record this wallet ever wrote passed through the log. That defeats the feature for the case it exists for, since our own backup advice is to copy the whole directory rather than wallet.dat alone. log_archive() cannot fix it: it only drops logs Berkeley DB considers obsolete and never the one it is writing to, and with one 10 MB log per directory that is precisely where the keys are. They also did not get there at encryption time -- they accumulated over the wallet's whole life -- so nothing done during the rewrite can archive them out. So the encrypt command now closes the environment and deletes the logs on its way out, after the flush that makes every .dat self-contained. This is not secure erasure; the blocks are unlinked, not overwritten. It closes the case of someone reading a copied data directory. Getting there uncovered a third bug, older than both. DBFlush(true) is not safe to call twice. The first call closes the environment, and txn_checkpoint() on a closed environment segfaults instead of returning an error, so the try/catch around it never sees anything. EncryptWallet() already flushes before renaming the wallet, and the command flushes again on the way out -- that pair crashed every single time. Guarded now. The same function has been calling log_archive() with the address of an uninitialised pointer since 0.1.0, on every clean shutdown, in a path whose catch(...) cannot catch a structured fault on Windows. DB_ARCH_REMOVE requires that argument to be NULL. Fixed while here, because the encryption cleanup depends on that call not being a coin flip. --------- Co-authored-by: Gilcimar Duarte --- src/Makefile | 3 + src/db.cpp | 602 ++++++++++++++++++++++++++++++++++++++++++++++++++--- src/db.h | 58 ++++++ src/main.cpp | 334 ++++++++++++++++++++++++++++- src/main.h | 40 ++++ src/main_gui.cpp | 101 ++++++++- src/makefile.mingw | 3 + src/script.cpp | 89 ++++---- src/selftest.cpp | 504 +++++++++++++++++++++++++++++++++++++++++++- src/walletcmd.cpp | 36 ++++ src/walletcmd.h | 4 + 11 files changed, 1698 insertions(+), 76 deletions(-) (limited to 'src/Makefile') diff --git a/src/Makefile b/src/Makefile index 82e7d06..3577dd6 100644 --- a/src/Makefile +++ b/src/Makefile @@ -139,5 +139,8 @@ tests: test_bip32 bitflash-node ./test_bip32 ./bitflash-node -selftest=wallet-keypool ./bitflash-node -selftest=wallet-hd + ./bitflash-node -selftest=wallet-format + ./bitflash-node -selftest=wallet-crypto + ./bitflash-node -selftest=wallet-encrypt ./bitflash-node -selftest=net-message ./bitflash-node -selftest=consensus-limits diff --git a/src/db.cpp b/src/db.cpp index b7e7b91..9334249 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -203,11 +203,72 @@ void CDB::Close() RandAddSeed(); } +// Close the Berkeley DB environment and delete its write-ahead logs. +// +// Encrypting the wallet rewrites wallet.dat, and that file does come out clean. +// The logs in database/ do not: every record this wallet has written since it +// was created passed through them, so the plaintext private keys are still +// there afterwards. Measured -- wallet.dat held no trace of a known key while +// database/log.0000000002 held all 118 bytes of it, and still did after a full +// node restart. +// +// It matters because our own backup advice, in the README and in the LEIA-ME +// shipped with the desktop folders, is to copy the whole directory rather than +// wallet.dat alone. That advice is right, and it exists because of #40 -- but +// following it after encrypting would carry the unencrypted key along. +// +// log_archive(DB_ARCH_REMOVE) cannot fix this. It removes only logs Berkeley DB +// considers obsolete, and never the log it is currently writing to. With one +// 10 MB log per data directory, the plaintext lives in exactly that file. The +// keys did not get there when the wallet was encrypted; they got there over the +// wallet's whole life, so nothing done at encryption time can archive them out. +// +// So the logs go, after a full flush. DBFlush(true) checkpoints, runs lsn_reset +// on every file and closes the environment -- lsn_reset is what makes a .dat +// self-contained rather than welded to this directory, which is the same +// property a backup depends on. Once that has happened there is nothing left in +// the logs to recover, and they can be removed rather than archived. +// +// This is not secure erasure: the blocks are unlinked, not overwritten, so disk +// forensics could still reach them. It closes the case this feature is actually +// for -- someone reading the copied data directory. +void PurgeDbEnvironmentLogs() +{ + // Everything below assumes nothing is mid-write, which is why this is only + // reached from the one-shot encrypt command, on its way out. + DBFlush(true); + + string strDbDir = GetAppDir() + "/database"; + int nRemoved = 0; + for (int i = 1; i < 1000; i++) + { + string strPath = strprintf("%s/log.%010d", strDbDir.c_str(), i); + if (!FileExists(strPath.c_str())) + continue; + if (remove(strPath.c_str()) == 0) + nRemoved++; + else + printf("PurgeDbEnvironmentLogs() : could not remove %s\n", strPath.c_str()); + } + printf("PurgeDbEnvironmentLogs() : removed %d database log file(s)\n", nRemoved); +} + void DBFlush(bool fShutdown) { // Flush log data to the actual data file // on all files that are not in use printf("DBFlush(%s)\n", fShutdown ? "true" : "false"); + // Calling this twice is not idempotent, it is fatal: the first shutdown + // flush closes the environment, and txn_checkpoint() on a closed + // environment segfaults rather than returning an error, so the try/catch + // below never sees it. EncryptWallet() already flushes before renaming the + // wallet file, and every one-shot command flushes again on its way out -- + // which is exactly the pair that crashed while this was being written. + if (!fDbEnvInit) + { + printf("DBFlush() : environment already closed, nothing to flush\n"); + return; + } CRITICAL_BLOCK(cs_db) { // This runs on the way out. Anything that throws here and is not caught @@ -235,9 +296,17 @@ void DBFlush(bool fShutdown) } if (fShutdown) { - char** listp; + // listp must be NULL when DB_ARCH_REMOVE is set. This passed the + // address of an UNINITIALISED pointer, and Berkeley DB is entitled + // to write through it -- which segfaults. It fires on every clean + // shutdown, because the loop just above empties mapFileUseCount, + // and a structured fault on Windows never reaches the catch(...) + // wrapped around it. Inherited from 0.1.0 and wrong since then. + // + // Found while making wallet encryption clean up after itself: + // calling DBFlush(true) a second time crashed here every time. if (mapFileUseCount.empty()) - try { dbenv.log_archive(&listp, DB_ARCH_REMOVE); } catch (...) { } + try { dbenv.log_archive(NULL, DB_ARCH_REMOVE); } catch (...) { } try { dbenv.close(0); } catch (...) { } fDbEnvInit = false; } @@ -636,6 +705,29 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) mapKeys[vchPubKey] = vchPrivKey; mapPubKeys[Hash160(vchPubKey)] = vchPubKey; } + else if (strType == "mkey") + { + unsigned int nID = 0; + ssKey >> nID; + CWalletMasterKey kMasterKey; + ssValue >> kMasterKey; + mapMasterKeys[nID] = kMasterKey; + nWalletMasterKeyMaxID = max(nWalletMasterKeyMaxID, nID); + fWalletEncrypted = true; + fWalletLocked = true; + } + else if (strType == "ckey") + { + vector vchPubKey; + ssKey >> vchPubKey; + vector vchCryptedSecret; + ssValue >> vchCryptedSecret; + if (!AddCryptedKey(vchPubKey, vchCryptedSecret)) + { + printf("LoadWallet: wallet.dat has an unreadable encrypted key record\n"); + return false; + } + } else if (strType == "defaultkey") { ssValue >> vchDefaultKeyRet; @@ -669,6 +761,32 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) { ssValue >> nHDChangeNext; } + else if (strType == "cryptedhdmaster") + { + ssValue >> vchCryptedHDMaster; + fWalletEncrypted = true; + fWalletLocked = true; + } + else if (strType == "cryptedhdchaincode") + { + ssValue >> vchCryptedHDChainCode; + fWalletEncrypted = true; + fWalletLocked = true; + } + else if (strType == "walletminversion") + { + int nMinVersion = 0; + ssValue >> nMinVersion; + if (nMinVersion > WALLET_FORMAT_SUPPORTED) + { + printf("LoadWallet: wallet.dat requires wallet format %d, " + "but this build only supports format %d\n", + nMinVersion, WALLET_FORMAT_SUPPORTED); + printf("LoadWallet: the file was left untouched. Upgrade " + "Bitflash before opening this wallet.\n"); + return false; + } + } else if (strType == "pool") { int64 nIndex; @@ -725,7 +843,17 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) return false; } - if (HaveHDSeed()) + // An encrypted wallet has a seed; it just has not been decrypted yet. + // + // The reset in the else branch was written when "no seed in memory after + // loading" could only mean "this wallet has no seed". With encryption that + // is no longer true, and taking the else branch here wiped the schema and + // the derivation counters that had just been read correctly out of the + // file. Unlocking later restores the seed but nothing restores those, so a + // BIP44 wallet came back deriving legacy addresses -- different addresses + // from the ones its own recovery phrase produces, while the audit reported + // the phrase as covering nothing. + if (HaveHDSeed() || !vchCryptedHDMaster.empty()) { if (nHDKeySchema == HD_SCHEMA_NONE) { @@ -801,11 +929,18 @@ bool LoadWallet() if (!CWalletDB("cr").LoadWallet(vchDefaultKey)) return false; - if (mapKeys.count(vchDefaultKey)) + if (!vchDefaultKey.empty() && mapPubKeys.count(Hash160(vchDefaultKey))) { // Set keyUser keyUser.SetPubKey(vchDefaultKey); - keyUser.SetPrivKey(mapKeys[vchDefaultKey]); + if (mapKeys.count(vchDefaultKey)) + keyUser.SetPrivKey(mapKeys[vchDefaultKey]); + } + else if (IsWalletEncrypted()) + { + printf("LoadWallet: encrypted wallet has no usable default public key. " + "The file was left untouched.\n"); + return false; } else { @@ -951,6 +1086,8 @@ bool DumpWallet(const string& strDest) { if (strDest.empty()) return error("DumpWallet() : no destination given\n"); + if (IsWalletLocked()) + return error("DumpWallet() : wallet is encrypted and locked; unlock it with /walletpassphrase first\n"); // Never over an existing file. The whole point is to hold the only copy of // something irreplaceable, and a mistyped path must not eat one. @@ -976,35 +1113,51 @@ bool DumpWallet(const string& strDest) fprintf(pf, "# The key is the DER encoding, hex; restore with -importwallet=FILE.\n"); fprintf(pf, "#\n"); + vector > vPubKeys; + { + CRITICAL_BLOCK(cs_mapKeys) + { + for (map, CPrivKey>::iterator mi = mapKeys.begin(); + mi != mapKeys.end(); ++mi) + vPubKeys.push_back((*mi).first); + for (map, vector >::iterator mi = mapCryptedKeys.begin(); + mi != mapCryptedKeys.end(); ++mi) + if (!mapKeys.count((*mi).first)) + vPubKeys.push_back((*mi).first); + } + } + int nKeys = 0; bool fOk = true; - CRITICAL_BLOCK(cs_mapKeys) + for (size_t i = 0; i < vPubKeys.size(); i++) { - for (map, CPrivKey>::iterator mi = mapKeys.begin(); - mi != mapKeys.end(); ++mi) + const vector& vchPubKey = vPubKeys[i]; + CPrivKey vchPrivKey; + string strError; + if (!GetWalletPrivKey(vchPubKey, vchPrivKey, strError)) { - const vector& vchPubKey = (*mi).first; - const CPrivKey& vchPrivKey = (*mi).second; - - string strAddress = PubKeyToAddress(vchPubKey); - string strLabel; - map::iterator mia = mapAddressBook.find(strAddress); - if (mia != mapAddressBook.end()) - strLabel = (*mia).second; - - // Labels are user text and could contain a newline, which would - // forge a second entry on read-back. - for (size_t i = 0; i < strLabel.size(); i++) - if (strLabel[i] == '\n' || strLabel[i] == '\r') - strLabel[i] = ' '; - - if (fprintf(pf, "%s %s %s\n", - HexStr(vchPrivKey.begin(), vchPrivKey.end(), false).c_str(), - strAddress.c_str(), - strLabel.c_str()) < 0) - { fOk = false; break; } - nKeys++; + fOk = false; + break; } + + string strAddress = PubKeyToAddress(vchPubKey); + string strLabel; + map::iterator mia = mapAddressBook.find(strAddress); + if (mia != mapAddressBook.end()) + strLabel = (*mia).second; + + // Labels are user text and could contain a newline, which would forge + // a second entry on read-back. + for (size_t j = 0; j < strLabel.size(); j++) + if (strLabel[j] == '\n' || strLabel[j] == '\r') + strLabel[j] = ' '; + + if (fprintf(pf, "%s %s %s\n", + HexStr(vchPrivKey.begin(), vchPrivKey.end(), false).c_str(), + strAddress.c_str(), + strLabel.c_str()) < 0) + { fOk = false; break; } + nKeys++; } if (fclose(pf) != 0) @@ -1071,10 +1224,7 @@ bool ImportWallet(const string& strSrc, int& nAddedRet, int& nSkippedRet) continue; } - bool fHave = false; - CRITICAL_BLOCK(cs_mapKeys) - fHave = mapKeys.count(key.GetPubKey()) > 0; - if (fHave) + if (WalletCanSpendKey(key.GetPubKey())) { nSkippedRet++; // already ours; importing twice is not an error continue; @@ -1099,3 +1249,389 @@ bool ImportWallet(const string& strSrc, int& nAddedRet, int& nSkippedRet) "belonging to the new keys\n"); return true; } + + +// ---- wallet encryption rewrite ------------------------------------------- + +class CWalletRewriteDB : public CDB +{ +public: + CWalletRewriteDB(const char* pszFile, const char* pszMode="r+", bool fTxn=false) + : CDB(pszFile, pszMode, fTxn) { } + + bool WriteRaw(CDataStream ssKey, CDataStream ssValue) + { + if (!pdb || ssKey.empty()) + return false; + + Dbt datKey(&ssKey[0], ssKey.size()); + Dbt datValue(ssValue.empty() ? NULL : &ssValue[0], ssValue.size()); + int ret = pdb->put(GetTxn(), &datKey, &datValue, 0); + + memset(datKey.get_data(), 0, datKey.get_size()); + if (datValue.get_data()) + memset(datValue.get_data(), 0, datValue.get_size()); + return ret == 0; + } + + bool WriteWalletMinVersion(int nVersion) + { + return Write(string("walletminversion"), nVersion); + } + + bool WriteMasterKey(unsigned int nID, const CWalletMasterKey& kMasterKey) + { + return Write(make_pair(string("mkey"), nID), kMasterKey, true); + } + + bool WriteCryptedKey(const vector& vchPubKey, + const vector& vchCryptedSecret) + { + return Write(make_pair(string("ckey"), vchPubKey), vchCryptedSecret, false); + } + + bool WriteCryptedHDMaster(const vector& vchCryptedMaster, + const vector& vchCryptedChainCode) + { + return Write(string("cryptedhdmaster"), vchCryptedMaster) + && Write(string("cryptedhdchaincode"), vchCryptedChainCode); + } + + bool WriteHDNext(unsigned int nNext) + { + return Write(string("hdnext"), nNext); + } + + bool WritePool(int64 nIndex, const vector& vchPubKey) + { + return Write(make_pair(string("pool"), nIndex), vchPubKey); + } + + bool CopyPublicRecordsTo(CWalletRewriteDB& dbTo, int& nCopiedRet, string& strErrorRet) + { + nCopiedRet = 0; + Dbc* pcursor = GetCursor(); + if (!pcursor) + { + strErrorRet = "could not open a wallet cursor"; + return false; + } + + unsigned int fFlags = DB_NEXT; + loop + { + CDataStream ssKey; + CDataStream ssValue; + int ret = ReadAtCursor(pcursor, ssKey, ssValue, fFlags); + if (ret == DB_NOTFOUND) + break; + if (ret != 0) + { + pcursor->close(); + strErrorRet = strprintf("could not read wallet record: Berkeley DB error %d", ret); + return false; + } + + string strType; + try + { + CDataStream ssType = ssKey; + ssType >> strType; + } + catch (...) + { + pcursor->close(); + strErrorRet = "wallet.dat has a record whose key type cannot be read"; + return false; + } + + if (strType == "key" || + strType == "pool" || + strType == "hdmaster" || + strType == "hdchaincode" || + strType == "mkey" || + strType == "ckey" || + strType == "cryptedhdmaster" || + strType == "cryptedhdchaincode" || + strType == "walletminversion") + continue; + + if (!dbTo.WriteRaw(ssKey, ssValue)) + { + pcursor->close(); + strErrorRet = strprintf("could not copy wallet record '%s'", strType.c_str()); + return false; + } + nCopiedRet++; + } + + pcursor->close(); + return true; + } +}; + +static string WalletEncryptBackupPath() +{ + string strBase = GetAppDir() + "/wallet.dat.before-encrypt."; + string strPath = strBase + i64tostr(GetTime()) + ".bak"; + for (int i = 1; FileExists(strPath.c_str()); i++) + strPath = strBase + i64tostr(GetTime()) + "." + itostr(i) + ".bak"; + return strPath; +} + +static bool EncryptPrivateKeyForWallet(const CKeyingMaterial& vchMasterKey, + const vector& vchPubKey, + const CPrivKey& vchPrivKey, + vector& vchCryptedRet, + string& strErrorRet) +{ + if (!EncryptSecret(vchMasterKey, + vector(vchPrivKey.begin(), vchPrivKey.end()), + WalletKeyIV(vchPubKey), vchCryptedRet)) + { + strErrorRet = "could not encrypt a private key"; + return false; + } + return true; +} + +bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet) +{ + strBackupRet.clear(); + strErrorRet.clear(); + + if (IsWalletEncrypted()) + { + strErrorRet = "wallet is already encrypted"; + return false; + } + if (strPassphrase.size() < 8) + { + strErrorRet = "passphrase must be at least 8 characters"; + return false; + } + + map, CPrivKey> mapPlainKeys; + vector vchPlainHDMaster; + vector vchPlainHDChainCode; + unsigned int nHDNextSnapshot = 0; + { + CRITICAL_BLOCK(cs_mapKeys) + mapPlainKeys = mapKeys; + } + { + CRITICAL_BLOCK(cs_keyPool) + { + vchPlainHDMaster = vchHDMaster; + vchPlainHDChainCode = vchHDChainCode; + nHDNextSnapshot = nHDNext; + } + } + if (mapPlainKeys.empty()) + { + strErrorRet = "wallet has no private keys to encrypt"; + return false; + } + + CKeyingMaterial vchMasterKey; + vchMasterKey.resize(32); + if (RAND_bytes(&vchMasterKey[0], vchMasterKey.size()) != 1) + { + strErrorRet = "could not create a random wallet master key"; + return false; + } + + CWalletMasterKey kMasterKey; + if (RAND_bytes(&kMasterKey.vchSalt[0], kMasterKey.vchSalt.size()) != 1) + { + strErrorRet = "could not create a wallet encryption salt"; + return false; + } + + CKeyingMaterial vchPassKey; + vector vchPassIV; + if (!DeriveWalletPassphraseKey(strPassphrase, kMasterKey.vchSalt, + kMasterKey.nDeriveIterations, + vchPassKey, vchPassIV) || + !EncryptSecret(vchPassKey, + vector(vchMasterKey.begin(), vchMasterKey.end()), + vchPassIV, kMasterKey.vchCryptedKey)) + { + strErrorRet = "could not encrypt the wallet master key"; + return false; + } + + vector, vector > > vCryptedKeys; + for (map, CPrivKey>::const_iterator mi = mapPlainKeys.begin(); + mi != mapPlainKeys.end(); ++mi) + { + vector vchCrypted; + if (!EncryptPrivateKeyForWallet(vchMasterKey, (*mi).first, (*mi).second, + vchCrypted, strErrorRet)) + return false; + vCryptedKeys.push_back(make_pair((*mi).first, vchCrypted)); + } + + vector vchCryptedHDMasterNew; + vector vchCryptedHDChainCodeNew; + if (!vchPlainHDMaster.empty() || !vchPlainHDChainCode.empty()) + { + if (vchPlainHDMaster.size() != 32 || vchPlainHDChainCode.size() != 32) + { + strErrorRet = "wallet has an incomplete HD seed"; + return false; + } + if (!EncryptSecret(vchMasterKey, vchPlainHDMaster, + WalletSecretIV("hdmaster"), vchCryptedHDMasterNew) || + !EncryptSecret(vchMasterKey, vchPlainHDChainCode, + WalletSecretIV("hdchaincode"), vchCryptedHDChainCodeNew)) + { + strErrorRet = "could not encrypt the HD seed"; + return false; + } + } + + vector > > vNewPool; + vector, vector > > vNewPoolKeys; + unsigned int nHDNextNew = nHDNextSnapshot; + for (int i = 0; i < KEYPOOL_SIZE; i++) + { + CKey key; + if (!vchPlainHDMaster.empty()) + { + string strDeriveError; + if (!DeriveHDKey(nHDNextNew, key, strDeriveError)) + { + strErrorRet = strprintf("could not derive replacement key-pool key %u: %s", + nHDNextNew, strDeriveError.c_str()); + return false; + } + nHDNextNew++; + } + else + { + key.MakeNewKey(); + } + + vector vchPubKey = key.GetPubKey(); + if (!mapPlainKeys.count(vchPubKey)) + { + vector vchCrypted; + if (!EncryptPrivateKeyForWallet(vchMasterKey, vchPubKey, key.GetPrivKey(), + vchCrypted, strErrorRet)) + return false; + vNewPoolKeys.push_back(make_pair(vchPubKey, vchCrypted)); + } + vNewPool.push_back(make_pair((int64)i + 1, vchPubKey)); + } + + string strTempFile = strprintf("wallet.encrypting.%lld.dat", GetTime()); + string strTempPath = GetAppDir() + "/" + strTempFile; + remove(strTempPath.c_str()); + + int nCopied = 0; + { + CWalletRewriteDB dbSource("wallet.dat", "r"); + CWalletRewriteDB dbTarget(strTempFile.c_str(), "c+"); + if (!dbSource.CopyPublicRecordsTo(dbTarget, nCopied, strErrorRet)) + { + remove(strTempPath.c_str()); + return false; + } + if (!dbTarget.WriteWalletMinVersion(WALLET_FORMAT_ENCRYPTED) || + !dbTarget.WriteMasterKey(nWalletMasterKeyMaxID + 1, kMasterKey)) + { + strErrorRet = "could not write encrypted wallet metadata"; + remove(strTempPath.c_str()); + return false; + } + for (size_t i = 0; i < vCryptedKeys.size(); i++) + { + if (!dbTarget.WriteCryptedKey(vCryptedKeys[i].first, vCryptedKeys[i].second)) + { + strErrorRet = "could not write an encrypted private key"; + remove(strTempPath.c_str()); + return false; + } + } + for (size_t i = 0; i < vNewPoolKeys.size(); i++) + { + if (!dbTarget.WriteCryptedKey(vNewPoolKeys[i].first, vNewPoolKeys[i].second)) + { + strErrorRet = "could not write an encrypted replacement key-pool key"; + remove(strTempPath.c_str()); + return false; + } + } + if (!vchCryptedHDMasterNew.empty() && + !dbTarget.WriteCryptedHDMaster(vchCryptedHDMasterNew, vchCryptedHDChainCodeNew)) + { + strErrorRet = "could not write the encrypted HD seed"; + remove(strTempPath.c_str()); + return false; + } + if (!dbTarget.WriteHDNext(nHDNextNew)) + { + strErrorRet = "could not write the replacement HD derivation counter"; + remove(strTempPath.c_str()); + return false; + } + for (size_t i = 0; i < vNewPool.size(); i++) + { + if (!dbTarget.WritePool(vNewPool[i].first, vNewPool[i].second)) + { + strErrorRet = "could not write replacement key-pool records"; + remove(strTempPath.c_str()); + return false; + } + } + } + + // The source and target handles are closed. Close the environment too + // before replacing files on Windows; the command exits immediately after + // this, and tests verify reload through a fresh process. + DBFlush(true); + + string strWalletPath = GetAppDir() + "/wallet.dat"; + string strBackupPath = WalletEncryptBackupPath(); + if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0) + { + strErrorRet = strprintf("could not move the original wallet to %s", strBackupPath.c_str()); + remove(strTempPath.c_str()); + return false; + } + if (rename(strTempPath.c_str(), strWalletPath.c_str()) != 0) + { + rename(strBackupPath.c_str(), strWalletPath.c_str()); + strErrorRet = "could not install the encrypted wallet; the original was restored"; + remove(strTempPath.c_str()); + return false; + } + + strBackupRet = strBackupPath; + printf("EncryptWallet() : copied %d public record(s), encrypted %u key(s), " + "rebuilt %d key-pool entry(s)\n", + nCopied, (unsigned int)(vCryptedKeys.size() + vNewPoolKeys.size()), + (int)vNewPool.size()); + + // The rewritten wallet.dat is clean, and that is not enough: every record + // this wallet ever wrote also passed through the environment's write-ahead + // log in database/, and Berkeley DB keeps those files. Measured on a real + // wallet -- wallet.dat held no trace of a known private key afterwards, + // while database/log.0000000002 still held all 118 bytes of it, and still + // did after a full node restart. + // + // That matters more here than it looks, because our own backup advice + // (README, and the LEIA-ME in the desktop folders) is to copy the whole + // directory rather than wallet.dat alone -- which is the right advice, and + // the reason for issue #40. Following it after encrypting would carry the + // unencrypted key along with the backup. + // + // DB_LOG_AUTO_REMOVE is deliberately not the answer: it sits commented out + // where this environment is opened, marked "causes corruption", and that + // note predates this fork. This is the conservative form instead -- force a + // checkpoint, then ask Berkeley DB to drop only the logs it says are no + // longer needed for recovery, once, at the one moment we know the wallet + // has just been rewritten. + return true; +} diff --git a/src/db.h b/src/db.h index 90238bf..b184b17 100644 --- a/src/db.h +++ b/src/db.h @@ -16,9 +16,43 @@ class CWalletTx; extern map mapAddressBook; extern bool fClient; +// Wallet file format guard. The current wallet stores private keys in plain +// records. The next format will encrypt them; builds that do not implement it +// must stop before they can show an empty-looking wallet. +static const int WALLET_FORMAT_PLAINTEXT = 1; +static const int WALLET_FORMAT_ENCRYPTED = 2; +static const int WALLET_FORMAT_SUPPORTED = WALLET_FORMAT_ENCRYPTED; + +class CWalletMasterKey +{ +public: + vector vchCryptedKey; + vector vchSalt; + unsigned int nDerivationMethod; + unsigned int nDeriveIterations; + + CWalletMasterKey() + { + nDerivationMethod = 0; + nDeriveIterations = 25000; + vchSalt.resize(8); + } + + IMPLEMENT_SERIALIZE + ( + READWRITE(vchCryptedKey); + READWRITE(vchSalt); + READWRITE(nDerivationMethod); + READWRITE(nDeriveIterations); + ) +}; + extern DbEnv dbenv; extern void DBFlush(bool fShutdown); +// Close the environment and delete its logs. Only the encryption command calls +// this: the logs hold the plaintext records the old wallet wrote. +extern void PurgeDbEnvironmentLogs(); @@ -411,6 +445,29 @@ public: && Write(string("hdchaincode"), vchChainCode); } + bool WriteWalletMinVersion(int nVersion) + { + return Write(string("walletminversion"), nVersion); + } + + bool WriteMasterKey(unsigned int nID, const CWalletMasterKey& kMasterKey) + { + return Write(make_pair(string("mkey"), nID), kMasterKey, true); + } + + bool WriteCryptedKey(const vector& vchPubKey, + const vector& vchCryptedSecret) + { + return Write(make_pair(string("ckey"), vchPubKey), vchCryptedSecret, false); + } + + bool WriteCryptedHDMaster(const vector& vchCryptedMaster, + const vector& vchCryptedChainCode) + { + return Write(string("cryptedhdmaster"), vchCryptedMaster) + && Write(string("cryptedhdchaincode"), vchCryptedChainCode); + } + bool WriteHDNext(unsigned int nNext) { return Write(string("hdnext"), nNext); @@ -478,6 +535,7 @@ bool BackupWallet(const string& strDest); // POSIX -- the output holds private keys in the clear. bool DumpWallet(const string& strDest); bool ImportWallet(const string& strSrc, int& nAddedRet, int& nSkippedRet); +bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet); inline bool SetAddressBookName(const string& strAddress, const string& strName) { diff --git a/src/main.cpp b/src/main.cpp index efece6c..0342650 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -72,7 +72,15 @@ vector > vWalletUpdated; CCriticalSection cs_mapWallet; map, CPrivKey> mapKeys; +map, vector > mapCryptedKeys; map > mapPubKeys; +map mapMasterKeys; +unsigned int nWalletMasterKeyMaxID = 0; +CKeyingMaterial vWalletMasterKey; +vector vchCryptedHDMaster; +vector vchCryptedHDChainCode; +bool fWalletEncrypted = false; +bool fWalletLocked = true; CCriticalSection cs_mapKeys; CKey keyUser; @@ -159,6 +167,21 @@ int MinerThreadCount() bool AddKey(const CKey& key) { + if (IsWalletEncrypted()) + { + string strError; + CPrivKey vchPrivKey = key.GetPrivKey(); + vector vchCryptedSecret; + if (IsWalletLocked()) + return error("AddKey() : wallet is locked\n"); + if (!EncryptSecret(vWalletMasterKey, vector(vchPrivKey.begin(), vchPrivKey.end()), + WalletKeyIV(key.GetPubKey()), vchCryptedSecret)) + return error("AddKey() : encrypting key failed\n"); + if (!AddCryptedKey(key.GetPubKey(), vchCryptedSecret)) + return false; + return CWalletDB().WriteCryptedKey(key.GetPubKey(), vchCryptedSecret); + } + CRITICAL_BLOCK(cs_mapKeys) { mapKeys[key.GetPubKey()] = key.GetPrivKey(); @@ -167,10 +190,287 @@ bool AddKey(const CKey& key) return CWalletDB().WriteKey(key.GetPubKey(), key.GetPrivKey()); } +bool IsWalletEncrypted() +{ + return fWalletEncrypted; +} + +bool IsWalletLocked() +{ + return fWalletEncrypted && fWalletLocked; +} + +void LockWallet() +{ + if (!fWalletEncrypted) + return; + CRITICAL_BLOCK(cs_mapKeys) + { + vWalletMasterKey.clear(); + if (!vchCryptedHDMaster.empty()) + vchHDMaster.clear(); + if (!vchCryptedHDChainCode.empty()) + vchHDChainCode.clear(); + fWalletLocked = true; + } +} + +bool DeriveWalletPassphraseKey(const string& strPassphrase, + const vector& vchSalt, + unsigned int nDeriveIterations, + CKeyingMaterial& vchKeyRet, + vector& vchIVRet) +{ + vchKeyRet.clear(); + vchIVRet.clear(); + if (vchSalt.size() != 8 || nDeriveIterations < 1) + return false; + + unsigned char chKey[32]; + unsigned char chIV[16]; + int n = EVP_BytesToKey(EVP_aes_256_cbc(), EVP_sha512(), &vchSalt[0], + (const unsigned char*)strPassphrase.data(), + (int)strPassphrase.size(), + (int)nDeriveIterations, + chKey, chIV); + if (n != 32) + { + memset(chKey, 0, sizeof(chKey)); + memset(chIV, 0, sizeof(chIV)); + return false; + } + vchKeyRet.assign(chKey, chKey + sizeof(chKey)); + vchIVRet.assign(chIV, chIV + sizeof(chIV)); + memset(chKey, 0, sizeof(chKey)); + memset(chIV, 0, sizeof(chIV)); + return true; +} + +bool EncryptSecret(const CKeyingMaterial& vchKey, + const vector& vchPlaintext, + const vector& vchIV, + vector& vchCiphertextRet) +{ + vchCiphertextRet.clear(); + if (vchKey.size() != 32 || vchIV.size() != 16) + return false; + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) + return false; + + bool fOk = false; + int nLen = 0; + int nFinal = 0; + vector vchOut(vchPlaintext.size() + EVP_CIPHER_block_size(EVP_aes_256_cbc())); + if (EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, &vchKey[0], &vchIV[0]) && + EVP_EncryptUpdate(ctx, &vchOut[0], &nLen, + vchPlaintext.empty() ? NULL : &vchPlaintext[0], + (int)vchPlaintext.size()) && + EVP_EncryptFinal_ex(ctx, &vchOut[0] + nLen, &nFinal)) + { + vchOut.resize(nLen + nFinal); + vchCiphertextRet.swap(vchOut); + fOk = true; + } + EVP_CIPHER_CTX_free(ctx); + return fOk; +} + +bool DecryptSecret(const CKeyingMaterial& vchKey, + const vector& vchCiphertext, + const vector& vchIV, + vector& vchPlaintextRet) +{ + vchPlaintextRet.clear(); + if (vchKey.size() != 32 || vchIV.size() != 16 || vchCiphertext.empty()) + return false; + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) + return false; + + bool fOk = false; + int nLen = 0; + int nFinal = 0; + vector vchOut(vchCiphertext.size()); + if (EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, &vchKey[0], &vchIV[0]) && + EVP_DecryptUpdate(ctx, &vchOut[0], &nLen, &vchCiphertext[0], + (int)vchCiphertext.size()) && + EVP_DecryptFinal_ex(ctx, &vchOut[0] + nLen, &nFinal)) + { + vchOut.resize(nLen + nFinal); + vchPlaintextRet.swap(vchOut); + fOk = true; + } + EVP_CIPHER_CTX_free(ctx); + return fOk; +} + +vector WalletKeyIV(const vector& vchPubKey) +{ + uint256 hash = Hash(vchPubKey.begin(), vchPubKey.end()); + vector vchIV(16); + memcpy(&vchIV[0], hash.begin(), vchIV.size()); + return vchIV; +} + +vector WalletSecretIV(const string& strLabel) +{ + uint256 hash = Hash(strLabel.begin(), strLabel.end()); + vector vchIV(16); + memcpy(&vchIV[0], hash.begin(), vchIV.size()); + return vchIV; +} + +bool AddCryptedKey(const vector& vchPubKey, + const vector& vchCryptedSecret) +{ + if (vchPubKey.empty() || vchCryptedSecret.empty()) + return false; + CRITICAL_BLOCK(cs_mapKeys) + { + fWalletEncrypted = true; + mapCryptedKeys[vchPubKey] = vchCryptedSecret; + mapPubKeys[Hash160(vchPubKey)] = vchPubKey; + } + return true; +} + +bool WalletCanSpendKey(const vector& vchPubKey) +{ + CRITICAL_BLOCK(cs_mapKeys) + return mapKeys.count(vchPubKey) > 0 || mapCryptedKeys.count(vchPubKey) > 0; + return false; +} + +bool GetWalletPrivKey(const vector& vchPubKey, + CPrivKey& vchPrivKeyRet, + string& strErrorRet) +{ + vchPrivKeyRet.clear(); + CRITICAL_BLOCK(cs_mapKeys) + { + map, CPrivKey>::iterator mi = mapKeys.find(vchPubKey); + if (mi != mapKeys.end()) + { + vchPrivKeyRet = (*mi).second; + return true; + } + + map, vector >::iterator mci = mapCryptedKeys.find(vchPubKey); + if (mci == mapCryptedKeys.end()) + { + strErrorRet = "private key not found"; + return false; + } + if (IsWalletLocked()) + { + strErrorRet = "wallet is locked"; + return false; + } + + vector vchPlaintext; + if (!DecryptSecret(vWalletMasterKey, (*mci).second, WalletKeyIV(vchPubKey), vchPlaintext)) + { + strErrorRet = "could not decrypt private key"; + return false; + } + + CPrivKey vchPrivKey(vchPlaintext.begin(), vchPlaintext.end()); + CKey key; + if (!key.SetPrivKey(vchPrivKey) || key.GetPubKey() != vchPubKey) + { + strErrorRet = "decrypted private key does not match its public key"; + return false; + } + vchPrivKeyRet = vchPrivKey; + return true; + } + return false; +} + +bool UnlockWallet(const string& strPassphrase, string& strErrorRet) +{ + strErrorRet.clear(); + if (!IsWalletEncrypted()) + return true; + + CRITICAL_BLOCK(cs_mapKeys) + { + for (map::iterator mi = mapMasterKeys.begin(); + mi != mapMasterKeys.end(); ++mi) + { + const CWalletMasterKey& kMasterKey = (*mi).second; + CKeyingMaterial vchPassKey; + vector vchPassIV; + if (!DeriveWalletPassphraseKey(strPassphrase, kMasterKey.vchSalt, + kMasterKey.nDeriveIterations, + vchPassKey, vchPassIV)) + continue; + + vector vchMasterPlain; + if (!DecryptSecret(vchPassKey, kMasterKey.vchCryptedKey, + vchPassIV, vchMasterPlain)) + continue; + if (vchMasterPlain.size() != 32) + continue; + + CKeyingMaterial vchCandidate(vchMasterPlain.begin(), vchMasterPlain.end()); + bool fValid = mapCryptedKeys.empty(); + for (map, vector >::iterator mci = mapCryptedKeys.begin(); + mci != mapCryptedKeys.end(); ++mci) + { + vector vchPlaintext; + if (!DecryptSecret(vchCandidate, (*mci).second, WalletKeyIV((*mci).first), vchPlaintext)) + break; + CPrivKey vchPrivKey(vchPlaintext.begin(), vchPlaintext.end()); + CKey key; + if (key.SetPrivKey(vchPrivKey) && key.GetPubKey() == (*mci).first) + fValid = true; + break; + } + if (fValid) + { + vWalletMasterKey = vchCandidate; + if (!vchCryptedHDMaster.empty()) + { + vector vchPlainHDMaster; + if (!DecryptSecret(vWalletMasterKey, vchCryptedHDMaster, + WalletSecretIV("hdmaster"), vchPlainHDMaster) || + vchPlainHDMaster.size() != 32) + { + strErrorRet = "could not decrypt HD master key"; + return false; + } + vchHDMaster = vchPlainHDMaster; + } + if (!vchCryptedHDChainCode.empty()) + { + vector vchPlainHDChainCode; + if (!DecryptSecret(vWalletMasterKey, vchCryptedHDChainCode, + WalletSecretIV("hdchaincode"), vchPlainHDChainCode) || + vchPlainHDChainCode.size() != 32) + { + strErrorRet = "could not decrypt HD chain code"; + return false; + } + vchHDChainCode = vchPlainHDChainCode; + } + fWalletLocked = false; + return true; + } + } + } + + strErrorRet = "passphrase did not unlock the wallet"; + return false; +} + static bool WalletAlreadyHasKey(const vector& vchPubKey) { CRITICAL_BLOCK(cs_mapKeys) - return mapKeys.count(vchPubKey) > 0; + return mapKeys.count(vchPubKey) > 0 || mapCryptedKeys.count(vchPubKey) > 0; return false; } @@ -509,6 +809,12 @@ bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet) void TopUpKeyPool() { + if (IsWalletLocked()) + { + printf("TopUpKeyPool() : wallet is encrypted and locked\n"); + return; + } + CRITICAL_BLOCK(cs_keyPool) { while ((int)mapKeyPool.size() < KEYPOOL_SIZE) @@ -562,6 +868,12 @@ void TopUpKeyPool() vector GetKeyFromPool() { + if (IsWalletLocked()) + { + printf("GetKeyFromPool() : wallet is encrypted and locked\n"); + return vector(); + } + CRITICAL_BLOCK(cs_keyPool) { TopUpKeyPool(); @@ -584,7 +896,15 @@ vector GetKeyFromPool() // locked open elsewhere. Generating on demand is what this code did for // its whole life, so fall back to it rather than refuse to mine. printf("GetKeyFromPool() : pool empty, generating a key on demand\n"); - return GenerateNewKey(); + try + { + return GenerateNewKey(); + } + catch (const std::exception& e) + { + printf("GetKeyFromPool() : failed to generate a key: %s\n", e.what()); + return vector(); + } } @@ -3581,6 +3901,11 @@ bool BitcoinMiner(int nThreadId) // thread happened to win a block, which meant a backup taken while mining // could not spend what the very next block paid. vector vchPubKey = GetKeyFromPool(); + if (vchPubKey.empty()) + { + printf("BitcoinMiner: no wallet key available for coinbase; mining stopped\n"); + return false; + } CBigNum bnExtraNonce = 0; while (fGenerateBitcoins) { @@ -3759,6 +4084,11 @@ bool BitcoinMiner(int nThreadId) // them out one at a time under cs_keyPool, which is // what keeps concurrent coinbases distinct (#58). vchPubKey = GetKeyFromPool(); + if (vchPubKey.empty()) + { + printf("BitcoinMiner: no replacement wallet key available; mining stopped\n"); + fGenerateBitcoins = false; + } // Process this block the same as if we had received it from another node if (!ProcessBlock(NULL, pblock.release())) diff --git a/src/main.h b/src/main.h index cca2cad..c458412 100644 --- a/src/main.h +++ b/src/main.h @@ -129,6 +129,46 @@ FILE* AppendBlockFile(unsigned int& nFileRet); bool AddKey(const CKey& key); vector GenerateNewKey(); +// --- Wallet encryption ---------------------------------------------------- +// +// Plain wallets keep private keys in mapKeys. Encrypted wallets keep public +// keys visible so balances can still be recognized while locked, and decrypt +// private keys only after an unlock passphrase has supplied the master key. +typedef vector > CKeyingMaterial; +extern map, vector > mapCryptedKeys; +extern map mapMasterKeys; +extern unsigned int nWalletMasterKeyMaxID; +extern CKeyingMaterial vWalletMasterKey; +extern vector vchCryptedHDMaster; +extern vector vchCryptedHDChainCode; +extern bool fWalletEncrypted; +extern bool fWalletLocked; +bool IsWalletEncrypted(); +bool IsWalletLocked(); +void LockWallet(); +bool UnlockWallet(const string& strPassphrase, string& strErrorRet); +bool DeriveWalletPassphraseKey(const string& strPassphrase, + const vector& vchSalt, + unsigned int nDeriveIterations, + CKeyingMaterial& vchKeyRet, + vector& vchIVRet); +bool EncryptSecret(const CKeyingMaterial& vchKey, + const vector& vchPlaintext, + const vector& vchIV, + vector& vchCiphertextRet); +bool DecryptSecret(const CKeyingMaterial& vchKey, + const vector& vchCiphertext, + const vector& vchIV, + vector& vchPlaintextRet); +vector WalletKeyIV(const vector& vchPubKey); +vector WalletSecretIV(const string& strLabel); +bool AddCryptedKey(const vector& vchPubKey, + const vector& vchCryptedSecret); +bool WalletCanSpendKey(const vector& vchPubKey); +bool GetWalletPrivKey(const vector& vchPubKey, + CPrivKey& vchPrivKeyRet, + string& strErrorRet); + // --- Key pool ------------------------------------------------------------- // // A backup of this wallet only ever contained the keys that existed the moment diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 6164ad7..a2577ca 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -40,6 +40,60 @@ static string argval2(int argc, char* argv[], const char* keySlash, const char* return argval(argc, argv, keyDash); } +static bool ReadPassphraseArgument(const string& strArg, string& strPassphraseRet, string& strErrorRet) +{ + strPassphraseRet.clear(); + strErrorRet.clear(); + if (strArg.empty()) + { + AttachTerminal(); + fprintf(stderr, "Enter wallet passphrase: "); + fflush(stderr); + char buf[4096]; + if (!fgets(buf, sizeof(buf), stdin)) + { + strErrorRet = "could not read passphrase from stdin"; + return false; + } + strPassphraseRet = buf; + } + else if (strArg[0] == '@') + { + string strPath = strArg.substr(1); + FILE* pf = fopen(strPath.c_str(), "rb"); + if (!pf) + { + strErrorRet = strprintf("could not read passphrase file %s", strPath.c_str()); + return false; + } + char buf[4096]; + if (!fgets(buf, sizeof(buf), pf)) + { + fclose(pf); + strErrorRet = strprintf("passphrase file %s is empty", strPath.c_str()); + return false; + } + fclose(pf); + strPassphraseRet = buf; + } + else + { + strErrorRet = "passphrase literals on the command line are unsafe; use the option without a value to read stdin, or use @FILE"; + return false; + } + + while (!strPassphraseRet.empty() && + (strPassphraseRet[strPassphraseRet.size() - 1] == '\n' || + strPassphraseRet[strPassphraseRet.size() - 1] == '\r')) + strPassphraseRet.resize(strPassphraseRet.size() - 1); + if (strPassphraseRet.empty()) + { + strErrorRet = "empty passphrase"; + return false; + } + return true; +} + // The real printf, not the one util.h remaps to OutputDebugStringF. Help that // goes to debug.log is help nobody asked for: `-help` printed a full page into // the data directory and returned 0 with an empty terminal, which reads as a @@ -56,7 +110,7 @@ static void PrintUsage() printf(" /debug\n"); printf(" /gen\n"); printf(" /nogui or /daemon\n"); - printf(" /selftest=wallet-keypool or /selftest=wallet-hd\n"); + printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, or wallet-encrypt\n"); printf("\n"); printf("Mining mode:\n"); printf(" /operator\n"); @@ -85,6 +139,10 @@ static void PrintUsage() printf(" /dumpwallet=FILE (export private keys as text -- readable by\n"); printf(" anyone, so guard it like cash)\n"); printf(" /importwallet=FILE (load keys from such a file back in)\n"); + printf(" /encryptwallet[=@FILE] (rewrite wallet.dat with encrypted private keys\n"); + printf(" and encrypted HD seed, then exit)\n"); + printf(" /walletpassphrase[=@FILE] (unlock an encrypted wallet for one-shot\n"); + printf(" commands; without @FILE reads stdin)\n"); printf(" /newaddress (print the next receiving address, then exit)\n"); printf(" /sendto=ADDRESS,AMOUNT (spend from this wallet, then exit -- the\n"); printf(" only way to send without the window)\n"); @@ -299,6 +357,39 @@ int main(int argc, char* argv[]) } printf("Height=%d\n", nBestHeight); + bool fEncryptWallet = arg(argc, argv, "/encryptwallet") || arg(argc, argv, "-encryptwallet"); + string strEncryptWallet = argval2(argc, argv, "/encryptwallet", "-encryptwallet"); + if (fEncryptWallet) + { + string strPassphrase; + string strPassphraseError; + if (!ReadPassphraseArgument(strEncryptWallet, strPassphrase, strPassphraseError)) + { + fprintf(stderr, "Cannot read encryption passphrase: %s\n", strPassphraseError.c_str()); + DBFlush(true); + return 1; + } + int nRet = CmdEncryptWallet(strPassphrase); + if (nRet != 0) + DBFlush(true); + return nRet; + } + + bool fWalletPassphrase = arg(argc, argv, "/walletpassphrase") || arg(argc, argv, "-walletpassphrase"); + string strWalletPassphrase = argval2(argc, argv, "/walletpassphrase", "-walletpassphrase"); + if (fWalletPassphrase) + { + string strPassphrase; + string strUnlockError; + if (!ReadPassphraseArgument(strWalletPassphrase, strPassphrase, strUnlockError) || + !UnlockWallet(strPassphrase, strUnlockError)) + { + fprintf(stderr, "Cannot unlock wallet: %s\n", strUnlockError.c_str()); + DBFlush(true); + return 1; + } + } + // Before the node opens sockets or touches anything: the wallet is loaded, // which is all a backup needs, and finishing here means the copy is taken // from a quiet directory rather than from under a running node. @@ -442,6 +533,14 @@ int main(int argc, char* argv[]) return 0; } + if (fGenerateBitcoins && IsWalletLocked()) + { + fprintf(stderr, "Cannot mine while the encrypted wallet is locked. " + "Start with /walletpassphrase or /walletpassphrase=@FILE, or disable mining.\n"); + DBFlush(true); + return 1; + } + ReacceptWalletTransactions(); if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; } diff --git a/src/makefile.mingw b/src/makefile.mingw index 9bbfef1..e076c49 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -111,5 +111,8 @@ tests: test_bip32.exe bitflash.exe ./test_bip32.exe ./bitflash.exe -selftest=wallet-keypool ./bitflash.exe -selftest=wallet-hd + ./bitflash.exe -selftest=wallet-format + ./bitflash.exe -selftest=wallet-crypto + ./bitflash.exe -selftest=wallet-encrypt ./bitflash.exe -selftest=net-message ./bitflash.exe -selftest=consensus-limits diff --git a/src/script.cpp b/src/script.cpp index 6d4fb34..1e6e1ca 100644 --- a/src/script.cpp +++ b/src/script.cpp @@ -1020,43 +1020,54 @@ bool Solver(const CScript& scriptPubKey, uint256 hash, int nHashType, CScript& s if (!Solver(scriptPubKey, vSolution)) return false; - // Compile solution - CRITICAL_BLOCK(cs_mapKeys) + // Compile solution. Public keys stay available for encrypted wallets so the + // wallet can recognize its own outputs while locked; private keys are + // fetched only for an actual signature. + foreach(PAIRTYPE(opcodetype, valtype)& item, vSolution) { - foreach(PAIRTYPE(opcodetype, valtype)& item, vSolution) + if (item.first == OP_PUBKEY) { - if (item.first == OP_PUBKEY) + const valtype& vchPubKey = item.second; + if (!WalletCanSpendKey(vchPubKey)) + return false; + if (hash != 0) { - // Sign - const valtype& vchPubKey = item.second; - if (!mapKeys.count(vchPubKey)) + CPrivKey vchPrivKey; + string strError; + if (!GetWalletPrivKey(vchPubKey, vchPrivKey, strError)) + return false; + vector vchSig; + if (!CKey::Sign(vchPrivKey, hash, vchSig)) return false; - if (hash != 0) + vchSig.push_back((unsigned char)nHashType); + scriptSigRet << vchSig; + } + } + else if (item.first == OP_PUBKEYHASH) + { + vector vchPubKey; + { + CRITICAL_BLOCK(cs_mapKeys) { - vector vchSig; - if (!CKey::Sign(mapKeys[vchPubKey], hash, vchSig)) + map::iterator mi = mapPubKeys.find(uint160(item.second)); + if (mi == mapPubKeys.end()) return false; - vchSig.push_back((unsigned char)nHashType); - scriptSigRet << vchSig; + vchPubKey = (*mi).second; } } - else if (item.first == OP_PUBKEYHASH) + if (!WalletCanSpendKey(vchPubKey)) + return false; + if (hash != 0) { - // Sign and give pubkey - map::iterator mi = mapPubKeys.find(uint160(item.second)); - if (mi == mapPubKeys.end()) + CPrivKey vchPrivKey; + string strError; + if (!GetWalletPrivKey(vchPubKey, vchPrivKey, strError)) return false; - const vector& vchPubKey = (*mi).second; - if (!mapKeys.count(vchPubKey)) + vector vchSig; + if (!CKey::Sign(vchPrivKey, hash, vchSig)) return false; - if (hash != 0) - { - vector vchSig; - if (!CKey::Sign(mapKeys[vchPubKey], hash, vchSig)) - return false; - vchSig.push_back((unsigned char)nHashType); - scriptSigRet << vchSig << vchPubKey; - } + vchSig.push_back((unsigned char)nHashType); + scriptSigRet << vchSig << vchPubKey; } } } @@ -1080,27 +1091,27 @@ bool ExtractPubKey(const CScript& scriptPubKey, bool fMineOnly, vector::iterator mi = mapPubKeys.find(uint160(item.second)); if (mi == mapPubKeys.end()) continue; vchPubKey = (*mi).second; } - if (!fMineOnly || mapKeys.count(vchPubKey)) - { - vchPubKeyRet = vchPubKey; - return true; - } + } + if (!fMineOnly || WalletCanSpendKey(vchPubKey)) + { + vchPubKeyRet = vchPubKey; + return true; } } return false; diff --git a/src/selftest.cpp b/src/selftest.cpp index aee2951..ff0289b 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -33,6 +33,8 @@ #ifdef _WIN32 #include +#include +#include #else #include #include @@ -601,6 +603,500 @@ static int RunWalletHDSelfTest() return nFail == 0 ? 0 : 1; } +static int RunWalletFormatSelfTest() +{ + fflush(stdout); + printf("wallet-format self-test\n"); + + std::string tmp; + std::string cwd; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) + { + printf(" FAIL could not move into the temporary data directory\n"); + RemoveTree(tmp); + return 1; + } + + int nFail = 0; + strSetDataDir = tmp; + printf(" temp datadir: %s\n", tmp.c_str()); + + try + { + class CWalletDBRaw : public CWalletDB + { + public: + CWalletDBRaw(const char* pszMode="r+") : CWalletDB(pszMode) { } + bool WriteStringRecord(const string& strType, int nValue) + { + return Write(strType, nValue); + } + bool EraseStringRecord(const string& strType) + { + return Erase(strType); + } + }; + + if (!LoadWallet()) + throw std::runtime_error("LoadWallet failed"); + + { + CWalletDB walletdb; + nFail += Check(walletdb.WriteWalletMinVersion(WALLET_FORMAT_SUPPORTED), + "the current wallet format marker can be written") ? 0 : 1; + } + + std::vector vchDefaultKey; + nFail += Check(CWalletDB("r").LoadWallet(vchDefaultKey), + "the current wallet format marker loads") ? 0 : 1; + + { + CWalletDBRaw walletdb; + nFail += Check(walletdb.WriteStringRecord("mkey", 1), + "a malformed encrypted master-key record can be written") ? 0 : 1; + } + + vchDefaultKey.clear(); + nFail += Check(!CWalletDB("r").LoadWallet(vchDefaultKey), + "a malformed encrypted master-key record is refused") ? 0 : 1; + + { + CWalletDBRaw walletdb; + nFail += Check(walletdb.EraseStringRecord("mkey"), + "the malformed encrypted master-key record can be removed") ? 0 : 1; + } + + { + CWalletDB walletdb; + nFail += Check(walletdb.WriteWalletMinVersion(WALLET_FORMAT_SUPPORTED + 1), + "a future wallet format marker can be written") ? 0 : 1; + } + + vchDefaultKey.clear(); + nFail += Check(!CWalletDB("r").LoadWallet(vchDefaultKey), + "a future wallet format marker is refused") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + DBFlush(true); + SetCurrentDir(cwd); + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + +static void ClearWalletRuntimeForTest() +{ + CRITICAL_BLOCK(cs_mapKeys) + { + mapKeys.clear(); + mapCryptedKeys.clear(); + mapPubKeys.clear(); + mapMasterKeys.clear(); + vWalletMasterKey.clear(); + vchCryptedHDMaster.clear(); + vchCryptedHDChainCode.clear(); + fWalletEncrypted = false; + fWalletLocked = true; + } + CRITICAL_BLOCK(cs_keyPool) + mapKeyPool.clear(); + vchHDMaster.clear(); + vchHDChainCode.clear(); + nHDNext = 0; +} + +static int RunWalletCryptoSelfTest() +{ + fflush(stdout); + printf("wallet-crypto self-test\n"); + + std::string tmp; + std::string cwd; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) + { + printf(" FAIL could not move into the temporary data directory\n"); + RemoveTree(tmp); + return 1; + } + + int nFail = 0; + strSetDataDir = tmp; + printf(" temp datadir: %s\n", tmp.c_str()); + + try + { + const string strPassphrase = "btf-test-passphrase"; + const string strWrongPassphrase = "btf-test-passphraser"; + + CKey key; + key.MakeNewKey(); + vector vchPubKey = key.GetPubKey(); + CPrivKey vchPrivKey = key.GetPrivKey(); + + CKeyingMaterial vchMasterKey; + for (int i = 0; i < 32; i++) + vchMasterKey.push_back((unsigned char)(i + 1)); + + CWalletMasterKey kMasterKey; + kMasterKey.vchSalt.clear(); + for (int i = 0; i < 8; i++) + kMasterKey.vchSalt.push_back((unsigned char)(0xa0 + i)); + kMasterKey.nDeriveIterations = 2500; + + CKeyingMaterial vchPassKey; + vector vchPassIV; + nFail += Check(DeriveWalletPassphraseKey(strPassphrase, kMasterKey.vchSalt, + kMasterKey.nDeriveIterations, + vchPassKey, vchPassIV), + "a passphrase key can be derived") ? 0 : 1; + nFail += Check(EncryptSecret(vchPassKey, + vector(vchMasterKey.begin(), vchMasterKey.end()), + vchPassIV, kMasterKey.vchCryptedKey), + "the wallet master key can be encrypted") ? 0 : 1; + + vector vchCryptedKey; + nFail += Check(EncryptSecret(vchMasterKey, + vector(vchPrivKey.begin(), vchPrivKey.end()), + WalletKeyIV(vchPubKey), vchCryptedKey), + "a private key can be encrypted under the wallet master key") ? 0 : 1; + + { + CWalletDB walletdb("cr"); + nFail += Check(walletdb.WriteWalletMinVersion(WALLET_FORMAT_ENCRYPTED), + "an encrypted wallet format marker can be written") ? 0 : 1; + nFail += Check(walletdb.WriteMasterKey(0, kMasterKey), + "the encrypted master key can be written") ? 0 : 1; + nFail += Check(walletdb.WriteCryptedKey(vchPubKey, vchCryptedKey), + "the encrypted private key can be written") ? 0 : 1; + nFail += Check(walletdb.WriteDefaultKey(vchPubKey), + "the encrypted wallet default key can be written") ? 0 : 1; + } + + ClearWalletRuntimeForTest(); + + vector vchDefaultKey; + nFail += Check(CWalletDB("r").LoadWallet(vchDefaultKey), + "an encrypted wallet can be loaded") ? 0 : 1; + nFail += Check(vchDefaultKey == vchPubKey, + "the encrypted wallet default public key loads") ? 0 : 1; + nFail += Check(IsWalletEncrypted() && IsWalletLocked(), + "an encrypted wallet loads locked") ? 0 : 1; + nFail += Check(WalletCanSpendKey(vchPubKey), + "an encrypted key is recognized as wallet-owned while locked") ? 0 : 1; + + CPrivKey vchDecrypted; + string strError; + nFail += Check(!GetWalletPrivKey(vchPubKey, vchDecrypted, strError), + "a locked encrypted key cannot be read") ? 0 : 1; + nFail += Check(!UnlockWallet(strWrongPassphrase, strError), + "the wrong-passphrase does not unlock") ? 0 : 1; + nFail += Check(UnlockWallet(strPassphrase, strError), + "the right passphrase unlocks") ? 0 : 1; + nFail += Check(!IsWalletLocked(), + "the wallet reports unlocked") ? 0 : 1; + nFail += Check(GetWalletPrivKey(vchPubKey, vchDecrypted, strError) && + vchDecrypted == vchPrivKey, + "an unlocked encrypted key decrypts to the original private key") ? 0 : 1; + LockWallet(); + nFail += Check(IsWalletLocked(), + "locking clears the unlocked state") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + DBFlush(true); + SetCurrentDir(cwd); + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + +static bool FileContainsBytes(const string& strPath, const vector& vchNeedle) +{ + if (vchNeedle.empty()) + return false; + FILE* pf = fopen(strPath.c_str(), "rb"); + if (!pf) + return false; + + vector vchHaystack; + unsigned char buf[4096]; + size_t n = 0; + while ((n = fread(buf, 1, sizeof(buf), pf)) > 0) + vchHaystack.insert(vchHaystack.end(), buf, buf + n); + fclose(pf); + + if (vchHaystack.size() < vchNeedle.size()) + return false; + return search(vchHaystack.begin(), vchHaystack.end(), + vchNeedle.begin(), vchNeedle.end()) != vchHaystack.end(); +} + +static bool FileContainsText(const string& strPath, const string& strNeedle) +{ + FILE* pf = fopen(strPath.c_str(), "rb"); + if (!pf) + return false; + + string strHaystack; + char buf[4096]; + size_t n = 0; + while ((n = fread(buf, 1, sizeof(buf), pf)) > 0) + strHaystack.append(buf, buf + n); + fclose(pf); + + return strHaystack.find(strNeedle) != string::npos; +} + +static bool WriteTextFile(const string& strPath, const string& strText) +{ + FILE* pf = fopen(strPath.c_str(), "wb"); + if (!pf) + return false; + bool fOk = fwrite(strText.data(), 1, strText.size(), pf) == strText.size(); + if (fclose(pf) != 0) + fOk = false; + return fOk; +} + +static string QuoteCommandArg(const string& str) +{ + string out = "\""; + for (size_t i = 0; i < str.size(); i++) + { + if (str[i] == '"') + out += "\\\""; + else + out += str[i]; + } + out += "\""; + return out; +} + +static int RunBitflashChild(const string& strExe, + const vector& vArgs, + const string* pStdinFile = NULL) +{ +#ifdef _WIN32 + vector argv; + argv.push_back((char*)strExe.c_str()); + for (size_t i = 0; i < vArgs.size(); i++) + argv.push_back((char*)vArgs[i].c_str()); + argv.push_back(NULL); + + int nOldOut = _dup(1); + int nOldErr = _dup(2); + int nOldIn = _dup(0); + int nNull = _open("NUL", _O_WRONLY); + int nIn = -1; + if (nNull >= 0) + { + _dup2(nNull, 1); + _dup2(nNull, 2); + } + if (pStdinFile) + { + nIn = _open(pStdinFile->c_str(), _O_RDONLY); + if (nIn >= 0) + _dup2(nIn, 0); + } + int nRet = _spawnv(_P_WAIT, strExe.c_str(), &argv[0]); + if (nIn >= 0) + _close(nIn); + if (nNull >= 0) + _close(nNull); + if (nOldIn >= 0) + { + _dup2(nOldIn, 0); + _close(nOldIn); + } + if (nOldOut >= 0) + { + _dup2(nOldOut, 1); + _close(nOldOut); + } + if (nOldErr >= 0) + { + _dup2(nOldErr, 2); + _close(nOldErr); + } + return nRet; +#else + string strCmd = QuoteCommandArg(strExe); + for (size_t i = 0; i < vArgs.size(); i++) + strCmd += " " + QuoteCommandArg(vArgs[i]); + if (pStdinFile) + strCmd += " < " + QuoteCommandArg(*pStdinFile); + strCmd += " > /dev/null 2>&1"; + return system(strCmd.c_str()); +#endif +} + +static int RunWalletEncryptSelfTest() +{ + fflush(stdout); + printf("wallet-encrypt self-test\n"); + + std::string tmp; + std::string cwd; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) + { + printf(" FAIL could not move into the temporary data directory\n"); + RemoveTree(tmp); + return 1; + } + + int nFail = 0; + bool fWalletEnvClosed = false; + strSetDataDir = tmp; + printf(" temp datadir: %s\n", tmp.c_str()); + + try + { + if (!LoadWallet()) + throw std::runtime_error("LoadWallet failed"); + + string strError; + const string strMnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + nFail += Check(SetHDSeedFromMnemonic(strMnemonic, strError), + "a phrase can be installed before encryption") ? 0 : 1; + TopUpKeyPool(); + + vector vchDefaultPubKey = keyUser.GetPubKey(); + CPrivKey vchDefaultPrivKey; + nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), + "the default private key is readable before encryption") ? 0 : 1; + vector vchDefaultPrivBytes(vchDefaultPrivKey.begin(), vchDefaultPrivKey.end()); + vector vchPlainHDMaster = vchHDMaster; + vector vchPlainHDChainCode = vchHDChainCode; + + string strBackup; + nFail += Check(EncryptWallet("btf-test-passphrase", strBackup, strError), + "wallet.dat can be rewritten encrypted") ? 0 : 1; + fWalletEnvClosed = true; + nFail += Check(FileExists(strBackup.c_str()), + "the original wallet is preserved as an explicit backup") ? 0 : 1; + + string strWalletPath = GetAppDir() + "/wallet.dat"; + nFail += Check(!FileContainsBytes(strWalletPath, vchDefaultPrivBytes), + "the active wallet no longer contains the default private key bytes") ? 0 : 1; + nFail += Check(!FileContainsBytes(strWalletPath, vchPlainHDMaster), + "the active wallet no longer contains the HD master key bytes") ? 0 : 1; + nFail += Check(!FileContainsBytes(strWalletPath, vchPlainHDChainCode), + "the active wallet no longer contains the HD chain-code bytes") ? 0 : 1; + +#ifdef _WIN32 + string strExe = cwd + "\\bitflash.exe"; +#else + string strExe = cwd + "/bitflash-node"; +#endif + string strWrongDump = tmp + "/wrong-pass-dump.txt"; + string strRightDump = tmp + "/right-pass-dump.txt"; + string strLiteralDump = tmp + "/literal-pass-dump.txt"; + string strStdinDump = tmp + "/stdin-pass-dump.txt"; + string strWrongPassFile = tmp + "/wrong-pass.txt"; + string strRightPassFile = tmp + "/right-pass.txt"; + nFail += Check(WriteTextFile(strWrongPassFile, "wrong-passphrase\n") && + WriteTextFile(strRightPassFile, "btf-test-passphrase\n"), + "passphrase files can be written for restarted commands") ? 0 : 1; + + vector vWrongArgs; + vWrongArgs.push_back("-datadir=" + tmp); + vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile); + vWrongArgs.push_back("-dumpwallet=" + strWrongDump); + vWrongArgs.push_back("-nogui"); + int nWrongRet = RunBitflashChild(strExe, vWrongArgs); + nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()), + "a restarted wallet rejects the wrong-passphrase") ? 0 : 1; + + vector vLiteralArgs; + vLiteralArgs.push_back("-datadir=" + tmp); + vLiteralArgs.push_back("-walletpassphrase=btf-test-passphrase"); + vLiteralArgs.push_back("-dumpwallet=" + strLiteralDump); + vLiteralArgs.push_back("-nogui"); + int nLiteralRet = RunBitflashChild(strExe, vLiteralArgs); + nFail += Check(nLiteralRet != 0 && !FileExists(strLiteralDump.c_str()), + "literal passphrases on the command line are refused") ? 0 : 1; + + vector vRightArgs; + vRightArgs.push_back("-datadir=" + tmp); + vRightArgs.push_back("-walletpassphrase=@" + strRightPassFile); + vRightArgs.push_back("-dumpwallet=" + strRightDump); + vRightArgs.push_back("-nogui"); + int nRightRet = RunBitflashChild(strExe, vRightArgs); + nFail += Check(nRightRet == 0 && FileExists(strRightDump.c_str()), + "a restarted wallet unlocks with the right passphrase") ? 0 : 1; + nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)), + "the restarted wallet can decrypt and dump the original key") ? 0 : 1; + + vector vStdinArgs; + vStdinArgs.push_back("-datadir=" + tmp); + vStdinArgs.push_back("-walletpassphrase"); + vStdinArgs.push_back("-dumpwallet=" + strStdinDump); + vStdinArgs.push_back("-nogui"); + int nStdinRet = RunBitflashChild(strExe, vStdinArgs, &strRightPassFile); + nFail += Check(nStdinRet == 0 && FileExists(strStdinDump.c_str()), + "a restarted wallet can read the passphrase from stdin") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + if (!fWalletEnvClosed) + DBFlush(true); + SetCurrentDir(cwd); + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + static int RunNetMessageSelfTest() { fflush(stdout); @@ -728,12 +1224,18 @@ int RunSelfTest(const std::string& name) return RunWalletKeyPoolSelfTest(); if (name == "wallet-hd") return RunWalletHDSelfTest(); + if (name == "wallet-format") + return RunWalletFormatSelfTest(); + if (name == "wallet-crypto") + return RunWalletCryptoSelfTest(); + if (name == "wallet-encrypt") + return RunWalletEncryptSelfTest(); if (name == "net-message") return RunNetMessageSelfTest(); if (name == "consensus-limits") return RunConsensusLimitsSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - printf("Known self-tests: wallet-keypool, wallet-hd, net-message, consensus-limits\n"); + printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits\n"); return 1; } diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index b449df9..06f35a5 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -406,6 +406,36 @@ int CmdRecoveryAudit() return 0; } +int CmdEncryptWallet(const std::string& strPassphrase) +{ + AttachTerminal(); + + string strBackup; + string strError; + if (!EncryptWallet(strPassphrase, strBackup, strError)) + { + fprintf(stderr, "Cannot encrypt wallet: %s\n", strError.c_str()); + fprintf(stderr, "Nothing was changed.\n"); + return 1; + } + + printf("Wallet encrypted.\n"); + printf("The old unencrypted wallet was moved to:\n"); + printf(" %s\n", strBackup.c_str()); + printf("That backup still contains private keys in plain text. Move it offline or\n"); + printf("delete it after you have a safer backup plan.\n"); + printf("Restart Bitflash before using the wallet again.\n"); + fflush(stdout); + + // The rewritten wallet.dat is clean; the environment's write-ahead logs are + // not, because every plaintext record this wallet ever wrote passed through + // them. Closing the environment and dropping the logs is the last step of + // encrypting, not an optimisation -- our own backup advice is to copy the + // whole directory, and following it otherwise carries the keys along. + PurgeDbEnvironmentLogs(); + return 0; +} + // Spend, from the command line. // // SendMoney() has been in this tree since 0.1.0 and only the window ever called @@ -421,6 +451,12 @@ int CmdSendTo(const std::string& strArg) { AttachTerminal(); + if (IsWalletLocked()) + { + fprintf(stderr, "Wallet is encrypted and locked. Start with /walletpassphrase or /walletpassphrase=@FILE to spend.\n"); + return 1; + } + std::string::size_type comma = strArg.rfind(','); if (comma == std::string::npos) { diff --git a/src/walletcmd.h b/src/walletcmd.h index 32d3eef..04d3fb7 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h @@ -53,6 +53,10 @@ int CmdShowDerived(int nCount); // by the installed recovery phrase or still depends on wallet.dat-only keys. int CmdRecoveryAudit(); +// Rewrite wallet.dat so private keys and the HD seed are encrypted with the +// given passphrase. Returns 0 on success and exits without starting the node. +int CmdEncryptWallet(const std::string& strPassphrase); + // Spend, from the command line. Until now the only way to send was the window, // so a headless node could be paid and could never pay -- it could hold a // balance it had no way to move. strArg is "ADDRESS,AMOUNT". -- cgit v1.2.3 From b3a5a858e536acf65e6c6761293626628f2227bf Mon Sep 17 00:00:00 2001 From: mtg0001 <106211915+mtg0001@users.noreply.github.com> Date: Tue, 4 Aug 2026 15:31:55 -0300 Subject: fuzz: add P2P message parser harness (#142) * fuzz: add P2P message parser harness * fuzz: avoid duplicate legacy header includes * fuzz: seed the corpus with our network magic, not Bitcoin's The two seed inputs began f5 be b6 d9, which is Bitcoin's message start. Ours is bf 20 5c fd. CMessageHeader::IsValid rejects on that field first, so both seeds died at the first check and the smoke run exercised the reject path and nothing else. libFuzzer would have to mutate its way onto four exact bytes before reaching any parser, which does not happen in 128 runs. The job would have gone green while covering none of what it was added to cover. --------- Co-authored-by: Gilcimar Duarte Co-authored-by: Bitflash-sh --- .github/workflows/ci.yml | 3 +++ Makefile | 7 +++++-- src/Makefile | 34 +++++++++++++++++++++++++++++++++- src/fuzz_net_message.cpp | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 src/fuzz_net_message.cpp (limited to 'src/Makefile') diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 985ba8a..9efd66f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,9 @@ jobs: - name: Run Linux selftests run: make tests + - name: Run P2P message fuzz smoke + run: make fuzz-net-message-smoke + windows-ucrt64: name: Windows UCRT64 selftests runs-on: windows-latest diff --git a/Makefile b/Makefile index 0788b61..736f5d8 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,7 @@ deps-apt: @if command -v apt-get >/dev/null 2>&1; then \ $(SUDO) apt-get install -y build-essential cmake git pkg-config autoconf \ libtool libssl-dev libdb5.3++-dev libsodium-dev nlohmann-json3-dev \ - libboost-system-dev libglfw3-dev libgl-dev python3-pil; \ + libboost-system-dev libglfw3-dev libgl-dev python3-pil clang; \ else \ echo "Not an apt system — install: g++ cmake git autoconf libtool"; \ echo " libssl libdb++ libsodium nlohmann-json boost glfw3 opengl python3-pil"; \ @@ -191,6 +191,9 @@ clean: tests: $(MAKE) -C src -f Makefile tests +fuzz-net-message-smoke: deps-linux + $(MAKE) -C src -f Makefile fuzz-net-message-smoke + checksums: ./scripts/make-release-checksums.sh @@ -201,5 +204,5 @@ verify-release: ./scripts/verify-release.sh $(if $(TAG),$(TAG),latest) .PHONY: linux windows clean appimage \ - tests checksums sign-checksums verify-release \ + tests fuzz-net-message-smoke checksums sign-checksums verify-release \ deps-linux deps-windows deps-apt deps-secp256k1 deps-randomx diff --git a/src/Makefile b/src/Makefile index 3577dd6..ac0226e 100644 --- a/src/Makefile +++ b/src/Makefile @@ -2,6 +2,7 @@ # Called by: make linux (root Makefile) CXX ?= g++ +FUZZ_CXX ?= clang++ SECP_INC ?= /usr/local/include SECP_LIB ?= /usr/local/lib @@ -33,6 +34,9 @@ CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ -I$(SECP_INC) -I$(RANDOMX_DIR)/src \ -I/usr/include/nlohmann \ -I$(IMGUI_DIR) +FUZZ_CXXFLAGS = $(filter-out -O2,$(CXXFLAGS)) -O1 -g -fno-omit-frame-pointer \ + -fsanitize=fuzzer-no-link,address,undefined +FUZZ_LDFLAGS = -fsanitize=fuzzer,address,undefined LIBS = -L$(SECP_LIB) -lsecp256k1 \ -L$(RANDOMX_DIR)/build -lrandomx \ @@ -117,6 +121,9 @@ NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \ NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \ obj-node/main_gui.o obj-node/headless.o +FUZZ_OBJS = $(patsubst obj/%,obj-fuzz/%,$(NODE_OBJS)) \ + obj-fuzz/fuzz_net_message.o + obj-node/%.o: %.cpp $(HEADERS) @mkdir -p obj-node $(CXX) -c $(CXXFLAGS) -DBITFLASH_NO_GUI -o $@ $< @@ -129,8 +136,31 @@ bitflash-node: $(NODE_ONLY_OBJS) $(CXX) $(CXXFLAGS) -o $@ $^ $(NODE_LIBS) @echo "Built: bitflash-node (headless, no libGL needed)" +obj-fuzz/%.o: %.cpp $(HEADERS) + @mkdir -p obj-fuzz + $(FUZZ_CXX) -c $(FUZZ_CXXFLAGS) -DBITFLASH_FUZZ -o $@ $< + +obj-fuzz/sha.o: sha.cpp sha.h + @mkdir -p obj-fuzz + $(FUZZ_CXX) -c $(FUZZ_CXXFLAGS) -DBITFLASH_FUZZ -O1 -o $@ $< + +obj-fuzz/fuzz_net_message.o: fuzz_net_message.cpp $(HEADERS) + @mkdir -p obj-fuzz + $(FUZZ_CXX) -c $(FUZZ_CXXFLAGS) -DBITFLASH_FUZZ -o $@ $< + +fuzz-net-message: $(FUZZ_OBJS) + $(FUZZ_CXX) $(filter-out -fsanitize=fuzzer-no-link%,$(FUZZ_CXXFLAGS)) \ + $(FUZZ_LDFLAGS) -o $@ $^ $(NODE_LIBS) + @echo "Built: fuzz-net-message" + +fuzz-net-message-smoke: fuzz-net-message + @mkdir -p ../work/fuzz-corpus/net-message + @printf '\277\040\134\375ping\000\000\000\000\000\000\000\000\000\000\000\000' > ../work/fuzz-corpus/net-message/ping-header + @printf '\277\040\134\375version\000\000\000\000\000\000\000\000\000' > ../work/fuzz-corpus/net-message/version-header + ./fuzz-net-message -runs=128 ../work/fuzz-corpus/net-message + clean: - rm -rf obj obj-node bitflash bitflash-node test_bip32 + rm -rf obj obj-node obj-fuzz bitflash bitflash-node fuzz-net-message test_bip32 test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h $(CXX) $(CXXFLAGS) -o $@ test_bip32.cpp bip32.cpp -lssl -lcrypto @@ -144,3 +174,5 @@ tests: test_bip32 bitflash-node ./bitflash-node -selftest=wallet-encrypt ./bitflash-node -selftest=net-message ./bitflash-node -selftest=consensus-limits + +.PHONY: tests fuzz-net-message-smoke 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 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// libFuzzer harness for the P2P message envelope parser. It feeds arbitrary +// receive-buffer bytes into ProcessMessages() through a dummy CNode, without +// opening sockets or starting node threads. + +#include "headers_core.h" + +#include +#include + +// Stand-ins normally supplied by the GUI/headless entry objects. +map mapAddressBook; +bool gPoolServerRunning = false; +void MainFrameRepaint() {} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + // Keep this first target focused on parser behavior, not multi-megabyte + // allocation pressure. The protocol size guards have their own selftest. + if (size > 4096) + return 0; + + CNode node(INVALID_SOCKET, CAddress("127.0.0.1")); + node.nVersion = 0; // exercise pre-version gating plus the version parser + if (size > 0) + node.vRecv.write((const char*)data, (int)size); + + ProcessMessages(&node); + return 0; +} -- cgit v1.2.3 From 4de5cca7e7a0a7104dae8d414add33f6e9562e4a Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 7 Aug 2026 10:55:54 -0300 Subject: fuzz: add script parser harness (#5) --- .github/workflows/ci.yml | 3 +++ Makefile | 5 +++- src/Makefile | 27 ++++++++++++++++++---- src/fuzz_script.cpp | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 src/fuzz_script.cpp (limited to 'src/Makefile') diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9efd66f..ba8074c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,9 @@ jobs: - name: Run P2P message fuzz smoke run: make fuzz-net-message-smoke + - name: Run script fuzz smoke + run: make fuzz-script-smoke + windows-ucrt64: name: Windows UCRT64 selftests runs-on: windows-latest diff --git a/Makefile b/Makefile index 736f5d8..d0f1b26 100644 --- a/Makefile +++ b/Makefile @@ -194,6 +194,9 @@ tests: fuzz-net-message-smoke: deps-linux $(MAKE) -C src -f Makefile fuzz-net-message-smoke +fuzz-script-smoke: deps-linux + $(MAKE) -C src -f Makefile fuzz-script-smoke + checksums: ./scripts/make-release-checksums.sh @@ -204,5 +207,5 @@ verify-release: ./scripts/verify-release.sh $(if $(TAG),$(TAG),latest) .PHONY: linux windows clean appimage \ - tests fuzz-net-message-smoke checksums sign-checksums verify-release \ + tests fuzz-net-message-smoke fuzz-script-smoke checksums sign-checksums verify-release \ deps-linux deps-windows deps-apt deps-secp256k1 deps-randomx diff --git a/src/Makefile b/src/Makefile index ac0226e..cc52fd6 100644 --- a/src/Makefile +++ b/src/Makefile @@ -121,8 +121,9 @@ NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \ NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \ obj-node/main_gui.o obj-node/headless.o -FUZZ_OBJS = $(patsubst obj/%,obj-fuzz/%,$(NODE_OBJS)) \ - obj-fuzz/fuzz_net_message.o +FUZZ_BASE_OBJS = $(patsubst obj/%,obj-fuzz/%,$(NODE_OBJS)) +FUZZ_NET_MESSAGE_OBJS = $(FUZZ_BASE_OBJS) obj-fuzz/fuzz_net_message.o +FUZZ_SCRIPT_OBJS = $(FUZZ_BASE_OBJS) obj-fuzz/fuzz_script.o obj-node/%.o: %.cpp $(HEADERS) @mkdir -p obj-node @@ -148,19 +149,35 @@ obj-fuzz/fuzz_net_message.o: fuzz_net_message.cpp $(HEADERS) @mkdir -p obj-fuzz $(FUZZ_CXX) -c $(FUZZ_CXXFLAGS) -DBITFLASH_FUZZ -o $@ $< -fuzz-net-message: $(FUZZ_OBJS) +obj-fuzz/fuzz_script.o: fuzz_script.cpp $(HEADERS) + @mkdir -p obj-fuzz + $(FUZZ_CXX) -c $(FUZZ_CXXFLAGS) -DBITFLASH_FUZZ -o $@ $< + +fuzz-net-message: $(FUZZ_NET_MESSAGE_OBJS) $(FUZZ_CXX) $(filter-out -fsanitize=fuzzer-no-link%,$(FUZZ_CXXFLAGS)) \ $(FUZZ_LDFLAGS) -o $@ $^ $(NODE_LIBS) @echo "Built: fuzz-net-message" +fuzz-script: $(FUZZ_SCRIPT_OBJS) + $(FUZZ_CXX) $(filter-out -fsanitize=fuzzer-no-link%,$(FUZZ_CXXFLAGS)) \ + $(FUZZ_LDFLAGS) -o $@ $^ $(NODE_LIBS) + @echo "Built: fuzz-script" + fuzz-net-message-smoke: fuzz-net-message @mkdir -p ../work/fuzz-corpus/net-message @printf '\277\040\134\375ping\000\000\000\000\000\000\000\000\000\000\000\000' > ../work/fuzz-corpus/net-message/ping-header @printf '\277\040\134\375version\000\000\000\000\000\000\000\000\000' > ../work/fuzz-corpus/net-message/version-header ./fuzz-net-message -runs=128 ../work/fuzz-corpus/net-message +fuzz-script-smoke: fuzz-script + @mkdir -p ../work/fuzz-corpus/script + @printf '\000\121' > ../work/fuzz-corpus/script/op-true + @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 + @printf '\000\115\377\377' > ../work/fuzz-corpus/script/truncated-pushdata2 + ./fuzz-script -runs=128 ../work/fuzz-corpus/script + clean: - rm -rf obj obj-node obj-fuzz bitflash bitflash-node fuzz-net-message test_bip32 + rm -rf obj obj-node obj-fuzz bitflash bitflash-node fuzz-net-message fuzz-script test_bip32 test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h $(CXX) $(CXXFLAGS) -o $@ test_bip32.cpp bip32.cpp -lssl -lcrypto @@ -175,4 +192,4 @@ tests: test_bip32 bitflash-node ./bitflash-node -selftest=net-message ./bitflash-node -selftest=consensus-limits -.PHONY: tests fuzz-net-message-smoke +.PHONY: tests fuzz-net-message-smoke fuzz-script-smoke 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 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// libFuzzer harness for raw script parsing and evaluation. It keeps the +// target deliberately small: arbitrary bytes become one CScript, then the +// parser, standard-script extractors and EvalScript all see that same input. + +#include "headers_core.h" + +#include +#include + +// Stand-ins normally supplied by the GUI/headless entry objects. +map mapAddressBook; +bool gPoolServerRunning = false; +void MainFrameRepaint() {} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + if (size < 1 || size > 4096) + return 0; + + try + { + int nHashType = data[0]; + const unsigned char* pbegin = data + 1; + const unsigned char* pend = data + size; + CScript script(pbegin, pend); + + CScript::const_iterator pc = script.begin(); + opcodetype opcode; + vector vchPush; + while (pc < script.end()) + { + if (!script.GetOp(pc, opcode, vchPush)) + break; + } + + script.GetSigOpCount(); + script.ToString(); + + vector vchPubKey; + ExtractPubKey(script, false, vchPubKey); + uint160 hash160; + ExtractHash160(script, hash160); + + CTransaction tx; + tx.vin.push_back(CTxIn(COutPoint(), CScript())); + tx.vout.push_back(CTxOut(0, CScript())); + + vector > stack; + EvalScript(script, tx, 0, nHashType, &stack); + } + catch (...) + { + } + + return 0; +} -- cgit v1.2.3 From 8c2e375600c0eb9bedb8f85ee4fa860825f23521 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 7 Aug 2026 11:40:46 -0300 Subject: test: cover pool Stratum share accounting --- src/Makefile | 1 + src/makefile.mingw | 1 + src/rpc.cpp | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/selftest.cpp | 6 +++- 4 files changed, 92 insertions(+), 1 deletion(-) (limited to 'src/Makefile') diff --git a/src/Makefile b/src/Makefile index cc52fd6..bc15ef8 100644 --- a/src/Makefile +++ b/src/Makefile @@ -191,5 +191,6 @@ tests: test_bip32 bitflash-node ./bitflash-node -selftest=wallet-encrypt ./bitflash-node -selftest=net-message ./bitflash-node -selftest=consensus-limits + ./bitflash-node -selftest=pool-stratum .PHONY: tests fuzz-net-message-smoke fuzz-script-smoke diff --git a/src/makefile.mingw b/src/makefile.mingw index e076c49..b946e50 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -116,3 +116,4 @@ tests: test_bip32.exe bitflash.exe ./bitflash.exe -selftest=wallet-encrypt ./bitflash.exe -selftest=net-message ./bitflash.exe -selftest=consensus-limits + ./bitflash.exe -selftest=pool-stratum diff --git a/src/rpc.cpp b/src/rpc.cpp index 8f3740d..383769a 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -1180,6 +1180,91 @@ static bool HandleLine(Miner* m, const std::string& rawLine, return true; } +int RunPoolStratumSelfTest() +{ + int nFail = 0; + auto check = [&](bool ok, const char* msg) { + fprintf(stdout, " %s %s\n", ok ? "ok" : "FAIL", msg); + if (!ok) nFail++; + }; + + fprintf(stdout, "pool-stratum self-test\n"); + + StratumJob savedJob = gCurrentJob; + bool savedHaveJob = gHaveJob; + + CTransaction coinbase; + coinbase.vin.resize(1); + coinbase.vin[0].prevout.SetNull(); + coinbase.vin[0].scriptSig << 1 << (CBigNum)1; + coinbase.vout.resize(1); + coinbase.vout[0].nValue = 50 * COIN; + + CBlock block; + block.vtx.push_back(coinbase); + block.nVersion = 1; + block.nBits = 0x1f0fffff; + block.nTime = 1; + block.nNonce = 1; + block.hashMerkleRoot = block.BuildMerkleTree(); + + gCurrentJob.jobId = "selftest"; + gCurrentJob.block = block; + gCurrentJob.target = 0; // accepted share, not a block candidate + gCurrentJob.height = 1; + gHaveJob = true; + + Miner miner(INVALID_SOCKET); + miner.authorised = true; + miner.address = "selftest-address"; + miner.worker = "worker"; + miner.shareTarget = ~uint256(0); // every RandomX hash is a valid share + miner.difficulty = 0.001; + miner.vardiffWindowStart = GetTime(); + + std::map roundShareCount; + uint64 roundShareTotal = 0; + int blocksFound = 0; + + uint256 targetFromNotify; + CBlock minerBlock = BuildMinerBlock(gCurrentJob, miner.extranonce2); + json params = MakeNotifyParams(gCurrentJob, minerBlock, miner.shareTarget, true); + std::vector targetBytes = FromHex(params[3].get()); + if (targetBytes.size() == 32) + memcpy(&targetFromNotify, &targetBytes[0], 32); + check(targetFromNotify == miner.shareTarget, + "mining.notify exposes the same share target the pool validates"); + + json submit = { + {"id", 7}, + {"method", "mining.submit"}, + {"params", json::array({miner.address, "selftest", ToHex(&minerBlock.nNonce, 4)})} + }; + HandleLine(&miner, submit.dump(), roundShareCount, roundShareTotal, blocksFound); + + check(roundShareTotal == 1, "fake low-difficulty share increments the round total"); + check(roundShareCount[miner.address] == 1, "fake share is credited to the submitting address"); + check(miner.sessionShares == 1 && miner.roundShares == 1 && miner.totalShares == 1, + "miner share counters advance together"); + check(blocksFound == 0, "share below block target is not counted as a found block"); + + json stale = { + {"id", 8}, + {"method", "mining.submit"}, + {"params", json::array({miner.address, "stale", ToHex(&minerBlock.nNonce, 4)})} + }; + HandleLine(&miner, stale.dump(), roundShareCount, roundShareTotal, blocksFound); + check(roundShareTotal == 1, "stale submit does not increment shares"); + + gCurrentJob = savedJob; + gHaveJob = savedHaveJob; + + fprintf(stdout, "%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + // --------------------------------------------------------------------------- // Stats refresh -- called every 2s from event loop // --------------------------------------------------------------------------- diff --git a/src/selftest.cpp b/src/selftest.cpp index ff0289b..fadc959 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -10,6 +10,8 @@ #include "selftest.h" #include "walletcmd.h" +extern int RunPoolStratumSelfTest(); + // Test results go to the terminal, not to debug.log. // // printf in this tree is OutputDebugStringF, which writes into the data @@ -1234,8 +1236,10 @@ int RunSelfTest(const std::string& name) return RunNetMessageSelfTest(); if (name == "consensus-limits") return RunConsensusLimitsSelfTest(); + if (name == "pool-stratum") + return RunPoolStratumSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits\n"); + printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum\n"); return 1; } -- cgit v1.2.3 From faf6c4d24304b770c2d43515da04832b2124c92a Mon Sep 17 00:00:00 2001 From: Bitflash Date: Tue, 11 Aug 2026 10:13:04 -0300 Subject: money: ParseMoney accepts full 8 decimals (0.5, 0.00000001) The legacy parser only accepted two decimal places (cents) and rejected 0.5, leaving six of the eight coin decimals unreachable by -sendto and the send window. Parse up to 8 fractional digits Bitcoin-style, reject >8 to avoid silent truncation. Adds the parse-money selftest (12 cases). --- src/Makefile | 1 + src/selftest.cpp | 37 +++++++++++++++++++++++++++++++++++++ src/util.cpp | 23 +++++++++++++---------- 3 files changed, 51 insertions(+), 10 deletions(-) (limited to 'src/Makefile') diff --git a/src/Makefile b/src/Makefile index bc15ef8..00a8182 100644 --- a/src/Makefile +++ b/src/Makefile @@ -192,5 +192,6 @@ tests: test_bip32 bitflash-node ./bitflash-node -selftest=net-message ./bitflash-node -selftest=consensus-limits ./bitflash-node -selftest=pool-stratum + ./bitflash-node -selftest=parse-money .PHONY: tests fuzz-net-message-smoke fuzz-script-smoke diff --git a/src/selftest.cpp b/src/selftest.cpp index 69a3596..dcb9192 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -1218,6 +1218,41 @@ static int RunConsensusLimitsSelfTest() return nFail == 0 ? 0 : 1; } +static int RunParseMoneySelfTest() +{ + printf("parse-money self-test\n"); + int nFail = 0; + struct Case { const char* in; bool ok; int64 val; }; + Case cases[] = { + {"0.5", true, 50000000LL}, + {"1", true, 100000000LL}, + {"1.23456789", true, 123456789LL}, + {"0.00000001", true, 1LL}, + {"10.00000000", true, 1000000000LL}, + {"1,000", true, 100000000000LL}, + {"1.5 ", true, 150000000LL}, + {"0.123456789", false, 0LL}, + {"1.2.3", false, 0LL}, + {"abc", false, 0LL}, + {"-1", false, 0LL}, + {"1e5", false, 0LL}, + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) + { + int64 v = -1; + bool r = ParseMoney(cases[i].in, v); + bool pass = (r == cases[i].ok) && (!r || v == cases[i].val); + char desc[160]; + snprintf(desc, sizeof(desc), "ParseMoney(\"%s\") %s and value matches", + cases[i].in, cases[i].ok ? "accepts" : "rejects"); + nFail += Check(pass, desc) ? 0 : 1; + } + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + int RunSelfTest(const std::string& name) { AttachTerminal(); @@ -1238,6 +1273,8 @@ int RunSelfTest(const std::string& name) return RunConsensusLimitsSelfTest(); if (name == "pool-stratum") return RunPoolStratumSelfTest(); + if (name == "parse-money") + return RunParseMoneySelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum\n"); 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) bool ParseMoney(const char* pszIn, int64& nRet) { string strWhole; - int64 nCents = 0; + int64 nUnits = 0; // fractional part, scaled to COIN (8 decimals) const char* p = pszIn; while (isspace(*p)) p++; @@ -348,12 +348,16 @@ bool ParseMoney(const char* pszIn, int64& nRet) if (*p == '.') { p++; - if (!isdigit(p[0]) || !isdigit(p[1])) + int64 nMult = COIN / 10; // weight of the first decimal digit + while (isdigit(*p) && nMult > 0) + { + nUnits += nMult * (*p - '0'); + nMult /= 10; + p++; + } + // reject more than 8 fractional digits rather than silently truncate + if (isdigit(*p)) return false; - nCents = atoi64(p); - if (nCents < 0 || nCents > 99) - return false; - p += 2; break; } if (isspace(*p)) @@ -365,13 +369,12 @@ bool ParseMoney(const char* pszIn, int64& nRet) for (; *p; p++) if (!isspace(*p)) return false; - if (strWhole.size() > 17) + if (strWhole.size() > 10) // 10 whole digits * COIN stays within int64 return false; int64 nWhole = atoi64(strWhole); - int64 nValue = nWhole * 100 + nCents; - if (nValue / 100 != nWhole) + int64 nValue = nWhole * COIN + nUnits; + if (nValue < 0 || (nValue - nUnits) / COIN != nWhole) return false; - nValue *= CENT; nRet = nValue; return true; } -- cgit v1.2.3 From ff3829b28fd2ddc85a9a3bb4424cd5b4719854ab Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Tue, 11 Aug 2026 15:38:31 -0300 Subject: net: add optional SOCKS5 outbound proxy --- README.md | 6 ++ src/Makefile | 7 +- src/btfrv.cpp | 9 ++ src/main_gui.cpp | 16 +++- src/makefile.mingw | 7 +- src/net.cpp | 4 + src/nostr.cpp | 50 +---------- src/proxy.cpp | 254 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/proxy.h | 31 +++++++ src/readme.txt | 3 + src/selftest.cpp | 42 ++++++++- 11 files changed, 374 insertions(+), 55 deletions(-) create mode 100644 src/proxy.cpp create mode 100644 src/proxy.h (limited to 'src/Makefile') diff --git a/README.md b/README.md index c28b0f1..1b68ccd 100644 --- a/README.md +++ b/README.md @@ -310,6 +310,7 @@ Other options worth knowing: ```bash -datadir=PATH # wallet and chain data elsewhere -port=N # P2P listen port, default 8433 +-socks=HOST:PORT # SOCKS5 for outbound Nostr and .btf rendezvous dials -debug # verbose log; without it debug.log is nearly silent -help # full list ``` @@ -318,6 +319,11 @@ Other options worth knowing: needed — the data directory takes an exclusive lock, so a second node pointed at the same one will refuse to start. +`-socks=127.0.0.1:9050` routes outbound Nostr discovery and `.btf` rendezvous +dials through a local SOCKS5 proxy such as Tor. When it is set, the node skips +plain HTTP external-IP probes instead of leaking a direct request outside the +proxy. + ### When something looks wrong **Menu > Diagnostics**, and the same report in `debug.log` every ten minutes: diff --git a/src/Makefile b/src/Makefile index 00a8182..511fe79 100644 --- a/src/Makefile +++ b/src/Makefile @@ -30,7 +30,7 @@ WARNFLAGS = -Wformat -Werror=format \ -Wno-narrowing -Wno-unused-result CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ - -DSECP256K1_STATIC \ + -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD \ -I$(SECP_INC) -I$(RANDOMX_DIR)/src \ -I/usr/include/nlohmann \ -I$(IMGUI_DIR) @@ -53,7 +53,7 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \ HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ - selftest.h walletcmd.h compat.h sockcount.h font_roboto.h + selftest.h walletcmd.h compat.h sockcount.h proxy.h font_roboto.h IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o @@ -61,7 +61,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ - obj/selftest.o obj/bip32.o obj/walletcmd.o + obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o GUI_OBJS = obj/gui.o obj/main_gui.o @@ -193,5 +193,6 @@ tests: test_bip32 bitflash-node ./bitflash-node -selftest=consensus-limits ./bitflash-node -selftest=pool-stratum ./bitflash-node -selftest=parse-money + ./bitflash-node -selftest=socks5-proxy .PHONY: tests fuzz-net-message-smoke fuzz-script-smoke diff --git a/src/btfrv.cpp b/src/btfrv.cpp index 6ac3280..d4f079f 100644 --- a/src/btfrv.cpp +++ b/src/btfrv.cpp @@ -5,6 +5,9 @@ // Windows/Winsock implementation (Layer 6 will #ifdef this for Linux/BSD sockets). #include "btfrv.h" +#ifdef BITFLASH_NODE_BUILD +#include "proxy.h" +#endif // Portable sockets: Winsock on Windows, BSD sockets on Linux/POSIX (for the // headless relay `bitflashd` that runs on the VPS). @@ -92,6 +95,11 @@ static bool WriteN(SOCKET s, const void* buf, int n) static SOCKET ConnectTo(const char* host, unsigned short port) { +#ifdef BITFLASH_NODE_BUILD + SOCKET s = BtfConnectSocket(host, port, SOCK_RV_DIAL, 10); + if (s == INVALID_SOCKET) + return INVALID_SOCKET; +#else struct addrinfo hints, *res = NULL; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_INET; @@ -119,6 +127,7 @@ static SOCKET ConnectTo(const char* host, unsigned short port) CLOSESOCK(s); freeaddrinfo(res); return INVALID_SOCKET; } freeaddrinfo(res); +#endif // Clear the timeout -- the socket is now a live tunnel and must not // time out when idle. recv() will block until data arrives or peer closes. diff --git a/src/main_gui.cpp b/src/main_gui.cpp index b036ac9..90a6050 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -1,6 +1,7 @@ // Bitflash entry point -- starts node threads then runs GUI (or headless). #include "headers_core.h" +#include "proxy.h" #include "selftest.h" #include "walletcmd.h" #include // hardware_concurrency, to sanity-check /genproclimit @@ -110,7 +111,8 @@ static void PrintUsage() printf(" /debug\n"); printf(" /gen\n"); printf(" /nogui or /daemon\n"); - printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, or wallet-encrypt\n"); + printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt,\n"); + printf(" net-message, consensus-limits, pool-stratum, parse-money, or socks5-proxy\n"); printf("\n"); printf("Mining mode:\n"); printf(" /operator\n"); @@ -135,6 +137,7 @@ static void PrintUsage() printf("\n"); printf("Network:\n"); printf(" /port=N (P2P listen port, default 8433)\n"); + printf(" /socks=HOST:PORT (SOCKS5 proxy for outbound Nostr and .btf relay dials)\n"); printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n"); printf("\n"); printf("Wallet:\n"); @@ -282,6 +285,17 @@ static void ParseStartupArguments(int argc, char* argv[]) if (!announceRelay.empty()) strBtfAnnounceRelay = announceRelay; + string socksProxy = argval2(argc, argv, "/socks", "-socks"); + if (!socksProxy.empty()) + { + string err; + if (!BtfSetSocks5Proxy(socksProxy, err)) + fprintf(stderr, "Ignoring /socks=%s: %s\n", socksProxy.c_str(), err.c_str()); + else + fprintf(stderr, "SOCKS5 proxy enabled for outbound discovery/relay dials: %s\n", + BtfSocks5ProxyName().c_str()); + } + // /btfseed=ADDRESS:ENCHEX -- extra bootstrap peers, repeatable. Useful for // testing the seed path and for private networks that ship no compiled list. for (int i = 1; i < argc; i++) diff --git a/src/makefile.mingw b/src/makefile.mingw index b946e50..28e925c 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -21,7 +21,7 @@ WARNFLAGS = -Wformat -Werror=format \ # 64, which capped this node at 63 watchable peers no matter how many it had. CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ -DWIN32 -DNOPCH -DFD_SETSIZE=1024 \ - -DSECP256K1_STATIC \ + -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD \ -I$(DEPS)/include -I$(RANDOMX)/src \ -I$(DEPS)/include/nlohmann \ -IC:/msys64/ucrt64/include \ @@ -42,7 +42,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ - obj/selftest.o obj/bip32.o obj/walletcmd.o + obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o @@ -56,7 +56,7 @@ GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ - selftest.h walletcmd.h sockcount.h compat.h font_roboto.h + selftest.h walletcmd.h sockcount.h proxy.h compat.h font_roboto.h all: bitflash.exe @@ -117,3 +117,4 @@ tests: test_bip32.exe bitflash.exe ./bitflash.exe -selftest=net-message ./bitflash.exe -selftest=consensus-limits ./bitflash.exe -selftest=pool-stratum + ./bitflash.exe -selftest=socks5-proxy diff --git a/src/net.cpp b/src/net.cpp index b9bb95d..fe9964e 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -17,6 +17,7 @@ #include #include "btfaddr.h" #include "btftunnel.h" +#include "proxy.h" void ThreadReconnectCachedBtfPeers(void* parg); void ThreadMessageHandler2(void* parg); @@ -361,6 +362,9 @@ string GetDiagnosticsText() bool GetMyExternalIP(unsigned int& ipRet) { + if (BtfSocks5ProxyEnabled()) + return error("GetMyExternalIP() skipped while SOCKS5 proxy is enabled\n"); + // Try several plain-text IP echo services in order. // Each returns just the IPv4 address as the first line of the HTTP body. struct { const char* host; const char* path; } services[] = { diff --git a/src/nostr.cpp b/src/nostr.cpp index 9bc538f..d286767 100644 --- a/src/nostr.cpp +++ b/src/nostr.cpp @@ -29,6 +29,7 @@ #include "btfaddr.h" #include "btfchan.h" +#include "proxy.h" using json = nlohmann::json; @@ -540,53 +541,8 @@ public: size_t colon = host.find(':'); if (colon != string::npos) { port = atoi(host.substr(colon + 1).c_str()); host = host.substr(0, colon); } - // Resolve and connect (TCP). AF_UNSPEC so we get both A and AAAA - // records back and try them in the order the resolver prefers, - // rather than hard-failing whenever the IPv4 path to a relay is - // filtered/rate-limited (increasingly common for CDN-fronted - // relays) while a working IPv6 route exists right there in the - // same getaddrinfo() result. - struct addrinfo hints, *res = NULL, *rp = NULL; - memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - char portstr[16]; sprintf(portstr, "%d", port); - if (getaddrinfo(host.c_str(), portstr, &hints, &res) != 0 || !res) - return error("Nostr: getaddrinfo %s failed", host.c_str()); - - bool fConnected = false; - for (rp = res; rp != NULL; rp = rp->ai_next) - { - hSocket = BtfSocketTag(socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol), SOCK_NOSTR); - if (hSocket == INVALID_SOCKET) - continue; - - // Socket receive/send timeout: Windows takes a DWORD of - // milliseconds; POSIX takes a struct timeval. -#ifdef _WIN32 - DWORD tv = timeoutSec * 1000; - setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); - setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)); -#else - struct timeval tv; - tv.tv_sec = timeoutSec; - tv.tv_usec = 0; - setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); - setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)); -#endif - - if (connect(hSocket, rp->ai_addr, (int)rp->ai_addrlen) == 0) - { - fConnected = true; - break; - } - - BtfCloseSocket(hSocket); - hSocket = INVALID_SOCKET; - } - freeaddrinfo(res); - - if (!fConnected) + hSocket = BtfConnectSocket(host, (unsigned short)port, SOCK_NOSTR, timeoutSec); + if (hSocket == INVALID_SOCKET) return error("Nostr: connect %s:%d failed", host.c_str(), port); // TLS diff --git a/src/proxy.cpp b/src/proxy.cpp new file mode 100644 index 0000000..1e62ff9 --- /dev/null +++ b/src/proxy.cpp @@ -0,0 +1,254 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. + +#include "headers_core.h" +#include "proxy.h" +#include "sockcount.h" + +static CCriticalSection cs_socks5; +static bool g_fSocks5Proxy = false; +static std::string g_socks5Host; +static unsigned short g_socks5Port = 0; + +static void SetSocketTimeout(SOCKET s, int nTimeoutSecs) +{ + if (nTimeoutSecs <= 0) + return; +#ifdef _WIN32 + DWORD tv = (DWORD)nTimeoutSecs * 1000; + setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)); + setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv)); +#else + struct timeval tv; + tv.tv_sec = nTimeoutSecs; + tv.tv_usec = 0; + setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); +#endif +} + +static bool ReadN(SOCKET s, void* buf, int n) +{ + char* p = (char*)buf; + int off = 0; + while (off < n) + { + int r = recv(s, p + off, n - off, 0); + if (r <= 0) + return false; + off += r; + } + return true; +} + +static bool WriteN(SOCKET s, const void* buf, int n) +{ + const char* p = (const char*)buf; + int off = 0; + while (off < n) + { + int r = send(s, p + off, n - off, 0); + if (r <= 0) + return false; + off += r; + } + return true; +} + +static bool ValidProxyHostChar(char c) +{ + return isalnum((unsigned char)c) || c == '.' || c == '-' || c == '_'; +} + +bool BtfParseSocks5Proxy(const std::string& spec, std::string& hostOut, + unsigned short& portOut, std::string& errOut) +{ + hostOut.clear(); + portOut = 0; + errOut.clear(); + + size_t colon = spec.rfind(':'); + if (colon == std::string::npos || colon == 0 || colon + 1 >= spec.size()) + { + errOut = "expected HOST:PORT"; + return false; + } + + std::string host = spec.substr(0, colon); + std::string port = spec.substr(colon + 1); + if (host.size() > 255) + { + errOut = "host is too long"; + return false; + } + for (size_t i = 0; i < host.size(); i++) + { + if (!ValidProxyHostChar(host[i])) + { + errOut = "host contains unsupported characters"; + return false; + } + } + for (size_t i = 0; i < port.size(); i++) + { + if (!isdigit((unsigned char)port[i])) + { + errOut = "port is not numeric"; + return false; + } + } + int nPort = atoi(port.c_str()); + if (nPort <= 0 || nPort > 65535) + { + errOut = "port is out of range"; + return false; + } + + hostOut = host; + portOut = (unsigned short)nPort; + return true; +} + +bool BtfSetSocks5Proxy(const std::string& spec, std::string& errOut) +{ + std::string host; + unsigned short port = 0; + if (!BtfParseSocks5Proxy(spec, host, port, errOut)) + return false; + CRITICAL_BLOCK(cs_socks5) + { + g_socks5Host = host; + g_socks5Port = port; + g_fSocks5Proxy = true; + } + return true; +} + +void BtfClearSocks5Proxy() +{ + CRITICAL_BLOCK(cs_socks5) + { + g_fSocks5Proxy = false; + g_socks5Host.clear(); + g_socks5Port = 0; + } +} + +bool BtfSocks5ProxyEnabled() +{ + CRITICAL_BLOCK(cs_socks5) + return g_fSocks5Proxy; + return false; +} + +std::string BtfSocks5ProxyName() +{ + CRITICAL_BLOCK(cs_socks5) + { + if (!g_fSocks5Proxy) + return ""; + return strprintf("%s:%u", g_socks5Host.c_str(), (unsigned)g_socks5Port); + } + return ""; +} + +static SOCKET ConnectDirect(const std::string& host, unsigned short port, + int nSockSite, int nTimeoutSecs) +{ + struct addrinfo hints, *res = NULL, *rp = NULL; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + char portstr[16]; + sprintf(portstr, "%u", (unsigned)port); + if (getaddrinfo(host.c_str(), portstr, &hints, &res) != 0 || !res) + return INVALID_SOCKET; + + SOCKET s = INVALID_SOCKET; + for (rp = res; rp != NULL; rp = rp->ai_next) + { + s = BtfSocketTag(socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol), nSockSite); + if (s == INVALID_SOCKET) + continue; + SetSocketTimeout(s, nTimeoutSecs); + if (connect(s, rp->ai_addr, (int)rp->ai_addrlen) == 0) + break; + BtfCloseSocket(s); + s = INVALID_SOCKET; + } + freeaddrinfo(res); + return s; +} + +static bool Socks5Connect(SOCKET s, const std::string& destHost, + unsigned short destPort) +{ + if (destHost.empty() || destHost.size() > 255) + return false; + + unsigned char hello[3] = { 0x05, 0x01, 0x00 }; // SOCKS5, one method, no auth + if (!WriteN(s, hello, sizeof(hello))) + return false; + unsigned char choice[2] = { 0, 0 }; + if (!ReadN(s, choice, sizeof(choice)) || choice[0] != 0x05 || choice[1] != 0x00) + return false; + + std::vector req; + req.reserve(7 + destHost.size()); + req.push_back(0x05); // version + req.push_back(0x01); // CONNECT + req.push_back(0x00); // reserved + req.push_back(0x03); // domain name; proxy resolves, avoiding local DNS leak + req.push_back((unsigned char)destHost.size()); + req.insert(req.end(), destHost.begin(), destHost.end()); + req.push_back((unsigned char)(destPort >> 8)); + req.push_back((unsigned char)(destPort & 0xff)); + if (!WriteN(s, &req[0], (int)req.size())) + return false; + + unsigned char rep[4] = {0,0,0,0}; + if (!ReadN(s, rep, sizeof(rep)) || rep[0] != 0x05 || rep[1] != 0x00) + return false; + int nAddr = 0; + if (rep[3] == 0x01) nAddr = 4; + else if (rep[3] == 0x04) nAddr = 16; + else if (rep[3] == 0x03) + { + unsigned char len = 0; + if (!ReadN(s, &len, 1)) + return false; + nAddr = len; + } + else + return false; + + std::vector discard(nAddr + 2); + return ReadN(s, &discard[0], (int)discard.size()); +} + +SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort, + int nSockSite, int nTimeoutSecs) +{ + std::string proxyHost; + unsigned short proxyPort = 0; + bool fProxy = false; + CRITICAL_BLOCK(cs_socks5) + { + fProxy = g_fSocks5Proxy; + proxyHost = g_socks5Host; + proxyPort = g_socks5Port; + } + + if (!fProxy) + return ConnectDirect(destHost, destPort, nSockSite, nTimeoutSecs); + + SOCKET s = ConnectDirect(proxyHost, proxyPort, nSockSite, nTimeoutSecs); + if (s == INVALID_SOCKET) + return INVALID_SOCKET; + if (!Socks5Connect(s, destHost, destPort)) + { + BtfCloseSocket(s); + return INVALID_SOCKET; + } + return s; +} diff --git a/src/proxy.h b/src/proxy.h new file mode 100644 index 0000000..b5e69c8 --- /dev/null +++ b/src/proxy.h @@ -0,0 +1,31 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// Optional SOCKS5 outbound proxy support. This is transport plumbing only: +// no consensus, wallet, mining, or .btf identity data depends on it. + +#ifndef BITFLASH_PROXY_H +#define BITFLASH_PROXY_H + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN 1 +#endif +#include +#else +#include "compat.h" +#endif + +#include + +bool BtfParseSocks5Proxy(const std::string& spec, std::string& hostOut, + unsigned short& portOut, std::string& errOut); +bool BtfSetSocks5Proxy(const std::string& spec, std::string& errOut); +void BtfClearSocks5Proxy(); +bool BtfSocks5ProxyEnabled(); +std::string BtfSocks5ProxyName(); + +SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort, + int nSockSite, int nTimeoutSecs); + +#endif diff --git a/src/readme.txt b/src/readme.txt index b8b89c1..96f32c3 100644 --- a/src/readme.txt +++ b/src/readme.txt @@ -42,6 +42,9 @@ The resulting executable is src/bitflash.exe. Command-line options (in addition to the original ones) ------------------------------------------------------- /port=N listen on P2P port N (default 8433) + /socks=HOST:PORT + route outbound Nostr and .btf rendezvous dials through a + SOCKS5 proxy, e.g. /socks=127.0.0.1:9050 for local Tor /solomine mine without requiring a connected peer (local testing) /operator operator mode (run pool server) /participant=ADDR diff --git a/src/selftest.cpp b/src/selftest.cpp index dcb9192..5b36b9d 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -7,6 +7,7 @@ #include "headers_core.h" #include "bip32.h" +#include "proxy.h" #include "selftest.h" #include "walletcmd.h" @@ -1253,6 +1254,43 @@ static int RunParseMoneySelfTest() return nFail == 0 ? 0 : 1; } +static int RunSocks5ProxySelfTest() +{ + printf("socks5-proxy self-test\n"); + int nFail = 0; + std::string host, err; + unsigned short port = 0; + + nFail += Check(BtfParseSocks5Proxy("127.0.0.1:9050", host, port, err) && + host == "127.0.0.1" && port == 9050, + "parses local Tor proxy endpoint") ? 0 : 1; + nFail += Check(BtfParseSocks5Proxy("localhost:9050", host, port, err) && + host == "localhost" && port == 9050, + "parses hostname proxy endpoint") ? 0 : 1; + nFail += Check(!BtfParseSocks5Proxy("localhost", host, port, err), + "rejects missing port") ? 0 : 1; + nFail += Check(!BtfParseSocks5Proxy("localhost:0", host, port, err), + "rejects zero port") ? 0 : 1; + nFail += Check(!BtfParseSocks5Proxy("localhost:70000", host, port, err), + "rejects out-of-range port") ? 0 : 1; + nFail += Check(!BtfParseSocks5Proxy("user:pass@localhost:9050", host, port, err), + "rejects unsupported authenticated proxy syntax") ? 0 : 1; + + BtfClearSocks5Proxy(); + nFail += Check(!BtfSocks5ProxyEnabled(), "starts disabled after clear") ? 0 : 1; + nFail += Check(BtfSetSocks5Proxy("127.0.0.1:9050", err) && + BtfSocks5ProxyEnabled() && + BtfSocks5ProxyName() == "127.0.0.1:9050", + "enables parsed SOCKS5 proxy") ? 0 : 1; + BtfClearSocks5Proxy(); + nFail += Check(!BtfSocks5ProxyEnabled(), "clear disables proxy") ? 0 : 1; + + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + int RunSelfTest(const std::string& name) { AttachTerminal(); @@ -1275,8 +1313,10 @@ int RunSelfTest(const std::string& name) return RunPoolStratumSelfTest(); if (name == "parse-money") return RunParseMoneySelfTest(); + if (name == "socks5-proxy") + return RunSocks5ProxySelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum\n"); + 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"); return 1; } -- cgit v1.2.3 From 53cc17d621f55b8904812fae5df90ef089bf5aaf Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Thu, 13 Aug 2026 11:54:35 -0300 Subject: net: add managed Tor hidden service bootstrap --- README.md | 13 +- docs/rendezvous.md | 8 +- docs/tor.md | 52 +++++- src/Makefile | 5 +- src/main_gui.cpp | 65 ++++--- src/makefile.mingw | 5 +- src/net.cpp | 2 + src/readme.txt | 3 + src/selftest.cpp | 37 +++- src/tor.cpp | 492 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/tor.h | 22 +++ 11 files changed, 665 insertions(+), 39 deletions(-) create mode 100644 src/tor.cpp create mode 100644 src/tor.h (limited to 'src/Makefile') diff --git a/README.md b/README.md index 181b6e8..a7ce2b2 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,7 @@ Other options worth knowing: -port=N # P2P listen port, default 8433 -socks=HOST:PORT # SOCKS5 for outbound Nostr, .btf relay, and onion peer dials -tor[=HOST:PORT] # Tor mode; default local Tor SOCKS5 proxy is 127.0.0.1:9050 +-managedtor[=PATH] # start Tor, create a hidden service, advertise its onion -onionservice=HOST.onion:PORT # advertise this node's Tor hidden service -debug # verbose log; without it debug.log is nearly silent -help # full list @@ -357,10 +358,14 @@ proxy. IPv6 proxy endpoints use brackets, for example `-socks=[::1]:9050`. `-tor` is a privacy shorthand for the usual local Tor SOCKS5 listener at `127.0.0.1:9050`. It routes outbound Nostr discovery, `.btf` rendezvous dials, and signed direct `.onion` peer dials through Tor, and keeps external-IP probes -disabled. Use `-tor=HOST:PORT` when Tor listens somewhere else. To make your own -node reachable without a rendezvous relay, expose port `8433` as a Tor hidden -service and pass `-onionservice=HOST.onion:8433`; peers with `-tor` will try it -before falling back to rendezvous. +disabled. Use `-tor=HOST:PORT` when Tor listens somewhere else. + +`-managedtor[=PATH]` starts a Tor process for this node, writes a local `torrc`, +creates a v3 hidden service for port `8433`, routes outbound discovery through +that Tor instance, and signs the generated `.onion:8433` endpoint into the +node's `.btf` descriptor. Without `PATH`, Bitflash looks for `tor/tor.exe`, +`tor.exe`, or `tor` beside the binary and then on `PATH`. The hidden-service key +is stored under the Bitflash data directory in `managed-tor/onion-service/`. ### When something looks wrong diff --git a/docs/rendezvous.md b/docs/rendezvous.md index e1888b9..68b5df3 100644 --- a/docs/rendezvous.md +++ b/docs/rendezvous.md @@ -308,9 +308,11 @@ tunnel. unreachable, discovery stalls until one is reachable again. Relay TLS is verified, which closes a man in the middle path on discovery, but availability still rests on the relay set. -- **Direct onion is not automatic Tor configuration.** Bitflash signs and - advertises a hidden-service endpoint, and Tor-mode peers can dial it. The Tor - daemon still owns hidden-service creation, private keys, rotation and uptime. +- **Direct onion needs Tor.** With `-managedtor`, Bitflash starts a Tor process, + writes a local hidden-service config, and advertises the generated onion + endpoint. With `-onionservice`, an operator can still run Tor manually and + provide the endpoint. In both cases Tor owns the hidden-service private key and + uptime. ## 11. Wire reference diff --git a/docs/tor.md b/docs/tor.md index b4f88f9..1c0f589 100644 --- a/docs/tor.md +++ b/docs/tor.md @@ -36,6 +36,47 @@ If both `-tor` and `-socks` are present, Tor mode wins and the node prints a warning. That keeps a command line with `-tor` from silently becoming a custom proxy route. +## Managed Tor mode + +`-managedtor` starts a Tor process for this node, writes a local `torrc`, creates +a v3 hidden service for the normal Bitflash P2P listener, routes outbound +discovery through that Tor instance, and signs the generated `.onion:8433` +endpoint into the node's `.btf` descriptor. + +```bash +bitflash -managedtor +``` + +Without a value, Bitflash looks for Tor in this order: + +1. `tor/tor.exe` or `tor/tor` beside the Bitflash binary; +2. `tor.exe` or `tor` beside the Bitflash binary; +3. `tor.exe` or `tor` on `PATH`. + +To point at a specific Tor executable: + +```bash +bitflash -managedtor=/opt/tor/bin/tor +bitflash -managedtor=C:\Tor Browser\Browser\TorBrowser\Tor\tor.exe +``` + +Managed Tor writes its state under the Bitflash data directory: + +```text +managed-tor/ + torrc + data/ + onion-service/ +``` + +The private key for the onion address lives in `managed-tor/onion-service/`. +Back up that directory if you want the same onion address after moving the node. +Delete it only if you intentionally want a fresh onion endpoint. + +`-managedtor` takes precedence over `-tor` and `-socks`, because it must choose +the local SOCKS port it starts. Diagnostics report the current managed Tor +state, including the SOCKS port and onion endpoint once Tor has generated it. + When Tor mode is enabled: - outbound Nostr relay dials use Tor; @@ -100,10 +141,10 @@ bitflash -operator -tor \ -announcerelay=exampleexampleexampleexampleexampleexampleexampleexample.onion:8434 ``` -## Direct full-node hidden service +## Manual direct full-node hidden service -The legacy P2P listener still binds to port `8433`. You can expose it through -Tor manually: +Managed Tor is the preferred path for ordinary nodes. Operators can still expose +the P2P listener through a separately managed Tor service: ```text HiddenServiceDir /var/lib/tor/bitflash-node/ @@ -139,6 +180,5 @@ This means rendezvous relays stop being the only way to reach a node that has published a hidden service. They remain useful bootstrap/fallback infrastructure, but an already-discovered onion-capable peer can be reached directly over Tor. -The node does not create or rotate Tor hidden services itself. Tor owns the -private key under `HiddenServiceDir`; back it up if you want the onion name to -stay stable. +Tor owns the private key under `HiddenServiceDir`; back it up if you want the +onion name to stay stable. diff --git a/src/Makefile b/src/Makefile index 511fe79..19ad33a 100644 --- a/src/Makefile +++ b/src/Makefile @@ -53,7 +53,7 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \ HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ - selftest.h walletcmd.h compat.h sockcount.h proxy.h font_roboto.h + selftest.h walletcmd.h compat.h sockcount.h proxy.h tor.h font_roboto.h IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o @@ -61,7 +61,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ - obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o + obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o GUI_OBJS = obj/gui.o obj/main_gui.o @@ -194,5 +194,6 @@ tests: test_bip32 bitflash-node ./bitflash-node -selftest=pool-stratum ./bitflash-node -selftest=parse-money ./bitflash-node -selftest=socks5-proxy + ./bitflash-node -selftest=managed-tor .PHONY: tests fuzz-net-message-smoke fuzz-script-smoke diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 239fb60..deed471 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -4,6 +4,7 @@ #include "nostr.h" #include "proxy.h" #include "selftest.h" +#include "tor.h" #include "walletcmd.h" #include // hardware_concurrency, to sanity-check /genproclimit #ifndef _WIN32 @@ -113,7 +114,7 @@ static void PrintUsage() printf(" /gen\n"); printf(" /nogui or /daemon\n"); printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt,\n"); - printf(" net-message, consensus-limits, pool-stratum, parse-money, or socks5-proxy\n"); + printf(" net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, or managed-tor\n"); printf("\n"); printf("Mining mode:\n"); printf(" /operator\n"); @@ -141,6 +142,7 @@ static void PrintUsage() printf(" /port=N (P2P listen port, default 8433)\n"); printf(" /socks=HOST:PORT (SOCKS5 proxy for Nostr, .btf relay, and onion dials)\n"); printf(" /tor[=HOST:PORT] (Tor mode; default SOCKS5 proxy is 127.0.0.1:9050)\n"); + printf(" /managedtor[=PATH] (start Tor, create a hidden service, advertise its onion)\n"); printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n"); printf("\n"); printf("Wallet:\n"); @@ -284,6 +286,24 @@ static void ParseStartupArguments(int argc, char* argv[]) vBtfMeetingRelays.push_back(rvRelay); } + // net.cpp has described nListenPort as "tunable via /port" since it was + // written, but nothing ever read the option, so the port was fixed at 8433 + // and a second node could not start on a machine already running one. Parse + // it before Tor setup so a managed hidden service maps the real listener. + string strPort = argval2(argc, argv, "/port", "-port"); + if (!strPort.empty()) + { + int nPort = atoi(strPort.c_str()); + if (nPort <= 0 || nPort > 65535) + fprintf(stderr, "Ignoring /port=%s: not a port number\n", strPort.c_str()); + else + { + nListenPort = htons((unsigned short)nPort); + // Or we would announce a port we never bound. + addrLocalHost.port = nListenPort; + } + } + string announceRelay = argval2(argc, argv, "/announcerelay", "-announcerelay"); if (!announceRelay.empty()) strBtfAnnounceRelay = announceRelay; @@ -299,10 +319,23 @@ static void ParseStartupArguments(int argc, char* argv[]) BtfLocalOnionEndpoint().c_str()); } + bool fManagedTor = arg(argc, argv, "/managedtor") || arg(argc, argv, "-managedtor"); bool fTorMode = arg(argc, argv, "/tor") || arg(argc, argv, "-tor"); - if (fTorMode) + string socksProxy = argval2(argc, argv, "/socks", "-socks"); + if (fManagedTor) + { + if (fTorMode || !socksProxy.empty()) + fprintf(stderr, "Warning: /managedtor takes precedence over /tor and /socks\n"); + + string torPath = argval2(argc, argv, "/managedtor", "-managedtor"); + string err; + if (!BtfStartManagedTor(torPath, err)) + fprintf(stderr, "Ignoring /managedtor=%s: %s\n", torPath.c_str(), err.c_str()); + else + fprintf(stderr, "Managed Tor enabled: %s\n", BtfManagedTorStatus().c_str()); + } + else if (fTorMode) { - string socksProxy = argval2(argc, argv, "/socks", "-socks"); if (!socksProxy.empty()) fprintf(stderr, "Warning: /tor takes precedence over /socks=%s\n", socksProxy.c_str()); @@ -317,7 +350,6 @@ static void ParseStartupArguments(int argc, char* argv[]) } else { - string socksProxy = argval2(argc, argv, "/socks", "-socks"); if (!socksProxy.empty()) { string err; @@ -348,22 +380,6 @@ static void ParseStartupArguments(int argc, char* argv[]) vBtfExtraSeeds.push_back(make_pair(val.substr(0, colon), val.substr(colon + 1))); } - // net.cpp has described nListenPort as "tunable via /port" since it was - // written, but nothing ever read the option, so the port was fixed at 8433 - // and a second node could not start on a machine already running one. - string strPort = argval2(argc, argv, "/port", "-port"); - if (!strPort.empty()) - { - int nPort = atoi(strPort.c_str()); - if (nPort <= 0 || nPort > 65535) - fprintf(stderr, "Ignoring /port=%s: not a port number\n", strPort.c_str()); - else - { - nListenPort = htons((unsigned short)nPort); - // Or we would announce a port we never bound. - addrLocalHost.port = nListenPort; - } - } } // Report a startup failure somewhere the user will actually see it. @@ -715,7 +731,12 @@ int main(int argc, char* argv[]) ReacceptWalletTransactions(); - if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; } + if (!StartNode(strErrors)) + { + fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); + BtfStopManagedTor(); + return 1; + } if (nMineMode == MINE_OPERATOR) { gPoolServerRunning = true; @@ -748,6 +769,7 @@ int main(int argc, char* argv[]) printf("Running headless. Ctrl-C to stop.\n"); while (!fShutdown) Sleep(500); StopNode(); + BtfStopManagedTor(); // Checkpoints, releases wallet.dat from this directory's Berkeley DB // environment, and closes it. Declared since 2009 and never once // called here, which is why a wallet.dat copied elsewhere would not @@ -767,6 +789,7 @@ int main(int argc, char* argv[]) int ret = RunGUI(argc, argv); fShutdown = true; StopNode(); + BtfStopManagedTor(); DBFlush(true); return ret; #endif diff --git a/src/makefile.mingw b/src/makefile.mingw index f82247a..333be96 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -37,7 +37,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ - obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o + obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o @@ -51,7 +51,7 @@ GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ - selftest.h walletcmd.h sockcount.h proxy.h compat.h font_roboto.h + selftest.h walletcmd.h sockcount.h proxy.h tor.h compat.h font_roboto.h all: bitflash.exe @@ -113,3 +113,4 @@ tests: test_bip32.exe bitflash.exe ./bitflash.exe -selftest=consensus-limits ./bitflash.exe -selftest=pool-stratum ./bitflash.exe -selftest=socks5-proxy + ./bitflash.exe -selftest=managed-tor diff --git a/src/net.cpp b/src/net.cpp index ab17332..6bc3c1c 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -22,6 +22,7 @@ #include "btfaddr.h" #include "btftunnel.h" #include "proxy.h" +#include "tor.h" void ThreadReconnectCachedBtfPeers(void* parg); void ThreadMessageHandler2(void* parg); @@ -296,6 +297,7 @@ string GetDiagnosticsText() str += strprintf(" peers watched %d of %d held by poll%s\n", nPeersWatched, nHeld, nHeld > nPeersWatched ? " <-- NOT ALL PEERS ARE BEING READ" : ""); + str += strprintf(" managed Tor %s\n", BtfManagedTorStatus().c_str()); if (nBlocksReceived > 0) str += strprintf(" blocks received %lld (%lld arrived without a parent, %.1f%%)\n", diff --git a/src/readme.txt b/src/readme.txt index 1e9cffc..9ddbc24 100644 --- a/src/readme.txt +++ b/src/readme.txt @@ -49,6 +49,9 @@ Command-line options (in addition to the original ones) /tor[=HOST:PORT] Tor mode for outbound Nostr, .btf rendezvous, and direct onion peer dials; defaults to the local Tor SOCKS5 proxy at 127.0.0.1:9050 + /managedtor[=PATH] + start Tor, create a v3 hidden service for this node, route outbound + discovery through it, and advertise the generated onion endpoint /solomine mine without requiring a connected peer (local testing) /operator operator mode (run pool server) /participant=ADDR diff --git a/src/selftest.cpp b/src/selftest.cpp index a0d9edd..4cb872e 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -11,6 +11,7 @@ #include "proxy.h" #include "selftest.h" #include "sockcount.h" +#include "tor.h" #include "walletcmd.h" extern int RunPoolStratumSelfTest(); @@ -1565,6 +1566,38 @@ static int RunSocks5ProxySelfTest() return nFail == 0 ? 0 : 1; } +static int RunManagedTorSelfTest() +{ + printf("managed-tor self-test\n"); + int nFail = 0; + + std::string torrc = BtfBuildManagedTorrcForTest("C:/Bitflash Managed Tor/data", + "C:/Bitflash Managed Tor/onion-service", + 19050, 8433); + nFail += Check(torrc.find("DataDirectory \"C:/Bitflash Managed Tor/data\"") != std::string::npos, + "quotes the Tor data directory") ? 0 : 1; + nFail += Check(torrc.find("SocksPort 127.0.0.1:19050") != std::string::npos, + "binds SOCKS5 to localhost only") ? 0 : 1; + nFail += Check(torrc.find("IsolateSOCKSAuth") != std::string::npos && + torrc.find("IsolateClientAddr") != std::string::npos && + torrc.find("IsolateDestAddr") != std::string::npos && + torrc.find("IsolateDestPort") != std::string::npos, + "enables strict Tor stream isolation") ? 0 : 1; + nFail += Check(torrc.find("HiddenServiceDir \"C:/Bitflash Managed Tor/onion-service\"") != std::string::npos, + "writes a hidden service directory") ? 0 : 1; + nFail += Check(torrc.find("HiddenServiceVersion 3") != std::string::npos, + "requests a v3 onion service") ? 0 : 1; + nFail += Check(torrc.find("HiddenServicePort 8433 127.0.0.1:8433") != std::string::npos, + "maps the onion service to the Bitflash P2P listener") ? 0 : 1; + nFail += Check(BtfManagedTorStatus() == "disabled", + "managed Tor starts disabled") ? 0 : 1; + + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + int RunSelfTest(const std::string& name) { AttachTerminal(); @@ -1589,8 +1622,10 @@ int RunSelfTest(const std::string& name) return RunParseMoneySelfTest(); if (name == "socks5-proxy") return RunSocks5ProxySelfTest(); + if (name == "managed-tor") + return RunManagedTorSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - 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"); + printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); return 1; } diff --git a/src/tor.cpp b/src/tor.cpp new file mode 100644 index 0000000..30a6f0b --- /dev/null +++ b/src/tor.cpp @@ -0,0 +1,492 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. + +#include "headers_core.h" +#include "tor.h" +#include "nostr.h" +#include "proxy.h" + +#include + +#ifndef _WIN32 +#include +#include +#endif + +static CCriticalSection cs_managedTor; +static bool g_fManagedTor = false; +static bool g_fManagedTorStopping = false; +static unsigned short g_managedTorSocksPort = 0; +static unsigned short g_managedTorP2PPort = 0; +static std::string g_managedTorPath; +static std::string g_managedTorDataDir; +static std::string g_managedTorHiddenServiceDir; +static std::string g_managedTorOnion; +static std::string g_managedTorStatus; +#ifdef _WIN32 +static PROCESS_INFORMATION g_managedTorProcess; +#else +static pid_t g_managedTorPid = -1; +#endif + +static std::string PathJoin(const std::string& a, const std::string& b) +{ + if (a.empty()) + return b; + char last = a[a.size() - 1]; + if (last == '/' || last == '\\') + return a + b; + return a + "/" + b; +} + +static std::string NormalizeTorrcPath(std::string path) +{ + for (size_t i = 0; i < path.size(); i++) + if (path[i] == '\\') + path[i] = '/'; + return path; +} + +static std::string QuoteTorrcPath(const std::string& path) +{ + std::string out = "\""; + for (size_t i = 0; i < path.size(); i++) + { + if (path[i] == '"' || path[i] == '\\') + out += '\\'; + out += path[i]; + } + out += "\""; + return out; +} + +static std::string QuoteCommandArg(const std::string& arg) +{ + std::string out = "\""; + for (size_t i = 0; i < arg.size(); i++) + { + if (arg[i] == '"') + out += '\\'; + out += arg[i]; + } + out += "\""; + return out; +} + +static bool EnsureDir(const std::string& path, std::string& errOut) +{ + if (path.empty()) + { + errOut = "directory path is empty"; + return false; + } +#ifdef _WIN32 + DWORD attr = GetFileAttributesA(path.c_str()); + if (attr != INVALID_FILE_ATTRIBUTES) + { + if (attr & FILE_ATTRIBUTE_DIRECTORY) + return true; + errOut = strprintf("%s exists and is not a directory", path.c_str()); + return false; + } + if (CreateDirectoryA(path.c_str(), NULL)) + return true; + DWORD err = GetLastError(); + if (err == ERROR_ALREADY_EXISTS) + return true; + errOut = strprintf("could not create %s (error %lu)", path.c_str(), (unsigned long)err); + return false; +#else + struct stat st; + if (stat(path.c_str(), &st) == 0) + { + if (S_ISDIR(st.st_mode)) + return true; + errOut = strprintf("%s exists and is not a directory", path.c_str()); + return false; + } + if (mkdir(path.c_str(), 0700) == 0 || errno == EEXIST) + return true; + errOut = strprintf("could not create %s: %s", path.c_str(), strerror(errno)); + return false; +#endif +} + +static bool WriteTextFile(const std::string& path, const std::string& data, + std::string& errOut) +{ + FILE* f = fopen(path.c_str(), "wb"); + if (!f) + { + errOut = strprintf("could not write %s", path.c_str()); + return false; + } + bool ok = fwrite(data.data(), 1, data.size(), f) == data.size(); + if (fclose(f) != 0) + ok = false; + if (!ok) + { + errOut = strprintf("could not finish writing %s", path.c_str()); + return false; + } + return true; +} + +static bool ReadOneLine(const std::string& path, std::string& out) +{ + out.clear(); + FILE* f = fopen(path.c_str(), "rb"); + if (!f) + return false; + char buf[256] = {0}; + if (!fgets(buf, sizeof(buf), f)) + { + fclose(f); + return false; + } + fclose(f); + out = buf; + while (!out.empty() && (out[out.size() - 1] == '\n' || out[out.size() - 1] == '\r' || + out[out.size() - 1] == ' ' || out[out.size() - 1] == '\t')) + out.erase(out.size() - 1); + return !out.empty(); +} + +static bool PickLocalPort(unsigned short& portOut, std::string& errOut) +{ + portOut = 0; + SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (s == INVALID_SOCKET) + { + errOut = "could not create local port probe socket"; + return false; + } + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) != 0) + { + closesocket(s); + errOut = "could not bind local port probe socket"; + return false; + } + socklen_t len = sizeof(addr); + if (getsockname(s, (struct sockaddr*)&addr, &len) != 0) + { + closesocket(s); + errOut = "could not read local port probe socket"; + return false; + } + portOut = ntohs(addr.sin_port); + closesocket(s); + if (portOut == 0) + { + errOut = "local port probe returned port zero"; + return false; + } + return true; +} + +static std::string ExecutableDir() +{ +#ifdef _WIN32 + char path[MAX_PATH + 1] = {0}; + DWORD n = GetModuleFileNameA(NULL, path, MAX_PATH); + if (n == 0 || n >= MAX_PATH) + return "."; + std::string s = path; + size_t slash = s.find_last_of("\\/"); + if (slash == std::string::npos) + return "."; + return s.substr(0, slash); +#else + char path[4096] = {0}; + ssize_t n = readlink("/proc/self/exe", path, sizeof(path) - 1); + if (n <= 0) + return "."; + path[n] = 0; + std::string s = path; + size_t slash = s.find_last_of('/'); + if (slash == std::string::npos) + return "."; + return s.substr(0, slash); +#endif +} + +static bool FileIsExecutableCandidate(const std::string& path) +{ +#ifdef _WIN32 + DWORD attr = GetFileAttributesA(path.c_str()); + return attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY); +#else + return access(path.c_str(), X_OK) == 0; +#endif +} + +static std::string ResolveTorPath(const std::string& torPathOpt) +{ + if (!torPathOpt.empty()) + return torPathOpt; + + std::vector candidates; + std::string exeDir = ExecutableDir(); +#ifdef _WIN32 + candidates.push_back(PathJoin(exeDir, "tor/tor.exe")); + candidates.push_back(PathJoin(exeDir, "tor.exe")); + candidates.push_back("tor.exe"); +#else + candidates.push_back(PathJoin(exeDir, "tor/tor")); + candidates.push_back(PathJoin(exeDir, "tor")); + candidates.push_back("tor"); +#endif + for (size_t i = 0; i < candidates.size(); i++) + { + if (candidates[i].find('/') == std::string::npos && + candidates[i].find('\\') == std::string::npos) + return candidates[i]; // Let the OS search PATH. + if (FileIsExecutableCandidate(candidates[i])) + return candidates[i]; + } +#ifdef _WIN32 + return "tor.exe"; +#else + return "tor"; +#endif +} + +std::string BtfBuildManagedTorrcForTest(const std::string& dataDir, + const std::string& hiddenServiceDir, + unsigned short socksPort, + unsigned short p2pPort) +{ + std::string torData = NormalizeTorrcPath(dataDir); + std::string hsDir = NormalizeTorrcPath(hiddenServiceDir); + std::string logFile = NormalizeTorrcPath(PathJoin(dataDir, "tor.log")); + + std::string s; + s += "# Generated by Bitflash. Edit only while Bitflash is not managing Tor.\n"; + s += "DataDirectory " + QuoteTorrcPath(torData) + "\n"; + s += strprintf("SocksPort 127.0.0.1:%u IsolateSOCKSAuth IsolateClientAddr IsolateDestAddr IsolateDestPort\n", + (unsigned)socksPort); + s += "HiddenServiceDir " + QuoteTorrcPath(hsDir) + "\n"; + s += "HiddenServiceVersion 3\n"; + s += strprintf("HiddenServicePort %u 127.0.0.1:%u\n", + (unsigned)p2pPort, (unsigned)p2pPort); + s += "Log notice file " + QuoteTorrcPath(logFile) + "\n"; + return s; +} + +static bool StartTorProcess(const std::string& torPath, const std::string& torrcPath, + std::string& errOut) +{ +#ifdef _WIN32 + memset(&g_managedTorProcess, 0, sizeof(g_managedTorProcess)); + STARTUPINFOA si; + memset(&si, 0, sizeof(si)); + si.cb = sizeof(si); + std::string cmd = QuoteCommandArg(torPath) + " -f " + QuoteCommandArg(torrcPath); + std::vector mutableCmd(cmd.begin(), cmd.end()); + mutableCmd.push_back(0); + if (!CreateProcessA(NULL, &mutableCmd[0], NULL, NULL, FALSE, CREATE_NO_WINDOW, + NULL, NULL, &si, &g_managedTorProcess)) + { + errOut = strprintf("could not start Tor process %s (error %lu)", + torPath.c_str(), (unsigned long)GetLastError()); + return false; + } + return true; +#else + g_managedTorPid = fork(); + if (g_managedTorPid < 0) + { + errOut = strprintf("could not fork Tor process: %s", strerror(errno)); + return false; + } + if (g_managedTorPid == 0) + { + execlp(torPath.c_str(), torPath.c_str(), "-f", torrcPath.c_str(), (char*)NULL); + _exit(127); + } + return true; +#endif +} + +static void ThreadManagedTorHostname(void*) +{ + std::string hostnamePath; + unsigned short p2pPort = 0; + CRITICAL_BLOCK(cs_managedTor) + { + hostnamePath = PathJoin(g_managedTorHiddenServiceDir, "hostname"); + p2pPort = g_managedTorP2PPort; + } + + for (int i = 0; i < 90 && !fShutdown; i++) + { + std::string onion; + if (ReadOneLine(hostnamePath, onion)) + { + std::string endpoint = strprintf("%s:%u", onion.c_str(), (unsigned)p2pPort); + std::string err; + if (BtfSetLocalOnionEndpoint(endpoint, err)) + { + CRITICAL_BLOCK(cs_managedTor) + { + g_managedTorOnion = BtfLocalOnionEndpoint(); + g_managedTorStatus = strprintf("running, SOCKS5 127.0.0.1:%u, onion %s", + (unsigned)g_managedTorSocksPort, + g_managedTorOnion.c_str()); + } + printf("Managed Tor hidden service ready: %s\n", BtfLocalOnionEndpoint().c_str()); + return; + } + CRITICAL_BLOCK(cs_managedTor) + g_managedTorStatus = strprintf("running, but generated onion endpoint was rejected: %s", + err.c_str()); + return; + } + Sleep(1000); + } + + CRITICAL_BLOCK(cs_managedTor) + { + if (!g_fManagedTorStopping) + g_managedTorStatus = strprintf("running, SOCKS5 127.0.0.1:%u, waiting for hidden service hostname", + (unsigned)g_managedTorSocksPort); + } +} + +bool BtfStartManagedTor(const std::string& torPathOpt, std::string& errOut) +{ + CRITICAL_BLOCK(cs_managedTor) + { + if (g_fManagedTor) + { + errOut = "managed Tor is already running"; + return false; + } + } + + std::string root = PathJoin(GetAppDir(), "managed-tor"); + std::string dataDir = PathJoin(root, "data"); + std::string hsDir = PathJoin(root, "onion-service"); + std::string torrcPath = PathJoin(root, "torrc"); + if (!EnsureDir(root, errOut) || !EnsureDir(dataDir, errOut) || !EnsureDir(hsDir, errOut)) + return false; + + unsigned short socksPort = 0; + if (!PickLocalPort(socksPort, errOut)) + return false; + + unsigned short p2pPort = ntohs(nListenPort); + std::string torrc = BtfBuildManagedTorrcForTest(dataDir, hsDir, socksPort, p2pPort); + if (!WriteTextFile(torrcPath, torrc, errOut)) + return false; + + std::string torPath = ResolveTorPath(torPathOpt); + { + CRITICAL_BLOCK(cs_managedTor) + { + g_fManagedTor = true; + g_fManagedTorStopping = false; + g_managedTorPath = torPath; + g_managedTorDataDir = dataDir; + g_managedTorHiddenServiceDir = hsDir; + g_managedTorSocksPort = socksPort; + g_managedTorP2PPort = p2pPort; + g_managedTorOnion.clear(); + g_managedTorStatus = strprintf("starting %s, SOCKS5 127.0.0.1:%u", + torPath.c_str(), (unsigned)socksPort); + } + } + + if (!StartTorProcess(torPath, torrcPath, errOut)) + { + CRITICAL_BLOCK(cs_managedTor) + { + g_fManagedTor = false; + g_managedTorStatus = strprintf("failed: %s", errOut.c_str()); + } + return false; + } + + std::string proxyErr; + if (!BtfEnableTorProxy(strprintf("127.0.0.1:%u", (unsigned)socksPort), proxyErr)) + { + BtfStopManagedTor(); + errOut = strprintf("could not enable managed Tor SOCKS5 proxy: %s", proxyErr.c_str()); + return false; + } + + if (_beginthread(ThreadManagedTorHostname, 0, NULL) == (uintptr_t)-1) + { + BtfStopManagedTor(); + errOut = "could not start managed Tor hostname watcher"; + return false; + } + + return true; +} + +void BtfStopManagedTor() +{ + bool fWasRunning = false; + CRITICAL_BLOCK(cs_managedTor) + { + fWasRunning = g_fManagedTor; + if (!fWasRunning) + return; + g_fManagedTorStopping = true; + g_fManagedTor = false; + g_managedTorStatus = "stopping"; + } +#ifdef _WIN32 + if (g_managedTorProcess.hProcess) + { + TerminateProcess(g_managedTorProcess.hProcess, 0); + WaitForSingleObject(g_managedTorProcess.hProcess, 5000); + CloseHandle(g_managedTorProcess.hProcess); + CloseHandle(g_managedTorProcess.hThread); + memset(&g_managedTorProcess, 0, sizeof(g_managedTorProcess)); + } +#else + if (g_managedTorPid > 0) + { + kill(g_managedTorPid, SIGTERM); + for (int i = 0; i < 50; i++) + { + int status = 0; + pid_t r = waitpid(g_managedTorPid, &status, WNOHANG); + if (r == g_managedTorPid) + break; + Sleep(100); + } + g_managedTorPid = -1; + } +#endif + CRITICAL_BLOCK(cs_managedTor) + { + g_managedTorStatus = "stopped"; + g_managedTorOnion.clear(); + } +} + +bool BtfManagedTorEnabled() +{ + CRITICAL_BLOCK(cs_managedTor) + return g_fManagedTor; + return false; +} + +std::string BtfManagedTorStatus() +{ + CRITICAL_BLOCK(cs_managedTor) + { + if (!g_fManagedTor && g_managedTorStatus.empty()) + return "disabled"; + return g_managedTorStatus; + } + return "disabled"; +} diff --git a/src/tor.h b/src/tor.h new file mode 100644 index 0000000..482e773 --- /dev/null +++ b/src/tor.h @@ -0,0 +1,22 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// Managed Tor process support. This is transport bootstrapping only: it does +// not touch consensus, wallet keys, mining, or the .btf identity. + +#ifndef BITFLASH_TOR_H +#define BITFLASH_TOR_H + +#include + +bool BtfStartManagedTor(const std::string& torPathOpt, std::string& errOut); +void BtfStopManagedTor(); +bool BtfManagedTorEnabled(); +std::string BtfManagedTorStatus(); + +std::string BtfBuildManagedTorrcForTest(const std::string& dataDir, + const std::string& hiddenServiceDir, + unsigned short socksPort, + unsigned short p2pPort); + +#endif -- cgit v1.2.3 From c49b92a9a79a398b51200e69f7dde8a467b9c925 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 14 Aug 2026 15:03:21 -0300 Subject: wallet: add experimental SQLite record store --- .github/workflows/ci.yml | 1 + Makefile | 8 +- docs/storage.md | 19 ++++ src/Makefile | 10 +- src/main_gui.cpp | 3 +- src/makefile.mingw | 8 +- src/selftest.cpp | 108 +++++++++++++++++++- src/wallet_sqlite.cpp | 254 +++++++++++++++++++++++++++++++++++++++++++++++ src/wallet_sqlite.h | 40 ++++++++ 9 files changed, 439 insertions(+), 12 deletions(-) create mode 100644 src/wallet_sqlite.cpp create mode 100644 src/wallet_sqlite.h (limited to 'src/Makefile') diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4045f4..e22b5a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,7 @@ jobs: mingw-w64-ucrt-x86_64-glfw mingw-w64-ucrt-x86_64-openssl mingw-w64-ucrt-x86_64-db + mingw-w64-ucrt-x86_64-sqlite3 mingw-w64-ucrt-x86_64-libsodium mingw-w64-ucrt-x86_64-nlohmann-json mingw-w64-ucrt-x86_64-boost diff --git a/Makefile b/Makefile index 60d1227..fab2507 100644 --- a/Makefile +++ b/Makefile @@ -43,10 +43,11 @@ deps-apt: @if command -v apt-get >/dev/null 2>&1; then \ $(SUDO) apt-get install -y build-essential cmake git pkg-config autoconf \ libtool libssl-dev libdb5.3++-dev libsodium-dev nlohmann-json3-dev \ - libboost-system-dev libglfw3-dev libgl-dev python3-pil clang; \ + libboost-system-dev libglfw3-dev libgl-dev libsqlite3-dev \ + python3-pil clang; \ else \ echo "Not an apt system — install: g++ cmake git autoconf libtool"; \ - echo " libssl libdb++ libsodium nlohmann-json boost glfw3 opengl python3-pil"; \ + echo " libssl libdb++ libsodium nlohmann-json boost glfw3 opengl sqlite3 python3-pil"; \ fi deps-secp256k1: @@ -113,7 +114,7 @@ img = ico.convert('RGBA'); \ img.save('/tmp/bitflash.AppDir/bitflash.png'); \ import shutil; shutil.copy('/tmp/bitflash.AppDir/bitflash.png', \ '/tmp/bitflash.AppDir/usr/share/icons/hicolor/256x256/apps/bitflash.png')" - @for lib in libsodium.so.23 libdb_cxx-5.3.so libglfw.so.3; do \ + @for lib in libsodium.so.23 libdb_cxx-5.3.so libsqlite3.so.0 libglfw.so.3; do \ path=$$(ldconfig -p | grep " $$lib " | awk '{print $$NF}' | head -1); \ [ -z "$$path" ] && path=$$(find /usr/lib /lib -name "$$lib" 2>/dev/null | head -1); \ [ -n "$$path" ] && cp -L "$$path" /tmp/bitflash.AppDir/usr/lib/ && echo " bundled $$lib" || echo " missing $$lib"; \ @@ -131,6 +132,7 @@ deps-windows: mingw-w64-ucrt-x86_64-glfw \ mingw-w64-ucrt-x86_64-openssl \ mingw-w64-ucrt-x86_64-db \ + mingw-w64-ucrt-x86_64-sqlite3 \ mingw-w64-ucrt-x86_64-libsodium \ mingw-w64-ucrt-x86_64-nlohmann-json \ mingw-w64-ucrt-x86_64-boost \ diff --git a/docs/storage.md b/docs/storage.md index c1cda45..f75f9b9 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -85,6 +85,25 @@ A future wallet backend must keep these properties: - recovery phrase encrypted and locked, requiring unlock before coverage can be audited. +## Experimental SQLite Wallet Store + +The SQLite wallet path starts as a byte-preserving record store, not as a new +wallet format: + +```sql +CREATE TABLE wallet_records ( + key BLOB PRIMARY KEY NOT NULL, + value BLOB NOT NULL +); +PRAGMA user_version=1; +``` + +Keys and values are the same serialized `CDataStream` bytes used by `wallet.dat`. +The first implementation is exercised only by `-selftest=wallet-sqlite`; the +runtime wallet still opens Berkeley DB. The purpose is to prove schema creation, +raw record write/read, close/reopen behavior, and value preservation before any +BDB-to-SQLite migration or runtime backend flag exists. + ## `blkindex.dat` `blkindex.dat` is accessed through `CTxDB`. diff --git a/src/Makefile b/src/Makefile index 19ad33a..cfa40f6 100644 --- a/src/Makefile +++ b/src/Makefile @@ -40,7 +40,7 @@ FUZZ_LDFLAGS = -fsanitize=fuzzer,address,undefined LIBS = -L$(SECP_LIB) -lsecp256k1 \ -L$(RANDOMX_DIR)/build -lrandomx \ - -lsodium -lssl -lcrypto -ldb_cxx -lpthread \ + -lsodium -lssl -lcrypto -ldb_cxx -lsqlite3 -lpthread \ -lglfw -lGL # Every header a .cpp here includes has to be listed, not just the interesting @@ -53,7 +53,7 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \ HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ - selftest.h walletcmd.h compat.h sockcount.h proxy.h tor.h font_roboto.h + selftest.h walletcmd.h wallet_sqlite.h compat.h sockcount.h proxy.h tor.h font_roboto.h IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o @@ -61,7 +61,8 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ - obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o + obj/selftest.o obj/bip32.o obj/walletcmd.o obj/wallet_sqlite.o \ + obj/proxy.o obj/tor.o GUI_OBJS = obj/gui.o obj/main_gui.o @@ -116,7 +117,7 @@ bitflash: $(IMGUI_OBJS) $(NODE_OBJS) $(GUI_OBJS) NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \ -L$(RANDOMX_DIR)/build -lrandomx \ - -lsodium -lssl -lcrypto -ldb_cxx -lpthread + -lsodium -lssl -lcrypto -ldb_cxx -lsqlite3 -lpthread NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \ obj-node/main_gui.o obj-node/headless.o @@ -187,6 +188,7 @@ tests: test_bip32 bitflash-node ./bitflash-node -selftest=wallet-keypool ./bitflash-node -selftest=wallet-hd ./bitflash-node -selftest=wallet-format + ./bitflash-node -selftest=wallet-sqlite ./bitflash-node -selftest=wallet-crypto ./bitflash-node -selftest=wallet-encrypt ./bitflash-node -selftest=net-message diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 705b4ef..965c818 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -114,7 +114,8 @@ static void PrintUsage() printf(" /gen\n"); printf(" /nogui or /daemon\n"); printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity,\n"); - printf(" db-env-reopen, wallet-crypto, wallet-encrypt, wallet-portability,\n"); + printf(" db-env-reopen, wallet-sqlite, wallet-crypto, wallet-encrypt,\n"); + printf(" wallet-portability,\n"); printf(" net-message,\n"); printf(" consensus-limits, pool-stratum,\n"); printf(" parse-money, socks5-proxy, or managed-tor\n"); diff --git a/src/makefile.mingw b/src/makefile.mingw index 333be96..2ff4e11 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -24,7 +24,7 @@ CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ LIBS = -L$(DEPS)/lib -lsecp256k1 \ -L$(RANDOMX)/build -lrandomx \ - -lsodium -lssl -lcrypto -ldb_cxx -lpthread \ + -lsodium -lssl -lcrypto -ldb_cxx -lsqlite3 -lpthread \ -LC:/msys64/ucrt64/lib \ -lglfw3 -lopengl32 -lgdi32 \ -lkernel32 -luser32 -lcomdlg32 -lwinspool -lwinmm \ @@ -37,7 +37,8 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ - obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o + obj/selftest.o obj/bip32.o obj/walletcmd.o obj/wallet_sqlite.o \ + obj/proxy.o obj/tor.o GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o @@ -51,7 +52,7 @@ GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ - selftest.h walletcmd.h sockcount.h proxy.h tor.h compat.h font_roboto.h + selftest.h walletcmd.h wallet_sqlite.h sockcount.h proxy.h tor.h compat.h font_roboto.h all: bitflash.exe @@ -107,6 +108,7 @@ tests: test_bip32.exe bitflash.exe ./bitflash.exe -selftest=wallet-keypool ./bitflash.exe -selftest=wallet-hd ./bitflash.exe -selftest=wallet-format + ./bitflash.exe -selftest=wallet-sqlite ./bitflash.exe -selftest=wallet-crypto ./bitflash.exe -selftest=wallet-encrypt ./bitflash.exe -selftest=net-message diff --git a/src/selftest.cpp b/src/selftest.cpp index 31c168f..f9c4a8e 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -13,6 +13,7 @@ #include "sockcount.h" #include "tor.h" #include "walletcmd.h" +#include "wallet_sqlite.h" extern int RunPoolStratumSelfTest(); @@ -62,6 +63,11 @@ static std::string HexStrLocal(const std::vector& v) return out; } +static vector DataStreamBytes(const CDataStream& ss) +{ + return vector(ss.begin(), ss.end()); +} + static bool MakeTempDir(std::string& out) { #ifdef _WIN32 @@ -1030,6 +1036,104 @@ static int RunDbEnvReopenSelfTest() return nFail == 0 ? 0 : 1; } +static int RunWalletSQLiteSelfTest() +{ + fflush(stdout); + printf("wallet-sqlite self-test\n"); + + std::string tmp; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + + int nFail = 0; + string strPath = tmp + "/wallet.sqlite"; + string strError; + + try + { + CDataStream ssKeySetting(SER_DISK); + ssKeySetting << make_pair(string("setting"), string("sqlite-roundtrip")); + CDataStream ssValueSetting(SER_DISK); + ssValueSetting << (int64)424242; + + vector vchPubKey; + vchPubKey.push_back(0x04); + for (int i = 0; i < 64; i++) + vchPubKey.push_back((unsigned char)i); + CDataStream ssKeyDefault(SER_DISK); + ssKeyDefault << string("defaultkey"); + CDataStream ssValueDefault(SER_DISK); + ssValueDefault << vchPubKey; + + { + CWalletDBSQLite db; + nFail += Check(db.Open(strPath, strError), + "SQLite wallet opens and creates schema") ? 0 : 1; + nFail += Check(db.BeginTransaction(strError), + "SQLite wallet begins a write transaction") ? 0 : 1; + nFail += Check(db.WriteRecord(DataStreamBytes(ssKeySetting), + DataStreamBytes(ssValueSetting), + strError), + "SQLite wallet writes a serialized setting record") ? 0 : 1; + nFail += Check(db.WriteRecord(DataStreamBytes(ssKeyDefault), + DataStreamBytes(ssValueDefault), + strError), + "SQLite wallet writes a serialized defaultkey record") ? 0 : 1; + nFail += Check(db.CommitTransaction(strError), + "SQLite wallet commits a write transaction") ? 0 : 1; + int nCount = 0; + nFail += Check(db.CountRecords(nCount, strError) && nCount == 2, + "SQLite wallet counts raw records") ? 0 : 1; + } + + { + CWalletDBSQLite db; + nFail += Check(db.Open(strPath, strError), + "SQLite wallet reopens") ? 0 : 1; + vector vchRead; + nFail += Check(db.ReadRecord(DataStreamBytes(ssKeySetting), + vchRead, + strError), + "SQLite wallet reads a serialized setting record") ? 0 : 1; + int64 nReadValue = 0; + CDataStream ssRead(vchRead, SER_DISK); + ssRead >> nReadValue; + nFail += Check(!ssRead.fail() && nReadValue == 424242, + "SQLite wallet preserves serialized values byte-for-byte") ? 0 : 1; + + vchRead.clear(); + nFail += Check(db.ReadRecord(DataStreamBytes(ssKeyDefault), + vchRead, + strError), + "SQLite wallet reads a serialized defaultkey record") ? 0 : 1; + vector vchReadPubKey; + CDataStream ssReadDefault(vchRead, SER_DISK); + ssReadDefault >> vchReadPubKey; + nFail += Check(!ssReadDefault.fail() && vchReadPubKey == vchPubKey, + "SQLite wallet preserves public key blobs") ? 0 : 1; + } + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + static void ClearWalletRuntimeForTest() { CRITICAL_BLOCK(cs_mapKeys) @@ -2289,6 +2393,8 @@ int RunSelfTest(const std::string& name) return RunWalletStorageSanitySelfTest(); if (name == "db-env-reopen") return RunDbEnvReopenSelfTest(); + if (name == "wallet-sqlite") + return RunWalletSQLiteSelfTest(); if (name == "wallet-crypto") return RunWalletCryptoSelfTest(); if (name == "wallet-encrypt") @@ -2309,6 +2415,6 @@ int RunSelfTest(const std::string& name) return RunManagedTorSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-crypto, wallet-encrypt, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); + printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-crypto, wallet-encrypt, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); return 1; } diff --git a/src/wallet_sqlite.cpp b/src/wallet_sqlite.cpp new file mode 100644 index 0000000..4104ac6 --- /dev/null +++ b/src/wallet_sqlite.cpp @@ -0,0 +1,254 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. + +#include "headers.h" +#include "wallet_sqlite.h" + +static void SetSQLiteError(sqlite3* pdb, const string& strContext, + string& strError) +{ + if (pdb) + strError = strContext + ": " + sqlite3_errmsg(pdb); + else + strError = strContext; +} + +CWalletDBSQLite::CWalletDBSQLite() : pdb(NULL) +{ +} + +CWalletDBSQLite::~CWalletDBSQLite() +{ + Close(); +} + +bool CWalletDBSQLite::Open(const string& strPath, string& strError) +{ + Close(); + + int ret = sqlite3_open_v2(strPath.c_str(), &pdb, + SQLITE_OPEN_READWRITE | + SQLITE_OPEN_CREATE | + SQLITE_OPEN_FULLMUTEX, + NULL); + if (ret != SQLITE_OK) + { + SetSQLiteError(pdb, "cannot open SQLite wallet", strError); + Close(); + return false; + } + + sqlite3_busy_timeout(pdb, 5000); + + if (!Exec("PRAGMA journal_mode=WAL;", strError)) + { + Close(); + return false; + } + if (!Exec("PRAGMA synchronous=FULL;", strError)) + { + Close(); + return false; + } + if (!InitSchema(strError)) + { + Close(); + return false; + } + return true; +} + +void CWalletDBSQLite::Close() +{ + if (pdb) + { + sqlite3_close(pdb); + pdb = NULL; + } +} + +bool CWalletDBSQLite::BeginTransaction(string& strError) +{ + if (!pdb) + { + strError = "SQLite wallet is not open"; + return false; + } + return Exec("BEGIN IMMEDIATE;", strError); +} + +bool CWalletDBSQLite::CommitTransaction(string& strError) +{ + if (!pdb) + { + strError = "SQLite wallet is not open"; + return false; + } + return Exec("COMMIT;", strError); +} + +bool CWalletDBSQLite::RollbackTransaction(string& strError) +{ + if (!pdb) + { + strError = "SQLite wallet is not open"; + return false; + } + return Exec("ROLLBACK;", strError); +} + +bool CWalletDBSQLite::Exec(const char* pszSql, string& strError) +{ + char* pszErr = NULL; + int ret = sqlite3_exec(pdb, pszSql, NULL, NULL, &pszErr); + if (ret != SQLITE_OK) + { + if (pszErr) + { + strError = pszErr; + sqlite3_free(pszErr); + } + else + SetSQLiteError(pdb, "SQLite exec failed", strError); + return false; + } + return true; +} + +bool CWalletDBSQLite::InitSchema(string& strError) +{ + return Exec("CREATE TABLE IF NOT EXISTS wallet_records (" + "key BLOB PRIMARY KEY NOT NULL," + "value BLOB NOT NULL" + ");", + strError) && + Exec("PRAGMA user_version=1;", strError); +} + +bool CWalletDBSQLite::WriteRecord(const vector& vchKey, + const vector& vchValue, + string& strError) +{ + if (!pdb) + { + strError = "SQLite wallet is not open"; + return false; + } + if (vchKey.empty()) + { + strError = "SQLite wallet record key is empty"; + return false; + } + + sqlite3_stmt* stmt = NULL; + const char* pszSql = + "INSERT OR REPLACE INTO wallet_records(key, value) VALUES(?, ?);"; + int ret = sqlite3_prepare_v2(pdb, pszSql, -1, &stmt, NULL); + if (ret != SQLITE_OK) + { + SetSQLiteError(pdb, "cannot prepare SQLite wallet write", strError); + return false; + } + + ret = sqlite3_bind_blob(stmt, 1, &vchKey[0], (int)vchKey.size(), + SQLITE_TRANSIENT); + if (ret == SQLITE_OK) + ret = sqlite3_bind_blob(stmt, 2, + vchValue.empty() ? NULL : &vchValue[0], + (int)vchValue.size(), + SQLITE_TRANSIENT); + if (ret == SQLITE_OK) + ret = sqlite3_step(stmt); + + bool fOk = (ret == SQLITE_DONE); + if (!fOk) + SetSQLiteError(pdb, "cannot write SQLite wallet record", strError); + sqlite3_finalize(stmt); + return fOk; +} + +bool CWalletDBSQLite::ReadRecord(const vector& vchKey, + vector& vchValueRet, + string& strError) +{ + vchValueRet.clear(); + if (!pdb) + { + strError = "SQLite wallet is not open"; + return false; + } + if (vchKey.empty()) + { + strError = "SQLite wallet record key is empty"; + return false; + } + + sqlite3_stmt* stmt = NULL; + const char* pszSql = "SELECT value FROM wallet_records WHERE key = ?;"; + int ret = sqlite3_prepare_v2(pdb, pszSql, -1, &stmt, NULL); + if (ret != SQLITE_OK) + { + SetSQLiteError(pdb, "cannot prepare SQLite wallet read", strError); + return false; + } + + ret = sqlite3_bind_blob(stmt, 1, &vchKey[0], (int)vchKey.size(), + SQLITE_TRANSIENT); + if (ret != SQLITE_OK) + { + SetSQLiteError(pdb, "cannot bind SQLite wallet key", strError); + sqlite3_finalize(stmt); + return false; + } + + ret = sqlite3_step(stmt); + if (ret == SQLITE_ROW) + { + const unsigned char* pch = + (const unsigned char*)sqlite3_column_blob(stmt, 0); + int nBytes = sqlite3_column_bytes(stmt, 0); + if (nBytes > 0 && pch) + vchValueRet.assign(pch, pch + nBytes); + sqlite3_finalize(stmt); + return true; + } + + if (ret == SQLITE_DONE) + strError = "SQLite wallet record not found"; + else + SetSQLiteError(pdb, "cannot read SQLite wallet record", strError); + sqlite3_finalize(stmt); + return false; +} + +bool CWalletDBSQLite::CountRecords(int& nRecordsRet, string& strError) +{ + nRecordsRet = 0; + if (!pdb) + { + strError = "SQLite wallet is not open"; + return false; + } + + sqlite3_stmt* stmt = NULL; + int ret = sqlite3_prepare_v2(pdb, + "SELECT COUNT(*) FROM wallet_records;", + -1, &stmt, NULL); + if (ret != SQLITE_OK) + { + SetSQLiteError(pdb, "cannot prepare SQLite wallet count", strError); + return false; + } + + ret = sqlite3_step(stmt); + if (ret != SQLITE_ROW) + { + SetSQLiteError(pdb, "cannot count SQLite wallet records", strError); + sqlite3_finalize(stmt); + return false; + } + + nRecordsRet = sqlite3_column_int(stmt, 0); + sqlite3_finalize(stmt); + return true; +} diff --git a/src/wallet_sqlite.h b/src/wallet_sqlite.h new file mode 100644 index 0000000..9812d3a --- /dev/null +++ b/src/wallet_sqlite.h @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. + +#ifndef BITFLASH_WALLET_SQLITE_H +#define BITFLASH_WALLET_SQLITE_H + +#include + +class CWalletDBSQLite +{ +public: + CWalletDBSQLite(); + ~CWalletDBSQLite(); + + bool Open(const string& strPath, string& strError); + void Close(); + + bool BeginTransaction(string& strError); + bool CommitTransaction(string& strError); + bool RollbackTransaction(string& strError); + + bool WriteRecord(const vector& vchKey, + const vector& vchValue, + string& strError); + bool ReadRecord(const vector& vchKey, + vector& vchValueRet, + string& strError); + bool CountRecords(int& nRecordsRet, string& strError); + +private: + sqlite3* pdb; + + bool Exec(const char* pszSql, string& strError); + bool InitSchema(string& strError); + + CWalletDBSQLite(const CWalletDBSQLite&); + void operator=(const CWalletDBSQLite&); +}; + +#endif -- cgit v1.2.3 From 59b9d11015f0e3a2475d0750fc534583c2ca9f77 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 14 Aug 2026 15:56:12 -0300 Subject: wallet: export BDB records to SQLite --- docs/storage.md | 6 ++ src/Makefile | 1 + src/main_gui.cpp | 15 ++++- src/makefile.mingw | 1 + src/selftest.cpp | 148 +++++++++++++++++++++++++++++++++++++++++++++++++- src/wallet_sqlite.cpp | 10 ++++ src/wallet_sqlite.h | 1 + src/walletcmd.cpp | 97 +++++++++++++++++++++++++++++++++ src/walletcmd.h | 5 ++ 9 files changed, 281 insertions(+), 3 deletions(-) (limited to 'src/Makefile') diff --git a/docs/storage.md b/docs/storage.md index f75f9b9..bb414b5 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -104,6 +104,12 @@ runtime wallet still opens Berkeley DB. The purpose is to prove schema creation, raw record write/read, close/reopen behavior, and value preservation before any BDB-to-SQLite migration or runtime backend flag exists. +`-walletsqliteexport=FILE` is the next staging tool. It streams every raw +`wallet.dat` record through `ScanWalletRecords()` and writes those same key and +value bytes into the SQLite table inside one transaction. It refuses to +overwrite an existing export and still does not change the runtime wallet +backend. + ## `blkindex.dat` `blkindex.dat` is accessed through `CTxDB`. diff --git a/src/Makefile b/src/Makefile index cfa40f6..b83ff58 100644 --- a/src/Makefile +++ b/src/Makefile @@ -189,6 +189,7 @@ tests: test_bip32 bitflash-node ./bitflash-node -selftest=wallet-hd ./bitflash-node -selftest=wallet-format ./bitflash-node -selftest=wallet-sqlite + ./bitflash-node -selftest=wallet-sqlite-migration ./bitflash-node -selftest=wallet-crypto ./bitflash-node -selftest=wallet-encrypt ./bitflash-node -selftest=net-message diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 965c818..6e89270 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -114,8 +114,8 @@ static void PrintUsage() printf(" /gen\n"); printf(" /nogui or /daemon\n"); printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity,\n"); - printf(" db-env-reopen, wallet-sqlite, wallet-crypto, wallet-encrypt,\n"); - printf(" wallet-portability,\n"); + printf(" db-env-reopen, wallet-sqlite, wallet-sqlite-migration,\n"); + printf(" wallet-crypto, wallet-encrypt, wallet-portability,\n"); printf(" net-message,\n"); printf(" consensus-limits, pool-stratum,\n"); printf(" parse-money, socks5-proxy, or managed-tor\n"); @@ -181,6 +181,8 @@ static void PrintUsage() printf(" (write the same audit as deterministic JSON)\n"); printf(" /walletstoragecheck (fail closed on inconsistent or unsafe\n"); printf(" wallet.dat storage state)\n"); + printf(" /walletsqliteexport=FILE (copy raw wallet.dat records into an\n"); + printf(" experimental SQLite store, then exit)\n"); printf(" /rescan (walk the chain for coins this wallet owns\n"); printf(" but never recorded, then exit)\n"); printf("\n"); @@ -529,6 +531,15 @@ int main(int argc, char* argv[]) return nRet; } + string strWalletSQLiteExport = + argval2(argc, argv, "/walletsqliteexport", "-walletsqliteexport"); + if (!strWalletSQLiteExport.empty()) + { + int nRet = CmdWalletSQLiteExport(strWalletSQLiteExport); + DBFlush(true); + return nRet; + } + // Berkeley DB reports failure by throwing, and CDB's constructor lets it // through. Nothing on this path caught anything, so an unreadable // blkindex.dat or wallet.dat -- one written by another platform's Berkeley diff --git a/src/makefile.mingw b/src/makefile.mingw index 2ff4e11..14e6b32 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -109,6 +109,7 @@ tests: test_bip32.exe bitflash.exe ./bitflash.exe -selftest=wallet-hd ./bitflash.exe -selftest=wallet-format ./bitflash.exe -selftest=wallet-sqlite + ./bitflash.exe -selftest=wallet-sqlite-migration ./bitflash.exe -selftest=wallet-crypto ./bitflash.exe -selftest=wallet-encrypt ./bitflash.exe -selftest=net-message diff --git a/src/selftest.cpp b/src/selftest.cpp index f9c4a8e..7a54271 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -1134,6 +1134,150 @@ static int RunWalletSQLiteSelfTest() return nFail == 0 ? 0 : 1; } +class CWalletRecordMapVisitor : public CWalletRecordVisitor +{ +public: + std::map, vector >& mapRecords; + + explicit CWalletRecordMapVisitor( + std::map, vector >& mapRecordsIn) + : mapRecords(mapRecordsIn) { } + + bool VisitWalletRecord(const CDataStream& ssKey, + const CDataStream& ssValue, + string& strErrorRet) + { + vector vchKey = DataStreamBytes(ssKey); + if (mapRecords.count(vchKey)) + { + strErrorRet = "duplicate serialized wallet key"; + return false; + } + mapRecords[vchKey] = DataStreamBytes(ssValue); + return true; + } +}; + +static int RunWalletSQLiteMigrationSelfTest() +{ + fflush(stdout); + printf("wallet-sqlite-migration self-test\n"); + + std::string tmp; + std::string cwd; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + if (!GetCurrentDir(cwd)) + { + printf(" FAIL could not read current directory\n"); + RemoveTree(tmp); + return 1; + } + + int nFail = 0; + printf(" temp root: %s\n", tmp.c_str()); + + try + { +#ifdef _WIN32 + string strExe = cwd + "\\bitflash.exe"; +#else + string strExe = cwd + "/bitflash-node"; +#endif + string strWalletDir = tmp + "/wallet"; + nFail += Check(MakeDirLocal(strWalletDir), + "temporary wallet directory can be created") ? 0 : 1; + + string strCreateOut = tmp + "/create.txt"; + vector vCreateArgs; + vCreateArgs.push_back("-datadir=" + strWalletDir); + vCreateArgs.push_back("-nomanagedtor"); + vCreateArgs.push_back("-nogui"); + vCreateArgs.push_back("-newphrase"); + int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); + nFail += Check(nCreateRet == 0 && + FileContainsText(strCreateOut, "Write these twelve words down"), + "a phrase wallet can be created for SQLite export") ? 0 : 1; + + string strSQLite = tmp + "/wallet.sqlite"; + string strExportOut = tmp + "/sqlite-export.txt"; + vector vExportArgs; + vExportArgs.push_back("-datadir=" + strWalletDir); + vExportArgs.push_back("-nomanagedtor"); + vExportArgs.push_back("-nogui"); + vExportArgs.push_back("-walletsqliteexport=" + strSQLite); + int nExportRet = RunBitflashChild(strExe, vExportArgs, NULL, &strExportOut); + nFail += Check(nExportRet == 0 && + FileExists(strSQLite.c_str()) && + FileContainsText(strExportOut, "SQLite wallet export written"), + "wallet.dat can be exported to SQLite") ? 0 : 1; + + string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt"; + int nOverwriteRet = + RunBitflashChild(strExe, vExportArgs, NULL, &strOverwriteOut); + nFail += Check(nOverwriteRet != 0 && + FileContainsText(strOverwriteOut, + "Refusing to overwrite existing SQLite wallet export"), + "SQLite wallet export refuses to overwrite") ? 0 : 1; + remove((strSQLite + "-wal").c_str()); + remove((strSQLite + "-shm").c_str()); + + std::map, vector > mapBDB; + string strOldDataDir = strSetDataDir; + string strScanError; + strSetDataDir = strWalletDir; + CWalletRecordMapVisitor visitor(mapBDB); + bool fScanOk = ScanWalletRecords(visitor, strScanError); + DBFlush(true); + strSetDataDir = strOldDataDir; + nFail += Check(fScanOk && !mapBDB.empty(), + "wallet.dat records can be scanned for comparison") ? 0 : 1; + + CWalletDBSQLite db; + string strError; + nFail += Check(db.Open(strSQLite, strError), + "exported SQLite wallet opens") ? 0 : 1; + int nSQLiteRecords = 0; + nFail += Check(db.CountRecords(nSQLiteRecords, strError) && + nSQLiteRecords == (int)mapBDB.size(), + "SQLite export has the same record count as wallet.dat") ? 0 : 1; + + bool fAllMatch = true; + for (std::map, vector >::const_iterator it = + mapBDB.begin(); it != mapBDB.end(); ++it) + { + vector vchSQLiteValue; + if (!db.ReadRecord(it->first, vchSQLiteValue, strError) || + vchSQLiteValue != it->second) + { + fAllMatch = false; + break; + } + } + nFail += Check(fAllMatch, + "SQLite export preserves every wallet.dat key/value byte-for-byte") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + static void ClearWalletRuntimeForTest() { CRITICAL_BLOCK(cs_mapKeys) @@ -2395,6 +2539,8 @@ int RunSelfTest(const std::string& name) return RunDbEnvReopenSelfTest(); if (name == "wallet-sqlite") return RunWalletSQLiteSelfTest(); + if (name == "wallet-sqlite-migration") + return RunWalletSQLiteMigrationSelfTest(); if (name == "wallet-crypto") return RunWalletCryptoSelfTest(); if (name == "wallet-encrypt") @@ -2415,6 +2561,6 @@ int RunSelfTest(const std::string& name) return RunManagedTorSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-crypto, wallet-encrypt, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); + printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); return 1; } diff --git a/src/wallet_sqlite.cpp b/src/wallet_sqlite.cpp index 4104ac6..4369af6 100644 --- a/src/wallet_sqlite.cpp +++ b/src/wallet_sqlite.cpp @@ -97,6 +97,16 @@ bool CWalletDBSQLite::RollbackTransaction(string& strError) return Exec("ROLLBACK;", strError); } +bool CWalletDBSQLite::Checkpoint(string& strError) +{ + if (!pdb) + { + strError = "SQLite wallet is not open"; + return false; + } + return Exec("PRAGMA wal_checkpoint(TRUNCATE);", strError); +} + bool CWalletDBSQLite::Exec(const char* pszSql, string& strError) { char* pszErr = NULL; diff --git a/src/wallet_sqlite.h b/src/wallet_sqlite.h index 9812d3a..5ef862d 100644 --- a/src/wallet_sqlite.h +++ b/src/wallet_sqlite.h @@ -18,6 +18,7 @@ public: bool BeginTransaction(string& strError); bool CommitTransaction(string& strError); bool RollbackTransaction(string& strError); + bool Checkpoint(string& strError); bool WriteRecord(const vector& vchKey, const vector& vchValue, diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 6fb09a3..20e7e33 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -12,6 +12,7 @@ #include "headers_core.h" #include "bip32.h" #include "walletcmd.h" +#include "wallet_sqlite.h" #undef printf @@ -790,6 +791,102 @@ int CmdWalletStorageAudit(const std::string& strJsonOut) return 0; } +static vector DataStreamBytesLocal(const CDataStream& ss) +{ + return vector(ss.begin(), ss.end()); +} + +class CWalletSQLiteExportVisitor : public CWalletRecordVisitor +{ +public: + CWalletDBSQLite& db; + int& nCopied; + + CWalletSQLiteExportVisitor(CWalletDBSQLite& dbIn, int& nCopiedIn) + : db(dbIn), nCopied(nCopiedIn) { } + + bool VisitWalletRecord(const CDataStream& ssKey, + const CDataStream& ssValue, + string& strErrorRet) + { + if (!db.WriteRecord(DataStreamBytesLocal(ssKey), + DataStreamBytesLocal(ssValue), + strErrorRet)) + return false; + nCopied++; + return true; + } +}; + +int CmdWalletSQLiteExport(const std::string& strDest) +{ + AttachTerminal(); + + if (strDest.empty()) + { + fprintf(stderr, "Missing SQLite wallet export path.\n"); + return 1; + } + if (FileExists(strDest.c_str())) + { + fprintf(stderr, "Refusing to overwrite existing SQLite wallet export: %s\n", + strDest.c_str()); + return 1; + } + + string strError; + CWalletDBSQLite db; + if (!db.Open(strDest, strError)) + { + fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str()); + return 1; + } + if (!db.BeginTransaction(strError)) + { + fprintf(stderr, "Cannot begin SQLite wallet export: %s\n", strError.c_str()); + return 1; + } + + int nCopied = 0; + CWalletSQLiteExportVisitor visitor(db, nCopied); + if (!ScanWalletRecords(visitor, strError)) + { + string strRollbackError; + db.RollbackTransaction(strRollbackError); + fprintf(stderr, "Cannot export wallet.dat to SQLite: %s\n", strError.c_str()); + return 1; + } + if (!db.CommitTransaction(strError)) + { + fprintf(stderr, "Cannot commit SQLite wallet export: %s\n", strError.c_str()); + return 1; + } + if (!db.Checkpoint(strError)) + { + fprintf(stderr, "Cannot checkpoint SQLite wallet export: %s\n", strError.c_str()); + return 1; + } + + int nCount = 0; + if (!db.CountRecords(nCount, strError)) + { + fprintf(stderr, "Cannot verify SQLite wallet export count: %s\n", + strError.c_str()); + return 1; + } + if (nCount != nCopied) + { + fprintf(stderr, "SQLite wallet export count mismatch: copied %d, stored %d\n", + nCopied, nCount); + return 1; + } + + printf("SQLite wallet export written to %s\n", strDest.c_str()); + printf(" records copied: %d\n", nCopied); + fflush(stdout); + return 0; +} + int CmdEncryptWallet(const std::string& strPassphrase) { AttachTerminal(); diff --git a/src/walletcmd.h b/src/walletcmd.h index 6077669..b1a29d2 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h @@ -70,6 +70,11 @@ int CmdWalletStorageAudit(const std::string& strJsonOut = ""); // or migration-unsafe mix of storage records. int CmdWalletStorageCheck(); +// Diagnostic/migration staging: copy every raw wallet.dat record into an +// experimental SQLite key/value store, preserving serialized bytes. This does +// not change which backend the runtime wallet opens. +int CmdWalletSQLiteExport(const std::string& strDest); + // Rewrite wallet.dat so private keys and the HD seed are encrypted with the // given passphrase. Returns 0 on success and exits without starting the node. int CmdEncryptWallet(const std::string& strPassphrase); -- cgit v1.2.3 From 28a2656d22a193fcd92d225964da5a01e8183623 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Thu, 20 Aug 2026 15:37:44 -0300 Subject: net: add isolated testnet parameters --- README.md | 3 ++- src/Makefile | 1 + src/main.cpp | 16 +++++++++--- src/main_gui.cpp | 7 ++++-- src/makefile.mingw | 1 + src/net.cpp | 32 +++++++++++++++++++++-- src/net.h | 20 +++++++++------ src/readme.txt | 3 ++- src/selftest.cpp | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 9 files changed, 140 insertions(+), 17 deletions(-) (limited to 'src/Makefile') diff --git a/README.md b/README.md index 94c727d..4843f5c 100644 --- a/README.md +++ b/README.md @@ -355,7 +355,8 @@ Other options worth knowing: ```bash -datadir=PATH # wallet and chain data elsewhere --port=N # P2P listen port, default 8433 +-testnet # isolated test network datadir, port 18433, and message magic +-port=N # P2P listen port, default 8433; testnet default 18433 -socks=HOST:PORT # SOCKS5 for outbound Nostr, .btf relay, and onion peer dials -tor[=HOST:PORT] # Tor mode; default local Tor SOCKS5 proxy is 127.0.0.1:9050 -managedtor[=PATH] # start Tor, create a hidden service, advertise its onion diff --git a/src/Makefile b/src/Makefile index b83ff58..ab0b307 100644 --- a/src/Makefile +++ b/src/Makefile @@ -193,6 +193,7 @@ tests: test_bip32 bitflash-node ./bitflash-node -selftest=wallet-crypto ./bitflash-node -selftest=wallet-encrypt ./bitflash-node -selftest=net-message + ./bitflash-node -selftest=network-params ./bitflash-node -selftest=consensus-limits ./bitflash-node -selftest=pool-stratum ./bitflash-node -selftest=parse-money diff --git a/src/main.cpp b/src/main.cpp index 5bd4028..6fd8c82 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2497,11 +2497,21 @@ string GetAppDir() { return "."; } - static bool fMkdirDone; - if (!fMkdirDone) + static string strCreatedBaseDir; + if (strCreatedBaseDir != strDir) { - fMkdirDone = true; _mkdir(strDir.c_str()); + strCreatedBaseDir = strDir; + } + if (IsTestNet()) + { + strDir += "/testnet"; + static string strCreatedTestnetDir; + if (strCreatedTestnetDir != strDir) + { + _mkdir(strDir.c_str()); + strCreatedTestnetDir = strDir; + } } return strDir; } diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 943799f..0a961bf 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -118,7 +118,7 @@ static void PrintUsage() printf(" wallet-crypto, wallet-encrypt, wallet-portability,\n"); printf(" net-message,\n"); printf(" consensus-limits, pool-stratum,\n"); - printf(" parse-money, socks5-proxy, or managed-tor\n"); + printf(" parse-money, network-params, socks5-proxy, or managed-tor\n"); printf("\n"); printf("Mining mode:\n"); printf(" /operator\n"); @@ -143,7 +143,8 @@ static void PrintUsage() printf(" /onionservice=HOST.onion:PORT (advertise this node's Tor hidden service)\n"); printf("\n"); printf("Network:\n"); - printf(" /port=N (P2P listen port, default 8433)\n"); + printf(" /testnet (isolated test network datadir, port, and magic)\n"); + printf(" /port=N (P2P listen port, default 8433; testnet 18433)\n"); printf(" /socks=HOST:PORT (SOCKS5 proxy for Nostr, .btf relay, and onion dials)\n"); printf(" /tor[=HOST:PORT] (Tor mode; default SOCKS5 proxy is 127.0.0.1:9050)\n"); printf(" /managedtor[=PATH] (start Tor, create a hidden service, advertise its onion)\n"); @@ -205,6 +206,8 @@ static void ParseStartupArguments(int argc, char* argv[]) if (arg(argc,argv,"/datadir") || arg(argc,argv,"-datadir")) strSetDataDir = argval2(argc, argv, "/datadir", "-datadir"); + SelectNetworkParams(arg(argc, argv, "/testnet") || arg(argc, argv, "-testnet")); + if (arg(argc,argv,"/debug") || arg(argc,argv,"-debug")) fDebug = true; diff --git a/src/makefile.mingw b/src/makefile.mingw index 55f186b..910e025 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -116,6 +116,7 @@ tests: test_bip32.exe bitflash.exe ./bitflash.exe -selftest=wallet-backend-default ./bitflash.exe -selftest=wallet-convert ./bitflash.exe -selftest=net-message + ./bitflash.exe -selftest=network-params ./bitflash.exe -selftest=consensus-limits ./bitflash.exe -selftest=pool-stratum ./bitflash.exe -selftest=socks5-proxy diff --git a/src/net.cpp b/src/net.cpp index 4603d2d..ecfd565 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -50,10 +50,14 @@ static int BtfPoll(BtfPollFd* pfd, unsigned int nfd, int nTimeoutMs) // // Global state variables // +static const char pchMainnetMessageStart[4] = { 0xbf, 0x20, 0x5c, 0xfd }; +static const char pchTestnetMessageStart[4] = { (char)0xce, (char)0xe2, (char)0xc0, (char)0xff }; +static bool fTestNet = false; +char pchMessageStart[4] = { 0xbf, 0x20, 0x5c, 0xfd }; bool fClient = false; uint64 nLocalServices = (fClient ? 0 : NODE_NETWORK); -CAddress addrLocalHost(0, DEFAULT_PORT, nLocalServices); -unsigned short nListenPort = DEFAULT_PORT; // local P2P port (tunable via /port) +CAddress addrLocalHost(0, GetDefaultPort(), nLocalServices); +unsigned short nListenPort = GetDefaultPort(); // local P2P port (tunable via /port) CNode nodeLocalHost(INVALID_SOCKET, CAddress("127.0.0.1", nLocalServices)); CNode* pnodeLocalHost = &nodeLocalHost; bool fShutdown = false; @@ -90,6 +94,29 @@ static int64 nBtfHandshakeSilent = 0; static string strBtfLastDialFail; static string strBtfLastHandshakeTimeout; +bool IsTestNet() +{ + return fTestNet; +} + +unsigned short GetDefaultPort() +{ + return htons(fTestNet ? TESTNET_PORT : MAINNET_PORT); +} + +void SelectNetworkParams(bool fTestNetIn) +{ + if (fTestNet == fTestNetIn) + return; + + fTestNet = fTestNetIn; + memcpy(pchMessageStart, + fTestNet ? pchTestnetMessageStart : pchMainnetMessageStart, + sizeof(pchMessageStart)); + nListenPort = GetDefaultPort(); + addrLocalHost.port = nListenPort; +} + void BtfChurnNoteResolveAttempt() { CRITICAL_BLOCK(cs_btfChurn) @@ -288,6 +315,7 @@ string GetDiagnosticsText() int nMedian = GetPeerMedianHeight(); str += "Bitflash node diagnostics\n"; + str += strprintf(" network %s\n", IsTestNet() ? "testnet" : "mainnet"); str += strprintf(" uptime %s\n", FormatAge(nNodeStartTime ? nNow - nNodeStartTime : -1).c_str()); if (nMedian < 0) diff --git a/src/net.h b/src/net.h index e3d4a93..56f1341 100644 --- a/src/net.h +++ b/src/net.h @@ -13,7 +13,8 @@ class CBlockIndex; -static const unsigned short DEFAULT_PORT = htons(8433); +static const unsigned short MAINNET_PORT = 8433; +static const unsigned short TESTNET_PORT = 18433; static const unsigned int PUBLISH_HOPS = 5; enum { @@ -26,6 +27,9 @@ enum bool GetMyExternalIP(unsigned int& ipRet); +bool IsTestNet(); +unsigned short GetDefaultPort(); +void SelectNetworkParams(bool fTestNetIn); CNode* ConnectNodeBtf(const string& strBtfAddr); CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, const string& strOnion, const unsigned char enc_pub[32], @@ -219,15 +223,17 @@ void CheckForShutdown(int n); // The message start string is designed to be unlikely to occur in normal data. // The characters are rarely used upper ascii, not valid as UTF-8, and produce // a large 4-byte int at any alignment. -// Magic bytes unique to the Bitflash network (0xbf = "BF"). Second byte bumped -// to 0x20 for the 2026 stable relaunch so the old test chain can't interfere. -static const char pchMessageStart[4] = { 0xbf, 0x20, 0x5c, 0xfd }; +// Magic bytes unique to the selected Bitflash network. Mainnet uses 0xbf = +// "BF"; testnet swaps the whole sequence so a test node cannot handshake with +// mainnet before consensus-level testnet activation exists. +extern char pchMessageStart[4]; class CMessageHeader { public: enum { COMMAND_SIZE=12 }; - char pchMessageStart[sizeof(::pchMessageStart)]; + enum { MESSAGE_START_SIZE=4 }; + char pchMessageStart[MESSAGE_START_SIZE]; char pchCommand[COMMAND_SIZE]; unsigned int nMessageSize; @@ -318,7 +324,7 @@ public: nServices = 0; memcpy(pchReserved, pchIPv4, sizeof(pchReserved)); ip = 0; - port = DEFAULT_PORT; + port = GetDefaultPort(); nTime = GetAdjustedTime(); nLastFailed = 0; } @@ -348,7 +354,7 @@ public: nServices = nServicesIn; memcpy(pchReserved, pchIPv4, sizeof(pchReserved)); ip = 0; - port = DEFAULT_PORT; + port = GetDefaultPort(); nTime = GetAdjustedTime(); nLastFailed = 0; diff --git a/src/readme.txt b/src/readme.txt index 752be66..5d62791 100644 --- a/src/readme.txt +++ b/src/readme.txt @@ -41,7 +41,8 @@ The resulting executable is src/bitflash.exe. Command-line options (in addition to the original ones) ------------------------------------------------------- - /port=N listen on P2P port N (default 8433) + /testnet use isolated test network datadir, port 18433, and message magic + /port=N listen on P2P port N (default 8433; testnet default 18433) /socks=HOST:PORT route outbound Nostr, .btf rendezvous, and direct onion peer dials through a SOCKS5 proxy, e.g. /socks=127.0.0.1:9050 for local Tor diff --git a/src/selftest.cpp b/src/selftest.cpp index dc9936e..30bd28e 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -3076,6 +3076,76 @@ static int RunNetMessageSelfTest() return nFail == 0 ? 0 : 1; } +static int RunNetworkParamsSelfTest() +{ + fflush(stdout); + printf("network-params self-test\n"); + + int nFail = 0; + std::string strOldDataDir = strSetDataDir; + try + { + SelectNetworkParams(false); + nFail += Check(!IsTestNet(), "starts on mainnet after explicit selection") ? 0 : 1; + nFail += Check(ntohs(GetDefaultPort()) == MAINNET_PORT, + "mainnet default P2P port is 8433") ? 0 : 1; + CAddress mainAddr("127.0.0.1"); + nFail += Check(mainAddr.port == htons(MAINNET_PORT), + "mainnet address parsing uses the mainnet default port") ? 0 : 1; + CMessageHeader mainHeader("ping", 0); + char mainMagic[4]; + memcpy(mainMagic, mainHeader.pchMessageStart, sizeof(mainMagic)); + + SelectNetworkParams(true); + nFail += Check(IsTestNet(), "testnet selection is sticky") ? 0 : 1; + nFail += Check(ntohs(GetDefaultPort()) == TESTNET_PORT, + "testnet default P2P port is 18433") ? 0 : 1; + CAddress testAddr("127.0.0.1"); + nFail += Check(testAddr.port == htons(TESTNET_PORT), + "testnet address parsing uses the testnet default port") ? 0 : 1; + CMessageHeader testHeader("ping", 0); + nFail += Check(memcmp(mainMagic, testHeader.pchMessageStart, sizeof(mainMagic)) != 0, + "testnet message magic differs from mainnet") ? 0 : 1; + + std::string tmp; + if (!MakeTempDir(tmp)) + { + nFail += Check(false, "creates a temporary data directory") ? 0 : 1; + } + else + { + strSetDataDir = tmp; + SelectNetworkParams(false); + std::string mainDir = GetAppDir(); + SelectNetworkParams(true); + std::string testDir = GetAppDir(); + nFail += Check(mainDir == tmp, + "mainnet uses the selected data directory") ? 0 : 1; + nFail += Check(testDir == tmp + "/testnet", + "testnet stores data below a testnet subdirectory") ? 0 : 1; + RemoveTree(tmp); + } + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + strSetDataDir = strOldDataDir; + SelectNetworkParams(false); + + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + static CBlock MakeSizedConsensusBlock(size_t nScriptBytes) { CTransaction tx; @@ -3517,6 +3587,8 @@ int RunSelfTest(const std::string& name) return RunWalletPortabilitySelfTest(); if (name == "net-message") return RunNetMessageSelfTest(); + if (name == "network-params") + return RunNetworkParamsSelfTest(); if (name == "consensus-limits") return RunConsensusLimitsSelfTest(); if (name == "pool-stratum") @@ -3529,6 +3601,6 @@ int RunSelfTest(const std::string& name) return RunManagedTorSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-sqlite-encrypt, wallet-backend-default, wallet-convert, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); + printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-sqlite-encrypt, wallet-backend-default, wallet-convert, wallet-portability, net-message, network-params, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); return 1; } -- cgit v1.2.3 From a7744058c49b1ba2c605f9e8380d9c3e8665bbc0 Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Wed, 19 Aug 2026 18:17:06 +0000 Subject: net: stop serving the 2009 marketplace to anonymous peers Four inherited handlers took attacker-controlled input and wrote to disk with nothing authorising it. None of them had a caller left anywhere in the tree, so all four were reachable only from the outside. checkorder called GenerateNewKey() -- AddKey(), a wallet.dat write -- for any peer that asked, and handed back the pubkey. Its comment claims the cache keeps one key per ip, and on a 2009 network of routable addresses it did. Not here: inbound peers arrive through a rendezvous tunnel and are tagged by BtfMarkerAddr() over three random bytes, so addr.ip is fresh every connection and the cache never hits. Reconnect in a loop and the node mints a key per connection, none of them in any backup taken beforehand, which for a pre-BIP32 wallet is how coins go missing. The map holding them was static and never trimmed. submitorder AddToWallet()'d a CWalletTx from any peer that sent one. It had to be a valid transaction; it did not have to be ours. review checked a signature the sender made with its own key, which proves possession and authorises nothing, then appended to reviews.dat by reading the whole vector, pushing one entry and writing it all back -- quadratic disk on an unbounded map off the socket -- then walked a link graph the same peer supplied and relayed the message to every other node. reply dispatched into CRequestTracker. Nothing ever called PushRequest, so the table was permanently empty and the handler was a parser and nothing else. Removed with the subsystem behind them: market.cpp, CProduct, CReview, CUser, CReviewDB, the advert publish templates and the subscribe channel machinery, none of which had an inbound handler either. MSG_REVIEW, MSG_PRODUCT and MSG_TABLE stay in the inv enum, unserved: CInv puts type on the wire as an integer and MSG_TX and MSG_BLOCK are shared with every other node. Unknown commands are already ignored for extensibility, so peers still running these lose nothing but an answer they never asked for. Verified by a clean build of bitflash-node. (cherry picked from commit ad9c393866c007a115f32d70a9017ce91066a7d1) --- src/Makefile | 4 +- src/db.cpp | 16 +--- src/db.h | 23 ----- src/headers_core.h | 1 - src/main.cpp | 115 ++++++++--------------- src/makefile.mingw | 4 +- src/market.cpp | 264 ----------------------------------------------------- src/market.h | 182 ------------------------------------ src/net.cpp | 109 ++-------------------- src/net.h | 138 ++++------------------------ 10 files changed, 69 insertions(+), 787 deletions(-) delete mode 100644 src/market.cpp delete mode 100644 src/market.h (limited to 'src/Makefile') diff --git a/src/Makefile b/src/Makefile index 3577dd6..171afe0 100644 --- a/src/Makefile +++ b/src/Makefile @@ -48,14 +48,14 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \ # binary. Nothing crashed here, and nothing would say so if it had. HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ - randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ + randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ selftest.h walletcmd.h compat.h sockcount.h font_roboto.h IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ - obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ + obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ obj/selftest.o obj/bip32.o obj/walletcmd.o diff --git a/src/db.cpp b/src/db.cpp index c8642e9..6f9461b 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -598,20 +598,12 @@ bool CTxDB::LoadBlockIndex() // -// CReviewDB +// CReviewDB is gone with the "review" handler. Its write path was +// read-whole-vector / append-one / write-whole-vector, driven straight from an +// unauthenticated peer, which is quadratic disk on an unbounded record. +// Existing reviews.dat files are simply left alone and never opened. // -bool CReviewDB::ReadReviews(uint256 hash, vector& vReviews) -{ - vReviews.size(); // msvc workaround, just need to do anything with vReviews - return Read(make_pair(string("reviews"), hash), vReviews); -} - -bool CReviewDB::WriteReviews(uint256 hash, const vector& vReviews) -{ - return Write(make_pair(string("reviews"), hash), vReviews); -} - diff --git a/src/db.h b/src/db.h index 4fb5cfd..37a252f 100644 --- a/src/db.h +++ b/src/db.h @@ -8,8 +8,6 @@ class CTxIndex; class CDiskBlockIndex; class CDiskTxPos; class COutPoint; -class CUser; -class CReview; class CAddress; class CWalletTx; @@ -314,27 +312,6 @@ public: -class CReviewDB : public CDB -{ -public: - CReviewDB(const char* pszMode="r+", bool fTxn=false) : CDB("reviews.dat", pszMode, fTxn) { } -private: - CReviewDB(const CReviewDB&); - void operator=(const CReviewDB&); -public: - bool ReadUser(uint256 hash, CUser& user) - { - return Read(make_pair(string("user"), hash), user); - } - - bool WriteUser(uint256 hash, const CUser& user) - { - return Write(make_pair(string("user"), hash), user); - } - - bool ReadReviews(uint256 hash, vector& vReviews); - bool WriteReviews(uint256 hash, const vector& vReviews); -}; diff --git a/src/headers_core.h b/src/headers_core.h index 6c0c602..6a60b52 100644 --- a/src/headers_core.h +++ b/src/headers_core.h @@ -66,7 +66,6 @@ inline typename std::common_type::type max(const T1& a, const T2& b) #include "nostr.h" #include "randomx_pow.h" #include "main.h" -#include "market.h" // GUI callback -- implemented in gui.cpp (or as no-op in headless builds) void MainFrameRepaint(); diff --git a/src/main.cpp b/src/main.cpp index 62068d2..efff96c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2782,9 +2782,10 @@ bool AlreadyHave(CTxDB& txdb, const CInv& inv) { case MSG_TX: return mapTransactions.count(inv.hash) || txdb.ContainsTx(inv.hash); case MSG_BLOCK: return mapBlockIndex.count(inv.hash) || mapOrphanBlocks.count(inv.hash); - case MSG_REVIEW: return true; - case MSG_PRODUCT: return mapProducts.count(inv.hash); } + // MSG_REVIEW / MSG_PRODUCT / MSG_TABLE are the 2009 marketplace inventory + // types. Nothing announces or serves them any more (see ProcessMessage), + // so they fall through to the default and are never requested. // Don't know what it is, just say we already got one return true; } @@ -2942,7 +2943,6 @@ bool ProcessMessages(CNode* pfrom) bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) { - static map > mapReuseKey; if (LogAcceptsCategory("net")) { printf("received: %-12s (%d bytes) ", strCommand.c_str(), (int)vRecv.size()); for (int i = 0; i < min(vRecv.size(), (unsigned int)25); i++) printf("%02x ", vRecv[i] & 0xff); printf("\n"); } if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0) { @@ -3181,22 +3181,18 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) } - else if (strCommand == "review") - { - CDataStream vMsg(vRecv); - CReview review; - vRecv >> review; - - CInv inv(MSG_REVIEW, review.GetHash()); - pfrom->AddInventoryKnown(inv); - - if (review.AcceptReview()) - { - // Relay the original message as-is in case it's a higher version than we know how to parse - RelayMessage(inv, vMsg); - mapAlreadyAskedFor.erase(inv); - } - } + // "review" used to be handled here. It took a CReview off the wire from an + // unauthenticated peer, checked a signature the sender had made with a key + // of its own choosing -- which proves possession of that key and authorises + // nothing -- and then appended the message to reviews.dat by reading the + // whole vector for the target hash, pushing one entry, and writing all of + // it back. That is quadratic disk for a stream an attacker controls, on a + // record whose mapValue is an unbounded map read straight + // from the socket, followed by AddAtomsAndPropagate walking a link graph + // the same peer supplied, and finally RelayMessage forwarding it to every + // other node. Unbounded remote disk growth with network amplification and + // no authorisation anywhere in the path. The marketplace it belonged to has + // not existed since 2009; the handler is gone rather than bounded. else if (strCommand == "block") @@ -3229,65 +3225,28 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) } - else if (strCommand == "checkorder") - { - uint256 hashReply; - CWalletTx order; - vRecv >> hashReply >> order; - - /// we have a chance to check the order here - - // Keep giving the same key to the same ip until they use it - if (!mapReuseKey.count(pfrom->addr.ip)) - mapReuseKey[pfrom->addr.ip] = GenerateNewKey(); - - // Send back approval of order and pubkey to use - CScript scriptPubKey; - scriptPubKey << mapReuseKey[pfrom->addr.ip] << OP_CHECKSIG; - pfrom->PushMessage("reply", hashReply, (int)0, scriptPubKey); - } - - - else if (strCommand == "submitorder") - { - uint256 hashReply; - CWalletTx wtxNew; - vRecv >> hashReply >> wtxNew; - - // Broadcast - if (!wtxNew.AcceptWalletTransaction()) - { - pfrom->PushMessage("reply", hashReply, (int)1); - return error("submitorder AcceptWalletTransaction() failed, returning error 1"); - } - wtxNew.fTimeReceivedIsTxTime = true; - AddToWallet(wtxNew); - wtxNew.RelayWalletTransaction(); - mapReuseKey.erase(pfrom->addr.ip); - - // Send back confirmation - pfrom->PushMessage("reply", hashReply, (int)0); - } - - - else if (strCommand == "reply") - { - uint256 hashReply; - vRecv >> hashReply; - - CRequestTracker tracker; - CRITICAL_BLOCK(pfrom->cs_mapRequests) - { - map::iterator mi = pfrom->mapRequests.find(hashReply); - if (mi != pfrom->mapRequests.end()) - { - tracker = (*mi).second; - pfrom->mapRequests.erase(mi); - } - } - if (!tracker.IsNull()) - tracker.fn(tracker.param1, vRecv); - } + // "checkorder", "submitorder" and "reply" -- the 2009 buy-from-a-merchant + // exchange -- used to be handled here. All three are gone. + // + // "checkorder" called GenerateNewKey() for any peer that asked, which is + // AddKey() and a wallet.dat write, and handed the pubkey back. The comment + // said "keep giving the same key to the same ip until they use it", and on + // a 2009 network of routable IPs that held. It does not hold here: every + // inbound peer arrives through a rendezvous tunnel and is tagged with + // BtfMarkerAddr() over three random bytes (net.cpp), so addr.ip is freshly + // random per connection and the cache never hits. Reconnect in a loop and + // the node mints a key per connection -- unbounded wallet growth, unbounded + // growth of the static map that held them, and every one of those keys + // missing from every backup taken beforehand, which for this pre-BIP32 + // wallet is how coins are lost. + // + // "submitorder" took a CWalletTx from an unauthenticated peer and, if it + // was merely a valid transaction, AddToWallet()'d it. Valid is not the same + // as ours: any peer could write chosen transactions into wallet.dat. + // + // "reply" dispatched to a CRequestTracker callback. Nothing in this tree + // has ever called PushRequest, so mapRequests was permanently empty and the + // handler could only ever be a parser for hostile input. else if (strCommand == "ping") diff --git a/src/makefile.mingw b/src/makefile.mingw index e076c49..72f8bbf 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -40,7 +40,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ - obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ + obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ obj/selftest.o obj/bip32.o obj/walletcmd.o @@ -55,7 +55,7 @@ GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o # binary. Nothing crashed here, and nothing would say so if it had. HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ - randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ + randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ selftest.h walletcmd.h sockcount.h compat.h font_roboto.h all: bitflash.exe diff --git a/src/market.cpp b/src/market.cpp deleted file mode 100644 index e5d34f4..0000000 --- a/src/market.cpp +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright (c) 2009 Satoshi Nakamoto -// Distributed under the MIT/X11 software license, see the accompanying -// file license.txt or http://www.opensource.org/licenses/mit-license.php. - -#include "headers.h" - - - - - - - - - - -// -// Global state variables -// - -//// later figure out how these are persisted -map mapMyProducts; - - - - -map mapProducts; -CCriticalSection cs_mapProducts; - -bool AdvertInsert(const CProduct& product) -{ - uint256 hash = product.GetHash(); - bool fNew = false; - bool fUpdated = false; - - CRITICAL_BLOCK(cs_mapProducts) - { - // Insert or find existing product - pair::iterator, bool> item = mapProducts.insert(make_pair(hash, product)); - CProduct* pproduct = &(*(item.first)).second; - fNew = item.second; - - // Update if newer - if (product.nSequence > pproduct->nSequence) - { - *pproduct = product; - fUpdated = true; - } - } - - //if (fNew) - // NotifyProductAdded(hash); - //else if (fUpdated) - // NotifyProductUpdated(hash); - - return (fNew || fUpdated); -} - -void AdvertErase(const CProduct& product) -{ - uint256 hash = product.GetHash(); - CRITICAL_BLOCK(cs_mapProducts) - mapProducts.erase(hash); - //NotifyProductDeleted(hash); -} - - - - - - - - - - - - - - - - - - - -template -unsigned int Union(T& v1, T& v2) -{ - // v1 = v1 union v2 - // v1 and v2 must be sorted - // returns the number of elements added to v1 - - ///// need to check that this is equivalent, then delete this comment - //vector vUnion(v1.size() + v2.size()); - //vUnion.erase(set_union(v1.begin(), v1.end(), - // v2.begin(), v2.end(), - // vUnion.begin()), - // vUnion.end()); - - T vUnion; - vUnion.reserve(v1.size() + v2.size()); - set_union(v1.begin(), v1.end(), - v2.begin(), v2.end(), - back_inserter(vUnion)); - unsigned int nAdded = vUnion.size() - v1.size(); - if (nAdded > 0) - v1 = vUnion; - return nAdded; -} - -void CUser::AddAtom(unsigned short nAtom, bool fOrigin) -{ - // Ignore duplicates - if (binary_search(vAtomsIn.begin(), vAtomsIn.end(), nAtom) || - find(vAtomsNew.begin(), vAtomsNew.end(), nAtom) != vAtomsNew.end()) - return; - - //// instead of zero atom, should change to free atom that propagates, - //// limited to lower than a certain value like 5 so conflicts quickly - // The zero atom never propagates, - // new atoms always propagate through the user that created them - if (nAtom == 0 || fOrigin) - { - vector vTmp(1, nAtom); - Union(vAtomsIn, vTmp); - if (fOrigin) - vAtomsOut.push_back(nAtom); - return; - } - - vAtomsNew.push_back(nAtom); - - if (vAtomsNew.size() >= nFlowthroughRate || vAtomsOut.empty()) - { - // Select atom to flow through to vAtomsOut - vAtomsOut.push_back(vAtomsNew[GetRand(vAtomsNew.size())]); - - // Merge vAtomsNew into vAtomsIn - sort(vAtomsNew.begin(), vAtomsNew.end()); - Union(vAtomsIn, vAtomsNew); - vAtomsNew.clear(); - } -} - -bool AddAtomsAndPropagate(uint256 hashUserStart, const vector& vAtoms, bool fOrigin) -{ - CReviewDB reviewdb; - map > pmapPropagate[2]; - pmapPropagate[0][hashUserStart] = vAtoms; - - for (int side = 0; !pmapPropagate[side].empty(); side = 1 - side) - { - map >& mapFrom = pmapPropagate[side]; - map >& mapTo = pmapPropagate[1 - side]; - - for (map >::iterator mi = mapFrom.begin(); mi != mapFrom.end(); ++mi) - { - const uint256& hashUser = (*mi).first; - const vector& vReceived = (*mi).second; - - ///// this would be a lot easier on the database if it put the new atom at the beginning of the list, - ///// so the change would be right next to the vector size. - - // Read user - CUser user; - reviewdb.ReadUser(hashUser, user); - unsigned int nIn = user.vAtomsIn.size(); - unsigned int nNew = user.vAtomsNew.size(); - unsigned int nOut = user.vAtomsOut.size(); - - // Add atoms received - foreach(unsigned short nAtom, vReceived) - user.AddAtom(nAtom, fOrigin); - fOrigin = false; - - // Don't bother writing to disk if no changes - if (user.vAtomsIn.size() == nIn && user.vAtomsNew.size() == nNew) - continue; - - // Propagate - if (user.vAtomsOut.size() > nOut) - foreach(const uint256& hash, user.vLinksOut) - mapTo[hash].insert(mapTo[hash].end(), user.vAtomsOut.begin() + nOut, user.vAtomsOut.end()); - - // Write back - if (!reviewdb.WriteUser(hashUser, user)) - return false; - } - mapFrom.clear(); - } - return true; -} - - - - - - -bool CReview::AcceptReview() -{ - // Timestamp - nTime = GetTime(); - - // Check signature - if (!CKey::Verify(vchPubKeyFrom, GetSigHash(), vchSig)) - return false; - - CReviewDB reviewdb; - - // Add review text to recipient - vector vReviews; - reviewdb.ReadReviews(hashTo, vReviews); - vReviews.push_back(*this); - if (!reviewdb.WriteReviews(hashTo, vReviews)) - return false; - - // Add link from sender - CUser user; - uint256 hashFrom = Hash(vchPubKeyFrom.begin(), vchPubKeyFrom.end()); - reviewdb.ReadUser(hashFrom, user); - user.vLinksOut.push_back(hashTo); - if (!reviewdb.WriteUser(hashFrom, user)) - return false; - - reviewdb.Close(); - - // Propagate atoms to recipient - vector vZeroAtom(1, 0); - if (!AddAtomsAndPropagate(hashTo, user.vAtomsOut.size() ? user.vAtomsOut : vZeroAtom, false)) - return false; - - return true; -} - - - - - -bool CProduct::CheckSignature() -{ - return (CKey::Verify(vchPubKeyFrom, GetSigHash(), vchSig)); -} - -bool CProduct::CheckProduct() -{ - if (!CheckSignature()) - return false; - - // Make sure it's a summary product - if (!mapDetails.empty() || !vOrderForm.empty()) - return false; - - // Look up seller's atom count - CReviewDB reviewdb("r"); - CUser user; - reviewdb.ReadUser(GetUserHash(), user); - nAtoms = user.GetAtomCount(); - reviewdb.Close(); - - ////// delme, this is now done by AdvertInsert - //// Store to memory - //CRITICAL_BLOCK(cs_mapProducts) - // mapProducts[GetHash()] = *this; - - return true; -} diff --git a/src/market.h b/src/market.h deleted file mode 100644 index b5c8b3b..0000000 --- a/src/market.h +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) 2009 Satoshi Nakamoto -// Distributed under the MIT/X11 software license, see the accompanying -// file license.txt or http://www.opensource.org/licenses/mit-license.php. - -class CUser; -class CReview; -class CProduct; - -static const unsigned int nFlowthroughRate = 2; - - - - -bool AdvertInsert(const CProduct& product); -void AdvertErase(const CProduct& product); -bool AddAtomsAndPropagate(uint256 hashUserStart, const vector& vAtoms, bool fOrigin); - - - - - - - - -class CUser -{ -public: - vector vAtomsIn; - vector vAtomsNew; - vector vAtomsOut; - vector vLinksOut; - - CUser() - { - } - - IMPLEMENT_SERIALIZE - ( - if (!(nType & SER_GETHASH)) - READWRITE(nVersion); - READWRITE(vAtomsIn); - READWRITE(vAtomsNew); - READWRITE(vAtomsOut); - READWRITE(vLinksOut); - ) - - void SetNull() - { - vAtomsIn.clear(); - vAtomsNew.clear(); - vAtomsOut.clear(); - vLinksOut.clear(); - } - - uint256 GetHash() const { return SerializeHash(*this); } - - - int GetAtomCount() const - { - return (vAtomsIn.size() + vAtomsNew.size()); - } - - void AddAtom(unsigned short nAtom, bool fOrigin); -}; - - - - - - - -class CReview -{ -public: - int nVersion; - uint256 hashTo; - map mapValue; - vector vchPubKeyFrom; - vector vchSig; - - // memory only - unsigned int nTime; - int nAtoms; - - - CReview() - { - nVersion = 1; - hashTo = 0; - nTime = 0; - nAtoms = 0; - } - - IMPLEMENT_SERIALIZE - ( - READWRITE(this->nVersion); - nVersion = this->nVersion; - if (!(nType & SER_DISK)) - READWRITE(hashTo); - READWRITE(mapValue); - READWRITE(vchPubKeyFrom); - if (!(nType & SER_GETHASH)) - READWRITE(vchSig); - ) - - uint256 GetHash() const { return SerializeHash(*this); } - uint256 GetSigHash() const { return SerializeHash(*this, SER_GETHASH|SER_SKIPSIG); } - uint256 GetUserHash() const { return Hash(vchPubKeyFrom.begin(), vchPubKeyFrom.end()); } - - - bool AcceptReview(); -}; - - - - - - - -class CProduct -{ -public: - int nVersion; - CAddress addr; - map mapValue; - map mapDetails; - vector > vOrderForm; - unsigned int nSequence; - vector vchPubKeyFrom; - vector vchSig; - - // disk only - int nAtoms; - - // memory only - set setSources; - - CProduct() - { - nVersion = 1; - nAtoms = 0; - nSequence = 0; - } - - IMPLEMENT_SERIALIZE - ( - READWRITE(this->nVersion); - nVersion = this->nVersion; - READWRITE(addr); - READWRITE(mapValue); - if (!(nType & SER_GETHASH)) - { - READWRITE(mapDetails); - READWRITE(vOrderForm); - READWRITE(nSequence); - } - READWRITE(vchPubKeyFrom); - if (!(nType & SER_GETHASH)) - READWRITE(vchSig); - if (nType & SER_DISK) - READWRITE(nAtoms); - ) - - uint256 GetHash() const { return SerializeHash(*this); } - uint256 GetSigHash() const { return SerializeHash(*this, SER_GETHASH|SER_SKIPSIG); } - uint256 GetUserHash() const { return Hash(vchPubKeyFrom.begin(), vchPubKeyFrom.end()); } - - - bool CheckSignature(); - bool CheckProduct(); -}; - - - - - - - - -extern map mapProducts; -extern CCriticalSection cs_mapProducts; -extern map mapMyProducts; diff --git a/src/net.cpp b/src/net.cpp index 72a7fda..e08f6f7 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -444,28 +444,9 @@ bool GetMyExternalIP(unsigned int& ipRet) -void AbandonRequests(void (*fn)(void*, CDataStream&), void* param1) -{ - // If the dialog might get closed before the reply comes back, - // call this in the destructor so it doesn't get called after it's deleted. - CRITICAL_BLOCK(cs_vNodes) - { - foreach(CNode* pnode, vNodes) - { - CRITICAL_BLOCK(pnode->cs_mapRequests) - { - for (map::iterator mi = pnode->mapRequests.begin(); mi != pnode->mapRequests.end();) - { - CRequestTracker& tracker = (*mi).second; - if (tracker.fn == fn && tracker.param1 == param1) - pnode->mapRequests.erase(mi++); - else - mi++; - } - } - } - } -} +// AbandonRequests() lived here, sweeping CNode::mapRequests for a callback a +// closing dialog had registered. Nothing ever registered one and nothing ever +// called this, so it went with CRequestTracker. @@ -473,25 +454,10 @@ void AbandonRequests(void (*fn)(void*, CDataStream&), void* param1) -// -// Subscription methods for the broadcast and subscription system. -// Channel numbers are message numbers, i.e. MSG_TABLE and MSG_PRODUCT. -// -// The subscription system uses a meet-in-the-middle strategy. -// With 100,000 nodes, if senders broadcast to 1000 random nodes and receivers -// subscribe to 1000 random nodes, 99.995% (1 - 0.99^1000) of messages will get through. -// - -bool AnySubscribed(unsigned int nChannel) -{ - if (pnodeLocalHost->IsSubscribed(nChannel)) - return true; - CRITICAL_BLOCK(cs_vNodes) - foreach(CNode* pnode, vNodes) - if (pnode->IsSubscribed(nChannel)) - return true; - return false; -} +// The broadcast/subscribe channel system (AnySubscribed, CNode::IsSubscribed, +// Subscribe, CancelSubscribe) stood here. Its channels were MSG_TABLE and +// MSG_PRODUCT, there was never an inbound handler for "subscribe", +// "sub-cancel", "publish" or "pub-cancel", and the objects it carried are gone. void CNode::PushGetBlocks(CBlockIndex* pindexBegin, uint256 hashEnd) { @@ -504,55 +470,6 @@ void CNode::PushGetBlocks(CBlockIndex* pindexBegin, uint256 hashEnd) PushMessage("getblocks", CBlockLocator(pindexBegin), hashEnd); } -bool CNode::IsSubscribed(unsigned int nChannel) -{ - if (nChannel >= vfSubscribe.size()) - return false; - return vfSubscribe[nChannel]; -} - -void CNode::Subscribe(unsigned int nChannel, unsigned int nHops) -{ - if (nChannel >= vfSubscribe.size()) - return; - - if (!AnySubscribed(nChannel)) - { - // Relay subscribe - CRITICAL_BLOCK(cs_vNodes) - foreach(CNode* pnode, vNodes) - if (pnode != this) - pnode->PushMessage("subscribe", nChannel, nHops); - } - - vfSubscribe[nChannel] = true; -} - -void CNode::CancelSubscribe(unsigned int nChannel) -{ - if (nChannel >= vfSubscribe.size()) - return; - - // Prevent from relaying cancel if wasn't subscribed - if (!vfSubscribe[nChannel]) - return; - vfSubscribe[nChannel] = false; - - if (!AnySubscribed(nChannel)) - { - // Relay subscription cancel - CRITICAL_BLOCK(cs_vNodes) - foreach(CNode* pnode, vNodes) - if (pnode != this) - pnode->PushMessage("sub-cancel", nChannel); - - // Clear memory, no longer subscribed - if (nChannel == MSG_PRODUCT) - CRITICAL_BLOCK(cs_mapProducts) - mapProducts.clear(); - } -} - @@ -1215,17 +1132,6 @@ void CNode::Disconnect() BtfCloseSocket(hSocket); hSocket = INVALID_SOCKET; - // All of a nodes broadcasts and subscriptions are automatically torn down - // when it goes down, so a node has to stay up to keep its broadcast going. - - CRITICAL_BLOCK(cs_mapProducts) - for (map::iterator mi = mapProducts.begin(); mi != mapProducts.end();) - AdvertRemoveSource(this, MSG_PRODUCT, 0, (*(mi++)).second); - - // Cancel subscriptions - for (unsigned int nChannel = 0; nChannel < vfSubscribe.size(); nChannel++) - if (vfSubscribe[nChannel]) - CancelSubscribe(nChannel); } @@ -1350,7 +1256,6 @@ void ThreadSocketHandler2(void* parg) bool fDelete = false; TRY_CRITICAL_BLOCK(pnode->cs_vSend) TRY_CRITICAL_BLOCK(pnode->cs_vRecv) - TRY_CRITICAL_BLOCK(pnode->cs_mapRequests) TRY_CRITICAL_BLOCK(pnode->cs_inventory) fDelete = true; if (fDelete) diff --git a/src/net.h b/src/net.h index 0317676..8b0245a 100644 --- a/src/net.h +++ b/src/net.h @@ -5,7 +5,6 @@ class CMessageHeader; class CAddress; class CInv; -class CRequestTracker; class CNode; // Defined in main.h, which is included after this file. Only ever used here as // a pointer, so the forward declaration is enough. @@ -14,7 +13,6 @@ class CBlockIndex; static const unsigned short DEFAULT_PORT = htons(8433); -static const unsigned int PUBLISH_HOPS = 5; enum { NODE_NETWORK = (1 << 0), @@ -214,8 +212,6 @@ void BtfChurnNoteHandshakeTimeout(const string& strBtfAddr, const string& strMee void BtfPexCollect(std::vector& vDescOut); // Verify descriptors a peer sent and remember the good ones. Returns how many. int BtfPexAccept(const std::vector& vDesc); -void AbandonRequests(void (*fn)(void*, CDataStream&), void* param1); -bool AnySubscribed(unsigned int nChannel); void ThreadBitcoinMiner(void* parg); // Starts MinerThreadCount() miners; returns how many came up. int StartMinerThreads(); @@ -489,13 +485,19 @@ public: +// MSG_REVIEW, MSG_PRODUCT and MSG_TABLE no longer have a handler, a producer +// or a store -- the marketplace they belonged to is gone. They stay in the +// enum, and their names stay in ppszTypeName below, because CInv serialises +// `type` as a plain integer: dropping them would renumber nothing today but +// would make it tempting to renumber later, and MSG_TX and MSG_BLOCK are wire +// values shared with every other node. Reserved, never emitted, never served. enum { MSG_TX = 1, MSG_BLOCK, - MSG_REVIEW, - MSG_PRODUCT, - MSG_TABLE, + MSG_REVIEW, // reserved, dead + MSG_PRODUCT, // reserved, dead + MSG_TABLE, // reserved, dead }; static const char* ppszTypeName[] = @@ -582,23 +584,9 @@ public: -class CRequestTracker -{ -public: - void (*fn)(void*, CDataStream&); - void* param1; - - explicit CRequestTracker(void (*fnIn)(void*, CDataStream&)=NULL, void* param1In=NULL) - { - fn = fnIn; - param1 = param1In; - } - - bool IsNull() - { - return fn == NULL; - } -}; +// CRequestTracker and the PushRequest/"reply" round trip lived here. Nothing +// in this tree ever called PushRequest, so the table was always empty and the +// "reply" handler was pure inbound parser. Both removed. @@ -644,9 +632,6 @@ protected: public: int64 nReleaseTime; int64 nDisconnectSince; - map mapRequests; - CCriticalSection cs_mapRequests; - // inventory based relay set setInventoryKnown; set setInventoryKnown2; @@ -654,9 +639,6 @@ public: CCriticalSection cs_inventory; multimap mapAskFor; - // publish and subscription - vector vfSubscribe; - // Last "btfpeers" we accepted from this node, to rate-limit the exchange. int64 nLastPexRecv; @@ -723,7 +705,6 @@ public: strBtfAddr.clear(); strBtfMeeting.clear(); nStartingHeight = -1; - vfSubscribe.assign(256, false); // Push a version message /// when NTP implemented, change to just nTime = GetAdjustedTime() @@ -1002,49 +983,6 @@ public: } - void PushRequest(const char* pszCommand, - void (*fn)(void*, CDataStream&), void* param1) - { - uint256 hashReply; - RAND_bytes((unsigned char*)&hashReply, sizeof(hashReply)); - - CRITICAL_BLOCK(cs_mapRequests) - mapRequests[hashReply] = CRequestTracker(fn, param1); - - PushMessage(pszCommand, hashReply); - } - - template - void PushRequest(const char* pszCommand, const T1& a1, - void (*fn)(void*, CDataStream&), void* param1) - { - uint256 hashReply; - RAND_bytes((unsigned char*)&hashReply, sizeof(hashReply)); - - CRITICAL_BLOCK(cs_mapRequests) - mapRequests[hashReply] = CRequestTracker(fn, param1); - - PushMessage(pszCommand, hashReply, a1); - } - - template - void PushRequest(const char* pszCommand, const T1& a1, const T2& a2, - void (*fn)(void*, CDataStream&), void* param1) - { - uint256 hashReply; - RAND_bytes((unsigned char*)&hashReply, sizeof(hashReply)); - - CRITICAL_BLOCK(cs_mapRequests) - mapRequests[hashReply] = CRequestTracker(fn, param1); - - PushMessage(pszCommand, hashReply, a1, a2); - } - - - - bool IsSubscribed(unsigned int nChannel); - void Subscribe(unsigned int nChannel, unsigned int nHops=0); - void CancelSubscribe(unsigned int nChannel); void Disconnect(); }; @@ -1101,50 +1039,8 @@ inline void RelayMessage<>(const CInv& inv, const CDataStream& ss) -// -// Templates for the publish and subscription system. -// The object being published as T& obj needs to have: -// a set setSources member -// specializations of AdvertInsert and AdvertErase -// Currently implemented for CTable and CProduct. -// - -template -void AdvertStartPublish(CNode* pfrom, unsigned int nChannel, unsigned int nHops, T& obj) -{ - // Add to sources - obj.setSources.insert(pfrom->addr.ip); - - if (!AdvertInsert(obj)) - return; - - // Relay - CRITICAL_BLOCK(cs_vNodes) - foreach(CNode* pnode, vNodes) - if (pnode != pfrom && (nHops < PUBLISH_HOPS || pnode->IsSubscribed(nChannel))) - pnode->PushMessage("publish", nChannel, nHops, obj); -} - -template -void AdvertStopPublish(CNode* pfrom, unsigned int nChannel, unsigned int nHops, T& obj) -{ - uint256 hash = obj.GetHash(); - - CRITICAL_BLOCK(cs_vNodes) - foreach(CNode* pnode, vNodes) - if (pnode != pfrom && (nHops < PUBLISH_HOPS || pnode->IsSubscribed(nChannel))) - pnode->PushMessage("pub-cancel", nChannel, nHops, hash); - - AdvertErase(obj); -} - -template -void AdvertRemoveSource(CNode* pfrom, unsigned int nChannel, unsigned int nHops, T& obj) -{ - // Remove a source - obj.setSources.erase(pfrom->addr.ip); - - // If no longer supported by any sources, cancel it - if (obj.setSources.empty()) - AdvertStopPublish(pfrom, nChannel, nHops, obj); -} +// The publish/subscribe advert templates that used to close this file +// (AdvertStartPublish / AdvertStopPublish / AdvertRemoveSource) are gone along +// with the marketplace objects they carried. Nothing ever handled an inbound +// "publish" or "pub-cancel", so the machinery only ever pushed; what it pushed +// was CProduct, which no longer exists. -- cgit v1.2.3 From cd4e86d431c3f5848183ca362175d1cb3edfa107 Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Sun, 23 Aug 2026 07:52:52 +0000 Subject: selftest: cover the payout ledger properties this series fixes A new `payout-ledger` self-test, 40 assertions, wired into RunSelfTest and into `make -C src tests`. It runs against a scratch data directory and an in-memory wallet map: it never opens wallet.dat, never touches a real datadir, and never sends anything. What it asserts, defect by defect: - a restart does not duplicate an in-flight record. A matured round is exploded, the process state is thrown away and reloaded from the files twice, and the in-flight list must still hold exactly one record per recipient. Both guards are checked separately -- once with the round-level set restored, and once with it deliberately emptied so only the per-recipient check against gPaying can save it. A paying file already containing the duplicate the old build wrote must collapse on load, with the attempt counts merged upwards rather than reset. - state updates address a slot, not a value. Two records equal in every field -- exactly what the old code produced -- and rescheduling or forgetting one must land on the one it was given. - the ledger file cannot be published from a stale snapshot. An older snapshot published after a newer one must not appear on disk, and four threads publishing 64 large images of the same file in different orders must leave it parseable and equal to the highest version, whatever order they finish in. - a payment with only a legacy comment is recognised as already paid, and the fallback's limits hold: it requires the right recipient, the exact amount and the right height; one legacy transaction cannot settle a second debt; a transaction carrying another round's structured metadata is never accepted as legacy evidence; and the structured match still wins where it applies. - grandfathered zero-hash rounds do not answer for each other, both as a direct test of the round comparison and end to end -- recording a txid against one zero-hash round must leave the other still owed. - the retry clock that is actually used survives a restart. - a forfeit is published rather than silently dropped: the ledger loses that recipient and only that recipient, and the round proof records it with a reason instead of going on advertising the address as owed. - on-disk compatibility, checked against the real shapes: the live production pending_payouts.json (amounts / blockHash / blockHeight / matureAtHeight and nothing else), the merged build's shape including the retryAfter this build no longer writes, and a round trip through this build. Each assertion was verified to fail when its defect is put back: seven mutations were applied one at a time to a scratch copy and every one was caught. Notably, re-introducing either half of the duplicate-record fix alone is caught by its own assertion while the other half still prevents the duplicate -- which is the defence in depth this series was asked for, checked rather than asserted. --- src/Makefile | 1 + src/rpc.cpp | 671 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/selftest.cpp | 5 +- 3 files changed, 676 insertions(+), 1 deletion(-) (limited to 'src/Makefile') diff --git a/src/Makefile b/src/Makefile index 1166daf..89ba559 100644 --- a/src/Makefile +++ b/src/Makefile @@ -192,6 +192,7 @@ tests: test_bip32 bitflash-node ./bitflash-node -selftest=net-message ./bitflash-node -selftest=consensus-limits ./bitflash-node -selftest=pool-stratum + ./bitflash-node -selftest=payout-ledger ./bitflash-node -selftest=parse-money ./bitflash-node -selftest=socks5-proxy diff --git a/src/rpc.cpp b/src/rpc.cpp index 45130ff..cebc349 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -38,6 +38,7 @@ #include #include +#include #include #include #include @@ -2749,6 +2750,676 @@ static bool HandleLine(Miner* m, const std::string& rawLine, return true; } +// --------------------------------------------------------------------------- +// payout-ledger self-test +// +// Regression cover for the payout defects fixed in this series. Everything +// here runs against a scratch data directory and an in-memory wallet map; it +// never opens wallet.dat, never touches a real datadir, and never sends +// anything. +// --------------------------------------------------------------------------- + +static bool PayoutTestReadFile(const std::string& path, std::string& out) +{ + FILE* f = fopen(path.c_str(), "r"); + if (!f) return false; + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + out.assign((size_t)(sz > 0 ? sz : 0), '\0'); + if (sz > 0 && fread(&out[0], 1, (size_t)sz, f) != (size_t)sz) { + fclose(f); + return false; + } + fclose(f); + return true; +} + +static bool PayoutTestWriteFile(const std::string& path, const std::string& data) +{ + FILE* f = fopen(path.c_str(), "w"); + if (!f) return false; + bool ok = data.empty() || fwrite(data.c_str(), 1, data.size(), f) == data.size(); + return fclose(f) == 0 && ok; +} + +// Wipe every piece of payout state this test touches, so each section starts +// from the same place and a "restart" can be simulated by reloading from disk. +static void PayoutTestResetState() +{ + { + std::lock_guard lk(gPayoutLedgerMutex); + gPendingPayouts.clear(); + } + { + std::lock_guard lk(gPayoutExecMutex); + gPaying.clear(); + gQueuedPayouts.clear(); + } + { + std::lock_guard lk(gPoolRoundsMutex); + gPoolRounds.clear(); + } + { + std::lock_guard lk(gClaimedTxidMutex); + gLegacyClaimedTxids.clear(); + } +} + +// A BTF address whose hash160 is entirely under the test's control, so the +// output script the probe compares against can be built without a wallet. +static std::string PayoutTestAddress(unsigned char seed) +{ + uint160 h = 0; + for (int i = 0; i < 20; i++) + ((unsigned char*)&h)[i] = (unsigned char)(seed + i); + return Hash160ToAddress(h); +} + +static CScript PayoutTestScriptFor(const std::string& address) +{ + uint160 h160; + CScript sc; + if (AddressToHash160(address, h160)) + sc << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG; + return sc; +} + +// Put a transaction in the wallet map that looks exactly like one this pool +// sent. `nonce` only exists to keep the hashes distinct. +static std::string PayoutTestAddWalletTx(const std::string& comment, + const std::string& address, + int64 amount, + int nonce, + const std::string& structuredId) +{ + CWalletTx wtx; + wtx.vin.resize(1); + wtx.vin[0].prevout.SetNull(); + wtx.vin[0].scriptSig << nonce; + wtx.vout.resize(1); + wtx.vout[0].nValue = amount; + wtx.vout[0].scriptPubKey = PayoutTestScriptFor(address); + if (!comment.empty()) + wtx.mapValue["comment"] = comment; + if (!structuredId.empty()) { + wtx.mapValue["pool_payout_id"] = structuredId; + wtx.mapValue["pool_payout_address"] = address; + wtx.mapValue["pool_payout_amount"] = strprintf("%lld", amount); + } + uint256 hash = wtx.GetHash(); + CRITICAL_BLOCK(cs_mapWallet) + mapWallet[hash] = wtx; + return hash.GetHex(); +} + +int RunPayoutLedgerSelfTest() +{ + int nFail = 0; + auto check = [&](bool ok, const char* msg) { + fprintf(stdout, " %s %s\n", ok ? "ok" : "FAIL", msg); + if (!ok) nFail++; + }; + + fprintf(stdout, "payout-ledger self-test\n"); + + const std::string savedDataDir = strSetDataDir; + const int savedBestHeight = nBestHeight; + + // Scratch data directory in the current working directory. GetAppDir() + // follows strSetDataDir, so this is what PayoutFilePath() and + // PayingFilePath() resolve against for the rest of the test. + std::string dir = strprintf("payout_selftest_%d", (int)GetTime()); +#ifdef _WIN32 + _mkdir(dir.c_str()); +#else + mkdir(dir.c_str(), 0700); +#endif + strSetDataDir = dir; + fprintf(stdout, " scratch datadir: %s\n", dir.c_str()); + + const std::string ledgerPath = PayoutFilePath(); + const std::string payingPath = PayingFilePath(); + const std::string addrA = PayoutTestAddress(0x11); + const std::string addrB = PayoutTestAddress(0x55); + + // -- 1. A restart must not duplicate an in-flight record ----------------- + // + // The ledger entry is grandfathered (zero hash) on purpose: it keeps + // PayoutBlockStillOnChain out of the picture, which needs a block index, + // and it exercises the height fallback at the same time. + { + PayoutTestResetState(); + remove(ledgerPath.c_str()); + remove(payingPath.c_str()); + + PendingPayout pp; + pp.blockHeight = 4242; + pp.blockHash = 0; + pp.matureAtHeight = 4242; + pp.amounts[addrA] = 10 * COIN; + pp.amounts[addrB] = 20 * COIN; + VersionedBlob snap; + { + std::lock_guard lk(gPayoutLedgerMutex); + gPendingPayouts.push_back(pp); + snap = SerializePendingPayoutsLocked(); + } + PublishPendingPayouts(snap); + + nBestHeight = 5000; + FlushMaturePayouts(); + size_t afterFirstFlush = 0; + { + std::lock_guard lk(gPayoutExecMutex); + afterFirstFlush = gPaying.size(); + } + check(afterFirstFlush == 2, + "a matured round explodes into one record per unpaid recipient"); + + // Restart: drop everything in memory and come back from the files. + for (int restart = 1; restart <= 2; restart++) { + PayoutTestResetState(); + LoadPendingPayouts(); + LoadPaying(); + ReconcilePayouts(); + + size_t queued = 0; + { + std::lock_guard lk(gPayoutExecMutex); + queued = gQueuedPayouts.size(); + } + if (restart == 1) + check(queued == 1, + "startup rebuilds the exploded-round set from the restored" + " in-flight list"); + + FlushMaturePayouts(); + size_t n = 0; + { + std::lock_guard lk(gPayoutExecMutex); + n = gPaying.size(); + } + check(n == 2, restart == 1 + ? "a restart does not duplicate an in-flight record" + : "a second restart does not duplicate one either"); + } + + // The other half of the guard: even with the round-level dedupe set + // deliberately emptied, the per-recipient check against gPaying must + // stop a second standing order for the same money. + { + std::lock_guard lk(gPayoutExecMutex); + gQueuedPayouts.clear(); + } + FlushMaturePayouts(); + size_t n = 0; + { + std::lock_guard lk(gPayoutExecMutex); + n = gPaying.size(); + } + check(n == 2, + "the flush refuses a second record for a (round, address) already" + " in flight even with the round set lost"); + } + + // -- 2. Duplicates already on disk are collapsed as they are read -------- + { + PayoutTestResetState(); + nlohmann::json arr = nlohmann::json::array(); + for (int i = 0; i < 2; i++) { + nlohmann::json o; + o["blockHeight"] = 4242; + o["blockHash"] = uint256(0).ToString(); + o["matureAtHeight"] = 4242; + o["address"] = addrA; + o["amount"] = (int64)(10 * COIN); + o["retries"] = i == 0 ? 7 : 0; + o["deferrals"] = i == 0 ? 0 : 3; + arr.push_back(o); + } + check(PayoutTestWriteFile(payingPath, arr.dump(2)), + "wrote a paying file containing the duplicate the old build made"); + LoadPaying(); + std::lock_guard lk(gPayoutExecMutex); + check(gPaying.size() == 1, + "loading collapses a duplicate in-flight record from disk"); + check(gPaying.size() == 1 && gPaying[0].retries == 7 && gPaying[0].deferrals == 3, + "collapsing merges the attempt counts upwards, not resetting them"); + } + + // -- 3. State updates address a slot, not a value ----------------------- + // + // Build the exact pathology the old code produced -- two records equal in + // every field -- and prove the update lands on the one asked for. Under + // value matching the first record absorbed everything and the second was + // untouchable. + { + PayoutTestResetState(); + PayingRecord a; + a.blockHeight = 4242; a.blockHash = 0; a.matureAtHeight = 4242; + a.address = addrA; a.amount = 10 * COIN; + PayingRecord b = a; + { + std::lock_guard lk(gPayoutExecMutex); + a.id = ++gPayingNextId; + b.id = ++gPayingNextId; + gPaying.push_back(a); + gPaying.push_back(b); + } + PayingRecord upd = b; + upd.retries = 5; + upd.retryAfter = 1234567; + ReschedulePayment(upd); + { + std::lock_guard lk(gPayoutExecMutex); + check(gPaying.size() == 2 && + gPaying[0].retries == 0 && gPaying[1].retries == 5, + "rescheduling updates the record it was given, not the first" + " one that looks like it"); + } + ForgetPayment(a); + { + std::lock_guard lk(gPayoutExecMutex); + check(gPaying.size() == 1 && gPaying[0].id == b.id, + "forgetting erases the record it was given, not the first" + " one that looks like it"); + } + } + + // -- 4. retryAfter survives a restart ------------------------------------ + { + PayoutTestResetState(); + remove(payingPath.c_str()); + PayingRecord r; + r.blockHeight = 77; r.blockHash = 0; r.matureAtHeight = 77; + r.address = addrA; r.amount = 3 * COIN; + r.retryAfter = 1700000000LL; + r.deferrals = 9; + VersionedBlob snap; + { + std::lock_guard lk(gPayoutExecMutex); + r.id = ++gPayingNextId; + gPaying.push_back(r); + snap = SerializePayingLocked(); + } + PublishPaying(snap); + PayoutTestResetState(); + LoadPaying(); + std::lock_guard lk(gPayoutExecMutex); + check(gPaying.size() == 1 && gPaying[0].retryAfter == 1700000000LL && + gPaying[0].deferrals == 9, + "the retry clock that is actually used survives a restart"); + } + + // -- 5. The ledger file cannot be published from a stale snapshot -------- + { + PayoutTestResetState(); + remove(ledgerPath.c_str()); + + PendingPayout one; + one.blockHeight = 100; one.blockHash = 0; one.matureAtHeight = 100; + one.amounts[addrA] = 1 * COIN; + VersionedBlob older, newer; + { + std::lock_guard lk(gPayoutLedgerMutex); + gPendingPayouts.push_back(one); + older = SerializePendingPayoutsLocked(); + PendingPayout two; + two.blockHeight = 200; two.blockHash = 0; two.matureAtHeight = 200; + two.amounts[addrB] = 2 * COIN; + gPendingPayouts.push_back(two); + newer = SerializePendingPayoutsLocked(); + } + check(newer.seq > older.seq, + "a later serialisation carries a strictly higher version"); + + // The order the race produced: the payout thread's older image + // arriving after the event loop's newer one. + PublishPendingPayouts(newer); + PublishPendingPayouts(older); + std::string onDisk; + check(PayoutTestReadFile(ledgerPath, onDisk) && onDisk == newer.blob, + "an older snapshot cannot overwrite a newer published ledger"); + + // And the ordinary direction still works. + VersionedBlob newest; + { + std::lock_guard lk(gPayoutLedgerMutex); + gPendingPayouts.clear(); + newest = SerializePendingPayoutsLocked(); + } + PublishPendingPayouts(newest); + check(PayoutTestReadFile(ledgerPath, onDisk) && onDisk == newest.blob, + "a newer snapshot does replace the published ledger"); + } + + // -- 6. Concurrent publishers cannot interleave a file ------------------- + // + // Two threads renaming the same ".tmp" over the same target was the + // original defect. The images are made big so a torn write shows up as + // unparseable JSON rather than by luck. + { + const std::string racePath = dir + "/publish_race_selftest.json"; + std::mutex raceMutex; + uint64 racePublished = 0; + + std::vector images; + for (int i = 0; i < 64; i++) { + nlohmann::json arr = nlohmann::json::array(); + for (int k = 0; k < 200; k++) { + nlohmann::json o; + o["image"] = i; + o["entry"] = k; + o["address"] = addrA; + o["amount"] = (int64)(k + 1) * COIN; + arr.push_back(o); + } + VersionedBlob v; + v.blob = arr.dump(2); + v.seq = (uint64)i + 1; + images.push_back(v); + } + + std::vector threads; + for (int t = 0; t < 4; t++) { + threads.push_back(std::thread([&, t]() { + for (size_t i = 0; i < images.size(); i++) { + size_t idx = (i * 7 + (size_t)t * 13) % images.size(); + PublishVersioned(racePath, images[idx], raceMutex, + racePublished, "publish race selftest"); + } + })); + } + for (std::thread& th : threads) + th.join(); + + std::string onDisk; + bool read = PayoutTestReadFile(racePath, onDisk); + bool parses = false; + if (read) { + try { parses = nlohmann::json::parse(onDisk).is_array(); } + catch (...) { parses = false; } + } + check(read && parses, + "four threads publishing the same file never leave it unparseable"); + check(read && onDisk == images.back().blob, + "the published file settles on the highest version, whatever the" + " completion order"); + remove(racePath.c_str()); + remove((racePath + ".tmp").c_str()); + } + + // -- 7. Grandfathered zero-hash rounds do not answer for each other ------ + { + uint256 h1 = 0, h2 = 0; + h1.SetHex("00000000000000000000000000000000000000000000000000000000000000aa"); + h2.SetHex("00000000000000000000000000000000000000000000000000000000000000bb"); + check(!SamePayoutRound(100, 0, 200, 0), + "two zero-hash entries at different heights are different rounds"); + check(SamePayoutRound(100, 0, 100, 0), + "two zero-hash entries at the same height are the same round"); + check(SamePayoutRound(100, h1, 200, h1), + "a real hash identifies the round on its own"); + check(!SamePayoutRound(100, h1, 100, h2), + "different hashes at one height are different rounds"); + check(!SamePayoutRound(100, h1, 100, 0), + "a grandfathered entry never matches a hash-keyed one"); + check(!PayoutRoundIdentifiable(0, 0) && PayoutRoundIdentifiable(0, h1) && + PayoutRoundIdentifiable(7, 0), + "an entry with neither hash nor height is not adjudicable"); + + // End to end: recording a payment against one grandfathered round must + // not settle the other's debt. + PayoutTestResetState(); + remove(ledgerPath.c_str()); + { + std::lock_guard lk(gPayoutLedgerMutex); + PendingPayout first; + first.blockHeight = 100; first.blockHash = 0; first.matureAtHeight = 100; + first.amounts[addrA] = 1 * COIN; + PendingPayout second; + second.blockHeight = 200; second.blockHash = 0; second.matureAtHeight = 200; + second.amounts[addrA] = 1 * COIN; + gPendingPayouts.push_back(first); + gPendingPayouts.push_back(second); + } + PayingRecord r; + r.blockHeight = 200; r.blockHash = 0; r.matureAtHeight = 200; + r.address = addrA; r.amount = 1 * COIN; + RecordPayoutTxid(r, "txid-for-the-second-round"); + { + std::lock_guard lk(gPayoutLedgerMutex); + // The second round was fully paid and retired; the first is + // untouched and still owed. + check(gPendingPayouts.size() == 1 && + gPendingPayouts[0].blockHeight == 100 && + gPendingPayouts[0].paidTxids.empty(), + "a txid recorded against one zero-hash round leaves the other" + " still owed"); + } + } + + // -- 8. A payment with only a legacy comment is recognised as paid ------- + { + PayoutTestResetState(); + const int height = 8080; + const int64 amount = 7 * COIN; + const std::string id = PayoutIdForBlock(height, 0); + const std::string comment = PayoutComment(height); + std::set none; + std::string found; + + check(FindRecordedPoolPayout(id, height, addrA, amount, none, found) + == PAYOUT_PROBE_NONE, + "an empty wallet is not evidence that anything was paid"); + + const std::string legacyTxid = + PayoutTestAddWalletTx(comment, addrA, amount, 1, ""); + check(FindRecordedPoolPayout(id, height, addrA, amount, none, found) + == PAYOUT_PROBE_LEGACY && found == legacyTxid, + "a payment carrying only a pre-upgrade comment is recognised as" + " already paid"); + + check(FindRecordedPoolPayout(id, height, addrB, amount, none, found) + == PAYOUT_PROBE_NONE, + "the legacy match requires the output to pay THIS recipient"); + check(FindRecordedPoolPayout(id, height, addrA, amount + COIN, none, found) + == PAYOUT_PROBE_NONE, + "the legacy match requires the output to pay exactly this amount"); + check(FindRecordedPoolPayout(id, height + 1, addrA, amount, none, found) + == PAYOUT_PROBE_NONE, + "the legacy match requires the comment for this block height"); + + std::set spent; + spent.insert(legacyTxid); + check(FindRecordedPoolPayout(id, height, addrA, amount, spent, found) + == PAYOUT_PROBE_NONE, + "one legacy transaction cannot settle a second debt"); + + // A transaction written by THIS build carries pool_payout_id, and must + // never be offered as legacy evidence for a different round -- that is + // exactly what the structured metadata exists to prevent. + const std::string otherId = PayoutIdForBlock(height, 0) + "-other"; + PayoutTestAddWalletTx(comment, addrB, amount, 2, otherId); + check(FindRecordedPoolPayout(id, height, addrB, amount, none, found) + == PAYOUT_PROBE_NONE, + "another round's structured payment is not legacy evidence for" + " this one"); + + // The structured path still wins where it applies. + const std::string structuredTxid = + PayoutTestAddWalletTx(comment, addrB, amount, 3, id); + check(FindRecordedPoolPayout(id, height, addrB, amount, none, found) + == PAYOUT_PROBE_STRUCTURED && found == structuredTxid, + "structured payout metadata is matched in preference to a comment"); + } + + // -- 9. On-disk compatibility with the live production ledger ------------ + { + PayoutTestResetState(); + // Exactly the shape of the file on the pool node: amounts, blockHash, + // blockHeight, matureAtHeight, and nothing else. + const char* live = + "[{\"amounts\":{\"%s\":1234500000}," + "\"blockHash\":\"00000000000000000000000000000000000000000000000000000000000000cd\"," + "\"blockHeight\":1000," + "\"matureAtHeight\":1120}]"; + check(PayoutTestWriteFile(ledgerPath, strprintf(live, addrA.c_str())), + "wrote a ledger file in the live production shape"); + LoadPendingPayouts(); + { + std::lock_guard lk(gPayoutLedgerMutex); + check(gPendingPayouts.size() == 1 && + gPendingPayouts[0].blockHeight == 1000 && + gPendingPayouts[0].matureAtHeight == 1120 && + gPendingPayouts[0].blockHash != 0 && + gPendingPayouts[0].amounts.size() == 1 && + gPendingPayouts[0].amounts[addrA] == 1234500000LL, + "the live production pending_payouts.json still loads"); + } + + // And the shape the merged build writes, retryAfter included, which + // this build no longer writes but must still read past. + PayoutTestResetState(); + const char* merged = + "[{\"amounts\":{\"%s\":100000000}," + "\"blockHash\":\"00000000000000000000000000000000000000000000000000000000000000ef\"," + "\"blockHeight\":2000,\"matureAtHeight\":2120,\"retryAfter\":0," + "\"paidTxids\":{\"%s\":\"abc123\"}}]"; + check(PayoutTestWriteFile(ledgerPath, + strprintf(merged, addrA.c_str(), addrA.c_str())), + "wrote a ledger file in the merged build's shape"); + LoadPendingPayouts(); + { + std::lock_guard lk(gPayoutLedgerMutex); + check(gPendingPayouts.size() == 1 && + gPendingPayouts[0].blockHeight == 2000 && + gPendingPayouts[0].paidTxids.size() == 1 && + gPendingPayouts[0].paidTxids[addrA] == "abc123", + "a ledger file carrying retryAfter and paidTxids still loads"); + } + + // A round trip through this build must not lose anything either. + VersionedBlob snap; + { + std::lock_guard lk(gPayoutLedgerMutex); + snap = SerializePendingPayoutsLocked(); + } + PublishPendingPayouts(snap); + PayoutTestResetState(); + LoadPendingPayouts(); + { + std::lock_guard lk(gPayoutLedgerMutex); + check(gPendingPayouts.size() == 1 && + gPendingPayouts[0].blockHeight == 2000 && + gPendingPayouts[0].matureAtHeight == 2120 && + gPendingPayouts[0].paidTxids[addrA] == "abc123", + "what this build writes, this build reads back unchanged"); + } + } + + // -- 10. A forfeit is published, not just dropped ------------------------ + { + PayoutTestResetState(); + remove(ledgerPath.c_str()); + const std::string roundsPath = dir + "/pool_rounds_selftest.json"; + const std::string savedRoundsFile = strPoolRoundsFile; + strPoolRoundsFile = roundsPath; + remove(roundsPath.c_str()); + + const int height = 606; + PoolRoundProof round; + round.id = PayoutIdForBlock(height, 0); + round.blockHeight = height; + round.matureAtHeight = height; + round.payouts[addrA] = 5 * COIN; + round.payouts[addrB] = 6 * COIN; + RecordPoolRound(round); + + { + std::lock_guard lk(gPayoutLedgerMutex); + PendingPayout pp; + pp.blockHeight = height; pp.blockHash = 0; pp.matureAtHeight = height; + pp.amounts[addrA] = 5 * COIN; + pp.amounts[addrB] = 6 * COIN; + gPendingPayouts.push_back(pp); + } + + PayingRecord r; + r.blockHeight = height; r.blockHash = 0; r.matureAtHeight = height; + r.address = addrA; r.amount = 5 * COIN; + ForfeitPayoutRecipient(r, "selftest reason"); + + { + std::lock_guard lk(gPayoutLedgerMutex); + check(gPendingPayouts.size() == 1 && + gPendingPayouts[0].amounts.count(addrA) == 0 && + gPendingPayouts[0].amounts.count(addrB) == 1, + "a forfeit removes only that recipient from the ledger entry"); + } + + std::string onDisk; + bool proofOk = false; + if (PayoutTestReadFile(roundsPath, onDisk)) { + try { + nlohmann::json root = nlohmann::json::parse(onDisk); + const nlohmann::json& p = root["rounds"][0]["payouts"]; + proofOk = p[addrA].value("forfeited", false) && + p[addrA].value("forfeitReason", "") == "selftest reason" && + p[addrA]["txid"].is_null() && + !p[addrB].contains("forfeited"); + } catch (...) { + proofOk = false; + } + } + check(proofOk, + "the published round proof records the forfeit instead of" + " advertising the address as still owed"); + + // And it survives a reload of the proof file. + { + std::lock_guard lk(gPoolRoundsMutex); + gPoolRounds.clear(); + } + LoadPoolRounds(); + { + std::lock_guard lk(gPoolRoundsMutex); + check(gPoolRounds.size() == 1 && + gPoolRounds[0].forfeited.count(addrA) == 1 && + gPoolRounds[0].forfeited[addrA] == "selftest reason", + "the forfeit is read back from pool_rounds.json"); + } + + remove(roundsPath.c_str()); + remove((roundsPath + ".tmp").c_str()); + strPoolRoundsFile = savedRoundsFile; + } + + // -- clean up ------------------------------------------------------------ + PayoutTestResetState(); + CRITICAL_BLOCK(cs_mapWallet) + mapWallet.clear(); + remove(ledgerPath.c_str()); + remove((ledgerPath + ".tmp").c_str()); + remove(payingPath.c_str()); + remove((payingPath + ".tmp").c_str()); + remove((dir + "/pool_status.json").c_str()); + remove((dir + "/pool_rounds.json").c_str()); +#ifdef _WIN32 + _rmdir(dir.c_str()); +#else + rmdir(dir.c_str()); +#endif + strSetDataDir = savedDataDir; + nBestHeight = savedBestHeight; + + fprintf(stdout, "%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + int RunPoolStratumSelfTest() { int nFail = 0; diff --git a/src/selftest.cpp b/src/selftest.cpp index eed6779..034e1f7 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -13,6 +13,7 @@ #include "walletcmd.h" extern int RunPoolStratumSelfTest(); +extern int RunPayoutLedgerSelfTest(); // Test results go to the terminal, not to debug.log. // @@ -1573,12 +1574,14 @@ int RunSelfTest(const std::string& name) return RunConsensusLimitsSelfTest(); if (name == "pool-stratum") return RunPoolStratumSelfTest(); + if (name == "payout-ledger") + return RunPayoutLedgerSelfTest(); if (name == "parse-money") return RunParseMoneySelfTest(); if (name == "socks5-proxy") return RunSocks5ProxySelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - 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"); + printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum, payout-ledger, parse-money, socks5-proxy\n"); return 1; } -- cgit v1.2.3 From e4f898c4a6b9900553c64b9804889b5fdae3e55f Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Wed, 2 Sep 2026 05:16:33 +0000 Subject: build: keep the stack non-executable, whoever links this RandomX links jit_compiler_x86_static.S, a hand-written assembly stub with no .note.GNU-stack marker. binutils reads a missing marker as a request for an executable stack, and older linkers grant it for the whole binary: NX off, process-wide, for a node that parses attacker-supplied blocks and messages. The same source therefore produced different security properties depending on where it was built. Ubuntu 26.04 gave GNU_STACK RW; Debian 12 -- the build target for the deploy image, because it is the oldest glibc we ship to and the last Debian that still packages the Berkeley DB C++ binding -- gave RWE. The only announcement was a linker warning in the middle of an otherwise clean build. -z noexecstack settles it in the tree instead of in the toolchain. RandomX's JIT maps its own W^X pages and never executes from the stack, so nothing that works today stops working. Both link lines take it, GUI and headless. Verify: readelf -lW bitflash-node | grep -A1 GNU_STACK # expect RW --- src/Makefile | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) (limited to 'src/Makefile') diff --git a/src/Makefile b/src/Makefile index b068376..1bc3ae8 100644 --- a/src/Makefile +++ b/src/Makefile @@ -100,7 +100,7 @@ obj/imgui_impl_opengl3.o: $(IMGUI_DIR)/imgui_impl_opengl3.cpp $(CXX) -c $(CXXFLAGS) -o $@ $< bitflash: $(IMGUI_OBJS) $(NODE_OBJS) $(GUI_OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ $(LIBS) + $(CXX) $(CXXFLAGS) $(LDHARDFLAGS) -o $@ $^ $(LIBS) @echo "Built: bitflash" # ---- headless node --------------------------------------------------------- @@ -114,6 +114,20 @@ bitflash: $(IMGUI_OBJS) $(NODE_OBJS) $(GUI_OBJS) # Objects go to obj-node/ because main_gui.cpp is compiled twice, with and # without -DBITFLASH_NO_GUI, and the two must not overwrite each other. +# RandomX links a hand-written assembly JIT stub, jit_compiler_x86_static.S, +# which carries no .note.GNU-stack marker. Older binutils -- Debian 12's, which +# is what builds against the oldest glibc we ship to -- reads a missing marker +# as "this object wants an executable stack" and marks the whole binary RWE, +# switching NX off for the process. Newer toolchains default the other way, so +# identical source produced GNU_STACK RW on Ubuntu 26.04 and RWE on bookworm, +# announced by nothing louder than a linker warning in the middle of the build. +# +# Assert it here rather than inherit whichever default the build host happens +# to carry. The JIT maps its own W^X pages and never executes from the stack, +# so nothing needs the permission this takes away. Check with: +# readelf -lW bitflash-node | grep -A1 GNU_STACK # expect RW, not RWE +LDHARDFLAGS = -Wl,-z,noexecstack + NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \ -L$(RANDOMX_DIR)/build -lrandomx \ -lsodium -lssl -lcrypto -ldb_cxx -lpthread @@ -134,7 +148,7 @@ obj-node/sha.o: sha.cpp sha.h $(CXX) -c $(CXXFLAGS) -DBITFLASH_NO_GUI -O3 -o $@ $< bitflash-node: $(NODE_ONLY_OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ $(NODE_LIBS) + $(CXX) $(CXXFLAGS) $(LDHARDFLAGS) -o $@ $^ $(NODE_LIBS) @echo "Built: bitflash-node (headless, no libGL needed)" obj-fuzz/%.o: %.cpp $(HEADERS) -- cgit v1.2.3 From fd627b134c686e7c9afb646dc37c573c5d36fd4a Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Wed, 2 Sep 2026 05:22:36 +0000 Subject: build: stop rm -rf'ing a RandomX tree the build did not create deps-randomx built into $(HOME)/RandomX and deleted that path whenever the library was missing, taking out hand-managed checkouts the build never owned. Default the build to the in-tree $(ROOT)/randomx-build, like secp256k1-build, overridable via RANDOMX_DIR and exported so src/Makefile's own fallback resolves to the same tree. As a backstop, the recipe writes a .bitflash-built marker after a successful build and refuses to delete any directory that lacks it, so a user-supplied RANDOMX_DIR pointing at a pre-existing tree is never removed. Closes #27 --- Makefile | 31 ++++++++++++++++++++++++------- src/Makefile | 4 +++- 2 files changed, 27 insertions(+), 8 deletions(-) (limited to 'src/Makefile') diff --git a/Makefile b/Makefile index 9f97b73..83a2c73 100644 --- a/Makefile +++ b/Makefile @@ -59,6 +59,17 @@ APPIMAGETOOL_SHA256 := ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3c # ---- Linux ---------------------------------------------------------------- +# RandomX used to build in $(HOME)/RandomX, and deps-randomx would rm -rf that +# path whenever the library was missing -- deleting a tree the build did not +# create and does not own (issue #27; a hand-managed pinned checkout there is +# what this machine's miners build against). The default is now in-tree, like +# secp256k1-build. A host that deliberately keeps RandomX elsewhere can set +# RANDOMX_DIR=... explicitly; the recipe below will still never delete a +# directory that lacks the marker file this build writes after a successful +# build. Exported so src/Makefile's own RANDOMX_DIR ?= picks the same tree. +RANDOMX_DIR ?= $(ROOT)/randomx-build +export RANDOMX_DIR + linux: deps-linux $(MAKE) -C src -f Makefile -j$(NPROC) strip src/bitflash @@ -109,19 +120,25 @@ deps-secp256k1: fi deps-randomx: - @if [ ! -f $(HOME)/RandomX/build/librandomx.a ]; then \ + @if [ ! -f $(RANDOMX_DIR)/build/librandomx.a ]; then \ + if [ -d $(RANDOMX_DIR) ] && [ ! -f $(RANDOMX_DIR)/.bitflash-built ]; then \ + echo "ERROR: $(RANDOMX_DIR) exists but was not created by this build"; \ + echo " refusing to rm -rf it -- remove it yourself or set RANDOMX_DIR"; \ + exit 1; \ + fi; \ echo "==> building RandomX"; \ - rm -rf $(HOME)/RandomX; \ - git init -q $(HOME)/RandomX; \ - cd $(HOME)/RandomX && \ + rm -rf $(RANDOMX_DIR); \ + git init -q $(RANDOMX_DIR); \ + cd $(RANDOMX_DIR) && \ git remote add origin $(RANDOMX_REPO) && \ git fetch -q --depth 1 origin $(RANDOMX_COMMIT) && \ git checkout -q --detach FETCH_HEAD && \ test "$$(git rev-parse HEAD)" = "$(RANDOMX_COMMIT)" && \ echo "==> RandomX pinned at $(RANDOMX_COMMIT)" && \ - mkdir -p $(HOME)/RandomX/build; \ - cd $(HOME)/RandomX/build && cmake .. -DCMAKE_BUILD_TYPE=Release && \ - make -j$(NPROC) randomx; \ + mkdir -p $(RANDOMX_DIR)/build && \ + cd $(RANDOMX_DIR)/build && cmake .. -DCMAKE_BUILD_TYPE=Release && \ + make -j$(NPROC) randomx && \ + touch $(RANDOMX_DIR)/.bitflash-built && \ echo "==> RandomX done"; \ else \ echo "==> RandomX already built"; \ diff --git a/src/Makefile b/src/Makefile index 1bc3ae8..2c6d304 100644 --- a/src/Makefile +++ b/src/Makefile @@ -6,7 +6,9 @@ FUZZ_CXX ?= clang++ SECP_INC ?= /usr/local/include SECP_LIB ?= /usr/local/lib -RANDOMX_DIR ?= $(HOME)/RandomX +# The root Makefile exports RANDOMX_DIR (default: /randomx-build, built +# by deps-randomx). This fallback only applies when src/ is built standalone. +RANDOMX_DIR ?= $(abspath $(CURDIR)/..)/randomx-build IMGUI_DIR = imgui -- cgit v1.2.3 From 5ef59833756da6f47274cb990eb874fb0ed99019 Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Wed, 2 Sep 2026 05:16:33 +0000 Subject: build: keep the stack non-executable, whoever links this RandomX links jit_compiler_x86_static.S, a hand-written assembly stub with no .note.GNU-stack marker. binutils reads a missing marker as a request for an executable stack, and older linkers grant it for the whole binary: NX off, process-wide, for a node that parses attacker-supplied blocks and messages. The same source therefore produced different security properties depending on where it was built. Ubuntu 26.04 gave GNU_STACK RW; Debian 12 -- the build target for the deploy image, because it is the oldest glibc we ship to and the last Debian that still packages the Berkeley DB C++ binding -- gave RWE. The only announcement was a linker warning in the middle of an otherwise clean build. -z noexecstack settles it in the tree instead of in the toolchain. RandomX's JIT maps its own W^X pages and never executes from the stack, so nothing that works today stops working. Both link lines take it, GUI and headless. Verify: readelf -lW bitflash-node | grep -A1 GNU_STACK # expect RW --- src/Makefile | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) (limited to 'src/Makefile') diff --git a/src/Makefile b/src/Makefile index fe48041..8ea3930 100644 --- a/src/Makefile +++ b/src/Makefile @@ -101,7 +101,7 @@ obj/imgui_impl_opengl3.o: $(IMGUI_DIR)/imgui_impl_opengl3.cpp $(CXX) -c $(CXXFLAGS) -o $@ $< bitflash: $(IMGUI_OBJS) $(NODE_OBJS) $(GUI_OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ $(LIBS) + $(CXX) $(CXXFLAGS) $(LDHARDFLAGS) -o $@ $^ $(LIBS) @echo "Built: bitflash" # ---- headless node --------------------------------------------------------- @@ -115,6 +115,20 @@ bitflash: $(IMGUI_OBJS) $(NODE_OBJS) $(GUI_OBJS) # Objects go to obj-node/ because main_gui.cpp is compiled twice, with and # without -DBITFLASH_NO_GUI, and the two must not overwrite each other. +# RandomX links a hand-written assembly JIT stub, jit_compiler_x86_static.S, +# which carries no .note.GNU-stack marker. Older binutils -- Debian 12's, which +# is what builds against the oldest glibc we ship to -- reads a missing marker +# as "this object wants an executable stack" and marks the whole binary RWE, +# switching NX off for the process. Newer toolchains default the other way, so +# identical source produced GNU_STACK RW on Ubuntu 26.04 and RWE on bookworm, +# announced by nothing louder than a linker warning in the middle of the build. +# +# Assert it here rather than inherit whichever default the build host happens +# to carry. The JIT maps its own W^X pages and never executes from the stack, +# so nothing needs the permission this takes away. Check with: +# readelf -lW bitflash-node | grep -A1 GNU_STACK # expect RW, not RWE +LDHARDFLAGS = -Wl,-z,noexecstack + NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \ -L$(RANDOMX_DIR)/build -lrandomx \ -lsodium -lssl -lcrypto -ldb_cxx -lsqlite3 -lpthread @@ -135,7 +149,7 @@ obj-node/sha.o: sha.cpp sha.h $(CXX) -c $(CXXFLAGS) -DBITFLASH_NO_GUI -O3 -o $@ $< bitflash-node: $(NODE_ONLY_OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ $(NODE_LIBS) + $(CXX) $(CXXFLAGS) $(LDHARDFLAGS) -o $@ $^ $(NODE_LIBS) @echo "Built: bitflash-node (headless, no libGL needed)" obj-fuzz/%.o: %.cpp $(HEADERS) -- cgit v1.2.3 From 60538c5a935787521a5ad8c2a7dce52b5f250f3a Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Sat, 5 Sep 2026 03:16:00 +0000 Subject: compactblock: import BIP152-style compact block relay from upstream 1.2.23 Brings src/compactblock.{cpp,h} and test_compactblock.cpp across verbatim from the upstream-v1.2.23 tag, and gives them a build. 1.2.23 is not a newer release of the line this fork ships. It is a lightweight tag (7ab5a8c) cut 2026-07-30, on a lineage that upstream/main (e0be70e, 2026-08-20) does not contain and no remote branch carries -- 272 commits that are not ours, against 376 of ours that are not theirs, from a merge-base at 56df983. Merging that tag would have deleted 18 files from src/, selftest.{cpp,h} and the whole 14-test suite among them, so the content is ported file by file instead. Compact block relay is the piece worth having: a peer that advertises sendcmpct gets 48-bit keyed short IDs per transaction rather than the transaction bytes, and reconstructs the block from its own mempool. It is purely additive on the wire -- a peer that never sends sendcmpct keeps receiving full block messages, so no coordinated upgrade is needed. This commit is the primitives and their unit test only. Nothing calls BuildCompactBlock or TryReconstructBlock yet: the P2P message handlers (sendcmpct/cmpctblock/getblocktxn/blocktxn) live in 1.2.23's main.cpp and are deliberately left for a follow-up, so that no attacker-controlled bytes reach this code until that wiring is reviewed on its own. The files needed no API adaptation -- mapTransactions, BEGIN/END and IMPLEMENT_SERIALIZE are unchanged across the divergence, and it compiled clean first time. Upstream never wired test_compactblock.cpp into its own Makefile, so it had never been built there; it links standalone the way test_bip32 does, supplying its own mapTransactions and GetAppDir to keep main.cpp out of the link. TEST_LIBS is overridable for the same reason NODE_LIBS is: the pinned dep tree ships libsodium as a static archive only. All 17 checks in test_compactblock pass, all 14 selftests still pass, and the consensus constants are unchanged against e4f898c. --- .gitignore | 1 + src/Makefile | 24 +++++-- src/compactblock.cpp | 155 ++++++++++++++++++++++++++++++++++++++++++++++ src/compactblock.h | 113 +++++++++++++++++++++++++++++++++ src/test_compactblock.cpp | 133 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 422 insertions(+), 4 deletions(-) create mode 100644 src/compactblock.cpp create mode 100644 src/compactblock.h create mode 100644 src/test_compactblock.cpp (limited to 'src/Makefile') diff --git a/.gitignore b/.gitignore index b18859f..77b1660 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ bitflash-node src/bitflash-node src/obj-node/ src/test_bip32 +src/test_compactblock # Release artefacts (produced by 'make windows' / 'make linux') Bitflash-*-windows/ diff --git a/src/Makefile b/src/Makefile index b931007..888110f 100644 --- a/src/Makefile +++ b/src/Makefile @@ -55,7 +55,8 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \ HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ - selftest.h walletcmd.h wallet_sqlite.h compat.h sockcount.h proxy.h tor.h font_roboto.h + selftest.h walletcmd.h wallet_sqlite.h compat.h sockcount.h proxy.h tor.h font_roboto.h \ + compactblock.h IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o @@ -64,7 +65,7 @@ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ obj/selftest.o obj/bip32.o obj/walletcmd.o obj/wallet_sqlite.o \ - obj/proxy.o obj/tor.o + obj/proxy.o obj/tor.o obj/compactblock.o GUI_OBJS = obj/gui.o obj/main_gui.o @@ -194,13 +195,28 @@ fuzz-script-smoke: fuzz-script ./fuzz-script -runs=128 ../work/fuzz-corpus/script clean: - rm -rf obj obj-node obj-fuzz bitflash bitflash-node fuzz-net-message fuzz-script test_bip32 + rm -rf obj obj-node obj-fuzz bitflash bitflash-node fuzz-net-message fuzz-script test_bip32 test_compactblock test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h $(CXX) $(CXXFLAGS) -o $@ test_bip32.cpp bip32.cpp -lssl -lcrypto -tests: test_bip32 bitflash-node +# Upstream shipped test_compactblock.cpp with no build rule at all, so it was +# never compiled there. It links the standalone way test_bip32 does: it supplies +# its own mapTransactions and GetAppDir so main.cpp stays out of the link, and +# only needs sha.cpp/util.cpp for Hash() and the logging helpers. +# +# TEST_LIBS is overridable for the same reason NODE_LIBS is: the pinned dep tree +# ships libsodium as a static archive only, so a build against it passes +# TEST_LIBS="... -l:libsodium.a ...". +TEST_LIBS ?= -lsodium -lssl -lcrypto -lpthread + +test_compactblock: test_compactblock.cpp compactblock.cpp compactblock.h $(HEADERS) + $(CXX) $(CXXFLAGS) -o $@ test_compactblock.cpp compactblock.cpp sha.cpp util.cpp \ + $(TEST_LIBS) + +tests: test_bip32 test_compactblock bitflash-node ./test_bip32 + ./test_compactblock ./bitflash-node -selftest=wallet-keypool ./bitflash-node -selftest=wallet-hd ./bitflash-node -selftest=wallet-format diff --git a/src/compactblock.cpp b/src/compactblock.cpp new file mode 100644 index 0000000..3999ab7 --- /dev/null +++ b/src/compactblock.cpp @@ -0,0 +1,155 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// See compactblock.h. + +#include "headers.h" +#include "compactblock.h" +#include +#include + +uint256 CCmpctBlock::HeaderHash() const +{ + // Same technique CBlock::GetHash() uses: the header fields are laid out + // contiguously and in the same order here as in CBlock, so hashing the + // raw member range gives the identical block-identity hash. + return Hash(BEGIN(nVersion), END(nNonce)); +} + +uint64 ComputeShortTxId(const uint256& hashPrevBlock, uint64 shortIdNonce, const uint256& txid) +{ + // Derive a 16-byte SipHash key from (hashPrevBlock, shortIdNonce), so + // short IDs are specific to this block: they can't be precomputed + // before the block (and its freshly-chosen nonce) exist, and the same + // transaction gets a different short ID in every block it appears in + // (relevant across reorgs/re-announcements). + unsigned char keyMaterial[32 + sizeof(uint64)]; + memcpy(keyMaterial, BEGIN(hashPrevBlock), 32); + memcpy(keyMaterial + 32, &shortIdNonce, sizeof(uint64)); + uint256 keyHash = Hash((const char*)keyMaterial, (const char*)keyMaterial + sizeof(keyMaterial)); + + unsigned char out[crypto_shorthash_BYTES]; // 8 bytes + crypto_shorthash(out, (const unsigned char*)BEGIN(txid), 32, (const unsigned char*)BEGIN(keyHash)); + + uint64 id = 0; + memcpy(&id, out, sizeof(uint64)); + return id & 0xFFFFFFFFFFFFULL; // low 48 bits -- matches the wire field width +} + +bool BuildCompactBlock(const CBlock& block, CCmpctBlock& out) +{ + if (block.vtx.empty()) + return false; // every real block has at least the coinbase + + out.nVersion = block.nVersion; + out.hashPrevBlock = block.hashPrevBlock; + out.hashMerkleRoot = block.hashMerkleRoot; + out.nTime = block.nTime; + out.nBits = block.nBits; + out.nNonce = block.nNonce; + + unsigned char nonceBytes[sizeof(uint64)]; + RAND_bytes(nonceBytes, sizeof(nonceBytes)); + memcpy(&out.shortIdNonce, nonceBytes, sizeof(uint64)); + + out.vPrefilledIndex.clear(); + out.vPrefilledTx.clear(); + out.vShortTxId.clear(); + + // Only the coinbase is prefilled (index 0) -- see file comment in + // compactblock.h for why that's enough for this chain's scale. + out.vPrefilledIndex.push_back(0); + out.vPrefilledTx.push_back(block.vtx[0]); + + for (size_t i = 1; i < block.vtx.size(); i++) + out.vShortTxId.push_back(ComputeShortTxId(out.hashPrevBlock, out.shortIdNonce, block.vtx[i].GetHash())); + + return true; +} + +bool TryReconstructBlock(const CCmpctBlock& cmpct, CBlock& outBlock, std::vector& vMissingIndex) +{ + vMissingIndex.clear(); + + size_t nTotal = cmpct.vPrefilledIndex.size() + cmpct.vShortTxId.size(); + if (cmpct.vPrefilledIndex.size() != cmpct.vPrefilledTx.size() || nTotal == 0) + return false; // malformed + + outBlock.SetNull(); + outBlock.nVersion = cmpct.nVersion; + outBlock.hashPrevBlock = cmpct.hashPrevBlock; + outBlock.hashMerkleRoot = cmpct.hashMerkleRoot; + outBlock.nTime = cmpct.nTime; + outBlock.nBits = cmpct.nBits; + outBlock.nNonce = cmpct.nNonce; + outBlock.vtx.assign(nTotal, CTransaction()); + + std::vector vFilled(nTotal, false); + for (size_t i = 0; i < cmpct.vPrefilledIndex.size(); i++) + { + unsigned int idx = cmpct.vPrefilledIndex[i]; + if (idx >= nTotal) + return false; // malformed + outBlock.vtx[idx] = cmpct.vPrefilledTx[i]; + vFilled[idx] = true; + } + + // Index every mempool tx by its short ID under this block's keying, so + // resolving each block position is a map lookup rather than a linear + // scan of the mempool per short ID. + std::map mapShortIdToTx; + for (std::map::const_iterator mi = mapTransactions.begin(); + mi != mapTransactions.end(); ++mi) + { + uint64 sid = ComputeShortTxId(cmpct.hashPrevBlock, cmpct.shortIdNonce, mi->first); + // A short-ID collision between two distinct mempool transactions is + // possible (48 bits, birthday bound ~2^24 mempool entries) though + // very unlikely at this chain's scale; on collision, prefer keeping + // neither mapped rather than risk resolving to the wrong tx -- + // that position just falls through to getblocktxn like a genuine + // miss, which is always correct, only occasionally not optimal. + if (mapShortIdToTx.count(sid)) + mapShortIdToTx[sid] = NULL; + else + mapShortIdToTx[sid] = &mi->second; + } + + size_t shortIdPos = 0; + for (size_t idx = 0; idx < nTotal; idx++) + { + if (vFilled[idx]) + continue; + if (shortIdPos >= cmpct.vShortTxId.size()) + return false; // malformed: more unfilled positions than short IDs + uint64 sid = cmpct.vShortTxId[shortIdPos++]; + + std::map::const_iterator found = mapShortIdToTx.find(sid); + if (found != mapShortIdToTx.end() && found->second != NULL) + { + outBlock.vtx[idx] = *found->second; + vFilled[idx] = true; + } + else + { + vMissingIndex.push_back(idx); + } + } + + return vMissingIndex.empty(); +} + +bool FillMissingAndFinish(CBlock& outBlock, const std::vector& vMissingIndex, + const std::vector& vTxn) +{ + if (vMissingIndex.size() != vTxn.size()) + return false; // peer didn't answer what was asked + + for (size_t i = 0; i < vMissingIndex.size(); i++) + { + unsigned int idx = vMissingIndex[i]; + if (idx >= outBlock.vtx.size()) + return false; // malformed + outBlock.vtx[idx] = vTxn[i]; + } + return true; +} diff --git a/src/compactblock.h b/src/compactblock.h new file mode 100644 index 0000000..745ee3b --- /dev/null +++ b/src/compactblock.h @@ -0,0 +1,113 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// Compact block relay (BIP152-style). Instead of sending a peer's full set +// of transactions for a block they're about to receive -- almost all of +// which they already have in their mempool from normal tx relay -- send a +// short (48-bit, keyed-hash) ID per transaction instead. A peer that already +// has every referenced transaction reconstructs the full block locally from +// its mempool without any of the transaction bytes crossing the wire at +// all; only genuinely missing transactions (typically just the coinbase) +// need a follow-up round trip. +// +// Compatibility: this is purely additive. A peer that never sends +// "sendcmpct" is never sent a compact block -- it keeps getting full +// "block" messages exactly as before (see the getdata handler in +// main.cpp). Old, unupgraded nodes on the live network are completely +// unaffected; ProcessMessage's unknown-command handling means they simply +// never see the new message types. No coordinated network upgrade needed. +// +// Deliberately scoped down from full BIP152: no high-bandwidth (unsolicited +// push) mode, no differential-index encoding for prefilled transactions, +// only the current chain tip is ever offered in compact form (an old block +// is unlikely to still overlap much with any peer's current mempool, so the +// bandwidth win doesn't apply -- full "block" is sent for those, same as +// today). Only the coinbase is prefilled; every other transaction position +// carries a short ID. + +#ifndef BITFLASH_COMPACTBLOCK_H +#define BITFLASH_COMPACTBLOCK_H + +// Requires CBlock/CTransaction already visible -- #include "headers.h" (or +// main.h) before this file. Deliberately doesn't include main.h itself: +// main.h has no include guard (matches this codebase's existing headers, it +// relies on being pulled in exactly once via headers.h), so a second +// #include "main.h" here would break every translation unit that already +// brought it in via headers.h first. +#include + +// The compact representation of a block: header fields, a per-block nonce +// (keys the short-ID hash so IDs can't be precomputed/ground across blocks), +// the coinbase in full, and a 48-bit short ID for every other transaction +// in block order. +class CCmpctBlock +{ +public: + // header + int nVersion; + uint256 hashPrevBlock; + uint256 hashMerkleRoot; + unsigned int nTime; + unsigned int nBits; + unsigned int nNonce; + + // Keys the short-ID hash for this block specifically -- chosen fresh + // per block (not reused from nNonce, which is public PoW data an + // attacker already knows and could otherwise grind against). + uint64 shortIdNonce; + + // Sent in full. In this implementation: always exactly the coinbase, + // at index 0. + std::vector vPrefilledIndex; + std::vector vPrefilledTx; + + // One short ID per NON-prefilled transaction, in ascending block-index + // order (i.e. the Nth entry here belongs to the Nth block position that + // isn't in vPrefilledIndex). + std::vector vShortTxId; + + IMPLEMENT_SERIALIZE + ( + READWRITE(this->nVersion); + nVersion = this->nVersion; + READWRITE(hashPrevBlock); + READWRITE(hashMerkleRoot); + READWRITE(nTime); + READWRITE(nBits); + READWRITE(nNonce); + READWRITE(shortIdNonce); + READWRITE(vPrefilledIndex); + READWRITE(vPrefilledTx); + READWRITE(vShortTxId); + ) + + uint256 HeaderHash() const; +}; + +// Compute the 48-bit short ID for a transaction under a given block's +// short-ID nonce. Keyed (via the block's hashPrevBlock + shortIdNonce) so +// IDs from one block can't be precomputed before that block exists, and +// differ block to block even for the same transaction. +uint64 ComputeShortTxId(const uint256& hashPrevBlock, uint64 shortIdNonce, const uint256& txid); + +// Build the compact form of `block` (which must have vtx[0] as the +// coinbase, as every block on this chain does). +bool BuildCompactBlock(const CBlock& block, CCmpctBlock& out); + +// Attempt to reconstruct a full block from a received compact block using +// the local mempool. On full success, returns true and fills `outBlock`. +// On partial success (some transactions not found locally), returns false +// and fills `vMissingIndex` with the block-position indices that still need +// to be requested via "getblocktxn" -- outBlock is left partially built +// (coinbase and any resolved positions filled; caller must not use it until +// FillMissingAndFinish completes it). +bool TryReconstructBlock(const CCmpctBlock& cmpct, CBlock& outBlock, std::vector& vMissingIndex); + +// Complete a partially-reconstructed block (from a prior TryReconstructBlock +// that returned false) using the transactions received in a "blocktxn" +// response. vTxn must be in the same order as the indices this node +// requested via "getblocktxn" for this block. +bool FillMissingAndFinish(CBlock& outBlock, const std::vector& vMissingIndex, + const std::vector& vTxn); + +#endif diff --git a/src/test_compactblock.cpp b/src/test_compactblock.cpp new file mode 100644 index 0000000..f4b5583 --- /dev/null +++ b/src/test_compactblock.cpp @@ -0,0 +1,133 @@ +// Standalone test of compactblock.cpp's core logic: build a compact block, +// reconstruct it from a simulated mempool, including the missing-tx path. +// Not wired into the P2P layer -- proves BuildCompactBlock/ +// TryReconstructBlock/FillMissingAndFinish are correct in isolation. + +#include "headers.h" +#include "compactblock.h" +#include +#include + +// main.cpp defines this; we're not linking main.cpp, so define it here. +map mapTransactions; +string GetAppDir() { return "."; } // util.h's debug-log path helper needs this + +static int g_fail = 0; +#define CHECK(cond, msg) do { \ + if (cond) printf(" ok %s\n", msg); \ + else { printf(" FAIL %s\n", msg); g_fail++; } \ +} while (0) + +// Build a syntactically-valid-enough CTransaction that GetHash()s uniquely +// per `nSeed` -- doesn't need to be consensus-valid, this test only +// exercises short-ID computation and reconstruction, not script validation. +static CTransaction MakeTx(int nSeed) +{ + CTransaction tx; + tx.nVersion = 1; + tx.nLockTime = nSeed; // cheapest way to make each tx's serialization (and hash) unique + return tx; +} + +int main() +{ + // Build a block: coinbase + 20 regular transactions. + CBlock block; + block.nVersion = 1; + block.hashPrevBlock = uint256("0x00000000000000000000000000000000000000000000000000000000000abc"); + block.nTime = 1234567890; + block.nBits = 0x1f0fffff; + block.nNonce = 42; + block.vtx.push_back(MakeTx(0)); // coinbase + for (int i = 1; i <= 20; i++) + block.vtx.push_back(MakeTx(i)); + block.hashMerkleRoot = block.BuildMerkleTree(); + + printf("build_and_reconstruct_full_mempool\n"); + { + CCmpctBlock cmpct; + CHECK(BuildCompactBlock(block, cmpct), "BuildCompactBlock succeeds"); + CHECK(cmpct.vPrefilledIndex.size() == 1 && cmpct.vPrefilledIndex[0] == 0, "only the coinbase is prefilled"); + CHECK(cmpct.vShortTxId.size() == 20, "one short ID per remaining tx"); + + // Simulate a peer with every transaction already in its mempool. + mapTransactions.clear(); + for (size_t i = 1; i < block.vtx.size(); i++) + mapTransactions[block.vtx[i].GetHash()] = block.vtx[i]; + + CBlock rebuilt; + vector vMissing; + CHECK(TryReconstructBlock(cmpct, rebuilt, vMissing), "fully reconstructs when mempool has everything"); + CHECK(vMissing.empty(), "nothing reported missing"); + CHECK(rebuilt.GetHash() == block.GetHash(), "reconstructed block hash matches original exactly"); + CHECK(rebuilt.vtx.size() == block.vtx.size(), "same transaction count"); + bool fAllMatch = true; + for (size_t i = 0; i < block.vtx.size(); i++) + if (rebuilt.vtx[i].GetHash() != block.vtx[i].GetHash()) fAllMatch = false; + CHECK(fAllMatch, "every transaction in the right position"); + } + + printf("partial_mempool_then_getblocktxn_completes_it\n"); + { + CCmpctBlock cmpct; + BuildCompactBlock(block, cmpct); + + // Simulate a peer missing 3 of the 20 non-coinbase transactions. + mapTransactions.clear(); + vector expectMissing; + for (size_t i = 1; i < block.vtx.size(); i++) + { + if (i == 5 || i == 12 || i == 19) { expectMissing.push_back((unsigned int)i); continue; } + mapTransactions[block.vtx[i].GetHash()] = block.vtx[i]; + } + + CBlock rebuilt; + vector vMissing; + CHECK(!TryReconstructBlock(cmpct, rebuilt, vMissing), "reports incomplete when mempool is missing some tx"); + CHECK(vMissing == expectMissing, "missing indices are exactly the ones not in mempool"); + + // Simulate the getblocktxn round trip: peer sends back the missing txs. + vector vTxn; + foreach(unsigned int idx, vMissing) + vTxn.push_back(block.vtx[idx]); + + CHECK(FillMissingAndFinish(rebuilt, vMissing, vTxn), "FillMissingAndFinish succeeds with matching response"); + CHECK(rebuilt.GetHash() == block.GetHash(), "block hash matches after filling in the missing tx"); + } + + printf("mismatched_blocktxn_response_rejected\n"); + { + CCmpctBlock cmpct; + BuildCompactBlock(block, cmpct); + mapTransactions.clear(); + mapTransactions[block.vtx[1].GetHash()] = block.vtx[1]; // only one in mempool + + CBlock rebuilt; + vector vMissing; + TryReconstructBlock(cmpct, rebuilt, vMissing); + CHECK(vMissing.size() == 19, "19 missing when mempool has only 1 of 20"); + + vector vWrongCount; + vWrongCount.push_back(block.vtx[2]); // way fewer than vMissing.size() + CHECK(!FillMissingAndFinish(rebuilt, vMissing, vWrongCount), "rejects a response with the wrong tx count"); + } + + printf("different_blocks_give_different_short_ids_for_same_tx\n"); + { + // Same transaction, two different (simulated) blocks -- short IDs + // must differ, since they're keyed per-block (hashPrevBlock + + // fresh nonce each time), not just derived from the txid alone. + CTransaction tx = MakeTx(99); + uint256 prevA = uint256("0x1111111111111111111111111111111111111111111111111111111111111111"); + uint256 prevB = uint256("0x2222222222222222222222222222222222222222222222222222222222222222"); + uint64 id1 = ComputeShortTxId(prevA, 0xAAAABBBBCCCCDDDDULL, tx.GetHash()); + uint64 id2 = ComputeShortTxId(prevB, 0xAAAABBBBCCCCDDDDULL, tx.GetHash()); + uint64 id3 = ComputeShortTxId(prevA, 0x1111222233334444ULL, tx.GetHash()); + CHECK(id1 != id2, "different hashPrevBlock -> different short ID"); + CHECK(id1 != id3, "different nonce -> different short ID"); + CHECK(id1 <= 0xFFFFFFFFFFFFULL, "short ID fits in 48 bits"); + } + + printf("\n%s (%d failures)\n", g_fail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", g_fail); + return g_fail == 0 ? 0 : 1; +} -- cgit v1.2.3