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_types::{
19        IOTA_AUTHENTICATOR_STATE_OBJECT_ID, IOTA_CLOCK_OBJECT_ID, IOTA_CLOCK_OBJECT_SHARED_VERSION,
20        IOTA_RANDOMNESS_STATE_OBJECT_ID,
21        base_types::{IotaAddress, ObjectID, 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, ReceivingObjectReadResult, ReceivingObjects, TransactionData,
31            TransactionDataAPI, TransactionKind,
32        },
33    };
34    use tracing::{error, instrument};
35
36    trait IntoChecked {
37        fn into_checked(self) -> CheckedInputObjects;
38    }
39
40    impl IntoChecked for InputObjects {
41        fn into_checked(self) -> CheckedInputObjects {
42            CheckedInputObjects::new_with_checked_transaction_inputs(self)
43        }
44    }
45
46    // Entry point for all checks related to gas.
47    // Called on both signing and execution.
48    // On success the gas part of the transaction (gas data and gas coins)
49    // is verified and good to go
50    pub fn get_gas_status(
51        objects: &InputObjects,
52        gas: &[ObjectRef],
53        protocol_config: &ProtocolConfig,
54        reference_gas_price: u64,
55        transaction: &TransactionData,
56    ) -> IotaResult<IotaGasStatus> {
57        check_gas(
58            objects,
59            protocol_config,
60            reference_gas_price,
61            gas,
62            transaction.gas_budget(),
63            transaction.gas_price(),
64            transaction.kind(),
65        )
66    }
67
68    #[instrument(level = "trace", skip_all, fields(tx_digest = ?transaction.digest()))]
69    pub fn check_transaction_input(
70        protocol_config: &ProtocolConfig,
71        reference_gas_price: u64,
72        transaction: &TransactionData,
73        input_objects: InputObjects,
74        receiving_objects: &ReceivingObjects,
75        metrics: &Arc<BytecodeVerifierMetrics>,
76        verifier_signing_config: &VerifierSigningConfig,
77    ) -> IotaResult<(IotaGasStatus, CheckedInputObjects)> {
78        let gas_status = check_transaction_input_inner(
79            protocol_config,
80            reference_gas_price,
81            transaction,
82            &input_objects,
83            &[],
84        )?;
85        check_receiving_objects(&input_objects, receiving_objects)?;
86        // Runs verifier, which could be expensive.
87        check_non_system_packages_to_be_published(
88            transaction,
89            protocol_config,
90            metrics,
91            verifier_signing_config,
92        )?;
93
94        Ok((gas_status, input_objects.into_checked()))
95    }
96
97    #[instrument(level = "trace", skip_all, fields(tx_digest = ?transaction.digest()))]
98    pub fn check_transaction_input_with_given_gas(
99        protocol_config: &ProtocolConfig,
100        reference_gas_price: u64,
101        transaction: &TransactionData,
102        mut input_objects: InputObjects,
103        receiving_objects: ReceivingObjects,
104        gas_object: Object,
105        metrics: &Arc<BytecodeVerifierMetrics>,
106        verifier_signing_config: &VerifierSigningConfig,
107    ) -> IotaResult<(IotaGasStatus, CheckedInputObjects)> {
108        let gas_object_ref = gas_object.compute_object_reference();
109        input_objects.push(ObjectReadResult::new_from_gas_object(&gas_object));
110
111        let gas_status = check_transaction_input_inner(
112            protocol_config,
113            reference_gas_price,
114            transaction,
115            &input_objects,
116            &[gas_object_ref],
117        )?;
118        check_receiving_objects(&input_objects, &receiving_objects)?;
119        // Runs verifier, which could be expensive.
120        check_non_system_packages_to_be_published(
121            transaction,
122            protocol_config,
123            metrics,
124            verifier_signing_config,
125        )?;
126
127        Ok((gas_status, input_objects.into_checked()))
128    }
129
130    // Since the purpose of this function is to audit certified transactions,
131    // the checks here should be a strict subset of the checks in
132    // check_transaction_input(). For checks not performed in this function but
133    // in check_transaction_input(), we should add a comment calling out the
134    // difference.
135    #[instrument(level = "trace", skip_all)]
136    pub fn check_certificate_input(
137        cert: &VerifiedExecutableTransaction,
138        input_objects: InputObjects,
139        protocol_config: &ProtocolConfig,
140        reference_gas_price: u64,
141    ) -> IotaResult<(IotaGasStatus, CheckedInputObjects)> {
142        let transaction = cert.data().transaction_data();
143        let gas_status = check_transaction_input_inner(
144            protocol_config,
145            reference_gas_price,
146            transaction,
147            &input_objects,
148            &[],
149        )?;
150        // NB: We do not check receiving objects when executing. Only at signing time do
151        // we check. NB: move verifier is only checked at signing time, not at
152        // execution.
153
154        Ok((gas_status, input_objects.into_checked()))
155    }
156
157    /// WARNING! This should only be used for the dev-inspect transaction. This
158    /// transaction type bypasses many of the normal object checks
159    #[instrument(level = "trace", skip_all)]
160    pub fn check_dev_inspect_input(
161        config: &ProtocolConfig,
162        kind: &TransactionKind,
163        input_objects: InputObjects,
164        // TODO: check ReceivingObjects for dev inspect?
165        _receiving_objects: ReceivingObjects,
166    ) -> IotaResult<CheckedInputObjects> {
167        kind.validity_check(config)?;
168        if kind.is_system_tx() {
169            return Err(UserInputError::Unsupported(format!(
170                "Transaction kind {kind} is not supported in dev-inspect"
171            ))
172            .into());
173        }
174        let mut used_objects: HashSet<IotaAddress> = HashSet::new();
175        for input_object in input_objects.iter() {
176            let Some(object) = input_object.as_object() else {
177                // object was deleted
178                continue;
179            };
180
181            if !object.is_immutable() {
182                fp_ensure!(
183                    used_objects.insert(object.id().into()),
184                    UserInputError::MutableObjectUsedMoreThanOnce {
185                        object_id: object.id()
186                    }
187                    .into()
188                );
189            }
190        }
191
192        Ok(input_objects.into_checked())
193    }
194
195    // Common checks performed for transactions and certificates.
196    #[instrument(level = "trace", skip_all)]
197    fn check_transaction_input_inner(
198        protocol_config: &ProtocolConfig,
199        reference_gas_price: u64,
200        transaction: &TransactionData,
201        input_objects: &InputObjects,
202        // Overrides the gas objects in the transaction.
203        gas_override: &[ObjectRef],
204    ) -> IotaResult<IotaGasStatus> {
205        // Cheap validity checks that is ok to run multiple times during processing.
206        let gas = if gas_override.is_empty() {
207            transaction.gas()
208        } else {
209            gas_override
210        };
211
212        let gas_status = get_gas_status(
213            input_objects,
214            gas,
215            protocol_config,
216            reference_gas_price,
217            transaction,
218        )?;
219        check_objects(transaction, input_objects)?;
220
221        Ok(gas_status)
222    }
223
224    #[instrument(level = "trace", skip_all)]
225    fn check_receiving_objects(
226        input_objects: &InputObjects,
227        receiving_objects: &ReceivingObjects,
228    ) -> Result<(), IotaError> {
229        let mut objects_in_txn: HashSet<_> = input_objects
230            .object_kinds()
231            .map(|x| x.object_id())
232            .collect();
233
234        // Since we're at signing we check that every object reference that we are
235        // receiving is the most recent version of that object. If it's been
236        // received at the version specified we let it through to allow the
237        // transaction to run and fail to unlock any other objects in
238        // the transaction. Otherwise, we return an error.
239        //
240        // If there are any object IDs in common (either between receiving objects and
241        // input objects) we return an error.
242        for ReceivingObjectReadResult {
243            object_ref: (object_id, version, object_digest),
244            object,
245        } in receiving_objects.iter()
246        {
247            fp_ensure!(
248                *version < SequenceNumber::MAX_VALID_EXCL,
249                UserInputError::InvalidSequenceNumber.into()
250            );
251
252            let Some(object) = object.as_object() else {
253                // object was previously received
254                continue;
255            };
256
257            if !(object.owner.is_address_owned()
258                && object.version() == *version
259                && object.digest() == *object_digest)
260            {
261                // Version mismatch
262                fp_ensure!(
263                    object.version() == *version,
264                    UserInputError::ObjectVersionUnavailableForConsumption {
265                        provided_obj_ref: (*object_id, *version, *object_digest),
266                        current_version: object.version(),
267                    }
268                    .into()
269                );
270
271                // Tried to receive a package
272                fp_ensure!(
273                    !object.is_package(),
274                    UserInputError::MovePackageAsObject {
275                        object_id: *object_id
276                    }
277                    .into()
278                );
279
280                // Digest mismatch
281                let expected_digest = object.digest();
282                fp_ensure!(
283                    expected_digest == *object_digest,
284                    UserInputError::InvalidObjectDigest {
285                        object_id: *object_id,
286                        expected_digest
287                    }
288                    .into()
289                );
290
291                match object.owner {
292                    Owner::AddressOwner(_) => {
293                        debug_assert!(
294                            false,
295                            "Receiving object {:?} is invalid but we expect it should be valid. {:?}",
296                            (*object_id, *version, *object_id),
297                            object
298                        );
299                        error!(
300                            "Receiving object {:?} is invalid but we expect it should be valid. {:?}",
301                            (*object_id, *version, *object_id),
302                            object
303                        );
304                        // We should never get here, but if for some reason we do just default to
305                        // object not found and reject signing the transaction.
306                        fp_bail!(
307                            UserInputError::ObjectNotFound {
308                                object_id: *object_id,
309                                version: Some(*version),
310                            }
311                            .into()
312                        )
313                    }
314                    Owner::ObjectOwner(owner) => {
315                        fp_bail!(
316                            UserInputError::InvalidChildObjectArgument {
317                                child_id: object.id(),
318                                parent_id: owner.into(),
319                            }
320                            .into()
321                        )
322                    }
323                    Owner::Shared { .. } => fp_bail!(UserInputError::NotSharedObject.into()),
324                    Owner::Immutable => fp_bail!(
325                        UserInputError::MutableParameterExpected {
326                            object_id: *object_id
327                        }
328                        .into()
329                    ),
330                };
331            }
332
333            fp_ensure!(
334                !objects_in_txn.contains(object_id),
335                UserInputError::DuplicateObjectRefInput.into()
336            );
337
338            objects_in_txn.insert(*object_id);
339        }
340        Ok(())
341    }
342
343    /// Check transaction gas data/info and gas coins consistency.
344    /// Return the gas status to be used for the lifecycle of the transaction.
345    #[instrument(level = "trace", skip_all)]
346    fn check_gas(
347        objects: &InputObjects,
348        protocol_config: &ProtocolConfig,
349        reference_gas_price: u64,
350        gas: &[ObjectRef],
351        gas_budget: u64,
352        gas_price: u64,
353        tx_kind: &TransactionKind,
354    ) -> IotaResult<IotaGasStatus> {
355        if tx_kind.is_system_tx() {
356            Ok(IotaGasStatus::new_unmetered())
357        } else {
358            let gas_status =
359                IotaGasStatus::new(gas_budget, gas_price, reference_gas_price, protocol_config)?;
360
361            // check balance and coins consistency
362            // load all gas coins
363            let objects: BTreeMap<_, _> = objects.iter().map(|o| (o.id(), o)).collect();
364            let mut gas_objects = vec![];
365            for obj_ref in gas {
366                let obj = objects.get(&obj_ref.0);
367                let obj = *obj.ok_or(UserInputError::ObjectNotFound {
368                    object_id: obj_ref.0,
369                    version: Some(obj_ref.1),
370                })?;
371                gas_objects.push(obj);
372            }
373            gas_status.check_gas_balance(&gas_objects, gas_budget)?;
374            Ok(gas_status)
375        }
376    }
377
378    /// Check all the objects used in the transaction against the database, and
379    /// ensure that they are all the correct version and number.
380    #[instrument(level = "trace", skip_all)]
381    fn check_objects(transaction: &TransactionData, objects: &InputObjects) -> UserInputResult<()> {
382        // We require that mutable objects cannot show up more than once.
383        let mut used_objects: HashSet<IotaAddress> = HashSet::new();
384        for object in objects.iter() {
385            if object.is_mutable() {
386                fp_ensure!(
387                    used_objects.insert(object.id().into()),
388                    UserInputError::MutableObjectUsedMoreThanOnce {
389                        object_id: object.id()
390                    }
391                );
392            }
393        }
394
395        if !transaction.is_genesis_tx() && objects.is_empty() {
396            return Err(UserInputError::ObjectInputArityViolation);
397        }
398
399        let gas_coins: HashSet<ObjectID> =
400            HashSet::from_iter(transaction.gas().iter().map(|obj_ref| obj_ref.0));
401        for object in objects.iter() {
402            let input_object_kind = object.input_object_kind;
403
404            match &object.object {
405                ObjectReadResultKind::Object(object) => {
406                    // For Gas Object, we check the object is owned by gas owner
407                    let owner_address = if gas_coins.contains(&object.id()) {
408                        transaction.gas_owner()
409                    } else {
410                        transaction.sender()
411                    };
412                    // Check if the object contents match the type of lock we need for
413                    // this object.
414                    let system_transaction = transaction.is_system_tx();
415                    check_one_object(
416                        &owner_address,
417                        input_object_kind,
418                        object,
419                        system_transaction,
420                    )?;
421                }
422                // We skip checking a deleted shared object because it no longer exists
423                ObjectReadResultKind::DeletedSharedObject(_, _) => (),
424                // We skip checking shared objects from cancelled transactions since we are not
425                // reading it.
426                ObjectReadResultKind::CancelledTransactionSharedObject(_) => (),
427            }
428        }
429
430        Ok(())
431    }
432
433    /// Check one object against a reference
434    fn check_one_object(
435        owner: &IotaAddress,
436        object_kind: InputObjectKind,
437        object: &Object,
438        system_transaction: bool,
439    ) -> UserInputResult {
440        match object_kind {
441            InputObjectKind::MovePackage(package_id) => {
442                fp_ensure!(
443                    object.data.try_as_package().is_some(),
444                    UserInputError::MoveObjectAsPackage {
445                        object_id: package_id
446                    }
447                );
448            }
449            InputObjectKind::ImmOrOwnedMoveObject((object_id, sequence_number, object_digest)) => {
450                fp_ensure!(
451                    !object.is_package(),
452                    UserInputError::MovePackageAsObject { object_id }
453                );
454                fp_ensure!(
455                    sequence_number < SequenceNumber::MAX_VALID_EXCL,
456                    UserInputError::InvalidSequenceNumber
457                );
458
459                // This is an invariant - we just load the object with the given ID and version.
460                assert_eq!(
461                    object.version(),
462                    sequence_number,
463                    "The fetched object version {} does not match the requested version {}, object id: {}",
464                    object.version(),
465                    sequence_number,
466                    object.id(),
467                );
468
469                // Check the digest matches - user could give a mismatched ObjectDigest
470                let expected_digest = object.digest();
471                fp_ensure!(
472                    expected_digest == object_digest,
473                    UserInputError::InvalidObjectDigest {
474                        object_id,
475                        expected_digest
476                    }
477                );
478
479                match object.owner {
480                    Owner::Immutable => {
481                        // Nothing else to check for Immutable.
482                    }
483                    Owner::AddressOwner(actual_owner) => {
484                        // Check the owner is correct.
485                        fp_ensure!(
486                            owner == &actual_owner,
487                            UserInputError::IncorrectUserSignature {
488                                error: format!(
489                                    "Object {object_id:?} is owned by account address {actual_owner:?}, but given owner/signer address is {owner:?}"
490                                ),
491                            }
492                        );
493                    }
494                    Owner::ObjectOwner(owner) => {
495                        return Err(UserInputError::InvalidChildObjectArgument {
496                            child_id: object.id(),
497                            parent_id: owner.into(),
498                        });
499                    }
500                    Owner::Shared { .. } => {
501                        // This object is a mutable shared object. However the transaction
502                        // specifies it as an owned object. This is inconsistent.
503                        return Err(UserInputError::NotSharedObject);
504                    }
505                };
506            }
507            InputObjectKind::SharedMoveObject {
508                id: IOTA_CLOCK_OBJECT_ID,
509                initial_shared_version: IOTA_CLOCK_OBJECT_SHARED_VERSION,
510                mutable: true,
511            } => {
512                // Only system transactions can accept the Clock
513                // object as a mutable parameter.
514                if system_transaction {
515                    return Ok(());
516                } else {
517                    return Err(UserInputError::ImmutableParameterExpected {
518                        object_id: IOTA_CLOCK_OBJECT_ID,
519                    });
520                }
521            }
522            InputObjectKind::SharedMoveObject {
523                id: IOTA_AUTHENTICATOR_STATE_OBJECT_ID,
524                ..
525            } => {
526                if system_transaction {
527                    return Ok(());
528                } else {
529                    return Err(UserInputError::InaccessibleSystemObject {
530                        object_id: IOTA_AUTHENTICATOR_STATE_OBJECT_ID,
531                    });
532                }
533            }
534            InputObjectKind::SharedMoveObject {
535                id: IOTA_RANDOMNESS_STATE_OBJECT_ID,
536                mutable: true,
537                ..
538            } => {
539                // Only system transactions can accept the Random
540                // object as a mutable parameter.
541                if system_transaction {
542                    return Ok(());
543                } else {
544                    return Err(UserInputError::ImmutableParameterExpected {
545                        object_id: IOTA_RANDOMNESS_STATE_OBJECT_ID,
546                    });
547                }
548            }
549            InputObjectKind::SharedMoveObject {
550                initial_shared_version: input_initial_shared_version,
551                ..
552            } => {
553                fp_ensure!(
554                    object.version() < SequenceNumber::MAX_VALID_EXCL,
555                    UserInputError::InvalidSequenceNumber
556                );
557
558                match object.owner {
559                    Owner::AddressOwner(_) | Owner::ObjectOwner(_) | Owner::Immutable => {
560                        // When someone locks an object as shared it must be shared already.
561                        return Err(UserInputError::NotSharedObject);
562                    }
563                    Owner::Shared {
564                        initial_shared_version: actual_initial_shared_version,
565                    } => {
566                        fp_ensure!(
567                            input_initial_shared_version == actual_initial_shared_version,
568                            UserInputError::SharedObjectStartingVersionMismatch
569                        )
570                    }
571                }
572            }
573        };
574        Ok(())
575    }
576
577    /// Check package verification timeout
578    #[instrument(level = "trace", skip_all)]
579    pub fn check_non_system_packages_to_be_published(
580        transaction: &TransactionData,
581        protocol_config: &ProtocolConfig,
582        metrics: &Arc<BytecodeVerifierMetrics>,
583        verifier_signing_config: &VerifierSigningConfig,
584    ) -> UserInputResult<()> {
585        // Only meter non-system programmable transaction blocks
586        if transaction.is_system_tx() {
587            return Ok(());
588        }
589
590        let TransactionKind::ProgrammableTransaction(pt) = transaction.kind() else {
591            return Ok(());
592        };
593
594        // Use the same verifier and meter for all packages, custom configured for
595        // signing.
596        let signing_limits = Some(verifier_signing_config.limits_for_signing());
597        let mut verifier = iota_execution::verifier(protocol_config, signing_limits, metrics);
598        let mut meter = verifier.meter(verifier_signing_config.meter_config_for_signing());
599
600        // Measure time for verifying all packages in the PTB
601        let shared_meter_verifier_timer = metrics
602            .verifier_runtime_per_ptb_success_latency
603            .start_timer();
604
605        let verifier_status = pt
606            .non_system_packages_to_be_published()
607            .try_for_each(|module_bytes| {
608                verifier.meter_module_bytes(protocol_config, module_bytes, meter.as_mut())
609            })
610            .map_err(|e| UserInputError::PackageVerificationTimedout { err: e.to_string() });
611
612        match verifier_status {
613            Ok(_) => {
614                // Success: stop and record the success timer
615                shared_meter_verifier_timer.stop_and_record();
616            }
617            Err(err) => {
618                // Failure: redirect the success timers output to the failure timer and
619                // discard the success timer
620                metrics
621                    .verifier_runtime_per_ptb_timeout_latency
622                    .observe(shared_meter_verifier_timer.stop_and_discard());
623                return Err(err);
624            }
625        };
626
627        Ok(())
628    }
629}