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