Skip to content

feat: prefill RPC caches for new validated tipsets#7068

Open
hanabi1224 wants to merge 1 commit into
mainfrom
hm/prefill-rpc-cache
Open

feat: prefill RPC caches for new validated tipsets#7068
hanabi1224 wants to merge 1 commit into
mainfrom
hm/prefill-rpc-cache

Conversation

@hanabi1224
Copy link
Copy Markdown
Contributor

@hanabi1224 hanabi1224 commented May 15, 2026

Summary of changes

Changes introduced in this pull request:

Reference issue to close (if applicable)

Closes

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.

Outside contributions

  • I have read and agree to the CONTRIBUTING document.
  • I have read and agree to the AI Policy document. I understand that failure to comply with the guidelines will lead to rejection of the pull request.

Summary by CodeRabbit

  • New Features

    • Added validated tipset broadcast mechanism for improved synchronization visibility.
  • Performance

    • Increased Ethereum block cache capacity from 500 to 1024 entries to improve RPC query performance.
    • Implemented proactive RPC cache warming during node synchronization to reduce latency on block-related requests.
  • Refactor

    • Optimized internal state manager architecture for improved efficiency.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 15, 2026

Walkthrough

This PR introduces a validated tipset broadcast channel in the sync state machine, refactors Ethereum block conversion to accept StateManager references instead of full context, and integrates a reactive cache-warming loop in the daemon that proactively loads and warms RPC caches for newly validated tipsets when the node is not fully synced.

Changes

Validated Tipset Broadcast and RPC Cache Warming

Layer / File(s) Summary
Validated Tipset Broadcast Infrastructure
src/chain_sync/chain_follower.rs
ChainFollower exposes public subscribe_validated_tipset() returning a broadcast receiver; SyncStateMachine adds a broadcast sender field (capacity 1024); mark_validated_tipset now returns bool indicating successful acceptance; conditional broadcast of validated tipset keys in update() only when success and subscribers exist, with logged warnings on send failures.
Block::from_filecoin_tipset Refactoring
src/rpc/methods/eth.rs, src/rpc/methods/chain.rs
Block::from_filecoin_tipset signature changed to accept &StateManager instead of Ctx, reducing data cloning; eth block cache default increased from 500 to 1024; RPC handlers (eth_getBlockByHash, eth_getBlockByNumber, new_heads) updated to pass &ctx.state_manager and internal calls use the state manager reference for loading executed tipsets and chain config.
RPC Cache Warming Loop
src/daemon/mod.rs
start_chain_follower_service spawns a background loop after starting ChainFollower that subscribes to validated tipsets and conditionally warms caches by spawning tasks to load tipsets and convert them via Block::from_filecoin_tipset (full and hash modes) when not synced; logs lagged broadcasts and gracefully shuts down on channel closure.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • ChainSafe/forest#6759: Adds subscriber checks before publishing head-change broadcast events, similar pattern to the conditional broadcast gating in this PR.
  • ChainSafe/forest#6402: Prior refactoring of Ethereum block conversion pipeline that modified Block::from_filecoin_tipset and updated RPC call sites.

Suggested reviewers

  • LesnyRumcajs
  • akaladarshi
  • sudo-shashank
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature: introducing a mechanism to prefill RPC caches when new validated tipsets are produced by the chain follower.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hm/prefill-rpc-cache
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch hm/prefill-rpc-cache

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

@hanabi1224 hanabi1224 added the RPC requires calibnet RPC checks to run on CI label May 15, 2026
@hanabi1224 hanabi1224 marked this pull request as ready for review May 15, 2026 14:38
@hanabi1224 hanabi1224 requested a review from a team as a code owner May 15, 2026 14:38
@hanabi1224 hanabi1224 requested review from LesnyRumcajs and akaladarshi and removed request for a team May 15, 2026 14:38
}

