<feed xmlns='http://www.w3.org/2005/Atom'>
<title>npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/src/main.h, branch main</title>
<subtitle>[no description]</subtitle>
<id>https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/atom?h=main</id>
<link rel='self' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/'/>
<updated>2026-09-05T05:08:30+00:00</updated>
<entry>
<title>Merge upstream/main into the  fork</title>
<updated>2026-09-05T05:08:30+00:00</updated>
<author>
<name>bitflash-maintainer</name>
<email>noreply@bitflash.invalid</email>
</author>
<published>2026-09-05T05:08:30+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=66e0b9158d050c46b1ff2509faeee3f4573cac1c'/>
<id>urn:sha1:66e0b9158d050c46b1ff2509faeee3f4573cac1c</id>
<content type='text'>
Brings the nine commits that are actually the latest on our lineage:
isolated testnet parameters and genesis, optional 2 MB large pages for
RandomX, and five README changes. This is what "up to date" means here --
not the 1.2.2x tags, which carry higher version numbers but were cut
2026-07-30, three weeks before the line we ship, and which upstream/main
does not contain.

Three conflicts, all resolved toward upstream's structure while keeping
what this fork knows:

main.cpp -- upstream moved genesis construction into BuildGenesisBlock() so
mainnet and the new testnet can share it. Verified byte-for-byte before
taking it: MAINNET_GENESIS_TIMESTAMP, _TIME (1753315200), _NONCE (3141),
_HASH, _MERKLE, the scriptPubKey, nBits and nVersion all equal what this
tree built inline. The mainnet genesis hash is unchanged and the asserts
still guard it. Kept our warning about never editing those inputs, and kept
our note that 1753315200 is 2025-07-24, not 2026 -- upstream's comment on
that constant states the wrong year.

net.h -- DEFAULT_PORT = htons(8433) becomes MAINNET_PORT/TESTNET_PORT in
host order. Nothing is lost: net.cpp's GetDefaultPort() applies htons()
itself and is what every caller uses, so the value reaching a sockaddr is
unchanged and mainnet stays on 8433.

selftest.cpp -- the known-tests string had diverged in both directions.
Took the union: upstream's network-params alongside our payout-ledger,
rather than dropping either.

