forest/rpc/methods/
eth.rs

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