Phase History
The Sv2 stack was built incrementally in clearly scoped phases. Each phase delivers a green test state and a production-ready slice; no half-baked, half-merged branches.
| Phase | Content | Status | Tests* |
|---|---|---|---|
| 7.1 | Foundation — codec + 12 messages + NOISE_NX | ✅ | +22 |
| 7.2 | Mining-Protocol state machine | ✅ | +16 |
| 7.3a | Sv2 server crate (TCP + NOISE + session loop) | ✅ | +2 |
| 7.3b | Daemon wiring + CLI (gen-sv2-key) | ✅ | (live) |
| 7.4 | V1↔V2 translator proxy + extended channels | ✅ | +22 |
| 7.5 | Job distribution + V1 mining.notify mapping | ✅ | +11 |
| 7.5b | Production polish (prev_hash byte order + BIP-320) | ✅ | +6 |
| 7.6a | Template-distribution foundation (7 TDP messages) | ✅ | +13 |
| 7.6b | TDP daemon wiring (Bitcoin-node client) | ⏳ | — |
*Each phase additionally turned tests green. Workspace total: 356.
Phase 7.1 — Foundation
Workspace member warppool-stratum-v2 with three modules:
codec.rs— Sv2 frame format (6-byte header[ext u16 LE][type u8][len u24 LE]),MAX_PAYLOAD_LEN = 1 MiB, compact-int encoding (Bitcoin var_int compatible), length-prefixed strings.messages.rs—ProtocolMessagetrait + 6 concrete structs (SetupConnection- 2 replies, OpenStandardMiningChannel + reply, SubmitSharesStandard). No
serde, because the Sv2 spec mandates exact byte layouts; every message has
encode_payload/decode_payloadmethods.
- 2 replies, OpenStandardMiningChannel + reply, SubmitSharesStandard). No
serde, because the Sv2 spec mandates exact byte layouts; every message has
noise.rs—NoiseSessionstate machine oversnow::HandshakeState/TransportState. Pattern:Noise_NX_25519_ChaChaPoly_BLAKE2s.
Quirks: snow 0.10 builder methods are Result-returning (use ? instead
of chaining); OpenStandardMiningChannel nominal_hash_rate: f32 blocks Eq
derive — only PartialEq.
Phase 7.2 — Mining-Protocol State Machine
Pure logic layer with no TCP coupling: the caller feeds in decoded Frames
and gets 0..n response frames back.
Three session phases: AwaitSetup → MiningProtocol → Closed. Phase 7.6a
added TemplateDistribution as a parallel phase.
ChannelRegistry with per-channel extranonce prefix (4-byte BE counter),
duplicate-sequence detection via last_sequence_number tracking.
Phase 7.3 — Sv2 Server + Daemon Wiring
7.3a server crate: accept loop + NOISE handshake (responder) +
length-prefixed encrypted-frame I/O. JobUpdate broadcast as
broadcast::Sender<JobUpdate>.
7.3b daemon wiring: StratumConfig.sv2_listen + Secrets.sv2_static_priv_key_hex.
CLI subcommand gen-sv2-key prints the public key to stderr (for miner config)
and the private key to stdout (for secrets.toml via redirection).
The daemon can host V1 (plain + TLS) and V2 (NOISE) in parallel.
Phase 7.4 — V1↔V2 Translator Proxy
New crate warppool-translator + binary dvb-warppool-translator. For every
V1 connection the translator opens a V2 connection to the upstream pool in
parallel and translates between the wire formats.
Extended channels as a prerequisite: standard channels have no
extranonce field in the submit, but V1 miners iterate extranonce2. Solution:
the translator opens an OpenExtendedMiningChannel upstream and sends
SubmitSharesExtended with miner-controlled extranonce.
State machine:
- V1
mining.subscribe→ buffered (no reply yet) - V1
mining.authorize→OpenExtendedMiningChannel{user_identity, min_extranonce_size=4}upstream - V2
OpenExtendedMiningChannel.Success→ deferred V1 replies (subscribe result withextranonce_prefixas extranonce1, set_difficulty from target, authorize OK) - V1
mining.submit→ V2SubmitSharesExtendedwith pad/truncate tochannel.extranonce_size - V2
SubmitShares.Success/Error→ V1 OK/Err with Stratum codes (low-diff→23, stale→21, duplicate→22)
Also done in 7.4: real SubmitShares.Success/.Error frames from the
Sv2 server (the old Phase 7.3 TODOs "silent ignore + log only" became real
responses).
Phase 7.5 — Job Distribution + V1 Notify
Four new Sv2 messages for block-template push:
NewMiningJob(0x1E) — standard channels (pre-computed merkle_root)NewExtendedMiningJob(0x22) — extended channels (with version_rolling_allowed, SEQ0_255 merkle_path, B0_64K coinbase_prefix/suffix)SetNewPrevHash(0x20) — references the previous job byjob_idSetTarget(0x21)
Server fan-out: JobUpdate extended with coinbase_prefix/suffix/merkle_root/
min_ntime/version_rolling_allowed. In the handle_connection job-broadcast arm,
build_job_frames(session, job) builds the kind-appropriate variant for every
open channel + a deferred SetNewPrevHash.
Translator: learns NewExtendedMiningJob + SetNewPrevHash + SetTarget;
caches job + prev_hash separately and pairs them via job_id. maybe_emit_notify()
sends V1 mining.notify with all 9 params:
[job_id, prev_hash_hex, cb1, cb2, [merkle_branches], version, nbits, ntime, clean_jobs].
clean_jobs=true on tip change.
SetTarget with an unchanged target is skipped (avoids redundant
mining.set_difficulty spam).
Phase 7.5b — Production Polish
Two real V1-miner production bugs fixed:
prev_hash byte order: prev_hash_to_v1_hex() reverses every 4-byte chunk
(slushpool convention, sha256-block-internal order). Previously BE display
bytes were forwarded as-is → real V1 miners parse that incorrectly.
BIP-320 version rolling:
mining.configurehandler: negotiates mask =miner_mask & ours_mask;ours_mask = 0x1FFFE000(16 bits, bits 13–28) when the upstream job hasversion_rolling_allowed=true, otherwise 0.mining.submitparses an optional 6th paramversion_bits_hex.- V2
SubmitSharesExtended.version = (job.version & !mask) | (miner_bits & mask)per BIP-320 XOR — previously hardcodedversion: 0(MVP bug).
Phase 7.6a — Template-Distribution Foundation
7 new TDP messages (its own sub-protocol, separate from Mining):
| ID | Name |
|---|---|
| 0x70 | CoinbaseOutputDataSize |
| 0x71 | NewTemplate |
| 0x72 | SetNewPrevHashTdp (distinct wire format from Mining 0x20!) |
| 0x73 | RequestTransactionData |
| 0x74 | RequestTransactionData.Success (with SEQ0_64K<B0_16M> tx list) |
| 0x75 | RequestTransactionData.Error |
| 0x76 | SubmitSolution |
Session routing: new phase TemplateDistribution. TDP frames are logged
but the foundation phase emits no response — Phase 7.6b wires up the
real handling. Cross-protocol frames (Mining in TDP phase or vice versa)
are an UnexpectedMessage error.
Phase 7.6b — TDP Daemon Wiring (gated)
Waiting on Bitcoin Core gaining native Sv2 TDP server support. Today this only exists in SRI forks; no path runs in production.
What Remains for Phase 8
- Reproducible builds (deterministic timestamps,
--remap-path-prefix) - Cosign signatures (partly in Phase 6 — keyless OIDC for SHA256SUMS)
- SBOM via Syft (partly via anchore/sbom-action)
- Auto-update with rollback (larger feature, its own crate
warppool-autoupdate) - Documentation site ← this one (Phase 8a)
This documentation itself is Phase 8a.