Skip to content

Conversation

@sudo-shashank
Copy link
Contributor

@sudo-shashank sudo-shashank commented Jan 15, 2026

Summary of changes

Changes introduced in this pull request:

  • Implement has_expensive_fork_between to check if executing tipsets between specified heights would trigger an expensive migration, excluding migrations at the target height.
  • Return ErrExpensiveFork error , matching Lotus behavior, to prevent performance degradation.

Reference issue to close (if applicable)

Closes #5965

Other information and links

Change checklist

  • I have performed a self-review of my own code,
  • I have made corresponding changes to the documentation. All new code adheres to the team's documentation standards,
  • I have added tests that prove my fix is effective or that my feature works (if possible),
  • I have made sure the CHANGELOG is up-to-date. All user-facing changes should be reflected in this document.

Summary by CodeRabbit

  • New Features

    • Safeguards to block message execution when an expensive state migration is detected between chain epochs.
    • Added expanded public checks to query expensive migration heights and detect expensive forks between epochs.
    • Introduced a distinct public error variant to surface expensive-fork conditions.
  • Tests

    • Added unit tests validating detection of expensive migrations between epochs.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 15, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Adds detection of "expensive" state-migration epochs and blocks RPC execution that would cross such a fork by returning an ExpensiveFork error instead of performing the migration.

Changes

Cohort / File(s) Summary
Error Handling
src/state_manager/errors.rs
Added ExpensiveFork variant to the Error enum (refusing explicit call due to state fork at epoch).
Expensive Migration Data
src/state_migration/mod.rs
Added pub fn get_expensive_migration_heights(chain: &NetworkChain) -> &'static [Height] returning network-specific expensive-migration heights.
State Manager Core
src/state_manager/mod.rs
Added expensive_migration_epochs: HashSet<ChainEpoch> field, initialized from get_expensive_migration_heights, and pub fn has_expensive_fork_between(&self, parent: ChainEpoch, height: ChainEpoch) -> bool. Integrated checks to detect expensive forks.
RPC Guard
src/rpc/methods/eth.rs
Added pre-flight parent-tipset load and call to has_expensive_fork_between; returns StateManagerError::ExpensiveFork when an expensive fork is detected before invoking apply_on_state_with_gas.
Tests
src/state_manager/tests.rs
Added test_has_expensive_fork_between validating detection of an expensive migration epoch in test chain setup.

Sequence Diagram(s)

