Skip to main content

sui_types/
transaction_deny_rules.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::BTreeSet;
5
6use serde::{Deserialize, Serialize};
7
8use crate::base_types::{ObjectID, SuiAddress};
9
10/// The shareable subset of `TransactionDenyConfig`, rules for transactions that
11/// the node will refuse.
12#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, Deserialize, Serialize)]
13#[serde(rename_all = "kebab-case")]
14pub struct TransactionDenyRules {
15    /// A list of object IDs that are not allowed to be accessed/used in transactions.
16    /// Note that since this is checked during transaction signing, only root object ids
17    /// are supported here (i.e. no child-objects).
18    /// Similarly this does not apply to wrapped objects as they are not directly accessible.
19    #[serde(default)]
20    pub object_deny_list: BTreeSet<ObjectID>,
21
22    /// A list of package object IDs that are not allowed to be called into in transactions,
23    /// either directly or indirectly through transitive dependencies.
24    /// Note that this does not apply to type arguments.
25    /// Also since we only compare the deny list against the upgraded package ID of each dependency
26    /// in the used package, when a package ID is denied, newer versions of that package are
27    /// still allowed. If we want to deny the entire upgrade family of a package, we need to
28    /// explicitly specify all the package IDs in the deny list.
29    #[serde(default)]
30    pub package_deny_list: BTreeSet<ObjectID>,
31
32    /// A list of sui addresses that are not allowed to be used as the sender or sponsor.
33    #[serde(default)]
34    pub address_deny_list: BTreeSet<SuiAddress>,
35
36    /// Whether publishing new packages is disabled.
37    #[serde(default)]
38    pub package_publish_disabled: bool,
39
40    /// Whether upgrading existing packages is disabled.
41    #[serde(default)]
42    pub package_upgrade_disabled: bool,
43
44    /// Whether usage of shared objects is disabled.
45    #[serde(default)]
46    pub shared_object_disabled: bool,
47
48    /// Whether user transactions are disabled (i.e. only system transactions are allowed).
49    /// This is essentially a kill switch for transactions processing to a degree.
50    #[serde(default)]
51    pub user_transaction_disabled: bool,
52
53    /// Whether gasless transactions are disabled.
54    #[serde(default)]
55    pub gasless_disabled: bool,
56
57    /// Whether receiving objects transferred to other objects is allowed.
58    #[serde(default)]
59    pub receiving_objects_disabled: bool,
60
61    /// Whether zklogin transaction is disabled.
62    #[serde(default)]
63    pub zklogin_sig_disabled: bool,
64
65    /// A list of disabled OAuth providers for zkLogin.
66    #[serde(default)]
67    pub zklogin_disabled_providers: BTreeSet<String>,
68}
69
70impl TransactionDenyRules {
71    /// Maximum total entry count across all set fields permitted in a single
72    /// peer-broadcast deny rules message.
73    pub const MAX_SHARE_ENTRIES: usize = 5_000;
74
75    /// Maximum length, in bytes, of any single zkLogin provider string accepted from
76    /// a peer recommendation. Real provider names are short (e.g. "Google", "Apple").
77    pub const MAX_ZKLOGIN_PROVIDER_LENGTH: usize = 256;
78
79    /// Maximum BCS-serialized size of a shared rules payload.
80    pub const MAX_SHARE_SERIALIZED_BYTES: u64 = 200 * 1024;
81
82    /// Union-merge another rules set into this one. Set fields are extended; boolean
83    /// fields are OR'd.
84    pub fn merge(&mut self, other: &Self) {
85        self.object_deny_list
86            .extend(other.object_deny_list.iter().copied());
87        self.package_deny_list
88            .extend(other.package_deny_list.iter().copied());
89        self.address_deny_list
90            .extend(other.address_deny_list.iter().copied());
91        self.package_publish_disabled |= other.package_publish_disabled;
92        self.package_upgrade_disabled |= other.package_upgrade_disabled;
93        self.shared_object_disabled |= other.shared_object_disabled;
94        self.user_transaction_disabled |= other.user_transaction_disabled;
95        self.gasless_disabled |= other.gasless_disabled;
96        self.receiving_objects_disabled |= other.receiving_objects_disabled;
97        self.zklogin_sig_disabled |= other.zklogin_sig_disabled;
98        self.zklogin_disabled_providers
99            .extend(other.zklogin_disabled_providers.iter().cloned());
100    }
101
102    /// Returns true if `self` contains every rule in `other`: each set field of
103    /// `other` is a subset of `self`'s, and every boolean set in `other` is set in
104    /// `self`. A peer's proposal "votes for" a pre-listed config only when it is a
105    /// superset of that config's rules.
106    pub fn is_superset_of(&self, other: &Self) -> bool {
107        self.object_deny_list.is_superset(&other.object_deny_list)
108            && self.package_deny_list.is_superset(&other.package_deny_list)
109            && self.address_deny_list.is_superset(&other.address_deny_list)
110            && self
111                .zklogin_disabled_providers
112                .is_superset(&other.zklogin_disabled_providers)
113            && (self.package_publish_disabled || !other.package_publish_disabled)
114            && (self.package_upgrade_disabled || !other.package_upgrade_disabled)
115            && (self.shared_object_disabled || !other.shared_object_disabled)
116            && (self.user_transaction_disabled || !other.user_transaction_disabled)
117            && (self.gasless_disabled || !other.gasless_disabled)
118            && (self.receiving_objects_disabled || !other.receiving_objects_disabled)
119            && (self.zklogin_sig_disabled || !other.zklogin_sig_disabled)
120    }
121
122    /// Iterate the individual rule elements of this rules set: one item per deny-list
123    /// entry and one per `true` boolean flag.
124    pub fn elements(&self) -> impl Iterator<Item = DenyElement> + '_ {
125        self.object_deny_list
126            .iter()
127            .map(|o| DenyElement::Object(*o))
128            .chain(
129                self.package_deny_list
130                    .iter()
131                    .map(|p| DenyElement::Package(*p)),
132            )
133            .chain(
134                self.address_deny_list
135                    .iter()
136                    .map(|a| DenyElement::Address(*a)),
137            )
138            .chain(
139                self.zklogin_disabled_providers
140                    .iter()
141                    .map(|p| DenyElement::ZkLoginProvider(p.clone())),
142            )
143            .chain(
144                self.package_publish_disabled
145                    .then_some(DenyElement::PackagePublishDisabled),
146            )
147            .chain(
148                self.package_upgrade_disabled
149                    .then_some(DenyElement::PackageUpgradeDisabled),
150            )
151            .chain(
152                self.shared_object_disabled
153                    .then_some(DenyElement::SharedObjectDisabled),
154            )
155            .chain(
156                self.user_transaction_disabled
157                    .then_some(DenyElement::UserTransactionDisabled),
158            )
159            .chain(
160                self.gasless_disabled
161                    .then_some(DenyElement::GaslessDisabled),
162            )
163            .chain(
164                self.receiving_objects_disabled
165                    .then_some(DenyElement::ReceivingObjectsDisabled),
166            )
167            .chain(
168                self.zklogin_sig_disabled
169                    .then_some(DenyElement::ZkLoginSigDisabled),
170            )
171    }
172
173    /// Apply a single rule element to this rules set.
174    pub fn apply_element(&mut self, element: &DenyElement) {
175        match element {
176            DenyElement::Object(o) => {
177                self.object_deny_list.insert(*o);
178            }
179            DenyElement::Package(p) => {
180                self.package_deny_list.insert(*p);
181            }
182            DenyElement::Address(a) => {
183                self.address_deny_list.insert(*a);
184            }
185            DenyElement::ZkLoginProvider(p) => {
186                self.zklogin_disabled_providers.insert(p.clone());
187            }
188            DenyElement::PackagePublishDisabled => self.package_publish_disabled = true,
189            DenyElement::PackageUpgradeDisabled => self.package_upgrade_disabled = true,
190            DenyElement::SharedObjectDisabled => self.shared_object_disabled = true,
191            DenyElement::UserTransactionDisabled => self.user_transaction_disabled = true,
192            DenyElement::GaslessDisabled => self.gasless_disabled = true,
193            DenyElement::ReceivingObjectsDisabled => self.receiving_objects_disabled = true,
194            DenyElement::ZkLoginSigDisabled => self.zklogin_sig_disabled = true,
195        }
196    }
197
198    pub fn is_empty(&self) -> bool {
199        *self == Self::default()
200    }
201
202    pub fn entry_count(&self) -> usize {
203        self.object_deny_list.len()
204            + self.package_deny_list.len()
205            + self.address_deny_list.len()
206            + self.zklogin_disabled_providers.len()
207    }
208
209    /// Reject rules that are too large to share via consensus. Used by both the
210    /// admin endpoint (before submission) and the consensus validator (on receive)
211    /// so the limits can't drift between the two checks.
212    pub fn check_share_limits(&self) -> Result<(), String> {
213        if self.entry_count() > Self::MAX_SHARE_ENTRIES {
214            return Err(format!(
215                "rules entry count {} exceeds limit ({})",
216                self.entry_count(),
217                Self::MAX_SHARE_ENTRIES,
218            ));
219        }
220        for provider in &self.zklogin_disabled_providers {
221            if provider.len() > Self::MAX_ZKLOGIN_PROVIDER_LENGTH {
222                return Err(format!(
223                    "zklogin provider name too long: {} bytes (max {})",
224                    provider.len(),
225                    Self::MAX_ZKLOGIN_PROVIDER_LENGTH,
226                ));
227            }
228        }
229        let serialized_size = bcs::serialized_size(self)
230            .map_err(|e| format!("failed to compute rules serialized size: {e}"))?
231            as u64;
232        if serialized_size > Self::MAX_SHARE_SERIALIZED_BYTES {
233            return Err(format!(
234                "rules serialized size {serialized_size} bytes exceeds limit ({})",
235                Self::MAX_SHARE_SERIALIZED_BYTES,
236            ));
237        }
238        Ok(())
239    }
240}
241
242/// A single indivisible rule within a `TransactionDenyRules`: one deny-list entry or
243/// one boolean kill switch. The "default" deny-config bucket votes on these
244/// individually.
245#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
246pub enum DenyElement {
247    Object(ObjectID),
248    Package(ObjectID),
249    Address(SuiAddress),
250    ZkLoginProvider(String),
251    PackagePublishDisabled,
252    PackageUpgradeDisabled,
253    SharedObjectDisabled,
254    UserTransactionDisabled,
255    GaslessDisabled,
256    ReceivingObjectsDisabled,
257    ZkLoginSigDisabled,
258}
259
260impl DenyElement {
261    pub fn kind(&self) -> DenyElementKind {
262        match self {
263            DenyElement::Object(_) => DenyElementKind::Object,
264            DenyElement::Package(_) => DenyElementKind::Package,
265            DenyElement::Address(_) => DenyElementKind::Address,
266            DenyElement::ZkLoginProvider(_) => DenyElementKind::ZkLoginProvider,
267            DenyElement::PackagePublishDisabled => DenyElementKind::PackagePublishDisabled,
268            DenyElement::PackageUpgradeDisabled => DenyElementKind::PackageUpgradeDisabled,
269            DenyElement::SharedObjectDisabled => DenyElementKind::SharedObjectDisabled,
270            DenyElement::UserTransactionDisabled => DenyElementKind::UserTransactionDisabled,
271            DenyElement::GaslessDisabled => DenyElementKind::GaslessDisabled,
272            DenyElement::ReceivingObjectsDisabled => DenyElementKind::ReceivingObjectsDisabled,
273            DenyElement::ZkLoginSigDisabled => DenyElementKind::ZkLoginSigDisabled,
274        }
275    }
276}
277
278/// A `DenyElement` stripped of its payload — the category that the per-kind default
279/// buckets in `PeerDenySyncConfig` partition over.
280#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
281#[serde(rename_all = "kebab-case")]
282pub enum DenyElementKind {
283    Object,
284    Package,
285    Address,
286    ZkLoginProvider,
287    PackagePublishDisabled,
288    PackageUpgradeDisabled,
289    SharedObjectDisabled,
290    UserTransactionDisabled,
291    GaslessDisabled,
292    ReceivingObjectsDisabled,
293    ZkLoginSigDisabled,
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use crate::base_types::dbg_addr;
300
301    #[test]
302    fn merge_unions_sets_and_ors_bools() {
303        let mut a = TransactionDenyRules::default();
304        a.object_deny_list.insert(ObjectID::from_single_byte(1));
305        a.address_deny_list.insert(dbg_addr(2));
306        a.package_publish_disabled = true;
307
308        let mut b = TransactionDenyRules::default();
309        b.object_deny_list.insert(ObjectID::from_single_byte(3));
310        b.address_deny_list.insert(dbg_addr(2));
311        b.user_transaction_disabled = true;
312        b.zklogin_disabled_providers.insert("Google".to_string());
313
314        a.merge(&b);
315
316        assert_eq!(a.object_deny_list.len(), 2);
317        assert!(a.object_deny_list.contains(&ObjectID::from_single_byte(1)));
318        assert!(a.object_deny_list.contains(&ObjectID::from_single_byte(3)));
319        assert_eq!(a.address_deny_list.len(), 1);
320        assert!(a.package_publish_disabled);
321        assert!(a.user_transaction_disabled);
322        assert!(a.zklogin_disabled_providers.contains("Google"));
323    }
324
325    #[test]
326    fn entry_count_sums_set_lengths() {
327        let mut r = TransactionDenyRules::default();
328        r.object_deny_list.insert(ObjectID::from_single_byte(1));
329        r.object_deny_list.insert(ObjectID::from_single_byte(2));
330        r.package_deny_list.insert(ObjectID::from_single_byte(3));
331        r.address_deny_list.insert(dbg_addr(4));
332        r.zklogin_disabled_providers.insert("a".to_string());
333        assert_eq!(r.entry_count(), 5);
334    }
335
336    #[test]
337    fn is_superset_of_sets_and_bools() {
338        let mut larger = TransactionDenyRules::default();
339        larger
340            .object_deny_list
341            .insert(ObjectID::from_single_byte(1));
342        larger
343            .object_deny_list
344            .insert(ObjectID::from_single_byte(2));
345        larger.package_publish_disabled = true;
346
347        let mut smaller = TransactionDenyRules::default();
348        smaller
349            .object_deny_list
350            .insert(ObjectID::from_single_byte(1));
351        smaller.package_publish_disabled = true;
352
353        assert!(larger.is_superset_of(&smaller));
354        assert!(larger.is_superset_of(&TransactionDenyRules::default()));
355        assert!(!smaller.is_superset_of(&larger));
356
357        // A boolean set in `other` but not `self` breaks the superset relation.
358        let mut other_bool = TransactionDenyRules::default();
359        other_bool
360            .object_deny_list
361            .insert(ObjectID::from_single_byte(1));
362        other_bool.user_transaction_disabled = true;
363        assert!(!larger.is_superset_of(&other_bool));
364    }
365
366    #[test]
367    fn deny_element_kind_matches_variant() {
368        let mut rules = TransactionDenyRules::default();
369        rules.object_deny_list.insert(ObjectID::from_single_byte(1));
370        rules
371            .package_deny_list
372            .insert(ObjectID::from_single_byte(2));
373        rules.address_deny_list.insert(dbg_addr(3));
374        rules
375            .zklogin_disabled_providers
376            .insert("Google".to_string());
377        rules.package_publish_disabled = true;
378        rules.package_upgrade_disabled = true;
379        rules.shared_object_disabled = true;
380        rules.user_transaction_disabled = true;
381        rules.gasless_disabled = true;
382        rules.receiving_objects_disabled = true;
383        rules.zklogin_sig_disabled = true;
384
385        let expected_kinds = [
386            DenyElementKind::Object,
387            DenyElementKind::Package,
388            DenyElementKind::Address,
389            DenyElementKind::ZkLoginProvider,
390            DenyElementKind::PackagePublishDisabled,
391            DenyElementKind::PackageUpgradeDisabled,
392            DenyElementKind::SharedObjectDisabled,
393            DenyElementKind::UserTransactionDisabled,
394            DenyElementKind::GaslessDisabled,
395            DenyElementKind::ReceivingObjectsDisabled,
396            DenyElementKind::ZkLoginSigDisabled,
397        ];
398
399        let actual_kinds: BTreeSet<DenyElementKind> = rules.elements().map(|e| e.kind()).collect();
400        let expected: BTreeSet<DenyElementKind> = expected_kinds.iter().copied().collect();
401        assert_eq!(actual_kinds, expected);
402    }
403
404    /// A payload within the entry and per-string caps can still exceed the wire
405    /// budget: ~1,100 max-length provider strings BCS-encode past the serialized-size
406    /// limit while sitting at a fifth of the entry cap.
407    #[test]
408    fn check_share_limits_bounds_serialized_size() {
409        let rules = TransactionDenyRules {
410            zklogin_disabled_providers: (0..1_100)
411                .map(|i| {
412                    format!(
413                        "{i:0>width$}",
414                        width = TransactionDenyRules::MAX_ZKLOGIN_PROVIDER_LENGTH
415                    )
416                })
417                .collect(),
418            ..Default::default()
419        };
420        assert!(rules.entry_count() < TransactionDenyRules::MAX_SHARE_ENTRIES);
421        let err = rules.check_share_limits().unwrap_err();
422        assert!(err.contains("serialized size"), "{err}");
423
424        // A maximal all-ID config stays within the byte budget: the entry cap is the
425        // binding limit for fixed-size entries.
426        let max_ids = TransactionDenyRules {
427            object_deny_list: (0..TransactionDenyRules::MAX_SHARE_ENTRIES)
428                .map(|i| {
429                    let mut bytes = [0u8; ObjectID::LENGTH];
430                    bytes[..8].copy_from_slice(&(i as u64).to_be_bytes());
431                    ObjectID::from_bytes(bytes).unwrap()
432                })
433                .collect(),
434            ..Default::default()
435        };
436        assert_eq!(
437            max_ids.entry_count(),
438            TransactionDenyRules::MAX_SHARE_ENTRIES
439        );
440        max_ids.check_share_limits().unwrap();
441    }
442
443    #[test]
444    fn elements_round_trips_through_apply_element() {
445        let mut original = TransactionDenyRules::default();
446        original
447            .object_deny_list
448            .insert(ObjectID::from_single_byte(1));
449        original
450            .package_deny_list
451            .insert(ObjectID::from_single_byte(2));
452        original.address_deny_list.insert(dbg_addr(3));
453        original
454            .zklogin_disabled_providers
455            .insert("Google".to_string());
456        original.package_publish_disabled = true;
457        original.user_transaction_disabled = true;
458        original.zklogin_sig_disabled = true;
459
460        let elements: Vec<DenyElement> = original.elements().collect();
461        assert_eq!(elements.len(), 7);
462
463        let mut rebuilt = TransactionDenyRules::default();
464        for element in &elements {
465            rebuilt.apply_element(element);
466        }
467        assert_eq!(rebuilt, original);
468    }
469}