1use crate::platform::encrypted_for::EncryptedForError;
3use dapi_grpc::platform::v0::StateTransitionBroadcastError as StateTransitionBroadcastErrorProto;
4use dapi_grpc::tonic::Code;
5pub use dash_context_provider::ContextProviderError;
6use dpp::block::block_info::BlockInfo;
7use dpp::block::epoch::EpochIndex;
8use dpp::consensus::basic::state_transition::{
9 OutputBelowMinimumError, StateTransitionMaxSizeExceededError, TransitionNoInputsError,
10 TransitionNoOutputsError,
11};
12use dpp::consensus::state::address_funds::{AddressDoesNotExistError, AddressNotEnoughFundsError};
13use dpp::consensus::ConsensusError;
14use dpp::serialization::PlatformDeserializableUntrusted;
15use dpp::validation::SimpleConsensusValidationResult;
16use dpp::version::PlatformVersionError;
17use dpp::{dashcore_rpc, ProtocolError};
18use rs_dapi_client::transport::TransportError;
19use rs_dapi_client::{CanRetry, DapiClientError, ExecutionError};
20use std::fmt::Debug;
21use std::time::Duration;
22
23#[allow(clippy::large_enum_variant)]
26#[derive(Debug, thiserror::Error)]
27pub enum Error {
28 #[error("SDK misconfigured: {0}")]
30 Config(String),
31 #[error("invalid request: {0}")]
40 InvalidRequest(String),
41 #[error("Drive error: {0}")]
43 Drive(#[from] drive::error::Error),
44 #[error("Drive error with associated proof: {0}")]
46 DriveProofError(drive::error::proof::ProofError, Vec<u8>, BlockInfo),
47 #[error("Protocol error: {0}")]
49 Protocol(#[from] ProtocolError),
50 #[error("Proof verification error: {0}")]
52 Proof(#[source] drive_proof_verifier::Error),
53 #[error("Invalid Proved Response error: {0}")]
55 InvalidProvedResponse(String),
56 #[error("proof authenticates the transition's affected state only, not its execution: {0}")]
61 ExecutionNotProved(String),
62 #[error("Dapi client error: {0}")]
64 DapiClientError(rs_dapi_client::DapiClientError),
65 #[cfg(feature = "mocks")]
66 #[error("Dapi mocks error: {0}")]
68 DapiMocksError(#[from] rs_dapi_client::mock::MockError),
69 #[error("Dash core error: {0}")]
71 CoreError(#[from] dpp::dashcore::Error),
72 #[error("Dash core error: {0}")]
74 MerkleBlockError(#[from] dpp::dashcore::merkle_tree::MerkleBlockError),
75 #[error("Core client error: {0}")]
77 CoreClientError(#[from] dashcore_rpc::Error),
78 #[error("Required {0} not found: {1}")]
80 MissingDependency(String, String),
81 #[error("Total credits in Platform are not found; it should never happen")]
83 TotalCreditsNotFound,
84 #[error("No epoch found on Platform; it should never happen")]
86 EpochNotFound,
87 #[error("SDK operation timeout {} secs reached: {}", .0.as_secs(), .1)]
89 TimeoutReached(Duration, String),
90
91 #[error("Object already exists: {0}")]
93 AlreadyExists(String),
94 #[error("Invalid credit transfer: {0}")]
96 InvalidCreditTransfer(String),
97 #[error("Identity nonce overflow: nonce has reached the maximum value ({0})")]
100 NonceOverflow(u64),
101 #[error("Identity nonce not found on platform: {0}")]
111 IdentityNonceNotFound(String),
112
113 #[error("Drive internal error: {0}")]
119 DriveInternalError(String),
120
121 #[error("SDK error: {0}")]
124 Generic(String),
125
126 #[error("Context provider error: {0}")]
128 ContextProviderError(#[from] ContextProviderError),
129
130 #[error("Operation cancelled: {0}")]
132 Cancelled(String),
133
134 #[error(transparent)]
136 StaleNode(#[from] StaleNodeError),
137
138 #[error(transparent)]
140 StateTransitionBroadcastError(#[from] StateTransitionBroadcastError),
141
142 #[error("no available addresses to retry, last error: {0}")]
145 NoAvailableAddressesToRetry(Box<Error>),
146
147 #[error(transparent)]
149 EncryptedFor(#[from] EncryptedForError),
150
151 #[error(
159 "{method} needs Platform protocol version {required}; {}",
160 describe_proven_protocol_version(*.network, *.pinned, .cause.is_some())
161 )]
162 ProtocolVersionNotActive {
163 method: &'static str,
165 required: u32,
167 network: u32,
170 pinned: bool,
174 #[source]
177 cause: Option<Box<Error>>,
178 },
179}
180
181fn describe_proven_protocol_version(
183 network: u32,
184 pinned: bool,
185 confirmation_failed: bool,
186) -> String {
187 if pinned {
188 "this SDK is pinned to an earlier protocol version".to_string()
189 } else if confirmation_failed {
190 "the network's protocol version could not be confirmed".to_string()
191 } else if network == 0 {
192 "the network's protocol version is not verified yet".to_string()
193 } else {
194 format!("the network is proven at protocol version {network}")
195 }
196}
197
198impl Error {
199 pub fn is_transient_protocol_version_refusal(&self) -> bool {
209 match self {
210 Self::ProtocolVersionNotActive {
211 cause: Some(cause), ..
212 } => !cause.cannot_verify_answers(),
213 _ => false,
214 }
215 }
216
217 fn cannot_verify_answers(&self) -> bool {
220 match self {
221 Self::NoAvailableAddressesToRetry(last_error) => last_error.cannot_verify_answers(),
222 Self::Proof(drive_proof_verifier::Error::ContextProviderNotSet) => true,
223 Self::Proof(drive_proof_verifier::Error::QuorumKeyUnavailable { error, .. })
224 | Self::ContextProviderError(error) => {
225 matches!(error, ContextProviderError::Config(_))
226 }
227 _ => false,
228 }
229 }
230
231 pub fn is_quorum_source_unavailable(&self) -> bool {
235 match self {
236 Self::ContextProviderError(ContextProviderError::QuorumSourceUnavailable(_)) => true,
237 Self::NoAvailableAddressesToRetry(last_error) => {
238 last_error.is_quorum_source_unavailable()
239 }
240 _ => false,
241 }
242 }
243
244 pub fn consensus_error(&self) -> Option<&ConsensusError> {
250 match self {
251 Self::StateTransitionBroadcastError(broadcast_err) => broadcast_err.cause.as_ref(),
252 Self::Protocol(ProtocolError::ConsensusError(ce)) => Some(ce.as_ref()),
253 Self::NoAvailableAddressesToRetry(inner) => inner.consensus_error(),
254 _ => None,
255 }
256 }
257
258 pub(crate) fn spares_responder(&self) -> bool {
261 matches!(
262 self,
263 Self::StaleNode(StaleNodeError::ProtocolVersion { .. })
264 )
265 }
266
267 pub(crate) fn into_exhausted_error(self) -> Self {
274 match self {
275 Self::StaleNode(StaleNodeError::ProtocolVersion {
276 received_protocol_version: signed,
277 expected_protocol_version: sdk,
278 }) => Self::Config(format!(
279 "the last answer names protocol version {signed}, below the SDK's protocol \
280 version {sdk}, and no verified answer has yet shown the network running \
281 {sdk}; if the SDK was set ahead of its network, let it follow the network's \
282 protocol version instead, or seed it with the network's version \
283 (with_initial_version)"
284 )),
285 error => error,
286 }
287 }
288}
289
290impl From<drive_proof_verifier::Error> for Error {
295 fn from(value: drive_proof_verifier::Error) -> Self {
296 match value {
297 drive_proof_verifier::Error::QuorumKeyUnavailable {
298 quorum_type,
299 quorum_hash,
300 core_chain_locked_height,
301 error: ContextProviderError::QuorumSourceUnavailable(reason),
302 } => Self::ContextProviderError(ContextProviderError::QuorumSourceUnavailable(format!(
303 "quorum {quorum_type}:{} at Core chain-locked height {core_chain_locked_height}: {reason}",
304 hex::encode(quorum_hash)
305 ))),
306 drive_proof_verifier::Error::UnusableVerifiedResult { reason } => {
308 Self::InvalidRequest(reason)
309 }
310 error => Self::Proof(error),
311 }
312 }
313}
314
315impl From<dash_platform_queries::Error> for Error {
316 fn from(value: dash_platform_queries::Error) -> Self {
317 match value {
318 dash_platform_queries::Error::Config(msg) => Self::Config(msg),
319 dash_platform_queries::Error::Drive(e) => Self::Drive(e),
320 dash_platform_queries::Error::Protocol(e) => Self::Protocol(e),
321 }
322 }
323}
324
325#[derive(Debug, thiserror::Error)]
327#[error("state transition broadcast error: {message}")]
328pub struct StateTransitionBroadcastError {
329 pub code: u32,
331 pub message: String,
333 pub cause: Option<ConsensusError>,
335}
336
337impl TryFrom<StateTransitionBroadcastErrorProto> for StateTransitionBroadcastError {
338 type Error = Error;
339
340 fn try_from(value: StateTransitionBroadcastErrorProto) -> Result<Self, Self::Error> {
341 let cause = if !value.data.is_empty() {
342 let consensus_error = ConsensusError::deserialize_from_bytes_untrusted(&value.data)
343 .map_err(|e| {
344 tracing::debug!("Failed to deserialize consensus error: {}", e);
345
346 Error::Protocol(e)
347 })?;
348
349 Some(consensus_error)
350 } else {
351 None
352 };
353
354 Ok(Self {
355 code: value.code,
356 message: value.message,
357 cause,
358 })
359 }
360}
361
362impl From<DapiClientError> for Error {
364 fn from(value: DapiClientError) -> Self {
365 if let DapiClientError::Transport(TransportError::Grpc(status)) = &value {
366 if let Some(consensus_error_value) = status
368 .metadata()
369 .get_bin("dash-serialized-consensus-error-bin")
370 {
371 return consensus_error_value
372 .to_bytes()
373 .map(|bytes| {
374 ConsensusError::deserialize_from_bytes_untrusted(&bytes)
375 .map(|consensus_error| {
376 Self::Protocol(ProtocolError::ConsensusError(Box::new(
377 consensus_error,
378 )))
379 })
380 .unwrap_or_else(|e| {
381 tracing::debug!("Failed to deserialize consensus error: {}", e);
382 Self::Protocol(e)
383 })
384 })
385 .unwrap_or_else(|e| {
386 tracing::debug!("Failed to deserialize consensus error: {}", e);
387 Self::Generic(format!("Invalid consensus error encoding: {e}"))
389 });
390 }
391 if status.code() == Code::Internal {
393 if let Some(drive_error_value) = status.metadata().get_bin("drive-error-data-bin") {
394 match drive_error_value.to_bytes() {
395 Ok(bytes) => {
396 if let Some(message) = extract_drive_error_message(&bytes) {
397 return Self::DriveInternalError(message);
398 }
399 }
400 Err(e) => {
401 tracing::debug!(
402 "Failed to decode drive-error-data-bin metadata: {}",
403 e
404 );
405 }
406 }
407 }
408 }
409
410 if status.code() == Code::AlreadyExists {
412 return Self::AlreadyExists(status.message().to_string());
413 }
414 }
415
416 Self::DapiClientError(value)
418 }
419}
420
421const MAX_CBOR_INPUT_SIZE: usize = 65_536;
429
430fn decode_cbor_value(bytes: &[u8]) -> Option<ciborium::Value> {
434 ciborium::from_reader::<ciborium::Value, _>(bytes).ok()
435}
436
437fn extract_drive_error_message(bytes: &[u8]) -> Option<String> {
447 if bytes.len() > MAX_CBOR_INPUT_SIZE {
448 tracing::debug!(
449 len = bytes.len(),
450 max = MAX_CBOR_INPUT_SIZE,
451 "drive-error-data-bin exceeds size cap; refusing to decode"
452 );
453 return None;
454 }
455 let value = decode_cbor_value(bytes)?;
456 let map = value.as_map()?;
457 for (key, val) in map {
458 if key.as_text() == Some("message") {
459 if let Some(msg) = val.as_text() {
460 if !msg.is_empty() {
461 return Some(msg.to_string());
462 }
463 }
464 }
465 }
466 None
467}
468
469impl From<PlatformVersionError> for Error {
470 fn from(value: PlatformVersionError) -> Self {
471 Self::Protocol(value.into())
472 }
473}
474
475impl From<ConsensusError> for Error {
476 fn from(value: ConsensusError) -> Self {
477 Self::Protocol(ProtocolError::ConsensusError(Box::new(value)))
478 }
479}
480
481impl From<StateTransitionMaxSizeExceededError> for Error {
482 fn from(value: StateTransitionMaxSizeExceededError) -> Self {
483 Self::Protocol(ProtocolError::ConsensusError(Box::new(value.into())))
484 }
485}
486
487impl From<TransitionNoInputsError> for Error {
488 fn from(value: TransitionNoInputsError) -> Self {
489 Self::Protocol(ProtocolError::ConsensusError(Box::new(value.into())))
490 }
491}
492
493impl From<TransitionNoOutputsError> for Error {
494 fn from(value: TransitionNoOutputsError) -> Self {
495 Self::Protocol(ProtocolError::ConsensusError(Box::new(value.into())))
496 }
497}
498
499impl From<OutputBelowMinimumError> for Error {
500 fn from(value: OutputBelowMinimumError) -> Self {
501 Self::Protocol(ProtocolError::ConsensusError(Box::new(value.into())))
502 }
503}
504
505impl From<SimpleConsensusValidationResult> for Error {
506 fn from(value: SimpleConsensusValidationResult) -> Self {
507 value
508 .errors
509 .into_iter()
510 .next()
511 .map(Error::from)
512 .unwrap_or_else(|| {
513 Error::Protocol(ProtocolError::CorruptedCodeExecution(
514 "state transition structure validation failed without an error".to_string(),
515 ))
516 })
517 }
518}
519
520impl From<AddressDoesNotExistError> for Error {
521 fn from(value: AddressDoesNotExistError) -> Self {
522 Self::Protocol(ProtocolError::ConsensusError(Box::new(value.into())))
523 }
524}
525
526impl From<AddressNotEnoughFundsError> for Error {
527 fn from(value: AddressNotEnoughFundsError) -> Self {
528 Self::Protocol(ProtocolError::ConsensusError(Box::new(value.into())))
529 }
530}
531
532impl<T> From<ExecutionError<T>> for Error
534where
535 ExecutionError<T>: ToString,
536{
537 fn from(value: ExecutionError<T>) -> Self {
538 Self::Generic(value.to_string())
540 }
541}
542
543impl CanRetry for Error {
544 fn can_retry(&self) -> bool {
545 matches!(
546 self,
547 Error::StaleNode(..) | Error::TimeoutReached(_, _) | Error::Proof(_)
548 )
549 }
550
551 fn is_no_available_addresses(&self) -> bool {
552 matches!(
553 self,
554 Error::DapiClientError(DapiClientError::NoAvailableAddresses)
555 | Error::DapiClientError(DapiClientError::NoAvailableAddressesToRetry(_))
556 )
557 }
558}
559
560#[derive(Debug, thiserror::Error)]
562pub enum StaleNodeError {
563 #[error("received height is outdated: expected {expected_height}, received {received_height}, tolerance {tolerance_blocks}; try another server")]
565 Height {
566 expected_height: u64,
568 received_height: u64,
570 tolerance_blocks: u64,
572 },
573 #[error(
575 "received invalid time: expected {expected_timestamp_ms}ms, received {received_timestamp_ms} ms, tolerance {tolerance_ms} ms; try another server"
576 )]
577 Time {
578 expected_timestamp_ms: u64,
580 received_timestamp_ms: u64,
582 tolerance_ms: u64,
584 },
585 #[error("received epoch is outdated: hinted {hinted_epoch}, proven started epoch {proven_epoch}; try another server")]
592 Epoch {
593 hinted_epoch: EpochIndex,
595 proven_epoch: EpochIndex,
597 },
598 #[error("received protocol version {received_protocol_version} is below the SDK's protocol version {expected_protocol_version}; try another server")]
607 ProtocolVersion {
608 expected_protocol_version: u32,
610 received_protocol_version: u32,
612 },
613 #[error("received an answer signed at protocol version {signed} to a request that needs {required}; try another server")]
620 BehindProtocolUpgrade {
621 required: u32,
623 signed: u32,
625 },
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631
632 #[test]
635 fn should_identify_only_typed_quorum_source_failures_through_retry_exhaustion() {
636 let mut source = Error::ContextProviderError(
637 ContextProviderError::QuorumSourceUnavailable("offline".to_string()),
638 );
639 assert!(source.is_quorum_source_unavailable());
640 for _ in 0..2 {
641 source = Error::NoAvailableAddressesToRetry(Box::new(source));
642 assert!(source.is_quorum_source_unavailable());
643 }
644 for unrelated in [
645 Error::ContextProviderError(ContextProviderError::InvalidQuorum(
646 "quorum source unavailable".to_string(),
647 )),
648 Error::Generic("QuorumSourceUnavailable: quorum source unavailable".to_string()),
649 ] {
650 assert!(!unrelated.is_quorum_source_unavailable());
651 let wrapped = Error::NoAvailableAddressesToRetry(Box::new(unrelated));
652 assert!(!wrapped.is_quorum_source_unavailable());
653 }
654 }
655
656 #[test]
660 fn should_not_retry_a_request_the_network_cannot_serve_yet() {
661 let refused = |network, pinned, cause: Option<Error>| Error::ProtocolVersionNotActive {
662 method: "get_contract_fee_pots",
663 required: 14,
664 network,
665 pinned,
666 cause: cause.map(Box::new),
667 };
668 for (error, reason) in [
669 (refused(13, false, None), "proven at protocol version 13"),
670 (refused(0, false, None), "not verified"),
671 (refused(0, true, None), "pinned"),
672 (refused(13, true, None), "pinned"),
673 (
674 refused(0, false, Some(Error::Generic("offline".to_string()))),
675 "could not be confirmed",
676 ),
677 ] {
678 assert!(!error.can_retry(), "{error}");
679 let message = error.to_string();
680 assert!(
681 message.contains("get_contract_fee_pots")
682 && message.contains("version 14")
683 && message.contains(reason),
684 "{message}"
685 );
686 }
687 assert!(std::error::Error::source(&refused(
688 0,
689 false,
690 Some(Error::Generic("offline".into()))
691 ))
692 .is_some());
693 let behind = Error::StaleNode(StaleNodeError::BehindProtocolUpgrade {
694 required: 14,
695 signed: 13,
696 });
697 assert!(behind.can_retry());
698 assert!(
699 !behind.spares_responder(),
700 "a node behind an upgrade the network has proven is banned"
701 );
702 }
703
704 #[test]
707 fn should_not_retry_a_verified_result_no_node_can_return_as_the_requested_type() {
708 let error = Error::from(drive_proof_verifier::Error::UnusableVerifiedResult {
709 reason: "a single-document fetch matched 2 documents".to_string(),
710 });
711 assert!(
712 matches!(&error, Error::InvalidRequest(reason) if reason.contains("matched 2")),
713 "{error:?}"
714 );
715 assert!(!error.can_retry());
716 }
717
718 #[test]
722 fn should_retry_a_request_error_raised_while_an_answer_is_verified() {
723 let error = Error::from(drive_proof_verifier::Error::RequestError {
724 error: "the time window does not resolve at the answer's block time".to_string(),
725 });
726 assert!(matches!(error, Error::Proof(_)), "{error:?}");
727 assert!(error.can_retry());
728 }
729
730 #[test]
734 fn should_call_a_refusal_transient_only_when_its_cause_can_pass() {
735 let refused = |cause: Option<Error>| Error::ProtocolVersionNotActive {
736 method: "get_contract_fee_pots",
737 required: 14,
738 network: 0,
739 pinned: false,
740 cause: cause.map(Box::new),
741 };
742 let quorum_key = |error| {
743 Error::Proof(drive_proof_verifier::Error::QuorumKeyUnavailable {
744 quorum_type: 106,
745 quorum_hash: [2; 32],
746 core_chain_locked_height: 1,
747 error,
748 })
749 };
750 let transient = [
751 Error::DapiClientError(DapiClientError::NoAvailableAddresses),
752 Error::ContextProviderError(ContextProviderError::QuorumSourceUnavailable(
753 "HTTP 503".to_string(),
754 )),
755 quorum_key(ContextProviderError::InvalidQuorum(
756 "unknown yet".to_string(),
757 )),
758 quorum_key(ContextProviderError::Generic(
760 "Failed to find quorum: HTTP 503".to_string(),
761 )),
762 Error::StaleNode(StaleNodeError::Time {
763 expected_timestamp_ms: 1,
764 received_timestamp_ms: 2,
765 tolerance_ms: 0,
766 }),
767 ];
768 for cause in transient {
769 let message = cause.to_string();
770 assert!(
771 refused(Some(cause)).is_transient_protocol_version_refusal(),
772 "{message}"
773 );
774 }
775 let permanent = [
776 Error::Proof(drive_proof_verifier::Error::ContextProviderNotSet),
777 quorum_key(ContextProviderError::Config(
778 "Non-trusted mode is not supported".to_string(),
779 )),
780 Error::NoAvailableAddressesToRetry(Box::new(quorum_key(ContextProviderError::Config(
781 "no quorum source".to_string(),
782 )))),
783 ];
784 for cause in permanent {
785 let message = cause.to_string();
786 assert!(
787 !refused(Some(cause)).is_transient_protocol_version_refusal(),
788 "{message}"
789 );
790 }
791 assert!(!refused(None).is_transient_protocol_version_refusal());
792 assert!(!Error::Generic("offline".to_string()).is_transient_protocol_version_refusal());
793 }
794
795 mod from_dapi_client_error {
796 use super::*;
797 use assert_matches::assert_matches;
798 use base64::Engine;
799 use dapi_grpc::tonic::metadata::{MetadataMap, MetadataValue};
800 use dpp::consensus::basic::identity::IdentityAssetLockProofLockedTransactionMismatchError;
801 use dpp::consensus::basic::BasicError;
802 use dpp::dashcore::hashes::Hash;
803 use dpp::dashcore::Txid;
804 use dpp::serialization::PlatformSerializableWithPlatformVersion;
805 use dpp::version::PlatformVersion;
806
807 #[test]
808 fn test_already_exists() {
809 let error = DapiClientError::Transport(TransportError::Grpc(
810 dapi_grpc::tonic::Status::new(Code::AlreadyExists, "Object already exists"),
811 ));
812
813 let sdk_error: Error = error.into();
814 assert!(matches!(sdk_error, Error::AlreadyExists(_)));
815 }
816
817 #[test]
818 fn test_consensus_error() {
819 let platform_version = PlatformVersion::latest();
820
821 let consensus_error = ConsensusError::BasicError(
822 BasicError::IdentityAssetLockProofLockedTransactionMismatchError(
823 IdentityAssetLockProofLockedTransactionMismatchError::new(
824 Txid::from_byte_array([0; 32]),
825 Txid::from_byte_array([1; 32]),
826 ),
827 ),
828 );
829
830 let consensus_error_bytes = consensus_error
831 .serialize_to_bytes_with_platform_version(platform_version)
832 .expect("serialize consensus error to bytes");
833
834 let mut metadata = MetadataMap::new();
835 metadata.insert_bin(
836 "dash-serialized-consensus-error-bin",
837 MetadataValue::from_bytes(&consensus_error_bytes),
838 );
839
840 let status =
841 dapi_grpc::tonic::Status::with_metadata(Code::InvalidArgument, "Test", metadata);
842
843 let error = DapiClientError::Transport(TransportError::Grpc(status));
844
845 let sdk_error = Error::from(error);
846
847 assert_matches!(
848 sdk_error,
849 Error::Protocol(ProtocolError::ConsensusError(e)) if matches!(*e, ConsensusError::BasicError(
850 BasicError::IdentityAssetLockProofLockedTransactionMismatchError(_)
851 ))
852 );
853 }
854
855 #[test]
856 fn test_consensus_error_with_fixture() {
857 let consensus_error_bytes = base64::engine::general_purpose::STANDARD.decode("ATUgJOJEYbuHBqyTeApO/ptxQ8IAw8nm9NbGROu1nyE/kqcgDTlFeUG0R4wwVcbZJMFErL+VSn63SUpP49cequ3fsKw=").expect("decode base64");
858 let consensus_error = MetadataValue::from_bytes(&consensus_error_bytes);
859
860 let mut metadata = MetadataMap::new();
861 metadata.insert_bin("dash-serialized-consensus-error-bin", consensus_error);
862
863 let status =
864 dapi_grpc::tonic::Status::with_metadata(Code::InvalidArgument, "Test", metadata);
865
866 let error = DapiClientError::Transport(TransportError::Grpc(status));
867
868 let sdk_error = Error::from(error);
869
870 assert_matches!(
871 sdk_error,
872 Error::Protocol(ProtocolError::ConsensusError(e)) if matches!(*e, ConsensusError::BasicError(
873 BasicError::IdentityAssetLockProofLockedTransactionMismatchError(_)
874 ))
875 );
876 }
877
878 #[test]
879 fn test_drive_error_data_bin_maps_to_drive_internal_error() {
880 let cbor_map = ciborium::Value::Map(vec![
881 (
882 ciborium::Value::Text("code".to_string()),
883 ciborium::Value::Integer(13.into()),
884 ),
885 (
886 ciborium::Value::Text("message".to_string()),
887 ciborium::Value::Text(
888 "storage: identity: a unique key with that hash already exists: \
889 the key already exists in the non unique set [1, 2, 3]"
890 .to_string(),
891 ),
892 ),
893 ]);
894 let mut cbor_bytes = Vec::new();
895 ciborium::into_writer(&cbor_map, &mut cbor_bytes).expect("CBOR serialization");
896
897 let mut metadata = MetadataMap::new();
898 metadata.insert_bin(
899 "drive-error-data-bin",
900 MetadataValue::from_bytes(&cbor_bytes),
901 );
902
903 let status =
904 dapi_grpc::tonic::Status::with_metadata(Code::Internal, "internal", metadata);
905 let error = DapiClientError::Transport(TransportError::Grpc(status));
906
907 let sdk_error = Error::from(error);
908
909 assert_matches!(sdk_error, Error::DriveInternalError(msg) if msg.contains("unique key"));
910 }
911
912 #[test]
913 fn test_internal_error_without_drive_metadata_falls_through() {
914 let status = dapi_grpc::tonic::Status::new(Code::Internal, "Internal error");
915 let error = DapiClientError::Transport(TransportError::Grpc(status));
916
917 let sdk_error = Error::from(error);
918
919 assert_matches!(sdk_error, Error::DapiClientError(_));
920 }
921
922 #[test]
923 fn test_non_internal_code_with_drive_metadata_not_intercepted() {
924 let cbor_map = ciborium::Value::Map(vec![(
925 ciborium::Value::Text("message".to_string()),
926 ciborium::Value::Text("some drive error".to_string()),
927 )]);
928 let mut cbor_bytes = Vec::new();
929 ciborium::into_writer(&cbor_map, &mut cbor_bytes).expect("CBOR serialization");
930
931 let mut metadata = MetadataMap::new();
932 metadata.insert_bin(
933 "drive-error-data-bin",
934 MetadataValue::from_bytes(&cbor_bytes),
935 );
936
937 let status =
938 dapi_grpc::tonic::Status::with_metadata(Code::Unavailable, "unavailable", metadata);
939 let error = DapiClientError::Transport(TransportError::Grpc(status));
940
941 let sdk_error = Error::from(error);
942
943 assert_matches!(sdk_error, Error::DapiClientError(_));
944 }
945
946 #[test]
947 fn test_malformed_cbor_in_drive_error_data_bin_falls_through() {
948 let garbage_bytes = vec![0xFF, 0xFE, 0x00, 0x01, 0x02];
949
950 let mut metadata = MetadataMap::new();
951 metadata.insert_bin(
952 "drive-error-data-bin",
953 MetadataValue::from_bytes(&garbage_bytes),
954 );
955
956 let status =
957 dapi_grpc::tonic::Status::with_metadata(Code::Internal, "internal", metadata);
958 let error = DapiClientError::Transport(TransportError::Grpc(status));
959
960 let sdk_error = Error::from(error);
961
962 assert_matches!(sdk_error, Error::DapiClientError(_));
963 }
964
965 #[test]
970 fn test_deeply_nested_cbor_rejected_without_stack_exhaustion() {
971 let payload = vec![0xA1u8; 60_000];
972 assert!(super::extract_drive_error_message(&payload).is_none());
973 }
974 }
975}