Skip to main content

iota_transaction_checks/
deny.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use iota_sdk_types::{Command, ObjectReference, Transaction, UserSignature};
6use iota_types::{
7    deny_rule_governance::DenyRuleConfig,
8    error::{IotaError, IotaResult, UserInputError},
9    storage::BackingPackageStore,
10    transaction::{InputObjectKind, TransactionAPI, TransactionKindExt},
11};
12use tracing::instrument;
13macro_rules! deny_if_true {
14    ($cond:expr, $msg:expr) => {
15        if ($cond) {
16            return Err(IotaError::UserInput {
17                error: UserInputError::TransactionDenied {
18                    error: $msg.to_string(),
19                },
20            });
21        }
22    };
23}
24
25/// Check that the provided transaction is allowed to be signed according to the
26/// deny config.
27#[instrument(level = "trace", skip_all, fields(tx_digest = ?tx.digest()))]
28pub fn check_transaction_for_validation(
29    tx: &Transaction,
30    tx_signatures: &[UserSignature],
31    input_object_kinds: &[InputObjectKind],
32    receiving_objects: &[ObjectReference],
33    filter_config: &dyn DenyRuleConfig,
34    package_store: &dyn BackingPackageStore,
35) -> IotaResult {
36    check_disabled_features(filter_config, tx, tx_signatures)?;
37
38    check_signers(filter_config, tx)?;
39
40    check_input_objects(filter_config, input_object_kinds)?;
41
42    check_package_dependencies(filter_config, tx, package_store)?;
43
44    check_receiving_objects(filter_config, receiving_objects)?;
45
46    Ok(())
47}
48
49#[instrument(level = "trace", skip_all)]
50fn check_receiving_objects(
51    filter_config: &dyn DenyRuleConfig,
52    receiving_objects: &[ObjectReference],
53) -> IotaResult {
54    deny_if_true!(
55        filter_config.receiving_objects_disabled() && !receiving_objects.is_empty(),
56        "Receiving objects is temporarily disabled".to_string()
57    );
58    if !filter_config.has_denied_objects() {
59        return Ok(());
60    }
61    for receiving_object in receiving_objects {
62        deny_if_true!(
63            filter_config.is_object_denied(&receiving_object.object_id),
64            format!(
65                "Access to object {:?} is temporarily disabled",
66                receiving_object.object_id
67            )
68        );
69    }
70    Ok(())
71}
72
73#[instrument(level = "trace", skip_all)]
74fn check_disabled_features(
75    filter_config: &dyn DenyRuleConfig,
76    tx: &Transaction,
77    tx_signatures: &[UserSignature],
78) -> IotaResult {
79    deny_if_true!(
80        filter_config.user_transaction_disabled(),
81        "Transaction signing is temporarily disabled"
82    );
83
84    tx_signatures.iter().try_for_each(|s| {
85        if let UserSignature::MoveAuthenticator(_) = s {
86            deny_if_true!(
87                filter_config.move_authenticator_disabled(),
88                "MoveAuthenticator is temporarily disabled"
89            );
90        }
91        Ok(())
92    })?;
93
94    if !filter_config.package_publish_disabled() && !filter_config.package_upgrade_disabled() {
95        return Ok(());
96    }
97
98    for command in tx.kind().iter_commands() {
99        deny_if_true!(
100            filter_config.package_publish_disabled() && matches!(command, Command::Publish(..)),
101            "Package publish is temporarily disabled"
102        );
103        deny_if_true!(
104            filter_config.package_upgrade_disabled() && matches!(command, Command::Upgrade(..)),
105            "Package upgrade is temporarily disabled"
106        );
107    }
108    Ok(())
109}
110
111#[instrument(level = "trace", skip_all)]
112fn check_signers(filter_config: &dyn DenyRuleConfig, tx: &Transaction) -> IotaResult {
113    if !filter_config.has_denied_addresses() {
114        return Ok(());
115    }
116    for signer in tx.signers() {
117        deny_if_true!(
118            filter_config.is_address_denied(&signer),
119            format!(
120                "Access to account address {:?} is temporarily disabled",
121                signer
122            )
123        );
124    }
125    Ok(())
126}
127
128#[instrument(level = "trace", skip_all)]
129fn check_input_objects(
130    filter_config: &dyn DenyRuleConfig,
131    input_object_kinds: &[InputObjectKind],
132) -> IotaResult {
133    let shared_object_disabled = filter_config.shared_object_disabled();
134    if !filter_config.has_denied_objects() && !shared_object_disabled {
135        // No need to iterate through the input objects if no relevant policy is set.
136        return Ok(());
137    }
138    for input_object_kind in input_object_kinds {
139        let id = input_object_kind.object_id();
140        deny_if_true!(
141            filter_config.is_object_denied(&id),
142            format!("Access to input object {id} is temporarily disabled")
143        );
144        deny_if_true!(
145            shared_object_disabled && input_object_kind.is_shared_object(),
146            "Usage of shared object in transactions is temporarily disabled"
147        );
148    }
149    Ok(())
150}
151
152#[instrument(level = "trace", skip_all)]
153fn check_package_dependencies(
154    filter_config: &dyn DenyRuleConfig,
155    tx: &Transaction,
156    package_store: &dyn BackingPackageStore,
157) -> IotaResult {
158    if !filter_config.has_denied_packages() {
159        return Ok(());
160    }
161    let mut dependencies = vec![];
162    for command in tx.kind().iter_commands() {
163        match command {
164            Command::Publish(cmd) => {
165                // It is possible that the deps list is inaccurate since it's provided
166                // by the user. But that's OK because this publish transaction will fail
167                // to execute in the end. Similar reasoning for Upgrade.
168                dependencies.extend(cmd.dependencies.iter().copied());
169            }
170            Command::Upgrade(cmd) => {
171                dependencies.extend(cmd.dependencies.iter().copied());
172                // It's crucial that we don't allow upgrading a package in the deny list,
173                // otherwise one can bypass the deny list by upgrading a package.
174                dependencies.push(cmd.package);
175            }
176            Command::MoveCall(cmd) => {
177                let package = package_store.get_package_object(&cmd.package)?.ok_or(
178                    IotaError::UserInput {
179                        error: UserInputError::ObjectNotFound {
180                            object_id: cmd.package,
181                            version: None,
182                        },
183                    },
184                )?;
185                // linkage_table maps from the original package ID to the upgraded ID for each
186                // dependency. Here we only check the upgraded (i.e. the latest) ID against the
187                // deny list. This means that we only make sure that the denied package is not
188                // currently used as a dependency. This allows us to deny an older version of
189                // package but permits the use of a newer version.
190                dependencies.extend(
191                    package
192                        .move_package()
193                        .linkage_table()
194                        .values()
195                        .map(|upgrade_info| upgrade_info.upgraded_id),
196                );
197                dependencies.push(package.move_package().id());
198            }
199            Command::TransferObjects(..)
200            | Command::SplitCoins(..)
201            | Command::MergeCoins(..)
202            | Command::MakeMoveVector(..) => {}
203            _ => unimplemented!("a new Command enum variant was added and needs to be handled"),
204        }
205    }
206    for dep in dependencies {
207        deny_if_true!(
208            filter_config.is_package_denied(&dep),
209            format!("Access to package {dep} is temporarily disabled")
210        );
211    }
212    Ok(())
213}