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