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