Skip to content

Pool/relay stability fixes, compact block relay, and signed auto-update + release automation - #31

Closed
Vic-Nas wants to merge 259 commits into
Bitflash-sh:mainfrom
Vic-Nas:main
Closed

Pool/relay stability fixes, compact block relay, and signed auto-update + release automation#31
Vic-Nas wants to merge 259 commits into
Bitflash-sh:mainfrom
Vic-Nas:main

Conversation

@Vic-Nas

@Vic-Nas Vic-Nas commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

This picks up from the earlier PR (closed), rebased on current main — it now includes the 1.2.1/1.2.2 hardening series (orphan cap, sigops cap, Stratum bounds, string-resize fix), which the earlier version predated. Grouped by area below; happy to split into separate PRs per area if that's easier to review, per your earlier offer.

Consensus / security

  • OP_RETURN spend bypass, unbounded opcodes, duplicate-tx (CVE-2012-2459), halving-height desync — carried from the original PR, confirmed byte-identical to what's on main now.
  • Nostr TLS: was accepting connections with no certificate verification at all (fully MITM-able); now verifies, and ships a vendored CA bundle so Windows doesn't depend on a system store that may not have it.
  • Six spots where a relay answering with [1] instead of ["EVENT",...] crashed the read thread (j[0].get<string>() with no type check) — same bug class as nostr: bound what a relay can make the node allocate #17, ported to our libwebsockets-based reader since the original frame-size-cap fix doesn't apply after the WebSocket rewrite below.

Pool / relay stability (the "broken before" work)

  • A long-running conflict where pool Stratum and p2p peer connections fought over the same Nostr relay slot, causing miners to get stuck — traced and fixed across several iterations rather than one patch (relay slot partitioning, operator-mode yielding, duplicate announcement removal, stale-registration handling).
  • Relay connect stalls and CDN handshake rejections fixed.
  • Log dedup window widened (was falsely collapsing distinct events), log rotation added so debug.log doesn't grow unbounded.

Networking

  • Hand-rolled WebSocket/TLS client replaced with libwebsockets.
  • Direct-IP listener removed (superseded by Nostr-based discovery); listen socket binds to 0.0.0.0 instead of a gethostname()-derived address.

Compact block relay

  • BIP152-style compact blocks, backward compatible with old nodes, own test file.

Auto-update + release automation (new since the earlier PR)

  • Self-updater: checks GitHub's latest release, verifies an Ed25519 signature on the asset before installing anything, fails closed on any error (network, missing signature, mismatch). Wired into the GUI with an explicit "Update Now" / "Skip" dialog — never applies without a click, check and apply stay separate functions.
  • Both REPO and the signing public key are mandatory build-time arguments now, not defaults — and the key is never hardcoded. tools/generate_update_key.sh generates a keypair locally; whoever merges this should run it themselves and use their own key, not carry mine over — that was your first concern last review, and this is the actual fix for it, not just a promise.
  • make linux-self / make windows-self build the identical app with the updater compiled out entirely (BITFLASH_NO_UPDATER) — no repo tie-in, no key required, no GitHub call ever made, for anyone who doesn't want auto-update at all.
  • GitHub Actions workflow: fires on VERSION bump, builds Linux (AppImage) and Windows (zip) in parallel, signs both with the release key, publishes one release with changelog ranked by diff size. A preflight job validates the signing key/pubkey pair before either build runs, so a bad secret fails in seconds instead of after a 5-minute build.

Vic-Nas and others added 30 commits July 26, 2026 13:16
Vic-Nas added 28 commits July 30, 2026 03:23
…t lock contention

RefreshWallet cleared g_txRows unconditionally, then tried a
non-blocking lock (TRY_CRITICAL_BLOCK) on cs_mapWallet to repopulate
it. GetBalance() right above it uses a blocking lock and always
succeeds -- so a moment where cs_mapWallet is busy (rapid block
processing during a sync burst holding it repeatedly, for instance)
left the balance correct but wiped the transaction list, and it
stayed wiped until some future refresh happened to win the race.
Reported symptom matched exactly: balance still reflects blocks won
earlier, transaction list empty.

