v0.6.23
NEWEST ENTRYYour node says which role it actually fills, and whether that helps. The status card could say a node was running, at the frontier, and advertising the archive service. It never said what the machine was actually doing for the network: checking blocks itself or following other people's signatures, whether a key it holds produces anything, whether any other node can reach it. Three things went wrong in that silence. On 3 September an operator offered a signing key; it was pinned and produced zero signatures for eleven days, because his node runs as a trusted mirror and a mirror consumes attestations rather than producing them. Nothing was broken, nothing was logged, and no screen could have said "this key signs nothing here". This project's own signer advertises the archive service while holding a key, which the engine clamps to the last sixteen blocks; on 13 September the explorer needed one signature from about 800 blocks back, every peer it asked refused, and it sat frozen for twenty-one hours. And a machine without a graphics card the engine accepts is launched in consensus mode, starts degraded, follows headers, advertises neither the consensus nor the archive service, and stalls below the Epoch A height while its operator believes it is helping.
A small card now reads the engine's own answers (getmatmultrustedstatus and
getnetworkinfo, both calls the app was already making) and says, one line
each: how this node validates, whether it holds a signing key and whether that
key can sign anything here, what history it serves, whether anyone has
connected to it, and where it stands on the chain, each with one plain
sentence on whether that helps other nodes. An engine that does not answer is
reported as unknown, never as "no": absence of an answer is exactly how the
pinned key went unnoticed. A node that has been up under half an hour with no
inbound connection is not yet told it is unreachable, the same grace the
"Helping the network" card already gives. The lines are decided and tested in
Rust; the screen only lays them out.
The engine moves to upstream's tagged v0.34.6. Upstream tagged 0.34.6 on
13 September at commit 3013c2c2, exactly one commit past the 9eb4e005
branch build that 0.6.18 through 0.6.22 have shipped. That restores the rule
this project set for itself when it shipped an untagged engine: ship no engine
bump until a tag exists that upstream itself calls clean AND both guards pass
on the box cutting the release. Both do, on the literal commit: the fork guard
finds the withdrawn stall-recovery height disabled on all five networks, and
the fleet guard finds a degraded consensus start allowed, the 1-of-1 mirror
refused, and the same two device classes in the sealed golden manifest as
before (NVIDIA sm_120, Apple M4-class).
What the one commit changes, read from the diff rather than from the release
notes: a peer that has only ever sent headers no longer counts as a source to
fetch a competing chain's block bodies from, the mining guard's deferred-reorg
window is kept armed while a reorg is pending, and SHA-256 becomes the only new
HTLC lock the wallet creates. No consensus rule moves; chainparams.cpp is
byte-identical to the previous pin, so the 203,000 bootstrap base and the
golden manifest are unchanged, and the manifest's seal re-verifies. CI built
the tag commit from a pristine tree on both platforms: Linux with
BUILD_GIT_DIRTY 0 and kernels for sm_75/86/89/100/120, Windows through the
mingw cross-compile and its regtest smoke on a real Windows runner. Both report
BTX daemon version v0.34.6.
The app installs it under the key v0.34.6-3013c2c2 rather than v0.34.6, and
that is the part a returning user actually receives. The install directory is
named after the key, and the app re-provisions an existing install only when
the key changes; every install since 0.6.18 already sits in a directory called
v0.34.6 holding the old build, so keeping the name would have shipped the
tag to fresh installs only, silently. The suffix is the commit's short hash, so
the directory says which engine is inside without running it. The binary still
reports v0.34.6, the staged package declares that in .btxd-version, and
provisioning verifies the binary against the declaration, exactly as the
v0.33.3-pr105b key did in 0.6.1. The chain in ~/.easybtx is untouched by
the swap.
A PC with no graphics card now follows the chain instead of stopping at it,
and can serve the explorer's history. Since 0.6.15 every Windows and Linux
node has started in consensus mode. On a machine with a capable NVIDIA card
that is right: the engine measures the card at startup and the node validates
every block itself, as this project's RTX 3060 has since 1 September. On a
machine with no card it was a node that helped nobody. The engine starts it
degraded, it follows headers, it stops below block 185,000 where the new proof
of work begins, and it can never advertise the archive service, because the
engine (init.cpp:2662-2673, identical at our pinned commit 9eb4e005, at
the v0.34.6 tag and on the unreleased 0.34.7 branch) grants that service only
to a trusted mirror or to a signer whose card is ready. Its owner believed it
was helping.
The role the network is short of needs neither a card nor a key. A node with NO signing key serves attestation history with no window limit at all, where a signer is clamped to the last sixteen blocks (0.6.22 said this about our own signer). On 13 September the explorer sat frozen for twenty-one hours for want of one historical signature that no peer it asked would serve. A keyless mirror would have answered.
So the app now looks for the NVIDIA driver before it starts the node. A machine
that has it (nvcuda.dll on Windows, libcuda.so.1 on Linux, both measured on
this project's 3060 box) is launched in consensus mode exactly as before. A
machine that does not is launched as a trusted mirror: the same three pinned
signer keys the Mac fallback already uses, threshold one, the
-allowsinglekeytrustedmirror=1 the 0.34 engine requires, and the mode stated
on the command line rather than left to the engine's default, for the reason
0.6.16 learned in five seconds. It follows the signed chain, and when "Serve
signed confirmations" is on, which the engine turns on by default in this
mode, it advertises the archive service and serves history at any height. The
status card calls it "Mirror" and says what is being trusted.
The trade, in the words of the decision record
(docs/decisions/2026-09-15-keyless-cpu-hosts-are-trusted-mirrors.md): at
threshold 1 every pinned key is a full authority alone, so one stolen signing
key could make these nodes accept MatMul-invalid blocks; btxd itself warns
about this at init. Mirrors also freeze when signers go quiet (2026-09-05/06
incidents) rather than validating on their own. A node that stalled for certain
and served nobody becomes one that follows on signatures, can serve, and
freezes if the signers do. The 31 August reasoning that put every PC in
consensus mode rested on an attestation supply that had measured dead in
mid-August; this project's validator has signed since 1 September and
LuckyPool's node carries attestations too, so that premise no longer holds.
What this does not fix, said so nobody expects it to: a PC whose NVIDIA card
has the driver but fails the engine's startup check (an older or weaker card)
still reads as a CUDA machine, still starts in consensus mode, and still stops
below block 185,000, with the card reading "Stopped" as before. The engine's
own verdict after start is the signal that would route such a machine to the
mirror, and using it is the next step, not this one. EASYBTX_NODE_TRUSTED_MIRROR=1
in the environment puts it on the mirror by hand meanwhile, and =0 on a
machine with no card is the one-flag return to the 0.6.15 through 0.6.22
posture.
Verified offline on this box against the shipped 0.34.6 engine, with exactly
the flags a card-less PC receives and no peers: the engine reaches "Done
loading" in two seconds, getmatmultrustedstatus reads trusted_mirror true,
local_signer false, serves_attestations true, three signers at threshold
one, and the node advertises the trusted-mirror and attestation-archive
service bits with the consensus bit clear; switching serving off drops the
archive bit and nothing else. Macs are untouched: an Apple Silicon Mac still
passes no mode flag and validates for itself, and the M5 fallback path is
unchanged. A PC with a capable card is untouched.
Every update check now says what it did, on screen and on disk. The app checks for a new version on launch and every six hours, and until now the result of that check went to exactly one place: the sentence beside the "Check now" button, and only when somebody had pressed it. An automatic check that found nothing said nothing. An automatic check that failed said nothing either, by design, because a six-hourly banner about being offline would have been noise. Measured 2026-09-15: this project's own signer box ran 0.6.21 for eight hours after the feed served 0.6.22, with two six-hourly checks due in that window, and nothing on the machine could say whether those checks ran, failed, or ran and found nothing. The release recipe has a step called "observe a real upgrade", and there was nothing to observe it with.
Two things now hold the answer. Every check writes one line to
update-check.log in the data folder, beside setup.log and through the same
private file helper: when, which of five outcomes (no-update,
check-failed, found, install-failed, installed), and a short detail
that says whether the check was automatic or pressed, the version offered, and
the error text when there was one. The file is kept under 64 KiB, so a node
that runs for years does not grow it forever. And the last of those lines is
kept in the app's settings file, from which Settings renders a permanent line
under the button: "Last check: today 14:03 — you're on the latest version",
or "couldn't check", or "v0.6.23 installed". It is rendered from the persisted
value, so it is there at launch before any check has run this session, and it
reads the same after the relaunch an install causes. The five words are one
list on both sides of the app, and a test keeps them equal; the Rust side
refuses a sixth unwritten. Writing the line can never change what the updater
does: a failure to record is a console warning, and the one place the app is
about to end its own process, the restart after an install, waits at most two
seconds for the line to land first.
The six-hourly check itself has also moved, out of the window and into the app's Rust side. Until now the recheck was a JavaScript timer inside the webview, and the hypothesis behind those eight silent hours, held as a hypothesis because the only measurement is the two checks that fell due and left no trace while the launch-time check on the same box did work right after a relaunch, is that WebKitGTK slows or suspends the timers of a hidden window, and this app spends its life hidden in the menu bar. The recheck now runs on a timer nothing throttles, two minutes after launch and every six hours after, through the same updater and into the same log with the same five words; the check at launch and the "Check now" button are unchanged. The log is what settles the question on the next release: six-hourly lines from a node left in the tray confirm it, and their absence points elsewhere.