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,
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    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                    // TODO: Batch signature verification for UserTransactionV1.
110                    //  For now verify individually, but this should be batched for performance
111                    //  similar to how certificates are batch-verified above.
112                    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                // No explicit size cap on the proposed rules: the proposal is
142                // bounded by `max_transaction_size_bytes` (256 KiB), enforced
143                // on every received block by starfish's block verifier.
144                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        // verify the certificate signatures as a batch
156        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        // All checkpoint sigs have been verified, forward them to the checkpoint
166        // service
167        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        // todo - we should un-comment line below once we have a way to revert
187        // those transactions at the end of epoch all certificates had
188        // valid signatures, schedule them for execution prior to sequencing
189        // which is unnecessary for owned object transactions.
190        // It is unnecessary to write to pending_certificates table because the
191        // certs will be written via consensus output.
192        // self.execution_scheduler
193        //     .enqueue_certificates(owned_tx_certs, &self.epoch_store)
194        //     .wrap_err("Failed to schedule certificates for execution")
195    }
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        // Initialize an authority with a (owned) gas object and a shared object; then
292        // make a test certificate.
293        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                // set it to an all-zero user signature
340                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    /// Verifies that `validate_transactions` correctly gates every
355    /// `ConsensusTransactionKind` variant against the current protocol config's
356    /// feature flags.
357    ///
358    /// The exhaustive match forces a compile error when new variants are added,
359    /// so the developer must explicitly map each variant to its gating flag.
360    ///
361    /// NOTE: This test is primarily useful for variants that are under
362    /// development or not yet fully rolled out on all networks. Once all
363    /// feature-gated variants are enabled on every network, this test can be
364    /// ignored — its value lies in catching missing gates during the upgrade
365    /// window between code deployment and protocol activation.
366    #[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        // Variants that can be validated without signature verification setup
407        // (or that carry a valid signature, like UserTransactionV1).
408        // CertifiedTransaction, CheckpointSignature, and
409        // SignedCapabilityNotificationV1 are excluded because they require valid
410        // cryptographic signatures and would fail before reaching the feature
411        // gate check; their gating is recorded in
412        // `consensus_transaction_feature_gate`.
413        #[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                    // Feature flag is disabled: must be rejected.
482                    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                    // Feature flag is enabled or variant is ungated: must not
490                    // be rejected as unsupported.
491                    assert!(
492                        !matches!(&result, Err(IotaError::UnsupportedFeature { .. })),
493                        "{name}: should not be rejected as UnsupportedFeature, got {result:?}",
494                    );
495                }
496            }
497        }
498    }
499}