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