sui_core/
consensus_validator.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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/// Validates transactions from consensus and votes on whether to execute the transactions
35/// based on their validity and the current state of the authority.
36#[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        // Intentionally do not check consensus overload, because this is validating transactions already in consensus.
57        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                    // TODO(fastpath): move deterministic verifications of user transactions here.
145                }
146
147                ConsensusTransactionKind::ExecutionTimeObservation(obs) => {
148                    // TODO: Use a separate limit for this that may truncate shared observations.
149                    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        // verify the certificate signatures as a batch
167        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        // All checkpoint sigs have been verified, forward them to the checkpoint service
176        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                // Cache the rejection vote reason (error) for the transaction
220                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        // Extract claims before consuming the transaction
243        let aliases = tx.aliases();
244        let claimed_immutable_ids = tx.get_immutable_objects();
245        let inner_tx = tx.into_tx();
246
247        // Currently validity_check() and verify_transaction() are not required to be consistent across validators,
248        // so they do not run in validate_transactions(). They can run there once we confirm it is safe.
249        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                    // always report aliases changed in simtests
264                    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        // aliases must have data when address_aliases() is enabled.
272        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    /// Verify that all claimed immutable objects are actually immutable.
303    /// Rejects if any claimed object doesn't exist locally (can't verify) or is not immutable.
304    /// This is stricter than general voting because the claim directly controls locking behavior.
305    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                    // Object exists but is NOT immutable - invalid claim
323                    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                    // Object not found - we can't verify the claim, so we must reject.
340                    // This branch should not happen because owned input objects are already validated to exist.
341                    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        // Initialize an authority with a (owned) gas object and a shared object.
472        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                // Create a transaction with an invalid signature
526                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        // verify_batch doesn't verify user transaction signatures (that happens in vote_transaction).
546        // Use verify_and_vote_batch to test that bogus transactions are rejected during voting.
547        let res_batch = validator.verify_and_vote_batch(&BlockRef::MIN, &batch);
548        assert!(res_batch.is_ok());
549        // All transactions should be in the rejection list since they have invalid signatures
550        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        // 1 account keypair
561        let (sender, keypair) = deterministic_random_account_key();
562
563        // 8 gas objects.
564        let gas_objects: Vec<Object> = (0..8)
565            .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
566            .collect();
567
568        // 2 owned objects.
569        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        // 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.
584        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        // Create two user transactions
594
595        // A valid transaction
596        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        // An invalid transaction where the input object is denied
606        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        // Now create the vector with the transactions and serialize them.
616        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        // WHEN
640        let rejected_transactions = validator
641            .verify_and_vote_batch(&BlockRef::MAX, &batch)
642            .unwrap();
643
644        // THEN
645        // The 2nd transaction should be rejected
646        assert_eq!(rejected_transactions, vec![1]);
647
648        // AND
649        // The reject reason should get cached
650        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        // Create a minimal checkpoint summary and sign it with the validator's protocol key
685        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        // This test uses ConsensusTransaction::new_user_transaction_message which creates a
726        // UserTransaction. When disable_preconsensus_locking=true (protocol version 105+),
727        // UserTransaction is not allowed. Gate with disable_preconsensus_locking=false.
728        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        // Create a transaction and execute it.
752        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        // Verify the transaction is executed.
769        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        // Now try to vote on the already executed transaction
777        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        // The executed transaction should NOT be rejected.
793        assert!(rejected_transactions.is_empty());
794    }
795}