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                // The aggregator builds this variant only from errors with
187                // non-retryable stake, but rendering an error must never
188                // panic: fall back to the unfiltered list.
189                if new_errors.is_empty() {
190                    let error_list = errors.iter().map(|(err, _, _)| err.to_string()).join(", ");
191                    return format!("{NON_RECOVERABLE_ERROR_MSG}: {error_list}.");
192                }
193
194                let mut error_list = vec![];
195                for err in new_errors.iter() {
196                    error_list.push(format!("- {err}"));
197                }
198
199                format!(
200                    "Transaction execution failed due to issues with transaction inputs, please review the errors and try again:\n{}",
201                    error_list.join("\n")
202                )
203            }
204            QuorumDriverError::QuorumDriverInternal { .. } => {
205                "Internal error occurred while executing transaction.".to_string()
206            }
207        }
208    }
209}
210
211pub type GroupedErrors = Vec<(IotaError, StakeUnit, Vec<ConciseAuthorityPublicKeyBytes>)>;
212
213#[derive(Serialize, Deserialize, Clone, Debug)]
214pub enum ExecuteTransactionRequestType {
215    WaitForEffectsCert,
216    WaitForLocalExecution,
217}
218
219#[derive(Serialize, Deserialize, Clone, Debug)]
220pub enum EffectsFinalityInfo {
221    Certified(AuthorityStrongQuorumSignInfo),
222    Checkpointed(EpochId, CheckpointSequenceNumber),
223    /// A quorum of validators have acknowledged effects (used in
224    /// TransactionDriver flow).
225    QuorumExecuted(EpochId),
226
227    /// Effects from a single validator without quorum certification.
228    /// The caller MUST wait for local checkpoint execution before returning
229    /// these to the client, as they have not been certified by a quorum.
230    UncertifiedSingleValidator(EpochId),
231}
232
233/// When requested to execute a transaction with WaitForLocalExecution,
234/// TransactionOrchestrator attempts to execute this transaction locally
235/// after it is finalized. This value represents whether the transaction
236/// is confirmed to be executed on this node before the response returns.
237pub type IsTransactionExecutedLocally = bool;
238
239#[derive(Debug, Clone)]
240pub struct QuorumDriverResponse {
241    pub effects_cert: VerifiedCertifiedTransactionEffects,
242    // pub events: TransactionEvents,
243    pub events: Option<TransactionEvents>,
244    // Input objects will only be populated in the happy path
245    pub input_objects: Option<Vec<Object>>,
246    // Output objects will only be populated in the happy path
247    pub output_objects: Option<Vec<Object>>,
248    pub auxiliary_data: Option<Vec<u8>>,
249}
250
251#[derive(Serialize, Deserialize, Clone, Debug)]
252pub struct ExecuteTransactionRequestV1 {
253    pub transaction: TransactionEnvelope,
254
255    pub include_events: bool,
256    pub include_input_objects: bool,
257    pub include_output_objects: bool,
258    pub include_auxiliary_data: bool,
259}
260
261impl ExecuteTransactionRequestV1 {
262    pub fn new<T: Into<TransactionEnvelope>>(transaction: T) -> Self {
263        Self {
264            transaction: transaction.into(),
265            include_events: true,
266            include_input_objects: false,
267            include_output_objects: false,
268            include_auxiliary_data: false,
269        }
270    }
271}
272
273#[derive(Serialize, Deserialize, Clone, Debug)]
274pub struct ExecuteTransactionResponseV1 {
275    pub effects: FinalizedEffects,
276
277    pub events: Option<TransactionEvents>,
278    // Input objects will only be populated in the happy path
279    pub input_objects: Option<Vec<Object>>,
280    // Output objects will only be populated in the happy path
281    pub output_objects: Option<Vec<Object>>,
282    pub auxiliary_data: Option<Vec<u8>>,
283}
284
285#[derive(Serialize, Deserialize, Clone, Debug)]
286pub struct FinalizedEffects {
287    pub effects: TransactionEffects,
288    pub finality_info: EffectsFinalityInfo,
289}
290
291impl FinalizedEffects {
292    pub fn new_from_effects_cert(effects_cert: CertifiedTransactionEffects) -> Self {
293        let (data, sig) = effects_cert.into_data_and_sig();
294        Self {
295            effects: data,
296            finality_info: EffectsFinalityInfo::Certified(sig),
297        }
298    }
299
300    pub fn epoch(&self) -> EpochId {
301        match &self.finality_info {
302            EffectsFinalityInfo::Certified(cert) => cert.epoch,
303            EffectsFinalityInfo::Checkpointed(epoch, _)
304            | EffectsFinalityInfo::QuorumExecuted(epoch)
305            | EffectsFinalityInfo::UncertifiedSingleValidator(epoch) => *epoch,
306        }
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    /// Rendering must not panic when filtering leaves no non-retryable
315    /// error.
316    #[test]
317    fn non_recoverable_error_rendering_survives_empty_filter() {
318        let error = QuorumDriverError::NonRecoverableTransactionError {
319            errors: vec![(
320                IotaError::ValidatorOverloadedRetryAfter {
321                    retry_after_secs: 1,
322                },
323                0,
324                vec![],
325            )],
326        };
327        assert!(error.to_error_message().contains(NON_RECOVERABLE_ERROR_MSG));
328    }
329}