forest-filecoin 0.33.2

Rust Filecoin implementation.
Documentation
// Copyright 2019-2026 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

#![allow(clippy::excessive_precision)]

use super::*;
use all_asserts::*;
use rstest::rstest;

// The finality parameter used by the Python reference (and Filecoin mainnet).
const TEST_FINALITY: i64 = 900;

// Test vectors generated by the Python reference implementation from FRC-0089:
// https://github.com/consensus-shipyard/ec-finality-calculator (finality_calc_validator.py)
//
// Parameters: blocks_per_epoch=5.0, byzantine_fraction=0.3
// Chain: 905 epochs generated with numpy.random.default_rng(0).poisson(4.5, 905)
// Python: scipy 1.15.2, numpy 2.2.3
// current_epoch = 904, target_epoch = current_epoch - depth
const PYTHON_REFERENCE_CHAIN: [i64; 905] = [
    2, 3, 4, 3, 4, 7, 5, 6, 5, 5, 4, 2, 3, 3, 10, 7, 3, 8, 6, 3, 2, 3, 5, 3, 7, 6, 5, 3, 4, 6, 6,
    8, 6, 3, 2, 6, 5, 2, 4, 4, 4, 6, 5, 7, 8, 6, 3, 0, 10, 8, 3, 7, 4, 6, 4, 6, 5, 2, 5, 5, 7, 6,
    2, 1, 3, 5, 3, 5, 10, 4, 0, 5, 11, 6, 8, 6, 4, 8, 3, 4, 3, 2, 5, 6, 6, 5, 3, 9, 5, 2, 9, 3, 6,
    5, 4, 6, 2, 3, 4, 7, 5, 8, 2, 6, 0, 3, 5, 6, 6, 4, 3, 6, 5, 2, 3, 4, 6, 1, 5, 3, 5, 7, 2, 4,
    11, 3, 4, 8, 5, 3, 6, 6, 7, 5, 1, 2, 1, 4, 4, 5, 6, 4, 2, 6, 5, 5, 1, 2, 5, 5, 0, 4, 4, 7, 4,
    10, 6, 4, 9, 5, 5, 1, 0, 3, 7, 1, 6, 4, 3, 5, 7, 6, 10, 3, 5, 4, 1, 6, 2, 2, 2, 5, 4, 7, 4, 2,
    5, 6, 3, 8, 4, 6, 6, 5, 3, 3, 3, 2, 5, 5, 6, 6, 4, 7, 4, 1, 3, 6, 10, 3, 3, 4, 6, 3, 6, 5, 4,
    3, 7, 6, 2, 4, 2, 3, 1, 9, 5, 1, 5, 6, 4, 3, 8, 3, 6, 3, 2, 2, 1, 2, 3, 6, 2, 4, 2, 4, 5, 5, 4,
    4, 7, 8, 8, 8, 8, 6, 2, 3, 3, 4, 4, 3, 3, 1, 4, 5, 6, 3, 4, 7, 4, 1, 2, 2, 10, 2, 2, 3, 3, 5,
    4, 5, 3, 5, 1, 8, 4, 2, 6, 4, 9, 4, 7, 2, 2, 4, 4, 3, 3, 4, 7, 6, 4, 2, 8, 1, 4, 3, 4, 7, 4, 0,
    6, 7, 4, 4, 6, 3, 5, 7, 4, 8, 2, 2, 6, 4, 5, 3, 3, 3, 1, 4, 2, 4, 3, 5, 2, 3, 0, 6, 4, 7, 3, 6,
    3, 4, 4, 6, 3, 3, 2, 7, 4, 4, 5, 3, 4, 5, 3, 4, 4, 3, 4, 1, 5, 4, 4, 5, 4, 2, 4, 5, 3, 6, 5, 6,
    3, 4, 4, 4, 5, 4, 4, 5, 4, 4, 2, 5, 2, 4, 2, 1, 6, 6, 5, 5, 4, 9, 3, 2, 6, 4, 2, 4, 7, 7, 5, 5,
    7, 8, 2, 5, 4, 5, 1, 4, 5, 2, 5, 6, 5, 4, 4, 8, 5, 5, 6, 6, 0, 2, 4, 5, 5, 3, 5, 4, 8, 2, 4, 6,
    7, 3, 5, 5, 7, 1, 2, 5, 3, 10, 5, 10, 1, 10, 3, 5, 5, 2, 6, 2, 5, 4, 2, 1, 5, 9, 2, 4, 4, 2, 2,
    5, 5, 6, 4, 1, 6, 5, 5, 2, 6, 1, 9, 4, 7, 3, 8, 5, 4, 5, 6, 8, 5, 4, 3, 3, 2, 3, 3, 4, 4, 7, 7,
    3, 4, 4, 4, 6, 3, 3, 4, 5, 4, 1, 3, 8, 5, 4, 5, 7, 5, 8, 2, 7, 9, 5, 3, 7, 5, 6, 6, 5, 6, 8, 4,
    6, 3, 5, 4, 6, 2, 2, 6, 5, 4, 6, 3, 3, 4, 5, 2, 3, 3, 6, 6, 4, 5, 4, 3, 8, 4, 8, 3, 5, 3, 6, 4,
    6, 1, 3, 3, 4, 8, 5, 7, 4, 5, 5, 1, 3, 6, 5, 3, 6, 3, 5, 5, 6, 5, 6, 5, 7, 6, 4, 7, 6, 5, 3, 3,
    2, 4, 8, 4, 5, 1, 4, 8, 1, 2, 2, 2, 4, 11, 1, 3, 3, 2, 1, 7, 7, 3, 4, 5, 2, 5, 6, 3, 6, 3, 9,
    3, 0, 4, 2, 5, 4, 3, 2, 7, 4, 2, 10, 7, 4, 3, 5, 8, 5, 5, 2, 3, 3, 8, 6, 5, 6, 6, 6, 9, 3, 3,
    2, 6, 5, 4, 4, 4, 2, 5, 2, 8, 4, 3, 2, 2, 3, 3, 7, 5, 0, 7, 3, 5, 3, 3, 3, 6, 3, 3, 1, 3, 5, 7,
    5, 4, 5, 2, 4, 3, 7, 9, 2, 4, 2, 7, 6, 5, 3, 2, 6, 3, 6, 3, 5, 6, 3, 3, 3, 3, 3, 7, 5, 3, 4, 4,
    9, 5, 7, 9, 4, 9, 2, 4, 3, 1, 4, 6, 1, 3, 5, 5, 6, 4, 4, 2, 7, 7, 4, 5, 3, 1, 4, 5, 2, 4, 5, 2,
    7, 2, 11, 5, 4, 8, 6, 4, 3, 3, 6, 5, 4, 3, 4, 7, 2, 2, 2, 4, 3, 5, 4, 5, 3, 6, 5, 5, 2, 6, 1,
    11, 3, 3, 5, 5, 6, 2, 5, 3, 4, 5, 5, 7, 7, 7, 9, 3, 4, 6, 3, 3, 2, 6, 6, 1, 3, 1, 5, 7, 5, 7,
    8, 4, 5, 2, 6, 6, 5, 7, 5, 5, 6, 4, 2, 7, 6, 5, 5, 9, 4, 3, 3, 1, 1, 4, 5, 5, 6, 7, 2, 4, 6, 3,
    5, 5, 5, 4, 2, 4, 3, 3, 5, 2, 4, 4, 5, 6, 3, 6, 4, 5, 4, 5, 2, 8, 6, 5, 6, 7, 6, 2, 4, 9, 1, 3,
    5, 4, 7, 2, 5, 4, 7, 9, 2, 3, 2, 2, 7, 4, 1, 2, 6, 5, 10, 2, 4, 3,
];

