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