Skip to content
Merged
26 changes: 26 additions & 0 deletions lib-blockchain/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,32 @@ Web4 Architecture:
└─────────────────────────────────────────────────┘
```

## Consensus Architecture

### Adaptive Difficulty Adjustment

The blockchain uses governance-controlled difficulty parameters through `DifficultyConfig`:

- **Configurable Parameters**: Target timespan, adjustment interval, and adjustment factors
- **Default Settings**: 14-day timespan, 2016-block interval, 4x max adjustment (Bitcoin-style)
- **Governance Updates**: Parameters can be modified via DAO proposals
- **Audit Trail**: `last_updated_at_height` tracks parameter changes

```rust
pub struct DifficultyConfig {
pub target_timespan: u64, // Default: 14 days
pub adjustment_interval: u64, // Default: 2016 blocks
pub min_adjustment_factor: u64, // Default: 4
pub max_adjustment_factor: u64, // Default: 4
pub last_updated_at_height: u64,
}
```

**Key Methods:**
- `target_block_time()` - Calculates target seconds per block (default: 600s)
- `clamp_timespan()` - Prevents extreme difficulty changes
- `validate()` - Ensures parameters are within safe ranges

## Economic Architecture

### Universal Basic Income System
Expand Down
10 changes: 9 additions & 1 deletion lib-blockchain/src/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::collections::{HashMap, HashSet};
use anyhow::Result;
use serde::{Serialize, Deserialize};
use tracing::{info, warn, error, debug};
use crate::types::{Hash, Difficulty};
use crate::types::{Hash, Difficulty, DifficultyConfig};
use crate::transaction::{Transaction, TransactionInput, TransactionOutput, IdentityTransactionData};
use crate::types::transaction_type::TransactionType;
use crate::block::Block;
Expand Down Expand Up @@ -40,6 +40,8 @@ pub struct Blockchain {
pub height: u64,
/// Current mining difficulty
pub difficulty: Difficulty,
/// Difficulty adjustment configuration (governance-controlled)
pub difficulty_config: DifficultyConfig,
/// Total work done (cumulative difficulty)
pub total_work: u128,
/// UTXO set for transaction validation
Expand Down Expand Up @@ -162,6 +164,7 @@ impl Blockchain {
blocks: vec![genesis_block.clone()],
height: 0,
difficulty: Difficulty::from_bits(crate::INITIAL_DIFFICULTY),
difficulty_config: DifficultyConfig::default(),
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing blockchain serialization test (test_blockchain_serialization) does not verify that the difficulty_config field is properly serialized and deserialized. Adding a blockchain field requires updating the test to verify backward compatibility and proper serialization of the new field, especially since this field is critical for governance and consensus operations.

Copilot uses AI. Check for mistakes.
total_work: 0,
utxo_set: HashMap::new(),
nullifier_set: HashSet::new(),
Expand Down Expand Up @@ -830,6 +833,11 @@ impl Blockchain {
self.height
}

/// Get the current difficulty configuration
pub fn get_difficulty_config(&self) -> &DifficultyConfig {
&self.difficulty_config
}

/// Check if a nullifier has been used
pub fn is_nullifier_used(&self, nullifier: &Hash) -> bool {
self.nullifier_set.contains(nullifier)
Expand Down
1 change: 1 addition & 0 deletions lib-blockchain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub mod contracts;

// Re-export core types for convenience
pub use types::*;
pub use types::DifficultyConfig;
pub use transaction::{*, WalletReference, WalletPrivateData};
pub use block::*;
pub use blockchain::{Blockchain, BlockchainImport, BlockchainBroadcastMessage, EconomicsTransaction, ValidatorInfo};
Expand Down
231 changes: 231 additions & 0 deletions lib-blockchain/src/types/difficulty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,137 @@ impl std::fmt::Display for Difficulty {
}
}

/// Governance-controlled difficulty adjustment parameters for PoUW consensus
///
/// This struct stores configurable parameters that control how mining difficulty
/// adjusts over time. These parameters can be updated through governance proposals
/// to adapt to changing network conditions.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DifficultyConfig {
/// Target timespan for difficulty adjustment period (in seconds)
/// Default: 14 days (1,209,600 seconds)
pub target_timespan: u64,

/// Number of blocks between difficulty adjustments
/// Default: 2016 blocks (Bitcoin-style)
pub adjustment_interval: u64,

/// Minimum adjustment factor (maximum difficulty decrease per adjustment)
/// Value of 4 means difficulty can decrease by at most 4x per adjustment
/// Default: 4
pub min_adjustment_factor: u64,

/// Maximum adjustment factor (maximum difficulty increase per adjustment)
/// Value of 4 means difficulty can increase by at most 4x per adjustment
/// Default: 4
pub max_adjustment_factor: u64,

/// Block height at which this configuration was last updated
/// Used for governance tracking and auditability
pub last_updated_at_height: u64,
}

impl DifficultyConfig {
/// Create a new DifficultyConfig with default values
pub fn new() -> Self {
Self::default()
}

/// Create a new DifficultyConfig with custom parameters
pub fn with_params(
target_timespan: u64,
adjustment_interval: u64,
min_adjustment_factor: u64,
max_adjustment_factor: u64,
last_updated_at_height: u64,
) -> Self {
Self {
target_timespan,
adjustment_interval,
min_adjustment_factor,
max_adjustment_factor,
last_updated_at_height,
}
}

/// Get the target block time in seconds
/// Calculated as target_timespan / adjustment_interval
pub fn target_block_time(&self) -> u64 {
if self.adjustment_interval == 0 {
return 600; // Default to 10 minutes if misconfigured
Comment on lines 139 to 143
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The target_block_time method returns a hardcoded 600 seconds when adjustment_interval is 0, but this creates an inconsistency - the method should either return an error or handle the zero case in a way that's consistent with validation. Since validate() already prevents adjustment_interval from being 0, this fallback may never be reached in validated configs. Consider documenting this defensive check or returning Result to handle invalid states explicitly.

Suggested change
/// Calculated as target_timespan / adjustment_interval
pub fn target_block_time(&self) -> u64 {
if self.adjustment_interval == 0 {
return 600; // Default to 10 minutes if misconfigured
///
/// Calculated as `target_timespan / adjustment_interval`.
///
/// # Panics
///
/// Panics if `adjustment_interval` is zero. Call [`validate`] on this
/// configuration before using it to ensure parameters are well-formed.
pub fn target_block_time(&self) -> u64 {
if self.adjustment_interval == 0 {
panic!("DifficultyConfig is invalid: adjustment_interval must be greater than 0");

Copilot uses AI. Check for mistakes.
}
self.target_timespan / self.adjustment_interval
}

/// Calculate the clamped timespan for difficulty adjustment
///
/// This prevents extreme difficulty changes by clamping the actual timespan
/// to be within [target_timespan / min_factor, target_timespan * max_factor]
pub fn clamp_timespan(&self, actual_timespan: u64) -> u64 {
let max_timespan = self.target_timespan.saturating_mul(self.max_adjustment_factor);
let min_timespan = self.target_timespan / self.min_adjustment_factor.max(1);

actual_timespan.clamp(min_timespan, max_timespan)
}

/// Validate the configuration parameters
///
/// Returns an error if any parameters are invalid or would cause issues
pub fn validate(&self) -> Result<(), String> {
if self.target_timespan == 0 {
return Err("target_timespan must be greater than 0".to_string());
}

if self.adjustment_interval == 0 {
return Err("adjustment_interval must be greater than 0".to_string());
}

if self.min_adjustment_factor == 0 {
return Err("min_adjustment_factor must be greater than 0".to_string());
}

if self.max_adjustment_factor == 0 {
return Err("max_adjustment_factor must be greater than 0".to_string());
}

// Check for reasonable limits
if self.target_timespan > 365 * 24 * 60 * 60 {
return Err("target_timespan cannot exceed 1 year".to_string());
}

if self.adjustment_interval > 1_000_000 {
return Err("adjustment_interval cannot exceed 1,000,000 blocks".to_string());
}

if self.min_adjustment_factor > 100 || self.max_adjustment_factor > 100 {
return Err("adjustment factors cannot exceed 100".to_string());
}

Ok(())
}
}

impl Default for DifficultyConfig {
fn default() -> Self {
Self {
// 14 days in seconds (same as Bitcoin)
target_timespan: 14 * 24 * 60 * 60,

// 2016 blocks (Bitcoin-style)
adjustment_interval: 2016,

// Maximum 4x decrease per adjustment
min_adjustment_factor: 4,

// Maximum 4x increase per adjustment
max_adjustment_factor: 4,

// Genesis block
last_updated_at_height: 0,
}
}
}

/// Calculate target from difficulty bits (Bitcoin-style)
pub fn calculate_target(difficulty_bits: u32) -> [u8; 32] {
let mut target = [0u8; 32];
Expand Down Expand Up @@ -214,4 +345,104 @@ mod tests {

assert!(min_diff.bits() > max_diff.bits()); // Lower bits = higher difficulty
}

#[test]
fn test_difficulty_config_default() {
let config = DifficultyConfig::default();

assert_eq!(config.target_timespan, 14 * 24 * 60 * 60); // 14 days
assert_eq!(config.adjustment_interval, 2016);
assert_eq!(config.min_adjustment_factor, 4);
assert_eq!(config.max_adjustment_factor, 4);
assert_eq!(config.last_updated_at_height, 0);
}

#[test]
fn test_difficulty_config_target_block_time() {
let config = DifficultyConfig::default();

// 14 days / 2016 blocks = 600 seconds (10 minutes)
assert_eq!(config.target_block_time(), 600);
}

#[test]
fn test_difficulty_config_clamp_timespan() {
let config = DifficultyConfig::default();

// Target timespan is 14 days
let target = 14 * 24 * 60 * 60;

// Test clamping at minimum (target / 4)
let too_fast = 1000;
let clamped = config.clamp_timespan(too_fast);
assert_eq!(clamped, target / 4);

// Test clamping at maximum (target * 4)
let too_slow = 100 * 24 * 60 * 60;
let clamped = config.clamp_timespan(too_slow);
assert_eq!(clamped, target * 4);

// Test no clamping for normal values
let normal = target;
let clamped = config.clamp_timespan(normal);
assert_eq!(clamped, normal);
}

#[test]
fn test_difficulty_config_validation() {
// Valid config should pass
let valid_config = DifficultyConfig::default();
assert!(valid_config.validate().is_ok());

// Invalid target_timespan
let mut invalid = DifficultyConfig::default();
invalid.target_timespan = 0;
assert!(invalid.validate().is_err());

// Invalid adjustment_interval
let mut invalid = DifficultyConfig::default();
invalid.adjustment_interval = 0;
assert!(invalid.validate().is_err());

// Invalid min_adjustment_factor
let mut invalid = DifficultyConfig::default();
invalid.min_adjustment_factor = 0;
assert!(invalid.validate().is_err());

// Invalid max_adjustment_factor
let mut invalid = DifficultyConfig::default();
invalid.max_adjustment_factor = 0;
assert!(invalid.validate().is_err());
}
Comment on lines 421 to 446
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation test only checks for zero values but doesn't test the upper bound validations defined in the validate method. Specifically, it should test cases where target_timespan exceeds 1 year, adjustment_interval exceeds 1,000,000, or adjustment factors exceed 100 to ensure those validation rules work correctly.

Copilot uses AI. Check for mistakes.

#[test]
fn test_difficulty_config_serialization() {
let config = DifficultyConfig::default();

// Serialize to JSON
let json = serde_json::to_string(&config).unwrap();

// Deserialize back
let deserialized: DifficultyConfig = serde_json::from_str(&json).unwrap();

// Should be equal
assert_eq!(config, deserialized);
}

#[test]
fn test_difficulty_config_with_params() {
let config = DifficultyConfig::with_params(
7 * 24 * 60 * 60, // 7 days
1008, // Half of Bitcoin's interval
2, // Max 2x decrease
2, // Max 2x increase
1000, // Updated at block 1000
);

assert_eq!(config.target_timespan, 7 * 24 * 60 * 60);
assert_eq!(config.adjustment_interval, 1008);
assert_eq!(config.min_adjustment_factor, 2);
assert_eq!(config.max_adjustment_factor, 2);
assert_eq!(config.last_updated_at_height, 1000);
}
}
2 changes: 1 addition & 1 deletion lib-blockchain/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub mod message_type;
// Re-export blockchain core types
pub use transaction_type::*;
pub use hash::*;
pub use difficulty::*;
pub use difficulty::{Difficulty, DifficultyConfig, calculate_target, meets_difficulty, target_to_difficulty, max_target, min_target, adjust_difficulty, difficulty_to_work};
pub use mining::*;

// Re-export DAO and SOV economy types
Expand Down
Loading