Now the clear only happens once the lock is actually acquired, so a
busy moment just skips that refresh cycle (keeps showing the last
good list) instead of emptying it in the meantime.
A push that bumps VERSION triggers the whole pipeline; if preflight,
either build, or the release step itself fails, that version number
was still consumed -- the next bump has to skip past it, leaving a
permanent gap (we've done this several times this session: 1.2.4
failed, 1.2.5 failed, etc., all burned).

New job watches preflight/build-linux/build-windows/release and, if
any of them actually failed (not skipped -- a job only shows
'skipped' if something upstream of it never ran), reverts the
VERSION-bump commit and pushes that. Scoped to push events only, not
workflow_dispatch (which can run on any commit for testing, not
necessarily one that bumped VERSION -- nothing to revert there). If
the revert can't apply cleanly (main moved on since, most likely from
a fix being pushed immediately after seeing the failure), it aborts
and leaves a warning rather than forcing anything -- safer to require
a manual look in that case than to guess.
…ease cleanup

git revert only ever touched VERSION here anyway (it's scoped to
exactly the diff of the one commit being reverted, which only ever
changes that one file) -- but 'revert commit' sounds like it could do
more than that, and there's no reason to rely on that nuance being
obvious. Now it's literal: read VERSION's content from the parent of
the failed commit, write that back, commit just that.

Also swapped the safety check from 'does the revert apply cleanly' to
'is the failed commit still the tip of main' -- simpler to reason
about and catches the same case (something pushed since this run
started) before touching anything, rather than attempting a change
and rolling back if it conflicts.
…eaningless protocol number

The Peers panel's 'Version' column was showing CNode::nVersion --
serialize.h's wire protocol constant (VERSION = 101, unchanged since
this forked from Bitcoin 0.1), not the app release version. Every
peer on any recent build showed identically 101, telling a user
nothing about who's actually up to date.

Appended BITFLASH_APP_VERSION as an optional 5th field on the
existing 'version' message rather than adding a whole new message
type -- the version handshake already exists precisely for this kind
of exchange. An older peer without this field simply won't have sent
it: checked via CDataStream::empty() before reading, not by reading
unconditionally and catching the exception, since a thrown exception
mid-handler would unwind out and skip everything below it (getblocks,
sendcmpct, pex, nick) for any peer on an older build -- not just this
one field.

