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