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