1use std::{
5 collections::{BTreeSet, HashMap, HashSet},
6 sync::Arc,
7};
8
9use consensus_core::{TransactionVerifier, ValidationError};
10use consensus_types::block::{BlockRef, TransactionIndex};
11use fastcrypto_tbls::dkg_v1;
12use itertools::Itertools;
13use mysten_common::ZipDebugEqIteratorExt;
14use mysten_common::assert_reachable;
15use mysten_metrics::monitored_scope;
16use nonempty::NonEmpty;
17use prometheus::{
18 IntCounter, IntCounterVec, Registry, register_int_counter_vec_with_registry,
19 register_int_counter_with_registry,
20};
21use sui_macros::fail_point_arg;
22#[cfg(msim)]
23use sui_types::base_types::AuthorityName;
24use sui_types::{
25 base_types::{ObjectID, ObjectRef},
26 error::{SuiError, SuiErrorKind, SuiResult, UserInputError},
27 messages_consensus::{ConsensusPosition, ConsensusTransaction, ConsensusTransactionKind},
28 transaction::{
29 CertifiedTransaction, InputObjectKind, PlainTransactionWithClaims, TransactionDataAPI,
30 },
31};
32use tap::TapFallible;
33use tracing::{debug, info, instrument, warn};
34
35use crate::{
36 authority::{AuthorityState, authority_per_epoch_store::AuthorityPerEpochStore},
37 checkpoints::CheckpointServiceNotify,
38 consensus_adapter::{ConsensusOverloadChecker, NoopConsensusOverloadChecker},
39};
40
41#[derive(Clone)]
44pub struct SuiTxValidator {
45 authority_state: Arc<AuthorityState>,
46 epoch_store: Arc<AuthorityPerEpochStore>,
47 consensus_overload_checker: Arc<dyn ConsensusOverloadChecker>,
48 checkpoint_service: Arc<dyn CheckpointServiceNotify + Send + Sync>,
49 metrics: Arc<SuiTxValidatorMetrics>,
50}
51
52impl SuiTxValidator {
53 pub fn new(
54 authority_state: Arc<AuthorityState>,
55 epoch_store: Arc<AuthorityPerEpochStore>,
56 checkpoint_service: Arc<dyn CheckpointServiceNotify + Send + Sync>,
57 metrics: Arc<SuiTxValidatorMetrics>,
58 ) -> Self {
59 info!(
60 "SuiTxValidator constructed for epoch {}",
61 epoch_store.epoch()
62 );
63 let consensus_overload_checker = Arc::new(NoopConsensusOverloadChecker {});
65 Self {
66 authority_state,
67 epoch_store,
68 consensus_overload_checker,
69 checkpoint_service,
70 metrics,
71 }
72 }
73
74 fn validate_transactions(&self, txs: &[ConsensusTransactionKind]) -> Result<(), SuiError> {
75 let epoch_store = &self.epoch_store;
76 let cert_batch: Vec<&CertifiedTransaction> = Vec::new();
78 let mut ckpt_messages = Vec::new();
79 let mut ckpt_batch = Vec::new();
80 for tx in txs.iter() {
81 match tx {
82 ConsensusTransactionKind::CertifiedTransaction(_) => {
83 return Err(SuiErrorKind::UnexpectedMessage(
84 "CertifiedTransaction cannot be used when preconsensus locking is disabled"
85 .to_string(),
86 )
87 .into());
88 }
89 ConsensusTransactionKind::CheckpointSignature(_) => {
90 return Err(SuiErrorKind::UnexpectedMessage(
91 "CheckpointSignature V1 is no longer supported".to_string(),
92 )
93 .into());
94 }
95 ConsensusTransactionKind::CheckpointSignatureV2(signature) => {
96 ckpt_messages.push(signature.as_ref());
97 ckpt_batch.push(&signature.summary);
98 }
99 ConsensusTransactionKind::RandomnessDkgMessage(_, bytes) => {
100 if bytes.len() > dkg_v1::DKG_MESSAGES_MAX_SIZE {
101 warn!("batch verification error: DKG Message too large");
102 return Err(SuiErrorKind::InvalidDkgMessageSize.into());
103 }
104 }
105 ConsensusTransactionKind::RandomnessDkgConfirmation(_, bytes) => {
106 if bytes.len() > dkg_v1::DKG_MESSAGES_MAX_SIZE {
107 warn!("batch verification error: DKG Confirmation too large");
108 return Err(SuiErrorKind::InvalidDkgMessageSize.into());
109 }
110 }
111
112 ConsensusTransactionKind::CapabilityNotification(_) => {
113 return Err(SuiErrorKind::UnexpectedMessage(
114 "CapabilityNotification V1 is no longer supported".to_string(),
115 )
116 .into());
117 }
118
119 ConsensusTransactionKind::RandomnessStateUpdate(_, _) => {
120 return Err(SuiErrorKind::UnexpectedMessage(
121 "RandomnessStateUpdate is no longer supported".to_string(),
122 )
123 .into());
124 }
125
126 ConsensusTransactionKind::EndOfPublish(_)
127 | ConsensusTransactionKind::NewJWKFetched(_, _, _)
128 | ConsensusTransactionKind::CapabilityNotificationV2(_) => {}
129
130 ConsensusTransactionKind::UserTransaction(_) => {
131 return Err(SuiErrorKind::UnexpectedMessage(
132 "ConsensusTransactionKind::UserTransaction cannot be used when address aliases is enabled or preconsensus locking is disabled".to_string(),
133 )
134 .into());
135 }
136
137 ConsensusTransactionKind::UserTransactionV2(tx) => {
138 if epoch_store.protocol_config().address_aliases() {
139 let has_aliases = if epoch_store
140 .protocol_config()
141 .fix_checkpoint_signature_mapping()
142 {
143 tx.aliases().is_some()
144 } else {
145 tx.aliases_v1().is_some()
146 };
147 if !has_aliases {
148 return Err(SuiErrorKind::UnexpectedMessage(
149 "ConsensusTransactionKind::UserTransactionV2 must contain an aliases claim".to_string(),
150 )
151 .into());
152 }
153 }
154
155 if let Some(aliases) = tx.aliases() {
156 let num_sigs = tx.tx().tx_signatures().len();
157 for (sig_idx, _) in aliases.iter() {
158 if (*sig_idx as usize) >= num_sigs {
159 return Err(SuiErrorKind::UnexpectedMessage(format!(
160 "UserTransactionV2 alias contains out-of-bounds signature index {sig_idx} (transaction has {num_sigs} signatures)",
161 )).into());
162 }
163 }
164 }
165
166 }
168
169 ConsensusTransactionKind::ExecutionTimeObservation(obs) => {
170 if obs.estimates.len()
172 > epoch_store
173 .protocol_config()
174 .max_programmable_tx_commands()
175 .try_into()
176 .unwrap()
177 {
178 return Err(SuiErrorKind::UnexpectedMessage(format!(
179 "ExecutionTimeObservation contains too many estimates: {}",
180 obs.estimates.len()
181 ))
182 .into());
183 }
184 }
185 }
186 }
187
188 let cert_count = cert_batch.len();
190 let ckpt_count = ckpt_batch.len();
191
192 epoch_store
193 .signature_verifier
194 .verify_certs_and_checkpoints(cert_batch, ckpt_batch)
195 .tap_err(|e| warn!("batch verification error: {}", e))?;
196
197 for ckpt in ckpt_messages {
199 self.checkpoint_service.notify_checkpoint_signature(ckpt)?;
200 }
201
202 self.metrics
203 .certificate_signatures_verified
204 .inc_by(cert_count as u64);
205 self.metrics
206 .checkpoint_signatures_verified
207 .inc_by(ckpt_count as u64);
208 Ok(())
209 }
210
211 #[instrument(level = "debug", skip_all, fields(block_ref))]
212 fn vote_transactions(
213 &self,
214 block_ref: &BlockRef,
215 txs: Vec<ConsensusTransactionKind>,
216 ) -> Vec<TransactionIndex> {
217 let epoch_store = &self.epoch_store;
218 let mut reject_txn_votes = Vec::new();
219 for (i, tx) in txs.into_iter().enumerate() {
220 let tx: PlainTransactionWithClaims = match tx {
221 ConsensusTransactionKind::UserTransactionV2(tx) => *tx,
222 _ => continue,
223 };
224
225 let tx_digest = *tx.tx().digest();
226 if let Err(error) = self.vote_transaction(epoch_store, tx) {
227 debug!(?tx_digest, "Voting to reject transaction: {error}");
228 self.metrics
229 .transaction_reject_votes
230 .with_label_values(&[error.to_variant_name()])
231 .inc();
232 reject_txn_votes.push(i as TransactionIndex);
233 epoch_store.set_rejection_vote_reason(
235 ConsensusPosition {
236 epoch: epoch_store.epoch(),
237 block: *block_ref,
238 index: i as TransactionIndex,
239 },
240 &error,
241 );
242 } else {
243 debug!(?tx_digest, "Voting to accept transaction");
244 }
245 }
246
247 reject_txn_votes
248 }
249
250 #[instrument(level = "debug", skip_all, err(level = "debug"), fields(tx_digest = ?tx.tx().digest()))]
251 fn vote_transaction(
252 &self,
253 epoch_store: &Arc<AuthorityPerEpochStore>,
254 tx: PlainTransactionWithClaims,
255 ) -> SuiResult<()> {
256 let aliases_v2 = tx.aliases();
258 let aliases_v1 = tx.aliases_v1();
259 let claimed_immutable_ids = tx.get_immutable_objects();
260 let inner_tx = tx.into_tx();
261
262 inner_tx.validity_check(&epoch_store.tx_validity_check_context())?;
265
266 self.authority_state.check_system_overload(
267 &*self.consensus_overload_checker,
268 inner_tx.data(),
269 self.authority_state.check_system_overload_at_signing(),
270 )?;
271
272 #[allow(unused_mut)]
273 let mut fail_point_always_report_aliases_changed = false;
274 fail_point_arg!(
275 "consensus-validator-always-report-aliases-changed",
276 |for_validators: Vec<AuthorityName>| {
277 if for_validators.contains(&self.authority_state.name) {
278 fail_point_always_report_aliases_changed = true;
280 }
281 }
282 );
283
284 let verified_tx = epoch_store.verify_transaction_with_current_aliases(inner_tx)?;
285
286 if epoch_store.protocol_config().address_aliases() {
288 let aliases_match = if epoch_store
289 .protocol_config()
290 .fix_checkpoint_signature_mapping()
291 {
292 let Some(claimed_v2) = aliases_v2 else {
294 return Err(
295 SuiErrorKind::InvalidRequest("missing address alias claim".into()).into(),
296 );
297 };
298 *verified_tx.aliases() == claimed_v2
299 } else {
300 let Some(claimed_v1) = aliases_v1 else {
302 return Err(
303 SuiErrorKind::InvalidRequest("missing address alias claim".into()).into(),
304 );
305 };
306 let computed_v1: Vec<_> = verified_tx
307 .tx()
308 .data()
309 .intent_message()
310 .value
311 .required_signers()
312 .into_iter()
313 .zip_eq(verified_tx.aliases().iter().map(|(_, seq)| *seq))
314 .collect();
315 let computed_v1 =
316 NonEmpty::from_vec(computed_v1).expect("must have at least one signer");
317 computed_v1 == claimed_v1
318 };
319
320 if !aliases_match || fail_point_always_report_aliases_changed {
321 return Err(SuiErrorKind::AliasesChanged.into());
322 }
323 }
324
325 let inner_tx = verified_tx.into_tx();
326 self.authority_state
327 .handle_vote_transaction(epoch_store, inner_tx.clone())?;
328
329 if !claimed_immutable_ids.is_empty() {
330 assert_reachable!("transaction has immutable input object claims");
331 let owned_object_refs: HashSet<ObjectRef> = inner_tx
332 .data()
333 .transaction_data()
334 .input_objects()?
335 .iter()
336 .filter_map(|obj| match obj {
337 InputObjectKind::ImmOrOwnedMoveObject(obj_ref) => Some(*obj_ref),
338 _ => None,
339 })
340 .collect();
341 self.verify_immutable_object_claims(&claimed_immutable_ids, owned_object_refs)?;
342 }
343
344 Ok(())
345 }
346
347 fn verify_immutable_object_claims(
351 &self,
352 claimed_ids: &[ObjectID],
353 owned_object_refs: HashSet<ObjectRef>,
354 ) -> SuiResult<()> {
355 let input_refs_by_id: HashMap<ObjectID, ObjectRef> = owned_object_refs
357 .iter()
358 .map(|obj_ref| (obj_ref.0, *obj_ref))
359 .collect();
360
361 for claimed_id in claimed_ids {
363 if !input_refs_by_id.contains_key(claimed_id) {
364 return Err(SuiErrorKind::ImmutableObjectClaimNotFoundInInput {
365 object_id: *claimed_id,
366 }
367 .into());
368 }
369 }
370
371 let input_ids: Vec<ObjectID> = input_refs_by_id.keys().copied().collect();
374 let objects = self
375 .authority_state
376 .get_object_cache_reader()
377 .get_objects(&input_ids);
378
379 let claimed_immutable_ids = claimed_ids.iter().cloned().collect::<BTreeSet<_>>();
380 let mut found_immutable_ids = BTreeSet::new();
381
382 for (obj_opt, object_id) in objects.into_iter().zip_debug_eq(input_ids.iter()) {
383 let input_ref = input_refs_by_id.get(object_id).unwrap();
384 match obj_opt {
385 Some(o) => {
386 let actual_ref = o.compute_object_reference();
389 if actual_ref != *input_ref {
390 return Err(SuiErrorKind::UserInputError {
391 error: UserInputError::ObjectVersionUnavailableForConsumption {
392 provided_obj_ref: *input_ref,
393 current_version: actual_ref.1,
394 },
395 }
396 .into());
397 }
398 if o.is_immutable() {
399 found_immutable_ids.insert(*object_id);
400 }
401 }
402 None => {
403 return Err(SuiErrorKind::UserInputError {
406 error: UserInputError::ObjectNotFound {
407 object_id: *object_id,
408 version: Some(input_ref.1),
409 },
410 }
411 .into());
412 }
413 }
414 }
415
416 if let Some(claimed_id) = claimed_immutable_ids
418 .difference(&found_immutable_ids)
419 .next()
420 {
421 let input_ref = input_refs_by_id.get(claimed_id).unwrap();
422 return Err(SuiErrorKind::InvalidImmutableObjectClaim {
423 claimed_object_id: *claimed_id,
424 found_object_ref: *input_ref,
425 }
426 .into());
427 }
428 if let Some(found_id) = found_immutable_ids
429 .difference(&claimed_immutable_ids)
430 .next()
431 {
432 return Err(SuiErrorKind::ImmutableObjectNotClaimed {
433 object_id: *found_id,
434 }
435 .into());
436 }
437
438 Ok(())
439 }
440}
441
442fn tx_kind_from_bytes(tx: &[u8]) -> Result<ConsensusTransactionKind, ValidationError> {
443 bcs::from_bytes::<ConsensusTransaction>(tx)
444 .map_err(|e| {
445 ValidationError::InvalidTransaction(format!(
446 "Failed to parse transaction bytes: {:?}",
447 e
448 ))
449 })
450 .map(|tx| tx.kind)
451}
452
453impl TransactionVerifier for SuiTxValidator {
454 fn verify_batch(&self, batch: &[&[u8]]) -> Result<(), ValidationError> {
455 let _scope = monitored_scope("ValidateBatch");
456
457 let txs: Vec<_> = batch
458 .iter()
459 .map(|tx| tx_kind_from_bytes(tx))
460 .collect::<Result<Vec<_>, _>>()?;
461
462 self.validate_transactions(&txs)
463 .map_err(|e| ValidationError::InvalidTransaction(e.to_string()))
464 }
465
466 fn verify_and_vote_batch(
467 &self,
468 block_ref: &BlockRef,
469 batch: &[&[u8]],
470 ) -> Result<Vec<TransactionIndex>, ValidationError> {
471 let _scope = monitored_scope("VerifyAndVoteBatch");
472
473 let txs: Vec<_> = batch
474 .iter()
475 .map(|tx| tx_kind_from_bytes(tx))
476 .collect::<Result<Vec<_>, _>>()?;
477
478 self.validate_transactions(&txs)
479 .map_err(|e| ValidationError::InvalidTransaction(e.to_string()))?;
480
481 Ok(self.vote_transactions(block_ref, txs))
482 }
483}
484
485pub struct SuiTxValidatorMetrics {
486 certificate_signatures_verified: IntCounter,
487 checkpoint_signatures_verified: IntCounter,
488 transaction_reject_votes: IntCounterVec,
489}
490
491impl SuiTxValidatorMetrics {
492 pub fn new(registry: &Registry) -> Arc<Self> {
493 Arc::new(Self {
494 certificate_signatures_verified: register_int_counter_with_registry!(
495 "tx_validator_certificate_signatures_verified",
496 "Number of certificates verified in consensus batch verifier",
497 registry
498 )
499 .unwrap(),
500 checkpoint_signatures_verified: register_int_counter_with_registry!(
501 "tx_validator_checkpoint_signatures_verified",
502 "Number of checkpoint verified in consensus batch verifier",
503 registry
504 )
505 .unwrap(),
506 transaction_reject_votes: register_int_counter_vec_with_registry!(
507 "tx_validator_transaction_reject_votes",
508 "Number of reject transaction votes per reason",
509 &["reason"],
510 registry
511 )
512 .unwrap(),
513 })
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 use std::collections::HashSet;
520 use std::num::NonZeroUsize;
521 use std::sync::Arc;
522
523 use consensus_core::TransactionVerifier as _;
524 use consensus_types::block::BlockRef;
525 use fastcrypto::traits::KeyPair;
526 use sui_config::transaction_deny_config::TransactionDenyConfigBuilder;
527 use sui_macros::sim_test;
528 use sui_protocol_config::ProtocolConfig;
529 use sui_types::crypto::deterministic_random_account_key;
530 use sui_types::error::{SuiErrorKind, UserInputError};
531 use sui_types::executable_transaction::VerifiedExecutableTransaction;
532 use sui_types::messages_checkpoint::{
533 CheckpointContents, CheckpointSignatureMessage, CheckpointSummary, SignedCheckpointSummary,
534 };
535 use sui_types::messages_consensus::ConsensusPosition;
536 use sui_types::{
537 base_types::{ExecutionDigests, ObjectID, ObjectRef},
538 crypto::Ed25519SuiSignature,
539 effects::TransactionEffectsAPI as _,
540 messages_consensus::ConsensusTransaction,
541 object::Object,
542 signature::GenericSignature,
543 transaction::{PlainTransactionWithClaims, Transaction},
544 };
545
546 use crate::authority::ExecutionEnv;
547 use crate::{
548 authority::test_authority_builder::TestAuthorityBuilder,
549 checkpoints::CheckpointServiceNoop,
550 consensus_adapter::consensus_tests::{
551 test_gas_objects, test_user_transaction, test_user_transactions,
552 },
553 consensus_validator::{SuiTxValidator, SuiTxValidatorMetrics},
554 };
555
556 #[sim_test]
557 async fn accept_valid_transaction() {
558 let mut objects = test_gas_objects();
560 let shared_object = Object::shared_for_testing();
561 objects.push(shared_object.clone());
562
563 let network_config =
564 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
565 .with_objects(objects.clone())
566 .build();
567
568 let state = TestAuthorityBuilder::new()
569 .with_network_config(&network_config, 0)
570 .build()
571 .await;
572 let name1 = state.name;
573 let transactions = test_user_transactions(&state, shared_object).await;
574
575 let first_transaction = transactions[0].clone();
576 let first_transaction_bytes: Vec<u8> =
577 bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
578 &name1,
579 first_transaction.into(),
580 ))
581 .unwrap();
582
583 let metrics = SuiTxValidatorMetrics::new(&Default::default());
584 let validator = SuiTxValidator::new(
585 state.clone(),
586 state.epoch_store_for_testing().clone(),
587 Arc::new(CheckpointServiceNoop {}),
588 metrics,
589 );
590 let res = validator.verify_batch(&[&first_transaction_bytes]);
591 assert!(res.is_ok(), "{res:?}");
592
593 let transaction_bytes: Vec<_> = transactions
594 .clone()
595 .into_iter()
596 .map(|tx| {
597 bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
598 &name1,
599 tx.into(),
600 ))
601 .unwrap()
602 })
603 .collect();
604
605 let batch: Vec<_> = transaction_bytes.iter().map(|t| t.as_slice()).collect();
606 let res_batch = validator.verify_batch(&batch);
607 assert!(res_batch.is_ok(), "{res_batch:?}");
608
609 let bogus_transaction_bytes: Vec<_> = transactions
610 .into_iter()
611 .map(|tx| {
612 let aliases = tx.aliases().clone();
614 let mut signed_tx: Transaction = tx.into_tx().into();
615 signed_tx.tx_signatures_mut_for_testing()[0] =
616 GenericSignature::Signature(sui_types::crypto::Signature::Ed25519SuiSignature(
617 Ed25519SuiSignature::default(),
618 ));
619 let tx_with_claims = PlainTransactionWithClaims::from_aliases(signed_tx, aliases);
620 bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
621 &name1,
622 tx_with_claims,
623 ))
624 .unwrap()
625 })
626 .collect();
627
628 let batch: Vec<_> = bogus_transaction_bytes
629 .iter()
630 .map(|t| t.as_slice())
631 .collect();
632 let res_batch = validator.verify_and_vote_batch(&BlockRef::MIN, &batch);
635 assert!(res_batch.is_ok());
636 let rejections = res_batch.unwrap();
638 assert_eq!(
639 rejections.len(),
640 batch.len(),
641 "All bogus transactions should be rejected"
642 );
643 }
644
645 #[tokio::test]
646 async fn test_verify_and_vote_batch() {
647 let (sender, keypair) = deterministic_random_account_key();
649
650 let gas_objects: Vec<Object> = (0..8)
652 .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
653 .collect();
654
655 let owned_objects: Vec<Object> = (0..2)
657 .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
658 .collect();
659 let denied_object = owned_objects[1].clone();
660
661 let mut objects = gas_objects.clone();
662 objects.extend(owned_objects.clone());
663
664 let network_config =
665 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
666 .committee_size(NonZeroUsize::new(1).unwrap())
667 .with_objects(objects.clone())
668 .build();
669
670 let transaction_deny_config = TransactionDenyConfigBuilder::new()
672 .add_denied_object(denied_object.id())
673 .build();
674 let state = TestAuthorityBuilder::new()
675 .with_network_config(&network_config, 0)
676 .with_transaction_deny_config(transaction_deny_config)
677 .build()
678 .await;
679
680 let valid_transaction = test_user_transaction(
684 &state,
685 sender,
686 &keypair,
687 gas_objects[0].clone(),
688 vec![owned_objects[0].clone()],
689 )
690 .await;
691
692 let invalid_transaction = test_user_transaction(
694 &state,
695 sender,
696 &keypair,
697 gas_objects[1].clone(),
698 vec![denied_object.clone()],
699 )
700 .await;
701
702 let transactions = vec![valid_transaction, invalid_transaction];
704 let serialized_transactions: Vec<_> = transactions
705 .into_iter()
706 .map(|t| {
707 bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
708 &state.name,
709 t.into(),
710 ))
711 .unwrap()
712 })
713 .collect();
714 let batch: Vec<_> = serialized_transactions
715 .iter()
716 .map(|t| t.as_slice())
717 .collect();
718
719 let validator = SuiTxValidator::new(
720 state.clone(),
721 state.epoch_store_for_testing().clone(),
722 Arc::new(CheckpointServiceNoop {}),
723 SuiTxValidatorMetrics::new(&Default::default()),
724 );
725
726 let rejected_transactions = validator
728 .verify_and_vote_batch(&BlockRef::MAX, &batch)
729 .unwrap();
730
731 assert_eq!(rejected_transactions, vec![1]);
734
735 let epoch_store = state.load_epoch_store_one_call_per_task();
738 let reason = epoch_store
739 .get_rejection_vote_reason(ConsensusPosition {
740 epoch: state.load_epoch_store_one_call_per_task().epoch(),
741 block: BlockRef::MAX,
742 index: 1,
743 })
744 .expect("Rejection vote reason should be set");
745
746 assert_eq!(
747 reason,
748 SuiErrorKind::UserInputError {
749 error: UserInputError::TransactionDenied {
750 error: format!(
751 "Access to input object {:?} is temporarily disabled",
752 denied_object.id()
753 )
754 }
755 }
756 );
757 }
758
759 #[sim_test]
760 async fn accept_checkpoint_signature_v2() {
761 let network_config =
762 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir().build();
763
764 let state = TestAuthorityBuilder::new()
765 .with_network_config(&network_config, 0)
766 .build()
767 .await;
768
769 let epoch_store = state.load_epoch_store_one_call_per_task();
770
771 let checkpoint_summary = CheckpointSummary::new(
773 &ProtocolConfig::get_for_max_version_UNSAFE(),
774 epoch_store.epoch(),
775 0,
776 0,
777 &CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]),
778 None,
779 Default::default(),
780 None,
781 0,
782 Vec::new(),
783 Vec::new(),
784 );
785
786 let keypair = network_config.validator_configs()[0].protocol_key_pair();
787 let authority = keypair.public().into();
788 let signed = SignedCheckpointSummary::new(
789 epoch_store.epoch(),
790 checkpoint_summary,
791 keypair,
792 authority,
793 );
794 let message = CheckpointSignatureMessage { summary: signed };
795
796 let tx = ConsensusTransaction::new_checkpoint_signature_message_v2(message);
797 let bytes = bcs::to_bytes(&tx).unwrap();
798
799 let validator = SuiTxValidator::new(
800 state.clone(),
801 state.epoch_store_for_testing().clone(),
802 Arc::new(CheckpointServiceNoop {}),
803 SuiTxValidatorMetrics::new(&Default::default()),
804 );
805
806 let res = validator.verify_batch(&[&bytes]);
807 assert!(res.is_ok(), "{res:?}");
808 }
809
810 #[sim_test]
811 async fn test_verify_immutable_object_claims() {
812 let (sender, _keypair) = deterministic_random_account_key();
813
814 let owned_object1 = Object::with_id_owner_for_testing(ObjectID::random(), sender);
816 let owned_object2 = Object::with_id_owner_for_testing(ObjectID::random(), sender);
817
818 let immutable_object1 = Object::immutable_with_id_for_testing(ObjectID::random());
820 let immutable_object2 = Object::immutable_with_id_for_testing(ObjectID::random());
821
822 let owned_id1 = owned_object1.id();
824 let owned_id2 = owned_object2.id();
825 let immutable_id1 = immutable_object1.id();
826 let immutable_id2 = immutable_object2.id();
827
828 let all_objects = vec![
829 owned_object1,
830 owned_object2,
831 immutable_object1,
832 immutable_object2,
833 ];
834
835 let network_config =
836 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
837 .committee_size(NonZeroUsize::new(1).unwrap())
838 .with_objects(all_objects)
839 .build();
840
841 let state = TestAuthorityBuilder::new()
842 .with_network_config(&network_config, 0)
843 .build()
844 .await;
845
846 let cache_reader = state.get_object_cache_reader();
848 let owned_ref1 = cache_reader
849 .get_object(&owned_id1)
850 .expect("owned_id1 not found")
851 .compute_object_reference();
852 let owned_ref2 = cache_reader
853 .get_object(&owned_id2)
854 .expect("owned_id2 not found")
855 .compute_object_reference();
856 let immutable_ref1 = cache_reader
857 .get_object(&immutable_id1)
858 .expect("immutable_id1 not found")
859 .compute_object_reference();
860 let immutable_ref2 = cache_reader
861 .get_object(&immutable_id2)
862 .expect("immutable_id2 not found")
863 .compute_object_reference();
864
865 let validator = SuiTxValidator::new(
866 state.clone(),
867 state.epoch_store_for_testing().clone(),
868 Arc::new(CheckpointServiceNoop {}),
869 SuiTxValidatorMetrics::new(&Default::default()),
870 );
871
872 {
874 let owned_refs: HashSet<ObjectRef> = [owned_ref1, owned_ref2].into_iter().collect();
875
876 let result = validator.verify_immutable_object_claims(&[], owned_refs);
877 assert!(
878 result.is_ok(),
879 "Empty claims with only owned objects should pass, got error: {:?}",
880 result.err()
881 );
882 }
883
884 {
886 let refs: HashSet<ObjectRef> = [owned_ref1, immutable_ref1].into_iter().collect();
887
888 let claimed_ids = vec![immutable_id1];
889 let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
890 assert!(result.is_ok(), "Correct immutable object claim should pass");
891 }
892
893 {
895 let refs: HashSet<ObjectRef> = [owned_ref1, immutable_ref1, immutable_ref2]
896 .into_iter()
897 .collect();
898
899 let claimed_ids = vec![immutable_id1, immutable_id2];
900 let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
901 assert!(
902 result.is_ok(),
903 "Multiple correct immutable claims should pass"
904 );
905 }
906
907 {
909 let refs: HashSet<ObjectRef> = [owned_ref1, immutable_ref1].into_iter().collect();
910
911 let claimed_ids: Vec<ObjectID> = vec![];
912 let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
913 assert!(result.is_err(), "Missing immutable claim should fail");
914
915 let err = result.unwrap_err();
916 assert!(
917 matches!(
918 err.as_inner(),
919 SuiErrorKind::ImmutableObjectNotClaimed { object_id }
920 if *object_id == immutable_id1
921 ),
922 "Expected ImmutableObjectNotClaimed error, got: {:?}",
923 err.as_inner()
924 );
925 }
926
927 {
929 let refs: HashSet<ObjectRef> = [owned_ref1, owned_ref2].into_iter().collect();
930
931 let claimed_ids = vec![owned_id1];
932 let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
933 assert!(
934 result.is_err(),
935 "False immutable claim on owned object should fail"
936 );
937
938 let err = result.unwrap_err();
939 assert!(
940 matches!(
941 err.as_inner(),
942 SuiErrorKind::InvalidImmutableObjectClaim { claimed_object_id, .. }
943 if *claimed_object_id == owned_id1
944 ),
945 "Expected InvalidImmutableObjectClaim error, got: {:?}",
946 err.as_inner()
947 );
948 }
949
950 {
952 let refs: HashSet<ObjectRef> = [owned_ref1, owned_ref2].into_iter().collect();
953
954 let claimed_ids = vec![immutable_id1];
955 let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
956 assert!(result.is_err(), "Claim not in inputs should fail");
957
958 let err = result.unwrap_err();
959 assert!(
960 matches!(
961 err.as_inner(),
962 SuiErrorKind::ImmutableObjectClaimNotFoundInInput { object_id }
963 if *object_id == immutable_id1
964 ),
965 "Expected ImmutableObjectClaimNotFoundInInput error, got: {:?}",
966 err.as_inner()
967 );
968 }
969
970 {
972 let non_existent_id = ObjectID::random();
973 let fake_ref = (
974 non_existent_id,
975 sui_types::base_types::SequenceNumber::new(),
976 sui_types::digests::ObjectDigest::random(),
977 );
978 let refs: HashSet<ObjectRef> = [owned_ref1, fake_ref].into_iter().collect();
979
980 let claimed_ids: Vec<ObjectID> = vec![];
981 let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
982 assert!(result.is_err(), "Non-existent object should fail");
983
984 let err = result.unwrap_err();
985 assert!(
986 matches!(
987 err.as_inner(),
988 SuiErrorKind::UserInputError { error: UserInputError::ObjectNotFound { object_id, .. } }
989 if *object_id == non_existent_id
990 ),
991 "Expected ObjectNotFound error, got: {:?}",
992 err.as_inner()
993 );
994 }
995
996 {
998 let wrong_version_ref = (
1000 immutable_ref1.0,
1001 sui_types::base_types::SequenceNumber::from_u64(999),
1002 immutable_ref1.2,
1003 );
1004
1005 let refs: HashSet<ObjectRef> = [owned_ref1, wrong_version_ref].into_iter().collect();
1006
1007 let claimed_ids = vec![immutable_id1];
1008 let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
1009 assert!(result.is_err(), "Version mismatch should fail");
1010
1011 let err = result.unwrap_err();
1012 assert!(
1013 matches!(
1014 err.as_inner(),
1015 SuiErrorKind::UserInputError { error: UserInputError::ObjectVersionUnavailableForConsumption { provided_obj_ref, current_version: _ } }
1016 if provided_obj_ref.0 == immutable_id1
1017 ),
1018 "Expected ObjectVersionUnavailableForConsumption error, got: {:?}",
1019 err.as_inner()
1020 );
1021 }
1022 }
1023
1024 #[sim_test]
1025 async fn accept_already_executed_transaction() {
1026 let (sender, keypair) = deterministic_random_account_key();
1027
1028 let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
1029 let owned_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
1030
1031 let network_config =
1032 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
1033 .committee_size(NonZeroUsize::new(1).unwrap())
1034 .with_objects(vec![gas_object.clone(), owned_object.clone()])
1035 .build();
1036
1037 let state = TestAuthorityBuilder::new()
1038 .with_network_config(&network_config, 0)
1039 .build()
1040 .await;
1041
1042 let epoch_store = state.load_epoch_store_one_call_per_task();
1043
1044 let transaction = test_user_transaction(
1046 &state,
1047 sender,
1048 &keypair,
1049 gas_object.clone(),
1050 vec![owned_object.clone()],
1051 )
1052 .await;
1053 let tx_digest = *transaction.tx().digest();
1054 let cert =
1055 VerifiedExecutableTransaction::new_from_consensus(transaction.clone().into_tx(), 0);
1056 let (executed_effects, _) = state
1057 .try_execute_immediately(&cert, ExecutionEnv::new(), &state.epoch_store_for_testing())
1058 .await
1059 .unwrap();
1060
1061 let read_effects = state
1063 .get_transaction_cache_reader()
1064 .get_executed_effects(&tx_digest)
1065 .expect("Transaction should be executed");
1066 assert_eq!(read_effects, executed_effects);
1067 assert_eq!(read_effects.executed_epoch(), epoch_store.epoch());
1068
1069 let serialized_tx = bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
1071 &state.name,
1072 transaction.into(),
1073 ))
1074 .unwrap();
1075 let validator = SuiTxValidator::new(
1076 state.clone(),
1077 state.epoch_store_for_testing().clone(),
1078 Arc::new(CheckpointServiceNoop {}),
1079 SuiTxValidatorMetrics::new(&Default::default()),
1080 );
1081 let rejected_transactions = validator
1082 .verify_and_vote_batch(&BlockRef::MAX, &[&serialized_tx])
1083 .expect("Verify and vote should succeed");
1084
1085 assert!(rejected_transactions.is_empty());
1087 }
1088
1089 #[tokio::test]
1090 async fn test_reject_invalid_alias_signature_index() {
1091 let (sender, keypair) = deterministic_random_account_key();
1092
1093 let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
1094 let owned_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
1095
1096 let network_config =
1097 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
1098 .committee_size(NonZeroUsize::new(1).unwrap())
1099 .with_objects(vec![gas_object.clone(), owned_object.clone()])
1100 .build();
1101
1102 let state = TestAuthorityBuilder::new()
1103 .with_network_config(&network_config, 0)
1104 .build()
1105 .await;
1106
1107 let transaction = test_user_transaction(
1108 &state,
1109 sender,
1110 &keypair,
1111 gas_object.clone(),
1112 vec![owned_object.clone()],
1113 )
1114 .await;
1115
1116 let inner_tx: Transaction = transaction.into_tx().into();
1119 let bogus_aliases = nonempty::nonempty![(255u8, None)];
1120 let tx_with_bogus_alias = PlainTransactionWithClaims::from_aliases(inner_tx, bogus_aliases);
1121
1122 let serialized_tx = bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
1123 &state.name,
1124 tx_with_bogus_alias,
1125 ))
1126 .unwrap();
1127
1128 let validator = SuiTxValidator::new(
1129 state.clone(),
1130 state.epoch_store_for_testing().clone(),
1131 Arc::new(CheckpointServiceNoop {}),
1132 SuiTxValidatorMetrics::new(&Default::default()),
1133 );
1134
1135 let res = validator.verify_batch(&[&serialized_tx]);
1136 assert!(
1137 res.is_err(),
1138 "Should reject transaction with out-of-bounds alias signature index"
1139 );
1140 }
1141}