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