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