Skip to content
Open
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
58 changes: 58 additions & 0 deletions src/networks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,38 @@ impl From<Height> for NetworkVersion {
}
}

/// Checks if the given height is an expensive migration.
/// See <https://github.com/filecoin-project/lotus/blob/master/chain/consensus/filcns/upgrades.go>
pub const fn is_expensive_migration(height: Height) -> bool {
matches!(
height,
Height::Assembly
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How about *Fix migrations on calibnet?

Copy link
Copy Markdown
Contributor Author

@sudo-shashank sudo-shashank May 20, 2026

Choose a reason for hiding this comment

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

*Fix are not marked as expensive migrations in lotus here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we could implement expensive check by checking height key existence in get_migrations result.

  • I think fix heights are not marked as expensive in Lotus because they don't work on mainnet, it's logically correct only on mainnet but wrong on calibnet unless they define separate mappings for calibnet and mainnet.
  • We could add unimplemented migrations to the get_migrations mappings with unimplemented_migration placeholder

@coderabbitai could you check in https://github.com/Filecoin-project/lotus if my understanding is correct

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check Lotus upgrades for *Fix migrations and their Expensive flags
# Also check if there's a separate calibnet upgrade schedule
echo "=== Mainnet upgrade schedule (Fix entries) ==="
rg -n "Fix|WatermelonFix|DragonFix|TockFix" chain/consensus/filcns/upgrades.go -A2 -B2 | head -120

echo ""
echo "=== Is there a calibnet-specific upgrade schedule? ==="
fd -t f "upgrades" chain/ --exec echo {}

echo ""
echo "=== expensiveUpgrades population ==="
rg -n "expensiveUpgrades|Expensive" chain/stmgr/stmgr.go -C3 | head -60

Repository: filecoin-project/lotus
Length of output: 5136


🏁 Script executed:

#!/bin/bash
# Check Forest's get_migrations function
echo "=== get_migrations function ==="
ast-grep --pattern 'fn get_migrations($$$) {
  $$$
}' | head -100

echo ""
echo "=== rg get_migrations ==="
rg -n "get_migrations" --type rust -C3 | head -100

echo ""
echo "=== unimplemented_migration ==="
rg -n "unimplemented_migration\|noop_migration\|no_migration" --type rust -C3 | head -60

Repository: ChainSafe/forest
Length of output: 1620


🏁 Script executed:

#!/bin/bash
echo "=== Full get_migrations function ==="
rg -n "" src/state_migration/mod.rs | head -80

echo ""
echo "=== Lotus buildconstants for Fix heights on mainnet ==="

Repository: ChainSafe/forest
Length of output: 3427


🏁 Script executed:

#!/bin/bash
echo "=== Fix heights values in buildconstants ==="
rg -n "WatermelonFix|DragonFix|TockFix|CalibrationDragon" build/buildconstants/ -C2 | head -80

echo ""
echo "=== mainnet buildconstants ==="
rg -n "WatermelonFix|DragonFix|TockFix" build/buildconstants/consensus.go -C1 | head -40

echo ""
echo "=== Separate mainnet vs calibnet upgrade schedule? ==="
rg -n "func.*Upgrade.*Schedule\|UpgradeSchedule\|defaultUpgrade" chain/consensus/filcns/upgrades.go | head -30

echo ""
echo "=== How the upgrade schedule is selected per-network ==="
rg -n "UpgradeSchedule\|GetUpgradeSchedule\|upgradeSchedule" chain/stmgr/stmgr.go -C3 | head -60

Repository: filecoin-project/lotus
Length of output: 5829


@hanabi1224 Your understanding is confirmed. Here's what the Lotus codebase shows:

*Why Fix heights are not marked expensive in Lotus:
In params_mainnet.go, all fix heights are set to negative values with explicit comments:

  • UpgradeWatermelonFixHeight = -1"This fix upgrade only ran on calibrationnet"
  • UpgradeWatermelonFix2Height = -2 — same comment
  • UpgradeCalibrationDragonFixHeight = -3 — same comment
  • UpgradeTockFixHeight = -1"Only applied to calibnet..."

Lotus uses a single upgrade schedule (upgrades.go), but per-network build constants make fix heights negative on mainnet so they're never triggered. Since they don't apply on mainnet, they were never marked Expensive: true. There is no separate mapping per-network — the fix is in the constants.

Forest's get_migrations is already per-network: It already correctly excludes all fix heights from mainnet and includes them in calibnet:

  • Mainnet: no WatermelonFix, WatermelonFix2, DragonFix, TockFix
  • Calibnet: includes all four

Regarding @hanabi1224's suggestion: Deriving the expensive check from get_migrations keyset makes a lot of sense and would be more correct than the current hard-coded is_expensive_migration predicate. The current approach misses marking fix migrations as expensive on calibnet, where they do have real migrations. Using get_migrations as the source of truth would automatically handle this correctly without any unimplemented_migration placeholder needed.

In practice this would mean: instead of the static is_expensive_migration(Height) function, has_expensive_fork_between (or the HeightInfo::expensive flag population) would check whether a given height has an entry in get_migrations(&network) for the relevant network. Any height with a registered migration is inherently expensive.


🧠 Learnings used
Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 6381
File: src/lotus_json/actors/states/cron_state.rs:8-8
Timestamp: 2026-01-05T12:54:40.850Z
Learning: In Rust code reviews, do not derive Eq for a struct if any field does not implement Eq (e.g., types from external dependencies). If a type like CronStateLotusJson includes fields wrapping external dependencies that lack Eq, derive PartialEq (or implement PartialEq manually) but avoid deriving Eq. This ensures comparisons compile and reflect actual equivalence semantics. When needed, consider implementing custom PartialEq (and possibly Eq) only after ensuring all fields (or wrappers) implement Eq, or keep PartialEq-only if full equality semantics cannot be expressed.

Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 6381
File: src/lotus_json/actors/states/evm_state.rs:41-44
Timestamp: 2026-01-05T12:56:13.802Z
Learning: In Rust codebases (e.g., Forest), do not add #[cfg(test)] to functions already annotated with #[test]. The #[test] attribute ensures the function is compiled only for tests, so a separate #[cfg(test)] is redundant and can be removed if present. Apply this check to all Rust files that contain #[test] functions.

Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 6666
File: src/tool/subcommands/archive_cmd.rs:628-631
Timestamp: 2026-03-02T09:43:34.946Z
Learning: In Rust sources related to Forest snapshot handling, implement the rule: enable message_receipts and events (message_receipts: true, events: true) only for GC snapshots as defined in src/db/gc/snapshot.rs, since these are internal snapshots created during garbage collection. For user-facing export commands such as src/tool/subcommands/archive_cmd.rs, disable receipts and events by default (message_receipts: false, events: false) to keep user-facing snapshots smaller, unless explicitly requested. This guidance targets Rust files; apply consistently across similar snapshot-related modules, using the narrowest applicable scope when extending beyond the two specified files.

Learnt from: LesnyRumcajs
Repo: ChainSafe/forest PR: 6903
File: src/shim/executor.rs:243-251
Timestamp: 2026-04-14T17:24:13.631Z
Learning: In the ChainSafe/forest (LesnyRumcajs) codebase, do not require rustdoc comments on public methods when the method’s purpose is clearly self-explanatory from its name (e.g., `entries(&self)`, `into_entries(self)`). When reviewing Rust (`.rs`) files, avoid flagging missing documentation for these obviously named public APIs, reserving doc requirements for less clear or non-obvious public methods.

Learnt from: sudo-shashank
Repo: ChainSafe/forest PR: 7061
File: src/networks/mod.rs:542-550
Timestamp: 2026-05-19T05:07:54.755Z
Learning: When reviewing ChainSafe/forest code that implements the Lotus-style “has expensive fork between” logic (e.g., `ChainConfig::has_expensive_fork_between(parent, height)`), accept comparisons that use an inclusive lower bound like `info.epoch >= parent`. This is intentional and matches Lotus’s `HasExpensiveForkBetween`, which iterates `for h := parent; h < height`, defining the interval `[parent, height)`—do not flag or “fix” `>= parent` to `> parent` or otherwise change the lower-bound semantics.

Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 7077
File: src/utils/cache/size_tracking.rs:174-182
Timestamp: 2026-05-19T10:55:04.684Z
Learning: In ChainSafe/forest code reviews, avoid raising review comments for issues that are likely compile-time failures (e.g., missing imports, unresolved types, or other compiler errors that CI will catch). Instead, focus feedback on logic, correctness, and design/maintainability concerns, since the CI pipeline reliably verifies compilation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@coderabbitai I meant we do need to add mappings like Height::Trust -> 'unimplemented_migration' to the mappings in Forest to properly mark Height::Trust as expensive to match Lotus. Note that some RPC methods returns error on expensive fork without running the migration, missing the mapping would lead to incorrect result

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== state_migration mod.rs full content ==="
cat src/state_migration/mod.rs

echo ""
echo "=== get_migrations function ==="
rg -n "get_migrations\|fn.*migration" src/state_migration/ -C2 | head -100

echo ""
echo "=== unimplemented_migration or noop or placeholder ==="
rg -n "unimplemented\|noop\|placeholder\|no.op" src/state_migration/ -i -C2 | head -60

echo ""
echo "=== Which heights are registered in get_migrations for mainnet vs calibnet? ==="
rg -n "Height::" src/state_migration/ -C1 | head -200

Repository: ChainSafe/forest
Length of output: 15570


@hanabi1224 Thank you for the clarification — understood now. The analysis confirms there are two concrete gaps:

Gap 1 — Old pre-NV17 heights (Assembly, Trust, Turbo, Hyperdrive, Chocolate, OhSnap, Skyr): These are in is_expensive_migration but have no entry in get_migrations for any network, because Forest only implements migrations from NV17 (Shark) onward. Adding unimplemented_migration placeholder stubs for these in get_migrations would let the keyset-based approach cover them correctly.

Gap 2 — Calibnet Fix heights (WatermelonFix, WatermelonFix2, DragonFix, TockFix): These already have real migration implementations registered in calibnet's get_migrations, but are not in is_expensive_migration. This is an actual correctness bug in the current PR: has_expensive_fork_between will return false for these epochs on calibnet, so the expensive-fork guard won't fire and the node could end up running a heavy migration during an RPC call instead of returning ErrExpensiveFork.

The proposed fix — deriving is_expensive_migration (or the HeightInfo::expensive flag) from the get_migrations keyset — would resolve both gaps:

  • Replace the static predicate with a lookup into get_migrations(&network)
  • Add unimplemented_migration stubs for old pre-NV17 expensive heights (Assembly, Trust, Turbo, Hyperdrive, Chocolate, OhSnap, Skyr) to each applicable network's migration list

This would make the migration map the single source of truth, with no possibility of the two lists drifting out of sync.

@sudo-shashank — does this refactor make sense to incorporate into this PR, or would you prefer to address it as a follow-up?


✏️ Learnings added
Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 7061
File: src/networks/mod.rs:230-230
Timestamp: 2026-05-22T06:39:02.436Z
Learning: In ChainSafe/forest, the `is_expensive_migration` static predicate in `src/networks/mod.rs` has a correctness gap: calibnet Fix heights (WatermelonFix, WatermelonFix2, DragonFix, TockFix) are registered in `src/state_migration/mod.rs` `get_migrations` for calibnet but are NOT marked expensive in `is_expensive_migration`, causing `has_expensive_fork_between` to return false for those epochs on calibnet. The preferred fix is to derive the expensive flag from the `get_migrations` keyset per-network, and add `unimplemented_migration` stubs for pre-NV17 expensive heights (Assembly, Trust, Turbo, Hyperdrive, Chocolate, OhSnap, Skyr) that lack Forest migration implementations.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 6381
File: src/lotus_json/actors/states/cron_state.rs:8-8
Timestamp: 2026-01-05T12:54:40.850Z
Learning: In Rust code reviews, do not derive Eq for a struct if any field does not implement Eq (e.g., types from external dependencies). If a type like CronStateLotusJson includes fields wrapping external dependencies that lack Eq, derive PartialEq (or implement PartialEq manually) but avoid deriving Eq. This ensures comparisons compile and reflect actual equivalence semantics. When needed, consider implementing custom PartialEq (and possibly Eq) only after ensuring all fields (or wrappers) implement Eq, or keep PartialEq-only if full equality semantics cannot be expressed.

Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 6381
File: src/lotus_json/actors/states/evm_state.rs:41-44
Timestamp: 2026-01-05T12:56:13.802Z
Learning: In Rust codebases (e.g., Forest), do not add #[cfg(test)] to functions already annotated with #[test]. The #[test] attribute ensures the function is compiled only for tests, so a separate #[cfg(test)] is redundant and can be removed if present. Apply this check to all Rust files that contain #[test] functions.

Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 6666
File: src/tool/subcommands/archive_cmd.rs:628-631
Timestamp: 2026-03-02T09:43:34.946Z
Learning: In Rust sources related to Forest snapshot handling, implement the rule: enable message_receipts and events (message_receipts: true, events: true) only for GC snapshots as defined in src/db/gc/snapshot.rs, since these are internal snapshots created during garbage collection. For user-facing export commands such as src/tool/subcommands/archive_cmd.rs, disable receipts and events by default (message_receipts: false, events: false) to keep user-facing snapshots smaller, unless explicitly requested. This guidance targets Rust files; apply consistently across similar snapshot-related modules, using the narrowest applicable scope when extending beyond the two specified files.

Learnt from: LesnyRumcajs
Repo: ChainSafe/forest PR: 6903
File: src/shim/executor.rs:243-251
Timestamp: 2026-04-14T17:24:13.631Z
Learning: In the ChainSafe/forest (LesnyRumcajs) codebase, do not require rustdoc comments on public methods when the method’s purpose is clearly self-explanatory from its name (e.g., `entries(&self)`, `into_entries(self)`). When reviewing Rust (`.rs`) files, avoid flagging missing documentation for these obviously named public APIs, reserving doc requirements for less clear or non-obvious public methods.

Learnt from: sudo-shashank
Repo: ChainSafe/forest PR: 7061
File: src/networks/mod.rs:542-550
Timestamp: 2026-05-19T05:07:54.755Z
Learning: When reviewing ChainSafe/forest code that implements the Lotus-style “has expensive fork between” logic (e.g., `ChainConfig::has_expensive_fork_between(parent, height)`), accept comparisons that use an inclusive lower bound like `info.epoch >= parent`. This is intentional and matches Lotus’s `HasExpensiveForkBetween`, which iterates `for h := parent; h < height`, defining the interval `[parent, height)`—do not flag or “fix” `>= parent` to `> parent` or otherwise change the lower-bound semantics.

Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 7077
File: src/utils/cache/size_tracking.rs:174-182
Timestamp: 2026-05-19T10:55:04.684Z
Learning: In ChainSafe/forest code reviews, avoid raising review comments for issues that are likely compile-time failures (e.g., missing imports, unresolved types, or other compiler errors that CI will catch). Instead, focus feedback on logic, correctness, and design/maintainability concerns, since the CI pipeline reliably verifies compilation.

| 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<Cid>,
#[serde(default)]
pub expensive: bool,
}

