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)]
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 {} is not supported in dev-inspect",
169                kind
170            ))
171            .into());
172        }
173        let mut used_objects: HashSet<IotaAddress> = HashSet::new();
174        for input_object in input_objects.iter() {
175            let Some(object) = input_object.as_object() else {
176                // object was deleted
177                continue;
178            };
179
180            if !object.is_immutable() {
181                fp_ensure!(
182                    used_objects.insert(object.id().into()),
183                    UserInputError::MutableObjectUsedMoreThanOnce {
184                        object_id: object.id()
185                    }
186                    .into()
187                );
188            }
189        }
190
191        Ok(input_objects.into_checked())
192    }
193
194    // Common checks performed for transactions and certificates.
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    fn check_receiving_objects(
223        input_objects: &InputObjects,
224        receiving_objects: &ReceivingObjects,
225    ) -> Result<(), IotaError> {
226        let mut objects_in_txn: HashSet<_> = input_objects
227            .object_kinds()
228            .map(|x| x.object_id())
229            .collect();
230
231        // Since we're at signing we check that every object reference that we are
232        // receiving is the most recent version of that object. If it's been
233        // received at the version specified we let it through to allow the
234        // transaction to run and fail to unlock any other objects in
235        // the transaction. Otherwise, we return an error.
236        //
237        // If there are any object IDs in common (either between receiving objects and
238        // input objects) we return an error.
239        for ReceivingObjectReadResult {
240            object_ref: (object_id, version, object_digest),
241            object,
242        } in receiving_objects.iter()
243        {
244            fp_ensure!(
245                *version < SequenceNumber::MAX,
246                UserInputError::InvalidSequenceNumber.into()
247            );
248
249            let Some(object) = object.as_object() else {
250                // object was previously received
251                continue;
252            };
253
254            if !(object.owner.is_address_owned()
255                && object.version() == *version
256                && object.digest() == *object_digest)
257            {
258                // Version mismatch
259                fp_ensure!(
260                    object.version() == *version,
261                    UserInputError::ObjectVersionUnavailableForConsumption {
262                        provided_obj_ref: (*object_id, *version, *object_digest),
263                        current_version: object.version(),
264                    }
265                    .into()
266                );
267
268                // Tried to receive a package
269                fp_ensure!(
270                    !object.is_package(),
271                    UserInputError::MovePackageAsObject {
272                        object_id: *object_id
273                    }
274                    .into()
275                );
276
277                // Digest mismatch
278                let expected_digest = object.digest();
279                fp_ensure!(
280                    expected_digest == *object_digest,
281                    UserInputError::InvalidObjectDigest {
282                        object_id: *object_id,
283                        expected_digest
284                    }
285                    .into()
286                );
287
288                match object.owner {
289                    Owner::AddressOwner(_) => {
290                        debug_assert!(
291                            false,
292                            "Receiving object {:?} is invalid but we expect it should be valid. {:?}",
293                            (*object_id, *version, *object_id),
294                            object
295                        );
296                        error!(
297                            "Receiving object {:?} is invalid but we expect it should be valid. {:?}",
298                            (*object_id, *version, *object_id),
299                            object
300                        );
301                        // We should never get here, but if for some reason we do just default to
302                        // object not found and reject signing the transaction.
303                        fp_bail!(
304                            UserInputError::ObjectNotFound {
305                                object_id: *object_id,
306                                version: Some(*version),
307                            }
308                            .into()
309                        )
310                    }
311                    Owner::ObjectOwner(owner) => {
312                        fp_bail!(
313                            UserInputError::InvalidChildObjectArgument {
314                                child_id: object.id(),
315                                parent_id: owner.into(),
316                            }
317                            .into()
318                        )
319                    }
320                    Owner::Shared { .. } => fp_bail!(UserInputError::NotSharedObject.into()),
321                    Owner::Immutable => fp_bail!(
322                        UserInputError::MutableParameterExpected {
323                            object_id: *object_id
324                        }
325                        .into()
326                    ),
327                };
328            }
329
330            fp_ensure!(
331                !objects_in_txn.contains(object_id),
332                UserInputError::DuplicateObjectRefInput.into()
333            );
334
335            objects_in_txn.insert(*object_id);
336        }
337        Ok(())
338    }
339
340    /// Check transaction gas data/info and gas coins consistency.
341    /// Return the gas status to be used for the lifecycle of the transaction.
342    #[instrument(level = "trace", skip_all)]
343    fn check_gas(
344        objects: &InputObjects,
345        protocol_config: &ProtocolConfig,
346        reference_gas_price: u64,
347        gas: &[ObjectRef],
348        gas_budget: u64,
349        gas_price: u64,
350        tx_kind: &TransactionKind,
351    ) -> IotaResult<IotaGasStatus> {
352        if tx_kind.is_system_tx() {
353            Ok(IotaGasStatus::new_unmetered())
354        } else {
355            let gas_status =
356                IotaGasStatus::new(gas_budget, gas_price, reference_gas_price, protocol_config)?;
357
358            // check balance and coins consistency
359            // load all gas coins
360            let objects: BTreeMap<_, _> = objects.iter().map(|o| (o.id(), o)).collect();
361            let mut gas_objects = vec![];
362            for obj_ref in gas {
363                let obj = objects.get(&obj_ref.0);
364                let obj = *obj.ok_or(UserInputError::ObjectNotFound {
365                    object_id: obj_ref.0,
366                    version: Some(obj_ref.1),
367                })?;
368                gas_objects.push(obj);
369            }
370            gas_status.check_gas_balance(&gas_objects, gas_budget)?;
371            Ok(gas_status)
372        }
373    }
374
375    /// Check all the objects used in the transaction against the database, and
376    /// ensure that they are all the correct version and number.
377    #[instrument(level = "trace", skip_all)]
378    fn check_objects(transaction: &TransactionData, objects: &InputObjects) -> UserInputResult<()> {
379        // We require that mutable objects cannot show up more than once.
380        let mut used_objects: HashSet<IotaAddress> = HashSet::new();
381        for object in objects.iter() {
382            if object.is_mutable() {
383                fp_ensure!(
384                    used_objects.insert(object.id().into()),
385                    UserInputError::MutableObjectUsedMoreThanOnce {
386                        object_id: object.id()
387                    }
388                );
389            }
390        }
391
392        if !transaction.is_genesis_tx() && objects.is_empty() {
393            return Err(UserInputError::ObjectInputArityViolation);
394        }
395
396        let gas_coins: HashSet<ObjectID> =
397            HashSet::from_iter(transaction.gas().iter().map(|obj_ref| obj_ref.0));
398        for object in objects.iter() {
399            let input_object_kind = object.input_object_kind;
400
401            match &object.object {
402                ObjectReadResultKind::Object(object) => {
403                    // For Gas Object, we check the object is owned by gas owner
404                    let owner_address = if gas_coins.contains(&object.id()) {
405                        transaction.gas_owner()
406                    } else {
407                        transaction.sender()
408                    };
409                    // Check if the object contents match the type of lock we need for
410                    // this object.
411                    let system_transaction = transaction.is_system_tx();
412                    check_one_object(
413                        &owner_address,
414                        input_object_kind,
415                        object,
416                        system_transaction,
417                    )?;
418                }
419                // We skip checking a deleted shared object because it no longer exists
420                ObjectReadResultKind::DeletedSharedObject(_, _) => (),
421                // We skip checking shared objects from cancelled transactions since we are not
422                // reading it.
423                ObjectReadResultKind::CancelledTransactionSharedObject(_) => (),
424            }
425        }
426
427        Ok(())
428    }
429
430    /// Check one object against a reference
431    fn check_one_object(
432        owner: &IotaAddress,
433        object_kind: InputObjectKind,
434        object: &Object,
435        system_transaction: bool,
436    ) -> UserInputResult {
437        match object_kind {
438            InputObjectKind::MovePackage(package_id) => {
439                fp_ensure!(
440                    object.data.try_as_package().is_some(),
441                    UserInputError::MoveObjectAsPackage {
442                        object_id: package_id
443                    }
444                );
445            }
446            InputObjectKind::ImmOrOwnedMoveObject((object_id, sequence_number, object_digest)) => {
447                fp_ensure!(
448                    !object.is_package(),
449                    UserInputError::MovePackageAsObject { object_id }
450                );
451                fp_ensure!(
452                    sequence_number < SequenceNumber::MAX,
453                    UserInputError::InvalidSequenceNumber
454                );
455
456                // This is an invariant - we just load the object with the given ID and version.
457                assert_eq!(
458                    object.version(),
459                    sequence_number,
460                    "The fetched object version {} does not match the requested version {}, object id: {}",
461                    object.version(),
462                    sequence_number,
463                    object.id(),
464                );
465
466                // Check the digest matches - user could give a mismatched ObjectDigest
467                let expected_digest = object.digest();
468                fp_ensure!(
469                    expected_digest == object_digest,
470                    UserInputError::InvalidObjectDigest {
471                        object_id,
472                        expected_digest
473                    }
474                );
475
476                match object.owner {
477                    Owner::Immutable => {
478                        // Nothing else to check for Immutable.
479                    }
480                    Owner::AddressOwner(actual_owner) => {
481                        // Check the owner is correct.
482                        fp_ensure!(
483                            owner == &actual_owner,
484                            UserInputError::IncorrectUserSignature {
485                                error: format!(
486                                    "Object {:?} is owned by account address {:?}, but given owner/signer address is {:?}",
487                                    object_id, actual_owner, 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,
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}