From fe4f628e60eca4e87601ac64001543b78cc10d96 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 17 Mar 2026 03:02:57 +0800 Subject: [PATCH 1/2] fix: optimize `tipset_by_height` --- src/blocks/checkpoints.rs | 93 ++++++++++++++++++++++++++ src/blocks/mod.rs | 1 + src/blocks/tipset.rs | 28 +++----- src/chain/store/chain_store.rs | 31 ++++++++- src/dev/subcommands/state_cmd.rs | 19 ++---- src/rpc/methods/chain.rs | 37 +++++----- src/rpc/methods/eth.rs | 9 +-- src/rpc/methods/eth/filter/mod.rs | 4 +- src/rpc/methods/eth/tipset_resolver.rs | 8 +-- src/rpc/methods/f3.rs | 8 +-- src/rpc/methods/state.rs | 12 ++-- src/state_manager/mod.rs | 12 ++-- src/tool/subcommands/index_cmd.rs | 6 +- 13 files changed, 178 insertions(+), 90 deletions(-) create mode 100644 src/blocks/checkpoints.rs diff --git a/src/blocks/checkpoints.rs b/src/blocks/checkpoints.rs new file mode 100644 index 000000000000..64333ea44ecb --- /dev/null +++ b/src/blocks/checkpoints.rs @@ -0,0 +1,93 @@ +// Copyright 2019-2026 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +//! Provides utilities for efficiently locating the genesis block and known checkpoints +//! in the Filecoin blockchain by leveraging a list of precomputed, hash-chained block CIDs. +//! This avoids scanning millions of epochs, significantly speeding up chain traversal. + +use crate::{ + blocks::{CachingBlockHeader, Tipset}, + networks::NetworkChain, + shim::clock::ChainEpoch, +}; +use ahash::HashMap; +use anyhow::Context as _; +use cid::Cid; +use fvm_ipld_blockstore::Blockstore; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use serde_with::{DisplayFromStr, serde_as}; +use std::sync::{LazyLock, OnceLock}; + +/// Holds mappings from chain epochs to block CIDs for each network. +#[serde_as] +#[derive(Serialize, Deserialize)] +pub struct KnownBlocks { + #[serde_as(as = "HashMap<_, DisplayFromStr>")] + pub calibnet: HashMap, + #[serde_as(as = "HashMap<_, DisplayFromStr>")] + pub mainnet: HashMap, +} + +/// Lazily loaded static instance of `KnownBlocks` from YAML. +/// Caches (`OnceLock`) are used to avoid recomputing known tipsets. +pub static KNOWN_BLOCKS: LazyLock = LazyLock::new(|| { + serde_yaml::from_str(include_str!("../../build/known_blocks.yaml")).expect("infallible") +}); + +/// Returns a cached, ascending-epoch list of known [`Tipset`]s for the given network. +pub fn known_tipsets( + bs: &impl Blockstore, + network: &NetworkChain, +) -> anyhow::Result<&'static Vec> { + static CACHE_CALIBNET: OnceLock> = OnceLock::new(); + static CACHE_MAINNET: OnceLock> = OnceLock::new(); + let (cache, known_blocks) = match network { + NetworkChain::Calibnet => (&CACHE_CALIBNET, &KNOWN_BLOCKS.calibnet), + NetworkChain::Mainnet => (&CACHE_MAINNET, &KNOWN_BLOCKS.mainnet), + _ => anyhow::bail!("unsupported network {network}"), + }; + if let Some(v) = cache.get() { + Ok(v) + } else { + let tipsets = known_blocks_to_known_tipsets(bs, known_blocks)?; + _ = cache.set(tipsets); + cache.get().context("infallible") + } +} + +fn known_blocks_to_known_tipsets( + bs: &impl Blockstore, + blocks: &HashMap, +) -> anyhow::Result> { + let mut tipsets: Vec = blocks + .values() + .map(|&b| block_cid_to_required_parent_tipset(bs, b)) + .try_collect()?; + tipsets.sort_by_key(|ts| ts.epoch()); + Ok(tipsets) +} + +fn block_cid_to_parent_tipset(bs: &impl Blockstore, block: Cid) -> anyhow::Result> { + if let Some(block) = CachingBlockHeader::load(bs, block)? { + Tipset::load(bs, &block.parents) + } else { + Ok(None) + } +} + +fn block_cid_to_required_parent_tipset(bs: &impl Blockstore, block: Cid) -> anyhow::Result { + block_cid_to_parent_tipset(bs, block)? + .with_context(|| format!("failed to load parent tipset of block {block}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_known_blocks() { + assert!(!KNOWN_BLOCKS.calibnet.is_empty()); + assert!(!KNOWN_BLOCKS.mainnet.is_empty()); + } +} diff --git a/src/blocks/mod.rs b/src/blocks/mod.rs index c3211be3bc02..9186beab9eec 100644 --- a/src/blocks/mod.rs +++ b/src/blocks/mod.rs @@ -6,6 +6,7 @@ use thiserror::Error; mod block; #[cfg(test)] mod chain4u; +pub mod checkpoints; mod election_proof; mod gossip_block; mod header; diff --git a/src/blocks/tipset.rs b/src/blocks/tipset.rs index ecda41502ddf..e7c2e910ee1d 100644 --- a/src/blocks/tipset.rs +++ b/src/blocks/tipset.rs @@ -19,7 +19,6 @@ use crate::{ multihash::MultihashCode, }, }; -use ahash::HashMap; use anyhow::Context as _; use cid::Cid; use fvm_ipld_blockstore::Blockstore; @@ -412,29 +411,20 @@ impl Tipset { /// Fetch the genesis block header for a given tipset. pub fn genesis(&self, store: &impl Blockstore) -> anyhow::Result { - // Scanning through millions of epochs to find the genesis is quite - // slow. Let's use a list of known blocks to short-circuit the search. - // The blocks are hash-chained together and known blocks are guaranteed - // to have a known genesis. - #[derive(Serialize, Deserialize)] - struct KnownHeaders { - calibnet: HashMap, - mainnet: HashMap, - } - - static KNOWN_HEADERS: OnceLock = OnceLock::new(); - let headers = KNOWN_HEADERS.get_or_init(|| { - serde_yaml::from_str(include_str!("../../build/known_blocks.yaml")).unwrap() - }); - for tipset in self.clone().chain(store) { // Search for known calibnet and mainnet blocks for (genesis_cid, known_blocks) in [ - (*calibnet::GENESIS_CID, &headers.calibnet), - (*mainnet::GENESIS_CID, &headers.mainnet), + ( + *calibnet::GENESIS_CID, + &super::checkpoints::KNOWN_BLOCKS.calibnet, + ), + ( + *mainnet::GENESIS_CID, + &super::checkpoints::KNOWN_BLOCKS.mainnet, + ), ] { if let Some(known_block_cid) = known_blocks.get(&tipset.epoch()) - && known_block_cid == &tipset.min_ticket_block().cid().to_string() + && known_block_cid == tipset.min_ticket_block().cid() { return store .get_cbor(&genesis_cid)? diff --git a/src/chain/store/chain_store.rs b/src/chain/store/chain_store.rs index 84086620907b..9cb81ebb929b 100644 --- a/src/chain/store/chain_store.rs +++ b/src/chain/store/chain_store.rs @@ -6,7 +6,6 @@ use super::{ index::{ChainIndex, ResolveNullTipset}, tipset_tracker::TipsetTracker, }; -use crate::libp2p_bitswap::{BitswapStoreRead, BitswapStoreReadWrite}; use crate::message::{ChainMessage, Message as MessageTrait, SignedMessage}; use crate::networks::{ChainConfig, Height}; use crate::rpc::eth::{eth_tx_from_signed_eth_message, types::EthHash}; @@ -17,6 +16,10 @@ use crate::shim::{ }; use crate::state_manager::StateOutput; use crate::utils::db::{BlockstoreExt, CborStoreExt}; +use crate::{ + blocks::checkpoints::known_tipsets, + libp2p_bitswap::{BitswapStoreRead, BitswapStoreReadWrite}, +}; use crate::{ blocks::{CachingBlockHeader, Tipset, TipsetKey, TxMeta}, db::HeaviestTipsetKeyProvider, @@ -240,6 +243,32 @@ where Tipset::from(self.genesis_block_header()) } + /// Find tipset at epoch `to` in the chain + pub fn tipset_by_height( + &self, + to: ChainEpoch, + from: Option, + resolve: ResolveNullTipset, + ) -> Result { + let best_known_from = if let Ok(known_tipsets) = + known_tipsets(self.blockstore(), &self.chain_config().network) + && let Some(ts) = known_tipsets.iter().find(|ts| ts.epoch() > to).cloned() + { + Some(ts) + } else { + None + }; + let from = match (from, best_known_from) { + // prefer `b` when `b` is closer to `to` + (Some(a), Some(b)) if b.epoch() < a.epoch() => b, + // prefer `a` when presents + (Some(a), _) => a, + (None, Some(b)) => b, + // fallback to chain head + (None, None) => self.heaviest_tipset(), + }; + self.chain_index().tipset_by_height(to, from, resolve) + } /// Subscribes head changes. pub fn subscribe_head_changes(&self) -> broadcast::Receiver { self.head_changes_tx.subscribe() diff --git a/src/dev/subcommands/state_cmd.rs b/src/dev/subcommands/state_cmd.rs index b57f58084766..09ed04d3a7e0 100644 --- a/src/dev/subcommands/state_cmd.rs +++ b/src/dev/subcommands/state_cmd.rs @@ -85,16 +85,9 @@ impl ComputeCommand { let (ts, ts_next) = { // We don't want to track all entries that are visited by `tipset_by_height` db.pause_tracking(); - let ts = chain_store.chain_index().tipset_by_height( - epoch, - chain_store.heaviest_tipset(), - ResolveNullTipset::TakeOlder, - )?; - let ts_next = chain_store.chain_index().tipset_by_height( - epoch + 1, - chain_store.heaviest_tipset(), - ResolveNullTipset::TakeNewer, - )?; + let ts = chain_store.tipset_by_height(epoch, None, ResolveNullTipset::TakeOlder)?; + let ts_next = + chain_store.tipset_by_height(epoch + 1, None, ResolveNullTipset::TakeNewer)?; db.resume_tracking(); SettingsStoreExt::write_obj( &db.tracker, @@ -215,11 +208,7 @@ impl ValidateCommand { let ts = { // We don't want to track all entries that are visited by `tipset_by_height` db.pause_tracking(); - let ts = chain_store.chain_index().tipset_by_height( - epoch, - chain_store.heaviest_tipset(), - ResolveNullTipset::TakeOlder, - )?; + let ts = chain_store.tipset_by_height(epoch, None, ResolveNullTipset::TakeOlder)?; db.resume_tracking(); SettingsStoreExt::write_obj(&db.tracker, crate::db::setting_keys::HEAD_KEY, ts.key())?; // Only track the desired tipset diff --git a/src/rpc/methods/chain.rs b/src/rpc/methods/chain.rs index 35b9dfa1eac9..f90e55e77cdf 100644 --- a/src/rpc/methods/chain.rs +++ b/src/rpc/methods/chain.rs @@ -178,9 +178,9 @@ impl RpcMethod<0> for ChainGetFinalizedTipset { Err(_) => { // fallback to ec finality tracing::warn!("F3 finalization unavailable, falling back to EC finality"); - let ec_tipset = ctx.chain_index().tipset_by_height( + let ec_tipset = ctx.chain_store().tipset_by_height( ec_finality_epoch, - head, + None, ResolveNullTipset::TakeOlder, )?; Ok(ec_tipset) @@ -438,8 +438,8 @@ impl RpcMethod<1> for ForestChainExport { let head = ctx.chain_store().load_required_tipset_or_heaviest(&tsk)?; let start_ts = - ctx.chain_index() - .tipset_by_height(epoch, head, ResolveNullTipset::TakeOlder)?; + ctx.chain_store() + .tipset_by_height(epoch, Some(head), ResolveNullTipset::TakeOlder)?; let options = Some(ExportOptions { skip_checksum, @@ -629,10 +629,9 @@ impl RpcMethod<1> for ForestChainExportDiff { ); } - let head = ctx.chain_store().heaviest_tipset(); let start_ts = - ctx.chain_index() - .tipset_by_height(from, head, ResolveNullTipset::TakeOlder)?; + ctx.chain_store() + .tipset_by_height(from, None, ResolveNullTipset::TakeOlder)?; crate::tool::subcommands::archive_cmd::do_export( &ctx.store_owned(), @@ -929,9 +928,9 @@ impl RpcMethod<2> for ChainGetTipSetByHeight { let ts = ctx .chain_store() .load_required_tipset_or_heaviest(&tipset_key)?; - let tss = ctx - .chain_index() - .tipset_by_height(height, ts, ResolveNullTipset::TakeOlder)?; + let tss = + ctx.chain_store() + .tipset_by_height(height, Some(ts), ResolveNullTipset::TakeOlder)?; Ok(tss) } } @@ -959,9 +958,9 @@ impl RpcMethod<2> for ChainGetTipSetAfterHeight { let ts = ctx .chain_store() .load_required_tipset_or_heaviest(&tipset_key)?; - let tss = ctx - .chain_index() - .tipset_by_height(height, ts, ResolveNullTipset::TakeNewer)?; + let tss = + ctx.chain_store() + .tipset_by_height(height, Some(ts), ResolveNullTipset::TakeNewer)?; Ok(tss) } } @@ -1102,9 +1101,9 @@ impl ChainGetTipSetV2 { if finalized.epoch() >= safe_height { Ok(finalized) } else { - Ok(ctx.chain_index().tipset_by_height( + Ok(ctx.chain_store().tipset_by_height( safe_height, - head, + None, ResolveNullTipset::TakeOlder, )?) } @@ -1140,9 +1139,9 @@ impl ChainGetTipSetV2 { pub fn get_ec_finalized_tipset(ctx: &Ctx) -> anyhow::Result { let head = ctx.chain_store().heaviest_tipset(); let ec_finality_epoch = (head.epoch() - ctx.chain_config().policy.chain_finality).max(0); - Ok(ctx.chain_index().tipset_by_height( + Ok(ctx.chain_store().tipset_by_height( ec_finality_epoch, - head, + None, ResolveNullTipset::TakeOlder, )?) } @@ -1160,9 +1159,9 @@ impl ChainGetTipSetV2 { // Get tipset by height. if let Some(height) = &selector.height { let anchor = Self::get_tipset_by_anchor(ctx, height.anchor.as_ref()).await?; - let ts = ctx.chain_index().tipset_by_height( + let ts = ctx.chain_store().tipset_by_height( height.at, - anchor, + Some(anchor), height.resolve_null_tipset_policy(), )?; return Ok(ts); diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index a2f7987cc8ad..e665201a04e7 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -932,9 +932,7 @@ fn resolve_block_number_tipset( if height > head.epoch() - 1 { bail!("requested a future epoch (beyond \"latest\")"); } - Ok(chain - .chain_index() - .tipset_by_height(height, head, resolve)?) + Ok(chain.tipset_by_height(height, None, resolve)?) } fn resolve_block_hash_tipset( @@ -947,10 +945,7 @@ fn resolve_block_hash_tipset( // verify that the tipset is in the canonical chain if require_canonical { // walk up the current chain (our head) until we reach ts.epoch() - let walk_ts = - chain - .chain_index() - .tipset_by_height(ts.epoch(), chain.heaviest_tipset(), resolve)?; + let walk_ts = chain.tipset_by_height(ts.epoch(), None, resolve)?; // verify that it equals the expected tipset if walk_ts != ts { bail!("tipset is not canonical"); diff --git a/src/rpc/methods/eth/filter/mod.rs b/src/rpc/methods/eth/filter/mod.rs index dec8bec0c854..22cee0a0f6bd 100644 --- a/src/rpc/methods/eth/filter/mod.rs +++ b/src/rpc/methods/eth/filter/mod.rs @@ -408,9 +408,9 @@ impl EthEventHandler { *range.end() }; - let max_tipset = ctx.chain_index().tipset_by_height( + let max_tipset = ctx.chain_store().tipset_by_height( max_height, - ctx.chain_store().heaviest_tipset(), + None, ResolveNullTipset::TakeOlder, )?; for tipset in max_tipset diff --git a/src/rpc/methods/eth/tipset_resolver.rs b/src/rpc/methods/eth/tipset_resolver.rs index bccea571b9df..5bef066000c8 100644 --- a/src/rpc/methods/eth/tipset_resolver.rs +++ b/src/rpc/methods/eth/tipset_resolver.rs @@ -157,9 +157,9 @@ where pub fn get_ec_safe_tipset(&self) -> anyhow::Result { let head = self.ctx.chain_store().heaviest_tipset(); let safe_height = (head.epoch() - SAFE_HEIGHT_DISTANCE).max(0); - Ok(self.ctx.chain_index().tipset_by_height( + Ok(self.ctx.chain_store().tipset_by_height( safe_height, - head, + None, ResolveNullTipset::TakeOlder, )?) } @@ -171,9 +171,9 @@ where let head = self.ctx.chain_store().heaviest_tipset(); let ec_finality_epoch = (head.epoch() - self.ctx.chain_config().policy.chain_finality).max(0); - Ok(self.ctx.chain_index().tipset_by_height( + Ok(self.ctx.chain_store().tipset_by_height( ec_finality_epoch, - head, + None, ResolveNullTipset::TakeOlder, )?) } diff --git a/src/rpc/methods/f3.rs b/src/rpc/methods/f3.rs index becdeeb5d892..b6925e8490d0 100644 --- a/src/rpc/methods/f3.rs +++ b/src/rpc/methods/f3.rs @@ -87,11 +87,9 @@ impl RpcMethod<1> for GetTipsetByEpoch { (epoch,): Self::Params, _: &http::Extensions, ) -> Result { - let ts = ctx.chain_index().tipset_by_height( - epoch, - ctx.chain_store().heaviest_tipset(), - ResolveNullTipset::TakeOlder, - )?; + let ts = ctx + .chain_store() + .tipset_by_height(epoch, None, ResolveNullTipset::TakeOlder)?; Ok(ts.into()) } } diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index e55384ae7ce9..0387b8634240 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -1536,19 +1536,17 @@ impl RpcMethod<2> for ForestStateCompute { ) -> Result { let n_epochs = n_epochs.map(|n| n.get()).unwrap_or(1) as ChainEpoch; let to_epoch = from_epoch + n_epochs - 1; - let to_ts = ctx.chain_index().tipset_by_height( - to_epoch, - ctx.chain_store().heaviest_tipset(), - ResolveNullTipset::TakeOlder, - )?; + let to_ts = + ctx.chain_store() + .tipset_by_height(to_epoch, None, ResolveNullTipset::TakeOlder)?; let from_ts = if from_epoch >= to_ts.epoch() { // When `from_epoch` is a null epoch or `n_epochs` is 1, // `to_ts.epoch()` could be less than or equal to `from_epoch` to_ts.clone() } else { - ctx.chain_index().tipset_by_height( + ctx.chain_store().tipset_by_height( from_epoch, - to_ts.clone(), + Some(to_ts.clone()), ResolveNullTipset::TakeOlder, )? }; diff --git a/src/state_manager/mod.rs b/src/state_manager/mod.rs index b8ac499e75ae..6ecf8beda09c 100644 --- a/src/state_manager/mod.rs +++ b/src/state_manager/mod.rs @@ -230,9 +230,9 @@ where let bundle_metadata = state.get_actor_bundle_metadata()?; if expected_bundle_metadata != bundle_metadata { let current_epoch = head.epoch(); - let target_head = self.chain_index().tipset_by_height( + let target_head = self.chain_store().tipset_by_height( (expected_height_info.epoch - 1).max(0), - head, + None, ResolveNullTipset::TakeOlder, )?; let target_epoch = target_head.epoch(); @@ -1604,8 +1604,8 @@ where let heaviest = self.heaviest_tipset(); let heaviest_epoch = heaviest.epoch(); let end = self - .chain_index() - .tipset_by_height(*epochs.end(), heaviest, ResolveNullTipset::TakeOlder) + .chain_store() + .tipset_by_height(*epochs.end(), None, ResolveNullTipset::TakeOlder) .with_context(|| { format!( "couldn't get a tipset at height {} behind heaviest tipset at height {heaviest_epoch}", @@ -1788,8 +1788,8 @@ where // Check if the next tipset has the same parent if let Ok(next_tipset) = - self.chain_index() - .tipset_by_height(next_epoch, heaviest, ResolveNullTipset::TakeNewer) + self.chain_store() + .tipset_by_height(next_epoch, None, ResolveNullTipset::TakeNewer) { // verify that the parent of the `next_tipset` is the same as the current tipset if !next_tipset.parents().eq(tipset.key()) { diff --git a/src/tool/subcommands/index_cmd.rs b/src/tool/subcommands/index_cmd.rs index 910e79b61abe..0d431a42b37c 100644 --- a/src/tool/subcommands/index_cmd.rs +++ b/src/tool/subcommands/index_cmd.rs @@ -101,11 +101,7 @@ impl IndexCommands { // ensure from epoch is not greater than head epoch. This can happen if the // assumed head is actually a null tipset. let from = std::cmp::min(*from, head_ts.epoch()); - chain_store.chain_index().tipset_by_height( - from, - head_ts, - ResolveNullTipset::TakeOlder, - )? + chain_store.tipset_by_height(from, None, ResolveNullTipset::TakeOlder)? } else { head_ts }; From 56878120c300b49b6ac10d0592eff2564b51ab0c Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Wed, 18 Mar 2026 19:06:57 +0800 Subject: [PATCH 2/2] more checks --- src/chain/store/chain_store.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/chain/store/chain_store.rs b/src/chain/store/chain_store.rs index 7d47b40ae2e0..8aa4e610bee9 100644 --- a/src/chain/store/chain_store.rs +++ b/src/chain/store/chain_store.rs @@ -250,6 +250,15 @@ where from: Option, resolve: ResolveNullTipset, ) -> Result { + let head = self.heaviest_tipset(); + // Fail fast when `to` is too large + if to > head.epoch() { + return Err(Error::Other(format!( + "looking for tipset with height greater than the current chain head, req: {to}, head: {}", + head.epoch() + ))); + } + let best_known_from = if let Ok(known_tipsets) = known_tipsets(self.blockstore(), &self.chain_config().network) && let Some(ts) = known_tipsets.iter().find(|ts| ts.epoch() > to).cloned() @@ -265,7 +274,7 @@ where (Some(a), _) => a, (None, Some(b)) => b, // fallback to chain head - (None, None) => self.heaviest_tipset(), + (None, None) => head, }; self.chain_index().tipset_by_height(to, from, resolve) }