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;
56const fn min_protocol_version(network: Network) -> u32 {
64 match network {
65 Network::Mainnet => dpp::version::v11::PROTOCOL_VERSION_11,
66 Network::Testnet => dpp::version::v12::PROTOCOL_VERSION_12,
67 Network::Devnet => dpp::version::v12::PROTOCOL_VERSION_12,
68 Network::Regtest => dpp::version::v12::PROTOCOL_VERSION_12,
69 }
70}
71
72const DEFAULT_METADATA_TIME_TOLERANCE_MS: u64 = 31 * 60 * 1000;
74
75const DEFAULT_REQUEST_SETTINGS: RequestSettings = RequestSettings {
79 retries: Some(3),
80 timeout: None,
81 ban_failed_address: None,
82 connect_timeout: None,
83 max_decoding_message_size: None,
84};
85
86fn default_address_list_for_network(network: Network) -> AddressList {
115 if !matches!(network, Network::Mainnet | Network::Testnet) {
116 panic!("default address list is only available for mainnet and testnet");
117 }
118
119 let seeds = dash_network_seeds::evo_seeds(network);
120 let filtered = address_list_from_seeds(&seeds, true);
121 if filtered.is_empty() {
122 tracing::warn!(
123 ?network,
124 "all seed entries have failing TLS probes; falling back to unfiltered seed list"
125 );
126 return address_list_from_seeds(&seeds, false);
127 }
128 filtered
129}
130
131fn seed_tls_deterministically_bad(platform: Option<&dash_network_seeds::PlatformStatus>) -> bool {
137 use dash_network_seeds::{Reachability, SslStatus};
138 let Some(platform) = platform else {
139 return false;
140 };
141 match platform.ssl {
142 SslStatus::Expired | SslStatus::SelfSigned | SslStatus::Untrusted => true,
143 SslStatus::NoHandshake => platform.reachable == Reachability::Ok,
144 SslStatus::Valid | SslStatus::Unknown => false,
145 }
146}
147
148fn address_list_from_seeds(
152 seeds: &[dash_network_seeds::MasternodeSeed],
153 skip_bad_tls: bool,
154) -> AddressList {
155 let mut list = AddressList::new();
156 for seed in seeds {
157 let Some(port) = seed.platform_http_port else {
158 continue;
159 };
160 if skip_bad_tls && seed_tls_deterministically_bad(seed.platform.as_ref()) {
161 continue;
162 }
163 let url = format!("https://{}:{}", seed.address.ip(), port);
164 if let Ok(uri) = url.parse::<Uri>() {
165 if let Ok(address) = Address::try_from(uri) {
166 list.add(address);
167 }
168 }
169 }
170 list
171}
172
173pub struct Sdk {
199 pub network: Network,
201 inner: SdkInstance,
202 proofs: bool,
206
207 nonce_cache: Arc<NonceCache>,
209
210 context_provider: ArcSwapOption<Box<dyn ContextProvider>>,
216
217 protocol_version: Arc<atomic::AtomicU32>,
219
220 version_pinned: bool,
224
225 metadata_last_seen_height: Arc<atomic::AtomicU64>,
229
230 metadata_height_tolerance: Option<u64>,
234
235 metadata_time_tolerance_ms: Option<u64>,
239
240 pub(crate) cancel_token: CancellationToken,
242
243 pub(crate) dapi_client_settings: RequestSettings,
245
246 #[cfg(feature = "mocks")]
247 dump_dir: Option<PathBuf>,
248}
249impl Clone for Sdk {
250 fn clone(&self) -> Self {
251 Self {
252 network: self.network,
253 inner: self.inner.clone(),
254 proofs: self.proofs,
255 nonce_cache: Arc::clone(&self.nonce_cache),
256 context_provider: ArcSwapOption::new(self.context_provider.load_full()),
257 cancel_token: self.cancel_token.clone(),
258 protocol_version: Arc::clone(&self.protocol_version),
259 version_pinned: self.version_pinned,
260 metadata_last_seen_height: Arc::clone(&self.metadata_last_seen_height),
261 metadata_height_tolerance: self.metadata_height_tolerance,
262 metadata_time_tolerance_ms: self.metadata_time_tolerance_ms,
263 dapi_client_settings: self.dapi_client_settings,
264 #[cfg(feature = "mocks")]
265 dump_dir: self.dump_dir.clone(),
266 }
267 }
268}
269
270impl Debug for Sdk {
271 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272 match &self.inner {
273 SdkInstance::Dapi { dapi, .. } => f
274 .debug_struct("Sdk")
275 .field("dapi", dapi)
276 .field("proofs", &self.proofs)
277 .finish(),
278 #[cfg(feature = "mocks")]
279 SdkInstance::Mock { mock, .. } => f
280 .debug_struct("Sdk")
281 .field("mock", mock)
282 .field("proofs", &self.proofs)
283 .finish(),
284 }
285 }
286}
287
288#[derive(Debug, Clone)]
293enum SdkInstance {
294 Dapi {
296 dapi: DapiClient,
298 },
299 #[cfg(feature = "mocks")]
301 Mock {
302 dapi: Arc<Mutex<MockDapiClient>>,
306 mock: Arc<Mutex<MockDashPlatformSdk>>,
308 address_list: AddressList,
309 },
310}
311
312impl Sdk {
313 pub fn new_mock() -> Self {
319 SdkBuilder::default()
320 .build()
321 .expect("mock should be created")
322 }
323
324 fn freshness_criteria(&self, method_name: &str) -> (Option<u64>, Option<u64>) {
329 match method_name {
330 "get_addresses_trunk_state" | "get_addresses_branch_state" => (
331 self.metadata_time_tolerance_ms
336 .is_none()
337 .then_some(self.metadata_height_tolerance)
338 .flatten(),
339 self.metadata_time_tolerance_ms
340 .map(|configured| configured.min(DEFAULT_METADATA_TIME_TOLERANCE_MS)),
341 ),
342 _ => (
343 self.metadata_height_tolerance,
344 self.metadata_time_tolerance_ms,
345 ),
346 }
347 }
348
349 pub fn verify_response_metadata(
351 &self,
352 method_name: &str,
353 metadata: &ResponseMetadata,
354 ) -> Result<(), Error> {
355 let (metadata_height_tolerance, metadata_time_tolerance_ms) =
356 self.freshness_criteria(method_name);
357 if let Some(time_tolerance) = metadata_time_tolerance_ms {
360 let now = chrono::Utc::now().timestamp_millis() as u64;
361 verify_metadata_time(metadata, now, time_tolerance)?;
362 };
363 if let Some(height_tolerance) = metadata_height_tolerance {
364 verify_metadata_height(
365 metadata,
366 height_tolerance,
367 Arc::clone(&(self.metadata_last_seen_height)),
368 )?;
369 };
370
371 self.maybe_update_protocol_version(metadata.protocol_version);
372
373 Ok(())
374 }
375
376 fn maybe_update_protocol_version(&self, received_version: u32) {
382 if self.version_pinned {
383 return;
384 }
385
386 if received_version == 0 {
387 return;
388 }
389
390 let current = self.protocol_version.load(Ordering::Relaxed);
391
392 if received_version <= current {
393 return;
394 }
395
396 if PlatformVersion::get(received_version).is_err() {
398 tracing::warn!(
399 received_version,
400 current_version = current,
401 "received unknown protocol version from network; keeping current"
402 );
403 return;
404 }
405
406 let previous = self
407 .protocol_version
408 .fetch_max(received_version, Ordering::Relaxed);
409 if previous < received_version {
410 tracing::info!(
411 target: "dash_sdk::protocol_version",
412 from = previous,
413 to = received_version,
414 "ratcheting protocol version upward"
415 );
416 }
417 }
418
419 pub async fn refresh_protocol_version(&self) -> Result<u32, Error> {
449 if !self.prove() {
450 return Ok(self.protocol_version_number());
451 }
452 if !self.version_pinned {
453 if let Err(error) = ExtendedEpochInfo::fetch_current(self).await {
454 tracing::warn!(
455 target: "dash_sdk::protocol_version",
456 %error,
457 version = self.protocol_version_number(),
458 "proven protocol-version refresh failed; keeping the highest \
459 proof-verified version learned so far (never falling back to \
460 an unverified one)"
461 );
462 }
463 }
464 Ok(self.protocol_version_number())
465 }
466
467 pub(crate) async fn parse_proof_with_metadata_and_proof<R, O: FromProof<R> + MockResponse>(
492 &self,
493 request: O::Request,
494 response: O::Response,
495 method_name: &'static str,
496 ) -> Result<(Option<O>, ResponseMetadata, Proof), Error>
497 where
498 O::Request: Mockable,
499 {
500 let provider = self
501 .context_provider()
502 .ok_or(drive_proof_verifier::Error::ContextProviderNotSet)?;
503
504 let (object, metadata, proof) = match self.inner {
505 SdkInstance::Dapi { .. } => O::maybe_from_proof_with_metadata(
506 request,
507 response,
508 self.network,
509 self.version(),
510 &provider,
511 ),
512 #[cfg(feature = "mocks")]
513 SdkInstance::Mock { ref mock, .. } => {
514 let guard = mock.lock().await;
515 guard.parse_proof_with_metadata(request, response)
516 }
517 }?;
518
519 self.verify_response_metadata(method_name, &metadata)
524 .inspect_err(|err| {
525 tracing::warn!(%err,method=method_name,"received response with stale metadata; try another server");
526 })?;
527
528 Ok((object, metadata, proof))
529 }
530
531 pub fn context_provider(&self) -> Option<impl ContextProvider> {
533 let provider_guard = self.context_provider.load();
534 let provider = provider_guard.as_ref().map(Arc::clone);
535
536 provider
537 }
538
539 #[cfg(feature = "mocks")]
550 pub fn mock(&mut self) -> MutexGuard<'_, MockDashPlatformSdk> {
551 if let Sdk {
552 inner: SdkInstance::Mock { ref mock, .. },
553 ..
554 } = self
555 {
556 mock.try_lock()
557 .expect("mock sdk is in use by another thread and cannot be reconfigured")
558 } else {
559 panic!("not a mock")
560 }
561 }
562
563 pub async fn get_identity_nonce(
568 &self,
569 identity_id: Identifier,
570 bump_first: bool,
571 settings: Option<PutSettings>,
572 ) -> Result<IdentityNonce, Error> {
573 let settings = settings.unwrap_or_default();
574 let nonce = self
575 .nonce_cache
576 .get_identity_nonce(self, identity_id, bump_first, &settings)
577 .await?;
578
579 tracing::trace!(
580 identity_id = %identity_id,
581 bump_first,
582 nonce,
583 "Fetched identity nonce"
584 );
585
586 Ok(nonce)
587 }
588
589 pub async fn get_identity_contract_nonce(
594 &self,
595 identity_id: Identifier,
596 contract_id: Identifier,
597 bump_first: bool,
598 settings: Option<PutSettings>,
599 ) -> Result<IdentityNonce, Error> {
600 let settings = settings.unwrap_or_default();
601 self.nonce_cache
602 .get_identity_contract_nonce(self, identity_id, contract_id, bump_first, &settings)
603 .await
604 }
605
606 pub async fn refresh_identity_nonce(&self, identity_id: &Identifier) {
610 self.nonce_cache.refresh(identity_id).await;
611 }
612
613 pub fn version<'v>(&self) -> &'v PlatformVersion {
621 let v = self.protocol_version.load(Ordering::Relaxed);
622 PlatformVersion::get(v).unwrap_or_else(|_| PlatformVersion::latest())
623 }
624
625 pub fn protocol_version_number(&self) -> u32 {
627 self.protocol_version.load(Ordering::Relaxed)
628 }
629
630 pub fn prove(&self) -> bool {
633 self.proofs
634 }
635
636 pub fn query_settings(&self) -> crate::platform::QuerySettings<'_> {
643 crate::platform::QuerySettings {
644 request_settings: &self.dapi_client_settings,
645 protocol_version: self.version(),
646 prove: self.prove(),
647 }
648 }
649
650 pub fn set_context_provider<C: ContextProvider + 'static>(&self, context_provider: C) {
658 self.context_provider
659 .swap(Some(Arc::new(Box::new(context_provider))));
660 }
661
662 pub fn cancelled(&self) -> WaitForCancellationFuture<'_> {
664 self.cancel_token.cancelled()
665 }
666
667 pub fn shutdown(&self) {
669 self.cancel_token.cancel();
670 }
671
672 pub fn address_list(&self) -> &AddressList {
674 match &self.inner {
675 SdkInstance::Dapi { dapi, .. } => dapi.address_list(),
676 #[cfg(feature = "mocks")]
677 SdkInstance::Mock { address_list, .. } => address_list,
678 }
679 }
680
681 pub fn address_ban_info(&self) -> Vec<AddressBanInfo> {
688 self.address_list().ban_info()
689 }
690}
691
692pub(crate) fn verify_metadata_time(
700 metadata: &ResponseMetadata,
701 now_ms: u64,
702 tolerance_ms: u64,
703) -> Result<(), Error> {
704 let metadata_time = metadata.time_ms;
705
706 if now_ms.abs_diff(metadata_time) > tolerance_ms {
708 return Err(StaleNodeError::Time {
709 expected_timestamp_ms: now_ms,
710 received_timestamp_ms: metadata_time,
711 tolerance_ms,
712 }
713 .into());
714 }
715
716 tracing::trace!(
717 expected_time = now_ms,
718 received_time = metadata_time,
719 tolerance_ms,
720 "received response with valid time"
721 );
722 Ok(())
723}
724
725fn verify_metadata_height(
728 metadata: &ResponseMetadata,
729 tolerance: u64,
730 last_seen_height: Arc<atomic::AtomicU64>,
731) -> Result<(), Error> {
732 let received_height = metadata.height;
733 let previous_height = last_seen_height.fetch_max(received_height, Ordering::AcqRel);
737 let expected_height = previous_height.max(last_seen_height.load(Ordering::Acquire));
738
739 if expected_height > tolerance && received_height < expected_height.saturating_sub(tolerance) {
740 return Err(StaleNodeError::Height {
741 expected_height,
742 received_height,
743 tolerance_blocks: tolerance,
744 }
745 .into());
746 }
747
748 tracing::trace!(
749 expected_height,
750 received_height,
751 tolerance,
752 "received response within the monotonic height window"
753 );
754
755 Ok(())
756}
757
758#[async_trait::async_trait]
759impl DapiRequestExecutor for Sdk {
760 async fn execute<R: TransportRequest>(
761 &self,
762 request: R,
763 settings: RequestSettings,
764 ) -> ExecutionResult<R::Response, DapiClientError> {
765 match self.inner {
766 SdkInstance::Dapi { ref dapi, .. } => dapi.execute(request, settings).await,
767 #[cfg(feature = "mocks")]
768 SdkInstance::Mock { ref dapi, .. } => {
769 let dapi_guard = dapi.lock().await;
770 dapi_guard.execute(request, settings).await
771 }
772 }
773 }
774}
775
776pub struct SdkBuilder {
789 addresses: Option<AddressList>,
793 settings: Option<RequestSettings>,
794
795 network: Network,
796
797 core_ip: String,
798 core_port: u16,
799 core_user: String,
800 core_password: Zeroizing<String>,
801
802 proofs: bool,
804
805 version: Option<&'static PlatformVersion>,
808
809 version_pinned: bool,
813
814 #[cfg(feature = "mocks")]
816 data_contract_cache_size: NonZeroUsize,
817
818 #[cfg(feature = "mocks")]
820 token_config_cache_size: NonZeroUsize,
821
822 #[cfg(feature = "mocks")]
824 quorum_public_keys_cache_size: NonZeroUsize,
825
826 context_provider: Option<Box<dyn ContextProvider>>,
828
829 metadata_height_tolerance: Option<u64>,
834
835 metadata_time_tolerance_ms: Option<u64>,
839
840 trusted_initial_height: Option<u64>,
843
844 #[cfg(feature = "mocks")]
846 dump_dir: Option<PathBuf>,
847
848 pub(crate) cancel_token: CancellationToken,
850
851 #[cfg(not(target_arch = "wasm32"))]
853 ca_certificate: Option<Certificate>,
854}
855
856impl Default for SdkBuilder {
857 fn default() -> Self {
859 Self {
860 addresses: None,
861 settings: None,
862 network: Network::Mainnet,
863 core_ip: "".to_string(),
864 core_port: 0,
865 core_password: "".to_string().into(),
866 core_user: "".to_string(),
867
868 proofs: true,
869 metadata_height_tolerance: Some(1),
870 metadata_time_tolerance_ms: None,
871 trusted_initial_height: None,
872
873 #[cfg(feature = "mocks")]
874 data_contract_cache_size: NonZeroUsize::new(DEFAULT_CONTRACT_CACHE_SIZE)
875 .expect("data contract cache size must be positive"),
876
877 #[cfg(feature = "mocks")]
878 token_config_cache_size: NonZeroUsize::new(DEFAULT_TOKEN_CONFIG_CACHE_SIZE)
879 .expect("token config cache size must be positive"),
880
881 #[cfg(feature = "mocks")]
882 quorum_public_keys_cache_size: NonZeroUsize::new(DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE)
883 .expect("quorum public keys cache size must be positive"),
884
885 context_provider: None,
886
887 cancel_token: CancellationToken::new(),
888
889 version: None,
893 version_pinned: false,
894 #[cfg(not(target_arch = "wasm32"))]
895 ca_certificate: None,
896
897 #[cfg(feature = "mocks")]
898 dump_dir: None,
899 }
900 }
901}
902
903impl SdkBuilder {
904 pub fn with_proofs(mut self, proofs: bool) -> Self {
909 self.proofs = proofs;
910 self
911 }
912 pub fn new(addresses: AddressList) -> Self {
914 Self {
915 addresses: Some(addresses),
916 metadata_time_tolerance_ms: Some(DEFAULT_METADATA_TIME_TOLERANCE_MS),
917 ..Default::default()
918 }
919 }
920
921 pub fn with_address_list(mut self, addresses: AddressList) -> Self {
923 self.addresses = Some(addresses);
924 self
925 }
926
927 pub fn new_mock() -> Self {
929 Self::default()
930 }
931
932 pub fn new_testnet() -> Self {
938 let address_list = default_address_list_for_network(Network::Testnet);
939
940 Self::new(address_list).with_network(Network::Testnet)
941 }
942
943 pub fn new_mainnet() -> Self {
956 let address_list = default_address_list_for_network(Network::Mainnet);
957
958 Self::new(address_list).with_network(Network::Mainnet)
959 }
960
961 pub fn with_network(mut self, network: Network) -> Self {
965 self.network = network;
966 self
967 }
968
969 #[cfg(not(target_arch = "wasm32"))]
979 pub fn with_ca_certificate(mut self, pem_certificate: Certificate) -> Self {
980 self.ca_certificate = Some(pem_certificate);
981 self
982 }
983
984 #[cfg(not(target_arch = "wasm32"))]
989 pub fn with_ca_certificate_file(
990 self,
991 certificate_file_path: impl AsRef<Path>,
992 ) -> std::io::Result<Self> {
993 let pem = std::fs::read(certificate_file_path)?;
994 let cert = Certificate::from_pem(pem);
995
996 Ok(self.with_ca_certificate(cert))
997 }
998
999 pub fn with_settings(mut self, settings: RequestSettings) -> Self {
1007 self.settings = Some(settings);
1008 self
1009 }
1010
1011 pub fn with_version(mut self, version: &'static PlatformVersion) -> Self {
1022 self.version = Some(version);
1023 self.version_pinned = true;
1024 self
1025 }
1026
1027 pub fn with_initial_version(mut self, version: &'static PlatformVersion) -> Self {
1043 self.version = Some(version);
1044 self.version_pinned = false;
1045 self
1046 }
1047
1048 pub fn with_context_provider<C: ContextProvider + 'static>(
1055 mut self,
1056 context_provider: C,
1057 ) -> Self {
1058 self.context_provider = Some(Box::new(context_provider));
1059
1060 self
1061 }
1062
1063 pub fn with_cancellation_token(mut self, cancel_token: CancellationToken) -> Self {
1067 self.cancel_token = cancel_token;
1068 self
1069 }
1070
1071 pub fn with_core(mut self, ip: &str, port: u16, user: &str, password: &str) -> Self {
1079 self.core_ip = ip.to_string();
1080 self.core_port = port;
1081 self.core_user = user.to_string();
1082 self.core_password = Zeroizing::from(password.to_string());
1083
1084 self
1085 }
1086
1087 pub fn with_height_tolerance(mut self, tolerance: Option<u64>) -> Self {
1099 self.metadata_height_tolerance = tolerance;
1100 self
1101 }
1102
1103 pub fn with_time_tolerance(mut self, tolerance_ms: Option<u64>) -> Self {
1120 self.metadata_time_tolerance_ms = tolerance_ms;
1121 self
1122 }
1123
1124 pub fn with_trusted_initial_height(mut self, height: u64) -> Self {
1130 self.trusted_initial_height = Some(height);
1131 self
1132 }
1133
1134 #[cfg(feature = "mocks")]
1147 pub fn with_dump_dir(mut self, dump_dir: &Path) -> Self {
1148 self.dump_dir = Some(dump_dir.to_path_buf());
1149 self
1150 }
1151
1152 pub fn build(self) -> Result<Sdk, Error> {
1160 let is_network_sdk = self.addresses.is_some();
1161 let has_height_anchor = self
1162 .trusted_initial_height
1163 .zip(self.metadata_height_tolerance)
1164 .is_some_and(|(height, tolerance)| height > tolerance);
1165 if is_network_sdk
1166 && self.proofs
1167 && self.metadata_time_tolerance_ms.is_none()
1168 && !has_height_anchor
1169 {
1170 return Err(Error::Config(
1171 "proof mode requires a trusted initial height or signed-time freshness policy"
1172 .to_string(),
1173 ));
1174 }
1175
1176 let dapi_client_settings = match self.settings {
1177 Some(settings) => DEFAULT_REQUEST_SETTINGS.override_by(settings),
1178 None => DEFAULT_REQUEST_SETTINGS,
1179 };
1180
1181 let initial_version = self.version.unwrap_or_else(|| {
1182 PlatformVersion::get(min_protocol_version(self.network))
1183 .expect("min_protocol_version for a network must be a valid version")
1184 });
1185
1186 let sdk= match self.addresses {
1187 Some(addresses) => {
1189 #[allow(unused_mut)] let mut dapi = DapiClient::new(addresses, dapi_client_settings);
1191 #[cfg(not(target_arch = "wasm32"))]
1192 if let Some(pem) = self.ca_certificate {
1193 dapi = dapi.with_ca_certificate(pem);
1194 }
1195
1196 #[cfg(feature = "mocks")]
1197 let dapi = dapi.dump_dir(self.dump_dir.clone());
1198
1199 #[allow(unused_mut)] let mut sdk= Sdk{
1201 network: self.network,
1202 dapi_client_settings,
1203 inner:SdkInstance::Dapi { dapi },
1204 proofs:self.proofs,
1205 context_provider: ArcSwapOption::new( self.context_provider.map(Arc::new)),
1206 cancel_token: self.cancel_token,
1207 nonce_cache: Default::default(),
1208 protocol_version: Arc::new(atomic::AtomicU32::new(initial_version.protocol_version)),
1211 version_pinned: self.version_pinned,
1212 metadata_last_seen_height: Arc::new(atomic::AtomicU64::new(
1213 self.trusted_initial_height.unwrap_or(0),
1214 )),
1215 metadata_height_tolerance: self.metadata_height_tolerance,
1216 metadata_time_tolerance_ms: self.metadata_time_tolerance_ms,
1217 #[cfg(feature = "mocks")]
1218 dump_dir: self.dump_dir,
1219 };
1220 if sdk.context_provider.load().is_none() {
1222 #[cfg(feature = "mocks")]
1223 if !self.core_ip.is_empty() {
1224 tracing::warn!(
1225 "ContextProvider not set, falling back to a mock one; use SdkBuilder::with_context_provider() to set it up");
1226 let mut context_provider = GrpcContextProvider::new(None,
1227 &self.core_ip, self.core_port, &self.core_user, &self.core_password,
1228 self.data_contract_cache_size, self.token_config_cache_size, self.quorum_public_keys_cache_size)?;
1229 #[cfg(feature = "mocks")]
1230 if sdk.dump_dir.is_some() {
1231 context_provider.set_dump_dir(sdk.dump_dir.clone());
1232 }
1233 let context_provider= Arc::new(context_provider);
1236 sdk.context_provider.swap(Some(Arc::new(Box::new(context_provider.clone()))));
1237 context_provider.set_sdk(Some(sdk.clone()));
1238 } else{
1239 return Err(Error::Config(concat!(
1240 "context provider is not set, configure it with SdkBuilder::with_context_provider() ",
1241 "or configure Core access with SdkBuilder::with_core() to use mock context provider")
1242 .to_string()));
1243 }
1244 #[cfg(not(feature = "mocks"))]
1245 return Err(Error::Config(concat!(
1246 "context provider is not set, configure it with SdkBuilder::with_context_provider() ",
1247 "or enable `mocks` feature to use mock context provider")
1248 .to_string()));
1249 };
1250
1251 sdk
1252 },
1253 #[cfg(feature = "mocks")]
1254 None => {
1256 let dapi =Arc::new(Mutex::new( MockDapiClient::new()));
1257 let context_provider = self.context_provider.unwrap_or_else(||{
1259 let mut cp=MockContextProvider::new();
1260 if let Some(ref dump_dir) = self.dump_dir {
1261 cp.quorum_keys_dir(Some(dump_dir.clone()));
1262 }
1263 Box::new(cp)
1264 }
1265 );
1266 let mock_sdk = MockDashPlatformSdk::new(Arc::clone(&dapi));
1267 let mock_sdk = Arc::new(Mutex::new(mock_sdk));
1268 let sdk= Sdk {
1269 network: self.network,
1270 dapi_client_settings,
1271 inner:SdkInstance::Mock {
1272 mock:mock_sdk.clone(),
1273 dapi,
1274 address_list: AddressList::new(),
1275 },
1276 dump_dir: self.dump_dir.clone(),
1277 proofs:self.proofs,
1278 nonce_cache: Default::default(),
1279 protocol_version: Arc::new(atomic::AtomicU32::new(initial_version.protocol_version)),
1280 version_pinned: self.version_pinned,
1281 context_provider: ArcSwapOption::new(Some(Arc::new(context_provider))),
1282 cancel_token: self.cancel_token,
1283 metadata_last_seen_height: Arc::new(atomic::AtomicU64::new(
1284 self.trusted_initial_height.unwrap_or(0),
1285 )),
1286 metadata_height_tolerance: self.metadata_height_tolerance,
1287 metadata_time_tolerance_ms: self.metadata_time_tolerance_ms,
1288 };
1289 let mut guard = mock_sdk.try_lock().expect("mock sdk is in use by another thread and cannot be reconfigured");
1290 guard.set_sdk(sdk.clone());
1291 if let Some(ref dump_dir) = self.dump_dir {
1292 guard.load_expectations_sync(dump_dir)?;
1293 };
1294
1295 sdk
1296 },
1297 #[cfg(not(feature = "mocks"))]
1298 None => return Err(Error::Config("Mock mode is not available. Please enable `mocks` feature or provide address list.".to_string())),
1299 };
1300
1301 Ok(sdk)
1302 }
1303}
1304
1305pub fn prettify_proof(proof: &Proof) -> String {
1306 let config = bincode::config::standard()
1307 .with_big_endian()
1308 .with_no_limit();
1309 let grovedb_proof: Result<GroveDBProof, DecodeError> =
1310 bincode::decode_from_slice(&proof.grovedb_proof, config).map(|(a, _)| a);
1311
1312 let grovedb_proof_string = match grovedb_proof {
1313 Ok(proof) => format!("{}", proof),
1314 Err(_) => "Invalid GroveDBProof".to_string(),
1315 };
1316 format!(
1317 "Proof {{
1318 grovedb_proof: {},
1319 quorum_hash: 0x{},
1320 signature: 0x{},
1321 round: {},
1322 block_id_hash: 0x{},
1323 quorum_type: {},
1324 }}",
1325 grovedb_proof_string,
1326 hex::encode(&proof.quorum_hash),
1327 hex::encode(&proof.signature),
1328 proof.round,
1329 hex::encode(&proof.block_id_hash),
1330 proof.quorum_type,
1331 )
1332}
1333
1334#[cfg(test)]
1335mod test {
1336 use std::sync::Arc;
1337
1338 use dapi_grpc::platform::v0::{GetIdentityRequest, ResponseMetadata};
1339 use rs_dapi_client::transport::TransportRequest;
1340 use test_case::test_matrix;
1341
1342 use crate::SdkBuilder;
1343
1344 use super::{min_protocol_version, Network};
1345
1346 const MAINNET_PLATFORM_HTTP_PORT: u16 = 443;
1348 const TESTNET_PLATFORM_HTTP_PORT: u16 = 1443;
1350
1351 #[test]
1352 fn new_testnet_sources_bootstrap_from_seeds() {
1353 let builder = SdkBuilder::new_testnet();
1354 let address_list = builder
1355 .addresses
1356 .as_ref()
1357 .expect("testnet builder should configure default addresses");
1358
1359 assert_eq!(builder.network, Network::Testnet);
1360 assert!(
1361 !address_list.is_empty(),
1362 "testnet must have at least one bootstrap address"
1363 );
1364 for address in address_list.get_live_addresses() {
1365 assert_eq!(
1366 address.uri().port_u16(),
1367 Some(TESTNET_PLATFORM_HTTP_PORT),
1368 "testnet bootstrap address must use the platform HTTP port",
1369 );
1370 }
1371 }
1372
1373 #[test]
1374 fn new_mainnet_sources_bootstrap_from_seeds() {
1375 let builder = SdkBuilder::new_mainnet();
1376 let address_list = builder
1377 .addresses
1378 .as_ref()
1379 .expect("mainnet builder should configure default addresses");
1380
1381 assert_eq!(builder.network, Network::Mainnet);
1382 assert!(
1383 !address_list.is_empty(),
1384 "mainnet must have at least one bootstrap address"
1385 );
1386 for address in address_list.get_live_addresses() {
1387 assert_eq!(
1388 address.uri().port_u16(),
1389 Some(MAINNET_PLATFORM_HTTP_PORT),
1390 "mainnet bootstrap address must use the platform HTTP port",
1391 );
1392 }
1393 }
1394
1395 mod seed_tls_filter {
1396 use super::super::{address_list_from_seeds, seed_tls_deterministically_bad};
1397 use dash_network_seeds::{
1398 CoreStatus, MasternodeSeed, MasternodeType, PlatformStatus, Reachability, SslStatus,
1399 };
1400
1401 fn seed(host: u8, platform: Option<PlatformStatus>) -> MasternodeSeed {
1404 MasternodeSeed {
1405 address: format!("203.0.113.{host}:9999").parse().unwrap(),
1406 mn_type: MasternodeType::Evo,
1407 platform_http_port: Some(443),
1408 core: CoreStatus::default(),
1409 platform,
1410 }
1411 }
1412
1413 fn status(ssl: SslStatus, reachable: Reachability) -> PlatformStatus {
1414 PlatformStatus {
1415 reachable,
1416 ssl,
1417 ..PlatformStatus::default()
1418 }
1419 }
1420
1421 #[test]
1427 fn classification_covers_every_status_combination() {
1428 let reachabilities = [
1429 Reachability::Unknown,
1430 Reachability::Ok,
1431 Reachability::Timeout,
1432 Reachability::Refused,
1433 Reachability::Error,
1434 ];
1435 for reachable in reachabilities {
1436 for ssl in [
1437 SslStatus::Expired,
1438 SslStatus::SelfSigned,
1439 SslStatus::Untrusted,
1440 ] {
1441 assert!(
1442 seed_tls_deterministically_bad(Some(&status(ssl, reachable))),
1443 "{ssl:?} must be rejected regardless of {reachable:?}"
1444 );
1445 }
1446 for ssl in [SslStatus::Valid, SslStatus::Unknown] {
1447 assert!(
1448 !seed_tls_deterministically_bad(Some(&status(ssl, reachable))),
1449 "{ssl:?} must never be rejected ({reachable:?})"
1450 );
1451 }
1452 assert_eq!(
1453 seed_tls_deterministically_bad(Some(&status(
1454 SslStatus::NoHandshake,
1455 reachable
1456 ))),
1457 reachable == Reachability::Ok,
1458 "NoHandshake must be rejected only when TCP connect succeeded ({reachable:?})"
1459 );
1460 }
1461 assert!(
1462 !seed_tls_deterministically_bad(None),
1463 "an unprobed seed must never be rejected"
1464 );
1465 }
1466
1467 #[test]
1468 fn filter_drops_only_deterministic_failures() {
1469 let seeds = vec![
1470 seed(1, Some(status(SslStatus::Valid, Reachability::Ok))),
1471 seed(2, Some(status(SslStatus::Expired, Reachability::Ok))),
1472 seed(
1473 3,
1474 Some(status(SslStatus::NoHandshake, Reachability::Timeout)),
1475 ),
1476 seed(4, Some(status(SslStatus::NoHandshake, Reachability::Ok))),
1477 seed(5, None),
1478 ];
1479 assert_eq!(address_list_from_seeds(&seeds, true).len(), 3);
1480 assert_eq!(address_list_from_seeds(&seeds, false).len(), 5);
1481 }
1482
1483 #[test]
1487 fn all_rejected_input_yields_empty_filtered_and_full_unfiltered() {
1488 let seeds = vec![
1489 seed(1, Some(status(SslStatus::Expired, Reachability::Ok))),
1490 seed(2, Some(status(SslStatus::Untrusted, Reachability::Timeout))),
1491 ];
1492 assert!(address_list_from_seeds(&seeds, true).is_empty());
1493 assert_eq!(address_list_from_seeds(&seeds, false).len(), 2);
1494 }
1495
1496 #[test]
1497 fn seed_without_platform_port_is_always_skipped() {
1498 let mut no_port = seed(1, Some(status(SslStatus::Valid, Reachability::Ok)));
1499 no_port.platform_http_port = None;
1500 assert!(address_list_from_seeds(&[no_port], false).is_empty());
1501 }
1502 }
1503
1504 #[test]
1508 fn bootstrap_counts_reasonable() {
1509 let mainnet = SdkBuilder::new_mainnet()
1510 .addresses
1511 .expect("mainnet builder should configure default addresses");
1512 let testnet = SdkBuilder::new_testnet()
1513 .addresses
1514 .expect("testnet builder should configure default addresses");
1515 assert!(
1516 mainnet.len() >= 10,
1517 "expected >=10 mainnet bootstrap addresses, got {}",
1518 mainnet.len()
1519 );
1520 assert!(
1521 testnet.len() >= 10,
1522 "expected >=10 testnet bootstrap addresses, got {}",
1523 testnet.len()
1524 );
1525 }
1526
1527 #[test]
1528 fn network_builders_enable_an_independent_time_anchor() {
1529 assert_eq!(
1530 SdkBuilder::new_testnet().metadata_time_tolerance_ms,
1531 Some(super::DEFAULT_METADATA_TIME_TOLERANCE_MS)
1532 );
1533 assert_eq!(SdkBuilder::new_mock().metadata_time_tolerance_ms, None);
1534 }
1535
1536 #[test]
1537 fn proof_enabled_network_builder_rejects_missing_freshness_anchor() {
1538 let error = SdkBuilder::new(super::AddressList::new())
1539 .with_time_tolerance(None)
1540 .build()
1541 .expect_err("network proof mode must have an independent freshness anchor");
1542
1543 assert!(
1544 matches!(error, crate::Error::Config(message) if message.contains("trusted initial height"))
1545 );
1546 }
1547
1548 #[test_matrix(0, 0; "zero height")]
1549 #[test_matrix(1, 1; "height equals tolerance")]
1550 #[test_matrix(1, 2; "height below tolerance")]
1551 fn proof_enabled_network_builder_rejects_ineffective_height_anchor(
1552 trusted_height: u64,
1553 tolerance: u64,
1554 ) {
1555 let error = SdkBuilder::new(super::AddressList::new())
1556 .with_time_tolerance(None)
1557 .with_height_tolerance(Some(tolerance))
1558 .with_trusted_initial_height(trusted_height)
1559 .build()
1560 .expect_err("trusted height must impose a freshness floor");
1561
1562 assert!(
1563 matches!(error, crate::Error::Config(message) if message.contains("trusted initial height"))
1564 );
1565 }
1566
1567 #[test]
1568 fn height_only_address_checkpoint_uses_trusted_height_floor() {
1569 let sdk = SdkBuilder::new_mock()
1570 .with_time_tolerance(None)
1571 .with_height_tolerance(Some(2))
1572 .with_trusted_initial_height(100)
1573 .build()
1574 .expect("effective trusted height should permit height-only proof mode");
1575
1576 assert!(matches!(
1577 sdk.verify_response_metadata(
1578 "get_addresses_trunk_state",
1579 &ResponseMetadata {
1580 height: 97,
1581 ..Default::default()
1582 },
1583 ),
1584 Err(crate::Error::StaleNode(
1585 super::StaleNodeError::Height { .. }
1586 ))
1587 ));
1588 assert_eq!(
1589 sdk.metadata_last_seen_height
1590 .load(std::sync::atomic::Ordering::Acquire),
1591 100,
1592 "a rejected stale checkpoint must not lower the trusted floor"
1593 );
1594 }
1595
1596 #[test]
1597 fn trusted_initial_height_seeds_the_high_water_mark() {
1598 let sdk = SdkBuilder::new_mock()
1599 .with_trusted_initial_height(42)
1600 .build()
1601 .expect("mock SDK should build");
1602
1603 assert_eq!(
1604 sdk.metadata_last_seen_height
1605 .load(std::sync::atomic::Ordering::Acquire),
1606 42
1607 );
1608 }
1609
1610 #[test_matrix(97..102, 100, 2, false; "valid height")]
1611 #[test_case(103, 100, 2, true; "invalid height")]
1612 fn test_verify_metadata_height(
1613 expected_height: u64,
1614 received_height: u64,
1615 tolerance: u64,
1616 expect_err: bool,
1617 ) {
1618 let metadata = ResponseMetadata {
1619 height: received_height,
1620 ..Default::default()
1621 };
1622
1623 let last_seen_height = Arc::new(std::sync::atomic::AtomicU64::new(expected_height));
1624
1625 let result =
1626 super::verify_metadata_height(&metadata, tolerance, Arc::clone(&last_seen_height));
1627
1628 assert_eq!(result.is_err(), expect_err);
1629 if result.is_ok() {
1630 assert_eq!(
1631 last_seen_height.load(std::sync::atomic::Ordering::Relaxed),
1632 expected_height.max(received_height),
1633 "height high-water mark must never decrease"
1634 );
1635 }
1636 }
1637
1638 #[test]
1639 fn accepted_height_tolerance_cannot_walk_the_watermark_backwards() {
1640 let last_seen_height = Arc::new(std::sync::atomic::AtomicU64::new(100));
1641
1642 super::verify_metadata_height(
1643 &ResponseMetadata {
1644 height: 99,
1645 ..Default::default()
1646 },
1647 1,
1648 Arc::clone(&last_seen_height),
1649 )
1650 .expect("one block behind is within tolerance");
1651 assert_eq!(
1652 last_seen_height.load(std::sync::atomic::Ordering::Acquire),
1653 100
1654 );
1655
1656 super::verify_metadata_height(
1657 &ResponseMetadata {
1658 height: 98,
1659 ..Default::default()
1660 },
1661 1,
1662 Arc::clone(&last_seen_height),
1663 )
1664 .expect_err("a second rollback step must be compared with the high-water mark");
1665 assert_eq!(
1666 last_seen_height.load(std::sync::atomic::Ordering::Acquire),
1667 100
1668 );
1669
1670 super::verify_metadata_height(
1671 &ResponseMetadata {
1672 height: 101,
1673 ..Default::default()
1674 },
1675 1,
1676 Arc::clone(&last_seen_height),
1677 )
1678 .expect("a newer height should advance the high-water mark");
1679 assert_eq!(
1680 last_seen_height.load(std::sync::atomic::Ordering::Acquire),
1681 101
1682 );
1683 }
1684
1685 #[test]
1686 fn cloned_sdk_verify_metadata_height() {
1687 let sdk1 = SdkBuilder::new_mock()
1688 .build()
1689 .expect("mock Sdk should be created");
1690
1691 let metadata = ResponseMetadata {
1693 height: 1,
1694 ..Default::default()
1695 };
1696
1697 let request = GetIdentityRequest::default();
1699 sdk1.verify_response_metadata(request.method_name(), &metadata)
1700 .expect("metadata should be valid");
1701
1702 assert_eq!(
1703 sdk1.metadata_last_seen_height
1704 .load(std::sync::atomic::Ordering::Relaxed),
1705 metadata.height,
1706 "initial height"
1707 );
1708
1709 let sdk2 = sdk1.clone();
1711 let sdk3 = sdk1.clone();
1712
1713 let metadata = ResponseMetadata {
1715 height: 2,
1716 ..Default::default()
1717 };
1718 let request = GetIdentityRequest::default();
1720 sdk2.verify_response_metadata(request.method_name(), &metadata)
1721 .expect("metadata should be valid");
1722
1723 assert_eq!(
1724 sdk1.metadata_last_seen_height
1725 .load(std::sync::atomic::Ordering::Relaxed),
1726 metadata.height,
1727 "first sdk should see height from second sdk"
1728 );
1729 assert_eq!(
1730 sdk3.metadata_last_seen_height
1731 .load(std::sync::atomic::Ordering::Relaxed),
1732 metadata.height,
1733 "third sdk should see height from second sdk"
1734 );
1735
1736 let metadata = ResponseMetadata {
1738 height: 3,
1739 ..Default::default()
1740 };
1741 let request = GetIdentityRequest::default();
1743 sdk3.verify_response_metadata(request.method_name(), &metadata)
1744 .expect("metadata should be valid");
1745
1746 assert_eq!(
1747 sdk1.metadata_last_seen_height
1748 .load(std::sync::atomic::Ordering::Relaxed),
1749 metadata.height,
1750 "first sdk should see height from third sdk"
1751 );
1752
1753 assert_eq!(
1754 sdk2.metadata_last_seen_height
1755 .load(std::sync::atomic::Ordering::Relaxed),
1756 metadata.height,
1757 "second sdk should see height from third sdk"
1758 );
1759
1760 let metadata = ResponseMetadata {
1762 height: 1,
1763 ..Default::default()
1764 };
1765
1766 let request = GetIdentityRequest::default();
1767 sdk1.verify_response_metadata(request.method_name(), &metadata)
1768 .expect_err("metadata should be invalid");
1769 }
1770
1771 fn mock_sdk_with_auto_detect(starting_version: u32) -> super::Sdk {
1774 use std::sync::atomic::Ordering;
1775
1776 let sdk = SdkBuilder::new_mock()
1777 .build()
1778 .expect("mock Sdk should be created");
1779 sdk.protocol_version
1780 .store(starting_version, Ordering::Relaxed);
1781 sdk
1782 }
1783
1784 #[test]
1785 fn test_version_update_from_metadata() {
1786 let sdk = mock_sdk_with_auto_detect(1);
1787
1788 assert_eq!(sdk.protocol_version_number(), 1);
1789
1790 let metadata = ResponseMetadata {
1791 protocol_version: 2,
1792 height: 1,
1793 ..Default::default()
1794 };
1795
1796 sdk.verify_response_metadata("test", &metadata)
1797 .expect("metadata should be valid");
1798
1799 assert_eq!(sdk.protocol_version_number(), 2);
1800 assert_eq!(sdk.version().protocol_version, 2);
1801 }
1802
1803 #[test]
1804 fn test_unknown_version_ignored() {
1805 use dpp::version::PlatformVersion;
1806
1807 let sdk = mock_sdk_with_auto_detect(PlatformVersion::latest().protocol_version);
1808 let original_version = sdk.protocol_version_number();
1809
1810 let metadata = ResponseMetadata {
1811 protocol_version: 999,
1812 height: 1,
1813 ..Default::default()
1814 };
1815
1816 sdk.verify_response_metadata("test", &metadata)
1817 .expect("metadata should be valid");
1818
1819 assert_eq!(sdk.protocol_version_number(), original_version);
1820 assert_eq!(sdk.version().protocol_version, original_version);
1821 }
1822
1823 #[test]
1824 fn test_version_shared_between_clones() {
1825 let sdk = mock_sdk_with_auto_detect(1);
1826
1827 let clone = sdk.clone();
1828
1829 let metadata = ResponseMetadata {
1830 protocol_version: 2,
1831 height: 1,
1832 ..Default::default()
1833 };
1834
1835 clone
1836 .verify_response_metadata("test", &metadata)
1837 .expect("metadata should be valid");
1838
1839 assert_eq!(
1840 sdk.protocol_version_number(),
1841 2,
1842 "original should see update from clone"
1843 );
1844 }
1845
1846 #[test]
1847 fn test_version_downgrade_ignored() {
1848 let sdk = mock_sdk_with_auto_detect(2);
1849
1850 assert_eq!(sdk.protocol_version_number(), 2);
1851
1852 let metadata = ResponseMetadata {
1853 protocol_version: 1,
1854 height: 1,
1855 ..Default::default()
1856 };
1857
1858 sdk.verify_response_metadata("test", &metadata)
1859 .expect("metadata should be valid");
1860
1861 assert_eq!(sdk.protocol_version_number(), 2);
1862 }
1863
1864 #[test]
1865 fn test_version_zero_ignored() {
1866 use dpp::version::PlatformVersion;
1867
1868 let sdk = mock_sdk_with_auto_detect(PlatformVersion::latest().protocol_version);
1869 let original_version = sdk.protocol_version_number();
1870
1871 let metadata = ResponseMetadata {
1872 protocol_version: 0,
1873 height: 1,
1874 ..Default::default()
1875 };
1876
1877 sdk.verify_response_metadata("test", &metadata)
1878 .expect("metadata should be valid");
1879
1880 assert_eq!(sdk.protocol_version_number(), original_version);
1881 }
1882
1883 #[test]
1884 fn test_concurrent_updates_converge_to_highest() {
1885 use std::thread;
1886
1887 let sdk = mock_sdk_with_auto_detect(1);
1888
1889 assert_eq!(sdk.protocol_version_number(), 1);
1890
1891 let mut handles = Vec::new();
1892 for version in [2u32, 3, 2, 3, 2, 3] {
1894 let sdk_clone = sdk.clone();
1895 handles.push(thread::spawn(move || {
1896 let metadata = ResponseMetadata {
1897 protocol_version: version,
1898 height: 1,
1899 ..Default::default()
1900 };
1901 sdk_clone
1902 .verify_response_metadata("test", &metadata)
1903 .expect("metadata should be valid");
1904 }));
1905 }
1906
1907 for h in handles {
1908 h.join().expect("thread should not panic");
1909 }
1910
1911 assert_eq!(
1913 sdk.protocol_version_number(),
1914 3,
1915 "concurrent updates must converge to highest version"
1916 );
1917 }
1918
1919 #[test]
1923 fn test_explicit_version_disables_auto_detect() {
1924 use dpp::version::PlatformVersion;
1925
1926 let pinned = PlatformVersion::get(min_protocol_version(Network::Mainnet))
1929 .expect("mainnet-floor PV exists");
1930 let sdk = SdkBuilder::new_mock()
1931 .with_version(pinned)
1932 .build()
1933 .expect("mock Sdk should be created");
1934
1935 assert_eq!(sdk.protocol_version_number(), pinned.protocol_version);
1936 assert!(sdk.version_pinned);
1937
1938 let metadata = ResponseMetadata {
1940 protocol_version: dpp::version::v12::PROTOCOL_VERSION_12,
1941 height: 1,
1942 ..Default::default()
1943 };
1944
1945 sdk.verify_response_metadata("test", &metadata)
1946 .expect("metadata should be valid");
1947
1948 assert_eq!(
1949 sdk.protocol_version_number(),
1950 pinned.protocol_version,
1951 "pinned version must not be auto-updated"
1952 );
1953 }
1954
1955 #[test]
1956 fn test_with_initial_version_seeds_to_older_network_version() {
1957 use dpp::version::PlatformVersion;
1958
1959 let floor = min_protocol_version(Network::Mainnet);
1963 let initial = PlatformVersion::get(floor).expect("mainnet-floor PV exists");
1964 let sdk = SdkBuilder::new_mock()
1965 .with_initial_version(initial)
1966 .build()
1967 .expect("mock Sdk should be created");
1968
1969 assert_eq!(
1970 sdk.protocol_version_number(),
1971 floor,
1972 "with_initial_version must seed the atomic without pinning"
1973 );
1974 assert_eq!(sdk.version().protocol_version, floor);
1975 assert!(
1976 !sdk.version_pinned,
1977 "with_initial_version must keep auto-detect enabled"
1978 );
1979
1980 let metadata = ResponseMetadata {
1982 protocol_version: floor,
1983 height: 1,
1984 ..Default::default()
1985 };
1986 sdk.verify_response_metadata("test", &metadata)
1987 .expect("metadata should be valid");
1988 assert_eq!(sdk.protocol_version_number(), floor);
1989
1990 let newer = dpp::version::v12::PROTOCOL_VERSION_12;
1992 assert!(newer > floor, "ratchet target must exceed the floor");
1993 let metadata = ResponseMetadata {
1994 protocol_version: newer,
1995 height: 2,
1996 ..Default::default()
1997 };
1998 sdk.verify_response_metadata("test", &metadata)
1999 .expect("metadata should be valid");
2000 assert_eq!(sdk.protocol_version_number(), newer);
2001 }
2002
2003 #[test]
2004 fn test_with_initial_version_after_with_version_restores_auto_detect() {
2005 use dpp::version::PlatformVersion;
2006
2007 let v_latest = PlatformVersion::latest();
2014 let v_old = PlatformVersion::get(min_protocol_version(Network::Mainnet))
2015 .expect("mainnet-floor PV exists");
2016 assert!(
2017 v_old.protocol_version < v_latest.protocol_version,
2018 "v_old must be below latest so the later ratchet is observable"
2019 );
2020
2021 let sdk = SdkBuilder::new_mock()
2022 .with_version(v_latest)
2023 .with_initial_version(v_old)
2024 .build()
2025 .expect("mock Sdk should be created");
2026
2027 assert_eq!(
2028 sdk.protocol_version_number(),
2029 v_old.protocol_version,
2030 "with_initial_version must overwrite the prior with_version seed"
2031 );
2032 assert!(
2033 !sdk.version_pinned,
2034 "with_initial_version must restore auto-detect after with_version disabled it"
2035 );
2036
2037 let metadata = ResponseMetadata {
2039 protocol_version: v_latest.protocol_version,
2040 height: 1,
2041 ..Default::default()
2042 };
2043 sdk.verify_response_metadata("test", &metadata)
2044 .expect("metadata should be valid");
2045 assert_eq!(sdk.protocol_version_number(), v_latest.protocol_version);
2046 }
2047
2048 #[test]
2049 fn test_mock_version_follows_outer_sdk_atomic() {
2050 use dpp::version::PlatformVersion;
2051
2052 let v_old = PlatformVersion::get(min_protocol_version(Network::Mainnet))
2058 .expect("mainnet-floor PV exists");
2059 let v_new = PlatformVersion::latest();
2060 assert!(
2061 v_old.protocol_version < v_new.protocol_version,
2062 "v_old must be below latest so the ratchet is observable"
2063 );
2064
2065 let mut sdk = SdkBuilder::new_mock()
2066 .with_initial_version(v_old)
2067 .build()
2068 .expect("mock Sdk should be created");
2069
2070 assert_eq!(sdk.version().protocol_version, v_old.protocol_version);
2071 {
2072 let mock = sdk.mock();
2073 assert_eq!(
2074 mock.version().protocol_version,
2075 v_old.protocol_version,
2076 "mock version must mirror outer SDK before ratchet"
2077 );
2078 }
2079
2080 let metadata = ResponseMetadata {
2081 protocol_version: v_new.protocol_version,
2082 height: 1,
2083 ..Default::default()
2084 };
2085 sdk.verify_response_metadata("test", &metadata)
2086 .expect("metadata should be valid");
2087
2088 assert_eq!(sdk.version().protocol_version, v_new.protocol_version);
2089 let mock = sdk.mock();
2090 assert_eq!(
2091 mock.version().protocol_version,
2092 v_new.protocol_version,
2093 "mock version must follow outer ratchet"
2094 );
2095 }
2096
2097 #[test]
2098 fn test_default_builder_seeds_initial_protocol_version_floor() {
2099 let sdk = SdkBuilder::new_mock()
2102 .build()
2103 .expect("mock Sdk should be created");
2104
2105 let expected = min_protocol_version(Network::Mainnet);
2106 assert_eq!(
2107 sdk.protocol_version_number(),
2108 expected,
2109 "unpinned mainnet SDK must boot at the mainnet floor, not latest()"
2110 );
2111 assert_eq!(sdk.version().protocol_version, expected);
2112 assert!(
2113 !sdk.version_pinned,
2114 "default SDK must keep auto-detect enabled"
2115 );
2116 }
2117
2118 #[test]
2119 fn test_default_floor_ratchets_up_but_never_down() {
2120 let sdk = SdkBuilder::new_mock()
2121 .build()
2122 .expect("mock Sdk should be created");
2123 let floor = min_protocol_version(Network::Mainnet);
2125 assert_eq!(sdk.protocol_version_number(), floor);
2126
2127 let target = dpp::version::v12::PROTOCOL_VERSION_12;
2130 assert!(
2131 target > floor,
2132 "ratchet test target must exceed the floor; bump it if the floor reaches v12"
2133 );
2134 sdk.maybe_update_protocol_version(target);
2135 assert_eq!(
2136 sdk.protocol_version_number(),
2137 target,
2138 "auto-detect must ratchet upward from the floor"
2139 );
2140
2141 sdk.maybe_update_protocol_version(floor - 1);
2143 assert_eq!(
2144 sdk.protocol_version_number(),
2145 target,
2146 "ratchet must never downgrade below the highest observed version"
2147 );
2148 }
2149
2150 #[test]
2161 fn test_ratchet_rejects_unknown_and_non_upward_versions() {
2162 let sdk = SdkBuilder::new_mock()
2163 .build()
2164 .expect("mock Sdk should be created");
2165 let floor = min_protocol_version(Network::Mainnet);
2167 assert_eq!(sdk.protocol_version_number(), floor);
2168
2169 sdk.maybe_update_protocol_version(dpp::version::LATEST_VERSION + 1);
2171 assert_eq!(
2172 sdk.protocol_version_number(),
2173 floor,
2174 "unknown protocol version must not move the stored version"
2175 );
2176
2177 sdk.maybe_update_protocol_version(0);
2179 assert_eq!(
2180 sdk.protocol_version_number(),
2181 floor,
2182 "zero protocol version must be ignored"
2183 );
2184
2185 sdk.maybe_update_protocol_version(floor);
2187 assert_eq!(sdk.protocol_version_number(), floor);
2188
2189 sdk.maybe_update_protocol_version(floor - 1);
2191 assert_eq!(
2192 sdk.protocol_version_number(),
2193 floor,
2194 "lower known version must not downgrade the stored version"
2195 );
2196 }
2197
2198 #[test]
2201 fn test_explicit_pin_below_floor_is_preserved() {
2202 use dpp::version::PlatformVersion;
2203
2204 let floor = min_protocol_version(Network::Mainnet);
2205 let below = floor - 1;
2206 let pinned = PlatformVersion::get(below).expect("sub-floor PV exists");
2207 let sdk = SdkBuilder::new_mock()
2208 .with_version(pinned)
2209 .build()
2210 .expect("mock Sdk should be created");
2211
2212 assert_eq!(
2213 sdk.protocol_version_number(),
2214 below,
2215 "a pin below the floor must be preserved"
2216 );
2217 assert!(sdk.version_pinned);
2219 }
2220
2221 #[test]
2229 fn test_testnet_default_builder_boots_at_per_network_floor() {
2230 let sdk = SdkBuilder::new_mock()
2231 .with_network(Network::Testnet)
2232 .build()
2233 .expect("mock Sdk should be created");
2234
2235 assert_eq!(
2236 sdk.protocol_version_number(),
2237 min_protocol_version(Network::Testnet),
2238 "testnet seeds directly at its per-network floor"
2239 );
2240 assert!(!sdk.version_pinned);
2241 }
2242
2243 #[test_matrix([90,91,100,109,110], 100, 10, false; "valid time")]
2244 #[test_matrix([0,89,111], 100, 10, true; "invalid time")]
2245 #[test_matrix([0,100], [0,100], 100, false; "zero time")]
2246 #[test_matrix([99,101], 100, 0, true; "zero tolerance")]
2247 fn test_verify_metadata_time(
2248 received_time: u64,
2249 now_time: u64,
2250 tolerance: u64,
2251 expect_err: bool,
2252 ) {
2253 let metadata = ResponseMetadata {
2254 time_ms: received_time,
2255 ..Default::default()
2256 };
2257
2258 let result = super::verify_metadata_time(&metadata, now_time, tolerance);
2259
2260 assert_eq!(result.is_err(), expect_err);
2261 }
2262
2263 async fn expect_epoch_refresh(sdk: &mut super::Sdk) {
2273 use crate::platform::types::epoch::EpochQuery;
2274 use crate::platform::LimitQuery;
2275 use dpp::block::extended_epoch_info::{v0::ExtendedEpochInfoV0, ExtendedEpochInfo};
2276 use drive_proof_verifier::types::ExtendedEpochInfos;
2277
2278 let probe_query = LimitQuery {
2284 query: EpochQuery::genesis(),
2285 limit: Some(1),
2286 start_info: None,
2287 };
2288 let confirmation_query = LimitQuery {
2289 query: EpochQuery::ascending_from(0),
2290 limit: Some(2),
2291 start_info: None,
2292 };
2293
2294 let epoch = ExtendedEpochInfo::from(ExtendedEpochInfoV0 {
2295 index: 0,
2296 first_block_time: 0,
2297 first_block_height: 0,
2298 first_core_block_height: 0,
2299 fee_multiplier_permille: 0,
2300 protocol_version: dpp::version::LATEST_VERSION,
2301 });
2302
2303 sdk.mock()
2304 .expect_fetch::<ExtendedEpochInfo, _>(probe_query, Some(epoch.clone()))
2305 .await
2306 .expect("register epoch probe expectation");
2307 sdk.mock()
2308 .expect_fetch_many::<_, ExtendedEpochInfo, _, ExtendedEpochInfos>(
2309 confirmation_query,
2310 Some(ExtendedEpochInfos::from_iter([(0, Some(epoch))])),
2311 )
2312 .await
2313 .expect("register epoch refresh expectation");
2314 }
2315
2316 #[tokio::test]
2322 async fn test_refresh_ratchets_up_via_proven_query() {
2323 let mut sdk = mock_sdk_with_auto_detect(super::min_protocol_version(Network::Mainnet));
2324 assert_eq!(
2325 sdk.protocol_version_number(),
2326 super::min_protocol_version(Network::Mainnet)
2327 );
2328
2329 expect_epoch_refresh(&mut sdk).await;
2330
2331 let resulting = sdk
2332 .refresh_protocol_version()
2333 .await
2334 .expect("refresh should succeed");
2335
2336 assert_eq!(
2337 resulting,
2338 dpp::version::LATEST_VERSION,
2339 "returned version must reflect the ratchet to the network's latest"
2340 );
2341 assert_eq!(sdk.protocol_version_number(), dpp::version::LATEST_VERSION);
2342 assert_eq!(sdk.version().protocol_version, dpp::version::LATEST_VERSION);
2343 }
2344
2345 #[tokio::test]
2350 async fn test_refresh_leaves_pinned_sdk_unchanged() {
2351 use dpp::version::PlatformVersion;
2352
2353 let pinned = PlatformVersion::get(min_protocol_version(Network::Mainnet))
2355 .expect("mainnet-floor PV exists");
2356 let sdk = SdkBuilder::new_mock()
2357 .with_version(pinned)
2358 .build()
2359 .expect("mock Sdk should be created");
2360 assert_eq!(sdk.protocol_version_number(), pinned.protocol_version);
2361 assert!(sdk.version_pinned);
2362
2363 let resulting = sdk
2366 .refresh_protocol_version()
2367 .await
2368 .expect("pinned refresh is a no-op and must not error");
2369
2370 assert_eq!(
2371 resulting, pinned.protocol_version,
2372 "pinned version must not move"
2373 );
2374 assert_eq!(sdk.protocol_version_number(), pinned.protocol_version);
2375 }
2376
2377 #[tokio::test]
2382 async fn test_refresh_query_unavailable_keeps_current_version() {
2383 let starting = min_protocol_version(Network::Mainnet);
2384 let sdk = mock_sdk_with_auto_detect(starting);
2385 assert_eq!(sdk.protocol_version_number(), starting);
2386
2387 let resulting = sdk
2388 .refresh_protocol_version()
2389 .await
2390 .expect("refresh is best-effort and must not error when the query fails");
2391
2392 assert_eq!(
2393 resulting, starting,
2394 "a failed refresh must leave the stored version untouched (no fallback)"
2395 );
2396 assert_eq!(sdk.protocol_version_number(), starting);
2397 }
2398}