// depth -> reorg probability from the Python reference.
#[rstest]
#[case(5, 1.58182730260265891863e-03)]
#[case(10, 1.67515743138728720072e-04)]
#[case(15, 2.80696481196116546684e-06)]
#[case(20, 6.84359796410981096872e-08)]
#[case(25, 1.46218662028857760238e-09)]
#[case(30, 4.62723254179747158594e-12)]
#[case(40, 3.53912692038794048900e-17)]
#[case(50, 1.37790735432279053542e-20)]
#[case(75, 2.40782990048672131651e-24)]
#[case(100, 3.21616912956779552478e-24)]
#[case(905, 1.)]
#[case(906, 1.)]
#[case(0, 1.)]
#[case(-1, 1.)]
fn test_calc_validator_prob_python_reference(#[case] depth: i64, #[case] want: f64) {
    let chain = PYTHON_REFERENCE_CHAIN.as_slice();
    let current_epoch = chain.len() as i64 - 1;
    let target_epoch = current_epoch - depth;
    let got = calc_validator_prob(
        chain,
        TEST_FINALITY,
        DEFAULT_BLOCKS_PER_EPOCH,
        DEFAULT_BYZANTINE_FRACTION,
        current_epoch,
        target_epoch,
    )
    .unwrap();
    let relative_error = (got - want).abs() / want.abs();
    assert_le!(
        relative_error,
        1e-12,
        "depth {depth}: got {got}, want {want}, relative error {relative_error}"
    );
}

