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 .load_required_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 = chain.chain_index().load_required_tipset_by_height(
973 ts.epoch(),
974 chain.heaviest_tipset(),
975 resolve,
976 )?;
977 if walk_ts != ts {
979 bail!("tipset is not canonical");
980 }
981 }
982 Ok(ts)
983}
984
985pub fn is_eth_address(addr: &VmAddress) -> bool {
986 if addr.protocol() != Protocol::Delegated {
987 return false;
988 }
989 let f4_addr: Result<DelegatedAddress, _> = addr.payload().try_into();
990
991 f4_addr.is_ok()
992}
993
994pub fn eth_tx_from_signed_eth_message(
1000 smsg: &SignedMessage,
1001 chain_id: EthChainIdType,
1002) -> Result<(EthAddress, EthTx)> {
1003 let from = smsg.message().from;
1005 if !is_eth_address(&from) {
1006 bail!("sender must be an eth account, was {from}");
1007 }
1008 let from = EthAddress::from_filecoin_address(&from)?;
1011 let tx = EthTx::from_signed_message(chain_id, smsg)?;
1012 Ok((from, tx))
1013}
1014
1015fn encode_filecoin_params_as_abi(
1018 method: MethodNum,
1019 codec: u64,
1020 params: &fvm_ipld_encoding::RawBytes,
1021) -> Result<EthBytes> {
1022 let mut buffer: Vec<u8> = vec![0x86, 0x8e, 0x10, 0xc4];
1023 buffer.append(&mut encode_filecoin_returns_as_abi(method, codec, params));
1024 Ok(EthBytes(buffer))
1025}
1026
1027fn encode_filecoin_returns_as_abi(
1028 exit_code: u64,
1029 codec: u64,
1030 data: &fvm_ipld_encoding::RawBytes,
1031) -> Vec<u8> {
1032 encode_as_abi_helper(exit_code, codec, data)
1033}
1034
1035fn round_up_word(value: usize) -> usize {
1037 value.div_ceil(EVM_WORD_LENGTH) * EVM_WORD_LENGTH
1038}
1039
1040fn encode_as_abi_helper(param1: u64, param2: u64, data: &[u8]) -> Vec<u8> {
1042 let static_args = [
1048 param1,
1049 param2,
1050 (EVM_WORD_LENGTH * 3) as u64,
1051 data.len() as u64,
1052 ];
1053 let padding = [0u8; 24];
1054 let buf: Vec<u8> = padding
1055 .iter() .chain(static_args[0].to_be_bytes().iter()) .chain(padding.iter())
1058 .chain(static_args[1].to_be_bytes().iter())
1059 .chain(padding.iter())
1060 .chain(static_args[2].to_be_bytes().iter())
1061 .chain(padding.iter())
1062 .chain(static_args[3].to_be_bytes().iter())
1063 .chain(data.iter()) .chain(std::iter::repeat_n(
1065 &0u8,
1066 round_up_word(data.len()) - data.len(),
1067 )) .cloned()
1069 .collect();
1070
1071 buf
1072}
1073
1074fn eth_tx_from_native_message<DB: Blockstore>(
1087 msg: &Message,
1088 state: &StateTree<DB>,
1089 chain_id: EthChainIdType,
1090) -> Result<ApiEthTx> {
1091 let from = match lookup_eth_address(&msg.from(), state) {
1093 Ok(Some(from)) => from,
1094 _ => bail!(
1095 "failed to lookup sender address {} when converting a native message to an eth txn",
1096 msg.from()
1097 ),
1098 };
1099 let mut to = match lookup_eth_address(&msg.to(), state) {
1102 Ok(Some(addr)) => Some(addr),
1103 Ok(None) => Some(EthAddress(
1104 ethereum_types::H160::from_str(REVERTED_ETH_ADDRESS).unwrap(),
1105 )),
1106 Err(err) => {
1107 bail!(err)
1108 }
1109 };
1110
1111 let codec = if !msg.params().is_empty() { CBOR } else { 0 };
1116
1117 let input = 'decode: {
1120 if (msg.method_num() == EVMMethod::InvokeContract as MethodNum
1121 || msg.method_num() == EAMMethod::CreateExternal as MethodNum)
1122 && let Ok(buffer) = decode_payload(msg.params(), codec)
1123 {
1124 if msg.method_num() == EAMMethod::CreateExternal as MethodNum {
1126 to = None;
1127 }
1128 break 'decode buffer;
1129 }
1130 encode_filecoin_params_as_abi(msg.method_num(), codec, msg.params())?
1133 };
1134
1135 Ok(ApiEthTx {
1136 to,
1137 from,
1138 input,
1139 nonce: EthUint64(msg.sequence),
1140 chain_id: EthUint64(chain_id),
1141 value: msg.value.clone().into(),
1142 r#type: EthUint64(EIP_1559_TX_TYPE.into()),
1143 gas: EthUint64(msg.gas_limit),
1144 max_fee_per_gas: Some(msg.gas_fee_cap.clone().into()),
1145 max_priority_fee_per_gas: Some(msg.gas_premium.clone().into()),
1146 access_list: vec![],
1147 ..ApiEthTx::default()
1148 })
1149}
1150
1151pub fn new_eth_tx_from_signed_message<DB: Blockstore>(
1152 smsg: &SignedMessage,
1153 state: &StateTree<DB>,
1154 chain_id: EthChainIdType,
1155) -> Result<ApiEthTx> {
1156 let (tx, hash) = if smsg.is_delegated() {
1157 let (from, tx) = eth_tx_from_signed_eth_message(smsg, chain_id)?;
1159 let hash = tx.eth_hash()?.into();
1160 let tx = ApiEthTx { from, ..tx.into() };
1161 (tx, hash)
1162 } else if smsg.is_secp256k1() {
1163 let tx = eth_tx_from_native_message(smsg.message(), state, chain_id)?;
1165 (tx, smsg.cid().into())
1166 } else {
1167 let tx = eth_tx_from_native_message(smsg.message(), state, chain_id)?;
1169 (tx, smsg.message().cid().into())
1170 };
1171 Ok(ApiEthTx { hash, ..tx })
1172}
1173
1174fn new_eth_tx_from_message_lookup<DB: Blockstore>(
1178 ctx: &Ctx<DB>,
1179 message_lookup: &MessageLookup,
1180 tx_index: Option<u64>,
1181) -> Result<ApiEthTx> {
1182 let ts = ctx
1183 .chain_store()
1184 .load_required_tipset_or_heaviest(&message_lookup.tipset)?;
1185
1186 let parent_ts = ctx
1188 .chain_store()
1189 .load_required_tipset_or_heaviest(ts.parents())?;
1190
1191 let parent_ts_cid = parent_ts.key().cid()?;
1192
1193 let tx_index = tx_index.map_or_else(
1195 || {
1196 let msgs = ctx.chain_store().messages_for_tipset(&parent_ts)?;
1197 msgs.iter()
1198 .position(|msg| msg.cid() == message_lookup.message)
1199 .context("cannot find the msg in the tipset")
1200 .map(|i| i as u64)
1201 },
1202 Ok,
1203 )?;
1204
1205 let smsg = get_signed_message(ctx, message_lookup.message)?;
1206
1207 let state = ctx.state_manager.get_state_tree(ts.parent_state())?;
1208
1209 Ok(ApiEthTx {
1210 block_hash: parent_ts_cid.into(),
1211 block_number: parent_ts.epoch().into(),
1212 transaction_index: tx_index.into(),
1213 ..new_eth_tx_from_signed_message(&smsg, &state, ctx.chain_config().eth_chain_id)?
1214 })
1215}
1216
1217fn new_eth_tx<DB: Blockstore>(
1218 ctx: &Ctx<DB>,
1219 state: &StateTree<DB>,
1220 block_height: ChainEpoch,
1221 msg_tipset_cid: &Cid,
1222 msg_cid: &Cid,
1223 tx_index: u64,
1224) -> Result<ApiEthTx> {
1225 let smsg = get_signed_message(ctx, *msg_cid)?;
1226 let tx = new_eth_tx_from_signed_message(&smsg, state, ctx.chain_config().eth_chain_id)?;
1227
1228 Ok(ApiEthTx {
1229 block_hash: (*msg_tipset_cid).into(),
1230 block_number: block_height.into(),
1231 transaction_index: tx_index.into(),
1232 ..tx
1233 })
1234}
1235
1236async fn new_eth_tx_receipt<DB: Blockstore + Send + Sync + 'static>(
1237 ctx: &Ctx<DB>,
1238 tipset: &Tipset,
1239 tx: &ApiEthTx,
1240 msg_receipt: &Receipt,
1241) -> anyhow::Result<EthTxReceipt> {
1242 let mut tx_receipt = EthTxReceipt {
1243 transaction_hash: tx.hash,
1244 from: tx.from,
1245 to: tx.to,
1246 transaction_index: tx.transaction_index,
1247 block_hash: tx.block_hash,
1248 block_number: tx.block_number,
1249 r#type: tx.r#type,
1250 status: (u64::from(msg_receipt.exit_code().is_success())).into(),
1251 gas_used: msg_receipt.gas_used().into(),
1252 ..EthTxReceipt::new()
1253 };
1254
1255 tx_receipt.cumulative_gas_used = EthUint64::default();
1256
1257 let gas_fee_cap = tx.gas_fee_cap()?;
1258 let gas_premium = tx.gas_premium()?;
1259
1260 let gas_outputs = GasOutputs::compute(
1261 msg_receipt.gas_used(),
1262 tx.gas.into(),
1263 &tipset.block_headers().first().parent_base_fee,
1264 &gas_fee_cap.0.into(),
1265 &gas_premium.0.into(),
1266 );
1267 let total_spent: BigInt = gas_outputs.total_spent().into();
1268
1269 let mut effective_gas_price = EthBigInt::default();
1270 if msg_receipt.gas_used() > 0 {
1271 effective_gas_price = (total_spent / msg_receipt.gas_used()).into();
1272 }
1273 tx_receipt.effective_gas_price = effective_gas_price;
1274
1275 if tx_receipt.to.is_none() && msg_receipt.exit_code().is_success() {
1276 let ret: eam::CreateExternalReturn =
1278 from_slice_with_fallback(msg_receipt.return_data().bytes())?;
1279
1280 tx_receipt.contract_address = Some(ret.eth_address.0.into());
1281 }
1282
1283 if msg_receipt.events_root().is_some() {
1284 let logs =
1285 eth_logs_for_block_and_transaction(ctx, tipset, &tx.block_hash, &tx.hash).await?;
1286 if !logs.is_empty() {
1287 tx_receipt.logs = logs;
1288 }
1289 }
1290
1291 let mut bloom = Bloom::default();
1292 for log in tx_receipt.logs.iter() {
1293 for topic in log.topics.iter() {
1294 bloom.accrue(topic.0.as_bytes());
1295 }
1296 bloom.accrue(log.address.0.as_bytes());
1297 }
1298 tx_receipt.logs_bloom = bloom.into();
1299
1300 Ok(tx_receipt)
1301}
1302
1303pub async fn eth_logs_for_block_and_transaction<DB: Blockstore + Send + Sync + 'static>(
1304 ctx: &Ctx<DB>,
1305 ts: &Tipset,
1306 block_hash: &EthHash,
1307 tx_hash: &EthHash,
1308) -> anyhow::Result<Vec<EthLog>> {
1309 let spec = EthFilterSpec {
1310 block_hash: Some(*block_hash),
1311 ..Default::default()
1312 };
1313
1314 eth_logs_with_filter(ctx, ts, Some(spec), Some(tx_hash)).await
1315}
1316
1317pub async fn eth_logs_with_filter<DB: Blockstore + Send + Sync + 'static>(
1318 ctx: &Ctx<DB>,
1319 ts: &Tipset,
1320 spec: Option<EthFilterSpec>,
1321 tx_hash: Option<&EthHash>,
1322) -> anyhow::Result<Vec<EthLog>> {
1323 let mut events = vec![];
1324 EthEventHandler::collect_events(
1325 ctx,
1326 ts,
1327 spec.as_ref(),
1328 SkipEvent::OnUnresolvedAddress,
1329 &mut events,
1330 )
1331 .await?;
1332
1333 let logs = eth_filter_logs_from_events(ctx, &events)?;
1334 Ok(match tx_hash {
1335 Some(hash) => logs
1336 .into_iter()
1337 .filter(|log| &log.transaction_hash == hash)
1338 .collect(),
1339 None => logs, })
1341}
1342
1343fn get_signed_message<DB: Blockstore>(ctx: &Ctx<DB>, message_cid: Cid) -> Result<SignedMessage> {
1344 let result: Result<SignedMessage, crate::chain::Error> =
1345 crate::chain::message_from_cid(ctx.store(), &message_cid);
1346
1347 result.or_else(|_| {
1348 let msg: Message = crate::chain::message_from_cid(ctx.store(), &message_cid)
1350 .with_context(|| format!("failed to find msg {message_cid}"))?;
1351 Ok(SignedMessage::new_unchecked(
1352 msg,
1353 Signature::new_bls(vec![]),
1354 ))
1355 })
1356}
1357
1358pub enum EthGetBlockByHash {}
1359impl RpcMethod<2> for EthGetBlockByHash {
1360 const NAME: &'static str = "Filecoin.EthGetBlockByHash";
1361 const NAME_ALIAS: Option<&'static str> = Some("eth_getBlockByHash");
1362 const PARAM_NAMES: [&'static str; 2] = ["blockHash", "fullTxInfo"];
1363 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1364 const PERMISSION: Permission = Permission::Read;
1365
1366 type Params = (EthHash, bool);
1367 type Ok = Block;
1368
1369 async fn handle(
1370 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
1371 (block_hash, full_tx_info): Self::Params,
1372 ext: &http::Extensions,
1373 ) -> Result<Self::Ok, ServerError> {
1374 let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
1375 let ts = resolver
1376 .tipset_by_block_number_or_hash(block_hash, ResolveNullTipset::TakeOlder)
1377 .await?;
1378 Block::from_filecoin_tipset(ctx, ts, full_tx_info.into())
1379 .await
1380 .map_err(ServerError::from)
1381 }
1382}
1383
1384pub enum EthGetBlockByNumber {}
1385impl RpcMethod<2> for EthGetBlockByNumber {
1386 const NAME: &'static str = "Filecoin.EthGetBlockByNumber";
1387 const NAME_ALIAS: Option<&'static str> = Some("eth_getBlockByNumber");
1388 const PARAM_NAMES: [&'static str; 2] = ["blockParam", "fullTxInfo"];
1389 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1390 const PERMISSION: Permission = Permission::Read;
1391 const DESCRIPTION: Option<&'static str> =
1392 Some("Retrieves a block by its number or a special tag.");
1393
1394 type Params = (BlockNumberOrPredefined, bool);
1395 type Ok = Block;
1396
1397 async fn handle(
1398 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
1399 (block_param, full_tx_info): Self::Params,
1400 ext: &http::Extensions,
1401 ) -> Result<Self::Ok, ServerError> {
1402 let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
1403 let ts = resolver
1404 .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
1405 .await?;
1406 Block::from_filecoin_tipset(ctx, ts, full_tx_info.into())
1407 .await
1408 .map_err(ServerError::from)
1409 }
1410}
1411
1412async fn get_block_receipts<DB: Blockstore + Send + Sync + 'static>(
1413 ctx: &Ctx<DB>,
1414 ts: Tipset,
1415 limit: Option<ChainEpoch>,
1416) -> Result<Vec<EthTxReceipt>> {
1417 if let Some(limit) = limit
1418 && limit > LOOKBACK_NO_LIMIT
1419 && ts.epoch() < ctx.chain_store().heaviest_tipset().epoch() - limit
1420 {
1421 bail!(
1422 "tipset {} is older than the allowed lookback limit",
1423 ts.key().format_lotus()
1424 );
1425 }
1426 let ts_ref = Arc::new(ts);
1427 let ts_key = ts_ref.key();
1428
1429 let ExecutedTipset {
1431 state_root,
1432 executed_messages,
1433 ..
1434 } = ctx.state_manager.load_executed_tipset(&ts_ref).await?;
1435
1436 let state_tree = ctx.state_manager.get_state_tree(&state_root)?;
1438
1439 let mut eth_receipts = Vec::with_capacity(executed_messages.len());
1440 for (
1441 i,
1442 ExecutedMessage {
1443 message, receipt, ..
1444 },
1445 ) in executed_messages.iter().enumerate()
1446 {
1447 let tx = new_eth_tx(
1448 ctx,
1449 &state_tree,
1450 ts_ref.epoch(),
1451 &ts_key.cid()?,
1452 &message.cid(),
1453 i as u64,
1454 )?;
1455
1456 let receipt = new_eth_tx_receipt(ctx, &ts_ref, &tx, receipt).await?;
1457 eth_receipts.push(receipt);
1458 }
1459 Ok(eth_receipts)
1460}
1461
1462pub enum EthGetBlockReceipts {}
1463impl RpcMethod<1> for EthGetBlockReceipts {
1464 const NAME: &'static str = "Filecoin.EthGetBlockReceipts";
1465 const NAME_ALIAS: Option<&'static str> = Some("eth_getBlockReceipts");
1466 const PARAM_NAMES: [&'static str; 1] = ["blockParam"];
1467 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1468 const PERMISSION: Permission = Permission::Read;
1469 const DESCRIPTION: Option<&'static str> = Some(
1470 "Retrieves all transaction receipts for a block by its number, hash or a special tag.",
1471 );
1472
1473 type Params = (BlockNumberOrHash,);
1474 type Ok = Vec<EthTxReceipt>;
1475
1476 async fn handle(
1477 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
1478 (block_param,): Self::Params,
1479 ext: &http::Extensions,
1480 ) -> Result<Self::Ok, ServerError> {
1481 let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
1482 let ts = resolver
1483 .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
1484 .await?;
1485 get_block_receipts(&ctx, ts, None)
1486 .await
1487 .map_err(ServerError::from)
1488 }
1489}
1490
1491pub enum EthGetBlockReceiptsLimited {}
1492impl RpcMethod<2> for EthGetBlockReceiptsLimited {
1493 const NAME: &'static str = "Filecoin.EthGetBlockReceiptsLimited";
1494 const NAME_ALIAS: Option<&'static str> = Some("eth_getBlockReceiptsLimited");
1495 const PARAM_NAMES: [&'static str; 2] = ["blockParam", "limit"];
1496 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1497 const PERMISSION: Permission = Permission::Read;
1498 const DESCRIPTION: Option<&'static str> = Some(
1499 "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.",
1500 );
1501
1502 type Params = (BlockNumberOrHash, ChainEpoch);
1503 type Ok = Vec<EthTxReceipt>;
1504
1505 async fn handle(
1506 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
1507 (block_param, limit): Self::Params,
1508 ext: &http::Extensions,
1509 ) -> Result<Self::Ok, ServerError> {
1510 let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
1511 let ts = resolver
1512 .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
1513 .await?;
1514 get_block_receipts(&ctx, ts, Some(limit))
1515 .await
1516 .map_err(ServerError::from)
1517 }
1518}
1519
1520pub enum EthGetBlockTransactionCountByHash {}
1521impl RpcMethod<1> for EthGetBlockTransactionCountByHash {
1522 const NAME: &'static str = "Filecoin.EthGetBlockTransactionCountByHash";
1523 const NAME_ALIAS: Option<&'static str> = Some("eth_getBlockTransactionCountByHash");
1524 const PARAM_NAMES: [&'static str; 1] = ["blockHash"];
1525 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1526 const PERMISSION: Permission = Permission::Read;
1527
1528 type Params = (EthHash,);
1529 type Ok = EthUint64;
1530
1531 async fn handle(
1532 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
1533 (block_hash,): Self::Params,
1534 _: &http::Extensions,
1535 ) -> Result<Self::Ok, ServerError> {
1536 let ts = get_tipset_from_hash(ctx.chain_store(), &block_hash)?;
1537
1538 let head = ctx.chain_store().heaviest_tipset();
1539 if ts.epoch() > head.epoch() {
1540 return Err(anyhow::anyhow!("requested a future epoch (beyond \"latest\")").into());
1541 }
1542 let count = count_messages_in_tipset(ctx.store(), &ts)?;
1543 Ok(EthUint64(count as _))
1544 }
1545}
1546
1547pub enum EthGetBlockTransactionCountByNumber {}
1548impl RpcMethod<1> for EthGetBlockTransactionCountByNumber {
1549 const NAME: &'static str = "Filecoin.EthGetBlockTransactionCountByNumber";
1550 const NAME_ALIAS: Option<&'static str> = Some("eth_getBlockTransactionCountByNumber");
1551 const PARAM_NAMES: [&'static str; 1] = ["blockNumber"];
1552 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1553 const PERMISSION: Permission = Permission::Read;
1554 const DESCRIPTION: Option<&'static str> = Some(
1555 "Returns the number of transactions in a block identified by its block number or a special tag.",
1556 );
1557
1558 type Params = (BlockNumberOrPredefined,);
1559 type Ok = EthUint64;
1560
1561 async fn handle(
1562 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
1563 (block_number,): Self::Params,
1564 ext: &http::Extensions,
1565 ) -> Result<Self::Ok, ServerError> {
1566 let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
1567 let ts = resolver
1568 .tipset_by_block_number_or_hash(block_number, ResolveNullTipset::TakeOlder)
1569 .await?;
1570 let count = count_messages_in_tipset(ctx.store(), &ts)?;
1571 Ok(EthUint64(count as _))
1572 }
1573}
1574
1575pub enum EthGetMessageCidByTransactionHash {}
1576impl RpcMethod<1> for EthGetMessageCidByTransactionHash {
1577 const NAME: &'static str = "Filecoin.EthGetMessageCidByTransactionHash";
1578 const NAME_ALIAS: Option<&'static str> = Some("eth_getMessageCidByTransactionHash");
1579 const PARAM_NAMES: [&'static str; 1] = ["txHash"];
1580 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1581 const PERMISSION: Permission = Permission::Read;
1582
1583 type Params = (EthHash,);
1584 type Ok = Option<Cid>;
1585
1586 async fn handle(
1587 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
1588 (tx_hash,): Self::Params,
1589 _: &http::Extensions,
1590 ) -> Result<Self::Ok, ServerError> {
1591 let result = ctx.chain_store().get_mapping(&tx_hash);
1592 match result {
1593 Ok(Some(cid)) => return Ok(Some(cid)),
1594 Ok(None) => tracing::debug!("Undefined key {tx_hash}"),
1595 _ => {
1596 result?;
1597 }
1598 }
1599
1600 let cid = tx_hash.to_cid();
1602
1603 let result: Result<Vec<SignedMessage>, crate::chain::Error> =
1604 crate::chain::messages_from_cids(ctx.store(), &[cid]);
1605 if result.is_ok() {
1606 return Ok(Some(cid));
1608 }
1609
1610 let result: Result<Vec<Message>, crate::chain::Error> =
1611 crate::chain::messages_from_cids(ctx.store(), &[cid]);
1612 if result.is_ok() {
1613 return Ok(Some(cid));
1615 }
1616
1617 Ok(None)
1619 }
1620}
1621
1622fn count_messages_in_tipset(store: &impl Blockstore, ts: &Tipset) -> anyhow::Result<usize> {
1623 let mut message_cids = CidHashSet::default();
1624 for block in ts.block_headers() {
1625 let (bls_messages, secp_messages) = crate::chain::store::block_messages(store, block)?;
1626 for m in bls_messages {
1627 message_cids.insert(m.cid());
1628 }
1629 for m in secp_messages {
1630 message_cids.insert(m.cid());
1631 }
1632 }
1633 Ok(message_cids.len())
1634}
1635
1636pub enum EthSyncing {}
1637impl RpcMethod<0> for EthSyncing {
1638 const NAME: &'static str = "Filecoin.EthSyncing";
1639 const NAME_ALIAS: Option<&'static str> = Some("eth_syncing");
1640 const PARAM_NAMES: [&'static str; 0] = [];
1641 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1642 const PERMISSION: Permission = Permission::Read;
1643
1644 type Params = ();
1645 type Ok = EthSyncingResult;
1646
1647 async fn handle(
1648 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
1649 (): Self::Params,
1650 ext: &http::Extensions,
1651 ) -> Result<Self::Ok, ServerError> {
1652 let sync_status: crate::chain_sync::SyncStatusReport =
1653 crate::rpc::sync::SyncStatus::handle(ctx, (), ext).await?;
1654 match sync_status.status {
1655 NodeSyncStatus::Synced => Ok(EthSyncingResult {
1656 done_sync: true,
1657 ..Default::default()
1659 }),
1660 NodeSyncStatus::Syncing => {
1661 let starting_block = match sync_status.get_min_starting_block() {
1662 Some(e) => Ok(e),
1663 None => Err(ServerError::internal_error(
1664 "missing syncing information, try again",
1665 None,
1666 )),
1667 }?;
1668
1669 Ok(EthSyncingResult {
1670 done_sync: sync_status.is_synced(),
1671 starting_block,
1672 current_block: sync_status.current_head_epoch,
1673 highest_block: sync_status.network_head_epoch,
1674 })
1675 }
1676 _ => Err(ServerError::internal_error("node is not syncing", None)),
1677 }
1678 }
1679}
1680
1681pub enum EthEstimateGas {}
1682
1683impl RpcMethod<2> for EthEstimateGas {
1684 const NAME: &'static str = "Filecoin.EthEstimateGas";
1685 const NAME_ALIAS: Option<&'static str> = Some("eth_estimateGas");
1686 const N_REQUIRED_PARAMS: usize = 1;
1687 const PARAM_NAMES: [&'static str; 2] = ["tx", "blockParam"];
1688 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1689 const PERMISSION: Permission = Permission::Read;
1690
1691 type Params = (EthCallMessage, Option<BlockNumberOrHash>);
1692 type Ok = EthUint64;
1693
1694 async fn handle(
1695 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
1696 (tx, block_param): Self::Params,
1697 ext: &http::Extensions,
1698 ) -> Result<Self::Ok, ServerError> {
1699 let tipset = if let Some(block_param) = block_param {
1700 let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
1701 resolver
1702 .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
1703 .await?
1704 } else {
1705 ctx.chain_store().heaviest_tipset()
1706 };
1707 eth_estimate_gas(&ctx, tx, tipset).await
1708 }
1709}
1710
1711async fn eth_estimate_gas<DB>(
1712 ctx: &Ctx<DB>,
1713 tx: EthCallMessage,
1714 tipset: Tipset,
1715) -> Result<EthUint64, ServerError>
1716where
1717 DB: Blockstore + Send + Sync + 'static,
1718{
1719 let mut msg = Message::try_from(tx)?;
1720 msg.gas_limit = 0;
1723
1724 match gas::estimate_message_gas(ctx, msg.clone(), None, tipset.key().clone().into()).await {
1725 Err(mut err) => {
1726 msg.set_gas_limit(BLOCK_GAS_LIMIT);
1733 if let Err(e) = apply_message(ctx, Some(tipset), msg).await {
1734 if e.downcast_ref::<EthErrors>()
1736 .is_some_and(|eth_err| matches!(eth_err, EthErrors::ExecutionReverted { .. }))
1737 {
1738 return Err(e.into());
1739 }
1740
1741 err = e.into();
1742 }
1743
1744 Err(anyhow::anyhow!("failed to estimate gas: {err}").into())
1745 }
1746 Ok(gassed_msg) => {
1747 let expected_gas = eth_gas_search(ctx, gassed_msg, &tipset.key().into()).await?;
1748 Ok(expected_gas.into())
1749 }
1750 }
1751}
1752
1753async fn apply_message<DB>(
1754 ctx: &Ctx<DB>,
1755 tipset: Option<Tipset>,
1756 msg: Message,
1757) -> Result<ApiInvocResult, Error>
1758where
1759 DB: Blockstore + Send + Sync + 'static,
1760{
1761 let (invoc_res, _) = ctx
1762 .state_manager
1763 .apply_on_state_with_gas(tipset, msg, VMFlush::Skip)
1764 .await
1765 .context("failed to apply on state with gas")?;
1766
1767 match &invoc_res.msg_rct {
1769 None => return Err(anyhow::anyhow!("no message receipt in execution result")),
1770 Some(receipt) => {
1771 if !receipt.exit_code().is_success() {
1772 let (data, reason) = decode_revert_reason(receipt.return_data());
1773
1774 return Err(EthErrors::execution_reverted(
1775 ExitCode::from(receipt.exit_code()),
1776 reason.as_str(),
1777 invoc_res.error.as_str(),
1778 data.as_slice(),
1779 )
1780 .into());
1781 }
1782 }
1783 };
1784
1785 Ok(invoc_res)
1786}
1787
1788pub async fn eth_gas_search<DB>(
1789 data: &Ctx<DB>,
1790 msg: Message,
1791 tsk: &ApiTipsetKey,
1792) -> anyhow::Result<u64>
1793where
1794 DB: Blockstore + Send + Sync + 'static,
1795{
1796 let (_invoc_res, apply_ret, prior_messages, ts) =
1797 gas::GasEstimateGasLimit::estimate_call_with_gas(data, msg.clone(), tsk).await?;
1798 if apply_ret.msg_receipt().exit_code().is_success() {
1799 return Ok(msg.gas_limit());
1800 }
1801
1802 let exec_trace = apply_ret.exec_trace();
1803 let _expected_exit_code: ExitCode = fvm_shared4::error::ExitCode::SYS_OUT_OF_GAS.into();
1804 if exec_trace.iter().any(|t| {
1805 matches!(
1806 t,
1807 &ExecutionEvent::CallReturn(CallReturn {
1808 exit_code: Some(_expected_exit_code),
1809 ..
1810 })
1811 )
1812 }) {
1813 let ret = gas_search(data, &msg, &prior_messages, ts).await?;
1814 Ok(((ret as f64) * data.mpool.config.gas_limit_overestimation) as u64)
1815 } else {
1816 anyhow::bail!(
1817 "message execution failed: exit {}, reason: {}",
1818 apply_ret.msg_receipt().exit_code(),
1819 apply_ret.failure_info().unwrap_or_default(),
1820 );
1821 }
1822}
1823
1824async fn gas_search<DB>(
1829 data: &Ctx<DB>,
1830 msg: &Message,
1831 prior_messages: &[ChainMessage],
1832 ts: Tipset,
1833) -> anyhow::Result<u64>
1834where
1835 DB: Blockstore + Send + Sync + 'static,
1836{
1837 let mut high = msg.gas_limit;
1838 let mut low = msg.gas_limit;
1839
1840 async fn can_succeed<DB>(
1841 data: &Ctx<DB>,
1842 mut msg: Message,
1843 prior_messages: &[ChainMessage],
1844 ts: Tipset,
1845 limit: u64,
1846 ) -> anyhow::Result<bool>
1847 where
1848 DB: Blockstore + Send + Sync + 'static,
1849 {
1850 msg.gas_limit = limit;
1851 let (_invoc_res, apply_ret, _, _) = data
1852 .state_manager
1853 .call_with_gas(&mut msg.into(), prior_messages, Some(ts), VMFlush::Skip)
1854 .await?;
1855 Ok(apply_ret.msg_receipt().exit_code().is_success())
1856 }
1857
1858 while high < BLOCK_GAS_LIMIT {
1859 if can_succeed(data, msg.clone(), prior_messages, ts.shallow_clone(), high).await? {
1860 break;
1861 }
1862 low = high;
1863 high = high.saturating_mul(2).min(BLOCK_GAS_LIMIT);
1864 }
1865
1866 let mut check_threshold = high / 100;
1867 while (high - low) > check_threshold {
1868 let median = (high + low) / 2;
1869 if can_succeed(
1870 data,
1871 msg.clone(),
1872 prior_messages,
1873 ts.shallow_clone(),
1874 median,
1875 )
1876 .await?
1877 {
1878 high = median;
1879 } else {
1880 low = median;
1881 }
1882 check_threshold = median / 100;
1883 }
1884
1885 Ok(high)
1886}
1887
1888pub enum EthFeeHistory {}
1889
1890impl RpcMethod<3> for EthFeeHistory {
1891 const NAME: &'static str = "Filecoin.EthFeeHistory";
1892 const NAME_ALIAS: Option<&'static str> = Some("eth_feeHistory");
1893 const N_REQUIRED_PARAMS: usize = 2;
1894 const PARAM_NAMES: [&'static str; 3] = ["blockCount", "newestBlockNumber", "rewardPercentiles"];
1895 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
1896 const PERMISSION: Permission = Permission::Read;
1897
1898 type Params = (EthUint64, BlockNumberOrPredefined, Option<Vec<f64>>);
1899 type Ok = EthFeeHistoryResult;
1900
1901 async fn handle(
1902 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
1903 (EthUint64(block_count), newest_block_number, reward_percentiles): Self::Params,
1904 ext: &http::Extensions,
1905 ) -> Result<Self::Ok, ServerError> {
1906 let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
1907 let tipset = resolver
1908 .tipset_by_block_number_or_hash(newest_block_number, ResolveNullTipset::TakeOlder)
1909 .await?;
1910 eth_fee_history(ctx, tipset, block_count, reward_percentiles).await
1911 }
1912}
1913
1914async fn eth_fee_history<B: Blockstore + Send + Sync + 'static>(
1915 ctx: Ctx<B>,
1916 tipset: Tipset,
1917 block_count: u64,
1918 reward_percentiles: Option<Vec<f64>>,
1919) -> Result<EthFeeHistoryResult, ServerError> {
1920 if block_count > 1024 {
1921 return Err(anyhow::anyhow!("block count should be smaller than 1024").into());
1922 }
1923
1924 let reward_percentiles = reward_percentiles.unwrap_or_default();
1925 validate_reward_percentiles(&reward_percentiles)?;
1926
1927 let mut oldest_block_height = 1;
1928 let mut base_fee_array = vec![EthBigInt::from(
1933 &tipset.block_headers().first().parent_base_fee,
1934 )];
1935 let mut rewards_array = vec![];
1936 let mut gas_used_ratio_array = vec![];
1937 for ts in tipset
1938 .chain(ctx.store())
1939 .filter(|i| i.epoch() > 0)
1940 .take(block_count as _)
1941 {
1942 let base_fee = &ts.block_headers().first().parent_base_fee;
1943 let ExecutedTipset {
1944 executed_messages, ..
1945 } = ctx.state_manager.load_executed_tipset(&ts).await?;
1946 let mut tx_gas_rewards = Vec::with_capacity(executed_messages.len());
1947 for ExecutedMessage {
1948 message, receipt, ..
1949 } in executed_messages.iter()
1950 {
1951 let premium = message.effective_gas_premium(base_fee);
1952 tx_gas_rewards.push(GasReward {
1953 gas_used: receipt.gas_used(),
1954 premium,
1955 });
1956 }
1957 let (rewards, total_gas_used) =
1958 calculate_rewards_and_gas_used(&reward_percentiles, tx_gas_rewards);
1959 let max_gas = BLOCK_GAS_LIMIT * (ts.block_headers().len() as u64);
1960
1961 base_fee_array.push(EthBigInt::from(base_fee));
1963 gas_used_ratio_array.push((total_gas_used as f64) / (max_gas as f64));
1964 rewards_array.push(rewards);
1965
1966 oldest_block_height = ts.epoch();
1967 }
1968
1969 base_fee_array.reverse();
1971 gas_used_ratio_array.reverse();
1972 rewards_array.reverse();
1973
1974 Ok(EthFeeHistoryResult {
1975 oldest_block: EthUint64(oldest_block_height as _),
1976 base_fee_per_gas: base_fee_array,
1977 gas_used_ratio: gas_used_ratio_array,
1978 reward: if reward_percentiles.is_empty() {
1979 None
1980 } else {
1981 Some(rewards_array)
1982 },
1983 })
1984}
1985
1986fn validate_reward_percentiles(reward_percentiles: &[f64]) -> anyhow::Result<()> {
1987 if reward_percentiles.len() > 100 {
1988 anyhow::bail!("length of the reward percentile array cannot be greater than 100");
1989 }
1990
1991 for (&rp_prev, &rp) in std::iter::once(&0.0)
1992 .chain(reward_percentiles.iter())
1993 .tuple_windows()
1994 {
1995 if !(0. ..=100.).contains(&rp) {
1996 anyhow::bail!("invalid reward percentile: {rp} should be between 0 and 100");
1997 }
1998 if rp < rp_prev {
1999 anyhow::bail!(
2000 "invalid reward percentile: {rp} should be larger than or equal to {rp_prev}"
2001 );
2002 }
2003 }
2004
2005 Ok(())
2006}
2007
2008fn calculate_rewards_and_gas_used(
2009 reward_percentiles: &[f64],
2010 mut tx_gas_rewards: Vec<GasReward>,
2011) -> (Vec<EthBigInt>, u64) {
2012 const MIN_GAS_PREMIUM: u64 = 100000;
2013
2014 let gas_used_total = tx_gas_rewards.iter().map(|i| i.gas_used).sum();
2015 let mut rewards = reward_percentiles
2016 .iter()
2017 .map(|_| EthBigInt(MIN_GAS_PREMIUM.into()))
2018 .collect_vec();
2019 if !tx_gas_rewards.is_empty() {
2020 tx_gas_rewards.sort_by(|a, b| a.premium.cmp(&b.premium));
2021 let mut idx = 0;
2022 let mut sum = 0;
2023 #[allow(clippy::indexing_slicing)]
2024 for (i, &percentile) in reward_percentiles.iter().enumerate() {
2025 let threshold = ((gas_used_total as f64) * percentile / 100.) as u64;
2026 while sum < threshold && idx < tx_gas_rewards.len() - 1 {
2027 sum += tx_gas_rewards[idx].gas_used;
2028 idx += 1;
2029 }
2030 rewards[i] = (&tx_gas_rewards[idx].premium).into();
2031 }
2032 }
2033 (rewards, gas_used_total)
2034}
2035
2036pub enum EthGetCode {}
2037impl RpcMethod<2> for EthGetCode {
2038 const NAME: &'static str = "Filecoin.EthGetCode";
2039 const NAME_ALIAS: Option<&'static str> = Some("eth_getCode");
2040 const PARAM_NAMES: [&'static str; 2] = ["ethAddress", "blockNumberOrHash"];
2041 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2042 const PERMISSION: Permission = Permission::Read;
2043 const DESCRIPTION: Option<&'static str> = Some(
2044 "Retrieves the contract code at a specific address and block state, identified by its number, hash, or a special tag.",
2045 );
2046
2047 type Params = (EthAddress, BlockNumberOrHash);
2048 type Ok = EthBytes;
2049
2050 async fn handle(
2051 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2052 (eth_address, block_param): Self::Params,
2053 ext: &http::Extensions,
2054 ) -> Result<Self::Ok, ServerError> {
2055 let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
2056 let ts = resolver
2057 .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
2058 .await?;
2059 eth_get_code(&ctx, &ts, ð_address).await
2060 }
2061}
2062
2063async fn eth_get_code<DB>(
2064 ctx: &Ctx<DB>,
2065 ts: &Tipset,
2066 eth_address: &EthAddress,
2067) -> Result<EthBytes, ServerError>
2068where
2069 DB: Blockstore + Send + Sync + 'static,
2070{
2071 let to_address = FilecoinAddress::try_from(eth_address)?;
2072 let TipsetState { state_root, .. } = ctx.state_manager.load_tipset_state(ts).await?;
2073 let state_tree = ctx.state_manager.get_state_tree(&state_root)?;
2074 let Some(actor) = state_tree
2075 .get_actor(&to_address)
2076 .with_context(|| format!("failed to lookup contract {}", eth_address.0))?
2077 else {
2078 return Ok(Default::default());
2079 };
2080
2081 if !is_evm_actor(&actor.code) {
2084 return Ok(Default::default());
2085 }
2086
2087 let message = Message {
2088 from: FilecoinAddress::SYSTEM_ACTOR,
2089 to: to_address,
2090 method_num: METHOD_GET_BYTE_CODE,
2091 gas_limit: BLOCK_GAS_LIMIT,
2092 ..Default::default()
2093 };
2094
2095 let api_invoc_result = 'invoc: {
2096 for ts in ts.shallow_clone().chain(ctx.store()) {
2097 match ctx
2098 .state_manager
2099 .call_on_state(state_root, &message, Some(ts))
2100 {
2101 Ok(res) => {
2102 break 'invoc res;
2103 }
2104 Err(e) => tracing::warn!(%e),
2105 }
2106 }
2107 return Err(anyhow::anyhow!("Call failed").into());
2108 };
2109 let Some(msg_rct) = api_invoc_result.msg_rct else {
2110 return Err(anyhow::anyhow!("no message receipt").into());
2111 };
2112 if !msg_rct.exit_code().is_success() || !api_invoc_result.error.is_empty() {
2113 return Err(anyhow::anyhow!(
2114 "GetBytecode failed: exit={} error={}",
2115 msg_rct.exit_code(),
2116 api_invoc_result.error
2117 )
2118 .into());
2119 }
2120
2121 let get_bytecode_return: GetBytecodeReturn =
2122 fvm_ipld_encoding::from_slice(msg_rct.return_data().as_slice())?;
2123 if let Some(cid) = get_bytecode_return.0 {
2124 Ok(EthBytes(ctx.store().get_required(&cid)?))
2125 } else {
2126 Ok(Default::default())
2127 }
2128}
2129
2130pub enum EthGetStorageAt {}
2131impl RpcMethod<3> for EthGetStorageAt {
2132 const NAME: &'static str = "Filecoin.EthGetStorageAt";
2133 const NAME_ALIAS: Option<&'static str> = Some("eth_getStorageAt");
2134 const PARAM_NAMES: [&'static str; 3] = ["ethAddress", "position", "blockNumberOrHash"];
2135 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2136 const PERMISSION: Permission = Permission::Read;
2137 const DESCRIPTION: Option<&'static str> = Some(
2138 "Retrieves the storage value at a specific position for a contract
2139 at a given block state, identified by its number, hash, or a special tag.",
2140 );
2141
2142 type Params = (EthAddress, EthBytes, BlockNumberOrHash);
2143 type Ok = EthBytes;
2144
2145 async fn handle(
2146 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2147 (eth_address, position, block_number_or_hash): Self::Params,
2148 ext: &http::Extensions,
2149 ) -> Result<Self::Ok, ServerError> {
2150 let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
2151 let ts = resolver
2152 .tipset_by_block_number_or_hash(block_number_or_hash, ResolveNullTipset::TakeOlder)
2153 .await?;
2154 get_storage_at(&ctx, ts, eth_address, position).await
2155 }
2156}
2157
2158async fn get_storage_at<DB: Blockstore + Send + Sync + 'static>(
2159 ctx: &Ctx<DB>,
2160 ts: Tipset,
2161 eth_address: EthAddress,
2162 position: EthBytes,
2163) -> Result<EthBytes, ServerError> {
2164 let to_address = FilecoinAddress::try_from(ð_address)?;
2165 let TipsetState { state_root, .. } = ctx.state_manager.load_tipset_state(&ts).await?;
2166 let make_empty_result = || EthBytes(vec![0; EVM_WORD_LENGTH]);
2167 let Some(actor) = ctx
2168 .state_manager
2169 .get_actor(&to_address, state_root)
2170 .with_context(|| format!("failed to lookup contract {}", eth_address.0))?
2171 else {
2172 return Ok(make_empty_result());
2173 };
2174
2175 if !is_evm_actor(&actor.code) {
2176 return Ok(make_empty_result());
2177 }
2178
2179 let params = RawBytes::new(GetStorageAtParams::new(position.0)?.serialize_params()?);
2180 let message = Message {
2181 from: FilecoinAddress::SYSTEM_ACTOR,
2182 to: to_address,
2183 method_num: METHOD_GET_STORAGE_AT,
2184 gas_limit: BLOCK_GAS_LIMIT,
2185 params,
2186 ..Default::default()
2187 };
2188 let api_invoc_result = 'invoc: {
2189 for ts in ts.chain(ctx.store()) {
2190 match ctx
2191 .state_manager
2192 .call_on_state(state_root, &message, Some(ts))
2193 {
2194 Ok(res) => {
2195 break 'invoc res;
2196 }
2197 Err(e) => tracing::warn!(%e),
2198 }
2199 }
2200 return Err(anyhow::anyhow!("Call failed").into());
2201 };
2202 let Some(msg_rct) = api_invoc_result.msg_rct else {
2203 return Err(anyhow::anyhow!("no message receipt").into());
2204 };
2205 if !msg_rct.exit_code().is_success() || !api_invoc_result.error.is_empty() {
2206 return Err(
2207 anyhow::anyhow!("failed to lookup storage slot: {}", api_invoc_result.error).into(),
2208 );
2209 }
2210
2211 let mut ret = fvm_ipld_encoding::from_slice::<RawBytes>(msg_rct.return_data().as_slice())?
2212 .bytes()
2213 .to_vec();
2214 if ret.len() < EVM_WORD_LENGTH {
2215 let mut with_padding = vec![0; EVM_WORD_LENGTH.saturating_sub(ret.len())];
2216 with_padding.append(&mut ret);
2217 Ok(EthBytes(with_padding))
2218 } else {
2219 Ok(EthBytes(ret))
2220 }
2221}
2222
2223pub enum EthGetTransactionCount {}
2224impl RpcMethod<2> for EthGetTransactionCount {
2225 const NAME: &'static str = "Filecoin.EthGetTransactionCount";
2226 const NAME_ALIAS: Option<&'static str> = Some("eth_getTransactionCount");
2227 const PARAM_NAMES: [&'static str; 2] = ["sender", "blockParam"];
2228 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2229 const PERMISSION: Permission = Permission::Read;
2230
2231 type Params = (EthAddress, BlockNumberOrHash);
2232 type Ok = EthUint64;
2233
2234 async fn handle(
2235 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2236 (sender, block_param): Self::Params,
2237 ext: &http::Extensions,
2238 ) -> Result<Self::Ok, ServerError> {
2239 let addr = sender.to_filecoin_address()?;
2240 match block_param {
2241 BlockNumberOrHash::PredefinedBlock(Predefined::Pending) => {
2242 Ok(EthUint64(ctx.mpool.get_sequence(&addr)?))
2243 }
2244 _ => {
2245 let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
2246 let ts = resolver
2247 .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
2248 .await?;
2249 eth_get_transaction_count(&ctx, &ts, addr).await
2250 }
2251 }
2252 }
2253}
2254
2255async fn eth_get_transaction_count<B>(
2256 ctx: &Ctx<B>,
2257 ts: &Tipset,
2258 addr: FilecoinAddress,
2259) -> Result<EthUint64, ServerError>
2260where
2261 B: Blockstore + Send + Sync + 'static,
2262{
2263 let TipsetState { state_root, .. } = ctx.state_manager.load_tipset_state(ts).await?;
2264
2265 let state_tree = ctx.state_manager.get_state_tree(&state_root)?;
2266 let actor = match state_tree.get_actor(&addr)? {
2267 Some(actor) => actor,
2268 None => return Ok(EthUint64(0)),
2269 };
2270
2271 if is_evm_actor(&actor.code) {
2272 let evm_state = evm::State::load(ctx.store(), actor.code, actor.state)?;
2273 if !evm_state.is_alive() {
2274 return Ok(EthUint64(0));
2275 }
2276 Ok(EthUint64(evm_state.nonce()))
2277 } else {
2278 Ok(EthUint64(actor.sequence))
2279 }
2280}
2281
2282pub enum EthMaxPriorityFeePerGas {}
2283impl RpcMethod<0> for EthMaxPriorityFeePerGas {
2284 const NAME: &'static str = "Filecoin.EthMaxPriorityFeePerGas";
2285 const NAME_ALIAS: Option<&'static str> = Some("eth_maxPriorityFeePerGas");
2286 const PARAM_NAMES: [&'static str; 0] = [];
2287 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2288 const PERMISSION: Permission = Permission::Read;
2289
2290 type Params = ();
2291 type Ok = EthBigInt;
2292
2293 async fn handle(
2294 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2295 (): Self::Params,
2296 _: &http::Extensions,
2297 ) -> Result<Self::Ok, ServerError> {
2298 match gas::estimate_gas_premium(&ctx, 0, &ApiTipsetKey(None)).await {
2299 Ok(gas_premium) => Ok(EthBigInt(gas_premium.atto().clone())),
2300 Err(_) => Ok(EthBigInt(num_bigint::BigInt::zero())),
2301 }
2302 }
2303}
2304
2305pub enum EthProtocolVersion {}
2306impl RpcMethod<0> for EthProtocolVersion {
2307 const NAME: &'static str = "Filecoin.EthProtocolVersion";
2308 const NAME_ALIAS: Option<&'static str> = Some("eth_protocolVersion");
2309 const PARAM_NAMES: [&'static str; 0] = [];
2310 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2311 const PERMISSION: Permission = Permission::Read;
2312
2313 type Params = ();
2314 type Ok = EthUint64;
2315
2316 async fn handle(
2317 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2318 (): Self::Params,
2319 _: &http::Extensions,
2320 ) -> Result<Self::Ok, ServerError> {
2321 let epoch = ctx.chain_store().heaviest_tipset().epoch();
2322 let version = u32::from(ctx.state_manager.get_network_version(epoch).0);
2323 Ok(EthUint64(version.into()))
2324 }
2325}
2326
2327pub enum EthGetTransactionByBlockNumberAndIndex {}
2328impl RpcMethod<2> for EthGetTransactionByBlockNumberAndIndex {
2329 const NAME: &'static str = "Filecoin.EthGetTransactionByBlockNumberAndIndex";
2330 const NAME_ALIAS: Option<&'static str> = Some("eth_getTransactionByBlockNumberAndIndex");
2331 const PARAM_NAMES: [&'static str; 2] = ["blockParam", "txIndex"];
2332 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2333 const PERMISSION: Permission = Permission::Read;
2334 const DESCRIPTION: Option<&'static str> =
2335 Some("Retrieves a transaction by its block number and index.");
2336
2337 type Params = (BlockNumberOrPredefined, EthUint64);
2338 type Ok = Option<ApiEthTx>;
2339
2340 async fn handle(
2341 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2342 (block_param, tx_index): Self::Params,
2343 ext: &http::Extensions,
2344 ) -> Result<Self::Ok, ServerError> {
2345 let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
2346 let ts = resolver
2347 .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
2348 .await?;
2349 eth_tx_by_block_num_and_idx(&ctx, &ts, tx_index)
2350 }
2351}
2352
2353fn eth_tx_by_block_num_and_idx<B>(
2354 ctx: &Ctx<B>,
2355 ts: &Tipset,
2356 tx_index: EthUint64,
2357) -> Result<Option<ApiEthTx>, ServerError>
2358where
2359 B: Blockstore + Send + Sync + 'static,
2360{
2361 let messages = ctx.chain_store().messages_for_tipset(ts)?;
2362
2363 let EthUint64(index) = tx_index;
2364 let msg = messages.get(index as usize).with_context(|| {
2365 format!(
2366 "failed to get transaction at index {}: index {} out of range: tipset contains {} messages",
2367 index,
2368 index,
2369 messages.len()
2370 )
2371 })?;
2372
2373 let state = ctx.state_manager.get_state_tree(ts.parent_state())?;
2374
2375 let tx = new_eth_tx(ctx, &state, ts.epoch(), &ts.key().cid()?, &msg.cid(), index)?;
2376
2377 Ok(Some(tx))
2378}
2379
2380pub enum EthGetTransactionByBlockHashAndIndex {}
2381impl RpcMethod<2> for EthGetTransactionByBlockHashAndIndex {
2382 const NAME: &'static str = "Filecoin.EthGetTransactionByBlockHashAndIndex";
2383 const NAME_ALIAS: Option<&'static str> = Some("eth_getTransactionByBlockHashAndIndex");
2384 const PARAM_NAMES: [&'static str; 2] = ["blockHash", "txIndex"];
2385 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2386 const PERMISSION: Permission = Permission::Read;
2387
2388 type Params = (EthHash, EthUint64);
2389 type Ok = Option<ApiEthTx>;
2390
2391 async fn handle(
2392 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2393 (block_hash, tx_index): Self::Params,
2394 _: &http::Extensions,
2395 ) -> Result<Self::Ok, ServerError> {
2396 let ts = get_tipset_from_hash(ctx.chain_store(), &block_hash)?;
2397
2398 let messages = ctx.chain_store().messages_for_tipset(&ts)?;
2399
2400 let EthUint64(index) = tx_index;
2401 let msg = messages.get(index as usize).with_context(|| {
2402 format!(
2403 "index {} out of range: tipset contains {} messages",
2404 index,
2405 messages.len()
2406 )
2407 })?;
2408
2409 let state = ctx.state_manager.get_state_tree(ts.parent_state())?;
2410
2411 let tx = new_eth_tx(
2412 &ctx,
2413 &state,
2414 ts.epoch(),
2415 &ts.key().cid()?,
2416 &msg.cid(),
2417 index,
2418 )?;
2419
2420 Ok(Some(tx))
2421 }
2422}
2423
2424pub enum EthGetTransactionByHash {}
2425impl RpcMethod<1> for EthGetTransactionByHash {
2426 const NAME: &'static str = "Filecoin.EthGetTransactionByHash";
2427 const NAME_ALIAS: Option<&'static str> = Some("eth_getTransactionByHash");
2428 const PARAM_NAMES: [&'static str; 1] = ["txHash"];
2429 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2430 const PERMISSION: Permission = Permission::Read;
2431
2432 type Params = (EthHash,);
2433 type Ok = Option<ApiEthTx>;
2434
2435 async fn handle(
2436 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2437 (tx_hash,): Self::Params,
2438 _: &http::Extensions,
2439 ) -> Result<Self::Ok, ServerError> {
2440 get_eth_transaction_by_hash(&ctx, &tx_hash, None).await
2441 }
2442}
2443
2444pub enum EthGetTransactionByHashLimited {}
2445impl RpcMethod<2> for EthGetTransactionByHashLimited {
2446 const NAME: &'static str = "Filecoin.EthGetTransactionByHashLimited";
2447 const NAME_ALIAS: Option<&'static str> = Some("eth_getTransactionByHashLimited");
2448 const PARAM_NAMES: [&'static str; 2] = ["txHash", "limit"];
2449 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2450 const PERMISSION: Permission = Permission::Read;
2451
2452 type Params = (EthHash, ChainEpoch);
2453 type Ok = Option<ApiEthTx>;
2454
2455 async fn handle(
2456 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2457 (tx_hash, limit): Self::Params,
2458 _: &http::Extensions,
2459 ) -> Result<Self::Ok, ServerError> {
2460 get_eth_transaction_by_hash(&ctx, &tx_hash, Some(limit)).await
2461 }
2462}
2463
2464async fn get_eth_transaction_by_hash(
2465 ctx: &Ctx<impl Blockstore + Send + Sync + 'static>,
2466 tx_hash: &EthHash,
2467 limit: Option<ChainEpoch>,
2468) -> Result<Option<ApiEthTx>, ServerError> {
2469 let message_cid = ctx.chain_store().get_mapping(tx_hash)?.unwrap_or_else(|| {
2470 tracing::debug!(
2471 "could not find transaction hash {} in Ethereum mapping",
2472 tx_hash
2473 );
2474 tx_hash.to_cid()
2476 });
2477
2478 if let Ok(Some((tipset, receipt))) = ctx
2480 .state_manager
2481 .search_for_message(None, message_cid, limit, Some(true))
2482 .await
2483 {
2484 let ipld = receipt.return_data().deserialize().unwrap_or(Ipld::Null);
2485 let message_lookup = MessageLookup {
2486 receipt,
2487 tipset: tipset.key().clone(),
2488 height: tipset.epoch(),
2489 message: message_cid,
2490 return_dec: ipld,
2491 };
2492
2493 if let Ok(tx) = new_eth_tx_from_message_lookup(ctx, &message_lookup, None) {
2494 return Ok(Some(tx));
2495 }
2496 }
2497
2498 let (pending, _) = ctx.mpool.pending();
2500
2501 if let Some(smsg) = pending.iter().find(|item| item.cid() == message_cid) {
2502 if let Ok(eth_tx) = EthTx::from_signed_message(ctx.chain_config().eth_chain_id, smsg) {
2510 return Ok(Some(eth_tx.into()));
2511 }
2512 }
2513
2514 Ok(None)
2516}
2517
2518pub enum EthGetTransactionHashByCid {}
2519impl RpcMethod<1> for EthGetTransactionHashByCid {
2520 const NAME: &'static str = "Filecoin.EthGetTransactionHashByCid";
2521 const NAME_ALIAS: Option<&'static str> = Some("eth_getTransactionHashByCid");
2522 const PARAM_NAMES: [&'static str; 1] = ["cid"];
2523 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2524 const PERMISSION: Permission = Permission::Read;
2525
2526 type Params = (Cid,);
2527 type Ok = Option<EthHash>;
2528
2529 async fn handle(
2530 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2531 (cid,): Self::Params,
2532 _: &http::Extensions,
2533 ) -> Result<Self::Ok, ServerError> {
2534 let smsgs_result: Result<Vec<SignedMessage>, crate::chain::Error> =
2535 crate::chain::messages_from_cids(ctx.store(), &[cid]);
2536 if let Ok(smsgs) = smsgs_result
2537 && let Some(smsg) = smsgs.first()
2538 {
2539 let hash = if smsg.is_delegated() {
2540 let chain_id = ctx.chain_config().eth_chain_id;
2541 let (_, tx) = eth_tx_from_signed_eth_message(smsg, chain_id)?;
2542 tx.eth_hash()?.into()
2543 } else if smsg.is_secp256k1() {
2544 smsg.cid().into()
2545 } else {
2546 smsg.message().cid().into()
2547 };
2548 return Ok(Some(hash));
2549 }
2550
2551 let msg_result = crate::chain::get_chain_message(ctx.store(), &cid);
2552 if let Ok(msg) = msg_result {
2553 return Ok(Some(msg.cid().into()));
2554 }
2555
2556 Ok(None)
2557 }
2558}
2559
2560pub enum EthCall {}
2561impl RpcMethod<2> for EthCall {
2562 const NAME: &'static str = "Filecoin.EthCall";
2563 const NAME_ALIAS: Option<&'static str> = Some("eth_call");
2564 const N_REQUIRED_PARAMS: usize = 2;
2565 const PARAM_NAMES: [&'static str; 2] = ["tx", "blockParam"];
2566 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2567 const PERMISSION: Permission = Permission::Read;
2568 type Params = (EthCallMessage, BlockNumberOrHash);
2569 type Ok = EthBytes;
2570 async fn handle(
2571 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2572 (tx, block_param): Self::Params,
2573 ext: &http::Extensions,
2574 ) -> Result<Self::Ok, ServerError> {
2575 let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
2576 let ts = resolver
2577 .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
2578 .await?;
2579 eth_call(&ctx, tx, ts).await
2580 }
2581}
2582
2583async fn eth_call<DB>(
2584 ctx: &Ctx<DB>,
2585 tx: EthCallMessage,
2586 ts: Tipset,
2587) -> Result<EthBytes, ServerError>
2588where
2589 DB: Blockstore + Send + Sync + 'static,
2590{
2591 let msg = Message::try_from(tx)?;
2592 let invoke_result = apply_message(ctx, Some(ts), msg.clone()).await?;
2593
2594 if msg.to() == FilecoinAddress::ETHEREUM_ACCOUNT_MANAGER_ACTOR {
2595 Ok(EthBytes::default())
2596 } else {
2597 let msg_rct = invoke_result.msg_rct.context("no message receipt")?;
2598 let return_data = msg_rct.return_data();
2599 if return_data.is_empty() {
2600 Ok(Default::default())
2601 } else {
2602 let bytes = decode_payload(&return_data, CBOR)?;
2603 Ok(bytes)
2604 }
2605 }
2606}
2607
2608pub enum EthNewFilter {}
2609impl RpcMethod<1> for EthNewFilter {
2610 const NAME: &'static str = "Filecoin.EthNewFilter";
2611 const NAME_ALIAS: Option<&'static str> = Some("eth_newFilter");
2612 const PARAM_NAMES: [&'static str; 1] = ["filterSpec"];
2613 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2614 const PERMISSION: Permission = Permission::Read;
2615
2616 type Params = (EthFilterSpec,);
2617 type Ok = FilterID;
2618
2619 async fn handle(
2620 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2621 (filter_spec,): Self::Params,
2622 _: &http::Extensions,
2623 ) -> Result<Self::Ok, ServerError> {
2624 let eth_event_handler = ctx.eth_event_handler.clone();
2625 let chain_height = ctx.chain_store().heaviest_tipset().epoch();
2626 Ok(eth_event_handler.eth_new_filter(&filter_spec, chain_height)?)
2627 }
2628}
2629
2630pub enum EthNewPendingTransactionFilter {}
2631impl RpcMethod<0> for EthNewPendingTransactionFilter {
2632 const NAME: &'static str = "Filecoin.EthNewPendingTransactionFilter";
2633 const NAME_ALIAS: Option<&'static str> = Some("eth_newPendingTransactionFilter");
2634 const PARAM_NAMES: [&'static str; 0] = [];
2635 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2636 const PERMISSION: Permission = Permission::Read;
2637
2638 type Params = ();
2639 type Ok = FilterID;
2640
2641 async fn handle(
2642 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2643 (): Self::Params,
2644 _: &http::Extensions,
2645 ) -> Result<Self::Ok, ServerError> {
2646 let eth_event_handler = ctx.eth_event_handler.clone();
2647 Ok(eth_event_handler.eth_new_pending_transaction_filter()?)
2648 }
2649}
2650
2651pub enum EthNewBlockFilter {}
2652impl RpcMethod<0> for EthNewBlockFilter {
2653 const NAME: &'static str = "Filecoin.EthNewBlockFilter";
2654 const NAME_ALIAS: Option<&'static str> = Some("eth_newBlockFilter");
2655 const PARAM_NAMES: [&'static str; 0] = [];
2656 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2657 const PERMISSION: Permission = Permission::Read;
2658
2659 type Params = ();
2660 type Ok = FilterID;
2661
2662 async fn handle(
2663 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2664 (): Self::Params,
2665 _: &http::Extensions,
2666 ) -> Result<Self::Ok, ServerError> {
2667 let eth_event_handler = ctx.eth_event_handler.clone();
2668
2669 Ok(eth_event_handler.eth_new_block_filter()?)
2670 }
2671}
2672
2673pub enum EthUninstallFilter {}
2674impl RpcMethod<1> for EthUninstallFilter {
2675 const NAME: &'static str = "Filecoin.EthUninstallFilter";
2676 const NAME_ALIAS: Option<&'static str> = Some("eth_uninstallFilter");
2677 const PARAM_NAMES: [&'static str; 1] = ["filterId"];
2678 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2679 const PERMISSION: Permission = Permission::Read;
2680
2681 type Params = (FilterID,);
2682 type Ok = bool;
2683
2684 async fn handle(
2685 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2686 (filter_id,): Self::Params,
2687 _: &http::Extensions,
2688 ) -> Result<Self::Ok, ServerError> {
2689 let eth_event_handler = ctx.eth_event_handler.clone();
2690
2691 Ok(eth_event_handler.eth_uninstall_filter(&filter_id)?)
2692 }
2693}
2694
2695pub enum EthUnsubscribe {}
2696impl RpcMethod<0> for EthUnsubscribe {
2697 const NAME: &'static str = "Filecoin.EthUnsubscribe";
2698 const NAME_ALIAS: Option<&'static str> = Some("eth_unsubscribe");
2699 const PARAM_NAMES: [&'static str; 0] = [];
2700 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2701 const PERMISSION: Permission = Permission::Read;
2702 const SUBSCRIPTION: bool = true;
2703
2704 type Params = ();
2705 type Ok = ();
2706
2707 async fn handle(
2713 _: Ctx<impl Blockstore + Send + Sync + 'static>,
2714 (): Self::Params,
2715 _: &http::Extensions,
2716 ) -> Result<Self::Ok, ServerError> {
2717 Ok(())
2718 }
2719}
2720
2721pub enum EthSubscribe {}
2722impl RpcMethod<0> for EthSubscribe {
2723 const NAME: &'static str = "Filecoin.EthSubscribe";
2724 const NAME_ALIAS: Option<&'static str> = Some("eth_subscribe");
2725 const PARAM_NAMES: [&'static str; 0] = [];
2726 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2727 const PERMISSION: Permission = Permission::Read;
2728 const SUBSCRIPTION: bool = true;
2729
2730 type Params = ();
2731 type Ok = ();
2732
2733 async fn handle(
2739 _: Ctx<impl Blockstore + Send + Sync + 'static>,
2740 (): Self::Params,
2741 _: &http::Extensions,
2742 ) -> Result<Self::Ok, ServerError> {
2743 Ok(())
2744 }
2745}
2746
2747pub enum EthAddressToFilecoinAddress {}
2748impl RpcMethod<1> for EthAddressToFilecoinAddress {
2749 const NAME: &'static str = "Filecoin.EthAddressToFilecoinAddress";
2750 const PARAM_NAMES: [&'static str; 1] = ["ethAddress"];
2751 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2752 const PERMISSION: Permission = Permission::Read;
2753 const DESCRIPTION: Option<&'static str> =
2754 Some("Converts an EthAddress into an f410 Filecoin Address");
2755 type Params = (EthAddress,);
2756 type Ok = FilecoinAddress;
2757 async fn handle(
2758 _ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2759 (eth_address,): Self::Params,
2760 _: &http::Extensions,
2761 ) -> Result<Self::Ok, ServerError> {
2762 Ok(eth_address.to_filecoin_address()?)
2763 }
2764}
2765
2766pub enum FilecoinAddressToEthAddress {}
2767impl RpcMethod<2> for FilecoinAddressToEthAddress {
2768 const NAME: &'static str = "Filecoin.FilecoinAddressToEthAddress";
2769 const N_REQUIRED_PARAMS: usize = 1;
2770 const PARAM_NAMES: [&'static str; 2] = ["filecoinAddress", "blockParam"];
2771 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2772 const PERMISSION: Permission = Permission::Read;
2773 const DESCRIPTION: Option<&'static str> =
2774 Some("Converts any Filecoin address to an EthAddress");
2775 type Params = (FilecoinAddress, Option<BlockNumberOrPredefined>);
2776 type Ok = EthAddress;
2777 async fn handle(
2778 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2779 (address, block_param): Self::Params,
2780 ext: &http::Extensions,
2781 ) -> Result<Self::Ok, ServerError> {
2782 if let Ok(eth_address) = EthAddress::from_filecoin_address(&address) {
2783 Ok(eth_address)
2784 } else {
2785 let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
2786 let block_param = block_param.unwrap_or_else(|| Predefined::Finalized.into());
2788 let ts = resolver
2789 .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
2790 .await?;
2791
2792 let id_address = ctx.state_manager.lookup_required_id(&address, &ts)?;
2793 Ok(EthAddress::from_filecoin_address(&id_address)?)
2794 }
2795 }
2796}
2797
2798async fn get_eth_transaction_receipt(
2799 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2800 tx_hash: EthHash,
2801 limit: Option<ChainEpoch>,
2802) -> Result<Option<EthTxReceipt>, ServerError> {
2803 let msg_cid = ctx.chain_store().get_mapping(&tx_hash)?.unwrap_or_else(|| {
2804 tracing::debug!(
2805 "could not find transaction hash {} in Ethereum mapping",
2806 tx_hash
2807 );
2808 tx_hash.to_cid()
2810 });
2811
2812 let option = ctx
2813 .state_manager
2814 .search_for_message(None, msg_cid, limit, Some(true))
2815 .await
2816 .with_context(|| format!("failed to lookup Eth Txn {tx_hash} as {msg_cid}"));
2817
2818 let option = match option {
2819 Ok(opt) => opt,
2820 Err(e) => {
2822 tracing::debug!("could not find transaction receipt for hash {tx_hash}: {e}");
2823 return Ok(None);
2824 }
2825 };
2826
2827 let (tipset, receipt) = option.context("not indexed")?;
2828 let ipld = receipt.return_data().deserialize().unwrap_or(Ipld::Null);
2829 let message_lookup = MessageLookup {
2830 receipt,
2831 tipset: tipset.key().clone(),
2832 height: tipset.epoch(),
2833 message: msg_cid,
2834 return_dec: ipld,
2835 };
2836
2837 let tx = new_eth_tx_from_message_lookup(&ctx, &message_lookup, None)
2838 .with_context(|| format!("failed to convert {tx_hash} into an Eth Tx"))?;
2839
2840 let ts = ctx
2841 .chain_index()
2842 .load_required_tipset(&message_lookup.tipset)?;
2843
2844 let parent_ts = ctx
2846 .chain_index()
2847 .load_required_tipset(ts.parents())
2848 .map_err(|e| {
2849 format!(
2850 "failed to lookup tipset {} when constructing the eth txn receipt: {}",
2851 ts.parents(),
2852 e
2853 )
2854 })?;
2855
2856 let tx_receipt = new_eth_tx_receipt(&ctx, &parent_ts, &tx, &message_lookup.receipt).await?;
2857
2858 Ok(Some(tx_receipt))
2859}
2860
2861pub enum EthGetTransactionReceipt {}
2862impl RpcMethod<1> for EthGetTransactionReceipt {
2863 const NAME: &'static str = "Filecoin.EthGetTransactionReceipt";
2864 const NAME_ALIAS: Option<&'static str> = Some("eth_getTransactionReceipt");
2865 const N_REQUIRED_PARAMS: usize = 1;
2866 const PARAM_NAMES: [&'static str; 1] = ["txHash"];
2867 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2868 const PERMISSION: Permission = Permission::Read;
2869 type Params = (EthHash,);
2870 type Ok = Option<EthTxReceipt>;
2871 async fn handle(
2872 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2873 (tx_hash,): Self::Params,
2874 _: &http::Extensions,
2875 ) -> Result<Self::Ok, ServerError> {
2876 get_eth_transaction_receipt(ctx, tx_hash, None).await
2877 }
2878}
2879
2880pub enum EthGetTransactionReceiptLimited {}
2881impl RpcMethod<2> for EthGetTransactionReceiptLimited {
2882 const NAME: &'static str = "Filecoin.EthGetTransactionReceiptLimited";
2883 const NAME_ALIAS: Option<&'static str> = Some("eth_getTransactionReceiptLimited");
2884 const N_REQUIRED_PARAMS: usize = 1;
2885 const PARAM_NAMES: [&'static str; 2] = ["txHash", "limit"];
2886 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2887 const PERMISSION: Permission = Permission::Read;
2888 type Params = (EthHash, ChainEpoch);
2889 type Ok = Option<EthTxReceipt>;
2890 async fn handle(
2891 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2892 (tx_hash, limit): Self::Params,
2893 _: &http::Extensions,
2894 ) -> Result<Self::Ok, ServerError> {
2895 get_eth_transaction_receipt(ctx, tx_hash, Some(limit)).await
2896 }
2897}
2898
2899pub enum EthSendRawTransaction {}
2900impl RpcMethod<1> for EthSendRawTransaction {
2901 const NAME: &'static str = "Filecoin.EthSendRawTransaction";
2902 const NAME_ALIAS: Option<&'static str> = Some("eth_sendRawTransaction");
2903 const PARAM_NAMES: [&'static str; 1] = ["rawTx"];
2904 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2905 const PERMISSION: Permission = Permission::Read;
2906
2907 type Params = (EthBytes,);
2908 type Ok = EthHash;
2909
2910 async fn handle(
2911 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2912 (raw_tx,): Self::Params,
2913 _: &http::Extensions,
2914 ) -> Result<Self::Ok, ServerError> {
2915 let tx_args = parse_eth_transaction(&raw_tx.0)?;
2916 let smsg = tx_args.get_signed_message(ctx.chain_config().eth_chain_id)?;
2917 let cid = ctx.mpool.as_ref().push(smsg).await?;
2918 Ok(cid.into())
2919 }
2920}
2921
2922pub enum EthSendRawTransactionUntrusted {}
2923impl RpcMethod<1> for EthSendRawTransactionUntrusted {
2924 const NAME: &'static str = "Filecoin.EthSendRawTransactionUntrusted";
2925 const NAME_ALIAS: Option<&'static str> = Some("eth_sendRawTransactionUntrusted");
2926 const PARAM_NAMES: [&'static str; 1] = ["rawTx"];
2927 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
2928 const PERMISSION: Permission = Permission::Read;
2929
2930 type Params = (EthBytes,);
2931 type Ok = EthHash;
2932
2933 async fn handle(
2934 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
2935 (raw_tx,): Self::Params,
2936 _: &http::Extensions,
2937 ) -> Result<Self::Ok, ServerError> {
2938 let tx_args = parse_eth_transaction(&raw_tx.0)?;
2939 let smsg = tx_args.get_signed_message(ctx.chain_config().eth_chain_id)?;
2940 let cid = ctx.mpool.as_ref().push_untrusted(smsg).await?;
2941 Ok(cid.into())
2942 }
2943}
2944
2945#[derive(Clone, Debug, PartialEq)]
2946pub struct CollectedEvent {
2947 pub(crate) entries: Vec<EventEntry>,
2948 pub(crate) emitter_addr: crate::shim::address::Address,
2949 pub(crate) event_idx: u64,
2950 pub(crate) reverted: bool,
2951 pub(crate) height: ChainEpoch,
2952 pub(crate) tipset_key: TipsetKey,
2953 msg_idx: u64,
2954 pub(crate) msg_cid: Cid,
2955}
2956
2957fn match_key(key: &str) -> Option<usize> {
2958 match key.get(0..2) {
2959 Some("t1") => Some(0),
2960 Some("t2") => Some(1),
2961 Some("t3") => Some(2),
2962 Some("t4") => Some(3),
2963 _ => None,
2964 }
2965}
2966
2967fn eth_log_from_event(entries: &[EventEntry]) -> Option<(EthBytes, Vec<EthHash>)> {
2968 let mut topics_found = [false; 4];
2969 let mut topics_found_count = 0;
2970 let mut data_found = false;
2971 let mut data: EthBytes = EthBytes::default();
2972 let mut topics: Vec<EthHash> = Vec::default();
2973 for entry in entries {
2974 if entry.codec != IPLD_RAW {
2977 return None;
2978 }
2979 if let Some(idx) = match_key(&entry.key) {
2981 let result: Result<[u8; EVM_WORD_LENGTH], _> = entry.value.0.clone().try_into();
2983 let bytes = if let Ok(value) = result {
2984 value
2985 } else {
2986 tracing::warn!(
2987 "got an EVM event topic with an invalid size (key: {}, size: {})",
2988 entry.key,
2989 entry.value.0.len()
2990 );
2991 return None;
2992 };
2993 if *topics_found.get(idx).expect("Infallible") {
2995 tracing::warn!("got a duplicate EVM event topic (key: {})", entry.key);
2996 return None;
2997 }
2998 *topics_found.get_mut(idx).expect("Infallible") = true;
2999 topics_found_count += 1;
3000 if topics.len() <= idx {
3002 topics.resize(idx + 1, EthHash::default());
3003 }
3004 *topics.get_mut(idx).expect("Infallible") = bytes.into();
3005 } else if entry.key == "d" {
3006 if data_found {
3008 tracing::warn!("got duplicate EVM event data");
3009 return None;
3010 }
3011 data_found = true;
3012 data = EthBytes(entry.value.0.clone());
3013 } else {
3014 tracing::warn!("unexpected event entry (key: {})", entry.key);
3017 }
3018 }
3019 if topics.len() != topics_found_count {
3021 tracing::warn!(
3022 "EVM event topic length mismatch (expected: {}, actual: {})",
3023 topics.len(),
3024 topics_found_count
3025 );
3026 return None;
3027 }
3028 Some((data, topics))
3029}
3030
3031fn eth_tx_hash_from_signed_message(
3032 message: &SignedMessage,
3033 eth_chain_id: EthChainIdType,
3034) -> anyhow::Result<EthHash> {
3035 if message.is_delegated() {
3036 let (_, tx) = eth_tx_from_signed_eth_message(message, eth_chain_id)?;
3037 Ok(tx.eth_hash()?.into())
3038 } else if message.is_secp256k1() {
3039 Ok(message.cid().into())
3040 } else {
3041 Ok(message.message().cid().into())
3042 }
3043}
3044
3045fn eth_tx_hash_from_message_cid<DB: Blockstore>(
3046 blockstore: &DB,
3047 message_cid: &Cid,
3048 eth_chain_id: EthChainIdType,
3049) -> anyhow::Result<Option<EthHash>> {
3050 if let Ok(smsg) = crate::chain::message_from_cid(blockstore, message_cid) {
3051 return Ok(Some(eth_tx_hash_from_signed_message(&smsg, eth_chain_id)?));
3053 }
3054 let result: Result<Message, _> = crate::chain::message_from_cid(blockstore, message_cid);
3055 if result.is_ok() {
3056 let hash: EthHash = (*message_cid).into();
3058 return Ok(Some(hash));
3059 }
3060 Ok(None)
3061}
3062
3063fn transform_events<F>(events: &[CollectedEvent], f: F) -> anyhow::Result<Vec<EthLog>>
3064where
3065 F: Fn(&CollectedEvent) -> anyhow::Result<Option<EthLog>>,
3066{
3067 events
3068 .iter()
3069 .filter_map(|event| match f(event) {
3070 Ok(Some(eth_log)) => Some(Ok(eth_log)),
3071 Ok(None) => None,
3072 Err(e) => Some(Err(e)),
3073 })
3074 .collect()
3075}
3076
3077fn eth_filter_logs_from_tipsets(events: &[CollectedEvent]) -> anyhow::Result<Vec<EthHash>> {
3078 events
3079 .iter()
3080 .map(|event| event.tipset_key.cid().map(Into::into))
3081 .collect()
3082}
3083
3084fn eth_filter_logs_from_messages<DB: Blockstore>(
3085 ctx: &Ctx<DB>,
3086 events: &[CollectedEvent],
3087) -> anyhow::Result<Vec<EthHash>> {
3088 events
3089 .iter()
3090 .filter_map(|event| {
3091 match eth_tx_hash_from_message_cid(
3092 ctx.store(),
3093 &event.msg_cid,
3094 ctx.state_manager.chain_config().eth_chain_id,
3095 ) {
3096 Ok(Some(hash)) => Some(Ok(hash)),
3097 Ok(None) => {
3098 tracing::warn!("Ignoring event");
3099 None
3100 }
3101 Err(err) => Some(Err(err)),
3102 }
3103 })
3104 .collect()
3105}
3106
3107fn eth_filter_logs_from_events<DB: Blockstore>(
3108 ctx: &Ctx<DB>,
3109 events: &[CollectedEvent],
3110) -> anyhow::Result<Vec<EthLog>> {
3111 transform_events(events, |event| {
3112 let (data, topics) = if let Some((data, topics)) = eth_log_from_event(&event.entries) {
3113 (data, topics)
3114 } else {
3115 tracing::warn!("Ignoring event");
3116 return Ok(None);
3117 };
3118 let transaction_hash = if let Some(transaction_hash) = eth_tx_hash_from_message_cid(
3119 ctx.store(),
3120 &event.msg_cid,
3121 ctx.state_manager.chain_config().eth_chain_id,
3122 )? {
3123 transaction_hash
3124 } else {
3125 tracing::warn!("Ignoring event");
3126 return Ok(None);
3127 };
3128 let address = EthAddress::from_filecoin_address(&event.emitter_addr)?;
3129 Ok(Some(EthLog {
3130 address,
3131 data,
3132 topics,
3133 removed: event.reverted,
3134 log_index: event.event_idx.into(),
3135 transaction_index: event.msg_idx.into(),
3136 transaction_hash,
3137 block_hash: event.tipset_key.cid()?.into(),
3138 block_number: (event.height as u64).into(),
3139 }))
3140 })
3141}
3142
3143fn eth_filter_result_from_events<DB: Blockstore>(
3144 ctx: &Ctx<DB>,
3145 events: &[CollectedEvent],
3146) -> anyhow::Result<EthFilterResult> {
3147 Ok(EthFilterResult::Logs(eth_filter_logs_from_events(
3148 ctx, events,
3149 )?))
3150}
3151
3152fn eth_filter_result_from_tipsets(events: &[CollectedEvent]) -> anyhow::Result<EthFilterResult> {
3153 Ok(EthFilterResult::Hashes(eth_filter_logs_from_tipsets(
3154 events,
3155 )?))
3156}
3157
3158fn eth_filter_result_from_messages<DB: Blockstore>(
3159 ctx: &Ctx<DB>,
3160 events: &[CollectedEvent],
3161) -> anyhow::Result<EthFilterResult> {
3162 Ok(EthFilterResult::Hashes(eth_filter_logs_from_messages(
3163 ctx, events,
3164 )?))
3165}
3166
3167pub enum EthGetLogs {}
3168impl RpcMethod<1> for EthGetLogs {
3169 const NAME: &'static str = "Filecoin.EthGetLogs";
3170 const NAME_ALIAS: Option<&'static str> = Some("eth_getLogs");
3171 const N_REQUIRED_PARAMS: usize = 1;
3172 const PARAM_NAMES: [&'static str; 1] = ["ethFilter"];
3173 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
3174 const PERMISSION: Permission = Permission::Read;
3175 type Params = (EthFilterSpec,);
3176 type Ok = EthFilterResult;
3177 async fn handle(
3178 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
3179 (eth_filter,): Self::Params,
3180 _: &http::Extensions,
3181 ) -> Result<Self::Ok, ServerError> {
3182 let pf = ctx
3183 .eth_event_handler
3184 .parse_eth_filter_spec(&ctx, ð_filter)
3185 .map_err(|e| {
3186 if e.downcast_ref::<EthErrors>()
3187 .is_some_and(|eth_err| matches!(eth_err, EthErrors::BlockRangeExceeded { .. }))
3188 {
3189 return e;
3190 }
3191 e.context("failed to parse events for filter")
3192 })?;
3193 let events = ctx
3194 .eth_event_handler
3195 .get_events_for_parsed_filter(&ctx, &pf, SkipEvent::OnUnresolvedAddress)
3196 .await
3197 .context("failed to get events for filter")?;
3198 Ok(eth_filter_result_from_events(&ctx, &events)?)
3199 }
3200}
3201
3202pub enum EthGetFilterLogs {}
3203impl RpcMethod<1> for EthGetFilterLogs {
3204 const NAME: &'static str = "Filecoin.EthGetFilterLogs";
3205 const NAME_ALIAS: Option<&'static str> = Some("eth_getFilterLogs");
3206 const N_REQUIRED_PARAMS: usize = 1;
3207 const PARAM_NAMES: [&'static str; 1] = ["filterId"];
3208 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
3209 const PERMISSION: Permission = Permission::Write;
3210 type Params = (FilterID,);
3211 type Ok = EthFilterResult;
3212 async fn handle(
3213 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
3214 (filter_id,): Self::Params,
3215 _: &http::Extensions,
3216 ) -> Result<Self::Ok, ServerError> {
3217 let eth_event_handler = ctx.eth_event_handler.clone();
3218 if let Some(store) = ð_event_handler.filter_store {
3219 let filter = store.get(&filter_id)?;
3220 if let Some(event_filter) = filter.as_any().downcast_ref::<EventFilter>() {
3221 let events = ctx
3222 .eth_event_handler
3223 .get_events_for_parsed_filter(
3224 &ctx,
3225 &event_filter.into(),
3226 SkipEvent::OnUnresolvedAddress,
3227 )
3228 .await?;
3229 let recent_events: Vec<CollectedEvent> = events
3230 .clone()
3231 .into_iter()
3232 .filter(|event| !event_filter.collected.contains(event))
3233 .collect();
3234 let filter = Arc::new(EventFilter {
3235 id: event_filter.id.clone(),
3236 tipsets: event_filter.tipsets.clone(),
3237 addresses: event_filter.addresses.clone(),
3238 keys_with_codec: event_filter.keys_with_codec.clone(),
3239 max_results: event_filter.max_results,
3240 collected: events.clone(),
3241 });
3242 store.update(filter);
3243 return Ok(eth_filter_result_from_events(&ctx, &recent_events)?);
3244 }
3245 }
3246 Err(anyhow::anyhow!("method not supported").into())
3247 }
3248}
3249
3250pub enum EthGetFilterChanges {}
3251impl RpcMethod<1> for EthGetFilterChanges {
3252 const NAME: &'static str = "Filecoin.EthGetFilterChanges";
3253 const NAME_ALIAS: Option<&'static str> = Some("eth_getFilterChanges");
3254 const N_REQUIRED_PARAMS: usize = 1;
3255 const PARAM_NAMES: [&'static str; 1] = ["filterId"];
3256 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
3257 const PERMISSION: Permission = Permission::Write;
3258 const DESCRIPTION: Option<&'static str> =
3259 Some("Returns event logs which occurred since the last poll");
3260
3261 type Params = (FilterID,);
3262 type Ok = EthFilterResult;
3263 async fn handle(
3264 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
3265 (filter_id,): Self::Params,
3266 _: &http::Extensions,
3267 ) -> Result<Self::Ok, ServerError> {
3268 let eth_event_handler = ctx.eth_event_handler.clone();
3269 if let Some(store) = ð_event_handler.filter_store {
3270 let filter = store.get(&filter_id)?;
3271 if let Some(event_filter) = filter.as_any().downcast_ref::<EventFilter>() {
3272 let events = ctx
3273 .eth_event_handler
3274 .get_events_for_parsed_filter(
3275 &ctx,
3276 &event_filter.into(),
3277 SkipEvent::OnUnresolvedAddress,
3278 )
3279 .await?;
3280 let recent_events: Vec<CollectedEvent> = events
3281 .clone()
3282 .into_iter()
3283 .filter(|event| !event_filter.collected.contains(event))
3284 .collect();
3285 let filter = Arc::new(EventFilter {
3286 id: event_filter.id.clone(),
3287 tipsets: event_filter.tipsets.clone(),
3288 addresses: event_filter.addresses.clone(),
3289 keys_with_codec: event_filter.keys_with_codec.clone(),
3290 max_results: event_filter.max_results,
3291 collected: events.clone(),
3292 });
3293 store.update(filter);
3294 return Ok(eth_filter_result_from_events(&ctx, &recent_events)?);
3295 }
3296 if let Some(tipset_filter) = filter.as_any().downcast_ref::<TipSetFilter>() {
3297 let events = ctx
3298 .eth_event_handler
3299 .get_events_for_parsed_filter(
3300 &ctx,
3301 &ParsedFilter::new_with_tipset(ParsedFilterTipsets::Range(
3302 RangeInclusive::new(
3304 tipset_filter
3305 .collected
3306 .unwrap_or(ctx.chain_store().heaviest_tipset().epoch() - 1),
3307 -1,
3309 ),
3310 )),
3311 SkipEvent::OnUnresolvedAddress,
3312 )
3313 .await?;
3314 let new_collected = events
3315 .iter()
3316 .max_by_key(|event| event.height)
3317 .map(|e| e.height);
3318 if let Some(height) = new_collected {
3319 let filter = Arc::new(TipSetFilter {
3320 id: tipset_filter.id.clone(),
3321 max_results: tipset_filter.max_results,
3322 collected: Some(height),
3323 });
3324 store.update(filter);
3325 }
3326 return Ok(eth_filter_result_from_tipsets(&events)?);
3327 }
3328 if let Some(mempool_filter) = filter.as_any().downcast_ref::<MempoolFilter>() {
3329 let events = ctx
3330 .eth_event_handler
3331 .get_events_for_parsed_filter(
3332 &ctx,
3333 &ParsedFilter::new_with_tipset(ParsedFilterTipsets::Range(
3334 RangeInclusive::new(
3336 mempool_filter
3337 .collected
3338 .unwrap_or(ctx.chain_store().heaviest_tipset().epoch() - 1),
3339 -1,
3341 ),
3342 )),
3343 SkipEvent::OnUnresolvedAddress,
3344 )
3345 .await?;
3346 let new_collected = events
3347 .iter()
3348 .max_by_key(|event| event.height)
3349 .map(|e| e.height);
3350 if let Some(height) = new_collected {
3351 let filter = Arc::new(MempoolFilter {
3352 id: mempool_filter.id.clone(),
3353 max_results: mempool_filter.max_results,
3354 collected: Some(height),
3355 });
3356 store.update(filter);
3357 }
3358 return Ok(eth_filter_result_from_messages(&ctx, &events)?);
3359 }
3360 }
3361 Err(anyhow::anyhow!("method not supported").into())
3362 }
3363}
3364
3365pub enum EthTraceBlock {}
3366impl RpcMethod<1> for EthTraceBlock {
3367 const NAME: &'static str = "Filecoin.EthTraceBlock";
3368 const NAME_ALIAS: Option<&'static str> = Some("trace_block");
3369 const N_REQUIRED_PARAMS: usize = 1;
3370 const PARAM_NAMES: [&'static str; 1] = ["blockParam"];
3371 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
3372 const PERMISSION: Permission = Permission::Read;
3373 const DESCRIPTION: Option<&'static str> = Some("Returns traces created at given block.");
3374
3375 type Params = (BlockNumberOrHash,);
3376 type Ok = Vec<EthBlockTrace>;
3377 async fn handle(
3378 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
3379 (block_param,): Self::Params,
3380 ext: &http::Extensions,
3381 ) -> Result<Self::Ok, ServerError> {
3382 let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
3383 let ts = resolver
3384 .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
3385 .await?;
3386 eth_trace_block(&ctx, &ts, ext).await
3387 }
3388}
3389
3390async fn execute_tipset_traces<DB>(
3392 ctx: &Ctx<DB>,
3393 ts: &Tipset,
3394 ext: &http::Extensions,
3395) -> Result<(StateTree<DB>, Vec<trace::TipsetTraceEntry>), ServerError>
3396where
3397 DB: Blockstore + Send + Sync + 'static,
3398{
3399 let (state_root, raw_traces) = {
3400 let sm = ctx.state_manager.clone();
3401 let ts = ts.shallow_clone();
3402 tokio::task::spawn_blocking(move || sm.execution_trace(&ts))
3403 .await
3404 .context("execution_trace task panicked")??
3405 };
3406
3407 let state = ctx.state_manager.get_state_tree(&state_root)?;
3408
3409 let mut entries = Vec::new();
3410 let mut msg_idx = 0;
3411 for ir in raw_traces {
3412 if ir.msg.from == system::ADDRESS.into() {
3413 continue;
3414 }
3415 msg_idx += 1;
3416 let tx_hash = EthGetTransactionHashByCid::handle(ctx.clone(), (ir.msg_cid,), ext).await?;
3417 let tx_hash = tx_hash
3418 .with_context(|| format!("cannot find transaction hash for cid {}", ir.msg_cid))?;
3419 entries.push(trace::TipsetTraceEntry {
3420 tx_hash,
3421 msg_position: msg_idx,
3422 invoc_result: ir,
3423 });
3424 }
3425
3426 Ok((state, entries))
3427}
3428
3429async fn eth_trace_block<DB>(
3430 ctx: &Ctx<DB>,
3431 ts: &Tipset,
3432 ext: &http::Extensions,
3433) -> Result<Vec<EthBlockTrace>, ServerError>
3434where
3435 DB: Blockstore + Send + Sync + 'static,
3436{
3437 let (state, entries) = execute_tipset_traces(ctx, ts, ext).await?;
3438 let block_hash: EthHash = ts.key().cid()?.into();
3439 let mut all_traces = vec![];
3440
3441 for entry in entries {
3442 for trace in entry.build_parity_traces(&state)? {
3443 all_traces.push(EthBlockTrace {
3444 trace,
3445 block_hash,
3446 block_number: ts.epoch(),
3447 transaction_hash: entry.tx_hash,
3448 transaction_position: entry.msg_position,
3449 });
3450 }
3451 }
3452 Ok(all_traces)
3453}
3454
3455pub enum EthDebugTraceTransaction {}
3456impl RpcMethod<2> for EthDebugTraceTransaction {
3457 const N_REQUIRED_PARAMS: usize = 1;
3458 const NAME: &'static str = "Forest.EthDebugTraceTransaction";
3459 const NAME_ALIAS: Option<&'static str> = Some("debug_traceTransaction");
3460 const PARAM_NAMES: [&'static str; 2] = ["txHash", "opts"];
3461 const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::{ V1 | V2 });
3462 const PERMISSION: Permission = Permission::Read;
3463 const DESCRIPTION: Option<&'static str> =
3464 Some("Replays a transaction and returns execution traces in Geth-compatible format.");
3465
3466 type Params = (String, Option<GethDebugTracingOptions>);
3467 type Ok = GethTrace;
3468
3469 async fn handle(
3470 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
3471 (tx_hash, opts): Self::Params,
3472 ext: &http::Extensions,
3473 ) -> Result<Self::Ok, ServerError> {
3474 let opts = opts.unwrap_or_default();
3475 debug_trace_transaction(ctx, ext, Self::api_path(ext)?, tx_hash, opts).await
3476 }
3477}
3478
3479async fn debug_trace_transaction<DB>(
3480 ctx: Ctx<DB>,
3481 ext: &http::Extensions,
3482 api_path: ApiPaths,
3483 tx_hash: String,
3484 opts: GethDebugTracingOptions,
3485) -> Result<GethTrace, ServerError>
3486where
3487 DB: Blockstore + Send + Sync + 'static,
3488{
3489 let tracer = match &opts.tracer {
3490 Some(t) => t.clone(),
3491 None => {
3492 tracing::debug!(
3493 "no tracer specified for debug_traceTransaction; defaulting to callTracer (struct logger not supported)"
3494 );
3495 GethDebugBuiltInTracerType::Call
3496 }
3497 };
3498
3499 let eth_hash = EthHash::from_str(&tx_hash).context("invalid transaction hash")?;
3500 let eth_txn = get_eth_transaction_by_hash(&ctx, ð_hash, None)
3501 .await?
3502 .ok_or(ServerError::internal_error("transaction not found", None))?;
3503
3504 if eth_txn.block_hash == EthHash::default() {
3506 return Err(ServerError::invalid_params(
3507 "no trace for pending transactions",
3508 None,
3509 ));
3510 }
3511
3512 if tracer == GethDebugBuiltInTracerType::Noop {
3513 return Ok(GethTrace::Noop(NoopFrame {}));
3514 }
3515
3516 let resolver = TipsetResolver::new(&ctx, api_path);
3517 let ts = resolver
3518 .tipset_by_block_number_or_hash(eth_txn.block_number, ResolveNullTipset::TakeOlder)
3519 .await?;
3520
3521 if tracer == GethDebugBuiltInTracerType::PreState {
3524 let prestate_config = opts.prestate_config()?;
3525
3526 let message_cid = ctx
3527 .chain_store()
3528 .get_mapping(ð_hash)?
3529 .unwrap_or_else(|| eth_hash.to_cid());
3530
3531 let (pre_root, invoc_result, post_root) = ctx
3532 .state_manager
3533 .replay_for_prestate(ts.shallow_clone(), message_cid)
3534 .await
3535 .map_err(|e| anyhow::anyhow!("replay for prestate failed: {e}"))?;
3536
3537 let execution_trace = invoc_result
3538 .execution_trace
3539 .context("no execution trace for transaction")?;
3540
3541 let mut touched = extract_touched_eth_addresses(&execution_trace);
3542 if let Ok(addr) = EthAddress::from_filecoin_address(&invoc_result.msg.from()) {
3543 touched.insert(addr);
3544 }
3545
3546 if let Ok(addr) = EthAddress::from_filecoin_address(&invoc_result.msg.to()) {
3547 touched.insert(addr);
3548 }
3549
3550 let pre_state = StateTree::new_from_root(ctx.store_owned(), &pre_root)?;
3551 let post_state = StateTree::new_from_root(ctx.store_owned(), &post_root)?;
3552
3553 let frame = trace::build_prestate_frame(
3554 ctx.store(),
3555 &pre_state,
3556 &post_state,
3557 &touched,
3558 &prestate_config,
3559 )?;
3560
3561 return Ok(GethTrace::PreState(frame));
3562 }
3563
3564 let (state, entries) = execute_tipset_traces(&ctx, &ts, ext).await?;
3565 let entry = entries
3566 .into_iter()
3567 .find(|e| e.tx_hash == eth_hash)
3568 .ok_or_else(|| ServerError::internal_error("transaction trace not found in block", None))?;
3569
3570 let execution_trace = entry
3571 .invoc_result
3572 .execution_trace
3573 .context("no execution trace for transaction")?;
3574
3575 let mut env = trace::base_environment(&state, &entry.invoc_result.msg.from).map_err(|e| {
3576 anyhow::anyhow!(
3577 "when processing message {}: {e}",
3578 entry.invoc_result.msg_cid
3579 )
3580 })?;
3581
3582 match tracer {
3583 GethDebugBuiltInTracerType::Call => {
3584 let call_config = opts.call_config()?;
3585 let frame = trace::build_geth_call_frame(&mut env, execution_trace, &call_config)?;
3586 Ok(GethTrace::Call(frame.unwrap_or_default()))
3587 }
3588 GethDebugBuiltInTracerType::FlatCall => {
3589 trace::build_traces(&mut env, &[], execution_trace)?;
3590 let block_hash: EthHash = ts.key().cid()?.into();
3591 let traces = env
3592 .traces
3593 .into_iter()
3594 .map(|t| EthBlockTrace {
3595 trace: t,
3596 block_hash,
3597 block_number: ts.epoch(),
3598 transaction_hash: eth_hash,
3599 transaction_position: entry.msg_position,
3600 })
3601 .collect();
3602 Ok(GethTrace::FlatCall(traces))
3603 }
3604 _ => Err(anyhow::anyhow!(
3605 "unexpected tracer type: noopTracer and prestateTracer should be handled above"
3606 )
3607 .into()),
3608 }
3609}
3610
3611pub enum EthTraceCall {}
3612impl RpcMethod<3> for EthTraceCall {
3613 const NAME: &'static str = "Forest.EthTraceCall";
3614 const NAME_ALIAS: Option<&'static str> = Some("trace_call");
3615 const N_REQUIRED_PARAMS: usize = 1;
3616 const PARAM_NAMES: [&'static str; 3] = ["tx", "traceTypes", "blockParam"];
3617 const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::{ V1 | V2 });
3618 const PERMISSION: Permission = Permission::Read;
3619 const DESCRIPTION: Option<&'static str> =
3620 Some("Returns parity style trace results for the given transaction.");
3621
3622 type Params = (
3623 EthCallMessage,
3624 NonEmpty<EthTraceType>,
3625 Option<BlockNumberOrHash>,
3626 );
3627 type Ok = EthTraceResults;
3628 async fn handle(
3629 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
3630 (tx, trace_types, block_param): Self::Params,
3631 ext: &http::Extensions,
3632 ) -> Result<Self::Ok, ServerError> {
3633 let msg = Message::try_from(tx)?;
3634 let block_param = block_param.unwrap_or_else(|| Predefined::Latest.into());
3635 let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
3636 let ts = resolver
3637 .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
3638 .await?;
3639
3640 let TipsetState {
3641 state_root: pre_state_root,
3642 ..
3643 } = ctx
3644 .state_manager
3645 .load_tipset_state(&ts)
3646 .await
3647 .context("failed to get tipset state")?;
3648 let pre_state = StateTree::new_from_root(ctx.store_owned(), &pre_state_root)?;
3649
3650 let (invoke_result, post_state_root) = ctx
3651 .state_manager
3652 .apply_on_state_with_gas(Some(ts.shallow_clone()), msg.clone(), VMFlush::Flush)
3653 .await
3654 .context("failed to apply message")?;
3655 let post_state_root =
3656 post_state_root.context("post-execution state root required for trace call")?;
3657 let post_state = StateTree::new_from_root(ctx.store_owned(), &post_state_root)?;
3658
3659 let mut trace_results = EthTraceResults {
3660 output: get_trace_output(&msg, &invoke_result)?,
3661 ..Default::default()
3662 };
3663
3664 let touched_addresses = invoke_result
3666 .execution_trace
3667 .as_ref()
3668 .map(extract_touched_eth_addresses)
3669 .unwrap_or_default();
3670
3671 if trace_types.contains(&EthTraceType::Trace)
3673 && let Some(exec_trace) = invoke_result.execution_trace
3674 {
3675 let mut env = trace::base_environment(&post_state, &msg.from())
3676 .context("failed to create trace environment")?;
3677 trace::build_traces(&mut env, &[], exec_trace)?;
3678 trace_results.trace = env.traces;
3679 }
3680
3681 if trace_types.contains(&EthTraceType::StateDiff) {
3683 let mut all_touched = touched_addresses;
3685 if let Ok(caller_eth) = EthAddress::from_filecoin_address(&msg.from()) {
3686 all_touched.insert(caller_eth);
3687 }
3688 if let Ok(to_eth) = EthAddress::from_filecoin_address(&msg.to()) {
3689 all_touched.insert(to_eth);
3690 }
3691
3692 let state_diff =
3693 trace::build_state_diff(ctx.store(), &pre_state, &post_state, &all_touched)?;
3694 trace_results.state_diff = Some(state_diff);
3695 }
3696
3697 Ok(trace_results)
3698 }
3699}
3700
3701fn get_trace_output(msg: &Message, invoke_result: &ApiInvocResult) -> Result<EthBytes> {
3703 if msg.to() == FilecoinAddress::ETHEREUM_ACCOUNT_MANAGER_ACTOR {
3704 return Ok(EthBytes::default());
3705 }
3706
3707 let msg_rct = invoke_result
3708 .msg_rct
3709 .as_ref()
3710 .context("missing message receipt")?;
3711 let return_data = msg_rct.return_data();
3712
3713 if return_data.is_empty() {
3714 return Ok(EthBytes::default());
3715 }
3716
3717 decode_payload(&return_data, CBOR).context("failed to decode return data")
3718}
3719
3720fn extract_touched_eth_addresses(trace: &crate::rpc::state::ExecutionTrace) -> HashSet<EthAddress> {
3722 let mut addresses = HashSet::default();
3723 let mut stack = vec![trace];
3724
3725 while let Some(current) = stack.pop() {
3726 if let Ok(eth_addr) = EthAddress::from_filecoin_address(¤t.msg.from) {
3727 addresses.insert(eth_addr);
3728 }
3729 if let Ok(eth_addr) = EthAddress::from_filecoin_address(¤t.msg.to) {
3730 addresses.insert(eth_addr);
3731 }
3732 stack.extend(¤t.subcalls);
3733 }
3734
3735 addresses
3736}
3737
3738pub enum EthTraceTransaction {}
3739impl RpcMethod<1> for EthTraceTransaction {
3740 const NAME: &'static str = "Filecoin.EthTraceTransaction";
3741 const NAME_ALIAS: Option<&'static str> = Some("trace_transaction");
3742 const N_REQUIRED_PARAMS: usize = 1;
3743 const PARAM_NAMES: [&'static str; 1] = ["txHash"];
3744 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
3745 const PERMISSION: Permission = Permission::Read;
3746 const DESCRIPTION: Option<&'static str> =
3747 Some("Returns the traces for a specific transaction.");
3748
3749 type Params = (String,);
3750 type Ok = Vec<EthBlockTrace>;
3751 async fn handle(
3752 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
3753 (tx_hash,): Self::Params,
3754 ext: &http::Extensions,
3755 ) -> Result<Self::Ok, ServerError> {
3756 let eth_hash = EthHash::from_str(&tx_hash).context("invalid transaction hash")?;
3757 let eth_txn = get_eth_transaction_by_hash(&ctx, ð_hash, None)
3758 .await?
3759 .ok_or(ServerError::internal_error("transaction not found", None))?;
3760
3761 let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
3762 let ts = resolver
3763 .tipset_by_block_number_or_hash(eth_txn.block_number, ResolveNullTipset::TakeOlder)
3764 .await?;
3765
3766 let traces = eth_trace_block(&ctx, &ts, ext)
3767 .await?
3768 .into_iter()
3769 .filter(|trace| trace.transaction_hash == eth_hash)
3770 .collect();
3771 Ok(traces)
3772 }
3773}
3774
3775pub enum EthTraceReplayBlockTransactions {}
3776impl RpcMethod<2> for EthTraceReplayBlockTransactions {
3777 const N_REQUIRED_PARAMS: usize = 2;
3778 const NAME: &'static str = "Filecoin.EthTraceReplayBlockTransactions";
3779 const NAME_ALIAS: Option<&'static str> = Some("trace_replayBlockTransactions");
3780 const PARAM_NAMES: [&'static str; 2] = ["blockParam", "traceTypes"];
3781 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
3782 const PERMISSION: Permission = Permission::Read;
3783 const DESCRIPTION: Option<&'static str> = Some(
3784 "Replays all transactions in a block returning the requested traces for each transaction.",
3785 );
3786
3787 type Params = (BlockNumberOrHash, Vec<String>);
3788 type Ok = Vec<EthReplayBlockTransactionTrace>;
3789
3790 async fn handle(
3791 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
3792 (block_param, trace_types): Self::Params,
3793 ext: &http::Extensions,
3794 ) -> Result<Self::Ok, ServerError> {
3795 if trace_types.as_slice() != ["trace"] {
3796 return Err(ServerError::invalid_params(
3797 "only 'trace' is supported",
3798 None,
3799 ));
3800 }
3801
3802 let resolver = TipsetResolver::new(&ctx, Self::api_path(ext)?);
3803 let ts = resolver
3804 .tipset_by_block_number_or_hash(block_param, ResolveNullTipset::TakeOlder)
3805 .await?;
3806
3807 eth_trace_replay_block_transactions(&ctx, &ts, ext).await
3808 }
3809}
3810
3811async fn eth_trace_replay_block_transactions<DB>(
3812 ctx: &Ctx<DB>,
3813 ts: &Tipset,
3814 ext: &http::Extensions,
3815) -> Result<Vec<EthReplayBlockTransactionTrace>, ServerError>
3816where
3817 DB: Blockstore + Send + Sync + 'static,
3818{
3819 let (state, entries) = execute_tipset_traces(ctx, ts, ext).await?;
3820
3821 let mut all_traces = vec![];
3822 for entry in entries {
3823 let traces = entry.build_parity_traces(&state)?;
3824 all_traces.push(EthReplayBlockTransactionTrace {
3825 full_trace: EthTraceResults::from_parity_traces(traces),
3826 transaction_hash: entry.tx_hash,
3827 vm_trace: None,
3828 });
3829 }
3830
3831 Ok(all_traces)
3832}
3833
3834async fn get_eth_block_number_from_string<DB: Blockstore + Send + Sync + 'static>(
3835 ctx: &Ctx<DB>,
3836 block: Option<&str>,
3837 resolve: ResolveNullTipset,
3838 api_path: ApiPaths,
3839) -> Result<EthUint64> {
3840 let block_param = block
3841 .map(BlockNumberOrHash::from_str)
3842 .transpose()?
3843 .unwrap_or(BlockNumberOrHash::PredefinedBlock(Predefined::Latest));
3844 let resolver = TipsetResolver::new(ctx, api_path);
3845 Ok(EthUint64(
3846 resolver
3847 .tipset_by_block_number_or_hash(block_param, resolve)
3848 .await?
3849 .epoch() as u64,
3850 ))
3851}
3852
3853pub enum EthTraceFilter {}
3854impl RpcMethod<1> for EthTraceFilter {
3855 const N_REQUIRED_PARAMS: usize = 1;
3856 const NAME: &'static str = "Filecoin.EthTraceFilter";
3857 const NAME_ALIAS: Option<&'static str> = Some("trace_filter");
3858 const PARAM_NAMES: [&'static str; 1] = ["filter"];
3859 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
3860 const PERMISSION: Permission = Permission::Read;
3861 const DESCRIPTION: Option<&'static str> =
3862 Some("Returns the traces for transactions matching the filter criteria.");
3863 type Params = (EthTraceFilterCriteria,);
3864 type Ok = Vec<EthBlockTrace>;
3865
3866 async fn handle(
3867 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
3868 (filter,): Self::Params,
3869 ext: &http::Extensions,
3870 ) -> Result<Self::Ok, ServerError> {
3871 let api_path = Self::api_path(ext)?;
3872 let from_block = get_eth_block_number_from_string(
3873 &ctx,
3874 filter.from_block.as_deref(),
3875 ResolveNullTipset::TakeNewer,
3876 api_path,
3877 )
3878 .await
3879 .context("cannot parse fromBlock")?;
3880
3881 let to_block = get_eth_block_number_from_string(
3882 &ctx,
3883 filter.to_block.as_deref(),
3884 ResolveNullTipset::TakeOlder,
3885 api_path,
3886 )
3887 .await
3888 .context("cannot parse toBlock")?;
3889
3890 let max_block_range = ctx.eth_event_handler.max_filter_height_range;
3891 if max_block_range > 0 && to_block.0 > from_block.0 {
3892 let range = i64::try_from(to_block.0.saturating_sub(from_block.0))
3893 .context("block range overflow")?;
3894 if range > max_block_range {
3895 return Err(EthErrors::limit_exceeded(max_block_range, range).into());
3896 }
3897 }
3898 Ok(trace_filter(ctx, filter, from_block, to_block, ext).await?)
3899 }
3900}
3901
3902async fn trace_filter(
3903 ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
3904 filter: EthTraceFilterCriteria,
3905 from_block: EthUint64,
3906 to_block: EthUint64,
3907 ext: &http::Extensions,
3908) -> Result<Vec<EthBlockTrace>> {
3909 let mut results = HashSet::default();
3910 if let Some(EthUint64(0)) = filter.count {
3911 return Ok(Vec::new());
3912 }
3913 let count = *filter.count.unwrap_or_default();
3914 ensure!(
3915 count <= *FOREST_TRACE_FILTER_MAX_RESULT,
3916 "invalid response count, requested {}, maximum supported is {}",
3917 count,
3918 *FOREST_TRACE_FILTER_MAX_RESULT
3919 );
3920
3921 let mut trace_counter = 0;
3922 'blocks: for blk_num in from_block.0..=to_block.0 {
3923 let block_traces = EthTraceBlock::handle(
3925 ctx.clone(),
3926 (BlockNumberOrHash::from_block_number(blk_num as i64),),
3927 ext,
3928 )
3929 .await?;
3930 for block_trace in block_traces {
3931 if block_trace
3932 .trace
3933 .match_filter_criteria(filter.from_address.as_ref(), filter.to_address.as_ref())?
3934 {
3935 trace_counter += 1;
3936 if let Some(after) = filter.after
3937 && trace_counter <= after.0
3938 {
3939 continue;
3940 }
3941
3942 results.insert(block_trace);
3943
3944 if filter.count.is_some() && results.len() >= count as usize {
3945 break 'blocks;
3946 } else if results.len() > *FOREST_TRACE_FILTER_MAX_RESULT as usize {
3947 bail!(
3948 "too many results, maximum supported is {}, try paginating requests with After and Count",
3949 *FOREST_TRACE_FILTER_MAX_RESULT
3950 );
3951 }
3952 }
3953 }
3954 }
3955
3956 Ok(results
3957 .into_iter()
3958 .sorted_by(|a, b| a.sort_key().cmp(&b.sort_key()))
3959 .collect_vec())
3960}
3961
3962#[cfg(test)]
3963mod test {
3964 use super::*;
3965 use crate::rpc::eth::EventEntry;
3966 use crate::rpc::state::{ExecutionTrace, MessageTrace, ReturnTrace};
3967 use crate::shim::{econ::TokenAmount, error::ExitCode};
3968 use crate::{
3969 db::MemoryDB,
3970 test_utils::{construct_bls_messages, construct_eth_messages, construct_messages},
3971 };
3972 use fvm_shared4::event::Flags;
3973 use quickcheck::Arbitrary;
3974 use quickcheck_macros::quickcheck;
3975 use rstest::rstest;
3976
3977 impl Arbitrary for EthHash {
3978 fn arbitrary(g: &mut quickcheck::Gen) -> Self {
3979 let arr: [u8; 32] = std::array::from_fn(|_ix| u8::arbitrary(g));
3980 Self(ethereum_types::H256(arr))
3981 }
3982 }
3983
3984 #[quickcheck]
3985 fn gas_price_result_serde_roundtrip(i: u128) {
3986 let r = EthBigInt(i.into());
3987 let encoded = serde_json::to_string(&r).unwrap();
3988 assert_eq!(encoded, format!("\"{i:#x}\""));
3989 let decoded: EthBigInt = serde_json::from_str(&encoded).unwrap();
3990 assert_eq!(r.0, decoded.0);
3991 }
3992
3993 #[test]
3994 fn test_abi_encoding() {
3995 const EXPECTED: &str = "000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001b1111111111111111111020200301000000044444444444444444010000000000";
3996 const DATA: &str = "111111111111111111102020030100000004444444444444444401";
3997 let expected_bytes = hex::decode(EXPECTED).unwrap();
3998 let data_bytes = hex::decode(DATA).unwrap();
3999
4000 assert_eq!(expected_bytes, encode_as_abi_helper(22, 81, &data_bytes));
4001 }
4002
4003 #[test]
4004 fn test_abi_encoding_empty_bytes() {
4005 const EXPECTED: &str = "0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000";
4007 let expected_bytes = hex::decode(EXPECTED).unwrap();
4008 let data_bytes = vec![];
4009
4010 assert_eq!(expected_bytes, encode_as_abi_helper(22, 81, &data_bytes));
4011 }
4012
4013 #[test]
4014 fn test_abi_encoding_one_byte() {
4015 const EXPECTED: &str = "0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001fd00000000000000000000000000000000000000000000000000000000000000";
4019 let expected_bytes = hex::decode(EXPECTED).unwrap();
4020 let data_bytes = vec![253];
4021
4022 assert_eq!(expected_bytes, encode_as_abi_helper(22, 81, &data_bytes));
4023 }
4024
4025 #[test]
4026 fn test_id_address_roundtrip() {
4027 let test_cases = [1u64, 2, 3, 100, 101];
4028
4029 for id in test_cases {
4030 let addr = FilecoinAddress::new_id(id);
4031
4032 let eth_addr = EthAddress::from_filecoin_address(&addr).unwrap();
4034 let fil_addr = eth_addr.to_filecoin_address().unwrap();
4035 assert_eq!(addr, fil_addr)
4036 }
4037 }
4038
4039 #[test]
4040 fn test_addr_serde_roundtrip() {
4041 let test_cases = [
4042 r#""0xd4c5fb16488Aa48081296299d54b0c648C9333dA""#,
4043 r#""0x2C2EC67e3e1FeA8e4A39601cB3A3Cd44f5fa830d""#,
4044 r#""0x01184F793982104363F9a8a5845743f452dE0586""#,
4045 ];
4046
4047 for addr in test_cases {
4048 let eth_addr: EthAddress = serde_json::from_str(addr).unwrap();
4049
4050 let encoded = serde_json::to_string(ð_addr).unwrap();
4051 assert_eq!(encoded, addr.to_lowercase());
4052
4053 let decoded: EthAddress = serde_json::from_str(&encoded).unwrap();
4054 assert_eq!(eth_addr, decoded);
4055 }
4056 }
4057
4058 #[quickcheck]
4059 fn test_fil_address_roundtrip(addr: FilecoinAddress) {
4060 if let Ok(eth_addr) = EthAddress::from_filecoin_address(&addr) {
4061 let fil_addr = eth_addr.to_filecoin_address().unwrap();
4062
4063 let protocol = addr.protocol();
4064 assert!(protocol == Protocol::ID || protocol == Protocol::Delegated);
4065 assert_eq!(addr, fil_addr);
4066 }
4067 }
4068
4069 #[rstest]
4070 #[case("\"0x013dbb9442ca9667baccc6230fcd5c1c4b2d4d2870f4bd20681d4d47cfd15184\"")]
4071 #[case("\"0xab8653edf9f51785664a643b47605a7ba3d917b5339a0724e7642c114d0e4738\"")]
4072 fn test_hash_serde_json(#[case] hash: &str) {
4073 let h: EthHash = serde_json::from_str(hash).unwrap();
4074 let c = h.to_cid();
4075 let h1: EthHash = c.into();
4076 assert_eq!(h, h1);
4077 }
4078
4079 #[quickcheck]
4080 fn test_eth_hash_roundtrip(eth_hash: EthHash) {
4081 let cid = eth_hash.to_cid();
4082 let hash = cid.into();
4083 assert_eq!(eth_hash, hash);
4084 }
4085
4086 #[test]
4087 fn test_block_constructor() {
4088 let block = Block::new(false, 1);
4089 assert_eq!(block.transactions_root, EthHash::empty_root());
4090
4091 let block = Block::new(true, 1);
4092 assert_eq!(block.transactions_root, EthHash::default());
4093 }
4094
4095 #[test]
4096 fn test_eth_tx_hash_from_signed_message() {
4097 let (_, signed) = construct_eth_messages(0);
4098 let tx_hash =
4099 eth_tx_hash_from_signed_message(&signed, crate::networks::calibnet::ETH_CHAIN_ID)
4100 .unwrap();
4101 assert_eq!(
4102 &format!("{tx_hash}"),
4103 "0xfc81dd8d9ffb045e7e2d494f925824098183263c7f402d69e18cc25e3422791b"
4104 );
4105
4106 let (_, signed) = construct_messages();
4107 let tx_hash =
4108 eth_tx_hash_from_signed_message(&signed, crate::networks::calibnet::ETH_CHAIN_ID)
4109 .unwrap();
4110 assert_eq!(tx_hash.to_cid(), signed.cid());
4111
4112 let (_, signed) = construct_bls_messages();
4113 let tx_hash =
4114 eth_tx_hash_from_signed_message(&signed, crate::networks::calibnet::ETH_CHAIN_ID)
4115 .unwrap();
4116 assert_eq!(tx_hash.to_cid(), signed.message().cid());
4117 }
4118
4119 #[test]
4120 fn test_eth_tx_hash_from_message_cid() {
4121 let blockstore = Arc::new(MemoryDB::default());
4122
4123 let (msg0, secp0) = construct_eth_messages(0);
4124 let (_msg1, secp1) = construct_eth_messages(1);
4125 let (msg2, bls0) = construct_bls_messages();
4126
4127 crate::chain::persist_objects(&blockstore, [msg0.clone(), msg2.clone()].iter()).unwrap();
4128 crate::chain::persist_objects(&blockstore, [secp0.clone(), bls0.clone()].iter()).unwrap();
4129
4130 let tx_hash = eth_tx_hash_from_message_cid(&blockstore, &secp0.cid(), 0).unwrap();
4131 assert!(tx_hash.is_some());
4132
4133 let tx_hash = eth_tx_hash_from_message_cid(&blockstore, &msg2.cid(), 0).unwrap();
4134 assert!(tx_hash.is_some());
4135
4136 let tx_hash = eth_tx_hash_from_message_cid(&blockstore, &secp1.cid(), 0).unwrap();
4137 assert!(tx_hash.is_none());
4138 }
4139
4140 #[test]
4141 fn test_eth_log_from_event() {
4142 let entries = vec![
4146 EventEntry {
4147 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4148 key: "t1".into(),
4149 codec: IPLD_RAW,
4150 value: vec![
4151 226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4152 81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4153 ]
4154 .into(),
4155 },
4156 EventEntry {
4157 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4158 key: "t2".into(),
4159 codec: IPLD_RAW,
4160 value: vec![
4161 116, 4, 227, 209, 4, 234, 120, 65, 195, 217, 230, 253, 32, 173, 254, 153, 180,
4162 173, 88, 107, 192, 141, 143, 59, 211, 175, 239, 137, 76, 241, 132, 222,
4163 ]
4164 .into(),
4165 },
4166 ];
4167 let (bytes, hashes) = eth_log_from_event(&entries).unwrap();
4168 assert!(bytes.0.is_empty());
4169 assert_eq!(hashes.len(), 2);
4170
4171 let entries = vec![
4172 EventEntry {
4173 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4174 key: "t1".into(),
4175 codec: IPLD_RAW,
4176 value: vec![
4177 226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4178 81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4179 ]
4180 .into(),
4181 },
4182 EventEntry {
4183 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4184 key: "t2".into(),
4185 codec: IPLD_RAW,
4186 value: vec![
4187 116, 4, 227, 209, 4, 234, 120, 65, 195, 217, 230, 253, 32, 173, 254, 153, 180,
4188 173, 88, 107, 192, 141, 143, 59, 211, 175, 239, 137, 76, 241, 132, 222,
4189 ]
4190 .into(),
4191 },
4192 EventEntry {
4193 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4194 key: "t3".into(),
4195 codec: IPLD_RAW,
4196 value: vec![
4197 226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4198 81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4199 ]
4200 .into(),
4201 },
4202 EventEntry {
4203 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4204 key: "t4".into(),
4205 codec: IPLD_RAW,
4206 value: vec![
4207 116, 4, 227, 209, 4, 234, 120, 65, 195, 217, 230, 253, 32, 173, 254, 153, 180,
4208 173, 88, 107, 192, 141, 143, 59, 211, 175, 239, 137, 76, 241, 132, 222,
4209 ]
4210 .into(),
4211 },
4212 ];
4213 let (bytes, hashes) = eth_log_from_event(&entries).unwrap();
4214 assert!(bytes.0.is_empty());
4215 assert_eq!(hashes.len(), 4);
4216
4217 let entries = vec![
4218 EventEntry {
4219 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4220 key: "t1".into(),
4221 codec: IPLD_RAW,
4222 value: vec![
4223 226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4224 81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4225 ]
4226 .into(),
4227 },
4228 EventEntry {
4229 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4230 key: "t1".into(),
4231 codec: IPLD_RAW,
4232 value: vec![
4233 116, 4, 227, 209, 4, 234, 120, 65, 195, 217, 230, 253, 32, 173, 254, 153, 180,
4234 173, 88, 107, 192, 141, 143, 59, 211, 175, 239, 137, 76, 241, 132, 222,
4235 ]
4236 .into(),
4237 },
4238 ];
4239 assert!(eth_log_from_event(&entries).is_none());
4240
4241 let entries = vec![
4242 EventEntry {
4243 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4244 key: "t3".into(),
4245 codec: IPLD_RAW,
4246 value: vec![
4247 226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4248 81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4249 ]
4250 .into(),
4251 },
4252 EventEntry {
4253 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4254 key: "t4".into(),
4255 codec: IPLD_RAW,
4256 value: vec![
4257 116, 4, 227, 209, 4, 234, 120, 65, 195, 217, 230, 253, 32, 173, 254, 153, 180,
4258 173, 88, 107, 192, 141, 143, 59, 211, 175, 239, 137, 76, 241, 132, 222,
4259 ]
4260 .into(),
4261 },
4262 EventEntry {
4263 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4264 key: "t1".into(),
4265 codec: IPLD_RAW,
4266 value: vec![
4267 226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4268 81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4269 ]
4270 .into(),
4271 },
4272 EventEntry {
4273 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4274 key: "t2".into(),
4275 codec: IPLD_RAW,
4276 value: vec![
4277 116, 4, 227, 209, 4, 234, 120, 65, 195, 217, 230, 253, 32, 173, 254, 153, 180,
4278 173, 88, 107, 192, 141, 143, 59, 211, 175, 239, 137, 76, 241, 132, 222,
4279 ]
4280 .into(),
4281 },
4282 ];
4283 let (bytes, hashes) = eth_log_from_event(&entries).unwrap();
4284 assert!(bytes.0.is_empty());
4285 assert_eq!(hashes.len(), 4);
4286
4287 let entries = vec![
4288 EventEntry {
4289 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4290 key: "t1".into(),
4291 codec: IPLD_RAW,
4292 value: vec![
4293 226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4294 81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4295 ]
4296 .into(),
4297 },
4298 EventEntry {
4299 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4300 key: "t3".into(),
4301 codec: IPLD_RAW,
4302 value: vec![
4303 116, 4, 227, 209, 4, 234, 120, 65, 195, 217, 230, 253, 32, 173, 254, 153, 180,
4304 173, 88, 107, 192, 141, 143, 59, 211, 175, 239, 137, 76, 241, 132, 222,
4305 ]
4306 .into(),
4307 },
4308 ];
4309 assert!(eth_log_from_event(&entries).is_none());
4310
4311 let entries = vec![EventEntry {
4312 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4313 key: "t1".into(),
4314 codec: DAG_CBOR,
4315 value: vec![
4316 226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11, 81,
4317 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4318 ]
4319 .into(),
4320 }];
4321 assert!(eth_log_from_event(&entries).is_none());
4322
4323 let entries = vec![EventEntry {
4324 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4325 key: "t1".into(),
4326 codec: IPLD_RAW,
4327 value: vec![
4328 226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11, 81,
4329 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149, 0,
4330 ]
4331 .into(),
4332 }];
4333 assert!(eth_log_from_event(&entries).is_none());
4334
4335 let entries = vec![
4336 EventEntry {
4337 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4338 key: "t1".into(),
4339 codec: IPLD_RAW,
4340 value: vec![
4341 226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4342 81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149,
4343 ]
4344 .into(),
4345 },
4346 EventEntry {
4347 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4348 key: "d".into(),
4349 codec: IPLD_RAW,
4350 value: vec![
4351 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,
4352 25, 34, 116, 232, 27, 26, 248,
4353 ]
4354 .into(),
4355 },
4356 ];
4357 let (bytes, hashes) = eth_log_from_event(&entries).unwrap();
4358 assert_eq!(bytes.0.len(), 32);
4359 assert_eq!(hashes.len(), 1);
4360
4361 let entries = vec![
4362 EventEntry {
4363 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4364 key: "t1".into(),
4365 codec: IPLD_RAW,
4366 value: vec![
4367 226, 71, 32, 244, 92, 183, 79, 45, 85, 241, 222, 235, 182, 9, 143, 80, 241, 11,
4368 81, 29, 171, 138, 125, 71, 196, 129, 154, 8, 220, 208, 184, 149, 0,
4369 ]
4370 .into(),
4371 },
4372 EventEntry {
4373 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4374 key: "d".into(),
4375 codec: IPLD_RAW,
4376 value: vec![
4377 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,
4378 25, 34, 116, 232, 27, 26, 248,
4379 ]
4380 .into(),
4381 },
4382 EventEntry {
4383 flags: (Flags::FLAG_INDEXED_ALL).bits(),
4384 key: "d".into(),
4385 codec: IPLD_RAW,
4386 value: vec![
4387 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,
4388 25, 34, 116, 232, 27, 26, 248,
4389 ]
4390 .into(),
4391 },
4392 ];
4393 assert!(eth_log_from_event(&entries).is_none());
4394 }
4395
4396 #[test]
4397 fn test_from_bytes_valid() {
4398 let zero_bytes = [0u8; 32];
4399 assert_eq!(
4400 EthUint64::from_bytes(&zero_bytes).unwrap().0,
4401 0,
4402 "zero bytes"
4403 );
4404
4405 let mut value_bytes = [0u8; 32];
4406 value_bytes[31] = 42;
4407 assert_eq!(
4408 EthUint64::from_bytes(&value_bytes).unwrap().0,
4409 42,
4410 "simple value"
4411 );
4412
4413 let mut max_bytes = [0u8; 32];
4414 max_bytes[24..32].copy_from_slice(&u64::MAX.to_be_bytes());
4415 assert_eq!(
4416 EthUint64::from_bytes(&max_bytes).unwrap().0,
4417 u64::MAX,
4418 "valid max value"
4419 );
4420 }
4421
4422 #[test]
4423 fn test_from_bytes_wrong_length() {
4424 let short_bytes = [0u8; 31];
4425 assert!(
4426 EthUint64::from_bytes(&short_bytes).is_err(),
4427 "bytes too short"
4428 );
4429
4430 let long_bytes = [0u8; 33];
4431 assert!(
4432 EthUint64::from_bytes(&long_bytes).is_err(),
4433 "bytes too long"
4434 );
4435
4436 let empty_bytes = [];
4437 assert!(
4438 EthUint64::from_bytes(&empty_bytes).is_err(),
4439 "bytes too short"
4440 );
4441 }
4442
4443 #[test]
4444 fn test_from_bytes_overflow() {
4445 let mut overflow_bytes = [0u8; 32];
4446 overflow_bytes[10] = 1;
4447 assert!(
4448 EthUint64::from_bytes(&overflow_bytes).is_err(),
4449 "overflow with non-zero byte at position 10"
4450 );
4451
4452 overflow_bytes = [0u8; 32];
4453 overflow_bytes[23] = 1;
4454 assert!(
4455 EthUint64::from_bytes(&overflow_bytes).is_err(),
4456 "overflow with non-zero byte at position 23"
4457 );
4458
4459 overflow_bytes = [0u8; 32];
4460 overflow_bytes
4461 .iter_mut()
4462 .take(24)
4463 .for_each(|byte| *byte = 0xFF);
4464
4465 assert!(
4466 EthUint64::from_bytes(&overflow_bytes).is_err(),
4467 "overflow bytes with non-zero bytes at positions 0-23"
4468 );
4469
4470 overflow_bytes = [0u8; 32];
4471 for i in 0..24 {
4472 overflow_bytes[i] = 0xFF;
4473 assert!(
4474 EthUint64::from_bytes(&overflow_bytes).is_err(),
4475 "overflow with non-zero byte at position {i}"
4476 );
4477 }
4478
4479 overflow_bytes = [0xFF; 32];
4480 assert!(
4481 EthUint64::from_bytes(&overflow_bytes).is_err(),
4482 "overflow with all ones"
4483 );
4484 }
4485
4486 fn create_execution_trace(from: FilecoinAddress, to: FilecoinAddress) -> ExecutionTrace {
4487 ExecutionTrace {
4488 msg: MessageTrace {
4489 from,
4490 to,
4491 value: TokenAmount::default(),
4492 method: 0,
4493 params: Default::default(),
4494 params_codec: 0,
4495 gas_limit: None,
4496 read_only: None,
4497 },
4498 msg_rct: ReturnTrace {
4499 exit_code: ExitCode::from(0u32),
4500 r#return: Default::default(),
4501 return_codec: 0,
4502 },
4503 invoked_actor: None,
4504 gas_charges: vec![],
4505 subcalls: vec![],
4506 }
4507 }
4508
4509 fn create_execution_trace_with_subcalls(
4510 from: FilecoinAddress,
4511 to: FilecoinAddress,
4512 subcalls: Vec<ExecutionTrace>,
4513 ) -> ExecutionTrace {
4514 let mut trace = create_execution_trace(from, to);
4515 trace.subcalls = subcalls;
4516 trace
4517 }
4518
4519 #[test]
4520 fn test_extract_touched_addresses_with_id_addresses() {
4521 let from = FilecoinAddress::new_id(100);
4523 let to = FilecoinAddress::new_id(200);
4524 let trace = create_execution_trace(from, to);
4525
4526 let addresses = extract_touched_eth_addresses(&trace);
4527
4528 assert_eq!(addresses.len(), 2);
4529 assert!(addresses.contains(&EthAddress::from_filecoin_address(&from).unwrap()));
4530 assert!(addresses.contains(&EthAddress::from_filecoin_address(&to).unwrap()));
4531 }
4532
4533 #[test]
4534 fn test_extract_touched_addresses_same_from_and_to() {
4535 let addr = FilecoinAddress::new_id(100);
4536 let trace = create_execution_trace(addr, addr);
4537
4538 let addresses = extract_touched_eth_addresses(&trace);
4539
4540 assert_eq!(addresses.len(), 1);
4542 assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr).unwrap()));
4543 }
4544
4545 #[test]
4546 fn test_extract_touched_addresses_with_subcalls() {
4547 let addr1 = FilecoinAddress::new_id(100);
4548 let addr2 = FilecoinAddress::new_id(200);
4549 let addr3 = FilecoinAddress::new_id(300);
4550 let addr4 = FilecoinAddress::new_id(400);
4551
4552 let subcall = create_execution_trace(addr3, addr4);
4553 let trace = create_execution_trace_with_subcalls(addr1, addr2, vec![subcall]);
4554
4555 let addresses = extract_touched_eth_addresses(&trace);
4556
4557 assert_eq!(addresses.len(), 4);
4558 assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr1).unwrap()));
4559 assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr2).unwrap()));
4560 assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr3).unwrap()));
4561 assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr4).unwrap()));
4562 }
4563
4564 #[test]
4565 fn test_extract_touched_addresses_with_nested_subcalls() {
4566 let addr1 = FilecoinAddress::new_id(100);
4567 let addr2 = FilecoinAddress::new_id(200);
4568 let addr3 = FilecoinAddress::new_id(300);
4569 let addr4 = FilecoinAddress::new_id(400);
4570 let addr5 = FilecoinAddress::new_id(500);
4571 let addr6 = FilecoinAddress::new_id(600);
4572
4573 let nested_subcall = create_execution_trace(addr5, addr6);
4575 let subcall = create_execution_trace_with_subcalls(addr3, addr4, vec![nested_subcall]);
4576 let trace = create_execution_trace_with_subcalls(addr1, addr2, vec![subcall]);
4577
4578 let addresses = extract_touched_eth_addresses(&trace);
4579
4580 assert_eq!(addresses.len(), 6);
4581 for addr in [addr1, addr2, addr3, addr4, addr5, addr6] {
4582 assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr).unwrap()));
4583 }
4584 }
4585
4586 #[test]
4587 fn test_extract_touched_addresses_with_multiple_subcalls() {
4588 let addr1 = FilecoinAddress::new_id(100);
4589 let addr2 = FilecoinAddress::new_id(200);
4590 let addr3 = FilecoinAddress::new_id(300);
4591 let addr4 = FilecoinAddress::new_id(400);
4592 let addr5 = FilecoinAddress::new_id(500);
4593 let addr6 = FilecoinAddress::new_id(600);
4594
4595 let subcall1 = create_execution_trace(addr3, addr4);
4596 let subcall2 = create_execution_trace(addr5, addr6);
4597 let trace = create_execution_trace_with_subcalls(addr1, addr2, vec![subcall1, subcall2]);
4598
4599 let addresses = extract_touched_eth_addresses(&trace);
4600
4601 assert_eq!(addresses.len(), 6);
4602 }
4603
4604 #[test]
4605 fn test_extract_touched_addresses_deduplicates_across_subcalls() {
4606 let addr1 = FilecoinAddress::new_id(100);
4608 let addr2 = FilecoinAddress::new_id(200);
4609
4610 let subcall = create_execution_trace(addr1, addr2); let trace = create_execution_trace_with_subcalls(addr1, addr2, vec![subcall]);
4612
4613 let addresses = extract_touched_eth_addresses(&trace);
4614
4615 assert_eq!(addresses.len(), 2);
4617 }
4618
4619 #[test]
4620 fn test_extract_touched_addresses_with_non_convertible_addresses() {
4621 let bls_addr = FilecoinAddress::new_bls(&[0u8; 48]).unwrap();
4623 let id_addr = FilecoinAddress::new_id(100);
4624
4625 let trace = create_execution_trace(bls_addr, id_addr);
4626 let addresses = extract_touched_eth_addresses(&trace);
4627
4628 assert_eq!(addresses.len(), 1);
4630 assert!(addresses.contains(&EthAddress::from_filecoin_address(&id_addr).unwrap()));
4631 }
4632}