From ff4e0e28528a2bee0137ee6f94348e40ecbeef67 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:20:09 +0200 Subject: [PATCH 1/7] fix(sdk): default initial protocol version to 10 when unpinned (upgrade-safe ratchet floor) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the caller pins neither an explicit PlatformVersion (with_version) nor an explicit initial version (with_initial_version), SdkBuilder previously seeded the protocol version to PlatformVersion::latest(). Combined with the upward-only ratchet in maybe_update_protocol_version (early-return when received <= current, then fetch_max), an unpinned SDK booted ABOVE a network still running an older protocol version (e.g. 10/11 during an upgrade window) and could never come back down — leaving it permanently stuck too high and shipping a too-new wire format. Autodetect could never fix it because it only ratchets up. Seed the default initial protocol version from a new bumpable constant DEFAULT_INITIAL_PROTOCOL_VERSION = 10 instead of latest(). The atomic protocol_version (the source read by version(), query_settings(), and proof parsing) is seeded from the builder's version field, so changing the default field is the single effective change point. Ratchet-up autodetection then converges upward (10 -> whatever the network reports), and starting low keeps requests compatible with not-yet-upgraded nodes during an upgrade window. Explicit with_version / with_initial_version overwrite the field, so pinned config is unaffected. Bump the constant when the network's supported floor moves. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/rs-sdk/src/sdk.rs | 93 +++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/packages/rs-sdk/src/sdk.rs b/packages/rs-sdk/src/sdk.rs index adf7c7d2e68..53f7470c7e9 100644 --- a/packages/rs-sdk/src/sdk.rs +++ b/packages/rs-sdk/src/sdk.rs @@ -50,6 +50,15 @@ pub const DEFAULT_CONTRACT_CACHE_SIZE: usize = 100; pub const DEFAULT_TOKEN_CONFIG_CACHE_SIZE: usize = 100; /// How many quorum public keys fit in the cache. pub const DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE: usize = 100; +/// Default initial protocol version used when the caller pins neither an explicit +/// [`PlatformVersion`] (via [`SdkBuilder::with_version`]) nor an explicit initial +/// protocol version (via [`SdkBuilder::with_initial_version`]). +/// +/// Deliberately set BELOW the latest known version: ratchet-up autodetection +/// (`maybe_update_protocol_version`) converges upward to the network's real version, +/// and starting low keeps requests compatible with not-yet-upgraded nodes during an +/// upgrade window. Bump this constant when the network's supported floor advances. +pub const DEFAULT_INITIAL_PROTOCOL_VERSION: u32 = 10; /// The default metadata time tolerance for checkpoint queries in milliseconds const ADDRESS_STATE_TIME_TOLERANCE_MS: u64 = 31 * 60 * 1000; @@ -351,8 +360,8 @@ impl Sdk { /// ## Protocol version bootstrapping /// /// On a fresh auto-detect SDK (i.e. one built without [`SdkBuilder::with_version()`]), the - /// first call to this method uses [`PlatformVersion::latest()`] as a fallback because no - /// network response has been received yet to teach the SDK the real network version. + /// first call to this method uses [`DEFAULT_INITIAL_PROTOCOL_VERSION`] as a fallback because + /// no network response has been received yet to teach the SDK the real network version. /// /// The actual network version is learned only *after* proof parsing succeeds, when /// [`Self::verify_response_metadata()`] processes `metadata.protocol_version`. If the @@ -483,7 +492,7 @@ impl Sdk { /// Return [Dash Platform version](PlatformVersion) information used by this SDK. /// - /// When auto-detection is enabled (default), returns [`PlatformVersion::latest()`] + /// When auto-detection is enabled (default), returns [`DEFAULT_INITIAL_PROTOCOL_VERSION`] /// until the first network response is received, then tracks the network's version. /// When pinned via [`SdkBuilder::with_version()`], always returns the pinned version. pub fn version<'v>(&self) -> &'v PlatformVersion { @@ -759,7 +768,8 @@ impl Default for SdkBuilder { cancel_token: CancellationToken::new(), - version: PlatformVersion::latest(), + version: PlatformVersion::get(DEFAULT_INITIAL_PROTOCOL_VERSION) + .expect("DEFAULT_INITIAL_PROTOCOL_VERSION must be a known PlatformVersion"), version_explicit: false, #[cfg(not(target_arch = "wasm32"))] ca_certificate: None, @@ -879,9 +889,11 @@ impl SdkBuilder { /// Configure platform version. /// - /// Select specific version of Dash Platform to use. + /// Select specific version of Dash Platform to use. This pins the version and + /// disables auto-detection. /// - /// Defaults to [PlatformVersion::latest()]. + /// When unset, the SDK starts at [`DEFAULT_INITIAL_PROTOCOL_VERSION`] and + /// ratchets upward via auto-detection. pub fn with_version(mut self, version: &'static PlatformVersion) -> Self { self.version = version; self.version_explicit = true; @@ -1663,6 +1675,75 @@ mod test { ); } + #[test] + fn test_default_builder_seeds_initial_protocol_version_floor() { + // A default builder (no with_version / with_initial_version) must seed the + // SDK at DEFAULT_INITIAL_PROTOCOL_VERSION, not at PlatformVersion::latest(). + let sdk = SdkBuilder::new_mock() + .build() + .expect("mock Sdk should be created"); + + assert_eq!( + sdk.protocol_version_number(), + super::DEFAULT_INITIAL_PROTOCOL_VERSION, + "unpinned SDK must boot at the upgrade-safe floor, not latest()" + ); + assert_eq!( + sdk.version().protocol_version, + super::DEFAULT_INITIAL_PROTOCOL_VERSION + ); + assert!( + sdk.auto_detect_protocol_version, + "default SDK must keep auto-detect enabled" + ); + } + + #[test] + fn test_default_floor_ratchets_up_but_never_down() { + let sdk = SdkBuilder::new_mock() + .build() + .expect("mock Sdk should be created"); + let floor = super::DEFAULT_INITIAL_PROTOCOL_VERSION; + assert_eq!(sdk.protocol_version_number(), floor); + + // Ratchet up: a newer network version raises the floor. + sdk.maybe_update_protocol_version(floor + 2); + assert_eq!( + sdk.protocol_version_number(), + floor + 2, + "auto-detect must ratchet upward from the floor" + ); + + // Never down: an older network version is ignored. + sdk.maybe_update_protocol_version(floor - 1); + assert_eq!( + sdk.protocol_version_number(), + floor + 2, + "ratchet must never downgrade below the highest observed version" + ); + } + + #[test] + fn test_explicit_pin_overrides_default_floor() { + use dpp::version::PlatformVersion; + + // Pin to a version that is deliberately different from the default floor so + // the override is observable regardless of where the floor is set. + let pinned_number = super::DEFAULT_INITIAL_PROTOCOL_VERSION - 1; + let pinned = PlatformVersion::get(pinned_number).expect("pinned PV exists"); + let sdk = SdkBuilder::new_mock() + .with_version(pinned) + .build() + .expect("mock Sdk should be created"); + + assert_eq!( + sdk.protocol_version_number(), + pinned_number, + "explicit with_version must win over the default floor" + ); + assert!(!sdk.auto_detect_protocol_version); + } + #[test_matrix([90,91,100,109,110], 100, 10, false; "valid time")] #[test_matrix([0,89,111], 100, 10, true; "invalid time")] #[test_matrix([0,100], [0,100], 100, false; "zero time")] From d3d5bb86fba0f292c6fe3db719205014849157b5 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:27:16 +0200 Subject: [PATCH 2/7] refactor(sdk): source default initial protocol version from rs-platform-version V10 constant Replace the magic literal `10` in DEFAULT_INITIAL_PROTOCOL_VERSION with the predefined `dpp::version::v10::PROTOCOL_VERSION_10` constant (`pub const PROTOCOL_VERSION_10: ProtocolVersion = 10`, where `ProtocolVersion = u32`). It is a true compile-time const, so it remains valid in our `const u32` initializer. The local DEFAULT_INITIAL_PROTOCOL_VERSION name is kept as the bumpable knob; only the source of the value changes. No behavioral change. Edits only packages/rs-sdk. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/rs-sdk/src/sdk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rs-sdk/src/sdk.rs b/packages/rs-sdk/src/sdk.rs index 53f7470c7e9..d76156afd27 100644 --- a/packages/rs-sdk/src/sdk.rs +++ b/packages/rs-sdk/src/sdk.rs @@ -58,7 +58,7 @@ pub const DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE: usize = 100; /// (`maybe_update_protocol_version`) converges upward to the network's real version, /// and starting low keeps requests compatible with not-yet-upgraded nodes during an /// upgrade window. Bump this constant when the network's supported floor advances. -pub const DEFAULT_INITIAL_PROTOCOL_VERSION: u32 = 10; +pub const DEFAULT_INITIAL_PROTOCOL_VERSION: u32 = dpp::version::v10::PROTOCOL_VERSION_10; /// The default metadata time tolerance for checkpoint queries in milliseconds const ADDRESS_STATE_TIME_TOLERANCE_MS: u64 = 31 * 60 * 1000; From 2f455220f4c0251083e18c79fdd94151f41f9c4f Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:14:13 +0200 Subject: [PATCH 3/7] test(sdk): pin initial protocol version in v3.1+ tests; harden ratchet test; doc count workaround MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PV10 default floor (V0 documents wire) made several mock tests panic in the local query encoder before any network call, because they exercise v3.1+-only semantics: - fetch::document_count::* (7 tests): Count / group_by require the V1 wire. Each now builds its mock SDK via count_capable_mock_sdk(), which pins the initial version to the hardcoded PV12 constant (dpp::version::v12::PROTOCOL_VERSION_12 — the first release wiring DRIVE_ABCI_QUERY_VERSIONS_V1) using with_initial_version (keeps auto-detect). - fetch::mock_fetch::test_mock_fetch_data_contract: mock_data_contract builds V2 document types; an unpinned SDK decodes the round-trip at the V0 floor and downgrades them to V1. Pinned to PV12 the same way. - fetch::document_query_v0_v1::sdk_builder_with_initial_version_seeds_atomic_without_pinning: its default-seed assertion expected latest(); updated to assert the new floor DEFAULT_INITIAL_PROTOCOL_VERSION. Hardened test_default_floor_ratchets_up_but_never_down: the ratchet target is now the hardcoded PV12 constant with a precondition asserting it exceeds the floor, so the test stays correct (and only accepts known versions) when the floor advances — replacing the brittle floor + 2 arithmetic. Documented on DEFAULT_INITIAL_PROTOCOL_VERSION the two ways to use the v3.1+-only query surfaces (Count / group_by / having) on an unpinned SDK: (a) with_initial_version at build time, or (b) issue one ratcheting query right after build() (e.g. ExtendedEpochInfo::fetch_current) to lift the protocol version to the network's actual version. Includes a compiling no_run example. No default-seed change, no proactive probe; scope is packages/rs-sdk only. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/rs-sdk/src/sdk.rs | 49 +++++++++++++++++-- packages/rs-sdk/tests/fetch/document_count.rs | 36 +++++++++++--- .../tests/fetch/document_query_v0_v1.rs | 8 +-- packages/rs-sdk/tests/fetch/mock_fetch.rs | 13 ++++- 4 files changed, 89 insertions(+), 17 deletions(-) diff --git a/packages/rs-sdk/src/sdk.rs b/packages/rs-sdk/src/sdk.rs index d76156afd27..474d2c8665b 100644 --- a/packages/rs-sdk/src/sdk.rs +++ b/packages/rs-sdk/src/sdk.rs @@ -58,6 +58,40 @@ pub const DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE: usize = 100; /// (`maybe_update_protocol_version`) converges upward to the network's real version, /// and starting low keeps requests compatible with not-yet-upgraded nodes during an /// upgrade window. Bump this constant when the network's supported floor advances. +/// +/// # v3.1+-only query surfaces +/// +/// An unpinned SDK starts at this upgrade-safe floor (PV10, V0 documents wire). +/// Under V0 the local query encoder rejects the v3.1+-only surfaces — `Count` +/// (`SelectProjection::count_star`), `group_by`, and `having` — with an +/// [`Error::Config`] *before* any network round-trip. To use them, either: +/// +/// 1. Pin a higher initial version at build time via +/// [`SdkBuilder::with_initial_version`], or +/// 2. Issue one ratcheting query right after `build()`. Any normal query works +/// at the floor and its response metadata ratchets the SDK's protocol version +/// up to the network's actual version; subsequent `Count` / `group_by` / +/// `having` queries then encode correctly. +/// +/// Example of option 2 — a parameterless current-state fetch warms up the ratchet: +/// +/// ```no_run +/// # use dash_sdk::{Sdk, SdkBuilder}; +/// # use dash_sdk::platform::fetch_current_no_parameters::FetchCurrent; +/// # use dpp::block::extended_epoch_info::ExtendedEpochInfo; +/// # async fn warm_up() -> Result<(), dash_sdk::Error> { +/// // Option 1 alternative: SdkBuilder::new_mainnet(addrs) +/// // .with_initial_version(dpp::version::PlatformVersion::latest()).build()?; +/// let sdk: Sdk = SdkBuilder::new_mock().build()?; +/// +/// // One ratcheting query: works at the floor, returns metadata, and lifts the +/// // SDK's protocol version to the network's actual version. +/// let _ = ExtendedEpochInfo::fetch_current(&sdk).await?; +/// +/// // Count / group_by / having queries now encode at the ratcheted version. +/// # Ok(()) +/// # } +/// ``` pub const DEFAULT_INITIAL_PROTOCOL_VERSION: u32 = dpp::version::v10::PROTOCOL_VERSION_10; /// The default metadata time tolerance for checkpoint queries in milliseconds const ADDRESS_STATE_TIME_TOLERANCE_MS: u64 = 31 * 60 * 1000; @@ -1706,11 +1740,18 @@ mod test { let floor = super::DEFAULT_INITIAL_PROTOCOL_VERSION; assert_eq!(sdk.protocol_version_number(), floor); - // Ratchet up: a newer network version raises the floor. - sdk.maybe_update_protocol_version(floor + 2); + // Ratchet up to a known higher version (PV12). Using a fixed known + // target rather than `floor + N` keeps the test correct as the floor + // advances; `maybe_update_protocol_version` only accepts known versions. + let target = dpp::version::v12::PROTOCOL_VERSION_12; + assert!( + target > floor, + "ratchet test target must exceed the floor; bump it if the floor reaches v12" + ); + sdk.maybe_update_protocol_version(target); assert_eq!( sdk.protocol_version_number(), - floor + 2, + target, "auto-detect must ratchet upward from the floor" ); @@ -1718,7 +1759,7 @@ mod test { sdk.maybe_update_protocol_version(floor - 1); assert_eq!( sdk.protocol_version_number(), - floor + 2, + target, "ratchet must never downgrade below the highest observed version" ); } diff --git a/packages/rs-sdk/tests/fetch/document_count.rs b/packages/rs-sdk/tests/fetch/document_count.rs index 68d3c238d56..5caeb5a2d47 100644 --- a/packages/rs-sdk/tests/fetch/document_count.rs +++ b/packages/rs-sdk/tests/fetch/document_count.rs @@ -28,24 +28,44 @@ //! `DocumentSplitCounts`), each `expect_fetch` call carries an //! explicit turbofish so the mock recorder knows which response //! type to register. +//! +//! Count / `group_by` are v3.1+-only query surfaces (V1 documents +//! wire), so these tests build the mock SDK pinned to a v3.1+ initial +//! protocol version via [`count_capable_mock_sdk`]. An unpinned SDK +//! defaults to the upgrade-safe floor (V0 wire), under which the local +//! encoder rejects these queries before any network call. use std::sync::Arc; use super::common::{mock_data_contract, mock_document_type}; use dash_sdk::{ platform::{documents::document_query::DocumentQuery, Fetch}, - Sdk, + Sdk, SdkBuilder, }; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::platform_value::Value; +use dpp::version::PlatformVersion; use drive::query::conditions::{WhereClause, WhereOperator}; use drive::query::ordering::OrderClause; use drive::query::SelectProjection; use drive_proof_verifier::{DocumentCount, DocumentSplitCounts, SplitCountEntry}; +/// Build a mock SDK whose initial protocol version is pinned to PV12 (the +/// first release wiring `DRIVE_ABCI_QUERY_VERSIONS_V1`, i.e. the V1 documents +/// wire) so Count / `group_by` queries encode. Uses `with_initial_version` +/// (keeps auto-detect) rather than `with_version`. +fn count_capable_mock_sdk() -> Sdk { + let pv = PlatformVersion::get(dpp::version::v12::PROTOCOL_VERSION_12) + .expect("PROTOCOL_VERSION_12 is a known version"); + SdkBuilder::new_mock() + .with_initial_version(pv) + .build() + .expect("mock Sdk should be created") +} + #[tokio::test] async fn test_mock_fetch_document_count_returns_expected() { - let mut sdk = Sdk::new_mock(); + let mut sdk = count_capable_mock_sdk(); let document_type = mock_document_type(); let data_contract = mock_data_contract(Some(&document_type)); @@ -71,7 +91,7 @@ async fn test_mock_fetch_document_count_returns_expected() { #[tokio::test] async fn test_mock_fetch_document_count_zero() { - let mut sdk = Sdk::new_mock(); + let mut sdk = count_capable_mock_sdk(); let document_type = mock_document_type(); let data_contract = mock_data_contract(Some(&document_type)); @@ -96,7 +116,7 @@ async fn test_mock_fetch_document_count_zero() { #[tokio::test] async fn test_mock_fetch_document_count_not_found() { - let mut sdk = Sdk::new_mock(); + let mut sdk = count_capable_mock_sdk(); let document_type = mock_document_type(); let data_contract = mock_data_contract(Some(&document_type)); @@ -126,7 +146,7 @@ async fn test_mock_fetch_document_count_not_found() { /// per-value shape. #[tokio::test] async fn test_mock_fetch_document_split_counts_with_in_clause() { - let mut sdk = Sdk::new_mock(); + let mut sdk = count_capable_mock_sdk(); let document_type = mock_document_type(); let data_contract = mock_data_contract(Some(&document_type)); @@ -178,7 +198,7 @@ async fn test_mock_fetch_document_split_counts_with_in_clause() { /// requests to the server's `RangeDistinctProof` dispatch. #[tokio::test] async fn test_mock_fetch_document_split_counts_with_distinct_range() { - let mut sdk = Sdk::new_mock(); + let mut sdk = count_capable_mock_sdk(); let document_type = mock_document_type(); let data_contract = mock_data_contract(Some(&document_type)); @@ -236,7 +256,7 @@ async fn test_mock_fetch_document_split_counts_with_distinct_range() { /// the dispatch even when the caller asks for a single `u64`. #[tokio::test] async fn test_mock_fetch_document_count_with_distinct_range_sums_entries() { - let mut sdk = Sdk::new_mock(); + let mut sdk = count_capable_mock_sdk(); let document_type = mock_document_type(); let data_contract = mock_data_contract(Some(&document_type)); @@ -278,7 +298,7 @@ async fn test_mock_fetch_document_count_with_distinct_range_sums_entries() { /// entries silently would fail here. #[tokio::test] async fn test_mock_fetch_document_split_counts_preserves_none_for_absent_in_values() { - let mut sdk = Sdk::new_mock(); + let mut sdk = count_capable_mock_sdk(); let document_type = mock_document_type(); let data_contract = mock_data_contract(Some(&document_type)); diff --git a/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs b/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs index 28081a70a03..577cb3e85b6 100644 --- a/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs +++ b/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs @@ -28,6 +28,7 @@ use std::sync::Arc; use super::common::{mock_data_contract, mock_document_type}; use dapi_grpc::platform::v0::get_documents_request::Version as ReqVersion; use dapi_grpc::platform::v0::GetDocumentsRequest; +use dash_sdk::sdk::DEFAULT_INITIAL_PROTOCOL_VERSION; use dash_sdk::{platform::documents::document_query::DocumentQuery, Error as SdkError, SdkBuilder}; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::platform_value::Value; @@ -221,12 +222,13 @@ fn encoder_dispatches_v0_via_query_settings_without_sdk() { #[test] fn sdk_builder_with_initial_version_seeds_atomic_without_pinning() { // Auto-detect default: the atomic seeds to `self.version` (which - // defaults to `latest()`). `version()` therefore returns `latest()` - // until the first response ratchets the atomic upward. + // defaults to the upgrade-safe floor `DEFAULT_INITIAL_PROTOCOL_VERSION`). + // `version()` therefore returns the floor until the first response + // ratchets the atomic upward. let sdk_default = SdkBuilder::new_mock().build().expect("mock sdk"); assert_eq!( sdk_default.version().protocol_version, - PlatformVersion::latest().protocol_version + DEFAULT_INITIAL_PROTOCOL_VERSION ); // `with_initial_version` seeds the atomic to the requested PV's diff --git a/packages/rs-sdk/tests/fetch/mock_fetch.rs b/packages/rs-sdk/tests/fetch/mock_fetch.rs index 1b96614ecd4..700d630fdd1 100644 --- a/packages/rs-sdk/tests/fetch/mock_fetch.rs +++ b/packages/rs-sdk/tests/fetch/mock_fetch.rs @@ -3,7 +3,7 @@ use super::common::{mock_data_contract, mock_document_type}; use dash_sdk::{ platform::{DocumentQuery, Fetch}, - Sdk, + Sdk, SdkBuilder, }; use dpp::{ data_contract::{ @@ -90,7 +90,16 @@ async fn test_mock_fetch_identity_not_found() { /// Given some data contract, when I fetch it by ID, I get it. #[tokio::test] async fn test_mock_fetch_data_contract() { - let mut sdk = Sdk::new_mock(); + // `mock_data_contract` builds V2 document types (v3.1+ semantics), so the + // SDK must decode the round-trip at a v3.1+ initial version (PV12) for the + // deserialized contract to match. An unpinned SDK defaults to the V0 floor + // and would downgrade the document type to V1. + let pv = PlatformVersion::get(dpp::version::v12::PROTOCOL_VERSION_12) + .expect("PROTOCOL_VERSION_12 is a known version"); + let mut sdk = SdkBuilder::new_mock() + .with_initial_version(pv) + .build() + .expect("mock Sdk should be created"); let document_type: DocumentType = mock_document_type(); let expected = mock_data_contract(Some(&document_type)); From 5b28a78cf99855427b30f70e701887733f1fa2bb Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:36:48 +0200 Subject: [PATCH 4/7] docs(sdk): condense protocol-version-floor comments on PR #3809 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comments/docs only — zero behavior change. Tightens the verbose inline and doc comments PR #3809 introduced, per the "fewer tokens, same signal" convention. - DEFAULT_INITIAL_PROTOCOL_VERSION rustdoc: trim the no_run doctest to a minimal compiling snippet and keep the load-bearing facts (floor < latest + bump note; the V0-floor Error::Config rejection of Count/group_by/having before any network call; the two ratchet options). - State the v3.1+ query-surface caveat ONCE in the constant; reduce the document_count module header and count_capable_mock_sdk helper doc to one-line pointers. - Collapse the new sdk.rs unit-test comments and the three test-file inline comments to single tight sentences, preserving the PV12-as-fixed-known- target and V2-type-decode rationale. Resolves two thepastaclaw review nitpicks in the same pass: - Delete the stale `new_mainnet(addrs)` commented alternative in the doctest (new_mainnet/new_testnet are now parameterless). - Replace the overstated "any normal query works at the floor" with the precise "one floor-compatible ratcheting query (no v3.1+ surfaces)". cargo fmt, cargo test --doc, and cargo build --tests for dash-sdk all pass. Co-Authored-By: Claude Opus 4.6 --- packages/rs-sdk/src/sdk.rs | 54 +++++++------------ packages/rs-sdk/tests/fetch/document_count.rs | 14 ++--- .../tests/fetch/document_query_v0_v1.rs | 7 ++- packages/rs-sdk/tests/fetch/mock_fetch.rs | 6 +-- 4 files changed, 30 insertions(+), 51 deletions(-) diff --git a/packages/rs-sdk/src/sdk.rs b/packages/rs-sdk/src/sdk.rs index 474d2c8665b..50010bfd5ff 100644 --- a/packages/rs-sdk/src/sdk.rs +++ b/packages/rs-sdk/src/sdk.rs @@ -50,45 +50,34 @@ pub const DEFAULT_CONTRACT_CACHE_SIZE: usize = 100; pub const DEFAULT_TOKEN_CONFIG_CACHE_SIZE: usize = 100; /// How many quorum public keys fit in the cache. pub const DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE: usize = 100; -/// Default initial protocol version used when the caller pins neither an explicit -/// [`PlatformVersion`] (via [`SdkBuilder::with_version`]) nor an explicit initial -/// protocol version (via [`SdkBuilder::with_initial_version`]). +/// Initial protocol version when the caller pins neither a [`PlatformVersion`] +/// (via [`SdkBuilder::with_version`]) nor an initial version (via +/// [`SdkBuilder::with_initial_version`]). /// -/// Deliberately set BELOW the latest known version: ratchet-up autodetection -/// (`maybe_update_protocol_version`) converges upward to the network's real version, -/// and starting low keeps requests compatible with not-yet-upgraded nodes during an -/// upgrade window. Bump this constant when the network's supported floor advances. +/// Set BELOW the latest version on purpose: ratchet-up autodetection +/// (`maybe_update_protocol_version`) converges to the network's real version, +/// so starting low keeps requests compatible with not-yet-upgraded nodes during +/// an upgrade window. Bump this constant as the network's supported floor advances. /// /// # v3.1+-only query surfaces /// -/// An unpinned SDK starts at this upgrade-safe floor (PV10, V0 documents wire). -/// Under V0 the local query encoder rejects the v3.1+-only surfaces — `Count` -/// (`SelectProjection::count_star`), `group_by`, and `having` — with an -/// [`Error::Config`] *before* any network round-trip. To use them, either: -/// -/// 1. Pin a higher initial version at build time via -/// [`SdkBuilder::with_initial_version`], or -/// 2. Issue one ratcheting query right after `build()`. Any normal query works -/// at the floor and its response metadata ratchets the SDK's protocol version -/// up to the network's actual version; subsequent `Count` / `group_by` / -/// `having` queries then encode correctly. -/// -/// Example of option 2 — a parameterless current-state fetch warms up the ratchet: +/// At this floor (PV10, V0 documents wire) the local encoder rejects the +/// v3.1+-only surfaces — `Count` (`SelectProjection::count_star`), `group_by`, +/// and `having` — with [`Error::Config`] *before* any network round-trip. To use +/// them either pin a higher initial version via [`SdkBuilder::with_initial_version`], +/// or issue one floor-compatible ratcheting query (no v3.1+ surfaces) right after +/// `build()` — e.g. the `ExtendedEpochInfo::fetch_current` current-state fetch below. +/// Its response metadata lifts the SDK to the network's version, after which `Count` / +/// `group_by` / `having` encode correctly. /// /// ```no_run /// # use dash_sdk::{Sdk, SdkBuilder}; /// # use dash_sdk::platform::fetch_current_no_parameters::FetchCurrent; /// # use dpp::block::extended_epoch_info::ExtendedEpochInfo; /// # async fn warm_up() -> Result<(), dash_sdk::Error> { -/// // Option 1 alternative: SdkBuilder::new_mainnet(addrs) -/// // .with_initial_version(dpp::version::PlatformVersion::latest()).build()?; /// let sdk: Sdk = SdkBuilder::new_mock().build()?; -/// -/// // One ratcheting query: works at the floor, returns metadata, and lifts the -/// // SDK's protocol version to the network's actual version. +/// // Ratchets the SDK up to the network's version; Count/group_by/having then encode. /// let _ = ExtendedEpochInfo::fetch_current(&sdk).await?; -/// -/// // Count / group_by / having queries now encode at the ratcheted version. /// # Ok(()) /// # } /// ``` @@ -1711,8 +1700,7 @@ mod test { #[test] fn test_default_builder_seeds_initial_protocol_version_floor() { - // A default builder (no with_version / with_initial_version) must seed the - // SDK at DEFAULT_INITIAL_PROTOCOL_VERSION, not at PlatformVersion::latest(). + // A default builder must seed the SDK at the floor, not latest(). let sdk = SdkBuilder::new_mock() .build() .expect("mock Sdk should be created"); @@ -1740,9 +1728,8 @@ mod test { let floor = super::DEFAULT_INITIAL_PROTOCOL_VERSION; assert_eq!(sdk.protocol_version_number(), floor); - // Ratchet up to a known higher version (PV12). Using a fixed known - // target rather than `floor + N` keeps the test correct as the floor - // advances; `maybe_update_protocol_version` only accepts known versions. + // Ratchet to a fixed known target (PV12), not `floor + N`: stays valid as the + // floor advances, and `maybe_update_protocol_version` only accepts known versions. let target = dpp::version::v12::PROTOCOL_VERSION_12; assert!( target > floor, @@ -1768,8 +1755,7 @@ mod test { fn test_explicit_pin_overrides_default_floor() { use dpp::version::PlatformVersion; - // Pin to a version that is deliberately different from the default floor so - // the override is observable regardless of where the floor is set. + // Pin off the floor so the override is observable wherever the floor sits. let pinned_number = super::DEFAULT_INITIAL_PROTOCOL_VERSION - 1; let pinned = PlatformVersion::get(pinned_number).expect("pinned PV exists"); let sdk = SdkBuilder::new_mock() diff --git a/packages/rs-sdk/tests/fetch/document_count.rs b/packages/rs-sdk/tests/fetch/document_count.rs index 5caeb5a2d47..e1f7150f346 100644 --- a/packages/rs-sdk/tests/fetch/document_count.rs +++ b/packages/rs-sdk/tests/fetch/document_count.rs @@ -29,11 +29,8 @@ //! explicit turbofish so the mock recorder knows which response //! type to register. //! -//! Count / `group_by` are v3.1+-only query surfaces (V1 documents -//! wire), so these tests build the mock SDK pinned to a v3.1+ initial -//! protocol version via [`count_capable_mock_sdk`]. An unpinned SDK -//! defaults to the upgrade-safe floor (V0 wire), under which the local -//! encoder rejects these queries before any network call. +//! Count / `group_by` need a v3.1+ initial version, so tests build via +//! [`count_capable_mock_sdk`] (see `DEFAULT_INITIAL_PROTOCOL_VERSION`). use std::sync::Arc; @@ -50,10 +47,9 @@ use drive::query::ordering::OrderClause; use drive::query::SelectProjection; use drive_proof_verifier::{DocumentCount, DocumentSplitCounts, SplitCountEntry}; -/// Build a mock SDK whose initial protocol version is pinned to PV12 (the -/// first release wiring `DRIVE_ABCI_QUERY_VERSIONS_V1`, i.e. the V1 documents -/// wire) so Count / `group_by` queries encode. Uses `with_initial_version` -/// (keeps auto-detect) rather than `with_version`. +/// Mock SDK seeded to PV12 — the first release wiring `DRIVE_ABCI_QUERY_VERSIONS_V1` +/// (V1 documents wire), so Count / `group_by` encode. Uses `with_initial_version`, +/// keeping auto-detect. fn count_capable_mock_sdk() -> Sdk { let pv = PlatformVersion::get(dpp::version::v12::PROTOCOL_VERSION_12) .expect("PROTOCOL_VERSION_12 is a known version"); diff --git a/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs b/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs index 577cb3e85b6..8939d167d82 100644 --- a/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs +++ b/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs @@ -221,10 +221,9 @@ fn encoder_dispatches_v0_via_query_settings_without_sdk() { #[test] fn sdk_builder_with_initial_version_seeds_atomic_without_pinning() { - // Auto-detect default: the atomic seeds to `self.version` (which - // defaults to the upgrade-safe floor `DEFAULT_INITIAL_PROTOCOL_VERSION`). - // `version()` therefore returns the floor until the first response - // ratchets the atomic upward. + // Auto-detect default: the atomic seeds to the floor + // `DEFAULT_INITIAL_PROTOCOL_VERSION`, which `version()` returns until the + // first response ratchets it upward. let sdk_default = SdkBuilder::new_mock().build().expect("mock sdk"); assert_eq!( sdk_default.version().protocol_version, diff --git a/packages/rs-sdk/tests/fetch/mock_fetch.rs b/packages/rs-sdk/tests/fetch/mock_fetch.rs index 700d630fdd1..302d2d2e77d 100644 --- a/packages/rs-sdk/tests/fetch/mock_fetch.rs +++ b/packages/rs-sdk/tests/fetch/mock_fetch.rs @@ -90,10 +90,8 @@ async fn test_mock_fetch_identity_not_found() { /// Given some data contract, when I fetch it by ID, I get it. #[tokio::test] async fn test_mock_fetch_data_contract() { - // `mock_data_contract` builds V2 document types (v3.1+ semantics), so the - // SDK must decode the round-trip at a v3.1+ initial version (PV12) for the - // deserialized contract to match. An unpinned SDK defaults to the V0 floor - // and would downgrade the document type to V1. + // `mock_data_contract` builds V2 document types, so the round-trip must decode + // at PV12; the unpinned V0 floor would downgrade the type to V1 and mismatch. let pv = PlatformVersion::get(dpp::version::v12::PROTOCOL_VERSION_12) .expect("PROTOCOL_VERSION_12 is a known version"); let mut sdk = SdkBuilder::new_mock() From ea76193c56821174472ef9e900d00d9103f609c2 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:56:23 +0200 Subject: [PATCH 5/7] docs(sdk): align floor-contract docs with PV10 default (CodeRabbit) Reword DEFAULT_INITIAL_PROTOCOL_VERSION, with_initial_version, and the rs-sdk-ffi platform_version field docs to describe the floor relative to the default initial protocol version rather than baking in a literal PV number that goes stale on the next floor bump. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/rs-sdk-ffi/src/types.rs | 6 ++++-- packages/rs-sdk/src/sdk.rs | 11 +++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/rs-sdk-ffi/src/types.rs b/packages/rs-sdk-ffi/src/types.rs index efdaa4e45be..268fb40a32a 100644 --- a/packages/rs-sdk-ffi/src/types.rs +++ b/packages/rs-sdk-ffi/src/types.rs @@ -94,8 +94,10 @@ pub struct DashSDKConfig { /// immediately, caller may free after the FFI call returns. pub quorum_url: *const c_char, /// Pin to a specific Dash Platform protocol version. - /// `0` keeps the SDK default (auto-detect / latest); any non-zero value - /// is forwarded to `SdkBuilder::with_version` and rejected if unknown. + /// `0` keeps the SDK default — auto-detect seeded at the default initial + /// protocol-version floor, ratcheting up to the network's version; any + /// non-zero value is forwarded to `SdkBuilder::with_version` and rejected + /// if unknown. pub platform_version: u32, } diff --git a/packages/rs-sdk/src/sdk.rs b/packages/rs-sdk/src/sdk.rs index 50010bfd5ff..a854c88ef0e 100644 --- a/packages/rs-sdk/src/sdk.rs +++ b/packages/rs-sdk/src/sdk.rs @@ -61,7 +61,7 @@ pub const DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE: usize = 100; /// /// # v3.1+-only query surfaces /// -/// At this floor (PV10, V0 documents wire) the local encoder rejects the +/// At the default floor the local encoder rejects the /// v3.1+-only surfaces — `Count` (`SelectProjection::count_star`), `group_by`, /// and `having` — with [`Error::Config`] *before* any network round-trip. To use /// them either pin a higher initial version via [`SdkBuilder::with_initial_version`], @@ -930,11 +930,10 @@ impl SdkBuilder { /// (via `fetch_max` in `maybe_update_protocol_version`) once the /// network's actual version is observed in response metadata. /// - /// Use this when an SDK built against `PlatformVersion::latest()` - /// must talk to a network running an older protocol version (e.g. - /// a v3.0 testnet from a v3.1+ binary). Without an explicit initial - /// version, the SDK's `version()` fallback returns `latest()` until - /// the first response is parsed, and the upward-only `fetch_max` + /// Use this when the SDK must talk to a network running a protocol + /// version *older* than the default floor (e.g. a v3.0 testnet from a + /// v3.1+ binary). Without an explicit initial version, the SDK seeds to + /// [`DEFAULT_INITIAL_PROTOCOL_VERSION`], and the upward-only `fetch_max` /// guard can never ratchet *down* to the older network — leaving /// any version-dispatched encoders (e.g. the documents query) to /// ship a too-new wire shape that the network rejects. From bcf3b145353572402611f394be82f469e5bdcf95 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:35:38 +0200 Subject: [PATCH 6/7] refactor(sdk): hide with_initial_version(); pin count mock to PV12 via with_version(); add ratchet-ordering guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-detect is the default now, so with_initial_version() is no longer the public way to enable it. Demote it to a test-only seed (#[cfg(test)] pub(crate)) and repoint user-facing docs at with_version() (the opt-out that pins + disables auto-detect). The count mock tests short-circuit the wire verifier, so auto-detect never ratchets there — switch document_count.rs and mock_fetch.rs from with_initial_version() to with_version() to pin the fixed PV12 wire shape, and update the docs to say "pins" not "seeds". Add a guard comment at the verify_response_metadata/ratchet call site documenting the verify-before-ratchet security invariant, plus a unit test asserting the ratchet rejects unknown/zero/non-upward versions (the full tampered-signed-proof path is documented as out of unit-test scope). Co-Authored-By: Claude Opus 4.6 --- packages/rs-sdk/src/sdk.rs | 99 +++++++++++++------ packages/rs-sdk/tests/fetch/document_count.rs | 14 +-- .../tests/fetch/document_query_v0_v1.rs | 40 ++------ packages/rs-sdk/tests/fetch/mock_fetch.rs | 5 +- 4 files changed, 89 insertions(+), 69 deletions(-) diff --git a/packages/rs-sdk/src/sdk.rs b/packages/rs-sdk/src/sdk.rs index d595ad0d9d5..05fdfe35d8a 100644 --- a/packages/rs-sdk/src/sdk.rs +++ b/packages/rs-sdk/src/sdk.rs @@ -50,9 +50,8 @@ pub const DEFAULT_CONTRACT_CACHE_SIZE: usize = 100; pub const DEFAULT_TOKEN_CONFIG_CACHE_SIZE: usize = 100; /// How many quorum public keys fit in the cache. pub const DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE: usize = 100; -/// Initial protocol version when the caller pins neither a [`PlatformVersion`] -/// (via [`SdkBuilder::with_version`]) nor an initial version (via -/// [`SdkBuilder::with_initial_version`]). +/// Initial protocol version for the default auto-detect mode — i.e. when the +/// caller does not pin a [`PlatformVersion`] via [`SdkBuilder::with_version`]. /// /// Set BELOW the latest version on purpose: ratchet-up autodetection /// (`maybe_update_protocol_version`) converges to the network's real version, @@ -64,9 +63,10 @@ pub const DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE: usize = 100; /// At the default floor the local encoder rejects the /// v3.1+-only surfaces — `Count` (`SelectProjection::count_star`), `group_by`, /// and `having` — with [`Error::Config`] *before* any network round-trip. To use -/// them either pin a higher initial version via [`SdkBuilder::with_initial_version`], -/// or issue one floor-compatible ratcheting query (no v3.1+ surfaces) right after -/// `build()` — e.g. the `ExtendedEpochInfo::fetch_current` current-state fetch below. +/// them either pin a higher version via [`SdkBuilder::with_version`] (which also +/// disables auto-detect), or issue one floor-compatible ratcheting query (no v3.1+ +/// surfaces) right after `build()` — e.g. the `ExtendedEpochInfo::fetch_current` +/// current-state fetch below. /// Its response metadata lifts the SDK to the network's version, after which `Count` / /// `group_by` / `having` encode correctly. /// @@ -367,7 +367,6 @@ impl Sdk { } } - // TODO: Changed to public for tests /// Retrieve object `O` from proof contained in `request` (of type `R`) and `response`. /// /// This method is used to retrieve objects from proofs returned by Dash Platform. @@ -420,6 +419,10 @@ impl Sdk { } }?; + // Security invariant: proof+signature verification above (the `?`) must + // precede this call, which ratchets the protocol version from the now-trusted + // `metadata.protocol_version`. Never reorder — the ratchet must not consume + // unverified metadata. self.verify_response_metadata(method_name, &metadata) .inspect_err(|err| { tracing::warn!(%err,method=method_name,"received response with stale metadata; try another server"); @@ -920,31 +923,20 @@ impl SdkBuilder { self } - /// Set the *initial* protocol version seed for auto-detect mode. + /// Test-only seed for the auto-detect atomic — NOT the public way to enable + /// auto-detect (auto-detect is the default; [`Self::with_version`] is the opt-out). /// - /// Unlike [`Self::with_version`], this leaves auto-detect active — - /// the SDK starts at `version.protocol_version` and ratchets upward - /// (via `fetch_max` in `maybe_update_protocol_version`) once the - /// network's actual version is observed in response metadata. + /// Auto-detect already starts every unpinned SDK at + /// [`DEFAULT_INITIAL_PROTOCOL_VERSION`] and ratchets upward via `fetch_max` in + /// `maybe_update_protocol_version` once the network's version is observed. This + /// seed exists only to let unit tests start *below* that floor — exercising the + /// upward-only ratchet from an older network's version without disabling auto-detect. /// - /// Use this when the SDK must talk to a network running a protocol - /// version *older* than the default floor (e.g. a v3.0 testnet from a - /// v3.1+ binary). Without an explicit initial version, the SDK seeds to - /// [`DEFAULT_INITIAL_PROTOCOL_VERSION`], and the upward-only `fetch_max` - /// guard can never ratchet *down* to the older network — leaving - /// any version-dispatched encoders (e.g. the documents query) to - /// ship a too-new wire shape that the network rejects. - /// - /// Seeds `self.version` and resets `version_explicit` to `false`, so - /// auto-detect is (re-)enabled. Builder chains use last-write-wins: - /// calling `with_initial_version` after `with_version` restores - /// auto-detect rather than silently keeping it disabled. - /// - /// **Caveat**: this protection only holds for encoders whose - /// `drive_abci.query..default_current_version` is correctly pinned per - /// historical PV. New versioned encoders must follow the same per-PV pinning - /// pattern as `document_query`. - pub fn with_initial_version(mut self, version: &'static PlatformVersion) -> Self { + /// Seeds `self.version` and keeps `version_explicit` `false`, so auto-detect stays + /// on. Builder chains are last-write-wins: a later `with_initial_version` re-enables + /// auto-detect that an earlier `with_version` disabled. + #[cfg(test)] + pub(crate) fn with_initial_version(mut self, version: &'static PlatformVersion) -> Self { self.version = version; self.version_explicit = false; self @@ -1747,6 +1739,53 @@ mod test { ); } + /// Regression guard for the verify-before-ratchet security invariant. + /// + /// The full tampered-*signed*-proof path isn't unit-testable here: it needs a + /// quorum BLS signature, a context provider, and a `FromProof` verifier round-trip. + /// That path's safety rests on `parse_proof_with_metadata_and_proof` running proof + /// verification (the `?`) BEFORE `verify_response_metadata` → `maybe_update_protocol_version` + /// (see the guard comment at that call site). Here we lock in the ratchet's own gates: + /// it must NOT raise the stored version off untrustworthy inputs (unknown / zero / lower), + /// so even a metadata value that slipped past verification can't move the SDK to a bogus + /// protocol version. + #[test] + fn test_ratchet_rejects_unknown_and_non_upward_versions() { + let sdk = SdkBuilder::new_mock() + .build() + .expect("mock Sdk should be created"); + let floor = super::DEFAULT_INITIAL_PROTOCOL_VERSION; + assert_eq!(sdk.protocol_version_number(), floor); + + // Unknown (above LATEST_VERSION): rejected, version unchanged. + sdk.maybe_update_protocol_version(dpp::version::LATEST_VERSION + 1); + assert_eq!( + sdk.protocol_version_number(), + floor, + "unknown protocol version must not move the stored version" + ); + + // Zero (e.g. metadata default / stripped field): ignored. + sdk.maybe_update_protocol_version(0); + assert_eq!( + sdk.protocol_version_number(), + floor, + "zero protocol version must be ignored" + ); + + // Equal: no-op (no spurious downgrade or churn). + sdk.maybe_update_protocol_version(floor); + assert_eq!(sdk.protocol_version_number(), floor); + + // Lower known version: ignored by the upward-only guard. + sdk.maybe_update_protocol_version(floor - 1); + assert_eq!( + sdk.protocol_version_number(), + floor, + "lower known version must not downgrade the stored version" + ); + } + #[test] fn test_explicit_pin_overrides_default_floor() { use dpp::version::PlatformVersion; diff --git a/packages/rs-sdk/tests/fetch/document_count.rs b/packages/rs-sdk/tests/fetch/document_count.rs index e1f7150f346..c7d88824d1e 100644 --- a/packages/rs-sdk/tests/fetch/document_count.rs +++ b/packages/rs-sdk/tests/fetch/document_count.rs @@ -29,8 +29,9 @@ //! explicit turbofish so the mock recorder knows which response //! type to register. //! -//! Count / `group_by` need a v3.1+ initial version, so tests build via -//! [`count_capable_mock_sdk`] (see `DEFAULT_INITIAL_PROTOCOL_VERSION`). +//! Count / `group_by` need a v3.1+ version, so tests build via +//! [`count_capable_mock_sdk`], which pins PV12 (the mock short-circuits the +//! verifier, so auto-detect never ratchets these tests on its own). use std::sync::Arc; @@ -47,14 +48,15 @@ use drive::query::ordering::OrderClause; use drive::query::SelectProjection; use drive_proof_verifier::{DocumentCount, DocumentSplitCounts, SplitCountEntry}; -/// Mock SDK seeded to PV12 — the first release wiring `DRIVE_ABCI_QUERY_VERSIONS_V1` -/// (V1 documents wire), so Count / `group_by` encode. Uses `with_initial_version`, -/// keeping auto-detect. +/// Mock SDK pinned to PV12 — the first release wiring `DRIVE_ABCI_QUERY_VERSIONS_V1` +/// (V1 documents wire), so Count / `group_by` encode. The mock transport short-circuits +/// the wire verifier, so auto-detect never ratchets here; `with_version` pins the fixed +/// wire version this test needs. fn count_capable_mock_sdk() -> Sdk { let pv = PlatformVersion::get(dpp::version::v12::PROTOCOL_VERSION_12) .expect("PROTOCOL_VERSION_12 is a known version"); SdkBuilder::new_mock() - .with_initial_version(pv) + .with_version(pv) .build() .expect("mock Sdk should be created") } diff --git a/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs b/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs index 8939d167d82..eb139437649 100644 --- a/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs +++ b/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs @@ -17,11 +17,10 @@ //! - Dispatch by SDK version: a `DocumentQuery` whose //! `protocol_version_override` field points at a V0 PlatformVersion //! round-trips through `TryFrom` as V0; default falls back to V1. -//! - `SdkBuilder::with_initial_version` semantics: builder seeds the -//! per-instance protocol_version atomic to the requested value -//! without flipping `version_explicit`, so auto-detect remains -//! active and `maybe_update_protocol_version` can still ratchet -//! upward via `fetch_max`. +//! +//! Builder seeding semantics (auto-detect default vs. the internal +//! `with_initial_version` seed) are covered by the in-crate unit tests in +//! `dash_sdk::sdk`. use std::sync::Arc; @@ -220,7 +219,7 @@ fn encoder_dispatches_v0_via_query_settings_without_sdk() { } #[test] -fn sdk_builder_with_initial_version_seeds_atomic_without_pinning() { +fn sdk_builder_default_seeds_atomic_to_floor() { // Auto-detect default: the atomic seeds to the floor // `DEFAULT_INITIAL_PROTOCOL_VERSION`, which `version()` returns until the // first response ratchets it upward. @@ -229,27 +228,6 @@ fn sdk_builder_with_initial_version_seeds_atomic_without_pinning() { sdk_default.version().protocol_version, DEFAULT_INITIAL_PROTOCOL_VERSION ); - - // `with_initial_version` seeds the atomic to the requested PV's - // protocol_version. Auto-detect REMAINS on (this is the contract - // distinguishing it from `with_version`): a future - // `maybe_update_protocol_version(higher)` call would ratchet - // upward via `fetch_max`. - let pv_first = PlatformVersion::get(1).expect("v1 is always known"); - let sdk_initial = SdkBuilder::new_mock() - .with_initial_version(pv_first) - .build() - .expect("mock sdk with initial version"); - assert_eq!( - sdk_initial.protocol_version_number(), - pv_first.protocol_version - ); - // `version()` reflects the seeded value — no auto-detect bump - // has occurred yet (no network responses parsed). - assert_eq!( - sdk_initial.version().protocol_version, - pv_first.protocol_version - ); } /// PROTOCOL_VERSION_11 corresponds to Dash Platform v3.0 (testnet at the @@ -278,10 +256,10 @@ fn protocol_version_for_v3_1_dev_keeps_document_query_v1() { assert_eq!(pv.drive_abci.query.document_query.max_version, 1); } -/// Wallet-team end-to-end shape: an SDK built with -/// `with_initial_version(PROTOCOL_VERSION_11)` (Dash Platform v3.0) must -/// dispatch to the V0 encoder — proving the full plumbing works -/// without monkey-patching `PlatformVersion::latest()` clones. +/// Wallet-team end-to-end shape: a query whose `QuerySettings.protocol_version` +/// is `PROTOCOL_VERSION_11` (Dash Platform v3.0) must dispatch to the V0 encoder — +/// proving the full plumbing works without monkey-patching +/// `PlatformVersion::latest()` clones. #[test] fn document_query_dispatches_v0_when_sdk_initial_version_is_v3_0_pv() { use dash_sdk::platform::{Query, QuerySettings}; diff --git a/packages/rs-sdk/tests/fetch/mock_fetch.rs b/packages/rs-sdk/tests/fetch/mock_fetch.rs index 302d2d2e77d..aea0d8e78f2 100644 --- a/packages/rs-sdk/tests/fetch/mock_fetch.rs +++ b/packages/rs-sdk/tests/fetch/mock_fetch.rs @@ -91,11 +91,12 @@ async fn test_mock_fetch_identity_not_found() { #[tokio::test] async fn test_mock_fetch_data_contract() { // `mock_data_contract` builds V2 document types, so the round-trip must decode - // at PV12; the unpinned V0 floor would downgrade the type to V1 and mismatch. + // at PV12; the unpinned default floor would downgrade the type and mismatch. The + // mock short-circuits the verifier, so `with_version` pins the fixed wire version. let pv = PlatformVersion::get(dpp::version::v12::PROTOCOL_VERSION_12) .expect("PROTOCOL_VERSION_12 is a known version"); let mut sdk = SdkBuilder::new_mock() - .with_initial_version(pv) + .with_version(pv) .build() .expect("mock Sdk should be created"); From 438ce907bcda80fdb5b0f11643d65f95d59e9863 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:33:45 +0200 Subject: [PATCH 7/7] refactor(sdk): bootstrap mock SDK to latest via proven fetch_current instead of with_version pin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure-mock tests that need the v3.1+/PV12 wire (Count / group_by in document_count.rs, V2 document types in mock_fetch.rs) used to hard-pin PV12 with `SdkBuilder::with_version(pv)`. Since the rs-sdk default boots at the PV10 floor and ratchets up only on proven response metadata, these tests now converge the same way production does: a cheap proven `ExtendedEpochInfo::fetch_current` ratchets the auto-detect SDK to latest before the real request encodes. To make the ratchet actually fire in mock mode, the mock's `parse_proof_with_metadata` now reports `LATEST_VERSION` for expectation hits instead of `ResponseMetadata::default()` (protocol_version = 0, which the ratchet ignores) — honestly simulating a latest-version network. A shared `bootstrap_mock_sdk_to_latest` helper in tests/fetch/common.rs registers and consumes the proven epoch expectation, asserting the SDK ends up at LATEST_VERSION. Both call sites reuse it. No production change: it already ratchets on real proven metadata. Full rs-sdk mock suite (main: 131 passed/4 ignored, lib: 139 passed) stays green with no other test needing adjustment. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/rs-sdk/src/mock/sdk.rs | 8 ++- packages/rs-sdk/tests/fetch/common.rs | 51 +++++++++++++++++++ packages/rs-sdk/tests/fetch/document_count.rs | 43 ++++++++-------- packages/rs-sdk/tests/fetch/mock_fetch.rs | 14 ++--- 4 files changed, 86 insertions(+), 30 deletions(-) diff --git a/packages/rs-sdk/src/mock/sdk.rs b/packages/rs-sdk/src/mock/sdk.rs index b9147e3bb48..397a30c417b 100644 --- a/packages/rs-sdk/src/mock/sdk.rs +++ b/packages/rs-sdk/src/mock/sdk.rs @@ -520,9 +520,15 @@ impl MockDashPlatformSdk { let key = Key::new(&request); let data = match self.from_proof_expectations.get(&key) { + // Report the latest protocol version so the proof path's ratchet + // (`maybe_update_protocol_version`) fires as it would against a real + // network; `default()` reports 0, which the ratchet ignores. Some(d) => ( Option::::mock_deserialize(self, d), - ResponseMetadata::default(), + ResponseMetadata { + protocol_version: dpp::version::LATEST_VERSION, + ..Default::default() + }, Proof::default(), ), None => { diff --git a/packages/rs-sdk/tests/fetch/common.rs b/packages/rs-sdk/tests/fetch/common.rs index 51e4fb4b3d0..5b2d4a49c17 100644 --- a/packages/rs-sdk/tests/fetch/common.rs +++ b/packages/rs-sdk/tests/fetch/common.rs @@ -1,8 +1,13 @@ +use dash_sdk::platform::fetch_current_no_parameters::FetchCurrent; +use dash_sdk::platform::types::epoch::EpochQuery; +use dash_sdk::platform::LimitQuery; use dash_sdk::{ mock::Mockable, platform::{Query, QuerySettings}, Sdk, }; +use dpp::block::extended_epoch_info::v0::{ExtendedEpochInfoV0, ExtendedEpochInfoV0Getters}; +use dpp::block::extended_epoch_info::ExtendedEpochInfo; use dpp::data_contract::config::DataContractConfig; use dpp::{data_contract::DataContractFactory, prelude::Identifier}; use hex::ToHex; @@ -98,6 +103,52 @@ pub fn mock_data_contract( .data_contract_owned() } +/// Ratchet a fresh auto-detect mock SDK from the protocol-version floor up to the +/// network's latest version, exactly as production does on its first proven response. +/// +/// An unpinned SDK boots at `DEFAULT_INITIAL_PROTOCOL_VERSION` (the upgrade-safe floor) +/// and only learns the real network version after a *proven* fetch, when response +/// metadata drives `maybe_update_protocol_version`. Mock tests that need the latest +/// wire (e.g. Count / `group_by`, or V2 document types) must therefore perform one +/// proven fetch before encoding their real request. This registers a cheap proven +/// `ExtendedEpochInfo::fetch_current` expectation and consumes it, leaving the SDK +/// ratcheted to `LATEST_VERSION`. +pub(crate) async fn bootstrap_mock_sdk_to_latest(sdk: &mut Sdk) { + let query = LimitQuery { + query: EpochQuery { + start: None, + ascending: false, + }, + limit: Some(1), + start_info: None, + }; + + let epoch = ExtendedEpochInfo::from(ExtendedEpochInfoV0 { + index: 0, + first_block_time: 0, + first_block_height: 0, + first_core_block_height: 0, + fee_multiplier_permille: 0, + protocol_version: dpp::version::LATEST_VERSION, + }); + + sdk.mock() + .expect_fetch::(query, Some(epoch.clone())) + .await + .expect("register epoch bootstrap expectation"); + + let fetched = ExtendedEpochInfo::fetch_current(sdk) + .await + .expect("bootstrap fetch_current should ratchet the SDK to latest"); + + assert_eq!(fetched.index(), epoch.index()); + assert_eq!( + sdk.version().protocol_version, + dpp::version::LATEST_VERSION, + "bootstrap must ratchet the auto-detect SDK to the network's latest protocol version" + ); +} + /// Enable logging for tests pub fn setup_logs() { let make_writer = if should_emit_test_logs_to_stdout() { diff --git a/packages/rs-sdk/tests/fetch/document_count.rs b/packages/rs-sdk/tests/fetch/document_count.rs index c7d88824d1e..02012630300 100644 --- a/packages/rs-sdk/tests/fetch/document_count.rs +++ b/packages/rs-sdk/tests/fetch/document_count.rs @@ -29,41 +29,40 @@ //! explicit turbofish so the mock recorder knows which response //! type to register. //! -//! Count / `group_by` need a v3.1+ version, so tests build via -//! [`count_capable_mock_sdk`], which pins PV12 (the mock short-circuits the -//! verifier, so auto-detect never ratchets these tests on its own). +//! Count / `group_by` need the latest (v3.1+) wire, so tests build via +//! [`count_capable_mock_sdk`], which boots a plain auto-detect mock SDK and then +//! ratchets it up via a cheap proven `fetch_current` — exactly as production +//! converges to the network's real protocol version on its first proven response. use std::sync::Arc; -use super::common::{mock_data_contract, mock_document_type}; +use super::common::{bootstrap_mock_sdk_to_latest, mock_data_contract, mock_document_type}; use dash_sdk::{ platform::{documents::document_query::DocumentQuery, Fetch}, Sdk, SdkBuilder, }; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::platform_value::Value; -use dpp::version::PlatformVersion; use drive::query::conditions::{WhereClause, WhereOperator}; use drive::query::ordering::OrderClause; use drive::query::SelectProjection; use drive_proof_verifier::{DocumentCount, DocumentSplitCounts, SplitCountEntry}; -/// Mock SDK pinned to PV12 — the first release wiring `DRIVE_ABCI_QUERY_VERSIONS_V1` -/// (V1 documents wire), so Count / `group_by` encode. The mock transport short-circuits -/// the wire verifier, so auto-detect never ratchets here; `with_version` pins the fixed -/// wire version this test needs. -fn count_capable_mock_sdk() -> Sdk { - let pv = PlatformVersion::get(dpp::version::v12::PROTOCOL_VERSION_12) - .expect("PROTOCOL_VERSION_12 is a known version"); - SdkBuilder::new_mock() - .with_version(pv) +/// Auto-detect mock SDK ratcheted to the latest protocol version (the first release +/// wiring `DRIVE_ABCI_QUERY_VERSIONS_V1`, so Count / `group_by` encode). The mock +/// short-circuits the wire verifier, so the proven `fetch_current` bootstrap is what +/// teaches the SDK the network version — no fixed pin needed. +async fn count_capable_mock_sdk() -> Sdk { + let mut sdk = SdkBuilder::new_mock() .build() - .expect("mock Sdk should be created") + .expect("mock Sdk should be created"); + bootstrap_mock_sdk_to_latest(&mut sdk).await; + sdk } #[tokio::test] async fn test_mock_fetch_document_count_returns_expected() { - let mut sdk = count_capable_mock_sdk(); + let mut sdk = count_capable_mock_sdk().await; let document_type = mock_document_type(); let data_contract = mock_data_contract(Some(&document_type)); @@ -89,7 +88,7 @@ async fn test_mock_fetch_document_count_returns_expected() { #[tokio::test] async fn test_mock_fetch_document_count_zero() { - let mut sdk = count_capable_mock_sdk(); + let mut sdk = count_capable_mock_sdk().await; let document_type = mock_document_type(); let data_contract = mock_data_contract(Some(&document_type)); @@ -114,7 +113,7 @@ async fn test_mock_fetch_document_count_zero() { #[tokio::test] async fn test_mock_fetch_document_count_not_found() { - let mut sdk = count_capable_mock_sdk(); + let mut sdk = count_capable_mock_sdk().await; let document_type = mock_document_type(); let data_contract = mock_data_contract(Some(&document_type)); @@ -144,7 +143,7 @@ async fn test_mock_fetch_document_count_not_found() { /// per-value shape. #[tokio::test] async fn test_mock_fetch_document_split_counts_with_in_clause() { - let mut sdk = count_capable_mock_sdk(); + let mut sdk = count_capable_mock_sdk().await; let document_type = mock_document_type(); let data_contract = mock_data_contract(Some(&document_type)); @@ -196,7 +195,7 @@ async fn test_mock_fetch_document_split_counts_with_in_clause() { /// requests to the server's `RangeDistinctProof` dispatch. #[tokio::test] async fn test_mock_fetch_document_split_counts_with_distinct_range() { - let mut sdk = count_capable_mock_sdk(); + let mut sdk = count_capable_mock_sdk().await; let document_type = mock_document_type(); let data_contract = mock_data_contract(Some(&document_type)); @@ -254,7 +253,7 @@ async fn test_mock_fetch_document_split_counts_with_distinct_range() { /// the dispatch even when the caller asks for a single `u64`. #[tokio::test] async fn test_mock_fetch_document_count_with_distinct_range_sums_entries() { - let mut sdk = count_capable_mock_sdk(); + let mut sdk = count_capable_mock_sdk().await; let document_type = mock_document_type(); let data_contract = mock_data_contract(Some(&document_type)); @@ -296,7 +295,7 @@ async fn test_mock_fetch_document_count_with_distinct_range_sums_entries() { /// entries silently would fail here. #[tokio::test] async fn test_mock_fetch_document_split_counts_preserves_none_for_absent_in_values() { - let mut sdk = count_capable_mock_sdk(); + let mut sdk = count_capable_mock_sdk().await; let document_type = mock_document_type(); let data_contract = mock_data_contract(Some(&document_type)); diff --git a/packages/rs-sdk/tests/fetch/mock_fetch.rs b/packages/rs-sdk/tests/fetch/mock_fetch.rs index aea0d8e78f2..a8c98b4d575 100644 --- a/packages/rs-sdk/tests/fetch/mock_fetch.rs +++ b/packages/rs-sdk/tests/fetch/mock_fetch.rs @@ -1,6 +1,6 @@ //! Tests of mocked Fetch trait implementations. -use super::common::{mock_data_contract, mock_document_type}; +use super::common::{bootstrap_mock_sdk_to_latest, mock_data_contract, mock_document_type}; use dash_sdk::{ platform::{DocumentQuery, Fetch}, Sdk, SdkBuilder, @@ -90,15 +90,15 @@ async fn test_mock_fetch_identity_not_found() { /// Given some data contract, when I fetch it by ID, I get it. #[tokio::test] async fn test_mock_fetch_data_contract() { - // `mock_data_contract` builds V2 document types, so the round-trip must decode - // at PV12; the unpinned default floor would downgrade the type and mismatch. The - // mock short-circuits the verifier, so `with_version` pins the fixed wire version. - let pv = PlatformVersion::get(dpp::version::v12::PROTOCOL_VERSION_12) - .expect("PROTOCOL_VERSION_12 is a known version"); + // `mock_data_contract` builds V2 document types that only decode at the latest + // protocol version; the unpinned default floor would downgrade the type and + // mismatch. A proven `fetch_current` bootstrap ratchets the auto-detect SDK up + // to the network's latest version before the data contract round-trips, exactly + // as production converges. let mut sdk = SdkBuilder::new_mock() - .with_version(pv) .build() .expect("mock Sdk should be created"); + bootstrap_mock_sdk_to_latest(&mut sdk).await; let document_type: DocumentType = mock_document_type(); let expected = mock_data_contract(Some(&document_type));