diff --git a/Cargo.toml b/Cargo.toml index c46b08eb..af48f769 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,3 +34,5 @@ codegen-units = 1 [profile.dev] opt-level = 0 debug = true + +[workspace.metadata.dev-tools] diff --git a/lib-blockchain/docs/api-reference.md b/lib-blockchain/docs/api-reference.md index 4f1519c1..aa66a53e 100644 --- a/lib-blockchain/docs/api-reference.md +++ b/lib-blockchain/docs/api-reference.md @@ -757,7 +757,7 @@ pub const INITIAL_DIFFICULTY: u64 = 0x00000FFF; pub const MAX_BLOCK_SIZE: usize = 1_048_576; // 1MB pub const MAX_TRANSACTIONS_PER_BLOCK: usize = 1000; pub const TARGET_BLOCK_TIME: u64 = 10; // seconds -pub const DIFFICULTY_ADJUSTMENT_INTERVAL: u64 = 2016; // blocks +pub const DIFFICULTY_ADJUSTMENT_INTERVAL: u64 = 2016; // blocks (fallback if consensus coordinator unavailable) ``` ### Contract Constants diff --git a/lib-blockchain/docs/architecture.md b/lib-blockchain/docs/architecture.md index 7119c76f..c234e22b 100644 --- a/lib-blockchain/docs/architecture.md +++ b/lib-blockchain/docs/architecture.md @@ -195,6 +195,7 @@ Coordinates with lib-consensus for: - **Validator Management**: Register and manage blockchain validators - **Block Production**: Coordinate block proposal and validation +- **Difficulty Management**: Coordinate difficulty adjustment and target calculation - **DAO Governance**: On-chain governance with proposal and voting - **Reward Distribution**: Distribute consensus rewards to participants - **Byzantine Fault Tolerance**: Handle malicious validators and network partitions diff --git a/lib-blockchain/src/blockchain.rs b/lib-blockchain/src/blockchain.rs index e2ab9720..e6ecb229 100644 --- a/lib-blockchain/src/blockchain.rs +++ b/lib-blockchain/src/blockchain.rs @@ -783,34 +783,117 @@ impl Blockchain { crate::types::hash::blake3_hash(&data) } - /// Adjust mining difficulty based on block times + /// Adjust mining difficulty based on block times. + /// + /// This method delegates to the consensus coordinator's DifficultyManager when available, + /// falling back to the legacy hardcoded constants for backward compatibility. + /// + /// The consensus engine owns the difficulty policy per architectural design. fn adjust_difficulty(&mut self) -> Result<()> { - if self.height % crate::DIFFICULTY_ADJUSTMENT_INTERVAL != 0 { + // Get adjustment parameters from consensus coordinator if available + let (adjustment_interval, target_timespan) = if let Some(coordinator) = &self.consensus_coordinator { + // Use a single tokio block_in_place to call async methods from sync context + // Acquire the coordinator read lock once to avoid race conditions and redundant locking + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let coord = coordinator.read().await; + let interval = coord.get_difficulty_adjustment_interval().await; + let config = coord.get_difficulty_config().await; + (interval, config.target_timespan) + }) + }) + } else { + // Fallback to hardcoded constants for backward compatibility + (crate::DIFFICULTY_ADJUSTMENT_INTERVAL, crate::TARGET_TIMESPAN) + }; + + // Check if we should adjust at this height + if self.height % adjustment_interval != 0 { return Ok(()); } - if self.height < crate::DIFFICULTY_ADJUSTMENT_INTERVAL { + if self.height < adjustment_interval { return Ok(()); } let current_block = &self.blocks[self.height as usize]; - let interval_start = &self.blocks[(self.height - crate::DIFFICULTY_ADJUSTMENT_INTERVAL) as usize]; - - let actual_timespan = current_block.timestamp() - interval_start.timestamp(); - let actual_timespan = actual_timespan.max(crate::TARGET_TIMESPAN / 4).min(crate::TARGET_TIMESPAN * 4); + let interval_start = &self.blocks[(self.height - adjustment_interval) as usize]; + + let interval_start_time = interval_start.timestamp(); + let interval_end_time = current_block.timestamp(); + + // Calculate new difficulty using consensus coordinator if available + let new_difficulty_bits = if let Some(coordinator) = &self.consensus_coordinator { + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let coord = coordinator.read().await; + coord.calculate_difficulty_adjustment( + self.height, + self.difficulty.bits(), + interval_start_time, + interval_end_time, + ).await + }) + }); + + match result { + Ok(Some(new_bits)) => new_bits, + Ok(None) => return Ok(()), // No adjustment needed + Err(e) => { + tracing::warn!("Difficulty adjustment via coordinator failed: {}, using fallback", e); + // Fallback to legacy calculation + self.calculate_difficulty_legacy(interval_start_time, interval_end_time, target_timespan) + } + } + } else { + // Legacy calculation without coordinator + self.calculate_difficulty_legacy(interval_start_time, interval_end_time, target_timespan) + }; - let new_difficulty_bits = (self.difficulty.bits() as u64 * crate::TARGET_TIMESPAN / actual_timespan) as u32; + let old_difficulty = self.difficulty.bits(); self.difficulty = Difficulty::from_bits(new_difficulty_bits); tracing::info!( "Difficulty adjusted from {} to {} at height {}", - self.difficulty.bits(), + old_difficulty, new_difficulty_bits, self.height ); Ok(()) } + + /// Legacy difficulty calculation using hardcoded constants. + /// Used when consensus coordinator is not available. + fn calculate_difficulty_legacy(&self, interval_start_time: u64, interval_end_time: u64, target_timespan: u64) -> u32 { + // Defensive check: target_timespan should be validated to be non-zero upstream, + // but avoid panicking here if that validation is ever bypassed. + if target_timespan == 0 { + tracing::warn!( + "calculate_difficulty_legacy called with target_timespan = 0; \ + returning current difficulty without adjustment" + ); + return self.difficulty.bits(); + } + + let actual_timespan = interval_end_time.saturating_sub(interval_start_time); + // Clamp to prevent extreme adjustments (4x range) + let actual_timespan = actual_timespan + .max(target_timespan / 4) + .min(target_timespan * 4); + + // Additional defensive check in case clamping still results in zero + // (can happen if target_timespan / 4 == 0 due to integer division with small values) + if actual_timespan == 0 { + tracing::warn!( + "calculate_difficulty_legacy computed actual_timespan = 0 after clamping; \ + returning current difficulty without adjustment" + ); + return self.difficulty.bits(); + } + + (self.difficulty.bits() as u64 * target_timespan / actual_timespan) as u32 + } /// Get the latest block pub fn latest_block(&self) -> Option<&Block> { diff --git a/lib-blockchain/src/integration/consensus_integration.rs b/lib-blockchain/src/integration/consensus_integration.rs index fa451e43..d0619366 100644 --- a/lib-blockchain/src/integration/consensus_integration.rs +++ b/lib-blockchain/src/integration/consensus_integration.rs @@ -16,7 +16,8 @@ use lib_consensus::{ DaoEngine, DaoProposalType, DaoVoteChoice, RewardCalculator, RewardRound, ConsensusProposal, ConsensusVote, VoteType, ConsensusStep, - ConsensusType, ConsensusProof, NoOpBroadcaster + ConsensusType, ConsensusProof, NoOpBroadcaster, + DifficultyConfig, DifficultyManager, }; use lib_crypto::{Hash, hash_blake3, KeyPair}; use lib_identity::IdentityId; @@ -80,6 +81,8 @@ pub struct BlockchainConsensusCoordinator { pending_proposals: Arc>>, /// Active consensus votes active_votes: Arc>>>, + /// Difficulty manager (owns difficulty adjustment policy) + difficulty_manager: Arc>, } // Manual Debug implementation because ConsensusEngine doesn't derive Debug @@ -107,6 +110,9 @@ impl BlockchainConsensusCoordinator { )); let (event_sender, event_receiver) = mpsc::unbounded_channel(); + + // Initialize difficulty manager with default configuration + let difficulty_manager = Arc::new(RwLock::new(DifficultyManager::default())); Ok(Self { consensus_engine, @@ -119,6 +125,38 @@ impl BlockchainConsensusCoordinator { current_round_cache: Arc::new(RwLock::new(None)), pending_proposals: Arc::new(RwLock::new(VecDeque::new())), active_votes: Arc::new(RwLock::new(HashMap::new())), + difficulty_manager, + }) + } + + /// Create a new blockchain consensus coordinator with custom difficulty configuration + pub async fn new_with_difficulty_config( + blockchain: Arc>, + mempool: Arc>, + consensus_config: ConsensusConfig, + difficulty_config: DifficultyConfig, + ) -> Result { + let consensus_engine = Arc::new(RwLock::new( + ConsensusEngine::new(consensus_config, Arc::new(NoOpBroadcaster))? + )); + + let (event_sender, event_receiver) = mpsc::unbounded_channel(); + + // Initialize difficulty manager with provided configuration + let difficulty_manager = Arc::new(RwLock::new(DifficultyManager::new(difficulty_config))); + + Ok(Self { + consensus_engine, + blockchain, + mempool, + local_validator_id: None, + event_sender, + event_receiver: Arc::new(RwLock::new(event_receiver)), + is_producing_blocks: false, + current_round_cache: Arc::new(RwLock::new(None)), + pending_proposals: Arc::new(RwLock::new(VecDeque::new())), + active_votes: Arc::new(RwLock::new(HashMap::new())), + difficulty_manager, }) } @@ -212,8 +250,68 @@ impl BlockchainConsensusCoordinator { current_round_cache: self.current_round_cache.clone(), pending_proposals: self.pending_proposals.clone(), active_votes: self.active_votes.clone(), + difficulty_manager: self.difficulty_manager.clone(), } } + + /// Get the difficulty manager + pub fn difficulty_manager(&self) -> &Arc> { + &self.difficulty_manager + } + + /// Get the current difficulty configuration + pub async fn get_difficulty_config(&self) -> DifficultyConfig { + let manager = self.difficulty_manager.read().await; + manager.config().clone() + } + + /// Calculate new difficulty using the consensus-owned algorithm + /// + /// This is the entry point for blockchain difficulty adjustment. + /// The blockchain calls this method and the consensus engine owns the algorithm. + pub async fn calculate_difficulty_adjustment( + &self, + height: u64, + current_difficulty: u32, + interval_start_time: u64, + interval_end_time: u64, + ) -> Result> { + let manager = self.difficulty_manager.read().await; + manager + .adjust_difficulty(height, current_difficulty, interval_start_time, interval_end_time) + .map_err(|e| anyhow!("Difficulty adjustment failed: {}", e)) + } + + /// Check if difficulty should be adjusted at the given height + pub async fn should_adjust_difficulty(&self, height: u64) -> bool { + let manager = self.difficulty_manager.read().await; + manager.should_adjust(height) + } + + /// Get the initial difficulty value from consensus policy + pub async fn get_initial_difficulty(&self) -> u32 { + let manager = self.difficulty_manager.read().await; + manager.initial_difficulty() + } + + /// Get the difficulty adjustment interval from consensus policy + pub async fn get_difficulty_adjustment_interval(&self) -> u64 { + let manager = self.difficulty_manager.read().await; + manager.adjustment_interval() + } + + /// Apply DAO governance updates to difficulty parameters + pub async fn apply_difficulty_governance_update( + &self, + initial_difficulty: Option, + adjustment_interval: Option, + target_timespan: Option, + ) -> Result<()> { + let mut manager = self.difficulty_manager.write().await; + manager + .apply_governance_update(initial_difficulty, adjustment_interval, target_timespan) + .map_err(|e| anyhow!("Failed to apply difficulty governance update: {}", e)) + } /// Main consensus event processing loop async fn consensus_event_loop(&self) { @@ -1528,6 +1626,46 @@ pub async fn initialize_consensus_integration( Ok(coordinator) } +/// Initialize consensus integration with custom difficulty configuration +/// +/// This variant allows specifying a custom `DifficultyConfig` for the blockchain +/// mining difficulty adjustment policy. +pub async fn initialize_consensus_integration_with_difficulty_config( + blockchain: Arc>, + mempool: Arc>, + consensus_type: ConsensusType, + difficulty_config: DifficultyConfig, +) -> Result { + let consensus_config = ConsensusConfig { + consensus_type, + min_stake: 1000 * 1_000_000, // 1000 ZHTP minimum stake + min_storage: 100 * 1024 * 1024 * 1024, // 100 GB minimum storage + max_validators: 100, + block_time: 10, // 10 second blocks + epoch_length_blocks: 100, + propose_timeout: 3000, + prevote_timeout: 1000, + precommit_timeout: 1000, + max_transactions_per_block: 1000, + max_difficulty: 0x00000000FFFFFFFF, + target_difficulty: 0x00000FFF, + byzantine_threshold: 1.0 / 3.0, + slash_double_sign: 5, + slash_liveness: 1, + development_mode: false, // Production mode by default + }; + + let coordinator = BlockchainConsensusCoordinator::new_with_difficulty_config( + blockchain, + mempool, + consensus_config, + difficulty_config, + ).await?; + + info!("Consensus integration initialized with custom difficulty config"); + Ok(coordinator) +} + /// Create a DAO proposal transaction (delegated to consensus engine) pub fn create_dao_proposal_transaction( proposer_keypair: &KeyPair, diff --git a/lib-blockchain/src/lib.rs b/lib-blockchain/src/lib.rs index 76abbfe5..d14d8436 100644 --- a/lib-blockchain/src/lib.rs +++ b/lib-blockchain/src/lib.rs @@ -58,10 +58,14 @@ pub use integration::consensus_integration::{ BlockchainConsensusCoordinator, ConsensusStatus, initialize_consensus_integration, + initialize_consensus_integration_with_difficulty_config, create_dao_proposal_transaction, create_dao_vote_transaction, }; +// Re-export difficulty types from lib-consensus for convenience +pub use lib_consensus::{DifficultyConfig, DifficultyManager, DifficultyError, DifficultyResult}; + // Re-export contracts when feature is enabled #[cfg(feature = "contracts")] pub use contracts::*; diff --git a/lib-blockchain/tests/consensus_integration_tests.rs b/lib-blockchain/tests/consensus_integration_tests.rs index b0967a6b..16247036 100644 --- a/lib-blockchain/tests/consensus_integration_tests.rs +++ b/lib-blockchain/tests/consensus_integration_tests.rs @@ -258,3 +258,159 @@ async fn test_consensus_coordinator_lifecycle() { // Note: The coordinator might not immediately reflect the stopped state // in the status due to async nature, but the stop call should succeed } + +#[tokio::test] +async fn test_difficulty_manager_integration() { + use lib_consensus::difficulty::DifficultyConfig; + + let blockchain = Arc::new(RwLock::new(Blockchain::new().unwrap())); + let mempool = Arc::new(RwLock::new(Mempool::default())); + + let coordinator = initialize_consensus_integration( + blockchain, + mempool, + ConsensusType::ProofOfStake, + ).await.unwrap(); + + // Test getting default difficulty config + let config = coordinator.get_difficulty_config().await; + assert_eq!(config.initial_difficulty, 0x1d00ffff, "Should have Bitcoin-compatible initial difficulty"); + assert_eq!(config.adjustment_interval, 2016, "Should have Bitcoin-compatible adjustment interval"); + assert_eq!(config.target_timespan, 14 * 24 * 60 * 60, "Should have 2-week target timespan"); + + // Test getting adjustment interval + let interval = coordinator.get_difficulty_adjustment_interval().await; + assert_eq!(interval, 2016); + + // Test getting initial difficulty + let initial = coordinator.get_initial_difficulty().await; + assert_eq!(initial, 0x1d00ffff); + + // Test should_adjust at various heights + assert!(!coordinator.should_adjust_difficulty(0).await, "Should not adjust at height 0"); + assert!(!coordinator.should_adjust_difficulty(1000).await, "Should not adjust before first interval"); + assert!(coordinator.should_adjust_difficulty(2016).await, "Should adjust at adjustment_interval"); + assert!(!coordinator.should_adjust_difficulty(2017).await, "Should not adjust between intervals"); + assert!(coordinator.should_adjust_difficulty(4032).await, "Should adjust at 2x adjustment_interval"); +} + +#[tokio::test] +async fn test_difficulty_adjustment_calculation() { + let blockchain = Arc::new(RwLock::new(Blockchain::new().unwrap())); + let mempool = Arc::new(RwLock::new(Mempool::default())); + + let coordinator = initialize_consensus_integration( + blockchain, + mempool, + ConsensusType::ProofOfStake, + ).await.unwrap(); + + let current_difficulty = 0x1d00ffff; + let config = coordinator.get_difficulty_config().await; + let target_timespan = config.target_timespan; + + // Test adjustment at correct height with on-target timing + let result = coordinator.calculate_difficulty_adjustment( + 2016, // height at adjustment interval + current_difficulty, // current difficulty + 0, // interval start time + target_timespan, // interval end time (exactly on target) + ).await; + + assert!(result.is_ok(), "Difficulty adjustment should succeed"); + let new_difficulty = result.unwrap(); + assert!(new_difficulty.is_some(), "Should return new difficulty at adjustment height"); + + // With on-target timing, difficulty should stay approximately the same + let diff = new_difficulty.unwrap(); + let diff_delta = (diff as i64 - current_difficulty as i64).abs(); + assert!(diff_delta < 100, "Difficulty should not change much with on-target timing"); + + // Test no adjustment at non-adjustment height + let result = coordinator.calculate_difficulty_adjustment( + 1000, // not at adjustment interval + current_difficulty, + 0, + target_timespan, + ).await; + + assert!(result.is_ok()); + assert!(result.unwrap().is_none(), "Should return None at non-adjustment height"); +} + +#[tokio::test] +async fn test_difficulty_governance_update() { + let blockchain = Arc::new(RwLock::new(Blockchain::new().unwrap())); + let mempool = Arc::new(RwLock::new(Mempool::default())); + + let coordinator = initialize_consensus_integration( + blockchain, + mempool, + ConsensusType::ProofOfStake, + ).await.unwrap(); + + // Update adjustment interval via governance + let result = coordinator.apply_difficulty_governance_update( + None, // don't change initial_difficulty + Some(1000), // change adjustment_interval to 1000 + None, // don't change target_timespan + ).await; + + assert!(result.is_ok(), "Governance update should succeed"); + + let config = coordinator.get_difficulty_config().await; + assert_eq!(config.adjustment_interval, 1000, "Adjustment interval should be updated"); + assert_eq!(config.initial_difficulty, 0x1d00ffff, "Initial difficulty should be unchanged"); + + // Update target timespan + let result = coordinator.apply_difficulty_governance_update( + None, + None, + Some(604800), // 1 week in seconds + ).await; + + assert!(result.is_ok()); + let config = coordinator.get_difficulty_config().await; + assert_eq!(config.target_timespan, 604800); + + // Test invalid update (zero interval) should fail + let result = coordinator.apply_difficulty_governance_update( + None, + Some(0), // invalid: zero interval + None, + ).await; + + assert!(result.is_err(), "Should reject zero adjustment interval"); + + // Config should be unchanged after failed update + let config = coordinator.get_difficulty_config().await; + assert_eq!(config.adjustment_interval, 1000, "Config should be unchanged after failed update"); +} + +#[tokio::test] +async fn test_difficulty_manager_with_custom_config() { + use lib_consensus::difficulty::DifficultyConfig; + use lib_blockchain::initialize_consensus_integration_with_difficulty_config; + + let blockchain = Arc::new(RwLock::new(Blockchain::new().unwrap())); + let mempool = Arc::new(RwLock::new(Mempool::default())); + + // Create custom difficulty config + let custom_config = DifficultyConfig::new( + 0x1d00fffe, // custom initial difficulty + 100, // custom adjustment interval + 86400, // 1 day target timespan + ).unwrap(); + + let coordinator = initialize_consensus_integration_with_difficulty_config( + blockchain, + mempool, + ConsensusType::ProofOfStake, + custom_config, + ).await.unwrap(); + + let config = coordinator.get_difficulty_config().await; + assert_eq!(config.initial_difficulty, 0x1d00fffe); + assert_eq!(config.adjustment_interval, 100); + assert_eq!(config.target_timespan, 86400); +} diff --git a/lib-consensus/README.md b/lib-consensus/README.md index 7459d098..297bae3f 100644 --- a/lib-consensus/README.md +++ b/lib-consensus/README.md @@ -36,6 +36,13 @@ A modularized, multi-layered consensus system combining Proof of Stake, Proof of - **Delegation Support**: Stake delegation with commission rates - **UBI Integration**: Universal Basic Income distribution through governance +### Difficulty Management + +- **Consensus-Owned Policy**: Difficulty adjustment logic owned by consensus engine +- **Bitcoin-Compatible**: Proven difficulty adjustment algorithm +- **DAO Governable**: Parameters (interval, target timespan) modifiable via governance +- **Safety Bounds**: Built-in clamping to prevent extreme difficulty swings + ## Architecture ``` @@ -54,6 +61,7 @@ lib-consensus/ │ │ ├── proposals.rs │ │ ├── voting.rs │ │ └── treasury.rs +│ ├── difficulty.rs # Difficulty management │ ├── proofs/ # Cryptographic proof systems │ │ ├── work_proof.rs │ │ ├── stake_proof.rs diff --git a/lib-consensus/docs/api-reference.md b/lib-consensus/docs/api-reference.md index f3095769..af584dee 100644 --- a/lib-consensus/docs/api-reference.md +++ b/lib-consensus/docs/api-reference.md @@ -5,6 +5,7 @@ - [Consensus Engine](#consensus-engine) - [Validator Management](#validator-management) - [DAO Governance](#dao-governance) +- [Difficulty Management](#difficulty-management) - [Reward System](#reward-system) - [Byzantine Fault Detection](#byzantine-fault-detection) - [Proof Systems](#proof-systems) @@ -281,6 +282,8 @@ pub struct DaoEngine { **`get_dao_treasury() -> DaoTreasury`** - Gets DAO treasury state (deprecated - use blockchain methods) +> **Note:** For full documentation on Difficulty Management, see the [Difficulty Management](#difficulty-management) section below. + ### DaoProposal Represents a DAO proposal. @@ -324,6 +327,7 @@ pub struct GovernanceParameterUpdate { } pub enum GovernanceParameterValue { + // Consensus engine parameters MinStake(u64), MinStorage(u64), MaxValidators(u32), @@ -332,12 +336,16 @@ pub enum GovernanceParameterValue { PrevoteTimeout(u64), PrecommitTimeout(u64), MaxTransactionsPerBlock(u32), - MaxDifficulty(u64), - TargetDifficulty(u64), + MaxDifficulty(u64), // PoUW difficulty (not blockchain mining) + TargetDifficulty(u64), // PoUW difficulty (not blockchain mining) ByzantineThreshold(f64), SlashDoubleSign(u8), SlashLiveness(u8), DevelopmentMode(bool), + // Blockchain mining difficulty parameters (delegated to DifficultyManager) + BlockchainInitialDifficulty(u32), // Initial difficulty for genesis block + BlockchainAdjustmentInterval(u64), // Blocks between difficulty adjustments + BlockchainTargetTimespan(u64), // Target time for adjustment interval (seconds) } ``` @@ -358,6 +366,202 @@ pub struct DaoVote { } ``` +## Difficulty Management + +The consensus package owns the blockchain mining difficulty adjustment policy. This ensures that difficulty parameters can be governed via DAO proposals and maintains clear separation of concerns. + +### DifficultyManager + +Manages blockchain mining difficulty calculations and DAO governance updates. + +```rust +pub struct DifficultyManager { + config: DifficultyConfig, +} +``` + +#### Methods + +**`new(config: DifficultyConfig) -> Self`** +- Creates a new difficulty manager with the given configuration +- `config`: Initial difficulty configuration +- Returns: New DifficultyManager instance + +**`default() -> Self`** +- Creates a new difficulty manager with Bitcoin-compatible defaults +- Initial difficulty: `0x1d00ffff` +- Adjustment interval: `2016` blocks +- Target timespan: `1209600` seconds (2 weeks) + +**`config() -> &DifficultyConfig`** +- Gets the current difficulty configuration +- Returns: Reference to DifficultyConfig + +**`initial_difficulty() -> u32`** +- Gets the initial difficulty value +- Returns: Initial difficulty in Bitcoin compact format + +**`adjustment_interval() -> u64`** +- Gets the adjustment interval (blocks between difficulty adjustments) +- Returns: Number of blocks + +**`target_timespan() -> u64`** +- Gets the target timespan for adjustment intervals +- Returns: Target time in seconds + +**`should_adjust(height: u64) -> bool`** +- Checks if difficulty should be adjusted at the given block height +- `height`: Current blockchain height +- Returns: `true` if adjustment should occur + +**`calculate_new_difficulty(current_difficulty: u32, actual_timespan: u64) -> DifficultyResult`** +- Calculates new difficulty based on actual vs target timespan +- `current_difficulty`: Current difficulty in compact format +- `actual_timespan`: Actual time taken for the last interval (seconds) +- Returns: New difficulty value, clamped to prevent extreme changes (4x max) +- Algorithm: `new_difficulty = current * target_timespan / actual_timespan` + +**`adjust_difficulty(height: u64, current_difficulty: u32, interval_start_time: u64, interval_end_time: u64) -> DifficultyResult>`** +- Main entry point for difficulty adjustment +- `height`: Current blockchain height +- `current_difficulty`: Current difficulty value +- `interval_start_time`: Timestamp of block at start of interval +- `interval_end_time`: Timestamp of current block +- Returns: `Some(new_difficulty)` if adjustment occurred, `None` otherwise + +**`apply_governance_update(initial_difficulty: Option, adjustment_interval: Option, target_timespan: Option) -> DifficultyResult<()>`** +- Applies DAO governance updates to difficulty parameters +- All parameters are optional (only specified parameters are updated) +- Validates configuration before applying (no zero values, no invalid ranges) +- Returns: `Ok(())` if successful, `Err(...)` if validation fails + +**`set_min_difficulty(min_difficulty: u32) -> DifficultyResult<()>`** +- Sets the minimum difficulty bound +- `min_difficulty`: Minimum allowed difficulty +- Returns: Error if `min_difficulty > max_difficulty` + +**`set_max_difficulty(max_difficulty: u32) -> DifficultyResult<()>`** +- Sets the maximum difficulty bound +- `max_difficulty`: Maximum allowed difficulty +- Returns: Error if `max_difficulty < min_difficulty` + +**`set_max_adjustment_factor(factor: u64) -> DifficultyResult<()>`** +- Sets the maximum adjustment factor per interval +- `factor`: Maximum multiplier/divisor for difficulty changes +- Default: `4` (difficulty can at most quadruple or quarter per interval) + +### DifficultyConfig + +Configuration for blockchain difficulty adjustment. + +```rust +pub struct DifficultyConfig { + pub initial_difficulty: u32, + pub adjustment_interval: u64, + pub target_timespan: u64, + pub min_difficulty: u32, + pub max_difficulty: u32, + pub max_adjustment_factor: u64, +} +``` + +**Fields:** +- `initial_difficulty`: Initial difficulty for genesis block (Bitcoin compact format) +- `adjustment_interval`: Number of blocks between difficulty adjustments +- `target_timespan`: Target time for completing an adjustment interval (seconds) +- `min_difficulty`: Minimum allowed difficulty (default: `1`) +- `max_difficulty`: Maximum allowed difficulty (default: `0xFFFFFFFF`) +- `max_adjustment_factor`: Maximum change per interval (default: `4`) + +**Default Values (Bitcoin-compatible):** +```rust +DifficultyConfig { + initial_difficulty: 0x1d00ffff, // Bitcoin's initial difficulty + adjustment_interval: 2016, // 2016 blocks + target_timespan: 14 * 24 * 60 * 60, // 2 weeks in seconds + min_difficulty: 1, + max_difficulty: 0xFFFFFFFF, + max_adjustment_factor: 4, +} +``` + +#### Methods + +**`new(initial_difficulty: u32, adjustment_interval: u64, target_timespan: u64) -> DifficultyResult`** +- Creates a new difficulty configuration with custom values +- Validates all parameters before creation +- Returns: Error if any parameter is invalid (e.g., zero values) + +**`validate() -> DifficultyResult<()>`** +- Validates the configuration +- Checks: non-zero values, min <= max, valid ranges +- Returns: `Ok(())` if valid, `Err(...)` with specific validation error + +### DifficultyError + +Errors that can occur during difficulty operations. + +```rust +pub enum DifficultyError { + InvalidDifficulty(String), + InvalidConfig(String), + CalculationError(String), +} +``` + +### Integration with Blockchain + +The `BlockchainConsensusCoordinator` (in `lib-blockchain`) manages a `DifficultyManager` instance: + +```rust +// In BlockchainConsensusCoordinator +pub async fn get_difficulty_config(&self) -> DifficultyConfig { ... } +pub async fn calculate_difficulty_adjustment(...) -> Result> { ... } +pub async fn apply_difficulty_governance_update(...) -> Result<()> { ... } +``` + +The blockchain delegates difficulty calculations to the consensus coordinator: + +```rust +// In Blockchain::adjust_difficulty() +if let Some(coordinator) = &self.consensus_coordinator { + let new_difficulty = coordinator.calculate_difficulty_adjustment( + height, current_difficulty, start_time, end_time + ).await?; + // Apply new difficulty +} +``` + +### DAO Governance Integration + +Difficulty parameters can be updated via DAO proposals: + +```rust +// Create a DAO proposal to adjust difficulty parameters +let params = DaoExecutionParams { + action: DaoExecutionAction::GovernanceParameterUpdate( + GovernanceParameterUpdate { + updates: vec![ + GovernanceParameterValue::BlockchainInitialDifficulty(0x1d00fffe), + GovernanceParameterValue::BlockchainAdjustmentInterval(1000), + GovernanceParameterValue::BlockchainTargetTimespan(604800), // 1 week + ] + } + ) +}; + +// Submit proposal +let proposal_id = dao_engine.create_dao_proposal( + proposer, + "Adjust Mining Difficulty Parameters".to_string(), + "Reduce adjustment interval to 1000 blocks...".to_string(), + DaoProposalType::GovernanceRules, + 14, // 14 day voting period +)?; + +// After proposal passes, parameters are automatically applied to DifficultyManager +``` + ## Reward System ### RewardCalculator diff --git a/lib-consensus/src/dao/dao_engine.rs b/lib-consensus/src/dao/dao_engine.rs index 58ccf4f6..78f9fd01 100644 --- a/lib-consensus/src/dao/dao_engine.rs +++ b/lib-consensus/src/dao/dao_engine.rs @@ -259,6 +259,26 @@ impl DaoEngine { GovernanceParameterValue::SlashDoubleSign(value) => config.slash_double_sign = *value, GovernanceParameterValue::SlashLiveness(value) => config.slash_liveness = *value, GovernanceParameterValue::DevelopmentMode(value) => config.development_mode = *value, + // Blockchain difficulty parameters are handled by DifficultyManager, + // not ConsensusConfig. These parameters are validated here but applied + // separately through the following flow: + // + // 1. DAO proposal with BlockchainInitialDifficulty/AdjustmentInterval/TargetTimespan + // parameters is validated by validate_governance_update() + // 2. After proposal passes voting, execute_passed_proposal() is called + // 3. For blockchain difficulty params, the caller (typically node runtime) + // extracts these values from the passed proposal + // 4. Caller invokes BlockchainConsensusCoordinator::apply_difficulty_governance_update() + // which delegates to DifficultyManager::apply_governance_update() + // + // See: lib-blockchain/src/integration/consensus_integration.rs + // BlockchainConsensusCoordinator::apply_difficulty_governance_update() + GovernanceParameterValue::BlockchainInitialDifficulty(_) + | GovernanceParameterValue::BlockchainAdjustmentInterval(_) + | GovernanceParameterValue::BlockchainTargetTimespan(_) => { + // No-op here: these are applied via the DifficultyManager pathway + // described in the comment above, not via ConsensusConfig mutation + } } } @@ -338,6 +358,22 @@ impl DaoEngine { } } GovernanceParameterValue::DevelopmentMode(_) => {} + // Blockchain difficulty parameters validation + GovernanceParameterValue::BlockchainInitialDifficulty(value) => { + if *value == 0 { + return Err(anyhow::anyhow!("Initial difficulty must be greater than zero")); + } + } + GovernanceParameterValue::BlockchainAdjustmentInterval(value) => { + if *value == 0 { + return Err(anyhow::anyhow!("Adjustment interval must be greater than zero")); + } + } + GovernanceParameterValue::BlockchainTargetTimespan(value) => { + if *value == 0 { + return Err(anyhow::anyhow!("Target timespan must be greater than zero")); + } + } } } diff --git a/lib-consensus/src/dao/dao_types.rs b/lib-consensus/src/dao/dao_types.rs index ce84dd60..3908f674 100644 --- a/lib-consensus/src/dao/dao_types.rs +++ b/lib-consensus/src/dao/dao_types.rs @@ -78,6 +78,13 @@ pub enum GovernanceParameterValue { SlashDoubleSign(u8), SlashLiveness(u8), DevelopmentMode(bool), + // Blockchain difficulty parameters (owned by consensus, used by blockchain) + /// Initial difficulty for new chains (Bitcoin-style compact representation) + BlockchainInitialDifficulty(u32), + /// Number of blocks between difficulty adjustments + BlockchainAdjustmentInterval(u64), + /// Target time for difficulty adjustment interval (seconds) + BlockchainTargetTimespan(u64), } /// Types of DAO proposals diff --git a/lib-consensus/src/difficulty.rs b/lib-consensus/src/difficulty.rs new file mode 100644 index 00000000..ce006559 --- /dev/null +++ b/lib-consensus/src/difficulty.rs @@ -0,0 +1,637 @@ +//! Difficulty Management Module for ZHTP Consensus +//! +//! This module owns the blockchain difficulty adjustment policy, following the principle +//! "consensus engine should own difficulty policy". It provides: +//! +//! - `DifficultyConfig`: Configuration parameters for difficulty adjustment +//! - `DifficultyManager`: Manages difficulty calculations and DAO governance updates +//! +//! # Architecture +//! +//! The consensus layer owns the difficulty policy, while the blockchain layer +//! calls into this module when difficulty adjustments are needed. This enables: +//! +//! 1. DAO governance of difficulty parameters +//! 2. Clear separation of concerns +//! 3. Unified parameter management in lib-consensus + +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// Errors that can occur during difficulty operations +#[derive(Debug, Error)] +pub enum DifficultyError { + #[error("Invalid difficulty value: {0}")] + InvalidDifficulty(String), + + #[error("Invalid configuration: {0}")] + InvalidConfig(String), + + #[error("Adjustment calculation error: {0}")] + CalculationError(String), +} + +/// Result type for difficulty operations +pub type DifficultyResult = Result; + +/// Configuration for blockchain difficulty adjustment. +/// +/// These parameters control how mining difficulty adapts to network hashrate. +/// All parameters can be updated via DAO governance proposals. +/// +/// # Bitcoin-Style Algorithm +/// +/// The difficulty adjustment follows Bitcoin's algorithm: +/// - Adjust every `adjustment_interval` blocks +/// - Target `target_timespan` seconds for each interval +/// - Clamp adjustments to 4x range (prevent extreme changes) +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct DifficultyConfig { + /// Initial difficulty for genesis block (Bitcoin-style compact representation) + /// Default: 0x1d00ffff (Bitcoin's initial difficulty) + pub initial_difficulty: u32, + + /// Number of blocks between difficulty adjustments + /// Default: 2016 blocks (same as Bitcoin) + pub adjustment_interval: u64, + + /// Target time for completing an adjustment interval (in seconds) + /// Default: 1,209,600 seconds (2 weeks, same as Bitcoin) + pub target_timespan: u64, + + /// Minimum allowed difficulty (prevents difficulty from dropping too low) + /// Default: 1 (minimum possible difficulty) + pub min_difficulty: u32, + + /// Maximum allowed difficulty (prevents difficulty from going too high) + /// Default: 0xFFFFFFFF (maximum u32) + pub max_difficulty: u32, + + /// Maximum adjustment factor per interval (prevents extreme changes) + /// Default: 4 (difficulty can at most quadruple or quarter each interval) + pub max_adjustment_factor: u64, +} + +impl Default for DifficultyConfig { + fn default() -> Self { + Self { + initial_difficulty: 0x1d00ffff, // Bitcoin's initial difficulty + adjustment_interval: 2016, // 2016 blocks + target_timespan: 14 * 24 * 60 * 60, // 2 weeks in seconds + min_difficulty: 1, + max_difficulty: 0xFFFFFFFF, + max_adjustment_factor: 4, + } + } +} + +impl DifficultyConfig { + /// Create a new difficulty configuration with custom values + pub fn new( + initial_difficulty: u32, + adjustment_interval: u64, + target_timespan: u64, + ) -> DifficultyResult { + let config = Self { + initial_difficulty, + adjustment_interval, + target_timespan, + ..Default::default() + }; + config.validate()?; + Ok(config) + } + + /// Validate the configuration parameters + pub fn validate(&self) -> DifficultyResult<()> { + if self.adjustment_interval == 0 { + return Err(DifficultyError::InvalidConfig( + "Adjustment interval must be greater than zero".to_string(), + )); + } + + if self.target_timespan == 0 { + return Err(DifficultyError::InvalidConfig( + "Target timespan must be greater than zero".to_string(), + )); + } + + if self.initial_difficulty == 0 { + return Err(DifficultyError::InvalidConfig( + "Initial difficulty must be greater than zero".to_string(), + )); + } + + if self.min_difficulty > self.max_difficulty { + return Err(DifficultyError::InvalidConfig( + "Min difficulty cannot exceed max difficulty".to_string(), + )); + } + + if self.max_adjustment_factor == 0 { + return Err(DifficultyError::InvalidConfig( + "Max adjustment factor must be greater than zero".to_string(), + )); + } + + Ok(()) + } +} + +/// Manages blockchain difficulty calculations and DAO governance updates. +/// +/// This struct is the single source of truth for difficulty policy in the +/// ZHTP consensus system. It: +/// +/// 1. Stores the current difficulty configuration +/// 2. Calculates new difficulty values based on block timing +/// 3. Accepts configuration updates from DAO governance +/// +/// # Thread Safety +/// +/// `DifficultyManager` is `Send + Sync` and can be safely shared across threads. +/// When integrated with `BlockchainConsensusCoordinator`, it should be wrapped +/// in `Arc>` for concurrent access. +#[derive(Debug, Clone)] +pub struct DifficultyManager { + /// Current difficulty configuration + config: DifficultyConfig, +} + +impl Default for DifficultyManager { + fn default() -> Self { + Self::new(DifficultyConfig::default()) + } +} + +impl DifficultyManager { + /// Create a new difficulty manager with the given configuration + pub fn new(config: DifficultyConfig) -> Self { + Self { config } + } + + /// Get the current difficulty configuration + pub fn config(&self) -> &DifficultyConfig { + &self.config + } + + /// Get a mutable reference to the configuration (for internal updates). + /// + /// This is intentionally private to ensure all external updates go through + /// validated governance or setter methods that enforce configuration invariants. + fn config_mut(&mut self) -> &mut DifficultyConfig { + &mut self.config + } + + /// Get the initial difficulty value + pub fn initial_difficulty(&self) -> u32 { + self.config.initial_difficulty + } + + /// Get the adjustment interval + pub fn adjustment_interval(&self) -> u64 { + self.config.adjustment_interval + } + + /// Get the target timespan + pub fn target_timespan(&self) -> u64 { + self.config.target_timespan + } + + /// Check if difficulty should be adjusted at the given height + pub fn should_adjust(&self, height: u64) -> bool { + // Note: height >= adjustment_interval implies height > 0 + // since adjustment_interval is validated to be > 0 + height >= self.config.adjustment_interval + && height % self.config.adjustment_interval == 0 + } + + /// Calculate the new difficulty based on actual vs target timespan. + /// + /// # Algorithm + /// + /// 1. Clamp actual timespan to prevent extreme adjustments: + /// - Minimum: target_timespan / max_adjustment_factor + /// - Maximum: target_timespan * max_adjustment_factor + /// + /// 2. Calculate new difficulty: + /// new_difficulty = current_difficulty * target_timespan / clamped_actual_timespan + /// + /// 3. Clamp result to [min_difficulty, max_difficulty] + /// + /// # Arguments + /// + /// * `current_difficulty` - Current difficulty in compact (bits) format + /// * `actual_timespan` - Actual time taken for the last adjustment interval (seconds) + /// + /// # Returns + /// + /// The new difficulty value in compact (bits) format + pub fn calculate_new_difficulty( + &self, + current_difficulty: u32, + actual_timespan: u64, + ) -> DifficultyResult { + if current_difficulty == 0 { + return Err(DifficultyError::InvalidDifficulty( + "Current difficulty cannot be zero".to_string(), + )); + } + + // Clamp actual timespan to prevent extreme adjustments + let min_timespan = self.config.target_timespan / self.config.max_adjustment_factor; + let max_timespan = self.config.target_timespan * self.config.max_adjustment_factor; + // Ensure min_timespan is at least 1 to prevent division by zero + // (can happen if target_timespan < max_adjustment_factor due to integer division) + let min_timespan = min_timespan.max(1); + let clamped_timespan = actual_timespan.max(min_timespan).min(max_timespan); + + // Calculate new difficulty: current * target / actual + // Using u64 intermediate to prevent overflow + let new_difficulty = (current_difficulty as u64) + .saturating_mul(self.config.target_timespan) + .checked_div(clamped_timespan) + .ok_or_else(|| { + DifficultyError::CalculationError("Division by zero in difficulty calculation".to_string()) + })?; + + // Clamp to valid range and convert back to u32 + let clamped = new_difficulty + .max(self.config.min_difficulty as u64) + .min(self.config.max_difficulty as u64) as u32; + + Ok(clamped) + } + + /// Adjust difficulty given block timing information. + /// + /// This is the main entry point for blockchain difficulty adjustment. + /// + /// # Arguments + /// + /// * `height` - Current blockchain height + /// * `current_difficulty` - Current difficulty value + /// * `interval_start_time` - Timestamp of the block at the start of the interval + /// * `interval_end_time` - Timestamp of the current block (end of interval) + /// + /// # Returns + /// + /// * `Ok(Some(new_difficulty))` - If adjustment was made + /// * `Ok(None)` - If no adjustment needed at this height + /// * `Err(...)` - If calculation failed + pub fn adjust_difficulty( + &self, + height: u64, + current_difficulty: u32, + interval_start_time: u64, + interval_end_time: u64, + ) -> DifficultyResult> { + // Check if we should adjust at this height + if !self.should_adjust(height) { + return Ok(None); + } + + // Calculate actual timespan + let actual_timespan = interval_end_time.saturating_sub(interval_start_time); + if actual_timespan == 0 { + // Edge case: identical timestamps between adjustment interval blocks + // Return None to indicate no adjustment needed (defensive behavior matching legacy fallback) + tracing::warn!( + "Difficulty adjustment skipped: actual_timespan is zero at height {}", + height + ); + return Ok(None); + } + + // Calculate and return new difficulty + let new_difficulty = self.calculate_new_difficulty(current_difficulty, actual_timespan)?; + Ok(Some(new_difficulty)) + } + + /// Update configuration from DAO governance. + /// + /// This method validates and applies governance parameter updates. + /// + /// # Arguments + /// + /// * `initial_difficulty` - New initial difficulty (optional) + /// * `adjustment_interval` - New adjustment interval (optional) + /// * `target_timespan` - New target timespan (optional) + /// + /// # Returns + /// + /// * `Ok(())` - If update was successful + /// * `Err(...)` - If validation failed (no changes made) + pub fn apply_governance_update( + &mut self, + initial_difficulty: Option, + adjustment_interval: Option, + target_timespan: Option, + ) -> DifficultyResult<()> { + // Create a temporary config with proposed changes + let mut new_config = self.config.clone(); + + if let Some(value) = initial_difficulty { + new_config.initial_difficulty = value; + } + if let Some(value) = adjustment_interval { + new_config.adjustment_interval = value; + } + if let Some(value) = target_timespan { + new_config.target_timespan = value; + } + + // Validate before applying + new_config.validate()?; + + // Apply the validated configuration + self.config = new_config; + Ok(()) + } + + /// Set the minimum difficulty bound + pub fn set_min_difficulty(&mut self, min_difficulty: u32) -> DifficultyResult<()> { + if min_difficulty > self.config.max_difficulty { + return Err(DifficultyError::InvalidConfig( + "Min difficulty cannot exceed max difficulty".to_string(), + )); + } + self.config.min_difficulty = min_difficulty; + Ok(()) + } + + /// Set the maximum difficulty bound + pub fn set_max_difficulty(&mut self, max_difficulty: u32) -> DifficultyResult<()> { + if max_difficulty < self.config.min_difficulty { + return Err(DifficultyError::InvalidConfig( + "Max difficulty cannot be less than min difficulty".to_string(), + )); + } + self.config.max_difficulty = max_difficulty; + Ok(()) + } + + /// Set the maximum adjustment factor + pub fn set_max_adjustment_factor(&mut self, factor: u64) -> DifficultyResult<()> { + if factor == 0 { + return Err(DifficultyError::InvalidConfig( + "Max adjustment factor must be greater than zero".to_string(), + )); + } + self.config.max_adjustment_factor = factor; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_config() { + let config = DifficultyConfig::default(); + assert_eq!(config.initial_difficulty, 0x1d00ffff); + assert_eq!(config.adjustment_interval, 2016); + assert_eq!(config.target_timespan, 14 * 24 * 60 * 60); + assert!(config.validate().is_ok()); + } + + #[test] + fn test_config_validation() { + // Zero adjustment interval should fail + let mut config = DifficultyConfig::default(); + config.adjustment_interval = 0; + assert!(config.validate().is_err()); + + // Zero target timespan should fail + let mut config = DifficultyConfig::default(); + config.target_timespan = 0; + assert!(config.validate().is_err()); + + // Zero initial difficulty should fail + let mut config = DifficultyConfig::default(); + config.initial_difficulty = 0; + assert!(config.validate().is_err()); + + // Min > max should fail + let mut config = DifficultyConfig::default(); + config.min_difficulty = 100; + config.max_difficulty = 10; + assert!(config.validate().is_err()); + } + + #[test] + fn test_should_adjust() { + let manager = DifficultyManager::default(); + + // Should not adjust at height 0 + assert!(!manager.should_adjust(0)); + + // Should not adjust before first interval + assert!(!manager.should_adjust(1000)); + + // Should adjust at exactly adjustment_interval + assert!(manager.should_adjust(2016)); + + // Should not adjust between intervals + assert!(!manager.should_adjust(2017)); + assert!(!manager.should_adjust(3000)); + + // Should adjust at multiples of interval + assert!(manager.should_adjust(4032)); + assert!(manager.should_adjust(6048)); + } + + #[test] + fn test_calculate_new_difficulty_faster_blocks() { + let manager = DifficultyManager::default(); + let current_difficulty = 0x1d00ffff; + + // Blocks came in twice as fast as expected + let target = manager.target_timespan(); + let actual = target / 2; + + let new_difficulty = manager + .calculate_new_difficulty(current_difficulty, actual) + .unwrap(); + + // Difficulty should increase (higher number = harder to mine) + assert!(new_difficulty > current_difficulty); + } + + #[test] + fn test_calculate_new_difficulty_slower_blocks() { + let manager = DifficultyManager::default(); + let current_difficulty = 0x1d00ffff; + + // Blocks came in twice as slow as expected + let target = manager.target_timespan(); + let actual = target * 2; + + let new_difficulty = manager + .calculate_new_difficulty(current_difficulty, actual) + .unwrap(); + + // Difficulty should decrease (lower number = easier to mine) + assert!(new_difficulty < current_difficulty); + } + + #[test] + fn test_difficulty_clamping() { + let manager = DifficultyManager::default(); + let current_difficulty = 0x1d00ffff; + let target = manager.target_timespan(); + + // Extremely fast blocks (should be clamped to 4x increase max) + let actual = target / 100; // 100x faster + let new_difficulty = manager + .calculate_new_difficulty(current_difficulty, actual) + .unwrap(); + + // Should be at most 4x increase due to clamping + assert!(new_difficulty <= current_difficulty * 4); + + // Extremely slow blocks (should be clamped to 4x decrease max) + let actual = target * 100; // 100x slower + let new_difficulty = manager + .calculate_new_difficulty(current_difficulty, actual) + .unwrap(); + + // Should be at least 1/4 due to clamping + assert!(new_difficulty >= current_difficulty / 4); + } + + #[test] + fn test_adjust_difficulty_integration() { + let manager = DifficultyManager::default(); + let current_difficulty = 0x1d00ffff; + let interval = manager.adjustment_interval(); + let target = manager.target_timespan(); + + // At adjustment height with on-target timing + let result = manager + .adjust_difficulty(interval, current_difficulty, 0, target) + .unwrap(); + + assert!(result.is_some()); + let new_difficulty = result.unwrap(); + // Should be approximately the same (might have minor rounding) + assert!((new_difficulty as i64 - current_difficulty as i64).abs() < 100); + + // Not at adjustment height - should return None + let result = manager + .adjust_difficulty(interval + 1, current_difficulty, 0, target) + .unwrap(); + assert!(result.is_none()); + } + + #[test] + fn test_governance_update() { + let mut manager = DifficultyManager::default(); + + // Update adjustment interval + manager + .apply_governance_update(None, Some(1000), None) + .unwrap(); + assert_eq!(manager.adjustment_interval(), 1000); + + // Update target timespan + manager + .apply_governance_update(None, None, Some(604800)) + .unwrap(); + assert_eq!(manager.target_timespan(), 604800); + + // Update initial difficulty + manager + .apply_governance_update(Some(0x1d00fffe), None, None) + .unwrap(); + assert_eq!(manager.initial_difficulty(), 0x1d00fffe); + + // Invalid update should fail and not change anything + let current_interval = manager.adjustment_interval(); + assert!(manager.apply_governance_update(None, Some(0), None).is_err()); + assert_eq!(manager.adjustment_interval(), current_interval); + } + + #[test] + fn test_difficulty_bounds() { + let mut manager = DifficultyManager::default(); + + // Set custom bounds + manager.set_min_difficulty(100).unwrap(); + manager.set_max_difficulty(1000000).unwrap(); + + // Calculation should respect bounds + // With very slow blocks, difficulty should hit min + let result = manager + .calculate_new_difficulty(200, manager.target_timespan() * 1000) + .unwrap(); + assert!(result >= 100); + } + + #[test] + fn test_set_min_difficulty_validation() { + let mut manager = DifficultyManager::default(); + + // Valid: set min below current max + assert!(manager.set_min_difficulty(100).is_ok()); + assert_eq!(manager.config().min_difficulty, 100); + + // Test min > max scenario + manager.set_max_difficulty(500).unwrap(); + let result = manager.set_min_difficulty(600); + assert!(result.is_err()); + } + + #[test] + fn test_set_max_difficulty_validation() { + let mut manager = DifficultyManager::default(); + + // Set a min first + manager.set_min_difficulty(100).unwrap(); + + // Valid: set max above current min + assert!(manager.set_max_difficulty(1000).is_ok()); + assert_eq!(manager.config().max_difficulty, 1000); + + // Invalid: set max below current min + let result = manager.set_max_difficulty(50); + assert!(result.is_err()); + } + + #[test] + fn test_set_max_adjustment_factor_validation() { + let mut manager = DifficultyManager::default(); + + // Valid: set factor to a positive value + assert!(manager.set_max_adjustment_factor(8).is_ok()); + assert_eq!(manager.config().max_adjustment_factor, 8); + + // Valid: set factor to 1 + assert!(manager.set_max_adjustment_factor(1).is_ok()); + + // Invalid: set factor to zero + let result = manager.set_max_adjustment_factor(0); + assert!(result.is_err()); + } + + #[test] + fn test_min_timespan_cannot_be_zero() { + // Test edge case where target_timespan < max_adjustment_factor + // could cause min_timespan to be 0 due to integer division + let config = DifficultyConfig { + initial_difficulty: 0x1d00ffff, + adjustment_interval: 10, + target_timespan: 3, // Small value + min_difficulty: 1, + max_difficulty: 0xFFFFFFFF, + max_adjustment_factor: 10, // Larger than target_timespan + }; + let manager = DifficultyManager::new(config); + + // This should not panic or return division by zero error + // Even though 3 / 10 = 0 in integer division, we protect against this + let result = manager.calculate_new_difficulty(1000, 1); + assert!(result.is_ok()); + } +} diff --git a/lib-consensus/src/lib.rs b/lib-consensus/src/lib.rs index ef50ceb7..e02d4b7c 100644 --- a/lib-consensus/src/lib.rs +++ b/lib-consensus/src/lib.rs @@ -12,6 +12,7 @@ compile_error!("dev-insecure must not be enabled in release builds"); pub mod byzantine; pub mod chain_evaluation; pub mod dao; +pub mod difficulty; pub mod engines; pub mod mining; pub mod network; @@ -23,6 +24,7 @@ pub mod validators; // Re-export commonly used types pub use chain_evaluation::{ChainDecision, ChainEvaluator, ChainMergeResult, ChainSummary}; +pub use difficulty::{DifficultyConfig, DifficultyError, DifficultyManager, DifficultyResult}; pub use engines::enhanced_bft_engine::{ConsensusStatus, EnhancedBftEngine}; pub use engines::ConsensusEngine; pub use mining::{should_mine_block, IdentityData};