Skip to main content

forest/rpc/methods/
eth.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4mod bloom;
5pub(crate) mod errors;
6mod eth_tx;
7pub mod filter;
8pub mod pubsub;
9pub(crate) mod pubsub_trait;
10pub mod tipset_resolver;
11pub(crate) mod trace;
12pub mod types;
13mod utils;
14
15pub use bloom::Bloom;
16pub(crate) use bloom::store_block_logs_bloom;
17use bloom::{EMPTY_BLOOM, FULL_BLOOM, accrue_eth_log, block_logs_bloom};
18pub use tipset_resolver::TipsetResolver;
19use tokio_util::sync::CancellationToken;
20
21use self::eth_tx::*;
22use self::filter::hex_str_to_epoch;
23use self::trace::types::*;
24use self::types::*;
25use super::gas;
26use crate::blocks::{Tipset, TipsetKey};
27use crate::chain::{ChainStore, compute_base_fee, index::ResolveNullTipset};
28use crate::chain_sync::NodeSyncStatus;
29use crate::cid_collections::CidHashSet;
30use crate::db::DbImpl;
31use crate::eth::{
32    EAMMethod, EVMMethod, EthChainId as EthChainIdType, EthEip1559TxArgs, EthLegacyEip155TxArgs,
33    EthLegacyHomesteadTxArgs, parse_eth_transaction,
34};
35use crate::lotus_json::{HasLotusJson, NotNullVec, lotus_json_with_self};
36use crate::message::{ChainMessage, MessageRead as _, MessageReadWrite as _, SignedMessage};
37use crate::networks::Height;
38use crate::prelude::*;
39use crate::rpc::{
40    ApiPaths, Ctx, EthEventHandler, LOOKBACK_NO_LIMIT, Permission, RpcMethod, RpcMethodExt as _,
41    error::ServerError,
42    eth::{
43        errors::{EthErrors, NULL_ROUND_CODE},
44        filter::{
45            EventRevertStatus, SkipEvent, event::EventFilter, mempool::MempoolFilter,
46            tipset::TipSetFilter,
47        },
48        utils::decode_revert_reason,
49    },
50    methods::chain::{ChainGetTipSetV2, PathChange},
51    state::ApiInvocResult,
52    types::{ApiTipsetKey, EventEntry, MessageLookup},
53};
54use crate::shim::actors::{EVMActorStateLoad as _, eam, evm, is_evm_actor, system};
55use crate::shim::address::{Address as FilecoinAddress, Protocol};
56use crate::shim::crypto::Signature;
57use crate::shim::econ::{BLOCK_GAS_LIMIT, TokenAmount};
58use crate::shim::error::ExitCode;
59use crate::shim::executor::Receipt;
60use crate::shim::fvm_shared_latest::MethodNum;
61use crate::shim::fvm_shared_latest::address::{Address as VmAddress, DelegatedAddress};
62use crate::shim::gas::GasOutputs;
63use crate::shim::message::Message;
64use crate::shim::trace::{CallReturn, ExecutionEvent};
65use crate::shim::{clock::ChainEpoch, state_tree::StateTree};
66use crate::state_manager::{ExecutedMessage, ExecutedTipset, StateManager, TipsetState, VMFlush};
67use crate::utils::cache::SizeTrackingCache;
68use crate::utils::db::BlockstoreExt as _;
69use crate::utils::encoding::from_slice_with_fallback;
70use crate::utils::misc::env::env_or_default;
71use crate::utils::multihash::prelude::*;
72use ahash::{HashMap, HashSet};
73use anyhow::{Error, Result, anyhow, bail, ensure};
74use enumflags2::{BitFlags, make_bitflags};
75use filter::{ParsedFilter, ParsedFilterTipsets};
76use fvm_ipld_encoding::{CBOR, DAG_CBOR, IPLD_RAW, RawBytes};
77use get_size2::GetSize;
78use ipld_core::ipld::Ipld;
79use nonzero_ext::nonzero;
80use num::BigInt;
81use nunny::Vec as NonEmpty;
82use schemars::JsonSchema;
83use serde::{Deserialize, Serialize};
84use std::num::NonZeroUsize;
85use std::ops::RangeInclusive;
86use std::str::FromStr;
87use std::sync::{LazyLock, OnceLock};
88use utils::{decode_payload, lookup_eth_address};
89
90static FOREST_TRACE_FILTER_MAX_RESULT: LazyLock<u64> =
91    LazyLock::new(|| env_or_default("FOREST_TRACE_FILTER_MAX_RESULT", 500));
92
93const MASKED_ID_PREFIX: [u8; 12] = [0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
94
95/// Ethereum address size in bytes.
96const ADDRESS_LENGTH: usize = 20;
97
98/// Ethereum Virtual Machine word size in bytes.
99const EVM_WORD_LENGTH: usize = 32;
100
101/// Keccak-256 of an RLP of an empty array.
102/// In Filecoin, we don't have the concept of uncle blocks but rather use tipsets to reward miners
103/// who craft blocks.
104const EMPTY_UNCLES: &str = "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347";
105
106/// Keccak-256 of the RLP of null.
107const EMPTY_ROOT: &str = "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421";
108
109/// The address used in messages to actors that have since been deleted.
110pub(crate) const REVERTED_ETH_ADDRESS: &str = "0xff0000000000000000000000ffffffffffffffff";
111
112#[derive(
113    Eq,
114    Hash,
115    PartialEq,
116    Debug,
117    Deserialize,
118    Serialize,
119    Default,
120    Clone,
121    Copy,
122    JsonSchema,
123    GetSize,
124    derive_more::From,
125    derive_more::Into,
126    derive_more::Deref,
127)]
128pub struct EthBigInt(
129    // `ethereum_types::U256` serializes as a `0x`-prefixed, leading-zero-trimmed hex string,
130    // which matches the Ethereum JSON-RPC wire format used by Lotus.
131    #[schemars(with = "String")]
132    #[get_size(ignore)]
133    ethereum_types::U256,
134);
135lotus_json_with_self!(EthBigInt);
136
137impl From<BigInt> for EthBigInt {
138    fn from(value: BigInt) -> Self {
139        (&value).into()
140    }
141}
142
143impl From<&BigInt> for EthBigInt {
144    fn from(value: &BigInt) -> Self {
145        // Eth values are non-negative, so the sign is dropped.
146        let (_sign, bytes) = value.to_bytes_be();
147        Self(ethereum_types::U256::from_big_endian(&bytes))
148    }
149}
150
151impl From<u64> for EthBigInt {
152    fn from(value: u64) -> Self {
153        Self(value.into())
154    }
155}
156
157impl From<TokenAmount> for EthBigInt {
158    fn from(amount: TokenAmount) -> Self {
159        (&amount).into()
160    }
161}
162
163impl From<&TokenAmount> for EthBigInt {
164    fn from(amount: &TokenAmount) -> Self {
165        amount.atto().into()
166    }
167}
168
169impl From<EthBigInt> for BigInt {
170    fn from(value: EthBigInt) -> Self {
171        // Eth values are non-negative, so the sign is always positive.
172        BigInt::from_bytes_be(num_bigint::Sign::Plus, &value.to_big_endian())
173    }
174}
175
176impl From<EthBigInt> for TokenAmount {
177    fn from(value: EthBigInt) -> Self {
178        TokenAmount::from_atto(value)
179    }
180}
181
182type GasPriceResult = EthBigInt;
183
184#[derive(PartialEq, Debug, Deserialize, Serialize, Default, Clone, JsonSchema, GetSize)]
185pub struct Nonce(
186    #[schemars(with = "String")]
187    #[serde(with = "crate::lotus_json::hexify_bytes")]
188    #[get_size(ignore)]
189    pub ethereum_types::H64,
190);
191lotus_json_with_self!(Nonce);
192
193#[derive(
194    Eq,
195    Hash,
196    PartialEq,
197    Debug,
198    Deserialize,
199    Serialize,
200    Default,
201    Clone,
202    Copy,
203    JsonSchema,
204    derive_more::From,
205    derive_more::Into,
206    derive_more::Deref,
207    GetSize,
208)]
209pub struct EthUint64(
210    #[schemars(with = "String")]
211    #[serde(with = "crate::lotus_json::hexify")]
212    pub u64,
213);
214
215lotus_json_with_self!(EthUint64);
216
217impl EthUint64 {
218    pub fn from_bytes(data: &[u8]) -> Result<Self> {
219        if data.len() != EVM_WORD_LENGTH {
220            bail!("eth int must be {EVM_WORD_LENGTH} bytes");
221        }
222
223        // big endian format stores u64 in the last 8 bytes,
224        // since ethereum words are 32 bytes, the first 24 bytes must be 0
225        if data
226            .get(..24)
227            .is_none_or(|slice| slice.iter().any(|&byte| byte != 0))
228        {
229            bail!("eth int overflows 64 bits");
230        }
231
232        // Extract the uint64 from the last 8 bytes
233        Ok(Self(u64::from_be_bytes(
234            data.get(24..EVM_WORD_LENGTH)
235                .ok_or_else(|| anyhow::anyhow!("data too short"))?
236                .try_into()?,
237        )))
238    }
239
240    pub fn to_hex_string(self) -> String {
241        format!("0x{}", hex::encode(self.0.to_be_bytes()))
242    }
243}
244
245#[derive(
246    PartialEq,
247    Debug,
248    Deserialize,
249    Serialize,
250    Default,
251    Clone,
252    Copy,
253    JsonSchema,
254    derive_more::From,
255    derive_more::Into,
256    derive_more::Deref,
257    GetSize,
258)]
259pub struct EthInt64(
260    #[schemars(with = "String")]
261    #[serde(with = "crate::lotus_json::hexify")]
262    pub i64,
263);
264
265lotus_json_with_self!(EthInt64);
266
267impl EthHash {
268    // Should ONLY be used for blocks and Filecoin messages. Eth transactions expect a different hashing scheme.
269    pub fn to_cid(self) -> cid::Cid {
270        let mh = MultihashCode::Blake2b256
271            .wrap(self.0.as_bytes())
272            .expect("should not fail");
273        Cid::new_v1(DAG_CBOR, mh)
274    }
275
276    pub fn empty_uncles() -> Self {
277        Self(ethereum_types::H256::from_str(EMPTY_UNCLES).unwrap())
278    }
279
280    pub fn empty_root() -> Self {
281        Self(ethereum_types::H256::from_str(EMPTY_ROOT).unwrap())
282    }
283}
284
285impl From<Cid> for EthHash {
286    fn from(cid: Cid) -> Self {
287        let (_, digest, _) = cid.hash().into_inner();
288        EthHash(ethereum_types::H256::from_slice(&digest[0..32]))
289    }
290}
291
292impl From<[u8; EVM_WORD_LENGTH]> for EthHash {
293    fn from(value: [u8; EVM_WORD_LENGTH]) -> Self {
294        Self(ethereum_types::H256(value))
295    }
296}
297
298#[derive(
299    PartialEq,
300    Debug,
301    Clone,
302    Copy,
303    Serialize,
304    Deserialize,
305    Default,
306    JsonSchema,
307    strum::Display,
308    strum::EnumString,
309)]
310#[strum(serialize_all = "lowercase")]
311#[serde(rename_all = "lowercase")]
312pub enum Predefined {
313    Earliest,
314    Pending,
315    #[default]
316    Latest,
317    Safe,
318    Finalized,
319}
320
321#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)]
322#[serde(rename_all = "camelCase")]
323pub struct BlockNumber {
324    block_number: EthInt64,
325}
326
327#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)]
328#[serde(rename_all = "camelCase")]
329pub struct BlockHash {
330    block_hash: EthHash,
331    #[serde(default)]
332    require_canonical: bool,
333}
334
335#[derive(
336    PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema, strum::Display, derive_more::From,
337)]
338#[serde(untagged)]
339pub enum BlockNumberOrHash {
340    #[schemars(with = "String")]
341    PredefinedBlock(Predefined),
342    BlockNumber(EthInt64),
343    BlockHash(EthHash),
344    BlockNumberObject(BlockNumber),
345    BlockHashObject(BlockHash),
346}
347lotus_json_with_self!(BlockNumberOrHash);
348
349impl BlockNumberOrHash {
350    pub fn from_block_number(number: i64) -> Self {
351        Self::BlockNumber(EthInt64(number))
352    }
353
354    /// Construct a block number using EIP-1898 Object scheme.
355    ///
356    /// For details see <https://eips.ethereum.org/EIPS/eip-1898>
357    pub fn from_block_number_object(number: i64) -> Self {
358        Self::BlockNumberObject(BlockNumber {
359            block_number: EthInt64(number),
360        })
361    }
362
363    /// Construct a block hash using EIP-1898 Object scheme.
364    ///
365    /// For details see <https://eips.ethereum.org/EIPS/eip-1898>
366    pub fn from_block_hash_object(hash: EthHash, require_canonical: bool) -> Self {
367        Self::BlockHashObject(BlockHash {
368            block_hash: hash,
369            require_canonical,
370        })
371    }
372
373    pub fn from_str(s: &str) -> Result<Self, Error> {
374        if s.starts_with("0x") {
375            let epoch = hex_str_to_epoch(s)?;
376            return Ok(BlockNumberOrHash::from_block_number(epoch));
377        }
378        s.parse::<Predefined>()
379            .map_err(|_| anyhow!("Invalid block identifier"))
380            .map(BlockNumberOrHash::from)
381    }
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, GetSize)]
385#[serde(untagged)] // try a Vec<String>, then a Vec<Tx>
386pub enum Transactions {
387    Hash(Vec<String>),
388    Full(Vec<ApiEthTx>),
389}
390
391impl Transactions {
392    pub fn is_empty(&self) -> bool {
393        match self {
394            Self::Hash(v) => v.is_empty(),
395            Self::Full(v) => v.is_empty(),
396        }
397    }
398}
399
400impl PartialEq for Transactions {
401    fn eq(&self, other: &Self) -> bool {
402        match (self, other) {
403            (Self::Hash(a), Self::Hash(b)) => a == b,
404            (Self::Full(a), Self::Full(b)) => a == b,
405            _ => self.is_empty() && other.is_empty(),
406        }
407    }
408}
409
410impl Default for Transactions {
411    fn default() -> Self {
412        Self::Hash(vec![])
413    }
414}
415
416#[derive(PartialEq, Debug, Clone, Default, Serialize, Deserialize, JsonSchema, GetSize)]
417#[serde(rename_all = "camelCase")]
418pub struct Block {
419    pub hash: EthHash,
420    pub parent_hash: EthHash,
421    pub sha3_uncles: EthHash,
422    pub miner: EthAddress,
423    pub state_root: EthHash,
424    pub transactions_root: EthHash,
425    pub receipts_root: EthHash,
426    pub logs_bloom: Bloom,
427    pub difficulty: EthUint64,
428    pub total_difficulty: EthUint64,
429    pub number: EthInt64,
430    pub gas_limit: EthUint64,
431    pub gas_used: EthUint64,
432    pub timestamp: EthUint64,
433    pub extra_data: EthBytes,
434    pub mix_hash: EthHash,
435    pub nonce: Nonce,
436    pub base_fee_per_gas: EthBigInt,
437    pub size: EthUint64,
438    // can be Vec<Tx> or Vec<String> depending on query params
439    pub transactions: Transactions,
440    pub uncles: Vec<EthHash>,
441}
442
443/// Specifies the level of detail for transactions in Ethereum blocks.
444#[derive(Debug, Clone, Copy, PartialEq, Eq)]
445pub enum TxInfo {
446    /// Return only transaction hashes
447    Hash,
448    /// Return full transaction objects
449    Full,
450}
451
452impl From<bool> for TxInfo {
453    fn from(full: bool) -> Self {
454        if full { TxInfo::Full } else { TxInfo::Hash }
455    }
456}
457
458impl Block {
459    pub fn new(has_transactions: bool, tipset_len: usize) -> Self {
460        Self {
461            gas_limit: EthUint64(BLOCK_GAS_LIMIT.saturating_mul(tipset_len as _)),
462            logs_bloom: Bloom(ethereum_types::Bloom(FULL_BLOOM)),
463            sha3_uncles: EthHash::empty_uncles(),
464            transactions_root: if has_transactions {
465                EthHash::default()
466            } else {
467                EthHash::empty_root()
468            },
469            ..Default::default()
470        }
471    }
472
473    /// Creates a new Ethereum block from a Filecoin tipset, executing transactions if requested.
474    ///
475    /// Reference: <https://github.com/filecoin-project/lotus/blob/941455f1d23e73b9ee92a1a4ce745d8848969858/node/impl/eth/utils.go#L44>
476    pub async fn from_filecoin_tipset(
477        state_manager: &StateManager,
478        tipset: crate::blocks::Tipset,
479        tx_info: TxInfo,
480    ) -> Result<Arc<Self>> {
481        static ETH_BLOCK_HASH_TX_CACHE: LazyLock<SizeTrackingCache<CidWrapper, Arc<Block>>> =
482            LazyLock::new(|| {
483                SizeTrackingCache::new_with_metrics("eth_block_hash_tx", Block::block_cache_size())
484            });
485
486        match tx_info {
487            TxInfo::Full => Self::from_filecoin_tipset_with_full_tx(state_manager, tipset).await,
488            TxInfo::Hash => {
489                let block_cid = tipset.key().cid()?;
490                ETH_BLOCK_HASH_TX_CACHE
491                    .get_or_insert_async(&CidWrapper::from(block_cid), async move {
492                        let block_with_full_tx =
493                            Self::from_filecoin_tipset_with_full_tx(state_manager, tipset).await?;
494                        Ok(Arc::new(
495                            Arc::unwrap_or_clone(block_with_full_tx)
496                                .downcast_full_transaction_to_hash(),
497                        ))
498                    })
499                    .await
500            }
501        }
502    }
503
504    async fn from_filecoin_tipset_with_full_tx(
505        state_manager: &StateManager,
506        tipset: crate::blocks::Tipset,
507    ) -> Result<Arc<Self>> {
508        static ETH_BLOCK_FULL_TX_CACHE: LazyLock<SizeTrackingCache<CidWrapper, Arc<Block>>> =
509            LazyLock::new(|| {
510                SizeTrackingCache::new_with_metrics("eth_block_full_tx", Block::block_cache_size())
511            });
512
513        let block_cid = tipset.key().cid()?;
514        ETH_BLOCK_FULL_TX_CACHE
515            .get_or_insert_async(&CidWrapper::from(block_cid), async move {
516                let parent_cid = tipset.parents().cid()?;
517                let block_number = EthInt64(tipset.epoch());
518                let block_hash: EthHash = block_cid.into();
519
520                let ExecutedTipset {
521                    state_root,
522                    executed_messages,
523                    ..
524                } = state_manager.load_executed_tipset_for_rpc(&tipset).await?;
525                let has_transactions = !executed_messages.is_empty();
526                let state_tree = state_manager.get_state_tree(&state_root)?;
527
528                let mut full_transactions = vec![];
529                let mut gas_used = 0;
530                for (
531                    i,
532                    ExecutedMessage {
533                        message, receipt, ..
534                    },
535                ) in executed_messages.iter().enumerate()
536                {
537                    let ti = EthUint64(i as u64);
538                    gas_used += receipt.gas_used();
539                    let mut tx = match message {
540                        ChainMessage::Signed(smsg) => new_eth_tx_from_signed_message(
541                            smsg,
542                            &state_tree,
543                            state_manager.chain_config().eth_chain_id,
544                        )?,
545                        ChainMessage::Unsigned(msg) => {
546                            let tx = eth_tx_from_native_message(
547                                msg,
548                                &state_tree,
549                                state_manager.chain_config().eth_chain_id,
550                            )?;
551                            ApiEthTx {
552                                hash: msg.cid().into(),
553                                ..tx
554                            }
555                        }
556                    };
557                    tx.block_hash = block_hash;
558                    tx.block_number = block_number;
559                    tx.transaction_index = ti;
560                    full_transactions.push(tx);
561                }
562
563                let logs_bloom =
564                    block_logs_bloom(state_manager, &tipset, &state_root, &executed_messages)?;
565
566                Ok(Arc::new(Block {
567                    hash: block_hash,
568                    number: block_number,
569                    parent_hash: parent_cid.into(),
570                    timestamp: EthUint64(tipset.block_headers().first().timestamp),
571                    base_fee_per_gas: tipset
572                        .block_headers()
573                        .first()
574                        .parent_base_fee
575                        .clone()
576                        .into(),
577                    gas_used: EthUint64(gas_used),
578                    transactions: Transactions::Full(full_transactions),
579                    logs_bloom,
580                    ..Block::new(has_transactions, tipset.len())
581                }))
582            })
583            .await
584    }
585
586    fn block_cache_size() -> NonZeroUsize {
587        const DEFAULT_CACHE_SIZE: NonZeroUsize = nonzero!(500usize);
588        static CACHE_SIZE: std::sync::LazyLock<NonZeroUsize> = std::sync::LazyLock::new(|| {
589            std::env::var("FOREST_ETH_BLOCK_CACHE_SIZE")
590                .ok()
591                .and_then(|s| s.parse().ok())
592                .unwrap_or(DEFAULT_CACHE_SIZE)
593        });
594        *CACHE_SIZE
595    }
596
597    fn downcast_full_transaction_to_hash(mut self) -> Self {
598        if let Transactions::Full(transactions) = &self.transactions {
599            self.transactions =
600                Transactions::Hash(transactions.iter().map(|tx| tx.hash.to_string()).collect())
601        }
602        self
603    }
604}
605
606lotus_json_with_self!(Block);
607
608#[derive(PartialEq, Debug, Clone, Default, Serialize, Deserialize, JsonSchema, GetSize)]
609#[serde(rename_all = "camelCase")]
610pub struct ApiEthTx {
611    pub chain_id: EthUint64,
612    pub nonce: EthUint64,
613    pub hash: EthHash,
614    pub block_hash: EthHash,
615    pub block_number: EthInt64,
616    pub transaction_index: EthUint64,
617    pub from: EthAddress,
618    // No `skip_serializing_if` (unlike the other `Option` fields): contract-creation txs must
619    // emit `"to": null` rather than drop the key.
620    #[serde(default)]
621    pub to: Option<EthAddress>,
622    pub value: EthBigInt,
623    pub r#type: EthUint64,
624    pub input: EthBytes,
625    pub gas: EthUint64,
626    #[serde(skip_serializing_if = "Option::is_none", default)]
627    pub max_fee_per_gas: Option<EthBigInt>,
628    #[serde(skip_serializing_if = "Option::is_none", default)]
629    pub max_priority_fee_per_gas: Option<EthBigInt>,
630    #[serde(skip_serializing_if = "Option::is_none", default)]
631    pub gas_price: Option<EthBigInt>,
632    #[schemars(with = "Vec<EthHash>")]
633    #[serde(
634        default,
635        skip_serializing_if = "Option::is_none",
636        serialize_with = "crate::lotus_json::serialize",
637        deserialize_with = "crate::lotus_json::deserialize_empty_not_null_opt"
638    )]
639    pub access_list: Option<NotNullVec<EthHash>>,
640    pub v: EthBigInt,
641    pub r: EthBigInt,
642    pub s: EthBigInt,
643}
644lotus_json_with_self!(ApiEthTx);
645
646impl ApiEthTx {
647    fn gas_fee_cap(&self) -> anyhow::Result<EthBigInt> {
648        self.max_fee_per_gas
649            .as_ref()
650            .or(self.gas_price.as_ref())
651            .cloned()
652            .context("gas fee cap is not set")
653    }
654
655    fn gas_premium(&self) -> anyhow::Result<EthBigInt> {
656        self.max_priority_fee_per_gas
657            .as_ref()
658            .or(self.gas_price.as_ref())
659            .cloned()
660            .context("gas premium is not set")
661    }
662}
663
664#[derive(Debug, Clone, Default, PartialEq, Eq)]
665pub struct EthSyncingResult {
666    pub done_sync: bool,
667    pub starting_block: i64,
668    pub current_block: i64,
669    pub highest_block: i64,
670}
671
672#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
673#[serde(untagged)]
674pub enum EthSyncingResultLotusJson {
675    DoneSync(bool),
676    Syncing {
677        #[schemars(with = "i64")]
678        #[serde(rename = "startingBlock", with = "crate::lotus_json::hexify")]
679        starting_block: i64,
680        #[schemars(with = "i64")]
681        #[serde(rename = "currentBlock", with = "crate::lotus_json::hexify")]
682        current_block: i64,
683        #[schemars(with = "i64")]
684        #[serde(rename = "highestBlock", with = "crate::lotus_json::hexify")]
685        highest_block: i64,
686    },
687}
688
689// TODO(forest): https://github.com/ChainSafe/forest/issues/4032
690//               this shouldn't exist
691impl HasLotusJson for EthSyncingResult {
692    type LotusJson = EthSyncingResultLotusJson;
693
694    #[cfg(test)]
695    fn snapshots() -> Vec<(serde_json::Value, Self)> {
696        vec![]
697    }
698
699    fn into_lotus_json(self) -> Self::LotusJson {
700        match self {
701            Self {
702                done_sync: false,
703                starting_block,
704                current_block,
705                highest_block,
706            } => EthSyncingResultLotusJson::Syncing {
707                starting_block,
708                current_block,
709                highest_block,
710            },
711            _ => EthSyncingResultLotusJson::DoneSync(false),
712        }
713    }
714
715    fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
716        match lotus_json {
717            EthSyncingResultLotusJson::DoneSync(syncing) => {
718                if syncing {
719                    // Dangerous to panic here, log error instead.
720                    tracing::error!("Invalid EthSyncingResultLotusJson: {syncing}");
721                }
722                Self {
723                    done_sync: true,
724                    ..Default::default()
725                }
726            }
727            EthSyncingResultLotusJson::Syncing {
728                starting_block,
729                current_block,
730                highest_block,
731            } => Self {
732                done_sync: false,
733                starting_block,
734                current_block,
735                highest_block,
736            },
737        }
738    }
739}
740
741#[derive(PartialEq, Debug, Default, Clone, Serialize, Deserialize, JsonSchema, GetSize)]
742#[serde(rename_all = "camelCase")]
743pub struct EthTxReceipt {
744    transaction_hash: EthHash,
745    transaction_index: EthUint64,
746    block_hash: EthHash,
747    block_number: EthInt64,
748    from: EthAddress,
749    to: Option<EthAddress>,
750    root: EthHash,
751    status: EthUint64,
752    contract_address: Option<EthAddress>,
753    cumulative_gas_used: EthUint64,
754    gas_used: EthUint64,
755    effective_gas_price: EthBigInt,
756    logs_bloom: EthBytes,
757    logs: Vec<EthLog>,
758    r#type: EthUint64,
759}
760lotus_json_with_self!(EthTxReceipt);
761
762impl EthTxReceipt {
763    fn new() -> Self {
764        Self {
765            logs_bloom: EthBytes(EMPTY_BLOOM.to_vec()),
766            ..Self::default()
767        }
768    }
769}
770
771/// Represents the results of an event filter execution.
772#[derive(PartialEq, Debug, Default, Clone, Serialize, Deserialize, JsonSchema, GetSize)]
773#[serde(rename_all = "camelCase")]
774pub struct EthLog {
775    /// The address of the actor that produced the event log.
776    address: EthAddress,
777    /// The value of the event log, excluding topics.
778    data: EthBytes,
779    /// List of topics associated with the event log.
780    topics: Vec<EthHash>,
781    /// Indicates whether the log was removed due to a chain reorganization.
782    removed: bool,
783    /// The index of the event log in the sequence of events produced by the message execution.
784    /// (this is the index in the events AMT on the message receipt)
785    log_index: EthUint64,
786    /// The index in the tipset of the transaction that produced the event log.
787    /// The index corresponds to the sequence of messages produced by `ChainGetParentMessages`
788    transaction_index: EthUint64,
789    /// The hash of the RLP message that produced the event log.
790    transaction_hash: EthHash,
791    /// The hash of the tipset containing the message that produced the log.
792    block_hash: EthHash,
793    /// The epoch of the tipset containing the message.
794    block_number: EthUint64,
795}
796lotus_json_with_self!(EthLog);
797
798pub enum Web3ClientVersion {}
799impl RpcMethod<0> for Web3ClientVersion {
800    const NAME: &'static str = "Filecoin.Web3ClientVersion";
801    const NAME_ALIAS: Option<&'static str> = Some("web3_clientVersion");
802    const PARAM_NAMES: [&'static str; 0] = [];
803    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
804    const PERMISSION: Permission = Permission::Read;
805    const DESCRIPTION: &'static str = "Returns the client version string of the running node.";
806
807    type Params = ();
808    type Ok = Arc<str>;
809
810    async fn handle(
811        _: Ctx,
812        (): Self::Params,
813        _: &http::Extensions,
814    ) -> Result<Self::Ok, ServerError> {
815        // Version string is baked in at build time; cache once.
816        static CACHED: OnceLock<Arc<str>> = OnceLock::new();
817        Ok(CACHED
818            .get_or_init(|| {
819                Arc::<str>::from(format!(
820                    "forest/{}",
821                    *crate::utils::version::FOREST_VERSION_STRING
822                ))
823            })
824            .clone())
825    }
826}
827
828pub enum EthAccounts {}
829impl RpcMethod<0> for EthAccounts {
830    const NAME: &'static str = "Filecoin.EthAccounts";
831    const NAME_ALIAS: Option<&'static str> = Some("eth_accounts");
832    const PARAM_NAMES: [&'static str; 0] = [];
833    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
834    const PERMISSION: Permission = Permission::Read;
835    const DESCRIPTION: &'static str = "Returns the list of addresses owned by the client; always empty since Forest does not manage private keys.";
836
837    type Params = ();
838    type Ok = NotNullVec<String>;
839
840    async fn handle(
841        _: Ctx,
842        (): Self::Params,
843        _: &http::Extensions,
844    ) -> Result<Self::Ok, ServerError> {
845        // EthAccounts will always return [] since we don't expect Forest to manage private keys
846        Ok(NotNullVec(vec![]))
847    }
848}
849
850pub enum EthBaseFee {}
851
852impl EthBaseFee {
853    fn get_base_fee(ctx: &Ctx, ts: &Tipset) -> anyhow::Result<TokenAmount> {
854        let heights = &ctx.chain_config().height_infos;
855        let smoke_height = heights
856            .get(&Height::Smoke)
857            .context("Missing Smoke height")?
858            .epoch;
859        let firehorse_height = heights
860            .get(&Height::FireHorse)
861            .context("Missing FireHorse height")?
862            .epoch;
863        compute_base_fee(ctx.db(), ts, smoke_height, firehorse_height)
864            .context("failed to compute base fee for eth_baseFee")
865    }
866}
867
868impl RpcMethod<0> for EthBaseFee {
869    const NAME: &'static str = "Filecoin.EthBaseFee";
870    const NAME_ALIAS: Option<&'static str> = Some("eth_baseFee");
871    const PARAM_NAMES: [&'static str; 0] = [];
872    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
873    const PERMISSION: Permission = Permission::Read;
874    const DESCRIPTION: &'static str =
875        "Returns the calculated base fee of the upcoming tipset in attoFIL";
876
877    type Params = ();
878    type Ok = EthBigInt;
879
880    async fn handle(
881        ctx: Ctx,
882        (): Self::Params,
883        _: &http::Extensions,
884    ) -> Result<Self::Ok, ServerError> {
885        let base_fee = Self::get_base_fee(&ctx, &ctx.chain_store().heaviest_tipset())?;
886        Ok(base_fee.atto().into())
887    }
888}
889
890pub enum BaseFeeByHeight {}
891impl RpcMethod<1> for BaseFeeByHeight {
892    const NAME: &'static str = "Forest.BaseFeeByHeight";
893    const NAME_ALIAS: Option<&'static str> = None;
894    const PARAM_NAMES: [&'static str; 1] = ["height"];
895    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
896    const PERMISSION: Permission = Permission::Read;
897    const DESCRIPTION: &'static str =
898        "Returns the calculated upcoming base fee of the tipset at the given height in attoFIL";
899
900    type Params = (ChainEpoch,);
901    type Ok = EthBigInt;
902
903    async fn handle(
904        ctx: Ctx,
905        (height,): Self::Params,
906        _: &http::Extensions,
907    ) -> Result<Self::Ok, ServerError> {
908        let ts = ctx
909            .chain_index()
910            .load_required_tipset_by_height(
911                height,
912                ctx.chain_store().heaviest_tipset(),
913                ResolveNullTipset::TakeOlder,
914            )
915            .await?;
916        let base_fee = EthBaseFee::get_base_fee(&ctx, &ts)?;
917        Ok(base_fee.atto().into())
918    }
919}
920
921pub enum EthBlockNumber {}
922impl RpcMethod<0> for EthBlockNumber {
923    const NAME: &'static str = "Filecoin.EthBlockNumber";
924    const NAME_ALIAS: Option<&'static str> = Some("eth_blockNumber");
925    const PARAM_NAMES: [&'static str; 0] = [];
926    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
927    const PERMISSION: Permission = Permission::Read;
928    const DESCRIPTION: &'static str = "Returns the height of the latest executed tipset, which is the parent of the current head.";
929
930    type Params = ();
931    type Ok = EthUint64;
932
933    async fn handle(
934        ctx: Ctx,
935        (): Self::Params,
936        _: &http::Extensions,
937    ) -> Result<Self::Ok, ServerError> {
938        // `eth_block_number` needs to return the height of the latest committed tipset.
939        // Ethereum clients expect all transactions included in this block to have execution outputs.
940        // This is the parent of the head tipset. The head tipset is speculative, has not been
941        // recognized by the network, and its messages are only included, not executed.
942        // See https://github.com/filecoin-project/ref-fvm/issues/1135.
943        let heaviest = ctx.chain_store().heaviest_tipset();
944        if heaviest.epoch() == 0 {
945            // We're at genesis.
946            return Ok(EthUint64::default());
947        }
948        // First non-null parent.
949        let effective_parent = heaviest.parents();
950        if let Ok(Some(parent)) = ctx.chain_index().load_tipset(effective_parent) {
951            Ok((parent.epoch() as u64).into())
952        } else {
953            Ok(EthUint64::default())
954        }
955    }
956}
957
958pub enum EthChainId {}
959impl RpcMethod<0> for EthChainId {
960    const NAME: &'static str = "Filecoin.EthChainId";
961    const NAME_ALIAS: Option<&'static str> = Some("eth_chainId");
962    const PARAM_NAMES: [&'static str; 0] = [];
963    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
964    const PERMISSION: Permission = Permission::Read;
965    const DESCRIPTION: &'static str = "Returns the EIP-155 chain ID of the current network.";
966
967    type Params = ();
968    type Ok = Arc<str>;
969
970    async fn handle(
971        ctx: Ctx,
972        (): Self::Params,
973        _: &http::Extensions,
974    ) -> Result<Self::Ok, ServerError> {
975        // `eth_chain_id` is fixed for the process lifetime; cache the hex form.
976        static CACHED: OnceLock<Arc<str>> = OnceLock::new();
977        Ok(CACHED
978            .get_or_init(|| Arc::<str>::from(format!("{:#x}", ctx.chain_config().eth_chain_id)))
979            .clone())
980    }
981}
982
983pub enum EthGasPrice {}
984impl RpcMethod<0> for EthGasPrice {
985    const NAME: &'static str = "Filecoin.EthGasPrice";
986    const NAME_ALIAS: Option<&'static str> = Some("eth_gasPrice");
987    const PARAM_NAMES: [&'static str; 0] = [];
988    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
989    const PERMISSION: Permission = Permission::Read;
990    const DESCRIPTION: &'static str = "Returns the current gas price in attoFIL";
991
992    type Params = ();
993    type Ok = GasPriceResult;
994
995    async fn handle(
996        ctx: Ctx,
997        (): Self::Params,
998        _: &http::Extensions,
999    ) -> Result<Self::Ok, ServerError> {
1000        // According to Geth's implementation, eth_gasPrice should return base + tip
1001        // Ref: https://github.com/ethereum/pm/issues/328#issuecomment-853234014
1002        let ts = ctx.chain_store().heaviest_tipset();
1003        let block0 = ts.block_headers().first();
1004        let base_fee = block0.parent_base_fee.atto();
1005        let tip = crate::rpc::gas::estimate_gas_premium(&ctx, 0, &ApiTipsetKey(None))
1006            .await
1007            .map(|gas_premium| gas_premium.atto().to_owned())
1008            .unwrap_or_default();
1009        Ok((base_fee + tip).into())
1010    }
1011}
1012
1013pub enum EthGetBalance {}
1014impl RpcMethod<2> for EthGetBalance {
1015    const NAME: &'static str = "Filecoin.EthGetBalance";
1016    const NAME_ALIAS: Option<&'static str> = Some("eth_getBalance");
1017    const PARAM_NAMES: [&'static str; 2] = ["address", "blockParam"];
1018    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1019    const PERMISSION: Permission = Permission::Read;
1020    const DESCRIPTION: &'static str =
1021        "Returns the balance of an Ethereum address at the specified block state";
1022
1023    type Params = (EthAddress, BlockNumberOrHash);
1024    type Ok = EthBigInt;
1025
1026    async fn handle(
1027        ctx: Ctx,
1028        (address, block_param): Self::Params,
1029        ext: &http::Extensions,
1030    ) -> Result<Self::Ok, ServerError> {
1031        let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
1032        let ts = resolver
1033            .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
1034            .await?;
1035        let balance = eth_get_balance(&ctx, &address, &ts).await?;
1036        Ok(balance)
1037    }
1038}
1039
1040async fn eth_get_balance(ctx: &Ctx, address: &EthAddress, ts: &Tipset) -> Result<EthBigInt> {
1041    let fil_addr = address.to_filecoin_address()?;
1042    let TipsetState { state_root, .. } = ctx.state_manager.load_tipset_state(ts).await?;
1043    let state_tree = ctx.state_manager.get_state_tree(&state_root)?;
1044    match state_tree.get_actor(&fil_addr)? {
1045        Some(actor) => Ok(actor.balance.atto().into()),
1046        None => Ok(EthBigInt::default()), // Balance is 0 if the actor doesn't exist
1047    }
1048}
1049
1050fn get_tipset_from_hash(chain_store: &ChainStore, block_hash: &EthHash) -> anyhow::Result<Tipset> {
1051    let tsk = chain_store.get_required_tipset_key(block_hash)?;
1052    Ok(chain_store.chain_index().load_required_tipset(&tsk)?)
1053}
1054
1055async fn resolve_block_number_tipset(
1056    chain: &ChainStore,
1057    block_number: EthInt64,
1058    resolve: ResolveNullTipset,
1059) -> anyhow::Result<Tipset> {
1060    let head = chain.heaviest_tipset();
1061    let height = ChainEpoch::from(block_number.0);
1062    if height > head.epoch() - 1 {
1063        bail!("requested a future epoch (beyond \"latest\")");
1064    }
1065    chain
1066        .chain_index()
1067        .load_required_tipset_by_height(height, head, resolve)
1068        .await
1069        .map_err(|e| match e {
1070            crate::chain::store::Error::NullRound(epoch) => EthErrors::null_round(epoch).into(),
1071            e => e.into(),
1072        })
1073}
1074
1075async fn resolve_block_hash_tipset(
1076    chain: &ChainStore,
1077    block_hash: &EthHash,
1078    require_canonical: bool,
1079    resolve: ResolveNullTipset,
1080) -> anyhow::Result<Tipset> {
1081    let ts = get_tipset_from_hash(chain, block_hash)?;
1082    // verify that the tipset is in the canonical chain
1083    if require_canonical {
1084        // walk up the current chain (our head) until we reach ts.epoch()
1085        let walk_ts = chain
1086            .chain_index()
1087            .load_required_tipset_by_height(ts.epoch(), chain.heaviest_tipset(), resolve)
1088            .await?;
1089        // verify that it equals the expected tipset
1090        if walk_ts != ts {
1091            bail!("tipset is not canonical");
1092        }
1093    }
1094    Ok(ts)
1095}
1096
1097pub fn is_eth_address(addr: &VmAddress) -> bool {
1098    if addr.protocol() != Protocol::Delegated {
1099        return false;
1100    }
1101    let f4_addr: Result<DelegatedAddress, _> = addr.payload().try_into();
1102
1103    f4_addr.is_ok()
1104}
1105
1106/// `eth_tx_from_signed_eth_message` does NOT populate:
1107/// - `hash`
1108/// - `block_hash`
1109/// - `block_number`
1110/// - `transaction_index`
1111pub fn eth_tx_from_signed_eth_message(
1112    smsg: &SignedMessage,
1113    chain_id: EthChainIdType,
1114) -> Result<(EthAddress, EthTx)> {
1115    // The from address is always an f410f address, never an ID or other address.
1116    let from = smsg.message().from;
1117    if !is_eth_address(&from) {
1118        bail!("sender must be an eth account, was {from}");
1119    }
1120    // This should be impossible to fail as we've already asserted that we have an
1121    // Ethereum Address sender...
1122    let from = EthAddress::from_filecoin_address(&from)?;
1123    let tx = EthTx::from_signed_message(chain_id, smsg)?;
1124    Ok((from, tx))
1125}
1126
1127/// See <https://docs.soliditylang.org/en/latest/abi-spec.html#function-selector-and-argument-encoding>
1128/// for ABI specification
1129fn encode_filecoin_params_as_abi(
1130    method: MethodNum,
1131    codec: u64,
1132    params: &fvm_ipld_encoding::RawBytes,
1133) -> Result<EthBytes> {
1134    let mut buffer: Vec<u8> = vec![0x86, 0x8e, 0x10, 0xc4];
1135    buffer.append(&mut encode_filecoin_returns_as_abi(method, codec, params));
1136    Ok(EthBytes(buffer))
1137}
1138
1139fn encode_filecoin_returns_as_abi(
1140    exit_code: u64,
1141    codec: u64,
1142    data: &fvm_ipld_encoding::RawBytes,
1143) -> Vec<u8> {
1144    encode_as_abi_helper(exit_code, codec, data)
1145}
1146
1147/// Round to the next multiple of `EVM` word length.
1148fn round_up_word(value: usize) -> usize {
1149    value.div_ceil(EVM_WORD_LENGTH) * EVM_WORD_LENGTH
1150}
1151
1152/// Format two numbers followed by an arbitrary byte array as solidity ABI.
1153fn encode_as_abi_helper(param1: u64, param2: u64, data: &[u8]) -> Vec<u8> {
1154    // The first two params are "static" numbers. Then, we record the offset of the "data" arg,
1155    // then, at that offset, we record the length of the data.
1156    //
1157    // In practice, this means we have 4 256-bit words back to back where the third arg (the
1158    // offset) is _always_ '32*3'.
1159    let static_args = [
1160        param1,
1161        param2,
1162        (EVM_WORD_LENGTH * 3) as u64,
1163        data.len() as u64,
1164    ];
1165    let padding = [0u8; 24];
1166    let buf: Vec<u8> = padding
1167        .iter() // Right pad
1168        .chain(static_args[0].to_be_bytes().iter()) // Copy u64
1169        .chain(padding.iter())
1170        .chain(static_args[1].to_be_bytes().iter())
1171        .chain(padding.iter())
1172        .chain(static_args[2].to_be_bytes().iter())
1173        .chain(padding.iter())
1174        .chain(static_args[3].to_be_bytes().iter())
1175        .chain(data.iter()) // Finally, we copy in the data
1176        .chain(std::iter::repeat_n(
1177            &0u8,
1178            round_up_word(data.len()) - data.len(),
1179        )) // Left pad
1180        .cloned()
1181        .collect();
1182
1183    buf
1184}
1185
1186/// Convert a native message to an eth transaction.
1187///
1188///   - The state-tree must be from after the message was applied (ideally the following tipset).
1189///   - In some cases, the "to" address may be `0xff0000000000000000000000ffffffffffffffff`. This
1190///     means that the "to" address has not been assigned in the passed state-tree and can only
1191///     happen if the transaction reverted.
1192///
1193/// `eth_tx_from_native_message` does NOT populate:
1194/// - `hash`
1195/// - `block_hash`
1196/// - `block_number`
1197/// - `transaction_index`
1198fn eth_tx_from_native_message<DB: Blockstore>(
1199    msg: &Message,
1200    state: &StateTree<DB>,
1201    chain_id: EthChainIdType,
1202) -> Result<ApiEthTx> {
1203    // Lookup the from address. This must succeed.
1204    let from = match lookup_eth_address(&msg.from(), state) {
1205        Ok(Some(from)) => from,
1206        _ => bail!(
1207            "failed to lookup sender address {} when converting a native message to an eth txn",
1208            msg.from()
1209        ),
1210    };
1211    // Lookup the to address. If the recipient doesn't exist, we replace the address with a
1212    // known sentinel address.
1213    let mut to = match lookup_eth_address(&msg.to(), state) {
1214        Ok(Some(addr)) => Some(addr),
1215        Ok(None) => Some(EthAddress(
1216            ethereum_types::H160::from_str(REVERTED_ETH_ADDRESS).unwrap(),
1217        )),
1218        Err(err) => {
1219            bail!(err)
1220        }
1221    };
1222
1223    // Finally, convert the input parameters to "solidity ABI".
1224
1225    // For empty, we use "0" as the codec. Otherwise, we use CBOR for message
1226    // parameters.
1227    let codec = if !msg.params().is_empty() { CBOR } else { 0 };
1228
1229    // We try to decode the input as an EVM method invocation and/or a contract creation. If
1230    // that fails, we encode the "native" parameters as Solidity ABI.
1231    let input = 'decode: {
1232        if (msg.method_num() == EVMMethod::InvokeContract as MethodNum
1233            || msg.method_num() == EAMMethod::CreateExternal as MethodNum)
1234            && let Ok(buffer) = decode_payload(msg.params(), codec)
1235        {
1236            // If this is a valid "create external", unset the "to" address.
1237            if msg.method_num() == EAMMethod::CreateExternal as MethodNum {
1238                to = None;
1239            }
1240            break 'decode buffer;
1241        }
1242        // Yeah, we're going to ignore errors here because the user can send whatever they
1243        // want and may send garbage.
1244        encode_filecoin_params_as_abi(msg.method_num(), codec, msg.params())?
1245    };
1246
1247    Ok(ApiEthTx {
1248        to,
1249        from,
1250        input,
1251        nonce: EthUint64(msg.sequence),
1252        chain_id: EthUint64(chain_id),
1253        value: msg.value.clone().into(),
1254        r#type: EthUint64(EIP_1559_TX_TYPE.into()),
1255        gas: EthUint64(msg.gas_limit),
1256        max_fee_per_gas: Some(msg.gas_fee_cap.clone().into()),
1257        max_priority_fee_per_gas: Some(msg.gas_premium.clone().into()),
1258        access_list: Some(NotNullVec(vec![])),
1259        ..ApiEthTx::default()
1260    })
1261}
1262
1263pub fn new_eth_tx_from_signed_message<DB: Blockstore>(
1264    smsg: &SignedMessage,
1265    state: &StateTree<DB>,
1266    chain_id: EthChainIdType,
1267) -> Result<ApiEthTx> {
1268    let (tx, hash) = if smsg.is_delegated() {
1269        // This is an eth tx
1270        let (from, tx) = eth_tx_from_signed_eth_message(smsg, chain_id)?;
1271        let hash = tx.eth_hash()?.into();
1272        let tx = ApiEthTx { from, ..tx.into() };
1273        (tx, hash)
1274    } else if smsg.is_secp256k1() {
1275        // Secp Filecoin Message
1276        let tx = eth_tx_from_native_message(smsg.message(), state, chain_id)?;
1277        (tx, smsg.cid().into())
1278    } else {
1279        // BLS Filecoin message
1280        let tx = eth_tx_from_native_message(smsg.message(), state, chain_id)?;
1281        (tx, smsg.message().cid().into())
1282    };
1283    Ok(ApiEthTx { hash, ..tx })
1284}
1285
1286/// Creates an Ethereum transaction from Filecoin message lookup. If `None` is passed for `tx_index`,
1287/// it looks up the transaction index of the message in the tipset.
1288/// Otherwise, it uses some index passed into the function.
1289fn new_eth_tx_from_message_lookup(
1290    ctx: &Ctx,
1291    message_lookup: &MessageLookup,
1292    tx_index: Option<u64>,
1293) -> Result<ApiEthTx> {
1294    let ts = ctx
1295        .chain_store()
1296        .load_required_tipset_or_heaviest(&message_lookup.tipset)?;
1297
1298    // This transaction is located in the parent tipset
1299    let parent_ts = ctx
1300        .chain_store()
1301        .load_required_tipset_or_heaviest(ts.parents())?;
1302
1303    let parent_ts_cid = parent_ts.key().cid()?;
1304
1305    // Lookup the transaction index
1306    let tx_index = tx_index.map_or_else(
1307        || {
1308            let msgs = ctx.chain_store().messages_for_tipset(&parent_ts)?;
1309            msgs.iter()
1310                .position(|msg| msg.cid() == message_lookup.message)
1311                .context("cannot find the msg in the tipset")
1312                .map(|i| i as u64)
1313        },
1314        Ok,
1315    )?;
1316
1317    let smsg = get_signed_message(ctx, message_lookup.message)?;
1318
1319    let state = ctx.state_manager.get_state_tree(ts.parent_state())?;
1320
1321    Ok(ApiEthTx {
1322        block_hash: parent_ts_cid.into(),
1323        block_number: parent_ts.epoch().into(),
1324        transaction_index: tx_index.into(),
1325        ..new_eth_tx_from_signed_message(&smsg, &state, ctx.chain_config().eth_chain_id)?
1326    })
1327}
1328
1329fn new_eth_tx(
1330    ctx: &Ctx,
1331    state: &StateTree<DbImpl>,
1332    block_height: ChainEpoch,
1333    msg_tipset_cid: &Cid,
1334    msg_cid: &Cid,
1335    tx_index: u64,
1336) -> Result<ApiEthTx> {
1337    let smsg = get_signed_message(ctx, *msg_cid)?;
1338    let tx = new_eth_tx_from_signed_message(&smsg, state, ctx.chain_config().eth_chain_id)?;
1339
1340    Ok(ApiEthTx {
1341        block_hash: (*msg_tipset_cid).into(),
1342        block_number: block_height.into(),
1343        transaction_index: tx_index.into(),
1344        ..tx
1345    })
1346}
1347
1348async fn new_eth_tx_receipt(
1349    ctx: &Ctx,
1350    tipset: &Tipset,
1351    tx: &ApiEthTx,
1352    msg_cid: Cid,
1353    msg_receipt: &Receipt,
1354) -> anyhow::Result<EthTxReceipt> {
1355    let mut tx_receipt = EthTxReceipt {
1356        transaction_hash: tx.hash,
1357        from: tx.from,
1358        to: tx.to,
1359        transaction_index: tx.transaction_index,
1360        block_hash: tx.block_hash,
1361        block_number: tx.block_number,
1362        r#type: tx.r#type,
1363        status: (u64::from(msg_receipt.exit_code().is_success())).into(),
1364        gas_used: msg_receipt.gas_used().into(),
1365        ..EthTxReceipt::new()
1366    };
1367
1368    tx_receipt.cumulative_gas_used = EthUint64::default();
1369
1370    let gas_fee_cap = tx.gas_fee_cap()?;
1371    let gas_premium = tx.gas_premium()?;
1372
1373    let gas_outputs = GasOutputs::compute(
1374        msg_receipt.gas_used(),
1375        tx.gas.into(),
1376        &tipset.block_headers().first().parent_base_fee,
1377        &gas_fee_cap.into(),
1378        &gas_premium.into(),
1379    );
1380    let total_spent: BigInt = gas_outputs.total_spent().into();
1381
1382    let mut effective_gas_price = EthBigInt::default();
1383    if msg_receipt.gas_used() > 0 {
1384        effective_gas_price = (total_spent / msg_receipt.gas_used()).into();
1385    }
1386    tx_receipt.effective_gas_price = effective_gas_price;
1387
1388    if tx_receipt.to.is_none() && msg_receipt.exit_code().is_success() {
1389        // Create and Create2 return the same things.
1390        let ret: eam::CreateExternalReturn =
1391            from_slice_with_fallback(msg_receipt.return_data().bytes())?;
1392
1393        tx_receipt.contract_address = Some(ret.eth_address.0.into());
1394    }
1395
1396    if msg_receipt.events_root().is_some() {
1397        let logs =
1398            eth_logs_for_block_and_transaction(ctx, tipset, &tx.block_hash, &msg_cid).await?;
1399        if !logs.is_empty() {
1400            tx_receipt.logs = logs;
1401        }
1402    }
1403
1404    let mut bloom = Bloom::default();
1405    for log in tx_receipt.logs.iter() {
1406        accrue_eth_log(&mut bloom, &log.address, &log.topics);
1407    }
1408    tx_receipt.logs_bloom = bloom.into();
1409
1410    Ok(tx_receipt)
1411}
1412
1413pub async fn eth_logs_for_block_and_transaction(
1414    ctx: &Ctx,
1415    ts: &Tipset,
1416    block_hash: &EthHash,
1417    msg_cid: &Cid,
1418) -> anyhow::Result<Vec<EthLog>> {
1419    // Refuse to serve events for tipsets at or after head (deferred execution).
1420    let heaviest_epoch = ctx.chain_store().heaviest_tipset().epoch();
1421    if ts.epoch() >= heaviest_epoch {
1422        return Err(EthErrors::EventsNotYetAvailable.into());
1423    }
1424
1425    let parsed_filter = ParsedFilter::new_with_tipset_and_msg(
1426        ParsedFilterTipsets::Hash(*block_hash),
1427        Some(*msg_cid),
1428    );
1429    let mut events = vec![];
1430    EthEventHandler::collect_events(
1431        &ctx.state_manager,
1432        ts,
1433        Some(&parsed_filter),
1434        SkipEvent::OnUnresolvedAddress,
1435        &mut events,
1436    )
1437    .await?;
1438    eth_filter_logs_from_events(ctx, &events)
1439}
1440
1441/// Collects the logs produced by a single chain head change, for the logs
1442/// subscription.
1443pub(in crate::rpc) async fn eth_logs_for_head_change(
1444    ctx: &Ctx,
1445    change: &PathChange<Tipset>,
1446) -> anyhow::Result<Vec<EthLog>> {
1447    let (receipt_ts, revert_status) = match change {
1448        PathChange::Revert(ts) => (ts, EventRevertStatus::Reverted),
1449        PathChange::Apply(ts) => (ts, EventRevertStatus::Applied),
1450    };
1451    // Genesis carries no events and has no parent message tipset to load.
1452    if receipt_ts.epoch() == 0 {
1453        return Ok(vec![]);
1454    }
1455    let msg_ts = ctx
1456        .chain_index()
1457        .load_required_tipset(receipt_ts.parents())?;
1458    let executed_ts = ctx
1459        .state_manager
1460        .load_executed_tipset_with_receipt(&msg_ts, receipt_ts)
1461        .await?;
1462    let mut events = vec![];
1463    EthEventHandler::collect_events_from_messages(
1464        &ctx.state_manager,
1465        &msg_ts,
1466        &executed_ts.executed_messages,
1467        None::<&ParsedFilter>,
1468        SkipEvent::OnUnresolvedAddress,
1469        revert_status,
1470        &mut events,
1471    )
1472    .await?;
1473    eth_filter_logs_from_events(ctx, &events)
1474}
1475
1476fn get_signed_message(ctx: &Ctx, message_cid: Cid) -> Result<SignedMessage> {
1477    let result: Result<SignedMessage, crate::chain::Error> =
1478        crate::chain::message_from_cid(ctx.db(), &message_cid);
1479
1480    result.or_else(|_| {
1481        // We couldn't find the signed message, it might be a BLS message, so search for a regular message.
1482        let msg: Message = crate::chain::message_from_cid(ctx.db(), &message_cid)
1483            .with_context(|| format!("failed to find msg {message_cid}"))?;
1484        Ok(SignedMessage::new_unchecked(
1485            msg,
1486            Signature::new_bls(vec![]),
1487        ))
1488    })
1489}
1490
1491pub enum EthGetBlockByHash {}
1492impl RpcMethod<2> for EthGetBlockByHash {
1493    const NAME: &'static str = "Filecoin.EthGetBlockByHash";
1494    const NAME_ALIAS: Option<&'static str> = Some("eth_getBlockByHash");
1495    const PARAM_NAMES: [&'static str; 2] = ["blockHash", "fullTxInfo"];
1496    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1497    const PERMISSION: Permission = Permission::Read;
1498    const DESCRIPTION: &'static str =
1499        "Retrieves a block by its hash, optionally including full transaction objects.";
1500
1501    type Params = (EthHash, bool);
1502    type Ok = Arc<Block>;
1503
1504    async fn handle(
1505        ctx: Ctx,
1506        (block_hash, full_tx_info): Self::Params,
1507        ext: &http::Extensions,
1508    ) -> Result<Self::Ok, ServerError> {
1509        let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
1510        let ts = resolver
1511            .tipset_by_block_number_or_hash(block_hash, ResolveNullTipset::TakeOlder)
1512            .await?;
1513        Block::from_filecoin_tipset(&ctx.state_manager, ts, full_tx_info.into())
1514            .await
1515            .map_err(ServerError::from)
1516    }
1517}
1518
1519pub enum EthGetBlockByNumber {}
1520impl RpcMethod<2> for EthGetBlockByNumber {
1521    const NAME: &'static str = "Filecoin.EthGetBlockByNumber";
1522    const NAME_ALIAS: Option<&'static str> = Some("eth_getBlockByNumber");
1523    const PARAM_NAMES: [&'static str; 2] = ["blockParam", "fullTxInfo"];
1524    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1525    const PERMISSION: Permission = Permission::Read;
1526    const DESCRIPTION: &'static str = "Retrieves a block by its number or a special tag.";
1527
1528    type Params = (BlockNumberOrPredefined, bool);
1529    type Ok = Arc<Block>;
1530
1531    async fn handle(
1532        ctx: Ctx,
1533        (block_param, full_tx_info): Self::Params,
1534        ext: &http::Extensions,
1535    ) -> Result<Self::Ok, ServerError> {
1536        let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
1537        let ts = resolver
1538            .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::Fail)
1539            .await?;
1540        Block::from_filecoin_tipset(&ctx.state_manager, ts, full_tx_info.into())
1541            .await
1542            .map_err(ServerError::from)
1543    }
1544}
1545
1546async fn get_block_receipts(
1547    ctx: &Ctx,
1548    ts: Tipset,
1549    limit: Option<ChainEpoch>,
1550) -> Result<Vec<EthTxReceipt>> {
1551    if let Some(limit) = limit
1552        && limit > LOOKBACK_NO_LIMIT
1553        && ts.epoch() < ctx.chain_store().heaviest_tipset().epoch() - limit
1554    {
1555        bail!(
1556            "tipset {} is older than the allowed lookback limit",
1557            ts.key().format_lotus()
1558        );
1559    }
1560    let ts_ref = Arc::new(ts);
1561    let ts_key = ts_ref.key();
1562
1563    // Execute the tipset to get the messages and receipts
1564    let ExecutedTipset {
1565        state_root,
1566        executed_messages,
1567        ..
1568    } = ctx
1569        .state_manager
1570        .load_executed_tipset_for_rpc(&ts_ref)
1571        .await?;
1572
1573    // Load the state tree
1574    let state_tree = ctx.state_manager.get_state_tree(&state_root)?;
1575
1576    let mut eth_receipts = Vec::with_capacity(executed_messages.len());
1577    for (
1578        i,
1579        ExecutedMessage {
1580            message, receipt, ..
1581        },
1582    ) in executed_messages.iter().enumerate()
1583    {
1584        let tx = new_eth_tx(
1585            ctx,
1586            &state_tree,
1587            ts_ref.epoch(),
1588            &ts_key.cid()?,
1589            &message.cid(),
1590            i as u64,
1591        )?;
1592
1593        let receipt = new_eth_tx_receipt(ctx, &ts_ref, &tx, message.cid(), receipt).await?;
1594        eth_receipts.push(receipt);
1595    }
1596    Ok(eth_receipts)
1597}
1598
1599// Reject null-round `eth_getBlockReceipts*` by default (matches lotus#13694); set this flag for
1600// the legacy previous-tipset behavior. See https://github.com/ChainSafe/forest/issues/7270.
1601crate::def_is_env_truthy!(
1602    legacy_null_round_block_receipts,
1603    "FOREST_ETH_GET_BLOCK_RECEIPTS_LEGACY_NULL_ROUND"
1604);
1605
1606/// `Fail` (default) or legacy `TakeOlder` for `eth_getBlockReceipts*` on a null round.
1607fn block_receipts_null_round() -> ResolveNullTipset {
1608    if legacy_null_round_block_receipts() {
1609        ResolveNullTipset::TakeOlder
1610    } else {
1611        ResolveNullTipset::Fail
1612    }
1613}
1614
1615pub enum EthGetBlockReceipts {}
1616impl RpcMethod<1> for EthGetBlockReceipts {
1617    const NAME: &'static str = "Filecoin.EthGetBlockReceipts";
1618    const NAME_ALIAS: Option<&'static str> = Some("eth_getBlockReceipts");
1619    const PARAM_NAMES: [&'static str; 1] = ["blockParam"];
1620    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1621    const PERMISSION: Permission = Permission::Read;
1622    const DESCRIPTION: &'static str =
1623        "Retrieves all transaction receipts for a block by its number, hash or a special tag.";
1624
1625    type Params = (BlockNumberOrHash,);
1626    type Ok = NotNullVec<EthTxReceipt>;
1627
1628    async fn handle(
1629        ctx: Ctx,
1630        (block_param,): Self::Params,
1631        ext: &http::Extensions,
1632    ) -> Result<Self::Ok, ServerError> {
1633        let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
1634        let ts = resolver
1635            .tipset_by_block_number_or_hash(block_param, block_receipts_null_round())
1636            .await?;
1637        get_block_receipts(&ctx, ts, None)
1638            .await
1639            .map(NotNullVec)
1640            .map_err(ServerError::from)
1641    }
1642}
1643
1644pub enum EthGetBlockReceiptsLimited {}
1645impl RpcMethod<2> for EthGetBlockReceiptsLimited {
1646    const NAME: &'static str = "Filecoin.EthGetBlockReceiptsLimited";
1647    const NAME_ALIAS: Option<&'static str> = Some("eth_getBlockReceiptsLimited");
1648    const PARAM_NAMES: [&'static str; 2] = ["blockParam", "limit"];
1649    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1650    const PERMISSION: Permission = Permission::Read;
1651    const DESCRIPTION: &'static str = "Retrieves all transaction receipts for a block identified by its number, hash or a special tag along with an optional limit on the chain epoch for state resolution.";
1652
1653    type Params = (BlockNumberOrHash, ChainEpoch);
1654    type Ok = NotNullVec<EthTxReceipt>;
1655
1656    async fn handle(
1657        ctx: Ctx,
1658        (block_param, limit): Self::Params,
1659        ext: &http::Extensions,
1660    ) -> Result<Self::Ok, ServerError> {
1661        let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
1662        let ts = resolver
1663            .tipset_by_block_number_or_hash(block_param, block_receipts_null_round())
1664            .await?;
1665        get_block_receipts(&ctx, ts, Some(limit))
1666            .await
1667            .map(NotNullVec)
1668            .map_err(ServerError::from)
1669    }
1670}
1671
1672pub enum EthGetBlockTransactionCountByHash {}
1673impl RpcMethod<1> for EthGetBlockTransactionCountByHash {
1674    const NAME: &'static str = "Filecoin.EthGetBlockTransactionCountByHash";
1675    const NAME_ALIAS: Option<&'static str> = Some("eth_getBlockTransactionCountByHash");
1676    const PARAM_NAMES: [&'static str; 1] = ["blockHash"];
1677    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1678    const PERMISSION: Permission = Permission::Read;
1679    const DESCRIPTION: &'static str =
1680        "Returns the number of messages in the tipset identified by the given block hash.";
1681
1682    type Params = (EthHash,);
1683    type Ok = EthUint64;
1684
1685    async fn handle(
1686        ctx: Ctx,
1687        (block_hash,): Self::Params,
1688        _: &http::Extensions,
1689    ) -> Result<Self::Ok, ServerError> {
1690        let ts = get_tipset_from_hash(ctx.chain_store(), &block_hash)?;
1691
1692        let head = ctx.chain_store().heaviest_tipset();
1693        if ts.epoch() > head.epoch() {
1694            return Err(anyhow::anyhow!("requested a future epoch (beyond \"latest\")").into());
1695        }
1696        let count = count_messages_in_tipset(ctx.db(), &ts)?;
1697        Ok(EthUint64(count as _))
1698    }
1699}
1700
1701pub enum EthGetBlockTransactionCountByNumber {}
1702impl RpcMethod<1> for EthGetBlockTransactionCountByNumber {
1703    const NAME: &'static str = "Filecoin.EthGetBlockTransactionCountByNumber";
1704    const NAME_ALIAS: Option<&'static str> = Some("eth_getBlockTransactionCountByNumber");
1705    const PARAM_NAMES: [&'static str; 1] = ["blockNumber"];
1706    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1707    const PERMISSION: Permission = Permission::Read;
1708    const DESCRIPTION: &'static str = "Returns the number of transactions in a block identified by its block number or a special tag.";
1709
1710    type Params = (BlockNumberOrPredefined,);
1711    type Ok = EthUint64;
1712
1713    async fn handle(
1714        ctx: Ctx,
1715        (block_number,): Self::Params,
1716        ext: &http::Extensions,
1717    ) -> Result<Self::Ok, ServerError> {
1718        let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
1719        let ts = resolver
1720            .tipset_by_block_number_or_hash(block_number, ResolveNullTipset::Fail)
1721            .await?;
1722        let count = count_messages_in_tipset(ctx.db(), &ts)?;
1723        Ok(EthUint64(count as _))
1724    }
1725}
1726
1727pub enum EthGetMessageCidByTransactionHash {}
1728impl RpcMethod<1> for EthGetMessageCidByTransactionHash {
1729    const NAME: &'static str = "Filecoin.EthGetMessageCidByTransactionHash";
1730    const NAME_ALIAS: Option<&'static str> = Some("eth_getMessageCidByTransactionHash");
1731    const PARAM_NAMES: [&'static str; 1] = ["txHash"];
1732    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1733    const PERMISSION: Permission = Permission::Read;
1734    const DESCRIPTION: &'static str = "Returns the CID of the Filecoin message corresponding to the given Ethereum transaction hash.";
1735
1736    type Params = (EthHash,);
1737    type Ok = Option<Cid>;
1738
1739    async fn handle(
1740        ctx: Ctx,
1741        (tx_hash,): Self::Params,
1742        _: &http::Extensions,
1743    ) -> Result<Self::Ok, ServerError> {
1744        let result = ctx.chain_store().get_mapping(&tx_hash);
1745        match result {
1746            Ok(Some(cid)) => return Ok(Some(cid)),
1747            Ok(None) => tracing::debug!("Undefined key {tx_hash}"),
1748            _ => {
1749                result?;
1750            }
1751        }
1752
1753        // This isn't an eth transaction we have the mapping for, so let's try looking it up as a filecoin message
1754        let cid = tx_hash.to_cid();
1755
1756        let result: Result<Vec<SignedMessage>, crate::chain::Error> =
1757            crate::chain::messages_from_cids(ctx.db(), &[cid]);
1758        if result.is_ok() {
1759            // This is an Eth Tx, Secp message, Or BLS message in the mpool
1760            return Ok(Some(cid));
1761        }
1762
1763        let result: Result<Vec<Message>, crate::chain::Error> =
1764            crate::chain::messages_from_cids(ctx.db(), &[cid]);
1765        if result.is_ok() {
1766            // This is a BLS message
1767            return Ok(Some(cid));
1768        }
1769
1770        // Ethereum clients expect an empty response when the message was not found
1771        Ok(None)
1772    }
1773}
1774
1775fn count_messages_in_tipset(store: &impl Blockstore, ts: &Tipset) -> anyhow::Result<usize> {
1776    let mut message_cids = CidHashSet::default();
1777    for block in ts.block_headers() {
1778        let (bls_messages, secp_messages) = crate::chain::store::block_messages(store, block)?;
1779        for m in bls_messages {
1780            message_cids.insert(m.cid());
1781        }
1782        for m in secp_messages {
1783            message_cids.insert(m.cid());
1784        }
1785    }
1786    Ok(message_cids.len())
1787}
1788
1789pub enum EthSyncing {}
1790impl RpcMethod<0> for EthSyncing {
1791    const NAME: &'static str = "Filecoin.EthSyncing";
1792    const NAME_ALIAS: Option<&'static str> = Some("eth_syncing");
1793    const PARAM_NAMES: [&'static str; 0] = [];
1794    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1795    const PERMISSION: Permission = Permission::Read;
1796    const DESCRIPTION: &'static str =
1797        "Returns the node's sync status, or `false` if the node is not currently syncing.";
1798
1799    type Params = ();
1800    type Ok = EthSyncingResult;
1801
1802    async fn handle(
1803        ctx: Ctx,
1804        (): Self::Params,
1805        ext: &http::Extensions,
1806    ) -> Result<Self::Ok, ServerError> {
1807        let sync_status = crate::rpc::sync::SyncStatus::handle(ctx, (), ext).await?;
1808        match sync_status.status {
1809            NodeSyncStatus::Synced => Ok(EthSyncingResult {
1810                done_sync: true,
1811                // Once the node is synced, other fields are not relevant for the API
1812                ..Default::default()
1813            }),
1814            NodeSyncStatus::Syncing => {
1815                let starting_block = match sync_status.get_min_starting_block() {
1816                    Some(e) => Ok(e),
1817                    None => Err(ServerError::internal_error(
1818                        "missing syncing information, try again",
1819                        None,
1820                    )),
1821                }?;
1822
1823                Ok(EthSyncingResult {
1824                    done_sync: sync_status.is_synced(),
1825                    starting_block,
1826                    current_block: sync_status.current_head_epoch,
1827                    highest_block: sync_status.network_head_epoch,
1828                })
1829            }
1830            _ => Err(ServerError::internal_error("node is not syncing", None)),
1831        }
1832    }
1833}
1834
1835pub enum EthEstimateGas {}
1836
1837impl RpcMethod<2> for EthEstimateGas {
1838    const NAME: &'static str = "Filecoin.EthEstimateGas";
1839    const NAME_ALIAS: Option<&'static str> = Some("eth_estimateGas");
1840    const N_REQUIRED_PARAMS: usize = 1;
1841    const PARAM_NAMES: [&'static str; 2] = ["tx", "blockParam"];
1842    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1843    const PERMISSION: Permission = Permission::Read;
1844    const DESCRIPTION: &'static str =
1845        "Estimates the amount of gas required to execute the given transaction.";
1846
1847    type Params = (EthCallMessage, Option<BlockNumberOrHash>);
1848    type Ok = EthUint64;
1849
1850    async fn handle(
1851        ctx: Ctx,
1852        (tx, block_param): Self::Params,
1853        ext: &http::Extensions,
1854    ) -> Result<Self::Ok, ServerError> {
1855        let tipset = if let Some(block_param) = block_param {
1856            let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
1857            resolver
1858                .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
1859                .await?
1860        } else {
1861            ctx.chain_store().heaviest_tipset()
1862        };
1863        eth_estimate_gas(&ctx, tx, tipset).await
1864    }
1865}
1866
1867async fn eth_estimate_gas(
1868    ctx: &Ctx,
1869    tx: EthCallMessage,
1870    tipset: Tipset,
1871) -> Result<EthUint64, ServerError> {
1872    let mut msg = Message::try_from(tx)?;
1873    // Set the gas limit to the zero sentinel value, which makes
1874    // gas estimation actually run.
1875    msg.gas_limit = 0;
1876
1877    match gas::estimate_message_gas(ctx, msg.clone(), None, tipset.key().clone().into()).await {
1878        Err(server_err) => {
1879            // On failure, GasEstimateMessageGas doesn't actually return the invocation result,
1880            // it just returns an error. That means we can't get the revert reason.
1881            //
1882            // So we re-execute the message with EthCall (well, applyMessage which contains the
1883            // guts of EthCall). This will give us an ethereum specific error with revert
1884            // information.
1885            msg.set_gas_limit(BLOCK_GAS_LIMIT);
1886            let err = match apply_message(ctx, Some(tipset), msg).await {
1887                Ok(_) => Error::msg(server_err.to_string()),
1888                Err(e)
1889                    if e.downcast_ref::<EthErrors>().is_some_and(|eth_err| {
1890                        matches!(eth_err, EthErrors::ExecutionReverted { .. })
1891                    }) =>
1892                {
1893                    return Err(e.into());
1894                }
1895                Err(e) => e,
1896            };
1897
1898            Err(err.context("failed to estimate gas").into())
1899        }
1900        Ok(gassed_msg) => {
1901            let expected_gas = eth_gas_search(ctx, gassed_msg, &tipset.key().into()).await?;
1902            Ok(expected_gas.into())
1903        }
1904    }
1905}
1906
1907async fn apply_message(
1908    ctx: &Ctx,
1909    tipset: Option<Tipset>,
1910    msg: Message,
1911) -> Result<ApiInvocResult, Error> {
1912    if let Some(ts) = &tipset
1913        && ts.epoch() > 0
1914        && ctx
1915            .chain_config()
1916            .has_expensive_fork_between(ts.epoch(), ts.epoch() + 1)
1917    {
1918        return Err(crate::state_manager::Error::ExpensiveFork { epoch: ts.epoch() }.into());
1919    }
1920
1921    let (invoc_res, _) = ctx
1922        .state_manager
1923        .apply_on_state_with_gas(tipset, msg, VMFlush::Skip)
1924        .await
1925        .context("failed to apply on state with gas")?;
1926
1927    // Extract receipt or return early if none
1928    match &invoc_res.msg_rct {
1929        None => return Err(anyhow::anyhow!("no message receipt in execution result")),
1930        Some(receipt) => {
1931            if !receipt.exit_code().is_success() {
1932                let (data, reason) = decode_revert_reason(receipt.return_data());
1933
1934                return Err(EthErrors::execution_reverted(
1935                    ExitCode::from(receipt.exit_code()),
1936                    reason.as_str(),
1937                    invoc_res.error.as_str(),
1938                    data.as_slice(),
1939                )
1940                .into());
1941            }
1942        }
1943    };
1944
1945    Ok(invoc_res)
1946}
1947
1948pub async fn eth_gas_search(data: &Ctx, msg: Message, tsk: &ApiTipsetKey) -> anyhow::Result<u64> {
1949    let (_invoc_res, apply_ret, prior_messages, ts) =
1950        gas::GasEstimateGasLimit::estimate_call_with_gas(data, msg.clone(), tsk).await?;
1951    if apply_ret.msg_receipt().exit_code().is_success() {
1952        return Ok(msg.gas_limit());
1953    }
1954
1955    let exec_trace = apply_ret.exec_trace();
1956    let _expected_exit_code: ExitCode = fvm_shared4::error::ExitCode::SYS_OUT_OF_GAS.into();
1957    if exec_trace.iter().any(|t| {
1958        matches!(
1959            t,
1960            &ExecutionEvent::CallReturn(CallReturn {
1961                exit_code: Some(_expected_exit_code),
1962                ..
1963            })
1964        )
1965    }) {
1966        let ret = gas_search(data, &msg, prior_messages, ts).await?;
1967        Ok(((ret as f64) * data.mpool.gas_limit_overestimation()) as u64)
1968    } else {
1969        anyhow::bail!(
1970            "message execution failed: exit {}, reason: {}",
1971            apply_ret.msg_receipt().exit_code(),
1972            apply_ret.failure_info().unwrap_or_default(),
1973        );
1974    }
1975}
1976
1977/// `gas_search` does an exponential search to find a gas value to execute the
1978/// message with. It first finds a high gas limit that allows the message to execute
1979/// by doubling the previous gas limit until it succeeds then does a binary
1980/// search till it gets within a range of 1%
1981async fn gas_search(
1982    data: &Ctx,
1983    msg: &Message,
1984    prior_messages: Arc<Vec<ChainMessage>>,
1985    ts: Tipset,
1986) -> anyhow::Result<u64> {
1987    let mut high = msg.gas_limit;
1988    let mut low = msg.gas_limit;
1989
1990    async fn can_succeed(
1991        data: &Ctx,
1992        mut msg: Message,
1993        prior_messages: Arc<Vec<ChainMessage>>,
1994        ts: Tipset,
1995        limit: u64,
1996    ) -> anyhow::Result<bool> {
1997        msg.gas_limit = limit;
1998        let (_invoc_res, apply_ret, _, _) = data
1999            .state_manager
2000            .call_with_gas(msg.into(), prior_messages, Some(ts), VMFlush::Skip)
2001            .await?;
2002        Ok(apply_ret.msg_receipt().exit_code().is_success())
2003    }
2004
2005    while high < BLOCK_GAS_LIMIT {
2006        if can_succeed(
2007            data,
2008            msg.clone(),
2009            prior_messages.shallow_clone(),
2010            ts.shallow_clone(),
2011            high,
2012        )
2013        .await?
2014        {
2015            break;
2016        }
2017        low = high;
2018        high = high.saturating_mul(2).min(BLOCK_GAS_LIMIT);
2019    }
2020
2021    let mut check_threshold = high / 100;
2022    while (high - low) > check_threshold {
2023        let median = (high + low) / 2;
2024        if can_succeed(
2025            data,
2026            msg.clone(),
2027            prior_messages.shallow_clone(),
2028            ts.shallow_clone(),
2029            median,
2030        )
2031        .await?
2032        {
2033            high = median;
2034        } else {
2035            low = median;
2036        }
2037        check_threshold = median / 100;
2038    }
2039
2040    Ok(high)
2041}
2042
2043pub enum EthFeeHistory {}
2044
2045impl RpcMethod<3> for EthFeeHistory {
2046    const NAME: &'static str = "Filecoin.EthFeeHistory";
2047    const NAME_ALIAS: Option<&'static str> = Some("eth_feeHistory");
2048    const N_REQUIRED_PARAMS: usize = 2;
2049    const PARAM_NAMES: [&'static str; 3] = ["blockCount", "newestBlockNumber", "rewardPercentiles"];
2050    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2051    const PERMISSION: Permission = Permission::Read;
2052    const DESCRIPTION: &'static str = "Returns historical gas information for a range of blocks, including base fee per gas, gas used ratio, and priority fee percentiles.";
2053
2054    type Params = (EthUint64, BlockNumberOrPredefined, Option<Vec<f64>>);
2055    type Ok = EthFeeHistoryResult;
2056
2057    async fn handle(
2058        ctx: Ctx,
2059        (EthUint64(block_count), newest_block_number, reward_percentiles): Self::Params,
2060        ext: &http::Extensions,
2061    ) -> Result<Self::Ok, ServerError> {
2062        let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
2063        let tipset = resolver
2064            .tipset_by_block_number_or_hash(newest_block_number, ResolveNullTipset::TakeOlder)
2065            .await?;
2066        eth_fee_history(ctx, tipset, block_count, reward_percentiles).await
2067    }
2068}
2069
2070async fn eth_fee_history(
2071    ctx: Ctx,
2072    tipset: Tipset,
2073    block_count: u64,
2074    reward_percentiles: Option<Vec<f64>>,
2075) -> Result<EthFeeHistoryResult, ServerError> {
2076    if block_count > 1024 {
2077        return Err(anyhow::anyhow!("block count should be smaller than 1024").into());
2078    }
2079
2080    let reward_percentiles = reward_percentiles.unwrap_or_default();
2081    validate_reward_percentiles(&reward_percentiles)?;
2082
2083    let mut oldest_block_height = 1;
2084    // NOTE: baseFeePerGas should include the next block after the newest of the returned range,
2085    //  because the next base fee can be inferred from the messages in the newest block.
2086    //  However, this is NOT the case in Filecoin due to deferred execution, so the best
2087    //  we can do is duplicate the last value.
2088    let mut base_fee_array = vec![EthBigInt::from(
2089        &tipset.block_headers().first().parent_base_fee,
2090    )];
2091    let mut rewards_array = vec![];
2092    let mut gas_used_ratio_array = vec![];
2093    for ts in tipset
2094        .chain(ctx.db())
2095        .filter(|i| i.epoch() > 0)
2096        .take(block_count as _)
2097    {
2098        let base_fee = &ts.block_headers().first().parent_base_fee;
2099        let ExecutedTipset {
2100            executed_messages, ..
2101        } = ctx.state_manager.load_executed_tipset_for_rpc(&ts).await?;
2102        let mut tx_gas_rewards = Vec::with_capacity(executed_messages.len());
2103        for ExecutedMessage {
2104            message, receipt, ..
2105        } in executed_messages.iter()
2106        {
2107            let premium = message.effective_gas_premium(base_fee);
2108            tx_gas_rewards.push(GasReward {
2109                gas_used: receipt.gas_used(),
2110                premium,
2111            });
2112        }
2113        let (rewards, total_gas_used) =
2114            calculate_rewards_and_gas_used(&reward_percentiles, tx_gas_rewards);
2115        let max_gas = BLOCK_GAS_LIMIT * (ts.block_headers().len() as u64);
2116
2117        // arrays should be reversed at the end
2118        base_fee_array.push(EthBigInt::from(base_fee));
2119        gas_used_ratio_array.push((total_gas_used as f64) / (max_gas as f64));
2120        rewards_array.push(rewards);
2121
2122        oldest_block_height = ts.epoch();
2123    }
2124
2125    // Reverse the arrays; we collected them newest to oldest; the client expects oldest to newest.
2126    base_fee_array.reverse();
2127    gas_used_ratio_array.reverse();
2128    rewards_array.reverse();
2129
2130    Ok(EthFeeHistoryResult {
2131        oldest_block: EthUint64(oldest_block_height as _),
2132        base_fee_per_gas: base_fee_array,
2133        gas_used_ratio: gas_used_ratio_array,
2134        reward: if reward_percentiles.is_empty() {
2135            None
2136        } else {
2137            Some(rewards_array)
2138        },
2139    })
2140}
2141
2142fn validate_reward_percentiles(reward_percentiles: &[f64]) -> anyhow::Result<()> {
2143    if reward_percentiles.len() > 100 {
2144        anyhow::bail!("length of the reward percentile array cannot be greater than 100");
2145    }
2146
2147    for (&rp_prev, &rp) in std::iter::once(&0.0)
2148        .chain(reward_percentiles.iter())
2149        .tuple_windows()
2150    {
2151        if !(0. ..=100.).contains(&rp) {
2152            anyhow::bail!("invalid reward percentile: {rp} should be between 0 and 100");
2153        }
2154        if rp < rp_prev {
2155            anyhow::bail!(
2156                "invalid reward percentile: {rp} should be larger than or equal to {rp_prev}"
2157            );
2158        }
2159    }
2160
2161    Ok(())
2162}
2163
2164fn calculate_rewards_and_gas_used(
2165    reward_percentiles: &[f64],
2166    mut tx_gas_rewards: Vec<GasReward>,
2167) -> (Vec<EthBigInt>, u64) {
2168    const MIN_GAS_PREMIUM: u64 = 100000;
2169
2170    let gas_used_total = tx_gas_rewards.iter().map(|i| i.gas_used).sum();
2171    let mut rewards = reward_percentiles
2172        .iter()
2173        .map(|_| EthBigInt::from(MIN_GAS_PREMIUM))
2174        .collect_vec();
2175    if !tx_gas_rewards.is_empty() {
2176        tx_gas_rewards.sort_by(|a, b| a.premium.cmp(&b.premium));
2177        let mut idx = 0;
2178        let mut sum = 0;
2179        #[allow(clippy::indexing_slicing)]
2180        for (i, &percentile) in reward_percentiles.iter().enumerate() {
2181            let threshold = ((gas_used_total as f64) * percentile / 100.) as u64;
2182            while sum < threshold && idx < tx_gas_rewards.len() - 1 {
2183                sum += tx_gas_rewards[idx].gas_used;
2184                idx += 1;
2185            }
2186            rewards[i] = (&tx_gas_rewards[idx].premium).into();
2187        }
2188    }
2189    (rewards, gas_used_total)
2190}
2191
2192pub enum EthGetCode {}
2193impl RpcMethod<2> for EthGetCode {
2194    const NAME: &'static str = "Filecoin.EthGetCode";
2195    const NAME_ALIAS: Option<&'static str> = Some("eth_getCode");
2196    const PARAM_NAMES: [&'static str; 2] = ["ethAddress", "blockNumberOrHash"];
2197    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2198    const PERMISSION: Permission = Permission::Read;
2199    const DESCRIPTION: &'static str = "Retrieves the contract code at a specific address and block state, identified by its number, hash, or a special tag.";
2200
2201    type Params = (EthAddress, BlockNumberOrHash);
2202    type Ok = EthBytes;
2203
2204    async fn handle(
2205        ctx: Ctx,
2206        (eth_address, block_param): Self::Params,
2207        ext: &http::Extensions,
2208    ) -> Result<Self::Ok, ServerError> {
2209        let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
2210        let ts = resolver
2211            .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
2212            .await?;
2213        eth_get_code(&ctx, &ts, &eth_address).await
2214    }
2215}
2216
2217async fn eth_get_code(
2218    ctx: &Ctx,
2219    ts: &Tipset,
2220    eth_address: &EthAddress,
2221) -> Result<EthBytes, ServerError> {
2222    let to_address = FilecoinAddress::try_from(eth_address)?;
2223    let TipsetState { state_root, .. } = ctx.state_manager.load_tipset_state(ts).await?;
2224    let state_tree = ctx.state_manager.get_state_tree(&state_root)?;
2225    let Some(actor) = state_tree
2226        .get_actor(&to_address)
2227        .with_context(|| format!("failed to lookup contract {}", eth_address.0))?
2228    else {
2229        return Ok(Default::default());
2230    };
2231
2232    // Not a contract. We could try to distinguish between accounts and "native" contracts here,
2233    // but it's not worth it.
2234    if !is_evm_actor(&actor.code) {
2235        return Ok(Default::default());
2236    }
2237
2238    let message = Arc::new(Message {
2239        from: FilecoinAddress::SYSTEM_ACTOR,
2240        to: to_address,
2241        method_num: METHOD_GET_BYTE_CODE,
2242        gas_limit: BLOCK_GAS_LIMIT,
2243        ..Default::default()
2244    });
2245
2246    // Rewind ts to escape the fork guard, but keep state_root fixed to the requested epoch: the
2247    // result comes from state_root (ts only supplies execution context), so recomputing it for the
2248    // parent would read an earlier epoch's bytecode.
2249    let mut ts = ts.shallow_clone();
2250    let api_invoc_result = loop {
2251        match ctx
2252            .state_manager
2253            .call_on_state(
2254                state_root,
2255                message.shallow_clone(),
2256                Some(ts.shallow_clone()),
2257            )
2258            .await
2259        {
2260            Ok(res) => break res,
2261            Err(crate::state_manager::Error::ExpensiveFork { .. }) => {
2262                ts = ctx
2263                    .chain_index()
2264                    .load_required_tipset(ts.parents())
2265                    .map_err(|e| anyhow::anyhow!("getting parent tipset: {e}"))?;
2266            }
2267            Err(e) => return Err(e.into()),
2268        }
2269    };
2270    let Some(msg_rct) = api_invoc_result.msg_rct else {
2271        return Err(anyhow::anyhow!("no message receipt").into());
2272    };
2273    if !msg_rct.exit_code().is_success() || !api_invoc_result.error.is_empty() {
2274        return Err(anyhow::anyhow!(
2275            "GetBytecode failed: exit={} error={}",
2276            msg_rct.exit_code(),
2277            api_invoc_result.error
2278        )
2279        .into());
2280    }
2281
2282    let get_bytecode_return: GetBytecodeReturn =
2283        fvm_ipld_encoding::from_slice(msg_rct.return_data().as_slice())?;
2284    if let Some(cid) = get_bytecode_return.0 {
2285        Ok(EthBytes(ctx.db().get_required(&cid)?))
2286    } else {
2287        Ok(Default::default())
2288    }
2289}
2290
2291pub enum EthGetStorageAt {}
2292impl RpcMethod<3> for EthGetStorageAt {
2293    const NAME: &'static str = "Filecoin.EthGetStorageAt";
2294    const NAME_ALIAS: Option<&'static str> = Some("eth_getStorageAt");
2295    const PARAM_NAMES: [&'static str; 3] = ["ethAddress", "position", "blockNumberOrHash"];
2296    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2297    const PERMISSION: Permission = Permission::Read;
2298    const DESCRIPTION: &'static str =
2299        "Retrieves the storage value at a specific position for a contract
2300        at a given block state, identified by its number, hash, or a special tag.";
2301
2302    type Params = (EthAddress, EthBytes, BlockNumberOrHash);
2303    type Ok = EthBytes;
2304
2305    async fn handle(
2306        ctx: Ctx,
2307        (eth_address, position, block_number_or_hash): Self::Params,
2308        ext: &http::Extensions,
2309    ) -> Result<Self::Ok, ServerError> {
2310        let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
2311        let ts = resolver
2312            .tipset_by_block_number_or_hash(block_number_or_hash, ResolveNullTipset::TakeOlder)
2313            .await?;
2314        get_storage_at(&ctx, ts, eth_address, position).await
2315    }
2316}
2317
2318async fn get_storage_at(
2319    ctx: &Ctx,
2320    ts: Tipset,
2321    eth_address: EthAddress,
2322    position: EthBytes,
2323) -> Result<EthBytes, ServerError> {
2324    let to_address = FilecoinAddress::try_from(&eth_address)?;
2325    let TipsetState { state_root, .. } = ctx.state_manager.load_tipset_state(&ts).await?;
2326    let make_empty_result = || EthBytes(vec![0; EVM_WORD_LENGTH]);
2327    let Some(actor) = ctx
2328        .state_manager
2329        .get_actor(&to_address, state_root)
2330        .with_context(|| format!("failed to lookup contract {}", eth_address.0))?
2331    else {
2332        return Ok(make_empty_result());
2333    };
2334
2335    if !is_evm_actor(&actor.code) {
2336        return Ok(make_empty_result());
2337    }
2338
2339    let params = RawBytes::new(GetStorageAtParams::new(position.0)?.serialize_params()?);
2340    let message = Arc::new(Message {
2341        from: FilecoinAddress::SYSTEM_ACTOR,
2342        to: to_address,
2343        method_num: METHOD_GET_STORAGE_AT,
2344        gas_limit: BLOCK_GAS_LIMIT,
2345        params,
2346        ..Default::default()
2347    });
2348    // Rewind ts to escape the fork guard, but keep state_root fixed to the requested epoch: the
2349    // result comes from state_root (ts only supplies execution context), so recomputing it for the
2350    // parent would read an earlier epoch's storage.
2351    let mut ts = ts;
2352    let api_invoc_result = loop {
2353        match ctx
2354            .state_manager
2355            .call_on_state(
2356                state_root,
2357                message.shallow_clone(),
2358                Some(ts.shallow_clone()),
2359            )
2360            .await
2361        {
2362            Ok(res) => break res,
2363            Err(crate::state_manager::Error::ExpensiveFork { .. }) => {
2364                ts = ctx
2365                    .chain_index()
2366                    .load_required_tipset(ts.parents())
2367                    .map_err(|e| anyhow::anyhow!("getting parent tipset: {e}"))?;
2368            }
2369            Err(e) => return Err(e.into()),
2370        }
2371    };
2372    let Some(msg_rct) = api_invoc_result.msg_rct else {
2373        return Err(anyhow::anyhow!("no message receipt").into());
2374    };
2375    if !msg_rct.exit_code().is_success() || !api_invoc_result.error.is_empty() {
2376        return Err(
2377            anyhow::anyhow!("failed to lookup storage slot: {}", api_invoc_result.error).into(),
2378        );
2379    }
2380
2381    let mut ret = fvm_ipld_encoding::from_slice::<RawBytes>(msg_rct.return_data().as_slice())?
2382        .bytes()
2383        .to_vec();
2384    if ret.len() < EVM_WORD_LENGTH {
2385        let mut with_padding = vec![0; EVM_WORD_LENGTH.saturating_sub(ret.len())];
2386        with_padding.append(&mut ret);
2387        Ok(EthBytes(with_padding))
2388    } else {
2389        Ok(EthBytes(ret))
2390    }
2391}
2392
2393pub enum EthGetTransactionCount {}
2394impl RpcMethod<2> for EthGetTransactionCount {
2395    const NAME: &'static str = "Filecoin.EthGetTransactionCount";
2396    const NAME_ALIAS: Option<&'static str> = Some("eth_getTransactionCount");
2397    const PARAM_NAMES: [&'static str; 2] = ["sender", "blockParam"];
2398    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2399    const PERMISSION: Permission = Permission::Read;
2400    const DESCRIPTION: &'static str = "Returns the number of transactions sent from an address (its nonce) at the specified block state.";
2401
2402    type Params = (EthAddress, BlockNumberOrHash);
2403    type Ok = EthUint64;
2404
2405    async fn handle(
2406        ctx: Ctx,
2407        (sender, block_param): Self::Params,
2408        ext: &http::Extensions,
2409    ) -> Result<Self::Ok, ServerError> {
2410        let addr = sender.to_filecoin_address()?;
2411        match block_param {
2412            BlockNumberOrHash::PredefinedBlock(Predefined::Pending) => {
2413                Ok(EthUint64(ctx.mpool.get_sequence(&addr).await?))
2414            }
2415            _ => {
2416                let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
2417                let ts = resolver
2418                    .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
2419                    .await?;
2420                eth_get_transaction_count(&ctx, &ts, addr).await
2421            }
2422        }
2423    }
2424}
2425
2426async fn eth_get_transaction_count(
2427    ctx: &Ctx,
2428    ts: &Tipset,
2429    addr: FilecoinAddress,
2430) -> Result<EthUint64, ServerError> {
2431    let TipsetState { state_root, .. } = ctx.state_manager.load_tipset_state(ts).await?;
2432
2433    let state_tree = ctx.state_manager.get_state_tree(&state_root)?;
2434    let actor = match state_tree.get_actor(&addr)? {
2435        Some(actor) => actor,
2436        None => return Ok(EthUint64(0)),
2437    };
2438
2439    if is_evm_actor(&actor.code) {
2440        let evm_state = evm::State::load(ctx.db(), actor.code, actor.state)?;
2441        if !evm_state.is_alive() {
2442            return Ok(EthUint64(0));
2443        }
2444        Ok(EthUint64(evm_state.nonce()))
2445    } else {
2446        Ok(EthUint64(actor.sequence))
2447    }
2448}
2449
2450pub enum EthMaxPriorityFeePerGas {}
2451impl RpcMethod<0> for EthMaxPriorityFeePerGas {
2452    const NAME: &'static str = "Filecoin.EthMaxPriorityFeePerGas";
2453    const NAME_ALIAS: Option<&'static str> = Some("eth_maxPriorityFeePerGas");
2454    const PARAM_NAMES: [&'static str; 0] = [];
2455    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2456    const PERMISSION: Permission = Permission::Read;
2457    const DESCRIPTION: &'static str = "Returns an estimate of the priority fee per gas (tip) needed for timely inclusion, in attoFIL.";
2458
2459    type Params = ();
2460    type Ok = EthBigInt;
2461
2462    async fn handle(
2463        ctx: Ctx,
2464        (): Self::Params,
2465        _: &http::Extensions,
2466    ) -> Result<Self::Ok, ServerError> {
2467        match gas::estimate_gas_premium(&ctx, 0, &ApiTipsetKey(None)).await {
2468            Ok(gas_premium) => Ok(gas_premium.atto().into()),
2469            Err(_) => Ok(EthBigInt::default()),
2470        }
2471    }
2472}
2473
2474pub enum EthProtocolVersion {}
2475impl RpcMethod<0> for EthProtocolVersion {
2476    const NAME: &'static str = "Filecoin.EthProtocolVersion";
2477    const NAME_ALIAS: Option<&'static str> = Some("eth_protocolVersion");
2478    const PARAM_NAMES: [&'static str; 0] = [];
2479    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2480    const PERMISSION: Permission = Permission::Read;
2481    const DESCRIPTION: &'static str =
2482        "Returns the current Filecoin network version, reported as the Ethereum protocol version.";
2483
2484    type Params = ();
2485    type Ok = EthUint64;
2486
2487    async fn handle(
2488        ctx: Ctx,
2489        (): Self::Params,
2490        _: &http::Extensions,
2491    ) -> Result<Self::Ok, ServerError> {
2492        let epoch = ctx.chain_store().heaviest_tipset().epoch();
2493        let version = u32::from(ctx.state_manager.get_network_version(epoch).0);
2494        Ok(EthUint64(version.into()))
2495    }
2496}
2497
2498pub enum EthGetTransactionByBlockNumberAndIndex {}
2499impl RpcMethod<2> for EthGetTransactionByBlockNumberAndIndex {
2500    const NAME: &'static str = "Filecoin.EthGetTransactionByBlockNumberAndIndex";
2501    const NAME_ALIAS: Option<&'static str> = Some("eth_getTransactionByBlockNumberAndIndex");
2502    const PARAM_NAMES: [&'static str; 2] = ["blockParam", "txIndex"];
2503    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2504    const PERMISSION: Permission = Permission::Read;
2505    const DESCRIPTION: &'static str = "Retrieves a transaction by its block number and index.";
2506
2507    type Params = (BlockNumberOrPredefined, EthUint64);
2508    type Ok = Option<ApiEthTx>;
2509
2510    async fn handle(
2511        ctx: Ctx,
2512        (block_param, tx_index): Self::Params,
2513        ext: &http::Extensions,
2514    ) -> Result<Self::Ok, ServerError> {
2515        let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
2516        let ts = resolver
2517            .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::Fail)
2518            .await?;
2519        eth_tx_by_tipset_and_idx(&ctx, &ts, tx_index).await
2520    }
2521}
2522
2523async fn eth_tx_by_tipset_and_idx(
2524    ctx: &Ctx,
2525    ts: &Tipset,
2526    tx_index: EthUint64,
2527) -> Result<Option<ApiEthTx>, ServerError> {
2528    let messages = ctx.chain_store().messages_for_tipset(ts)?;
2529
2530    let EthUint64(index) = tx_index;
2531    let msg = messages.get(index as usize).with_context(|| {
2532        format!(
2533            "transaction index {index} out of range: tipset contains {} messages",
2534            messages.len()
2535        )
2536    })?;
2537
2538    // Resolve addresses against the tipset's post-execution state so that newly created actors
2539    // are included correctly.
2540    let TipsetState { state_root, .. } = ctx.state_manager.load_tipset_state(ts).await?;
2541    let state = ctx.state_manager.get_state_tree(&state_root)?;
2542
2543    let tx = new_eth_tx(ctx, &state, ts.epoch(), &ts.key().cid()?, &msg.cid(), index)?;
2544
2545    Ok(Some(tx))
2546}
2547
2548pub enum EthGetTransactionByBlockHashAndIndex {}
2549impl RpcMethod<2> for EthGetTransactionByBlockHashAndIndex {
2550    const NAME: &'static str = "Filecoin.EthGetTransactionByBlockHashAndIndex";
2551    const NAME_ALIAS: Option<&'static str> = Some("eth_getTransactionByBlockHashAndIndex");
2552    const PARAM_NAMES: [&'static str; 2] = ["blockHash", "txIndex"];
2553    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2554    const PERMISSION: Permission = Permission::Read;
2555    const DESCRIPTION: &'static str = "Retrieves a transaction by its block hash and index.";
2556
2557    type Params = (EthHash, EthUint64);
2558    type Ok = Option<ApiEthTx>;
2559
2560    async fn handle(
2561        ctx: Ctx,
2562        (block_hash, tx_index): Self::Params,
2563        _: &http::Extensions,
2564    ) -> Result<Self::Ok, ServerError> {
2565        let ts = get_tipset_from_hash(ctx.chain_store(), &block_hash)?;
2566        eth_tx_by_tipset_and_idx(&ctx, &ts, tx_index).await
2567    }
2568}
2569
2570pub enum EthGetTransactionByHash {}
2571impl RpcMethod<1> for EthGetTransactionByHash {
2572    const NAME: &'static str = "Filecoin.EthGetTransactionByHash";
2573    const NAME_ALIAS: Option<&'static str> = Some("eth_getTransactionByHash");
2574    const PARAM_NAMES: [&'static str; 1] = ["txHash"];
2575    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2576    const PERMISSION: Permission = Permission::Read;
2577    const DESCRIPTION: &'static str = "Retrieves a transaction by its hash.";
2578
2579    type Params = (EthHash,);
2580    type Ok = Option<ApiEthTx>;
2581
2582    async fn handle(
2583        ctx: Ctx,
2584        (tx_hash,): Self::Params,
2585        _: &http::Extensions,
2586    ) -> Result<Self::Ok, ServerError> {
2587        let cancellation_token = CancellationToken::new();
2588        let _drop_guard = cancellation_token.drop_guard_ref();
2589        get_eth_transaction_by_hash(&ctx, &tx_hash, None, &cancellation_token).await
2590    }
2591}
2592
2593pub enum EthGetTransactionByHashLimited {}
2594impl RpcMethod<2> for EthGetTransactionByHashLimited {
2595    const NAME: &'static str = "Filecoin.EthGetTransactionByHashLimited";
2596    const NAME_ALIAS: Option<&'static str> = Some("eth_getTransactionByHashLimited");
2597    const PARAM_NAMES: [&'static str; 2] = ["txHash", "limit"];
2598    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2599    const PERMISSION: Permission = Permission::Read;
2600    const DESCRIPTION: &'static str =
2601        "Retrieves a transaction by its hash, limiting state resolution to the given chain epoch.";
2602
2603    type Params = (EthHash, ChainEpoch);
2604    type Ok = Option<ApiEthTx>;
2605
2606    async fn handle(
2607        ctx: Ctx,
2608        (tx_hash, limit): Self::Params,
2609        _: &http::Extensions,
2610    ) -> Result<Self::Ok, ServerError> {
2611        let cancellation_token = CancellationToken::new();
2612        let _drop_guard = cancellation_token.drop_guard_ref();
2613        get_eth_transaction_by_hash(&ctx, &tx_hash, Some(limit), &cancellation_token).await
2614    }
2615}
2616
2617async fn get_eth_transaction_by_hash(
2618    ctx: &Ctx,
2619    tx_hash: &EthHash,
2620    limit: Option<ChainEpoch>,
2621    cancellation_token: &CancellationToken,
2622) -> Result<Option<ApiEthTx>, ServerError> {
2623    let message_cid = ctx.chain_store().get_mapping(tx_hash)?.unwrap_or_else(|| {
2624        tracing::debug!(
2625            "could not find transaction hash {} in Ethereum mapping",
2626            tx_hash
2627        );
2628        // This isn't an eth transaction we have the mapping for, so let's look it up as a filecoin message
2629        tx_hash.to_cid()
2630    });
2631
2632    // First, try to get the cid from mined transactions
2633    if let Ok(Some((tipset, receipt))) = ctx
2634        .state_manager
2635        .search_for_message(None, message_cid, limit, Some(true), cancellation_token)
2636        .await
2637    {
2638        let ipld = receipt.return_data().deserialize().unwrap_or(Ipld::Null);
2639        let message_lookup = MessageLookup {
2640            receipt,
2641            tipset: tipset.key().clone(),
2642            height: tipset.epoch(),
2643            message: message_cid,
2644            return_dec: ipld,
2645        };
2646
2647        if let Ok(tx) = new_eth_tx_from_message_lookup(ctx, &message_lookup, None) {
2648            return Ok(Some(tx));
2649        }
2650    }
2651
2652    // If not found, try to get it from the mempool
2653    let (pending, _) = ctx.mpool.pending();
2654
2655    if let Some(smsg) = pending.iter().find(|item| item.cid() == message_cid) {
2656        // We only return pending eth-account messages because we can't guarantee
2657        // that the from/to addresses of other messages are conversable to 0x-style
2658        // addresses. So we just ignore them.
2659        //
2660        // This should be "fine" as anyone using an "Ethereum-centric" block
2661        // explorer shouldn't care about seeing pending messages from native
2662        // accounts.
2663        if let Ok(eth_tx) = EthTx::from_signed_message(ctx.chain_config().eth_chain_id, smsg) {
2664            return Ok(Some(eth_tx.into()));
2665        }
2666    }
2667
2668    // Ethereum clients expect an empty response when the message was not found
2669    Ok(None)
2670}
2671
2672pub enum EthGetTransactionHashByCid {}
2673
2674impl EthGetTransactionHashByCid {
2675    fn run(db: &DbImpl, eth_chain_id: EthChainIdType, cid: Cid) -> anyhow::Result<Option<EthHash>> {
2676        let smsgs_result: Result<Vec<SignedMessage>, crate::chain::Error> =
2677            crate::chain::messages_from_cids(db, &[cid]);
2678        if let Ok(smsgs) = smsgs_result
2679            && let Some(smsg) = smsgs.first()
2680        {
2681            return Ok(Some(eth_tx_hash_from_signed_message(smsg, eth_chain_id)?));
2682        }
2683
2684        let msg_result = crate::chain::get_chain_message(db, &cid);
2685        if let Ok(msg) = msg_result {
2686            return Ok(Some(msg.cid().into()));
2687        }
2688
2689        Ok(None)
2690    }
2691}
2692
2693impl RpcMethod<1> for EthGetTransactionHashByCid {
2694    const NAME: &'static str = "Filecoin.EthGetTransactionHashByCid";
2695    const NAME_ALIAS: Option<&'static str> = Some("eth_getTransactionHashByCid");
2696    const PARAM_NAMES: [&'static str; 1] = ["cid"];
2697    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2698    const PERMISSION: Permission = Permission::Read;
2699    const DESCRIPTION: &'static str =
2700        "Returns the Ethereum transaction hash for the given Filecoin message CID.";
2701
2702    type Params = (Cid,);
2703    type Ok = Option<EthHash>;
2704
2705    async fn handle(
2706        ctx: Ctx,
2707        (cid,): Self::Params,
2708        _: &http::Extensions,
2709    ) -> Result<Self::Ok, ServerError> {
2710        Ok(Self::run(ctx.db(), ctx.chain_config().eth_chain_id, cid)?)
2711    }
2712}
2713
2714pub enum EthCall {}
2715impl RpcMethod<2> for EthCall {
2716    const NAME: &'static str = "Filecoin.EthCall";
2717    const NAME_ALIAS: Option<&'static str> = Some("eth_call");
2718    const N_REQUIRED_PARAMS: usize = 2;
2719    const PARAM_NAMES: [&'static str; 2] = ["tx", "blockParam"];
2720    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2721    const PERMISSION: Permission = Permission::Read;
2722    const DESCRIPTION: &'static str = "Executes a read-only message call against the given block state without creating an on-chain transaction, returning the call output.";
2723    type Params = (EthCallMessage, BlockNumberOrHash);
2724    type Ok = EthBytes;
2725    async fn handle(
2726        ctx: Ctx,
2727        (tx, block_param): Self::Params,
2728        ext: &http::Extensions,
2729    ) -> Result<Self::Ok, ServerError> {
2730        let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
2731        let ts = resolver
2732            .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
2733            .await?;
2734        eth_call(&ctx, tx, ts).await
2735    }
2736}
2737
2738async fn eth_call(ctx: &Ctx, tx: EthCallMessage, ts: Tipset) -> Result<EthBytes, ServerError> {
2739    let msg = Message::try_from(tx)?;
2740    let invoke_result = apply_message(ctx, Some(ts), msg.clone()).await?;
2741
2742    if msg.to() == FilecoinAddress::ETHEREUM_ACCOUNT_MANAGER_ACTOR {
2743        Ok(EthBytes::default())
2744    } else {
2745        let msg_rct = invoke_result.msg_rct.context("no message receipt")?;
2746        let return_data = msg_rct.return_data();
2747        if return_data.is_empty() {
2748            Ok(Default::default())
2749        } else {
2750            let bytes = decode_payload(&return_data, CBOR)?;
2751            Ok(bytes)
2752        }
2753    }
2754}
2755
2756pub enum EthNewFilter {}
2757impl RpcMethod<1> for EthNewFilter {
2758    const NAME: &'static str = "Filecoin.EthNewFilter";
2759    const NAME_ALIAS: Option<&'static str> = Some("eth_newFilter");
2760    const PARAM_NAMES: [&'static str; 1] = ["filterSpec"];
2761    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2762    const PERMISSION: Permission = Permission::Read;
2763    const DESCRIPTION: &'static str =
2764        "Installs a persistent filter for matching event logs based on the given filter spec.";
2765
2766    type Params = (EthFilterSpec,);
2767    type Ok = FilterID;
2768
2769    async fn handle(
2770        ctx: Ctx,
2771        (filter_spec,): Self::Params,
2772        _: &http::Extensions,
2773    ) -> Result<Self::Ok, ServerError> {
2774        let eth_event_handler = ctx.eth_event_handler.clone();
2775        let chain_height = ctx.chain_store().heaviest_tipset().epoch();
2776        Ok(eth_event_handler.eth_new_filter(&filter_spec, chain_height)?)
2777    }
2778}
2779
2780pub enum EthNewPendingTransactionFilter {}
2781impl RpcMethod<0> for EthNewPendingTransactionFilter {
2782    const NAME: &'static str = "Filecoin.EthNewPendingTransactionFilter";
2783    const NAME_ALIAS: Option<&'static str> = Some("eth_newPendingTransactionFilter");
2784    const PARAM_NAMES: [&'static str; 0] = [];
2785    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2786    const PERMISSION: Permission = Permission::Read;
2787    const DESCRIPTION: &'static str =
2788        "Installs a persistent filter that tracks new messages arriving in the message pool.";
2789
2790    type Params = ();
2791    type Ok = FilterID;
2792
2793    async fn handle(
2794        ctx: Ctx,
2795        (): Self::Params,
2796        _: &http::Extensions,
2797    ) -> Result<Self::Ok, ServerError> {
2798        let eth_event_handler = ctx.eth_event_handler.clone();
2799        Ok(eth_event_handler.eth_new_pending_transaction_filter()?)
2800    }
2801}
2802
2803pub enum EthNewBlockFilter {}
2804impl RpcMethod<0> for EthNewBlockFilter {
2805    const NAME: &'static str = "Filecoin.EthNewBlockFilter";
2806    const NAME_ALIAS: Option<&'static str> = Some("eth_newBlockFilter");
2807    const PARAM_NAMES: [&'static str; 0] = [];
2808    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2809    const PERMISSION: Permission = Permission::Read;
2810    const DESCRIPTION: &'static str =
2811        "Installs a persistent filter that tracks the arrival of new blocks.";
2812
2813    type Params = ();
2814    type Ok = FilterID;
2815
2816    async fn handle(
2817        ctx: Ctx,
2818        (): Self::Params,
2819        _: &http::Extensions,
2820    ) -> Result<Self::Ok, ServerError> {
2821        let eth_event_handler = ctx.eth_event_handler.clone();
2822
2823        Ok(eth_event_handler.eth_new_block_filter()?)
2824    }
2825}
2826
2827pub enum EthUninstallFilter {}
2828impl RpcMethod<1> for EthUninstallFilter {
2829    const NAME: &'static str = "Filecoin.EthUninstallFilter";
2830    const NAME_ALIAS: Option<&'static str> = Some("eth_uninstallFilter");
2831    const PARAM_NAMES: [&'static str; 1] = ["filterId"];
2832    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2833    const PERMISSION: Permission = Permission::Read;
2834    const DESCRIPTION: &'static str = "Uninstalls the filter with the given ID.";
2835
2836    type Params = (FilterID,);
2837    type Ok = bool;
2838
2839    async fn handle(
2840        ctx: Ctx,
2841        (filter_id,): Self::Params,
2842        _: &http::Extensions,
2843    ) -> Result<Self::Ok, ServerError> {
2844        let eth_event_handler = ctx.eth_event_handler.clone();
2845
2846        Ok(eth_event_handler.eth_uninstall_filter(&filter_id)?)
2847    }
2848}
2849
2850pub enum EthUnsubscribe {}
2851impl RpcMethod<0> for EthUnsubscribe {
2852    const NAME: &'static str = "Filecoin.EthUnsubscribe";
2853    const NAME_ALIAS: Option<&'static str> = Some("eth_unsubscribe");
2854    const PARAM_NAMES: [&'static str; 0] = [];
2855    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2856    const PERMISSION: Permission = Permission::Read;
2857    const DESCRIPTION: &'static str =
2858        "Cancels an existing websocket subscription identified by its subscription ID.";
2859    const SUBSCRIPTION: bool = true;
2860
2861    type Params = ();
2862    type Ok = ();
2863
2864    // This method is a placeholder and is never actually called.
2865    // Subscription handling is performed in [`pubsub.rs`](pubsub).
2866    //
2867    // We still need to implement the [`RpcMethod`] trait to expose method metadata
2868    // like [`NAME`](Self::NAME), [`NAME_ALIAS`](Self::NAME_ALIAS), [`PERMISSION`](Self::PERMISSION), etc..
2869    async fn handle(
2870        _: Ctx,
2871        (): Self::Params,
2872        _: &http::Extensions,
2873    ) -> Result<Self::Ok, ServerError> {
2874        Ok(())
2875    }
2876}
2877
2878pub enum EthSubscribe {}
2879impl RpcMethod<0> for EthSubscribe {
2880    const NAME: &'static str = "Filecoin.EthSubscribe";
2881    const NAME_ALIAS: Option<&'static str> = Some("eth_subscribe");
2882    const PARAM_NAMES: [&'static str; 0] = [];
2883    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2884    const PERMISSION: Permission = Permission::Read;
2885    const DESCRIPTION: &'static str = "Creates a websocket subscription that streams events (new heads, pending transactions, or logs) to the client.";
2886    const SUBSCRIPTION: bool = true;
2887
2888    type Params = ();
2889    type Ok = ();
2890
2891    // This method is a placeholder and is never actually called.
2892    // Subscription handling is performed in [`pubsub.rs`](pubsub).
2893    //
2894    // We still need to implement the [`RpcMethod`] trait to expose method metadata
2895    // like [`NAME`](Self::NAME), [`NAME_ALIAS`](Self::NAME_ALIAS), [`PERMISSION`](Self::PERMISSION), etc..
2896    async fn handle(
2897        _: Ctx,
2898        (): Self::Params,
2899        _: &http::Extensions,
2900    ) -> Result<Self::Ok, ServerError> {
2901        Ok(())
2902    }
2903}
2904
2905pub enum EthAddressToFilecoinAddress {}
2906impl RpcMethod<1> for EthAddressToFilecoinAddress {
2907    const NAME: &'static str = "Filecoin.EthAddressToFilecoinAddress";
2908    const PARAM_NAMES: [&'static str; 1] = ["ethAddress"];
2909    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2910    const PERMISSION: Permission = Permission::Read;
2911    const DESCRIPTION: &'static str = "Converts an EthAddress into an f410 Filecoin Address";
2912    type Params = (EthAddress,);
2913    type Ok = FilecoinAddress;
2914    async fn handle(
2915        _ctx: Ctx,
2916        (eth_address,): Self::Params,
2917        _: &http::Extensions,
2918    ) -> Result<Self::Ok, ServerError> {
2919        Ok(eth_address.to_filecoin_address()?)
2920    }
2921}
2922
2923pub enum FilecoinAddressToEthAddress {}
2924impl RpcMethod<2> for FilecoinAddressToEthAddress {
2925    const NAME: &'static str = "Filecoin.FilecoinAddressToEthAddress";
2926    const N_REQUIRED_PARAMS: usize = 1;
2927    const PARAM_NAMES: [&'static str; 2] = ["filecoinAddress", "blockParam"];
2928    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2929    const PERMISSION: Permission = Permission::Read;
2930    const DESCRIPTION: &'static str = "Converts any Filecoin address to an EthAddress";
2931    type Params = (FilecoinAddress, Option<BlockNumberOrPredefined>);
2932    type Ok = EthAddress;
2933    async fn handle(
2934        ctx: Ctx,
2935        (address, block_param): Self::Params,
2936        ext: &http::Extensions,
2937    ) -> Result<Self::Ok, ServerError> {
2938        if let Ok(eth_address) = EthAddress::from_filecoin_address(&address) {
2939            Ok(eth_address)
2940        } else {
2941            let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
2942            // Default to Finalized for Lotus parity
2943            let block_param = block_param.unwrap_or_else(|| Predefined::Finalized.into());
2944            let ts = resolver
2945                .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
2946                .await?;
2947
2948            let id_address = ctx.state_manager.lookup_required_id(&address, &ts)?;
2949            Ok(EthAddress::from_filecoin_address(&id_address)?)
2950        }
2951    }
2952}
2953
2954async fn get_eth_transaction_receipt_with_cache(
2955    ctx: Ctx,
2956    tx_hash: EthHash,
2957    limit: Option<ChainEpoch>,
2958    cancellation_token: &CancellationToken,
2959) -> Result<Option<EthTxReceipt>, ServerError> {
2960    static CACHE: LazyLock<SizeTrackingCache<EthHash, EthTxReceipt>> = LazyLock::new(|| {
2961        const DEFAULT_CACHE_SIZE: NonZeroUsize = nonzero!(10000usize); // ~12.5MiB on mainnet
2962        let cache_size = env_or_default(
2963            "FOREST_ETH_TRANSACTION_RECEIPT_CACHE_SIZE",
2964            DEFAULT_CACHE_SIZE,
2965        );
2966        SizeTrackingCache::new_with_metrics("eth_transaction_receipt", cache_size)
2967    });
2968
2969    enum TmpError {
2970        NotFound,
2971        Error(ServerError),
2972    }
2973
2974    // Do not update cache when not found by returning an error
2975    match CACHE
2976        .get_or_insert_async(&tx_hash, {
2977            let ctx = ctx.shallow_clone();
2978            async move {
2979                let receipt = get_eth_transaction_receipt(ctx, tx_hash, limit, cancellation_token)
2980                    .await
2981                    .map_err(TmpError::Error)?
2982                    .ok_or(TmpError::NotFound)?;
2983                Ok(receipt)
2984            }
2985        })
2986        .await
2987    {
2988        Ok(r) => {
2989            let Some(max_lookback_epoch_inclusive) = StateManager::max_lookback_epoch_inclusive(
2990                ctx.chain_store().heaviest_tipset().epoch(),
2991                limit,
2992            ) else {
2993                return Ok(None);
2994            };
2995            if r.block_number.0 >= max_lookback_epoch_inclusive {
2996                Ok(Some(r))
2997            } else {
2998                // Cache hit but beyond the lookback limit
2999                Ok(None)
3000            }
3001        }
3002        Err(TmpError::NotFound) => Ok(None),
3003        Err(TmpError::Error(e)) => Err(e),
3004    }
3005}
3006
3007async fn get_eth_transaction_receipt(
3008    ctx: Ctx,
3009    tx_hash: EthHash,
3010    limit: Option<ChainEpoch>,
3011    cancellation_token: &CancellationToken,
3012) -> Result<Option<EthTxReceipt>, ServerError> {
3013    let msg_cid = ctx.chain_store().get_mapping(&tx_hash)?.unwrap_or_else(|| {
3014        tracing::debug!(
3015            "could not find transaction hash {} in Ethereum mapping",
3016            tx_hash
3017        );
3018        // This isn't an eth transaction we have the mapping for, so let's look it up as a filecoin message
3019        tx_hash.to_cid()
3020    });
3021
3022    let option = ctx
3023        .state_manager
3024        .search_for_message(None, msg_cid, limit, Some(true), cancellation_token)
3025        .await
3026        .with_context(|| format!("failed to lookup Eth Txn {tx_hash} as {msg_cid}"));
3027
3028    // Ethereum clients expect an empty response when the message was not found
3029    // or not executed yet
3030    let (tipset, receipt) = match option {
3031        Ok(Some(found)) => found,
3032        Ok(None) => return Ok(None),
3033        Err(e) => {
3034            tracing::debug!("could not find transaction receipt for hash {tx_hash}: {e}");
3035            return Ok(None);
3036        }
3037    };
3038    let ipld = receipt.return_data().deserialize().unwrap_or(Ipld::Null);
3039    let message_lookup = MessageLookup {
3040        receipt,
3041        tipset: tipset.key().clone(),
3042        height: tipset.epoch(),
3043        message: msg_cid,
3044        return_dec: ipld,
3045    };
3046
3047    let tx = new_eth_tx_from_message_lookup(&ctx, &message_lookup, None)
3048        .with_context(|| format!("failed to convert {tx_hash} into an Eth Tx"))?;
3049
3050    let ts = ctx
3051        .chain_index()
3052        .load_required_tipset(&message_lookup.tipset)?;
3053
3054    // The tx is located in the parent tipset
3055    let parent_ts = ctx
3056        .chain_index()
3057        .load_required_tipset(ts.parents())
3058        .map_err(|e| {
3059            format!(
3060                "failed to lookup tipset {} when constructing the eth txn receipt: {}",
3061                ts.parents(),
3062                e
3063            )
3064        })?;
3065
3066    let tx_receipt =
3067        new_eth_tx_receipt(&ctx, &parent_ts, &tx, msg_cid, &message_lookup.receipt).await?;
3068
3069    Ok(Some(tx_receipt))
3070}
3071
3072pub enum EthGetTransactionReceipt {}
3073impl RpcMethod<1> for EthGetTransactionReceipt {
3074    const NAME: &'static str = "Filecoin.EthGetTransactionReceipt";
3075    const NAME_ALIAS: Option<&'static str> = Some("eth_getTransactionReceipt");
3076    const N_REQUIRED_PARAMS: usize = 1;
3077    const PARAM_NAMES: [&'static str; 1] = ["txHash"];
3078    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
3079    const PERMISSION: Permission = Permission::Read;
3080    const DESCRIPTION: &'static str =
3081        "Returns the receipt of a transaction identified by its hash.";
3082    type Params = (EthHash,);
3083    type Ok = Option<EthTxReceipt>;
3084    async fn handle(
3085        ctx: Ctx,
3086        (tx_hash,): Self::Params,
3087        _: &http::Extensions,
3088    ) -> Result<Self::Ok, ServerError> {
3089        let cancellation_token = CancellationToken::new();
3090        let _drop_guard = cancellation_token.drop_guard_ref();
3091        get_eth_transaction_receipt_with_cache(ctx, tx_hash, None, &cancellation_token).await
3092    }
3093}
3094
3095pub enum EthGetTransactionReceiptLimited {}
3096impl RpcMethod<2> for EthGetTransactionReceiptLimited {
3097    const NAME: &'static str = "Filecoin.EthGetTransactionReceiptLimited";
3098    const NAME_ALIAS: Option<&'static str> = Some("eth_getTransactionReceiptLimited");
3099    const N_REQUIRED_PARAMS: usize = 1;
3100    const PARAM_NAMES: [&'static str; 2] = ["txHash", "limit"];
3101    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
3102    const PERMISSION: Permission = Permission::Read;
3103    const DESCRIPTION: &'static str = "Returns the receipt of a transaction identified by its hash, limiting state resolution to the given chain epoch.";
3104    type Params = (EthHash, ChainEpoch);
3105    type Ok = Option<EthTxReceipt>;
3106    async fn handle(
3107        ctx: Ctx,
3108        (tx_hash, limit): Self::Params,
3109        _: &http::Extensions,
3110    ) -> Result<Self::Ok, ServerError> {
3111        let cancellation_token = CancellationToken::new();
3112        let _drop_guard = cancellation_token.drop_guard_ref();
3113        get_eth_transaction_receipt_with_cache(ctx, tx_hash, Some(limit), &cancellation_token).await
3114    }
3115}
3116
3117pub enum EthSendRawTransaction {}
3118impl RpcMethod<1> for EthSendRawTransaction {
3119    const NAME: &'static str = "Filecoin.EthSendRawTransaction";
3120    const NAME_ALIAS: Option<&'static str> = Some("eth_sendRawTransaction");
3121    const PARAM_NAMES: [&'static str; 1] = ["rawTx"];
3122    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
3123    const PERMISSION: Permission = Permission::Read;
3124    const DESCRIPTION: &'static str =
3125        "Submits a signed raw transaction to the message pool and returns its transaction hash.";
3126
3127    type Params = (EthBytes,);
3128    type Ok = EthHash;
3129
3130    async fn handle(
3131        ctx: Ctx,
3132        (raw_tx,): Self::Params,
3133        _: &http::Extensions,
3134    ) -> Result<Self::Ok, ServerError> {
3135        let tx_args = parse_eth_transaction(&raw_tx.0)?;
3136        let smsg = tx_args.get_signed_message(ctx.chain_config().eth_chain_id)?;
3137        let cid = ctx.mpool.push(smsg).await?;
3138        Ok(cid.into())
3139    }
3140}
3141
3142pub enum EthSendRawTransactionUntrusted {}
3143impl RpcMethod<1> for EthSendRawTransactionUntrusted {
3144    const NAME: &'static str = "Filecoin.EthSendRawTransactionUntrusted";
3145    const NAME_ALIAS: Option<&'static str> = Some("eth_sendRawTransactionUntrusted");
3146    const PARAM_NAMES: [&'static str; 1] = ["rawTx"];
3147    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
3148    const PERMISSION: Permission = Permission::Read;
3149    const DESCRIPTION: &'static str = "Submits a signed raw transaction from an untrusted source to the message pool and returns its transaction hash.";
3150
3151    type Params = (EthBytes,);
3152    type Ok = EthHash;
3153
3154    async fn handle(
3155        ctx: Ctx,
3156        (raw_tx,): Self::Params,
3157        _: &http::Extensions,
3158    ) -> Result<Self::Ok, ServerError> {
3159        let tx_args = parse_eth_transaction(&raw_tx.0)?;
3160        let smsg = tx_args.get_signed_message(ctx.chain_config().eth_chain_id)?;
3161        let cid = ctx.mpool.push_untrusted(smsg).await?;
3162        Ok(cid.into())
3163    }
3164}
3165
3166#[derive(Clone, Debug, PartialEq)]
3167pub struct CollectedEvent {
3168    pub(crate) entries: Vec<EventEntry>,
3169    pub(crate) emitter_addr: crate::shim::address::Address,
3170    pub(crate) event_idx: u64,
3171    pub(crate) reverted: bool,
3172    pub(crate) height: ChainEpoch,
3173    pub(crate) tipset_key: TipsetKey,
3174    msg_idx: u64,
3175    pub(crate) msg_cid: Cid,
3176}
3177
3178/// Positions `(message index, event index)` of collected events, grouped by tipset.
3179/// Identifies events without retaining their entry payloads; grouping by tipset lets
3180/// membership checks borrow the tipset key and stores each distinct key only once.
3181pub type SeenEventPositions = HashMap<TipsetKey, HashSet<(u64, u64)>>;
3182
3183fn match_key(key: &str) -> Option<usize> {
3184    match key.get(0..2) {
3185        Some("t1") => Some(0),
3186        Some("t2") => Some(1),
3187        Some("t3") => Some(2),
3188        Some("t4") => Some(3),
3189        _ => None,
3190    }
3191}
3192
3193fn eth_log_from_event(entries: &[EventEntry]) -> Option<(EthBytes, Vec<EthHash>)> {
3194    let mut topics_found = [false; 4];
3195    let mut topics_found_count = 0;
3196    let mut data_found = false;
3197    let mut data: EthBytes = EthBytes::default();
3198    let mut topics: Vec<EthHash> = Vec::default();
3199    for entry in entries {
3200        // Drop events with non-raw topics. Built-in actors emit CBOR, and anything else would be
3201        // invalid anyway.
3202        if entry.codec != IPLD_RAW {
3203            return None;
3204        }
3205        // Check if the key is t1..t4
3206        if let Some(idx) = match_key(&entry.key) {
3207            // Drop events with mis-sized topics.
3208            let result: Result<[u8; EVM_WORD_LENGTH], _> = entry.value.0.clone().try_into();
3209            let bytes = if let Ok(value) = result {
3210                value
3211            } else {
3212                tracing::warn!(
3213                    "got an EVM event topic with an invalid size (key: {}, size: {})",
3214                    entry.key,
3215                    entry.value.0.len()
3216                );
3217                return None;
3218            };
3219            // Drop events with duplicate topics.
3220            if *topics_found.get(idx).expect("Infallible") {
3221                tracing::warn!("got a duplicate EVM event topic (key: {})", entry.key);
3222                return None;
3223            }
3224            *topics_found.get_mut(idx).expect("Infallible") = true;
3225            topics_found_count += 1;
3226            // Extend the topics array
3227            if topics.len() <= idx {
3228                topics.resize(idx + 1, EthHash::default());
3229            }
3230            *topics.get_mut(idx).expect("Infallible") = bytes.into();
3231        } else if entry.key == "d" {
3232            // Drop events with duplicate data fields.
3233            if data_found {
3234                tracing::warn!("got duplicate EVM event data");
3235                return None;
3236            }
3237            data_found = true;
3238            data = EthBytes(entry.value.0.clone());
3239        } else {
3240            // Skip entries we don't understand (makes it easier to extend things).
3241            // But we warn for now because we don't expect them.
3242            tracing::warn!("unexpected event entry (key: {})", entry.key);
3243        }
3244    }
3245    // Drop events with skipped topics.
3246    if topics.len() != topics_found_count {
3247        tracing::warn!(
3248            "EVM event topic length mismatch (expected: {}, actual: {})",
3249            topics.len(),
3250            topics_found_count
3251        );
3252        return None;
3253    }
3254    Some((data, topics))
3255}
3256
3257pub(crate) fn eth_tx_hash_from_signed_message(
3258    message: &SignedMessage,
3259    eth_chain_id: EthChainIdType,
3260) -> anyhow::Result<EthHash> {
3261    if message.is_delegated() {
3262        let (_, tx) = eth_tx_from_signed_eth_message(message, eth_chain_id)?;
3263        Ok(tx.eth_hash()?.into())
3264    } else if message.is_secp256k1() {
3265        Ok(message.cid().into())
3266    } else {
3267        Ok(message.message().cid().into())
3268    }
3269}
3270
3271fn eth_tx_hash_from_message_cid<DB: Blockstore>(
3272    blockstore: &DB,
3273    message_cid: &Cid,
3274    eth_chain_id: EthChainIdType,
3275) -> anyhow::Result<Option<EthHash>> {
3276    if let Ok(smsg) = crate::chain::message_from_cid(blockstore, message_cid) {
3277        // This is an Eth Tx, Secp message, Or BLS message in the mpool
3278        return Ok(Some(eth_tx_hash_from_signed_message(&smsg, eth_chain_id)?));
3279    }
3280    let result: Result<Message, _> = crate::chain::message_from_cid(blockstore, message_cid);
3281    if result.is_ok() {
3282        // This is a BLS message
3283        let hash: EthHash = (*message_cid).into();
3284        return Ok(Some(hash));
3285    }
3286    Ok(None)
3287}
3288
3289fn eth_filter_logs_from_tipsets(events: &[CollectedEvent]) -> anyhow::Result<Vec<EthHash>> {
3290    events
3291        .iter()
3292        .map(|event| event.tipset_key.cid().map(Into::into))
3293        .collect()
3294}
3295
3296fn eth_filter_logs_from_events(
3297    ctx: &Ctx,
3298    events: &[CollectedEvent],
3299) -> anyhow::Result<Vec<EthLog>> {
3300    let chain_id = ctx.state_manager.chain_config().eth_chain_id;
3301    let mut tx_hash_by_msg: HashMap<Cid, EthHash> = HashMap::new();
3302    let mut block_hash_by_tipset: HashMap<TipsetKey, EthHash> = HashMap::new();
3303    let mut eth_addr_by_emitter: HashMap<FilecoinAddress, EthAddress> = HashMap::new();
3304
3305    let mut logs = Vec::with_capacity(events.len());
3306    for event in events {
3307        let (data, topics) = match eth_log_from_event(&event.entries) {
3308            Some(parts) => parts,
3309            None => {
3310                tracing::debug!("Ignoring event");
3311                continue;
3312            }
3313        };
3314
3315        let transaction_hash = if let Some(h) = tx_hash_by_msg.get(&event.msg_cid) {
3316            *h
3317        } else {
3318            match eth_tx_hash_from_message_cid(ctx.db(), &event.msg_cid, chain_id)? {
3319                Some(h) => {
3320                    tx_hash_by_msg.insert(event.msg_cid, h);
3321                    h
3322                }
3323                None => {
3324                    tracing::debug!("Ignoring event");
3325                    continue;
3326                }
3327            }
3328        };
3329
3330        let block_hash = if let Some(h) = block_hash_by_tipset.get(&event.tipset_key) {
3331            *h
3332        } else {
3333            let h: EthHash = event.tipset_key.cid()?.into();
3334            block_hash_by_tipset.insert(event.tipset_key.clone(), h);
3335            h
3336        };
3337
3338        let address = if let Some(a) = eth_addr_by_emitter.get(&event.emitter_addr) {
3339            *a
3340        } else {
3341            let a = EthAddress::from_filecoin_address(&event.emitter_addr)?;
3342            eth_addr_by_emitter.insert(event.emitter_addr, a);
3343            a
3344        };
3345
3346        logs.push(EthLog {
3347            address,
3348            data,
3349            topics,
3350            removed: event.reverted,
3351            log_index: event.event_idx.into(),
3352            transaction_index: event.msg_idx.into(),
3353            transaction_hash,
3354            block_hash,
3355            block_number: (event.height as u64).into(),
3356        });
3357    }
3358    Ok(logs)
3359}
3360
3361/// Accrues a single Ethereum log's address and topics into `bloom` using the standard `M3:2048` scheme.
3362fn eth_filter_result_from_events(
3363    ctx: &Ctx,
3364    events: &[CollectedEvent],
3365) -> anyhow::Result<EthFilterResult> {
3366    Ok(EthFilterResult::Logs(eth_filter_logs_from_events(
3367        ctx, events,
3368    )?))
3369}
3370
3371fn eth_filter_result_from_tipsets(events: &[CollectedEvent]) -> anyhow::Result<EthFilterResult> {
3372    Ok(EthFilterResult::Hashes(eth_filter_logs_from_tipsets(
3373        events,
3374    )?))
3375}
3376
3377pub enum EthGetLogs {}
3378impl RpcMethod<1> for EthGetLogs {
3379    const NAME: &'static str = "Filecoin.EthGetLogs";
3380    const NAME_ALIAS: Option<&'static str> = Some("eth_getLogs");
3381    const N_REQUIRED_PARAMS: usize = 1;
3382    const PARAM_NAMES: [&'static str; 1] = ["ethFilter"];
3383    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
3384    const PERMISSION: Permission = Permission::Read;
3385    const DESCRIPTION: &'static str = "Returns event logs matching the given filter specification.";
3386    type Params = (EthFilterSpec,);
3387    type Ok = EthFilterResult;
3388    async fn handle(
3389        ctx: Ctx,
3390        (eth_filter,): Self::Params,
3391        _: &http::Extensions,
3392    ) -> Result<Self::Ok, ServerError> {
3393        let pf = Arc::new(
3394            ctx.eth_event_handler
3395                .parse_eth_filter_spec(&ctx, &eth_filter)
3396                .map_err(|e| {
3397                    if e.downcast_ref::<EthErrors>().is_some_and(|eth_err| {
3398                        matches!(eth_err, EthErrors::BlockRangeExceeded { .. })
3399                    }) {
3400                        return e;
3401                    }
3402                    e.context("failed to parse events for filter")
3403                })?,
3404        );
3405        let events = ctx
3406            .eth_event_handler
3407            .get_events_for_parsed_filter(&ctx, &pf, SkipEvent::OnUnresolvedAddress)
3408            .await
3409            .context("failed to get events for filter")?;
3410        Ok(eth_filter_result_from_events(&ctx, &events)?)
3411    }
3412}
3413
3414/// Shared implementation of `eth_getFilterLogs` / `eth_getFilterChanges` for installed event
3415/// filters: collects the filter's full result set from the canonical chain, returns only the
3416/// events that were not present in the previous poll.
3417async fn poll_event_filter(
3418    ctx: &Ctx,
3419    event_filter: &EventFilter,
3420) -> anyhow::Result<Vec<CollectedEvent>> {
3421    let events = ctx
3422        .eth_event_handler
3423        .get_events_for_parsed_filter(
3424            ctx,
3425            &Arc::new(event_filter.into()),
3426            SkipEvent::OnUnresolvedAddress,
3427        )
3428        .await?;
3429    let mut seen_positions = SeenEventPositions::default();
3430    let mut recent_events = Vec::new();
3431    for event in events {
3432        let position = (event.msg_idx, event.event_idx);
3433        let already_seen = event_filter
3434            .seen_positions
3435            .get(&event.tipset_key)
3436            .is_some_and(|positions| positions.contains(&position));
3437        match seen_positions.get_mut(&event.tipset_key) {
3438            Some(positions) => {
3439                positions.insert(position);
3440            }
3441            None => {
3442                seen_positions.insert(event.tipset_key.clone(), HashSet::from_iter([position]));
3443            }
3444        }
3445        if !already_seen {
3446            recent_events.push(event);
3447        }
3448    }
3449    if let Some(store) = &ctx.eth_event_handler.filter_store {
3450        store.update(Arc::new(EventFilter {
3451            id: event_filter.id.clone(),
3452            tipsets: event_filter.tipsets.clone(),
3453            addresses: event_filter.addresses.clone(),
3454            keys_with_codec: event_filter.keys_with_codec.clone(),
3455            max_results: event_filter.max_results,
3456            seen_positions,
3457        }));
3458    }
3459    Ok(recent_events)
3460}
3461
3462pub enum EthGetFilterLogs {}
3463impl RpcMethod<1> for EthGetFilterLogs {
3464    const NAME: &'static str = "Filecoin.EthGetFilterLogs";
3465    const NAME_ALIAS: Option<&'static str> = Some("eth_getFilterLogs");
3466    const N_REQUIRED_PARAMS: usize = 1;
3467    const PARAM_NAMES: [&'static str; 1] = ["filterId"];
3468    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
3469    const PERMISSION: Permission = Permission::Write;
3470    const DESCRIPTION: &'static str = "Returns event logs matching the filter with the given ID that have not been collected since the last poll.";
3471    type Params = (FilterID,);
3472    type Ok = EthFilterResult;
3473    async fn handle(
3474        ctx: Ctx,
3475        (filter_id,): Self::Params,
3476        _: &http::Extensions,
3477    ) -> Result<Self::Ok, ServerError> {
3478        let eth_event_handler = ctx.eth_event_handler.clone();
3479        if let Some(store) = &eth_event_handler.filter_store {
3480            let filter = store.get(&filter_id)?;
3481            if let Some(event_filter) = filter.as_any().downcast_ref::<EventFilter>() {
3482                let recent_events = poll_event_filter(&ctx, event_filter).await?;
3483                return Ok(eth_filter_result_from_events(&ctx, &recent_events)?);
3484            }
3485        }
3486        Err(anyhow::anyhow!("method not supported").into())
3487    }
3488}
3489
3490pub enum EthGetFilterChanges {}
3491impl RpcMethod<1> for EthGetFilterChanges {
3492    const NAME: &'static str = "Filecoin.EthGetFilterChanges";
3493    const NAME_ALIAS: Option<&'static str> = Some("eth_getFilterChanges");
3494    const N_REQUIRED_PARAMS: usize = 1;
3495    const PARAM_NAMES: [&'static str; 1] = ["filterId"];
3496    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
3497    const PERMISSION: Permission = Permission::Write;
3498    const DESCRIPTION: &'static str = "Returns event logs which occurred since the last poll";
3499
3500    type Params = (FilterID,);
3501    type Ok = EthFilterResult;
3502    async fn handle(
3503        ctx: Ctx,
3504        (filter_id,): Self::Params,
3505        _: &http::Extensions,
3506    ) -> Result<Self::Ok, ServerError> {
3507        let eth_event_handler = ctx.eth_event_handler.clone();
3508        if let Some(store) = &eth_event_handler.filter_store {
3509            let filter = store.get(&filter_id)?;
3510            if let Some(event_filter) = filter.as_any().downcast_ref::<EventFilter>() {
3511                let recent_events = poll_event_filter(&ctx, event_filter).await?;
3512                return Ok(eth_filter_result_from_events(&ctx, &recent_events)?);
3513            }
3514            if let Some(tipset_filter) = filter.as_any().downcast_ref::<TipSetFilter>() {
3515                let events = ctx
3516                    .eth_event_handler
3517                    .get_events_for_parsed_filter(
3518                        &ctx,
3519                        &Arc::new(ParsedFilter::new_with_tipset(ParsedFilterTipsets::Range(
3520                            // heaviest tipset doesn't have events because its messages haven't been executed yet
3521                            RangeInclusive::new(
3522                                tipset_filter
3523                                    .collected
3524                                    .unwrap_or(ctx.chain_store().heaviest_tipset().epoch() - 1),
3525                                // Use -1 to indicate that the range extends until the latest available tipset.
3526                                -1,
3527                            ),
3528                        ))),
3529                        SkipEvent::OnUnresolvedAddress,
3530                    )
3531                    .await?;
3532                let new_collected = events
3533                    .iter()
3534                    .max_by_key(|event| event.height)
3535                    .map(|e| e.height);
3536                if let Some(height) = new_collected {
3537                    let filter = Arc::new(TipSetFilter {
3538                        id: tipset_filter.id.clone(),
3539                        max_results: tipset_filter.max_results,
3540                        collected: Some(height),
3541                    });
3542                    store.update(filter);
3543                }
3544                return Ok(eth_filter_result_from_tipsets(&events)?);
3545            }
3546            if let Some(mempool_filter) = filter.as_any().downcast_ref::<MempoolFilter>() {
3547                return Ok(EthFilterResult::Hashes(mempool_filter.drain()));
3548            }
3549        }
3550        Err(anyhow::anyhow!("method not supported").into())
3551    }
3552}
3553
3554pub enum EthTraceBlock {}
3555impl RpcMethod<1> for EthTraceBlock {
3556    const NAME: &'static str = "Filecoin.EthTraceBlock";
3557    const NAME_ALIAS: Option<&'static str> = Some("trace_block");
3558    const N_REQUIRED_PARAMS: usize = 1;
3559    const PARAM_NAMES: [&'static str; 1] = ["blockParam"];
3560    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
3561    const PERMISSION: Permission = Permission::Read;
3562    const DESCRIPTION: &'static str = "Returns traces created at given block.";
3563
3564    type Params = (BlockNumberOrHash,);
3565    type Ok = NotNullVec<EthBlockTrace>;
3566    async fn handle(
3567        ctx: Ctx,
3568        (block_param,): Self::Params,
3569        ext: &http::Extensions,
3570    ) -> Result<Self::Ok, ServerError> {
3571        let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
3572        let ts = resolver
3573            .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::Fail)
3574            .await?;
3575        eth_trace_block(&ctx.state_manager, &ts)
3576            .await
3577            .map(NotNullVec)
3578    }
3579}
3580
3581/// Replays a tipset and resolves every non-system transaction into a [`trace::TipsetTraceEntry`].
3582async fn execute_tipset_traces(
3583    state_manager: &StateManager,
3584    ts: &Tipset,
3585) -> Result<(StateTree<DbImpl>, Vec<trace::TipsetTraceEntry>), ServerError> {
3586    let (state_root, raw_traces) = state_manager.execution_trace(ts).await?;
3587    let state = state_manager.get_state_tree(&state_root)?;
3588
3589    // Resolve every non-system message's tx hash in parallel. Each lookup is
3590    // an independent DB read; running them sequentially adds O(N) IO
3591    // latency to every trace_block response.
3592    let raw = non_system_traces_with_positions(raw_traces).collect_vec();
3593    let mut entries: Vec<trace::TipsetTraceEntry> = Vec::with_capacity(raw.len());
3594    let mut join_set = tokio::task::JoinSet::new();
3595    let db = state_manager.db();
3596    let eth_chain_id = state_manager.chain_config().eth_chain_id;
3597    for (msg_position, invoc_result) in raw {
3598        let db = db.shallow_clone();
3599        join_set.spawn_blocking(move || {
3600            let tx_hash = EthGetTransactionHashByCid::run(&db, eth_chain_id, invoc_result.msg_cid)?
3601                .with_context(|| {
3602                    format!(
3603                        "cannot find transaction hash for cid {}",
3604                        invoc_result.msg_cid
3605                    )
3606                })?;
3607            anyhow::Ok(trace::TipsetTraceEntry {
3608                tx_hash,
3609                msg_position,
3610                invoc_result,
3611            })
3612        });
3613    }
3614    while let Some(joined) = join_set.join_next().await {
3615        entries.push(joined.context("trace tx-hash task panicked")??);
3616    }
3617    entries.sort_by_key(|e| e.msg_position);
3618
3619    Ok((state, entries))
3620}
3621
3622/// Yields non-system traces paired with 0-indexed positions matching
3623/// `transactionIndex` from `eth_getBlockByNumber`. System-actor messages
3624/// are filtered out without consuming a position.
3625fn non_system_traces_with_positions(
3626    raw_traces: impl IntoIterator<Item = Arc<ApiInvocResult>>,
3627) -> impl Iterator<Item = (i64, Arc<ApiInvocResult>)> {
3628    raw_traces
3629        .into_iter()
3630        .filter(|ir| ir.msg.from != system::ADDRESS.into())
3631        .enumerate()
3632        .map(|(idx, ir)| (idx as i64, ir))
3633}
3634
3635/// Builds the Parity-style block traces for `ts`, caching the result by tipset.
3636/// Unlike [`StateManager::execution_trace`], this also caches the tx-hash
3637/// lookups and parity-trace construction.
3638pub(crate) async fn eth_trace_block(
3639    state_manager: &StateManager,
3640    ts: &Tipset,
3641) -> Result<Vec<EthBlockTrace>, ServerError> {
3642    // 64 most-recent blocks; bounded by count, not bytes (a few MiB on mainnet,
3643    // see the `cache_eth_trace_block_size` metric).
3644    const ETH_TRACE_BLOCK_CACHE_SIZE: NonZeroUsize = nonzero!(64usize);
3645    static ETH_TRACE_BLOCK_CACHE: LazyLock<SizeTrackingCache<CidWrapper, Arc<Vec<EthBlockTrace>>>> =
3646        LazyLock::new(|| {
3647            SizeTrackingCache::new_with_metrics("eth_trace_block", ETH_TRACE_BLOCK_CACHE_SIZE)
3648        });
3649
3650    let block_cid = ts.key().cid()?;
3651    let traces = ETH_TRACE_BLOCK_CACHE
3652        .get_or_insert_async(&CidWrapper::from(block_cid), async move {
3653            let (state, entries) = execute_tipset_traces(state_manager, ts).await?;
3654            let block_hash: EthHash = block_cid.into();
3655            let mut all_traces = vec![];
3656
3657            for entry in entries {
3658                for trace in entry.build_parity_traces(&state)? {
3659                    all_traces.push(EthBlockTrace {
3660                        trace,
3661                        block_hash,
3662                        block_number: ts.epoch(),
3663                        transaction_hash: entry.tx_hash,
3664                        transaction_position: entry.msg_position,
3665                    });
3666                }
3667            }
3668            anyhow::Ok(Arc::new(all_traces))
3669        })
3670        .await?;
3671    Ok(Arc::unwrap_or_clone(traces))
3672}
3673
3674pub enum EthDebugTraceTransaction {}
3675impl RpcMethod<2> for EthDebugTraceTransaction {
3676    const N_REQUIRED_PARAMS: usize = 1;
3677    const NAME: &'static str = "Forest.EthDebugTraceTransaction";
3678    const NAME_ALIAS: Option<&'static str> = Some("debug_traceTransaction");
3679    const PARAM_NAMES: [&'static str; 2] = ["txHash", "opts"];
3680    const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::{ V1 | V2 });
3681    const PERMISSION: Permission = Permission::Read;
3682    const DESCRIPTION: &'static str =
3683        "Replays a transaction and returns execution traces in Geth-compatible format.";
3684
3685    type Params = (String, Option<GethDebugTracingOptions>);
3686    type Ok = GethTrace;
3687
3688    async fn handle(
3689        ctx: Ctx,
3690        (tx_hash, opts): Self::Params,
3691        ext: &http::Extensions,
3692    ) -> Result<Self::Ok, ServerError> {
3693        let opts = opts.unwrap_or_default();
3694        let cancellation_token = CancellationToken::new();
3695        let _drop_guard = cancellation_token.drop_guard_ref();
3696        debug_trace_transaction(
3697            ctx,
3698            Self::api_path(ext)?,
3699            tx_hash,
3700            opts,
3701            &cancellation_token,
3702        )
3703        .await
3704    }
3705}
3706
3707async fn debug_trace_transaction(
3708    ctx: Ctx,
3709    api_path: ApiPaths,
3710    tx_hash: String,
3711    opts: GethDebugTracingOptions,
3712    cancellation_token: &CancellationToken,
3713) -> Result<GethTrace, ServerError> {
3714    let tracer = match &opts.tracer {
3715        Some(t) => t.clone(),
3716        None => {
3717            tracing::debug!(
3718                "no tracer specified for debug_traceTransaction; defaulting to callTracer (struct logger not supported)"
3719            );
3720            GethDebugBuiltInTracerType::Call
3721        }
3722    };
3723
3724    let eth_hash = EthHash::from_str(&tx_hash).context("invalid transaction hash")?;
3725    let eth_txn = get_eth_transaction_by_hash(&ctx, &eth_hash, None, cancellation_token)
3726        .await?
3727        .ok_or(ServerError::internal_error("transaction not found", None))?;
3728
3729    // Mempool/pending transactions cannot be traced — they have no containing tipset.
3730    if eth_txn.block_hash == EthHash::default() {
3731        return Err(ServerError::invalid_params(
3732            "no trace for pending transactions",
3733            None,
3734        ));
3735    }
3736
3737    if tracer == GethDebugBuiltInTracerType::Noop {
3738        return Ok(GethTrace::Noop(NoopFrame {}));
3739    }
3740
3741    let resolver = TipsetResolver::new(&ctx, api_path);
3742    let ts = resolver
3743        .tipset_by_block_number_or_hash(eth_txn.block_number, ResolveNullTipset::TakeOlder)
3744        .await?;
3745
3746    // prestateTracer uses per-message replay for exact state boundaries,
3747    // so it does not need the full tipset trace.
3748    if tracer == GethDebugBuiltInTracerType::PreState {
3749        let prestate_config = opts.prestate_config()?;
3750
3751        let message_cid = ctx
3752            .chain_store()
3753            .get_mapping(&eth_hash)?
3754            .unwrap_or_else(|| eth_hash.to_cid());
3755
3756        let (pre_root, invoc_result, post_root) = ctx
3757            .state_manager
3758            .replay_for_prestate(ts.shallow_clone(), message_cid)
3759            .await
3760            .map_err(|e| anyhow::anyhow!("replay for prestate failed: {e}"))?;
3761
3762        let execution_trace = invoc_result
3763            .execution_trace
3764            .context("no execution trace for transaction")?;
3765
3766        let mut touched = extract_touched_eth_addresses(&execution_trace);
3767        if let Ok(addr) = EthAddress::from_filecoin_address(&invoc_result.msg.from()) {
3768            touched.insert(addr);
3769        }
3770
3771        if let Ok(addr) = EthAddress::from_filecoin_address(&invoc_result.msg.to()) {
3772            touched.insert(addr);
3773        }
3774
3775        let pre_state = StateTree::new_from_root(ctx.db(), &pre_root)?;
3776        let post_state = StateTree::new_from_root(ctx.db(), &post_root)?;
3777
3778        let frame = trace::build_prestate_frame(
3779            ctx.db(),
3780            &pre_state,
3781            &post_state,
3782            &touched,
3783            &prestate_config,
3784        )?;
3785
3786        return Ok(GethTrace::PreState(frame));
3787    }
3788
3789    let (state, entries) = execute_tipset_traces(&ctx.state_manager, &ts).await?;
3790    let entry = entries
3791        .into_iter()
3792        .find(|e| e.tx_hash == eth_hash)
3793        .ok_or_else(|| ServerError::internal_error("transaction trace not found in block", None))?;
3794
3795    let execution_trace = entry
3796        .invoc_result
3797        .execution_trace
3798        .clone()
3799        .context("no execution trace for transaction")?;
3800
3801    let mut env = trace::base_environment(&state, &entry.invoc_result.msg.from).map_err(|e| {
3802        anyhow::anyhow!(
3803            "when processing message {}: {e}",
3804            entry.invoc_result.msg_cid
3805        )
3806    })?;
3807
3808    match tracer {
3809        GethDebugBuiltInTracerType::Call => {
3810            let call_config = opts.call_config()?;
3811            let frame = trace::build_geth_call_frame(&mut env, execution_trace, &call_config)?;
3812            Ok(GethTrace::Call(frame.unwrap_or_default()))
3813        }
3814        GethDebugBuiltInTracerType::FlatCall => {
3815            trace::build_traces(&mut env, &[], execution_trace)?;
3816            let block_hash: EthHash = ts.key().cid()?.into();
3817            let traces = env
3818                .traces
3819                .into_iter()
3820                .map(|t| EthBlockTrace {
3821                    trace: t,
3822                    block_hash,
3823                    block_number: ts.epoch(),
3824                    transaction_hash: eth_hash,
3825                    transaction_position: entry.msg_position,
3826                })
3827                .collect();
3828            Ok(GethTrace::FlatCall(traces))
3829        }
3830        _ => Err(anyhow::anyhow!(
3831            "unexpected tracer type: noopTracer and prestateTracer should be handled above"
3832        )
3833        .into()),
3834    }
3835}
3836
3837pub enum EthTraceCall {}
3838impl RpcMethod<3> for EthTraceCall {
3839    const NAME: &'static str = "Forest.EthTraceCall";
3840    const NAME_ALIAS: Option<&'static str> = Some("trace_call");
3841    const N_REQUIRED_PARAMS: usize = 1;
3842    const PARAM_NAMES: [&'static str; 3] = ["tx", "traceTypes", "blockParam"];
3843    const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::{ V1 | V2 });
3844    const PERMISSION: Permission = Permission::Read;
3845    const DESCRIPTION: &'static str =
3846        "Returns parity style trace results for the given transaction.";
3847
3848    type Params = (
3849        EthCallMessage,
3850        NonEmpty<EthTraceType>,
3851        Option<BlockNumberOrHash>,
3852    );
3853    type Ok = EthTraceResults;
3854    async fn handle(
3855        ctx: Ctx,
3856        (tx, trace_types, block_param): Self::Params,
3857        ext: &http::Extensions,
3858    ) -> Result<Self::Ok, ServerError> {
3859        let msg = Message::try_from(tx)?;
3860        let block_param = block_param.unwrap_or_else(|| Predefined::Latest.into());
3861        let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
3862        let ts = resolver
3863            .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
3864            .await?;
3865
3866        let TipsetState {
3867            state_root: pre_state_root,
3868            ..
3869        } = ctx
3870            .state_manager
3871            .load_tipset_state(&ts)
3872            .await
3873            .context("failed to get tipset state")?;
3874        let pre_state = StateTree::new_from_root(ctx.db(), &pre_state_root)?;
3875
3876        let (invoke_result, post_state_root) = ctx
3877            .state_manager
3878            .apply_on_state_with_gas(Some(ts.shallow_clone()), msg.clone(), VMFlush::Flush)
3879            .await
3880            .context("failed to apply message")?;
3881        let post_state_root =
3882            post_state_root.context("post-execution state root required for trace call")?;
3883        let post_state = StateTree::new_from_root(ctx.db(), &post_state_root)?;
3884
3885        let mut trace_results = EthTraceResults {
3886            output: get_trace_output(&msg, &invoke_result)?,
3887            ..Default::default()
3888        };
3889
3890        // Extract touched addresses for state diff (do this before consuming exec_trace)
3891        let touched_addresses = invoke_result
3892            .execution_trace
3893            .as_ref()
3894            .map(extract_touched_eth_addresses)
3895            .unwrap_or_default();
3896
3897        // Build call traces if requested
3898        if trace_types.contains(&EthTraceType::Trace)
3899            && let Some(exec_trace) = invoke_result.execution_trace
3900        {
3901            let mut env = trace::base_environment(&post_state, &msg.from())
3902                .context("failed to create trace environment")?;
3903            trace::build_traces(&mut env, &[], exec_trace)?;
3904            trace_results.trace = env.traces;
3905        }
3906
3907        // Build state diff if requested
3908        if trace_types.contains(&EthTraceType::StateDiff) {
3909            // Add the caller address to touched addresses
3910            let mut all_touched = touched_addresses;
3911            if let Ok(caller_eth) = EthAddress::from_filecoin_address(&msg.from()) {
3912                all_touched.insert(caller_eth);
3913            }
3914            if let Ok(to_eth) = EthAddress::from_filecoin_address(&msg.to()) {
3915                all_touched.insert(to_eth);
3916            }
3917
3918            let state_diff =
3919                trace::build_state_diff(ctx.db(), &pre_state, &post_state, &all_touched)?;
3920            trace_results.state_diff = Some(state_diff);
3921        }
3922
3923        Ok(trace_results)
3924    }
3925}
3926
3927/// Get output bytes from trace execution result.
3928fn get_trace_output(msg: &Message, invoke_result: &ApiInvocResult) -> Result<EthBytes> {
3929    if msg.to() == FilecoinAddress::ETHEREUM_ACCOUNT_MANAGER_ACTOR {
3930        return Ok(EthBytes::default());
3931    }
3932
3933    let msg_rct = invoke_result
3934        .msg_rct
3935        .as_ref()
3936        .context("missing message receipt")?;
3937    let return_data = msg_rct.return_data();
3938
3939    if return_data.is_empty() {
3940        return Ok(EthBytes::default());
3941    }
3942
3943    decode_payload(&return_data, CBOR).context("failed to decode return data")
3944}
3945
3946/// Extract all unique Ethereum addresses touched during execution from the trace.
3947fn extract_touched_eth_addresses(trace: &crate::rpc::state::ExecutionTrace) -> HashSet<EthAddress> {
3948    let mut addresses = HashSet::default();
3949    let mut stack = vec![trace];
3950
3951    while let Some(current) = stack.pop() {
3952        if let Ok(eth_addr) = EthAddress::from_filecoin_address(&current.msg.from) {
3953            addresses.insert(eth_addr);
3954        }
3955        if let Ok(eth_addr) = EthAddress::from_filecoin_address(&current.msg.to) {
3956            addresses.insert(eth_addr);
3957        }
3958        stack.extend(&current.subcalls);
3959    }
3960
3961    addresses
3962}
3963
3964pub enum EthTraceTransaction {}
3965impl RpcMethod<1> for EthTraceTransaction {
3966    const NAME: &'static str = "Filecoin.EthTraceTransaction";
3967    const NAME_ALIAS: Option<&'static str> = Some("trace_transaction");
3968    const N_REQUIRED_PARAMS: usize = 1;
3969    const PARAM_NAMES: [&'static str; 1] = ["txHash"];
3970    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
3971    const PERMISSION: Permission = Permission::Read;
3972    const DESCRIPTION: &'static str = "Returns the traces for a specific transaction.";
3973
3974    type Params = (String,);
3975    type Ok = NotNullVec<EthBlockTrace>;
3976    async fn handle(
3977        ctx: Ctx,
3978        (tx_hash,): Self::Params,
3979        ext: &http::Extensions,
3980    ) -> Result<Self::Ok, ServerError> {
3981        let cancellation_token = CancellationToken::new();
3982        let _drop_guard = cancellation_token.drop_guard_ref();
3983        let eth_hash = EthHash::from_str(&tx_hash).context("invalid transaction hash")?;
3984        let eth_txn = get_eth_transaction_by_hash(&ctx, &eth_hash, None, &cancellation_token)
3985            .await?
3986            .ok_or(ServerError::internal_error("transaction not found", None))?;
3987
3988        let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
3989        let ts = resolver
3990            .tipset_by_block_number_or_hash(eth_txn.block_number, ResolveNullTipset::TakeOlder)
3991            .await?;
3992
3993        let traces = eth_trace_block(&ctx.state_manager, &ts)
3994            .await?
3995            .into_iter()
3996            .filter(|trace| trace.transaction_hash == eth_hash)
3997            .collect();
3998        Ok(NotNullVec(traces))
3999    }
4000}
4001
4002pub enum EthTraceReplayBlockTransactions {}
4003impl RpcMethod<2> for EthTraceReplayBlockTransactions {
4004    const N_REQUIRED_PARAMS: usize = 2;
4005    const NAME: &'static str = "Filecoin.EthTraceReplayBlockTransactions";
4006    const NAME_ALIAS: Option<&'static str> = Some("trace_replayBlockTransactions");
4007    const PARAM_NAMES: [&'static str; 2] = ["blockParam", "traceTypes"];
4008    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
4009    const PERMISSION: Permission = Permission::Read;
4010    const DESCRIPTION: &'static str =
4011        "Replays all transactions in a block returning the requested traces for each transaction.";
4012
4013    type Params = (BlockNumberOrHash, Vec<String>);
4014    type Ok = NotNullVec<EthReplayBlockTransactionTrace>;
4015
4016    async fn handle(
4017        ctx: Ctx,
4018        (block_param, trace_types): Self::Params,
4019        ext: &http::Extensions,
4020    ) -> Result<Self::Ok, ServerError> {
4021        if trace_types.as_slice() != ["trace"] {
4022            return Err(ServerError::invalid_params(
4023                "only 'trace' is supported",
4024                None,
4025            ));
4026        }
4027
4028        let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
4029        let ts = resolver
4030            .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::Fail)
4031            .await?;
4032
4033        eth_trace_replay_block_transactions(&ctx, &ts)
4034            .await
4035            .map(NotNullVec)
4036    }
4037}
4038
4039async fn eth_trace_replay_block_transactions(
4040    ctx: &Ctx,
4041    ts: &Tipset,
4042) -> Result<Vec<EthReplayBlockTransactionTrace>, ServerError> {
4043    let (state, entries) = execute_tipset_traces(&ctx.state_manager, ts).await?;
4044
4045    let mut all_traces = vec![];
4046    for entry in entries {
4047        let traces = entry.build_parity_traces(&state)?;
4048        all_traces.push(EthReplayBlockTransactionTrace {
4049            full_trace: EthTraceResults::from_parity_traces(traces),
4050            transaction_hash: entry.tx_hash,
4051            vm_trace: None,
4052        });
4053    }
4054
4055    Ok(all_traces)
4056}
4057
4058async fn get_eth_block_number_from_string(
4059    ctx: &Ctx,
4060    block: Option<&str>,
4061    resolve: ResolveNullTipset,
4062    api_path: ApiPaths,
4063) -> Result<EthUint64> {
4064    let block_param = block
4065        .map(BlockNumberOrHash::from_str)
4066        .transpose()?
4067        .unwrap_or(BlockNumberOrHash::PredefinedBlock(Predefined::Latest));
4068    let resolver = TipsetResolver::new(ctx, api_path);
4069    Ok(EthUint64(
4070        resolver
4071            .tipset_by_block_number_or_hash(block_param, resolve)
4072            .await?
4073            .epoch() as u64,
4074    ))
4075}
4076
4077pub enum EthTraceFilter {}
4078impl RpcMethod<1> for EthTraceFilter {
4079    const N_REQUIRED_PARAMS: usize = 1;
4080    const NAME: &'static str = "Filecoin.EthTraceFilter";
4081    const NAME_ALIAS: Option<&'static str> = Some("trace_filter");
4082    const PARAM_NAMES: [&'static str; 1] = ["filter"];
4083    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
4084    const PERMISSION: Permission = Permission::Read;
4085    const DESCRIPTION: &'static str =
4086        "Returns the traces for transactions matching the filter criteria.";
4087    type Params = (EthTraceFilterCriteria,);
4088    type Ok = NotNullVec<EthBlockTrace>;
4089
4090    async fn handle(
4091        ctx: Ctx,
4092        (filter,): Self::Params,
4093        ext: &http::Extensions,
4094    ) -> Result<Self::Ok, ServerError> {
4095        let api_path = Self::api_path(ext)?;
4096        let from_block = get_eth_block_number_from_string(
4097            &ctx,
4098            filter.from_block.as_deref(),
4099            ResolveNullTipset::TakeNewer,
4100            api_path,
4101        )
4102        .await
4103        .context("cannot parse fromBlock")?;
4104
4105        let to_block = get_eth_block_number_from_string(
4106            &ctx,
4107            filter.to_block.as_deref(),
4108            ResolveNullTipset::TakeOlder,
4109            api_path,
4110        )
4111        .await
4112        .context("cannot parse toBlock")?;
4113
4114        let max_block_range = ctx.eth_event_handler.max_filter_height_range;
4115        if max_block_range > 0 && to_block.0 > from_block.0 {
4116            let range = i64::try_from(to_block.0.saturating_sub(from_block.0))
4117                .context("block range overflow")?;
4118            if range > max_block_range {
4119                return Err(EthErrors::limit_exceeded(max_block_range, range).into());
4120            }
4121        }
4122        Ok(NotNullVec(
4123            trace_filter(ctx, filter, from_block, to_block, ext).await?,
4124        ))
4125    }
4126}
4127
4128async fn trace_filter(
4129    ctx: Ctx,
4130    filter: EthTraceFilterCriteria,
4131    from_block: EthUint64,
4132    to_block: EthUint64,
4133    ext: &http::Extensions,
4134) -> Result<Vec<EthBlockTrace>> {
4135    let mut results = HashSet::default();
4136    if let Some(EthUint64(0)) = filter.count {
4137        return Ok(Vec::new());
4138    }
4139    let count = *filter.count.unwrap_or_default();
4140    ensure!(
4141        count <= *FOREST_TRACE_FILTER_MAX_RESULT,
4142        "invalid response count, requested {}, maximum supported is {}",
4143        count,
4144        *FOREST_TRACE_FILTER_MAX_RESULT
4145    );
4146
4147    let mut trace_counter = 0;
4148    'blocks: for blk_num in from_block.0..=to_block.0 {
4149        // For BlockNumber, EthTraceBlock and EthTraceBlockV2 are equivalent.
4150        let block_traces = match EthTraceBlock::handle(
4151            ctx.clone(),
4152            (BlockNumberOrHash::from_block_number(blk_num as i64),),
4153            ext,
4154        )
4155        .await
4156        {
4157            Ok(block_traces) => block_traces,
4158            Err(e) if e.code() == NULL_ROUND_CODE => continue 'blocks,
4159            Err(e) => return Err(e.into()),
4160        };
4161        for block_trace in block_traces.0 {
4162            if block_trace
4163                .trace
4164                .match_filter_criteria(filter.from_address.as_ref(), filter.to_address.as_ref())?
4165            {
4166                trace_counter += 1;
4167                if let Some(after) = filter.after
4168                    && trace_counter <= after.0
4169                {
4170                    continue;
4171                }
4172
4173                results.insert(block_trace);
4174
4175                if filter.count.is_some() && results.len() >= count as usize {
4176                    break 'blocks;
4177                } else if results.len() > *FOREST_TRACE_FILTER_MAX_RESULT as usize {
4178                    bail!(
4179                        "too many results, maximum supported is {}, try paginating requests with After and Count",
4180                        *FOREST_TRACE_FILTER_MAX_RESULT
4181                    );
4182                }
4183            }
4184        }
4185    }
4186
4187    Ok(results
4188        .into_iter()
4189        .sorted_by(|a, b| a.sort_key().cmp(&b.sort_key()))
4190        .collect_vec())
4191}
4192
4193#[cfg(test)]
4194mod test {
4195    use super::*;
4196    use crate::rpc::eth::EventEntry;
4197    use crate::rpc::state::{ExecutionTrace, MessageTrace, ReturnTrace};
4198    use crate::shim::{econ::TokenAmount, error::ExitCode};
4199    use crate::{
4200        db::MemoryDB,
4201        test_utils::{construct_bls_messages, construct_eth_messages, construct_messages},
4202    };
4203    use fvm_shared4::event::Flags;
4204    use quickcheck::Arbitrary;
4205    use quickcheck_macros::quickcheck;
4206    use rstest::rstest;
4207
4208    impl Arbitrary for EthHash {
4209        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
4210            let arr: [u8; 32] = std::array::from_fn(|_ix| u8::arbitrary(g));
4211            Self(ethereum_types::H256(arr))
4212        }
4213    }
4214
4215    #[rstest]
4216    // Non-empty access list → JSON array.
4217    #[case::populated_array(ApiEthTx { access_list: Some(NotNullVec(vec![EthHash::default()])), ..Default::default() }, Some(1))]
4218    // `access_list: None` → field omitted.
4219    #[case::explicit_none_omitted(ApiEthTx { access_list: None, ..Default::default() }, None)]
4220    // Legacy tx → field omitted.
4221    #[case::legacy_homestead_omitted(EthLegacyHomesteadTxArgs::default().into(), None)]
4222    // Typed tx with no entries → `[]`.
4223    #[case::eip1559_empty_array(EthEip1559TxArgs::default().into(), Some(0))]
4224    fn access_list_serialization(#[case] tx: ApiEthTx, #[case] expected: Option<usize>) {
4225        let json = serde_json::to_value(tx.into_lotus_json()).unwrap();
4226        match expected {
4227            Some(len) => assert_eq!(
4228                json["accessList"]
4229                    .as_array()
4230                    .expect("accessList should serialize as an array")
4231                    .len(),
4232                len
4233            ),
4234            None => assert!(!json.as_object().unwrap().contains_key("accessList")),
4235        }
4236    }
4237
4238    #[rstest]
4239    // Contract creation → `"to": null` present, not omitted.
4240    #[case::contract_creation(None)]
4241    // Normal tx → `"to"` is the recipient address.
4242    #[case::normal(Some(EthAddress::default()))]
4243    fn to_is_always_serialized(#[case] to: Option<EthAddress>) {
4244        let json = serde_json::to_value(
4245            ApiEthTx {
4246                to,
4247                ..Default::default()
4248            }
4249            .into_lotus_json(),
4250        )
4251        .unwrap();
4252        assert!(
4253            json.as_object().unwrap().contains_key("to"),
4254            "`to` key must always be present"
4255        );
4256        assert_eq!(json["to"], serde_json::to_value(to).unwrap());
4257    }
4258
4259    #[rstest]
4260    // `"accessList": null` → `None`.
4261    #[case::null_to_none(Some(serde_json::Value::Null), None)]
4262    // Omitted/Missing field → `None`.
4263    #[case::missing_to_none(None, None)]
4264    // `"accessList": []` → `None`.
4265    #[case::empty_array_to_none(Some(serde_json::json!([])), None)]
4266    // Non-empty array → `Some(...)`.
4267    #[case::populated_array_to_some(
4268        Some(serde_json::json!([EthHash::default()])),
4269        Some(NotNullVec(vec![EthHash::default()]))
4270    )]
4271    fn access_list_deserialization(
4272        #[case] access_list_value: Option<serde_json::Value>,
4273        #[case] expected: Option<NotNullVec<EthHash>>,
4274    ) {
4275        let mut json = serde_json::to_value(ApiEthTx::default().into_lotus_json()).unwrap();
4276        let obj = json.as_object_mut().unwrap();
4277        match access_list_value {
4278            Some(value) => {
4279                obj.insert("accessList".into(), value);
4280            }
4281            None => {
4282                obj.remove("accessList");
4283            }
4284        }
4285        let tx = ApiEthTx::from_lotus_json(serde_json::from_value(json).unwrap());
4286        assert_eq!(tx.access_list, expected);
4287    }
4288
4289    #[quickcheck]
4290    fn gas_price_result_serde_roundtrip(i: u128) {
4291        let r = EthBigInt(ethereum_types::U256::from(i));
4292        let encoded = serde_json::to_string(&r).unwrap();
4293        assert_eq!(encoded, format!("\"{i:#x}\""));
4294        let decoded: EthBigInt = serde_json::from_str(&encoded).unwrap();
4295        assert_eq!(r, decoded);
4296    }
4297
4298    /// `transactionPosition` must be 0-indexed and system-actor messages must
4299    /// be filtered without consuming a position.
4300    #[test]
4301    fn non_system_traces_with_positions_is_zero_indexed() {
4302        use crate::shim::address::Address as ShimAddress;
4303        use crate::shim::message::Message_v3;
4304
4305        let invoc_with_from = |from: ShimAddress| -> Arc<ApiInvocResult> {
4306            ApiInvocResult {
4307                msg: Message_v3 {
4308                    to: ShimAddress::new_id(1).into(),
4309                    from: from.into(),
4310                    ..Message_v3::default()
4311                }
4312                .into(),
4313                ..Default::default()
4314            }
4315            .into()
4316        };
4317
4318        let raw_traces = vec![
4319            invoc_with_from(system::ADDRESS.into()),
4320            invoc_with_from(ShimAddress::new_id(1000)),
4321            invoc_with_from(system::ADDRESS.into()),
4322            invoc_with_from(ShimAddress::new_id(1001)),
4323            invoc_with_from(ShimAddress::new_id(1002)),
4324        ];
4325
4326        let positions: Vec<i64> = non_system_traces_with_positions(raw_traces)
4327            .map(|(pos, _)| pos)
4328            .collect();
4329
4330        assert_eq!(positions, vec![0, 1, 2]);
4331    }
4332
4333    #[test]
4334    fn test_abi_encoding() {
4335        const EXPECTED: &str = "000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001b1111111111111111111020200301000000044444444444444444010000000000";
4336        const DATA: &str = "111111111111111111102020030100000004444444444444444401";
4337        let expected_bytes = hex::decode(EXPECTED).unwrap();
4338        let data_bytes = hex::decode(DATA).unwrap();
4339
4340        assert_eq!(expected_bytes, encode_as_abi_helper(22, 81, &data_bytes));
4341    }
4342
4343    #[test]
4344    fn test_abi_encoding_empty_bytes() {
4345        // Generated using https://abi.hashex.org/
4346        const EXPECTED: &str = "0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000";
4347        let expected_bytes = hex::decode(EXPECTED).unwrap();
4348        let data_bytes = vec![];
4349
4350        assert_eq!(expected_bytes, encode_as_abi_helper(22, 81, &data_bytes));
4351    }
4352
4353    #[test]
4354    fn test_abi_encoding_one_byte() {
4355        // According to https://docs.soliditylang.org/en/latest/abi-spec.html and handcrafted
4356        // Uint64, Uint64, Bytes[]
4357        // 22, 81, [253]
4358        const EXPECTED: &str = "0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001fd00000000000000000000000000000000000000000000000000000000000000";
4359        let expected_bytes = hex::decode(EXPECTED).unwrap();
4360        let data_bytes = vec![253];
4361
4362        assert_eq!(expected_bytes, encode_as_abi_helper(22, 81, &data_bytes));
4363    }
4364
4365    #[test]
4366    fn test_id_address_roundtrip() {
4367        let test_cases = [1u64, 2, 3, 100, 101];
4368
4369        for id in test_cases {
4370            let addr = FilecoinAddress::new_id(id);
4371
4372            // roundtrip
4373            let eth_addr = EthAddress::from_filecoin_address(&addr).unwrap();
4374            let fil_addr = eth_addr.to_filecoin_address().unwrap();
4375            assert_eq!(addr, fil_addr)
4376        }
4377    }
4378
4379    #[test]
4380    fn test_addr_serde_roundtrip() {
4381        let test_cases = [
4382            r#""0xd4c5fb16488Aa48081296299d54b0c648C9333dA""#,
4383            r#""0x2C2EC67e3e1FeA8e4A39601cB3A3Cd44f5fa830d""#,
4384            r#""0x01184F793982104363F9a8a5845743f452dE0586""#,
4385        ];
4386
4387        for addr in test_cases {
4388            let eth_addr: EthAddress = serde_json::from_str(addr).unwrap();
4389
4390            let encoded = serde_json::to_string(&eth_addr).unwrap();
4391            assert_eq!(encoded, addr.to_lowercase());
4392
4393            let decoded: EthAddress = serde_json::from_str(&encoded).unwrap();
4394            assert_eq!(eth_addr, decoded);
4395        }
4396    }
4397
4398    #[quickcheck]
4399    fn test_fil_address_roundtrip(addr: FilecoinAddress) {
4400        if let Ok(eth_addr) = EthAddress::from_filecoin_address(&addr) {
4401            let fil_addr = eth_addr.to_filecoin_address().unwrap();
4402
4403            let protocol = addr.protocol();
4404            assert!(protocol == Protocol::ID || protocol == Protocol::Delegated);
4405            assert_eq!(addr, fil_addr);
4406        }
4407    }
4408
4409    #[rstest]
4410    #[case("\"0x013dbb9442ca9667baccc6230fcd5c1c4b2d4d2870f4bd20681d4d47cfd15184\"")]
4411    #[case("\"0xab8653edf9f51785664a643b47605a7ba3d917b5339a0724e7642c114d0e4738\"")]
4412    fn test_hash_serde_json(#[case] hash: &str) {
4413        let h: EthHash = serde_json::from_str(hash).unwrap();
4414        let c = h.to_cid();
4415        let h1: EthHash = c.into();
4416        assert_eq!(h, h1);
4417    }
4418
4419    #[quickcheck]
4420    fn test_eth_hash_roundtrip(eth_hash: EthHash) {
4421        let cid = eth_hash.to_cid();
4422        let hash = cid.into();
4423        assert_eq!(eth_hash, hash);
4424    }
4425
4426    #[test]
4427    fn test_block_constructor() {
4428        let block = Block::new(false, 1);
4429        assert_eq!(block.transactions_root, EthHash::empty_root());
4430
4431        let block = Block::new(true, 1);
4432        assert_eq!(block.transactions_root, EthHash::default());
4433    }
4434
4435    #[test]
4436    fn test_eth_tx_hash_from_signed_message() {
4437        let (_, signed) = construct_eth_messages(0);
4438        let tx_hash =
4439            eth_tx_hash_from_signed_message(&signed, crate::networks::calibnet::ETH_CHAIN_ID)
4440                .unwrap();
4441        assert_eq!(
4442            &format!("{tx_hash}"),
4443            "0xfc81dd8d9ffb045e7e2d494f925824098183263c7f402d69e18cc25e3422791b"
4444        );
4445
4446        let (_, signed) = construct_messages();
4447        let tx_hash =
4448            eth_tx_hash_from_signed_message(&signed, crate::networks::calibnet::ETH_CHAIN_ID)
4449                .unwrap();
4450        assert_eq!(tx_hash.to_cid(), signed.cid());
4451
4452        let (_, signed) = construct_bls_messages();
4453        let tx_hash =
4454            eth_tx_hash_from_signed_message(&signed, crate::networks::calibnet::ETH_CHAIN_ID)
4455                .unwrap();
4456        assert_eq!(tx_hash.to_cid(), signed.message().cid());
4457    }
4458
4459    #[test]
4460    fn test_eth_tx_hash_from_message_cid() {
4461        let blockstore = Arc::new(MemoryDB::default());
4462
4463        let (msg0, secp0) = construct_eth_messages(0);
4464        let (_msg1, secp1) = construct_eth_messages(1);
4465        let (msg2, bls0) = construct_bls_messages();
4466
4467        crate::chain::persist_objects(&blockstore, [msg0.clone(), msg2.clone()].iter()).unwrap();
4468        crate::chain::persist_objects(&blockstore, [secp0.clone(), bls0.clone()].iter()).unwrap();
4469
4470        let tx_hash = eth_tx_hash_from_message_cid(&blockstore, &secp0.cid(), 0).unwrap();
4471        assert!(tx_hash.is_some());
4472
4473        let tx_hash = eth_tx_hash_from_message_cid(&blockstore, &msg2.cid(), 0).unwrap();
4474        assert!(tx_hash.is_some());
4475
4476        let tx_hash = eth_tx_hash_from_message_cid(&blockstore, &secp1.cid(), 0).unwrap();
4477        assert!(tx_hash.is_none());
4478    }
4479
4480    #[test]
4481    fn test_eth_log_from_event() {
4482        // The value member of these event entries correspond to existing topics on Calibnet,
4483        // but they could just as easily be vectors filled with random bytes.
4484
4485        let entries = vec![
4486            EventEntry {
4487                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4488                key: "t1".into(),
4489                codec: IPLD_RAW,
4490                value: vec![
4491                    226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4492                    81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4493                ]
4494                .into(),
4495            },
4496            EventEntry {
4497                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4498                key: "t2".into(),
4499                codec: IPLD_RAW,
4500                value: vec![
4501                    116, 4, 227, 209, 4, 234, 120, 65, 195, 217, 230, 253, 32, 173, 254, 153, 180,
4502                    173, 88, 107, 192, 141, 143, 59, 211, 175, 239, 137, 76, 241, 132, 222,
4503                ]
4504                .into(),
4505            },
4506        ];
4507        let (bytes, hashes) = eth_log_from_event(&entries).unwrap();
4508        assert!(bytes.0.is_empty());
4509        assert_eq!(hashes.len(), 2);
4510
4511        let entries = vec![
4512            EventEntry {
4513                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4514                key: "t1".into(),
4515                codec: IPLD_RAW,
4516                value: vec![
4517                    226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4518                    81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4519                ]
4520                .into(),
4521            },
4522            EventEntry {
4523                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4524                key: "t2".into(),
4525                codec: IPLD_RAW,
4526                value: vec![
4527                    116, 4, 227, 209, 4, 234, 120, 65, 195, 217, 230, 253, 32, 173, 254, 153, 180,
4528                    173, 88, 107, 192, 141, 143, 59, 211, 175, 239, 137, 76, 241, 132, 222,
4529                ]
4530                .into(),
4531            },
4532            EventEntry {
4533                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4534                key: "t3".into(),
4535                codec: IPLD_RAW,
4536                value: vec![
4537                    226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4538                    81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4539                ]
4540                .into(),
4541            },
4542            EventEntry {
4543                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4544                key: "t4".into(),
4545                codec: IPLD_RAW,
4546                value: vec![
4547                    116, 4, 227, 209, 4, 234, 120, 65, 195, 217, 230, 253, 32, 173, 254, 153, 180,
4548                    173, 88, 107, 192, 141, 143, 59, 211, 175, 239, 137, 76, 241, 132, 222,
4549                ]
4550                .into(),
4551            },
4552        ];
4553        let (bytes, hashes) = eth_log_from_event(&entries).unwrap();
4554        assert!(bytes.0.is_empty());
4555        assert_eq!(hashes.len(), 4);
4556
4557        let entries = vec![
4558            EventEntry {
4559                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4560                key: "t1".into(),
4561                codec: IPLD_RAW,
4562                value: vec![
4563                    226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4564                    81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4565                ]
4566                .into(),
4567            },
4568            EventEntry {
4569                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4570                key: "t1".into(),
4571                codec: IPLD_RAW,
4572                value: vec![
4573                    116, 4, 227, 209, 4, 234, 120, 65, 195, 217, 230, 253, 32, 173, 254, 153, 180,
4574                    173, 88, 107, 192, 141, 143, 59, 211, 175, 239, 137, 76, 241, 132, 222,
4575                ]
4576                .into(),
4577            },
4578        ];
4579        assert!(eth_log_from_event(&entries).is_none());
4580
4581        let entries = vec![
4582            EventEntry {
4583                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4584                key: "t3".into(),
4585                codec: IPLD_RAW,
4586                value: vec![
4587                    226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4588                    81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4589                ]
4590                .into(),
4591            },
4592            EventEntry {
4593                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4594                key: "t4".into(),
4595                codec: IPLD_RAW,
4596                value: vec![
4597                    116, 4, 227, 209, 4, 234, 120, 65, 195, 217, 230, 253, 32, 173, 254, 153, 180,
4598                    173, 88, 107, 192, 141, 143, 59, 211, 175, 239, 137, 76, 241, 132, 222,
4599                ]
4600                .into(),
4601            },
4602            EventEntry {
4603                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4604                key: "t1".into(),
4605                codec: IPLD_RAW,
4606                value: vec![
4607                    226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4608                    81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4609                ]
4610                .into(),
4611            },
4612            EventEntry {
4613                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4614                key: "t2".into(),
4615                codec: IPLD_RAW,
4616                value: vec![
4617                    116, 4, 227, 209, 4, 234, 120, 65, 195, 217, 230, 253, 32, 173, 254, 153, 180,
4618                    173, 88, 107, 192, 141, 143, 59, 211, 175, 239, 137, 76, 241, 132, 222,
4619                ]
4620                .into(),
4621            },
4622        ];
4623        let (bytes, hashes) = eth_log_from_event(&entries).unwrap();
4624        assert!(bytes.0.is_empty());
4625        assert_eq!(hashes.len(), 4);
4626
4627        let entries = vec![
4628            EventEntry {
4629                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4630                key: "t1".into(),
4631                codec: IPLD_RAW,
4632                value: vec![
4633                    226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4634                    81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4635                ]
4636                .into(),
4637            },
4638            EventEntry {
4639                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4640                key: "t3".into(),
4641                codec: IPLD_RAW,
4642                value: vec![
4643                    116, 4, 227, 209, 4, 234, 120, 65, 195, 217, 230, 253, 32, 173, 254, 153, 180,
4644                    173, 88, 107, 192, 141, 143, 59, 211, 175, 239, 137, 76, 241, 132, 222,
4645                ]
4646                .into(),
4647            },
4648        ];
4649        assert!(eth_log_from_event(&entries).is_none());
4650
4651        let entries = vec![EventEntry {
4652            flags: (Flags::FLAG_INDEXED_ALL).bits(),
4653            key: "t1".into(),
4654            codec: DAG_CBOR,
4655            value: vec![
4656                226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11, 81,
4657                29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4658            ]
4659            .into(),
4660        }];
4661        assert!(eth_log_from_event(&entries).is_none());
4662
4663        let entries = vec![EventEntry {
4664            flags: (Flags::FLAG_INDEXED_ALL).bits(),
4665            key: "t1".into(),
4666            codec: IPLD_RAW,
4667            value: vec![
4668                226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11, 81,
4669                29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149, 0,
4670            ]
4671            .into(),
4672        }];
4673        assert!(eth_log_from_event(&entries).is_none());
4674
4675        let entries = vec![
4676            EventEntry {
4677                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4678                key: "t1".into(),
4679                codec: IPLD_RAW,
4680                value: vec![
4681                    226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4682                    81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4683                ]
4684                .into(),
4685            },
4686            EventEntry {
4687                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4688                key: "d".into(),
4689                codec: IPLD_RAW,
4690                value: vec![
4691                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 190,
4692                    25, 34, 116, 232, 27, 26, 248,
4693                ]
4694                .into(),
4695            },
4696        ];
4697        let (bytes, hashes) = eth_log_from_event(&entries).unwrap();
4698        assert_eq!(bytes.0.len(), 32);
4699        assert_eq!(hashes.len(), 1);
4700
4701        let entries = vec![
4702            EventEntry {
4703                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4704                key: "t1".into(),
4705                codec: IPLD_RAW,
4706                value: vec![
4707                    226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4708                    81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149, 0,
4709                ]
4710                .into(),
4711            },
4712            EventEntry {
4713                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4714                key: "d".into(),
4715                codec: IPLD_RAW,
4716                value: vec![
4717                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 190,
4718                    25, 34, 116, 232, 27, 26, 248,
4719                ]
4720                .into(),
4721            },
4722            EventEntry {
4723                flags: (Flags::FLAG_INDEXED_ALL).bits(),
4724                key: "d".into(),
4725                codec: IPLD_RAW,
4726                value: vec![
4727                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 190,
4728                    25, 34, 116, 232, 27, 26, 248,
4729                ]
4730                .into(),
4731            },
4732        ];
4733        assert!(eth_log_from_event(&entries).is_none());
4734    }
4735
4736    #[test]
4737    fn test_from_bytes_valid() {
4738        let zero_bytes = [0u8; 32];
4739        assert_eq!(
4740            EthUint64::from_bytes(&zero_bytes).unwrap().0,
4741            0,
4742            "zero bytes"
4743        );
4744
4745        let mut value_bytes = [0u8; 32];
4746        value_bytes[31] = 42;
4747        assert_eq!(
4748            EthUint64::from_bytes(&value_bytes).unwrap().0,
4749            42,
4750            "simple value"
4751        );
4752
4753        let mut max_bytes = [0u8; 32];
4754        max_bytes[24..32].copy_from_slice(&u64::MAX.to_be_bytes());
4755        assert_eq!(
4756            EthUint64::from_bytes(&max_bytes).unwrap().0,
4757            u64::MAX,
4758            "valid max value"
4759        );
4760    }
4761
4762    #[test]
4763    fn test_from_bytes_wrong_length() {
4764        let short_bytes = [0u8; 31];
4765        assert!(
4766            EthUint64::from_bytes(&short_bytes).is_err(),
4767            "bytes too short"
4768        );
4769
4770        let long_bytes = [0u8; 33];
4771        assert!(
4772            EthUint64::from_bytes(&long_bytes).is_err(),
4773            "bytes too long"
4774        );
4775
4776        let empty_bytes = [];
4777        assert!(
4778            EthUint64::from_bytes(&empty_bytes).is_err(),
4779            "bytes too short"
4780        );
4781    }
4782
4783    #[test]
4784    fn test_from_bytes_overflow() {
4785        let mut overflow_bytes = [0u8; 32];
4786        overflow_bytes[10] = 1;
4787        assert!(
4788            EthUint64::from_bytes(&overflow_bytes).is_err(),
4789            "overflow with non-zero byte at position 10"
4790        );
4791
4792        overflow_bytes = [0u8; 32];
4793        overflow_bytes[23] = 1;
4794        assert!(
4795            EthUint64::from_bytes(&overflow_bytes).is_err(),
4796            "overflow with non-zero byte at position 23"
4797        );
4798
4799        overflow_bytes = [0u8; 32];
4800        overflow_bytes
4801            .iter_mut()
4802            .take(24)
4803            .for_each(|byte| *byte = 0xFF);
4804
4805        assert!(
4806            EthUint64::from_bytes(&overflow_bytes).is_err(),
4807            "overflow bytes with non-zero bytes at positions 0-23"
4808        );
4809
4810        overflow_bytes = [0u8; 32];
4811        for i in 0..24 {
4812            overflow_bytes[i] = 0xFF;
4813            assert!(
4814                EthUint64::from_bytes(&overflow_bytes).is_err(),
4815                "overflow with non-zero byte at position {i}"
4816            );
4817        }
4818
4819        overflow_bytes = [0xFF; 32];
4820        assert!(
4821            EthUint64::from_bytes(&overflow_bytes).is_err(),
4822            "overflow with all ones"
4823        );
4824    }
4825
4826    fn create_execution_trace(from: FilecoinAddress, to: FilecoinAddress) -> ExecutionTrace {
4827        ExecutionTrace {
4828            msg: MessageTrace {
4829                from,
4830                to,
4831                value: TokenAmount::default(),
4832                method: 0,
4833                params: Default::default(),
4834                params_codec: 0,
4835                gas_limit: None,
4836                read_only: None,
4837            },
4838            msg_rct: ReturnTrace {
4839                exit_code: ExitCode::from(0u32),
4840                r#return: Default::default(),
4841                return_codec: 0,
4842            },
4843            invoked_actor: None,
4844            gas_charges: vec![],
4845            subcalls: vec![],
4846            logs: vec![],
4847            ipld_ops: vec![],
4848        }
4849    }
4850
4851    fn create_execution_trace_with_subcalls(
4852        from: FilecoinAddress,
4853        to: FilecoinAddress,
4854        subcalls: Vec<ExecutionTrace>,
4855    ) -> ExecutionTrace {
4856        let mut trace = create_execution_trace(from, to);
4857        trace.subcalls = subcalls;
4858        trace
4859    }
4860
4861    #[test]
4862    fn test_extract_touched_addresses_with_id_addresses() {
4863        // ID addresses (e.g., f0100) can be converted to EthAddress
4864        let from = FilecoinAddress::new_id(100);
4865        let to = FilecoinAddress::new_id(200);
4866        let trace = create_execution_trace(from, to);
4867
4868        let addresses = extract_touched_eth_addresses(&trace);
4869
4870        assert_eq!(addresses.len(), 2);
4871        assert!(addresses.contains(&EthAddress::from_filecoin_address(&from).unwrap()));
4872        assert!(addresses.contains(&EthAddress::from_filecoin_address(&to).unwrap()));
4873    }
4874
4875    #[test]
4876    fn test_extract_touched_addresses_same_from_and_to() {
4877        let addr = FilecoinAddress::new_id(100);
4878        let trace = create_execution_trace(addr, addr);
4879
4880        let addresses = extract_touched_eth_addresses(&trace);
4881
4882        // Should deduplicate
4883        assert_eq!(addresses.len(), 1);
4884        assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr).unwrap()));
4885    }
4886
4887    #[test]
4888    fn test_extract_touched_addresses_with_subcalls() {
4889        let addr1 = FilecoinAddress::new_id(100);
4890        let addr2 = FilecoinAddress::new_id(200);
4891        let addr3 = FilecoinAddress::new_id(300);
4892        let addr4 = FilecoinAddress::new_id(400);
4893
4894        let subcall = create_execution_trace(addr3, addr4);
4895        let trace = create_execution_trace_with_subcalls(addr1, addr2, vec![subcall]);
4896
4897        let addresses = extract_touched_eth_addresses(&trace);
4898
4899        assert_eq!(addresses.len(), 4);
4900        assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr1).unwrap()));
4901        assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr2).unwrap()));
4902        assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr3).unwrap()));
4903        assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr4).unwrap()));
4904    }
4905
4906    #[test]
4907    fn test_extract_touched_addresses_with_nested_subcalls() {
4908        let addr1 = FilecoinAddress::new_id(100);
4909        let addr2 = FilecoinAddress::new_id(200);
4910        let addr3 = FilecoinAddress::new_id(300);
4911        let addr4 = FilecoinAddress::new_id(400);
4912        let addr5 = FilecoinAddress::new_id(500);
4913        let addr6 = FilecoinAddress::new_id(600);
4914
4915        // Create nested structure: trace -> subcall1 -> nested_subcall
4916        let nested_subcall = create_execution_trace(addr5, addr6);
4917        let subcall = create_execution_trace_with_subcalls(addr3, addr4, vec![nested_subcall]);
4918        let trace = create_execution_trace_with_subcalls(addr1, addr2, vec![subcall]);
4919
4920        let addresses = extract_touched_eth_addresses(&trace);
4921
4922        assert_eq!(addresses.len(), 6);
4923        for addr in [addr1, addr2, addr3, addr4, addr5, addr6] {
4924            assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr).unwrap()));
4925        }
4926    }
4927
4928    #[test]
4929    fn test_extract_touched_addresses_with_multiple_subcalls() {
4930        let addr1 = FilecoinAddress::new_id(100);
4931        let addr2 = FilecoinAddress::new_id(200);
4932        let addr3 = FilecoinAddress::new_id(300);
4933        let addr4 = FilecoinAddress::new_id(400);
4934        let addr5 = FilecoinAddress::new_id(500);
4935        let addr6 = FilecoinAddress::new_id(600);
4936
4937        let subcall1 = create_execution_trace(addr3, addr4);
4938        let subcall2 = create_execution_trace(addr5, addr6);
4939        let trace = create_execution_trace_with_subcalls(addr1, addr2, vec![subcall1, subcall2]);
4940
4941        let addresses = extract_touched_eth_addresses(&trace);
4942
4943        assert_eq!(addresses.len(), 6);
4944    }
4945
4946    #[test]
4947    fn test_extract_touched_addresses_deduplicates_across_subcalls() {
4948        // Same address appears in parent and subcall
4949        let addr1 = FilecoinAddress::new_id(100);
4950        let addr2 = FilecoinAddress::new_id(200);
4951
4952        let subcall = create_execution_trace(addr1, addr2); // addr1 repeated
4953        let trace = create_execution_trace_with_subcalls(addr1, addr2, vec![subcall]);
4954
4955        let addresses = extract_touched_eth_addresses(&trace);
4956
4957        // Should deduplicate
4958        assert_eq!(addresses.len(), 2);
4959    }
4960
4961    #[test]
4962    fn test_extract_touched_addresses_with_non_convertible_addresses() {
4963        // BLS addresses cannot be converted to EthAddress
4964        let bls_addr = FilecoinAddress::new_bls(&[0u8; 48]).unwrap();
4965        let id_addr = FilecoinAddress::new_id(100);
4966
4967        let trace = create_execution_trace(bls_addr, id_addr);
4968        let addresses = extract_touched_eth_addresses(&trace);
4969
4970        // Only the ID address should be in the set
4971        assert_eq!(addresses.len(), 1);
4972        assert!(addresses.contains(&EthAddress::from_filecoin_address(&id_addr).unwrap()));
4973    }
4974}