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