upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/rpc.cpp
AgeCommit message (Collapse)Author
2026-08-19pool: refuse jobs when no peer has reported a height for 20 minutesbitflash-maintainer
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)
2026-08-19pool: gate jobs on peer height, not tip agebitflash-maintainer
The sync gate compared the tip timestamp against 30 minutes. That cannot distinguish "we are not synced" from "the network is slow", and it failed in both directions on the same day. A pool mining its own fork keeps its tip fresh, so the check passed for the whole of heights 3966-4085 -- 120 consecutive blocks, all orphaned, 5890.50 BTF of payouts voided, and nothing in the log to say so. Then, once the pool stopped finding blocks, the same check refused to issue jobs against a tip that was the network's own best. The pool is ~90% of the hashrate, so nobody else could move the chain either: stale tip, no jobs; no jobs, stale tip. Gate on the height peers announce in their version message instead. Median, so one peer cannot stall us with an absurd claim, and a block of slack to match WarnIfBehind. Peers predating the announcement report -1 and are ignored; with no reports at all we mine our best chain, which is what the node does anyway without a pool attached. (cherry picked from commit 4e76e34f37a82c0885f75db7dfb9e49db410171d)
2026-08-19payout: make in-flight payments survive a restartbitflash-maintainer
The exec queue was memory-only, and FlushMaturePayouts erased a matured entry from pending_payouts.json *before* handing it over. Any restart in that window lost those payments with no record that anyone was owed anything. It was not hypothetical: a container restart on 2026-07-30 silently dropped a backlog of ~1,156 BTF, and there was nothing left to reconstruct it from. Re-queueing blindly would have double-paid whoever had already been sent to, so the payment needs to be idempotent rather than merely persistent. In-flight work is now a persisted list of per-payment records (paying_payouts.json), one per (block, address) -- the granularity SendMoney works at, and therefore the granularity at which a payment can be half-done when the process dies. Records are written, via a temp file and rename so a crash cannot truncate them, *before* the ledger entry is erased, so the debt is always described by at least one of the two files. Each attempt updates the record (retries, deferrals, backoff); settling it removes the record and rewrites the file, which is what stops a restart from paying twice. On startup ReconcilePayouts runs before the payout thread: - a record whose payment the wallet already shows -- matched on the payout comment plus the exact amount and destination -- is dropped, not resent. SendMoney commits the transaction to the wallet before it returns, so a crash immediately after sending is recoverable. - a ledger entry that has already been exploded into records is dropped, which is the crash-in-the-window case where both files describe one debt. The wallet check is deliberately biased towards "already paid": skipping a payment is visible in the log and recoverable, paying twice is neither. Verified live: sampling paying_payouts.json during a payout burst caught 3 records / 18.56 BTF mid-flight, cleared once paid, and the paid lines now carry the txid. (cherry picked from commit d0248f2e65dbe745f2e9b1d09e1f53a4354d3640)
2026-08-19payout: zero-initialise blockHash; nostr: drop four dead relaysbitflash-maintainer
Two fixes. uint256's default constructor is empty (uint256.h:514), so the blockHash field added in 6963862 was uninitialised garbage on every PendingPayout built without an explicit hash -- which is every entry loaded from a ledger written before that commit. Those entries were meant to be grandfathered past the orphan check on a zero hash; instead each carried a random value that matched no block in mapBlockIndex, and the first maturity sweep after deploy voided the whole loaded ledger: 100 entries, 4950 BTF. Only one "was orphaned -- voiding" line reached the log because LogPrint collapsed the other 99 as duplicates, so the destruction was nearly silent. Nothing had been paid at that point and the file was restored from a pre-deploy backup, so no obligation was lost. Assigning 0 explicitly is all that was missing. Separately, four of the twelve relays are dead. Probed all twelve from two independent networks (a residential line and the pool's VPS) over v4 and v6, with DNS confirmed against 1.1.1.1: relay.current.fyi, relay.nostr.bg -- NXDOMAIN, no A and no AAAA. The domains are gone. (A getaddrinfo call on the VPS reports an address for them, but that is the VPS's own IPv6 handed back by its local resolver, not a real host.) relay.nostr.band, nostr.fmt.wiz.biz -- DNS resolves and TCP connects, but the TLS handshake never completes, from either network. Whether they are down or refusing non-browser clients, this node cannot use them. The remaining eight all complete a WebSocket upgrade. A dead relay is not free: each holds a connect/retry slot and delays discovery for no added redundancy. (cherry picked from commit 144bc96f150d23507d57d29cb339f177dcd6b092)
2026-08-19payout: key obligations on block hash, and wait for coin instead of expiringbitflash-maintainer
A 280-block reorg (heights 3075-3354) orphaned 292 of the pool's 707 blocks and destroyed 14,600 BTF of coinbase. The payout ledger did not notice: it records only matureAtHeight, and a reorg replaces the block at a height without changing the height, so ~14.4k BTF of obligations outlived the coin that was supposed to fund them. The pool then paid fork-era miners out of real-chain coin until the wallet was empty -- 17,827 BTF paid against 17,900 BTF of matured supply, leaving under 1.1 BTF spendable and every subsequent payment failing. Three changes, all in the payout path: - Record the funding block's hash and height, persist both, and void an obligation whose block is no longer in the main chain. Checked both at maturity and again in the exec queue, since an entry can sit there for hours across retries and the reorg can land at any point in that window. Entries loaded from a file written before this are grandfathered rather than voided on a missing field. - Stop spending the retry budget on maturity. SelectCoins failing for want of spendable coin is not an error, but it consumed one of 1440 retries every 60s, so a payment the pool fully intended to honour would hit GIVING UP 24h later and the miner would silently never be paid. Those attempts now defer without incrementing retries, logged hourly rather than every minute. GIVING UP now means what it says: 1440 real failures against a block still on-chain with the money available. - Schedule at COINBASE_SPENDABLE_DEPTH instead of COINBASE_MATURITY. At depth 100 a block's own coinbase is 20 blocks short of spendable, so its payout had to come from unrelated mature coin and the pool carried a permanent ~19-block float. The "96% of 934 payments went through at depth 100" note that justified the shorter wait was measured while this node was alone on a private fork paying itself. The extra 20 blocks also put the maturity gate past the reorg that caused all of the above. (cherry picked from commit 696386220a3d2fac88cb8045931392f4909e0985)
2026-08-19payout: say what actually failed instead of blaming transaction sizebitflash-maintainer
SendMoney's "oversized transaction that requires a transaction fee of 0.00" is not an oversized transaction and there is no fee. That branch is taken when nValue + nFeeRequired > GetBalance() -- an empty spendable balance -- and the fee reads 0.00 because CreateTransaction failed before setting one. 3,267 of these in debug.log sent me looking for a transaction-size problem that does not exist. Report what is short, by how much, and against what balance. Name COINBASE_SPENDABLE_DEPTH in main.h and derive GetBlocksToMaturity from it, because the distinction it captures is genuinely surprising: CMerkleTx::GetCredit values a mined coinbase at zero until COINBASE_MATURITY+20 deep, not COINBASE_MATURITY. I first changed the payout gate to that deeper value, on the theory that each payout was firing 20 blocks before its own coinbase could be spent. Measuring it refuted that: across 934 payments, 96% succeeded at depth 100, the moment they were queued, which is only possible because a payout is funded from whatever coin is already mature rather than from its own block. The gate stays at COINBASE_MATURITY, with a comment recording the measurement so the next person does not re-make the same change. The real shortfall is structural and not addressed here: the pool pays out ~99% of each block's subsidy while only matured coinbases are spendable, so it runs with almost no buffer, and any orphaned block it queued a payout for drains that buffer permanently. The 24h retry loop absorbs this -- 0 payouts forfeited -- but the buffer is what needs fixing. (cherry picked from commit e28df01f90d621b0cd818f28d70a20b8ab5d1a8d)
2026-08-19pool: wait to be paired before wrapping the rendezvous socketbitflash-maintainer
Upstream #30 split rendezvous registration from waiting for a dial, so RvServiceRegister now returns as soon as the relay lists the service and no longer consumes the relay's 0x01 pairing byte. It updated its own caller in net.cpp and left this one behind. AcceptOneFn went straight from RvServiceRegister to BtfServiceWrap, so BtfServiceWrap read that 0x01 as the first byte of the miner's 32-byte ephemeral key and every byte after it was shifted by one. The X25519 handshake has no confirmation step, so it completed against a garbage shared secret, the tunnel was handed to the Stratum event loop, and nothing failed loudly: the pool logged a miner arriving and then dropped it as "never authorized", while the miner logged a pool that closed on it with gotSubscribeAck=0. Loopback miners were unaffected, which is exactly what made it look like a relay problem. Block in RvServiceWaitPaired before wrapping, and close and re-register if the relay drops us first. test_rvaccept.cpp covers both shapes against a real bitflashd on loopback: the pre-fix order establishes a tunnel that carries no bytes, the fixed order round-trips. Also corrects the btftunnel.h contract comment, which still described the pre-#30 behaviour and is what the old code was written against -- upstream's own test_relayproc.cpp, test_btftunnel.cpp and test_btfrv.cpp are still on that stale contract and fail the same way. (cherry picked from commit 3e3a36b2bf1e6ffb4861507089b743688619377f)
2026-08-19payout: retry failed SendMoney for 24h, not 30minbitflash-maintainer
A pool that just found its first blocks has zero mature balance until 100 blocks later; orphaned coinbases (observed: reorgs orphaned our blocks 2651-2655) make that worse. 30 one-minute retries forfeited payments that would have succeeded hours later. (cherry picked from commit 49e80023ca23baee8a4b80cfb891e7a358dc39ba)
2026-08-19pool: sync-gate job creation, retry failed payouts; widen nostr relay setbitflash-maintainer
- 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)
2026-08-04public commitbitflash-coin