fn mark_validated_tipset(&mut self, tipset: FullTipset, is_proposed_head: bool) {
fn mark_validated_tipset(&mut self, tipset: FullTipset, is_proposed_head: bool) -> bool {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's unclear to me what the return bool here denotes. Seems like it should be a Result.

Copy link
Copy Markdown
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/chain_sync/chain_follower.rs (1)

631-637: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Use broadcast::channel instead of Sender::new for Tokio broadcast initialization.

Line 636 uses tokio::sync::broadcast::Sender::new(1024), which is not part of Tokio 1.x's public API. The correct approach is tokio::sync::broadcast::channel(capacity), which returns a tuple of (Sender, Receiver).

Proposed fix
 impl SyncStateMachine {
     pub fn new(
         cs: ChainStore,
         bad_block_cache: Option<BadBlockCache>,
         stateless_mode: bool,
     ) -> Self {
+        let (validated_tipset_broadcast_tx, _) = tokio::sync::broadcast::channel(1024);
         Self {
             cs,
             bad_block_cache,
             tipsets: HashMap::default(),
             stateless_mode,
-            validated_tipset_broadcast_tx: tokio::sync::broadcast::Sender::new(1024),
+            validated_tipset_broadcast_tx,
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/chain_sync/chain_follower.rs` around lines 631 - 637, The initialization
uses tokio::sync::broadcast::Sender::new which doesn't exist in Tokio 1.x;
replace it by calling tokio::sync::broadcast::channel(capacity) and assign the
returned Sender to validated_tipset_broadcast_tx (e.g., let
(validated_tipset_broadcast_tx, _rx) = tokio::sync::broadcast::channel(1024)
before constructing Self), keeping the symbol validated_tipset_broadcast_tx as
the Sender used in the struct and ensuring any needed Receiver is retained or
discarded appropriately.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/daemon/mod.rs`:
- Around line 350-383: The loop prefetches tipsets and calls
crate::rpc::eth::Block::from_filecoin_tipset even when RPC is disabled; guard
this work with the RPC flag by checking enable_rpc (or the equivalent config
passed in) before entering the spawn or before handling Ok(tsk) so that when
enable_rpc is false you skip loading from validated_tipset_rx and the
Block::from_filecoin_tipset calls. Locate the validated_tipset_rx handling in
the service spawn and short-circuit using the enable_rpc boolean (or avoid
spawning the inner tokio::spawn that uses state_manager.chain_index() and
Block::from_filecoin_tipset) to prevent unnecessary CPU/IO when RPC is disabled.
- Around line 360-375: The spawned task currently uses the `?` operator on
`state_manager.chain_index().load_required_tipset(&tsk)?`, which will silently
propagate errors; change this to explicitly handle the Result: call
`state_manager.chain_index().load_required_tipset(&tsk)`, match on Err/Ok, and
on Err log a warning (including `tsk` and the error) and return early from the
task (e.g., via `return`/`anyhow::Ok(())`) so the subsequent
`Block::from_filecoin_tipset` calls only run when the tipset loaded
successfully; reference `state_manager.chain_index().load_required_tipset(&tsk)`
and the cache warmup loop invoking
`crate::rpc::eth::Block::from_filecoin_tipset`.

---

Outside diff comments:
In `@src/chain_sync/chain_follower.rs`:
- Around line 631-637: The initialization uses
tokio::sync::broadcast::Sender::new which doesn't exist in Tokio 1.x; replace it
by calling tokio::sync::broadcast::channel(capacity) and assign the returned
Sender to validated_tipset_broadcast_tx (e.g., let
(validated_tipset_broadcast_tx, _rx) = tokio::sync::broadcast::channel(1024)
before constructing Self), keeping the symbol validated_tipset_broadcast_tx as
the Sender used in the struct and ensuring any needed Receiver is retained or
discarded appropriately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a8bf0584-72dc-4011-844e-4225fbdc22ff

📥 Commits

Reviewing files that changed from the base of the PR and between 08b2a2b and 1f0a3f3.

📒 Files selected for processing (4)
  • src/chain_sync/chain_follower.rs
  • src/daemon/mod.rs
  • src/rpc/methods/chain.rs
  • src/rpc/methods/eth.rs

Comment thread src/daemon/mod.rs
Comment on lines +350 to +383
// Prefill RPC method caches for newly validated tipsets to speed up subsequent RPC calls.
services.spawn(async move {
loop {
match validated_tipset_rx.recv().await {
Ok(_) if !sync_status.read().is_synced() => {
// Skip if the node is catching up to avoid unnecessary work, as the head may be changing rapidly.
continue;
}
Ok(tsk) => {
let state_manager = state_manager.shallow_clone();
tokio::spawn(async move {
let ts = state_manager.chain_index().load_required_tipset(&tsk)?;
for tx_info in
[crate::rpc::eth::TxInfo::Full, crate::rpc::eth::TxInfo::Hash]
{
if let Err(e) = crate::rpc::eth::Block::from_filecoin_tipset(
&state_manager,
ts.shallow_clone(),
tx_info,
)
.await {
warn!("failed to call `Block::from_filecoin_tipset` for cache warmup: {e:#}");
}
}
anyhow::Ok(())
});
}
Err(RecvError::Lagged(n)) => {
warn!("validated tipset broadcast lagged: skipped {n} tipsets")
}
Err(RecvError::Closed) => break Ok(()),
}
}
});
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Skip tipset cache warmup when RPC is disabled.

This loop still does tipset loads and ETH block conversion even when enable_rpc is false, which adds unnecessary CPU/IO load.

💡 Suggested change
-fn start_chain_follower_service(
+fn start_chain_follower_service(
     services: &mut JoinSet<anyhow::Result<()>>,
     chain_follower: ChainFollower,
+    enable_rpc: bool,
 ) {
     let sync_status = chain_follower.sync_status.shallow_clone();
     let state_manager = chain_follower.state_manager.shallow_clone();
     let mut validated_tipset_rx = chain_follower.subscribe_validated_tipset();
     services.spawn(async move { chain_follower.run().await });
+    if !enable_rpc {
+        return;
+    }
     // Prefill RPC method caches for newly validated tipsets to speed up subsequent RPC calls.
     services.spawn(async move {
         loop {
// outside this hunk (call site):
start_chain_follower_service(&mut services, chain_follower, config.client.enable_rpc);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/daemon/mod.rs` around lines 350 - 383, The loop prefetches tipsets and
calls crate::rpc::eth::Block::from_filecoin_tipset even when RPC is disabled;
guard this work with the RPC flag by checking enable_rpc (or the equivalent
config passed in) before entering the spawn or before handling Ok(tsk) so that
when enable_rpc is false you skip loading from validated_tipset_rx and the
Block::from_filecoin_tipset calls. Locate the validated_tipset_rx handling in
the service spawn and short-circuit using the enable_rpc boolean (or avoid
spawning the inner tokio::spawn that uses state_manager.chain_index() and
Block::from_filecoin_tipset) to prevent unnecessary CPU/IO when RPC is disabled.

Comment thread src/daemon/mod.rs
Comment on lines +360 to +375
tokio::spawn(async move {
let ts = state_manager.chain_index().load_required_tipset(&tsk)?;
for tx_info in
[crate::rpc::eth::TxInfo::Full, crate::rpc::eth::TxInfo::Hash]
{
if let Err(e) = crate::rpc::eth::Block::from_filecoin_tipset(
&state_manager,
ts.shallow_clone(),
tx_info,
)
.await {
warn!("failed to call `Block::from_filecoin_tipset` for cache warmup: {e:#}");
}
}
anyhow::Ok(())
});
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify this path currently has a fallible load with `?` and no dedicated load-failure log.
rg -nC3 'load_required_tipset\(&tsk\)\?' src/daemon/mod.rs
rg -nC3 'failed to call `Block::from_filecoin_tipset` for cache warmup' src/daemon/mod.rs
rg -nC3 'tokio::spawn\(async move \{' src/daemon/mod.rs

Repository: ChainSafe/forest

Length of output: 1236


Add error logging for tipset load failures in cache warmup task.

The load_required_tipset(&tsk)? call on line 361 uses the ? operator in a spawned task. If this fails, the error propagates silently without logging, making debugging difficult. Explicit error handling with a warning message is needed to maintain visibility into cache warmup failures.

Suggested change
                    tokio::spawn(async move {
-                        let ts = state_manager.chain_index().load_required_tipset(&tsk)?;
+                        let ts = match state_manager.chain_index().load_required_tipset(&tsk) {
+                            Ok(ts) => ts,
+                            Err(e) => {
+                                warn!(
+                                    "failed to load tipset {tsk} for RPC cache warmup: {e:#}"
+                                );
+                                return;
+                            }
+                        };
                         for tx_info in
                             [crate::rpc::eth::TxInfo::Full, crate::rpc::eth::TxInfo::Hash]
                         {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tokio::spawn(async move {
let ts = state_manager.chain_index().load_required_tipset(&tsk)?;
for tx_info in
[crate::rpc::eth::TxInfo::Full, crate::rpc::eth::TxInfo::Hash]
{
if let Err(e) = crate::rpc::eth::Block::from_filecoin_tipset(
&state_manager,
ts.shallow_clone(),
tx_info,
)
.await {
warn!("failed to call `Block::from_filecoin_tipset` for cache warmup: {e:#}");
}
}
anyhow::Ok(())
});
tokio::spawn(async move {
let ts = match state_manager.chain_index().load_required_tipset(&tsk) {
Ok(ts) => ts,
Err(e) => {
warn!(
"failed to load tipset {tsk} for RPC cache warmup: {e:#}"
);
return;
}
};
for tx_info in
[crate::rpc::eth::TxInfo::Full, crate::rpc::eth::TxInfo::Hash]
{
if let Err(e) = crate::rpc::eth::Block::from_filecoin_tipset(
&state_manager,
ts.shallow_clone(),
tx_info,
)
.await {
warn!("failed to call `Block::from_filecoin_tipset` for cache warmup: {e:#}");
}
}
anyhow::Ok(())
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/daemon/mod.rs` around lines 360 - 375, The spawned task currently uses
the `?` operator on `state_manager.chain_index().load_required_tipset(&tsk)?`,
which will silently propagate errors; change this to explicitly handle the
Result: call `state_manager.chain_index().load_required_tipset(&tsk)`, match on
Err/Ok, and on Err log a warning (including `tsk` and the error) and return
early from the task (e.g., via `return`/`anyhow::Ok(())`) so the subsequent
`Block::from_filecoin_tipset` calls only run when the tipset loaded
successfully; reference `state_manager.chain_index().load_required_tipset(&tsk)`
and the cache warmup loop invoking
`crate::rpc::eth::Block::from_filecoin_tipset`.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 15, 2026

Codecov Report

❌ Patch coverage is 13.20755% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.31%. Comparing base (08b2a2b) to head (1f0a3f3).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/daemon/mod.rs 0.00% 26 Missing ⚠️
src/chain_sync/chain_follower.rs 15.78% 16 Missing ⚠️
src/rpc/methods/eth.rs 57.14% 1 Missing and 2 partials ⚠️
src/rpc/methods/chain.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
Files with missing lines Coverage Δ
src/rpc/methods/chain.rs 55.65% <0.00%> (ø)
src/rpc/methods/eth.rs 65.38% <57.14%> (ø)
src/chain_sync/chain_follower.rs 36.26% <15.78%> (-0.53%) ⬇️
src/daemon/mod.rs 27.34% <0.00%> (-1.21%) ⬇️

... and 9 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 08b2a2b...1f0a3f3. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@LesnyRumcajs
Copy link
Copy Markdown
Member

no green checkmark, no review

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.

2 participants