use crate::constants::*;
use crate::error::{ConsensusError, Result};
use crate::types::*;
#[cfg(feature = "spec-lock")]
use blvm_spec_lock::spec_locked;
use sha2::{Digest, Sha256};
#[cfg_attr(feature = "spec-lock", spec_locked("7.1", "GetNextWorkRequired"))]
pub fn get_next_work_required(
_current_header: &BlockHeader,
prev_headers: &[BlockHeader],
) -> Result<Natural> {
get_next_work_required_internal(_current_header, prev_headers, false)
}
pub fn get_next_work_required_corrected(
_current_header: &BlockHeader,
prev_headers: &[BlockHeader],
) -> Result<Natural> {
get_next_work_required_internal(_current_header, prev_headers, true)
}
fn get_next_work_required_internal(
_current_header: &BlockHeader,
prev_headers: &[BlockHeader],
use_corrected: bool,
) -> Result<Natural> {
if prev_headers.len() < 2 {
return Err(ConsensusError::InvalidProofOfWork(
"Insufficient headers for difficulty adjustment".into(),
));
}
let last_header = &prev_headers[prev_headers.len() - 1];
let previous_bits = last_header.bits;
let first_timestamp = prev_headers[0].timestamp;
let last_timestamp = last_header.timestamp;
if last_timestamp < first_timestamp {
return Err(ConsensusError::InvalidProofOfWork(
"Invalid timestamp order in difficulty adjustment".into(),
));
}
let time_span = last_timestamp - first_timestamp;
let expected_time = if use_corrected {
let num_intervals = prev_headers.len() as u64;
if num_intervals == DIFFICULTY_ADJUSTMENT_INTERVAL {
(DIFFICULTY_ADJUSTMENT_INTERVAL - 1) * TARGET_TIME_PER_BLOCK
} else {
(num_intervals - 1) * TARGET_TIME_PER_BLOCK
}
} else {
DIFFICULTY_ADJUSTMENT_INTERVAL * TARGET_TIME_PER_BLOCK
};
let clamped_timespan = time_span.max(expected_time / 4).min(expected_time * 4);
debug_assert!(
clamped_timespan >= expected_time / 4,
"Clamped timespan ({}) must be >= expected_time/4 ({})",
clamped_timespan,
expected_time / 4
);
debug_assert!(
clamped_timespan <= expected_time * 4,
"Clamped timespan ({}) must be <= expected_time*4 ({})",
clamped_timespan,
expected_time * 4
);
let old_target = expand_target(previous_bits)?;
if old_target.is_zero() {
return Err(ConsensusError::InvalidProofOfWork(
"Previous block target is zero (invalid compact bits)".into(),
));
}
let multiplied_target = match old_target.checked_mul_u64(clamped_timespan) {
Some(t) => t,
None => {
return Ok(previous_bits);
}
};
debug_assert!(
multiplied_target >= old_target || clamped_timespan < expected_time,
"Multiplied target should be >= old target when timespan >= expected_time"
);
let new_target = multiplied_target.div_u64(expected_time);
if new_target.is_zero() {
return Err(ConsensusError::InvalidProofOfWork(
"Difficulty adjustment produced zero expanded target".into(),
));
}
let new_bits = compress_target(&new_target)?;
let clamped_bits = new_bits.min(MAX_TARGET as Natural);
debug_assert!(
clamped_bits > 0,
"Clamped bits ({clamped_bits}) must be positive"
);
debug_assert!(
clamped_bits <= MAX_TARGET as Natural,
"Clamped bits ({clamped_bits}) must be <= MAX_TARGET ({MAX_TARGET})"
);
if clamped_bits == 0 {
return Err(ConsensusError::InvalidProofOfWork(
"Difficulty adjustment resulted in zero target".into(),
));
}
Ok(clamped_bits)
}
struct PowParams {
limit_bits: u64,
interval: u64,
timespan: u64,
spacing: u64,
allow_min_difficulty: bool,
no_retargeting: bool,
enforce_bip94: bool,
}
fn pow_params(network: Network) -> PowParams {
let mainnet_timespan = DIFFICULTY_ADJUSTMENT_INTERVAL * TARGET_TIME_PER_BLOCK;
match network {
Network::Mainnet => PowParams {
limit_bits: MAX_TARGET as u64,
interval: DIFFICULTY_ADJUSTMENT_INTERVAL,
timespan: mainnet_timespan,
spacing: TARGET_TIME_PER_BLOCK,
allow_min_difficulty: false,
no_retargeting: false,
enforce_bip94: false,
},
Network::Testnet => PowParams {
limit_bits: MAX_TARGET as u64,
interval: DIFFICULTY_ADJUSTMENT_INTERVAL,
timespan: mainnet_timespan,
spacing: TARGET_TIME_PER_BLOCK,
allow_min_difficulty: true,
no_retargeting: false,
enforce_bip94: false,
},
Network::Testnet4 => PowParams {
limit_bits: MAX_TARGET as u64,
interval: DIFFICULTY_ADJUSTMENT_INTERVAL,
timespan: mainnet_timespan,
spacing: TARGET_TIME_PER_BLOCK,
allow_min_difficulty: true,
no_retargeting: false,
enforce_bip94: true,
},
Network::Signet => PowParams {
limit_bits: 0x1e0377ae,
interval: DIFFICULTY_ADJUSTMENT_INTERVAL,
timespan: mainnet_timespan,
spacing: TARGET_TIME_PER_BLOCK,
allow_min_difficulty: false,
no_retargeting: false,
enforce_bip94: false,
},
Network::Regtest => PowParams {
limit_bits: 0x207fffff,
interval: 144,
timespan: 24 * 60 * 60,
spacing: TARGET_TIME_PER_BLOCK,
allow_min_difficulty: true,
no_retargeting: true,
enforce_bip94: false,
},
}
}
pub fn next_required_bits(
network: Network,
height: u64,
block_timestamp: u64,
ancestor: &dyn Fn(u64) -> Option<(u64, u64)>,
) -> Result<Option<u64>> {
if height == 0 {
return Ok(None);
}
let params = pow_params(network);
let parent_height = height - 1;
let (parent_bits, parent_time) = ancestor(parent_height).ok_or_else(|| {
ConsensusError::InvalidProofOfWork(
format!("missing ancestor header at height {parent_height} for difficulty").into(),
)
})?;
if params.interval == 0 || height % params.interval != 0 {
if params.allow_min_difficulty {
let limit_time = parent_time.saturating_add(params.spacing.saturating_mul(2));
if block_timestamp > limit_time {
return Ok(Some(params.limit_bits));
}
let mut walk_height = parent_height;
let mut bits = parent_bits;
while walk_height > 0 && walk_height % params.interval != 0 && bits == params.limit_bits
{
walk_height -= 1;
let (prev_bits, _) = ancestor(walk_height).ok_or_else(|| {
ConsensusError::InvalidProofOfWork(
format!("missing ancestor header at height {walk_height} for difficulty")
.into(),
)
})?;
bits = prev_bits;
}
return Ok(Some(bits));
}
return Ok(Some(parent_bits));
}
if params.no_retargeting {
return Ok(Some(parent_bits));
}
let first_height = parent_height - (params.interval - 1);
let (first_bits, first_time) = ancestor(first_height).ok_or_else(|| {
ConsensusError::InvalidProofOfWork(
format!("missing ancestor header at height {first_height} for difficulty").into(),
)
})?;
let base_bits = if params.enforce_bip94 {
first_bits
} else {
parent_bits
};
Ok(Some(retarget_bits(
base_bits,
first_time,
parent_time,
params.timespan,
params.limit_bits,
)?))
}
fn retarget_bits(
prev_bits: u64,
first_time: u64,
last_time: u64,
timespan: u64,
limit_bits: u64,
) -> Result<u64> {
let actual = (last_time as i64).saturating_sub(first_time as i64);
let min_span = (timespan / 4) as i64;
let max_span = timespan.saturating_mul(4) as i64;
let span = if actual < min_span {
min_span
} else if actual > max_span {
max_span
} else {
actual
} as u64;
let old = expand_target(prev_bits)?;
if old.is_zero() {
return Ok(0);
}
let limit = expand_target(limit_bits)?;
let product = old.checked_mul_u64(span).ok_or_else(|| {
ConsensusError::InvalidProofOfWork("difficulty adjustment overflow".into())
})?;
let new_target = product.div_u64(timespan);
if new_target.is_zero() {
return Ok(0);
}
if new_target > limit {
return Ok(limit_bits);
}
compress_target(&new_target)
}
#[cfg_attr(feature = "spec-lock", spec_locked("7.2", "CheckProofOfWork"))]
#[cfg_attr(feature = "production", inline(always))]
#[cfg_attr(not(feature = "production"), inline)]
pub fn check_proof_of_work(header: &BlockHeader) -> Result<bool> {
let header_bytes = serialize_header(header);
let hash1 = Sha256::digest(header_bytes);
let hash2 = Sha256::digest(hash1);
let mut hash_bytes = [0u8; 32];
hash_bytes.copy_from_slice(&hash2);
proof_hash_meets_target(header.bits, &U256::from_bytes(&hash_bytes))
}
#[inline]
fn proof_hash_meets_target(bits: Natural, hash_value: &U256) -> Result<bool> {
if bits & 0x0080_0000 != 0 {
return Ok(false);
}
let target = expand_target(bits)?;
if target.is_zero() {
return Ok(false);
}
Ok(*hash_value <= target)
}
pub fn meet_regtest_proof_of_work(header: &mut BlockHeader) {
header.bits = 0x207f_ffff;
header.nonce = 0;
while !check_proof_of_work(header).unwrap_or(false) {
header.nonce = header.nonce.wrapping_add(1);
}
}
#[cfg_attr(feature = "spec-lock", spec_locked("7.2", "CheckProofOfWork"))]
#[cfg(feature = "production")]
pub fn batch_check_proof_of_work(headers: &[BlockHeader]) -> Result<Vec<(bool, Option<Hash>)>> {
use crate::optimizations::simd_vectorization;
if headers.is_empty() {
return Ok(Vec::new());
}
let header_bytes_vec: Vec<[u8; 80]> = {
#[cfg(feature = "rayon")]
{
use rayon::prelude::*;
headers.par_iter().map(serialize_header).collect()
}
#[cfg(not(feature = "rayon"))]
{
headers.iter().map(serialize_header).collect()
}
};
let header_refs: Vec<&[u8]> = header_bytes_vec.iter().map(|v| v.as_slice()).collect();
let aligned_hashes = simd_vectorization::batch_double_sha256_aligned(&header_refs);
let hashes: Vec<[u8; 32]> = aligned_hashes.iter().map(|h| *h.as_bytes()).collect();
let mut results = Vec::with_capacity(headers.len());
for (i, header) in headers.iter().enumerate() {
let hash = hashes[i];
let hash_value = U256::from_bytes(&hash);
match proof_hash_meets_target(header.bits, &hash_value) {
Ok(true) => results.push((true, Some(hash))),
Ok(false) => results.push((false, None)),
Err(_) => results.push((false, None)),
}
}
Ok(results)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct U256([u64; 4]);
impl U256 {
pub fn zero() -> Self {
U256([0; 4])
}
fn from_u32(value: u32) -> Self {
U256([value as u64, 0, 0, 0])
}
#[cfg(test)]
fn from_u64(value: u64) -> Self {
U256([value, 0, 0, 0])
}
fn get_low_64(&self) -> u64 {
self.0[0]
}
pub fn to_le_bytes(&self) -> [u8; 32] {
let mut bytes = [0u8; 32];
for (i, &word) in self.0.iter().enumerate() {
let word_bytes = word.to_le_bytes();
bytes[i * 8..(i + 1) * 8].copy_from_slice(&word_bytes);
}
bytes
}
pub fn from_le_bytes(bytes: [u8; 32]) -> Self {
let mut words = [0u64; 4];
for (i, word) in words.iter_mut().enumerate() {
let mut buf = [0u8; 8];
buf.copy_from_slice(&bytes[i * 8..(i + 1) * 8]);
*word = u64::from_le_bytes(buf);
}
U256(words)
}
pub fn gbt_target_hex(&self) -> String {
let mut bytes = self.to_le_bytes();
bytes.reverse();
hex::encode(bytes)
}
pub fn low_u128(&self) -> u128 {
self.0[0] as u128 | ((self.0[1] as u128) << 64)
}
pub fn from_u128(value: u128) -> Self {
U256([value as u64, (value >> 64) as u64, 0, 0])
}
pub fn one() -> Self {
U256([1, 0, 0, 0])
}
pub fn is_zero(&self) -> bool {
self.0.iter().all(|&w| w == 0)
}
#[allow(clippy::should_implement_trait)]
pub fn not(self) -> Self {
U256([!self.0[0], !self.0[1], !self.0[2], !self.0[3]])
}
pub fn checked_add(self, other: Self) -> Option<Self> {
let mut result = [0u64; 4];
let mut carry = 0u128;
for (i, word) in result.iter_mut().enumerate() {
let sum = self.0[i] as u128 + other.0[i] as u128 + carry;
*word = sum as u64;
carry = sum >> 64;
}
if carry != 0 { None } else { Some(U256(result)) }
}
pub fn saturating_add(self, other: Self) -> Self {
self.checked_add(other).unwrap_or(U256([u64::MAX; 4]))
}
fn sub(self, other: Self) -> Self {
let mut result = [0u64; 4];
let mut borrow = 0u8;
for (i, word) in result.iter_mut().enumerate() {
let mut minuend = self.0[i] as u128;
if borrow != 0 {
minuend += 1u128 << 64;
borrow = 0;
}
if minuend >= other.0[i] as u128 {
*word = (minuend - other.0[i] as u128) as u64;
} else {
*word = (minuend + (1u128 << 64) - other.0[i] as u128) as u64;
borrow = 1;
}
}
U256(result)
}
fn bit(self, bit: u32) -> bool {
(self.0[(bit / 64) as usize] >> (bit % 64)) & 1 == 1
}
fn with_bit_set(self, bit: u32) -> Self {
let mut result = self;
result.0[(bit / 64) as usize] |= 1u64 << (bit % 64);
result
}
#[allow(clippy::should_implement_trait)]
pub fn div(self, divisor: Self) -> Self {
if divisor.is_zero() {
return U256::zero();
}
if self < divisor {
return U256::zero();
}
let mut quotient = U256::zero();
let mut remainder = U256::zero();
for bit in (0..256).rev() {
remainder = remainder.shl(1);
if self.bit(bit) {
remainder = remainder
.checked_add(U256::one())
.unwrap_or(U256([u64::MAX; 4]));
}
if remainder >= divisor {
remainder = remainder.sub(divisor);
quotient = quotient.with_bit_set(bit);
}
}
quotient
}
pub fn to_be_bytes(self) -> [u8; 32] {
let mut bytes = self.to_le_bytes();
bytes.reverse();
bytes
}
pub fn from_be_bytes(bytes: &[u8; 32]) -> Self {
let mut le = *bytes;
le.reverse();
Self::from_bytes(&le)
}
#[cfg(test)]
fn to_bytes(&self) -> [u8; 32] {
self.to_le_bytes()
}
fn shl(&self, shift: u32) -> Self {
if shift >= 256 {
return U256::zero();
}
let mut result = U256::zero();
let word_shift = (shift / 64) as usize;
let bit_shift = shift % 64;
for i in 0..4 {
if i + word_shift < 4 {
result.0[i + word_shift] |= self.0[i] << bit_shift;
if bit_shift > 0 && i + word_shift + 1 < 4 {
result.0[i + word_shift + 1] |= self.0[i] >> (64 - bit_shift);
}
}
}
result
}
fn shr(&self, shift: u32) -> Self {
if shift >= 256 {
return U256::zero();
}
let mut result = U256::zero();
let word_shift = (shift / 64) as usize;
let bit_shift = shift % 64;
debug_assert!(
word_shift < 4,
"Word shift ({word_shift}) must be < 4 (shift: {shift})"
);
debug_assert!(
bit_shift < 64,
"Bit shift ({bit_shift}) must be < 64 (shift: {shift})"
);
if bit_shift == 0 {
for i in word_shift..4 {
result.0[i - word_shift] = self.0[i];
}
} else {
for i in word_shift..4 {
let mut word = self.0[i] >> bit_shift;
if i + 1 < 4 {
word |= self.0[i + 1] << (64 - bit_shift);
}
result.0[i - word_shift] = word;
}
}
result
}
fn from_bytes(bytes: &[u8; 32]) -> Self {
let mut words = [0u64; 4];
for (i, word) in words.iter_mut().enumerate() {
let start = i * 8;
let _end = start + 8;
*word = u64::from_le_bytes([
bytes[start],
bytes[start + 1],
bytes[start + 2],
bytes[start + 3],
bytes[start + 4],
bytes[start + 5],
bytes[start + 6],
bytes[start + 7],
]);
}
U256(words)
}
fn checked_mul_u64(&self, rhs: u64) -> Option<Self> {
let mut carry = 0u128;
let mut result = U256::zero();
#[cfg(feature = "production")]
{
let product = (self.0[0] as u128) * (rhs as u128) + carry;
result.0[0] = product as u64;
carry = product >> 64;
let product = (self.0[1] as u128) * (rhs as u128) + carry;
result.0[1] = product as u64;
carry = product >> 64;
let product = (self.0[2] as u128) * (rhs as u128) + carry;
result.0[2] = product as u64;
carry = product >> 64;
let product = (self.0[3] as u128) * (rhs as u128) + carry;
result.0[3] = product as u64;
carry = product >> 64;
if carry > 0 {
return None; }
}
#[cfg(not(feature = "production"))]
{
for i in 0..4 {
let product = (self.0[i] as u128) * (rhs as u128) + carry;
result.0[i] = product as u64;
carry = product >> 64;
if i == 3 && carry > 0 {
return None; }
}
}
Some(result)
}
fn div_u64(&self, rhs: u64) -> Self {
if rhs == 0 {
return U256([u64::MAX; 4]);
}
let mut remainder = 0u128;
let mut result = U256::zero();
#[cfg(feature = "production")]
{
let dividend = (remainder << 64) | (self.0[3] as u128);
let quotient = dividend / (rhs as u128);
remainder = dividend % (rhs as u128);
debug_assert!(quotient <= u64::MAX as u128, "Quotient must fit in u64");
result.0[3] = quotient as u64;
let dividend = (remainder << 64) | (self.0[2] as u128);
let quotient = dividend / (rhs as u128);
remainder = dividend % (rhs as u128);
debug_assert!(quotient <= u64::MAX as u128, "Quotient must fit in u64");
result.0[2] = quotient as u64;
let dividend = (remainder << 64) | (self.0[1] as u128);
let quotient = dividend / (rhs as u128);
remainder = dividend % (rhs as u128);
debug_assert!(quotient <= u64::MAX as u128, "Quotient must fit in u64");
result.0[1] = quotient as u64;
let dividend = (remainder << 64) | (self.0[0] as u128);
let quotient = dividend / (rhs as u128);
remainder = dividend % (rhs as u128);
debug_assert!(quotient <= u64::MAX as u128, "Quotient must fit in u64");
result.0[0] = quotient as u64;
}
#[cfg(not(feature = "production"))]
{
for i in (0..4).rev() {
let dividend = (remainder << 64) | (self.0[i] as u128);
let quotient = dividend / (rhs as u128);
remainder = dividend % (rhs as u128);
debug_assert!(
quotient <= u64::MAX as u128,
"Quotient ({quotient}) must fit in u64"
);
result.0[i] = quotient as u64;
}
}
debug_assert!(
result <= *self,
"Division result ({result:?}) must be <= dividend ({self:?})"
);
debug_assert!(
remainder < rhs as u128,
"Remainder ({remainder}) must be < divisor ({rhs})"
);
result
}
fn highest_set_bit(&self) -> Option<u32> {
for (i, &word) in self.0.iter().rev().enumerate() {
if word != 0 {
let word_index = (3 - i) as u32;
let bit_pos = word_index * 64 + (63 - word.leading_zeros());
return Some(bit_pos);
}
}
None
}
fn to_f64(self) -> f64 {
if self.is_zero() {
return 0.0;
}
let mut result = 0.0_f64;
result += self.0[0] as f64;
result += (self.0[1] as f64) * 2.0_f64.powi(64);
result += (self.0[2] as f64) * 2.0_f64.powi(128);
result += (self.0[3] as f64) * 2.0_f64.powi(192);
result
}
}
impl serde::Serialize for U256 {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
serializer.serialize_str(&self.gbt_target_hex())
}
}
impl<'de> serde::Deserialize<'de> for U256 {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Self, D::Error> {
let hex_str = <String as serde::Deserialize>::deserialize(deserializer)?;
if hex_str.len() != 64 {
return Err(serde::de::Error::custom("target hex must be 64 characters"));
}
let mut bytes = [0u8; 32];
for i in 0..32 {
bytes[i] = u8::from_str_radix(&hex_str[i * 2..i * 2 + 2], 16)
.map_err(serde::de::Error::custom)?;
}
bytes.reverse();
Ok(U256::from_le_bytes(bytes))
}
}
impl PartialOrd for U256 {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for U256 {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
for (a, b) in self.0.iter().rev().zip(other.0.iter().rev()) {
match a.cmp(b) {
std::cmp::Ordering::Equal => continue,
other => return other,
}
}
std::cmp::Ordering::Equal
}
}
pub fn difficulty_from_bits(bits: Natural) -> Result<f64> {
let target = expand_target(bits)?;
if target.is_zero() {
return Ok(1.0);
}
let max_target = U256::from_bytes(&[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0x00, 0x00, 0x00,
]);
let max_f64 = max_target.to_f64();
let target_f64 = target.to_f64();
if target_f64 == 0.0 {
return Ok(1.0);
}
Ok((max_f64 / target_f64).max(1.0))
}
#[cfg_attr(feature = "spec-lock", spec_locked("7.1", "ExpandTarget"))]
pub fn expand_target(bits: Natural) -> Result<U256> {
let exponent = (bits >> 24) as u8;
let mantissa = bits & 0x007fffff;
if !(3..=32).contains(&exponent) {
return Err(ConsensusError::InvalidProofOfWork(
"Invalid target exponent".into(),
));
}
if mantissa == 0 {
return Ok(U256::zero());
}
if exponent <= 3 {
let shift = 8 * (3 - exponent);
let mantissa_u256 = U256::from_u32(mantissa as u32);
Ok(mantissa_u256.shr(shift as u32))
} else {
let shift = 8u32 * (exponent as u32 - 3);
if shift >= 256 {
return Err(crate::error::ConsensusError::InvalidProofOfWork(
"Target too large".into(),
));
}
let mantissa_u256 = U256::from_u32(mantissa as u32);
Ok(mantissa_u256.shl(shift))
}
}
#[cfg_attr(feature = "spec-lock", spec_locked("11.3", "GetBlockProof"))]
pub fn get_block_proof(bits: Natural) -> Result<U256> {
let target = expand_target(bits)?;
if target.is_zero() {
return Ok(U256::zero());
}
let target_plus_one = target
.checked_add(U256::one())
.ok_or_else(|| ConsensusError::InvalidProofOfWork("Target overflow".into()))?;
let quotient = target.not().div(target_plus_one);
Ok(quotient
.checked_add(U256::one())
.unwrap_or(U256([u64::MAX; 4])))
}
#[cfg_attr(feature = "spec-lock", spec_locked("7.1", "CompressTarget"))]
pub(crate) fn compress_target(target: &U256) -> Result<Natural> {
if target.is_zero() {
return Ok(0x1d000000); }
let highest_bit = target
.highest_set_bit()
.ok_or_else(|| ConsensusError::InvalidProofOfWork("Cannot compress zero target".into()))?;
let n_size = (highest_bit + 1).div_ceil(8);
let mut n_compact: u64;
if n_size <= 3 {
let low_64 = target.get_low_64();
let shift_bytes = 3 - n_size;
n_compact = low_64 << (8 * shift_bytes);
} else {
let shift_bytes = n_size - 3;
let shifted = target.shr(shift_bytes * 8);
n_compact = shifted.get_low_64();
}
let mut n_size_final = n_size;
while (n_compact & 0x00800000) != 0 {
n_compact >>= 8;
n_size_final += 1;
}
let mantissa = (n_compact & 0x007fffff) as u32;
if n_size_final > 32 {
return Err(ConsensusError::InvalidProofOfWork(
format!("Target too large: exponent {n_size_final} exceeds maximum 32").into(),
));
}
let bits = (n_size_final << 24) | mantissa;
Ok(bits as Natural)
}
fn serialize_header(header: &BlockHeader) -> [u8; 80] {
let mut bytes = [0u8; 80];
bytes[0..4].copy_from_slice(&(header.version as u32).to_le_bytes());
bytes[4..36].copy_from_slice(&header.prev_block_hash);
bytes[36..68].copy_from_slice(&header.merkle_root);
bytes[68..72].copy_from_slice(&(header.timestamp as u32).to_le_bytes());
bytes[72..76].copy_from_slice(&(header.bits as u32).to_le_bytes());
bytes[76..80].copy_from_slice(&(header.nonce as u32).to_le_bytes());
bytes
}
#[cfg(test)]
fn u256_from_bytes(bytes: &[u8]) -> u128 {
let mut value = 0u128;
for (i, &byte) in bytes.iter().enumerate() {
if i < 16 {
value |= (byte as u128) << (8 * (15 - i));
}
}
value
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
fn compact_target_bytes(exponent: u8, mantissa: u32) -> [u8; 32] {
let mut out = [0u8; 32];
if mantissa == 0 || !(3..=32).contains(&exponent) {
return out;
}
let shift = (exponent as usize).saturating_sub(3);
for (i, byte) in mantissa.to_le_bytes().iter().enumerate() {
let pos = shift + i;
if pos < 32 {
out[pos] = *byte;
}
}
out
}
fn arb_block_header() -> impl Strategy<Value = BlockHeader> {
(
any::<i64>(),
any::<[u8; 32]>(),
any::<[u8; 32]>(),
any::<u64>(),
0x03000000u32..0x1d00ffffu32,
any::<u64>(),
)
.prop_map(
|(version, prev_block_hash, merkle_root, timestamp, bits, nonce)| BlockHeader {
version,
prev_block_hash,
merkle_root,
timestamp,
bits: bits as u64,
nonce,
},
)
}
proptest! {
#[test]
fn prop_expand_target_valid_range(
exponent in 0u8..=40,
mantissa in 0u32..=0x007f_ffff
) {
let bits = ((exponent as u64) << 24) | (mantissa as u64);
let result = expand_target(bits);
if !(3..=32).contains(&exponent) {
prop_assert!(result.is_err());
} else {
let expected = compact_target_bytes(exponent, mantissa);
prop_assert_eq!(result.unwrap().to_le_bytes(), expected);
let signed = bits | 0x0080_0000;
prop_assert_eq!(expand_target(signed).unwrap().to_le_bytes(), expected);
}
}
}
proptest! {
#[test]
fn prop_check_proof_of_work_deterministic(
header in arb_block_header()
) {
let result1 = check_proof_of_work(&header);
let result2 = check_proof_of_work(&header);
prop_assert_eq!(result1, result2);
}
}
proptest! {
#[test]
fn prop_get_next_work_required_bounds(
n in 0usize..6,
span in 0u64..5_000_000,
ordered in any::<bool>()
) {
let start = 1_700_000_000u64;
let mut headers: Vec<BlockHeader> = (0..n)
.map(|i| BlockHeader {
version: 1,
prev_block_hash: [0; 32],
merkle_root: [1; 32],
timestamp: start + (i as u64) * span,
bits: 0x1d00ffff,
nonce: 0,
})
.collect();
if n >= 2 && !ordered {
headers[n - 1].timestamp = start.saturating_sub(1);
}
let current = headers.first().cloned().unwrap_or(BlockHeader {
version: 1,
prev_block_hash: [0; 32],
merkle_root: [1; 32],
timestamp: start,
bits: 0x1d00ffff,
nonce: 0,
});
let result = get_next_work_required(¤t, &headers);
if n < 2 || !ordered {
prop_assert!(result.is_err());
} else {
let bits = result.unwrap();
prop_assert!(bits > 0);
prop_assert!(bits <= MAX_TARGET as Natural);
let next = expand_target(bits).unwrap();
let previous = expand_target(0x1d00ffff).unwrap();
prop_assert!(next <= previous);
}
}
}
}
#[cfg(test)]
#[path = "pow_tests.rs"]
mod tests;