Needed a 5-arg PushMessage overload; net.h only had up to 4. Column
renamed 'App Version', shows 'unknown' for a peer that hasn't sent
one (older build, or the field hasn't arrived yet).
…ersion field

Bare '1.2.17' is ambiguous across forks -- two peers can show the
same number while running genuinely different code. Now sends
'1.2.17 (owner/repo)' via compile-time string literal concatenation
of BITFLASH_APP_VERSION and UPDATE_REPO (same pattern UPDATE_REPO is
already used with elsewhere); a -self build has no UPDATE_REPO at
all, so it sends '1.2.17 (self-built)' instead. Bumped the receive-
side length cap from 32 to 96 to comfortably fit a repo name too.
…status bar

Two fixes:

1. 4h was overly conservative -- the check itself is one lightweight
   GitHub API GET, nowhere near enough to worry about load even
   checked fairly often for one node. 30 minutes.

2. Found a real bug while looking at this: the old 'Up to Date'
   modal popup (g_showNoUpdateMsg/DrawNoUpdateDialog) survived an
   earlier revert-and-reapply and was still wired up. It made sense
   once, as feedback for a one-off manual check button -- but that
   button doesn't exist anymore, and with periodic re-checking it
   would have popped up every 30 minutes whenever nothing was found,
   which is most of the time. Removed entirely.

Replaced both gaps with what was actually being asked for: the status
bar's 'Up to date' text now shows how long ago the last check
completed ('checked 12m ago' etc.), so periodic checking is visibly
happening even when there's nothing to report, instead of either a
recurring popup or total silence.
…ddresses got paid

Walked back an earlier decision that was wrong. A UTXO transaction's
inputs spend specific previous outputs, and each of those has its own
address -- that address is the previous owner of the coins now paid
to you, which is what 'who sent this' means here. Not a different or
harder question than 'who did I pay' (which was already implemented
via the payee address on a Sent row); this was skipped before over a
privacy concern that wasn't really the point of what was asked.

ExtractSenderAddrs traces each input via CTxDB::ReadDiskTx(prevout),
extracts that output's address, applies the same address-book
formatting Sent rows already use (split out into a shared
FormatAddrList so both paths stay consistent). Skips an input whose
previous transaction can't be found rather than failing the whole
row -- still shows whatever does resolve. Coinbase transactions have
no real sender and are handled separately already (shown as
'Generated'), so this returns empty for those.
…sh#37); fix /gen not actually mining (port of Bitflash-sh#42)

Two real gaps, both pulled from upstream, both squarely in "the
command line has been neglected" territory.

## Headless build (Bitflash-sh#37)

The GUI binary needs libGL present at load time even when started
with -nogui/-daemon: the dynamic loader resolves every dependency
before main() runs, so the flag arrives far too late. A clean server
-- where a node most naturally lives -- fails outright with a shared-
library error that gives no hint the program would otherwise work
fine. The AppImage additionally needs FUSE, which minimal servers
lack.

`make linux-node-build` (wired into both `linux` and `linux-self`)
produces a second binary, `$(APPNAME)-node-<version>-x86_64`, from
the same node objects, without ImGui/GLFW/OpenGL at all -- no
graphics or FUSE dependency. Two small moves were needed: headless.cpp
provides an empty MainFrameRepaint() (declared in headers_core.h,
previously only ever defined in gui.cpp, which a headless build
doesn't compile), and DateTimeStr moved from gui.cpp to util.cpp
since it's not actually GUI-specific.

Adapted rather than copied: our src/Makefile carries extra objects
(nostr/compactblock/update) and the mandatory REPO/UPDATE_PUBKEY (or
NO_UPDATE=1) machinery upstream doesn't have. Caught one real bug
while wiring the root Makefile's linux-node-build target: forwarding
NO_UPDATE unconditionally (even as an empty value) would have made
`ifdef NO_UPDATE` true in the child invocation regardless -- GNU
Make's ifdef checks whether a variable was set at all, not whether
it's non-empty -- silently disabling the updater in every node build,
including regular (non-self) ones. Branched explicitly instead.

## /gen not mining (Bitflash-sh#42)

nMineMode defaults to MINE_RELAY and nothing in ParseStartupArguments
ever changed it for the plain /gen case (only /operator and
/participant set their own mode) -- so a node started with /gen alone
spawned ThreadBitcoinMiner, hit BitcoinMiner()'s relay guard, and
returned immediately. The node ran indefinitely looking completely
healthy -- synced, held peers, no errors -- while mining nothing, with
the only hint being nMineMode = 0 in a startup line that reads like a
status echo. This affects every headless miner, since the GUI was the
only other way to select a mining mode. /gen now implies MINE_SOLO
unless /operator or /participant asked for something else.
…RM64 build (port of Bitflash-sh#41); gitignore: cover key files (port of Bitflash-sh#43)

## Bootstrap seeds (Bitflash-sh#36)

A node with an empty cache has only the Nostr relays for discovery --
if those are down, blocked, or just slow, a first run has no way into
the network at all, which is exactly the moment a user has the least
patience. Seeds are the floor under that: a compiled-in list of
long-lived peers, tried automatically only when the peer cache is
empty.

A seed entry is deliberately just an address and an encryption key,
no meeting node -- the rendezvous relay pairs on the service's public
key, and a .btf address is that key, so a seed is findable by walking
the known relays. Recording which relay a seed was on would rot the
moment it failed over, which is the exact "advertising a rendezvous
nobody is registered at" failure this project has chased before.
Seeds carry no special authority: they hand out the same signed,
self-certifying descriptors any peer does, so a hostile seed can
stall a bootstrap but can't forge a peer or feed a false chain.

The compiled list here is empty, deliberately -- upstream shipped
their own dedicated bootstrap node's address in their version of this
commit, but that's their infrastructure, not something this fork has
any relationship to or authority to point users at. /btfseed=
ADDRESS:ENCHEX (repeatable, CLI-only) covers testing and private
networks without needing a compiled entry.

Adapted the hex-decode call to our own BtfPeerCacheHexToBytes rather
than a generic HexToBytes upstream doesn't have under that name.

## ARM64 build failure (Bitflash-sh#41)

ByteReverse(word32) in sha.h used the x86 bswap instruction via
inline asm, guarded only by #if defined(__GNUC__) -- true on every
architecture GCC/Clang target, so on aarch64 the assembler rejected
the instruction outright. The 64-bit ByteReverse right below it
already had the correct guard (&& defined(__x86_64__)); the 32-bit
one just never got it. Fails the build on Apple Silicon, ARM servers,
or a Raspberry Pi -- all plausible headless-node hosts.

## .gitignore gaps (Bitflash-sh#43)

*.dat/wallet.dat were covered, but btf_enc.key (this node's .btf
identity) and nostr.key (its Nostr signing key) are not .dat files
and are exactly as sensitive -- already listed explicitly in our
.gitignore from an earlier session, but walletrpc.token (bearer
authority to spend from the wallet over the wallet API) was missing
entirely, along with a general *.key catch-all and patterns for key
backup archives, mempool dumps, and stray .patch/.rej/.orig files. A
node run with its data directory inside a working tree -- the obvious
thing to do while developing -- would otherwise leave all of this
untracked but committable.
StartUpdateCheck only ever ran from inside RunGUI, so a node started
with -nogui/-daemon (or the new headless bitflash-node binary) never
checked for an update at all -- and that's exactly the deployment
most likely to run unattended for a long stretch, which is when
missing a critical update matters most.

No modal to show in a log-only context, so it's a plain log line
instead: same 30-minute interval the GUI uses, checked inline in the
existing sleep loop rather than a new thread. A no-op on a -self
build, same as everywhere else Update_CheckLatest is called (it's a
stub there).
…ith the GUI

Both settings were only ever changeable from the GUI's Options dialog
-- a headless node was stuck with a randomly generated nickname
forever and a transaction fee of 0 with no way to override either
without a window it doesn't have. Persisted the same way the GUI
does (CWalletDB::WriteSetting), so a later GUI session on the same
wallet.dat picks up whatever was set here.
…ash-sh#40)

BitcoinMiner() called key.MakeNewKey() once per mining session and
paid every block's reward to that key -- generated fresh, with no
backup covering it until AddKey() ran, and that only happened *after*
a block was successfully found. Two real ways to lose funds:

1. A wallet.dat backup taken before mining starts doesn't contain
   this key. Mine blocks, restore that backup later, and the reward
   is unrecoverable even though the coins are still on-chain.
   Reported exactly this way against this codebase in Bitflash-sh#40.
2. AddKey() only ran after a block was found and about to be
   processed. A crash between broadcasting the block and that save
   meant the reward's private key existed only in memory and was
   simply gone -- no backup/restore needed to trigger this one.

Both are the same class of bug the keypool already exists to
prevent, just never applied here. vchMinerPubKey = GetKeyFromPool()
instead: the key is durably in wallet.dat (via TopUpKeyPool's AddKey,
already run before the key is ever handed out) from the moment
mining starts, not from the moment a block happens to be found. The
old save-then-generate-next block after a found block is now just
draw-the-next-one, since there's nothing left to save at that point.
Was badly stale: referenced v1.1.0 filenames, a hardcoded 'Bitflash-*'
naming pattern that hasn't been true since release automation shipped,
an 'Options' menu that's been renamed to 'Mine', and build
instructions (plain 'make linux'/'make windows') that fail outright
now that REPO/UPDATE_PUBKEY are mandatory -- someone following this
doc today would hit a wall immediately.

Also added real content that was never documented at all: the
headless bitflash-node binary (no libGL dependency, the actual
recommended path for a server), the periodic signed-update-check
system and how it fails closed, the address book/peers panel, node
nicknames, and current CLI flags. And a prominent backup warning
(Bitflash-sh#40): wallet.dat alone isn't a safe backup without its
database/ subfolder, and a backup is a point-in-time snapshot, not
something that stays valid forever.
Cut from 173 to 111 lines. Kept: what it works like, how to build,
mining modes, headless deployment, the backup warning. Dropped the
separate Peers/Updates sections as their own headers -- folded the
one-line versions of each into Quick start and Headless instead.
Chose the lower-risk path over adopting a TUI library (FTXUI/
cpp-terminal were the two real candidates, both cross-platform and
well-maintained, but pulling either in means integrating a new
dependency into two separate build systems for a display-only
feature). Raw ANSI escape codes instead: cursor-home + per-line clear
each redraw, no full-screen flicker, works on any terminal that
understands ANSI (which is everywhere this app already targets --
Linux, macOS, and Windows 10+ consoles once VT100 processing is
enabled per-handle, done here via SetConsoleMode).

Shows height, peer count, mining mode/status, and -- for participant
mode -- owed balance, round shares, and the overdue-payout warning
already built earlier this session; for operator mode, miner count
and blocks found this session. Update-check status folds in too,
same 30-minute background check as before, just displayed instead of
only logged.

Auto-detects whether stdout is an actual interactive terminal
(isatty/_isatty) and falls back to the existing plain scrolling log
lines otherwise -- a systemd service, a pipe, anything redirected to
a file gets exactly the old behavior, since ANSI redraw codes in a
log file would be either noise or literally corrupted-looking text,
and nobody's watching a redirected stream live to read a dashboard
anyway.

Real risk this could have introduced: background threads' printf/
LogPrint already echo to the console on non-Windows (confirmed in
util.h), which would interleave with and corrupt an in-place-
redrawing display. Added g_fSuppressConsoleEcho, checked right at
that echo site -- set only while the dashboard owns the screen, so
debug.log keeps getting every line as always, console output just
comes from the dashboard alone during that time.
…ndored, MIT)

The previous version was raw ANSI escape codes with no real
cross-platform testing behind it -- exactly the kind of one-off
terminal-handling code that looks fine in review and breaks on a real
Windows console (a failed VT100 enable had no fallback, would have
shown literal escape-code garbage on screen instead of a dashboard).
Correctly called out as the wrong trade-off: I'd avoided a new build
dependency at the cost of correctness on the actual thing being
built.

Vendored FTXUI's official amalgamated release (v7.0.1, MIT, from
GitHub Releases -- same integration shape as ImGui already gets in
this repo: two files, no CMake, no package manager) instead. It's
packaged for Debian/Ubuntu/Arch/vcpkg/Conan and used in exactly this
category of app already (resource monitors, CPU meters), which is
the actual point: battle-tested where the hand-rolled version wasn't.
Confirmed by actually building and running a test program against
the vendored files before committing to this, not just reading docs.

src/dashboard.cpp uses FTXUI's real DOM API (vbox/hbox/text/
separator, Screen::ResetPosition for in-place redraw) instead of raw
escape sequences, and needs none of the manual Windows console-mode
code the old version had -- FTXUI handles that internally (confirmed
in its vendored source, and by the test program running correctly
with zero setup).

FTXUI requires C++17; the rest of this codebase stays gnu++14.
FTXUI_CXXFLAGS derives a C++17 variant of the existing flags (same
defines/includes, just the standard swapped) so only ftxui.cpp and
dashboard.cpp build under it -- nothing else changes standard. Wired
into both Makefiles identically (Linux and Windows/MSYS2), added to
NODE_OBJS so both the GUI binary (-nogui/-daemon at runtime) and the
headless bitflash-node binary link it.

Actually compiled this, not just inspected it -- installed the
missing dev packages in this sandbox to get a real build, which
caught a real bug before it could ship: `using ftxui::Dimension;` is
invalid (Dimension is a namespace, not a type), which would have
failed to build outright. Fixed by qualifying ftxui::Dimension::Full()
directly at its one call site. Confirmed clean compiles across all
three build variants (with-updater, -self, and BITFLASH_NO_GUI) plus
spot-checked that untouched core files (main.cpp, net.cpp, rpc.cpp,
util.cpp) still compile cleanly after the util.h change from the
previous pass (g_fSuppressConsoleEcho, still needed: FTXUI doesn't
know about this codebase's own console-echo path in OutputDebugStringF,
so background threads' log lines would still interleave with and
corrupt the redraw without it).
… duration format

Went back and actually compared against the GUI instead of assuming
the dashboard's original scope was right. Real gap: the GUI's header
shows the wallet address and balance first, before anything else --
same place here now, for the same reason (it's what anyone checking
on a running node wants to see first). Nickname added too, small but
free given it's already loaded.

Deliberately still not mirroring the GUI's full surface: the
transaction list, peer/cached-peer/address-book tables, and pool
selection are genuinely list/management views that don't fit a fixed
non-scrolling frame the way a scrollable GUI panel does -- peer count
stays a count, not an attempt to cram a table into 80 columns. That's
a real design choice, not the same kind of oversight the missing
balance/address was.

Also matched the GUI's uptime/duration format (seconds under a
minute, minutes under an hour, hours after) instead of the
dashboard's own minutes-only format, which read as '150m ago'
instead of '2h ago' on anything long-running -- same FormatDuration
used for uptime, the update-check age, and the overdue-payout
warning, so all three read consistently instead of each doing their
own thing.

