sui_types/
execution_params.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::HashSet;
5
6use once_cell::sync::Lazy;
7
8use crate::{
9    base_types::SequenceNumber, digests::TransactionDigest, error::ExecutionErrorKind,
10    execution_status::CongestedObjects, transaction::CheckedInputObjects,
11};
12
13pub type ExecutionOrEarlyError = Result<(), ExecutionErrorKind>;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum BalanceWithdrawStatus {
17    NoWithdraw,
18    SufficientBalance,
19    // TODO(address-balances): Add information on the address and type?
20    InsufficientBalance,
21}
22
23/// Determine if a transaction is predetermined to fail execution.
24/// If so, return the error kind, otherwise return `None`.
25/// When we pass this to the execution engine, we will not execute the transaction
26/// if it is predetermined to fail execution.
27pub fn get_early_execution_error(
28    transaction_digest: &TransactionDigest,
29    input_objects: &CheckedInputObjects,
30    config_certificate_deny_set: &HashSet<TransactionDigest>,
31    balance_withdraw_status: &BalanceWithdrawStatus,
32) -> Option<ExecutionErrorKind> {
33    if is_certificate_denied(transaction_digest, config_certificate_deny_set) {
34        return Some(ExecutionErrorKind::CertificateDenied);
35    }
36
37    if input_objects
38        .inner()
39        .contains_consensus_stream_ended_objects()
40    {
41        return Some(ExecutionErrorKind::InputObjectDeleted);
42    }
43
44    let cancelled_objects = input_objects.inner().get_cancelled_objects();
45    if let Some((cancelled_objects, reason)) = cancelled_objects {
46        match reason {
47            SequenceNumber::CONGESTED => {
48                return Some(
49                    ExecutionErrorKind::ExecutionCancelledDueToSharedObjectCongestion {
50                        congested_objects: CongestedObjects(cancelled_objects),
51                    },
52                );
53            }
54            SequenceNumber::RANDOMNESS_UNAVAILABLE => {
55                return Some(ExecutionErrorKind::ExecutionCancelledDueToRandomnessUnavailable);
56            }
57            _ => panic!("invalid cancellation reason SequenceNumber: {reason}"),
58        }
59    }
60
61    if matches!(
62        balance_withdraw_status,
63        BalanceWithdrawStatus::InsufficientBalance
64    ) {
65        return Some(ExecutionErrorKind::InsufficientBalanceForWithdraw);
66    }
67
68    None
69}
70
71/// If a transaction digest shows up in this list, when executing such transaction,
72/// we will always return `ExecutionError::CertificateDenied` without executing it (but still do
73/// gas smashing). Because this list is not gated by protocol version, there are a few important
74/// criteria for adding a digest to this list:
75/// 1. The certificate must be causing all validators to either panic or hang forever deterministically.
76/// 2. If we ever ship a fix to make it no longer panic or hang when executing such transaction, we
77///    must make sure the transaction is already in this list. Otherwise nodes running the newer
78///    version without these transactions in the list will generate forked result.
79///
80/// Below is a scenario of when we need to use this list:
81/// 1. We detect that a specific transaction is causing all validators to either panic or hang forever deterministically.
82/// 2. We push a CertificateDenyConfig to deny such transaction to all validators asap.
83/// 3. To make sure that all fullnodes are able to sync to the latest version, we need to add the
84///    transaction digest to this list as well asap, and ship this binary to all fullnodes, so that
85///    they can sync past this transaction.
86/// 4. We then can start fixing the issue, and ship the fix to all nodes.
87/// 5. Unfortunately, we can't remove the transaction digest from this list, because if we do so,
88///    any future node that sync from genesis will fork on this transaction. We may be able to
89///    remove it once we have stable snapshots and the binary has a minimum supported protocol
90///    version past the epoch.
91fn get_denied_certificates() -> &'static HashSet<TransactionDigest> {
92    static DENIED_CERTIFICATES: Lazy<HashSet<TransactionDigest>> = Lazy::new(|| HashSet::from([]));
93    Lazy::force(&DENIED_CERTIFICATES)
94}
95
96// This is needed to initialize static variables in the simtest environment.
97#[cfg(msim)]
98pub fn get_denied_certificates_for_sim_test() -> &'static HashSet<TransactionDigest> {
99    get_denied_certificates()
100}
101
102fn is_certificate_denied(
103    transaction_digest: &TransactionDigest,
104    certificate_deny_set: &HashSet<TransactionDigest>,
105) -> bool {
106    certificate_deny_set.contains(transaction_digest)
107        || get_denied_certificates().contains(transaction_digest)
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::{
114        base_types::ObjectID,
115        transaction::{
116            CheckedInputObjects, InputObjectKind, InputObjects, ObjectReadResult,
117            ObjectReadResultKind, SharedObjectMutability,
118        },
119    };
120
121    fn create_test_input_objects() -> CheckedInputObjects {
122        let input_objects = InputObjects::new(vec![]);
123        CheckedInputObjects::new_for_replay(input_objects)
124    }
125
126    #[test]
127    fn test_early_execution_error_insufficient_balance() {
128        let tx_digest = crate::digests::TransactionDigest::random();
129        let input_objects = create_test_input_objects();
130        let deny_set = HashSet::new();
131
132        // Test with insufficient balance
133        let result = get_early_execution_error(
134            &tx_digest,
135            &input_objects,
136            &deny_set,
137            &BalanceWithdrawStatus::InsufficientBalance,
138        );
139        assert_eq!(
140            result,
141            Some(ExecutionErrorKind::InsufficientBalanceForWithdraw)
142        );
143
144        // Test with sufficient balance
145        let result = get_early_execution_error(
146            &tx_digest,
147            &input_objects,
148            &deny_set,
149            &BalanceWithdrawStatus::SufficientBalance,
150        );
151        assert_eq!(result, None);
152    }
153
154    #[test]
155    fn test_early_execution_error_precedence() {
156        let tx_digest = crate::digests::TransactionDigest::random();
157        let input_objects = create_test_input_objects();
158
159        // Test that certificate denial takes precedence over insufficient balance
160        let mut deny_set = HashSet::new();
161        deny_set.insert(tx_digest);
162        let result = get_early_execution_error(
163            &tx_digest,
164            &input_objects,
165            &deny_set,
166            &BalanceWithdrawStatus::InsufficientBalance,
167        );
168        assert_eq!(result, Some(ExecutionErrorKind::CertificateDenied));
169
170        // Test that deleted input objects take precedence over insufficient balance
171        let input_objects = InputObjects::new(vec![
172            // canceled object
173            ObjectReadResult {
174                input_object_kind: InputObjectKind::SharedMoveObject {
175                    id: ObjectID::random(),
176                    initial_shared_version: SequenceNumber::MIN,
177                    mutability: SharedObjectMutability::Immutable,
178                },
179                object: ObjectReadResultKind::ObjectConsensusStreamEnded(
180                    SequenceNumber::MIN, // doesn't matter
181                    tx_digest,
182                ),
183            },
184        ]);
185        deny_set.clear();
186        let result = get_early_execution_error(
187            &tx_digest,
188            &CheckedInputObjects::new_for_replay(input_objects),
189            &deny_set,
190            &BalanceWithdrawStatus::InsufficientBalance,
191        );
192        assert_eq!(result, Some(ExecutionErrorKind::InputObjectDeleted));
193
194        // Test that canceled takes precedence over insufficient balance
195        let input_objects = InputObjects::new(vec![
196            // canceled object
197            ObjectReadResult {
198                input_object_kind: InputObjectKind::SharedMoveObject {
199                    id: ObjectID::random(),
200                    initial_shared_version: SequenceNumber::MIN,
201                    mutability: SharedObjectMutability::Immutable,
202                },
203                object: ObjectReadResultKind::CancelledTransactionSharedObject(
204                    SequenceNumber::CONGESTED,
205                ),
206            },
207        ]);
208        let result = get_early_execution_error(
209            &tx_digest,
210            &CheckedInputObjects::new_for_replay(input_objects),
211            &deny_set,
212            &BalanceWithdrawStatus::InsufficientBalance,
213        );
214        assert!(matches!(
215            result,
216            Some(ExecutionErrorKind::ExecutionCancelledDueToSharedObjectCongestion { .. })
217        ));
218    }
219}