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 nonempty::NonEmpty;
8use once_cell::sync::Lazy;
9
10use crate::{
11    base_types::SequenceNumber, digests::TransactionDigest, execution_status::CongestedObjects,
12    execution_status::ExecutionErrorKind, transaction::CheckedInputObjects,
13};
14
15/// Execution inputs computed before running a transaction: whether to fail it early (and with
16/// which errors), plus context for gas charging. An execution input only - never serialized into
17/// `TransactionEffects`, so adding fields here does not change effects or their digests.
18#[derive(Debug, Clone)]
19pub struct ExecutionOrEarlyError {
20    early_errors: Option<NonEmpty<ExecutionErrorKind>>,
21    /// Accumulator (settlement) root version assigned to this transaction. Gates the mainnet
22    /// address-balance gas-smash short-circuit. Populated only for mainnet committed execution;
23    /// `None` elsewhere, leaving that gate inert.
24    accumulator_version: Option<SequenceNumber>,
25}
26
27impl ExecutionOrEarlyError {
28    /// Execute the transaction normally (no predetermined early error).
29    pub fn ok(accumulator_version: Option<SequenceNumber>) -> Self {
30        Self {
31            early_errors: None,
32            accumulator_version,
33        }
34    }
35
36    /// Skip execution and fail the transaction with `errors`.
37    pub fn failed(
38        errors: NonEmpty<ExecutionErrorKind>,
39        accumulator_version: Option<SequenceNumber>,
40    ) -> Self {
41        Self {
42            early_errors: Some(errors),
43            accumulator_version,
44        }
45    }
46
47    pub fn is_ok(&self) -> bool {
48        self.early_errors.is_none()
49    }
50
51    /// The predetermined early errors, if any.
52    pub fn early_errors(&self) -> Option<&NonEmpty<ExecutionErrorKind>> {
53        self.early_errors.as_ref()
54    }
55
56    /// Consume self, returning the predetermined early errors, if any.
57    pub fn into_early_errors(self) -> Option<NonEmpty<ExecutionErrorKind>> {
58        self.early_errors
59    }
60
61    pub fn accumulator_version(&self) -> Option<SequenceNumber> {
62        self.accumulator_version
63    }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum FundsWithdrawStatus {
68    /// Either we don't know yet whether the funds withdrawals are sufficient or not,
69    /// or we know for sure that the funds withdrawals are sufficient.
70    /// The reason we don't need to distinguish between unknown and sufficient funds is that
71    /// in either case we would have to go ahead and execute the transaction anyway.
72    MaybeSufficient,
73    // TODO(address-funds): Add information on the address and type?
74    /// We know for sure that the funds withdrawals in this transaction do not all have enough funds.
75    /// This takes account of both address and object funds withdrawals.
76    Insufficient,
77}
78
79/// Determine if a transaction is predetermined to fail execution.
80/// Returns all matching error kinds, or `None` if there is no early failure.
81/// When we pass this to the execution engine, we will not execute the transaction
82/// if it is predetermined to fail execution.
83pub fn get_early_execution_error(
84    transaction_digest: &TransactionDigest,
85    input_objects: &CheckedInputObjects,
86    config_certificate_deny_set: &HashSet<TransactionDigest>,
87    funds_withdraw_status: &FundsWithdrawStatus,
88) -> Option<NonEmpty<ExecutionErrorKind>> {
89    let mut errors = vec![];
90    if is_certificate_denied(transaction_digest, config_certificate_deny_set) {
91        errors.push(ExecutionErrorKind::CertificateDenied);
92    }
93
94    if input_objects
95        .inner()
96        .contains_consensus_stream_ended_objects()
97    {
98        errors.push(ExecutionErrorKind::InputObjectDeleted);
99    }
100
101    let cancelled_objects = input_objects.inner().get_cancelled_objects();
102    if let Some((cancelled_objects, reason)) = cancelled_objects {
103        match reason {
104            SequenceNumber::CONGESTED => {
105                errors.push(
106                    ExecutionErrorKind::ExecutionCancelledDueToSharedObjectCongestion {
107                        congested_objects: CongestedObjects(cancelled_objects),
108                    },
109                );
110            }
111            SequenceNumber::RANDOMNESS_UNAVAILABLE => {
112                errors.push(ExecutionErrorKind::ExecutionCancelledDueToRandomnessUnavailable);
113            }
114            _ => panic!("invalid cancellation reason SequenceNumber: {reason}"),
115        }
116    }
117
118    if matches!(funds_withdraw_status, FundsWithdrawStatus::Insufficient) {
119        assert_reachable!("insufficient funds for withdraw");
120        errors.push(ExecutionErrorKind::InsufficientFundsForWithdraw);
121    }
122
123    NonEmpty::from_vec(errors)
124}
125
126/// If a transaction digest shows up in this list, when executing such transaction,
127/// we will always return `ExecutionError::CertificateDenied` without executing it (but still do
128/// gas smashing). Because this list is not gated by protocol version, there are a few important
129/// criteria for adding a digest to this list:
130/// 1. The certificate must be causing all validators to either panic or hang forever deterministically.
131/// 2. If we ever ship a fix to make it no longer panic or hang when executing such transaction, we
132///    must make sure the transaction is already in this list. Otherwise nodes running the newer
133///    version without these transactions in the list will generate forked result.
134///
135/// Below is a scenario of when we need to use this list:
136/// 1. We detect that a specific transaction is causing all validators to either panic or hang forever deterministically.
137/// 2. We push a CertificateDenyConfig to deny such transaction to all validators asap.
138/// 3. To make sure that all fullnodes are able to sync to the latest version, we need to add the
139///    transaction digest to this list as well asap, and ship this binary to all fullnodes, so that
140///    they can sync past this transaction.
141/// 4. We then can start fixing the issue, and ship the fix to all nodes.
142/// 5. Unfortunately, we can't remove the transaction digest from this list, because if we do so,
143///    any future node that sync from genesis will fork on this transaction. We may be able to
144///    remove it once we have stable snapshots and the binary has a minimum supported protocol
145///    version past the epoch.
146fn get_denied_certificates() -> &'static HashSet<TransactionDigest> {
147    static DENIED_CERTIFICATES: Lazy<HashSet<TransactionDigest>> = Lazy::new(|| HashSet::from([]));
148    Lazy::force(&DENIED_CERTIFICATES)
149}
150
151// This is needed to initialize static variables in the simtest environment.
152#[cfg(msim)]
153pub fn get_denied_certificates_for_sim_test() -> &'static HashSet<TransactionDigest> {
154    get_denied_certificates()
155}
156
157fn is_certificate_denied(
158    transaction_digest: &TransactionDigest,
159    certificate_deny_set: &HashSet<TransactionDigest>,
160) -> bool {
161    certificate_deny_set.contains(transaction_digest)
162        || get_denied_certificates().contains(transaction_digest)
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::{
169        base_types::ObjectID,
170        transaction::{
171            CheckedInputObjects, InputObjectKind, InputObjects, ObjectReadResult,
172            ObjectReadResultKind, SharedObjectMutability,
173        },
174    };
175
176    fn create_test_input_objects() -> CheckedInputObjects {
177        let input_objects = InputObjects::new(vec![]);
178        CheckedInputObjects::new_for_replay(input_objects)
179    }
180
181    #[test]
182    fn test_early_execution_error_insufficient_balance() {
183        let tx_digest = crate::digests::TransactionDigest::random();
184        let input_objects = create_test_input_objects();
185        let deny_set = HashSet::new();
186
187        // Test with insufficient balance
188        let result = get_early_execution_error(
189            &tx_digest,
190            &input_objects,
191            &deny_set,
192            &FundsWithdrawStatus::Insufficient,
193        );
194        assert_eq!(
195            result.unwrap().into_iter().collect::<Vec<_>>(),
196            vec![ExecutionErrorKind::InsufficientFundsForWithdraw],
197        );
198
199        // Test with sufficient balance
200        let result = get_early_execution_error(
201            &tx_digest,
202            &input_objects,
203            &deny_set,
204            &FundsWithdrawStatus::MaybeSufficient,
205        );
206        assert!(result.is_none());
207    }
208
209    #[test]
210    fn test_early_execution_error_collects_all() {
211        let tx_digest = crate::digests::TransactionDigest::random();
212        let input_objects = create_test_input_objects();
213
214        // Certificate denial + insufficient balance collected together.
215        let mut deny_set = HashSet::new();
216        deny_set.insert(tx_digest);
217        let result = get_early_execution_error(
218            &tx_digest,
219            &input_objects,
220            &deny_set,
221            &FundsWithdrawStatus::Insufficient,
222        );
223        assert_eq!(
224            result.unwrap().into_iter().collect::<Vec<_>>(),
225            vec![
226                ExecutionErrorKind::CertificateDenied,
227                ExecutionErrorKind::InsufficientFundsForWithdraw,
228            ],
229        );
230
231        // Deleted input objects + insufficient balance.
232        let input_objects = InputObjects::new(vec![ObjectReadResult {
233            input_object_kind: InputObjectKind::SharedMoveObject {
234                id: ObjectID::random(),
235                initial_shared_version: SequenceNumber::MIN,
236                mutability: SharedObjectMutability::Immutable,
237            },
238            object: ObjectReadResultKind::ObjectConsensusStreamEnded(
239                SequenceNumber::MIN, // doesn't matter
240                tx_digest,
241            ),
242        }]);
243        deny_set.clear();
244        let result = get_early_execution_error(
245            &tx_digest,
246            &CheckedInputObjects::new_for_replay(input_objects),
247            &deny_set,
248            &FundsWithdrawStatus::Insufficient,
249        );
250        assert_eq!(
251            result.unwrap().into_iter().collect::<Vec<_>>(),
252            vec![
253                ExecutionErrorKind::InputObjectDeleted,
254                ExecutionErrorKind::InsufficientFundsForWithdraw,
255            ],
256        );
257
258        // Cancelled (congestion) + insufficient balance.
259        let input_objects = InputObjects::new(vec![ObjectReadResult {
260            input_object_kind: InputObjectKind::SharedMoveObject {
261                id: ObjectID::random(),
262                initial_shared_version: SequenceNumber::MIN,
263                mutability: SharedObjectMutability::Immutable,
264            },
265            object: ObjectReadResultKind::CancelledTransactionSharedObject(
266                SequenceNumber::CONGESTED,
267            ),
268        }]);
269        let result = get_early_execution_error(
270            &tx_digest,
271            &CheckedInputObjects::new_for_replay(input_objects),
272            &deny_set,
273            &FundsWithdrawStatus::Insufficient,
274        );
275        let result: Vec<_> = result.unwrap().into_iter().collect();
276        assert_eq!(result.len(), 2);
277        assert!(matches!(
278            result[0],
279            ExecutionErrorKind::ExecutionCancelledDueToSharedObjectCongestion { .. }
280        ));
281        assert_eq!(result[1], ExecutionErrorKind::InsufficientFundsForWithdraw);
282    }
283}