Skip to content
Open
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ This is a non-mandatory release for all node operators. It enables F3 finality r

- [#6631](https://github.com/ChainSafe/forest/issues/6631): Backported F3 finality resolution to ETH v1 RPC methods.

- [#6686](https://github.com/ChainSafe/forest/issues/6686): [FIP-0115](https://github.com/filecoin-project/FIPs/pull/1233) implementation. This will go live on the next network upgrade.

### Removed

- [#6681](https://github.com/ChainSafe/forest/pull/6681): Removed `tracing-chrome` feature and all related code as it was deemed unused. If you didn't set `CHROME_TRACE_FILE` manually before, you shouldn't be affected by this change. If you were using this feature, reach out.
Expand Down
199 changes: 173 additions & 26 deletions src/chain/store/base_fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ use crate::shim::econ::{BLOCK_GAS_LIMIT, TokenAmount};
use ahash::{HashSet, HashSetExt};
use fvm_ipld_blockstore::Blockstore;

use super::weighted_quick_select::weighted_quick_select;

pub const BLOCK_GAS_TARGET_INDEX: u64 = BLOCK_GAS_LIMIT * 80 / 100 - 1;

/// Used in calculating the base fee change.
pub const BLOCK_GAS_TARGET: u64 = BLOCK_GAS_LIMIT / 2;

Expand Down Expand Up @@ -37,51 +41,92 @@ fn compute_next_base_fee(
};

// Limit absolute change at the block gas target.
if delta.abs() > BLOCK_GAS_TARGET as i64 {
delta = if delta.is_positive() {
BLOCK_GAS_TARGET as i64
} else {
-(BLOCK_GAS_TARGET as i64)
};
}
delta = delta.clamp(-(BLOCK_GAS_TARGET as i64), BLOCK_GAS_TARGET as i64);

// cap change at 12.5% (BaseFeeMaxChangeDenom) by capping delta
let change: TokenAmount = (base_fee * delta)
.div_floor(BLOCK_GAS_TARGET)
.div_floor(BASE_FEE_MAX_CHANGE_DENOM);
let mut next_base_fee = base_fee + change;
if next_base_fee.atto() < &MINIMUM_BASE_FEE.into() {
next_base_fee = TokenAmount::from_atto(MINIMUM_BASE_FEE);
}
next_base_fee
(base_fee + change).max(TokenAmount::from_atto(MINIMUM_BASE_FEE))
}

pub fn compute_base_fee<DB>(
db: &DB,
ts: &Tipset,
smoke_height: ChainEpoch,
xxx_height: ChainEpoch,
) -> Result<TokenAmount, crate::chain::Error>
where
DB: Blockstore,
{
let mut total_limit = 0;
// FIP-0115: https://github.com/filecoin-project/FIPs/pull/1233
if ts.epoch() >= xxx_height {
return compute_next_base_fee_from_premiums(db, ts);
}

compute_next_base_fee_from_utlilization(db, ts, smoke_height)
}

fn compute_next_base_fee_from_premiums<DB>(
db: &DB,
ts: &Tipset,
) -> Result<TokenAmount, crate::chain::Error>
where
DB: Blockstore,
{
let mut limits = Vec::new();
let mut premiums = Vec::new();
let mut seen = HashSet::new();
let parent_base_fee = &ts.block_headers().first().parent_base_fee;

// Add all unique messages' gas limit to get the total for the Tipset.
for b in ts.block_headers() {
let (msg1, msg2) = crate::chain::block_messages(db, b)?;
for m in msg1 {
let m_cid = m.cid();
if !seen.contains(&m_cid) {
total_limit += m.gas_limit();
seen.insert(m_cid);
let (bls_msgs, secp_msgs) = crate::chain::block_messages(db, b)?;
for m in bls_msgs
.iter()
.map(|m| m as &dyn Message)
.chain(secp_msgs.iter().map(|m| m as &dyn Message))
{
if seen.insert((m.from(), m.sequence())) {
limits.push(m.gas_limit());
premiums.push(m.effective_gas_premium(parent_base_fee));
}
}
for m in msg2 {
let m_cid = m.cid();
if !seen.contains(&m_cid) {
total_limit += m.gas_limit();
seen.insert(m_cid);
}

let percentile_premium = weighted_quick_select(premiums, limits, BLOCK_GAS_TARGET_INDEX);
Ok(compute_next_base_fee_from_premium(
parent_base_fee,
percentile_premium,
))
}

pub(crate) fn compute_next_base_fee_from_premium(
base_fee: &TokenAmount,
percentile_premium: TokenAmount,
) -> TokenAmount {
let denom = TokenAmount::from_atto(BASE_FEE_MAX_CHANGE_DENOM);
let max_adj = (base_fee + (&denom - &TokenAmount::from_atto(1))) / denom;
TokenAmount::from_atto(MINIMUM_BASE_FEE)
.max(base_fee + (&max_adj).min(&(&percentile_premium - &max_adj)))
}

fn compute_next_base_fee_from_utlilization<DB>(
db: &DB,
ts: &Tipset,
smoke_height: ChainEpoch,
) -> Result<TokenAmount, crate::chain::Error>
where
DB: Blockstore,
{
let mut total_limit = 0;
let mut seen = HashSet::new();

// Add all unique messages' gas limit to get the total for the Tipset.
for b in ts.block_headers() {
let (bls_msgs, secp_msgs) = crate::chain::block_messages(db, b)?;
for m in bls_msgs.iter().chain(secp_msgs.iter().map(|m| &m.message)) {
if seen.insert(m.cid()) {
total_limit += m.gas_limit;
}
}
}
Expand Down Expand Up @@ -174,6 +219,108 @@ mod tests {
});
let ts = Tipset::from(h0);
let smoke_height = ChainConfig::default().epoch(Height::Smoke);
assert!(compute_base_fee(&blockstore, &ts, smoke_height).is_err());
let xxx_height = ChainConfig::default().epoch(Height::Xxx);
assert!(compute_base_fee(&blockstore, &ts, smoke_height, xxx_height).is_err());
}

#[test]
fn test_next_base_fee_from_premium() {
// Test cases from the FIP-0115
// <https://github.com/filecoin-project/FIPs/blob/b84b89a34ccb3d239493392a7867d6b082193b38/FIPS/fip-0115.md#basefee_next>
let test_cases = vec![
(100, 0, 100),
(100, 13, 100),
(100, 14, 101),
(100, 26, 113),
(801, 0, 700),
(801, 20, 720),
(801, 40, 740),
(801, 60, 760),
(801, 80, 780),
(801, 100, 800),
(801, 120, 820),
(801, 140, 840),
(801, 160, 860),
(801, 180, 880),
(801, 200, 900),
(801, 201, 901),
(808, 0, 707),
(808, 1, 708),
(808, 201, 908),
(808, 202, 909),
(808, 203, 909),
];

for (base_fee, premium_p, expected) in test_cases {
let base_fee = TokenAmount::from_atto(base_fee);
let premium = TokenAmount::from_atto(premium_p);

let result = compute_next_base_fee_from_premium(&base_fee, premium);

assert_eq!(
result,
TokenAmount::from_atto(expected),
"Failed for base_fee={}, premium_p={}",
base_fee.atto(),
premium_p
);
}
}

mod quickcheck_tests {
use super::*;
use num::Zero;
use quickcheck_macros::quickcheck;

#[quickcheck]
fn prop_next_base_fee_never_below_minimum(base_fee: TokenAmount, premium: TokenAmount) {
let result = compute_next_base_fee_from_premium(&base_fee, premium);
assert!(result >= TokenAmount::from_atto(MINIMUM_BASE_FEE));
}

#[quickcheck]
fn prop_next_base_fee_change_bounded(base_fee: TokenAmount, premium: TokenAmount) -> bool {
if base_fee.is_zero() {
return true;
}
let min_fee = TokenAmount::from_atto(MINIMUM_BASE_FEE);
let result = compute_next_base_fee_from_premium(&base_fee, premium);

// maxAdj = ceil(base_fee / 8)
let denom = TokenAmount::from_atto(BASE_FEE_MAX_CHANGE_DENOM);
let max_adj = (&base_fee + &denom - TokenAmount::from_atto(1)) / &denom;

// Result should be within [max(MIN, base - max_adj), max(MIN, base + max_adj)]
let lower = min_fee.clone().max(&base_fee - &max_adj);
let upper = min_fee.max(&base_fee + &max_adj);

result >= lower && result <= upper
}

#[quickcheck]
fn prop_next_base_fee_monotonic_in_premium(
base_fee: TokenAmount,
premium1: TokenAmount,
premium2: TokenAmount,
) {
let result1 = compute_next_base_fee_from_premium(&base_fee, premium1.clone());
let result2 = compute_next_base_fee_from_premium(&base_fee, premium2.clone());

// Higher premium should give higher or equal result
if premium1 <= premium2 {
assert!(result1 <= result2);
} else {
assert!(result1 >= result2);
}
}

#[quickcheck]
fn prop_zero_premium_decreases_or_maintains_base_fee(base_fee: TokenAmount) {
let min_fee = TokenAmount::from_atto(MINIMUM_BASE_FEE);
let result = compute_next_base_fee_from_premium(&base_fee, TokenAmount::zero());

// With zero premium, base fee should decrease (or stay at minimum)
assert!(result <= base_fee || result == min_fee);
}
}
}
1 change: 1 addition & 0 deletions src/chain/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ mod chain_store;
mod errors;
pub mod index;
mod tipset_tracker;
mod weighted_quick_select;

pub use self::{base_fee::*, chain_store::*, errors::*};
Loading
Loading