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