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(signature) => {
84                    ckpt_messages.push(signature.as_ref());
85                    ckpt_batch.push(&signature.summary);
86                }
87                ConsensusTransactionKind::CheckpointSignatureV2(signature) => {
88                    if !epoch_store
89                        .protocol_config()
90                        .consensus_checkpoint_signature_key_includes_digest()
91                    {
92                        return Err(SuiErrorKind::UnexpectedMessage(
93                            "ConsensusTransactionKind::CheckpointSignatureV2 is unsupported"
94                                .to_string(),
95                        )
96                        .into());
97                    }
98                    ckpt_messages.push(signature.as_ref());
99                    ckpt_batch.push(&signature.summary);
100                }
101                ConsensusTransactionKind::RandomnessDkgMessage(_, bytes) => {
102                    if bytes.len() > dkg_v1::DKG_MESSAGES_MAX_SIZE {
103                        warn!("batch verification error: DKG Message too large");
104                        return Err(SuiErrorKind::InvalidDkgMessageSize.into());
105                    }
106                }
107                ConsensusTransactionKind::RandomnessDkgConfirmation(_, bytes) => {
108                    if bytes.len() > dkg_v1::DKG_MESSAGES_MAX_SIZE {
109                        warn!("batch verification error: DKG Confirmation too large");
110                        return Err(SuiErrorKind::InvalidDkgMessageSize.into());
111                    }
112                }
113
114                ConsensusTransactionKind::CapabilityNotification(_) => {}
115
116                ConsensusTransactionKind::EndOfPublish(_)
117                | ConsensusTransactionKind::NewJWKFetched(_, _, _)
118                | ConsensusTransactionKind::CapabilityNotificationV2(_)
119                | ConsensusTransactionKind::RandomnessStateUpdate(_, _) => {}
120
121                ConsensusTransactionKind::UserTransaction(_) => {
122                    if epoch_store.protocol_config().address_aliases()
123                        || epoch_store.protocol_config().disable_preconsensus_locking()
124                    {
125                        return Err(SuiErrorKind::UnexpectedMessage(
126                            "ConsensusTransactionKind::UserTransaction cannot be used when address aliases is enabled or preconsensus locking is disabled".to_string(),
127                        )
128                        .into());
129                    }
130                }
131
132                ConsensusTransactionKind::UserTransactionV2(tx) => {
133                    if !(epoch_store.protocol_config().address_aliases()
134                        || epoch_store.protocol_config().disable_preconsensus_locking())
135                    {
136                        return Err(SuiErrorKind::UnexpectedMessage(
137                            "ConsensusTransactionKind::UserTransactionV2 must be used when either address aliases is enabled or preconsensus locking is disabled".to_string(),
138                        )
139                        .into());
140                    }
141                    if epoch_store.protocol_config().address_aliases() && tx.aliases().is_none() {
142                        return Err(SuiErrorKind::UnexpectedMessage(
143                            "ConsensusTransactionKind::UserTransactionV2 must contain an aliases claim".to_string(),
144                        )
145                        .into());
146                    }
147                    // TODO(fastpath): move deterministic verifications of user transactions here.
148                }
149
150                ConsensusTransactionKind::ExecutionTimeObservation(obs) => {
151                    // TODO: Use a separate limit for this that may truncate shared observations.
152                    if obs.estimates.len()
153                        > epoch_store
154                            .protocol_config()
155                            .max_programmable_tx_commands()
156                            .try_into()
157                            .unwrap()
158                    {
159                        return Err(SuiErrorKind::UnexpectedMessage(format!(
160                            "ExecutionTimeObservation contains too many estimates: {}",
161                            obs.estimates.len()
162                        ))
163                        .into());
164                    }
165                }
166            }
167        }
168
169        // verify the certificate signatures as a batch
170        let cert_count = cert_batch.len();
171        let ckpt_count = ckpt_batch.len();
172
173        epoch_store
174            .signature_verifier
175            .verify_certs_and_checkpoints(cert_batch, ckpt_batch)
176            .tap_err(|e| warn!("batch verification error: {}", e))?;
177
178        // All checkpoint sigs have been verified, forward them to the checkpoint service
179        for ckpt in ckpt_messages {
180            self.checkpoint_service
181                .notify_checkpoint_signature(&epoch_store, ckpt)?;
182        }
183
184        self.metrics
185            .certificate_signatures_verified
186            .inc_by(cert_count as u64);
187        self.metrics
188            .checkpoint_signatures_verified
189            .inc_by(ckpt_count as u64);
190        Ok(())
191    }
192
193    #[instrument(level = "debug", skip_all, fields(block_ref))]
194    fn vote_transactions(
195        &self,
196        block_ref: &BlockRef,
197        txs: Vec<ConsensusTransactionKind>,
198    ) -> Vec<TransactionIndex> {
199        let epoch_store = self.epoch_store.clone();
200        if !epoch_store.protocol_config().mysticeti_fastpath() {
201            return vec![];
202        }
203
204        let mut reject_txn_votes = Vec::new();
205        for (i, tx) in txs.into_iter().enumerate() {
206            let tx: PlainTransactionWithClaims = match tx {
207                ConsensusTransactionKind::UserTransaction(tx) => {
208                    TransactionWithClaims::no_aliases(*tx)
209                }
210                ConsensusTransactionKind::UserTransactionV2(tx) => *tx,
211                _ => continue,
212            };
213
214            let tx_digest = *tx.tx().digest();
215            if let Err(error) = self.vote_transaction(&epoch_store, tx) {
216                debug!(?tx_digest, "Voting to reject transaction: {error}");
217                self.metrics
218                    .transaction_reject_votes
219                    .with_label_values(&[error.to_variant_name()])
220                    .inc();
221                reject_txn_votes.push(i as TransactionIndex);
222                // Cache the rejection vote reason (error) for the transaction
223                epoch_store.set_rejection_vote_reason(
224                    ConsensusPosition {
225                        epoch: epoch_store.epoch(),
226                        block: *block_ref,
227                        index: i as TransactionIndex,
228                    },
229                    &error,
230                );
231            } else {
232                debug!(?tx_digest, "Voting to accept transaction");
233            }
234        }
235
236        reject_txn_votes
237    }
238
239    #[instrument(level = "debug", skip_all, err(level = "debug"), fields(tx_digest = ?tx.tx().digest()))]
240    fn vote_transaction(
241        &self,
242        epoch_store: &Arc<AuthorityPerEpochStore>,
243        tx: PlainTransactionWithClaims,
244    ) -> SuiResult<()> {
245        // Extract claims before consuming the transaction
246        let aliases = tx.aliases();
247        let claimed_immutable_ids = tx.get_immutable_objects();
248        let inner_tx = tx.into_tx();
249
250        // Currently validity_check() and verify_transaction() are not required to be consistent across validators,
251        // so they do not run in validate_transactions(). They can run there once we confirm it is safe.
252        inner_tx.validity_check(&epoch_store.tx_validity_check_context())?;
253
254        self.authority_state.check_system_overload(
255            &*self.consensus_overload_checker,
256            inner_tx.data(),
257            self.authority_state.check_system_overload_at_signing(),
258        )?;
259
260        #[allow(unused_mut)]
261        let mut fail_point_always_report_aliases_changed = false;
262        fail_point_arg!(
263            "consensus-validator-always-report-aliases-changed",
264            |for_validators: Vec<AuthorityName>| {
265                if for_validators.contains(&self.authority_state.name) {
266                    // always report aliases changed in simtests
267                    fail_point_always_report_aliases_changed = true;
268                }
269            }
270        );
271
272        let verified_tx = epoch_store.verify_transaction_with_current_aliases(inner_tx)?;
273
274        // aliases must have data when address_aliases() is enabled.
275        if epoch_store.protocol_config().address_aliases()
276            && (*verified_tx.aliases() != aliases.unwrap()
277                || fail_point_always_report_aliases_changed)
278        {
279            return Err(SuiErrorKind::AliasesChanged.into());
280        }
281
282        let inner_tx = verified_tx.into_tx();
283        self.authority_state
284            .handle_vote_transaction(epoch_store, inner_tx.clone())?;
285
286        if epoch_store.protocol_config().disable_preconsensus_locking()
287            && !claimed_immutable_ids.is_empty()
288        {
289            let owned_object_refs: HashSet<ObjectRef> = inner_tx
290                .data()
291                .transaction_data()
292                .input_objects()?
293                .iter()
294                .filter_map(|obj| match obj {
295                    InputObjectKind::ImmOrOwnedMoveObject(obj_ref) => Some(*obj_ref),
296                    _ => None,
297                })
298                .collect();
299            self.verify_immutable_object_claims(&claimed_immutable_ids, owned_object_refs)?;
300        }
301
302        Ok(())
303    }
304
305    /// Verify that all claimed immutable objects are actually immutable.
306    /// Rejects if any claimed object doesn't exist locally (can't verify) or is not immutable.
307    /// This is stricter than general voting because the claim directly controls locking behavior.
308    fn verify_immutable_object_claims(
309        &self,
310        claimed_ids: &[ObjectID],
311        owned_object_refs: HashSet<ObjectRef>,
312    ) -> SuiResult<()> {
313        if claimed_ids.is_empty() {
314            return Ok(());
315        }
316
317        let objects = self
318            .authority_state
319            .get_object_cache_reader()
320            .get_objects(claimed_ids);
321
322        for (obj, id) in objects.into_iter().zip(claimed_ids.iter()) {
323            match obj {
324                Some(o) => {
325                    // Object exists but is NOT immutable - invalid claim
326                    let object_ref = o.compute_object_reference();
327                    if !owned_object_refs.contains(&o.compute_object_reference()) {
328                        return Err(SuiErrorKind::ImmutableObjectClaimNotFoundInInput {
329                            object_id: *id,
330                        }
331                        .into());
332                    }
333                    if !o.is_immutable() {
334                        return Err(SuiErrorKind::InvalidImmutableObjectClaim {
335                            claimed_object_id: *id,
336                            found_object_ref: object_ref,
337                        }
338                        .into());
339                    }
340                }
341                None => {
342                    // Object not found - we can't verify the claim, so we must reject.
343                    // This branch should not happen because owned input objects are already validated to exist.
344                    return Err(SuiErrorKind::UserInputError {
345                        error: UserInputError::ObjectNotFound {
346                            object_id: *id,
347                            version: None,
348                        },
349                    }
350                    .into());
351                }
352            }
353        }
354
355        Ok(())
356    }
357}
358
359fn tx_kind_from_bytes(tx: &[u8]) -> Result<ConsensusTransactionKind, ValidationError> {
360    bcs::from_bytes::<ConsensusTransaction>(tx)
361        .map_err(|e| {
362            ValidationError::InvalidTransaction(format!(
363                "Failed to parse transaction bytes: {:?}",
364                e
365            ))
366        })
367        .map(|tx| tx.kind)
368}
369
370impl TransactionVerifier for SuiTxValidator {
371    fn verify_batch(&self, batch: &[&[u8]]) -> Result<(), ValidationError> {
372        let _scope = monitored_scope("ValidateBatch");
373
374        let txs: Vec<_> = batch
375            .iter()
376            .map(|tx| tx_kind_from_bytes(tx))
377            .collect::<Result<Vec<_>, _>>()?;
378
379        self.validate_transactions(&txs)
380            .map_err(|e| ValidationError::InvalidTransaction(e.to_string()))
381    }
382
383    fn verify_and_vote_batch(
384        &self,
385        block_ref: &BlockRef,
386        batch: &[&[u8]],
387    ) -> Result<Vec<TransactionIndex>, ValidationError> {
388        let _scope = monitored_scope("VerifyAndVoteBatch");
389
390        let txs: Vec<_> = batch
391            .iter()
392            .map(|tx| tx_kind_from_bytes(tx))
393            .collect::<Result<Vec<_>, _>>()?;
394
395        self.validate_transactions(&txs)
396            .map_err(|e| ValidationError::InvalidTransaction(e.to_string()))?;
397
398        Ok(self.vote_transactions(block_ref, txs))
399    }
400}
401
402pub struct SuiTxValidatorMetrics {
403    certificate_signatures_verified: IntCounter,
404    checkpoint_signatures_verified: IntCounter,
405    transaction_reject_votes: IntCounterVec,
406}
407
408impl SuiTxValidatorMetrics {
409    pub fn new(registry: &Registry) -> Arc<Self> {
410        Arc::new(Self {
411            certificate_signatures_verified: register_int_counter_with_registry!(
412                "tx_validator_certificate_signatures_verified",
413                "Number of certificates verified in consensus batch verifier",
414                registry
415            )
416            .unwrap(),
417            checkpoint_signatures_verified: register_int_counter_with_registry!(
418                "tx_validator_checkpoint_signatures_verified",
419                "Number of checkpoint verified in consensus batch verifier",
420                registry
421            )
422            .unwrap(),
423            transaction_reject_votes: register_int_counter_vec_with_registry!(
424                "tx_validator_transaction_reject_votes",
425                "Number of reject transaction votes per reason",
426                &["reason"],
427                registry
428            )
429            .unwrap(),
430        })
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use std::num::NonZeroUsize;
437    use std::sync::Arc;
438
439    use consensus_core::TransactionVerifier as _;
440    use consensus_types::block::BlockRef;
441    use fastcrypto::traits::KeyPair;
442    use sui_config::transaction_deny_config::TransactionDenyConfigBuilder;
443    use sui_macros::sim_test;
444    use sui_protocol_config::{Chain, ProtocolConfig, ProtocolVersion};
445    use sui_types::crypto::deterministic_random_account_key;
446    use sui_types::error::{SuiErrorKind, UserInputError};
447    use sui_types::executable_transaction::VerifiedExecutableTransaction;
448    use sui_types::messages_checkpoint::{
449        CheckpointContents, CheckpointSignatureMessage, CheckpointSummary, SignedCheckpointSummary,
450    };
451    use sui_types::messages_consensus::ConsensusPosition;
452    use sui_types::{
453        base_types::{ExecutionDigests, ObjectID},
454        crypto::Ed25519SuiSignature,
455        effects::TransactionEffectsAPI as _,
456        messages_consensus::ConsensusTransaction,
457        object::Object,
458        signature::GenericSignature,
459        transaction::{PlainTransactionWithClaims, Transaction},
460    };
461
462    use crate::authority::ExecutionEnv;
463    use crate::{
464        authority::test_authority_builder::TestAuthorityBuilder,
465        checkpoints::CheckpointServiceNoop,
466        consensus_adapter::consensus_tests::{
467            test_gas_objects, test_user_transaction, test_user_transactions,
468        },
469        consensus_validator::{SuiTxValidator, SuiTxValidatorMetrics},
470    };
471
472    #[sim_test]
473    async fn accept_valid_transaction() {
474        // Initialize an authority with a (owned) gas object and a shared object.
475        let mut objects = test_gas_objects();
476        let shared_object = Object::shared_for_testing();
477        objects.push(shared_object.clone());
478
479        let network_config =
480            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
481                .with_objects(objects.clone())
482                .build();
483
484        let state = TestAuthorityBuilder::new()
485            .with_network_config(&network_config, 0)
486            .build()
487            .await;
488        let name1 = state.name;
489        let transactions = test_user_transactions(&state, shared_object).await;
490
491        let first_transaction = transactions[0].clone();
492        let first_transaction_bytes: Vec<u8> =
493            bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
494                &name1,
495                first_transaction.into(),
496            ))
497            .unwrap();
498
499        let metrics = SuiTxValidatorMetrics::new(&Default::default());
500        let validator = SuiTxValidator::new(
501            state.clone(),
502            state.epoch_store_for_testing().clone(),
503            Arc::new(CheckpointServiceNoop {}),
504            metrics,
505        );
506        let res = validator.verify_batch(&[&first_transaction_bytes]);
507        assert!(res.is_ok(), "{res:?}");
508
509        let transaction_bytes: Vec<_> = transactions
510            .clone()
511            .into_iter()
512            .map(|tx| {
513                bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
514                    &name1,
515                    tx.into(),
516                ))
517                .unwrap()
518            })
519            .collect();
520
521        let batch: Vec<_> = transaction_bytes.iter().map(|t| t.as_slice()).collect();
522        let res_batch = validator.verify_batch(&batch);
523        assert!(res_batch.is_ok(), "{res_batch:?}");
524
525        let bogus_transaction_bytes: Vec<_> = transactions
526            .into_iter()
527            .map(|tx| {
528                // Create a transaction with an invalid signature
529                let aliases = tx.aliases().clone();
530                let mut signed_tx: Transaction = tx.into_tx().into();
531                signed_tx.tx_signatures_mut_for_testing()[0] =
532                    GenericSignature::Signature(sui_types::crypto::Signature::Ed25519SuiSignature(
533                        Ed25519SuiSignature::default(),
534                    ));
535                let tx_with_claims = PlainTransactionWithClaims::from_aliases(signed_tx, aliases);
536                bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
537                    &name1,
538                    tx_with_claims,
539                ))
540                .unwrap()
541            })
542            .collect();
543
544        let batch: Vec<_> = bogus_transaction_bytes
545            .iter()
546            .map(|t| t.as_slice())
547            .collect();
548        // verify_batch doesn't verify user transaction signatures (that happens in vote_transaction).
549        // Use verify_and_vote_batch to test that bogus transactions are rejected during voting.
550        let res_batch = validator.verify_and_vote_batch(&BlockRef::MIN, &batch);
551        assert!(res_batch.is_ok());
552        // All transactions should be in the rejection list since they have invalid signatures
553        let rejections = res_batch.unwrap();
554        assert_eq!(
555            rejections.len(),
556            batch.len(),
557            "All bogus transactions should be rejected"
558        );
559    }
560
561    #[tokio::test]
562    async fn test_verify_and_vote_batch() {
563        // 1 account keypair
564        let (sender, keypair) = deterministic_random_account_key();
565
566        // 8 gas objects.
567        let gas_objects: Vec<Object> = (0..8)
568            .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
569            .collect();
570
571        // 2 owned objects.
572        let owned_objects: Vec<Object> = (0..2)
573            .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
574            .collect();
575        let denied_object = owned_objects[1].clone();
576
577        let mut objects = gas_objects.clone();
578        objects.extend(owned_objects.clone());
579
580        let network_config =
581            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
582                .committee_size(NonZeroUsize::new(1).unwrap())
583                .with_objects(objects.clone())
584                .build();
585
586        // 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.
587        let transaction_deny_config = TransactionDenyConfigBuilder::new()
588            .add_denied_object(denied_object.id())
589            .build();
590        let state = TestAuthorityBuilder::new()
591            .with_network_config(&network_config, 0)
592            .with_transaction_deny_config(transaction_deny_config)
593            .build()
594            .await;
595
596        // Create two user transactions
597
598        // A valid transaction
599        let valid_transaction = test_user_transaction(
600            &state,
601            sender,
602            &keypair,
603            gas_objects[0].clone(),
604            vec![owned_objects[0].clone()],
605        )
606        .await;
607
608        // An invalid transaction where the input object is denied
609        let invalid_transaction = test_user_transaction(
610            &state,
611            sender,
612            &keypair,
613            gas_objects[1].clone(),
614            vec![denied_object.clone()],
615        )
616        .await;
617
618        // Now create the vector with the transactions and serialize them.
619        let transactions = vec![valid_transaction, invalid_transaction];
620        let serialized_transactions: Vec<_> = transactions
621            .into_iter()
622            .map(|t| {
623                bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
624                    &state.name,
625                    t.into(),
626                ))
627                .unwrap()
628            })
629            .collect();
630        let batch: Vec<_> = serialized_transactions
631            .iter()
632            .map(|t| t.as_slice())
633            .collect();
634
635        let validator = SuiTxValidator::new(
636            state.clone(),
637            state.epoch_store_for_testing().clone(),
638            Arc::new(CheckpointServiceNoop {}),
639            SuiTxValidatorMetrics::new(&Default::default()),
640        );
641
642        // WHEN
643        let rejected_transactions = validator
644            .verify_and_vote_batch(&BlockRef::MAX, &batch)
645            .unwrap();
646
647        // THEN
648        // The 2nd transaction should be rejected
649        assert_eq!(rejected_transactions, vec![1]);
650
651        // AND
652        // The reject reason should get cached
653        let epoch_store = state.load_epoch_store_one_call_per_task();
654        let reason = epoch_store
655            .get_rejection_vote_reason(ConsensusPosition {
656                epoch: state.load_epoch_store_one_call_per_task().epoch(),
657                block: BlockRef::MAX,
658                index: 1,
659            })
660            .expect("Rejection vote reason should be set");
661
662        assert_eq!(
663            reason,
664            SuiErrorKind::UserInputError {
665                error: UserInputError::TransactionDenied {
666                    error: format!(
667                        "Access to input object {:?} is temporarily disabled",
668                        denied_object.id()
669                    )
670                }
671            }
672        );
673    }
674
675    #[sim_test]
676    async fn reject_checkpoint_signature_v2_when_flag_disabled() {
677        // Build a single-validator network and authority with protocol version < 93 (flag disabled)
678        let network_config =
679            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir().build();
680
681        let disabled_cfg =
682            ProtocolConfig::get_for_version(ProtocolVersion::new(92), Chain::Unknown);
683        let state = TestAuthorityBuilder::new()
684            .with_network_config(&network_config, 0)
685            .with_protocol_config(disabled_cfg)
686            .build()
687            .await;
688
689        let epoch_store = state.load_epoch_store_one_call_per_task();
690
691        // Create a minimal checkpoint summary and sign it with the validator's protocol key
692        let checkpoint_summary = CheckpointSummary::new(
693            &ProtocolConfig::get_for_max_version_UNSAFE(),
694            epoch_store.epoch(),
695            0,
696            0,
697            &CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]),
698            None,
699            Default::default(),
700            None,
701            0,
702            Vec::new(),
703            Vec::new(),
704        );
705
706        let keypair = network_config.validator_configs()[0].protocol_key_pair();
707        let authority = keypair.public().into();
708        let signed = SignedCheckpointSummary::new(
709            epoch_store.epoch(),
710            checkpoint_summary,
711            keypair,
712            authority,
713        );
714        let message = CheckpointSignatureMessage { summary: signed };
715
716        let tx = ConsensusTransaction::new_checkpoint_signature_message_v2(message);
717        let bytes = bcs::to_bytes(&tx).unwrap();
718
719        let validator = SuiTxValidator::new(
720            state.clone(),
721            state.epoch_store_for_testing().clone(),
722            Arc::new(CheckpointServiceNoop {}),
723            SuiTxValidatorMetrics::new(&Default::default()),
724        );
725
726        let res = validator.verify_batch(&[&bytes]);
727        assert!(res.is_err());
728    }
729
730    #[sim_test]
731    async fn accept_checkpoint_signature_v2_when_flag_enabled() {
732        // Build a single-validator network and authority with protocol version >= 93 (flag enabled)
733        let network_config =
734            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir().build();
735
736        let enabled_cfg = ProtocolConfig::get_for_version(ProtocolVersion::new(93), Chain::Unknown);
737        let state = TestAuthorityBuilder::new()
738            .with_network_config(&network_config, 0)
739            .with_protocol_config(enabled_cfg)
740            .build()
741            .await;
742
743        let epoch_store = state.load_epoch_store_one_call_per_task();
744
745        // Create a minimal checkpoint summary and sign it with the validator's protocol key
746        let checkpoint_summary = CheckpointSummary::new(
747            &ProtocolConfig::get_for_max_version_UNSAFE(),
748            epoch_store.epoch(),
749            0,
750            0,
751            &CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]),
752            None,
753            Default::default(),
754            None,
755            0,
756            Vec::new(),
757            Vec::new(),
758        );
759
760        let keypair = network_config.validator_configs()[0].protocol_key_pair();
761        let authority = keypair.public().into();
762        let signed = SignedCheckpointSummary::new(
763            epoch_store.epoch(),
764            checkpoint_summary,
765            keypair,
766            authority,
767        );
768        let message = CheckpointSignatureMessage { summary: signed };
769
770        let tx = ConsensusTransaction::new_checkpoint_signature_message_v2(message);
771        let bytes = bcs::to_bytes(&tx).unwrap();
772
773        let validator = SuiTxValidator::new(
774            state.clone(),
775            state.epoch_store_for_testing().clone(),
776            Arc::new(CheckpointServiceNoop {}),
777            SuiTxValidatorMetrics::new(&Default::default()),
778        );
779
780        let res = validator.verify_batch(&[&bytes]);
781        assert!(res.is_ok(), "{res:?}");
782    }
783
784    #[sim_test]
785    async fn accept_already_executed_transaction() {
786        // This test uses ConsensusTransaction::new_user_transaction_message which creates a
787        // UserTransaction. When disable_preconsensus_locking=true (protocol version 105+),
788        // UserTransaction is not allowed. Gate with disable_preconsensus_locking=false.
789        let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
790            config.set_disable_preconsensus_locking_for_testing(false);
791            config
792        });
793
794        let (sender, keypair) = deterministic_random_account_key();
795
796        let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
797        let owned_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
798
799        let network_config =
800            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
801                .committee_size(NonZeroUsize::new(1).unwrap())
802                .with_objects(vec![gas_object.clone(), owned_object.clone()])
803                .build();
804
805        let state = TestAuthorityBuilder::new()
806            .with_network_config(&network_config, 0)
807            .build()
808            .await;
809
810        let epoch_store = state.load_epoch_store_one_call_per_task();
811
812        // Create a transaction and execute it.
813        let transaction = test_user_transaction(
814            &state,
815            sender,
816            &keypair,
817            gas_object.clone(),
818            vec![owned_object.clone()],
819        )
820        .await
821        .into_tx();
822        let tx_digest = *transaction.digest();
823        let cert = VerifiedExecutableTransaction::new_from_consensus(transaction.clone(), 0);
824        let (executed_effects, _) = state
825            .try_execute_immediately(&cert, ExecutionEnv::new(), &state.epoch_store_for_testing())
826            .await
827            .unwrap();
828
829        // Verify the transaction is executed.
830        let read_effects = state
831            .get_transaction_cache_reader()
832            .get_executed_effects(&tx_digest)
833            .expect("Transaction should be executed");
834        assert_eq!(read_effects, executed_effects);
835        assert_eq!(read_effects.executed_epoch(), epoch_store.epoch());
836
837        // Now try to vote on the already executed transaction
838        let serialized_tx = bcs::to_bytes(&ConsensusTransaction::new_user_transaction_message(
839            &state.name,
840            transaction.into_inner().clone(),
841        ))
842        .unwrap();
843        let validator = SuiTxValidator::new(
844            state.clone(),
845            state.epoch_store_for_testing().clone(),
846            Arc::new(CheckpointServiceNoop {}),
847            SuiTxValidatorMetrics::new(&Default::default()),
848        );
849        let rejected_transactions = validator
850            .verify_and_vote_batch(&BlockRef::MAX, &[&serialized_tx])
851            .expect("Verify and vote should succeed");
852
853        // The executed transaction should NOT be rejected.
854        assert!(rejected_transactions.is_empty());
855    }
856}