diff --git a/stratum-core/Cargo.toml b/stratum-core/Cargo.toml index 73312d6807..ff770cdf3b 100644 --- a/stratum-core/Cargo.toml +++ b/stratum-core/Cargo.toml @@ -22,7 +22,7 @@ parsers_sv2 = { path = "../sv2/parsers-sv2", version = "^0.2.0" } handlers_sv2 = { path = "../sv2/handlers-sv2", version = "^0.2.0" } channels_sv2 = { path = "../sv2/channels-sv2", version = "^4.0.0" } common_messages_sv2 = { path = "../sv2/subprotocols/common-messages", version = "^7.0.0" } -mining_sv2 = { path = "../sv2/subprotocols/mining", version = "^8.0.0" } +mining_sv2 = { path = "../sv2/subprotocols/mining", version = "^9.0.0" } template_distribution_sv2 = { path = "../sv2/subprotocols/template-distribution", version = "^5.0.0" } job_declaration_sv2 = { path = "../sv2/subprotocols/job-declaration", version = "^7.0.0" } sv1_api = { path = "../sv1", version = "^3.0.0", optional = true } diff --git a/stratum-core/stratum-translation/Cargo.toml b/stratum-core/stratum-translation/Cargo.toml index 80cc421bcc..0fa620be75 100644 --- a/stratum-core/stratum-translation/Cargo.toml +++ b/stratum-core/stratum-translation/Cargo.toml @@ -11,7 +11,7 @@ path = "src/lib.rs" [dependencies] binary_sv2 = { path = "../../sv2/binary-sv2", version = "^5.0.0" } -mining_sv2 = { path = "../../sv2/subprotocols/mining", version = "^8.0.0" } +mining_sv2 = { path = "../../sv2/subprotocols/mining", version = "^9.0.0" } channels_sv2 = { path = "../../sv2/channels-sv2", version = "^4.0.0" } v1 = { path = "../../sv1", package = "sv1_api", version = "^3.0.0" } tracing = { workspace = true } diff --git a/sv2/channels-sv2/Cargo.toml b/sv2/channels-sv2/Cargo.toml index 0b8c768af9..02a51b6fb9 100644 --- a/sv2/channels-sv2/Cargo.toml +++ b/sv2/channels-sv2/Cargo.toml @@ -15,7 +15,7 @@ keywords = ["stratum", "mining", "bitcoin", "protocol"] [dependencies] binary_sv2 = { path = "../binary-sv2", version = "^5.0.0" } -mining_sv2 = { path = "../subprotocols/mining", version = "^8.0.0" } +mining_sv2 = { path = "../subprotocols/mining", version = "^9.0.0" } template_distribution_sv2 = { path = "../subprotocols/template-distribution", version = "^5.0.0" } tracing = { workspace = true } bitcoin = { workspace = true } diff --git a/sv2/channels-sv2/src/client/extended.rs b/sv2/channels-sv2/src/client/extended.rs index cd3c62c2a5..e1cd28c086 100644 --- a/sv2/channels-sv2/src/client/extended.rs +++ b/sv2/channels-sv2/src/client/extended.rs @@ -14,7 +14,7 @@ use crate::{ }, merkle_root::merkle_root_from_path, target::{bytes_to_hex, u256_to_block_hash}, - MAX_EXTRANONCE_PREFIX_LEN, + MAX_EXTRANONCE_LEN, }; use alloc::{format, string::String, vec, vec::Vec}; use binary_sv2::{self, Sv2Option}; @@ -148,7 +148,7 @@ impl<'a> ExtendedChannel<'a> { &mut self, new_extranonce_prefix: Vec, ) -> Result<(), ExtendedChannelError> { - if new_extranonce_prefix.len() > MAX_EXTRANONCE_PREFIX_LEN { + if new_extranonce_prefix.len() > MAX_EXTRANONCE_LEN { return Err(ExtendedChannelError::NewExtranoncePrefixTooLarge); } diff --git a/sv2/channels-sv2/src/client/standard.rs b/sv2/channels-sv2/src/client/standard.rs index 7e403ec746..b0d926f447 100644 --- a/sv2/channels-sv2/src/client/standard.rs +++ b/sv2/channels-sv2/src/client/standard.rs @@ -14,7 +14,7 @@ use crate::{ }, merkle_root::merkle_root_from_path, target::{bytes_to_hex, u256_to_block_hash}, - MAX_EXTRANONCE_PREFIX_LEN, + MAX_EXTRANONCE_LEN, }; use alloc::{format, string::String, vec::Vec}; use binary_sv2::{self, Sv2Option}; @@ -113,7 +113,7 @@ impl<'a> StandardChannel<'a> { &mut self, extranonce_prefix: Vec, ) -> Result<(), StandardChannelError> { - if extranonce_prefix.len() > MAX_EXTRANONCE_PREFIX_LEN { + if extranonce_prefix.len() > MAX_EXTRANONCE_LEN { return Err(StandardChannelError::NewExtranoncePrefixTooLarge); } diff --git a/sv2/channels-sv2/src/extranonce_manager.rs b/sv2/channels-sv2/src/extranonce_manager.rs new file mode 100644 index 0000000000..821534ab28 --- /dev/null +++ b/sv2/channels-sv2/src/extranonce_manager.rs @@ -0,0 +1,847 @@ +//! Extranonce prefix allocation for downstream SV2 channels. +//! +//! [`ExtranonceAllocator`] manages unique extranonce prefixes for both standard and +//! extended mining channels. It replaces the previous `ExtendedExtranonce` factory +//! from `mining_sv2`, adding support for prefix reuse when channels close. +//! +//! # Extranonce layout +//! +//! | upstream_prefix | server_id | local_prefix_id | rollable_extranonce_size | +//! |:---------------:|:---------:|:---------------:|:-----------------------:| +//! | (fixed) | (optional static) | (dynamic, unique per channel) | (for rolling or downstream allocation) | +//! +//! - **upstream_prefix**: Fixed bytes assigned by the upstream node (empty for a Pool server). +//! - **local_prefix**: Per-channel bytes controlled by *this* node, composed of: +//! - **server_id** (optional): Static identifier for this server instance. +//! - **local_prefix_id**: Dynamic bytes tracked by a bit vector — each channel gets a unique value. +//! - **rollable_extranonce_size**: Bytes left for the downstream miner to roll (extended channels), or do further nested allocation layers. +//! Standard channels zero-pad this portion in the prefix since they don't roll. +//! +//! # Sizing `local_prefix_len` and `max_channels` +//! +//! `local_prefix_len` controls how many bytes are reserved in the extranonce for +//! this node's per-channel identification. It is split into `server_id` (optional, +//! static) and `local_prefix_id` (dynamic, one unique value per channel): +//! +//! ```text +//! local_prefix_id_len = local_prefix_len - server_id.len() +//! ``` +//! +//! `max_channels` can be **at most** `2^(local_prefix_id_len * 8)`, but it can also +//! be smaller. When it is smaller, the unused high bytes of `local_prefix_id` will +//! always be zero — effectively wasting extranonce space that could have gone to +//! `rollable_extranonce_size`. +//! +//! **Rule of thumb:** match `local_prefix_id_len` to `max_channels` so that no +//! bytes are permanently zero. +//! +//! | `local_prefix_id_len` | Max addressable | Bitmap memory | +//! |-----------------------|-------------------|---------------| +//! | 1 byte | 256 | 32 B | +//! | 2 bytes | 65,536 | 8 KB | +//! | 3 bytes | 16,777,216 | 2 MB | +//! | 4 bytes | 4,294,967,296 | 512 MB | +//! +//! **Example — Pool with `server_id`:** +//! +//! ```text +//! local_prefix_len = 4, server_id = 2 bytes → local_prefix_id_len = 2 +//! max_channels = 65,536 → uses full 2-byte range, no wasted bytes ✓ +//! ``` +//! +//! **Counter-example — no `server_id`, oversized `local_prefix_len`:** +//! +//! ```text +//! local_prefix_len = 4, server_id = none → local_prefix_id_len = 4 +//! max_channels = 65,536 → ids encoded in 4 bytes, top 2 always zero ✗ +//! Better: local_prefix_len = 2, giving 2 extra bytes to rollable_extranonce_size +//! ``` +//! +//! # Usage +//! +//! ## Pool (root node, no upstream) +//! +//! A pool creates the allocator with [`ExtranonceAllocator::new`], providing an optional +//! `server_id` to distinguish multiple pool server instances. It then calls +//! [`allocate_standard`](ExtranonceAllocator::allocate_standard) or +//! [`allocate_extended`](ExtranonceAllocator::allocate_extended) each time a downstream +//! opens a channel. +//! +//! ``` +//! use channels_sv2::extranonce_manager::ExtranonceAllocator; +//! +//! // Pool: 20-byte extranonce, 4-byte local prefix (2 for server_id, 2 for channel id), +//! // leaving 16 bytes for downstream rolling. +//! let mut allocator = ExtranonceAllocator::new( +//! 20, // total_extranonce_len +//! 4, // local_prefix_len +//! Some(vec![0x00, 0x01]), // server_id (2 bytes) +//! 65_536, // max_channels (bitmap: 65_536 / 8 = 8 KB) +//! ).unwrap(); +//! +//! // When a downstream opens a standard channel: +//! let prefix = allocator.allocate_standard().unwrap(); +//! let prefix_bytes = prefix.as_bytes(); // pass to StandardChannel constructor +//! let id = prefix.local_prefix_id(); // store for later freeing +//! +//! // When the channel closes: +//! allocator.free(id); +//! ``` +//! +//! ## JDC/ Translator / Proxies (receives upstream extranonce prefix) +//! +//! Proxies (JDC and Translator included) receive an `extranonce_prefix` from their upstream node +//! (via `OpenExtendedMiningChannelSuccess` or `SetExtranoncePrefix`). They create +//! the allocator with [`ExtranonceAllocator::from_upstream`] and then subdivide +//! the remaining space for their own downstream channels. +//! +//! ``` +//! use channels_sv2::extranonce_manager::ExtranonceAllocator; +//! +//! // Upstream assigned prefix [0xAA, 0xBB, 0xCC, 0xDD] with total extranonce of 20 bytes. +//! let upstream_prefix = vec![0xAA, 0xBB, 0xCC, 0xDD]; +//! let mut allocator = ExtranonceAllocator::from_upstream( +//! upstream_prefix, +//! 4, // local_prefix_len (bytes this node reserves for channel identification) +//! 20, // total_extranonce_len +//! 65_536, // max_channels (bitmap: 65_536 / 8 = 8 KB) +//! ).unwrap(); +//! +//! // When a downstream opens an extended channel with min_extranonce_size from the request: +//! let prefix = allocator.allocate_extended(12).unwrap(); +//! let prefix_bytes = prefix.as_bytes(); // pass to ExtendedChannel constructor +//! ``` + +extern crate alloc; +use alloc::vec; +use alloc::vec::Vec; + +/// Maximum extranonce length in bytes (Sv2 spec). +pub const MAX_EXTRANONCE_LEN: usize = 32; + +// --------------------------------------------------------------------------- +// ExtranonceAllocator +// --------------------------------------------------------------------------- + +/// Manages extranonce prefix allocation for downstream channels. +/// +/// A single allocator handles both standard and extended channels. Each call to +/// [`allocate_standard`](Self::allocate_standard) or [`allocate_extended`](Self::allocate_extended) +/// returns a unique [`ExtranoncePrefix`] whose bytes can be passed directly to +/// channel constructors. Because both channel types draw from the same +/// `local_prefix_id` pool (a single shared bitmap), their extranonces are +/// guaranteed never to overlap. When a channel closes, call [`free`](Self::free) +/// with the prefix's [`local_prefix_id`](ExtranoncePrefix::local_prefix_id) to +/// make it available for reuse. +/// +/// # Memory +/// +/// The allocator uses an internal bitmap of `max_channels` bits (`max_channels / 8` +/// bytes). See the module-level +/// [Sizing `local_prefix_len` and `max_channels`](index.html#sizing-local_prefix_len-and-max_channels) +/// section for a reference table. +pub struct ExtranonceAllocator { + upstream_prefix: Vec, + server_id: Vec, + local_prefix_id_len: usize, + local_prefix_len: usize, + rollable_extranonce_size: usize, + total_extranonce_len: usize, + allocation_bitmap: BitVector, + max_channels: usize, + last_allocated_id: Option, + last_freed_id: Option, +} + +impl core::fmt::Debug for ExtranonceAllocator { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("ExtranonceAllocator") + .field("upstream_prefix_len", &self.upstream_prefix.len()) + .field("local_prefix_len", &self.local_prefix_len) + .field("rollable_extranonce_size", &self.rollable_extranonce_size) + .field("total_extranonce_len", &self.total_extranonce_len) + .field("max_channels", &self.max_channels) + .field("allocated_count", &self.allocation_bitmap.count_ones()) + .finish() + } +} + +impl ExtranonceAllocator { + /// Create a fresh allocator (for Pool server / root nodes). + /// + /// Use this when there is no upstream node — the pool is the root of the + /// extranonce hierarchy. + /// + /// - `total_extranonce_len`: Total extranonce length in bytes (≤ 32). + /// - `local_prefix_len`: Bytes reserved for this node's per-channel prefix. + /// - `server_id`: Optional static server identifier (placed at the start of local_prefix). + /// - `max_channels`: Maximum concurrent channels. Determines the size of the + /// internal allocation bitmap (`max_channels / 8` bytes). See the + /// [Memory](#memory) section on [`ExtranonceAllocator`] for a reference table. + pub fn new( + total_extranonce_len: usize, + local_prefix_len: usize, + server_id: Option>, + max_channels: usize, + ) -> Result { + let server_id = server_id.unwrap_or_default(); + Self::validate_and_create( + Vec::new(), + server_id, + local_prefix_len, + total_extranonce_len, + max_channels, + ) + } + + /// Create an allocator seeded with an upstream-assigned prefix (for proxies, + /// JD clients, and translators). + /// + /// Use this when the node receives an `extranonce_prefix` from upstream via + /// `OpenExtendedMiningChannelSuccess` or `SetExtranoncePrefix`, and needs to + /// subdivide the remaining space for its own downstream channels. + /// + /// - `upstream_prefix`: The prefix bytes received from the upstream node. + /// - `local_prefix_len`: Bytes this node reserves for per-channel identification. + /// - `total_extranonce_len`: Total extranonce length. + /// - `max_channels`: Maximum concurrent channels. Determines the size of the + /// internal allocation bitmap (`max_channels / 8` bytes). See the + /// [Memory](#memory) section on [`ExtranonceAllocator`] for a reference table. + pub fn from_upstream( + upstream_prefix: Vec, + local_prefix_len: usize, + total_extranonce_len: usize, + max_channels: usize, + ) -> Result { + Self::validate_and_create( + upstream_prefix, + Vec::new(), + local_prefix_len, + total_extranonce_len, + max_channels, + ) + } + + /// Allocate a prefix for an extended channel. + /// + /// `min_rollable_size` is the downstream's requested minimum extranonce space + /// for rolling (from the `OpenExtendedMiningChannel` message). It must be + /// ≤ `rollable_extranonce_size`. + /// + /// Returns a prefix of length `upstream_prefix_len + local_prefix_len`. + pub fn allocate_extended( + &mut self, + min_rollable_size: usize, + ) -> Result { + if min_rollable_size > self.rollable_extranonce_size { + return Err(ExtranonceAllocatorError::InvalidRollableSize); + } + let id = self + .find_free_id() + .ok_or(ExtranonceAllocatorError::CapacityExhausted)?; + self.mark_allocated(id); + Ok(ExtranoncePrefix::new(id, self.build_extended_prefix(id))) + } + + /// Allocate a prefix for a standard channel. + /// + /// Returns a prefix of length `total_extranonce_len` (the full extranonce). + /// The `rollable_extranonce_size` portion is zero-padded since standard channels + /// don't roll. + pub fn allocate_standard(&mut self) -> Result { + let id = self + .find_free_id() + .ok_or(ExtranonceAllocatorError::CapacityExhausted)?; + self.mark_allocated(id); + Ok(ExtranoncePrefix::new(id, self.build_standard_prefix(id))) + } + + /// Free a previously allocated local prefix id, making it available for reuse. + pub fn free(&mut self, local_prefix_id: usize) { + if local_prefix_id < self.max_channels { + self.allocation_bitmap.set(local_prefix_id, false); + self.last_freed_id = Some(local_prefix_id); + } + } + + /// Bytes available for downstream rolling (extended channels). + pub fn rollable_extranonce_size(&self) -> usize { + self.rollable_extranonce_size + } + + /// Length of the upstream prefix portion. + pub fn upstream_prefix_len(&self) -> usize { + self.upstream_prefix.len() + } + + /// The upstream prefix bytes. + pub fn upstream_prefix(&self) -> &[u8] { + &self.upstream_prefix + } + + /// Total extranonce length. + pub fn total_extranonce_len(&self) -> usize { + self.total_extranonce_len + } + + /// Length of the local prefix portion (server_id + local_prefix_id). + pub fn local_prefix_len(&self) -> usize { + self.local_prefix_len + } + + /// Number of currently allocated channels. + pub fn allocated_count(&self) -> usize { + self.allocation_bitmap.count_ones() + } + + /// Maximum number of concurrent channels. + pub fn max_channels(&self) -> usize { + self.max_channels + } + + /// Validate configuration and create the allocator. Shared by `new()` and `from_upstream()`. + fn validate_and_create( + upstream_prefix: Vec, + server_id: Vec, + local_prefix_len: usize, + total_extranonce_len: usize, + max_channels: usize, + ) -> Result { + if total_extranonce_len > MAX_EXTRANONCE_LEN { + return Err(ExtranonceAllocatorError::ExceedsMaxLength); + } + if upstream_prefix.len() + local_prefix_len > total_extranonce_len { + return Err(ExtranonceAllocatorError::InvalidLengths); + } + if max_channels == 0 { + return Err(ExtranonceAllocatorError::InvalidLengths); + } + if server_id.len() >= local_prefix_len { + return Err(ExtranonceAllocatorError::ServerIdTooLong); + } + + let local_prefix_id_len = local_prefix_len - server_id.len(); + // Each `local_prefix_id` is encoded as `local_prefix_id_len` bytes, so the + // largest representable id is `2^(local_prefix_id_len * 8) - 1`. If that + // already saturates `usize` we skip the shift to avoid overflow. + let max_addressable = if local_prefix_id_len >= core::mem::size_of::() { + usize::MAX + } else { + 1usize << (local_prefix_id_len * 8) + }; + if max_channels > max_addressable { + return Err(ExtranonceAllocatorError::MaxChannelsTooLarge); + } + + let rollable_extranonce_size = + total_extranonce_len - upstream_prefix.len() - local_prefix_len; + let allocation_bitmap = BitVector::new(max_channels); + + Ok(Self { + upstream_prefix, + server_id, + local_prefix_id_len, + local_prefix_len, + rollable_extranonce_size, + total_extranonce_len, + allocation_bitmap, + max_channels, + last_allocated_id: None, + last_freed_id: None, + }) + } + + /// Mark a local_prefix_id as in-use and record it as the latest allocation. + fn mark_allocated(&mut self, id: usize) { + self.allocation_bitmap.set(id, true); + self.last_allocated_id = Some(id); + } + + /// Find the next available local_prefix_id. + /// + /// Scans forward from the last allocation point, wrapping around. + /// Prefers any free id over `last_freed_id` to avoid immediately reusing + /// a prefix whose previous channel may still have in-flight shares. + /// Falls back to `last_freed_id` if it is the only one available. + fn find_free_id(&self) -> Option { + let start = self + .last_allocated_id + .map_or(0, |id| (id + 1) % self.max_channels); + + // Try to find a free id that isn't the one we just freed. + if let Some(id) = self.first_free_id_wrapping(start, self.last_freed_id) { + return Some(id); + } + + // Fall back to last_freed_id if it's the only one left. + self.last_freed_id + .filter(|&id| id < self.max_channels && !self.allocation_bitmap.get(id)) + } + + /// Scan the bitmap starting at `from`, wrapping around to cover all ids. + /// Skips `skip` (if provided) so the caller can avoid a specific id. + fn first_free_id_wrapping(&self, from: usize, skip: Option) -> Option { + self.first_free_id_in_range(from, self.max_channels, skip) + .or_else(|| { + if from > 0 { + self.first_free_id_in_range(0, from, skip) + } else { + None + } + }) + } + + /// Scan `[from, to)` for the first free id, skipping `skip` if provided. + fn first_free_id_in_range(&self, from: usize, to: usize, skip: Option) -> Option { + let mut cursor = from; + while cursor < to { + match self.allocation_bitmap.find_first_zero_in_range(cursor, to) { + Some(id) if Some(id) == skip => cursor = id + 1, + result => return result, + } + } + None + } + + /// Build the prefix bytes for an extended channel. + /// + /// Layout: `[upstream_prefix | server_id | encoded local_prefix_id]` + fn build_extended_prefix(&self, local_prefix_id: usize) -> Vec { + let mut prefix = Vec::with_capacity(self.upstream_prefix.len() + self.local_prefix_len); + prefix.extend_from_slice(&self.upstream_prefix); + prefix.extend_from_slice(&self.server_id); + prefix.extend_from_slice(&Self::local_prefix_id_to_bytes( + local_prefix_id, + self.local_prefix_id_len, + )); + prefix + } + + /// Build the prefix bytes for a standard channel. + /// + /// Same as the extended prefix, plus zero-padded rollable bytes at the end + /// (standard channels don't roll, so the pool fills the full extranonce). + fn build_standard_prefix(&self, local_prefix_id: usize) -> Vec { + let mut prefix = self.build_extended_prefix(local_prefix_id); + prefix.resize(self.total_extranonce_len, 0); + prefix + } + + /// Encode a `local_prefix_id` (usize) into `len` big-endian bytes. + /// + /// Example: `local_prefix_id_to_bytes(258, 2)` → `[0x01, 0x02]` + fn local_prefix_id_to_bytes(local_prefix_id: usize, len: usize) -> Vec { + let mut result = vec![0u8; len]; + let be_bytes = local_prefix_id.to_be_bytes(); + let copy_len = be_bytes.len().min(len); + let src_start = be_bytes.len() - copy_len; + let dst_start = len - copy_len; + result[dst_start..].copy_from_slice(&be_bytes[src_start..]); + result + } +} + +// --------------------------------------------------------------------------- +// ExtranoncePrefix +// --------------------------------------------------------------------------- + +/// An allocated extranonce prefix returned by the allocator. +/// +/// Stores both the raw prefix bytes (to pass to channel constructors) and the +/// `local_prefix_id` (to pass back to [`ExtranonceAllocator::free`] when the channel closes). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ExtranoncePrefix { + local_prefix_id: usize, + prefix: Vec, +} + +impl ExtranoncePrefix { + fn new(local_prefix_id: usize, prefix: Vec) -> Self { + Self { + local_prefix_id, + prefix, + } + } + + /// The raw prefix bytes — pass these to channel constructors. + pub fn as_bytes(&self) -> &[u8] { + &self.prefix + } + + /// Consume and return the prefix bytes. + pub fn into_bytes(self) -> Vec { + self.prefix + } + + /// The assigned local prefix id — store this and pass to [`ExtranonceAllocator::free`] + /// when the channel closes. + pub fn local_prefix_id(&self) -> usize { + self.local_prefix_id + } +} + +// --------------------------------------------------------------------------- +// ExtranonceAllocatorError +// --------------------------------------------------------------------------- + +/// Errors returned by [`ExtranonceAllocator`] operations. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExtranonceAllocatorError { + /// `total_extranonce_len` exceeds [`MAX_EXTRANONCE_LEN`] (32). + ExceedsMaxLength, + /// The combination of upstream_prefix, local_prefix_len, and total_extranonce_len is invalid. + InvalidLengths, + /// `server_id` is too long — it must be strictly shorter than `local_prefix_len`. + ServerIdTooLong, + /// `max_channels` exceeds the range addressable by `local_prefix_id` bytes. + MaxChannelsTooLarge, + /// All channels are allocated — no more capacity. + CapacityExhausted, + /// The requested rollable extranonce size exceeds `rollable_extranonce_size`. + InvalidRollableSize, +} + +impl core::fmt::Display for ExtranonceAllocatorError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::ExceedsMaxLength => write!(f, "total extranonce length exceeds 32 bytes"), + Self::InvalidLengths => write!(f, "invalid length configuration"), + Self::ServerIdTooLong => { + write!( + f, + "server_id must be strictly shorter than local_prefix_len" + ) + } + Self::MaxChannelsTooLarge => { + write!( + f, + "max_channels exceeds addressable range of local_prefix_id bytes" + ) + } + Self::CapacityExhausted => write!(f, "all channels are allocated — no more capacity"), + Self::InvalidRollableSize => { + write!(f, "requested rollable size exceeds available space") + } + } + } +} + +// --------------------------------------------------------------------------- +// BitVector (internal) +// --------------------------------------------------------------------------- + +/// A compact bit vector that tracks which `local_prefix_id` values are in use. +/// +/// Bits are packed into `u64` words — each word holds the allocation state for +/// 64 consecutive ids. A set bit means the id is allocated; a clear bit means +/// it is free. +/// +/// ```text +/// words[0] words[1] words[2] ... +/// ┌────────────────────┐ ┌────────────────────┐ ┌────────────── +/// │ ids 0..63 │ │ ids 64..127 │ │ ids 128..191 +/// │ (64 bits per word) │ │ │ │ +/// └────────────────────┘ └────────────────────┘ └────────────── +/// ``` +/// +/// `capacity` is the actual number of valid ids (i.e. `max_channels`). +/// Because the storage is rounded up to the nearest multiple of 64, the +/// last word may contain trailing bits beyond `capacity` — these are +/// never set and are excluded from search results. +/// +/// Word-level operations allow bulk checks: +/// - A word equal to `u64::MAX` means all 64 ids in that range are taken — +/// the scanner skips the entire word in one comparison. +/// - `trailing_zeros()` on the inverted word finds the first free id within +/// a word using a single CPU instruction (`TZCNT`/`BSF` on x86). +/// - `count_ones()` compiles to the hardware `POPCNT` instruction. +struct BitVector { + /// Packed allocation bits. `words[i]` covers ids `i*64 .. (i+1)*64`. + words: Vec, + /// Number of valid ids tracked (= `max_channels`). May be less than + /// `words.len() * 64` when `max_channels` is not a multiple of 64. + capacity: usize, +} + +impl BitVector { + fn new(capacity: usize) -> Self { + let num_words = capacity.div_ceil(64); + Self { + words: vec![0u64; num_words], + capacity, + } + } + + #[cfg(test)] + fn len(&self) -> usize { + self.capacity + } + + fn get(&self, index: usize) -> bool { + debug_assert!(index < self.capacity); + let word = index / 64; + let bit = index % 64; + (self.words[word] >> bit) & 1 == 1 + } + + fn set(&mut self, index: usize, value: bool) { + debug_assert!(index < self.capacity); + let word = index / 64; + let bit = index % 64; + if value { + self.words[word] |= 1u64 << bit; + } else { + self.words[word] &= !(1u64 << bit); + } + } + + fn count_ones(&self) -> usize { + self.words.iter().map(|w| w.count_ones() as usize).sum() + } + + /// Find the first zero bit in `[from, to)`, skipping fully-set u64 words. + /// Returns `None` if every bit in the range is set. + fn find_first_zero_in_range(&self, from: usize, to: usize) -> Option { + if from >= to { + return None; + } + + let mut idx = from; + while idx < to { + let word_idx = idx / 64; + let word = self.words[word_idx]; + + if word == u64::MAX { + idx = (word_idx + 1) * 64; + continue; + } + + let bit_offset = idx % 64; + let mask = !0u64 << bit_offset; + let zeros_masked = !word & mask; + + if zeros_masked != 0 { + let first_zero = word_idx * 64 + zeros_masked.trailing_zeros() as usize; + if first_zero < to && first_zero < self.capacity { + return Some(first_zero); + } + } + + idx = (word_idx + 1) * 64; + } + + None + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + + // -- BitVector unit tests ----------------------------------------------- + + #[test] + fn bitvector_basic() { + let mut bv = BitVector::new(128); + assert_eq!(bv.len(), 128); + assert!(!bv.get(0)); + + bv.set(0, true); + assert!(bv.get(0)); + + bv.set(0, false); + assert!(!bv.get(0)); + } + + #[test] + fn bitvector_count_ones() { + let mut bv = BitVector::new(256); + assert_eq!(bv.count_ones(), 0); + + for i in (0..256).step_by(2) { + bv.set(i, true); + } + assert_eq!(bv.count_ones(), 128); + } + + #[test] + fn bitvector_find_zero_skips_full_words() { + let mut bv = BitVector::new(192); + for i in 0..128 { + bv.set(i, true); + } + let found = bv.find_first_zero_in_range(0, 192); + assert_eq!(found, Some(128)); + } + + #[test] + fn bitvector_find_zero_partial_range() { + let mut bv = BitVector::new(64); + bv.set(3, true); + bv.set(4, true); + + assert_eq!(bv.find_first_zero_in_range(3, 10), Some(5)); + assert_eq!(bv.find_first_zero_in_range(0, 3), Some(0)); + } + + #[test] + fn bitvector_find_zero_all_set() { + let mut bv = BitVector::new(64); + for i in 0..64 { + bv.set(i, true); + } + assert_eq!(bv.find_first_zero_in_range(0, 64), None); + } + + // -- ExtranonceAllocator tests ------------------------------------------ + + #[test] + fn pool_basic_allocation() { + let mut alloc = ExtranonceAllocator::new(20, 4, Some(vec![0x00, 0x01]), 65536).unwrap(); + + assert_eq!(alloc.total_extranonce_len(), 20); + assert_eq!(alloc.local_prefix_len(), 4); + assert_eq!(alloc.upstream_prefix_len(), 0); + assert_eq!(alloc.rollable_extranonce_size(), 16); + assert_eq!(alloc.max_channels(), 65536); + assert_eq!(alloc.allocated_count(), 0); + + let ext = alloc.allocate_extended(16).unwrap(); + assert_eq!(ext.as_bytes().len(), 4); + assert_eq!(&ext.as_bytes()[0..2], &[0x00, 0x01]); + assert_eq!(alloc.allocated_count(), 1); + + let std = alloc.allocate_standard().unwrap(); + assert_eq!(std.as_bytes().len(), 20); + assert_eq!(&std.as_bytes()[0..2], &[0x00, 0x01]); + assert_eq!(alloc.allocated_count(), 2); + } + + #[test] + fn proxy_from_upstream() { + let upstream_prefix = vec![0xAA, 0xBB, 0xCC, 0xDD]; + let mut alloc = + ExtranonceAllocator::from_upstream(upstream_prefix.clone(), 4, 20, 65536).unwrap(); + + assert_eq!(alloc.upstream_prefix(), &[0xAA, 0xBB, 0xCC, 0xDD]); + assert_eq!(alloc.upstream_prefix_len(), 4); + assert_eq!(alloc.rollable_extranonce_size(), 12); + + let ext = alloc.allocate_extended(12).unwrap(); + assert_eq!(ext.as_bytes().len(), 8); + assert_eq!(&ext.as_bytes()[0..4], &[0xAA, 0xBB, 0xCC, 0xDD]); + } + + #[test] + fn uniqueness() { + let mut alloc = ExtranonceAllocator::new(20, 4, None, 256).unwrap(); + let mut seen = HashSet::new(); + + for _ in 0..256 { + let p = alloc.allocate_extended(16).unwrap(); + assert!(seen.insert(p.as_bytes().to_vec()), "duplicate prefix"); + } + + assert_eq!(alloc.allocated_count(), 256); + } + + #[test] + fn exhaustion_and_reuse() { + let mut alloc = ExtranonceAllocator::new(6, 2, None, 256).unwrap(); + + let mut ids = Vec::new(); + for _ in 0..256 { + let p = alloc.allocate_extended(4).unwrap(); + ids.push(p.local_prefix_id()); + } + + assert!(alloc.allocate_extended(4).is_err()); + assert_eq!(alloc.allocated_count(), 256); + + alloc.free(ids[42]); + assert_eq!(alloc.allocated_count(), 255); + + let reused = alloc.allocate_extended(4).unwrap(); + assert_eq!(reused.local_prefix_id(), ids[42]); + assert_eq!(alloc.allocated_count(), 256); + } + + #[test] + fn free_and_skip_last_freed() { + let mut alloc = ExtranonceAllocator::new(6, 2, None, 256).unwrap(); + + let mut ids = Vec::new(); + for _ in 0..256 { + ids.push(alloc.allocate_extended(4).unwrap().local_prefix_id()); + } + + let id_a = ids[10]; + let id_b = ids[11]; + alloc.free(id_a); + alloc.free(id_b); + + let reused = alloc.allocate_extended(4).unwrap(); + assert_ne!( + reused.local_prefix_id(), + id_b, + "should skip the last-freed id" + ); + assert_eq!(reused.local_prefix_id(), id_a); + } + + #[test] + fn standard_prefix_includes_rollable_zeros() { + let mut alloc = ExtranonceAllocator::new(20, 4, Some(vec![0x01]), 256).unwrap(); + + let std = alloc.allocate_standard().unwrap(); + assert_eq!(std.as_bytes().len(), 20); + assert!(std.as_bytes()[4..].iter().all(|&b| b == 0)); + } + + #[test] + fn invalid_rollable_size() { + let mut alloc = ExtranonceAllocator::new(20, 4, None, 256).unwrap(); + let err = alloc.allocate_extended(17); + assert_eq!(err, Err(ExtranonceAllocatorError::InvalidRollableSize)); + } + + #[test] + fn validation_errors() { + assert_eq!( + ExtranonceAllocator::new(33, 4, None, 256).unwrap_err(), + ExtranonceAllocatorError::ExceedsMaxLength + ); + assert_eq!( + ExtranonceAllocator::new(20, 2, Some(vec![1, 2]), 256).unwrap_err(), + ExtranonceAllocatorError::ServerIdTooLong + ); + assert_eq!( + ExtranonceAllocator::new(20, 2, Some(vec![1]), 257).unwrap_err(), + ExtranonceAllocatorError::MaxChannelsTooLarge + ); + assert_eq!( + ExtranonceAllocator::new(4, 5, None, 256).unwrap_err(), + ExtranonceAllocatorError::InvalidLengths + ); + assert_eq!( + ExtranonceAllocator::new(20, 4, None, 0).unwrap_err(), + ExtranonceAllocatorError::InvalidLengths + ); + } + + #[test] + fn no_overlap_standard_and_extended() { + let mut alloc = ExtranonceAllocator::new(8, 2, None, 256).unwrap(); + + let ext = alloc.allocate_extended(6).unwrap(); + let std = alloc.allocate_standard().unwrap(); + + assert_ne!(ext.local_prefix_id(), std.local_prefix_id()); + assert_ne!(&ext.as_bytes()[..2], &std.as_bytes()[..2]); + } +} diff --git a/sv2/channels-sv2/src/lib.rs b/sv2/channels-sv2/src/lib.rs index 33a8868c2b..85f1dc1863 100644 --- a/sv2/channels-sv2/src/lib.rs +++ b/sv2/channels-sv2/src/lib.rs @@ -15,8 +15,8 @@ //! - [`client`] module is `no_std` compatible. To enable it build the crate with `no_std` feature. #![cfg_attr(feature = "no_std", no_std)] -/// Maximum length for extranonce prefixes in bytes -const MAX_EXTRANONCE_PREFIX_LEN: usize = 32; +pub mod extranonce_manager; +pub use extranonce_manager::MAX_EXTRANONCE_LEN; #[cfg(not(feature = "no_std"))] pub mod server; diff --git a/sv2/channels-sv2/src/server/extended.rs b/sv2/channels-sv2/src/server/extended.rs index d07ec91d04..4534946a9b 100644 --- a/sv2/channels-sv2/src/server/extended.rs +++ b/sv2/channels-sv2/src/server/extended.rs @@ -48,7 +48,7 @@ use crate::{ share_accounting::{ShareAccounting, ShareValidationError, ShareValidationResult}, }, target::{bytes_to_hex, hash_rate_to_target, u256_to_block_hash}, - MAX_EXTRANONCE_PREFIX_LEN, + MAX_EXTRANONCE_LEN, }; use bitcoin::{ blockdata::block::{Header, Version}, @@ -214,7 +214,7 @@ where return Err(ExtendedChannelError::RequestedMaxTargetOutOfRange); } - if extranonce_prefix.len() > MAX_EXTRANONCE_PREFIX_LEN { + if extranonce_prefix.len() > MAX_EXTRANONCE_LEN { return Err(ExtendedChannelError::ExtranoncePrefixTooLarge); } @@ -291,7 +291,7 @@ where &mut self, extranonce_prefix: Vec, ) -> Result<(), ExtendedChannelError> { - if extranonce_prefix.len() > MAX_EXTRANONCE_PREFIX_LEN { + if extranonce_prefix.len() > MAX_EXTRANONCE_LEN { return Err(ExtendedChannelError::ExtranoncePrefixTooLarge); } diff --git a/sv2/channels-sv2/src/server/standard.rs b/sv2/channels-sv2/src/server/standard.rs index 813f536783..66557e96c7 100644 --- a/sv2/channels-sv2/src/server/standard.rs +++ b/sv2/channels-sv2/src/server/standard.rs @@ -43,7 +43,7 @@ use crate::{ share_accounting::{ShareAccounting, ShareValidationError, ShareValidationResult}, }, target::{bytes_to_hex, hash_rate_to_target, u256_to_block_hash}, - MAX_EXTRANONCE_PREFIX_LEN, + MAX_EXTRANONCE_LEN, }; use bitcoin::{ absolute::LockTime, @@ -201,7 +201,7 @@ where return Err(StandardChannelError::RequestedMaxTargetOutOfRange); } - if extranonce_prefix.len() > MAX_EXTRANONCE_PREFIX_LEN { + if extranonce_prefix.len() > MAX_EXTRANONCE_LEN { return Err(StandardChannelError::ExtranoncePrefixTooLarge); } @@ -256,7 +256,7 @@ where &mut self, extranonce_prefix: Vec, ) -> Result<(), StandardChannelError> { - if extranonce_prefix.len() > MAX_EXTRANONCE_PREFIX_LEN { + if extranonce_prefix.len() > MAX_EXTRANONCE_LEN { return Err(StandardChannelError::ExtranoncePrefixTooLarge); } diff --git a/sv2/handlers-sv2/Cargo.toml b/sv2/handlers-sv2/Cargo.toml index fd08ca8f5f..f6bd0292c4 100644 --- a/sv2/handlers-sv2/Cargo.toml +++ b/sv2/handlers-sv2/Cargo.toml @@ -16,7 +16,7 @@ parsers_sv2 = { path = "../parsers-sv2", version = "^0.2.0"} binary_sv2 = { path = "../binary-sv2", version = "^5.0.0" } common_messages_sv2 = { path = "../subprotocols/common-messages", version = "^7.0.0" } framing_sv2 = { path = "../framing-sv2", version = "^6.0.0" } -mining_sv2 = { path = "../subprotocols/mining", version = "^8.0.0" } +mining_sv2 = { path = "../subprotocols/mining", version = "^9.0.0" } template_distribution_sv2 = { path = "../subprotocols/template-distribution", version = "^5.0.0" } job_declaration_sv2 = { path = "../subprotocols/job-declaration", version = "^7.0.0" } extensions_sv2 = { path = "../extensions-sv2", version = "^0.1.0" } diff --git a/sv2/parsers-sv2/Cargo.toml b/sv2/parsers-sv2/Cargo.toml index 52807a6f41..0c57874ec1 100644 --- a/sv2/parsers-sv2/Cargo.toml +++ b/sv2/parsers-sv2/Cargo.toml @@ -15,7 +15,7 @@ keywords = ["stratum", "mining", "bitcoin", "protocol"] binary_sv2 = { path = "../binary-sv2", version = "^5.0.0" } framing_sv2 = { path = "../framing-sv2", version = "^6.0.0" } common_messages_sv2 = { path = "../subprotocols/common-messages", version = "^7.0.0" } -mining_sv2 = { path = "../subprotocols/mining", version = "^8.0.0" } +mining_sv2 = { path = "../subprotocols/mining", version = "^9.0.0" } template_distribution_sv2 = { path = "../subprotocols/template-distribution", version = "^5.0.0" } job_declaration_sv2 = { path = "../subprotocols/job-declaration", version = "^7.0.0" } extensions_sv2 = { path = "../extensions-sv2", version = "^0.1.0" } diff --git a/sv2/subprotocols/mining/Cargo.toml b/sv2/subprotocols/mining/Cargo.toml index e540911cca..9a409b9b5a 100644 --- a/sv2/subprotocols/mining/Cargo.toml +++ b/sv2/subprotocols/mining/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mining_sv2" -version = "8.0.0" +version = "9.0.0" authors = ["The Stratum V2 Developers"] edition = "2021" readme = "README.md" diff --git a/sv2/subprotocols/mining/src/lib.rs b/sv2/subprotocols/mining/src/lib.rs index d8433bde32..d54b00e9c7 100644 --- a/sv2/subprotocols/mining/src/lib.rs +++ b/sv2/subprotocols/mining/src/lib.rs @@ -24,10 +24,6 @@ #![no_std] -use binary_sv2::{B032, U256}; -use core::convert::TryInto; - -#[macro_use] extern crate alloc; mod close_channel; @@ -42,7 +38,6 @@ mod submit_shares; mod update_channel; pub use close_channel::CloseChannel; -use core::ops::Range; pub use new_mining_job::{NewExtendedMiningJob, NewMiningJob}; pub use open_channel::{ OpenExtendedMiningChannel, OpenExtendedMiningChannelSuccess, OpenMiningChannelError, @@ -105,1095 +100,3 @@ pub const CHANNEL_BIT_SUBMIT_SHARES_STANDARD: bool = true; pub const CHANNEL_BIT_SUBMIT_SHARES_SUCCESS: bool = true; pub const CHANNEL_BIT_UPDATE_CHANNEL: bool = true; pub const CHANNEL_BIT_UPDATE_CHANNEL_ERROR: bool = true; - -pub const MAX_EXTRANONCE_LEN: usize = 32; - -impl From for alloc::vec::Vec { - fn from(v: Extranonce) -> Self { - v.extranonce - } -} - -// WARNING: do not derive Copy on this type. Some operations performed to a copy of an extranonce -// do not affect the original, and this may lead to different extranonce inconsistency -/// Extranonce bytes which need to be added to the coinbase to form a fully valid submission. -/// -/// Representation is in big endian, so tail is for the digits relative to smaller powers -/// -/// `full coinbase = coinbase_tx_prefix + extranonce + coinbase_tx_suffix`. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Extranonce { - extranonce: alloc::vec::Vec, -} - -// this function converts a U256 type in little endian to Extranonce type -impl<'a> From> for Extranonce { - fn from(v: U256<'a>) -> Self { - let extranonce: alloc::vec::Vec = v.inner_as_ref().into(); - Self { extranonce } - } -} - -// This function converts an Extranonce type to U256n little endian -impl From for U256<'_> { - fn from(v: Extranonce) -> Self { - let inner = v.extranonce; - debug_assert!(inner.len() <= 32); - // below unwraps never panics - inner.try_into().unwrap() - } -} - -// this function converts an extranonce to the type B032 -impl<'a> From> for Extranonce { - fn from(v: B032<'a>) -> Self { - let extranonce: alloc::vec::Vec = v.inner_as_ref().into(); - Self { extranonce } - } -} - -// this function converts an Extranonce type in B032 in little endian -impl From for B032<'_> { - fn from(v: Extranonce) -> Self { - let inner = v.extranonce.to_vec(); - // below unwraps never panics - inner.try_into().unwrap() - } -} - -impl Default for Extranonce { - fn default() -> Self { - Self { - extranonce: vec![0; 32], - } - } -} - -impl core::convert::TryFrom> for Extranonce { - type Error = (); - - fn try_from(v: alloc::vec::Vec) -> Result { - if v.len() > MAX_EXTRANONCE_LEN { - Err(()) - } else { - Ok(Extranonce { extranonce: v }) - } - } -} - -impl Extranonce { - pub fn new(len: usize) -> Option { - if len > MAX_EXTRANONCE_LEN { - None - } else { - let extranonce = vec![0; len]; - Some(Self { extranonce }) - } - } - - /// this function converts a Extranonce type to b032 type - pub fn from_vec_with_len(mut extranonce: alloc::vec::Vec, len: usize) -> Self { - extranonce.resize(len, 0); - Self { extranonce } - } - - pub fn into_b032(self) -> B032<'static> { - self.into() - } - // B032 type is more used, this is why the output signature is not ExtendedExtranoncee the B032 - // type is more used, this is why the output signature is not ExtendedExtranoncee - #[allow(clippy::should_implement_trait)] - pub fn next(&mut self) -> Option> { - increment_bytes_be(&mut self.extranonce).ok()?; - // below unwraps never panics - Some(self.extranonce.clone().try_into().unwrap()) - } - - pub fn to_vec(self) -> alloc::vec::Vec { - self.extranonce - } -} - -impl From<&mut ExtendedExtranonce> for Extranonce { - fn from(v: &mut ExtendedExtranonce) -> Self { - let mut extranonce = v.inner.to_vec(); - extranonce.truncate(v.range_2.end); - Self { extranonce } - } -} - -#[derive(Debug, Clone)] -/// Downstream and upstream are relative to user P. In simple terms, upstream is -/// the part of the protocol that a user P sees when looking above, and downstream is what they see -/// when looking below. -/// -/// An `ExtendedExtranonce` is defined by 3 ranges: -/// -/// - `range_0`: Represents the extended extranonce part reserved by upstream relative to P (for -/// most upstream nodes, e.g., a pool, this is `[0..0]`) and it is fixed for P. -/// -/// - `range_1`: Represents the extended extranonce part reserved for P. P assigns to every relative -/// downstream a unique extranonce with different values in range_1 in the following way: if D_i -/// is the (i+1)-th downstream that connected to P, then D_i gets from P an extranonce with -/// range_1=i (note that the concatenation of range_0 and range_1 is the range_0 relative to D_i, -/// and range_2 of P is the range_1 of D_i). -/// -/// - `range_2`: Represents the range that P reserves for the downstreams. -/// -/// -/// In the following scenarios, we examine the extended extranonce in some cases: -/// -/// **Scenario 1: P is a pool** -/// - `range_0` → `0..0`: There is no upstream relative to the pool P, so no space is reserved by -/// the upstream. -/// - `range_1` → `0..16`: The pool P increments these bytes to ensure each downstream gets a unique -/// extended extranonce search space. The pool could optionally choose to set some fixed bytes as -/// `static_prefix` (no bigger than 2 bytes), which are set on the beginning of this range and -/// will not be incremented. These bytes are used to allow unique allocation for the pool's mining -/// server (if there are more than one). -/// - `range_2` → `16..32`: These bytes are not changed by the pool but are changed by the pool's -/// downstream. -/// -/// **Scenario 2: P is a translator proxy** -/// - `range_0` → `0..16`: These bytes are set by the upstream and P shouldn't change them. -/// - `range_1` → `16..24`: These bytes are modified by P each time an Sv1 mining device connects, -/// ensuring each connected Sv1 mining device gets a different extended extranonce search space. -/// - `range_2` → `24..32`: These bytes are left free for the Sv1 mining device. -/// -/// **Scenario 3: P is an Sv1 mining device** -/// - `range_0` → `0..24`: These bytes are set by the device's upstreams. -/// - `range_1` → `24..32`: These bytes are changed by P (if capable) to increment the search space. -/// - `range_2` → `32..32`: No more downstream. -/// -/// # Examples -/// -/// Basic usage without static prefix: -/// -/// ``` -/// use mining_sv2::*; -/// use core::convert::TryInto; -/// -/// // Create an extended extranonce of len 32, reserving the first 7 bytes for the pool -/// let mut pool_extended_extranonce = ExtendedExtranonce::new(0..0, 0..7, 7..32, None).unwrap(); -/// -/// // On open extended channel (requesting to use a range of 4 bytes), the pool allocates this extranonce_prefix: -/// let new_extended_channel_extranonce_prefix = pool_extended_extranonce.next_prefix_extended(4).unwrap(); -/// let expected_extranonce_prefix = vec![0, 0, 0, 0, 0, 0, 1]; -/// assert_eq!(new_extended_channel_extranonce_prefix.clone().to_vec(), expected_extranonce_prefix); -/// -/// // On open extended channel (requesting to use a range of 20 bytes), the pool allocates this extranonce_prefix: -/// let new_extended_channel_extranonce_prefix = pool_extended_extranonce.next_prefix_extended(20).unwrap(); -/// let expected_extranonce_prefix = vec![0, 0, 0, 0, 0, 0, 2]; -/// assert_eq!(new_extended_channel_extranonce_prefix.clone().to_vec(), expected_extranonce_prefix); -/// -/// // On open extended channel (requesting to use a range of 26 bytes, which is too much), we get an error: -/// let new_extended_channel_extranonce_prefix_error = pool_extended_extranonce.next_prefix_extended(26); -/// assert!(new_extended_channel_extranonce_prefix_error.is_err()); -/// -/// // Then the pool receives a request to open a standard channel -/// let new_standard_channel_extranonce = pool_extended_extranonce.next_prefix_standard().unwrap(); -/// // For standard channels, only the bytes in range_2 are incremented -/// let expected_standard_extranonce = vec![0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; -/// assert_eq!(new_standard_channel_extranonce.to_vec(), expected_standard_extranonce); -/// -/// // Now the proxy receives the ExtendedExtranonce previously created -/// // The proxy knows the extranonce space reserved to the pool is 7 bytes and that the total -/// // extranonce len is 32 bytes and decides to reserve 4 bytes for itself and leave the remaining 21 for -/// // further downstreams. -/// let range_0 = 0..7; -/// let range_1 = 7..11; -/// let range_2 = 11..32; -/// let mut proxy_extended_extranonce = ExtendedExtranonce::from_upstream_extranonce( -/// new_extended_channel_extranonce_prefix, -/// range_0, -/// range_1, -/// range_2 -/// ).unwrap(); -/// -/// // The proxy generates an extended extranonce for downstream (allowing it to use a range of 3 bytes) -/// let new_extended_channel_extranonce_prefix = proxy_extended_extranonce.next_prefix_extended(3).unwrap(); -/// let expected_extranonce_prefix = vec![0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1]; -/// assert_eq!(new_extended_channel_extranonce_prefix.clone().to_vec(), expected_extranonce_prefix); -/// -/// // When the proxy receives a share from downstream and wants to recreate the full extranonce -/// // e.g., because it wants to check the share's work -/// let received_extranonce: Extranonce = vec![0, 0, 0, 0, 0, 0, 0, 8, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(); -/// let share_complete_extranonce = proxy_extended_extranonce.extranonce_from_downstream_extranonce(received_extranonce.clone()).unwrap(); -/// let expected_complete_extranonce = vec![0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 8, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; -/// assert_eq!(share_complete_extranonce.to_vec(), expected_complete_extranonce); -/// -/// // Now the proxy wants to send the extranonce received from downstream and the part of extranonce -/// // owned by itself to the pool -/// let extranonce_to_send = proxy_extended_extranonce.without_upstream_part(Some(received_extranonce)).unwrap(); -/// let expected_extranonce_to_send = vec![0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 8, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; -/// assert_eq!(extranonce_to_send.to_vec(), expected_extranonce_to_send); -/// ``` -/// -/// Using static prefix: -/// -/// ``` -/// use mining_sv2::*; -/// use core::convert::TryInto; -/// -/// // Create an extended extranonce with a static prefix -/// let static_prefix = vec![0x42, 0x43]; // Example static prefix -/// let mut pool_extended_extranonce = ExtendedExtranonce::new( -/// 0..0, -/// 0..7, -/// 7..32, -/// Some(static_prefix.clone()) -/// ).unwrap(); -/// -/// // When using static prefix, only bytes after the static prefix are incremented -/// let new_extended_channel_extranonce = pool_extended_extranonce.next_prefix_extended(3).unwrap(); -/// let expected_extranonce = vec![0x42, 0x43, 0, 0, 0, 0, 1]; -/// assert_eq!(new_extended_channel_extranonce.clone().to_vec(), expected_extranonce); -/// -/// // For standard channels, only range_2 is incremented while range_1 (including static prefix) is preserved -/// let new_standard_channel_extranonce = pool_extended_extranonce.next_prefix_standard().unwrap(); -/// // Note that the static prefix (0x42, 0x43) and the incremented bytes in range_1 are preserved -/// let expected_standard_extranonce = vec![0x42, 0x43, 0, 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, 1]; -/// assert_eq!(new_standard_channel_extranonce.to_vec(), expected_standard_extranonce); -/// -/// // Now the proxy receives the ExtendedExtranonce previously created -/// // The proxy knows the extranonce space reserved to the pool is 7 bytes and that the total -/// // extranonce len is 32 bytes and decides to reserve 4 bytes for itself and leave the remaining 21 for -/// // further downstreams. -/// let range_0 = 0..7; -/// let range_1 = 7..11; -/// let range_2 = 11..32; -/// let mut proxy_extended_extranonce = ExtendedExtranonce::from_upstream_extranonce( -/// new_extended_channel_extranonce, -/// range_0, -/// range_1, -/// range_2 -/// ).unwrap(); -/// -/// // The proxy generates an extended extranonce for downstream -/// let new_extended_channel_extranonce_prefix = proxy_extended_extranonce.next_prefix_extended(3).unwrap(); -/// let expected_extranonce_prefix = vec![0x42, 0x43, 0, 0, 0, 0, 1, 0, 0, 0, 1]; -/// assert_eq!(new_extended_channel_extranonce_prefix.clone().to_vec(), expected_extranonce_prefix); -/// -/// // When the proxy receives a share from downstream and wants to recreate the full extranonce -/// // e.g., because it wants to check the share's work -/// let received_extranonce: Extranonce = vec![0, 0, 0, 0, 0, 0, 0, 8, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(); -/// let share_complete_extranonce = proxy_extended_extranonce.extranonce_from_downstream_extranonce(received_extranonce.clone()).unwrap(); -/// let expected_complete_extranonce = vec![0x42, 0x43, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 8, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; -/// assert_eq!(share_complete_extranonce.to_vec(), expected_complete_extranonce); -/// -/// // Now the proxy wants to send the extranonce received from downstream and the part of extranonce -/// // owned by itself to the pool -/// let extranonce_to_send = proxy_extended_extranonce.without_upstream_part(Some(received_extranonce)).unwrap(); -/// let expected_extranonce_to_send = vec![0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 8, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; -pub struct ExtendedExtranonce { - inner: alloc::vec::Vec, - range_0: core::ops::Range, - range_1: core::ops::Range, - range_2: core::ops::Range, - static_prefix: Option>, -} - -/// Error type for ExtendedExtranonce operations -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ExtendedExtranonceError { - /// The range_2.end is greater than MAX_EXTRANONCE_LEN - ExceedsMaxLength, - /// The ranges are invalid (e.g. range_0.end != range_1.start) - InvalidRanges, - /// The downstream extranonce length doesn't match the expected length - InvalidDownstreamLength, - /// The extranonce bytes in range_1 are at maximum value and can't be incremented - MaxValueReached, - /// The static prefix length is invalid - InvalidStaticPrefixLength, -} - -/// the trait PartialEq is implemented in such a way that only the relevant bytes are compared. -/// If range_2.end is set to 20, then the following ExtendedExtranonces are equal -/// ExtendedExtranonce { -/// inner: [0000 0000 0000 0000 0000 0000 0000 0000], -/// range_0: [0..5], -/// range_1: [5..10], -/// range_2: [10, 20], -/// } -/// ExtendedExtranonce { -/// inner: [0000 0000 0000 0000 0000 1111 1111 1111], -/// range_0: [0..5], -/// range_1: [5..10], -/// range_2: [10, 20], -/// } -impl PartialEq for ExtendedExtranonce { - fn eq(&self, other: &Self) -> bool { - let len = self.range_2.end; - self.inner[0..len] == other.inner[0..len] - && self.range_0 == other.range_0 - && self.range_1 == other.range_1 - && self.range_2 == other.range_2 - } -} - -impl ExtendedExtranonce { - /// every extranonce start from zero. - pub fn new( - range_0: Range, - range_1: Range, - range_2: Range, - static_prefix: Option>, - ) -> Result { - // Validate ranges - if range_0.start != 0 - || range_0.end != range_1.start - || range_1.end != range_2.start - || range_1.end < range_1.start - || range_2.end < range_2.start - { - return Err(ExtendedExtranonceError::InvalidRanges); - } - - if let Some(static_prefix) = static_prefix.clone() { - if static_prefix.len() > core::cmp::min(2, range_1.end - range_1.start) { - return Err(ExtendedExtranonceError::InvalidStaticPrefixLength); - } - } - - // Check if range_2.end exceeds MAX_EXTRANONCE_LEN - if range_2.end > MAX_EXTRANONCE_LEN { - return Err(ExtendedExtranonceError::ExceedsMaxLength); - } - - let mut inner = vec![0; range_2.end]; - if let Some(static_prefix) = static_prefix.clone() { - inner[range_1.start..range_1.start + static_prefix.len()] - .copy_from_slice(&static_prefix); - } - - Ok(Self { - inner, - range_0, - range_1, - range_2, - static_prefix, - }) - } - - pub fn new_with_inner_only_test( - range_0: Range, - range_1: Range, - range_2: Range, - mut inner: alloc::vec::Vec, - ) -> Result { - // Validate ranges - if range_0.start != 0 - || range_0.end != range_1.start - || range_1.end != range_2.start - || range_1.end < range_1.start - || range_2.end < range_2.start - { - return Err(ExtendedExtranonceError::InvalidRanges); - } - - // Check if range_2.end exceeds MAX_EXTRANONCE_LEN - if range_2.end > MAX_EXTRANONCE_LEN { - return Err(ExtendedExtranonceError::ExceedsMaxLength); - } - - inner.resize(MAX_EXTRANONCE_LEN, 0); - Ok(Self { - inner, - range_0, - range_1, - range_2, - static_prefix: None, - }) - } - - pub fn get_len(&self) -> usize { - self.range_2.end - } - - pub fn get_range2_len(&self) -> usize { - self.range_2.end - self.range_2.start - } - - pub fn get_range0_len(&self) -> usize { - self.range_0.end - self.range_0.start - } - - pub fn get_prefix_len(&self) -> usize { - self.range_1.end - self.range_0.start - } - - /// Suppose that P receives from the upstream an extranonce that needs to be converted into any - /// ExtendedExtranonce, eg when an extended channel is opened. Then range_0 (that should - /// be provided along the Extranonce) is reserved for the upstream and can't be modiefied by - /// P. If the bytes recerved to P (range_1 and range_2) are not set to zero, returns None, - /// otherwise returns Some(ExtendedExtranonce). If the range_2.end field is greater than 32, - /// returns None. - pub fn from_upstream_extranonce( - v: Extranonce, - range_0: Range, - range_1: Range, - range_2: Range, - ) -> Result { - // Validate ranges - if range_0.start != 0 - || range_0.end != range_1.start - || range_1.end != range_2.start - || range_1.end < range_1.start - || range_2.end < range_2.start - { - return Err(ExtendedExtranonceError::InvalidRanges); - } - - // Check if range_2.end exceeds MAX_EXTRANONCE_LEN - if range_2.end > MAX_EXTRANONCE_LEN { - return Err(ExtendedExtranonceError::ExceedsMaxLength); - } - - let mut inner = v.extranonce; - inner.resize(range_2.end, 0); - let rest = vec![0; range_2.end - inner.len()]; - let inner = [inner, rest].concat(); - Ok(Self { - inner, - range_0, - range_1, - range_2, - static_prefix: None, - }) - } - - /// Specular of [Self::from_upstream_extranonce] - pub fn extranonce_from_downstream_extranonce( - &self, - dowstream_extranonce: Extranonce, - ) -> Result { - if dowstream_extranonce.extranonce.len() != self.range_2.end - self.range_2.start { - return Err(ExtendedExtranonceError::InvalidDownstreamLength); - } - let mut res = self.inner[self.range_0.start..self.range_1.end].to_vec(); - for b in dowstream_extranonce.extranonce { - res.push(b) - } - res.try_into() - .map_err(|_| ExtendedExtranonceError::ExceedsMaxLength) - } - - /// Calculates the next extranonce for standard channels. - pub fn next_prefix_standard(&mut self) -> Result { - let non_reserved_extranonces_bytes = &mut self.inner[self.range_2.start..self.range_2.end]; - match increment_bytes_be(non_reserved_extranonces_bytes) { - Ok(_) => Ok(self.into()), - Err(_) => Err(ExtendedExtranonceError::MaxValueReached), - } - } - - /// Calculates the next extranonce for extended channels. - /// The required_len variable represents the range requested by the downstream to use. - /// The part that is incremented is range_1, as every downstream must have different jobs. - pub fn next_prefix_extended( - &mut self, - required_len: usize, - ) -> Result { - if required_len > self.range_2.end - self.range_2.start { - return Err(ExtendedExtranonceError::InvalidDownstreamLength); - }; - - // Determine the start position for extended_part based on static_prefix - // If static_prefix is Some, some bytes are meant to be fixed and not - // incremented - let extended_part_start = - self.range_1.start + self.static_prefix.as_ref().map_or(0, |p| p.len()); - - let extended_part = &mut self.inner[extended_part_start..self.range_1.end]; - match increment_bytes_be(extended_part) { - Ok(_) => { - let result = self.inner[..self.range_1.end].to_vec(); - // Safe unwrap result will be always less the MAX_EXTRANONCE_LEN - result - .try_into() - .map_err(|_| ExtendedExtranonceError::ExceedsMaxLength) - } - Err(_) => Err(ExtendedExtranonceError::MaxValueReached), - } - } - - /// Return a vec with the extranonce bytes that belong to self and downstream removing the - /// ones owned by upstream (using Sv1 terms the extranonce1 is removed) - /// If dowstream_extranonce is Some(v) it replace the downstream extranonce part with v - pub fn without_upstream_part( - &self, - downstream_extranonce: Option, - ) -> Result { - match downstream_extranonce { - Some(downstream_extranonce) => { - if downstream_extranonce.extranonce.len() != self.range_2.end - self.range_2.start { - return Err(ExtendedExtranonceError::InvalidDownstreamLength); - } - let mut res = self.inner[self.range_1.start..self.range_1.end].to_vec(); - for b in downstream_extranonce.extranonce { - res.push(b) - } - res.try_into() - .map_err(|_| ExtendedExtranonceError::ExceedsMaxLength) - } - None => self.inner[self.range_1.start..self.range_2.end] - .to_vec() - .try_into() - .map_err(|_| ExtendedExtranonceError::ExceedsMaxLength), - } - } - - pub fn upstream_part(&self) -> Extranonce { - self.inner[self.range_0.start..self.range_1.end] - .to_vec() - .try_into() - .unwrap() - } -} -/// This function is used to increment extranonces, and it is used in next_standard and in -/// next_extended methods. If the input consists of an array of 255 as u8 (the maximum value) then -/// the input cannot be incremented. In this case, the input is not changed and the function returns -/// Err(()). In every other case, the function increments the input and returns Ok(()) -fn increment_bytes_be(bs: &mut [u8]) -> Result<(), ()> { - for b in bs.iter_mut().rev() { - if *b != u8::MAX { - *b += 1; - return Ok(()); - } else { - *b = 0; - } - } - for b in bs.iter_mut() { - *b = u8::MAX - } - Err(()) -} - -#[cfg(test)] -pub mod tests { - use super::*; - use alloc::vec::Vec; - use quickcheck_macros; - - #[test] - fn test_extranonce_errors() { - let extranonce = Extranonce::try_from(vec![0; MAX_EXTRANONCE_LEN + 1]); - assert!(extranonce.is_err()); - - assert!(Extranonce::new(MAX_EXTRANONCE_LEN + 1).is_none()); - } - - #[test] - fn test_from_upstream_extranonce_error() { - let range_0 = 0..0; - let range_1 = 0..0; - let range_2 = 0..MAX_EXTRANONCE_LEN + 1; - let extranonce = Extranonce::new(10).unwrap(); - - let extended_extranonce = - ExtendedExtranonce::from_upstream_extranonce(extranonce, range_0, range_1, range_2); - assert!(extended_extranonce.is_err()); - assert_eq!( - extended_extranonce.unwrap_err(), - ExtendedExtranonceError::ExceedsMaxLength - ); - } - - #[test] - fn test_invalid_ranges() { - // Test with range_0.start != 0 - let result = ExtendedExtranonce::new(1..2, 2..3, 3..10, None); - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), ExtendedExtranonceError::InvalidRanges); - - // Test with range_0.end != range_1.start - let result = ExtendedExtranonce::new(0..2, 3..4, 4..10, None); - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), ExtendedExtranonceError::InvalidRanges); - - // Test with range_1.end != range_2.start - let result = ExtendedExtranonce::new(0..2, 2..4, 5..10, None); - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), ExtendedExtranonceError::InvalidRanges); - } - - #[test] - fn test_extranonce_from_downstream_extranonce() { - let downstream_len = 10; - - let downstream_extranonce = Extranonce::new(downstream_len).unwrap(); - - let range_0 = 0..4; - let range_1 = 4..downstream_len; - let range_2 = downstream_len..(downstream_len * 2 + 1); - - let extended_extraonce = ExtendedExtranonce::new(range_0, range_1, range_2, None).unwrap(); - - let extranonce = - extended_extraonce.extranonce_from_downstream_extranonce(downstream_extranonce); - - assert!(extranonce.is_err()); - assert_eq!( - extranonce.unwrap_err(), - ExtendedExtranonceError::InvalidDownstreamLength - ); - - // Test with a valid downstream extranonce - let extra_content: Vec = vec![5; downstream_len]; - let downstream_extranonce = - Extranonce::from_vec_with_len(extra_content.clone(), downstream_len); - - let range_0 = 0..4; - let range_1 = 4..downstream_len; - let range_2 = downstream_len..(downstream_len * 2); - - let extended_extraonce = ExtendedExtranonce::new(range_0, range_1, range_2, None).unwrap(); - - let extranonce = - extended_extraonce.extranonce_from_downstream_extranonce(downstream_extranonce); - - assert!(extranonce.is_ok()); - - //validate that the extranonce is the concatenation of the upstream part and the downstream - // part - assert_eq!( - extra_content, - extranonce.unwrap().extranonce.to_vec()[downstream_len..downstream_len * 2] - ); - } - - // Test from_vec_with_len - #[test] - fn test_extranonce_from_vec_with_len() { - let extranonce = Extranonce::new(10).unwrap(); - let extranonce2 = Extranonce::from_vec_with_len(extranonce.extranonce, 22); - assert_eq!(extranonce2.extranonce.len(), 22); - } - - #[test] - fn test_extranonce_without_upstream_part() { - let downstream_len = 10; - - let downstream_extranonce = Extranonce::new(downstream_len).unwrap(); - - let range_0 = 0..4; - let range_1 = 4..downstream_len; - let range_2 = downstream_len..(downstream_len * 2 + 1); - - let extended_extraonce = ExtendedExtranonce::new(range_0, range_1, range_2, None).unwrap(); - - assert_eq!( - extended_extraonce.without_upstream_part(Some(downstream_extranonce.clone())), - Err(ExtendedExtranonceError::InvalidDownstreamLength) - ); - - let range_0 = 0..4; - let range_1 = 4..downstream_len; - let range_2 = downstream_len..(downstream_len * 2); - let upstream_extranonce = Extranonce::from_vec_with_len(vec![5; 14], downstream_len); - - let extended_extraonce = ExtendedExtranonce::from_upstream_extranonce( - upstream_extranonce.clone(), - range_0, - range_1.clone(), - range_2, - ) - .unwrap(); - - let extranonce = extended_extraonce - .without_upstream_part(Some(downstream_extranonce.clone())) - .unwrap(); - assert_eq!( - extranonce.extranonce[0..6], - upstream_extranonce.extranonce[0..6] - ); - assert_eq!(extranonce.extranonce[7..], vec![0; 9]); - } - - // This test checks the behaviour of the function increment_bytes_be for a the MAX value - // converted in be array of u8 - #[test] - fn test_incrment_bytes_be_max() { - let input = u8::MAX; - let mut input = input.to_be_bytes(); - let result = increment_bytes_be(&mut input[..]); - assert!(result == Err(())); - assert!(u8::from_be_bytes(input) == u8::MAX); - } - - // thest the function incrment_bytes_be for values different from MAX - #[quickcheck_macros::quickcheck] - fn test_increment_by_one(input: u8) -> bool { - let expected1 = match input { - u8::MAX => input, - _ => input + 1, - }; - let mut input = input.to_be_bytes(); - let _ = increment_bytes_be(&mut input[..]); - let incremented_by_1 = u8::from_be_bytes(input); - incremented_by_1 == expected1 - } - use core::convert::TryFrom; - - // check that the composition of the functions Extranonce to U256 and U256 to Extranonce is the - // identity function - #[quickcheck_macros::quickcheck] - fn test_extranonce_from_u256(mut input: Vec) -> bool { - input.resize(MAX_EXTRANONCE_LEN, 0); - - let extranonce_start = Extranonce::try_from(input.clone()).unwrap(); - let u256 = U256::<'static>::from(extranonce_start.clone()); - let extranonce_final = Extranonce::from(u256); - extranonce_start == extranonce_final - } - - // do the same of the above but with B032 type - #[quickcheck_macros::quickcheck] - fn test_extranonce_from_b032(mut input: Vec) -> bool { - input.resize(MAX_EXTRANONCE_LEN, 0); - let extranonce_start = Extranonce::try_from(input.clone()).unwrap(); - let b032 = B032::<'static>::from(extranonce_start.clone()); - let extranonce_final = Extranonce::from(b032); - extranonce_start == extranonce_final - } - - // this test checks the functions from_upstream_extranonce and from_extranonce. - #[quickcheck_macros::quickcheck] - fn test_extranonce_from_extended_extranonce(input: (u8, u8, Vec, usize)) -> bool { - let inner = from_arbitrary_vec_to_array(input.2.clone()); - let extranonce_len = input.3 % MAX_EXTRANONCE_LEN + 1; - let r0 = input.0 as usize; - let r1 = input.1 as usize; - let r0 = r0 % (extranonce_len + 1); - let r1 = r1 % (extranonce_len + 1); - let mut ranges = Vec::from([r0, r1]); - ranges.sort(); - let range_0 = 0..ranges[0]; - let range_1 = ranges[0]..ranges[1]; - let range_2 = ranges[1]..extranonce_len; - - let mut extended_extranonce_start = ExtendedExtranonce::new_with_inner_only_test( - range_0.clone(), - range_1.clone(), - range_2.clone(), - inner.to_vec(), - ) - .unwrap(); - - assert_eq!(extended_extranonce_start.get_len(), extranonce_len); - assert_eq!( - extended_extranonce_start.get_range2_len(), - extranonce_len - ranges[1] - ); - - let extranonce_result = extended_extranonce_start.next_prefix_extended(0); - - // todo: refactor this test to avoid skipping the test if next_extended fails - if extranonce_result.is_err() { - return true; // Skip test if next_extended fails - } - - let extranonce = extranonce_result.unwrap(); - - let extended_extranonce_final = ExtendedExtranonce::from_upstream_extranonce( - extranonce, - range_0.clone(), - range_1.clone(), - range_2.clone(), - ); - - match extended_extranonce_final { - Ok(extended_extranonce_final) => { - for b in extended_extranonce_final.inner[range_2.start..range_2.end].iter() { - if b != &0 { - return false; - } - } - extended_extranonce_final.inner[range_0.clone().start..range_1.end] - == extended_extranonce_start.inner[range_0.start..range_1.end] - } - Err(_) => { - // If from_upstream_extranonce fails, it should be because the inner bytes in - // range_1..range_2 are not zero - for b in inner[range_1.start..range_2.end].iter() { - if b != &0 { - return true; - } - } - false - } - } - } - - // test next_standard_method - #[quickcheck_macros::quickcheck] - fn test_next_standard_extranonce(input: (u8, u8, Vec, usize)) -> bool { - let inner = from_arbitrary_vec_to_array(input.2.clone()); - let extranonce_len = input.3 % MAX_EXTRANONCE_LEN + 1; - let r0 = input.0 as usize; - let r1 = input.1 as usize; - let r0 = r0 % (extranonce_len + 1); - let r1 = r1 % (extranonce_len + 1); - let mut ranges = Vec::from([r0, r1]); - ranges.sort(); - let range_0 = 0..ranges[0]; - let range_1 = ranges[0]..ranges[1]; - let range_2 = ranges[1]..extranonce_len; - - let extended_extranonce_start = ExtendedExtranonce::new_with_inner_only_test( - range_0.clone(), - range_1.clone(), - range_2.clone(), - inner.to_vec(), - ) - .unwrap(); - - let mut extranonce_copy: Extranonce = - Extranonce::from(&mut extended_extranonce_start.clone()); - let extranonce_expected_b032: Option = extranonce_copy.next(); - - match extended_extranonce_start.clone().next_prefix_standard() { - Ok(extranonce_next) => match extranonce_expected_b032 { - Some(b032) => - // the range_2 of extranonce_next must be equal to the range_2 of the - // conversion of extranonce_copy.next() converted in extranonce - { - extranonce_next.extranonce[range_2.start..range_2.end] == Extranonce::from(b032.clone()).extranonce[range_2.start..range_2.end] - // the range_1 of the conversion of extranonce_copy.next() converted in - // extranonce must remain unchanged - && Extranonce::from(b032.clone()).extranonce[range_1.start..range_1.end]== extended_extranonce_start.inner[range_1.start..range_1.end] - } - None => false, - }, - // if .next_standard() method falls in None case, this means that the range_2 is at - // maximum value, so every entry must be 255 as u8 - Err(ExtendedExtranonceError::MaxValueReached) => { - for b in inner[range_2.start..range_2.end].iter() { - if b != &255_u8 { - return false; - } - } - true - } - Err(_) => false, // Other errors are not expected in this test - } - } - - #[quickcheck_macros::quickcheck] - fn test_next_stndard2(input: (u8, u8, Vec, usize)) -> bool { - let inner = from_arbitrary_vec_to_array(input.2.clone()); - let extranonce_len = input.3 % MAX_EXTRANONCE_LEN + 1; - let r0 = input.0 as usize; - let r1 = input.1 as usize; - let r0 = r0 % (extranonce_len + 1); - let r1 = r1 % (extranonce_len + 1); - let mut ranges = Vec::from([r0, r1]); - ranges.sort(); - let range_0 = 0..ranges[0]; - let range_1 = ranges[0]..ranges[1]; - let range_2 = ranges[1]..extranonce_len; - - let mut extended_extranonce_start = ExtendedExtranonce::new_with_inner_only_test( - range_0.clone(), - range_1.clone(), - range_2.clone(), - inner.to_vec(), - ) - .unwrap(); - - match extended_extranonce_start.next_prefix_standard() { - Ok(v) => { - extended_extranonce_start.inner[range_2.clone()] == v.extranonce[range_2] - && extended_extranonce_start.inner[range_0.clone()] - == v.extranonce[range_0.clone()] - } - Err(_) => true, // Any error is acceptable for this test - } - } - - #[quickcheck_macros::quickcheck] - fn test_next_extended_extranonce(input: (u8, u8, Vec, usize, usize)) -> bool { - let inner = from_arbitrary_vec_to_array(input.2.clone()); - let extranonce_len = input.3 % MAX_EXTRANONCE_LEN + 1; - let r0 = input.0 as usize; - let r1 = input.1 as usize; - let r0 = r0 % (extranonce_len + 1); - let r1 = r1 % (extranonce_len + 1); - let required_len = input.4; - let mut ranges = Vec::from([r0, r1]); - ranges.sort(); - let range_0 = 0..ranges[0]; - let range_1 = ranges[0]..ranges[1]; - let range_2 = ranges[1]..extranonce_len; - - let mut extended_extranonce = ExtendedExtranonce::new_with_inner_only_test( - range_0.clone(), - range_1.clone(), - range_2.clone(), - inner.to_vec(), - ) - .unwrap(); - - match extended_extranonce.next_prefix_extended(required_len) { - Ok(extranonce) => extended_extranonce.inner[..range_1.end] == extranonce.extranonce[..], - Err(ExtendedExtranonceError::InvalidDownstreamLength) => { - required_len > range_2.end - range_2.start - } - Err(ExtendedExtranonceError::MaxValueReached) => { - let mut range_1_start = inner[range_1.clone()].to_vec(); - increment_bytes_be(&mut range_1_start).is_err() - } - Err(_) => false, // Other errors are not expected in this test - } - } - - #[quickcheck_macros::quickcheck] - fn test_vec_from_extranonce(input: Vec) -> bool { - let input_start = from_arbitrary_vec_to_array(input).to_vec(); - let extranonce_start = Extranonce::try_from(input_start.clone()).unwrap(); - let vec_final = Vec::from(extranonce_start.clone()); - input_start == vec_final - } - - use core::convert::TryInto; - pub fn from_arbitrary_vec_to_array(vec: Vec) -> [u8; 32] { - if vec.len() >= 32 { - vec[0..32].try_into().unwrap() - } else { - let mut result = Vec::new(); - for _ in 0..(32 - vec.len()) { - result.push(0); - } - for element in vec { - result.push(element) - } - result[..].try_into().unwrap() - } - } - - #[test] - fn test_extended_extranonce_get_prefix_len() { - let range_0 = 0..2; - let range_1 = 2..4; - let range_2 = 4..9; - let extended = ExtendedExtranonce::new(range_0, range_1, range_2, None).unwrap(); - let prefix_len = extended.get_prefix_len(); - assert!(prefix_len == 4); - } - - #[test] - fn test_extended_extranonce_with_static_prefix() { - let range_0 = 0..0; - let range_1 = 0..4; - let range_2 = 4..8; - let static_prefix = vec![0x42, 0x43]; // Some fixed data - - // Create an ExtendedExtranonce with static prefix - let extended = ExtendedExtranonce::new( - range_0.clone(), - range_1.clone(), - range_2.clone(), - Some(static_prefix.clone()), - ) - .unwrap(); - - // Verify the static prefix was stored - assert_eq!(extended.static_prefix, Some(static_prefix.clone())); - - // Verify the inner data contains the static prefix - assert_eq!( - extended.inner[range_1.start..range_1.start + static_prefix.len()], - static_prefix[..] - ); - } - - #[test] - fn test_extended_extranonce_invalid_static_prefix_length() { - let range_0 = 0..0; - let range_1 = 0..4; - let range_2 = 4..8; - let static_prefix = vec![0x42, 0x43, 0x44]; // Length > 2 not allowed - - // Create an ExtendedExtranonce with static prefix that's too long - let result = ExtendedExtranonce::new(range_0, range_1, range_2, Some(static_prefix)); - - // Verify the correct error is returned - assert!(result.is_err()); - assert_eq!( - result.unwrap_err(), - ExtendedExtranonceError::InvalidStaticPrefixLength - ); - } - - #[test] - fn test_next_extended_with_static_prefix() { - let range_0 = 0..0; - let range_1 = 0..4; - let range_2 = 4..8; - let static_prefix = vec![0x42, 0x43]; // Fixed data of length 2 - - // Create an ExtendedExtranonce with static prefix - let mut extended = ExtendedExtranonce::new( - range_0, - range_1.clone(), - range_2, - Some(static_prefix.clone()), - ) - .unwrap(); - - // Call next_extended - let result = extended.next_prefix_extended(3).unwrap(); - - // Verify the result contains the static prefix - assert_eq!(result.extranonce[0..static_prefix.len()], static_prefix[..]); - - // Call next_extended again - let result2 = extended.next_prefix_extended(3).unwrap(); - - // Verify the fixed part remains unchanged - assert_eq!( - result2.extranonce[0..static_prefix.len()], - static_prefix[..] - ); - - // Verify the incremented part has changed - assert_ne!(result.extranonce, result2.extranonce); - } - - #[test] - fn test_multiple_next_extended_with_static_prefix() { - let range_0 = 0..0; - let range_1 = 0..4; - let range_2 = 4..8; - let static_prefix = vec![0x42, 0x43]; // Fixed data of length 2 - - // Create an ExtendedExtranonce with static prefix - let mut extended = ExtendedExtranonce::new( - range_0, - range_1.clone(), - range_2, - Some(static_prefix.clone()), - ) - .unwrap(); - - // Generate multiple extranonces and verify they all have the same fixed part - let mut results = Vec::new(); - for _ in 0..5 { - let result = extended.next_prefix_extended(3).unwrap(); - results.push(result); - } - - // Verify all results have the same fixed part - for result in &results { - assert_eq!(result.extranonce[0..static_prefix.len()], static_prefix[..]); - } - - // Verify all results have different incremented parts - for i in 0..results.len() { - for j in i + 1..results.len() { - assert_ne!(results[i].extranonce, results[j].extranonce); - } - } - } -} diff --git a/sv2/subprotocols/mining/src/new_mining_job.rs b/sv2/subprotocols/mining/src/new_mining_job.rs index 9167be974e..6b084d2362 100644 --- a/sv2/subprotocols/mining/src/new_mining_job.rs +++ b/sv2/subprotocols/mining/src/new_mining_job.rs @@ -153,9 +153,17 @@ impl NewExtendedMiningJob<'_> { #[cfg(test)] mod tests { use super::*; - use crate::tests::from_arbitrary_vec_to_array; + use alloc::vec::Vec; use core::convert::TryFrom; + fn from_arbitrary_vec_to_array(vec: Vec) -> [u8; 32] { + let mut result = [0_u8; 32]; + let start = 32_usize.saturating_sub(vec.len()); + let copy_len = vec.len().min(32); + result[start..start + copy_len].copy_from_slice(&vec[..copy_len]); + result + } + #[quickcheck_macros::quickcheck] #[allow(clippy::too_many_arguments)] fn test_new_extended_mining_job( diff --git a/sv2/subprotocols/mining/src/open_channel.rs b/sv2/subprotocols/mining/src/open_channel.rs index 6b0046d139..a9a03d2e1f 100644 --- a/sv2/subprotocols/mining/src/open_channel.rs +++ b/sv2/subprotocols/mining/src/open_channel.rs @@ -269,10 +269,17 @@ impl OpenMiningChannelError<'_> { mod tests { use super::*; - use crate::tests::from_arbitrary_vec_to_array; use alloc::{string::String, vec::Vec}; use core::convert::TryFrom; + fn from_arbitrary_vec_to_array(vec: Vec) -> [u8; 32] { + let mut result = [0_u8; 32]; + let start = 32_usize.saturating_sub(vec.len()); + let copy_len = vec.len().min(32); + result[start..start + copy_len].copy_from_slice(&vec[..copy_len]); + result + } + // *** OPEN STANDARD MINING CHANNEL *** #[quickcheck_macros::quickcheck] fn test_open_standard_mining_channel_fns(