sui_core/transaction_driver/
error.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::BTreeMap;
5use std::time::Duration;
6
7use itertools::Itertools as _;
8use sui_types::{
9    base_types::{AuthorityName, ConciseableName},
10    committee::{EpochId, StakeUnit},
11    digests::TransactionEffectsDigest,
12    error::{ErrorCategory, SuiError, SuiErrorKind},
13};
14use thiserror::Error;
15
16/// Errors emitted from individual validators during transaction driver operations.
17///
18/// These errors are associated with the transaction and authority externally, so it is unnecessary
19/// to include those information in these messages.
20///
21/// NOTE: these errors will be aggregated across authorities by status and reported to the caller.
22/// So the error messages should not contain authority specific information, such as authority name.
23#[derive(Eq, PartialEq, Clone, Debug, Error)]
24pub(crate) enum TransactionRequestError {
25    #[error("Request timed out submitting transaction")]
26    TimedOutSubmittingTransaction,
27    #[error("Request timed out getting full effects")]
28    TimedOutGettingFullEffectsAtValidator,
29    #[error("{0}")]
30    ValidatorInternal(String),
31
32    // Rejected by the validator when voting on the transaction.
33    #[error("{0}")]
34    RejectedAtValidator(SuiError),
35    #[error("Transaction rejected by consensus")]
36    RejectedByConsensus,
37    // Transaction status has been dropped from cache at the validator.
38    #[error("Transaction status expired")]
39    StatusExpired(EpochId, u32),
40    // Request to submit transaction or get full effects failed.
41    #[error("{0}")]
42    Aborted(SuiError),
43}
44
45impl TransactionRequestError {
46    pub(crate) fn categorize(&self) -> ErrorCategory {
47        match self {
48            TransactionRequestError::TimedOutSubmittingTransaction => ErrorCategory::Unavailable,
49            TransactionRequestError::TimedOutGettingFullEffectsAtValidator => {
50                ErrorCategory::Unavailable
51            }
52            TransactionRequestError::ValidatorInternal(_) => ErrorCategory::Internal,
53
54            TransactionRequestError::RejectedAtValidator(error) => error.categorize(),
55            TransactionRequestError::RejectedByConsensus => ErrorCategory::Aborted,
56            TransactionRequestError::StatusExpired(_, _) => ErrorCategory::Aborted,
57            TransactionRequestError::Aborted(error) => error.categorize(),
58        }
59    }
60
61    pub(crate) fn is_submission_retriable(&self) -> bool {
62        self.categorize().is_submission_retriable()
63    }
64}
65
66/// Client facing errors on transaction processing via Transaction Driver.
67///
68/// NOTE: every error should indicate if it is retriable.
69#[derive(Eq, PartialEq, Clone)]
70pub enum TransactionDriverError {
71    /// TransactionDriver encountered an internal error.
72    /// Non-retriable.
73    ClientInternal { error: String },
74    /// The transaction failed validation from local state.
75    /// Non-retriable.
76    ValidationFailed { error: String },
77    /// Transient failure during transaction processing that prevents the transaction from finalization.
78    /// Retriable with new transaction submission.
79    Aborted {
80        submission_non_retriable_errors: AggregatedRequestErrors,
81        submission_retriable_errors: AggregatedRequestErrors,
82        observed_effects_digests: AggregatedEffectsDigests,
83    },
84    /// Over validity threshold of validators rejected the transaction as invalid.
85    /// Non-retriable.
86    RejectedByValidators {
87        submission_non_retriable_errors: AggregatedRequestErrors,
88        submission_retriable_errors: AggregatedRequestErrors,
89    },
90    /// Transaction execution observed multiple effects digests, and it is no longer possible to
91    /// certify any of them.
92    /// Non-retriable.
93    ForkedExecution {
94        observed_effects_digests: AggregatedEffectsDigests,
95        submission_non_retriable_errors: AggregatedRequestErrors,
96        submission_retriable_errors: AggregatedRequestErrors,
97    },
98    /// Transaction timed out but we return last retriable error if it exists.
99    /// Non-retriable.
100    TimeoutWithLastRetriableError {
101        last_error: Option<Box<TransactionDriverError>>,
102        attempts: u32,
103        timeout: Duration,
104    },
105}
106
107impl TransactionDriverError {
108    pub(crate) fn is_submission_retriable(&self) -> bool {
109        self.categorize().is_submission_retriable()
110    }
111
112    pub fn categorize(&self) -> ErrorCategory {
113        match self {
114            TransactionDriverError::ClientInternal { .. } => ErrorCategory::Internal,
115            TransactionDriverError::ValidationFailed { .. } => ErrorCategory::InvalidTransaction,
116            TransactionDriverError::Aborted {
117                submission_retriable_errors,
118                submission_non_retriable_errors,
119                ..
120            } => {
121                if let Some((_, _, _, category)) = submission_retriable_errors.errors.first() {
122                    *category
123                } else if let Some((_, _, _, category)) =
124                    submission_non_retriable_errors.errors.first()
125                {
126                    *category
127                } else {
128                    ErrorCategory::Aborted
129                }
130            }
131            TransactionDriverError::RejectedByValidators {
132                submission_non_retriable_errors,
133                submission_retriable_errors,
134                ..
135            } => {
136                if let Some((_, _, _, category)) = submission_non_retriable_errors.errors.first() {
137                    *category
138                } else if let Some((_, _, _, category)) = submission_retriable_errors.errors.first()
139                {
140                    *category
141                } else {
142                    // There should be at least one error.
143                    ErrorCategory::Internal
144                }
145            }
146            TransactionDriverError::ForkedExecution { .. } => ErrorCategory::Internal,
147            TransactionDriverError::TimeoutWithLastRetriableError { .. } => {
148                ErrorCategory::Unavailable
149            }
150        }
151    }
152
153    fn display_aborted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        let TransactionDriverError::Aborted {
155            submission_non_retriable_errors,
156            submission_retriable_errors,
157            observed_effects_digests,
158        } = self
159        else {
160            return Ok(());
161        };
162        let mut msgs =
163            vec!["Transaction processing aborted (retriable with another submission).".to_string()];
164        if submission_retriable_errors.total_stake > 0 {
165            msgs.push(format!(
166                "Retriable errors: [{submission_retriable_errors}]."
167            ));
168        }
169        if submission_non_retriable_errors.total_stake > 0 {
170            msgs.push(format!(
171                "Non-retriable errors: [{submission_non_retriable_errors}]."
172            ));
173        }
174        if !observed_effects_digests.digests.is_empty() {
175            msgs.push(format!(
176                "Observed effects digests: [{observed_effects_digests}]."
177            ));
178        }
179        write!(f, "{}", msgs.join(" "))
180    }
181
182    fn display_validation_failed(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        let TransactionDriverError::ValidationFailed { error } = self else {
184            return Ok(());
185        };
186        write!(f, "Transaction failed validation: {}", error)
187    }
188
189    fn display_invalid_transaction(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        let TransactionDriverError::RejectedByValidators {
191            submission_non_retriable_errors,
192            submission_retriable_errors,
193        } = self
194        else {
195            return Ok(());
196        };
197        let mut msgs = vec!["Transaction is rejected as invalid by more than 1/3 of validators by stake (non-retriable).".to_string()];
198        if submission_non_retriable_errors.total_stake > 0 {
199            msgs.push(format!(
200                "Non-retriable errors: [{submission_non_retriable_errors}]."
201            ));
202        }
203        if submission_retriable_errors.total_stake > 0 {
204            msgs.push(format!(
205                "Retriable errors: [{submission_retriable_errors}]."
206            ));
207        }
208        write!(f, "{}", msgs.join(" "))
209    }
210
211    fn display_forked_execution(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212        let TransactionDriverError::ForkedExecution {
213            observed_effects_digests,
214            submission_non_retriable_errors,
215            submission_retriable_errors,
216        } = self
217        else {
218            return Ok(());
219        };
220        let mut msgs =
221            vec!["Transaction execution observed forked outputs (non-retriable).".to_string()];
222        msgs.push(format!(
223            "Observed effects digests: [{observed_effects_digests}]."
224        ));
225        if submission_non_retriable_errors.total_stake > 0 {
226            msgs.push(format!(
227                "Non-retriable errors: [{submission_non_retriable_errors}]."
228            ));
229        }
230        if submission_retriable_errors.total_stake > 0 {
231            msgs.push(format!(
232                "Retriable errors: [{submission_retriable_errors}]."
233            ));
234        }
235        write!(f, "{}", msgs.join(" "))
236    }
237}
238
239impl std::fmt::Display for TransactionDriverError {
240    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241        match self {
242            TransactionDriverError::ClientInternal { error } => {
243                write!(f, "TransactionDriver internal error: {}", error)
244            }
245            TransactionDriverError::Aborted { .. } => self.display_aborted(f),
246            TransactionDriverError::ValidationFailed { .. } => self.display_validation_failed(f),
247            TransactionDriverError::RejectedByValidators { .. } => {
248                self.display_invalid_transaction(f)
249            }
250            TransactionDriverError::ForkedExecution { .. } => self.display_forked_execution(f),
251            TransactionDriverError::TimeoutWithLastRetriableError {
252                last_error,
253                attempts,
254                timeout,
255            } => {
256                write!(
257                    f,
258                    "Transaction timed out after {} attempts. Timeout: {:?}. Last error: {}",
259                    attempts,
260                    timeout,
261                    last_error
262                        .as_ref()
263                        .map(|e| e.to_string())
264                        .unwrap_or_default()
265                )
266            }
267        }
268    }
269}
270
271impl std::fmt::Debug for TransactionDriverError {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        write!(f, "{}", self)
274    }
275}
276
277impl std::error::Error for TransactionDriverError {}
278
279#[derive(Eq, PartialEq, Clone, Debug, Default)]
280pub struct AggregatedRequestErrors {
281    pub errors: Vec<(String, Vec<AuthorityName>, StakeUnit, ErrorCategory)>,
282    // The total stake of all errors.
283    pub total_stake: StakeUnit,
284}
285
286impl std::fmt::Display for AggregatedRequestErrors {
287    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288        let msg = self
289            .errors
290            .iter()
291            .map(|(error, names, stake, _category)| {
292                format!(
293                    "{} {{ {} }} with {} stake",
294                    error,
295                    names.iter().map(|n| n.concise_owned()).join(", "),
296                    stake
297                )
298            })
299            .join("; ");
300        write!(f, "{}", msg)?;
301        Ok(())
302    }
303}
304
305// TODO(fastpath): This is a temporary fix to unify the error message between QD and TD.
306// Match special handling of UserInputError in sui-json-rpc/src/error.rs NonRecoverableTransactionError
307fn format_transaction_request_error(error: &TransactionRequestError) -> String {
308    match error {
309        TransactionRequestError::RejectedAtValidator(sui_error) => match sui_error.as_inner() {
310            SuiErrorKind::UserInputError { error: user_error } => user_error.to_string(),
311            _ => sui_error.to_string(),
312        },
313        _ => error.to_string(),
314    }
315}
316
317pub(crate) fn aggregate_request_errors(
318    errors: Vec<(AuthorityName, StakeUnit, TransactionRequestError)>,
319) -> AggregatedRequestErrors {
320    let mut total_stake = 0;
321    let mut aggregated_errors =
322        BTreeMap::<String, (Vec<AuthorityName>, StakeUnit, ErrorCategory)>::new();
323
324    for (name, stake, error) in errors {
325        total_stake += stake;
326        let key = format_transaction_request_error(&error);
327        let entry = aggregated_errors
328            .entry(key)
329            .or_insert_with(|| (vec![], 0, error.categorize()));
330        entry.0.push(name);
331        entry.1 += stake;
332    }
333
334    let mut errors: Vec<_> = aggregated_errors
335        .into_iter()
336        .map(|(error, (names, stake, category))| (error, names, stake, category))
337        .collect();
338    errors.sort_by_key(|(_, _, stake, _)| std::cmp::Reverse(*stake));
339
340    AggregatedRequestErrors {
341        errors,
342        total_stake,
343    }
344}
345
346#[derive(Eq, PartialEq, Clone, Debug)]
347pub struct AggregatedEffectsDigests {
348    pub digests: Vec<(TransactionEffectsDigest, Vec<AuthorityName>, StakeUnit)>,
349}
350
351impl std::fmt::Display for AggregatedEffectsDigests {
352    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353        let msg = self
354            .digests
355            .iter()
356            .map(|(digest, names, stake)| {
357                format!(
358                    "{} {{ {} }} with {} stake",
359                    digest,
360                    names.iter().map(|n| n.concise_owned()).join(", "),
361                    stake
362                )
363            })
364            .join("; ");
365        write!(f, "{}", msg)?;
366        Ok(())
367    }
368}
369
370impl AggregatedEffectsDigests {
371    #[cfg(test)]
372    pub fn total_stake(&self) -> StakeUnit {
373        self.digests.iter().map(|(_, _, stake)| stake).sum()
374    }
375}