Skip to content
Closed
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
13 changes: 13 additions & 0 deletions src/rpc/methods/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ use crate::shim::gas::GasOutputs;
use crate::shim::message::Message;
use crate::shim::trace::{CallReturn, ExecutionEvent};
use crate::shim::{clock::ChainEpoch, state_tree::StateTree};
use crate::state_manager::Error as StateManagerError;
use crate::utils::cache::SizeTrackingLruCache;
use crate::utils::db::BlockstoreExt as _;
use crate::utils::encoding::from_slice_with_fallback;
Expand Down Expand Up @@ -2017,6 +2018,18 @@ async fn apply_message<DB>(
where
DB: Blockstore + Send + Sync + 'static,
{
if let Some(tipset) = tipset.as_ref()
&& tipset.epoch() > 0
{
let parent_ts = Tipset::load_required(ctx.store(), tipset.parents())
.context("failed to load parent tipset")?;

ensure!(
!ctx.state_manager
.has_expensive_fork_between(parent_ts.epoch(), tipset.epoch() + 1),
Copy link
Contributor

@hanabi1224 hanabi1224 Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would it be 2 epochs in the range?

Copy link
Contributor Author

@sudo-shashank sudo-shashank Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess lotus does this to early detect an expensive fork

StateManagerError::ExpensiveFork
);
}
let invoc_res = ctx
.state_manager
.apply_on_state_with_gas(tipset, msg)
Expand Down
2 changes: 2 additions & 0 deletions src/state_manager/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub enum Error {
/// Other state manager error
#[error("{0}")]
Other(String),
#[error("refusing explicit call due to state fork at epoch")]
ExpensiveFork,
}

impl Error {
Expand Down
34 changes: 32 additions & 2 deletions src/state_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@ use crate::state_manager::cache::{
TipsetStateCache,
};
use crate::state_manager::chain_rand::draw_randomness;
use crate::state_migration::get_expensive_migration_heights;
use crate::state_migration::run_state_migrations;
use crate::utils::get_size::{
GetSize, vec_heap_size_helper, vec_with_stack_only_item_heap_size_helper,
};
use ahash::{HashMap, HashMapExt};
use ahash::{HashMap, HashMapExt, HashSet};
use anyhow::{Context as _, bail};
use bls_signatures::{PublicKey as BlsPublicKey, Serialize as _};
use chain_rand::ChainRand;
Expand Down Expand Up @@ -167,6 +168,7 @@ pub struct StateManager<DB> {
engine: Arc<MultiEngine>,
/// Handler for caching/retrieving tipset events and receipts.
receipt_event_cache_handler: Box<dyn TipsetReceiptEventCacheHandler>,
expensive_migration_epochs: HashSet<ChainEpoch>,
}

#[allow(clippy::type_complexity)]
Expand All @@ -186,20 +188,28 @@ where
) -> Result<Self, anyhow::Error> {
let genesis = cs.genesis_block_header();
let beacon = Arc::new(cs.chain_config().get_beacon_schedule(genesis.timestamp));
let chain_config = cs.chain_config();

let cache_handler: Box<dyn TipsetReceiptEventCacheHandler> =
if cs.chain_config().enable_receipt_event_caching {
if chain_config.enable_receipt_event_caching {
Box::new(EnabledTipsetDataCache::new())
} else {
Box::new(DisabledTipsetDataCache::new())
};

let expensive_migration_epochs: HashSet<_> =
get_expensive_migration_heights(&chain_config.network)
.iter()
.map(|&h| chain_config.epoch(h))
.collect();

Ok(Self {
cs,
cache: TipsetStateCache::new("state_output"), // For StateOutputValue
beacon,
engine,
receipt_event_cache_handler: cache_handler,
expensive_migration_epochs,
})
}

Expand Down Expand Up @@ -448,6 +458,17 @@ where
let state = miner::State::load(self.blockstore(), actor.code, actor.state)?;
state.load_sectors_ext(self.blockstore(), None)
}

/// Returns true where executing tipsets between the specified heights would trigger an expensive migration.
/// Note: migrations occurring at the target height are not included, as they're executed after the target height.
pub fn has_expensive_fork_between(&self, parent: ChainEpoch, height: ChainEpoch) -> bool {
for epoch in parent..height {
if self.expensive_migration_epochs.contains(&epoch) {
return true;
}
}
false
}
}

impl<DB> StateManager<DB>
Expand Down Expand Up @@ -588,6 +609,15 @@ where
) -> Result<ApiInvocResult, Error> {
let mut msg = msg.clone();

if tipset.epoch() > 0 {
let parent_ts = Tipset::load_required(self.blockstore(), tipset.parents())
.map_err(|e| Error::Other(format!("failed to load parent tipset: {e}")))?;

if self.has_expensive_fork_between(parent_ts.epoch(), tipset.epoch() + 1) {
return Err(Error::ExpensiveFork);
}
}

let state_cid = state_cid.unwrap_or(*tipset.parent_state());

let tipset_messages = self
Expand Down
17 changes: 17 additions & 0 deletions src/state_manager/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::db::MemoryDB;
use crate::networks::ChainConfig;
use crate::shim::clock::ChainEpoch;
use crate::shim::executor::{Receipt, StampedEvent};
use crate::state_migration::get_expensive_migration_heights;
use crate::utils::db::CborStoreExt;
use crate::utils::multihash::MultihashCode;
use cid::Cid;
Expand Down Expand Up @@ -433,3 +434,19 @@ fn test_state_output_get_size() {
let s = StateOutputValue::default();
assert_eq!(s.get_size(), std::mem::size_of_val(&s));
}

#[test]
fn test_has_expensive_fork_between() {
let TestChainSetup { state_manager, .. } = setup_chain_with_tipsets();
let chain_config = state_manager.chain_config();

let expensive_epoch = get_expensive_migration_heights(&chain_config.network)
.iter()
.find_map(|height| {
let epoch = chain_config.epoch(*height);
(epoch > 0).then_some(epoch)
})
.expect("expected at least one expensive migration epoch > 0");

assert!(state_manager.has_expensive_fork_between(expensive_epoch, expensive_epoch + 1));
}
19 changes: 19 additions & 0 deletions src/state_migration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,25 @@ where
}
}

/// Returns the heights at which expensive migrations occur.
pub fn get_expensive_migration_heights(chain: &NetworkChain) -> &'static [Height] {
match chain {
NetworkChain::Mainnet | NetworkChain::Calibnet | NetworkChain::Devnet(_) => &[
Height::Shark,
Height::Hygge,
Height::Lightning,
Height::Watermelon,
Height::Dragon,
Height::Waffle,
Height::TukTuk,
Height::Teep,
Height::GoldenWeek,
],

NetworkChain::Butterflynet => &[Height::Teep, Height::GoldenWeek],
}
}

/// Run state migrations
pub fn run_state_migrations<DB>(
epoch: ChainEpoch,
Expand Down
Loading