Skip to main content

dash_sdk/
sdk.rs

1//! [Sdk] entrypoint to Dash Platform.
2
3use crate::error::{Error, StaleNodeError};
4use crate::internal_cache::NonceCache;
5use crate::mock::MockResponse;
6#[cfg(feature = "mocks")]
7use crate::mock::{provider::GrpcContextProvider, MockDashPlatformSdk};
8use crate::platform::fetch_current_no_parameters::FetchCurrent;
9use crate::platform::transition::put_settings::PutSettings;
10use crate::platform::Identifier;
11use arc_swap::ArcSwapOption;
12use dapi_grpc::mock::Mockable;
13use dapi_grpc::platform::v0::{Proof, ResponseMetadata};
14#[cfg(not(target_arch = "wasm32"))]
15use dapi_grpc::tonic::transport::Certificate;
16use dash_context_provider::ContextProvider;
17#[cfg(feature = "mocks")]
18use dash_context_provider::MockContextProvider;
19use dpp::bincode;
20use dpp::bincode::error::DecodeError;
21use dpp::block::extended_epoch_info::ExtendedEpochInfo;
22use dpp::dashcore::Network;
23use dpp::prelude::IdentityNonce;
24use dpp::version::PlatformVersion;
25use drive::grovedb::operations::proof::GroveDBProof;
26use drive_proof_verifier::FromProof;
27pub use http::Uri;
28#[cfg(feature = "mocks")]
29use rs_dapi_client::mock::MockDapiClient;
30pub use rs_dapi_client::Address;
31pub use rs_dapi_client::AddressBanInfo;
32pub use rs_dapi_client::AddressList;
33pub use rs_dapi_client::RequestSettings;
34use rs_dapi_client::{
35    transport::TransportRequest, DapiClient, DapiClientError, DapiRequestExecutor, ExecutionResult,
36};
37use std::fmt::Debug;
38#[cfg(feature = "mocks")]
39use std::num::NonZeroUsize;
40use std::path::Path;
41#[cfg(feature = "mocks")]
42use std::path::PathBuf;
43use std::sync::atomic::Ordering;
44use std::sync::{atomic, Arc};
45#[cfg(feature = "mocks")]
46use tokio::sync::{Mutex, MutexGuard};
47use tokio_util::sync::{CancellationToken, WaitForCancellationFuture};
48use zeroize::Zeroizing;
49
50/// How many data contracts fit in the cache.
51pub const DEFAULT_CONTRACT_CACHE_SIZE: usize = 100;
52/// How many token configs fit in the cache.
53pub const DEFAULT_TOKEN_CONFIG_CACHE_SIZE: usize = 100;
54/// How many quorum public keys fit in the cache.
55pub const DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE: usize = 100;
56/// Per-network *default* seed used only when an unpinned SDK has no explicit
57/// initial version.
58///
59/// Not a runtime clamp: [`SdkBuilder::with_initial_version`] can seed an unpinned
60/// SDK *below* this value (no construction-time floor), and auto-detect
61/// ([`Sdk::maybe_update_protocol_version`]) only ratchets the stored version
62/// *upward* via `fetch_max` when the network reports a newer one.
63const fn min_protocol_version(network: Network) -> u32 {
64    match network {
65        Network::Mainnet => dpp::version::v11::PROTOCOL_VERSION_11,
66        Network::Testnet => dpp::version::v12::PROTOCOL_VERSION_12,
67        Network::Devnet => dpp::version::v12::PROTOCOL_VERSION_12,
68        Network::Regtest => dpp::version::v12::PROTOCOL_VERSION_12,
69    }
70}
71
72/// Default signed-metadata freshness window for network SDKs.
73const DEFAULT_METADATA_TIME_TOLERANCE_MS: u64 = 31 * 60 * 1000;
74
75/// The default request settings for the SDK, used when the user does not provide any.
76///
77/// Use [SdkBuilder::with_settings] to set custom settings.
78const DEFAULT_REQUEST_SETTINGS: RequestSettings = RequestSettings {
79    retries: Some(3),
80    timeout: None,
81    ban_failed_address: None,
82    connect_timeout: None,
83    max_decoding_message_size: None,
84};
85
86/// Build the default DAPI bootstrap address list for `network` from
87/// [`dash_network_seeds`].
88///
89/// The seed lists are single-source-of-truth, weekly-refreshed upstream in
90/// `rust-dashcore`. We filter to Evo (HPMN) masternodes — the only ones that
91/// run Dash Platform — and build `https://<ip>:<platform_http_port>` URIs.
92/// The Core port on `seed.address` is intentionally discarded: DAPI clients
93/// need the platform HTTP port, not the Core P2P port.
94///
95/// Malformed upstream entries are silently skipped rather than panicking;
96/// the DAPI client handles retry/rotation across the remaining addresses.
97///
98/// Seeds whose recorded Platform TLS probe shows a certificate that this
99/// client's rustls stack would deterministically reject (`Expired`,
100/// `SelfSigned`, `Untrusted`) are skipped: every connect to them fails the
101/// handshake, so keeping them in rotation only costs retry/ban churn.
102/// `NoHandshake` is skipped only when the probe's TCP connect succeeded
103/// (`reachable == Ok`) — the prober also stamps `NoHandshake` on TCP
104/// timeouts and probe-budget expiry, which are transient conditions best
105/// left to runtime banning. `Valid` and `Unknown` (not probed) are kept. If the
106/// filter would empty the list (e.g. a seed file with all-stale probes),
107/// it falls back to the unfiltered set so the client can still bootstrap
108/// and let runtime banning sort it out.
109///
110/// ## Panics
111///
112/// Panics on networks other than `Mainnet` and `Testnet` — no upstream
113/// seed list exists for devnet/regtest.
114fn default_address_list_for_network(network: Network) -> AddressList {
115    if !matches!(network, Network::Mainnet | Network::Testnet) {
116        panic!("default address list is only available for mainnet and testnet");
117    }
118
119    let seeds = dash_network_seeds::evo_seeds(network);
120    let filtered = address_list_from_seeds(&seeds, true);
121    if filtered.is_empty() {
122        tracing::warn!(
123            ?network,
124            "all seed entries have failing TLS probes; falling back to unfiltered seed list"
125        );
126        return address_list_from_seeds(&seeds, false);
127    }
128    filtered
129}
130
131/// Whether a seed's recorded Platform TLS probe is a failure this client
132/// would deterministically reproduce on every connect. `NoHandshake` is
133/// also stamped by the prober on TCP timeout / probe-budget expiry, which
134/// are transient — it only counts when the probe's TCP connect itself
135/// succeeded. An unprobed seed (`None` / `Unknown`) is never rejected.
136fn seed_tls_deterministically_bad(platform: Option<&dash_network_seeds::PlatformStatus>) -> bool {
137    use dash_network_seeds::{Reachability, SslStatus};
138    let Some(platform) = platform else {
139        return false;
140    };
141    match platform.ssl {
142        SslStatus::Expired | SslStatus::SelfSigned | SslStatus::Untrusted => true,
143        SslStatus::NoHandshake => platform.reachable == Reachability::Ok,
144        SslStatus::Valid | SslStatus::Unknown => false,
145    }
146}
147
148/// Build an [`AddressList`] of `https://<ip>:<platform_http_port>` entries
149/// from `seeds`, optionally skipping seeds whose TLS probe is a
150/// deterministic failure (see [`seed_tls_deterministically_bad`]).
151fn address_list_from_seeds(
152    seeds: &[dash_network_seeds::MasternodeSeed],
153    skip_bad_tls: bool,
154) -> AddressList {
155    let mut list = AddressList::new();
156    for seed in seeds {
157        let Some(port) = seed.platform_http_port else {
158            continue;
159        };
160        if skip_bad_tls && seed_tls_deterministically_bad(seed.platform.as_ref()) {
161            continue;
162        }
163        let url = format!("https://{}:{}", seed.address.ip(), port);
164        if let Ok(uri) = url.parse::<Uri>() {
165            if let Ok(address) = Address::try_from(uri) {
166                list.add(address);
167            }
168        }
169    }
170    list
171}
172
173/// Dash Platform SDK
174///
175/// This is the main entry point for interacting with Dash Platform.
176/// It can be initialized in two modes:
177/// - `Normal`: Connects to a remote Dash Platform node.
178/// - `Mock`: Uses a mock implementation of Dash Platform.
179///
180/// Recommended method of initialization is to use [`SdkBuilder`]. There are also some helper
181/// methods:
182///
183/// * [`SdkBuilder::new_testnet()`] Create a [SdkBuilder] that connects to testnet.
184/// * [`SdkBuilder::new_mainnet()`] Create a [SdkBuilder] that connects to mainnet.
185/// * [`SdkBuilder::new_mock()`] Create a mock [SdkBuilder].
186/// * [`Sdk::new_mock()`] Create a mock [Sdk].
187///
188/// ## Thread safety
189///
190/// Sdk is thread safe and can be shared between threads.
191/// It uses internal locking when needed.
192///
193/// It is also safe to clone the Sdk.
194///
195/// ## Examples
196///
197/// See tests/ for examples of using the SDK.
198pub struct Sdk {
199    /// The network that the sdk is configured for (Dash (mainnet), Testnet, Devnet, Regtest)
200    pub network: Network,
201    inner: SdkInstance,
202    /// Use proofs when retrieving data from Platform.
203    ///
204    /// This is set to `true` by default. `false` is not implemented yet.
205    proofs: bool,
206
207    /// Nonce cache managed exclusively by the SDK.
208    nonce_cache: Arc<NonceCache>,
209
210    /// Context provider used by the SDK.
211    ///
212    /// ## Panics
213    ///
214    /// Note that setting this to None can panic.
215    context_provider: ArcSwapOption<Box<dyn ContextProvider>>,
216
217    /// Protocol version number detected from the network. Shared between clones.
218    protocol_version: Arc<atomic::AtomicU32>,
219
220    /// Whether the protocol version is pinned, i.e. auto-detection from network
221    /// response metadata is disabled. Set to `true` when the user explicitly calls
222    /// [`SdkBuilder::with_version()`].
223    version_pinned: bool,
224
225    /// Last seen height; used to determine if the remote node is stale.
226    ///
227    /// This is clone-able and can be shared between threads.
228    metadata_last_seen_height: Arc<atomic::AtomicU64>,
229
230    /// How many blocks difference is allowed between the last height and the current height received in metadata.
231    ///
232    /// See [SdkBuilder::with_height_tolerance] for more information.
233    metadata_height_tolerance: Option<u64>,
234
235    /// How many milliseconds difference is allowed between the time received in response and current local time.
236    ///
237    /// See [SdkBuilder::with_time_tolerance] for more information.
238    metadata_time_tolerance_ms: Option<u64>,
239
240    /// Cancellation token; once cancelled, all pending requests should be aborted.
241    pub(crate) cancel_token: CancellationToken,
242
243    /// Global settings of dapi client
244    pub(crate) dapi_client_settings: RequestSettings,
245
246    #[cfg(feature = "mocks")]
247    dump_dir: Option<PathBuf>,
248}
249impl Clone for Sdk {
250    fn clone(&self) -> Self {
251        Self {
252            network: self.network,
253            inner: self.inner.clone(),
254            proofs: self.proofs,
255            nonce_cache: Arc::clone(&self.nonce_cache),
256            context_provider: ArcSwapOption::new(self.context_provider.load_full()),
257            cancel_token: self.cancel_token.clone(),
258            protocol_version: Arc::clone(&self.protocol_version),
259            version_pinned: self.version_pinned,
260            metadata_last_seen_height: Arc::clone(&self.metadata_last_seen_height),
261            metadata_height_tolerance: self.metadata_height_tolerance,
262            metadata_time_tolerance_ms: self.metadata_time_tolerance_ms,
263            dapi_client_settings: self.dapi_client_settings,
264            #[cfg(feature = "mocks")]
265            dump_dir: self.dump_dir.clone(),
266        }
267    }
268}
269
270impl Debug for Sdk {
271    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272        match &self.inner {
273            SdkInstance::Dapi { dapi, .. } => f
274                .debug_struct("Sdk")
275                .field("dapi", dapi)
276                .field("proofs", &self.proofs)
277                .finish(),
278            #[cfg(feature = "mocks")]
279            SdkInstance::Mock { mock, .. } => f
280                .debug_struct("Sdk")
281                .field("mock", mock)
282                .field("proofs", &self.proofs)
283                .finish(),
284        }
285    }
286}
287
288/// Internal Sdk instance.
289///
290/// This is used to store the actual Sdk instance, which can be either a real Sdk or a mock Sdk.
291/// We use it to avoid exposing internals defined below to the public.
292#[derive(Debug, Clone)]
293enum SdkInstance {
294    /// Real Sdk, using DAPI with gRPC transport
295    Dapi {
296        /// DAPI client used to communicate with Dash Platform.
297        dapi: DapiClient,
298    },
299    /// Mock SDK
300    #[cfg(feature = "mocks")]
301    Mock {
302        /// Mock DAPI client used to communicate with Dash Platform.
303        ///
304        /// Dapi client is wrapped in a tokio [Mutex](tokio::sync::Mutex) as it's used in async context.
305        dapi: Arc<Mutex<MockDapiClient>>,
306        /// Mock SDK implementation processing mock expectations and responses.
307        mock: Arc<Mutex<MockDashPlatformSdk>>,
308        address_list: AddressList,
309    },
310}
311
312impl Sdk {
313    /// Initialize Dash Platform SDK in mock mode.
314    ///
315    /// This is a helper method that uses [`SdkBuilder`] to initialize the SDK in mock mode.
316    ///
317    /// See also [`SdkBuilder`].
318    pub fn new_mock() -> Self {
319        SdkBuilder::default()
320            .build()
321            .expect("mock should be created")
322    }
323
324    /// Return freshness criteria (height tolerance and time tolerance) for given request method.
325    ///
326    /// Note that if self.metadata_height_tolerance or self.metadata_time_tolerance_ms is None,
327    /// respective tolerance will be None regardless of method, to allow disabling staleness checks globally.
328    fn freshness_criteria(&self, method_name: &str) -> (Option<u64>, Option<u64>) {
329        match method_name {
330            "get_addresses_trunk_state" | "get_addresses_branch_state" => (
331                // Address synchronization checkpoints can lag the latest
332                // Platform height. Prefer their signed time when available,
333                // but retain the independently trusted height floor for the
334                // explicitly supported height-only configuration.
335                self.metadata_time_tolerance_ms
336                    .is_none()
337                    .then_some(self.metadata_height_tolerance)
338                    .flatten(),
339                self.metadata_time_tolerance_ms
340                    .map(|configured| configured.min(DEFAULT_METADATA_TIME_TOLERANCE_MS)),
341            ),
342            _ => (
343                self.metadata_height_tolerance,
344                self.metadata_time_tolerance_ms,
345            ),
346        }
347    }
348
349    /// Verify response metadata against the current state of the SDK.
350    pub fn verify_response_metadata(
351        &self,
352        method_name: &str,
353        metadata: &ResponseMetadata,
354    ) -> Result<(), Error> {
355        let (metadata_height_tolerance, metadata_time_tolerance_ms) =
356            self.freshness_criteria(method_name);
357        // Check the independent local-clock anchor before mutating the
358        // response-derived height high-water mark.
359        if let Some(time_tolerance) = metadata_time_tolerance_ms {
360            let now = chrono::Utc::now().timestamp_millis() as u64;
361            verify_metadata_time(metadata, now, time_tolerance)?;
362        };
363        if let Some(height_tolerance) = metadata_height_tolerance {
364            verify_metadata_height(
365                metadata,
366                height_tolerance,
367                Arc::clone(&(self.metadata_last_seen_height)),
368            )?;
369        };
370
371        self.maybe_update_protocol_version(metadata.protocol_version);
372
373        Ok(())
374    }
375
376    /// Update the stored protocol version if `received_version` is newer and known.
377    ///
378    /// Uses `fetch_max` so the highest version always wins under concurrent updates.
379    /// The version is stored per-SDK instance (not in the process-wide global),
380    /// so multiple SDK instances can track different networks independently.
381    fn maybe_update_protocol_version(&self, received_version: u32) {
382        if self.version_pinned {
383            return;
384        }
385
386        if received_version == 0 {
387            return;
388        }
389
390        let current = self.protocol_version.load(Ordering::Relaxed);
391
392        if received_version <= current {
393            return;
394        }
395
396        // Validate that we know this version before accepting it
397        if PlatformVersion::get(received_version).is_err() {
398            tracing::warn!(
399                received_version,
400                current_version = current,
401                "received unknown protocol version from network; keeping current"
402            );
403            return;
404        }
405
406        let previous = self
407            .protocol_version
408            .fetch_max(received_version, Ordering::Relaxed);
409        if previous < received_version {
410            tracing::info!(
411                target: "dash_sdk::protocol_version",
412                from = previous,
413                to = received_version,
414                "ratcheting protocol version upward"
415            );
416        }
417    }
418
419    /// Eagerly teach this SDK the network's current protocol version and ratchet up to it.
420    ///
421    /// Issues ordinary **proven** `getEpochsInfo` queries
422    /// ([`ExtendedEpochInfo::fetch_current`]) and discards the epoch payload. The
423    /// protocol version those queries carry in their verified response metadata is
424    /// ratcheted in by the *same* [`Self::maybe_update_protocol_version`] path
425    /// every other query uses — only after proof + quorum-signature verification
426    /// succeeds. Refresh therefore inherits the exact cryptographic trust of
427    /// ordinary traffic; it adds no second, weaker source of truth.
428    ///
429    /// On a pinned SDK ([`SdkBuilder::with_version`], `version_pinned`
430    /// on) this issues no request and returns the pinned version.
431    ///
432    /// If the fetch fails the failure is **non-fatal**: whatever version was
433    /// already learned is kept — we never fall back to an unverified one. Note
434    /// that [`ExtendedEpochInfo::fetch_current`] makes more than one round trip,
435    /// and each verified response ratchets the version on its own. A refresh that
436    /// ends in an error may therefore still have raised the stored version, and
437    /// the value returned here reflects that. This is by construction: every
438    /// ratchet step is proof-verified and upward-only, so a partial refresh can
439    /// only ever leave the SDK closer to the network's real version.
440    ///
441    /// On a proofs-disabled SDK ([`SdkBuilder::with_proofs`]`(false)`) this is a
442    /// no-op that returns the current version: refresh relies on a proven query,
443    /// so with proofs off there is no trusted source to ratchet from.
444    ///
445    /// Returns the SDK's protocol version number after the (possible) ratchet.
446    ///
447    /// [`SdkBuilder::with_version`]: SdkBuilder::with_version
448    pub async fn refresh_protocol_version(&self) -> Result<u32, Error> {
449        if !self.prove() {
450            return Ok(self.protocol_version_number());
451        }
452        if !self.version_pinned {
453            if let Err(error) = ExtendedEpochInfo::fetch_current(self).await {
454                tracing::warn!(
455                    target: "dash_sdk::protocol_version",
456                    %error,
457                    version = self.protocol_version_number(),
458                    "proven protocol-version refresh failed; keeping the highest \
459                     proof-verified version learned so far (never falling back to \
460                     an unverified one)"
461                );
462            }
463        }
464        Ok(self.protocol_version_number())
465    }
466
467    /// Retrieve object `O` from proof contained in `request` (of type `R`) and `response`.
468    ///
469    /// This method is used to retrieve objects from proofs returned by Dash Platform.
470    ///
471    /// ## Generic Parameters
472    ///
473    /// - `R`: Type of the request that was used to fetch the proof.
474    /// - `O`: Type of the object to be retrieved from the proof.
475    ///
476    /// ## Protocol version bootstrapping
477    ///
478    /// On a fresh auto-detect SDK (i.e. one built without [`SdkBuilder::with_version()`]), the
479    /// first call to this method uses the per-network [`min_protocol_version`] floor as a fallback
480    /// because no network response has been received yet to teach the SDK the real network version.
481    ///
482    /// The actual network version is learned only *after* proof parsing succeeds, when
483    /// [`Self::verify_response_metadata()`] processes `metadata.protocol_version`.  If the
484    /// connected network runs an older protocol version **and** proof interpretation differs
485    /// between that version and the seeded [`min_protocol_version`], the very first request may
486    /// fail before the SDK can correct itself.  Subsequent requests will use the correct version.
487    ///
488    /// This is a known bootstrap limitation.  Callers that must guarantee correct version
489    /// behaviour on the first request should pin the version explicitly via
490    /// [`SdkBuilder::with_version()`].
491    pub(crate) async fn parse_proof_with_metadata_and_proof<R, O: FromProof<R> + MockResponse>(
492        &self,
493        request: O::Request,
494        response: O::Response,
495        method_name: &'static str,
496    ) -> Result<(Option<O>, ResponseMetadata, Proof), Error>
497    where
498        O::Request: Mockable,
499    {
500        let provider = self
501            .context_provider()
502            .ok_or(drive_proof_verifier::Error::ContextProviderNotSet)?;
503
504        let (object, metadata, proof) = match self.inner {
505            SdkInstance::Dapi { .. } => O::maybe_from_proof_with_metadata(
506                request,
507                response,
508                self.network,
509                self.version(),
510                &provider,
511            ),
512            #[cfg(feature = "mocks")]
513            SdkInstance::Mock { ref mock, .. } => {
514                let guard = mock.lock().await;
515                guard.parse_proof_with_metadata(request, response)
516            }
517        }?;
518
519        // Security invariant: proof+signature verification above (the `?`) must
520        // precede this call, which ratchets the protocol version from the now-trusted
521        // `metadata.protocol_version`. Never reorder — the ratchet must not consume
522        // unverified metadata.
523        self.verify_response_metadata(method_name, &metadata)
524            .inspect_err(|err| {
525                tracing::warn!(%err,method=method_name,"received response with stale metadata; try another server");
526            })?;
527
528        Ok((object, metadata, proof))
529    }
530
531    /// Return [ContextProvider] used by the SDK.
532    pub fn context_provider(&self) -> Option<impl ContextProvider> {
533        let provider_guard = self.context_provider.load();
534        let provider = provider_guard.as_ref().map(Arc::clone);
535
536        provider
537    }
538
539    /// Returns a mutable reference to the `MockDashPlatformSdk` instance.
540    ///
541    /// Use returned object to configure mock responses with methods like `expect_fetch`.
542    ///
543    /// # Panics
544    ///
545    /// Panics when:
546    ///
547    /// * the `self` instance is not a `Mock` variant,
548    /// * the `self` instance is in use by another thread.
549    #[cfg(feature = "mocks")]
550    pub fn mock(&mut self) -> MutexGuard<'_, MockDashPlatformSdk> {
551        if let Sdk {
552            inner: SdkInstance::Mock { ref mock, .. },
553            ..
554        } = self
555        {
556            mock.try_lock()
557                .expect("mock sdk is in use by another thread and cannot be reconfigured")
558        } else {
559            panic!("not a mock")
560        }
561    }
562
563    /// Get or fetch identity nonce, querying Platform when stale or absent.
564    /// Treats a missing nonce as `0` before applying the optional bump; on first
565    /// interaction this may return `0` or `1` depending on `bump_first`. Does not
566    /// verify identity existence.
567    pub async fn get_identity_nonce(
568        &self,
569        identity_id: Identifier,
570        bump_first: bool,
571        settings: Option<PutSettings>,
572    ) -> Result<IdentityNonce, Error> {
573        let settings = settings.unwrap_or_default();
574        let nonce = self
575            .nonce_cache
576            .get_identity_nonce(self, identity_id, bump_first, &settings)
577            .await?;
578
579        tracing::trace!(
580            identity_id = %identity_id,
581            bump_first,
582            nonce,
583            "Fetched identity nonce"
584        );
585
586        Ok(nonce)
587    }
588
589    /// Get or fetch identity-contract nonce, querying Platform when stale or absent.
590    /// Treats a missing nonce as `0` before applying the optional bump; on first
591    /// interaction this may return `0` or `1` depending on `bump_first`. Does not
592    /// verify identity or contract existence.
593    pub async fn get_identity_contract_nonce(
594        &self,
595        identity_id: Identifier,
596        contract_id: Identifier,
597        bump_first: bool,
598        settings: Option<PutSettings>,
599    ) -> Result<IdentityNonce, Error> {
600        let settings = settings.unwrap_or_default();
601        self.nonce_cache
602            .get_identity_contract_nonce(self, identity_id, contract_id, bump_first, &settings)
603            .await
604    }
605
606    /// Marks identity nonce cache entries as stale so they are re-fetched from
607    /// Platform on the next call to [`get_identity_nonce`] or
608    /// [`get_identity_contract_nonce`].
609    pub async fn refresh_identity_nonce(&self, identity_id: &Identifier) {
610        self.nonce_cache.refresh(identity_id).await;
611    }
612
613    /// Return [Dash Platform version](PlatformVersion) information used by this SDK.
614    ///
615    /// With auto-detection (default) the SDK starts at the per-network
616    /// [`min_protocol_version`] (or the seed set via
617    /// [`SdkBuilder::with_initial_version`]) and then tracks the network's version
618    /// — auto-detection only ever ratchets *upward* (`fetch_max`). A version pinned
619    /// via [`SdkBuilder::with_version()`] is returned as pinned.
620    pub fn version<'v>(&self) -> &'v PlatformVersion {
621        let v = self.protocol_version.load(Ordering::Relaxed);
622        PlatformVersion::get(v).unwrap_or_else(|_| PlatformVersion::latest())
623    }
624
625    /// Return the raw protocol version number currently used by this SDK.
626    pub fn protocol_version_number(&self) -> u32 {
627        self.protocol_version.load(Ordering::Relaxed)
628    }
629
630    // TODO: Move to settings
631    /// Indicate if the sdk should request and verify proofs.
632    pub fn prove(&self) -> bool {
633        self.proofs
634    }
635
636    /// Build a [`QuerySettings`] borrowing this SDK's protocol version
637    /// and `prove` flag.
638    ///
639    /// Hand the resulting context to [`crate::platform::Query::query`] when
640    /// you need to encode a user-facing query into a wire `TransportRequest`
641    /// without taking a full `&Sdk` dependency through the encoder layer.
642    pub fn query_settings(&self) -> crate::platform::QuerySettings<'_> {
643        crate::platform::QuerySettings {
644            request_settings: &self.dapi_client_settings,
645            protocol_version: self.version(),
646            prove: self.prove(),
647        }
648    }
649
650    // TODO: If we remove this setter we don't need to use ArcSwap.
651    //   It's good enough to set Context once when you initialize the SDK.
652    /// Set the [ContextProvider] to use.
653    ///
654    /// [ContextProvider] is used to access state information, like data contracts and quorum public keys.
655    ///
656    /// Note that this will overwrite any previous context provider.
657    pub fn set_context_provider<C: ContextProvider + 'static>(&self, context_provider: C) {
658        self.context_provider
659            .swap(Some(Arc::new(Box::new(context_provider))));
660    }
661
662    /// Returns a future that resolves when the Sdk is cancelled (e.g. shutdown was requested).
663    pub fn cancelled(&self) -> WaitForCancellationFuture<'_> {
664        self.cancel_token.cancelled()
665    }
666
667    /// Request shutdown of the Sdk and all related operations.
668    pub fn shutdown(&self) {
669        self.cancel_token.cancel();
670    }
671
672    /// Return the [DapiClient] address list
673    pub fn address_list(&self) -> &AddressList {
674        match &self.inner {
675            SdkInstance::Dapi { dapi, .. } => dapi.address_list(),
676            #[cfg(feature = "mocks")]
677            SdkInstance::Mock { address_list, .. } => address_list,
678        }
679    }
680
681    /// Return an owned snapshot of every DAPI address' ban state,
682    /// including the reason the address was banned (when recorded).
683    ///
684    /// Delegates to [`AddressList::ban_info`]. Useful for diagnostics
685    /// and surfacing ban state up through the platform-wallet FFI to
686    /// the iOS example app.
687    pub fn address_ban_info(&self) -> Vec<AddressBanInfo> {
688        self.address_list().ban_info()
689    }
690}
691
692/// If received metadata time differs from local time by more than `tolerance`, the remote node is considered stale.
693///
694/// ## Parameters
695///
696/// - `metadata`: Metadata of the received response
697/// - `now_ms`: Current local time in milliseconds
698/// - `tolerance_ms`: Tolerance in milliseconds
699pub(crate) fn verify_metadata_time(
700    metadata: &ResponseMetadata,
701    now_ms: u64,
702    tolerance_ms: u64,
703) -> Result<(), Error> {
704    let metadata_time = metadata.time_ms;
705
706    // metadata_time - tolerance_ms <= now_ms <= metadata_time + tolerance_ms
707    if now_ms.abs_diff(metadata_time) > tolerance_ms {
708        return Err(StaleNodeError::Time {
709            expected_timestamp_ms: now_ms,
710            received_timestamp_ms: metadata_time,
711            tolerance_ms,
712        }
713        .into());
714    }
715
716    tracing::trace!(
717        expected_time = now_ms,
718        received_time = metadata_time,
719        tolerance_ms,
720        "received response with valid time"
721    );
722    Ok(())
723}
724
725/// If current metadata height is behind previously seen height by more than `tolerance`, the remote node
726///  is considered stale.
727fn verify_metadata_height(
728    metadata: &ResponseMetadata,
729    tolerance: u64,
730    last_seen_height: Arc<atomic::AtomicU64>,
731) -> Result<(), Error> {
732    let received_height = metadata.height;
733    // Linearize the response at an atomic max update, then reload so a racing
734    // higher response that committed before this validation completes is also
735    // considered. A lower accepted response can never reduce the baseline.
736    let previous_height = last_seen_height.fetch_max(received_height, Ordering::AcqRel);
737    let expected_height = previous_height.max(last_seen_height.load(Ordering::Acquire));
738
739    if expected_height > tolerance && received_height < expected_height.saturating_sub(tolerance) {
740        return Err(StaleNodeError::Height {
741            expected_height,
742            received_height,
743            tolerance_blocks: tolerance,
744        }
745        .into());
746    }
747
748    tracing::trace!(
749        expected_height,
750        received_height,
751        tolerance,
752        "received response within the monotonic height window"
753    );
754
755    Ok(())
756}
757
758#[async_trait::async_trait]
759impl DapiRequestExecutor for Sdk {
760    async fn execute<R: TransportRequest>(
761        &self,
762        request: R,
763        settings: RequestSettings,
764    ) -> ExecutionResult<R::Response, DapiClientError> {
765        match self.inner {
766            SdkInstance::Dapi { ref dapi, .. } => dapi.execute(request, settings).await,
767            #[cfg(feature = "mocks")]
768            SdkInstance::Mock { ref dapi, .. } => {
769                let dapi_guard = dapi.lock().await;
770                dapi_guard.execute(request, settings).await
771            }
772        }
773    }
774}
775
776/// Dash Platform SDK Builder, used to configure and [`SdkBuilder::build()`] the [Sdk].
777///
778/// [SdkBuilder] implements a "builder" design pattern to allow configuration of the Sdk before it is instantiated.
779/// It allows creation of Sdk in two modes:
780/// - `Normal`: Connects to a remote Dash Platform node.
781/// - `Mock`: Uses a mock implementation of Dash Platform.
782///
783/// Mandatory steps of initialization in normal mode are:
784///
785/// 1. Create an instance of [SdkBuilder] with [`SdkBuilder::new()`]
786/// 2. Configure the builder with [`SdkBuilder::with_core()`]
787/// 3. Call [`SdkBuilder::build()`] to create the [Sdk] instance.
788pub struct SdkBuilder {
789    /// List of addresses to connect to.
790    ///
791    /// If `None`, a mock client will be created.
792    addresses: Option<AddressList>,
793    settings: Option<RequestSettings>,
794
795    network: Network,
796
797    core_ip: String,
798    core_port: u16,
799    core_user: String,
800    core_password: Zeroizing<String>,
801
802    /// If true, request and verify proofs of the responses.
803    proofs: bool,
804
805    /// Platform version to use in this Sdk; if None, the SDK will auto-detect the version
806    /// from network metadata and update it as needed.
807    version: Option<&'static PlatformVersion>,
808
809    /// Whether the protocol version is pinned, i.e. the user explicitly called
810    /// `with_version()`. When true, auto-detection of protocol version from network
811    /// metadata is disabled.
812    version_pinned: bool,
813
814    /// Cache size for data contracts. Used by mock [GrpcContextProvider].
815    #[cfg(feature = "mocks")]
816    data_contract_cache_size: NonZeroUsize,
817
818    /// Cache size for token configs. Used by mock [GrpcContextProvider].
819    #[cfg(feature = "mocks")]
820    token_config_cache_size: NonZeroUsize,
821
822    /// Cache size for quorum public keys. Used by mock [GrpcContextProvider].
823    #[cfg(feature = "mocks")]
824    quorum_public_keys_cache_size: NonZeroUsize,
825
826    /// Context provider used by the SDK.
827    context_provider: Option<Box<dyn ContextProvider>>,
828
829    /// How many blocks difference is allowed between the last seen metadata height and the height received in response
830    /// metadata.
831    ///
832    /// See [SdkBuilder::with_height_tolerance] for more information.
833    metadata_height_tolerance: Option<u64>,
834
835    /// How many milliseconds difference is allowed between the time received in response metadata and current local time.
836    ///
837    /// See [SdkBuilder::with_time_tolerance] for more information.
838    metadata_time_tolerance_ms: Option<u64>,
839
840    /// Independently trusted initial Platform height used to seed the
841    /// monotonic freshness high-water mark.
842    trusted_initial_height: Option<u64>,
843
844    /// directory where dump files will be stored
845    #[cfg(feature = "mocks")]
846    dump_dir: Option<PathBuf>,
847
848    /// Cancellation token; once cancelled, all pending requests should be aborted.
849    pub(crate) cancel_token: CancellationToken,
850
851    /// CA certificate to use for TLS connections.
852    #[cfg(not(target_arch = "wasm32"))]
853    ca_certificate: Option<Certificate>,
854}
855
856impl Default for SdkBuilder {
857    /// Create default SdkBuilder that will create a mock client.
858    fn default() -> Self {
859        Self {
860            addresses: None,
861            settings: None,
862            network: Network::Mainnet,
863            core_ip: "".to_string(),
864            core_port: 0,
865            core_password: "".to_string().into(),
866            core_user: "".to_string(),
867
868            proofs: true,
869            metadata_height_tolerance: Some(1),
870            metadata_time_tolerance_ms: None,
871            trusted_initial_height: None,
872
873            #[cfg(feature = "mocks")]
874            data_contract_cache_size: NonZeroUsize::new(DEFAULT_CONTRACT_CACHE_SIZE)
875                .expect("data contract cache size must be positive"),
876
877            #[cfg(feature = "mocks")]
878            token_config_cache_size: NonZeroUsize::new(DEFAULT_TOKEN_CONFIG_CACHE_SIZE)
879                .expect("token config cache size must be positive"),
880
881            #[cfg(feature = "mocks")]
882            quorum_public_keys_cache_size: NonZeroUsize::new(DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE)
883                .expect("quorum public keys cache size must be positive"),
884
885            context_provider: None,
886
887            cancel_token: CancellationToken::new(),
888
889            // No version configured; `build()` defaults to the per-network
890            // `min_protocol_version` unless `with_version`/`with_initial_version`
891            // sets one.
892            version: None,
893            version_pinned: false,
894            #[cfg(not(target_arch = "wasm32"))]
895            ca_certificate: None,
896
897            #[cfg(feature = "mocks")]
898            dump_dir: None,
899        }
900    }
901}
902
903impl SdkBuilder {
904    /// Enable or disable proofs on requests.
905    ///
906    /// In mock/offline testing with recorded vectors, set to false to match dumps
907    /// that were captured without proofs.
908    pub fn with_proofs(mut self, proofs: bool) -> Self {
909        self.proofs = proofs;
910        self
911    }
912    /// Create a new SdkBuilder with provided address list.
913    pub fn new(addresses: AddressList) -> Self {
914        Self {
915            addresses: Some(addresses),
916            metadata_time_tolerance_ms: Some(DEFAULT_METADATA_TIME_TOLERANCE_MS),
917            ..Default::default()
918        }
919    }
920
921    /// Replace the address list on this builder.
922    pub fn with_address_list(mut self, addresses: AddressList) -> Self {
923        self.addresses = Some(addresses);
924        self
925    }
926
927    /// Create a new SdkBuilder that will generate mock client.
928    pub fn new_mock() -> Self {
929        Self::default()
930    }
931
932    /// Create a new SdkBuilder instance preconfigured for testnet.
933    ///
934    /// This is a helper method that preconfigures [SdkBuilder] for testnet use.
935    /// Use this method if you want to connect to Dash Platform testnet during development and testing
936    /// of your solution.
937    pub fn new_testnet() -> Self {
938        let address_list = default_address_list_for_network(Network::Testnet);
939
940        Self::new(address_list).with_network(Network::Testnet)
941    }
942
943    /// Create a new SdkBuilder instance preconfigured for mainnet (production network).
944    ///
945    /// This is a helper method that preconfigures [SdkBuilder] for production use.
946    /// Use this method if you want to connect to Dash Platform mainnet with production-ready product.
947    ///
948    /// ## Panics
949    ///
950    /// This method panics if the mainnet configuration cannot be loaded.
951    ///
952    /// ## Unstable
953    ///
954    /// This method is unstable and can be changed in the future.
955    pub fn new_mainnet() -> Self {
956        let address_list = default_address_list_for_network(Network::Mainnet);
957
958        Self::new(address_list).with_network(Network::Mainnet)
959    }
960
961    /// Configure network type.
962    ///
963    /// Defaults to Network::Mainnet which is mainnet.
964    pub fn with_network(mut self, network: Network) -> Self {
965        self.network = network;
966        self
967    }
968
969    /// Configure CA certificate to use when verifying TLS connections.
970    ///
971    /// Used mainly for testing purposes and local networks.
972    ///
973    /// If not set, uses standard system CA certificates.
974    ///
975    /// ## Parameters
976    ///
977    /// - `pem_certificate`: PEM-encoded CA certificate. User must ensure that the certificate is valid.
978    #[cfg(not(target_arch = "wasm32"))]
979    pub fn with_ca_certificate(mut self, pem_certificate: Certificate) -> Self {
980        self.ca_certificate = Some(pem_certificate);
981        self
982    }
983
984    /// Load CA certificate from a PEM-encoded file.
985    ///
986    /// This is a convenience method that reads the certificate from a file and sets it using
987    /// [SdkBuilder::with_ca_certificate()].
988    #[cfg(not(target_arch = "wasm32"))]
989    pub fn with_ca_certificate_file(
990        self,
991        certificate_file_path: impl AsRef<Path>,
992    ) -> std::io::Result<Self> {
993        let pem = std::fs::read(certificate_file_path)?;
994        let cert = Certificate::from_pem(pem);
995
996        Ok(self.with_ca_certificate(cert))
997    }
998
999    /// Configure request settings.
1000    ///
1001    /// Tune request settings used to connect to the Dash Platform.
1002    ///
1003    /// Defaults to [`DEFAULT_REQUEST_SETTINGS`], which sets retries to 3.
1004    ///
1005    /// See [`RequestSettings`] for more information.
1006    pub fn with_settings(mut self, settings: RequestSettings) -> Self {
1007        self.settings = Some(settings);
1008        self
1009    }
1010
1011    /// Configure platform version.
1012    ///
1013    /// Select specific version of Dash Platform to use. This pins the version and
1014    /// disables auto-detection.
1015    ///
1016    /// The pinned version is used as-is; it is not clamped to the per-network
1017    /// [`min_protocol_version`].
1018    ///
1019    /// When unset, the SDK starts at the per-network [`min_protocol_version`] and
1020    /// ratchets upward via auto-detection.
1021    pub fn with_version(mut self, version: &'static PlatformVersion) -> Self {
1022        self.version = Some(version);
1023        self.version_pinned = true;
1024        self
1025    }
1026
1027    /// Override the initial protocol version seed while keeping auto-detect on.
1028    ///
1029    /// Unpinned SDKs otherwise seed at the per-network [`min_protocol_version`] and
1030    /// ratchet upward via `fetch_max` in `maybe_update_protocol_version` once the
1031    /// network's version is observed. This replaces that seed with `version`.
1032    ///
1033    /// The seed is used verbatim — including versions *below* the per-network floor
1034    /// (no construction-time clamp; configuring a valid seed is the caller's
1035    /// responsibility). A sub-floor seed is only corrected once a proven response
1036    /// ratchets the version upward; callers needing eager on-init discovery should
1037    /// call [`Sdk::refresh_protocol_version`] after building.
1038    ///
1039    /// Seeds `self.version` and keeps `version_pinned` `false`, so auto-detect stays
1040    /// on. Builder chains are last-write-wins: a later `with_initial_version` re-enables
1041    /// auto-detect that an earlier `with_version` disabled.
1042    pub fn with_initial_version(mut self, version: &'static PlatformVersion) -> Self {
1043        self.version = Some(version);
1044        self.version_pinned = false;
1045        self
1046    }
1047
1048    /// Configure context provider to use.
1049    ///
1050    /// Context provider is used to retrieve data contracts and quorum public keys from application state.
1051    /// It should be implemented by the user of this SDK to provide stateful information about the application.
1052    ///
1053    /// See [ContextProvider] for more information and [GrpcContextProvider] for an example implementation.
1054    pub fn with_context_provider<C: ContextProvider + 'static>(
1055        mut self,
1056        context_provider: C,
1057    ) -> Self {
1058        self.context_provider = Some(Box::new(context_provider));
1059
1060        self
1061    }
1062
1063    /// Set cancellation token that will be used by the Sdk.
1064    ///
1065    /// Once that cancellation token is cancelled, all pending requests shall terminate.
1066    pub fn with_cancellation_token(mut self, cancel_token: CancellationToken) -> Self {
1067        self.cancel_token = cancel_token;
1068        self
1069    }
1070
1071    /// Use Dash Core as a wallet and context provider.
1072    ///
1073    /// This is a convenience method that configures the SDK to use Dash Core as a wallet and context provider.
1074    ///
1075    /// For more control over the configuration, use [`SdkBuilder::with_context_provider()`].
1076    ///
1077    /// This is temporary implementation, intended for development purposes.
1078    pub fn with_core(mut self, ip: &str, port: u16, user: &str, password: &str) -> Self {
1079        self.core_ip = ip.to_string();
1080        self.core_port = port;
1081        self.core_user = user.to_string();
1082        self.core_password = Zeroizing::from(password.to_string());
1083
1084        self
1085    }
1086
1087    /// Change number of blocks difference allowed between the last height and the height received in current response.
1088    ///
1089    /// If height received in response metadata is behind previously seen height by more than this value, the node
1090    /// is considered stale, and the request will fail.
1091    ///
1092    /// If None, the height is not checked.
1093    ///
1094    /// Note that this feature doesn't guarantee that you are getting latest data, but it significantly decreases
1095    /// probability of getting old data.
1096    ///
1097    /// This is set to `1` by default.
1098    pub fn with_height_tolerance(mut self, tolerance: Option<u64>) -> Self {
1099        self.metadata_height_tolerance = tolerance;
1100        self
1101    }
1102
1103    /// How many milliseconds difference is allowed between the time received in response and current local time.
1104    /// If the received time differs from local time by more than this value, the remote node is stale.
1105    ///
1106    /// If None, the time is not checked.
1107    ///
1108    /// Network builders default to 31 minutes. Mock builders default to
1109    /// `None`. Disabling this for a proof-enabled network SDK requires a
1110    /// trusted initial height with height checking enabled.
1111    ///
1112    /// Note that enabling this check can cause issues if the local time is not synchronized with the network time,
1113    /// when the network is stalled or time between blocks increases significantly.
1114    ///
1115    /// Selecting a safe value for this parameter depends on maximum time between blocks mined on the network.
1116    /// For example, if the network is configured to mine a block every maximum 3 minutes, setting this value
1117    /// to a bit more than 6 minutes (to account for misbehaving proposers, network delays and local time
1118    /// synchronization issues) should be safe.
1119    pub fn with_time_tolerance(mut self, tolerance_ms: Option<u64>) -> Self {
1120        self.metadata_time_tolerance_ms = tolerance_ms;
1121        self
1122    }
1123
1124    /// Seed proof freshness with an independently trusted Platform height.
1125    ///
1126    /// This can be used instead of the local-clock policy. The checkpoint must
1127    /// come from a trusted source and should be persisted with its network and
1128    /// provenance by the caller.
1129    pub fn with_trusted_initial_height(mut self, height: u64) -> Self {
1130        self.trusted_initial_height = Some(height);
1131        self
1132    }
1133
1134    /// Configure directory where dumps of all requests and responses will be saved.
1135    /// Useful for debugging.
1136    ///
1137    /// This function will create the directory if it does not exist and save dumps of
1138    /// * all requests and responses - in files named `msg-*.json`
1139    /// * retrieved quorum public keys - in files named `quorum_pubkey-*.json`
1140    /// * retrieved data contracts - in files named `data_contract-*.json`
1141    ///
1142    /// These files can be used together with [MockDashPlatformSdk] to replay the requests and responses.
1143    /// See [MockDashPlatformSdk::load_expectations_sync()] for more information.
1144    ///
1145    /// Available only when `mocks` feature is enabled.
1146    #[cfg(feature = "mocks")]
1147    pub fn with_dump_dir(mut self, dump_dir: &Path) -> Self {
1148        self.dump_dir = Some(dump_dir.to_path_buf());
1149        self
1150    }
1151
1152    /// Build the Sdk instance.
1153    ///
1154    /// This method will create the Sdk instance based on the configuration provided to the builder.
1155    ///
1156    /// # Errors
1157    ///
1158    /// This method will return an error if the Sdk cannot be created.
1159    pub fn build(self) -> Result<Sdk, Error> {
1160        let is_network_sdk = self.addresses.is_some();
1161        let has_height_anchor = self
1162            .trusted_initial_height
1163            .zip(self.metadata_height_tolerance)
1164            .is_some_and(|(height, tolerance)| height > tolerance);
1165        if is_network_sdk
1166            && self.proofs
1167            && self.metadata_time_tolerance_ms.is_none()
1168            && !has_height_anchor
1169        {
1170            return Err(Error::Config(
1171                "proof mode requires a trusted initial height or signed-time freshness policy"
1172                    .to_string(),
1173            ));
1174        }
1175
1176        let dapi_client_settings = match self.settings {
1177            Some(settings) => DEFAULT_REQUEST_SETTINGS.override_by(settings),
1178            None => DEFAULT_REQUEST_SETTINGS,
1179        };
1180
1181        let initial_version = self.version.unwrap_or_else(|| {
1182            PlatformVersion::get(min_protocol_version(self.network))
1183                .expect("min_protocol_version for a network must be a valid version")
1184        });
1185
1186        let sdk= match self.addresses {
1187            // non-mock mode
1188            Some(addresses) => {
1189                #[allow(unused_mut)] // needs to be mutable for features other than wasm
1190                let mut dapi = DapiClient::new(addresses, dapi_client_settings);
1191                #[cfg(not(target_arch = "wasm32"))]
1192                if let Some(pem) = self.ca_certificate {
1193                    dapi = dapi.with_ca_certificate(pem);
1194                }
1195
1196                #[cfg(feature = "mocks")]
1197                let dapi = dapi.dump_dir(self.dump_dir.clone());
1198
1199                #[allow(unused_mut)] // needs to be mutable for #[cfg(feature = "mocks")]
1200                let mut sdk= Sdk{
1201                    network: self.network,
1202                    dapi_client_settings,
1203                    inner:SdkInstance::Dapi { dapi },
1204                    proofs:self.proofs,
1205                    context_provider: ArcSwapOption::new( self.context_provider.map(Arc::new)),
1206                    cancel_token: self.cancel_token,
1207                    nonce_cache: Default::default(),
1208                    // Seed atomic with the initial version; whether the version is
1209                    // pinned is controlled separately by `version_pinned`.
1210                    protocol_version: Arc::new(atomic::AtomicU32::new(initial_version.protocol_version)),
1211                    version_pinned: self.version_pinned,
1212                    metadata_last_seen_height: Arc::new(atomic::AtomicU64::new(
1213                        self.trusted_initial_height.unwrap_or(0),
1214                    )),
1215                    metadata_height_tolerance: self.metadata_height_tolerance,
1216                    metadata_time_tolerance_ms: self.metadata_time_tolerance_ms,
1217                    #[cfg(feature = "mocks")]
1218                    dump_dir: self.dump_dir,
1219                };
1220                // if context provider is not set correctly (is None), it means we need to fall back to core wallet
1221                if  sdk.context_provider.load().is_none() {
1222                    #[cfg(feature = "mocks")]
1223                    if !self.core_ip.is_empty() {
1224                        tracing::warn!(
1225                            "ContextProvider not set, falling back to a mock one; use SdkBuilder::with_context_provider() to set it up");
1226                        let mut context_provider = GrpcContextProvider::new(None,
1227                            &self.core_ip, self.core_port, &self.core_user, &self.core_password,
1228                            self.data_contract_cache_size, self.token_config_cache_size, self.quorum_public_keys_cache_size)?;
1229                        #[cfg(feature = "mocks")]
1230                        if sdk.dump_dir.is_some() {
1231                            context_provider.set_dump_dir(sdk.dump_dir.clone());
1232                        }
1233                        // We have cyclical dependency Sdk <-> GrpcContextProvider, so we just do some
1234                        // workaround using additional Arc.
1235                        let context_provider= Arc::new(context_provider);
1236                        sdk.context_provider.swap(Some(Arc::new(Box::new(context_provider.clone()))));
1237                        context_provider.set_sdk(Some(sdk.clone()));
1238                    } else{
1239                        return Err(Error::Config(concat!(
1240                            "context provider is not set, configure it with SdkBuilder::with_context_provider() ",
1241                            "or configure Core access with SdkBuilder::with_core() to use mock context provider")
1242                            .to_string()));
1243                    }
1244                    #[cfg(not(feature = "mocks"))]
1245                    return Err(Error::Config(concat!(
1246                        "context provider is not set, configure it with SdkBuilder::with_context_provider() ",
1247                        "or enable `mocks` feature to use mock context provider")
1248                        .to_string()));
1249                };
1250
1251                sdk
1252            },
1253            #[cfg(feature = "mocks")]
1254            // mock mode
1255            None => {
1256                let dapi =Arc::new(Mutex::new(  MockDapiClient::new()));
1257                // We create mock context provider that will use the mock DAPI client to retrieve data contracts.
1258                let  context_provider = self.context_provider.unwrap_or_else(||{
1259                    let mut cp=MockContextProvider::new();
1260                    if let Some(ref dump_dir) = self.dump_dir {
1261                        cp.quorum_keys_dir(Some(dump_dir.clone()));
1262                    }
1263                    Box::new(cp)
1264                }
1265                );
1266                let mock_sdk = MockDashPlatformSdk::new(Arc::clone(&dapi));
1267                let mock_sdk = Arc::new(Mutex::new(mock_sdk));
1268                let sdk= Sdk {
1269                    network: self.network,
1270                    dapi_client_settings,
1271                    inner:SdkInstance::Mock {
1272                        mock:mock_sdk.clone(),
1273                        dapi,
1274                        address_list: AddressList::new(),
1275                    },
1276                    dump_dir: self.dump_dir.clone(),
1277                    proofs:self.proofs,
1278                    nonce_cache: Default::default(),
1279                    protocol_version: Arc::new(atomic::AtomicU32::new(initial_version.protocol_version)),
1280                    version_pinned: self.version_pinned,
1281                    context_provider: ArcSwapOption::new(Some(Arc::new(context_provider))),
1282                    cancel_token: self.cancel_token,
1283                    metadata_last_seen_height: Arc::new(atomic::AtomicU64::new(
1284                        self.trusted_initial_height.unwrap_or(0),
1285                    )),
1286                    metadata_height_tolerance: self.metadata_height_tolerance,
1287                    metadata_time_tolerance_ms: self.metadata_time_tolerance_ms,
1288                };
1289                let mut guard = mock_sdk.try_lock().expect("mock sdk is in use by another thread and cannot be reconfigured");
1290                guard.set_sdk(sdk.clone());
1291                if let Some(ref dump_dir) = self.dump_dir {
1292                    guard.load_expectations_sync(dump_dir)?;
1293                };
1294
1295                sdk
1296            },
1297            #[cfg(not(feature = "mocks"))]
1298            None => return Err(Error::Config("Mock mode is not available. Please enable `mocks` feature or provide address list.".to_string())),
1299        };
1300
1301        Ok(sdk)
1302    }
1303}
1304
1305pub fn prettify_proof(proof: &Proof) -> String {
1306    let config = bincode::config::standard()
1307        .with_big_endian()
1308        .with_no_limit();
1309    let grovedb_proof: Result<GroveDBProof, DecodeError> =
1310        bincode::decode_from_slice(&proof.grovedb_proof, config).map(|(a, _)| a);
1311
1312    let grovedb_proof_string = match grovedb_proof {
1313        Ok(proof) => format!("{}", proof),
1314        Err(_) => "Invalid GroveDBProof".to_string(),
1315    };
1316    format!(
1317        "Proof {{
1318            grovedb_proof: {},
1319            quorum_hash: 0x{},
1320            signature: 0x{},
1321            round: {},
1322            block_id_hash: 0x{},
1323            quorum_type: {},
1324        }}",
1325        grovedb_proof_string,
1326        hex::encode(&proof.quorum_hash),
1327        hex::encode(&proof.signature),
1328        proof.round,
1329        hex::encode(&proof.block_id_hash),
1330        proof.quorum_type,
1331    )
1332}
1333
1334#[cfg(test)]
1335mod test {
1336    use std::sync::Arc;
1337
1338    use dapi_grpc::platform::v0::{GetIdentityRequest, ResponseMetadata};
1339    use rs_dapi_client::transport::TransportRequest;
1340    use test_case::test_matrix;
1341
1342    use crate::SdkBuilder;
1343
1344    use super::{min_protocol_version, Network};
1345
1346    /// Mainnet Evo masternodes expose the Platform HTTP endpoint on 443.
1347    const MAINNET_PLATFORM_HTTP_PORT: u16 = 443;
1348    /// Testnet Evo masternodes expose the Platform HTTP endpoint on 1443.
1349    const TESTNET_PLATFORM_HTTP_PORT: u16 = 1443;
1350
1351    #[test]
1352    fn new_testnet_sources_bootstrap_from_seeds() {
1353        let builder = SdkBuilder::new_testnet();
1354        let address_list = builder
1355            .addresses
1356            .as_ref()
1357            .expect("testnet builder should configure default addresses");
1358
1359        assert_eq!(builder.network, Network::Testnet);
1360        assert!(
1361            !address_list.is_empty(),
1362            "testnet must have at least one bootstrap address"
1363        );
1364        for address in address_list.get_live_addresses() {
1365            assert_eq!(
1366                address.uri().port_u16(),
1367                Some(TESTNET_PLATFORM_HTTP_PORT),
1368                "testnet bootstrap address must use the platform HTTP port",
1369            );
1370        }
1371    }
1372
1373    #[test]
1374    fn new_mainnet_sources_bootstrap_from_seeds() {
1375        let builder = SdkBuilder::new_mainnet();
1376        let address_list = builder
1377            .addresses
1378            .as_ref()
1379            .expect("mainnet builder should configure default addresses");
1380
1381        assert_eq!(builder.network, Network::Mainnet);
1382        assert!(
1383            !address_list.is_empty(),
1384            "mainnet must have at least one bootstrap address"
1385        );
1386        for address in address_list.get_live_addresses() {
1387            assert_eq!(
1388                address.uri().port_u16(),
1389                Some(MAINNET_PLATFORM_HTTP_PORT),
1390                "mainnet bootstrap address must use the platform HTTP port",
1391            );
1392        }
1393    }
1394
1395    mod seed_tls_filter {
1396        use super::super::{address_list_from_seeds, seed_tls_deterministically_bad};
1397        use dash_network_seeds::{
1398            CoreStatus, MasternodeSeed, MasternodeType, PlatformStatus, Reachability, SslStatus,
1399        };
1400
1401        /// `host` disambiguates seeds — [`AddressList`] dedupes by URI, so
1402        /// every test seed needs a distinct IP.
1403        fn seed(host: u8, platform: Option<PlatformStatus>) -> MasternodeSeed {
1404            MasternodeSeed {
1405                address: format!("203.0.113.{host}:9999").parse().unwrap(),
1406                mn_type: MasternodeType::Evo,
1407                platform_http_port: Some(443),
1408                core: CoreStatus::default(),
1409                platform,
1410            }
1411        }
1412
1413        fn status(ssl: SslStatus, reachable: Reachability) -> PlatformStatus {
1414            PlatformStatus {
1415                reachable,
1416                ssl,
1417                ..PlatformStatus::default()
1418            }
1419        }
1420
1421        /// Every `SslStatus` × probe-reachability combination, against the
1422        /// contract: cert-level verdicts (`Expired`/`SelfSigned`/`Untrusted`)
1423        /// are deterministic regardless of reachability; `NoHandshake` is
1424        /// deterministic only when the probe's TCP connect succeeded;
1425        /// `Valid`/`Unknown`/unprobed are never rejected.
1426        #[test]
1427        fn classification_covers_every_status_combination() {
1428            let reachabilities = [
1429                Reachability::Unknown,
1430                Reachability::Ok,
1431                Reachability::Timeout,
1432                Reachability::Refused,
1433                Reachability::Error,
1434            ];
1435            for reachable in reachabilities {
1436                for ssl in [
1437                    SslStatus::Expired,
1438                    SslStatus::SelfSigned,
1439                    SslStatus::Untrusted,
1440                ] {
1441                    assert!(
1442                        seed_tls_deterministically_bad(Some(&status(ssl, reachable))),
1443                        "{ssl:?} must be rejected regardless of {reachable:?}"
1444                    );
1445                }
1446                for ssl in [SslStatus::Valid, SslStatus::Unknown] {
1447                    assert!(
1448                        !seed_tls_deterministically_bad(Some(&status(ssl, reachable))),
1449                        "{ssl:?} must never be rejected ({reachable:?})"
1450                    );
1451                }
1452                assert_eq!(
1453                    seed_tls_deterministically_bad(Some(&status(
1454                        SslStatus::NoHandshake,
1455                        reachable
1456                    ))),
1457                    reachable == Reachability::Ok,
1458                    "NoHandshake must be rejected only when TCP connect succeeded ({reachable:?})"
1459                );
1460            }
1461            assert!(
1462                !seed_tls_deterministically_bad(None),
1463                "an unprobed seed must never be rejected"
1464            );
1465        }
1466
1467        #[test]
1468        fn filter_drops_only_deterministic_failures() {
1469            let seeds = vec![
1470                seed(1, Some(status(SslStatus::Valid, Reachability::Ok))),
1471                seed(2, Some(status(SslStatus::Expired, Reachability::Ok))),
1472                seed(
1473                    3,
1474                    Some(status(SslStatus::NoHandshake, Reachability::Timeout)),
1475                ),
1476                seed(4, Some(status(SslStatus::NoHandshake, Reachability::Ok))),
1477                seed(5, None),
1478            ];
1479            assert_eq!(address_list_from_seeds(&seeds, true).len(), 3);
1480            assert_eq!(address_list_from_seeds(&seeds, false).len(), 5);
1481        }
1482
1483        /// The all-rejected input exercises the empty-filter result the
1484        /// caller falls back from; the fallback itself must retain the
1485        /// full set.
1486        #[test]
1487        fn all_rejected_input_yields_empty_filtered_and_full_unfiltered() {
1488            let seeds = vec![
1489                seed(1, Some(status(SslStatus::Expired, Reachability::Ok))),
1490                seed(2, Some(status(SslStatus::Untrusted, Reachability::Timeout))),
1491            ];
1492            assert!(address_list_from_seeds(&seeds, true).is_empty());
1493            assert_eq!(address_list_from_seeds(&seeds, false).len(), 2);
1494        }
1495
1496        #[test]
1497        fn seed_without_platform_port_is_always_skipped() {
1498            let mut no_port = seed(1, Some(status(SslStatus::Valid, Reachability::Ok)));
1499            no_port.platform_http_port = None;
1500            assert!(address_list_from_seeds(&[no_port], false).is_empty());
1501        }
1502    }
1503
1504    /// Smoke signal: the upstream seed lists are far larger than 10 entries on
1505    /// both networks. If parsing drops most of them we want a loud test
1506    /// failure rather than silently shipping a near-empty bootstrap list.
1507    #[test]
1508    fn bootstrap_counts_reasonable() {
1509        let mainnet = SdkBuilder::new_mainnet()
1510            .addresses
1511            .expect("mainnet builder should configure default addresses");
1512        let testnet = SdkBuilder::new_testnet()
1513            .addresses
1514            .expect("testnet builder should configure default addresses");
1515        assert!(
1516            mainnet.len() >= 10,
1517            "expected >=10 mainnet bootstrap addresses, got {}",
1518            mainnet.len()
1519        );
1520        assert!(
1521            testnet.len() >= 10,
1522            "expected >=10 testnet bootstrap addresses, got {}",
1523            testnet.len()
1524        );
1525    }
1526
1527    #[test]
1528    fn network_builders_enable_an_independent_time_anchor() {
1529        assert_eq!(
1530            SdkBuilder::new_testnet().metadata_time_tolerance_ms,
1531            Some(super::DEFAULT_METADATA_TIME_TOLERANCE_MS)
1532        );
1533        assert_eq!(SdkBuilder::new_mock().metadata_time_tolerance_ms, None);
1534    }
1535
1536    #[test]
1537    fn proof_enabled_network_builder_rejects_missing_freshness_anchor() {
1538        let error = SdkBuilder::new(super::AddressList::new())
1539            .with_time_tolerance(None)
1540            .build()
1541            .expect_err("network proof mode must have an independent freshness anchor");
1542
1543        assert!(
1544            matches!(error, crate::Error::Config(message) if message.contains("trusted initial height"))
1545        );
1546    }
1547
1548    #[test_matrix(0, 0; "zero height")]
1549    #[test_matrix(1, 1; "height equals tolerance")]
1550    #[test_matrix(1, 2; "height below tolerance")]
1551    fn proof_enabled_network_builder_rejects_ineffective_height_anchor(
1552        trusted_height: u64,
1553        tolerance: u64,
1554    ) {
1555        let error = SdkBuilder::new(super::AddressList::new())
1556            .with_time_tolerance(None)
1557            .with_height_tolerance(Some(tolerance))
1558            .with_trusted_initial_height(trusted_height)
1559            .build()
1560            .expect_err("trusted height must impose a freshness floor");
1561
1562        assert!(
1563            matches!(error, crate::Error::Config(message) if message.contains("trusted initial height"))
1564        );
1565    }
1566
1567    #[test]
1568    fn height_only_address_checkpoint_uses_trusted_height_floor() {
1569        let sdk = SdkBuilder::new_mock()
1570            .with_time_tolerance(None)
1571            .with_height_tolerance(Some(2))
1572            .with_trusted_initial_height(100)
1573            .build()
1574            .expect("effective trusted height should permit height-only proof mode");
1575
1576        assert!(matches!(
1577            sdk.verify_response_metadata(
1578                "get_addresses_trunk_state",
1579                &ResponseMetadata {
1580                    height: 97,
1581                    ..Default::default()
1582                },
1583            ),
1584            Err(crate::Error::StaleNode(
1585                super::StaleNodeError::Height { .. }
1586            ))
1587        ));
1588        assert_eq!(
1589            sdk.metadata_last_seen_height
1590                .load(std::sync::atomic::Ordering::Acquire),
1591            100,
1592            "a rejected stale checkpoint must not lower the trusted floor"
1593        );
1594    }
1595
1596    #[test]
1597    fn trusted_initial_height_seeds_the_high_water_mark() {
1598        let sdk = SdkBuilder::new_mock()
1599            .with_trusted_initial_height(42)
1600            .build()
1601            .expect("mock SDK should build");
1602
1603        assert_eq!(
1604            sdk.metadata_last_seen_height
1605                .load(std::sync::atomic::Ordering::Acquire),
1606            42
1607        );
1608    }
1609
1610    #[test_matrix(97..102, 100, 2, false; "valid height")]
1611    #[test_case(103, 100, 2, true; "invalid height")]
1612    fn test_verify_metadata_height(
1613        expected_height: u64,
1614        received_height: u64,
1615        tolerance: u64,
1616        expect_err: bool,
1617    ) {
1618        let metadata = ResponseMetadata {
1619            height: received_height,
1620            ..Default::default()
1621        };
1622
1623        let last_seen_height = Arc::new(std::sync::atomic::AtomicU64::new(expected_height));
1624
1625        let result =
1626            super::verify_metadata_height(&metadata, tolerance, Arc::clone(&last_seen_height));
1627
1628        assert_eq!(result.is_err(), expect_err);
1629        if result.is_ok() {
1630            assert_eq!(
1631                last_seen_height.load(std::sync::atomic::Ordering::Relaxed),
1632                expected_height.max(received_height),
1633                "height high-water mark must never decrease"
1634            );
1635        }
1636    }
1637
1638    #[test]
1639    fn accepted_height_tolerance_cannot_walk_the_watermark_backwards() {
1640        let last_seen_height = Arc::new(std::sync::atomic::AtomicU64::new(100));
1641
1642        super::verify_metadata_height(
1643            &ResponseMetadata {
1644                height: 99,
1645                ..Default::default()
1646            },
1647            1,
1648            Arc::clone(&last_seen_height),
1649        )
1650        .expect("one block behind is within tolerance");
1651        assert_eq!(
1652            last_seen_height.load(std::sync::atomic::Ordering::Acquire),
1653            100
1654        );
1655
1656        super::verify_metadata_height(
1657            &ResponseMetadata {
1658                height: 98,
1659                ..Default::default()
1660            },
1661            1,
1662            Arc::clone(&last_seen_height),
1663        )
1664        .expect_err("a second rollback step must be compared with the high-water mark");
1665        assert_eq!(
1666            last_seen_height.load(std::sync::atomic::Ordering::Acquire),
1667            100
1668        );
1669
1670        super::verify_metadata_height(
1671            &ResponseMetadata {
1672                height: 101,
1673                ..Default::default()
1674            },
1675            1,
1676            Arc::clone(&last_seen_height),
1677        )
1678        .expect("a newer height should advance the high-water mark");
1679        assert_eq!(
1680            last_seen_height.load(std::sync::atomic::Ordering::Acquire),
1681            101
1682        );
1683    }
1684
1685    #[test]
1686    fn cloned_sdk_verify_metadata_height() {
1687        let sdk1 = SdkBuilder::new_mock()
1688            .build()
1689            .expect("mock Sdk should be created");
1690
1691        // First message verified, height 1.
1692        let metadata = ResponseMetadata {
1693            height: 1,
1694            ..Default::default()
1695        };
1696
1697        // use dummy request type to satisfy generic parameter
1698        let request = GetIdentityRequest::default();
1699        sdk1.verify_response_metadata(request.method_name(), &metadata)
1700            .expect("metadata should be valid");
1701
1702        assert_eq!(
1703            sdk1.metadata_last_seen_height
1704                .load(std::sync::atomic::Ordering::Relaxed),
1705            metadata.height,
1706            "initial height"
1707        );
1708
1709        // now, we clone sdk and do two requests.
1710        let sdk2 = sdk1.clone();
1711        let sdk3 = sdk1.clone();
1712
1713        // Second message verified, height 2.
1714        let metadata = ResponseMetadata {
1715            height: 2,
1716            ..Default::default()
1717        };
1718        // use dummy request type to satisfy generic parameter
1719        let request = GetIdentityRequest::default();
1720        sdk2.verify_response_metadata(request.method_name(), &metadata)
1721            .expect("metadata should be valid");
1722
1723        assert_eq!(
1724            sdk1.metadata_last_seen_height
1725                .load(std::sync::atomic::Ordering::Relaxed),
1726            metadata.height,
1727            "first sdk should see height from second sdk"
1728        );
1729        assert_eq!(
1730            sdk3.metadata_last_seen_height
1731                .load(std::sync::atomic::Ordering::Relaxed),
1732            metadata.height,
1733            "third sdk should see height from second sdk"
1734        );
1735
1736        // Third message verified, height 3.
1737        let metadata = ResponseMetadata {
1738            height: 3,
1739            ..Default::default()
1740        };
1741        // use dummy request type to satisfy generic parameter
1742        let request = GetIdentityRequest::default();
1743        sdk3.verify_response_metadata(request.method_name(), &metadata)
1744            .expect("metadata should be valid");
1745
1746        assert_eq!(
1747            sdk1.metadata_last_seen_height
1748                .load(std::sync::atomic::Ordering::Relaxed),
1749            metadata.height,
1750            "first sdk should see height from third sdk"
1751        );
1752
1753        assert_eq!(
1754            sdk2.metadata_last_seen_height
1755                .load(std::sync::atomic::Ordering::Relaxed),
1756            metadata.height,
1757            "second sdk should see height from third sdk"
1758        );
1759
1760        // Now, using sdk1 for height 1 again should fail, as we are already at 3, with default tolerance 1.
1761        let metadata = ResponseMetadata {
1762            height: 1,
1763            ..Default::default()
1764        };
1765
1766        let request = GetIdentityRequest::default();
1767        sdk1.verify_response_metadata(request.method_name(), &metadata)
1768            .expect_err("metadata should be invalid");
1769    }
1770
1771    /// Helper: build a mock SDK with auto-detect enabled and a specific starting version.
1772    /// Does NOT call `with_version()` (which would disable auto-detect).
1773    fn mock_sdk_with_auto_detect(starting_version: u32) -> super::Sdk {
1774        use std::sync::atomic::Ordering;
1775
1776        let sdk = SdkBuilder::new_mock()
1777            .build()
1778            .expect("mock Sdk should be created");
1779        sdk.protocol_version
1780            .store(starting_version, Ordering::Relaxed);
1781        sdk
1782    }
1783
1784    #[test]
1785    fn test_version_update_from_metadata() {
1786        let sdk = mock_sdk_with_auto_detect(1);
1787
1788        assert_eq!(sdk.protocol_version_number(), 1);
1789
1790        let metadata = ResponseMetadata {
1791            protocol_version: 2,
1792            height: 1,
1793            ..Default::default()
1794        };
1795
1796        sdk.verify_response_metadata("test", &metadata)
1797            .expect("metadata should be valid");
1798
1799        assert_eq!(sdk.protocol_version_number(), 2);
1800        assert_eq!(sdk.version().protocol_version, 2);
1801    }
1802
1803    #[test]
1804    fn test_unknown_version_ignored() {
1805        use dpp::version::PlatformVersion;
1806
1807        let sdk = mock_sdk_with_auto_detect(PlatformVersion::latest().protocol_version);
1808        let original_version = sdk.protocol_version_number();
1809
1810        let metadata = ResponseMetadata {
1811            protocol_version: 999,
1812            height: 1,
1813            ..Default::default()
1814        };
1815
1816        sdk.verify_response_metadata("test", &metadata)
1817            .expect("metadata should be valid");
1818
1819        assert_eq!(sdk.protocol_version_number(), original_version);
1820        assert_eq!(sdk.version().protocol_version, original_version);
1821    }
1822
1823    #[test]
1824    fn test_version_shared_between_clones() {
1825        let sdk = mock_sdk_with_auto_detect(1);
1826
1827        let clone = sdk.clone();
1828
1829        let metadata = ResponseMetadata {
1830            protocol_version: 2,
1831            height: 1,
1832            ..Default::default()
1833        };
1834
1835        clone
1836            .verify_response_metadata("test", &metadata)
1837            .expect("metadata should be valid");
1838
1839        assert_eq!(
1840            sdk.protocol_version_number(),
1841            2,
1842            "original should see update from clone"
1843        );
1844    }
1845
1846    #[test]
1847    fn test_version_downgrade_ignored() {
1848        let sdk = mock_sdk_with_auto_detect(2);
1849
1850        assert_eq!(sdk.protocol_version_number(), 2);
1851
1852        let metadata = ResponseMetadata {
1853            protocol_version: 1,
1854            height: 1,
1855            ..Default::default()
1856        };
1857
1858        sdk.verify_response_metadata("test", &metadata)
1859            .expect("metadata should be valid");
1860
1861        assert_eq!(sdk.protocol_version_number(), 2);
1862    }
1863
1864    #[test]
1865    fn test_version_zero_ignored() {
1866        use dpp::version::PlatformVersion;
1867
1868        let sdk = mock_sdk_with_auto_detect(PlatformVersion::latest().protocol_version);
1869        let original_version = sdk.protocol_version_number();
1870
1871        let metadata = ResponseMetadata {
1872            protocol_version: 0,
1873            height: 1,
1874            ..Default::default()
1875        };
1876
1877        sdk.verify_response_metadata("test", &metadata)
1878            .expect("metadata should be valid");
1879
1880        assert_eq!(sdk.protocol_version_number(), original_version);
1881    }
1882
1883    #[test]
1884    fn test_concurrent_updates_converge_to_highest() {
1885        use std::thread;
1886
1887        let sdk = mock_sdk_with_auto_detect(1);
1888
1889        assert_eq!(sdk.protocol_version_number(), 1);
1890
1891        let mut handles = Vec::new();
1892        // Spawn threads that race to update to version 2 and version 3
1893        for version in [2u32, 3, 2, 3, 2, 3] {
1894            let sdk_clone = sdk.clone();
1895            handles.push(thread::spawn(move || {
1896                let metadata = ResponseMetadata {
1897                    protocol_version: version,
1898                    height: 1,
1899                    ..Default::default()
1900                };
1901                sdk_clone
1902                    .verify_response_metadata("test", &metadata)
1903                    .expect("metadata should be valid");
1904            }));
1905        }
1906
1907        for h in handles {
1908            h.join().expect("thread should not panic");
1909        }
1910
1911        // Highest known version (3) must win regardless of thread ordering
1912        assert_eq!(
1913            sdk.protocol_version_number(),
1914            3,
1915            "concurrent updates must converge to highest version"
1916        );
1917    }
1918
1919    // TC-7 (global DPP version sync) removed — set_current() is no longer called
1920    // from the SDK. Version is stored per-instance, not in the process-wide global.
1921
1922    #[test]
1923    fn test_explicit_version_disables_auto_detect() {
1924        use dpp::version::PlatformVersion;
1925
1926        // Pin at the mainnet default version. The network reporting a newer
1927        // version must still be ignored, because the pin disables auto-detect.
1928        let pinned = PlatformVersion::get(min_protocol_version(Network::Mainnet))
1929            .expect("mainnet-floor PV exists");
1930        let sdk = SdkBuilder::new_mock()
1931            .with_version(pinned)
1932            .build()
1933            .expect("mock Sdk should be created");
1934
1935        assert_eq!(sdk.protocol_version_number(), pinned.protocol_version);
1936        assert!(sdk.version_pinned);
1937
1938        // Network reports version 12 (> pinned) — should be ignored because version is pinned
1939        let metadata = ResponseMetadata {
1940            protocol_version: dpp::version::v12::PROTOCOL_VERSION_12,
1941            height: 1,
1942            ..Default::default()
1943        };
1944
1945        sdk.verify_response_metadata("test", &metadata)
1946            .expect("metadata should be valid");
1947
1948        assert_eq!(
1949            sdk.protocol_version_number(),
1950            pinned.protocol_version,
1951            "pinned version must not be auto-updated"
1952        );
1953    }
1954
1955    #[test]
1956    fn test_with_initial_version_seeds_to_older_network_version() {
1957        use dpp::version::PlatformVersion;
1958
1959        // Caller seeds the auto-detect atomic at the mainnet default version.
1960        // `version_pinned` stays false, so fetch_max can still ratchet upward
1961        // when the network later moves to a newer PV.
1962        let floor = min_protocol_version(Network::Mainnet);
1963        let initial = PlatformVersion::get(floor).expect("mainnet-floor PV exists");
1964        let sdk = SdkBuilder::new_mock()
1965            .with_initial_version(initial)
1966            .build()
1967            .expect("mock Sdk should be created");
1968
1969        assert_eq!(
1970            sdk.protocol_version_number(),
1971            floor,
1972            "with_initial_version must seed the atomic without pinning"
1973        );
1974        assert_eq!(sdk.version().protocol_version, floor);
1975        assert!(
1976            !sdk.version_pinned,
1977            "with_initial_version must keep auto-detect enabled"
1978        );
1979
1980        // Metadata at the floor is accepted (matches current seed, no ratchet needed).
1981        let metadata = ResponseMetadata {
1982            protocol_version: floor,
1983            height: 1,
1984            ..Default::default()
1985        };
1986        sdk.verify_response_metadata("test", &metadata)
1987            .expect("metadata should be valid");
1988        assert_eq!(sdk.protocol_version_number(), floor);
1989
1990        // And a newer network version still ratchets upward.
1991        let newer = dpp::version::v12::PROTOCOL_VERSION_12;
1992        assert!(newer > floor, "ratchet target must exceed the floor");
1993        let metadata = ResponseMetadata {
1994            protocol_version: newer,
1995            height: 2,
1996            ..Default::default()
1997        };
1998        sdk.verify_response_metadata("test", &metadata)
1999            .expect("metadata should be valid");
2000        assert_eq!(sdk.protocol_version_number(), newer);
2001    }
2002
2003    #[test]
2004    fn test_with_initial_version_after_with_version_restores_auto_detect() {
2005        use dpp::version::PlatformVersion;
2006
2007        // Last-write-wins composability: a later `with_initial_version`
2008        // must re-enable auto-detect that an earlier `with_version`
2009        // disabled.
2010        //
2011        // `v_old` sits at the mainnet default version so the last-write-wins
2012        // effect stays observable.
2013        let v_latest = PlatformVersion::latest();
2014        let v_old = PlatformVersion::get(min_protocol_version(Network::Mainnet))
2015            .expect("mainnet-floor PV exists");
2016        assert!(
2017            v_old.protocol_version < v_latest.protocol_version,
2018            "v_old must be below latest so the later ratchet is observable"
2019        );
2020
2021        let sdk = SdkBuilder::new_mock()
2022            .with_version(v_latest)
2023            .with_initial_version(v_old)
2024            .build()
2025            .expect("mock Sdk should be created");
2026
2027        assert_eq!(
2028            sdk.protocol_version_number(),
2029            v_old.protocol_version,
2030            "with_initial_version must overwrite the prior with_version seed"
2031        );
2032        assert!(
2033            !sdk.version_pinned,
2034            "with_initial_version must restore auto-detect after with_version disabled it"
2035        );
2036
2037        // Ratchet upward via metadata observation works because auto-detect is on.
2038        let metadata = ResponseMetadata {
2039            protocol_version: v_latest.protocol_version,
2040            height: 1,
2041            ..Default::default()
2042        };
2043        sdk.verify_response_metadata("test", &metadata)
2044            .expect("metadata should be valid");
2045        assert_eq!(sdk.protocol_version_number(), v_latest.protocol_version);
2046    }
2047
2048    #[test]
2049    fn test_mock_version_follows_outer_sdk_atomic() {
2050        use dpp::version::PlatformVersion;
2051
2052        // Build a mock SDK with auto-detect, seeded at the mainnet default
2053        // version. After a metadata-driven ratchet to a newer PV, both the outer
2054        // SDK's `version()` and the inner
2055        // `MockDashPlatformSdk::version()` must report the same value — single
2056        // source of truth.
2057        let v_old = PlatformVersion::get(min_protocol_version(Network::Mainnet))
2058            .expect("mainnet-floor PV exists");
2059        let v_new = PlatformVersion::latest();
2060        assert!(
2061            v_old.protocol_version < v_new.protocol_version,
2062            "v_old must be below latest so the ratchet is observable"
2063        );
2064
2065        let mut sdk = SdkBuilder::new_mock()
2066            .with_initial_version(v_old)
2067            .build()
2068            .expect("mock Sdk should be created");
2069
2070        assert_eq!(sdk.version().protocol_version, v_old.protocol_version);
2071        {
2072            let mock = sdk.mock();
2073            assert_eq!(
2074                mock.version().protocol_version,
2075                v_old.protocol_version,
2076                "mock version must mirror outer SDK before ratchet"
2077            );
2078        }
2079
2080        let metadata = ResponseMetadata {
2081            protocol_version: v_new.protocol_version,
2082            height: 1,
2083            ..Default::default()
2084        };
2085        sdk.verify_response_metadata("test", &metadata)
2086            .expect("metadata should be valid");
2087
2088        assert_eq!(sdk.version().protocol_version, v_new.protocol_version);
2089        let mock = sdk.mock();
2090        assert_eq!(
2091            mock.version().protocol_version,
2092            v_new.protocol_version,
2093            "mock version must follow outer ratchet"
2094        );
2095    }
2096
2097    #[test]
2098    fn test_default_builder_seeds_initial_protocol_version_floor() {
2099        // A default (unpinned) builder uses the mainnet network, so it must seed
2100        // the SDK at the mainnet `min_protocol_version` floor, not at latest().
2101        let sdk = SdkBuilder::new_mock()
2102            .build()
2103            .expect("mock Sdk should be created");
2104
2105        let expected = min_protocol_version(Network::Mainnet);
2106        assert_eq!(
2107            sdk.protocol_version_number(),
2108            expected,
2109            "unpinned mainnet SDK must boot at the mainnet floor, not latest()"
2110        );
2111        assert_eq!(sdk.version().protocol_version, expected);
2112        assert!(
2113            !sdk.version_pinned,
2114            "default SDK must keep auto-detect enabled"
2115        );
2116    }
2117
2118    #[test]
2119    fn test_default_floor_ratchets_up_but_never_down() {
2120        let sdk = SdkBuilder::new_mock()
2121            .build()
2122            .expect("mock Sdk should be created");
2123        // Default (mainnet) boot floor.
2124        let floor = min_protocol_version(Network::Mainnet);
2125        assert_eq!(sdk.protocol_version_number(), floor);
2126
2127        // Ratchet to a fixed known target (PV12), not `floor + N`: stays valid as the
2128        // floor advances, and `maybe_update_protocol_version` only accepts known versions.
2129        let target = dpp::version::v12::PROTOCOL_VERSION_12;
2130        assert!(
2131            target > floor,
2132            "ratchet test target must exceed the floor; bump it if the floor reaches v12"
2133        );
2134        sdk.maybe_update_protocol_version(target);
2135        assert_eq!(
2136            sdk.protocol_version_number(),
2137            target,
2138            "auto-detect must ratchet upward from the floor"
2139        );
2140
2141        // Never down: an older network version is ignored.
2142        sdk.maybe_update_protocol_version(floor - 1);
2143        assert_eq!(
2144            sdk.protocol_version_number(),
2145            target,
2146            "ratchet must never downgrade below the highest observed version"
2147        );
2148    }
2149
2150    /// Regression guard for the verify-before-ratchet security invariant.
2151    ///
2152    /// The full tampered-*signed*-proof path isn't unit-testable here: it needs a
2153    /// quorum BLS signature, a context provider, and a `FromProof` verifier round-trip.
2154    /// Both ratchet sites run the `FromProof` verifier (structural + `verify_tenderdash_proof`)
2155    /// BEFORE `verify_response_metadata` → `maybe_update_protocol_version`: the query path via
2156    /// `parse_proof_with_metadata_and_proof`, the broadcast wait-path in `broadcast.rs` (see the
2157    /// guard comments at both call sites). Here we lock in the ratchet's own gates: it must NOT
2158    /// raise the stored version off untrustworthy inputs (unknown / zero / lower), so even a
2159    /// metadata value that slipped past verification can't move the SDK to a bogus version.
2160    #[test]
2161    fn test_ratchet_rejects_unknown_and_non_upward_versions() {
2162        let sdk = SdkBuilder::new_mock()
2163            .build()
2164            .expect("mock Sdk should be created");
2165        // Default (mainnet) boot floor.
2166        let floor = min_protocol_version(Network::Mainnet);
2167        assert_eq!(sdk.protocol_version_number(), floor);
2168
2169        // Unknown (above LATEST_VERSION): rejected, version unchanged.
2170        sdk.maybe_update_protocol_version(dpp::version::LATEST_VERSION + 1);
2171        assert_eq!(
2172            sdk.protocol_version_number(),
2173            floor,
2174            "unknown protocol version must not move the stored version"
2175        );
2176
2177        // Zero (e.g. metadata default / stripped field): ignored.
2178        sdk.maybe_update_protocol_version(0);
2179        assert_eq!(
2180            sdk.protocol_version_number(),
2181            floor,
2182            "zero protocol version must be ignored"
2183        );
2184
2185        // Equal: no-op (no spurious downgrade or churn).
2186        sdk.maybe_update_protocol_version(floor);
2187        assert_eq!(sdk.protocol_version_number(), floor);
2188
2189        // Lower known version: ignored by the upward-only guard.
2190        sdk.maybe_update_protocol_version(floor - 1);
2191        assert_eq!(
2192            sdk.protocol_version_number(),
2193            floor,
2194            "lower known version must not downgrade the stored version"
2195        );
2196    }
2197
2198    /// A pin *below* the per-network [`min_protocol_version`] is preserved as-is
2199    /// (no construction-time clamp) and `version_pinned` stays `true`.
2200    #[test]
2201    fn test_explicit_pin_below_floor_is_preserved() {
2202        use dpp::version::PlatformVersion;
2203
2204        let floor = min_protocol_version(Network::Mainnet);
2205        let below = floor - 1;
2206        let pinned = PlatformVersion::get(below).expect("sub-floor PV exists");
2207        let sdk = SdkBuilder::new_mock()
2208            .with_version(pinned)
2209            .build()
2210            .expect("mock Sdk should be created");
2211
2212        assert_eq!(
2213            sdk.protocol_version_number(),
2214            below,
2215            "a pin below the floor must be preserved"
2216        );
2217        // Still pinned: auto-detect stays disabled.
2218        assert!(sdk.version_pinned);
2219    }
2220
2221    // -----------------------------------------------------------------
2222    // per-network protocol-version floor + non-mainnet boot/refresh
2223    // -----------------------------------------------------------------
2224
2225    /// An unpinned testnet SDK boots at the `min_protocol_version` floor, just
2226    /// like the mainnet default, and stays there until a proven response ratchets
2227    /// it upward.
2228    #[test]
2229    fn test_testnet_default_builder_boots_at_per_network_floor() {
2230        let sdk = SdkBuilder::new_mock()
2231            .with_network(Network::Testnet)
2232            .build()
2233            .expect("mock Sdk should be created");
2234
2235        assert_eq!(
2236            sdk.protocol_version_number(),
2237            min_protocol_version(Network::Testnet),
2238            "testnet seeds directly at its per-network floor"
2239        );
2240        assert!(!sdk.version_pinned);
2241    }
2242
2243    #[test_matrix([90,91,100,109,110], 100, 10, false; "valid time")]
2244    #[test_matrix([0,89,111], 100, 10, true; "invalid time")]
2245    #[test_matrix([0,100], [0,100], 100, false; "zero time")]
2246    #[test_matrix([99,101], 100, 0, true; "zero tolerance")]
2247    fn test_verify_metadata_time(
2248        received_time: u64,
2249        now_time: u64,
2250        tolerance: u64,
2251        expect_err: bool,
2252    ) {
2253        let metadata = ResponseMetadata {
2254            time_ms: received_time,
2255            ..Default::default()
2256        };
2257
2258        let result = super::verify_metadata_time(&metadata, now_time, tolerance);
2259
2260        assert_eq!(result.is_err(), expect_err);
2261    }
2262
2263    // -----------------------------------------------------------------
2264    // refresh_protocol_version
2265    // -----------------------------------------------------------------
2266
2267    /// Register a proven `ExtendedEpochInfo::fetch_current` expectation on the
2268    /// mock SDK. The mock injects `LATEST_VERSION` into the proven response's
2269    /// metadata, so consuming this expectation drives `refresh_protocol_version`
2270    /// through the same verified `maybe_update_protocol_version` ratchet a real
2271    /// quorum-signed response would — the exact path production relies on.
2272    async fn expect_epoch_refresh(sdk: &mut super::Sdk) {
2273        use crate::platform::types::epoch::EpochQuery;
2274        use crate::platform::LimitQuery;
2275        use dpp::block::extended_epoch_info::{v0::ExtendedEpochInfoV0, ExtendedEpochInfo};
2276        use drive_proof_verifier::types::ExtendedEpochInfos;
2277
2278        // Must match the two queries `ExtendedEpochInfo::fetch_current` issues: a
2279        // genesis probe, then a two-epoch ascending confirmation from the hinted
2280        // current epoch (mock expectation metadata reports epoch 0, so the hint is
2281        // 0). The confirmation answers with epoch 0 alone, which is how a real
2282        // proof says "no epoch above 0 has started".
2283        let probe_query = LimitQuery {
2284            query: EpochQuery::genesis(),
2285            limit: Some(1),
2286            start_info: None,
2287        };
2288        let confirmation_query = LimitQuery {
2289            query: EpochQuery::ascending_from(0),
2290            limit: Some(2),
2291            start_info: None,
2292        };
2293
2294        let epoch = ExtendedEpochInfo::from(ExtendedEpochInfoV0 {
2295            index: 0,
2296            first_block_time: 0,
2297            first_block_height: 0,
2298            first_core_block_height: 0,
2299            fee_multiplier_permille: 0,
2300            protocol_version: dpp::version::LATEST_VERSION,
2301        });
2302
2303        sdk.mock()
2304            .expect_fetch::<ExtendedEpochInfo, _>(probe_query, Some(epoch.clone()))
2305            .await
2306            .expect("register epoch probe expectation");
2307        sdk.mock()
2308            .expect_fetch_many::<_, ExtendedEpochInfo, _, ExtendedEpochInfos>(
2309                confirmation_query,
2310                Some(ExtendedEpochInfos::from_iter([(0, Some(epoch))])),
2311            )
2312            .await
2313            .expect("register epoch refresh expectation");
2314    }
2315
2316    /// Seeded below `LATEST_VERSION`, a proven refresh ratchets the SDK up to the
2317    /// network's version through the *verified* metadata path (the mock injects
2318    /// `LATEST_VERSION` into the proven response's metadata, exactly as a real
2319    /// quorum-signed response would). Mirrors the testnet shielded-fee
2320    /// under-reservation regression.
2321    #[tokio::test]
2322    async fn test_refresh_ratchets_up_via_proven_query() {
2323        let mut sdk = mock_sdk_with_auto_detect(super::min_protocol_version(Network::Mainnet));
2324        assert_eq!(
2325            sdk.protocol_version_number(),
2326            super::min_protocol_version(Network::Mainnet)
2327        );
2328
2329        expect_epoch_refresh(&mut sdk).await;
2330
2331        let resulting = sdk
2332            .refresh_protocol_version()
2333            .await
2334            .expect("refresh should succeed");
2335
2336        assert_eq!(
2337            resulting,
2338            dpp::version::LATEST_VERSION,
2339            "returned version must reflect the ratchet to the network's latest"
2340        );
2341        assert_eq!(sdk.protocol_version_number(), dpp::version::LATEST_VERSION);
2342        assert_eq!(sdk.version().protocol_version, dpp::version::LATEST_VERSION);
2343    }
2344
2345    /// A pinned (explicit `with_version`) SDK has opted out of version tracking:
2346    /// `refresh_protocol_version` short-circuits to a no-op that returns the
2347    /// pinned version without issuing any network request — so it succeeds even
2348    /// with no mock expectation registered.
2349    #[tokio::test]
2350    async fn test_refresh_leaves_pinned_sdk_unchanged() {
2351        use dpp::version::PlatformVersion;
2352
2353        // Pin at the mainnet default version.
2354        let pinned = PlatformVersion::get(min_protocol_version(Network::Mainnet))
2355            .expect("mainnet-floor PV exists");
2356        let sdk = SdkBuilder::new_mock()
2357            .with_version(pinned)
2358            .build()
2359            .expect("mock Sdk should be created");
2360        assert_eq!(sdk.protocol_version_number(), pinned.protocol_version);
2361        assert!(sdk.version_pinned);
2362
2363        // No expectation registered: a pinned refresh must not even attempt the
2364        // query, so this returns Ok with the pinned version unchanged.
2365        let resulting = sdk
2366            .refresh_protocol_version()
2367            .await
2368            .expect("pinned refresh is a no-op and must not error");
2369
2370        assert_eq!(
2371            resulting, pinned.protocol_version,
2372            "pinned version must not move"
2373        );
2374        assert_eq!(sdk.protocol_version_number(), pinned.protocol_version);
2375    }
2376
2377    /// When the proven query is unavailable (no mock expectation, so the fetch
2378    /// errors), refresh is non-fatal and does *not* fall back to an unverified
2379    /// version: it leaves the stored version exactly where it was. There is no
2380    /// runtime clamp — the auto-detect ratchet only ever moves it upward.
2381    #[tokio::test]
2382    async fn test_refresh_query_unavailable_keeps_current_version() {
2383        let starting = min_protocol_version(Network::Mainnet);
2384        let sdk = mock_sdk_with_auto_detect(starting);
2385        assert_eq!(sdk.protocol_version_number(), starting);
2386
2387        let resulting = sdk
2388            .refresh_protocol_version()
2389            .await
2390            .expect("refresh is best-effort and must not error when the query fails");
2391
2392        assert_eq!(
2393            resulting, starting,
2394            "a failed refresh must leave the stored version untouched (no fallback)"
2395        );
2396        assert_eq!(sdk.protocol_version_number(), starting);
2397    }
2398}