sui_core/transaction_driver/
error.rsuse std::collections::BTreeMap;
use std::time::Duration;
use itertools::Itertools as _;
use sui_types::{
base_types::{AuthorityName, ConciseableName},
committee::{EpochId, StakeUnit},
digests::TransactionEffectsDigest,
error::SuiError,
};
use thiserror::Error;
#[derive(Eq, PartialEq, Clone, Debug, Error)]
pub(crate) enum TransactionRequestError {
#[error("Request timed out submitting transaction")]
TimedOutSubmittingTransaction,
#[error("Request timed out getting full effects")]
TimedOutGettingFullEffectsAtValidator,
#[error("Failed to find execution data")]
ExecutionDataNotFound,
#[error("{0}")]
RejectedAtValidator(SuiError),
#[error("Transaction rejected by consensus")]
RejectedByConsensus,
#[error("Transaction status expired")]
StatusExpired(EpochId, u32),
#[error("{0}")]
Aborted(SuiError),
}
impl TransactionRequestError {
pub fn is_submission_retriable(&self) -> bool {
match self {
TransactionRequestError::RejectedAtValidator(error) => {
error.is_transaction_submission_retriable()
}
TransactionRequestError::Aborted(error) => error.is_transaction_submission_retriable(),
_ => true,
}
}
}
#[derive(Eq, PartialEq, Clone)]
pub enum TransactionDriverError {
Internal { error: String },
Aborted {
submission_non_retriable_errors: AggregatedRequestErrors,
submission_retriable_errors: AggregatedRequestErrors,
observed_effects_digests: AggregatedEffectsDigests,
},
ValidationFailed { error: String },
InvalidTransaction {
submission_non_retriable_errors: AggregatedRequestErrors,
submission_retriable_errors: AggregatedRequestErrors,
},
ForkedExecution {
observed_effects_digests: AggregatedEffectsDigests,
submission_non_retriable_errors: AggregatedRequestErrors,
submission_retriable_errors: AggregatedRequestErrors,
},
TimeoutWithLastRetriableError {
last_error: Option<Box<TransactionDriverError>>,
attempts: u32,
timeout: Duration,
},
}
impl TransactionDriverError {
pub fn is_retriable(&self) -> bool {
match self {
TransactionDriverError::Internal { .. } => true,
TransactionDriverError::Aborted { .. } => true,
TransactionDriverError::ValidationFailed { .. } => false,
TransactionDriverError::InvalidTransaction { .. } => false,
TransactionDriverError::ForkedExecution { .. } => false,
TransactionDriverError::TimeoutWithLastRetriableError { .. } => true,
}
}
fn display_aborted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let TransactionDriverError::Aborted {
submission_non_retriable_errors,
submission_retriable_errors,
observed_effects_digests,
} = self
else {
return Ok(());
};
let mut msgs = vec![
"Transaction processing aborted (retriable with the same transaction).".to_string(),
];
if submission_retriable_errors.total_stake > 0 {
msgs.push(format!(
"Retriable errors: [{submission_retriable_errors}]."
));
}
if submission_non_retriable_errors.total_stake > 0 {
msgs.push(format!(
"Non-retriable errors: [{submission_non_retriable_errors}]."
));
}
if !observed_effects_digests.digests.is_empty() {
msgs.push(format!(
"Observed effects digests: [{observed_effects_digests}]."
));
}
write!(f, "{}", msgs.join(" "))
}
fn display_validation_failed(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let TransactionDriverError::ValidationFailed { error } = self else {
return Ok(());
};
write!(f, "Transaction failed validation: {}", error)
}
fn display_invalid_transaction(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let TransactionDriverError::InvalidTransaction {
submission_non_retriable_errors,
submission_retriable_errors,
} = self
else {
return Ok(());
};
let mut msgs = vec!["Transaction is rejected as invalid by more than 1/3 of validators by stake (non-retriable).".to_string()];
if submission_non_retriable_errors.total_stake > 0 {
msgs.push(format!(
"Non-retriable errors: [{submission_non_retriable_errors}]."
));
}
if submission_retriable_errors.total_stake > 0 {
msgs.push(format!(
"Retriable errors: [{submission_retriable_errors}]."
));
}
write!(f, "{}", msgs.join(" "))
}
fn display_forked_execution(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let TransactionDriverError::ForkedExecution {
observed_effects_digests,
submission_non_retriable_errors,
submission_retriable_errors,
} = self
else {
return Ok(());
};
let mut msgs =
vec!["Transaction execution observed forked outputs (non-retriable).".to_string()];
msgs.push(format!(
"Observed effects digests: [{observed_effects_digests}]."
));
if submission_non_retriable_errors.total_stake > 0 {
msgs.push(format!(
"Non-retriable errors: [{submission_non_retriable_errors}]."
));
}
if submission_retriable_errors.total_stake > 0 {
msgs.push(format!(
"Retriable errors: [{submission_retriable_errors}]."
));
}
write!(f, "{}", msgs.join(" "))
}
}
impl std::fmt::Display for TransactionDriverError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TransactionDriverError::Internal { error } => write!(f, "Internal error: {}", error),
TransactionDriverError::Aborted { .. } => self.display_aborted(f),
TransactionDriverError::ValidationFailed { .. } => self.display_validation_failed(f),
TransactionDriverError::InvalidTransaction { .. } => {
self.display_invalid_transaction(f)
}
TransactionDriverError::ForkedExecution { .. } => self.display_forked_execution(f),
TransactionDriverError::TimeoutWithLastRetriableError {
last_error,
attempts,
timeout,
} => {
write!(
f,
"Transaction timed out after {} attempts. Timeout: {:?}. Last error: {}",
attempts,
timeout,
last_error
.as_ref()
.map(|e| e.to_string())
.unwrap_or_default()
)
}
}
}
}
impl std::fmt::Debug for TransactionDriverError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self)
}
}
impl std::error::Error for TransactionDriverError {}
#[derive(Eq, PartialEq, Clone, Debug, Default)]
pub struct AggregatedRequestErrors {
pub errors: Vec<(String, Vec<AuthorityName>, StakeUnit)>,
pub total_stake: StakeUnit,
}
impl std::fmt::Display for AggregatedRequestErrors {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = self
.errors
.iter()
.map(|(error, names, stake)| {
format!(
"{} {{ {} }} with {} stake",
error,
names.iter().map(|n| n.concise_owned()).join(", "),
stake
)
})
.join("; ");
write!(f, "{}", msg)?;
Ok(())
}
}
fn format_transaction_request_error(error: &TransactionRequestError) -> String {
match error {
TransactionRequestError::RejectedAtValidator(sui_error) => match sui_error {
SuiError::UserInputError { error: user_error } => user_error.to_string(),
_ => sui_error.to_string(),
},
_ => error.to_string(),
}
}
pub(crate) fn aggregate_request_errors(
errors: Vec<(AuthorityName, StakeUnit, TransactionRequestError)>,
) -> AggregatedRequestErrors {
let mut total_stake = 0;
let mut aggregated_errors = BTreeMap::<String, (Vec<AuthorityName>, StakeUnit)>::new();
for (name, stake, error) in errors {
total_stake += stake;
let key = format_transaction_request_error(&error);
let entry = aggregated_errors.entry(key).or_default();
entry.0.push(name);
entry.1 += stake;
}
let mut errors: Vec<_> = aggregated_errors
.into_iter()
.map(|(error, (names, stake))| (error, names, stake))
.collect();
errors.sort_by_key(|(_, _, stake)| std::cmp::Reverse(*stake));
AggregatedRequestErrors {
errors,
total_stake,
}
}
#[derive(Eq, PartialEq, Clone, Debug)]
pub struct AggregatedEffectsDigests {
pub digests: Vec<(TransactionEffectsDigest, Vec<AuthorityName>, StakeUnit)>,
}
impl std::fmt::Display for AggregatedEffectsDigests {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = self
.digests
.iter()
.map(|(digest, names, stake)| {
format!(
"{} {{ {} }} with {} stake",
digest,
names.iter().map(|n| n.concise_owned()).join(", "),
stake
)
})
.join("; ");
write!(f, "{}", msg)?;
Ok(())
}
}
impl AggregatedEffectsDigests {
#[cfg(test)]
pub fn total_stake(&self) -> StakeUnit {
self.digests.iter().map(|(_, _, stake)| stake).sum()
}
}