sui_transaction_checks/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4pub mod deny;
5
6pub use checked::*;
7
8#[sui_macros::with_checked_arithmetic]
9mod checked {
10    use std::collections::{BTreeMap, HashSet};
11    use std::sync::Arc;
12    use sui_config::verifier_signing_config::VerifierSigningConfig;
13    use sui_protocol_config::ProtocolConfig;
14    use sui_types::base_types::{ObjectID, ObjectRef};
15    use sui_types::error::{SuiResult, UserInputError, UserInputResult};
16    use sui_types::executable_transaction::VerifiedExecutableTransaction;
17    use sui_types::metrics::BytecodeVerifierMetrics;
18    use sui_types::transaction::{
19        CheckedInputObjects, InputObjectKind, InputObjects, ObjectReadResult, ObjectReadResultKind,
20        ReceivingObjectReadResult, ReceivingObjects, SharedObjectMutability, TransactionData,
21        TransactionDataAPI, TransactionKind,
22    };
23    use sui_types::{
24        SUI_AUTHENTICATOR_STATE_OBJECT_ID, SUI_CLOCK_OBJECT_ID, SUI_CLOCK_OBJECT_SHARED_VERSION,
25        SUI_RANDOMNESS_STATE_OBJECT_ID,
26    };
27    use sui_types::{
28        base_types::{SequenceNumber, SuiAddress},
29        error::SuiError,
30        fp_bail, fp_ensure,
31        gas::SuiGasStatus,
32        object::{Object, Owner},
33    };
34    use tracing::error;
35    use tracing::instrument;
36
37    trait IntoChecked {
38        fn into_checked(self) -> CheckedInputObjects;
39    }
40
41    impl IntoChecked for InputObjects {
42        fn into_checked(self) -> CheckedInputObjects {
43            CheckedInputObjects::new_with_checked_transaction_inputs(self)
44        }
45    }
46
47    // Entry point for all checks related to gas.
48    // Called on both signing and execution.
49    // On success the gas part of the transaction (gas data and gas coins)
50    // is verified and good to go
51    fn get_gas_status(
52        objects: &InputObjects,
53        gas: &[ObjectRef],
54        protocol_config: &ProtocolConfig,
55        reference_gas_price: u64,
56        transaction: &TransactionData,
57        gas_ownership_checks: bool,
58    ) -> SuiResult<SuiGasStatus> {
59        if transaction.kind().is_system_tx() {
60            Ok(SuiGasStatus::new_unmetered())
61        } else {
62            check_gas(
63                objects,
64                protocol_config,
65                reference_gas_price,
66                gas,
67                transaction,
68                gas_ownership_checks,
69            )
70        }
71    }
72
73    #[instrument(level = "trace", skip_all)]
74    pub fn check_transaction_input(
75        protocol_config: &ProtocolConfig,
76        reference_gas_price: u64,
77        transaction: &TransactionData,
78        input_objects: InputObjects,
79        receiving_objects: &ReceivingObjects,
80        metrics: &Arc<BytecodeVerifierMetrics>,
81        verifier_signing_config: &VerifierSigningConfig,
82    ) -> SuiResult<(SuiGasStatus, CheckedInputObjects)> {
83        let gas_status = check_transaction_input_inner(
84            protocol_config,
85            reference_gas_price,
86            transaction,
87            &input_objects,
88            &[],
89        )?;
90        check_receiving_objects(&input_objects, receiving_objects)?;
91        // Runs verifier, which could be expensive.
92        check_non_system_packages_to_be_published(
93            transaction,
94            protocol_config,
95            metrics,
96            verifier_signing_config,
97        )?;
98
99        Ok((gas_status, input_objects.into_checked()))
100    }
101
102    pub fn check_transaction_input_with_given_gas(
103        protocol_config: &ProtocolConfig,
104        reference_gas_price: u64,
105        transaction: &TransactionData,
106        mut input_objects: InputObjects,
107        receiving_objects: ReceivingObjects,
108        gas_object: Object,
109        metrics: &Arc<BytecodeVerifierMetrics>,
110        verifier_signing_config: &VerifierSigningConfig,
111    ) -> SuiResult<(SuiGasStatus, CheckedInputObjects)> {
112        let gas_object_ref = gas_object.compute_object_reference();
113        input_objects.push(ObjectReadResult::new_from_gas_object(&gas_object));
114
115        let gas_status = check_transaction_input_inner(
116            protocol_config,
117            reference_gas_price,
118            transaction,
119            &input_objects,
120            &[gas_object_ref],
121        )?;
122        check_receiving_objects(&input_objects, &receiving_objects)?;
123        // Runs verifier, which could be expensive.
124        check_non_system_packages_to_be_published(
125            transaction,
126            protocol_config,
127            metrics,
128            verifier_signing_config,
129        )?;
130
131        Ok((gas_status, input_objects.into_checked()))
132    }
133
134    // Since the purpose of this function is to audit certified transactions,
135    // the checks here should be a strict subset of the checks in check_transaction_input().
136    // For checks not performed in this function but in check_transaction_input(),
137    // we should add a comment calling out the difference.
138    #[instrument(level = "trace", skip_all)]
139    pub fn check_certificate_input(
140        cert: &VerifiedExecutableTransaction,
141        input_objects: InputObjects,
142        protocol_config: &ProtocolConfig,
143        reference_gas_price: u64,
144    ) -> SuiResult<(SuiGasStatus, CheckedInputObjects)> {
145        let transaction = cert.data().transaction_data();
146        let gas_status = check_transaction_input_inner(
147            protocol_config,
148            reference_gas_price,
149            transaction,
150            &input_objects,
151            &[],
152        )?;
153        // NB: We do not check receiving objects when executing. Only at signing time do we check.
154        // NB: move verifier is only checked at signing time, not at execution.
155
156        Ok((gas_status, input_objects.into_checked()))
157    }
158
159    /// WARNING! This should only be used for the dev-inspect transaction. This transaction type
160    /// bypasses many of the normal object checks
161    pub fn check_dev_inspect_input(
162        config: &ProtocolConfig,
163        transaction: &TransactionData,
164        input_objects: InputObjects,
165        // TODO: check ReceivingObjects for dev inspect?
166        _receiving_objects: ReceivingObjects,
167        reference_gas_price: u64,
168    ) -> SuiResult<(SuiGasStatus, CheckedInputObjects)> {
169        let kind = transaction.kind();
170        kind.validity_check(config)?;
171        if kind.is_system_tx() {
172            return Err(UserInputError::Unsupported(format!(
173                "Transaction kind {} is not supported in dev-inspect",
174                kind
175            ))
176            .into());
177        }
178        let mut used_objects: HashSet<SuiAddress> = HashSet::new();
179        for input_object in input_objects.iter() {
180            let Some(object) = input_object.as_object() else {
181                // object was deleted
182                continue;
183            };
184
185            if !object.is_immutable() {
186                fp_ensure!(
187                    used_objects.insert(object.id().into()),
188                    UserInputError::MutableObjectUsedMoreThanOnce {
189                        object_id: object.id()
190                    }
191                    .into()
192                );
193            }
194        }
195
196        let gas_status = get_gas_status(
197            &input_objects,
198            &transaction.gas_data().payment, //gas,
199            config,
200            reference_gas_price,
201            transaction,
202            false, // gas_ownership_checks - false means mostly transaction level checks
203        )?;
204
205        Ok((gas_status, input_objects.into_checked()))
206    }
207
208    // Common checks performed for transactions and certificates.
209    fn check_transaction_input_inner(
210        protocol_config: &ProtocolConfig,
211        reference_gas_price: u64,
212        transaction: &TransactionData,
213        input_objects: &InputObjects,
214        // Overrides the gas objects in the transaction.
215        gas_override: &[ObjectRef],
216    ) -> SuiResult<SuiGasStatus> {
217        let gas = if gas_override.is_empty() {
218            transaction.gas()
219        } else {
220            gas_override
221        };
222
223        let gas_status = get_gas_status(
224            input_objects,
225            gas,
226            protocol_config,
227            reference_gas_price,
228            transaction,
229            true, // gas_ownership_checks
230        )?;
231        check_objects(transaction, input_objects)?;
232
233        Ok(gas_status)
234    }
235
236    fn check_receiving_objects(
237        input_objects: &InputObjects,
238        receiving_objects: &ReceivingObjects,
239    ) -> Result<(), SuiError> {
240        let mut objects_in_txn: HashSet<_> = input_objects
241            .object_kinds()
242            .map(|x| x.object_id())
243            .collect();
244
245        // Since we're at signing we check that every object reference that we are receiving is the
246        // most recent version of that object. If it's been received at the version specified we
247        // let it through to allow the transaction to run and fail to unlock any other objects in
248        // the transaction. Otherwise, we return an error.
249        //
250        // If there are any object IDs in common (either between receiving objects and input
251        // objects) we return an error.
252        for ReceivingObjectReadResult {
253            object_ref: (object_id, version, object_digest),
254            object,
255        } in receiving_objects.iter()
256        {
257            fp_ensure!(
258                *version < SequenceNumber::MAX,
259                UserInputError::InvalidSequenceNumber.into()
260            );
261
262            let Some(object) = object.as_object() else {
263                // object was previously received
264                continue;
265            };
266
267            if !(object.owner.is_address_owned()
268                && object.version() == *version
269                && object.digest() == *object_digest)
270            {
271                // Version mismatch
272                fp_ensure!(
273                    object.version() == *version,
274                    UserInputError::ObjectVersionUnavailableForConsumption {
275                        provided_obj_ref: (*object_id, *version, *object_digest),
276                        current_version: object.version(),
277                    }
278                    .into()
279                );
280
281                // Tried to receive a package
282                fp_ensure!(
283                    !object.is_package(),
284                    UserInputError::MovePackageAsObject {
285                        object_id: *object_id
286                    }
287                    .into()
288                );
289
290                // Digest mismatch
291                let expected_digest = object.digest();
292                fp_ensure!(
293                    expected_digest == *object_digest,
294                    UserInputError::InvalidObjectDigest {
295                        object_id: *object_id,
296                        expected_digest
297                    }
298                    .into()
299                );
300
301                match object.owner {
302                    Owner::AddressOwner(_) => {
303                        debug_assert!(
304                            false,
305                            "Receiving object {:?} is invalid but we expect it should be valid. {:?}",
306                            (*object_id, *version, *object_id),
307                            object
308                        );
309                        error!(
310                            "Receiving object {:?} is invalid but we expect it should be valid. {:?}",
311                            (*object_id, *version, *object_id),
312                            object
313                        );
314                        // We should never get here, but if for some reason we do just default to
315                        // object not found and reject signing the transaction.
316                        fp_bail!(
317                            UserInputError::ObjectNotFound {
318                                object_id: *object_id,
319                                version: Some(*version),
320                            }
321                            .into()
322                        )
323                    }
324                    Owner::ObjectOwner(owner) => {
325                        fp_bail!(
326                            UserInputError::InvalidChildObjectArgument {
327                                child_id: object.id(),
328                                parent_id: owner.into(),
329                            }
330                            .into()
331                        )
332                    }
333                    Owner::Shared { .. } | Owner::ConsensusAddressOwner { .. } => {
334                        fp_bail!(UserInputError::NotSharedObjectError.into())
335                    }
336                    Owner::Immutable => fp_bail!(
337                        UserInputError::MutableParameterExpected {
338                            object_id: *object_id
339                        }
340                        .into()
341                    ),
342                };
343            }
344
345            fp_ensure!(
346                !objects_in_txn.contains(object_id),
347                UserInputError::DuplicateObjectRefInput.into()
348            );
349
350            objects_in_txn.insert(*object_id);
351        }
352        Ok(())
353    }
354
355    /// Check transaction gas data/info and gas coins consistency.
356    /// Return the gas status to be used for the lifecycle of the transaction.
357    #[instrument(level = "trace", skip_all)]
358    fn check_gas(
359        objects: &InputObjects,
360        protocol_config: &ProtocolConfig,
361        reference_gas_price: u64,
362        gas: &[ObjectRef],
363        transaction: &TransactionData,
364        gas_ownership_checks: bool,
365    ) -> SuiResult<SuiGasStatus> {
366        let gas_budget = transaction.gas_budget();
367        let gas_price = transaction.gas_price();
368        let gas_paid_from_address_balance = transaction.is_gas_paid_from_address_balance();
369
370        let gas_status =
371            SuiGasStatus::new(gas_budget, gas_price, reference_gas_price, protocol_config)?;
372
373        // check balance and coins consistency
374        // load all gas coins
375        let objects: BTreeMap<_, _> = objects.iter().map(|o| (o.id(), o)).collect();
376        let mut gas_objects = vec![];
377        for obj_ref in gas {
378            let obj = objects.get(&obj_ref.0);
379            let obj = *obj.ok_or(UserInputError::ObjectNotFound {
380                object_id: obj_ref.0,
381                version: Some(obj_ref.1),
382            })?;
383            gas_objects.push(obj);
384        }
385
386        // Skip gas balance check for address balance payments
387        // We reserve gas budget in advance
388        if !gas_paid_from_address_balance {
389            if gas_ownership_checks {
390                gas_status.check_gas_objects(&gas_objects)?;
391            }
392            gas_status.check_gas_data(&gas_objects, gas_budget)?;
393        }
394        Ok(gas_status)
395    }
396
397    /// Check all the objects used in the transaction against the database, and ensure
398    /// that they are all the correct version and number.
399    #[instrument(level = "trace", skip_all)]
400    fn check_objects(transaction: &TransactionData, objects: &InputObjects) -> UserInputResult<()> {
401        // We require that mutable objects cannot show up more than once.
402        let mut used_objects: HashSet<SuiAddress> = HashSet::new();
403        for object in objects.iter() {
404            if object.is_mutable() {
405                fp_ensure!(
406                    used_objects.insert(object.id().into()),
407                    UserInputError::MutableObjectUsedMoreThanOnce {
408                        object_id: object.id()
409                    }
410                );
411            }
412        }
413
414        if !transaction.is_genesis_tx() && objects.is_empty() {
415            return Err(UserInputError::ObjectInputArityViolation);
416        }
417
418        let gas_coins: HashSet<ObjectID> =
419            HashSet::from_iter(transaction.gas().iter().map(|obj_ref| obj_ref.0));
420        for object in objects.iter() {
421            let input_object_kind = object.input_object_kind;
422
423            match &object.object {
424                ObjectReadResultKind::Object(object) => {
425                    // For Gas Object, we check the object is owned by gas owner
426                    let owner_address = if gas_coins.contains(&object.id()) {
427                        transaction.gas_owner()
428                    } else {
429                        transaction.sender()
430                    };
431                    // Check if the object contents match the type of lock we need for
432                    // this object.
433                    let system_transaction = transaction.is_system_tx();
434                    check_one_object(
435                        &owner_address,
436                        input_object_kind,
437                        object,
438                        system_transaction,
439                    )?;
440                }
441                // We skip checking a removed consensus object because it no longer exists.
442                ObjectReadResultKind::ObjectConsensusStreamEnded(_, _) => (),
443                // We skip checking shared objects from cancelled transactions since we are not reading it.
444                ObjectReadResultKind::CancelledTransactionSharedObject(_) => (),
445            }
446        }
447
448        Ok(())
449    }
450
451    /// Check one object against a reference
452    fn check_one_object(
453        owner: &SuiAddress,
454        object_kind: InputObjectKind,
455        object: &Object,
456        system_transaction: bool,
457    ) -> UserInputResult {
458        match object_kind {
459            InputObjectKind::MovePackage(package_id) => {
460                fp_ensure!(
461                    object.data.try_as_package().is_some(),
462                    UserInputError::MoveObjectAsPackage {
463                        object_id: package_id
464                    }
465                );
466            }
467            InputObjectKind::ImmOrOwnedMoveObject((object_id, sequence_number, object_digest)) => {
468                fp_ensure!(
469                    !object.is_package(),
470                    UserInputError::MovePackageAsObject { object_id }
471                );
472                fp_ensure!(
473                    sequence_number < SequenceNumber::MAX,
474                    UserInputError::InvalidSequenceNumber
475                );
476
477                // This is an invariant - we just load the object with the given ID and version.
478                assert_eq!(
479                    object.version(),
480                    sequence_number,
481                    "The fetched object version {} does not match the requested version {}, object id: {}",
482                    object.version(),
483                    sequence_number,
484                    object.id(),
485                );
486
487                // Check the digest matches - user could give a mismatched ObjectDigest
488                let expected_digest = object.digest();
489                fp_ensure!(
490                    expected_digest == object_digest,
491                    UserInputError::InvalidObjectDigest {
492                        object_id,
493                        expected_digest
494                    }
495                );
496
497                match object.owner {
498                    Owner::Immutable => {
499                        // Nothing else to check for Immutable.
500                    }
501                    Owner::AddressOwner(actual_owner) => {
502                        // Check the owner is correct.
503                        fp_ensure!(
504                            owner == &actual_owner,
505                            UserInputError::IncorrectUserSignature {
506                                error: format!(
507                                    "Object {object_id:?} is owned by account address {actual_owner:?}, but given owner/signer address is {owner:?}"
508                                ),
509                            }
510                        );
511                    }
512                    Owner::ObjectOwner(owner) => {
513                        return Err(UserInputError::InvalidChildObjectArgument {
514                            child_id: object.id(),
515                            parent_id: owner.into(),
516                        });
517                    }
518                    Owner::Shared { .. } | Owner::ConsensusAddressOwner { .. } => {
519                        // This object is a mutable consensus object. However the transaction
520                        // specifies it as an owned object. This is inconsistent.
521                        return Err(UserInputError::NotOwnedObjectError);
522                    }
523                };
524            }
525            InputObjectKind::SharedMoveObject {
526                id: SUI_CLOCK_OBJECT_ID,
527                initial_shared_version: SUI_CLOCK_OBJECT_SHARED_VERSION,
528                mutability: SharedObjectMutability::Mutable,
529            } => {
530                // Only system transactions can accept the Clock
531                // object as a mutable parameter.
532                if system_transaction {
533                    return Ok(());
534                } else {
535                    return Err(UserInputError::ImmutableParameterExpectedError {
536                        object_id: SUI_CLOCK_OBJECT_ID,
537                    });
538                }
539            }
540            InputObjectKind::SharedMoveObject {
541                id: SUI_AUTHENTICATOR_STATE_OBJECT_ID,
542                ..
543            } => {
544                if system_transaction {
545                    return Ok(());
546                } else {
547                    return Err(UserInputError::InaccessibleSystemObject {
548                        object_id: SUI_AUTHENTICATOR_STATE_OBJECT_ID,
549                    });
550                }
551            }
552            InputObjectKind::SharedMoveObject {
553                id: SUI_RANDOMNESS_STATE_OBJECT_ID,
554                mutability: SharedObjectMutability::Mutable,
555                ..
556            } => {
557                // Only system transactions can accept the Random
558                // object as a mutable parameter.
559                if system_transaction {
560                    return Ok(());
561                } else {
562                    return Err(UserInputError::ImmutableParameterExpectedError {
563                        object_id: SUI_RANDOMNESS_STATE_OBJECT_ID,
564                    });
565                }
566            }
567            InputObjectKind::SharedMoveObject {
568                id: object_id,
569                initial_shared_version: input_initial_shared_version,
570                ..
571            } => {
572                fp_ensure!(
573                    object.version() < SequenceNumber::MAX,
574                    UserInputError::InvalidSequenceNumber
575                );
576
577                match &object.owner {
578                    Owner::AddressOwner(_) | Owner::ObjectOwner(_) | Owner::Immutable => {
579                        // When someone locks an object as shared it must be shared already.
580                        return Err(UserInputError::NotSharedObjectError);
581                    }
582                    Owner::Shared {
583                        initial_shared_version: actual_initial_shared_version,
584                    } => {
585                        fp_ensure!(
586                            input_initial_shared_version == *actual_initial_shared_version,
587                            UserInputError::SharedObjectStartingVersionMismatch
588                        )
589                    }
590                    Owner::ConsensusAddressOwner {
591                        start_version: actual_initial_shared_version,
592                        owner: actual_owner,
593                    } => {
594                        fp_ensure!(
595                            input_initial_shared_version == *actual_initial_shared_version,
596                            UserInputError::SharedObjectStartingVersionMismatch
597                        );
598                        // Check the owner is correct.
599                        fp_ensure!(
600                            owner == actual_owner,
601                            UserInputError::IncorrectUserSignature {
602                                error: format!(
603                                    "Object {object_id:?} is owned by account address {actual_owner:?}, but given owner/signer address is {owner:?}"
604                                ),
605                            }
606                        )
607                    }
608                }
609            }
610        };
611        Ok(())
612    }
613
614    /// Check package verification timeout
615    #[instrument(level = "trace", skip_all)]
616    pub fn check_non_system_packages_to_be_published(
617        transaction: &TransactionData,
618        protocol_config: &ProtocolConfig,
619        metrics: &Arc<BytecodeVerifierMetrics>,
620        verifier_signing_config: &VerifierSigningConfig,
621    ) -> UserInputResult<()> {
622        // Only meter non-system programmable transaction blocks
623        if transaction.is_system_tx() {
624            return Ok(());
625        }
626
627        let TransactionKind::ProgrammableTransaction(pt) = transaction.kind() else {
628            return Ok(());
629        };
630
631        // Use the same verifier and meter for all packages, custom configured for signing.
632        let signing_limits = Some(verifier_signing_config.limits_for_signing());
633        let mut verifier = sui_execution::verifier(protocol_config, signing_limits, metrics);
634        let mut meter = verifier.meter(verifier_signing_config.meter_config_for_signing());
635
636        // Measure time for verifying all packages in the PTB
637        let shared_meter_verifier_timer = metrics
638            .verifier_runtime_per_ptb_success_latency
639            .start_timer();
640
641        let verifier_status = pt
642            .non_system_packages_to_be_published()
643            .try_for_each(|module_bytes| {
644                verifier.meter_module_bytes(protocol_config, module_bytes, meter.as_mut())
645            })
646            .map_err(|e| UserInputError::PackageVerificationTimeout { err: e.to_string() });
647
648        match verifier_status {
649            Ok(_) => {
650                // Success: stop and record the success timer
651                shared_meter_verifier_timer.stop_and_record();
652            }
653            Err(err) => {
654                // Failure: redirect the success timers output to the failure timer and
655                // discard the success timer
656                metrics
657                    .verifier_runtime_per_ptb_timeout_latency
658                    .observe(shared_meter_verifier_timer.stop_and_discard());
659                return Err(err);
660            }
661        };
662
663        Ok(())
664    }
665}