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