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