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