Skip to main content

iota_core/consensus_manager/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{
6    path::PathBuf,
7    sync::Arc,
8    time::{Duration, Instant},
9};
10
11use arc_swap::ArcSwapOption;
12use async_trait::async_trait;
13use fastcrypto::traits::KeyPair as _;
14use iota_config::{ConsensusConfig, NodeConfig};
15use iota_metrics::RegistryService;
16use iota_protocol_config::ProtocolVersion;
17use iota_types::{committee::EpochId, error::IotaResult, messages_consensus::ConsensusTransaction};
18use prometheus_filtered::{IntGauge, Registry, register_int_gauge_with_registry};
19use starfish_core::{CommitConsumerMonitor, CommitIndex};
20use tokio::{
21    sync::{Mutex, MutexGuard, broadcast},
22    time::{sleep, timeout},
23};
24use tracing::info;
25
26use crate::{
27    authority::authority_per_epoch_store::AuthorityPerEpochStore,
28    consensus_adapter::{BlockStatusReceiver, ConsensusClient},
29    consensus_handler::ConsensusHandlerInitializer,
30    consensus_manager::starfish_manager::StarfishManager,
31    consensus_validator::IotaTxValidator,
32    starfish_adapter::LazyStarfishClient,
33};
34
35pub mod starfish_manager;
36
37#[derive(PartialEq)]
38pub(crate) enum Running {
39    True(EpochId, ProtocolVersion),
40    False,
41}
42
43#[async_trait]
44pub trait ConsensusManagerTrait {
45    async fn start(
46        &self,
47        node_config: &NodeConfig,
48        epoch_store: Arc<AuthorityPerEpochStore>,
49        consensus_handler_initializer: ConsensusHandlerInitializer,
50        tx_validator: IotaTxValidator,
51    );
52
53    async fn shutdown(&self);
54
55    async fn is_running(&self) -> bool;
56
57    fn replay_waiter(&self) -> ReplayWaiter;
58}
59
60/// Used by IOTA validator to start consensus protocol for each epoch.
61pub struct ConsensusManager {
62    consensus_config: ConsensusConfig,
63    starfish_manager: StarfishManager,
64}
65
66impl ConsensusManager {
67    pub fn new(
68        node_config: &NodeConfig,
69        consensus_config: &ConsensusConfig,
70        registry_service: &RegistryService,
71        metrics_registry: &Registry,
72        consensus_client: Arc<UpdatableConsensusClient>,
73    ) -> Self {
74        let metrics = Arc::new(ConsensusManagerMetrics::new(metrics_registry));
75        let starfish_client = Arc::new(LazyStarfishClient::new());
76        consensus_client.set(starfish_client.clone());
77        let starfish_manager = StarfishManager::new(
78            node_config.protocol_key_pair().copy(),
79            node_config.network_key_pair().copy(),
80            consensus_config.db_path().to_path_buf(),
81            registry_service.clone(),
82            metrics,
83            starfish_client,
84        );
85        Self {
86            consensus_config: consensus_config.clone(),
87            starfish_manager,
88        }
89    }
90
91    pub fn get_storage_base_path(&self) -> PathBuf {
92        self.consensus_config.db_path().to_path_buf()
93    }
94}
95
96#[async_trait]
97impl ConsensusManagerTrait for ConsensusManager {
98    async fn start(
99        &self,
100        node_config: &NodeConfig,
101        epoch_store: Arc<AuthorityPerEpochStore>,
102        consensus_handler_initializer: ConsensusHandlerInitializer,
103        tx_validator: IotaTxValidator,
104    ) {
105        info!("Starting consensus protocol Starfish ...");
106        self.starfish_manager
107            .start(
108                node_config,
109                epoch_store,
110                consensus_handler_initializer,
111                tx_validator,
112            )
113            .await
114    }
115
116    async fn shutdown(&self) {
117        info!("Shutting down consensus ...");
118        self.starfish_manager.shutdown().await;
119    }
120
121    async fn is_running(&self) -> bool {
122        self.starfish_manager.is_running().await
123    }
124
125    fn replay_waiter(&self) -> ReplayWaiter {
126        self.starfish_manager.replay_waiter()
127    }
128}
129
130/// A ConsensusClient that can be updated internally at any time. This usually
131/// happening during epoch change where a client is set after the new consensus
132/// is started for the new epoch.
133#[derive(Default)]
134pub struct UpdatableConsensusClient {
135    // An extra layer of Arc<> is needed as required by ArcSwapAny.
136    client: ArcSwapOption<Arc<dyn ConsensusClient>>,
137}
138
139impl UpdatableConsensusClient {
140    pub fn new() -> Self {
141        Self {
142            client: ArcSwapOption::empty(),
143        }
144    }
145
146    async fn get(&self) -> Arc<Arc<dyn ConsensusClient>> {
147        const START_TIMEOUT: Duration = Duration::from_secs(30);
148        const RETRY_INTERVAL: Duration = Duration::from_millis(100);
149        if let Ok(client) = timeout(START_TIMEOUT, async {
150            loop {
151                let Some(client) = self.client.load_full() else {
152                    sleep(RETRY_INTERVAL).await;
153                    continue;
154                };
155                return client;
156            }
157        })
158        .await
159        {
160            return client;
161        }
162
163        panic!("Timed out after {START_TIMEOUT:?} waiting for Consensus to start!",);
164    }
165
166    pub fn set(&self, client: Arc<dyn ConsensusClient>) {
167        self.client.store(Some(Arc::new(client)));
168    }
169
170    pub fn clear(&self) {
171        self.client.store(None);
172    }
173}
174
175#[async_trait]
176impl ConsensusClient for UpdatableConsensusClient {
177    async fn submit(
178        &self,
179        transactions: &[ConsensusTransaction],
180        epoch_store: &Arc<AuthorityPerEpochStore>,
181    ) -> IotaResult<BlockStatusReceiver> {
182        let client = self.get().await;
183        client.submit(transactions, epoch_store).await
184    }
185}
186
187/// The commit consumer of a started consensus run, with the commit the replay
188/// waits for, as [`replay_target`] computes it.
189pub(crate) type StartedConsumer = (Arc<CommitConsumerMonitor>, Option<CommitIndex>);
190
191/// Returns the commit the checkpoint builder waits for the consensus handler to handle before it
192/// starts building, with deterministic validation. This is `last_validated_commit`, or
193/// `resume_point` when that is `None`, capped at `last_commit_before_synced_checkpoint_execution`
194/// when that is set.
195pub(crate) fn replay_target(
196    last_validated_commit: Option<u64>,
197    resume_point: CommitIndex,
198    last_commit_before_synced_checkpoint_execution: Option<u64>,
199) -> CommitIndex {
200    let target = match last_validated_commit {
201        Some(index) => CommitIndex::try_from(index).unwrap_or(CommitIndex::MAX),
202        None => resume_point,
203    };
204    match last_commit_before_synced_checkpoint_execution {
205        Some(last) => target.min(CommitIndex::try_from(last).unwrap_or(CommitIndex::MAX)),
206        None => target,
207    }
208}
209
210/// Receives, through the `consensus-replay-target` fail point, the commit the
211/// replay waits for (every stored commit without deterministic validation)
212/// and the highest commit consensus had stored at startup.
213pub type ConsensusReplayTargetReport = Arc<dyn Fn(CommitIndex, CommitIndex) + Send + Sync>;
214
215/// Waits for consensus to finish replaying at consensus handler.
216pub struct ReplayWaiter {
217    consumer_monitor_receiver: broadcast::Receiver<StartedConsumer>,
218}
219
220impl ReplayWaiter {
221    pub(crate) fn new(consumer_monitor_receiver: broadcast::Receiver<StartedConsumer>) -> Self {
222        Self {
223            consumer_monitor_receiver,
224        }
225    }
226
227    pub(crate) async fn wait_for_replay(mut self) {
228        loop {
229            info!("Waiting for consensus to start replaying ...");
230            let Ok((monitor, target)) = self.consumer_monitor_receiver.recv().await else {
231                continue;
232            };
233            let highest_observed = monitor.highest_observed_commit_at_startup();
234            let target = target.map_or(highest_observed, |target| target.min(highest_observed));
235            info!(
236                target,
237                highest_observed, "Waiting for consensus handler to finish replaying ..."
238            );
239            monitor.wait_until_handled(target).await;
240            break;
241        }
242    }
243}
244
245impl Clone for ReplayWaiter {
246    fn clone(&self) -> Self {
247        Self {
248            consumer_monitor_receiver: self.consumer_monitor_receiver.resubscribe(),
249        }
250    }
251}
252
253pub struct ConsensusManagerMetrics {
254    start_latency: IntGauge,
255    shutdown_latency: IntGauge,
256}
257
258impl ConsensusManagerMetrics {
259    pub fn new(registry: &Registry) -> Self {
260        Self {
261            start_latency: register_int_gauge_with_registry!(
262                "consensus_manager_start_latency",
263                "The latency of starting up consensus nodes",
264                registry,
265            )
266            .unwrap(),
267            shutdown_latency: register_int_gauge_with_registry!(
268                "consensus_manager_shutdown_latency",
269                "The latency of shutting down consensus nodes",
270                registry,
271            )
272            .unwrap(),
273        }
274    }
275}
276
277pub(crate) struct RunningLockGuard<'a> {
278    state_guard: MutexGuard<'a, Running>,
279    metrics: &'a ConsensusManagerMetrics,
280    epoch: Option<EpochId>,
281    protocol_version: Option<ProtocolVersion>,
282    start: Instant,
283}
284
285impl<'a> RunningLockGuard<'a> {
286    pub(crate) async fn acquire_start(
287        metrics: &'a ConsensusManagerMetrics,
288        running_mutex: &'a Mutex<Running>,
289        epoch: EpochId,
290        version: ProtocolVersion,
291    ) -> Option<RunningLockGuard<'a>> {
292        let running = running_mutex.lock().await;
293        if let Running::True(epoch, version) = *running {
294            tracing::warn!(
295                "Consensus is already Running for epoch {epoch:?} & protocol version {version:?} - shutdown first before starting",
296            );
297            return None;
298        }
299
300        tracing::info!("Starting up consensus for epoch {epoch:?} & protocol version {version:?}");
301
302        Some(RunningLockGuard {
303            state_guard: running,
304            metrics,
305            start: Instant::now(),
306            epoch: Some(epoch),
307            protocol_version: Some(version),
308        })
309    }
310
311    pub(crate) async fn acquire_shutdown(
312        metrics: &'a ConsensusManagerMetrics,
313        running_mutex: &'a Mutex<Running>,
314    ) -> Option<RunningLockGuard<'a>> {
315        let running = running_mutex.lock().await;
316        if let Running::True(epoch, version) = *running {
317            tracing::info!(
318                "Shutting down consensus for epoch {epoch:?} & protocol version {version:?}"
319            );
320        } else {
321            tracing::warn!("Consensus shutdown was called but consensus is not running");
322            return None;
323        }
324
325        Some(RunningLockGuard {
326            state_guard: running,
327            metrics,
328            start: Instant::now(),
329            epoch: None,
330            protocol_version: None,
331        })
332    }
333}
334
335impl Drop for RunningLockGuard<'_> {
336    fn drop(&mut self) {
337        match *self.state_guard {
338            // consensus was running and now will have to be marked as shutdown
339            Running::True(epoch, version) => {
340                tracing::info!(
341                    "Consensus shutdown for epoch {epoch:?} & protocol version {version:?} is complete - took {} seconds",
342                    self.start.elapsed().as_secs_f64()
343                );
344
345                self.metrics
346                    .shutdown_latency
347                    .set(self.start.elapsed().as_secs_f64() as i64);
348
349                *self.state_guard = Running::False;
350            }
351            // consensus was not running and now will be marked as started
352            Running::False => {
353                tracing::info!(
354                    "Starting up consensus for epoch {} & protocol version {:?} is complete - took {} seconds",
355                    self.epoch.unwrap(),
356                    self.protocol_version.unwrap(),
357                    self.start.elapsed().as_secs_f64()
358                );
359
360                self.metrics
361                    .start_latency
362                    .set(self.start.elapsed().as_secs_f64() as i64);
363
364                *self.state_guard =
365                    Running::True(self.epoch.unwrap(), self.protocol_version.unwrap());
366            }
367        }
368    }
369}