1pub mod deny;
5
6pub use checked::*;
7
8#[sui_macros::with_checked_arithmetic]
9mod checked {
10 use std::collections::{BTreeMap, HashSet};
11 use std::sync::Arc;
12 use sui_config::verifier_signing_config::VerifierSigningConfig;
13 use sui_protocol_config::ProtocolConfig;
14 use sui_types::base_types::{ObjectID, ObjectRef};
15 use sui_types::error::{SuiResult, UserInputError, UserInputResult};
16 use sui_types::executable_transaction::VerifiedExecutableTransaction;
17 use sui_types::gas::SuiGasStatusAPI;
18 use sui_types::metrics::BytecodeVerifierMetrics;
19 use sui_types::object::ObjectPermission;
20 use sui_types::transaction::{
21 CheckedInputObjects, InputObjectKind, InputObjects, ObjectReadResultKind,
22 ReceivingObjectReadResult, ReceivingObjects, SharedObjectMutability, TransactionData,
23 TransactionDataAPI, TransactionKind,
24 };
25 use sui_types::{
26 SUI_ACCUMULATOR_ROOT_OBJECT_ID, SUI_ADDRESS_ALIAS_STATE_OBJECT_ID, SUI_BRIDGE_OBJECT_ID,
27 SUI_CLOCK_OBJECT_ID, SUI_COIN_REGISTRY_OBJECT_ID, SUI_DENY_LIST_OBJECT_ID,
28 SUI_DISPLAY_REGISTRY_OBJECT_ID, SUI_RANDOMNESS_STATE_OBJECT_ID, SUI_SYSTEM_STATE_OBJECT_ID,
29 };
30 use sui_types::{
31 base_types::{SequenceNumber, SuiAddress},
32 coin_reservation::ParsedDigest,
33 error::SuiError,
34 fp_bail, fp_ensure,
35 gas::SuiGasStatus,
36 object::{Object, Owner},
37 };
38 use tracing::error;
39 use tracing::instrument;
40
41 trait IntoChecked {
42 fn into_checked(self) -> CheckedInputObjects;
43 }
44
45 impl IntoChecked for InputObjects {
46 fn into_checked(self) -> CheckedInputObjects {
47 CheckedInputObjects::new_with_checked_transaction_inputs(self)
48 }
49 }
50
51 fn get_gas_status(
56 objects: &InputObjects,
57 gas: &[ObjectRef],
58 protocol_config: &ProtocolConfig,
59 reference_gas_price: u64,
60 transaction: &TransactionData,
61 gas_ownership_checks: bool,
62 ) -> SuiResult<SuiGasStatus> {
63 if transaction.kind().is_system_tx() {
64 Ok(SuiGasStatus::new_unmetered(protocol_config))
65 } else {
66 let is_gasless =
67 protocol_config.enable_gasless() && transaction.is_gasless_transaction();
68 check_gas(
69 objects,
70 protocol_config,
71 reference_gas_price,
72 gas,
73 transaction,
74 gas_ownership_checks,
75 is_gasless,
76 )
77 }
78 }
79
80 #[instrument(level = "trace", skip_all)]
81 pub fn check_transaction_input(
82 protocol_config: &ProtocolConfig,
83 reference_gas_price: u64,
84 transaction: &TransactionData,
85 input_objects: InputObjects,
86 receiving_objects: &ReceivingObjects,
87 metrics: &Arc<BytecodeVerifierMetrics>,
88 verifier_signing_config: &VerifierSigningConfig,
89 ) -> SuiResult<(SuiGasStatus, CheckedInputObjects)> {
90 let gas_status = check_transaction_input_inner(
91 protocol_config,
92 reference_gas_price,
93 transaction,
94 &input_objects,
95 &[],
96 )?;
97 transaction.check_allowance_inputs(&input_objects)?;
98 check_receiving_objects(&input_objects, receiving_objects)?;
99 check_non_system_packages_to_be_published(
101 transaction,
102 protocol_config,
103 metrics,
104 verifier_signing_config,
105 )?;
106
107 Ok((gas_status, input_objects.into_checked()))
108 }
109
110 #[instrument(level = "trace", skip_all)]
115 pub fn check_certificate_input(
116 cert: &VerifiedExecutableTransaction,
117 input_objects: InputObjects,
118 protocol_config: &ProtocolConfig,
119 reference_gas_price: u64,
120 ) -> SuiResult<(SuiGasStatus, CheckedInputObjects)> {
121 let transaction = cert.data().transaction_data();
122 let gas_status = check_transaction_input_inner(
123 protocol_config,
124 reference_gas_price,
125 transaction,
126 &input_objects,
127 &[],
128 )?;
129 Ok((gas_status, input_objects.into_checked()))
135 }
136
137 pub fn check_dev_inspect_input(
140 config: &ProtocolConfig,
141 transaction: &TransactionData,
142 input_objects: InputObjects,
143 _receiving_objects: ReceivingObjects,
145 reference_gas_price: u64,
146 ) -> SuiResult<(SuiGasStatus, CheckedInputObjects)> {
147 let kind = transaction.kind();
148 kind.validity_check(config)?;
149 if kind.is_system_tx() {
150 return Err(UserInputError::Unsupported(format!(
151 "Transaction kind {} is not supported in dev-inspect",
152 kind
153 ))
154 .into());
155 }
156 let mut used_objects: HashSet<SuiAddress> = HashSet::new();
157 for input_object in input_objects.iter() {
158 let Some(object) = input_object.as_object() else {
159 continue;
161 };
162
163 if !object.is_immutable() {
164 fp_ensure!(
165 used_objects.insert(object.id().into()),
166 UserInputError::MutableObjectUsedMoreThanOnce {
167 object_id: object.id()
168 }
169 .into()
170 );
171 }
172 }
173
174 let gas_status = get_gas_status(
175 &input_objects,
176 &transaction.gas_data().payment, config,
178 reference_gas_price,
179 transaction,
180 false, )?;
182
183 Ok((gas_status, input_objects.into_checked()))
184 }
185
186 fn check_transaction_input_inner(
188 protocol_config: &ProtocolConfig,
189 reference_gas_price: u64,
190 transaction: &TransactionData,
191 input_objects: &InputObjects,
192 gas_override: &[ObjectRef],
194 ) -> SuiResult<SuiGasStatus> {
195 let gas = if gas_override.is_empty() {
196 transaction.gas()
197 } else {
198 gas_override
199 };
200
201 let gas_status = get_gas_status(
202 input_objects,
203 gas,
204 protocol_config,
205 reference_gas_price,
206 transaction,
207 true, )?;
209 check_objects(transaction, input_objects, protocol_config)?;
210 check_replay_protection(transaction, input_objects)?;
211
212 if protocol_config.enable_gasless() && transaction.is_gasless_transaction() {
213 check_gasless_object_inputs(input_objects, protocol_config)?;
214 }
215
216 Ok(gas_status)
217 }
218
219 fn check_replay_protection(
227 transaction: &TransactionData,
228 input_objects: &InputObjects,
229 ) -> UserInputResult<()> {
230 let has_replay_protection = transaction.expiration().is_replay_protected()
231 || !transaction.gas_data().payment.is_empty()
232 || input_objects
233 .iter()
234 .any(|obj| obj.is_replay_protected_input());
235
236 if !has_replay_protection {
237 return Err(UserInputError::InvalidExpiration {
238 error: "Transactions must either have address-owned inputs, or a ValidDuring expiration with at most two epochs of validity"
239 .to_string(),
240 });
241 }
242
243 Ok(())
244 }
245
246 fn check_receiving_objects(
247 input_objects: &InputObjects,
248 receiving_objects: &ReceivingObjects,
249 ) -> Result<(), SuiError> {
250 let mut objects_in_txn: HashSet<_> = input_objects
251 .object_kinds()
252 .map(|x| x.object_id())
253 .collect();
254
255 for ReceivingObjectReadResult {
263 object_ref: (object_id, version, object_digest),
264 object,
265 } in receiving_objects.iter()
266 {
267 fp_ensure!(
268 *version < SequenceNumber::MAX,
269 UserInputError::InvalidSequenceNumber.into()
270 );
271
272 let Some(object) = object.as_object() else {
273 continue;
275 };
276
277 if !(object.owner.is_address_owned()
278 && object.version() == *version
279 && object.digest() == *object_digest)
280 {
281 fp_ensure!(
283 object.version() == *version,
284 UserInputError::ObjectVersionUnavailableForConsumption {
285 provided_obj_ref: (*object_id, *version, *object_digest),
286 current_version: object.version(),
287 }
288 .into()
289 );
290
291 fp_ensure!(
293 !object.is_package(),
294 UserInputError::MovePackageAsObject {
295 object_id: *object_id
296 }
297 .into()
298 );
299
300 let expected_digest = object.digest();
302 fp_ensure!(
303 expected_digest == *object_digest,
304 UserInputError::InvalidObjectDigest {
305 object_id: *object_id,
306 expected_digest
307 }
308 .into()
309 );
310
311 match object.owner {
312 Owner::AddressOwner(_) => {
313 debug_assert!(
314 false,
315 "Receiving object {:?} is invalid but we expect it should be valid. {:?}",
316 (*object_id, *version, *object_id),
317 object
318 );
319 error!(
320 "Receiving object {:?} is invalid but we expect it should be valid. {:?}",
321 (*object_id, *version, *object_id),
322 object
323 );
324 fp_bail!(
327 UserInputError::ObjectNotFound {
328 object_id: *object_id,
329 version: Some(*version),
330 }
331 .into()
332 )
333 }
334 Owner::ObjectOwner(owner) => {
335 fp_bail!(
336 UserInputError::InvalidChildObjectArgument {
337 child_id: object.id(),
338 parent_id: owner.into(),
339 }
340 .into()
341 )
342 }
343 Owner::Shared { .. }
344 | Owner::ConsensusAddressOwner { .. }
345 | Owner::Party { .. } => {
346 fp_bail!(UserInputError::NotSharedObjectError.into())
347 }
348 Owner::Immutable => fp_bail!(
349 UserInputError::MutableParameterExpected {
350 object_id: *object_id
351 }
352 .into()
353 ),
354 };
355 }
356
357 fp_ensure!(
358 !objects_in_txn.contains(object_id),
359 UserInputError::DuplicateObjectRefInput.into()
360 );
361
362 objects_in_txn.insert(*object_id);
363 }
364 Ok(())
365 }
366
367 #[instrument(level = "trace", skip_all)]
370 fn check_gas(
371 objects: &InputObjects,
372 protocol_config: &ProtocolConfig,
373 reference_gas_price: u64,
374 gas: &[ObjectRef],
375 transaction: &TransactionData,
376 gas_ownership_checks: bool,
377 is_gasless: bool,
378 ) -> SuiResult<SuiGasStatus> {
379 let gas_budget = transaction.gas_budget();
380 let gas_price = transaction.gas_price();
381 let gas_paid_from_address_balance = transaction.is_gas_paid_from_address_balance();
382
383 let gas_status = if is_gasless {
384 debug_assert_ne!(reference_gas_price, 0);
385 let rgp = reference_gas_price.max(1);
386 let compute_cap = protocol_config.gasless_max_computation_units() * rgp;
387 SuiGasStatus::new(compute_cap, rgp, reference_gas_price, protocol_config)?
388 } else {
389 SuiGasStatus::new(gas_budget, gas_price, reference_gas_price, protocol_config)?
390 };
391
392 let objects: BTreeMap<_, _> = objects.iter().map(|o| (o.id(), o)).collect();
395
396 let (gas_objects, available_address_balance_gas) = if gas_paid_from_address_balance {
397 (vec![], gas_budget)
400 } else {
401 let mut available_address_balance_gas: u64 = 0;
404 let mut gas_objects = vec![];
405 for obj_ref in gas {
406 if let Ok(parsed) = ParsedDigest::try_from(obj_ref.2) {
407 available_address_balance_gas =
408 available_address_balance_gas.saturating_add(parsed.reservation_amount());
409 } else {
410 let obj = objects.get(&obj_ref.0);
411 let obj = *obj.ok_or(UserInputError::ObjectNotFound {
412 object_id: obj_ref.0,
413 version: Some(obj_ref.1),
414 })?;
415 gas_objects.push(obj);
416 }
417 }
418 (gas_objects, available_address_balance_gas)
419 };
420
421 if !is_gasless {
422 if gas_ownership_checks {
423 gas_status.check_gas_objects(&gas_objects)?;
424 }
425 gas_status.check_gas_balance(
426 &gas_objects,
427 gas_budget,
428 available_address_balance_gas,
429 )?;
430 }
431 Ok(gas_status)
432 }
433
434 #[instrument(level = "trace", skip_all)]
437 fn check_objects(
438 transaction: &TransactionData,
439 objects: &InputObjects,
440 protocol_config: &ProtocolConfig,
441 ) -> UserInputResult<()> {
442 let mut used_objects: HashSet<SuiAddress> = HashSet::new();
444 for object in objects.iter() {
445 if object.is_mutable() {
446 fp_ensure!(
447 used_objects.insert(object.id().into()),
448 UserInputError::MutableObjectUsedMoreThanOnce {
449 object_id: object.id()
450 }
451 );
452 }
453 }
454
455 let gas_only_contains_coin_reservations = !transaction.gas().is_empty()
459 && transaction
460 .gas()
461 .iter()
462 .all(|obj_ref| ParsedDigest::is_coin_reservation_digest(&obj_ref.2));
463
464 let allow_empty_objects = protocol_config.enable_coin_reservation_obj_refs()
465 && (transaction.is_gas_paid_from_address_balance()
466 || gas_only_contains_coin_reservations);
467 if !transaction.is_genesis_tx() && objects.is_empty() && !allow_empty_objects {
468 return Err(UserInputError::ObjectInputArityViolation);
469 }
470
471 let gas_coins: HashSet<ObjectID> =
472 HashSet::from_iter(transaction.gas().iter().map(|obj_ref| obj_ref.0));
473 for object in objects.iter() {
474 let input_object_kind = object.input_object_kind;
475
476 match &object.object {
477 ObjectReadResultKind::Object(object) => {
478 let owner_address = if gas_coins.contains(&object.id()) {
480 transaction.gas_owner()
481 } else {
482 transaction.sender()
483 };
484 let system_transaction = transaction.is_system_tx();
487 check_one_object(
488 &owner_address,
489 input_object_kind,
490 object,
491 system_transaction,
492 )?;
493 }
494 ObjectReadResultKind::ObjectConsensusStreamEnded(_, _) => (),
496 ObjectReadResultKind::CancelledTransactionSharedObject(_) => (),
498 }
499 }
500
501 Ok(())
502 }
503
504 fn check_one_object(
506 owner: &SuiAddress,
507 object_kind: InputObjectKind,
508 object: &Object,
509 system_transaction: bool,
510 ) -> UserInputResult {
511 if matches!(object.owner, Owner::Party { .. }) {
513 return Err(UserInputError::Unsupported(
514 "Party-owned objects are not yet supported".to_string(),
515 ));
516 }
517
518 match object_kind {
519 InputObjectKind::MovePackage(package_id) => {
520 fp_ensure!(
521 object.data.try_as_package().is_some(),
522 UserInputError::MoveObjectAsPackage {
523 object_id: package_id
524 }
525 );
526 }
527 InputObjectKind::ImmOrOwnedMoveObject((object_id, sequence_number, object_digest)) => {
528 fp_ensure!(
529 !object.is_package(),
530 UserInputError::MovePackageAsObject { object_id }
531 );
532 fp_ensure!(
533 sequence_number < SequenceNumber::MAX,
534 UserInputError::InvalidSequenceNumber
535 );
536
537 assert_eq!(
539 object.version(),
540 sequence_number,
541 "The fetched object version {} does not match the requested version {}, object id: {}",
542 object.version(),
543 sequence_number,
544 object.id(),
545 );
546
547 let expected_digest = object.digest();
549 fp_ensure!(
550 expected_digest == object_digest,
551 UserInputError::InvalidObjectDigest {
552 object_id,
553 expected_digest
554 }
555 );
556
557 match object.owner {
558 Owner::Immutable => {
559 }
561 Owner::AddressOwner(actual_owner) => {
562 fp_ensure!(
564 owner == &actual_owner,
565 UserInputError::IncorrectUserSignature {
566 error: format!(
567 "Object {object_id:?} is owned by account address {actual_owner:?}, but given owner/signer address is {owner:?}"
568 ),
569 }
570 );
571 }
572 Owner::ObjectOwner(owner) => {
573 return Err(UserInputError::InvalidChildObjectArgument {
574 child_id: object.id(),
575 parent_id: owner.into(),
576 });
577 }
578 Owner::Shared { .. }
579 | Owner::ConsensusAddressOwner { .. }
580 | Owner::Party { .. } => {
581 return Err(UserInputError::NotOwnedObjectError);
584 }
585 };
586 }
587 InputObjectKind::SharedMoveObject {
588 id: object_id,
589 initial_shared_version: input_initial_shared_version,
590 mutability,
591 } => {
592 fp_ensure!(
593 object.version() < SequenceNumber::MAX,
594 UserInputError::InvalidSequenceNumber
595 );
596
597 if object_id.is_system_object() {
598 if system_transaction {
601 return Ok(());
602 }
603
604 match (object_id, mutability) {
605 (SUI_SYSTEM_STATE_OBJECT_ID, _)
607 | (SUI_ADDRESS_ALIAS_STATE_OBJECT_ID, _)
608 | (SUI_COIN_REGISTRY_OBJECT_ID, _)
609 | (SUI_DISPLAY_REGISTRY_OBJECT_ID, _)
610 | (SUI_DENY_LIST_OBJECT_ID, _)
611 | (SUI_BRIDGE_OBJECT_ID, _)
612
613 | (SUI_CLOCK_OBJECT_ID, SharedObjectMutability::Immutable)
615 | (SUI_RANDOMNESS_STATE_OBJECT_ID, SharedObjectMutability::Immutable)
616 | (SUI_ACCUMULATOR_ROOT_OBJECT_ID, SharedObjectMutability::Immutable) => (),
617
618 _ => {
620 return Err(UserInputError::ImmutableParameterExpectedError {
621 object_id,
622 });
623 }
624 }
625 }
626
627 match &object.owner {
628 Owner::AddressOwner(_) | Owner::ObjectOwner(_) | Owner::Immutable => {
629 return Err(UserInputError::NotSharedObjectError);
631 }
632 Owner::Shared {
633 initial_shared_version: actual_initial_shared_version,
634 } => {
635 fp_ensure!(
636 input_initial_shared_version == *actual_initial_shared_version,
637 UserInputError::SharedObjectStartingVersionMismatch
638 )
639 }
640 Owner::ConsensusAddressOwner {
641 start_version: actual_initial_shared_version,
642 owner: actual_owner,
643 } => {
644 fp_ensure!(
645 input_initial_shared_version == *actual_initial_shared_version,
646 UserInputError::SharedObjectStartingVersionMismatch
647 );
648 fp_ensure!(
650 owner == actual_owner,
651 UserInputError::IncorrectUserSignature {
652 error: format!(
653 "Object {object_id:?} is owned by account address {actual_owner:?}, but given owner/signer address is {owner:?}"
654 ),
655 }
656 )
657 }
658
659 Owner::Party {
660 start_version: actual_initial_shared_version,
661 permissions,
662 } => {
663 fp_ensure!(
664 input_initial_shared_version == *actual_initial_shared_version,
665 UserInputError::SharedObjectStartingVersionMismatch
666 );
667 let sender_permissions = permissions.permissions_for(owner);
669 match mutability {
670 SharedObjectMutability::Immutable => {
671 fp_ensure!(
673 sender_permissions.can_use_immutably(),
674 UserInputError::IncorrectUserSignature {
675 error: format!(
676 "Sender address {owner:?} does not have immutable access permissions for object {object_id:?} with party ownership. The required permission is {}, but the permissions for the sender for this object are {sender_permissions}",
677 ObjectPermission::ImmutableUsage,
678 ),
679 }
680 )
681 }
682 SharedObjectMutability::Mutable => {
683 fp_ensure!(
685 sender_permissions.can_use_mutably(),
686 UserInputError::IncorrectUserSignature {
687 error: format!(
688 "Sender address {owner:?} does not have mutable access permissions for object {object_id:?} with party ownership. The required permission is {}, but the permissions for the sender for this object are {sender_permissions}",
689 ObjectPermission::MutableUsage,
690 ),
691 }
692 )
693 }
694 SharedObjectMutability::NonExclusiveWrite => {
695 todo!("Party WIP")
697 }
698 }
699 }
700 }
701 }
702 };
703 Ok(())
704 }
705
706 pub fn check_gasless_object_inputs(
709 input_objects: &InputObjects,
710 protocol_config: &ProtocolConfig,
711 ) -> UserInputResult<()> {
712 let allowed_token_types =
713 sui_types::transaction::get_gasless_allowed_token_types(protocol_config);
714
715 for obj_read in input_objects.iter() {
716 let Some(object) = obj_read.as_object() else {
717 continue;
718 };
719 if object.is_package() {
720 continue;
721 }
722 match object.owner() {
723 Owner::AddressOwner(_) | Owner::ConsensusAddressOwner { .. } => (),
724 Owner::Immutable
725 | Owner::Shared { .. }
726 | Owner::ObjectOwner(_)
727 | Owner::Party { .. } => {
728 return Err(UserInputError::Unsupported(
729 "Gasless transactions only support owned object inputs".to_string(),
730 ));
731 }
732 }
733 let coin_type = object.coin_type_maybe().ok_or_else(|| {
735 UserInputError::Unsupported(
736 "Gasless transactions can only use Coin<T> object inputs, \
737 but found a non-Coin object"
738 .to_string(),
739 )
740 })?;
741 fp_ensure!(
742 allowed_token_types.contains_key(&coin_type),
743 UserInputError::Unsupported(
744 "Gasless transactions only support allowlisted types for Coin inputs"
745 .to_string()
746 )
747 );
748 }
749 Ok(())
750 }
751
752 #[instrument(level = "trace", skip_all)]
754 pub fn check_non_system_packages_to_be_published(
755 transaction: &TransactionData,
756 protocol_config: &ProtocolConfig,
757 metrics: &Arc<BytecodeVerifierMetrics>,
758 verifier_signing_config: &VerifierSigningConfig,
759 ) -> UserInputResult<()> {
760 if transaction.is_system_tx() {
762 return Ok(());
763 }
764
765 let TransactionKind::ProgrammableTransaction(pt) = transaction.kind() else {
766 return Ok(());
767 };
768
769 let signing_limits = Some(verifier_signing_config.limits_for_signing());
771 let mut verifier = sui_execution::verifier(protocol_config, signing_limits, metrics);
772 let mut meter = verifier.meter(verifier_signing_config.meter_config_for_signing());
773
774 let shared_meter_verifier_timer = metrics
776 .verifier_runtime_per_ptb_success_latency
777 .start_timer();
778
779 let verifier_status = pt
780 .non_system_packages_to_be_published()
781 .try_for_each(|module_bytes| {
782 verifier.meter_module_bytes(protocol_config, module_bytes, meter.as_mut())
783 })
784 .map_err(|e| UserInputError::PackageVerificationTimeout { err: e.to_string() });
785
786 match verifier_status {
787 Ok(_) => {
788 shared_meter_verifier_timer.stop_and_record();
790 }
791 Err(err) => {
792 metrics
795 .verifier_runtime_per_ptb_timeout_latency
796 .observe(shared_meter_verifier_timer.stop_and_discard());
797 return Err(err);
798 }
799 };
800
801 Ok(())
802 }
803}