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    /// The fullnode's local validity check proved the transaction invalid.
42    /// The same bytes can never execute.
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:
50            BTreeMap<TransactionDigest, (Vec<(AuthorityName, ObjectReference)>, StakeUnit)>,
51    },
52    #[error("Transaction timed out before reaching finality")]
53    TimeoutBeforeFinality,
54    #[error(
55        "Transaction failed to reach finality with transient error after {total_attempts} attempts."
56    )]
57    FailedWithTransientErrorAfterMaximumAttempts { total_attempts: u32 },
58    #[error("{NON_RECOVERABLE_ERROR_MSG}: {errors:?}.")]
59    NonRecoverableTransactionError { errors: GroupedErrors },
60    #[error(
61        "Transaction is not processed because {overloaded_stake} of validators by stake are overloaded with certificates pending execution."
62    )]
63    SystemOverload {
64        overloaded_stake: StakeUnit,
65        errors: GroupedErrors,
66    },
67    #[error("Transaction is already finalized but with different user signatures")]
68    TxAlreadyFinalizedWithDifferentUserSignatures,
69    #[error(
70        "Transaction is not processed because {overload_stake} of validators are overloaded and asked client to retry after {retry_after_secs}."
71    )]
72    SystemOverloadRetryAfter {
73        overload_stake: StakeUnit,
74        errors: GroupedErrors,
75        retry_after_secs: u64,
76    },
77    /// Over 1/3 of validator stake rejected the transaction during
78    /// submission. The verdicts may depend on validator-local state, and an
79    /// earlier submission attempt may already be in consensus, so the
80    /// transaction may still execute.
81    #[error("Invalid transaction: {0}.")]
82    RejectedByValidators(IotaError),
83}
84
85impl QuorumDriverError {
86    /// Stable machine-readable discriminant for clients. The JSON-RPC layer
87    /// exposes it as the error object's `data.reason` field on errors whose
88    /// `data` was previously empty. `ObjectsDoubleUsed` is the exception: it
89    /// keeps its pre-existing conflicting-transactions `data` payload.
90    pub fn reason(&self) -> &'static str {
91        match self {
92            QuorumDriverError::QuorumDriverInternal(_) => "internal",
93            QuorumDriverError::InvalidUserSignature(_) => "invalid_user_signature",
94            QuorumDriverError::InvalidTransaction(_) => "invalid_transaction",
95            QuorumDriverError::RejectedByValidators(_) => "rejected_by_validators",
96            QuorumDriverError::ObjectsDoubleUsed { .. } => "objects_double_used",
97            QuorumDriverError::TimeoutBeforeFinality => "timeout_before_finality",
98            QuorumDriverError::FailedWithTransientErrorAfterMaximumAttempts { .. } => {
99                "failed_with_transient_error_after_maximum_attempts"
100            }
101            QuorumDriverError::NonRecoverableTransactionError { .. } => {
102                "non_recoverable_transaction_error"
103            }
104            QuorumDriverError::SystemOverload { .. } => "system_overload",
105            QuorumDriverError::TxAlreadyFinalizedWithDifferentUserSignatures => {
106                "tx_already_finalized_with_different_user_signatures"
107            }
108            QuorumDriverError::SystemOverloadRetryAfter { .. } => "system_overload_retry_after",
109        }
110    }
111
112    pub fn to_error_message(&self) -> String {
113        match self {
114            QuorumDriverError::InvalidUserSignature(err) => {
115                format!("Invalid user signature: {err}")
116            }
117            QuorumDriverError::InvalidTransaction(err)
118            | QuorumDriverError::RejectedByValidators(err) => {
119                format!("Invalid transaction: {err}")
120            }
121            QuorumDriverError::TxAlreadyFinalizedWithDifferentUserSignatures => {
122                "The transaction is already finalized but with different user signatures"
123                    .to_string()
124            }
125            QuorumDriverError::TimeoutBeforeFinality
126            | QuorumDriverError::FailedWithTransientErrorAfterMaximumAttempts { .. }
127            | QuorumDriverError::SystemOverload { .. }
128            | QuorumDriverError::SystemOverloadRetryAfter { .. } => self.to_string(),
129            QuorumDriverError::ObjectsDoubleUsed { conflicting_txes } => {
130                let weights: Vec<u64> =
131                    conflicting_txes.values().map(|(_, stake)| *stake).collect();
132                let remaining: u64 = TOTAL_VOTING_POWER - weights.iter().sum::<u64>();
133
134                // better version of above
135                let reason = if weights.iter().all(|w| remaining + w < QUORUM_THRESHOLD) {
136                    "equivocated until the next epoch"
137                } else {
138                    "reserved for another transaction"
139                };
140
141                format!(
142                    "Failed to sign transaction by a quorum of validators because one or more of its objects is {}. Other transactions locking these objects:\n{}",
143                    reason,
144                    conflicting_txes
145                        .iter()
146                        .sorted_by(|(_, (_, a)), (_, (_, b))| b.cmp(a))
147                        .map(|(digest, (_, stake))| format!(
148                            "- {} (stake {}.{})",
149                            digest,
150                            stake / 100,
151                            stake % 100,
152                        ))
153                        .join("\n"),
154                )
155            }
156            QuorumDriverError::NonRecoverableTransactionError { errors } => {
157                let new_errors: Vec<String> = errors
158                    .iter()
159                    // sort by total stake, descending, so users see the most prominent one
160                    // first
161                    .sorted_by(|(_, a, _), (_, b, _)| b.cmp(a))
162                    .filter_map(|(err, _, _)| {
163                        match &err {
164                            // Special handling of UserInputError:
165                            // ObjectNotFound and DependentPackageNotFound are considered
166                            // retryable errors but they have different treatment
167                            // in AuthorityAggregator.
168                            // The optimal fix would be to examine if the total stake
169                            // of ObjectNotFound/DependentPackageNotFound exceeds the
170                            // quorum threshold, but it takes a Committee here.
171                            // So, we take an easier route and consider them non-retryable
172                            // at all. Combining this with the sorting above, clients will
173                            // see the dominant error first.
174                            IotaError::UserInput { error } => Some(error.to_string()),
175                            _ => {
176                                if err.is_retryable().0 {
177                                    None
178                                } else {
179                                    Some(err.to_string())
180                                }
181                            }
182                        }
183                    })
184                    .collect();
185
186                assert!(
187                    !new_errors.is_empty(),
188                    "NonRecoverableTransactionError should have at least one non-retryable error"
189                );
190
191                let mut error_list = vec![];
192                for err in new_errors.iter() {
193                    error_list.push(format!("- {err}"));
194                }
195
196                format!(
197                    "Transaction execution failed due to issues with transaction inputs, please review the errors and try again:\n{}",
198                    error_list.join("\n")
199                )
200            }
201            QuorumDriverError::QuorumDriverInternal { .. } => {
202                "Internal error occurred while executing transaction.".to_string()
203            }
204        }
205    }
206}
207
208pub type GroupedErrors = Vec<(IotaError, StakeUnit, Vec<ConciseAuthorityPublicKeyBytes>)>;
209
210#[derive(Serialize, Deserialize, Clone, Debug)]
211pub enum ExecuteTransactionRequestType {
212    WaitForEffectsCert,
213    WaitForLocalExecution,
214}
215
216#[derive(Serialize, Deserialize, Clone, Debug)]
217pub enum EffectsFinalityInfo {
218    Certified(AuthorityStrongQuorumSignInfo),
219    Checkpointed(EpochId, CheckpointSequenceNumber),
220    /// A quorum of validators have acknowledged effects (used in
221    /// TransactionDriver flow).
222    QuorumExecuted(EpochId),
223
224    /// Effects from a single validator without quorum certification.
225    /// The caller MUST wait for local checkpoint execution before returning
226    /// these to the client, as they have not been certified by a quorum.
227    UncertifiedSingleValidator(EpochId),
228}
229
230/// When requested to execute a transaction with WaitForLocalExecution,
231/// TransactionOrchestrator attempts to execute this transaction locally
232/// after it is finalized. This value represents whether the transaction
233/// is confirmed to be executed on this node before the response returns.
234pub type IsTransactionExecutedLocally = bool;
235
236#[derive(Debug, Clone)]
237pub struct QuorumDriverResponse {
238    pub effects_cert: VerifiedCertifiedTransactionEffects,
239    // pub events: TransactionEvents,
240    pub events: Option<TransactionEvents>,
241    // Input objects will only be populated in the happy path
242    pub input_objects: Option<Vec<Object>>,
243    // Output objects will only be populated in the happy path
244    pub output_objects: Option<Vec<Object>>,
245    pub auxiliary_data: Option<Vec<u8>>,
246}
247
248#[derive(Serialize, Deserialize, Clone, Debug)]
249pub struct ExecuteTransactionRequestV1 {
250    pub transaction: TransactionEnvelope,
251
252    pub include_events: bool,
253    pub include_input_objects: bool,
254    pub include_output_objects: bool,
255    pub include_auxiliary_data: bool,
256}
257
258impl ExecuteTransactionRequestV1 {
259    pub fn new<T: Into<TransactionEnvelope>>(transaction: T) -> Self {
260        Self {
261            transaction: transaction.into(),
262            include_events: true,
263            include_input_objects: false,
264            include_output_objects: false,
265            include_auxiliary_data: false,
266        }
267    }
268}
269
270#[derive(Serialize, Deserialize, Clone, Debug)]
271pub struct ExecuteTransactionResponseV1 {
272    pub effects: FinalizedEffects,
273
274    pub events: Option<TransactionEvents>,
275    // Input objects will only be populated in the happy path
276    pub input_objects: Option<Vec<Object>>,
277    // Output objects will only be populated in the happy path
278    pub output_objects: Option<Vec<Object>>,
279    pub auxiliary_data: Option<Vec<u8>>,
280}
281
282#[derive(Serialize, Deserialize, Clone, Debug)]
283pub struct FinalizedEffects {
284    pub effects: TransactionEffects,
285    pub finality_info: EffectsFinalityInfo,
286}
287
288impl FinalizedEffects {
289    pub fn new_from_effects_cert(effects_cert: CertifiedTransactionEffects) -> Self {
290        let (data, sig) = effects_cert.into_data_and_sig();
291        Self {
292            effects: data,
293            finality_info: EffectsFinalityInfo::Certified(sig),
294        }
295    }
296
297    pub fn epoch(&self) -> EpochId {
298        match &self.finality_info {
299            EffectsFinalityInfo::Certified(cert) => cert.epoch,
300            EffectsFinalityInfo::Checkpointed(epoch, _)
301            | EffectsFinalityInfo::QuorumExecuted(epoch)
302            | EffectsFinalityInfo::UncertifiedSingleValidator(epoch) => *epoch,
303        }
304    }
305}