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
443 lines
14 KiB
Rust
443 lines
14 KiB
Rust
//! 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)));
|
||
}
|
||
}
|