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