Recompiled against the real headers after these changes (same
process as the initial FTXUI port) -- clean, no new errors.
Previously the dashboard could only tell you an update existed --
'run the GUI build to apply it there' was a dead end for anyone
running bitflash-node unattended on a box with no GUI anywhere on it.

Rebuilt on FTXUI's component module (Renderer + CatchEvent) instead
of the previous static print loop, using ScreenInteractive/Loop --
already-vendored, already-tested library code, not hand-rolled
keyboard input handling. Verified by compiling and partially linking
against the real headers again (same process as the initial FTXUI
port): confirmed every component-module symbol used here (Renderer,
CatchEvent, ScreenInteractive, Loop, Event) resolves cleanly from the
vendored ftxui.cpp, with zero FTXUI-related link errors.

Ctrl-C is deliberately left to FTXUI's own default handling rather
than mixing in a custom signal() call for this path -- confirmed via
FTXUI's own issue tracker that combining an external SIGINT handler
with the library's internal terminal/signal management is a known
way to end up with a hung terminal on exit. Periodic redraw and the
update check itself run on a background thread using
screen.PostEvent(Event::Custom), the same pattern FTXUI's maintainers
point people to across multiple discussions for exactly this
'redraw on a timer, not just on input' case.

Pressing 'u' only does anything once an update is actually shown as
available (a no-op otherwise); it hands off to
Update_ApplyAndRelaunch on its own thread, same signature-verification
path as the GUI's Update Now button, same fail-closed behavior.
…date modal

