Skip to main content

sui_core/consensus_manager/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3use crate::authority::authority_per_epoch_store::AuthorityPerEpochStore;
4use crate::consensus_adapter::{BlockStatusReceiver, ConsensusClient};
5use crate::consensus_handler::{ConsensusHandlerInitializer, MysticetiConsensusHandler};
6use crate::consensus_transaction_pool::{
7    ConsensusTransactionPool, TransactionPoolClient, TransactionPoolContext,
8};
9use crate::consensus_validator::SuiTxValidator;
10use crate::mysticeti_adapter::LazyMysticetiClient;
11use arc_swap::ArcSwapOption;
12use async_trait::async_trait;
13use consensus_config::{
14    ChainType, Committee, ConsensusProtocolConfig, NetworkKeyPair,
15    NetworkPublicKey as ConsensusNetworkPublicKey, Parameters, ProtocolKeyPair, Stake,
16};
17use consensus_core::{
18    Clock, CommitConsumerArgs, CommitIndex, ConsensusAuthority, ConsumerMonitor, NetworkType,
19    RandomnessSignatureHandler, TransactionPool, storage::rocksdb_store::RocksDBStore,
20};
21use core::panic;
22use fastcrypto::encoding::{Encoding, Hex};
23use fastcrypto::traits::KeyPair as _;
24use mysten_common::debug_fatal;
25use mysten_metrics::{RegistryID, RegistryService};
26use mysten_network::Multiaddr;
27use prometheus::{
28    IntGauge, IntGaugeVec, Registry, register_int_gauge_vec_with_registry,
29    register_int_gauge_with_registry,
30};
31use std::collections::BTreeMap;
32use std::path::PathBuf;
33use std::sync::Arc;
34use std::time::{Duration, Instant};
35use sui_config::node::ConsensusTransactionPoolConfig;
36use sui_config::{ConsensusConfig, NodeConfig};
37use sui_network::endpoint_manager::{AddressSource, ConsensusAddressUpdater};
38use sui_protocol_config::{Chain, ProtocolConfig, ProtocolVersion};
39use sui_types::crypto::NetworkPublicKey;
40use sui_types::error::{SuiErrorKind, SuiResult};
41use sui_types::messages_consensus::{ConsensusPosition, ConsensusTransaction};
42use sui_types::node_role::NodeRole;
43use sui_types::{
44    committee::EpochId, sui_system_state::epoch_start_sui_system_state::EpochStartSystemStateTrait,
45};
46use tokio::sync::{Mutex, broadcast};
47use tokio::time::{sleep, timeout};
48use tracing::{error, info, warn};
49
50#[cfg(test)]
51#[path = "../unit_tests/consensus_manager_tests.rs"]
52pub mod consensus_manager_tests;
53
54#[derive(PartialEq)]
55enum Running {
56    True(EpochId, ProtocolVersion),
57    False,
58}
59
60/// Stores address updates that should be persisted across epoch changes.
61/// We store the consensus NetworkPublicKey to avoid repeated conversions.
62struct AddressOverridesMap {
63    // We store the AddressSource on a BTreeMap as it helps accessing the keys in priority order.
64    map: BTreeMap<
65        ConsensusNetworkPublicKey,
66        BTreeMap<sui_network::endpoint_manager::AddressSource, Vec<Multiaddr>>,
67    >,
68}
69
70impl AddressOverridesMap {
71    pub fn new() -> Self {
72        Self {
73            map: BTreeMap::new(),
74        }
75    }
76
77    pub fn insert(
78        &mut self,
79        network_pubkey: ConsensusNetworkPublicKey,
80        source: sui_network::endpoint_manager::AddressSource,
81        addresses: Vec<Multiaddr>,
82    ) {
83        self.map
84            .entry(network_pubkey)
85            .or_default()
86            .insert(source, addresses);
87    }
88
89    pub fn remove(
90        &mut self,
91        network_pubkey: ConsensusNetworkPublicKey,
92        source: sui_network::endpoint_manager::AddressSource,
93    ) {
94        self.map
95            .entry(network_pubkey.clone())
96            .or_default()
97            .remove(&source);
98
99        // If no sources remain for this peer, remove the peer entry entirely
100        if self.map.get(&network_pubkey.clone()).unwrap().is_empty() {
101            self.map.remove(&network_pubkey);
102        }
103    }
104
105    /// Returns the highest-priority active override `(source, address)` for the
106    /// peer, or `None` when no override is installed (the on-chain committee
107    /// address is in use).
108    pub fn get_highest_priority_source_and_address(
109        &self,
110        network_pubkey: ConsensusNetworkPublicKey,
111    ) -> Option<(sui_network::endpoint_manager::AddressSource, Multiaddr)> {
112        self.map
113            .get(&network_pubkey)
114            .and_then(|sources| sources.first_key_value())
115            .and_then(|(source, addresses)| {
116                addresses.first().cloned().map(|address| (*source, address))
117            })
118    }
119
120    pub fn get_all_highest_priority_addresses(
121        &self,
122    ) -> Vec<(ConsensusNetworkPublicKey, Multiaddr)> {
123        let mut result = Vec::new();
124
125        for (network_pubkey, sources) in self.map.iter() {
126            if let Some((_source, addresses)) = sources.first_key_value()
127                && let Some(address) = addresses.first()
128            {
129                result.push((network_pubkey.clone(), address.clone()));
130            }
131        }
132        result
133    }
134}
135
136/// Rebuilds the consensus `Committee` with Mysticeti v3 threshold parameters.
137/// `malicious_stake` and `crash_stake` come from env vars with reference-budget
138/// defaults (`f = c = 1250`); the nominal `threshold_total_stake = 5f + 3c + 1`
139/// is derived inside `Committee::new_v3`. This is a temporary iteration knob
140/// until the thresholds are promoted into `ProtocolConfig`.
141fn apply_v3_threshold_overrides(committee: Committee) -> Committee {
142    let malicious_stake: Stake = std::env::var("SUI_CONSENSUS_V3_MALICIOUS_STAKE")
143        .ok()
144        .and_then(|s| s.parse().ok())
145        .unwrap_or(1_250);
146    let crash_stake: Stake = std::env::var("SUI_CONSENSUS_V3_CRASH_STAKE")
147        .ok()
148        .and_then(|s| s.parse().ok())
149        .unwrap_or(1_250);
150    info!(
151        "consensus_manager: applying v3 committee thresholds \
152         (malicious_stake={malicious_stake}, crash_stake={crash_stake})"
153    );
154    Committee::new_v3(
155        committee.epoch(),
156        committee.authorities_slice().to_vec(),
157        malicious_stake,
158        crash_stake,
159    )
160}
161
162fn to_consensus_protocol_config(config: &ProtocolConfig) -> ConsensusProtocolConfig {
163    let chain_type = match config.chain() {
164        Chain::Mainnet => ChainType::Mainnet,
165        Chain::Testnet => ChainType::Testnet,
166        Chain::Unknown => ChainType::Unknown,
167    };
168    ConsensusProtocolConfig::new(
169        config.version.as_u64(),
170        chain_type,
171        config.max_transaction_size_bytes(),
172        config.max_transactions_in_block_bytes(),
173        config.max_num_transactions_in_block(),
174        config.gc_depth(),
175        config.consensus_slim_block_propagation(),
176        /* transaction_voting_enabled */ true,
177        config.mysticeti_num_leaders_per_round(),
178        config.consensus_bad_nodes_stake_threshold(),
179        /* enable_v3 */ false,
180        /* leader_schedule_window_size */ 300,
181        /* leader_schedule_update_interval */ 12,
182        config.shuffle_consensus_commit_blocks(),
183    )
184}
185
186/// Used by Sui to start consensus protocol for each epoch.
187/// Supports both validator mode (with protocol keypair) and observer mode (without).
188pub struct ConsensusManager {
189    consensus_config: ConsensusConfig,
190    protocol_keypair: Option<ProtocolKeyPair>,
191    network_keypair: NetworkKeyPair,
192    storage_base_path: PathBuf,
193    metrics: Arc<ConsensusManagerMetrics>,
194    registry_service: RegistryService,
195    authority: ArcSwapOption<(ConsensusAuthority, RegistryID)>,
196
197    // Use a shared lazy Mysticeti client so we can update the internal Mysticeti
198    // client that gets created for every new epoch.
199    client: Arc<LazyMysticetiClient>,
200    consensus_client: Arc<UpdatableConsensusClient>,
201    transaction_pool_context: Option<Arc<TransactionPoolContext>>,
202    transaction_pool: ArcSwapOption<ConsensusTransactionPool>,
203
204    consensus_handler: Mutex<Option<MysticetiConsensusHandler>>,
205
206    #[cfg(test)]
207    pub(crate) consumer_monitor: ArcSwapOption<ConsumerMonitor>,
208    #[cfg(not(test))]
209    consumer_monitor: ArcSwapOption<ConsumerMonitor>,
210    consumer_monitor_sender: broadcast::Sender<Arc<ConsumerMonitor>>,
211
212    running: Mutex<Running>,
213
214    #[cfg(test)]
215    pub(crate) boot_counter: Mutex<u64>,
216    #[cfg(not(test))]
217    boot_counter: Mutex<u64>,
218
219    // Persistent storage for address updates across epoch changes.
220    // Keyed by NetworkPublicKey and then by AddressSource.
221    address_overrides: parking_lot::Mutex<AddressOverridesMap>,
222}
223
224impl ConsensusManager {
225    pub fn new(
226        node_config: &NodeConfig,
227        consensus_config: &ConsensusConfig,
228        registry_service: &RegistryService,
229        consensus_client: Arc<UpdatableConsensusClient>,
230        transaction_pool_context: Option<Arc<TransactionPoolContext>>,
231        node_role: NodeRole,
232    ) -> Self {
233        let metrics = Arc::new(ConsensusManagerMetrics::new(
234            &registry_service.default_registry(),
235        ));
236        let client = Arc::new(LazyMysticetiClient::new());
237        let (consumer_monitor_sender, _) = broadcast::channel(1);
238        let protocol_keypair = if node_role.is_validator() {
239            Some(ProtocolKeyPair::new(node_config.worker_key_pair().copy()))
240        } else {
241            None
242        };
243        Self {
244            consensus_config: consensus_config.clone(),
245            protocol_keypair,
246            network_keypair: NetworkKeyPair::new(node_config.network_key_pair().copy()),
247            storage_base_path: consensus_config.db_path().to_path_buf(),
248            metrics,
249            registry_service: registry_service.clone(),
250            authority: ArcSwapOption::empty(),
251            client,
252            consensus_client,
253            transaction_pool_context,
254            transaction_pool: ArcSwapOption::empty(),
255            consensus_handler: Mutex::new(None),
256            consumer_monitor: ArcSwapOption::empty(),
257            consumer_monitor_sender,
258            running: Mutex::new(Running::False),
259            boot_counter: Mutex::new(0),
260            address_overrides: parking_lot::Mutex::new(AddressOverridesMap::new()),
261        }
262    }
263
264    pub async fn start(
265        &self,
266        node_config: &NodeConfig,
267        epoch_store: Arc<AuthorityPerEpochStore>,
268        consensus_handler_initializer: ConsensusHandlerInitializer,
269        tx_validator: SuiTxValidator,
270        randomness_signature_handler: Option<Arc<dyn RandomnessSignatureHandler>>,
271    ) {
272        let epoch = epoch_store.epoch();
273        let protocol_config = epoch_store.protocol_config();
274        let consensus_protocol_config = to_consensus_protocol_config(protocol_config);
275        let system_state = epoch_store.epoch_start_state();
276        let committee = if consensus_protocol_config.enable_v3() {
277            apply_v3_threshold_overrides(system_state.get_consensus_committee())
278        } else {
279            system_state.get_consensus_committee()
280        };
281
282        // Ensure start() is not called twice.
283        let start_time = Instant::now();
284        let mut running = self.running.lock().await;
285        if let Running::True(running_epoch, running_version) = *running {
286            error!(
287                "Consensus is already Running for epoch {running_epoch:?} & protocol version {running_version:?} - shutdown first before starting",
288            );
289            return;
290        }
291        *running = Running::True(epoch, protocol_config.version);
292
293        info!(
294            "Starting up consensus for epoch {epoch:?} & protocol version {:?}",
295            protocol_config.version
296        );
297
298        let is_validator = epoch_store.is_validator();
299        if is_validator && self.protocol_keypair.is_none() {
300            // The manager was built for a non-validator role and reused across a
301            // promotion to validator; consensus cannot sign proposals in this state.
302            debug_fatal!("validator epoch {epoch} started without a protocol keypair");
303        }
304        let pool_context = self
305            .transaction_pool_context
306            .as_ref()
307            .filter(|_| is_validator);
308        let transaction_pool: Option<Arc<dyn TransactionPool>> = if let Some(context) = pool_context
309        {
310            let config = &node_config.consensus_transaction_pool;
311            if let Some(ttl) = config.max_queue_age()
312                && ttl < ConsensusTransactionPoolConfig::MIN_RECOMMENDED_MAX_QUEUE_AGE
313            {
314                warn!(
315                    ?ttl,
316                    "consensus-transaction-pool.max-queue-age-secs is below {:?} and can expire \
317                     staggered entries before this validator proposes them",
318                    ConsensusTransactionPoolConfig::MIN_RECOMMENDED_MAX_QUEUE_AGE
319                );
320            }
321            let pool = Arc::new(ConsensusTransactionPool::new(
322                epoch_store.clone(),
323                context.cache_writer().clone(),
324                config.max_pending_transactions(&self.consensus_config),
325                config.max_user_bytes_per_second(
326                    epoch_store
327                        .protocol_config()
328                        .max_transactions_in_block_bytes(),
329                ),
330                config.max_queue_age(),
331                context.metrics().clone(),
332                context.adapter_metrics().clone(),
333            ));
334            context.set_active(epoch, pool.clone());
335            self.transaction_pool.store(Some(pool.clone()));
336            self.consensus_client
337                .set(Arc::new(TransactionPoolClient::new(context.clone())));
338            Some(pool)
339        } else {
340            if let Some(context) = &self.transaction_pool_context {
341                context.set_unavailable(epoch);
342            }
343            self.consensus_client.set(self.client.clone());
344            None
345        };
346
347        let consensus_config = node_config
348            .consensus_config()
349            .expect("consensus_config should exist");
350
351        let parameters = Parameters {
352            db_path: self.get_store_path(epoch),
353            listen_address_override: consensus_config.listen_address.clone(),
354            ..consensus_config.parameters.clone().unwrap_or_default()
355        };
356
357        let registry = Registry::new_custom(Some("consensus".to_string()), None).unwrap();
358
359        let consensus_handler = consensus_handler_initializer.new_consensus_handler();
360
361        let num_prior_commits = protocol_config.consensus_num_requested_prior_commits_at_startup();
362        let last_processed_commit_index =
363            consensus_handler.last_processed_subdag_index() as CommitIndex;
364        let replay_after_commit_index =
365            last_processed_commit_index.saturating_sub(num_prior_commits);
366
367        let (commit_consumer, commit_receiver) =
368            CommitConsumerArgs::new(replay_after_commit_index, last_processed_commit_index);
369        let monitor = commit_consumer.monitor();
370
371        // Spin up the new Mysticeti consensus handler to listen for committed sub dags, before starting authority.
372        let handler = MysticetiConsensusHandler::new(
373            last_processed_commit_index,
374            consensus_handler,
375            commit_receiver,
376            monitor.clone(),
377        );
378        let mut consensus_handler = self.consensus_handler.lock().await;
379        *consensus_handler = Some(handler);
380
381        // If there is a previous consumer monitor, it indicates that the consensus engine has been restarted, due to an epoch change. However, that on its
382        // own doesn't tell us much whether it participated on an active epoch or an old one. We need to check if it has handled any commits to determine this.
383        // If indeed any commits did happen, then we assume that node did participate on previous run.
384        let participated_on_previous_run =
385            if let Some(previous_monitor) = self.consumer_monitor.swap(Some(monitor.clone())) {
386                previous_monitor.highest_handled_commit() > 0
387            } else {
388                false
389            };
390
391        // Increment the boot counter only if the consensus successfully participated in the previous run.
392        // This is typical during normal epoch changes, where the node restarts as expected, and the boot counter is incremented to prevent amnesia recovery on the next start.
393        // If the node is recovering from a restore process and catching up across multiple epochs, it won't handle any commits until it reaches the last active epoch.
394        // In this scenario, we do not increment the boot counter, as we need amnesia recovery to run.
395        let mut boot_counter = self.boot_counter.lock().await;
396        if participated_on_previous_run {
397            *boot_counter += 1;
398        } else {
399            info!(
400                "Node has not participated in previous epoch consensus. Boot counter ({}) will not increment.",
401                *boot_counter
402            );
403        }
404
405        let authority = ConsensusAuthority::start(
406            NetworkType::Tonic,
407            epoch_store.epoch_start_config().epoch_start_timestamp_ms(),
408            committee.clone(),
409            parameters.clone(),
410            consensus_protocol_config,
411            self.protocol_keypair.clone(),
412            self.network_keypair.clone(),
413            Arc::new(Clock::default()),
414            Arc::new(tx_validator.clone()),
415            transaction_pool,
416            commit_consumer,
417            registry.clone(),
418            *boot_counter,
419            randomness_signature_handler,
420        )
421        .await;
422        let client = pool_context
423            .is_none()
424            .then(|| authority.transaction_client());
425
426        let registry_id = self.registry_service.add(registry.clone());
427
428        let registered_authority = Arc::new((authority, registry_id));
429        self.authority.swap(Some(registered_authority.clone()));
430
431        // Reapply all stored address updates to the new consensus instance.
432        let highest_priority_addresses = self
433            .address_overrides
434            .lock()
435            .get_all_highest_priority_addresses();
436        for (network_pubkey, address) in highest_priority_addresses {
437            registered_authority
438                .0
439                .update_peer_address(network_pubkey, Some(address.clone()));
440        }
441
442        // Initialize the client to send transactions to this Mysticeti instance.
443        if let Some(client) = client {
444            self.client.set(client);
445        }
446
447        // Send the consumer monitor to the replay waiter.
448        let _ = self.consumer_monitor_sender.send(monitor);
449
450        let elapsed = start_time.elapsed().as_secs_f64();
451        self.metrics.start_latency.set(elapsed as i64);
452
453        tracing::info!(
454            "Started consensus for epoch {} & protocol version {:?} completed - took {} seconds",
455            epoch,
456            protocol_config.version,
457            elapsed
458        );
459    }
460
461    pub async fn shutdown(&self) {
462        info!("Shutting down consensus ...");
463
464        // Ensure shutdown() is called on a running consensus and get the epoch/version info.
465        let start_time = Instant::now();
466        let mut running = self.running.lock().await;
467        let (shutdown_epoch, shutdown_version) = match *running {
468            Running::True(epoch, version) => {
469                tracing::info!(
470                    "Shutting down consensus for epoch {epoch:?} & protocol version {version:?}"
471                );
472                *running = Running::False;
473                (epoch, version)
474            }
475            Running::False => {
476                error!("Consensus shutdown was called but consensus is not running");
477                return;
478            }
479        };
480
481        // Stop consensus submissions.
482        let pool = self.transaction_pool.swap(None);
483        if let Some(pool) = &pool {
484            pool.close();
485        }
486        self.client.clear();
487
488        // swap with empty to ensure there is no other reference to authority and we can safely do Arc unwrap
489        let r = self.authority.swap(None).unwrap();
490        let Ok((authority, registry_id)) = Arc::try_unwrap(r) else {
491            panic!("Failed to retrieve the Mysticeti authority");
492        };
493
494        // shutdown the authority and wait for it
495        authority.stop().await;
496
497        // drop the old consensus handler to force stop any underlying task running.
498        let mut consensus_handler = self.consensus_handler.lock().await;
499        if let Some(mut handler) = consensus_handler.take() {
500            handler.abort().await;
501        }
502
503        // unregister the registry id
504        self.registry_service.remove(registry_id);
505
506        if pool.is_none() {
507            self.consensus_client.clear();
508        }
509
510        let elapsed = start_time.elapsed().as_secs_f64();
511        self.metrics.shutdown_latency.set(elapsed as i64);
512
513        tracing::info!(
514            "Consensus stopped for epoch {shutdown_epoch:?} & protocol version {shutdown_version:?} is complete - took {} seconds",
515            elapsed
516        );
517    }
518
519    pub async fn is_running(&self) -> bool {
520        let running = self.running.lock().await;
521        matches!(*running, Running::True(_, _))
522    }
523
524    pub fn replay_waiter(&self) -> ReplayWaiter {
525        let consumer_monitor_receiver = self.consumer_monitor_sender.subscribe();
526        ReplayWaiter::new(consumer_monitor_receiver)
527    }
528
529    pub fn get_storage_base_path(&self) -> PathBuf {
530        self.consensus_config.db_path().to_path_buf()
531    }
532
533    pub fn consensus_store(&self) -> Option<Arc<RocksDBStore>> {
534        self.authority.load().as_ref().map(|a| a.0.store())
535    }
536
537    pub fn address_overrides_snapshot(
538        &self,
539    ) -> BTreeMap<
540        ConsensusNetworkPublicKey,
541        BTreeMap<sui_network::endpoint_manager::AddressSource, Vec<Multiaddr>>,
542    > {
543        self.address_overrides.lock().map.clone()
544    }
545
546    fn get_store_path(&self, epoch: EpochId) -> PathBuf {
547        let mut store_path = self.storage_base_path.clone();
548        store_path.push(format!("{}", epoch));
549        store_path
550    }
551}
552
553impl Drop for ConsensusManager {
554    fn drop(&mut self) {
555        // Abrupt node teardown can bypass async shutdown; explicitly disarm any
556        // pending pool acknowledgements before the manager's fields are dropped.
557        if let Some(pool) = self.transaction_pool.swap(None) {
558            pool.close();
559        }
560    }
561}
562
563// Implementing the interface so we can update the consensus peer addresses when requested.
564impl ConsensusAddressUpdater for ConsensusManager {
565    fn update_address(
566        &self,
567        network_pubkey: NetworkPublicKey,
568        source: sui_network::endpoint_manager::AddressSource,
569        addresses: Vec<Multiaddr>,
570    ) -> SuiResult<()> {
571        // Convert to consensus network public key once
572        let network_pubkey = ConsensusNetworkPublicKey::new(network_pubkey.clone());
573
574        // Determine which override (if any) should be used after this update.
575        let highest_priority = {
576            let mut address_overrides = self.address_overrides.lock();
577
578            if addresses.is_empty() {
579                address_overrides.remove(network_pubkey.clone(), source);
580            } else {
581                address_overrides.insert(network_pubkey.clone(), source, addresses.clone());
582            }
583
584            address_overrides.get_highest_priority_source_and_address(network_pubkey.clone())
585        };
586        self.metrics.set_active_address_source(
587            &Hex::encode(network_pubkey.to_bytes()),
588            highest_priority.as_ref().map(|(source, _)| *source),
589        );
590
591        // Apply the update to running consensus if it exists
592        let address_to_apply = highest_priority.map(|(_, address)| address);
593        if let Some(authority) = self.authority.load_full() {
594            authority
595                .0
596                .update_peer_address(network_pubkey, address_to_apply);
597            Ok(())
598        } else {
599            info!(
600                "Consensus authority node is not running, address update persisted for peer {:?} from source {:?} and will be applied on next start",
601                network_pubkey, source
602            );
603            Err(SuiErrorKind::GenericAuthorityError {
604                error: "Consensus authority node is not running. Can not apply address update"
605                    .to_string(),
606            }
607            .into())
608        }
609    }
610}
611
612/// A ConsensusClient that can be updated internally at any time. This usually happening during epoch
613/// change where a client is set after the new consensus is started for the new epoch.
614#[derive(Default)]
615pub struct UpdatableConsensusClient {
616    // An extra layer of Arc<> is needed as required by ArcSwapAny.
617    client: ArcSwapOption<Arc<dyn ConsensusClient>>,
618}
619
620impl UpdatableConsensusClient {
621    pub fn new() -> Self {
622        Self {
623            client: ArcSwapOption::empty(),
624        }
625    }
626
627    async fn get(&self) -> Arc<Arc<dyn ConsensusClient>> {
628        const START_TIMEOUT: Duration = Duration::from_secs(300);
629        const RETRY_INTERVAL: Duration = Duration::from_millis(100);
630        if let Ok(client) = timeout(START_TIMEOUT, async {
631            loop {
632                let Some(client) = self.client.load_full() else {
633                    sleep(RETRY_INTERVAL).await;
634                    continue;
635                };
636                return client;
637            }
638        })
639        .await
640        {
641            return client;
642        }
643
644        panic!(
645            "Timed out after {:?} waiting for Consensus to start!",
646            START_TIMEOUT,
647        );
648    }
649
650    pub fn set(&self, client: Arc<dyn ConsensusClient>) {
651        self.client.store(Some(Arc::new(client)));
652    }
653
654    pub fn clear(&self) {
655        self.client.store(None);
656    }
657}
658
659#[async_trait]
660impl ConsensusClient for UpdatableConsensusClient {
661    async fn submit(
662        &self,
663        transactions: &[ConsensusTransaction],
664        epoch_store: &Arc<AuthorityPerEpochStore>,
665    ) -> SuiResult<(Vec<ConsensusPosition>, BlockStatusReceiver)> {
666        let client = self.get().await;
667        client.submit(transactions, epoch_store).await
668    }
669}
670
671/// Waits for consensus to finish replaying at consensus handler.
672pub struct ReplayWaiter {
673    consumer_monitor_receiver: broadcast::Receiver<Arc<ConsumerMonitor>>,
674}
675
676impl ReplayWaiter {
677    pub(crate) fn new(
678        consumer_monitor_receiver: broadcast::Receiver<Arc<ConsumerMonitor>>,
679    ) -> Self {
680        Self {
681            consumer_monitor_receiver,
682        }
683    }
684
685    pub(crate) async fn wait_for_replay(mut self) {
686        loop {
687            info!("Waiting for consensus to start replaying ...");
688            let Ok(monitor) = self.consumer_monitor_receiver.recv().await else {
689                continue;
690            };
691            info!("Waiting for consensus handler to finish replaying ...");
692            monitor
693                .replay_to_consumer_last_processed_commit_complete()
694                .await;
695            break;
696        }
697    }
698}
699
700impl Clone for ReplayWaiter {
701    fn clone(&self) -> Self {
702        Self {
703            consumer_monitor_receiver: self.consumer_monitor_receiver.resubscribe(),
704        }
705    }
706}
707
708pub struct ConsensusManagerMetrics {
709    start_latency: IntGauge,
710    shutdown_latency: IntGauge,
711    active_address_source: IntGaugeVec,
712}
713
714impl ConsensusManagerMetrics {
715    pub fn new(registry: &Registry) -> Self {
716        Self {
717            start_latency: register_int_gauge_with_registry!(
718                "consensus_manager_start_latency",
719                "The latency of starting up consensus nodes",
720                registry,
721            )
722            .unwrap(),
723            shutdown_latency: register_int_gauge_with_registry!(
724                "consensus_manager_shutdown_latency",
725                "The latency of shutting down consensus nodes",
726                registry,
727            )
728            .unwrap(),
729            active_address_source: register_int_gauge_vec_with_registry!(
730                "consensus_active_address_source",
731                "Active consensus address source per committee peer, encoded as the gauge \
732                 value: 0=committee (no override active; the on-chain committee address is in \
733                 use), 1=admin, 2=config, 3=discovery, 4=seed, 5=chain (override priority \
734                 highest to lowest). One series per peer; `peer_id` is the full hex consensus \
735                 network public key.",
736                &["peer_id"],
737                registry,
738            )
739            .unwrap(),
740        }
741    }
742
743    /// Records the active consensus address source for a committee peer as a single
744    /// per-peer gauge whose value is the source's `metric_code`. `active =
745    /// Some(source)` means an override is installed; `None` means no override.
746    fn set_active_address_source(
747        &self,
748        peer_id: &str,
749        active: Option<sui_network::endpoint_manager::AddressSource>,
750    ) {
751        let code = active.map_or(
752            AddressSource::DEFAULT_ADDRESS_SOURCE_CODE,
753            sui_network::endpoint_manager::AddressSource::metric_code,
754        );
755        self.active_address_source
756            .with_label_values(&[peer_id])
757            .set(code);
758    }
759}