From 7b7e35080240f134adbed4f5675648bd3da27fea Mon Sep 17 00:00:00 2001 From: Georges Palauqui Date: Mon, 16 Sep 2024 16:10:51 +0200 Subject: [PATCH] impl a no_std/no_alloc Stratum v1 client --- .github/workflows/lint.yaml | 9 +- .vscode/settings.json | 5 + Cargo.toml | 7 + shell.nix | 44 ++ stratum-v1/Cargo.toml | 44 ++ stratum-v1/examples/tokio-cli.rs | 238 +++++++++++ stratum-v1/src/client/job.rs | 414 ++++++++++++++++++ stratum-v1/src/client/mod.rs | 312 ++++++++++++++ stratum-v1/src/client/notification.rs | 442 +++++++++++++++++++ stratum-v1/src/client/request.rs | 369 ++++++++++++++++ stratum-v1/src/client/response.rs | 590 ++++++++++++++++++++++++++ stratum-v1/src/error.rs | 82 ++++ stratum-v1/src/fmt.rs | 247 +++++++++++ stratum-v1/src/lib.rs | 19 + 14 files changed, 2819 insertions(+), 3 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 shell.nix create mode 100644 stratum-v1/Cargo.toml create mode 100644 stratum-v1/examples/tokio-cli.rs create mode 100644 stratum-v1/src/client/job.rs create mode 100644 stratum-v1/src/client/mod.rs create mode 100644 stratum-v1/src/client/notification.rs create mode 100644 stratum-v1/src/client/request.rs create mode 100644 stratum-v1/src/client/response.rs create mode 100644 stratum-v1/src/error.rs create mode 100644 stratum-v1/src/fmt.rs create mode 100644 stratum-v1/src/lib.rs diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 43f39bf..f7c96cf 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -24,9 +24,12 @@ jobs: # requires a nightly Rust compiler for defining a global allocator and # the exception handler. # - # So, disable it for now. + # The stratum-v1 crate won't compile with all features enabled because + # if has 2 mutualy exclusive features: defmt and log. + # + # So, disable them for now. - run: cargo check --no-default-features --workspace --exclude foundation-ffi - - run: cargo check --all-features + - run: cargo check --all-features --workspace --exclude stratum-v1 is-the-code-formatted: name: Is the code formatted? @@ -63,4 +66,4 @@ jobs: toolchain: 1.77 - run: cargo test - run: cargo test --no-default-features --workspace --exclude foundation-ffi - - run: cargo test --all-features + - run: cargo test --all-features --workspace --exclude stratum-v1 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..779e2db --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: © 2024 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +{ + "nixEnvSelector.nixFile": "${workspaceFolder}/shell.nix" +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index ca2555b..771a6e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "codecs", "ffi", "firmware", + "stratum-v1", "test-vectors", "ur", "urtypes", @@ -28,10 +29,15 @@ bs58 = "0.5" clap = { version = "4", features = ["cargo"] } crc = "3" criterion = { version = "0.4" } +defmt = "0.3" +derive_more = { version = "1.0", default-features = false } +embedded-io = "0.6" +embedded-io-async = "0.6" faster-hex = { version = "0.9", default-features = false } heapless = { version = "0.8", default-features = false } itertools = { version = "0.10", default-features = false } libfuzzer-sys = "0.4" +log = { version = "0.4" } minicbor = { version = "0.24", features = ["derive"] } nom = { version = "7", default-features = false } phf = { version = "0.11", features = ["macros"], default-features = false } @@ -39,6 +45,7 @@ rand_xoshiro = "0.6" secp256k1 = { version = "0.29", default-features = false } serde = { version = "1.0.156", features = ["derive"], default-features = false } serde_json = "1" +serde-json-core = { version = "0.6.0" } uuid = { version = "1", default-features = false } # The crates in this workspace. diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..236e622 --- /dev/null +++ b/shell.nix @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: © 2024 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +{ pkgs ? import {} }: + let + overrides = (builtins.fromTOML (builtins.readFile ./rust-toolchain.toml)); + libPath = with pkgs; lib.makeLibraryPath [ + # load external libraries that you need in your rust project here + ]; +in + pkgs.mkShell rec { + buildInputs = with pkgs; [ + clang + # Replace llvmPackages with llvmPackages_X, where X is the latest LLVM version (at the time of writing, 16) + llvmPackages.bintools + reuse + rustup + ]; + RUSTC_VERSION = overrides.toolchain.channel; + # https://github.com/rust-lang/rust-bindgen#environment-variables + LIBCLANG_PATH = pkgs.lib.makeLibraryPath [ pkgs.llvmPackages_latest.libclang.lib ]; + shellHook = '' + export PATH=$PATH:''${CARGO_HOME:-~/.cargo}/bin + export PATH=$PATH:''${RUSTUP_HOME:-~/.rustup}/toolchains/$RUSTC_VERSION-x86_64-unknown-linux-gnu/bin/ + ''; + # Add precompiled library to rustc search path + RUSTFLAGS = (builtins.map (a: ''-L ${a}/lib'') [ + # add libraries here (e.g. pkgs.libvmi) + ]); + LD_LIBRARY_PATH = libPath; + # Add glibc, clang, glib, and other headers to bindgen search path + BINDGEN_EXTRA_CLANG_ARGS = + # Includes normal include path + (builtins.map (a: ''-I"${a}/include"'') [ + # add dev libraries here (e.g. pkgs.libvmi.dev) + pkgs.glibc.dev + ]) + # Includes with special directory paths + ++ [ + ''-I"${pkgs.llvmPackages_latest.libclang.lib}/lib/clang/${pkgs.llvmPackages_latest.libclang.version}/include"'' + ''-I"${pkgs.glib.dev}/include/glib-2.0"'' + ''-I${pkgs.glib.out}/lib/glib-2.0/include/'' + ]; + } \ No newline at end of file diff --git a/stratum-v1/Cargo.toml b/stratum-v1/Cargo.toml new file mode 100644 index 0000000..e53d1c0 --- /dev/null +++ b/stratum-v1/Cargo.toml @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: © 2024 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +[package] +categories = ["embedded", "no-std"] +description = """Stratum v1 client. + +This provides a `#[no_std]` library to implement a Stratum v1 client.""" +edition = "2021" +homepage.workspace = true +license = "GPL-3.0-or-later AND GPL-3.0-only" +name = "stratum-v1" +version = "0.1.0" + +[dependencies] +bitcoin_hashes = { workspace = true } +defmt = { workspace = true, optional = true } +derive_more = { workspace = true, features = ["from"] } +embedded-io-async = { workspace = true } +faster-hex = { version = "0.10", default-features = false } +heapless = { workspace = true, features = ["serde"] } +log = { workspace = true, optional = true } +serde = { workspace = true } +serde-json-core = { workspace = true, features = ["custom-error-messages"] } + +[features] +defmt-03 = [ + "dep:defmt", + "embedded-io-async/defmt-03", + # "faster-hex/defmt-03", # will enable it after faster-hex publish PR#54 + "heapless/defmt-03", + "serde-json-core/defmt", +] + +[dev-dependencies] +embedded-io = { workspace = true, features = ["std"] } +env_logger = "0.11" +inquire = "0.7" +log = { workspace = true } +tokio = { version = "1", features = ["full"] } + +[[example]] +name = "stratum-v1-cli" +path = "examples/tokio-cli.rs" diff --git a/stratum-v1/examples/tokio-cli.rs b/stratum-v1/examples/tokio-cli.rs new file mode 100644 index 0000000..e09f857 --- /dev/null +++ b/stratum-v1/examples/tokio-cli.rs @@ -0,0 +1,238 @@ +// SPDX-FileCopyrightText: © 2024 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later + +// #![allow(static_mut_refs)] + +use stratum_v1::{Client, Extensions, Message, Share, VersionRolling}; + +use heapless::{String, Vec}; +use inquire::Select; +use log::error; +use std::{ + net::{Ipv4Addr, SocketAddr}, + str::FromStr, + sync::Arc, + time::Duration, +}; +use tokio::{ + net::TcpStream, + sync::{watch, Mutex}, +}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::init(); + + let pool = + Select::new("Which Pool should be used?", vec!["Public-Pool", "Braiins"]).prompt()?; + + let addr = match pool { + "Public-Pool" => SocketAddr::new(Ipv4Addr::new(68, 235, 52, 36).into(), 21496), + "Braiins" => SocketAddr::new(Ipv4Addr::new(64, 225, 5, 77).into(), 3333), + _ => unreachable!(), + }; + + let stream = TcpStream::connect(addr).await?; + + let conn = adapter::FromTokio::::new(stream); + + let mut client = Client::<_, 1480, 512>::new(conn); + client.enable_software_rolling(true, false, false); + + let client_tx = Arc::new(Mutex::new(client)); + let client_rx = Arc::clone(&client_tx); + + let (authorized_tx, mut authorized_rx) = watch::channel(false); + + tokio::spawn(async move { + loop { + let mut c = client_rx.lock().await; + match c.poll_message().await { + Ok(msg) => match msg { + Some(Message::Configured) => { + c.send_connect(Some(String::<32>::from_str("demo").unwrap())) + .await + .unwrap(); + } + Some(Message::Connected) => { + c.send_authorize( + match pool { + "Public-Pool" => String::<64>::from_str( + "1HLQGxzAQWnLore3fWHc2W8UP1CgMv1GKQ.miner1", + ) + .unwrap(), + "Braiins" => String::<64>::from_str("slush.miner1").unwrap(), + _ => unreachable!(), + }, + String::<64>::from_str("x").unwrap(), + ) + .await + .unwrap(); + } + Some(Message::Authorized) => { + authorized_tx.send(true).unwrap(); + } + Some(Message::Share { + accepted: _, + rejected: _, + }) => { + // TODO update the display if any + } + Some(Message::VersionMask(_mask)) => { + // TODO use mask for hardware version rolling is available + } + Some(Message::Difficulty(_diff)) => { + // TODO use diff to filter ASIC reported hits + } + Some(Message::CleanJobs) => { + // TODO clean the job queue and immediately start hashing a new job + } + None => {} + }, + Err(e) => { + error!("Client receive_message error: {:?}", e); + } + } + } + }); + { + let mut c = client_tx.lock().await; + let exts = Extensions { + version_rolling: Some(VersionRolling { + mask: Some(0x1fffe000), + min_bit_count: Some(10), + }), + minimum_difficulty: None, + subscribe_extranonce: None, + info: None, + }; + c.send_configure(exts).await.unwrap(); + } + authorized_rx.changed().await.unwrap(); + loop { + // TODO: use client.roll_job() to get a new job at the rate the hardware need it + tokio::time::sleep(Duration::from_millis(5000)).await; + { + let mut c = client_tx.lock().await; + let mut extranonce2 = Vec::new(); + extranonce2.resize(4, 0).unwrap(); + extranonce2[3] = 0x01; + let fake_share = Share { + job_id: String::<64>::from_str("01").unwrap(), // TODO will come from the Job + extranonce2, // TODO will come from the Job + ntime: 1722789905, // TODO will come from the Job + nonce: 0, // TODO will come from the ASIC hit + version_bits: None, // TODO will come from the ASIC hit if hardware version rolling is enabled + }; + c.send_submit(fake_share).await.unwrap(); + } + } +} + +trait Readable { + fn poll_read_ready( + &self, + cx: &mut core::task::Context<'_>, + ) -> core::task::Poll>; +} + +impl Readable for TcpStream { + fn poll_read_ready( + &self, + cx: &mut core::task::Context<'_>, + ) -> core::task::Poll> { + self.poll_read_ready(cx) + } +} + +mod adapter { + use core::future::poll_fn; + use core::pin::Pin; + use core::task::Poll; + + /// Adapter from `tokio::io` traits. + #[derive(Clone)] + pub struct FromTokio { + inner: T, + } + + impl FromTokio { + /// Create a new adapter. + pub fn new(inner: T) -> Self { + Self { inner } + } + + // /// Consume the adapter, returning the inner object. + // pub fn into_inner(self) -> T { + // self.inner + // } + } + + // impl FromTokio { + // /// Borrow the inner object. + // pub fn inner(&self) -> &T { + // &self.inner + // } + + // /// Mutably borrow the inner object. + // pub fn inner_mut(&mut self) -> &mut T { + // &mut self.inner + // } + // } + + impl embedded_io::ErrorType for FromTokio { + type Error = std::io::Error; + } + + impl embedded_io_async::Read for FromTokio { + async fn read(&mut self, buf: &mut [u8]) -> Result { + // The current tokio implementation (https://github.com/tokio-rs/tokio/blob/tokio-1.33.0/tokio/src/io/poll_evented.rs#L165) + // does not consider the case of buf.is_empty() as a special case, + // which can cause Poll::Pending to be returned at the end of the stream when called with an empty buffer. + // This poll will, however, never become ready, as no more bytes will be received. + if buf.is_empty() { + return Ok(0); + } + + poll_fn(|cx| { + let mut buf = tokio::io::ReadBuf::new(buf); + match Pin::new(&mut self.inner).poll_read(cx, &mut buf) { + Poll::Ready(r) => match r { + Ok(()) => Poll::Ready(Ok(buf.filled().len())), + Err(e) => Poll::Ready(Err(e)), + }, + Poll::Pending => Poll::Pending, + } + }) + .await + } + } + + impl embedded_io_async::ReadReady for FromTokio { + fn read_ready(&mut self) -> Result { + // TODO: This crash at runtime : + // Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) + // attempted to block the current thread while the thread is being used to drive asynchronous tasks. + tokio::runtime::Handle::current().block_on(poll_fn(|cx| { + match Pin::new(&mut self.inner).poll_read_ready(cx) { + Poll::Ready(_) => Poll::Ready(Ok(true)), + Poll::Pending => Poll::Ready(Ok(false)), + } + })) + } + } + + impl embedded_io_async::Write for FromTokio { + async fn write(&mut self, buf: &[u8]) -> Result { + match poll_fn(|cx| Pin::new(&mut self.inner).poll_write(cx, buf)).await { + Ok(0) if !buf.is_empty() => Err(std::io::ErrorKind::WriteZero.into()), + Ok(n) => Ok(n), + Err(e) => Err(e), + } + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + poll_fn(|cx| Pin::new(&mut self.inner).poll_flush(cx)).await + } + } +} diff --git a/stratum-v1/src/client/job.rs b/stratum-v1/src/client/job.rs new file mode 100644 index 0000000..85560cb --- /dev/null +++ b/stratum-v1/src/client/job.rs @@ -0,0 +1,414 @@ +// SPDX-FileCopyrightText: © 2024 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later + +use super::notification::Work; +use crate::{Error, Result}; + +use bitcoin_hashes::{sha256d::Hash as DHash, Hash}; +use heapless::{String, Vec}; + +#[derive(Debug, PartialEq)] +pub struct Header { + pub version: i32, + pub prev_blockhash: [u8; 32], + pub merkle_root: [u8; 32], + pub ntime: u32, + pub nbits: u32, + pub nonce: u32, +} + +#[derive(Debug, PartialEq)] +pub struct Job { + pub job_id: String<32>, + pub extranonce2: Vec, + pub header: Header, +} + +#[cfg(feature = "defmt-03")] +impl defmt::Format for Job { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!( + fmt, + "Job {{ job_id: {}, extranonce2: {:?}, header: {{ version: {:x}, prev_block_hash: {:x}, merkle_root: {:x}, ntime: {:x}, nbits: {:x}, nonce: {:x} }} }}", + self.job_id, + self.extranonce2, + self.header.version, + self.header.prev_blockhash, + self.header.merkle_root, + self.header.ntime, + self.header.nbits, + self.header.nonce + ); + } +} + +#[derive(Debug, Default)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub(crate) struct JobCreator { + last_work: Option, + version_mask: i32, + pub(crate) version_rolling: bool, + version_bits: u16, + extranonce1: Vec, + extranonce2_size: usize, + pub(crate) extranonce2_rolling: bool, + extranonce2: Vec, + pub(crate) ntime_rolling: bool, + ntime_bits: u32, +} + +impl JobCreator { + pub(crate) fn set_version_mask(&mut self, mask: u32) { + self.version_mask = mask as i32; + } + + pub(crate) fn set_extranonces( + &mut self, + extranonce1: Vec, + extranonce2_size: usize, + ) -> Result<()> { + self.extranonce1 = extranonce1; + self.extranonce2_size = extranonce2_size; + self.extranonce2 + .resize_default(extranonce2_size) + .map_err(|_| Error::VecFull) + } + + pub(crate) fn set_work(&mut self, work: Work) -> Result<()> { + self.last_work = Some(work); + self.version_bits = 0; + self.extranonce2 + .resize_default(self.extranonce2_size) + .map_err(|_| Error::VecFull)?; + self.extranonce2.fill(0); + self.ntime_bits = 0; + Ok(()) + } + + fn merkle_root(&self, work: &Work) -> Result<[u8; 32]> { + let mut coinbase = Vec::::new(); + coinbase + .extend_from_slice(work.coinb1.as_slice()) + .map_err(|_| Error::VecFull)?; + coinbase + .extend_from_slice(self.extranonce1.as_slice()) + .map_err(|_| Error::VecFull)?; + coinbase + .extend_from_slice(self.extranonce2.as_slice()) + .map_err(|_| Error::VecFull)?; + coinbase + .extend_from_slice(work.coinb2.as_slice()) + .map_err(|_| Error::VecFull)?; + let coinbase_id = DHash::hash(coinbase.as_slice()).to_byte_array(); + let mut merkle_root = coinbase_id; + for node in &work.merkle_branch { + let mut to_hash = [0; 64]; + to_hash[..32].clone_from_slice(merkle_root.as_slice()); + to_hash[32..].copy_from_slice(node.as_slice()); + merkle_root = DHash::hash(to_hash.as_slice()).to_byte_array(); + } + Ok(merkle_root) + } + + pub(crate) fn roll(&mut self) -> Result { + let work = self.last_work.as_ref().ok_or(Error::NoWork)?; + let rolled_version = if self.version_rolling { + self.version_bits = self.version_bits.wrapping_add(1); + (work.version & !self.version_mask) + | (((self.version_bits as i32) << self.version_mask.trailing_zeros()) + & self.version_mask) + } else { + work.version + }; + if self.extranonce2_rolling { + for i in (0..self.extranonce2_size).rev() { + match self.extranonce2[i].checked_add(1) { + Some(v) => { + self.extranonce2[i] = v; + break; + } + None => self.extranonce2[i] = 0, + } + } + } + let rolled_ntime = if self.ntime_rolling { + self.ntime_bits = self.ntime_bits.wrapping_add(1); + work.ntime + self.ntime_bits + } else { + work.ntime + }; + Ok(Job { + job_id: work.job_id.clone(), + extranonce2: self.extranonce2.clone(), + header: Header { + version: rolled_version, + prev_blockhash: work.prev_hash, + merkle_root: self.merkle_root(work)?, + ntime: rolled_ntime, + nbits: work.nbits, + nonce: 0, + }, + }) + } +} + +#[cfg(test)] +mod tests { + use core::str::FromStr; + + use super::*; + + #[test] + fn test_roll() { + let mut job_creator = JobCreator::default(); + assert_eq!(job_creator.roll(), Err(Error::NoWork)); + let job_id = hstring!(32, "1234"); + job_creator + .set_work(Work { + job_id: job_id.clone(), + prev_hash: [0; 32], + coinb1: Vec::new(), + coinb2: Vec::new(), + merkle_branch: Vec::new(), + version: 0x2000_0000, + nbits: 0x1234_5678, + ntime: 0, + clean_jobs: false, + }) + .unwrap(); + job_creator.set_version_mask(0x1fff_e000); + job_creator.set_extranonces(Vec::new(), 1).unwrap(); + assert_eq!( + job_creator.roll(), + Ok(Job { + job_id: job_id.clone(), + extranonce2: hvec!(u8, 8, &[0]), + header: Header { + version: 0x2000_0000, + prev_blockhash: [0; 32], + merkle_root: [ + 0x14, 0x06, 0xe0, 0x58, 0x81, 0xe2, 0x99, 0x36, 0x77, 0x66, 0xd3, 0x13, + 0xe2, 0x6c, 0x05, 0x56, 0x4e, 0xc9, 0x1b, 0xf7, 0x21, 0xd3, 0x17, 0x26, + 0xbd, 0x6e, 0x46, 0xe6, 0x06, 0x89, 0x53, 0x9a, + ], + ntime: 0, + nbits: 0x1234_5678, + nonce: 0, + } + }) + ); + job_creator.version_rolling = true; + assert_eq!( + job_creator.roll(), + Ok(Job { + job_id: job_id.clone(), + extranonce2: hvec!(u8, 8, &[0]), + header: Header { + version: 0x2000_2000, + prev_blockhash: [0; 32], + merkle_root: [ + 0x14, 0x06, 0xe0, 0x58, 0x81, 0xe2, 0x99, 0x36, 0x77, 0x66, 0xd3, 0x13, + 0xe2, 0x6c, 0x05, 0x56, 0x4e, 0xc9, 0x1b, 0xf7, 0x21, 0xd3, 0x17, 0x26, + 0xbd, 0x6e, 0x46, 0xe6, 0x06, 0x89, 0x53, 0x9a, + ], + ntime: 0, + nbits: 0x1234_5678, + nonce: 0, + } + }) + ); + job_creator.ntime_rolling = true; + assert_eq!( + job_creator.roll(), + Ok(Job { + job_id: job_id.clone(), + extranonce2: hvec!(u8, 8, &[0]), + header: Header { + version: 0x2000_4000, + prev_blockhash: [0; 32], + merkle_root: [ + 0x14, 0x06, 0xe0, 0x58, 0x81, 0xe2, 0x99, 0x36, 0x77, 0x66, 0xd3, 0x13, + 0xe2, 0x6c, 0x05, 0x56, 0x4e, 0xc9, 0x1b, 0xf7, 0x21, 0xd3, 0x17, 0x26, + 0xbd, 0x6e, 0x46, 0xe6, 0x06, 0x89, 0x53, 0x9a, + ], + ntime: 1, + nbits: 0x1234_5678, + nonce: 0, + } + }) + ); + job_creator.extranonce2_rolling = true; + assert_eq!( + job_creator.roll(), + Ok(Job { + job_id: job_id.clone(), + extranonce2: hvec!(u8, 8, &[1]), + header: Header { + version: 0x2000_6000, + prev_blockhash: [0; 32], + merkle_root: [ + 0x9c, 0x12, 0xcf, 0xdc, 0x04, 0xc7, 0x45, 0x84, 0xd7, 0x87, 0xac, 0x3d, + 0x23, 0x77, 0x21, 0x32, 0xc1, 0x85, 0x24, 0xbc, 0x7a, 0xb2, 0x8d, 0xec, + 0x42, 0x19, 0xb8, 0xfc, 0x5b, 0x42, 0x5f, 0x70, + ], + ntime: 2, + nbits: 0x1234_5678, + nonce: 0, + } + }) + ); + job_creator + .set_work(Work { + job_id: job_id.clone(), + prev_hash: [0; 32], + coinb1: Vec::new(), + coinb2: Vec::new(), + merkle_branch: Vec::new(), + version: 0x2000_0000, + nbits: 0x1234_5678, + ntime: 0, + clean_jobs: false, + }) + .unwrap(); + assert_eq!( + job_creator.roll(), + Ok(Job { + job_id: job_id.clone(), + extranonce2: hvec!(u8, 8, &[1]), + header: Header { + version: 0x2000_2000, + prev_blockhash: [0; 32], + merkle_root: [ + 0x9c, 0x12, 0xcf, 0xdc, 0x04, 0xc7, 0x45, 0x84, 0xd7, 0x87, 0xac, 0x3d, + 0x23, 0x77, 0x21, 0x32, 0xc1, 0x85, 0x24, 0xbc, 0x7a, 0xb2, 0x8d, 0xec, + 0x42, 0x19, 0xb8, 0xfc, 0x5b, 0x42, 0x5f, 0x70, + ], + ntime: 1, + nbits: 0x1234_5678, + nonce: 0, + } + }) + ); + } + + #[test] + fn test_merkle_root() { + // example from https://github.com/stratum-mining/stratum/pull/305/files + let mut job_creator = JobCreator::default(); + job_creator + .set_extranonces(hvec!(u8, 8, &[120, 55, 179, 37]), 4) + .unwrap(); + assert_eq!( + job_creator.merkle_root(&Work { + job_id: hstring!(32, "662ede"), + prev_hash: [ + 0xa8, 0x0f, 0x3e, 0x7f, 0xb2, 0xfa, 0xe8, 0x23, 0x68, 0x12, 0xba, 0xa7, 0x66, + 0xc2, 0xc6, 0x14, 0x1b, 0x91, 0x18, 0x53, 0x00, 0x01, 0xc1, 0xce, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + coinb1: hvec!( + u8, + 128, + &[ + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 75, 3, 63, 146, 11, + 250, 190, 109, 109, 86, 6, 110, 64, 228, 218, 247, 203, 127, 75, 141, 53, + 51, 197, 180, 38, 117, 115, 221, 103, 2, 11, 85, 213, 65, 221, 74, 90, 97, + 128, 91, 182, 1, 0, 0, 0, 0, 0, 0, 0, 49, 101, 7, 7, 139, 168, 76, 0, 1, 0, + 0, 0, 0, 0, 0, 70, 84, 183, 110, 24, 47, 115, 108, 117, 115, 104, 47, 0, 0, + 0, 0, 3, + ] + ), + coinb2: hvec!( + u8, + 130, + &[ + 25, 118, 169, 20, 124, 21, 78, 209, 220, 89, 96, 158, 61, 38, 171, 178, + 223, 46, 163, 213, 135, 205, 140, 65, 136, 172, 0, 0, 0, 0, 0, 0, 0, 0, 44, + 106, 76, 41, 82, 83, 75, 66, 76, 79, 67, 75, 58, 216, 82, 49, 182, 148, + 133, 228, 178, 20, 248, 55, 219, 145, 83, 227, 86, 32, 97, 240, 182, 3, + 175, 116, 196, 69, 114, 83, 46, 0, 71, 230, 205, 0, 0, 0, 0, 0, 0, 0, 0, + 38, 106, 36, 170, 33, 169, 237, 179, 75, 32, 206, 223, 111, 113, 150, 112, + 248, 21, 36, 163, 123, 107, 168, 153, 76, 233, 86, 77, 218, 162, 59, 48, + 26, 180, 38, 62, 34, 3, 185, 0, 0, 0, 0, + ] + ), + merkle_branch: hveca!( + u8, + 32, + 16, + &[ + [ + 122, 97, 64, 124, 164, 158, 164, 14, 87, 119, 226, 169, 34, 196, 251, + 51, 31, 131, 109, 250, 13, 54, 94, 6, 177, 27, 156, 154, 101, 30, 123, + 159, + ], + [ + 180, 113, 121, 253, 215, 85, 129, 38, 108, 2, 86, 66, 46, 12, 131, 139, + 130, 87, 29, 92, 59, 164, 247, 114, 251, 140, 129, 88, 127, 196, 125, + 116, + ], + [ + 171, 77, 225, 148, 80, 32, 41, 157, 246, 77, 161, 49, 87, 139, 214, + 236, 149, 164, 192, 128, 195, 9, 5, 168, 131, 27, 250, 9, 60, 179, 206, + 94, + ], + [ + 6, 187, 202, 75, 155, 220, 255, 166, 199, 35, 182, 220, 20, 96, 123, + 41, 109, 40, 186, 142, 13, 139, 230, 164, 116, 177, 217, 23, 16, 123, + 135, 202, + ], + [ + 109, 45, 171, 89, 223, 39, 132, 14, 150, 128, 241, 113, 136, 227, 105, + 123, 224, 48, 66, 240, 189, 186, 222, 49, 173, 143, 80, 90, 110, 219, + 192, 235, + ], + [ + 196, 7, 21, 180, 228, 161, 182, 132, 28, 153, 242, 12, 210, 127, 157, + 86, 62, 123, 181, 33, 84, 3, 105, 129, 148, 162, 5, 152, 64, 7, 196, + 156, + ], + [ + 22, 16, 18, 180, 109, 237, 68, 167, 197, 10, 195, 134, 11, 119, 219, + 184, 49, 140, 239, 45, 27, 210, 212, 120, 186, 60, 155, 105, 106, 219, + 218, 32, + ], + [ + 83, 228, 21, 241, 42, 240, 8, 254, 109, 156, 59, 171, 167, 46, 183, 60, + 27, 63, 241, 211, 235, 179, 147, 99, 46, 3, 22, 166, 159, 169, 183, + 159, + ], + [ + 230, 81, 3, 190, 66, 73, 200, 55, 94, 135, 209, 50, 92, 193, 114, 202, + 141, 170, 124, 142, 206, 29, 88, 9, 22, 110, 203, 145, 238, 66, 166, + 35, + ], + [ + 43, 106, 86, 239, 237, 74, 208, 202, 247, 133, 88, 42, 15, 77, 163, + 186, 85, 26, 89, 151, 5, 19, 30, 122, 108, 220, 215, 104, 152, 226, + 113, 55, + ], + [ + 148, 76, 200, 221, 206, 54, 56, 45, 252, 60, 123, 202, 195, 73, 144, + 65, 168, 184, 59, 130, 145, 229, 250, 44, 213, 70, 175, 128, 34, 31, + 102, 80, + ], + [ + 203, 112, 102, 31, 49, 147, 24, 25, 245, 61, 179, 146, 205, 127, 126, + 100, 78, 204, 228, 146, 209, 154, 89, 194, 209, 81, 57, 167, 88, 251, + 44, 76, + ] + ] + ), + version: 0x2000_0000, + nbits: 0x1703_1abe, + ntime: 0x66aa_d286, + clean_jobs: false, + }), + Ok([ + 73, 100, 41, 247, 106, 44, 1, 242, 3, 64, 100, 1, 98, 155, 40, 91, 170, 255, 170, + 29, 193, 255, 244, 71, 236, 29, 134, 218, 94, 45, 78, 77, + ]) + ); + } +} diff --git a/stratum-v1/src/client/mod.rs b/stratum-v1/src/client/mod.rs new file mode 100644 index 0000000..43bd163 --- /dev/null +++ b/stratum-v1/src/client/mod.rs @@ -0,0 +1,312 @@ +// SPDX-FileCopyrightText: © 2024 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later + +mod job; +mod notification; +mod request; +mod response; + +use crate::{Error, Result}; +pub use job::Job; +use job::JobCreator; +use notification::Notification; +use request::ReqKind; +pub use request::{Extensions, Info, Share, VersionRolling}; +use response::Subscription; + +use embedded_io_async::{Read, ReadReady, Write}; +use heapless::{FnvIndexMap, String, Vec}; + +#[derive(Debug)] +// #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub struct Client { + network_conn: C, + rx_buf: [u8; RX_BUF_SIZE], + rx_free_pos: usize, + tx_buf: [u8; TX_BUF_SIZE], + reqs: FnvIndexMap, + job_creator: JobCreator, + configuration: Option, + subscriptions: Vec, + shares_accepted: u64, + shares_rejected: u64, + req_id: u64, + connected: bool, + authorized: bool, + user: String<64>, +} + +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub enum Message { + Configured, + Connected, + Authorized, + Share { accepted: u64, rejected: u64 }, + VersionMask(u32), + Difficulty(f64), + CleanJobs, +} + +impl + Client +{ + pub fn new(network_conn: C) -> Client { + Client { + network_conn, + rx_buf: [0; RX_BUF_SIZE], + rx_free_pos: 0, + tx_buf: [0; TX_BUF_SIZE], + reqs: FnvIndexMap::new(), + job_creator: JobCreator::default(), + configuration: None, + subscriptions: Vec::new(), + shares_accepted: 0, + shares_rejected: 0, + req_id: 0, + connected: false, + authorized: false, + user: String::new(), + } + } +} + +impl + Client +{ + pub fn enable_software_rolling(&mut self, version: bool, extranonce2: bool, ntime: bool) { + self.job_creator.version_rolling = version; + self.job_creator.extranonce2_rolling = extranonce2; + self.job_creator.ntime_rolling = ntime; + debug!( + "Software Rolling Enabled : version: {}, extranonce2: {}, ntime: {}", + version, extranonce2, ntime + ); + } + + pub async fn roll_job(&mut self) -> Result { + self.job_creator.roll() + } + + pub async fn poll_message(&mut self) -> Result> { + let mut msg = None; + let mut start = 0; + while let Some(stop) = self.rx_buf[start..self.rx_free_pos] + .iter() + .position(|&c| c == b'\n') + { + let line = &self.rx_buf[start..stop]; + debug!( + "Received Message [{}..{}], free pos: {}", + start, stop, self.rx_free_pos + ); + trace!("{:?}", line); + if let Some(id) = response::parse_id(line)? { + // it's a Response + match self.reqs.get(&id) { + Some(ReqKind::Configure) => { + self.configuration = Some(response::parse_configure(line)?); + self.reqs.remove(&id); + info!("Stratum v1 Client Configured"); + msg = Some(Message::Configured); + } + Some(ReqKind::Connect) => { + let conn = response::parse_connect(line)?; + self.subscriptions = conn.subscriptions; + self.job_creator + .set_extranonces(conn.extranonce1, conn.extranonce2_size)?; + self.connected = true; + self.reqs.remove(&id); + info!("Stratum v1 Client Connected"); + msg = Some(Message::Connected); + } + Some(ReqKind::Authorize) => { + if response::parse_authorize(line)? { + self.authorized = true; + self.reqs.remove(&id); + info!("Stratum v1 Client Authorized"); + msg = Some(Message::Authorized); + } + } + Some(ReqKind::Submit) => { + match response::parse_submit(line) { + Ok(_) => { + self.shares_accepted += 1; + info!( + "Share #{} Accepted, count: {}/{}", + id, self.shares_accepted, self.shares_rejected + ); + } + Err(Error::Pool { + code: _c, // TODO: use this code to differentiate why share has been rejected + message: _, + detail: _, + }) => { + self.shares_rejected += 1; + info!( + "Share #{} Rejected, count: {}/{}", + id, self.shares_accepted, self.shares_rejected + ); + } + Err(e) => return Err(e), + } + self.reqs.remove(&id); + msg = Some(Message::Share { + accepted: self.shares_accepted, + rejected: self.shares_rejected, + }); + } + None => return Err(Error::IdNotFound(id)), + } + } else { + // it's a Notification + match notification::parse_method(line)? { + Notification::SetVersionMask => { + let mask = notification::parse_set_version_mask(line)?; + self.job_creator.set_version_mask(mask); + msg = Some(Message::VersionMask(mask)); + info!("Set Version Mask: 0x{:x}", mask); + } + Notification::SetDifficulty => { + let diff = notification::parse_set_difficulty(line)?; + msg = Some(Message::Difficulty(diff)); + info!("Set Difficulty: {}", diff); + } + Notification::Notify => { + let work = notification::parse_notify(line)?; + if work.clean_jobs { + msg = Some(Message::CleanJobs); + } + info!("New Work: {:?}", work); + self.job_creator.set_work(work)?; + } + } + } + start = stop + 1; + } + if start > 0 && self.rx_free_pos > start { + debug!("copy {} bytes @0", self.rx_free_pos - start); + self.rx_buf.copy_within(start..self.rx_free_pos, 0); + self.rx_free_pos -= start; + } + if self + .network_conn + .read_ready() + .map_err(|_| Error::NetworkError)? + { + let n = self + .network_conn + .read(self.rx_buf[self.rx_free_pos..].as_mut()) + .await + .map_err(|_| Error::NetworkError)?; + debug!("read {} bytes @{}", n, self.rx_free_pos); + trace!("{:?}", &self.rx_buf[self.rx_free_pos..self.rx_free_pos + n]); + self.rx_free_pos += n; + } + Ok(msg) + } + + fn prepare_req(&mut self, req_kind: ReqKind) -> Result<()> { + self.req_id += 1; + self.reqs + .insert(self.req_id, req_kind) + .map_err(|_| Error::MapFull)?; + Ok(()) + } + + async fn send_req(&mut self, req_len: usize) -> Result<()> { + self.tx_buf[req_len] = 0x0a; + trace!("{:?}", &self.tx_buf[..req_len + 1]); + self.network_conn + .write_all(&self.tx_buf[..req_len + 1]) + .await + .map_err(|_| Error::NetworkError) + } + + /// # Configure Client + /// + /// ## Parameters + /// + /// exts: a list of extensions to configure. + /// + pub async fn send_configure(&mut self, exts: Extensions) -> Result<()> { + if self.configuration.is_some() { + return Err(Error::AlreadyConfigured); + } + self.prepare_req(ReqKind::Configure)?; + let n = request::configure(self.req_id, exts, self.tx_buf.as_mut_slice())?; + debug!("Send Configure: {} bytes, id = {}", n, self.req_id); + self.send_req(n).await + } + + /// # Connect Client + /// + /// ## Parameters + /// + /// identifier: a string to identify the client to the pool. + /// + pub async fn send_connect(&mut self, identifier: Option>) -> Result<()> { + if self.configuration.is_none() { + return Err(Error::NotConfigured); + } + if self.connected { + return Err(Error::AlreadyConnected); + } + self.prepare_req(ReqKind::Connect)?; + let n = request::connect(self.req_id, identifier, self.tx_buf.as_mut_slice())?; + debug!("Send Connect: {} bytes, id = {}", n, self.req_id); + self.send_req(n).await + } + + /// # Authorize Client + /// + /// ## Parameters + /// + /// user: a string with user name. + /// Usually composed by ".". + /// + /// pass: a string with user password. + /// + pub async fn send_authorize(&mut self, user: String<64>, pass: String<64>) -> Result<()> { + if !self.connected { + return Err(Error::NotConnected); + } + if self.authorized { + return Err(Error::AlreadyAuthorized); + } + self.prepare_req(ReqKind::Authorize)?; + self.user = user.clone(); + let n = request::authorize(self.req_id, user, pass, self.tx_buf.as_mut_slice())?; + debug!("Send Authorize: {} bytes, id = {}", n, self.req_id); + self.send_req(n).await + } + + /// # Submit a Share + /// + /// ## Parameters + /// + /// job_id: a string with the Job ID given in the Mining Job Notification. + /// + /// extranonce2: a slice of 8-bits unsigned integer with the share's Extranonce2. + /// + /// ntime: a 32-bits unsigned integer with the share's nTime. + /// + /// nonce: a 32-bits unsigned integer with the share's nOnce. + /// + /// version_bits: an optional 32-bits unsigned integer with the share's version_bits. + /// + pub async fn send_submit(&mut self, share: Share) -> Result<()> { + if !self.authorized { + return Err(Error::Unauthorized); + } + self.prepare_req(ReqKind::Submit)?; + let n = request::submit( + self.req_id, + self.user.clone(), + share, + self.tx_buf.as_mut_slice(), + )?; + debug!("Send Submit: {} bytes, id = {}", n, self.req_id); + self.send_req(n).await + } +} diff --git a/stratum-v1/src/client/notification.rs b/stratum-v1/src/client/notification.rs new file mode 100644 index 0000000..cac2778 --- /dev/null +++ b/stratum-v1/src/client/notification.rs @@ -0,0 +1,442 @@ +// SPDX-FileCopyrightText: © 2024 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later + +use crate::{Error, Result}; +use faster_hex::hex_decode; +use heapless::{String, Vec}; +use serde::Deserialize; + +use super::request::Request; + +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub struct Work { + pub job_id: String<32>, + pub prev_hash: [u8; 32], + pub coinb1: Vec, + pub coinb2: Vec, + pub merkle_branch: Vec<[u8; 32], 16>, + pub version: i32, + pub nbits: u32, + pub ntime: u32, + pub clean_jobs: bool, +} + +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub enum Notification { + SetVersionMask, + Notify, + SetDifficulty, +} + +pub(crate) fn parse_method(resp: &[u8]) -> Result { + #[derive(Debug, Deserialize)] + #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] + struct MethodOnly { + method: String<32>, + } + match serde_json_core::from_slice::(resp)? + .0 + .method + .as_str() + { + "mining.set_version_mask" => Ok(Notification::SetVersionMask), + "mining.notify" => Ok(Notification::Notify), + "mining.set_difficulty" => Ok(Notification::SetDifficulty), + _ => Err(Error::UnknownNotification), + } +} + +pub(crate) fn parse_set_version_mask(resp: &[u8]) -> Result { + let mut v = [0; 4]; + hex_decode( + serde_json_core::from_slice::, 1>>>(resp)? + .0 + .params + .ok_or(Error::RpcBadRequest)? + .pop() + .ok_or(Error::VecEmpty)? + .as_bytes(), + &mut v, + )?; + Ok(u32::from_be_bytes(v)) +} + +pub(crate) fn parse_notify(resp: &[u8]) -> Result { + #[derive(Debug, Deserialize)] + #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] + struct WorkRaw( + // Job ID. This is included when miners submit a results so work can be matched with proper transactions. + String<32>, + // Hash of previous block. Used to build the header. + String<64>, + // Generation transaction (part 1). The miner inserts ExtraNonce1 and ExtraNonce2 after this section of the transaction data. + String<256>, + // Generation transaction (part 2). The miner appends this after the first part of the transaction data and the two ExtraNonce values. + String<260>, + // List of merkle branches. The generation transaction is hashed against the merkle branches to build the final merkle root. + Vec, 16>, + // Bitcoin block version. Used in the block header. + String<8>, + // nBits. The encoded network difficulty. Used in the block header. + String<8>, + // nTime. The current time. nTime rolling should be supported, but should not increase faster than actual time. + String<8>, + // Clean Jobs. If true, miners should abort their current work and immediately use the new job, even if it degrades hashrate in the short term. + // If false, they can still use the current job, but should move to the new one as soon as possible without impacting hashrate. + bool, + ); + + impl TryFrom for Work { + type Error = Error; + + fn try_from(raw: WorkRaw) -> Result { + let mut work = Work { + job_id: raw.0, + prev_hash: [0; 32], + coinb1: Vec::new(), + coinb2: Vec::new(), + merkle_branch: Vec::new(), + version: 0, + nbits: 0, + ntime: 0, + clean_jobs: raw.8, + }; + for i in 0..8 { + hex_decode( + &raw.1.as_bytes()[8 * i..8 * (i + 1)], + &mut work.prev_hash[32 - 4 * (i + 1)..32 - 4 * i], + )?; + } + work.coinb1 + .resize(raw.2.len() / 2, 0) + .map_err(|_| Error::FixedSizeTooSmall { + fixed: 70, + needed: raw.2.len() / 2, + })?; + hex_decode(raw.2.as_bytes(), &mut work.coinb1)?; + work.coinb2 + .resize(raw.3.len() / 2, 0) + .map_err(|_| Error::FixedSizeTooSmall { + fixed: 88, + needed: raw.3.len() / 2, + })?; + hex_decode(raw.3.as_bytes(), &mut work.coinb2)?; + for (i, b) in raw.4.iter().enumerate() { + let mut buf = [0; 32]; + hex_decode(b.as_bytes(), &mut buf)?; + work.merkle_branch + .push(buf) + .map_err(|_| Error::FixedSizeTooSmall { + fixed: 16, + needed: i, + })?; + } + let mut v = [0; 4]; + hex_decode(raw.5.as_bytes(), &mut v)?; + work.version = i32::from_be_bytes(v); + hex_decode(raw.6.as_bytes(), &mut v)?; + work.nbits = u32::from_be_bytes(v); + hex_decode(raw.7.as_bytes(), &mut v)?; + work.ntime = u32::from_be_bytes(v); + Ok(work) + } + } + + serde_json_core::from_slice::>(resp)? + .0 + .params + .ok_or(Error::RpcBadRequest)? + .try_into() +} + +pub(crate) fn parse_set_difficulty(resp: &[u8]) -> Result { + serde_json_core::from_slice::>>(resp)? + .0 + .params + .ok_or(Error::RpcBadRequest)? + .pop() + .ok_or(Error::VecEmpty) +} + +#[cfg(test)] +mod tests { + use core::str::FromStr; + use heapless::Vec; + + use super::*; + + #[test] + fn test_parse_set_version_mask() { + let resp = br#"{"params":["1fffe000"], "id":null, "method": "mining.set_version_mask"}"#; + assert_eq!(parse_set_version_mask(resp), Ok(0x1fff_e000)); + + let resp = br#"{"params":["1fffe00z"], "id":null, "method": "mining.set_version_mask"}"#; + assert_eq!( + parse_set_version_mask(resp), + Err(Error::HexError(faster_hex::Error::InvalidChar)) + ); + + let resp = br#"{"params":["1fffe0000"], "id":null, "method": "mining.set_version_mask"}"#; + assert_eq!( + parse_set_version_mask(resp), + Err(Error::JsonError( + serde_json_core::de::Error::CustomErrorWithMessage(hstring!( + 64, + "invalid length 9, expected a string no more than 8 bytes long" + )) + )) + ); + + let resp = br#"{"params":["1fffe00"], "id":null, "method": "mining.set_version_mask"}"#; + assert_eq!( + parse_set_version_mask(resp), + Err(Error::HexError(faster_hex::Error::InvalidLength(8))) + ); + } + + #[test] + fn test_notify() { + // example from https://bitcointalk.org/index.php?topic=557866.5 + assert_eq!( + parse_notify(br#"{"params": ["bf", "4d16b6f85af6e2198f44ae2a6de67f78487ae5611b77c6c0440b921e00000000","01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff20020862062f503253482f04b8864e5008","072f736c7573682f000000000100f2052a010000001976a914d23fcdf86f7e756a64a7a9688ef9903327048ed988ac00000000", [],"00000002", "1c2ac4af", "504e86b9", false], "id": null, "method": "mining.notify"}"#), + Ok(Work { + job_id: "bf".try_into().unwrap(), + prev_hash: [ + 0x00, 0x00, 0x00, 0x00, 0x44, 0x0b, 0x92, 0x1e, 0x1b, 0x77, 0xc6, 0xc0, 0x48, + 0x7a, 0xe5, 0x61, 0x6d, 0xe6, 0x7f, 0x78, 0x8f, 0x44, 0xae, 0x2a, 0x5a, 0xf6, + 0xe2, 0x19, 0x4d, 0x16, 0xb6, 0xf8, + ], + coinb1: hvec!( + u8, + 128, + &[ + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0xff, 0xff, 0xff, 0x20, 0x02, 0x08, 0x62, 0x06, 0x2f, 0x50, + 0x32, 0x53, 0x48, 0x2f, 0x04, 0xb8, 0x86, 0x4e, 0x50, 0x08, + ] + ), + coinb2: hvec!( + u8, + 130, + &[ + 0x07, 0x2f, 0x73, 0x6c, 0x75, 0x73, 0x68, 0x2f, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0xf2, 0x05, 0x2a, 0x01, 0x00, 0x00, 0x00, 0x19, 0x76, 0xa9, + 0x14, 0xd2, 0x3f, 0xcd, 0xf8, 0x6f, 0x7e, 0x75, 0x6a, 0x64, 0xa7, 0xa9, + 0x68, 0x8e, 0xf9, 0x90, 0x33, 0x27, 0x04, 0x8e, 0xd9, 0x88, 0xac, 0x00, + 0x00, 0x00, 0x00, + ] + ), + merkle_branch: Vec::new(), + version: 0x0000_00002, + nbits: 0x1c2a_c4af, + ntime: 0x504e_86b9, + clean_jobs: false + }) + ); + + // example from actual mining job + assert_eq!( + parse_notify(br#"{"params": ["278", "9c16805af67958e9c183d0fa47e4b8245fea76e26cfe874b0000000e00000000","02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff3b03219200005374726174756d207632204e6562756c6120506f6f6c","ffffffff0200f2052a01000000160014d4989f3137807deab9a76e549eef5c5a03448ca40000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900000000",[],"20000000","19609307","66ab8012",true], "id": null, "method": "mining.notify"}"#), + Ok(Work { + job_id: "278".try_into().unwrap(), + prev_hash: [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x6c, 0xfe, 0x87, 0x4b, 0x5f, + 0xea, 0x76, 0xe2, 0x47, 0xe4, 0xb8, 0x24, 0xc1, 0x83, 0xd0, 0xfa, 0xf6, 0x79, + 0x58, 0xe9, 0x9c, 0x16, 0x80, 0x5a, + ], + coinb1: hvec!( + u8, + 128, + &[ + 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0xff, 0xff, 0xff, 0x3b, 0x03, 0x21, 0x92, 0x00, 0x00, 0x53, + 0x74, 0x72, 0x61, 0x74, 0x75, 0x6d, 0x20, 0x76, 0x32, 0x20, 0x4e, 0x65, + 0x62, 0x75, 0x6c, 0x61, 0x20, 0x50, 0x6f, 0x6f, 0x6c, + ] + ), + coinb2: hvec!( + u8, + 130, + &[ + 0xff, 0xff, 0xff, 0xff, 0x02, 0x00, 0xf2, 0x05, 0x2a, 0x01, 0x00, 0x00, + 0x00, 0x16, 0x00, 0x14, 0xd4, 0x98, 0x9f, 0x31, 0x37, 0x80, 0x7d, 0xea, + 0xb9, 0xa7, 0x6e, 0x54, 0x9e, 0xef, 0x5c, 0x5a, 0x03, 0x44, 0x8c, 0xa4, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x26, 0x6a, 0x24, 0xaa, + 0x21, 0xa9, 0xed, 0xe2, 0xf6, 0x1c, 0x3f, 0x71, 0xd1, 0xde, 0xfd, 0x3f, + 0xa9, 0x99, 0xdf, 0xa3, 0x69, 0x53, 0x75, 0x5c, 0x69, 0x06, 0x89, 0x79, + 0x99, 0x62, 0xb4, 0x8b, 0xeb, 0xd8, 0x36, 0x97, 0x4e, 0x8c, 0xf9, 0x00, + 0x00, 0x00, 0x00, + ] + ), + merkle_branch: Vec::new(), + version: 0x2000_0000, + nbits: 0x1960_9307, + ntime: 0x66ab_8012, + clean_jobs: true + }) + ); + + // example from actual mining job + assert_eq!( + parse_notify( + br#"{"id":null,"method":"mining.notify","params":["662ede","a80f3e7fb2fae8236812baa766c2c6141b9118530001c1ce0000000000000000","02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff17035a0b0d5075626c69632d506f6f6c","ffffffff0294b1f512000000001976a91495e381440a0faf41a7206b86b0d770bcabfef2cf88ac0000000000000000266a24aa21a9edee8a2981199af032120e8bc567edb3d3f3335278b508ced184bcbff13137364d00000000",["48133ccb9355395e02960124db8bf4f98f3cb05908f5c27abc77f020fe13feee","bb4120b2bd74d2204588eed34911b311f1e9ec1561c0fe7730b795a3b5d28fa6","53e7786db850a2bb49a88c003f278aa48866ccdadc5bc7b27e77af1bf3ca2669","ce8944028b7360405f2929922e00e08276af6cab0f119aa8ca3bceb29e2306d2","b2debecba153be2fe9da0f193e33a9d1fe888779dfd855770f0f7020c4047f26","548b2b9331cc40b9d68fd142b1ae2322e33ddb1b65cdf94cfd089ec967362118","8af1d2d31f86c0a240a11f55320335560642f3633ba481929fc431a9996fc874","62e671ae9d5d20aa7fc7f45856367f3dedb96b1736ec559292c8b9e0134672b1","10d017262b9152c47aa4e261c84fb6e8fb07f0b760a4da172b09f548f42d1db4","f1b01197d16a068329d39cdcbe0040029b51095635f332ca91e426e5da8bbe7d","ef342a7b654060a5048aaf1d6b0ac18f51a0cee6d58856f21a9cbec799ee6e9e","f6bb407984e000a2a619e26a46e91e113001aebd563407ea6202d8b042cf9294","5d60ed1a9ee3bbfac2b0f24abf0c72fe7c39c4b51b046903a4dfe21ca5312c25"],"20000000","17031abe","66aad286",false]}"# + ), + Ok(Work { + job_id: hstring!(32, "662ede"), + prev_hash: [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc1, 0xce, 0x1b, + 0x91, 0x18, 0x53, 0x66, 0xc2, 0xc6, 0x14, 0x68, 0x12, 0xba, 0xa7, 0xb2, 0xfa, + 0xe8, 0x23, 0xa8, 0x0f, 0x3e, 0x7f, + ], + coinb1: hvec!( + u8, + 128, + &[ + 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0xff, 0xff, 0xff, 0x17, 0x03, 0x5a, 0x0b, 0x0d, 0x50, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x2d, 0x50, 0x6f, 0x6f, 0x6c, + ] + ), + coinb2: hvec!( + u8, + 130, + &[ + 0xff, 0xff, 0xff, 0xff, 0x02, 0x94, 0xb1, 0xf5, 0x12, 0x00, 0x00, 0x00, + 0x00, 0x19, 0x76, 0xa9, 0x14, 0x95, 0xe3, 0x81, 0x44, 0x0a, 0x0f, 0xaf, + 0x41, 0xa7, 0x20, 0x6b, 0x86, 0xb0, 0xd7, 0x70, 0xbc, 0xab, 0xfe, 0xf2, + 0xcf, 0x88, 0xac, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x26, + 0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed, 0xee, 0x8a, 0x29, 0x81, 0x19, 0x9a, + 0xf0, 0x32, 0x12, 0x0e, 0x8b, 0xc5, 0x67, 0xed, 0xb3, 0xd3, 0xf3, 0x33, + 0x52, 0x78, 0xb5, 0x08, 0xce, 0xd1, 0x84, 0xbc, 0xbf, 0xf1, 0x31, 0x37, + 0x36, 0x4d, 0x00, 0x00, 0x00, 0x00, + ] + ), + merkle_branch: hveca!(u8, 32, 16, &[ + [ + 0x48, 0x13, 0x3c, 0xcb, 0x93, 0x55, 0x39, 0x5e, 0x02, 0x96, 0x01, 0x24, 0xdb, 0x8b, + 0xf4, 0xf9, 0x8f, 0x3c, 0xb0, 0x59, 0x08, 0xf5, 0xc2, 0x7a, 0xbc, 0x77, 0xf0, 0x20, + 0xfe, 0x13, 0xfe, 0xee, + ], + [ + 0xbb, 0x41, 0x20, 0xb2, 0xbd, 0x74, 0xd2, 0x20, 0x45, 0x88, 0xee, 0xd3, 0x49, 0x11, + 0xb3, 0x11, 0xf1, 0xe9, 0xec, 0x15, 0x61, 0xc0, 0xfe, 0x77, 0x30, 0xb7, 0x95, 0xa3, + 0xb5, 0xd2, 0x8f, 0xa6, + ], + [ + 0x53, 0xe7, 0x78, 0x6d, 0xb8, 0x50, 0xa2, 0xbb, 0x49, 0xa8, 0x8c, 0x00, 0x3f, 0x27, + 0x8a, 0xa4, 0x88, 0x66, 0xcc, 0xda, 0xdc, 0x5b, 0xc7, 0xb2, 0x7e, 0x77, 0xaf, 0x1b, + 0xf3, 0xca, 0x26, 0x69, + ], + [ + 0xce, 0x89, 0x44, 0x02, 0x8b, 0x73, 0x60, 0x40, 0x5f, 0x29, 0x29, 0x92, 0x2e, 0x00, + 0xe0, 0x82, 0x76, 0xaf, 0x6c, 0xab, 0x0f, 0x11, 0x9a, 0xa8, 0xca, 0x3b, 0xce, 0xb2, + 0x9e, 0x23, 0x06, 0xd2, + ], + [ + 0xb2, 0xde, 0xbe, 0xcb, 0xa1, 0x53, 0xbe, 0x2f, 0xe9, 0xda, 0x0f, 0x19, 0x3e, 0x33, + 0xa9, 0xd1, 0xfe, 0x88, 0x87, 0x79, 0xdf, 0xd8, 0x55, 0x77, 0x0f, 0x0f, 0x70, 0x20, + 0xc4, 0x04, 0x7f, 0x26, + ], + [ + 0x54, 0x8b, 0x2b, 0x93, 0x31, 0xcc, 0x40, 0xb9, 0xd6, 0x8f, 0xd1, 0x42, 0xb1, 0xae, + 0x23, 0x22, 0xe3, 0x3d, 0xdb, 0x1b, 0x65, 0xcd, 0xf9, 0x4c, 0xfd, 0x08, 0x9e, 0xc9, + 0x67, 0x36, 0x21, 0x18, + ], + [ + 0x8a, 0xf1, 0xd2, 0xd3, 0x1f, 0x86, 0xc0, 0xa2, 0x40, 0xa1, 0x1f, 0x55, 0x32, 0x03, + 0x35, 0x56, 0x06, 0x42, 0xf3, 0x63, 0x3b, 0xa4, 0x81, 0x92, 0x9f, 0xc4, 0x31, 0xa9, + 0x99, 0x6f, 0xc8, 0x74, + ], + [ + 0x62, 0xe6, 0x71, 0xae, 0x9d, 0x5d, 0x20, 0xaa, 0x7f, 0xc7, 0xf4, 0x58, 0x56, 0x36, + 0x7f, 0x3d, 0xed, 0xb9, 0x6b, 0x17, 0x36, 0xec, 0x55, 0x92, 0x92, 0xc8, 0xb9, 0xe0, + 0x13, 0x46, 0x72, 0xb1, + ], + [ + 0x10, 0xd0, 0x17, 0x26, 0x2b, 0x91, 0x52, 0xc4, 0x7a, 0xa4, 0xe2, 0x61, 0xc8, 0x4f, + 0xb6, 0xe8, 0xfb, 0x07, 0xf0, 0xb7, 0x60, 0xa4, 0xda, 0x17, 0x2b, 0x09, 0xf5, 0x48, + 0xf4, 0x2d, 0x1d, 0xb4, + ], + [ + 0xf1, 0xb0, 0x11, 0x97, 0xd1, 0x6a, 0x06, 0x83, 0x29, 0xd3, 0x9c, 0xdc, 0xbe, 0x00, + 0x40, 0x02, 0x9b, 0x51, 0x09, 0x56, 0x35, 0xf3, 0x32, 0xca, 0x91, 0xe4, 0x26, 0xe5, + 0xda, 0x8b, 0xbe, 0x7d, + ], + [ + 0xef, 0x34, 0x2a, 0x7b, 0x65, 0x40, 0x60, 0xa5, 0x04, 0x8a, 0xaf, 0x1d, 0x6b, 0x0a, + 0xc1, 0x8f, 0x51, 0xa0, 0xce, 0xe6, 0xd5, 0x88, 0x56, 0xf2, 0x1a, 0x9c, 0xbe, 0xc7, + 0x99, 0xee, 0x6e, 0x9e, + ], + [ + 0xf6, 0xbb, 0x40, 0x79, 0x84, 0xe0, 0x00, 0xa2, 0xa6, 0x19, 0xe2, 0x6a, 0x46, 0xe9, + 0x1e, 0x11, 0x30, 0x01, 0xae, 0xbd, 0x56, 0x34, 0x07, 0xea, 0x62, 0x02, 0xd8, 0xb0, + 0x42, 0xcf, 0x92, 0x94, + ], + [ + 0x5d, 0x60, 0xed, 0x1a, 0x9e, 0xe3, 0xbb, 0xfa, 0xc2, 0xb0, 0xf2, 0x4a, 0xbf, 0x0c, + 0x72, 0xfe, 0x7c, 0x39, 0xc4, 0xb5, 0x1b, 0x04, 0x69, 0x03, 0xa4, 0xdf, 0xe2, 0x1c, + 0xa5, 0x31, 0x2c, 0x25, + ] + ]), + version: 0x2000_0000, + nbits: 0x1703_1abe, + ntime: 0x66aa_d286, + clean_jobs: false + }) + ); + } + + #[test] + fn test_parse_set_difficulty() { + assert_eq!( + parse_set_difficulty( + br#"{"params": [2.5], "id": null, "method": "mining.set_difficulty"}"# + ), + Ok(2.5) + ); + + assert_eq!( + parse_set_difficulty( + br#"{"params": [2.5a], "id": null, "method": "mining.set_difficulty"}"# + ), + Err(Error::JsonError( + serde_json_core::de::Error::ExpectedListCommaOrEnd + )) + ); + } + + #[test] + fn test_parse_method() { + assert_eq!( + parse_method( + br#"{"params":["1fffe000"], "id":null, "method": "mining.set_version_mask"}"# + ), + Ok(Notification::SetVersionMask) + ); + + assert_eq!( + parse_method( + br#"{"params": ["bf", "4d16b6f85af6e2198f44ae2a6de67f78487ae5611b77c6c0440b921e00000000","01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff20020862062f503253482f04b8864e5008","072f736c7573682f000000000100f2052a010000001976a914d23fcdf86f7e756a64a7a9688ef9903327048ed988ac00000000", [],"00000002", "1c2ac4af", "504e86b9", false], "id": null, "method": "mining.notify"}"# + ), + Ok(Notification::Notify) + ); + + assert_eq!( + parse_method(br#"{"params": [2.5], "id": null, "method": "mining.set_difficulty"}"#), + Ok(Notification::SetDifficulty) + ); + + assert_eq!( + parse_method(br#"{"params": [], "id": null, "method": "mining.broken"}"#), + Err(Error::UnknownNotification) + ); + } +} diff --git a/stratum-v1/src/client/request.rs b/stratum-v1/src/client/request.rs new file mode 100644 index 0000000..b2e027a --- /dev/null +++ b/stratum-v1/src/client/request.rs @@ -0,0 +1,369 @@ +// SPDX-FileCopyrightText: © 2024 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later + +use crate::{Error, Result}; +use faster_hex::hex_string; +use heapless::{String, Vec}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub(crate) enum ReqKind { + Configure, + Connect, + Authorize, + Submit, +} + +///Request representation. +/// +///Note that omitting `id` means that request is notification, rather than call, which expects +///response. +///This can be used to indicate lack of interest in response. +/// +///Type parameters: +/// +///- `P` - to specify type of `params` field, which is optional. Normally it should be collection of values or object. But choice is yours. +///- `T` - specifies textual type. By default it uses static buffer of 32 bytes, which is more than enough in normal cases. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub struct Request

