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, TransitionNoInputsError, TransitionNoOutputsError,
10};
11use dpp::consensus::state::address_funds::{AddressDoesNotExistError, AddressNotEnoughFundsError};
12use dpp::consensus::ConsensusError;
13use dpp::serialization::PlatformDeserializableUntrusted;
14use dpp::validation::SimpleConsensusValidationResult;
15use dpp::version::PlatformVersionError;
16use dpp::{dashcore_rpc, ProtocolError};
17use rs_dapi_client::transport::TransportError;
18use rs_dapi_client::{CanRetry, DapiClientError, ExecutionError};
19use std::fmt::Debug;
20use std::time::Duration;
21
22#[allow(clippy::large_enum_variant)]
25#[derive(Debug, thiserror::Error)]
26pub enum Error {
27 #[error("SDK misconfigured: {0}")]
29 Config(String),
30 #[error("Drive error: {0}")]
32 Drive(#[from] drive::error::Error),
33 #[error("Drive error with associated proof: {0}")]
35 DriveProofError(drive::error::proof::ProofError, Vec<u8>, BlockInfo),
36 #[error("Protocol error: {0}")]
38 Protocol(#[from] ProtocolError),
39 #[error("Proof verification error: {0}")]
41 Proof(#[from] drive_proof_verifier::Error),
42 #[error("Invalid Proved Response error: {0}")]
44 InvalidProvedResponse(String),
45 #[error("proof authenticates the transition's affected state only, not its execution: {0}")]
50 ExecutionNotProved(String),
51 #[error("Dapi client error: {0}")]
53 DapiClientError(rs_dapi_client::DapiClientError),
54 #[cfg(feature = "mocks")]
55 #[error("Dapi mocks error: {0}")]
57 DapiMocksError(#[from] rs_dapi_client::mock::MockError),
58 #[error("Dash core error: {0}")]
60 CoreError(#[from] dpp::dashcore::Error),
61 #[error("Dash core error: {0}")]
63 MerkleBlockError(#[from] dpp::dashcore::merkle_tree::MerkleBlockError),
64 #[error("Core client error: {0}")]
66 CoreClientError(#[from] dashcore_rpc::Error),
67 #[error("Required {0} not found: {1}")]
69 MissingDependency(String, String),
70 #[error("Total credits in Platform are not found; it should never happen")]
72 TotalCreditsNotFound,
73 #[error("No epoch found on Platform; it should never happen")]
75 EpochNotFound,
76 #[error("SDK operation timeout {} secs reached: {}", .0.as_secs(), .1)]
78 TimeoutReached(Duration, String),
79
80 #[error("Object already exists: {0}")]
82 AlreadyExists(String),
83 #[error("Invalid credit transfer: {0}")]
85 InvalidCreditTransfer(String),
86 #[error("Identity nonce overflow: nonce has reached the maximum value ({0})")]
89 NonceOverflow(u64),
90 #[error("Identity nonce not found on platform: {0}")]
100 IdentityNonceNotFound(String),
101
102 #[error("Drive internal error: {0}")]
108 DriveInternalError(String),
109
110 #[error("SDK error: {0}")]
113 Generic(String),
114
115 #[error("Context provider error: {0}")]
117 ContextProviderError(#[from] ContextProviderError),
118
119 #[error("Operation cancelled: {0}")]
121 Cancelled(String),
122
123 #[error(transparent)]
125 StaleNode(#[from] StaleNodeError),
126
127 #[error(transparent)]
129 StateTransitionBroadcastError(#[from] StateTransitionBroadcastError),
130
131 #[error("no available addresses to retry, last error: {0}")]
134 NoAvailableAddressesToRetry(Box<Error>),
135
136 #[error(transparent)]
138 EncryptedFor(#[from] EncryptedForError),
139}
140
141impl From<dash_platform_queries::Error> for Error {
142 fn from(value: dash_platform_queries::Error) -> Self {
143 match value {
144 dash_platform_queries::Error::Config(msg) => Self::Config(msg),
145 dash_platform_queries::Error::Drive(e) => Self::Drive(e),
146 dash_platform_queries::Error::Protocol(e) => Self::Protocol(e),
147 }
148 }
149}
150
151#[derive(Debug, thiserror::Error)]
153#[error("state transition broadcast error: {message}")]
154pub struct StateTransitionBroadcastError {
155 pub code: u32,
157 pub message: String,
159 pub cause: Option<ConsensusError>,
161}
162
163impl TryFrom<StateTransitionBroadcastErrorProto> for StateTransitionBroadcastError {
164 type Error = Error;
165
166 fn try_from(value: StateTransitionBroadcastErrorProto) -> Result<Self, Self::Error> {
167 let cause = if !value.data.is_empty() {
168 let consensus_error = ConsensusError::deserialize_from_bytes_untrusted(&value.data)
169 .map_err(|e| {
170 tracing::debug!("Failed to deserialize consensus error: {}", e);
171
172 Error::Protocol(e)
173 })?;
174
175 Some(consensus_error)
176 } else {
177 None
178 };
179
180 Ok(Self {
181 code: value.code,
182 message: value.message,
183 cause,
184 })
185 }
186}
187
188impl From<DapiClientError> for Error {
190 fn from(value: DapiClientError) -> Self {
191 if let DapiClientError::Transport(TransportError::Grpc(status)) = &value {
192 if let Some(consensus_error_value) = status
194 .metadata()
195 .get_bin("dash-serialized-consensus-error-bin")
196 {
197 return consensus_error_value
198 .to_bytes()
199 .map(|bytes| {
200 ConsensusError::deserialize_from_bytes_untrusted(&bytes)
201 .map(|consensus_error| {
202 Self::Protocol(ProtocolError::ConsensusError(Box::new(
203 consensus_error,
204 )))
205 })
206 .unwrap_or_else(|e| {
207 tracing::debug!("Failed to deserialize consensus error: {}", e);
208 Self::Protocol(e)
209 })
210 })
211 .unwrap_or_else(|e| {
212 tracing::debug!("Failed to deserialize consensus error: {}", e);
213 Self::Generic(format!("Invalid consensus error encoding: {e}"))
215 });
216 }
217 if status.code() == Code::Internal {
219 if let Some(drive_error_value) = status.metadata().get_bin("drive-error-data-bin") {
220 match drive_error_value.to_bytes() {
221 Ok(bytes) => {
222 if let Some(message) = extract_drive_error_message(&bytes) {
223 return Self::DriveInternalError(message);
224 }
225 }
226 Err(e) => {
227 tracing::debug!(
228 "Failed to decode drive-error-data-bin metadata: {}",
229 e
230 );
231 }
232 }
233 }
234 }
235
236 if status.code() == Code::AlreadyExists {
238 return Self::AlreadyExists(status.message().to_string());
239 }
240 }
241
242 Self::DapiClientError(value)
244 }
245}
246
247const MAX_CBOR_INPUT_SIZE: usize = 65_536;
255
256fn decode_cbor_value(bytes: &[u8]) -> Option<ciborium::Value> {
260 ciborium::from_reader::<ciborium::Value, _>(bytes).ok()
261}
262
263fn extract_drive_error_message(bytes: &[u8]) -> Option<String> {
273 if bytes.len() > MAX_CBOR_INPUT_SIZE {
274 tracing::debug!(
275 len = bytes.len(),
276 max = MAX_CBOR_INPUT_SIZE,
277 "drive-error-data-bin exceeds size cap; refusing to decode"
278 );
279 return None;
280 }
281 let value = decode_cbor_value(bytes)?;
282 let map = value.as_map()?;
283 for (key, val) in map {
284 if key.as_text() == Some("message") {
285 if let Some(msg) = val.as_text() {
286 if !msg.is_empty() {
287 return Some(msg.to_string());
288 }
289 }
290 }
291 }
292 None
293}
294
295impl From<PlatformVersionError> for Error {
296 fn from(value: PlatformVersionError) -> Self {
297 Self::Protocol(value.into())
298 }
299}
300
301impl From<ConsensusError> for Error {
302 fn from(value: ConsensusError) -> Self {
303 Self::Protocol(ProtocolError::ConsensusError(Box::new(value)))
304 }
305}
306
307impl From<TransitionNoInputsError> for Error {
308 fn from(value: TransitionNoInputsError) -> Self {
309 Self::Protocol(ProtocolError::ConsensusError(Box::new(value.into())))
310 }
311}
312
313impl From<TransitionNoOutputsError> for Error {
314 fn from(value: TransitionNoOutputsError) -> Self {
315 Self::Protocol(ProtocolError::ConsensusError(Box::new(value.into())))
316 }
317}
318
319impl From<OutputBelowMinimumError> for Error {
320 fn from(value: OutputBelowMinimumError) -> Self {
321 Self::Protocol(ProtocolError::ConsensusError(Box::new(value.into())))
322 }
323}
324
325impl From<SimpleConsensusValidationResult> for Error {
326 fn from(value: SimpleConsensusValidationResult) -> Self {
327 value
328 .errors
329 .into_iter()
330 .next()
331 .map(Error::from)
332 .unwrap_or_else(|| {
333 Error::Protocol(ProtocolError::CorruptedCodeExecution(
334 "state transition structure validation failed without an error".to_string(),
335 ))
336 })
337 }
338}
339
340impl From<AddressDoesNotExistError> for Error {
341 fn from(value: AddressDoesNotExistError) -> Self {
342 Self::Protocol(ProtocolError::ConsensusError(Box::new(value.into())))
343 }
344}
345
346impl From<AddressNotEnoughFundsError> for Error {
347 fn from(value: AddressNotEnoughFundsError) -> Self {
348 Self::Protocol(ProtocolError::ConsensusError(Box::new(value.into())))
349 }
350}
351
352impl<T> From<ExecutionError<T>> for Error
354where
355 ExecutionError<T>: ToString,
356{
357 fn from(value: ExecutionError<T>) -> Self {
358 Self::Generic(value.to_string())
360 }
361}
362
363impl CanRetry for Error {
364 fn can_retry(&self) -> bool {
365 matches!(
366 self,
367 Error::StaleNode(..) | Error::TimeoutReached(_, _) | Error::Proof(_)
368 )
369 }
370
371 fn is_no_available_addresses(&self) -> bool {
372 matches!(
373 self,
374 Error::DapiClientError(DapiClientError::NoAvailableAddresses)
375 | Error::DapiClientError(DapiClientError::NoAvailableAddressesToRetry(_))
376 )
377 }
378}
379
380#[derive(Debug, thiserror::Error)]
382pub enum StaleNodeError {
383 #[error("received height is outdated: expected {expected_height}, received {received_height}, tolerance {tolerance_blocks}; try another server")]
385 Height {
386 expected_height: u64,
388 received_height: u64,
390 tolerance_blocks: u64,
392 },
393 #[error(
395 "received invalid time: expected {expected_timestamp_ms}ms, received {received_timestamp_ms} ms, tolerance {tolerance_ms} ms; try another server"
396 )]
397 Time {
398 expected_timestamp_ms: u64,
400 received_timestamp_ms: u64,
402 tolerance_ms: u64,
404 },
405 #[error("received epoch is outdated: hinted {hinted_epoch}, proven started epoch {proven_epoch}; try another server")]
412 Epoch {
413 hinted_epoch: EpochIndex,
415 proven_epoch: EpochIndex,
417 },
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 mod from_dapi_client_error {
425 use super::*;
426 use assert_matches::assert_matches;
427 use base64::Engine;
428 use dapi_grpc::tonic::metadata::{MetadataMap, MetadataValue};
429 use dpp::consensus::basic::identity::IdentityAssetLockProofLockedTransactionMismatchError;
430 use dpp::consensus::basic::BasicError;
431 use dpp::dashcore::hashes::Hash;
432 use dpp::dashcore::Txid;
433 use dpp::serialization::PlatformSerializableWithPlatformVersion;
434 use dpp::version::PlatformVersion;
435
436 #[test]
437 fn test_already_exists() {
438 let error = DapiClientError::Transport(TransportError::Grpc(
439 dapi_grpc::tonic::Status::new(Code::AlreadyExists, "Object already exists"),
440 ));
441
442 let sdk_error: Error = error.into();
443 assert!(matches!(sdk_error, Error::AlreadyExists(_)));
444 }
445
446 #[test]
447 fn test_consensus_error() {
448 let platform_version = PlatformVersion::latest();
449
450 let consensus_error = ConsensusError::BasicError(
451 BasicError::IdentityAssetLockProofLockedTransactionMismatchError(
452 IdentityAssetLockProofLockedTransactionMismatchError::new(
453 Txid::from_byte_array([0; 32]),
454 Txid::from_byte_array([1; 32]),
455 ),
456 ),
457 );
458
459 let consensus_error_bytes = consensus_error
460 .serialize_to_bytes_with_platform_version(platform_version)
461 .expect("serialize consensus error to bytes");
462
463 let mut metadata = MetadataMap::new();
464 metadata.insert_bin(
465 "dash-serialized-consensus-error-bin",
466 MetadataValue::from_bytes(&consensus_error_bytes),
467 );
468
469 let status =
470 dapi_grpc::tonic::Status::with_metadata(Code::InvalidArgument, "Test", metadata);
471
472 let error = DapiClientError::Transport(TransportError::Grpc(status));
473
474 let sdk_error = Error::from(error);
475
476 assert_matches!(
477 sdk_error,
478 Error::Protocol(ProtocolError::ConsensusError(e)) if matches!(*e, ConsensusError::BasicError(
479 BasicError::IdentityAssetLockProofLockedTransactionMismatchError(_)
480 ))
481 );
482 }
483
484 #[test]
485 fn test_consensus_error_with_fixture() {
486 let consensus_error_bytes = base64::engine::general_purpose::STANDARD.decode("ATUgJOJEYbuHBqyTeApO/ptxQ8IAw8nm9NbGROu1nyE/kqcgDTlFeUG0R4wwVcbZJMFErL+VSn63SUpP49cequ3fsKw=").expect("decode base64");
487 let consensus_error = MetadataValue::from_bytes(&consensus_error_bytes);
488
489 let mut metadata = MetadataMap::new();
490 metadata.insert_bin("dash-serialized-consensus-error-bin", consensus_error);
491
492 let status =
493 dapi_grpc::tonic::Status::with_metadata(Code::InvalidArgument, "Test", metadata);
494
495 let error = DapiClientError::Transport(TransportError::Grpc(status));
496
497 let sdk_error = Error::from(error);
498
499 assert_matches!(
500 sdk_error,
501 Error::Protocol(ProtocolError::ConsensusError(e)) if matches!(*e, ConsensusError::BasicError(
502 BasicError::IdentityAssetLockProofLockedTransactionMismatchError(_)
503 ))
504 );
505 }
506
507 #[test]
508 fn test_drive_error_data_bin_maps_to_drive_internal_error() {
509 let cbor_map = ciborium::Value::Map(vec![
510 (
511 ciborium::Value::Text("code".to_string()),
512 ciborium::Value::Integer(13.into()),
513 ),
514 (
515 ciborium::Value::Text("message".to_string()),
516 ciborium::Value::Text(
517 "storage: identity: a unique key with that hash already exists: \
518 the key already exists in the non unique set [1, 2, 3]"
519 .to_string(),
520 ),
521 ),
522 ]);
523 let mut cbor_bytes = Vec::new();
524 ciborium::into_writer(&cbor_map, &mut cbor_bytes).expect("CBOR serialization");
525
526 let mut metadata = MetadataMap::new();
527 metadata.insert_bin(
528 "drive-error-data-bin",
529 MetadataValue::from_bytes(&cbor_bytes),
530 );
531
532 let status =
533 dapi_grpc::tonic::Status::with_metadata(Code::Internal, "internal", metadata);
534 let error = DapiClientError::Transport(TransportError::Grpc(status));
535
536 let sdk_error = Error::from(error);
537
538 assert_matches!(sdk_error, Error::DriveInternalError(msg) if msg.contains("unique key"));
539 }
540
541 #[test]
542 fn test_internal_error_without_drive_metadata_falls_through() {
543 let status = dapi_grpc::tonic::Status::new(Code::Internal, "Internal error");
544 let error = DapiClientError::Transport(TransportError::Grpc(status));
545
546 let sdk_error = Error::from(error);
547
548 assert_matches!(sdk_error, Error::DapiClientError(_));
549 }
550
551 #[test]
552 fn test_non_internal_code_with_drive_metadata_not_intercepted() {
553 let cbor_map = ciborium::Value::Map(vec![(
554 ciborium::Value::Text("message".to_string()),
555 ciborium::Value::Text("some drive error".to_string()),
556 )]);
557 let mut cbor_bytes = Vec::new();
558 ciborium::into_writer(&cbor_map, &mut cbor_bytes).expect("CBOR serialization");
559
560 let mut metadata = MetadataMap::new();
561 metadata.insert_bin(
562 "drive-error-data-bin",
563 MetadataValue::from_bytes(&cbor_bytes),
564 );
565
566 let status =
567 dapi_grpc::tonic::Status::with_metadata(Code::Unavailable, "unavailable", metadata);
568 let error = DapiClientError::Transport(TransportError::Grpc(status));
569
570 let sdk_error = Error::from(error);
571
572 assert_matches!(sdk_error, Error::DapiClientError(_));
573 }
574
575 #[test]
576 fn test_malformed_cbor_in_drive_error_data_bin_falls_through() {
577 let garbage_bytes = vec![0xFF, 0xFE, 0x00, 0x01, 0x02];
578
579 let mut metadata = MetadataMap::new();
580 metadata.insert_bin(
581 "drive-error-data-bin",
582 MetadataValue::from_bytes(&garbage_bytes),
583 );
584
585 let status =
586 dapi_grpc::tonic::Status::with_metadata(Code::Internal, "internal", metadata);
587 let error = DapiClientError::Transport(TransportError::Grpc(status));
588
589 let sdk_error = Error::from(error);
590
591 assert_matches!(sdk_error, Error::DapiClientError(_));
592 }
593
594 #[test]
599 fn test_deeply_nested_cbor_rejected_without_stack_exhaustion() {
600 let payload = vec![0xA1u8; 60_000];
601 assert!(super::extract_drive_error_message(&payload).is_none());
602 }
603 }
604}