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