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