Skip to content

Commit 2d3ab7c

Browse files
committed
port lotus fix
1 parent e5c6427 commit 2d3ab7c

6 files changed

Lines changed: 87 additions & 53 deletions

File tree

src/networks/mod.rs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -512,20 +512,26 @@ 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 lowest expensive state migration epoch 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+
.filter(|epoch| *epoch >= parent && *epoch < height)
528+
.min()
529+
}
530+
531+
/// Reports whether an expensive migration is triggered in the half-open epoch interval
532+
/// `[parent, height)`.
533+
pub fn has_expensive_fork_between(&self, parent: ChainEpoch, height: ChainEpoch) -> bool {
534+
self.expensive_fork_between(parent, height).is_some()
529535
}
530536

531537
pub async fn genesis_bytes<DB: SettingsStore>(

src/rpc/error.rs

Lines changed: 12 additions & 0 deletions
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.
47+
pub(crate) const EXPENSIVE_FORK_CODE: i32 = -32002;
4548
}
4649

4750
impl ServerError {
@@ -98,6 +101,15 @@ 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 @ crate::state_manager::Error::ExpensiveFork { epoch }) =
105+
error.downcast_ref::<crate::state_manager::Error>()
106+
{
107+
return Self::new(
108+
implementation_defined_errors::EXPENSIVE_FORK_CODE,
109+
sm_error.to_string(),
110+
Some(serde_json::Value::from(*epoch)),
111+
);
112+
}
101113

102114
// Default fallback, not using `format!("{e:#}")` here to match Lotus error
103115
Self::internal_error(error.to_string(), None)

src/rpc/methods/eth.rs

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1854,14 +1854,11 @@ async fn apply_message(
18541854
) -> Result<ApiInvocResult, Error> {
18551855
if let Some(ts) = &tipset
18561856
&& ts.epoch() > 0
1857-
{
1858-
let parent = ctx.chain_index().load_required_tipset(ts.parents())?;
1859-
if ctx
1857+
&& ctx
18601858
.chain_config()
1861-
.has_expensive_fork_between(parent.epoch(), ts.epoch() + 1)
1862-
{
1863-
return Err(crate::state_manager::Error::ExpensiveFork.into());
1864-
}
1859+
.has_expensive_fork_between(ts.epoch(), ts.epoch() + 1)
1860+
{
1861+
return Err(crate::state_manager::Error::ExpensiveFork { epoch: ts.epoch() }.into());
18651862
}
18661863

18671864
let (invoc_res, _) = ctx
@@ -2182,19 +2179,24 @@ async fn eth_get_code(
21822179
..Default::default()
21832180
};
21842181

2185-
let api_invoc_result = 'invoc: {
2186-
for ts in ts.shallow_clone().chain(ctx.db()) {
2187-
match ctx
2188-
.state_manager
2189-
.call_on_state(state_root, &message, Some(ts))
2190-
{
2191-
Ok(res) => {
2192-
break 'invoc res;
2193-
}
2194-
Err(e) => tracing::warn!(%e),
2182+
// Rewind ts to escape the fork guard, but keep state_root fixed to the requested epoch: the
2183+
// result comes from state_root (ts only supplies execution context), so recomputing it for the
2184+
// parent would read an earlier epoch's bytecode.
2185+
let mut ts = ts.shallow_clone();
2186+
let api_invoc_result = loop {
2187+
match ctx
2188+
.state_manager
2189+
.call_on_state(state_root, &message, Some(ts.shallow_clone()))
2190+
{
2191+
Ok(res) => break res,
2192+
Err(crate::state_manager::Error::ExpensiveFork { .. }) => {
2193+
ts = ctx
2194+
.chain_index()
2195+
.load_required_tipset(ts.parents())
2196+
.map_err(|e| anyhow::anyhow!("getting parent tipset: {e}"))?;
21952197
}
2198+
Err(e) => return Err(e.into()),
21962199
}
2197-
return Err(anyhow::anyhow!("Call failed").into());
21982200
};
21992201
let Some(msg_rct) = api_invoc_result.msg_rct else {
22002202
return Err(anyhow::anyhow!("no message receipt").into());
@@ -2275,19 +2277,24 @@ async fn get_storage_at(
22752277
params,
22762278
..Default::default()
22772279
};
2278-
let api_invoc_result = 'invoc: {
2279-
for ts in ts.chain(ctx.db()) {
2280-
match ctx
2281-
.state_manager
2282-
.call_on_state(state_root, &message, Some(ts))
2283-
{
2284-
Ok(res) => {
2285-
break 'invoc res;
2286-
}
2287-
Err(e) => tracing::warn!(%e),
2280+
// Rewind ts to escape the fork guard, but keep state_root fixed to the requested epoch: the
2281+
// result comes from state_root (ts only supplies execution context), so recomputing it for the
2282+
// parent would read an earlier epoch's storage.
2283+
let mut ts = ts;
2284+
let api_invoc_result = loop {
2285+
match ctx
2286+
.state_manager
2287+
.call_on_state(state_root, &message, Some(ts.shallow_clone()))
2288+
{
2289+
Ok(res) => break res,
2290+
Err(crate::state_manager::Error::ExpensiveFork { .. }) => {
2291+
ts = ctx
2292+
.chain_index()
2293+
.load_required_tipset(ts.parents())
2294+
.map_err(|e| anyhow::anyhow!("getting parent tipset: {e}"))?;
22882295
}
2296+
Err(e) => return Err(e.into()),
22892297
}
2290-
return Err(anyhow::anyhow!("Call failed").into());
22912298
};
22922299
let Some(msg_rct) = api_invoc_result.msg_rct else {
22932300
return Err(anyhow::anyhow!("no message receipt").into());

src/rpc/methods/state.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,14 @@ 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+
// Parent-state `call` refuses when a migration spans the parent→tipset window; walk back
94+
// to the parent tipset and retry. This does not serve U+1 at the requested tipset (unlike
95+
// `eth_call`, which uses explicit tipset state).
9896
//
9997
// See: <https://github.com/filecoin-project/lotus/blob/797feebc63bfbd4fdfb742b674c97bfb7846cccb/node/impl/full/state.go#L147>
10098
loop {
10199
match state_manager.call(message, Some(tipset.shallow_clone())) {
102-
Err(crate::state_manager::Error::ExpensiveFork) => {
100+
Err(crate::state_manager::Error::ExpensiveFork { .. }) => {
103101
tipset = state_manager
104102
.chain_index()
105103
.load_required_tipset(tipset.parents())

src/state_manager/errors.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

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

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

@@ -13,8 +14,10 @@ pub enum Error {
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: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,20 @@ 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+
// Explicit-state calls already hold every fork below the tipset epoch, so only a
30+
// migration at the tipset epoch itself is refused. Parent-state calls (no explicit
31+
// state) lag a tipset, so a migration at the parent epoch must also be refused.
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) = chain_config.expensive_fork_between(fork_floor, fork_height) {
42+
return Err(Error::ExpensiveFork { epoch });
3543
}
3644
}
3745
ts

0 commit comments

Comments
 (0)