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        } else if let GenericSignature::MoveAuthenticator(_) = s {
95            deny_if_true!(
96                filter_config.move_authenticator_disabled(),
97                "MoveAuthenticator is temporarily disabled"
98            );
99        }
100        Ok(())
101    })?;
102
103    if !filter_config.package_publish_disabled() && !filter_config.package_upgrade_disabled() {
104        return Ok(());
105    }
106
107    for command in tx_data.kind().iter_commands() {
108        deny_if_true!(
109            filter_config.package_publish_disabled() && matches!(command, Command::Publish(..)),
110            "Package publish is temporarily disabled"
111        );
112        deny_if_true!(
113            filter_config.package_upgrade_disabled() && matches!(command, Command::Upgrade(..)),
114            "Package upgrade is temporarily disabled"
115        );
116    }
117    Ok(())
118}
119
120#[instrument(level = "trace", skip_all)]
121fn check_signers(filter_config: &TransactionDenyConfig, tx_data: &TransactionData) -> IotaResult {
122    let deny_map = filter_config.get_address_deny_set();
123    if deny_map.is_empty() {
124        return Ok(());
125    }
126    for signer in tx_data.signers() {
127        deny_if_true!(
128            deny_map.contains(&signer),
129            format!(
130                "Access to account address {:?} is temporarily disabled",
131                signer
132            )
133        );
134    }
135    Ok(())
136}
137
138#[instrument(level = "trace", skip_all)]
139fn check_input_objects(
140    filter_config: &TransactionDenyConfig,
141    input_object_kinds: &[InputObjectKind],
142) -> IotaResult {
143    let deny_map = filter_config.get_object_deny_set();
144    let shared_object_disabled = filter_config.shared_object_disabled();
145    if deny_map.is_empty() && !shared_object_disabled {
146        // No need to iterate through the input objects if no relevant policy is set.
147        return Ok(());
148    }
149    for input_object_kind in input_object_kinds {
150        let id = input_object_kind.object_id();
151        deny_if_true!(
152            deny_map.contains(&id),
153            format!("Access to input object {:?} is temporarily disabled", id)
154        );
155        deny_if_true!(
156            shared_object_disabled && input_object_kind.is_shared_object(),
157            "Usage of shared object in transactions is temporarily disabled"
158        );
159    }
160    Ok(())
161}
162
163#[instrument(level = "trace", skip_all)]
164fn check_package_dependencies(
165    filter_config: &TransactionDenyConfig,
166    tx_data: &TransactionData,
167    package_store: &dyn BackingPackageStore,
168) -> IotaResult {
169    let deny_map = filter_config.get_package_deny_set();
170    if deny_map.is_empty() {
171        return Ok(());
172    }
173    let mut dependencies = vec![];
174    for command in tx_data.kind().iter_commands() {
175        match command {
176            Command::Publish(_, deps) => {
177                // It is possible that the deps list is inaccurate since it's provided
178                // by the user. But that's OK because this publish transaction will fail
179                // to execute in the end. Similar reasoning for Upgrade.
180                dependencies.extend(deps.iter().copied());
181            }
182            Command::Upgrade(_, deps, package_id, _) => {
183                dependencies.extend(deps.iter().copied());
184                // It's crucial that we don't allow upgrading a package in the deny list,
185                // otherwise one can bypass the deny list by upgrading a package.
186                dependencies.push(*package_id);
187            }
188            Command::MoveCall(call) => {
189                let package = package_store.get_package_object(&call.package)?.ok_or(
190                    IotaError::UserInput {
191                        error: UserInputError::ObjectNotFound {
192                            object_id: call.package,
193                            version: None,
194                        },
195                    },
196                )?;
197                // linkage_table maps from the original package ID to the upgraded ID for each
198                // dependency. Here we only check the upgraded (i.e. the latest) ID against the
199                // deny list. This means that we only make sure that the denied package is not
200                // currently used as a dependency. This allows us to deny an older version of
201                // package but permits the use of a newer version.
202                dependencies.extend(
203                    package
204                        .move_package()
205                        .linkage_table()
206                        .values()
207                        .map(|upgrade_info| upgrade_info.upgraded_id),
208                );
209                dependencies.push(package.move_package().id());
210            }
211            Command::TransferObjects(..)
212            | &Command::SplitCoins(..)
213            | &Command::MergeCoins(..)
214            | &Command::MakeMoveVec(..) => {}
215        }
216    }
217    for dep in dependencies {
218        deny_if_true!(
219            deny_map.contains(&dep),
220            format!("Access to package {:?} is temporarily disabled", dep)
221        );
222    }
223    Ok(())
224}