sui_transaction_checks/
lib.rs

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