Skip to main content

iota_core/
consensus_validator.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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/// Allows verifying the validity of transactions
26#[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                    // TODO: Batch signature verification for UserTransactionV1.
113                    //  For now verify individually, but this should be batched for performance
114                    //  similar to how certificates are batch-verified above.
115                    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                // No explicit size cap on the proposed rules: the proposal is
145                // bounded by `max_transaction_size_bytes` (256 KiB), enforced
146                // on every received block by starfish's block verifier.
147                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        // verify the certificate signatures as a batch
159        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        // All checkpoint sigs have been verified, forward them to the checkpoint
169        // service
170        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        // todo - we should un-comment line below once we have a way to revert
190        // those transactions at the end of epoch all certificates had
191        // valid signatures, schedule them for execution prior to sequencing
192        // which is unnecessary for owned object transactions.
193        // It is unnecessary to write to pending_certificates table because the
194        // certs will be written via consensus output.
195        // self.transaction_manager
196        //     .enqueue_certificates(owned_tx_certs, &self.epoch_store)
197        //     .wrap_err("Failed to schedule certificates for execution")
198    }
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        // Initialize an authority with a (owned) gas object and a shared object; then
294        // make a test certificate.
295        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                // set it to an all-zero user signature
343                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    /// Verifies that `validate_transactions` correctly gates every
358    /// `ConsensusTransactionKind` variant against the current protocol config's
359    /// feature flags.
360    ///
361    /// The exhaustive match forces a compile error when new variants are added,
362    /// so the developer must explicitly map each variant to its gating flag.
363    ///
364    /// NOTE: This test is primarily useful for variants that are under
365    /// development or not yet fully rolled out on all networks. Once all
366    /// feature-gated variants are enabled on every network, this test can be
367    /// ignored — its value lies in catching missing gates during the upgrade
368    /// window between code deployment and protocol activation.
369    #[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        // Returns the feature flag value that gates a variant, or `None` if the
411        // variant is always allowed. The exhaustive match ensures this function
412        // must be updated when new variants are added to ConsensusTransactionKind.
413        #[allow(deprecated)]
414        fn is_feature_gated(
415            kind: &ConsensusTransactionKind,
416            config: &ProtocolConfig,
417        ) -> Option<bool> {
418            match kind {
419                // Always allowed (no feature flag gating).
420                ConsensusTransactionKind::CertifiedTransaction(_)
421                | ConsensusTransactionKind::CheckpointSignature(_)
422                | ConsensusTransactionKind::EndOfPublish(_)
423                | ConsensusTransactionKind::CapabilityNotificationV1(_)
424                | ConsensusTransactionKind::SignedCapabilityNotificationV1(_)
425                | ConsensusTransactionKind::RandomnessDkgMessage(_, _)
426                | ConsensusTransactionKind::RandomnessDkgConfirmation(_, _) => None,
427
428                // Gated behind `enable_pcool_flow`.
429                ConsensusTransactionKind::UserTransactionV1(_) => Some(config.enable_pcool_flow()),
430
431                // Gated behind `calculate_validator_scores`.
432                ConsensusTransactionKind::MisbehaviorReport(_) => {
433                    Some(config.calculate_validator_scores())
434                }
435
436                // Always rejected: zkLogin JWK support was never enabled on
437                // IOTA and the variant is retained only for serialization
438                // compatibility.
439                ConsensusTransactionKind::NewJWKFetchedDeprecated => Some(false),
440
441                // Gated behind `enable_pcool_flow`.
442                ConsensusTransactionKind::OverloadNotificationV1(_, _, _) => {
443                    Some(config.enable_pcool_flow())
444                }
445
446                // Gated behind `deny_rule_governance`.
447                ConsensusTransactionKind::TransactionDenyRuleProposal(_) => {
448                    Some(config.deny_rule_governance())
449                }
450            }
451        }
452
453        // Variants that can be validated without signature verification setup
454        // (or that carry a valid signature, like UserTransactionV1).
455        // CertifiedTransaction, CheckpointSignature, and
456        // SignedCapabilityNotificationV1 are excluded because they require valid
457        // cryptographic signatures and would fail before reaching the feature
458        // gate check; their gating is verified by the exhaustive match above.
459        #[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                    // Feature flag is disabled: must be rejected.
528                    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                    // Feature flag is enabled or variant is ungated: must not
536                    // be rejected as unsupported.
537                    assert!(
538                        !matches!(&result, Err(IotaError::UnsupportedFeature { .. })),
539                        "{name}: should not be rejected as UnsupportedFeature, got {result:?}",
540                    );
541                }
542            }
543        }
544    }
545}