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