1use 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#[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 #[error("{0}")]
34 RejectedAtValidator(SuiError),
35 #[error("Transaction rejected by consensus")]
36 RejectedByConsensus,
37 #[error("Transaction status expired")]
39 StatusExpired(EpochId, u32),
40 #[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#[derive(Eq, PartialEq, Clone)]
70pub enum TransactionDriverError {
71 ClientInternal { error: String },
74 ValidationFailed { error: String },
77 Aborted {
80 submission_non_retriable_errors: AggregatedRequestErrors,
81 submission_retriable_errors: AggregatedRequestErrors,
82 observed_effects_digests: AggregatedEffectsDigests,
83 },
84 RejectedByValidators {
87 submission_non_retriable_errors: AggregatedRequestErrors,
88 submission_retriable_errors: AggregatedRequestErrors,
89 },
90 ForkedExecution {
94 observed_effects_digests: AggregatedEffectsDigests,
95 submission_non_retriable_errors: AggregatedRequestErrors,
96 submission_retriable_errors: AggregatedRequestErrors,
97 },
98 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 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 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
305fn 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}