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