diff --git a/packages/rs-sdk-ffi/src/protocol_version/queries/refresh.rs b/packages/rs-sdk-ffi/src/protocol_version/queries/refresh.rs index b474185aa9..9bd238f4a3 100644 --- a/packages/rs-sdk-ffi/src/protocol_version/queries/refresh.rs +++ b/packages/rs-sdk-ffi/src/protocol_version/queries/refresh.rs @@ -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`. @@ -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. /// diff --git a/packages/rs-sdk/src/sdk.rs b/packages/rs-sdk/src/sdk.rs index 675650f97c..05d241e58d 100644 --- a/packages/rs-sdk/src/sdk.rs +++ b/packages/rs-sdk/src/sdk.rs @@ -2012,10 +2012,11 @@ 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) @@ -2023,22 +2024,15 @@ mod test { .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); } @@ -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::(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() @@ -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, @@ -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); } diff --git a/packages/rs-sdk/src/sdk/refresh.rs b/packages/rs-sdk/src/sdk/refresh.rs index da234100e9..057ec73645 100644 --- a/packages/rs-sdk/src/sdk/refresh.rs +++ b/packages/rs-sdk/src/sdk/refresh.rs @@ -1,107 +1,105 @@ //! Protocol-version refresh for [`Sdk`]. //! -//! Houses [`Sdk::refresh_protocol_version`] and its private helper -//! [`extract_network_protocol_version`]. The shared +//! Houses [`Sdk::refresh_protocol_version`], a thin eager wrapper around the +//! SDK's ordinary proven-query machinery. The shared //! [`super::min_protocol_version`] / [`Sdk::maybe_update_protocol_version`] //! helpers stay in the parent `sdk` module — this child module reaches them //! through `super::` / `self`. use super::Sdk; -use crate::error::Error; -use rs_dapi_client::{DapiRequestExecutor, IntoInner}; +use crate::platform::fetch_current_no_parameters::FetchCurrent; +use crate::Error; +use dpp::block::extended_epoch_info::ExtendedEpochInfo; use std::sync::atomic::Ordering; impl Sdk { - /// Query the connected network for its current protocol version and ratchet - /// this SDK's auto-detected protocol version up to it. + /// Eagerly teach this SDK the network's current protocol version and ratchet + /// up to it. /// /// ## Why this exists (bootstrap problem) /// /// An auto-detect SDK (one built without [`SdkBuilder::with_version()`]) is - /// seeded with [`PlatformVersion::latest()`] (or a caller-supplied initial - /// version) and only learns the network's *actual* protocol version after the - /// first metadata-bearing platform response is parsed (see + /// seeded at the per-network floor (or a caller-supplied initial version) and + /// only learns the network's *actual* protocol version after the first + /// metadata-bearing platform response is parsed (see /// [`Self::verify_response_metadata`]). Fee-sensitive flows — shielded pool /// shield/unshield/transfer/withdraw — compute their reserve from /// `self.version()`, so an SDK that hasn't yet observed network metadata can /// under-reserve against a network running a newer protocol version. Calling - /// this method on app start / network switch teaches the SDK the network - /// version eagerly, before any such flow runs. + /// this method on app start / network switch closes that window before any such + /// flow runs. /// - /// ## How it works + /// ## How it works — one trust path, not two /// - /// Issues an **unproved** `getStatus` request (no proof parsing), which keeps - /// working even when proofed queries fail (e.g. UNIMPLEMENTED on stale - /// evonodes) and is immune to proof-interpretation version skew. The - /// network's current Drive protocol version is read from the response and fed - /// into [`Self::maybe_update_protocol_version`], which applies the usual - /// guards: pinned (non-auto-detect) SDKs are left untouched, version `0` and - /// unknown versions are ignored, and the stored version only ever ratchets - /// upward via `fetch_max`. + /// This issues an ordinary **proven** `getEpochsInfo` query + /// ([`ExtendedEpochInfo::fetch_current`]) and discards the epoch payload. The + /// protocol version that query carries in its response metadata is ratcheted + /// into this SDK by the *same* [`Self::maybe_update_protocol_version`] path + /// every other query uses, and **only after** proof + quorum-signature + /// verification succeeds (the version is bound to the Tenderdash + /// `StateId.app_version`; see the security invariant in + /// [`Self::parse_proof_with_metadata_and_proof`]). So refresh inherits exactly + /// the same cryptographic trust as ordinary traffic — it adds **no** second, + /// weaker source of truth, it merely runs one proven query eagerly instead of + /// waiting for the next one. + /// + /// If the proven query fails (e.g. no [`ContextProvider`] is set, a transport + /// error, or `UNIMPLEMENTED` on a stale evonode) the failure is **non-fatal**: + /// we deliberately do *not* fall back to an unverified version. The stored + /// version is left untouched and then clamped to the per-network floor, so it + /// can never sit below the network's known minimum even when the refresh + /// round-trip fails. + /// + /// ## Pinned SDKs (version updating disabled) + /// + /// An SDK pinned via [`SdkBuilder::with_version()`] has explicitly opted out + /// of version tracking, so there is nothing to refresh. This method + /// short-circuits for a pinned SDK: it issues **no** network request and + /// returns the pinned version unchanged. (Construction already raised any + /// sub-floor pin up to the per-network floor, so the floor clamp would be a + /// no-op anyway.) + /// + /// For an auto-detect SDK the usual ratchet guards still apply: version `0` + /// and unknown/future versions are ignored, and the stored version only ever + /// ratchets upward via `fetch_max`. /// /// ## Returns /// - /// The SDK's protocol version number after the (possible) ratchet. A response - /// that omits the protocol-version field is treated as a non-fatal no-op: a - /// warning is logged and the current version number is returned unchanged. + /// The SDK's protocol version number after the (possible) ratchet and the + /// per-network floor clamp. + /// + /// [`SdkBuilder::with_version()`]: super::SdkBuilder::with_version + /// [`ContextProvider`]: crate::platform::ContextProvider pub async fn refresh_protocol_version(&self) -> Result { - use dapi_grpc::platform::v0::{get_status_request, GetStatusRequest}; - - let request = GetStatusRequest { - version: Some(get_status_request::Version::V0( - get_status_request::GetStatusRequestV0 {}, - )), - }; - - let response = self - .execute(request, self.dapi_client_settings) - .await - .into_inner()?; + // A pinned SDK (built via `SdkBuilder::with_version`) has opted out of + // version tracking: `maybe_update_protocol_version` is a no-op for it, so + // the proven query below could never change anything. Skip the round-trip + // and return the pinned version. (Construction already raised any sub-floor + // pin up to the per-network floor, so there is nothing left to clamp.) + if !self.auto_detect_protocol_version { + return Ok(self.protocol_version_number()); + } - match extract_network_protocol_version(&response) { - Some(network_version) => { - self.maybe_update_protocol_version(network_version); - } - None => { - tracing::warn!( - target: "dash_sdk::protocol_version", - "getStatus response did not contain a Drive protocol version; \ - keeping current protocol version" - ); - } + // A proven query whose response metadata flows through the verified + // `maybe_update_protocol_version` ratchet (see this method's docs). We only + // care about the side effect on the protocol version, not the epoch payload. + if let Err(error) = ExtendedEpochInfo::fetch_current(self).await { + tracing::warn!( + target: "dash_sdk::protocol_version", + %error, + "proven protocol-version refresh failed; keeping current version \ + (never falling back to an unverified one)" + ); } // Refresh-time floor (clamp site 2 of 2; the other is `SdkBuilder::build`). - // Independently of what the network reported — a too-low value the ratchet - // ignored, an unknown/zero version, or a missing version block — the stored - // version must never end up below the per-network minimum. `fetch_max` keeps - // this monotonic and concurrency-safe alongside the auto-detect ratchet. + // Independently of whether the proven query ran or ratcheted the version, + // the stored version must never end up below the per-network minimum. + // `fetch_max` keeps this monotonic and concurrency-safe alongside the + // auto-detect ratchet. self.protocol_version .fetch_max(super::min_protocol_version(self.network), Ordering::Relaxed); Ok(self.protocol_version_number()) } } - -/// Extract the network's current Drive protocol version from a `getStatus` -/// response. -/// -/// Walks `version → V0(v0) → v0.version → protocol → drive → current`, returning -/// `None` if any link in that chain is absent (e.g. a node that did not populate -/// the version block). Mirrors the field path used by -/// `drive_proof_verifier::types::evonode_status::Version::try_from`. -pub(super) fn extract_network_protocol_version( - response: &dapi_grpc::platform::v0::GetStatusResponse, -) -> Option { - use dapi_grpc::platform::v0::get_status_response; - - match &response.version { - Some(get_status_response::Version::V0(v0)) => v0 - .version - .as_ref() - .and_then(|v| v.protocol.as_ref()) - .and_then(|p| p.drive.as_ref()) - .map(|d| d.current), - None => None, - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift b/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift index 6c21e6d2c5..5f292a1303 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift @@ -511,13 +511,16 @@ public final class SDK: @unchecked Sendable { /// Refresh this SDK's protocol version from the connected network. /// - /// Issues an unproved `getStatus` on the Rust side and ratchets the SDK's - /// auto-detected protocol version up to the network's current version. The + /// Issues a proven `getEpochsInfo` query on the Rust side and ratchets the + /// SDK's auto-detected protocol version up to the network's version through + /// the proof + quorum-signature-verified path (no unverified fallback). The /// new version is shared across every clone of the underlying `Sdk` /// (including the clone held by a `PlatformWalletManager`), so fee-sensitive /// flows pick it up automatically. /// - /// Call on app start and after every network switch. Bridges + /// Call on app start and after every network switch. 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. Bridges /// `dash_sdk_refresh_protocol_version`. /// /// - Returns: the SDK's protocol version number after the (possible) ratchet. diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift index f149aaf7eb..23eeaa3943 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift @@ -156,9 +156,9 @@ class AppState: ObservableObject { /// Kick off a network protocol-version refresh for `sdk` without /// blocking UI readiness. /// - /// `SDK.refreshProtocolVersion()` blocks (it drives an unproved - /// `getStatus` to completion on the Rust runtime), so run it on a - /// background task. The ratchet propagates to the shared + /// `SDK.refreshProtocolVersion()` blocks (it drives a proven + /// `getEpochsInfo` query to completion on the Rust runtime), so run + /// it on a background task. The ratchet propagates to the shared /// `Arc` behind every clone of the SDK — including the /// one a `PlatformWalletManager` holds — so shielded fee math sees /// the network's real version. Failure is non-fatal: the SDK still diff --git a/packages/wasm-sdk/tests/unit/builder-with-addresses.spec.ts b/packages/wasm-sdk/tests/unit/builder-with-addresses.spec.ts index a60d2a42f1..86df4fbe0f 100644 --- a/packages/wasm-sdk/tests/unit/builder-with-addresses.spec.ts +++ b/packages/wasm-sdk/tests/unit/builder-with-addresses.spec.ts @@ -162,12 +162,16 @@ describe('WasmSdkBuilder', () => { [TEST_ADDRESS_1], 'testnet', ); + // `withVersion(1)` requests a version below the network protocol-version + // floor. The SDK never operates below that floor, so an explicit sub-floor + // pin is raised to it — the requested `1` surfaces as the (higher) testnet + // floor, not `1`. builder = builder.withVersion(1); expect(builder).to.be.an.instanceof(sdk.WasmSdkBuilder); const built = await builder.build(); expect(built).to.be.an.instanceof(sdk.WasmSdk); expect(built.version()).to.be.a('number'); - expect(built.version()).to.equal(1); + expect(built.version()).to.be.greaterThan(1); built.free(); }); @@ -185,7 +189,8 @@ describe('WasmSdkBuilder', () => { const built = await builder.build(); expect(built).to.be.an.instanceof(sdk.WasmSdk); expect(built.version()).to.be.a('number'); - expect(built.version()).to.equal(1); + // Sub-floor pin (1) is raised to the network protocol-version floor. + expect(built.version()).to.be.greaterThan(1); built.free(); }); });