diff --git a/CHANGELOG.md b/CHANGELOG.md index bca2f1687495..1012eab00eb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ ### Added +- [#6231](https://github.com/ChainSafe/forest/pull/6231) Implemented `Filecoin.ChainGetTipSet` for API v2. + ### Changed ### Removed diff --git a/scripts/tests/api_compare/docker-compose.yml b/scripts/tests/api_compare/docker-compose.yml index ca6db3dec47e..f4af8a385aca 100644 --- a/scripts/tests/api_compare/docker-compose.yml +++ b/scripts/tests/api_compare/docker-compose.yml @@ -278,6 +278,7 @@ services: forest-tool api compare $(ls /data/*.car.zst | tail -n 1) \ --forest $$FOREST_API_INFO \ --lotus $$LOTUS_API_INFO \ + --offline \ --n-tipsets 5 \ --filter-file /data/filter-list-offline \ --report-mode=failure-only \ diff --git a/src/blocks/tipset.rs b/src/blocks/tipset.rs index edb568eddadd..ef657d8b195f 100644 --- a/src/blocks/tipset.rs +++ b/src/blocks/tipset.rs @@ -141,6 +141,13 @@ impl IntoIterator for TipsetKey { } } +#[cfg(test)] +impl Default for TipsetKey { + fn default() -> Self { + nunny::vec![Cid::default()].into() + } +} + /// An immutable set of blocks at the same height with the same parent set. /// Blocks in a tipset are canonically ordered by ticket size. /// diff --git a/src/chain/store/index.rs b/src/chain/store/index.rs index a579c3434c3a..05026bf3a8fe 100644 --- a/src/chain/store/index.rs +++ b/src/chain/store/index.rs @@ -157,7 +157,7 @@ impl ChainIndex { } if to > from.epoch() { return Err(Error::Other(format!( - "Looking for tipset {to} with height greater than start point {from}", + "looking for tipset with height greater than start point, req: {to}, head: {from}", from = from.epoch() ))); } diff --git a/src/rpc/client.rs b/src/rpc/client.rs index 1ea2c10bfb56..171c4e68f9b0 100644 --- a/src/rpc/client.rs +++ b/src/rpc/client.rs @@ -14,8 +14,7 @@ use std::fmt::{self, Debug}; use std::sync::LazyLock; use std::time::Duration; -use anyhow::{Context as _, bail}; -use enumflags2::BitFlags; +use anyhow::bail; use futures::future::Either; use http::{HeaderMap, HeaderValue, header}; use jsonrpsee::core::ClientError; @@ -93,15 +92,17 @@ impl Client { &self, req: Request, ) -> Result { + let max_api_path = req + .api_path() + .map_err(|e| ClientError::Custom(e.to_string()))?; let Request { method_name, params, - api_paths, timeout, .. } = req; let method_name = method_name.as_ref(); - let client = self.get_or_init_client(api_paths).await?; + let client = self.get_or_init_client(max_api_path).await?; let span = tracing::debug_span!("request", method = %method_name, url = %client.url); let work = async { // jsonrpsee's clients have a global `timeout`, but not a per-request timeout, which @@ -149,31 +150,16 @@ impl Client { }; work.instrument(span.or_current()).await } - async fn get_or_init_client( - &self, - version: BitFlags, - ) -> Result<&UrlClient, ClientError> { - let path = version - .iter() - .max() - .context("No supported versions") - .map_err(|e| ClientError::Custom(e.to_string()))?; + async fn get_or_init_client(&self, path: ApiPaths) -> Result<&UrlClient, ClientError> { match path { ApiPaths::V0 => &self.v0, ApiPaths::V1 => &self.v1, ApiPaths::V2 => &self.v2, } .get_or_try_init(|| async { - let url = self - .base_url - .join(match path { - ApiPaths::V0 => "rpc/v0", - ApiPaths::V1 => "rpc/v1", - ApiPaths::V2 => "rpc/v2", - }) - .map_err(|it| { - ClientError::Custom(format!("creating url for endpoint failed: {it}")) - })?; + let url = self.base_url.join(path.path()).map_err(|it| { + ClientError::Custom(format!("creating url for endpoint failed: {it}")) + })?; UrlClient::new(url, self.token.clone()).await }) .await diff --git a/src/rpc/methods/chain.rs b/src/rpc/methods/chain.rs index 773c0c6295cf..44c4f229bfc3 100644 --- a/src/rpc/methods/chain.rs +++ b/src/rpc/methods/chain.rs @@ -1,7 +1,7 @@ // Copyright 2019-2025 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -mod types; +pub mod types; use enumflags2::{BitFlags, make_bitflags}; use types::*; @@ -50,6 +50,21 @@ use tokio_util::sync::CancellationToken; const HEAD_CHANNEL_CAPACITY: usize = 10; +/// [`SAFE_HEIGHT_DISTANCE`] is the distance from the latest tipset, i.e. "heaviest", that +/// is considered to be safe from re-orgs at an increasingly diminishing +/// probability. +/// +/// This is used to determine the safe tipset when using the "safe" tag in +/// [`TipsetSelector`] or via Eth JSON-RPC APIs. Note that "safe" doesn't guarantee +/// finality, but rather a high probability of not being reverted. For guaranteed +/// finality, use the "finalized" tag. +/// +/// This constant is experimental and may change in the future. +/// Discussion on this current value and a tracking item to document the +/// probabilistic impact of various values is in +/// https://github.com/filecoin-project/go-f3/issues/944 +const SAFE_HEIGHT_DISTANCE: ChainEpoch = 200; + static CHAIN_EXPORT_LOCK: LazyLock>> = LazyLock::new(|| Mutex::new(None)); @@ -974,16 +989,120 @@ impl RpcMethod<1> for ChainGetTipSet { async fn handle( ctx: Ctx, - (ApiTipsetKey(tipset_key),): Self::Params, + (ApiTipsetKey(tsk),): Self::Params, ) -> Result { - let ts = ctx - .chain_store() - .load_required_tipset_or_heaviest(&tipset_key)?; - Ok(ts) + if let Some(tsk) = &tsk { + let ts = ctx.chain_index().load_required_tipset(tsk)?; + Ok(ts) + } else { + // It contains Lotus error message `NewTipSet called with zero length array of blocks` for parity tests + Err(anyhow::anyhow!( + "TipsetKey cannot be empty (NewTipSet called with zero length array of blocks)" + ) + .into()) + } } } pub enum ChainGetTipSetV2 {} + +impl ChainGetTipSetV2 { + pub async fn get_tipset_by_anchor( + ctx: &Ctx, + anchor: &Option, + ) -> anyhow::Result> { + if let Some(anchor) = anchor { + match (&anchor.key.0, &anchor.tag) { + // Anchor is zero-valued. Fall back to heaviest tipset. + (None, None) => Ok(Some(ctx.state_manager.heaviest_tipset())), + // Get tipset at the specified key. + (Some(tsk), None) => Ok(Some(ctx.chain_index().load_required_tipset(tsk)?)), + (None, Some(tag)) => Self::get_tipset_by_tag(ctx, *tag).await, + _ => { + anyhow::bail!("invalid anchor") + } + } + } else { + // No anchor specified. Fall back to finalized tipset. + Self::get_tipset_by_tag(ctx, TipsetTag::Finalized).await + } + } + + pub async fn get_tipset_by_tag( + ctx: &Ctx, + tag: TipsetTag, + ) -> anyhow::Result> { + match tag { + TipsetTag::Latest => Ok(Some(ctx.state_manager.heaviest_tipset())), + TipsetTag::Finalized => Self::get_latest_finalized_tipset(ctx).await, + TipsetTag::Safe => Some(Self::get_latest_safe_tipset(ctx).await).transpose(), + } + } + + pub async fn get_latest_safe_tipset( + ctx: &Ctx, + ) -> anyhow::Result { + let finalized = Self::get_latest_finalized_tipset(ctx).await?; + let head = ctx.chain_store().heaviest_tipset(); + let safe_height = (head.epoch() - SAFE_HEIGHT_DISTANCE).max(0); + if let Some(finalized) = finalized + && finalized.epoch() >= safe_height + { + Ok(finalized) + } else { + Ok(ctx.chain_index().tipset_by_height( + safe_height, + head, + ResolveNullTipset::TakeOlder, + )?) + } + } + + pub async fn get_latest_finalized_tipset( + ctx: &Ctx, + ) -> anyhow::Result> { + let Ok(f3_finalized_cert) = + crate::rpc::f3::F3GetLatestCertificate::handle(ctx.clone(), ()).await + else { + return Self::get_ec_finalized_tipset(ctx); + }; + + let f3_finalized_head = f3_finalized_cert.chain_head(); + let head = ctx.chain_store().heaviest_tipset(); + // Latest F3 finalized tipset is older than EC finality, falling back to EC finality + if head.epoch() > f3_finalized_head.epoch + ctx.chain_config().policy.chain_finality { + return Self::get_ec_finalized_tipset(ctx); + } + + let ts = ctx + .chain_index() + .load_required_tipset(&f3_finalized_head.key) + .map_err(|e| { + anyhow::anyhow!( + "Failed to load F3 finalized tipset at epoch {} with key {}: {e}", + f3_finalized_head.epoch, + f3_finalized_head.key, + ) + })?; + Ok(Some(ts)) + } + + pub fn get_ec_finalized_tipset(ctx: &Ctx) -> anyhow::Result> { + let head = ctx.chain_store().heaviest_tipset(); + let ec_finality_epoch = head.epoch() - ctx.chain_config().policy.chain_finality; + if ec_finality_epoch >= 0 { + let ts = ctx.chain_index().tipset_by_height( + ec_finality_epoch, + head, + ResolveNullTipset::TakeOlder, + )?; + Ok(Some(ts)) + } else { + Ok(None) + } + } +} + impl RpcMethod<1> for ChainGetTipSetV2 { const NAME: &'static str = "Filecoin.ChainGetTipSet"; const PARAM_NAMES: [&'static str; 1] = ["tipsetSelector"]; @@ -991,11 +1110,35 @@ impl RpcMethod<1> for ChainGetTipSetV2 { const PERMISSION: Permission = Permission::Read; const DESCRIPTION: Option<&'static str> = Some("Returns the tipset with the specified CID."); - type Params = (ApiTipsetKey,); - type Ok = Tipset; + type Params = (TipsetSelector,); + type Ok = Option; - async fn handle(_: Ctx, _: Self::Params) -> Result { - Err(ServerError::unsupported_method()) + async fn handle( + ctx: Ctx, + (selector,): Self::Params, + ) -> Result { + selector.validate()?; + // Get tipset by key. + if let ApiTipsetKey(Some(tsk)) = &selector.key { + let ts = ctx.chain_index().load_required_tipset(tsk)?; + return Ok(Some(ts)); + } + // Get tipset by height. + if let Some(height) = &selector.height { + let anchor = Self::get_tipset_by_anchor(&ctx, &height.anchor).await?; + let ts = ctx.chain_index().tipset_by_height( + height.at, + anchor.unwrap_or_else(|| ctx.chain_store().heaviest_tipset()), + height.resolve_null_tipset_policy(), + )?; + return Ok(Some(ts)); + } + // Get tipset by tag, either latest or finalized. + if let Some(tag) = &selector.tag { + let ts = Self::get_tipset_by_tag(&ctx, *tag).await?; + return Ok(ts); + } + Err(anyhow::anyhow!("no tipset found for selector").into()) } } diff --git a/src/rpc/methods/chain/types.rs b/src/rpc/methods/chain/types.rs index d1309da561cb..7b119ab6cb4c 100644 --- a/src/rpc/methods/chain/types.rs +++ b/src/rpc/methods/chain/types.rs @@ -3,6 +3,9 @@ use super::*; +#[cfg(test)] +mod tests; + #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq, Default)] #[serde(rename_all = "PascalCase")] pub struct ObjStat { @@ -10,3 +13,124 @@ pub struct ObjStat { pub links: usize, } lotus_json_with_self!(ObjStat); + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +pub struct TipsetSelector { + #[serde( + with = "crate::lotus_json", + skip_serializing_if = "ApiTipsetKey::is_none", + default + )] + #[schemars(with = "LotusJson")] + pub key: ApiTipsetKey, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub height: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub tag: Option, +} +lotus_json_with_self!(TipsetSelector); + +impl TipsetSelector { + /// Validate ensures that the [`TipsetSelector`] is valid. It checks that only one of + /// the selection criteria is specified. + pub fn validate(&self) -> anyhow::Result<()> { + match (&self.key.0, &self.tag, &self.height) { + (Some(_), None, None) | (None, Some(_), None) => {} + (None, None, Some(height)) => { + height.validate()?; + } + _ => { + let criteria = [ + self.key.0.is_some(), + self.tag.is_some(), + self.height.is_some(), + ] + .into_iter() + .filter(|&b| b) + .count(); + anyhow::bail!( + "exactly one tipset selection criteria must be specified, found: {criteria}" + ) + } + } + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +pub struct TipsetHeight { + pub at: ChainEpoch, + pub previous: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub anchor: Option, +} +lotus_json_with_self!(TipsetHeight); + +impl TipsetHeight { + /// Ensures that the [`TipsetHeight`] is valid. It checks that the height is + /// not negative and the anchor is valid. + pub fn validate(&self) -> anyhow::Result<()> { + anyhow::ensure!( + self.at >= 0, + "invalid tipset height: epoch cannot be less than zero" + ); + TipsetAnchor::validate(&self.anchor)?; + Ok(()) + } + + pub fn resolve_null_tipset_policy(&self) -> ResolveNullTipset { + if self.previous { + ResolveNullTipset::TakeOlder + } else { + ResolveNullTipset::TakeNewer + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +pub struct TipsetAnchor { + #[serde(with = "crate::lotus_json")] + #[schemars(with = "LotusJson")] + pub key: ApiTipsetKey, + #[serde(skip_serializing_if = "Option::is_none")] + pub tag: Option, +} +lotus_json_with_self!(TipsetAnchor); + +impl TipsetAnchor { + /// Validate ensures that the [`TipsetAnchor`] is valid. It checks that at most one + /// of [`TipsetKey`] or [`TipsetTag`] is specified. Otherwise, it returns an error. + /// + /// Note that a [`None`] anchor is valid, and is considered to be + /// equivalent to the default anchor, which is the tipset tagged as [`TipsetTag::Finalized`]. + pub fn validate(anchor: &Option) -> anyhow::Result<()> { + if let Some(anchor) = anchor { + anyhow::ensure!( + anchor.key.0.is_none() || anchor.tag.is_none(), + "invalid tipset anchor: at most one of key or tag must be specified" + ); + } + // None anchor is valid, and considered to be an equivalent to whatever the API decides the default to be. + Ok(()) + } +} + +#[derive( + Debug, + Clone, + Copy, + strum::Display, + strum::EnumString, + Serialize, + Deserialize, + PartialEq, + Eq, + JsonSchema, +)] +#[strum(serialize_all = "lowercase")] +#[serde(rename_all = "lowercase")] +pub enum TipsetTag { + Latest, + Finalized, + Safe, +} diff --git a/src/rpc/methods/chain/types/tests.rs b/src/rpc/methods/chain/types/tests.rs new file mode 100644 index 000000000000..214cb6a9e8f3 --- /dev/null +++ b/src/rpc/methods/chain/types/tests.rs @@ -0,0 +1,100 @@ +// Copyright 2019-2025 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use super::*; + +#[test] +fn test_tipset_selector_success_1() { + let s = TipsetSelector { + key: Some(TipsetKey::default()).into(), + height: None, + tag: None, + }; + s.validate().unwrap(); +} + +#[test] +fn test_tipset_selector_success_2() { + let s = TipsetSelector { + key: None.into(), + height: Some(TipsetHeight { + at: 100, + previous: true, + anchor: None, + }), + tag: None, + }; + s.validate().unwrap(); +} + +#[test] +fn test_tipset_selector_success_3() { + let s = TipsetSelector { + key: None.into(), + height: None, + tag: Some(TipsetTag::Finalized), + }; + s.validate().unwrap(); +} + +#[test] +fn test_tipset_selector_failure_1() { + let s = TipsetSelector { + key: None.into(), + height: None, + tag: None, + }; + s.validate().unwrap_err(); +} + +#[test] +fn test_tipset_selector_failure_2() { + let s = TipsetSelector { + key: Some(TipsetKey::default()).into(), + height: Some(TipsetHeight { + at: 100, + previous: true, + anchor: None, + }), + tag: None, + }; + s.validate().unwrap_err(); +} + +#[test] +fn test_tipset_selector_failure_3() { + let s = TipsetSelector { + key: Some(TipsetKey::default()).into(), + height: None, + tag: Some(TipsetTag::Finalized), + }; + s.validate().unwrap_err(); +} + +#[test] +fn test_tipset_selector_failure_4() { + let s = TipsetSelector { + key: None.into(), + height: Some(TipsetHeight { + at: 100, + previous: true, + anchor: None, + }), + tag: Some(TipsetTag::Finalized), + }; + s.validate().unwrap_err(); +} + +#[test] +fn test_tipset_selector_failure_5() { + let s = TipsetSelector { + key: Some(TipsetKey::default()).into(), + height: Some(TipsetHeight { + at: 100, + previous: true, + anchor: None, + }), + tag: Some(TipsetTag::Finalized), + }; + s.validate().unwrap_err(); +} diff --git a/src/rpc/reflect/mod.rs b/src/rpc/reflect/mod.rs index f03aac0d342c..693cd797d79e 100644 --- a/src/rpc/reflect/mod.rs +++ b/src/rpc/reflect/mod.rs @@ -162,6 +162,14 @@ impl ApiPaths { uri.path().split("/").last().expect("infallible"), )?) } + + pub fn path(&self) -> &'static str { + match self { + Self::V0 => "rpc/v0", + Self::V1 => "rpc/v1", + Self::V2 => "rpc/v2", + } + } } /// Utility methods, defined as an extension trait to avoid having to specify diff --git a/src/rpc/request.rs b/src/rpc/request.rs index 86d5ff33d11b..30c9d61fa97b 100644 --- a/src/rpc/request.rs +++ b/src/rpc/request.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0, MIT use super::ApiPaths; +use anyhow::Context as _; use enumflags2::BitFlags; use jsonrpsee::core::traits::ToRpcParams; use serde::{Deserialize, Serialize}; @@ -41,6 +42,14 @@ impl Request { timeout: self.timeout, } } + + pub fn max_api_path(api_paths: BitFlags) -> anyhow::Result { + api_paths.iter().max().context("No supported versions") + } + + pub fn api_path(&self) -> anyhow::Result { + Self::max_api_path(self.api_paths) + } } impl ToRpcParams for Request { diff --git a/src/rpc/types/mod.rs b/src/rpc/types/mod.rs index 6d53a930db23..85fbbd6852ec 100644 --- a/src/rpc/types/mod.rs +++ b/src/rpc/types/mod.rs @@ -157,7 +157,15 @@ lotus_json_with_self!(MessageLookup); derive_more::From, derive_more::Into, )] -pub struct ApiTipsetKey(pub Option); +pub struct ApiTipsetKey( + #[serde(skip_serializing_if = "Option::is_none", default)] pub Option, +); + +impl ApiTipsetKey { + pub fn is_none(&self) -> bool { + self.0.is_none() + } +} /// This wrapper is needed because of a bug in Lotus. /// See: . diff --git a/src/tool/subcommands/api_cmd.rs b/src/tool/subcommands/api_cmd.rs index 8187f8c03536..6080a18423f1 100644 --- a/src/tool/subcommands/api_cmd.rs +++ b/src/tool/subcommands/api_cmd.rs @@ -14,9 +14,7 @@ use crate::db::db_engine::db_root; use crate::eth::EthChainId as EthChainIdType; use crate::lotus_json::HasLotusJson; use crate::networks::NetworkChain; -use crate::rpc; -use crate::rpc::eth::types::*; -use crate::rpc::prelude::*; +use crate::rpc::{self, ApiPaths, eth::types::*, prelude::*}; use crate::shim::address::Address; use crate::tool::offline_server::start_offline_server; use crate::tool::subcommands::api_cmd::stateful_tests::TestTransaction; @@ -122,6 +120,9 @@ pub enum ApiCommands { /// Empty lines and lines starting with `#` are ignored. #[arg(long)] filter_file: Option, + /// Filter methods for the specific API version. + #[arg(long)] + filter_version: Option, /// Cancel test run on the first failure #[arg(long)] fail_fast: bool, @@ -285,6 +286,7 @@ impl ApiCommands { lotus: UrlFromMultiAddr(lotus), filter, filter_file, + filter_version, fail_fast, run_ignored, max_concurrent_requests, @@ -300,16 +302,17 @@ impl ApiCommands { api_compare_tests::run_tests( tests, - forest.clone(), - lotus.clone(), + forest, + lotus, max_concurrent_requests, - filter_file.clone(), - filter.clone(), + filter_file, + filter, + filter_version, run_ignored, fail_fast, - dump_dir.clone(), + dump_dir, &test_criteria_overrides, - report_dir.clone(), + report_dir, report_mode, ) .await?; @@ -366,8 +369,7 @@ impl ApiCommands { }, index, db, - // https://github.com/ChainSafe/forest/issues/6220 - api_path: None, + api_path: Some(test_dump.path), } }; @@ -471,6 +473,9 @@ impl ApiCommands { #[derive(clap::Args, Debug, Clone)] pub struct CreateTestsArgs { + /// The nodes to test against is offline, the chain is out of sync. + #[arg(long, default_value_t = false)] + offline: bool, /// The number of tipsets to use to generate test cases. #[arg(short, long, default_value = "10")] n_tipsets: usize, diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index 44a46b485b6a..67f6c2b01769 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -11,6 +11,7 @@ use crate::message::{Message as _, SignedMessage}; use crate::rpc::FilterList; use crate::rpc::auth::AuthNewParams; use crate::rpc::beacon::BeaconGetEntry; +use crate::rpc::chain::types::*; use crate::rpc::eth::{ BlockNumberOrHash, EthInt64, ExtBlockNumberOrHash, ExtPredefined, Predefined, new_eth_tx_from_signed_message, types::*, @@ -145,12 +146,14 @@ impl TestSummary { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TestDump { pub request: rpc::Request, + pub path: rpc::ApiPaths, pub forest_response: Result, pub lotus_response: Result, } impl std::fmt::Display for TestDump { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Request path: {}", self.path.path())?; writeln!(f, "Request dump: {:?}", self.request)?; writeln!(f, "Request params JSON: {}", self.request.params)?; let (forest_response, lotus_response) = ( @@ -404,6 +407,7 @@ impl RpcTest { lotus_status, test_dump: Some(TestDump { request: self.request.clone(), + path: self.request.api_path().expect("invalid api paths"), forest_response, lotus_response, }), @@ -432,6 +436,7 @@ fn chain_tests() -> Vec { fn chain_tests_with_tipset( store: &Arc, + offline: bool, tipset: &Tipset, ) -> anyhow::Result> { let mut tests = vec![ @@ -443,7 +448,57 @@ fn chain_tests_with_tipset( tipset.epoch(), Default::default(), ))?), - RpcTest::identity(ChainGetTipSet::request((tipset.key().clone().into(),))?), + RpcTest::identity(ChainGetTipSet::request((tipset.key().into(),))?), + RpcTest::identity(ChainGetTipSet::request((None.into(),))?) + .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError), + RpcTest::identity(ChainGetTipSetV2::request((TipsetSelector { + key: None.into(), + height: None, + tag: None, + },))?) + .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError), + RpcTest::identity(ChainGetTipSetV2::request((TipsetSelector { + key: tipset.key().into(), + height: None, + tag: Some(TipsetTag::Latest), + },))?) + .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError), + RpcTest::identity(ChainGetTipSetV2::request((TipsetSelector { + key: tipset.key().into(), + height: None, + tag: None, + },))?), + RpcTest::identity(ChainGetTipSetV2::request((TipsetSelector { + key: None.into(), + height: Some(TipsetHeight { + at: tipset.epoch(), + previous: true, + anchor: Some(TipsetAnchor { + key: None.into(), + tag: None, + }), + }), + tag: None, + },))?), + RpcTest::identity(ChainGetTipSetV2::request((TipsetSelector { + key: None.into(), + height: Some(TipsetHeight { + at: tipset.epoch(), + previous: true, + anchor: None, + }), + tag: None, + },))?) + .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError) + .ignore("this case should pass when F3 is back on calibnet"), + validate_tagged_tipset_v2( + ChainGetTipSetV2::request((TipsetSelector { + key: None.into(), + height: None, + tag: Some(TipsetTag::Latest), + },))?, + offline, + ), RpcTest::identity(ChainGetPath::request(( tipset.key().clone(), tipset.parents().clone(), @@ -456,6 +511,29 @@ fn chain_tests_with_tipset( RpcTest::basic(ChainGetFinalizedTipset::request(())?), ]; + if !offline { + tests.extend([ + // Requires F3, disabled for offline RPC server + validate_tagged_tipset_v2( + ChainGetTipSetV2::request((TipsetSelector { + key: None.into(), + height: None, + tag: Some(TipsetTag::Safe), + },))?, + offline, + ), + // Requires F3, disabled for offline RPC server + validate_tagged_tipset_v2( + ChainGetTipSetV2::request((TipsetSelector { + key: None.into(), + height: None, + tag: Some(TipsetTag::Finalized), + },))?, + offline, + ), + ]); + } + for block in tipset.block_headers() { let block_cid = *block.cid(); tests.extend([ @@ -2031,6 +2109,7 @@ fn f3_tests_with_tipset(tipset: &Tipset) -> anyhow::Result> { // CIDs. Right now, only the last `n_tipsets` tipsets are used. fn snapshot_tests( store: Arc, + offline: bool, num_tipsets: usize, miner_address: Option
, eth_chain_id: u64, @@ -2046,7 +2125,7 @@ fn snapshot_tests( .expect("Infallible"); for tipset in shared_tipset.chain(&store).take(num_tipsets) { - tests.extend(chain_tests_with_tipset(&store, &tipset)?); + tests.extend(chain_tests_with_tipset(&store, offline, &tipset)?); tests.extend(miner_tests_with_tipset(&store, &tipset, miner_address)?); tests.extend(state_tests_with_tipset(&store, &tipset)?); tests.extend(eth_tests_with_tipset(&store, &tipset)); @@ -2110,6 +2189,7 @@ fn sample_signed_messages<'a>( pub(super) async fn create_tests( CreateTestsArgs { + offline, n_tipsets, miner_address, worker_address, @@ -2132,6 +2212,7 @@ pub(super) async fn create_tests( revalidate_chain(store.clone(), n_tipsets).await?; tests.extend(snapshot_tests( store, + offline, n_tipsets, miner_address, eth_chain_id, @@ -2202,6 +2283,7 @@ pub(super) async fn run_tests( max_concurrent_requests: usize, filter_file: Option, filter: String, + filter_version: Option, run_ignored: RunIgnored, fail_fast: bool, dump_dir: Option, @@ -2257,6 +2339,12 @@ pub(super) async fn run_tests( continue; } + if let Some(filter_version) = filter_version + && !test.request.api_paths.contains(filter_version) + { + continue; + } + // Acquire a permit from the semaphore before spawning a test let permit = semaphore.clone().acquire_owned().await?; let forest = forest.clone(); @@ -2370,3 +2458,17 @@ fn validate_message_lookup(req: rpc::Request) -> RpcTest { forest == lotus }) } + +fn validate_tagged_tipset_v2(req: rpc::Request>, offline: bool) -> RpcTest { + RpcTest::validate(req, move |forest, lotus| match (forest, lotus) { + (None, None) => true, + (Some(forest), Some(lotus)) => { + if offline { + true + } else { + (forest.epoch() - lotus.epoch()).abs() <= 2 + } + } + _ => false, + }) +} diff --git a/src/tool/subcommands/api_cmd/generate_test_snapshot.rs b/src/tool/subcommands/api_cmd/generate_test_snapshot.rs index f644de54c59c..f60f4e59afa8 100644 --- a/src/tool/subcommands/api_cmd/generate_test_snapshot.rs +++ b/src/tool/subcommands/api_cmd/generate_test_snapshot.rs @@ -44,7 +44,9 @@ pub async fn run_test_with_dump( let params_raw = Some(serde_json::to_string(&test_dump.request.params)?); macro_rules! run_test { ($ty:ty) => { - if test_dump.request.method_name.as_ref() == <$ty>::NAME { + if test_dump.request.method_name.as_ref() == <$ty>::NAME + && <$ty>::API_PATHS.contains(test_dump.path) + { let params = <$ty>::parse_params(params_raw.clone(), ParamStructure::Either)?; match <$ty>::handle(ctx.clone(), params).await { Ok(result) => { diff --git a/src/tool/subcommands/api_cmd/test_snapshots.txt b/src/tool/subcommands/api_cmd/test_snapshots.txt index 017369332bec..0f1543e50108 100644 --- a/src/tool/subcommands/api_cmd/test_snapshots.txt +++ b/src/tool/subcommands/api_cmd/test_snapshots.txt @@ -13,6 +13,9 @@ filecoin_chaingetparentmessages_1736937305551928.rpcsnap.json.zst filecoin_chaingetparentreceipts_1736937305550289.rpcsnap.json.zst filecoin_chaingetpath_1736937942817384.rpcsnap.json.zst filecoin_chaingettipset_1736937942817675.rpcsnap.json.zst +filecoin_chaingettipset_height_1762420743420210.rpcsnap.json.zst +filecoin_chaingettipset_key_1762420743419725.rpcsnap.json.zst +filecoin_chaingettipset_tag_1762420743420262.rpcsnap.json.zst filecoin_chaingettipsetafterheight_1736937942817771.rpcsnap.json.zst filecoin_chaingettipsetbyheight_1741271398549509.rpcsnap.json.zst filecoin_chainhasobj_1736937942818251.rpcsnap.json.zst