From 704c2602799703e212d7065f72aca4fd71602a60 Mon Sep 17 00:00:00 2001 From: illuzen Date: Thu, 25 Jun 2026 14:16:13 +0800 Subject: [PATCH 1/7] reduce max message size --- miner-api/src/lib.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/miner-api/src/lib.rs b/miner-api/src/lib.rs index 5e540991..028313d2 100644 --- a/miner-api/src/lib.rs +++ b/miner-api/src/lib.rs @@ -1,8 +1,11 @@ use serde::{Deserialize, Serialize}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -/// Maximum message size (16 MB) to prevent memory exhaustion attacks. -pub const MAX_MESSAGE_SIZE: u32 = 16 * 1024 * 1024; +/// Maximum message size (1 KB) to prevent memory exhaustion attacks. +/// +/// Real MinerMessage payloads are only a few hundred bytes (Ready, NewJob, JobResult). +/// 1 KB provides sufficient headroom while minimizing the amplification attack surface. +pub const MAX_MESSAGE_SIZE: u32 = 1024; /// Status codes returned in API responses. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] From d30784b33bd2c31f385e5ecb564d6793c9bffaea Mon Sep 17 00:00:00 2001 From: illuzen Date: Thu, 25 Jun 2026 14:18:48 +0800 Subject: [PATCH 2/7] always access current_difficulty thru safe getter --- pallets/qpow/src/lib.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pallets/qpow/src/lib.rs b/pallets/qpow/src/lib.rs index 3337a417..0ebcd91e 100644 --- a/pallets/qpow/src/lib.rs +++ b/pallets/qpow/src/lib.rs @@ -109,7 +109,7 @@ pub mod pallet { /// Called at the end of each block to adjust mining difficulty. fn on_finalize(block_number: BlockNumberFor) { - let current_difficulty = >::get(); + let current_difficulty = Self::get_difficulty(); log::debug!(target: "qpow", "📢 QPoW: before submit at block {:?}, current_difficulty={:?}", block_number, @@ -137,7 +137,10 @@ pub mod pallet { fn adjust_difficulty() { let now = pallet_timestamp::Pallet::::now().saturated_into::(); let last_time = >::get(); - let current_difficulty = >::get(); + // Use get_difficulty() to handle zero/missing storage consistently with verification. + // This ensures we use InitialDifficulty as the base when storage is unset, + // rather than computing from zero which would clamp to min_difficulty. + let current_difficulty = Self::get_difficulty(); let current_block_number = >::block_number(); // Calculate block time (use target for genesis block) From 393c33e33c98d9f3a243c8ddde8a563158c6eb17 Mon Sep 17 00:00:00 2001 From: illuzen Date: Thu, 25 Jun 2026 16:00:02 +0800 Subject: [PATCH 3/7] update docstring, accept non-canonical fork work accumulation --- client/consensus/qpow/src/chain_management.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/consensus/qpow/src/chain_management.rs b/client/consensus/qpow/src/chain_management.rs index e6a41290..16626b28 100644 --- a/client/consensus/qpow/src/chain_management.rs +++ b/client/consensus/qpow/src/chain_management.rs @@ -163,8 +163,10 @@ pub fn is_heavier( /// This should be called synchronously after each block import to ensure finalization /// happens before the next block is imported. /// -/// Also cleans up achieved work entries for blocks that are now deep enough in the -/// finalized chain that they can never be involved in fork choice again. +/// Cleans up achieved work entries for canonical blocks that are now finalized. +/// Note: Non-canonical fork blocks are not cleaned up since we cannot enumerate them +/// by height. This is acceptable because fork blocks require valid PoW to create, +/// making accumulation attacks expensive, and the entries are small (~96 bytes each). pub fn finalize_canonical_at_depth(client: &C) -> Result<(), ConsensusError> where B: BlockT, From 5016e060b49ae637603a0cfe03d1c2594e02cdf1 Mon Sep 17 00:00:00 2001 From: illuzen Date: Thu, 25 Jun 2026 16:01:05 +0800 Subject: [PATCH 4/7] allow override of initial difficulty --- pallets/qpow/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pallets/qpow/src/lib.rs b/pallets/qpow/src/lib.rs index 0ebcd91e..1c5148a0 100644 --- a/pallets/qpow/src/lib.rs +++ b/pallets/qpow/src/lib.rs @@ -77,12 +77,12 @@ pub mod pallet { #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { - let initial_difficulty = T::InitialDifficulty::get(); - - >::put(initial_difficulty); + // Use the genesis config value, not the runtime constant. + // This allows chain-spec overrides of initial difficulty. + >::put(self.initial_difficulty); log::info!(target: "qpow", "Genesis: Set initial difficulty to {:x}", - initial_difficulty.low_u64()); + self.initial_difficulty.low_u64()); } } From d61c47f15dede26d4ecaf38d5a52553658eb9b2c Mon Sep 17 00:00:00 2001 From: illuzen Date: Thu, 25 Jun 2026 16:02:54 +0800 Subject: [PATCH 5/7] sharpen finalization cleanup condition --- client/consensus/qpow/src/chain_management.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/consensus/qpow/src/chain_management.rs b/client/consensus/qpow/src/chain_management.rs index 16626b28..80134376 100644 --- a/client/consensus/qpow/src/chain_management.rs +++ b/client/consensus/qpow/src/chain_management.rs @@ -269,10 +269,11 @@ where log::debug!("✓ Finalized block #{:?} ({:?})", finalize_number, finalize_hash); - // Clean up achieved work for the previously finalized block. - // Once a block is finalized and we've moved past it, its achieved work - // is no longer needed for fork choice decisions. - if last_finalized_before > Zero::zero() { + // Clean up achieved work entries for blocks that are now below the finalized tip. + // Only clean up if finalization actually advanced (last_finalized_after > last_finalized_before), + // and only delete entries strictly below the new finalized height. + // The finalized tip's entry must be preserved as it's the parent work source for children. + if last_finalized_after > last_finalized_before && last_finalized_before > Zero::zero() { if let Ok(Some(old_finalized_hash)) = client.hash(last_finalized_before) { if let Err(e) = delete_cumulative_achieved_work::(client, old_finalized_hash) { // Non-fatal: log warning but don't fail the finalization From 9c7c8db600bbb484210aa6cbdd0aa8236ec3dcd5 Mon Sep 17 00:00:00 2001 From: illuzen Date: Thu, 25 Jun 2026 16:25:14 +0800 Subject: [PATCH 6/7] verify seal atomically to prevent mining stalls --- client/consensus/qpow/src/chain_management.rs | 4 +- client/consensus/qpow/src/worker.rs | 106 +++++++----------- node/src/service.rs | 11 +- 3 files changed, 44 insertions(+), 77 deletions(-) diff --git a/client/consensus/qpow/src/chain_management.rs b/client/consensus/qpow/src/chain_management.rs index 80134376..e643c49b 100644 --- a/client/consensus/qpow/src/chain_management.rs +++ b/client/consensus/qpow/src/chain_management.rs @@ -270,8 +270,8 @@ where log::debug!("✓ Finalized block #{:?} ({:?})", finalize_number, finalize_hash); // Clean up achieved work entries for blocks that are now below the finalized tip. - // Only clean up if finalization actually advanced (last_finalized_after > last_finalized_before), - // and only delete entries strictly below the new finalized height. + // Only clean up if finalization actually advanced (last_finalized_after > + // last_finalized_before), and only delete entries strictly below the new finalized height. // The finalized tip's entry must be preserved as it's the parent work source for children. if last_finalized_after > last_finalized_before && last_finalized_before > Zero::zero() { if let Ok(Some(old_finalized_hash)) = client.hash(last_finalized_before) { diff --git a/client/consensus/qpow/src/worker.rs b/client/consensus/qpow/src/worker.rs index eb768d8d..1ccb43d9 100644 --- a/client/consensus/qpow/src/worker.rs +++ b/client/consensus/qpow/src/worker.rs @@ -134,77 +134,49 @@ where self.build.lock().as_ref().map(|b| b.metadata.clone()) } - /// Verify a seal without consuming the build. - /// - /// Returns `true` if the seal is valid for the current block, `false` otherwise. - /// Returns `false` if there's no current build. - /// Logs detailed information on failure for debugging. - pub fn verify_seal(&self, seal: &Seal) -> bool { - let build = self.build.lock(); - let build = match build.as_ref() { - Some(b) => b, - None => { - warn!(target: LOG_TARGET, "verify_seal: No current build available"); - return false; - }, - }; + /// Submit a mined seal. The seal will be validated before consuming the build. + /// Returns true if the submission is successful. + pub async fn submit(&self, seal: Seal) -> bool { + // Atomically verify and take the build in a single lock acquisition. + // This prevents TOCTOU issues where a rebuild could land between verify and consume. + let build = { + let mut build_guard = self.build.lock(); + let build_ref = match build_guard.as_ref() { + Some(b) => b, + None => { + warn!(target: LOG_TARGET, "Unable to import mined block: build does not exist"); + return false; + }, + }; - // Convert seal to nonce [u8; 64] - let nonce: [u8; 64] = match seal.as_slice().try_into() { - Ok(arr) => arr, - Err(_) => { - warn!(target: LOG_TARGET, "Seal does not have exactly 64 bytes, got {}", seal.len()); - return false; - }, - }; + // Verify seal before consuming the build + let nonce: [u8; 64] = match seal.as_slice().try_into() { + Ok(arr) => arr, + Err(_) => { + warn!(target: LOG_TARGET, "Seal does not have exactly 64 bytes, got {}", seal.len()); + return false; + }, + }; - let pre_hash = build.metadata.pre_hash.0; - let best_hash = build.metadata.best_hash; - let difficulty = build.metadata.difficulty; - let extrinsic_count = build.proposal.block.extrinsics().len(); - - // Verify using runtime API - match self.client.runtime_api().verify_nonce_local_mining(best_hash, pre_hash, nonce) { - Ok(true) => true, - Ok(false) => { - log::error!( - target: LOG_TARGET, - "verify_seal FAILED:\n\ - pre_hash (block template): {}\n\ - best_hash (parent block): {}\n\ - difficulty: {}\n\ - nonce (seal): {}\n\ - extrinsics in block: {}", - hex::encode(pre_hash), - best_hash, - difficulty, - hex::encode(nonce), - extrinsic_count, - ); - false - }, - Err(e) => { - warn!(target: LOG_TARGET, "Runtime API error verifying seal: {:?}", e); - false - }, - } - } + let pre_hash = build_ref.metadata.pre_hash.0; + let best_hash = build_ref.metadata.best_hash; - /// Submit a mined seal. The seal will be validated again. Returns true if the submission is - /// successful. - pub async fn submit(&self, seal: Seal) -> bool { - let build = if let Some(build) = { - let mut build = self.build.lock(); - let value = build.take(); - if value.is_some() { - self.increment_version(); + match self.client.runtime_api().verify_nonce_local_mining(best_hash, pre_hash, nonce) { + Ok(true) => { + // Seal is valid, now take the build + let value = build_guard.take(); + self.increment_version(); + value.unwrap() // Safe: we checked it exists above + }, + Ok(false) => { + warn!(target: LOG_TARGET, "Seal verification failed, not consuming build"); + return false; + }, + Err(e) => { + warn!(target: LOG_TARGET, "Runtime API error verifying seal: {:?}", e); + return false; + }, } - value - } { - build - } else { - warn!(target: LOG_TARGET, "Unable to import mined block: build does not exist",); - return false; }; let seal = DigestItem::Seal(POW_ENGINE_ID, seal); diff --git a/node/src/service.rs b/node/src/service.rs index 13965cad..34c50139 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -212,12 +212,7 @@ async fn handle_external_mining( }, }; - // Verify the seal before attempting to submit (submit consumes the build) - if !worker_handle.verify_seal(&seal) { - continue; - } - - // Seal is valid, submit it + // Submit the seal (submit verifies atomically before consuming the build) if worker_handle.submit(seal.clone()).await { let mining_time = mining_start_time.elapsed().as_secs(); log::info!( @@ -229,9 +224,9 @@ async fn handle_external_mining( return ExternalMiningOutcome::Success; } - // Submit failed for some other reason (should be rare after verify_seal passed) + // Submit failed (seal invalid or import error) log::warn!( - "⛏️ Failed to submit verified seal from miner {}, continuing to wait (job {})", + "⛏️ Failed to submit seal from miner {}, continuing to wait (job {})", miner_id, job_id ); From 5dc2e66b15ceff9a23df3211d291048257f07074 Mon Sep 17 00:00:00 2001 From: illuzen Date: Thu, 25 Jun 2026 16:30:16 +0800 Subject: [PATCH 7/7] overflow instead of saturate when looping on nonces --- qpow-math/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qpow-math/src/lib.rs b/qpow-math/src/lib.rs index 35f7996d..30399756 100644 --- a/qpow-math/src/lib.rs +++ b/qpow-math/src/lib.rs @@ -83,7 +83,7 @@ pub fn mine_range( return Some((nonce_bytes, hash)); } - nonce = nonce.saturating_add(U512::from(1u64)); + nonce = nonce.overflowing_add(U512::from(1u64)).0; } None