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