upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/main.cpp
AgeCommit message (Collapse)Author
2026-08-23net: stop serving the 2009 marketplace to anonymous peersbitflash-maintainer
Four inherited handlers took attacker-controlled input and wrote to disk with nothing authorising it. None of them had a caller left anywhere in the tree, so all four were reachable only from the outside. checkorder called GenerateNewKey() -- AddKey(), a wallet.dat write -- for any peer that asked, and handed back the pubkey. Its comment claims the cache keeps one key per ip, and on a 2009 network of routable addresses it did. Not here: inbound peers arrive through a rendezvous tunnel and are tagged by BtfMarkerAddr() over three random bytes, so addr.ip is fresh every connection and the cache never hits. Reconnect in a loop and the node mints a key per connection, none of them in any backup taken beforehand, which for a pre-BIP32 wallet is how coins go missing. The map holding them was static and never trimmed. submitorder AddToWallet()'d a CWalletTx from any peer that sent one. It had to be a valid transaction; it did not have to be ours. review checked a signature the sender made with its own key, which proves possession and authorises nothing, then appended to reviews.dat by reading the whole vector, pushing one entry and writing it all back -- quadratic disk on an unbounded map<string,string> off the socket -- then walked a link graph the same peer supplied and relayed the message to every other node. reply dispatched into CRequestTracker. Nothing ever called PushRequest, so the table was permanently empty and the handler was a parser and nothing else. Removed with the subsystem behind them: market.cpp, CProduct, CReview, CUser, CReviewDB, the advert publish templates and the subscribe channel machinery, none of which had an inbound handler either. MSG_REVIEW, MSG_PRODUCT and MSG_TABLE stay in the inv enum, unserved: CInv puts type on the wire as an integer and MSG_TX and MSG_BLOCK are shared with every other node. Unknown commands are already ignored for extensibility, so peers still running these lose nothing but an answer they never asked for. Verified by a clean build of bitflash-node. (cherry picked from commit ad9c393866c007a115f32d70a9017ce91066a7d1)
2026-08-23size thread pools by the cores we are allowed, not the ones the host hasbitflash-maintainer
std::thread::hardware_concurrency() reports the MACHINE's core count. Inside a container that is the wrong number: with --cpuset-cpus 0-5 on a 12-core host it still answers 12, and every caller that sized a pool from it oversubscribed 2x. Measured on the pool box: - the pool node logged "Mining with 11 thread(s) of 12 core(s)" while confined to 6 CPUs - a burst miner launched with /hashthreads=2 filled the ~2 GB RandomX dataset with 12 threads, alongside the pool node in the same 8 GB container AvailableCoreCount() asks sched_getaffinity what the cpuset actually permits, which is the number all of these meant. It cannot make the dataset build slower: the CPU budget is identical either way, so fewer threads on the same cores is the same work without the thrash. Falls back to hardware_concurrency where the call is unavailable, and never returns 0 because callers subtract from it. Not the cause of the burst miner dying -- that was a corrupt Berkeley DB environment in the worker datadir -- but it is why the failure landed on the dataset build, and it is wrong on its own terms. (cherry picked from commit b3bee16f3eef8982bff1174bd9d0a9c21473732c)
2026-08-23net: do not consume a header until the body is there, and cap what one can claimbitflash-maintainer
Upstream #109's message-stall rework, hand-merged so it keeps the progress timing this tree already had. The old path consumed the header, found the payload short, inserted the header back at the front of vRecv, then Sleep(100) inside the shared message thread. A peer that announces a large payload and drips bytes made every pass pay an O(n) insert and a 100 ms stall -- one peer slowing the thread that serves all of them. Now the header is read from a copy and only skipped once the whole body has arrived, so there is nothing to put back and nothing to sleep for. Kept OURS, not upstream's, on the timeout: upstream restarts its clock only when the (size, command) pair changes, which drops a peer sending one big message slowly but honestly. This times the STALL -- the clock restarts on any progress -- which is the distinction that matters on a slow link. Also from #109: a header could announce 256 MB while serialize.h refuses anything over MAX_SIZE (32 MB), so the check admitted an allocation the parser would reject anyway and burned a peer slot for two minutes doing it. MAX_PROTOCOL_MESSAGE_SIZE ties the two together. 8x less a peer can make us hold, and this node takes public Stratum. And #103's other half: ~CNode closed its socket with a raw closesocket(), invisible to the accounting, and left hSocket set afterwards -- which is how a descriptor number that now belongs to somebody else gets closed a second time. (cherry picked from commit 2f25e2e07985e9bc732cadfbb3c9ac233895a814)
2026-08-23consensus: cap block size at 1 MB, and stop assembling blocks we would rejectbitflash-maintainer
CheckBlock() validated block size against MAX_SIZE, the serializer's generic 32 MB guard, not a block rule. MAX_BLOCK_SIZE did not exist in this tree at all: Bitcoin's 1 MB cap arrived in 0.3.x, after the 0.1.0 this fork came from. A miner could produce a 32 MB block and every node would download, verify and store it. Upstream #111 (from #110 by mtg0001). The cap is a tightening, so a node that does not upgrade is the permissive one. Measured upstream over 5709 blocks: largest 34,290 bytes, median 216, so 1 MB rejects nothing that exists. Upstream shipped the validation half only. Both places that ASSEMBLE a block still filled to MAX_SIZE/2 (16 MB), which was consistent against a 32 MB validation cap and is inverted by a 1 MB one -- the node would solve a block and then reject it itself. Fixed both: main.cpp CreateNewBlock, the node's own miner rpc.cpp RebuildJob, the POOL's job builder -- the one that matters here, because every miner on the pool would be working a template the network rejects, and this pool is ~90% of hashrate Half the cap in both, preserving the original margin: each test runs against a transaction that is already counted, so the loop can overshoot by one. (cherry picked from commit a3be9f04903447fa2741aff4eb529fe45f916877)
2026-08-23pool: refuse jobs when no peer has reported a height for 20 minutesbitflash-maintainer
The peer-height gate replaced a tip-age check that was wrong in both directions. It is a strict improvement and it is still blind in the one case that matters most. When the node goes deaf -- peers still in vNodes, nothing arriving -- no peer announces anything, GetPeerMedianHeight() returns -1, and the fallback mines our best chain. That fallback is right for a node that has not been told yet and wrong for a node that has stopped being able to hear, and a height nobody sent cannot tell those apart. A wedged node is exactly a node with no peer height information, so the gate was silent for the whole of heights 3966-4085: 120 blocks, 5890.50 BTF voided. Time can tell them apart. GetPeerHeightAgeSecs() measures how long since any peer reported a height, counting from first use rather than the epoch so a starting node does not refuse jobs before its first handshake. Past 20 minutes the pool stops issuing jobs and says so unconditionally in the log. /poolmineblind overrides it: without an escape hatch a genuinely isolated pool that is 90% of the hashrate could never restart its own chain, which is the deadlock the tip-age check used to cause. Also stop nStartingHeight from going stale. It was a snapshot taken once during the version handshake and never touched again, so on a long-lived connection the median is built from heights the peers had hours ago, and the behind-the-network check drifts toward 'everyone is behind us' and quietly stops firing. A peer that hands us a block is demonstrably at least at that block's height. (cherry picked from commit 0800c18faf703b4acde1b131d0b742d37a86d92b)
2026-08-23payout: say what actually failed instead of blaming transaction sizebitflash-maintainer
SendMoney's "oversized transaction that requires a transaction fee of 0.00" is not an oversized transaction and there is no fee. That branch is taken when nValue + nFeeRequired > GetBalance() -- an empty spendable balance -- and the fee reads 0.00 because CreateTransaction failed before setting one. 3,267 of these in debug.log sent me looking for a transaction-size problem that does not exist. Report what is short, by how much, and against what balance. Name COINBASE_SPENDABLE_DEPTH in main.h and derive GetBlocksToMaturity from it, because the distinction it captures is genuinely surprising: CMerkleTx::GetCredit values a mined coinbase at zero until COINBASE_MATURITY+20 deep, not COINBASE_MATURITY. I first changed the payout gate to that deeper value, on the theory that each payout was firing 20 blocks before its own coinbase could be spent. Measuring it refuted that: across 934 payments, 96% succeeded at depth 100, the moment they were queued, which is only possible because a payout is funded from whatever coin is already mature rather than from its own block. The gate stays at COINBASE_MATURITY, with a comment recording the measurement so the next person does not re-make the same change. The real shortfall is structural and not addressed here: the pool pays out ~99% of each block's subsidy while only matured coinbases are spendable, so it runs with almost no buffer, and any orphaned block it queued a payout for drains that buffer permanently. The 24h retry loop absorbs this -- 0 payouts forfeited -- but the buffer is what needs fixing. (cherry picked from commit e28df01f90d621b0cd818f28d70a20b8ab5d1a8d)
2026-08-23participant: honor explicit /hashthreads=1 (floor of 2 only in auto mode)bitflash-maintainer
The min-2-threads floor applied even when the user explicitly asked for 1 thread, so /hashthreads=1 silently spawned 2. (cherry picked from commit a67b9e84f4f7607d307081b4a9873f5d9e78cf8a)
2026-08-23operator: /noannounce flag -- serve miners without publishing pool announcementsbitflash-maintainer
Descriptor (38501) still published so miners can resolve the pool; only the public pool listing event (38503) is suppressed. (cherry picked from commit 17bbdae1ec97945c37c1d5b2486e5c01a4d7ad87)
2026-08-23pool: sync-gate job creation, retry failed payouts; widen nostr relay setbitflash-maintainer
- RebuildJob: no jobs with zero peers or a tip older than 30min; mining unsynced produced orphaned blocks and payouts that can never be paid (observed in production: 8 orphan 'found blocks') - payout exec: on SendMoney failure requeue with 60s backoff, up to 30 attempts, instead of silently forfeiting the payment - nostr: add oxtr.dev, nos.social, fmt.wiz.biz, current.fyi, nostr.bg (12 relays total) for announcement redundancy (cherry picked from commit f1986cd7e4a6f69469a2256eb96ff03f3f146792)
2026-08-04wallet encryption: the stack from #130–#133, with the two blockers fixed ↵Bitflash
(#134) * wallet: refuse future encrypted wallet formats * wallet: add encrypted key unlock core * wallet: add encryptwallet rewrite command * wallet: read passphrases from stdin or files * wallet: keep the derivation across encryption, and take the logs with it Two things stopped the encryption stack from being safe to merge. Both were found against a wallet holding real coin, not in the self-tests. 1. Encrypting turned a BIP44 wallet into a legacy one. LoadWallet() ends by resetting the schema and the derivation counters when no seed is in memory. That was written when "no seed after loading" could only mean the wallet had none. An encrypted wallet has a seed -- it just has not been decrypted yet -- so the reset threw away metadata that had been read correctly a moment earlier. Unlocking later restores the seed and nothing restores the rest. Measured on the wallet from the #124 proof: after encrypting, index 1 moved off BAtgSgra2X8Dqo5PJzPCLJhPwrrC2thYVJ, the address actually holding 1.00 BTF, and the audit went from "1.00 covered by the phrase" to "0.00 covered, 1.00 wallet.dat-only". Encrypt your wallet and the program told you your recovery phrase no longer covered your money. Now the condition also accepts a wallet whose seed is present but encrypted. Same wallet, before and after: schema bip44, same four derived addresses, 1.00 BTF still reported as covered. 2. The plaintext keys stayed behind in the Berkeley DB logs. wallet.dat comes out clean -- verified by scanning the raw file for the 118 bytes of a known private key. database/log.0000000002 did not: it held the whole key, and still did after a full node restart, because every record this wallet ever wrote passed through the log. That defeats the feature for the case it exists for, since our own backup advice is to copy the whole directory rather than wallet.dat alone. log_archive() cannot fix it: it only drops logs Berkeley DB considers obsolete and never the one it is writing to, and with one 10 MB log per directory that is precisely where the keys are. They also did not get there at encryption time -- they accumulated over the wallet's whole life -- so nothing done during the rewrite can archive them out. So the encrypt command now closes the environment and deletes the logs on its way out, after the flush that makes every .dat self-contained. This is not secure erasure; the blocks are unlinked, not overwritten. It closes the case of someone reading a copied data directory. Getting there uncovered a third bug, older than both. DBFlush(true) is not safe to call twice. The first call closes the environment, and txn_checkpoint() on a closed environment segfaults instead of returning an error, so the try/catch around it never sees anything. EncryptWallet() already flushes before renaming the wallet, and the command flushes again on the way out -- that pair crashed every single time. Guarded now. The same function has been calling log_archive() with the address of an uninitialised pointer since 0.1.0, on every clean shutdown, in a path whose catch(...) cannot catch a structured fault on Windows. DB_ARCH_REMOVE requires that argument to be NULL. Fixed while here, because the encryption cleanup depends on that call not being a coin flip. --------- Co-authored-by: Gilcimar Duarte <ti@gpm.infra>
2026-08-03Activate BIP44 wallet derivation (#124)mtg0001
* Add BIP32 path derivation foundation * Add HD wallet schema metadata * Add provisional BIP44 coin type metadata * Activate BIP44 wallet derivation * wallet: scan BIP44 restore depth per branch * wallet: widen default BIP44 restore lookahead --------- Co-authored-by: Gilcimar Duarte <ti@gpm.infra>
2026-08-03Add wallet recovery balance audit (#117)mtg0001
Co-authored-by: Gilcimar Duarte <ti@gpm.infra>
2026-08-02Add consensus block size limit (#111, from #110)Bitflash
CheckBlock() validated block size against MAX_SIZE, the serializer's generic 32 MB guard rather than a block rule; MAX_BLOCK_SIZE did not exist in this tree at all, because Bitcoin's 1 MB cap arrived in 0.3.x, after the 0.1.0 this fork came from. A miner could produce a 32 MB block and every node would download, verify and store it. Original work by mtg0001 in #110, rebased over #109 with the self-test conflict resolved, plus a comment explaining why the inherited count-against-bytes test stays. Measured before choosing the number: 5709 blocks read from blk0001.dat give a largest block of 34,290 bytes and a median of 216, so 1 MB leaves 29x of headroom and rejects nothing that exists. A node built from this branch then synced genesis to 5706 on the live network with zero size-limit rejections and zero CheckBlock errors -- the same test that caught BIP30 rejecting a real block at height 859.
2026-08-02Keep HD change recoverable and bound message stalls (#109)mtg0001
Change from an HD wallet now goes to a newly derived phrase-backed key instead of reusing a spent input's key. Reusing an input key silently moved value back to a key the phrase cannot reproduce; if derivation fails the transaction is refused rather than creating unrecoverable change. Legacy wallets with no seed keep the 0.1.0 behaviour. Restore also scans past the pre-generated key-pool window before treating an empty batch as the end. The message parser no longer consumes a header it cannot complete: it reads from a copy, so the O(n) reinsert and the 100 ms sleep inside the shared message thread are gone, and a peer that sits on an incomplete payload is disconnected after two minutes. The wire cap drops from 256 MB to MAX_SIZE. New net-message self-test covers complete, incomplete, stale and oversized headers. Verified here: clean rebuild, all self-tests pass, lock order checked (no path takes cs_keyPool before cs_mapWallet), and a node built from this branch synced from genesis to the chain tip on the live network with zero rejections.
2026-08-01wallet: reserve index 0 before deriving it, and name the address the phrase ↵Bitflash
does not cover (#104) Follow-up to #102. Reserves derivation index 0 before deriving it, so a failure between installing the seed and writing the default key cannot leave the pool free to hand that index out as an ordinary key. Renames the address that stops being the default to 'Your Address (created before the recovery phrase)', which was otherwise left under the same name as the new one. Also documents why the key-pool clear precedes the seed write, correcting a review comment of mine that asked for the reverse.
2026-08-01Keep new phrase addresses recoverable (#102)mtg0001
-newphrase installed a seed but left the wallet's visible address as the pre-seed random key and the old random pool intact, so the next address handed out -- a mining payout included -- could be one the twelve words cannot reproduce, while the window said a recovery phrase existed. Installing a phrase now clears the stale pool records (private keys stay in wallet.dat), reserves a derived default receiving key, tolerates already-stored derived keys so repeated installs do not stall the pool, and -restorephrase reports what it recovered rather than the wallet's total transaction count. Self-test covers installing a phrase over a pre-existing random pool.
2026-08-01wallet: derive the key pool from a seed, so a phrase could bring it back (#93)Bitflash
Adds BIP32 derivation behind the wallet key pool, with no user-facing way to create a seed yet. wallet.dat gains hdmaster/hdchaincode/hdnext; the mnemonic is never stored. TopUpKeyPool derives m/n' when a seed exists and falls back to a random key, with a log line, if derivation fails. New -selftest=wallet-hd, wired into make tests on both platforms.
2026-08-01Add wallet keypool self-test (#91)mtg0001
Co-authored-by: Gilcimar Duarte <ti@gpm.infra>
2026-08-01wallet: walk the chain for coins a key owns but the wallet never saw (#88)Bitflash
A transaction only entered the wallet when its block arrived over the network, so a key added afterwards was invisible: coins on the chain, private key in wallet.dat, balance zero. /importwallet has always ended that way -- it writes the keys and the money never shows up, which looks exactly like a failed import. ScanForWalletTransactions walks the main chain and hands every transaction that IsMine() to AddToWalletIfMine, the same two primitives the live path uses. Wired into /importwallet, and offered as /rescan. It is also the half that any recovery-phrase feature has to stand on: deriving keys from twelve words is easy, and without this a restore restores nothing.
2026-08-01wallet: generate keys ahead of use so a backup outlives the next block (#85)Bitflash
The wallet made a key per address and per block mined, so a backup only ever contained the keys that existed the moment it was taken -- mine one block and the coin lands on a key the backup has never heard of. The README warned about it, which is an admission rather than a fix. Keys are now generated a hundred at a time and handed out one at a time, each written to wallet.dat before it can be pointed at. A backup covers the next hundred addresses. Deterministic derivation (#47) stays open; this buys back the distance between a backup and the next mistake.
2026-08-01mine: remember whether this node was mining, with the command line winning (#84)Bitflash
2026-08-01diag: let the node say what is wrong with it (#81)Bitflash
* Bump to 1.2.11 One change, and it is the reason a node on Windows kept collecting peers it could not hear. FD_SETSIZE was never defined in this tree, so it defaulted to 64 and select() could watch the listen socket plus 63 peers -- everything past that stayed connected and was never read again. Windows FD_SET stops adding once its array is full and says nothing, so there was no error to notice. Because new connections go on the end of vNodes, it was always the newest connection that starved, including the ones dialed to fix connectivity. Measured on two nodes: 277 and 90 CNode objects against 93 and 90 live sockets, 316 and 355 sockets in a bound-but-never-connected state, and a status bar reporting hundreds of peers for a node really talking to thirty. 1.2.11 raises FD_SETSIZE to 1024 on Windows, caps connections at 125, drops a node stuck undeliverable after 60 seconds instead of holding it forever, stops answering a failed select() with POSIX code that has no meaning on Windows, and warns when there are more peers than select can watch. * diag: let the node say what is wrong with it Every hard problem in this project so far was found from outside the node -- sockets read over WMI, committed memory compared against a README, a grep over somebody debug.log. In each case the node knew and had no way to say so. One report, rendered identically for the GUI panel and the log: peers held vs peers actually watched by select(), blocks received and how many arrived without a parent, proof-of-work mode with the live miner thread count, and per peer the age of the last message in each direction plus anything stuck unsent.
2026-07-31wallet: stop paying the recipient the transaction fee on a fee retry (#74)Bitflash
CreateTransaction kept the payee amount in nValue and then did int64 nValueOut = nValue; nValue += nFee; with both lines inside the retry loop. The first pass is fine. When the fee turns out to be too low the loop starts over, and on the second pass nValueOut is read from an nValue that already carries the fee -- so the recipient is sent the amount plus the fee, out of the sender's pocket, and it compounds on every further retry. Reachable, but it takes two things at once, which is why it has sat here: the user must have set a transaction fee in Options (with the default of zero, nValue += nFee adds nothing on the first pass and nothing is corrupted), and the transaction must be big enough for GetMinFee to demand more than that -- over 10 KB, so a wallet paying from many small inputs. Keep nValue meaning what the payee asked for and carry nValue + nFee in a separate nTotalValue, the way Bitcoin fixed it in 107d9e288. Verified by building the same transaction on both versions, with a synthetic wallet of 200 coins of 50, a configured fee of 0.05, and a request of 5,000 that needs 101 inputs and 28 KB -- enough for GetMinFee to demand 0.12 and force the retry: before payee gets 5,000.05 change 49.83 after payee gets 5,000.00 change 49.88 Two other fixes from the same era were considered and left out. Satoshi's CENT floor on the change output (496823249) exists to avoid a dust output that his fee rules then penalised; this fee schedule has no such rule, so adopting it would only donate sub-cent change to the miner. The 1-confirmation rule (a790fa46f) is a spending-policy change, not a bug fix, and deserves its own decision.
2026-07-31net: bound what a peer can make the node allocate (#73)mtg0001
Three independent bounds on what an untrusted peer can make this node allocate: - serialize.h: reject a compact size above MAX_SIZE before reading a string or a vector, instead of discovering it read by read. MAX_SIZE moves from main.h to serialize.h, where the checks live; headers_core.h includes serialize.h first, so every existing user still sees it. - main.cpp: cap an orphan transaction at 10,000 inputs. Orphans are stored and never validated, so mapOrphanTransactionsByPrev grew with one entry per input of every orphan a peer cared to send. - rpc.cpp: drop a Stratum line nested deeper than 20 before json::parse recurses into it. The scan tracks string state, so a worker name containing { or [ is no longer mistaken for nesting. Verified: builds clean on UCRT64; the depth scanner passes a table of legitimate Stratum lines and nesting attacks; a node built from this branch synced the chain from genesis to 4462 with no deserialization failure and no rejected block.
2026-07-31net: bound orphan blocks and stop asking a peer the same question twice (#70)Bitflash
Two defects that feed each other. Measured on a 32-core production node: 1287 of 4117 processed blocks arrived with a missing parent, 97 arrived that the node already had, and 490 transactions arrived whose inputs were unknown. mapOrphanBlocks had no ceiling at all. Orphan transactions have had one since 0.1.0 -- MAX_ORPHAN_TRANSACTIONS with LimitOrphanTx -- and blocks never did. An orphan block whose parent never turns up stays in memory for the life of the process, each one holding a whole block, and a peer can send an unlimited number of them just by inventing the previous-block hash. LimitOrphanBlocks mirrors the transaction one: random eviction, both maps kept in step, and the CBlock deleted because mapOrphanBlocks owns it. Every orphan block also fired a getblocks at the sender, and an inv for a block already held as an orphan fired another, with no filtering. So a node missing part of the chain asked the same peer the same question over and over, and the duplicate answers made more orphans, which asked again. CNode::PushGetBlocks remembers the last request and drops a repeat -- the same fix Bitcoin made in e2c2648c1. One trap found by testing rather than by reading: eviction picks at random and may pick the block just inserted, so GetOrphanRoot has to be read out of pblock before LimitOrphanBlocks runs, not after. The first version had that backwards and would have read freed memory. Verified with fabricated orphans, four of the seven sharing one hashPrevBlock so the multimap path is exercised: 7 in, limit(3) leaves 3, limit(0) leaves 0, both maps in step at every stage, no crash. Then a run against the live network to 4348 with no scaffolding left behind. The storm itself was not reproducible on demand -- the network is healthy today and produced one orphan block in seven minutes -- so there is no before/after on the metric that motivated this.
2026-07-31mine: do not submit a solution for a parent that is no longer the tip (#69)Bitflash
The mining loop tests pindexPrev != pindexBest every 256 hashes, but never again once a solution turns up. A thread that found one just after somebody else extended the chain submitted a block built on a stale parent -- a block born orphaned, at a height already taken. Every miner thread on a machine works from the same pindexPrev, so the moment one wins, all the others are hashing a doomed template until their next checkpoint. On a 32-core box that is thirty threads with a standing chance of producing a guaranteed orphan. Re-check the tip inside cs_main before submitting, and drop the solution if it moved. Verified on an isolated chain with three miner threads: 2790 blocks accepted, so nothing legitimate is being discarded, and 2 solutions dropped for a moved tip -- which is the race actually happening and being caught. Honest about the size of it: two in 2790 is not what fills a wallet with orphans. On the 32-core node in production, 1287 of 4117 processed blocks arrive with a missing parent, which is a different problem and the one worth chasing next.
2026-07-31wallet: believe the chain about what has already been spent (#62)Bitflash
fSpent lives in wallet.dat and is written when this node spends something. A wallet.dat restored from backup, or copied and used on another machine, carries whatever that flag was when the copy was taken -- so it can say "unspent" about coins the chain shows as gone. The balance reads high and nothing contradicts it until a send is attempted against coins that do not exist. #47 is about this shape of problem and #40 is someone living through it; Bitcoin fixed the same thing in 53d508072. On startup, after the block index is loaded and before anything reports a balance, walk the wallet and mark spent whatever the transaction index says is spent. One direction only. Marking spent when the chain says spent is safe; clearing the flag because the chain has not caught up would offer up coins already on their way out. fSpent is one flag per transaction rather than per output, which is how 0.1.0 has always worked -- CommitTransactionSpent already marks a whole previous transaction when it spends any part of it -- so this matches rather than changing the model. It reports every run, including the runs where it finds nothing, with a breakdown of why: RescanSpentFlags() : examined 17, already-spent 0, not-indexed 17, corrected 0 A check that speaks only when it finds something is indistinguishable from one that never ran, and that confusion has cost this project real time. The breakdown earned its place immediately: it is how the test below turned from a guess into an answer. Verified running against a wallet with 17 transactions and against an empty one, with no false corrections in either. The correcting branch itself is NOT exercised by a real case: on this chain no test wallet holds a transaction that is both in the transaction index and spent, and the counters show why -- all 17 were absent from the index, so the comparison never had anything to compare.
2026-07-31db: verify the tail of the chain on startup and back out of a bad tip (#61)Bitflash
A node that accepted blocks its current rules would reject -- because it was running an older build, or because the tip it landed on was never valid -- had no way to notice. It kept the branch, kept building on it, and kept mining coins the rest of the network throws away. The only cure was deleting blk*.dat by hand, which requires knowing something is wrong. A node here once mined a 323-block fork for five days that way. On load, walk back from the tip running CheckBlock on each block. If one fails, move the best chain back to its parent, disconnect what came after, and let the node re-download from peers. Satoshi added this after the block 74638 incident for the same reason: so it would not have needed anyone to delete files. Only the last 288 blocks are checked by default -- about ten hours at a two-minute target -- because every check costs a RandomX hash. /checkblocks=N changes it, 0 does the whole chain. The repair deliberately runs in LoadBlockIndex() in main.cpp rather than in CTxDB::LoadBlockIndex where the detection happens. CTxDB::LoadBlockIndex walks the index with a cursor it never closes, so it still holds read locks when it returns, and a write transaction opened alongside it waits on them forever. That is measured, not guessed: the first version came up, printed the diagnosis, and hung with nothing in any log. Closing that cursor is a separate fix worth making on its own. A failed rollback reports and starts anyway. Refusing to start would leave the operator with no node to read the message from. What this catches is what CheckBlock catches: proof of work, merkle root, duplicate transactions, sigops, timestamps. It does not re-verify signatures or spends, which would mean rebuilding the transaction index. Verified by forcing a block at tip-5 to fail: the node reported the bad block, moved back six, stayed up, and re-synced past its old tip. Without the forced failure it verifies 288 and says so.
2026-07-30net: announce our height in the handshake, and say when we are behind (#56)Bitflash
A node running this code cannot tell "in sync with everyone" from "not hearing anyone". The version message carries no height -- 0.1.0 never had the field and this fork inherited the gap -- so the only evidence of a problem is a number on screen that stopped moving, which looks the same as a network with nothing to report. It is the structural reason a broken node here has always been indistinguishable from an idle one. Append nBestHeight to the version message and keep what a peer announced in CNode::nStartingHeight. The field goes last, so a node that predates it stops reading after addr and treats the rest as trailing bytes, which cost it one log line. Reading it is guarded by vRecv.empty() and never required: peers that do not send it keep nStartingHeight at -1 and are left out of the median. Every release so far sends the short form. GetPeerMedianHeight() takes the median rather than the maximum so a single peer claiming an absurd height cannot move it. Then use it. After five minutes more than one block behind the peer median, the node says so in the log -- not behind a debug category, because an operator who already enabled -debug is not the one who needs telling -- and the GUI carries the same sentence in its status area. "8 blocks behind for 27 minutes" is something a user can act on; a frozen counter is not. Verified against two nodes on the live network: the one starting from an empty datadir read its peer at 3797, reported 2636 then 1274 then 433 blocks behind as it caught up, and went quiet on its own once level. The other logged "peer height = -1" for the older nodes it met on the real network, which is the compatibility path doing its job. Known limit: nStartingHeight is a snapshot from handshake time and goes stale on a long-lived connection, so this catches a node that is receiving something but not blocks, not one that has gone completely deaf. The inactivity timeout from #55 covers that case and refreshes these heights when it forces the reconnect.
2026-07-30mine: put the block height in the coinbase, so two can never be identical (#59)Bitflash
Nothing in a coinbase belonged to the block it was in. The transaction was a function of three things: the difficulty, a counter, and the payout key. nBits holds still for long stretches, and bnExtraNonce is a local that restarts at zero whenever the miner does, so the same combination comes round again and produces a byte-identical transaction with the same txid. Two blocks then name the same transaction. The index keeps whichever arrived last, and the outputs of the first stop being reachable, with nothing logged and nothing shown. It has already happened twice on the main chain -- coinbase c9e56f7131 at heights 841 and 859, e635f4441f at 845 and 860 -- and 100 BTF are gone because of it. #58 has the full account. Putting the height first makes two coinbases at different heights unequal by construction, whatever the counter and the key do afterwards. Nothing validates coinbase content today, so this changes only what this node produces: blocks stay acceptable to every node already on the network, with no coordination and no activation height. Requiring the height, and enforcing BIP30 on top of it, is the separate coordinated step that this one has to come before. Verified by mining with the patch: the coinbase reads 5104ffff0f1f0101 -- OP_1 for height 1, then nBits, then the counter -- and the scriptSig is 8 bytes against the 100-byte ceiling in CheckTransaction.
2026-07-30net: notice a peer that has gone quiet, and stay audible to peers that check ↵Bitflash
(#55) A connection whose path dies silently -- NAT drops an idle mapping, a relay restarts, a route changes -- never delivers FIN. Nothing becomes readable, no error is raised, and the node simply stops hearing from that peer while the socket looks perfectly healthy. #51 bounded this for the rendezvous registration; the connection that comes out of the rendezvous had the same hole, and so did every ordinary peer. Stamp nLastRecv and nLastSend on the socket path and act on them: - nothing at all within 60s of connecting, disconnect. In testing this fired twice per run against dead .btf descriptors handed out by the relay, which is where the ghosts have been coming from. - queued to send for 10 minutes with nothing going out, disconnect. - nothing received for 30 minutes, disconnect. A healthy peer relays a block inventory about every two minutes, so this is fifteen intervals of silence. Bitcoin used ninety minutes against ten-minute blocks. Disconnecting is the whole cure: reconnection already works, and a peer we cannot hear is worth what no peer is worth. A node with nothing to relay would now look dead to a peer applying the same rule, so send "ping" after ten minutes of having said nothing. It carries no payload and expects no answer -- arriving is the entire content, because the receive path has already stamped nLastRecv by the time it is parsed. Requiring an answer would have read every node predating this change as dead, since those ignore unknown commands rather than replying. Verified on the live network: synced 0 to 3790 with five peers and no spurious disconnects, then again with the interval cut to 20s, which put 54 pings on the wire with no peer dropping us and no exceptions.
2026-07-30mine: hash on every core instead of one (#52)Bitflash
Satoshi left a note above ThreadBitcoinMiner in 2009: //// todo: start one thread per processor, //// use getenv("NUMBER_OF_PROCESSORS") Nobody did, and it stopped being a nicety when the proof of work became RandomX. SHA-256d on a 2009 desktop lost little to a single thread. RandomX is built to be fed by every core at once, so one thread on a 32-core machine mines at a thirty-second of what the box can do -- and the only outward sign is a CPU that looks suspiciously idle. A user reported exactly that: 30 minutes of "Mining", no blocks, Bitflash sitting at 3.3% CPU while the 2 GB dataset was allocated and fast mode was active. Nothing was broken. It was hashing on one core. bitflash -gen # every core but one bitflash -gen -genproclimit=8 # exactly eight Automatic leaves one core to the rest of the node -- accepting blocks, talking to peers, drawing the window -- because a node that mines beautifully while falling behind the chain has won nothing. /genproclimit overrides it, and a count several times the core count is refused rather than quietly thrashing. The wrapper was already built for this: RandomXCreateMinerVM says "own VM, not shared", and the ~2 GB dataset is one allocation every thread reads. Only the 2 MB scratchpad multiplies, so sixteen threads cost about 32 MB more than one. The startup line says so, because "why is it using 2 GB" is the first question anyone asks. Two things had to be right for this to be safe: - Shutdown. Miners share slot 3 of vfThreadRunning and StopNode waits for every slot before DBFlush tears the database down. With several, the first one out announced that everyone had finished while the rest were still calling ProcessBlock, so they are counted now and only the last one clears the flag. The three CheckForShutdown(3) calls inside the miner became plain returns: _endthread() does not unwind on Windows, so the bookkeeping would have been skipped there. - Duplicated work. Each thread makes its own key, and the coinbase pays it, so every thread has a different merkle root and searches a different space. Block assembly was already under cs_main and cs_mapTransactions; AddKey and ProcessBlock take their own locks. Verified on both platforms: four distinct thread ids reach the hashing loop and all report back on shutdown, which exits 0 with DBFlush running. Auto picks cores-1 (1 on a 2-core box, correctly reported). Speedup itself needs a many-core machine to show and has not been measured here -- the claim in this message is about threads doing work, not a benchmark. Not touched: RandomX's memory. RANDOMX_DATASET_BASE_SIZE and SCRATCHPAD_L3 are part of the hash function, so a knob for "use more RAM" would either do nothing or fork the chain. Threads are the only real dial.
2026-07-30build: check format strings, which nothing ever did (#44)Bitflash
The segfault in issue #5 was a format string: `%I64d` is MSVC-only, glibc parses it as the GNU `I` flag with a width of 64 and an `int` conversion, the vararg run desynchronises, and the following `%s` dereferences an integer. It killed the node mid-sync at the first block carrying a real transaction, deterministically. GCC can catch this. It never got the chance, for two reasons that had to be fixed together: - `-w` sat in both makefiles and suppressed every diagnostic. It cannot be narrowed; it wins over any -W or -Werror= that follows, so it had to go. The four -Wno- entries replacing it are the categories this 2009-era tree actually produces (measured: 248 deprecated-declarations, 157 write-strings, 18 narrowing, 1 unused-result), silenced by name instead of wholesale. - Even with -Wformat on, nothing was checked. GCC only inspects format strings for functions it knows are printf-like, and every format string here goes through strprintf, error, my_snprintf, or OutputDebugStringF -- `printf` itself is #defined to the last of those. None carried a format attribute, so the entire tree was invisible to the check. They are annotated now. Annotating surfaced 77 warnings across 17 distinct sites, all fixed here. Most were size_t or int64 passed to %d, harmless on x86-64 but wrong. Two were not cosmetic: - CBlockIndex::ToString printed pprev/pnext with %08x, truncating both pointers to 32 bits, so the two fields that line exists to show were unreliable on every 64-bit build. - CInv::GetCommand threw "type=% unknown type", where the % ate the space as a flag and the u as its conversion. The message read "type= 3nknown type". PRId64 is now one spelling for every target. Its MSVCRT branch was what produced "I64d" in the first place, it is pointless against UCRT which takes the C99 forms, the rest of the tree already prints int64 with %lld on Windows, and its only user (i64tostr) is dead code. Collapsing it lets both builds share the gnu_printf archetype -- ms_printf would reject %zu, which this tree uses and UCRT supports, while waving through the %I64d that crashes. Verified on both platforms: clean build, and reintroducing the issue #5 specifier now fails compilation rather than shipping. main.h:410:50: error: unknown conversion type character 'I' in format main.h:410:70: error: format '%s' expects argument of type 'char*', but argument 3 has type 'long long int' That second line is the crash itself, caught at build time. Runtime checked on both: node syncs past the block from issue #5, and the repaired log lines read correctly -- before: CTxOut(nValue= 50.%08I64d, scriptPubKey=(null)) after: CTxOut(nValue=50.00000000, scriptPubKey=0xE1C7EF3E706002) Four '_WIN32_WINNT redefined' warnings remain on Windows. Silencing them means letting a system header pick the Windows API level instead of this tree, which is a separate decision from format checking.
2026-07-29net: exchange signed .btf descriptors between peers (#32)Bitflash
Discovery has depended entirely on the Nostr relays. Two connected nodes never told each other who else existed, so a relay outage left a running network unable to grow, and a node that found one stale peer had no way to learn there was anything better. The old `addr` gossip went out with the IRC seed. Peers now trade signed descriptors over a new "btfpeers" message: our own first, then peers that actually answered us. The receiver checks each Schnorr signature against the key the `.btf` address decodes to, so the sender is trusted for nothing -- forging an entry needs somebody else's secret key. That leaves flooding with self-generated keys, which the per-message caps and the "peers that answered" ordering are there to blunt. Unknown commands are ignored (main.cpp), so nodes without this just drop the message; nothing here is a protocol break. The cache from the peer-cache change now keeps each peer's descriptor verbatim alongside the parsed fields, because that signature is what makes a peer relayable. Entries without one -- resolved through Nostr, or written before this -- stay dialable, just not passed on. A dial never clears a descriptor already on file.
2026-07-29Remove peer exchange from main; it was merged unreviewed (#27)Bitflash
PR #25 was opened, reviewed and approved as a single-file change to net.cpp: the .btf peer cache, +169/-0. I then pushed peer exchange and a /port option onto that same branch while the PR was open, which silently widened it to six files. The merge carried all of it in. Nothing about the peer exchange had been validated. Its two-node test never completed -- node B could not dial node A at all -- so what landed on main is unproven network protocol code that no one agreed to take. This restores main to what #25 actually described. The peer cache stays, with its runtime numbers (36.3s to 2.1s to first peer). #26 stays. What comes out: - the "btfpeers" message, both directions - BtfPexCollect / BtfPexAccept and the per-message caps - CNode::nLastPexRecv - the `desc` field on the cached peer record - BtfLocalDescriptor / BtfSecpContext - /port Peer exchange returns as its own pull request once its test passes. The /port option is worth having on its own -- two nodes cannot share a machine without it, which is what blocked the test -- and should go up separately rather than ride along again. Release v1.2.2 is unaffected: it was built from ba2de82, before either merge.
2026-07-29Remember .btf peers that answered, and dial them first on start (#25)Bitflash
* net: remember .btf peers that answered, and dial them first on start Discovery depends entirely on the Nostr relays. A node that has been up for hours knows exactly which peers respond, and throws all of it away on exit; the next start reaches for the relays again and walks the same descriptor list, of which roughly 96% is dead. That is what makes a restart take minutes before the first tunnel opens -- measured today at about seven. Every peer that completes a tunnel is now written to btfpeers.json with its meeting node and encryption key, which is everything needed to redial it. On start a thread tries those before the relays have answered anything, stopping once eight are connected. Entries older than a week are dropped, and the file holds at most fifty. Cache only: nothing here changes how a first-ever run bootstraps, and a missing or corrupt file just falls through to relay discovery. * net: exchange signed .btf descriptors between peers Discovery has depended entirely on the Nostr relays. Two connected nodes never told each other who else existed, so a relay outage left a running network unable to grow, and a node that found one stale peer had no way to learn there was anything better. The old `addr` gossip went out with the IRC seed. Peers now trade signed descriptors over a new "btfpeers" message: our own first, then peers that actually answered us. The receiver checks each Schnorr signature against the key the `.btf` address decodes to, so the sender is trusted for nothing -- forging an entry needs somebody else's secret key. That leaves flooding with self-generated keys, which the per-message caps and the "peers that answered" ordering are there to blunt. Unknown commands are ignored (main.cpp), so nodes without this just drop the message; nothing here is a protocol break. The cache from the peer-cache change now keeps each peer's descriptor verbatim alongside the parsed fields, because that signature is what makes a peer relayable. Entries without one -- resolved through Nostr, or written before this -- stay dialable, just not passed on. A dial never clears a descriptor already on file. * main_gui: implement /port, which net.cpp already documented net.cpp has said nListenPort is "tunable via /port" since it was written, but nothing ever parsed the option. Two nodes could not share a machine, which is exactly what testing peer exchange needs. Sets addrLocalHost.port too, so we do not advertise a port we never bound.
2026-07-28main: cap signature operations per block (#20)Bitflash
MAX_SIZE bounds a block's bytes, not what it costs to check. Every OP_CHECKSIG is an elliptic-curve verification, and blocks may be 32 MB here, so one packed with signature operations takes every node minutes of CPU to reject -- while at this chain's difficulty it stays cheap to mine. There was no counter at all. CheckBlock now sums signature operations across the block and rejects past 20000, the figure Bitcoin settled on. OP_CHECKMULTISIG counts as 20 since it may verify that many. Consensus change. Real blocks here carry one or two operations each -- coinbase pay-to-pubkey and standard pay-to-pubkey-hash spends -- so the ceiling is four orders of magnitude above anything in the chain, but it still needs verifying against the full history before merge.
2026-07-28main: cap orphan transactions held in memory (#16)Bitflash
A transaction naming a parent the node does not have is never validated, just stored in mapOrphanTransactions -- and there was no ceiling. Sending them costs a peer nothing: no proof of work, no valid signature, no fee, and the node keeps every one. Any peer could grow that map until the node died of memory exhaustion. Cap at 100, evicting at random rather than in map order so an attacker cannot keep its own orphans resident by choosing hashes that sort low. Local policy, not consensus -- no effect on block or transaction validity.
2026-07-28Security: anyone can spend anyone's coins, plus three more consensus bugs (#12)Bitflash
* script: fix OP_RETURN spend bypass, disable unbounded opcodes OP_RETURN let anyone spend anyone else's outputs. VerifySignature evaluates scriptSig and scriptPubKey as one concatenated script. OP_RETURN was implemented as 'pc = pend', which jumps past everything that follows, and EvalScript ends with CastToBool(stack.back()). So a scriptSig of <OP_TRUE OP_RETURN> skipped the entire scriptPubKey -- OP_CHECKSIG included -- and the final check read the attacker's own OP_TRUE. No signature required, any output spendable. This is the 0.1.0 behaviour Bitcoin fixed in 0.3.5. OP_RETURN now fails the script outright. Also disables the opcodes Bitcoin removed for being unbounded, and caps pushed elements at 520 bytes: OP_CAT grows a stack element with no limit -- <x> OP_DUP OP_CAT repeated doubles it each time, so a script that fits in a transaction exhausts memory on every node that validates it. OP_LSHIFT shifts by an attacker-supplied amount with no ceiling, same effect. There were no element, stack or opcode-count limits of any kind. Nothing on this chain uses these opcodes -- all outputs are standard pay-to-pubkey-hash or coinbase pay-to-pubkey -- so no existing transaction changes validity. * main: reject blocks containing a duplicate transaction (CVE-2012-2459) BuildMerkleTree duplicates the trailing hash whenever a tree level has an odd width. Appending a copy of the last transaction therefore produces a block whose merkle root -- and whose block hash -- match a legitimate block exactly. A node handed the forgery rejects it (the duplicated inputs are already spent) and remembers that hash as bad. When the genuine block arrives carrying the same hash, it is refused too, and the node stops following the real chain. CheckBlock now rejects any block that carries the same transaction twice, which is what the mutation requires. * main: halve the subsidy on the block's height, not the node's tip GetBlockValue derived the halving from nBestHeight -- the validating node's own chain tip -- rather than from the height of the block being valued. The subsidy a block may pay is a property of that block, so reading it off local state makes validity depend on how far the node has synced: across a halving boundary a node still catching up would allow 50 BTF where a synced node allows 25, and the two would follow different chains. Reorgs have the same problem in miniature. Harmless today (every node computes nHeight/210000 == 0 at height ~2400) and it changes no existing block's validity, but it splits the network at height 210000. The height is now passed in explicitly: pindex->nHeight when connecting, tip + 1 when mining or building pool work.
2026-07-28Merge PR #6 (large refactor), preserving main's fixes (#9)Bitflash
* 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>
2026-07-27Add Linux GUI installer via Wine (#3)Victorio Nascimento
* Add Linux GUI installer via Wine * Fix launcher creation and add BTF_RELAY env var support * Pool server, Linux GUI, unified binary, README update * Root Makefile: make linux / make windows, cleaner README * Remove headless.cpp * Fix XPM color count mismatch causing toolbar image errors on Linux * XPM icons: same code on Windows and Linux, no ifdefs around image loading * Fix GTK icon crash and Linux printf format * btfrv: 10s socket timeout on relay connects * Faster peer discovery: more dials per pass, shorter backoff and sleep * Parallel btf peer connections * Fix GetMyExternalIP - was disabled, now uses modern IP echo services * Fix tunnel disconnect on idle and restore external IP detection * Filter stale Nostr descriptors * Sort peers by freshness, fresh first * Pool: add .btf Stratum listener — miners connect via rendezvous, no port forwarding needed * btf pool: self-replacing listeners, one per relay, no queuing between miners * Mining modes: solo/operator/participant — pool server on-demand, participant Stratum client * Status bar: show external IP and .btf address * README: clean rewrite, concept-first, minimal * make linux produces AppImage * make linux produces AppImage * Pool dashboard: connect instructions, tabs, share bars * Perf fixes: GetMyExternalIP threaded, btf listener counted * Perf fixes: GetMyExternalIP threaded, btf listener counted * Fix pool payouts: defer until coinbase maturity * Pool: clean mode switching, IP warning * Pool: clean mode switching * Fix gPoolRunning: remove static * AppImage: use real icon from bitcoin.ico * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Fix exit crash; Mining menu * Windows build: use mingw32-make * Windows build: simpler make invocation * Add deps-windows target * Windows build fixes * Windows build fixes * Windows makefile: fix obj dir * Windows: output to repo root * Windows: bundle DLLs into zip * Replace wxWidgets with ImGui+GLFW: same build on Linux and Windows * Fix %I64d that slipped back through rebase (main.h, net.h) * MinGW: add _mkdir shim for non-MSVC Windows; fix %I64d * Fix gPoolRunning extern outside ifndef _WIN32 * Remove -lboost_system: boost is header-only in this codebase * Status bar: copy buttons for IP and .btf address * Windows: add app icon via windres, strip wx stuff from rc file * Clean GUI: single Options menu item, bigger font, better spacing, cleaner options dialog; update README * GUI: embed Roboto font at 16px for clear readability on all platforms * Windows: windres depends on obj dir target * Fix balance hidden off-screen: put on own line below address bar * Windows: inline mkdir in every rule, remove dir target that confused make * Windows: move all target to top so it's the default rule * Fix pool server not starting: always start when operator selected, re-sync dialog state on open * Write debug.log to data directory (AppData/Roaming/Bitflash on Windows) * Participant miner: use wallet address as username so payouts go to wallet * Add .gitattributes to lock LF line endings, prevent CRLF drift * Refactor pool mining to btf-only transport * Persist pool settings and startup wiring * Prune stale peers and expose pool queries * Fix build regressions and add CLI mode/arg parity * Fix rpc miner stats declaration order * Centralize pool runtime extern declarations * Separate operator/participant dashboards and restore peer stats * Add live telemetry strip and clearer per-mode runtime metrics * Reduce mining dashboard clutter and remove manual pool entry * Count only authorized pool miners * Seed initial pool job on startup * Use a real share target for pool work * Switch pool accounting to round-based shares * Fix nostr pool announcer stats call sites * Fix pool stats definition order * Improve pool worker status display * Add mutex include for participant status * Replace ImClamp in dashboard layout * Clarify participant work status * Log participant status transitions * Reduce peer discovery log noise * Add participant status badge * Track participant auth acknowledgements * Log and validate pool job delivery --------- Co-authored-by: Bitflash Pool Dev <pool@bitflash>
2026-07-24Bitflash: fair-launch releaseBitflash-sh
Satoshi's bitcoin-0.1.0 reborn with RandomX CPU proof of work, anonymous .btf addressing over Nostr with CGNAT rendezvous, automatic peer discovery, a headless Linux node, and Windows GUI wallet. Fair launch, no premine, 21M cap.