Skip to content

Commit 2c661a5

Browse files
committed
Update expensive migration check
1 parent e5c6427 commit 2c661a5

7 files changed

Lines changed: 130 additions & 40 deletions

File tree

src/networks/mod.rs

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -512,20 +512,19 @@ impl ChainConfig {
512512
.unwrap_or(0)
513513
}
514514

515-
/// Returns true if executing between `parent` and `height` (exclusive of `height`) would
516-
/// cross an expensive state migration, as registered in
517-
/// [`crate::state_migration::get_migrations`].
518-
pub fn has_expensive_fork_between(&self, parent: ChainEpoch, height: ChainEpoch) -> bool {
515+
/// Returns the epoch of an expensive state migration in `[parent, height)` if one exists.
516+
pub fn expensive_fork_between(
517+
&self,
518+
parent: ChainEpoch,
519+
height: ChainEpoch,
520+
) -> Option<ChainEpoch> {
519521
if parent >= height {
520-
return false;
522+
return None;
521523
}
522524
crate::state_migration::get_migrations::<crate::db::DbImpl>(&self.network)
523525
.iter()
524-
.any(|(h, _)| {
525-
self.height_infos
526-
.get(h)
527-
.is_some_and(|info| info.epoch >= parent && info.epoch < height)
528-
})
526+
.filter_map(|(h, _)| self.height_infos.get(h).map(|info| info.epoch))
527+
.find(|epoch| *epoch >= parent && *epoch < height)
529528
}
530529

531530
pub async fn genesis_bytes<DB: SettingsStore>(
@@ -695,11 +694,13 @@ mod tests {
695694
}
696695

697696
#[test]
698-
fn has_expensive_fork_between_matches_upgrade_epochs() {
697+
fn expensive_fork_between_matches_upgrade_epochs() {
699698
let cfg = ChainConfig::mainnet();
700699
let shark = cfg.epoch(Height::Shark);
701-
assert!(cfg.has_expensive_fork_between(shark - 1, shark + 1));
702-
assert!(!cfg.has_expensive_fork_between(shark - 1, shark));
700+
assert_eq!(cfg.expensive_fork_between(shark - 1, shark + 1), Some(shark));
701+
assert_eq!(cfg.expensive_fork_between(shark - 1, shark), None);
702+
assert_eq!(cfg.expensive_fork_between(shark, shark + 1), Some(shark));
703+
assert_eq!(cfg.expensive_fork_between(shark + 1, shark + 2), None);
703704
}
704705

705706
#[test]

src/rpc/error.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ pub(crate) mod implementation_defined_errors {
4242
/// node. Note that it's not the same as not found, as we are explicitly not supporting it,
4343
/// e.g., because it's deprecated or Lotus is doing the same.
4444
pub(crate) const UNSUPPORTED_METHOD: i32 = -32001;
45+
/// EIP-1474 "resource unavailable": explicit call targets a block whose state cannot be
46+
/// served without running an expensive migration on demand. Matches Lotus `ExpensiveFork`.
47+
pub(crate) const EXPENSIVE_FORK_CODE: i32 = -32002;
4548
}
4649

4750
impl ServerError {
@@ -98,6 +101,9 @@ impl From<anyhow::Error> for ServerError {
98101
if let Some(eth_error) = error.downcast_ref::<EthErrors>() {
99102
return eth_error.clone().into();
100103
}
104+
if let Some(sm_error) = error.downcast_ref::<crate::state_manager::Error>() {
105+
return sm_error.clone().into();
106+
}
101107

102108
// Default fallback, not using `format!("{e:#}")` here to match Lotus error
103109
Self::internal_error(error.to_string(), None)
@@ -158,7 +164,6 @@ from2internal! {
158164
crate::key_management::Error,
159165
crate::libp2p::ParseError,
160166
crate::message_pool::Error,
161-
crate::state_manager::Error,
162167
fil_actors_shared::fvm_ipld_amt::Error,
163168
futures::channel::oneshot::Canceled,
164169
fvm_ipld_encoding::Error,
@@ -178,6 +183,19 @@ from2internal! {
178183
jsonrpsee::core::client::error::Error,
179184
}
180185

186+
impl From<crate::state_manager::Error> for ServerError {
187+
fn from(error: crate::state_manager::Error) -> Self {
188+
match error {
189+
crate::state_manager::Error::ExpensiveFork { epoch } => Self::new(
190+
implementation_defined_errors::EXPENSIVE_FORK_CODE,
191+
error.to_string(),
192+
Some(serde_json::Value::from(epoch)),
193+
),
194+
other => Self::internal_error(other.to_string(), None),
195+
}
196+
}
197+
}
198+
181199
impl From<ServerError> for ClientError {
182200
fn from(value: ServerError) -> Self {
183201
Self::Call(value.inner)
@@ -195,3 +213,50 @@ impl<T> From<tokio::sync::mpsc::error::SendError<T>> for ServerError {
195213
Self::internal_error(e, None)
196214
}
197215
}
216+
217+
#[cfg(test)]
218+
mod tests {
219+
use super::*;
220+
use crate::state_manager::Error as StateManagerError;
221+
222+
#[test]
223+
fn expensive_fork_converts_to_server_error_with_correct_code() {
224+
let err = StateManagerError::ExpensiveFork { epoch: 42 };
225+
let server_err: ServerError = err.into();
226+
227+
assert_eq!(
228+
server_err.known_code(),
229+
implementation_defined_errors::EXPENSIVE_FORK_CODE.into()
230+
);
231+
assert_eq!(
232+
server_err.inner.message(),
233+
"required historical state unavailable: refusing explicit call due to state fork at epoch 42"
234+
);
235+
assert_eq!(
236+
server_err
237+
.inner
238+
.data()
239+
.and_then(|d| d.get().parse::<i64>().ok()),
240+
Some(42)
241+
);
242+
}
243+
244+
#[test]
245+
fn expensive_fork_via_anyhow_preserves_code() {
246+
let sm_err = StateManagerError::ExpensiveFork { epoch: 99 };
247+
let anyhow_err: anyhow::Error = sm_err.into();
248+
let server_err: ServerError = anyhow_err.into();
249+
250+
assert_eq!(
251+
server_err.known_code(),
252+
implementation_defined_errors::EXPENSIVE_FORK_CODE.into()
253+
);
254+
assert_eq!(
255+
server_err
256+
.inner
257+
.data()
258+
.and_then(|d| d.get().parse::<i64>().ok()),
259+
Some(99)
260+
);
261+
}
262+
}

src/rpc/methods/eth.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1853,15 +1853,11 @@ async fn apply_message(
18531853
msg: Message,
18541854
) -> Result<ApiInvocResult, Error> {
18551855
if let Some(ts) = &tipset
1856-
&& ts.epoch() > 0
1857-
{
1858-
let parent = ctx.chain_index().load_required_tipset(ts.parents())?;
1859-
if ctx
1856+
&& let Some(epoch) = ctx
18601857
.chain_config()
1861-
.has_expensive_fork_between(parent.epoch(), ts.epoch() + 1)
1862-
{
1863-
return Err(crate::state_manager::Error::ExpensiveFork.into());
1864-
}
1858+
.expensive_fork_between(ts.epoch(), ts.epoch() + 1)
1859+
{
1860+
return Err(crate::state_manager::Error::ExpensiveFork { epoch }.into());
18651861
}
18661862

18671863
let (invoc_res, _) = ctx

src/rpc/methods/state.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,15 @@ impl StateCall {
9090
.chain_store()
9191
.load_required_tipset_or_heaviest(&tsk)?;
9292

93-
// Match Lotus' `StateCall` behavior: if the call refuses due to an expensive
94-
// state fork between the parent and the target tipset, walk back to the parent
95-
// tipset and retry. This loop terminates when the call returns a non-`ExpensiveFork`
96-
// result (success or different error), or when we fail to load the parent tipset
97-
// (e.g. we walked back past genesis).
93+
// Match Lotus' `StateCall` behavior: parent-state `Call` refuses when a migration spans
94+
// the parent→tipset window; walk back to the parent tipset and retry. This does not
95+
// serve U+1 at the requested tipset (unlike `eth_call`, which uses explicit tipset
96+
// state).
9897
//
9998
// See: <https://github.com/filecoin-project/lotus/blob/797feebc63bfbd4fdfb742b674c97bfb7846cccb/node/impl/full/state.go#L147>
10099
loop {
101100
match state_manager.call(message, Some(tipset.shallow_clone())) {
102-
Err(crate::state_manager::Error::ExpensiveFork) => {
101+
Err(crate::state_manager::Error::ExpensiveFork { .. }) => {
103102
tipset = state_manager
104103
.chain_index()
105104
.load_required_tipset(tipset.parents())

src/state_manager/errors.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@
33

44
use std::fmt::{Debug, Display};
55

6+
use crate::shim::clock::ChainEpoch;
67
use thiserror::Error;
78
use tokio::task::JoinError;
89

910
/// State manager error
10-
#[derive(Debug, PartialEq, Error)]
11+
#[derive(Clone, Debug, PartialEq, Error)]
1112
pub enum Error {
1213
/// Error originating from state
1314
#[error("{0}")]
1415
State(String),
1516
/// Refusing explicit call due to an expensive state migration at the requested epoch.
16-
#[error("refusing explicit call due to state fork at epoch")]
17-
ExpensiveFork,
17+
#[error(
18+
"required historical state unavailable: refusing explicit call due to state fork at epoch {epoch}"
19+
)]
20+
ExpensiveFork { epoch: ChainEpoch },
1821
/// Other state manager error
1922
#[error("{0}")]
2023
Other(String),

src/state_manager/message_simulation.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,37 @@ impl StateManager {
2626

2727
let tipset = if let Some(ts) = tipset {
2828
if ts.epoch() > 0 {
29-
let parent = self
30-
.chain_index()
31-
.load_required_tipset(ts.parents())
32-
.map_err(Error::other)?;
33-
if chain_config.has_expensive_fork_between(parent.epoch(), ts.epoch() + 1) {
34-
return Err(Error::ExpensiveFork);
29+
// Lotus `callInternal` uses a parent-based fork floor for parent-state calls
30+
// (`state_cid` unset) and a tipset-height floor for explicit-state calls (Eth
31+
// `TipSetState` path).
32+
let (fork_floor, fork_height) = if state_cid.is_some() {
33+
(ts.epoch(), ts.epoch() + 1)
34+
} else {
35+
let parent = self
36+
.chain_index()
37+
.load_required_tipset(ts.parents())
38+
.map_err(Error::other)?;
39+
(parent.epoch(), ts.epoch() + 1)
40+
};
41+
if let Some(epoch) =
42+
chain_config.expensive_fork_between(fork_floor, fork_height)
43+
{
44+
return Err(Error::ExpensiveFork { epoch });
3545
}
3646
}
3747
ts
3848
} else {
3949
// Search back till we find a height with no fork, or we reach the beginning.
50+
// Uses the parent-based window (Lotus `callInternal` with `ts == nil`).
4051
let mut heaviest_ts = self.heaviest_tipset();
4152
while heaviest_ts.epoch() > 0 {
4253
let parent = self
4354
.chain_index()
4455
.load_required_tipset(heaviest_ts.parents())
4556
.map_err(Error::other)?;
46-
if !chain_config.has_expensive_fork_between(parent.epoch(), heaviest_ts.epoch() + 1)
57+
if chain_config
58+
.expensive_fork_between(parent.epoch(), heaviest_ts.epoch() + 1)
59+
.is_some()
4760
{
4861
break;
4962
}

src/tool/subcommands/api_cmd/api_compare_tests.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2455,7 +2455,7 @@ fn eth_expensive_fork_error_tests(store: Arc<ManyCar>) -> anyhow::Result<Vec<Rpc
24552455
.max()
24562456
.ok_or_else(|| anyhow::anyhow!("calibnet must define at least one expensive fork"))?;
24572457

2458-
Ok(vec![
2458+
let mut tests = vec![
24592459
RpcTest::identity(EthCall::request((
24602460
EthCallMessage::default(),
24612461
BlockNumberOrHash::from_block_number(expensive_fork_epoch),
@@ -2469,7 +2469,20 @@ fn eth_expensive_fork_error_tests(store: Arc<ManyCar>) -> anyhow::Result<Vec<Rpc
24692469
Some(BlockNumberOrHash::from_block_number(expensive_fork_epoch)),
24702470
))?)
24712471
.policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2472-
])
2472+
];
2473+
2474+
// eth_call must succeed at U+1 when migration ran at U.
2475+
let after_fork_epoch = expensive_fork_epoch + 1;
2476+
if after_fork_epoch <= heaviest_tipset.epoch() {
2477+
tests.push(
2478+
RpcTest::identity(EthCall::request((
2479+
EthCallMessage::default(),
2480+
BlockNumberOrHash::from_block_number(after_fork_epoch),
2481+
))?),
2482+
);
2483+
}
2484+
2485+
Ok(tests)
24732486
}
24742487

24752488
// Extract tests that use chain-specific data such as block CIDs or message

0 commit comments

Comments
 (0)