<feed xmlns='http://www.w3.org/2005/Atom'>
<title>npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/src/btfrv.cpp, 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-03T21:49:35+00:00</updated>
<entry>
<title>merge: !8 fix/tunnel-retry into deploy/all-2026-08-24</title>
<updated>2026-09-03T21:49:35+00:00</updated>
<author>
<name>bitflash-maintainer</name>
<email>noreply@bitflash.invalid</email>
</author>
<published>2026-09-03T21:49:35+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=ea8fe94658bf20d2b669b619231238f12e6bd8fc'/>
<id>urn:sha1:ea8fe94658bf20d2b669b619231238f12e6bd8fc</id>
<content type='text'>
</content>
</entry>
<entry>
<title>tunnel: report which stage failed instead of guessing the cause</title>
<updated>2026-09-02T09:03:01+00:00</updated>
<author>
<name>bitflash-maintainer</name>
<email>noreply@bitflash.invalid</email>
</author>
<published>2026-09-02T09:03:01+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=7c70eb4498e88725241493033edfe89554ff1b18'/>
<id>urn:sha1:7c70eb4498e88725241493033edfe89554ff1b18</id>
<content type='text'>
</content>
</entry>
<entry>
<title>net: a refused thread spawn drops one peer, not the node</title>
<updated>2026-09-02T07:09:45+00:00</updated>
<author>
<name>bitflash-maintainer</name>
<email>noreply@bitflash.invalid</email>
</author>
<published>2026-09-02T07:09:45+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=0b566b4869b9e86c90162befd87f9882b6dd143c'/>
<id>urn:sha1:0b566b4869b9e86c90162befd87f9882b6dd143c</id>
<content type='text'>
std::thread throws std::system_error when pthread_create is refused
(RLIMIT_NPROC, a container's pids cgroup, memory pressure), and nothing
caught it, so the node died by std::terminate. Threads are spawned per
peer connection, so the pressure that triggers this scales with peer
count, which is attacker-influenced. Observed killing a healthy
participant worker mid-mining.

Every spawn site in the node build, and what a failure now does:

- btftunnel.cpp: both tunnel pumps are guarded. The inbound pump starts
  first because it owns cleanup of the pump end and rendezvous socket;
  if the second spawn is refused, shutdown() wakes the first out of its
  read and it cleans up itself -- closing those descriptors from the
  failure path instead would race it onto reused descriptors. Both
  paths return INVALID_SOCKET, which every caller already treats as
  "this connection failed".
- btfrv.cpp: the relay's accept loop drops the connection it cannot
  serve and stops accepting for a moment -- longer after consecutive
  failures, capped at 5s -- instead of dying at an arbitrary one. The
  per-pair second pump now runs on the handling thread itself, leaving
  exactly one spawn to guard and nothing running yet to race.
- main.cpp: a participant that cannot spawn its hash workers stops the
  ones that did start, drops the pool connection, and returns to the
  reconnect loop, the same way an ordinary disconnect does.
- randomx_pow.cpp: dataset init joins the workers it got, releases the
  dataset, and stays in light mode -- the same trade the allocation
  failure path above it already makes.
- rpc.cpp, net.cpp: the remaining unchecked _beginthread calls (which
  report failure by value, they cannot throw) now say precisely what
  did not start. The pool's accept and payout threads failing silently
  left the pool running deaf -- nobody accepts, nobody gets paid, and
  nothing in the log says why.

Left alone deliberately: the per-relay accept spawns (rpc.cpp) already
check, log and retry on the next poll; startup-critical spawns in
net.cpp keep failing hard; the selftest-only std::thread uses may abort
a selftest run, which is acceptable where dying over one peer is not.

Verified: full node rebuilds clean and links; pool-stratum selftest
passes; a scratch harness under RLIMIT_NPROC=1 confirms _beginthread
reports -1 and std::thread throws system_error, the premises of every
path above.
</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>pool: sync-gate job creation, retry failed payouts; widen nostr relay set</title>
<updated>2026-08-23T04:37:36+00:00</updated>
<author>
<name>bitflash-maintainer</name>
<email>noreply@bitflash.invalid</email>
</author>
<published>2026-08-08T02:29:26+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=6e4865c14c858d00c6cfa6967e66a86b31cd12f8'/>
<id>urn:sha1:6e4865c14c858d00c6cfa6967e66a86b31cd12f8</id>
<content type='text'>
- 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)
</content>
</entry>
<entry>
<title>net: add optional SOCKS5 outbound proxy</title>
<updated>2026-08-11T18:38:31+00:00</updated>
<author>
<name>Nsable-sh</name>
<email>nsable-sh@noreply.git.bitflash.network</email>
</author>
<published>2026-08-11T18:38:31+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=ff3829b28fd2ddc85a9a3bb4424cd5b4719854ab'/>
<id>urn:sha1:ff3829b28fd2ddc85a9a3bb4424cd5b4719854ab</id>
<content type='text'>
</content>
</entry>
<entry>
<title>diag: count sockets by where they were created, and whether the close took (#86)</title>
<updated>2026-08-01T16:10:23+00:00</updated>
<author>
<name>Bitflash</name>
<email>btf01@proton.me</email>
</author>
<published>2026-08-01T16:10:23+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=21c8bb63cb85f3001ddee9237b1d7e689675d293'/>
<id>urn:sha1:21c8bb63cb85f3001ddee9237b1d7e689675d293</id>
<content type='text'>
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.</content>
</entry>
<entry>
<title>rendezvous: bound the wait for a dial, so a dead path is noticed (#51)</title>
<updated>2026-07-30T16:38:12+00:00</updated>
<author>
<name>Bitflash</name>
<email>btf01@proton.me</email>
</author>
<published>2026-07-30T16:38:12+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=9f7141b28e2da9db7d7cb099d1f13f28742ff0fa'/>
<id>urn:sha1:9f7141b28e2da9db7d7cb099d1f13f28742ff0fa</id>
<content type='text'>
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 -&gt; 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.</content>
</entry>
<entry>
<title>rendezvous: register and wait for a dial as separate steps (#30)</title>
<updated>2026-07-29T16:28:50+00:00</updated>
<author>
<name>Bitflash</name>
<email>btf01@proton.me</email>
</author>
<published>2026-07-29T16:28:50+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=997e511e120272e8f2ddd29716b486e597410f02'/>
<id>urn:sha1:997e511e120272e8f2ddd29716b486e597410f02</id>
<content type='text'>
A node could not become reachable. RvServiceRegister connected, sent its
'S'+pubkey header, and then blocked reading the relay's 0x01 byte -- which the
relay only writes when a client pairs with the service. So the call did not
return when the node was registered; it returned when somebody had already
dialled it.

ThreadBtfAccept called BtfSetActiveRelay after that. The result is a deadlock:

  - to be dialled, a node must publish a descriptor naming its meeting node
  - to publish, BtfActiveRelay() must be set
  - it is only set once somebody has dialled

Before #26 this was masked. BtfActiveRelay() fell back to
vBtfMeetingRelays[0], so a node always published something, and ThreadBtfAccept
picks its relay at random -- so it worked whenever the two happened to match,
roughly one time in the number of relays. That is the flakiness this project has
been chasing: not a bad relay, an advertisement that usually named the wrong one.

#26 removed the fallback, which was right on its own terms and turned an
unreliable heuristic into a reliable deadlock. This is the other half of that
change and they belong together.

RvServiceRegister now returns as soon as the relay has the service listed.
RvServiceWaitPaired does the blocking read. ThreadBtfAccept registers, advertises,
then waits. RvClientConnect is untouched -- a dialer blocking until it is paired
is correct.

Registration failure also had no log line at all, which is why this stayed
invisible: a node that never registered looked identical to one waiting quietly
for a peer. There are now lines for registering, failing over, and being dropped.

Verified: a fresh node reports `registered at 90.156.222.107:8434, waiting for a
dial` and then self-resolves with `meeting_node=90.156.222.107:8434` -- the relay
it is actually on. Two nodes on one machine reach each other.</content>
</entry>
<entry>
<title>Merge PR #6 (large refactor), preserving main's fixes (#9)</title>
<updated>2026-07-28T15:55:04+00:00</updated>
<author>
<name>Bitflash</name>
<email>btf01@proton.me</email>
</author>
<published>2026-07-28T15:55:04+00:00</published>
<link rel='alternate' type='text/html' href='https://upleb.uk/npub1q9geyrch3feyczz242sxjaa05yczv50dlpug082sjln2kl0wrnxst0nrfa/bitflash/commit/?id=663784eae9b04ec71e3e1eebf445910e5a8add42'/>
<id>urn:sha1:663784eae9b04ec71e3e1eebf445910e5a8add42</id>
<content type='text'>
* Add Linux GUI installer via Wine

* Fix launcher creation and add BTF_RELAY env var support

* Pool server, Linux GUI, unified binary, README update

* Root Makefile: make linux / make windows, cleaner README

* Remove headless.cpp

* Fix XPM color count mismatch causing toolbar image errors on Linux

* XPM icons: same code on Windows and Linux, no ifdefs around image loading

* Fix GTK icon crash and Linux printf format

* btfrv: 10s socket timeout on relay connects

* Faster peer discovery: more dials per pass, shorter backoff and sleep

* Parallel btf peer connections

* Fix GetMyExternalIP - was disabled, now uses modern IP echo services

* Fix tunnel disconnect on idle and restore external IP detection

* Filter stale Nostr descriptors

* Sort peers by freshness, fresh first

* Pool: add .btf Stratum listener — miners connect via rendezvous, no port forwarding needed

* btf pool: self-replacing listeners, one per relay, no queuing between miners

* Mining modes: solo/operator/participant — pool server on-demand, participant Stratum client

* Status bar: show external IP and .btf address

* README: clean rewrite, concept-first, minimal

* make linux produces AppImage

* make linux produces AppImage

* Pool dashboard: connect instructions, tabs, share bars

* Perf fixes: GetMyExternalIP threaded, btf listener counted

* Perf fixes: GetMyExternalIP threaded, btf listener counted

* Fix pool payouts: defer until coinbase maturity

* Pool: clean mode switching, IP warning

* Pool: clean mode switching

* Fix gPoolRunning: remove static

* AppImage: use real icon from bitcoin.ico

* Fix exit crash; Mining menu

* Fix exit crash; Mining menu

* Fix exit crash; Mining menu

* Fix exit crash; Mining menu

* Fix exit crash; Mining menu

* Windows build: use mingw32-make

* Windows build: simpler make invocation

* Add deps-windows target

* Windows build fixes

* Windows build fixes

* Windows makefile: fix obj dir

* Windows: output to repo root

* Windows: bundle DLLs into zip

* Replace wxWidgets with ImGui+GLFW: same build on Linux and Windows

* Fix %I64d that slipped back through rebase (main.h, net.h)

* MinGW: add _mkdir shim for non-MSVC Windows; fix %I64d

* Fix gPoolRunning extern outside ifndef _WIN32

* Remove -lboost_system: boost is header-only in this codebase

* Status bar: copy buttons for IP and .btf address

* Windows: add app icon via windres, strip wx stuff from rc file

* Clean GUI: single Options menu item, bigger font, better spacing, cleaner options dialog; update README

* GUI: embed Roboto font at 16px for clear readability on all platforms

* Windows: windres depends on obj dir target

* Fix balance hidden off-screen: put on own line below address bar

* Windows: inline mkdir in every rule, remove dir target that confused make

* Windows: move all target to top so it's the default rule

* Fix pool server not starting: always start when operator selected, re-sync dialog state on open

* Write debug.log to data directory (AppData/Roaming/Bitflash on Windows)

* Participant miner: use wallet address as username so payouts go to wallet

* Add .gitattributes to lock LF line endings, prevent CRLF drift

* Refactor pool mining to btf-only transport

* Persist pool settings and startup wiring

* Prune stale peers and expose pool queries

* Fix build regressions and add CLI mode/arg parity

* Fix rpc miner stats declaration order

* Centralize pool runtime extern declarations

* Separate operator/participant dashboards and restore peer stats

* Add live telemetry strip and clearer per-mode runtime metrics

* Reduce mining dashboard clutter and remove manual pool entry

* Count only authorized pool miners

* Seed initial pool job on startup

* Use a real share target for pool work

* Switch pool accounting to round-based shares

* Fix nostr pool announcer stats call sites

* Fix pool stats definition order

* Improve pool worker status display

* Add mutex include for participant status

* Replace ImClamp in dashboard layout

* Clarify participant work status

* Log participant status transitions

* Reduce peer discovery log noise

* Add participant status badge

* Track participant auth acknowledgements

* Log and validate pool job delivery

* Stop persisting mining mode/pool settings across restarts

LoadWallet() runs after CLI flags are parsed in main_gui.cpp, and was
restoring nMineMode/strParticipantPool/strPoolName/strPoolDashboardUrl/
dPoolFeePercent/fGenerateBitcoins from wallet.dat every time, silently
overwriting whatever was just set on the command line or in the current
Options session -- no error, no log line. This is why pool name/address
could show correctly in the UI but stay stale in the logs (UI held the
in-memory value just set; logs eventually reflected whatever LoadWallet
clobbered it back to), and why mode changed behavior across restarts.

Mode/pool config is now decided fresh every launch: CLI flags if given,
otherwise the compiled-in default. Nothing is written back to wallet.dat
for these settings anymore, so there's nothing left to go stale or fight
over load order.

* Add explicit Relay mode as the new startup default

4th mining mode: node runs/syncs, never mines, no pool server -- an
explicit option instead of an implicit side-effect of 'Solo + never
click Start Mining'. Now the compiled-in default (was Solo).

- main.h: MINE_RELAY = 0, renumbered other modes (nothing compares
  nMineMode against raw integers elsewhere, verified).
- main.cpp: default nMineMode = MINE_RELAY; BitcoinMiner() early-returns
  for Relay, mirroring the existing Participant-mode guard.
- gui.cpp: added Relay radio button, status label, and telemetry-strip
  text. Start/Stop Mining button is hidden for Relay (meaningless -- it
  never mines by definition); OK forces fGenerateBitcoins off if you
  switch into Relay while previously mining.

* Announce pool changes immediately instead of waiting up to 60s

ThreadBtfPoolAnnouncer re-read strPoolName/dPoolFeePercent/etc live, but
only once per iteration of its 60s sleep loop, so a pool name/fee change
in Options could take up to a minute to reach other nodes.

- nostr.h/.cpp: add gAnnounceNow flag; the announcer's sleep loop now
  wakes early and publishes immediately when it's set, then clears it.
- gui.cpp: Options OK handler sets gAnnounceNow when operator pool
  settings actually changed (name/dashboard/fee/mode), so a saved
  change is announced right away instead of on the next tick.

* Give each pool miner a distinct extranonce2 so jobs no longer overlap

mining.notify previously broadcast one identical 80-byte header (baked
from a single server-side extraNonce) to every connected miner, so any
two miners on the same job searched the exact same nNonce space instead
of disjoint ranges.

Each Miner connection is now assigned a unique extranonce2 on connect.
BuildMinerBlock() takes the shared job template and appends that miner's
extranonce2 to the coinbase scriptSig, recomputing the merkle root, so
every miner gets its own header variant of the same job. mining.submit
validates and, on a full block, assembles the block using that same
miner-specific variant (via m-&gt;extranonce2), not the shared template.

No client-side protocol change needed: main.cpp's participant miner
already just takes whatever header bytes mining.notify hands it and
submits (jobId, nonce) back, so it works unmodified against per-miner
headers.

Still simplified vs standard Stratum (no coinb1/coinb2/merkle-branch
wire split, no mining.extranonce.subscribe reply payload) since this
protocol was already a custom simplification pre-existing this fix.
Syntax-checked with g++ -fsyntax-only (full link needs secp256k1/
RandomX/GLFW not present in this sandbox).

* Fix debug.log never being written on the real Windows build

printf-to-debug.log routing was gated on:
    #if defined(_WINDOWS) || !defined(_WIN32)
_WINDOWS is only ever defined by the legacy, unused MSVC makefile.vc.
The actual Windows build (makefile.mingw) never defines it, and _WIN32
IS defined there (mingw builtin), so on real Windows this condition was
always false || false = false. printf() fell through to plain libc
printf (stdout only), so debug.log was never created, and output was
only visible if the process happened to be run attached to a console.

OutputDebugStringF() was already platform-safe internally (always
writes debug.log; only echoes to console when not on Windows), so the
outer condition was pure dead weight. Made the routing unconditional.

* Reduce base UI font size one more step (16px -&gt; 14px)

* Remove Start/Stop Mining button; mining state follows selected mode

The button let a user configure a pool then click a separate toggle,
creating states where the button's label/action didn't match what the
selected mode actually needed. OK now starts/stops the miner thread
itself based on nMineMode: Relay clears fGenerateBitcoins, the other
three modes set it and spawn ThreadBitcoinMiner if it isn't already
running (guarded by vfThreadRunning[3]).

* Fix participant miner stuck waiting for notify after buffered multi-line recv

Root cause of the operator/participant pairing successfully but no work
ever flowing: PoolParticipantMiner()'s receive loop only pulled one line
out of the socket per select() wakeup. The server sends the authorize
reply, mining.set_difficulty, and mining.notify back-to-back on the same
connection, and these often land in a single TCP segment — one recv()
inside StratumRecvLine() buffers all three lines into 'buf', but only the
first was extracted and processed. The remaining lines (frequently
mining.notify itself) sat in 'buf' indefinitely, because select() only
reports readability of the OS socket, not whether our own buffer still
holds unprocessed complete lines — and the server had nothing further to
send once the initial burst was over, so select() never fired again.

Fix: drain any complete line already sitting in 'buf' first; only fall
back to select()+recv() when 'buf' has no complete line left.

* Fix mining.submit rejecting every share due to wrong param-count check

Server required params.size() &gt;= 4 for mining.submit, but the client
only ever sends 3: [username, jobId, nonceHex] (indices 0-2, matching
what the handler actually reads: p[1]=jobId, p[2]=nonceHex). Every
submission was bounced with 'bad params' before its nonce was ever
checked against the job, producing an endless share-sent/share-rejected
loop with no other visible cause. Likely a leftover from the standard
5-field Stratum mining.submit shape, which this simplified 3-field
protocol never matched. Threshold corrected to size() &gt;= 3.

* Fix mining.submit rejecting legitimate shares that weren't also winning blocks

Pool share difficulty is intentionally ~256x easier than full network
difficulty (MakeShareTarget() shifts the block target left 8 bits), so
miners are expected to submit many shares per actual block found. But
the handler replied to the miner based on ProcessBlock()/CheckBlock()
success — validation against the much harder network target — rather
than on whether the share met pool difficulty. Every legitimate share
that wasn't also a winning block (the vast majority, by design) failed
CheckBlock()'s nBits check and got reported to the miner as rejected.

Now: reply(true) as soon as a submission clears pool-share difficulty.
Only if it ALSO clears full network difficulty (job.target, not
job.shareTarget) does it get submitted as a block — with no second
reply sent, since the share was already acknowledged.

* Fix dead-peer dial priority: freshness sort was undone by full shuffle

ConnectDiscoveredBtfPeers sorted candidates newest-first, then immediately
shuffled the whole list, making the freshness sort a no-op — every pass was
fully random. Meanwhile g_btfPeerFails was tracked but never used to
prioritize, so a peer that had failed 20 times in a row got equal footing
with an untried or previously-successful peer; the only effect was backoff,
capped at 15 minutes, so dead descriptors cycled back into the dial pool
constantly.

- Track successful connections (g_btfPeerEverConnected) and use it, plus
  fail history, to order candidates in tiers: known-good, then untried,
  then known-bad (worst offenders last). Freshness order is preserved
  within each tier instead of being discarded.
- Raise the backoff cap for chronic failures (6+ in a row) from 15 minutes
  to 6 hours, so descriptors that are clearly dead stop consuming dial
  budget every pass while still getting retried eventually.

* Batch-resolve .btf descriptors to stop hammering public relays during dial passes

Root cause of 'stuck at 1 peer' despite good peer discovery: each parallel
dial thread in ConnectDiscoveredBtfPeers called ConnectNodeBtf, which itself
walked all configured relays (relay.damus.io, nos.lol, relay.nostr.band,
relay.snort.social) to resolve that one peer's descriptor. With up to
BTF_DIALS_PER_PASS (12) threads dialing at once, a single pass could fire up
to 12 * nNostrRelays near-simultaneous fresh connections at the same handful
of public relays -- easily enough to trip rate limits or get handshakes
refused (exactly what the logs showed: 'handshake refused by relay.damus.io',
repeated connect failures to relay.nostr.band / relay.snort.social). Peers
that were genuinely reachable looked dead simply because their descriptor
couldn't be fetched under that load, and then got penalized as known-bad by
the dial-priority tiering from the previous fix.

- Add BtfResolveMany: resolves every still-unresolved candidate's descriptor
  in a single Nostr REQ per relay (multiple authors in one filter), instead
  of one relay connection per candidate. A 12-candidate pass now costs at
  most a few relay round-trips total instead of dozens.
- Add ConnectNodeBtfResolved / ConnectNodeBtfTail (net.cpp): given an
  already-resolved descriptor, dial straight to the peer's rendezvous node
  with zero further relay contact. ConnectDiscoveredBtfPeers now batch-
  resolves all candidates up front, then fans out tunnel-only connect
  threads in parallel -- no thundering herd against the relays.
- Candidates that fail to resolve are recorded as a failure immediately
  (same backoff/fail-count bookkeeping as before) instead of spawning a
  thread with nothing to dial.

* Stop reconnecting to relays constantly: pool one persistent connection per relay

Real root cause of the ongoing 'handshake refused' / 'connect failed' spam
and peer count stuck at 0-1: every relay-touching operation (startup self-
test, per-pass seeding, batch descriptor resolve, pool announce, pool query)
opened its own brand-new CWebSocket every single time it ran. Two independent
threads (ThreadNostrSeed and ThreadBtfPoolAnnouncer) each reconnect from
scratch on their own ~60s cycle, so a node could fire well over a dozen
fresh TCP+TLS+WS handshakes at the same 4 public relays inside any given
minute. Public relays -- relay.damus.io especially -- rate-limit new
connections per source IP fairly aggressively, so this node was effectively
DoS-ing itself: peers were genuinely reachable, but their descriptors
couldn't be fetched because the relay had already started refusing this
node's own repeated reconnects.

- Add a small per-relay connection pool (g_relayPool) and an RAII
  CRelayLease that reuses a relay's live socket across calls instead of
  reconnecting every time, only reconnecting when a socket actually drops
  or was never established.
- Route every relay call site through it: the startup self-test, the
  self-test loop, SeedFromRelay, BtfResolve, BtfResolveMany,
  BtfPublishPoolAnnouncement, BtfQueryPoolAnnouncement.
- Only log 'connected to &lt;relay&gt;' on an actual fresh (re)connect, not on
  every pass that's just reusing an already-live socket, so the log
  reflects what's really happening instead of implying constant churn.

This cuts fresh relay connections from several per minute down to
essentially one per relay for the life of the process (until a relay
actually drops the connection), which should make the rate-limit-driven
refusals go away and let descriptor resolution -- and therefore dialing --
actually succeed at the rate the peer list implies it should.

* Fix wallet crash on rejected send; remove legacy IP-based peer discovery

- SendMoney() no longer throws when a committed tx is rejected by
  AcceptTransaction() (e.g. raced input already spent). It now unwinds
  the wallet state via RollbackTransactionSpent() and returns false,
  which the GUI already handles as 'Transaction failed.'
- Removed the dead IP-based peer discovery/connect path now that
  discovery is entirely Nostr/.btf: CAddrDB/addr.dat, mapAddresses,
  AddAddress, ConnectNode(CAddress), FindNode(CAddress), ConnectSocket
  (SOCKS4 proxy helper), addrProxy, ThreadOpenConnections/2 class-C
  dialer and its thread spawn, the addr/getaddr P2P handlers, and the
  dead vAddrToSend/setAddrKnown fields on CNode.
- Deleted irc.cpp/irc.h (already-disabled legacy seed code) and
  scrubbed references from headers_core.h and all four makefiles.

* Raise peer connection target and add dial diversity; fix IPv4-only relay connect

- BTF_TARGET_CONN: 8 -&gt; 24. With ~100 known .btf peers, average degree
  needs only to exceed ln(N)~=4.6 for the network to stay connected
  with overwhelming probability (Erdos-Renyi); 24 is a healthy margin.
- ConnectDiscoveredBtfPeers now splits each pass's dial budget ~70/30
  between the existing reliability-tiered order and a random sample
  from the full fresh peer pool. Pure reliability bias means every
  node converges on the same 'known good' hub set, which correlates
  peer sets across the network -- a relay outage then drops the same
  edges everywhere at once instead of independent ones. The random
  slice keeps a floor of uncorrelated edges.
- CWebSocket::Connect (relay websocket) now resolves with AF_UNSPEC
  and tries every returned address (IPv6 or IPv4) in turn instead of
  hard-failing on AF_INET-only resolution, which was one plausible
  cause of the relay.nostr.band:443 connect failures (CDN-fronted
  relays increasingly rate-limit/filter the v4 path).

* Remove accidentally committed build binary; ignore src/bitflash

* Fix pool participants getting stuck after the first job

JobWatcherFn only rebuilt/broadcast a new Stratum job when a new block
arrived on this node or no job existed yet. Once a job was built after
startup, gHaveJob stayed true forever unless *this* node found the
block, so with no new block (slow interval, or this node just not
seeing one) every connected miner sat hashing the exact same job --
stale timestamp, stale mempool snapshot -- indefinitely. From the
miner's side that's indistinguishable from being stuck: it worked at
connect (gets the one-time job sent on authorize) and then nothing
ever arrives again.

Added a 60s periodic refresh so the job (and therefore mining.notify)
gets rebroadcast on a timer too, same as a normal stratum pool, not
only on new-block/no-job events.

* Diversify Nostr discovery relay list

Went from 4 relays to 7, and reduced Cloudflare concentration: previously
2 of 4 (damus.io, snort.social) sat behind the same CDN, so a Cloudflare-
side issue could take out half the list even though they're nominally
independent relays. Kept damus.io (most established) as the one CF-fronted
entry and filled the rest with different hosts (nos.lol, nostr.band on
Hetzner, nostr21.com, offchain.pub, nostr.mom, primal.net) so peer
discovery doesn't have a single shared upstream as a point of failure.
7 relays is within the commonly recommended 5-10 range for redundancy.

* Fix data race in RebuildJob: chain-state reads weren't holding cs_main

pindexBest, nBestHeight, GetNextWorkRequired() and GetMedianTimePast()
were all read without cs_main. A block being accepted concurrently by
the P2P thread could interleave with this: pindexPrev captured from the
old best block, but nBestHeight already bumped to the new one by the
time job.height is computed, or pindexBest reassigned mid-read. That
mismatch would make QueuePayouts() schedule a payout for the wrong
maturity height for whatever block gets found against that job.
Wrapped the whole chain-state snapshot in one CRITICAL_BLOCK(cs_main),
consistent with how the rest of the codebase touches this state.

* add operator fee

* Add categorized, burst-collapsing logging for pool/worker/payout flow

debug.log was growing unboundedly because every printf() call wrote
unconditionally, so loops that log once per item (discovered peers,
connect attempts) produced a near-identical line per address with no
cap. Add LogPrint(category, ...): a drop-in printf replacement that
tags each call with a subsystem (pool/worker/payout/net/nostr) and
collapses repeated calls from the same call site into one line per
3s window plus a '(xN similar in 3s)' summary, so log growth is
bounded by elapsed time instead of event count.

Wire it through the whole pool -&gt; work -&gt; payout pipeline (rpc.cpp
operator side, main.cpp participant/worker side, net.cpp/nostr.cpp
peer &amp; relay discovery), and along the way fix several previously
*silent* failure points that produced exactly the reported symptom
(worker stuck asking for work, nothing logged):

- Relay listener registration failing: no log at all, so a pool with
  every relay down looked identical to a healthy idle pool.
- No relays configured: silent infinite 5s retry loop.
- Initial job seed failing at pool startup: silent.
- JobWatcher: added a stuck-job watchdog that, after 15s with no
  job available, logs a specific diagnosis (no chain tip yet vs.
  RebuildJob() failing) instead of retrying forever in silence.
- QueuePayouts(): a block found with zero recorded shares queued no
  payout and logged nothing.
- FlushMaturePayouts(): an unparseable payout address was skipped
  with no log.
- Participant miner: the 'waiting for pool notify' stall was only
  ever shown as a transient GUI status string, never written to
  debug.log. Now logs on a 15s cadence while stalled, and after 120s
  proactively drops the tunnel so the existing reconnect-in-10s loop
  gets a fresh attempt instead of hanging indefinitely.
- Participant miner: share rejections were only a transient GUI
  status string; now logged with the pool's rejection reason.

All edited files verified with g++ -fsyntax-only (gnu++14).

* Fix two bugs causing worker stuck + log bloat

1. Worker stuck asking for work (main.cpp):
   The participant miner checked msg["result"].is_boolean() to detect
   the mining.subscribe acknowledgement (id==1), but the pool sends
   an array ([[["mining.notify",sid],sid,4]]) as per Stratum spec,
   not a boolean. is_boolean() always returned false, so gotSubscribeAck
   was never set true. This didn't directly block authorize (id==2 path
   is independent), but it did mean the status never advanced through
   "subscribed", leaving the progress logging misleading. More
   importantly, the same wrong assumption meant any pool that also
   gated job dispatch on a prior subscribe confirmation would leave the
   worker stuck forever with no diagnosis.
   Fix: accept any non-null result for id==1 (the array is the
   session/extranonce info; its contents don't matter for our use case).

2. Log bloat from ProcessMessages (main.cpp):
   ProcessMessages() used a bare printf() which fired unconditionally
   for every P2P message received -- the bulk of debug.log was this one
   line repeating thousands of times. Convert to LogPrint("net", ...)
   so it is gated behind fDebug (off by default) and burst-collapsed
   by the dedup introduced in the previous commit.

* Fix log bloat, em-dashes, dead code, status bar, Connected/Detected peers

Log bloat:
- Gate all per-message/per-block/per-loop printf in main.cpp, net.h,
  net.cpp behind LogAcceptsCategory("net") -- only visible with -debug.
  This includes: received:/sending: hex dumps, got inventory, askfor,
  getblocks, getdata, received block, AddToWallet, RelayWalletTransactions,
  ProcessMessage FAILED, mining loop N-transactions line, retarget block,
  socket recv/send errors. AddToBlockIndex new best and ProcessBlock
  ACCEPTED intentionally left always-on (one line per block).
- PoolParticipantMiner: status -&gt; now LogPrint(worker) not printf.

Em-dashes:
- Replace all UTF-8 em-dashes (0xE2 0x80 0x94) with ASCII '--' in
  every source file (rpc.cpp, gui.cpp, main.cpp, main_gui.cpp, net.cpp,
  nostr.cpp, util.cpp, btfrv.cpp, db.cpp, db.h, main.h, headers.h,
  headers_core.h, util.h). 40 occurrences across 14 files.

Dead code:
- Remove unused gJobSeq atomic from rpc.cpp (written, never read).
- Fix minerReward: was computed but payout loop used blockReward directly
  (operator fee was advertised but never deducted). Now payout correctly
  uses minerReward = blockReward * (1 - feeFrac).

Status bar / peers:
- Add GetDiscoveredPeerCount() to nostr.cpp/nostr.h (reads g_btfPeers
  size under cs_btfPeers).
- Replace scrollable inline status text with DrawStatusBar(): a separate
  ImGui overlay window pinned to bottom of screen (NoScrollbar,
  NoBringToDisplayFront). Labels changed from 'Peers N' to
  'Connected peers: N  Detected peers: N'.

* Rewrite gui.cpp: proper fixed status bar, scrollable content child

Layout architecture:
- DrawMainWindow() covers the full display width, height minus STATUS_BAR_H
  (22px). NoScrollbar + NoScrollWithMouse on the outer window so the
  chrome (menu, address/balance strip, telemetry strip, separators) never
  scrolls.
- A BeginChild("##scroll") fills the remaining height after the fixed
  chrome, sized to GetContentRegionAvail().y -- which is already the space
  between the cursor and the bottom of the outer window. All mode-specific
  content (transaction table, participant pool list, operator workers/payouts)
  lives inside this child. Content that overflows gets a scrollbar; nothing
  bleeds past the outer window edge.
- DrawStatusBar() is a separate overlay window pinned to the bottom 22px,
  drawn last so it is always on top. NoBringToDisplayFront is NOT used here
  -- drawing last is sufficient and avoids interaction issues with dialogs.
- Status bar background is fully opaque (not alpha-blended) so it never
  shows content behind it.

Labels and content:
- 'Peers N' replaced with 'Connected peers: N  Detected peers: N'
  using GetDiscoveredPeerCount().
- Mining indicator: '* Mode' instead of the 'filled circle' Unicode glyph
  that renders as boxes on some Linux font configs.
- FmtAge used consistently; 'blocks deep' shortened to 'N deep' in tx list.
- Options dialog: em-dashes removed from RadioButton labels (ASCII '--').
- Removed usedY dead variable (GetCursorPosY() result was unused).

* Fix blocking bugs, dead code, pool/participant flow correctness

Bugs fixed:

StratumRecvLine blocking (main.cpp):
  The participant miner's recv loop called recv() with no timeout after
  select() said the socket was readable. If the server sent a partial
  line (split across two TCP segments), the second recv() would block
  indefinitely waiting for the rest -- the classic 'stuck asking for
  work' hang even though the pool was alive and had sent data.
  Fix: single non-blocking recv() pass (MSG_DONTWAIT on Linux,
  FIONREAD+recv on Windows). If no complete line is in the buffer yet,
  return false; the outer select() loop retries when more data arrives.

BroadcastJob holding mutex during send() (rpc.cpp):
  BroadcastJob() held gMinersMutex across all SendLine() calls.
  send() can block if a slow miner's TCP receive buffer is full.
  With the mutex held, MinerThreadFn could not acquire it to remove a
  disconnecting miner, causing a deadlock-adjacent stall: broadcast
  blocked on slow miner, disconnect blocked on broadcast, new jobs
  not delivered. Fix: snapshot {fd, authorised, address, extranonce2}
  under the mutex, release it, then send to each fd outside the lock.

Dead code removed (rpc.cpp):
  - gRpcUser, gRpcPassword: defined, never read or set by anything.
  - dHashesPerSec, nHPSTimerStart: leftover Bitcoin 0.1 stubs, unused.
  - IsInitialBlockDownload(): stub that returned pindexBest==NULL,
    never called anywhere in this codebase.
  - struct Share: declared with three fields, never instantiated.
  - Duplicate comment block before payout section.

Other cleanup (main.cpp):
  - STUCK_NO_JOB_TIMEOUT_SECS moved from inside the loop body to file
    scope (was 'static const' inside a loop -- correct but confusing).
  - pblock-&gt;print() in solo miner gated behind LogAcceptsCategory("net")
    -- was unconditionally dumping a full block to stdout on every solve.
  - ERROR in BitcoinMiner printf converted to LogPrint("net").

* Pool/participant: fix recv blocking, BroadcastJob mutex, listener spawn, escaped em-dash

main.cpp -- StratumRecvLine rewrite:
  The previous version called recv() in a while loop -- if select() said
  the socket was readable but only a partial line had arrived, the second
  recv() call would block indefinitely waiting for the rest of the line.
  This was the root cause of the recurring stuck-worker bug.
  Rewrite: single non-blocking recv (MSG_DONTWAIT on Linux, FIONREAD+recv
  on Windows). If no complete newline is in the buffer yet, return false
  immediately; the outer 50ms select() loop retries when more data arrives.
  This means the hash loop always stays responsive regardless of network
  fragmentation.

rpc.cpp -- BroadcastJob mutex held across send():
  BroadcastJob held gMinersMutex for the entire duration of sending jobs
  to all connected miners. send() can block if a slow miner's TCP buffer
  is full. With the mutex held, MinerThreadFn could not acquire it to
  clean up a disconnecting miner, stalling the whole broadcast.
  Fix: snapshot {fd, authorised, address, extranonce2} under the mutex,
  release it, then send to each fd without holding any lock.

rpc.cpp -- LaunchThread failure not handled in BtfStratumAcceptOneFn:
  The replacement listener thread was launched via LaunchThread() which
  wraps _beginthread() but discarded the return value. If _beginthread
  fails (thread limit, OOM), the listener count stayed at 1 with no thread
  behind it -- that relay would get no new listener until the 30s outer
  loop in BtfStratumAcceptFn fired. Fix: call _beginthread() directly,
  check the return value, decrement and log on failure.

rpc.cpp -- escaped em-dash in log string:
  BtfStratumAcceptFn had a literal \xe2\x80\x94 in a LogPrint format
  string that survived the em-dash sweep (it was source-encoded, not a
  UTF-8 byte in the file). Replaced with '--'.

* Rewrite pool server: single event loop, no per-miner threads

Replace the old architecture (one thread per connected miner + separate
JobWatcher thread + BtfStratumAcceptFn thread with listener count tracking)
with a clean two-layer design:

Accept layer (one thread per relay, AcceptOneFn):
  Each relay gets exactly one persistent thread. It loops: calls blocking
  RvServiceRegister (waits for a miner to knock), then BtfServiceWrap
  (echan handshake), then pushes the finished plain socket to a small
  lock-protected queue, then loops immediately to accept the next miner.
  No listener count tracking. No replacement thread spawning. No races
  around spawn failure. The relay always has exactly one pending accept.

Event loop (PoolEventLoop, runs on the ThreadRPCServer thread):
  Single thread owns all connected miner sockets. Each tick:
  1. Drain the accept queue -- pick up any newly-connected miners.
  2. Check for a new block -- rebuild job and broadcast if so.
  3. No-job watchdog -- log and retry if job is missing too long.
  4. Periodic job refresh -- restamp nTime every 60s.
  5. select() over all miner fds with 50ms timeout.
  6. For each readable fd: non-blocking RecvLine, drain all complete
     lines, dispatch each to HandleLine, collect dead fds.
  7. Refresh GUI stats snapshot every 2s.

Because the event loop is the only writer to gMiners, no mutex is needed
on the miner list. The only mutexes remaining are:
  - gStatsMutex: guards the GUI-visible snapshot (StatsSnapshot,
    gWorkerStats, gPayoutViews) written by the event loop and read by
    GUI threads via GetPoolOperatorStats/GetPoolWorkerStats/GetPendingPayouts.
  - gReadyMutex: guards the accept queue between AcceptOneFn threads
    and the event loop.

Other improvements in this rewrite:
  - gPendingPayouts is event-loop-only (no mutex needed).
  - Round share tracking lives in the event loop (roundShareCount map +
    roundShareTotal counter) -- zeroed on block found, no per-miner stat
    mutex needed mid-round.
  - QueuePayouts takes the share snapshot as a parameter instead of
    reaching into gMinerStats under a separate mutex.
  - BroadcastJob no longer snapshots miners to avoid a mutex -- it runs
    directly in the event loop with exclusive access to gMiners.
  - AcceptOneFn backs off 30s on relay registration failure instead of
    returning -- one relay going down doesn't kill the accept thread.
  - No goto. No per-thread dynamic allocation for listener contexts on
    each accepted connection.

* Thorough logging and correctness fixes across the full pool/participant flow

Every failure point in the operator-&gt;miner-&gt;payout chain now logs exactly
what broke, with enough context to diagnose without a debugger.

Operator side (rpc.cpp):

RebuildJob: logs 'no chain tip (pindexBest is null)' on failure instead
of silently returning false.

HandleLine:
  - JSON parse errors logged with the raw line (first 80 chars)
  - subscribe: logs fd and session id
  - authorize: logs address, worker, and whether the address is valid
  - submit: logs stale-job case with submitted vs current job id;
    logs high-hash with actual hash and target values;
    logs valid shares with nonce and running session count;
    logs BLOCK CANDIDATE separately from BLOCK ACCEPTED/REJECTED so
    a ProcessBlock failure is unambiguous
  - unknown method logged (reveals miner software quirks)
  - reply() logs send failures with method name

QueuePayouts: logs total payout amount, miner count, fee applied, and
how many miners were below dust threshold.

FlushMaturePayouts: logs 'insufficient funds or wallet locked?' on
SendMoney failure instead of just 'FAILED'.

BroadcastJob: logs job id and miner count on every broadcast.

Event loop: logs when each new miner socket enters the loop; logs
disconnect with session and round share counts.

No-job watchdog: RebuildJob retried every 5s (not every 50ms tick),
eliminating cs_main hammering when there is no chain tip.

AcceptLoopFn: re-polls BtfAllRelays every 60s so relays discovered
after startup are picked up; logs each new relay thread spawned.

AcceptOneFn: logs relay name on start/stop; logs BtfServiceWrap
(echan handshake) failure separately from RvServiceRegister failure.

RecvLine: disconnected out-param distinguishes 'socket closed' from
'no complete line yet', preventing incorrect disconnection on partial recv.

Participant side (main.cpp):

StratumRecvLine: same disconnected out-param fix as RecvLine in rpc.cpp.
Previously a false return could mean either 'disconnect' or 'no data yet',
causing spurious reconnects when a partial line arrived.

PoolParticipantMiner rewritten with step-by-step logging:
  Step 1 resolve: logs specific failure (no descriptor vs bad format)
  Step 2 tunnel:  logs relay and failure reason
  Step 3 address: warns explicitly if payout address cannot be determined
  Step 4 subscribe/authorize: logs each send and each response
  Step 5 RandomX: logs VM mode
  Step 6 main loop:
    - malformed mining.notify (wrong param count or short header/target)
      logged with sizes instead of silently ignored
    - authorize rejected: logs reason, does not disconnect (some pools
      reject bad addresses but still send work)
    - share accepted/rejected: logs share id and rejection reason
    - unknown server method logged
    - pool disconnect logged with handshake state at that moment
      (gotSubscribeAck, gotAuthorizeAck, haveJob) so you know exactly
      how far the connection got
    - nonce space exhausted logged (was silent before)
    - on exit: logs total hashes, shares sent, shares accepted

* Fix build error: ImGuiWindowFlags_NoBringToDisplayFront -&gt; NoBringToFrontOnFocus

NoBringToDisplayFront does not exist in this version of ImGui (v1.89.x).
The correct flag is ImGuiWindowFlags_NoBringToFrontOnFocus (bit 13).
Used in two places: ##statusbar and ##main windows.

Also remove dead variable usedY (GetCursorPosY() result assigned but
never read) and replace the confusing 'ImGuiWindowFlags_NoScrollbar * 0'
expression (evaluates to 0 but looks like a mistake) with a plain 0 in
the BeginChild call.

* Remove duplicate status logging from SetParticipantMiningStatus

Every transition in PoolParticipantMiner already has a direct LogPrint
with full context (addresses, job ids, nonces, error reasons). The
LogPrint inside SetParticipantMiningStatus was firing on top of those,
producing redundant 'PoolParticipantMiner: status -&gt; ...' lines after
every meaningful log entry. SetParticipantMiningStatus now just updates
the GUI string silently.

* Implement vardiff: stable share rate instead of fixed difficulty

Previously the pool set shareTarget = blockTarget &gt;&gt; 8 for every miner
forever. This meant at low network difficulty a fast miner found shares
every few nonces (dozens per second), while at high difficulty a slow
miner might go hours without one -- both are bad for fair payout
accounting and pool stability.

Pool (rpc.cpp):
  Per-miner difficulty that adjusts to target one share every 15 seconds.
  After every 5 shares, check actual seconds/share vs target. If the
  ratio is off by more than 50% (VARDIFF_RETARGET_RATIO), recompute:
    new_difficulty = current_difficulty / (actual_secs_per_share / target)
  i.e. shares coming too fast -&gt; difficulty goes up; too slow -&gt; down.
  Only apply if the change exceeds 10% to avoid jitter.
  Difficulty is bounded to [0.001, 1e12].
  Each miner gets their own shareTarget derived from their difficulty via
  ShareTargetFromDifficulty(), which maps Stratum difficulty to a uint256
  target using the Bitcoin difficulty-1 base (0x1d00ffff compact).
  New difficulty is sent to the miner via mining.set_difficulty.

Participant (main.cpp):
  Apply the received difficulty value from mining.set_difficulty.
  Previously the handler was a no-op ('acknowledge only'); now it parses
  the difficulty double and logs the change. The share target used for
  comparison comes from the pool's mining.notify p[3] field, which the
  pool updates after each vardiff adjustment -- this is the standard
  approach and matches how cpuminer, xmrig etc. handle it.

With vardiff, share rate is roughly constant regardless of miner speed
or network difficulty. Payouts are proportional to shares submitted,
so each miner's payout accurately reflects their CPU contribution.

* Fix share target mismatch: pool rejected shares it should have accepted

Root cause: the pool put job.shareTarget (from blockTarget&lt;&lt;8, a fixed
per-job value) into mining.notify p[3], but checked m-&gt;shareTarget
(from ShareTargetFromDifficulty, the vardiff value) on submit. With
difficulty=1.0, ShareTargetFromDifficulty gives a much HARDER target
than blockTarget&lt;&lt;8, so shares that the participant thought were valid
(passed the easier notify target) were rejected by the pool (failed the
harder difficulty-1 target).

Fix: MakeNotifyParams now takes the per-miner shareTarget as an explicit
parameter. SendJob passes m-&gt;shareTarget. Both sides -- the participant
reading p[3] and HandleLine checking m-&gt;shareTarget on submit -- now use
exactly the same value. Single source of truth.

Cleanup:
- Remove shareTarget field from StratumJob (was only set, never read
  after the MakeNotifyParams call was fixed).
- Remove MakeShareTarget() function (was only used to populate that
  now-dead field).
- Participant set_difficulty log: 'initial' vs 'updated' instead of
  'unchanged' (first connect sends 1.0 which matches the default 1.0,
  so it was misleadingly logged as 'unchanged').

* Fix double broadcast, correct block reward calculation, improve payout log

Double broadcast (event loop vs HandleLine):
  When a pool miner finds a block, HandleLine calls RebuildJob() +
  BroadcastJob(true) immediately. The event loop then detects
  nBestHeight changed (step 2) and was calling RebuildJob() +
  BroadcastJob(true) again -- miners received duplicate job broadcasts.
  Fix: step 2 now checks gHaveJob before rebuilding. If HandleLine
  already built and broadcast a job for this height (gHaveJob=true),
  step 2 just logs 'job already broadcast by submit handler' and skips.
  If the block came from an external peer (not our miner), gHaveJob is
  false and step 2 handles it as before.

Block reward calculation in QueuePayouts:
  Was calling CBlock().GetBlockValue(0) on an empty block object.
  GetBlockValue uses nBestHeight globally, which by the time QueuePayouts
  runs has already been incremented to the height AFTER the found block.
  Fix: compute subsidy directly as '50 COIN &gt;&gt; (blockHeight / 210000)'
  using the actual found height. At current scale this makes no difference
  (same subsidy for thousands of blocks) but is now correct by construction.

Payout log now shows:
  - Full block subsidy
  - Miner share after fee
  - Operator fee amount in BTF (not just percentage)
  - Number of recipients, maturity height, dust-filtered count
  Example: '[payout] block 2096: subsidy 50.00 BTF, miners get 49.00
  (fee 2.00% = 1.00 to operator), 3 recipients, matures height 2196'

* Fix 6 bugs found in full flow re-read

Bug 1 -- Double broadcast (re-applied fix that was lost):
  When HandleLine accepted a block it called RebuildJob()+BroadcastJob(true).
  The event loop then detected nBestHeight changed and did it again. Miners
  received two back-to-back mining.notify clean=true, wasting their work.
  Fix: step 2 of event loop checks gHaveJob before rebuilding. If HandleLine
  already built a fresh job (gHaveJob=true), step 2 just logs and skips.

Bug 3 -- Disconnect log showed global totals, not session totals:
  gParticipantSharesSent/Accepted accumulate since node start, not since the
  last connect. Added sessionSharesSent/sessionSharesAccepted local counters
  that reset each time PoolParticipantMiner() is called.

Bug 4 -- Dead function ParticipantShareTargetFromDiff (main.cpp):
  Defined but never called anywhere. Removed.

Bug 5 -- Invalid-address miner shares silently burned:
  If a miner authorized with an invalid address, their share of the block
  reward was neither paid out nor logged -- it just vanished. QueuePayouts
  now validates addresses up front, logs the forfeited amount explicitly
  ('X BTF forfeited (bad address)'), and excludes those entries so the
  payout log is accurate.

Bug 6 -- SendMoney called from event loop (stalls miner I/O):
  FlushMaturePayouts called SendMoney which takes cs_main. If the P2P
  thread held cs_main (processing a block), the event loop would stall
  for seconds, causing all connected miners to time out waiting for
  responses. Fix: FlushMaturePayouts now only moves matured entries to
  gPayoutExecQueue (fast, no locks except a brief mutex). A new
  PayoutThreadFn thread drains the queue and calls SendMoney
  independently, so the event loop is never blocked by wallet I/O.

Bug 7 -- Dead goto + dead HandleLine return-false path:
  HandleLine always returns true; the goto next_miner path was never
  reached. Removed goto and simplified the select drain loop.

* Fix share-target scale mismatch and dead pool hashrate reporting

- ShareTargetFromDifficulty() based Stratum difficulty 1 on Bitcoin's
  SHA256 diff-1 constant (0x1d00ffff, ~2^32 hashes/share) instead of
  this chain's actual PoW floor (bnProofOfWorkLimit = ~uint256(0)&gt;&gt;12,
  ~2^12 hashes/share). That's a ~2^20 (~1,048,576x) mismatch: at the
  supposedly-easy difficulty of 1.0, workers were being asked for
  shares roughly a million times harder than the chain's own easiest
  target. Vardiff never gets a chance to correct this because it only
  retargets after real shares come in, and none were coming in. Fixed
  by basing the diff-1 reference on bnProofOfWorkLimit instead.

- Pool hashrate was never computed anywhere despite the data (per-miner
  difficulty + share timing) being available: ThreadBtfPoolAnnouncer in
  nostr.cpp hardcoded ann.hashRate = 0.0, PoolWorkerStatView had no
  hashrate field, and the GUI workers table had no column for it.
  Added an EWMA hashrate estimate per miner (updated on each accepted
  share from expected-hashes-at-difficulty / time-since-last-share),
  threaded it through GetPoolOperatorStats/GetPoolWorkerStats, the pool
  announcement payload, and the GUI (new Hashrate column + totals in
  the telemetry line).

Verified: full clean build via src/Makefile (secp256k1 + RandomX deps
built from source, g++ 13, all four touched files compile and link).

* Remove stale wxWidgets-era build files and orphaned assets

These predate the ImGui/GLFW migration (83af065) and are dead weight:

- src/makefile.linux, build-linux.sh: superseded by src/Makefile +
  root Makefile (make linux). The old ones reference obj/uibase.o and
  obj/ui.o, which don't exist -- they never worked post-migration.
- src/makefile.vc: MSVC build for the old wx UI (obj\ui.obj,
  obj\uibase.obj, obj\irc.obj) -- irc.cpp doesn't even exist anymore,
  and nothing invokes this makefile. Windows builds go through
  makefile.mingw via the root Makefile's  target.
- src/ui.rc, src/uiproject.fbp: wxFormBuilder project/resource files
  for the old wx UI. The current Windows resource file is bitflash.rc.
- src/rc/{addressbook,send}*.bmp, check.ico, bitcoin_bc_original.ico:
  icons used only by the old wx UI. bitcoin.ico (used by bitflash.rc)
  is kept.

Verified nothing includes uibase.h/ui.h/irc.h anywhere, and did a full
clean rebuild afterward (secp256k1 + RandomX from source, src/Makefile)
to confirm removal doesn't break the real build.

* Mine with RandomX fast (2GB dataset) mode instead of light mode forever

RandomXInitDataset() was fully implemented and ready to switch mining
into fast mode, but nothing ever called it -- not in PoolParticipantMiner()
(pool worker), not in the solo BitcoinMiner() loop. RandomXCreateMinerVM()
falls back to light mode whenever the dataset isn't built, so every miner,
participant or solo, has always been hashing in light/cache-only mode.

Light mode exists for cheap single-hash verification (which is why the
pool's own share verification correctly stays on it) -- it recomputes
dataset items on the fly for every hash, which is dramatically slower
than fast mode's direct dataset lookups. Combined with the share-target
scale bug fixed in e87a905, this compounds into exactly the symptom
reported: a participant sitting there hashing for many minutes per share
instead of the vardiff-targeted ~15s, even after the target fix landed.

Fix: build the ~2GB dataset once (multi-threaded, sized to
hardware_concurrency) before mining starts, in both the participant and
solo paths, so RandomXCreateMinerVM() actually gets to use fast mode.

Verified: full clean rebuild via src/Makefile.

* Add vardiff stall watchdog: force difficulty down on zero shares, not just share count

ShareTargetFromDifficulty already fixed the diff-1 scale mismatch (chain
floor vs Bitcoin's SHA256 diff-1), so difficulty 1 correctly means ~4096
expected hashes on this chain, not ~2^32. That part was already correct.

But the normal vardiff retarget in HandleLine only runs after
VARDIFF_SHARES_PER_CHECK (5) shares arrive. A miner that never finds even
one share -- e.g. stuck in RandomX light/cache mode instead of the fast
2GB dataset, or just slower hardware than the target assumes -- never
reaches that check, so it can sit at its initial/current difficulty
indefinitely with vardiff having no samples to act on.

Add a time-based fallback in PoolEventLoop: if a miner's vardiff window
has been open for VARDIFF_STALL_SECS (90s, 6x the 15s target) with zero
shares, force difficulty down by VARDIFF_STALL_CUT (4x) regardless of
share count, and re-notify via mining.set_difficulty. Once a miner is
already at VARDIFF_MIN, log it plainly as a hashrate/connectivity issue
instead of a difficulty issue, so it's not confused with the same-looking
but different problem this was actually fixed for before.

* Fix PrintException rethrow killing the whole node on any transient exception

The uploaded participant log shows the real bug behind the mining stalls:
the node process itself was dying and restarting from scratch every
minute or two ('UNKNOWN EXCEPTION ... in ThreadMessageHandler()' /
'ThreadSocketHandler()'), wiping the RandomX dataset/cache and losing all
progress toward a share -- not a difficulty problem.

Every background thread (ThreadMessageHandler, ThreadSocketHandler,
ThreadBtfConnect, the Nostr helpers) wraps its real work in a
'try { Foo2() } catch(...) { log, sleep 5s, loop again }' self-healing
retry pattern specifically so one bad exception doesn't take the whole
node down. But PrintException(), called from inside every one of those
catch blocks, ended with 'if (!fShutdown) throw;' -- re-throwing the just-
caught exception during ordinary operation. Nothing above the retry loop
catches it, so it escapes the thread and std::terminate()s the entire
process on the very first transient error (a flaky relay connect, a
malformed message, whatever). The comment's intent (suppress noise while
already shutting down) was inverted from what the surrounding retry loops
actually need.

Removed the rethrow. Threads now log the exception and let their own
retry loop handle it, as designed.

* Push a fresh job immediately after any difficulty change, not just on next broadcast

mining.set_difficulty alone doesn't change what the worker actually hashes
against -- the participant only reads its share target from
mining.notify's params[3] (see main.cpp's mining.notify handler); the
set_difficulty message just updates a locally-logged currentDifficulty
value. So both the normal vardiff retarget (in HandleLine, on 5 shares)
and the new stall watchdog were updating m-&gt;shareTarget and telling the
worker about the new difficulty, but the worker kept hashing against its
old target until the next natural job broadcast -- a new block, or the
60s trickle refresh. That could delay any difficulty correction by up to
a minute for no reason.

Call SendJob(m, false) right after each set_difficulty send so the
updated shareTarget reaches the worker in the very next tick instead of
waiting on the next unrelated broadcast.

* Build RandomX dataset before stratum handshake, not after

Worker was sending mining.subscribe/authorize, then blocking for the
duration of the synchronous ~2GB RandomX dataset build, and only then
reading the socket. The pool's replies (subscribe ack, authorize ack,
set_difficulty, first job) sat unread in the OS receive buffer for that
entire window -- nothing was lost, but with the build taking anywhere
from seconds to a couple minutes depending on core count, the worker
looked and logged as permanently stuck on 'waiting for pool response'
when it just wasn't listening yet.

Move the dataset build ahead of the subscribe/authorize sends so the
socket is actively being read the moment the pool replies. Also add
missing RandomXDestroyMinerVM cleanup on the two early-return paths
that now happen after VM creation.

* Reset round shares on externally-found blocks, not just our own

Round shares were only cleared in HandleLine() after our own miner's
block got accepted. When the chain height advanced because someone
else found the block, the main loop rebuilt the job but left
roundShareCount/roundShareTotal/Miner::roundShares untouched, so they
kept accumulating across rounds instead of resetting.

* Show pool hashrate in participant view; show immature coinbase value with maturity countdown

* Persist pending payouts to disk -- survive operator restarts

* Fix log escapes, stall timeout 90-&gt;45s, save-before-exec ordering, Immature tx status

* Sort transactions by nTimeReceived not chain median time -- new txs always on top

* Sort txs by chain depth not nTimeReceived -- survives resync, unconfirmed always first

* Fix orphan block false-positive payout; meaningful tx status instead of raw depth

* Show Orphaned status in red for orphaned coinbases; distinguish from genuinely unconfirmed

---------

Co-authored-by: Vic-Nas &lt;nasci.victorio@gmail.com&gt;
Co-authored-by: Bitflash Pool Dev &lt;pool@bitflash&gt;
Co-authored-by: Victorio Nascimento &lt;155357371+Vic-Nas@users.noreply.github.com&gt;
Co-authored-by: Claude &lt;claude@anthropic.com&gt;</content>
</entry>
</feed>