{ + #[serde(skip_serializing_if = "Option::is_none")] + ///An identifier established by the Client. + /// + ///If not present, request is notification to which + ///there should be no response. + pub id: Option, + ///A String containing the name of the method to be invoked + /// + ///By default is static buffer of 32 bytes. + pub method: String<32>, + #[serde(skip_serializing_if = "Option::is_none")] + ///A Structured value that holds the parameter values to be used during the invocation of the method + pub params: Option

, +} + +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub struct VersionRolling { + /// Bits set to 1 can be changed by the miner. + /// If a miner changes bits with mask value 0, the server will reject the submit. + pub mask: Option, + /// Minimum number of bits that it needs for efficient version rolling in hardware. + pub min_bit_count: Option, +} + +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub struct Info { + /// Exact URL used by the mining software to connect to the stratum server. + pub connection_url: Option>, + /// Manufacturer specific hardware revision string. + pub hw_version: Option>, + /// Manufacturer specific software version. + pub sw_version: Option>, + /// Unique identifier of the mining device. + pub hw_id: Option>, +} + +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub struct Extensions { + /// This extension allows the miner to change the value of some bits in the version field + /// in the block header. Currently there are no standard bits used for version rolling + /// so they need to be negotiated between a miner and a server. + pub version_rolling: Option, + /// This extension allows miner to request a minimum difficulty for the connected machine. + /// It solves a problem in the original stratum protocol where there is no way how to + /// communicate hard limit of the connected device. + pub minimum_difficulty: Option, + /// Miner advertises its capability of receiving message "mining.set_extranonce" message + /// (useful for hash rate routing scenarios). + pub subscribe_extranonce: Option<()>, + /// Miner provides additional text-based information. + pub info: Option, +} + +pub(crate) fn configure(id: u64, exts: Extensions, buf: &mut [u8]) -> Result { + let method = "mining.configure".try_into().unwrap(); + + type ExtList = Vec, 4>; + + #[derive(Debug, Serialize)] + #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] + struct ExtParams { + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "version-rolling.mask")] + version_rolling_mask: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "version-rolling.min-bit-count")] + version_rolling_min_bit_count: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "minimum-difficulty.value")] + minimum_difficulty_value: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "info.connection-url")] + info_connection_url: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "info.hw-version")] + info_hw_version: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "info.sw-version")] + info_sw_version: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "info.hw-id")] + info_hw_id: Option>, + } + + #[derive(Debug, Serialize)] + #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] + struct ConfigureParams(ExtList, ExtParams); + + let mut ext_list = Vec::new(); + let mut ext_params = ExtParams { + version_rolling_mask: None, + version_rolling_min_bit_count: None, + minimum_difficulty_value: None, + info_connection_url: None, + info_hw_version: None, + info_sw_version: None, + info_hw_id: None, + }; + if let Some(version_rolling) = &exts.version_rolling { + ext_list + .push("version-rolling".try_into().unwrap()) + .unwrap(); + if let Some(mask) = version_rolling.mask { + ext_params.version_rolling_mask = Some(hex_string::<8>(&mask.to_be_bytes())); + } + if let Some(min_bit_count) = version_rolling.min_bit_count { + let min_bit_count = min_bit_count as u32; + ext_params.version_rolling_min_bit_count = + Some(hex_string::<8>(&min_bit_count.to_be_bytes())); + } + } + if let Some(minimum_difficulty) = &exts.minimum_difficulty { + ext_list + .push("minimum-difficulty".try_into().unwrap()) + .unwrap(); + ext_params.minimum_difficulty_value = Some(*minimum_difficulty); + } + if let Some(()) = &exts.subscribe_extranonce { + ext_list + .push("subscribe-extranonce".try_into().unwrap()) + .unwrap(); + } + if let Some(info) = &exts.info { + ext_list.push("info".try_into().unwrap()).unwrap(); + if let Some(connection_url) = &info.connection_url { + ext_params.info_connection_url = Some(connection_url.clone()); + } + if let Some(hw_version) = &info.hw_version { + ext_params.info_hw_version = Some(hw_version.clone()); + } + if let Some(sw_version) = &info.sw_version { + ext_params.info_sw_version = Some(sw_version.clone()); + } + if let Some(hw_id) = &info.hw_id { + ext_params.info_hw_id = Some(hw_id.clone()); + } + } + let params = Some(ConfigureParams(ext_list, ext_params)); + let req = Request:: { + method, + params, + id: Some(id), + }; + serde_json_core::to_slice(&req, buf).map_err(|_| Error::JsonBufferFull) +} + +pub(crate) fn connect(id: u64, identifier: Option>, buf: &mut [u8]) -> Result { + let method = "mining.subscribe".try_into().unwrap(); + let mut vec = Vec::, 1>::new(); + if let Some(identifier) = identifier { + vec.push(identifier).map_err(|_| Error::VecFull)?; + } + let params = Some(vec); + let req = Request::, 1>> { + method, + params, + id: Some(id), + }; + serde_json_core::to_slice(&req, buf).map_err(|_| Error::JsonBufferFull) +} + +pub(crate) fn authorize( + id: u64, + user: String<64>, + pass: String<64>, + buf: &mut [u8], +) -> Result { + let method = "mining.authorize".try_into().unwrap(); + let mut vec = Vec::, 2>::new(); + vec.push(user).map_err(|_| Error::VecFull)?; + vec.push(pass).map_err(|_| Error::VecFull)?; + let params = Some(vec); + let req = Request::, 2>> { + method, + params, + id: Some(id), + }; + serde_json_core::to_slice(&req, buf).map_err(|_| Error::JsonBufferFull) +} + +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub struct Share { + pub job_id: String<64>, + pub extranonce2: Vec, + pub ntime: u32, + pub nonce: u32, + pub version_bits: Option, +} + +pub(crate) fn submit(id: u64, user: String<64>, share: Share, buf: &mut [u8]) -> Result { + let method = "mining.submit".try_into().unwrap(); + let mut vec = Vec::, 6>::new(); + vec.push(user).map_err(|_| Error::VecFull)?; + vec.push(share.job_id).map_err(|_| Error::VecFull)?; + vec.push(hex_string::<64>(share.extranonce2.as_slice())) + .map_err(|_| Error::VecFull)?; + vec.push(hex_string::<64>(&share.ntime.to_be_bytes())) + .map_err(|_| Error::VecFull)?; + vec.push(hex_string::<64>(&share.nonce.to_be_bytes())) + .map_err(|_| Error::VecFull)?; + if let Some(v) = share.version_bits { + vec.push(hex_string::<64>(&v.to_be_bytes())) + .map_err(|_| Error::VecFull)?; + } + let params = Some(vec); + let req = Request::, 6>> { + method, + params, + id: Some(id), + }; + serde_json_core::to_slice(&req, buf).map_err(|_| Error::JsonBufferFull) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_configure() { + let mut buf = [0u8; 1024]; + let exts = Extensions { + version_rolling: Some(VersionRolling { + mask: Some(0x1fffe000), + min_bit_count: Some(2), + }), + minimum_difficulty: None, + subscribe_extranonce: None, + info: None, + }; + let len = configure(0, exts, buf.as_mut_slice()); + assert!(len.is_ok()); + assert_eq!(len.unwrap(), 146); + assert_eq!(&buf[..146], br#"{"id":0,"method":"mining.configure","params":[["version-rolling"],{"version-rolling.mask":"1fffe000","version-rolling.min-bit-count":"00000002"}]}"#); + + let exts = Extensions { + version_rolling: Some(VersionRolling { + mask: Some(0x1fffe000), + min_bit_count: Some(2), + }), + minimum_difficulty: Some(2048), + subscribe_extranonce: None, + info: None, + }; + let len = configure(0, exts, buf.as_mut_slice()); + assert!(len.is_ok()); + assert_eq!(len.unwrap(), 199); + assert_eq!(&buf[..199], br#"{"id":0,"method":"mining.configure","params":[["version-rolling","minimum-difficulty"],{"version-rolling.mask":"1fffe000","version-rolling.min-bit-count":"00000002","minimum-difficulty.value":2048}]}"#); + } + + #[test] + fn test_connect() { + let mut buf = [0u8; 1024]; + let len = connect(0, Some("test".try_into().unwrap()), buf.as_mut_slice()); + assert!(len.is_ok()); + assert_eq!(len.unwrap(), 54); + assert_eq!( + &buf[..54], + br#"{"id":0,"method":"mining.subscribe","params":["test"]}"# + ); + + let len = connect(1, Some("".try_into().unwrap()), buf.as_mut_slice()); + assert!(len.is_ok()); + assert_eq!(len.unwrap(), 50); + assert_eq!( + &buf[..50], + br#"{"id":1,"method":"mining.subscribe","params":[""]}"# + ); + + let len = connect(1, None, buf.as_mut_slice()); + assert!(len.is_ok()); + assert_eq!(len.unwrap(), 48); + assert_eq!( + &buf[..48], + br#"{"id":1,"method":"mining.subscribe","params":[]}"# + ); + } + + #[test] + fn test_authorize() { + let mut buf = [0u8; 1024]; + let len = authorize( + 1, + "slush.miner1".try_into().unwrap(), + "password".try_into().unwrap(), + buf.as_mut_slice(), + ); + assert!(len.is_ok()); + assert_eq!(len.unwrap(), 73); + assert_eq!( + &buf[..73], + br#"{"id":1,"method":"mining.authorize","params":["slush.miner1","password"]}"# + ); + + let len = authorize( + 2, + "".try_into().unwrap(), + "".try_into().unwrap(), + buf.as_mut_slice(), + ); + assert!(len.is_ok()); + assert_eq!(len.unwrap(), 53); + assert_eq!( + &buf[..53], + br#"{"id":2,"method":"mining.authorize","params":["",""]}"# + ); + } + + #[test] + fn test_submit() { + let mut buf = [0u8; 1024]; + let share = Share { + job_id: "bf".try_into().unwrap(), + extranonce2: hvec!(u8, 8, &[0, 0, 0, 1]), + ntime: 1347323629, + nonce: 0xb295_7c02, + version_bits: None, + }; + let len = super::submit( + 1, + "slush.miner1".try_into().unwrap(), + share, + buf.as_mut_slice(), + ); + assert!(len.is_ok()); + assert_eq!(len.unwrap(), 97); + assert_eq!(&buf[..97], br#"{"id":1,"method":"mining.submit","params":["slush.miner1","bf","00000001","504e86ed","b2957c02"]}"#); + } +} diff --git a/stratum-v1/src/client/response.rs b/stratum-v1/src/client/response.rs new file mode 100644 index 0000000..c6a105f --- /dev/null +++ b/stratum-v1/src/client/response.rs @@ -0,0 +1,590 @@ +// SPDX-FileCopyrightText: © 2024 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later + +use crate::{Error, Extensions, Info, Result, VersionRolling}; +use faster_hex::hex_decode; +use heapless::{String, Vec}; +use serde::{Deserialize, Deserializer}; + +pub(crate) fn parse_id(resp: &[u8]) -> Result> { + #[derive(Debug, Deserialize)] + #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] + struct IdOnly { + id: Option, + } + let id = serde_json_core::from_slice::(resp)?.0.id; + match id { + None => Ok(None), + Some(id) => Ok(Some(id)), + } +} + +///Response representation. +/// +///When omitting `id`, it shall be serialized as `null` and means you're unable to identify `id` of +///`Request`. +///Note that JSON-RPCv2 specifies that `id` must be always present, therefore you're encouraged to +///treat missing `id` as error, unless response is error itself, in which case it might be +///indication that server treats request as invalid (e.g. unable to parse request's id). +/// +///`jsonrpc` may be omitted during deserialization and defaults to v2. +/// +///Type parameters: +/// +///- `R` - Type of payload for successful response +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub struct Response { + ///An identifier established by the Client. + /// + ///If not present, it is sent in response to invalid request (e.g. unable to recognize id). + /// + ///Must be present always, so `None` is serialized as `null` + pub id: Option, + + ///Content of response, depending on whether it is success or failure. + pub payload: Result, +} + +impl<'de, R: Deserialize<'de>> Deserialize<'de> for Response { + fn deserialize>(der: D) -> core::result::Result { + use core::marker::PhantomData; + use serde::de::{self, Visitor}; + + #[derive(Debug, Deserialize)] + #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] + struct RespErr(isize, String<32>, Option>); + + impl From for Error { + fn from(err: RespErr) -> Self { + Error::Pool { + code: err.0, + message: err.1, + detail: err.2, + } + } + } + + struct MapVisit(PhantomData); + + enum Key { + Result, + Error, + Id, + } + + struct KeyVisitor; + + impl<'a> Visitor<'a> for KeyVisitor { + type Value = Key; + + #[inline] + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + formatter.write_str("Key must be a string and one of the following values: ['result', 'error', 'id']") + } + + #[inline] + fn visit_str( + self, + text: &str, + ) -> core::result::Result { + if text.eq_ignore_ascii_case("result") { + Ok(Key::Result) + } else if text.eq_ignore_ascii_case("error") { + Ok(Key::Error) + } else if text.eq_ignore_ascii_case("id") { + Ok(Key::Id) + } else { + Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Str(text), + &self, + )) + } + } + } + + impl<'a> Deserialize<'a> for Key { + #[inline] + fn deserialize>(des: D) -> core::result::Result { + des.deserialize_str(KeyVisitor) + } + } + + impl<'de, R: Deserialize<'de>> Visitor<'de> for MapVisit { + type Value = Response; + + #[inline] + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + formatter.write_str("Object resembling JSON-RPC response type") + } + + fn visit_map>( + self, + mut map: A, + ) -> core::result::Result { + //Normally you'd use unitialized struct, but it is highly unlikely to guarantee + //safety of field-by-field initialization + let mut result = None; + let mut id = None; + + while let Some(key) = map.next_key::()? { + match key { + //If for some reason user wishes to convey success with NULL, we need to respect that. + //This cannot be the case for error as its format is well defined + //And while spec does say `result` field MUST be object, theoretically NULL should qualify too. + //This is hack because bitch cannot have specialization stabilized forever + Key::Result if core::mem::size_of::() == 0 => { + // Error has priority over Result, if both exist Result is ignored + if result.is_none() { + result = Some(Ok(map.next_value::()?)); + } + } + Key::Result => { + match map.next_value::>()? { + Some(value) => { + // Error has priority over Result, if both exist Result is ignored + if result.is_none() { + result = Some(Ok(value)); + } + } + None => continue, + } + } + Key::Error => match map.next_value::>()? { + Some(error) => { + // Error has priority over Result, if both exist Result is ignored + result = Some(Err(error)); + } + None => continue, + }, + Key::Id => { + id = map.next_value::>()?; + } + } + } + + Ok(Self::Value { + payload: match result { + Some(payload) => payload.map_err(|e| e.into()), + None => { + return Err(serde::de::Error::custom( + "JSON-RPC Response is missing either result or error field.", + )); + } + }, + id, + }) + } + } + + der.deserialize_map(MapVisit(PhantomData)) + } +} + +pub(crate) fn parse_configure(resp: &[u8]) -> Result { + #[derive(Debug, Deserialize)] + #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] + pub struct ConfigureRespRaw { + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "version-rolling")] + pub version_rolling: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "version-rolling.mask")] + pub version_rolling_mask: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "version-rolling.min-bit-count")] + pub version_rolling_min_bit_count: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "minimum-difficulty")] + pub minimum_difficulty: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "minimum-difficulty.value")] + pub minimum_difficulty_value: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "subscribe-extranonce")] + pub subscribe_extranonce: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "info")] + pub info: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "info.connection-url")] + pub info_connection_url: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "info.hw-version")] + pub info_hw_version: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "info.sw-version")] + pub info_sw_version: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "info.hw-id")] + pub info_hw_id: Option>, + } + + impl TryFrom for Extensions { + type Error = Error; + + fn try_from(raw: ConfigureRespRaw) -> Result { + Ok(Extensions { + version_rolling: if raw.version_rolling.is_some_and(|v| v) { + Some(VersionRolling { + mask: if raw.version_rolling_mask.is_some() { + let mut v = [0; 4]; + hex_decode(raw.version_rolling_mask.unwrap().as_bytes(), &mut v)?; + Some(u32::from_be_bytes(v)) + } else { + None + }, + min_bit_count: raw.version_rolling_min_bit_count, + }) + } else { + None + }, + minimum_difficulty: if raw.minimum_difficulty.is_some_and(|v| v) { + raw.minimum_difficulty_value + } else { + None + }, + subscribe_extranonce: if raw.subscribe_extranonce.is_some_and(|v| v) { + Some(()) + } else { + None + }, + info: if raw.info.is_some_and(|v| v) { + Some(Info { + connection_url: raw.info_connection_url, + hw_version: raw.info_hw_version, + sw_version: raw.info_sw_version, + hw_id: raw.info_hw_id, + }) + } else { + None + }, + }) + } + } + + serde_json_core::from_slice::>(resp)? + .0 + .payload + .map_err(|_| Error::RpcOther)? + .try_into() +} + +pub type Subscription = Vec, 2>; + +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub struct ConnectResp { + pub subscriptions: Vec, + pub extranonce1: Vec, + pub extranonce2_size: usize, +} + +pub(crate) fn parse_connect(resp: &[u8]) -> Result { + #[derive(Debug, Deserialize)] + #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] + struct ConnectRespRaw( + // Subscriptions details - 2-tuple with name of subscribed notification and subscription ID. Theoretically it may be used for unsubscribing, but obviously miners won't use it. + Vec, 2>, 2>, + // Extranonce1 - Hex-encoded, per-connection unique string which will be used for coinbase serialization later. Keep it safe! + String<16>, + // Extranonce2_size - Represents expected length of extranonce2 which will be generated by the miner. + usize, + ); + + impl TryFrom for ConnectResp { + type Error = Error; + + fn try_from(raw: ConnectRespRaw) -> Result { + Ok(Self { + subscriptions: raw.0, + extranonce1: { + let mut v = Vec::new(); + v.resize(raw.1.len() / 2, 0).map_err(|_| Error::VecFull)?; + hex_decode(raw.1.as_bytes(), v.as_mut_slice())?; + v + }, + extranonce2_size: raw.2, + }) + } + } + + serde_json_core::from_slice::>(resp)? + .0 + .payload? + .try_into() +} + +pub(crate) fn parse_authorize(resp: &[u8]) -> Result { + serde_json_core::from_slice::>(resp)? + .0 + .payload +} + +pub(crate) fn parse_submit(resp: &[u8]) -> Result { + serde_json_core::from_slice::>(resp)? + .0 + .payload +} + +#[cfg(test)] +mod tests { + use core::str::FromStr; + use heapless::Vec; + + use super::*; + + #[test] + fn test_parse_id() { + let resp = br#"{"id": 1, "result": [ [ ["mining.set_difficulty", "b4b6693b72a50c7116db18d6497cac52"], ["mining.notify", "ae6812eb4cd7735a302a8a9dd95cf71f"]], "08000002", 4], "error": null}"#; + assert_eq!(parse_id(resp), Ok(Some(1))); + + let resp = + br#"{"error":null,"id":2,"result":[[["mining.notify","e26e1928"]],"e26e1928",4]}"#; + assert_eq!(parse_id(resp), Ok(Some(2))); + + let resp = br#"{ "id": null, "method": "mining.set_difficulty", "params": [2]}"#; + assert_eq!(parse_id(resp), Ok(None)); + + let resp = br#"{ "id": "ab", "method": "mining.set_difficulty", "params": [2]}"#; + assert_eq!( + parse_id(resp), + Err(Error::JsonError(serde_json_core::de::Error::InvalidType)) + ); + } + + #[test] + fn test_parse_configure() { + let resp = br#"{"error": null,"id": 1,"result": {"version-rolling": true,"version-rolling.mask": "18000000","minimum-difficulty": true}}"#; + assert_eq!( + parse_configure(resp), + Ok(Extensions { + version_rolling: Some(VersionRolling { + mask: Some(0x1800_0000), + min_bit_count: None + }), + minimum_difficulty: None, + subscribe_extranonce: None, + info: None, + }) + ); + } + + #[test] + fn test_parse_connect() { + let resp = br#"{"id": 1, "result": [ [ ["mining.set_difficulty", "b4b6693b72a50c7116db18d6497cac52"], ["mining.notify", "ae6812eb4cd7735a302a8a9dd95cf71f"]], "08000002", 4], "error": null}"#; + let mut subs = Vec::new(); + let mut sub = Vec::new(); + sub.push(hstring!(32, "mining.set_difficulty")).unwrap(); + sub.push(hstring!(32, "b4b6693b72a50c7116db18d6497cac52")) + .unwrap(); + subs.push(sub).unwrap(); + let mut sub = Vec::new(); + sub.push(hstring!(32, "mining.notify")).unwrap(); + sub.push(hstring!(32, "ae6812eb4cd7735a302a8a9dd95cf71f")) + .unwrap(); + subs.push(sub).unwrap(); + let mut extranonce1 = Vec::new(); + extranonce1 + .extend_from_slice(&[0x08, 0x00, 0x00, 0x02]) + .unwrap(); + assert_eq!( + parse_connect(resp), + Ok(ConnectResp { + subscriptions: subs, + extranonce1, + extranonce2_size: 4, + }) + ); + + let resp = br#"{"id":2,"result":[[["mining.set_difficulty","1"],["mining.notify","1"]],"00",6],"error":null}"#; + let mut subs = Vec::new(); + let mut sub = Vec::new(); + sub.push(hstring!(32, "mining.set_difficulty")).unwrap(); + sub.push(hstring!(32, "1")).unwrap(); + subs.push(sub).unwrap(); + let mut sub = Vec::new(); + sub.push(hstring!(32, "mining.notify")).unwrap(); + sub.push(hstring!(32, "1")).unwrap(); + subs.push(sub).unwrap(); + assert_eq!( + parse_connect(resp), + Ok(ConnectResp { + subscriptions: subs, + extranonce1: hvec!(u8, 8, &[0x00]), + extranonce2_size: 6, + }) + ); + + let resp = + br#"{"id":2,"error":null,"result":[[["mining.notify","e26e1928"]],"e26e1928",4]}"#; + let mut subs = Vec::new(); + let mut sub = Vec::new(); + sub.push(hstring!(32, "mining.notify")).unwrap(); + sub.push(hstring!(32, "e26e1928")).unwrap(); + subs.push(sub).unwrap(); + assert_eq!( + parse_connect(resp), + Ok(ConnectResp { + subscriptions: subs, + extranonce1: hvec!(u8, 8, &[0xe2, 0x6e, 0x19, 0x28]), + extranonce2_size: 4, + }) + ); + + let resp = br#"{"id": 10, "result": null, "error": [20, "Other/Unknown", null]}"#; + assert_eq!( + parse_connect(resp), + Err(Error::Pool { + code: 20, + message: hstring!(32, "Other/Unknown"), + detail: None + }) + ); + } + + #[test] + fn test_parse_authorize() { + let resp = br#"{"id": 2, "result": true, "error": null}"#; + assert_eq!(parse_authorize(resp), Ok(true)); + + let resp = br#"{"id":3,"result":true,"error":null}"#; + assert_eq!(parse_authorize(resp), Ok(true)); + + let resp = br#"{"id": 10, "result": null, "error": [25, "Not subscribed", null]}"#; + assert_eq!( + parse_authorize(resp), + Err(Error::Pool { + code: 25, + message: hstring!(32, "Not subscribed"), + detail: None + }) + ); + + // Public-Pool + let resp = + br#"{"id":3,"result":null,"error":[20,"Authorization validation error",", slush"]}"#; + assert_eq!( + parse_authorize(resp), + Err(Error::Pool { + code: 20, + message: hstring!(32, "Authorization validation error"), + detail: Some(hstring!(32, ", slush")), + }) + ); + + // Braiins Pool + let resp = br#"{"id":3,"result":false,"error":null}"#; + assert_eq!(parse_authorize(resp), Ok(false)); + } + + #[test] + fn test_parse_submit() { + let resp = br#"{"id": 2, "result": true, "error": null}"#; + assert_eq!(parse_submit(resp), Ok(true)); + + // Public-Pool + let resp = br#"{"id":5,"result":null,"error":[23,"Difficulty too low",""]}"#; + assert_eq!( + parse_submit(resp), + Err(Error::Pool { + code: 23, + message: hstring!(32, "Difficulty too low"), + detail: Some(hstring!(32, "")) + }) + ); + let resp = br#"{"id":84,"result":null,"error":[21,"Job not found",""]}"#; + assert_eq!( + parse_submit(resp), + Err(Error::Pool { + code: 21, + message: hstring!(32, "Job not found"), + detail: Some(hstring!(32, "")) + }) + ); + // Philon Proxy + let resp = br#"{"error":[23,"Low difficulty share",null],"id":26,"result":false}"#; + assert_eq!( + parse_submit(resp), + Err(Error::Pool { + code: 23, + message: hstring!(32, "Low difficulty share"), + detail: None + }) + ); + let resp = br#"{"error":[-32601,"Method not found",null],"id":1708966505,"result":false}"#; + assert_eq!( + parse_submit(resp), + Err(Error::Pool { + code: -32601, + message: hstring!(32, "Method not found"), + detail: None + }) + ); + // Braiins Pool + let resp = br#"{"id":87,"result":null,"error":[30,"SInvalidJobId",null]}"#; + assert_eq!( + parse_submit(resp), + Err(Error::Pool { + code: 30, + message: hstring!(32, "SInvalidJobId"), + detail: None + }) + ); + let resp = br#"{"id":87,"result":null,"error":[33,"SInvalidVersion",null]}"#; + assert_eq!( + parse_submit(resp), + Err(Error::Pool { + code: 33, + message: hstring!(32, "SInvalidVersion"), + detail: None + }) + ); + let resp = br#"{"id":5,"result":null,"error":[34,"SInvalidTime",null]}"#; + assert_eq!( + parse_submit(resp), + Err(Error::Pool { + code: 34, + message: hstring!(32, "SInvalidTime"), + detail: None + }) + ); + let resp = br#"{"id":5,"result":null,"error":[35,"SInvalidExnSize",null]}"#; + assert_eq!( + parse_submit(resp), + Err(Error::Pool { + code: 35, + message: hstring!(32, "SInvalidExnSize"), + detail: None + }) + ); + let resp = br#"{"id":5,"result":null,"error":[38,"STooLowDiff",null]}"#; + assert_eq!( + parse_submit(resp), + Err(Error::Pool { + code: 38, + message: hstring!(32, "STooLowDiff"), + detail: None + }) + ); + let resp = br#"{"id":5,"result":null,"error":[39,"SStaleJobNoSub",null]}"#; + assert_eq!( + parse_submit(resp), + Err(Error::Pool { + code: 39, + message: hstring!(32, "SStaleJobNoSub"), + detail: None + }) + ); + } +} diff --git a/stratum-v1/src/error.rs b/stratum-v1/src/error.rs new file mode 100644 index 0000000..66b4fc3 --- /dev/null +++ b/stratum-v1/src/error.rs @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: © 2024 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later + +use derive_more::From; +use heapless::String; + +pub type Result = core::result::Result; + +#[derive(Debug, Clone, From, PartialEq)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub enum Error { + /// Client is already configured against the Pool + AlreadyConfigured, + /// Client is not configured against the Pool + NotConfigured, + /// Client is already connected to Pool + AlreadyConnected, + /// Client is not connected to Pool + NotConnected, + /// Client is already authorised by Pool + AlreadyAuthorized, + /// Client has not yet being authorized to submit a share + Unauthorized, + /// Client has received an unknown Notficiation from Pool + UnknownNotification, + + /// One of the fixed size Vec or String si to small to contain the data + FixedSizeTooSmall { + fixed: usize, + needed: usize, + }, + + /// The RPC Request has a bad format + RpcBadRequest, + /// The RPC Response is incoherent + RpcResponseIncoherent, + /// The Vec poped is empty + VecEmpty, + + /// Queue is full + QueueFull, + + /// Map is full + MapFull, + + NoWork, + + /// Pool reported an error + Pool { + code: isize, + message: String<32>, + detail: Option>, + }, + + /// Network error + // #[from] + // NetworkError(embedded_io::ErrorKind), + NetworkError, + + IdNotFound(u64), + + /// correspond to serde_json_core::ser:Error::BufferFull + JsonBufferFull, + /// correspond to all serde_json_core::de:Error + #[from] + JsonError(serde_json_core::de::Error), + /// correspond to all json_rpc_types::Error + RpcOther, + /// correspond to heapless::Vec::push() + VecFull, + /// correspond to all faster_hex::Error + #[from] + HexError(faster_hex::Error), +} + +// impl core::error::Error for Error {} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{self:?}") + } +} diff --git a/stratum-v1/src/fmt.rs b/stratum-v1/src/fmt.rs new file mode 100644 index 0000000..35ade47 --- /dev/null +++ b/stratum-v1/src/fmt.rs @@ -0,0 +1,247 @@ +// SPDX-FileCopyrightText: © 2024 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later + +#![macro_use] +#![allow(unused_macros)] + +#[cfg(all(feature = "defmt-03", feature = "log"))] +compile_error!("You may not enable both `defmt` and `log` features."); + +macro_rules! assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::assert!($($x)*); + } + }; +} + +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::debug_assert!($($x)*); + } + }; +} + +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::debug_assert_eq!($($x)*); + } + }; +} + +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::debug_assert_ne!($($x)*); + } + }; +} + +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::todo!($($x)*); + } + }; +} + +macro_rules! unreachable { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::unreachable!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::unreachable!($($x)*); + } + }; +} + +macro_rules! panic { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::panic!($($x)*); + } + }; +} + +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt-03")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt-03")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt-03")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt-03")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt-03")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt-03")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt-03")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt-03")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt-03")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt-03")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt-03")] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt-03"))] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +#[allow(dead_code)] +pub trait Try { + type Ok; + type Error; + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} + +macro_rules! hstring { + ($l:expr, $s:expr) => { + heapless::String::<$l>::from_str($s).unwrap() + }; +} + +macro_rules! hvec { + ($t:ident, $l:expr, $s:expr) => { + heapless::Vec::<$t, $l>::from_slice($s).unwrap() + }; +} + +macro_rules! hveca { + ($t:ident, $la:expr, $l:expr, $s:expr) => { + heapless::Vec::<[$t; $la], $l>::from_slice($s).unwrap() + }; +} diff --git a/stratum-v1/src/lib.rs b/stratum-v1/src/lib.rs new file mode 100644 index 0000000..3a2c79e --- /dev/null +++ b/stratum-v1/src/lib.rs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: © 2024 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Stratum v1 client. +//! +//! This library provides client side functions to create requests and parse responses for Stratum v1 protocol. + +#![no_std] +// #![allow(static_mut_refs)] +// #![allow(stable_features)] // remove this once rust 1.81 is stable +// #![feature(error_in_core)] +#![macro_use] +pub(crate) mod fmt; + +mod client; +mod error; + +pub use client::{Client, Extensions, Info, Job, Message, Share, VersionRolling}; +pub use error::{Error, Result};