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