Skip to main content

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