| Age | Commit message (Collapse) | Author |
|
The two dependency clones were `git clone --depth 1 <url>` with no
revision, which resolves to whatever the default branch points at the
moment the build happens to run.
For RandomX that is the proof of work. Its master is not frozen -- v2.0
changed the algorithm in March -- so two builds of the same Bitflash
commit, made weeks apart, can hash the same header differently. That is
not a supply-chain worry, it is a chain split delivered as a build
artifact, with no version recorded anywhere to explain it afterwards. For
libsecp256k1 it is signature verification, the rule deciding who may
spend.
Both now fetch a full commit id, which is the only form here that
verifies what it got, since git resolves the object by its own hash. The
RandomX pin is the commit this tree has been building and syncing
against, so today's behaviour is unchanged; secp256k1 is pinned to the
v0.8.0 release commit.
appimagetool came from the "continuous" tag, which upstream moves in
place on every push, fetched over the network and executed unverified.
Pinned to a numbered release and checked by digest before it is allowed
to run.
It also ran from /tmp on a fixed name, guarded by `[ ! -x
/tmp/appimagetool ]` -- an executable already sitting there is taken as
the tool, so any local account could leave one and have it run as
whoever built. The AppDir had the same shape one step later, through
symlinks on the rm -rf. Both moved under the build tree, which the build
already owns, and cleaned by `make clean`.
(cherry picked from commit 664aebca08d17a9dbae12c25f21cd8ef2abe7aa6)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
From the first of two wedges captured on the pool. It is a different
proximate fault from the second one, and this is the part that turns a
single bad socket into a node-wide outage.
A failed select() zeroes both descriptor sets, so no peer is serviced on
that pass. That is correct as far as it goes. What was missing is that
nothing stopped the offending descriptor coming straight back:
- the invalid socket was only marked fDisconnect, and a node is not
removed from vNodes until it has drained or DISCONNECT_DRAIN_SECS has
elapsed -- and it cannot drain, because its socket is gone;
- the select set was rebuilt from vNodes with no check, so the same dead
descriptor went back in on the very next pass and failed select()
again.
Measured in the capture: 1,027 consecutive 'its socket is no longer valid'
passes across 61 seconds -- the full drain timeout -- one bad socket, and
for the whole of it the node read nothing from anybody. The peer that
caused it had 57 bytes unsent, so it took the slow path out.
Now the descriptor is forgotten as soon as getsockopt says it is not ours:
select() succeeds again on the next pass and every other peer is served
normally while the dead node ages out as before.
Deliberately forgotten rather than closed. getsockopt has just said this
descriptor is no longer ours; calling close() on it is precisely how an
unrelated socket gets closed instead, which is the fault fixed in e742d0b.
(cherry picked from commit f14c513b4e39d6203b73584f6dc01acbb5a9546e)
|
|
Found auditing the previous commit for the push. Timing from the first
rewind would drop a peer whose message is large and merely slow, which
would be a fault of our own making rather than one we fixed.
Restart the clock whenever the buffer has grown by even one byte since the
last look. A transfer still making progress is never dropped however long
it takes; only one that has stopped growing while the node goes on
expecting more is, which is the actual failure.
(cherry picked from commit 5dddb26bae02013ce87719b11953aadf0bfa2594)
|
|
THIS IS THE WEDGE, and the note asking for this fix has been in the file
since Bitcoin 0.1: 'need a mechanism to give up waiting for overlong
message size error'.
ProcessMessages reads a header, finds the body has not all arrived, pushes
the header back onto the front of vRecv and breaks. Correct for a message
still in flight. For one that never completes it is permanent: every later
pass re-reads the same header and breaks in the same place, so that peer is
never parsed again for the life of the process -- no blocks, no inv, not
even its version message.
Nothing rescues it:
- a header may claim up to 256 MB (0x10000000) and still pass
CMessageHeader::IsValid(), so a garbage or truncated header is accepted
and then waited on forever;
- nLastRecv is refreshed by any byte that arrives (net.cpp), whether or
not it parses, so the inactivity timeout that drops a silent peer never
fires on a peer that is merely unreadable;
- the socket stays ESTABLISHED and select() keeps offering it, so nothing
at the socket layer notices either.
It also explains the signature that made no sense. A peer stuck before its
version message is parsed keeps nStartingHeight == -1, so a node with a full
vNodes still reports GetPeerMedianHeight() == -1 -- which is exactly what
was observed, and exactly what disabled the pool's behind-the-network gate
and let it fork.
Evidence, from two wedges captured on the pool by the watchdog:
MESSAGE-BREAK 2 ran 2014 times in the deaf window against 607 in the healthy
window before it, while blocks received went to zero and the node kept
processing miner traffic normally throughout.
Now: a peer that announces a body and does not deliver it within two minutes
is dropped, and says so in the log unconditionally. Two minutes is far
beyond any legitimate header-to-body gap and well inside
BTF_RECV_TIMEOUT_SECS.
(cherry picked from commit ca1e99288e03ac7beb2b97cf615a3255b4635d95)
|
|
THE WEDGE.
CNode::Disconnect() closed hSocket and left the member holding the stale
number. ~CNode's only guard is 'hSocket != INVALID_SOCKET' and nothing in
the tree ever assigned that sentinel, so the destructor closed the same
number a second time.
The two closes are five minutes apart by design: ThreadSocketHandler2
disconnects a node, sets nReleaseTime to GetTime() + 5*60, and parks it in
vNodesDisconnected; GetRefCount() reports it busy until that expires, so
'delete pnode' -- and the second close -- cannot run before then.
Linux hands out the lowest free descriptor. Measured on the pool: 202 open
descriptors with a high-water mark of 216, so a freed number is reissued
within seconds -- to a Nostr relay TLS socket, a rendezvous socket, a
tunnel end, or a miner connection. Five minutes later the destructor
closes it. The victim gets no error and no log line. It simply stops.
That is the whole signature, including the part that never made sense:
.btf block delivery and Nostr descriptor lookup share no lock, no queue
and no thread, so they should not fail together. They share the file
descriptor table, and a stray close is drawn from all of it.
It also explains why #78 did not help and may have hurt. #78 drops stuck
nodes -- it fired 41 times in one window -- and every drop is another
Disconnect(), which is another delayed close armed against an innocent
socket.
Second bug, same symptom, different subsystem: the pooled relay sockets.
ResolveDescriptor returned the instant a descriptor verified, leaving the
relay's EOSE unread on a socket that goes straight back into g_relayPool,
so the next caller read our leftovers as its own answer -- permanently one
message out of step, and no code path anywhere inspected the subscription
id that would have caught it. Worse, BtfResolve set lease.fOk = true on
every failure path including a dead RecvText, labelling it 'exchange
completed, just no match'. fOk = false is the only thing that ever closes
a pooled socket or clears fConnected, so a relay connection that died was
pinned in the pool, reported healthy, and reused for the life of the
process. That is 'no Nostr descriptor found' for a descriptor that nak can
see, cleared only by a restart.
Now: read to EOSE before returning, match the subscription id, and keep a
pooled socket only when the relay actually finished the conversation.
(cherry picked from commit ed4640c306cc46dbdbd92cb6dc55e7528651269c)
|
|
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)
|
|
The sync gate compared the tip timestamp against 30 minutes. That cannot
distinguish "we are not synced" from "the network is slow", and it failed
in both directions on the same day.
A pool mining its own fork keeps its tip fresh, so the check passed for
the whole of heights 3966-4085 -- 120 consecutive blocks, all orphaned,
5890.50 BTF of payouts voided, and nothing in the log to say so. Then,
once the pool stopped finding blocks, the same check refused to issue
jobs against a tip that was the network's own best. The pool is ~90% of
the hashrate, so nobody else could move the chain either: stale tip, no
jobs; no jobs, stale tip.
Gate on the height peers announce in their version message instead.
Median, so one peer cannot stall us with an absurd claim, and a block of
slack to match WarnIfBehind. Peers predating the announcement report -1
and are ignored; with no reports at all we mine our best chain, which is
what the node does anyway without a pool attached.
(cherry picked from commit 4e76e34f37a82c0885f75db7dfb9e49db410171d)
|
|
The exec queue was memory-only, and FlushMaturePayouts erased a matured entry
from pending_payouts.json *before* handing it over. Any restart in that window
lost those payments with no record that anyone was owed anything. It was not
hypothetical: a container restart on 2026-07-30 silently dropped a backlog of
~1,156 BTF, and there was nothing left to reconstruct it from.
Re-queueing blindly would have double-paid whoever had already been sent to,
so the payment needs to be idempotent rather than merely persistent.
In-flight work is now a persisted list of per-payment records
(paying_payouts.json), one per (block, address) -- the granularity SendMoney
works at, and therefore the granularity at which a payment can be half-done
when the process dies. Records are written, via a temp file and rename so a
crash cannot truncate them, *before* the ledger entry is erased, so the debt
is always described by at least one of the two files. Each attempt updates the
record (retries, deferrals, backoff); settling it removes the record and
rewrites the file, which is what stops a restart from paying twice.
On startup ReconcilePayouts runs before the payout thread:
- a record whose payment the wallet already shows -- matched on the payout
comment plus the exact amount and destination -- is dropped, not resent.
SendMoney commits the transaction to the wallet before it returns, so a
crash immediately after sending is recoverable.
- a ledger entry that has already been exploded into records is dropped,
which is the crash-in-the-window case where both files describe one debt.
The wallet check is deliberately biased towards "already paid": skipping a
payment is visible in the log and recoverable, paying twice is neither.
Verified live: sampling paying_payouts.json during a payout burst caught 3
records / 18.56 BTF mid-flight, cleared once paid, and the paid lines now
carry the txid.
(cherry picked from commit d0248f2e65dbe745f2e9b1d09e1f53a4354d3640)
|
|
Two fixes.
uint256's default constructor is empty (uint256.h:514), so the blockHash
field added in 6963862 was uninitialised garbage on every PendingPayout
built without an explicit hash -- which is every entry loaded from a ledger
written before that commit. Those entries were meant to be grandfathered
past the orphan check on a zero hash; instead each carried a random value
that matched no block in mapBlockIndex, and the first maturity sweep after
deploy voided the whole loaded ledger: 100 entries, 4950 BTF. Only one
"was orphaned -- voiding" line reached the log because LogPrint collapsed
the other 99 as duplicates, so the destruction was nearly silent. Nothing
had been paid at that point and the file was restored from a pre-deploy
backup, so no obligation was lost. Assigning 0 explicitly is all that was
missing.
Separately, four of the twelve relays are dead. Probed all twelve from two
independent networks (a residential line and the pool's VPS) over v4 and v6,
with DNS confirmed against 1.1.1.1:
relay.current.fyi, relay.nostr.bg -- NXDOMAIN, no A and no AAAA. The
domains are gone. (A getaddrinfo call on the VPS reports an address for
them, but that is the VPS's own IPv6 handed back by its local resolver,
not a real host.)
relay.nostr.band, nostr.fmt.wiz.biz -- DNS resolves and TCP connects, but
the TLS handshake never completes, from either network. Whether they are
down or refusing non-browser clients, this node cannot use them.
The remaining eight all complete a WebSocket upgrade. A dead relay is not
free: each holds a connect/retry slot and delays discovery for no added
redundancy.
(cherry picked from commit 144bc96f150d23507d57d29cb339f177dcd6b092)
|
|
A 280-block reorg (heights 3075-3354) orphaned 292 of the pool's 707 blocks
and destroyed 14,600 BTF of coinbase. The payout ledger did not notice: it
records only matureAtHeight, and a reorg replaces the block at a height
without changing the height, so ~14.4k BTF of obligations outlived the coin
that was supposed to fund them. The pool then paid fork-era miners out of
real-chain coin until the wallet was empty -- 17,827 BTF paid against 17,900
BTF of matured supply, leaving under 1.1 BTF spendable and every subsequent
payment failing.
Three changes, all in the payout path:
- Record the funding block's hash and height, persist both, and void an
obligation whose block is no longer in the main chain. Checked both at
maturity and again in the exec queue, since an entry can sit there for
hours across retries and the reorg can land at any point in that window.
Entries loaded from a file written before this are grandfathered rather
than voided on a missing field.
- Stop spending the retry budget on maturity. SelectCoins failing for want
of spendable coin is not an error, but it consumed one of 1440 retries
every 60s, so a payment the pool fully intended to honour would hit
GIVING UP 24h later and the miner would silently never be paid. Those
attempts now defer without incrementing retries, logged hourly rather
than every minute. GIVING UP now means what it says: 1440 real failures
against a block still on-chain with the money available.
- Schedule at COINBASE_SPENDABLE_DEPTH instead of COINBASE_MATURITY. At
depth 100 a block's own coinbase is 20 blocks short of spendable, so its
payout had to come from unrelated mature coin and the pool carried a
permanent ~19-block float. The "96% of 934 payments went through at depth
100" note that justified the shorter wait was measured while this node was
alone on a private fork paying itself. The extra 20 blocks also put the
maturity gate past the reorg that caused all of the above.
(cherry picked from commit 696386220a3d2fac88cb8045931392f4909e0985)
|
|
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)
|
|
Upstream #30 split rendezvous registration from waiting for a dial, so
RvServiceRegister now returns as soon as the relay lists the service and
no longer consumes the relay's 0x01 pairing byte. It updated its own
caller in net.cpp and left this one behind.
AcceptOneFn went straight from RvServiceRegister to BtfServiceWrap, so
BtfServiceWrap read that 0x01 as the first byte of the miner's 32-byte
ephemeral key and every byte after it was shifted by one. The X25519
handshake has no confirmation step, so it completed against a garbage
shared secret, the tunnel was handed to the Stratum event loop, and
nothing failed loudly: the pool logged a miner arriving and then dropped
it as "never authorized", while the miner logged a pool that closed on it
with gotSubscribeAck=0. Loopback miners were unaffected, which is exactly
what made it look like a relay problem.
Block in RvServiceWaitPaired before wrapping, and close and re-register if
the relay drops us first.
test_rvaccept.cpp covers both shapes against a real bitflashd on loopback:
the pre-fix order establishes a tunnel that carries no bytes, the fixed
order round-trips. Also corrects the btftunnel.h contract comment, which
still described the pre-#30 behaviour and is what the old code was written
against -- upstream's own test_relayproc.cpp, test_btftunnel.cpp and
test_btfrv.cpp are still on that stale contract and fail the same way.
(cherry picked from commit 3e3a36b2bf1e6ffb4861507089b743688619377f)
|
|
SSL_CTX_new leaves verification at SSL_VERIFY_NONE and loads no trust
store, and nothing here ever changed that. SNI was set, a certificate was
received, and it was then ignored: every wss:// relay connection this node
has ever made would have completed against an attacker's certificate.
Signed events limit the damage -- a MITM cannot forge an announcement
attributable to our key -- but it can drop the rendezvous registration so
miners never find the pool, censor subscriptions, or answer them with
arbitrary content, which is the input the allocation bounds exist to
contain.
Load the system trust store, require SSL_VERIFY_PEER, and bind the check
to the host we dialed with SSL_set1_host, since chain validation alone
would accept any certificate a trusted CA ever issued for any name. Floor
the protocol at TLS 1.2. Verification failures report the specific X509
reason rather than a generic handshake error, and a missing CA store is a
hard failure instead of a silent downgrade to trusting everything.
(cherry picked from commit 6020740e68463deb1cc3c5b477372fcd1a245adb)
|
|
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)
|
|
Descriptor (38501) still published so miners can resolve the pool;
only the public pool listing event (38503) is suppressed.
(cherry picked from commit 17bbdae1ec97945c37c1d5b2486e5c01a4d7ad87)
|
|
A pool that just found its first blocks has zero mature balance until
100 blocks later; orphaned coinbases (observed: reorgs orphaned our
blocks 2651-2655) make that worse. 30 one-minute retries forfeited
payments that would have succeeded hours later.
(cherry picked from commit 49e80023ca23baee8a4b80cfb891e7a358dc39ba)
|
|
A pool started with /rvrelay=ONE still spawned an accept thread for every
Nostr-discovered relay (BtfAllRelays always merged g_discoveredRelays), and
each thread optimistically overwrote the advertised meeting node before
registering -- so a discovered Tailscale-only relay (a mesh host)
flapped the published .btf descriptor and public miners could not connect.
fBtfRelayPinned (set only when /rvrelay parses) makes BtfAllRelays return
exactly the pinned list. Also validate the /rvrelay value at parse time:
with the pin in place no discovered relay can mask a typo.
(cherry picked from commit 0a4d2b02fa5382c5e788b5f0e2f8a844695dec47)
|
|
- 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)
|
|
docs/ has held the README's images since the fair-launch release. I
added doc/ yesterday for the derivation spec without looking, and #108
then correctly used docs/ -- leaving the repository with both, one
letter apart, which is the kind of thing that quietly costs someone ten
minutes a year forever.
The mistake was mine, so the move is mine: derivation.md and
verify-derivation.py go to docs/, and the two README links follow.
No content changes. The verifier still reproduces the wallet from its
new path, which is the only thing here that can actually break.
|
|
Co-authored-by: Gilcimar Duarte <ti@gpm.infra>
|
|
Every fatal startup path wrote to stderr and to printf, and on Windows
neither reaches anybody. printf is remapped to OutputDebugStringF, so it
lands in debug.log inside the data directory. A -mwindows binary started
by double-clicking has no stderr at all, and AttachTerminal() cannot
help: it attaches to a parent console, which a double-clicked exe does
not have.
So Bitflash exited silently. Someone whose wallet.dat needs a newer
build -- exactly what the wallet format guard was added to tell them --
double-clicked and watched nothing happen. The guard was correct and
mute, which is the failure it was written to prevent.
Fatal startup errors now go through one reporter: debug.log as before,
stderr as before, and a message box when there is a person in front of
the screen. Applied to the three that mean "your data will not open":
the block index failing to load, the block index throwing, and
wallet.dat failing or throwing.
The box is deliberately not shown for a headless node. Those run under
scheduled tasks and services, where a modal dialog waits forever for a
click nobody is there to make -- a hung process instead of an exit code.
Headless keeps stderr and the log.
LoadWallet() now also reports why it refused, in a sentence, instead of
only logging it. The unsupported-format case is the one that has to be
understood by whoever reads it, and "LoadWallet failed" is not that.
Verified by breaking a throwaway wallet on purpose and starting both
ways. Headless: exit code 1, no dialog, no process left behind. Windowed
with no console: a box naming the file, the Berkeley DB error, the usual
cause, and where to look for more.
|
|
(#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>
|
|
The twelve words were only as good as this binary staying alive. The
derivation path, the address version byte and the public-key encoding
existed nowhere outside src/, so anyone holding a phrase and no working
Bitflash had the keys and no way to find them.
doc/derivation.md states all of it: BIP-39 to seed, BIP-32 to children,
both layouts this wallet has used (legacy m/i' and BIP-44
m/44'/4346950'/0'/{0,1}/i), secp256k1 with UNCOMPRESSED public keys, and
Base58Check with version byte 25.
The uncompressed part is the trap worth naming. Hashing the same key in
compressed form yields a different, perfectly valid-looking address that
holds nothing, and nothing in the source says out loud which one is
used -- it falls out of OpenSSL's default in code inherited from 0.1.0.
doc/verify-derivation.py implements the document from scratch, sharing
no code with this project and importing nothing outside the standard
library, then checks it against addresses the wallet actually produced.
Both tables were generated by a binary first: the legacy one by the
published v1.2.15, which knows only that layout, and the BIP-44 one by
the current tree. The script reproduces all eight.
That is the point of shipping the checker rather than only the prose: if
the two ever drift, the script fails and says which side is wrong.
|
|
The right icon has been in the exe the whole time. GLFW never found it.
GLFW registers its own window class and fills hIcon with
LoadImageW(module, L"GLFW_ICON", ...), falling back to IDI_APPLICATION
when that exact resource name is missing. Ours was named "bitflash", so
the lookup failed and every window this program has opened wore the
generic Windows icon -- in the title bar and in the taskbar -- while the
correct artwork sat unused a few kilobytes away.
One line, verified by looking at it: rebuilt, ran the window, and zoomed
in on both places the user pointed at. The title bar and the taskbar
button now show the green BF coin. Checked the shell path too, since it
resolves icons differently: ExtractAssociatedIcon on the new exe still
returns the coin, so renaming the resource did not cost the file icon in
Explorer.
|
|
* 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>
|
|
|
|
CmdSendTo() has been in walletcmd.cpp since #115 and nothing declared
it or called it. The flag was dead: `bitflash -sendto=ADDRESS,AMOUNT`
started a node and said nothing, because an unrecognised argument is
not an error here.
The v1.2.14 notes announce it under "A headless node can finally
spend", and the headless Linux binary is exactly the one that does not
have it -- `-help` there lists /newaddress and stops. The published
Windows binary does have it, because it was built from a working tree
that carried these two files uncommitted. So the artifacts disagree
with each other and neither matches the tag.
This adds only what was missing: the declaration in walletcmd.h, the
dispatch in main(), and the -help line. No behaviour change to
CmdSendTo itself.
Checked on a build of this branch, against a throwaway data directory:
-help lists /sendto=ADDRESS,AMOUNT
-sendto=NOTANADDRESS,1 "Not a valid address", exit 1
-sendto=<valid>,-5 "Not a valid amount", exit 1
-sendto=<valid>,999 refused on an empty wallet, exit 1
All five self-tests still pass.
|
|
Co-authored-by: Gilcimar Duarte <ti@gpm.infra>
|
|
Creating a recovery phrase from the window aborted with an ImGui
assertion on the success path:
Expression: (g.DisabledStackSize > 0) && "Calling EndDisabled() too many times!"
File: imgui/imgui.cpp Line: 8435
The Finish button is wrapped in BeginDisabled/EndDisabled guarded by
!g_phraseWrittenDown, and its own click handler sets g_phraseWrittenDown
to false. So the pair was opened while the checkbox was ticked and
closed after the handler had unticked it: BeginDisabled was never
called, EndDisabled was, and ImGui asserted on the unmatched close.
It fires on the only path that installs a seed -- tick the box, press
Finish -- so every phrase created from the window has hit it since the
feature shipped in 1.2.12. Three releases carried it because nothing
tests the window.
Reproduced on the published 1.2.14 binary and confirmed fixed on this
build, driving the real window: Wallet Safety -> Create recovery phrase
-> tick -> Finish. The old build raises the assertion; the fixed build
closes the dialog, reports "This wallet has a recovery phrase", and the
address changes to a derived one.
The seed itself was already written before the assertion, so the wallet
was never left half-installed -- and ImGui's IM_ASSERT_USER_ERROR_RET
returns before touching its state, so answering Ignore was safe. Abort
was not: it kills the process with no DBFlush.
The restore dialog next door already does this correctly, copying the
condition into a local first. This makes the create dialog match.
|
|
A block had no size limit, spending could move coins onto a key your recovery phrase cannot reproduce, and one peer could hold up the whole node with a message it never finished sending. All three are fixed here, and all three were found by auditing the tree rather than by anyone hitting them.
## Blocks now have a size limit — and this one is consensus
`CheckBlock()` measured a block against `MAX_SIZE`, the serializer's generic 32 MB guard. There was no `MAX_BLOCK_SIZE` anywhere in the tree: Bitcoin's 1 MB cap arrived in 0.3.x, after the 0.1.0 this project came from, so the inheritance was simply missing. A miner could produce a 32 MB block and every node would download, verify and store it.
The cap is now **1 MB**, chosen against the chain rather than tradition. Reading all 5709 blocks out of `blk0001.dat`:
| | |
|---|---|
| largest block ever mined | 34,290 bytes |
| median | 216 bytes |
| p99 | 14,451 bytes |
1 MB leaves **29× of headroom** over the largest block this chain has ever carried, so nothing existing is rejected. A node built with the rule then synced genesis → 5706 on the live network with zero rejections, which is the same test that once caught BIP30 rejecting a real block at height 859.
**Because it is consensus, it matters that you upgrade.** Until most nodes have it, a block above 1 MB would be accepted by old nodes and rejected by new ones — which is how a chain splits. Nobody is mining such blocks today; the gap is worth closing before somebody does.
## Spending could put your change on a key the phrase cannot recover
When a wallet spends, the leftover goes back to you as *change*. This tree sent change to **the same key as one of the coins being spent** — 0.1.0 behaviour, and a privacy problem long tracked as #23.
With recovery phrases it became a way to lose money. If the coin being spent sat on a key created *before* the seed — the wallet's original default address, an imported key, anything older than `-newphrase` — then the change went back to that same pre-seed key. So spending quietly moved your balance onto a key the twelve words do not reproduce, in a wallet you believed the words covered.
Change now comes from a newly derived, phrase-backed key. **If that derivation fails, the transaction is refused rather than created**, because a failed send is recoverable and unrecoverable change is not. Wallets with no seed keep the old behaviour exactly.
Restore also now scans past the pre-generated key-pool window before accepting an empty batch as the end, so a restore cannot stop short while used addresses sit just beyond it.
## One peer could hold up the whole node
`ProcessMessages` runs in a single thread that serves every peer. When a message header announced more bytes than had arrived, the old code consumed the header, inserted it back at the *front* of the receive buffer — an O(n) memmove on a buffer that could hold 256 MB — and then slept 100 ms **inside that shared thread**. A peer that announced a large message and dripped bytes could make every pass pay both costs, forever, while looking perfectly active to the inactivity reaper.
The parser now reads the header from a copy without consuming it, never sleeps in the shared thread, and disconnects a peer that sits on an incomplete message for two minutes. The wire cap drops from 256 MB to `MAX_SIZE`.
## The rendezvous holds more than one registration
A node's registration at a rendezvous relay was consumed by the first caller, and registering again *replaced* the previous one — so a node was reachable to exactly one peer at a time and unreachable for the whole gap between being paired and registering again. Measured on a live node: descriptors resolved 53 times out of 54, its own registrations paired 11 out of 12, and **half of every outbound dial failed**.
All three relays now keep a queue per node and pair with the oldest registration still alive. **This is already deployed** — you get it without upgrading. The matching node-side change, which parks spare registrations, is deliberately **not** in this release: it measured worse in the field than the code it replaced and will not ship until that is explained.
## Restoring from a phrase now looks far enough
Found by proving the change fix above with real coin, and caused by it.
Restoring derived forward in batches of a hundred and stopped at the first batch that turned up nothing. The wallet digs a wider gap than that by itself: a restore leaves the derivation counter at the depth it scanned, the key pool derives a hundred more, and change takes the index after those. So the address used *after* a restore can sit two hundred indices past the last one used before it, with nothing in between.
On a real wallet: receive, restore, spend once — the outputs land at indices 201 and 302. Restoring again reported
```
Restored: 1 transaction(s) across 201 derived addresses
```
and the spend was invisible, both the payment and the change. The money was derived and covered by the phrase; the search stopped short of it. Three quiet batches instead of one, and the same wallet reports `2 transaction(s) across 601 derived addresses`.
Change returning to an input's key used to hide this, because that key sat at a low index the scan had already passed. Fixing recoverability moved change out to where the search stopped looking, so the two had to be fixed together.
The gap is self-inflicted and grows with each restore, so a long enough history can still outrun a fixed number of quiet batches. `-restoredepth=N` looks as far as you ask. The structural fix is tracked separately.
## A headless node can finally spend
`SendMoney()` has been in this tree since 0.1.0 and only the window ever called it, so a node without a screen could be paid and could never pay — it could hold a balance with no way to move it. Found while trying to prove the change fix, which needs a spend.
```
bitflash -sendto=ADDRESS,AMOUNT
```
Refuses an address that does not decode, an amount that is zero, negative or not a number, and a wallet whose chain is not loaded — all before touching the wallet. A transaction created while the node is stopped is not broadcast until the node starts.
## Also
- `-help` printed nothing at all on Windows. This tree remaps `printf` to the debug-log writer, and a `-mwindows` binary has no console, so the one command whose job is to tell you something returned 0 in silence.
- The README described a program from several releases ago — deterministic restore listed as an open issue, mining mode described as forgotten between restarts, and every flag written in the slash form that MSYS2 rewrites into a path.
- Diagnostics now report why a socket close failed, per error code and per site, and flag a handle handed back while some part of the program still claims it.
## Downloads
| File | Platform | Notes |
|------|----------|-------|
| `Bitflash-1.2.14-windows.zip` | Windows x86_64 | Extract and run |
| `Bitflash-1.2.14-x86_64.AppImage` | Linux x86_64 | Desktop/GUI — `chmod +x` and run |
| `bitflash-node-1.2.14-x86_64` | Linux x86_64 | Headless server node, no libGL or FUSE |
Verify with `sha256sum -c SHA256SUMS`.
Everything in [v1.2.13](https://github.com/Bitflash-sh/bitflash/releases/tag/v1.2.13) is included.
|
|
Restoring from a phrase gave up after the first batch of a hundred addresses that turned up nothing, and the wallet digs a wider gap than that by itself: a restore leaves nHDNext at the depth it scanned, the key pool derives KEYPOOL_SIZE more, and change takes the index after those. Measured on a real wallet with real coin -- restore, spend once, and the outputs land at indices 201 and 302; restoring again found only the first transaction and reported the spend as gone. With -restoredepth=500 the same wallet found both, which is what said this was reach and not loss.
Three quiet batches instead of one: 2 transaction(s) across 601 derived addresses on the same wallet, chain and phrase.
Worse after #109, not before it: change used to return to an input's key, a low index the scan had already passed. Making change recoverable moved it out to where the scan stopped looking.
Does not fix the gap growing with every restore -- pulling nHDNext back to the last used index is the structural answer and needs its own testing.
|
|
#113 added three accept threads and called the extras harmless against an un-upgraded relay. They are not: an old relay closes the previous registration whenever the same node registers again, so the threads evict each other in a loop. Measured on the live network -- the node whose registrations landed on the un-upgraded New Jersey relay made 189 registrations in fifteen minutes with zero pairings, unreachable through it and hammering it, while the node on the upgraded relay made 9 with 7 pairings. One relay of three is upgraded, so this would hit two thirds of every node's registrations.
The relay half of #113 stays; it is compatible in both directions and is the prerequisite for raising this again, which the comment now states.
|
|
A registration was consumed by the first caller and registering again replaced the previous one, so a node was reachable to one peer at a time and unreachable for the gap between being paired and registering again. Measured on a live node: descriptors resolved 53/54, our own registrations paired 11/12, nothing paired and went silent -- and half of every outbound dial failed, 15 of 30 in eleven minutes.
The relay keeps a queue per pubkey capped at four, pairs with the oldest still-alive registration and discards the dead; the node runs three accept threads, each holding its own registration. The wire protocol does not change, so either side can be deployed first.
Bench with both relays built from source and waves of concurrent dialers: old+1 65%, new+1 58%, old+3 68%, new+3 98%. Each half alone lands within noise of doing nothing.
|
|
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.
|
|
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.
|
|
The deaf-node paragraph cited the bench A/B -- two nodes on one machine for half an hour. Both mining machines have since run 1.2.13 in production: zero spurious disconnections against the 741 the unpatched side logged, and 13 and 18 peers held where the unpatched node held 11, ten of them inbound. Also says what the pre-1.2.11 socket pile cost and what returns the ports.
|
|
The README still said deterministic restore was an open issue, told people the mining mode was not remembered between restarts, and wrote every flag in the slash form that MSYS2 turns into a path before the node sees it. Rewritten around the phrase and the file as two backups covering different keys, the -restorephrase note for anyone who made a phrase on 1.2.12, -rescan, the diagnostics report, and the two upgrade reasons measured this week.
Also: -help printed nothing at all. This tree remaps printf to OutputDebugStringF, so the usage page went into debug.log, and a -mwindows binary has no console to write to either way -- the one command whose job is to tell you something returned 0 and said nothing. It now takes the real printf and attaches the terminal. The help text was missing -restoredepth, -showderived and the wallet-hd self-test.
|
|
Two things a node could do wrong without ever saying so: close a peer's socket twice, and hand out addresses a recovery phrase cannot reproduce.
## If you created a recovery phrase on 1.2.12, do this first
Run, with the same twelve words:
```
Bitflash.exe -restorephrase="your twelve words here"
```
1.2.12 installed the seed but left the wallet's visible address as the old random key, and left the old random key pool in place. The window said a recovery phrase existed while the next address the wallet handed out — a mining payout included — could be one the words cannot reproduce. Restoring with the same phrase repairs it: the pool is cleared, and the default address becomes a derived one.
Coins already sitting on pre-phrase addresses stay where they are. They are still yours, still spendable, and still **not** covered by the words — which is why `wallet.dat` backups keep mattering. The wallet now names that address `Your Address (created before the recovery phrase)` instead of leaving two entries both called `Your Address` with only one of them recoverable.
Thanks to **@mtg0001**, who found this within hours of 1.2.12 shipping (#102).
## A peer's socket was closed twice, and the second close hit whoever had the number
`CNode::Disconnect()` closed the socket and left the handle number in the object; the destructor then closed that same number again. There is no assignment of `INVALID_SOCKET` to that field anywhere in the tree, so the guard meant to prevent this never fired.
A double close is not a harmless no-op. This node opens hundreds of sockets a minute, and Windows reissues a handle number as soon as its last close lands — so the second close takes down a connection that now belongs to something else entirely.
The same omission left a closed handle in the `select()` set, where it makes `select()` fail on every iteration for as long as that node is in the list. The node then spends its socket loop in an error path instead of reading its peers. **That is the deaf-node behaviour this project has been chasing since 1.2.7**, and the `dropping node: its socket is no longer valid` line in the log is the recovery path, not the disease.
Measured, two nodes on the same machine and the same network, stopped at matched socket volume:
| | sockets opened | failed closes | `no longer valid` | peers held |
|---|---|---|---|---|
| without the fix | 552 | 4 | **741** | 11 |
| **with the fix** | 547 | **0** | **0** | **21** |
Half the peers, on the same machine, at the same minute. All 741 drops were the same peer, over and over — one dead handle in the set is enough.
## The socket leak was already fixed, and the way it was being measured was wrong
Earlier notes, including 1.2.12's, said sockets were still leaking. They were not. Windows lists a wildcard-bound socket **twice** in `Get-NetTCPConnection` — once for the binding (`0.0.0.0:port`, state `Bound`) and once for the connection — and 35 of one node's 38 `Bound` rows shared a local port with one of its own established connections. Counting those rows makes the number climb whenever the node talks to more peers, which is why the fix that worked looked like it had done nothing.
Counting **orphans** instead — a `Bound` row whose port appears nowhere else in that process — measured on three machines the same evening:
| node | version | uptime | orphans |
|---|---|---|---|
| 32-core miner | 1.2.10 | 26 h | **1262**, still taking on 30–90 an hour |
| second miner | 1.2.11 | 4h40m | 3 |
| test bench | 1.2.12 | 2 min | 3 |
The leak was real and **1.2.11 ended it**. If you are still on 1.2.10 or earlier, that alone is worth the upgrade.
## Also in this release
- The diagnostics report now says why a close failed, per error code and per site, totals what the node believes it is holding, and counts a handle handed out while some part of the program still claims it (#98, #100, #101).
- Every header is listed in both makefiles (#99). `sockcount.h` was missing, so editing it rebuilt some objects and not others and left two layouts of the same shared struct in one binary — a warning in the middle of a build log and nothing else.
## Downloads
| File | Platform | Notes |
|------|----------|-------|
| `Bitflash-1.2.13-windows.zip` | Windows x86_64 | Extract and run |
| `Bitflash-1.2.13-x86_64.AppImage` | Linux x86_64 | Desktop/GUI — `chmod +x` and run |
| `bitflash-node-1.2.13-x86_64` | Linux x86_64 | Headless server node, no libGL or FUSE |
Verify with `sha256sum -c SHA256SUMS`.
Everything in [v1.2.12](https://github.com/Bitflash-sh/bitflash/releases/tag/v1.2.12) is included.
|
|
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.
|
|
-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.
|
|
CNode::Disconnect() closed the socket and left the handle number in the object; ~CNode() then closed it again with a raw closesocket() the accounting never saw. There is no assignment of INVALID_SOCKET to CNode::hSocket anywhere in this tree, so the destructor's guard never fired. Between the two closes the OS can reissue that number, and the second close then takes down somebody else's connection. The same omission left a closed handle in the select() set, which fails every iteration and drives the recovery path that prints 'dropping node: its socket is no longer valid'. Measured A/B on the real network at matched socket volume: unpatched 552 sockets, 4 closes failing with 10038, 741 invalid-socket drops (all one peer), 11 peers held; patched 547 sockets, 0, 0, and 21 peers held. No protocol change.
|
|
A live node reported one close failing with 10038 (WSAENOTSOCK) at the tunnel's pump end. It leaks nothing; what it says is that a site's bookkeeping and its sockets disagree about ownership -- and Windows reissues a handle number only after its last close, so a site whose bookkeeping outlives its socket will eventually close a number that belongs to a live connection. BtfSocketTag now counts a handle arriving while another site still claims it, which names the site to go and read.
|
|
Windows lists a wildcard-bound socket twice in MSFT_NetTCPConnection, so counting Bound rows makes the number climb whenever the node talks to more peers -- 35 of a node's 38 Bound rows shared a local port with one of its own established connections. Counting orphans instead: a 1.2.10 node at 26h held 1262 and was still taking on 30-90 an hour; nodes on 1.2.11 and 1.2.12 held 3, transient. The leak was real and 1.2.11 ended it. Adds a live-unconn column (getpeername on every tagged socket, reports 1: the listener) and rewrites the notes that said otherwise.
|
|
sockcount.h was in neither makefile's HEADERS, so editing it rebuilt only the objects whose .cpp had also changed and the rest kept the old struct layout -- one object indexing a shared function-local static by the old layout and another by the new one, in the same binary. compat.h and font_roboto.h were missing from one list each. Verified with a full clean rebuild: no duplicate-section warning.
|
|
The 1.2.11 accounting answered 'which site' and stopped. On a node with 3h51m of uptime it reported 58 live sockets while Windows attributed 112 to the process, 47 of them bound and never connected, against 55 closes that had returned an error -- with nothing recording what those errors were. Close failures are now counted per code per site, and the table totals, so the live figure can be compared directly against what the OS attributes to the process. Instrumentation only.
|
|
Version bump for the 1.2.12 release. One line of Makefile; the release notes live here.
## The reason this release is not optional
**#87 — a node that was mining before 1.2.11 can come back as a relay and never say so.** The 1.2.11 mining rework left `fGenerateBitcoins` readable on its own: a wallet carrying that flag from an older build, with no mining mode stored beside it, loads as "not mining" and the window shows nothing wrong. It happened here, on a real node, and it was found only because somebody looked at the hashrate. Anyone running 1.2.11 who mined on an earlier build should assume they are affected.
## Recovery phrase (#93, #96)
Twelve words that bring a wallet back. Proved end to end before merging: a live wallet was paid on a derived address, `wallet.dat` was deleted, and the coin came back from the phrase alone.
- `-newphrase` creates a phrase, installs the seed, fills the key pool from it and shows the words once. It refuses when a seed already exists.
- `-restorephrase="WORDS"` installs the seed and walks forward in batches of 100, rescanning after each and stopping when a batch turns up nothing. `-restoredepth=N` to look further.
- `-newaddress` prints the next receiving address; `-showderived=N` lists what a phrase produces.
- The same two operations are reachable from the window.
**What the phrase does not cover, stated plainly:** keys that existed before the seed was installed — including the wallet's original default address — are not derived from it and do not come back from the words. The commands say this at the point of use. Keep the `wallet.dat` backup as well; the phrase is a second line, not a replacement.
The words are printed to the terminal only, never to `debug.log`.
## Wallet safety
- **#88** — `ScanForWalletTransactions` plus `-rescan`: the node can now walk the chain for coins a key owns but the wallet never saw. `-importwallet` scans too. Without this, restoring from a phrase would have derived the right keys and found nothing.
- **#89, #90** — a backup panel in the window, with the default backup written outside the directory it protects.
- **#95** — a scan with no chain data loaded is refused instead of reporting "no transactions found", which read as a verdict to somebody who had just lost a wallet.
## Tests and diagnostics
**#91** wallet key pool self-test, **#92** self-test output where the person running it can see it, **#94** MSYS2 argument conversion (use `-selftest=...`, not `/selftest=...`, under MSYS2). `make tests` runs the BIP32/BIP39 vectors and both wallet self-tests on Linux and Windows.
## Still not fixed
The socket accounting shipped in 1.2.11 is instrumentation, not a fix — sockets still leak. The three obvious causes have been measured and eliminated; the next step is reading a real node's diagnostic report, not another guess.
|
|
Adds -newphrase, -restorephrase, -restoredepth, -newaddress and -showderived, plus the window buttons that reach the same code. Restore derives forward in batches of 100 and rescans after each, stopping when a whole batch turns up nothing; the phrase is shown on the terminal only, never in debug.log. Proved end to end on a live wallet: paid a derived address, deleted wallet.dat, restored from the twelve words and recovered the coin. Coins on the pre-seed default address do not come back, and the command says so.
|
|
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.
|
|
Co-authored-by: Gilcimar Duarte <ti@gpm.infra>
|
|
On Windows the self-test from #91 printed nothing. printf in this tree is
OutputDebugStringF, which writes to the data directory debug.log, and the
Windows binary is linked -mwindows with no console. A failure was still caught,
since a non-zero return stops make, but it arrived with no way to tell which
check failed.
And because the first line printed before strSetDataDir pointed at the temporary
directory, every line was appended to the real debug.log -- the file users are
asked to paste into issues.
The file now gets the real printf, the data directory is set before anything
prints, and on Windows a console is borrowed only when stdout has nowhere to go.
That last condition matters: the first version reopened stdout unconditionally
and took the output away from `make tests > log`, which is the case that counts.
|
|
Co-authored-by: Gilcimar Duarte <ti@gpm.infra>
|
|
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.
|
|
Two follow-ups to #89. The default backup path was inside the data directory --
same disk, same folder the README says to copy wholesale, same folder the user
eventually deletes, which is the moment the backup was for. It now defaults to
the home directory and the dialog says so when a path under the data directory
is chosen anyway.
And nLastWalletBackup was written by the GUI, so anyone backing up with
/backupwallet -- the method the README documents first -- was told forever that
they had never backed up. BackupWallet records it now, so both paths count.
|
|
Co-authored-by: Gilcimar Duarte <ti@gpm.infra>
|
|
fGenerateBitcoins and nMineMode only mean anything together, and a wallet.dat
can hold one without the other: Bitcoin 0.1.0 already wrote fGenerateBitcoins
when mining was toggled, years before this fork had a mode. #84 restored that
lone flag, which puts the node in the one state the miner cannot act on --
generate on, mode relay -- so every miner thread starts and returns at the relay
guard. The machine looks like it is mining and hashes nothing.
That happened to a 32-core node on its first 1.2.11 start: "Mining with 31
thread(s)" followed by 31 lines of "BitcoinMiner thread N returned true".
The flag is now believed only when the mode it belongs to was stored with it,
and the pair is made coherent either way.
|
|
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.
|
|
A node was found holding 1502 sockets in Windows bound state, one ephemeral port
each, 9% of the machine range in nineteen hours. Reading the code did not find
it: every connect() failure path here closes its socket.
So count. Every socket is tagged at creation and untagged at close, across all
ten sites, with a counter for closes that fail. Already rules out three
explanations: nothing is created or closed behind the counter (untagged closes
are zero), no close is failing, and the leaked sockets are not in the live set --
tracked live matches what the OS reports as established, while the bound pile is
something the program already closed successfully.
|
|
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.
|
|
|
|
* 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.
|
|
Co-authored-by: Gilcimar Duarte <ti@gpm.infra>
|
|
They came in with #78 and are build output, not source: 5 MB of Linux
binaries in a tree whose .gitignore already excludes the Windows zip and
the AppImage but had no pattern for the headless node. Adding one so the
next release cannot repeat it.
|
|
|
|
1.2.9 shipped before the last seven merges landed, and one of them is holding
two live nodes hostage.
Measured on a 32-core node running 1.2.9 today: 65,134 MB committed, which is
30 RandomX datasets at 2,142 MB each -- one per miner thread, for a node that
advertises 2 GB. A 4-core node on the same build sits at 6,568 MB, three
datasets. #68 allocates the dataset once and closes the window where a thread
could mine against one that was not finished being filled.
The rest of the release:
- #67 orphans no longer stack above earned blocks in the GUI list
- #69 no submitting a solution for a parent that is no longer the tip
- #70 a ceiling on orphan blocks, and getblocks stops repeating itself
- #71 Berkeley DB cursors get closed instead of left to the handle
- #73 bounds on what a peer can make the node allocate
- #74 the recipient no longer collects the fee when a transaction retries
|
|
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.
|
|
Three functions open a cursor and none of them close it: CTxDB::LoadBlockIndex,
CTxDB::ReadOwnerTxes and CWalletDB::LoadWallet.
Berkeley DB requires every cursor to be closed before the handle it came
from. ~CDB() calls Close(), which calls pdb->close() inside a catch-all
that swallows whatever it returns -- so the violation was real and silent,
which is this codebase's favourite kind.
An open cursor also holds read locks, and that is not theoretical here: the
first version of the startup chain repair (#61) opened a write transaction
beside the cursor LoadBlockIndex was still holding and waited on it
forever, coming up, printing its diagnosis and hanging with nothing in any
log. The repair had to be moved to the caller to get clear of it.
A scope guard rather than a close call at each return, because all three
functions have several exits and the point is to stop relying on anyone
remembering. Same fix Bitcoin made in c5c7911da.
Verified: node starts, verifies 288 blocks, loads the wallet, runs the
spent-flag rescan and syncs the live chain to 4422 with no Berkeley DB
error anywhere and db.log still empty.
|
|
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.
|
|
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.
|
|
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.
|
|
The wallet list sorted on GetDepthInMainChain() ascending so pending
transactions came first. An orphaned coinbase is also depth 0 -- and it
stays depth 0 forever, because it is never going to be confirmed.
So every orphan a node had ever produced sat permanently at the top of the
list, above every block it had actually won. A miner with a week of
ordinary orphaned races opened the wallet and saw nothing but failures.
The balance was right the whole time; the list was telling a different
story. That is how this was found: a screenshot of a healthy,
correctly-mining node that looked like it was failing at everything.
Sort on (rank, depth) instead, where rank is 0 for pending, 1 for
confirmed, 2 for orphaned. Orphans go to the bottom, pending stays on top,
confirmed stays ordered most-recent-first.
Verified against a datadir whose chain had been reorganised away, so every
coinbase in it is orphaned: all 17 wallet transactions classify as rank 2.
Not tested against a mixed wallet -- the machine that has one is in
production with its datadir locked by the running node.
|
|
Every miner thread calls RandomXInitDataset at startup. Its only guard is
g_fFast, which is set at the very end -- after the ~2 GB dataset has been
filled, which takes tens of seconds. So all of them sailed past the guard
together and each allocated its own dataset.
Measured in production on a 32-core machine running 31 miner threads: the
log says "initializing ~2 GB dataset" 34 times, and the process holds
65,127 MB of private pages while its own startup line promises 2142 MB
"(~2080 MB shared RandomX dataset + ~2 MB per thread)". The word shared
was not true. A 4-core machine showed the same thing at its own scale: 4
allocations for 3 threads.
The waste was the mild half. g_dataset is assigned the moment memory is
allocated, before it holds anything, so the last thread to allocate swung
the pointer out from under threads still filling their own. A miner could
then hash against a dataset another thread had not finished writing, and
proof of work computed that way is not reproducible by any other node --
the block would be found invalid by everyone, including this node's own
verifier, which uses the cache rather than the dataset.
This only became reachable with multi-threaded mining (#52); with one
thread there was nobody to race.
Holding the lock across the whole initialisation is deliberate: a thread
arriving mid-init has nothing useful to do until the dataset exists, and
blocking is how it waits for exactly that.
Verified with three mining threads: one "initializing ~2 GB dataset", one
"dataset ready", and 2362 MB of private memory against 2086 MB advertised
-- the difference being the 256 MB cache. Before, three threads meant three
datasets.
|
|
relay.nostr.band went down and stayed down. Every node running 1.2.8 or
earlier spends ten seconds of every discovery pass waiting for it and
writes an error line each time -- the seed logged 2029 of them. #64
replaces it with a relay that answers and backs off from any relay that
stops answering, so the next one to go down costs nothing.
|
|
A service registers at the relay and its socket sits in the waiting map
until a client dials it. Nothing ever checked whether that service was
still there. A node that went away left its registration behind, and the
next client to ask for that key got paired with a corpse: both sides were
told the pairing succeeded, then the tunnel failed with nothing to explain
it. That is the "tunnel to X failed" line the dialling node reports.
A reaper walks the waiting map every 60 seconds and drops registrations
whose far end is provably gone, and the pairing path checks the same thing
before handing a registration over, so a client gets a clean refusal
instead of a broken tunnel.
Provably gone means: the socket became readable and recv returned 0 or an
error. A registered service sends nothing while it waits, so readable is
already abnormal; recv distinguishes a closed peer from anything else.
Sockets that are simply idle are left alone. select() rather than
MSG_DONTWAIT so this compiles on Windows and never touches the socket's
blocking mode.
Deliberately no expiry by age. Nodes older than v1.2.7 register once and
block forever waiting to be dialled -- they never re-register, so a clock
based rule would quietly make every one of them unreachable, and being
blocked in recv they would never find out. Only provable death counts.
Known limit: this catches a peer that closed or reset, not one whose path
died silently with no FIN. Detecting that needs a probe, and there is no
byte the relay can send to a waiting service without corrupting the
protocol it is waiting on.
Verified on the VPS on a spare port: a node registered, was killed with
SIGKILL, and the next reaper pass logged
"reaper: dropped 1 dead registration(s), 0 waiting".
|
|
relay.nostr.band went down and stayed down -- it resolves and refuses TCP
on 443, from several networks. The seed accumulated 1092 identical error
lines about it.
The log noise was the small half. The discovery loop walks every relay in
turn with a ten-second connect timeout, so a dead relay added ten seconds
to every pass, forever, and that loop is what finds peers -- a relay that
answers nobody was slowing down the ones that answer everybody.
Back off per relay: 30s after the first failure, doubling to a fifteen
minute ceiling, cleared the moment it answers again. Only when an attempt
happens changes; nothing about whether discovery is correct.
Also swapped relay.nostr.band for nostr.bitcoiner.social, on 206.245.132.32
-- neither Cloudflare nor Hetzner, which is the diversity rule the list
already follows.
Verified with a deliberately bogus relay in the list: backoff stepped
30s -> 60s -> 120s and the dead entry produced nine lines in four minutes
instead of one per pass, while the node still connected to five relays,
discovered 54 fresh peers and followed the chain.
|
|
Six changes since 1.2.7, and the reason not to sit on this is #58:
coinbase transactions were not unique, two of them overwrote older ones
on the main chain, and 100 BTF stopped being reachable. Nothing in how
they were built had changed since, so it could happen again at any time.
The height now goes into the coinbase on both paths that build one
(#59, #60), which makes a repeat impossible without any coordination.
Also in: a node now notices a peer it can no longer hear and stays
audible to peers that check the same way (#55); it announces its height
in the handshake and says out loud when it falls behind the network
(#56); script evaluation has ceilings on operation count and stack depth
(#57); the tail of the chain is verified at startup and a bad tip is
backed out of instead of being mined on (#61); and the wallet now
believes the chain about what it has already spent, which is what a
restored backup gets wrong (#62).
|
|
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.
|
|
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.
|
|
#59 changed BitcoinMiner and stopped there. RebuildJob in rpc.cpp builds
its own coinbase for the pool operator's job template, and that template is
mined and submitted like any other block, so it had the same hole: nothing
in the transaction belonged to the block, and a repeated combination of
difficulty, counter and key produces a byte-identical coinbase.
The claim in #59 that two coinbases at different heights can no longer be
identical only becomes true with this.
|
|
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.
|
|
v1.2.1 disabled the unbounded opcodes and capped a single pushed element at
520 bytes. What it did not add was a ceiling on how many items there can be,
so a script still pays for itself once, in bytes, and then multiplies: 520
bytes per stack entry means a script of N bytes made almost entirely of
OP_DUP occupies roughly 520N in memory, on every node that validates it.
Two limits, both from Bitcoin:
- at most 201 operations per script, counted outside the fExec test so
the cost of a script cannot depend on which branch a signature happens
to take. Pushes are exempt, being already bounded by the script length
and by MAX_SCRIPT_ELEMENT_SIZE.
- at most 1000 entries across both stacks together, counted together
because OP_TOALTSTACK would otherwise move the problem rather than
solve it.
This tightens validation, so a node that does not upgrade accepts a
superset of what this one accepts and is not stranded.
Verified against the live chain: a node with these rules synced all 3910
blocks from genesis and accepted every one, so nothing in use relies on
scripts these limits would reject.
|
|
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.
|
|
(#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.
|
|
Rendezvous no longer waits forever for a dial (#51), keys can be
exported and imported as text (#49), and mining uses every core
instead of one (#52).
#51 is the reason not to sit on this: without it a node goes
silently deaf after a while and only a restart brings it back.
|
|
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.
|
|
A node registers at a relay and waits to be dialled. The wait
had no bound, and ConnectTo() clears the socket timeout on
purpose -- a registration is idle by design and may sit for a
long time.
What that cannot survive is the path dying with neither end
saying so, which is the ordinary case and not the exotic one:
a NAT drops the idle mapping, a relay restarts, a route
changes. No FIN arrives, nothing becomes readable, and recv()
never returns. The thread stays alive and asleep, so
ThreadBtfAccept never reaches the `continue` that would
register again. The node is unreachable and every outward sign
says it is fine.
Caught on the bootstrap seed: 27 minutes without a block,
eight behind the network, relay reachable the whole time, and
a restart fixing it instantly. Nothing in the log marked the
moment it went deaf.
I tried TCP keepalive first and it does not work here. With
probes every 5s and a 2-probe limit, a socket whose replies
were being dropped was still ESTAB after four minutes with the
probe counter stuck at zero. It is measured, not assumed, and
that approach is not in this change.
So the bound is ours to enforce, at the one place that knows
what waiting means. RvServiceWaitPaired takes a timeout;
expiring is not an error but "nobody came, go round again",
which the loop above already handles. Cost is one reconnect
per relay per five minutes.
Verified by killing a registration's path silently -- dropping
only the relay's replies, so no RST or FIN is ever seen. With
a 60s bound for the test:
rendezvous: no dial at 92.246.128.180:8434 within 60s
(or it dropped us), re-registering
Registrations went 1 -> 2 and the node carried on. Before this,
the same treatment left it blocked past four minutes with no
log line at all.
The socket goes back to blocking once a dial lands, because
from that point it is a data tunnel and a tunnel is allowed to
sit quiet between messages.
|
|
The last door out. Everything else assumes Berkeley DB will
cooperate. When it will not -- a build that cannot read the
file, a truncation, an environment past recovering -- the keys
are usually still perfectly intact and were unreachable
anyway. #40 is one person's account of exactly that, and #46
only rescues the cases where BDB still opens the file at all.
bitflash -dumpwallet=keys.txt
bitflash -importwallet=keys.txt
One key per line with its address and label. No database, no
environment, no version coupling: anything that can read a
line can recover from it.
The format is the DER private key in hex, which round-trips
exactly through SetPrivKey. Not interchangeable with other
wallets -- worth having eventually, but a WIF encoder I got
subtly wrong would be worse than no encoder at all, and this
file exists for the case where nothing else works.
Care taken, because the failure modes here cost money:
- Refuses to overwrite an existing file. A mistyped path
must not eat the only copy of something irreplaceable.
- Written owner-only on POSIX, before any key reaches it.
Windows inherits the folder's ACL; the README says so
rather than pretending otherwise.
- Newlines in labels are flattened, so a label cannot forge
an extra entry on read-back.
- Import skips keys already present, so running it twice
does nothing, and one unreadable line is reported and
stepped over instead of costing the reader every other key
in the file.
Verified on Linux and Windows: a key exported from one wallet
imports into a different one and its address then appears
there; re-importing adds nothing; a corrupted line is skipped
while the rest still load; the dump is 0600 and refuses to
overwrite.
This does not solve #47. Keys still are not derived from a
seed, so a dump is as much a snapshot as any other backup.
What it does is make the keys reachable no matter what state
the database is in.
|
|
Wallet backup and clean shutdown (#46).
Two reasons not to sit on this: every Linux node aborts on the
way out instead of releasing wallet.dat, and until a node has
shut down cleanly once, the wallet it wrote cannot be moved
anywhere else.
|
|
* wallet: make wallet.dat a file you can actually back up
Issue #40: a wallet.dat copied out on its own will not open
anywhere else. The file is intact -- same bytes, same keys --
and the node refuses it with "Db::put: Invalid argument",
thrown from a path that aborted the process. The reporter
believes they lost funds to this, having saved the one file
anybody would think to save.
The chain, from the bottom:
_endthread() is pthread_exit() on POSIX, and glibc implements
that by throwing abi::__forced_unwind through the stack. That
is not a real exception; catching it without rethrowing is a
fatal error by contract. CATCH_PRINT_EXCEPTION ended in a bare
catch(...), so every Linux shutdown ended:
UNKNOWN EXCEPTION bitflash-node in ThreadSocketHandler()
FATAL: exception not rethrown
An abort, mid-shutdown, so nothing after StopNode() ran.
Which mattered because Berkeley DB stamps every page with a
log sequence number belonging to the environment that wrote
it. Carried elsewhere, those numbers point at logs that do not
exist and BDB refuses to write. DBFlush(true) calls lsn_reset,
which is precisely the cure -- and DBFlush has been declared
since 2009 and called from nowhere. Even had it been called,
the abort stood in front of it.
So the wallet was never released from its directory, and the
one file users copy was the one file that could not travel.
Three fixes, each covering a different case:
- Rethrow abi::__forced_unwind, and call DBFlush(true) on
both shutdown paths. Shutdown completes, and wallet.dat is
left portable by construction.
- Adopt orphaned files on open: a freshly created database
environment beside pre-existing .dat files means they came
from elsewhere, so reset their sequence numbers. This is
what recovers wallets already stranded by older builds --
including, if they still hold the file, the reporter's.
- BackupWallet(), reachable as /backupwallet=FILE, which
checkpoints, copies, and clears the copy's sequence
numbers. Bitcoin 0.1.0 had a backup command; this tree
lost it in the wxWidgets removal and has shipped with no
way to take a backup since.
DBFlush's own txn_checkpoint and lsn_reset are wrapped too --
they throw, and throwing on the way out is how this started.
Verified on Linux and Windows: shutdown exits 0 rather than
134, DBFlush runs, and a lone wallet.dat opens in an empty
directory with no exceptions and byte-identical key records
(defaultkey, key, name, setting, version) before and after.
Not addressed here, and the larger half of #40: keys are
generated as they are needed rather than derived from a seed,
so any backup is a snapshot and coins paid to addresses
created after it are not in it. /backupwallet says so on every
run. The real answer is deterministic derivation, which is a
wallet format change and its own piece of work.
* README: document what a wallet backup actually requires
The PR description claimed this section existed and it did not.
Covers the two things that cost money and are not obvious: a lone
wallet.dat is refused elsewhere because Berkeley DB ties it to the
database/ directory beside it, and every backup is a snapshot
because keys are made on demand rather than derived from a seed.
|
|
Headless mining fix (#42), ARM64 build (#41), format-string
checking (#44), gitignore for key material (#43).
#42 is the reason this release exists: -gen has mined nothing on
headless nodes since v1.2.0.
|
|
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.
|
|
nMineMode defaults to MINE_RELAY (0) and ParseStartupArguments() never changed
it, so a node started with /gen would spawn ThreadBitcoinMiner, enter
BitcoinMiner(), hit
if (nMineMode == MINE_RELAY)
return true;
and return before doing any work. The node then ran indefinitely looking
completely healthy -- it synced, it held peers, it reported no errors -- while
earning nothing. Nothing in the log said otherwise; the only hint was
`nMineMode = 0` in a startup line that reads like a status echo.
This affects every headless miner, because the GUI is the only other way to
select a mining mode.
/gen now implies MINE_SOLO unless /operator or /participant asked for something
else. Verify with `nMineMode = 1` at startup.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
|
ByteReverse(word32) in src/sha.h uses the x86 `bswap` instruction via inline
asm, guarded only by `#if defined(__GNUC__)`. GCC and Clang define __GNUC__ on
every architecture they target, so on aarch64 the guard passes and the
assembler then rejects the instruction:
Error: unknown mnemonic `bswap' -- `bswap %eax'
This makes the tree fail to build on any ARM64 host -- Apple Silicon, ARM
servers, a Raspberry Pi -- at a point far from anything the user did.
Narrow the guard to the architectures where the instruction exists. The
portable C fallback directly below is then used everywhere else; it was
already there and already correct.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
|
.gitignore ignores *.dat and wallet.dat, but the node also writes three files
in its data directory that are not .dat and are just as sensitive:
btf_enc.key this node's .btf service key -- its network identity
nostr.key the Nostr key its descriptors are signed with
walletrpc.token bearer token authorising spends over the wallet API
A node run with its data directory inside a working tree -- which is the
obvious thing to do while developing -- leaves all three untracked but
committable, and `git add -A` takes them. Losing btf_enc.key means losing your
address; publishing walletrpc.token hands over the wallet to anyone who can
reach the port.
Also ignore key backup archives and stray .patch/.rej/.orig files.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
|
Seeds (#36), headless node (#37), README (#35), wallet crash fix (#38).
|
|
Berkeley DB throws when it cannot open a file written by
another platform's build (e.g. BDB 5.3 wallet read by 6.2).
Nothing on the call path caught these, so the exception
unwound into std::terminate -- surfacing on Windows as
STATUS_STACK_BUFFER_OVERRUN (0xC0000409) with no useful
message.
Three layers of defense:
1. main_gui.cpp: try/catch around LoadBlockIndex() and
LoadWallet(), printing the reason and exiting cleanly.
2. db.cpp LoadWallet: try/catch around the record-reading
loop (Satoshi's 2009 TODO), tracking the last record type
so the error message says what it choked on.
3. db.cpp cleanup paths: every dbenv.close(), pdb->close(),
txn_checkpoint() and log_archive() wrapped in try/catch
so the environment teardown after a failed open does not
throw a second exception into std::terminate.
Tested: cross-platform wallet (Linux BDB 5.3 → Windows 6.2)
now prints a clear message and exits with code 1, wallet
left untouched.
Closes #34
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
|
|
The GUI build requires libGL at load time even when started with -nogui. The
dynamic loader resolves every dependency before main() runs, so the flag arrives
far too late -- on a clean server the binary dies with
error while loading shared libraries: libGL.so.1: cannot open shared object file
and nothing suggests it would otherwise have worked. A server is where a node
most naturally lives, so this is the deployment that mattered most and the one
that failed. The AppImage additionally needs FUSE, which minimal servers lack.
Bundling libGL into the AppImage would be the wrong fix: GL has to come from the
host's driver stack, and shipping our own breaks machines that have a real GPU.
So: a second binary, built from the same node objects without ImGui, GLFW or
OpenGL. `make linux-node` produces bitflash-node-<version>-x86_64, 2.5 MB
stripped, with no graphics or FUSE dependency at all. `make linux` now builds it
alongside the AppImage. Anyone wanting a window keeps using the AppImage.
Two small moves were needed. MainFrameRepaint is called from main.cpp and
net.cpp but defined in gui.cpp, so headless.cpp provides an empty one rather
than putting ifdefs at six call sites. DateTimeStr also lived in gui.cpp while
main.cpp logs block times with it -- nothing about formatting a date is GUI
work, so it moved to util.cpp and both builds share one definition.
Verified on Debian 13:
ldd bitflash-node | grep -iE 'libGL|glfw|X11' -> nothing
ldd bitflash | grep -iE 'libGL|glfw' -> libglfw.so.3, libGL.so.1, libGLdispatch.so.0
and at runtime, from an empty datadir:
btfseed: reached ygffz37...btf via 90.156.222.107:8434
rendezvous: registered at 90.156.222.107:8434, waiting for a dial
self-resolve OK (meeting_node=90.156.222.107:8434)
btfpeers: kept 15 of 15 descriptor(s)
first block at ~30s, 3182 blocks accepted, synced to the tip
The GUI builds on Linux and Windows were rebuilt and are unaffected.
|