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