Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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

- [#6380](https://github.com/ChainSafe/forest/pull/6380) Implemented `Filecoin.EthFeeHistory` for API v2.

### Changed

### Removed
Expand Down
230 changes: 134 additions & 96 deletions src/rpc/methods/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2050,121 +2050,159 @@ impl RpcMethod<3> for EthFeeHistory {
ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
(EthUint64(block_count), newest_block_number, reward_percentiles): Self::Params,
) -> Result<Self::Ok, ServerError> {
if block_count > 1024 {
return Err(anyhow::anyhow!("block count should be smaller than 1024").into());
}

let reward_percentiles = reward_percentiles.unwrap_or_default();
Self::validate_reward_precentiles(&reward_percentiles)?;

let tipset = tipset_by_ext_block_number_or_hash(
ctx.chain_store(),
newest_block_number.into(),
ResolveNullTipset::TakeOlder,
)?;
let mut oldest_block_height = 1;
// NOTE: baseFeePerGas should include the next block after the newest of the returned range,
// because the next base fee can be inferred from the messages in the newest block.
// However, this is NOT the case in Filecoin due to deferred execution, so the best
// we can do is duplicate the last value.
let mut base_fee_array = vec![EthBigInt::from(
&tipset.block_headers().first().parent_base_fee,
)];
let mut rewards_array = vec![];
let mut gas_used_ratio_array = vec![];
for ts in tipset
.chain(ctx.store())
.filter(|i| i.epoch() > 0)
.take(block_count as _)
{
let base_fee = &ts.block_headers().first().parent_base_fee;
let (_state_root, messages_and_receipts) = execute_tipset(&ctx, &ts).await?;
let mut tx_gas_rewards = Vec::with_capacity(messages_and_receipts.len());
for (message, receipt) in messages_and_receipts {
let premium = message.effective_gas_premium(base_fee);
tx_gas_rewards.push(GasReward {
gas_used: receipt.gas_used(),
premium,
});
}
let (rewards, total_gas_used) =
Self::calculate_rewards_and_gas_used(&reward_percentiles, tx_gas_rewards);
let max_gas = BLOCK_GAS_LIMIT * (ts.block_headers().len() as u64);

// arrays should be reversed at the end
base_fee_array.push(EthBigInt::from(base_fee));
gas_used_ratio_array.push((total_gas_used as f64) / (max_gas as f64));
rewards_array.push(rewards);
eth_fee_history(ctx, tipset, block_count, reward_percentiles).await
}
}

oldest_block_height = ts.epoch();
}
pub enum EthFeeHistoryV2 {}

// Reverse the arrays; we collected them newest to oldest; the client expects oldest to newest.
base_fee_array.reverse();
gas_used_ratio_array.reverse();
rewards_array.reverse();

Ok(EthFeeHistoryResult {
oldest_block: EthUint64(oldest_block_height as _),
base_fee_per_gas: base_fee_array,
gas_used_ratio: gas_used_ratio_array,
reward: if reward_percentiles.is_empty() {
None
} else {
Some(rewards_array)
},
})
impl RpcMethod<3> for EthFeeHistoryV2 {
const NAME: &'static str = "Filecoin.EthFeeHistory";
const NAME_ALIAS: Option<&'static str> = Some("eth_feeHistory");
const N_REQUIRED_PARAMS: usize = 2;
const PARAM_NAMES: [&'static str; 3] = ["blockCount", "newestBlockNumber", "rewardPercentiles"];
const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::V2);
const PERMISSION: Permission = Permission::Read;

type Params = (EthUint64, ExtBlockNumberOrHash, Option<Vec<f64>>);
type Ok = EthFeeHistoryResult;

async fn handle(
ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
(EthUint64(block_count), newest_block_number, reward_percentiles): Self::Params,
) -> Result<Self::Ok, ServerError> {
let tipset = tipset_by_block_number_or_hash_v2(
&ctx,
newest_block_number,
ResolveNullTipset::TakeOlder,
)
.await?;

eth_fee_history(ctx, tipset, block_count, reward_percentiles).await
}
}

