From 8cc05736af1160af15a86f82c896f3d400a054ce Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Sun, 29 Mar 2026 23:28:04 +0800 Subject: [PATCH 01/12] feat(rpc): impl Filecoin.ChainGetTipSetFinalityStatus --- src/chain/ec_finality/calculator/mod.rs | 11 +- src/chain/ec_finality/mod.rs | 2 +- src/chain/mod.rs | 2 +- src/chain/store/index.rs | 11 ++ src/rpc/methods/chain.rs | 119 +++++++++++++++++- src/rpc/methods/chain/types.rs | 38 ++++++ src/rpc/mod.rs | 1 + .../forest__rpc__tests__rpc__v2.snap | 39 ++++++ .../subcommands/api_cmd/api_compare_tests.rs | 15 ++- .../subcommands/api_cmd/test_snapshots.txt | 1 + 10 files changed, 226 insertions(+), 13 deletions(-) diff --git a/src/chain/ec_finality/calculator/mod.rs b/src/chain/ec_finality/calculator/mod.rs index 2f21a6e2f60a..52f30eb94eb5 100644 --- a/src/chain/ec_finality/calculator/mod.rs +++ b/src/chain/ec_finality/calculator/mod.rs @@ -19,27 +19,26 @@ mod skellam; mod tests; use anyhow::Context as _; +use std::sync::LazyLock; // `BISECT_LOW` and `BISECT_HIGH` define the search range for the bisect algorithm // that finds the epoch depth at which the finality guarantee is met. A low // bound of 3 avoids evaluating trivially shallow depths; a high bound of // 200 accommodates degraded chains that take longer to finalize. -#[allow(dead_code)] pub const BISECT_LOW: i64 = 3; -#[allow(dead_code)] pub const BISECT_HIGH: i64 = 200; // the Filecoin mainnet expected block production rate. -#[allow(dead_code)] pub const DEFAULT_BLOCKS_PER_EPOCH: f64 = 5.0; // the standard Filecoin security assumption for adversarial mining power. -#[allow(dead_code)] pub const DEFAULT_BYZANTINE_FRACTION: f64 = 0.3; // the target reorg probability as a power of 2. 2^-30 (~one-in-a-billion) is the standard Filecoin finality guarantee. -#[allow(dead_code)] -pub const DEFAULT_SAFETY_EXPONENT: i64 = -30; +pub const DEFAULT_SAFETY_EXPONENT: i32 = -30; + +pub static DEFAULT_GUARANTEE: LazyLock = + LazyLock::new(|| f64::powi(2., DEFAULT_SAFETY_EXPONENT)); /// Computes the upper-bound probability that a confirmed /// tipset could be reorganized out of the canonical chain. This is a port diff --git a/src/chain/ec_finality/mod.rs b/src/chain/ec_finality/mod.rs index d4b92acfefdd..607d69471df2 100644 --- a/src/chain/ec_finality/mod.rs +++ b/src/chain/ec_finality/mod.rs @@ -1,4 +1,4 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -mod calculator; +pub mod calculator; diff --git a/src/chain/mod.rs b/src/chain/mod.rs index a2e3d56cdda4..f53751fffd3f 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -1,7 +1,7 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -mod ec_finality; +pub mod ec_finality; mod snapshot_format; pub mod store; #[cfg(test)] diff --git a/src/chain/store/index.rs b/src/chain/store/index.rs index 1d08857834b3..c2e58a13b635 100644 --- a/src/chain/store/index.rs +++ b/src/chain/store/index.rs @@ -105,6 +105,17 @@ impl ChainIndex { .ok_or_else(|| Error::NotFound("Key for header".into())) } + /// Returns an iterator of all tipsets, each tipset is cached. + /// Use [`Tipset::chain`] if you don't want every tipset to be cached. + pub fn chain_with_cache(&self, ts: Tipset) -> impl Iterator { + let mut tipset = Some(ts); + std::iter::from_fn(move || { + let child = tipset.take()?; + tipset = self.load_required_tipset(child.parents()).ok(); + Some(child) + }) + } + /// Find tipset at epoch `to` in the chain of ancestors starting at `from`. /// If the tipset is _not_ in the chain of ancestors (i.e., if the `to` /// epoch is higher than `from.epoch()`), an error will be returned. diff --git a/src/rpc/methods/chain.rs b/src/rpc/methods/chain.rs index 620ab00869a0..ccc72e1db510 100644 --- a/src/rpc/methods/chain.rs +++ b/src/rpc/methods/chain.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0, MIT pub mod types; -use enumflags2::{BitFlags, make_bitflags}; use types::*; #[cfg(test)] @@ -34,10 +33,12 @@ use crate::utils::io::VoidAsyncWriter; use crate::utils::misc::env::is_env_truthy; use anyhow::{Context as _, Result}; use cid::Cid; +use enumflags2::{BitFlags, make_bitflags}; use fvm_ipld_blockstore::Blockstore; use fvm_ipld_encoding::{CborStore, RawBytes}; use hex::ToHex; use ipld_core::ipld::Ipld; +use itertools::Itertools as _; use jsonrpsee::types::Params; use jsonrpsee::types::error::ErrorObjectOwned; use num::BigInt; @@ -1138,6 +1139,121 @@ impl RpcMethod<1> for ChainGetTipSetV2 { } } +pub enum ChainGetTipSetFinalityStatus {} + +impl ChainGetTipSetFinalityStatus { + pub fn get_finality_status(ctx: &Ctx) -> ChainFinalityStatus { + let head = ctx.chain_store().heaviest_tipset(); + let (ec_finality_threshold_depth, ec_finalized_tip_set) = + Self::get_ec_finality_threshold_depth_and_tipset_with_cache(ctx, head.clone()); + let f3_finalized_tip_set = ctx.chain_store().f3_finalized_tipset(); + let finalized_tip_set = match (&ec_finalized_tip_set, &f3_finalized_tip_set) { + (Some(ec), Some(f3)) => { + if ec.epoch() >= f3.epoch() { + Some(ec.clone()) + } else { + Some(f3.clone()) + } + } + (Some(ec), None) => Some(ec.clone()), + (None, Some(f3)) => Some(f3.clone()), + (None, None) => None, + }; + ChainFinalityStatus { + ec_finality_threshold_depth, + ec_finalized_tip_set, + f3_finalized_tip_set, + finalized_tip_set, + head, + } + } + + fn get_ec_finality_threshold_depth_and_tipset_with_cache( + ctx: &Ctx, + head: Tipset, + ) -> (i64, Option) { + static CACHE: parking_lot::Mutex)>> = + parking_lot::Mutex::new(None); + let mut cache = CACHE.lock(); + if let Some((cached_head, cached_threshold, cached_tipset)) = &*cache + && cached_head == &head + { + (*cached_threshold, cached_tipset.clone()) + } else { + let (threshold, tipset) = + Self::get_ec_finality_threshold_depth_and_tipset(ctx, head.clone()); + *cache = Some((head, threshold, tipset.clone())); + (threshold, tipset) + } + } + + fn get_ec_finality_threshold_depth_and_tipset( + ctx: &Ctx, + head: Tipset, + ) -> (i64, Option) { + use crate::chain::ec_finality::calculator::{ + DEFAULT_BLOCKS_PER_EPOCH, DEFAULT_BYZANTINE_FRACTION, DEFAULT_GUARANTEE, + find_threshold_depth, + }; + + let finality = ctx.chain_config().policy.chain_finality; + let chain_len = finality as usize + 5; + let chain_rev = ctx + .chain_index() + .chain_with_cache(head) + .take(chain_len) + .collect_vec(); + let chain_n_blocks = chain_rev + .iter() + .rev() + .map(|ts| ts.len() as i64) + .collect_vec(); + let threshold = match find_threshold_depth( + &chain_n_blocks, + finality, + DEFAULT_BLOCKS_PER_EPOCH, + DEFAULT_BYZANTINE_FRACTION, + *DEFAULT_GUARANTEE, + ) { + Ok(threshold) => threshold, + Err(e) => { + tracing::error!( + "Failed to calculate EC finality threshold depth: {e:#}, chain: {chain_n_blocks:?}" + ); + -1 + } + }; + let finalized = if let Ok(threshold) = usize::try_from(threshold) + && let Some(ts) = chain_rev.get(threshold) + { + Some(ts.clone()) + } else { + None + }; + (threshold, finalized) + } +} + +impl RpcMethod<0> for ChainGetTipSetFinalityStatus { + const NAME: &'static str = "Filecoin.ChainGetTipSetFinalityStatus"; + const PARAM_NAMES: [&'static str; 0] = []; + const API_PATHS: BitFlags = make_bitflags!(ApiPaths::{ V2 }); + const PERMISSION: Permission = Permission::Read; + const DESCRIPTION: Option<&'static str> = + Some("Returns a breakdown of how the node is currently determining finality."); + + type Params = (); + type Ok = ChainFinalityStatus; + + async fn handle( + ctx: Ctx, + (): Self::Params, + _: &http::Extensions, + ) -> Result { + Ok(Self::get_finality_status(&ctx)) + } +} + pub enum ChainSetHead {} impl RpcMethod<1> for ChainSetHead { const NAME: &'static str = "Filecoin.ChainSetHead"; @@ -1582,7 +1698,6 @@ mod tests { networks::{self, ChainConfig}, }; use PathChange::{Apply, Revert}; - use itertools::Itertools as _; use std::sync::Arc; #[test] diff --git a/src/rpc/methods/chain/types.rs b/src/rpc/methods/chain/types.rs index 54b728981ea0..a788372a7be1 100644 --- a/src/rpc/methods/chain/types.rs +++ b/src/rpc/methods/chain/types.rs @@ -10,3 +10,41 @@ pub struct ObjStat { pub links: usize, } lotus_json_with_self!(ObjStat); + +/// Describes how the node is currently determining finality, +// combining probabilistic EC finality (based on observed chain health) with +// F3 fast finality when available. +#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ChainFinalityStatus { + /// The shallowest epoch depth at which the + /// probability of a chain reorganization drops below 2^-30 (~one in a + /// billion). A value of -1 indicates the threshold was not met within the + /// search range, which suggests degraded chain health. + pub ec_finality_threshold_depth: i64, + + /// The most recent tipset where the reorg probability + /// is below 2^-30, based on observed block production. Nil if the + /// threshold is not met. + #[serde(with = "crate::lotus_json")] + #[schemars(with = "LotusJson>")] + pub ec_finalized_tip_set: Option, + + /// The tipset finalized by F3 (Fast Finality), if F3 + /// is running and has issued a certificate. Nil if F3 is not available. + #[serde(with = "crate::lotus_json")] + #[schemars(with = "LotusJson>")] + pub f3_finalized_tip_set: Option, + + /// The overall finalized tipset used by the node, + /// taking the most recent of F3 and EC calculator results. + #[serde(with = "crate::lotus_json")] + #[schemars(with = "LotusJson>")] + pub finalized_tip_set: Option, + + /// The current chain head used for the computation. + #[serde(with = "crate::lotus_json")] + #[schemars(with = "LotusJson")] + pub head: Tipset, +} +lotus_json_with_self!(ChainFinalityStatus); diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 7dc550a388b2..7e597dc7235f 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -79,6 +79,7 @@ macro_rules! for_each_rpc_method { $callback!($crate::rpc::chain::ChainGetPath); $callback!($crate::rpc::chain::ChainGetTipSet); $callback!($crate::rpc::chain::ChainGetTipSetV2); + $callback!($crate::rpc::chain::ChainGetTipSetFinalityStatus); $callback!($crate::rpc::chain::ChainGetTipSetAfterHeight); $callback!($crate::rpc::chain::ChainGetTipSetByHeight); $callback!($crate::rpc::chain::ChainHasObj); diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap index e79961f0ed8f..85646b16c370 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap @@ -20,6 +20,15 @@ methods: schema: $ref: "#/components/schemas/Tipset" paramStructure: by-position + - name: Filecoin.ChainGetTipSetFinalityStatus + description: Returns a breakdown of how the node is currently determining finality. + params: [] + result: + name: Filecoin.ChainGetTipSetFinalityStatus.Result + required: true + schema: + $ref: "#/components/schemas/ChainFinalityStatus" + paramStructure: by-position - name: Forest.ChainExport params: - name: params @@ -1819,6 +1828,32 @@ components: - $ref: "#/components/schemas/EthInt64" Bloom: type: string + ChainFinalityStatus: + description: "Describes how the node is currently determining finality," + type: object + properties: + ecFinalityThresholdDepth: + description: "The shallowest epoch depth at which the\nprobability of a chain reorganization drops below 2^-30 (~one in a\nbillion). A value of -1 indicates the threshold was not met within the\nsearch range, which suggests degraded chain health." + type: integer + format: int64 + ecFinalizedTipSet: + description: "The most recent tipset where the reorg probability\nis below 2^-30, based on observed block production. Nil if the\nthreshold is not met." + $ref: "#/components/schemas/Nullable_Tipset" + f3FinalizedTipSet: + description: "The tipset finalized by F3 (Fast Finality), if F3\nis running and has issued a certificate. Nil if F3 is not available." + $ref: "#/components/schemas/Nullable_Tipset" + finalizedTipSet: + description: "The overall finalized tipset used by the node,\ntaking the most recent of F3 and EC calculator results." + $ref: "#/components/schemas/Nullable_Tipset" + head: + description: The current chain head used for the computation. + $ref: "#/components/schemas/Tipset" + required: + - ecFinalityThresholdDepth + - ecFinalizedTipSet + - f3FinalizedTipSet + - finalizedTipSet + - head ChangedType: description: Represents a changed value with before and after states. type: object @@ -2696,6 +2731,10 @@ components: anyOf: - $ref: "#/components/schemas/Ticket" - type: "null" + Nullable_Tipset: + anyOf: + - $ref: "#/components/schemas/Tipset" + - type: "null" PoStProof: type: object properties: diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index 8490123bff31..e80a02061f89 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -476,10 +476,19 @@ fn common_tests() -> Vec { ] } -fn chain_tests() -> Vec { +fn chain_tests(offline: bool) -> Vec { vec![ - RpcTest::basic(ChainHead::request(()).unwrap()), RpcTest::identity(ChainGetGenesis::request(()).unwrap()), + if offline { + RpcTest::basic(ChainHead::request(()).unwrap()) + } else { + RpcTest::identity(ChainHead::request(()).unwrap()) + }, + if offline { + RpcTest::basic(ChainGetTipSetFinalityStatus::request(()).unwrap()) + } else { + RpcTest::identity(ChainGetTipSetFinalityStatus::request(()).unwrap()) + }, ] } @@ -2505,7 +2514,7 @@ pub(super) async fn create_tests( let mut tests = vec![]; tests.extend(auth_tests()?); tests.extend(common_tests()); - tests.extend(chain_tests()); + tests.extend(chain_tests(offline)); tests.extend(mpool_tests()); tests.extend(net_tests()); tests.extend(node_tests()); diff --git a/src/tool/subcommands/api_cmd/test_snapshots.txt b/src/tool/subcommands/api_cmd/test_snapshots.txt index ef9ffed5986b..a1ae73b00cef 100644 --- a/src/tool/subcommands/api_cmd/test_snapshots.txt +++ b/src/tool/subcommands/api_cmd/test_snapshots.txt @@ -18,6 +18,7 @@ filecoin_chaingettipset_key_1762420743419725.rpcsnap.json.zst filecoin_chaingettipset_tag_1762420743420262.rpcsnap.json.zst filecoin_chaingettipsetafterheight_1736937942817771.rpcsnap.json.zst filecoin_chaingettipsetbyheight_1741271398549509.rpcsnap.json.zst +filecoin_chaingettipsetfinalitystatus_1774798719525307.rpcsnap.json.zst filecoin_chainhasobj_1736937942818251.rpcsnap.json.zst filecoin_chainhead_1741269640489512.rpcsnap.json.zst filecoin_chainreadobj_1736937942818676.rpcsnap.json.zst From 7df271cb9087e77c9f50987dc2995b3631e266a5 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Mon, 30 Mar 2026 06:27:40 +0800 Subject: [PATCH 02/12] fix rpc test --- scripts/tests/api_compare/.env | 2 +- scripts/tests/api_compare/filter-list-gateway | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/tests/api_compare/.env b/scripts/tests/api_compare/.env index 310389039eb6..7d93452a6fe7 100644 --- a/scripts/tests/api_compare/.env +++ b/scripts/tests/api_compare/.env @@ -1,6 +1,6 @@ # Note: this should be a `fat` image so that it contains the pre-downloaded filecoin proof parameters FOREST_IMAGE=ghcr.io/chainsafe/forest:edge-fat -LOTUS_IMAGE=filecoin/lotus-all-in-one:v1.35.0-calibnet +LOTUS_IMAGE=filecoin/lotus-all-in-one:v1.35.1-rc1-calibnet FIL_PROOFS_PARAMETER_CACHE=/var/tmp/filecoin-proof-parameters LOTUS_RPC_PORT=1234 FOREST_RPC_PORT=2345 diff --git a/scripts/tests/api_compare/filter-list-gateway b/scripts/tests/api_compare/filter-list-gateway index df81739d886b..5c87cd836b57 100644 --- a/scripts/tests/api_compare/filter-list-gateway +++ b/scripts/tests/api_compare/filter-list-gateway @@ -59,3 +59,7 @@ # broken !Filecoin.EthGetFilterLogs +# https://github.com/filecoin-project/lotus/pull/13562 +!Filecoin.StateSearchMsg +# https://github.com/filecoin-project/lotus/pull/13562 +!Filecoin.EthGetTransactionReceiptLimited From 30ae122ed3df106084ad38346d10ba3242a7c914 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Mon, 30 Mar 2026 07:00:03 +0800 Subject: [PATCH 03/12] fix --- scripts/tests/api_compare/filter-list-gateway | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/tests/api_compare/filter-list-gateway b/scripts/tests/api_compare/filter-list-gateway index 5c87cd836b57..037739dceeb9 100644 --- a/scripts/tests/api_compare/filter-list-gateway +++ b/scripts/tests/api_compare/filter-list-gateway @@ -4,6 +4,7 @@ !Filecoin.AuthNew !Filecoin.BeaconGetEntry +!Filecoin.ChainGetTipSetFinalityStatus !Filecoin.ChainSetHead !Filecoin.ChainStatObj !Filecoin.ChainTipSetWeight From 00abf1237becd6b640333e7c4b4df7f7fb322c66 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 31 Mar 2026 08:06:43 +0800 Subject: [PATCH 04/12] handle null rounds --- src/chain/ec_finality/calculator/mod.rs | 4 +- src/chain/ec_finality/calculator/tests.rs | 22 ++++------ src/rpc/methods/chain.rs | 51 ++++++++++++++--------- 3 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/chain/ec_finality/calculator/mod.rs b/src/chain/ec_finality/calculator/mod.rs index 52f30eb94eb5..0f0c743c51e7 100644 --- a/src/chain/ec_finality/calculator/mod.rs +++ b/src/chain/ec_finality/calculator/mod.rs @@ -24,9 +24,9 @@ use std::sync::LazyLock; // `BISECT_LOW` and `BISECT_HIGH` define the search range for the bisect algorithm // that finds the epoch depth at which the finality guarantee is met. A low // bound of 3 avoids evaluating trivially shallow depths; a high bound of -// 200 accommodates degraded chains that take longer to finalize. +// 450 accommodates degraded chains that take longer to finalize. pub const BISECT_LOW: i64 = 3; -pub const BISECT_HIGH: i64 = 200; +pub const BISECT_HIGH: i64 = 450; // the Filecoin mainnet expected block production rate. pub const DEFAULT_BLOCKS_PER_EPOCH: f64 = 5.0; diff --git a/src/chain/ec_finality/calculator/tests.rs b/src/chain/ec_finality/calculator/tests.rs index 2d394699e19e..e49b638bf475 100644 --- a/src/chain/ec_finality/calculator/tests.rs +++ b/src/chain/ec_finality/calculator/tests.rs @@ -93,7 +93,6 @@ fn test_calc_validator_prob_healthy_chain() { let chain = vec![5; 905]; let current_epoch = chain.len() as i64 - 1; - let guarantee = 2_f64.powi(-30); let prob30 = calc_validator_prob( &chain, @@ -106,7 +105,7 @@ fn test_calc_validator_prob_healthy_chain() { .unwrap(); assert_lt!( prob30, - guarantee, + *DEFAULT_GUARANTEE, "healthy chain at depth 30 should be below 2^-30" ); @@ -132,7 +131,6 @@ fn test_calc_validator_prob_degraded_chain() { // finality than a healthy chain at the same depth let chain = vec![2; 905]; let current_epoch = chain.len() as i64 - 1; - let guarantee = 2_f64.powi(-30); let prob30 = calc_validator_prob( &chain, @@ -145,7 +143,7 @@ fn test_calc_validator_prob_degraded_chain() { .unwrap(); assert_ge!( prob30, - guarantee, + *DEFAULT_GUARANTEE, "degraded chain at depth 30 should NOT achieve 2^-30" ); } @@ -153,14 +151,13 @@ fn test_calc_validator_prob_degraded_chain() { #[test] fn test_find_threshold_depth_healthy_chain() { let chain = vec![5; 905]; - let guarantee = 2_f64.powi(-30); let depth = find_threshold_depth( &chain, TEST_FINALITY, DEFAULT_BLOCKS_PER_EPOCH, DEFAULT_BYZANTINE_FRACTION, - guarantee, + *DEFAULT_GUARANTEE, ) .unwrap(); assert_gt!(depth, 0, "healthy chain should find a threshold"); @@ -173,17 +170,16 @@ fn test_find_threshold_depth_healthy_chain() { #[test] fn test_find_threshold_depth_degraded_chain() { - // All-2s chain is too degraded to achieve 2^-30 within the bisect + // All-1s chain is too degraded to achieve 2^-30 within the bisect // search range (BisectHigh=200), so threshold is not found - let chain = vec![2; 905]; - let guarantee = 2_f64.powi(-30); + let chain = vec![1; 905]; let depth = find_threshold_depth( &chain, TEST_FINALITY, DEFAULT_BLOCKS_PER_EPOCH, DEFAULT_BYZANTINE_FRACTION, - guarantee, + *DEFAULT_GUARANTEE, ) .unwrap(); assert_eq!( @@ -197,14 +193,13 @@ fn test_find_threshold_depth_mildly_degraded_chain() { // All-3s chain is degraded but should still find a threshold, // just deeper than a healthy chain let chain = vec![3; 905]; - let guarantee = 2_f64.powi(-30); let depth = find_threshold_depth( &chain, TEST_FINALITY, DEFAULT_BLOCKS_PER_EPOCH, DEFAULT_BYZANTINE_FRACTION, - guarantee, + *DEFAULT_GUARANTEE, ) .unwrap(); assert_gt!( @@ -226,13 +221,12 @@ fn test_find_threshold_depth_mildly_degraded_chain() { #[case(1)] fn test_find_threshold_depth_too_short_chain(#[case] chain_len: usize) { let chain = vec![3; chain_len]; - let guarantee = 2_f64.powi(-30); let depth = find_threshold_depth( &chain, TEST_FINALITY, DEFAULT_BLOCKS_PER_EPOCH, DEFAULT_BYZANTINE_FRACTION, - guarantee, + *DEFAULT_GUARANTEE, ) .unwrap(); assert_eq!(depth, -1, "input chain is too short"); diff --git a/src/rpc/methods/chain.rs b/src/rpc/methods/chain.rs index ccc72e1db510..d3ede5a06446 100644 --- a/src/rpc/methods/chain.rs +++ b/src/rpc/methods/chain.rs @@ -299,7 +299,7 @@ impl RpcMethod<1> for ChainGetParentReceipts { gas_used: r.gas_used(), events_root: r.events_root(), }) - .collect(); + .collect_vec(); Ok(receipts) } @@ -1198,18 +1198,28 @@ impl ChainGetTipSetFinalityStatus { let finality = ctx.chain_config().policy.chain_finality; let chain_len = finality as usize + 5; - let chain_rev = ctx - .chain_index() - .chain_with_cache(head) - .take(chain_len) - .collect_vec(); - let chain_n_blocks = chain_rev - .iter() - .rev() - .map(|ts| ts.len() as i64) - .collect_vec(); - let threshold = match find_threshold_depth( - &chain_n_blocks, + let mut chain = Vec::with_capacity(chain_len); + let mut ts = head.clone(); + while chain.len() < chain_len { + chain.push(ts.len() as i64); + if let Ok(parent) = ctx.chain_index().load_required_tipset(ts.parents()) { + // insert 0 for null rounds + for _ in 1..(ts.epoch() - parent.epoch()) { + if chain.len() < chain_len { + chain.push(0); + } else { + break; + } + } + ts = parent; + } else { + break; + } + } + // Reverse to chronological order (oldest first). + chain.reverse(); + let depth = match find_threshold_depth( + &chain, finality, DEFAULT_BLOCKS_PER_EPOCH, DEFAULT_BYZANTINE_FRACTION, @@ -1218,19 +1228,22 @@ impl ChainGetTipSetFinalityStatus { Ok(threshold) => threshold, Err(e) => { tracing::error!( - "Failed to calculate EC finality threshold depth: {e:#}, chain: {chain_n_blocks:?}" + "Failed to calculate EC finality threshold depth: {e:#}, chain: {chain:?}" ); -1 } }; - let finalized = if let Ok(threshold) = usize::try_from(threshold) - && let Some(ts) = chain_rev.get(threshold) - { + let finalized = if depth >= 0 + && let Ok(ts) = ctx.chain_index().tipset_by_height( + (head.epoch() - depth).max(0), + head, + ResolveNullTipset::TakeOlder, + ) { Some(ts.clone()) } else { None }; - (threshold, finalized) + (depth, finalized) } } @@ -1657,7 +1670,7 @@ impl PathChanges { .into_iter() .map(PathChange::Revert) .chain(applies.into_iter().map(PathChange::Apply)) - .collect() + .collect_vec() } } From 9da2ef645b26798b733c9af8b65aeb520d53fe01 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 31 Mar 2026 08:11:34 +0800 Subject: [PATCH 05/12] cleanup --- src/chain/store/index.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/chain/store/index.rs b/src/chain/store/index.rs index c2e58a13b635..1d08857834b3 100644 --- a/src/chain/store/index.rs +++ b/src/chain/store/index.rs @@ -105,17 +105,6 @@ impl ChainIndex { .ok_or_else(|| Error::NotFound("Key for header".into())) } - /// Returns an iterator of all tipsets, each tipset is cached. - /// Use [`Tipset::chain`] if you don't want every tipset to be cached. - pub fn chain_with_cache(&self, ts: Tipset) -> impl Iterator { - let mut tipset = Some(ts); - std::iter::from_fn(move || { - let child = tipset.take()?; - tipset = self.load_required_tipset(child.parents()).ok(); - Some(child) - }) - } - /// Find tipset at epoch `to` in the chain of ancestors starting at `from`. /// If the tipset is _not_ in the chain of ancestors (i.e., if the `to` /// epoch is higher than `from.epoch()`), an error will be returned. From 6db0a02e9a17e75877f4cae1c91a79dc84ec1865 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 31 Mar 2026 17:09:59 +0800 Subject: [PATCH 06/12] use lotus image with fix --- scripts/tests/api_compare/.env | 2 +- scripts/tests/api_compare/filter-list-gateway | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/scripts/tests/api_compare/.env b/scripts/tests/api_compare/.env index 7d93452a6fe7..dcb79c96fe58 100644 --- a/scripts/tests/api_compare/.env +++ b/scripts/tests/api_compare/.env @@ -1,6 +1,6 @@ # Note: this should be a `fat` image so that it contains the pre-downloaded filecoin proof parameters FOREST_IMAGE=ghcr.io/chainsafe/forest:edge-fat -LOTUS_IMAGE=filecoin/lotus-all-in-one:v1.35.1-rc1-calibnet +LOTUS_IMAGE=filecoin/lotus-all-in-one:v1.35.1-calibnet FIL_PROOFS_PARAMETER_CACHE=/var/tmp/filecoin-proof-parameters LOTUS_RPC_PORT=1234 FOREST_RPC_PORT=2345 diff --git a/scripts/tests/api_compare/filter-list-gateway b/scripts/tests/api_compare/filter-list-gateway index 037739dceeb9..284beb4e6095 100644 --- a/scripts/tests/api_compare/filter-list-gateway +++ b/scripts/tests/api_compare/filter-list-gateway @@ -60,7 +60,3 @@ # broken !Filecoin.EthGetFilterLogs -# https://github.com/filecoin-project/lotus/pull/13562 -!Filecoin.StateSearchMsg -# https://github.com/filecoin-project/lotus/pull/13562 -!Filecoin.EthGetTransactionReceiptLimited From a304f85ff7a651c2d5901bf34fa12d55cdc21c94 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 31 Mar 2026 18:42:08 +0800 Subject: [PATCH 07/12] resolve AI comments --- src/chain/ec_finality/calculator/tests.rs | 2 +- src/rpc/methods/chain/types.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/chain/ec_finality/calculator/tests.rs b/src/chain/ec_finality/calculator/tests.rs index e49b638bf475..711acf31ba13 100644 --- a/src/chain/ec_finality/calculator/tests.rs +++ b/src/chain/ec_finality/calculator/tests.rs @@ -171,7 +171,7 @@ fn test_find_threshold_depth_healthy_chain() { #[test] fn test_find_threshold_depth_degraded_chain() { // All-1s chain is too degraded to achieve 2^-30 within the bisect - // search range (BisectHigh=200), so threshold is not found + // search range (BisectHigh=450), so threshold is not found let chain = vec![1; 905]; let depth = find_threshold_depth( diff --git a/src/rpc/methods/chain/types.rs b/src/rpc/methods/chain/types.rs index a788372a7be1..5f9c744995bf 100644 --- a/src/rpc/methods/chain/types.rs +++ b/src/rpc/methods/chain/types.rs @@ -12,8 +12,8 @@ pub struct ObjStat { lotus_json_with_self!(ObjStat); /// Describes how the node is currently determining finality, -// combining probabilistic EC finality (based on observed chain health) with -// F3 fast finality when available. +/// combining probabilistic EC finality (based on observed chain health) with +/// F3 fast finality when available. #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ChainFinalityStatus { From 5a8d34f1a521c893f78a96c9f38468da28541265 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 31 Mar 2026 18:44:23 +0800 Subject: [PATCH 08/12] changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61adc5920fd8..280307c0be4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ ### Added +- [#6811](https://github.com/ChainSafe/forest/pull/6811): Added v2 RPC method `Filecoin.ChainGetTipSetFinalityStatus`. + - [#6710](https://github.com/ChainSafe/forest/pull/6710): Added support for f4 addresses in forest-wallet. ### Changed From 129b78337419eb4bdaf5b0ad9dcc5dd636646290 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 31 Mar 2026 19:14:52 +0800 Subject: [PATCH 09/12] fix rpc snap --- src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap index 85646b16c370..4b167bdf9614 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap @@ -1829,7 +1829,7 @@ components: Bloom: type: string ChainFinalityStatus: - description: "Describes how the node is currently determining finality," + description: "Describes how the node is currently determining finality,\ncombining probabilistic EC finality (based on observed chain health) with\nF3 fast finality when available." type: object properties: ecFinalityThresholdDepth: From c60ce45674f5847aa538c2b10690c05b94948813 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 31 Mar 2026 19:16:37 +0800 Subject: [PATCH 10/12] fix bad merge --- scripts/tests/api_compare/filter-list-gateway | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/tests/api_compare/filter-list-gateway b/scripts/tests/api_compare/filter-list-gateway index ab772c1bf70c..2fcc7b51517f 100644 --- a/scripts/tests/api_compare/filter-list-gateway +++ b/scripts/tests/api_compare/filter-list-gateway @@ -59,3 +59,7 @@ !Filecoin.WalletVerify # broken +# https://github.com/filecoin-project/lotus/pull/13562 +!Filecoin.StateSearchMsg +# https://github.com/filecoin-project/lotus/pull/13562 +!Filecoin.EthGetTransactionReceiptLimited From 54ce2729bf3566fa02d128648c2711483e7bc119 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 7 Apr 2026 18:13:52 +0800 Subject: [PATCH 11/12] resolve comments --- src/rpc/methods/chain.rs | 16 +++++++++------- src/rpc/methods/chain/types.rs | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/rpc/methods/chain.rs b/src/rpc/methods/chain.rs index d3ede5a06446..0e619d06c6d9 100644 --- a/src/rpc/methods/chain.rs +++ b/src/rpc/methods/chain.rs @@ -1196,20 +1196,22 @@ impl ChainGetTipSetFinalityStatus { find_threshold_depth, }; + const FINALITY_CHAIN_EXTRA_EPOCHS: usize = 5; + let finality = ctx.chain_config().policy.chain_finality; - let chain_len = finality as usize + 5; + let chain_len = finality as usize + FINALITY_CHAIN_EXTRA_EPOCHS; let mut chain = Vec::with_capacity(chain_len); let mut ts = head.clone(); while chain.len() < chain_len { chain.push(ts.len() as i64); if let Ok(parent) = ctx.chain_index().load_required_tipset(ts.parents()) { // insert 0 for null rounds - for _ in 1..(ts.epoch() - parent.epoch()) { - if chain.len() < chain_len { - chain.push(0); - } else { - break; - } + if let Ok(n_null_tipsets_to_pad) = usize::try_from(ts.epoch() - parent.epoch() - 1) + && n_null_tipsets_to_pad > 0 + { + let target_len = + (chain.len().saturating_add(n_null_tipsets_to_pad)).min(chain_len); + chain.resize(target_len, 0); } ts = parent; } else { diff --git a/src/rpc/methods/chain/types.rs b/src/rpc/methods/chain/types.rs index 5f9c744995bf..eb0f2b038de0 100644 --- a/src/rpc/methods/chain/types.rs +++ b/src/rpc/methods/chain/types.rs @@ -24,14 +24,14 @@ pub struct ChainFinalityStatus { pub ec_finality_threshold_depth: i64, /// The most recent tipset where the reorg probability - /// is below 2^-30, based on observed block production. Nil if the + /// is below 2^-30, based on observed block production. [`None`] if the /// threshold is not met. #[serde(with = "crate::lotus_json")] #[schemars(with = "LotusJson>")] pub ec_finalized_tip_set: Option, /// The tipset finalized by F3 (Fast Finality), if F3 - /// is running and has issued a certificate. Nil if F3 is not available. + /// is running and has issued a certificate. [`None`] if F3 is not available. #[serde(with = "crate::lotus_json")] #[schemars(with = "LotusJson>")] pub f3_finalized_tip_set: Option, From 59425234444d1dc3f9085e29102fc634825da520 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 7 Apr 2026 19:53:28 +0800 Subject: [PATCH 12/12] fix rpc insta snapshot --- src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap index 7ba82b531127..6b6c55ffcaa0 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap @@ -1866,10 +1866,10 @@ components: type: integer format: int64 ecFinalizedTipSet: - description: "The most recent tipset where the reorg probability\nis below 2^-30, based on observed block production. Nil if the\nthreshold is not met." + description: "The most recent tipset where the reorg probability\nis below 2^-30, based on observed block production. [`None`] if the\nthreshold is not met." $ref: "#/components/schemas/Nullable_Tipset" f3FinalizedTipSet: - description: "The tipset finalized by F3 (Fast Finality), if F3\nis running and has issued a certificate. Nil if F3 is not available." + description: "The tipset finalized by F3 (Fast Finality), if F3\nis running and has issued a certificate. [`None`] if F3 is not available." $ref: "#/components/schemas/Nullable_Tipset" finalizedTipSet: description: "The overall finalized tipset used by the node,\ntaking the most recent of F3 and EC calculator results."