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