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