impl EthFeeHistory {
fn validate_reward_precentiles(reward_percentiles: &[f64]) -> anyhow::Result<()> {
if reward_percentiles.len() > 100 {
anyhow::bail!("length of the reward percentile array cannot be greater than 100");
async fn eth_fee_history<B: Blockstore + Send + Sync + 'static>(
ctx: Ctx<B>,
tipset: Tipset,
block_count: u64,
reward_percentiles: Option<Vec<f64>>,
) -> Result<EthFeeHistoryResult, ServerError> {
if block_count > 1024 {
return Err(anyhow::anyhow!("block count should be smaller than 1024").into());
}

let reward_percentiles = reward_percentiles.unwrap_or_default();
validate_reward_percentiles(&reward_percentiles)?;

let mut oldest_block_height = 1;
// NOTE: baseFeePerGas should include the next block after the newest of the returned range,
// because the next base fee can be inferred from the messages in the newest block.
// However, this is NOT the case in Filecoin due to deferred execution, so the best
// we can do is duplicate the last value.
let mut base_fee_array = vec![EthBigInt::from(
&tipset.block_headers().first().parent_base_fee,
)];
let mut rewards_array = vec![];
let mut gas_used_ratio_array = vec![];
for ts in tipset
.chain(ctx.store())
.filter(|i| i.epoch() > 0)
.take(block_count as _)
{
let base_fee = &ts.block_headers().first().parent_base_fee;
let (_state_root, messages_and_receipts) = execute_tipset(&ctx, &ts).await?;
let mut tx_gas_rewards = Vec::with_capacity(messages_and_receipts.len());
for (message, receipt) in messages_and_receipts {
let premium = message.effective_gas_premium(base_fee);
tx_gas_rewards.push(GasReward {
gas_used: receipt.gas_used(),
premium,
});
}
let (rewards, total_gas_used) =
calculate_rewards_and_gas_used(&reward_percentiles, tx_gas_rewards);
let max_gas = BLOCK_GAS_LIMIT * (ts.block_headers().len() as u64);

for (&rp, &rp_prev) in reward_percentiles
.iter()
.zip(std::iter::once(&0.).chain(reward_percentiles.iter()))
{
if !(0. ..=100.).contains(&rp) {
anyhow::bail!("invalid reward percentile: {rp} should be between 0 and 100");
}
if rp < rp_prev {
anyhow::bail!("invalid reward percentile: {rp} should be larger than {rp_prev}");
}
}
// arrays should be reversed at the end
base_fee_array.push(EthBigInt::from(base_fee));
gas_used_ratio_array.push((total_gas_used as f64) / (max_gas as f64));
rewards_array.push(rewards);

Ok(())
oldest_block_height = ts.epoch();
}

fn calculate_rewards_and_gas_used(
reward_percentiles: &[f64],
mut tx_gas_rewards: Vec<GasReward>,
) -> (Vec<EthBigInt>, u64) {
const MIN_GAS_PREMIUM: u64 = 100000;

let gas_used_total = tx_gas_rewards.iter().map(|i| i.gas_used).sum();
let mut rewards = reward_percentiles
.iter()
.map(|_| EthBigInt(MIN_GAS_PREMIUM.into()))
.collect_vec();
if !tx_gas_rewards.is_empty() {
tx_gas_rewards.sort_by_key(|i| i.premium.clone());
let mut idx = 0;
let mut sum = 0;
#[allow(clippy::indexing_slicing)]
for (i, &percentile) in reward_percentiles.iter().enumerate() {
let threshold = ((gas_used_total as f64) * percentile / 100.) as u64;
while sum < threshold && idx < tx_gas_rewards.len() - 1 {
sum += tx_gas_rewards[idx].gas_used;
idx += 1;
}
rewards[i] = (&tx_gas_rewards[idx].premium).into();
// Reverse the arrays; we collected them newest to oldest; the client expects oldest to newest.
base_fee_array.reverse();
gas_used_ratio_array.reverse();
rewards_array.reverse();

Ok(EthFeeHistoryResult {
oldest_block: EthUint64(oldest_block_height as _),
base_fee_per_gas: base_fee_array,
gas_used_ratio: gas_used_ratio_array,
reward: if reward_percentiles.is_empty() {
None
} else {
Some(rewards_array)
},
})
}

fn validate_reward_percentiles(reward_percentiles: &[f64]) -> anyhow::Result<()> {
if reward_percentiles.len() > 100 {
anyhow::bail!("length of the reward percentile array cannot be greater than 100");
}

for (&rp_prev, &rp) in std::iter::once(&0.0)
.chain(reward_percentiles.iter())
.tuple_windows()
{
if !(0. ..=100.).contains(&rp) {
anyhow::bail!("invalid reward percentile: {rp} should be between 0 and 100");
}
if rp < rp_prev {
anyhow::bail!(
"invalid reward percentile: {rp} should be larger than or equal to{rp_prev}"
);
}
}

Ok(())
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

fn calculate_rewards_and_gas_used(
reward_percentiles: &[f64],
mut tx_gas_rewards: Vec<GasReward>,
) -> (Vec<EthBigInt>, u64) {
const MIN_GAS_PREMIUM: u64 = 100000;

let gas_used_total = tx_gas_rewards.iter().map(|i| i.gas_used).sum();
let mut rewards = reward_percentiles
.iter()
.map(|_| EthBigInt(MIN_GAS_PREMIUM.into()))
.collect_vec();
if !tx_gas_rewards.is_empty() {
tx_gas_rewards.sort_by_key(|i| i.premium.clone());
let mut idx = 0;
let mut sum = 0;
#[allow(clippy::indexing_slicing)]
for (i, &percentile) in reward_percentiles.iter().enumerate() {
let threshold = ((gas_used_total as f64) * percentile / 100.) as u64;
while sum < threshold && idx < tx_gas_rewards.len() - 1 {
sum += tx_gas_rewards[idx].gas_used;
idx += 1;
}
rewards[i] = (&tx_gas_rewards[idx].premium).into();
}
(rewards, gas_used_total)
}
(rewards, gas_used_total)
}

pub enum EthGetCode {}
Expand Down
1 change: 1 addition & 0 deletions src/rpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ macro_rules! for_each_rpc_method {
$callback!($crate::rpc::eth::EthChainId);
$callback!($crate::rpc::eth::EthEstimateGas);
$callback!($crate::rpc::eth::EthFeeHistory);
$callback!($crate::rpc::eth::EthFeeHistoryV2);
$callback!($crate::rpc::eth::EthGasPrice);
$callback!($crate::rpc::eth::EthGetBalance);
$callback!($crate::rpc::eth::EthGetBlockByHash);
Expand Down
57 changes: 57 additions & 0 deletions src/tool/subcommands/api_cmd/api_compare_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1848,6 +1848,63 @@ fn eth_tests_with_tipset<DB: Blockstore>(store: &Arc<DB>, shared_tipset: &Tipset
))
.unwrap(),
),
RpcTest::identity(
EthFeeHistoryV2::request((
10.into(),
ExtBlockNumberOrHash::from_block_number(shared_tipset.epoch()),
None,
))
.unwrap(),
),
RpcTest::identity(
EthFeeHistoryV2::request((
10.into(),
ExtBlockNumberOrHash::from_block_number(shared_tipset.epoch()),
Some(vec![10., 50., 90.]),
))
.unwrap(),
),
RpcTest::identity(
EthFeeHistoryV2::request((
10.into(),
ExtBlockNumberOrHash::from_predefined(ExtPredefined::Earliest),
None,
))
.unwrap(),
)
.policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
RpcTest::basic(
EthFeeHistoryV2::request((
10.into(),
ExtBlockNumberOrHash::from_predefined(ExtPredefined::Pending),
Some(vec![10., 50., 90.]),
))
.unwrap(),
),
RpcTest::basic(
EthFeeHistoryV2::request((
10.into(),
ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest),
None,
))
.unwrap(),
),
RpcTest::basic(
EthFeeHistoryV2::request((
10.into(),
ExtBlockNumberOrHash::from_predefined(ExtPredefined::Safe),
None,
))
.unwrap(),
),
RpcTest::basic(
EthFeeHistoryV2::request((
10.into(),
ExtBlockNumberOrHash::from_predefined(ExtPredefined::Finalized),
Some(vec![10., 50., 90.]),
))
.unwrap(),
),
RpcTest::identity(
EthGetCode::request((
// https://filfox.info/en/address/f410fpoidg73f7krlfohnla52dotowde5p2sejxnd4mq
Expand Down
1 change: 1 addition & 0 deletions src/tool/subcommands/api_cmd/test_snapshots.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ filecoin_ethcall_v2_1765790311230270.rpcsnap.json.zst
filecoin_ethcall_v2_1765790311230334.rpcsnap.json.zst
filecoin_ethchainid_1736937942819147.rpcsnap.json.zst
filecoin_ethfeehistory_1737446676883828.rpcsnap.json.zst
filecoin_ethfeehistory_v2_1767605175056660.rpcsnap.json.zst
filecoin_ethgasprice_1758725940980141.rpcsnap.json.zst
filecoin_ethgetbalance_1740048634848277.rpcsnap.json.zst
filecoin_ethgetblockbyhash_1740132537807408.rpcsnap.json.zst
Expand Down
Loading