sui_rpc_api/
error.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use sui_types::error::ErrorCategory;
5use tonic::Code;
6
7use crate::proto::google::rpc::{BadRequest, ErrorInfo, RetryInfo};
8pub use sui_rpc::proto::sui::rpc::v2::ErrorReason;
9
10pub type Result<T, E = RpcError> = std::result::Result<T, E>;
11
12/// An error encountered while serving an RPC request.
13///
14/// General error type used by top-level RPC service methods. The main purpose of this error type
15/// is to provide a convenient type for converting between internal errors and a response that
16/// needs to be sent to a calling client.
17#[derive(Debug)]
18pub struct RpcError {
19    code: Code,
20    message: Option<String>,
21    details: Option<Box<ErrorDetails>>,
22}
23
24impl RpcError {
25    pub fn new<T: Into<String>>(code: Code, message: T) -> Self {
26        Self {
27            code,
28            message: Some(message.into()),
29            details: None,
30        }
31    }
32
33    pub fn not_found() -> Self {
34        Self {
35            code: Code::NotFound,
36            message: None,
37            details: None,
38        }
39    }
40
41    pub fn into_status_proto(self) -> crate::proto::google::rpc::Status {
42        crate::proto::google::rpc::Status {
43            code: self.code.into(),
44            message: self.message.unwrap_or_default(),
45            details: self
46                .details
47                .map(ErrorDetails::into_status_details)
48                .unwrap_or_default(),
49        }
50    }
51}
52
53impl From<RpcError> for tonic::Status {
54    fn from(value: RpcError) -> Self {
55        use prost::Message;
56
57        let code = value.code;
58        let status = value.into_status_proto();
59        let details = status.encode_to_vec().into();
60        let message = status.message;
61
62        tonic::Status::with_details(code, message, details)
63    }
64}
65
66impl From<sui_types::storage::error::Error> for RpcError {
67    fn from(value: sui_types::storage::error::Error) -> Self {
68        Self {
69            code: Code::Internal,
70            message: Some(value.to_string()),
71            details: None,
72        }
73    }
74}
75
76impl From<anyhow::Error> for RpcError {
77    fn from(value: anyhow::Error) -> Self {
78        Self {
79            code: Code::Internal,
80            message: Some(value.to_string()),
81            details: None,
82        }
83    }
84}
85
86impl From<sui_types::sui_sdk_types_conversions::SdkTypeConversionError> for RpcError {
87    fn from(value: sui_types::sui_sdk_types_conversions::SdkTypeConversionError) -> Self {
88        Self {
89            code: Code::Internal,
90            message: Some(value.to_string()),
91            details: None,
92        }
93    }
94}
95
96impl From<bcs::Error> for RpcError {
97    fn from(value: bcs::Error) -> Self {
98        Self {
99            code: Code::Internal,
100            message: Some(value.to_string()),
101            details: None,
102        }
103    }
104}
105
106impl From<sui_types::quorum_driver_types::QuorumDriverError> for RpcError {
107    fn from(error: sui_types::quorum_driver_types::QuorumDriverError) -> Self {
108        use itertools::Itertools;
109        use sui_types::error::SuiErrorKind;
110        use sui_types::quorum_driver_types::QuorumDriverError::*;
111
112        match error {
113            InvalidUserSignature(err) => {
114                let message = {
115                    let err = match err.as_inner() {
116                        SuiErrorKind::UserInputError { error } => error.to_string(),
117                        _ => err.to_string(),
118                    };
119                    format!("Invalid user signature: {err}")
120                };
121
122                RpcError::new(Code::InvalidArgument, message)
123            }
124            QuorumDriverInternalError(err) => RpcError::new(Code::Internal, err.to_string()),
125            ObjectsDoubleUsed { conflicting_txes } => {
126                let new_map = conflicting_txes
127                    .into_iter()
128                    .map(|(digest, (pairs, _))| {
129                        (
130                            digest,
131                            pairs.into_iter().map(|(_, obj_ref)| obj_ref).collect(),
132                        )
133                    })
134                    .collect::<std::collections::BTreeMap<_, Vec<_>>>();
135
136                let message = format!(
137                        "Failed to sign transaction by a quorum of validators because of locked objects. Conflicting Transactions:\n{new_map:#?}",
138                    );
139
140                RpcError::new(Code::FailedPrecondition, message)
141            }
142            TimeoutBeforeFinality | FailedWithTransientErrorAfterMaximumAttempts { .. } => {
143                // TODO add a Retry-After header
144                RpcError::new(
145                    Code::Unavailable,
146                    "timed-out before finality could be reached",
147                )
148            }
149            TimeoutBeforeFinalityWithErrors { last_error, attempts, timeout } => {
150                // TODO add a Retry-After header
151                RpcError::new(
152                    Code::Unavailable,
153                    format!("Transaction timed out before finality could be reached. Attempts: {attempts} & timeout: {timeout:?}. Last error: {last_error}"),
154                )
155            }
156            NonRecoverableTransactionError { errors } => {
157                let new_errors: Vec<String> = errors
158                    .into_iter()
159                    // sort by total stake, descending, so users see the most prominent one first
160                    .sorted_by(|(_, a, _), (_, b, _)| b.cmp(a))
161                    .filter_map(|(err, _, _)| {
162                        match err.as_inner(){
163                            // Special handling of UserInputError:
164                            // ObjectNotFound and DependentPackageNotFound are considered
165                            // retryable errors but they have different treatment
166                            // in AuthorityAggregator.
167                            // The optimal fix would be to examine if the total stake
168                            // of ObjectNotFound/DependentPackageNotFound exceeds the
169                            // quorum threshold, but it takes a Committee here.
170                            // So, we take an easier route and consider them non-retryable
171                            // at all. Combining this with the sorting above, clients will
172                            // see the dominant error first.
173                            SuiErrorKind::UserInputError { error } => Some(error.to_string()),
174                            _ => {
175                                if err.is_retryable().0 {
176                                    None
177                                } else {
178                                    Some(err.to_string())
179                                }
180                            }
181                        }
182                    })
183                    .collect();
184
185                assert!(
186                    !new_errors.is_empty(),
187                    "NonRecoverableTransactionError should have at least one non-retryable error"
188                );
189
190                let error_list = new_errors.join(", ");
191                let error_msg = format!("Transaction execution failed due to issues with transaction inputs, please review the errors and try again: {}.", error_list);
192
193                RpcError::new(Code::InvalidArgument, error_msg)
194            }
195            TxAlreadyFinalizedWithDifferentUserSignatures => RpcError::new(
196                Code::Aborted,
197                "The transaction is already finalized but with different user signatures",
198            ),
199            SystemOverload { .. } | SystemOverloadRetryAfter { .. } => {
200                // TODO add a Retry-After header
201                RpcError::new(Code::Unavailable, "system is overloaded")
202            }
203            TransactionFailed { category, details } => RpcError::new(
204                // TODO(fastpath): add a Retry-After header.
205                match category {
206                    ErrorCategory::Internal => Code::Internal,
207                    ErrorCategory::Aborted => Code::Aborted,
208                    ErrorCategory::InvalidTransaction => Code::InvalidArgument,
209                    ErrorCategory::LockConflict => Code::FailedPrecondition,
210                    ErrorCategory::ValidatorOverloaded => Code::ResourceExhausted,
211                    ErrorCategory::Unavailable => Code::Unavailable,
212                },
213                details,
214            ),
215            PendingExecutionInTransactionOrchestrator => RpcError::new(
216                Code::AlreadyExists,
217                "Transaction is already being processed in transaction orchestrator (most likely by quorum driver), wait for results",
218            ),
219        }
220    }
221}
222
223impl From<crate::proto::google::rpc::bad_request::FieldViolation> for RpcError {
224    fn from(value: crate::proto::google::rpc::bad_request::FieldViolation) -> Self {
225        BadRequest::from(value).into()
226    }
227}
228
229impl From<BadRequest> for RpcError {
230    fn from(value: BadRequest) -> Self {
231        let message = value
232            .field_violations
233            .first()
234            .map(|violation| violation.description.clone());
235        let details = ErrorDetails::new().with_bad_request(value);
236
237        RpcError {
238            code: Code::InvalidArgument,
239            message,
240            details: Some(Box::new(details)),
241        }
242    }
243}
244
245#[derive(Clone, Debug, Default)]
246pub struct ErrorDetails {
247    error_info: Option<ErrorInfo>,
248    bad_request: Option<BadRequest>,
249    retry_info: Option<RetryInfo>,
250}
251
252impl ErrorDetails {
253    pub fn new() -> Self {
254        Self::default()
255    }
256
257    pub fn error_info(&self) -> Option<&ErrorInfo> {
258        self.error_info.as_ref()
259    }
260
261    pub fn bad_request(&self) -> Option<&BadRequest> {
262        self.bad_request.as_ref()
263    }
264
265    pub fn retry_info(&self) -> Option<&RetryInfo> {
266        self.retry_info.as_ref()
267    }
268
269    pub fn details(&self) -> &[prost_types::Any] {
270        &[]
271    }
272
273    pub fn with_bad_request(mut self, bad_request: BadRequest) -> Self {
274        self.bad_request = Some(bad_request);
275        self
276    }
277
278    #[allow(clippy::boxed_local)]
279    fn into_status_details(self: Box<Self>) -> Vec<prost_types::Any> {
280        let mut details = Vec::new();
281
282        if let Some(error_info) = &self.error_info {
283            details.push(
284                prost_types::Any::from_msg(error_info).expect("Message encoding cannot fail"),
285            );
286        }
287
288        if let Some(bad_request) = &self.bad_request {
289            details.push(
290                prost_types::Any::from_msg(bad_request).expect("Message encoding cannot fail"),
291            );
292        }
293
294        if let Some(retry_info) = &self.retry_info {
295            details.push(
296                prost_types::Any::from_msg(retry_info).expect("Message encoding cannot fail"),
297            );
298        }
299        details
300    }
301}
302
303#[derive(Debug)]
304pub struct ObjectNotFoundError {
305    object_id: sui_sdk_types::Address,
306    version: Option<sui_sdk_types::Version>,
307}
308
309impl ObjectNotFoundError {
310    pub fn new(object_id: sui_sdk_types::Address) -> Self {
311        Self {
312            object_id,
313            version: None,
314        }
315    }
316
317    pub fn new_with_version(
318        object_id: sui_sdk_types::Address,
319        version: sui_sdk_types::Version,
320    ) -> Self {
321        Self {
322            object_id,
323            version: Some(version),
324        }
325    }
326}
327
328impl std::fmt::Display for ObjectNotFoundError {
329    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330        write!(f, "Object {}", self.object_id)?;
331
332        if let Some(version) = self.version {
333            write!(f, " with version {version}")?;
334        }
335
336        write!(f, " not found")
337    }
338}
339
340impl std::error::Error for ObjectNotFoundError {}
341
342impl From<ObjectNotFoundError> for crate::RpcError {
343    fn from(value: ObjectNotFoundError) -> Self {
344        Self::new(tonic::Code::NotFound, value.to_string())
345    }
346}
347
348#[derive(Debug)]
349pub struct CheckpointNotFoundError {
350    sequence_number: Option<u64>,
351    digest: Option<sui_sdk_types::Digest>,
352}
353
354impl CheckpointNotFoundError {
355    pub fn sequence_number(sequence_number: u64) -> Self {
356        Self {
357            sequence_number: Some(sequence_number),
358            digest: None,
359        }
360    }
361
362    pub fn digest(digest: sui_sdk_types::Digest) -> Self {
363        Self {
364            sequence_number: None,
365            digest: Some(digest),
366        }
367    }
368}
369
370impl std::fmt::Display for CheckpointNotFoundError {
371    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372        write!(f, "Checkpoint ")?;
373
374        if let Some(s) = self.sequence_number {
375            write!(f, "{s} ")?;
376        }
377
378        if let Some(d) = &self.digest {
379            write!(f, "{d} ")?;
380        }
381
382        write!(f, "not found")
383    }
384}
385
386impl std::error::Error for CheckpointNotFoundError {}
387
388impl From<CheckpointNotFoundError> for crate::RpcError {
389    fn from(value: CheckpointNotFoundError) -> Self {
390        Self::new(tonic::Code::NotFound, value.to_string())
391    }
392}