Skip to main content

iota_transaction_checks/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5pub mod deny;
6
7pub use checked::*;
8
9#[iota_macros::with_checked_arithmetic]
10mod checked {
11    use std::{
12        collections::{BTreeMap, HashSet},
13        sync::Arc,
14    };
15
16    use iota_config::verifier_signing_config::VerifierSigningConfig;
17    use iota_protocol_config::ProtocolConfig;
18    use iota_sdk_types::ObjectId;
19    use iota_types::{
20        IOTA_AUTHENTICATOR_STATE_OBJECT_ID, IOTA_CLOCK_OBJECT_SHARED_VERSION,
21        base_types::{IotaAddress, ObjectRef, SequenceNumber},
22        error::{IotaError, IotaResult, UserInputError, UserInputResult},
23        executable_transaction::VerifiedExecutableTransaction,
24        fp_bail, fp_ensure,
25        gas::IotaGasStatus,
26        metrics::BytecodeVerifierMetrics,
27        object::{Object, Owner},
28        transaction::{
29            CheckedInputObjects, InputObjectKind, InputObjects, ObjectReadResult,
30            ObjectReadResultKind, ProgrammableTransactionExt, ReceivingObjectReadResult,
31            ReceivingObjects, TransactionData, TransactionDataAPI, TransactionKind,
32            TransactionKindExt,
33        },
34    };
35    use tracing::{error, instrument};
36
37    trait IntoChecked {
38        fn into_checked(self) -> CheckedInputObjects;
39    }
40
41    impl IntoChecked for InputObjects {
42        fn into_checked(self) -> CheckedInputObjects {
43            CheckedInputObjects::new_with_checked_transaction_inputs(self)
44        }
45    }
46
47    // Entry point for all checks related to gas.
48    // Called on both signing and execution.
49    // On success the gas part of the transaction (gas data and gas coins)
50    // is verified and good to go
51    fn get_gas_status(
52        objects: &InputObjects,
53        gas: &[ObjectRef],
54        protocol_config: &ProtocolConfig,
55        reference_gas_price: u64,
56        transaction: &TransactionData,
57        authentication_gas_budget: u64,
58        is_execute_transaction_to_effects: bool,
59    ) -> IotaResult<IotaGasStatus> {
60        if transaction.is_system_tx() {
61            Ok(IotaGasStatus::new_unmetered())
62        } else {
63            check_gas(
64                objects,
65                protocol_config,
66                reference_gas_price,
67                gas,
68                transaction.gas_price(),
69                transaction.gas_budget(),
70                authentication_gas_budget,
71                is_execute_transaction_to_effects,
72            )
73        }
74    }
75
76    #[instrument(level = "trace", skip_all, fields(tx_digest = ?transaction.digest()))]
77    pub fn check_transaction_input(
78        protocol_config: &ProtocolConfig,
79        reference_gas_price: u64,
80        transaction: &TransactionData,
81        input_objects: InputObjects,
82        receiving_objects: &ReceivingObjects,
83        metrics: &Arc<BytecodeVerifierMetrics>,
84        verifier_signing_config: &VerifierSigningConfig,
85        authentication_gas_budget: u64,
86    ) -> IotaResult<(IotaGasStatus, CheckedInputObjects)> {
87        let gas_status = check_transaction_input_inner(
88            protocol_config,
89            reference_gas_price,
90            transaction,
91            &input_objects,
92            &[],
93            authentication_gas_budget,
94            false,
95        )?;
96        check_receiving_objects(&input_objects, receiving_objects)?;
97        // Runs verifier, which could be expensive.
98        check_non_system_packages_to_be_published(
99            transaction,
100            protocol_config,
101            metrics,
102            verifier_signing_config,
103        )?;
104
105        Ok((gas_status, input_objects.into_checked()))
106    }
107
108    #[instrument(level = "trace", skip_all, fields(tx_digest = ?transaction.digest()))]
109    pub fn check_transaction_input_with_given_gas(
110        protocol_config: &ProtocolConfig,
111        reference_gas_price: u64,
112        transaction: &TransactionData,
113        mut input_objects: InputObjects,
114        receiving_objects: ReceivingObjects,
115        gas_object: Object,
116        metrics: &Arc<BytecodeVerifierMetrics>,
117        verifier_signing_config: &VerifierSigningConfig,
118    ) -> IotaResult<(IotaGasStatus, CheckedInputObjects)> {
119        let gas_object_ref = gas_object.compute_object_reference();
120        input_objects.push(ObjectReadResult::new_from_gas_object(&gas_object));
121
122        let gas_status = check_transaction_input_inner(
123            protocol_config,
124            reference_gas_price,
125            transaction,
126            &input_objects,
127            &[gas_object_ref],
128            0,
129            true,
130        )?;
131        check_receiving_objects(&input_objects, &receiving_objects)?;
132        // Runs verifier, which could be expensive.
133        check_non_system_packages_to_be_published(
134            transaction,
135            protocol_config,
136            metrics,
137            verifier_signing_config,
138        )?;
139
140        Ok((gas_status, input_objects.into_checked()))
141    }
142
143    // Since the purpose of this function is to audit certified transactions,
144    // the checks here should be a strict subset of the checks in
145    // check_transaction_input(). For checks not performed in this function but
146    // in check_transaction_input(), we should add a comment calling out the
147    // difference.
148    #[instrument(level = "trace", skip_all)]
149    pub fn check_certificate_input(
150        cert: &VerifiedExecutableTransaction,
151        input_objects: InputObjects,
152        protocol_config: &ProtocolConfig,
153        reference_gas_price: u64,
154    ) -> IotaResult<(IotaGasStatus, CheckedInputObjects)> {
155        let transaction = cert.data().transaction_data();
156        let gas_status = check_transaction_input_inner(
157            protocol_config,
158            reference_gas_price,
159            transaction,
160            &input_objects,
161            &[],
162            0,
163            true,
164        )?;
165        // NB: We do not check receiving objects when executing. Only at signing
166        // time do we check. NB: move verifier is only checked at
167        // signing time, not at execution.
168
169        Ok((gas_status, input_objects.into_checked()))
170    }
171
172    /// WARNING! This should only be used for the dev-inspect transaction. This
173    /// transaction type bypasses many of the normal object checks
174    #[instrument(level = "trace", skip_all)]
175    pub fn check_dev_inspect_input(
176        config: &ProtocolConfig,
177        kind: &TransactionKind,
178        input_objects: InputObjects,
179        // TODO: check ReceivingObjects for dev inspect?
180        _receiving_objects: ReceivingObjects,
181    ) -> IotaResult<CheckedInputObjects> {
182        kind.validity_check(config)?;
183        if kind.is_system() {
184            return Err(UserInputError::Unsupported(format!(
185                "Transaction kind {kind} is not supported in dev-inspect"
186            ))
187            .into());
188        }
189        let mut used_objects: HashSet<IotaAddress> = HashSet::new();
190        for input_object in input_objects.iter() {
191            let Some(object) = input_object.as_object() else {
192                // object was deleted
193                continue;
194            };
195
196            if !object.is_immutable() {
197                fp_ensure!(
198                    used_objects.insert(object.id().into()),
199                    UserInputError::MutableObjectUsedMoreThanOnce {
200                        object_id: object.id()
201                    }
202                    .into()
203                );
204            }
205        }
206
207        Ok(input_objects.into_checked())
208    }
209
210    /// A common function to check the `MoveAuthenticator` inputs for signing.
211    ///
212    /// Checks that the authenticator inputs meet the requirements and returns
213    /// checked authenticator input objects, among which we also find the
214    /// account object.
215    #[instrument(level = "trace", skip_all)]
216    pub fn check_move_authenticator_input_for_signing(
217        authenticator_input_objects: InputObjects,
218    ) -> IotaResult<CheckedInputObjects> {
219        check_move_authenticator_objects(&authenticator_input_objects)?;
220
221        Ok(authenticator_input_objects.into_checked())
222    }
223
224    /// A function to aggregate the checked authenticator input objects for
225    /// multiple `MoveAuthenticators` into one `CheckedInputObjects` to be used
226    /// for execution.
227    pub fn aggregate_authenticator_input_objects(
228        per_authenticator_checked_input_objects: &[&CheckedInputObjects],
229    ) -> IotaResult<CheckedInputObjects> {
230        let mut aggregated_authenticator_input_objects =
231            CheckedInputObjects::new_with_checked_transaction_inputs(InputObjects::new(vec![]));
232
233        for authenticator_checked_input_objects in per_authenticator_checked_input_objects.iter() {
234            aggregated_authenticator_input_objects = checked_input_objects_union(
235                aggregated_authenticator_input_objects,
236                authenticator_checked_input_objects,
237            )?;
238        }
239
240        Ok(aggregated_authenticator_input_objects)
241    }
242
243    /// A function to check the `MoveAuthenticator` inputs for execution and
244    /// then for certificate execution.
245    /// To be used instead of check_certificate_input when there is a Move
246    /// authenticator present.
247    ///
248    /// Checks that there is enough gas to pay for the authenticator and
249    /// transaction execution in the transaction inputs. And that the
250    /// authenticator inputs meet the requirements.
251    /// It returns the gas status, the checked authenticator input objects, and
252    /// the union of the checked authenticator input objects and transaction
253    /// input objects.
254    #[instrument(level = "trace", skip_all)]
255    pub fn check_certificate_and_move_authenticator_input(
256        cert: &VerifiedExecutableTransaction,
257        tx_input_objects: InputObjects,
258        per_authenticator_input_objects: Vec<InputObjects>,
259        authenticator_gas_budget: u64,
260        protocol_config: &ProtocolConfig,
261        reference_gas_price: u64,
262    ) -> IotaResult<(IotaGasStatus, Vec<CheckedInputObjects>, CheckedInputObjects)> {
263        // Check Move authenticator inputs first
264        per_authenticator_input_objects
265            .iter()
266            .try_for_each(check_move_authenticator_objects)?;
267
268        // Check certificate inputs next
269        let transaction = cert.data().transaction_data();
270        let gas_status = check_transaction_input_inner(
271            protocol_config,
272            reference_gas_price,
273            transaction,
274            &tx_input_objects,
275            &[],
276            authenticator_gas_budget,
277            true,
278        )?;
279
280        let per_authenticator_checked_input_objects = per_authenticator_input_objects
281            .into_iter()
282            .map(|objects| objects.into_checked())
283            .collect::<Vec<_>>();
284
285        // Create a checked union of input objects
286        let mut input_objects_union = tx_input_objects.into_checked();
287        for objects in per_authenticator_checked_input_objects.iter() {
288            input_objects_union = checked_input_objects_union(input_objects_union, objects)?;
289        }
290
291        Ok((
292            gas_status,
293            per_authenticator_checked_input_objects,
294            input_objects_union,
295        ))
296    }
297
298    // Common checks performed for transactions and certificates.
299    fn check_transaction_input_inner(
300        protocol_config: &ProtocolConfig,
301        reference_gas_price: u64,
302        transaction: &TransactionData,
303        input_objects: &InputObjects,
304        // Overrides the gas objects in the transaction.
305        gas_override: &[ObjectRef],
306        authentication_gas_budget: u64,
307        is_execute_transaction_to_effects: bool,
308    ) -> IotaResult<IotaGasStatus> {
309        // Cheap validity checks that is ok to run multiple times during processing.
310        let gas = if gas_override.is_empty() {
311            transaction.gas()
312        } else {
313            gas_override
314        };
315
316        let gas_status = get_gas_status(
317            input_objects,
318            gas,
319            protocol_config,
320            reference_gas_price,
321            transaction,
322            authentication_gas_budget,
323            is_execute_transaction_to_effects,
324        )?;
325        check_objects(transaction, input_objects)?;
326
327        Ok(gas_status)
328    }
329
330    #[instrument(level = "trace", skip_all)]
331    fn check_receiving_objects(
332        input_objects: &InputObjects,
333        receiving_objects: &ReceivingObjects,
334    ) -> Result<(), IotaError> {
335        let mut objects_in_txn: HashSet<_> = input_objects
336            .object_kinds()
337            .map(|x| x.object_id())
338            .collect();
339
340        // Since we're at signing we check that every object reference that we are
341        // receiving is the most recent version of that object. If it's been
342        // received at the version specified we let it through to allow the
343        // transaction to run and fail to unlock any other objects in
344        // the transaction. Otherwise, we return an error.
345        //
346        // If there are any object IDs in common (either between receiving objects and
347        // input objects) we return an error.
348        for ReceivingObjectReadResult { object_ref, object } in receiving_objects.iter() {
349            fp_ensure!(
350                object_ref.version < SequenceNumber::MAX_VALID_EXCL,
351                UserInputError::InvalidSequenceNumber.into()
352            );
353
354            let Some(object) = object.as_object() else {
355                // object was previously received
356                continue;
357            };
358
359            if !(object.owner.is_address()
360                && object.version() == object_ref.version
361                && object.digest() == object_ref.digest)
362            {
363                // Version mismatch
364                fp_ensure!(
365                    object.version() == object_ref.version,
366                    UserInputError::ObjectVersionUnavailableForConsumption {
367                        provided_obj_ref: *object_ref,
368                        current_version: object.version(),
369                    }
370                    .into()
371                );
372
373                // Tried to receive a package
374                fp_ensure!(
375                    !object.is_package(),
376                    UserInputError::MovePackageAsObject {
377                        object_id: object_ref.object_id
378                    }
379                    .into()
380                );
381
382                // Digest mismatch
383                let expected_digest = object.digest();
384                fp_ensure!(
385                    expected_digest == object_ref.digest,
386                    UserInputError::InvalidObjectDigest {
387                        object_id: object_ref.object_id,
388                        expected_digest
389                    }
390                    .into()
391                );
392
393                match object.owner {
394                    Owner::Address(_) => {
395                        debug_assert!(
396                            false,
397                            "Receiving object {object_ref:?} is invalid but we expect it should be valid. {object:?}"
398                        );
399                        error!(
400                            "Receiving object {:?} is invalid but we expect it should be valid. {:?}",
401                            object_ref, object
402                        );
403                        // We should never get here, but if for some reason we do just default to
404                        // object not found and reject signing the transaction.
405                        fp_bail!(
406                            UserInputError::ObjectNotFound {
407                                object_id: object_ref.object_id,
408                                version: Some(object_ref.version),
409                            }
410                            .into()
411                        )
412                    }
413                    Owner::Object(owner) => {
414                        fp_bail!(
415                            UserInputError::InvalidChildObjectArgument {
416                                child_id: object.id(),
417                                parent_id: owner,
418                            }
419                            .into()
420                        )
421                    }
422                    Owner::Shared(_) => fp_bail!(UserInputError::NotSharedObject.into()),
423                    Owner::Immutable => fp_bail!(
424                        UserInputError::MutableParameterExpected {
425                            object_id: object_ref.object_id
426                        }
427                        .into()
428                    ),
429                    _ => {
430                        unimplemented!("a new Owner enum variant was added and needs to be handled")
431                    }
432                };
433            }
434
435            fp_ensure!(
436                !objects_in_txn.contains(&object_ref.object_id),
437                UserInputError::DuplicateObjectRefInput.into()
438            );
439
440            objects_in_txn.insert(object_ref.object_id);
441        }
442        Ok(())
443    }
444
445    /// Check transaction gas data/info and gas coins consistency.
446    /// Return the gas status to be used for the lifecycle of the transaction.
447    #[instrument(level = "trace", skip_all)]
448    fn check_gas(
449        objects: &InputObjects,
450        protocol_config: &ProtocolConfig,
451        reference_gas_price: u64,
452        gas: &[ObjectRef],
453        gas_price: u64,
454        transaction_gas_budget: u64,
455        authentication_gas_budget: u64,
456        is_execute_transaction_to_effects: bool,
457    ) -> IotaResult<IotaGasStatus> {
458        let gas_budget_to_set = if authentication_gas_budget > 0 {
459            // If there is an authentication gas budget, then we are checking if
460            // max_gas_budget is Some. If not, that is UserInputError.
461            let protocol_max_auth_gas =
462                protocol_config.max_auth_gas_as_option().ok_or_else(|| {
463                    UserInputError::Unsupported(
464                        "Transaction requires authentication gas but max_auth_gas is not enabled"
465                            .to_string(),
466                    )
467                })?;
468
469            // Execution phase:
470            //  - meter transaction + authentication;
471            //  - it needs the full budget.
472            // Signing phase:
473            //  - meter only authentication;
474            //  - it only needs authentication budget.
475            if is_execute_transaction_to_effects {
476                transaction_gas_budget
477            } else {
478                authentication_gas_budget.min(protocol_max_auth_gas)
479            }
480        } else {
481            // If there is no authentication gas budget, then we are only checking the
482            // transaction gas budget.
483            transaction_gas_budget
484        };
485
486        // Budget to check is always the one set by the user (which should cover full
487        // transaction + authentication costs).
488        let gas_budget_to_check = transaction_gas_budget;
489
490        let gas_status = IotaGasStatus::new(
491            gas_budget_to_set,
492            gas_price,
493            reference_gas_price,
494            protocol_config,
495        )?;
496
497        // Check balance and coins consistency
498        // Load all gas coins
499        let objects: BTreeMap<_, _> = objects.iter().map(|o| (o.id(), o)).collect();
500        let mut gas_objects = vec![];
501        for obj_ref in gas {
502            let obj = objects.get(&obj_ref.object_id);
503            let obj = *obj.ok_or(UserInputError::ObjectNotFound {
504                object_id: obj_ref.object_id,
505                version: Some(obj_ref.version),
506            })?;
507            gas_objects.push(obj);
508        }
509        gas_status.check_gas_balance(&gas_objects, gas_budget_to_check)?;
510        Ok(gas_status)
511    }
512
513    /// Check all the objects used in the transaction against the database, and
514    /// ensure that they are all the correct version and number.
515    #[instrument(level = "trace", skip_all)]
516    fn check_objects(transaction: &TransactionData, objects: &InputObjects) -> UserInputResult<()> {
517        // We require that mutable objects cannot show up more than once.
518        let mut used_objects: HashSet<IotaAddress> = HashSet::new();
519        for object in objects.iter() {
520            if object.is_mutable() {
521                fp_ensure!(
522                    used_objects.insert(object.id().into()),
523                    UserInputError::MutableObjectUsedMoreThanOnce {
524                        object_id: object.id()
525                    }
526                );
527            }
528        }
529
530        if !transaction.is_genesis_tx() && objects.is_empty() {
531            return Err(UserInputError::ObjectInputArityViolation);
532        }
533
534        let gas_coins: HashSet<ObjectId> =
535            HashSet::from_iter(transaction.gas().iter().map(|obj_ref| obj_ref.object_id));
536        for object in objects.iter() {
537            let input_object_kind = object.input_object_kind;
538
539            match &object.object {
540                ObjectReadResultKind::Object(object) => {
541                    // For Gas Object, we check the object is owned by gas owner
542                    let owner_address = if gas_coins.contains(&object.id()) {
543                        transaction.gas_owner()
544                    } else {
545                        transaction.sender()
546                    };
547                    // Check if the object contents match the type of lock we need for
548                    // this object.
549                    let system_transaction = transaction.is_system_tx();
550                    check_one_object(
551                        &owner_address,
552                        input_object_kind,
553                        object,
554                        system_transaction,
555                    )?;
556                }
557                // We skip checking a deleted shared object because it no longer exists
558                ObjectReadResultKind::DeletedSharedObject(_, _) => (),
559                // We skip checking shared objects from cancelled transactions since we are not
560                // reading it.
561                ObjectReadResultKind::CancelledTransactionSharedObject(_) => (),
562            }
563        }
564
565        Ok(())
566    }
567
568    /// Check one object against a reference
569    fn check_one_object(
570        owner: &IotaAddress,
571        object_kind: InputObjectKind,
572        object: &Object,
573        system_transaction: bool,
574    ) -> UserInputResult {
575        match object_kind {
576            InputObjectKind::MovePackage(package_id) => {
577                fp_ensure!(
578                    object.data.as_package_opt().is_some(),
579                    UserInputError::MoveObjectAsPackage {
580                        object_id: package_id
581                    }
582                );
583            }
584            InputObjectKind::ImmOrOwnedMoveObject(object_ref) => {
585                fp_ensure!(
586                    !object.is_package(),
587                    UserInputError::MovePackageAsObject {
588                        object_id: object_ref.object_id
589                    }
590                );
591                fp_ensure!(
592                    object_ref.version < SequenceNumber::MAX_VALID_EXCL,
593                    UserInputError::InvalidSequenceNumber
594                );
595
596                // This is an invariant - we just load the object with the given ID and version.
597                assert_eq!(
598                    object.version(),
599                    object_ref.version,
600                    "The fetched object version {} does not match the requested version {}, object id: {}",
601                    object.version(),
602                    object_ref.version,
603                    object.id(),
604                );
605
606                // Check the digest matches - user could give a mismatched ObjectDigest
607                let expected_digest = object.digest();
608                fp_ensure!(
609                    expected_digest == object_ref.digest,
610                    UserInputError::InvalidObjectDigest {
611                        object_id: object_ref.object_id,
612                        expected_digest
613                    }
614                );
615
616                match object.owner {
617                    Owner::Immutable => {
618                        // Nothing else to check for Immutable.
619                    }
620                    Owner::Address(actual_owner) => {
621                        // Check the owner is correct.
622                        fp_ensure!(
623                            owner == &actual_owner,
624                            UserInputError::IncorrectUserSignature {
625                                error: format!(
626                                    "Object {} is owned by account address {}, but given owner/signer address is {}",
627                                    object_ref.object_id, actual_owner, owner
628                                ),
629                            }
630                        );
631                    }
632                    Owner::Object(owner) => {
633                        return Err(UserInputError::InvalidChildObjectArgument {
634                            child_id: object.id(),
635                            parent_id: owner,
636                        });
637                    }
638                    Owner::Shared(_) => {
639                        // This object is a mutable shared object. However the transaction
640                        // specifies it as an owned object. This is inconsistent.
641                        return Err(UserInputError::NotSharedObject);
642                    }
643                    _ => {
644                        unimplemented!("a new Owner enum variant was added and needs to be handled")
645                    }
646                };
647            }
648            InputObjectKind::SharedMoveObject {
649                id: ObjectId::CLOCK,
650                initial_shared_version: IOTA_CLOCK_OBJECT_SHARED_VERSION,
651                mutable: true,
652            } => {
653                // Only system transactions can accept the Clock
654                // object as a mutable parameter.
655                if system_transaction {
656                    return Ok(());
657                } else {
658                    return Err(UserInputError::ImmutableParameterExpected {
659                        object_id: ObjectId::CLOCK,
660                    });
661                }
662            }
663            InputObjectKind::SharedMoveObject {
664                id: ObjectId::AUTHENTICATOR_STATE,
665                ..
666            } => {
667                if system_transaction {
668                    return Ok(());
669                } else {
670                    return Err(UserInputError::InaccessibleSystemObject {
671                        object_id: ObjectId::AUTHENTICATOR_STATE,
672                    });
673                }
674            }
675            InputObjectKind::SharedMoveObject {
676                id: ObjectId::RANDOMNESS_STATE,
677                mutable: true,
678                ..
679            } => {
680                // Only system transactions can accept the Random
681                // object as a mutable parameter.
682                if system_transaction {
683                    return Ok(());
684                } else {
685                    return Err(UserInputError::ImmutableParameterExpected {
686                        object_id: ObjectId::RANDOMNESS_STATE,
687                    });
688                }
689            }
690            InputObjectKind::SharedMoveObject {
691                initial_shared_version: input_initial_shared_version,
692                ..
693            } => {
694                fp_ensure!(
695                    object.version() < SequenceNumber::MAX_VALID_EXCL,
696                    UserInputError::InvalidSequenceNumber
697                );
698
699                match object.owner {
700                    Owner::Address(_) | Owner::Object(_) | Owner::Immutable => {
701                        // When someone locks an object as shared it must be shared already.
702                        return Err(UserInputError::NotSharedObject);
703                    }
704                    Owner::Shared(actual_initial_shared_version) => {
705                        fp_ensure!(
706                            input_initial_shared_version == actual_initial_shared_version,
707                            UserInputError::SharedObjectStartingVersionMismatch
708                        )
709                    }
710                    _ => {
711                        unimplemented!("a new Owner enum variant was added and needs to be handled")
712                    }
713                }
714            }
715        };
716        Ok(())
717    }
718
719    /// Check all the `MoveAuthenticator` related input objects against the
720    /// database.
721    #[instrument(level = "trace", skip_all)]
722    fn check_move_authenticator_objects(
723        authenticator_objects: &InputObjects,
724    ) -> UserInputResult<()> {
725        for object in authenticator_objects.iter() {
726            let input_object_kind = object.input_object_kind;
727
728            match &object.object {
729                ObjectReadResultKind::Object(object) => {
730                    check_one_move_authenticator_object(input_object_kind, object)?;
731                }
732                // We skip checking a deleted shared object because it no longer exists.
733                ObjectReadResultKind::DeletedSharedObject(_, _) => (),
734                // We skip checking shared objects from cancelled transactions since we are not
735                // reading it.
736                ObjectReadResultKind::CancelledTransactionSharedObject(_) => (),
737            }
738        }
739
740        Ok(())
741    }
742
743    /// Check one `MoveAuthenticator` input object.
744    fn check_one_move_authenticator_object(
745        object_kind: InputObjectKind,
746        object: &Object,
747    ) -> UserInputResult {
748        match object_kind {
749            InputObjectKind::MovePackage(package_id) => {
750                return Err(UserInputError::PackageIsInMoveAuthenticatorInput { package_id });
751            }
752            InputObjectKind::ImmOrOwnedMoveObject(object_ref) => {
753                fp_ensure!(
754                    !object.is_package(),
755                    UserInputError::MovePackageAsObject {
756                        object_id: object_ref.object_id
757                    }
758                );
759                fp_ensure!(
760                    object_ref.version < SequenceNumber::MAX_VALID_EXCL,
761                    UserInputError::InvalidSequenceNumber
762                );
763
764                // This is an invariant - we just load the object with the given ID and version.
765                assert_eq!(
766                    object.version(),
767                    object_ref.version,
768                    "The fetched object version {} does not match the requested version {}, object id: {}",
769                    object.version(),
770                    object_ref.version,
771                    object.id(),
772                );
773
774                // Check the digest matches - user could give a mismatched `ObjectDigest`.
775                let expected_digest = object.digest();
776                fp_ensure!(
777                    expected_digest == object_ref.digest,
778                    UserInputError::InvalidObjectDigest {
779                        object_id: object_ref.object_id,
780                        expected_digest
781                    }
782                );
783
784                match object.owner {
785                    Owner::Immutable => {
786                        // Nothing else to check for Immutable.
787                    }
788                    Owner::Address(_) => {
789                        return Err(UserInputError::AddressOwnedIsInMoveAuthenticatorInput {
790                            object_id: object.id(),
791                        });
792                    }
793                    Owner::Object(_) => {
794                        return Err(UserInputError::ObjectOwnedIsInMoveAuthenticatorInput {
795                            object_id: object.id(),
796                        });
797                    }
798                    Owner::Shared(_) => {
799                        // This object is a mutable shared object. However the transaction
800                        // specifies it as an owned object. This is inconsistent.
801                        return Err(UserInputError::NotSharedObject);
802                    }
803                    _ => {
804                        unimplemented!("a new Owner enum variant was added and needs to be handled")
805                    }
806                };
807            }
808            InputObjectKind::SharedMoveObject {
809                id: IOTA_AUTHENTICATOR_STATE_OBJECT_ID,
810                ..
811            } => {
812                return Err(UserInputError::InaccessibleSystemObject {
813                    object_id: IOTA_AUTHENTICATOR_STATE_OBJECT_ID,
814                });
815            }
816            InputObjectKind::SharedMoveObject {
817                id, mutable: true, ..
818            } => {
819                return Err(UserInputError::MutableSharedIsInMoveAuthenticatorInput {
820                    object_id: id,
821                });
822            }
823            InputObjectKind::SharedMoveObject {
824                initial_shared_version: input_initial_shared_version,
825                ..
826            } => {
827                fp_ensure!(
828                    object.version() < SequenceNumber::MAX_VALID_EXCL,
829                    UserInputError::InvalidSequenceNumber
830                );
831
832                match object.owner {
833                    Owner::Address(_) | Owner::Object(_) | Owner::Immutable => {
834                        // When someone locks an object as shared it must be shared already.
835                        return Err(UserInputError::NotSharedObject);
836                    }
837                    Owner::Shared(actual_initial_shared_version) => {
838                        fp_ensure!(
839                            input_initial_shared_version == actual_initial_shared_version,
840                            UserInputError::SharedObjectStartingVersionMismatch
841                        )
842                    }
843                    _ => {
844                        unimplemented!("a new Owner enum variant was added and needs to be handled")
845                    }
846                }
847            }
848        };
849        Ok(())
850    }
851
852    /// Create a union of two CheckedInputObjects, ensuring consistency
853    /// for objects that appear in both sets. The base_set is consumed and
854    /// returned with the union. The other_set is borrowed.
855    /// In the case of shared objects, the mutability can differ, but the
856    /// initial shared version must match. For other object kinds, they must
857    /// match exactly.
858    fn checked_input_objects_union(
859        base_set: CheckedInputObjects,
860        other_set: &CheckedInputObjects,
861    ) -> IotaResult<CheckedInputObjects> {
862        let mut base_set = base_set.into_inner();
863        for other_object in other_set.inner().iter() {
864            if let Some(base_object) = base_set.find_object_id_mut(other_object.id()) {
865                // This is an invariant
866                assert_eq!(
867                    base_object.object, other_object.object,
868                    "The object read result for input objects with the same id must be equal"
869                );
870
871                // In the case of an alive object, check that the object kind matches exactly,
872                // or that, if it is a shared object, only the mutability changes
873                if let ObjectReadResultKind::Object(_) = &other_object.object {
874                    base_object
875                        .input_object_kind
876                        .left_union_with_checks(&other_object.input_object_kind)?;
877                }
878            } else {
879                base_set.push(other_object.clone());
880            }
881        }
882        Ok(base_set.into_checked())
883    }
884
885    /// Check package verification timeout
886    #[instrument(level = "trace", skip_all)]
887    pub fn check_non_system_packages_to_be_published(
888        transaction: &TransactionData,
889        protocol_config: &ProtocolConfig,
890        metrics: &Arc<BytecodeVerifierMetrics>,
891        verifier_signing_config: &VerifierSigningConfig,
892    ) -> UserInputResult<()> {
893        // Only meter non-system programmable transaction blocks
894        if transaction.is_system_tx() {
895            return Ok(());
896        }
897
898        let TransactionKind::Programmable(pt) = transaction.kind() else {
899            return Ok(());
900        };
901
902        // Use the same verifier and meter for all packages, custom configured for
903        // signing.
904        let signing_limits = Some(verifier_signing_config.limits_for_signing());
905        let mut verifier = iota_execution::verifier(protocol_config, signing_limits, metrics);
906        let mut meter = verifier.meter(verifier_signing_config.meter_config_for_signing());
907
908        // Measure time for verifying all packages in the PTB
909        let shared_meter_verifier_timer = metrics
910            .verifier_runtime_per_ptb_success_latency
911            .start_timer();
912
913        let verifier_status = pt
914            .non_system_packages_to_be_published()
915            .try_for_each(|module_bytes| {
916                verifier.meter_module_bytes(protocol_config, module_bytes, meter.as_mut())
917            })
918            .map_err(|e| UserInputError::PackageVerificationTimedout { err: e.to_string() });
919
920        match verifier_status {
921            Ok(_) => {
922                // Success: stop and record the success timer
923                shared_meter_verifier_timer.stop_and_record();
924            }
925            Err(err) => {
926                // Failure: redirect the success timers output to the failure timer and
927                // discard the success timer
928                metrics
929                    .verifier_runtime_per_ptb_timeout_latency
930                    .observe(shared_meter_verifier_timer.stop_and_discard());
931                return Err(err);
932            }
933        };
934
935        Ok(())
936    }
937}