extern crate alloc;
use super::HashMap;
use crate::{
bip141::try_strip_bip141,
chain_tip::ChainTip,
client::{
error::ExtendedChannelError,
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::Vec};
use binary_sv2::{self, Sv2Option};
use bitcoin::{
absolute::LockTime,
blockdata::block::{Header, Version as BlockVersion},
consensus::{serialize, Decodable},
hashes::sha256d::Hash,
transaction::Version,
CompactTarget, OutPoint, Sequence, Target, Transaction, TxIn, TxOut, Witness,
};
use mining_sv2::{
NewExtendedMiningJob, SetCustomMiningJob, SetCustomMiningJobSuccess,
SetNewPrevHash as SetNewPrevHashMp, SubmitSharesExtended,
};
use tracing::debug;
pub type ExtendedJob<'a> = (NewExtendedMiningJob<'a>, Vec<u8>, Target);
#[derive(Clone, Debug)]
pub struct ExtendedChannel<'a> {
channel_id: u32,
user_identity: String,
extranonce_prefix: Vec<u8>,
rollable_extranonce_size: u16,
target: Target,
nominal_hashrate: f32,
version_rolling: bool,
future_jobs: HashMap<u32, ExtendedJob<'a>>,
active_job: Option<ExtendedJob<'a>>,
past_jobs: HashMap<u32, ExtendedJob<'a>>,
stale_jobs: HashMap<u32, ExtendedJob<'a>>,
share_accounting: ShareAccounting,
chain_tip: Option<ChainTip>,
}
impl<'a> ExtendedChannel<'a> {
pub fn new(
channel_id: u32,
user_identity: String,
extranonce_prefix: Vec<u8>,
target: Target,
nominal_hashrate: f32,
version_rolling: bool,
rollable_extranonce_size: u16,
) -> Self {
Self {
channel_id,
user_identity,
extranonce_prefix,
rollable_extranonce_size,
target,
nominal_hashrate,
version_rolling,
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_extranonce_prefix(&self) -> &Vec<u8> {
&self.extranonce_prefix
}
pub fn is_version_rolling(&self) -> bool {
self.version_rolling
}
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,
new_extranonce_prefix: Vec<u8>,
) -> Result<(), ExtendedChannelError> {
if new_extranonce_prefix.len() > MAX_EXTRANONCE_PREFIX_LEN {
return Err(ExtendedChannelError::NewExtranoncePrefixTooLarge);
}
self.extranonce_prefix = new_extranonce_prefix;
Ok(())
}
pub fn get_full_extranonce_size(&self) -> usize {
self.extranonce_prefix.len() + self.rollable_extranonce_size as usize
}
pub fn get_rollable_extranonce_size(&self) -> u16 {
self.rollable_extranonce_size
}
pub fn get_target(&self) -> &Target {
&self.target
}
pub fn set_target(&mut self, new_target: Target) {
self.target = new_target;
}
pub fn get_nominal_hashrate(&self) -> f32 {
self.nominal_hashrate
}
pub fn get_active_job(&self) -> Option<&ExtendedJob<'a>> {
self.active_job.as_ref()
}
pub fn get_future_jobs(&self) -> &HashMap<u32, ExtendedJob<'a>> {
&self.future_jobs
}
pub fn get_past_jobs(&self) -> &HashMap<u32, ExtendedJob<'a>> {
&self.past_jobs
}
pub fn get_stale_jobs(&self) -> &HashMap<u32, ExtendedJob<'a>> {
&self.stale_jobs
}
pub fn get_share_accounting(&self) -> &ShareAccounting {
&self.share_accounting
}
pub fn on_new_extended_mining_job(
&mut self,
mut new_extended_mining_job: NewExtendedMiningJob<'a>,
) -> Result<(), ExtendedChannelError> {
let new_extended_mining_job = match try_strip_bip141(
new_extended_mining_job.coinbase_tx_prefix.inner_as_ref(),
new_extended_mining_job.coinbase_tx_suffix.inner_as_ref(),
)
.map_err(ExtendedChannelError::FailedToTryToStripBip141)?
{
Some((coinbase_tx_prefix_stripped_bip141, coinbase_tx_suffix_stripped_bip141)) => {
new_extended_mining_job.coinbase_tx_prefix = coinbase_tx_prefix_stripped_bip141
.try_into()
.map_err(|_| ExtendedChannelError::FailedToSerializeToB064K)?;
new_extended_mining_job.coinbase_tx_suffix = coinbase_tx_suffix_stripped_bip141
.try_into()
.map_err(|_| ExtendedChannelError::FailedToSerializeToB064K)?;
new_extended_mining_job
}
None => new_extended_mining_job,
};
match new_extended_mining_job.min_ntime.clone().into_inner() {
Some(_min_ntime) => {
if let Some(active_job) = self.active_job.clone() {
self.past_jobs.insert(active_job.0.job_id, active_job);
}
self.active_job = Some((
new_extended_mining_job,
self.extranonce_prefix.clone(),
self.target,
));
}
None => {
self.future_jobs.insert(
new_extended_mining_job.job_id,
(
new_extended_mining_job,
self.extranonce_prefix.clone(),
self.target,
),
);
}
}
Ok(())
}
pub fn on_set_custom_mining_job_success(
&mut self,
set_custom_mining_job: SetCustomMiningJob<'a>,
set_custom_mining_job_success: SetCustomMiningJobSuccess,
) -> Result<(), ExtendedChannelError> {
if set_custom_mining_job.channel_id != set_custom_mining_job_success.channel_id
|| set_custom_mining_job.channel_id != self.channel_id
{
return Err(ExtendedChannelError::ChannelIdMismatch);
}
if set_custom_mining_job.request_id != set_custom_mining_job_success.request_id {
return Err(ExtendedChannelError::RequestIdMismatch);
}
let Some(chain_tip) = self.chain_tip.clone() else {
return Err(ExtendedChannelError::NoChainTip);
};
if set_custom_mining_job.min_ntime != chain_tip.min_ntime()
|| set_custom_mining_job.prev_hash != chain_tip.prev_hash()
|| set_custom_mining_job.nbits != chain_tip.nbits()
{
return Err(ExtendedChannelError::ChainTipMismatch);
}
let deserialized_outputs = Vec::<TxOut>::consensus_decode(
&mut set_custom_mining_job
.coinbase_tx_outputs
.inner_as_ref()
.to_vec()
.as_slice(),
)
.map_err(|_| ExtendedChannelError::FailedToDeserializeCoinbaseOutputs)?;
let mut script_sig = vec![];
script_sig.extend_from_slice(set_custom_mining_job.coinbase_prefix.inner_as_ref());
let full_extranonce_size = self.get_full_extranonce_size();
let full_extranonce = vec![0; full_extranonce_size];
script_sig.extend_from_slice(&full_extranonce);
let tx_in = TxIn {
previous_output: OutPoint::null(),
script_sig: script_sig.into(),
sequence: Sequence(set_custom_mining_job.coinbase_tx_input_n_sequence),
witness: Witness::from(vec![vec![0; 32]]),
};
let coinbase = Transaction {
version: Version::non_standard(set_custom_mining_job.coinbase_tx_version as i32),
lock_time: LockTime::from_consensus(set_custom_mining_job.coinbase_tx_locktime),
input: vec![tx_in],
output: deserialized_outputs,
};
let serialized_coinbase = serialize(&coinbase);
let prefix_index = 4 + 2 + 1 + 32 + 4 + 1 + set_custom_mining_job.coinbase_prefix.inner_as_ref().len();
let coinbase_tx_prefix = serialized_coinbase[0..prefix_index].to_vec();
let suffix_index = prefix_index + full_extranonce_size;
let coinbase_tx_suffix = serialized_coinbase[suffix_index..].to_vec();
let (coinbase_tx_prefix_stripped_bip141, coinbase_tx_suffix_stripped_bip141) =
try_strip_bip141(&coinbase_tx_prefix, &coinbase_tx_suffix)
.map_err(ExtendedChannelError::FailedToTryToStripBip141)?
.ok_or(ExtendedChannelError::FailedToStripBip141)?;
let new_extended_mining_job = NewExtendedMiningJob {
channel_id: set_custom_mining_job.channel_id,
job_id: set_custom_mining_job_success.job_id,
min_ntime: Sv2Option::new(Some(set_custom_mining_job.min_ntime)),
version: set_custom_mining_job.version,
version_rolling_allowed: self.version_rolling,
coinbase_tx_prefix: coinbase_tx_prefix_stripped_bip141
.try_into()
.map_err(|_| ExtendedChannelError::FailedToSerializeToB064K)?,
coinbase_tx_suffix: coinbase_tx_suffix_stripped_bip141
.try_into()
.map_err(|_| ExtendedChannelError::FailedToSerializeToB064K)?,
merkle_path: set_custom_mining_job.merkle_path,
};
if let Some(active_job) = self.active_job.clone() {
self.past_jobs.insert(active_job.0.job_id, active_job);
}
self.active_job = Some((
new_extended_mining_job,
self.extranonce_prefix.clone(),
self.target,
));
Ok(())
}
pub fn on_chain_tip_update(&mut self, chain_tip: ChainTip) -> Result<(), ExtendedChannelError> {
self.chain_tip = Some(chain_tip);
self.future_jobs.clear();
self.stale_jobs = self.past_jobs.clone();
self.past_jobs.clear();
self.share_accounting.flush_seen_shares();
Ok(())
}
pub fn on_set_new_prev_hash(
&mut self,
set_new_prev_hash: SetNewPrevHashMp<'a>,
) -> Result<(), ExtendedChannelError> {
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(ExtendedChannelError::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: SubmitSharesExtended,
) -> 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 extranonce_size = share.extranonce.inner_as_ref().len();
if extranonce_size != self.rollable_extranonce_size as usize {
return Err(ShareValidationError::BadExtranonceSize);
}
let mut full_extranonce = vec![];
full_extranonce.extend_from_slice(job.1.as_slice());
full_extranonce.extend_from_slice(share.extranonce.inner_as_ref());
let merkle_root: [u8; 32] = merkle_root_from_path(
job.0.coinbase_tx_prefix.inner_as_ref(),
job.0.coinbase_tx_suffix.inner_as_ref(),
full_extranonce.as_ref(),
&job.0.merkle_path.inner_as_ref(),
)
.ok_or(ShareValidationError::Invalid)?
.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 = CompactTarget::from_consensus(chain_tip.nbits());
if !job.0.version_rolling_allowed {
if (share.version & 0x1fffe000) != 0 {
return Err(ShareValidationError::VersionRollingNotAllowed);
}
}
let header = Header {
version: BlockVersion::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.2;
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::{
extended::ExtendedChannel,
share_accounting::{ShareValidationError, ShareValidationResult},
};
use binary_sv2::Sv2Option;
use bitcoin::Target;
use mining_sv2::{
NewExtendedMiningJob, SetNewPrevHash as SetNewPrevHashMp, SubmitSharesExtended,
};
use std::convert::TryInto;
#[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 version_rolling = true;
let rollable_extranonce_size = 4u16;
let mut channel = ExtendedChannel::new(
channel_id,
user_identity,
extranonce_prefix.clone(),
target,
nominal_hashrate,
version_rolling,
rollable_extranonce_size,
);
let future_job = NewExtendedMiningJob {
channel_id: 1,
job_id: 1,
min_ntime: Sv2Option::new(None),
version: 536870912,
version_rolling_allowed: true,
coinbase_tx_prefix: vec![
2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 34, 82, 0,
]
.try_into()
.unwrap(),
coinbase_tx_suffix: vec![
255, 255, 255, 255, 2, 0, 242, 5, 42, 1, 0, 0, 0, 22, 0, 20, 235, 225, 183, 220,
194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0,
0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222,
253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139,
235, 216, 54, 151, 78, 140, 249, 0, 0, 0, 0,
]
.try_into()
.unwrap(),
merkle_path: vec![].try_into().unwrap(),
};
channel
.on_new_extended_mining_job(future_job.clone())
.unwrap();
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,
extranonce_prefix,
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 version_rolling = true;
let rollable_extranonce_size = 4u16;
let mut channel = ExtendedChannel::new(
channel_id,
user_identity,
extranonce_prefix.clone(),
target,
nominal_hashrate,
version_rolling,
rollable_extranonce_size,
);
let ntime: u32 = 1746839905;
let active_job = NewExtendedMiningJob {
channel_id: 1,
job_id: 1,
min_ntime: Sv2Option::new(Some(ntime)),
version: 536870912,
version_rolling_allowed: true,
coinbase_tx_prefix: vec![
2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 34, 82, 0,
]
.try_into()
.unwrap(),
coinbase_tx_suffix: vec![
255, 255, 255, 255, 2, 0, 242, 5, 42, 1, 0, 0, 0, 22, 0, 20, 235, 225, 183, 220,
194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0,
0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222,
253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139,
235, 216, 54, 151, 78, 140, 249, 0, 0, 0, 0,
]
.try_into()
.unwrap(),
merkle_path: vec![].try_into().unwrap(),
};
channel
.on_new_extended_mining_job(active_job.clone())
.unwrap();
assert_eq!(channel.get_future_jobs().len(), 0);
assert_eq!(
channel.get_active_job(),
Some(&(
active_job.clone(),
extranonce_prefix.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_extended_mining_job(new_active_job.clone())
.unwrap();
assert_eq!(channel.get_future_jobs().len(), 0);
assert_eq!(
channel.get_active_job(),
Some(&(
new_active_job,
extranonce_prefix,
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 = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
]
.to_vec();
let target = Target::from_le_bytes([0xff; 32]);
let nominal_hashrate = 1.0;
let version_rolling = true;
let rollable_extranonce_size = 8u16;
let mut channel = ExtendedChannel::new(
channel_id,
user_identity,
extranonce_prefix.clone(),
target,
nominal_hashrate,
version_rolling,
rollable_extranonce_size,
);
let future_job = NewExtendedMiningJob {
channel_id: 1,
job_id: 1,
min_ntime: Sv2Option::new(None),
version: 536870912,
version_rolling_allowed: true,
coinbase_tx_prefix: vec![
2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 34, 82, 0,
]
.try_into()
.unwrap(),
coinbase_tx_suffix: vec![
255, 255, 255, 255, 2, 0, 242, 5, 42, 1, 0, 0, 0, 22, 0, 20, 235, 225, 183, 220,
194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0,
0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222,
253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139,
235, 216, 54, 151, 78, 140, 249, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
]
.try_into()
.unwrap(),
merkle_path: vec![].try_into().unwrap(),
};
channel
.on_new_extended_mining_job(future_job.clone())
.unwrap();
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 = SubmitSharesExtended {
channel_id,
sequence_number: 0,
job_id: 1,
nonce: 741057,
ntime: 1745596971,
version: 536870912,
extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(),
};
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 = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
]
.to_vec();
let target = Target::from_be_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 version_rolling = true;
let rollable_extranonce_size = 8u16;
let mut channel = ExtendedChannel::new(
channel_id,
user_identity,
extranonce_prefix.clone(),
target,
nominal_hashrate,
version_rolling,
rollable_extranonce_size,
);
let future_job = NewExtendedMiningJob {
channel_id: 1,
job_id: 1,
min_ntime: Sv2Option::new(None),
version: 536870912,
version_rolling_allowed: true,
coinbase_tx_prefix: vec![
2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 34, 82, 0,
]
.try_into()
.unwrap(),
coinbase_tx_suffix: vec![
255, 255, 255, 255, 2, 0, 242, 5, 42, 1, 0, 0, 0, 22, 0, 20, 235, 225, 183, 220,
194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0,
0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222,
253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139,
235, 216, 54, 151, 78, 140, 249, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
]
.try_into()
.unwrap(),
merkle_path: vec![].try_into().unwrap(),
};
channel
.on_new_extended_mining_job(future_job.clone())
.unwrap();
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 = SubmitSharesExtended {
channel_id,
sequence_number: 0,
job_id: 1,
nonce: 741057,
ntime: 1745596971,
version: 536870912,
extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(),
};
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 = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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 version_rolling = true;
let rollable_extranonce_size = 8u16;
let mut channel = ExtendedChannel::new(
channel_id,
user_identity,
extranonce_prefix.clone(),
target,
nominal_hashrate,
version_rolling,
rollable_extranonce_size,
);
let future_job = NewExtendedMiningJob {
channel_id: 1,
job_id: 1,
min_ntime: Sv2Option::new(None),
version: 536870912,
version_rolling_allowed: true,
coinbase_tx_prefix: vec![
2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 34, 82, 0,
]
.try_into()
.unwrap(),
coinbase_tx_suffix: vec![
255, 255, 255, 255, 2, 0, 242, 5, 42, 1, 0, 0, 0, 22, 0, 20, 235, 225, 183, 220,
194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0,
0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222,
253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139,
235, 216, 54, 151, 78, 140, 249, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
]
.try_into()
.unwrap(),
merkle_path: vec![].try_into().unwrap(),
};
channel
.on_new_extended_mining_job(future_job.clone())
.unwrap();
let nbits: u32 = 453040064;
let ntime: u32 = 1746839905;
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 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 = SubmitSharesExtended {
channel_id,
sequence_number: 0,
job_id: 1,
nonce: 102103,
ntime: 1745596971,
version: 536870912,
extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(),
};
let res = channel.validate_share(valid_share);
assert!(matches!(res, Ok(ShareValidationResult::Valid(_))));
let repeated_share = SubmitSharesExtended {
channel_id,
sequence_number: 1,
job_id: 1,
nonce: 102103,
ntime: 1745596971,
version: 536870912,
extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(),
};
let res = channel.validate_share(repeated_share);
assert!(matches!(
res.unwrap_err(),
ShareValidationError::DuplicateShare
));
}
}