Qbit Mining Bootstrap v3.0.0
Release candidates
Release notes
qbit-mining-bootstrap 3.0.0 Release Notes
Release date: 2026-10-22 (tag v3.0.0, cut from the 3.0.x branch).
Since 2.x.x
- Native Rust mining path:
qbit-prism-serverreplaces the Python coordinator and builder subprocesses, handling Stratum, job refresh, block submission, CTV broadcasting and HTTP while retaining the existing miner protocol. - Multiple serving frontends: PostgreSQL coordinates share ordering, duplicate detection, block candidates and broadcasts across instances. Miners can reconnect to another frontend and resume unexpired work; an HA deployment reference covers the database standby and failover.
- Stronger block handling and operations: durable submission reservations, reconciliation of uncertain node responses, submission holds, native metrics and recovery commands support crashes, rehearsals and incident recovery.
- Large-ledger maintenance: partition-based share archiving, bounded audit imports, parallel recovery exports and deferred share-hash backfills improve maintenance and shorten the migration outage.
- Upgrade planning required: moving from 2.x.x requires a one-way database migration and configuration review. There are documented payout-accounting changes, and per-worker credited-difficulty overrides are no longer supported. Follow the migration guide and the upgrade instructions below.
3.0.0 is 3.0.0-rc.6 plus #770's fix for #765, in which import-audits held
SETTLEMENT_LOCK while it sent and stored each legacy audit body, so a large
body stalled settlement and template refresh on the frontends serving beside
it, and #760's fix for #759, in which the node RPC client reused an idle
connection past qbitd's own idle close. Those two pull requests were prepared
as 3.0.0-rc.7 (#772), which was never tagged. 3.0.x branches from 3.x.x
at #770's merge, 4c134913. 3.x.x continues as 3.1 from 3.1.0-rc.1, which
adds the dual writer and migrations 027, 028 and 031; 3.0.0 has none of them.
#291 records the cutover qualification and the go/no-go for the production
rollout.
Everything in the 3.0.0-rc.1, 3.0.0-rc.2, 3.0.0-rc.3, 3.0.0-rc.4, 3.0.0-rc.5 and 3.0.0-rc.6 notes applies, with the changes below; see also the development-line status. Like rc.6, an upgrade from the previous candidate replaces the binary only: 3.0.0 adds no migration (Upgrading).
Changes since 3.0.0-rc.6
- #770 (#765):
import-auditswrites each legacy audit before it takesSETTLEMENT_LOCK, holds the lock only to check the write guard and commit, and bounds every phase of a row's transaction.- Before: for each row, rc.6 took
SETTLEMENT_LOCK, checked the write guard, and only then encoded the row's UPDATE and sent it: the canonical body asbytea, beside the audit's commitment and witness Merkle leaves as JSON. The lock was held while the client encoded and sent the statement and while the server stored it. Nothing bounded the send: the session'sstatement_timeout(PRISM_DATABASE_STATEMENT_TIMEOUT_MS, 15 s by default) starts only once the whole statement has arrived. In #765, a legacy audit with a ~112 MB canonical body held the lock for about 90 s, until it was killed, while the server waited for the rest of the statement (idle in transaction, waiting onClientRead). Every otherSETTLEMENT_LOCKuser waited behind it: the frontends' advisory-lock waits timed out, template refresh was deferred, and settlement would have waited too.docker stopdidn't stop it either: in a container the command runs as PID 1, which ignores a SIGTERM it has no handler for. - Now, per row: the body is loaded, verified and canonicalized, and all
nine UPDATE parameters are encoded, on one blocking thread before any
transaction opens. The UPDATE is then sent and run on the row's own
connection, holding only that row's lock. Only then does the importer
poll for
SETTLEMENT_LOCKwithpg_try_advisory_xact_lock, every 25 ms and never queued, check the write guard again under it, and commit. Every otherSETTLEMENT_LOCKholder still sees a row committed before it took the lock, or not at all. The write guard also runs before the write, so a halted cluster or a live legacy writer is refused before anything is sent. - A lock holder that needs the row goes first. A poll that misses the
lock also walks the lock holder's blockers (
pg_blocking_pids, then theirs in turn). If the holder waits on this row, directly or through another session, the row is rolled back so the holder can proceed, and the command fails naming the row. Other sessions waiting on the row, such as a second import, just wait. - Bounds: two new settings, read by
import-auditsonly.PRISM_IMPORT_AUDITS_WRITE_TIMEOUT_MS(60000; 1000 to 600000) is the write's transaction-localidle_in_transaction_session_timeout,statement_timeoutandlock_timeout. So the server ends a session that stops sending mid-statement, and cancels an UPDATE that runs, or waits for the row, that long.PRISM_IMPORT_AUDITS_LOCK_TIMEOUT_MS(5000; 100 to 600000) bounds the wait forSETTLEMENT_LOCK. Once the lock is held, it is also the transaction's idle and statement timeout, so a client or statement that stalls under the lock loses its session, and the lock, within that long.- The row's connection also sets
client_connection_check_intervalto 1 s where the server supports it (PostgreSQL 14 or later, on Linux). An abandoned row is then rolled back within about a second, even while the server is storing it.
- A row that exceeds a bound is rolled back, and the command exits 1
with an error naming the row,
importing legacy audit <block hash>, and its cause, such asSETTLEMENT_LOCK stayed busy for 2000 ms; the row is rolled back and nothing was stored. Rows imported before it stay imported, and a re-run resumes from it. - SIGTERM or SIGINT stops the command. It stops before the next row,
or rolls back a row that has not been granted the lock, and exits 1 with
import-audits stopped on request after importing <n> audit bodies; …. A row that holds the lock finishes its commit first, and a stop that lands once the last row is in ends as a finished import. A second signal exits at once. - Each row it writes prints one line on stderr,
imported, orunchangedfor a row that changed after it was read, for exampleimport-audits: block_hash=<hash> imported parameter_bytes=block_hash:64,…,canonical_audit_bytes:<n> total_parameter_bytes=<n> prepare_ms=… write_ms=… lock_wait_ms=… lock_held_ms=… total_ms=…. stdout keeps the completeness counts andImported <n> audit bodies. - Measured in a release build, over TLS through a TCP proxy, on a
synthetic legacy row with a 112.2 MB canonical body and a 37.5 MB
witness-leaves array. rc.6's order held
SETTLEMENT_LOCKfor 3.145 s: 0.10 s encoding the statement, 3.01 s sending it and the server storing it, about 0.15 s of that the send, and 28 ms for the guard and the commit. 3.0.0 holds it for 13 ms. With the client's send slowed to 16 MiB/s, the hold was 14.9 s against 16 ms. A session taking the lock every 20 ms waited at most 3.13 s and 14.9 s beside rc.6's order, and 3 ms and 17 ms beside 3.0.0's. - Why #765's send took so long is not proven: it did not reproduce
here. The likely cause is #771, below. 3.0.0's
import-auditsdoesn't depend on it: whatever slows or stops the send now happens before the lock and within the write timeout. The client also polls the statement every 100 ms, which un-sticks #771's lost wakeup if it occurs. - It changes
import-auditsonly: the command, its transaction and two setting names. There is no migration or schema change, and nothing the frontends or thepublic-apiservice run changes.
- Before: for each row, rc.6 took
- #760 (#759): every node RPC client keeps an idle connection for at most
20 s.
- Before: the frontends' qbitd RPC client kept an idle kept-alive
connection for 60 s, and the public API's node client for reqwest's
default 90 s, while qbitd closes one after its
-rpcservertimeout, 30 s by default. A request written into a connection just as the node closes it is never read, and fails after the connection existed: the plaintransport failederror, which nothing retries. Onsubmitblockthat is an unknown outcome, and the block is never offered again. One deployment, withPRISM_CTV_BROADCASTER_POLL_SECONDS=30(the default is 10), logged the error from the CTV broadcaster, whose passes ran in step with the 2 s refresh, on about one pass in eight to fifteen. - Now every node client is built by
node_client_builder(): the frontends', the public API's and the load harness's. It keeps an idle connection for at most 20 s (POOL_IDLE_TIMEOUT), so the client drops it before the node can. A compile-time assert keeps the bound below qbitd's 30 s default. Relay calls,submitblockincluded, share the bounded pool as before. - Operator rule: keep qbitd's
-rpcservertimeout, and the idle timeout of any proxy between a PRISM process and its node, above 20 s. qbitd's default, 30 s, is.
- Before: the frontends' qbitd RPC client kept an idle kept-alive
connection for 60 s, and the public API's node client for reqwest's
default 90 s, while qbitd closes one after its
- TLS dependencies, for the RustSec advisories
cargo denyreports against rc.6's lockfile: rustls 0.23.45 to 0.23.46 (RUSTSEC-2026-0346, -0347, -0348 and -0349), rustls-webpki 0.103.15 to 0.103.16 (RUSTSEC-2026-0382) and webpki-roots 1.0.9 to 1.0.10 (RUSTSEC-2026-0381), in the root and fuzz lockfiles. They are patch releases within the versions rc.6 already resolved, with no other dependency change. The serving binary's PostgreSQL (sqlx) and HTTPS (reqwest) clients use them.
Upgrading
- From 2.0.x, or from 3.0.0-rc.4: as in the
rc.5 notes, with 3.0.0
in rc.5's place. That covers
migratefor 026, which every start of 3.0.0 requires, as rc.5's does; the cutover'smigrate --defer-share-hashes --offline-indexes, and finishing the backfill after go-live withbackfill-share-hashes; rolling back to rc.4 at fence 2; and the operating rules still in force: #737's statement timeout for rc.4's tools, #738's snapshot rule andfatal-state clear --timeout-seconds. From an earlier candidate, the upgrade sections of each later candidate's notes apply in turn. - From 3.0.0-rc.5: replace the binary, as the rc.6 notes describe for rc.6; everything below about rc.6 applies too.
- From 3.0.0-rc.6: no migration; replace the binary on every frontend and
tool, and on the
public-apiservice. 3.0.0 requires the same native schema migrations as rc.6, 2 to 26, declares the same capabilities and understands the same share-hash fence values, 1 and 2. The same settings give its frontends the same configuration fingerprint, so they can replace rc.6's one at a time. It changes no evidence or admission-gate script.- The two new settings are accepted by 3.0.0 binaries only. rc.6's
runandcheck-configrefuse aPRISM_*name they don't know in production mode (QBIT_CHAINmain or mainnet,QBIT_PRODUCTIONorQBIT_TOOLS_PRODUCTION), and warn about it otherwise. So pass them only to a 3.0.0import-auditsrun, never through an environment that an rc.6 frontend reads. - #760's node rule: check that qbitd's
-rpcservertimeout, and the idle timeout of any proxy between a PRISM process (a frontend, a tool or thepublic-apiservice) and its node, are above 20 s. import-auditsbeside serving frontends no longer needs the guard #765 describes, one that kills it with SIGKILL once a settlement waiter has waited a few seconds. Its hold is now the guard read and the commit, anddocker stopstops it.
- The two new settings are accepted by 3.0.0 binaries only. rc.6's
- Rolling back to rc.6: first remove the two settings from every
environment an rc.6 binary will read, then replace the binary; nothing
else changes. With rc.6, #765 returns: run its
import-auditsbeside serving frontends only under the guard #765 describes. #759 returns too: the node clients keep an idle connection for 60 s again, 90 s on the public API.
Verification
The runtime changes between rc.6 and 3.0.0 are in import-audits, in how
long the node clients keep an idle connection, and in the TLS crates' patch
releases above:
- #770, merged as
4c134913:import-auditsonly, inledger/migration.rs(Ledger::import_legacy_audits_withand its per-row transaction) andtools.rs(the command, its two settings, its stop signal and its stderr line), with the two names inconfig/native-settings.txtand a re-export inledger.rs. It addstests/import_audits_settlement_lock.rsand its entries intest/prism-gated-tests.txt, and updates the server's README, the ledger runbook and the pool-acquire census. - #760, merged as
12f1ae8e: the node client's idle bound inrpc.rs, used by the public API's node client (api.rs) and the load harness's, with the node requirement in the migration guide.
Neither touches a migration, a capability, the share append
(ledger/window.rs), the order lock or settlement. #760 changes only how
long each node client keeps an idle connection. So the serving evidence
rc.3's notes cite still applies: the D1 peak-second gate, the 750/s ceiling
and the failover drill under load, run 1, all measured on #719's build.
In CI, the changes are proved by their tests:
- #770 by
import_audits_settlement_lock's 13 gated tests. They put the importer's connections behind a relay that slows or stalls what the client sends, which leaves the server in #765's state, and check:- that a session taking
SETTLEMENT_LOCKevery 20 ms beside the import of a 107 MB body with a 37.5 MB witness-leaves array waits less than 3 s, while the throttled write takes longer than that (rc.6's order fails it); - that a stall mid-send and a stall under the lock each end within their bound, and that a busy lock fails the row within its bound;
- that a lock holder waiting on the row goes first, directly or behind another session;
- that a stop mid-send, mid-execution, before the grant, between rows and after the last row each behaves as above;
- and, on the real command, its SIGTERM exit and its busy-lock output.
- that a session taking
- #760 by
rpc_idle_close: the real client against a loopback node that drops a request arriving after its idle limit, as the race does. With a bound below the node's, the next call opens a new connection and is answered; with a bound past it, the call fails with the production error.
Otherwise the evidence is the same set as rc.6's (#557):
- CI on the version-bump pull request, and dispatched on the tag;
- the L3 production-window matrix (
l3-full) on thev3.0.0tag push. The version-bump pull request targets3.0.x, where the matrix doesn't run on pull requests, so the tag runs it rather than promoting a pull request's run (#608); - the targeted nightly batch:
short-plan-fake-node,short-plan-real-node,soak-smoke,faults-short-real-nodeandfaults-failover-fake-node; - the live regtest weekly scenarios.
rc.1's full nightly set and 5.5 h soak carry over. The evidence bundle on the
v3.0.0 GitHub release lists every run, and #291 links to it.
Known issues
As in the rc.6 notes, with these changes:
- Fixed since rc.6:
- #765, by #770: rc.6's
import-auditsheldSETTLEMENT_LOCKwhile it sent and stored each body, with nothing bounding the send, and ignoreddocker stop(above). - #759, by #760: the node clients kept an idle connection past qbitd's idle close, so a call could fail in the close (above).
- #765, by #770: rc.6's
- Open since rc.6, not fixed in 3.0.0:
- #771: sqlx 0.8.6's rustls stream can lose the wakeup of a large write.
A large statement sent over TLS can then stall, with the server waiting
for the rest of it, until something closes the connection. It is the
likely cause of #765's hold. 3.0.0's
import-auditsworks around it (above), but other large statements from the serving binary don't. The fix is upstream, launchbadge/sqlx#4251, which no 0.8.x release carries; #771 tracks backporting it. - #762: a
submitblockwhose deadline,PRISM_BLOCK_SUBMIT_RPC_TIMEOUT_SECONDS(1 s by default), passes while its connection is still opening is recorded as an unknown offer, not an unsent one, so that block is never offered again. It needs no free pooled connection when the call starts and a connect slower than the deadline, which the 2 s refresh calls make unlikely. #760 leaves relay calls on the bounded pool for this reason. - #757: five alert-rule UIDs in
docs/prism-native-alert-rules.jsonexceed Grafana's 40-character limit, so provisioning the rule set as published stops Grafana from starting. Until it's fixed, map the five to short forms when rendering the rules; #757 lists them. - #758: the two accepted-block revision-work warnings,
PrismAcceptedRevisionWorkPendingandPrismAcceptedRevisionWorkTrackingUnknown, fire after every pool block on a frontend that has no miners and doesn't commit the settlements, a backup frontend say, and the second latches until that frontend restarts. Until it's fixed, silence the two on idle frontends.
- #771: sqlx 0.8.6's rustls stream can lose the wakeup of a large write.
A large statement sent over TLS can then stall, with the server waiting
for the rest of it, until something closes the connection. It is the
likely cause of #765's hold. 3.0.0's
- Still open from rc.6, as the rc.6 notes describe them: #752, #738, #739, #724's other answer, #737's open part, #741, #731, #734, #716's open part, #711 and #712.
Migration and recovery (D5)
Native schema migration is one-way. No down-migrations are provided. Prefer forward repair or a compatible native image retaining the latest durable state. Both migration and ledger operations runbooks contain the numbered reconciliation procedure and exact queries.
Before the first native share is acknowledged, restore the complete pre-migration database and artifact backup and restart the pinned old image in isolation from the migrated database. After the first native share is acknowledged, restoring an older database loses those accepted records. Any rollback that discards acknowledged history requires an explicit accounting reconciliation and recovery decision; it is not an ordinary image rollback. Keep every native frontend stopped while performing an isolated restore/recovery operation against its replacement database.
Native import-audits is the canonical backfill for recoverable legacy bodies,
including those without a canonical sidecar. It verifies the original audit
digest, trusted ledger key and recorded coinbase. Rehearse it on an isolated
source restore before migrating the source. Production self-check fails when
audits lack shared stored bodies or canonical availability; native immutable
snapshots provide canonical reconstruction without a duplicate full byte copy.
The one-shot commands self-check, import-audits, backfill-ctv and
broadcast-ctv no longer register a frontend heartbeat (#381). Earlier 3.x.x
builds left a starting row under a generated instance ID after every
invocation, which fatal-state clear then refused, and overwrote the live
frontend's status when the ID was configured. Rows those builds left behind are
not removed automatically; the fatal-state runbook's investigation still applies
to them.