pub struct HeightInfoWithActorManifest<'a> {
Expand Down Expand Up @@ -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)
Comment thread
sudo-shashank marked this conversation as resolved.
}

pub async fn genesis_bytes<DB: SettingsStore>(
&self,
db: &DB,
Expand Down Expand Up @@ -599,6 +637,7 @@ macro_rules! make_height {
HeightInfo {
epoch: $epoch,
bundle: None,
expensive: $crate::networks::is_expensive_migration(Height::$id),
},
)
};
Expand All @@ -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),
},
)
};
Expand Down Expand Up @@ -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);
Expand Down
14 changes: 13 additions & 1 deletion src/rpc/methods/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
Expand All @@ -1814,6 +1814,18 @@ async fn apply_message(
tipset: Option<Tipset>,
msg: Message,
) -> Result<ApiInvocResult, Error> {
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)
Expand Down
22 changes: 20 additions & 2 deletions src/rpc/methods/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,28 @@ impl StateCall {
message: &Message,
tsk: Option<TipsetKey>,
) -> anyhow::Result<ApiInvocResult> {
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: <https://github.com/filecoin-project/lotus/blob/797feebc63bfbd4fdfb742b674c97bfb7846cccb/node/impl/full/state.go#L147>
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?),
}
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/state_manager/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment thread
sudo-shashank marked this conversation as resolved.
/// Other state manager error
#[error("{0}")]
Other(String),
Expand Down
73 changes: 54 additions & 19 deletions src/state_manager/message_simulation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,63 @@ use crate::shim::address::Protocol;
use crate::shim::crypto::{Signature, SignatureType};
use crate::shim::executor::ApplyRet;
use crate::shim::message::Message;
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<Cid>,
msg: &Message,
rand: ChainRand,
tipset: &Tipset,
tipset: Option<Tipset>,
) -> Result<ApiInvocResult, Error> {
let mut msg = msg.clone();
let chain_config = self.chain_config();

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 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
};
Comment thread
sudo-shashank marked this conversation as resolved.

