iota_types/
quorum_driver_types.rs

1// Copyright (c) 2021, Facebook, Inc. and its affiliates
2// Copyright (c) Mysten Labs, Inc.
3// Modifications Copyright (c) 2024 IOTA Stiftung
4// SPDX-License-Identifier: Apache-2.0
5
6use std::collections::BTreeMap;
7
8use itertools::Itertools;
9use serde::{Deserialize, Serialize};
10use strum::AsRefStr;
11use thiserror::Error;
12
13use crate::{
14    base_types::{AuthorityName, EpochId, ObjectRef, TransactionDigest},
15    committee::{QUORUM_THRESHOLD, StakeUnit, TOTAL_VOTING_POWER},
16    crypto::{AuthorityStrongQuorumSignInfo, ConciseAuthorityPublicKeyBytes},
17    effects::{
18        CertifiedTransactionEffects, TransactionEffects, TransactionEvents,
19        VerifiedCertifiedTransactionEffects,
20    },
21    error::IotaError,
22    messages_checkpoint::CheckpointSequenceNumber,
23    object::Object,
24    transaction::Transaction,
25};
26
27pub type QuorumDriverResult = Result<QuorumDriverResponse, QuorumDriverError>;
28
29pub type QuorumDriverEffectsQueueResult =
30    Result<(Transaction, QuorumDriverResponse), (TransactionDigest, QuorumDriverError)>;
31
32pub const NON_RECOVERABLE_ERROR_MSG: &str =
33    "Transaction has non recoverable errors from at least 1/3 of validators";
34
35/// Client facing errors regarding transaction submission via Quorum Driver.
36/// Every invariant needs detailed documents to instruct client handling.
37#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Error, Hash, AsRefStr)]
38pub enum QuorumDriverError {
39    #[error("QuorumDriver internal error: {0}.")]
40    QuorumDriverInternal(IotaError),
41    #[error("Invalid user signature: {0}.")]
42    InvalidUserSignature(IotaError),
43    #[error("Invalid transaction: {0}.")]
44    InvalidTransaction(IotaError),
45    #[error(
46        "Failed to sign transaction by a quorum of validators because of locked objects: {conflicting_txes:?}"
47    )]
48    ObjectsDoubleUsed {
49        conflicting_txes: BTreeMap<TransactionDigest, (Vec<(AuthorityName, ObjectRef)>, StakeUnit)>,
50    },
51    #[error("Transaction timed out before reaching finality")]
52    TimeoutBeforeFinality,
53    #[error(
54        "Transaction failed to reach finality with transient error after {total_attempts} attempts."
55    )]
56    FailedWithTransientErrorAfterMaximumAttempts { total_attempts: u32 },
57    #[error("{NON_RECOVERABLE_ERROR_MSG}: {errors:?}.")]
58    NonRecoverableTransactionError { errors: GroupedErrors },
59    #[error(
60        "Transaction is not processed because {overloaded_stake} of validators by stake are overloaded with certificates pending execution."
61    )]
62    SystemOverload {
63        overloaded_stake: StakeUnit,
64        errors: GroupedErrors,
65    },
66    #[error("Transaction is already finalized but with different user signatures")]
67    TxAlreadyFinalizedWithDifferentUserSignatures,
68    #[error(
69        "Transaction is not processed because {overload_stake} of validators are overloaded and asked client to retry after {retry_after_secs}."
70    )]
71    SystemOverloadRetryAfter {
72        overload_stake: StakeUnit,
73        errors: GroupedErrors,
74        retry_after_secs: u64,
75    },
76}
77
78impl QuorumDriverError {
79    pub fn to_error_message(&self) -> String {
80        match self {
81            QuorumDriverError::InvalidUserSignature(err) => {
82                format!("Invalid user signature: {err}")
83            }
84            QuorumDriverError::InvalidTransaction(err) => {
85                format!("Invalid transaction: {err}")
86            }
87            QuorumDriverError::TxAlreadyFinalizedWithDifferentUserSignatures => {
88                "The transaction is already finalized but with different user signatures"
89                    .to_string()
90            }
91            QuorumDriverError::TimeoutBeforeFinality
92            | QuorumDriverError::FailedWithTransientErrorAfterMaximumAttempts { .. }
93            | QuorumDriverError::SystemOverload { .. }
94            | QuorumDriverError::SystemOverloadRetryAfter { .. } => self.to_string(),
95            QuorumDriverError::ObjectsDoubleUsed { conflicting_txes } => {
96                let weights: Vec<u64> =
97                    conflicting_txes.values().map(|(_, stake)| *stake).collect();
98                let remaining: u64 = TOTAL_VOTING_POWER - weights.iter().sum::<u64>();
99
100                // better version of above
101                let reason = if weights.iter().all(|w| remaining + w < QUORUM_THRESHOLD) {
102                    "equivocated until the next epoch"
103                } else {
104                    "reserved for another transaction"
105                };
106
107                format!(
108                    "Failed to sign transaction by a quorum of validators because one or more of its objects is {}. Other transactions locking these objects:\n{}",
109                    reason,
110                    conflicting_txes
111                        .iter()
112                        .sorted_by(|(_, (_, a)), (_, (_, b))| b.cmp(a))
113                        .map(|(digest, (_, stake))| format!(
114                            "- {} (stake {}.{})",
115                            digest,
116                            stake / 100,
117                            stake % 100,
118                        ))
119                        .join("\n"),
120                )
121            }
122            QuorumDriverError::NonRecoverableTransactionError { errors } => {
123                let new_errors: Vec<String> = errors
124                    .iter()
125                    // sort by total stake, descending, so users see the most prominent one
126                    // first
127                    .sorted_by(|(_, a, _), (_, b, _)| b.cmp(a))
128                    .filter_map(|(err, _, _)| {
129                        match &err {
130                            // Special handling of UserInputError:
131                            // ObjectNotFound and DependentPackageNotFound are considered
132                            // retryable errors but they have different treatment
133                            // in AuthorityAggregator.
134                            // The optimal fix would be to examine if the total stake
135                            // of ObjectNotFound/DependentPackageNotFound exceeds the
136                            // quorum threshold, but it takes a Committee here.
137                            // So, we take an easier route and consider them non-retryable
138                            // at all. Combining this with the sorting above, clients will
139                            // see the dominant error first.
140                            IotaError::UserInput { error } => Some(error.to_string()),
141                            _ => {
142                                if err.is_retryable().0 {
143                                    None
144                                } else {
145                                    Some(err.to_string())
146                                }
147                            }
148                        }
149                    })
150                    .collect();
151
152                assert!(
153                    !new_errors.is_empty(),
154                    "NonRecoverableTransactionError should have at least one non-retryable error"
155                );
156
157                let mut error_list = vec![];
158                for err in new_errors.iter() {
159                    error_list.push(format!("- {err}"));
160                }
161
162                format!(
163                    "Transaction execution failed due to issues with transaction inputs, please review the errors and try again:\n{}",
164                    error_list.join("\n")
165                )
166            }
167            QuorumDriverError::QuorumDriverInternal { .. } => {
168                "Internal error occurred while executing transaction.".to_string()
169            }
170        }
171    }
172}
173
174pub type GroupedErrors = Vec<(IotaError, StakeUnit, Vec<ConciseAuthorityPublicKeyBytes>)>;
175
176#[derive(Serialize, Deserialize, Clone, Debug)]
177pub enum ExecuteTransactionRequestType {
178    WaitForEffectsCert,
179    WaitForLocalExecution,
180}
181
182#[derive(Serialize, Deserialize, Clone, Debug)]
183pub enum EffectsFinalityInfo {
184    Certified(AuthorityStrongQuorumSignInfo),
185    Checkpointed(EpochId, CheckpointSequenceNumber),
186}
187
188/// When requested to execute a transaction with WaitForLocalExecution,
189/// TransactionOrchestrator attempts to execute this transaction locally
190/// after it is finalized. This value represents whether the transaction
191/// is confirmed to be executed on this node before the response returns.
192pub type IsTransactionExecutedLocally = bool;
193
194#[derive(Debug, Clone)]
195pub struct QuorumDriverResponse {
196    pub effects_cert: VerifiedCertifiedTransactionEffects,
197    // pub events: TransactionEvents,
198    pub events: Option<TransactionEvents>,
199    // Input objects will only be populated in the happy path
200    pub input_objects: Option<Vec<Object>>,
201    // Output objects will only be populated in the happy path
202    pub output_objects: Option<Vec<Object>>,
203    pub auxiliary_data: Option<Vec<u8>>,
204}
205
206#[derive(Serialize, Deserialize, Clone, Debug)]
207pub struct ExecuteTransactionRequestV1 {
208    pub transaction: Transaction,
209
210    pub include_events: bool,
211    pub include_input_objects: bool,
212    pub include_output_objects: bool,
213    pub include_auxiliary_data: bool,
214}
215
216impl ExecuteTransactionRequestV1 {
217    pub fn new<T: Into<Transaction>>(transaction: T) -> Self {
218        Self {
219            transaction: transaction.into(),
220            include_events: true,
221            include_input_objects: false,
222            include_output_objects: false,
223            include_auxiliary_data: false,
224        }
225    }
226}
227
228#[derive(Serialize, Deserialize, Clone, Debug)]
229pub struct ExecuteTransactionResponseV1 {
230    pub effects: FinalizedEffects,
231
232    pub events: Option<TransactionEvents>,
233    // Input objects will only be populated in the happy path
234    pub input_objects: Option<Vec<Object>>,
235    // Output objects will only be populated in the happy path
236    pub output_objects: Option<Vec<Object>>,
237    pub auxiliary_data: Option<Vec<u8>>,
238}
239
240#[derive(Serialize, Deserialize, Clone, Debug)]
241pub struct FinalizedEffects {
242    pub effects: TransactionEffects,
243    pub finality_info: EffectsFinalityInfo,
244}
245
246impl FinalizedEffects {
247    pub fn new_from_effects_cert(effects_cert: CertifiedTransactionEffects) -> Self {
248        let (data, sig) = effects_cert.into_data_and_sig();
249        Self {
250            effects: data,
251            finality_info: EffectsFinalityInfo::Certified(sig),
252        }
253    }
254
255    pub fn epoch(&self) -> EpochId {
256        match &self.finality_info {
257            EffectsFinalityInfo::Certified(cert) => cert.epoch,
258            EffectsFinalityInfo::Checkpointed(epoch, _) => *epoch,
259        }
260    }
261}