Skip to main content

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