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