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