synor/crates/synor-consensus/src/difficulty.rs
Gulshan Yadav 5c643af64c fix: resolve all clippy warnings for CI
Fix all Rust clippy warnings that were causing CI failures when built
with RUSTFLAGS=-Dwarnings. Changes include:

- Replace derivable_impls with derive macros for BlockBody, Network, etc.
- Use div_ceil() instead of manual implementation
- Fix should_implement_trait by renaming from_str to parse
- Add type aliases for type_complexity warnings
- Use or_default(), is_some_and(), is_multiple_of() where appropriate
- Remove needless borrows and redundant closures
- Fix manual_strip with strip_prefix()
- Add allow attributes for intentional patterns (too_many_arguments,
  needless_range_loop in cryptographic code, assertions_on_constants)
- Remove unused imports, mut bindings, and dead code in tests
2026-01-08 05:58:22 +05:30

443 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Difficulty adjustment algorithm (DAA) for Synor.
//!
//! Synor uses a DAA similar to Kaspa, which adjusts difficulty every block
//! based on a weighted moving average of recent block times.
//!
//! Target: 10 blocks per second (100ms block time)
use synor_types::Hash256;
use thiserror::Error;
/// DAA parameters.
#[derive(Clone, Debug)]
pub struct DaaParams {
/// Target time between blocks (milliseconds).
pub target_time_ms: u64,
/// Number of blocks in the DAA window.
pub window_size: u64,
/// Maximum difficulty adjustment factor per block.
pub max_adjustment_factor: f64,
/// Minimum difficulty (for testing).
pub min_difficulty: u64,
}
impl Default for DaaParams {
fn default() -> Self {
DaaParams {
target_time_ms: crate::TARGET_BLOCK_TIME_MS,
window_size: 2641, // ~264 seconds of blocks at 10 bps
max_adjustment_factor: 4.0,
min_difficulty: 1,
}
}
}
impl DaaParams {
/// Creates parameters for testing.
pub fn for_testing() -> Self {
DaaParams {
target_time_ms: 1000, // 1 second for easier testing
window_size: 10,
max_adjustment_factor: 4.0,
min_difficulty: 1,
}
}
}
/// Block timestamp with DAA score.
#[derive(Clone, Copy, Debug)]
pub struct DaaBlock {
/// Timestamp in milliseconds.
pub timestamp: u64,
/// DAA score (cumulative count of blocks).
pub daa_score: u64,
/// Difficulty bits.
pub bits: u32,
}
/// Difficulty manager.
pub struct DifficultyManager {
/// DAA parameters.
params: DaaParams,
}
impl DifficultyManager {
/// Creates a new difficulty manager.
pub fn new(params: DaaParams) -> Self {
DifficultyManager { params }
}
/// Creates with default parameters.
pub fn with_defaults() -> Self {
Self::new(DaaParams::default())
}
/// Calculates the next difficulty target.
///
/// Takes a window of recent blocks and computes the adjusted difficulty.
pub fn calculate_next_difficulty(&self, window: &[DaaBlock]) -> Result<u32, DifficultyError> {
if window.is_empty() {
return Err(DifficultyError::EmptyWindow);
}
if window.len() == 1 {
// Not enough blocks, keep same difficulty
return Ok(window[0].bits);
}
// Get timestamps
let first = &window[0];
let last = &window[window.len() - 1];
// Calculate actual time taken
let actual_time = last.timestamp.saturating_sub(first.timestamp);
// Calculate expected time
let blocks_in_window = window.len() as u64 - 1;
let expected_time = blocks_in_window * self.params.target_time_ms;
if expected_time == 0 {
return Err(DifficultyError::InvalidWindow);
}
// Calculate adjustment ratio
let ratio = actual_time as f64 / expected_time as f64;
// Clamp ratio to prevent extreme adjustments
let clamped_ratio = ratio.clamp(
1.0 / self.params.max_adjustment_factor,
self.params.max_adjustment_factor,
);
// Current difficulty
let current_diff = self.bits_to_difficulty(last.bits);
// New difficulty = current * ratio
// (If blocks are too fast, ratio < 1, difficulty increases)
// (If blocks are too slow, ratio > 1, difficulty decreases)
let new_diff = (current_diff as f64 / clamped_ratio) as u64;
// Ensure minimum difficulty
let final_diff = new_diff.max(self.params.min_difficulty);
Ok(self.difficulty_to_bits(final_diff))
}
/// Converts difficulty bits to target hash.
pub fn bits_to_target(&self, bits: u32) -> Hash256 {
// Compact format: bits = exponent || coefficient
// Target = coefficient * 2^(8*(exponent-3))
let exponent = (bits >> 24) as u8;
let coefficient = bits & 0x00FFFFFF;
if exponent <= 3 {
let shifted = coefficient >> (8 * (3 - exponent as u32));
let mut bytes = [0u8; 32];
bytes[31] = shifted as u8;
bytes[30] = (shifted >> 8) as u8;
bytes[29] = (shifted >> 16) as u8;
return Hash256::from_bytes(bytes);
}
let byte_offset = (exponent as usize) - 3;
let mut bytes = [0u8; 32];
if byte_offset < 32 {
bytes[31 - byte_offset] = (coefficient & 0xFF) as u8;
if byte_offset + 1 < 32 {
bytes[31 - byte_offset - 1] = ((coefficient >> 8) & 0xFF) as u8;
}
if byte_offset + 2 < 32 {
bytes[31 - byte_offset - 2] = ((coefficient >> 16) & 0xFF) as u8;
}
}
Hash256::from_bytes(bytes)
}
/// Converts difficulty bits to numeric difficulty.
///
/// Uses Bitcoin's pool difficulty formula where 0x1d00ffff = difficulty 1.
/// Formula: difficulty = 0xffff × 256^(0x1d - exponent) / coefficient
pub fn bits_to_difficulty(&self, bits: u32) -> u64 {
let exponent = (bits >> 24) as i32;
let coefficient = (bits & 0x00FFFFFF) as u64;
if coefficient == 0 {
return u64::MAX;
}
// Reference point: 0x1d00ffff = difficulty 1
// difficulty = (REF_COEF / coefficient) × 256^(REF_EXP - exponent)
const REF_EXP: i32 = 0x1d; // 29
const REF_COEF: u64 = 0xffff; // 65535
let exp_diff = REF_EXP - exponent;
if exp_diff == 0 {
// Same exponent as reference
REF_COEF / coefficient
} else if exp_diff > 0 {
// Smaller target = higher difficulty
if exp_diff >= 8 {
// Would overflow, return very high difficulty
u64::MAX / coefficient
} else {
let scale = 256u64.pow(exp_diff as u32);
REF_COEF.saturating_mul(scale) / coefficient
}
} else {
// Larger target = lower difficulty (exp_diff < 0)
let abs_exp = (-exp_diff) as u32;
if abs_exp >= 8 {
// Very low difficulty
1
} else {
let scale = 256u64.pow(abs_exp);
REF_COEF / coefficient.saturating_mul(scale)
}
}
}
/// Converts numeric difficulty to bits.
///
/// Reverse of bits_to_difficulty. Finds exponent and coefficient such that
/// the resulting bits encodes approximately the given difficulty.
pub fn difficulty_to_bits(&self, difficulty: u64) -> u32 {
if difficulty == 0 {
return 0x207fffff; // Very easy (high target)
}
if difficulty == 1 {
return 0x1d00ffff; // Reference difficulty
}
const REF_EXP: i32 = 0x1d; // 29
const REF_COEF: u64 = 0xffff; // 65535
// We need: difficulty = REF_COEF × 256^(REF_EXP - exp) / coef
// Rearranging: coef = REF_COEF × 256^(REF_EXP - exp) / difficulty
let mut exp = REF_EXP;
let mut multiplier = 1u64;
// Calculate initial coefficient at reference exponent
let mut coef = REF_COEF / difficulty;
// If coefficient is 0, difficulty is too high - need to increase multiplier
if coef == 0 {
while coef == 0 && exp > 3 {
exp -= 1;
multiplier = multiplier.saturating_mul(256);
coef = REF_COEF.saturating_mul(multiplier) / difficulty;
}
}
// Normalize coefficient to valid range [1, 0x7fffff]
while coef > 0x7fffff && exp < 32 {
coef /= 256;
exp += 1;
}
// Clamp to valid ranges
coef = coef.clamp(1, 0x7fffff);
exp = exp.clamp(3, 32);
((exp as u32) << 24) | (coef as u32)
}
/// Returns the genesis difficulty bits.
pub fn genesis_bits() -> u32 {
// Low difficulty for easy initial mining
0x207fffff // Very low difficulty
}
/// Returns the minimum difficulty bits.
pub fn min_bits(&self) -> u32 {
self.difficulty_to_bits(self.params.min_difficulty)
}
/// Returns the parameters.
pub fn params(&self) -> &DaaParams {
&self.params
}
/// Validates that a block's PoW meets the target.
pub fn validate_pow(&self, block_hash: &Hash256, bits: u32) -> bool {
let target = self.bits_to_target(bits);
block_hash <= &target
}
/// Estimates hashrate from difficulty and block time.
pub fn estimate_hashrate(&self, bits: u32) -> f64 {
let difficulty = self.bits_to_difficulty(bits) as f64;
let block_time_seconds = self.params.target_time_ms as f64 / 1000.0;
// Hashrate ≈ difficulty * 2^32 / block_time
difficulty * 4_294_967_296.0 / block_time_seconds
}
}
impl Default for DifficultyManager {
fn default() -> Self {
Self::with_defaults()
}
}
impl std::fmt::Debug for DifficultyManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DifficultyManager")
.field("params", &self.params)
.finish()
}
}
/// Errors related to difficulty calculations.
#[derive(Debug, Error)]
pub enum DifficultyError {
#[error("Empty block window")]
EmptyWindow,
#[error("Invalid block window")]
InvalidWindow,
#[error("Difficulty overflow")]
Overflow,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bits_roundtrip() {
let manager = DifficultyManager::with_defaults();
// Test reference difficulty 1
let original_bits = 0x1d00ffff;
let difficulty = manager.bits_to_difficulty(original_bits);
assert_eq!(difficulty, 1);
let back_to_bits = manager.difficulty_to_bits(difficulty);
assert_eq!(back_to_bits, original_bits);
// Test higher difficulty (0x1c00ffff ≈ 256)
let harder_bits = 0x1c00ffff;
let harder_diff = manager.bits_to_difficulty(harder_bits);
assert_eq!(harder_diff, 256);
let back_to_harder = manager.difficulty_to_bits(harder_diff);
// May not be exact, but converted back should give same difficulty
let roundtrip_diff = manager.bits_to_difficulty(back_to_harder);
assert!(
(roundtrip_diff as i64 - harder_diff as i64).abs() < 10,
"Expected ~256, got {} after roundtrip",
roundtrip_diff
);
}
#[test]
fn test_genesis_bits() {
let bits = DifficultyManager::genesis_bits();
assert!(bits > 0);
}
#[test]
fn test_calculate_difficulty_stable() {
let params = DaaParams::for_testing();
let manager = DifficultyManager::new(params.clone());
// Use 0x1c00ffff (difficulty ~256) which has room to adjust both ways
let starting_bits = 0x1c00ffff;
// Create window where blocks came at exactly target rate
let window: Vec<DaaBlock> = (0..10)
.map(|i| DaaBlock {
timestamp: i * params.target_time_ms,
daa_score: i,
bits: starting_bits,
})
.collect();
let new_bits = manager.calculate_next_difficulty(&window).unwrap();
// Difficulty should stay similar (within max adjustment)
let old_diff = manager.bits_to_difficulty(starting_bits);
let new_diff = manager.bits_to_difficulty(new_bits);
let ratio = new_diff as f64 / old_diff as f64;
assert!(
ratio > 0.25 && ratio < 4.0,
"Expected ratio in (0.25, 4.0), got {} (old={}, new={})",
ratio,
old_diff,
new_diff
);
}
#[test]
fn test_calculate_difficulty_fast_blocks() {
let params = DaaParams::for_testing();
let manager = DifficultyManager::new(params.clone());
// Use 0x1c00ffff (difficulty ~256) which has room to adjust both ways
let starting_bits = 0x1c00ffff;
// Create window where blocks came too fast (half target time)
let window: Vec<DaaBlock> = (0..10)
.map(|i| DaaBlock {
timestamp: i * params.target_time_ms / 2,
daa_score: i,
bits: starting_bits,
})
.collect();
let new_bits = manager.calculate_next_difficulty(&window).unwrap();
// Difficulty should increase (bits should decrease)
let old_diff = manager.bits_to_difficulty(starting_bits);
let new_diff = manager.bits_to_difficulty(new_bits);
assert!(
new_diff > old_diff,
"Expected difficulty to increase: old={}, new={}",
old_diff,
new_diff
);
}
#[test]
fn test_calculate_difficulty_slow_blocks() {
let params = DaaParams::for_testing();
let manager = DifficultyManager::new(params.clone());
// Use 0x1c00ffff (difficulty ~256) which has room to decrease
let starting_bits = 0x1c00ffff;
// Create window where blocks came too slow (double target time)
let window: Vec<DaaBlock> = (0..10)
.map(|i| DaaBlock {
timestamp: i * params.target_time_ms * 2,
daa_score: i,
bits: starting_bits,
})
.collect();
let new_bits = manager.calculate_next_difficulty(&window).unwrap();
// Difficulty should decrease (bits should increase)
let old_diff = manager.bits_to_difficulty(starting_bits);
let new_diff = manager.bits_to_difficulty(new_bits);
assert!(
new_diff < old_diff,
"Expected difficulty to decrease: old={}, new={}",
old_diff,
new_diff
);
}
#[test]
fn test_empty_window_error() {
let manager = DifficultyManager::with_defaults();
let result = manager.calculate_next_difficulty(&[]);
assert!(matches!(result, Err(DifficultyError::EmptyWindow)));
}
}