Skip to main content

sui_core/unit_tests/
consensus_test_utils.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Common test utilities for consensus handler testing
5
6use std::collections::HashSet;
7use std::sync::Arc;
8
9use consensus_core::BlockStatus;
10use consensus_types::block::BlockRef;
11use itertools::Itertools;
12use parking_lot::Mutex;
13use prometheus::Registry;
14use sui_types::digests::{Digest, TransactionDigest};
15use sui_types::error::SuiResult;
16use sui_types::executable_transaction::VerifiedExecutableTransaction;
17use sui_types::messages_consensus::{
18    AuthorityIndex, ConsensusPosition, ConsensusTransaction, ConsensusTransactionKind,
19};
20use sui_types::sui_system_state::epoch_start_sui_system_state::EpochStartSystemStateTrait;
21use sui_types::transaction::VerifiedTransaction;
22
23use crate::authority::authority_per_epoch_store::{
24    AuthorityPerEpochStore, ExecutionIndicesWithStatsV2,
25};
26use crate::authority::backpressure::BackpressureManager;
27use crate::authority::shared_object_version_manager::Schedulable;
28use crate::authority::{AuthorityMetrics, AuthorityState, ExecutionEnv};
29use crate::consensus_adapter::{
30    BlockStatusReceiver, ConsensusAdapter, ConsensusAdapterMetrics, ConsensusClient,
31};
32use crate::consensus_handler::{
33    ConsensusHandler, ExecutionSchedulerSender, SequencedConsensusTransaction,
34    SequencedConsensusTransactionKind,
35};
36use crate::consensus_throughput_calculator::ConsensusThroughputCalculator;
37use crate::consensus_types::consensus_output_api::{ConsensusCommitAPI, ParsedTransaction};
38use crate::mock_consensus::with_block_status;
39
40pub(crate) type CapturedTransactions = Arc<Mutex<Vec<crate::consensus_handler::SchedulerMessage>>>;
41
42pub struct TestConsensusCommit {
43    pub transactions: Vec<ConsensusTransaction>,
44    pub round: u64,
45    pub timestamp_ms: u64,
46    pub sub_dag_index: u64,
47    /// Indices into `transactions` reported as rejected by consensus voting.
48    rejected_indices: HashSet<usize>,
49    transaction_authors: Option<Vec<consensus_config::AuthorityIndex>>,
50}
51
52impl TestConsensusCommit {
53    pub fn new(
54        transactions: Vec<ConsensusTransaction>,
55        round: u64,
56        timestamp_ms: u64,
57        sub_dag_index: u64,
58    ) -> Self {
59        Self {
60            transactions,
61            round,
62            timestamp_ms,
63            sub_dag_index,
64            rejected_indices: HashSet::new(),
65            transaction_authors: None,
66        }
67    }
68
69    pub fn empty(round: u64, timestamp_ms: u64, sub_dag_index: u64) -> Self {
70        Self::new(vec![], round, timestamp_ms, sub_dag_index)
71    }
72
73    pub fn with_rejected_indices(mut self, indices: impl IntoIterator<Item = usize>) -> Self {
74        self.rejected_indices = indices.into_iter().collect();
75        self
76    }
77
78    pub fn with_transaction_authors(mut self, authors: impl IntoIterator<Item = u32>) -> Self {
79        self.transaction_authors = Some(
80            authors
81                .into_iter()
82                .map(consensus_config::AuthorityIndex::new_for_test)
83                .collect(),
84        );
85        self
86    }
87}
88
89impl std::fmt::Display for TestConsensusCommit {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        write!(
92            f,
93            "TestConsensusCommitAPI(round={}, timestamp_ms={}, sub_dag_index={})",
94            self.round, self.timestamp_ms, self.sub_dag_index
95        )
96    }
97}
98
99impl ConsensusCommitAPI for TestConsensusCommit {
100    fn commit_ref(&self) -> consensus_core::CommitRef {
101        consensus_core::CommitRef::default()
102    }
103
104    fn leader_round(&self) -> u64 {
105        self.round
106    }
107
108    fn leader_author_index(&self) -> AuthorityIndex {
109        0
110    }
111
112    fn commit_timestamp_ms(&self) -> u64 {
113        self.timestamp_ms
114    }
115
116    fn commit_sub_dag_index(&self) -> u64 {
117        self.sub_dag_index
118    }
119
120    fn transactions(&self) -> Vec<(BlockRef, Vec<ParsedTransaction>)> {
121        if let Some(authors) = &self.transaction_authors {
122            return self
123                .transactions
124                .iter()
125                .zip_eq(authors)
126                .enumerate()
127                .map(|(i, (tx, author))| {
128                    let block_ref = BlockRef {
129                        author: *author,
130                        round: self.round as u32,
131                        digest: Default::default(),
132                    };
133                    let parsed_tx = ParsedTransaction {
134                        transaction: tx.clone(),
135                        rejected: self.rejected_indices.contains(&i),
136                        serialized_len: 0,
137                    };
138                    (block_ref, vec![parsed_tx])
139                })
140                .collect();
141        }
142
143        let block_ref = BlockRef {
144            author: consensus_config::AuthorityIndex::ZERO,
145            round: self.round as u32,
146            digest: Default::default(),
147        };
148
149        let parsed_txs: Vec<ParsedTransaction> = self
150            .transactions
151            .iter()
152            .enumerate()
153            .map(|(i, tx)| ParsedTransaction {
154                transaction: tx.clone(),
155                rejected: self.rejected_indices.contains(&i),
156                serialized_len: 0,
157            })
158            .collect();
159
160        vec![(block_ref, parsed_txs)]
161    }
162
163    fn rejected_transactions_digest(&self) -> Digest {
164        Digest::default()
165    }
166
167    fn rejected_transactions_debug_string(&self) -> String {
168        "no rejected transactions from TestConsensusCommit".to_string()
169    }
170}
171
172pub struct TestConsensusHandlerSetup<C> {
173    pub consensus_handler: ConsensusHandler<C>,
174    pub captured_transactions: CapturedTransactions,
175    pub metrics: Arc<AuthorityMetrics>,
176}
177
178/// Makes a consensus adapter with the standard test wiring (limits, metrics), backed by the
179/// given consensus client.
180pub fn make_consensus_adapter_with_client_for_test(
181    state: &Arc<AuthorityState>,
182    client: Arc<dyn ConsensusClient>,
183    max_pending_local_submissions: usize,
184) -> Arc<ConsensusAdapter> {
185    Arc::new(ConsensusAdapter::new(
186        client,
187        state.checkpoint_store.clone(),
188        state.name,
189        100_000,
190        max_pending_local_submissions,
191        ConsensusAdapterMetrics::new_test(),
192        Arc::new(tokio::sync::Notify::new()),
193    ))
194}
195
196pub fn make_consensus_adapter_for_test(
197    state: Arc<AuthorityState>,
198    process_via_checkpoint: HashSet<TransactionDigest>,
199    execute: bool,
200    mock_block_status_receivers: Vec<BlockStatusReceiver>,
201) -> Arc<ConsensusAdapter> {
202    make_consensus_adapter_for_test_with_submit_limit(
203        state,
204        process_via_checkpoint,
205        execute,
206        mock_block_status_receivers,
207        100_000,
208    )
209}
210
211pub fn make_consensus_adapter_for_test_with_submit_limit(
212    state: Arc<AuthorityState>,
213    process_via_checkpoint: HashSet<TransactionDigest>,
214    execute: bool,
215    mock_block_status_receivers: Vec<BlockStatusReceiver>,
216    max_pending_local_submissions: usize,
217) -> Arc<ConsensusAdapter> {
218    #[derive(Clone)]
219    struct SubmitDirectly {
220        state: Arc<AuthorityState>,
221        process_via_checkpoint: HashSet<TransactionDigest>,
222        execute: bool,
223        mock_block_status_receivers: Arc<Mutex<Vec<BlockStatusReceiver>>>,
224    }
225
226    #[async_trait::async_trait]
227    impl ConsensusClient for SubmitDirectly {
228        async fn submit(
229            &self,
230            transactions: &[ConsensusTransaction],
231            epoch_store: &Arc<AuthorityPerEpochStore>,
232        ) -> SuiResult<(Vec<ConsensusPosition>, BlockStatusReceiver)> {
233            // If transactions are empty, then we are performing a ping check and will attempt to ping consensus and simulate a transaction submission to consensus.
234            if transactions.is_empty() {
235                return Ok((
236                    vec![ConsensusPosition::ping(epoch_store.epoch(), BlockRef::MIN)],
237                    with_block_status(BlockStatus::Sequenced(BlockRef::MIN)),
238                ));
239            }
240
241            let num_transactions = transactions.len();
242            let mut executed_via_checkpoint = 0;
243
244            // Simple processing - just mark transactions for checkpoint execution if needed
245            for txn in transactions {
246                if let ConsensusTransactionKind::UserTransactionV2(tx) = &txn.kind {
247                    let transaction_digest = tx.tx().digest();
248                    if self.process_via_checkpoint.contains(transaction_digest) {
249                        epoch_store
250                            .insert_finalized_transactions(vec![*transaction_digest].as_slice(), 10)
251                            .expect("Should not fail");
252                        executed_via_checkpoint += 1;
253                    }
254                }
255            }
256
257            let sequenced_transactions: Vec<SequencedConsensusTransaction> = transactions
258                .iter()
259                .map(|txn| SequencedConsensusTransaction::new_test(txn.clone()))
260                .collect();
261
262            let keys = sequenced_transactions
263                .iter()
264                .map(|tx| tx.key())
265                .collect::<Vec<_>>();
266
267            // Only execute transactions if explicitly requested and not via checkpoint
268            if self.execute {
269                for tx in sequenced_transactions {
270                    if let Some(transaction_digest) = tx.transaction.executable_transaction_digest()
271                    {
272                        // Skip if already executed via checkpoint
273                        if self.process_via_checkpoint.contains(&transaction_digest) {
274                            continue;
275                        }
276
277                        // Extract executable transaction from consensus transaction
278                        let executable_tx = match &tx.transaction {
279                            SequencedConsensusTransactionKind::External(ext) => match &ext.kind {
280                                ConsensusTransactionKind::UserTransactionV2(tx) => {
281                                    Some(VerifiedExecutableTransaction::new_from_consensus(
282                                        VerifiedTransaction::new_unchecked(tx.tx().clone()),
283                                        0,
284                                    ))
285                                }
286                                _ => None,
287                            },
288                            SequencedConsensusTransactionKind::System(sys_tx) => {
289                                Some(sys_tx.clone())
290                            }
291                        };
292
293                        if let Some(exec_tx) = executable_tx {
294                            let versions = epoch_store.assign_shared_object_versions_for_tests(
295                                self.state.get_object_cache_reader().as_ref(),
296                                std::slice::from_ref(&exec_tx),
297                            )?;
298
299                            let assigned_version = versions
300                                .into_map()
301                                .into_iter()
302                                .next()
303                                .map(|(_, v)| v)
304                                .unwrap_or_default();
305
306                            self.state.execution_scheduler().enqueue(
307                                vec![(
308                                    Schedulable::Transaction(exec_tx),
309                                    ExecutionEnv::new().with_assigned_versions(assigned_version),
310                                )],
311                                epoch_store,
312                            );
313                        }
314                    }
315                }
316            }
317
318            epoch_store.process_notifications(keys.iter());
319
320            assert_eq!(
321                executed_via_checkpoint,
322                self.process_via_checkpoint.len(),
323                "Some transactions were not executed via checkpoint"
324            );
325
326            assert!(
327                !self.mock_block_status_receivers.lock().is_empty(),
328                "No mock submit responses left"
329            );
330
331            let mut consensus_positions = Vec::new();
332            for index in 0..num_transactions {
333                consensus_positions.push(ConsensusPosition {
334                    epoch: epoch_store.epoch(),
335                    index: index as u16,
336                    block: BlockRef::MIN,
337                });
338            }
339
340            Ok((
341                consensus_positions,
342                self.mock_block_status_receivers.lock().remove(0),
343            ))
344        }
345    }
346    // Make a new consensus adapter instance.
347    let client = Arc::new(SubmitDirectly {
348        state: state.clone(),
349        process_via_checkpoint,
350        execute,
351        mock_block_status_receivers: Arc::new(Mutex::new(mock_block_status_receivers)),
352    });
353    make_consensus_adapter_with_client_for_test(&state, client, max_pending_local_submissions)
354}
355
356/// Creates a ConsensusHandler for testing with a mock ExecutionSchedulerSender that captures transactions
357pub async fn setup_consensus_handler_for_testing_with_checkpoint_service<C>(
358    authority: &Arc<AuthorityState>,
359    checkpoint_service: Arc<C>,
360) -> TestConsensusHandlerSetup<C>
361where
362    C: Send + Sync + 'static,
363{
364    let epoch_store = authority.epoch_store_for_testing();
365    let consensus_committee = epoch_store.epoch_start_state().get_consensus_committee();
366    let metrics = Arc::new(AuthorityMetrics::new(&Registry::new()));
367    let throughput_calculator = ConsensusThroughputCalculator::new(None, metrics.clone());
368    let backpressure_manager = BackpressureManager::new_for_tests();
369    let last_consensus_stats = ExecutionIndicesWithStatsV2 {
370        stats: crate::authority::authority_per_epoch_store::ConsensusStats::new(
371            consensus_committee.size(),
372        ),
373        ..Default::default()
374    };
375
376    let captured_transactions: CapturedTransactions = Arc::new(Mutex::new(Vec::new()));
377    let captured_tx_clone = captured_transactions.clone();
378
379    let (tx_sender, mut receiver) =
380        mysten_metrics::monitored_mpsc::unbounded_channel("test_execution_scheduler");
381
382    tokio::spawn(async move {
383        while let Some(item) = receiver.recv().await {
384            captured_tx_clone.lock().push(item);
385        }
386    });
387
388    let execution_scheduler_sender = ExecutionSchedulerSender::new_for_testing(tx_sender);
389
390    let consensus_handler = ConsensusHandler::new_for_testing(
391        epoch_store.clone(),
392        checkpoint_service,
393        execution_scheduler_sender,
394        authority.get_object_cache_reader().clone(),
395        consensus_committee,
396        metrics.clone(),
397        Arc::new(throughput_calculator),
398        backpressure_manager.subscribe(),
399        authority.traffic_controller.clone(),
400        authority.transaction_deny_config_manager().clone(),
401        last_consensus_stats,
402    );
403
404    TestConsensusHandlerSetup {
405        consensus_handler,
406        captured_transactions,
407        metrics,
408    }
409}
410
411/// Creates a ConsensusHandler for testing with CheckpointServiceNoop
412#[cfg(test)]
413pub async fn setup_consensus_handler_for_testing(
414    authority: &Arc<AuthorityState>,
415) -> TestConsensusHandlerSetup<crate::checkpoints::CheckpointServiceNoop> {
416    setup_consensus_handler_for_testing_with_checkpoint_service(
417        authority,
418        Arc::new(crate::checkpoints::CheckpointServiceNoop {}),
419    )
420    .await
421}