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