From 2d3ab7cf8e259e623f7b0a3ad280d0c587a6f9e3 Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 5 Jun 2026 14:36:14 +0530 Subject: [PATCH] port lotus fix --- src/networks/mod.rs | 26 ++++++---- src/rpc/error.rs | 12 +++++ src/rpc/methods/eth.rs | 65 ++++++++++++++----------- src/rpc/methods/state.rs | 10 ++-- src/state_manager/errors.rs | 7 ++- src/state_manager/message_simulation.rs | 20 +++++--- 6 files changed, 87 insertions(+), 53 deletions(-) diff --git a/src/networks/mod.rs b/src/networks/mod.rs index b66f203da1e0..4bf1e27fbc50 100644 --- a/src/networks/mod.rs +++ b/src/networks/mod.rs @@ -512,20 +512,26 @@ impl ChainConfig { .unwrap_or(0) } - /// Returns true if executing between `parent` and `height` (exclusive of `height`) would - /// cross an expensive state migration, as registered in - /// [`crate::state_migration::get_migrations`]. - pub fn has_expensive_fork_between(&self, parent: ChainEpoch, height: ChainEpoch) -> bool { + /// Returns the lowest expensive state migration epoch in `[parent, height)` if one exists. + pub fn expensive_fork_between( + &self, + parent: ChainEpoch, + height: ChainEpoch, + ) -> Option { if parent >= height { - return false; + return None; } crate::state_migration::get_migrations::(&self.network) .iter() - .any(|(h, _)| { - self.height_infos - .get(h) - .is_some_and(|info| info.epoch >= parent && info.epoch < height) - }) + .filter_map(|(h, _)| self.height_infos.get(h).map(|info| info.epoch)) + .filter(|epoch| *epoch >= parent && *epoch < height) + .min() + } + + /// Reports whether an expensive migration is triggered in the half-open epoch interval + /// `[parent, height)`. + pub fn has_expensive_fork_between(&self, parent: ChainEpoch, height: ChainEpoch) -> bool { + self.expensive_fork_between(parent, height).is_some() } pub async fn genesis_bytes( diff --git a/src/rpc/error.rs b/src/rpc/error.rs index 5789e2736637..3816f5d81e99 100644 --- a/src/rpc/error.rs +++ b/src/rpc/error.rs @@ -42,6 +42,9 @@ pub(crate) mod implementation_defined_errors { /// node. Note that it's not the same as not found, as we are explicitly not supporting it, /// e.g., because it's deprecated or Lotus is doing the same. pub(crate) const UNSUPPORTED_METHOD: i32 = -32001; + /// EIP-1474 "resource unavailable": explicit call targets a block whose state cannot be + /// served without running an expensive migration on demand. + pub(crate) const EXPENSIVE_FORK_CODE: i32 = -32002; } impl ServerError { @@ -98,6 +101,15 @@ impl From for ServerError { if let Some(eth_error) = error.downcast_ref::() { return eth_error.clone().into(); } + if let Some(sm_error @ crate::state_manager::Error::ExpensiveFork { epoch }) = + error.downcast_ref::() + { + return Self::new( + implementation_defined_errors::EXPENSIVE_FORK_CODE, + sm_error.to_string(), + Some(serde_json::Value::from(*epoch)), + ); + } // Default fallback, not using `format!("{e:#}")` here to match Lotus error Self::internal_error(error.to_string(), None) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 4dce20921f96..5bd91ed8b1f7 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -1854,14 +1854,11 @@ async fn apply_message( ) -> Result { if let Some(ts) = &tipset && ts.epoch() > 0 - { - let parent = ctx.chain_index().load_required_tipset(ts.parents())?; - if ctx + && ctx .chain_config() - .has_expensive_fork_between(parent.epoch(), ts.epoch() + 1) - { - return Err(crate::state_manager::Error::ExpensiveFork.into()); - } + .has_expensive_fork_between(ts.epoch(), ts.epoch() + 1) + { + return Err(crate::state_manager::Error::ExpensiveFork { epoch: ts.epoch() }.into()); } let (invoc_res, _) = ctx @@ -2182,19 +2179,24 @@ async fn eth_get_code( ..Default::default() }; - let api_invoc_result = 'invoc: { - for ts in ts.shallow_clone().chain(ctx.db()) { - match ctx - .state_manager - .call_on_state(state_root, &message, Some(ts)) - { - Ok(res) => { - break 'invoc res; - } - Err(e) => tracing::warn!(%e), + // Rewind ts to escape the fork guard, but keep state_root fixed to the requested epoch: the + // result comes from state_root (ts only supplies execution context), so recomputing it for the + // parent would read an earlier epoch's bytecode. + let mut ts = ts.shallow_clone(); + let api_invoc_result = loop { + match ctx + .state_manager + .call_on_state(state_root, &message, Some(ts.shallow_clone())) + { + Ok(res) => break res, + Err(crate::state_manager::Error::ExpensiveFork { .. }) => { + ts = ctx + .chain_index() + .load_required_tipset(ts.parents()) + .map_err(|e| anyhow::anyhow!("getting parent tipset: {e}"))?; } + Err(e) => return Err(e.into()), } - return Err(anyhow::anyhow!("Call failed").into()); }; let Some(msg_rct) = api_invoc_result.msg_rct else { return Err(anyhow::anyhow!("no message receipt").into()); @@ -2275,19 +2277,24 @@ async fn get_storage_at( params, ..Default::default() }; - let api_invoc_result = 'invoc: { - for ts in ts.chain(ctx.db()) { - match ctx - .state_manager - .call_on_state(state_root, &message, Some(ts)) - { - Ok(res) => { - break 'invoc res; - } - Err(e) => tracing::warn!(%e), + // Rewind ts to escape the fork guard, but keep state_root fixed to the requested epoch: the + // result comes from state_root (ts only supplies execution context), so recomputing it for the + // parent would read an earlier epoch's storage. + let mut ts = ts; + let api_invoc_result = loop { + match ctx + .state_manager + .call_on_state(state_root, &message, Some(ts.shallow_clone())) + { + Ok(res) => break res, + Err(crate::state_manager::Error::ExpensiveFork { .. }) => { + ts = ctx + .chain_index() + .load_required_tipset(ts.parents()) + .map_err(|e| anyhow::anyhow!("getting parent tipset: {e}"))?; } + Err(e) => return Err(e.into()), } - return Err(anyhow::anyhow!("Call failed").into()); }; let Some(msg_rct) = api_invoc_result.msg_rct else { return Err(anyhow::anyhow!("no message receipt").into()); diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index 1a3f45b0181a..18ca63f03e70 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -90,16 +90,14 @@ impl StateCall { .chain_store() .load_required_tipset_or_heaviest(&tsk)?; - // 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). + // Parent-state `call` refuses when a migration spans the parent→tipset window; walk back + // to the parent tipset and retry. This does not serve U+1 at the requested tipset (unlike + // `eth_call`, which uses explicit tipset state). // // See: loop { match state_manager.call(message, Some(tipset.shallow_clone())) { - Err(crate::state_manager::Error::ExpensiveFork) => { + Err(crate::state_manager::Error::ExpensiveFork { .. }) => { tipset = state_manager .chain_index() .load_required_tipset(tipset.parents()) diff --git a/src/state_manager/errors.rs b/src/state_manager/errors.rs index 57cdc1435395..2651ffb3b3c6 100644 --- a/src/state_manager/errors.rs +++ b/src/state_manager/errors.rs @@ -3,6 +3,7 @@ use std::fmt::{Debug, Display}; +use crate::shim::clock::ChainEpoch; use thiserror::Error; use tokio::task::JoinError; @@ -13,8 +14,10 @@ pub enum Error { #[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, + #[error( + "required historical state unavailable: refusing explicit call due to state fork at epoch {epoch}" + )] + ExpensiveFork { epoch: ChainEpoch }, /// 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 a9947ccd25ef..1ff0529d239e 100644 --- a/src/state_manager/message_simulation.rs +++ b/src/state_manager/message_simulation.rs @@ -26,12 +26,20 @@ impl StateManager { 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); + // Explicit-state calls already hold every fork below the tipset epoch, so only a + // migration at the tipset epoch itself is refused. Parent-state calls (no explicit + // state) lag a tipset, so a migration at the parent epoch must also be refused. + let (fork_floor, fork_height) = if state_cid.is_some() { + (ts.epoch(), ts.epoch() + 1) + } else { + let parent = self + .chain_index() + .load_required_tipset(ts.parents()) + .map_err(Error::other)?; + (parent.epoch(), ts.epoch() + 1) + }; + if let Some(epoch) = chain_config.expensive_fork_between(fork_floor, fork_height) { + return Err(Error::ExpensiveFork { epoch }); } } ts