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