//! 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 { 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 = (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 = (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 = (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))); } }