Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion stratum-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,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 = "^3.0.0" }
common_messages_sv2 = { path = "../sv2/subprotocols/common-messages", version = "^6.0.0" }
mining_sv2 = { path = "../sv2/subprotocols/mining", version = "^6.0.0" }
mining_sv2 = { path = "../sv2/subprotocols/mining", version = "^7.0.0" }
template_distribution_sv2 = { path = "../sv2/subprotocols/template-distribution", version = "^4.0.0" }
job_declaration_sv2 = { path = "../sv2/subprotocols/job-declaration", version = "^6.0.0" }
sv1_api = { path = "../sv1", version = "^2.1.0", optional = true }
Expand Down
2 changes: 1 addition & 1 deletion stratum-core/stratum-translation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ path = "src/lib.rs"
[dependencies]
bitcoin = { version = "0.32.5" }
binary_sv2 = { path = "../../sv2/binary-sv2", version = "^5.0.0" }
mining_sv2 = { path = "../../sv2/subprotocols/mining", version = "^6.0.0" }
mining_sv2 = { path = "../../sv2/subprotocols/mining", version = "^7.0.0" }
channels_sv2 = { path = "../../sv2/channels-sv2", version = "^3.0.0" }
v1 = { path = "../../sv1", package = "sv1_api", version = "^2.0.0" }
tracing = "0.1"
2 changes: 1 addition & 1 deletion sv2/channels-sv2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ keywords = ["stratum", "mining", "bitcoin", "protocol"]
[dependencies]
binary_sv2 = { path = "../binary-sv2", version = "^5.0.0" }
common_messages_sv2 = { path = "../subprotocols/common-messages", version = "^6.0.0" }
mining_sv2 = { path = "../subprotocols/mining", version = "^6.0.0" }
mining_sv2 = { path = "../subprotocols/mining", version = "^7.0.0" }
template_distribution_sv2 = { path = "../subprotocols/template-distribution", version = "^4.0.0" }
tracing = { version = "0.1"}
bitcoin = { version = "0.32.5" }
Expand Down
2 changes: 2 additions & 0 deletions sv2/channels-sv2/src/client/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,6 @@ pub enum StandardChannelError {
pub enum GroupChannelError {
/// The specified job ID was not found in the group channel.
JobIdNotFound,
/// The full extranonce size for the group channel does not match the full extranonce size for the channel.
FullExtranonceSizeMismatch,
}
4 changes: 4 additions & 0 deletions sv2/channels-sv2/src/client/extended.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ impl<'a> ExtendedChannel<'a> {

/// Handles a [`NewExtendedMiningJob`] message received from upstream.
///
/// The message could be either directed at this channel, or at a group channel it belongs to.
///
/// - If [`NewExtendedMiningJob::min_ntime`] is empty, the job is considered a future job and
/// added to the future jobs list (see [`get_future_jobs`](ExtendedChannel::get_future_jobs)).
/// - Otherwise, the job is activated and previous active job moves to the past jobs list.
Expand Down Expand Up @@ -400,6 +402,8 @@ impl<'a> ExtendedChannel<'a> {

/// Handles a [`SetNewPrevHash`](SetNewPrevHashMp) message from upstream.
///
/// The message could be either directed at this channel, or at a group channel it belongs to.
///
/// - If the referenced `job_id` is not a future job, returns an error.
/// - If it is a future job, activates it as the current job.
/// - Marks all past jobs as stale and clears them.
Expand Down
87 changes: 71 additions & 16 deletions sv2/channels-sv2/src/client/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
//!
//! This module provides the [`GroupChannel`] struct, which acts as a mining client's
//! abstraction over the state of a Sv2 group channel. It tracks group-level job state
//! and associated standard channels, but delegates share validation and job lifecycle
//! to standard channels.
//! and associated standard and extended channels, but delegates share validation and job lifecycle
//! to the channels themselves.

use super::{HashMap, HashSet};
use crate::client::error::GroupChannelError;
Expand All @@ -13,57 +13,87 @@ use mining_sv2::{NewExtendedMiningJob, SetNewPrevHash as SetNewPrevHashMp};
///
/// Tracks:
/// - the group channel's unique `group_channel_id`
/// - associated `standard_channel_ids` (indexed by `channel_id`)
/// - associated `channel_ids` (indexed by `channel_id`)
/// - future jobs (indexed by `job_id`, to be activated upon receipt of a
/// [`SetNewPrevHash`](SetNewPrevHashMp) message)
/// - active job
///
/// Does **not** track:
/// - past or stale jobs
/// - share validation state (handled per-standard channel)
/// - share validation state (handled per-channel)
#[derive(Debug, Clone)]
pub struct GroupChannel<'a> {
/// Unique identifier for the group channel
group_channel_id: u32,
/// Set of channel IDs associated with this group channel
standard_channel_ids: HashSet<u32>,
channel_ids: HashSet<u32>,
/// Future jobs, indexed by job_id, waiting to be activated
future_jobs: HashMap<u32, NewExtendedMiningJob<'a>>,
/// Currently active mining job for the group channel
active_job: Option<NewExtendedMiningJob<'a>>,
/// Full extranonce size for jobs associated with this group channel.
/// The constructor initializes this as None, but as new channels are added, we keep this updated.
/// At no point in time, two channels can belong to the same group while having different full extranonce sizes.
full_extranonce_size: Option<usize>,
}

impl<'a> GroupChannel<'a> {
/// Creates a new [`GroupChannel`] with the given group_channel_id.
pub fn new(group_channel_id: u32) -> Self {
Self {
group_channel_id,
standard_channel_ids: HashSet::new(),
channel_ids: HashSet::new(),
future_jobs: HashMap::new(),
active_job: None,
full_extranonce_size: None,
}
}

/// Adds a [`StandardChannel`](crate::client::standard::StandardChannel) to the group channel
/// by referencing its `channel_id`.
pub fn add_standard_channel_id(&mut self, standard_channel_id: u32) {
self.standard_channel_ids.insert(standard_channel_id);
/// Adds a channel to the group by its `channel_id` with the specified `full_extranonce_size`.
/// For extended channels, the `full_extranonce_size` is the sum of its `extranonce_prefix` size and its `rollable_extranonce_size`.
/// For standard channels, the `full_extranonce_size` is the size of its `extranonce_prefix`.
///
/// If this is the first channel ever added to the group, sets the group's `full_extranonce_size`.
/// If other channels already exist, validates that the `full_extranonce_size` matches.
///
/// Returns an error if the provided `full_extranonce_size` doesn't match the existing value.
pub fn add_channel_id(
&mut self,
channel_id: u32,
full_extranonce_size: usize,
) -> Result<(), GroupChannelError> {
self.channel_ids.insert(channel_id);

match self.full_extranonce_size {
// if the full extranonce size is already set, check if it matches the new full extranonce size
Some(existing_size) => {
if existing_size != full_extranonce_size {
return Err(GroupChannelError::FullExtranonceSizeMismatch);
}
}
// if the full extranonce size is not yet set, set it
None => {
self.full_extranonce_size = Some(full_extranonce_size);
}
}

Ok(())
}

/// Removes a [`StandardChannel`](crate::client::standard::StandardChannel) from the group
/// Removes a channel from the group channel
/// channel by its `channel_id`.
pub fn remove_standard_channel_id(&mut self, standard_channel_id: u32) {
self.standard_channel_ids.remove(&standard_channel_id);
pub fn remove_channel_id(&mut self, channel_id: u32) {
self.channel_ids.remove(&channel_id);
}

/// Returns the group channel ID.
pub fn get_group_channel_id(&self) -> u32 {
self.group_channel_id
}

/// Returns a reference to all standard channel IDs associated with this group channel.
pub fn get_standard_channel_ids(&self) -> &HashSet<u32> {
&self.standard_channel_ids
/// Returns a reference to all channel IDs associated with this group channel.
pub fn get_channel_ids(&self) -> &HashSet<u32> {
&self.channel_ids
}

/// Returns a reference to the current active job, if any.
Expand All @@ -76,6 +106,11 @@ impl<'a> GroupChannel<'a> {
&self.future_jobs
}

/// Returns the full extranonce size for jobs associated with this group channel.
pub fn get_full_extranonce_size(&self) -> Option<usize> {
self.full_extranonce_size
}

/// Handles a newly received [`NewExtendedMiningJob`] message from upstream.
///
/// - If `min_ntime` is present, sets this job as active.
Expand Down Expand Up @@ -117,3 +152,23 @@ impl<'a> GroupChannel<'a> {
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_add_channel_id() {
let mut group_channel = GroupChannel::new(1);
group_channel.add_channel_id(1, 10).unwrap();
assert_eq!(group_channel.get_full_extranonce_size(), Some(10));

// add a second channel with the same full extranonce size
group_channel.add_channel_id(2, 10).unwrap();
assert_eq!(group_channel.get_full_extranonce_size(), Some(10));

// add a third channel with a different full extranonce size
// this should return an error
assert!(group_channel.add_channel_id(3, 12).is_err());
}
}
2 changes: 2 additions & 0 deletions sv2/channels-sv2/src/server/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ pub enum ExtendedChannelError {
RequestedMinExtranonceSizeTooLarge,
ExtranoncePrefixTooLarge,
ScriptSigSizeTooLarge,
InvalidJobOrigin,
}

#[derive(Debug)]
pub enum GroupChannelError {
FullExtranonceSizeMismatch,
ChainTipNotSet,
TemplateIdNotFound,
JobFactoryError(JobFactoryError),
Expand Down
Loading
Loading