From 03a69ccfb36861948e11b0871bb9af8f0b280709 Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Wed, 2 Sep 2026 14:14:52 +0000 Subject: payout: never collapse a log line that reports money moving LogPrint()'s burst collapse is right for share and peer chatter and wrong for anything that reports an irreversible change to money. A reorg voided 100 ledger entries totalling 4,950 BTF and the log carried one line, naming one block and 49.49 BTF. The other 99 were counted and never printed, and no "(N similar)" summary appeared either -- a summary was only ever flushed when that same call site fired again, and after the reorg it never did. The missing 4,950 BTF was found by counting entries in pending_payouts.json. Add LogPrintAlways(), which skips the collapse entirely, and move the four payout lines that report money to it: an obligation voided, an obligation dropped, a payment sent, a payment that failed. All four are bursty by nature -- one reorg voids every entry funded by the orphaned block, from a single source line -- which is precisely why one line must not stand in for all of them. Their volume is bounded by how much money moved, which is a number worth the lines. Also sweep expired windows for every call site rather than only the one currently logging, so a burst that stops is still summarised, and report the window's real elapsed time instead of a hardcoded 3s. --- src/rpc.cpp | 10 +++--- src/util.cpp | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++++------ src/util.h | 22 +++++++++++--- 3 files changed, 114 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/rpc.cpp b/src/rpc.cpp index a4b53ca..2e8f4b8 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -1732,7 +1732,9 @@ static void FlushMaturePayouts() } int64 total = 0; for (auto& kv : it->amounts) total += kv.second; - LogPrint("payout", "[payout] block %d (%s) was orphaned -- voiding %zu" + // Always: one line per voided entry, never a collapsed summary. + // A reorg voids these in a burst from this one source line. + LogPrintAlways("payout", "[payout] block %d (%s) was orphaned -- voiding %zu" " payment(s) totalling %s\n", it->blockHeight, it->blockHash.ToString().substr(0, 14).c_str(), it->amounts.size(), FormatMoney(total).c_str()); @@ -2036,7 +2038,7 @@ static void PayoutThreadFn(void*) // funding block can land at any point in that window. Upstream has // no equivalent -- it decides once, when the entry matures. if (!PayoutBlockStillOnChain(r.blockHash)) { - LogPrint("payout", "[payout] block %d (%s) orphaned while queued" + LogPrintAlways("payout", "[payout] block %d (%s) orphaned while queued" " -- dropping %s to %s\n", r.blockHeight, r.blockHash.ToString().substr(0, 14).c_str(), FormatMoney(r.amount).c_str(), r.address.c_str()); @@ -2205,7 +2207,7 @@ static void PayoutThreadFn(void*) // described by at least one of them at every instant. RecordPayoutTxid(r, wtx.GetHash().GetHex()); ForgetPayment(r); - LogPrint("payout", "[payout] paid %s to %s (tx %s)\n", + LogPrintAlways("payout", "[payout] paid %s to %s (tx %s)\n", FormatMoney(r.amount).c_str(), r.address.c_str(), wtx.GetHash().ToString().substr(0, 12).c_str()); } else { @@ -2263,7 +2265,7 @@ static void PayoutThreadFn(void*) // going away must not turn the log into one line a minute // forever, and must not go quiet either. if (r.retries <= 5 || r.retries % 60 == 0) - LogPrint("payout", "[payout] FAILED to pay %s to %s" + LogPrintAlways("payout", "[payout] FAILED to pay %s to %s" " (%s spendable, wallet unlocked, block %d on" " the main chain) -- attempt %d, retrying in" " 60s; this debt stands until it is paid\n", diff --git a/src/util.cpp b/src/util.cpp index 0cd0133..b5b145f 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -47,6 +47,61 @@ struct LogDedupState { static std::map g_logDedup; static CCriticalSection cs_logDedup; +// A pending "(N similar)" summary, lifted out of the table while the lock is +// held and written after it is released. +struct LogDedupFlush { + std::string category; + int suppressed; + int64 elapsed; // how long the window that counted them ran + std::string lastMsg; +}; + +static int64 g_logDedupLastSweep = 0; + +static void EmitDedupSummary(const LogDedupFlush& f) +{ + OutputDebugStringF("[%s] (%d similar in %llds, last: %s)\n", + f.category.c_str(), f.suppressed, + (long long)f.elapsed, f.lastMsg.c_str()); +} + +// A window is otherwise only closed out when its own call site fires again, so +// a burst that simply stops -- 100 ledger entries voided in one pass and then +// silence -- leaves its count sitting in the table and the summary is never +// written at all. That is how 99 of 100 voids went unreported, with no "(N +// similar)" line to say anything was missing (issue #3). Sweep every expired +// window instead, so a finished burst is summarised as soon as anything at all +// is logged. +// +// Expired entries are erased rather than reopened: the window is over, so there +// is no state left worth keeping, and the next call from that site then takes +// the "first time" path and prints directly -- exactly what the rollover branch +// below would have done. It also stops the table growing without bound. +// +// Caller holds cs_logDedup; the summaries are written after it is released. +static void SweepExpiredWindowsLocked(int64 now, std::vector& out) +{ + for (std::map::iterator it = g_logDedup.begin(); + it != g_logDedup.end(); ) + { + if (now - it->second.windowStart < DEDUP_WINDOW_SECS) + { + ++it; + continue; + } + if (it->second.suppressed > 0) + { + LogDedupFlush f; + f.category = it->first.substr(0, it->first.find('|')); + f.suppressed = it->second.suppressed; + f.elapsed = now - it->second.windowStart; + f.lastMsg = it->second.lastMsg; + out.push_back(f); + } + g_logDedup.erase(it++); + } +} + void LogPrintDedup(const char* category, const char* file, int line, const std::string& msgIn) { // Call sites follow the existing printf() convention of ending their @@ -61,11 +116,19 @@ void LogPrintDedup(const char* category, const char* file, int line, const std:: int64 now = GetTime(); bool fEmitNow = false; - int suppressedToFlush = 0; - std::string flushedMsg; + std::vector flushes; CRITICAL_BLOCK(cs_logDedup) { + // At most once a second. The table holds one entry per call site that + // has fired in the last few seconds, so the walk is short, but it must + // not run once per line of a burst. + if (now != g_logDedupLastSweep) + { + g_logDedupLastSweep = now; + SweepExpiredWindowsLocked(now, flushes); + } + std::map::iterator it = g_logDedup.find(key); if (it == g_logDedup.end()) { @@ -76,10 +139,18 @@ void LogPrintDedup(const char* category, const char* file, int line, const std:: } else if (now - it->second.windowStart >= DEDUP_WINDOW_SECS) { - // Window rolled over: flush what was suppressed during it (if - // anything), then start a new window with this call printed. - suppressedToFlush = it->second.suppressed; - flushedMsg = it->second.lastMsg; + // Window rolled over within the same second as the last sweep: + // flush what was suppressed during it (if anything), then start a + // new window with this call printed. + if (it->second.suppressed > 0) + { + LogDedupFlush f; + f.category = category; + f.suppressed = it->second.suppressed; + f.elapsed = now - it->second.windowStart; + f.lastMsg = it->second.lastMsg; + flushes.push_back(f); + } it->second.windowStart = now; it->second.suppressed = 0; it->second.lastMsg = msg; @@ -94,13 +165,23 @@ void LogPrintDedup(const char* category, const char* file, int line, const std:: } } - if (suppressedToFlush > 0) - OutputDebugStringF("[%s] (%d similar in %llds, last: %s)\n", - category, suppressedToFlush, (long long)DEDUP_WINDOW_SECS, flushedMsg.c_str()); + for (size_t i = 0; i < flushes.size(); i++) + EmitDedupSummary(flushes[i]); if (fEmitNow) OutputDebugStringF("[%s] %s\n", category, msg.c_str()); } +// LogPrintAlways() -- see the doc comment in util.h. No window, no counting, +// no table entry: every call writes its own line. +void LogPrintNoCollapse(const char* category, const std::string& msgIn) +{ + std::string msg = msgIn; + while (!msg.empty() && (msg.back() == '\n' || msg.back() == '\r')) + msg.pop_back(); + + OutputDebugStringF("[%s] %s\n", category, msg.c_str()); +} + diff --git a/src/util.h b/src/util.h index d72518b..fe605a1 100644 --- a/src/util.h +++ b/src/util.h @@ -106,15 +106,29 @@ extern bool fDebug; // 2. Burst collapse: repeated LogPrint() calls from the *same source line* // only emit one line per DEDUP_WINDOW_SECS; if more calls land inside // that window they're counted instead of reprinted, and the count is -// flushed as a single "(xN similar in Ws)" line the moment the window -// rolls over. A loop discovering 500 peers by address now produces a -// small, bounded number of lines instead of 500 -- log growth is capped -// by elapsed time, not by how many similar events happen to fire. +// flushed as a single "(xN similar in Ws)" line once the window expires +// -- whether or not that call site ever fires again. A loop discovering +// 500 peers by address now produces a small, bounded number of lines +// instead of 500 -- log growth is capped by elapsed time, not by how +// many similar events happen to fire. +// +// Use LogPrintAlways() instead for anything that reports an irreversible +// change to money: an obligation voided, a payment sent, a payment that +// failed. One line standing in for an unknown number of identical events is +// a tidy report of peer chatter and a false report of a ledger. A reorg +// voiding 100 entries hits one source line 100 times inside one window; +// under LogPrint() that read as a single block losing a single 49.49 BTF +// payment, while 4,950 BTF of obligations were actually destroyed, and the +// discrepancy was only found by counting entries in pending_payouts.json +// (issue #3). Bounded log growth is not worth that ambiguity here. // --------------------------------------------------------------------------- bool LogAcceptsCategory(const char* category); void LogPrintDedup(const char* category, const char* file, int line, const std::string& msg); +void LogPrintNoCollapse(const char* category, const std::string& msg); #define LogPrint(category, ...) \ do { if (LogAcceptsCategory(category)) LogPrintDedup((category), __FILE__, __LINE__, strprintf(__VA_ARGS__)); } while (0) +#define LogPrintAlways(category, ...) \ + do { if (LogAcceptsCategory(category)) LogPrintNoCollapse((category), strprintf(__VA_ARGS__)); } while (0) // Every format string in this project goes through one of the four functions // below -- `printf` itself is remapped to OutputDebugStringF just above, and -- cgit v1.2.3 From 0b7363211f954698e593699f32ed13b425fce2bc Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Wed, 2 Sep 2026 14:33:56 +0000 Subject: genesis: say that the 2025 nTime is known and must not be "fixed" nTime is 1753315200, which is 2025-07-24T00:00:00Z. The comment beside it claimed 2026-07-24, and the headline in pszTimestamp reads 24/Jul/2026, so the constant looked like a typo somebody ought to correct. Nothing is wrong with the data. nTime matters only as an input to the genesis hash, the block hashes to hashGenesisBlock with it, the asserts pass, and the whole network agrees. The hazard is entirely in acting on the mismatch. Editing nTime alone trips the assert -- loud, and it reverts. Editing it and regenerating GENESIS_NONCE and hashGenesisBlock to match is the quiet one: that node starts without complaint, builds its own chain from height 0, and rejects every real peer, and the re-mining branch a few lines below prints exactly the constants needed to do it. So: correct the comment rather than the constant, state the discrepancy as known and uncorrectable, and have the re-mining printout say what those constants are and are not for. No value changes -- pszTimestamp, the scriptPubKey, nTime, nBits, nNonce, GENESIS_NONCE, hashGenesisBlock and hashGenesisMerkleRoot are all byte-identical. --- src/main.cpp | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/main.cpp b/src/main.cpp index 7b3e555..a68e3b0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2678,7 +2678,18 @@ bool LoadBlockIndex(bool fAllowNew) // CTxOut(nValue=50.00000000, scriptPubKey=0x5F1DF16B2B704C8A578D0B) // vMerkleTree: 4a5e1e - // Bitflash genesis block -- own header (fair launch, 2026 stable relaunch) + // Bitflash genesis block -- own header (fair launch) + // + // Do not change pszTimestamp, the scriptPubKey, nTime or nNonce. They + // are the inputs to the genesis hash, and the genesis hash is + // consensus. Changing one on its own trips the assert below, which is + // loud and reverts cleanly; changing one and regenerating + // GENESIS_NONCE and hashGenesisBlock to match -- which is exactly what + // the re-mining branch below prints out and invites -- gives a node + // that starts without complaint, builds its own chain from height 0, + // and rejects every real peer as being on a different chain. That one + // fails silently, and the damage scales with how many people run the + // "fixed" build. char* pszTimestamp = "Bitflash 24/Jul/2026 Fair launch: one CPU one vote, no premine"; CTransaction txNew; txNew.vin.resize(1); @@ -2691,7 +2702,14 @@ bool LoadBlockIndex(bool fAllowNew) block.hashPrevBlock = 0; block.hashMerkleRoot = block.BuildMerkleTree(); block.nVersion = 1; - block.nTime = 1753315200; // 2026-07-24 00:00:00 UTC (stable relaunch) + // 1753315200 is 2025-07-24T00:00:00Z. The headline in pszTimestamp + // above reads 2026 and does not match it. This is a known discrepancy + // and not a typo in the constant: the value is only consensus-critical + // as an input to the genesis hash, the block hashes to + // hashGenesisBlock with it, and every node on the network agrees. The + // mismatch is in the launched chain and cannot be corrected -- see the + // warning above before deciding otherwise. + block.nTime = 1753315200; block.nBits = 0x1f0fffff; // = bnProofOfWorkLimit (Bitflash RandomX floor) block.nNonce = GENESIS_NONCE; @@ -2718,6 +2736,11 @@ bool LoadBlockIndex(bool fAllowNew) printf("powhash = %s\n", block.GetPoWHash().ToString().c_str()); printf("hash = %s\n", block.GetHash().ToString().c_str()); printf("merkle = %s\n", block.hashMerkleRoot.ToString().c_str()); + printf("----------------------------------------\n"); + printf("These constants bring up a NEW chain. Do not paste them\n"); + printf("into a build for this network to make an edited nTime or\n"); + printf("pszTimestamp fit -- a node whose genesis differs starts\n"); + printf("cleanly on a chain of its own and rejects every real peer.\n"); printf("========================================\n"); } -- cgit v1.2.3 From 437d23184e6edd2d1777ef25e5a5a091adbca551 Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Wed, 2 Sep 2026 14:35:18 +0000 Subject: docs: third-party RandomX miners cannot mine Bitflash pools The README offered XMRig and SRBMiner command lines pointed at the local Stratum bridge. Neither can work. In RandomX mode both target the Monero-style JSON-RPC protocol (login/job/submit) while the pool speaks Bitcoin-style Stratum (mining.subscribe/notify/submit); both place the nonce at byte 39 of the job blob, which in an 80-byte Bitflash header is inside the merkle root, not the nonce at byte 76; and both require a 32-byte seed hash as 64 hex characters, where randomx_pow.cpp keys RandomX from a fixed 36-byte string that is not a hash at all. None of it is configurable. -stratumbridge is a byte relay between a local socket and the .btf tunnel (BridgePump, main.cpp) -- it fixes reachability, the one blocker an operator could otherwise do something about, and translates nothing. Say that where the commands used to be, so the question gets answered instead of asked again. Also correct the magic bytes in src/readme.txt, which documented bf 1a 5c fd; net.h has bf 20 5c fd. --- README.md | 18 ++++++++++++++---- src/readme.txt | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/README.md b/README.md index 1782bc3..77a0ad8 100644 --- a/README.md +++ b/README.md @@ -289,12 +289,22 @@ See [public pool directory](docs/pool-directory.md) for the website/API shape. **Participant** — mine to someone else's pool. Enter or select the pool's `.btf` address and enable Start Mining. The built-in participant miner connects to operator pools through the `.btf` -path. External miners can use Bitflash as a local Stratum bridge: +path. Use it — **third-party RandomX miners such as XMRig and SRBMiner cannot +mine Bitflash pools**, and no amount of configuration changes that. In RandomX +mode they speak the Monero-style JSON-RPC protocol (`login`/`job`/`submit`) and +assume a Monero-style job: the nonce at byte 39 of the blob, and a 32-byte seed +hash given as 64 hex characters. Bitflash pools speak Bitcoin-style Stratum +(`mining.subscribe`/`mining.notify`/`mining.submit`) over an 80-byte header +whose nonce is at byte 76 — byte 39 falls inside the merkle root — and key +RandomX from a fixed 36-byte string that is not a hash at all. + +`-stratumbridge` carries that same Bitcoin-style Stratum over the `.btf` path, +so a miner on this machine can reach a pool it otherwise could not address. It +is a byte relay, not a protocol translator, so it helps only a miner that +already speaks the pool's dialect, and it does not make the miners above work: ```bash ./bitflash -nogui -stratumbridge=POOL_BTF_ADDRESS -stratumbridgeport=3333 -SRBMiner-MULTI --algorithm randomx --pool 127.0.0.1:3333 --wallet YOUR_BTF_ADDRESS --password x -xmrig -a rx/0 -o 127.0.0.1:3333 -u YOUR_BTF_ADDRESS -p x ``` Estimate rewards and electricity cost locally: @@ -322,7 +332,7 @@ Bitflash has a reliable public market. -poolstatusfile=/var/www/pool_status.json \ -poolroundsfile=/var/www/pool_rounds.json ./bitflash -nogui -gen -participant=POOL_BTF_ADDRESS # mine to pool -./bitflash -nogui -stratumbridge=POOL_BTF_ADDRESS # local bridge for XMRig/SRBMiner +./bitflash -nogui -stratumbridge=POOL_BTF_ADDRESS # Stratum over .btf (see Mining) ``` Every option takes `-` or `/`. **Under MSYS2 use the `-` form** — the shell diff --git a/src/readme.txt b/src/readme.txt index 752be66..7490a9a 100644 --- a/src/readme.txt +++ b/src/readme.txt @@ -16,7 +16,7 @@ This product includes software developed by the OpenSSL Project. Differences from the original Bitcoin ------------------------------------- - Own genesis block (22/Jul/2026 headline, independent chain) -- Own magic bytes (0xbf 0x1a 0x5c 0xfd) and P2P port 8433 +- Own magic bytes (0xbf 0x20 0x5c 0xfd) and P2P port 8433 - Addresses start with "B" (ADDRESSVERSION = 25) - Consensus fixes applied: value overflow (CVE-2010-5139), duplicate inputs, money range checks (MoneyRange) -- cgit v1.2.3 From b441687b5344454bea16384fe1280e470964faaa Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Thu, 3 Sep 2026 18:31:32 +0000 Subject: payout: move the last two burst-capable money lines to LogPrintAlways Integration review found two more payout lines still on plain LogPrint, both fired per record inside the batch loop in PayoutThreadFn -- the same one-window burst shape as the four already converted. The "writing off" line in ForfeitPayoutRecipient and the "SKIPPED bad address" line at the write-off in PayoutThreadFn both report obligations being destroyed, which is the class of line one summary must never stand in for (issue #3). Message text is unchanged. --- src/rpc.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/rpc.cpp b/src/rpc.cpp index 2e8f4b8..6531fe8 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -1933,7 +1933,7 @@ static void RecordPayoutTxid(const PayingRecord& r, const std::string& txid) // disagreeing. static void ForfeitPayoutRecipient(const PayingRecord& r, const std::string& reason) { - LogPrint("payout", "[payout] writing off %s owed to %s for block %d: %s\n", + LogPrintAlways("payout", "[payout] writing off %s owed to %s for block %d: %s\n", FormatMoney(r.amount).c_str(), r.address.c_str(), r.blockHeight, reason.c_str()); VersionedBlob blob; @@ -2082,7 +2082,7 @@ static void PayoutThreadFn(void*) const std::string reason = strprintf("'%s' is not a valid address, so no transaction" " can pay it", r.address.c_str()); - LogPrint("payout", "[payout] SKIPPED bad address '%s' --" + LogPrintAlways("payout", "[payout] SKIPPED bad address '%s' --" " %s owed for block %d cannot be paid\n", r.address.c_str(), FormatMoney(r.amount).c_str(), r.blockHeight); -- cgit v1.2.3