sui_core/
consensus_validator.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{
5    authority::{AuthorityState, authority_per_epoch_store::AuthorityPerEpochStore},
6    checkpoints::CheckpointServiceNotify,
7    consensus_adapter::{ConsensusOverloadChecker, NoopConsensusOverloadChecker},
8};
9use consensus_core::{TransactionVerifier, ValidationError};
10use consensus_types::block::{BlockRef, TransactionIndex};
11use fastcrypto_tbls::dkg_v1;
12use mysten_metrics::monitored_scope;
13use prometheus::{
14    IntCounter, IntCounterVec, Registry, register_int_counter_vec_with_registry,
15    register_int_counter_with_registry,
16};
17use std::sync::Arc;
18use sui_macros::fail_point_arg;
19#[cfg(msim)]
20use sui_types::base_types::AuthorityName;
21use sui_types::{
22    error::{SuiError, SuiErrorKind, SuiResult},
23    messages_consensus::{ConsensusPosition, ConsensusTransaction, ConsensusTransactionKind},
24    transaction::{TransactionDataAPI, TransactionWithAliases, WithAliases},
25};
26use tap::TapFallible;
27use tracing::{debug, info, instrument, warn};
28
29/// Validates transactions from consensus and votes on whether to execute the transactions
30/// based on their validity and the current state of the authority.
31#[derive(Clone)]
32pub struct SuiTxValidator {
33    authority_state: Arc<AuthorityState>,
34    consensus_overload_checker: Arc<dyn ConsensusOverloadChecker>,
35    checkpoint_service: Arc<dyn CheckpointServiceNotify + Send + Sync>,
36    metrics: Arc<SuiTxValidatorMetrics>,
37}
38
39impl SuiTxValidator {
40    pub fn new(
41        authority_state: Arc<AuthorityState>,
42        checkpoint_service: Arc<dyn CheckpointServiceNotify + Send + Sync>,
43        metrics: Arc<SuiTxValidatorMetrics>,
44    ) -> Self {
45        let epoch_store = authority_state.load_epoch_store_one_call_per_task().clone();
46        info!(
47            "SuiTxValidator constructed for epoch {}",
48            epoch_store.epoch()
49        );
50        // Intentionally do not check consensus overload, because this is validating transactions already in consensus.
51        let consensus_overload_checker = Arc::new(NoopConsensusOverloadChecker {});
52        Self {
53            authority_state,
54            consensus_overload_checker,
55            checkpoint_service,
56            metrics,
57        }
58    }
59
60    fn validate_transactions(&self, txs: &[ConsensusTransactionKind]) -> Result<(), SuiError> {
61        let epoch_store = self.authority_state.load_epoch_store_one_call_per_task();
62
63        let mut cert_batch = Vec::new();
64        let mut ckpt_messages = Vec::new();
65        let mut ckpt_batch = Vec::new();
66        for tx in txs.iter() {
67            match tx {
68                ConsensusTransactionKind::CertifiedTransaction(certificate) => {
69                    cert_batch.push(certificate.as_ref());
70                }
71                ConsensusTransactionKind::CheckpointSignature(signature) => {
72                    ckpt_messages.push(signature.as_ref());
73                    ckpt_batch.push(&signature.summary);
74                }
75                ConsensusTransactionKind::CheckpointSignatureV2(signature) => {
76                    if !epoch_store
77                        .protocol_config()
78                        .consensus_checkpoint_signature_key_includes_digest()
79                    {
80                        return Err(SuiErrorKind::UnexpectedMessage(
81                            "ConsensusTransactionKind::CheckpointSignatureV2 is unsupported"
82                                .to_string(),
83                        )
84                        .into());
85                    }
86                    ckpt_messages.push(signature.as_ref());
87                    ckpt_batch.push(&signature.summary);
88                }
89                ConsensusTransactionKind::RandomnessDkgMessage(_, bytes) => {
90                    if bytes.len() > dkg_v1::DKG_MESSAGES_MAX_SIZE {
91                        warn!("batch verification error: DKG Message too large");
92                        return Err(SuiErrorKind::InvalidDkgMessageSize.into());
93                    }
94                }
95                ConsensusTransactionKind::RandomnessDkgConfirmation(_, bytes) => {
96                    if bytes.len() > dkg_v1::DKG_MESSAGES_MAX_SIZE {
97                        warn!("batch verification error: DKG Confirmation too large");
98                        return Err(SuiErrorKind::InvalidDkgMessageSize.into());
99                    }
100                }
101
102                ConsensusTransactionKind::CapabilityNotification(_) => {}
103
104                ConsensusTransactionKind::EndOfPublish(_)
105                | ConsensusTransactionKind::NewJWKFetched(_, _, _)
106                | ConsensusTransactionKind::CapabilityNotificationV2(_)
107                | ConsensusTransactionKind::RandomnessStateUpdate(_, _) => {}
108
109                ConsensusTransactionKind::UserTransaction(_)
110                | ConsensusTransactionKind::UserTransactionV2(_) => {
111                    if !epoch_store.protocol_config().mysticeti_fastpath() {
112                        return Err(SuiErrorKind::UnexpectedMessage(
113                            "ConsensusTransactionKind::UserTransaction is unsupported".to_string(),
114                        )
115                        .into());
116                    }
117                    // TODO(fastpath): move deterministic verifications of user transactions here,
118                    // for example validity_check() and verify_transaction().
119                }
120
121                ConsensusTransactionKind::ExecutionTimeObservation(obs) => {
122                    // TODO: Use a separate limit for this that may truncate shared observations.
123                    if obs.estimates.len()
124                        > epoch_store
125                            .protocol_config()
126                            .max_programmable_tx_commands()
127                            .try_into()
128                            .unwrap()
129                    {
130                        return Err(SuiErrorKind::UnexpectedMessage(format!(
131                            "ExecutionTimeObservation contains too many estimates: {}",
132                            obs.estimates.len()
133                        ))
134                        .into());
135                    }
136                }
137            }
138        }
139
140        // verify the certificate signatures as a batch
141        let cert_count = cert_batch.len();
142        let ckpt_count = ckpt_batch.len();
143
144        epoch_store
145            .signature_verifier
146            .verify_certs_and_checkpoints(cert_batch, ckpt_batch)
147            .tap_err(|e| warn!("batch verification error: {}", e))?;
148
149        // All checkpoint sigs have been verified, forward them to the checkpoint service
150        for ckpt in ckpt_messages {
151            self.checkpoint_service
152                .notify_checkpoint_signature(&epoch_store, ckpt)?;
153        }
154
155        self.metrics
156            .certificate_signatures_verified
157            .inc_by(cert_count as u64);
158        self.metrics
159            .checkpoint_signatures_verified
160            .inc_by(ckpt_count as u64);
161        Ok(())
162    }
163
164    #[instrument(level = "debug", skip_all, fields(block_ref))]
165    fn vote_transactions(
166        &self,
167        block_ref: &BlockRef,
168        txs: Vec<ConsensusTransactionKind>,
169    ) -> Vec<TransactionIndex> {
170        let epoch_store = self.authority_state.load_epoch_store_one_call_per_task();
171        if !epoch_store.protocol_config().mysticeti_fastpath() {
172            return vec![];
173        }
174
175        let mut result = Vec::new();
176        for (i, tx) in txs.into_iter().enumerate() {
177            let tx = match tx {
178                ConsensusTransactionKind::UserTransaction(tx) => {
179                    let no_aliases_allowed = tx
180                        .intent_message()
181                        .value
182                        .required_signers()
183                        .map(|s| (s, None));
184                    WithAliases::new(*tx, no_aliases_allowed)
185                }
186                ConsensusTransactionKind::UserTransactionV2(tx) => *tx,
187                _ => continue,
188            };
189
190            let tx_digest = *tx.tx().digest();
191            if let Err(error) = self.vote_transaction(&epoch_store, tx) {
192                debug!(?tx_digest, "Voting to reject transaction: {error}");
193                self.metrics
194                    .transaction_reject_votes
195                    .with_label_values(&[error.to_variant_name()])
196                    .inc();
197                result.push(i as TransactionIndex);
198                // Cache the rejection vote reason (error) for the transaction
199                epoch_store.set_rejection_vote_reason(
200                    ConsensusPosition {
201                        epoch: epoch_store.epoch(),
202                        block: *block_ref,
203                        index: i as TransactionIndex,
204                    },
205                    &error,
206                );
207            } else {
208                debug!(?tx_digest, "Voting to accept transaction");
209            }
210        }
211
212        result
213    }
214
215    #[instrument(level = "debug", skip_all, err(level = "debug"), fields(tx_digest = ?tx.tx().digest()))]
216    fn vote_transaction(
217        &self,
218        epoch_store: &Arc<AuthorityPerEpochStore>,
219        tx: TransactionWithAliases,
220    ) -> SuiResult<()> {
221        let (tx, aliases) = tx.into_inner();
222
223        // Currently validity_check() and verify_transaction() are not required to be consistent across validators,
224        // so they do not run in validate_transactions(). They can run there once we confirm it is safe.
225        tx.validity_check(&epoch_store.tx_validity_check_context())?;
226
227        self.authority_state.check_system_overload(
228            &*self.consensus_overload_checker,
229            tx.data(),
230            self.authority_state.check_system_overload_at_signing(),
231        )?;
232
233        #[allow(unused_mut)]
234        let mut fail_point_always_report_aliases_changed = false;
235        fail_point_arg!(
236            "consensus-validator-always-report-aliases-changed",
237            |for_validators: Vec<AuthorityName>| {
238                if for_validators.contains(&self.authority_state.name) {
239                    // always report aliases changed in simtests
240                    fail_point_always_report_aliases_changed = true;
241                }
242            }
243        );
244
245        let verified_tx = epoch_store.verify_transaction_with_current_aliases(tx)?;
246        if *verified_tx.aliases() != aliases || fail_point_always_report_aliases_changed {
247            return Err(SuiErrorKind::AliasesChanged.into());
248        }
249
250        self.authority_state
251            .handle_vote_transaction(epoch_store, verified_tx.into_tx())?;
252
253        Ok(())
254    }
255}
256
257fn tx_kind_from_bytes(tx: &[u8]) -> Result<ConsensusTransactionKind, ValidationError> {
258    bcs::from_bytes::<ConsensusTransaction>(tx)
259        .map_err(|e| {
260            ValidationError::InvalidTransaction(format!(
261                "Failed to parse transaction bytes: {:?}",
262                e
263            ))
264        })
265        .map(|tx| tx.kind)
266}
267
268impl TransactionVerifier for SuiTxValidator {
269    fn verify_batch(&self, batch: &[&[u8]]) -> Result<(), ValidationError> {
270        let _scope = monitored_scope("ValidateBatch");
271
272        let txs: Vec<_> = batch
273            .iter()
274            .map(|tx| tx_kind_from_bytes(tx))
275            .collect::<Result<Vec<_>, _>>()?;
276
277        self.validate_transactions(&txs)
278            .map_err(|e| ValidationError::InvalidTransaction(e.to_string()))
279    }
280
281    fn verify_and_vote_batch(
282        &self,
283        block_ref: &BlockRef,
284        batch: &[&[u8]],
285    ) -> Result<Vec<TransactionIndex>, ValidationError> {
286        let _scope = monitored_scope("VerifyAndVoteBatch");
287
288        let txs: Vec<_> = batch
289            .iter()
290            .map(|tx| tx_kind_from_bytes(tx))
291            .collect::<Result<Vec<_>, _>>()?;
292
293        self.validate_transactions(&txs)
294            .map_err(|e| ValidationError::InvalidTransaction(e.to_string()))?;
295
296        Ok(self.vote_transactions(block_ref, txs))
297    }
298}
299
300pub struct SuiTxValidatorMetrics {
301    certificate_signatures_verified: IntCounter,
302    checkpoint_signatures_verified: IntCounter,
303    transaction_reject_votes: IntCounterVec,
304}
305
306impl SuiTxValidatorMetrics {
307    pub fn new(registry: &Registry) -> Arc<Self> {
308        Arc::new(Self {
309            certificate_signatures_verified: register_int_counter_with_registry!(
310                "tx_validator_certificate_signatures_verified",
311                "Number of certificates verified in consensus batch verifier",
312                registry
313            )
314            .unwrap(),
315            checkpoint_signatures_verified: register_int_counter_with_registry!(
316                "tx_validator_checkpoint_signatures_verified",
317                "Number of checkpoint verified in consensus batch verifier",
318                registry
319            )
320            .unwrap(),
321            transaction_reject_votes: register_int_counter_vec_with_registry!(
322                "tx_validator_transaction_reject_votes",
323                "Number of reject transaction votes per reason",
324                &["reason"],
325                registry
326            )
327            .unwrap(),
328        })
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use std::num::NonZeroUsize;
335    use std::sync::Arc;
336
337    use consensus_core::TransactionVerifier as _;
338    use consensus_types::block::BlockRef;
339    use fastcrypto::traits::KeyPair;
340    use sui_config::transaction_deny_config::TransactionDenyConfigBuilder;
341    use sui_macros::sim_test;
342    use sui_protocol_config::{Chain, ProtocolConfig, ProtocolVersion};
343    use sui_types::crypto::deterministic_random_account_key;
344    use sui_types::error::{SuiErrorKind, UserInputError};
345    use sui_types::executable_transaction::VerifiedExecutableTransaction;
346    use sui_types::messages_checkpoint::{
347        CheckpointContents, CheckpointSignatureMessage, CheckpointSummary, SignedCheckpointSummary,
348    };
349    use sui_types::messages_consensus::ConsensusPosition;
350    use sui_types::{
351        base_types::{ExecutionDigests, ObjectID},
352        crypto::Ed25519SuiSignature,
353        effects::TransactionEffectsAPI as _,
354        messages_consensus::ConsensusTransaction,
355        object::Object,
356        signature::GenericSignature,
357    };
358
359    use crate::authority::ExecutionEnv;
360    use crate::{
361        authority::test_authority_builder::TestAuthorityBuilder,
362        checkpoints::CheckpointServiceNoop,
363        consensus_adapter::consensus_tests::{
364            test_certificates, test_gas_objects, test_user_transaction,
365        },
366        consensus_validator::{SuiTxValidator, SuiTxValidatorMetrics},
367    };
368
369    #[sim_test]
370    async fn accept_valid_transaction() {
371        // Initialize an authority with a (owned) gas object and a shared object; then
372        // make a test certificate.
373        let mut objects = test_gas_objects();
374        let shared_object = Object::shared_for_testing();
375        objects.push(shared_object.clone());
376
377        let network_config =
378            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
379                .with_objects(objects.clone())
380                .build();
381
382        let state = TestAuthorityBuilder::new()
383            .with_network_config(&network_config, 0)
384            .build()
385            .await;
386        let name1 = state.name;
387        let certificates = test_certificates(&state, shared_object).await;
388
389        let first_transaction = certificates[0].clone();
390        let first_transaction_bytes: Vec<u8> = bcs::to_bytes(
391            &ConsensusTransaction::new_certificate_message(&name1, first_transaction),
392        )
393        .unwrap();
394
395        let metrics = SuiTxValidatorMetrics::new(&Default::default());
396        let validator =
397            SuiTxValidator::new(state.clone(), Arc::new(CheckpointServiceNoop {}), metrics);
398        let res = validator.verify_batch(&[&first_transaction_bytes]);
399        assert!(res.is_ok(), "{res:?}");
400
401        let transaction_bytes: Vec<_> = certificates
402            .clone()
403            .into_iter()
404            .map(|cert| {
405                bcs::to_bytes(&ConsensusTransaction::new_certificate_message(&name1, cert)).unwrap()
406            })
407            .collect();
408
409        let batch: Vec<_> = transaction_bytes.iter().map(|t| t.as_slice()).collect();
410        let res_batch = validator.verify_batch(&batch);
411        assert!(res_batch.is_ok(), "{res_batch:?}");
412
413        let bogus_transaction_bytes: Vec<_> = certificates
414            .into_iter()
415            .map(|mut cert| {
416                // set it to an all-zero user signature
417                cert.tx_signatures_mut_for_testing()[0] =
418                    GenericSignature::Signature(sui_types::crypto::Signature::Ed25519SuiSignature(
419                        Ed25519SuiSignature::default(),
420                    ));
421                bcs::to_bytes(&ConsensusTransaction::new_certificate_message(&name1, cert)).unwrap()
422            })
423            .collect();
424
425        let batch: Vec<_> = bogus_transaction_bytes
426            .iter()
427            .map(|t| t.as_slice())
428            .collect();
429        let res_batch = validator.verify_batch(&batch);
430        assert!(res_batch.is_err());
431    }
432
433    #[tokio::test]
434    async fn test_verify_and_vote_batch() {
435        // 1 account keypair
436        let (sender, keypair) = deterministic_random_account_key();
437
438        // 8 gas objects.
439        let gas_objects: Vec<Object> = (0..8)
440            .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
441            .collect();
442
443        // 2 owned objects.
444        let owned_objects: Vec<Object> = (0..2)
445            .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
446            .collect();
447        let denied_object = owned_objects[1].clone();
448
449        let mut objects = gas_objects.clone();
450        objects.extend(owned_objects.clone());
451
452        let network_config =
453            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
454                .committee_size(NonZeroUsize::new(1).unwrap())
455                .with_objects(objects.clone())
456                .build();
457
458        // Add the 2nd object in the deny list. Once we try to process/vote on the transaction that depends on this object, it will be rejected.
459        let transaction_deny_config = TransactionDenyConfigBuilder::new()
460            .add_denied_object(denied_object.id())
461            .build();
462        let state = TestAuthorityBuilder::new()
463            .with_network_config(&network_config, 0)
464            .with_transaction_deny_config(transaction_deny_config)
465            .build()
466            .await;
467
468        // Create two user transactions
469
470        // A valid transaction
471        let valid_transaction = test_user_transaction(
472            &state,
473            sender,
474            &keypair,
475            gas_objects[0].clone(),
476            vec![owned_objects[0].clone()],
477        )
478        .await;
479
480        // An invalid transaction where the input object is denied
481        let invalid_transaction = test_user_transaction(
482            &state,
483            sender,
484            &keypair,
485            gas_objects[1].clone(),
486            vec![denied_object.clone()],
487        )
488        .await;
489
490        // Now create the vector with the transactions and serialize them.
491        let transactions = vec![valid_transaction, invalid_transaction];
492        let serialized_transactions: Vec<_> = transactions
493            .into_iter()
494            .map(|t| {
495                bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
496                    &state.name,
497                    t.into(),
498                ))
499                .unwrap()
500            })
501            .collect();
502        let batch: Vec<_> = serialized_transactions
503            .iter()
504            .map(|t| t.as_slice())
505            .collect();
506
507        let validator = SuiTxValidator::new(
508            state.clone(),
509            Arc::new(CheckpointServiceNoop {}),
510            SuiTxValidatorMetrics::new(&Default::default()),
511        );
512
513        // WHEN
514        let rejected_transactions = validator
515            .verify_and_vote_batch(&BlockRef::MAX, &batch)
516            .unwrap();
517
518        // THEN
519        // The 2nd transaction should be rejected
520        assert_eq!(rejected_transactions, vec![1]);
521
522        // AND
523        // The reject reason should get cached
524        let epoch_store = state.load_epoch_store_one_call_per_task();
525        let reason = epoch_store
526            .get_rejection_vote_reason(ConsensusPosition {
527                epoch: state.load_epoch_store_one_call_per_task().epoch(),
528                block: BlockRef::MAX,
529                index: 1,
530            })
531            .expect("Rejection vote reason should be set");
532
533        assert_eq!(
534            reason,
535            SuiErrorKind::UserInputError {
536                error: UserInputError::TransactionDenied {
537                    error: format!(
538                        "Access to input object {:?} is temporarily disabled",
539                        denied_object.id()
540                    )
541                }
542            }
543        );
544    }
545
546    #[sim_test]
547    async fn reject_checkpoint_signature_v2_when_flag_disabled() {
548        // Build a single-validator network and authority with protocol version < 93 (flag disabled)
549        let network_config =
550            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir().build();
551
552        let disabled_cfg =
553            ProtocolConfig::get_for_version(ProtocolVersion::new(92), Chain::Unknown);
554        let state = TestAuthorityBuilder::new()
555            .with_network_config(&network_config, 0)
556            .with_protocol_config(disabled_cfg)
557            .build()
558            .await;
559
560        let epoch_store = state.load_epoch_store_one_call_per_task();
561
562        // Create a minimal checkpoint summary and sign it with the validator's protocol key
563        let checkpoint_summary = CheckpointSummary::new(
564            &ProtocolConfig::get_for_max_version_UNSAFE(),
565            epoch_store.epoch(),
566            0,
567            0,
568            &CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]),
569            None,
570            Default::default(),
571            None,
572            0,
573            Vec::new(),
574            Vec::new(),
575        );
576
577        let keypair = network_config.validator_configs()[0].protocol_key_pair();
578        let authority = keypair.public().into();
579        let signed = SignedCheckpointSummary::new(
580            epoch_store.epoch(),
581            checkpoint_summary,
582            keypair,
583            authority,
584        );
585        let message = CheckpointSignatureMessage { summary: signed };
586
587        let tx = ConsensusTransaction::new_checkpoint_signature_message_v2(message);
588        let bytes = bcs::to_bytes(&tx).unwrap();
589
590        let validator = SuiTxValidator::new(
591            state.clone(),
592            Arc::new(CheckpointServiceNoop {}),
593            SuiTxValidatorMetrics::new(&Default::default()),
594        );
595
596        let res = validator.verify_batch(&[&bytes]);
597        assert!(res.is_err());
598    }
599
600    #[sim_test]
601    async fn accept_checkpoint_signature_v2_when_flag_enabled() {
602        // Build a single-validator network and authority with protocol version >= 93 (flag enabled)
603        let network_config =
604            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir().build();
605
606        let enabled_cfg = ProtocolConfig::get_for_version(ProtocolVersion::new(93), Chain::Unknown);
607        let state = TestAuthorityBuilder::new()
608            .with_network_config(&network_config, 0)
609            .with_protocol_config(enabled_cfg)
610            .build()
611            .await;
612
613        let epoch_store = state.load_epoch_store_one_call_per_task();
614
615        // Create a minimal checkpoint summary and sign it with the validator's protocol key
616        let checkpoint_summary = CheckpointSummary::new(
617            &ProtocolConfig::get_for_max_version_UNSAFE(),
618            epoch_store.epoch(),
619            0,
620            0,
621            &CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]),
622            None,
623            Default::default(),
624            None,
625            0,
626            Vec::new(),
627            Vec::new(),
628        );
629
630        let keypair = network_config.validator_configs()[0].protocol_key_pair();
631        let authority = keypair.public().into();
632        let signed = SignedCheckpointSummary::new(
633            epoch_store.epoch(),
634            checkpoint_summary,
635            keypair,
636            authority,
637        );
638        let message = CheckpointSignatureMessage { summary: signed };
639
640        let tx = ConsensusTransaction::new_checkpoint_signature_message_v2(message);
641        let bytes = bcs::to_bytes(&tx).unwrap();
642
643        let validator = SuiTxValidator::new(
644            state.clone(),
645            Arc::new(CheckpointServiceNoop {}),
646            SuiTxValidatorMetrics::new(&Default::default()),
647        );
648
649        let res = validator.verify_batch(&[&bytes]);
650        assert!(res.is_ok(), "{res:?}");
651    }
652
653    #[sim_test]
654    async fn accept_already_executed_transaction() {
655        let (sender, keypair) = deterministic_random_account_key();
656
657        let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
658        let owned_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
659
660        let network_config =
661            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
662                .committee_size(NonZeroUsize::new(1).unwrap())
663                .with_objects(vec![gas_object.clone(), owned_object.clone()])
664                .build();
665
666        let state = TestAuthorityBuilder::new()
667            .with_network_config(&network_config, 0)
668            .build()
669            .await;
670
671        let epoch_store = state.load_epoch_store_one_call_per_task();
672
673        // Create a transaction and execute it.
674        let transaction = test_user_transaction(
675            &state,
676            sender,
677            &keypair,
678            gas_object.clone(),
679            vec![owned_object.clone()],
680        )
681        .await
682        .into_tx();
683        let tx_digest = *transaction.digest();
684        let cert = VerifiedExecutableTransaction::new_from_quorum_execution(transaction.clone(), 0);
685        let (executed_effects, _) = state
686            .try_execute_immediately(&cert, ExecutionEnv::new(), &state.epoch_store_for_testing())
687            .await
688            .unwrap();
689
690        // Verify the transaction is executed.
691        let read_effects = state
692            .get_transaction_cache_reader()
693            .get_executed_effects(&tx_digest)
694            .expect("Transaction should be executed");
695        assert_eq!(read_effects, executed_effects);
696        assert_eq!(read_effects.executed_epoch(), epoch_store.epoch());
697
698        // Now try to vote on the already executed transaction
699        let serialized_tx = bcs::to_bytes(&ConsensusTransaction::new_user_transaction_message(
700            &state.name,
701            transaction.into_inner().clone(),
702        ))
703        .unwrap();
704        let validator = SuiTxValidator::new(
705            state.clone(),
706            Arc::new(CheckpointServiceNoop {}),
707            SuiTxValidatorMetrics::new(&Default::default()),
708        );
709        let rejected_transactions = validator
710            .verify_and_vote_batch(&BlockRef::MAX, &[&serialized_tx])
711            .expect("Verify and vote should succeed");
712
713        // The executed transaction should NOT be rejected.
714        assert!(rejected_transactions.is_empty());
715    }
716}