1use 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
62pub trait ProofResponseMetadata {
65 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
78pub(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 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
110pub const DEFAULT_CONTRACT_CACHE_SIZE: usize = 100;
112pub const DEFAULT_TOKEN_CONFIG_CACHE_SIZE: usize = 100;
114pub const DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE: usize = 100;
116pub 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
138pub type ProtocolVersionObserver = Arc<dyn Fn(u32) + Send + Sync>;
141
142const DEFAULT_METADATA_TIME_TOLERANCE_MS: u64 = 31 * 60 * 1000;
144
145const 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
156fn 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
201fn 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
218fn 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#[derive(Clone, Copy)]
245pub(crate) struct MetadataArrival {
246 height: u64,
247 time_ms: u64,
248}
249
250pub struct Sdk {
276 pub network: Network,
278 inner: SdkInstance,
279 proofs: bool,
284
285 nonce_cache: Arc<NonceCache>,
287
288 context_provider: ArcSwapOption<Box<dyn ContextProvider>>,
294
295 protocol_version: Arc<atomic::AtomicU32>,
297
298 version_pinned: bool,
302
303 verified_protocol_version: Arc<atomic::AtomicU32>,
309
310 protocol_version_observer: Option<ProtocolVersionObserver>,
313
314 #[cfg(all(test, feature = "mocks"))]
316 enforce_protocol_gate_on_mock: bool,
317
318 metadata_last_seen_height: Arc<atomic::AtomicU64>,
322
323 metadata_height_tolerance: Option<u64>,
327
328 metadata_time_tolerance_ms: Option<u64>,
332
333 pub(crate) cancel_token: CancellationToken,
335
336 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#[derive(Debug, Clone)]
390enum SdkInstance {
391 Dapi {
393 dapi: DapiClient,
395 },
396 #[cfg(feature = "mocks")]
398 Mock {
399 dapi: Arc<Mutex<MockDapiClient>>,
403 mock: Arc<Mutex<MockDashPlatformSdk>>,
405 address_list: AddressList,
406 },
407}
408
409impl Sdk {
410 pub fn new_mock() -> Self {
416 SdkBuilder::default()
417 .build()
418 .expect("mock should be created")
419 }
420
421 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 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 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 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 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 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 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 fn record_verified_protocol_version(&self, protocol_version: u32) {
525 self.verified_protocol_version
526 .fetch_max(protocol_version, Ordering::Relaxed);
527 }
528
529 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 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 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 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 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 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 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 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 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 self.verify_response_metadata_as_of(method_name, &verified.1, None)?;
795 } else {
796 self.accept_verified_metadata(method_name, &verified.1, None)?;
799 }
800 Ok(verified)
801 }
802 }
803 }
804
805 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 #[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 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 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 pub async fn refresh_identity_nonce(&self, identity_id: &Identifier) {
884 self.nonce_cache.refresh(identity_id).await;
885 }
886
887 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 pub fn protocol_version_number(&self) -> u32 {
901 self.protocol_version.load(Ordering::Acquire)
902 }
903
904 pub fn prove(&self) -> bool {
907 self.proofs
908 }
909
910 pub fn query_settings(&self) -> crate::platform::QuerySettings<'_> {
921 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 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 pub fn cancelled(&self) -> WaitForCancellationFuture<'_> {
952 self.cancel_token.cancelled()
953 }
954
955 pub fn shutdown(&self) {
957 self.cancel_token.cancel();
958 }
959
960 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 pub fn address_ban_info(&self) -> Vec<AddressBanInfo> {
976 self.address_list().ban_info()
977 }
978}
979
980pub(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 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
1013fn 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 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
1033fn 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
1077pub struct SdkBuilder {
1090 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 proofs: bool,
1105
1106 version: Option<&'static PlatformVersion>,
1109
1110 version_pinned: bool,
1114
1115 protocol_version_observer: Option<ProtocolVersionObserver>,
1117
1118 #[cfg(feature = "mocks")]
1120 data_contract_cache_size: NonZeroUsize,
1121
1122 #[cfg(feature = "mocks")]
1124 token_config_cache_size: NonZeroUsize,
1125
1126 #[cfg(feature = "mocks")]
1128 quorum_public_keys_cache_size: NonZeroUsize,
1129
1130 context_provider: Option<Box<dyn ContextProvider>>,
1132
1133 metadata_height_tolerance: Option<u64>,
1138
1139 metadata_time_tolerance_ms: Option<u64>,
1143
1144 trusted_initial_height: Option<u64>,
1147
1148 #[cfg(feature = "mocks")]
1150 dump_dir: Option<PathBuf>,
1151
1152 pub(crate) cancel_token: CancellationToken,
1154
1155 #[cfg(not(target_arch = "wasm32"))]
1157 ca_certificate: Option<Certificate>,
1158}
1159
1160impl Default for SdkBuilder {
1161 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 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 pub fn with_proofs(mut self, proofs: bool) -> Self {
1216 self.proofs = proofs;
1217 self
1218 }
1219 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 pub fn with_address_list(mut self, addresses: AddressList) -> Self {
1230 self.addresses = Some(addresses);
1231 self
1232 }
1233
1234 pub fn new_mock() -> Self {
1236 Self::default()
1237 }
1238
1239 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 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 pub fn with_network(mut self, network: Network) -> Self {
1272 self.network = network;
1273 self
1274 }
1275
1276 #[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 #[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 pub fn with_settings(mut self, settings: RequestSettings) -> Self {
1314 self.settings = Some(settings);
1315 self
1316 }
1317
1318 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 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 pub fn with_protocol_version_observer(mut self, observer: ProtocolVersionObserver) -> Self {
1374 self.protocol_version_observer = Some(observer);
1375 self
1376 }
1377
1378 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 pub fn with_cancellation_token(mut self, cancel_token: CancellationToken) -> Self {
1397 self.cancel_token = cancel_token;
1398 self
1399 }
1400
1401 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 pub fn with_height_tolerance(mut self, tolerance: Option<u64>) -> Self {
1429 self.metadata_height_tolerance = tolerance;
1430 self
1431 }
1432
1433 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 pub fn with_trusted_initial_height(mut self, height: u64) -> Self {
1460 self.trusted_initial_height = Some(height);
1461 self
1462 }
1463
1464 #[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 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 Some(addresses) => {
1519 #[allow(unused_mut)] 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)] 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 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 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 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 None => {
1590 let dapi =Arc::new(Mutex::new( MockDapiClient::new()));
1591 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 const MAINNET_PLATFORM_HTTP_PORT: u16 = 443;
1686 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 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 #[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 #[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 #[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 let metadata = ResponseMetadata {
2038 height: 1,
2039 ..Default::default()
2040 };
2041
2042 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 let sdk2 = sdk1.clone();
2056 let sdk3 = sdk1.clone();
2057
2058 let metadata = ResponseMetadata {
2060 height: 2,
2061 ..Default::default()
2062 };
2063 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 let metadata = ResponseMetadata {
2083 height: 3,
2084 ..Default::default()
2085 };
2086 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 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 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 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 assert_eq!(
2258 sdk.protocol_version_number(),
2259 3,
2260 "concurrent updates must converge to highest version"
2261 );
2262 }
2263
2264 #[test]
2268 fn test_explicit_version_disables_auto_detect() {
2269 use dpp::version::PlatformVersion;
2270
2271 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 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 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 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 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 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 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 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 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 let floor = min_protocol_version(Network::Mainnet);
2470 assert_eq!(sdk.protocol_version_number(), floor);
2471
2472 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 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 #[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 let floor = min_protocol_version(Network::Mainnet);
2512 assert_eq!(sdk.protocol_version_number(), floor);
2513
2514 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 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 sdk.maybe_update_protocol_version(floor);
2532 assert_eq!(sdk.protocol_version_number(), floor);
2533
2534 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 #[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 assert!(sdk.version_pinned);
2564 }
2565
2566 #[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 #[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 #[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 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 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 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 #[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 #[tokio::test]
2772 async fn test_refresh_leaves_pinned_sdk_unchanged() {
2773 use dpp::version::PlatformVersion;
2774
2775 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 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 #[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}