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