1use std::sync::Arc;
6
7use eyre::WrapErr;
8use fastcrypto_tbls::dkg_v1;
9use iota_metrics::monitored_scope;
10use iota_types::{
11 base_types::ConciseableName,
12 error::IotaError,
13 messages_consensus::{ConsensusTransaction, ConsensusTransactionKind},
14};
15use prometheus_filtered::{IntCounter, Registry, register_int_counter_with_registry};
16use starfish_core;
17use tap::TapFallible;
18use tracing::{info, instrument, warn};
19
20use crate::{
21 authority::authority_per_epoch_store::AuthorityPerEpochStore,
22 checkpoints::CheckpointServiceNotify, transaction_manager::TransactionManager,
23};
24
25#[derive(Clone)]
27pub struct IotaTxValidator {
28 epoch_store: Arc<AuthorityPerEpochStore>,
29 checkpoint_service: Arc<dyn CheckpointServiceNotify + Send + Sync>,
30 _transaction_manager: Arc<TransactionManager>,
31 metrics: Arc<IotaTxValidatorMetrics>,
32}
33
34impl IotaTxValidator {
35 pub fn new(
36 epoch_store: Arc<AuthorityPerEpochStore>,
37 checkpoint_service: Arc<dyn CheckpointServiceNotify + Send + Sync>,
38 transaction_manager: Arc<TransactionManager>,
39 metrics: Arc<IotaTxValidatorMetrics>,
40 ) -> Self {
41 info!(
42 "IotaTxValidator constructed for epoch {}",
43 epoch_store.epoch()
44 );
45 Self {
46 epoch_store,
47 checkpoint_service,
48 _transaction_manager: transaction_manager,
49 metrics,
50 }
51 }
52
53 #[instrument(level = "trace", skip_all)]
54 fn validate_transactions(&self, txs: Vec<ConsensusTransactionKind>) -> Result<(), IotaError> {
55 let mut cert_batch = Vec::new();
56 let mut ckpt_messages = Vec::new();
57 let mut ckpt_batch = Vec::new();
58 let mut authority_cap_batch = Vec::new();
59 let mut user_tx_v1_count: u64 = 0;
60
61 for tx in txs.iter() {
62 match tx {
63 ConsensusTransactionKind::CertifiedTransaction(certificate) => {
64 cert_batch.push(certificate.as_ref());
65 }
66 ConsensusTransactionKind::CheckpointSignature(signature) => {
67 ckpt_messages.push(signature.as_ref());
68 ckpt_batch.push(&signature.summary);
69 }
70 ConsensusTransactionKind::RandomnessDkgMessage(_, bytes) => {
71 if bytes.len() > dkg_v1::DKG_MESSAGES_MAX_SIZE {
72 warn!("batch verification error: DKG Message too large");
73 return Err(IotaError::InvalidDkgMessageSize);
74 }
75 }
76 ConsensusTransactionKind::RandomnessDkgConfirmation(_, bytes) => {
77 if bytes.len() > dkg_v1::DKG_MESSAGES_MAX_SIZE {
78 warn!("batch verification error: DKG Confirmation too large");
79 return Err(IotaError::InvalidDkgMessageSize);
80 }
81 }
82 ConsensusTransactionKind::SignedCapabilityNotificationV1(signed_cap) => {
83 authority_cap_batch.push(signed_cap);
84 }
85
86 ConsensusTransactionKind::MisbehaviorReport(_) => {
87 if !self
88 .epoch_store
89 .protocol_config()
90 .calculate_validator_scores()
91 {
92 return Err(IotaError::UnsupportedFeature {
93 error: "MisbehaviorReport not supported at current protocol version"
94 .into(),
95 });
96 }
97 }
98 #[allow(deprecated)]
99 ConsensusTransactionKind::NewJWKFetchedDeprecated => {
100 return Err(IotaError::UnsupportedFeature {
101 error: "NewJWKFetched (zkLogin) is deprecated and not supported".into(),
102 });
103 }
104
105 ConsensusTransactionKind::UserTransactionV1(transaction) => {
106 if !self.epoch_store.protocol_config().enable_pcool_flow() {
107 return Err(IotaError::UnsupportedFeature {
108 error: "UserTransactionV1 not supported at current protocol version"
109 .into(),
110 });
111 }
112 self.epoch_store
116 .signature_verifier
117 .verify_tx(transaction.data())
118 .tap_err(|e| {
119 warn!("UserTransactionV1 signature verification failed: {}", e)
120 })?;
121 user_tx_v1_count += 1;
122 }
123
124 ConsensusTransactionKind::EndOfPublish(_)
125 | ConsensusTransactionKind::CapabilityNotificationV1(_) => {}
126
127 ConsensusTransactionKind::OverloadNotificationV1(authority_name, _, percentage) => {
128 if !self.epoch_store.protocol_config().enable_pcool_flow() {
129 return Err(IotaError::UnsupportedFeature {
130 error:
131 "OverloadNotificationV1 not supported at current protocol version"
132 .into(),
133 });
134 }
135 if *percentage > 100 {
136 return Err(IotaError::HandleConsensusTransactionFailure(format!(
137 "OverloadNotificationV1 with invalid percentage {percentage} from \
138 authority {}",
139 authority_name.concise(),
140 )));
141 }
142 }
143
144 ConsensusTransactionKind::TransactionDenyRuleProposal(_) => {
148 if !self.epoch_store.protocol_config().deny_rule_governance() {
149 return Err(IotaError::UnsupportedFeature {
150 error: "TransactionDenyRuleProposal not supported at current protocol version"
151 .into(),
152 });
153 }
154 }
155 }
156 }
157
158 let cert_count = cert_batch.len();
160 let ckpt_count = ckpt_batch.len();
161 let authority_cap_count = authority_cap_batch.len();
162
163 self.epoch_store
164 .signature_verifier
165 .verify_certs_and_checkpoints(cert_batch, ckpt_batch, authority_cap_batch)
166 .tap_err(|e| warn!("batch verification error: {}", e))?;
167
168 for ckpt in ckpt_messages {
171 self.checkpoint_service
172 .notify_checkpoint_signature(&self.epoch_store, ckpt)?;
173 }
174
175 self.metrics
176 .certificate_signatures_verified
177 .inc_by(cert_count as u64);
178 self.metrics
179 .checkpoint_signatures_verified
180 .inc_by(ckpt_count as u64);
181 self.metrics
182 .authority_capabilities_verified
183 .inc_by(authority_cap_count as u64);
184 self.metrics
185 .user_transaction_signatures_verified
186 .inc_by(user_tx_v1_count);
187 Ok(())
188
189 }
199}
200
201fn tx_from_bytes(tx: &[u8]) -> Result<ConsensusTransaction, eyre::Report> {
202 bcs::from_bytes::<ConsensusTransaction>(tx)
203 .wrap_err("Malformed transaction (failed to deserialize)")
204}
205
206impl starfish_core::TransactionVerifier for IotaTxValidator {
207 #[instrument(level = "trace", skip_all)]
208 fn verify_batch(
209 &self,
210 batch: &[&[u8]],
211 ) -> core::result::Result<(), starfish_core::ValidationError> {
212 let _scope = monitored_scope("ValidateBatch");
213
214 let txs = batch
215 .iter()
216 .map(|tx| {
217 tx_from_bytes(tx)
218 .map(|tx| tx.kind)
219 .map_err(|e| starfish_core::ValidationError::InvalidTransaction(e.to_string()))
220 })
221 .collect::<core::result::Result<Vec<_>, _>>()?;
222
223 self.validate_transactions(txs)
224 .map_err(|e| starfish_core::ValidationError::InvalidTransaction(e.to_string()))
225 }
226}
227
228pub struct IotaTxValidatorMetrics {
229 certificate_signatures_verified: IntCounter,
230 checkpoint_signatures_verified: IntCounter,
231 authority_capabilities_verified: IntCounter,
232 user_transaction_signatures_verified: IntCounter,
233}
234
235impl IotaTxValidatorMetrics {
236 pub fn new(registry: &Registry) -> Arc<Self> {
237 Arc::new(Self {
238 certificate_signatures_verified: register_int_counter_with_registry!(
239 "certificate_signatures_verified",
240 "Number of certificates verified in consensus batch verifier",
241 registry
242 )
243 .unwrap(),
244 checkpoint_signatures_verified: register_int_counter_with_registry!(
245 "checkpoint_signatures_verified",
246 "Number of checkpoint verified in consensus batch verifier",
247 registry
248 )
249 .unwrap(),
250 authority_capabilities_verified: register_int_counter_with_registry!(
251 "authority_capabilities_verified",
252 "Number of signed authority capabilities verified in consensus batch verifier",
253 registry
254 )
255 .unwrap(),
256 user_transaction_signatures_verified: register_int_counter_with_registry!(
257 "user_transaction_signatures_verified",
258 "Number of UserTransactionV1 signatures verified in consensus validator",
259 registry
260 )
261 .unwrap(),
262 })
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use std::sync::Arc;
269
270 use iota_macros::sim_test;
271 use iota_protocol_config::Chain;
272 use iota_sdk_types::{ObjectId, UserSignature};
273 use iota_types::{
274 error::IotaError,
275 messages_consensus::{
276 ConsensusTransaction, ConsensusTransactionKind, MisbehaviorObservationsV1,
277 VersionedMisbehaviorReport,
278 },
279 object::Object,
280 transaction::SenderSignedTransactionAPI,
281 };
282 use starfish_core::TransactionVerifier as _;
283
284 use crate::{
285 authority::test_authority_builder::TestAuthorityBuilder,
286 checkpoints::CheckpointServiceNoop,
287 consensus_adapter::consensus_tests::{test_certificates, test_gas_objects},
288 consensus_validator::{IotaTxValidator, IotaTxValidatorMetrics},
289 };
290
291 #[sim_test]
292 async fn accept_valid_transaction() {
293 let mut objects = test_gas_objects();
296 let shared_object = Object::shared_for_testing();
297 objects.push(shared_object.clone());
298
299 let network_config =
300 iota_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
301 .with_objects(objects.clone())
302 .build();
303
304 let state = TestAuthorityBuilder::new()
305 .with_network_config(&network_config, 0)
306 .build()
307 .await;
308 let name1 = state.name;
309 let certificates = test_certificates(&state, shared_object).await;
310
311 let first_transaction = certificates[0].clone();
312 let first_transaction_bytes: Vec<u8> = bcs::to_bytes(
313 &ConsensusTransaction::new_certificate_message(&name1, first_transaction),
314 )
315 .unwrap();
316
317 let metrics = IotaTxValidatorMetrics::new(&Default::default());
318 let validator = IotaTxValidator::new(
319 state.epoch_store_for_testing().clone(),
320 Arc::new(CheckpointServiceNoop {}),
321 state.transaction_manager().clone(),
322 metrics,
323 );
324 let res = validator.verify_batch(&[&first_transaction_bytes]);
325 assert!(res.is_ok(), "{res:?}");
326
327 let transaction_bytes: Vec<_> = certificates
328 .clone()
329 .into_iter()
330 .map(|cert| {
331 bcs::to_bytes(&ConsensusTransaction::new_certificate_message(&name1, cert)).unwrap()
332 })
333 .collect();
334
335 let batch: Vec<_> = transaction_bytes.iter().map(|t| t.as_slice()).collect();
336 let res_batch = validator.verify_batch(&batch);
337 assert!(res_batch.is_ok(), "{res_batch:?}");
338
339 let bogus_transaction_bytes: Vec<_> = certificates
340 .into_iter()
341 .map(|mut cert| {
342 cert.tx_signatures_mut_for_testing()[0] =
344 UserSignature::Simple(iota_types::crypto::zero_ed25519_signature());
345 bcs::to_bytes(&ConsensusTransaction::new_certificate_message(&name1, cert)).unwrap()
346 })
347 .collect();
348
349 let batch: Vec<_> = bogus_transaction_bytes
350 .iter()
351 .map(|t| t.as_slice())
352 .collect();
353 let res_batch = validator.verify_batch(&batch);
354 assert!(res_batch.is_err());
355 }
356
357 #[sim_test]
370 async fn validate_transactions_feature_gating() {
371 use iota_protocol_config::ProtocolConfig;
372 use iota_types::crypto::{
373 AccountKeyPair, AuthorityPublicKeyBytes, deterministic_random_account_key,
374 };
375
376 use crate::test_utils::make_transfer_iota_transaction;
377
378 let (sender, sender_key): (_, AccountKeyPair) = deterministic_random_account_key();
379 let gas_object_id = ObjectId::random();
380 let gas_object = Object::with_id_owner_for_testing(gas_object_id, sender);
381
382 let network_config =
383 iota_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
384 .with_objects(vec![gas_object.clone()])
385 .build();
386
387 let state = TestAuthorityBuilder::new()
388 .with_network_config(&network_config, 0)
389 .with_chain_override(Chain::Mainnet)
390 .build()
391 .await;
392
393 let rgp = state.epoch_store_for_testing().reference_gas_price();
394 let gas_ref = state.get_object(&gas_object_id).unwrap().object_ref();
395 let recipient = iota_types::crypto::get_key_pair::<AccountKeyPair>().0;
396 let signed_tx =
397 make_transfer_iota_transaction(gas_ref, recipient, None, sender, &sender_key, rgp);
398
399 let metrics = IotaTxValidatorMetrics::new(&Default::default());
400 let validator = IotaTxValidator::new(
401 state.epoch_store_for_testing().clone(),
402 Arc::new(CheckpointServiceNoop {}),
403 state.transaction_manager().clone(),
404 metrics,
405 );
406
407 let protocol_config = validator.epoch_store.protocol_config();
408 let authority = AuthorityPublicKeyBytes::default();
409
410 #[allow(deprecated)]
414 fn is_feature_gated(
415 kind: &ConsensusTransactionKind,
416 config: &ProtocolConfig,
417 ) -> Option<bool> {
418 match kind {
419 ConsensusTransactionKind::CertifiedTransaction(_)
421 | ConsensusTransactionKind::CheckpointSignature(_)
422 | ConsensusTransactionKind::EndOfPublish(_)
423 | ConsensusTransactionKind::CapabilityNotificationV1(_)
424 | ConsensusTransactionKind::SignedCapabilityNotificationV1(_)
425 | ConsensusTransactionKind::RandomnessDkgMessage(_, _)
426 | ConsensusTransactionKind::RandomnessDkgConfirmation(_, _) => None,
427
428 ConsensusTransactionKind::UserTransactionV1(_) => Some(config.enable_pcool_flow()),
430
431 ConsensusTransactionKind::MisbehaviorReport(_) => {
433 Some(config.calculate_validator_scores())
434 }
435
436 ConsensusTransactionKind::NewJWKFetchedDeprecated => Some(false),
440
441 ConsensusTransactionKind::OverloadNotificationV1(_, _, _) => {
443 Some(config.enable_pcool_flow())
444 }
445
446 ConsensusTransactionKind::TransactionDenyRuleProposal(_) => {
448 Some(config.deny_rule_governance())
449 }
450 }
451 }
452
453 #[allow(deprecated)]
460 let testable_variants: Vec<(&str, ConsensusTransactionKind)> = vec![
461 (
462 "EndOfPublish",
463 ConsensusTransactionKind::EndOfPublish(authority),
464 ),
465 (
466 "NewJWKFetchedDeprecated",
467 ConsensusTransactionKind::NewJWKFetchedDeprecated,
468 ),
469 (
470 "CapabilityNotificationV1",
471 ConsensusTransactionKind::CapabilityNotificationV1(
472 iota_types::messages_consensus::AuthorityCapabilitiesV1::new(
473 authority,
474 Chain::Mainnet,
475 iota_types::supported_protocol_versions::SupportedProtocolVersions::SYSTEM_DEFAULT,
476 vec![],
477 ),
478 ),
479 ),
480 (
481 "RandomnessDkgMessage",
482 ConsensusTransactionKind::RandomnessDkgMessage(authority, vec![]),
483 ),
484 (
485 "RandomnessDkgConfirmation",
486 ConsensusTransactionKind::RandomnessDkgConfirmation(authority, vec![]),
487 ),
488 (
489 "MisbehaviorReport",
490 ConsensusTransactionKind::MisbehaviorReport(VersionedMisbehaviorReport::new_v1(
491 authority,
492 0,
493 MisbehaviorObservationsV1 {
494 faulty_blocks_provable: vec![],
495 faulty_blocks_unprovable: vec![],
496 missing_proposals: vec![],
497 equivocations: vec![],
498 },
499 )),
500 ),
501 (
502 "UserTransactionV1",
503 ConsensusTransactionKind::UserTransactionV1(Box::new(signed_tx)),
504 ),
505 (
506 "OverloadNotificationV1",
507 ConsensusTransactionKind::OverloadNotificationV1(authority, 0, 50),
508 ),
509 (
510 "TransactionDenyRuleProposal",
511 ConsensusTransactionKind::TransactionDenyRuleProposal(
512 iota_types::messages_consensus::TransactionDenyRuleProposal {
513 authority,
514 generation: 0,
515 proposed_rules: Default::default(),
516 },
517 ),
518 ),
519 ];
520
521 for (name, kind) in testable_variants {
522 let gated = is_feature_gated(&kind, protocol_config);
523 let result = validator.validate_transactions(vec![kind]);
524
525 match gated {
526 Some(false) => {
527 assert!(
529 matches!(&result, Err(IotaError::UnsupportedFeature { .. })),
530 "{name}: feature flag is disabled, expected UnsupportedFeature, \
531 got {result:?}",
532 );
533 }
534 Some(true) | None => {
535 assert!(
538 !matches!(&result, Err(IotaError::UnsupportedFeature { .. })),
539 "{name}: should not be rejected as UnsupportedFeature, got {result:?}",
540 );
541 }
542 }
543 }
544 }
545}