use crate::{
chain_tip::ChainTip,
merkle_root::merkle_root_from_path,
server::{
error::ExtendedChannelError,
jobs::{extended::ExtendedJob, factory::JobFactory, job_store::JobStore, JobOrigin},
share_accounting::{ShareAccounting, ShareValidationError, ShareValidationResult},
},
target::{bytes_to_hex, hash_rate_to_target, target_to_difficulty, u256_to_block_hash},
};
use binary_sv2::{self};
use bitcoin::{
blockdata::block::{Header, Version},
hashes::sha256d::Hash,
transaction::TxOut,
CompactTarget, Target as BitcoinTarget,
};
use mining_sv2::{SetCustomMiningJob, SubmitSharesExtended, Target, MAX_EXTRANONCE_LEN};
use std::{collections::HashMap, convert::TryInto};
use template_distribution_sv2::{NewTemplate, SetNewPrevHash as SetNewPrevHashTdp};
use tracing::debug;
#[derive(Debug)]
pub struct ExtendedChannel<'a> {
channel_id: u32,
user_identity: String,
extranonce_prefix: Vec<u8>,
rollable_extranonce_size: u16,
requested_max_target: Target,
target: Target, nominal_hashrate: f32,
job_store: Box<dyn JobStore<ExtendedJob<'a>>>,
job_factory: JobFactory,
share_accounting: ShareAccounting,
expected_share_per_minute: f32,
chain_tip: Option<ChainTip>,
}
impl<'a> ExtendedChannel<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
channel_id: u32,
user_identity: String,
extranonce_prefix: Vec<u8>,
max_target: Target,
nominal_hashrate: f32,
version_rolling_allowed: bool,
requested_min_rollable_extranonce_size: u16,
share_batch_size: usize,
expected_share_per_minute: f32,
job_store: Box<dyn JobStore<ExtendedJob<'a>>>,
) -> Result<Self, ExtendedChannelError> {
let target_u256 =
match hash_rate_to_target(nominal_hashrate.into(), expected_share_per_minute.into()) {
Ok(target_u256) => target_u256,
Err(_) => {
return Err(ExtendedChannelError::InvalidNominalHashrate);
}
};
let target: Target = target_u256.clone().into();
if target > max_target {
return Err(ExtendedChannelError::RequestedMaxTargetOutOfRange);
}
let available_rollable_extranonce_size =
(MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16;
if requested_min_rollable_extranonce_size > available_rollable_extranonce_size {
return Err(ExtendedChannelError::RequestedMinExtranonceSizeTooLarge);
}
Ok(Self {
channel_id,
user_identity,
extranonce_prefix,
rollable_extranonce_size: available_rollable_extranonce_size,
requested_max_target: max_target,
target,
nominal_hashrate,
job_store,
job_factory: JobFactory::new(version_rolling_allowed),
share_accounting: ShareAccounting::new(share_batch_size),
expected_share_per_minute,
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 get_chain_tip(&self) -> Option<&ChainTip> {
self.chain_tip.as_ref()
}
pub fn get_shares_per_minute(&self) -> f32 {
self.expected_share_per_minute
}
#[cfg(test)]
fn set_chain_tip(&mut self, chain_tip: ChainTip) {
self.chain_tip = Some(chain_tip);
}
pub fn set_extranonce_prefix(
&mut self,
extranonce_prefix: Vec<u8>,
) -> Result<(), ExtendedChannelError> {
let new_rollable_extranonce_size =
MAX_EXTRANONCE_LEN as u16 - extranonce_prefix.len() as u16;
if new_rollable_extranonce_size < self.rollable_extranonce_size {
return Err(ExtendedChannelError::NewExtranoncePrefixTooLarge);
}
self.extranonce_prefix = extranonce_prefix;
self.rollable_extranonce_size = new_rollable_extranonce_size;
Ok(())
}
pub fn get_rollable_extranonce_size(&self) -> u16 {
self.rollable_extranonce_size
}
pub fn get_requested_max_target(&self) -> &Target {
&self.requested_max_target
}
pub fn get_target(&self) -> &Target {
&self.target
}
pub fn set_target(&mut self, target: Target) {
self.target = target;
}
pub fn get_future_template_to_job_id(&self) -> &HashMap<u64, u32> {
self.job_store.get_future_template_to_job_id()
}
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 update_channel(
&mut self,
new_nominal_hashrate: f32,
requested_max_target: Option<Target>,
) -> Result<(), ExtendedChannelError> {
let target_u256 = match hash_rate_to_target(
new_nominal_hashrate.into(),
self.expected_share_per_minute.into(),
) {
Ok(target_u256) => target_u256,
Err(_) => {
return Err(ExtendedChannelError::InvalidNominalHashrate);
}
};
let requested_max_target = match requested_max_target {
Some(ref requested_max_target) => requested_max_target.clone(),
None => self.requested_max_target.clone(),
};
let mut target_bytes = target_u256.to_vec();
target_bytes.reverse(); let max_target_u256: binary_sv2::U256 = requested_max_target.clone().into();
let mut max_target_bytes = max_target_u256.to_vec();
max_target_bytes.reverse();
let old_target_u256: binary_sv2::U256 = self.target.clone().into();
let mut old_target_bytes = old_target_u256.to_vec();
old_target_bytes.reverse();
debug!(
"updating channel target \nold target:\t{}\nnew target:\t{}\nmax_target:\t{}",
bytes_to_hex(&old_target_bytes),
bytes_to_hex(&target_bytes),
bytes_to_hex(&max_target_bytes)
);
let new_target: Target = target_u256.into();
if new_target > requested_max_target {
return Err(ExtendedChannelError::RequestedMaxTargetOutOfRange);
}
self.nominal_hashrate = new_nominal_hashrate;
self.target = new_target;
self.requested_max_target = requested_max_target;
Ok(())
}
pub fn get_active_job(&self) -> Option<&ExtendedJob<'a>> {
self.job_store.get_active_job()
}
pub fn get_future_jobs(&self) -> &HashMap<u32, ExtendedJob<'a>> {
self.job_store.get_future_jobs()
}
pub fn get_past_jobs(&self) -> &HashMap<u32, ExtendedJob<'a>> {
self.job_store.get_past_jobs()
}
pub fn get_share_accounting(&self) -> &ShareAccounting {
&self.share_accounting
}
pub fn on_new_template(
&mut self,
template: NewTemplate<'a>,
coinbase_reward_outputs: Vec<TxOut>,
) -> Result<(), ExtendedChannelError> {
match template.future_template {
true => {
let new_job = self
.job_factory
.new_extended_job(
self.channel_id,
None,
self.extranonce_prefix.clone(),
template.clone(),
coinbase_reward_outputs,
)
.map_err(ExtendedChannelError::JobFactoryError)?;
self.job_store.add_future_job(template.template_id, new_job);
}
false => {
match self.chain_tip.clone() {
None => return Err(ExtendedChannelError::ChainTipNotSet),
Some(chain_tip) => {
let new_job = self
.job_factory
.new_extended_job(
self.channel_id,
Some(chain_tip),
self.extranonce_prefix.clone(),
template.clone(),
coinbase_reward_outputs,
)
.map_err(ExtendedChannelError::JobFactoryError)?;
self.job_store.add_active_job(new_job);
}
}
}
}
Ok(())
}
pub fn on_set_new_prev_hash(
&mut self,
set_new_prev_hash: SetNewPrevHashTdp<'a>,
) -> Result<(), ExtendedChannelError> {
match self.job_store.get_future_jobs().is_empty() {
true => {
return Err(ExtendedChannelError::TemplateIdNotFound);
}
false => {
self.job_store.activate_future_job(
set_new_prev_hash.template_id,
set_new_prev_hash.header_timestamp,
);
}
}
self.share_accounting.flush_seen_shares();
let set_new_prev_hash_static = set_new_prev_hash.into_static();
let new_chain_tip = ChainTip::new(
set_new_prev_hash_static.prev_hash,
set_new_prev_hash_static.n_bits,
set_new_prev_hash_static.header_timestamp,
);
self.chain_tip = Some(new_chain_tip);
Ok(())
}
pub fn on_set_custom_mining_job(
&mut self,
set_custom_mining_job: SetCustomMiningJob<'a>,
) -> Result<u32, ExtendedChannelError> {
let new_job = self
.job_factory
.new_custom_job(set_custom_mining_job, self.extranonce_prefix.clone())
.map_err(ExtendedChannelError::JobFactoryError)?;
let job_id = new_job.get_job_id();
self.job_store.add_active_job(new_job);
Ok(job_id)
}
pub fn validate_share(
&mut self,
share: SubmitSharesExtended,
) -> Result<ShareValidationResult, ShareValidationError> {
let job_id = share.job_id;
let is_active_job = self
.job_store
.get_active_job()
.is_some_and(|job| job.get_job_id() == job_id);
let is_past_job = self.job_store.get_past_jobs().contains_key(&job_id);
let is_stale_job = self.job_store.get_stale_jobs().contains_key(&job_id);
if is_stale_job {
return Err(ShareValidationError::Stale);
}
if !is_active_job && !is_past_job && !is_stale_job {
return Err(ShareValidationError::InvalidJobId);
}
let job = if is_active_job {
self.job_store
.get_active_job()
.expect("active job must exist")
} else if is_past_job {
self.job_store
.get_past_jobs()
.get(&job_id)
.expect("past job must exist")
} else {
self.job_store
.get_stale_jobs()
.get(&job_id)
.expect("stale job must exist")
};
let extranonce_prefix = job.get_extranonce_prefix();
let mut full_extranonce = vec![];
full_extranonce.extend(extranonce_prefix.clone());
full_extranonce.extend(share.extranonce.inner_as_ref());
let merkle_root: [u8; 32] = merkle_root_from_path(
job.get_coinbase_tx_prefix().inner_as_ref(),
job.get_coinbase_tx_suffix().inner_as_ref(),
full_extranonce.as_ref(),
&job.get_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::from_consensus(chain_tip.nbits());
if !job.version_rolling_allowed() {
if (share.version & 0x1fffe000) != 0 {
return Err(ShareValidationError::VersionRollingNotAllowed);
}
}
let header = Header {
version: Version::from_consensus(share.version as i32),
prev_blockhash: u256_to_block_hash(prev_hash.clone()),
merkle_root: (*Hash::from_bytes_ref(&merkle_root)).into(),
time: share.ntime,
bits: nbits,
nonce: share.nonce,
};
let hash = header.block_hash();
let raw_hash: [u8; 32] = *hash.to_raw_hash().as_ref();
let hash_as_target: Target = raw_hash.into();
let hash_as_diff = target_to_difficulty(hash_as_target.clone());
let network_target = BitcoinTarget::from_compact(nbits);
let hash_as_u256: binary_sv2::U256 = hash_as_target.clone().into();
let mut hash_bytes = hash_as_u256.to_vec();
hash_bytes.reverse(); let target_u256: binary_sv2::U256 = self.target.clone().into();
let mut target_bytes = target_u256.to_vec();
target_bytes.reverse();
debug!(
"share validation \nshare:\t\t{}\nchannel target:\t{}\nnetwork target:\t{}",
bytes_to_hex(&hash_bytes),
bytes_to_hex(&target_bytes),
format!("{:x}", network_target)
);
if network_target.is_met_by(hash) {
self.share_accounting.update_share_accounting(
target_to_difficulty(self.target.clone()) as u64,
share.sequence_number,
hash.to_raw_hash(),
);
let mut coinbase = vec![];
coinbase.extend(job.get_coinbase_tx_prefix().inner_as_ref());
coinbase.extend(full_extranonce);
coinbase.extend(job.get_coinbase_tx_suffix().inner_as_ref());
match job.get_origin() {
JobOrigin::NewTemplate(template) => {
let template_id = template.template_id;
return Ok(ShareValidationResult::BlockFound(
Some(template_id),
coinbase,
));
}
JobOrigin::SetCustomMiningJob(_set_custom_mining_job) => {
return Ok(ShareValidationResult::BlockFound(None, coinbase));
}
}
}
if hash_as_target <= self.target {
if self.share_accounting.is_share_seen(hash.to_raw_hash()) {
return Err(ShareValidationError::DuplicateShare);
}
self.share_accounting.update_share_accounting(
target_to_difficulty(self.target.clone()) as u64,
share.sequence_number,
hash.to_raw_hash(),
);
self.share_accounting.update_best_diff(hash_as_diff);
let last_sequence_number = self.share_accounting.get_last_share_sequence_number();
let new_submits_accepted_count = self.share_accounting.get_shares_accepted();
let new_shares_sum = self.share_accounting.get_share_work_sum();
if self.share_accounting.should_acknowledge() {
Ok(ShareValidationResult::ValidWithAcknowledgement(
last_sequence_number,
new_submits_accepted_count,
new_shares_sum,
))
} else {
Ok(ShareValidationResult::Valid)
}
} else {
Err(ShareValidationError::DoesNotMeetTarget)
}
}
}
#[cfg(test)]
mod tests {
use crate::{
chain_tip::ChainTip,
server::{
error::ExtendedChannelError,
extended::ExtendedChannel,
jobs::{job_store::DefaultJobStore, JobOrigin},
share_accounting::{ShareValidationError, ShareValidationResult},
},
};
use binary_sv2::Sv2Option;
use bitcoin::{transaction::TxOut, Amount, ScriptBuf};
use mining_sv2::{NewExtendedMiningJob, SubmitSharesExtended, Target, MAX_EXTRANONCE_LEN};
use std::convert::TryInto;
use template_distribution_sv2::{NewTemplate, SetNewPrevHash};
const SATS_AVAILABLE_IN_TEMPLATE: u64 = 5000000000;
#[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 max_target = [0xff; 32].into();
let expected_share_per_minute = 1.0;
let nominal_hashrate = 1.0;
let version_rolling_allowed = true;
let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16;
let share_batch_size = 100;
let job_store = Box::new(DefaultJobStore::new());
let mut channel = ExtendedChannel::new(
channel_id,
user_identity,
extranonce_prefix,
max_target,
nominal_hashrate,
version_rolling_allowed,
rollable_extranonce_size,
share_batch_size,
expected_share_per_minute,
job_store,
)
.unwrap();
let template = NewTemplate {
template_id: 1,
future_template: true,
version: 536870912,
coinbase_tx_version: 2,
coinbase_prefix: vec![82, 0].try_into().unwrap(),
coinbase_tx_input_sequence: 4294967295,
coinbase_tx_value_remaining: SATS_AVAILABLE_IN_TEMPLATE,
coinbase_tx_outputs_count: 1,
coinbase_tx_outputs: vec![
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,
]
.try_into()
.unwrap(),
coinbase_tx_locktime: 0,
merkle_path: vec![].try_into().unwrap(),
};
let pubkey_hash = [
235, 225, 183, 220, 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194,
8, 252,
];
let mut script_bytes = vec![0]; script_bytes.push(20); script_bytes.extend_from_slice(&pubkey_hash);
let script = ScriptBuf::from(script_bytes);
let coinbase_reward_outputs = vec![TxOut {
value: Amount::from_sat(SATS_AVAILABLE_IN_TEMPLATE),
script_pubkey: script,
}];
assert!(channel.get_future_jobs().is_empty());
channel
.on_new_template(template.clone(), coinbase_reward_outputs)
.unwrap();
assert!(channel.get_active_job().is_none());
let future_job_id = channel
.get_future_template_to_job_id()
.get(&template.template_id)
.unwrap();
let future_job = channel
.get_future_jobs()
.get(future_job_id)
.unwrap()
.clone();
let expected_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(),
};
assert_eq!(future_job.get_job_message(), &expected_job);
let ntime = 1746839905;
let set_new_prev_hash = SetNewPrevHash {
template_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(),
header_timestamp: ntime,
n_bits: 503543726,
target: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
174, 119, 3, 0, 0,
]
.into(),
};
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.activate(ntime);
let activated_job = channel.get_active_job().unwrap();
assert_eq!(
activated_job.get_job_message(),
previously_future_job.get_job_message()
);
}
#[test]
fn test_non_future_job_creation_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 max_target = [0xff; 32].into();
let expected_share_per_minute = 1.0;
let nominal_hashrate = 1.0;
let version_rolling_allowed = true;
let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16;
let share_batch_size = 100;
let job_store = Box::new(DefaultJobStore::new());
let mut channel = ExtendedChannel::new(
channel_id,
user_identity,
extranonce_prefix,
max_target,
nominal_hashrate,
version_rolling_allowed,
rollable_extranonce_size,
share_batch_size,
expected_share_per_minute,
job_store,
)
.unwrap();
let ntime = 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,
]
.into();
let n_bits = 503543726;
let chain_tip = ChainTip::new(prev_hash, n_bits, ntime);
channel.set_chain_tip(chain_tip);
let template = NewTemplate {
template_id: 1,
future_template: false,
version: 536870912,
coinbase_tx_version: 2,
coinbase_prefix: vec![82, 0].try_into().unwrap(),
coinbase_tx_input_sequence: 4294967295,
coinbase_tx_value_remaining: SATS_AVAILABLE_IN_TEMPLATE,
coinbase_tx_outputs_count: 1,
coinbase_tx_outputs: vec![
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,
]
.try_into()
.unwrap(),
coinbase_tx_locktime: 0,
merkle_path: vec![].try_into().unwrap(),
};
let pubkey_hash = [
235, 225, 183, 220, 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194,
8, 252,
];
let mut script_bytes = vec![0]; script_bytes.push(20); script_bytes.extend_from_slice(&pubkey_hash);
let script = ScriptBuf::from(script_bytes);
let coinbase_reward_outputs = vec![TxOut {
value: Amount::from_sat(SATS_AVAILABLE_IN_TEMPLATE),
script_pubkey: script,
}];
channel
.on_new_template(template.clone(), coinbase_reward_outputs)
.unwrap();
assert!(channel.get_future_jobs().is_empty());
let active_job = channel.get_active_job().unwrap().clone();
let expected_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, 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(),
};
assert_eq!(active_job.get_job_message(), &expected_job);
}
#[test]
fn test_custom_job_creation_flow() {
let jdc_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 max_target = [0xff; 32].into();
let expected_share_per_minute = 1.0;
let nominal_hashrate = 1.0;
let version_rolling_allowed = true;
let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16;
let share_batch_size = 100;
let job_store = Box::new(DefaultJobStore::new());
let mut jdc_extended_channel = ExtendedChannel::new(
jdc_channel_id,
user_identity,
extranonce_prefix,
max_target,
nominal_hashrate,
version_rolling_allowed,
rollable_extranonce_size,
share_batch_size,
expected_share_per_minute,
job_store,
)
.unwrap();
let template = NewTemplate {
template_id: 1,
future_template: true,
version: 536870912,
coinbase_tx_version: 2,
coinbase_prefix: vec![82, 0].try_into().unwrap(),
coinbase_tx_input_sequence: 4294967295,
coinbase_tx_value_remaining: SATS_AVAILABLE_IN_TEMPLATE,
coinbase_tx_outputs_count: 1,
coinbase_tx_outputs: vec![
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,
]
.try_into()
.unwrap(),
coinbase_tx_locktime: 0,
merkle_path: vec![].try_into().unwrap(),
};
let pubkey_hash = [
235, 225, 183, 220, 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194,
8, 252,
];
let mut script_bytes = vec![0]; script_bytes.push(20); script_bytes.extend_from_slice(&pubkey_hash);
let script = ScriptBuf::from(script_bytes);
let coinbase_reward_outputs = vec![TxOut {
value: Amount::from_sat(SATS_AVAILABLE_IN_TEMPLATE),
script_pubkey: script,
}];
jdc_extended_channel
.on_new_template(template.clone(), coinbase_reward_outputs.clone())
.unwrap();
let ntime = 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,
]
.into();
let n_bits = 503543726;
let chain_tip = ChainTip::new(prev_hash, n_bits, ntime);
let set_new_prev_hash = SetNewPrevHash {
template_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(),
header_timestamp: ntime,
n_bits: 503543726,
target: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
174, 119, 3, 0, 0,
]
.into(),
};
jdc_extended_channel
.on_set_new_prev_hash(set_new_prev_hash)
.unwrap();
let jdc_active_job = jdc_extended_channel.get_active_job().unwrap().clone();
let mining_job_token = vec![0].try_into().unwrap();
let set_custom_mining_job = jdc_active_job
.clone()
.into_custom_job(0, mining_job_token, chain_tip)
.unwrap();
let pool_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 max_target = [0xff; 32].into();
let expected_share_per_minute = 1.0;
let nominal_hashrate = 1.0;
let version_rolling_allowed = true;
let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16;
let share_batch_size = 100;
let job_store = Box::new(DefaultJobStore::new());
let mut pool_extended_channel = ExtendedChannel::new(
pool_channel_id,
user_identity,
extranonce_prefix.clone(),
max_target,
nominal_hashrate,
version_rolling_allowed,
rollable_extranonce_size,
share_batch_size,
expected_share_per_minute,
job_store,
)
.unwrap();
pool_extended_channel
.on_set_custom_mining_job(set_custom_mining_job.clone())
.unwrap();
let pool_active_job = pool_extended_channel.get_active_job().unwrap().clone();
assert_eq!(
pool_active_job.get_origin(),
&JobOrigin::SetCustomMiningJob(set_custom_mining_job)
);
assert_eq!(
pool_active_job.get_job_message(),
jdc_active_job.get_job_message()
);
assert_eq!(pool_active_job.get_extranonce_prefix(), &extranonce_prefix);
assert_eq!(
pool_active_job.get_coinbase_outputs(),
jdc_active_job.get_coinbase_outputs()
);
}
#[test]
fn test_coinbase_reward_outputs_sum_above_template_value() {
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 max_target = [0xff; 32].into();
let expected_share_per_minute = 1.0;
let nominal_hashrate = 1.0;
let version_rolling_allowed = true;
let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16;
let share_batch_size = 100;
let job_store = Box::new(DefaultJobStore::new());
let mut channel = ExtendedChannel::new(
channel_id,
user_identity,
extranonce_prefix,
max_target,
nominal_hashrate,
version_rolling_allowed,
rollable_extranonce_size,
share_batch_size,
expected_share_per_minute,
job_store,
)
.unwrap();
let template = NewTemplate {
template_id: 1,
future_template: true,
version: 536870912,
coinbase_tx_version: 2,
coinbase_prefix: vec![82, 0].try_into().unwrap(),
coinbase_tx_input_sequence: 4294967295,
coinbase_tx_value_remaining: SATS_AVAILABLE_IN_TEMPLATE,
coinbase_tx_outputs_count: 1,
coinbase_tx_outputs: vec![
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,
]
.try_into()
.unwrap(),
coinbase_tx_locktime: 0,
merkle_path: vec![].try_into().unwrap(),
};
let pubkey_hash = [
235, 225, 183, 220, 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194,
8, 252,
];
let mut script_bytes = vec![0]; script_bytes.push(20); script_bytes.extend_from_slice(&pubkey_hash);
let script = ScriptBuf::from(script_bytes);
let invalid_coinbase_reward_outputs = vec![TxOut {
value: Amount::from_sat(SATS_AVAILABLE_IN_TEMPLATE + 1),
script_pubkey: script,
}];
let res = channel.on_new_template(template.clone(), invalid_coinbase_reward_outputs);
assert!(res.is_err());
assert!(channel.get_future_jobs().is_empty());
}
#[test]
fn test_share_validation_block_found() {
let channel_id = 1;
let user_identity = "user_identity".to_string();
let extranonce_prefix = [
83, 116, 114, 97, 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 0,
0, 0, 0, 0, 0, 0, 1,
]
.to_vec();
let max_target = [0xff; 32].into();
let expected_share_per_minute = 1.0;
let nominal_hashrate = 1.0;
let version_rolling_allowed = true;
let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16;
let share_batch_size = 100;
let job_store = Box::new(DefaultJobStore::new());
let mut channel = ExtendedChannel::new(
channel_id,
user_identity,
extranonce_prefix,
max_target,
nominal_hashrate,
version_rolling_allowed,
rollable_extranonce_size,
share_batch_size,
expected_share_per_minute,
job_store,
)
.unwrap();
let template_id = 1;
let template = NewTemplate {
template_id,
future_template: false,
version: 536870912,
coinbase_tx_version: 2,
coinbase_prefix: vec![82, 0].try_into().unwrap(),
coinbase_tx_input_sequence: 4294967295,
coinbase_tx_value_remaining: SATS_AVAILABLE_IN_TEMPLATE,
coinbase_tx_outputs_count: 1,
coinbase_tx_outputs: vec![
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,
]
.try_into()
.unwrap(),
coinbase_tx_locktime: 0,
merkle_path: vec![].try_into().unwrap(),
};
let pubkey_hash = [
235, 225, 183, 220, 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194,
8, 252,
];
let mut script_bytes = vec![0]; script_bytes.push(20); script_bytes.extend_from_slice(&pubkey_hash);
let script = ScriptBuf::from(script_bytes);
let coinbase_reward_outputs = vec![TxOut {
value: Amount::from_sat(SATS_AVAILABLE_IN_TEMPLATE),
script_pubkey: script,
}];
let ntime = 1745596910;
let prev_hash = [
251, 175, 106, 40, 35, 87, 122, 90, 58, 51, 78, 32, 202, 236, 228, 36, 154, 174, 206,
144, 147, 195, 21, 224, 195, 103, 214, 189, 51, 190, 24, 98,
]
.into();
let n_bits = 545259519;
let chain_tip = ChainTip::new(prev_hash, n_bits, ntime);
channel.set_chain_tip(chain_tip);
channel
.on_new_template(template.clone(), coinbase_reward_outputs)
.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].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 = [
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 max_target = [0xff; 32].into();
let expected_share_per_minute = 1.0;
let nominal_hashrate = 100.0; let version_rolling_allowed = true;
let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16;
let share_batch_size = 100;
let job_store = Box::new(DefaultJobStore::new());
let mut channel = ExtendedChannel::new(
channel_id,
user_identity,
extranonce_prefix,
max_target,
nominal_hashrate,
version_rolling_allowed,
rollable_extranonce_size,
share_batch_size,
expected_share_per_minute,
job_store,
)
.unwrap();
let template_id = 1;
let template = NewTemplate {
template_id,
future_template: false,
version: 536870912,
coinbase_tx_version: 2,
coinbase_prefix: vec![82, 0].try_into().unwrap(),
coinbase_tx_input_sequence: 4294967295,
coinbase_tx_value_remaining: SATS_AVAILABLE_IN_TEMPLATE,
coinbase_tx_outputs_count: 1,
coinbase_tx_outputs: vec![
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,
]
.try_into()
.unwrap(),
coinbase_tx_locktime: 0,
merkle_path: vec![].try_into().unwrap(),
};
let pubkey_hash = [
235, 225, 183, 220, 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194,
8, 252,
];
let mut script_bytes = vec![0]; script_bytes.push(20); script_bytes.extend_from_slice(&pubkey_hash);
let script = ScriptBuf::from(script_bytes);
let coinbase_reward_outputs = vec![TxOut {
value: Amount::from_sat(SATS_AVAILABLE_IN_TEMPLATE),
script_pubkey: script,
}];
let ntime = 1745596910;
let 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();
let n_bits = 453040064;
let chain_tip = ChainTip::new(prev_hash, n_bits, ntime);
channel.set_chain_tip(chain_tip);
channel
.on_new_template(template.clone(), coinbase_reward_outputs)
.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].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 = [
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 max_target = [0xff; 32].into();
let expected_share_per_minute = 1.0;
let nominal_hashrate = 1_000.0; let version_rolling_allowed = true;
let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16;
let share_batch_size = 100;
let job_store = Box::new(DefaultJobStore::new());
let mut channel = ExtendedChannel::new(
channel_id,
user_identity,
extranonce_prefix,
max_target,
nominal_hashrate,
version_rolling_allowed,
rollable_extranonce_size,
share_batch_size,
expected_share_per_minute,
job_store,
)
.unwrap();
let template_id = 1;
let template = NewTemplate {
template_id,
future_template: false,
version: 536870912,
coinbase_tx_version: 2,
coinbase_prefix: vec![82, 0].try_into().unwrap(),
coinbase_tx_input_sequence: 4294967295,
coinbase_tx_value_remaining: SATS_AVAILABLE_IN_TEMPLATE,
coinbase_tx_outputs_count: 1,
coinbase_tx_outputs: vec![
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,
]
.try_into()
.unwrap(),
coinbase_tx_locktime: 0,
merkle_path: vec![].try_into().unwrap(),
};
let pubkey_hash = [
235, 225, 183, 220, 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194,
8, 252,
];
let mut script_bytes = vec![0]; script_bytes.push(20); script_bytes.extend_from_slice(&pubkey_hash);
let script = ScriptBuf::from(script_bytes);
let coinbase_reward_outputs = vec![TxOut {
value: Amount::from_sat(SATS_AVAILABLE_IN_TEMPLATE),
script_pubkey: script,
}];
let n_bits = 453040064;
let ntime = 1745611105;
let prev_hash = [
23, 205, 72, 134, 153, 86, 220, 153, 224, 28, 216, 146, 228, 120, 227, 157, 213, 99,
160, 163, 128, 59, 139, 190, 158, 62, 0, 0, 0, 0, 0, 0,
]
.into();
let chain_tip = ChainTip::new(prev_hash, n_bits, ntime);
channel.set_chain_tip(chain_tip);
channel
.on_new_template(template.clone(), coinbase_reward_outputs)
.unwrap();
let valid_share = SubmitSharesExtended {
channel_id,
sequence_number: 1,
job_id: 1,
nonce: 159386,
ntime: 1745611105,
version: 536870912,
extranonce: vec![1, 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: 2,
job_id: 1,
nonce: 159386,
ntime: 1745611105,
version: 536870912,
extranonce: vec![1, 0, 0, 0, 0].try_into().unwrap(),
};
let res = channel.validate_share(repeated_share);
assert!(matches!(res, Err(ShareValidationError::DuplicateShare)));
}
#[test]
fn test_update_channel() {
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 expected_share_per_minute = 1.0;
let initial_hashrate = 10.0;
let version_rolling_allowed = true;
let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16;
let share_batch_size = 100;
let job_store = Box::new(DefaultJobStore::new());
let max_target: Target = [0xff; 32].into();
let mut channel = ExtendedChannel::new(
channel_id,
user_identity,
extranonce_prefix,
max_target.clone(),
initial_hashrate,
version_rolling_allowed,
rollable_extranonce_size,
share_batch_size,
expected_share_per_minute,
job_store,
)
.unwrap();
let initial_target = channel.get_target().clone();
let new_hashrate = 100.0;
channel
.update_channel(new_hashrate, Some(max_target.clone()))
.unwrap();
let new_target = channel.get_target().clone();
assert_ne!(initial_target, new_target);
assert_eq!(channel.get_nominal_hashrate(), new_hashrate);
let result = channel.update_channel(-1.0, Some(max_target.clone()));
assert!(result.is_err());
assert!(matches!(
result,
Err(ExtendedChannelError::InvalidNominalHashrate)
));
let not_so_permissive_max_target: Target = [
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x00,
]
.into();
let very_small_hashrate = 0.1;
let result = channel.update_channel(
very_small_hashrate,
Some(not_so_permissive_max_target.clone()),
);
assert!(result.is_err());
assert!(matches!(
result,
Err(ExtendedChannelError::RequestedMaxTargetOutOfRange)
));
let sufficiently_big_hashrate = 1000.0;
let result = channel.update_channel(
sufficiently_big_hashrate,
Some(not_so_permissive_max_target),
);
assert!(result.is_ok());
}
#[test]
fn test_update_extranonce_prefix() {
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 max_target = [0xff; 32].into();
let expected_share_per_minute = 1.0;
let nominal_hashrate = 1_000.0;
let version_rolling_allowed = true;
let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16;
let share_batch_size = 100;
let job_store = Box::new(DefaultJobStore::new());
let mut channel = ExtendedChannel::new(
channel_id,
user_identity,
extranonce_prefix.clone(),
max_target,
nominal_hashrate,
version_rolling_allowed,
rollable_extranonce_size,
share_batch_size,
expected_share_per_minute,
job_store,
)
.unwrap();
let current_extranonce_prefix = channel.get_extranonce_prefix();
assert_eq!(current_extranonce_prefix, &extranonce_prefix);
let new_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, 2,
]
.to_vec();
channel
.set_extranonce_prefix(new_extranonce_prefix.clone())
.unwrap();
let current_extranonce_prefix = channel.get_extranonce_prefix();
assert_eq!(current_extranonce_prefix, &new_extranonce_prefix);
let new_extranonce_prefix_too_large = [
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, 2, 0,
]
.to_vec();
let result = channel.set_extranonce_prefix(new_extranonce_prefix_too_large.clone());
assert!(result.is_err());
assert!(matches!(
result,
Err(ExtendedChannelError::NewExtranoncePrefixTooLarge)
));
}
}