From 25e8f63c3673360775b08f5a051696bd35f8c6a6 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 5 Feb 2026 14:08:55 +0530 Subject: [PATCH 1/6] return default(0) when actor not found in eth API --- src/rpc/methods/eth.rs | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 556c796b75cd..086e8412af42 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -974,7 +974,7 @@ impl RpcMethod<2> for EthGetBalance { block_param, ResolveNullTipset::TakeOlder, )?; - let balance = eth_get_balance(&ctx, &address, &ts)?; + let balance = eth_get_balance(&ctx, &address, &ts).await?; Ok(balance) } } @@ -998,20 +998,26 @@ impl RpcMethod<2> for EthGetBalanceV2 { ) -> Result { let ts = tipset_by_block_number_or_hash_v2(&ctx, block_param, ResolveNullTipset::TakeOlder) .await?; - let balance = eth_get_balance(&ctx, &address, &ts)?; + let balance = eth_get_balance(&ctx, &address, &ts).await?; Ok(balance) } } -fn eth_get_balance( +async fn eth_get_balance( ctx: &Ctx, address: &EthAddress, ts: &Tipset, ) -> Result { let fil_addr = address.to_filecoin_address()?; - let state = StateTree::new_from_root(ctx.store_owned(), ts.parent_state())?; - let actor = state.get_required_actor(&fil_addr)?; - Ok(EthBigInt(actor.balance.atto().clone())) + let (state_cid, _) = ctx + .state_manager + .tipset_state(ts, StateLookupPolicy::Enabled) + .await?; + let state = ctx.state_manager.get_state_tree(&state_cid)?; + match state.get_actor(&fil_addr)? { + Some(actor) => Ok(EthBigInt(actor.balance.atto().clone())), + None => Ok(EthBigInt::default()), + } } fn get_tipset_from_hash( @@ -2483,7 +2489,14 @@ where .state_manager .tipset_state(ts, StateLookupPolicy::Enabled) .await?; - let actor = ctx.state_manager.get_required_actor(&to_address, state)?; + let state_tree = ctx.state_manager.get_state_tree(&state)?; + let Some(actor) = state_tree + .get_actor(&to_address) + .with_context(|| format!("failed to lookup contract {}", eth_address.0))? + else { + return Ok(Default::default()); + }; + // Not a contract. We could try to distinguish between accounts and "native" contracts here, // but it's not worth it. if !is_evm_actor(&actor.code) { @@ -2599,7 +2612,11 @@ async fn get_storage_at( .tipset_state(&ts, StateLookupPolicy::Enabled) .await?; let make_empty_result = || EthBytes(vec![0; EVM_WORD_LENGTH]); - let Some(actor) = ctx.state_manager.get_actor(&to_address, state)? else { + let Some(actor) = ctx + .state_manager + .get_actor(&to_address, state) + .with_context(|| format!("failed to lookup contract {}", eth_address.0))? + else { return Ok(make_empty_result()); }; @@ -2726,8 +2743,12 @@ where .tipset_state(ts, StateLookupPolicy::Enabled) .await?; - let state = StateTree::new_from_root(ctx.store_owned(), &state_cid)?; - let actor = state.get_required_actor(&addr)?; + let state = ctx.state_manager.get_state_tree(&state_cid)?; + let actor = match state.get_actor(&addr)? { + Some(actor) => actor, + None => return Ok(EthUint64(0)), + }; + if is_evm_actor(&actor.code) { let evm_state = evm::State::load(ctx.store(), actor.code, actor.state)?; if !evm_state.is_alive() { From a2d121840be90bece646abc6d6788431e7af2432 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 5 Feb 2026 14:30:54 +0530 Subject: [PATCH 2/6] use state manager in eth api instead of state tree --- src/rpc/methods/eth.rs | 24 ++++++++++++------------ src/shim/state_tree.rs | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 086e8412af42..de3ffbd9487d 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -586,7 +586,7 @@ impl Block { let (state_root, msgs_and_receipts) = execute_tipset(&ctx, &tipset).await?; - let state_tree = StateTree::new_from_root(ctx.store_owned(), &state_root)?; + let state_tree = ctx.state_manager.get_state_tree(&state_root)?; let mut full_transactions = vec![]; let mut gas_used = 0; @@ -1013,10 +1013,10 @@ async fn eth_get_balance( .state_manager .tipset_state(ts, StateLookupPolicy::Enabled) .await?; - let state = ctx.state_manager.get_state_tree(&state_cid)?; - match state.get_actor(&fil_addr)? { + let state_tree = ctx.state_manager.get_state_tree(&state_cid)?; + match state_tree.get_actor(&fil_addr)? { Some(actor) => Ok(EthBigInt(actor.balance.atto().clone())), - None => Ok(EthBigInt::default()), + None => Ok(EthBigInt::default()), // Balance is 0 if the actor doesn't exist } } @@ -1445,7 +1445,7 @@ fn new_eth_tx_from_message_lookup( let smsg = get_signed_message(ctx, message_lookup.message)?; - let state = StateTree::new_from_root(ctx.store().into(), ts.parent_state())?; + let state = ctx.state_manager.get_state_tree(&ts.parent_state())?; Ok(ApiEthTx { block_hash: parent_ts_cid.into(), @@ -1700,7 +1700,7 @@ async fn get_block_receipts( let (state_root, msgs_and_receipts) = execute_tipset(ctx, &ts_ref).await?; // Load the state tree - let state_tree = StateTree::new_from_root(ctx.store_owned(), &state_root)?; + let state_tree = ctx.state_manager.get_state_tree(&state_root)?; let mut eth_receipts = Vec::with_capacity(msgs_and_receipts.len()); for (i, (msg, receipt)) in msgs_and_receipts.into_iter().enumerate() { @@ -2743,8 +2743,8 @@ where .tipset_state(ts, StateLookupPolicy::Enabled) .await?; - let state = ctx.state_manager.get_state_tree(&state_cid)?; - let actor = match state.get_actor(&addr)? { + let state_tree = ctx.state_manager.get_state_tree(&state_cid)?; + let actor = match state_tree.get_actor(&addr)? { Some(actor) => actor, None => return Ok(EthUint64(0)), }; @@ -2876,7 +2876,7 @@ where ) })?; - let state = StateTree::new_from_root(ctx.store_owned(), ts.parent_state())?; + let state = ctx.state_manager.get_state_tree(ts.parent_state())?; let tx = new_eth_tx(ctx, &state, ts.epoch(), &ts.key().cid()?, &msg.cid(), index)?; @@ -2911,7 +2911,7 @@ impl RpcMethod<2> for EthGetTransactionByBlockHashAndIndex { ) })?; - let state = StateTree::new_from_root(ctx.store_owned(), ts.parent_state())?; + let state = ctx.state_manager.get_state_tree(ts.parent_state())?; let tx = new_eth_tx( &ctx, @@ -3893,7 +3893,7 @@ where DB: Blockstore + Send + Sync + 'static, { let (state_root, trace) = ctx.state_manager.execution_trace(ts)?; - let state = StateTree::new_from_root(ctx.store_owned(), &state_root)?; + let state = ctx.state_manager.get_state_tree(&state_root)?; let cid = ts.key().cid()?; let block_hash: EthHash = cid.into(); let mut all_traces = vec![]; @@ -4047,7 +4047,7 @@ where { let (state_root, trace) = ctx.state_manager.execution_trace(ts)?; - let state = StateTree::new_from_root(ctx.store_owned(), &state_root)?; + let state = ctx.state_manager.get_state_tree(&state_root)?; let mut all_traces = vec![]; for ir in trace.into_iter() { diff --git a/src/shim/state_tree.rs b/src/shim/state_tree.rs index 5b31dfa203af..49067dceaae5 100644 --- a/src/shim/state_tree.rs +++ b/src/shim/state_tree.rs @@ -191,7 +191,7 @@ where /// Get required actor state from an address. Will be resolved to ID address. pub fn get_required_actor(&self, addr: &Address) -> anyhow::Result { self.get_actor(addr)? - .with_context(|| format!("Actor not found: addr={addr}")) + .with_context(|| format!("Actor not found: addr {addr}")) } /// Get the actor bundle metadata From 170d077def488be3769fea521dc338a427b75cf1 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 5 Feb 2026 14:34:38 +0530 Subject: [PATCH 3/6] fix linter issue --- src/rpc/methods/eth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index de3ffbd9487d..46e9a588afc3 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -1445,7 +1445,7 @@ fn new_eth_tx_from_message_lookup( let smsg = get_signed_message(ctx, message_lookup.message)?; - let state = ctx.state_manager.get_state_tree(&ts.parent_state())?; + let state = ctx.state_manager.get_state_tree(ts.parent_state())?; Ok(ApiEthTx { block_hash: parent_ts_cid.into(), From f43a1496e6cdd98665812b55fbf9300c913f3d46 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 5 Feb 2026 17:00:24 +0530 Subject: [PATCH 4/6] add snashot tests for the fixes --- src/shim/state_tree.rs | 2 +- .../subcommands/api_cmd/api_compare_tests.rs | 72 ++++++++++++++++++- .../subcommands/api_cmd/test_snapshots.txt | 12 +++- 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/src/shim/state_tree.rs b/src/shim/state_tree.rs index 49067dceaae5..5b31dfa203af 100644 --- a/src/shim/state_tree.rs +++ b/src/shim/state_tree.rs @@ -191,7 +191,7 @@ where /// Get required actor state from an address. Will be resolved to ID address. pub fn get_required_actor(&self, addr: &Address) -> anyhow::Result { self.get_actor(addr)? - .with_context(|| format!("Actor not found: addr {addr}")) + .with_context(|| format!("Actor not found: addr={addr}")) } /// Get the actor bundle metadata diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index 65ad68986ba9..afe71fc6c681 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -52,6 +52,7 @@ use ipld_core::ipld::Ipld; use itertools::Itertools as _; use jsonrpsee::types::ErrorCode; use libp2p::PeerId; +use libsecp256k1::{PublicKey, SecretKey}; use num_traits::Signed; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -123,6 +124,13 @@ static KNOWN_CALIBNET_F4_ADDRESS: LazyLock
= LazyLock::new(|| { .into() }); +fn generate_eth_random_address() -> anyhow::Result { + let rng = &mut crate::utils::rand::forest_os_rng(); + let secret_key = SecretKey::random(rng); + let public_key = PublicKey::from_secret_key(&secret_key); + EthAddress::eth_address_from_pub_key(&public_key.serialize()) +} + const TICKET_QUALITY_GREEDY: f64 = 0.9; const TICKET_QUALITY_OPTIMAL: f64 = 0.8; const ZERO_ADDRESS: &str = "0x0000000000000000000000000000000000000000"; @@ -1642,6 +1650,13 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset )) .unwrap(), ), + RpcTest::identity( + EthGetBalance::request(( + generate_eth_random_address().unwrap(), + BlockNumberOrHash::from_predefined(Predefined::Latest), + )) + .unwrap(), + ), RpcTest::identity( EthGetBalanceV2::request(( EthAddress::from_str("0xff38c072f286e3b20b3954ca9f99c05fbecc64aa").unwrap(), @@ -1713,6 +1728,13 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset )) .unwrap(), ), + RpcTest::identity( + EthGetBalanceV2::request(( + generate_eth_random_address().unwrap(), + ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest), + )) + .unwrap(), + ), RpcTest::identity( EthGetBlockByNumber::request(( BlockNumberOrPredefined::BlockNumber(EthInt64(shared_tipset.epoch())), @@ -1930,6 +1952,13 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset )) .unwrap(), ), + RpcTest::identity( + EthGetTransactionCount::request(( + generate_eth_random_address().unwrap(), + BlockNumberOrHash::from_predefined(Predefined::Latest), + )) + .unwrap(), + ), RpcTest::identity( EthGetTransactionCountV2::request(( EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(), @@ -1973,6 +2002,13 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset )) .unwrap(), ), + RpcTest::identity( + EthGetTransactionCountV2::request(( + generate_eth_random_address().unwrap(), + ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest), + )) + .unwrap(), + ), RpcTest::identity( EthGetStorageAt::request(( // https://filfox.info/en/address/f410fpoidg73f7krlfohnla52dotowde5p2sejxnd4mq @@ -2007,6 +2043,14 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset )) .unwrap(), ), + RpcTest::identity( + EthGetStorageAt::request(( + generate_eth_random_address().unwrap(), + EthBytes(vec![0x0]), + BlockNumberOrHash::from_predefined(Predefined::Latest), + )) + .unwrap(), + ), RpcTest::identity( EthGetStorageAtV2::request(( EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(), @@ -2031,6 +2075,14 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset )) .unwrap(), ), + RpcTest::identity( + EthGetStorageAtV2::request(( + generate_eth_random_address().unwrap(), + EthBytes(vec![0x0]), + ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest), + )) + .unwrap(), + ), RpcTest::identity( EthFeeHistory::request(( 10.into(), @@ -2186,6 +2238,13 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset )) .unwrap(), ), + RpcTest::identity( + EthGetCode::request(( + generate_eth_random_address().unwrap(), + BlockNumberOrHash::from_predefined(Predefined::Latest), + )) + .unwrap(), + ), RpcTest::identity( EthGetCodeV2::request(( // https://filfox.info/en/address/f410fpoidg73f7krlfohnla52dotowde5p2sejxnd4mq @@ -2208,6 +2267,13 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset )) .unwrap(), ), + RpcTest::identity( + EthGetCodeV2::request(( + generate_eth_random_address().unwrap(), + ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest), + )) + .unwrap(), + ), RpcTest::identity( EthGetTransactionByBlockNumberAndIndex::request(( BlockNumberOrPredefined::BlockNumber(shared_tipset.epoch().into()), @@ -2561,9 +2627,9 @@ fn eth_state_tests_with_tipset( tests.push(RpcTest::identity(EthGetTransactionByHashLimited::request( (tx.hash.clone(), shared_tipset.epoch()), )?)); - tests.push(RpcTest::identity( - EthTraceTransaction::request((tx.hash.to_string(),)).unwrap(), - )); + tests.push(RpcTest::identity(EthTraceTransaction::request((tx + .hash + .to_string(),))?)); if smsg.message.from.protocol() == Protocol::Delegated && smsg.message.to.protocol() == Protocol::Delegated { diff --git a/src/tool/subcommands/api_cmd/test_snapshots.txt b/src/tool/subcommands/api_cmd/test_snapshots.txt index 2fb408f12991..97510d6d957a 100644 --- a/src/tool/subcommands/api_cmd/test_snapshots.txt +++ b/src/tool/subcommands/api_cmd/test_snapshots.txt @@ -59,7 +59,11 @@ filecoin_ethfeehistory_1737446676883828.rpcsnap.json.zst filecoin_ethfeehistory_v2_1767605175056660.rpcsnap.json.zst filecoin_ethgasprice_1758725940980141.rpcsnap.json.zst filecoin_ethgetbalance_1740048634848277.rpcsnap.json.zst -filecoin_ethgetbalance_v2_1768188109932986.rpcsnap.json.zst +filecoin_ethgetbalance_v1_unknown_addr_1770287845559622.rpcsnap.json.zst +filecoin_ethgetbalance_v2_latest_1770291948779489.rpcsnap.json.zst +filecoin_ethgetbalance_v2_safe_1770291948803158.rpcsnap.json.zst +filecoin_ethgetbalance_v2_pending_1770291949065157.rpcsnap.json.zst +filecoin_ethgetbalance_v2_unknown_addr_1770287845559744.rpcsnap.json.zst filecoin_ethgetblockbyhash_1740132537807408.rpcsnap.json.zst filecoin_ethgetblockbynumber_1737446676696328.rpcsnap.json.zst filecoin_ethgetblockbynumber_v2_1768192171057588.rpcsnap.json.zst @@ -77,17 +81,21 @@ filecoin_ethgetblocktransactioncountbynumber_v2_1769099953141937.rpcsnap.json.zs filecoin_ethgetcode_1765803672602510.rpcsnap.json.zst # latest filecoin_ethgetcode_1765803672604518.rpcsnap.json.zst # concrete filecoin_ethgetcode_1765803672655291.rpcsnap.json.zst # pending +filecoin_ethgetcode_v1_unknown_addr_1770288914643996.rpcsnap.json.zst filecoin_ethgetcode_v2_1769605928230413.rpcsnap.json.zst filecoin_ethgetcode_v2_finalized_1769605928335700.rpcsnap.json.zst filecoin_ethgetcode_v2_safe_1769605928230488.rpcsnap.json.zst +filecoin_ethgetcode_v2_unknown_addr_1770288914644057.rpcsnap.json.zst filecoin_ethgetlogs_1759922913569082.rpcsnap.json.zst filecoin_ethgetmessagecidbytransactionhash_1737446676697418.rpcsnap.json.zst filecoin_ethgetstorageat_1765803742043605.rpcsnap.json.zst # latest filecoin_ethgetstorageat_1765803742046844.rpcsnap.json.zst # concrete filecoin_ethgetstorageat_1765803742145789.rpcsnap.json.zst # pending +filecoin_ethgetstorageat_v1_unknown_addr_1770288667897125.rpcsnap.json.zst filecoin_ethgetstorageat_v2_1769611091439289.rpcsnap.json.zst filecoin_ethgetstorageat_v2_finalized_1769611091511354.rpcsnap.json.zst filecoin_ethgetstorageat_v2_safe_1769611091439347.rpcsnap.json.zst +filecoin_ethgetstorageat_v2_unknown_addr_1770288667897031.rpcsnap.json.zst filecoin_ethgettransactionbyblockhashandindex_1740132538373654.rpcsnap.json.zst filecoin_ethgettransactionbyblocknumberandindex_1740132538304408.rpcsnap.json.zst filecoin_ethgettransactionbyblocknumberandindex_latest_1769103643171646.rpcsnap.json.zst @@ -98,7 +106,9 @@ filecoin_ethgettransactionbyblocknumberandindex_v2_safe_1769103643174698.rpcsnap filecoin_ethgettransactionbyhash_1741272955520821.rpcsnap.json.zst filecoin_ethgettransactionbyhashlimited_1741272955509708.rpcsnap.json.zst filecoin_ethgettransactioncount_1740132538183426.rpcsnap.json.zst +filecoin_ethgettransactioncount_v1_unknown_addr_1770288294132251.rpcsnap.json.zst filecoin_ethgettransactioncount_v2_1767847407595348.rpcsnap.json.zst +filecoin_ethgettransactioncount_v2_unknown_addr_1770288294132366.rpcsnap.json.zst filecoin_ethgettransactionhashbycid_1737446676698540.rpcsnap.json.zst filecoin_ethgettransactionreceipt_1741272955712904.rpcsnap.json.zst filecoin_ethgettransactionreceipt_1765811578590165.rpcsnap.json.zst # transaction not found From 4a98db7ba0aa0876c46fdcc003df0c15769f9641 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 5 Feb 2026 17:31:00 +0530 Subject: [PATCH 5/6] fix linter issue --- src/tool/subcommands/api_cmd/test_snapshots.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tool/subcommands/api_cmd/test_snapshots.txt b/src/tool/subcommands/api_cmd/test_snapshots.txt index 97510d6d957a..9a1cdb1d6e7c 100644 --- a/src/tool/subcommands/api_cmd/test_snapshots.txt +++ b/src/tool/subcommands/api_cmd/test_snapshots.txt @@ -61,8 +61,8 @@ filecoin_ethgasprice_1758725940980141.rpcsnap.json.zst filecoin_ethgetbalance_1740048634848277.rpcsnap.json.zst filecoin_ethgetbalance_v1_unknown_addr_1770287845559622.rpcsnap.json.zst filecoin_ethgetbalance_v2_latest_1770291948779489.rpcsnap.json.zst -filecoin_ethgetbalance_v2_safe_1770291948803158.rpcsnap.json.zst filecoin_ethgetbalance_v2_pending_1770291949065157.rpcsnap.json.zst +filecoin_ethgetbalance_v2_safe_1770291948803158.rpcsnap.json.zst filecoin_ethgetbalance_v2_unknown_addr_1770287845559744.rpcsnap.json.zst filecoin_ethgetblockbyhash_1740132537807408.rpcsnap.json.zst filecoin_ethgetblockbynumber_1737446676696328.rpcsnap.json.zst From 9036fe92b2a6dff418e3e31b055d01289a0edd4d Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 5 Feb 2026 23:34:08 +0530 Subject: [PATCH 6/6] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09642e7bd1c6..6474ac085986 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,8 @@ - [#6535](https://github.com/ChainSafe/forest/pull/6535): Fixed incorrect replace by fee behavior when at limits of pending messages in mempool. +- [#6541](https://github.com/ChainSafe/forest/pull/6541): Fixed "actor not found" errors when running Foundry (forge) scripts. The `eth_getBalance`, `eth_getTransactionCount`, and `eth_getCode` methods now return default values (0 balance, 0 nonce, empty code) for non-existent addresses, matching Lotus and standard Ethereum behavior. + ## Forest v0.31.1 "Quadrantids" This is a non-mandatory release for all node operators. It includes the support for more V2 API's and a few critical API fixes.