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