#[test]
fn test_calc_validator_prob_healthy_chain() {
    // A perfectly healthy chain with 5 blocks per epoch should achieve
    // 2^-30 finality well within 30 epochs
    let chain = vec![5; 905];

    let current_epoch = chain.len() as i64 - 1;

    let prob30 = calc_validator_prob(
        &chain,
        TEST_FINALITY,
        DEFAULT_BLOCKS_PER_EPOCH,
        DEFAULT_BYZANTINE_FRACTION,
        current_epoch,
        current_epoch - 30,
    )
    .unwrap();
    assert_lt!(
        prob30,
        *DEFAULT_GUARANTEE,
        "healthy chain at depth 30 should be below 2^-30"
    );

    let prob5 = calc_validator_prob(
        &chain,
        TEST_FINALITY,
        DEFAULT_BLOCKS_PER_EPOCH,
        DEFAULT_BYZANTINE_FRACTION,
        current_epoch,
        current_epoch - 5,
    )
    .unwrap();
    assert_gt!(
        prob5,
        prob30,
        "shallower depth should have higher reorg probability"
    );
}

#[test]
fn test_calc_validator_prob_degraded_chain() {
    // A degraded chain with only 2 blocks per epoch should have much worse
    // finality than a healthy chain at the same depth
    let chain = vec![2; 905];
    let current_epoch = chain.len() as i64 - 1;

    let prob30 = calc_validator_prob(
        &chain,
        TEST_FINALITY,
        DEFAULT_BLOCKS_PER_EPOCH,
        DEFAULT_BYZANTINE_FRACTION,
        current_epoch,
        current_epoch - 30,
    )
    .unwrap();
    assert_ge!(
        prob30,
        *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 depth = find_threshold_depth(
        &chain,
        TEST_FINALITY,
        DEFAULT_BLOCKS_PER_EPOCH,
        DEFAULT_BYZANTINE_FRACTION,
        *DEFAULT_GUARANTEE,
    )
    .unwrap();
    assert_gt!(depth, 0, "healthy chain should find a threshold");
    assert_lt!(
        depth,
        35,
        "healthy chain should finalize well before depth 35"
    );
}

#[test]
fn test_find_threshold_depth_degraded_chain() {
    // 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,
        *DEFAULT_GUARANTEE,
    )
    .unwrap();
    assert_eq!(
        depth, -1,
        "severely degraded chain should not find threshold within search range"
    );
}

#[test]
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 depth = find_threshold_depth(
        &chain,
        TEST_FINALITY,
        DEFAULT_BLOCKS_PER_EPOCH,
        DEFAULT_BYZANTINE_FRACTION,
        *DEFAULT_GUARANTEE,
    )
    .unwrap();
    assert_gt!(
        depth,
        35,
        "mildly degraded chain should require more depth than healthy"
    );
    assert_lt!(
        depth,
        BISECT_HIGH,
        "mildly degraded chain should still find a threshold"
    );
}

#[rstest]
#[case(4)]
#[case(3)]
#[case(2)]
#[case(1)]
fn test_find_threshold_depth_too_short_chain(#[case] chain_len: usize) {
    let chain = vec![3; chain_len];
    let depth = find_threshold_depth(
        &chain,
        TEST_FINALITY,
        DEFAULT_BLOCKS_PER_EPOCH,
        DEFAULT_BYZANTINE_FRACTION,
        *DEFAULT_GUARANTEE,
    )
    .unwrap();
    assert_eq!(depth, -1, "input chain is too short");
}

#[test]
fn test_find_threshold_depth_too_large_guarantee() {
    let chain = vec![3; 200];
    let guarantee = 2_f64;
    let depth = find_threshold_depth(
        &chain,
        TEST_FINALITY,
        DEFAULT_BLOCKS_PER_EPOCH,
        DEFAULT_BYZANTINE_FRACTION,
        guarantee,
    )
    .unwrap();
    assert_eq!(depth, BISECT_LOW, "guarantee is too large");
}

#[rstest]
#[case(0., -0.1, f64::NEG_INFINITY)]
#[case(0., -1., f64::NEG_INFINITY)]
#[case(0., 1., f64::NEG_INFINITY)]
#[case(0., 0.1, f64::NEG_INFINITY)]
#[case(0., 0., 0.)]
fn poisson_log_prob_tests(#[case] lambda: f64, #[case] x: f64, #[case] want: f64) {
    let got = poisson_log_prob(lambda, x);
    assert_eq!(got, want);
}