Repository navigation
v3.0.0
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 takes
SETTLEMENT_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 forSETTLEMENT_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, or
unchangedfor a row that changed after it was read, for example
import-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 and
Imported <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 heldSETTLEMENT_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'simport-audits
doesn'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 plain
transport 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
- 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 coversmigratefor 026, which every start of
3.0.0 requires, as rc.5's does; the cutover's
migrate --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 and
fatal-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 thepublic-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_PRODUCTIONor
QBIT_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 itsimport-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, in
ledger/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 in
config/native-settings.txtand a re-export inledger.rs. It adds
tests/import_audits_settlement_lock.rsand its entries in
test/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:
- 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'simport-auditsworks
around it (above), but other large statements from the serving binary
don't. The fix is upstream, transact-rs/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.json
exceed 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,
PrismAcceptedRevisionWorkPendingand
PrismAcceptedRevisionWorkTrackingUnknown, 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.
- 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.