1use std::sync::Arc;
5
6use consensus_core::{TransactionVerifier, ValidationError};
7use consensus_types::block::{BlockRef, TransactionIndex};
8use fastcrypto_tbls::dkg_v1;
9use mysten_metrics::monitored_scope;
10use prometheus::{
11 IntCounter, IntCounterVec, Registry, register_int_counter_vec_with_registry,
12 register_int_counter_with_registry,
13};
14use sui_macros::fail_point_arg;
15#[cfg(msim)]
16use sui_types::base_types::AuthorityName;
17use sui_types::{
18 error::{SuiError, SuiErrorKind, SuiResult},
19 messages_consensus::{ConsensusPosition, ConsensusTransaction, ConsensusTransactionKind},
20 transaction::{TransactionDataAPI, TransactionWithAliases, WithAliases},
21};
22use tap::TapFallible;
23use tracing::{debug, info, instrument, warn};
24
25use crate::{
26 authority::{AuthorityState, authority_per_epoch_store::AuthorityPerEpochStore},
27 checkpoints::CheckpointServiceNotify,
28 consensus_adapter::{ConsensusOverloadChecker, NoopConsensusOverloadChecker},
29};
30
31#[derive(Clone)]
34pub struct SuiTxValidator {
35 authority_state: Arc<AuthorityState>,
36 consensus_overload_checker: Arc<dyn ConsensusOverloadChecker>,
37 checkpoint_service: Arc<dyn CheckpointServiceNotify + Send + Sync>,
38 metrics: Arc<SuiTxValidatorMetrics>,
39}
40
41impl SuiTxValidator {
42 pub fn new(
43 authority_state: Arc<AuthorityState>,
44 checkpoint_service: Arc<dyn CheckpointServiceNotify + Send + Sync>,
45 metrics: Arc<SuiTxValidatorMetrics>,
46 ) -> Self {
47 let epoch_store = authority_state.load_epoch_store_one_call_per_task().clone();
48 info!(
49 "SuiTxValidator constructed for epoch {}",
50 epoch_store.epoch()
51 );
52 let consensus_overload_checker = Arc::new(NoopConsensusOverloadChecker {});
54 Self {
55 authority_state,
56 consensus_overload_checker,
57 checkpoint_service,
58 metrics,
59 }
60 }
61
62 fn validate_transactions(&self, txs: &[ConsensusTransactionKind]) -> Result<(), SuiError> {
63 let epoch_store = self.authority_state.load_epoch_store_one_call_per_task();
64
65 let mut cert_batch = Vec::new();
66 let mut ckpt_messages = Vec::new();
67 let mut ckpt_batch = Vec::new();
68 for tx in txs.iter() {
69 match tx {
70 ConsensusTransactionKind::CertifiedTransaction(certificate) => {
71 cert_batch.push(certificate.as_ref());
72 }
73 ConsensusTransactionKind::CheckpointSignature(signature) => {
74 ckpt_messages.push(signature.as_ref());
75 ckpt_batch.push(&signature.summary);
76 }
77 ConsensusTransactionKind::CheckpointSignatureV2(signature) => {
78 if !epoch_store
79 .protocol_config()
80 .consensus_checkpoint_signature_key_includes_digest()
81 {
82 return Err(SuiErrorKind::UnexpectedMessage(
83 "ConsensusTransactionKind::CheckpointSignatureV2 is unsupported"
84 .to_string(),
85 )
86 .into());
87 }
88 ckpt_messages.push(signature.as_ref());
89 ckpt_batch.push(&signature.summary);
90 }
91 ConsensusTransactionKind::RandomnessDkgMessage(_, bytes) => {
92 if bytes.len() > dkg_v1::DKG_MESSAGES_MAX_SIZE {
93 warn!("batch verification error: DKG Message too large");
94 return Err(SuiErrorKind::InvalidDkgMessageSize.into());
95 }
96 }
97 ConsensusTransactionKind::RandomnessDkgConfirmation(_, bytes) => {
98 if bytes.len() > dkg_v1::DKG_MESSAGES_MAX_SIZE {
99 warn!("batch verification error: DKG Confirmation too large");
100 return Err(SuiErrorKind::InvalidDkgMessageSize.into());
101 }
102 }
103
104 ConsensusTransactionKind::CapabilityNotification(_) => {}
105
106 ConsensusTransactionKind::EndOfPublish(_)
107 | ConsensusTransactionKind::NewJWKFetched(_, _, _)
108 | ConsensusTransactionKind::CapabilityNotificationV2(_)
109 | ConsensusTransactionKind::RandomnessStateUpdate(_, _) => {}
110
111 ConsensusTransactionKind::UserTransaction(_)
112 | ConsensusTransactionKind::UserTransactionV2(_) => {
113 if !epoch_store.protocol_config().mysticeti_fastpath() {
114 return Err(SuiErrorKind::UnexpectedMessage(
115 "ConsensusTransactionKind::UserTransaction is unsupported".to_string(),
116 )
117 .into());
118 }
119 }
122
123 ConsensusTransactionKind::ExecutionTimeObservation(obs) => {
124 if obs.estimates.len()
126 > epoch_store
127 .protocol_config()
128 .max_programmable_tx_commands()
129 .try_into()
130 .unwrap()
131 {
132 return Err(SuiErrorKind::UnexpectedMessage(format!(
133 "ExecutionTimeObservation contains too many estimates: {}",
134 obs.estimates.len()
135 ))
136 .into());
137 }
138 }
139 }
140 }
141
142 let cert_count = cert_batch.len();
144 let ckpt_count = ckpt_batch.len();
145
146 epoch_store
147 .signature_verifier
148 .verify_certs_and_checkpoints(cert_batch, ckpt_batch)
149 .tap_err(|e| warn!("batch verification error: {}", e))?;
150
151 for ckpt in ckpt_messages {
153 self.checkpoint_service
154 .notify_checkpoint_signature(&epoch_store, ckpt)?;
155 }
156
157 self.metrics
158 .certificate_signatures_verified
159 .inc_by(cert_count as u64);
160 self.metrics
161 .checkpoint_signatures_verified
162 .inc_by(ckpt_count as u64);
163 Ok(())
164 }
165
166 #[instrument(level = "debug", skip_all, fields(block_ref))]
167 fn vote_transactions(
168 &self,
169 block_ref: &BlockRef,
170 txs: Vec<ConsensusTransactionKind>,
171 ) -> Vec<TransactionIndex> {
172 let epoch_store = self.authority_state.load_epoch_store_one_call_per_task();
173 if !epoch_store.protocol_config().mysticeti_fastpath() {
174 return vec![];
175 }
176
177 let mut result = Vec::new();
178 for (i, tx) in txs.into_iter().enumerate() {
179 let tx = match tx {
180 ConsensusTransactionKind::UserTransaction(tx) => {
181 let no_aliases_allowed = tx
182 .intent_message()
183 .value
184 .required_signers()
185 .map(|s| (s, None));
186 WithAliases::new(*tx, no_aliases_allowed)
187 }
188 ConsensusTransactionKind::UserTransactionV2(tx) => *tx,
189 _ => continue,
190 };
191
192 let tx_digest = *tx.tx().digest();
193 if let Err(error) = self.vote_transaction(&epoch_store, tx) {
194 debug!(?tx_digest, "Voting to reject transaction: {error}");
195 self.metrics
196 .transaction_reject_votes
197 .with_label_values(&[error.to_variant_name()])
198 .inc();
199 result.push(i as TransactionIndex);
200 epoch_store.set_rejection_vote_reason(
202 ConsensusPosition {
203 epoch: epoch_store.epoch(),
204 block: *block_ref,
205 index: i as TransactionIndex,
206 },
207 &error,
208 );
209 } else {
210 debug!(?tx_digest, "Voting to accept transaction");
211 }
212 }
213
214 result
215 }
216
217 #[instrument(level = "debug", skip_all, err(level = "debug"), fields(tx_digest = ?tx.tx().digest()))]
218 fn vote_transaction(
219 &self,
220 epoch_store: &Arc<AuthorityPerEpochStore>,
221 tx: TransactionWithAliases,
222 ) -> SuiResult<()> {
223 let (tx, aliases) = tx.into_inner();
224
225 tx.validity_check(&epoch_store.tx_validity_check_context())?;
228
229 self.authority_state.check_system_overload(
230 &*self.consensus_overload_checker,
231 tx.data(),
232 self.authority_state.check_system_overload_at_signing(),
233 )?;
234
235 #[allow(unused_mut)]
236 let mut fail_point_always_report_aliases_changed = false;
237 fail_point_arg!(
238 "consensus-validator-always-report-aliases-changed",
239 |for_validators: Vec<AuthorityName>| {
240 if for_validators.contains(&self.authority_state.name) {
241 fail_point_always_report_aliases_changed = true;
243 }
244 }
245 );
246
247 let verified_tx = epoch_store.verify_transaction_with_current_aliases(tx)?;
248 if *verified_tx.aliases() != aliases || fail_point_always_report_aliases_changed {
249 return Err(SuiErrorKind::AliasesChanged.into());
250 }
251
252 self.authority_state
253 .handle_vote_transaction(epoch_store, verified_tx.into_tx())?;
254
255 Ok(())
256 }
257}
258
259fn tx_kind_from_bytes(tx: &[u8]) -> Result<ConsensusTransactionKind, ValidationError> {
260 bcs::from_bytes::<ConsensusTransaction>(tx)
261 .map_err(|e| {
262 ValidationError::InvalidTransaction(format!(
263 "Failed to parse transaction bytes: {:?}",
264 e
265 ))
266 })
267 .map(|tx| tx.kind)
268}
269
270impl TransactionVerifier for SuiTxValidator {
271 fn verify_batch(&self, batch: &[&[u8]]) -> Result<(), ValidationError> {
272 let _scope = monitored_scope("ValidateBatch");
273
274 let txs: Vec<_> = batch
275 .iter()
276 .map(|tx| tx_kind_from_bytes(tx))
277 .collect::<Result<Vec<_>, _>>()?;
278
279 self.validate_transactions(&txs)
280 .map_err(|e| ValidationError::InvalidTransaction(e.to_string()))
281 }
282
283 fn verify_and_vote_batch(
284 &self,
285 block_ref: &BlockRef,
286 batch: &[&[u8]],
287 ) -> Result<Vec<TransactionIndex>, ValidationError> {
288 let _scope = monitored_scope("VerifyAndVoteBatch");
289
290 let txs: Vec<_> = batch
291 .iter()
292 .map(|tx| tx_kind_from_bytes(tx))
293 .collect::<Result<Vec<_>, _>>()?;
294
295 self.validate_transactions(&txs)
296 .map_err(|e| ValidationError::InvalidTransaction(e.to_string()))?;
297
298 Ok(self.vote_transactions(block_ref, txs))
299 }
300}
301
302pub struct SuiTxValidatorMetrics {
303 certificate_signatures_verified: IntCounter,
304 checkpoint_signatures_verified: IntCounter,
305 transaction_reject_votes: IntCounterVec,
306}
307
308impl SuiTxValidatorMetrics {
309 pub fn new(registry: &Registry) -> Arc<Self> {
310 Arc::new(Self {
311 certificate_signatures_verified: register_int_counter_with_registry!(
312 "tx_validator_certificate_signatures_verified",
313 "Number of certificates verified in consensus batch verifier",
314 registry
315 )
316 .unwrap(),
317 checkpoint_signatures_verified: register_int_counter_with_registry!(
318 "tx_validator_checkpoint_signatures_verified",
319 "Number of checkpoint verified in consensus batch verifier",
320 registry
321 )
322 .unwrap(),
323 transaction_reject_votes: register_int_counter_vec_with_registry!(
324 "tx_validator_transaction_reject_votes",
325 "Number of reject transaction votes per reason",
326 &["reason"],
327 registry
328 )
329 .unwrap(),
330 })
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use std::num::NonZeroUsize;
337 use std::sync::Arc;
338
339 use consensus_core::TransactionVerifier as _;
340 use consensus_types::block::BlockRef;
341 use fastcrypto::traits::KeyPair;
342 use sui_config::transaction_deny_config::TransactionDenyConfigBuilder;
343 use sui_macros::sim_test;
344 use sui_protocol_config::{Chain, ProtocolConfig, ProtocolVersion};
345 use sui_types::crypto::deterministic_random_account_key;
346 use sui_types::error::{SuiErrorKind, UserInputError};
347 use sui_types::executable_transaction::VerifiedExecutableTransaction;
348 use sui_types::messages_checkpoint::{
349 CheckpointContents, CheckpointSignatureMessage, CheckpointSummary, SignedCheckpointSummary,
350 };
351 use sui_types::messages_consensus::ConsensusPosition;
352 use sui_types::{
353 base_types::{ExecutionDigests, ObjectID},
354 crypto::Ed25519SuiSignature,
355 effects::TransactionEffectsAPI as _,
356 messages_consensus::ConsensusTransaction,
357 object::Object,
358 signature::GenericSignature,
359 transaction::{Transaction, TransactionWithAliases},
360 };
361
362 use crate::authority::ExecutionEnv;
363 use crate::{
364 authority::test_authority_builder::TestAuthorityBuilder,
365 checkpoints::CheckpointServiceNoop,
366 consensus_adapter::consensus_tests::{
367 test_gas_objects, test_user_transaction, test_user_transactions,
368 },
369 consensus_validator::{SuiTxValidator, SuiTxValidatorMetrics},
370 };
371
372 #[sim_test]
373 async fn accept_valid_transaction() {
374 let mut objects = test_gas_objects();
377 let shared_object = Object::shared_for_testing();
378 objects.push(shared_object.clone());
379
380 let network_config =
381 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
382 .with_objects(objects.clone())
383 .build();
384
385 let state = TestAuthorityBuilder::new()
386 .with_network_config(&network_config, 0)
387 .build()
388 .await;
389 let name1 = state.name;
390 let transactions = test_user_transactions(&state, shared_object).await;
391
392 let first_transaction = transactions[0].clone();
393 let first_transaction_bytes: Vec<u8> =
394 bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
395 &name1,
396 first_transaction.into(),
397 ))
398 .unwrap();
399
400 let metrics = SuiTxValidatorMetrics::new(&Default::default());
401 let validator =
402 SuiTxValidator::new(state.clone(), Arc::new(CheckpointServiceNoop {}), metrics);
403 let res = validator.verify_batch(&[&first_transaction_bytes]);
404 assert!(res.is_ok(), "{res:?}");
405
406 let transaction_bytes: Vec<_> = transactions
407 .clone()
408 .into_iter()
409 .map(|tx| {
410 bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
411 &name1,
412 tx.into(),
413 ))
414 .unwrap()
415 })
416 .collect();
417
418 let batch: Vec<_> = transaction_bytes.iter().map(|t| t.as_slice()).collect();
419 let res_batch = validator.verify_batch(&batch);
420 assert!(res_batch.is_ok(), "{res_batch:?}");
421
422 let bogus_transaction_bytes: Vec<_> = transactions
423 .into_iter()
424 .map(|tx| {
425 let aliases = tx.aliases().clone();
427 let mut signed_tx: Transaction = tx.into_tx().into();
428 signed_tx.tx_signatures_mut_for_testing()[0] =
429 GenericSignature::Signature(sui_types::crypto::Signature::Ed25519SuiSignature(
430 Ed25519SuiSignature::default(),
431 ));
432 let tx_with_aliases = TransactionWithAliases::new(signed_tx, aliases);
433 bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
434 &name1,
435 tx_with_aliases,
436 ))
437 .unwrap()
438 })
439 .collect();
440
441 let batch: Vec<_> = bogus_transaction_bytes
442 .iter()
443 .map(|t| t.as_slice())
444 .collect();
445 let res_batch = validator.verify_and_vote_batch(&BlockRef::MIN, &batch);
448 assert!(res_batch.is_ok());
449 let rejections = res_batch.unwrap();
451 assert_eq!(
452 rejections.len(),
453 batch.len(),
454 "All bogus transactions should be rejected"
455 );
456 }
457
458 #[tokio::test]
459 async fn test_verify_and_vote_batch() {
460 let (sender, keypair) = deterministic_random_account_key();
462
463 let gas_objects: Vec<Object> = (0..8)
465 .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
466 .collect();
467
468 let owned_objects: Vec<Object> = (0..2)
470 .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
471 .collect();
472 let denied_object = owned_objects[1].clone();
473
474 let mut objects = gas_objects.clone();
475 objects.extend(owned_objects.clone());
476
477 let network_config =
478 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
479 .committee_size(NonZeroUsize::new(1).unwrap())
480 .with_objects(objects.clone())
481 .build();
482
483 let transaction_deny_config = TransactionDenyConfigBuilder::new()
485 .add_denied_object(denied_object.id())
486 .build();
487 let state = TestAuthorityBuilder::new()
488 .with_network_config(&network_config, 0)
489 .with_transaction_deny_config(transaction_deny_config)
490 .build()
491 .await;
492
493 let valid_transaction = test_user_transaction(
497 &state,
498 sender,
499 &keypair,
500 gas_objects[0].clone(),
501 vec![owned_objects[0].clone()],
502 )
503 .await;
504
505 let invalid_transaction = test_user_transaction(
507 &state,
508 sender,
509 &keypair,
510 gas_objects[1].clone(),
511 vec![denied_object.clone()],
512 )
513 .await;
514
515 let transactions = vec![valid_transaction, invalid_transaction];
517 let serialized_transactions: Vec<_> = transactions
518 .into_iter()
519 .map(|t| {
520 bcs::to_bytes(&ConsensusTransaction::new_user_transaction_v2_message(
521 &state.name,
522 t.into(),
523 ))
524 .unwrap()
525 })
526 .collect();
527 let batch: Vec<_> = serialized_transactions
528 .iter()
529 .map(|t| t.as_slice())
530 .collect();
531
532 let validator = SuiTxValidator::new(
533 state.clone(),
534 Arc::new(CheckpointServiceNoop {}),
535 SuiTxValidatorMetrics::new(&Default::default()),
536 );
537
538 let rejected_transactions = validator
540 .verify_and_vote_batch(&BlockRef::MAX, &batch)
541 .unwrap();
542
543 assert_eq!(rejected_transactions, vec![1]);
546
547 let epoch_store = state.load_epoch_store_one_call_per_task();
550 let reason = epoch_store
551 .get_rejection_vote_reason(ConsensusPosition {
552 epoch: state.load_epoch_store_one_call_per_task().epoch(),
553 block: BlockRef::MAX,
554 index: 1,
555 })
556 .expect("Rejection vote reason should be set");
557
558 assert_eq!(
559 reason,
560 SuiErrorKind::UserInputError {
561 error: UserInputError::TransactionDenied {
562 error: format!(
563 "Access to input object {:?} is temporarily disabled",
564 denied_object.id()
565 )
566 }
567 }
568 );
569 }
570
571 #[sim_test]
572 async fn reject_checkpoint_signature_v2_when_flag_disabled() {
573 let network_config =
575 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir().build();
576
577 let disabled_cfg =
578 ProtocolConfig::get_for_version(ProtocolVersion::new(92), Chain::Unknown);
579 let state = TestAuthorityBuilder::new()
580 .with_network_config(&network_config, 0)
581 .with_protocol_config(disabled_cfg)
582 .build()
583 .await;
584
585 let epoch_store = state.load_epoch_store_one_call_per_task();
586
587 let checkpoint_summary = CheckpointSummary::new(
589 &ProtocolConfig::get_for_max_version_UNSAFE(),
590 epoch_store.epoch(),
591 0,
592 0,
593 &CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]),
594 None,
595 Default::default(),
596 None,
597 0,
598 Vec::new(),
599 Vec::new(),
600 );
601
602 let keypair = network_config.validator_configs()[0].protocol_key_pair();
603 let authority = keypair.public().into();
604 let signed = SignedCheckpointSummary::new(
605 epoch_store.epoch(),
606 checkpoint_summary,
607 keypair,
608 authority,
609 );
610 let message = CheckpointSignatureMessage { summary: signed };
611
612 let tx = ConsensusTransaction::new_checkpoint_signature_message_v2(message);
613 let bytes = bcs::to_bytes(&tx).unwrap();
614
615 let validator = SuiTxValidator::new(
616 state.clone(),
617 Arc::new(CheckpointServiceNoop {}),
618 SuiTxValidatorMetrics::new(&Default::default()),
619 );
620
621 let res = validator.verify_batch(&[&bytes]);
622 assert!(res.is_err());
623 }
624
625 #[sim_test]
626 async fn accept_checkpoint_signature_v2_when_flag_enabled() {
627 let network_config =
629 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir().build();
630
631 let enabled_cfg = ProtocolConfig::get_for_version(ProtocolVersion::new(93), Chain::Unknown);
632 let state = TestAuthorityBuilder::new()
633 .with_network_config(&network_config, 0)
634 .with_protocol_config(enabled_cfg)
635 .build()
636 .await;
637
638 let epoch_store = state.load_epoch_store_one_call_per_task();
639
640 let checkpoint_summary = CheckpointSummary::new(
642 &ProtocolConfig::get_for_max_version_UNSAFE(),
643 epoch_store.epoch(),
644 0,
645 0,
646 &CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]),
647 None,
648 Default::default(),
649 None,
650 0,
651 Vec::new(),
652 Vec::new(),
653 );
654
655 let keypair = network_config.validator_configs()[0].protocol_key_pair();
656 let authority = keypair.public().into();
657 let signed = SignedCheckpointSummary::new(
658 epoch_store.epoch(),
659 checkpoint_summary,
660 keypair,
661 authority,
662 );
663 let message = CheckpointSignatureMessage { summary: signed };
664
665 let tx = ConsensusTransaction::new_checkpoint_signature_message_v2(message);
666 let bytes = bcs::to_bytes(&tx).unwrap();
667
668 let validator = SuiTxValidator::new(
669 state.clone(),
670 Arc::new(CheckpointServiceNoop {}),
671 SuiTxValidatorMetrics::new(&Default::default()),
672 );
673
674 let res = validator.verify_batch(&[&bytes]);
675 assert!(res.is_ok(), "{res:?}");
676 }
677
678 #[sim_test]
679 async fn accept_already_executed_transaction() {
680 let (sender, keypair) = deterministic_random_account_key();
681
682 let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
683 let owned_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
684
685 let network_config =
686 sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
687 .committee_size(NonZeroUsize::new(1).unwrap())
688 .with_objects(vec![gas_object.clone(), owned_object.clone()])
689 .build();
690
691 let state = TestAuthorityBuilder::new()
692 .with_network_config(&network_config, 0)
693 .build()
694 .await;
695
696 let epoch_store = state.load_epoch_store_one_call_per_task();
697
698 let transaction = test_user_transaction(
700 &state,
701 sender,
702 &keypair,
703 gas_object.clone(),
704 vec![owned_object.clone()],
705 )
706 .await
707 .into_tx();
708 let tx_digest = *transaction.digest();
709 let cert = VerifiedExecutableTransaction::new_from_consensus(transaction.clone(), 0);
710 let (executed_effects, _) = state
711 .try_execute_immediately(&cert, ExecutionEnv::new(), &state.epoch_store_for_testing())
712 .await
713 .unwrap();
714
715 let read_effects = state
717 .get_transaction_cache_reader()
718 .get_executed_effects(&tx_digest)
719 .expect("Transaction should be executed");
720 assert_eq!(read_effects, executed_effects);
721 assert_eq!(read_effects.executed_epoch(), epoch_store.epoch());
722
723 let serialized_tx = bcs::to_bytes(&ConsensusTransaction::new_user_transaction_message(
725 &state.name,
726 transaction.into_inner().clone(),
727 ))
728 .unwrap();
729 let validator = SuiTxValidator::new(
730 state.clone(),
731 Arc::new(CheckpointServiceNoop {}),
732 SuiTxValidatorMetrics::new(&Default::default()),
733 );
734 let rejected_transactions = validator
735 .verify_and_vote_batch(&BlockRef::MAX, &[&serialized_tx])
736 .expect("Verify and vote should succeed");
737
738 assert!(rejected_transactions.is_empty());
740 }
741}