Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions scripts/tests/api_compare/filter-list-gateway
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

!Filecoin.AuthNew
!Filecoin.BeaconGetEntry
!Filecoin.ChainGetTipSetFinalityStatus
!Filecoin.ChainSetHead
!Filecoin.ChainStatObj
!Filecoin.ChainTipSetWeight
Expand Down
15 changes: 7 additions & 8 deletions src/chain/ec_finality/calculator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
// 450 accommodates degraded chains that take longer to finalize.
pub const BISECT_LOW: i64 = 3;
#[allow(dead_code)]
pub const BISECT_HIGH: i64 = 200;
pub const BISECT_HIGH: i64 = 450;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// 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<f64> =
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
Expand Down
24 changes: 9 additions & 15 deletions src/chain/ec_finality/calculator/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"
);

Expand All @@ -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,
Expand All @@ -145,22 +143,21 @@ fn test_calc_validator_prob_degraded_chain() {
.unwrap();
assert_ge!(
prob30,
guarantee,
*DEFAULT_GUARANTEE,
"degraded chain at depth 30 should NOT achieve 2^-30"
);
}

#[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");
Expand All @@ -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
// search range (BisectHigh=200), so threshold is not found
let chain = vec![2; 905];
let guarantee = 2_f64.powi(-30);
// All-1s chain is too degraded to achieve 2^-30 within the bisect
// search range (BisectHigh=450), so threshold is not found
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!(
Expand All @@ -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!(
Expand All @@ -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");
Expand Down
2 changes: 1 addition & 1 deletion src/chain/ec_finality/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2019-2026 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

mod calculator;
pub mod calculator;
2 changes: 1 addition & 1 deletion src/chain/mod.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down
138 changes: 134 additions & 4 deletions src/rpc/methods/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0, MIT

pub mod types;
use enumflags2::{BitFlags, make_bitflags};
use types::*;

#[cfg(test)]
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -298,7 +299,7 @@ impl RpcMethod<1> for ChainGetParentReceipts {
gas_used: r.gas_used(),
events_root: r.events_root(),
})
.collect();
.collect_vec();

Ok(receipts)
}
Expand Down Expand Up @@ -1138,6 +1139,136 @@ impl RpcMethod<1> for ChainGetTipSetV2 {
}
}

pub enum ChainGetTipSetFinalityStatus {}

impl ChainGetTipSetFinalityStatus {
pub fn get_finality_status(ctx: &Ctx<impl Blockstore>) -> 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<impl Blockstore>,
head: Tipset,
) -> (i64, Option<Tipset>) {
static CACHE: parking_lot::Mutex<Option<(Tipset, i64, Option<Tipset>)>> =
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<impl Blockstore>,
head: Tipset,
) -> (i64, Option<Tipset>) {
use crate::chain::ec_finality::calculator::{
DEFAULT_BLOCKS_PER_EPOCH, DEFAULT_BYZANTINE_FRACTION, DEFAULT_GUARANTEE,
find_threshold_depth,
};

const FINALITY_CHAIN_EXTRA_EPOCHS: usize = 5;
Comment thread
hanabi1224 marked this conversation as resolved.

let finality = ctx.chain_config().policy.chain_finality;
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
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);
}
Comment thread
hanabi1224 marked this conversation as resolved.
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,
*DEFAULT_GUARANTEE,
) {
Ok(threshold) => threshold,
Err(e) => {
tracing::error!(
"Failed to calculate EC finality threshold depth: {e:#}, chain: {chain:?}"
);
-1
}
};
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
};
(depth, finalized)
}
}

impl RpcMethod<0> for ChainGetTipSetFinalityStatus {
const NAME: &'static str = "Filecoin.ChainGetTipSetFinalityStatus";
const PARAM_NAMES: [&'static str; 0] = [];
const API_PATHS: BitFlags<ApiPaths> = 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<impl Blockstore + Send + Sync + 'static>,
(): Self::Params,
_: &http::Extensions,
) -> Result<Self::Ok, ServerError> {
Ok(Self::get_finality_status(&ctx))
}
}

pub enum ChainSetHead {}
impl RpcMethod<1> for ChainSetHead {
const NAME: &'static str = "Filecoin.ChainSetHead";
Expand Down Expand Up @@ -1541,7 +1672,7 @@ impl<T> PathChanges<T> {
.into_iter()
.map(PathChange::Revert)
.chain(applies.into_iter().map(PathChange::Apply))
.collect()
.collect_vec()
}
}

Expand Down Expand Up @@ -1582,7 +1713,6 @@ mod tests {
networks::{self, ChainConfig},
};
use PathChange::{Apply, Revert};
use itertools::Itertools as _;
use std::sync::Arc;

#[test]
Expand Down
38 changes: 38 additions & 0 deletions src/rpc/methods/chain/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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. [`None`] if the
/// threshold is not met.
#[serde(with = "crate::lotus_json")]
#[schemars(with = "LotusJson<Option<Tipset>>")]
pub ec_finalized_tip_set: Option<Tipset>,

/// The tipset finalized by F3 (Fast Finality), if F3
/// is running and has issued a certificate. [`None`] if F3 is not available.
#[serde(with = "crate::lotus_json")]
#[schemars(with = "LotusJson<Option<Tipset>>")]
pub f3_finalized_tip_set: Option<Tipset>,

/// 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<Option<Tipset>>")]
pub finalized_tip_set: Option<Tipset>,

/// The current chain head used for the computation.
#[serde(with = "crate::lotus_json")]
#[schemars(with = "LotusJson<Tipset>")]
pub head: Tipset,
}
lotus_json_with_self!(ChainFinalityStatus);
Loading
Loading