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