Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/docs/users/reference/env_variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
20 changes: 19 additions & 1 deletion src/chain/store/chain_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,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,
Expand Down
34 changes: 34 additions & 0 deletions src/chain/store/tipset_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,6 +20,8 @@ pub(in crate::chain) struct TipsetTracker<DB> {
entries: Mutex<BTreeMap<ChainEpoch, Vec<Cid>>>,
db: Arc<DB>,
chain_config: Arc<ChainConfig>,
/// Optional slasher service for consensus fault detection
slasher_service: Option<Arc<SlasherService>>,
}

impl<DB: Blockstore> TipsetTracker<DB> {
Expand All @@ -27,6 +30,21 @@ impl<DB: Blockstore> TipsetTracker<DB> {
entries: Default::default(),
db,
chain_config,
slasher_service: None,
}
}

/// Create a new [`TipsetTracker`] with slasher service enabled
pub fn with_slasher(
db: Arc<DB>,
chain_config: Arc<ChainConfig>,
slasher_service: Arc<SlasherService>,
) -> Self {
Self {
entries: Default::default(),
db,
chain_config,
slasher_service: Some(slasher_service),
}
}

Expand All @@ -42,6 +60,7 @@ impl<DB: Blockstore> TipsetTracker<DB> {
cids.push(*header.cid());
drop(map_lock);

self.check_consensus_faults(header);
self.check_multiple_blocks_from_same_miner(&cids_to_verify, header);
self.prune_entries(header.epoch);
}
Expand All @@ -68,6 +87,20 @@ impl<DB: Blockstore> TipsetTracker<DB> {
}
}

/// Process block with slasher service for consensus fault detection
fn check_consensus_faults(&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) {
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ mod metrics;
mod networks;
mod rpc;
mod shim;
mod slasher;
mod state_manager;
mod state_migration;
mod statediff;
Expand Down
65 changes: 65 additions & 0 deletions src/slasher/db.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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<Option<Vec<u8>>> {
Ok(self.db.get(column, key)?)
}
}
9 changes: 9 additions & 0 deletions src/slasher/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright 2019-2025 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

pub mod db;
pub mod service;
pub mod types;

#[cfg(test)]
mod tests;
Loading
Loading