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