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