From ad5e956308d8994f67d1bba35607e02b08869d29 Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 15 May 2026 14:47:03 +0530 Subject: [PATCH 1/8] run req migration --- src/state_manager/message_simulation.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/state_manager/message_simulation.rs b/src/state_manager/message_simulation.rs index 637c73169793..dbbbeb07cc4c 100644 --- a/src/state_manager/message_simulation.rs +++ b/src/state_manager/message_simulation.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0, MIT use super::circulating_supply::GenesisInfo; +use super::state_computation::TipsetExecutor; use super::utils::structured; use super::*; use crate::interpreter::{ExecutionContext, IMPLICIT_MESSAGE_GAS_LIMIT, VM, VMTrace}; @@ -26,7 +27,23 @@ impl StateManager { ) -> Result { let mut msg = msg.clone(); - let state_cid = state_cid.unwrap_or(*tipset.parent_state()); + let state_cid = match state_cid { + Some(cid) => cid, + None => { + let genesis_timestamp = self.chain_store().genesis_block_header().timestamp; + let exec = TipsetExecutor::new( + self.chain_index().shallow_clone(), + self.chain_config().shallow_clone(), + self.beacon_schedule().shallow_clone(), + &self.engine, + tipset.shallow_clone(), + ); + let mut no_cb = NO_CALLBACK; + let (state_cid, _, _) = + exec.prepare_parent_state(genesis_timestamp, VMTrace::NotTraced, &mut no_cb)?; + state_cid + } + }; let tipset_messages = self .chain_store() @@ -37,8 +54,6 @@ impl StateManager { .iter() .filter(|ts_msg| ts_msg.message().from() == msg.from()); - // Handle state forks - let height = tipset.epoch(); let genesis_info = GenesisInfo::from_chain_config(self.chain_config().clone()); let mut vm = VM::new( From b391f5483a94cfd069a15ecab3072815bfeb9d27 Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 15 May 2026 15:28:58 +0530 Subject: [PATCH 2/8] Add context --- src/state_manager/message_simulation.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/state_manager/message_simulation.rs b/src/state_manager/message_simulation.rs index dbbbeb07cc4c..fc89847853e7 100644 --- a/src/state_manager/message_simulation.rs +++ b/src/state_manager/message_simulation.rs @@ -12,6 +12,7 @@ use crate::shim::address::Protocol; use crate::shim::crypto::{Signature, SignatureType}; use crate::shim::executor::ApplyRet; use crate::shim::message::Message; +use anyhow::Context; use fvm_shared4::crypto::signature::SECP_SIG_LEN; use std::time::Duration; use tracing::instrument; @@ -39,8 +40,9 @@ impl StateManager { tipset.shallow_clone(), ); let mut no_cb = NO_CALLBACK; - let (state_cid, _, _) = - exec.prepare_parent_state(genesis_timestamp, VMTrace::NotTraced, &mut no_cb)?; + let (state_cid, _, _) = exec + .prepare_parent_state(genesis_timestamp, VMTrace::NotTraced, &mut no_cb) + .context("failed to prepare parent state in call_raw")?; state_cid } }; From 9405aae45d156bf66253bae29c0aa5c909a8948b Mon Sep 17 00:00:00 2001 From: Shashank Date: Mon, 18 May 2026 15:25:36 +0530 Subject: [PATCH 3/8] impl expensive fork check --- src/networks/mod.rs | 58 +++++++++++++++++++ src/state_manager/errors.rs | 3 + src/state_manager/message_simulation.rs | 76 +++++++++++++++---------- 3 files changed, 108 insertions(+), 29 deletions(-) diff --git a/src/networks/mod.rs b/src/networks/mod.rs index b564f0d28655..f2177b5395ff 100644 --- a/src/networks/mod.rs +++ b/src/networks/mod.rs @@ -222,11 +222,38 @@ impl From for NetworkVersion { } } +/// Checks if the given height is an expensive migration. +/// See +pub const fn is_expensive_migration(height: Height) -> bool { + matches!( + height, + Height::Assembly + | Height::Trust + | Height::Turbo + | Height::Hyperdrive + | Height::Chocolate + | Height::OhSnap + | Height::Skyr + | Height::Shark + | Height::Hygge + | Height::Lightning + | Height::Watermelon + | Height::Dragon + | Height::Waffle + | Height::TukTuk + | Height::Teep + | Height::GoldenWeek + | Height::FireHorse + ) +} + #[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(derive_quickcheck_arbitrary::Arbitrary))] pub struct HeightInfo { pub epoch: ChainEpoch, pub bundle: Option, + #[serde(default)] + pub expensive: bool, } pub struct HeightInfoWithActorManifest<'a> { @@ -512,6 +539,17 @@ impl ChainConfig { .unwrap_or(0) } + /// Returns true if executing between `parent` and `height` (exclusive of `height`) would + /// cross an expensive state migration. + pub fn has_expensive_fork_between(&self, parent: ChainEpoch, height: ChainEpoch) -> bool { + if parent >= height { + return false; + } + self.height_infos + .values() + .any(|info| info.expensive && info.epoch >= parent && info.epoch < height) + } + pub async fn genesis_bytes( &self, db: &DB, @@ -599,6 +637,7 @@ macro_rules! make_height { HeightInfo { epoch: $epoch, bundle: None, + expensive: $crate::networks::is_expensive_migration(Height::$id), }, ) }; @@ -608,6 +647,7 @@ macro_rules! make_height { HeightInfo { epoch: $epoch, bundle: Some(Cid::try_from($bundle).unwrap()), + expensive: $crate::networks::is_expensive_migration(Height::$id), }, ) }; @@ -678,6 +718,24 @@ mod tests { heights_are_present(&mainnet::HEIGHT_INFOS); } + #[test] + fn height_info_expensive_flag_matches_is_expensive_migration() { + for height in Height::iter() { + let Some(info) = mainnet::HEIGHT_INFOS.get(&height) else { + continue; + }; + assert_eq!(info.expensive, is_expensive_migration(height), "{height:?}"); + } + } + + #[test] + fn has_expensive_fork_between_matches_upgrade_epochs() { + let cfg = ChainConfig::mainnet(); + let shark = cfg.epoch(Height::Shark); + assert!(cfg.has_expensive_fork_between(shark - 1, shark + 1)); + assert!(!cfg.has_expensive_fork_between(shark - 1, shark)); + } + #[test] fn test_calibnet_heights() { heights_are_present(&calibnet::HEIGHT_INFOS); diff --git a/src/state_manager/errors.rs b/src/state_manager/errors.rs index d10d8ae9b4ee..57cdc1435395 100644 --- a/src/state_manager/errors.rs +++ b/src/state_manager/errors.rs @@ -12,6 +12,9 @@ pub enum Error { /// Error originating from state #[error("{0}")] State(String), + /// Refusing explicit call due to an expensive state migration at the requested epoch. + #[error("refusing explicit call due to state fork at epoch")] + ExpensiveFork, /// Other state manager error #[error("{0}")] Other(String), diff --git a/src/state_manager/message_simulation.rs b/src/state_manager/message_simulation.rs index fc89847853e7..bdbba00cbd15 100644 --- a/src/state_manager/message_simulation.rs +++ b/src/state_manager/message_simulation.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0, MIT use super::circulating_supply::GenesisInfo; -use super::state_computation::TipsetExecutor; use super::utils::structured; use super::*; use crate::interpreter::{ExecutionContext, IMPLICIT_MESSAGE_GAS_LIMIT, VM, VMTrace}; @@ -12,50 +11,73 @@ use crate::shim::address::Protocol; use crate::shim::crypto::{Signature, SignatureType}; use crate::shim::executor::ApplyRet; use crate::shim::message::Message; -use anyhow::Context; +use crate::state_migration::run_state_migrations; use fvm_shared4::crypto::signature::SECP_SIG_LEN; use std::time::Duration; use tracing::instrument; impl StateManager { - #[instrument(skip(self, rand))] + #[instrument(skip(self))] fn call_raw( &self, state_cid: Option, msg: &Message, - rand: ChainRand, - tipset: &Tipset, + tipset: Option, ) -> Result { let mut msg = msg.clone(); + let chain_config = self.chain_config(); - let state_cid = match state_cid { - Some(cid) => cid, - None => { - let genesis_timestamp = self.chain_store().genesis_block_header().timestamp; - let exec = TipsetExecutor::new( - self.chain_index().shallow_clone(), - self.chain_config().shallow_clone(), - self.beacon_schedule().shallow_clone(), - &self.engine, - tipset.shallow_clone(), - ); - let mut no_cb = NO_CALLBACK; - let (state_cid, _, _) = exec - .prepare_parent_state(genesis_timestamp, VMTrace::NotTraced, &mut no_cb) - .context("failed to prepare parent state in call_raw")?; - state_cid + let tipset = if let Some(ts) = tipset { + if ts.epoch() > 0 { + let parent = self + .chain_index() + .load_required_tipset(ts.parents()) + .map_err(Error::other)?; + if chain_config.has_expensive_fork_between(parent.epoch(), ts.epoch() + 1) { + return Err(Error::ExpensiveFork); + } + } + ts + } else { + // Search back till we find a height with no fork, or we reach the beginning. + let mut heaviest_ts = self.heaviest_tipset(); + while heaviest_ts.epoch() > 0 { + let parent = self + .chain_index() + .load_required_tipset(heaviest_ts.parents()) + .map_err(Error::other)?; + if !chain_config.has_expensive_fork_between(parent.epoch(), heaviest_ts.epoch() + 1) + { + break; + } + heaviest_ts = parent; } + heaviest_ts }; + let state_cid = state_cid.unwrap_or(*tipset.parent_state()); + let tipset_messages = self .chain_store() - .messages_for_tipset(tipset) + .messages_for_tipset(&tipset) .map_err(|err| Error::Other(err.to_string()))?; let prior_messsages = tipset_messages .iter() .filter(|ts_msg| ts_msg.message().from() == msg.from()); + // Handle state forks + let state_cid = match run_state_migrations( + tipset.epoch(), + self.chain_config(), + self.db(), + &state_cid, + ) { + Ok(Some(new_state)) => new_state, + Ok(None) => state_cid, + Err(e) => return Err(Error::other(e)), + }; + let height = tipset.epoch(); let genesis_info = GenesisInfo::from_chain_config(self.chain_config().clone()); let mut vm = VM::new( @@ -63,7 +85,7 @@ impl StateManager { heaviest_tipset: tipset.shallow_clone(), state_tree_root: state_cid, epoch: height, - rand: Box::new(rand), + rand: Box::new(self.chain_rand(tipset.shallow_clone())), base_fee: tipset.block_headers().first().parent_base_fee.clone(), circ_supply: genesis_info.get_vm_circulating_supply( height, @@ -113,9 +135,7 @@ impl StateManager { /// runs the given message and returns its result without any persisted /// changes. pub fn call(&self, message: &Message, tipset: Option) -> Result { - let ts = tipset.unwrap_or_else(|| self.heaviest_tipset()); - let chain_rand = self.chain_rand(ts.shallow_clone()); - self.call_raw(None, message, chain_rand, &ts) + self.call_raw(None, message, tipset) } /// Same as [`StateManager::call`] but runs the message on the given state and not @@ -126,9 +146,7 @@ impl StateManager { message: &Message, tipset: Option, ) -> Result { - let ts = tipset.unwrap_or_else(|| self.cs.heaviest_tipset()); - let chain_rand = self.chain_rand(ts.shallow_clone()); - self.call_raw(Some(state_cid), message, chain_rand, &ts) + self.call_raw(Some(state_cid), message, tipset) } pub async fn apply_on_state_with_gas( From 4066a6fda7cc8587dc73d2310f25c3f911c9fa13 Mon Sep 17 00:00:00 2001 From: Shashank Date: Tue, 19 May 2026 10:37:55 +0530 Subject: [PATCH 4/8] fmt error msg --- .../subcommands/api_cmd/api_compare_tests.rs | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index 76ff27b83c63..cb47a8869f05 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -4,6 +4,7 @@ use super::{CreateTestsArgs, ReportMode, RunIgnored, TestCriteriaOverride}; use crate::blocks::{ElectionProof, Ticket, Tipset}; use crate::chain::ChainStore; +use crate::chain::index::{ChainIndex, ResolveNullTipset}; use crate::db::car::ManyCar; use crate::eth::EthChainId as EthChainIdType; use crate::lotus_json::HasLotusJson; @@ -2444,6 +2445,33 @@ fn f3_tests_with_tipset(tipset: &Tipset) -> anyhow::Result> { ]) } +fn state_expensive_fork_error_tests(store: Arc) -> anyhow::Result> { + let heaviest_tipset = store.heaviest_tipset()?; + let chain_config = handle_chain_config(&NetworkChain::Calibnet)?; + let expensive_fork_epoch = chain_config + .height_infos + .values() + .filter(|info| info.expensive && info.epoch <= heaviest_tipset.epoch()) + .map(|info| info.epoch) + .max() + .expect("calibnet must define at least one expensive fork"); + + let chain_index = ChainIndex::new(store); + let tipset = chain_index.load_required_tipset_by_height( + expensive_fork_epoch, + heaviest_tipset, + ResolveNullTipset::TakeNewer, + )?; + + Ok(vec![ + RpcTest::identity(StateCall::request(( + Message::default(), + tipset.key().into(), + ))?) + .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError), + ]) +} + // Extract tests that use chain-specific data such as block CIDs or message // CIDs. Right now, only the last `n_tipsets` tipsets are used. fn snapshot_tests( @@ -2463,6 +2491,8 @@ fn snapshot_tests( .last() .expect("Infallible"); + tests.extend(state_expensive_fork_error_tests(store.clone())?); + for tipset in shared_tipset.chain(&store).take(num_tipsets) { tests.extend(chain_tests_with_tipset(&store, offline, &tipset)?); tests.extend(miner_tests_with_tipset(&store, &tipset, miner_address)?); @@ -2555,7 +2585,7 @@ pub(super) async fn create_tests( let store = Arc::new(ManyCar::try_from(snapshot_files.clone())?); revalidate_chain(store.clone(), n_tipsets).await?; tests.extend(snapshot_tests( - store, + store.clone(), offline, n_tipsets, miner_address, From 23e212883a853a5c8900cd3240069ebc52a780f9 Mon Sep 17 00:00:00 2001 From: Shashank Date: Tue, 19 May 2026 12:51:35 +0530 Subject: [PATCH 5/8] add exp fork check in eth call --- src/rpc/methods/eth.rs | 12 ++++++++++++ src/tool/subcommands/api_cmd/api_compare_tests.rs | 13 +++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index aef3812ac298..834dd8f18b2c 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -1814,6 +1814,18 @@ async fn apply_message( tipset: Option, msg: Message, ) -> Result { + if let Some(ts) = &tipset + && ts.epoch() > 0 + { + let parent = ctx.chain_index().load_required_tipset(ts.parents())?; + if ctx + .chain_config() + .has_expensive_fork_between(parent.epoch(), ts.epoch() + 1) + { + return Err(crate::state_manager::Error::ExpensiveFork.into()); + } + } + let (invoc_res, _) = ctx .state_manager .apply_on_state_with_gas(tipset, msg, VMFlush::Skip) diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index cb47a8869f05..1edc54e53cb7 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -2469,6 +2469,19 @@ fn state_expensive_fork_error_tests(store: Arc) -> anyhow::Result Date: Tue, 19 May 2026 13:51:16 +0530 Subject: [PATCH 6/8] fix test --- src/tool/subcommands/api_cmd/api_compare_tests.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index 1edc54e53cb7..d61dd6a01f1a 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -2468,12 +2468,12 @@ fn state_expensive_fork_error_tests(store: Arc) -> anyhow::Result) -> anyhow::Result Date: Tue, 19 May 2026 14:58:38 +0530 Subject: [PATCH 7/8] fix test --- src/rpc/methods/eth.rs | 2 +- src/rpc/methods/state.rs | 22 +++++++++++++++++-- src/state_manager/message_simulation.rs | 18 +++++++-------- .../subcommands/api_cmd/api_compare_tests.rs | 17 ++------------ 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 834dd8f18b2c..b43f9cb12176 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -1800,7 +1800,7 @@ async fn eth_estimate_gas( err = e.into(); } - Err(anyhow::anyhow!("failed to estimate gas: {err}").into()) + Err(anyhow::anyhow!("failed to estimate gas: {}", err.message()).into()) } Ok(gassed_msg) => { let expected_gas = eth_gas_search(ctx, gassed_msg, &tipset.key().into()).await?; diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index e16367ba31f9..9cb49456fc01 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -86,10 +86,28 @@ impl StateCall { message: &Message, tsk: Option, ) -> anyhow::Result { - let tipset = state_manager + let mut tipset = state_manager .chain_store() .load_required_tipset_or_heaviest(&tsk)?; - Ok(state_manager.call(message, Some(tipset))?) + + // Match Lotus' `StateCall` behavior: if the call refuses due to an expensive + // state fork between the parent and the target tipset, walk back to the parent + // tipset and retry. This loop terminates when the call returns a non-`ExpensiveFork` + // result (success or different error), or when we fail to load the parent tipset + // (e.g. we walked back past genesis). + // + // See: + loop { + match state_manager.call(message, Some(tipset.shallow_clone())) { + Err(crate::state_manager::Error::ExpensiveFork) => { + tipset = state_manager + .chain_index() + .load_required_tipset(tipset.parents()) + .map_err(|e| anyhow::anyhow!("getting parent tipset: {e}"))?; + } + result => return Ok(result?), + } + } } } diff --git a/src/state_manager/message_simulation.rs b/src/state_manager/message_simulation.rs index bdbba00cbd15..f77a32ea4799 100644 --- a/src/state_manager/message_simulation.rs +++ b/src/state_manager/message_simulation.rs @@ -57,15 +57,6 @@ impl StateManager { let state_cid = state_cid.unwrap_or(*tipset.parent_state()); - let tipset_messages = self - .chain_store() - .messages_for_tipset(&tipset) - .map_err(|err| Error::Other(err.to_string()))?; - - let prior_messsages = tipset_messages - .iter() - .filter(|ts_msg| ts_msg.message().from() == msg.from()); - // Handle state forks let state_cid = match run_state_migrations( tipset.epoch(), @@ -100,6 +91,15 @@ impl StateManager { VMTrace::Traced, )?; + let tipset_messages = self + .chain_store() + .messages_for_tipset(&tipset) + .map_err(|err| Error::Other(err.to_string()))?; + + let prior_messsages = tipset_messages + .iter() + .filter(|ts_msg| ts_msg.message().from() == msg.from()); + for m in prior_messsages { vm.apply_message(m)?; } diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index d61dd6a01f1a..0906e5c94eec 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -4,7 +4,6 @@ use super::{CreateTestsArgs, ReportMode, RunIgnored, TestCriteriaOverride}; use crate::blocks::{ElectionProof, Ticket, Tipset}; use crate::chain::ChainStore; -use crate::chain::index::{ChainIndex, ResolveNullTipset}; use crate::db::car::ManyCar; use crate::eth::EthChainId as EthChainIdType; use crate::lotus_json::HasLotusJson; @@ -2445,7 +2444,7 @@ fn f3_tests_with_tipset(tipset: &Tipset) -> anyhow::Result> { ]) } -fn state_expensive_fork_error_tests(store: Arc) -> anyhow::Result> { +fn eth_expensive_fork_error_tests(store: Arc) -> anyhow::Result> { let heaviest_tipset = store.heaviest_tipset()?; let chain_config = handle_chain_config(&NetworkChain::Calibnet)?; let expensive_fork_epoch = chain_config @@ -2456,19 +2455,7 @@ fn state_expensive_fork_error_tests(store: Arc) -> anyhow::Result Date: Wed, 20 May 2026 12:43:31 +0530 Subject: [PATCH 8/8] propagate error --- src/tool/subcommands/api_cmd/api_compare_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index 0906e5c94eec..783f580f9cc8 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -2453,7 +2453,7 @@ fn eth_expensive_fork_error_tests(store: Arc) -> anyhow::Result