g_showUpdateDialog was doing two jobs -- whether the modal is open,
and whether the status bar thinks an update exists -- so clicking
Skip (which correctly only closes the modal) also incorrectly made
the status bar forget the update was ever found.

Split into two flags: g_showUpdateDialog controls only the modal now,
g_updateAvailable is set once a check finds something newer and stays
true regardless of dismissing the modal, until an actual update
succeeds (which relaunches the process, making the state moot) or the
app restarts. Status bar's Update button now also re-opens the modal
when clicked, giving a way back to it after a dismiss.
GetDepthInMainChain() returns 0 for both a genuinely unconfirmed tx
and an orphaned coinbase (block not in main chain) -- same value,
so every depth-0 row had no defined order relative to each other and
fell back to mapWallet's raw hash ordering, which has nothing to do
with time. Visible in a screenshot as orphaned 'Generated' rows with
dates jumping around non-chronologically.

Added time as a tiebreaker within equal depth (most recent first),
kept depth as the primary key exactly as before -- a resync
re-stamping many txs with the same time just means those particular
ties fall back to hash order again, no worse than the prior
behavior, but the common case (normal operation, not mid-resync) now
sorts correctly.
…ional auto-return

Participant mode retried a dead pool forever with no way out short of
manually switching modes. New: /fallbacktosolo=MINUTES (GUI: "Fall
back to solo mining after") — after the pool's been unreachable that
long, switch to solo automatically. 0/unset keeps the old behavior
(retry forever). /noautoreturn (GUI: unchecking "Automatically go
back to the pool") controls whether it then periodically retries the
original pool, or just stays solo until the user intervenes.

Settings are CLI-flag/GUI-session only, not persisted to wallet.dat —
same reasoning already established for nMineMode/strParticipantPool
in db.cpp's LoadWallet: a saved value would silently override
whatever was just requested on the command line, with no error.

Thread-safety was the real design constraint here, not the settings.
BitcoinMiner() is invoked once per thread launch (ThreadBitcoinMiner
doesn't loop), and the solo-mining hot loop's exit condition is only
`while (fGenerateBitcoins)` — it never re-checks nMineMode. Naively
flipping nMineMode out from under a running thread would leave it
mining solo forever regardless, while a second thread started
alongside it in participant mode — two mining threads at once. Both
transition directions instead follow the same shape: the thread
that's currently running is the one that decides to stop, spawns its
own successor (via the existing _beginthread(ThreadBitcoinMiner, ...)
call the GUI's Start Mining button already uses) with the new mode
already set, and only then returns, ending itself. No other thread is
ever running when the mode changes, in either direction.

The auto-return check lives at the very top of the solo loop's outer
iteration, before that iteration's RandomX VM is created — the one
place in the loop where returning early needs no cleanup at all.

g_fAutoFallenBackToSolo (whether the current solo session exists
*because of* an automatic fallback, vs. the user picking solo
deliberately) is cleared on any manual mode change from the GUI —
otherwise a stale flag from an old auto-fallback could later hijack a
solo session the user chose on purpose, switching it back to
participant mode against their explicit choice.

Compiled all three touched files for real against the actual headers
(main.cpp, gui.cpp, main_gui.cpp) before committing, given this
touches mining thread control flow directly -- clean across all
three.
Prompted by a maintainer's root-cause of Bitflash-sh#5 (the segfault
at block 1062): %I64d, an MSVC-only specifier inherited from the
original Bitcoin 0.1 codebase, desyncs vararg consumption on glibc --
the compiler had been warning about it the whole time, and -w (used
project-wide, here too) silently threw the warning away. We don't
have %I64d anywhere in our tree (checked exhaustively, zero hits
outside properly-guarded vendored ImGui code) -- but the lesson is
about the suppression, not that specific bug: -w hides exactly the
warning class that would have caught it, project-wide, permanently.

-Wformat -Wformat-security -Werror=format added back after -w on
both Makefiles, narrow enough to not fight the blanket suppression
everywhere else. Verified clean on Linux by actually compiling every
major source file (main/net/gui/main_gui/util/db/rpc/dashboard.cpp)
plus the vendored FTXUI source with these flags live -- zero hits.
Windows verified more narrowly: installed a real mingw-w64 cross
compiler in this sandbox and confirmed %lld (what this codebase
actually uses throughout) doesn't false-positive under it, but a
full cross-compile of the real Windows build wasn't feasible here
(would need a mingw-built OpenSSL/websockets/curl chain this sandbox
doesn't have) -- lower confidence than the Linux side, worth watching
the first real Windows CI run after this lands.
…r no-op fix)

My previous attempt (adding -Wformat -Werror=format after -w in
CXXFLAGS) did nothing. Verified by direct test: a genuine format bug
(%d given a long long) still compiles clean with -w present in the
command line, regardless of where -Wformat/-Werror=format are placed
relative to it. -w overrides them unconditionally.

Ported and adapted Bitflash-sh#44 properly instead, which diagnosed
this exact problem against the same root cause (the Bitflash-sh#5 segfault) and
fixed it correctly: -w removed entirely, replaced with -Wno-
deprecated-declarations/-Wno-write-strings/-Wno-narrowing/-Wno-
unused-result (the categories this tree's own code legitimately
triggers) plus -Wformat -Wformat-security -Werror=format. Also
required BF_FORMAT annotations on strprintf/my_snprintf/ErrorImpl/
OutputDebugStringF -- printf itself is #defined to the last of those,
and GCC only checks format strings for functions it knows are
printf-like, so without the attributes -Wformat had nothing to
inspect regardless of -w. gnu_printf archetype specifically: MinGW's
alternative, ms_printf, models legacy msvcrt and would reject %zu
(used here, supported by UCRT) while accepting %I64d (the specifier
that actually crashes on glibc) -- exactly backwards for what this
needs to catch. Collapsed PRId64/PRIu64/PRIx64 to one spelling
(dropping the MSVCRT branch that produced %I64d in the first place),
needed for both platforms to share one format archetype.

Adapted rather than copied where our fork has diverged: PrintBlockTree
doesn't exist here (removed earlier this session as dead code, so its
fixes in the upstream diff don't apply), and `error` here is a macro
over ErrorImpl (ours), not a plain function (theirs).

Removing -w surfaced real bugs beyond what the upstream PR's diff
covers, since this fork carries additional code: a SECP256K1_STATIC
macro-redefinition warning in nostr.cpp/btfaddr.cpp (guarded the same
way upstream did), and one more size_t-into-%d mismatch in util.cpp's
AddTimeData logging that wasn't in their tree.

Verified by actually compiling every source file in the tree with the
real flags (not just the ones this diff touches) -- main/net/gui/
main_gui/util/db/rpc/script/update/market/dashboard.cpp all clean.
nostr/btfaddr/compactblock/randomx_pow.cpp couldn't be verified this
same way here (their C dependencies -- secp256k1, libsodium, RandomX
-- aren't vendored in this sandbox, unrelated to this change), but
nostr.cpp and btfaddr.cpp got the one warning each that could be
checked without those headers (the macro redefinition, both fixed).
…itflash-sh#46)

Issue Bitflash-sh#40's first half: a wallet.dat copied out on its own doesn't
open anywhere else, even though it's byte-identical and holds the
same keys. Root cause chain, from the bottom:

_endthread() is pthread_exit() on POSIX, which glibc implements by
throwing abi::__forced_unwind through the stack -- not a real
exception, and catching it without rethrowing is a fatal error by
contract. CATCH_PRINT_EXCEPTION's bare catch(...) did exactly that,
so every Linux shutdown aborted mid-way rather than completing. That
mattered because Berkeley DB stamps every page with a log sequence
number tied to the environment that wrote it; DBFlush(true) is what
clears that (via lsn_reset) before the file is safe to move, and
DBFlush has been declared since 2009 and never actually called on
either shutdown path. So the wallet was never released from its
directory, and the one file anyone would think to back up was the
one file that couldn't travel.

Three fixes:
- Rethrow abi::__forced_unwind rather than swallowing it (util.h),
  and call the now-effective DBFlush(true) on both shutdown paths
  (headless and GUI) in main_gui.cpp.
- Adopt orphaned wallet.dat/blkindex.dat on open: a freshly created
  database/ environment beside pre-existing .dat files means those
  files came from elsewhere, so their sequence numbers get reset.
  Recovers wallets already stranded by older builds.
- BackupWallet(), reachable as /backupwallet=FILE: checkpoints,
  copies, clears the copy's sequence numbers. Bitcoin 0.1.0 had a
  backup command; this tree hasn't had one since the wxWidgets
  removal.

DBFlush's own lsn_reset call is now wrapped too (its txn_checkpoint
already was, from this fork's earlier BDB-exception fix) -- letting
it throw on the way out is how this whole chain started.

Adapted rather than copied: this fork's dead-code sweep earlier this
session correctly removed FileExists as unused at the time -- it has
a real caller again now (both the orphan-adoption check and
BackupWallet), restored scoped to db.cpp rather than re-exposed in
util.h, since that's the only file that needs it. DBFlush's
txn_checkpoint wrapping already existed here from a prior fix; only
lsn_reset needed adding.

Updated the README's backup guidance to point at /backupwallet as the
correct way to do this, rather than copying the data directory by
hand.

Still not addressed, and the larger half of Bitflash-sh#40: keys are generated
on demand rather than derived from a seed, so a backup is a snapshot
and coins paid to addresses created after it aren't in it. That's a
wallet-format change and its own piece of work, matching what
upstream's own PR description says about scope.

Verified by compiling every touched and dependent file for real
against the actual headers, with the format-checking flags from the
previous commit live -- clean throughout.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants