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