1use std::{collections::HashSet, sync::Arc};
5
6use consensus_core::{TransactionVerifier, ValidationError};
7use consensus_types::block::{BlockRef, TransactionIndex};
8use fastcrypto_tbls::dkg_v1;
9use mysten_metrics::monitored_scope;
10use prometheus::{
11 IntCounter, IntCounterVec, Registry, register_int_counter_vec_with_registry,
12 register_int_counter_with_registry,
13};
14use sui_macros::fail_point_arg;
15#[cfg(msim)]
16use sui_types::base_types::AuthorityName;
17use sui_types::{
18 base_types::{ObjectID, ObjectRef},
19 error::{SuiError, SuiErrorKind, SuiResult, UserInputError},
20 messages_consensus::{ConsensusPosition, ConsensusTransaction, ConsensusTransactionKind},
21 transaction::{
22 InputObjectKind, PlainTransactionWithClaims, TransactionDataAPI, TransactionWithClaims,
23 },
24};
25use tap::TapFallible;
26use tracing::{debug, info, instrument, warn};
27
28use crate::{
29 authority::{AuthorityState, authority_per_epoch_store::AuthorityPerEpochStore},
30 checkpoints::CheckpointServiceNotify,
31 consensus_adapter::{ConsensusOverloadChecker, NoopConsensusOverloadChecker},
32};
33
34#[derive(Clone)]
37pub struct SuiTxValidator {
38 authority_state: Arc<AuthorityState>,
39 epoch_store: Arc<AuthorityPerEpochStore>,
40 consensus_overload_checker: Arc<dyn ConsensusOverloadChecker>,
41 checkpoint_service: Arc<dyn CheckpointServiceNotify + Send + Sync>,
42 metrics: Arc<SuiTxValidatorMetrics>,
43}
44
45impl SuiTxValidator {
46 pub fn new(
47 authority_state: Arc<AuthorityState>,
48 epoch_store: Arc<AuthorityPerEpochStore>,
49 checkpoint_service: Arc<dyn CheckpointServiceNotify + Send + Sync>,
50 metrics: Arc<SuiTxValidatorMetrics>,
51 ) -> Self {
52 info!(
53 "SuiTxValidator constructed for epoch {}",
54 epoch_store.epoch()
55 );
56 let consensus_overload_checker = Arc::new(NoopConsensusOverloadChecker {});
58 Self {
59 authority_state,
60 epoch_store,
61 consensus_overload_checker,
62 checkpoint_service,
63 metrics,
64 }
65 }
66
67 fn validate_transactions(&self, txs: &[ConsensusTransactionKind]) -> Result<(), SuiError> {
68 let epoch_store = self.epoch_store.clone();
69 let mut cert_batch = Vec::new();
70 let mut ckpt_messages = Vec::new();
71 let mut ckpt_batch = Vec::new();
72 for tx in txs.iter() {
73 match tx {
74 ConsensusTransactionKind::CertifiedTransaction(certificate) => {
75 if epoch_store.protocol_config().disable_preconsensus_locking() {
76 return Err(SuiErrorKind::UnexpectedMessage(
77 "CertifiedTransaction cannot be used when preconsensus locking is disabled".to_string(),
78 )
79 .into());
80 }
81 cert_batch.push(certificate.as_ref());
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::EndOfPublish(_)
114 | ConsensusTransactionKind::NewJWKFetched(_, _, _)
115 | ConsensusTransactionKind::CapabilityNotificationV2(_)
116 | ConsensusTransactionKind::RandomnessStateUpdate(_, _) => {}
117
118 ConsensusTransactionKind::UserTransaction(_) => {
119 if epoch_store.protocol_config().address_aliases()
120 || epoch_store.protocol_config().disable_preconsensus_locking()
121 {
122 return Err(SuiErrorKind::UnexpectedMessage(
123 "ConsensusTransactionKind::UserTransaction cannot be used when address aliases is enabled or preconsensus locking is disabled".to_string(),
124 )
125 .into());
126 }
127 }
128
129 ConsensusTransactionKind::UserTransactionV2(tx) => {
130 if !(epoch_store.protocol_config().address_aliases()
131 || epoch_store.protocol_config().disable_preconsensus_locking())
132 {
133 return Err(SuiErrorKind::UnexpectedMessage(
134 "ConsensusTransactionKind::UserTransactionV2 must be used when either address aliases is enabled or preconsensus locking is disabled".to_string(),
135 )
136 .into());
137 }
138 if epoch_store.protocol_config().address_aliases() && tx.aliases().is_none() {
139 return Err(SuiErrorKind::UnexpectedMessage(
140 "ConsensusTransactionKind::UserTransactionV2 must contain an aliases claim".to_string(),
141 )
142 .into());
143 }
144 }
146
147 ConsensusTransactionKind::ExecutionTimeObservation(obs) => {
148 if obs.estimates.len()
150 > epoch_store
151 .protocol_config()
152 .max_programmable_tx_commands()
153 .try_into()
154 .unwrap()
155 {
156 return Err(SuiErrorKind::UnexpectedMessage(format!(
157 "ExecutionTimeObservation contains too many estimates: {}",
158 obs.estimates.len()
159 ))
160 .into());
161 }
162 }
163 }
164 }
165
166 let cert_count = cert_batch.len();
168 let ckpt_count = ckpt_batch.len();
169
170 epoch_store
171 .signature_verifier
172 .verify_certs_and_checkpoints(cert_batch, ckpt_batch)
173 .tap_err(|e| warn!("batch verification error: {}", e))?;
174
175 for ckpt in ckpt_messages {
177 self.checkpoint_service
178 .notify_checkpoint_signature(&epoch_store, ckpt)?;
179 }
180
181 self.metrics
182 .certificate_signatures_verified
183 .inc_by(cert_count as u64);
184 self.metrics
185 .checkpoint_signatures_verified
186 .inc_by(ckpt_count as u64);
187 Ok(())
188 }
189
190 #[instrument(level = "debug", skip_all, fields(block_ref))]
191 fn vote_transactions(
192 &self,
193 block_ref: &BlockRef,
194 txs: Vec<ConsensusTransactionKind>,
195 ) -> Vec<TransactionIndex> {
196 let epoch_store = self.epoch_store.clone();
197 if !epoch_store.protocol_config().mysticeti_fastpath() {
198 return vec![];
199 }
200
201 let mut reject_txn_votes = Vec::new();
202 for (i, tx) in txs.into_iter().enumerate() {
203 let tx: PlainTransactionWithClaims = match tx {
204 ConsensusTransactionKind::UserTransaction(tx) => {
205 TransactionWithClaims::no_aliases(*tx)
206 }
207 ConsensusTransactionKind::UserTransactionV2(tx) => *tx,
208 _ => continue,
209 };
210
211 let tx_digest = *tx.tx().digest();
212 if let Err(error) = self.vote_transaction(&epoch_store, tx) {
213 debug!(?tx_digest, "Voting to reject transaction: {error}");
214 self.metrics
215 .transaction_reject_votes
216 .with_label_values(&[error.to_variant_name()])
217 .inc();
218 reject_txn_votes.push(i as TransactionIndex);
219 epoch_store.set_rejection_vote_reason(
221 ConsensusPosition {
222 epoch: epoch_store.epoch(),
223 block: *block_ref,
224 index: i as TransactionIndex,
225 },
226 &error,
227 );
228 } else {
229 debug!(?tx_digest, "Voting to accept transaction");
230 }
231 }
232
233 reject_txn_votes
234 }
235
236 #[instrument(level = "debug", skip_all, err(level = "debug"), fields(tx_digest = ?tx.tx().digest()))]
237 fn vote_transaction(
238 &self,
239 epoch_store: &Arc<AuthorityPerEpochStore>,
240 tx: PlainTransactionWithClaims,
241 ) -> SuiResult<()> {
242 let aliases = tx.aliases();
244 let claimed_immutable_ids = tx.get_immutable_objects();
245 let inner_tx = tx.into_tx();
246
247 inner_tx.validity_check(&epoch_store.tx_validity_check_context())?;
250
251 self.authority_state.check_system_overload(
252 &*self.consensus_overload_checker,
253 inner_tx.data(),
254 self.authority_state.check_system_overload_at_signing(),
255 )?;
256
257 #[allow(unused_mut)]
258 let mut fail_point_always_report_aliases_changed = false;
259 fail_point_arg!(
260 "consensus-validator-always-report-aliases-changed",
261 |for_validators: Vec<AuthorityName>| {
262 if for_validators.contains(&self.authority_state.name) {
263 fail_point_always_report_aliases_changed = true;
265 }
266 }
267 );
268
269 let verified_tx = epoch_store.verify_transaction_with_current_aliases(inner_tx)?;
270
271 if epoch_store.protocol_config().address_aliases()
273 && (*verified_tx.aliases() != aliases.unwrap()
274 || fail_point_always_report_aliases_changed)
275 {
276 return Err(SuiErrorKind::AliasesChanged.into());
277 }
278
279 let inner_tx = verified_tx.into_tx();
280 self.authority_state
281 .handle_vote_transaction(epoch_store, inner_tx.clone())?;
282
283 if epoch_store.protocol_config().disable_preconsensus_locking()
284 && !claimed_immutable_ids.is_empty()
285 {
286 let owned_object_refs: HashSet<ObjectRef> = inner_tx
287 .data()
288 .transaction_data()
289 .input_objects()?
290 .iter()
291 .filter_map(|obj| match obj {
292 InputObjectKind::ImmOrOwnedMoveObject(obj_ref) => Some(*obj_ref),
293 _ => None,
294 })
295 .collect();
296 self.verify_immutable_object_claims(&claimed_immutable_ids, owned_object_refs)?;
297 }
298
299 Ok(())
300 }
301
302 fn verify_immutable_object_claims(
306 &self,
307 claimed_ids: &[ObjectID],
308 owned_object_refs: HashSet<ObjectRef>,
309 ) -> SuiResult<()> {
310 if claimed_ids.is_empty() {
311 return Ok(());
312 }
313
314 let objects = self
315 .authority_state
316 .get_object_cache_reader()
317 .get_objects(claimed_ids);
318
319 for (obj, id) in objects.into_iter().zip(claimed_ids.iter()) {
320 match obj {
321 Some(o) => {
322 let object_ref = o.compute_object_reference();
324 if !owned_object_refs.contains(&o.compute_object_reference()) {
325 return Err(SuiErrorKind::ImmutableObjectClaimNotFoundInInput {
326 object_id: *id,
327 }
328 .into());
329 }
330 if !o.is_immutable() {
331 return Err(SuiErrorKind::InvalidImmutableObjectClaim {
332 claimed_object_id: *id,
333 found_object_ref: object_ref,
334 }
335 .into());
336 }
337 }
338 None => {
339 return Err(SuiErrorKind::UserInputError {
342 error: UserInputError::ObjectNotFound {
343 object_id: *id,
344 version: None,
345 },
346 }
347 .into());
348 }
349 }
350 }
351
352 Ok(())
353 }
354}
355
356fn tx_kind_from_bytes(tx: &[u8]) -> Result<ConsensusTransactionKind, ValidationError> {
357 bcs::from_bytes::<ConsensusTransaction>(tx)
358 .map_err(|e| {
359 ValidationError::InvalidTransaction(format!(
360 "Failed to parse transaction bytes: {:?}",
361 e
362 ))
363 })
364 .map(|tx| tx.kind)
365}
366
367impl TransactionVerifier for SuiTxValidator {
368 fn verify_batch(&self, batch: &[&[u8]]) -> Result<(), ValidationError> {
369 let _scope = monitored_scope("ValidateBatch");
370
371 let txs: Vec<_> = batch
372 .iter()
373 .map(|tx| tx_kind_from_bytes(tx))
374 .collect::<Result<Vec<_>, _>>()?;
375
376 self.validate_transactions(&txs)
377 .map_err(|e| ValidationError::InvalidTransaction(e.to_string()))
378 }
379
380 fn verify_and_vote_batch(
381 &self,
382 block_ref: &BlockRef,
383 batch: &[&[u8]],
384 ) -> Result<Vec<TransactionIndex>, ValidationError> {
385 let _scope = monitored_scope("VerifyAndVoteBatch");
386
387 let txs: Vec<_> = batch
388 .iter()
389 .map(|tx| tx_kind_from_bytes(tx))
390 .collect::<Result<Vec<_>, _>>()?;
391
392 self.validate_transactions(&txs)
393 .map_err(|e| ValidationError::InvalidTransaction(e.to_string()))?;
394
395 Ok(self.vote_transactions(block_ref, txs))
396 }
397}
398
399pub struct SuiTxValidatorMetrics {
400 certificate_signatures_verified: IntCounter,
401 checkpoint_signatures_verified: IntCounter,
402 transaction_reject_votes: IntCounterVec,
403}
404
405impl SuiTxValidatorMetrics {
406 pub fn new(registry: &Registry) -> Arc<Self> {
407 Arc::new(Self {
408 certificate_signatures_verified: register_int_counter_with_registry!(
409 "tx_validator_certificate_signatures_verified",
410 "Number of certificates verified in consensus batch verifier",
411 registry
412 )
413 .unwrap(),
414 checkpoint_signatures_verified: register_int_counter_with_registry!(
415 "tx_validator_checkpoint_signatures_verified",
416 "Number of checkpoint verified in consensus batch verifier",
417 registry
418 )
419 .unwrap(),
420 transaction_reject_votes: register_int_counter_vec_with_registry!(
421 "tx_validator_transaction_reject_votes",
422 "Number of reject transaction votes per reason",
423 &["reason"],
424 registry
425 )
426 .unwrap(),
427 })
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use std::num::NonZeroUsize;
434 use std::sync::Arc;
435
436 use consensus_core::TransactionVerifier as _;
437 use consensus_types::block::BlockRef;
438 use fastcrypto::traits::KeyPair;
439 use sui_config::transaction_deny_config::TransactionDenyConfigBuilder;
440 use sui_macros::sim_test;
441 use sui_protocol_config::ProtocolConfig;
442 use sui_types::crypto::deterministic_random_account_key;
443 use sui_types::error::{SuiErrorKind, UserInputError};
444 use sui_types::executable_transaction::VerifiedExecutableTransaction;
445 use sui_types::messages_checkpoint::{
446 CheckpointContents, CheckpointSignatureMessage, CheckpointSummary, SignedCheckpointSummary,
447 };
448 use sui_types::messages_consensus::ConsensusPosition;
449 use sui_types::{
450 base_types::{ExecutionDigests, ObjectID},
451 crypto::Ed25519SuiSignature,
452 effects::TransactionEffectsAPI as _,
453 messages_consensus::ConsensusTransaction,
454 object::Object,
455 signature::GenericSignature,
456 transaction::{PlainTransactionWithClaims, Transaction},
457 };
458
459 use crate::authority::ExecutionEnv;
460 use crate::{
461 authority::test_authority_builder::TestAuthorityBuilder,
462 checkpoints::CheckpointServiceNoop,
463 consensus_adapter::consensus_tests::{
464 test_gas_objects, test_user_transaction, test_user_transactions,
465 },
466 consensus_validator::{SuiTxValidator, SuiTxValidatorMetrics},
467 };
468
469 #[sim_test]
470 async fn accept_valid_transaction() {
471 let mut objects = test_gas_objects();
473 let shared_object = Object::shared_for_testing();
474 objects.push(shared_object.clone());
475
476 let network_config =
477 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
478 .with_objects(objects.clone())
479 .build();
480
481 let state = TestAuthorityBuilder::new()
482 .with_network_config(&network_config, 0)
483 .build()
484 .await;
485 let name1 = state.name;
486 let transactions = test_user_transactions(&state, shared_object).await;
487
488 let first_transaction = transactions[0].clone();
489 let first_transaction_bytes: Vec<u8> =
490 bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
491 &name1,
492 first_transaction.into(),
493 ))
494 .unwrap();
495
496 let metrics = SuiTxValidatorMetrics::new(&Default::default());
497 let validator = SuiTxValidator::new(
498 state.clone(),
499 state.epoch_store_for_testing().clone(),
500 Arc::new(CheckpointServiceNoop {}),
501 metrics,
502 );
503 let res = validator.verify_batch(&[&first_transaction_bytes]);
504 assert!(res.is_ok(), "{res:?}");
505
506 let transaction_bytes: Vec<_> = transactions
507 .clone()
508 .into_iter()
509 .map(|tx| {
510 bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
511 &name1,
512 tx.into(),
513 ))
514 .unwrap()
515 })
516 .collect();
517
518 let batch: Vec<_> = transaction_bytes.iter().map(|t| t.as_slice()).collect();
519 let res_batch = validator.verify_batch(&batch);
520 assert!(res_batch.is_ok(), "{res_batch:?}");
521
522 let bogus_transaction_bytes: Vec<_> = transactions
523 .into_iter()
524 .map(|tx| {
525 let aliases = tx.aliases().clone();
527 let mut signed_tx: Transaction = tx.into_tx().into();
528 signed_tx.tx_signatures_mut_for_testing()[0] =
529 GenericSignature::Signature(sui_types::crypto::Signature::Ed25519SuiSignature(
530 Ed25519SuiSignature::default(),
531 ));
532 let tx_with_claims = PlainTransactionWithClaims::from_aliases(signed_tx, aliases);
533 bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
534 &name1,
535 tx_with_claims,
536 ))
537 .unwrap()
538 })
539 .collect();
540
541 let batch: Vec<_> = bogus_transaction_bytes
542 .iter()
543 .map(|t| t.as_slice())
544 .collect();
545 let res_batch = validator.verify_and_vote_batch(&BlockRef::MIN, &batch);
548 assert!(res_batch.is_ok());
549 let rejections = res_batch.unwrap();
551 assert_eq!(
552 rejections.len(),
553 batch.len(),
554 "All bogus transactions should be rejected"
555 );
556 }
557
558 #[tokio::test]
559 async fn test_verify_and_vote_batch() {
560 let (sender, keypair) = deterministic_random_account_key();
562
563 let gas_objects: Vec<Object> = (0..8)
565 .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
566 .collect();
567
568 let owned_objects: Vec<Object> = (0..2)
570 .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
571 .collect();
572 let denied_object = owned_objects[1].clone();
573
574 let mut objects = gas_objects.clone();
575 objects.extend(owned_objects.clone());
576
577 let network_config =
578 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
579 .committee_size(NonZeroUsize::new(1).unwrap())
580 .with_objects(objects.clone())
581 .build();
582
583 let transaction_deny_config = TransactionDenyConfigBuilder::new()
585 .add_denied_object(denied_object.id())
586 .build();
587 let state = TestAuthorityBuilder::new()
588 .with_network_config(&network_config, 0)
589 .with_transaction_deny_config(transaction_deny_config)
590 .build()
591 .await;
592
593 let valid_transaction = test_user_transaction(
597 &state,
598 sender,
599 &keypair,
600 gas_objects[0].clone(),
601 vec![owned_objects[0].clone()],
602 )
603 .await;
604
605 let invalid_transaction = test_user_transaction(
607 &state,
608 sender,
609 &keypair,
610 gas_objects[1].clone(),
611 vec![denied_object.clone()],
612 )
613 .await;
614
615 let transactions = vec![valid_transaction, invalid_transaction];
617 let serialized_transactions: Vec<_> = transactions
618 .into_iter()
619 .map(|t| {
620 bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
621 &state.name,
622 t.into(),
623 ))
624 .unwrap()
625 })
626 .collect();
627 let batch: Vec<_> = serialized_transactions
628 .iter()
629 .map(|t| t.as_slice())
630 .collect();
631
632 let validator = SuiTxValidator::new(
633 state.clone(),
634 state.epoch_store_for_testing().clone(),
635 Arc::new(CheckpointServiceNoop {}),
636 SuiTxValidatorMetrics::new(&Default::default()),
637 );
638
639 let rejected_transactions = validator
641 .verify_and_vote_batch(&BlockRef::MAX, &batch)
642 .unwrap();
643
644 assert_eq!(rejected_transactions, vec![1]);
647
648 let epoch_store = state.load_epoch_store_one_call_per_task();
651 let reason = epoch_store
652 .get_rejection_vote_reason(ConsensusPosition {
653 epoch: state.load_epoch_store_one_call_per_task().epoch(),
654 block: BlockRef::MAX,
655 index: 1,
656 })
657 .expect("Rejection vote reason should be set");
658
659 assert_eq!(
660 reason,
661 SuiErrorKind::UserInputError {
662 error: UserInputError::TransactionDenied {
663 error: format!(
664 "Access to input object {:?} is temporarily disabled",
665 denied_object.id()
666 )
667 }
668 }
669 );
670 }
671
672 #[sim_test]
673 async fn accept_checkpoint_signature_v2() {
674 let network_config =
675 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir().build();
676
677 let state = TestAuthorityBuilder::new()
678 .with_network_config(&network_config, 0)
679 .build()
680 .await;
681
682 let epoch_store = state.load_epoch_store_one_call_per_task();
683
684 let checkpoint_summary = CheckpointSummary::new(
686 &ProtocolConfig::get_for_max_version_UNSAFE(),
687 epoch_store.epoch(),
688 0,
689 0,
690 &CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]),
691 None,
692 Default::default(),
693 None,
694 0,
695 Vec::new(),
696 Vec::new(),
697 );
698
699 let keypair = network_config.validator_configs()[0].protocol_key_pair();
700 let authority = keypair.public().into();
701 let signed = SignedCheckpointSummary::new(
702 epoch_store.epoch(),
703 checkpoint_summary,
704 keypair,
705 authority,
706 );
707 let message = CheckpointSignatureMessage { summary: signed };
708
709 let tx = ConsensusTransaction::new_checkpoint_signature_message_v2(message);
710 let bytes = bcs::to_bytes(&tx).unwrap();
711
712 let validator = SuiTxValidator::new(
713 state.clone(),
714 state.epoch_store_for_testing().clone(),
715 Arc::new(CheckpointServiceNoop {}),
716 SuiTxValidatorMetrics::new(&Default::default()),
717 );
718
719 let res = validator.verify_batch(&[&bytes]);
720 assert!(res.is_ok(), "{res:?}");
721 }
722
723 #[sim_test]
724 async fn accept_already_executed_transaction() {
725 let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
729 config.set_disable_preconsensus_locking_for_testing(false);
730 config
731 });
732
733 let (sender, keypair) = deterministic_random_account_key();
734
735 let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
736 let owned_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
737
738 let network_config =
739 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
740 .committee_size(NonZeroUsize::new(1).unwrap())
741 .with_objects(vec![gas_object.clone(), owned_object.clone()])
742 .build();
743
744 let state = TestAuthorityBuilder::new()
745 .with_network_config(&network_config, 0)
746 .build()
747 .await;
748
749 let epoch_store = state.load_epoch_store_one_call_per_task();
750
751 let transaction = test_user_transaction(
753 &state,
754 sender,
755 &keypair,
756 gas_object.clone(),
757 vec![owned_object.clone()],
758 )
759 .await
760 .into_tx();
761 let tx_digest = *transaction.digest();
762 let cert = VerifiedExecutableTransaction::new_from_consensus(transaction.clone(), 0);
763 let (executed_effects, _) = state
764 .try_execute_immediately(&cert, ExecutionEnv::new(), &state.epoch_store_for_testing())
765 .await
766 .unwrap();
767
768 let read_effects = state
770 .get_transaction_cache_reader()
771 .get_executed_effects(&tx_digest)
772 .expect("Transaction should be executed");
773 assert_eq!(read_effects, executed_effects);
774 assert_eq!(read_effects.executed_epoch(), epoch_store.epoch());
775
776 let serialized_tx = bcs::to_bytes(&ConsensusTransaction::new_user_transaction_message(
778 &state.name,
779 transaction.into_inner().clone(),
780 ))
781 .unwrap();
782 let validator = SuiTxValidator::new(
783 state.clone(),
784 state.epoch_store_for_testing().clone(),
785 Arc::new(CheckpointServiceNoop {}),
786 SuiTxValidatorMetrics::new(&Default::default()),
787 );
788 let rejected_transactions = validator
789 .verify_and_vote_batch(&BlockRef::MAX, &[&serialized_tx])
790 .expect("Verify and vote should succeed");
791
792 assert!(rejected_transactions.is_empty());
794 }
795}