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