Skip to main content

forest/rpc/methods/
eth.rs

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