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