From 8e7c82c76c8b5afa9af96415ba130c23d6e491a3 Mon Sep 17 00:00:00 2001 From: illuzen Date: Wed, 6 May 2026 13:28:56 +0800 Subject: [PATCH 01/10] asymmetric clamps --- pallets/qpow/src/lib.rs | 33 ++++++++++++++++++++++++++------- pallets/qpow/src/mock.rs | 6 ++++-- runtime/src/configs/mod.rs | 13 +++++++++---- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/pallets/qpow/src/lib.rs b/pallets/qpow/src/lib.rs index fd62ef9a..f280bd0d 100644 --- a/pallets/qpow/src/lib.rs +++ b/pallets/qpow/src/lib.rs @@ -58,8 +58,15 @@ pub mod pallet { #[pallet::constant] type InitialDifficulty: Get; + /// Maximum percentage increase in difficulty per block (e.g., 3% = 3/100) + /// Used when blocks are faster than target (difficulty needs to increase) #[pallet::constant] - type DifficultyAdjustPercentClamp: Get; + type DifficultyIncreaseClamp: Get; + + /// Maximum percentage decrease in difficulty per block (e.g., 10% = 10/100) + /// Used when blocks are slower than target (difficulty needs to decrease) + #[pallet::constant] + type DifficultyDecreaseClamp: Get; #[pallet::constant] type TargetBlockTime: Get; @@ -249,13 +256,25 @@ pub mod pallet { ) -> U512 { log::debug!(target: "qpow", "📊 Calculating new difficulty ---------------------------------------------"); let observed_block_time = observed_block_time.max(1); - let clamp = T::DifficultyAdjustPercentClamp::get(); // 10% let one = FixedU128::one(); - let ratio = - FixedU128::from_rational(target_block_time as u128, observed_block_time as u128) - .min(one.saturating_add(clamp)) - .max(one.saturating_sub(clamp)); - log::debug!(target: "qpow", "💧 Clamped block_time ratio as FixedU128: {} ", ratio); + + // Calculate raw ratio: target_time / observed_time + // If observed > target (slow blocks): ratio < 1 -> difficulty decreases + // If observed < target (fast blocks): ratio > 1 -> difficulty increases + let raw_ratio = FixedU128::from_rational(target_block_time as u128, observed_block_time as u128); + + // Apply asymmetric clamping based on direction + let ratio = if raw_ratio > one { + // Difficulty increasing (blocks too fast) - use smaller clamp + let increase_clamp = T::DifficultyIncreaseClamp::get(); + raw_ratio.min(one.saturating_add(increase_clamp)) + } else { + // Difficulty decreasing (blocks too slow) - use larger clamp + let decrease_clamp = T::DifficultyDecreaseClamp::get(); + raw_ratio.max(one.saturating_sub(decrease_clamp)) + }; + + log::debug!(target: "qpow", "💧 Raw ratio: {}, Clamped ratio: {}", raw_ratio, ratio); let ratio_512 = U512::from(ratio.into_inner()); let max_difficulty = Self::get_max_difficulty(); diff --git a/pallets/qpow/src/mock.rs b/pallets/qpow/src/mock.rs index 2022f124..446425d7 100644 --- a/pallets/qpow/src/mock.rs +++ b/pallets/qpow/src/mock.rs @@ -70,14 +70,16 @@ impl pallet_timestamp::Config for Test { parameter_types! { pub const TestInitialDifficulty: U512 = U512([1000000, 0, 0, 0, 0, 0, 0, 0]); - pub const TestDifficultyAdjustPercentClamp: FixedU128 = FixedU128::from_rational(10, 100); + pub const TestDifficultyIncreaseClamp: FixedU128 = FixedU128::from_rational(3, 100); + pub const TestDifficultyDecreaseClamp: FixedU128 = FixedU128::from_rational(10, 100); } impl pallet_qpow::Config for Test { type WeightInfo = (); type EmaAlpha = ConstU32<500>; type InitialDifficulty = TestInitialDifficulty; - type DifficultyAdjustPercentClamp = TestDifficultyAdjustPercentClamp; + type DifficultyIncreaseClamp = TestDifficultyIncreaseClamp; + type DifficultyDecreaseClamp = TestDifficultyDecreaseClamp; type TargetBlockTime = ConstU64<1000>; type MaxReorgDepth = ConstU32<10>; } diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index 272a26e6..997c440e 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -152,19 +152,24 @@ parameter_types! { pub const TimestampBucketSize: u64 = 2 * TARGET_BLOCK_TIME_MS; // Nyquist frequency /// Initial mining difficulty - low value for development pub const QPoWInitialDifficulty: U512 = U512([1189189, 0, 0, 0, 0, 0, 0, 0]); - /// Difficulty adjustment percent clamp - pub const DifficultyAdjustPercentClamp: FixedU128 = FixedU128::from_rational(10, 100); + /// Maximum difficulty increase per block (2%) - conservative to prevent flash mining attacks + pub const DifficultyIncreaseClamp: FixedU128 = FixedU128::from_rational(2, 100); + /// Maximum difficulty decrease per block (5%) - moderate for reasonable recovery + pub const DifficultyDecreaseClamp: FixedU128 = FixedU128::from_rational(5, 100); } impl pallet_qpow::Config for Runtime { // Starting difficulty - should be challenging enough to require some work but not too high type InitialDifficulty = QPoWInitialDifficulty; - type DifficultyAdjustPercentClamp = DifficultyAdjustPercentClamp; + type DifficultyIncreaseClamp = DifficultyIncreaseClamp; + type DifficultyDecreaseClamp = DifficultyDecreaseClamp; type TargetBlockTime = TargetBlockTime; type MaxReorgDepth = ConstU32<180>; type WeightInfo = (); - type EmaAlpha = ConstU32<100>; // out of 1000, last_block_time * alpha + (previous_ema * (1 - alpha)) on moving average + // EMA smoothing factor: 20/1000 = 2% weight on new block, 98% on previous EMA + // This gives ~50 block effective window for very smooth difficulty curve + type EmaAlpha = ConstU32<20>; } parameter_types! { From 1dca545da7775ae3d847827de727483fc5a52f2e Mon Sep 17 00:00:00 2001 From: illuzen Date: Fri, 8 May 2026 13:01:18 +0800 Subject: [PATCH 02/10] more tweaking --- runtime/src/configs/mod.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index 997c440e..58d80f0f 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -152,10 +152,10 @@ parameter_types! { pub const TimestampBucketSize: u64 = 2 * TARGET_BLOCK_TIME_MS; // Nyquist frequency /// Initial mining difficulty - low value for development pub const QPoWInitialDifficulty: U512 = U512([1189189, 0, 0, 0, 0, 0, 0, 0]); - /// Maximum difficulty increase per block (2%) - conservative to prevent flash mining attacks - pub const DifficultyIncreaseClamp: FixedU128 = FixedU128::from_rational(2, 100); - /// Maximum difficulty decrease per block (5%) - moderate for reasonable recovery - pub const DifficultyDecreaseClamp: FixedU128 = FixedU128::from_rational(5, 100); + /// Maximum difficulty increase per block (5%) + pub const DifficultyIncreaseClamp: FixedU128 = FixedU128::from_rational(5, 100); + /// Maximum difficulty decrease per block (10%) - faster recovery + pub const DifficultyDecreaseClamp: FixedU128 = FixedU128::from_rational(10, 100); } impl pallet_qpow::Config for Runtime { @@ -167,9 +167,9 @@ impl pallet_qpow::Config for Runtime { type MaxReorgDepth = ConstU32<180>; type WeightInfo = (); - // EMA smoothing factor: 20/1000 = 2% weight on new block, 98% on previous EMA - // This gives ~50 block effective window for very smooth difficulty curve - type EmaAlpha = ConstU32<20>; + // EMA smoothing factor: 10/1000 = 1% weight on new block, 99% on previous EMA + // This gives ~100 block effective window for very smooth difficulty curve + type EmaAlpha = ConstU32<10>; } parameter_types! { From 8b33aa6a70cf4a489f237028241c2357231398e6 Mon Sep 17 00:00:00 2001 From: illuzen Date: Mon, 18 May 2026 19:57:43 +0900 Subject: [PATCH 03/10] use ethereum params --- runtime/src/configs/mod.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index 58d80f0f..a0afd946 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -150,12 +150,6 @@ parameter_types! { /// Target block time ms pub const TargetBlockTime: u64 = TARGET_BLOCK_TIME_MS; pub const TimestampBucketSize: u64 = 2 * TARGET_BLOCK_TIME_MS; // Nyquist frequency - /// Initial mining difficulty - low value for development - pub const QPoWInitialDifficulty: U512 = U512([1189189, 0, 0, 0, 0, 0, 0, 0]); - /// Maximum difficulty increase per block (5%) - pub const DifficultyIncreaseClamp: FixedU128 = FixedU128::from_rational(5, 100); - /// Maximum difficulty decrease per block (10%) - faster recovery - pub const DifficultyDecreaseClamp: FixedU128 = FixedU128::from_rational(10, 100); } impl pallet_qpow::Config for Runtime { From e96abc03d1e3f207393c6dfc54629f8cd35e1c46 Mon Sep 17 00:00:00 2001 From: illuzen Date: Mon, 18 May 2026 19:57:59 +0900 Subject: [PATCH 04/10] add ethereum params --- runtime/src/configs/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index a0afd946..4364c808 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -150,6 +150,11 @@ parameter_types! { /// Target block time ms pub const TargetBlockTime: u64 = TARGET_BLOCK_TIME_MS; pub const TimestampBucketSize: u64 = 2 * TARGET_BLOCK_TIME_MS; // Nyquist frequency + pub const QPoWInitialDifficulty: U512 = U512([2_700_000, 0, 0, 0, 0, 0, 0, 0]); + /// Maximum difficulty increase per block (~0.05%) - Ethereum-style slow increase + pub const DifficultyIncreaseClamp: FixedU128 = FixedU128::from_rational(1, 2048); + /// Maximum difficulty decrease per block (~5%) - Ethereum-style fast recovery + pub const DifficultyDecreaseClamp: FixedU128 = FixedU128::from_rational(5, 100); } impl pallet_qpow::Config for Runtime { From d6fa95588ac92fdfa1f1364e4cad96190be9a0f2 Mon Sep 17 00:00:00 2001 From: illuzen Date: Wed, 20 May 2026 17:20:05 +0900 Subject: [PATCH 05/10] increase min difficulty --- pallets/qpow/src/lib.rs | 6 ++---- pallets/qpow/src/tests.rs | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pallets/qpow/src/lib.rs b/pallets/qpow/src/lib.rs index f280bd0d..ae9c27c1 100644 --- a/pallets/qpow/src/lib.rs +++ b/pallets/qpow/src/lib.rs @@ -406,10 +406,8 @@ pub mod pallet { } pub fn get_min_difficulty() -> Difficulty { - // This value is related to clamp value, - // ie, if clamp is 10% this value must be at least 10 - // 1000 is safe for clamp values >= 0.01% - U512::from(1000u64) + // Minimum difficulty floor - same as Ethereum's minimum (2^17 = 131072) + U512::from(131_072u64) } pub fn get_max_difficulty() -> Difficulty { diff --git a/pallets/qpow/src/tests.rs b/pallets/qpow/src/tests.rs index e2c2c142..5fa332db 100644 --- a/pallets/qpow/src/tests.rs +++ b/pallets/qpow/src/tests.rs @@ -110,7 +110,7 @@ fn test_difficulty_bounds() { let max_difficulty = QPow::get_max_difficulty(); let initial_difficulty = QPow::initial_difficulty(); - assert_eq!(min_difficulty, U512::from(1000u64)); + assert_eq!(min_difficulty, U512::from(131_072u64)); assert!(max_difficulty > initial_difficulty); assert!(initial_difficulty > min_difficulty); }); @@ -372,7 +372,7 @@ fn test_zero_observed_block_time() { #[test] fn test_min_difficulty_derived_from_clamp() { new_test_ext().execute_with(|| { - assert_eq!(QPow::get_min_difficulty(), U512::from(1000u64)); + assert_eq!(QPow::get_min_difficulty(), U512::from(131_072u64)); }); } From d08ec7733852d60168f70b85ac0332fa63e83ab8 Mon Sep 17 00:00:00 2001 From: illuzen Date: Wed, 20 May 2026 18:14:49 +0900 Subject: [PATCH 06/10] just go all the way ethereum --- node/src/prometheus.rs | 9 -- pallets/qpow/src/lib.rs | 188 ++++++++---------------- pallets/qpow/src/mock.rs | 7 +- pallets/qpow/src/tests.rs | 16 -- pallets/reversible-transfers/src/lib.rs | 1 - primitives/consensus/qpow/src/lib.rs | 9 +- runtime/src/apis.rs | 4 - runtime/src/configs/mod.rs | 14 +- 8 files changed, 66 insertions(+), 182 deletions(-) diff --git a/node/src/prometheus.rs b/node/src/prometheus.rs index d8e6273d..7613e33e 100644 --- a/node/src/prometheus.rs +++ b/node/src/prometheus.rs @@ -44,13 +44,6 @@ impl BusinessMetrics { C: ProvideRuntimeApi, C::Api: sp_consensus_qpow::QPoWApi, { - // Get values via the runtime API - we'll handle potential errors gracefully - let block_time_ema = - client.runtime_api().get_block_time_ema(block_hash).unwrap_or_else(|e| { - log::warn!("Failed to get median_block_time: {:?}", e); - 0 - }); - let difficulty = client.runtime_api().get_difficulty(block_hash).unwrap_or_else(|e| { log::warn!("Failed to get difficulty: {:?}", e); U512::zero() @@ -73,9 +66,7 @@ impl BusinessMetrics { 0 }); - // Update the metrics with the values we retrieved gauge.with_label_values(&["chain_height"]).set(chain_height as f64); - gauge.with_label_values(&["block_time_ema"]).set(block_time_ema as f64); gauge.with_label_values(&["difficulty"]).set(Self::pack_u512_to_f64(difficulty)); gauge.with_label_values(&["last_block_time"]).set(last_block_time as f64); gauge diff --git a/pallets/qpow/src/lib.rs b/pallets/qpow/src/lib.rs index ae9c27c1..89d5db75 100644 --- a/pallets/qpow/src/lib.rs +++ b/pallets/qpow/src/lib.rs @@ -22,12 +22,11 @@ pub mod pallet { use core::ops::Shr; use frame_support::{ pallet_prelude::*, - sp_runtime::{traits::One, SaturatedConversion, Saturating}, + sp_runtime::{traits::One, SaturatedConversion}, traits::{BuildGenesisConfig, Time}, }; use frame_system::pallet_prelude::BlockNumberFor; use qpow_math::{achieved_difficulty_from_hash, get_nonce_hash, is_valid_nonce}; - use sp_arithmetic::FixedU128; use sp_core::U512; pub type NonceType = [u8; 64]; @@ -48,33 +47,14 @@ pub mod pallet { #[pallet::storage] pub type CurrentDifficulty = StorageValue<_, Difficulty, ValueQuery>; - // Exponential Moving Average of block times (in milliseconds) - #[pallet::storage] - pub type BlockTimeEma = StorageValue<_, BlockDuration, ValueQuery>; - #[pallet::config] pub trait Config: frame_system::Config + pallet_timestamp::Config { - /// Pallet's weight info #[pallet::constant] type InitialDifficulty: Get; - /// Maximum percentage increase in difficulty per block (e.g., 3% = 3/100) - /// Used when blocks are faster than target (difficulty needs to increase) - #[pallet::constant] - type DifficultyIncreaseClamp: Get; - - /// Maximum percentage decrease in difficulty per block (e.g., 10% = 10/100) - /// Used when blocks are slower than target (difficulty needs to decrease) - #[pallet::constant] - type DifficultyDecreaseClamp: Get; - #[pallet::constant] type TargetBlockTime: Get; - /// EMA smoothing factor (0-1000, where 1000 = 1.0) - #[pallet::constant] - type EmaAlpha: Get; - #[pallet::constant] type MaxReorgDepth: Get; @@ -99,14 +79,10 @@ pub mod pallet { fn build(&self) { let initial_difficulty = T::InitialDifficulty::get(); - // Set current difficulty for the genesis block >::put(initial_difficulty); log::info!(target: "qpow", "Genesis: Set initial difficulty to {:x}", initial_difficulty.low_u64()); - - // Initialize EMA with target block time - >::put(T::TargetBlockTime::get()); } } @@ -146,36 +122,6 @@ pub mod pallet { } impl Pallet { - fn update_block_time_ema(block_time: u64) { - let current_ema = >::get(); - let alpha = T::EmaAlpha::get(); - - // Initialize EMA with target block time if this is the first block - if current_ema == 0 { - >::put(T::TargetBlockTime::get()); - return; - } - - // Calculate EMA: new_ema = alpha * block_time + (1 - alpha) * current_ema - // Alpha is scaled by 1000, so we divide by 1000 - let alpha_scaled = alpha as u64; - let one_minus_alpha = 1000u64.saturating_sub(alpha_scaled); - - let weighted_current = block_time.saturating_mul(alpha_scaled); - let weighted_ema = current_ema.saturating_mul(one_minus_alpha); - let new_ema = (weighted_current.saturating_add(weighted_ema)) / 1000; - - >::put(new_ema); - - log::debug!(target: "qpow", - "📊 Updated EMA: {}ms -> {}ms (new block: {}ms, alpha: {})", - current_ema, - new_ema, - block_time, - alpha_scaled - ); - } - fn percentage_change(big_a: U512, big_b: U512) -> (U512, bool) { let a = big_a.shr(10); let b = big_b.shr(10); @@ -190,132 +136,118 @@ pub mod pallet { } fn adjust_difficulty() { - // Get current time let now = pallet_timestamp::Pallet::::now().saturated_into::(); let last_time = >::get(); let current_difficulty = >::get(); let current_block_number = >::block_number(); - // Only calculate block time if we're past the genesis block - if current_block_number > One::one() { - let block_time = now.saturating_sub(last_time); + // Calculate block time (use target for genesis block) + let block_time = if current_block_number > One::one() { + let duration = now.saturating_sub(last_time); log::debug!(target: "qpow", "Time calculation: now={}, last_time={}, diff={}ms", now, last_time, - block_time + duration ); - // Store the actual block duration - >::put(block_time); - - Self::update_block_time_ema(block_time); - } + >::put(duration); + + duration + } else { + T::TargetBlockTime::get() + }; - // Add last block time for the next calculations >::put(now); - let observed_block_time = >::get(); let target_time = T::TargetBlockTime::get(); + let new_difficulty = Self::calculate_difficulty(current_difficulty, block_time, target_time); - let new_difficulty = - Self::calculate_difficulty(current_difficulty, observed_block_time, target_time); - - // Save new difficulty >::put(new_difficulty); log::debug!(target: "qpow", "Stored new difficulty: {}", new_difficulty.low_u128()); - // Propagate new Event Self::deposit_event(Event::DifficultyAdjusted { old_difficulty: current_difficulty, new_difficulty, - observed_block_time, + observed_block_time: block_time, }); let (pct_change, is_positive) = Self::percentage_change(current_difficulty, new_difficulty); log::debug!(target: "qpow", - "🟢 Adjusted mining difficulty {}{}%: {:x} -> {:x} (observed block time: {}ms, target: {}ms) ", + "🟢 Adjusted mining difficulty {}{}%: {:x} -> {:x} (block time: {}ms, target: {}ms) ", if is_positive {"+"} else {"-"}, pct_change, current_difficulty.low_u64(), new_difficulty.low_u64(), - observed_block_time, + block_time, target_time ); } + /// Calculate new difficulty based on block time. + /// Uses the same formula as Ethereum PoW: + /// diff = parent_diff + (parent_diff / 2048) * max(1 - block_time / divisor, -99) + /// + /// The divisor is 10 seconds for a 12s target (scales proportionally). + /// This creates these zones: + /// - < divisor: difficulty increases by 1/2048 (~0.05%) + /// - divisor to 2*divisor: no change + /// - 2*divisor to 3*divisor: difficulty decreases by 1/2048 + /// - etc, up to max decrease of 99/2048 (~4.8%) pub fn calculate_difficulty( - current_difficulty: U512, - observed_block_time: u64, - target_block_time: u64, + parent_difficulty: U512, + block_time_ms: u64, + target_time_ms: u64, ) -> U512 { log::debug!(target: "qpow", "📊 Calculating new difficulty ---------------------------------------------"); - let observed_block_time = observed_block_time.max(1); - let one = FixedU128::one(); - // Calculate raw ratio: target_time / observed_time - // If observed > target (slow blocks): ratio < 1 -> difficulty decreases - // If observed < target (fast blocks): ratio > 1 -> difficulty increases - let raw_ratio = FixedU128::from_rational(target_block_time as u128, observed_block_time as u128); + // Divisor scales with target: 10s divisor for 12s target + // divisor = target * 10 / 12 = target * 5 / 6 + let divisor_ms = (target_time_ms * 5 / 6).max(1); + let time_factor = (block_time_ms / divisor_ms) as i64; + let adjustment = core::cmp::max(1i64 - time_factor, -99i64); - // Apply asymmetric clamping based on direction - let ratio = if raw_ratio > one { - // Difficulty increasing (blocks too fast) - use smaller clamp - let increase_clamp = T::DifficultyIncreaseClamp::get(); - raw_ratio.min(one.saturating_add(increase_clamp)) + log::debug!(target: "qpow", "Block time: {}ms, divisor: {}ms, time_factor: {}, adjustment: {}", + block_time_ms, divisor_ms, time_factor, adjustment); + + // Difficulty increment = parent_diff / 2048 + let increment = parent_difficulty / U512::from(2048u64); + + // Calculate new difficulty + let new_difficulty = if adjustment >= 0 { + parent_difficulty.saturating_add(increment.saturating_mul(U512::from(adjustment as u64))) } else { - // Difficulty decreasing (blocks too slow) - use larger clamp - let decrease_clamp = T::DifficultyDecreaseClamp::get(); - raw_ratio.max(one.saturating_sub(decrease_clamp)) + let decrease = increment.saturating_mul(U512::from((-adjustment) as u64)); + parent_difficulty.saturating_sub(decrease) }; - log::debug!(target: "qpow", "💧 Raw ratio: {}, Clamped ratio: {}", raw_ratio, ratio); - - let ratio_512 = U512::from(ratio.into_inner()); - let max_difficulty = Self::get_max_difficulty(); - - // For Bitcoin-style difficulty adjustment: - // If observed_time > target_time (slow blocks), difficulty should decrease - // If observed_time < target_time (fast blocks), difficulty should increase - // new_difficulty = current_difficulty * target_time / observed_time - let mut adjusted = match current_difficulty.checked_mul(ratio_512) { - Some(numerator) => { - // unchecked division, we know the denominator is not zero - let result = numerator / U512::from(one.into_inner()); - log::debug!(target: "qpow", - "Difficulty calculation: current={:x}, target_time={}, observed_time={}, new={:x}", - current_difficulty.low_u32() as u16, target_block_time, observed_block_time, result.low_u32() as u16); - result - }, - None => { - log::error!("Multiplication overflow in difficulty calculation"); - return max_difficulty; - }, - }; - + // Apply min/max bounds let min_difficulty = Self::get_min_difficulty(); - if adjusted < min_difficulty { + let max_difficulty = Self::get_max_difficulty(); + + let bounded = if new_difficulty < min_difficulty { log::warn!("Min difficulty achieved, clipping to: {:x}", min_difficulty.low_u64()); - adjusted = min_difficulty; - } else if adjusted > max_difficulty { + min_difficulty + } else if new_difficulty > max_difficulty { log::warn!("Max difficulty achieved, clipping to: {:x}", max_difficulty.low_u64()); - adjusted = max_difficulty; - } + max_difficulty + } else { + new_difficulty + }; log::debug!(target: "qpow", "🟢 Current Difficulty: {:x}", - current_difficulty.low_u64() + parent_difficulty.low_u64() ); - log::debug!(target: "qpow", "🟢 Next Difficulty: {:x}", adjusted.low_u64()); - log::debug!(target: "qpow", "🕒 Observed Block Time Sum: {}ms", observed_block_time); - log::debug!(target: "qpow", "🎯 Target Block Time Sum: {target_block_time}ms"); + log::debug!(target: "qpow", "🟢 Next Difficulty: {:x}", bounded.low_u64()); + log::debug!(target: "qpow", "🕒 Block Time: {}ms", block_time_ms); - adjusted + bounded } } @@ -414,10 +346,6 @@ pub mod pallet { U512::MAX } - pub fn get_block_time_ema() -> u64 { - >::get() - } - pub fn get_last_block_time() -> Timestamp { >::get() } diff --git a/pallets/qpow/src/mock.rs b/pallets/qpow/src/mock.rs index 446425d7..bb9e7bf9 100644 --- a/pallets/qpow/src/mock.rs +++ b/pallets/qpow/src/mock.rs @@ -8,7 +8,7 @@ use primitive_types::U512; use sp_core::H256; use sp_runtime::{ traits::{BlakeTwo256, IdentityLookup}, - BuildStorage, FixedU128, + BuildStorage, }; type Block = frame_system::mocking::MockBlock; @@ -70,16 +70,11 @@ impl pallet_timestamp::Config for Test { parameter_types! { pub const TestInitialDifficulty: U512 = U512([1000000, 0, 0, 0, 0, 0, 0, 0]); - pub const TestDifficultyIncreaseClamp: FixedU128 = FixedU128::from_rational(3, 100); - pub const TestDifficultyDecreaseClamp: FixedU128 = FixedU128::from_rational(10, 100); } impl pallet_qpow::Config for Test { type WeightInfo = (); - type EmaAlpha = ConstU32<500>; type InitialDifficulty = TestInitialDifficulty; - type DifficultyIncreaseClamp = TestDifficultyIncreaseClamp; - type DifficultyDecreaseClamp = TestDifficultyDecreaseClamp; type TargetBlockTime = ConstU64<1000>; type MaxReorgDepth = ConstU32<10>; } diff --git a/pallets/qpow/src/tests.rs b/pallets/qpow/src/tests.rs index 5fa332db..6224a7b6 100644 --- a/pallets/qpow/src/tests.rs +++ b/pallets/qpow/src/tests.rs @@ -177,22 +177,6 @@ fn test_difficulty_storage_and_retrieval() { }); } -#[test] -fn test_ema_block_time_tracking() { - new_test_ext().execute_with(|| { - // Initial EMA should be target block time - let target_time = ::TargetBlockTime::get(); - let initial_ema = QPow::get_block_time_ema(); - assert_eq!(initial_ema, target_time); - - // Run blocks and check EMA updates - run_to_block(2); - let updated_ema = QPow::get_block_time_ema(); - // EMA should still exist (exact value depends on timing) - assert!(updated_ema > 0); - }); -} - #[test] fn test_difficulty_calculation() { new_test_ext().execute_with(|| { diff --git a/pallets/reversible-transfers/src/lib.rs b/pallets/reversible-transfers/src/lib.rs index 697ff4cf..bf06eccd 100644 --- a/pallets/reversible-transfers/src/lib.rs +++ b/pallets/reversible-transfers/src/lib.rs @@ -26,7 +26,6 @@ pub use weights::WeightInfo; use alloc::vec::Vec; use frame_support::{ - defensive, pallet_prelude::*, traits::tokens::{fungibles::MutateHold as AssetsHold, Fortitude, Restriction}, }; diff --git a/primitives/consensus/qpow/src/lib.rs b/primitives/consensus/qpow/src/lib.rs index 544ddc66..6e38f6b8 100644 --- a/primitives/consensus/qpow/src/lib.rs +++ b/primitives/consensus/qpow/src/lib.rs @@ -19,17 +19,18 @@ sp_api::decl_runtime_apis! { /// Get the current difficulty (max_distance / distance_threshold) fn get_difficulty() -> U512; - /// Get block ema - fn get_block_time_ema() -> u64; - /// Get last block timestamp fn get_last_block_time() -> u64; - // Get last block mining time + /// Get last block mining time fn get_last_block_duration() -> u64; + fn get_chain_height() -> u32; + fn verify_nonce_on_import_block(block_hash: [u8; 32], nonce: [u8; 64]) -> bool; + fn verify_nonce_local_mining(block_hash: [u8; 32], nonce: [u8; 64]) -> bool; + fn verify_and_get_achieved_difficulty(block_hash: [u8; 32], nonce: [u8; 64]) -> (bool, U512); } } diff --git a/runtime/src/apis.rs b/runtime/src/apis.rs index ab088a7f..d4e0f33f 100644 --- a/runtime/src/apis.rs +++ b/runtime/src/apis.rs @@ -146,10 +146,6 @@ impl_runtime_apis! { pallet_qpow::Pallet::::get_difficulty() } - fn get_block_time_ema() -> u64 { - pallet_qpow::Pallet::::get_block_time_ema() - } - fn get_last_block_time() -> u64 { pallet_qpow::Pallet::::get_last_block_time() } diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index 4364c808..c0fcd63f 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -55,7 +55,7 @@ use smallvec::smallvec; use qp_scheduler::BlockNumberOrTimestamp; use sp_runtime::{ traits::{BlakeTwo256, One}, - AccountId32, FixedU128, Perbill, Permill, + AccountId32, Perbill, Permill, }; use sp_version::RuntimeVersion; @@ -150,25 +150,15 @@ parameter_types! { /// Target block time ms pub const TargetBlockTime: u64 = TARGET_BLOCK_TIME_MS; pub const TimestampBucketSize: u64 = 2 * TARGET_BLOCK_TIME_MS; // Nyquist frequency + /// Initial mining difficulty pub const QPoWInitialDifficulty: U512 = U512([2_700_000, 0, 0, 0, 0, 0, 0, 0]); - /// Maximum difficulty increase per block (~0.05%) - Ethereum-style slow increase - pub const DifficultyIncreaseClamp: FixedU128 = FixedU128::from_rational(1, 2048); - /// Maximum difficulty decrease per block (~5%) - Ethereum-style fast recovery - pub const DifficultyDecreaseClamp: FixedU128 = FixedU128::from_rational(5, 100); } impl pallet_qpow::Config for Runtime { - // Starting difficulty - should be challenging enough to require some work but not too high type InitialDifficulty = QPoWInitialDifficulty; - type DifficultyIncreaseClamp = DifficultyIncreaseClamp; - type DifficultyDecreaseClamp = DifficultyDecreaseClamp; type TargetBlockTime = TargetBlockTime; type MaxReorgDepth = ConstU32<180>; - type WeightInfo = (); - // EMA smoothing factor: 10/1000 = 1% weight on new block, 99% on previous EMA - // This gives ~100 block effective window for very smooth difficulty curve - type EmaAlpha = ConstU32<10>; } parameter_types! { From c29a76598fdb3d40a3cc57f0fcac93fcee08ef59 Mon Sep 17 00:00:00 2001 From: illuzen Date: Wed, 20 May 2026 18:25:40 +0900 Subject: [PATCH 07/10] fmt --- pallets/qpow/src/lib.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pallets/qpow/src/lib.rs b/pallets/qpow/src/lib.rs index 89d5db75..82b22aa7 100644 --- a/pallets/qpow/src/lib.rs +++ b/pallets/qpow/src/lib.rs @@ -153,7 +153,7 @@ pub mod pallet { ); >::put(duration); - + duration } else { T::TargetBlockTime::get() @@ -162,7 +162,8 @@ pub mod pallet { >::put(now); let target_time = T::TargetBlockTime::get(); - let new_difficulty = Self::calculate_difficulty(current_difficulty, block_time, target_time); + let new_difficulty = + Self::calculate_difficulty(current_difficulty, block_time, target_time); >::put(new_difficulty); @@ -192,7 +193,7 @@ pub mod pallet { /// Calculate new difficulty based on block time. /// Uses the same formula as Ethereum PoW: /// diff = parent_diff + (parent_diff / 2048) * max(1 - block_time / divisor, -99) - /// + /// /// The divisor is 10 seconds for a 12s target (scales proportionally). /// This creates these zones: /// - < divisor: difficulty increases by 1/2048 (~0.05%) @@ -205,31 +206,32 @@ pub mod pallet { target_time_ms: u64, ) -> U512 { log::debug!(target: "qpow", "📊 Calculating new difficulty ---------------------------------------------"); - + // Divisor scales with target: 10s divisor for 12s target // divisor = target * 10 / 12 = target * 5 / 6 let divisor_ms = (target_time_ms * 5 / 6).max(1); let time_factor = (block_time_ms / divisor_ms) as i64; let adjustment = core::cmp::max(1i64 - time_factor, -99i64); - + log::debug!(target: "qpow", "Block time: {}ms, divisor: {}ms, time_factor: {}, adjustment: {}", block_time_ms, divisor_ms, time_factor, adjustment); - + // Difficulty increment = parent_diff / 2048 let increment = parent_difficulty / U512::from(2048u64); - + // Calculate new difficulty let new_difficulty = if adjustment >= 0 { - parent_difficulty.saturating_add(increment.saturating_mul(U512::from(adjustment as u64))) + parent_difficulty + .saturating_add(increment.saturating_mul(U512::from(adjustment as u64))) } else { let decrease = increment.saturating_mul(U512::from((-adjustment) as u64)); parent_difficulty.saturating_sub(decrease) }; - + // Apply min/max bounds let min_difficulty = Self::get_min_difficulty(); let max_difficulty = Self::get_max_difficulty(); - + let bounded = if new_difficulty < min_difficulty { log::warn!("Min difficulty achieved, clipping to: {:x}", min_difficulty.low_u64()); min_difficulty From 2fa368d7e889804c3a11f71e6bcbdbad632a9599 Mon Sep 17 00:00:00 2001 From: illuzen Date: Wed, 20 May 2026 19:16:26 +0900 Subject: [PATCH 08/10] fix benchmarks and update initial difficulty --- pallets/qpow/src/benchmarking.rs | 3 --- runtime/src/configs/mod.rs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pallets/qpow/src/benchmarking.rs b/pallets/qpow/src/benchmarking.rs index 4216fdaa..d83493dd 100644 --- a/pallets/qpow/src/benchmarking.rs +++ b/pallets/qpow/src/benchmarking.rs @@ -32,9 +32,6 @@ mod benchmarks { pallet_timestamp::Pallet::::set_timestamp(now); >::put(now.saturating_sub(T::TargetBlockTime::get())); - // Initialize EMA - >::put(T::TargetBlockTime::get()); - #[block] { QPoW::::on_finalize(block_number); diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index c0fcd63f..f38ef13c 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -151,7 +151,7 @@ parameter_types! { pub const TargetBlockTime: u64 = TARGET_BLOCK_TIME_MS; pub const TimestampBucketSize: u64 = 2 * TARGET_BLOCK_TIME_MS; // Nyquist frequency /// Initial mining difficulty - pub const QPoWInitialDifficulty: U512 = U512([2_700_000, 0, 0, 0, 0, 0, 0, 0]); + pub const QPoWInitialDifficulty: U512 = U512([4_000_000, 0, 0, 0, 0, 0, 0, 0]); } impl pallet_qpow::Config for Runtime { From d563a322fcad3aad076fcd50932b70c90f5cc3a6 Mon Sep 17 00:00:00 2001 From: illuzen Date: Thu, 21 May 2026 11:51:50 +0900 Subject: [PATCH 09/10] addressed nits --- .../grafana/dashboards/quantus-node/overview.json | 4 ++-- .../dashboards/quantus-node/quantus-business.json | 4 ++-- pallets/qpow/src/benchmarking.rs | 2 +- pallets/qpow/src/lib.rs | 9 ++++----- pallets/qpow/src/tests.rs | 8 ++++---- pallets/qpow/src/weights.rs | 12 ++++-------- 6 files changed, 17 insertions(+), 22 deletions(-) diff --git a/miner-stack/grafana/dashboards/quantus-node/overview.json b/miner-stack/grafana/dashboards/quantus-node/overview.json index 17b0a241..4fb11b21 100644 --- a/miner-stack/grafana/dashboards/quantus-node/overview.json +++ b/miner-stack/grafana/dashboards/quantus-node/overview.json @@ -118,11 +118,11 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "qpow_metrics{data_group=\"block_time_ema\", job=\"quantus-node\"}", + "expr": "qpow_metrics{data_group=\"last_block_duration\", job=\"quantus-node\"}", "refId": "A" } ], - "title": "Block Time (EMA)", + "title": "Last Block Duration", "type": "stat" }, { diff --git a/miner-stack/grafana/dashboards/quantus-node/quantus-business.json b/miner-stack/grafana/dashboards/quantus-node/quantus-business.json index 9c0ac493..d2895751 100644 --- a/miner-stack/grafana/dashboards/quantus-node/quantus-business.json +++ b/miner-stack/grafana/dashboards/quantus-node/quantus-business.json @@ -271,12 +271,12 @@ }, "targets": [ { - "expr": "qpow_metrics{data_group=\"block_time_ema\"}", + "expr": "qpow_metrics{data_group=\"last_block_duration\"}", "legendFormat": "{{instance}}", "refId": "A" } ], - "title": "Block Time EMA", + "title": "Last Block Duration", "type": "timeseries" }, { diff --git a/pallets/qpow/src/benchmarking.rs b/pallets/qpow/src/benchmarking.rs index d83493dd..2e0cdf8c 100644 --- a/pallets/qpow/src/benchmarking.rs +++ b/pallets/qpow/src/benchmarking.rs @@ -15,7 +15,7 @@ use sp_runtime::traits::Get; mod benchmarks { use super::*; - /// Benchmark for the on_finalize hook which performs EMA-based difficulty adjustment. + /// Benchmark for the on_finalize hook which performs difficulty adjustment. #[benchmark] fn on_finalize() { // Setup state with typical block for difficulty adjustment diff --git a/pallets/qpow/src/lib.rs b/pallets/qpow/src/lib.rs index 82b22aa7..71e360f6 100644 --- a/pallets/qpow/src/lib.rs +++ b/pallets/qpow/src/lib.rs @@ -107,8 +107,7 @@ pub mod pallet { ::WeightInfo::on_finalize() } - /// Called at the end of each block to adjust mining difficulty based on - /// observed block times using Exponential Moving Average (EMA). + /// Called at the end of each block to adjust mining difficulty. fn on_finalize(block_number: BlockNumberFor) { let current_difficulty = >::get(); log::debug!(target: "qpow", @@ -207,9 +206,9 @@ pub mod pallet { ) -> U512 { log::debug!(target: "qpow", "📊 Calculating new difficulty ---------------------------------------------"); - // Divisor scales with target: 10s divisor for 12s target - // divisor = target * 10 / 12 = target * 5 / 6 - let divisor_ms = (target_time_ms * 5 / 6).max(1); + // Divisor scales with target: 8s divisor for 12s target + // divisor = target * 8 / 12 = target * 2 / 3 + let divisor_ms = (target_time_ms * 2 / 3).max(1); let time_factor = (block_time_ms / divisor_ms) as i64; let adjustment = core::cmp::max(1i64 - time_factor, -99i64); diff --git a/pallets/qpow/src/tests.rs b/pallets/qpow/src/tests.rs index 6224a7b6..2ce75e54 100644 --- a/pallets/qpow/src/tests.rs +++ b/pallets/qpow/src/tests.rs @@ -330,8 +330,8 @@ fn test_difficulty_recovers_after_sleep() { } let recovered = QPow::get_difficulty(); - // EMA smoothing limits the spike, but alpha=500 is aggressive so recovery - // takes many blocks. 20 normal blocks bring difficulty to ~18% of pre-sleep. + // Ethereum-style adjustment decreases difficulty by up to 99/2048 per block during sleep, + // then increases slowly during recovery. 20 normal blocks bring difficulty back partially. assert!( recovered > pre_sleep / 10, "Difficulty should stay above 10% after sleep. Pre: {}, Post: {}", @@ -364,7 +364,7 @@ fn test_min_difficulty_derived_from_clamp() { fn test_min_difficulty_can_increase() { new_test_ext().execute_with(|| { let min_diff = QPow::get_min_difficulty(); - // Fast blocks → ratio clamped to 1.1 → floor(1000 * 1.1) = 1100 + // Fast blocks → positive adjustment → difficulty increases by 1/2048 let result = QPow::calculate_difficulty(min_diff, 1, 1000); assert!( result > min_diff, @@ -379,7 +379,7 @@ fn test_min_difficulty_can_increase() { fn test_min_difficulty_floors_on_slow_blocks() { new_test_ext().execute_with(|| { let min_diff = QPow::get_min_difficulty(); - // Slow blocks → ratio clamped to 0.9 → floor(1000 * 0.9) = 900, clips to 1000 + // Slow blocks → negative adjustment, but clips to min difficulty let result = QPow::calculate_difficulty(min_diff, 100_000, 1000); assert_eq!(result, min_diff); }); diff --git a/pallets/qpow/src/weights.rs b/pallets/qpow/src/weights.rs index 545e68a1..04fbc845 100644 --- a/pallets/qpow/src/weights.rs +++ b/pallets/qpow/src/weights.rs @@ -63,8 +63,6 @@ impl WeightInfo for SubstrateWeight { /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `QPoW::LastBlockTime` (r:1 w:1) /// Proof: `QPoW::LastBlockTime` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `QPoW::BlockTimeEma` (r:1 w:1) - /// Proof: `QPoW::BlockTimeEma` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `QPoW::LastBlockDuration` (r:0 w:1) /// Proof: `QPoW::LastBlockDuration` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) fn on_finalize() -> Weight { @@ -73,8 +71,8 @@ impl WeightInfo for SubstrateWeight { // Estimated: `1549` // Minimum execution time: 106_000_000 picoseconds. Weight::from_parts(109_000_000, 1549) - .saturating_add(T::DbWeight::get().reads(5_u64)) - .saturating_add(T::DbWeight::get().writes(4_u64)) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) } } @@ -86,8 +84,6 @@ impl WeightInfo for () { /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `QPoW::LastBlockTime` (r:1 w:1) /// Proof: `QPoW::LastBlockTime` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `QPoW::BlockTimeEma` (r:1 w:1) - /// Proof: `QPoW::BlockTimeEma` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `QPoW::LastBlockDuration` (r:0 w:1) /// Proof: `QPoW::LastBlockDuration` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) fn on_finalize() -> Weight { @@ -96,7 +92,7 @@ impl WeightInfo for () { // Estimated: `1549` // Minimum execution time: 106_000_000 picoseconds. Weight::from_parts(109_000_000, 1549) - .saturating_add(RocksDbWeight::get().reads(5_u64)) - .saturating_add(RocksDbWeight::get().writes(4_u64)) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) } } From feacb80bb26dbcd2536066f5964aee0819f0fcf9 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Thu, 21 May 2026 12:36:59 +0800 Subject: [PATCH 10/10] fix stale doc comment: divisor is 8s for 12s target --- pallets/qpow/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pallets/qpow/src/lib.rs b/pallets/qpow/src/lib.rs index 71e360f6..9ac058d4 100644 --- a/pallets/qpow/src/lib.rs +++ b/pallets/qpow/src/lib.rs @@ -193,7 +193,7 @@ pub mod pallet { /// Uses the same formula as Ethereum PoW: /// diff = parent_diff + (parent_diff / 2048) * max(1 - block_time / divisor, -99) /// - /// The divisor is 10 seconds for a 12s target (scales proportionally). + /// The divisor is 8 seconds for a 12s target (scales proportionally). /// This creates these zones: /// - < divisor: difficulty increases by 1/2048 (~0.05%) /// - divisor to 2*divisor: no change