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.

PhaseContentStatusTests*
7.1Foundation — codec + 12 messages + NOISE_NX+22
7.2Mining-Protocol state machine+16
7.3aSv2 server crate (TCP + NOISE + session loop)+2
7.3bDaemon wiring + CLI (gen-sv2-key)(live)
7.4V1↔V2 translator proxy + extended channels+22
7.5Job distribution + V1 mining.notify mapping+11
7.5bProduction polish (prev_hash byte order + BIP-320)+6
7.6aTemplate-distribution foundation (7 TDP messages)+13
7.6bTDP 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.rsProtocolMessage trait + 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_payload methods.
  • noise.rsNoiseSession state machine over snow::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: AwaitSetupMiningProtocolClosed. 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:

  1. V1 mining.subscribebuffered (no reply yet)
  2. V1 mining.authorizeOpenExtendedMiningChannel{user_identity, min_extranonce_size=4} upstream
  3. V2 OpenExtendedMiningChannel.Success → deferred V1 replies (subscribe result with extranonce_prefix as extranonce1, set_difficulty from target, authorize OK)
  4. V1 mining.submit → V2 SubmitSharesExtended with pad/truncate to channel.extranonce_size
  5. 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 by job_id
  • SetTarget (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.configure handler: negotiates mask = miner_mask & ours_mask; ours_mask = 0x1FFFE000 (16 bits, bits 13–28) when the upstream job has version_rolling_allowed=true, otherwise 0.
  • mining.submit parses an optional 6th param version_bits_hex.
  • V2 SubmitSharesExtended.version = (job.version & !mask) | (miner_bits & mask) per BIP-320 XOR — previously hardcoded version: 0 (MVP bug).

Phase 7.6a — Template-Distribution Foundation

7 new TDP messages (its own sub-protocol, separate from Mining):

IDName
0x70CoinbaseOutputDataSize
0x71NewTemplate
0x72SetNewPrevHashTdp (distinct wire format from Mining 0x20!)
0x73RequestTransactionData
0x74RequestTransactionData.Success (with SEQ0_64K<B0_16M> tx list)
0x75RequestTransactionData.Error
0x76SubmitSolution

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.