From 504ec83ae7a17fde7cabdcbb75c1c3052203e4fd Mon Sep 17 00:00:00 2001 From: Shashank Date: Wed, 4 Feb 2026 13:30:19 +0530 Subject: [PATCH 01/11] Impl EthTraceFilterV2 --- src/rpc/methods/eth.rs | 63 +++++++++++++++++++ src/rpc/mod.rs | 1 + .../subcommands/api_cmd/api_compare_tests.rs | 12 ++++ .../subcommands/api_cmd/test_snapshots.txt | 1 + 4 files changed, 77 insertions(+) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 8b219bf132cb..32e7b58c039a 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -4080,6 +4080,22 @@ fn get_eth_block_number_from_string( )) } +async fn get_eth_block_number_from_string_v2( + ctx: &Ctx, + block: Option<&str>, + resolve: ResolveNullTipset, +) -> Result { + let block_param = match block { + Some(block_str) => ExtBlockNumberOrHash::from_str(block_str)?, + None => bail!("cannot parse fromBlock"), + }; + Ok(EthUint64( + tipset_by_block_number_or_hash_v2(ctx, block_param, resolve) + .await? + .epoch() as u64, + )) +} + pub enum EthTraceFilter {} impl RpcMethod<1> for EthTraceFilter { const N_REQUIRED_PARAMS: usize = 1; @@ -4125,6 +4141,53 @@ impl RpcMethod<1> for EthTraceFilter { } } +pub enum EthTraceFilterV2 {} +impl RpcMethod<1> for EthTraceFilterV2 { + const N_REQUIRED_PARAMS: usize = 1; + const NAME: &'static str = "Filecoin.EthTraceFilter"; + const NAME_ALIAS: Option<&'static str> = Some("trace_filter"); + const PARAM_NAMES: [&'static str; 1] = ["filter"]; + const API_PATHS: BitFlags = make_bitflags!(ApiPaths::V2); + const PERMISSION: Permission = Permission::Read; + const DESCRIPTION: Option<&'static str> = + Some("Returns the traces for transactions matching the filter criteria."); + type Params = (EthTraceFilterCriteria,); + type Ok = Vec; + + async fn handle( + ctx: Ctx, + (filter,): Self::Params, + ) -> Result { + let from_block = get_eth_block_number_from_string_v2( + &ctx, + filter.from_block.as_deref(), + ResolveNullTipset::TakeNewer, + ) + .await + .context("cannot parse fromBlock")?; + + let to_block = get_eth_block_number_from_string_v2( + &ctx, + filter.to_block.as_deref(), + ResolveNullTipset::TakeOlder, + ) + .await + .context("cannot parse toBlock")?; + + Ok(trace_filter(ctx, filter, from_block, to_block) + .await? + .into_iter() + .sorted_by_key(|trace| { + ( + trace.block_number, + trace.transaction_position, + trace.trace.trace_address.clone(), + ) + }) + .collect::>()) + } +} + async fn trace_filter( ctx: Ctx, filter: EthTraceFilterCriteria, diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 6986ae7e05aa..e08c7fa7aa3a 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -154,6 +154,7 @@ macro_rules! for_each_rpc_method { $callback!($crate::rpc::eth::EthTraceBlock); $callback!($crate::rpc::eth::EthTraceBlockV2); $callback!($crate::rpc::eth::EthTraceFilter); + $callback!($crate::rpc::eth::EthTraceFilterV2); $callback!($crate::rpc::eth::EthTraceTransaction); $callback!($crate::rpc::eth::EthTraceReplayBlockTransactions); $callback!($crate::rpc::eth::EthTraceReplayBlockTransactionsV2); diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index 65ad68986ba9..b1d6e3a70566 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -2403,6 +2403,18 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset // both nodes could fail on, e.g., "too many results, maximum supported is 500, try paginating // requests with After and Count" .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError), + RpcTest::identity( + EthTraceFilterV2::request((EthTraceFilterCriteria { + from_block: Some(format!( + "0x{:x}", + shared_tipset.epoch() - (SAFE_EPOCH_DELAY + 1) + )), + to_block: Some(format!("0x{:x}", shared_tipset.epoch() - SAFE_EPOCH_DELAY)), + ..Default::default() + },)) + .unwrap(), + ) + .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError), RpcTest::identity( EthGetTransactionReceipt::request(( // A transaction that should not exist, to test the `null` response in case diff --git a/src/tool/subcommands/api_cmd/test_snapshots.txt b/src/tool/subcommands/api_cmd/test_snapshots.txt index 2fb408f12991..2df013195232 100644 --- a/src/tool/subcommands/api_cmd/test_snapshots.txt +++ b/src/tool/subcommands/api_cmd/test_snapshots.txt @@ -117,6 +117,7 @@ filecoin_ethtraceblock_v2_safe_1769092401374979.rpcsnap.json.zst filecoin_ethtracefilter_1742371405673188.rpcsnap.json.zst filecoin_ethtracefilter_1742983898701553.rpcsnap.json.zst filecoin_ethtracefilter_1746449543820062.rpcsnap.json.zst +filecoin_ethtracefilter_1770191732520294.rpcsnap.json.zst filecoin_ethtracereplayblocktransactions_1768898971081023.rpcsnap.json.zst filecoin_ethtracereplayblocktransactions_1768898971153948.rpcsnap.json.zst filecoin_ethtracetransaction_1741765677273941.rpcsnap.json.zst From cc77df0a32fddf77ec1ffc0020d049d96ca45ff8 Mon Sep 17 00:00:00 2001 From: Shashank Date: Wed, 4 Feb 2026 13:36:12 +0530 Subject: [PATCH 02/11] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f50121804636..6f2ac7b1d121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,8 @@ - [#6498](https://github.com/ChainSafe/forest/pull/6498): Implemented `Filecoin.EthGetBlockReceiptsLimited` for API v2. +- [#6522](https://github.com/ChainSafe/forest/pull/6522): Implemented `Filecoin.EthTraceFilter` for API v2. + ### Changed - [#6471](https://github.com/ChainSafe/forest/pull/6471): Moved `forest-tool state` subcommand to `forest-dev`. From 8ac9a07025b042f9a1c4c591cc1723f387345667 Mon Sep 17 00:00:00 2001 From: Shashank Date: Sat, 7 Feb 2026 06:42:48 +0530 Subject: [PATCH 03/11] fix open rpc test --- .../forest__rpc__tests__rpc__v2.snap | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap index 8c05524bd277..7493b19c4e58 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap @@ -1088,6 +1088,40 @@ methods: items: $ref: "#/components/schemas/EthBlockTrace" paramStructure: by-position + - name: Filecoin.EthTraceFilter + description: Returns the traces for transactions matching the filter criteria. + params: + - name: filter + required: true + schema: + $ref: "#/components/schemas/EthTraceFilterCriteria" + result: + name: Filecoin.EthTraceFilter.Result + required: false + schema: + type: + - array + - "null" + items: + $ref: "#/components/schemas/EthBlockTrace" + paramStructure: by-position + - name: trace_filter + description: Returns the traces for transactions matching the filter criteria. + params: + - name: filter + required: true + schema: + $ref: "#/components/schemas/EthTraceFilterCriteria" + result: + name: trace_filter.Result + required: false + schema: + type: + - array + - "null" + items: + $ref: "#/components/schemas/EthBlockTrace" + paramStructure: by-position - name: Filecoin.EthTraceTransaction description: Returns the traces for a specific transaction. params: @@ -1884,6 +1918,39 @@ components: - traceAddress - action - result + EthTraceFilterCriteria: + type: object + properties: + after: + description: "After specifies the offset for pagination of trace results. The number of traces to skip before returning results.\nOptional, default: None." + anyOf: + - $ref: "#/components/schemas/EthUint64" + - type: "null" + count: + description: "Limits the number of traces returned.\nOptional, default: all traces." + anyOf: + - $ref: "#/components/schemas/EthUint64" + - type: "null" + fromAddress: + description: "Actor address or a list of addresses from which transactions that generate traces should originate.\nOptional, default: None.\nThe JSON decoding must treat a string as equivalent to an array with one value, for example\n\"0x8888f1f195afa192cfee86069858\" must be decoded as [ \"0x8888f1f195afa192cfee86069858\" ]" + anyOf: + - $ref: "#/components/schemas/EthAddressList" + - type: "null" + fromBlock: + description: "Interpreted as an epoch (in hex) or one of \"latest\" for last mined block, \"pending\" for not yet committed messages.\nOptional, default: \"latest\".\nNote: \"earliest\" is not a permitted value." + type: + - string + - "null" + toAddress: + description: "Actor address or a list of addresses to which transactions that generate traces are sent.\nOptional, default: None.\nThe JSON decoding must treat a string as equivalent to an array with one value, for example\n\"0x8888f1f195afa192cfee86069858\" must be decoded as [ \"0x8888f1f195afa192cfee86069858\" ]" + anyOf: + - $ref: "#/components/schemas/EthAddressList" + - type: "null" + toBlock: + description: "Interpreted as an epoch (in hex) or one of \"latest\" for last mined block, \"pending\" for not yet committed messages.\nOptional, default: \"latest\".\nNote: \"earliest\" is not a permitted value." + type: + - string + - "null" EthTxReceipt: type: object properties: From 366bf75eb47823d8df77aa71a4fc6a42131a5432 Mon Sep 17 00:00:00 2001 From: Shashank Date: Mon, 9 Feb 2026 22:19:59 +0530 Subject: [PATCH 04/11] fix changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f22f9121e08..48cdd18dca53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ ### Added +- [#6522](https://github.com/ChainSafe/forest/pull/6522): Implemented `Filecoin.EthTraceFilter` for API v2. + ### Changed ### Removed @@ -63,8 +65,6 @@ This is a non-mandatory release for all node operators. It resets F3 on calibnet - [#6498](https://github.com/ChainSafe/forest/pull/6498): Implemented `Filecoin.EthGetBlockReceiptsLimited` for API v2. -- [#6522](https://github.com/ChainSafe/forest/pull/6522): Implemented `Filecoin.EthTraceFilter` for API v2. - ### Changed - [#6471](https://github.com/ChainSafe/forest/pull/6471): Moved `forest-tool state` subcommand to `forest-dev`. From 0c0b285f7078da3f5d89a0cd046976e0d8fbb2aa Mon Sep 17 00:00:00 2001 From: Shashank Date: Tue, 10 Feb 2026 21:59:32 +0530 Subject: [PATCH 05/11] update test snapshot --- 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 2337c36f235f..ead22e72cd6c 100644 --- a/src/tool/subcommands/api_cmd/test_snapshots.txt +++ b/src/tool/subcommands/api_cmd/test_snapshots.txt @@ -127,7 +127,7 @@ filecoin_ethtraceblock_v2_safe_1769092401374979.rpcsnap.json.zst filecoin_ethtracefilter_1742371405673188.rpcsnap.json.zst filecoin_ethtracefilter_1742983898701553.rpcsnap.json.zst filecoin_ethtracefilter_1746449543820062.rpcsnap.json.zst -filecoin_ethtracefilter_1770191732520294.rpcsnap.json.zst +filecoin_ethtracefilter_v2_1770740801450325.rpcsnap.json.zst filecoin_ethtracereplayblocktransactions_1768898971081023.rpcsnap.json.zst filecoin_ethtracereplayblocktransactions_1768898971153948.rpcsnap.json.zst filecoin_ethtracetransaction_1741765677273941.rpcsnap.json.zst From db32e8d6183aa0a14ac3449dfa01acd50d5a53cd Mon Sep 17 00:00:00 2001 From: Shashank Date: Mon, 16 Feb 2026 13:19:22 +0530 Subject: [PATCH 06/11] default to latest --- src/rpc/methods/eth.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 2c0bf8224dba..24ff360eb2d2 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -4115,10 +4115,7 @@ fn get_eth_block_number_from_string( block: Option<&str>, resolve: ResolveNullTipset, ) -> Result { - let block_param = match block { - Some(block_str) => ExtBlockNumberOrHash::from_str(block_str)?, - None => bail!("cannot parse fromBlock"), - }; + let block_param = ExtBlockNumberOrHash::from_str(block.unwrap_or("latest"))?; Ok(EthUint64( tipset_by_ext_block_number_or_hash(chain_store, block_param, resolve)?.epoch() as u64, )) @@ -4129,10 +4126,7 @@ async fn get_eth_block_number_from_string_v2, resolve: ResolveNullTipset, ) -> Result { - let block_param = match block { - Some(block_str) => ExtBlockNumberOrHash::from_str(block_str)?, - None => bail!("cannot parse fromBlock"), - }; + let block_param = ExtBlockNumberOrHash::from_str(block.unwrap_or("latest"))?; Ok(EthUint64( tipset_by_block_number_or_hash_v2(ctx, block_param, resolve) .await? From 1e3d31aa1f59ce8edc20e5623cd19042261f0abb Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 19 Feb 2026 15:07:12 +0530 Subject: [PATCH 07/11] refactor and add more test --- CHANGELOG.md | 2 + src/rpc/methods/eth.rs | 98 ++++++++++--------- .../subcommands/api_cmd/api_compare_tests.rs | 23 +++++ .../subcommands/api_cmd/test_snapshots.txt | 3 + 4 files changed, 78 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a08ae036744d..583a17bbf24e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ ### Changed +- [#6522](https://github.com/ChainSafe/forest/pull/6522): `Filecoin.EthTraceFilter` filter options `from_block` and `to_block` now default to `latest` tag when omitted for v1 and v2 API. + ### Removed ### Fixed diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 24ff360eb2d2..0ed4bb8aaf3a 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -306,8 +306,19 @@ pub enum Predefined { Latest, } -#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, Default, JsonSchema)] -#[serde(rename_all = "camelCase")] +#[derive( + PartialEq, + Debug, + Clone, + Serialize, + Deserialize, + Default, + JsonSchema, + strum::Display, + strum::EnumString, +)] +#[strum(serialize_all = "lowercase")] +#[serde(rename_all = "lowercase")] pub enum ExtPredefined { Earliest, Pending, @@ -413,6 +424,12 @@ pub enum ExtBlockNumberOrHash { BlockHashObject(BlockHash), } +impl Default for ExtBlockNumberOrHash { + fn default() -> Self { + Self::PredefinedBlock(ExtPredefined::default()) + } +} + lotus_json_with_self!(ExtBlockNumberOrHash); #[allow(dead_code)] @@ -449,24 +466,13 @@ impl ExtBlockNumberOrHash { } pub fn from_str(s: &str) -> Result { - match s { - "earliest" => Ok(ExtBlockNumberOrHash::from_predefined( - ExtPredefined::Earliest, - )), - "pending" => Ok(ExtBlockNumberOrHash::from_predefined( - ExtPredefined::Pending, - )), - "latest" | "" => Ok(ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest)), - "safe" => Ok(ExtBlockNumberOrHash::from_predefined(ExtPredefined::Safe)), - "finalized" => Ok(ExtBlockNumberOrHash::from_predefined( - ExtPredefined::Finalized, - )), - hex if hex.starts_with("0x") => { - let epoch = hex_str_to_epoch(hex)?; - Ok(ExtBlockNumberOrHash::from_block_number(epoch)) - } - _ => Err(anyhow!("Invalid block identifier")), + if s.starts_with("0x") { + let epoch = hex_str_to_epoch(s)?; + return Ok(ExtBlockNumberOrHash::from_block_number(epoch)); } + s.parse::() + .map(ExtBlockNumberOrHash::from_predefined) + .map_err(|_| anyhow!("Invalid block identifier")) } } @@ -4115,7 +4121,10 @@ fn get_eth_block_number_from_string( block: Option<&str>, resolve: ResolveNullTipset, ) -> Result { - let block_param = ExtBlockNumberOrHash::from_str(block.unwrap_or("latest"))?; + let block_param = block + .map(ExtBlockNumberOrHash::from_str) + .transpose()? + .unwrap_or_default(); Ok(EthUint64( tipset_by_ext_block_number_or_hash(chain_store, block_param, resolve)?.epoch() as u64, )) @@ -4126,7 +4135,10 @@ async fn get_eth_block_number_from_string_v2, resolve: ResolveNullTipset, ) -> Result { - let block_param = ExtBlockNumberOrHash::from_str(block.unwrap_or("latest"))?; + let block_param = block + .map(ExtBlockNumberOrHash::from_str) + .transpose()? + .unwrap_or_default(); Ok(EthUint64( tipset_by_block_number_or_hash_v2(ctx, block_param, resolve) .await? @@ -4165,17 +4177,7 @@ impl RpcMethod<1> for EthTraceFilter { ) .context("cannot parse toBlock")?; - Ok(trace_filter(ctx, filter, from_block, to_block) - .await? - .into_iter() - .sorted_by_key(|trace| { - ( - trace.block_number, - trace.transaction_position, - trace.trace.trace_address.clone(), - ) - }) - .collect_vec()) + Ok(trace_filter(ctx, filter, from_block, to_block).await?) } } @@ -4212,17 +4214,7 @@ impl RpcMethod<1> for EthTraceFilterV2 { .await .context("cannot parse toBlock")?; - Ok(trace_filter(ctx, filter, from_block, to_block) - .await? - .into_iter() - .sorted_by_key(|trace| { - ( - trace.block_number, - trace.transaction_position, - trace.trace.trace_address.clone(), - ) - }) - .collect::>()) + Ok(trace_filter(ctx, filter, from_block, to_block).await?) } } @@ -4231,10 +4223,10 @@ async fn trace_filter( filter: EthTraceFilterCriteria, from_block: EthUint64, to_block: EthUint64, -) -> Result> { +) -> Result> { let mut results = HashSet::default(); if let Some(EthUint64(0)) = filter.count { - return Ok(results); + return Ok(Vec::new()); } let count = *filter.count.unwrap_or_default(); ensure!( @@ -4245,7 +4237,8 @@ async fn trace_filter( ); let mut trace_counter = 0; - for blk_num in from_block.0..=to_block.0 { + 'blocks: for blk_num in from_block.0..=to_block.0 { + // For BlockNumber, EthTraceBlock and EthTraceBlockV2 are equivalent. let block_traces = EthTraceBlock::handle( ctx.clone(), (ExtBlockNumberOrHash::from_block_number(blk_num as i64),), @@ -4266,7 +4259,7 @@ async fn trace_filter( results.insert(block_trace); if filter.count.is_some() && results.len() >= count as usize { - return Ok(results); + break 'blocks; } else if results.len() > *FOREST_TRACE_FILTER_MAX_RESULT as usize { bail!( "too many results, maximum supported is {}, try paginating requests with After and Count", @@ -4277,7 +4270,16 @@ async fn trace_filter( } } - Ok(results) + Ok(results + .into_iter() + .sorted_by_key(|trace| { + ( + trace.block_number, + trace.transaction_position, + trace.trace.trace_address.clone(), + ) + }) + .collect_vec()) } #[cfg(test)] diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index ccee3389837c..471c7781184b 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -2474,6 +2474,14 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset // both nodes could fail on, e.g., "too many results, maximum supported is 500, try paginating // requests with After and Count" .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError), + RpcTest::identity( + EthTraceFilter::request((EthTraceFilterCriteria { + from_block: Some(ExtPredefined::Safe.to_string()), + count: Some(1.into()), + ..Default::default() + },)) + .unwrap(), + ), RpcTest::identity( EthTraceFilterV2::request((EthTraceFilterCriteria { from_block: Some(format!( @@ -2486,6 +2494,21 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset .unwrap(), ) .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError), + RpcTest::identity( + EthTraceFilterV2::request((EthTraceFilterCriteria { + from_block: Some(ExtPredefined::Latest.to_string()), + count: Some(1.into()), + ..Default::default() + },)) + .unwrap(), + ), + RpcTest::identity( + EthTraceFilterV2::request((EthTraceFilterCriteria { + count: Some(1.into()), + ..Default::default() + },)) + .unwrap(), + ), RpcTest::identity( EthGetTransactionReceipt::request(( // A transaction that should not exist, to test the `null` response in case diff --git a/src/tool/subcommands/api_cmd/test_snapshots.txt b/src/tool/subcommands/api_cmd/test_snapshots.txt index ead22e72cd6c..a68becb3cd60 100644 --- a/src/tool/subcommands/api_cmd/test_snapshots.txt +++ b/src/tool/subcommands/api_cmd/test_snapshots.txt @@ -127,7 +127,10 @@ filecoin_ethtraceblock_v2_safe_1769092401374979.rpcsnap.json.zst filecoin_ethtracefilter_1742371405673188.rpcsnap.json.zst filecoin_ethtracefilter_1742983898701553.rpcsnap.json.zst filecoin_ethtracefilter_1746449543820062.rpcsnap.json.zst +filecoin_ethtracefilter_safe_1771492815155978.rpcsnap.json.zst filecoin_ethtracefilter_v2_1770740801450325.rpcsnap.json.zst +filecoin_ethtracefilter_v2_default_1771492815053207.rpcsnap.json.zst +filecoin_ethtracefilter_v2_latest_1771492815053002.rpcsnap.json.zst filecoin_ethtracereplayblocktransactions_1768898971081023.rpcsnap.json.zst filecoin_ethtracereplayblocktransactions_1768898971153948.rpcsnap.json.zst filecoin_ethtracetransaction_1741765677273941.rpcsnap.json.zst From f6d20290620e7660b7b02f518e71d9ef81703024 Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 19 Feb 2026 15:56:44 +0530 Subject: [PATCH 08/11] exclude from offline check --- scripts/tests/api_compare/filter-list-offline | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/tests/api_compare/filter-list-offline b/scripts/tests/api_compare/filter-list-offline index 060691866265..712c55ded541 100644 --- a/scripts/tests/api_compare/filter-list-offline +++ b/scripts/tests/api_compare/filter-list-offline @@ -31,3 +31,4 @@ !Filecoin.EthGetTransactionByBlockNumberAndIndex !eth_getTransactionByBlockNumberAndIndex !Filecoin.ChainSetHead +!Filecoin.EthTraceFilter From 6ea084b9240fc27f26f5d095e4e4af2f053daaaa Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 20 Feb 2026 11:30:44 +0530 Subject: [PATCH 09/11] refactor for BlockNumberOrHash --- src/rpc/methods/eth.rs | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 0ed4bb8aaf3a..b604126ed4d3 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -297,8 +297,19 @@ impl From<[u8; EVM_WORD_LENGTH]> for EthHash { } } -#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, Default, JsonSchema)] -#[serde(rename_all = "camelCase")] +#[derive( + PartialEq, + Debug, + Clone, + Serialize, + Deserialize, + Default, + JsonSchema, + strum::Display, + strum::EnumString, +)] +#[strum(serialize_all = "lowercase")] +#[serde(rename_all = "lowercase")] pub enum Predefined { Earliest, Pending, @@ -400,16 +411,13 @@ impl BlockNumberOrHash { } pub fn from_str(s: &str) -> Result { - match s { - "earliest" => Ok(BlockNumberOrHash::from_predefined(Predefined::Earliest)), - "pending" => Ok(BlockNumberOrHash::from_predefined(Predefined::Pending)), - "latest" | "" => Ok(BlockNumberOrHash::from_predefined(Predefined::Latest)), - hex if hex.starts_with("0x") => { - let epoch = hex_str_to_epoch(hex)?; - Ok(BlockNumberOrHash::from_block_number(epoch)) - } - _ => Err(anyhow!("Invalid block identifier")), + if s.starts_with("0x") { + let epoch = hex_str_to_epoch(s)?; + return Ok(BlockNumberOrHash::from_block_number(epoch)); } + s.parse::() + .map(BlockNumberOrHash::from_predefined) + .map_err(|_| anyhow!("Invalid block identifier")) } } From 171e53c501b3ee29ee01b75102994c6e82870c1e Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 20 Feb 2026 12:01:36 +0530 Subject: [PATCH 10/11] add Default for BlockNumberOrHash --- src/rpc/methods/eth.rs | 6 ++++++ src/rpc/methods/eth/filter/mod.rs | 22 ++++++++++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index b604126ed4d3..ac1abeb50844 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -376,6 +376,12 @@ pub enum BlockNumberOrHash { BlockHashObject(BlockHash), } +impl Default for BlockNumberOrHash { + fn default() -> Self { + Self::PredefinedBlock(Predefined::default()) + } +} + lotus_json_with_self!(BlockNumberOrHash); impl BlockNumberOrHash { diff --git a/src/rpc/methods/eth/filter/mod.rs b/src/rpc/methods/eth/filter/mod.rs index a7a6eaac6be3..8764b9c3f692 100644 --- a/src/rpc/methods/eth/filter/mod.rs +++ b/src/rpc/methods/eth/filter/mod.rs @@ -447,14 +447,20 @@ impl EthFilterSpec { } ParsedFilterTipsets::Hash(block_hash.clone()) } else { - let from_block = self.from_block.as_deref().unwrap_or(""); - let to_block = self.to_block.as_deref().unwrap_or(""); - let (min, max) = parse_block_range( - chain_height, - BlockNumberOrHash::from_str(from_block)?, - BlockNumberOrHash::from_str(to_block)?, - max_filter_height_range, - )?; + let from_block = self + .from_block + .as_deref() + .map(BlockNumberOrHash::from_str) + .transpose()? + .unwrap_or_default(); + let to_block = self + .to_block + .as_deref() + .map(BlockNumberOrHash::from_str) + .transpose()? + .unwrap_or_default(); + let (min, max) = + parse_block_range(chain_height, from_block, to_block, max_filter_height_range)?; ParsedFilterTipsets::Range(RangeInclusive::new(min, max)) }; From 591a1eddeb53c78c5a7c9cfa43ee64fa506960a8 Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 20 Feb 2026 15:23:38 +0530 Subject: [PATCH 11/11] fix parse_block_range test --- src/rpc/methods/eth/filter/mod.rs | 85 +++++++++++++++++-------------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/src/rpc/methods/eth/filter/mod.rs b/src/rpc/methods/eth/filter/mod.rs index 8764b9c3f692..37b50aa3c0c6 100644 --- a/src/rpc/methods/eth/filter/mod.rs +++ b/src/rpc/methods/eth/filter/mod.rs @@ -973,19 +973,7 @@ mod tests { assert_eq!(min_height, 1); // hex_str_to_epoch("0x1") = 1 assert_eq!(max_height, 10); // hex_str_to_epoch("0xA") = 10 - // Test case 3: from_block = "latest", to_block = "" - let result = parse_block_range( - heaviest, - BlockNumberOrHash::from_str("latest").unwrap(), - BlockNumberOrHash::from_str("").unwrap(), - max_range, - ); - assert!(result.is_ok()); - let (min_height, max_height) = result.unwrap(); - assert_eq!(min_height, heaviest); - assert_eq!(max_height, -1); - - // Test case 4: Range too large + // Test case 3: Range too large let result = parse_block_range( heaviest, BlockNumberOrHash::from_str("earliest").unwrap(), @@ -994,7 +982,7 @@ mod tests { ); assert!(result.is_err()); - // Test case 5: from_block = "latest", to_block = "earliest" + // Test case 4: from_block = "latest", to_block = "earliest" let result = parse_block_range( heaviest, BlockNumberOrHash::from_str("latest").unwrap(), @@ -1003,7 +991,7 @@ mod tests { ); assert!(result.is_err()); - // Test case 6: from_block = "earliest", to_block = "earliest" + // Test case 5: from_block = "earliest", to_block = "earliest" let result = parse_block_range( heaviest, BlockNumberOrHash::from_str("earliest").unwrap(), @@ -1015,7 +1003,7 @@ mod tests { assert_eq!(min_height, 0); assert_eq!(max_height, 0); - // Test case 7: from_block = "latest", to_block = "latest" + // Test case 6: from_block = "latest", to_block = "latest" let result = parse_block_range( heaviest, BlockNumberOrHash::from_str("latest").unwrap(), @@ -1027,68 +1015,89 @@ mod tests { assert_eq!(min_height, heaviest); assert_eq!(max_height, -1); - // Test case 8: from_block = "earliest", to_block = "" + // Test case 7: Both blocks are non-negative but from_block > to_block. let result = parse_block_range( heaviest, - BlockNumberOrHash::from_str("earliest").unwrap(), - BlockNumberOrHash::from_str("").unwrap(), + BlockNumberOrHash::from_str("0xA").unwrap(), + BlockNumberOrHash::from_str("0x1").unwrap(), max_range, ); - assert!(result.is_ok()); - let (min_height, max_height) = result.unwrap(); - assert_eq!(min_height, 0); - assert_eq!(max_height, -1); + assert!(result.is_err()); - // Test case 9: from_block = "", to_block = "earliest" + // Test case 8: Both blocks are non-negative, order is correct, but the range is too large. let result = parse_block_range( heaviest, - BlockNumberOrHash::from_str("").unwrap(), BlockNumberOrHash::from_str("earliest").unwrap(), + BlockNumberOrHash::from_str("0x65").unwrap(), max_range, ); assert!(result.is_err()); - // Test case 10: from_block = "", to_block = "latest" + // Test case 9: Range exactly equal to max_range (boundary, should succeed). let result = parse_block_range( heaviest, - BlockNumberOrHash::from_str("").unwrap(), + BlockNumberOrHash::from_str("earliest").unwrap(), + BlockNumberOrHash::from_str("0x64").unwrap(), + max_range, + ); + assert!(result.is_ok()); + let (min_height, max_height) = result.unwrap(); + assert_eq!(min_height, 0); + assert_eq!(max_height, 100); + + // Test case 10: Past range exactly equal to max_range (heaviest - min_height == max_range, should succeed). + let result = parse_block_range( + 100, // heaviest + BlockNumberOrHash::from_str("earliest").unwrap(), BlockNumberOrHash::from_str("latest").unwrap(), max_range, ); assert!(result.is_ok()); let (min_height, max_height) = result.unwrap(); - assert_eq!(min_height, heaviest); + assert_eq!(min_height, 0); assert_eq!(max_height, -1); - // Test case 11: from_block = "", to_block = "" + // Test case 11: Single block by numeric height. let result = parse_block_range( heaviest, - BlockNumberOrHash::from_str("").unwrap(), - BlockNumberOrHash::from_str("").unwrap(), + BlockNumberOrHash::from_str("0x32").unwrap(), + BlockNumberOrHash::from_str("0x32").unwrap(), max_range, ); assert!(result.is_ok()); let (min_height, max_height) = result.unwrap(); - assert_eq!(min_height, heaviest); - assert_eq!(max_height, -1); + assert_eq!(min_height, 50); + assert_eq!(max_height, 50); - // Test case 12: Both blocks are non-negative but from_block > to_block. + // Test case 12: Unsupported type for from_block (BlockHash) returns error. let result = parse_block_range( heaviest, - BlockNumberOrHash::from_str("0xA").unwrap(), - BlockNumberOrHash::from_str("0x1").unwrap(), + BlockNumberOrHash::BlockHash(EthHash::default()), + BlockNumberOrHash::from_str("latest").unwrap(), max_range, ); assert!(result.is_err()); - // Test case 13: Both blocks are non-negative, order is correct, but the range is too large. + // Test case 13: Unsupported type for to_block (BlockHash) returns error. let result = parse_block_range( heaviest, BlockNumberOrHash::from_str("earliest").unwrap(), - BlockNumberOrHash::from_str("0x65").unwrap(), + BlockNumberOrHash::BlockHash(EthHash::default()), max_range, ); assert!(result.is_err()); + + // Test case 14: "pending" behaves like "latest" for from_block and to_block. + let result = parse_block_range( + heaviest, + BlockNumberOrHash::from_str("pending").unwrap(), + BlockNumberOrHash::from_str("pending").unwrap(), + max_range, + ); + assert!(result.is_ok()); + let (min_height, max_height) = result.unwrap(); + assert_eq!(min_height, heaviest); + assert_eq!(max_height, -1); } #[test]