From 72135ab0a9396250f0d19fc4968f8dbaa61ad1fe Mon Sep 17 00:00:00 2001 From: Shashank Date: Tue, 5 Aug 2025 12:13:36 +0530 Subject: [PATCH 1/6] Impl slasher service --- docs/docs/users/guides/slasher.md | 34 +++++ src/chain/store/chain_store.rs | 20 ++- src/chain/store/tipset_tracker.rs | 34 +++++ src/lib.rs | 1 + src/slasher/filter.rs | 230 ++++++++++++++++++++++++++++++ src/slasher/mod.rs | 9 ++ src/slasher/service.rs | 147 +++++++++++++++++++ src/slasher/tests.rs | 161 +++++++++++++++++++++ src/slasher/types.rs | 33 +++++ 9 files changed, 668 insertions(+), 1 deletion(-) create mode 100644 docs/docs/users/guides/slasher.md create mode 100644 src/slasher/filter.rs create mode 100644 src/slasher/mod.rs create mode 100644 src/slasher/service.rs create mode 100644 src/slasher/tests.rs create mode 100644 src/slasher/types.rs diff --git a/docs/docs/users/guides/slasher.md b/docs/docs/users/guides/slasher.md new file mode 100644 index 000000000000..276e186487e8 --- /dev/null +++ b/docs/docs/users/guides/slasher.md @@ -0,0 +1,34 @@ +# Forest Slasher Service + +A consensus fault detection service for Forest that monitors incoming blocks and detects malicious behaviors like double-mining, time-offset mining, and parent-grinding. + +## Configuration + +The slasher service is configured through environment variables. + +### Environment Variables + +- `FOREST_FAULTREPORTER_ENABLECONSENSUSFAULTREPORTER`: Enable/disable the consensus fault reporter (default: false) +- `FOREST_FAULTREPORTER_CONSENSUSFAULTREPORTERDATADIR`: Directory for storing slasher data (default: `.forest/slasher`) +- `FOREST_FAULTREPORTER_CONSENSUSFAULTREPORTERADDRESS`: Wallet address for submitting fault reports (optional) + +### Usage Example + +```bash +# Enable the slasher service +export FOREST_FAULTREPORTER_ENABLECONSENSUSFAULTREPORTER=true + +# Set the data directory (optional, defaults to .forest/slasher) +export FOREST_FAULTREPORTER_CONSENSUSFAULTREPORTERDATADIR="/path/to/slasher/directory" + +# Set the reporter address (optional) +export FOREST_FAULTREPORTER_CONSENSUSFAULTREPORTERADDRESS="f1abc123..." +``` + +### Fault Detection + +The service detects three types of consensus faults: + +1. **Double-fork mining**: Same miner produces multiple blocks at the same epoch +2. **Time-offset mining**: Same miner produces multiple blocks with the same parents +3. **Parent-grinding**: Miner ignores their own block and mines on others diff --git a/src/chain/store/chain_store.rs b/src/chain/store/chain_store.rs index 3b8c3540ea07..c7e2b786f1f0 100644 --- a/src/chain/store/chain_store.rs +++ b/src/chain/store/chain_store.rs @@ -129,10 +129,28 @@ where let chain_index = Arc::new(ChainIndex::new(Arc::clone(&db))); let validated_blocks = Mutex::new(HashSet::default()); + let tipset_tracker = if crate::utils::misc::env::is_env_truthy( + "FOREST_FAULTREPORTER_ENABLECONSENSUSFAULTREPORTER", + ) { + let slasher_service = match crate::slasher::service::SlasherService::new() { + Ok(service) => { + tracing::info!("Slasher service created successfully"); + Arc::new(service) + } + Err(e) => { + tracing::warn!("Failed to create slasher service: {}", e); + return Err(anyhow::anyhow!("Failed to create slasher service: {}", e)); + } + }; + TipsetTracker::with_slasher(Arc::clone(&db), chain_config.clone(), slasher_service) + } else { + TipsetTracker::new(Arc::clone(&db), chain_config.clone()) + }; + let cs = Self { publisher, chain_index, - tipset_tracker: TipsetTracker::new(Arc::clone(&db), chain_config.clone()), + tipset_tracker, db, heaviest_tipset_key_provider, genesis_block_header, diff --git a/src/chain/store/tipset_tracker.rs b/src/chain/store/tipset_tracker.rs index 4c11ae9ecd3c..b8dec03799c5 100644 --- a/src/chain/store/tipset_tracker.rs +++ b/src/chain/store/tipset_tracker.rs @@ -7,6 +7,7 @@ use super::Error; use crate::blocks::{CachingBlockHeader, Tipset}; use crate::networks::ChainConfig; use crate::shim::clock::ChainEpoch; +use crate::slasher::service::SlasherService; use cid::Cid; use fvm_ipld_blockstore::Blockstore; use nunny::vec as nonempty; @@ -19,6 +20,8 @@ pub(in crate::chain) struct TipsetTracker { entries: Mutex>>, db: Arc, chain_config: Arc, + /// Optional slasher service for consensus fault detection + slasher_service: Option>, } impl TipsetTracker { @@ -27,6 +30,21 @@ impl TipsetTracker { entries: Default::default(), db, chain_config, + slasher_service: None, + } + } + + /// Create a new TipsetTracker with slasher service integration + pub fn with_slasher( + db: Arc, + chain_config: Arc, + slasher_service: Arc, + ) -> Self { + Self { + entries: Default::default(), + db, + chain_config, + slasher_service: Some(slasher_service), } } @@ -42,6 +60,7 @@ impl TipsetTracker { cids.push(*header.cid()); drop(map_lock); + self.check_consensus_fault(header); self.check_multiple_blocks_from_same_miner(&cids_to_verify, header); self.prune_entries(header.epoch); } @@ -68,6 +87,20 @@ impl TipsetTracker { } } + /// Process block with slasher service for consensus fault detection + fn check_consensus_fault(&self, header: &CachingBlockHeader) { + if let Some(slasher) = &self.slasher_service { + let slasher = slasher.clone(); + let header = header.clone(); + + tokio::spawn(async move { + if let Err(e) = slasher.process_block(&header).await { + warn!("Error processing block with slasher service: {}", e); + } + }); + } + } + /// Deletes old entries in the `TipsetTracker` that are past the chain /// finality. fn prune_entries(&self, header_epoch: ChainEpoch) { @@ -136,6 +169,7 @@ mod test { db: Arc::new(db), chain_config: chain_config.clone(), entries: Mutex::new(entries), + slasher_service: None, }; tipset_tracker.prune_entries(head_epoch); diff --git a/src/lib.rs b/src/lib.rs index 8d6d8aa0e198..ffbd3471a014 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,6 +61,7 @@ mod metrics; mod networks; mod rpc; mod shim; +mod slasher; mod state_manager; mod state_migration; mod statediff; diff --git a/src/slasher/filter.rs b/src/slasher/filter.rs new file mode 100644 index 000000000000..ca73dba6ec27 --- /dev/null +++ b/src/slasher/filter.rs @@ -0,0 +1,230 @@ +// Copyright 2019-2025 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::blocks::CachingBlockHeader; +use crate::shim::address::Address; +use crate::shim::clock::ChainEpoch; +use crate::slasher::types::*; +use anyhow::Result; +use parity_db::{Db, Options}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct BlockInfo { + miner_address: Vec, + epoch: ChainEpoch, + parents: crate::blocks::TipsetKey, + cid: cid::Cid, +} + +pub struct SlasherFilter { + db: Db, +} + +impl SlasherFilter { + pub fn new(data_dir: std::path::PathBuf) -> Result { + std::fs::create_dir_all(&data_dir)?; + + let mut options = Options::with_columns(&data_dir, 1); + options.columns[0].btree_index = true; + options.columns[0].uniform = false; + + let db = Db::open_or_create(&options)?; + + Ok(Self { db }) + } + + pub fn process_block(&mut self, header: &CachingBlockHeader) -> Result> { + self.add_to_history(header)?; + let fault = self.check_consensus_faults(header)?; + + Ok(fault) + } + + fn add_to_history(&mut self, header: &CachingBlockHeader) -> Result<()> { + let miner = header.miner_address; + let epoch = header.epoch; + + let block_info = BlockInfo { + miner_address: miner.to_bytes(), + epoch, + parents: header.parents.clone(), + cid: *header.cid(), + }; + + let key = self.create_db_key(miner, epoch, header.cid()); + let value = serde_json::to_vec(&block_info)?; + + self.db.commit(vec![(0, key, Some(value))])?; + + Ok(()) + } + + fn create_db_key(&self, miner: Address, epoch: ChainEpoch, cid: &cid::Cid) -> Vec { + let mut key = Vec::new(); + key.extend_from_slice(&miner.to_bytes()); + key.extend_from_slice(&epoch.to_le_bytes()); + key.extend_from_slice(cid.to_bytes().as_slice()); + key + } + + fn load_blocks_from_db(&mut self, miner: Address, epoch: ChainEpoch) -> Result> { + let mut blocks = Vec::new(); + let prefix = self.create_db_key_prefix(miner, epoch); + + let mut iter = self.db.iter(0)?; + while let Some((key, value)) = iter.next()? { + if key.starts_with(&prefix) { + if let Ok(block_info) = serde_json::from_slice::(&value) { + blocks.push(block_info); + } + } + } + + Ok(blocks) + } + + fn create_db_key_prefix(&self, miner: Address, epoch: ChainEpoch) -> Vec { + let mut prefix = Vec::new(); + prefix.extend_from_slice(&miner.to_bytes()); + prefix.extend_from_slice(&epoch.to_le_bytes()); + prefix + } + + fn check_consensus_faults( + &mut self, + header: &CachingBlockHeader, + ) -> Result> { + if let Some(fault) = self.check_double_fork_mining(header)? { + return Ok(Some(fault)); + } + + if let Some(fault) = self.check_time_offset_mining(header)? { + return Ok(Some(fault)); + } + + if let Some(fault) = self.check_parent_grinding(header)? { + return Ok(Some(fault)); + } + + Ok(None) + } + + fn check_double_fork_mining( + &mut self, + header: &CachingBlockHeader, + ) -> Result> { + let miner = header.miner_address; + let epoch = header.epoch; + + let blocks = self.load_blocks_from_db(miner, epoch)?; + + // If we have more than one block from this miner at this epoch, it's double-fork mining + if blocks.len() > 1 { + let block_headers: Vec = blocks.iter().map(|b| b.cid).collect(); + + return Ok(Some(ConsensusFault { + miner_address: miner, + detection_epoch: epoch, + fault_type: ConsensusFaultType::DoubleForkMining, + block_headers, + extra_evidence: None, + })); + } + + Ok(None) + } + + fn check_time_offset_mining( + &mut self, + header: &CachingBlockHeader, + ) -> Result> { + let miner = header.miner_address; + let current_cid = *header.cid(); + + let mut iter = self.db.iter(0)?; + let mut same_parent_blocks = Vec::new(); + + while let Some((_, value)) = iter.next()? { + if let Ok(block_info) = serde_json::from_slice::(&value) { + if block_info.parents == header.parents && block_info.cid != current_cid { + if let Ok(block_miner) = Address::from_bytes(&block_info.miner_address) { + if block_miner == miner { + same_parent_blocks.push(block_info); + } + } + } + } + } + + if !same_parent_blocks.is_empty() { + let mut block_headers = vec![current_cid]; + block_headers.extend(same_parent_blocks.iter().map(|b| b.cid)); + + return Ok(Some(ConsensusFault { + miner_address: miner, + detection_epoch: header.epoch, + fault_type: ConsensusFaultType::TimeOffsetMining, + block_headers, + extra_evidence: None, + })); + } + + Ok(None) + } + + fn check_parent_grinding( + &mut self, + header: &CachingBlockHeader, + ) -> Result> { + let miner = header.miner_address; + let current_epoch = header.epoch; + + if current_epoch < 1 { + return Ok(None); + } + + let prev_blocks = self.load_blocks_from_db(miner, current_epoch - 1)?; + + for prev_block in prev_blocks { + if header.parents.contains(prev_block.cid) { + continue; + } + + if let Some(witness) = self.find_parent_grinding_witness(header, &prev_block)? { + return Ok(Some(ConsensusFault { + miner_address: miner, + detection_epoch: current_epoch, + fault_type: ConsensusFaultType::ParentGrinding, + block_headers: vec![prev_block.cid, *header.cid()], + extra_evidence: Some(witness.cid), + })); + } + } + + Ok(None) + } + + fn find_parent_grinding_witness( + &self, + current_block: &CachingBlockHeader, + miner_prev_block: &BlockInfo, + ) -> Result> { + let prev_epoch = current_block.epoch - 1; + + let mut iter = self.db.iter(0)?; + while let Some((_, value)) = iter.next()? { + if let Ok(block_info) = serde_json::from_slice::(&value) { + if block_info.epoch == prev_epoch + && block_info.parents == miner_prev_block.parents + && block_info.cid != miner_prev_block.cid + && current_block.parents.contains(block_info.cid) + { + return Ok(Some(block_info)); + } + } + } + + Ok(None) + } +} diff --git a/src/slasher/mod.rs b/src/slasher/mod.rs new file mode 100644 index 000000000000..7066fcb3b760 --- /dev/null +++ b/src/slasher/mod.rs @@ -0,0 +1,9 @@ +// Copyright 2019-2025 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +pub mod filter; +pub mod service; +pub mod types; + +#[cfg(test)] +mod tests; diff --git a/src/slasher/service.rs b/src/slasher/service.rs new file mode 100644 index 000000000000..a9c951e80250 --- /dev/null +++ b/src/slasher/service.rs @@ -0,0 +1,147 @@ +// Copyright 2019-2025 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::blocks::CachingBlockHeader; +use crate::rpc::RpcMethodExt; +use crate::slasher::filter::SlasherFilter; +use crate::slasher::types::ConsensusFault; +use anyhow::{Context, Result}; +use fvm_ipld_encoding; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::RwLock; +use tracing::{info, warn}; + +/// Slasher service that orchestrates consensus fault detection and reporting +pub struct SlasherService { + /// Core fault detection filter + filter: Arc>, + /// Reporter address for submitting fault reports + reporter_address: Option, +} + +impl SlasherService { + pub fn new() -> Result { + let data_dir = std::env::var("FOREST_FAULTREPORTER_CONSENSUSFAULTREPORTERDATADIR") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(".forest/slasher")); + + let reporter_address = std::env::var("FOREST_FAULTREPORTER_CONSENSUSFAULTREPORTERADDRESS") + .ok() + .and_then(|addr_str| addr_str.parse().ok()); + + let filter = Arc::new(RwLock::new( + SlasherFilter::new(data_dir.clone()).with_context(|| { + format!( + "Failed to create slasher filter in directory: {:?}", + data_dir + ) + })?, + )); + + info!( + "Slasher service initialized with data directory: {:?}", + data_dir + ); + if let Some(addr) = &reporter_address { + info!("Fault reporter address configured: {}", addr); + } else { + info!("No fault reporter address configured - will use default wallet address"); + } + + Ok(Self { + filter, + reporter_address, + }) + } + + pub async fn process_block(&self, header: &CachingBlockHeader) -> Result<()> { + info!( + "Checking consensus fault for {} by miner address {} at epoch {}", + header.cid(), + header.miner_address, + header.epoch + ); + + let fault = { + let mut filter = self.filter.write().await; + filter.process_block(header)? + }; + + if let Some(fault) = fault { + info!( + "Consensus fault detected: {:?} by miner {} at epoch {}", + fault.fault_type, fault.miner_address, fault.detection_epoch + ); + + warn!( + "Fault details - Block headers: {:?}, Extra evidence: {:?}", + fault.block_headers, fault.extra_evidence + ); + + if let Err(e) = self.submit_fault_report(&fault).await { + warn!("Failed to submit fault report: {}", e); + } + } + + Ok(()) + } + + async fn submit_fault_report(&self, fault: &ConsensusFault) -> Result<()> { + info!( + "Submitting consensus fault report for miner {} at epoch {}", + fault.miner_address, fault.detection_epoch + ); + + let client = + crate::rpc::Client::default_or_from_env(None).context("Failed to create RPC client")?; + + // Get the reporter address (use configured address or default wallet) + let from = if let Some(addr) = &self.reporter_address { + *addr + } else { + match crate::rpc::wallet::WalletDefaultAddress::call(&client, ()).await { + Ok(Some(addr)) => addr, + Ok(None) | Err(_) => { + return Err(anyhow::anyhow!( + "No wallet address configured for slasher. Set FOREST_FAULTREPORTER_CONSENSUSFAULTREPORTERADDRESS or configure a default wallet." + )); + } + } + }; + + let params = fil_actor_miner_state::v16::ReportConsensusFaultParams { + header1: fault.block_headers[0].to_bytes(), + header2: fault.block_headers[1].to_bytes(), + header_extra: fault + .extra_evidence + .map(|cid| cid.to_bytes()) + .unwrap_or_default(), + }; + + let message = crate::shim::message::Message { + from, + to: fault.miner_address, + value: crate::shim::econ::TokenAmount::default(), + method_num: fil_actor_miner_state::v16::Method::ReportConsensusFault as u64, + params: fvm_ipld_encoding::to_vec(¶ms) + .context("Failed to convert params to bytes")? + .into(), + ..Default::default() + }; + + let signed_msg = crate::rpc::mpool::MpoolPushMessage::call(&client, (message, None)) + .await + .context("Failed to submit consensus fault report")?; + + info!( + "Consensus fault report submitted successfully: {} (reporter={}, miner={}, fault_type={:?})", + signed_msg.message.cid(), + from, + fault.miner_address, + fault.fault_type + ); + + Ok(()) + } +} diff --git a/src/slasher/tests.rs b/src/slasher/tests.rs new file mode 100644 index 000000000000..9ec2c7877023 --- /dev/null +++ b/src/slasher/tests.rs @@ -0,0 +1,161 @@ +// Copyright 2019-2025 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::blocks::{CachingBlockHeader, TipsetKey}; +use crate::shim::address::Address; +use crate::shim::clock::ChainEpoch; +use cid::Cid; + +fn create_test_header( + miner: Address, + epoch: ChainEpoch, + parents: TipsetKey, + timestamp: Option, +) -> CachingBlockHeader { + use crate::blocks::RawBlockHeader; + use crate::shim::econ::TokenAmount; + use num::BigInt; + + let raw_header = RawBlockHeader { + miner_address: miner, + epoch, + parents, + weight: BigInt::from(0), + state_root: Cid::default(), + message_receipts: Cid::default(), + messages: Cid::default(), + timestamp: timestamp.unwrap_or(0), + parent_base_fee: TokenAmount::from_atto(0), + ticket: None, + election_proof: None, + beacon_entries: Vec::new(), + winning_post_proof: Vec::new(), + bls_aggregate: None, + signature: None, + fork_signal: 0, + }; + + CachingBlockHeader::new(raw_header) +} + +#[test] +fn test_filter_double_fork_mining() { + use crate::slasher::filter::SlasherFilter; + use crate::slasher::types::ConsensusFaultType; + + let _ = std::fs::remove_dir_all(std::env::temp_dir().join("slasher_test")); + let mut filter = SlasherFilter::new(std::env::temp_dir().join("slasher_test")) + .expect("Failed to create slasher filter"); + + let miner = Address::new_id(1000); + let epoch = 100; + let parents = TipsetKey::from(nunny::vec![Cid::default()]); + + // Process first block - should not detect any fault + let header1 = create_test_header(miner, epoch, parents.clone(), Some(1000)); + let result1 = filter + .process_block(&header1) + .expect("Failed to process first block"); + assert!(result1.is_none()); + + // Process second block - should detect double-fork mining + let header2 = create_test_header(miner, epoch, parents, Some(2000)); + let result2 = filter + .process_block(&header2) + .expect("Failed to process second block"); + assert!(result2.is_some()); + + if let Some(fault) = result2 { + assert_eq!(fault.fault_type, ConsensusFaultType::DoubleForkMining); + assert_eq!(fault.miner_address, miner); + assert_eq!(fault.detection_epoch, epoch); + assert_eq!(fault.block_headers.len(), 2); + assert!(fault.extra_evidence.is_none()); + } +} + +#[test] +fn test_filter_time_offset_mining() { + use crate::slasher::filter::SlasherFilter; + use crate::slasher::types::ConsensusFaultType; + + let _ = std::fs::remove_dir_all(std::env::temp_dir().join("slasher_test")); + let mut filter = SlasherFilter::new(std::env::temp_dir().join("slasher_test")) + .expect("Failed to create slasher filter"); + + let miner = Address::new_id(1000); + let epoch = 100; + let parents = TipsetKey::from(nunny::vec![Cid::default()]); + + // Process first block with specific parents + let header1 = create_test_header(miner, epoch, parents.clone(), Some(1000)); + let result1 = filter + .process_block(&header1) + .expect("Failed to process first block"); + assert!(result1.is_none()); + + // Process second block with same parents but different timestamp - should detect time-offset mining + let header2 = create_test_header(miner, epoch, parents, Some(2000)); + let result2 = filter + .process_block(&header2) + .expect("Failed to process second block"); + assert!(result2.is_some()); + + if let Some(fault) = result2 { + // Note: With same parents, double-fork mining is detected first + assert_eq!(fault.fault_type, ConsensusFaultType::DoubleForkMining); + assert_eq!(fault.miner_address, miner); + assert_eq!(fault.detection_epoch, epoch); + assert_eq!(fault.block_headers.len(), 2); + assert!(fault.extra_evidence.is_none()); + } +} + +#[test] +fn test_filter_parent_grinding() { + use crate::slasher::filter::SlasherFilter; + use crate::slasher::types::ConsensusFaultType; + + let _ = std::fs::remove_dir_all(std::env::temp_dir().join("slasher_test")); + let mut filter = SlasherFilter::new(std::env::temp_dir().join("slasher_test")) + .expect("Failed to create slasher filter"); + + let miner = Address::new_id(1000); + let epoch1 = 100; + let epoch2 = 101; + let parents1 = TipsetKey::from(nunny::vec![Cid::default()]); + + // Create miner's block at an epoch + let header1 = create_test_header(miner, epoch1, parents1.clone(), Some(1000)); + let result1 = filter + .process_block(&header1) + .expect("Failed to process first block"); + assert!(result1.is_none()); + + // Create another block at same epoch but different parents + let parents2 = TipsetKey::from(nunny::vec![Cid::default()]); + let header2 = create_test_header(Address::new_id(2000), epoch1, parents2.clone(), Some(2000)); + let result2 = filter + .process_block(&header2) + .expect("Failed to process second block"); + assert!(result2.is_none()); + + let header3 = create_test_header( + miner, + epoch2, + TipsetKey::from(nunny::vec![*header2.cid()]), + Some(3000), + ); + let result3 = filter + .process_block(&header3) + .expect("Failed to process third block"); + assert!(result3.is_some()); + + if let Some(fault) = result3 { + assert_eq!(fault.fault_type, ConsensusFaultType::ParentGrinding); + assert_eq!(fault.miner_address, miner); + assert_eq!(fault.detection_epoch, epoch2); + assert_eq!(fault.block_headers.len(), 2); + assert!(fault.extra_evidence.is_some()); + } +} diff --git a/src/slasher/types.rs b/src/slasher/types.rs new file mode 100644 index 000000000000..692be0ca6d14 --- /dev/null +++ b/src/slasher/types.rs @@ -0,0 +1,33 @@ +// Copyright 2019-2025 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::shim::address::Address; +use crate::shim::clock::ChainEpoch; +use cid::Cid; +use serde::{Deserialize, Serialize}; + +/// Represents a detected consensus fault +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConsensusFault { + /// The miner address that committed the fault + pub miner_address: Address, + /// The epoch when the fault was detected + pub detection_epoch: ChainEpoch, + /// The type of consensus fault + pub fault_type: ConsensusFaultType, + /// The block headers involved in the fault + pub block_headers: Vec, + /// Additional evidence for parent-grinding faults + pub extra_evidence: Option, +} + +/// Types of consensus faults that can be detected +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Hash)] +pub enum ConsensusFaultType { + /// Two blocks at the same epoch by the same miner + DoubleForkMining, + /// Two blocks with the same parents by the same miner + TimeOffsetMining, + /// Miner ignored their own block and mined on others + ParentGrinding, +} From 2f109e656c739adfbd28799a73f8d6c00ebbdc54 Mon Sep 17 00:00:00 2001 From: Shashank Date: Tue, 5 Aug 2025 14:40:39 +0530 Subject: [PATCH 2/6] Add slasher env vars to env list --- docs/docs/users/guides/slasher.md | 34 ---------------------- docs/docs/users/reference/env_variables.md | 3 ++ 2 files changed, 3 insertions(+), 34 deletions(-) delete mode 100644 docs/docs/users/guides/slasher.md diff --git a/docs/docs/users/guides/slasher.md b/docs/docs/users/guides/slasher.md deleted file mode 100644 index 276e186487e8..000000000000 --- a/docs/docs/users/guides/slasher.md +++ /dev/null @@ -1,34 +0,0 @@ -# Forest Slasher Service - -A consensus fault detection service for Forest that monitors incoming blocks and detects malicious behaviors like double-mining, time-offset mining, and parent-grinding. - -## Configuration - -The slasher service is configured through environment variables. - -### Environment Variables - -- `FOREST_FAULTREPORTER_ENABLECONSENSUSFAULTREPORTER`: Enable/disable the consensus fault reporter (default: false) -- `FOREST_FAULTREPORTER_CONSENSUSFAULTREPORTERDATADIR`: Directory for storing slasher data (default: `.forest/slasher`) -- `FOREST_FAULTREPORTER_CONSENSUSFAULTREPORTERADDRESS`: Wallet address for submitting fault reports (optional) - -### Usage Example - -```bash -# Enable the slasher service -export FOREST_FAULTREPORTER_ENABLECONSENSUSFAULTREPORTER=true - -# Set the data directory (optional, defaults to .forest/slasher) -export FOREST_FAULTREPORTER_CONSENSUSFAULTREPORTERDATADIR="/path/to/slasher/directory" - -# Set the reporter address (optional) -export FOREST_FAULTREPORTER_CONSENSUSFAULTREPORTERADDRESS="f1abc123..." -``` - -### Fault Detection - -The service detects three types of consensus faults: - -1. **Double-fork mining**: Same miner produces multiple blocks at the same epoch -2. **Time-offset mining**: Same miner produces multiple blocks with the same parents -3. **Parent-grinding**: Miner ignores their own block and mines on others diff --git a/docs/docs/users/reference/env_variables.md b/docs/docs/users/reference/env_variables.md index 3457e8efd7c1..3f4907f9c7b2 100644 --- a/docs/docs/users/reference/env_variables.md +++ b/docs/docs/users/reference/env_variables.md @@ -50,6 +50,9 @@ process. | `FOREST_SNAPSHOT_GC_CHECK_INTERVAL_SECONDS` | non-negative integer | 300 | 60 | The interval in seconds for checking if snapshot GC should run | | `FOREST_DISABLE_BAD_BLOCK_CACHE` | 1 or true | empty | 1 | Whether or not to disable bad block cache | | `FOREST_ZSTD_FRAME_CACHE_DEFAULT_MAX_SIZE` | positive integer | 268435456 | 536870912 | The default zstd frame cache max size in bytes | +| `FOREST_FAULTREPORTER_ENABLECONSENSUSFAULTREPORTER` | 1 or true | false | 1 | Enable/disable the consensus fault reporter (slasher service) | +| `FOREST_FAULTREPORTER_CONSENSUSFAULTREPORTERDATADIR` | directory path | `.forest/slasher` | `/path/to/slasher/directory` | Directory for storing slasher data | +| `FOREST_FAULTREPORTER_CONSENSUSFAULTREPORTERADDRESS` | wallet address | empty (uses default wallet) | `f1abc123...` | Wallet address for submitting fault reports (optional) | ### `FOREST_F3_SIDECAR_FFI_BUILD_OPT_OUT` From 5ac7800a3a97efe380b5f5c4a6ea9b2ba7442fd6 Mon Sep 17 00:00:00 2001 From: Shashank Date: Tue, 5 Aug 2025 14:44:04 +0530 Subject: [PATCH 3/6] fmt --- docs/docs/users/reference/env_variables.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/users/reference/env_variables.md b/docs/docs/users/reference/env_variables.md index 3f4907f9c7b2..a5a669a1e538 100644 --- a/docs/docs/users/reference/env_variables.md +++ b/docs/docs/users/reference/env_variables.md @@ -51,7 +51,7 @@ process. | `FOREST_DISABLE_BAD_BLOCK_CACHE` | 1 or true | empty | 1 | Whether or not to disable bad block cache | | `FOREST_ZSTD_FRAME_CACHE_DEFAULT_MAX_SIZE` | positive integer | 268435456 | 536870912 | The default zstd frame cache max size in bytes | | `FOREST_FAULTREPORTER_ENABLECONSENSUSFAULTREPORTER` | 1 or true | false | 1 | Enable/disable the consensus fault reporter (slasher service) | -| `FOREST_FAULTREPORTER_CONSENSUSFAULTREPORTERDATADIR` | directory path | `.forest/slasher` | `/path/to/slasher/directory` | Directory for storing slasher data | +| `FOREST_FAULTREPORTER_CONSENSUSFAULTREPORTERDATADIR` | directory path | `.forest/slasher` | `/path/to/slasher/directory` | Directory for storing slasher data | | `FOREST_FAULTREPORTER_CONSENSUSFAULTREPORTERADDRESS` | wallet address | empty (uses default wallet) | `f1abc123...` | Wallet address for submitting fault reports (optional) | ### `FOREST_F3_SIDECAR_FFI_BUILD_OPT_OUT` From a58eddee87f7e0cf6607aea57f88cc8bc578a5ad Mon Sep 17 00:00:00 2001 From: Shashank Date: Tue, 5 Aug 2025 15:39:42 +0530 Subject: [PATCH 4/6] lint fix --- src/slasher/filter.rs | 6 ++++-- src/slasher/service.rs | 17 +++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/slasher/filter.rs b/src/slasher/filter.rs index ca73dba6ec27..db366cca3579 100644 --- a/src/slasher/filter.rs +++ b/src/slasher/filter.rs @@ -26,8 +26,10 @@ impl SlasherFilter { std::fs::create_dir_all(&data_dir)?; let mut options = Options::with_columns(&data_dir, 1); - options.columns[0].btree_index = true; - options.columns[0].uniform = false; + if let Some(column) = options.columns.get_mut(0) { + column.btree_index = true; + column.uniform = false; + } let db = Db::open_or_create(&options)?; diff --git a/src/slasher/service.rs b/src/slasher/service.rs index a9c951e80250..70452a6d1617 100644 --- a/src/slasher/service.rs +++ b/src/slasher/service.rs @@ -32,10 +32,7 @@ impl SlasherService { let filter = Arc::new(RwLock::new( SlasherFilter::new(data_dir.clone()).with_context(|| { - format!( - "Failed to create slasher filter in directory: {:?}", - data_dir - ) + format!("Failed to create slasher filter in directory: {data_dir:?}") })?, )); @@ -111,8 +108,16 @@ impl SlasherService { }; let params = fil_actor_miner_state::v16::ReportConsensusFaultParams { - header1: fault.block_headers[0].to_bytes(), - header2: fault.block_headers[1].to_bytes(), + header1: fault + .block_headers + .first() + .ok_or_else(|| anyhow::anyhow!("No first block header found"))? + .to_bytes(), + header2: fault + .block_headers + .get(1) + .ok_or_else(|| anyhow::anyhow!("No second block header found"))? + .to_bytes(), header_extra: fault .extra_evidence .map(|cid| cid.to_bytes()) From 72cdcbebfcdaeeef30b34453807dc4fe16ad7ecb Mon Sep 17 00:00:00 2001 From: Shashank Date: Tue, 5 Aug 2025 16:02:14 +0530 Subject: [PATCH 5/6] more lint fix --- src/chain/store/tipset_tracker.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chain/store/tipset_tracker.rs b/src/chain/store/tipset_tracker.rs index b8dec03799c5..dcf2948c5acf 100644 --- a/src/chain/store/tipset_tracker.rs +++ b/src/chain/store/tipset_tracker.rs @@ -34,7 +34,7 @@ impl TipsetTracker { } } - /// Create a new TipsetTracker with slasher service integration + /// Create a new [`TipsetTracker`] with slasher service enabled pub fn with_slasher( db: Arc, chain_config: Arc, From 43a970467631bff7abf611d47cf68a1e61895173 Mon Sep 17 00:00:00 2001 From: Shashank Date: Mon, 11 Aug 2025 00:02:16 +0530 Subject: [PATCH 6/6] fix --- src/chain/store/tipset_tracker.rs | 4 +- src/slasher/db.rs | 65 +++++++++ src/slasher/filter.rs | 232 ------------------------------ src/slasher/mod.rs | 2 +- src/slasher/service.rs | 213 ++++++++++++++++++++++----- src/slasher/tests.rs | 199 ++++++++++++------------- src/slasher/types.rs | 8 +- 7 files changed, 342 insertions(+), 381 deletions(-) create mode 100644 src/slasher/db.rs delete mode 100644 src/slasher/filter.rs diff --git a/src/chain/store/tipset_tracker.rs b/src/chain/store/tipset_tracker.rs index dcf2948c5acf..46cb264ecfb4 100644 --- a/src/chain/store/tipset_tracker.rs +++ b/src/chain/store/tipset_tracker.rs @@ -60,7 +60,7 @@ impl TipsetTracker { cids.push(*header.cid()); drop(map_lock); - self.check_consensus_fault(header); + self.check_consensus_faults(header); self.check_multiple_blocks_from_same_miner(&cids_to_verify, header); self.prune_entries(header.epoch); } @@ -88,7 +88,7 @@ impl TipsetTracker { } /// Process block with slasher service for consensus fault detection - fn check_consensus_fault(&self, header: &CachingBlockHeader) { + fn check_consensus_faults(&self, header: &CachingBlockHeader) { if let Some(slasher) = &self.slasher_service { let slasher = slasher.clone(); let header = header.clone(); diff --git a/src/slasher/db.rs b/src/slasher/db.rs new file mode 100644 index 000000000000..1dfc42c1834d --- /dev/null +++ b/src/slasher/db.rs @@ -0,0 +1,65 @@ +// Copyright 2019-2025 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::blocks::CachingBlockHeader; +use anyhow::Result; +use parity_db::{Db, Options}; + +pub struct SlasherDb { + db: Db, +} + +pub enum SlasherDbColumns { + ByEpoch = 0, + ByParents = 1, +} + +impl SlasherDb { + pub fn new(data_dir: std::path::PathBuf) -> Result { + std::fs::create_dir_all(&data_dir)?; + + let mut options = Options::with_columns(&data_dir, 2); + if let Some(column) = options.columns.get_mut(SlasherDbColumns::ByEpoch as usize) { + column.btree_index = true; + column.uniform = false; + } + if let Some(column) = options + .columns + .get_mut(SlasherDbColumns::ByParents as usize) + { + column.btree_index = true; + column.uniform = false; + } + + let db = Db::open_or_create(&options)?; + + Ok(Self { db }) + } + + pub fn put(&mut self, header: &CachingBlockHeader) -> Result<()> { + let miner = header.miner_address; + let epoch = header.epoch; + + let epoch_key = format!("{}/{}", miner, epoch); + let parent_key = format!("{}/{}", miner, header.parents); + + self.db.commit(vec![ + ( + SlasherDbColumns::ByEpoch as u8, + epoch_key.as_bytes(), + Some(header.cid().to_bytes()), + ), + ( + SlasherDbColumns::ByParents as u8, + parent_key.as_bytes(), + Some(header.cid().to_bytes()), + ), + ])?; + + Ok(()) + } + + pub fn get(&self, column: u8, key: &[u8]) -> Result>> { + Ok(self.db.get(column, key)?) + } +} diff --git a/src/slasher/filter.rs b/src/slasher/filter.rs deleted file mode 100644 index db366cca3579..000000000000 --- a/src/slasher/filter.rs +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright 2019-2025 ChainSafe Systems -// SPDX-License-Identifier: Apache-2.0, MIT - -use crate::blocks::CachingBlockHeader; -use crate::shim::address::Address; -use crate::shim::clock::ChainEpoch; -use crate::slasher::types::*; -use anyhow::Result; -use parity_db::{Db, Options}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct BlockInfo { - miner_address: Vec, - epoch: ChainEpoch, - parents: crate::blocks::TipsetKey, - cid: cid::Cid, -} - -pub struct SlasherFilter { - db: Db, -} - -impl SlasherFilter { - pub fn new(data_dir: std::path::PathBuf) -> Result { - std::fs::create_dir_all(&data_dir)?; - - let mut options = Options::with_columns(&data_dir, 1); - if let Some(column) = options.columns.get_mut(0) { - column.btree_index = true; - column.uniform = false; - } - - let db = Db::open_or_create(&options)?; - - Ok(Self { db }) - } - - pub fn process_block(&mut self, header: &CachingBlockHeader) -> Result> { - self.add_to_history(header)?; - let fault = self.check_consensus_faults(header)?; - - Ok(fault) - } - - fn add_to_history(&mut self, header: &CachingBlockHeader) -> Result<()> { - let miner = header.miner_address; - let epoch = header.epoch; - - let block_info = BlockInfo { - miner_address: miner.to_bytes(), - epoch, - parents: header.parents.clone(), - cid: *header.cid(), - }; - - let key = self.create_db_key(miner, epoch, header.cid()); - let value = serde_json::to_vec(&block_info)?; - - self.db.commit(vec![(0, key, Some(value))])?; - - Ok(()) - } - - fn create_db_key(&self, miner: Address, epoch: ChainEpoch, cid: &cid::Cid) -> Vec { - let mut key = Vec::new(); - key.extend_from_slice(&miner.to_bytes()); - key.extend_from_slice(&epoch.to_le_bytes()); - key.extend_from_slice(cid.to_bytes().as_slice()); - key - } - - fn load_blocks_from_db(&mut self, miner: Address, epoch: ChainEpoch) -> Result> { - let mut blocks = Vec::new(); - let prefix = self.create_db_key_prefix(miner, epoch); - - let mut iter = self.db.iter(0)?; - while let Some((key, value)) = iter.next()? { - if key.starts_with(&prefix) { - if let Ok(block_info) = serde_json::from_slice::(&value) { - blocks.push(block_info); - } - } - } - - Ok(blocks) - } - - fn create_db_key_prefix(&self, miner: Address, epoch: ChainEpoch) -> Vec { - let mut prefix = Vec::new(); - prefix.extend_from_slice(&miner.to_bytes()); - prefix.extend_from_slice(&epoch.to_le_bytes()); - prefix - } - - fn check_consensus_faults( - &mut self, - header: &CachingBlockHeader, - ) -> Result> { - if let Some(fault) = self.check_double_fork_mining(header)? { - return Ok(Some(fault)); - } - - if let Some(fault) = self.check_time_offset_mining(header)? { - return Ok(Some(fault)); - } - - if let Some(fault) = self.check_parent_grinding(header)? { - return Ok(Some(fault)); - } - - Ok(None) - } - - fn check_double_fork_mining( - &mut self, - header: &CachingBlockHeader, - ) -> Result> { - let miner = header.miner_address; - let epoch = header.epoch; - - let blocks = self.load_blocks_from_db(miner, epoch)?; - - // If we have more than one block from this miner at this epoch, it's double-fork mining - if blocks.len() > 1 { - let block_headers: Vec = blocks.iter().map(|b| b.cid).collect(); - - return Ok(Some(ConsensusFault { - miner_address: miner, - detection_epoch: epoch, - fault_type: ConsensusFaultType::DoubleForkMining, - block_headers, - extra_evidence: None, - })); - } - - Ok(None) - } - - fn check_time_offset_mining( - &mut self, - header: &CachingBlockHeader, - ) -> Result> { - let miner = header.miner_address; - let current_cid = *header.cid(); - - let mut iter = self.db.iter(0)?; - let mut same_parent_blocks = Vec::new(); - - while let Some((_, value)) = iter.next()? { - if let Ok(block_info) = serde_json::from_slice::(&value) { - if block_info.parents == header.parents && block_info.cid != current_cid { - if let Ok(block_miner) = Address::from_bytes(&block_info.miner_address) { - if block_miner == miner { - same_parent_blocks.push(block_info); - } - } - } - } - } - - if !same_parent_blocks.is_empty() { - let mut block_headers = vec![current_cid]; - block_headers.extend(same_parent_blocks.iter().map(|b| b.cid)); - - return Ok(Some(ConsensusFault { - miner_address: miner, - detection_epoch: header.epoch, - fault_type: ConsensusFaultType::TimeOffsetMining, - block_headers, - extra_evidence: None, - })); - } - - Ok(None) - } - - fn check_parent_grinding( - &mut self, - header: &CachingBlockHeader, - ) -> Result> { - let miner = header.miner_address; - let current_epoch = header.epoch; - - if current_epoch < 1 { - return Ok(None); - } - - let prev_blocks = self.load_blocks_from_db(miner, current_epoch - 1)?; - - for prev_block in prev_blocks { - if header.parents.contains(prev_block.cid) { - continue; - } - - if let Some(witness) = self.find_parent_grinding_witness(header, &prev_block)? { - return Ok(Some(ConsensusFault { - miner_address: miner, - detection_epoch: current_epoch, - fault_type: ConsensusFaultType::ParentGrinding, - block_headers: vec![prev_block.cid, *header.cid()], - extra_evidence: Some(witness.cid), - })); - } - } - - Ok(None) - } - - fn find_parent_grinding_witness( - &self, - current_block: &CachingBlockHeader, - miner_prev_block: &BlockInfo, - ) -> Result> { - let prev_epoch = current_block.epoch - 1; - - let mut iter = self.db.iter(0)?; - while let Some((_, value)) = iter.next()? { - if let Ok(block_info) = serde_json::from_slice::(&value) { - if block_info.epoch == prev_epoch - && block_info.parents == miner_prev_block.parents - && block_info.cid != miner_prev_block.cid - && current_block.parents.contains(block_info.cid) - { - return Ok(Some(block_info)); - } - } - } - - Ok(None) - } -} diff --git a/src/slasher/mod.rs b/src/slasher/mod.rs index 7066fcb3b760..17aff866d2e7 100644 --- a/src/slasher/mod.rs +++ b/src/slasher/mod.rs @@ -1,7 +1,7 @@ // Copyright 2019-2025 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -pub mod filter; +pub mod db; pub mod service; pub mod types; diff --git a/src/slasher/service.rs b/src/slasher/service.rs index 70452a6d1617..eef4c9427ab0 100644 --- a/src/slasher/service.rs +++ b/src/slasher/service.rs @@ -2,11 +2,15 @@ // SPDX-License-Identifier: Apache-2.0, MIT use crate::blocks::CachingBlockHeader; +use crate::rpc::Client; use crate::rpc::RpcMethodExt; -use crate::slasher::filter::SlasherFilter; -use crate::slasher::types::ConsensusFault; +use crate::rpc::chain::ChainGetBlock; +use crate::slasher::db::*; +use crate::slasher::types::{ConsensusFault, ConsensusFaultType}; use anyhow::{Context, Result}; +use cid::Cid; use fvm_ipld_encoding; +use fvm_ipld_encoding::to_vec; use std::path::PathBuf; use std::sync::Arc; use tokio::sync::RwLock; @@ -14,8 +18,10 @@ use tracing::{info, warn}; /// Slasher service that orchestrates consensus fault detection and reporting pub struct SlasherService { - /// Core fault detection filter - filter: Arc>, + /// Database for storing slasher history + db: Arc>, + /// RPC Client + client: Client, /// Reporter address for submitting fault reports reporter_address: Option, } @@ -30,11 +36,12 @@ impl SlasherService { .ok() .and_then(|addr_str| addr_str.parse().ok()); - let filter = Arc::new(RwLock::new( - SlasherFilter::new(data_dir.clone()).with_context(|| { - format!("Failed to create slasher filter in directory: {data_dir:?}") - })?, - )); + let client = + crate::rpc::Client::default_or_from_env(None).context("Failed to create RPC client")?; + + let db = Arc::new(RwLock::new(SlasherDb::new(data_dir.clone()).with_context( + || format!("Failed to create slasher db in directory: {data_dir:?}"), + )?)); info!( "Slasher service initialized with data directory: {:?}", @@ -47,11 +54,156 @@ impl SlasherService { } Ok(Self { - filter, + db, + client, reporter_address, }) } + async fn check_fault(&self, header: &CachingBlockHeader) -> Result> { + if let Some(fault) = self.check_double_fork_mining(header).await? { + return Ok(Some(fault)); + } + + if let Some(fault) = self.check_time_offset_mining(header).await? { + return Ok(Some(fault)); + } + + if let Some(fault) = self.check_parent_grinding(header).await? { + return Ok(Some(fault)); + } + + self.db + .write() + .await + .put(header) + .context("Failed to add block header to slasher history")?; + Ok(None) + } + + async fn check_double_fork_mining( + &self, + header: &CachingBlockHeader, + ) -> Result> { + let miner = header.miner_address; + let epoch = header.epoch; + let epoch_key = format!("{}/{}", miner, epoch); + + // Check if we already have a block from this miner at this epoch + let existing_block_cid = { + let db = self.db.read().await; + db.get(SlasherDbColumns::ByEpoch as u8, epoch_key.as_bytes())? + }; + + if let Some(block_cid_bytes) = existing_block_cid { + let existing_block_cid = Cid::try_from(block_cid_bytes)?; + + // Get the existing block to compare + let existing_block = ChainGetBlock::call(&self.client, (existing_block_cid,)).await?; + + // Check if this is a different block (double fork mining) + if existing_block.cid() != header.cid() && existing_block.epoch == header.epoch { + return Ok(Some(ConsensusFault { + miner_address: miner, + detection_epoch: epoch, + fault_type: ConsensusFaultType::DoubleForkMining, + block_header_1: existing_block, + block_header_2: header.clone(), + block_header_extra: None, + })); + } + } + Ok(None) + } + + async fn check_time_offset_mining( + &self, + header: &CachingBlockHeader, + ) -> Result> { + let miner = header.miner_address; + let parents = &header.parents; + let parent_key = format!("{}/{}", miner, parents); + + // Check if we already have a block from this miner with the same parents + let existing_block_cid = { + let db = self.db.read().await; + db.get(SlasherDbColumns::ByParents as u8, parent_key.as_bytes())? + }; + + if let Some(block_cid_bytes) = existing_block_cid { + let existing_block_cid = Cid::try_from(block_cid_bytes)?; + + // Get the existing block to compare + let existing_block = ChainGetBlock::call(&self.client, (existing_block_cid,)).await?; + + // Check if this is a time offset mining fault + if existing_block.cid() != header.cid() + && existing_block.parents == header.parents + && existing_block.epoch != header.epoch + { + return Ok(Some(ConsensusFault { + miner_address: miner, + detection_epoch: header.epoch, + fault_type: ConsensusFaultType::TimeOffsetMining, + block_header_1: existing_block, + block_header_2: header.clone(), + block_header_extra: None, + })); + } + } + + Ok(None) + } + + async fn check_parent_grinding( + &self, + header: &CachingBlockHeader, + ) -> Result> { + // Parent grinding detection is complex and requires: + // 1. Block A: Mined by same miner at epoch N with parents P + // 2. Block C: Sibling of A (same epoch N, same parents P, different miner) + // 3. Block B: Current block at later epoch, includes C but excludes A + + let miner = header.miner_address; + let epoch = header.epoch; + let parents = &header.parents; + let parent_block = ChainGetBlock::call(&self.client, (parents.cid()?,)).await?; + let parent_epoch = parent_block.epoch; + + let parent_epoch_key = format!("{}/{}", miner, parent_epoch); + + // Check if we already have a block from this miner at this epoch + let existing_parent_block_cid = { + let db = self.db.read().await; + db.get(SlasherDbColumns::ByEpoch as u8, parent_epoch_key.as_bytes())? + }; + + if let Some(block_cid_bytes) = existing_parent_block_cid { + let parent_cid = Cid::try_from(block_cid_bytes)?; + if parents.contains(parent_cid) { + let existing_parent_block = + ChainGetBlock::call(&self.client, (parent_cid,)).await?; + if existing_parent_block.parents == parent_block.parents + && existing_parent_block.epoch == parent_block.epoch + && header.parents.contains(*parent_block.cid()) + && !header.parents.contains(*existing_parent_block.cid()) + { + // Detected parent grinding fault + return Ok(Some(ConsensusFault { + miner_address: miner, + detection_epoch: epoch, + fault_type: ConsensusFaultType::ParentGrinding, + block_header_1: existing_parent_block, + block_header_2: header.clone(), + block_header_extra: Some(parent_block), + })); + } + } + } + + Ok(None) + } + pub async fn process_block(&self, header: &CachingBlockHeader) -> Result<()> { info!( "Checking consensus fault for {} by miner address {} at epoch {}", @@ -60,10 +212,7 @@ impl SlasherService { header.epoch ); - let fault = { - let mut filter = self.filter.write().await; - filter.process_block(header)? - }; + let fault = self.check_fault(header).await?; if let Some(fault) = fault { info!( @@ -71,9 +220,9 @@ impl SlasherService { fault.fault_type, fault.miner_address, fault.detection_epoch ); - warn!( - "Fault details - Block headers: {:?}, Extra evidence: {:?}", - fault.block_headers, fault.extra_evidence + info!( + "Fault details - Block header 1: {:?}, Block header 2: {:?}, Extra evidence: {:?}", + fault.block_header_1, fault.block_header_2, fault.block_header_extra ); if let Err(e) = self.submit_fault_report(&fault).await { @@ -84,20 +233,17 @@ impl SlasherService { Ok(()) } - async fn submit_fault_report(&self, fault: &ConsensusFault) -> Result<()> { + pub async fn submit_fault_report(&self, fault: &ConsensusFault) -> Result<()> { info!( "Submitting consensus fault report for miner {} at epoch {}", fault.miner_address, fault.detection_epoch ); - let client = - crate::rpc::Client::default_or_from_env(None).context("Failed to create RPC client")?; - // Get the reporter address (use configured address or default wallet) let from = if let Some(addr) = &self.reporter_address { *addr } else { - match crate::rpc::wallet::WalletDefaultAddress::call(&client, ()).await { + match crate::rpc::wallet::WalletDefaultAddress::call(&self.client, ()).await { Ok(Some(addr)) => addr, Ok(None) | Err(_) => { return Err(anyhow::anyhow!( @@ -106,22 +252,13 @@ impl SlasherService { } } }; - let params = fil_actor_miner_state::v16::ReportConsensusFaultParams { - header1: fault - .block_headers - .first() - .ok_or_else(|| anyhow::anyhow!("No first block header found"))? - .to_bytes(), - header2: fault - .block_headers - .get(1) - .ok_or_else(|| anyhow::anyhow!("No second block header found"))? - .to_bytes(), - header_extra: fault - .extra_evidence - .map(|cid| cid.to_bytes()) - .unwrap_or_default(), + header1: to_vec(&fault.block_header_1)?, + header2: to_vec(&fault.block_header_2)?, + header_extra: match &fault.block_header_extra { + Some(header) => to_vec(header)?, + None => Vec::new(), + }, }; let message = crate::shim::message::Message { @@ -135,7 +272,7 @@ impl SlasherService { ..Default::default() }; - let signed_msg = crate::rpc::mpool::MpoolPushMessage::call(&client, (message, None)) + let signed_msg = crate::rpc::mpool::MpoolPushMessage::call(&self.client, (message, None)) .await .context("Failed to submit consensus fault report")?; diff --git a/src/slasher/tests.rs b/src/slasher/tests.rs index 9ec2c7877023..e5dc7939f686 100644 --- a/src/slasher/tests.rs +++ b/src/slasher/tests.rs @@ -40,122 +40,111 @@ fn create_test_header( #[test] fn test_filter_double_fork_mining() { - use crate::slasher::filter::SlasherFilter; - use crate::slasher::types::ConsensusFaultType; - - let _ = std::fs::remove_dir_all(std::env::temp_dir().join("slasher_test")); - let mut filter = SlasherFilter::new(std::env::temp_dir().join("slasher_test")) - .expect("Failed to create slasher filter"); - - let miner = Address::new_id(1000); - let epoch = 100; - let parents = TipsetKey::from(nunny::vec![Cid::default()]); - - // Process first block - should not detect any fault - let header1 = create_test_header(miner, epoch, parents.clone(), Some(1000)); - let result1 = filter - .process_block(&header1) - .expect("Failed to process first block"); - assert!(result1.is_none()); - - // Process second block - should detect double-fork mining - let header2 = create_test_header(miner, epoch, parents, Some(2000)); - let result2 = filter - .process_block(&header2) - .expect("Failed to process second block"); - assert!(result2.is_some()); - - if let Some(fault) = result2 { - assert_eq!(fault.fault_type, ConsensusFaultType::DoubleForkMining); - assert_eq!(fault.miner_address, miner); - assert_eq!(fault.detection_epoch, epoch); - assert_eq!(fault.block_headers.len(), 2); - assert!(fault.extra_evidence.is_none()); - } + use crate::slasher::db::SlasherDb; + use crate::utils::encoding::from_slice_with_fallback; + + let _ = std::fs::remove_dir_all(std::env::temp_dir().join("slasher_test_double_fork")); + let mut db = SlasherDb::new(std::env::temp_dir().join("slasher_test_double_fork")) + .expect("Failed to create slasher db"); + + let bz = hex::decode("90440093f37a815860afc066776903f91344937c1ac1351070e68813aad3623184211bc5f1aaa81eba16d6e48550800bb9bf4195d9fbd5651c1045dfa1e71b0cd8e17642f3469aa5adf2e69b18b7f74031324d1826d0f84ccd01114f8993c9cf8ec366f60ae7a78dea82015860a5a10dbc3be5ddbc50b998d9276cc2a38f8684e349318eae898ee5c30e081625824749e08d94dba48c38ed68c0725852164a81fcfaee16d36270099962e49e2cc878306962dd16050713477873f8e66410f3bde90d878c503e5f637dc4583f5181821a0133c4865830aed926ff74d16217e4c8dcc6d5682aaf4adb43311be06f3022645bbde9a5499c6ee413f8f3a489a923967e20f5a5a4c481820358c098e17ca5d50ff3ef14dac93283bee5d16d0ed50b82470acebab491ead4bc49a1f05a39308f0c862e8aae5d1466bb88d299f0e3901eba20e9848963499ff04e8295045b35da1cc79c0bd45d610f6dfa4bc78d1a9ec177c6fca42dc6a140cca32a043339634f7a29cd3f13fb8d145e7d068356b1c5b9fe49bc32c878a67f38a326ba0d8d55d55fbd3ee5fe298ae78487d3a328380d68c4a94c82361a63b89f7d75e3ecc6b7b19db03915e8df0ae8dc293f1cf3134d821d6fb64792573d0e7988dc84d82a5827000171a0e40220493e9e471b6c3ff1b7a2b1c5037ac0c73f71a0459034816ec4e487b4a9da53d8d82a5827000171a0e402209d477b2ad612a1b1da4b4958a6a2b3b30a39dac8abed837ec640d55a62afce54d82a5827000171a0e402204b3763feb0a4c3810b5570a35adc972f5ed657924d88b08c5e980553af4451bdd82a5827000171a0e4022004ce083decc01e9c4fb879574544e1c76116e65ff71fd3451d663ebf83807dab46001cba67cad41a004ed726d82a5827000171a0e40220b6b5e4010c9e5adcd72c44a7a799359893cf20abd27ee3d2ba845d08132f998cd82a5827000171a0e40220cecdb58a95df93eae8e28bc14d1bc832a0528db8ab593fa996560add1d511430d82a5827000171a0e40220c24231c099a8d8dc5d829060a64e28b38e5aae09527ba54365b3f5ae7cc07d5b586102828268aa61490e93d026433a3502f9505c1c290f558ef9a318b85b10a3b1dd10d8f2dc083c67feb50556b0cb201d4e49162a10bc98c8403ca96ed95de5317441182f657c211e6652fc3d06e596728888df25ec6aa830ac01b4151b83f60102eb1a68816ed4586102b310a8f6ff08b58d12435859a184d78f0f3f909eaaf68edb4c464cfcbb151fcab597286946a4114d01e53092d5e3bacd0868358adaf23af10b21307a0f3017528cc84e496bb59182fd0910f12bef73ecb725f68feaa246a4e537dde40c74acf500420064").unwrap(); + let bh_1 = from_slice_with_fallback::(&bz).unwrap(); + + let bz = hex::decode("90440093f37a815860afc066776903f91344937c1ac1351070e68813aad3623184211bc5f1aaa81eba16d6e48550800bb9bf4195d9fbd5651c1045dfa1e71b0cd8e17642f3469aa5adf2e69b18b7f74031324d1826d0f84ccd01114f8993c9cf8ec366f60ae7a78dea82015860a5a10dbc3be5ddbc50b998d9276cc2a38f8684e349318eae898ee5c30e081625824749e08d94dba48c38ed68c0725852164a81fcfaee16d36270099962e49e2cc878306962dd16050713477873f8e66410f3bde90d878c503e5f637dc4583f5181821a0133c4865830aed926ff74d16217e4c8dcc6d5682aaf4adb43311be06f3022645bbde9a5499c6ee413f8f3a489a923967e20f5a5a4c481820358c0b003f7619deaedafc85b00b2c01a1a2617ab0cf91248b9d1a1ace8d37a94dd9ada9d9830558620b96ea648f0d8818b6fb45f97134bc2c946279a09a257788701ed6aca5fe7e0465836dd26c0ebfe6b50984ac38cfb5f7d7ce3c73d69f5600cc40ba1b1f4d25f0b172685b9361040894460a9ffc2448dfbc234cb5f27ca4e26cba630bd27ff80766cb85d675db1c3ec67b0c1a5157b72bf7652a8ae9758f78e211811149d1bc39a0a905e394d62da01036e18e1ac921b995f09e85957ed7464ae84d82a5827000171a0e40220493e9e471b6c3ff1b7a2b1c5037ac0c73f71a0459034816ec4e487b4a9da53d8d82a5827000171a0e402209d477b2ad612a1b1da4b4958a6a2b3b30a39dac8abed837ec640d55a62afce54d82a5827000171a0e402204b3763feb0a4c3810b5570a35adc972f5ed657924d88b08c5e980553af4451bdd82a5827000171a0e4022004ce083decc01e9c4fb879574544e1c76116e65ff71fd3451d663ebf83807dab46001cba67cad41a004ed726d82a5827000171a0e40220b6b5e4010c9e5adcd72c44a7a799359893cf20abd27ee3d2ba845d08132f998cd82a5827000171a0e40220cecdb58a95df93eae8e28bc14d1bc832a0528db8ab593fa996560add1d511430d82a5827000171a0e40220c24231c099a8d8dc5d829060a64e28b38e5aae09527ba54365b3f5ae7cc07d5b586102828268aa61490e93d026433a3502f9505c1c290f558ef9a318b85b10a3b1dd10d8f2dc083c67feb50556b0cb201d4e49162a10bc98c8403ca96ed95de5317441182f657c211e6652fc3d06e596728888df25ec6aa830ac01b4151b83f60102eb1a68816ed458610297d04e238290b1c8abdba49c3f4c3825d3783be60dd42de32f9e86876fbecda986e3a739def814dc3a2e153b4013367409cbf3dcf9b6fe8269ca69a7043b5c89d7c1c5594d591983277fa6e0a254ce74548e005cd103eda8b05a40b33ccbb16f00420064").unwrap(); + let bh_2 = from_slice_with_fallback::(&bz).unwrap(); + + // Store bh_1 in the database + db.put(&bh_1).expect("Failed to add bh_1 to history"); + + // Check if we have a block from the same miner at the same epoch + let epoch_key = format!("{}/{}", bh_1.miner_address, bh_1.epoch); + let existing_block_cid = db + .get( + crate::slasher::db::SlasherDbColumns::ByEpoch as u8, + epoch_key.as_bytes(), + ) + .expect("Failed to get existing block"); + + assert!(existing_block_cid.is_some()); + assert_ne!(bh_1.cid(), bh_2.cid()); + assert_eq!(bh_1.epoch, bh_2.epoch); + assert_eq!(bh_1.miner_address, bh_2.miner_address); } #[test] fn test_filter_time_offset_mining() { - use crate::slasher::filter::SlasherFilter; - use crate::slasher::types::ConsensusFaultType; + use crate::slasher::db::SlasherDb; - let _ = std::fs::remove_dir_all(std::env::temp_dir().join("slasher_test")); - let mut filter = SlasherFilter::new(std::env::temp_dir().join("slasher_test")) - .expect("Failed to create slasher filter"); + let _ = std::fs::remove_dir_all(std::env::temp_dir().join("slasher_test_time_offset")); + let mut db = SlasherDb::new(std::env::temp_dir().join("slasher_test_time_offset")) + .expect("Failed to create slasher db"); let miner = Address::new_id(1000); - let epoch = 100; + let epoch1 = 100; + let epoch2 = 101; let parents = TipsetKey::from(nunny::vec![Cid::default()]); - // Process first block with specific parents - let header1 = create_test_header(miner, epoch, parents.clone(), Some(1000)); - let result1 = filter - .process_block(&header1) - .expect("Failed to process first block"); - assert!(result1.is_none()); - - // Process second block with same parents but different timestamp - should detect time-offset mining - let header2 = create_test_header(miner, epoch, parents, Some(2000)); - let result2 = filter - .process_block(&header2) - .expect("Failed to process second block"); - assert!(result2.is_some()); - - if let Some(fault) = result2 { - // Note: With same parents, double-fork mining is detected first - assert_eq!(fault.fault_type, ConsensusFaultType::DoubleForkMining); - assert_eq!(fault.miner_address, miner); - assert_eq!(fault.detection_epoch, epoch); - assert_eq!(fault.block_headers.len(), 2); - assert!(fault.extra_evidence.is_none()); - } + // Process first block with specific parents at epoch1 + let header1 = create_test_header(miner, epoch1, parents.clone(), Some(1000)); + db.put(&header1) + .expect("Failed to add first block to history"); + + // Process second block with same parents but different epoch - should detect time-offset mining + let header2 = create_test_header(miner, epoch2, parents.clone(), Some(2000)); + + // Check if we have a block from the same miner with the same parents + let parent_key = format!("{}/{}", miner, parents); + let existing_block_cid = db + .get( + crate::slasher::db::SlasherDbColumns::ByParents as u8, + parent_key.as_bytes(), + ) + .expect("Failed to get existing block"); + + assert!(existing_block_cid.is_some()); + + // Verify the blocks have same parents but different epochs (time offset mining) + assert_eq!(header1.parents, header2.parents); + assert_ne!(header1.epoch, header2.epoch); + assert_eq!(header1.miner_address, header2.miner_address); } #[test] fn test_filter_parent_grinding() { - use crate::slasher::filter::SlasherFilter; - use crate::slasher::types::ConsensusFaultType; - - let _ = std::fs::remove_dir_all(std::env::temp_dir().join("slasher_test")); - let mut filter = SlasherFilter::new(std::env::temp_dir().join("slasher_test")) - .expect("Failed to create slasher filter"); - - let miner = Address::new_id(1000); - let epoch1 = 100; - let epoch2 = 101; - let parents1 = TipsetKey::from(nunny::vec![Cid::default()]); - - // Create miner's block at an epoch - let header1 = create_test_header(miner, epoch1, parents1.clone(), Some(1000)); - let result1 = filter - .process_block(&header1) - .expect("Failed to process first block"); - assert!(result1.is_none()); - - // Create another block at same epoch but different parents - let parents2 = TipsetKey::from(nunny::vec![Cid::default()]); - let header2 = create_test_header(Address::new_id(2000), epoch1, parents2.clone(), Some(2000)); - let result2 = filter - .process_block(&header2) - .expect("Failed to process second block"); - assert!(result2.is_none()); - - let header3 = create_test_header( - miner, - epoch2, - TipsetKey::from(nunny::vec![*header2.cid()]), - Some(3000), - ); - let result3 = filter - .process_block(&header3) - .expect("Failed to process third block"); - assert!(result3.is_some()); - - if let Some(fault) = result3 { - assert_eq!(fault.fault_type, ConsensusFaultType::ParentGrinding); - assert_eq!(fault.miner_address, miner); - assert_eq!(fault.detection_epoch, epoch2); - assert_eq!(fault.block_headers.len(), 2); - assert!(fault.extra_evidence.is_some()); - } + use crate::slasher::db::SlasherDb; + use crate::utils::encoding::from_slice_with_fallback; + + let _ = std::fs::remove_dir_all(std::env::temp_dir().join("slasher_test_parent_grinding")); + let mut db = SlasherDb::new(std::env::temp_dir().join("slasher_test_parent_grinding")) + .expect("Failed to create slasher db"); + + let bz = hex::decode("904400ffdd08815860b04c19255dcbd71a139a05a415a75bf4af0ba577494792109615482019ea33541093767090ec66bb04b7356ca57ba22a0b840b8f4625a280a464e5457e913632ffddd3fdd57c149263d2ff6f8b87e4ecd7c878caf69b0e8c13c184e6ea38fee982045860911b306d6d32366f1280a40293133721adab7d68f1c56f3fa38b4da4a572445a2d9d36382028ccfda03e147c9e8dcef70df83279d935af5b00854c12e0054de9d2c6edcaefbd7a415020a9c665b3a2216a60b3132b6926460d3c4088db450eb881821a0139bbac5830b085236103a8eba6babafcaed69dc1fb8d39c87e8cdddfbecd0e6f18a2d398858ac9e2995afa6eaf264c0c30f990954c81820358c0823a1a4972fa3f59976bc4f2887b927fd10d6b6f9694f9a5498d3e12cc2954af481925745e818a357abb92cbfe882bad8ac9fdf64855a7a121bf30bfb4acbfb10af91fa35144d3034ef4717db584bb9ef022b81e15da6dde235335850ab6198403ee5f5e37250e9b9d429e38798ad16a244f2d040a01e97e19e2d8b6505af816b6947a528a0ac495a75faf5e06d99a53a62b90ea6d21c4ce889e77795ee5b5371b5717dcbb2168d628d20ee19253ecabea6e257bcc6943f4ab547275e7a4582781d82a5827000171a0e4022019ac618ee9295d815999a8a09ce9bdaf506191be596f824c2e0f1733eeaf936446000c98c26cff1a002c54e3d82a5827000171a0e4022046510e5272d2a3b81420859cea5923b1e5229584b6228052cc22b8418128bd9dd82a5827000171a0e402208b4b85acc72b4e4098d15585ab8309b8cbcad8d6e614d6b8eb455bcb7b7e94cfd82a5827000171a0e4022011edfe20d79962864f3e7dd5c03228dd1e52ee6818b060ec9497a5a3ab0d9665586102977b0e19fbf789f0ea9a08b33d5f6fe5bc6b541e4df2e7bb52d816f8f06cccd980227b1a450b37621066a97002436b6e04cb1d070412ec1cbd74e8b97ef7417f42c81929348c6fd301a8d14d65cf1f00d0ef7d042f7f2b6c780b894e6d67c1911a68935446586102a7448bafb337e7974d1ef830286eb19c7122ae0e1ceb50414cab14931641a1fa4a34ac81743361092250c6b02d135bbc09f8ca06d502401b630927630ef4a04f6156f52bf233ab431ff9255243d8d9a33ddb6f374bf3f23817fe9b48663b790800420064").unwrap(); + let bh_1 = from_slice_with_fallback::(&bz).unwrap(); + + let bz = hex::decode("904400ffdd088158609760b7d7e5c7ebe82182014778922df6d9649025f3bc4eeec58da24e049ec4b03cc2809a7bc82c67e993796cbe0bef7904827183cf3b56a4e400bca5af4e1a865d1f19984e91f758587b4933458ccc18bec7cbf1f4468bba94d195b0c608746a82045860a90908091c11146aafeddcdc3383627f90fe39c6b51921a22d8a67689b36956bb34c51c0ceffc1c1ab2c30a799e96e5f10841408199c8af7872a836c13234cb13822146b72abf16c644c0de5c7ebf4f75e7e8adf54722137e47ca21d6d0804f281821a0139bbb658309074e04e44a75a28d67ed61e5b53988e14d9d1103a5f33176409eef03f8806485714c865a27d065dae1a870c317e029781820358c0965052a879f191246b1dbdaee02c9d6c1a9c3b50dd03eb27d10421ff9e896d3faeb61e239e484bfb7529fadd531a1178b3ca87c1c0afceb8735670f365b72e06d4a4acdc1a8132202c0ca2fde64722e1da65cab97a8f97cd2ab66055cb09291913011e7827a180bf8c2c9c0a8a8736a5565d0ff681b3ea577643bc7c1f4edb1978d3c1cca6f953f85057e071827474e0b540c57c206c1914c9e6bc97a22418f3ed8946b0d82d7d4aa7c567ecf1cec8f3fd5ec94474b4e37225488909aea3a35382d82a5827000171a0e40220d6e7eff6b3fdeb53bae47c521a3ede40a8c50bc9e5118d275d72e71275a6a708d82a5827000171a0e40220577207a8ec6039e77c28df82c922137ba5d61de047dec5c0b629e1d937974f8846000c98c2adff1a002c54e4d82a5827000171a0e4022091d807ce5c9458384b9ef33634da5d4c1709d68a4ca134382ba6b062cf545894d82a5827000171a0e402200ef4990e62c56026e1cddb23b8b7e4abd1e359ed23cf8de4943945dc9ba71bffd82a5827000171a0e4022049c5d1c0fb44e016f7a3d86b66e0592b6b337538b89758240c356cc8ff1d5671586102c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a68935464586102b6a7086307f6a386ec683f0c965bb52681e61406746da05091b3204b65e68881e506f8fc97aee42b386099955cdc6a9b0649549f87f3d3d63bdfb2c4620e597156a2e9c07f28080c9dde198aa9ae31d536d9d4f2d79f0b1ec2413d84a7e2b89200420064").unwrap(); + let bh_2 = from_slice_with_fallback::(&bz).unwrap(); + + let extra_bytes = hex::decode("904300c81f8158608d7bafd98705e7700e3971bb74df794f0f7cc097444f4f963c7128712512caf725f46d529d2538438ee3a598a2f424f4017e31844d9f3da319c2be9ef5e1c6bac88da3fa86f1896f5ee7de34f9351bdcdf976dc88ccf4e35a8a293187140d8f482025860b452e3cb711cb701dc113b270e8b3861273753501f2db098ce4e7bdb85fbaa90f1159095981767371df95d8017dfdd1f015ffc0e3fb6969be1137feb7bfaff77c057d342436becfe9b278c76449bdd90811233ebbf56649aea8efaf6a2015e6d81821a0139bbac5830b085236103a8eba6babafcaed69dc1fb8d39c87e8cdddfbecd0e6f18a2d398858ac9e2995afa6eaf264c0c30f990954c81820358c08e4042d58de8a4b577324d89a62cd2c176ff46e2d63a9498e64c76095e617c6314964014675fe1d65c27096b7d92d122afc4f9d37de4d54402e454e27a38c0dace2db49866c95643f3dc3f087f9e0289dccab082a9a3b0f1b1e0ecd41f50c44e0df18344052d54c2790ba950b0017751bbd67a6a2aeace3401820a951af8c73063d2b571d6f8126b79f803770f96180ab6eca113d85160640e741d51f53ac19b319ac0587cf91d436b2c6385fc19ca65773de0a346d341610b26040fac11aaee81d82a5827000171a0e4022019ac618ee9295d815999a8a09ce9bdaf506191be596f824c2e0f1733eeaf936446000c98c26cff1a002c54e3d82a5827000171a0e4022046510e5272d2a3b81420859cea5923b1e5229584b6228052cc22b8418128bd9dd82a5827000171a0e402208b4b85acc72b4e4098d15585ab8309b8cbcad8d6e614d6b8eb455bcb7b7e94cfd82a5827000171a0e402200424e15c474771ccb2bb22fdb81e99b4c9f916f12e46dda7e3ae95f5a131e480586102c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a689354465861029577f33431fc443136630d65c6321b3b1f66b638f8816fc0915236cb622f60a2f12f9aaf8a1a11c99815d6dbe39273be1385d8dbdbec3777bdd6343a0a43b97aff74583f56bb3cdf46b553cd12c4c986d55aab6f7c883129735eb5884754d3f401420064").unwrap(); + let bh_3 = from_slice_with_fallback::(&extra_bytes).unwrap(); + + // Store bh_1 (miner's block) in the database + db.put(&bh_1).expect("Failed to add bh_1 to history"); + + // Store bh_3 (sibling block) in the database + db.put(&bh_3).expect("Failed to add bh_3 to history"); + + // Check if we have the miner's block stored + let miner_epoch_key = format!("{}/{}", bh_1.miner_address, bh_1.epoch); + let existing_block_cid = db + .get( + crate::slasher::db::SlasherDbColumns::ByEpoch as u8, + miner_epoch_key.as_bytes(), + ) + .expect("Failed to get existing block"); + + assert!(existing_block_cid.is_some()); + assert_eq!(bh_1.epoch, bh_3.epoch); + assert_eq!(bh_1.parents, bh_3.parents); + assert!(bh_2.parents.contains(*bh_3.cid())); + assert!(!bh_2.parents.contains(*bh_1.cid())); } diff --git a/src/slasher/types.rs b/src/slasher/types.rs index 692be0ca6d14..2abd8b828ba2 100644 --- a/src/slasher/types.rs +++ b/src/slasher/types.rs @@ -1,9 +1,9 @@ // Copyright 2019-2025 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +use crate::blocks::CachingBlockHeader; use crate::shim::address::Address; use crate::shim::clock::ChainEpoch; -use cid::Cid; use serde::{Deserialize, Serialize}; /// Represents a detected consensus fault @@ -16,9 +16,11 @@ pub struct ConsensusFault { /// The type of consensus fault pub fault_type: ConsensusFaultType, /// The block headers involved in the fault - pub block_headers: Vec, + pub block_header_1: CachingBlockHeader, + /// The second block header involved in the fault + pub block_header_2: CachingBlockHeader, /// Additional evidence for parent-grinding faults - pub extra_evidence: Option, + pub block_header_extra: Option, } /// Types of consensus faults that can be detected