Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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: 1 addition & 1 deletion scripts/tests/api_compare/.env
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 1 addition & 4 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 Expand Up @@ -59,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
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
22 changes: 8 additions & 14 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
// 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!(
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
136 changes: 132 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,134 @@ 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,
};

let finality = ctx.chain_config().policy.chain_finality;
let chain_len = finality as usize + 5;
Comment thread
hanabi1224 marked this conversation as resolved.
Outdated
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;
}
}
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 +1670,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 +1711,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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
#[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<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. Nil if F3 is not available.
Comment thread
hanabi1224 marked this conversation as resolved.
Outdated
#[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