Repository navigation
release: hybrid v2 transport release gate and default-on (#184 part 9) #222
Description
Part 9 of 9 · #184 · Size calendar-bound · Base 1.1.0 · Needs part 2 (#186), part 3 (#190), part 4 (#199), part 5 (#206), part 7 (#212), part 8 (#215)
Consolidates work items R0 (#200), R4 (#205), R2 (#218), R3 (#219), R1 (#217), R5 (#220), R6 (#221) and R7 (#222), each formerly its own sub-issue. Their full text is below as sections; the closed issues point here.
Release (2026-10-23)
Release N is 1.1.0, the release after 1.0.1, cut from the 1.1.0 branch. 1.x.x becomes 1.0.1 and takes no more feature merges, so #233 targets 1.1.0.
Release steps (2026-10-27)
#256 prepared the release candidate (version 1.1.0-rc1, man pages, doc/release-notes-1.1.0.md) and merged as 1b6e6b6. 1.1.0 ships after 1.0.1 (#174). The remaining steps, carried over from #256's maintainer checklist:
- v1.0.1 ships (Release v1.0.1: test results, remaining fixes, and release gates #174, release: prepare v1.0.1 #181).
-
1.x.xmerges into1.1.0through a pull request that is merged as a merge commit, not squashed, so later merges from1.x.xstay clean. Expect conflicts inCMakeLists.txt(keep1.1.0-rc1) and the seven man pages (regenerate them). The merge brings doc: fix pipe rendering and tighten P2MR v1 consensus profile #252. The pull request also fills the notes' "Changes in v1.0.1" section: a link torelease-notes-1.0.1.mdand v1.0.1's behavior changes, which are the-fallbackfeedefault (wallet: default -fallbackfee to the minimum relay fee #151), fee-onlysendentries (wallet: show fee-only change-only self-transfers #150) and theencryptwalleterror code (wallet: close races around CWallet::EncryptWallet #183). - The notes are current: every other pull request merged into
1.1.0after release: prepare v1.1.0-rc1 #256 is cited in them. -
v1.1.0-rc1is tagged on the1.1.0commit that completes the steps above. - Release trust: a pull request adds
doc/release-trust-1.1.0-rc1.md, followingdoc/release-trust-1.0.0.mdand release: prepare v1.0.1 #181's checklist. Its merge commit is thetrusted_release_reffor publishing rc1. - rc1 is published as a prerelease: pass
--prereleaseand an explicit--notes-filepointing to a publish-time copy of the notes, whose "Release verification" carries the P2MR v1 conformance line from the evidence run on the tag (release-process.md steps 1 to 10). While Remove unsigned platform artifact waiver after signing accounts are ready #70 is open, pass--allow-unsigned-platform-artifactsand not--allow-codesigning-artifacts. - Then R6 and D4 run on rc1, and R7 decides the default (checklist below). If R7 reverts net: turn hybrid v2 transport on by default #241, the notes,
-v2pqtransport's help text, theqbitd.1andqbit-qt.1pages, the operator guide, the Bitcoin Core differences page and net: turn hybrid v2 transport on by default #241's tests change with it. - Final release: a pull request drops
-rc1and regenerates the man pages. Thenv1.1.0is tagged,doc/release-trust-1.1.0.mdis added the same way, and the release is published. This issue and p2p: add hybrid post-quantum key exchange (ML-KEM-1024) to BIP324 v2 transport #184 close then.
Plan change (2026-10-06)
The gate is shortened: no mainnet canary and no long soak. The hybrid transport is tested on testnet4 for a short period, then turned on for mainnet.
- R0 (14-day canary baseline) and R5 (14-day mainnet canary) are dropped. Their sections are kept below for reference.
- R6 becomes a short
testnet4soak across platforms. The duration is the release-cut decision's, not a fixed 7 days. - R2 (fuzz campaign) and R3 (flood measurement) stay: they need compute time, not calendar gating, and R3 sets the load-shedding default.
- R4 (outside security review) is dropped (2026-10-23): the release ships on the evidence in this checklist.
Goal
The release gate for release N, as one checklist: the canary baseline, the fuzz campaign, the flood measurement, the outside review, the one-line default-on PR, the 14-day mainnet canary, the 7-day testnet4 soak and the release-cut decision. This issue closes at release cut.
Checklist
In rough time order. Each item is a section below.
-
R0 · release: run the 14-day canary baseline(dropped) -
R4 · release: scoped outside security review of hybrid transport(dropped) - R2 · release: fuzz campaign for hybrid transport (at least 504 CPU-hours) (504.6 CPU-hours, no crashes, report; new seeds merged in test: add fuzz seeds for transport paths the release campaign reached #255)
- R3 · release: measure handshake floods and set the load-shedding threshold (settled by CPU benchmarks; the threshold stays at 1,000 per second)
- R1 · net: turn hybrid v2 transport on by default (net: turn hybrid v2 transport on by default #241, merged into
feature/pq-transportas 4d4b11a) -
R5 · release: 14-day mainnet canary on release-candidate builds(dropped) - R6 · release: short
testnet4soak across platforms - D4 (crypto: vendor mlkem-native and build ML-KEM-1024 with runtime backend selection, tests and cross-platform CI (#184 part 3) #190) · build: the release candidate's Guix builds give the same hashes across builders, and the ELF hardening notes of its binaries match v1.0.0's: IBT and SHSTK on x86_64, BTI and PAC on aarch64 (
readelf -n). Moved here from Hybrid post-quantum v2 transport (#184) — integration #233. - R7 · release: release-cut decision for the hybrid transport default
Starting early
- R0 starts as soon as part 2's canary tools merge, and runs during implementation.
- R4 starts once part 3's library and part 4's spec (H2) are up, and doesn't block coding.
- R2 and R3 run as soon as their tools merge (parts 3, 4 and 8), and must finish before the canary.
- R1 merges just before the release candidate. R5 and R6 then run on the release candidate, and R7 closes the gate.
R0 · release: run the 14-day canary baseline
Needs C5 (#186). Calendar: 14 days, overlapping implementation.
Goal
Fourteen days of baseline samples from the team's mainnet nodes while they still run v1.0.0, so the canary (R5) has something to compare against, especially the ratio of inbound connections between the canary and control nodes.
Scope
- Deploy C5's sampler, every 5 minutes, on the three team nodes the canary uses: the archive node and the pool node that will run the release candidate, and the archive node that stays on v1.0.0 as the control. All three run v1.0.0 during the baseline.
- Fields the v1.0.0 RPC lacks are recorded as
NA;connections_pqandgetpqtransportinfoare recorded from the canary's start. - Compute the baseline
connections_inratio of the canary archive node to the control. - Give the team's deployment configuration a per-host switch for
-v2pqtransport, so canary hosts can be turned on later without hand edits. - Check the network census and crawler tools the team operates against non-empty version contents: Core's test framework, which crawlers often copy, asserts the contents are empty.
Acceptance criteria
- 14 consecutive days of samples on all three nodes, with the evaluator's baseline summary (sample counts, gaps, the
connections_inratio) posted here. Addresses stay on the nodes. - The census and crawler tools are confirmed to handle non-empty version contents, or fixed.
R4 · release: scoped outside security review of hybrid transport
Needs H2 (#199), D3 (#190). Starts on the spec and library; runs alongside the handshake work and the canary.
Goal
An independent review of the new attack surface before release N ships it on by default.
Scope
-
In scope:
- the hybrid key recipe and transcript;
- the record reader (
PQHandshake::ParseContents); - the handshake state changes in
V2Transport, including key confirmation; - the fallback and load shedding;
- the library integration settings: config, backend selection, capability hook and build flags.
Upstream mlkem-native's proofs don't cover qbit's wrapper, runtime configuration, compiler integration, transcript or state machine; those are the point of this review. It is not a full audit, and should take about 1–2 weeks.
-
The reviewer and a target date are set before E3's PR opens. The review starts on the spec (H2) and the library (D3) while the handshake PRs are in review, and doesn't block coding.
-
If no paid reviewer is available by the target date, ask the mlkem-native maintainers to review the library integration, and the BIP324 authors to review the protocol through the published spec.
-
Each high or critical finding is fixed and re-checked by the reviewer before release cut. A fix that changes the wire format, key recipe or state machine restarts the canary (R5), the soak (R6) and the flood measurement (R3) on the changed code.
Acceptance criteria
- The review is delivered, and a summary of findings and their resolution is posted here.
- Every high or critical finding is fixed and re-checked.
- The reviewer and dates are recorded in R7's decision record.
R2 · release: fuzz campaign for hybrid transport (at least 504 CPU-hours)
Needs D5 (#190), E2 (#199), G4 (#199). Must finish before the canary.
Goal
Long fuzzing of every target that covers the new code, finished before the canary (R5) starts.
Scope
-
Targets:
mlkem,mlkem_backend_diff,pq_records,p2p_v2_pq_malicious_peer,bip324_cipher_roundtrip,p2p_transport_bidirectional_v2andp2p_transport_bidirectional_v1v2. -
Configurations, at least 24 CPU-hours per target in each:
- ASan + UBSan with native code;
- ASan + UBSan forced portable (
MLKEM_FORCE_PORTABLE=1); - MSan, portable C (
ci/test/00_setup_env_native_fuzz_with_msan.sh).
That is 7 × 3 × 24 = 504 CPU-hours. More machines shorten the calendar time, not the required coverage time.
-
Every crash is fixed, and every interesting input becomes a regression seed in the required corpus.
-
A wire, key-recipe or state-machine change after the campaign reruns the affected targets.
Acceptance criteria
- A report is posted here: per target and configuration, CPU-hours, corpus size, coverage, and crashes (none open). Report.
- New seeds merged: test: add fuzz seeds for transport paths the release campaign reached #255, into
1.1.0as 94bbdb8.
R3 · release: measure handshake floods and set the load-shedding threshold
Needs H1 (#215), F4 (#206). Must finish before the canary; repeat if the handshake code changes.
Goal
Show that hybrid transport doesn't slow block propagation meaningfully under an inbound connection flood, and set F4's shedding threshold from measurements.
Scope
- Machines: a non-AVX2 x86-64 machine (portable C), and a Raspberry Pi 4 or another Cortex-A72-class AArch64 Linux machine (also portable C: native AArch64 code is macOS-only until upstream's assembly carries BTI landing pads).
- Lab: H1's
run-item3.shtopology and its three flood modes (junk,handshake,maxversion), each with A's switch on and off. - Pass criteria, switch on against the same flood with the switch off:
- flooded median propagation at most 1.10×, and flooded 95th percentile at most 1.25×;
- no block slower than 5 s;
- CPU time per attempted junk handshake and per completed handshake each below 2.5× (expect about 1.6× and 2.2× on portable C);
- unflooded figures recorded for reference.
maxversion: report socket-thread delay and memory. If a limit fails, the parser gets an explicit work bound (a follow-up issue, which blocks the canary).- Shedding: measure with shedding at the default threshold and with it disabled, and choose the final threshold. If it changes, open a PR updating the constant.
- 100 blocks per configuration.
Acceptance criteria
- The table, with each figure and its sample count, is posted here, and every criterion passes.
- The threshold is decided, with a PR if it changed: it stays at 1,000 offers per second, so no PR.
Result (2026-10-23): R3 was settled with CPU benchmarks instead of the flood lab, so propagation under a flood and maxversion weren't measured. The figures, and which criteria they cover, are in the R3 comment below.
R1 · net: turn hybrid v2 transport on by default
Needs B3 (#186), C3 (#190), C4 (#186), D5 (#190), G2 (#212), G3 (#212), G4 (#199), H1 (#215), H3 (#215). Merge just before the release candidate is cut.
Goal
Release N ships hybrid v2 transport on by default. This one-line pull request makes that change, and the canary (R5) runs exactly what ships.
Scope
DEFAULT_V2_PQ_TRANSPORT = true.- Update the docs and test expectations that assume the default:
- operator guide and release-note wording: on by default, switch off with
v2pqtransport=0; rpc_net.py's expected fields;- the functional-suite modes.
- operator guide and release-note wording: on by default, switch off with
- An explicit
-v2pqtransport=0, or the default with-v2transport=0, still never warns. - Merge just before the release candidate is cut.
- If the release gate isn't clean at release cut (R7), revert this PR: release N then ships switched off, and a point release turns it on once the open item is resolved.
Acceptance criteria
- Full CI is green, including
pq-functional, the plain-v2 i686 job and the previous-releases job. (On1.1.0at db8dbe5: Full Validation and previous releases. The previous-releases job passed on a rerun; its first attempt hit an intermittentfeature_pruning.pyhang that0.1.x's nightly also hit on 2026-10-17.) - The release candidate is built from a commit that includes this change.
R5 · release: 14-day mainnet canary on release-candidate builds
Needs R0, R1, R2, R3. Calendar: 14 days.
Goal
Fourteen clean days of the release candidate, on by default, on mainnet, with a v1.0.0 node as the live control.
Scope
- Nodes. Two team mainnet nodes run the release-candidate build with the feature on (the default) and
-logips: an archive node and a pool node.-debug=netisn't needed. The pool node pins the archive node with-addnode, so at least one hybrid session runs on mainnet all the time. A third team archive node stays on v1.0.0 for the 14 days as the control. - Outside operators, optional: a few may be invited to switch on and pin a team node; their addresses join the known-good list.
- Evidence. C5's sampler every 5 minutes on all three nodes, and the evaluator reviewed daily. Addresses stay on the nodes.
- Clean means all of:
- no crash or assertion;
- the pinned link is manual and hybrid in at least 99% of the 5-minute samples, not counting restarts;
- no
malformed_recordorfirst_packet_failedentry, and no fallback entry, between known-good endpoints (team nodes, soak nodes and invited operators), read per endpoint fromrecent_failuresandfallback_set; - any
closed_after_switchbetween known-good endpoints matches a restart, eviction or network event in the logs; - other failures, which anyone can trigger from outside, are triage items: each is reproduced in the regtest lab within 24 hours, and only a confirmed qbit bug is a no-go;
- the canary archive node's
connections_instays within ±20% of the control's, relative to their baseline ratio (R0), not counting the 48 hours after a restart.
- A wire, key-recipe or state-machine change during the canary restarts it on the changed code.
Acceptance criteria
- 14 consecutive clean days. The evaluator's final output (every figure with its sample count) and the triage notes are posted here.
R6 · release: 7-day testnet4 soak across platforms
Needs R1. Calendar: 7 days, alongside the canary.
Goal
A week of the release candidate across the platforms and backends that matter, where every link between upgraded nodes is pinned and watched.
Scope
-
-testnet4, 7 days. Upgraded nodes:- x86_64 with AVX2;
- x86-64 without AVX2 (portable C);
- Raspberry Pi 4 (AArch64 Linux, portable C);
- an Apple Silicon Mac (the only native AArch64 code path);
- a Windows (mingw) build.
Plus one v1.0.0 node.
-
Every pair of upgraded nodes is pinned with
-addnode. Each pinned link must be manual and hybrid in every 5-minute sample (C5's sampler), apart from restarts. -
Expected disconnects: feelers and the normal churn of automatic peers.
-
The soak fails on:
- any
malformed_recordorfirst_packet_failedentry, or any fallback entry, between soak nodes, read per endpoint fromgetpqtransportinfo; - a
closed_after_switchbetween soak nodes that no restart explains; - the loss of a pinned hybrid link.
- any
-
The startup line on each node shows the expected backend:
x86_64-avx2;portableon the non-AVX2 machine and the Raspberry Pi;aarch64-neonon the Mac; andx86_64-avx2on Windows if its CPU has AVX2.
Acceptance criteria
- 7 clean days, with the per-node sample summary and startup lines posted here.
R7 · release: release-cut decision for the hybrid transport default
Needs R4, R5, R6.
Goal
One decision record at release cut that says whether release N ships hybrid transport on or off, with the evidence for every release-gate item.
Scope
The record, a comment here linked from #184, lists each gate item with its evidence link and figures:
- outside review (R4): delivered, with every high or critical finding fixed and re-checked;
- canary (R5): 14 clean days;
- flood measurement (R3): every criterion met;
- testnet4 soak (R6): 7 clean days;
- fuzz campaign (R2): finished with no open crash;
- green CI on the release candidate, nightly or on demand: the
--v2pqtransportfunctional suite and thes390x-unit,aarch64-unit,macos-arm64-unitand previous-releases jobs.
Decision rule. If every item holds, release N ships on by default, and the control node upgrades with everyone else. If any item is open or raised a concern, revert R1: release N ships switched off, and a point release turns it on once that item is resolved. Either way, operators can switch it off with -v2pqtransport=0, and v1.0.0 nodes keep working with ECDH-only keys.
Acceptance criteria
- The decision record is posted, and R1 is either kept or reverted in the release branch to match.
This issue closes at release cut, with R7's decision record. Its only pull request is the one-line default-on change (R1). Post the evidence for each item as a comment here. See Release gate and rollout in #184.
Activity
[-]release: release-cut decision for the hybrid transport default (#184 R7)[/-][+]release: hybrid v2 transport release gate and default-on (#184 part 9)[/+]1 remaining item
test: exercise hybrid transport across the functional suite
test: exercise hybrid transport across the functional suite
test: exercise hybrid transport across the functional suite
test: exercise hybrid transport across the functional suite
net: turn hybrid v2 transport on by default
net: turn hybrid v2 transport on by default
net: turn hybrid v2 transport on by default (#241)
djh58 commentedon Oct 23, 2026
R3 result: the load-shedding threshold stays at 1,000 offers per second
DEFAULT_PQ_SHED_THRESHOLD_PER_Sstays at 1000, so there's no code change.Method. R3 was settled with part 8's CPU benchmarks (
bench_qbit -filter='MLKEM_.*|V2_TRANSPORT_HANDSHAKE_.*'), not the flood lab. The threshold limits what one offer costs a responder, and that cost is exactly what the benchmarks measure. Propagation under a flood andmaxversionwere not measured.Figures: nanobench medians, about 2 s of measurement per row (
-min-time=2000).The R2 fuzz campaign was running on the Ryzen machine during these measurements, so its figures are higher and noisier. Its handshake rows are two paired runs at
-min-time=8000, shown as run 1 / run 2.At the threshold.
Criteria.
maxversion: not measured.djh58 commentedon Oct 26, 2026
R2 result: 504.6 CPU-hours of fuzzing, no crashes
All 21 runs (7 targets × 3 configurations × 24 CPU-hours) finished with no crash, leak, timeout or out-of-memory input. Every grown corpus also replays cleanly with CI's fuzz flags.
Setup.
7d0d2de, a commit on the docs part's branch (doc, bench: operator guide, release note and handshake benchmark for hybrid v2 transport #240).asan-native: ASan + UBSan, native ML-KEM (the AVX2 backend);asan-portable: the same flags, built without native ML-KEM and run withMLKEM_FORCE_PORTABLE=1;msan-portable: MSan with an MSan-built libc++, portable C.-timeout=60 -rss_limit_mb=2048.integergroup, which made the transport targets 13 to 57 times slower. The CI-flag replay below includes it.covedges,ftfeatures) are shown at the start of the run → at the end. The start corpus is what CI replays: theqa-assetsinputs plus the committed qbit seeds.covftmlkemmlkemmlkemmlkem_backend_diffmlkem_backend_diffmlkem_backend_diffpq_recordspq_recordspq_recordsp2p_v2_pq_malicious_peerp2p_v2_pq_malicious_peerp2p_v2_pq_malicious_peerbip324_cipher_roundtripbip324_cipher_roundtripbip324_cipher_roundtripp2p_transport_bidirectional_v2p2p_transport_bidirectional_v2p2p_transport_bidirectional_v2p2p_transport_bidirectional_v1v2p2p_transport_bidirectional_v1v2p2p_transport_bidirectional_v1v2Total: 504.6 CPU-hours; 0 crash artifacts; 0 failed CI-flag replays.
Code changed since the campaign. The fuzzed code changed in four ways after
7d0d2de:mlkem_backend_diffuses to force portable C now counts overlapping uses (init, crypto: fix settings.json warning, lock secrets, check CPUID #248).The connection manager's fallback and accounting changes (#250) are outside these targets. None of the changes touches the wire format, the key derivation or the transport state machine, so R2's rerun rule doesn't apply. As a check, every grown corpus was also replayed once through a CI-flag binary built at
529e19f, #233's current head: all 48,787 inputs, 21 of 21 corpora, ran without a failure.Limits of this evidence.
mlkem_backend_diffcompares every result with forced portable C. In the two portable configurations it compared portable C with itself. Review of Hybrid post-quantum v2 transport (#184) — integration #233 found this, and init, crypto: fix settings.json warning, lock secrets, check CPUID #248 now skips the comparison there. Those runs still fuzzed portable C under ASan and MSan. The native-against-portable comparison ran inasan-native.mlkem_tests).Acceptance.
test/fuzz/qbit_corporaare generated bygenerate_seeds.pyand contain no fuzzer output. The grown corpora reach code they don't: 271 more edges inp2p_v2_pq_malicious_peer(asan-native), and 5 to 26 in the ML-KEM and record-parser targets. A pull request into1.1.0will add generated seeds for that code. R2 closes when it merges.test: add fuzz seeds for transport paths the release campaign reached
test: add fuzz seeds for transport paths the release campaign reached (…
release: prepare v1.1.0-rc1 (#256)