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