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