From 2417f12d2d912a8477d68bdc7989b4c2c298392e Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 4 Nov 2025 20:30:58 +0800 Subject: [PATCH 1/7] feat(rpc): Filecoin.ChainGetTipSet for v2 endpoint --- src/rpc/methods/chain.rs | 54 +++++++++++++++++-- src/rpc/methods/chain/types.rs | 99 ++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 3 deletions(-) diff --git a/src/rpc/methods/chain.rs b/src/rpc/methods/chain.rs index 108426bf82f5..040b2d7d09c8 100644 --- a/src/rpc/methods/chain.rs +++ b/src/rpc/methods/chain.rs @@ -988,6 +988,26 @@ impl RpcMethod<1> for ChainGetTipSet { } pub enum ChainGetTipSetV2 {} + +impl ChainGetTipSetV2 { + pub fn get_tipset_by_anchor( + ctx: &Ctx, + anchor: &Option, + ) -> anyhow::Result> { + } + + pub fn get_tipset_by_tag( + ctx: &Ctx, + tag: &TipsetTag, + ) -> anyhow::Result> { + match tag { + TipsetTag::Latest => Ok(ctx.state_manager.heaviest_tipset()), + TipsetTag::Finalized => {} + TipsetTag::Safe => {} + } + } +} + impl RpcMethod<1> for ChainGetTipSetV2 { const NAME: &'static str = "Filecoin.ChainGetTipSet"; const PARAM_NAMES: [&'static str; 1] = ["tipsetSelector"]; @@ -995,11 +1015,39 @@ 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 Params = (TipsetSelector,); type Ok = Tipset; - 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((*ts).clone()); + } + // Get tipset by height. + if let Some(height) = &selector.height { + let anchor = Self::get_tipset_by_anchor(&ctx, &height.anchor)?; + let ts = ctx.chain_index().tipset_by_height( + height.at, + anchor, + if height.previous { + ResolveNullTipset::TakeOlder + } else { + ResolveNullTipset::TakeNewer + }, + )?; + return Ok((*ts).clone()); + } + // Get tipset by tag, either latest or finalized. + if let Some(tag) = &selector.tag { + let ts = Self::get_tipset_by_tag(&ctx, tag)?; + return Ok((*ts).clone()); + } + 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..1bcefbec111a 100644 --- a/src/rpc/methods/chain/types.rs +++ b/src/rpc/methods/chain/types.rs @@ -10,3 +10,102 @@ pub struct ObjStat { pub links: usize, } lotus_json_with_self!(ObjStat); + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct TipsetSelector { + #[serde(with = "crate::lotus_json")] + #[schemars(with = "LotusJson")] + pub key: ApiTipsetKey, + #[serde(skip_serializing_if = "Option::is_none")] + pub height: Option, + #[serde(skip_serializing_if = "Option::is_none")] + 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. If no criteria are specified, it returns + /// nil, indicating that the default selection criteria should be used as defined + /// by the Lotus API Specification. + pub fn validate(&self) -> anyhow::Result<()> { + let mut criteria = 0; + if self.key.0.is_some() { + criteria += 1; + } + if self.tag.is_some() { + criteria += 1; + } + if let Some(height) = &self.height { + criteria += 1; + height.validate()?; + } + if criteria != 1 { + anyhow::bail!( + "exactly one tipset selection criteria must be specified, found: {criteria}" + ) + } + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, 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. + /// + /// A zero-valued height is considered to be valid. + pub fn validate(&self) -> anyhow::Result<()> { + if self.at.is_negative() { + anyhow::bail!("invalid tipset height: epoch cannot be less than zero"); + } + if let Some(anchor) = &self.anchor { + anchor.validate()?; + } + // An unspecified Anchor is valid, because it's an optional field, and falls back to whatever the API decides the default to be. + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, 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 nil or a zero-valued anchor is valid, and is considered to be + /// equivalent to the default anchor, which is the tipset tagged as "finalized". + pub fn validate(&self) -> anyhow::Result<()> { + if self.key.0.is_some() && self.tag.is_some() { + anyhow::bail!("invalid tipset anchor: at most one of key or tag must be specified"); + } + // Zero-valued 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, JsonSchema, +)] +#[strum(serialize_all = "lowercase")] +pub enum TipsetTag { + Latest, + Finalized, + Safe, +} From 530760245dd6a18eb90f592189fc9bba8c884465 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Wed, 5 Nov 2025 11:15:57 +0800 Subject: [PATCH 2/7] complete impl --- src/rpc/methods/chain.rs | 127 ++++++++++++++++++++++++++++++++++----- 1 file changed, 111 insertions(+), 16 deletions(-) diff --git a/src/rpc/methods/chain.rs b/src/rpc/methods/chain.rs index 9ab5eac203ef..ddf595ce3d01 100644 --- a/src/rpc/methods/chain.rs +++ b/src/rpc/methods/chain.rs @@ -54,6 +54,21 @@ use tokio_util::sync::CancellationToken; const HEAD_CHANNEL_CAPACITY: usize = 10; +// SafeHeightDistance 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)); @@ -990,20 +1005,100 @@ impl RpcMethod<1> for ChainGetTipSet { pub enum ChainGetTipSetV2 {} impl ChainGetTipSetV2 { - pub fn get_tipset_by_anchor( - ctx: &Ctx, + 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 fn get_tipset_by_tag( + 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, - tag: &TipsetTag, - ) -> anyhow::Result> { - match tag { - TipsetTag::Latest => Ok(ctx.state_manager.heaviest_tipset()), - TipsetTag::Finalized => {} - TipsetTag::Safe => {} + ) -> 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) } } } @@ -1016,31 +1111,31 @@ impl RpcMethod<1> for ChainGetTipSetV2 { const DESCRIPTION: Option<&'static str> = Some("Returns the tipset with the specified CID."); type Params = (TipsetSelector,); - type Ok = Arc; + type Ok = Option>; async fn handle( - ctx: Ctx, + 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(ts); + return Ok(Some(ts)); } // Get tipset by height. if let Some(height) = &selector.height { - let anchor = Self::get_tipset_by_anchor(&ctx, &height.anchor)?; + let anchor = Self::get_tipset_by_anchor(&ctx, &height.anchor).await?; let ts = ctx.chain_index().tipset_by_height( height.at, - anchor, + anchor.unwrap_or_else(|| ctx.chain_store().heaviest_tipset()), height.resolve_null_tipset_policy(), )?; - return Ok(ts); + 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)?; + let ts = Self::get_tipset_by_tag(&ctx, *tag).await?; return Ok(ts); } Err(anyhow::anyhow!("no tipset found for selector").into()) From 43ad5ef41fdbe5afc2d0faa266ba3e7c72f365e1 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Thu, 6 Nov 2025 10:01:26 +0800 Subject: [PATCH 3/7] parity tests --- src/rpc/client.rs | 32 +++------ src/rpc/methods/chain.rs | 2 +- src/rpc/methods/chain/types.rs | 31 +++++++-- src/rpc/methods/chain/types/tests.rs | 17 +++++ src/rpc/reflect/mod.rs | 8 +++ src/rpc/request.rs | 9 +++ src/rpc/types/mod.rs | 10 ++- src/tool/subcommands/api_cmd.rs | 24 ++++--- .../subcommands/api_cmd/api_compare_tests.rs | 69 ++++++++++++++++++- .../api_cmd/generate_test_snapshot.rs | 4 +- 10 files changed, 161 insertions(+), 45 deletions(-) create mode 100644 src/rpc/methods/chain/types/tests.rs 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 ddf595ce3d01..da967c1df090 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::*; diff --git a/src/rpc/methods/chain/types.rs b/src/rpc/methods/chain/types.rs index ef350bcfd623..778a63b35a00 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 { @@ -11,14 +14,18 @@ pub struct ObjStat { } lotus_json_with_self!(ObjStat); -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct TipsetSelector { - #[serde(with = "crate::lotus_json")] + #[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")] + #[serde(skip_serializing_if = "Option::is_none", default)] pub height: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(skip_serializing_if = "Option::is_none", default)] pub tag: Option, } lotus_json_with_self!(TipsetSelector); @@ -49,7 +56,7 @@ impl TipsetSelector { } } -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct TipsetHeight { pub at: ChainEpoch, pub previous: bool, @@ -83,7 +90,7 @@ impl TipsetHeight { } } -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct TipsetAnchor { #[serde(with = "crate::lotus_json")] #[schemars(with = "LotusJson")] @@ -109,9 +116,19 @@ impl TipsetAnchor { } #[derive( - Debug, Clone, Copy, strum::Display, strum::EnumString, Serialize, Deserialize, JsonSchema, + 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, diff --git a/src/rpc/methods/chain/types/tests.rs b/src/rpc/methods/chain/types/tests.rs new file mode 100644 index 000000000000..3bf4d82160b7 --- /dev/null +++ b/src/rpc/methods/chain/types/tests.rs @@ -0,0 +1,17 @@ +// Copyright 2019-2025 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use super::*; + +#[test] +fn test_tipset_selector_serde() { + let s = TipsetSelector { + key: None.into(), + height: None, + tag: None, + }; + let json = serde_json::to_value(&s).unwrap(); + println!("{json}"); + let s2: TipsetSelector = serde_json::from_value(json).unwrap(); + assert_eq!(s, s2); +} 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..8cf18040da88 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), } }; diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index e808862b0e1b..47f1b3144fb9 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, }), @@ -443,7 +447,49 @@ fn chain_tests_with_tipset( tipset.epoch(), Default::default(), ))?), - RpcTest::identity(ChainGetTipSet::request((tipset.key().clone().into(),))?), + RpcTest::identity(ChainGetTipSet::request((tipset.key().into(),))?), + validate_tagged_tipset(ChainGetTipSet::request((None.into(),))?), + 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: None, + }), + tag: None, + },))?), + validate_tagged_tipset_v2(ChainGetTipSetV2::request((TipsetSelector { + key: None.into(), + height: None, + tag: Some(TipsetTag::Safe), + },))?), + validate_tagged_tipset_v2(ChainGetTipSetV2::request((TipsetSelector { + key: None.into(), + height: None, + tag: Some(TipsetTag::Latest), + },))?), + validate_tagged_tipset_v2(ChainGetTipSetV2::request((TipsetSelector { + key: None.into(), + height: None, + tag: Some(TipsetTag::Finalized), + },))?), RpcTest::identity(ChainGetPath::request(( tipset.key().clone(), tipset.parents().clone(), @@ -2202,6 +2248,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 +2304,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 +2423,17 @@ fn validate_message_lookup(req: rpc::Request) -> RpcTest { forest == lotus }) } + +fn validate_tagged_tipset(req: rpc::Request>) -> RpcTest { + RpcTest::validate(req, |forest, lotus| { + (forest.epoch() - lotus.epoch()).abs() <= 2 + }) +} + +fn validate_tagged_tipset_v2(req: rpc::Request>>) -> RpcTest { + RpcTest::validate(req, |forest, lotus| match (forest, lotus) { + (None, None) => true, + (Some(forest), Some(lotus)) => (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) => { From 51bd6c59ac8d7da096f0e0469c6ea8263fc82b56 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Thu, 6 Nov 2025 17:45:42 +0800 Subject: [PATCH 4/7] fix tests --- src/chain/store/index.rs | 2 +- src/rpc/methods/chain.rs | 13 ++++++---- .../subcommands/api_cmd/api_compare_tests.rs | 25 +++++++++++++------ .../subcommands/api_cmd/test_snapshots.txt | 3 +++ 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/chain/store/index.rs b/src/chain/store/index.rs index 3fd73602899a..3cf523b30472 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/methods/chain.rs b/src/rpc/methods/chain.rs index da967c1df090..72971ecf28b1 100644 --- a/src/rpc/methods/chain.rs +++ b/src/rpc/methods/chain.rs @@ -993,12 +993,15 @@ 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 { + // Error message here matches lotus + Err(anyhow::anyhow!("NewTipSet called with zero length array of blocks").into()) + } } } diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index 47f1b3144fb9..9aed2e45160d 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -448,7 +448,8 @@ fn chain_tests_with_tipset( Default::default(), ))?), RpcTest::identity(ChainGetTipSet::request((tipset.key().into(),))?), - validate_tagged_tipset(ChainGetTipSet::request((None.into(),))?), + RpcTest::identity(ChainGetTipSet::request((None.into(),))?) + .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError), RpcTest::identity(ChainGetTipSetV2::request((TipsetSelector { key: None.into(), height: None, @@ -471,10 +472,24 @@ fn chain_tests_with_tipset( height: Some(TipsetHeight { at: tipset.epoch(), previous: true, - anchor: None, + 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, @@ -2424,12 +2439,6 @@ fn validate_message_lookup(req: rpc::Request) -> RpcTest { }) } -fn validate_tagged_tipset(req: rpc::Request>) -> RpcTest { - RpcTest::validate(req, |forest, lotus| { - (forest.epoch() - lotus.epoch()).abs() <= 2 - }) -} - fn validate_tagged_tipset_v2(req: rpc::Request>>) -> RpcTest { RpcTest::validate(req, |forest, lotus| match (forest, lotus) { (None, None) => true, 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 From 10ccd6a5453cc398d1bb85680d49775ce33a47cf Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Thu, 6 Nov 2025 23:18:58 +0800 Subject: [PATCH 5/7] fix offline RPC parity test --- scripts/tests/api_compare/docker-compose.yml | 1 + src/tool/subcommands/api_cmd.rs | 3 + .../subcommands/api_cmd/api_compare_tests.rs | 64 +++++++++++++------ 3 files changed, 49 insertions(+), 19 deletions(-) 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/tool/subcommands/api_cmd.rs b/src/tool/subcommands/api_cmd.rs index 8cf18040da88..6080a18423f1 100644 --- a/src/tool/subcommands/api_cmd.rs +++ b/src/tool/subcommands/api_cmd.rs @@ -473,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 9aed2e45160d..402f1b5f64fb 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -436,6 +436,7 @@ fn chain_tests() -> Vec { fn chain_tests_with_tipset( store: &Arc, + offline: bool, tipset: &Tipset, ) -> anyhow::Result> { let mut tests = vec![ @@ -490,21 +491,14 @@ fn chain_tests_with_tipset( },))?) .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::Safe), - },))?), - validate_tagged_tipset_v2(ChainGetTipSetV2::request((TipsetSelector { - key: None.into(), - height: None, - tag: Some(TipsetTag::Latest), - },))?), - validate_tagged_tipset_v2(ChainGetTipSetV2::request((TipsetSelector { - key: None.into(), - height: None, - tag: Some(TipsetTag::Finalized), - },))?), + 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(), @@ -517,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([ @@ -2092,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, @@ -2107,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)); @@ -2171,6 +2189,7 @@ fn sample_signed_messages<'a>( pub(super) async fn create_tests( CreateTestsArgs { + offline, n_tipsets, miner_address, worker_address, @@ -2193,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, @@ -2439,10 +2459,16 @@ fn validate_message_lookup(req: rpc::Request) -> RpcTest { }) } -fn validate_tagged_tipset_v2(req: rpc::Request>>) -> RpcTest { - RpcTest::validate(req, |forest, lotus| match (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)) => (forest.epoch() - lotus.epoch()).abs() <= 2, + (Some(forest), Some(lotus)) => { + if offline { + true + } else { + (forest.epoch() - lotus.epoch()).abs() <= 2 + } + } _ => false, }) } From 6c84c1a014190d74b39c03cb0609b4d4e11345be Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Fri, 7 Nov 2025 10:21:41 +0800 Subject: [PATCH 6/7] changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43653feb63ae..574a4d70da67 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 From cf3d257d3d8a386fb91cd02d780f4d9ae6648939 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 2 Dec 2025 19:01:50 +0800 Subject: [PATCH 7/7] resolve comments --- src/blocks/tipset.rs | 7 +++ src/rpc/methods/chain.rs | 33 +++++----- src/rpc/methods/chain/types.rs | 72 ++++++++++----------- src/rpc/methods/chain/types/tests.rs | 93 ++++++++++++++++++++++++++-- 4 files changed, 149 insertions(+), 56 deletions(-) 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/rpc/methods/chain.rs b/src/rpc/methods/chain.rs index 7039c8289189..44c4f229bfc3 100644 --- a/src/rpc/methods/chain.rs +++ b/src/rpc/methods/chain.rs @@ -50,19 +50,19 @@ use tokio_util::sync::CancellationToken; const HEAD_CHANNEL_CAPACITY: usize = 10; -// SafeHeightDistance 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 +/// [`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>> = @@ -995,8 +995,11 @@ impl RpcMethod<1> for ChainGetTipSet { let ts = ctx.chain_index().load_required_tipset(tsk)?; Ok(ts) } else { - // Error message here matches lotus - Err(anyhow::anyhow!("NewTipSet called with zero length array of blocks").into()) + // 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()) } } } diff --git a/src/rpc/methods/chain/types.rs b/src/rpc/methods/chain/types.rs index 778a63b35a00..7b119ab6cb4c 100644 --- a/src/rpc/methods/chain/types.rs +++ b/src/rpc/methods/chain/types.rs @@ -31,26 +31,27 @@ pub struct TipsetSelector { 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. If no criteria are specified, it returns - /// nil, indicating that the default selection criteria should be used as defined - /// by the Lotus API Specification. + /// Validate ensures that the [`TipsetSelector`] is valid. It checks that only one of + /// the selection criteria is specified. pub fn validate(&self) -> anyhow::Result<()> { - let mut criteria = 0; - if self.key.0.is_some() { - criteria += 1; - } - if self.tag.is_some() { - criteria += 1; - } - if let Some(height) = &self.height { - criteria += 1; - height.validate()?; - } - if criteria != 1 { - anyhow::bail!( - "exactly one tipset selection criteria must be specified, found: {criteria}" - ) + 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(()) } @@ -68,16 +69,12 @@ 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. - /// - /// A zero-valued height is considered to be valid. pub fn validate(&self) -> anyhow::Result<()> { - if self.at.is_negative() { - anyhow::bail!("invalid tipset height: epoch cannot be less than zero"); - } - if let Some(anchor) = &self.anchor { - anchor.validate()?; - } - // An unspecified Anchor is valid, because it's an optional field, and falls back to whatever the API decides the default to be. + anyhow::ensure!( + self.at >= 0, + "invalid tipset height: epoch cannot be less than zero" + ); + TipsetAnchor::validate(&self.anchor)?; Ok(()) } @@ -101,16 +98,19 @@ pub struct TipsetAnchor { 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. + /// 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 nil or a zero-valued anchor is valid, and is considered to be - /// equivalent to the default anchor, which is the tipset tagged as "finalized". - pub fn validate(&self) -> anyhow::Result<()> { - if self.key.0.is_some() && self.tag.is_some() { - anyhow::bail!("invalid tipset anchor: at most one of key or tag must be specified"); + /// 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" + ); } - // Zero-valued anchor is valid, and considered to be an equivalent to whatever the API decides the default to be. + // None anchor is valid, and considered to be an equivalent to whatever the API decides the default to be. Ok(()) } } diff --git a/src/rpc/methods/chain/types/tests.rs b/src/rpc/methods/chain/types/tests.rs index 3bf4d82160b7..214cb6a9e8f3 100644 --- a/src/rpc/methods/chain/types/tests.rs +++ b/src/rpc/methods/chain/types/tests.rs @@ -4,14 +4,97 @@ use super::*; #[test] -fn test_tipset_selector_serde() { +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, }; - let json = serde_json::to_value(&s).unwrap(); - println!("{json}"); - let s2: TipsetSelector = serde_json::from_value(json).unwrap(); - assert_eq!(s, s2); + 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(); }