1use crate::error::{Error, StaleNodeError};
4use crate::internal_cache::NonceCache;
5use crate::mock::MockResponse;
6#[cfg(feature = "mocks")]
7use crate::mock::{provider::GrpcContextProvider, MockDashPlatformSdk};
8use crate::platform::fetch_current_no_parameters::FetchCurrent;
9use crate::platform::transition::put_settings::PutSettings;
10use crate::platform::Identifier;
11use arc_swap::ArcSwapOption;
12use dapi_grpc::mock::Mockable;
13use dapi_grpc::platform::v0::{Proof, ResponseMetadata};
14use dapi_grpc::platform::VersionedGrpcResponse;
15#[cfg(not(target_arch = "wasm32"))]
16use dapi_grpc::tonic::transport::Certificate;
17use dash_context_provider::ContextProvider;
18#[cfg(feature = "mocks")]
19use dash_context_provider::MockContextProvider;
20use dpp::bincode;
21use dpp::bincode::error::DecodeError;
22use dpp::block::extended_epoch_info::ExtendedEpochInfo;
23use dpp::dashcore::Network;
24use dpp::platform_serialization::PLATFORM_BINCODE_CONFIG;
25use dpp::prelude::IdentityNonce;
26use dpp::version::PlatformVersion;
27use drive::grovedb::operations::proof::GroveDBProof;
28use drive_proof_verifier::FromProof;
29pub use http::Uri;
30#[cfg(feature = "mocks")]
31use rs_dapi_client::mock::MockDapiClient;
32#[cfg(not(target_arch = "wasm32"))]
33use rs_dapi_client::transport::Socks5Proxy;
34pub use rs_dapi_client::Address;
35pub use rs_dapi_client::AddressBanInfo;
36pub use rs_dapi_client::AddressList;
37pub use rs_dapi_client::RequestSettings;
38use rs_dapi_client::{
39 transport::TransportRequest, DapiClient, DapiClientError, DapiRequestExecutor, ExecutionResult,
40};
41use std::fmt::Debug;
42#[cfg(feature = "mocks")]
43use std::num::NonZeroUsize;
44use std::path::Path;
45#[cfg(feature = "mocks")]
46use std::path::PathBuf;
47use std::sync::atomic::Ordering;
48use std::sync::{atomic, Arc};
49#[cfg(feature = "mocks")]
50use tokio::sync::{Mutex, MutexGuard};
51use tokio_util::sync::{CancellationToken, WaitForCancellationFuture};
52use zeroize::Zeroizing;
53
54#[cfg(all(test, feature = "mocks"))]
55mod document_query_shape_tests;
56#[cfg(all(test, feature = "mocks"))]
57mod identity_discovery_tests;
58mod protocol_gate;
59#[cfg(all(test, feature = "mocks"))]
60mod protocol_gate_tests;
61mod quorum_key;
62#[cfg(all(test, feature = "mocks"))]
63mod signed_version_tests;
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct ProtocolVersionInfo {
69 pub verified_version: Option<u32>,
71 pub effective_version: u32,
73 pub maximum_supported_version: u32,
75 pub newer_protocol_available: bool,
77}
78
79pub trait ProofResponseMetadata {
82 fn response_metadata(&self) -> Result<&ResponseMetadata, drive_proof_verifier::Error>;
84}
85
86impl<T: VersionedGrpcResponse> ProofResponseMetadata for T {
87 fn response_metadata(&self) -> Result<&ResponseMetadata, drive_proof_verifier::Error> {
88 self.metadata()
89 .map_err(|error| drive_proof_verifier::Error::ResponseDecodeError {
90 error: error.to_string(),
91 })
92 }
93}
94
95pub(crate) fn verifier_version(
100 stored: &'static PlatformVersion,
101 signed: u32,
102) -> &'static PlatformVersion {
103 if signed <= stored.protocol_version {
104 return stored;
105 }
106
107 match (signed <= PlatformVersion::latest().protocol_version)
111 .then(|| PlatformVersion::get(signed))
112 {
113 Some(Ok(version)) => version,
114 _ => {
115 let newest = PlatformVersion::latest();
116 tracing::debug!(
117 signed_protocol_version = signed,
118 stored_protocol_version = stored.protocol_version,
119 verifier_protocol_version = newest.protocol_version,
120 "unverified protocol version is unknown; selecting newest known verifier table"
121 );
122 newest
123 }
124 }
125}
126
127pub const DEFAULT_CONTRACT_CACHE_SIZE: usize = 100;
129pub const DEFAULT_TOKEN_CONFIG_CACHE_SIZE: usize = 100;
131pub const DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE: usize = 100;
133pub const fn min_protocol_version(network: Network) -> u32 {
147 match network {
148 Network::Mainnet => dpp::version::v13::PROTOCOL_VERSION_13,
149 Network::Testnet => dpp::version::v13::PROTOCOL_VERSION_13,
150 Network::Devnet => dpp::version::v14::PROTOCOL_VERSION_14,
151 Network::Regtest => dpp::version::v13::PROTOCOL_VERSION_13,
152 }
153}
154
155pub type ProtocolVersionObserver = Arc<dyn Fn(u32) + Send + Sync>;
158
159const DEFAULT_METADATA_TIME_TOLERANCE_MS: u64 = 31 * 60 * 1000;
161
162const DEFAULT_REQUEST_SETTINGS: RequestSettings = RequestSettings {
166 retries: Some(3),
167 timeout: None,
168 ban_failed_address: None,
169 connect_timeout: None,
170 max_decoding_message_size: None,
171};
172
173fn default_address_list_for_network(network: Network) -> AddressList {
202 if !matches!(network, Network::Mainnet | Network::Testnet) {
203 panic!("default address list is only available for mainnet and testnet");
204 }
205
206 let seeds = dash_network_seeds::evo_seeds(network);
207 let filtered = address_list_from_seeds(&seeds, true);
208 if filtered.is_empty() {
209 tracing::warn!(
210 ?network,
211 "all seed entries have failing TLS probes; falling back to unfiltered seed list"
212 );
213 return address_list_from_seeds(&seeds, false);
214 }
215 filtered
216}
217
218fn seed_tls_deterministically_bad(platform: Option<&dash_network_seeds::PlatformStatus>) -> bool {
224 use dash_network_seeds::{Reachability, SslStatus};
225 let Some(platform) = platform else {
226 return false;
227 };
228 match platform.ssl {
229 SslStatus::Expired | SslStatus::SelfSigned | SslStatus::Untrusted => true,
230 SslStatus::NoHandshake => platform.reachable == Reachability::Ok,
231 SslStatus::Valid | SslStatus::Unknown => false,
232 }
233}
234
235fn address_list_from_seeds(
239 seeds: &[dash_network_seeds::MasternodeSeed],
240 skip_bad_tls: bool,
241) -> AddressList {
242 let mut list = AddressList::new();
243 for seed in seeds {
244 let Some(port) = seed.platform_http_port else {
245 continue;
246 };
247 if skip_bad_tls && seed_tls_deterministically_bad(seed.platform.as_ref()) {
248 continue;
249 }
250 let url = format!("https://{}:{}", seed.address.ip(), port);
251 if let Ok(uri) = url.parse::<Uri>() {
252 if let Ok(address) = Address::try_from(uri) {
253 list.add(address);
254 }
255 }
256 }
257 list
258}
259
260#[derive(Clone, Copy)]
262pub(crate) struct MetadataArrival {
263 height: u64,
264 time_ms: u64,
265}
266
267pub struct Sdk {
293 pub network: Network,
295 inner: SdkInstance,
296 proofs: bool,
301
302 nonce_cache: Arc<NonceCache>,
304
305 context_provider: ArcSwapOption<Box<dyn ContextProvider>>,
311
312 protocol_version: Arc<atomic::AtomicU32>,
314
315 version_pinned: bool,
319
320 verified_protocol_version: Arc<atomic::AtomicU32>,
326
327 protocol_version_observer: Option<ProtocolVersionObserver>,
330
331 #[cfg(all(test, feature = "mocks"))]
333 enforce_protocol_gate_on_mock: bool,
334
335 metadata_last_seen_height: Arc<atomic::AtomicU64>,
339
340 metadata_height_tolerance: Option<u64>,
344
345 metadata_time_tolerance_ms: Option<u64>,
349
350 pub(crate) cancel_token: CancellationToken,
352
353 pub(crate) dapi_client_settings: RequestSettings,
355
356 #[cfg(feature = "mocks")]
357 dump_dir: Option<PathBuf>,
358}
359impl Clone for Sdk {
360 fn clone(&self) -> Self {
361 Self {
362 network: self.network,
363 inner: self.inner.clone(),
364 proofs: self.proofs,
365 nonce_cache: Arc::clone(&self.nonce_cache),
366 context_provider: ArcSwapOption::new(self.context_provider.load_full()),
367 cancel_token: self.cancel_token.clone(),
368 protocol_version: Arc::clone(&self.protocol_version),
369 version_pinned: self.version_pinned,
370 verified_protocol_version: Arc::clone(&self.verified_protocol_version),
371 protocol_version_observer: self.protocol_version_observer.clone(),
372 #[cfg(all(test, feature = "mocks"))]
373 enforce_protocol_gate_on_mock: self.enforce_protocol_gate_on_mock,
374 metadata_last_seen_height: Arc::clone(&self.metadata_last_seen_height),
375 metadata_height_tolerance: self.metadata_height_tolerance,
376 metadata_time_tolerance_ms: self.metadata_time_tolerance_ms,
377 dapi_client_settings: self.dapi_client_settings,
378 #[cfg(feature = "mocks")]
379 dump_dir: self.dump_dir.clone(),
380 }
381 }
382}
383
384impl Debug for Sdk {
385 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386 match &self.inner {
387 SdkInstance::Dapi { dapi, .. } => f
388 .debug_struct("Sdk")
389 .field("dapi", dapi)
390 .field("proofs", &self.proofs)
391 .finish(),
392 #[cfg(feature = "mocks")]
393 SdkInstance::Mock { mock, .. } => f
394 .debug_struct("Sdk")
395 .field("mock", mock)
396 .field("proofs", &self.proofs)
397 .finish(),
398 }
399 }
400}
401
402#[derive(Debug, Clone)]
407enum SdkInstance {
408 Dapi {
410 dapi: DapiClient,
412 },
413 #[cfg(feature = "mocks")]
415 Mock {
416 dapi: Arc<Mutex<MockDapiClient>>,
420 mock: Arc<Mutex<MockDashPlatformSdk>>,
422 address_list: AddressList,
423 },
424}
425
426impl Sdk {
427 pub fn new_mock() -> Self {
433 SdkBuilder::default()
434 .build()
435 .expect("mock should be created")
436 }
437
438 fn freshness_criteria(&self, method_name: &str) -> (Option<u64>, Option<u64>) {
443 match method_name {
444 "get_addresses_infos" | "get_address_info" => (
446 self.metadata_height_tolerance.map(|_| 0),
447 self.metadata_time_tolerance_ms,
448 ),
449 "get_addresses_trunk_state" | "get_addresses_branch_state" => (
450 self.metadata_time_tolerance_ms
455 .is_none()
456 .then_some(self.metadata_height_tolerance)
457 .flatten(),
458 self.metadata_time_tolerance_ms
459 .map(|configured| configured.min(DEFAULT_METADATA_TIME_TOLERANCE_MS)),
460 ),
461 _ => (
462 self.metadata_height_tolerance,
463 self.metadata_time_tolerance_ms,
464 ),
465 }
466 }
467
468 pub fn verify_response_metadata(
479 &self,
480 method_name: &str,
481 metadata: &ResponseMetadata,
482 ) -> Result<(), Error> {
483 self.check_response_freshness_as_of(method_name, metadata, None)
484 }
485
486 pub(crate) fn verify_response_metadata_as_of(
490 &self,
491 method_name: &str,
492 metadata: &ResponseMetadata,
493 arrival: Option<MetadataArrival>,
494 ) -> Result<(), Error> {
495 self.check_response_freshness_as_of(method_name, metadata, arrival)?;
496 self.maybe_update_protocol_version(metadata.protocol_version);
497 Ok(())
498 }
499
500 fn check_response_freshness_as_of(
505 &self,
506 method_name: &str,
507 metadata: &ResponseMetadata,
508 arrival: Option<MetadataArrival>,
509 ) -> Result<(), Error> {
510 let (metadata_height_tolerance, metadata_time_tolerance_ms) =
511 self.freshness_criteria(method_name);
512 if let Some(time_tolerance) = metadata_time_tolerance_ms {
515 let now = arrival
516 .map(|arrival| arrival.time_ms)
517 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis() as u64);
518 verify_metadata_time(metadata, now, time_tolerance)?;
519 };
520 if let Some(height_tolerance) = metadata_height_tolerance {
521 verify_metadata_height(
522 metadata,
523 height_tolerance,
524 Arc::clone(&(self.metadata_last_seen_height)),
525 arrival.map(|arrival| arrival.height),
526 )?;
527 };
528
529 Ok(())
530 }
531
532 fn learn_verified_protocol_version(&self, protocol_version: u32) {
540 self.record_verified_protocol_version(protocol_version);
541 self.maybe_update_protocol_version(protocol_version);
542 }
543
544 fn record_verified_protocol_version(&self, protocol_version: u32) {
547 self.verified_protocol_version
548 .fetch_max(protocol_version, Ordering::Relaxed);
549 }
550
551 pub(crate) fn accept_verified_metadata(
559 &self,
560 method_name: &str,
561 metadata: &ResponseMetadata,
562 arrival: Option<MetadataArrival>,
563 ) -> Result<(), Error> {
564 if metadata.protocol_version > self.protocol_version_number()
565 && PlatformVersion::get(metadata.protocol_version).is_err()
566 {
567 tracing::warn!(
568 signed_protocol_version = metadata.protocol_version,
569 stored_protocol_version = self.protocol_version_number(),
570 verifier_protocol_version = PlatformVersion::latest().protocol_version,
571 "authenticated network protocol version is unknown; using newest known verifier table"
572 );
573 }
574 self.check_response_freshness_as_of(method_name, metadata, arrival)
575 .inspect_err(|err| {
576 tracing::warn!(%err, method = method_name, "received response with stale metadata; try another server");
577 })?;
578 self.learn_verified_protocol_version(metadata.protocol_version);
579 Ok(())
580 }
581
582 fn classify_verifier_error(
609 &self,
610 error: drive_proof_verifier::Error,
611 method_name: &str,
612 metadata: Option<&ResponseMetadata>,
613 arrival: MetadataArrival,
614 ) -> Error {
615 if let drive_proof_verifier::Error::UnusableVerifiedResult { .. } = error {
616 return match metadata {
621 Some(metadata) => {
622 match self.accept_verified_metadata(method_name, metadata, Some(arrival)) {
623 Ok(()) => error.into(),
624 Err(stale) => stale,
625 }
626 }
627 None => Error::Proof(error),
628 };
629 }
630 if let drive_proof_verifier::Error::ProofProtocolVersionMismatch { signed, verifier } =
631 error
632 {
633 if signed != 0
636 && signed < verifier
637 && self.verified_protocol_version.load(Ordering::Relaxed) < verifier
638 {
639 if let Some(metadata) = metadata {
640 if let Err(stale) =
641 self.check_unverified_freshness(method_name, metadata, arrival)
642 {
643 return stale;
644 }
645 }
646 return StaleNodeError::ProtocolVersion {
647 expected_protocol_version: verifier,
648 received_protocol_version: signed,
649 }
650 .into();
651 }
652 }
653 error.into()
654 }
655
656 fn check_unverified_freshness(
659 &self,
660 method_name: &str,
661 metadata: &ResponseMetadata,
662 arrival: MetadataArrival,
663 ) -> Result<(), Error> {
664 let (height_tolerance, time_tolerance) = self.freshness_criteria(method_name);
665 if let Some(tolerance) = time_tolerance {
666 verify_metadata_time(metadata, arrival.time_ms, tolerance)?;
667 }
668 if let Some(tolerance) = height_tolerance {
669 check_metadata_height(metadata.height, arrival.height, tolerance)?;
670 }
671 Ok(())
672 }
673
674 fn metadata_arrival(&self) -> MetadataArrival {
676 MetadataArrival {
677 height: self.metadata_last_seen_height.load(Ordering::Acquire),
678 time_ms: chrono::Utc::now().timestamp_millis() as u64,
679 }
680 }
681
682 fn maybe_update_protocol_version(&self, received_version: u32) {
688 if self.version_pinned {
689 return;
690 }
691
692 if received_version == 0 {
693 return;
694 }
695
696 let current = self.protocol_version.load(Ordering::Relaxed);
697
698 if received_version <= current {
699 return;
700 }
701
702 if PlatformVersion::get(received_version).is_err() {
704 tracing::warn!(
705 received_version,
706 current_version = current,
707 "received unknown protocol version from network; keeping current"
708 );
709 return;
710 }
711
712 let previous = self
713 .protocol_version
714 .fetch_max(received_version, Ordering::Release);
715 if previous < received_version {
716 tracing::info!(
717 target: "dash_sdk::protocol_version",
718 from = previous,
719 to = received_version,
720 "ratcheting protocol version upward"
721 );
722 if let Some(observer) = &self.protocol_version_observer {
723 observer(received_version);
724 }
725 }
726 }
727
728 pub async fn refresh_protocol_version(&self) -> Result<u32, Error> {
758 if !self.prove() {
759 return Ok(self.protocol_version_number());
760 }
761 if !self.version_pinned {
762 if let Err(error) = ExtendedEpochInfo::fetch_current(self).await {
763 tracing::warn!(
764 target: "dash_sdk::protocol_version",
765 %error,
766 version = self.protocol_version_number(),
767 "proven protocol-version refresh failed; keeping the highest \
768 proof-verified version learned so far (never falling back to \
769 an unverified one)"
770 );
771 }
772 }
773 Ok(self.protocol_version_number())
774 }
775
776 pub(crate) async fn parse_proof_with_metadata_and_proof<R, O: FromProof<R> + MockResponse>(
801 &self,
802 request: O::Request,
803 response: O::Response,
804 method_name: &'static str,
805 ) -> Result<(Option<O>, ResponseMetadata, Proof), Error>
806 where
807 O::Request: Mockable + Clone,
808 O::Response: Clone + ProofResponseMetadata,
809 {
810 let provider = self
811 .context_provider()
812 .ok_or(drive_proof_verifier::Error::ContextProviderNotSet)?;
813
814 match self.inner {
815 SdkInstance::Dapi { .. } => {
816 self.verify_fetching_quorum_key::<R, O>(request, response, &provider, method_name)
817 .await
818 }
819 #[cfg(feature = "mocks")]
820 SdkInstance::Mock { ref mock, .. } => {
821 let metadata = response.response_metadata().ok().cloned();
822 let arrival = self.metadata_arrival();
823 let (verified, from_expectation) = mock
824 .lock()
825 .await
826 .parse_proof_with_metadata(request, response, self.version())
827 .map_err(|error| {
828 self.classify_verifier_error(error, method_name, metadata.as_ref(), arrival)
829 })?;
830 if from_expectation {
831 self.verify_response_metadata_as_of(method_name, &verified.1, None)?;
837 } else {
838 self.accept_verified_metadata(method_name, &verified.1, None)?;
841 }
842 Ok(verified)
843 }
844 }
845 }
846
847 pub fn context_provider(&self) -> Option<impl ContextProvider> {
849 let provider_guard = self.context_provider.load();
850 let provider = provider_guard.as_ref().map(Arc::clone);
851
852 provider
853 }
854
855 #[cfg(feature = "mocks")]
866 pub fn mock(&mut self) -> MutexGuard<'_, MockDashPlatformSdk> {
867 if let Sdk {
868 inner: SdkInstance::Mock { ref mock, .. },
869 ..
870 } = self
871 {
872 mock.try_lock()
873 .expect("mock sdk is in use by another thread and cannot be reconfigured")
874 } else {
875 panic!("not a mock")
876 }
877 }
878
879 pub async fn get_identity_nonce(
884 &self,
885 identity_id: Identifier,
886 bump_first: bool,
887 settings: Option<PutSettings>,
888 ) -> Result<IdentityNonce, Error> {
889 let settings = settings.unwrap_or_default();
890 let nonce = self
891 .nonce_cache
892 .get_identity_nonce(self, identity_id, bump_first, &settings)
893 .await?;
894
895 tracing::trace!(
896 identity_id = %identity_id,
897 bump_first,
898 nonce,
899 "Fetched identity nonce"
900 );
901
902 Ok(nonce)
903 }
904
905 pub async fn get_identity_contract_nonce(
910 &self,
911 identity_id: Identifier,
912 contract_id: Identifier,
913 bump_first: bool,
914 settings: Option<PutSettings>,
915 ) -> Result<IdentityNonce, Error> {
916 let settings = settings.unwrap_or_default();
917 self.nonce_cache
918 .get_identity_contract_nonce(self, identity_id, contract_id, bump_first, &settings)
919 .await
920 }
921
922 pub async fn refresh_identity_nonce(&self, identity_id: &Identifier) {
926 self.nonce_cache.refresh(identity_id).await;
927 }
928
929 pub fn version<'v>(&self) -> &'v PlatformVersion {
937 let v = self.protocol_version.load(Ordering::Acquire);
938 PlatformVersion::get(v).unwrap_or_else(|_| PlatformVersion::latest())
939 }
940
941 pub fn protocol_version_number(&self) -> u32 {
943 self.protocol_version.load(Ordering::Acquire)
944 }
945
946 pub fn protocol_version_info(&self) -> ProtocolVersionInfo {
949 let observed = self.verified_protocol_version.load(Ordering::Relaxed);
950 let maximum_supported_version = PlatformVersion::latest().protocol_version;
951 ProtocolVersionInfo {
952 verified_version: (observed != 0).then_some(observed),
953 effective_version: self.protocol_version_number(),
954 maximum_supported_version,
955 newer_protocol_available: observed > maximum_supported_version,
956 }
957 }
958
959 pub fn prove(&self) -> bool {
962 self.proofs
963 }
964
965 pub fn query_settings(&self) -> crate::platform::QuerySettings<'_> {
976 let stored = self.version();
980 let verified = self.verified_protocol_version.load(Ordering::Relaxed);
981 crate::platform::QuerySettings {
982 request_settings: &self.dapi_client_settings,
983 protocol_version: protocol_gate::version_for_requests(
984 stored,
985 verified,
986 self.version_pinned,
987 ),
988 verified_protocol_version: verified,
989 prove: self.prove(),
990 }
991 }
992
993 pub fn set_context_provider<C: ContextProvider + 'static>(&self, context_provider: C) {
1001 self.context_provider
1002 .swap(Some(Arc::new(Box::new(context_provider))));
1003 }
1004
1005 pub fn cancelled(&self) -> WaitForCancellationFuture<'_> {
1007 self.cancel_token.cancelled()
1008 }
1009
1010 pub fn shutdown(&self) {
1012 self.cancel_token.cancel();
1013 }
1014
1015 pub fn address_list(&self) -> &AddressList {
1017 match &self.inner {
1018 SdkInstance::Dapi { dapi, .. } => dapi.address_list(),
1019 #[cfg(feature = "mocks")]
1020 SdkInstance::Mock { address_list, .. } => address_list,
1021 }
1022 }
1023
1024 pub fn address_ban_info(&self) -> Vec<AddressBanInfo> {
1031 self.address_list().ban_info()
1032 }
1033}
1034
1035pub(crate) fn verify_metadata_time(
1043 metadata: &ResponseMetadata,
1044 now_ms: u64,
1045 tolerance_ms: u64,
1046) -> Result<(), Error> {
1047 let metadata_time = metadata.time_ms;
1048
1049 if now_ms.abs_diff(metadata_time) > tolerance_ms {
1051 return Err(StaleNodeError::Time {
1052 expected_timestamp_ms: now_ms,
1053 received_timestamp_ms: metadata_time,
1054 tolerance_ms,
1055 }
1056 .into());
1057 }
1058
1059 tracing::trace!(
1060 expected_time = now_ms,
1061 received_time = metadata_time,
1062 tolerance_ms,
1063 "received response with valid time"
1064 );
1065 Ok(())
1066}
1067
1068fn verify_metadata_height(
1071 metadata: &ResponseMetadata,
1072 tolerance: u64,
1073 last_seen_height: Arc<atomic::AtomicU64>,
1074 seen_height: Option<u64>,
1075) -> Result<(), Error> {
1076 let received_height = metadata.height;
1077 let previous_height = last_seen_height.fetch_max(received_height, Ordering::AcqRel);
1083 let expected_height = seen_height
1084 .unwrap_or_else(|| previous_height.max(last_seen_height.load(Ordering::Acquire)));
1085 check_metadata_height(received_height, expected_height, tolerance)
1086}
1087
1088fn check_metadata_height(
1091 received_height: u64,
1092 expected_height: u64,
1093 tolerance: u64,
1094) -> Result<(), Error> {
1095 if expected_height > tolerance && received_height < expected_height.saturating_sub(tolerance) {
1096 return Err(StaleNodeError::Height {
1097 expected_height,
1098 received_height,
1099 tolerance_blocks: tolerance,
1100 }
1101 .into());
1102 }
1103
1104 tracing::trace!(
1105 expected_height,
1106 received_height,
1107 tolerance,
1108 "received response within the monotonic height window"
1109 );
1110
1111 Ok(())
1112}
1113
1114#[async_trait::async_trait]
1115impl DapiRequestExecutor for Sdk {
1116 async fn execute<R: TransportRequest>(
1117 &self,
1118 request: R,
1119 settings: RequestSettings,
1120 ) -> ExecutionResult<R::Response, DapiClientError> {
1121 match self.inner {
1122 SdkInstance::Dapi { ref dapi, .. } => dapi.execute(request, settings).await,
1123 #[cfg(feature = "mocks")]
1124 SdkInstance::Mock { ref dapi, .. } => {
1125 let dapi_guard = dapi.lock().await;
1126 dapi_guard.execute(request, settings).await
1127 }
1128 }
1129 }
1130}
1131
1132pub struct SdkBuilder {
1145 addresses: Option<AddressList>,
1149 settings: Option<RequestSettings>,
1150
1151 network: Network,
1152
1153 core_ip: String,
1154 core_port: u16,
1155 core_user: String,
1156 core_password: Zeroizing<String>,
1157
1158 proofs: bool,
1160
1161 version: Option<&'static PlatformVersion>,
1164
1165 version_pinned: bool,
1169
1170 protocol_version_observer: Option<ProtocolVersionObserver>,
1172
1173 #[cfg(feature = "mocks")]
1175 data_contract_cache_size: NonZeroUsize,
1176
1177 #[cfg(feature = "mocks")]
1179 token_config_cache_size: NonZeroUsize,
1180
1181 #[cfg(feature = "mocks")]
1183 quorum_public_keys_cache_size: NonZeroUsize,
1184
1185 context_provider: Option<Box<dyn ContextProvider>>,
1187
1188 metadata_height_tolerance: Option<u64>,
1193
1194 metadata_time_tolerance_ms: Option<u64>,
1198
1199 trusted_initial_height: Option<u64>,
1202
1203 #[cfg(feature = "mocks")]
1205 dump_dir: Option<PathBuf>,
1206
1207 pub(crate) cancel_token: CancellationToken,
1209
1210 #[cfg(not(target_arch = "wasm32"))]
1212 ca_certificate: Option<Certificate>,
1213
1214 #[cfg(not(target_arch = "wasm32"))]
1216 proxy: Option<Socks5Proxy>,
1217}
1218
1219impl Default for SdkBuilder {
1220 fn default() -> Self {
1222 Self {
1223 addresses: None,
1224 settings: None,
1225 network: Network::Mainnet,
1226 core_ip: "".to_string(),
1227 core_port: 0,
1228 core_password: "".to_string().into(),
1229 core_user: "".to_string(),
1230
1231 proofs: true,
1232 metadata_height_tolerance: Some(1),
1233 metadata_time_tolerance_ms: None,
1234 trusted_initial_height: None,
1235
1236 #[cfg(feature = "mocks")]
1237 data_contract_cache_size: NonZeroUsize::new(DEFAULT_CONTRACT_CACHE_SIZE)
1238 .expect("data contract cache size must be positive"),
1239
1240 #[cfg(feature = "mocks")]
1241 token_config_cache_size: NonZeroUsize::new(DEFAULT_TOKEN_CONFIG_CACHE_SIZE)
1242 .expect("token config cache size must be positive"),
1243
1244 #[cfg(feature = "mocks")]
1245 quorum_public_keys_cache_size: NonZeroUsize::new(DEFAULT_QUORUM_PUBLIC_KEYS_CACHE_SIZE)
1246 .expect("quorum public keys cache size must be positive"),
1247
1248 context_provider: None,
1249
1250 cancel_token: CancellationToken::new(),
1251
1252 version: None,
1256 version_pinned: false,
1257 protocol_version_observer: None,
1258 #[cfg(not(target_arch = "wasm32"))]
1259 ca_certificate: None,
1260 #[cfg(not(target_arch = "wasm32"))]
1261 proxy: None,
1262
1263 #[cfg(feature = "mocks")]
1264 dump_dir: None,
1265 }
1266 }
1267}
1268
1269impl SdkBuilder {
1270 pub fn with_proofs(mut self, proofs: bool) -> Self {
1277 self.proofs = proofs;
1278 self
1279 }
1280 pub fn new(addresses: AddressList) -> Self {
1282 Self {
1283 addresses: Some(addresses),
1284 metadata_time_tolerance_ms: Some(DEFAULT_METADATA_TIME_TOLERANCE_MS),
1285 ..Default::default()
1286 }
1287 }
1288
1289 pub fn with_address_list(mut self, addresses: AddressList) -> Self {
1291 self.addresses = Some(addresses);
1292 self
1293 }
1294
1295 pub fn new_mock() -> Self {
1297 Self::default()
1298 }
1299
1300 pub fn new_testnet() -> Self {
1306 let address_list = default_address_list_for_network(Network::Testnet);
1307
1308 Self::new(address_list).with_network(Network::Testnet)
1309 }
1310
1311 pub fn new_mainnet() -> Self {
1324 let address_list = default_address_list_for_network(Network::Mainnet);
1325
1326 Self::new(address_list).with_network(Network::Mainnet)
1327 }
1328
1329 pub fn with_network(mut self, network: Network) -> Self {
1333 self.network = network;
1334 self
1335 }
1336
1337 #[cfg(not(target_arch = "wasm32"))]
1347 pub fn with_ca_certificate(mut self, pem_certificate: Certificate) -> Self {
1348 self.ca_certificate = Some(pem_certificate);
1349 self
1350 }
1351
1352 #[cfg(not(target_arch = "wasm32"))]
1357 pub fn with_ca_certificate_file(
1358 self,
1359 certificate_file_path: impl AsRef<Path>,
1360 ) -> std::io::Result<Self> {
1361 let pem = std::fs::read(certificate_file_path)?;
1362 let cert = Certificate::from_pem(pem);
1363
1364 Ok(self.with_ca_certificate(cert))
1365 }
1366
1367 #[cfg(not(target_arch = "wasm32"))]
1373 pub fn with_proxy(mut self, proxy: Socks5Proxy) -> Self {
1374 self.proxy = Some(proxy);
1375 self
1376 }
1377
1378 pub fn with_settings(mut self, settings: RequestSettings) -> Self {
1386 self.settings = Some(settings);
1387 self
1388 }
1389
1390 pub fn with_version(mut self, version: &'static PlatformVersion) -> Self {
1410 self.version = Some(version);
1411 self.version_pinned = true;
1412 self
1413 }
1414
1415 pub fn with_initial_version(mut self, version: &'static PlatformVersion) -> Self {
1431 self.version = Some(version);
1432 self.version_pinned = false;
1433 self
1434 }
1435
1436 pub fn with_protocol_version_observer(mut self, observer: ProtocolVersionObserver) -> Self {
1446 self.protocol_version_observer = Some(observer);
1447 self
1448 }
1449
1450 pub fn with_context_provider<C: ContextProvider + 'static>(
1457 mut self,
1458 context_provider: C,
1459 ) -> Self {
1460 self.context_provider = Some(Box::new(context_provider));
1461
1462 self
1463 }
1464
1465 pub fn with_cancellation_token(mut self, cancel_token: CancellationToken) -> Self {
1469 self.cancel_token = cancel_token;
1470 self
1471 }
1472
1473 pub fn with_core(mut self, ip: &str, port: u16, user: &str, password: &str) -> Self {
1481 self.core_ip = ip.to_string();
1482 self.core_port = port;
1483 self.core_user = user.to_string();
1484 self.core_password = Zeroizing::from(password.to_string());
1485
1486 self
1487 }
1488
1489 pub fn with_height_tolerance(mut self, tolerance: Option<u64>) -> Self {
1501 self.metadata_height_tolerance = tolerance;
1502 self
1503 }
1504
1505 pub fn with_time_tolerance(mut self, tolerance_ms: Option<u64>) -> Self {
1522 self.metadata_time_tolerance_ms = tolerance_ms;
1523 self
1524 }
1525
1526 pub fn with_trusted_initial_height(mut self, height: u64) -> Self {
1532 self.trusted_initial_height = Some(height);
1533 self
1534 }
1535
1536 #[cfg(feature = "mocks")]
1549 pub fn with_dump_dir(mut self, dump_dir: &Path) -> Self {
1550 self.dump_dir = Some(dump_dir.to_path_buf());
1551 self
1552 }
1553
1554 pub fn build(self) -> Result<Sdk, Error> {
1562 let is_network_sdk = self.addresses.is_some();
1563 let has_height_anchor = self
1564 .trusted_initial_height
1565 .zip(self.metadata_height_tolerance)
1566 .is_some_and(|(height, tolerance)| height > tolerance);
1567 if is_network_sdk
1568 && self.proofs
1569 && self.metadata_time_tolerance_ms.is_none()
1570 && !has_height_anchor
1571 {
1572 return Err(Error::Config(
1573 "proof mode requires a trusted initial height or signed-time freshness policy"
1574 .to_string(),
1575 ));
1576 }
1577
1578 let dapi_client_settings = match self.settings {
1579 Some(settings) => DEFAULT_REQUEST_SETTINGS.override_by(settings),
1580 None => DEFAULT_REQUEST_SETTINGS,
1581 };
1582
1583 let initial_version = self.version.unwrap_or_else(|| {
1584 PlatformVersion::get(min_protocol_version(self.network))
1585 .expect("min_protocol_version for a network must be a valid version")
1586 });
1587
1588 let sdk= match self.addresses {
1589 Some(addresses) => {
1591 #[allow(unused_mut)] let mut dapi = DapiClient::new(addresses, dapi_client_settings);
1593 #[cfg(not(target_arch = "wasm32"))]
1594 if let Some(pem) = self.ca_certificate {
1595 dapi = dapi.with_ca_certificate(pem);
1596 }
1597 #[cfg(not(target_arch = "wasm32"))]
1598 if let Some(proxy) = self.proxy {
1599 dapi = dapi.with_proxy(proxy);
1600 }
1601
1602 #[cfg(feature = "mocks")]
1603 let dapi = dapi.dump_dir(self.dump_dir.clone());
1604
1605 #[allow(unused_mut)] let mut sdk= Sdk{
1607 network: self.network,
1608 dapi_client_settings,
1609 inner:SdkInstance::Dapi { dapi },
1610 proofs:self.proofs,
1611 context_provider: ArcSwapOption::new( self.context_provider.map(Arc::new)),
1612 cancel_token: self.cancel_token,
1613 nonce_cache: Default::default(),
1614 protocol_version: Arc::new(atomic::AtomicU32::new(initial_version.protocol_version)),
1617 version_pinned: self.version_pinned,
1618 verified_protocol_version: Arc::new(atomic::AtomicU32::new(0)),
1619 protocol_version_observer: self.protocol_version_observer.clone(),
1620 #[cfg(all(test, feature = "mocks"))]
1621 enforce_protocol_gate_on_mock: false,
1622 metadata_last_seen_height: Arc::new(atomic::AtomicU64::new(
1623 self.trusted_initial_height.unwrap_or(0),
1624 )),
1625 metadata_height_tolerance: self.metadata_height_tolerance,
1626 metadata_time_tolerance_ms: self.metadata_time_tolerance_ms,
1627 #[cfg(feature = "mocks")]
1628 dump_dir: self.dump_dir,
1629 };
1630 if sdk.context_provider.load().is_none() {
1632 #[cfg(feature = "mocks")]
1633 if !self.core_ip.is_empty() {
1634 tracing::warn!(
1635 "ContextProvider not set, falling back to a mock one; use SdkBuilder::with_context_provider() to set it up");
1636 let mut context_provider = GrpcContextProvider::new(None,
1637 &self.core_ip, self.core_port, &self.core_user, &self.core_password,
1638 self.data_contract_cache_size, self.token_config_cache_size, self.quorum_public_keys_cache_size)?;
1639 #[cfg(feature = "mocks")]
1640 if sdk.dump_dir.is_some() {
1641 context_provider.set_dump_dir(sdk.dump_dir.clone());
1642 }
1643 let context_provider= Arc::new(context_provider);
1646 sdk.context_provider.swap(Some(Arc::new(Box::new(context_provider.clone()))));
1647 context_provider.set_sdk(Some(sdk.clone()));
1648 } else{
1649 return Err(Error::Config(concat!(
1650 "context provider is not set, configure it with SdkBuilder::with_context_provider() ",
1651 "or configure Core access with SdkBuilder::with_core() to use mock context provider")
1652 .to_string()));
1653 }
1654 #[cfg(not(feature = "mocks"))]
1655 return Err(Error::Config(concat!(
1656 "context provider is not set, configure it with SdkBuilder::with_context_provider() ",
1657 "or enable `mocks` feature to use mock context provider")
1658 .to_string()));
1659 };
1660
1661 sdk
1662 },
1663 #[cfg(feature = "mocks")]
1664 None => {
1666 let dapi =Arc::new(Mutex::new( MockDapiClient::new()));
1667 let context_provider = self.context_provider.unwrap_or_else(||{
1669 let mut cp=MockContextProvider::new();
1670 if let Some(ref dump_dir) = self.dump_dir {
1671 cp.quorum_keys_dir(Some(dump_dir.clone()));
1672 }
1673 Box::new(cp)
1674 }
1675 );
1676 let mock_sdk = MockDashPlatformSdk::new(Arc::clone(&dapi));
1677 let mock_sdk = Arc::new(Mutex::new(mock_sdk));
1678 let sdk= Sdk {
1679 network: self.network,
1680 dapi_client_settings,
1681 inner:SdkInstance::Mock {
1682 mock:mock_sdk.clone(),
1683 dapi,
1684 address_list: AddressList::new(),
1685 },
1686 dump_dir: self.dump_dir.clone(),
1687 proofs:self.proofs,
1688 nonce_cache: Default::default(),
1689 protocol_version: Arc::new(atomic::AtomicU32::new(initial_version.protocol_version)),
1690 version_pinned: self.version_pinned,
1691 verified_protocol_version: Arc::new(atomic::AtomicU32::new(0)),
1692 protocol_version_observer: self.protocol_version_observer.clone(),
1693 #[cfg(all(test, feature = "mocks"))]
1694 enforce_protocol_gate_on_mock: false,
1695 context_provider: ArcSwapOption::new(Some(Arc::new(context_provider))),
1696 cancel_token: self.cancel_token,
1697 metadata_last_seen_height: Arc::new(atomic::AtomicU64::new(
1698 self.trusted_initial_height.unwrap_or(0),
1699 )),
1700 metadata_height_tolerance: self.metadata_height_tolerance,
1701 metadata_time_tolerance_ms: self.metadata_time_tolerance_ms,
1702 };
1703 let mut guard = mock_sdk.try_lock().expect("mock sdk is in use by another thread and cannot be reconfigured");
1704 guard.set_sdk(sdk.clone());
1705 if let Some(ref dump_dir) = self.dump_dir {
1706 guard.load_expectations_sync(dump_dir)?;
1707 };
1708
1709 sdk
1710 },
1711 #[cfg(not(feature = "mocks"))]
1712 None => return Err(Error::Config("Mock mode is not available. Please enable `mocks` feature or provide address list.".to_string())),
1713 };
1714
1715 Ok(sdk)
1716 }
1717}
1718
1719pub fn prettify_proof(proof: &Proof) -> String {
1720 let config = PLATFORM_BINCODE_CONFIG;
1721 let grovedb_proof: Result<GroveDBProof, DecodeError> =
1722 bincode::decode_from_slice_untrusted(&proof.grovedb_proof, config).map(|(a, _)| a);
1723
1724 let grovedb_proof_string = match grovedb_proof {
1725 Ok(proof) => format!("{}", proof),
1726 Err(_) => "Invalid GroveDBProof".to_string(),
1727 };
1728 format!(
1729 "Proof {{
1730 grovedb_proof: {},
1731 quorum_hash: 0x{},
1732 signature: 0x{},
1733 round: {},
1734 block_id_hash: 0x{},
1735 quorum_type: {},
1736 }}",
1737 grovedb_proof_string,
1738 hex::encode(&proof.quorum_hash),
1739 hex::encode(&proof.signature),
1740 proof.round,
1741 hex::encode(&proof.block_id_hash),
1742 proof.quorum_type,
1743 )
1744}
1745
1746#[cfg(test)]
1747pub(crate) mod test {
1748 use std::future::ready;
1749 use std::sync::Arc;
1750
1751 use crate::sync::retry;
1752 use dapi_grpc::platform::v0::{GetIdentityRequest, ResponseMetadata};
1753 use rs_dapi_client::transport::TransportRequest;
1754 use rs_dapi_client::{AddressList, ExecutionError, ExecutionResponse, RequestSettings};
1755 use test_case::test_matrix;
1756
1757 use crate::SdkBuilder;
1758
1759 use super::{min_protocol_version, Network};
1760
1761 #[test_matrix(["get_addresses_infos", "get_address_info"], [Some(1), Some(20)])]
1762 #[tokio::test]
1763 async fn should_retry_one_block_stale_address_nonce_read_on_fresh_node(
1764 method: &str,
1765 tolerance: Option<u64>,
1766 ) {
1767 let sdk = SdkBuilder::new_mock()
1768 .with_height_tolerance(tolerance)
1769 .with_trusted_initial_height(100)
1770 .build()
1771 .unwrap();
1772 let addresses: AddressList = "http://127.0.0.1:3001,http://127.0.0.1:3002"
1773 .parse()
1774 .unwrap();
1775 let nodes = addresses.get_live_addresses();
1776 let lagging = &nodes[0];
1777 let fresh = &nodes[1];
1778 let mut sends = 0;
1779 let response = retry(
1782 &addresses,
1783 RequestSettings {
1784 retries: Some(1),
1785 ban_failed_address: Some(true),
1786 ..Default::default()
1787 },
1788 |_| {
1789 sends += 1;
1790 let (node, height, nonce) = if sends == 1 {
1791 (lagging, 99, 0)
1792 } else {
1793 assert_eq!(addresses.get_live_address(), Some(fresh.clone()));
1794 (fresh, 100, 1)
1795 };
1796 ready(
1797 sdk.verify_response_metadata(
1798 method,
1799 &ResponseMetadata {
1800 height,
1801 ..Default::default()
1802 },
1803 )
1804 .map(|()| ExecutionResponse {
1805 inner: nonce,
1806 retries: 0,
1807 address: node.clone(),
1808 })
1809 .map_err(|inner| ExecutionError {
1810 inner,
1811 retries: 0,
1812 address: Some(node.clone()),
1813 }),
1814 )
1815 },
1816 )
1817 .await
1818 .unwrap();
1819 assert_eq!(sends, 2);
1820 assert_eq!(response.inner, 1);
1821 assert_eq!(response.address, *fresh);
1822 assert!(addresses.is_banned(lagging));
1823 }
1824
1825 #[test_matrix(["get_addresses_infos", "get_address_info", "get_identity"])]
1826 fn should_preserve_disabled_height_checks_for_address_info(method: &str) {
1827 let sdk = SdkBuilder::new_mock()
1828 .with_height_tolerance(None)
1829 .with_trusted_initial_height(100)
1830 .build()
1831 .unwrap();
1832 sdk.verify_response_metadata(
1833 method,
1834 &ResponseMetadata {
1835 height: 99,
1836 ..Default::default()
1837 },
1838 )
1839 .expect("height checks remain disabled");
1840 }
1841
1842 const MAINNET_PLATFORM_HTTP_PORT: u16 = 443;
1844 const TESTNET_PLATFORM_HTTP_PORT: u16 = 1443;
1846
1847 #[cfg(all(feature = "mocks", not(target_arch = "wasm32")))]
1848 #[test]
1849 fn with_proxy_reaches_the_dapi_client() {
1850 use rs_dapi_client::transport::{ProxyEndpoint, Socks5Auth, Socks5Proxy};
1851
1852 let proxy = Socks5Proxy {
1853 endpoint: ProxyEndpoint::Tcp("127.0.0.1:9050".parse().expect("address")),
1854 auth: Socks5Auth::RandomPerConnection,
1855 };
1856 let sdk = SdkBuilder::new("https://127.0.0.1:1443".parse().expect("address list"))
1857 .with_context_provider(dash_context_provider::MockContextProvider::new())
1858 .with_proxy(proxy.clone())
1859 .build()
1860 .expect("sdk");
1861
1862 match &sdk.inner {
1863 super::SdkInstance::Dapi { dapi } => assert_eq!(dapi.proxy, Some(proxy)),
1864 super::SdkInstance::Mock { .. } => panic!("expected a network SDK"),
1865 }
1866 }
1867
1868 #[test]
1869 fn new_testnet_sources_bootstrap_from_seeds() {
1870 let builder = SdkBuilder::new_testnet();
1871 let address_list = builder
1872 .addresses
1873 .as_ref()
1874 .expect("testnet builder should configure default addresses");
1875
1876 assert_eq!(builder.network, Network::Testnet);
1877 assert!(
1878 !address_list.is_empty(),
1879 "testnet must have at least one bootstrap address"
1880 );
1881 for address in address_list.get_live_addresses() {
1882 assert_eq!(
1883 address.uri().port_u16(),
1884 Some(TESTNET_PLATFORM_HTTP_PORT),
1885 "testnet bootstrap address must use the platform HTTP port",
1886 );
1887 }
1888 }
1889
1890 #[test]
1891 fn new_mainnet_sources_bootstrap_from_seeds() {
1892 let builder = SdkBuilder::new_mainnet();
1893 let address_list = builder
1894 .addresses
1895 .as_ref()
1896 .expect("mainnet builder should configure default addresses");
1897
1898 assert_eq!(builder.network, Network::Mainnet);
1899 assert!(
1900 !address_list.is_empty(),
1901 "mainnet must have at least one bootstrap address"
1902 );
1903 for address in address_list.get_live_addresses() {
1904 assert_eq!(
1905 address.uri().port_u16(),
1906 Some(MAINNET_PLATFORM_HTTP_PORT),
1907 "mainnet bootstrap address must use the platform HTTP port",
1908 );
1909 }
1910 }
1911
1912 mod seed_tls_filter {
1913 use super::super::{address_list_from_seeds, seed_tls_deterministically_bad};
1914 use dash_network_seeds::{
1915 CoreStatus, MasternodeSeed, MasternodeType, PlatformStatus, Reachability, SslStatus,
1916 };
1917
1918 fn seed(host: u8, platform: Option<PlatformStatus>) -> MasternodeSeed {
1921 MasternodeSeed {
1922 address: format!("203.0.113.{host}:9999").parse().unwrap(),
1923 mn_type: MasternodeType::Evo,
1924 platform_http_port: Some(443),
1925 core: CoreStatus::default(),
1926 platform,
1927 }
1928 }
1929
1930 fn status(ssl: SslStatus, reachable: Reachability) -> PlatformStatus {
1931 PlatformStatus {
1932 reachable,
1933 ssl,
1934 ..PlatformStatus::default()
1935 }
1936 }
1937
1938 #[test]
1944 fn classification_covers_every_status_combination() {
1945 let reachabilities = [
1946 Reachability::Unknown,
1947 Reachability::Ok,
1948 Reachability::Timeout,
1949 Reachability::Refused,
1950 Reachability::Error,
1951 ];
1952 for reachable in reachabilities {
1953 for ssl in [
1954 SslStatus::Expired,
1955 SslStatus::SelfSigned,
1956 SslStatus::Untrusted,
1957 ] {
1958 assert!(
1959 seed_tls_deterministically_bad(Some(&status(ssl, reachable))),
1960 "{ssl:?} must be rejected regardless of {reachable:?}"
1961 );
1962 }
1963 for ssl in [SslStatus::Valid, SslStatus::Unknown] {
1964 assert!(
1965 !seed_tls_deterministically_bad(Some(&status(ssl, reachable))),
1966 "{ssl:?} must never be rejected ({reachable:?})"
1967 );
1968 }
1969 assert_eq!(
1970 seed_tls_deterministically_bad(Some(&status(
1971 SslStatus::NoHandshake,
1972 reachable
1973 ))),
1974 reachable == Reachability::Ok,
1975 "NoHandshake must be rejected only when TCP connect succeeded ({reachable:?})"
1976 );
1977 }
1978 assert!(
1979 !seed_tls_deterministically_bad(None),
1980 "an unprobed seed must never be rejected"
1981 );
1982 }
1983
1984 #[test]
1985 fn filter_drops_only_deterministic_failures() {
1986 let seeds = vec![
1987 seed(1, Some(status(SslStatus::Valid, Reachability::Ok))),
1988 seed(2, Some(status(SslStatus::Expired, Reachability::Ok))),
1989 seed(
1990 3,
1991 Some(status(SslStatus::NoHandshake, Reachability::Timeout)),
1992 ),
1993 seed(4, Some(status(SslStatus::NoHandshake, Reachability::Ok))),
1994 seed(5, None),
1995 ];
1996 assert_eq!(address_list_from_seeds(&seeds, true).len(), 3);
1997 assert_eq!(address_list_from_seeds(&seeds, false).len(), 5);
1998 }
1999
2000 #[test]
2004 fn all_rejected_input_yields_empty_filtered_and_full_unfiltered() {
2005 let seeds = vec![
2006 seed(1, Some(status(SslStatus::Expired, Reachability::Ok))),
2007 seed(2, Some(status(SslStatus::Untrusted, Reachability::Timeout))),
2008 ];
2009 assert!(address_list_from_seeds(&seeds, true).is_empty());
2010 assert_eq!(address_list_from_seeds(&seeds, false).len(), 2);
2011 }
2012
2013 #[test]
2014 fn seed_without_platform_port_is_always_skipped() {
2015 let mut no_port = seed(1, Some(status(SslStatus::Valid, Reachability::Ok)));
2016 no_port.platform_http_port = None;
2017 assert!(address_list_from_seeds(&[no_port], false).is_empty());
2018 }
2019 }
2020
2021 #[test]
2025 fn bootstrap_counts_reasonable() {
2026 let mainnet = SdkBuilder::new_mainnet()
2027 .addresses
2028 .expect("mainnet builder should configure default addresses");
2029 let testnet = SdkBuilder::new_testnet()
2030 .addresses
2031 .expect("testnet builder should configure default addresses");
2032 assert!(
2033 mainnet.len() >= 10,
2034 "expected >=10 mainnet bootstrap addresses, got {}",
2035 mainnet.len()
2036 );
2037 assert!(
2038 testnet.len() >= 10,
2039 "expected >=10 testnet bootstrap addresses, got {}",
2040 testnet.len()
2041 );
2042 }
2043
2044 #[test]
2045 fn network_builders_enable_an_independent_time_anchor() {
2046 assert_eq!(
2047 SdkBuilder::new_testnet().metadata_time_tolerance_ms,
2048 Some(super::DEFAULT_METADATA_TIME_TOLERANCE_MS)
2049 );
2050 assert_eq!(SdkBuilder::new_mock().metadata_time_tolerance_ms, None);
2051 }
2052
2053 #[test]
2054 fn proof_enabled_network_builder_rejects_missing_freshness_anchor() {
2055 let error = SdkBuilder::new(super::AddressList::new())
2056 .with_time_tolerance(None)
2057 .build()
2058 .expect_err("network proof mode must have an independent freshness anchor");
2059
2060 assert!(
2061 matches!(error, crate::Error::Config(message) if message.contains("trusted initial height"))
2062 );
2063 }
2064
2065 #[test_matrix(0, 0; "zero height")]
2066 #[test_matrix(1, 1; "height equals tolerance")]
2067 #[test_matrix(1, 2; "height below tolerance")]
2068 fn proof_enabled_network_builder_rejects_ineffective_height_anchor(
2069 trusted_height: u64,
2070 tolerance: u64,
2071 ) {
2072 let error = SdkBuilder::new(super::AddressList::new())
2073 .with_time_tolerance(None)
2074 .with_height_tolerance(Some(tolerance))
2075 .with_trusted_initial_height(trusted_height)
2076 .build()
2077 .expect_err("trusted height must impose a freshness floor");
2078
2079 assert!(
2080 matches!(error, crate::Error::Config(message) if message.contains("trusted initial height"))
2081 );
2082 }
2083
2084 #[test]
2085 fn height_only_address_checkpoint_uses_trusted_height_floor() {
2086 let sdk = SdkBuilder::new_mock()
2087 .with_time_tolerance(None)
2088 .with_height_tolerance(Some(2))
2089 .with_trusted_initial_height(100)
2090 .build()
2091 .expect("effective trusted height should permit height-only proof mode");
2092
2093 assert!(matches!(
2094 sdk.verify_response_metadata(
2095 "get_addresses_trunk_state",
2096 &ResponseMetadata {
2097 height: 97,
2098 ..Default::default()
2099 },
2100 ),
2101 Err(crate::Error::StaleNode(
2102 super::StaleNodeError::Height { .. }
2103 ))
2104 ));
2105 assert_eq!(
2106 sdk.metadata_last_seen_height
2107 .load(std::sync::atomic::Ordering::Acquire),
2108 100,
2109 "a rejected stale checkpoint must not lower the trusted floor"
2110 );
2111 }
2112
2113 #[test]
2114 fn trusted_initial_height_seeds_the_high_water_mark() {
2115 let sdk = SdkBuilder::new_mock()
2116 .with_trusted_initial_height(42)
2117 .build()
2118 .expect("mock SDK should build");
2119
2120 assert_eq!(
2121 sdk.metadata_last_seen_height
2122 .load(std::sync::atomic::Ordering::Acquire),
2123 42
2124 );
2125 }
2126
2127 #[test_matrix(97..102, 100, 2, false; "valid height")]
2128 #[test_case(103, 100, 2, true; "invalid height")]
2129 fn test_verify_metadata_height(
2130 expected_height: u64,
2131 received_height: u64,
2132 tolerance: u64,
2133 expect_err: bool,
2134 ) {
2135 let metadata = ResponseMetadata {
2136 height: received_height,
2137 ..Default::default()
2138 };
2139
2140 let last_seen_height = Arc::new(std::sync::atomic::AtomicU64::new(expected_height));
2141
2142 let result = super::verify_metadata_height(
2143 &metadata,
2144 tolerance,
2145 Arc::clone(&last_seen_height),
2146 None,
2147 );
2148
2149 assert_eq!(result.is_err(), expect_err);
2150 if result.is_ok() {
2151 assert_eq!(
2152 last_seen_height.load(std::sync::atomic::Ordering::Relaxed),
2153 expected_height.max(received_height),
2154 "height high-water mark must never decrease"
2155 );
2156 }
2157 }
2158
2159 #[test]
2160 fn accepted_height_tolerance_cannot_walk_the_watermark_backwards() {
2161 let last_seen_height = Arc::new(std::sync::atomic::AtomicU64::new(100));
2162
2163 super::verify_metadata_height(
2164 &ResponseMetadata {
2165 height: 99,
2166 ..Default::default()
2167 },
2168 1,
2169 Arc::clone(&last_seen_height),
2170 None,
2171 )
2172 .expect("one block behind is within tolerance");
2173 assert_eq!(
2174 last_seen_height.load(std::sync::atomic::Ordering::Acquire),
2175 100
2176 );
2177
2178 super::verify_metadata_height(
2179 &ResponseMetadata {
2180 height: 98,
2181 ..Default::default()
2182 },
2183 1,
2184 Arc::clone(&last_seen_height),
2185 None,
2186 )
2187 .expect_err("a second rollback step must be compared with the high-water mark");
2188 assert_eq!(
2189 last_seen_height.load(std::sync::atomic::Ordering::Acquire),
2190 100
2191 );
2192
2193 super::verify_metadata_height(
2194 &ResponseMetadata {
2195 height: 101,
2196 ..Default::default()
2197 },
2198 1,
2199 Arc::clone(&last_seen_height),
2200 None,
2201 )
2202 .expect("a newer height should advance the high-water mark");
2203 assert_eq!(
2204 last_seen_height.load(std::sync::atomic::Ordering::Acquire),
2205 101
2206 );
2207 }
2208
2209 #[test]
2210 fn cloned_sdk_verify_metadata_height() {
2211 let sdk1 = SdkBuilder::new_mock()
2212 .build()
2213 .expect("mock Sdk should be created");
2214
2215 let metadata = ResponseMetadata {
2217 height: 1,
2218 ..Default::default()
2219 };
2220
2221 let request = GetIdentityRequest::default();
2223 sdk1.verify_response_metadata(request.method_name(), &metadata)
2224 .expect("metadata should be valid");
2225
2226 assert_eq!(
2227 sdk1.metadata_last_seen_height
2228 .load(std::sync::atomic::Ordering::Relaxed),
2229 metadata.height,
2230 "initial height"
2231 );
2232
2233 let sdk2 = sdk1.clone();
2235 let sdk3 = sdk1.clone();
2236
2237 let metadata = ResponseMetadata {
2239 height: 2,
2240 ..Default::default()
2241 };
2242 let request = GetIdentityRequest::default();
2244 sdk2.verify_response_metadata(request.method_name(), &metadata)
2245 .expect("metadata should be valid");
2246
2247 assert_eq!(
2248 sdk1.metadata_last_seen_height
2249 .load(std::sync::atomic::Ordering::Relaxed),
2250 metadata.height,
2251 "first sdk should see height from second sdk"
2252 );
2253 assert_eq!(
2254 sdk3.metadata_last_seen_height
2255 .load(std::sync::atomic::Ordering::Relaxed),
2256 metadata.height,
2257 "third sdk should see height from second sdk"
2258 );
2259
2260 let metadata = ResponseMetadata {
2262 height: 3,
2263 ..Default::default()
2264 };
2265 let request = GetIdentityRequest::default();
2267 sdk3.verify_response_metadata(request.method_name(), &metadata)
2268 .expect("metadata should be valid");
2269
2270 assert_eq!(
2271 sdk1.metadata_last_seen_height
2272 .load(std::sync::atomic::Ordering::Relaxed),
2273 metadata.height,
2274 "first sdk should see height from third sdk"
2275 );
2276
2277 assert_eq!(
2278 sdk2.metadata_last_seen_height
2279 .load(std::sync::atomic::Ordering::Relaxed),
2280 metadata.height,
2281 "second sdk should see height from third sdk"
2282 );
2283
2284 let metadata = ResponseMetadata {
2286 height: 1,
2287 ..Default::default()
2288 };
2289
2290 let request = GetIdentityRequest::default();
2291 sdk1.verify_response_metadata(request.method_name(), &metadata)
2292 .expect_err("metadata should be invalid");
2293 }
2294
2295 fn mock_sdk_with_auto_detect(starting_version: u32) -> super::Sdk {
2298 use std::sync::atomic::Ordering;
2299
2300 let sdk = SdkBuilder::new_mock()
2301 .build()
2302 .expect("mock Sdk should be created");
2303 sdk.protocol_version
2304 .store(starting_version, Ordering::Relaxed);
2305 sdk
2306 }
2307
2308 #[test]
2309 fn test_protocol_version_info_tracks_observation_without_changing_pinned_version() {
2310 let version = super::PlatformVersion::latest();
2311 let sdk = SdkBuilder::new_mock()
2312 .with_version(version)
2313 .build()
2314 .unwrap();
2315 let clone = sdk.clone();
2316 let initial = sdk.protocol_version_info();
2317 assert_eq!(initial.verified_version, None);
2318 assert!(!initial.newer_protocol_available);
2319 sdk.record_verified_protocol_version(version.protocol_version);
2320 assert!(!clone.protocol_version_info().newer_protocol_available);
2321 sdk.record_verified_protocol_version(version.protocol_version + 1);
2322 let info = clone.protocol_version_info();
2323 assert_eq!(info.verified_version, Some(version.protocol_version + 1));
2324 assert_eq!(info.effective_version, version.protocol_version);
2325 assert_eq!(info.maximum_supported_version, version.protocol_version);
2326 assert!(info.newer_protocol_available);
2327 sdk.record_verified_protocol_version(version.protocol_version);
2328 assert_eq!(sdk.protocol_version_info(), info);
2329 }
2330
2331 #[test]
2332 fn test_version_update_from_metadata() {
2333 let sdk = mock_sdk_with_auto_detect(1);
2334
2335 assert_eq!(sdk.protocol_version_number(), 1);
2336
2337 let metadata = ResponseMetadata {
2338 protocol_version: 2,
2339 height: 1,
2340 ..Default::default()
2341 };
2342
2343 sdk.verify_response_metadata_as_of("test", &metadata, None)
2344 .expect("metadata should be valid");
2345
2346 assert_eq!(sdk.protocol_version_number(), 2);
2347 assert_eq!(sdk.version().protocol_version, 2);
2348 }
2349
2350 #[test]
2351 fn test_unknown_version_ignored() {
2352 use dpp::version::PlatformVersion;
2353
2354 let sdk = mock_sdk_with_auto_detect(PlatformVersion::latest().protocol_version);
2355 let original_version = sdk.protocol_version_number();
2356
2357 let metadata = ResponseMetadata {
2358 protocol_version: 999,
2359 height: 1,
2360 ..Default::default()
2361 };
2362
2363 sdk.verify_response_metadata_as_of("test", &metadata, None)
2364 .expect("metadata should be valid");
2365
2366 assert_eq!(sdk.protocol_version_number(), original_version);
2367 assert_eq!(sdk.version().protocol_version, original_version);
2368 }
2369
2370 #[test]
2371 fn test_version_shared_between_clones() {
2372 let sdk = mock_sdk_with_auto_detect(1);
2373
2374 let clone = sdk.clone();
2375
2376 let metadata = ResponseMetadata {
2377 protocol_version: 2,
2378 height: 1,
2379 ..Default::default()
2380 };
2381
2382 clone
2383 .verify_response_metadata_as_of("test", &metadata, None)
2384 .expect("metadata should be valid");
2385
2386 assert_eq!(
2387 sdk.protocol_version_number(),
2388 2,
2389 "original should see update from clone"
2390 );
2391 }
2392
2393 #[test]
2394 fn test_version_downgrade_ignored() {
2395 let sdk = mock_sdk_with_auto_detect(2);
2396
2397 assert_eq!(sdk.protocol_version_number(), 2);
2398
2399 let metadata = ResponseMetadata {
2400 protocol_version: 1,
2401 height: 1,
2402 ..Default::default()
2403 };
2404
2405 sdk.verify_response_metadata_as_of("test", &metadata, None)
2406 .expect("metadata should be valid");
2407
2408 assert_eq!(sdk.protocol_version_number(), 2);
2409 }
2410
2411 #[test]
2412 fn test_version_zero_ignored() {
2413 use dpp::version::PlatformVersion;
2414
2415 let sdk = mock_sdk_with_auto_detect(PlatformVersion::latest().protocol_version);
2416 let original_version = sdk.protocol_version_number();
2417
2418 let metadata = ResponseMetadata {
2419 protocol_version: 0,
2420 height: 1,
2421 ..Default::default()
2422 };
2423
2424 sdk.verify_response_metadata_as_of("test", &metadata, None)
2425 .expect("metadata should be valid");
2426
2427 assert_eq!(sdk.protocol_version_number(), original_version);
2428 }
2429
2430 #[test]
2431 fn test_concurrent_updates_converge_to_highest() {
2432 use std::thread;
2433
2434 let sdk = mock_sdk_with_auto_detect(1);
2435
2436 assert_eq!(sdk.protocol_version_number(), 1);
2437
2438 let mut handles = Vec::new();
2439 for version in [2u32, 3, 2, 3, 2, 3] {
2441 let sdk_clone = sdk.clone();
2442 handles.push(thread::spawn(move || {
2443 let metadata = ResponseMetadata {
2444 protocol_version: version,
2445 height: 1,
2446 ..Default::default()
2447 };
2448 sdk_clone
2449 .verify_response_metadata_as_of("test", &metadata, None)
2450 .expect("metadata should be valid");
2451 }));
2452 }
2453
2454 for h in handles {
2455 h.join().expect("thread should not panic");
2456 }
2457
2458 assert_eq!(
2460 sdk.protocol_version_number(),
2461 3,
2462 "concurrent updates must converge to highest version"
2463 );
2464 }
2465
2466 #[test]
2470 fn test_explicit_version_disables_auto_detect() {
2471 use dpp::version::PlatformVersion;
2472
2473 let pinned = PlatformVersion::get(min_protocol_version(Network::Mainnet))
2476 .expect("mainnet-floor PV exists");
2477 let sdk = SdkBuilder::new_mock()
2478 .with_version(pinned)
2479 .build()
2480 .expect("mock Sdk should be created");
2481
2482 assert_eq!(sdk.protocol_version_number(), pinned.protocol_version);
2483 assert!(sdk.version_pinned);
2484
2485 let metadata = ResponseMetadata {
2487 protocol_version: dpp::version::v14::PROTOCOL_VERSION_14,
2488 height: 1,
2489 ..Default::default()
2490 };
2491
2492 sdk.verify_response_metadata_as_of("test", &metadata, None)
2493 .expect("metadata should be valid");
2494
2495 assert_eq!(
2496 sdk.protocol_version_number(),
2497 pinned.protocol_version,
2498 "pinned version must not be auto-updated"
2499 );
2500 }
2501
2502 #[test]
2503 fn test_with_initial_version_seeds_to_older_network_version() {
2504 use dpp::version::PlatformVersion;
2505
2506 let floor = min_protocol_version(Network::Mainnet);
2510 let initial = PlatformVersion::get(floor).expect("mainnet-floor PV exists");
2511 let sdk = SdkBuilder::new_mock()
2512 .with_initial_version(initial)
2513 .build()
2514 .expect("mock Sdk should be created");
2515
2516 assert_eq!(
2517 sdk.protocol_version_number(),
2518 floor,
2519 "with_initial_version must seed the atomic without pinning"
2520 );
2521 assert_eq!(sdk.version().protocol_version, floor);
2522 assert!(
2523 !sdk.version_pinned,
2524 "with_initial_version must keep auto-detect enabled"
2525 );
2526
2527 let metadata = ResponseMetadata {
2529 protocol_version: floor,
2530 height: 1,
2531 ..Default::default()
2532 };
2533 sdk.verify_response_metadata_as_of("test", &metadata, None)
2534 .expect("metadata should be valid");
2535 assert_eq!(sdk.protocol_version_number(), floor);
2536
2537 let newer = dpp::version::v14::PROTOCOL_VERSION_14;
2539 assert!(newer > floor, "ratchet target must exceed the floor");
2540 let metadata = ResponseMetadata {
2541 protocol_version: newer,
2542 height: 2,
2543 ..Default::default()
2544 };
2545 sdk.verify_response_metadata_as_of("test", &metadata, None)
2546 .expect("metadata should be valid");
2547 assert_eq!(sdk.protocol_version_number(), newer);
2548 }
2549
2550 #[test]
2551 fn test_with_initial_version_after_with_version_restores_auto_detect() {
2552 use dpp::version::PlatformVersion;
2553
2554 let v_latest = PlatformVersion::latest();
2561 let v_old = PlatformVersion::get(min_protocol_version(Network::Mainnet))
2562 .expect("mainnet-floor PV exists");
2563 assert!(
2564 v_old.protocol_version < v_latest.protocol_version,
2565 "v_old must be below latest so the later ratchet is observable"
2566 );
2567
2568 let sdk = SdkBuilder::new_mock()
2569 .with_version(v_latest)
2570 .with_initial_version(v_old)
2571 .build()
2572 .expect("mock Sdk should be created");
2573
2574 assert_eq!(
2575 sdk.protocol_version_number(),
2576 v_old.protocol_version,
2577 "with_initial_version must overwrite the prior with_version seed"
2578 );
2579 assert!(
2580 !sdk.version_pinned,
2581 "with_initial_version must restore auto-detect after with_version disabled it"
2582 );
2583
2584 let metadata = ResponseMetadata {
2586 protocol_version: v_latest.protocol_version,
2587 height: 1,
2588 ..Default::default()
2589 };
2590 sdk.verify_response_metadata_as_of("test", &metadata, None)
2591 .expect("metadata should be valid");
2592 assert_eq!(sdk.protocol_version_number(), v_latest.protocol_version);
2593 }
2594
2595 #[test]
2596 fn test_mock_version_follows_outer_sdk_atomic() {
2597 use dpp::version::PlatformVersion;
2598
2599 let v_old = PlatformVersion::get(min_protocol_version(Network::Mainnet))
2605 .expect("mainnet-floor PV exists");
2606 let v_new = PlatformVersion::latest();
2607 assert!(
2608 v_old.protocol_version < v_new.protocol_version,
2609 "v_old must be below latest so the ratchet is observable"
2610 );
2611
2612 let mut sdk = SdkBuilder::new_mock()
2613 .with_initial_version(v_old)
2614 .build()
2615 .expect("mock Sdk should be created");
2616
2617 assert_eq!(sdk.version().protocol_version, v_old.protocol_version);
2618 {
2619 let mock = sdk.mock();
2620 assert_eq!(
2621 mock.version().protocol_version,
2622 v_old.protocol_version,
2623 "mock version must mirror outer SDK before ratchet"
2624 );
2625 }
2626
2627 let metadata = ResponseMetadata {
2628 protocol_version: v_new.protocol_version,
2629 height: 1,
2630 ..Default::default()
2631 };
2632 sdk.verify_response_metadata_as_of("test", &metadata, None)
2633 .expect("metadata should be valid");
2634
2635 assert_eq!(sdk.version().protocol_version, v_new.protocol_version);
2636 let mock = sdk.mock();
2637 assert_eq!(
2638 mock.version().protocol_version,
2639 v_new.protocol_version,
2640 "mock version must follow outer ratchet"
2641 );
2642 }
2643
2644 #[test]
2645 fn test_default_builder_seeds_initial_protocol_version_floor() {
2646 let sdk = SdkBuilder::new_mock()
2649 .build()
2650 .expect("mock Sdk should be created");
2651
2652 let expected = min_protocol_version(Network::Mainnet);
2653 assert_eq!(
2654 sdk.protocol_version_number(),
2655 expected,
2656 "unpinned mainnet SDK must boot at the mainnet floor, not latest()"
2657 );
2658 assert_eq!(sdk.version().protocol_version, expected);
2659 assert!(
2660 !sdk.version_pinned,
2661 "default SDK must keep auto-detect enabled"
2662 );
2663 }
2664
2665 #[test]
2666 fn test_default_floor_ratchets_up_but_never_down() {
2667 let sdk = SdkBuilder::new_mock()
2668 .build()
2669 .expect("mock Sdk should be created");
2670 let floor = min_protocol_version(Network::Mainnet);
2672 assert_eq!(sdk.protocol_version_number(), floor);
2673
2674 let target = dpp::version::v14::PROTOCOL_VERSION_14;
2677 assert!(
2678 target > floor,
2679 "ratchet test target must exceed the floor; bump it if the floor reaches v14"
2680 );
2681 sdk.maybe_update_protocol_version(target);
2682 assert_eq!(
2683 sdk.protocol_version_number(),
2684 target,
2685 "auto-detect must ratchet upward from the floor"
2686 );
2687
2688 sdk.maybe_update_protocol_version(floor - 1);
2690 assert_eq!(
2691 sdk.protocol_version_number(),
2692 target,
2693 "ratchet must never downgrade below the highest observed version"
2694 );
2695 }
2696
2697 #[test]
2708 fn test_ratchet_rejects_unknown_and_non_upward_versions() {
2709 let sdk = SdkBuilder::new_mock()
2710 .build()
2711 .expect("mock Sdk should be created");
2712 let floor = min_protocol_version(Network::Mainnet);
2714 assert_eq!(sdk.protocol_version_number(), floor);
2715
2716 sdk.maybe_update_protocol_version(dpp::version::LATEST_VERSION + 1);
2718 assert_eq!(
2719 sdk.protocol_version_number(),
2720 floor,
2721 "unknown protocol version must not move the stored version"
2722 );
2723
2724 sdk.maybe_update_protocol_version(0);
2726 assert_eq!(
2727 sdk.protocol_version_number(),
2728 floor,
2729 "zero protocol version must be ignored"
2730 );
2731
2732 sdk.maybe_update_protocol_version(floor);
2734 assert_eq!(sdk.protocol_version_number(), floor);
2735
2736 sdk.maybe_update_protocol_version(floor - 1);
2738 assert_eq!(
2739 sdk.protocol_version_number(),
2740 floor,
2741 "lower known version must not downgrade the stored version"
2742 );
2743 }
2744
2745 #[test]
2748 fn test_explicit_pin_below_floor_is_preserved() {
2749 use dpp::version::PlatformVersion;
2750
2751 let floor = min_protocol_version(Network::Mainnet);
2752 let below = floor - 1;
2753 let pinned = PlatformVersion::get(below).expect("sub-floor PV exists");
2754 let sdk = SdkBuilder::new_mock()
2755 .with_version(pinned)
2756 .build()
2757 .expect("mock Sdk should be created");
2758
2759 assert_eq!(
2760 sdk.protocol_version_number(),
2761 below,
2762 "a pin below the floor must be preserved"
2763 );
2764 assert!(sdk.version_pinned);
2766 }
2767
2768 #[test]
2776 fn test_testnet_default_builder_boots_at_per_network_floor() {
2777 let sdk = SdkBuilder::new_mock()
2778 .with_network(Network::Testnet)
2779 .build()
2780 .expect("mock Sdk should be created");
2781
2782 assert_eq!(
2783 sdk.protocol_version_number(),
2784 min_protocol_version(Network::Testnet),
2785 "testnet seeds directly at its per-network floor"
2786 );
2787 assert!(!sdk.version_pinned);
2788 }
2789
2790 #[test]
2794 fn test_per_network_floors() {
2795 assert_eq!(
2796 min_protocol_version(Network::Devnet),
2797 dpp::version::v14::PROTOCOL_VERSION_14,
2798 "devnet floor must be the current development version"
2799 );
2800 for network in [Network::Mainnet, Network::Testnet, Network::Regtest] {
2801 assert_eq!(
2802 min_protocol_version(network),
2803 dpp::version::v13::PROTOCOL_VERSION_13,
2804 "{network} floor must be the lowest version the network runs"
2805 );
2806 }
2807 let sdk = SdkBuilder::new_mock()
2808 .with_network(Network::Devnet)
2809 .build()
2810 .expect("mock Sdk should be created");
2811 assert_eq!(
2812 sdk.protocol_version_number(),
2813 min_protocol_version(Network::Devnet)
2814 );
2815 assert!(!sdk.version_pinned);
2816 }
2817
2818 #[test]
2821 fn test_protocol_version_observer_fires_only_on_upward_ratchet() {
2822 use dpp::version::PlatformVersion;
2823 use std::sync::Mutex;
2824 let seen: Arc<Mutex<Vec<u32>>> = Arc::new(Mutex::new(Vec::new()));
2825 let observer = {
2826 let seen = Arc::clone(&seen);
2827 Arc::new(move |version: u32| {
2828 seen.lock().expect("observer log lock").push(version);
2829 })
2830 };
2831 let sdk = SdkBuilder::new_mock()
2832 .with_protocol_version_observer(observer)
2833 .build()
2834 .expect("mock Sdk should be created");
2835 let floor = min_protocol_version(Network::Mainnet);
2836 let target = dpp::version::v14::PROTOCOL_VERSION_14;
2837 assert!(target > floor, "ratchet target must exceed the floor");
2838
2839 sdk.maybe_update_protocol_version(floor);
2840 sdk.maybe_update_protocol_version(floor - 1);
2841 sdk.maybe_update_protocol_version(0);
2842 sdk.maybe_update_protocol_version(dpp::version::LATEST_VERSION + 1);
2843 assert!(
2844 seen.lock().expect("observer log lock").is_empty(),
2845 "rejected inputs must not notify the observer"
2846 );
2847
2848 sdk.maybe_update_protocol_version(target);
2849 sdk.maybe_update_protocol_version(target);
2850 assert_eq!(
2851 *seen.lock().expect("observer log lock"),
2852 vec![target],
2853 "one upward step must notify exactly once"
2854 );
2855
2856 let clone = sdk.clone();
2858 assert!(clone.protocol_version_observer.is_some());
2859 let pinned = SdkBuilder::new_mock()
2860 .with_version(PlatformVersion::get(floor).expect("floor PV exists"))
2861 .with_protocol_version_observer(Arc::new(|_| panic!("pinned SDK must not ratchet")))
2862 .build()
2863 .expect("mock Sdk should be created");
2864 pinned.maybe_update_protocol_version(target);
2865 }
2866
2867 #[test_matrix([90,91,100,109,110], 100, 10, false; "valid time")]
2868 #[test_matrix([0,89,111], 100, 10, true; "invalid time")]
2869 #[test_matrix([0,100], [0,100], 100, false; "zero time")]
2870 #[test_matrix([99,101], 100, 0, true; "zero tolerance")]
2871 fn test_verify_metadata_time(
2872 received_time: u64,
2873 now_time: u64,
2874 tolerance: u64,
2875 expect_err: bool,
2876 ) {
2877 let metadata = ResponseMetadata {
2878 time_ms: received_time,
2879 ..Default::default()
2880 };
2881
2882 let result = super::verify_metadata_time(&metadata, now_time, tolerance);
2883
2884 assert_eq!(result.is_err(), expect_err);
2885 }
2886
2887 pub(crate) async fn expect_epoch_refresh(sdk: &mut super::Sdk) {
2897 use crate::platform::types::epoch::EpochQuery;
2898 use crate::platform::LimitQuery;
2899 use dpp::block::extended_epoch_info::{v0::ExtendedEpochInfoV0, ExtendedEpochInfo};
2900 use drive_proof_verifier::types::ExtendedEpochInfos;
2901
2902 let probe_query = LimitQuery {
2908 query: EpochQuery::genesis(),
2909 limit: Some(1),
2910 start_info: None,
2911 };
2912 let confirmation_query = LimitQuery {
2913 query: EpochQuery::ascending_from(0),
2914 limit: Some(2),
2915 start_info: None,
2916 };
2917
2918 let epoch = ExtendedEpochInfo::from(ExtendedEpochInfoV0 {
2919 index: 0,
2920 first_block_time: 0,
2921 first_block_height: 0,
2922 first_core_block_height: 0,
2923 fee_multiplier_permille: 0,
2924 protocol_version: dpp::version::LATEST_VERSION,
2925 });
2926
2927 sdk.mock()
2928 .expect_fetch::<ExtendedEpochInfo, _>(probe_query, Some(epoch.clone()))
2929 .await
2930 .expect("register epoch probe expectation");
2931 sdk.mock()
2932 .expect_fetch_many::<_, ExtendedEpochInfo, _, ExtendedEpochInfos>(
2933 confirmation_query,
2934 Some(ExtendedEpochInfos::from_iter([(0, Some(epoch))])),
2935 )
2936 .await
2937 .expect("register epoch refresh expectation");
2938 }
2939
2940 #[tokio::test]
2946 async fn test_refresh_ratchets_up_via_proven_query() {
2947 let mut sdk = mock_sdk_with_auto_detect(super::min_protocol_version(Network::Mainnet));
2948 assert_eq!(
2949 sdk.protocol_version_number(),
2950 super::min_protocol_version(Network::Mainnet)
2951 );
2952
2953 expect_epoch_refresh(&mut sdk).await;
2954
2955 let resulting = sdk
2956 .refresh_protocol_version()
2957 .await
2958 .expect("refresh should succeed");
2959
2960 assert_eq!(
2961 resulting,
2962 dpp::version::LATEST_VERSION,
2963 "returned version must reflect the ratchet to the network's latest"
2964 );
2965 assert_eq!(sdk.protocol_version_number(), dpp::version::LATEST_VERSION);
2966 assert_eq!(sdk.version().protocol_version, dpp::version::LATEST_VERSION);
2967 }
2968
2969 #[tokio::test]
2974 async fn test_refresh_leaves_pinned_sdk_unchanged() {
2975 use dpp::version::PlatformVersion;
2976
2977 let pinned = PlatformVersion::get(min_protocol_version(Network::Mainnet))
2979 .expect("mainnet-floor PV exists");
2980 let sdk = SdkBuilder::new_mock()
2981 .with_version(pinned)
2982 .build()
2983 .expect("mock Sdk should be created");
2984 assert_eq!(sdk.protocol_version_number(), pinned.protocol_version);
2985 assert!(sdk.version_pinned);
2986
2987 let resulting = sdk
2990 .refresh_protocol_version()
2991 .await
2992 .expect("pinned refresh is a no-op and must not error");
2993
2994 assert_eq!(
2995 resulting, pinned.protocol_version,
2996 "pinned version must not move"
2997 );
2998 assert_eq!(sdk.protocol_version_number(), pinned.protocol_version);
2999 }
3000
3001 #[tokio::test]
3006 async fn test_refresh_query_unavailable_keeps_current_version() {
3007 let starting = min_protocol_version(Network::Mainnet);
3008 let sdk = mock_sdk_with_auto_detect(starting);
3009 assert_eq!(sdk.protocol_version_number(), starting);
3010
3011 let resulting = sdk
3012 .refresh_protocol_version()
3013 .await
3014 .expect("refresh is best-effort and must not error when the query fails");
3015
3016 assert_eq!(
3017 resulting, starting,
3018 "a failed refresh must leave the stored version untouched (no fallback)"
3019 );
3020 assert_eq!(sdk.protocol_version_number(), starting);
3021 }
3022}