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