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};
14macro_rules! deny_if_true {
15    ($cond:expr, $msg:expr) => {
16        if ($cond) {
17            return Err(IotaError::UserInput {
18                error: UserInputError::TransactionDenied {
19                    error: $msg.to_string(),
20                },
21            });
22        }
23    };
24}
25
26/// Check that the provided transaction is allowed to be signed according to the
27/// deny config.
28pub fn check_transaction_for_signing(
29    tx_data: &TransactionData,
30    tx_signatures: &[GenericSignature],
31    input_object_kinds: &[InputObjectKind],
32    receiving_objects: &[ObjectRef],
33    filter_config: &TransactionDenyConfig,
34    package_store: &dyn BackingPackageStore,
35) -> IotaResult {
36    check_disabled_features(filter_config, tx_data, tx_signatures)?;
37
38    check_signers(filter_config, tx_data)?;
39
40    check_input_objects(filter_config, input_object_kinds)?;
41
42    check_package_dependencies(filter_config, tx_data, package_store)?;
43
44    check_receiving_objects(filter_config, receiving_objects)?;
45
46    Ok(())
47}
48
49fn check_receiving_objects(
50    filter_config: &TransactionDenyConfig,
51    receiving_objects: &[ObjectRef],
52) -> IotaResult {
53    deny_if_true!(
54        filter_config.receiving_objects_disabled() && !receiving_objects.is_empty(),
55        "Receiving objects is temporarily disabled".to_string()
56    );
57    for (id, _, _) in receiving_objects {
58        deny_if_true!(
59            filter_config.get_object_deny_set().contains(id),
60            format!("Access to object {:?} is temporarily disabled", id)
61        );
62    }
63    Ok(())
64}
65
66fn check_disabled_features(
67    filter_config: &TransactionDenyConfig,
68    tx_data: &TransactionData,
69    tx_signatures: &[GenericSignature],
70) -> IotaResult {
71    deny_if_true!(
72        filter_config.user_transaction_disabled(),
73        "Transaction signing is temporarily disabled"
74    );
75
76    tx_signatures.iter().try_for_each(|s| {
77        if let GenericSignature::ZkLoginAuthenticator(z) = s {
78            deny_if_true!(
79                filter_config.zklogin_sig_disabled(),
80                "zkLogin authenticator is temporarily disabled"
81            );
82            deny_if_true!(
83                filter_config.zklogin_disabled_providers().contains(
84                    &OIDCProvider::from_iss(z.get_iss())
85                        .map_err(|_| IotaError::UnexpectedMessage)?
86                        .to_string()
87                ),
88                "zkLogin OAuth provider 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_data.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
111fn check_signers(filter_config: &TransactionDenyConfig, tx_data: &TransactionData) -> IotaResult {
112    let deny_map = filter_config.get_address_deny_set();
113    if deny_map.is_empty() {
114        return Ok(());
115    }
116    for signer in tx_data.signers() {
117        deny_if_true!(
118            deny_map.contains(&signer),
119            format!(
120                "Access to account address {:?} is temporarily disabled",
121                signer
122            )
123        );
124    }
125    Ok(())
126}
127
128fn check_input_objects(
129    filter_config: &TransactionDenyConfig,
130    input_object_kinds: &[InputObjectKind],
131) -> IotaResult {
132    let deny_map = filter_config.get_object_deny_set();
133    let shared_object_disabled = filter_config.shared_object_disabled();
134    if deny_map.is_empty() && !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            deny_map.contains(&id),
142            format!("Access to input object {:?} is temporarily disabled", id)
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
152fn check_package_dependencies(
153    filter_config: &TransactionDenyConfig,
154    tx_data: &TransactionData,
155    package_store: &dyn BackingPackageStore,
156) -> IotaResult {
157    let deny_map = filter_config.get_package_deny_set();
158    if deny_map.is_empty() {
159        return Ok(());
160    }
161    let mut dependencies = vec![];
162    for command in tx_data.kind().iter_commands() {
163        match command {
164            Command::Publish(_, deps) => {
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(deps.iter().copied());
169            }
170            Command::Upgrade(_, deps, package_id, _) => {
171                dependencies.extend(deps.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(*package_id);
175            }
176            Command::MoveCall(call) => {
177                let package = package_store.get_package_object(&call.package)?.ok_or(
178                    IotaError::UserInput {
179                        error: UserInputError::ObjectNotFound {
180                            object_id: call.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::MakeMoveVec(..) => {}
203        }
204    }
205    for dep in dependencies {
206        deny_if_true!(
207            deny_map.contains(&dep),
208            format!("Access to package {:?} is temporarily disabled", dep)
209        );
210    }
211    Ok(())
212}