Gates: 15/15 selftests (our 14 plus upstream's network-params), consensus
constants unchanged against e4f898c except the pre-existing
MAX_BLOCK_SIZE_GEN, mainnet genesis intact, compactblock's 17 checks still
pass, money-log check ok, GNU_STACK RW.
</content>
</entry>
<entry>
<title>merge: upstream 1.2.20 into deploy/all-2026-08-24</title>
<updated>2026-09-03T22:33:20+00:00</updated>
<author>
<name>bitflash-maintainer</name>
<email>noreply@bitflash.invalid</email>
</author>
<published>2026-09-03T22:33:20+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=b810d86752ddb2674118f737582c924bb9eb74c1'/>
<id>urn:sha1:b810d86752ddb2674118f737582c924bb9eb74c1</id>
<content type='text'>
# Conflicts:
#	Makefile
</content>
</entry>
<entry>
<title>mine: count sigops while selecting, not only when validating</title>
<updated>2026-09-02T14:16:48+00:00</updated>
<author>
<name>bitflash-maintainer</name>
<email>noreply@bitflash.invalid</email>
</author>
<published>2026-09-02T14:16:48+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=2ed8fae6088c99f1a5dfcfd760c1a24c2b2706db'/>
<id>urn:sha1:2ed8fae6088c99f1a5dfcfd760c1a24c2b2706db</id>
<content type='text'>
CheckBlock() caps a block at MAX_BLOCK_SIGOPS and neither assembler counted
against it. Bytes do not bound what a block costs to verify: every OP_CHECKSIG
is an elliptic-curve verification, so a template comfortably inside the size
budget can still be one the network refuses for demanding more work than a
block is allowed to.

Counted with CTransaction::GetSigOpCount -- the same function CheckBlock()
sums, not a second implementation of it -- and seeded from the coinbase,
because CheckBlock() sums over all of vtx. The comparison matches too:
CheckBlock() fails on `&gt; MAX_BLOCK_SIGOPS`, so a block exactly on the limit is
valid and selection accepts it. Assembly and validation differing by one is
the shape of the bug this series started with.

Unlike the size budget this takes no margin. There is no use for headroom in a
sigop count, so assembly holds itself to the consensus number exactly.

Assembly only; MAX_BLOCK_SIGOPS and the CheckBlock() test are unchanged.
</content>
</entry>
<entry>
<title>mine: ask whether a transaction fits before appending it</title>
<updated>2026-09-02T14:14:48+00:00</updated>
<author>
<name>bitflash-maintainer</name>
<email>noreply@bitflash.invalid</email>
</author>
<published>2026-09-02T14:14:48+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=74c068829967bbff60da3506613844df4da1ae80'/>
<id>urn:sha1:74c068829967bbff60da3506613844df4da1ae80</id>
<content type='text'>
The node miner tested its size budget only on the outer while loop. The inner
for walked all of mapTransactions with no test, so a single pass could append
the entire mempool -- the overshoot was bounded by mempool size, not by one
transaction as the comment claimed.

The pool builder did test, but only after the push, which bounds the overshoot
at one transaction. Nothing caps how big one transaction may be: neither
CheckTransaction nor AcceptTransaction limits it, so the mempool will hold one
of any size the wire allows and "one transaction" is worth up to MAX_SIZE. A
lone 1.5 MB transaction offering a large fee is therefore enough to make every
template unmineable, and the fee is never paid because the block never
confirms.

Both now ask before appending and skip what does not fit, so the overshoot is
zero and an oversized transaction costs the template nothing rather than
invalidating it. The budget moves to MAX_BLOCK_SIZE_GEN so the two assemblers
cannot drift on it again -- drift is what produced this.

The test is ordered before ConnectInputs on purpose: ConnectInputs credits
nFees by reference on success, so declining a transaction after calling it
would leave the coinbase claiming a fee the block does not collect.

Assembly only. CheckBlock, CheckTransaction and AcceptTransaction are
untouched: this changes what we produce, not what the network accepts.
</content>
</entry>
<entry>
<title>Merge upstream v1.2.20 into the  fork</title>
<updated>2026-09-01T17:14:12+00:00</updated>
<author>
<name>bitflash-maintainer</name>
<email>noreply@bitflash.invalid</email>
</author>
<published>2026-09-01T17:14:12+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=b4f0bc693ef7175702f82bcb2110b627922e8cbd'/>
<id>urn:sha1:b4f0bc693ef7175702f82bcb2110b627922e8cbd</id>
<content type='text'>
Merges upstream tag v1.2.20 (6aa3dade) into our 1.2.19+sec tree. The merge
base is bc3de30's descendant 9e39a50 -- upstream's own v1.2.19 -- so this is
an ordinary three-way merge, not a replay. Five conflict blocks in four
files, all resolved below.

No consensus change. CheckBlock, ConnectBlock, DisconnectBlock, ConnectInputs,
AcceptBlock, AcceptTransaction, GetNextWorkRequired and GetBlockValue hash
identical to v1.2.19; script.cpp and script.h are byte-identical on both
sides. main.cpp is +3 lines and main.h +2, all wallet-backend plumbing.
rpc.cpp is untouched, so the payout ledger arrives unchanged.

Makefile, VERSION. OURS, renumbered. 1.2.20+sec, for the same reason 1.2.19
carried +sec: this tree is not the upstream release of that number and the
string names every release artifact.

Makefile, AppImage recipe. OURS for the AppDir path, THEIRS for the bundled
library. Upstream's hunk reverts the icon paths to a hardcoded
/tmp/bitflash.AppDir, which is the local-user pre-placement hazard the fork
moved out of /tmp in the first place; taking their side wholesale would have
reintroduced it silently, inside a conflict about something else. The AppDir
stays $(APPDIR), and libsqlite3.so.0 joins the bundled list, which the new
backend genuinely needs.

src/Makefile, src/makefile.mingw, HEADERS. BOTH. Ours drops market.h,
theirs adds wallet_sqlite.h; the resolution does both. NODE_OBJS auto-merged
correctly in both files -- obj/wallet_sqlite.o present, obj/market.o still
gone -- and was verified rather than assumed, because that line has trapped
this fork before.

src/selftest.cpp, known-tests list. BOTH, union. Upstream adds eight
wallet-storage tests, we keep payout-ledger. The dispatch chain itself
auto-merged and still routes payout-ledger to RunPayoutLedgerSelfTest.

Hardening verified intact after merge: the TLS 1.2 floor in nostr.cpp, the
/rvrelay pin, opt-in managed Tor, strict sig3-on-onion descriptor
verification, and the marketplace removal. Upstream shipped no build logs or
__pycache__ this time; the three added files are docs/storage.md and
src/wallet_sqlite.{cpp,h}.

The SQLite backend this release introduces is NOT enabled here. An existing
wallet.dat resolves to Berkeley DB on every branch of the new selection
logic, and the conversion wizard is GUI-only and not compiled into the
headless node. The review findings that gate ever turning it on -- the
non-atomic backend marker, the connection-wide WriteHDMaster transaction,
and the encrypt-swap crash window -- are recorded on the tracker, not fixed
here.
</content>
</entry>
<entry>
<title>payout: ask the wallet why a send failed instead of guessing from the balance</title>
<updated>2026-08-23T11:41:28+00:00</updated>
<author>
<name>bitflash-maintainer</name>
<email>noreply@bitflash.invalid</email>
</author>
<published>2026-08-23T11:41:28+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=9d6fcb2058d90e3ecfb011e2168e4eee04e3a90a'/>
<id>urn:sha1:9d6fcb2058d90e3ecfb011e2168e4eee04e3a90a</id>
<content type='text'>
A payment that the pool could not fund was being recorded as a fault
against the payment, and 1440 faults erased the debt with the coin still
in the operator's wallet.

The classifier read GetBalance() after the failure and asked whether it
covered `amount + nTransactionFee + CENT`. Every term of that was wrong.
nTransactionFee is 0 on this pool -- it is only ever assigned from the
GUI preferences dialog, which a headless node does not have -- so the
entire reserve was one CENT. The fee it is meant to reserve for is
GetMinFee(fDiscount=true), which is a step function of the serialised
size: 0 below 10,000 bytes and (1 + bytes/1000) * CENT at or above it.
There is no required-fee value between 1 and 11 CENT, so a one-CENT
reserve is never the right number -- below the step nothing needs
reserving, at the step it is short by a factor of eleven.

Worse, the trigger correlates with the failure. A pool wallet close to
broke selects many small inputs BECAUSE it is close to broke, and about
56 signed P2PKH inputs is 10 KB, which a pool that has been paying out
accumulates as change. So: balance of amount + 0.05, the pre-send gate
passes, CreateTransaction builds a transaction over 10 KB, the fee
becomes 0.11, SelectCoins(amount + 0.11) fails, SendMoney returns false
-- and (amount + 0.05) &lt; (amount + 0 + 0.01) is false, so "funded", so a
fault. The condition is stable, so it recurred every 60 s until the
budget ran out and ForfeitPayoutRecipient erased the miner's money.

Three changes.

The wallet now says what it did. CreateTransaction takes an optional
WalletSendResult out-parameter and sets WALLET_SEND_INSUFFICIENT_FUNDS
on the one return that means a funding shortfall -- SelectCoins failing
to cover nValue plus whatever fee that pass is working with, which on
the second and later passes is GetMinFee() of the transaction the first
pass actually built. SendMoney gains a four-argument overload returning
that plus the fee that was required. The three-argument SendMoney stays,
as a separate declaration rather than a default argument, because
gui.cpp declares it itself instead of including main.h; walletcmd.cpp
and the GUI are unchanged. These are wallet functions, not consensus.

ClassifyPayoutSendFailure() then classifies the answer rather than
guessing: locked or short of coin defers, anything else is a fault.
There is no reserve left to get wrong, and the size-dependent fee is
accounted for exactly, because the wallet computed it.

And the give-up branch is gone. 1440 failures is not a decidable state,
it is a count of how long we have been asking, and this pool reached it
by misclassifying a permanent funding shortfall. A payment that keeps
failing now stays in flight, keeps its ledger entry, keeps being
reported as owed in pool_status.json, and keeps retrying once a minute.
A debt that is never written off cannot be written off wrongly. The
remaining call to ForfeitPayoutRecipient is the one decidable case --
the ledger names a string that is not a valid address, so no transaction
can pay it -- which previously only dropped the in-flight record and
left the ledger and the round proof claiming the debt forever.

Tested against a real wallet and the real CreateTransaction, not against
injected values, because the honest question is whether a wallet holding
many small coins reports the state this code assumes. 60 coins of 1 BTF,
balance 60.00, amount 59.95: CreateTransaction returns false with
WALLET_SEND_INSUFFICIENT_FUNDS and a required fee of 0.11 -- and since
GetMinFee returns 0 below 10,000 bytes, a required fee of 11 CENT is
itself the measurement that the transaction crossed the step. The same
test asserts that the old reserve judged that state "funded", and that
the same wallet pays a 3-input transaction with a required fee of 0.00,
which is why no fixed reserve is right for both.

Property: a send that failed because the wallet could not fund it is
deferred, whatever the transaction's size, and no number of failures
erases a debt.
</content>
</entry>
<entry>
<title>Merge upstream v1.2.18 into the  fork</title>
<updated>2026-08-23T05:21:31+00:00</updated>
<author>
<name>bitflash-maintainer</name>
<email>noreply@bitflash.invalid</email>
</author>
<published>2026-08-23T05:21:31+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=a9b4f8940907c2bf51e431172b628de89734a908'/>
<id>urn:sha1:a9b4f8940907c2bf51e431172b628de89734a908</id>
<content type='text'>
Merges upstream tag v1.2.18 (bc3de30) into our 24 commits. The merge base is
a259215, upstream's own commit, whose src/ tree is byte-identical to 68de575 --
our commits were replayed onto a259215 first so that git had a real base to work
from. That produced 28 conflict hunks in 9 files instead of the 21 conflict
events / 51 hunks a direct cherry-pick onto the tag would have forced, most of
which were artifacts of lifting our commits away from their own predecessors.

No consensus change. main.cpp's GetBlockValue, GetNextWorkRequired,
ConnectInputs, ConnectBlock, CheckBlock, AcceptBlock and AcceptTransaction are
untouched by this merge; script.cpp, serialize.h and randomx_pow.h are
identical on both sides. The frozen-chain oracle reproduces the pinned UTXO root
116a2b6b... byte for byte, and the merged binary loads the same 12,586-block
chain to the same tip aac622f0a3b2f8... with all blocks re-verified and none
rejected.

Which side won, per file and per hunk.

src/rpc.cpp -- 15 hunks.

1. RebuildJob transaction selection. THEIRS for the multi-pass loop and the
   nFees fix, OURS for the cap. Their vfAlreadyAdded/fFoundSomething loop lands
   dependent transactions that our single pass dropped deterministically, and
   their nFees is accumulated across the scan instead of being redeclared inside
   it -- ours passed a hardcoded 0 to GetBlockValue, so every pool block built
   here discarded its fees. But their bound is MAX_SIZE/2 = 16 MB, and
   MAX_BLOCK_SIZE is 1 MB, so their template can exceed what their own
   CheckBlock accepts. Cap kept at MAX_BLOCK_SIZE/2, with our inner break so a
   single pass cannot overshoot by more than one transaction. Our RebuildJob
   sync gating and deaf-node detection are kept; upstream has none.

2. struct PendingPayout. OURS for the key, THEIRS for paidTxids. Their id is
   the string "block-&lt;height&gt;", which is height keying -- the exact bug 44849e6
   fixed after a 280-block reorg left ~14.4k BTF owed against blocks that no
   longer existed. Height keying also lets the crash-recovery probe match the
   previous round's transaction after a reorg and silently skip a miner, so
   their id is dropped entirely and blockHash stays the key. paidTxids is taken
   because it is what makes an entry survivable. Their nextAttempt and our
   retryAfter are the same concept under two names; ours is kept and theirs
   dropped. matureAtHeight gained an explicit = 0, which neither side had in
   our style of initialiser.

3./4. Ledger writer and loader. Mechanical follow-ons of 2. The loader keeps
   ours and takes their tolerance for an entry with no usable height, guarded so
   it cannot fire on a well-formed entry. One redundancy was removed rather than
   merged: their unguarded `if (obj.contains("blockHeight")) get&lt;int&gt;()` sat two
   lines above our is_number-guarded version, and an unguarded get&lt;int&gt;() on a
   malformed value throws out of the whole loop, losing every entry in the file.
   Verified against the live production pending_payouts.json (2 entries, keys
   amounts/blockHash/blockHeight/matureAtHeight): loads both. A file in the new
   shape, with retryAfter and paidTxids, also loads.

5. Insertion point after the loader. BOTH. Our PayingRecord/gPaying in-flight
   ledger and their PoolRoundProof/PoolRoundToJson are disjoint features that
   landed at the same offset. Both kept, in full.

6. Writer tail. THEIRS, copied into our writer. Checking fclose() matters here
   because fclose is where the buffered write actually reaches the OS: renaming
   a short .tmp over a good file is precisely the truncation the atomic write
   exists to prevent. The Windows remove()-before-rename() is required because
   rename() there will not replace an existing file.

7. Loader bodies. BOTH -- LoadPaying and LoadPoolRounds are different functions
   that happened to share a common tail.

8. Function block, 111 vs 36 lines. BOTH, with one deliberate deletion.
   PayoutBlockStillOnChain, PayoutComment, RecordPoolRound, MarkPoolRoundPaid
   all kept. WalletAlreadyPaid is DELETED and ReconcilePayouts rewritten,
   because our reconciliation and their FindRecordedPoolPayout are two answers
   to the same crash-recovery question and running both would give the same debt
   two authorities. Their wallet probe wins: it matches structured metadata
   rather than a comment string, so it can tell two rounds paying the same
   address the same amount apart. ReconcilePayouts keeps only the job that is
   still real -- dropping in-flight records the ledger already shows paid, and
   arming each restored record for its one wallet probe. It no longer touches
   gPendingPayouts at all; under the new model dropping ledger entries there
   would reintroduce the loss this merge adopts their model to prevent. Their
   gPayoutExecMutex/gPayoutThreadRunning re-declarations were dropped as
   duplicates of ours, and gPayoutExecQueue was not taken: gPaying is the exec
   queue in this tree, at finer granularity.

9. QueuePayouts signature. THEIRS for the round-proof arguments, OURS for the
   type of blockHash. It stays a uint256 and is converted to hex only where the
   round proof needs text -- a ledger key that has been through ToString() is a
   key that can be compared wrong.

10. Maturity policy. OURS. matureAtHeight = blockHeight +
    COINBASE_SPENDABLE_DEPTH (120), not + COINBASE_MATURITY (100). At depth 100
    the block's own coinbase is still 20 blocks short of spendable, so the
    payment must come from unrelated mature coin and the pool runs a permanent
    float. Our own history contradicts itself here -- d6972a0 argued for
    COINBASE_MATURITY citing "96% of 934 payments", 44849e6 reversed it on the
    grounds that the measurement was taken while the node was alone on a private
    fork paying itself. HEAD's position is kept. This is a considered
    disagreement with upstream, not an oversight. The live production ledger
    confirms it: both entries have matureAtHeight - blockHeight = 120.

11. Comment above FlushMaturePayouts. Rewritten to describe the merged design.

12. FlushMaturePayouts, the central difference. THEIRS for the model. The ledger
    entry now survives on disk until every recipient has a txid, instead of
    being erased as soon as its payments were handed off -- the old ordering
    left a window where the debt was described by nothing at all, and a
    container restart on 2026-07-30 silently dropped ~1,156 BTF through it. Our
    gPaying is kept on top as the in-flight working set, because it carries the
    per-recipient retry and deferral state a per-round nextAttempt cannot
    express. It is explicitly a cache derived from the ledger, not a second
    record of debt: if it were lost the next flush would rebuild it.

13. PayoutThreadFn, 79 vs 70 lines. OURS for the loop and the reorg re-check,
    THEIRS for the per-recipient dedupe. The re-check on every attempt is
    load-bearing and upstream has no equivalent -- it decides once, at maturity,
    and a payment can sit through hours of retries during which the reorg that
    kills its funding block can land. Their paidTxids check is taken, against
    the ledger rather than against the batch. Our deferral policy is kept:
    waiting for coin is not a failure and must not consume the retry budget.

    Two fixes upstream does not have are folded in here rather than deferred to
    a follow-up commit:

    - FindRecordedPoolPayout runs once per record, on its first attempt, not on
      every attempt. It is a linear scan of mapWallet under cs_mapWallet, per
      recipient; upstream repeats it for every retry, which with 2000 rotating
      payout addresses and a budget of 1440 retries is a full wallet scan per
      address per minute for a day. The question it answers can only change
      across a restart, and every restart re-arms the flag.
    - The ledger file write no longer happens under gPayoutLedgerMutex in the
      per-recipient loop. SavePendingPayoutsLocked was split into a serialise
      step that needs the lock and a write step that does not, so the mutex is
      held only long enough to read the structure. The orphan scan in
      FlushMaturePayouts was likewise restructured so the ledger mutex is not
      held across PayoutBlockStillOnChain's cs_main acquisition.

    One case the merged model created and neither side handled: under
    survives-until-fully-paid, our GIVING UP path dropped the in-flight record
    but left the recipient owed in the ledger forever, so every later flush
    would explode it again -- a forfeit that silently becomes an infinite retry.
    ForfeitPayoutRecipient writes the amount off so the entry can retire.

14. QueuePayouts call site. Follow-on of 9.

15. ThreadRPCServer startup. BOTH. LoadPoolRounds added; LoadPaying and
    ReconcilePayouts stay, and stay before the payout thread starts.

    Crash safety, stated as the property that must hold: after kill -9 at any
    point, restart pays each recipient exactly once. Before SendMoney, the
    ledger has no txid and the payment is retried. Between SendMoney and the
    ledger write, the wallet holds the transaction and the first-attempt probe
    finds it. After the ledger write, paidTxids suppresses it. If the in-flight
    file is lost entirely, the ledger rebuilds it and the probe still covers the
    middle window.

src/nostr.cpp -- 3 hunks. THEIRS as the base, with two things restored from
ours. Theirs loads the Windows ROOT certificate store, which ours lacks and
genuinely needs, since OpenSSL's default verify paths are usually empty there
and our version would fail closed. But the plan's claim that theirs is a
superset of our 06bceb4 is FALSE: upstream's nostr.cpp contains no
min_proto_version, no TLS1_2, no SSL_OP_NO_*, and no set_options anywhere, so
taking it wholesale would have removed the TLS-version floor outright. Restored.
Second, their SSL_get_verify_result check sits after a successful handshake,
where with SSL_VERIFY_PEER set it is nearly dead code -- a bad certificate
aborts inside SSL_connect. The reporting belongs in the failure branch, which is
where the merge put it; their post-handshake duplicate was dropped. Our
relay-list and socket-pooling hunks auto-merged and were verified intact.

src/net.cpp -- 1 hunk. THEIRS. Their guard, hSocket == INVALID_SOCKET ||
pnode-&gt;hSocket != hSocket, subsumes ours and additionally catches descriptor
recycling, which ours did not; our version also referenced a pnode that only
existed in the select() loop. Our other db34011 hunk, nulling pnode-&gt;hSocket in
the poll-failure branch, auto-merged and was NOT re-applied -- plan section 7
Batch D says to re-apply it, and that instruction is stale. Two pieces of
residue the merge left behind were cleaned up: a comment still describing
select() semantics, and a pnode-&gt;hSocket != INVALID_SOCKET guard made redundant
by a continue two lines above it.

src/main.cpp -- 2 hunks. h1 BOTH, pure add/add adjacency between our
ParticipantHashThreadCount and their SecureClear forward declarations. h2 OURS,
and their handler discarded: 44004e3 no-ops checkorder/submitorder but keeps
market.cpp and market.h, whereas 11766be deletes the marketplace outright
including CRequestTracker, subscribe/publish and mapProducts. Ours is strictly
more.

src/main_gui.cpp -- 2 hunks. BOTH, additive on both sides. Our /payoutaddr,
/payoutfile and /hashthreads alongside their /stratumbridge and
/stratumbridgeport.

src/btfrv.cpp -- 1 hunk, not predicted by the plan. False adjacency: our
INVALID_SOCKET check and their #endif landed at the same offset. Ordering is
load-bearing -- our check goes inside the #else arm, before the #endif, or the
non-proxy build silently loses it. The BITFLASH_NODE_BUILD arm already returns
early, so both paths now have the check.

Makefile -- 2 hunks plus a hazard with no conflict marker. THEIRS for both
dependency-fetch recipes: checkout --detach FETCH_HEAD followed by test
"$(git rev-parse HEAD)" = "$(..._COMMIT)" asserts the pin actually landed, where
ours only checked it out. Everything of ours that they lack is kept: the
appimagetool 1.9.1 release pin with a sha256 check, where upstream fetches the
mutable "continuous" tag and executes it undigested, and the move of both the
tool and the AppDir out of /tmp, where any local user could pre-place an
executable and have it run as whoever runs make.

The hazard: both sides added a pin block in a different place, so git merged
both, outside any conflict region. SECP256K1_COMMIT and RANDOMX_COMMIT ended up
assigned twice, near L16 and near L44, and GNU Make takes the last := -- a naive
resolution would have silently built against our old secp256k1 pin while
appearing to adopt upstream's. The duplicate block was deleted by hand. Each of
the four variables is now assigned exactly once, and make -n resolves
SECP256K1_COMMIT to 7fecac74 with 6e2c8bc4 appearing nowhere.

Also applied here, from plan phase 4: SECP256K1_COMMIT moved to upstream's
7fecac74aed8e1fd9078380d67dd04663705c989 so signature validation matches the
binaries the rest of the network runs. secp256k1 was rebuilt from scratch
against the new pin on the build host and the checked-out dependency commit
verified. RANDOMX_COMMIT already matched at 1e9d4b2d and is unchanged.

src/Makefile, src/makefile.mingw -- 1 hunk each, both the HEADERS list. Ours
drops market.h, theirs adds proxy.h; the resolution does both. Taking theirs
wholesale would have left the Makefiles referencing a header git had already
deleted in our favour, silently, without a conflict.

Files the plan predicted would conflict and did not: net.h, main.h and db.cpp
auto-merged. Verified rather than assumed. net.h carries both our
BTF_MSG_WAIT_MAX_SECS work, including the constructor fix from 2d84982 that
starts nMsgWaitSince and nMsgWaitSize at 0, and their comment rewrite with
extern int nPeersWatched. The dead members that fix removed are gone from every
file.

Deviations from MERGE-1.2.18-PLAN.md.

- Section 6's mechanism was not used. Replayed onto the real merge base and
  merged, rather than cherry-picking onto the tag. Same result, 28 hunks
  instead of 51.
- Section 2 calls 09ff575 a superset of our 06bceb4. It is not; upstream has no
  TLS-version floor at all. Ours restored on top of theirs.
- Section 7 Batch D says to re-apply our db34011 line into their poll-failure
  branch. It auto-merged; re-applying would have duplicated it.
- Section 2 says do not take the stratum bridge. It is taken, and left off.
  fStratumBridge defaults false in main.cpp, the thread is only spawned when
  /stratumbridge is passed, and the listener binds 127.0.0.1 only. Excising it
  would cost real divergence and future conflict for no gain while it is
  disabled. Its defects -- select()/FD_SET on a node that can hold more than
  1024 descriptors, and an uncapped thread per connection -- are real only when
  it is enabled and are being reported upstream separately.
- Section 7's staged batches were not run as separate gated commits. The
  conflict surface is interleaved within single functions, chiefly the payout
  ledger, and splitting it would have meant committing states that do not
  build. The merge is one commit, as briefed.
- Batch F's two fixes are folded into hunk 13 rather than deferred, because
  they change the same lines the resolution rewrites.
- The plan did not anticipate the btfrv.cpp conflict, the duplicate pin block,
  the market.h trap in the two Makefiles, or the give-up/infinite-retry
  interaction created by adopting upstream's ledger model.
</content>
</entry>
<entry>
<title>mine: one pool session in participant mode, not one per core</title>
<updated>2026-08-23T04:37:36+00:00</updated>
<author>
<name>bitflash-maintainer</name>
<email>noreply@bitflash.invalid</email>
</author>
<published>2026-08-19T22:46:53+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=c4c19bd894c8fc30c558e560e326aab538b23170'/>
<id>urn:sha1:c4c19bd894c8fc30c558e560e326aab538b23170</id>
<content type='text'>
StartMinerThreads() sizes its pool with MinerThreadCount(), which is right for
solo mining -- each thread hashes a template of its own -- and wrong for
participant mode, where every thread entering BitcoinMiner() is a whole miner:
its own Stratum session, its own payout address off the rotation, its own pool
of /hashthreads workers. The two counts multiplied instead of dividing.

Measured on the pool box with /hashthreads=2: a container with no cpuset logged
"Mining with 11 thread(s) of 12 core(s)" -- 11 sessions, 22 hash threads on 12
cores, 1058% CPU, load 22.68 -- while the same binary under --cpuset-cpus 10,11
logged one session and 199%. The second miner (8 cores, /hashthreads=1) ran 7
sessions and went 74C to 89C. No code differed between those runs. Only the
cpuset did, which means the cpuset was holding the thread count down by
accident and every deployment without one was oversubscribed.

Participant mode now starts exactly one session, so /hashthreads means what it
says instead of being multiplied by the core count. The worker count moves to
ParticipantHashThreadCount(), next to MinerThreadCount() where the two are
easy to confuse, so what is printed at startup and what actually hashes cannot
drift apart -- and it IS printed now. It was only ever visible under -debug,
which is how eleven sessions ran unnoticed.

Same host, same cpuset (0-3), same arguments, measured as a delta over 20 s
more than four minutes after start:

  before  399% CPU, 6-7 threads hot, 3 sessions
  after   201% CPU, exactly 2 threads at 99.9%, 1 session

Solo mining is untouched: /gen without /participant still starts
MinerThreadCount() threads.

(cherry picked from commit 45bc9ed5bc132712092f3e7902fef746c6cae898)
</content>
</entry>
<entry>
<title>pool: refuse jobs when no peer has reported a height for 20 minutes</title>
<updated>2026-08-23T04:37:36+00:00</updated>
<author>
<name>bitflash-maintainer</name>
<email>noreply@bitflash.invalid</email>
</author>
<published>2026-08-11T20:52:09+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=3d7c30ebb6ac0c586635c693a40bc39180f539b9'/>
<id>urn:sha1:3d7c30ebb6ac0c586635c693a40bc39180f539b9</id>
<content type='text'>
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)
</content>
</entry>
<entry>
<title>payout: say what actually failed instead of blaming transaction size</title>
<updated>2026-08-23T04:37:36+00:00</updated>
<author>
<name>bitflash-maintainer</name>
<email>noreply@bitflash.invalid</email>
</author>
<published>2026-08-09T03:18:25+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=5abe59dd09e7dbc5795f5f6d9ac2ef303d418134'/>
<id>urn:sha1:5abe59dd09e7dbc5795f5f6d9ac2ef303d418134</id>
<content type='text'>
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 &gt; 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)
</content>
</entry>
</feed>
