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