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