sequenceDiagram
  participant RPC as RPC apply_message
  participant TS as TipsetStore
  participant SM as StateManager
  participant Chain as ChainConfig

  RPC->>TS: load parent tipset for provided tipset
  RPC->>SM: has_expensive_fork_between(parent.epoch, tipset.epoch+1)
  alt expensive fork detected
    SM-->>RPC: Err::ExpensiveFork
    RPC-->>Client: return error
  else no expensive fork
    RPC->>SM: apply_on_state_with_gas(...)
    SM->>Chain: (may run migrations / state transitions)
    SM-->>RPC: result
    RPC-->>Client: return result
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • LesnyRumcajs
  • akaladarshi
  • hanabi1224
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Implement expensive fork check' directly and clearly summarizes the main change—adding functionality to detect and handle expensive forks before executing messages.
Linked Issues check ✅ Passed The PR implements all core requirements from #5965: a has_expensive_fork_between check, ExpensiveFork error return, per-network configuration of expensive migrations, and guards in RPC methods.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing expensive fork detection: error type, migration tracking, fork detection logic, and related tests—no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@sudo-shashank sudo-shashank added the RPC requires calibnet RPC checks to run on CI label Jan 15, 2026
@sudo-shashank sudo-shashank marked this pull request as ready for review January 15, 2026 10:49
@sudo-shashank sudo-shashank requested a review from a team as a code owner January 15, 2026 10:49
@sudo-shashank sudo-shashank requested review from LesnyRumcajs, akaladarshi and hanabi1224 and removed request for a team January 15, 2026 10:49
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/state_manager/errors.rs`:
- Around line 18-19: The ExpensiveFork enum variant's error message should match
Lotus and include a doc comment: update the #[error(...)] on the ExpensiveFork
variant in src/state_manager/errors.rs to use the text "refusing explicit call
due to state fork at epoch" and add a short /// doc comment above the
ExpensiveFork variant describing when this error is returned (e.g., returned
when an explicit call is refused due to a state fork at a particular epoch);
ensure the variant name ExpensiveFork remains unchanged and that the new doc
comment follows Rust doc comment conventions.
🧹 Nitpick comments (4)
src/state_manager/tests.rs (1)

11-11: Strengthen test to cover the “exclude target height” boundary.

Right now the test only covers the positive case. Add a negative assertion to validate that an expensive migration at the target height is excluded (per the method docs/intent).

Proposed diff
-use crate::state_manager::get_expensive_migration_heights;
+use crate::state_migration::get_expensive_migration_heights;

 #[test]
 fn test_has_expensive_fork_between() {
     let TestChainSetup { state_manager, .. } = setup_chain_with_tipsets();
     let chain_config = state_manager.chain_config();

     let expensive_epoch = get_expensive_migration_heights(&chain_config.network)
         .iter()
         .find_map(|height| {
             let epoch = chain_config.epoch(*height);
             (epoch > 0).then_some(epoch)
         })
         .expect("expected at least one expensive migration epoch > 0");

+    // Excludes migrations at the target height.
+    assert!(!state_manager.has_expensive_fork_between(expensive_epoch - 1, expensive_epoch));
     assert!(state_manager.has_expensive_fork_between(expensive_epoch, expensive_epoch + 1));
 }

Also applies to: 438-452

src/rpc/methods/eth.rs (1)

57-57: Avoid cloning the tipset; use .context() for the parent load.

Proposed diff
-    if let Some(tipset) = tipset.clone()
-        && tipset.epoch() > 0
-    {
-        let parent_ts = Tipset::load_required(ctx.store(), tipset.parents())
-            .map_err(|e| anyhow::anyhow!("failed to load parent tipset: {e}"))?;
+    if let Some(tipset) = tipset.as_ref()
+        && tipset.epoch() > 0
+    {
+        let parent_ts = Tipset::load_required(ctx.store(), tipset.parents())
+            .context("failed to load parent tipset")?;
 
         ensure!(
             !ctx.state_manager
                 .has_expensive_fork_between(parent_ts.epoch(), tipset.epoch() + 1),
             StateManagerError::ExpensiveFork
         );
     }

Also applies to: 2021-2032

src/state_manager/mod.rs (1)

462-472: Make this a doc comment (///) and consider an O(#expensive_epochs) implementation.

This is a public method; repo guidelines call for doc comments. Also, iterating the set is enough (the set is tiny) and avoids scanning large epoch ranges.

Proposed diff
-    // HasExpensiveForkBetween returns true where executing tipsets between the specified heights would
-    // trigger an expensive migration.
-    // Note: migrations occurring at the target height are not included, as they're executed after the target height.
+    /// Returns `true` if executing tipsets in `[parent, height)` would run an expensive migration.
+    ///
+    /// Note: migrations at `height` are excluded (they run after `height`).
     pub fn has_expensive_fork_between(&self, parent: ChainEpoch, height: ChainEpoch) -> bool {
-        for epoch in parent..height {
-            if self.expensive_migration_epochs.contains(&epoch) {
-                return true;
-            }
-        }
-        false
+        let range = parent..height;
+        self.expensive_migration_epochs
+            .iter()
+            .any(|&epoch| range.contains(&epoch))
     }
src/state_migration/mod.rs (1)

92-133: De-duplicate the common list using a constant.

The arrays for Mainnet, Calibnet, and Devnet(_) are identical and should be extracted to a const COMMON_EXPENSIVE_MIGRATIONS to reduce duplication and maintenance burden. The proposed refactoring is correct and does not change behavior.

Note: Verify whether Calibnet's Fix migrations (WatermelonFix, WatermelonFix2, DragonFix, TockFix) should be included in the expensive set by cross-referencing Lotus's network upgrade definitions, as they are currently excluded while being present in get_migrations().

Proposed diff
+const COMMON_EXPENSIVE_MIGRATIONS: &[Height] = &[
+    Height::Shark,
+    Height::Hygge,
+    Height::Lightning,
+    Height::Watermelon,
+    Height::Dragon,
+    Height::Waffle,
+    Height::TukTuk,
+    Height::Teep,
+    Height::GoldenWeek,
+];
+
 /// Returns the heights at which expensive migrations occur.
 pub fn get_expensive_migration_heights(chain: &NetworkChain) -> &'static [Height] {
     match chain {
-        NetworkChain::Mainnet => &[
-            Height::Shark,
-            Height::Hygge,
-            Height::Lightning,
-            Height::Watermelon,
-            Height::Dragon,
-            Height::Waffle,
-            Height::TukTuk,
-            Height::Teep,
-            Height::GoldenWeek,
-        ],
-
-        NetworkChain::Calibnet => &[
-            Height::Shark,
-            Height::Hygge,
-            Height::Lightning,
-            Height::Watermelon,
-            Height::Dragon,
-            Height::Waffle,
-            Height::TukTuk,
-            Height::Teep,
-            Height::GoldenWeek,
-        ],
+        NetworkChain::Mainnet | NetworkChain::Calibnet | NetworkChain::Devnet(_) => {
+            COMMON_EXPENSIVE_MIGRATIONS
+        }
 
         NetworkChain::Butterflynet => &[Height::Teep, Height::GoldenWeek],
-
-        NetworkChain::Devnet(_) => &[
-            Height::Shark,
-            Height::Hygge,
-            Height::Lightning,
-            Height::Watermelon,
-            Height::Dragon,
-            Height::Waffle,
-            Height::TukTuk,
-            Height::Teep,
-            Height::GoldenWeek,
-        ],
     }
 }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b7f3ec6 and 90e39f7.

📒 Files selected for processing (5)
  • src/rpc/methods/eth.rs
  • src/state_manager/errors.rs
  • src/state_manager/mod.rs
  • src/state_manager/tests.rs
  • src/state_migration/mod.rs
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Use anyhow::Result<T> for most operations and add context with .context() when operations fail
Use tokio::task::spawn_blocking for CPU-intensive work such as VM execution and cryptography operations
Use .get() instead of indexing with [index] for safe element access
Do not use unwrap() in production code; use ? or expect() with descriptive messages instead
Do not use dbg! macro in non-test Rust code
Do not use todo! macro in non-test Rust code
Use strum crate for enum string conversions
Use derive_more crate for common trait implementations
Document all public functions and structs with doc comments
Use #[cfg(test)] or #[cfg(feature = "doctest-private")] for test-only code
Use channel-based communication (flume, tokio channels) between async tasks
Use consistent formatting following cargo fmt standards
Ensure code passes cargo clippy --all-targets --no-deps -- --deny=warnings linter checks

Files:

  • src/state_manager/errors.rs
  • src/state_manager/tests.rs
  • src/state_migration/mod.rs
  • src/state_manager/mod.rs
  • src/rpc/methods/eth.rs
**/mod.rs

📄 CodeRabbit inference engine (AGENTS.md)

Each module should have a mod.rs file that exports the public API with private submodules for implementation details

Files:

  • src/state_migration/mod.rs
  • src/state_manager/mod.rs
🧠 Learnings (7)
📓 Common learnings
Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 5944
File: src/chain/store/index.rs:0-0
Timestamp: 2025-08-18T03:09:47.932Z
Learning: In Forest's tipset_by_height caching implementation, hanabi1224 prefers performance-conscious solutions that leverage finality guarantees rather than expensive chain walking for fork detection. The approach of constraining cache lookups to finalized epochs (using CHECKPOINT_INTERVAL >= CHAIN_FINALITY) provides fork safety without the performance cost of ancestry verification.
📚 Learning: 2026-01-05T12:54:40.850Z
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.

Applied to files:

  • src/state_manager/errors.rs
  • src/state_manager/tests.rs
  • src/state_migration/mod.rs
  • src/state_manager/mod.rs
  • src/rpc/methods/eth.rs
📚 Learning: 2026-01-05T12:56:13.802Z
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.

Applied to files:

  • src/state_manager/errors.rs
  • src/state_manager/tests.rs
  • src/state_migration/mod.rs
  • src/state_manager/mod.rs
  • src/rpc/methods/eth.rs
📚 Learning: 2025-09-11T16:03:14.328Z
Learnt from: akaladarshi
Repo: ChainSafe/forest PR: 6000
File: src/tool/subcommands/api_cmd/state_decode_params_tests/miner.rs:4-9
Timestamp: 2025-09-11T16:03:14.328Z
Learning: In the Forest codebase's state_decode_params_tests module, common imports like `to_vec`, `TokenAmount`, `Cid`, etc. are centralized in the mod.rs file and made available to submodules through `use super::*;`, eliminating the need for duplicate imports in individual test files.

Applied to files:

  • src/state_manager/tests.rs
📚 Learning: 2025-08-18T03:09:47.932Z
Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 5944
File: src/chain/store/index.rs:0-0
Timestamp: 2025-08-18T03:09:47.932Z
Learning: In Forest's tipset_by_height caching implementation, hanabi1224 prefers performance-conscious solutions that leverage finality guarantees rather than expensive chain walking for fork detection. The approach of constraining cache lookups to finalized epochs (using CHECKPOINT_INTERVAL >= CHAIN_FINALITY) provides fork safety without the performance cost of ancestry verification.

Applied to files:

  • src/state_manager/tests.rs
  • src/state_manager/mod.rs
  • src/rpc/methods/eth.rs
📚 Learning: 2025-10-17T14:24:47.046Z
Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 6167
File: src/tool/subcommands/state_compute_cmd.rs:89-91
Timestamp: 2025-10-17T14:24:47.046Z
Learning: In `src/tool/subcommands/state_compute_cmd.rs`, when using `ReadOpsTrackingStore` to generate minimal snapshots, `HEAD_KEY` should be written to `db.tracker` (not `db` itself) before calling `export_forest_car()`, because the export reads from the tracker MemoryDB which accumulates only the accessed data during computation.

Applied to files:

  • src/state_manager/mod.rs
  • src/rpc/methods/eth.rs
📚 Learning: 2025-08-08T12:11:55.266Z
Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 5867
File: src/ipld/util.rs:461-487
Timestamp: 2025-08-08T12:11:55.266Z
Learning: Forest (src/ipld/util.rs, Rust): In UnorderedChainStream::poll_next, dropping `extract_sender` (when no more tipsets and the extract queue is empty) is the intended shutdown signal for workers. Any subsequent attempt to enqueue work after this drop is a logic error and should be treated as an error; do not change `send()` to ignore a missing sender.

Applied to files:

  • src/rpc/methods/eth.rs
🧬 Code graph analysis (2)
src/state_manager/tests.rs (2)
src/state_migration/mod.rs (1)
  • get_expensive_migration_heights (93-133)
src/state_manager/mod.rs (1)
  • has_expensive_fork_between (465-472)
src/state_manager/mod.rs (1)
src/state_migration/mod.rs (2)
  • get_expensive_migration_heights (93-133)
  • run_state_migrations (136-205)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: tests-release
  • GitHub Check: Build forest binaries on Linux AMD64
  • GitHub Check: Coverage
🔇 Additional comments (2)
src/state_manager/mod.rs (2)

59-65: Good: precompute expensive migration epochs during StateManager init.

Keeps the runtime checks cheap and avoids repeated Height -> epoch mapping.

Also applies to: 171-213


613-620: Pre-flight expensive fork guard in call_raw matches the intent.

Returning Error::ExpensiveFork early prevents RPC-triggered costly migrations in this path.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@sudo-shashank sudo-shashank marked this pull request as draft January 15, 2026 11:22

ensure!(
!ctx.state_manager
.has_expensive_fork_between(parent_ts.epoch(), tipset.epoch() + 1),
Copy link
Contributor

@hanabi1224 hanabi1224 Jan 15, 2026

Choose a reason for hiding this comment

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

Why would it be 2 epochs in the range?

Copy link
Contributor Author

@sudo-shashank sudo-shashank Jan 15, 2026

Choose a reason for hiding this comment

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

i guess lotus does this to early detect an expensive fork

@sudo-shashank sudo-shashank marked this pull request as ready for review January 15, 2026 13:48
@sudo-shashank sudo-shashank marked this pull request as draft January 15, 2026 14:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

RPC requires calibnet RPC checks to run on CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Investigate expensive fork handling in Forest

3 participants