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