Skip to main content

iota_config/
transaction_deny_config.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::collections::HashSet;
6
7use iota_sdk_types::{Address, ObjectId};
8use iota_types::deny_rule_governance::{DenyRuleConfig, DenyRuleSet};
9use once_cell::sync::OnceCell;
10use serde::{Deserialize, Serialize};
11
12#[derive(Clone, Debug, Default, Deserialize, Serialize)]
13#[serde(rename_all = "kebab-case")]
14pub struct TransactionDenyConfig {
15    /// A list of object IDs that are not allowed to be accessed/used in
16    /// transactions. Note that since this is checked during transaction
17    /// signing, only root object ids are supported here (i.e. no
18    /// child-objects). Similarly this does not apply to wrapped objects as
19    /// they are not directly accessible.
20    #[serde(default, skip_serializing_if = "Vec::is_empty")]
21    object_deny_list: Vec<ObjectId>,
22
23    /// A list of package object IDs that are not allowed to be called into in
24    /// transactions, either directly or indirectly through transitive
25    /// dependencies. Note that this does not apply to type arguments.
26    /// Also since we only compare the deny list against the upgraded package ID
27    /// of each dependency in the used package, when a package ID is denied,
28    /// newer versions of that package are still allowed. If we want to deny
29    /// the entire upgrade family of a package, we need to explicitly
30    /// specify all the package IDs in the deny list. TODO: We could
31    /// consider making this more flexible, e.g. whether to check in type args,
32    /// whether to block entire upgrade family, whether to allow upgrade and
33    /// etc.
34    #[serde(default, skip_serializing_if = "Vec::is_empty")]
35    package_deny_list: Vec<ObjectId>,
36
37    /// A list of iota addresses that are not allowed to be used as the sender
38    /// or sponsor.
39    #[serde(default, skip_serializing_if = "Vec::is_empty")]
40    address_deny_list: Vec<Address>,
41
42    /// Whether publishing new packages is disabled.
43    #[serde(default)]
44    package_publish_disabled: bool,
45
46    /// Whether upgrading existing packages is disabled.
47    #[serde(default)]
48    package_upgrade_disabled: bool,
49
50    /// Whether usage of shared objects is disabled.
51    #[serde(default)]
52    shared_object_disabled: bool,
53
54    /// Whether user transactions are disabled (i.e. only system transactions
55    /// are allowed). This is essentially a kill switch for transactions
56    /// processing to a degree.
57    #[serde(default)]
58    user_transaction_disabled: bool,
59
60    /// In-memory maps for faster lookup of various lists.
61    #[serde(skip)]
62    object_deny_set: OnceCell<HashSet<ObjectId>>,
63
64    #[serde(skip)]
65    package_deny_set: OnceCell<HashSet<ObjectId>>,
66
67    #[serde(skip)]
68    address_deny_set: OnceCell<HashSet<Address>>,
69
70    /// Whether receiving objects transferred to other objects is allowed
71    #[serde(default)]
72    receiving_objects_disabled: bool,
73
74    /// Whether `MoveAuthenticator` is disabled
75    #[serde(default)]
76    move_authenticator_disabled: bool,
77    // TODO: We could consider add a deny list for types that we want to disable public transfer.
78    // TODO: We could also consider disable more types of commands, such as transfer, split and
79    // etc.
80}
81
82impl TransactionDenyConfig {
83    pub fn get_object_deny_set(&self) -> &HashSet<ObjectId> {
84        self.object_deny_set
85            .get_or_init(|| self.object_deny_list.iter().cloned().collect())
86    }
87
88    pub fn get_package_deny_set(&self) -> &HashSet<ObjectId> {
89        self.package_deny_set
90            .get_or_init(|| self.package_deny_list.iter().cloned().collect())
91    }
92
93    pub fn get_address_deny_set(&self) -> &HashSet<Address> {
94        self.address_deny_set
95            .get_or_init(|| self.address_deny_list.iter().cloned().collect())
96    }
97
98    pub fn package_publish_disabled(&self) -> bool {
99        self.package_publish_disabled
100    }
101
102    pub fn package_upgrade_disabled(&self) -> bool {
103        self.package_upgrade_disabled
104    }
105
106    pub fn shared_object_disabled(&self) -> bool {
107        self.shared_object_disabled
108    }
109
110    pub fn user_transaction_disabled(&self) -> bool {
111        self.user_transaction_disabled
112    }
113
114    pub fn receiving_objects_disabled(&self) -> bool {
115        self.receiving_objects_disabled
116    }
117
118    pub fn move_authenticator_disabled(&self) -> bool {
119        self.move_authenticator_disabled
120    }
121
122    /// The full-state [`DenyRuleSet`] equivalent of this config, for
123    /// announcing it as a deny rule proposal.
124    pub fn to_deny_rule_set(&self) -> DenyRuleSet {
125        DenyRuleSet {
126            denied_addresses: self.address_deny_list.iter().copied().collect(),
127            denied_objects: self.object_deny_list.iter().copied().collect(),
128            denied_packages: self.package_deny_list.iter().copied().collect(),
129            package_publish_disabled: self.package_publish_disabled,
130            package_upgrade_disabled: self.package_upgrade_disabled,
131            shared_object_disabled: self.shared_object_disabled,
132            user_transaction_disabled: self.user_transaction_disabled,
133            receiving_objects_disabled: self.receiving_objects_disabled,
134            move_authenticator_disabled: self.move_authenticator_disabled,
135        }
136    }
137}
138
139#[derive(Default)]
140pub struct TransactionDenyConfigBuilder {
141    config: TransactionDenyConfig,
142}
143
144impl TransactionDenyConfigBuilder {
145    pub fn new() -> Self {
146        Self::default()
147    }
148
149    pub fn build(self) -> TransactionDenyConfig {
150        self.config
151    }
152
153    pub fn disable_user_transaction(mut self) -> Self {
154        self.config.user_transaction_disabled = true;
155        self
156    }
157
158    pub fn disable_shared_object_transaction(mut self) -> Self {
159        self.config.shared_object_disabled = true;
160        self
161    }
162
163    pub fn disable_package_publish(mut self) -> Self {
164        self.config.package_publish_disabled = true;
165        self
166    }
167
168    pub fn disable_package_upgrade(mut self) -> Self {
169        self.config.package_upgrade_disabled = true;
170        self
171    }
172
173    pub fn disable_receiving_objects(mut self) -> Self {
174        self.config.receiving_objects_disabled = true;
175        self
176    }
177
178    pub fn add_denied_object(mut self, id: ObjectId) -> Self {
179        self.config.object_deny_list.push(id);
180        self
181    }
182
183    pub fn add_denied_address(mut self, address: Address) -> Self {
184        self.config.address_deny_list.push(address);
185        self
186    }
187
188    pub fn add_denied_package(mut self, id: ObjectId) -> Self {
189        self.config.package_deny_list.push(id);
190        self
191    }
192
193    pub fn disable_move_authenticator(mut self) -> Self {
194        self.config.move_authenticator_disabled = true;
195        self
196    }
197}
198
199impl DenyRuleConfig for TransactionDenyConfig {
200    fn is_address_denied(&self, address: &Address) -> bool {
201        self.get_address_deny_set().contains(address)
202    }
203
204    fn is_object_denied(&self, id: &ObjectId) -> bool {
205        self.get_object_deny_set().contains(id)
206    }
207
208    fn is_package_denied(&self, id: &ObjectId) -> bool {
209        self.get_package_deny_set().contains(id)
210    }
211
212    fn has_denied_addresses(&self) -> bool {
213        !self.address_deny_list.is_empty()
214    }
215
216    fn has_denied_objects(&self) -> bool {
217        !self.object_deny_list.is_empty()
218    }
219
220    fn has_denied_packages(&self) -> bool {
221        !self.package_deny_list.is_empty()
222    }
223
224    fn package_publish_disabled(&self) -> bool {
225        self.package_publish_disabled
226    }
227
228    fn package_upgrade_disabled(&self) -> bool {
229        self.package_upgrade_disabled
230    }
231
232    fn shared_object_disabled(&self) -> bool {
233        self.shared_object_disabled
234    }
235
236    fn user_transaction_disabled(&self) -> bool {
237        self.user_transaction_disabled
238    }
239
240    fn receiving_objects_disabled(&self) -> bool {
241        self.receiving_objects_disabled
242    }
243
244    fn move_authenticator_disabled(&self) -> bool {
245        self.move_authenticator_disabled
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use iota_sdk_types::{Address, ObjectId};
252    use iota_types::deny_rule_governance::DenyRuleSet;
253
254    use super::{DenyRuleConfig, TransactionDenyConfig, TransactionDenyConfigBuilder};
255
256    #[test]
257    fn trait_impl_reflects_config() {
258        let addr = Address::new([1u8; 32]);
259        let obj = ObjectId::new([2u8; 32]);
260        let pkg = ObjectId::new([3u8; 32]);
261        let config = TransactionDenyConfigBuilder::new()
262            .add_denied_address(addr)
263            .add_denied_object(obj)
264            .add_denied_package(pkg)
265            .disable_user_transaction()
266            .disable_shared_object_transaction()
267            .disable_move_authenticator()
268            .build();
269
270        // Exercise the trait via dynamic dispatch, exactly as the deny checks do.
271        let deny: &dyn DenyRuleConfig = &config;
272        assert!(deny.is_address_denied(&addr));
273        assert!(!deny.is_address_denied(&Address::new([9u8; 32])));
274        assert!(deny.is_object_denied(&obj));
275        assert!(!deny.is_object_denied(&ObjectId::new([9u8; 32])));
276        assert!(deny.is_package_denied(&pkg));
277        assert!(deny.has_denied_addresses());
278        assert!(deny.has_denied_objects());
279        assert!(deny.has_denied_packages());
280        assert!(deny.user_transaction_disabled());
281        assert!(deny.shared_object_disabled());
282        assert!(deny.move_authenticator_disabled());
283        assert!(!deny.package_publish_disabled());
284        assert!(!deny.package_upgrade_disabled());
285        assert!(!deny.receiving_objects_disabled());
286
287        let empty: &dyn DenyRuleConfig = &TransactionDenyConfig::default();
288        assert!(!empty.has_denied_addresses());
289        assert!(!empty.has_denied_objects());
290        assert!(!empty.has_denied_packages());
291    }
292
293    #[test]
294    fn to_deny_rule_set_round_trips_lists_and_switches() {
295        let addr = Address::new([1u8; 32]);
296        let obj = ObjectId::new([2u8; 32]);
297        let pkg = ObjectId::new([3u8; 32]);
298        let rules = TransactionDenyConfigBuilder::new()
299            .add_denied_address(addr)
300            .add_denied_object(obj)
301            .add_denied_package(pkg)
302            .build()
303            .to_deny_rule_set();
304        assert_eq!(rules.denied_addresses, [addr].into());
305        assert_eq!(rules.denied_objects, [obj].into());
306        assert_eq!(rules.denied_packages, [pkg].into());
307
308        // One switch per config: a single sample can't distinguish the six
309        // boolean mappings, so pin each switch position separately.
310        type Setter = fn(TransactionDenyConfigBuilder) -> TransactionDenyConfigBuilder;
311        type Getter = fn(&DenyRuleSet) -> bool;
312        let switches: [(&str, Setter, Getter); 6] = [
313            (
314                "package_publish_disabled",
315                TransactionDenyConfigBuilder::disable_package_publish,
316                |r| r.package_publish_disabled,
317            ),
318            (
319                "package_upgrade_disabled",
320                TransactionDenyConfigBuilder::disable_package_upgrade,
321                |r| r.package_upgrade_disabled,
322            ),
323            (
324                "shared_object_disabled",
325                TransactionDenyConfigBuilder::disable_shared_object_transaction,
326                |r| r.shared_object_disabled,
327            ),
328            (
329                "user_transaction_disabled",
330                TransactionDenyConfigBuilder::disable_user_transaction,
331                |r| r.user_transaction_disabled,
332            ),
333            (
334                "receiving_objects_disabled",
335                TransactionDenyConfigBuilder::disable_receiving_objects,
336                |r| r.receiving_objects_disabled,
337            ),
338            (
339                "move_authenticator_disabled",
340                TransactionDenyConfigBuilder::disable_move_authenticator,
341                |r| r.move_authenticator_disabled,
342            ),
343        ];
344        for (hot, (hot_name, set, _)) in switches.iter().enumerate() {
345            let rules = set(TransactionDenyConfigBuilder::new())
346                .build()
347                .to_deny_rule_set();
348            for (i, (name, _, get)) in switches.iter().enumerate() {
349                assert_eq!(get(&rules), i == hot, "set {hot_name}, checked {name}");
350            }
351        }
352
353        // An empty config produces the default (nothing denied) set — the node
354        // uses this to decide whether to submit a proposal at all.
355        assert_eq!(
356            TransactionDenyConfig::default().to_deny_rule_set(),
357            DenyRuleSet::default()
358        );
359    }
360}