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