let prior_messsages = tipset_messages
.iter()
.filter(|ts_msg| ts_msg.message().from() == msg.from());
let state_cid = state_cid.unwrap_or(*tipset.parent_state());

// 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());
Expand All @@ -46,7 +76,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,
Expand All @@ -61,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)?;
}
Expand Down Expand Up @@ -96,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<Tipset>) -> Result<ApiInvocResult, Error> {
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
Expand All @@ -109,9 +146,7 @@ impl StateManager {
message: &Message,
tipset: Option<Tipset>,
) -> Result<ApiInvocResult, Error> {
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(
Expand Down
32 changes: 31 additions & 1 deletion src/tool/subcommands/api_cmd/api_compare_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2444,6 +2444,34 @@ fn f3_tests_with_tipset(tipset: &Tipset) -> anyhow::Result<Vec<RpcTest>> {
])
}

fn eth_expensive_fork_error_tests(store: Arc<ManyCar>) -> anyhow::Result<Vec<RpcTest>> {
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()
.ok_or_else(|| anyhow::anyhow!("calibnet must define at least one expensive fork"))?;

Ok(vec![
RpcTest::identity(EthCall::request((
EthCallMessage::default(),
BlockNumberOrHash::from_block_number(expensive_fork_epoch),
))?)
.policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
RpcTest::identity(EthEstimateGas::request((
EthCallMessage {
from: Some(generate_eth_random_address()?),
..Default::default()
},
Some(BlockNumberOrHash::from_block_number(expensive_fork_epoch)),
))?)
.policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
])
}

// 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(
Expand All @@ -2463,6 +2491,8 @@ fn snapshot_tests(
.last()
.expect("Infallible");

tests.extend(eth_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)?);
Expand Down Expand Up @@ -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,
Expand Down
Loading