Skip to content
15 changes: 9 additions & 6 deletions client/consensus/qpow/src/chain_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,10 @@ pub fn is_heavier<N: PartialOrd>(
/// 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<B, C, BE>(client: &C) -> Result<(), ConsensusError>
where
B: BlockT<Hash = H256>,
Expand Down Expand Up @@ -267,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::<B, C>(client, old_finalized_hash) {
// Non-fatal: log warning but don't fail the finalization
Expand Down
106 changes: 39 additions & 67 deletions client/consensus/qpow/src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 5 additions & 2 deletions miner-api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down
11 changes: 3 additions & 8 deletions node/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand All @@ -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
);
Expand Down
15 changes: 9 additions & 6 deletions pallets/qpow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,12 @@ pub mod pallet {
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
let initial_difficulty = T::InitialDifficulty::get();

<CurrentDifficulty<T>>::put(initial_difficulty);
// Use the genesis config value, not the runtime constant.
// This allows chain-spec overrides of initial difficulty.
<CurrentDifficulty<T>>::put(self.initial_difficulty);

log::info!(target: "qpow", "Genesis: Set initial difficulty to {:x}",
initial_difficulty.low_u64());
self.initial_difficulty.low_u64());
}
}

Expand All @@ -109,7 +109,7 @@ pub mod pallet {

/// Called at the end of each block to adjust mining difficulty.
fn on_finalize(block_number: BlockNumberFor<T>) {
let current_difficulty = <CurrentDifficulty<T>>::get();
let current_difficulty = Self::get_difficulty();
log::debug!(target: "qpow",
"📢 QPoW: before submit at block {:?}, current_difficulty={:?}",
block_number,
Expand Down Expand Up @@ -137,7 +137,10 @@ pub mod pallet {
fn adjust_difficulty() {
let now = pallet_timestamp::Pallet::<T>::now().saturated_into::<u64>();
let last_time = <LastBlockTime<T>>::get();
let current_difficulty = <CurrentDifficulty<T>>::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 = <frame_system::Pallet<T>>::block_number();

// Calculate block time (use target for genesis block)
Expand Down
2 changes: 1 addition & 1 deletion qpow-math/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading