Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions packages/rs-sdk-ffi/src/protocol_version/queries/refresh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ use std::ffi::CString;

/// Refresh the SDK's protocol version from the connected network.
///
/// Issues an unproved `getStatus` against the network and ratchets this SDK's
/// auto-detected protocol version up to the network's current Drive protocol
/// version (see [`dash_sdk::Sdk::refresh_protocol_version`]). The resulting
/// Issues an ordinary **proven** `getEpochsInfo` query and ratchets this SDK's
/// auto-detected protocol version up to the network's version through the same
/// proof + quorum-signature-verified path every other query uses — no unverified
/// fallback (see [`dash_sdk::Sdk::refresh_protocol_version`]). The resulting
/// protocol version number propagates to every clone of this SDK — including
/// the `Sdk` clone held by a `PlatformWalletManager` — because the version is
/// stored in a shared `Arc<AtomicU32>`.
Expand All @@ -16,6 +17,10 @@ use std::ffi::CString;
/// flows (shielded pool shield/unshield/transfer/withdraw) reserve against the
/// network's actual protocol version instead of the SDK's seed version.
///
/// For an SDK pinned to a fixed protocol version (version updating disabled)
/// this is a no-op: no network request is made and the pinned version is
/// returned unchanged.
///
/// # Parameters
/// * `sdk_handle` - Handle to the SDK instance.
///
Expand Down
243 changes: 68 additions & 175 deletions packages/rs-sdk/src/sdk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2012,33 +2012,27 @@ mod test {
assert!(sdk.auto_detect_protocol_version);
}

