upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/main.cpp
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-07-28 12:55:04 -0300
committerGitHub <noreply@github.com>2026-07-28 12:55:04 -0300
commit663784eae9b04ec71e3e1eebf445910e5a8add42 (patch)
tree1482b0778a11c1b46cfbd476a5d91e12ef31204b /src/main.cpp
parent1d0314c0d220231afc56193ba66b70234fa24092 (diff)
Merge PR #6 (large refactor), preserving main's fixes (#9)
* 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 <relay>' 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 <nasci.victorio@gmail.com> Co-authored-by: Bitflash Pool Dev <pool@bitflash> Co-authored-by: Victorio Nascimento <155357371+Vic-Nas@users.noreply.github.com> Co-authored-by: Claude <claude@anthropic.com>
Diffstat (limited to 'src/main.cpp')
-rw-r--r--src/main.cpp631
1 files changed, 414 insertions, 217 deletions
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 @@
13#include "sha.h" 13#include "sha.h"
14#include <atomic> 14#include <atomic>
15#include <mutex> 15#include <mutex>
16#include <thread>
16#include "btfaddr.h" 17#include "btfaddr.h"
17#include "btftunnel.h" 18#include "btftunnel.h"
18#ifndef _WIN32 19#ifndef _WIN32
@@ -73,7 +74,7 @@ bool fSoloMineTest = false; // /solomine: mine without requiring a peer (local t
73int fGenerateBitcoins; 74int fGenerateBitcoins;
74int64 nTransactionFee = 0; 75int64 nTransactionFee = 0;
75CAddress addrIncoming; 76CAddress addrIncoming;
76int nMineMode = MINE_SOLO; 77int nMineMode = MINE_RELAY;
77string strParticipantPool; // participant mode: pool .btf address 78string strParticipantPool; // participant mode: pool .btf address
78string strPoolName = "Bitflash Pool"; 79string strPoolName = "Bitflash Pool";
79string strPoolDashboardUrl; 80string strPoolDashboardUrl;
@@ -88,11 +89,10 @@ static std::string gParticipantStatus;
88 89
89void SetParticipantMiningStatus(const std::string& status) 90void SetParticipantMiningStatus(const std::string& status)
90{ 91{
92 // Updates the GUI status string only. Logging is done at each call site
93 // with full context; duplicating it here just adds noise to the log.
91 std::lock_guard<std::mutex> lk(gParticipantStatusMutex); 94 std::lock_guard<std::mutex> lk(gParticipantStatusMutex);
92 if (gParticipantStatus == status)
93 return;
94 gParticipantStatus = status; 95 gParticipantStatus = status;
95 printf("PoolParticipantMiner: status -> %s\n", status.c_str());
96} 96}
97 97
98std::string GetParticipantMiningStatus() 98std::string GetParticipantMiningStatus()
@@ -159,7 +159,7 @@ bool AddToWallet(const CWalletTx& wtxIn)
159 wtx.nTimeReceived = GetAdjustedTime(); 159 wtx.nTimeReceived = GetAdjustedTime();
160 160
161 //// debug print 161 //// debug print
162 printf("AddToWallet %s %s\n", wtxIn.GetHash().ToString().substr(0,6).c_str(), fInsertedNew ? "new" : "update"); 162 if (LogAcceptsCategory("net")) printf("AddToWallet %s %s\n", wtxIn.GetHash().ToString().substr(0,6).c_str(), fInsertedNew ? "new" : "update");
163 163
164 if (!fInsertedNew) 164 if (!fInsertedNew)
165 { 165 {
@@ -372,7 +372,7 @@ int CMerkleTx::SetMerkleBranch(const CBlock* pblock)
372 { 372 {
373 vMerkleBranch.clear(); 373 vMerkleBranch.clear();
374 nIndex = -1; 374 nIndex = -1;
375 printf("ERROR: SetMerkleBranch() : couldn't find tx in block\n"); 375 if (LogAcceptsCategory("net")) printf("ERROR: SetMerkleBranch() : couldn't find tx in block\n");
376 return 0; 376 return 0;
377 } 377 }
378 378
@@ -433,7 +433,7 @@ void CWalletTx::AddSupportingTransactions(CTxDB& txdb)
433 } 433 }
434 else 434 else
435 { 435 {
436 printf("ERROR: AddSupportingTransactions() : unsupported transaction\n"); 436 if (LogAcceptsCategory("net")) printf("ERROR: AddSupportingTransactions() : unsupported transaction\n");
437 continue; 437 continue;
438 } 438 }
439 439
@@ -519,7 +519,7 @@ bool CTransaction::AcceptTransaction(CTxDB& txdb, bool fCheckInputs, bool* pfMis
519 { 519 {
520 if (ptxOld) 520 if (ptxOld)
521 { 521 {
522 printf("mapTransaction.erase(%s) replacing with new version\n", ptxOld->GetHash().ToString().c_str()); 522 if (LogAcceptsCategory("net")) printf("mapTransaction.erase(%s) replacing with new version\n", ptxOld->GetHash().ToString().c_str());
523 mapTransactions.erase(ptxOld->GetHash()); 523 mapTransactions.erase(ptxOld->GetHash());
524 } 524 }
525 AddToMemoryPool(); 525 AddToMemoryPool();
@@ -530,7 +530,7 @@ bool CTransaction::AcceptTransaction(CTxDB& txdb, bool fCheckInputs, bool* pfMis
530 if (ptxOld) 530 if (ptxOld)
531 EraseFromWallet(ptxOld->GetHash()); 531 EraseFromWallet(ptxOld->GetHash());
532 532
533 printf("AcceptTransaction(): accepted %s\n", hash.ToString().substr(0,6).c_str()); 533 if (LogAcceptsCategory("net")) printf("AcceptTransaction(): accepted %s\n", hash.ToString().substr(0,6).c_str());
534 return true; 534 return true;
535} 535}
536 536
@@ -669,7 +669,7 @@ void CWalletTx::RelayWalletTransaction(CTxDB& txdb)
669 uint256 hash = GetHash(); 669 uint256 hash = GetHash();
670 if (!txdb.ContainsTx(hash)) 670 if (!txdb.ContainsTx(hash))
671 { 671 {
672 printf("Relaying wtx %s\n", hash.ToString().substr(0,6).c_str()); 672 if (LogAcceptsCategory("net")) printf("Relaying wtx %s\n", hash.ToString().substr(0,6).c_str());
673 RelayMessage(CInv(MSG_TX, hash), (CTransaction)*this); 673 RelayMessage(CInv(MSG_TX, hash), (CTransaction)*this);
674 } 674 }
675 } 675 }
@@ -683,7 +683,7 @@ void RelayWalletTransactions()
683 nLastTime = GetTime(); 683 nLastTime = GetTime();
684 684
685 // Rebroadcast any of our txes that aren't in a block yet 685 // Rebroadcast any of our txes that aren't in a block yet
686 printf("RelayWalletTransactions()\n"); 686 if (LogAcceptsCategory("net")) printf("RelayWalletTransactions()\n");
687 CTxDB txdb("r"); 687 CTxDB txdb("r");
688 CRITICAL_BLOCK(cs_mapWallet) 688 CRITICAL_BLOCK(cs_mapWallet)
689 { 689 {
@@ -764,7 +764,7 @@ unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast)
764 764
765 // Limit adjustment step 765 // Limit adjustment step
766 unsigned int nActualTimespan = pindexLast->nTime - pindexFirst->nTime; 766 unsigned int nActualTimespan = pindexLast->nTime - pindexFirst->nTime;
767 printf(" nActualTimespan = %d before bounds\n", nActualTimespan); 767 if (LogAcceptsCategory("net")) printf(" nActualTimespan = %d before bounds\n", nActualTimespan);
768 if (nActualTimespan < nTargetTimespan/4) 768 if (nActualTimespan < nTargetTimespan/4)
769 nActualTimespan = nTargetTimespan/4; 769 nActualTimespan = nTargetTimespan/4;
770 if (nActualTimespan > nTargetTimespan*4) 770 if (nActualTimespan > nTargetTimespan*4)
@@ -780,10 +780,7 @@ unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast)
780 bnNew = bnProofOfWorkLimit; 780 bnNew = bnProofOfWorkLimit;
781 781
782 /// debug print 782 /// debug print
783 printf("\n\n\nGetNextWorkRequired RETARGET *****\n"); 783 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()); }
784 printf("nTargetTimespan = %d nActualTimespan = %d\n", nTargetTimespan, nActualTimespan);
785 printf("Before: %08x %s\n", pindexLast->nBits, CBigNum().SetCompact(pindexLast->nBits).getuint256().ToString().c_str());
786 printf("After: %08x %s\n", bnNew.GetCompact(), bnNew.getuint256().ToString().c_str());
787 784
788 return bnNew.GetCompact(); 785 return bnNew.GetCompact();
789} 786}
@@ -1037,7 +1034,7 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex)
1037 1034
1038bool Reorganize(CTxDB& txdb, CBlockIndex* pindexNew) 1035bool Reorganize(CTxDB& txdb, CBlockIndex* pindexNew)
1039{ 1036{
1040 printf("*** REORGANIZE ***\n"); 1037 LogPrint("net", "*** REORGANIZE ***\n");
1041 1038
1042 // Find the fork 1039 // Find the fork
1043 CBlockIndex* pfork = pindexBest; 1040 CBlockIndex* pfork = pindexBest;
@@ -1712,7 +1709,7 @@ bool ProcessMessages(CNode* pfrom)
1712 CDataStream& vRecv = pfrom->vRecv; 1709 CDataStream& vRecv = pfrom->vRecv;
1713 if (vRecv.empty()) 1710 if (vRecv.empty())
1714 return true; 1711 return true;
1715 printf("ProcessMessages(%d bytes)\n", vRecv.size()); 1712 LogPrint("net", "ProcessMessages(%d bytes)\n", vRecv.size());
1716 1713
1717 // 1714 //
1718 // Message format 1715 // Message format
@@ -1730,13 +1727,13 @@ bool ProcessMessages(CNode* pfrom)
1730 { 1727 {
1731 if (vRecv.size() > sizeof(CMessageHeader)) 1728 if (vRecv.size() > sizeof(CMessageHeader))
1732 { 1729 {
1733 printf("\n\nPROCESSMESSAGE MESSAGESTART NOT FOUND\n\n"); 1730 if (LogAcceptsCategory("net")) printf("\n\nPROCESSMESSAGE MESSAGESTART NOT FOUND\n\n");
1734 vRecv.erase(vRecv.begin(), vRecv.end() - sizeof(CMessageHeader)); 1731 vRecv.erase(vRecv.begin(), vRecv.end() - sizeof(CMessageHeader));
1735 } 1732 }
1736 break; 1733 break;
1737 } 1734 }
1738 if (pstart - vRecv.begin() > 0) 1735 if (pstart - vRecv.begin() > 0)
1739 printf("\n\nPROCESSMESSAGE SKIPPED %d BYTES\n\n", pstart - vRecv.begin()); 1736 if (LogAcceptsCategory("net")) printf("\n\nPROCESSMESSAGE SKIPPED %d BYTES\n\n", pstart - vRecv.begin());
1740 vRecv.erase(vRecv.begin(), pstart); 1737 vRecv.erase(vRecv.begin(), pstart);
1741 1738
1742 // Read header 1739 // Read header
@@ -1744,7 +1741,7 @@ bool ProcessMessages(CNode* pfrom)
1744 vRecv >> hdr; 1741 vRecv >> hdr;
1745 if (!hdr.IsValid()) 1742 if (!hdr.IsValid())
1746 { 1743 {
1747 printf("\n\nPROCESSMESSAGE: ERRORS IN HEADER %s\n\n\n", hdr.GetCommand().c_str()); 1744 if (LogAcceptsCategory("net")) printf("\n\nPROCESSMESSAGE: ERRORS IN HEADER %s\n\n\n", hdr.GetCommand().c_str());
1748 continue; 1745 continue;
1749 } 1746 }
1750 string strCommand = hdr.GetCommand(); 1747 string strCommand = hdr.GetCommand();
@@ -1755,7 +1752,7 @@ bool ProcessMessages(CNode* pfrom)
1755 { 1752 {
1756 // Rewind and wait for rest of message 1753 // Rewind and wait for rest of message
1757 ///// need a mechanism to give up waiting for overlong message size error 1754 ///// need a mechanism to give up waiting for overlong message size error
1758 printf("MESSAGE-BREAK 2\n"); 1755 if (LogAcceptsCategory("net")) printf("MESSAGE-BREAK 2\n");
1759 vRecv.insert(vRecv.begin(), BEGIN(hdr), END(hdr)); 1756 vRecv.insert(vRecv.begin(), BEGIN(hdr), END(hdr));
1760 Sleep(100); 1757 Sleep(100);
1761 break; 1758 break;
@@ -1776,7 +1773,7 @@ bool ProcessMessages(CNode* pfrom)
1776 } 1773 }
1777 CATCH_PRINT_EXCEPTION("ProcessMessage()") 1774 CATCH_PRINT_EXCEPTION("ProcessMessage()")
1778 if (!fRet) 1775 if (!fRet)
1779 printf("ProcessMessage(%s, %d bytes) from %s to %s FAILED\n", strCommand.c_str(), nMessageSize, pfrom->addr.ToString().c_str(), addrLocalHost.ToString().c_str()); 1776 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());
1780 } 1777 }
1781 1778
1782 vRecv.Compact(); 1779 vRecv.Compact();
@@ -1789,13 +1786,10 @@ bool ProcessMessages(CNode* pfrom)
1789bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) 1786bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
1790{ 1787{
1791 static map<unsigned int, vector<unsigned char> > mapReuseKey; 1788 static map<unsigned int, vector<unsigned char> > mapReuseKey;
1792 printf("received: %-12s (%d bytes) ", strCommand.c_str(), vRecv.size()); 1789 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"); }
1793 for (int i = 0; i < min(vRecv.size(), (unsigned int)25); i++)
1794 printf("%02x ", vRecv[i] & 0xff);
1795 printf("\n");
1796 if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0) 1790 if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0)
1797 { 1791 {
1798 printf("dropmessages DROPPING RECV MESSAGE\n"); 1792 if (LogAcceptsCategory("net")) printf("dropmessages DROPPING RECV MESSAGE\n");
1799 return true; 1793 return true;
1800 } 1794 }
1801 1795
@@ -1833,7 +1827,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
1833 pfrom->PushMessage("getblocks", CBlockLocator(pindexBest), uint256(0)); 1827 pfrom->PushMessage("getblocks", CBlockLocator(pindexBest), uint256(0));
1834 } 1828 }
1835 1829
1836 printf("version addrMe = %s\n", addrMe.ToString().c_str()); 1830 if (LogAcceptsCategory("net")) printf("version addrMe = %s\n", addrMe.ToString().c_str());
1837 } 1831 }
1838 1832
1839 1833
@@ -1844,30 +1838,6 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
1844 } 1838 }
1845 1839
1846 1840
1847 else if (strCommand == "addr")
1848 {
1849 vector<CAddress> vAddr;
1850 vRecv >> vAddr;
1851
1852 // Store the new addresses
1853 CAddrDB addrdb;
1854 foreach(const CAddress& addr, vAddr)
1855 {
1856 if (fShutdown)
1857 return true;
1858 if (AddAddress(addrdb, addr))
1859 {
1860 // Put on lists to send to other nodes
1861 pfrom->setAddrKnown.insert(addr);
1862 CRITICAL_BLOCK(cs_vNodes)
1863 foreach(CNode* pnode, vNodes)
1864 if (!pnode->setAddrKnown.count(addr))
1865 pnode->vAddrToSend.push_back(addr);
1866 }
1867 }
1868 }
1869
1870
1871 else if (strCommand == "inv") 1841 else if (strCommand == "inv")
1872 { 1842 {
1873 vector<CInv> vInv; 1843 vector<CInv> vInv;
@@ -1881,7 +1851,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
1881 pfrom->AddInventoryKnown(inv); 1851 pfrom->AddInventoryKnown(inv);
1882 1852
1883 bool fAlreadyHave = AlreadyHave(txdb, inv); 1853 bool fAlreadyHave = AlreadyHave(txdb, inv);
1884 printf(" got inventory: %s %s\n", inv.ToString().c_str(), fAlreadyHave ? "have" : "new"); 1854 if (LogAcceptsCategory("net")) printf(" got inventory: %s %s\n", inv.ToString().c_str(), fAlreadyHave ? "have" : "new");
1885 1855
1886 if (!fAlreadyHave) 1856 if (!fAlreadyHave)
1887 pfrom->AskFor(inv); 1857 pfrom->AskFor(inv);
@@ -1900,7 +1870,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
1900 { 1870 {
1901 if (fShutdown) 1871 if (fShutdown)
1902 return true; 1872 return true;
1903 printf("received getdata for: %s\n", inv.ToString().c_str()); 1873 if (LogAcceptsCategory("net")) printf("received getdata for: %s\n", inv.ToString().c_str());
1904 1874
1905 if (inv.type == MSG_BLOCK) 1875 if (inv.type == MSG_BLOCK)
1906 { 1876 {
@@ -1940,12 +1910,12 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
1940 // Send the rest of the chain 1910 // Send the rest of the chain
1941 if (pindex) 1911 if (pindex)
1942 pindex = pindex->pnext; 1912 pindex = pindex->pnext;
1943 printf("getblocks %d to %s\n", (pindex ? pindex->nHeight : -1), hashStop.ToString().substr(0,14).c_str()); 1913 if (LogAcceptsCategory("net")) printf("getblocks %d to %s\n", (pindex ? pindex->nHeight : -1), hashStop.ToString().substr(0,14).c_str());
1944 for (; pindex; pindex = pindex->pnext) 1914 for (; pindex; pindex = pindex->pnext)
1945 { 1915 {
1946 if (pindex->GetBlockHash() == hashStop) 1916 if (pindex->GetBlockHash() == hashStop)
1947 { 1917 {
1948 printf(" getblocks stopping at %d %s\n", pindex->nHeight, pindex->GetBlockHash().ToString().substr(0,14).c_str()); 1918 if (LogAcceptsCategory("net")) printf(" getblocks stopping at %d %s\n", pindex->nHeight, pindex->GetBlockHash().ToString().substr(0,14).c_str());
1949 break; 1919 break;
1950 } 1920 }
1951 1921
@@ -1997,7 +1967,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
1997 1967
1998 if (tx.AcceptTransaction(true)) 1968 if (tx.AcceptTransaction(true))
1999 { 1969 {
2000 printf(" accepted orphan tx %s\n", inv.hash.ToString().substr(0,6).c_str()); 1970 if (LogAcceptsCategory("net")) printf(" accepted orphan tx %s\n", inv.hash.ToString().substr(0,6).c_str());
2001 AddToWalletIfMine(tx, NULL); 1971 AddToWalletIfMine(tx, NULL);
2002 RelayMessage(inv, vMsg); 1972 RelayMessage(inv, vMsg);
2003 mapAlreadyAskedFor.erase(inv); 1973 mapAlreadyAskedFor.erase(inv);
@@ -2011,7 +1981,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
2011 } 1981 }
2012 else if (fMissingInputs) 1982 else if (fMissingInputs)
2013 { 1983 {
2014 printf("storing orphan tx %s\n", inv.hash.ToString().substr(0,6).c_str()); 1984 if (LogAcceptsCategory("net")) printf("storing orphan tx %s\n", inv.hash.ToString().substr(0,6).c_str());
2015 AddOrphanTx(vMsg); 1985 AddOrphanTx(vMsg);
2016 } 1986 }
2017 } 1987 }
@@ -2041,7 +2011,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
2041 vRecv >> *pblock; 2011 vRecv >> *pblock;
2042 2012
2043 //// debug print 2013 //// debug print
2044 printf("received block:\n"); pblock->print(); 2014 if (LogAcceptsCategory("net")) { printf("received block:\n"); pblock->print(); }
2045 2015
2046 CInv inv(MSG_BLOCK, pblock->GetHash()); 2016 CInv inv(MSG_BLOCK, pblock->GetHash());
2047 pfrom->AddInventoryKnown(inv); 2017 pfrom->AddInventoryKnown(inv);
@@ -2051,25 +2021,6 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
2051 } 2021 }
2052 2022
2053 2023
2054 else if (strCommand == "getaddr")
2055 {
2056 pfrom->vAddrToSend.clear();
2057 //// need to expand the time range if not enough found
2058 int64 nSince = GetAdjustedTime() - 60 * 60; // in the last hour
2059 CRITICAL_BLOCK(cs_mapAddresses)
2060 {
2061 foreach(const PAIRTYPE(vector<unsigned char>, CAddress)& item, mapAddresses)
2062 {
2063 if (fShutdown)
2064 return true;
2065 const CAddress& addr = item.second;
2066 if (addr.nTime > nSince)
2067 pfrom->vAddrToSend.push_back(addr);
2068 }
2069 }
2070 }
2071
2072
2073 else if (strCommand == "checkorder") 2024 else if (strCommand == "checkorder")
2074 { 2025 {
2075 uint256 hashReply; 2026 uint256 hashReply;
@@ -2134,12 +2085,12 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
2134 else 2085 else
2135 { 2086 {
2136 // Ignore unknown commands for extensibility 2087 // Ignore unknown commands for extensibility
2137 printf("ProcessMessage(%s) : Ignored unknown message\n", strCommand.c_str()); 2088 if (LogAcceptsCategory("net")) printf("ProcessMessage(%s) : Ignored unknown message\n", strCommand.c_str());
2138 } 2089 }
2139 2090
2140 2091
2141 if (!vRecv.empty()) 2092 if (!vRecv.empty())
2142 printf("ProcessMessage(%s) : %d extra bytes\n", strCommand.c_str(), vRecv.size()); 2093 if (LogAcceptsCategory("net")) printf("ProcessMessage(%s) : %d extra bytes\n", strCommand.c_str(), vRecv.size());
2143 2094
2144 return true; 2095 return true;
2145} 2096}
@@ -2163,19 +2114,6 @@ bool SendMessages(CNode* pto)
2163 2114
2164 2115
2165 // 2116 //
2166 // Message: addr
2167 //
2168 vector<CAddress> vAddrToSend;
2169 vAddrToSend.reserve(pto->vAddrToSend.size());
2170 foreach(const CAddress& addr, pto->vAddrToSend)
2171 if (!pto->setAddrKnown.count(addr))
2172 vAddrToSend.push_back(addr);
2173 pto->vAddrToSend.clear();
2174 if (!vAddrToSend.empty())
2175 pto->PushMessage("addr", vAddrToSend);
2176
2177
2178 //
2179 // Message: inventory 2117 // Message: inventory
2180 // 2118 //
2181 vector<CInv> vInventoryToSend; 2119 vector<CInv> vInventoryToSend;
@@ -2204,7 +2142,7 @@ bool SendMessages(CNode* pto)
2204 while (!pto->mapAskFor.empty() && (*pto->mapAskFor.begin()).first <= nNow) 2142 while (!pto->mapAskFor.empty() && (*pto->mapAskFor.begin()).first <= nNow)
2205 { 2143 {
2206 const CInv& inv = (*pto->mapAskFor.begin()).second; 2144 const CInv& inv = (*pto->mapAskFor.begin()).second;
2207 printf("sending getdata: %s\n", inv.ToString().c_str()); 2145 if (LogAcceptsCategory("net")) printf("sending getdata: %s\n", inv.ToString().c_str());
2208 if (!AlreadyHave(txdb, inv)) 2146 if (!AlreadyHave(txdb, inv))
2209 vAskFor.push_back(inv); 2147 vAskFor.push_back(inv);
2210 pto->mapAskFor.erase(pto->mapAskFor.begin()); 2148 pto->mapAskFor.erase(pto->mapAskFor.begin());
@@ -2280,13 +2218,21 @@ void BlockSHA256(const void* pin, unsigned int nBlocks, void* pout)
2280 2218
2281 2219
2282// --------------------------------------------------------------------------- 2220// ---------------------------------------------------------------------------
2283// Participant pool miner — connects to an external Stratum server, receives 2221// Participant pool miner -- connects to an external Stratum server, receives
2284// RandomX jobs, hashes with the local CPU, and submits shares. 2222// RandomX jobs, hashes with the local CPU, and submits shares.
2285// Called from BitcoinMiner() when nMineMode == MINE_PARTICIPANT. 2223// Called from BitcoinMiner() when nMineMode == MINE_PARTICIPANT.
2286// --------------------------------------------------------------------------- 2224// ---------------------------------------------------------------------------
2287 2225
2288using json = nlohmann::json; 2226using json = nlohmann::json;
2289 2227
2228
2229// ---------------------------------------------------------------------------
2230// Participant miner -- connects to a pool over a .btf tunnel, runs Stratum,
2231// hashes with RandomX, submits shares. Called from BitcoinMiner().
2232// ---------------------------------------------------------------------------
2233
2234using json = nlohmann::json;
2235
2290static std::string ToHexStr(const void* p, size_t n) 2236static std::string ToHexStr(const void* p, size_t n)
2291{ 2237{
2292 const unsigned char* b = (const unsigned char*)p; 2238 const unsigned char* b = (const unsigned char*)p;
@@ -2300,10 +2246,10 @@ static std::vector<unsigned char> FromHexStr(const std::string& s)
2300{ 2246{
2301 std::vector<unsigned char> v; 2247 std::vector<unsigned char> v;
2302 for (size_t i = 0; i+1 < s.size(); i += 2) { 2248 for (size_t i = 0; i+1 < s.size(); i += 2) {
2303 auto h=[](char c)->int{ 2249 auto h = [](char c)->int{
2304 if(c>='0'&&c<='9')return c-'0'; 2250 if(c>='0'&&c<='9') return c-'0';
2305 if(c>='a'&&c<='f')return c-'a'+10; 2251 if(c>='a'&&c<='f') return c-'a'+10;
2306 if(c>='A'&&c<='F')return c-'A'+10; 2252 if(c>='A'&&c<='F') return c-'A'+10;
2307 return 0; 2253 return 0;
2308 }; 2254 };
2309 v.push_back((unsigned char)((h(s[i])<<4)|h(s[i+1]))); 2255 v.push_back((unsigned char)((h(s[i])<<4)|h(s[i+1])));
@@ -2313,14 +2259,13 @@ static std::vector<unsigned char> FromHexStr(const std::string& s)
2313 2259
2314static void UpdateParticipantHashRate(uint64 hashesSinceSample, int64 sampleStart) 2260static void UpdateParticipantHashRate(uint64 hashesSinceSample, int64 sampleStart)
2315{ 2261{
2316 int64 now = GetTime(); 2262 int64 elapsed = GetTime() - sampleStart;
2317 int64 elapsed = now - sampleStart; 2263 if (elapsed <= 0) elapsed = 1;
2318 if (elapsed <= 0) 2264 gParticipantHashRateX1000.store(
2319 elapsed = 1; 2265 (uint64)((double)hashesSinceSample / (double)elapsed * 1000.0));
2320 double rate = (double)hashesSinceSample / (double)elapsed;
2321 gParticipantHashRateX1000.store((uint64)(rate * 1000.0));
2322} 2266}
2323 2267
2268// Blocking send of a JSON line to the pool.
2324static bool StratumSendLine(SOCKET s, const json& j) 2269static bool StratumSendLine(SOCKET s, const json& j)
2325{ 2270{
2326 std::string line = j.dump() + "\n"; 2271 std::string line = j.dump() + "\n";
@@ -2333,38 +2278,59 @@ static bool StratumSendLine(SOCKET s, const json& j)
2333 return true; 2278 return true;
2334} 2279}
2335 2280
2336static bool StratumRecvLine(SOCKET s, std::string& buf, std::string& line) 2281// Non-blocking recv into carry buffer. Returns true + fills line when a
2282// complete newline-terminated message is ready. Returns false in two cases:
2283// disconnected=true -- socket closed by peer; caller must stop
2284// disconnected=false -- no complete line yet; caller should select() and retry
2285static bool StratumRecvLine(SOCKET s, std::string& buf,
2286 std::string& line, bool& disconnected)
2337{ 2287{
2338 while (true) { 2288 disconnected = false;
2339 size_t pos = buf.find('\n'); 2289 char tmp[4096];
2340 if (pos != std::string::npos) { 2290#ifdef _WIN32
2341 line = buf.substr(0, pos); 2291 u_long avail = 0;
2342 if (!line.empty() && line.back()=='\r') line.pop_back(); 2292 ioctlsocket(s, FIONREAD, &avail);
2343 buf = buf.substr(pos+1); 2293 if (avail > 0) {
2344 return true; 2294 int r = recv(s, tmp, (int)std::min((u_long)(sizeof(tmp)-1), avail), 0);
2345 } 2295 if (r > 0) { tmp[r] = 0; buf += tmp; }
2346 char tmp[4096]; 2296 else if (r == 0) { disconnected = true; return false; }
2347 int r = recv(s, tmp, sizeof(tmp)-1, 0); 2297 }
2348 if (r <= 0) return false; 2298#else
2349 tmp[r] = 0; 2299 {
2350 buf += tmp; 2300 int r = recv(s, tmp, sizeof(tmp)-1, MSG_DONTWAIT);
2301 if (r > 0) { tmp[r] = 0; buf += tmp; }
2302 else if (r == 0) { disconnected = true; return false; }
2303 // r < 0: EAGAIN -- no data right now
2351 } 2304 }
2305#endif
2306 size_t pos = buf.find('\n');
2307 if (pos == std::string::npos) return false;
2308 line = buf.substr(0, pos);
2309 if (!line.empty() && line.back() == '\r') line.pop_back();
2310 buf = buf.substr(pos + 1);
2311 return true;
2352} 2312}
2353 2313
2314// How long to wait for a mining.notify after authorize before giving up
2315static const int64 STUCK_NO_JOB_TIMEOUT_SECS = 120;
2316
2354static bool PoolParticipantMiner() 2317static bool PoolParticipantMiner()
2355{ 2318{
2356 printf("PoolParticipantMiner: connecting to %s\n", strParticipantPool.c_str()); 2319 // ------------------------------------------------------------------
2357 SetParticipantMiningStatus("resolving pool address"); 2320 // Step 1: resolve .btf address -> meeting node
2358 2321 // ------------------------------------------------------------------
2359 if (strParticipantPool.empty()) { 2322 if (strParticipantPool.empty()) {
2360 printf("PoolParticipantMiner: no pool selected\n"); 2323 LogPrint("worker", "[worker] no pool selected\n");
2361 SetParticipantMiningStatus("no pool selected"); 2324 SetParticipantMiningStatus("no pool selected");
2362 return false; 2325 return false;
2363 } 2326 }
2364 2327
2328 LogPrint("worker", "[worker] connecting to pool %s\n", strParticipantPool.c_str());
2329 SetParticipantMiningStatus("resolving pool address");
2330
2365 unsigned char target_pubkey[32]; 2331 unsigned char target_pubkey[32];
2366 if (!btf::ParseAddress(strParticipantPool, target_pubkey)) { 2332 if (!btf::ParseAddress(strParticipantPool, target_pubkey)) {
2367 printf("PoolParticipantMiner: invalid .btf address %s\n", strParticipantPool.c_str()); 2333 LogPrint("worker", "[worker] invalid .btf address: %s\n", strParticipantPool.c_str());
2368 SetParticipantMiningStatus("invalid pool address"); 2334 SetParticipantMiningStatus("invalid pool address");
2369 return false; 2335 return false;
2370 } 2336 }
@@ -2372,35 +2338,48 @@ static bool PoolParticipantMiner()
2372 std::string meetingHostPort; 2338 std::string meetingHostPort;
2373 unsigned char service_enc_pub[32]; 2339 unsigned char service_enc_pub[32];
2374 if (!BtfResolve(strParticipantPool, meetingHostPort, service_enc_pub)) { 2340 if (!BtfResolve(strParticipantPool, meetingHostPort, service_enc_pub)) {
2375 printf("PoolParticipantMiner: failed to resolve %s\n", strParticipantPool.c_str()); 2341 LogPrint("worker", "[worker] failed to resolve pool %s"
2342 " (no Nostr descriptor found -- pool may be offline or relays down)\n",
2343 strParticipantPool.c_str());
2376 SetParticipantMiningStatus("failed to resolve pool"); 2344 SetParticipantMiningStatus("failed to resolve pool");
2377 return false; 2345 return false;
2378 } 2346 }
2347 LogPrint("worker", "[worker] pool resolved to meeting node %s\n", meetingHostPort.c_str());
2379 2348
2380 size_t colon = meetingHostPort.rfind(':'); 2349 size_t colon = meetingHostPort.rfind(':');
2381 if (colon == std::string::npos) { 2350 if (colon == std::string::npos) {
2382 printf("PoolParticipantMiner: bad meeting node %s\n", meetingHostPort.c_str()); 2351 LogPrint("worker", "[worker] bad meeting node format '%s'\n", meetingHostPort.c_str());
2383 SetParticipantMiningStatus("bad pool endpoint"); 2352 SetParticipantMiningStatus("bad pool endpoint");
2384 return false; 2353 return false;
2385 } 2354 }
2386 std::string host = meetingHostPort.substr(0, colon); 2355 std::string host = meetingHostPort.substr(0, colon);
2387 int port = atoi(meetingHostPort.substr(colon + 1).c_str()); 2356 int port = atoi(meetingHostPort.substr(colon + 1).c_str());
2388 if (port <= 0 || port > 65535) { 2357 if (port <= 0 || port > 65535) {
2389 printf("PoolParticipantMiner: bad meeting port %d\n", port); 2358 LogPrint("worker", "[worker] bad meeting port %d in '%s'\n",
2359 port, meetingHostPort.c_str());
2390 SetParticipantMiningStatus("bad pool port"); 2360 SetParticipantMiningStatus("bad pool port");
2391 return false; 2361 return false;
2392 } 2362 }
2393 2363
2394 btf_socket_t s = btf::BtfClientTunnel(host.c_str(), (unsigned short)port, target_pubkey, service_enc_pub); 2364 // ------------------------------------------------------------------
2365 // Step 2: open encrypted tunnel to pool
2366 // ------------------------------------------------------------------
2367 SetParticipantMiningStatus("opening tunnel");
2368 btf_socket_t s = btf::BtfClientTunnel(host.c_str(), (unsigned short)port,
2369 target_pubkey, service_enc_pub);
2395 if (s == INVALID_SOCKET) { 2370 if (s == INVALID_SOCKET) {
2396 printf("PoolParticipantMiner: tunnel connect failed via %s\n", meetingHostPort.c_str()); 2371 LogPrint("worker", "[worker] tunnel connect FAILED via %s"
2372 " (relay unreachable or pool not listening)\n",
2373 meetingHostPort.c_str());
2397 SetParticipantMiningStatus("tunnel connect failed"); 2374 SetParticipantMiningStatus("tunnel connect failed");
2398 return false; 2375 return false;
2399 } 2376 }
2400 printf("PoolParticipantMiner: connected via %s\n", meetingHostPort.c_str()); 2377 LogPrint("worker", "[worker] tunnel open via %s\n", meetingHostPort.c_str());
2401 SetParticipantMiningStatus("tunnel connected"); 2378 SetParticipantMiningStatus("tunnel open");
2402 2379
2403 // Use our wallet address as mining username so payouts go to our wallet 2380 // ------------------------------------------------------------------
2381 // Step 3: determine our payout address (mining username)
2382 // ------------------------------------------------------------------
2404 std::string username; 2383 std::string username;
2405 { 2384 {
2406 std::vector<unsigned char> vchPubKey; 2385 std::vector<unsigned char> vchPubKey;
@@ -2408,102 +2387,260 @@ static bool PoolParticipantMiner()
2408 username = PubKeyToAddress(vchPubKey); 2387 username = PubKeyToAddress(vchPubKey);
2409 } 2388 }
2410 if (username.empty()) username = BtfLocalAddress(); 2389 if (username.empty()) username = BtfLocalAddress();
2411 if (username.empty()) username = "unknown"; 2390 if (username.empty()) {
2412 2391 LogPrint("worker", "[worker] WARNING: could not determine wallet address"
2392 " -- shares will not pay out\n");
2393 username = "unknown";
2394 }
2395 LogPrint("worker", "[worker] payout address: %s\n", username.c_str());
2396
2397 // ------------------------------------------------------------------
2398 // Step 4: build the fast (~2 GB dataset) RandomX mode if not already
2399 // built, then start our VM. Without this, RandomXCreateMinerVM() falls
2400 // back to "light" mode (cache only) forever, which is dramatically
2401 // slower per hash than fast mode -- it recomputes dataset items on the
2402 // fly for every single hash instead of a direct memory lookup. That
2403 // alone can turn a "few seconds per share" pool into "many minutes per
2404 // share" regardless of how easy the share target is.
2405 //
2406 // This MUST happen before we send mining.subscribe/authorize, not
2407 // after: it's a synchronous, single-threaded-per-call build that can
2408 // take anywhere from several seconds to a couple of minutes depending
2409 // on core count. If it runs after the handshake messages are sent,
2410 // the pool's replies (subscribe ack, authorize ack, set_difficulty,
2411 // and the first job) all land in the OS socket buffer while we're
2412 // not reading -- nothing is lost, but we sit there looking (and
2413 // logging) like we're "stuck waiting for pool response" for the
2414 // entire build duration, when really we're just not listening yet.
2415 // Building the dataset first means that by the time we say
2416 // "subscribing", we're genuinely ready to read the reply the instant
2417 // it arrives.
2418 // ------------------------------------------------------------------
2419 if (!RandomXFastReady()) {
2420 SetParticipantMiningStatus("building RandomX dataset (~2GB, one-time)");
2421 unsigned int nThreads = std::thread::hardware_concurrency();
2422 RandomXInitDataset(nThreads > 0 ? (int)nThreads : 1);
2423 }
2424 void* rxvm = RandomXCreateMinerVM();
2425 if (!rxvm) {
2426 LogPrint("worker", "[worker] failed to create RandomX VM\n");
2427 SetParticipantMiningStatus("RandomX init failed");
2428 closesocket(s); return false;
2429 }
2430 LogPrint("worker", "[worker] RandomX VM ready (%s)\n",
2431 RandomXFastReady() ? "fast 2GB" : "light 256MB");
2432
2433 // ------------------------------------------------------------------
2434 // Step 5: Stratum subscribe + authorize. The socket is read from
2435 // immediately after this, in the main loop below, so nothing sits
2436 // unread behind a blocking dataset build anymore.
2437 // ------------------------------------------------------------------
2413 std::string buf; 2438 std::string buf;
2414 int msgId = 1; 2439 int msgId = 1;
2415 std::set<int> pendingSubmitIds; 2440 std::set<int> pendingSubmitIds;
2416 2441
2417 // Subscribe
2418 SetParticipantMiningStatus("subscribing"); 2442 SetParticipantMiningStatus("subscribing");
2419 if (!StratumSendLine(s, json{{"id",msgId++},{"method","mining.subscribe"},{"params",json::array()}})) 2443 LogPrint("worker", "[worker->pool] sending mining.subscribe\n");
2420 { SetParticipantMiningStatus("subscribe failed"); closesocket(s); return false; } 2444 if (!StratumSendLine(s, json{{"id",msgId++},{"method","mining.subscribe"},
2445 {"params",json::array()}})) {
2446 LogPrint("worker", "[worker] subscribe send FAILED -- pool dropped connection\n");
2447 SetParticipantMiningStatus("subscribe send failed");
2448 RandomXDestroyMinerVM(rxvm);
2449 closesocket(s); return false;
2450 }
2421 2451
2422 // Authorize
2423 SetParticipantMiningStatus("authorizing"); 2452 SetParticipantMiningStatus("authorizing");
2453 LogPrint("worker", "[worker->pool] sending mining.authorize as %s\n", username.c_str());
2424 if (!StratumSendLine(s, json{{"id",msgId++},{"method","mining.authorize"}, 2454 if (!StratumSendLine(s, json{{"id",msgId++},{"method","mining.authorize"},
2425 {"params",json::array({username,"x"})}})) 2455 {"params",json::array({username,"x"})}})) {
2426 { SetParticipantMiningStatus("authorize failed"); closesocket(s); return false; } 2456 LogPrint("worker", "[worker] authorize send FAILED -- pool dropped connection\n");
2457 SetParticipantMiningStatus("authorize send failed");
2458 RandomXDestroyMinerVM(rxvm);
2459 closesocket(s); return false;
2460 }
2427 2461
2428 // Job state 2462 // ------------------------------------------------------------------
2463 // Step 6: main loop -- receive messages, hash, submit shares
2464 // ------------------------------------------------------------------
2429 std::string jobId; 2465 std::string jobId;
2430 unsigned char header[80]; 2466 unsigned char header[80] = {};
2431 uint256 target; 2467 uint256 target;
2432 bool haveJob = false; 2468 double currentDifficulty = 1.0; // updated by mining.set_difficulty from pool
2469 bool haveJob = false;
2433 bool gotSubscribeAck = false; 2470 bool gotSubscribeAck = false;
2434 bool gotAuthorizeAck = false; 2471 bool gotAuthorizeAck = false;
2435 int64 authWaitStart = 0; 2472 int64 authWaitStart = 0;
2436 unsigned int nNonce = 1; 2473 int64 lastStuckLog = 0;
2437 uint64 hashesSinceSample = 0; 2474 unsigned int nNonce = 1;
2438 int64 sampleStart = GetTime(); 2475 uint64 hashesSinceSample = 0;
2476 uint64 sessionSharesSent = 0; // this connection only
2477 uint64 sessionSharesAccepted = 0;
2478 int64 sampleStart = GetTime();
2439 2479
2440 void* rxvm = RandomXCreateMinerVM(); 2480 SetParticipantMiningStatus("waiting for pool response");
2441 if (!rxvm) { SetParticipantMiningStatus("failed to start RandomX miner"); closesocket(s); return false; }
2442 printf("PoolParticipantMiner: RandomX VM ready (%s)\n",
2443 RandomXFastReady() ? "fast 2GB" : "light 256MB");
2444 SetParticipantMiningStatus("authorized; asking pool for work");
2445 2481
2446 while (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) 2482 while (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown)
2447 { 2483 {
2448 // Non-blocking receive check 2484 // -- Receive: check buffer first, then select --
2449 { 2485 {
2450 fd_set fds; FD_ZERO(&fds); FD_SET(s, &fds); 2486 bool haveBuffered = buf.find('\n') != std::string::npos;
2451 struct timeval tv={0,50000}; // 50ms 2487 bool readable = false;
2452 if (select((int)s+1, &fds, NULL, NULL, &tv) > 0) { 2488 if (!haveBuffered) {
2489 fd_set fds; FD_ZERO(&fds); FD_SET(s, &fds);
2490 struct timeval tv = {0, 50000}; // 50ms
2491 readable = select((int)s+1, &fds, NULL, NULL, &tv) > 0;
2492 }
2493
2494 if (haveBuffered || readable) {
2453 std::string line; 2495 std::string line;
2454 if (!StratumRecvLine(s, buf, line)) break; 2496 bool disc = false;
2455 try { 2497 while (StratumRecvLine(s, buf, line, disc)) {
2456 json msg = json::parse(line); 2498 json msg;
2457 if (msg.contains("id") && msg["id"].is_number_integer() && msg.contains("result")) { 2499 try { msg = json::parse(line); } catch (...) {
2458 int id = msg["id"].get<int>(); 2500 LogPrint("worker", "[pool->worker] JSON parse error: %.80s\n",
2459 if (id == 1 && msg["result"].is_boolean() && msg["result"].get<bool>()) { 2501 line.c_str());
2460 gotSubscribeAck = true; 2502 continue;
2461 SetParticipantMiningStatus("subscribed; authorizing"); 2503 }
2462 } else if (id == 2 && msg["result"].is_boolean()) { 2504
2463 gotAuthorizeAck = msg["result"].get<bool>(); 2505 // -- Response to our subscribe (id=1) --
2464 authWaitStart = GetTime(); 2506 if (msg.contains("id") && msg["id"].is_number_integer()
2465 SetParticipantMiningStatus(gotAuthorizeAck ? "authorized; asking pool for work" : "authorization rejected"); 2507 && msg["id"].get<int>() == 1
2508 && msg.contains("result") && !msg["result"].is_null()) {
2509 gotSubscribeAck = true;
2510 LogPrint("worker", "[pool->worker] subscribe ack received\n");
2511 SetParticipantMiningStatus("subscribed; waiting for authorize ack");
2512 }
2513
2514 // -- Response to our authorize (id=2) --
2515 else if (msg.contains("id") && msg["id"].is_number_integer()
2516 && msg["id"].get<int>() == 2
2517 && msg.contains("result")) {
2518 bool ok = msg["result"].is_boolean() && msg["result"].get<bool>();
2519 authWaitStart = GetTime();
2520 if (ok) {
2521 gotAuthorizeAck = true;
2522 LogPrint("worker", "[pool->worker] authorize OK -- waiting for job\n");
2523 SetParticipantMiningStatus("authorized; waiting for job");
2524 } else {
2525 std::string reason = msg.value("error", json("unspecified")).is_string()
2526 ? msg["error"].get<std::string>() : "unspecified";
2527 LogPrint("worker", "[pool->worker] authorize REJECTED: %s\n",
2528 reason.c_str());
2529 SetParticipantMiningStatus("authorization rejected");
2530 // Don't break -- pool might still send a job (some pools
2531 // reject bad addresses but still send work)
2466 } 2532 }
2467 if (pendingSubmitIds.erase(id) > 0) { 2533 }
2468 if (msg["result"].is_boolean() && msg["result"].get<bool>()) { 2534
2535 // -- Share submission response --
2536 else if (msg.contains("id") && msg["id"].is_number_integer()) {
2537 int rid = msg["id"].get<int>();
2538 if (pendingSubmitIds.erase(rid) > 0) {
2539 bool accepted = msg.contains("result")
2540 && msg["result"].is_boolean()
2541 && msg["result"].get<bool>();
2542 if (accepted) {
2469 gParticipantSharesAccepted.fetch_add(1); 2543 gParticipantSharesAccepted.fetch_add(1);
2544 sessionSharesAccepted++;
2545 LogPrint("worker", "[pool->worker] share id=%d ACCEPTED\n", rid);
2470 SetParticipantMiningStatus("share accepted"); 2546 SetParticipantMiningStatus("share accepted");
2471 } else { 2547 } else {
2472 SetParticipantMiningStatus("share rejected"); 2548 std::string reason = (msg.contains("error")
2549 && msg["error"].is_string())
2550 ? msg["error"].get<std::string>() : "unspecified";
2551 LogPrint("worker", "[pool->worker] share id=%d REJECTED: %s\n",
2552 rid, reason.c_str());
2553 SetParticipantMiningStatus("share rejected: " + reason);
2473 } 2554 }
2474 } 2555 }
2475 } 2556 }
2476 std::string method = msg.value("method",""); 2557
2477 if (method == "mining.notify") { 2558 // -- Server notification --
2478 auto& p = msg["params"]; 2559 else {
2479 jobId = p[0].get<std::string>(); 2560 std::string method = msg.value("method", "");
2480 // p[1] = full 80-byte header hex 2561
2481 auto hdrBytes = FromHexStr(p[1].get<std::string>()); 2562 if (method == "mining.notify") {
2482 if (hdrBytes.size() >= 80) 2563 auto& p = msg["params"];
2483 memcpy(header, hdrBytes.data(), 80); 2564 if (!p.is_array() || p.size() < 4) {
2484 // p[3] = share target hex (little-endian uint256) 2565 LogPrint("worker", "[pool->worker] malformed mining.notify"
2485 auto tgtBytes = FromHexStr(p[3].get<std::string>()); 2566 " (params size=%zu)\n", p.is_array() ? p.size() : 0);
2486 if (tgtBytes.size() >= 32) 2567 } else {
2487 memcpy(&target, tgtBytes.data(), 32); 2568 std::string newJobId = p[0].get<std::string>();
2488 haveJob = true; 2569 auto hdrBytes = FromHexStr(p[1].get<std::string>());
2489 nNonce = 1; 2570 auto tgtBytes = FromHexStr(p[3].get<std::string>());
2490 SetParticipantMiningStatus("job received; hashing"); 2571
2491 printf("PoolParticipantMiner: new job %s\n", jobId.c_str()); 2572 if (hdrBytes.size() < 80) {
2492 } else if (method == "mining.set_difficulty") { 2573 LogPrint("worker", "[pool->worker] mining.notify:"
2493 // acknowledged, we use the target from notify 2574 " header too short (%zu bytes)\n",
2575 hdrBytes.size());
2576 } else if (tgtBytes.size() < 32) {
2577 LogPrint("worker", "[pool->worker] mining.notify:"
2578 " target too short (%zu bytes)\n",
2579 tgtBytes.size());
2580 } else {
2581 jobId = newJobId;
2582 memcpy(header, hdrBytes.data(), 80);
2583 memcpy(&target, tgtBytes.data(), 32);
2584 haveJob = true;
2585 nNonce = 1;
2586 LogPrint("worker", "[pool->worker] new job %s received\n",
2587 jobId.c_str());
2588 SetParticipantMiningStatus("hashing job " + jobId);
2589 }
2590 }
2591 } else if (method == "mining.set_difficulty") {
2592 // Pool adjusts our share difficulty via vardiff.
2593 // Apply it immediately so we start hashing at the new target.
2594 if (msg.contains("params") && msg["params"].is_array()
2595 && msg["params"].size() > 0
2596 && msg["params"][0].is_number()) {
2597 double newDiff = msg["params"][0].get<double>();
2598 if (newDiff > 0.0) {
2599 bool changed = (newDiff != currentDifficulty);
2600 currentDifficulty = newDiff;
2601 LogPrint("worker", "[pool->worker] set_difficulty %.6f%s\n",
2602 newDiff, changed ? " (updated)" : " (initial)");
2603 }
2604 } else {
2605 LogPrint("worker", "[pool->worker] set_difficulty"
2606 " received (no valid params)\n");
2607 }
2608 } else if (!method.empty()) {
2609 LogPrint("worker", "[pool->worker] unknown server method: %s\n",
2610 method.c_str());
2611 }
2494 } 2612 }
2495 } catch(...) {} 2613 }
2614
2615 if (disc) {
2616 LogPrint("worker", "[worker] pool closed the connection"
2617 " (gotSubscribeAck=%d gotAuthorizeAck=%d haveJob=%d)\n",
2618 gotSubscribeAck, gotAuthorizeAck, haveJob);
2619 break;
2620 }
2496 } 2621 }
2497 } 2622 }
2498 2623
2499 if (gotAuthorizeAck && !haveJob && authWaitStart > 0 && GetTime() - authWaitStart > 10) { 2624 // -- Stuck watchdog: authorized but no job arriving --
2500 SetParticipantMiningStatus("authorized; waiting for pool notify"); 2625 if (gotAuthorizeAck && !haveJob && authWaitStart > 0) {
2501 authWaitStart = 0; 2626 int64 waited = GetTime() - authWaitStart;
2627 if (waited > 10 && GetTime() - lastStuckLog >= 15) {
2628 LogPrint("worker", "[worker] authorized but no job after %llds"
2629 " -- pool tunnel is live, waiting on mining.notify\n",
2630 (long long)waited);
2631 lastStuckLog = GetTime();
2632 }
2633 if (waited > STUCK_NO_JOB_TIMEOUT_SECS) {
2634 LogPrint("worker", "[worker] no job received after %llds -- disconnecting"
2635 " and reconnecting\n", (long long)STUCK_NO_JOB_TIMEOUT_SECS);
2636 SetParticipantMiningStatus("timeout waiting for job; reconnecting");
2637 break;
2638 }
2502 } 2639 }
2503 2640
2504 if (!haveJob) { Sleep(100); continue; } 2641 if (!haveJob) { Sleep(100); continue; }
2505 2642
2506 // Hash one nonce 2643 // -- Hash one nonce --
2507 memcpy(header+76, &nNonce, 4); 2644 memcpy(header+76, &nNonce, 4);
2508 uint256 hash = RandomXHashWithVM(rxvm, header, 80); 2645 uint256 hash = RandomXHashWithVM(rxvm, header, 80);
2509 hashesSinceSample++; 2646 hashesSinceSample++;
@@ -2512,38 +2649,59 @@ static bool PoolParticipantMiner()
2512 UpdateParticipantHashRate(hashesSinceSample, sampleStart); 2649 UpdateParticipantHashRate(hashesSinceSample, sampleStart);
2513 2650
2514 if (hash <= target) { 2651 if (hash <= target) {
2515 printf("PoolParticipantMiner: share found! nonce=%u\n", nNonce); 2652 LogPrint("worker", "[worker] share found! nonce=%u job=%s difficulty=%.4f\n",
2653 nNonce, jobId.c_str(), currentDifficulty);
2516 SetParticipantMiningStatus("share found; submitting"); 2654 SetParticipantMiningStatus("share found; submitting");
2517 int submitId = msgId++; 2655 int submitId = msgId++;
2518 pendingSubmitIds.insert(submitId); 2656 pendingSubmitIds.insert(submitId);
2519 gParticipantSharesSent.fetch_add(1); 2657 gParticipantSharesSent.fetch_add(1);
2658 sessionSharesSent++;
2520 json submit = {{"id",submitId},{"method","mining.submit"}, 2659 json submit = {{"id",submitId},{"method","mining.submit"},
2521 {"params",json::array({username, jobId, 2660 {"params",json::array({username, jobId, ToHexStr(&nNonce,4)})}};
2522 ToHexStr(&nNonce,4)})}}; 2661 if (!StratumSendLine(s, submit)) {
2523 if (!StratumSendLine(s, submit)) break; 2662 LogPrint("worker", "[worker] share submit send FAILED"
2663 " -- pool connection lost\n");
2664 break;
2665 }
2524 } 2666 }
2525 2667
2526 nNonce++; 2668 nNonce++;
2527 if (nNonce == 0) { haveJob = false; } // exhausted, wait for new job 2669 if (nNonce == 0) {
2670 LogPrint("worker", "[worker] nonce space exhausted for job %s"
2671 " -- waiting for new job\n", jobId.c_str());
2672 haveJob = false;
2673 }
2528 } 2674 }
2529 2675
2676 // ------------------------------------------------------------------
2677 // Cleanup
2678 // ------------------------------------------------------------------
2530 RandomXDestroyMinerVM(rxvm); 2679 RandomXDestroyMinerVM(rxvm);
2531 closesocket(s); 2680 closesocket(s);
2532 UpdateParticipantHashRate(hashesSinceSample, sampleStart); 2681 UpdateParticipantHashRate(hashesSinceSample, sampleStart);
2533 SetParticipantMiningStatus("participant miner stopped"); 2682 SetParticipantMiningStatus("disconnected");
2534 printf("PoolParticipantMiner: disconnected\n"); 2683 LogPrint("worker", "[worker] disconnected from pool"
2684 " (hashes this session: %llu, shares sent: %llu, accepted: %llu)\n",
2685 (unsigned long long)hashesSinceSample,
2686 (unsigned long long)sessionSharesSent,
2687 (unsigned long long)sessionSharesAccepted);
2535 return true; 2688 return true;
2536} 2689}
2537 2690
2538bool BitcoinMiner() 2691bool BitcoinMiner()
2539{ 2692{
2693 // Relay mode: never mines, just relays/syncs. (Shouldn't normally get here
2694 // since the GUI hides Start Mining for Relay, but guard it regardless.)
2695 if (nMineMode == MINE_RELAY)
2696 return true;
2697
2540 // Participant mode: connect to external pool instead of mining solo/operator 2698 // Participant mode: connect to external pool instead of mining solo/operator
2541 if (nMineMode == MINE_PARTICIPANT) 2699 if (nMineMode == MINE_PARTICIPANT)
2542 { 2700 {
2543 while (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { 2701 while (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) {
2544 PoolParticipantMiner(); 2702 PoolParticipantMiner();
2545 if (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { 2703 if (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) {
2546 printf("PoolParticipantMiner: reconnecting in 10s\n"); 2704 LogPrint("worker", "PoolParticipantMiner: reconnecting in 10s\n");
2547 for (int i = 0; i < 10 && !fShutdown; i++) Sleep(1000); 2705 for (int i = 0; i < 10 && !fShutdown; i++) Sleep(1000);
2548 } 2706 }
2549 } 2707 }
@@ -2554,9 +2712,16 @@ bool BitcoinMiner()
2554 if (nMineMode == MINE_OPERATOR) 2712 if (nMineMode == MINE_OPERATOR)
2555 return true; 2713 return true;
2556 2714
2557 printf("BitcoinMiner started\n"); 2715 LogPrint("net", "BitcoinMiner started\n");
2558 SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_LOWEST); 2716 SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_LOWEST);
2559 2717
2718 // Build the fast (~2 GB dataset) RandomX mode once, up front, so we're
2719 // not stuck mining in the much slower light/cache-only mode forever.
2720 if (!RandomXFastReady()) {
2721 unsigned int nThreads = std::thread::hardware_concurrency();
2722 RandomXInitDataset(nThreads > 0 ? (int)nThreads : 1);
2723 }
2724
2560 CKey key; 2725 CKey key;
2561 key.MakeNewKey(); 2726 key.MakeNewKey();
2562 CBigNum bnExtraNonce = 0; 2727 CBigNum bnExtraNonce = 0;
@@ -2639,7 +2804,7 @@ bool BitcoinMiner()
2639 } 2804 }
2640 pblock->nBits = nBits; 2805 pblock->nBits = nBits;
2641 pblock->vtx[0].vout[0].nValue = pblock->GetBlockValue(nFees); 2806 pblock->vtx[0].vout[0].nValue = pblock->GetBlockValue(nFees);
2642 printf("\n\nRunning BitcoinMiner with %d transactions in block\n", pblock->vtx.size()); 2807 if (LogAcceptsCategory("net")) printf("\n\nRunning BitcoinMiner with %d transactions in block\n", pblock->vtx.size());
2643 2808
2644 2809
2645 // 2810 //
@@ -2658,11 +2823,11 @@ bool BitcoinMiner()
2658 void* rxvm = RandomXCreateMinerVM(); 2823 void* rxvm = RandomXCreateMinerVM();
2659 if (!rxvm) 2824 if (!rxvm)
2660 { 2825 {
2661 printf("BitcoinMiner: failed to create RandomX VM\n"); 2826 LogPrint("net", "BitcoinMiner: failed to create RandomX VM\n");
2662 Sleep(1000); 2827 Sleep(1000);
2663 continue; 2828 continue;
2664 } 2829 }
2665 printf("BitcoinMiner: mining with RandomX (%s)\n", 2830 LogPrint("net", "BitcoinMiner: mining with RandomX (%s)\n",
2666 RandomXFastReady() ? "fast 2GB" : "light 256MB"); 2831 RandomXFastReady() ? "fast 2GB" : "light 256MB");
2667 2832
2668 unsigned int nStart = GetTime(); 2833 unsigned int nStart = GetTime();
@@ -2675,9 +2840,9 @@ bool BitcoinMiner()
2675 if (hash <= hashTarget) 2840 if (hash <= hashTarget)
2676 { 2841 {
2677 //// debug print 2842 //// debug print
2678 printf("BitcoinMiner:\n"); 2843 if (LogAcceptsCategory("net")) printf("BitcoinMiner:\n");
2679 printf("RandomX proof-of-work found \n powhash: %s \ntarget: %s\n", hash.GetHex().c_str(), hashTarget.GetHex().c_str()); 2844 if (LogAcceptsCategory("net")) printf("RandomX proof-of-work found \n powhash: %s \ntarget: %s\n", hash.GetHex().c_str(), hashTarget.GetHex().c_str());
2680 pblock->print(); 2845 if (LogAcceptsCategory("net")) pblock->print();
2681 2846
2682 SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_NORMAL); 2847 SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_NORMAL);
2683 CRITICAL_BLOCK(cs_main) 2848 CRITICAL_BLOCK(cs_main)
@@ -2692,7 +2857,7 @@ bool BitcoinMiner()
2692 2857
2693 // Process this block the same as if we had received it from another node 2858 // Process this block the same as if we had received it from another node
2694 if (!ProcessBlock(NULL, pblock.release())) 2859 if (!ProcessBlock(NULL, pblock.release()))
2695 printf("ERROR in BitcoinMiner, ProcessBlock, block not accepted\n"); 2860 LogPrint("net", "ERROR in BitcoinMiner, ProcessBlock, block not accepted\n");
2696 } 2861 }
2697 SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_LOWEST); 2862 SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_LOWEST);
2698 2863
@@ -2860,11 +3025,11 @@ bool SelectCoins(int64 nTargetValue, set<CWalletTx*>& setCoinsRet)
2860 setCoinsRet.insert(vValue[i].second); 3025 setCoinsRet.insert(vValue[i].second);
2861 3026
2862 //// debug print 3027 //// debug print
2863 printf("SelectCoins() best subset: "); 3028 if (LogAcceptsCategory("net")) printf("SelectCoins() best subset: ");
2864 for (int i = 0; i < vValue.size(); i++) 3029 for (int i = 0; i < vValue.size(); i++)
2865 if (vfBest[i]) 3030 if (vfBest[i])
2866 printf("%s ", FormatMoney(vValue[i].first).c_str()); 3031 if (LogAcceptsCategory("net")) printf("%s ", FormatMoney(vValue[i].first).c_str());
2867 printf("total %s\n", FormatMoney(nBest).c_str()); 3032 if (LogAcceptsCategory("net")) printf("total %s\n", FormatMoney(nBest).c_str());
2868 } 3033 }
2869 3034
2870 return true; 3035 return true;
@@ -2953,6 +3118,34 @@ bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew, in
2953 return true; 3118 return true;
2954} 3119}
2955 3120
3121// Undo what CommitTransactionSpent did: unmark the inputs it marked spent
3122// and erase the half-committed wallet entry. Used when a transaction is
3123// accepted into the wallet but then rejected by the mempool/chain, so we
3124// don't end up with a wallet that thinks money is spent that never left.
3125void RollbackTransactionSpent(const CWalletTx& wtxNew)
3126{
3127 CRITICAL_BLOCK(cs_main)
3128 CRITICAL_BLOCK(cs_mapWallet)
3129 {
3130 set<CWalletTx*> setCoins;
3131 foreach(const CTxIn& txin, wtxNew.vin)
3132 {
3133 map<uint256, CWalletTx>::iterator mi = mapWallet.find(txin.prevout.hash);
3134 if (mi != mapWallet.end())
3135 setCoins.insert(&mi->second);
3136 }
3137 foreach(CWalletTx* pcoin, setCoins)
3138 {
3139 pcoin->fSpent = false;
3140 pcoin->WriteToDisk();
3141 vWalletUpdated.push_back(make_pair(pcoin->GetHash(), false));
3142 }
3143
3144 EraseFromWallet(wtxNew.GetHash());
3145 }
3146 MainFrameRepaint();
3147}
3148
2956// Call after CreateTransaction unless you want to abort 3149// Call after CreateTransaction unless you want to abort
2957bool CommitTransactionSpent(const CWalletTx& wtxNew) 3150bool CommitTransactionSpent(const CWalletTx& wtxNew)
2958{ 3151{
@@ -3003,13 +3196,17 @@ bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew)
3003 return error("SendMoney() : Error finalizing transaction"); 3196 return error("SendMoney() : Error finalizing transaction");
3004 } 3197 }
3005 3198
3006 printf("SendMoney: %s\n", wtxNew.GetHash().ToString().substr(0,6).c_str()); 3199 if (LogAcceptsCategory("net")) printf("SendMoney: %s\n", wtxNew.GetHash().ToString().substr(0,6).c_str());
3007 3200
3008 // Broadcast 3201 // Broadcast
3009 if (!wtxNew.AcceptTransaction()) 3202 if (!wtxNew.AcceptTransaction())
3010 { 3203 {
3011 // This must not fail. The transaction has already been signed and recorded. 3204 // The transaction was already signed and committed to the wallet
3012 throw runtime_error("SendMoney() : wtxNew.AcceptTransaction() failed\n"); 3205 // above, but the chain/mempool just rejected it (e.g. a raced
3206 // input already spent by another transaction). Unwind the
3207 // wallet state we just wrote instead of crashing the app.
3208 RollbackTransactionSpent(wtxNew);
3209 return error("SendMoney() : wtxNew.AcceptTransaction() failed");
3013 } 3210 }
3014 wtxNew.RelayWalletTransaction(); 3211 wtxNew.RelayWalletTransaction();
3015 } 3212 }