1use std::collections::BTreeSet;
5
6use serde::{Deserialize, Serialize};
7
8use crate::base_types::{ObjectID, SuiAddress};
9
10#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, Deserialize, Serialize)]
13#[serde(rename_all = "kebab-case")]
14pub struct TransactionDenyRules {
15 #[serde(default)]
20 pub object_deny_list: BTreeSet<ObjectID>,
21
22 #[serde(default)]
30 pub package_deny_list: BTreeSet<ObjectID>,
31
32 #[serde(default)]
34 pub address_deny_list: BTreeSet<SuiAddress>,
35
36 #[serde(default)]
38 pub package_publish_disabled: bool,
39
40 #[serde(default)]
42 pub package_upgrade_disabled: bool,
43
44 #[serde(default)]
46 pub shared_object_disabled: bool,
47
48 #[serde(default)]
51 pub user_transaction_disabled: bool,
52
53 #[serde(default)]
55 pub gasless_disabled: bool,
56
57 #[serde(default)]
59 pub receiving_objects_disabled: bool,
60
61 #[serde(default)]
63 pub zklogin_sig_disabled: bool,
64
65 #[serde(default)]
67 pub zklogin_disabled_providers: BTreeSet<String>,
68}
69
70impl TransactionDenyRules {
71 pub const MAX_SHARE_ENTRIES: usize = 5_000;
74
75 pub const MAX_ZKLOGIN_PROVIDER_LENGTH: usize = 256;
78
79 pub const MAX_SHARE_SERIALIZED_BYTES: u64 = 200 * 1024;
81
82 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 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 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 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 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#[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#[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 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 #[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 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}