/// A testnet refresh that reports a version below the floor leaves the SDK at
/// the floor (12), never below it.
/// A testnet SDK boots at the floor (12). When a refresh's proven query is
/// unavailable, refresh stays at the floor — never below it, and never
/// trusting an unverified value.
#[tokio::test]
async fn test_testnet_refresh_below_floor_stays_at_floor() {
async fn test_testnet_refresh_keeps_floor_when_query_unavailable() {
let floor = super::min_protocol_version(Network::Testnet);
let sdk = SdkBuilder::new_mock()
.with_network(Network::Testnet)
.build()
.expect("mock Sdk should be created");
assert_eq!(sdk.protocol_version_number(), floor);

// Network reports a known version below the floor (e.g. 11).
let below = dpp::version::v11::PROTOCOL_VERSION_11;
assert!(
below < floor,
"test requires a reported version below the floor"
);
expect_get_status(&sdk, status_response_with_drive_current(below)).await;

// No mock expectation registered -> the proven fetch errors (non-fatal).
let resulting = sdk
.refresh_protocol_version()
.await
.expect("refresh should succeed");
.expect("refresh is best-effort and must not error when the query fails");

assert_eq!(
resulting, floor,
"a testnet refresh reporting below the floor must leave the SDK at the floor"
"a failed testnet refresh must leave the SDK at the floor"
);
assert_eq!(sdk.protocol_version_number(), floor);
}
Expand Down Expand Up @@ -2067,150 +2061,77 @@ mod test {
// refresh_protocol_version
// -----------------------------------------------------------------

/// Build a `GetStatusResponse` whose Drive protocol `current` equals
/// `drive_current`, leaving the rest of the version tree populated the
/// minimal amount needed to walk to that field.
fn status_response_with_drive_current(
drive_current: u32,
) -> dapi_grpc::platform::v0::GetStatusResponse {
use dapi_grpc::platform::v0::get_status_response::{
get_status_response_v0::{version::protocol, version::Protocol, Version as VersionV0},
GetStatusResponseV0, Version,
};
use dapi_grpc::platform::v0::GetStatusResponse;

let drive = protocol::Drive {
latest: drive_current,
current: drive_current,
next_epoch: drive_current,
};
let protocol = Protocol {
tenderdash: None,
drive: Some(drive),
};
let version = VersionV0 {
software: None,
protocol: Some(protocol),
};
let v0 = GetStatusResponseV0 {
version: Some(version),
node: None,
chain: None,
network: None,
state_sync: None,
time: None,
};
GetStatusResponse {
version: Some(Version::V0(v0)),
}
}

/// Build a `GetStatusResponse` with no version block at all (a node that
/// did not report its protocol version).
fn status_response_without_version() -> dapi_grpc::platform::v0::GetStatusResponse {
dapi_grpc::platform::v0::GetStatusResponse { version: None }
}

/// Register a `GetStatusRequest -> response` expectation on the mock SDK's
/// inner DAPI client so `refresh_protocol_version` can execute it.
async fn expect_get_status(
sdk: &super::Sdk,
response: dapi_grpc::platform::v0::GetStatusResponse,
) {
use dapi_grpc::platform::v0::{get_status_request, GetStatusRequest};
use rs_dapi_client::ExecutionResponse;

let request = GetStatusRequest {
version: Some(get_status_request::Version::V0(
get_status_request::GetStatusRequestV0 {},
)),
/// Register a proven `ExtendedEpochInfo::fetch_current` expectation on the
/// mock SDK. The mock injects `LATEST_VERSION` into the proven response's
/// metadata, so consuming this expectation drives `refresh_protocol_version`
/// through the same verified `maybe_update_protocol_version` ratchet a real
/// quorum-signed response would — the exact path production relies on.
async fn expect_epoch_refresh(sdk: &mut super::Sdk) {
use crate::platform::types::epoch::EpochQuery;
use crate::platform::LimitQuery;
use dpp::block::extended_epoch_info::{v0::ExtendedEpochInfoV0, ExtendedEpochInfo};

// Must match the query `ExtendedEpochInfo::fetch_current` issues.
let query = LimitQuery {
query: EpochQuery {
start: None,
ascending: false,
},
limit: Some(1),
start_info: None,
};

match sdk.inner {
super::SdkInstance::Mock { ref dapi, .. } => {
let mut guard = dapi.lock().await;
guard
.expect(
&request,
&Ok(ExecutionResponse {
inner: response,
retries: 0,
address: "http://127.0.0.1".parse().expect("valid address"),
}),
)
.expect("expectation registered");
}
_ => panic!("expected a mock SDK"),
}
}

#[test]
fn test_extract_network_protocol_version_present() {
let response = status_response_with_drive_current(12);
assert_eq!(
super::refresh::extract_network_protocol_version(&response),
Some(12)
);
}

#[test]
fn test_extract_network_protocol_version_missing_version_block() {
let response = status_response_without_version();
assert_eq!(
super::refresh::extract_network_protocol_version(&response),
None
);
}

/// Seeded at 10, network reports 12 -> SDK ratchets to 12.
/// Mirrors the testnet shielded-fee under-reservation regression.
#[tokio::test]
async fn test_refresh_ratchets_up_to_network_version() {
let sdk = mock_sdk_with_auto_detect(10);
assert_eq!(sdk.protocol_version_number(), 10);

expect_get_status(&sdk, status_response_with_drive_current(12)).await;

let resulting = sdk
.refresh_protocol_version()
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::<ExtendedEpochInfo, _>(query, Some(epoch))
.await
.expect("refresh should succeed");

assert_eq!(resulting, 12, "returned version must reflect the ratchet");
assert_eq!(sdk.protocol_version_number(), 12);
assert_eq!(sdk.version().protocol_version, 12);
.expect("register epoch refresh expectation");
}

/// An unknown (future) version is ignored by the `maybe_update`
/// guard, leaving the SDK on its current version.
/// Seeded below `LATEST_VERSION`, a proven refresh ratchets the SDK up to the
/// network's version through the *verified* metadata path (the mock injects
/// `LATEST_VERSION` into the proven response's metadata, exactly as a real
/// quorum-signed response would). Mirrors the testnet shielded-fee
/// under-reservation regression.
#[tokio::test]
async fn test_refresh_ignores_unknown_version() {
use dpp::version::PlatformVersion;

let sdk = mock_sdk_with_auto_detect(PlatformVersion::latest().protocol_version);
let original = sdk.protocol_version_number();
async fn test_refresh_ratchets_up_via_proven_query() {
let mut sdk = mock_sdk_with_auto_detect(10);
assert_eq!(sdk.protocol_version_number(), 10);

expect_get_status(&sdk, status_response_with_drive_current(9999)).await;
expect_epoch_refresh(&mut sdk).await;

let resulting = sdk
.refresh_protocol_version()
.await
.expect("refresh should succeed");

assert_eq!(resulting, original, "unknown version must be ignored");
assert_eq!(sdk.protocol_version_number(), original);
assert_eq!(
resulting,
dpp::version::LATEST_VERSION,
"returned version must reflect the ratchet to the network's latest"
);
assert_eq!(sdk.protocol_version_number(), dpp::version::LATEST_VERSION);
assert_eq!(sdk.version().protocol_version, dpp::version::LATEST_VERSION);
}

/// A pinned (explicit `with_version`) SDK has auto-detect disabled and
/// must not move even when the network reports a newer version.
/// A pinned (explicit `with_version`) SDK has opted out of version tracking:
/// `refresh_protocol_version` short-circuits to a no-op that returns the
/// pinned version without issuing any network request — so it succeeds even
/// with no mock expectation registered.
#[tokio::test]
async fn test_refresh_leaves_pinned_sdk_unchanged() {
use dpp::version::PlatformVersion;

// Pin at the mainnet floor (11) so the pin survives construction (a
// sub-floor pin would be raised to the floor). Refresh must still be a
// no-op: auto-detect is off, and the refresh-time floor clamp is a no-op
// because the value already equals the floor.
// sub-floor pin would be raised to the floor).
let pinned = PlatformVersion::get(super::min_protocol_version(Network::Mainnet))
.expect("mainnet floor PV exists");
let sdk = SdkBuilder::new_mock()
Expand All @@ -2220,16 +2141,12 @@ mod test {
assert_eq!(sdk.protocol_version_number(), pinned.protocol_version);
assert!(!sdk.auto_detect_protocol_version);

expect_get_status(
&sdk,
status_response_with_drive_current(dpp::version::v12::PROTOCOL_VERSION_12),
)
.await;

// No expectation registered: a pinned refresh must not even attempt the
// query, so this returns Ok with the pinned version unchanged.
let resulting = sdk
.refresh_protocol_version()
.await
.expect("refresh should succeed");
.expect("pinned refresh is a no-op and must not error");

assert_eq!(
resulting, pinned.protocol_version,
Expand All @@ -2238,50 +2155,26 @@ mod test {
assert_eq!(sdk.protocol_version_number(), pinned.protocol_version);
}

/// A response without a version block is a non-fatal no-op: the call
/// succeeds and the version stays put.
///
/// Seeded at the mainnet floor so the refresh-time floor clamp is itself a
/// no-op and we observe only the missing-version-block behavior.
/// When the proven query is unavailable (no mock expectation, so the fetch
/// errors), refresh is non-fatal and does *not* fall back to an unverified
/// version: it just clamps the stored version to the per-network floor. Seeded
/// below the floor via the raw atomic to prove the clamp raises it.
#[tokio::test]
async fn test_refresh_missing_version_is_noop() {
let floor = super::min_protocol_version(Network::Mainnet);
let sdk = mock_sdk_with_auto_detect(floor);

expect_get_status(&sdk, status_response_without_version()).await;

let resulting = sdk
.refresh_protocol_version()
.await
.expect("refresh should succeed even without a version block");

assert_eq!(resulting, floor);
assert_eq!(sdk.protocol_version_number(), floor);
}

/// The refresh-time floor is a hard lower bound: even when the network
/// reports a version *below* the per-network minimum (and even on an SDK
/// artificially seeded below the floor), `refresh_protocol_version` leaves the
/// stored version at the floor — never below it.
#[tokio::test]
async fn test_refresh_raises_below_floor_to_network_floor() {
async fn test_refresh_query_unavailable_clamps_to_floor() {
let floor = super::min_protocol_version(Network::Mainnet);
// Seed below the floor via the raw atomic (construction would never allow
// this; `mock_sdk_with_auto_detect` uses `.store()`, bypassing the clamp).
let sdk = mock_sdk_with_auto_detect(floor - 1);
assert_eq!(sdk.protocol_version_number(), floor - 1);

// Network reports a known version that is still below the floor.
expect_get_status(&sdk, status_response_with_drive_current(floor - 1)).await;

let resulting = sdk
.refresh_protocol_version()
.await
.expect("refresh should succeed");
.expect("refresh is best-effort and must not error when the query fails");

assert_eq!(
resulting, floor,
"refresh must raise a below-floor version up to the network floor"
"a failed refresh must still raise a below-floor version up to the floor"
);
assert_eq!(sdk.protocol_version_number(), floor);
}
Expand Down
Loading
Loading