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(
71 &self,
72 block_ref: &BlockRef,
73 txs: &[ConsensusTransactionKind],
74 ) -> Result<(), SuiError> {
75 let epoch_store = &self.epoch_store;
76 let proposer = block_ref.author.value_u32();
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 epoch_store
169 .check_allowed_proposer(tx.tx().data().transaction_data(), proposer)?;
170
171 }
173
174 ConsensusTransactionKind::ExecutionTimeObservation(obs) => {
175 if obs.estimates.len()
177 > epoch_store
178 .protocol_config()
179 .max_programmable_tx_commands()
180 .try_into()
181 .unwrap()
182 {
183 return Err(SuiErrorKind::UnexpectedMessage(format!(
184 "ExecutionTimeObservation contains too many estimates: {}",
185 obs.estimates.len()
186 ))
187 .into());
188 }
189 }
190
191 ConsensusTransactionKind::UpdateTransactionDenyConfig(msg) => {
192 if !epoch_store
193 .protocol_config()
194 .share_transaction_deny_config_in_consensus()
195 {
196 return Err(SuiErrorKind::UnexpectedMessage(
197 "UpdateTransactionDenyConfig is not supported by current protocol \
198 version"
199 .to_string(),
200 )
201 .into());
202 }
203 if let Some(rules) = msg.rules() {
204 rules.check_share_limits().map_err(|e| -> SuiError {
205 SuiErrorKind::UnexpectedMessage(format!(
206 "UpdateTransactionDenyConfig: {e}"
207 ))
208 .into()
209 })?;
210 }
211 }
212 }
213 }
214
215 let ckpt_count = ckpt_batch.len();
216
217 crate::signature_verifier::batch_verify_checkpoints(epoch_store.committee(), &ckpt_batch)
218 .tap_err(|e| warn!("batch verification error: {}", e))?;
219
220 for ckpt in ckpt_messages {
222 self.checkpoint_service.notify_checkpoint_signature(ckpt)?;
223 }
224
225 self.metrics
226 .checkpoint_signatures_verified
227 .inc_by(ckpt_count as u64);
228 Ok(())
229 }
230
231 fn apply_deny_config_updates(
236 &self,
237 block_ref: &BlockRef,
238 updates: Vec<SharedTransactionDenyConfig>,
239 ) {
240 if updates.is_empty() {
241 return;
242 }
243 let committee = self.epoch_store.committee();
244 let Some(author) = committee.authority_by_index(block_ref.author.value_u32()) else {
245 warn!(
246 "Dropping UpdateTransactionDenyConfig batch: block author index {} not in committee",
247 block_ref.author
248 );
249 return;
250 };
251 self.authority_state
252 .transaction_deny_config_manager()
253 .apply_updates(*author, updates);
254 }
255
256 #[instrument(level = "debug", skip_all, fields(block_ref))]
257 fn vote_transactions(
258 &self,
259 block_ref: &BlockRef,
260 txs: Vec<ConsensusTransactionKind>,
261 ) -> Vec<TransactionIndex> {
262 let epoch_store = &self.epoch_store;
263 let mut reject_txn_votes = Vec::new();
264 for (i, tx) in txs.into_iter().enumerate() {
265 let tx: PlainTransactionWithClaims = match tx {
266 ConsensusTransactionKind::UserTransactionV2(tx) => *tx,
267 _ => continue,
268 };
269
270 let tx_digest = *tx.tx().digest();
271 if let Err(error) = self.vote_transaction(epoch_store, tx) {
272 debug!(?tx_digest, "Voting to reject transaction: {error}");
273 self.metrics
274 .transaction_reject_votes
275 .with_label_values(&[error.to_variant_name()])
276 .inc();
277 reject_txn_votes.push(i as TransactionIndex);
278 epoch_store.set_rejection_vote_reason(
280 ConsensusPosition {
281 epoch: epoch_store.epoch(),
282 block: *block_ref,
283 index: i as TransactionIndex,
284 },
285 &error,
286 );
287 } else {
288 debug!(?tx_digest, "Voting to accept transaction");
289 }
290 }
291
292 reject_txn_votes
293 }
294
295 #[instrument(level = "debug", skip_all, err(level = "debug"), fields(tx_digest = ?tx.tx().digest()))]
296 fn vote_transaction(
297 &self,
298 epoch_store: &Arc<AuthorityPerEpochStore>,
299 tx: PlainTransactionWithClaims,
300 ) -> SuiResult<()> {
301 let aliases_v2 = tx.aliases();
303 let aliases_v1 = tx.aliases_v1();
304 let claimed_immutable_ids = tx.get_immutable_objects();
305 let inner_tx = tx.into_tx();
306
307 inner_tx.validity_check(&epoch_store.tx_validity_check_context())?;
310
311 self.authority_state.check_system_overload(
312 inner_tx.data(),
313 self.authority_state.check_system_overload_at_signing(),
314 )?;
315
316 #[allow(unused_mut)]
317 let mut fail_point_always_report_aliases_changed = false;
318 fail_point_arg!(
319 "consensus-validator-always-report-aliases-changed",
320 |for_validators: Vec<AuthorityName>| {
321 if for_validators.contains(&self.authority_state.name) {
322 fail_point_always_report_aliases_changed = true;
324 }
325 }
326 );
327
328 let verified_tx = epoch_store.verify_transaction_with_current_aliases(inner_tx)?;
329
330 if epoch_store.protocol_config().address_aliases() {
332 let aliases_match = if epoch_store
333 .protocol_config()
334 .fix_checkpoint_signature_mapping()
335 {
336 let Some(claimed_v2) = aliases_v2 else {
338 return Err(
339 SuiErrorKind::InvalidRequest("missing address alias claim".into()).into(),
340 );
341 };
342 *verified_tx.aliases() == claimed_v2
343 } else {
344 let Some(claimed_v1) = aliases_v1 else {
346 return Err(
347 SuiErrorKind::InvalidRequest("missing address alias claim".into()).into(),
348 );
349 };
350 let computed_v1: Vec<_> = verified_tx
351 .tx()
352 .data()
353 .intent_message()
354 .value
355 .required_signers()
356 .into_iter()
357 .zip_eq(verified_tx.aliases().iter().map(|(_, seq)| *seq))
358 .collect();
359 let computed_v1 =
360 NonEmpty::from_vec(computed_v1).expect("must have at least one signer");
361 computed_v1 == claimed_v1
362 };
363
364 if !aliases_match || fail_point_always_report_aliases_changed {
365 return Err(SuiErrorKind::AliasesChanged.into());
366 }
367 }
368
369 let inner_tx = verified_tx.into_tx();
370 self.authority_state
371 .handle_vote_transaction(epoch_store, inner_tx.clone())?;
372
373 if !claimed_immutable_ids.is_empty() {
374 assert_reachable!("transaction has immutable input object claims");
375 let owned_object_refs: HashSet<ObjectRef> = inner_tx
376 .data()
377 .transaction_data()
378 .input_objects()?
379 .iter()
380 .filter_map(|obj| match obj {
381 InputObjectKind::ImmOrOwnedMoveObject(obj_ref) => Some(*obj_ref),
382 _ => None,
383 })
384 .collect();
385 self.verify_immutable_object_claims(&claimed_immutable_ids, owned_object_refs)?;
386 }
387
388 Ok(())
389 }
390
391 fn verify_immutable_object_claims(
395 &self,
396 claimed_ids: &[ObjectID],
397 owned_object_refs: HashSet<ObjectRef>,
398 ) -> SuiResult<()> {
399 let input_refs_by_id: HashMap<ObjectID, ObjectRef> = owned_object_refs
401 .iter()
402 .map(|obj_ref| (obj_ref.0, *obj_ref))
403 .collect();
404
405 for claimed_id in claimed_ids {
407 if !input_refs_by_id.contains_key(claimed_id) {
408 return Err(SuiErrorKind::ImmutableObjectClaimNotFoundInInput {
409 object_id: *claimed_id,
410 }
411 .into());
412 }
413 }
414
415 let input_ids: Vec<ObjectID> = input_refs_by_id.keys().copied().collect();
418 let objects = self
419 .authority_state
420 .get_object_cache_reader()
421 .get_objects(&input_ids);
422
423 let claimed_immutable_ids = claimed_ids.iter().cloned().collect::<BTreeSet<_>>();
424 let mut found_immutable_ids = BTreeSet::new();
425
426 for (obj_opt, object_id) in objects.into_iter().zip_debug_eq(input_ids.iter()) {
427 let input_ref = input_refs_by_id.get(object_id).unwrap();
428 match obj_opt {
429 Some(o) => {
430 let actual_ref = o.compute_object_reference();
433 if actual_ref != *input_ref {
434 return Err(SuiErrorKind::UserInputError {
435 error: UserInputError::ObjectVersionUnavailableForConsumption {
436 provided_obj_ref: *input_ref,
437 current_version: actual_ref.1,
438 },
439 }
440 .into());
441 }
442 if o.is_immutable() {
443 found_immutable_ids.insert(*object_id);
444 }
445 }
446 None => {
447 return Err(SuiErrorKind::UserInputError {
450 error: UserInputError::ObjectNotFound {
451 object_id: *object_id,
452 version: Some(input_ref.1),
453 },
454 }
455 .into());
456 }
457 }
458 }
459
460 if let Some(claimed_id) = claimed_immutable_ids
462 .difference(&found_immutable_ids)
463 .next()
464 {
465 let input_ref = input_refs_by_id.get(claimed_id).unwrap();
466 return Err(SuiErrorKind::InvalidImmutableObjectClaim {
467 claimed_object_id: *claimed_id,
468 found_object_ref: *input_ref,
469 }
470 .into());
471 }
472 if let Some(found_id) = found_immutable_ids
473 .difference(&claimed_immutable_ids)
474 .next()
475 {
476 return Err(SuiErrorKind::ImmutableObjectNotClaimed {
477 object_id: *found_id,
478 }
479 .into());
480 }
481
482 Ok(())
483 }
484}
485
486fn tx_kind_from_bytes(tx: &[u8]) -> Result<ConsensusTransactionKind, ValidationError> {
487 bcs::from_bytes::<ConsensusTransaction>(tx)
488 .map_err(|e| {
489 ValidationError::InvalidTransaction(format!(
490 "Failed to parse transaction bytes: {:?}",
491 e
492 ))
493 })
494 .map(|tx| tx.kind)
495}
496
497impl TransactionVerifier for SuiTxValidator {
498 fn verify_batch(&self, block_ref: &BlockRef, batch: &[&[u8]]) -> Result<(), ValidationError> {
499 let _scope = monitored_scope("ValidateBatch");
500
501 let txs: Vec<_> = batch
502 .iter()
503 .map(|tx| tx_kind_from_bytes(tx))
504 .collect::<Result<Vec<_>, _>>()?;
505
506 self.validate_transactions(block_ref, &txs)
507 .map_err(|e| ValidationError::InvalidTransaction(e.to_string()))
508 }
509
510 fn verify_and_vote_batch(
511 &self,
512 block_ref: &BlockRef,
513 batch: &[&[u8]],
514 ) -> Result<Vec<TransactionIndex>, ValidationError> {
515 let _scope = monitored_scope("VerifyAndVoteBatch");
516
517 let txs: Vec<_> = batch
518 .iter()
519 .map(|tx| tx_kind_from_bytes(tx))
520 .collect::<Result<Vec<_>, _>>()?;
521
522 self.validate_transactions(block_ref, &txs)
523 .map_err(|e| ValidationError::InvalidTransaction(e.to_string()))?;
524
525 let deny_config_updates: Vec<_> = txs
526 .iter()
527 .filter_map(|tx| match tx {
528 ConsensusTransactionKind::UpdateTransactionDenyConfig(msg) => Some((**msg).clone()),
529 _ => None,
530 })
531 .collect();
532 self.apply_deny_config_updates(block_ref, deny_config_updates);
533
534 Ok(self.vote_transactions(block_ref, txs))
535 }
536}
537
538pub struct SuiTxValidatorMetrics {
539 checkpoint_signatures_verified: IntCounter,
540 transaction_reject_votes: IntCounterVec,
541}
542
543impl SuiTxValidatorMetrics {
544 pub fn new(registry: &Registry) -> Arc<Self> {
545 Arc::new(Self {
546 checkpoint_signatures_verified: register_int_counter_with_registry!(
547 "tx_validator_checkpoint_signatures_verified",
548 "Number of checkpoint verified in consensus batch verifier",
549 registry
550 )
551 .unwrap(),
552 transaction_reject_votes: register_int_counter_vec_with_registry!(
553 "tx_validator_transaction_reject_votes",
554 "Number of reject transaction votes per reason",
555 &["reason"],
556 registry
557 )
558 .unwrap(),
559 })
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use std::collections::HashSet;
566 use std::num::NonZeroUsize;
567 use std::sync::Arc;
568
569 use consensus_config::AuthorityIndex;
570 use consensus_core::TransactionVerifier as _;
571 use consensus_types::block::{BlockDigest, BlockRef};
572 use fastcrypto::traits::KeyPair;
573 use sui_config::transaction_deny_config::TransactionDenyConfigBuilder;
574 use sui_macros::sim_test;
575 use sui_protocol_config::ProtocolConfig;
576 use sui_types::crypto::deterministic_random_account_key;
577 use sui_types::error::{SuiErrorKind, UserInputError};
578 use sui_types::executable_transaction::VerifiedExecutableTransaction;
579 use sui_types::messages_checkpoint::{
580 CheckpointContents, CheckpointSignatureMessage, CheckpointSummary, SignedCheckpointSummary,
581 };
582 use sui_types::messages_consensus::ConsensusPosition;
583 use sui_types::{
584 base_types::{ExecutionDigests, ObjectID, ObjectRef},
585 crypto::Ed25519SuiSignature,
586 effects::TransactionEffectsAPI as _,
587 messages_consensus::{
588 ConsensusTransaction, SharedTransactionDenyConfig, SharedTransactionDenyConfigV1,
589 },
590 object::Object,
591 signature::GenericSignature,
592 transaction::{
593 AllowedProposers, PlainTransactionWithClaims, Transaction, TransactionDataAPI as _,
594 TransactionExpiration,
595 },
596 };
597
598 use crate::authority::ExecutionEnv;
599 use crate::{
600 authority::test_authority_builder::TestAuthorityBuilder,
601 checkpoints::CheckpointServiceNoop,
602 consensus_adapter::consensus_tests::{
603 test_gas_objects, test_user_transaction, test_user_transactions,
604 },
605 consensus_validator::{SuiTxValidator, SuiTxValidatorMetrics},
606 };
607
608 #[sim_test]
609 async fn accept_valid_transaction() {
610 let mut objects = test_gas_objects();
612 let shared_object = Object::shared_for_testing();
613 objects.push(shared_object.clone());
614
615 let network_config =
616 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
617 .with_objects(objects.clone())
618 .build();
619
620 let state = TestAuthorityBuilder::new()
621 .with_network_config(&network_config, 0)
622 .build()
623 .await;
624 let name1 = state.name;
625 let transactions = test_user_transactions(&state, shared_object).await;
626
627 let first_transaction = transactions[0].clone();
628 let first_transaction_bytes: Vec<u8> =
629 bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
630 &name1,
631 first_transaction.into(),
632 ))
633 .unwrap();
634
635 let metrics = SuiTxValidatorMetrics::new(&Default::default());
636 let validator = SuiTxValidator::new(
637 state.clone(),
638 state.epoch_store_for_testing().clone(),
639 Arc::new(CheckpointServiceNoop {}),
640 metrics,
641 );
642 let res = validator.verify_batch(&BlockRef::MIN, &[&first_transaction_bytes]);
643 assert!(res.is_ok(), "{res:?}");
644
645 let transaction_bytes: Vec<_> = transactions
646 .clone()
647 .into_iter()
648 .map(|tx| {
649 bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
650 &name1,
651 tx.into(),
652 ))
653 .unwrap()
654 })
655 .collect();
656
657 let batch: Vec<_> = transaction_bytes.iter().map(|t| t.as_slice()).collect();
658 let res_batch = validator.verify_batch(&BlockRef::MIN, &batch);
659 assert!(res_batch.is_ok(), "{res_batch:?}");
660
661 let bogus_transaction_bytes: Vec<_> = transactions
662 .into_iter()
663 .map(|tx| {
664 let aliases = tx.aliases().clone();
666 let mut signed_tx: Transaction = tx.into_tx().into();
667 signed_tx.tx_signatures_mut_for_testing()[0] =
668 GenericSignature::Signature(sui_types::crypto::Signature::Ed25519SuiSignature(
669 Ed25519SuiSignature::default(),
670 ));
671 let tx_with_claims = PlainTransactionWithClaims::from_aliases(signed_tx, aliases);
672 bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
673 &name1,
674 tx_with_claims,
675 ))
676 .unwrap()
677 })
678 .collect();
679
680 let batch: Vec<_> = bogus_transaction_bytes
681 .iter()
682 .map(|t| t.as_slice())
683 .collect();
684 let res_batch = validator.verify_and_vote_batch(&BlockRef::MIN, &batch);
687 assert!(res_batch.is_ok());
688 let rejections = res_batch.unwrap();
690 assert_eq!(
691 rejections.len(),
692 batch.len(),
693 "All bogus transactions should be rejected"
694 );
695 }
696
697 #[tokio::test]
698 async fn test_verify_and_vote_batch() {
699 let (sender, keypair) = deterministic_random_account_key();
701
702 let gas_objects: Vec<Object> = (0..8)
704 .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
705 .collect();
706
707 let owned_objects: Vec<Object> = (0..2)
709 .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
710 .collect();
711 let denied_object = owned_objects[1].clone();
712
713 let mut objects = gas_objects.clone();
714 objects.extend(owned_objects.clone());
715
716 let network_config =
717 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
718 .committee_size(NonZeroUsize::new(1).unwrap())
719 .with_objects(objects.clone())
720 .build();
721
722 let transaction_deny_config = TransactionDenyConfigBuilder::new()
724 .add_denied_object(denied_object.id())
725 .build();
726 let state = TestAuthorityBuilder::new()
727 .with_network_config(&network_config, 0)
728 .with_transaction_deny_config(transaction_deny_config)
729 .build()
730 .await;
731
732 let valid_transaction = test_user_transaction(
736 &state,
737 sender,
738 &keypair,
739 gas_objects[0].clone(),
740 vec![owned_objects[0].clone()],
741 )
742 .await;
743
744 let invalid_transaction = test_user_transaction(
746 &state,
747 sender,
748 &keypair,
749 gas_objects[1].clone(),
750 vec![denied_object.clone()],
751 )
752 .await;
753
754 let transactions = vec![valid_transaction, invalid_transaction];
756 let serialized_transactions: Vec<_> = transactions
757 .into_iter()
758 .map(|t| {
759 bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
760 &state.name,
761 t.into(),
762 ))
763 .unwrap()
764 })
765 .collect();
766 let batch: Vec<_> = serialized_transactions
767 .iter()
768 .map(|t| t.as_slice())
769 .collect();
770
771 let validator = SuiTxValidator::new(
772 state.clone(),
773 state.epoch_store_for_testing().clone(),
774 Arc::new(CheckpointServiceNoop {}),
775 SuiTxValidatorMetrics::new(&Default::default()),
776 );
777
778 let rejected_transactions = validator
780 .verify_and_vote_batch(&BlockRef::MAX, &batch)
781 .unwrap();
782
783 assert_eq!(rejected_transactions, vec![1]);
786
787 let epoch_store = state.load_epoch_store_one_call_per_task();
790 let reason = epoch_store
791 .get_rejection_vote_reason(ConsensusPosition {
792 epoch: state.load_epoch_store_one_call_per_task().epoch(),
793 block: BlockRef::MAX,
794 index: 1,
795 })
796 .expect("Rejection vote reason should be set");
797
798 assert_eq!(
799 reason,
800 SuiErrorKind::UserInputError {
801 error: UserInputError::TransactionDenied {
802 error: format!(
803 "Access to input object {:?} is temporarily disabled",
804 denied_object.id()
805 )
806 }
807 }
808 );
809 }
810
811 #[sim_test]
812 async fn accept_checkpoint_signature_v2() {
813 let network_config =
814 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir().build();
815
816 let state = TestAuthorityBuilder::new()
817 .with_network_config(&network_config, 0)
818 .build()
819 .await;
820
821 let epoch_store = state.load_epoch_store_one_call_per_task();
822
823 let checkpoint_summary = CheckpointSummary::new(
825 &ProtocolConfig::get_for_max_version_UNSAFE(),
826 epoch_store.epoch(),
827 0,
828 0,
829 &CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]),
830 None,
831 Default::default(),
832 None,
833 0,
834 Vec::new(),
835 Vec::new(),
836 );
837
838 let keypair = network_config.validator_configs()[0].protocol_key_pair();
839 let authority = keypair.public().into();
840 let signed = SignedCheckpointSummary::new(
841 epoch_store.epoch(),
842 checkpoint_summary,
843 keypair,
844 authority,
845 );
846 let message = CheckpointSignatureMessage { summary: signed };
847
848 let tx = ConsensusTransaction::new_checkpoint_signature_message_v2(message);
849 let bytes = bcs::to_bytes(&tx).unwrap();
850
851 let validator = SuiTxValidator::new(
852 state.clone(),
853 state.epoch_store_for_testing().clone(),
854 Arc::new(CheckpointServiceNoop {}),
855 SuiTxValidatorMetrics::new(&Default::default()),
856 );
857
858 let res = validator.verify_batch(&BlockRef::MIN, &[&bytes]);
859 assert!(res.is_ok(), "{res:?}");
860 }
861
862 #[sim_test]
863 async fn test_verify_immutable_object_claims() {
864 let (sender, _keypair) = deterministic_random_account_key();
865
866 let owned_object1 = Object::with_id_owner_for_testing(ObjectID::random(), sender);
868 let owned_object2 = Object::with_id_owner_for_testing(ObjectID::random(), sender);
869
870 let immutable_object1 = Object::immutable_with_id_for_testing(ObjectID::random());
872 let immutable_object2 = Object::immutable_with_id_for_testing(ObjectID::random());
873
874 let owned_id1 = owned_object1.id();
876 let owned_id2 = owned_object2.id();
877 let immutable_id1 = immutable_object1.id();
878 let immutable_id2 = immutable_object2.id();
879
880 let all_objects = vec![
881 owned_object1,
882 owned_object2,
883 immutable_object1,
884 immutable_object2,
885 ];
886
887 let network_config =
888 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
889 .committee_size(NonZeroUsize::new(1).unwrap())
890 .with_objects(all_objects)
891 .build();
892
893 let state = TestAuthorityBuilder::new()
894 .with_network_config(&network_config, 0)
895 .build()
896 .await;
897
898 let cache_reader = state.get_object_cache_reader();
900 let owned_ref1 = cache_reader
901 .get_object(&owned_id1)
902 .expect("owned_id1 not found")
903 .compute_object_reference();
904 let owned_ref2 = cache_reader
905 .get_object(&owned_id2)
906 .expect("owned_id2 not found")
907 .compute_object_reference();
908 let immutable_ref1 = cache_reader
909 .get_object(&immutable_id1)
910 .expect("immutable_id1 not found")
911 .compute_object_reference();
912 let immutable_ref2 = cache_reader
913 .get_object(&immutable_id2)
914 .expect("immutable_id2 not found")
915 .compute_object_reference();
916
917 let validator = SuiTxValidator::new(
918 state.clone(),
919 state.epoch_store_for_testing().clone(),
920 Arc::new(CheckpointServiceNoop {}),
921 SuiTxValidatorMetrics::new(&Default::default()),
922 );
923
924 {
926 let owned_refs: HashSet<ObjectRef> = [owned_ref1, owned_ref2].into_iter().collect();
927
928 let result = validator.verify_immutable_object_claims(&[], owned_refs);
929 assert!(
930 result.is_ok(),
931 "Empty claims with only owned objects should pass, got error: {:?}",
932 result.err()
933 );
934 }
935
936 {
938 let refs: HashSet<ObjectRef> = [owned_ref1, immutable_ref1].into_iter().collect();
939
940 let claimed_ids = vec![immutable_id1];
941 let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
942 assert!(result.is_ok(), "Correct immutable object claim should pass");
943 }
944
945 {
947 let refs: HashSet<ObjectRef> = [owned_ref1, immutable_ref1, immutable_ref2]
948 .into_iter()
949 .collect();
950
951 let claimed_ids = vec![immutable_id1, immutable_id2];
952 let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
953 assert!(
954 result.is_ok(),
955 "Multiple correct immutable claims should pass"
956 );
957 }
958
959 {
961 let refs: HashSet<ObjectRef> = [owned_ref1, immutable_ref1].into_iter().collect();
962
963 let claimed_ids: Vec<ObjectID> = vec![];
964 let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
965 assert!(result.is_err(), "Missing immutable claim should fail");
966
967 let err = result.unwrap_err();
968 assert!(
969 matches!(
970 err.as_inner(),
971 SuiErrorKind::ImmutableObjectNotClaimed { object_id }
972 if *object_id == immutable_id1
973 ),
974 "Expected ImmutableObjectNotClaimed error, got: {:?}",
975 err.as_inner()
976 );
977 }
978
979 {
981 let refs: HashSet<ObjectRef> = [owned_ref1, owned_ref2].into_iter().collect();
982
983 let claimed_ids = vec![owned_id1];
984 let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
985 assert!(
986 result.is_err(),
987 "False immutable claim on owned object should fail"
988 );
989
990 let err = result.unwrap_err();
991 assert!(
992 matches!(
993 err.as_inner(),
994 SuiErrorKind::InvalidImmutableObjectClaim { claimed_object_id, .. }
995 if *claimed_object_id == owned_id1
996 ),
997 "Expected InvalidImmutableObjectClaim error, got: {:?}",
998 err.as_inner()
999 );
1000 }
1001
1002 {
1004 let refs: HashSet<ObjectRef> = [owned_ref1, owned_ref2].into_iter().collect();
1005
1006 let claimed_ids = vec![immutable_id1];
1007 let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
1008 assert!(result.is_err(), "Claim not in inputs should fail");
1009
1010 let err = result.unwrap_err();
1011 assert!(
1012 matches!(
1013 err.as_inner(),
1014 SuiErrorKind::ImmutableObjectClaimNotFoundInInput { object_id }
1015 if *object_id == immutable_id1
1016 ),
1017 "Expected ImmutableObjectClaimNotFoundInInput error, got: {:?}",
1018 err.as_inner()
1019 );
1020 }
1021
1022 {
1024 let non_existent_id = ObjectID::random();
1025 let fake_ref = (
1026 non_existent_id,
1027 sui_types::base_types::SequenceNumber::new(),
1028 sui_types::digests::ObjectDigest::random(),
1029 );
1030 let refs: HashSet<ObjectRef> = [owned_ref1, fake_ref].into_iter().collect();
1031
1032 let claimed_ids: Vec<ObjectID> = vec![];
1033 let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
1034 assert!(result.is_err(), "Non-existent object should fail");
1035
1036 let err = result.unwrap_err();
1037 assert!(
1038 matches!(
1039 err.as_inner(),
1040 SuiErrorKind::UserInputError { error: UserInputError::ObjectNotFound { object_id, .. } }
1041 if *object_id == non_existent_id
1042 ),
1043 "Expected ObjectNotFound error, got: {:?}",
1044 err.as_inner()
1045 );
1046 }
1047
1048 {
1050 let wrong_version_ref = (
1052 immutable_ref1.0,
1053 sui_types::base_types::SequenceNumber::from_u64(999),
1054 immutable_ref1.2,
1055 );
1056
1057 let refs: HashSet<ObjectRef> = [owned_ref1, wrong_version_ref].into_iter().collect();
1058
1059 let claimed_ids = vec![immutable_id1];
1060 let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
1061 assert!(result.is_err(), "Version mismatch should fail");
1062
1063 let err = result.unwrap_err();
1064 assert!(
1065 matches!(
1066 err.as_inner(),
1067 SuiErrorKind::UserInputError { error: UserInputError::ObjectVersionUnavailableForConsumption { provided_obj_ref, current_version: _ } }
1068 if provided_obj_ref.0 == immutable_id1
1069 ),
1070 "Expected ObjectVersionUnavailableForConsumption error, got: {:?}",
1071 err.as_inner()
1072 );
1073 }
1074 }
1075
1076 #[sim_test]
1077 async fn accept_already_executed_transaction() {
1078 let (sender, keypair) = deterministic_random_account_key();
1079
1080 let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
1081 let owned_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
1082
1083 let network_config =
1084 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
1085 .committee_size(NonZeroUsize::new(1).unwrap())
1086 .with_objects(vec![gas_object.clone(), owned_object.clone()])
1087 .build();
1088
1089 let state = TestAuthorityBuilder::new()
1090 .with_network_config(&network_config, 0)
1091 .build()
1092 .await;
1093
1094 let epoch_store = state.load_epoch_store_one_call_per_task();
1095
1096 let transaction = test_user_transaction(
1098 &state,
1099 sender,
1100 &keypair,
1101 gas_object.clone(),
1102 vec![owned_object.clone()],
1103 )
1104 .await;
1105 let tx_digest = *transaction.tx().digest();
1106 let cert =
1107 VerifiedExecutableTransaction::new_from_consensus(transaction.clone().into_tx(), 0);
1108 let (executed_effects, _) = state
1109 .try_execute_immediately(&cert, ExecutionEnv::new(), &state.epoch_store_for_testing())
1110 .unwrap();
1111
1112 let read_effects = state
1114 .get_transaction_cache_reader()
1115 .get_executed_effects(&tx_digest)
1116 .expect("Transaction should be executed");
1117 assert_eq!(read_effects, executed_effects);
1118 assert_eq!(read_effects.executed_epoch(), epoch_store.epoch());
1119
1120 let serialized_tx = bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
1122 &state.name,
1123 transaction.into(),
1124 ))
1125 .unwrap();
1126 let validator = SuiTxValidator::new(
1127 state.clone(),
1128 state.epoch_store_for_testing().clone(),
1129 Arc::new(CheckpointServiceNoop {}),
1130 SuiTxValidatorMetrics::new(&Default::default()),
1131 );
1132 let rejected_transactions = validator
1133 .verify_and_vote_batch(&BlockRef::MAX, &[&serialized_tx])
1134 .expect("Verify and vote should succeed");
1135
1136 assert!(rejected_transactions.is_empty());
1138 }
1139
1140 #[tokio::test]
1141 async fn test_reject_invalid_alias_signature_index() {
1142 let (sender, keypair) = deterministic_random_account_key();
1143
1144 let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
1145 let owned_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
1146
1147 let network_config =
1148 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
1149 .committee_size(NonZeroUsize::new(1).unwrap())
1150 .with_objects(vec![gas_object.clone(), owned_object.clone()])
1151 .build();
1152
1153 let state = TestAuthorityBuilder::new()
1154 .with_network_config(&network_config, 0)
1155 .build()
1156 .await;
1157
1158 let transaction = test_user_transaction(
1159 &state,
1160 sender,
1161 &keypair,
1162 gas_object.clone(),
1163 vec![owned_object.clone()],
1164 )
1165 .await;
1166
1167 let inner_tx: Transaction = transaction.into_tx().into();
1170 let bogus_aliases = nonempty::nonempty![(255u8, None)];
1171 let tx_with_bogus_alias = PlainTransactionWithClaims::from_aliases(inner_tx, bogus_aliases);
1172
1173 let serialized_tx = bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
1174 &state.name,
1175 tx_with_bogus_alias,
1176 ))
1177 .unwrap();
1178
1179 let validator = SuiTxValidator::new(
1180 state.clone(),
1181 state.epoch_store_for_testing().clone(),
1182 Arc::new(CheckpointServiceNoop {}),
1183 SuiTxValidatorMetrics::new(&Default::default()),
1184 );
1185
1186 let res = validator.verify_batch(&BlockRef::MIN, &[&serialized_tx]);
1187 assert!(
1188 res.is_err(),
1189 "Should reject transaction with out-of-bounds alias signature index"
1190 );
1191 }
1192
1193 #[tokio::test]
1196 async fn test_reject_disallowed_proposer() {
1197 let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut c| {
1198 c.set_allowed_proposers_for_testing(true);
1199 c
1200 });
1201
1202 let (sender, keypair) = deterministic_random_account_key();
1203 let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
1204 let owned_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
1205
1206 let network_config =
1207 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
1208 .committee_size(NonZeroUsize::new(4).unwrap())
1209 .with_objects(vec![gas_object.clone(), owned_object.clone()])
1210 .build();
1211
1212 let state = TestAuthorityBuilder::new()
1213 .with_network_config(&network_config, 0)
1214 .build()
1215 .await;
1216
1217 let transaction =
1218 test_user_transaction(&state, sender, &keypair, gas_object, vec![owned_object]).await;
1219
1220 let aliases = transaction.aliases().clone();
1223 let mut inner_tx: Transaction = transaction.into_tx().into();
1224 *inner_tx
1225 .data_mut_for_testing()
1226 .inner_mut()
1227 .intent_message
1228 .value
1229 .expiration_mut_for_testing() = TransactionExpiration::Validity {
1230 min_epoch: Some(0),
1231 max_epoch: Some(0),
1232 min_timestamp: None,
1233 max_timestamp: None,
1234 chain: state.get_chain_identifier(),
1235 nonce: 0,
1236 allowed_proposers: Some(AllowedProposers {
1237 epoch: state.epoch_store_for_testing().epoch(),
1238 proposers: nonempty::nonempty![2],
1239 }),
1240 };
1241
1242 let serialized_tx = bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
1243 &state.name,
1244 PlainTransactionWithClaims::from_aliases(inner_tx, aliases),
1245 ))
1246 .unwrap();
1247
1248 let validator = SuiTxValidator::new(
1249 state.clone(),
1250 state.epoch_store_for_testing().clone(),
1251 Arc::new(CheckpointServiceNoop {}),
1252 SuiTxValidatorMetrics::new(&Default::default()),
1253 );
1254
1255 let block_from = |author: u32| {
1256 BlockRef::new(
1257 1,
1258 AuthorityIndex::new_for_test(author),
1259 BlockDigest::default(),
1260 )
1261 };
1262 for disallowed in [0, 1, 3] {
1263 assert!(
1264 validator
1265 .verify_batch(&block_from(disallowed), &[&serialized_tx])
1266 .is_err(),
1267 "block from authority {disallowed} should be rejected"
1268 );
1269 }
1270 let res = validator.verify_batch(&block_from(2), &[&serialized_tx]);
1271 assert!(res.is_ok(), "{res:?}");
1272 }
1273
1274 #[tokio::test]
1279 async fn deny_config_updates_applied_at_verification() {
1280 let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut c| {
1281 c.set_share_transaction_deny_config_in_consensus_for_testing(true);
1282 c
1283 });
1284
1285 let network_config =
1288 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
1289 .committee_size(NonZeroUsize::new(1).unwrap())
1290 .build();
1291 let state = TestAuthorityBuilder::new()
1292 .with_network_config(&network_config, 0)
1293 .build()
1294 .await;
1295 let validator = SuiTxValidator::new(
1296 state.clone(),
1297 state.epoch_store_for_testing().clone(),
1298 Arc::new(CheckpointServiceNoop {}),
1299 SuiTxValidatorMetrics::new(&Default::default()),
1300 );
1301 let manager = state.transaction_deny_config_manager().clone();
1302
1303 let now_ms = crate::authority::AuthorityState::unixtime_now_ms();
1304 let msg_bytes = |authority, generation| {
1305 bcs::to_bytes(&ConsensusTransaction::new_update_transaction_deny_config(
1306 SharedTransactionDenyConfig::V1(SharedTransactionDenyConfigV1 {
1307 authority,
1308 generation,
1309 rules: Some(sui_types::transaction_deny_rules::TransactionDenyRules {
1310 package_publish_disabled: true,
1311 ..Default::default()
1312 }),
1313 }),
1314 ))
1315 .unwrap()
1316 };
1317
1318 let far_future = msg_bytes(
1320 state.name,
1321 now_ms + SharedTransactionDenyConfig::MAX_GENERATION_FUTURE_DRIFT_MS + 600_000,
1322 );
1323 assert!(
1324 validator
1325 .verify_and_vote_batch(&BlockRef::MIN, &[&far_future])
1326 .is_ok()
1327 );
1328 assert!(manager.peer_configs_snapshot().is_empty());
1329
1330 let spoofed = msg_bytes(sui_types::base_types::AuthorityName::ZERO, now_ms);
1333 assert!(
1334 validator
1335 .verify_and_vote_batch(&BlockRef::MIN, &[&spoofed])
1336 .is_ok()
1337 );
1338 assert!(manager.peer_configs_snapshot().is_empty());
1339
1340 let sane = msg_bytes(state.name, now_ms);
1342 assert!(
1343 validator
1344 .verify_and_vote_batch(&BlockRef::MIN, &[&sane])
1345 .is_ok()
1346 );
1347 let snapshot = manager.peer_configs_snapshot();
1348 assert_eq!(snapshot.get(&state.name).unwrap().generation(), now_ms);
1349 }
1350}