From f0fdbf641a257e2033b5f3a09e32f64d04ba69d7 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Fri, 15 Aug 2025 22:13:58 +0800 Subject: [PATCH 01/14] fix: add cache to improve tipset_by_height performance --- src/chain/store/index.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/chain/store/index.rs b/src/chain/store/index.rs index cd375b19e7ef..6e601ec5015c 100644 --- a/src/chain/store/index.rs +++ b/src/chain/store/index.rs @@ -1,6 +1,7 @@ // Copyright 2019-2025 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +use std::sync::LazyLock; use std::{num::NonZeroUsize, sync::Arc}; use crate::beacon::{BeaconEntry, IGNORE_DRAND_VAR}; @@ -13,6 +14,7 @@ use crate::utils::misc::env::is_env_truthy; use fvm_ipld_blockstore::Blockstore; use itertools::Itertools; use nonzero_ext::nonzero; +use num::Integer; const DEFAULT_TIPSET_CACHE_SIZE: NonZeroUsize = nonzero!(131072_usize); @@ -120,6 +122,28 @@ impl ChainIndex { from: Arc, resolve: ResolveNullTipset, ) -> Result, Error> { + const CHECKPOINT_INTERVAL: ChainEpoch = 1000; + static CACHE: LazyLock>> = + LazyLock::new(|| { + SizeTrackingLruCache::new_with_default_metrics_registry( + "tipset_by_height".into(), + 4096.try_into().expect("infallible"), + ) + }); + + fn next_checkpoint(epoch: ChainEpoch) -> ChainEpoch { + let m = epoch.mod_floor(&CHECKPOINT_INTERVAL); + if m == 0 { + epoch + } else { + epoch - m + CHECKPOINT_INTERVAL + } + } + + let checkpoint_from_epoch = next_checkpoint(to); + let checkpoint_from = CACHE.get_cloned(&checkpoint_from_epoch); + let from = checkpoint_from.unwrap_or(from); + if to == 0 { return Ok(Arc::new(Tipset::from(from.genesis(&self.db)?))); } @@ -131,6 +155,10 @@ impl ChainIndex { } for (child, parent) in self.chain(from).tuple_windows() { + if child.epoch() % CHECKPOINT_INTERVAL == 0 { + CACHE.push(child.epoch(), child.clone()); + } + if to == child.epoch() { return Ok(child); } From 0500263d7493dfc280dcc8d0061691bfb7b0c496 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Fri, 15 Aug 2025 23:42:53 +0800 Subject: [PATCH 02/14] fix --- src/chain/store/chain_store.rs | 13 ++++++++++--- src/chain/store/index.rs | 7 +------ src/chain_sync/chain_follower.rs | 2 +- src/chain_sync/mod.rs | 2 +- src/chain_sync/validation.rs | 3 +-- src/libp2p/chain_exchange/provider.rs | 2 +- src/rpc/methods/chain.rs | 3 +-- src/rpc/methods/f3.rs | 2 +- src/rpc/methods/state.rs | 11 +++++++++++ 9 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/chain/store/chain_store.rs b/src/chain/store/chain_store.rs index 34b7636ce3d5..6a80e2174131 100644 --- a/src/chain/store/chain_store.rs +++ b/src/chain/store/chain_store.rs @@ -467,7 +467,7 @@ pub fn block_messages( where DB: Blockstore, { - let (bls_cids, secpk_cids) = read_msg_cids(db, &bh.messages)?; + let (bls_cids, secpk_cids) = read_msg_cids(db, bh)?; let bls_msgs: Vec = messages_from_cids(db, &bls_cids)?; let secp_msgs: Vec = messages_from_cids(db, &secpk_cids)?; @@ -491,17 +491,23 @@ where } /// Returns a tuple of CIDs for both unsigned and signed messages -pub fn read_msg_cids(db: &DB, msg_cid: &Cid) -> Result<(Vec, Vec), Error> +pub fn read_msg_cids( + db: &DB, + block_header: &CachingBlockHeader, +) -> Result<(Vec, Vec), Error> where DB: Blockstore, { + let msg_cid = &block_header.messages; if let Some(roots) = db.get_cbor::(msg_cid)? { let bls_cids = read_amt_cids(db, &roots.bls_message_root)?; let secpk_cids = read_amt_cids(db, &roots.secp_message_root)?; Ok((bls_cids, secpk_cids)) } else { Err(Error::UndefinedKey(format!( - "no msg root with cid {msg_cid}" + "no msg root with cid {msg_cid} at epoch {} in block {}", + block_header.epoch, + block_header.cid(), ))) } } @@ -635,6 +641,7 @@ where // message to get all messages for block_header into a single iterator let mut get_message_for_block_header = |b: &CachingBlockHeader| -> Result, Error> { + tracing::info!("block_messages({}): {}", b.epoch, b.cid()); let (unsigned, signed) = block_messages(&db, b)?; let mut messages = Vec::with_capacity(unsigned.len() + signed.len()); let unsigned_box = unsigned.into_iter().map(ChainMessage::Unsigned); diff --git a/src/chain/store/index.rs b/src/chain/store/index.rs index 6e601ec5015c..4d059c3ad20c 100644 --- a/src/chain/store/index.rs +++ b/src/chain/store/index.rs @@ -132,12 +132,7 @@ impl ChainIndex { }); fn next_checkpoint(epoch: ChainEpoch) -> ChainEpoch { - let m = epoch.mod_floor(&CHECKPOINT_INTERVAL); - if m == 0 { - epoch - } else { - epoch - m + CHECKPOINT_INTERVAL - } + epoch - epoch.mod_floor(&CHECKPOINT_INTERVAL) + CHECKPOINT_INTERVAL } let checkpoint_from_epoch = next_checkpoint(to); diff --git a/src/chain_sync/chain_follower.rs b/src/chain_sync/chain_follower.rs index ffa8a347b31a..c4022f850059 100644 --- a/src/chain_sync/chain_follower.rs +++ b/src/chain_sync/chain_follower.rs @@ -431,7 +431,7 @@ fn handle_peer_disconnected_event( network.peer_manager().unmark_peer_bad(&peer_id); } -async fn get_full_tipset( +pub async fn get_full_tipset( network: SyncNetworkContext, chain_store: Arc>, peer_id: Option, diff --git a/src/chain_sync/mod.rs b/src/chain_sync/mod.rs index 7647168c2268..2d7d52c3261e 100644 --- a/src/chain_sync/mod.rs +++ b/src/chain_sync/mod.rs @@ -13,7 +13,7 @@ mod validation; pub use self::{ bad_block_cache::BadBlockCache, - chain_follower::ChainFollower, + chain_follower::{ChainFollower, get_full_tipset}, chain_muxer::SyncConfig, consensus::collect_errs, sync_status::{ForkSyncInfo, ForkSyncStage, NodeSyncStatus, SyncStatusReport}, diff --git a/src/chain_sync/validation.rs b/src/chain_sync/validation.rs index b05100956101..560d84775099 100644 --- a/src/chain_sync/validation.rs +++ b/src/chain_sync/validation.rs @@ -73,7 +73,7 @@ impl TipsetValidator<'_> { // matches the mst root in the block header 2. Ensuring it has not // previously been seen in the bad blocks cache for block in self.0.blocks() { - self.validate_msg_root(&chainstore.db, block)?; + Self::validate_msg_root(&chainstore.db, block)?; if let Some(bad_block_cache) = bad_block_cache && bad_block_cache.peek(block.cid()).is_some() { @@ -104,7 +104,6 @@ impl TipsetValidator<'_> { } pub fn validate_msg_root( - &self, blockstore: &DB, block: &Block, ) -> Result<(), TipsetValidationError> { diff --git a/src/libp2p/chain_exchange/provider.rs b/src/libp2p/chain_exchange/provider.rs index 8d01e6c041f4..aa8329b3821f 100644 --- a/src/libp2p/chain_exchange/provider.rs +++ b/src/libp2p/chain_exchange/provider.rs @@ -96,7 +96,7 @@ where let mut secp_msg_includes: Vec> = vec![]; for block_header in tipset.block_headers().iter() { - let (bls_cids, secp_cids) = crate::chain::read_msg_cids(db, &block_header.messages)?; + let (bls_cids, secp_cids) = crate::chain::read_msg_cids(db, block_header)?; let mut bls_include = Vec::with_capacity(bls_cids.len()); for bls_cid in bls_cids.into_iter() { diff --git a/src/rpc/methods/chain.rs b/src/rpc/methods/chain.rs index 48558b345ab3..68a671718357 100644 --- a/src/rpc/methods/chain.rs +++ b/src/rpc/methods/chain.rs @@ -554,8 +554,7 @@ impl RpcMethod<1> for ChainGetBlockMessages { (block_cid,): Self::Params, ) -> Result { let blk: CachingBlockHeader = ctx.store().get_cbor_required(&block_cid)?; - let blk_msgs = &blk.messages; - let (unsigned_cids, signed_cids) = crate::chain::read_msg_cids(ctx.store(), blk_msgs)?; + let (unsigned_cids, signed_cids) = crate::chain::read_msg_cids(ctx.store(), &blk)?; let (bls_msg, secp_msg) = crate::chain::block_messages_from_cids(ctx.store(), &unsigned_cids, &signed_cids)?; let cids = unsigned_cids.into_iter().chain(signed_cids).collect(); diff --git a/src/rpc/methods/f3.rs b/src/rpc/methods/f3.rs index 16e24272b357..2a7bc1228ff3 100644 --- a/src/rpc/methods/f3.rs +++ b/src/rpc/methods/f3.rs @@ -165,7 +165,7 @@ impl GetPowerTable { const BLOCKSTORE_CACHE_CAP: usize = 65536; static BLOCKSTORE_CACHE: LazyLock = LazyLock::new(|| { LruBlockstoreReadCache::new_with_default_metrics_registry( - "get_powertable_cache".into(), + "get_powertable".into(), BLOCKSTORE_CACHE_CAP.try_into().expect("Infallible"), ) }); diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index d2a4a1fc482f..13e1c50447b5 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -6,6 +6,7 @@ pub use types::*; use crate::blocks::{Tipset, TipsetKey}; use crate::chain::index::ResolveNullTipset; +use crate::chain_sync::TipsetValidator; use crate::cid_collections::CidHashSet; use crate::eth::EthChainId; use crate::interpreter::{MessageCallbackCtx, VMTrace}; @@ -1437,6 +1438,16 @@ impl RpcMethod<1> for ForestStateCompute { ctx.chain_store().heaviest_tipset(), ResolveNullTipset::TakeOlder, )?; + let fts = crate::chain_sync::get_full_tipset( + ctx.sync_network_context.clone(), + ctx.chain_store().clone(), + None, + tipset.key().clone(), + ) + .await?; + for block in fts.blocks() { + TipsetValidator::validate_msg_root(ctx.store(), block)?; + } let StateOutput { state_root, .. } = ctx .state_manager .compute_tipset_state( From 8fb563f5685cc8098f0ddee14c42130c9fe04cf6 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Fri, 15 Aug 2025 23:49:12 +0800 Subject: [PATCH 03/14] fix --- src/chain_sync/chain_follower.rs | 1 + src/rpc/methods/state.rs | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/chain_sync/chain_follower.rs b/src/chain_sync/chain_follower.rs index c4022f850059..3f6ac9064b5b 100644 --- a/src/chain_sync/chain_follower.rs +++ b/src/chain_sync/chain_follower.rs @@ -449,6 +449,7 @@ pub async fn get_full_tipset( for block in tipset.blocks() { block.persist(&chain_store.db)?; + TipsetValidator::validate_msg_root(&chain_store.db, block)?; } Ok(tipset) diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index 13e1c50447b5..ddbc7ef94cda 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -6,7 +6,6 @@ pub use types::*; use crate::blocks::{Tipset, TipsetKey}; use crate::chain::index::ResolveNullTipset; -use crate::chain_sync::TipsetValidator; use crate::cid_collections::CidHashSet; use crate::eth::EthChainId; use crate::interpreter::{MessageCallbackCtx, VMTrace}; @@ -1438,16 +1437,13 @@ impl RpcMethod<1> for ForestStateCompute { ctx.chain_store().heaviest_tipset(), ResolveNullTipset::TakeOlder, )?; - let fts = crate::chain_sync::get_full_tipset( + crate::chain_sync::get_full_tipset( ctx.sync_network_context.clone(), ctx.chain_store().clone(), None, tipset.key().clone(), ) .await?; - for block in fts.blocks() { - TipsetValidator::validate_msg_root(ctx.store(), block)?; - } let StateOutput { state_root, .. } = ctx .state_manager .compute_tipset_state( From 518014fd6c2517f73055c357d044b5eb86d76240 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Mon, 18 Aug 2025 08:53:23 +0800 Subject: [PATCH 04/14] refactor --- src/blocks/tipset.rs | 10 ++++++++ src/chain/store/chain_store.rs | 1 - src/chain_sync/chain_follower.rs | 34 +++++++++++--------------- src/chain_sync/mod.rs | 2 +- src/chain_sync/network_context.rs | 36 +++++++++++++++++++++++++--- src/libp2p/chain_exchange/message.rs | 8 +++++-- src/rpc/methods/f3.rs | 2 +- src/rpc/methods/state.rs | 26 ++++++++++---------- 8 files changed, 78 insertions(+), 41 deletions(-) diff --git a/src/blocks/tipset.rs b/src/blocks/tipset.rs index b01964dfdfc0..71e981a2837e 100644 --- a/src/blocks/tipset.rs +++ b/src/blocks/tipset.rs @@ -8,6 +8,7 @@ use std::{ use super::{Block, CachingBlockHeader, RawBlockHeader, Ticket}; use crate::{ + chain_sync::TipsetValidator, cid_collections::SmallCidNonEmptyVec, networks::{calibnet, mainnet}, shim::clock::ChainEpoch, @@ -546,6 +547,15 @@ impl FullTipset { pub fn weight(&self) -> &BigInt { &self.first_block().header().weight } + /// Persists the tipset into the blockstore. + pub fn persist(&self, db: &impl Blockstore) -> anyhow::Result<()> { + for block in self.blocks() { + // To persist `TxMeta` that is required for loading tipset messages + TipsetValidator::validate_msg_root(db, block)?; + block.persist(db)?; + } + Ok(()) + } } fn verify_block_headers<'a>( diff --git a/src/chain/store/chain_store.rs b/src/chain/store/chain_store.rs index 6a80e2174131..96d019742bdf 100644 --- a/src/chain/store/chain_store.rs +++ b/src/chain/store/chain_store.rs @@ -641,7 +641,6 @@ where // message to get all messages for block_header into a single iterator let mut get_message_for_block_header = |b: &CachingBlockHeader| -> Result, Error> { - tracing::info!("block_messages({}): {}", b.epoch, b.cid()); let (unsigned, signed) = block_messages(&db, b)?; let mut messages = Vec::with_capacity(unsigned.len() + signed.len()); let unsigned_box = unsigned.into_iter().map(ChainMessage::Unsigned); diff --git a/src/chain_sync/chain_follower.rs b/src/chain_sync/chain_follower.rs index 3f6ac9064b5b..b59b5619caed 100644 --- a/src/chain_sync/chain_follower.rs +++ b/src/chain_sync/chain_follower.rs @@ -176,7 +176,7 @@ pub async fn chain_follower( network.clone(), state_manager.chain_store().clone(), Some(source), - tipset_keys, + &tipset_keys, ) .await .inspect_err(|e| debug!("Querying full tipset failed: {}", e)) @@ -188,7 +188,7 @@ pub async fn chain_follower( network.clone(), state_manager.chain_store().clone(), None, - key, + &key, ) .await } @@ -435,22 +435,18 @@ pub async fn get_full_tipset( network: SyncNetworkContext, chain_store: Arc>, peer_id: Option, - tipset_keys: TipsetKey, + tipset_keys: &TipsetKey, ) -> anyhow::Result { // Attempt to load from the store - if let Ok(full_tipset) = load_full_tipset(&chain_store, tipset_keys.clone()) { + if let Ok(full_tipset) = load_full_tipset(&chain_store, tipset_keys) { return Ok(full_tipset); } // Load from the network let tipset = network - .chain_exchange_fts(peer_id, &tipset_keys.clone()) + .chain_exchange_full_tipset(peer_id, tipset_keys) .await .map_err(|e| anyhow::anyhow!(e))?; - - for block in tipset.blocks() { - block.persist(&chain_store.db)?; - TipsetValidator::validate_msg_root(&chain_store.db, block)?; - } + tipset.persist(&chain_store.db)?; Ok(tipset) } @@ -459,15 +455,15 @@ async fn get_full_tipset_batch( network: SyncNetworkContext, chain_store: Arc>, peer_id: Option, - tipset_keys: TipsetKey, + tipset_keys: &TipsetKey, ) -> anyhow::Result> { // Attempt to load from the store - if let Ok(full_tipset) = load_full_tipset(&chain_store, tipset_keys.clone()) { + if let Ok(full_tipset) = load_full_tipset(&chain_store, tipset_keys) { return Ok(vec![full_tipset]); } // Load from the network let tipsets = network - .chain_exchange_full_tipsets(peer_id, &tipset_keys.clone()) + .chain_exchange_full_tipsets(peer_id, tipset_keys) .await .map_err(|e| anyhow::anyhow!(e))?; @@ -480,13 +476,12 @@ async fn get_full_tipset_batch( Ok(tipsets) } -fn load_full_tipset( +pub fn load_full_tipset( chain_store: &ChainStore, - tipset_keys: TipsetKey, + tipset_keys: &TipsetKey, ) -> anyhow::Result { // Retrieve tipset from store based on passed in TipsetKey - let ts = chain_store.chain_index.load_required_tipset(&tipset_keys)?; - + let ts = chain_store.chain_index.load_required_tipset(tipset_keys)?; let blocks: Vec<_> = ts .block_headers() .iter() @@ -500,7 +495,6 @@ fn load_full_tipset( }) }) .try_collect()?; - // Construct FullTipset let fts = FullTipset::new(blocks)?; Ok(fts) @@ -603,7 +597,7 @@ impl SyncStateMachine { if self.stateless_mode || tipset.key() == self.cs.genesis_tipset().key() { // Skip validation in stateless mode and for genesis tipset true - } else if let Ok(parent_ts) = load_full_tipset(&self.cs, tipset.parents().clone()) { + } else if let Ok(parent_ts) = load_full_tipset(&self.cs, tipset.parents()) { let head_ts = self.cs.heaviest_tipset(); // Treat post-head-epoch tipsets as not validated to fix // basically, the follow task should always start from the current head which could be manually set @@ -878,7 +872,7 @@ impl SyncTask { } SyncTask::FetchTipset(key, _epoch) => { if let Ok(parents) = - get_full_tipset_batch(network.clone(), cs.clone(), None, key).await + get_full_tipset_batch(network.clone(), cs.clone(), None, &key).await { Some(SyncEvent::NewFullTipsets( parents.into_iter().map(Arc::new).collect(), diff --git a/src/chain_sync/mod.rs b/src/chain_sync/mod.rs index 2d7d52c3261e..b1631b12fe53 100644 --- a/src/chain_sync/mod.rs +++ b/src/chain_sync/mod.rs @@ -13,7 +13,7 @@ mod validation; pub use self::{ bad_block_cache::BadBlockCache, - chain_follower::{ChainFollower, get_full_tipset}, + chain_follower::{ChainFollower, load_full_tipset}, chain_muxer::SyncConfig, consensus::collect_errs, sync_status::{ForkSyncInfo, ForkSyncStage, NodeSyncStatus, SyncStatusReport}, diff --git a/src/chain_sync/network_context.rs b/src/chain_sync/network_context.rs index 8def73d7e9eb..e3807a6186b1 100644 --- a/src/chain_sync/network_context.rs +++ b/src/chain_sync/network_context.rs @@ -159,10 +159,39 @@ where .await } + /// Send a `chain_exchange` request for messages to assemble a full tipset with a local tipset, + /// If `peer_id` is `None`, requests will be sent to a set of shuffled peers. + pub async fn chain_exchange_messages( + &self, + peer_id: Option, + ts: &Tipset, + ) -> Result { + let mut bundles: Vec = self + .handle_chain_exchange_request( + peer_id, + ts.key(), + NonZeroU64::new(1).expect("Infallible"), + MESSAGES, + |_| true, + ) + .await + .expect("infallible"); + + if bundles.len() != 1 { + return Err(format!( + "chain exchange request returned {} tipsets, 1 expected.", + bundles.len() + )); + } + let mut bundle = bundles.remove(0); + bundle.blocks = ts.block_headers().to_vec(); + bundle.try_into() + } + /// Send a `chain_exchange` request for a single full tipset (includes /// messages) If `peer_id` is `None`, requests will be sent to a set of /// shuffled peers. - pub async fn chain_exchange_fts( + pub async fn chain_exchange_full_tipset( &self, peer_id: Option, tsk: &TipsetKey, @@ -179,7 +208,7 @@ where if fts.len() != 1 { return Err(format!( - "Full tipset request returned {} tipsets", + "Full tipset request returned {} tipsets, 1 expected.", fts.len() )); } @@ -212,7 +241,8 @@ where validate: F, ) -> Result, String> where - T: TryFrom + Send + Sync + 'static, + T: TryFrom + Send + Sync + 'static, + >::Error: std::fmt::Display, F: Fn(&Vec) -> bool, { let request = ChainExchangeRequest { diff --git a/src/libp2p/chain_exchange/message.rs b/src/libp2p/chain_exchange/message.rs index 7c9955e9d913..bdd34f1a17c9 100644 --- a/src/libp2p/chain_exchange/message.rs +++ b/src/libp2p/chain_exchange/message.rs @@ -125,7 +125,8 @@ impl ChainExchangeResponse { /// implementation. pub fn into_result(self) -> Result, String> where - T: TryFrom, + T: TryFrom, + >::Error: std::fmt::Display, { if self.status != ChainExchangeResponseStatus::Success && self.status != ChainExchangeResponseStatus::PartialResponse @@ -133,7 +134,10 @@ impl ChainExchangeResponse { return Err(format!("Status {:?}: {}", self.status, self.message)); } - self.chain.into_iter().map(T::try_from).collect() + self.chain + .into_iter() + .map(|i| T::try_from(i).map_err(|e| e.to_string())) + .collect() } } /// Contains all BLS and SECP messages and their indexes per block diff --git a/src/rpc/methods/f3.rs b/src/rpc/methods/f3.rs index 2a7bc1228ff3..aecd711ffb69 100644 --- a/src/rpc/methods/f3.rs +++ b/src/rpc/methods/f3.rs @@ -583,7 +583,7 @@ impl RpcMethod<1> for Finalize { ); let fts = ctx .sync_network_context - .chain_exchange_fts(None, &tsk) + .chain_exchange_full_tipset(None, &tsk) .await?; for block in fts.blocks() { block.persist(ctx.store())?; diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index ddbc7ef94cda..9ba1580459e5 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -1432,25 +1432,25 @@ impl RpcMethod<1> for ForestStateCompute { ctx: Ctx, (epoch,): Self::Params, ) -> Result { - let tipset = ctx.chain_index().tipset_by_height( + let ts = ctx.chain_index().tipset_by_height( epoch, ctx.chain_store().heaviest_tipset(), ResolveNullTipset::TakeOlder, )?; - crate::chain_sync::get_full_tipset( - ctx.sync_network_context.clone(), - ctx.chain_store().clone(), - None, - tipset.key().clone(), - ) - .await?; + // Attempt to load full tipset from the store + if crate::chain_sync::load_full_tipset(ctx.chain_store(), ts.key()).is_err() { + // Load full tipset from the network + let fts = ctx + .sync_network_context + .chain_exchange_messages(None, &ts) + .await + .map_err(|e| anyhow::anyhow!(e))?; + fts.persist(ctx.store())?; + } + let StateOutput { state_root, .. } = ctx .state_manager - .compute_tipset_state( - tipset, - crate::state_manager::NO_CALLBACK, - VMTrace::NotTraced, - ) + .compute_tipset_state(ts, crate::state_manager::NO_CALLBACK, VMTrace::NotTraced) .await?; Ok(state_root) From 1e0a4dab01f84d96f1ca81f00633eb04db24a983 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Mon, 18 Aug 2025 10:25:58 +0800 Subject: [PATCH 05/14] resolve AI comments --- src/chain_sync/network_context.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/chain_sync/network_context.rs b/src/chain_sync/network_context.rs index e3807a6186b1..bb4a5a90820c 100644 --- a/src/chain_sync/network_context.rs +++ b/src/chain_sync/network_context.rs @@ -174,8 +174,7 @@ where MESSAGES, |_| true, ) - .await - .expect("infallible"); + .await?; if bundles.len() != 1 { return Err(format!( From b49d29b5fa006403ceed2b54cc6dc8e37b7710ff Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Mon, 18 Aug 2025 11:06:59 +0800 Subject: [PATCH 06/14] resolve AI comment --- src/chain/store/index.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/chain/store/index.rs b/src/chain/store/index.rs index 4d059c3ad20c..90f76617476d 100644 --- a/src/chain/store/index.rs +++ b/src/chain/store/index.rs @@ -119,7 +119,7 @@ impl ChainIndex { pub fn tipset_by_height( &self, to: ChainEpoch, - from: Arc, + mut from: Arc, resolve: ResolveNullTipset, ) -> Result, Error> { const CHECKPOINT_INTERVAL: ChainEpoch = 1000; @@ -135,9 +135,16 @@ impl ChainIndex { epoch - epoch.mod_floor(&CHECKPOINT_INTERVAL) + CHECKPOINT_INTERVAL } - let checkpoint_from_epoch = next_checkpoint(to); - let checkpoint_from = CACHE.get_cloned(&checkpoint_from_epoch); - let from = checkpoint_from.unwrap_or(from); + let from_epoch = from.epoch(); + + let mut checkpoint_from_epoch = to; + while checkpoint_from_epoch < from_epoch { + if let Some(checkpoint_from) = CACHE.get_cloned(&checkpoint_from_epoch) { + from = checkpoint_from; + break; + } + checkpoint_from_epoch = next_checkpoint(checkpoint_from_epoch); + } if to == 0 { return Ok(Arc::new(Tipset::from(from.genesis(&self.db)?))); @@ -150,7 +157,11 @@ impl ChainIndex { } for (child, parent) in self.chain(from).tuple_windows() { - if child.epoch() % CHECKPOINT_INTERVAL == 0 { + // use `child.epoch() + CHECKPOINT_INTERVAL <= from_epoch` where `CHECKPOINT_INTERVAL>=CHAIN_FINALITY && CHECKPOINT_INTERVAL>=F3_CHAIN_FINALITY` + // to ensure the cached child is finalized(not on a fork). + if child.epoch() % CHECKPOINT_INTERVAL == 0 + && child.epoch() + CHECKPOINT_INTERVAL <= from_epoch + { CACHE.push(child.epoch(), child.clone()); } From 54c67d681cf5312eb0c49d45538ef1ed181481f7 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Mon, 18 Aug 2025 17:37:49 +0800 Subject: [PATCH 07/14] resolve comments --- src/chain/store/index.rs | 33 +++++++++++++++++---------------- src/shim/mod.rs | 1 + src/shim/policy.rs | 4 ++++ 3 files changed, 22 insertions(+), 16 deletions(-) create mode 100644 src/shim/policy.rs diff --git a/src/chain/store/index.rs b/src/chain/store/index.rs index 90f76617476d..8ab7dcd5807e 100644 --- a/src/chain/store/index.rs +++ b/src/chain/store/index.rs @@ -122,25 +122,28 @@ impl ChainIndex { mut from: Arc, resolve: ResolveNullTipset, ) -> Result, Error> { - const CHECKPOINT_INTERVAL: ChainEpoch = 1000; - static CACHE: LazyLock>> = - LazyLock::new(|| { - SizeTrackingLruCache::new_with_default_metrics_registry( - "tipset_by_height".into(), - 4096.try_into().expect("infallible"), - ) - }); + use crate::shim::policy::policy_constants::CHAIN_FINALITY; + static CACHE: LazyLock> = LazyLock::new(|| { + SizeTrackingLruCache::new_with_default_metrics_registry( + "tipset_by_height".into(), + 4096.try_into().expect("infallible"), + ) + }); + + // use `CHAIN_FINALITY` as checkpoint interval fn next_checkpoint(epoch: ChainEpoch) -> ChainEpoch { - epoch - epoch.mod_floor(&CHECKPOINT_INTERVAL) + CHECKPOINT_INTERVAL + epoch - epoch.mod_floor(&CHAIN_FINALITY) + CHAIN_FINALITY } let from_epoch = from.epoch(); let mut checkpoint_from_epoch = to; while checkpoint_from_epoch < from_epoch { - if let Some(checkpoint_from) = CACHE.get_cloned(&checkpoint_from_epoch) { - from = checkpoint_from; + if let Some(checkpoint_from_key) = CACHE.get_cloned(&checkpoint_from_epoch) + && let Ok(Some(checkpoint_from)) = Tipset::load(&self.db, &checkpoint_from_key) + { + from = checkpoint_from.into(); break; } checkpoint_from_epoch = next_checkpoint(checkpoint_from_epoch); @@ -157,12 +160,10 @@ impl ChainIndex { } for (child, parent) in self.chain(from).tuple_windows() { - // use `child.epoch() + CHECKPOINT_INTERVAL <= from_epoch` where `CHECKPOINT_INTERVAL>=CHAIN_FINALITY && CHECKPOINT_INTERVAL>=F3_CHAIN_FINALITY` + // use `child.epoch() + CHAIN_FINALITY <= from_epoch` // to ensure the cached child is finalized(not on a fork). - if child.epoch() % CHECKPOINT_INTERVAL == 0 - && child.epoch() + CHECKPOINT_INTERVAL <= from_epoch - { - CACHE.push(child.epoch(), child.clone()); + if child.epoch() % CHAIN_FINALITY == 0 && child.epoch() + CHAIN_FINALITY <= from_epoch { + CACHE.push(child.epoch(), child.key().clone()); } if to == child.epoch() { diff --git a/src/shim/mod.rs b/src/shim/mod.rs index 5c8508cdce47..387cf2fe8c7a 100644 --- a/src/shim/mod.rs +++ b/src/shim/mod.rs @@ -16,6 +16,7 @@ pub mod kernel; pub mod machine; pub mod message; pub mod piece; +pub mod policy; pub mod randomness; pub mod sector; pub mod state_tree; diff --git a/src/shim/policy.rs b/src/shim/policy.rs new file mode 100644 index 000000000000..5eb4320f2467 --- /dev/null +++ b/src/shim/policy.rs @@ -0,0 +1,4 @@ +// Copyright 2019-2025 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +pub use fil_actors_shared::v16::runtime::policy_constants; From cbf06fc0da5f9811e5331f1249456eb49c0d8ddd Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Mon, 18 Aug 2025 18:03:24 +0800 Subject: [PATCH 08/14] fix --- src/chain/store/index.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chain/store/index.rs b/src/chain/store/index.rs index 8ab7dcd5807e..4f0ef0cb530b 100644 --- a/src/chain/store/index.rs +++ b/src/chain/store/index.rs @@ -141,9 +141,9 @@ impl ChainIndex { let mut checkpoint_from_epoch = to; while checkpoint_from_epoch < from_epoch { if let Some(checkpoint_from_key) = CACHE.get_cloned(&checkpoint_from_epoch) - && let Ok(Some(checkpoint_from)) = Tipset::load(&self.db, &checkpoint_from_key) + && let Ok(Some(checkpoint_from)) = self.load_tipset(&checkpoint_from_key) { - from = checkpoint_from.into(); + from = checkpoint_from; break; } checkpoint_from_epoch = next_checkpoint(checkpoint_from_epoch); From bf5c12739ce6bf415eb8293f459193c3f7460a9e Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Mon, 18 Aug 2025 22:33:36 +0800 Subject: [PATCH 09/14] feat: add `--n_epochs` to `forest-cli state compute` --- src/cli/subcommands/state_cmd.rs | 16 ++++++-- src/lotus_json/mod.rs | 1 + src/rpc/methods/state.rs | 66 ++++++++++++++++++++++---------- 3 files changed, 58 insertions(+), 25 deletions(-) diff --git a/src/cli/subcommands/state_cmd.rs b/src/cli/subcommands/state_cmd.rs index f223e379d4e1..9427d9daecc8 100644 --- a/src/cli/subcommands/state_cmd.rs +++ b/src/cli/subcommands/state_cmd.rs @@ -8,6 +8,7 @@ use crate::shim::address::{CurrentNetwork, Error, Network, StrictAddress}; use crate::shim::clock::ChainEpoch; use cid::Cid; use clap::Subcommand; +use std::num::NonZeroUsize; use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; @@ -24,6 +25,9 @@ pub enum StateCommands { /// Which epoch to compute the state transition for #[arg(long)] epoch: ChainEpoch, + /// Number of tipset epochs to compute state for, default is 1 + #[arg(short, long)] + n_epochs: Option, }, /// Read the state of an actor ReadState { @@ -43,11 +47,15 @@ impl StateCommands { .await?; println!("{ret}"); } - StateCommands::Compute { epoch } => { - let ret = client - .call(ForestStateCompute::request((epoch,))?.with_timeout(Duration::MAX)) + StateCommands::Compute { epoch, n_epochs } => { + let state_roots = client + .call( + ForestStateCompute::request((epoch, n_epochs))?.with_timeout(Duration::MAX), + ) .await?; - println!("{ret}"); + for state_root in state_roots { + println!("{state_root}"); + } } Self::ReadState { actor_address } => { let tipset = ChainHead::call(&client, ()).await?; diff --git a/src/lotus_json/mod.rs b/src/lotus_json/mod.rs index 6d2d35c56ec2..12adf9de4b63 100644 --- a/src/lotus_json/mod.rs +++ b/src/lotus_json/mod.rs @@ -526,6 +526,7 @@ lotus_json_with_self!( DeadlineInfo, PaddedPieceSize, Uuid, + std::num::NonZeroUsize, ); // TODO(forest): https://github.com/ChainSafe/forest/issues/4032 diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index 9ba1580459e5..0ebd050a57b2 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0, MIT mod types; +use futures::stream::FuturesOrdered; pub use types::*; use crate::blocks::{Tipset, TipsetKey}; @@ -68,6 +69,7 @@ use nunny::vec as nonempty; use parking_lot::Mutex; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::num::NonZeroUsize; use std::ops::Mul; use std::path::PathBuf; use std::{sync::Arc, time::Duration}; @@ -1419,41 +1421,63 @@ impl RpcMethod<2> for StateFetchRoot { pub enum ForestStateCompute {} -impl RpcMethod<1> for ForestStateCompute { +impl RpcMethod<2> for ForestStateCompute { const NAME: &'static str = "Forest.StateCompute"; - const PARAM_NAMES: [&'static str; 1] = ["epoch"]; + const N_REQUIRED_PARAMS: usize = 1; + const PARAM_NAMES: [&'static str; 2] = ["epoch", "n_epochs"]; const API_PATHS: BitFlags = ApiPaths::all(); const PERMISSION: Permission = Permission::Read; - type Params = (ChainEpoch,); - type Ok = Cid; + type Params = (ChainEpoch, Option); + type Ok = Vec; async fn handle( ctx: Ctx, - (epoch,): Self::Params, + (from_epoch, n_epochs): Self::Params, ) -> Result { - let ts = ctx.chain_index().tipset_by_height( - epoch, + 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, )?; - // Attempt to load full tipset from the store - if crate::chain_sync::load_full_tipset(ctx.chain_store(), ts.key()).is_err() { - // Load full tipset from the network - let fts = ctx - .sync_network_context - .chain_exchange_messages(None, &ts) - .await - .map_err(|e| anyhow::anyhow!(e))?; - fts.persist(ctx.store())?; + let from_ts = ctx.chain_index().tipset_by_height( + from_epoch, + ctx.chain_store().heaviest_tipset(), + ResolveNullTipset::TakeOlder, + )?; + + let mut futures = FuturesOrdered::new(); + for ts in to_ts + .chain_arc(ctx.store()) + .take_while(|ts| ts.epoch() >= from_ts.epoch()) + { + let chain_store = ctx.chain_store().clone(); + let network_context = ctx.sync_network_context.clone(); + futures.push_front(async move { + if crate::chain_sync::load_full_tipset(&chain_store, ts.key()).is_err() { + // Backfill full tipset from the network + let fts = network_context + .chain_exchange_messages(None, &ts) + .await + .map_err(|e| anyhow::anyhow!(e))?; + fts.persist(&chain_store.db)?; + } + anyhow::Ok(ts) + }); } - let StateOutput { state_root, .. } = ctx - .state_manager - .compute_tipset_state(ts, crate::state_manager::NO_CALLBACK, VMTrace::NotTraced) - .await?; + let mut results = vec![]; + while let Ok(ts) = futures.select_next_some().await { + let StateOutput { state_root, .. } = ctx + .state_manager + .compute_tipset_state(ts, crate::state_manager::NO_CALLBACK, VMTrace::NotTraced) + .await?; + results.push(state_root); + } - Ok(state_root) + Ok(results) } } From fee02ebb9a118f9f5d8e4d7fd2ba6298d41f6a52 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 19 Aug 2025 07:55:46 +0800 Subject: [PATCH 10/14] changelog --- CHANGELOG.md | 4 ++++ src/cli/subcommands/state_cmd.rs | 27 ++++++++++++++++++++++----- src/rpc/methods/state.rs | 11 ++++++++--- src/rpc/methods/state/types.rs | 15 +++++++++++++++ 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 344ddeb7fd4f..660eb91f4a7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,10 @@ - [#5867](https://github.com/ChainSafe/forest/pull/5867) Added `--unordered` to `forest-cli snapshot export` for exporting `CAR` blocks in non-deterministic order for better performance with more parallelization. +- [#5946](https://github.com/ChainSafe/forest/pull/5946) Added `--n-epochs` to `forest-cli state compute` for computating state trees in batch. + +- [#5946](https://github.com/ChainSafe/forest/pull/5946) Added `--verbose` to `forest-cli state compute` for printing epochs and tipset keys along with state roots. + - [#5886](https://github.com/ChainSafe/forest/issues/5886) Add `forest-tool archive merge-f3` subcommand for merging a v1 Filecoin snapshot and an F3 snapshot into a v2 Filecoin snapshot. - [#4976](https://github.com/ChainSafe/forest/issues/4976) Add support for the `Filecoin.EthSubscribe` and `Filecoin.EthUnsubscribe` API methods to enable subscriptions to Ethereum event types: `heads` and `logs`. diff --git a/src/cli/subcommands/state_cmd.rs b/src/cli/subcommands/state_cmd.rs index 63bdde7830ff..b922d589eac0 100644 --- a/src/cli/subcommands/state_cmd.rs +++ b/src/cli/subcommands/state_cmd.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0, MIT use crate::lotus_json::HasLotusJson; -use crate::rpc::state::ForestStateCompute; +use crate::rpc::state::{ForestComputeStateOutput, ForestStateCompute}; use crate::rpc::{self, prelude::*}; use crate::shim::address::{CurrentNetwork, Error, Network, StrictAddress}; use crate::shim::clock::ChainEpoch; @@ -21,6 +21,7 @@ pub enum StateCommands { #[arg(short, long)] save_to_file: Option, }, + /// Compute state trees for epochs Compute { /// Which epoch to compute the state transition for #[arg(long)] @@ -28,6 +29,9 @@ pub enum StateCommands { /// Number of tipset epochs to compute state for. Default is 1 #[arg(short, long)] n_epochs: Option, + /// Print epoch and tipset key along with state root + #[arg(short, long)] + verbose: bool, }, /// Read the state of an actor ReadState { @@ -47,14 +51,27 @@ impl StateCommands { .await?; println!("{ret}"); } - StateCommands::Compute { epoch, n_epochs } => { - let state_roots = client + StateCommands::Compute { + epoch, + n_epochs, + verbose, + } => { + let results = client .call( ForestStateCompute::request((epoch, n_epochs))?.with_timeout(Duration::MAX), ) .await?; - for state_root in state_roots { - println!("{state_root}"); + for ForestComputeStateOutput { + state_root, + epoch, + tipset_key, + } in results + { + if verbose { + println!("{state_root} (epoch: {epoch}, tipset key: {tipset_key})"); + } else { + println!("{state_root}"); + } } } Self::ReadState { actor_address } => { diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index 27003a5c3e06..6c97980b1a8c 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -1429,7 +1429,7 @@ impl RpcMethod<2> for ForestStateCompute { const PERMISSION: Permission = Permission::Read; type Params = (ChainEpoch, Option); - type Ok = Vec; + type Ok = Vec; async fn handle( ctx: Ctx, @@ -1470,13 +1470,18 @@ impl RpcMethod<2> for ForestStateCompute { let mut results = vec![]; while let Some(Ok(ts)) = futures.next().await { + let epoch = ts.epoch(); + let tipset_key = ts.key().clone(); let StateOutput { state_root, .. } = ctx .state_manager .compute_tipset_state(ts, crate::state_manager::NO_CALLBACK, VMTrace::NotTraced) .await?; - results.push(state_root); + results.push(ForestComputeStateOutput { + state_root, + epoch, + tipset_key, + }); } - Ok(results) } } diff --git a/src/rpc/methods/state/types.rs b/src/rpc/methods/state/types.rs index c017f1c274f6..36cd381a1273 100644 --- a/src/rpc/methods/state/types.rs +++ b/src/rpc/methods/state/types.rs @@ -1,6 +1,7 @@ // Copyright 2019-2025 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +use crate::blocks::TipsetKey; use crate::lotus_json::{LotusJson, lotus_json_with_self}; use crate::message::Message as _; use crate::shim::executor::ApplyRet; @@ -32,6 +33,20 @@ pub struct ComputeStateOutput { lotus_json_with_self!(ComputeStateOutput); +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct ForestComputeStateOutput { + #[schemars(with = "LotusJson")] + #[serde(with = "crate::lotus_json")] + pub state_root: Cid, + pub epoch: ChainEpoch, + #[schemars(with = "LotusJson")] + #[serde(with = "crate::lotus_json")] + pub tipset_key: TipsetKey, +} + +lotus_json_with_self!(ForestComputeStateOutput); + #[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] #[serde(rename_all = "PascalCase")] pub struct ApiInvocResult { From 33e47c025b2613928f36b89968c99c94b6cc9283 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 19 Aug 2025 19:06:46 +0800 Subject: [PATCH 11/14] Update src/rpc/methods/state.rs Co-authored-by: Hubert --- src/rpc/methods/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index 6c97980b1a8c..4768a1eef963 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -1468,7 +1468,7 @@ impl RpcMethod<2> for ForestStateCompute { }); } - let mut results = vec![]; + let mut results = Vec::with_capacity(n_epochs); while let Some(Ok(ts)) = futures.next().await { let epoch = ts.epoch(); let tipset_key = ts.key().clone(); From 51235d0ad67839809372f43198ec12852a98f2a9 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 19 Aug 2025 19:12:01 +0800 Subject: [PATCH 12/14] fix build --- src/rpc/methods/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index 4768a1eef963..340714703b10 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -1468,7 +1468,7 @@ impl RpcMethod<2> for ForestStateCompute { }); } - let mut results = Vec::with_capacity(n_epochs); + let mut results = Vec::with_capacity(n_epochs as _); while let Some(Ok(ts)) = futures.next().await { let epoch = ts.epoch(); let tipset_key = ts.key().clone(); From c23e81f84394fc4dd78f1dcdf74da90844fd874d Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 19 Aug 2025 19:19:37 +0800 Subject: [PATCH 13/14] resolve AI comments --- src/chain_sync/chain_follower.rs | 4 ++-- src/rpc/methods/state.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/chain_sync/chain_follower.rs b/src/chain_sync/chain_follower.rs index b59b5619caed..6e4b05a0fb89 100644 --- a/src/chain_sync/chain_follower.rs +++ b/src/chain_sync/chain_follower.rs @@ -446,7 +446,7 @@ pub async fn get_full_tipset( .chain_exchange_full_tipset(peer_id, tipset_keys) .await .map_err(|e| anyhow::anyhow!(e))?; - tipset.persist(&chain_store.db)?; + tipset.persist(chain_store.blockstore())?; Ok(tipset) } @@ -469,7 +469,7 @@ async fn get_full_tipset_batch( for tipset in tipsets.iter() { for block in tipset.blocks() { - block.persist(&chain_store.db)?; + block.persist(chain_store.blockstore())?; } } diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index 340714703b10..071d94db9d93 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -1462,7 +1462,7 @@ impl RpcMethod<2> for ForestStateCompute { .chain_exchange_messages(None, &ts) .await .map_err(|e| anyhow::anyhow!(e))?; - fts.persist(&chain_store.db)?; + fts.persist(chain_store.blockstore())?; } anyhow::Ok(ts) }); From b372888d8aae74982c85572accc5c32fad9a0460 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 19 Aug 2025 19:39:08 +0800 Subject: [PATCH 14/14] changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 660eb91f4a7a..d30ce777f306 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ ### Breaking +- [#5946](https://github.com/ChainSafe/forest/pull/5946) Updated parameters and response of `Forest.StateCompute` RPC method to support new `forest-cli state compute` options. + ### Added - [#5835](https://github.com/ChainSafe/forest/issues/5835) Add `--format` flag to the `forest-cli snapshot export` subcommand. This allows exporting a Filecoin snapshot in v2 format(FRC-0108).