Skip to main content

forest/rpc/methods/
eth.rs

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