Skip to main content

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