sui_core/
consensus_validator.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    collections::{BTreeSet, HashMap, HashSet},
6    sync::Arc,
7};
8
9use consensus_core::{TransactionVerifier, ValidationError};
10use consensus_types::block::{BlockRef, TransactionIndex};
11use fastcrypto_tbls::dkg_v1;
12use mysten_metrics::monitored_scope;
13use prometheus::{
14    IntCounter, IntCounterVec, Registry, register_int_counter_vec_with_registry,
15    register_int_counter_with_registry,
16};
17use sui_macros::fail_point_arg;
18#[cfg(msim)]
19use sui_types::base_types::AuthorityName;
20use sui_types::{
21    base_types::{ObjectID, ObjectRef},
22    error::{SuiError, SuiErrorKind, SuiResult, UserInputError},
23    messages_consensus::{ConsensusPosition, ConsensusTransaction, ConsensusTransactionKind},
24    transaction::{
25        InputObjectKind, PlainTransactionWithClaims, TransactionDataAPI, TransactionWithClaims,
26    },
27};
28use tap::TapFallible;
29use tracing::{debug, info, instrument, warn};
30
31use crate::{
32    authority::{AuthorityState, authority_per_epoch_store::AuthorityPerEpochStore},
33    checkpoints::CheckpointServiceNotify,
34    consensus_adapter::{ConsensusOverloadChecker, NoopConsensusOverloadChecker},
35};
36
37/// Validates transactions from consensus and votes on whether to execute the transactions
38/// based on their validity and the current state of the authority.
39#[derive(Clone)]
40pub struct SuiTxValidator {
41    authority_state: Arc<AuthorityState>,
42    epoch_store: Arc<AuthorityPerEpochStore>,
43    consensus_overload_checker: Arc<dyn ConsensusOverloadChecker>,
44    checkpoint_service: Arc<dyn CheckpointServiceNotify + Send + Sync>,
45    metrics: Arc<SuiTxValidatorMetrics>,
46}
47
48impl SuiTxValidator {
49    pub fn new(
50        authority_state: Arc<AuthorityState>,
51        epoch_store: Arc<AuthorityPerEpochStore>,
52        checkpoint_service: Arc<dyn CheckpointServiceNotify + Send + Sync>,
53        metrics: Arc<SuiTxValidatorMetrics>,
54    ) -> Self {
55        info!(
56            "SuiTxValidator constructed for epoch {}",
57            epoch_store.epoch()
58        );
59        // Intentionally do not check consensus overload, because this is validating transactions already in consensus.
60        let consensus_overload_checker = Arc::new(NoopConsensusOverloadChecker {});
61        Self {
62            authority_state,
63            epoch_store,
64            consensus_overload_checker,
65            checkpoint_service,
66            metrics,
67        }
68    }
69
70    fn validate_transactions(&self, txs: &[ConsensusTransactionKind]) -> Result<(), SuiError> {
71        let epoch_store = self.epoch_store.clone();
72        let mut cert_batch = Vec::new();
73        let mut ckpt_messages = Vec::new();
74        let mut ckpt_batch = Vec::new();
75        for tx in txs.iter() {
76            match tx {
77                ConsensusTransactionKind::CertifiedTransaction(certificate) => {
78                    if epoch_store.protocol_config().disable_preconsensus_locking() {
79                        return Err(SuiErrorKind::UnexpectedMessage(
80                            "CertifiedTransaction cannot be used when preconsensus locking is disabled".to_string(),
81                        )
82                        .into());
83                    }
84                    cert_batch.push(certificate.as_ref());
85                }
86                ConsensusTransactionKind::CheckpointSignature(_) => {
87                    return Err(SuiErrorKind::UnexpectedMessage(
88                        "CheckpointSignature V1 is no longer supported".to_string(),
89                    )
90                    .into());
91                }
92                ConsensusTransactionKind::CheckpointSignatureV2(signature) => {
93                    ckpt_messages.push(signature.as_ref());
94                    ckpt_batch.push(&signature.summary);
95                }
96                ConsensusTransactionKind::RandomnessDkgMessage(_, bytes) => {
97                    if bytes.len() > dkg_v1::DKG_MESSAGES_MAX_SIZE {
98                        warn!("batch verification error: DKG Message too large");
99                        return Err(SuiErrorKind::InvalidDkgMessageSize.into());
100                    }
101                }
102                ConsensusTransactionKind::RandomnessDkgConfirmation(_, bytes) => {
103                    if bytes.len() > dkg_v1::DKG_MESSAGES_MAX_SIZE {
104                        warn!("batch verification error: DKG Confirmation too large");
105                        return Err(SuiErrorKind::InvalidDkgMessageSize.into());
106                    }
107                }
108
109                ConsensusTransactionKind::CapabilityNotification(_) => {
110                    return Err(SuiErrorKind::UnexpectedMessage(
111                        "CapabilityNotification V1 is no longer supported".to_string(),
112                    )
113                    .into());
114                }
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 immutable object claims are complete and accurate.
306    /// This ensures claimed_ids exactly matches the set of immutable objects in owned_object_refs.
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        // Build map from object_id to input ref for version/digest verification
314        let input_refs_by_id: HashMap<ObjectID, ObjectRef> = owned_object_refs
315            .iter()
316            .map(|obj_ref| (obj_ref.0, *obj_ref))
317            .collect();
318
319        // First check: all claimed object IDs must be among the input object IDs
320        for claimed_id in claimed_ids {
321            if !input_refs_by_id.contains_key(claimed_id) {
322                return Err(SuiErrorKind::ImmutableObjectClaimNotFoundInInput {
323                    object_id: *claimed_id,
324                }
325                .into());
326            }
327        }
328
329        // Fetch all input objects and collect the actual immutable ones,
330        // verifying existence and version/digest match
331        let input_ids: Vec<ObjectID> = input_refs_by_id.keys().copied().collect();
332        let objects = self
333            .authority_state
334            .get_object_cache_reader()
335            .get_objects(&input_ids);
336
337        let claimed_immutable_ids = claimed_ids.iter().cloned().collect::<BTreeSet<_>>();
338        let mut found_immutable_ids = BTreeSet::new();
339
340        for (obj_opt, object_id) in objects.into_iter().zip(input_ids.iter()) {
341            let input_ref = input_refs_by_id.get(object_id).unwrap();
342            match obj_opt {
343                Some(o) => {
344                    // The object read here might drift from the one read earlier in validate_owned_object_versions(),
345                    // so re-check if input reference still matches actual object.
346                    let actual_ref = o.compute_object_reference();
347                    if actual_ref != *input_ref {
348                        return Err(SuiErrorKind::UserInputError {
349                            error: UserInputError::ObjectVersionUnavailableForConsumption {
350                                provided_obj_ref: *input_ref,
351                                current_version: actual_ref.1,
352                            },
353                        }
354                        .into());
355                    }
356                    if o.is_immutable() {
357                        found_immutable_ids.insert(*object_id);
358                    }
359                }
360                None => {
361                    // Object not found - we can't verify the claim, so we must reject.
362                    // This branch should not happen because owned input objects are already validated to exist.
363                    return Err(SuiErrorKind::UserInputError {
364                        error: UserInputError::ObjectNotFound {
365                            object_id: *object_id,
366                            version: Some(input_ref.1),
367                        },
368                    }
369                    .into());
370                }
371            }
372        }
373
374        // Compare claimed_ids with actual immutable objects - must match exactly
375        if let Some(claimed_id) = claimed_immutable_ids
376            .difference(&found_immutable_ids)
377            .next()
378        {
379            let input_ref = input_refs_by_id.get(claimed_id).unwrap();
380            return Err(SuiErrorKind::InvalidImmutableObjectClaim {
381                claimed_object_id: *claimed_id,
382                found_object_ref: *input_ref,
383            }
384            .into());
385        }
386        if let Some(found_id) = found_immutable_ids
387            .difference(&claimed_immutable_ids)
388            .next()
389        {
390            return Err(SuiErrorKind::ImmutableObjectNotClaimed {
391                object_id: *found_id,
392            }
393            .into());
394        }
395
396        Ok(())
397    }
398}
399
400fn tx_kind_from_bytes(tx: &[u8]) -> Result<ConsensusTransactionKind, ValidationError> {
401    bcs::from_bytes::<ConsensusTransaction>(tx)
402        .map_err(|e| {
403            ValidationError::InvalidTransaction(format!(
404                "Failed to parse transaction bytes: {:?}",
405                e
406            ))
407        })
408        .map(|tx| tx.kind)
409}
410
411impl TransactionVerifier for SuiTxValidator {
412    fn verify_batch(&self, batch: &[&[u8]]) -> Result<(), ValidationError> {
413        let _scope = monitored_scope("ValidateBatch");
414
415        let txs: Vec<_> = batch
416            .iter()
417            .map(|tx| tx_kind_from_bytes(tx))
418            .collect::<Result<Vec<_>, _>>()?;
419
420        self.validate_transactions(&txs)
421            .map_err(|e| ValidationError::InvalidTransaction(e.to_string()))
422    }
423
424    fn verify_and_vote_batch(
425        &self,
426        block_ref: &BlockRef,
427        batch: &[&[u8]],
428    ) -> Result<Vec<TransactionIndex>, ValidationError> {
429        let _scope = monitored_scope("VerifyAndVoteBatch");
430
431        let txs: Vec<_> = batch
432            .iter()
433            .map(|tx| tx_kind_from_bytes(tx))
434            .collect::<Result<Vec<_>, _>>()?;
435
436        self.validate_transactions(&txs)
437            .map_err(|e| ValidationError::InvalidTransaction(e.to_string()))?;
438
439        Ok(self.vote_transactions(block_ref, txs))
440    }
441}
442
443pub struct SuiTxValidatorMetrics {
444    certificate_signatures_verified: IntCounter,
445    checkpoint_signatures_verified: IntCounter,
446    transaction_reject_votes: IntCounterVec,
447}
448
449impl SuiTxValidatorMetrics {
450    pub fn new(registry: &Registry) -> Arc<Self> {
451        Arc::new(Self {
452            certificate_signatures_verified: register_int_counter_with_registry!(
453                "tx_validator_certificate_signatures_verified",
454                "Number of certificates verified in consensus batch verifier",
455                registry
456            )
457            .unwrap(),
458            checkpoint_signatures_verified: register_int_counter_with_registry!(
459                "tx_validator_checkpoint_signatures_verified",
460                "Number of checkpoint verified in consensus batch verifier",
461                registry
462            )
463            .unwrap(),
464            transaction_reject_votes: register_int_counter_vec_with_registry!(
465                "tx_validator_transaction_reject_votes",
466                "Number of reject transaction votes per reason",
467                &["reason"],
468                registry
469            )
470            .unwrap(),
471        })
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use std::collections::HashSet;
478    use std::num::NonZeroUsize;
479    use std::sync::Arc;
480
481    use consensus_core::TransactionVerifier as _;
482    use consensus_types::block::BlockRef;
483    use fastcrypto::traits::KeyPair;
484    use sui_config::transaction_deny_config::TransactionDenyConfigBuilder;
485    use sui_macros::sim_test;
486    use sui_protocol_config::ProtocolConfig;
487    use sui_types::crypto::deterministic_random_account_key;
488    use sui_types::error::{SuiErrorKind, UserInputError};
489    use sui_types::executable_transaction::VerifiedExecutableTransaction;
490    use sui_types::messages_checkpoint::{
491        CheckpointContents, CheckpointSignatureMessage, CheckpointSummary, SignedCheckpointSummary,
492    };
493    use sui_types::messages_consensus::ConsensusPosition;
494    use sui_types::{
495        base_types::{ExecutionDigests, ObjectID, ObjectRef},
496        crypto::Ed25519SuiSignature,
497        effects::TransactionEffectsAPI as _,
498        messages_consensus::ConsensusTransaction,
499        object::Object,
500        signature::GenericSignature,
501        transaction::{PlainTransactionWithClaims, Transaction},
502    };
503
504    use crate::authority::ExecutionEnv;
505    use crate::{
506        authority::test_authority_builder::TestAuthorityBuilder,
507        checkpoints::CheckpointServiceNoop,
508        consensus_adapter::consensus_tests::{
509            test_gas_objects, test_user_transaction, test_user_transactions,
510        },
511        consensus_validator::{SuiTxValidator, SuiTxValidatorMetrics},
512    };
513
514    #[sim_test]
515    async fn accept_valid_transaction() {
516        // Initialize an authority with a (owned) gas object and a shared object.
517        let mut objects = test_gas_objects();
518        let shared_object = Object::shared_for_testing();
519        objects.push(shared_object.clone());
520
521        let network_config =
522            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
523                .with_objects(objects.clone())
524                .build();
525
526        let state = TestAuthorityBuilder::new()
527            .with_network_config(&network_config, 0)
528            .build()
529            .await;
530        let name1 = state.name;
531        let transactions = test_user_transactions(&state, shared_object).await;
532
533        let first_transaction = transactions[0].clone();
534        let first_transaction_bytes: Vec<u8> =
535            bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
536                &name1,
537                first_transaction.into(),
538            ))
539            .unwrap();
540
541        let metrics = SuiTxValidatorMetrics::new(&Default::default());
542        let validator = SuiTxValidator::new(
543            state.clone(),
544            state.epoch_store_for_testing().clone(),
545            Arc::new(CheckpointServiceNoop {}),
546            metrics,
547        );
548        let res = validator.verify_batch(&[&first_transaction_bytes]);
549        assert!(res.is_ok(), "{res:?}");
550
551        let transaction_bytes: Vec<_> = transactions
552            .clone()
553            .into_iter()
554            .map(|tx| {
555                bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
556                    &name1,
557                    tx.into(),
558                ))
559                .unwrap()
560            })
561            .collect();
562
563        let batch: Vec<_> = transaction_bytes.iter().map(|t| t.as_slice()).collect();
564        let res_batch = validator.verify_batch(&batch);
565        assert!(res_batch.is_ok(), "{res_batch:?}");
566
567        let bogus_transaction_bytes: Vec<_> = transactions
568            .into_iter()
569            .map(|tx| {
570                // Create a transaction with an invalid signature
571                let aliases = tx.aliases().clone();
572                let mut signed_tx: Transaction = tx.into_tx().into();
573                signed_tx.tx_signatures_mut_for_testing()[0] =
574                    GenericSignature::Signature(sui_types::crypto::Signature::Ed25519SuiSignature(
575                        Ed25519SuiSignature::default(),
576                    ));
577                let tx_with_claims = PlainTransactionWithClaims::from_aliases(signed_tx, aliases);
578                bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
579                    &name1,
580                    tx_with_claims,
581                ))
582                .unwrap()
583            })
584            .collect();
585
586        let batch: Vec<_> = bogus_transaction_bytes
587            .iter()
588            .map(|t| t.as_slice())
589            .collect();
590        // verify_batch doesn't verify user transaction signatures (that happens in vote_transaction).
591        // Use verify_and_vote_batch to test that bogus transactions are rejected during voting.
592        let res_batch = validator.verify_and_vote_batch(&BlockRef::MIN, &batch);
593        assert!(res_batch.is_ok());
594        // All transactions should be in the rejection list since they have invalid signatures
595        let rejections = res_batch.unwrap();
596        assert_eq!(
597            rejections.len(),
598            batch.len(),
599            "All bogus transactions should be rejected"
600        );
601    }
602
603    #[tokio::test]
604    async fn test_verify_and_vote_batch() {
605        // 1 account keypair
606        let (sender, keypair) = deterministic_random_account_key();
607
608        // 8 gas objects.
609        let gas_objects: Vec<Object> = (0..8)
610            .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
611            .collect();
612
613        // 2 owned objects.
614        let owned_objects: Vec<Object> = (0..2)
615            .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
616            .collect();
617        let denied_object = owned_objects[1].clone();
618
619        let mut objects = gas_objects.clone();
620        objects.extend(owned_objects.clone());
621
622        let network_config =
623            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
624                .committee_size(NonZeroUsize::new(1).unwrap())
625                .with_objects(objects.clone())
626                .build();
627
628        // 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.
629        let transaction_deny_config = TransactionDenyConfigBuilder::new()
630            .add_denied_object(denied_object.id())
631            .build();
632        let state = TestAuthorityBuilder::new()
633            .with_network_config(&network_config, 0)
634            .with_transaction_deny_config(transaction_deny_config)
635            .build()
636            .await;
637
638        // Create two user transactions
639
640        // A valid transaction
641        let valid_transaction = test_user_transaction(
642            &state,
643            sender,
644            &keypair,
645            gas_objects[0].clone(),
646            vec![owned_objects[0].clone()],
647        )
648        .await;
649
650        // An invalid transaction where the input object is denied
651        let invalid_transaction = test_user_transaction(
652            &state,
653            sender,
654            &keypair,
655            gas_objects[1].clone(),
656            vec![denied_object.clone()],
657        )
658        .await;
659
660        // Now create the vector with the transactions and serialize them.
661        let transactions = vec![valid_transaction, invalid_transaction];
662        let serialized_transactions: Vec<_> = transactions
663            .into_iter()
664            .map(|t| {
665                bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
666                    &state.name,
667                    t.into(),
668                ))
669                .unwrap()
670            })
671            .collect();
672        let batch: Vec<_> = serialized_transactions
673            .iter()
674            .map(|t| t.as_slice())
675            .collect();
676
677        let validator = SuiTxValidator::new(
678            state.clone(),
679            state.epoch_store_for_testing().clone(),
680            Arc::new(CheckpointServiceNoop {}),
681            SuiTxValidatorMetrics::new(&Default::default()),
682        );
683
684        // WHEN
685        let rejected_transactions = validator
686            .verify_and_vote_batch(&BlockRef::MAX, &batch)
687            .unwrap();
688
689        // THEN
690        // The 2nd transaction should be rejected
691        assert_eq!(rejected_transactions, vec![1]);
692
693        // AND
694        // The reject reason should get cached
695        let epoch_store = state.load_epoch_store_one_call_per_task();
696        let reason = epoch_store
697            .get_rejection_vote_reason(ConsensusPosition {
698                epoch: state.load_epoch_store_one_call_per_task().epoch(),
699                block: BlockRef::MAX,
700                index: 1,
701            })
702            .expect("Rejection vote reason should be set");
703
704        assert_eq!(
705            reason,
706            SuiErrorKind::UserInputError {
707                error: UserInputError::TransactionDenied {
708                    error: format!(
709                        "Access to input object {:?} is temporarily disabled",
710                        denied_object.id()
711                    )
712                }
713            }
714        );
715    }
716
717    #[sim_test]
718    async fn accept_checkpoint_signature_v2() {
719        let network_config =
720            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir().build();
721
722        let state = TestAuthorityBuilder::new()
723            .with_network_config(&network_config, 0)
724            .build()
725            .await;
726
727        let epoch_store = state.load_epoch_store_one_call_per_task();
728
729        // Create a minimal checkpoint summary and sign it with the validator's protocol key
730        let checkpoint_summary = CheckpointSummary::new(
731            &ProtocolConfig::get_for_max_version_UNSAFE(),
732            epoch_store.epoch(),
733            0,
734            0,
735            &CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]),
736            None,
737            Default::default(),
738            None,
739            0,
740            Vec::new(),
741            Vec::new(),
742        );
743
744        let keypair = network_config.validator_configs()[0].protocol_key_pair();
745        let authority = keypair.public().into();
746        let signed = SignedCheckpointSummary::new(
747            epoch_store.epoch(),
748            checkpoint_summary,
749            keypair,
750            authority,
751        );
752        let message = CheckpointSignatureMessage { summary: signed };
753
754        let tx = ConsensusTransaction::new_checkpoint_signature_message_v2(message);
755        let bytes = bcs::to_bytes(&tx).unwrap();
756
757        let validator = SuiTxValidator::new(
758            state.clone(),
759            state.epoch_store_for_testing().clone(),
760            Arc::new(CheckpointServiceNoop {}),
761            SuiTxValidatorMetrics::new(&Default::default()),
762        );
763
764        let res = validator.verify_batch(&[&bytes]);
765        assert!(res.is_ok(), "{res:?}");
766    }
767
768    #[sim_test]
769    async fn test_verify_immutable_object_claims() {
770        let (sender, _keypair) = deterministic_random_account_key();
771
772        // Create owned objects
773        let owned_object1 = Object::with_id_owner_for_testing(ObjectID::random(), sender);
774        let owned_object2 = Object::with_id_owner_for_testing(ObjectID::random(), sender);
775
776        // Create immutable objects
777        let immutable_object1 = Object::immutable_with_id_for_testing(ObjectID::random());
778        let immutable_object2 = Object::immutable_with_id_for_testing(ObjectID::random());
779
780        // Save IDs before moving objects
781        let owned_id1 = owned_object1.id();
782        let owned_id2 = owned_object2.id();
783        let immutable_id1 = immutable_object1.id();
784        let immutable_id2 = immutable_object2.id();
785
786        let all_objects = vec![
787            owned_object1,
788            owned_object2,
789            immutable_object1,
790            immutable_object2,
791        ];
792
793        let network_config =
794            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
795                .committee_size(NonZeroUsize::new(1).unwrap())
796                .with_objects(all_objects)
797                .build();
798
799        let state = TestAuthorityBuilder::new()
800            .with_network_config(&network_config, 0)
801            .build()
802            .await;
803
804        // Retrieve actual object references from the state (as they are after genesis)
805        let cache_reader = state.get_object_cache_reader();
806        let owned_ref1 = cache_reader
807            .get_object(&owned_id1)
808            .expect("owned_id1 not found")
809            .compute_object_reference();
810        let owned_ref2 = cache_reader
811            .get_object(&owned_id2)
812            .expect("owned_id2 not found")
813            .compute_object_reference();
814        let immutable_ref1 = cache_reader
815            .get_object(&immutable_id1)
816            .expect("immutable_id1 not found")
817            .compute_object_reference();
818        let immutable_ref2 = cache_reader
819            .get_object(&immutable_id2)
820            .expect("immutable_id2 not found")
821            .compute_object_reference();
822
823        let validator = SuiTxValidator::new(
824            state.clone(),
825            state.epoch_store_for_testing().clone(),
826            Arc::new(CheckpointServiceNoop {}),
827            SuiTxValidatorMetrics::new(&Default::default()),
828        );
829
830        // Test 1: Empty claims with no immutable objects in inputs - should pass
831        {
832            let owned_refs: HashSet<ObjectRef> = [owned_ref1, owned_ref2].into_iter().collect();
833
834            let result = validator.verify_immutable_object_claims(&[], owned_refs);
835            assert!(
836                result.is_ok(),
837                "Empty claims with only owned objects should pass, got error: {:?}",
838                result.err()
839            );
840        }
841
842        // Test 2: Correct claims - immutable objects properly claimed - should pass
843        {
844            let refs: HashSet<ObjectRef> = [owned_ref1, immutable_ref1].into_iter().collect();
845
846            let claimed_ids = vec![immutable_id1];
847            let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
848            assert!(result.is_ok(), "Correct immutable object claim should pass");
849        }
850
851        // Test 3: Multiple correct claims - should pass
852        {
853            let refs: HashSet<ObjectRef> = [owned_ref1, immutable_ref1, immutable_ref2]
854                .into_iter()
855                .collect();
856
857            let claimed_ids = vec![immutable_id1, immutable_id2];
858            let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
859            assert!(
860                result.is_ok(),
861                "Multiple correct immutable claims should pass"
862            );
863        }
864
865        // Test 4: Missing claim - immutable object not claimed - should fail
866        {
867            let refs: HashSet<ObjectRef> = [owned_ref1, immutable_ref1].into_iter().collect();
868
869            let claimed_ids: Vec<ObjectID> = vec![];
870            let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
871            assert!(result.is_err(), "Missing immutable claim should fail");
872
873            let err = result.unwrap_err();
874            assert!(
875                matches!(
876                    err.as_inner(),
877                    SuiErrorKind::ImmutableObjectNotClaimed { object_id }
878                    if *object_id == immutable_id1
879                ),
880                "Expected ImmutableObjectNotClaimed error, got: {:?}",
881                err.as_inner()
882            );
883        }
884
885        // Test 5: False claim - owned object claimed as immutable - should fail
886        {
887            let refs: HashSet<ObjectRef> = [owned_ref1, owned_ref2].into_iter().collect();
888
889            let claimed_ids = vec![owned_id1];
890            let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
891            assert!(
892                result.is_err(),
893                "False immutable claim on owned object should fail"
894            );
895
896            let err = result.unwrap_err();
897            assert!(
898                matches!(
899                    err.as_inner(),
900                    SuiErrorKind::InvalidImmutableObjectClaim { claimed_object_id, .. }
901                    if *claimed_object_id == owned_id1
902                ),
903                "Expected InvalidImmutableObjectClaim error, got: {:?}",
904                err.as_inner()
905            );
906        }
907
908        // Test 6: Claim not in inputs - should fail
909        {
910            let refs: HashSet<ObjectRef> = [owned_ref1, owned_ref2].into_iter().collect();
911
912            let claimed_ids = vec![immutable_id1];
913            let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
914            assert!(result.is_err(), "Claim not in inputs should fail");
915
916            let err = result.unwrap_err();
917            assert!(
918                matches!(
919                    err.as_inner(),
920                    SuiErrorKind::ImmutableObjectClaimNotFoundInInput { object_id }
921                    if *object_id == immutable_id1
922                ),
923                "Expected ImmutableObjectClaimNotFoundInInput error, got: {:?}",
924                err.as_inner()
925            );
926        }
927
928        // Test 7: Object not found (non-existent object) - should fail
929        {
930            let non_existent_id = ObjectID::random();
931            let fake_ref = (
932                non_existent_id,
933                sui_types::base_types::SequenceNumber::new(),
934                sui_types::digests::ObjectDigest::random(),
935            );
936            let refs: HashSet<ObjectRef> = [owned_ref1, fake_ref].into_iter().collect();
937
938            let claimed_ids: Vec<ObjectID> = vec![];
939            let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
940            assert!(result.is_err(), "Non-existent object should fail");
941
942            let err = result.unwrap_err();
943            assert!(
944                matches!(
945                    err.as_inner(),
946                    SuiErrorKind::UserInputError { error: UserInputError::ObjectNotFound { object_id, .. } }
947                    if *object_id == non_existent_id
948                ),
949                "Expected ObjectNotFound error, got: {:?}",
950                err.as_inner()
951            );
952        }
953
954        // Test 8: Version/digest mismatch for immutable object - should fail
955        {
956            // Use a wrong version for the immutable object
957            let wrong_version_ref = (
958                immutable_ref1.0,
959                sui_types::base_types::SequenceNumber::from_u64(999),
960                immutable_ref1.2,
961            );
962
963            let refs: HashSet<ObjectRef> = [owned_ref1, wrong_version_ref].into_iter().collect();
964
965            let claimed_ids = vec![immutable_id1];
966            let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
967            assert!(result.is_err(), "Version mismatch should fail");
968
969            let err = result.unwrap_err();
970            assert!(
971                matches!(
972                    err.as_inner(),
973                    SuiErrorKind::UserInputError { error: UserInputError::ObjectVersionUnavailableForConsumption { provided_obj_ref, current_version: _ } }
974                    if provided_obj_ref.0 == immutable_id1
975                ),
976                "Expected ObjectVersionUnavailableForConsumption error, got: {:?}",
977                err.as_inner()
978            );
979        }
980    }
981
982    #[sim_test]
983    async fn accept_already_executed_transaction() {
984        // This test uses ConsensusTransaction::new_user_transaction_message which creates a
985        // UserTransaction. When disable_preconsensus_locking=true (protocol version 105+),
986        // UserTransaction is not allowed. Gate with disable_preconsensus_locking=false.
987        let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
988            config.set_disable_preconsensus_locking_for_testing(false);
989            config
990        });
991
992        let (sender, keypair) = deterministic_random_account_key();
993
994        let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
995        let owned_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
996
997        let network_config =
998            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
999                .committee_size(NonZeroUsize::new(1).unwrap())
1000                .with_objects(vec![gas_object.clone(), owned_object.clone()])
1001                .build();
1002
1003        let state = TestAuthorityBuilder::new()
1004            .with_network_config(&network_config, 0)
1005            .build()
1006            .await;
1007
1008        let epoch_store = state.load_epoch_store_one_call_per_task();
1009
1010        // Create a transaction and execute it.
1011        let transaction = test_user_transaction(
1012            &state,
1013            sender,
1014            &keypair,
1015            gas_object.clone(),
1016            vec![owned_object.clone()],
1017        )
1018        .await
1019        .into_tx();
1020        let tx_digest = *transaction.digest();
1021        let cert = VerifiedExecutableTransaction::new_from_consensus(transaction.clone(), 0);
1022        let (executed_effects, _) = state
1023            .try_execute_immediately(&cert, ExecutionEnv::new(), &state.epoch_store_for_testing())
1024            .await
1025            .unwrap();
1026
1027        // Verify the transaction is executed.
1028        let read_effects = state
1029            .get_transaction_cache_reader()
1030            .get_executed_effects(&tx_digest)
1031            .expect("Transaction should be executed");
1032        assert_eq!(read_effects, executed_effects);
1033        assert_eq!(read_effects.executed_epoch(), epoch_store.epoch());
1034
1035        // Now try to vote on the already executed transaction
1036        let serialized_tx = bcs::to_bytes(&ConsensusTransaction::new_user_transaction_message(
1037            &state.name,
1038            transaction.into_inner().clone(),
1039        ))
1040        .unwrap();
1041        let validator = SuiTxValidator::new(
1042            state.clone(),
1043            state.epoch_store_for_testing().clone(),
1044            Arc::new(CheckpointServiceNoop {}),
1045            SuiTxValidatorMetrics::new(&Default::default()),
1046        );
1047        let rejected_transactions = validator
1048            .verify_and_vote_batch(&BlockRef::MAX, &[&serialized_tx])
1049            .expect("Verify and vote should succeed");
1050
1051        // The executed transaction should NOT be rejected.
1052        assert!(rejected_transactions.is_empty());
1053    }
1054}