Skip to main content

netget/server/stratum_v2/
wire.rs

1//! Stratum V2 on the wire, for both roles: the binary data types (sv2-spec §3.1), the frame
2//! header (§3.2), the Common and Mining Protocol messages this server and client exchange, the
3//! encrypted transport framing (§4.6), and the arithmetic a pool and a miner must agree on byte
4//! for byte — the coinbase a job describes, its merkle root, the 80-byte header a share commits
5//! to, its double SHA-256 and where that falls against a target.
6use super::noise::{CipherState, Session, MAC_LEN, MAX_CIPHERTEXT_CHUNK, MAX_PLAINTEXT_CHUNK};
7use anyhow::{bail, ensure, Context, Result};
8use sha2::{Digest, Sha256};
9use std::time::Duration;
10use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
11
12/// extension_type (U16), msg_type (U8), msg_length (U24).
13pub const HEADER_LEN: usize = 6;
14/// The header as one transport message: six bytes and a MAC.
15pub const ENCRYPTED_HEADER_LEN: usize = HEADER_LEN + MAC_LEN;
16/// Bit 15 of extension_type: the payload starts with the channel it is for.
17pub const CHANNEL_MSG_BIT: u16 = 0x8000;
18pub const IO_TIMEOUT: Duration = Duration::from_secs(30);
19/// The only protocol version there is.
20pub const PROTOCOL_VERSION: u16 = 2;
21/// `SetupConnection.protocol` for the Mining Protocol.
22pub const MINING_PROTOCOL: u8 = 0;
23
24/// SetupConnection flags a mining client may send (sv2-spec §5.3.1).
25pub const REQUIRES_STANDARD_JOBS: u32 = 1 << 0;
26pub const REQUIRES_WORK_SELECTION: u32 = 1 << 1;
27pub const REQUIRES_VERSION_ROLLING: u32 = 1 << 2;
28/// The general-purpose version bits (BIP 320) a miner may roll.
29pub const VERSION_ROLLING_MASK: u32 = 0x1fff_e000;
30/// How far past now a share's nTime may be: Bitcoin's MAX_FUTURE_BLOCK_TIME.
31pub const MAX_FUTURE_NTIME: u32 = 7200;
32/// Bytes of extranonce prefix this pool assigns each channel.
33pub const EXTRANONCE_PREFIX_LEN: usize = 4;
34/// An extended channel's extranonce is at most this long (B0_32, sv2-spec §5.3.4).
35pub const MAX_EXTRANONCE: usize = 32;
36/// The coinbase message a pool job carries, in bytes; the scriptSig must stay under 100.
37pub const MAX_COINBASE_MESSAGE: usize = 32;
38/// Difficulty 1: the target 0x00000000FFFF0000…, as a number.
39pub const DIFF1: f64 = 26959535291011309493156476344723991336010898738574164086137773096960.0;
40
41pub mod msg {
42    pub const SETUP_CONNECTION: u8 = 0x00;
43    pub const SETUP_CONNECTION_SUCCESS: u8 = 0x01;
44    pub const SETUP_CONNECTION_ERROR: u8 = 0x02;
45    pub const CHANNEL_ENDPOINT_CHANGED: u8 = 0x03;
46    pub const RECONNECT: u8 = 0x04;
47    pub const OPEN_STANDARD_MINING_CHANNEL: u8 = 0x10;
48    pub const OPEN_STANDARD_MINING_CHANNEL_SUCCESS: u8 = 0x11;
49    pub const OPEN_MINING_CHANNEL_ERROR: u8 = 0x12;
50    pub const OPEN_EXTENDED_MINING_CHANNEL: u8 = 0x13;
51    pub const OPEN_EXTENDED_MINING_CHANNEL_SUCCESS: u8 = 0x14;
52    pub const NEW_MINING_JOB: u8 = 0x15;
53    pub const UPDATE_CHANNEL: u8 = 0x16;
54    pub const UPDATE_CHANNEL_ERROR: u8 = 0x17;
55    pub const CLOSE_CHANNEL: u8 = 0x18;
56    pub const SET_EXTRANONCE_PREFIX: u8 = 0x19;
57    pub const SUBMIT_SHARES_STANDARD: u8 = 0x1a;
58    pub const SUBMIT_SHARES_EXTENDED: u8 = 0x1b;
59    pub const SUBMIT_SHARES_SUCCESS: u8 = 0x1c;
60    pub const SUBMIT_SHARES_ERROR: u8 = 0x1d;
61    pub const NEW_EXTENDED_MINING_JOB: u8 = 0x1f;
62    pub const SET_NEW_PREV_HASH: u8 = 0x20;
63    pub const SET_TARGET: u8 = 0x21;
64    pub const SET_CUSTOM_MINING_JOB: u8 = 0x22;
65    pub const SET_GROUP_CHANNEL: u8 = 0x25;
66}
67
68/// Whether a core message carries the channel_msg bit (sv2-spec §8).
69pub fn channel_bit(msg_type: u8) -> bool {
70    matches!(msg_type, 0x03 | 0x15..=0x24)
71}
72
73/// One frame: who defined the message, which it is, and its plaintext payload.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct Frame {
76    pub extension_type: u16,
77    pub msg_type: u8,
78    pub payload: Vec<u8>,
79}
80
81impl Frame {
82    /// The extension the message belongs to, without the channel_msg bit.
83    pub fn extension(&self) -> u16 {
84        self.extension_type & !CHANNEL_MSG_BIT
85    }
86
87    pub fn header(&self) -> [u8; HEADER_LEN] {
88        let mut h = [0u8; HEADER_LEN];
89        h[0..2].copy_from_slice(&self.extension_type.to_le_bytes());
90        h[2] = self.msg_type;
91        h[3..6].copy_from_slice(&(self.payload.len() as u32).to_le_bytes()[..3]);
92        h
93    }
94}
95
96/// Bytes on the wire for a payload of `plaintext` bytes: 65519-byte chunks, each with a MAC.
97pub fn ciphertext_len(plaintext: usize) -> usize {
98    let rest = plaintext % MAX_PLAINTEXT_CHUNK;
99    plaintext / MAX_PLAINTEXT_CHUNK * MAX_CIPHERTEXT_CHUNK
100        + if rest > 0 { rest + MAC_LEN } else { 0 }
101}
102
103/// Encrypt one frame: the header as one transport message, then the payload in chunks.
104pub fn seal(cipher: &mut CipherState, frame: &Frame) -> Result<Vec<u8>> {
105    ensure!(
106        frame.payload.len() < 1 << 24,
107        "a payload is at most 2^24-1 bytes"
108    );
109    let mut out = cipher.encrypt(&[], &frame.header())?;
110    for chunk in frame.payload.chunks(MAX_PLAINTEXT_CHUNK) {
111        out.extend_from_slice(&cipher.encrypt(&[], chunk)?);
112    }
113    Ok(out)
114}
115
116/// The receiving half of an established session.
117pub struct FrameReader<R> {
118    reader: R,
119    cipher: CipherState,
120    max_payload: usize,
121}
122
123impl<R: AsyncRead + Unpin> FrameReader<R> {
124    pub fn new(reader: R, cipher: CipherState, max_payload: usize) -> Self {
125        Self {
126            reader,
127            cipher,
128            max_payload,
129        }
130    }
131
132    /// The next frame; None at a clean end of stream between frames. A header that does not
133    /// authenticate, a declared length over the bound, or a payload chunk that does not
134    /// authenticate is an error, and the session cannot continue after one.
135    pub async fn next(&mut self, idle: Duration) -> Result<Option<Frame>> {
136        let mut header_ct = [0u8; ENCRYPTED_HEADER_LEN];
137        let first = tokio::time::timeout(idle, self.reader.read(&mut header_ct[..1]))
138            .await
139            .context("Stratum V2 peer idle")??;
140        if first == 0 {
141            return Ok(None);
142        }
143        tokio::time::timeout(IO_TIMEOUT, self.reader.read_exact(&mut header_ct[1..]))
144            .await
145            .context("Stratum V2 frame header deadline")??;
146        let header = self
147            .cipher
148            .decrypt(&[], &header_ct)
149            .context("frame header did not authenticate")?;
150        let extension_type = u16::from_le_bytes([header[0], header[1]]);
151        let msg_type = header[2];
152        let length = u32::from_le_bytes([header[3], header[4], header[5], 0]) as usize;
153        ensure!(
154            length <= self.max_payload,
155            "frame declares a {length}-byte payload, more than the {} this side accepts",
156            self.max_payload
157        );
158        let mut ct = vec![0u8; ciphertext_len(length)];
159        tokio::time::timeout(IO_TIMEOUT, self.reader.read_exact(&mut ct))
160            .await
161            .context("Stratum V2 frame payload deadline")??;
162        let mut payload = Vec::with_capacity(length);
163        for chunk in ct.chunks(MAX_CIPHERTEXT_CHUNK) {
164            payload.extend_from_slice(
165                &self
166                    .cipher
167                    .decrypt(&[], chunk)
168                    .context("frame payload did not authenticate")?,
169            );
170        }
171        ensure!(payload.len() == length, "payload length mismatch");
172        Ok(Some(Frame {
173            extension_type,
174            msg_type,
175            payload,
176        }))
177    }
178}
179
180/// The sending half of an established session.
181pub struct FrameWriter<W> {
182    writer: W,
183    cipher: CipherState,
184}
185
186impl<W: AsyncWrite + Unpin> FrameWriter<W> {
187    pub fn new(writer: W, cipher: CipherState) -> Self {
188        Self { writer, cipher }
189    }
190
191    pub async fn send(&mut self, message: &Message) -> Result<usize> {
192        let bytes = seal(&mut self.cipher, &message.encode())?;
193        tokio::time::timeout(IO_TIMEOUT, self.writer.write_all(&bytes))
194            .await
195            .context("Stratum V2 write deadline")??;
196        Ok(bytes.len())
197    }
198
199    pub async fn shutdown(&mut self) {
200        let _ = self.writer.shutdown().await;
201    }
202}
203
204/// Split a session over a stream's two halves.
205pub fn transport<R: AsyncRead + Unpin, W: AsyncWrite + Unpin>(
206    reader: R,
207    writer: W,
208    session: Session,
209    max_payload: usize,
210) -> (FrameReader<R>, FrameWriter<W>) {
211    (
212        FrameReader::new(reader, session.recv, max_payload),
213        FrameWriter::new(writer, session.send),
214    )
215}
216
217/// Reads the binary data types, refusing anything that runs past the payload.
218pub struct Reader<'a> {
219    buf: &'a [u8],
220    pos: usize,
221}
222
223impl<'a> Reader<'a> {
224    pub fn new(buf: &'a [u8]) -> Self {
225        Self { buf, pos: 0 }
226    }
227
228    fn take(&mut self, n: usize) -> Result<&'a [u8]> {
229        ensure!(
230            self.buf.len() - self.pos >= n,
231            "message ends {} bytes into a {n}-byte field",
232            self.buf.len() - self.pos
233        );
234        let s = &self.buf[self.pos..self.pos + n];
235        self.pos += n;
236        Ok(s)
237    }
238
239    pub fn u8(&mut self) -> Result<u8> {
240        Ok(self.take(1)?[0])
241    }
242    pub fn u16(&mut self) -> Result<u16> {
243        Ok(u16::from_le_bytes(self.take(2)?.try_into()?))
244    }
245    pub fn u24(&mut self) -> Result<u32> {
246        let b = self.take(3)?;
247        Ok(u32::from_le_bytes([b[0], b[1], b[2], 0]))
248    }
249    pub fn u32(&mut self) -> Result<u32> {
250        Ok(u32::from_le_bytes(self.take(4)?.try_into()?))
251    }
252    pub fn u64(&mut self) -> Result<u64> {
253        Ok(u64::from_le_bytes(self.take(8)?.try_into()?))
254    }
255    /// F32: NaN, ±infinity and negative values (including -0.0) are protocol errors.
256    pub fn f32(&mut self) -> Result<f32> {
257        let v = f32::from_le_bytes(self.take(4)?.try_into()?);
258        ensure!(
259            v.is_finite() && v.is_sign_positive(),
260            "F32 field is {v}, which the protocol forbids"
261        );
262        Ok(v)
263    }
264    pub fn bool(&mut self) -> Result<bool> {
265        Ok(self.u8()? & 1 == 1)
266    }
267    pub fn u256(&mut self) -> Result<[u8; 32]> {
268        Ok(self.take(32)?.try_into()?)
269    }
270    fn bytes_u8_prefix(&mut self, max: usize, what: &str) -> Result<Vec<u8>> {
271        let n = self.u8()? as usize;
272        ensure!(n <= max, "{what} declares {n} bytes, at most {max} allowed");
273        Ok(self.take(n)?.to_vec())
274    }
275    pub fn str0_255(&mut self) -> Result<Vec<u8>> {
276        self.bytes_u8_prefix(255, "STR0_255")
277    }
278    pub fn b0_32(&mut self) -> Result<Vec<u8>> {
279        self.bytes_u8_prefix(32, "B0_32")
280    }
281    pub fn b0_255(&mut self) -> Result<Vec<u8>> {
282        self.bytes_u8_prefix(255, "B0_255")
283    }
284    pub fn b0_64k(&mut self) -> Result<Vec<u8>> {
285        let n = self.u16()? as usize;
286        Ok(self.take(n)?.to_vec())
287    }
288    pub fn option_u32(&mut self) -> Result<Option<u32>> {
289        match self.u8()? {
290            0 => Ok(None),
291            1 => Ok(Some(self.u32()?)),
292            n => bail!("OPTION declares {n} elements, at most 1 allowed"),
293        }
294    }
295    pub fn seq0_255_u256(&mut self) -> Result<Vec<[u8; 32]>> {
296        let n = self.u8()? as usize;
297        (0..n).map(|_| self.u256()).collect()
298    }
299    pub fn seq0_64k_u32(&mut self) -> Result<Vec<u32>> {
300        let n = self.u16()? as usize;
301        (0..n).map(|_| self.u32()).collect()
302    }
303    /// The whole payload must have been read: trailing bytes are a malformed message.
304    pub fn finish(&self) -> Result<()> {
305        ensure!(
306            self.pos == self.buf.len(),
307            "{} unexpected bytes after the message",
308            self.buf.len() - self.pos
309        );
310        Ok(())
311    }
312}
313
314#[derive(Default)]
315pub struct Writer(pub Vec<u8>);
316
317impl Writer {
318    pub fn u8(&mut self, v: u8) -> &mut Self {
319        self.0.push(v);
320        self
321    }
322    pub fn u16(&mut self, v: u16) -> &mut Self {
323        self.0.extend_from_slice(&v.to_le_bytes());
324        self
325    }
326    pub fn u32(&mut self, v: u32) -> &mut Self {
327        self.0.extend_from_slice(&v.to_le_bytes());
328        self
329    }
330    pub fn u64(&mut self, v: u64) -> &mut Self {
331        self.0.extend_from_slice(&v.to_le_bytes());
332        self
333    }
334    pub fn f32(&mut self, v: f32) -> &mut Self {
335        self.0.extend_from_slice(&v.to_le_bytes());
336        self
337    }
338    pub fn bool(&mut self, v: bool) -> &mut Self {
339        self.u8(u8::from(v))
340    }
341    pub fn u256(&mut self, v: &[u8; 32]) -> &mut Self {
342        self.0.extend_from_slice(v);
343        self
344    }
345    /// STR0_255, B0_32 and B0_255; callers keep the length within the type's bound.
346    pub fn short_bytes(&mut self, v: &[u8]) -> &mut Self {
347        let v = &v[..v.len().min(255)];
348        self.u8(v.len() as u8);
349        self.0.extend_from_slice(v);
350        self
351    }
352    pub fn b0_64k(&mut self, v: &[u8]) -> &mut Self {
353        let v = &v[..v.len().min(65535)];
354        self.u16(v.len() as u16);
355        self.0.extend_from_slice(v);
356        self
357    }
358    pub fn option_u32(&mut self, v: Option<u32>) -> &mut Self {
359        match v {
360            Some(v) => self.u8(1).u32(v),
361            None => self.u8(0),
362        }
363    }
364    pub fn seq0_255_u256(&mut self, v: &[[u8; 32]]) -> &mut Self {
365        let v = &v[..v.len().min(255)];
366        self.u8(v.len() as u8);
367        for h in v {
368            self.u256(h);
369        }
370        self
371    }
372    pub fn seq0_64k_u32(&mut self, v: &[u32]) -> &mut Self {
373        let v = &v[..v.len().min(65535)];
374        self.u16(v.len() as u16);
375        for n in v {
376            self.u32(*n);
377        }
378        self
379    }
380}
381
382/// The Common and Mining Protocol messages both roles speak. Strings are kept as the bytes
383/// the peer sent; `text` renders them for events and logs.
384#[derive(Debug, Clone, PartialEq)]
385pub enum Message {
386    SetupConnection {
387        protocol: u8,
388        min_version: u16,
389        max_version: u16,
390        flags: u32,
391        endpoint_host: Vec<u8>,
392        endpoint_port: u16,
393        vendor: Vec<u8>,
394        hardware_version: Vec<u8>,
395        firmware: Vec<u8>,
396        device_id: Vec<u8>,
397    },
398    SetupConnectionSuccess {
399        used_version: u16,
400        flags: u32,
401    },
402    SetupConnectionError {
403        flags: u32,
404        error_code: Vec<u8>,
405    },
406    ChannelEndpointChanged {
407        channel_id: u32,
408    },
409    Reconnect {
410        new_host: Vec<u8>,
411        new_port: u16,
412    },
413    OpenStandardMiningChannel {
414        request_id: u32,
415        user_identity: Vec<u8>,
416        nominal_hash_rate: f32,
417        max_target: [u8; 32],
418    },
419    OpenStandardMiningChannelSuccess {
420        request_id: u32,
421        channel_id: u32,
422        target: [u8; 32],
423        extranonce_prefix: Vec<u8>,
424        group_channel_id: u32,
425    },
426    OpenExtendedMiningChannel {
427        request_id: u32,
428        user_identity: Vec<u8>,
429        nominal_hash_rate: f32,
430        max_target: [u8; 32],
431        min_extranonce_size: u16,
432    },
433    OpenExtendedMiningChannelSuccess {
434        request_id: u32,
435        channel_id: u32,
436        target: [u8; 32],
437        extranonce_size: u16,
438        extranonce_prefix: Vec<u8>,
439        group_channel_id: u32,
440    },
441    OpenMiningChannelError {
442        request_id: u32,
443        error_code: Vec<u8>,
444    },
445    UpdateChannel {
446        channel_id: u32,
447        nominal_hash_rate: f32,
448        max_target: [u8; 32],
449    },
450    UpdateChannelError {
451        channel_id: u32,
452        error_code: Vec<u8>,
453    },
454    CloseChannel {
455        channel_id: u32,
456        reason_code: Vec<u8>,
457    },
458    SetExtranoncePrefix {
459        channel_id: u32,
460        extranonce_prefix: Vec<u8>,
461    },
462    SubmitSharesStandard {
463        channel_id: u32,
464        sequence_number: u32,
465        job_id: u32,
466        nonce: u32,
467        ntime: u32,
468        version: u32,
469    },
470    SubmitSharesExtended {
471        channel_id: u32,
472        sequence_number: u32,
473        job_id: u32,
474        nonce: u32,
475        ntime: u32,
476        version: u32,
477        extranonce: Vec<u8>,
478    },
479    SubmitSharesSuccess {
480        channel_id: u32,
481        last_sequence_number: u32,
482        new_submits_accepted_count: u32,
483        new_shares_sum: u64,
484    },
485    SubmitSharesError {
486        channel_id: u32,
487        sequence_number: u32,
488        error_code: Vec<u8>,
489    },
490    NewMiningJob {
491        channel_id: u32,
492        job_id: u32,
493        min_ntime: Option<u32>,
494        version: u32,
495        merkle_root: [u8; 32],
496    },
497    NewExtendedMiningJob {
498        channel_id: u32,
499        job_id: u32,
500        min_ntime: Option<u32>,
501        version: u32,
502        version_rolling_allowed: bool,
503        merkle_path: Vec<[u8; 32]>,
504        coinbase_tx_prefix: Vec<u8>,
505        coinbase_tx_suffix: Vec<u8>,
506    },
507    SetNewPrevHash {
508        channel_id: u32,
509        job_id: u32,
510        prev_hash: [u8; 32],
511        min_ntime: u32,
512        nbits: u32,
513    },
514    SetTarget {
515        channel_id: u32,
516        maximum_target: [u8; 32],
517    },
518    SetGroupChannel {
519        group_channel_id: u32,
520        channel_ids: Vec<u32>,
521    },
522}
523
524impl Message {
525    pub fn msg_type(&self) -> u8 {
526        use Message::*;
527        match self {
528            SetupConnection { .. } => msg::SETUP_CONNECTION,
529            SetupConnectionSuccess { .. } => msg::SETUP_CONNECTION_SUCCESS,
530            SetupConnectionError { .. } => msg::SETUP_CONNECTION_ERROR,
531            ChannelEndpointChanged { .. } => msg::CHANNEL_ENDPOINT_CHANGED,
532            Reconnect { .. } => msg::RECONNECT,
533            OpenStandardMiningChannel { .. } => msg::OPEN_STANDARD_MINING_CHANNEL,
534            OpenStandardMiningChannelSuccess { .. } => msg::OPEN_STANDARD_MINING_CHANNEL_SUCCESS,
535            OpenExtendedMiningChannel { .. } => msg::OPEN_EXTENDED_MINING_CHANNEL,
536            OpenExtendedMiningChannelSuccess { .. } => msg::OPEN_EXTENDED_MINING_CHANNEL_SUCCESS,
537            OpenMiningChannelError { .. } => msg::OPEN_MINING_CHANNEL_ERROR,
538            UpdateChannel { .. } => msg::UPDATE_CHANNEL,
539            UpdateChannelError { .. } => msg::UPDATE_CHANNEL_ERROR,
540            CloseChannel { .. } => msg::CLOSE_CHANNEL,
541            SetExtranoncePrefix { .. } => msg::SET_EXTRANONCE_PREFIX,
542            SubmitSharesStandard { .. } => msg::SUBMIT_SHARES_STANDARD,
543            SubmitSharesExtended { .. } => msg::SUBMIT_SHARES_EXTENDED,
544            SubmitSharesSuccess { .. } => msg::SUBMIT_SHARES_SUCCESS,
545            SubmitSharesError { .. } => msg::SUBMIT_SHARES_ERROR,
546            NewMiningJob { .. } => msg::NEW_MINING_JOB,
547            NewExtendedMiningJob { .. } => msg::NEW_EXTENDED_MINING_JOB,
548            SetNewPrevHash { .. } => msg::SET_NEW_PREV_HASH,
549            SetTarget { .. } => msg::SET_TARGET,
550            SetGroupChannel { .. } => msg::SET_GROUP_CHANNEL,
551        }
552    }
553
554    /// The message's name as the specification writes it.
555    pub fn name(&self) -> &'static str {
556        name_of(self.msg_type())
557    }
558
559    pub fn encode(&self) -> Frame {
560        use Message::*;
561        let mut w = Writer::default();
562        match self {
563            SetupConnection {
564                protocol,
565                min_version,
566                max_version,
567                flags,
568                endpoint_host,
569                endpoint_port,
570                vendor,
571                hardware_version,
572                firmware,
573                device_id,
574            } => {
575                w.u8(*protocol)
576                    .u16(*min_version)
577                    .u16(*max_version)
578                    .u32(*flags)
579                    .short_bytes(endpoint_host)
580                    .u16(*endpoint_port)
581                    .short_bytes(vendor)
582                    .short_bytes(hardware_version)
583                    .short_bytes(firmware)
584                    .short_bytes(device_id);
585            }
586            SetupConnectionSuccess {
587                used_version,
588                flags,
589            } => {
590                w.u16(*used_version).u32(*flags);
591            }
592            SetupConnectionError { flags, error_code } => {
593                w.u32(*flags).short_bytes(error_code);
594            }
595            ChannelEndpointChanged { channel_id } => {
596                w.u32(*channel_id);
597            }
598            Reconnect { new_host, new_port } => {
599                w.short_bytes(new_host).u16(*new_port);
600            }
601            OpenStandardMiningChannel {
602                request_id,
603                user_identity,
604                nominal_hash_rate,
605                max_target,
606            } => {
607                w.u32(*request_id)
608                    .short_bytes(user_identity)
609                    .f32(*nominal_hash_rate)
610                    .u256(max_target);
611            }
612            OpenStandardMiningChannelSuccess {
613                request_id,
614                channel_id,
615                target,
616                extranonce_prefix,
617                group_channel_id,
618            } => {
619                w.u32(*request_id)
620                    .u32(*channel_id)
621                    .u256(target)
622                    .short_bytes(extranonce_prefix)
623                    .u32(*group_channel_id);
624            }
625            OpenExtendedMiningChannel {
626                request_id,
627                user_identity,
628                nominal_hash_rate,
629                max_target,
630                min_extranonce_size,
631            } => {
632                w.u32(*request_id)
633                    .short_bytes(user_identity)
634                    .f32(*nominal_hash_rate)
635                    .u256(max_target)
636                    .u16(*min_extranonce_size);
637            }
638            OpenExtendedMiningChannelSuccess {
639                request_id,
640                channel_id,
641                target,
642                extranonce_size,
643                extranonce_prefix,
644                group_channel_id,
645            } => {
646                w.u32(*request_id)
647                    .u32(*channel_id)
648                    .u256(target)
649                    .u16(*extranonce_size)
650                    .short_bytes(extranonce_prefix)
651                    .u32(*group_channel_id);
652            }
653            OpenMiningChannelError {
654                request_id,
655                error_code,
656            } => {
657                w.u32(*request_id).short_bytes(error_code);
658            }
659            UpdateChannel {
660                channel_id,
661                nominal_hash_rate,
662                max_target,
663            } => {
664                w.u32(*channel_id).f32(*nominal_hash_rate).u256(max_target);
665            }
666            UpdateChannelError {
667                channel_id,
668                error_code,
669            } => {
670                w.u32(*channel_id).short_bytes(error_code);
671            }
672            CloseChannel {
673                channel_id,
674                reason_code,
675            } => {
676                w.u32(*channel_id).short_bytes(reason_code);
677            }
678            SetExtranoncePrefix {
679                channel_id,
680                extranonce_prefix,
681            } => {
682                w.u32(*channel_id).short_bytes(extranonce_prefix);
683            }
684            SubmitSharesStandard {
685                channel_id,
686                sequence_number,
687                job_id,
688                nonce,
689                ntime,
690                version,
691            } => {
692                w.u32(*channel_id)
693                    .u32(*sequence_number)
694                    .u32(*job_id)
695                    .u32(*nonce)
696                    .u32(*ntime)
697                    .u32(*version);
698            }
699            SubmitSharesExtended {
700                channel_id,
701                sequence_number,
702                job_id,
703                nonce,
704                ntime,
705                version,
706                extranonce,
707            } => {
708                w.u32(*channel_id)
709                    .u32(*sequence_number)
710                    .u32(*job_id)
711                    .u32(*nonce)
712                    .u32(*ntime)
713                    .u32(*version)
714                    .short_bytes(extranonce);
715            }
716            SubmitSharesSuccess {
717                channel_id,
718                last_sequence_number,
719                new_submits_accepted_count,
720                new_shares_sum,
721            } => {
722                w.u32(*channel_id)
723                    .u32(*last_sequence_number)
724                    .u32(*new_submits_accepted_count)
725                    .u64(*new_shares_sum);
726            }
727            SubmitSharesError {
728                channel_id,
729                sequence_number,
730                error_code,
731            } => {
732                w.u32(*channel_id)
733                    .u32(*sequence_number)
734                    .short_bytes(error_code);
735            }
736            NewMiningJob {
737                channel_id,
738                job_id,
739                min_ntime,
740                version,
741                merkle_root,
742            } => {
743                w.u32(*channel_id)
744                    .u32(*job_id)
745                    .option_u32(*min_ntime)
746                    .u32(*version)
747                    .u256(merkle_root);
748            }
749            NewExtendedMiningJob {
750                channel_id,
751                job_id,
752                min_ntime,
753                version,
754                version_rolling_allowed,
755                merkle_path,
756                coinbase_tx_prefix,
757                coinbase_tx_suffix,
758            } => {
759                w.u32(*channel_id)
760                    .u32(*job_id)
761                    .option_u32(*min_ntime)
762                    .u32(*version)
763                    .bool(*version_rolling_allowed)
764                    .seq0_255_u256(merkle_path)
765                    .b0_64k(coinbase_tx_prefix)
766                    .b0_64k(coinbase_tx_suffix);
767            }
768            SetNewPrevHash {
769                channel_id,
770                job_id,
771                prev_hash,
772                min_ntime,
773                nbits,
774            } => {
775                w.u32(*channel_id)
776                    .u32(*job_id)
777                    .u256(prev_hash)
778                    .u32(*min_ntime)
779                    .u32(*nbits);
780            }
781            SetTarget {
782                channel_id,
783                maximum_target,
784            } => {
785                w.u32(*channel_id).u256(maximum_target);
786            }
787            SetGroupChannel {
788                group_channel_id,
789                channel_ids,
790            } => {
791                w.u32(*group_channel_id).seq0_64k_u32(channel_ids);
792            }
793        }
794        let msg_type = self.msg_type();
795        Frame {
796            extension_type: if channel_bit(msg_type) {
797                CHANNEL_MSG_BIT
798            } else {
799                0
800            },
801            msg_type,
802            payload: w.0,
803        }
804    }
805
806    /// Decode a core-protocol frame. Unknown message types, fields that run past the payload,
807    /// bytes left over after the last field, and forbidden values are all errors.
808    pub fn decode(frame: &Frame) -> Result<Message> {
809        use Message::*;
810        ensure!(
811            frame.extension() == 0,
812            "extension 0x{:04x} is not a core protocol message",
813            frame.extension()
814        );
815        let mut r = Reader::new(&frame.payload);
816        let m = match frame.msg_type {
817            msg::SETUP_CONNECTION => SetupConnection {
818                protocol: r.u8()?,
819                min_version: r.u16()?,
820                max_version: r.u16()?,
821                flags: r.u32()?,
822                endpoint_host: r.str0_255()?,
823                endpoint_port: r.u16()?,
824                vendor: r.str0_255()?,
825                hardware_version: r.str0_255()?,
826                firmware: r.str0_255()?,
827                device_id: r.str0_255()?,
828            },
829            msg::SETUP_CONNECTION_SUCCESS => SetupConnectionSuccess {
830                used_version: r.u16()?,
831                flags: r.u32()?,
832            },
833            msg::SETUP_CONNECTION_ERROR => SetupConnectionError {
834                flags: r.u32()?,
835                error_code: r.str0_255()?,
836            },
837            msg::CHANNEL_ENDPOINT_CHANGED => ChannelEndpointChanged {
838                channel_id: r.u32()?,
839            },
840            msg::RECONNECT => Reconnect {
841                new_host: r.str0_255()?,
842                new_port: r.u16()?,
843            },
844            msg::OPEN_STANDARD_MINING_CHANNEL => OpenStandardMiningChannel {
845                request_id: r.u32()?,
846                user_identity: r.str0_255()?,
847                nominal_hash_rate: r.f32()?,
848                max_target: r.u256()?,
849            },
850            msg::OPEN_STANDARD_MINING_CHANNEL_SUCCESS => OpenStandardMiningChannelSuccess {
851                request_id: r.u32()?,
852                channel_id: r.u32()?,
853                target: r.u256()?,
854                extranonce_prefix: r.b0_32()?,
855                group_channel_id: r.u32()?,
856            },
857            msg::OPEN_EXTENDED_MINING_CHANNEL => OpenExtendedMiningChannel {
858                request_id: r.u32()?,
859                user_identity: r.str0_255()?,
860                nominal_hash_rate: r.f32()?,
861                max_target: r.u256()?,
862                min_extranonce_size: r.u16()?,
863            },
864            msg::OPEN_EXTENDED_MINING_CHANNEL_SUCCESS => OpenExtendedMiningChannelSuccess {
865                request_id: r.u32()?,
866                channel_id: r.u32()?,
867                target: r.u256()?,
868                extranonce_size: r.u16()?,
869                extranonce_prefix: r.b0_32()?,
870                group_channel_id: r.u32()?,
871            },
872            msg::OPEN_MINING_CHANNEL_ERROR => OpenMiningChannelError {
873                request_id: r.u32()?,
874                error_code: r.str0_255()?,
875            },
876            msg::UPDATE_CHANNEL => UpdateChannel {
877                channel_id: r.u32()?,
878                nominal_hash_rate: r.f32()?,
879                max_target: r.u256()?,
880            },
881            msg::UPDATE_CHANNEL_ERROR => UpdateChannelError {
882                channel_id: r.u32()?,
883                error_code: r.str0_255()?,
884            },
885            msg::CLOSE_CHANNEL => CloseChannel {
886                channel_id: r.u32()?,
887                reason_code: r.str0_255()?,
888            },
889            msg::SET_EXTRANONCE_PREFIX => SetExtranoncePrefix {
890                channel_id: r.u32()?,
891                extranonce_prefix: r.b0_32()?,
892            },
893            msg::SUBMIT_SHARES_STANDARD => SubmitSharesStandard {
894                channel_id: r.u32()?,
895                sequence_number: r.u32()?,
896                job_id: r.u32()?,
897                nonce: r.u32()?,
898                ntime: r.u32()?,
899                version: r.u32()?,
900            },
901            msg::SUBMIT_SHARES_EXTENDED => SubmitSharesExtended {
902                channel_id: r.u32()?,
903                sequence_number: r.u32()?,
904                job_id: r.u32()?,
905                nonce: r.u32()?,
906                ntime: r.u32()?,
907                version: r.u32()?,
908                extranonce: r.b0_32()?,
909            },
910            msg::SUBMIT_SHARES_SUCCESS => SubmitSharesSuccess {
911                channel_id: r.u32()?,
912                last_sequence_number: r.u32()?,
913                new_submits_accepted_count: r.u32()?,
914                new_shares_sum: r.u64()?,
915            },
916            msg::SUBMIT_SHARES_ERROR => SubmitSharesError {
917                channel_id: r.u32()?,
918                sequence_number: r.u32()?,
919                error_code: r.str0_255()?,
920            },
921            msg::NEW_MINING_JOB => NewMiningJob {
922                channel_id: r.u32()?,
923                job_id: r.u32()?,
924                min_ntime: r.option_u32()?,
925                version: r.u32()?,
926                merkle_root: r.u256()?,
927            },
928            msg::NEW_EXTENDED_MINING_JOB => NewExtendedMiningJob {
929                channel_id: r.u32()?,
930                job_id: r.u32()?,
931                min_ntime: r.option_u32()?,
932                version: r.u32()?,
933                version_rolling_allowed: r.bool()?,
934                merkle_path: r.seq0_255_u256()?,
935                coinbase_tx_prefix: r.b0_64k()?,
936                coinbase_tx_suffix: r.b0_64k()?,
937            },
938            msg::SET_NEW_PREV_HASH => SetNewPrevHash {
939                channel_id: r.u32()?,
940                job_id: r.u32()?,
941                prev_hash: r.u256()?,
942                min_ntime: r.u32()?,
943                nbits: r.u32()?,
944            },
945            msg::SET_TARGET => SetTarget {
946                channel_id: r.u32()?,
947                maximum_target: r.u256()?,
948            },
949            msg::SET_GROUP_CHANNEL => SetGroupChannel {
950                group_channel_id: r.u32()?,
951                channel_ids: r.seq0_64k_u32()?,
952            },
953            other => bail!(
954                "message type 0x{other:02x} ({}) is not handled",
955                name_of(other)
956            ),
957        };
958        r.finish()
959            .with_context(|| format!("{} is malformed", name_of(frame.msg_type)))?;
960        Ok(m)
961    }
962}
963
964pub fn name_of(msg_type: u8) -> &'static str {
965    match msg_type {
966        msg::SETUP_CONNECTION => "SetupConnection",
967        msg::SETUP_CONNECTION_SUCCESS => "SetupConnection.Success",
968        msg::SETUP_CONNECTION_ERROR => "SetupConnection.Error",
969        msg::CHANNEL_ENDPOINT_CHANGED => "ChannelEndpointChanged",
970        msg::RECONNECT => "Reconnect",
971        msg::OPEN_STANDARD_MINING_CHANNEL => "OpenStandardMiningChannel",
972        msg::OPEN_STANDARD_MINING_CHANNEL_SUCCESS => "OpenStandardMiningChannel.Success",
973        msg::OPEN_MINING_CHANNEL_ERROR => "OpenMiningChannel.Error",
974        msg::OPEN_EXTENDED_MINING_CHANNEL => "OpenExtendedMiningChannel",
975        msg::OPEN_EXTENDED_MINING_CHANNEL_SUCCESS => "OpenExtendedMiningChannel.Success",
976        msg::NEW_MINING_JOB => "NewMiningJob",
977        msg::UPDATE_CHANNEL => "UpdateChannel",
978        msg::UPDATE_CHANNEL_ERROR => "UpdateChannel.Error",
979        msg::CLOSE_CHANNEL => "CloseChannel",
980        msg::SET_EXTRANONCE_PREFIX => "SetExtranoncePrefix",
981        msg::SUBMIT_SHARES_STANDARD => "SubmitSharesStandard",
982        msg::SUBMIT_SHARES_EXTENDED => "SubmitSharesExtended",
983        msg::SUBMIT_SHARES_SUCCESS => "SubmitShares.Success",
984        msg::SUBMIT_SHARES_ERROR => "SubmitShares.Error",
985        msg::NEW_EXTENDED_MINING_JOB => "NewExtendedMiningJob",
986        msg::SET_NEW_PREV_HASH => "SetNewPrevHash",
987        msg::SET_TARGET => "SetTarget",
988        msg::SET_CUSTOM_MINING_JOB => "SetCustomMiningJob",
989        0x23 => "SetCustomMiningJob.Success",
990        0x24 => "SetCustomMiningJob.Error",
991        msg::SET_GROUP_CHANNEL => "SetGroupChannel",
992        _ => "unknown",
993    }
994}
995
996/// A peer-supplied string as events and logs show it: lossy UTF-8 with controls dropped.
997pub fn text(bytes: &[u8]) -> String {
998    crate::utils::sanitize::strip_controls(&String::from_utf8_lossy(bytes))
999}
1000
1001/// Error codes are printable ASCII (sv2-spec §3.5), at most 255 bytes.
1002pub fn valid_error_code(code: &str) -> bool {
1003    !code.is_empty() && code.len() <= 255 && code.bytes().all(|b| (0x20..0x7f).contains(&b))
1004}
1005
1006pub fn sha256d(data: &[u8]) -> [u8; 32] {
1007    Sha256::digest(Sha256::digest(data)).into()
1008}
1009
1010/// The 80-byte block header a share commits to. Hashes go in as the raw bytes U256 carries.
1011pub fn header(
1012    version: u32,
1013    prev_hash: &[u8; 32],
1014    merkle_root: &[u8; 32],
1015    ntime: u32,
1016    nbits: u32,
1017    nonce: u32,
1018) -> [u8; 80] {
1019    let mut h = [0u8; 80];
1020    h[0..4].copy_from_slice(&version.to_le_bytes());
1021    h[4..36].copy_from_slice(prev_hash);
1022    h[36..68].copy_from_slice(merkle_root);
1023    h[68..72].copy_from_slice(&ntime.to_le_bytes());
1024    h[72..76].copy_from_slice(&nbits.to_le_bytes());
1025    h[76..80].copy_from_slice(&nonce.to_le_bytes());
1026    h
1027}
1028
1029/// Whether a hash, read as a little-endian 256-bit number, is at most the target.
1030pub fn meets(hash: &[u8; 32], target: &[u8; 32]) -> bool {
1031    for i in (0..32).rev() {
1032        if hash[i] != target[i] {
1033            return hash[i] < target[i];
1034        }
1035    }
1036    true
1037}
1038
1039/// A little-endian 256-bit number as a float.
1040pub fn le_value(n: &[u8; 32]) -> f64 {
1041    n.iter()
1042        .rev()
1043        .fold(0f64, |acc, b| acc * 256.0 + f64::from(*b))
1044}
1045
1046/// The difficulty a hash meets: DIFF1 / hash.
1047pub fn difficulty(hash: &[u8; 32]) -> f64 {
1048    let v = le_value(hash);
1049    if v == 0.0 {
1050        f64::MAX
1051    } else {
1052        DIFF1 / v
1053    }
1054}
1055
1056/// The difficulty a target stands for.
1057pub fn target_difficulty(target: &[u8; 32]) -> f64 {
1058    difficulty(target)
1059}
1060
1061/// The target for a difficulty: DIFF1 / difficulty, the largest target when that overflows.
1062pub fn target_for(difficulty: f64) -> [u8; 32] {
1063    if !(difficulty.is_finite() && difficulty > 0.0) {
1064        return [0xff; 32];
1065    }
1066    let t = DIFF1 / difficulty;
1067    if t.is_nan() || t >= 2f64.powi(256) {
1068        return [0xff; 32];
1069    }
1070    if t < 1.0 {
1071        let mut one = [0u8; 32];
1072        one[0] = 1;
1073        return one;
1074    }
1075    // t = m · 2^e with a 53-bit integer m; write m shifted left by e.
1076    let bits = t.to_bits();
1077    let exp = ((bits >> 52) & 0x7ff) as i32 - 1075;
1078    let mantissa = (bits & ((1u64 << 52) - 1)) | (1u64 << 52);
1079    let mut out = [0u8; 32];
1080    if exp < 0 {
1081        let v = mantissa >> (-exp);
1082        out[..8].copy_from_slice(&v.to_le_bytes());
1083        return out;
1084    }
1085    let (byte_shift, bit_shift) = ((exp / 8) as usize, (exp % 8) as u32);
1086    let wide = u128::from(mantissa) << bit_shift;
1087    for (i, b) in wide.to_le_bytes().iter().enumerate() {
1088        if let Some(slot) = out.get_mut(byte_shift + i) {
1089            *slot = *b;
1090        }
1091    }
1092    out
1093}
1094
1095/// The smaller of two targets.
1096pub fn min_target(a: [u8; 32], b: [u8; 32]) -> [u8; 32] {
1097    if meets(&a, &b) {
1098        a
1099    } else {
1100        b
1101    }
1102}
1103
1104/// A hash as block explorers and RPC show it: byte-reversed hex.
1105pub fn display_hex(hash: &[u8; 32]) -> String {
1106    let mut h = *hash;
1107    h.reverse();
1108    hex::encode(h)
1109}
1110
1111/// A hash written as RPC shows it, back to raw bytes. None means all zeros.
1112pub fn parse_display_hash(s: Option<&str>) -> Result<[u8; 32]> {
1113    let Some(s) = s.filter(|s| !s.is_empty()) else {
1114        return Ok([0u8; 32]);
1115    };
1116    let mut b: [u8; 32] = hex::decode(s)
1117        .ok()
1118        .and_then(|v| v.try_into().ok())
1119        .context("prev_hash must be 64 hex digits")?;
1120    b.reverse();
1121    Ok(b)
1122}
1123
1124/// The merkle root a coinbase and a path (deepest first) give.
1125pub fn merkle_root(coinbase: &[u8], path: &[[u8; 32]]) -> [u8; 32] {
1126    let mut root = sha256d(coinbase);
1127    for leaf in path {
1128        let mut both = [0u8; 64];
1129        both[..32].copy_from_slice(&root);
1130        both[32..].copy_from_slice(leaf);
1131        root = sha256d(&both);
1132    }
1133    root
1134}
1135
1136/// A script push of `data` (at most 75 bytes, a single opcode).
1137fn push(script: &mut Vec<u8>, data: &[u8]) {
1138    script.push(data.len() as u8);
1139    script.extend_from_slice(data);
1140}
1141
1142/// BIP 34's height push: the minimal little-endian encoding of a positive height.
1143fn height_push(height: u32) -> Vec<u8> {
1144    let mut n = height.to_le_bytes().to_vec();
1145    while n.len() > 1 && n[n.len() - 1] == 0 {
1146        n.pop();
1147    }
1148    if n[n.len() - 1] & 0x80 != 0 {
1149        n.push(0);
1150    }
1151    n
1152}
1153
1154/// A pool job's coinbase, split where the extranonce goes: a version-2 transaction with one
1155/// input whose scriptSig pushes the BIP 34 height, the message and `extranonce_len` bytes of
1156/// extranonce (prefix and miner's part together), and one zero-value `OP_RETURN "netget"`
1157/// output. Nothing else is in the block, so the merkle path is empty.
1158pub fn coinbase_halves(height: u32, message: &[u8], extranonce_len: usize) -> (Vec<u8>, Vec<u8>) {
1159    let message = &message[..message.len().min(MAX_COINBASE_MESSAGE)];
1160    let mut script = Vec::new();
1161    push(&mut script, &height_push(height));
1162    if !message.is_empty() {
1163        push(&mut script, message);
1164    }
1165    script.push(extranonce_len as u8);
1166    let script_len = script.len() + extranonce_len;
1167    let mut prefix = Vec::new();
1168    prefix.extend_from_slice(&2u32.to_le_bytes());
1169    prefix.push(1);
1170    prefix.extend_from_slice(&[0u8; 32]);
1171    prefix.extend_from_slice(&u32::MAX.to_le_bytes());
1172    prefix.push(script_len as u8);
1173    prefix.extend_from_slice(&script);
1174    let mut suffix = Vec::new();
1175    suffix.extend_from_slice(&u32::MAX.to_le_bytes());
1176    suffix.push(1);
1177    suffix.extend_from_slice(&0u64.to_le_bytes());
1178    let out_script = [&[0x6a, 6][..], b"netget"].concat();
1179    suffix.push(out_script.len() as u8);
1180    suffix.extend_from_slice(&out_script);
1181    suffix.extend_from_slice(&0u32.to_le_bytes());
1182    (prefix, suffix)
1183}
1184
1185/// Whether a share's version differs from the job's only in the bits a miner may roll.
1186pub fn version_allowed(job_version: u32, share_version: u32, rolling: bool) -> bool {
1187    if rolling {
1188        (job_version ^ share_version) & !VERSION_ROLLING_MASK == 0
1189    } else {
1190        job_version == share_version
1191    }
1192}
1193
1194pub fn now_secs() -> u32 {
1195    crate::utils::clock::SystemTime::now()
1196        .duration_since(crate::utils::clock::UNIX_EPOCH)
1197        .map(|d| d.as_secs() as u32)
1198        .unwrap_or(0)
1199}
1200
1201/// SetupConnection flags by name, for events.
1202pub fn flag_names(flags: u32) -> Vec<&'static str> {
1203    let mut out = Vec::new();
1204    if flags & REQUIRES_STANDARD_JOBS != 0 {
1205        out.push("REQUIRES_STANDARD_JOBS");
1206    }
1207    if flags & REQUIRES_WORK_SELECTION != 0 {
1208        out.push("REQUIRES_WORK_SELECTION");
1209    }
1210    if flags & REQUIRES_VERSION_ROLLING != 0 {
1211        out.push("REQUIRES_VERSION_ROLLING");
1212    }
1213    if flags & !0b111 != 0 {
1214        out.push("UNKNOWN");
1215    }
1216    out
1217}
1218
1219/// Responder side of the handshake on a fresh connection: read `-> e` within `deadline`,
1220/// answer with the static key and its certificate. The session keys come back.
1221pub async fn accept_handshake<S: AsyncRead + AsyncWrite + Unpin>(
1222    stream: &mut S,
1223    static_key: &super::noise::EllKey,
1224    authority: &super::noise::Authority,
1225    validity_secs: u32,
1226    deadline: Duration,
1227) -> Result<Session> {
1228    let mut first = [0u8; super::noise::INITIATOR_MESSAGE_LEN];
1229    tokio::time::timeout(deadline, stream.read_exact(&mut first))
1230        .await
1231        .context("Stratum V2 handshake deadline")?
1232        .context("connection closed during the Noise handshake")?;
1233    let (reply, session) =
1234        super::noise::respond(&first, static_key, authority, validity_secs, now_secs())?;
1235    tokio::time::timeout(IO_TIMEOUT, stream.write_all(&reply))
1236        .await
1237        .context("Stratum V2 write deadline")??;
1238    Ok(session)
1239}
1240
1241/// Initiator side: send `-> e`, read the responder's answer within `deadline` and verify its
1242/// certificate against `authority` before returning the session.
1243pub async fn connect_handshake<S: AsyncRead + AsyncWrite + Unpin>(
1244    stream: &mut S,
1245    authority: &[u8; 32],
1246    deadline: Duration,
1247) -> Result<(Session, super::noise::Certificate)> {
1248    let (initiator, first) = super::noise::Initiator::start()?;
1249    tokio::time::timeout(IO_TIMEOUT, stream.write_all(&first))
1250        .await
1251        .context("Stratum V2 write deadline")??;
1252    let mut reply = [0u8; super::noise::RESPONDER_MESSAGE_LEN];
1253    tokio::time::timeout(deadline, stream.read_exact(&mut reply))
1254        .await
1255        .context("Stratum V2 handshake deadline")?
1256        .context("the pool closed the connection during the Noise handshake")?;
1257    initiator.finish(&reply, authority, now_secs())
1258}