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