Skip to main content

forest/rpc/methods/
eth.rs

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