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};
14#[cfg(not(target_arch = "wasm32"))]
15use dapi_grpc::tonic::transport::Certificate;
16use dash_context_provider::ContextProvider;
17#[cfg(feature = "mocks")]
18use dash_context_provider::MockContextProvider;
19use dpp::bincode;
20use dpp::bincode::error::DecodeError;
21use dpp::block::extended_epoch_info::ExtendedEpochInfo;
22use dpp::dashcore::Network;
23use dpp::prelude::IdentityNonce;
24use dpp::version::PlatformVersion;
25use drive::grovedb::operations::proof::GroveDBProof;
26use drive_proof_verifier::FromProof;
27pub use http::Uri;
28#[cfg(feature = "mocks")]
29use rs_dapi_client::mock::MockDapiClient;
30pub use rs_dapi_client::Address;
31pub use rs_dapi_client::AddressBanInfo;
32pub use rs_dapi_client::AddressList;
33pub use rs_dapi_client::RequestSettings;
34use rs_dapi_client::{
35 transport::TransportRequest, DapiClient, DapiClientError, DapiRequestExecutor, ExecutionResult,
36};
37use std::fmt::Debug;
38#[cfg(feature = "mocks")]
39use std::num::NonZeroUsize;
40use std::path::Path;
41#[cfg(feature = "mocks")]
42use std::path::PathBuf;
43use std::sync::atomic::Ordering;
44use std::sync::{atomic, Arc};
45#[cfg(feature = "mocks")]
46use tokio::sync::{Mutex, MutexGuard};
47use tokio_util::sync::{CancellationToken, WaitForCancellationFuture};
48use zeroize::Zeroizing;
49
50pub const DEFAULT_CONTRACT_CACHE_SIZE: usize = 100;
52pub const DEFAULT_TOKEN_CONFIG_CACHE_SIZE: usize = 100;
54pub const DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE: usize = 100;
56pub const fn min_protocol_version(network: Network) -> u32 {
71 match network {
72 Network::Mainnet => dpp::version::v13::PROTOCOL_VERSION_13,
73 Network::Testnet => dpp::version::v13::PROTOCOL_VERSION_13,
74 Network::Devnet => dpp::version::v14::PROTOCOL_VERSION_14,
75 Network::Regtest => dpp::version::v13::PROTOCOL_VERSION_13,
76 }
77}
78
79pub type ProtocolVersionObserver = Arc<dyn Fn(u32) + Send + Sync>;
82
83const DEFAULT_METADATA_TIME_TOLERANCE_MS: u64 = 31 * 60 * 1000;
85
86const DEFAULT_REQUEST_SETTINGS: RequestSettings = RequestSettings {
90 retries: Some(3),
91 timeout: None,
92 ban_failed_address: None,
93 connect_timeout: None,
94 max_decoding_message_size: None,
95};
96
97fn default_address_list_for_network(network: Network) -> AddressList {
126 if !matches!(network, Network::Mainnet | Network::Testnet) {
127 panic!("default address list is only available for mainnet and testnet");
128 }
129
130 let seeds = dash_network_seeds::evo_seeds(network);
131 let filtered = address_list_from_seeds(&seeds, true);
132 if filtered.is_empty() {
133 tracing::warn!(
134 ?network,
135 "all seed entries have failing TLS probes; falling back to unfiltered seed list"
136 );
137 return address_list_from_seeds(&seeds, false);
138 }
139 filtered
140}
141
142fn seed_tls_deterministically_bad(platform: Option<&dash_network_seeds::PlatformStatus>) -> bool {
148 use dash_network_seeds::{Reachability, SslStatus};
149 let Some(platform) = platform else {
150 return false;
151 };
152 match platform.ssl {
153 SslStatus::Expired | SslStatus::SelfSigned | SslStatus::Untrusted => true,
154 SslStatus::NoHandshake => platform.reachable == Reachability::Ok,
155 SslStatus::Valid | SslStatus::Unknown => false,
156 }
157}
158
159fn address_list_from_seeds(
163 seeds: &[dash_network_seeds::MasternodeSeed],
164 skip_bad_tls: bool,
165) -> AddressList {
166 let mut list = AddressList::new();
167 for seed in seeds {
168 let Some(port) = seed.platform_http_port else {
169 continue;
170 };
171 if skip_bad_tls && seed_tls_deterministically_bad(seed.platform.as_ref()) {
172 continue;
173 }
174 let url = format!("https://{}:{}", seed.address.ip(), port);
175 if let Ok(uri) = url.parse::<Uri>() {
176 if let Ok(address) = Address::try_from(uri) {
177 list.add(address);
178 }
179 }
180 }
181 list
182}
183
184pub struct Sdk {
210 pub network: Network,
212 inner: SdkInstance,
213 proofs: bool,
217
218 nonce_cache: Arc<NonceCache>,
220
221 context_provider: ArcSwapOption<Box<dyn ContextProvider>>,
227
228 protocol_version: Arc<atomic::AtomicU32>,
230
231 version_pinned: bool,
235
236 protocol_version_observer: Option<ProtocolVersionObserver>,
239
240 metadata_last_seen_height: Arc<atomic::AtomicU64>,
244
245 metadata_height_tolerance: Option<u64>,
249
250 metadata_time_tolerance_ms: Option<u64>,
254
255 pub(crate) cancel_token: CancellationToken,
257
258 pub(crate) dapi_client_settings: RequestSettings,
260
261 #[cfg(feature = "mocks")]
262 dump_dir: Option<PathBuf>,
263}
264impl Clone for Sdk {
265 fn clone(&self) -> Self {
266 Self {
267 network: self.network,
268 inner: self.inner.clone(),
269 proofs: self.proofs,
270 nonce_cache: Arc::clone(&self.nonce_cache),
271 context_provider: ArcSwapOption::new(self.context_provider.load_full()),
272 cancel_token: self.cancel_token.clone(),
273 protocol_version: Arc::clone(&self.protocol_version),
274 version_pinned: self.version_pinned,
275 protocol_version_observer: self.protocol_version_observer.clone(),
276 metadata_last_seen_height: Arc::clone(&self.metadata_last_seen_height),
277 metadata_height_tolerance: self.metadata_height_tolerance,
278 metadata_time_tolerance_ms: self.metadata_time_tolerance_ms,
279 dapi_client_settings: self.dapi_client_settings,
280 #[cfg(feature = "mocks")]
281 dump_dir: self.dump_dir.clone(),
282 }
283 }
284}
285
286impl Debug for Sdk {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 match &self.inner {
289 SdkInstance::Dapi { dapi, .. } => f
290 .debug_struct("Sdk")
291 .field("dapi", dapi)
292 .field("proofs", &self.proofs)
293 .finish(),
294 #[cfg(feature = "mocks")]
295 SdkInstance::Mock { mock, .. } => f
296 .debug_struct("Sdk")
297 .field("mock", mock)
298 .field("proofs", &self.proofs)
299 .finish(),
300 }
301 }
302}
303
304#[derive(Debug, Clone)]
309enum SdkInstance {
310 Dapi {
312 dapi: DapiClient,
314 },
315 #[cfg(feature = "mocks")]
317 Mock {
318 dapi: Arc<Mutex<MockDapiClient>>,
322 mock: Arc<Mutex<MockDashPlatformSdk>>,
324 address_list: AddressList,
325 },
326}
327
328impl Sdk {
329 pub fn new_mock() -> Self {
335 SdkBuilder::default()
336 .build()
337 .expect("mock should be created")
338 }
339
340 fn freshness_criteria(&self, method_name: &str) -> (Option<u64>, Option<u64>) {
345 match method_name {
346 "get_addresses_trunk_state" | "get_addresses_branch_state" => (
347 self.metadata_time_tolerance_ms
352 .is_none()
353 .then_some(self.metadata_height_tolerance)
354 .flatten(),
355 self.metadata_time_tolerance_ms
356 .map(|configured| configured.min(DEFAULT_METADATA_TIME_TOLERANCE_MS)),
357 ),
358 _ => (
359 self.metadata_height_tolerance,
360 self.metadata_time_tolerance_ms,
361 ),
362 }
363 }
364
365 pub fn verify_response_metadata(
367 &self,
368 method_name: &str,
369 metadata: &ResponseMetadata,
370 ) -> Result<(), Error> {
371 let (metadata_height_tolerance, metadata_time_tolerance_ms) =
372 self.freshness_criteria(method_name);
373 if let Some(time_tolerance) = metadata_time_tolerance_ms {
376 let now = chrono::Utc::now().timestamp_millis() as u64;
377 verify_metadata_time(metadata, now, time_tolerance)?;
378 };
379 if let Some(height_tolerance) = metadata_height_tolerance {
380 verify_metadata_height(
381 metadata,
382 height_tolerance,
383 Arc::clone(&(self.metadata_last_seen_height)),
384 )?;
385 };
386
387 self.maybe_update_protocol_version(metadata.protocol_version);
388
389 Ok(())
390 }
391
392 fn maybe_update_protocol_version(&self, received_version: u32) {
398 if self.version_pinned {
399 return;
400 }
401
402 if received_version == 0 {
403 return;
404 }
405
406 let current = self.protocol_version.load(Ordering::Relaxed);
407
408 if received_version <= current {
409 return;
410 }
411
412 if PlatformVersion::get(received_version).is_err() {
414 tracing::warn!(
415 received_version,
416 current_version = current,
417 "received unknown protocol version from network; keeping current"
418 );
419 return;
420 }
421
422 let previous = self
423 .protocol_version
424 .fetch_max(received_version, Ordering::Relaxed);
425 if previous < received_version {
426 tracing::info!(
427 target: "dash_sdk::protocol_version",
428 from = previous,
429 to = received_version,
430 "ratcheting protocol version upward"
431 );
432 if let Some(observer) = &self.protocol_version_observer {
433 observer(received_version);
434 }
435 }
436 }
437
438 pub async fn refresh_protocol_version(&self) -> Result<u32, Error> {
468 if !self.prove() {
469 return Ok(self.protocol_version_number());
470 }
471 if !self.version_pinned {
472 if let Err(error) = ExtendedEpochInfo::fetch_current(self).await {
473 tracing::warn!(
474 target: "dash_sdk::protocol_version",
475 %error,
476 version = self.protocol_version_number(),
477 "proven protocol-version refresh failed; keeping the highest \
478 proof-verified version learned so far (never falling back to \
479 an unverified one)"
480 );
481 }
482 }
483 Ok(self.protocol_version_number())
484 }
485
486 pub(crate) async fn parse_proof_with_metadata_and_proof<R, O: FromProof<R> + MockResponse>(
511 &self,
512 request: O::Request,
513 response: O::Response,
514 method_name: &'static str,
515 ) -> Result<(Option<O>, ResponseMetadata, Proof), Error>
516 where
517 O::Request: Mockable,
518 {
519 let provider = self
520 .context_provider()
521 .ok_or(drive_proof_verifier::Error::ContextProviderNotSet)?;
522
523 let (object, metadata, proof) = match self.inner {
524 SdkInstance::Dapi { .. } => O::maybe_from_proof_with_metadata(
525 request,
526 response,
527 self.network,
528 self.version(),
529 &provider,
530 ),
531 #[cfg(feature = "mocks")]
532 SdkInstance::Mock { ref mock, .. } => {
533 let guard = mock.lock().await;
534 guard.parse_proof_with_metadata(request, response)
535 }
536 }?;
537
538 self.verify_response_metadata(method_name, &metadata)
543 .inspect_err(|err| {
544 tracing::warn!(%err,method=method_name,"received response with stale metadata; try another server");
545 })?;
546
547 Ok((object, metadata, proof))
548 }
549
550 pub fn context_provider(&self) -> Option<impl ContextProvider> {
552 let provider_guard = self.context_provider.load();
553 let provider = provider_guard.as_ref().map(Arc::clone);
554
555 provider
556 }
557
558 #[cfg(feature = "mocks")]
569 pub fn mock(&mut self) -> MutexGuard<'_, MockDashPlatformSdk> {
570 if let Sdk {
571 inner: SdkInstance::Mock { ref mock, .. },
572 ..
573 } = self
574 {
575 mock.try_lock()
576 .expect("mock sdk is in use by another thread and cannot be reconfigured")
577 } else {
578 panic!("not a mock")
579 }
580 }
581
582 pub async fn get_identity_nonce(
587 &self,
588 identity_id: Identifier,
589 bump_first: bool,
590 settings: Option<PutSettings>,
591 ) -> Result<IdentityNonce, Error> {
592 let settings = settings.unwrap_or_default();
593 let nonce = self
594 .nonce_cache
595 .get_identity_nonce(self, identity_id, bump_first, &settings)
596 .await?;
597
598 tracing::trace!(
599 identity_id = %identity_id,
600 bump_first,
601 nonce,
602 "Fetched identity nonce"
603 );
604
605 Ok(nonce)
606 }
607
608 pub async fn get_identity_contract_nonce(
613 &self,
614 identity_id: Identifier,
615 contract_id: Identifier,
616 bump_first: bool,
617 settings: Option<PutSettings>,
618 ) -> Result<IdentityNonce, Error> {
619 let settings = settings.unwrap_or_default();
620 self.nonce_cache
621 .get_identity_contract_nonce(self, identity_id, contract_id, bump_first, &settings)
622 .await
623 }
624
625 pub async fn refresh_identity_nonce(&self, identity_id: &Identifier) {
629 self.nonce_cache.refresh(identity_id).await;
630 }
631
632 pub fn version<'v>(&self) -> &'v PlatformVersion {
640 let v = self.protocol_version.load(Ordering::Relaxed);
641 PlatformVersion::get(v).unwrap_or_else(|_| PlatformVersion::latest())
642 }
643
644 pub fn protocol_version_number(&self) -> u32 {
646 self.protocol_version.load(Ordering::Relaxed)
647 }
648
649 pub fn prove(&self) -> bool {
652 self.proofs
653 }
654
655 pub fn query_settings(&self) -> crate::platform::QuerySettings<'_> {
662 crate::platform::QuerySettings {
663 request_settings: &self.dapi_client_settings,
664 protocol_version: self.version(),
665 prove: self.prove(),
666 }
667 }
668
669 pub fn set_context_provider<C: ContextProvider + 'static>(&self, context_provider: C) {
677 self.context_provider
678 .swap(Some(Arc::new(Box::new(context_provider))));
679 }
680
681 pub fn cancelled(&self) -> WaitForCancellationFuture<'_> {
683 self.cancel_token.cancelled()
684 }
685
686 pub fn shutdown(&self) {
688 self.cancel_token.cancel();
689 }
690
691 pub fn address_list(&self) -> &AddressList {
693 match &self.inner {
694 SdkInstance::Dapi { dapi, .. } => dapi.address_list(),
695 #[cfg(feature = "mocks")]
696 SdkInstance::Mock { address_list, .. } => address_list,
697 }
698 }
699
700 pub fn address_ban_info(&self) -> Vec<AddressBanInfo> {
707 self.address_list().ban_info()
708 }
709}
710
711pub(crate) fn verify_metadata_time(
719 metadata: &ResponseMetadata,
720 now_ms: u64,
721 tolerance_ms: u64,
722) -> Result<(), Error> {
723 let metadata_time = metadata.time_ms;
724
725 if now_ms.abs_diff(metadata_time) > tolerance_ms {
727 return Err(StaleNodeError::Time {
728 expected_timestamp_ms: now_ms,
729 received_timestamp_ms: metadata_time,
730 tolerance_ms,
731 }
732 .into());
733 }
734
735 tracing::trace!(
736 expected_time = now_ms,
737 received_time = metadata_time,
738 tolerance_ms,
739 "received response with valid time"
740 );
741 Ok(())
742}
743
744fn verify_metadata_height(
747 metadata: &ResponseMetadata,
748 tolerance: u64,
749 last_seen_height: Arc<atomic::AtomicU64>,
750) -> Result<(), Error> {
751 let received_height = metadata.height;
752 let previous_height = last_seen_height.fetch_max(received_height, Ordering::AcqRel);
756 let expected_height = previous_height.max(last_seen_height.load(Ordering::Acquire));
757
758 if expected_height > tolerance && received_height < expected_height.saturating_sub(tolerance) {
759 return Err(StaleNodeError::Height {
760 expected_height,
761 received_height,
762 tolerance_blocks: tolerance,
763 }
764 .into());
765 }
766
767 tracing::trace!(
768 expected_height,
769 received_height,
770 tolerance,
771 "received response within the monotonic height window"
772 );
773
774 Ok(())
775}
776
777#[async_trait::async_trait]
778impl DapiRequestExecutor for Sdk {
779 async fn execute<R: TransportRequest>(
780 &self,
781 request: R,
782 settings: RequestSettings,
783 ) -> ExecutionResult<R::Response, DapiClientError> {
784 match self.inner {
785 SdkInstance::Dapi { ref dapi, .. } => dapi.execute(request, settings).await,
786 #[cfg(feature = "mocks")]
787 SdkInstance::Mock { ref dapi, .. } => {
788 let dapi_guard = dapi.lock().await;
789 dapi_guard.execute(request, settings).await
790 }
791 }
792 }
793}
794
795pub struct SdkBuilder {
808 addresses: Option<AddressList>,
812 settings: Option<RequestSettings>,
813
814 network: Network,
815
816 core_ip: String,
817 core_port: u16,
818 core_user: String,
819 core_password: Zeroizing<String>,
820
821 proofs: bool,
823
824 version: Option<&'static PlatformVersion>,
827
828 version_pinned: bool,
832
833 protocol_version_observer: Option<ProtocolVersionObserver>,
835
836 #[cfg(feature = "mocks")]
838 data_contract_cache_size: NonZeroUsize,
839
840 #[cfg(feature = "mocks")]
842 token_config_cache_size: NonZeroUsize,
843
844 #[cfg(feature = "mocks")]
846 quorum_public_keys_cache_size: NonZeroUsize,
847
848 context_provider: Option<Box<dyn ContextProvider>>,
850
851 metadata_height_tolerance: Option<u64>,
856
857 metadata_time_tolerance_ms: Option<u64>,
861
862 trusted_initial_height: Option<u64>,
865
866 #[cfg(feature = "mocks")]
868 dump_dir: Option<PathBuf>,
869
870 pub(crate) cancel_token: CancellationToken,
872
873 #[cfg(not(target_arch = "wasm32"))]
875 ca_certificate: Option<Certificate>,
876}
877
878impl Default for SdkBuilder {
879 fn default() -> Self {
881 Self {
882 addresses: None,
883 settings: None,
884 network: Network::Mainnet,
885 core_ip: "".to_string(),
886 core_port: 0,
887 core_password: "".to_string().into(),
888 core_user: "".to_string(),
889
890 proofs: true,
891 metadata_height_tolerance: Some(1),
892 metadata_time_tolerance_ms: None,
893 trusted_initial_height: None,
894
895 #[cfg(feature = "mocks")]
896 data_contract_cache_size: NonZeroUsize::new(DEFAULT_CONTRACT_CACHE_SIZE)
897 .expect("data contract cache size must be positive"),
898
899 #[cfg(feature = "mocks")]
900 token_config_cache_size: NonZeroUsize::new(DEFAULT_TOKEN_CONFIG_CACHE_SIZE)
901 .expect("token config cache size must be positive"),
902
903 #[cfg(feature = "mocks")]
904 quorum_public_keys_cache_size: NonZeroUsize::new(DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE)
905 .expect("quorum public keys cache size must be positive"),
906
907 context_provider: None,
908
909 cancel_token: CancellationToken::new(),
910
911 version: None,
915 version_pinned: false,
916 protocol_version_observer: None,
917 #[cfg(not(target_arch = "wasm32"))]
918 ca_certificate: None,
919
920 #[cfg(feature = "mocks")]
921 dump_dir: None,
922 }
923 }
924}
925
926impl SdkBuilder {
927 pub fn with_proofs(mut self, proofs: bool) -> Self {
932 self.proofs = proofs;
933 self
934 }
935 pub fn new(addresses: AddressList) -> Self {
937 Self {
938 addresses: Some(addresses),
939 metadata_time_tolerance_ms: Some(DEFAULT_METADATA_TIME_TOLERANCE_MS),
940 ..Default::default()
941 }
942 }
943
944 pub fn with_address_list(mut self, addresses: AddressList) -> Self {
946 self.addresses = Some(addresses);
947 self
948 }
949
950 pub fn new_mock() -> Self {
952 Self::default()
953 }
954
955 pub fn new_testnet() -> Self {
961 let address_list = default_address_list_for_network(Network::Testnet);
962
963 Self::new(address_list).with_network(Network::Testnet)
964 }
965
966 pub fn new_mainnet() -> Self {
979 let address_list = default_address_list_for_network(Network::Mainnet);
980
981 Self::new(address_list).with_network(Network::Mainnet)
982 }
983
984 pub fn with_network(mut self, network: Network) -> Self {
988 self.network = network;
989 self
990 }
991
992 #[cfg(not(target_arch = "wasm32"))]
1002 pub fn with_ca_certificate(mut self, pem_certificate: Certificate) -> Self {
1003 self.ca_certificate = Some(pem_certificate);
1004 self
1005 }
1006
1007 #[cfg(not(target_arch = "wasm32"))]
1012 pub fn with_ca_certificate_file(
1013 self,
1014 certificate_file_path: impl AsRef<Path>,
1015 ) -> std::io::Result<Self> {
1016 let pem = std::fs::read(certificate_file_path)?;
1017 let cert = Certificate::from_pem(pem);
1018
1019 Ok(self.with_ca_certificate(cert))
1020 }
1021
1022 pub fn with_settings(mut self, settings: RequestSettings) -> Self {
1030 self.settings = Some(settings);
1031 self
1032 }
1033
1034 pub fn with_version(mut self, version: &'static PlatformVersion) -> Self {
1045 self.version = Some(version);
1046 self.version_pinned = true;
1047 self
1048 }
1049
1050 pub fn with_initial_version(mut self, version: &'static PlatformVersion) -> Self {
1066 self.version = Some(version);
1067 self.version_pinned = false;
1068 self
1069 }
1070
1071 pub fn with_protocol_version_observer(mut self, observer: ProtocolVersionObserver) -> Self {
1081 self.protocol_version_observer = Some(observer);
1082 self
1083 }
1084
1085 pub fn with_context_provider<C: ContextProvider + 'static>(
1092 mut self,
1093 context_provider: C,
1094 ) -> Self {
1095 self.context_provider = Some(Box::new(context_provider));
1096
1097 self
1098 }
1099
1100 pub fn with_cancellation_token(mut self, cancel_token: CancellationToken) -> Self {
1104 self.cancel_token = cancel_token;
1105 self
1106 }
1107
1108 pub fn with_core(mut self, ip: &str, port: u16, user: &str, password: &str) -> Self {
1116 self.core_ip = ip.to_string();
1117 self.core_port = port;
1118 self.core_user = user.to_string();
1119 self.core_password = Zeroizing::from(password.to_string());
1120
1121 self
1122 }
1123
1124 pub fn with_height_tolerance(mut self, tolerance: Option<u64>) -> Self {
1136 self.metadata_height_tolerance = tolerance;
1137 self
1138 }
1139
1140 pub fn with_time_tolerance(mut self, tolerance_ms: Option<u64>) -> Self {
1157 self.metadata_time_tolerance_ms = tolerance_ms;
1158 self
1159 }
1160
1161 pub fn with_trusted_initial_height(mut self, height: u64) -> Self {
1167 self.trusted_initial_height = Some(height);
1168 self
1169 }
1170
1171 #[cfg(feature = "mocks")]
1184 pub fn with_dump_dir(mut self, dump_dir: &Path) -> Self {
1185 self.dump_dir = Some(dump_dir.to_path_buf());
1186 self
1187 }
1188
1189 pub fn build(self) -> Result<Sdk, Error> {
1197 let is_network_sdk = self.addresses.is_some();
1198 let has_height_anchor = self
1199 .trusted_initial_height
1200 .zip(self.metadata_height_tolerance)
1201 .is_some_and(|(height, tolerance)| height > tolerance);
1202 if is_network_sdk
1203 && self.proofs
1204 && self.metadata_time_tolerance_ms.is_none()
1205 && !has_height_anchor
1206 {
1207 return Err(Error::Config(
1208 "proof mode requires a trusted initial height or signed-time freshness policy"
1209 .to_string(),
1210 ));
1211 }
1212
1213 let dapi_client_settings = match self.settings {
1214 Some(settings) => DEFAULT_REQUEST_SETTINGS.override_by(settings),
1215 None => DEFAULT_REQUEST_SETTINGS,
1216 };
1217
1218 let initial_version = self.version.unwrap_or_else(|| {
1219 PlatformVersion::get(min_protocol_version(self.network))
1220 .expect("min_protocol_version for a network must be a valid version")
1221 });
1222
1223 let sdk= match self.addresses {
1224 Some(addresses) => {
1226 #[allow(unused_mut)] let mut dapi = DapiClient::new(addresses, dapi_client_settings);
1228 #[cfg(not(target_arch = "wasm32"))]
1229 if let Some(pem) = self.ca_certificate {
1230 dapi = dapi.with_ca_certificate(pem);
1231 }
1232
1233 #[cfg(feature = "mocks")]
1234 let dapi = dapi.dump_dir(self.dump_dir.clone());
1235
1236 #[allow(unused_mut)] let mut sdk= Sdk{
1238 network: self.network,
1239 dapi_client_settings,
1240 inner:SdkInstance::Dapi { dapi },
1241 proofs:self.proofs,
1242 context_provider: ArcSwapOption::new( self.context_provider.map(Arc::new)),
1243 cancel_token: self.cancel_token,
1244 nonce_cache: Default::default(),
1245 protocol_version: Arc::new(atomic::AtomicU32::new(initial_version.protocol_version)),
1248 version_pinned: self.version_pinned,
1249 protocol_version_observer: self.protocol_version_observer.clone(),
1250 metadata_last_seen_height: Arc::new(atomic::AtomicU64::new(
1251 self.trusted_initial_height.unwrap_or(0),
1252 )),
1253 metadata_height_tolerance: self.metadata_height_tolerance,
1254 metadata_time_tolerance_ms: self.metadata_time_tolerance_ms,
1255 #[cfg(feature = "mocks")]
1256 dump_dir: self.dump_dir,
1257 };
1258 if sdk.context_provider.load().is_none() {
1260 #[cfg(feature = "mocks")]
1261 if !self.core_ip.is_empty() {
1262 tracing::warn!(
1263 "ContextProvider not set, falling back to a mock one; use SdkBuilder::with_context_provider() to set it up");
1264 let mut context_provider = GrpcContextProvider::new(None,
1265 &self.core_ip, self.core_port, &self.core_user, &self.core_password,
1266 self.data_contract_cache_size, self.token_config_cache_size, self.quorum_public_keys_cache_size)?;
1267 #[cfg(feature = "mocks")]
1268 if sdk.dump_dir.is_some() {
1269 context_provider.set_dump_dir(sdk.dump_dir.clone());
1270 }
1271 let context_provider= Arc::new(context_provider);
1274 sdk.context_provider.swap(Some(Arc::new(Box::new(context_provider.clone()))));
1275 context_provider.set_sdk(Some(sdk.clone()));
1276 } else{
1277 return Err(Error::Config(concat!(
1278 "context provider is not set, configure it with SdkBuilder::with_context_provider() ",
1279 "or configure Core access with SdkBuilder::with_core() to use mock context provider")
1280 .to_string()));
1281 }
1282 #[cfg(not(feature = "mocks"))]
1283 return Err(Error::Config(concat!(
1284 "context provider is not set, configure it with SdkBuilder::with_context_provider() ",
1285 "or enable `mocks` feature to use mock context provider")
1286 .to_string()));
1287 };
1288
1289 sdk
1290 },
1291 #[cfg(feature = "mocks")]
1292 None => {
1294 let dapi =Arc::new(Mutex::new( MockDapiClient::new()));
1295 let context_provider = self.context_provider.unwrap_or_else(||{
1297 let mut cp=MockContextProvider::new();
1298 if let Some(ref dump_dir) = self.dump_dir {
1299 cp.quorum_keys_dir(Some(dump_dir.clone()));
1300 }
1301 Box::new(cp)
1302 }
1303 );
1304 let mock_sdk = MockDashPlatformSdk::new(Arc::clone(&dapi));
1305 let mock_sdk = Arc::new(Mutex::new(mock_sdk));
1306 let sdk= Sdk {
1307 network: self.network,
1308 dapi_client_settings,
1309 inner:SdkInstance::Mock {
1310 mock:mock_sdk.clone(),
1311 dapi,
1312 address_list: AddressList::new(),
1313 },
1314 dump_dir: self.dump_dir.clone(),
1315 proofs:self.proofs,
1316 nonce_cache: Default::default(),
1317 protocol_version: Arc::new(atomic::AtomicU32::new(initial_version.protocol_version)),
1318 version_pinned: self.version_pinned,
1319 protocol_version_observer: self.protocol_version_observer.clone(),
1320 context_provider: ArcSwapOption::new(Some(Arc::new(context_provider))),
1321 cancel_token: self.cancel_token,
1322 metadata_last_seen_height: Arc::new(atomic::AtomicU64::new(
1323 self.trusted_initial_height.unwrap_or(0),
1324 )),
1325 metadata_height_tolerance: self.metadata_height_tolerance,
1326 metadata_time_tolerance_ms: self.metadata_time_tolerance_ms,
1327 };
1328 let mut guard = mock_sdk.try_lock().expect("mock sdk is in use by another thread and cannot be reconfigured");
1329 guard.set_sdk(sdk.clone());
1330 if let Some(ref dump_dir) = self.dump_dir {
1331 guard.load_expectations_sync(dump_dir)?;
1332 };
1333
1334 sdk
1335 },
1336 #[cfg(not(feature = "mocks"))]
1337 None => return Err(Error::Config("Mock mode is not available. Please enable `mocks` feature or provide address list.".to_string())),
1338 };
1339
1340 Ok(sdk)
1341 }
1342}
1343
1344pub fn prettify_proof(proof: &Proof) -> String {
1345 let config = bincode::config::standard()
1346 .with_big_endian()
1347 .with_no_limit();
1348 let grovedb_proof: Result<GroveDBProof, DecodeError> =
1349 bincode::decode_from_slice_untrusted(&proof.grovedb_proof, config).map(|(a, _)| a);
1350
1351 let grovedb_proof_string = match grovedb_proof {
1352 Ok(proof) => format!("{}", proof),
1353 Err(_) => "Invalid GroveDBProof".to_string(),
1354 };
1355 format!(
1356 "Proof {{
1357 grovedb_proof: {},
1358 quorum_hash: 0x{},
1359 signature: 0x{},
1360 round: {},
1361 block_id_hash: 0x{},
1362 quorum_type: {},
1363 }}",
1364 grovedb_proof_string,
1365 hex::encode(&proof.quorum_hash),
1366 hex::encode(&proof.signature),
1367 proof.round,
1368 hex::encode(&proof.block_id_hash),
1369 proof.quorum_type,
1370 )
1371}
1372
1373#[cfg(test)]
1374mod test {
1375 use std::sync::Arc;
1376
1377 use dapi_grpc::platform::v0::{GetIdentityRequest, ResponseMetadata};
1378 use rs_dapi_client::transport::TransportRequest;
1379 use test_case::test_matrix;
1380
1381 use crate::SdkBuilder;
1382
1383 use super::{min_protocol_version, Network};
1384
1385 const MAINNET_PLATFORM_HTTP_PORT: u16 = 443;
1387 const TESTNET_PLATFORM_HTTP_PORT: u16 = 1443;
1389
1390 #[test]
1391 fn new_testnet_sources_bootstrap_from_seeds() {
1392 let builder = SdkBuilder::new_testnet();
1393 let address_list = builder
1394 .addresses
1395 .as_ref()
1396 .expect("testnet builder should configure default addresses");
1397
1398 assert_eq!(builder.network, Network::Testnet);
1399 assert!(
1400 !address_list.is_empty(),
1401 "testnet must have at least one bootstrap address"
1402 );
1403 for address in address_list.get_live_addresses() {
1404 assert_eq!(
1405 address.uri().port_u16(),
1406 Some(TESTNET_PLATFORM_HTTP_PORT),
1407 "testnet bootstrap address must use the platform HTTP port",
1408 );
1409 }
1410 }
1411
1412 #[test]
1413 fn new_mainnet_sources_bootstrap_from_seeds() {
1414 let builder = SdkBuilder::new_mainnet();
1415 let address_list = builder
1416 .addresses
1417 .as_ref()
1418 .expect("mainnet builder should configure default addresses");
1419
1420 assert_eq!(builder.network, Network::Mainnet);
1421 assert!(
1422 !address_list.is_empty(),
1423 "mainnet must have at least one bootstrap address"
1424 );
1425 for address in address_list.get_live_addresses() {
1426 assert_eq!(
1427 address.uri().port_u16(),
1428 Some(MAINNET_PLATFORM_HTTP_PORT),
1429 "mainnet bootstrap address must use the platform HTTP port",
1430 );
1431 }
1432 }
1433
1434 mod seed_tls_filter {
1435 use super::super::{address_list_from_seeds, seed_tls_deterministically_bad};
1436 use dash_network_seeds::{
1437 CoreStatus, MasternodeSeed, MasternodeType, PlatformStatus, Reachability, SslStatus,
1438 };
1439
1440 fn seed(host: u8, platform: Option<PlatformStatus>) -> MasternodeSeed {
1443 MasternodeSeed {
1444 address: format!("203.0.113.{host}:9999").parse().unwrap(),
1445 mn_type: MasternodeType::Evo,
1446 platform_http_port: Some(443),
1447 core: CoreStatus::default(),
1448 platform,
1449 }
1450 }
1451
1452 fn status(ssl: SslStatus, reachable: Reachability) -> PlatformStatus {
1453 PlatformStatus {
1454 reachable,
1455 ssl,
1456 ..PlatformStatus::default()
1457 }
1458 }
1459
1460 #[test]
1466 fn classification_covers_every_status_combination() {
1467 let reachabilities = [
1468 Reachability::Unknown,
1469 Reachability::Ok,
1470 Reachability::Timeout,
1471 Reachability::Refused,
1472 Reachability::Error,
1473 ];
1474 for reachable in reachabilities {
1475 for ssl in [
1476 SslStatus::Expired,
1477 SslStatus::SelfSigned,
1478 SslStatus::Untrusted,
1479 ] {
1480 assert!(
1481 seed_tls_deterministically_bad(Some(&status(ssl, reachable))),
1482 "{ssl:?} must be rejected regardless of {reachable:?}"
1483 );
1484 }
1485 for ssl in [SslStatus::Valid, SslStatus::Unknown] {
1486 assert!(
1487 !seed_tls_deterministically_bad(Some(&status(ssl, reachable))),
1488 "{ssl:?} must never be rejected ({reachable:?})"
1489 );
1490 }
1491 assert_eq!(
1492 seed_tls_deterministically_bad(Some(&status(
1493 SslStatus::NoHandshake,
1494 reachable
1495 ))),
1496 reachable == Reachability::Ok,
1497 "NoHandshake must be rejected only when TCP connect succeeded ({reachable:?})"
1498 );
1499 }
1500 assert!(
1501 !seed_tls_deterministically_bad(None),
1502 "an unprobed seed must never be rejected"
1503 );
1504 }
1505
1506 #[test]
1507 fn filter_drops_only_deterministic_failures() {
1508 let seeds = vec![
1509 seed(1, Some(status(SslStatus::Valid, Reachability::Ok))),
1510 seed(2, Some(status(SslStatus::Expired, Reachability::Ok))),
1511 seed(
1512 3,
1513 Some(status(SslStatus::NoHandshake, Reachability::Timeout)),
1514 ),
1515 seed(4, Some(status(SslStatus::NoHandshake, Reachability::Ok))),
1516 seed(5, None),
1517 ];
1518 assert_eq!(address_list_from_seeds(&seeds, true).len(), 3);
1519 assert_eq!(address_list_from_seeds(&seeds, false).len(), 5);
1520 }
1521
1522 #[test]
1526 fn all_rejected_input_yields_empty_filtered_and_full_unfiltered() {
1527 let seeds = vec![
1528 seed(1, Some(status(SslStatus::Expired, Reachability::Ok))),
1529 seed(2, Some(status(SslStatus::Untrusted, Reachability::Timeout))),
1530 ];
1531 assert!(address_list_from_seeds(&seeds, true).is_empty());
1532 assert_eq!(address_list_from_seeds(&seeds, false).len(), 2);
1533 }
1534
1535 #[test]
1536 fn seed_without_platform_port_is_always_skipped() {
1537 let mut no_port = seed(1, Some(status(SslStatus::Valid, Reachability::Ok)));
1538 no_port.platform_http_port = None;
1539 assert!(address_list_from_seeds(&[no_port], false).is_empty());
1540 }
1541 }
1542
1543 #[test]
1547 fn bootstrap_counts_reasonable() {
1548 let mainnet = SdkBuilder::new_mainnet()
1549 .addresses
1550 .expect("mainnet builder should configure default addresses");
1551 let testnet = SdkBuilder::new_testnet()
1552 .addresses
1553 .expect("testnet builder should configure default addresses");
1554 assert!(
1555 mainnet.len() >= 10,
1556 "expected >=10 mainnet bootstrap addresses, got {}",
1557 mainnet.len()
1558 );
1559 assert!(
1560 testnet.len() >= 10,
1561 "expected >=10 testnet bootstrap addresses, got {}",
1562 testnet.len()
1563 );
1564 }
1565
1566 #[test]
1567 fn network_builders_enable_an_independent_time_anchor() {
1568 assert_eq!(
1569 SdkBuilder::new_testnet().metadata_time_tolerance_ms,
1570 Some(super::DEFAULT_METADATA_TIME_TOLERANCE_MS)
1571 );
1572 assert_eq!(SdkBuilder::new_mock().metadata_time_tolerance_ms, None);
1573 }
1574
1575 #[test]
1576 fn proof_enabled_network_builder_rejects_missing_freshness_anchor() {
1577 let error = SdkBuilder::new(super::AddressList::new())
1578 .with_time_tolerance(None)
1579 .build()
1580 .expect_err("network proof mode must have an independent freshness anchor");
1581
1582 assert!(
1583 matches!(error, crate::Error::Config(message) if message.contains("trusted initial height"))
1584 );
1585 }
1586
1587 #[test_matrix(0, 0; "zero height")]
1588 #[test_matrix(1, 1; "height equals tolerance")]
1589 #[test_matrix(1, 2; "height below tolerance")]
1590 fn proof_enabled_network_builder_rejects_ineffective_height_anchor(
1591 trusted_height: u64,
1592 tolerance: u64,
1593 ) {
1594 let error = SdkBuilder::new(super::AddressList::new())
1595 .with_time_tolerance(None)
1596 .with_height_tolerance(Some(tolerance))
1597 .with_trusted_initial_height(trusted_height)
1598 .build()
1599 .expect_err("trusted height must impose a freshness floor");
1600
1601 assert!(
1602 matches!(error, crate::Error::Config(message) if message.contains("trusted initial height"))
1603 );
1604 }
1605
1606 #[test]
1607 fn height_only_address_checkpoint_uses_trusted_height_floor() {
1608 let sdk = SdkBuilder::new_mock()
1609 .with_time_tolerance(None)
1610 .with_height_tolerance(Some(2))
1611 .with_trusted_initial_height(100)
1612 .build()
1613 .expect("effective trusted height should permit height-only proof mode");
1614
1615 assert!(matches!(
1616 sdk.verify_response_metadata(
1617 "get_addresses_trunk_state",
1618 &ResponseMetadata {
1619 height: 97,
1620 ..Default::default()
1621 },
1622 ),
1623 Err(crate::Error::StaleNode(
1624 super::StaleNodeError::Height { .. }
1625 ))
1626 ));
1627 assert_eq!(
1628 sdk.metadata_last_seen_height
1629 .load(std::sync::atomic::Ordering::Acquire),
1630 100,
1631 "a rejected stale checkpoint must not lower the trusted floor"
1632 );
1633 }
1634
1635 #[test]
1636 fn trusted_initial_height_seeds_the_high_water_mark() {
1637 let sdk = SdkBuilder::new_mock()
1638 .with_trusted_initial_height(42)
1639 .build()
1640 .expect("mock SDK should build");
1641
1642 assert_eq!(
1643 sdk.metadata_last_seen_height
1644 .load(std::sync::atomic::Ordering::Acquire),
1645 42
1646 );
1647 }
1648
1649 #[test_matrix(97..102, 100, 2, false; "valid height")]
1650 #[test_case(103, 100, 2, true; "invalid height")]
1651 fn test_verify_metadata_height(
1652 expected_height: u64,
1653 received_height: u64,
1654 tolerance: u64,
1655 expect_err: bool,
1656 ) {
1657 let metadata = ResponseMetadata {
1658 height: received_height,
1659 ..Default::default()
1660 };
1661
1662 let last_seen_height = Arc::new(std::sync::atomic::AtomicU64::new(expected_height));
1663
1664 let result =
1665 super::verify_metadata_height(&metadata, tolerance, Arc::clone(&last_seen_height));
1666
1667 assert_eq!(result.is_err(), expect_err);
1668 if result.is_ok() {
1669 assert_eq!(
1670 last_seen_height.load(std::sync::atomic::Ordering::Relaxed),
1671 expected_height.max(received_height),
1672 "height high-water mark must never decrease"
1673 );
1674 }
1675 }
1676
1677 #[test]
1678 fn accepted_height_tolerance_cannot_walk_the_watermark_backwards() {
1679 let last_seen_height = Arc::new(std::sync::atomic::AtomicU64::new(100));
1680
1681 super::verify_metadata_height(
1682 &ResponseMetadata {
1683 height: 99,
1684 ..Default::default()
1685 },
1686 1,
1687 Arc::clone(&last_seen_height),
1688 )
1689 .expect("one block behind is within tolerance");
1690 assert_eq!(
1691 last_seen_height.load(std::sync::atomic::Ordering::Acquire),
1692 100
1693 );
1694
1695 super::verify_metadata_height(
1696 &ResponseMetadata {
1697 height: 98,
1698 ..Default::default()
1699 },
1700 1,
1701 Arc::clone(&last_seen_height),
1702 )
1703 .expect_err("a second rollback step must be compared with the high-water mark");
1704 assert_eq!(
1705 last_seen_height.load(std::sync::atomic::Ordering::Acquire),
1706 100
1707 );
1708
1709 super::verify_metadata_height(
1710 &ResponseMetadata {
1711 height: 101,
1712 ..Default::default()
1713 },
1714 1,
1715 Arc::clone(&last_seen_height),
1716 )
1717 .expect("a newer height should advance the high-water mark");
1718 assert_eq!(
1719 last_seen_height.load(std::sync::atomic::Ordering::Acquire),
1720 101
1721 );
1722 }
1723
1724 #[test]
1725 fn cloned_sdk_verify_metadata_height() {
1726 let sdk1 = SdkBuilder::new_mock()
1727 .build()
1728 .expect("mock Sdk should be created");
1729
1730 let metadata = ResponseMetadata {
1732 height: 1,
1733 ..Default::default()
1734 };
1735
1736 let request = GetIdentityRequest::default();
1738 sdk1.verify_response_metadata(request.method_name(), &metadata)
1739 .expect("metadata should be valid");
1740
1741 assert_eq!(
1742 sdk1.metadata_last_seen_height
1743 .load(std::sync::atomic::Ordering::Relaxed),
1744 metadata.height,
1745 "initial height"
1746 );
1747
1748 let sdk2 = sdk1.clone();
1750 let sdk3 = sdk1.clone();
1751
1752 let metadata = ResponseMetadata {
1754 height: 2,
1755 ..Default::default()
1756 };
1757 let request = GetIdentityRequest::default();
1759 sdk2.verify_response_metadata(request.method_name(), &metadata)
1760 .expect("metadata should be valid");
1761
1762 assert_eq!(
1763 sdk1.metadata_last_seen_height
1764 .load(std::sync::atomic::Ordering::Relaxed),
1765 metadata.height,
1766 "first sdk should see height from second sdk"
1767 );
1768 assert_eq!(
1769 sdk3.metadata_last_seen_height
1770 .load(std::sync::atomic::Ordering::Relaxed),
1771 metadata.height,
1772 "third sdk should see height from second sdk"
1773 );
1774
1775 let metadata = ResponseMetadata {
1777 height: 3,
1778 ..Default::default()
1779 };
1780 let request = GetIdentityRequest::default();
1782 sdk3.verify_response_metadata(request.method_name(), &metadata)
1783 .expect("metadata should be valid");
1784
1785 assert_eq!(
1786 sdk1.metadata_last_seen_height
1787 .load(std::sync::atomic::Ordering::Relaxed),
1788 metadata.height,
1789 "first sdk should see height from third sdk"
1790 );
1791
1792 assert_eq!(
1793 sdk2.metadata_last_seen_height
1794 .load(std::sync::atomic::Ordering::Relaxed),
1795 metadata.height,
1796 "second sdk should see height from third sdk"
1797 );
1798
1799 let metadata = ResponseMetadata {
1801 height: 1,
1802 ..Default::default()
1803 };
1804
1805 let request = GetIdentityRequest::default();
1806 sdk1.verify_response_metadata(request.method_name(), &metadata)
1807 .expect_err("metadata should be invalid");
1808 }
1809
1810 fn mock_sdk_with_auto_detect(starting_version: u32) -> super::Sdk {
1813 use std::sync::atomic::Ordering;
1814
1815 let sdk = SdkBuilder::new_mock()
1816 .build()
1817 .expect("mock Sdk should be created");
1818 sdk.protocol_version
1819 .store(starting_version, Ordering::Relaxed);
1820 sdk
1821 }
1822
1823 #[test]
1824 fn test_version_update_from_metadata() {
1825 let sdk = mock_sdk_with_auto_detect(1);
1826
1827 assert_eq!(sdk.protocol_version_number(), 1);
1828
1829 let metadata = ResponseMetadata {
1830 protocol_version: 2,
1831 height: 1,
1832 ..Default::default()
1833 };
1834
1835 sdk.verify_response_metadata("test", &metadata)
1836 .expect("metadata should be valid");
1837
1838 assert_eq!(sdk.protocol_version_number(), 2);
1839 assert_eq!(sdk.version().protocol_version, 2);
1840 }
1841
1842 #[test]
1843 fn test_unknown_version_ignored() {
1844 use dpp::version::PlatformVersion;
1845
1846 let sdk = mock_sdk_with_auto_detect(PlatformVersion::latest().protocol_version);
1847 let original_version = sdk.protocol_version_number();
1848
1849 let metadata = ResponseMetadata {
1850 protocol_version: 999,
1851 height: 1,
1852 ..Default::default()
1853 };
1854
1855 sdk.verify_response_metadata("test", &metadata)
1856 .expect("metadata should be valid");
1857
1858 assert_eq!(sdk.protocol_version_number(), original_version);
1859 assert_eq!(sdk.version().protocol_version, original_version);
1860 }
1861
1862 #[test]
1863 fn test_version_shared_between_clones() {
1864 let sdk = mock_sdk_with_auto_detect(1);
1865
1866 let clone = sdk.clone();
1867
1868 let metadata = ResponseMetadata {
1869 protocol_version: 2,
1870 height: 1,
1871 ..Default::default()
1872 };
1873
1874 clone
1875 .verify_response_metadata("test", &metadata)
1876 .expect("metadata should be valid");
1877
1878 assert_eq!(
1879 sdk.protocol_version_number(),
1880 2,
1881 "original should see update from clone"
1882 );
1883 }
1884
1885 #[test]
1886 fn test_version_downgrade_ignored() {
1887 let sdk = mock_sdk_with_auto_detect(2);
1888
1889 assert_eq!(sdk.protocol_version_number(), 2);
1890
1891 let metadata = ResponseMetadata {
1892 protocol_version: 1,
1893 height: 1,
1894 ..Default::default()
1895 };
1896
1897 sdk.verify_response_metadata("test", &metadata)
1898 .expect("metadata should be valid");
1899
1900 assert_eq!(sdk.protocol_version_number(), 2);
1901 }
1902
1903 #[test]
1904 fn test_version_zero_ignored() {
1905 use dpp::version::PlatformVersion;
1906
1907 let sdk = mock_sdk_with_auto_detect(PlatformVersion::latest().protocol_version);
1908 let original_version = sdk.protocol_version_number();
1909
1910 let metadata = ResponseMetadata {
1911 protocol_version: 0,
1912 height: 1,
1913 ..Default::default()
1914 };
1915
1916 sdk.verify_response_metadata("test", &metadata)
1917 .expect("metadata should be valid");
1918
1919 assert_eq!(sdk.protocol_version_number(), original_version);
1920 }
1921
1922 #[test]
1923 fn test_concurrent_updates_converge_to_highest() {
1924 use std::thread;
1925
1926 let sdk = mock_sdk_with_auto_detect(1);
1927
1928 assert_eq!(sdk.protocol_version_number(), 1);
1929
1930 let mut handles = Vec::new();
1931 for version in [2u32, 3, 2, 3, 2, 3] {
1933 let sdk_clone = sdk.clone();
1934 handles.push(thread::spawn(move || {
1935 let metadata = ResponseMetadata {
1936 protocol_version: version,
1937 height: 1,
1938 ..Default::default()
1939 };
1940 sdk_clone
1941 .verify_response_metadata("test", &metadata)
1942 .expect("metadata should be valid");
1943 }));
1944 }
1945
1946 for h in handles {
1947 h.join().expect("thread should not panic");
1948 }
1949
1950 assert_eq!(
1952 sdk.protocol_version_number(),
1953 3,
1954 "concurrent updates must converge to highest version"
1955 );
1956 }
1957
1958 #[test]
1962 fn test_explicit_version_disables_auto_detect() {
1963 use dpp::version::PlatformVersion;
1964
1965 let pinned = PlatformVersion::get(min_protocol_version(Network::Mainnet))
1968 .expect("mainnet-floor PV exists");
1969 let sdk = SdkBuilder::new_mock()
1970 .with_version(pinned)
1971 .build()
1972 .expect("mock Sdk should be created");
1973
1974 assert_eq!(sdk.protocol_version_number(), pinned.protocol_version);
1975 assert!(sdk.version_pinned);
1976
1977 let metadata = ResponseMetadata {
1979 protocol_version: dpp::version::v14::PROTOCOL_VERSION_14,
1980 height: 1,
1981 ..Default::default()
1982 };
1983
1984 sdk.verify_response_metadata("test", &metadata)
1985 .expect("metadata should be valid");
1986
1987 assert_eq!(
1988 sdk.protocol_version_number(),
1989 pinned.protocol_version,
1990 "pinned version must not be auto-updated"
1991 );
1992 }
1993
1994 #[test]
1995 fn test_with_initial_version_seeds_to_older_network_version() {
1996 use dpp::version::PlatformVersion;
1997
1998 let floor = min_protocol_version(Network::Mainnet);
2002 let initial = PlatformVersion::get(floor).expect("mainnet-floor PV exists");
2003 let sdk = SdkBuilder::new_mock()
2004 .with_initial_version(initial)
2005 .build()
2006 .expect("mock Sdk should be created");
2007
2008 assert_eq!(
2009 sdk.protocol_version_number(),
2010 floor,
2011 "with_initial_version must seed the atomic without pinning"
2012 );
2013 assert_eq!(sdk.version().protocol_version, floor);
2014 assert!(
2015 !sdk.version_pinned,
2016 "with_initial_version must keep auto-detect enabled"
2017 );
2018
2019 let metadata = ResponseMetadata {
2021 protocol_version: floor,
2022 height: 1,
2023 ..Default::default()
2024 };
2025 sdk.verify_response_metadata("test", &metadata)
2026 .expect("metadata should be valid");
2027 assert_eq!(sdk.protocol_version_number(), floor);
2028
2029 let newer = dpp::version::v14::PROTOCOL_VERSION_14;
2031 assert!(newer > floor, "ratchet target must exceed the floor");
2032 let metadata = ResponseMetadata {
2033 protocol_version: newer,
2034 height: 2,
2035 ..Default::default()
2036 };
2037 sdk.verify_response_metadata("test", &metadata)
2038 .expect("metadata should be valid");
2039 assert_eq!(sdk.protocol_version_number(), newer);
2040 }
2041
2042 #[test]
2043 fn test_with_initial_version_after_with_version_restores_auto_detect() {
2044 use dpp::version::PlatformVersion;
2045
2046 let v_latest = PlatformVersion::latest();
2053 let v_old = PlatformVersion::get(min_protocol_version(Network::Mainnet))
2054 .expect("mainnet-floor PV exists");
2055 assert!(
2056 v_old.protocol_version < v_latest.protocol_version,
2057 "v_old must be below latest so the later ratchet is observable"
2058 );
2059
2060 let sdk = SdkBuilder::new_mock()
2061 .with_version(v_latest)
2062 .with_initial_version(v_old)
2063 .build()
2064 .expect("mock Sdk should be created");
2065
2066 assert_eq!(
2067 sdk.protocol_version_number(),
2068 v_old.protocol_version,
2069 "with_initial_version must overwrite the prior with_version seed"
2070 );
2071 assert!(
2072 !sdk.version_pinned,
2073 "with_initial_version must restore auto-detect after with_version disabled it"
2074 );
2075
2076 let metadata = ResponseMetadata {
2078 protocol_version: v_latest.protocol_version,
2079 height: 1,
2080 ..Default::default()
2081 };
2082 sdk.verify_response_metadata("test", &metadata)
2083 .expect("metadata should be valid");
2084 assert_eq!(sdk.protocol_version_number(), v_latest.protocol_version);
2085 }
2086
2087 #[test]
2088 fn test_mock_version_follows_outer_sdk_atomic() {
2089 use dpp::version::PlatformVersion;
2090
2091 let v_old = PlatformVersion::get(min_protocol_version(Network::Mainnet))
2097 .expect("mainnet-floor PV exists");
2098 let v_new = PlatformVersion::latest();
2099 assert!(
2100 v_old.protocol_version < v_new.protocol_version,
2101 "v_old must be below latest so the ratchet is observable"
2102 );
2103
2104 let mut sdk = SdkBuilder::new_mock()
2105 .with_initial_version(v_old)
2106 .build()
2107 .expect("mock Sdk should be created");
2108
2109 assert_eq!(sdk.version().protocol_version, v_old.protocol_version);
2110 {
2111 let mock = sdk.mock();
2112 assert_eq!(
2113 mock.version().protocol_version,
2114 v_old.protocol_version,
2115 "mock version must mirror outer SDK before ratchet"
2116 );
2117 }
2118
2119 let metadata = ResponseMetadata {
2120 protocol_version: v_new.protocol_version,
2121 height: 1,
2122 ..Default::default()
2123 };
2124 sdk.verify_response_metadata("test", &metadata)
2125 .expect("metadata should be valid");
2126
2127 assert_eq!(sdk.version().protocol_version, v_new.protocol_version);
2128 let mock = sdk.mock();
2129 assert_eq!(
2130 mock.version().protocol_version,
2131 v_new.protocol_version,
2132 "mock version must follow outer ratchet"
2133 );
2134 }
2135
2136 #[test]
2137 fn test_default_builder_seeds_initial_protocol_version_floor() {
2138 let sdk = SdkBuilder::new_mock()
2141 .build()
2142 .expect("mock Sdk should be created");
2143
2144 let expected = min_protocol_version(Network::Mainnet);
2145 assert_eq!(
2146 sdk.protocol_version_number(),
2147 expected,
2148 "unpinned mainnet SDK must boot at the mainnet floor, not latest()"
2149 );
2150 assert_eq!(sdk.version().protocol_version, expected);
2151 assert!(
2152 !sdk.version_pinned,
2153 "default SDK must keep auto-detect enabled"
2154 );
2155 }
2156
2157 #[test]
2158 fn test_default_floor_ratchets_up_but_never_down() {
2159 let sdk = SdkBuilder::new_mock()
2160 .build()
2161 .expect("mock Sdk should be created");
2162 let floor = min_protocol_version(Network::Mainnet);
2164 assert_eq!(sdk.protocol_version_number(), floor);
2165
2166 let target = dpp::version::v14::PROTOCOL_VERSION_14;
2169 assert!(
2170 target > floor,
2171 "ratchet test target must exceed the floor; bump it if the floor reaches v14"
2172 );
2173 sdk.maybe_update_protocol_version(target);
2174 assert_eq!(
2175 sdk.protocol_version_number(),
2176 target,
2177 "auto-detect must ratchet upward from the floor"
2178 );
2179
2180 sdk.maybe_update_protocol_version(floor - 1);
2182 assert_eq!(
2183 sdk.protocol_version_number(),
2184 target,
2185 "ratchet must never downgrade below the highest observed version"
2186 );
2187 }
2188
2189 #[test]
2200 fn test_ratchet_rejects_unknown_and_non_upward_versions() {
2201 let sdk = SdkBuilder::new_mock()
2202 .build()
2203 .expect("mock Sdk should be created");
2204 let floor = min_protocol_version(Network::Mainnet);
2206 assert_eq!(sdk.protocol_version_number(), floor);
2207
2208 sdk.maybe_update_protocol_version(dpp::version::LATEST_VERSION + 1);
2210 assert_eq!(
2211 sdk.protocol_version_number(),
2212 floor,
2213 "unknown protocol version must not move the stored version"
2214 );
2215
2216 sdk.maybe_update_protocol_version(0);
2218 assert_eq!(
2219 sdk.protocol_version_number(),
2220 floor,
2221 "zero protocol version must be ignored"
2222 );
2223
2224 sdk.maybe_update_protocol_version(floor);
2226 assert_eq!(sdk.protocol_version_number(), floor);
2227
2228 sdk.maybe_update_protocol_version(floor - 1);
2230 assert_eq!(
2231 sdk.protocol_version_number(),
2232 floor,
2233 "lower known version must not downgrade the stored version"
2234 );
2235 }
2236
2237 #[test]
2240 fn test_explicit_pin_below_floor_is_preserved() {
2241 use dpp::version::PlatformVersion;
2242
2243 let floor = min_protocol_version(Network::Mainnet);
2244 let below = floor - 1;
2245 let pinned = PlatformVersion::get(below).expect("sub-floor PV exists");
2246 let sdk = SdkBuilder::new_mock()
2247 .with_version(pinned)
2248 .build()
2249 .expect("mock Sdk should be created");
2250
2251 assert_eq!(
2252 sdk.protocol_version_number(),
2253 below,
2254 "a pin below the floor must be preserved"
2255 );
2256 assert!(sdk.version_pinned);
2258 }
2259
2260 #[test]
2268 fn test_testnet_default_builder_boots_at_per_network_floor() {
2269 let sdk = SdkBuilder::new_mock()
2270 .with_network(Network::Testnet)
2271 .build()
2272 .expect("mock Sdk should be created");
2273
2274 assert_eq!(
2275 sdk.protocol_version_number(),
2276 min_protocol_version(Network::Testnet),
2277 "testnet seeds directly at its per-network floor"
2278 );
2279 assert!(!sdk.version_pinned);
2280 }
2281
2282 #[test]
2286 fn test_per_network_floors() {
2287 assert_eq!(
2288 min_protocol_version(Network::Devnet),
2289 dpp::version::v14::PROTOCOL_VERSION_14,
2290 "devnet floor must be the current development version"
2291 );
2292 for network in [Network::Mainnet, Network::Testnet, Network::Regtest] {
2293 assert_eq!(
2294 min_protocol_version(network),
2295 dpp::version::v13::PROTOCOL_VERSION_13,
2296 "{network} floor must be the lowest version the network runs"
2297 );
2298 }
2299 let sdk = SdkBuilder::new_mock()
2300 .with_network(Network::Devnet)
2301 .build()
2302 .expect("mock Sdk should be created");
2303 assert_eq!(
2304 sdk.protocol_version_number(),
2305 min_protocol_version(Network::Devnet)
2306 );
2307 assert!(!sdk.version_pinned);
2308 }
2309
2310 #[test]
2313 fn test_protocol_version_observer_fires_only_on_upward_ratchet() {
2314 use dpp::version::PlatformVersion;
2315 use std::sync::Mutex;
2316 let seen: Arc<Mutex<Vec<u32>>> = Arc::new(Mutex::new(Vec::new()));
2317 let observer = {
2318 let seen = Arc::clone(&seen);
2319 Arc::new(move |version: u32| {
2320 seen.lock().expect("observer log lock").push(version);
2321 })
2322 };
2323 let sdk = SdkBuilder::new_mock()
2324 .with_protocol_version_observer(observer)
2325 .build()
2326 .expect("mock Sdk should be created");
2327 let floor = min_protocol_version(Network::Mainnet);
2328 let target = dpp::version::v14::PROTOCOL_VERSION_14;
2329 assert!(target > floor, "ratchet target must exceed the floor");
2330
2331 sdk.maybe_update_protocol_version(floor);
2332 sdk.maybe_update_protocol_version(floor - 1);
2333 sdk.maybe_update_protocol_version(0);
2334 sdk.maybe_update_protocol_version(dpp::version::LATEST_VERSION + 1);
2335 assert!(
2336 seen.lock().expect("observer log lock").is_empty(),
2337 "rejected inputs must not notify the observer"
2338 );
2339
2340 sdk.maybe_update_protocol_version(target);
2341 sdk.maybe_update_protocol_version(target);
2342 assert_eq!(
2343 *seen.lock().expect("observer log lock"),
2344 vec![target],
2345 "one upward step must notify exactly once"
2346 );
2347
2348 let clone = sdk.clone();
2350 assert!(clone.protocol_version_observer.is_some());
2351 let pinned = SdkBuilder::new_mock()
2352 .with_version(PlatformVersion::get(floor).expect("floor PV exists"))
2353 .with_protocol_version_observer(Arc::new(|_| panic!("pinned SDK must not ratchet")))
2354 .build()
2355 .expect("mock Sdk should be created");
2356 pinned.maybe_update_protocol_version(target);
2357 }
2358
2359 #[test_matrix([90,91,100,109,110], 100, 10, false; "valid time")]
2360 #[test_matrix([0,89,111], 100, 10, true; "invalid time")]
2361 #[test_matrix([0,100], [0,100], 100, false; "zero time")]
2362 #[test_matrix([99,101], 100, 0, true; "zero tolerance")]
2363 fn test_verify_metadata_time(
2364 received_time: u64,
2365 now_time: u64,
2366 tolerance: u64,
2367 expect_err: bool,
2368 ) {
2369 let metadata = ResponseMetadata {
2370 time_ms: received_time,
2371 ..Default::default()
2372 };
2373
2374 let result = super::verify_metadata_time(&metadata, now_time, tolerance);
2375
2376 assert_eq!(result.is_err(), expect_err);
2377 }
2378
2379 async fn expect_epoch_refresh(sdk: &mut super::Sdk) {
2389 use crate::platform::types::epoch::EpochQuery;
2390 use crate::platform::LimitQuery;
2391 use dpp::block::extended_epoch_info::{v0::ExtendedEpochInfoV0, ExtendedEpochInfo};
2392 use drive_proof_verifier::types::ExtendedEpochInfos;
2393
2394 let probe_query = LimitQuery {
2400 query: EpochQuery::genesis(),
2401 limit: Some(1),
2402 start_info: None,
2403 };
2404 let confirmation_query = LimitQuery {
2405 query: EpochQuery::ascending_from(0),
2406 limit: Some(2),
2407 start_info: None,
2408 };
2409
2410 let epoch = ExtendedEpochInfo::from(ExtendedEpochInfoV0 {
2411 index: 0,
2412 first_block_time: 0,
2413 first_block_height: 0,
2414 first_core_block_height: 0,
2415 fee_multiplier_permille: 0,
2416 protocol_version: dpp::version::LATEST_VERSION,
2417 });
2418
2419 sdk.mock()
2420 .expect_fetch::<ExtendedEpochInfo, _>(probe_query, Some(epoch.clone()))
2421 .await
2422 .expect("register epoch probe expectation");
2423 sdk.mock()
2424 .expect_fetch_many::<_, ExtendedEpochInfo, _, ExtendedEpochInfos>(
2425 confirmation_query,
2426 Some(ExtendedEpochInfos::from_iter([(0, Some(epoch))])),
2427 )
2428 .await
2429 .expect("register epoch refresh expectation");
2430 }
2431
2432 #[tokio::test]
2438 async fn test_refresh_ratchets_up_via_proven_query() {
2439 let mut sdk = mock_sdk_with_auto_detect(super::min_protocol_version(Network::Mainnet));
2440 assert_eq!(
2441 sdk.protocol_version_number(),
2442 super::min_protocol_version(Network::Mainnet)
2443 );
2444
2445 expect_epoch_refresh(&mut sdk).await;
2446
2447 let resulting = sdk
2448 .refresh_protocol_version()
2449 .await
2450 .expect("refresh should succeed");
2451
2452 assert_eq!(
2453 resulting,
2454 dpp::version::LATEST_VERSION,
2455 "returned version must reflect the ratchet to the network's latest"
2456 );
2457 assert_eq!(sdk.protocol_version_number(), dpp::version::LATEST_VERSION);
2458 assert_eq!(sdk.version().protocol_version, dpp::version::LATEST_VERSION);
2459 }
2460
2461 #[tokio::test]
2466 async fn test_refresh_leaves_pinned_sdk_unchanged() {
2467 use dpp::version::PlatformVersion;
2468
2469 let pinned = PlatformVersion::get(min_protocol_version(Network::Mainnet))
2471 .expect("mainnet-floor PV exists");
2472 let sdk = SdkBuilder::new_mock()
2473 .with_version(pinned)
2474 .build()
2475 .expect("mock Sdk should be created");
2476 assert_eq!(sdk.protocol_version_number(), pinned.protocol_version);
2477 assert!(sdk.version_pinned);
2478
2479 let resulting = sdk
2482 .refresh_protocol_version()
2483 .await
2484 .expect("pinned refresh is a no-op and must not error");
2485
2486 assert_eq!(
2487 resulting, pinned.protocol_version,
2488 "pinned version must not move"
2489 );
2490 assert_eq!(sdk.protocol_version_number(), pinned.protocol_version);
2491 }
2492
2493 #[tokio::test]
2498 async fn test_refresh_query_unavailable_keeps_current_version() {
2499 let starting = min_protocol_version(Network::Mainnet);
2500 let sdk = mock_sdk_with_auto_detect(starting);
2501 assert_eq!(sdk.protocol_version_number(), starting);
2502
2503 let resulting = sdk
2504 .refresh_protocol_version()
2505 .await
2506 .expect("refresh is best-effort and must not error when the query fails");
2507
2508 assert_eq!(
2509 resulting, starting,
2510 "a failed refresh must leave the stored version untouched (no fallback)"
2511 );
2512 assert_eq!(sdk.protocol_version_number(), starting);
2513 }
2514}