1use iota_sdk_types::{Command, ObjectId, ObjectReference, Transaction, UserSignature};
6use iota_types::{
7 account_abstraction::authenticator_function::AuthenticatorFunctionRef,
8 deny_rule_governance::DenyRuleConfig,
9 error::{IotaError, IotaResult, UserInputError},
10 storage::BackingPackageStore,
11 transaction::{InputObjectKind, TransactionAPI, TransactionKindExt},
12};
13use tracing::instrument;
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#[instrument(level = "trace", skip_all, fields(tx_digest = ?tx.digest()))]
29pub fn check_transaction_for_validation(
30 tx: &Transaction,
31 tx_signatures: &[UserSignature],
32 input_object_kinds: &[InputObjectKind],
33 receiving_objects: &[ObjectReference],
34 filter_config: &dyn DenyRuleConfig,
35 package_store: &dyn BackingPackageStore,
36) -> IotaResult {
37 check_disabled_features(filter_config, tx, tx_signatures)?;
38
39 check_signers(filter_config, tx)?;
40
41 check_input_objects(filter_config, input_object_kinds)?;
42
43 check_package_dependencies(filter_config, tx, package_store)?;
44
45 check_receiving_objects(filter_config, receiving_objects)?;
46
47 Ok(())
48}
49
50#[instrument(level = "trace", skip_all)]
51fn check_receiving_objects(
52 filter_config: &dyn DenyRuleConfig,
53 receiving_objects: &[ObjectReference],
54) -> IotaResult {
55 deny_if_true!(
56 filter_config.receiving_objects_disabled() && !receiving_objects.is_empty(),
57 "Receiving objects is temporarily disabled".to_string()
58 );
59 if !filter_config.has_denied_objects() {
60 return Ok(());
61 }
62 for receiving_object in receiving_objects {
63 deny_if_true!(
64 filter_config.is_object_denied(&receiving_object.object_id),
65 format!(
66 "Access to object {:?} is temporarily disabled",
67 receiving_object.object_id
68 )
69 );
70 }
71 Ok(())
72}
73
74#[instrument(level = "trace", skip_all)]
75fn check_disabled_features(
76 filter_config: &dyn DenyRuleConfig,
77 tx: &Transaction,
78 tx_signatures: &[UserSignature],
79) -> IotaResult {
80 deny_if_true!(
81 filter_config.user_transaction_disabled(),
82 "Transaction signing is temporarily disabled"
83 );
84
85 tx_signatures.iter().try_for_each(|s| {
86 if let UserSignature::MoveAuthenticator(_) = s {
87 deny_if_true!(
88 filter_config.move_authenticator_disabled(),
89 "MoveAuthenticator is temporarily disabled"
90 );
91 }
92 Ok(())
93 })?;
94
95 if !filter_config.package_publish_disabled() && !filter_config.package_upgrade_disabled() {
96 return Ok(());
97 }
98
99 for command in tx.kind().iter_commands() {
100 deny_if_true!(
101 filter_config.package_publish_disabled() && matches!(command, Command::Publish(..)),
102 "Package publish is temporarily disabled"
103 );
104 deny_if_true!(
105 filter_config.package_upgrade_disabled() && matches!(command, Command::Upgrade(..)),
106 "Package upgrade is temporarily disabled"
107 );
108 }
109 Ok(())
110}
111
112#[instrument(level = "trace", skip_all)]
113fn check_signers(filter_config: &dyn DenyRuleConfig, tx: &Transaction) -> IotaResult {
114 if !filter_config.has_denied_addresses() {
115 return Ok(());
116 }
117 for signer in tx.signers() {
118 deny_if_true!(
119 filter_config.is_address_denied(&signer),
120 format!(
121 "Access to account address {:?} is temporarily disabled",
122 signer
123 )
124 );
125 }
126 Ok(())
127}
128
129#[instrument(level = "trace", skip_all)]
130fn check_input_objects(
131 filter_config: &dyn DenyRuleConfig,
132 input_object_kinds: &[InputObjectKind],
133) -> IotaResult {
134 let shared_object_disabled = filter_config.shared_object_disabled();
135 if !filter_config.has_denied_objects() && !shared_object_disabled {
136 return Ok(());
138 }
139 for input_object_kind in input_object_kinds {
140 let id = input_object_kind.object_id();
141 deny_if_true!(
142 filter_config.is_object_denied(&id),
143 format!("Access to input object {id} is temporarily disabled")
144 );
145 deny_if_true!(
146 shared_object_disabled && input_object_kind.is_shared_object(),
147 "Usage of shared object in transactions is temporarily disabled"
148 );
149 }
150 Ok(())
151}
152
153#[instrument(level = "trace", skip_all)]
154fn check_package_dependencies(
155 filter_config: &dyn DenyRuleConfig,
156 tx: &Transaction,
157 package_store: &dyn BackingPackageStore,
158) -> IotaResult {
159 if !filter_config.has_denied_packages() {
160 return Ok(());
161 }
162 let mut dependencies = vec![];
163 for command in tx.kind().iter_commands() {
164 match command {
165 Command::Publish(cmd) => {
166 dependencies.extend(cmd.dependencies.iter().copied());
170 }
171 Command::Upgrade(cmd) => {
172 dependencies.extend(cmd.dependencies.iter().copied());
173 dependencies.push(cmd.package);
176 }
177 Command::MoveCall(cmd) => {
178 dependencies.extend(package_and_dependency_ids(cmd.package, package_store)?);
179 }
180 Command::TransferObjects(..)
181 | Command::SplitCoins(..)
182 | Command::MergeCoins(..)
183 | Command::MakeMoveVector(..) => {}
184 _ => unimplemented!("a new Command enum variant was added and needs to be handled"),
185 }
186 }
187 for dep in dependencies {
188 deny_if_true!(
189 filter_config.is_package_denied(&dep),
190 format!("Access to package {dep} is temporarily disabled")
191 );
192 }
193 Ok(())
194}
195
196#[instrument(level = "trace", skip_all)]
208pub fn check_authenticator_packages<'a>(
209 filter_config: &dyn DenyRuleConfig,
210 authenticator_function_refs: impl IntoIterator<Item = &'a AuthenticatorFunctionRef>,
211 package_store: &dyn BackingPackageStore,
212) -> IotaResult {
213 if !filter_config.has_denied_packages() {
214 return Ok(());
215 }
216 for authenticator_function_ref in authenticator_function_refs {
217 let package_id = match authenticator_function_ref {
218 AuthenticatorFunctionRef::V1(v1) => v1.package,
219 };
220 for dep in package_and_dependency_ids(package_id, package_store)? {
221 deny_if_true!(
222 filter_config.is_package_denied(&dep),
223 format!(
224 "Access to package {dep} from a Move authenticator is temporarily disabled"
225 )
226 );
227 }
228 }
229 Ok(())
230}
231
232fn package_and_dependency_ids(
240 package_id: ObjectId,
241 package_store: &dyn BackingPackageStore,
242) -> IotaResult<Vec<ObjectId>> {
243 let package = package_store
244 .get_package_object(&package_id)?
245 .ok_or(IotaError::UserInput {
246 error: UserInputError::ObjectNotFound {
247 object_id: package_id,
248 version: None,
249 },
250 })?;
251 let package = package.move_package();
252 Ok(package
253 .linkage_table()
254 .values()
255 .map(|upgrade_info| upgrade_info.upgraded_id)
256 .chain(std::iter::once(package.id()))
257 .collect())
258}
259
260#[cfg(test)]
261mod tests {
262 use std::collections::{BTreeMap, HashMap};
263
264 use iota_sdk_types::{
265 DenyRuleSet, MovePackage, ObjectId, TransactionDigest, UpgradeInfo, Version,
266 };
267 use iota_types::{
268 account_abstraction::authenticator_function::{
269 AuthenticatorFunctionRef, AuthenticatorFunctionRefV1,
270 },
271 object::Object,
272 storage::PackageObject,
273 };
274
275 use super::*;
276
277 #[derive(Default)]
280 struct TestPackageStore(HashMap<ObjectId, PackageObject>);
281
282 impl BackingPackageStore for TestPackageStore {
283 fn get_package_object(&self, package_id: &ObjectId) -> IotaResult<Option<PackageObject>> {
284 Ok(self.0.get(package_id).cloned())
285 }
286 }
287
288 impl TestPackageStore {
289 fn with_package(
293 mut self,
294 id: ObjectId,
295 dependencies: impl IntoIterator<Item = ObjectId>,
296 ) -> Self {
297 let linkage_table: BTreeMap<_, _> = dependencies
298 .into_iter()
299 .map(|dep| {
300 (
301 dep,
302 UpgradeInfo {
303 upgraded_id: dep,
304 upgraded_version: Version::OBJECT_START,
305 },
306 )
307 })
308 .collect();
309 let package = MovePackage::new(
310 id,
311 Version::OBJECT_START,
312 BTreeMap::new(),
313 vec![],
314 linkage_table,
315 );
316 self.0.insert(
317 id,
318 PackageObject::new(Object::new_from_package(
319 package,
320 TransactionDigest::genesis_marker(),
321 )),
322 );
323 self
324 }
325 }
326
327 fn id(byte: u8) -> ObjectId {
328 ObjectId::new([byte; 32])
329 }
330
331 fn authenticator_ref(package: ObjectId) -> AuthenticatorFunctionRef {
332 AuthenticatorFunctionRef::V1(AuthenticatorFunctionRefV1 {
333 package,
334 module: "account".to_string(),
335 function: "authenticate".to_string(),
336 })
337 }
338
339 fn deny_packages(packages: impl IntoIterator<Item = ObjectId>) -> DenyRuleSet {
340 DenyRuleSet {
341 denied_packages: packages.into_iter().collect(),
342 ..Default::default()
343 }
344 }
345
346 fn assert_denied(result: IotaResult) {
347 assert!(matches!(
348 result.unwrap_err(),
349 IotaError::UserInput {
350 error: UserInputError::TransactionDenied { .. }
351 }
352 ));
353 }
354
355 #[test]
356 fn denies_the_authenticator_package_itself() {
357 let package = id(1);
358 let store = TestPackageStore::default().with_package(package, []);
359
360 assert_denied(check_authenticator_packages(
361 &deny_packages([package]),
362 &[authenticator_ref(package)],
363 &store,
364 ));
365 }
366
367 #[test]
368 fn denies_a_dependency_of_the_authenticator_package() {
369 let (package, dependency) = (id(1), id(2));
370 let store = TestPackageStore::default().with_package(package, [dependency]);
371
372 assert_denied(check_authenticator_packages(
373 &deny_packages([dependency]),
374 &[authenticator_ref(package)],
375 &store,
376 ));
377 }
378
379 #[test]
380 fn denies_when_any_one_of_several_authenticators_is_denied() {
381 let (sender_package, sponsor_package) = (id(1), id(2));
384 let store = TestPackageStore::default()
385 .with_package(sender_package, [])
386 .with_package(sponsor_package, []);
387
388 assert_denied(check_authenticator_packages(
389 &deny_packages([sender_package]),
390 &[
391 authenticator_ref(sponsor_package),
392 authenticator_ref(sender_package),
393 ],
394 &store,
395 ));
396 }
397
398 #[test]
399 fn allows_a_package_that_is_not_denied() {
400 let (package, dependency, denied) = (id(1), id(2), id(3));
401 let store = TestPackageStore::default().with_package(package, [dependency]);
402
403 check_authenticator_packages(
404 &deny_packages([denied]),
405 &[authenticator_ref(package)],
406 &store,
407 )
408 .unwrap();
409 }
410
411 #[test]
412 fn reads_no_package_when_nothing_is_denied() {
413 check_authenticator_packages(
416 &DenyRuleSet::default(),
417 &[authenticator_ref(id(1))],
418 &TestPackageStore::default(),
419 )
420 .unwrap();
421 }
422
423 #[test]
424 fn reports_a_missing_authenticator_package() {
425 let missing = id(1);
426
427 assert!(matches!(
428 check_authenticator_packages(
429 &deny_packages([id(9)]),
430 &[authenticator_ref(missing)],
431 &TestPackageStore::default(),
432 )
433 .unwrap_err(),
434 IotaError::UserInput {
435 error: UserInputError::ObjectNotFound { object_id, .. }
436 } if object_id == missing
437 ));
438 }
439}