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 itertools::Itertools;
13use mysten_common::ZipDebugEqIteratorExt;
14use mysten_common::assert_reachable;
15use mysten_metrics::monitored_scope;
16use nonempty::NonEmpty;
17use prometheus::{
18    IntCounter, IntCounterVec, Registry, register_int_counter_vec_with_registry,
19    register_int_counter_with_registry,
20};
21use sui_macros::fail_point_arg;
22#[cfg(msim)]
23use sui_types::base_types::AuthorityName;
24use sui_types::{
25    base_types::{ObjectID, ObjectRef},
26    error::{SuiError, SuiErrorKind, SuiResult, UserInputError},
27    messages_consensus::{ConsensusPosition, ConsensusTransaction, ConsensusTransactionKind},
28    transaction::{InputObjectKind, PlainTransactionWithClaims, TransactionDataAPI},
29};
30use tap::TapFallible;
31use tracing::{debug, info, instrument, warn};
32
33use crate::{
34    authority::{AuthorityState, authority_per_epoch_store::AuthorityPerEpochStore},
35    checkpoints::CheckpointServiceNotify,
36};
37
38/// Validates transactions from consensus and votes on whether to execute the transactions
39/// based on their validity and the current state of the authority.
40#[derive(Clone)]
41pub struct SuiTxValidator {
42    authority_state: Arc<AuthorityState>,
43    epoch_store: Arc<AuthorityPerEpochStore>,
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        Self {
60            authority_state,
61            epoch_store,
62            checkpoint_service,
63            metrics,
64        }
65    }
66
67    fn validate_transactions(&self, txs: &[ConsensusTransactionKind]) -> Result<(), SuiError> {
68        let epoch_store = &self.epoch_store;
69        let mut ckpt_messages = Vec::new();
70        let mut ckpt_batch = Vec::new();
71        for tx in txs.iter() {
72            match tx {
73                ConsensusTransactionKind::CertifiedTransaction(_) => {
74                    return Err(SuiErrorKind::UnexpectedMessage(
75                        "CertifiedTransaction cannot be used when preconsensus locking is disabled"
76                            .to_string(),
77                    )
78                    .into());
79                }
80                ConsensusTransactionKind::CheckpointSignature(_) => {
81                    return Err(SuiErrorKind::UnexpectedMessage(
82                        "CheckpointSignature V1 is no longer supported".to_string(),
83                    )
84                    .into());
85                }
86                ConsensusTransactionKind::CheckpointSignatureV2(signature) => {
87                    ckpt_messages.push(signature.as_ref());
88                    ckpt_batch.push(&signature.summary);
89                }
90                ConsensusTransactionKind::RandomnessDkgMessage(_, bytes) => {
91                    if bytes.len() > dkg_v1::DKG_MESSAGES_MAX_SIZE {
92                        warn!("batch verification error: DKG Message too large");
93                        return Err(SuiErrorKind::InvalidDkgMessageSize.into());
94                    }
95                }
96                ConsensusTransactionKind::RandomnessDkgConfirmation(_, bytes) => {
97                    if bytes.len() > dkg_v1::DKG_MESSAGES_MAX_SIZE {
98                        warn!("batch verification error: DKG Confirmation too large");
99                        return Err(SuiErrorKind::InvalidDkgMessageSize.into());
100                    }
101                }
102
103                ConsensusTransactionKind::CapabilityNotification(_) => {
104                    return Err(SuiErrorKind::UnexpectedMessage(
105                        "CapabilityNotification V1 is no longer supported".to_string(),
106                    )
107                    .into());
108                }
109
110                ConsensusTransactionKind::RandomnessStateUpdate(_, _) => {
111                    return Err(SuiErrorKind::UnexpectedMessage(
112                        "RandomnessStateUpdate is no longer supported".to_string(),
113                    )
114                    .into());
115                }
116
117                ConsensusTransactionKind::EndOfPublish(_)
118                | ConsensusTransactionKind::NewJWKFetched(_, _, _)
119                | ConsensusTransactionKind::CapabilityNotificationV2(_) => {}
120
121                ConsensusTransactionKind::UserTransaction(_) => {
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                ConsensusTransactionKind::UserTransactionV2(tx) => {
129                    if epoch_store.protocol_config().address_aliases() {
130                        let has_aliases = if epoch_store
131                            .protocol_config()
132                            .fix_checkpoint_signature_mapping()
133                        {
134                            tx.aliases().is_some()
135                        } else {
136                            tx.aliases_v1().is_some()
137                        };
138                        if !has_aliases {
139                            return Err(SuiErrorKind::UnexpectedMessage(
140                                "ConsensusTransactionKind::UserTransactionV2 must contain an aliases claim".to_string(),
141                            )
142                            .into());
143                        }
144                    }
145
146                    if let Some(aliases) = tx.aliases() {
147                        let num_sigs = tx.tx().tx_signatures().len();
148                        for (sig_idx, _) in aliases.iter() {
149                            if (*sig_idx as usize) >= num_sigs {
150                                return Err(SuiErrorKind::UnexpectedMessage(format!(
151                                    "UserTransactionV2 alias contains out-of-bounds signature index {sig_idx} (transaction has {num_sigs} signatures)",
152                                )).into());
153                            }
154                        }
155                    }
156
157                    // TODO(fastpath): move deterministic verifications of user transactions here.
158                }
159
160                ConsensusTransactionKind::ExecutionTimeObservation(obs) => {
161                    // TODO: Use a separate limit for this that may truncate shared observations.
162                    if obs.estimates.len()
163                        > epoch_store
164                            .protocol_config()
165                            .max_programmable_tx_commands()
166                            .try_into()
167                            .unwrap()
168                    {
169                        return Err(SuiErrorKind::UnexpectedMessage(format!(
170                            "ExecutionTimeObservation contains too many estimates: {}",
171                            obs.estimates.len()
172                        ))
173                        .into());
174                    }
175                }
176            }
177        }
178
179        let ckpt_count = ckpt_batch.len();
180
181        crate::signature_verifier::batch_verify_checkpoints(epoch_store.committee(), &ckpt_batch)
182            .tap_err(|e| warn!("batch verification error: {}", e))?;
183
184        // All checkpoint sigs have been verified, forward them to the checkpoint service
185        for ckpt in ckpt_messages {
186            self.checkpoint_service.notify_checkpoint_signature(ckpt)?;
187        }
188
189        self.metrics
190            .checkpoint_signatures_verified
191            .inc_by(ckpt_count as u64);
192        Ok(())
193    }
194
195    #[instrument(level = "debug", skip_all, fields(block_ref))]
196    fn vote_transactions(
197        &self,
198        block_ref: &BlockRef,
199        txs: Vec<ConsensusTransactionKind>,
200    ) -> Vec<TransactionIndex> {
201        let epoch_store = &self.epoch_store;
202        let mut reject_txn_votes = Vec::new();
203        for (i, tx) in txs.into_iter().enumerate() {
204            let tx: PlainTransactionWithClaims = match tx {
205                ConsensusTransactionKind::UserTransactionV2(tx) => *tx,
206                _ => continue,
207            };
208
209            let tx_digest = *tx.tx().digest();
210            if let Err(error) = self.vote_transaction(epoch_store, tx) {
211                debug!(?tx_digest, "Voting to reject transaction: {error}");
212                self.metrics
213                    .transaction_reject_votes
214                    .with_label_values(&[error.to_variant_name()])
215                    .inc();
216                reject_txn_votes.push(i as TransactionIndex);
217                // Cache the rejection vote reason (error) for the transaction
218                epoch_store.set_rejection_vote_reason(
219                    ConsensusPosition {
220                        epoch: epoch_store.epoch(),
221                        block: *block_ref,
222                        index: i as TransactionIndex,
223                    },
224                    &error,
225                );
226            } else {
227                debug!(?tx_digest, "Voting to accept transaction");
228            }
229        }
230
231        reject_txn_votes
232    }
233
234    #[instrument(level = "debug", skip_all, err(level = "debug"), fields(tx_digest = ?tx.tx().digest()))]
235    fn vote_transaction(
236        &self,
237        epoch_store: &Arc<AuthorityPerEpochStore>,
238        tx: PlainTransactionWithClaims,
239    ) -> SuiResult<()> {
240        // Extract claims before consuming the transaction
241        let aliases_v2 = tx.aliases();
242        let aliases_v1 = tx.aliases_v1();
243        let claimed_immutable_ids = tx.get_immutable_objects();
244        let inner_tx = tx.into_tx();
245
246        // Currently validity_check() and verify_transaction() are not required to be consistent across validators,
247        // so they do not run in validate_transactions(). They can run there once we confirm it is safe.
248        inner_tx.validity_check(&epoch_store.tx_validity_check_context())?;
249
250        self.authority_state.check_system_overload(
251            inner_tx.data(),
252            self.authority_state.check_system_overload_at_signing(),
253        )?;
254
255        #[allow(unused_mut)]
256        let mut fail_point_always_report_aliases_changed = false;
257        fail_point_arg!(
258            "consensus-validator-always-report-aliases-changed",
259            |for_validators: Vec<AuthorityName>| {
260                if for_validators.contains(&self.authority_state.name) {
261                    // always report aliases changed in simtests
262                    fail_point_always_report_aliases_changed = true;
263                }
264            }
265        );
266
267        let verified_tx = epoch_store.verify_transaction_with_current_aliases(inner_tx)?;
268
269        // aliases must have data when address_aliases() is enabled.
270        if epoch_store.protocol_config().address_aliases() {
271            let aliases_match = if epoch_store
272                .protocol_config()
273                .fix_checkpoint_signature_mapping()
274            {
275                // V2 format comparison
276                let Some(claimed_v2) = aliases_v2 else {
277                    return Err(
278                        SuiErrorKind::InvalidRequest("missing address alias claim".into()).into(),
279                    );
280                };
281                *verified_tx.aliases() == claimed_v2
282            } else {
283                // V1 format comparison: derive V1 from verified_tx and compare
284                let Some(claimed_v1) = aliases_v1 else {
285                    return Err(
286                        SuiErrorKind::InvalidRequest("missing address alias claim".into()).into(),
287                    );
288                };
289                let computed_v1: Vec<_> = verified_tx
290                    .tx()
291                    .data()
292                    .intent_message()
293                    .value
294                    .required_signers()
295                    .into_iter()
296                    .zip_eq(verified_tx.aliases().iter().map(|(_, seq)| *seq))
297                    .collect();
298                let computed_v1 =
299                    NonEmpty::from_vec(computed_v1).expect("must have at least one signer");
300                computed_v1 == claimed_v1
301            };
302
303            if !aliases_match || fail_point_always_report_aliases_changed {
304                return Err(SuiErrorKind::AliasesChanged.into());
305            }
306        }
307
308        let inner_tx = verified_tx.into_tx();
309        self.authority_state
310            .handle_vote_transaction(epoch_store, inner_tx.clone())?;
311
312        if !claimed_immutable_ids.is_empty() {
313            assert_reachable!("transaction has immutable input object claims");
314            let owned_object_refs: HashSet<ObjectRef> = inner_tx
315                .data()
316                .transaction_data()
317                .input_objects()?
318                .iter()
319                .filter_map(|obj| match obj {
320                    InputObjectKind::ImmOrOwnedMoveObject(obj_ref) => Some(*obj_ref),
321                    _ => None,
322                })
323                .collect();
324            self.verify_immutable_object_claims(&claimed_immutable_ids, owned_object_refs)?;
325        }
326
327        Ok(())
328    }
329
330    /// Verify immutable object claims are complete and accurate.
331    /// This ensures claimed_ids exactly matches the set of immutable objects in owned_object_refs.
332    /// This is stricter than general voting because the claim directly controls locking behavior.
333    fn verify_immutable_object_claims(
334        &self,
335        claimed_ids: &[ObjectID],
336        owned_object_refs: HashSet<ObjectRef>,
337    ) -> SuiResult<()> {
338        // Build map from object_id to input ref for version/digest verification
339        let input_refs_by_id: HashMap<ObjectID, ObjectRef> = owned_object_refs
340            .iter()
341            .map(|obj_ref| (obj_ref.0, *obj_ref))
342            .collect();
343
344        // First check: all claimed object IDs must be among the input object IDs
345        for claimed_id in claimed_ids {
346            if !input_refs_by_id.contains_key(claimed_id) {
347                return Err(SuiErrorKind::ImmutableObjectClaimNotFoundInInput {
348                    object_id: *claimed_id,
349                }
350                .into());
351            }
352        }
353
354        // Fetch all input objects and collect the actual immutable ones,
355        // verifying existence and version/digest match
356        let input_ids: Vec<ObjectID> = input_refs_by_id.keys().copied().collect();
357        let objects = self
358            .authority_state
359            .get_object_cache_reader()
360            .get_objects(&input_ids);
361
362        let claimed_immutable_ids = claimed_ids.iter().cloned().collect::<BTreeSet<_>>();
363        let mut found_immutable_ids = BTreeSet::new();
364
365        for (obj_opt, object_id) in objects.into_iter().zip_debug_eq(input_ids.iter()) {
366            let input_ref = input_refs_by_id.get(object_id).unwrap();
367            match obj_opt {
368                Some(o) => {
369                    // The object read here might drift from the one read earlier in validate_owned_object_versions(),
370                    // so re-check if input reference still matches actual object.
371                    let actual_ref = o.compute_object_reference();
372                    if actual_ref != *input_ref {
373                        return Err(SuiErrorKind::UserInputError {
374                            error: UserInputError::ObjectVersionUnavailableForConsumption {
375                                provided_obj_ref: *input_ref,
376                                current_version: actual_ref.1,
377                            },
378                        }
379                        .into());
380                    }
381                    if o.is_immutable() {
382                        found_immutable_ids.insert(*object_id);
383                    }
384                }
385                None => {
386                    // Object not found - we can't verify the claim, so we must reject.
387                    // This branch should not happen because owned input objects are already validated to exist.
388                    return Err(SuiErrorKind::UserInputError {
389                        error: UserInputError::ObjectNotFound {
390                            object_id: *object_id,
391                            version: Some(input_ref.1),
392                        },
393                    }
394                    .into());
395                }
396            }
397        }
398
399        // Compare claimed_ids with actual immutable objects - must match exactly
400        if let Some(claimed_id) = claimed_immutable_ids
401            .difference(&found_immutable_ids)
402            .next()
403        {
404            let input_ref = input_refs_by_id.get(claimed_id).unwrap();
405            return Err(SuiErrorKind::InvalidImmutableObjectClaim {
406                claimed_object_id: *claimed_id,
407                found_object_ref: *input_ref,
408            }
409            .into());
410        }
411        if let Some(found_id) = found_immutable_ids
412            .difference(&claimed_immutable_ids)
413            .next()
414        {
415            return Err(SuiErrorKind::ImmutableObjectNotClaimed {
416                object_id: *found_id,
417            }
418            .into());
419        }
420
421        Ok(())
422    }
423}
424
425fn tx_kind_from_bytes(tx: &[u8]) -> Result<ConsensusTransactionKind, ValidationError> {
426    bcs::from_bytes::<ConsensusTransaction>(tx)
427        .map_err(|e| {
428            ValidationError::InvalidTransaction(format!(
429                "Failed to parse transaction bytes: {:?}",
430                e
431            ))
432        })
433        .map(|tx| tx.kind)
434}
435
436impl TransactionVerifier for SuiTxValidator {
437    fn verify_batch(&self, batch: &[&[u8]]) -> Result<(), ValidationError> {
438        let _scope = monitored_scope("ValidateBatch");
439
440        let txs: Vec<_> = batch
441            .iter()
442            .map(|tx| tx_kind_from_bytes(tx))
443            .collect::<Result<Vec<_>, _>>()?;
444
445        self.validate_transactions(&txs)
446            .map_err(|e| ValidationError::InvalidTransaction(e.to_string()))
447    }
448
449    fn verify_and_vote_batch(
450        &self,
451        block_ref: &BlockRef,
452        batch: &[&[u8]],
453    ) -> Result<Vec<TransactionIndex>, ValidationError> {
454        let _scope = monitored_scope("VerifyAndVoteBatch");
455
456        let txs: Vec<_> = batch
457            .iter()
458            .map(|tx| tx_kind_from_bytes(tx))
459            .collect::<Result<Vec<_>, _>>()?;
460
461        self.validate_transactions(&txs)
462            .map_err(|e| ValidationError::InvalidTransaction(e.to_string()))?;
463
464        Ok(self.vote_transactions(block_ref, txs))
465    }
466}
467
468pub struct SuiTxValidatorMetrics {
469    checkpoint_signatures_verified: IntCounter,
470    transaction_reject_votes: IntCounterVec,
471}
472
473impl SuiTxValidatorMetrics {
474    pub fn new(registry: &Registry) -> Arc<Self> {
475        Arc::new(Self {
476            checkpoint_signatures_verified: register_int_counter_with_registry!(
477                "tx_validator_checkpoint_signatures_verified",
478                "Number of checkpoint verified in consensus batch verifier",
479                registry
480            )
481            .unwrap(),
482            transaction_reject_votes: register_int_counter_vec_with_registry!(
483                "tx_validator_transaction_reject_votes",
484                "Number of reject transaction votes per reason",
485                &["reason"],
486                registry
487            )
488            .unwrap(),
489        })
490    }
491}
492
493#[cfg(test)]
494mod tests {
495    use std::collections::HashSet;
496    use std::num::NonZeroUsize;
497    use std::sync::Arc;
498
499    use consensus_core::TransactionVerifier as _;
500    use consensus_types::block::BlockRef;
501    use fastcrypto::traits::KeyPair;
502    use sui_config::transaction_deny_config::TransactionDenyConfigBuilder;
503    use sui_macros::sim_test;
504    use sui_protocol_config::ProtocolConfig;
505    use sui_types::crypto::deterministic_random_account_key;
506    use sui_types::error::{SuiErrorKind, UserInputError};
507    use sui_types::executable_transaction::VerifiedExecutableTransaction;
508    use sui_types::messages_checkpoint::{
509        CheckpointContents, CheckpointSignatureMessage, CheckpointSummary, SignedCheckpointSummary,
510    };
511    use sui_types::messages_consensus::ConsensusPosition;
512    use sui_types::{
513        base_types::{ExecutionDigests, ObjectID, ObjectRef},
514        crypto::Ed25519SuiSignature,
515        effects::TransactionEffectsAPI as _,
516        messages_consensus::ConsensusTransaction,
517        object::Object,
518        signature::GenericSignature,
519        transaction::{PlainTransactionWithClaims, Transaction},
520    };
521
522    use crate::authority::ExecutionEnv;
523    use crate::{
524        authority::test_authority_builder::TestAuthorityBuilder,
525        checkpoints::CheckpointServiceNoop,
526        consensus_adapter::consensus_tests::{
527            test_gas_objects, test_user_transaction, test_user_transactions,
528        },
529        consensus_validator::{SuiTxValidator, SuiTxValidatorMetrics},
530    };
531
532    #[sim_test]
533    async fn accept_valid_transaction() {
534        // Initialize an authority with a (owned) gas object and a shared object.
535        let mut objects = test_gas_objects();
536        let shared_object = Object::shared_for_testing();
537        objects.push(shared_object.clone());
538
539        let network_config =
540            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
541                .with_objects(objects.clone())
542                .build();
543
544        let state = TestAuthorityBuilder::new()
545            .with_network_config(&network_config, 0)
546            .build()
547            .await;
548        let name1 = state.name;
549        let transactions = test_user_transactions(&state, shared_object).await;
550
551        let first_transaction = transactions[0].clone();
552        let first_transaction_bytes: Vec<u8> =
553            bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
554                &name1,
555                first_transaction.into(),
556            ))
557            .unwrap();
558
559        let metrics = SuiTxValidatorMetrics::new(&Default::default());
560        let validator = SuiTxValidator::new(
561            state.clone(),
562            state.epoch_store_for_testing().clone(),
563            Arc::new(CheckpointServiceNoop {}),
564            metrics,
565        );
566        let res = validator.verify_batch(&[&first_transaction_bytes]);
567        assert!(res.is_ok(), "{res:?}");
568
569        let transaction_bytes: Vec<_> = transactions
570            .clone()
571            .into_iter()
572            .map(|tx| {
573                bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
574                    &name1,
575                    tx.into(),
576                ))
577                .unwrap()
578            })
579            .collect();
580
581        let batch: Vec<_> = transaction_bytes.iter().map(|t| t.as_slice()).collect();
582        let res_batch = validator.verify_batch(&batch);
583        assert!(res_batch.is_ok(), "{res_batch:?}");
584
585        let bogus_transaction_bytes: Vec<_> = transactions
586            .into_iter()
587            .map(|tx| {
588                // Create a transaction with an invalid signature
589                let aliases = tx.aliases().clone();
590                let mut signed_tx: Transaction = tx.into_tx().into();
591                signed_tx.tx_signatures_mut_for_testing()[0] =
592                    GenericSignature::Signature(sui_types::crypto::Signature::Ed25519SuiSignature(
593                        Ed25519SuiSignature::default(),
594                    ));
595                let tx_with_claims = PlainTransactionWithClaims::from_aliases(signed_tx, aliases);
596                bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
597                    &name1,
598                    tx_with_claims,
599                ))
600                .unwrap()
601            })
602            .collect();
603
604        let batch: Vec<_> = bogus_transaction_bytes
605            .iter()
606            .map(|t| t.as_slice())
607            .collect();
608        // verify_batch doesn't verify user transaction signatures (that happens in vote_transaction).
609        // Use verify_and_vote_batch to test that bogus transactions are rejected during voting.
610        let res_batch = validator.verify_and_vote_batch(&BlockRef::MIN, &batch);
611        assert!(res_batch.is_ok());
612        // All transactions should be in the rejection list since they have invalid signatures
613        let rejections = res_batch.unwrap();
614        assert_eq!(
615            rejections.len(),
616            batch.len(),
617            "All bogus transactions should be rejected"
618        );
619    }
620
621    #[tokio::test]
622    async fn test_verify_and_vote_batch() {
623        // 1 account keypair
624        let (sender, keypair) = deterministic_random_account_key();
625
626        // 8 gas objects.
627        let gas_objects: Vec<Object> = (0..8)
628            .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
629            .collect();
630
631        // 2 owned objects.
632        let owned_objects: Vec<Object> = (0..2)
633            .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
634            .collect();
635        let denied_object = owned_objects[1].clone();
636
637        let mut objects = gas_objects.clone();
638        objects.extend(owned_objects.clone());
639
640        let network_config =
641            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
642                .committee_size(NonZeroUsize::new(1).unwrap())
643                .with_objects(objects.clone())
644                .build();
645
646        // 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.
647        let transaction_deny_config = TransactionDenyConfigBuilder::new()
648            .add_denied_object(denied_object.id())
649            .build();
650        let state = TestAuthorityBuilder::new()
651            .with_network_config(&network_config, 0)
652            .with_transaction_deny_config(transaction_deny_config)
653            .build()
654            .await;
655
656        // Create two user transactions
657
658        // A valid transaction
659        let valid_transaction = test_user_transaction(
660            &state,
661            sender,
662            &keypair,
663            gas_objects[0].clone(),
664            vec![owned_objects[0].clone()],
665        )
666        .await;
667
668        // An invalid transaction where the input object is denied
669        let invalid_transaction = test_user_transaction(
670            &state,
671            sender,
672            &keypair,
673            gas_objects[1].clone(),
674            vec![denied_object.clone()],
675        )
676        .await;
677
678        // Now create the vector with the transactions and serialize them.
679        let transactions = vec![valid_transaction, invalid_transaction];
680        let serialized_transactions: Vec<_> = transactions
681            .into_iter()
682            .map(|t| {
683                bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
684                    &state.name,
685                    t.into(),
686                ))
687                .unwrap()
688            })
689            .collect();
690        let batch: Vec<_> = serialized_transactions
691            .iter()
692            .map(|t| t.as_slice())
693            .collect();
694
695        let validator = SuiTxValidator::new(
696            state.clone(),
697            state.epoch_store_for_testing().clone(),
698            Arc::new(CheckpointServiceNoop {}),
699            SuiTxValidatorMetrics::new(&Default::default()),
700        );
701
702        // WHEN
703        let rejected_transactions = validator
704            .verify_and_vote_batch(&BlockRef::MAX, &batch)
705            .unwrap();
706
707        // THEN
708        // The 2nd transaction should be rejected
709        assert_eq!(rejected_transactions, vec![1]);
710
711        // AND
712        // The reject reason should get cached
713        let epoch_store = state.load_epoch_store_one_call_per_task();
714        let reason = epoch_store
715            .get_rejection_vote_reason(ConsensusPosition {
716                epoch: state.load_epoch_store_one_call_per_task().epoch(),
717                block: BlockRef::MAX,
718                index: 1,
719            })
720            .expect("Rejection vote reason should be set");
721
722        assert_eq!(
723            reason,
724            SuiErrorKind::UserInputError {
725                error: UserInputError::TransactionDenied {
726                    error: format!(
727                        "Access to input object {:?} is temporarily disabled",
728                        denied_object.id()
729                    )
730                }
731            }
732        );
733    }
734
735    #[sim_test]
736    async fn accept_checkpoint_signature_v2() {
737        let network_config =
738            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir().build();
739
740        let state = TestAuthorityBuilder::new()
741            .with_network_config(&network_config, 0)
742            .build()
743            .await;
744
745        let epoch_store = state.load_epoch_store_one_call_per_task();
746
747        // Create a minimal checkpoint summary and sign it with the validator's protocol key
748        let checkpoint_summary = CheckpointSummary::new(
749            &ProtocolConfig::get_for_max_version_UNSAFE(),
750            epoch_store.epoch(),
751            0,
752            0,
753            &CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]),
754            None,
755            Default::default(),
756            None,
757            0,
758            Vec::new(),
759            Vec::new(),
760        );
761
762        let keypair = network_config.validator_configs()[0].protocol_key_pair();
763        let authority = keypair.public().into();
764        let signed = SignedCheckpointSummary::new(
765            epoch_store.epoch(),
766            checkpoint_summary,
767            keypair,
768            authority,
769        );
770        let message = CheckpointSignatureMessage { summary: signed };
771
772        let tx = ConsensusTransaction::new_checkpoint_signature_message_v2(message);
773        let bytes = bcs::to_bytes(&tx).unwrap();
774
775        let validator = SuiTxValidator::new(
776            state.clone(),
777            state.epoch_store_for_testing().clone(),
778            Arc::new(CheckpointServiceNoop {}),
779            SuiTxValidatorMetrics::new(&Default::default()),
780        );
781
782        let res = validator.verify_batch(&[&bytes]);
783        assert!(res.is_ok(), "{res:?}");
784    }
785
786    #[sim_test]
787    async fn test_verify_immutable_object_claims() {
788        let (sender, _keypair) = deterministic_random_account_key();
789
790        // Create owned objects
791        let owned_object1 = Object::with_id_owner_for_testing(ObjectID::random(), sender);
792        let owned_object2 = Object::with_id_owner_for_testing(ObjectID::random(), sender);
793
794        // Create immutable objects
795        let immutable_object1 = Object::immutable_with_id_for_testing(ObjectID::random());
796        let immutable_object2 = Object::immutable_with_id_for_testing(ObjectID::random());
797
798        // Save IDs before moving objects
799        let owned_id1 = owned_object1.id();
800        let owned_id2 = owned_object2.id();
801        let immutable_id1 = immutable_object1.id();
802        let immutable_id2 = immutable_object2.id();
803
804        let all_objects = vec![
805            owned_object1,
806            owned_object2,
807            immutable_object1,
808            immutable_object2,
809        ];
810
811        let network_config =
812            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
813                .committee_size(NonZeroUsize::new(1).unwrap())
814                .with_objects(all_objects)
815                .build();
816
817        let state = TestAuthorityBuilder::new()
818            .with_network_config(&network_config, 0)
819            .build()
820            .await;
821
822        // Retrieve actual object references from the state (as they are after genesis)
823        let cache_reader = state.get_object_cache_reader();
824        let owned_ref1 = cache_reader
825            .get_object(&owned_id1)
826            .expect("owned_id1 not found")
827            .compute_object_reference();
828        let owned_ref2 = cache_reader
829            .get_object(&owned_id2)
830            .expect("owned_id2 not found")
831            .compute_object_reference();
832        let immutable_ref1 = cache_reader
833            .get_object(&immutable_id1)
834            .expect("immutable_id1 not found")
835            .compute_object_reference();
836        let immutable_ref2 = cache_reader
837            .get_object(&immutable_id2)
838            .expect("immutable_id2 not found")
839            .compute_object_reference();
840
841        let validator = SuiTxValidator::new(
842            state.clone(),
843            state.epoch_store_for_testing().clone(),
844            Arc::new(CheckpointServiceNoop {}),
845            SuiTxValidatorMetrics::new(&Default::default()),
846        );
847
848        // Test 1: Empty claims with no immutable objects in inputs - should pass
849        {
850            let owned_refs: HashSet<ObjectRef> = [owned_ref1, owned_ref2].into_iter().collect();
851
852            let result = validator.verify_immutable_object_claims(&[], owned_refs);
853            assert!(
854                result.is_ok(),
855                "Empty claims with only owned objects should pass, got error: {:?}",
856                result.err()
857            );
858        }
859
860        // Test 2: Correct claims - immutable objects properly claimed - should pass
861        {
862            let refs: HashSet<ObjectRef> = [owned_ref1, immutable_ref1].into_iter().collect();
863
864            let claimed_ids = vec![immutable_id1];
865            let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
866            assert!(result.is_ok(), "Correct immutable object claim should pass");
867        }
868
869        // Test 3: Multiple correct claims - should pass
870        {
871            let refs: HashSet<ObjectRef> = [owned_ref1, immutable_ref1, immutable_ref2]
872                .into_iter()
873                .collect();
874
875            let claimed_ids = vec![immutable_id1, immutable_id2];
876            let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
877            assert!(
878                result.is_ok(),
879                "Multiple correct immutable claims should pass"
880            );
881        }
882
883        // Test 4: Missing claim - immutable object not claimed - should fail
884        {
885            let refs: HashSet<ObjectRef> = [owned_ref1, immutable_ref1].into_iter().collect();
886
887            let claimed_ids: Vec<ObjectID> = vec![];
888            let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
889            assert!(result.is_err(), "Missing immutable claim should fail");
890
891            let err = result.unwrap_err();
892            assert!(
893                matches!(
894                    err.as_inner(),
895                    SuiErrorKind::ImmutableObjectNotClaimed { object_id }
896                    if *object_id == immutable_id1
897                ),
898                "Expected ImmutableObjectNotClaimed error, got: {:?}",
899                err.as_inner()
900            );
901        }
902
903        // Test 5: False claim - owned object claimed as immutable - should fail
904        {
905            let refs: HashSet<ObjectRef> = [owned_ref1, owned_ref2].into_iter().collect();
906
907            let claimed_ids = vec![owned_id1];
908            let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
909            assert!(
910                result.is_err(),
911                "False immutable claim on owned object should fail"
912            );
913
914            let err = result.unwrap_err();
915            assert!(
916                matches!(
917                    err.as_inner(),
918                    SuiErrorKind::InvalidImmutableObjectClaim { claimed_object_id, .. }
919                    if *claimed_object_id == owned_id1
920                ),
921                "Expected InvalidImmutableObjectClaim error, got: {:?}",
922                err.as_inner()
923            );
924        }
925
926        // Test 6: Claim not in inputs - should fail
927        {
928            let refs: HashSet<ObjectRef> = [owned_ref1, owned_ref2].into_iter().collect();
929
930            let claimed_ids = vec![immutable_id1];
931            let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
932            assert!(result.is_err(), "Claim not in inputs should fail");
933
934            let err = result.unwrap_err();
935            assert!(
936                matches!(
937                    err.as_inner(),
938                    SuiErrorKind::ImmutableObjectClaimNotFoundInInput { object_id }
939                    if *object_id == immutable_id1
940                ),
941                "Expected ImmutableObjectClaimNotFoundInInput error, got: {:?}",
942                err.as_inner()
943            );
944        }
945
946        // Test 7: Object not found (non-existent object) - should fail
947        {
948            let non_existent_id = ObjectID::random();
949            let fake_ref = (
950                non_existent_id,
951                sui_types::base_types::SequenceNumber::new(),
952                sui_types::digests::ObjectDigest::random(),
953            );
954            let refs: HashSet<ObjectRef> = [owned_ref1, fake_ref].into_iter().collect();
955
956            let claimed_ids: Vec<ObjectID> = vec![];
957            let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
958            assert!(result.is_err(), "Non-existent object should fail");
959
960            let err = result.unwrap_err();
961            assert!(
962                matches!(
963                    err.as_inner(),
964                    SuiErrorKind::UserInputError { error: UserInputError::ObjectNotFound { object_id, .. } }
965                    if *object_id == non_existent_id
966                ),
967                "Expected ObjectNotFound error, got: {:?}",
968                err.as_inner()
969            );
970        }
971
972        // Test 8: Version/digest mismatch for immutable object - should fail
973        {
974            // Use a wrong version for the immutable object
975            let wrong_version_ref = (
976                immutable_ref1.0,
977                sui_types::base_types::SequenceNumber::from_u64(999),
978                immutable_ref1.2,
979            );
980
981            let refs: HashSet<ObjectRef> = [owned_ref1, wrong_version_ref].into_iter().collect();
982
983            let claimed_ids = vec![immutable_id1];
984            let result = validator.verify_immutable_object_claims(&claimed_ids, refs);
985            assert!(result.is_err(), "Version mismatch should fail");
986
987            let err = result.unwrap_err();
988            assert!(
989                matches!(
990                    err.as_inner(),
991                    SuiErrorKind::UserInputError { error: UserInputError::ObjectVersionUnavailableForConsumption { provided_obj_ref, current_version: _ } }
992                    if provided_obj_ref.0 == immutable_id1
993                ),
994                "Expected ObjectVersionUnavailableForConsumption error, got: {:?}",
995                err.as_inner()
996            );
997        }
998    }
999
1000    #[sim_test]
1001    async fn accept_already_executed_transaction() {
1002        let (sender, keypair) = deterministic_random_account_key();
1003
1004        let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
1005        let owned_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
1006
1007        let network_config =
1008            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
1009                .committee_size(NonZeroUsize::new(1).unwrap())
1010                .with_objects(vec![gas_object.clone(), owned_object.clone()])
1011                .build();
1012
1013        let state = TestAuthorityBuilder::new()
1014            .with_network_config(&network_config, 0)
1015            .build()
1016            .await;
1017
1018        let epoch_store = state.load_epoch_store_one_call_per_task();
1019
1020        // Create a transaction and execute it.
1021        let transaction = test_user_transaction(
1022            &state,
1023            sender,
1024            &keypair,
1025            gas_object.clone(),
1026            vec![owned_object.clone()],
1027        )
1028        .await;
1029        let tx_digest = *transaction.tx().digest();
1030        let cert =
1031            VerifiedExecutableTransaction::new_from_consensus(transaction.clone().into_tx(), 0);
1032        let (executed_effects, _) = state
1033            .try_execute_immediately(&cert, ExecutionEnv::new(), &state.epoch_store_for_testing())
1034            .await
1035            .unwrap();
1036
1037        // Verify the transaction is executed.
1038        let read_effects = state
1039            .get_transaction_cache_reader()
1040            .get_executed_effects(&tx_digest)
1041            .expect("Transaction should be executed");
1042        assert_eq!(read_effects, executed_effects);
1043        assert_eq!(read_effects.executed_epoch(), epoch_store.epoch());
1044
1045        // Now try to vote on the already executed transaction using UserTransactionV2
1046        let serialized_tx = bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
1047            &state.name,
1048            transaction.into(),
1049        ))
1050        .unwrap();
1051        let validator = SuiTxValidator::new(
1052            state.clone(),
1053            state.epoch_store_for_testing().clone(),
1054            Arc::new(CheckpointServiceNoop {}),
1055            SuiTxValidatorMetrics::new(&Default::default()),
1056        );
1057        let rejected_transactions = validator
1058            .verify_and_vote_batch(&BlockRef::MAX, &[&serialized_tx])
1059            .expect("Verify and vote should succeed");
1060
1061        // The executed transaction should NOT be rejected.
1062        assert!(rejected_transactions.is_empty());
1063    }
1064
1065    #[tokio::test]
1066    async fn test_reject_invalid_alias_signature_index() {
1067        let (sender, keypair) = deterministic_random_account_key();
1068
1069        let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
1070        let owned_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
1071
1072        let network_config =
1073            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
1074                .committee_size(NonZeroUsize::new(1).unwrap())
1075                .with_objects(vec![gas_object.clone(), owned_object.clone()])
1076                .build();
1077
1078        let state = TestAuthorityBuilder::new()
1079            .with_network_config(&network_config, 0)
1080            .build()
1081            .await;
1082
1083        let transaction = test_user_transaction(
1084            &state,
1085            sender,
1086            &keypair,
1087            gas_object.clone(),
1088            vec![owned_object.clone()],
1089        )
1090        .await;
1091
1092        // Extract the inner transaction and construct a PlainTransactionWithClaims
1093        // with a bogus alias where sig_idx = 255 (far exceeding the 1 signature).
1094        let inner_tx: Transaction = transaction.into_tx().into();
1095        let bogus_aliases = nonempty::nonempty![(255u8, None)];
1096        let tx_with_bogus_alias = PlainTransactionWithClaims::from_aliases(inner_tx, bogus_aliases);
1097
1098        let serialized_tx = bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
1099            &state.name,
1100            tx_with_bogus_alias,
1101        ))
1102        .unwrap();
1103
1104        let validator = SuiTxValidator::new(
1105            state.clone(),
1106            state.epoch_store_for_testing().clone(),
1107            Arc::new(CheckpointServiceNoop {}),
1108            SuiTxValidatorMetrics::new(&Default::default()),
1109        );
1110
1111        let res = validator.verify_batch(&[&serialized_tx]);
1112        assert!(
1113            res.is_err(),
1114            "Should reject transaction with out-of-bounds alias signature index"
1115        );
1116    }
1117}