extern crate alloc;
use super::HashMap;
use crate::{
chain_tip::ChainTip,
client::{
error::StandardChannelError,
share_accounting::{ShareAccounting, ShareValidationError, ShareValidationResult},
},
merkle_root::merkle_root_from_path,
target::{bytes_to_hex, u256_to_block_hash},
MAX_EXTRANONCE_PREFIX_LEN,
};
use alloc::{format, string::String, vec::Vec};
use binary_sv2::{self, Sv2Option};
use bitcoin::{
blockdata::block::{Header, Version},
hashes::sha256d::Hash,
CompactTarget, Target,
};
use mining_sv2::{
NewExtendedMiningJob, NewMiningJob, SetNewPrevHash as SetNewPrevHashMp, SubmitSharesStandard,
};
use tracing::debug;
pub type StandardJob<'a> = (NewMiningJob<'a>, Target);
#[derive(Debug, Clone)]
pub struct StandardChannel<'a> {
channel_id: u32,
user_identity: String,
extranonce_prefix: Vec<u8>,
target: Target,
nominal_hashrate: f32,
future_jobs: HashMap<u32, StandardJob<'a>>,
active_job: Option<StandardJob<'a>>,
past_jobs: HashMap<u32, StandardJob<'a>>,
stale_jobs: HashMap<u32, StandardJob<'a>>,
share_accounting: ShareAccounting,
chain_tip: Option<ChainTip>,
}
impl<'a> StandardChannel<'a> {
pub fn new(
channel_id: u32,
user_identity: String,
extranonce_prefix: Vec<u8>,
target: Target,
nominal_hashrate: f32,
) -> Self {
Self {
channel_id,
user_identity,
extranonce_prefix,
target,
nominal_hashrate,
future_jobs: HashMap::new(),
active_job: None,
past_jobs: HashMap::new(),
stale_jobs: HashMap::new(),
share_accounting: ShareAccounting::new(),
chain_tip: None,
}
}
pub fn get_channel_id(&self) -> u32 {
self.channel_id
}
pub fn get_user_identity(&self) -> &String {
&self.user_identity
}
pub fn get_chain_tip(&self) -> Option<&ChainTip> {
self.chain_tip.as_ref()
}
pub fn set_chain_tip(&mut self, chain_tip: ChainTip) {
self.chain_tip = Some(chain_tip);
}
pub fn set_extranonce_prefix(
&mut self,
extranonce_prefix: Vec<u8>,
) -> Result<(), StandardChannelError> {
if extranonce_prefix.len() > MAX_EXTRANONCE_PREFIX_LEN {
return Err(StandardChannelError::NewExtranoncePrefixTooLarge);
}
self.extranonce_prefix = extranonce_prefix;
Ok(())
}
pub fn get_extranonce_prefix(&self) -> &Vec<u8> {
&self.extranonce_prefix
}
pub fn get_target(&self) -> &Target {
&self.target
}
pub fn set_target(&mut self, target: Target) {
self.target = target;
}
pub fn get_nominal_hashrate(&self) -> f32 {
self.nominal_hashrate
}
pub fn get_future_jobs(&self) -> &HashMap<u32, StandardJob<'a>> {
&self.future_jobs
}
pub fn get_active_job(&self) -> Option<&StandardJob<'a>> {
self.active_job.as_ref()
}
pub fn get_past_jobs(&self) -> &HashMap<u32, StandardJob<'a>> {
&self.past_jobs
}
pub fn get_stale_jobs(&self) -> &HashMap<u32, StandardJob<'a>> {
&self.stale_jobs
}
pub fn get_share_accounting(&self) -> &ShareAccounting {
&self.share_accounting
}
pub fn on_new_group_channel_job(&mut self, new_extended_mining_job: NewExtendedMiningJob<'a>) {
let merkle_root = merkle_root_from_path(
new_extended_mining_job.coinbase_tx_prefix.inner_as_ref(),
new_extended_mining_job.coinbase_tx_suffix.inner_as_ref(),
&self.extranonce_prefix,
&new_extended_mining_job.merkle_path.inner_as_ref(),
)
.expect("merkle root must be valid")
.try_into()
.expect("merkle root must be 32 bytes");
let new_mining_job = NewMiningJob {
channel_id: self.channel_id,
job_id: new_extended_mining_job.job_id,
merkle_root,
version: new_extended_mining_job.version,
min_ntime: new_extended_mining_job.min_ntime,
};
self.on_new_mining_job(new_mining_job);
}
pub fn on_new_mining_job(&mut self, new_mining_job: NewMiningJob<'a>) {
match new_mining_job.min_ntime.clone().into_inner() {
Some(_min_ntime) => {
if let Some(active_job) = self.active_job.as_ref() {
self.past_jobs
.insert(active_job.0.job_id, active_job.clone());
}
self.active_job = Some((new_mining_job, self.target));
}
None => {
self.future_jobs
.insert(new_mining_job.job_id, (new_mining_job, self.target));
}
}
}
pub fn on_set_new_prev_hash(
&mut self,
set_new_prev_hash: SetNewPrevHashMp<'a>,
) -> Result<(), StandardChannelError> {
match self.future_jobs.remove(&set_new_prev_hash.job_id) {
Some(mut activated_job) => {
activated_job.0.min_ntime = Sv2Option::new(Some(set_new_prev_hash.min_ntime));
self.active_job = Some(activated_job);
}
None => return Err(StandardChannelError::JobIdNotFound),
}
self.future_jobs.clear();
self.stale_jobs = self.past_jobs.clone();
self.past_jobs.clear();
self.share_accounting.flush_seen_shares();
self.chain_tip = Some(set_new_prev_hash.into());
Ok(())
}
pub fn validate_share(
&mut self,
share: SubmitSharesStandard,
) -> Result<ShareValidationResult, ShareValidationError> {
let job_id = share.job_id;
let is_active_job = self
.active_job
.as_ref()
.is_some_and(|job| job.0.job_id == job_id);
let is_past_job = self.past_jobs.contains_key(&job_id);
let is_stale_job = self.stale_jobs.contains_key(&job_id);
if is_stale_job {
return Err(ShareValidationError::Stale);
}
let job = if is_active_job {
self.active_job.as_ref().expect("active job must exist")
} else if is_past_job {
self.past_jobs.get(&job_id).expect("past job must exist")
} else {
return Err(ShareValidationError::InvalidJobId);
};
let merkle_root: [u8; 32] = job
.0
.merkle_root
.inner_as_ref()
.try_into()
.expect("merkle root must be 32 bytes");
let chain_tip = self
.chain_tip
.as_ref()
.ok_or(ShareValidationError::NoChainTip)?;
let prev_hash = chain_tip.prev_hash();
let nbits = CompactTarget::from_consensus(chain_tip.nbits());
let header = Header {
version: Version::from_consensus(share.version as i32),
prev_blockhash: u256_to_block_hash(prev_hash.clone()),
merkle_root: (*Hash::from_bytes_ref(&merkle_root)).into(),
time: share.ntime,
bits: nbits,
nonce: share.nonce,
};
let share_hash = header.block_hash();
let raw_share_hash: [u8; 32] = *share_hash.to_raw_hash().as_ref();
let share_hash_target = Target::from_le_bytes(raw_share_hash);
let share_hash_as_diff = share_hash_target.difficulty_float();
let network_target = Target::from_compact(nbits);
let job_target = job.1;
let share_hash_target_bytes = share_hash_target.to_be_bytes();
let job_target_bytes = job_target.to_be_bytes();
debug!(
"share validation \nshare:\t\t{}\njob target:\t{}\nnetwork target:\t{}",
bytes_to_hex(&share_hash_target_bytes),
bytes_to_hex(&job_target_bytes),
format!("{:x}", network_target)
);
if network_target.is_met_by(share_hash) {
self.share_accounting.update_share_accounting(
job_target.difficulty_float(),
share.sequence_number,
share_hash.to_raw_hash(),
);
return Ok(ShareValidationResult::BlockFound(share_hash.to_raw_hash()));
}
if share_hash_target < job_target {
if self
.share_accounting
.is_share_seen(share_hash.to_raw_hash())
{
return Err(ShareValidationError::DuplicateShare);
}
self.share_accounting.update_share_accounting(
job_target.difficulty_float(),
share.sequence_number,
share_hash.to_raw_hash(),
);
self.share_accounting.update_best_diff(share_hash_as_diff);
return Ok(ShareValidationResult::Valid(share_hash.to_raw_hash()));
}
Err(ShareValidationError::DoesNotMeetTarget)
}
}
#[cfg(test)]
mod tests {
use crate::client::{
share_accounting::{ShareValidationError, ShareValidationResult},
standard::StandardChannel,
};
use binary_sv2::Sv2Option;
use bitcoin::Target;
use mining_sv2::{NewMiningJob, SetNewPrevHash as SetNewPrevHashMp, SubmitSharesStandard};
#[test]
fn test_future_job_activation_flow() {
let channel_id = 1;
let user_identity = "user_identity".to_string();
let extranonce_prefix = [
83, 116, 114, 97, 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 0,
0, 0, 0, 0, 0, 0, 1,
]
.to_vec();
let target = Target::from_le_bytes([0xff; 32]);
let nominal_hashrate = 1.0;
let mut channel = StandardChannel::new(
channel_id,
user_identity,
extranonce_prefix,
target,
nominal_hashrate,
);
let future_job = NewMiningJob {
channel_id,
job_id: 1,
merkle_root: [
189, 200, 25, 246, 119, 73, 34, 42, 209, 112, 237, 50, 169, 71, 163, 192, 24, 84,
56, 86, 147, 71, 243, 44, 18, 107, 167, 169, 169, 66, 186, 98,
]
.into(),
version: 536870912,
min_ntime: Sv2Option::new(None),
};
channel.on_new_mining_job(future_job.clone());
assert_eq!(channel.get_future_jobs().len(), 1);
assert_eq!(channel.get_active_job(), None);
assert_eq!(channel.get_past_jobs().len(), 0);
let ntime: u32 = 1746839905;
let set_new_prev_hash = SetNewPrevHashMp {
channel_id,
job_id: future_job.job_id,
prev_hash: [
200, 53, 253, 129, 214, 31, 43, 84, 179, 58, 58, 76, 128, 213, 24, 53, 38, 144,
205, 88, 172, 20, 251, 22, 217, 141, 21, 221, 21, 0, 0, 0,
]
.into(),
nbits: 503543726,
min_ntime: ntime,
};
channel.on_set_new_prev_hash(set_new_prev_hash).unwrap();
assert!(channel.get_future_jobs().is_empty());
let mut previously_future_job = future_job.clone();
previously_future_job.min_ntime = Sv2Option::new(Some(ntime));
assert_eq!(
channel.get_active_job(),
Some(&(previously_future_job, channel.get_target().clone()))
);
}
#[test]
fn test_past_jobs_flow() {
let channel_id = 1;
let user_identity = "user_identity".to_string();
let extranonce_prefix = [
83, 116, 114, 97, 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 0,
0, 0, 0, 0, 0, 0, 1,
]
.to_vec();
let target = Target::from_le_bytes([0xff; 32]);
let nominal_hashrate = 1.0;
let mut channel = StandardChannel::new(
channel_id,
user_identity,
extranonce_prefix,
target,
nominal_hashrate,
);
let ntime: u32 = 1746839905;
let active_job = NewMiningJob {
channel_id,
job_id: 1,
merkle_root: [
189, 200, 25, 246, 119, 73, 34, 42, 209, 112, 237, 50, 169, 71, 163, 192, 24, 84,
56, 86, 147, 71, 243, 44, 18, 107, 167, 169, 169, 66, 186, 98,
]
.into(),
version: 536870912,
min_ntime: Sv2Option::new(Some(ntime)),
};
channel.on_new_mining_job(active_job.clone());
assert_eq!(channel.get_future_jobs().len(), 0);
assert_eq!(
channel.get_active_job(),
Some(&(active_job.clone(), channel.get_target().clone()))
);
assert_eq!(channel.get_past_jobs().len(), 0);
let mut new_active_job = active_job.clone();
new_active_job.job_id = 2;
channel.on_new_mining_job(new_active_job.clone());
assert_eq!(channel.get_future_jobs().len(), 0);
assert_eq!(
channel.get_active_job(),
Some(&(new_active_job, channel.get_target().clone()))
);
assert_eq!(channel.get_past_jobs().len(), 1);
}
#[test]
fn test_share_validation_block_found() {
let channel_id = 1;
let user_identity = "user_identity".to_string();
let extranonce_prefix = [
83, 116, 114, 97, 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 0,
0, 0, 0, 0, 0, 0, 1,
]
.to_vec();
let target = Target::from_le_bytes([0xff; 32]);
let nominal_hashrate = 1.0;
let mut channel = StandardChannel::new(
channel_id,
user_identity,
extranonce_prefix,
target,
nominal_hashrate,
);
let future_job = NewMiningJob {
channel_id,
job_id: 1,
merkle_root: [
189, 200, 25, 246, 119, 73, 34, 42, 209, 112, 237, 50, 169, 71, 163, 192, 24, 84,
56, 86, 147, 71, 243, 44, 18, 107, 167, 169, 169, 66, 186, 98,
]
.into(),
version: 536870912,
min_ntime: Sv2Option::new(None),
};
channel.on_new_mining_job(future_job.clone());
let nbits = 545259519;
let prev_hash = [
200, 53, 253, 129, 214, 31, 43, 84, 179, 58, 58, 76, 128, 213, 24, 53, 38, 144, 205,
88, 172, 20, 251, 22, 217, 141, 21, 221, 21, 0, 0, 0,
];
let ntime: u32 = 1746839905;
let set_new_prev_hash = SetNewPrevHashMp {
channel_id,
job_id: future_job.job_id,
prev_hash: prev_hash.into(),
nbits,
min_ntime: ntime,
};
channel.on_set_new_prev_hash(set_new_prev_hash).unwrap();
let share_valid_block = SubmitSharesStandard {
channel_id,
sequence_number: 0,
job_id: future_job.job_id,
nonce: 3,
ntime: 1745596932,
version: 536870912,
};
let res = channel.validate_share(share_valid_block);
assert!(matches!(res, Ok(ShareValidationResult::BlockFound(_))));
}
#[test]
fn test_share_validation_does_not_meet_target() {
let channel_id = 1;
let user_identity = "user_identity".to_string();
let extranonce_prefix = [
83, 116, 114, 97, 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 0,
0, 0, 0, 0, 0, 0, 1,
]
.to_vec();
let target = Target::from_le_bytes([
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, 0x00, 0x00,
]);
let nominal_hashrate = 1.0;
let mut channel = StandardChannel::new(
channel_id,
user_identity,
extranonce_prefix,
target,
nominal_hashrate,
);
let future_job = NewMiningJob {
channel_id,
job_id: 1,
merkle_root: [
189, 200, 25, 246, 119, 73, 34, 42, 209, 112, 237, 50, 169, 71, 163, 192, 24, 84,
56, 86, 147, 71, 243, 44, 18, 107, 167, 169, 169, 66, 186, 98,
]
.into(),
version: 536870912,
min_ntime: Sv2Option::new(None),
};
channel.on_new_mining_job(future_job.clone());
let nbits = 453040064;
let prev_hash = [
200, 53, 253, 129, 214, 31, 43, 84, 179, 58, 58, 76, 128, 213, 24, 53, 38, 144, 205,
88, 172, 20, 251, 22, 217, 141, 21, 221, 21, 0, 0, 0,
];
let ntime: u32 = 1746839905;
let set_new_prev_hash = SetNewPrevHashMp {
channel_id,
job_id: future_job.job_id,
prev_hash: prev_hash.into(),
nbits,
min_ntime: ntime,
};
channel.on_set_new_prev_hash(set_new_prev_hash).unwrap();
let share_low_diff = SubmitSharesStandard {
channel_id,
sequence_number: 0,
job_id: future_job.job_id,
nonce: 3,
ntime: 1745596932,
version: 536870912,
};
let res = channel.validate_share(share_low_diff);
assert!(matches!(
res.unwrap_err(),
ShareValidationError::DoesNotMeetTarget
));
}
#[test]
fn test_share_validation_valid_share() {
let channel_id = 1;
let user_identity = "user_identity".to_string();
let extranonce_prefix = [
83, 116, 114, 97, 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 0,
0, 0, 0, 0, 0, 0, 1,
]
.to_vec();
let target = Target::from_le_bytes([
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, 0x00, 0x00,
]);
let nominal_hashrate = 1.0;
let mut channel = StandardChannel::new(
channel_id,
user_identity,
extranonce_prefix,
target,
nominal_hashrate,
);
let future_job = NewMiningJob {
channel_id,
job_id: 1,
merkle_root: [
189, 200, 25, 246, 119, 73, 34, 42, 209, 112, 237, 50, 169, 71, 163, 192, 24, 84,
56, 86, 147, 71, 243, 44, 18, 107, 167, 169, 169, 66, 186, 98,
]
.into(),
version: 536870912,
min_ntime: Sv2Option::new(None),
};
channel.on_new_mining_job(future_job.clone());
let nbits = 453040064;
let prev_hash = [
200, 53, 253, 129, 214, 31, 43, 84, 179, 58, 58, 76, 128, 213, 24, 53, 38, 144, 205,
88, 172, 20, 251, 22, 217, 141, 21, 221, 21, 0, 0, 0,
];
let ntime: u32 = 1746839905;
let set_new_prev_hash = SetNewPrevHashMp {
channel_id,
job_id: future_job.job_id,
prev_hash: prev_hash.into(),
nbits,
min_ntime: ntime,
};
channel.on_set_new_prev_hash(set_new_prev_hash).unwrap();
let valid_share = SubmitSharesStandard {
channel_id,
sequence_number: 0,
job_id: future_job.job_id,
nonce: 244405,
ntime: 1745596932,
version: 536870912,
};
let res = channel.validate_share(valid_share);
assert!(matches!(res, Ok(ShareValidationResult::Valid(_))));
}
}