extern crate alloc;
use super::{HashMap, MAX_FUTURE_JOBS, MAX_PAST_JOBS};
use crate::{
bip141::try_strip_bip141,
chain_tip::ChainTip,
client::{
error::ExtendedChannelError,
share_accounting::{ShareAccounting, ShareValidationError, ShareValidationResult},
},
extranonce_manager::{prefix::RetiredExtranoncePrefixes, ExtranoncePrefix},
merkle_root::merkle_root_from_path,
target::{bytes_to_hex, u256_to_block_hash},
MAX_EXTRANONCE_LEN, MAX_FUTURE_BLOCK_TIME, VERSION_ROLLING_MASK,
};
use alloc::{collections::VecDeque, format, string::String, vec, vec::Vec};
use binary_sv2::Sv2OptionOwned;
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::{
NewExtendedMiningJobOwned, SetCustomMiningJobOwned, SetCustomMiningJobSuccess,
SetNewPrevHashOwned as SetNewPrevHashMp, SubmitSharesExtendedOwned,
ERROR_CODE_SUBMIT_SHARES_BAD_EXTRANONCE_SIZE, ERROR_CODE_SUBMIT_SHARES_DIFFICULTY_TOO_LOW,
ERROR_CODE_SUBMIT_SHARES_DUPLICATE_SHARE, ERROR_CODE_SUBMIT_SHARES_INVALID_JOB_ID,
ERROR_CODE_SUBMIT_SHARES_INVALID_NON_ROLLABLE_VERSION_BIT,
ERROR_CODE_SUBMIT_SHARES_INVALID_SHARE, ERROR_CODE_SUBMIT_SHARES_STALE_SHARE,
ERROR_CODE_VERSION_ROLLING_NOT_ALLOWED,
};
use tracing::debug;
#[derive(Debug, Clone, PartialEq)]
pub struct ExtendedJob {
pub job_message: NewExtendedMiningJobOwned,
pub extranonce_prefix: Vec<u8>,
pub target: Target,
}
#[derive(Debug)]
pub struct ExtendedChannel {
channel_id: u32,
user_identity: String,
extranonce_prefix: ExtranoncePrefix,
rollable_extranonce_size: u16,
target: Target,
nominal_hashrate: f32,
version_rolling: bool,
future_jobs: HashMap<u32, ExtendedJob>,
future_job_order: VecDeque<u32>,
active_job: Option<ExtendedJob>,
past_jobs: HashMap<u32, ExtendedJob>,
past_job_order: VecDeque<u32>,
stale_jobs: HashMap<u32, ExtendedJob>,
max_past_jobs: usize,
share_accounting: ShareAccounting,
chain_tip: Option<ChainTip>,
retired_extranonce_prefixes: RetiredExtranoncePrefixes,
}
impl ExtendedChannel {
#[allow(clippy::too_many_arguments)]
pub fn new(
channel_id: u32,
user_identity: String,
extranonce_prefix: ExtranoncePrefix,
target: Target,
nominal_hashrate: f32,
version_rolling: bool,
rollable_extranonce_size: u16,
max_past_jobs: Option<usize>,
) -> Result<Self, ExtendedChannelError> {
if target == Target::ZERO {
return Err(ExtendedChannelError::InvalidTarget);
}
if extranonce_prefix.len() + rollable_extranonce_size as usize > MAX_EXTRANONCE_LEN as usize
{
return Err(ExtendedChannelError::NewExtranoncePrefixTooLarge);
}
let max_past_jobs = match max_past_jobs {
Some(cap) if cap > 0 => cap,
_ => MAX_PAST_JOBS,
};
Ok(Self {
channel_id,
user_identity,
extranonce_prefix,
rollable_extranonce_size,
target,
nominal_hashrate,
version_rolling,
future_jobs: HashMap::new(),
future_job_order: VecDeque::new(),
active_job: None,
past_jobs: HashMap::new(),
past_job_order: VecDeque::new(),
stale_jobs: HashMap::new(),
max_past_jobs,
share_accounting: ShareAccounting::new(),
chain_tip: None,
retired_extranonce_prefixes: RetiredExtranoncePrefixes::default(),
})
}
pub fn get_channel_id(&self) -> u32 {
self.channel_id
}
pub fn get_user_identity(&self) -> &str {
&self.user_identity
}
pub fn get_extranonce_prefix(&self) -> &[u8] {
self.extranonce_prefix.as_bytes()
}
pub fn upstream_prefix_len(&self) -> u8 {
self.extranonce_prefix.upstream_prefix_len()
}
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) {
match &self.chain_tip {
None => self.chain_tip = Some(chain_tip),
Some(current) if *current == chain_tip => {}
Some(_) => self.update_chain_tip(chain_tip),
}
}
pub fn set_extranonce_prefix(
&mut self,
new_extranonce_prefix: ExtranoncePrefix,
) -> Result<(), ExtendedChannelError> {
let full_extranonce_size =
new_extranonce_prefix.len() + self.rollable_extranonce_size as usize;
if full_extranonce_size > MAX_EXTRANONCE_LEN as usize {
return Err(ExtendedChannelError::NewExtranoncePrefixTooLarge);
}
let retired_extranonce_prefix =
core::mem::replace(&mut self.extranonce_prefix, new_extranonce_prefix);
self.retired_extranonce_prefixes.retire(
retired_extranonce_prefix,
self.future_jobs
.values()
.chain(self.active_job.iter())
.chain(self.past_jobs.values())
.map(|job| job.extranonce_prefix.as_slice()),
);
Ok(())
}
pub fn set_upstream_extranonce_prefix(
&mut self,
upstream_prefix: &[u8],
) -> Result<(), ExtendedChannelError> {
let full_extranonce_size = upstream_prefix.len()
+ self.extranonce_prefix.preserved_len()
+ self.rollable_extranonce_size as usize;
if full_extranonce_size > MAX_EXTRANONCE_LEN as usize {
return Err(ExtendedChannelError::NewExtranoncePrefixTooLarge);
}
let snapshot = self
.extranonce_prefix
.snapshot_for_upstream_update(upstream_prefix);
self.extranonce_prefix
.set_upstream_prefix(upstream_prefix)
.map_err(|_| ExtendedChannelError::NewExtranoncePrefixTooLarge)?;
if let Some(snapshot) = snapshot {
self.retired_extranonce_prefixes.retire(
snapshot,
self.future_jobs
.values()
.chain(self.active_job.iter())
.chain(self.past_jobs.values())
.map(|job| job.extranonce_prefix.as_slice()),
);
}
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) -> Result<(), ExtendedChannelError> {
if new_target == Target::ZERO {
return Err(ExtendedChannelError::InvalidTarget);
}
self.target = new_target;
for future_job in self.future_jobs.values_mut() {
future_job.target = new_target;
}
Ok(())
}
pub fn get_nominal_hashrate(&self) -> f32 {
self.nominal_hashrate
}
pub fn set_nominal_hashrate(&mut self, hashrate: f32) {
self.nominal_hashrate = hashrate;
}
pub fn get_active_job(&self) -> Option<&ExtendedJob> {
self.active_job.as_ref()
}
pub fn get_future_jobs(&self) -> impl Iterator<Item = (&u32, &ExtendedJob)> + '_ {
self.future_jobs.iter()
}
pub fn get_future_job(&self, job_id: u32) -> Option<&ExtendedJob> {
self.future_jobs.get(&job_id)
}
pub fn get_future_jobs_count(&self) -> usize {
self.future_jobs.len()
}
pub fn get_past_jobs(&self) -> impl Iterator<Item = (&u32, &ExtendedJob)> + '_ {
self.past_jobs.iter()
}
pub fn get_past_job(&self, job_id: u32) -> Option<&ExtendedJob> {
self.past_jobs.get(&job_id)
}
pub fn get_past_jobs_count(&self) -> usize {
self.past_jobs.len()
}
pub fn get_stale_jobs(&self) -> impl Iterator<Item = (&u32, &ExtendedJob)> + '_ {
self.stale_jobs.iter()
}
pub fn get_stale_job(&self, job_id: u32) -> Option<&ExtendedJob> {
self.stale_jobs.get(&job_id)
}
pub fn get_stale_jobs_count(&self) -> usize {
self.stale_jobs.len()
}
pub fn get_share_accounting(&self) -> &ShareAccounting {
&self.share_accounting
}
pub fn on_share_acknowledgement(
&mut self,
new_submits_accepted_count: u32,
new_shares_sum: u64,
) {
self.share_accounting
.on_share_acknowledgement(new_submits_accepted_count, new_shares_sum);
}
pub fn on_share_rejection(&mut self, error_code: &str) {
self.share_accounting.on_share_rejection(error_code);
}
pub fn on_new_extended_mining_job(
&mut self,
new_extended_mining_job: NewExtendedMiningJobOwned,
) -> Result<(), ExtendedChannelError> {
let mut new_extended_mining_job = new_extended_mining_job;
let new_extended_mining_job = match try_strip_bip141(
new_extended_mining_job.coinbase_tx_prefix.as_bytes(),
new_extended_mining_job.coinbase_tx_suffix.as_bytes(),
)
.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 self
.chain_tip
.as_ref()
.is_some_and(|chain_tip| min_ntime < chain_tip.min_ntime())
{
return Err(ExtendedChannelError::JobMinNtimeBelowChainTip);
}
self.stale_jobs.remove(&new_extended_mining_job.job_id);
let displaced_job = self.active_job.replace(ExtendedJob {
job_message: new_extended_mining_job,
extranonce_prefix: self.extranonce_prefix.as_bytes().to_vec(),
target: self.target,
});
if let Some(displaced_job) = displaced_job {
self.retire_job_to_past(displaced_job);
}
}
None => {
let job_id = new_extended_mining_job.job_id;
self.future_jobs.insert(
job_id,
ExtendedJob {
job_message: new_extended_mining_job,
extranonce_prefix: self.extranonce_prefix.as_bytes().to_vec(),
target: self.target,
},
);
self.future_job_order.retain(|id| *id != job_id);
self.future_job_order.push_back(job_id);
if self.future_jobs.len() > MAX_FUTURE_JOBS {
if let Some(evicted_job_id) = self.future_job_order.pop_front() {
self.future_jobs.remove(&evicted_job_id);
}
}
self.prune_retired_extranonce_prefixes();
}
}
Ok(())
}
pub fn on_set_custom_mining_job_success(
&mut self,
set_custom_mining_job: SetCustomMiningJobOwned,
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
.to_owned_bytes()
.as_slice(),
)
.map_err(|_| ExtendedChannelError::FailedToDeserializeCoinbaseOutputs)?;
let mut script_sig = vec![];
script_sig.extend_from_slice(set_custom_mining_job.coinbase_prefix.as_bytes());
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.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 = NewExtendedMiningJobOwned {
channel_id: set_custom_mining_job.channel_id,
job_id: set_custom_mining_job_success.job_id,
min_ntime: Sv2OptionOwned::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,
};
self.stale_jobs.remove(&new_extended_mining_job.job_id);
let displaced_job = self.active_job.replace(ExtendedJob {
job_message: new_extended_mining_job,
extranonce_prefix: self.extranonce_prefix.as_bytes().to_vec(),
target: self.target,
});
if let Some(displaced_job) = displaced_job {
self.retire_job_to_past(displaced_job);
}
Ok(())
}
fn retire_job_to_past(&mut self, job: ExtendedJob) {
let job_id = job.job_message.job_id;
self.past_jobs.insert(job_id, job);
self.past_job_order.retain(|id| *id != job_id);
self.past_job_order.push_back(job_id);
if self.past_jobs.len() > self.max_past_jobs {
if let Some(evicted_job_id) = self.past_job_order.pop_front() {
self.past_jobs.remove(&evicted_job_id);
}
}
self.prune_retired_extranonce_prefixes();
}
fn prune_retired_extranonce_prefixes(&mut self) {
self.retired_extranonce_prefixes.prune(
self.future_jobs
.values()
.chain(self.active_job.iter())
.chain(self.past_jobs.values())
.map(|job| job.extranonce_prefix.as_slice()),
);
}
pub fn on_chain_tip_update(&mut self, chain_tip: ChainTip) -> Result<(), ExtendedChannelError> {
self.update_chain_tip(chain_tip);
Ok(())
}
fn update_chain_tip(&mut self, chain_tip: ChainTip) {
let is_new_prev_hash = self
.chain_tip
.as_ref()
.is_some_and(|previous| previous.prev_hash() != chain_tip.prev_hash());
self.chain_tip = Some(chain_tip);
self.future_jobs.clear();
self.future_job_order.clear();
self.stale_jobs = core::mem::take(&mut self.past_jobs);
self.past_job_order.clear();
if let Some(active_job) = self.active_job.take() {
self.stale_jobs
.insert(active_job.job_message.job_id, active_job);
}
self.prune_retired_extranonce_prefixes();
if is_new_prev_hash {
self.share_accounting.flush_seen_shares();
}
}
pub fn on_set_new_prev_hash(
&mut self,
set_new_prev_hash: SetNewPrevHashMp,
) -> Result<(), ExtendedChannelError> {
let previously_active_job = match self.future_jobs.remove(&set_new_prev_hash.job_id) {
Some(mut activated_job) => {
activated_job.job_message.min_ntime =
Sv2OptionOwned::new(Some(set_new_prev_hash.min_ntime));
self.active_job.replace(activated_job)
}
None => {
return Err(ExtendedChannelError::JobIdNotFound);
}
};
self.future_jobs.clear();
self.future_job_order.clear();
self.stale_jobs = core::mem::take(&mut self.past_jobs);
self.past_job_order.clear();
if let Some(previously_active_job) = previously_active_job {
self.stale_jobs.insert(
previously_active_job.job_message.job_id,
previously_active_job,
);
}
self.stale_jobs.remove(&set_new_prev_hash.job_id);
self.prune_retired_extranonce_prefixes();
if self
.chain_tip
.as_ref()
.is_some_and(|chain_tip| chain_tip.prev_hash() != set_new_prev_hash.prev_hash)
{
self.share_accounting.flush_seen_shares();
}
self.chain_tip = Some(set_new_prev_hash.into());
Ok(())
}
pub fn validate_share(
&mut self,
share: SubmitSharesExtendedOwned,
) -> Result<ShareValidationResult, ShareValidationError> {
let job_id = share.job_id;
let is_active_job = self
.active_job
.as_ref()
.is_some_and(|job| job.job_message.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(
ERROR_CODE_SUBMIT_SHARES_STALE_SHARE,
));
}
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(
ERROR_CODE_SUBMIT_SHARES_INVALID_JOB_ID,
));
};
let extranonce_size = share.extranonce.len();
if extranonce_size != self.rollable_extranonce_size as usize {
return Err(ShareValidationError::BadExtranonceSize(
ERROR_CODE_SUBMIT_SHARES_BAD_EXTRANONCE_SIZE,
));
}
let mut full_extranonce = vec![];
full_extranonce.extend_from_slice(job.extranonce_prefix.as_slice());
full_extranonce.extend_from_slice(share.extranonce.as_bytes());
let merkle_root: [u8; 32] = merkle_root_from_path(
job.job_message.coinbase_tx_prefix.as_bytes(),
job.job_message.coinbase_tx_suffix.as_bytes(),
full_extranonce.as_ref(),
job.job_message.merkle_path.as_slice(),
)
.ok_or(ShareValidationError::Invalid(
ERROR_CODE_SUBMIT_SHARES_INVALID_SHARE,
))?;
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());
let job_min_ntime = job
.job_message
.min_ntime
.as_ref()
.copied()
.expect("active and past jobs carry a min_ntime");
if share.ntime < job_min_ntime {
return Err(ShareValidationError::Invalid(
ERROR_CODE_SUBMIT_SHARES_INVALID_SHARE,
));
}
if share.ntime > job_min_ntime.saturating_add(MAX_FUTURE_BLOCK_TIME) {
return Err(ShareValidationError::Invalid(
ERROR_CODE_SUBMIT_SHARES_INVALID_SHARE,
));
}
let version_rolling_mask = if job.job_message.version_rolling_allowed {
VERSION_ROLLING_MASK
} else {
0
};
if (share.version & !version_rolling_mask)
!= (job.job_message.version & !version_rolling_mask)
{
if job.job_message.version_rolling_allowed {
return Err(ShareValidationError::Invalid(
ERROR_CODE_SUBMIT_SHARES_INVALID_NON_ROLLABLE_VERSION_BIT,
));
}
return Err(ShareValidationError::VersionRollingNotAllowed(
ERROR_CODE_VERSION_ROLLING_NOT_ALLOWED,
));
}
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.target;
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) {
if self
.share_accounting
.is_share_seen(share_hash.to_raw_hash())
{
return Err(ShareValidationError::DuplicateShare(
ERROR_CODE_SUBMIT_SHARES_DUPLICATE_SHARE,
));
}
self.share_accounting.track_validated_share(
share.sequence_number,
share_hash.to_raw_hash(),
job_target.difficulty_float(),
);
self.share_accounting.increment_blocks_found();
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(
ERROR_CODE_SUBMIT_SHARES_DUPLICATE_SHARE,
));
}
self.share_accounting.track_validated_share(
share.sequence_number,
share_hash.to_raw_hash(),
job_target.difficulty_float(),
);
self.share_accounting.update_best_diff(share_hash_as_diff);
return Ok(ShareValidationResult::Valid(share_hash.to_raw_hash()));
}
Err(ShareValidationError::DoesNotMeetTarget(
ERROR_CODE_SUBMIT_SHARES_DIFFICULTY_TOO_LOW,
))
}
}
#[cfg(test)]
mod tests {
use super::ExtendedJob;
use crate::{
chain_tip::ChainTip,
client::{
error::ExtendedChannelError,
extended::ExtendedChannel,
share_accounting::{ShareValidationError, ShareValidationResult},
MAX_FUTURE_JOBS, MAX_PAST_JOBS,
},
extranonce_manager::{
ExtranonceAllocator, ExtranonceAllocatorError, ExtranoncePrefix, MAX_EXTRANONCE_LEN,
},
};
use binary_sv2::Sv2OptionOwned as Sv2Option;
use bitcoin::Target;
use mining_sv2::{
NewExtendedMiningJobOwned as NewExtendedMiningJob, SetNewPrevHashOwned as SetNewPrevHashMp,
SubmitSharesExtendedOwned as SubmitSharesExtended,
ERROR_CODE_SUBMIT_SHARES_INVALID_NON_ROLLABLE_VERSION_BIT,
};
use std::convert::TryInto;
#[test]
fn upstream_prefix_update_only_affects_subsequent_jobs() {
let mut allocator =
ExtranonceAllocator::from_upstream_prefix(vec![0xaa], vec![0xbb], 5, 256).unwrap();
let allocated_prefix = allocator.allocate_extended(2).unwrap();
let old_prefix = allocated_prefix.as_bytes().to_vec();
let mut channel = ExtendedChannel::new(
1,
"user_identity".to_string(),
allocated_prefix.into(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
2,
None,
)
.unwrap();
let job = |job_id| NewExtendedMiningJob {
channel_id: 1,
job_id,
min_ntime: Sv2Option::new(Some(1)),
version: 536870912,
version_rolling_allowed: true,
coinbase_tx_prefix: vec![1, 0, 0, 0, 1, 0].try_into().unwrap(),
coinbase_tx_suffix: vec![].try_into().unwrap(),
merkle_path: vec![].try_into().unwrap(),
};
channel.on_new_extended_mining_job(job(1)).unwrap();
channel
.set_upstream_extranonce_prefix(&[0xcc, 0xdd])
.unwrap();
let new_prefix = channel.get_extranonce_prefix().to_vec();
channel.on_new_extended_mining_job(job(2)).unwrap();
assert_eq!(old_prefix, &[0xaa, 0xbb, 0x00]);
assert_eq!(new_prefix, &[0xcc, 0xdd, 0xbb, 0x00]);
assert_eq!(
&channel.get_past_job(1).unwrap().extranonce_prefix,
&old_prefix
);
assert_eq!(
&channel.get_active_job().unwrap().extranonce_prefix,
&new_prefix
);
assert_eq!(allocator.allocated_count(), 1);
}
#[test]
fn new_enforces_full_extranonce_size() {
let result = ExtendedChannel::new(
1,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(vec![0xaa; MAX_EXTRANONCE_LEN as usize]).unwrap(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
1,
None,
);
assert!(matches!(
result,
Err(ExtendedChannelError::NewExtranoncePrefixTooLarge)
));
}
#[test]
fn set_extranonce_prefix_enforces_full_extranonce_size() {
let rollable_extranonce_size = 4;
let mut channel = ExtendedChannel::new(
1,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(vec![0xaa]).unwrap(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
rollable_extranonce_size,
None,
)
.unwrap();
let largest_valid_prefix =
vec![0xbb; MAX_EXTRANONCE_LEN as usize - rollable_extranonce_size as usize];
channel
.set_extranonce_prefix(
ExtranoncePrefix::from_wire(largest_valid_prefix.clone()).unwrap(),
)
.unwrap();
assert_eq!(channel.get_extranonce_prefix(), &largest_valid_prefix);
let result = channel.set_extranonce_prefix(
ExtranoncePrefix::from_wire(vec![0xcc; largest_valid_prefix.len() + 1]).unwrap(),
);
assert!(matches!(
result,
Err(ExtendedChannelError::NewExtranoncePrefixTooLarge)
));
assert_eq!(channel.get_extranonce_prefix(), &largest_valid_prefix);
}
#[test]
fn set_upstream_extranonce_prefix_enforces_full_extranonce_size_transactionally() {
let mut allocator =
ExtranonceAllocator::from_upstream_prefix(vec![0xaa], vec![0xbb], 5, 256).unwrap();
let allocated_prefix = allocator.allocate_extended(2).unwrap();
let mut channel = ExtendedChannel::new(
1,
"user_identity".to_string(),
allocated_prefix.into(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
2,
None,
)
.unwrap();
channel.set_upstream_extranonce_prefix(&[0xcc; 28]).unwrap();
let largest_valid_prefix = channel.get_extranonce_prefix().to_vec();
let upstream_prefix_len = channel.upstream_prefix_len();
let result = channel.set_upstream_extranonce_prefix(&[0xdd; 29]);
assert!(matches!(
result,
Err(ExtendedChannelError::NewExtranoncePrefixTooLarge)
));
assert_eq!(channel.get_extranonce_prefix(), &largest_valid_prefix);
assert_eq!(channel.upstream_prefix_len(), upstream_prefix_len);
assert_eq!(allocator.allocated_count(), 1);
}
#[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,
ExtranoncePrefix::from_wire(extranonce_prefix.clone()).unwrap(),
target,
nominal_hashrate,
version_rolling,
rollable_extranonce_size,
None,
)
.unwrap();
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_count(), 1);
assert_eq!(channel.get_active_job(), None);
assert_eq!(channel.get_past_jobs_count(), 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_eq!(channel.get_future_jobs_count(), 0);
let mut previously_future_job = future_job.clone();
previously_future_job.min_ntime = Sv2Option::new(Some(ntime));
assert_eq!(
channel.get_active_job(),
Some(&ExtendedJob {
job_message: previously_future_job,
extranonce_prefix,
target: channel.get_target().clone()
})
);
}
#[test]
fn test_future_jobs_are_bounded() {
let channel_id = 1;
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 mut channel = ExtendedChannel::new(
channel_id,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
4u16,
None,
)
.unwrap();
let future_job = NewExtendedMiningJob {
channel_id,
job_id: 0,
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(),
};
let flood_size = 10_000u32;
for job_id in 0..flood_size {
let mut job = future_job.clone();
job.job_id = job_id;
channel.on_new_extended_mining_job(job).unwrap();
}
assert_eq!(channel.get_future_jobs_count(), MAX_FUTURE_JOBS);
for job_id in 0..flood_size - MAX_FUTURE_JOBS as u32 {
assert!(channel.get_future_job(job_id).is_none());
}
for job_id in flood_size - MAX_FUTURE_JOBS as u32..flood_size {
assert!(channel.get_future_job(job_id).is_some());
}
}
#[test]
fn test_replaced_future_job_moves_to_back_of_eviction_order() {
let channel_id = 1;
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 mut channel = ExtendedChannel::new(
channel_id,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
4u16,
None,
)
.unwrap();
let future_job = NewExtendedMiningJob {
channel_id,
job_id: 0,
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(),
};
for job_id in 0..MAX_FUTURE_JOBS as u32 {
let mut job = future_job.clone();
job.job_id = job_id;
channel.on_new_extended_mining_job(job).unwrap();
}
channel
.on_new_extended_mining_job(future_job.clone())
.unwrap();
let mut job = future_job.clone();
job.job_id = MAX_FUTURE_JOBS as u32;
channel.on_new_extended_mining_job(job).unwrap();
assert_eq!(channel.get_future_jobs_count(), MAX_FUTURE_JOBS);
assert!(channel.get_future_job(1).is_none());
assert!(channel.get_future_job(0).is_some());
let set_new_prev_hash = SetNewPrevHashMp {
channel_id,
job_id: 0,
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: 1746839905,
};
channel.on_set_new_prev_hash(set_new_prev_hash).unwrap();
}
#[test]
fn test_past_jobs_are_bounded() {
let channel_id = 1;
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 mut channel = ExtendedChannel::new(
channel_id,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
4u16,
None,
)
.unwrap();
let active_job = NewExtendedMiningJob {
channel_id,
job_id: 0,
min_ntime: Sv2Option::new(Some(1746839905)),
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(),
};
let flood_size = 10_000u32;
for job_id in 0..flood_size {
let mut job = active_job.clone();
job.job_id = job_id;
channel.on_new_extended_mining_job(job).unwrap();
}
assert_eq!(channel.get_past_jobs_count(), MAX_PAST_JOBS);
for job_id in 0..flood_size - 1 - MAX_PAST_JOBS as u32 {
assert!(channel.get_past_job(job_id).is_none());
}
for job_id in flood_size - 1 - MAX_PAST_JOBS as u32..flood_size - 1 {
assert!(channel.get_past_job(job_id).is_some());
}
}
#[test]
fn test_past_jobs_respect_constructor_override() {
let custom_cap = 3usize;
assert!(custom_cap < MAX_PAST_JOBS);
let channel_id = 1;
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 mut channel = ExtendedChannel::new(
channel_id,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
4u16,
Some(custom_cap),
)
.unwrap();
let active_job = NewExtendedMiningJob {
channel_id,
job_id: 0,
min_ntime: Sv2Option::new(Some(1746839905)),
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(),
};
let job_count = 20u32;
for job_id in 0..job_count {
let mut job = active_job.clone();
job.job_id = job_id;
channel.on_new_extended_mining_job(job).unwrap();
}
assert_eq!(channel.get_past_jobs_count(), custom_cap);
for job_id in 0..job_count - 1 - custom_cap as u32 {
assert!(channel.get_past_job(job_id).is_none());
}
for job_id in job_count - 1 - custom_cap as u32..job_count - 1 {
assert!(channel.get_past_job(job_id).is_some());
}
let mut zero_cap_channel = ExtendedChannel::new(
channel_id,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(vec![0; 27]).unwrap(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
4u16,
Some(0),
)
.unwrap();
for job_id in 0..MAX_PAST_JOBS as u32 + 2 {
let mut job = active_job.clone();
job.job_id = job_id;
zero_cap_channel.on_new_extended_mining_job(job).unwrap();
}
assert_eq!(zero_cap_channel.get_past_jobs_count(), MAX_PAST_JOBS);
}
#[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,
ExtranoncePrefix::from_wire(extranonce_prefix.clone()).unwrap(),
target,
nominal_hashrate,
version_rolling,
rollable_extranonce_size,
None,
)
.unwrap();
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_count(), 0);
assert_eq!(
channel.get_active_job(),
Some(&ExtendedJob {
job_message: active_job.clone(),
extranonce_prefix: extranonce_prefix.clone(),
target: channel.get_target().clone()
})
);
assert_eq!(channel.get_past_jobs_count(), 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_count(), 0);
assert_eq!(
channel.get_active_job(),
Some(&ExtendedJob {
job_message: new_active_job,
extranonce_prefix,
target: channel.get_target().clone()
})
);
assert_eq!(channel.get_past_jobs_count(), 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,
ExtranoncePrefix::from_wire(extranonce_prefix.clone()).unwrap(),
target,
nominal_hashrate,
version_rolling,
rollable_extranonce_size,
None,
)
.unwrap();
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 = 1745596970;
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.clone());
assert!(matches!(res, Ok(ShareValidationResult::BlockFound(_))));
assert_eq!(channel.get_share_accounting().get_blocks_found(), 1);
let res = channel.validate_share(share_valid_block);
assert!(matches!(
res.unwrap_err(),
ShareValidationError::DuplicateShare(_)
));
assert_eq!(channel.get_share_accounting().get_blocks_found(), 1);
}
#[test]
fn test_share_validation_ntime_below_min_ntime() {
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,
ExtranoncePrefix::from_wire(extranonce_prefix.clone()).unwrap(),
target,
nominal_hashrate,
version_rolling,
rollable_extranonce_size,
None,
)
.unwrap();
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 set_new_prev_hash = SetNewPrevHashMp {
channel_id,
job_id: future_job.job_id,
prev_hash: prev_hash.into(),
nbits,
min_ntime: 1745596972,
};
channel.on_set_new_prev_hash(set_new_prev_hash).unwrap();
let share_below_min_ntime = 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_below_min_ntime);
assert!(matches!(res.unwrap_err(), ShareValidationError::Invalid(_)));
assert_eq!(channel.get_share_accounting().get_blocks_found(), 0);
}
#[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,
ExtranoncePrefix::from_wire(extranonce_prefix.clone()).unwrap(),
target,
nominal_hashrate,
version_rolling,
rollable_extranonce_size,
None,
)
.unwrap();
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 = 1745596970;
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,
ExtranoncePrefix::from_wire(extranonce_prefix.clone()).unwrap(),
target,
nominal_hashrate,
version_rolling,
rollable_extranonce_size,
None,
)
.unwrap();
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 = 1745596970;
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(_)
));
}
#[test]
fn test_share_validation_invalid_non_rollable_version_bit() {
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,
ExtranoncePrefix::from_wire(extranonce_prefix.clone()).unwrap(),
target,
nominal_hashrate,
version_rolling,
rollable_extranonce_size,
None,
)
.unwrap();
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 = 1745596970;
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 = SubmitSharesExtended {
channel_id,
sequence_number: 0,
job_id: 1,
nonce: 0,
ntime: 1745596971,
version: 536870913,
extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(),
};
let res = channel.validate_share(share);
let err = res.expect_err("share with non-rollable version bits must be rejected");
match err {
ShareValidationError::Invalid(code) => {
assert_eq!(
code,
ERROR_CODE_SUBMIT_SHARES_INVALID_NON_ROLLABLE_VERSION_BIT
);
}
other => panic!("expected ShareValidationError::Invalid, got {other:?}"),
}
}
#[test]
fn test_share_validation_version_rolling_not_allowed() {
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 = false;
let rollable_extranonce_size = 8u16;
let mut channel = ExtendedChannel::new(
channel_id,
user_identity,
ExtranoncePrefix::from_wire(extranonce_prefix.clone()).unwrap(),
target,
nominal_hashrate,
version_rolling,
rollable_extranonce_size,
None,
)
.unwrap();
let future_job = NewExtendedMiningJob {
channel_id: 1,
job_id: 1,
min_ntime: Sv2Option::new(None),
version: 536870912,
version_rolling_allowed: false,
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 = 1745596970;
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_non_rollable_bit = SubmitSharesExtended {
channel_id,
sequence_number: 0,
job_id: 1,
nonce: 0,
ntime: 1745596971,
version: 536870913,
extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(),
};
let res = channel.validate_share(share_non_rollable_bit);
assert!(matches!(
res.unwrap_err(),
ShareValidationError::VersionRollingNotAllowed(_)
));
let share_rolled_bit = SubmitSharesExtended {
channel_id,
sequence_number: 1,
job_id: 1,
nonce: 0,
ntime: 1745596971,
version: 0x20000020,
extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(),
};
let res = channel.validate_share(share_rolled_bit);
assert!(matches!(
res.unwrap_err(),
ShareValidationError::VersionRollingNotAllowed(_)
));
}
#[test]
fn test_share_validation_version_rolling_not_allowed_matching_version() {
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 = false;
let rollable_extranonce_size = 8u16;
let mut channel = ExtendedChannel::new(
channel_id,
user_identity,
ExtranoncePrefix::from_wire(extranonce_prefix.clone()).unwrap(),
target,
nominal_hashrate,
version_rolling,
rollable_extranonce_size,
None,
)
.unwrap();
let future_job = NewExtendedMiningJob {
channel_id: 1,
job_id: 1,
min_ntime: Sv2Option::new(None),
version: 536870912,
version_rolling_allowed: false,
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 = 1745596970;
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_rollable_version_bits() {
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,
ExtranoncePrefix::from_wire(extranonce_prefix.clone()).unwrap(),
target,
nominal_hashrate,
version_rolling,
rollable_extranonce_size,
None,
)
.unwrap();
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 = 1745596970;
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 rolled_version = 0x20000000 | 0x1fffe0;
let share = SubmitSharesExtended {
channel_id,
sequence_number: 0,
job_id: 1,
nonce: 0,
ntime: 1745596971,
version: rolled_version,
extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(),
};
assert!(channel.validate_share(share).is_ok());
}
#[test]
fn test_set_target_refreshes_future_jobs() {
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,
ExtranoncePrefix::from_wire(extranonce_prefix.clone()).unwrap(),
target,
nominal_hashrate,
version_rolling,
rollable_extranonce_size,
None,
)
.unwrap();
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 new_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,
0x00, 0x50, 0x00, 0x00,
]);
channel.set_target(new_target).unwrap();
let nbits: u32 = 453040064;
let ntime: u32 = 1745596970;
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 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(share);
assert!(matches!(
res,
Err(ShareValidationError::DoesNotMeetTarget(_))
));
}
#[test]
fn test_chain_tip_update_retires_active_job() {
let channel_id = 1;
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 mut channel = ExtendedChannel::new(
channel_id,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
8u16,
None,
)
.unwrap();
let active_job = NewExtendedMiningJob {
channel_id,
job_id: 1,
min_ntime: Sv2Option::new(Some(1745596970)),
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(active_job).unwrap();
assert!(channel.get_active_job().is_some());
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,
];
channel
.on_chain_tip_update(ChainTip::new(prev_hash.into(), 545259519, 1745596980))
.unwrap();
assert!(channel.get_active_job().is_none());
assert_eq!(channel.get_stale_jobs_count(), 1);
assert!(channel.get_stale_job(1).is_some());
assert_eq!(channel.get_past_jobs_count(), 0);
}
#[test]
fn test_set_new_prev_hash_retires_active_job() {
let channel_id = 1;
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 mut channel = ExtendedChannel::new(
channel_id,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
8u16,
None,
)
.unwrap();
let job_template = NewExtendedMiningJob {
channel_id,
job_id: 1,
min_ntime: Sv2Option::new(Some(1745596970)),
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(job_template.clone())
.unwrap();
let mut future_job = job_template;
future_job.job_id = 2;
future_job.min_ntime = Sv2Option::new(None);
channel.on_new_extended_mining_job(future_job).unwrap();
let prev_hash: [u8; 32] = [
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 unknown_job_set_new_prev_hash = SetNewPrevHashMp {
channel_id,
job_id: 42,
prev_hash: prev_hash.into(),
nbits: 545259519,
min_ntime: 1745596980,
};
assert!(matches!(
channel.on_set_new_prev_hash(unknown_job_set_new_prev_hash),
Err(ExtendedChannelError::JobIdNotFound)
));
assert_eq!(channel.get_active_job().unwrap().job_message.job_id, 1);
assert_eq!(channel.get_stale_jobs_count(), 0);
let set_new_prev_hash = SetNewPrevHashMp {
channel_id,
job_id: 2,
prev_hash: prev_hash.into(),
nbits: 545259519,
min_ntime: 1745596980,
};
channel.on_set_new_prev_hash(set_new_prev_hash).unwrap();
assert_eq!(channel.get_active_job().unwrap().job_message.job_id, 2);
assert_eq!(channel.get_stale_jobs_count(), 1);
assert!(channel.get_stale_job(1).is_some());
assert_eq!(channel.get_past_jobs_count(), 0);
}
fn extended_channel_with_past_jobs_at_cap() -> (ExtendedChannel, NewExtendedMiningJob) {
let channel_id = 1;
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 mut channel = ExtendedChannel::new(
channel_id,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
8u16,
None,
)
.unwrap();
let job_template = NewExtendedMiningJob {
channel_id,
job_id: 0,
min_ntime: Sv2Option::new(Some(1745596970)),
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(),
};
for job_id in 0..=MAX_PAST_JOBS as u32 {
let mut job = job_template.clone();
job.job_id = job_id;
channel.on_new_extended_mining_job(job).unwrap();
}
assert_eq!(channel.get_past_jobs_count(), MAX_PAST_JOBS);
(channel, job_template)
}
#[test]
fn test_set_new_prev_hash_keeps_all_past_jobs_in_stale_set() {
let (mut channel, job_template) = extended_channel_with_past_jobs_at_cap();
let channel_id = job_template.channel_id;
let future_job_id = 100;
let mut future_job = job_template;
future_job.job_id = future_job_id;
future_job.min_ntime = Sv2Option::new(None);
channel.on_new_extended_mining_job(future_job).unwrap();
let prev_hash: [u8; 32] = [
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,
];
channel
.on_set_new_prev_hash(SetNewPrevHashMp {
channel_id,
job_id: future_job_id,
prev_hash: prev_hash.into(),
nbits: 545259519,
min_ntime: 1745596980,
})
.unwrap();
assert_eq!(channel.get_stale_jobs_count(), MAX_PAST_JOBS + 1);
for job_id in 0..=MAX_PAST_JOBS as u32 {
assert!(channel.get_stale_job(job_id).is_some());
}
assert_eq!(channel.get_past_jobs_count(), 0);
assert_eq!(
channel.get_active_job().unwrap().job_message.job_id,
future_job_id
);
}
#[test]
fn test_chain_tip_update_keeps_all_past_jobs_in_stale_set() {
let (mut channel, _job_template) = extended_channel_with_past_jobs_at_cap();
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,
];
channel
.on_chain_tip_update(ChainTip::new(prev_hash.into(), 545259519, 1745596980))
.unwrap();
assert_eq!(channel.get_stale_jobs_count(), MAX_PAST_JOBS + 1);
for job_id in 0..=MAX_PAST_JOBS as u32 {
assert!(channel.get_stale_job(job_id).is_some());
}
assert_eq!(channel.get_past_jobs_count(), 0);
assert!(channel.get_active_job().is_none());
}
#[test]
fn test_share_validation_ntime_below_job_min_ntime() {
let channel_id = 1;
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 mut channel = ExtendedChannel::new(
channel_id,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
8u16,
None,
)
.unwrap();
let job = |job_id: u32, min_ntime: Option<u32>| NewExtendedMiningJob {
channel_id,
job_id,
min_ntime: Sv2Option::new(min_ntime),
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(),
};
let share = |sequence_number: u32, job_id: u32, ntime: u32| SubmitSharesExtended {
channel_id,
sequence_number,
job_id,
nonce: 741057,
ntime,
version: 536870912,
extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(),
};
let tip_ntime: u32 = 1745596930;
channel.on_new_extended_mining_job(job(1, None)).unwrap();
channel
.on_set_new_prev_hash(SetNewPrevHashMp {
channel_id,
job_id: 1,
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: 453040064,
min_ntime: tip_ntime,
})
.unwrap();
let res = channel.validate_share(share(0, 1, tip_ntime - 1));
assert!(matches!(res.unwrap_err(), ShareValidationError::Invalid(_)));
let job_min_ntime = tip_ntime + 3;
channel
.on_new_extended_mining_job(job(2, Some(job_min_ntime)))
.unwrap();
let res = channel.validate_share(share(1, 2, job_min_ntime - 1));
assert!(matches!(res.unwrap_err(), ShareValidationError::Invalid(_)));
let res = channel.validate_share(share(
3,
2,
job_min_ntime + crate::MAX_FUTURE_BLOCK_TIME + 1,
));
assert!(matches!(res.unwrap_err(), ShareValidationError::Invalid(_)));
let res = channel.validate_share(share(4, 2, job_min_ntime + crate::MAX_FUTURE_BLOCK_TIME));
assert!(matches!(res, Ok(ShareValidationResult::Valid(_))));
let res = channel.validate_share(share(2, 2, job_min_ntime));
assert!(matches!(res, Ok(ShareValidationResult::Valid(_))));
}
#[test]
fn test_share_validation_ntime_above_max_future_block_time() {
let channel_id = 1;
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 mut channel = ExtendedChannel::new(
channel_id,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
8u16,
None,
)
.unwrap();
channel
.on_new_extended_mining_job(NewExtendedMiningJob {
channel_id,
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(),
})
.unwrap();
let tip_ntime: u32 = 1745596930;
channel
.on_set_new_prev_hash(SetNewPrevHashMp {
channel_id,
job_id: 1,
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: 453040064,
min_ntime: tip_ntime,
})
.unwrap();
let share = |sequence_number: u32, ntime: u32| SubmitSharesExtended {
channel_id,
sequence_number,
job_id: 1,
nonce: 741057,
ntime,
version: 536870912,
extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(),
};
let res = channel.validate_share(share(0, tip_ntime + crate::MAX_FUTURE_BLOCK_TIME + 1));
assert!(matches!(res.unwrap_err(), ShareValidationError::Invalid(_)));
let res = channel.validate_share(share(1, u32::MAX));
assert!(matches!(res.unwrap_err(), ShareValidationError::Invalid(_)));
let res = channel.validate_share(share(2, tip_ntime + crate::MAX_FUTURE_BLOCK_TIME));
assert!(matches!(res, Ok(ShareValidationResult::Valid(_))));
}
#[test]
fn test_reused_job_id_resolves_to_the_live_job() {
let channel_id = 1;
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 mut channel = ExtendedChannel::new(
channel_id,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
8u16,
None,
)
.unwrap();
let job = |min_ntime: Option<u32>| NewExtendedMiningJob {
channel_id,
job_id: 1,
min_ntime: Sv2Option::new(min_ntime),
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(job(Some(1745596970)))
.unwrap();
channel.on_new_extended_mining_job(job(None)).unwrap();
channel
.on_set_new_prev_hash(SetNewPrevHashMp {
channel_id,
job_id: 1,
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: 453040064,
min_ntime: 1745596980,
})
.unwrap();
assert_eq!(channel.get_active_job().unwrap().job_message.job_id, 1);
assert!(channel.get_stale_job(1).is_none());
assert_eq!(channel.get_stale_jobs_count(), 0);
let share = |sequence_number: u32, ntime: u32| SubmitSharesExtended {
channel_id,
sequence_number,
job_id: 1,
nonce: 0,
ntime,
version: 536870912,
extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(),
};
assert!(matches!(
channel.validate_share(share(0, 1745596980)),
Ok(ShareValidationResult::Valid(_))
));
let mut future_job = job(None);
future_job.job_id = 2;
channel.on_new_extended_mining_job(future_job).unwrap();
channel
.on_set_new_prev_hash(SetNewPrevHashMp {
channel_id,
job_id: 2,
prev_hash: [
154, 124, 239, 231, 221, 122, 160, 173, 164, 175, 87, 33, 74, 214, 191, 107,
73, 34, 0, 162, 227, 16, 44, 40, 33, 73, 0, 0, 0, 0, 0, 0,
]
.into(),
nbits: 453040064,
min_ntime: 1745596990,
})
.unwrap();
assert!(channel.get_stale_job(1).is_some());
channel
.on_new_extended_mining_job(job(Some(1745596990)))
.unwrap();
assert_eq!(channel.get_active_job().unwrap().job_message.job_id, 1);
assert!(channel.get_stale_job(1).is_none());
assert!(matches!(
channel.validate_share(share(1, 1745596990)),
Ok(ShareValidationResult::Valid(_))
));
}
#[test]
fn test_repeated_prev_hash_keeps_seen_shares() {
let channel_id = 1;
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 mut channel = ExtendedChannel::new(
channel_id,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
8u16,
None,
)
.unwrap();
let future_job = |job_id: u32| NewExtendedMiningJob {
channel_id,
job_id,
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(),
};
let set_new_prev_hash = |job_id: u32| SetNewPrevHashMp {
channel_id,
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: 453040064,
min_ntime: 1745596970,
};
let share = |sequence_number: u32, job_id: u32| SubmitSharesExtended {
channel_id,
sequence_number,
job_id,
nonce: 0,
ntime: 1745596971,
version: 536870912,
extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(),
};
channel.on_new_extended_mining_job(future_job(1)).unwrap();
channel.on_set_new_prev_hash(set_new_prev_hash(1)).unwrap();
assert!(matches!(
channel.validate_share(share(0, 1)),
Ok(ShareValidationResult::Valid(_))
));
channel.on_new_extended_mining_job(future_job(2)).unwrap();
channel.on_set_new_prev_hash(set_new_prev_hash(2)).unwrap();
assert!(matches!(
channel.validate_share(share(1, 2)),
Err(ShareValidationError::DuplicateShare(_))
));
assert_eq!(channel.get_share_accounting().get_validated_shares(), 1);
}
fn active_job_template(job_id: u32) -> NewExtendedMiningJob {
NewExtendedMiningJob {
channel_id: 1,
job_id,
min_ntime: Sv2Option::new(Some(1745596970)),
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(),
}
}
fn extended_channel_with_rotated_extranonce_prefix(
) -> (ExtranonceAllocator, ExtendedChannel, Vec<u8>) {
let mut allocator = ExtranonceAllocator::new(vec![], 32, 2).unwrap();
let prefix_1 = allocator.allocate_extended(8).unwrap();
let prefix_2 = allocator.allocate_extended(8).unwrap();
assert_ne!(prefix_1.as_bytes(), prefix_2.as_bytes());
assert_eq!(allocator.allocated_count(), 2);
let prefix_1_bytes = prefix_1.as_bytes().to_vec();
let rollable_extranonce_size = (32 - prefix_1_bytes.len()) as u16;
let mut channel = ExtendedChannel::new(
1,
"user_identity".to_string(),
prefix_1.into(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
rollable_extranonce_size,
None,
)
.unwrap();
channel
.on_new_extended_mining_job(active_job_template(1))
.unwrap();
assert_eq!(
channel.get_active_job().unwrap().extranonce_prefix,
prefix_1_bytes
);
channel.set_extranonce_prefix(prefix_2.into()).unwrap();
(allocator, channel, prefix_1_bytes)
}
#[test]
fn test_mixed_prefix_updates_reserve_slot_until_job_eviction() {
for future in [false, true] {
let mut allocator =
ExtranonceAllocator::from_upstream_prefix(vec![0xaa], vec![0xbb], 32, 2).unwrap();
let prefix = allocator.allocate_extended(8).unwrap();
let old_bytes = prefix.as_bytes().to_vec();
let rollable_size = (32 - prefix.len()) as u16;
let mut channel = ExtendedChannel::new(
1,
"user_identity".to_string(),
prefix.into(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
rollable_size,
Some(1),
)
.unwrap();
let job_id = 1;
let mut job = active_job_template(job_id);
if future {
job.min_ntime = Sv2Option::new(None);
}
channel.on_new_extended_mining_job(job).unwrap();
allocator.set_upstream_prefix(vec![0xcc]).unwrap();
channel.set_upstream_extranonce_prefix(&[0xcc]).unwrap();
assert_eq!(allocator.allocated_count(), 1);
assert_eq!(&channel.get_extranonce_prefix()[1..], &old_bytes[1..]);
for upstream in [0xdd, 0xaa, 0xcc, 0xcc] {
allocator.set_upstream_prefix(vec![upstream]).unwrap();
channel.set_upstream_extranonce_prefix(&[upstream]).unwrap();
assert_eq!(channel.retired_extranonce_prefixes.len(), 1);
}
let current_bytes = channel.get_extranonce_prefix().to_vec();
assert!(matches!(
channel.set_upstream_extranonce_prefix(&[0xee; 2]),
Err(ExtendedChannelError::NewExtranoncePrefixTooLarge)
));
assert_eq!(channel.get_extranonce_prefix(), current_bytes);
assert_eq!(channel.retired_extranonce_prefixes.len(), 1);
assert_eq!(allocator.allocated_count(), 1);
let replacement = allocator.allocate_extended(8).unwrap();
channel.set_extranonce_prefix(replacement.into()).unwrap();
let job = if future {
channel.get_future_job(job_id).unwrap()
} else {
channel.get_active_job().unwrap()
};
assert_eq!(job.extranonce_prefix, old_bytes);
assert_eq!(allocator.allocated_count(), 2);
allocator.set_upstream_prefix(vec![0xaa]).unwrap();
assert!(matches!(
allocator.allocate_extended(8),
Err(ExtranonceAllocatorError::CapacityExhausted)
));
if future {
channel
.on_set_new_prev_hash(SetNewPrevHashMp {
channel_id: 1,
job_id,
prev_hash: [2; 32].into(),
min_ntime: 1745596970,
nbits: 545259519,
})
.unwrap();
assert_eq!(
channel.get_active_job().unwrap().extranonce_prefix,
old_bytes
);
assert_eq!(allocator.allocated_count(), 2);
}
for job_id in 2..=3 {
channel
.on_new_extended_mining_job(active_job_template(job_id))
.unwrap();
}
assert!(channel.get_past_job(1).is_none());
assert_eq!(allocator.allocated_count(), 1);
let reused = allocator.allocate_extended(8).unwrap();
assert_eq!(reused.as_bytes(), old_bytes);
drop(reused);
drop(channel);
assert_eq!(allocator.allocated_count(), 0);
}
}
#[test]
fn test_rotated_extranonce_prefix_slot_not_reused_while_job_live() {
let (mut allocator, channel, prefix_1_bytes) =
extended_channel_with_rotated_extranonce_prefix();
assert_eq!(allocator.allocated_count(), 2);
assert!(matches!(
allocator.allocate_extended(8),
Err(ExtranonceAllocatorError::CapacityExhausted)
));
assert_eq!(
channel.get_active_job().unwrap().extranonce_prefix,
prefix_1_bytes
);
}
#[test]
fn test_retired_extranonce_prefix_released_after_jobs_go_stale() {
let (mut allocator, mut channel, _prefix_1_bytes) =
extended_channel_with_rotated_extranonce_prefix();
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,
];
channel
.on_chain_tip_update(ChainTip::new(prev_hash.into(), 545259519, 1745596980))
.unwrap();
assert!(channel.get_stale_job(1).is_some());
assert_eq!(allocator.allocated_count(), 1);
assert!(allocator.allocate_extended(8).is_ok());
}
#[test]
fn test_retired_extranonce_prefix_released_after_job_eviction() {
let (mut allocator, mut channel, _prefix_1_bytes) =
extended_channel_with_rotated_extranonce_prefix();
for job_id in 2..2 + MAX_PAST_JOBS as u32 + 2 {
channel
.on_new_extended_mining_job(active_job_template(job_id))
.unwrap();
}
assert!(channel.get_past_job(1).is_none());
assert_eq!(allocator.allocated_count(), 1);
assert!(allocator.allocate_extended(8).is_ok());
}
#[test]
fn test_retired_extranonce_prefix_survives_install_of_a_job_under_its_bytes() {
let mut allocator_1 = ExtranonceAllocator::new(vec![], 32, 1).unwrap();
let prefix_1 = allocator_1.allocate_extended(8).unwrap();
let mut allocator_2 = ExtranonceAllocator::new(vec![], 32, 1).unwrap();
let prefix_2 = allocator_2.allocate_extended(8).unwrap();
assert_eq!(prefix_1.as_bytes(), prefix_2.as_bytes());
let prefix_len = prefix_1.as_bytes().len();
let rollable_extranonce_size = (32 - prefix_len) as u16;
let mut channel = ExtendedChannel::new(
1,
"user_identity".to_string(),
prefix_1.into(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
rollable_extranonce_size,
Some(1),
)
.unwrap();
channel
.on_new_extended_mining_job(active_job_template(1))
.unwrap();
channel
.set_extranonce_prefix(ExtranoncePrefix::from_wire(vec![7; prefix_len]).unwrap())
.unwrap();
channel
.on_new_extended_mining_job(active_job_template(2))
.unwrap();
assert_eq!(allocator_1.allocated_count(), 1);
channel.set_extranonce_prefix(prefix_2.into()).unwrap();
channel
.on_new_extended_mining_job(active_job_template(3))
.unwrap();
assert!(channel.get_past_job(1).is_none());
assert_eq!(allocator_1.allocated_count(), 1);
assert!(matches!(
allocator_1.allocate_extended(8),
Err(ExtranonceAllocatorError::CapacityExhausted)
));
}
#[test]
fn test_zero_target_is_rejected() {
let channel_id = 1;
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 res = ExtendedChannel::new(
channel_id,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(extranonce_prefix.clone()).unwrap(),
Target::ZERO,
1.0,
true,
8u16,
None,
);
assert!(matches!(res, Err(ExtendedChannelError::InvalidTarget)));
let target = Target::from_le_bytes([0xff; 32]);
let mut channel = ExtendedChannel::new(
channel_id,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(),
target,
1.0,
true,
8u16,
None,
)
.unwrap();
let mut future_job = active_job_template(1);
future_job.min_ntime = Sv2Option::new(None);
channel.on_new_extended_mining_job(future_job).unwrap();
assert!(matches!(
channel.set_target(Target::ZERO),
Err(ExtendedChannelError::InvalidTarget)
));
assert_eq!(channel.get_target(), &target);
assert_eq!(channel.get_future_job(1).unwrap().target, target);
}
#[test]
fn test_immediately_active_job_below_chain_tip_min_ntime_is_rejected() {
let channel_id = 1;
let mut channel = ExtendedChannel::new(
channel_id,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(vec![
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
])
.unwrap(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
8u16,
None,
)
.unwrap();
let tip_ntime: u32 = 1745596970;
let mut future_job = active_job_template(1);
future_job.min_ntime = Sv2Option::new(None);
channel.on_new_extended_mining_job(future_job).unwrap();
channel
.on_set_new_prev_hash(SetNewPrevHashMp {
channel_id,
job_id: 1,
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: 453040064,
min_ntime: tip_ntime,
})
.unwrap();
let mut below_tip = active_job_template(2);
below_tip.min_ntime = Sv2Option::new(Some(tip_ntime - 1));
assert!(matches!(
channel.on_new_extended_mining_job(below_tip),
Err(ExtendedChannelError::JobMinNtimeBelowChainTip)
));
assert_eq!(channel.get_active_job().unwrap().job_message.job_id, 1);
assert_eq!(channel.get_past_jobs_count(), 0);
let mut at_tip = active_job_template(3);
at_tip.min_ntime = Sv2Option::new(Some(tip_ntime));
channel.on_new_extended_mining_job(at_tip).unwrap();
assert_eq!(channel.get_active_job().unwrap().job_message.job_id, 3);
}
#[test]
fn test_set_chain_tip_replacement_retires_the_jobs_of_the_previous_tip() {
let channel_id = 1;
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 mut channel = ExtendedChannel::new(
channel_id,
"user_identity".to_string(),
ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(),
Target::from_le_bytes([0xff; 32]),
1.0,
true,
8u16,
None,
)
.unwrap();
let nbits = 453040064;
let first_tip = ChainTip::new(
[
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,
1745596970,
);
channel.set_chain_tip(first_tip.clone());
channel
.on_new_extended_mining_job(active_job_template(1))
.unwrap();
let share = |sequence_number: u32, nonce: u32| SubmitSharesExtended {
channel_id,
sequence_number,
job_id: 1,
nonce,
ntime: 1745596970,
version: 536870912,
extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(),
};
assert!(matches!(
channel.validate_share(share(0, 0)),
Ok(ShareValidationResult::Valid(_))
));
channel.set_chain_tip(first_tip);
assert_eq!(channel.get_active_job().unwrap().job_message.job_id, 1);
assert!(matches!(
channel.validate_share(share(1, 1)),
Ok(ShareValidationResult::Valid(_))
));
channel.set_chain_tip(ChainTip::new(
[
154, 124, 239, 231, 221, 122, 160, 173, 164, 175, 87, 33, 74, 214, 191, 107, 73,
34, 0, 162, 227, 16, 44, 40, 33, 73, 0, 0, 0, 0, 0, 0,
]
.into(),
nbits,
1745596980,
));
assert!(channel.get_active_job().is_none());
assert!(channel.get_stale_job(1).is_some());
assert!(matches!(
channel.validate_share(share(2, 0)),
Err(ShareValidationError::Stale(_))
));
}
}