Skip to main content

snarkos_node_consensus/
lib.rs

1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16#![forbid(unsafe_code)]
17
18mod transactions_queue;
19use transactions_queue::TransactionsQueue;
20
21#[macro_use]
22extern crate tracing;
23
24#[cfg(feature = "metrics")]
25extern crate snarkos_node_metrics as metrics;
26
27use snarkos_account::Account;
28use snarkos_node_bft::{
29    BFT,
30    MAX_BATCH_DELAY,
31    Primary,
32    helpers::{
33        ConsensusReceiver,
34        ConsensusSender,
35        PrimaryReceiver,
36        PrimarySender,
37        Storage as NarwhalStorage,
38        fmt_id,
39        init_consensus_channels,
40        init_primary_channels,
41    },
42    spawn_blocking,
43};
44use snarkos_node_bft_ledger_service::LedgerService;
45use snarkos_node_bft_storage_service::BFTPersistentStorage;
46use snarkos_node_sync::{BlockSync, Ping};
47use snarkos_utilities::NodeDataDir;
48
49use snarkvm::{
50    ledger::{
51        CheckBlockError,
52        block::Transaction,
53        narwhal::{BatchHeader, Data, Subdag, Transmission, TransmissionID},
54        puzzle::{Solution, SolutionID},
55    },
56    prelude::*,
57    utilities::flatten_error,
58};
59
60use aleo_std::StorageMode;
61use anyhow::{Context, Result, bail};
62use cfg_if::cfg_if;
63use colored::Colorize;
64use indexmap::IndexMap;
65#[cfg(feature = "locktick")]
66use locktick::parking_lot::{Mutex, RwLock};
67use lru::LruCache;
68#[cfg(not(feature = "locktick"))]
69use parking_lot::{Mutex, RwLock};
70use std::{future::Future, net::SocketAddr, num::NonZeroUsize, sync::Arc};
71use tokio::{
72    sync::{Notify, oneshot},
73    task::JoinHandle,
74};
75
76#[cfg(feature = "metrics")]
77use std::collections::HashMap;
78
79/// The capacity of the queue reserved for deployments.
80/// Note: This is an inbound queue capacity, not a Narwhal-enforced capacity.
81const CAPACITY_FOR_DEPLOYMENTS: usize = 1 << 10;
82/// The capacity of the queue reserved for executions.
83/// Note: This is an inbound queue capacity, not a Narwhal-enforced capacity.
84const CAPACITY_FOR_EXECUTIONS: usize = 1 << 10;
85/// The capacity of the queue reserved for solutions.
86/// Note: This is an inbound queue capacity, not a Narwhal-enforced capacity.
87const CAPACITY_FOR_SOLUTIONS: usize = 1 << 10;
88/// The **suggested** maximum number of deployments in each interval.
89/// Note: This is an inbound queue limit, not a Narwhal-enforced limit.
90const MAX_DEPLOYMENTS_PER_INTERVAL: usize = 1;
91
92type BftRunArgs<N> = Arc<Mutex<Option<(ConsensusSender<N>, PrimaryReceiver<N>)>>>;
93
94/// Wrapper around `BFT` that adds additional functionality, such as a mempool.
95///
96/// Consensus acts as a rate limiter to prevents workers in BFT from being overloaded.
97/// Each worker maintains a ready queue (which is essentially also a mempool), but verifies transactions/solutions
98/// before enqueuing them.
99/// Consensus only passes more transactions/solutions to the BFT layer if its ready queues are not already full.
100#[derive(Clone)]
101pub struct Consensus<N: Network> {
102    /// The ledger.
103    ledger: Arc<dyn LedgerService<N>>,
104    /// The BFT.
105    bft: BFT<N>,
106    /// The primary sender.
107    primary_sender: PrimarySender<N>,
108    /// The unconfirmed solutions queue.
109    solutions_queue: Arc<Mutex<LruCache<SolutionID<N>, Solution<N>>>>,
110    /// The unconfirmed transactions queue.
111    transactions_queue: Arc<RwLock<TransactionsQueue<N>>>,
112    /// The recently-seen unconfirmed solutions.
113    seen_solutions: Arc<Mutex<LruCache<SolutionID<N>, ()>>>,
114    /// The recently-seen unconfirmed transactions.
115    seen_transactions: Arc<Mutex<LruCache<N::TransactionID, ()>>>,
116    #[cfg(feature = "metrics")]
117    transmissions_tracker: Arc<Mutex<HashMap<TransmissionID<N>, i64>>>,
118    /// The spawned handles.
119    handles: Arc<Mutex<Vec<JoinHandle<()>>>>,
120    /// The ping logic.
121    ping: Arc<Ping<N>>,
122    /// The block sync logic.
123    block_sync: Arc<BlockSync<N>>,
124    /// Notifies when a block is committed, and relays it to the primary.
125    block_commit_notify: Arc<Notify>,
126    /// Holds the consensus receiver until [`Self::start_consensus_handlers`] is called.
127    consensus_receiver: Arc<Mutex<Option<ConsensusReceiver<N>>>>,
128    /// Holds the arguments for `BFT::run` until [`Self::start_consensus_handlers`] is called.
129    bft_run_args: BftRunArgs<N>,
130}
131
132impl<N: Network> Consensus<N> {
133    /// Initializes a new instance of consensus.
134    #[allow(clippy::too_many_arguments)]
135    pub async fn new(
136        account: Account<N>,
137        ledger: Arc<dyn LedgerService<N>>,
138        block_sync: Arc<BlockSync<N>>,
139        ip: Option<SocketAddr>,
140        trusted_validators: &[SocketAddr],
141        trusted_peers_only: bool,
142        storage_mode: StorageMode,
143        node_data_dir: NodeDataDir,
144        ping: Arc<Ping<N>>,
145        dev: Option<u16>,
146    ) -> Result<Self> {
147        // Initialize the primary channels.
148        let (primary_sender, primary_receiver) = init_primary_channels::<N>();
149        // Initialize the Narwhal transmissions.
150        let transmissions = Arc::new(BFTPersistentStorage::open(storage_mode.clone())?);
151        // Initialize the Narwhal storage.
152        let storage = NarwhalStorage::new(ledger.clone(), transmissions, BatchHeader::<N>::MAX_GC_ROUNDS as u64)
153            .with_context(|| "Failed to initialize the BFT storage")?;
154        // Initialize the BFT.
155        let bft = BFT::new(
156            account,
157            storage,
158            ledger.clone(),
159            block_sync.clone(),
160            ip,
161            trusted_validators,
162            trusted_peers_only,
163            node_data_dir,
164            dev,
165        )?;
166        // Initialize the consensus channels.
167        let (consensus_sender, consensus_receiver) = init_consensus_channels();
168
169        // Create a new instance of Consensus.
170        let _self = Self {
171            ledger,
172            bft,
173            block_sync,
174            primary_sender,
175            solutions_queue: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(CAPACITY_FOR_SOLUTIONS).unwrap()))),
176            transactions_queue: Default::default(),
177            seen_solutions: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(1 << 16).unwrap()))),
178            seen_transactions: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(1 << 16).unwrap()))),
179            #[cfg(feature = "metrics")]
180            transmissions_tracker: Default::default(),
181            handles: Default::default(),
182            ping: ping.clone(),
183            block_commit_notify: Arc::new(Notify::new()),
184            consensus_receiver: Arc::new(Mutex::new(Some(consensus_receiver))),
185            bft_run_args: Arc::new(Mutex::new(Some((consensus_sender, primary_receiver)))),
186        };
187
188        Ok(_self)
189    }
190
191    /// Returns the underlying `BFT` struct.
192    pub const fn bft(&self) -> &BFT<N> {
193        &self.bft
194    }
195
196    pub fn contains_transaction(&self, transaction_id: &N::TransactionID) -> bool {
197        self.transactions_queue.read().contains(transaction_id)
198    }
199}
200
201impl<N: Network> Consensus<N> {
202    /// Returns the number of unconfirmed transmissions in the BFT's workers (not in the mempool).
203    pub fn num_unconfirmed_transmissions(&self) -> usize {
204        self.bft.num_unconfirmed_transmissions()
205    }
206
207    /// Returns the number of unconfirmed ratifications in the BFT's workers (not in the mempool).
208    pub fn num_unconfirmed_ratifications(&self) -> usize {
209        self.bft.num_unconfirmed_ratifications()
210    }
211
212    /// Returns the number unconfirmed solutions in the BFT's workers (not in the mempool).
213    pub fn num_unconfirmed_solutions(&self) -> usize {
214        self.bft.num_unconfirmed_solutions()
215    }
216
217    /// Returns the number of unconfirmed transactions.
218    pub fn num_unconfirmed_transactions(&self) -> usize {
219        self.bft.num_unconfirmed_transactions()
220    }
221}
222
223impl<N: Network> Consensus<N> {
224    /// Returns the unconfirmed transmission IDs.
225    pub fn unconfirmed_transmission_ids(&self) -> impl '_ + Iterator<Item = TransmissionID<N>> {
226        self.worker_transmission_ids().chain(self.inbound_transmission_ids())
227    }
228
229    /// Returns the unconfirmed transmissions.
230    pub fn unconfirmed_transmissions(&self) -> impl '_ + Iterator<Item = (TransmissionID<N>, Transmission<N>)> {
231        self.worker_transmissions().chain(self.inbound_transmissions())
232    }
233
234    /// Returns the unconfirmed solutions.
235    pub fn unconfirmed_solutions(&self) -> impl '_ + Iterator<Item = (SolutionID<N>, Data<Solution<N>>)> {
236        self.worker_solutions().chain(self.inbound_solutions())
237    }
238
239    /// Returns the unconfirmed transactions.
240    pub fn unconfirmed_transactions(&self) -> impl '_ + Iterator<Item = (N::TransactionID, Data<Transaction<N>>)> {
241        self.worker_transactions().chain(self.inbound_transactions())
242    }
243}
244
245impl<N: Network> Consensus<N> {
246    /// Returns the worker transmission IDs.
247    pub fn worker_transmission_ids(&self) -> impl '_ + Iterator<Item = TransmissionID<N>> {
248        self.bft.worker_transmission_ids()
249    }
250
251    /// Returns the worker transmissions.
252    pub fn worker_transmissions(&self) -> impl '_ + Iterator<Item = (TransmissionID<N>, Transmission<N>)> {
253        self.bft.worker_transmissions()
254    }
255
256    /// Returns the worker solutions.
257    pub fn worker_solutions(&self) -> impl '_ + Iterator<Item = (SolutionID<N>, Data<Solution<N>>)> {
258        self.bft.worker_solutions()
259    }
260
261    /// Returns the worker transactions.
262    pub fn worker_transactions(&self) -> impl '_ + Iterator<Item = (N::TransactionID, Data<Transaction<N>>)> {
263        self.bft.worker_transactions()
264    }
265}
266
267impl<N: Network> Consensus<N> {
268    /// Returns the transmission IDs in the inbound queue.
269    pub fn inbound_transmission_ids(&self) -> impl '_ + Iterator<Item = TransmissionID<N>> {
270        self.inbound_transmissions().map(|(id, _)| id)
271    }
272
273    /// Returns the transmissions in the inbound queue.
274    pub fn inbound_transmissions(&self) -> impl '_ + Iterator<Item = (TransmissionID<N>, Transmission<N>)> {
275        self.inbound_transactions()
276            .map(|(id, tx)| {
277                (
278                    TransmissionID::Transaction(id, tx.to_checksum::<N>().unwrap_or_default()),
279                    Transmission::Transaction(tx),
280                )
281            })
282            .chain(self.inbound_solutions().map(|(id, solution)| {
283                (
284                    TransmissionID::Solution(id, solution.to_checksum::<N>().unwrap_or_default()),
285                    Transmission::Solution(solution),
286                )
287            }))
288    }
289
290    /// Returns the solutions in the inbound queue.
291    pub fn inbound_solutions(&self) -> impl '_ + Iterator<Item = (SolutionID<N>, Data<Solution<N>>)> {
292        // Return an iterator over the solutions in the inbound queue.
293        self.solutions_queue.lock().clone().into_iter().map(|(id, solution)| (id, Data::Object(solution)))
294    }
295
296    /// Returns the transactions in the inbound queue.
297    pub fn inbound_transactions(&self) -> impl '_ + Iterator<Item = (N::TransactionID, Data<Transaction<N>>)> {
298        // Return an iterator over the deployment and execution transactions in the inbound queue.
299        self.transactions_queue.read().transactions().map(|(id, tx)| (id, Data::Object(tx)))
300    }
301}
302
303impl<N: Network> Consensus<N> {
304    /// Adds the given unconfirmed solution to the memory pool, which will then eventually be passed
305    /// to the BFT layer for inclusion in a batch.
306    pub async fn add_unconfirmed_solution(&self, solution: Solution<N>) -> Result<()> {
307        // Calculate the transmission checksum.
308        let checksum = Data::<Solution<N>>::Buffer(solution.to_bytes_le()?.into()).to_checksum::<N>()?;
309        // Queue the unconfirmed solution.
310        {
311            let solution_id = solution.id();
312
313            // Check if the transaction was recently seen.
314            if self.seen_solutions.lock().put(solution_id, ()).is_some() {
315                // If the transaction was recently seen, return early.
316                return Ok(());
317            }
318            // Check if the solution already exists in the ledger.
319            if self.ledger.contains_transmission(&TransmissionID::Solution(solution_id, checksum))? {
320                bail!("Solution '{}' exists in the ledger {}", fmt_id(solution_id), "(skipping)".dimmed());
321            }
322            #[cfg(feature = "metrics")]
323            {
324                metrics::increment_gauge(metrics::consensus::UNCONFIRMED_SOLUTIONS, 1f64);
325                let timestamp = snarkos_node_bft::helpers::now();
326                self.transmissions_tracker.lock().insert(TransmissionID::Solution(solution.id(), checksum), timestamp);
327            }
328            // Add the solution to the memory pool.
329            trace!("Received unconfirmed solution '{}' in the queue", fmt_id(solution_id));
330            if self.solutions_queue.lock().put(solution_id, solution).is_some() {
331                bail!("Solution '{}' exists in the memory pool", fmt_id(solution_id));
332            }
333        }
334
335        // Try to process the unconfirmed solutions in the memory pool.
336        self.process_unconfirmed_solutions().await
337    }
338
339    /// Processes unconfirmed solutions in the mempool, and passes them to the BFT layer
340    /// (if sufficient space is available).
341    async fn process_unconfirmed_solutions(&self) -> Result<()> {
342        // If the memory pool of this node is full, return early.
343        let num_unconfirmed_solutions = self.num_unconfirmed_solutions();
344        let num_unconfirmed_transmissions = self.num_unconfirmed_transmissions();
345        if num_unconfirmed_solutions >= N::MAX_SOLUTIONS
346            || num_unconfirmed_transmissions >= Primary::<N>::MAX_TRANSMISSIONS_TOLERANCE
347        {
348            return Ok(());
349        }
350        // Retrieve the solutions.
351        let solutions = {
352            // Determine the available capacity.
353            let capacity = N::MAX_SOLUTIONS.saturating_sub(num_unconfirmed_solutions);
354            // Acquire the lock on the queue.
355            let mut queue = self.solutions_queue.lock();
356            // Determine the number of solutions to send.
357            let num_solutions = queue.len().min(capacity);
358            // Drain the solutions from the queue.
359            (0..num_solutions).filter_map(|_| queue.pop_lru().map(|(_, solution)| solution)).collect::<Vec<_>>()
360        };
361        // Iterate over the solutions.
362        for solution in solutions.into_iter() {
363            let solution_id = solution.id();
364            trace!("Adding unconfirmed solution '{}' to the memory pool...", fmt_id(solution_id));
365            // Send the unconfirmed solution to the primary.
366            match self.primary_sender.send_unconfirmed_solution(solution_id, Data::Object(solution)).await {
367                Ok(true) => {}
368                Ok(false) => debug!(
369                    "Unable to add unconfirmed solution '{}' to the memory pool. Already exists.",
370                    fmt_id(solution_id)
371                ),
372                Err(err) => {
373                    let err = err.context(format!(
374                        "Unable to add unconfirmed solution '{}' to the memory pool",
375                        fmt_id(solution_id)
376                    ));
377
378                    // If the node is synced and the occurs after the first 10 blocks of an epoch, log it as a warning, otherwise trace it.
379                    if self.bft.is_synced() && self.ledger.latest_block_height() % N::NUM_BLOCKS_PER_EPOCH > 10 {
380                        warn!("{}", flatten_error(err));
381                    } else {
382                        trace!("{}", flatten_error(err));
383                    }
384                }
385            }
386        }
387        Ok(())
388    }
389
390    /// Adds the given unconfirmed transaction to the memory pool, which will then eventually be passed
391    /// to the BFT layer for inclusion in a batch.
392    pub async fn add_unconfirmed_transaction(&self, transaction: Transaction<N>) -> Result<()> {
393        // Calculate the transmission checksum.
394        let checksum = Data::<Transaction<N>>::Buffer(transaction.to_bytes_le()?.into()).to_checksum::<N>()?;
395        // Queue the unconfirmed transaction.
396        {
397            let transaction_id = transaction.id();
398
399            // Check that the transaction is not a fee transaction.
400            if transaction.is_fee() {
401                bail!("Transaction '{}' is a fee transaction {}", fmt_id(transaction_id), "(skipping)".dimmed());
402            }
403            // Check if the transaction was recently seen.
404            if self.seen_transactions.lock().put(transaction_id, ()).is_some() {
405                // If the transaction was recently seen, return early.
406                return Ok(());
407            }
408            // Check if the transaction already exists in the ledger.
409            if self.ledger.contains_transmission(&TransmissionID::Transaction(transaction_id, checksum))? {
410                bail!("Transaction '{}' exists in the ledger {}", fmt_id(transaction_id), "(skipping)".dimmed());
411            }
412            // Check that the transaction is not in the mempool.
413            if self.contains_transaction(&transaction_id) {
414                bail!("Transaction '{}' exists in the memory pool", fmt_id(transaction_id));
415            }
416            #[cfg(feature = "metrics")]
417            {
418                metrics::increment_gauge(metrics::consensus::UNCONFIRMED_TRANSACTIONS, 1f64);
419                let timestamp = snarkos_node_bft::helpers::now();
420                self.transmissions_tracker
421                    .lock()
422                    .insert(TransmissionID::Transaction(transaction.id(), checksum), timestamp);
423            }
424            // Add the transaction to the memory pool.
425            trace!("Received unconfirmed transaction '{}' in the queue", fmt_id(transaction_id));
426            let priority_fee = transaction.priority_fee_amount()?;
427            self.transactions_queue.write().insert(transaction_id, transaction, priority_fee)?;
428        }
429
430        // Try to process the unconfirmed transactions in the memory pool.
431        self.process_unconfirmed_transactions().await
432    }
433
434    /// Processes unconfirmed transactions in the mempool, and passes them to the BFT layer
435    /// (if sufficient space is available).
436    async fn process_unconfirmed_transactions(&self) -> Result<()> {
437        // If the memory pool of this node is full, return early.
438        let num_unconfirmed_transmissions = self.num_unconfirmed_transmissions();
439        if num_unconfirmed_transmissions >= Primary::<N>::MAX_TRANSMISSIONS_TOLERANCE {
440            return Ok(());
441        }
442        // Retrieve the transactions.
443        let transactions = {
444            // Determine the available capacity.
445            let capacity = Primary::<N>::MAX_TRANSMISSIONS_TOLERANCE.saturating_sub(num_unconfirmed_transmissions);
446            // Acquire the lock on the transactions queue.
447            let mut tx_queue = self.transactions_queue.write();
448            // Determine the number of deployments to send.
449            let num_deployments = tx_queue.deployments.len().min(capacity).min(MAX_DEPLOYMENTS_PER_INTERVAL);
450            // Determine the number of executions to send.
451            let num_executions = tx_queue.executions.len().min(capacity.saturating_sub(num_deployments));
452            // Create an iterator which will select interleaved deployments and executions within the capacity.
453            // Note: interleaving ensures we will never have consecutive invalid deployments blocking the queue.
454            let selector_iter = (0..num_deployments).map(|_| true).interleave((0..num_executions).map(|_| false));
455            // Drain the transactions from the queue, interleaving deployments and executions.
456            selector_iter
457                .filter_map(
458                    |select_deployment| {
459                        if select_deployment { tx_queue.deployments.pop() } else { tx_queue.executions.pop() }
460                    },
461                )
462                .map(|(_, tx)| tx)
463                .collect_vec()
464        };
465        // Iterate over the transactions.
466        for transaction in transactions.into_iter() {
467            let transaction_id = transaction.id();
468            // Determine the type of the transaction. The fee type is technically not possible here.
469            let tx_type_str = match transaction {
470                Transaction::Deploy(..) => "deployment",
471                Transaction::Execute(..) => "execution",
472                Transaction::Fee(..) => "fee",
473            };
474            trace!("Adding unconfirmed {tx_type_str} transaction '{}' to the memory pool...", fmt_id(transaction_id));
475            // Send the unconfirmed transaction to the primary.
476            match self.primary_sender.send_unconfirmed_transaction(transaction_id, Data::Object(transaction)).await {
477                Ok(true) => {}
478                Ok(false) => debug!(
479                    "Unable to add unconfirmed {tx_type_str} transaction '{}' to the memory pool. Already exists.",
480                    fmt_id(transaction_id)
481                ),
482                Err(err) => {
483                    // If the BFT is synced, then log the warning.
484                    if self.bft.is_synced() {
485                        let err = err.context(format!(
486                            "Unable to add unconfirmed {tx_type_str} transaction '{}' to the memory pool",
487                            fmt_id(transaction_id)
488                        ));
489                        warn!("{}", flatten_error(err));
490                    }
491                }
492            }
493        }
494        Ok(())
495    }
496}
497
498impl<N: Network> Consensus<N> {
499    /// Starts the BFT and consensus handlers.
500    ///
501    /// This consumes the deferred state stored during construction and must only be invoked once,
502    /// after the node has finished syncing from the CDN, so that no blocks are committed during
503    /// the initial sync.
504    pub async fn start_consensus_handlers(&self) -> Result<()> {
505        info!("Starting the consensus instance...");
506
507        // Take the BFT run arguments stored during construction and start the BFT handlers.
508        let Some((consensus_sender, primary_receiver)) = self.bft_run_args.lock().take() else {
509            bail!("The consensus handlers have already been started");
510        };
511        self.bft
512            .clone()
513            .run(Some(self.ping.clone()), Some(consensus_sender), self.primary_sender.clone(), primary_receiver)
514            .await?;
515
516        // Take the consensus receiver stored during construction and start the consensus handlers.
517        let Some(consensus_receiver) = self.consensus_receiver.lock().take() else {
518            bail!("The consensus handlers have already been started");
519        };
520        let ConsensusReceiver { mut rx_consensus_subdag } = consensus_receiver;
521
522        // Process the committed subdag and transmissions from the BFT.
523        let self_ = self.clone();
524        self.spawn(async move {
525            while let Some((committed_subdag, transmissions, callback)) = rx_consensus_subdag.recv().await {
526                self_.process_bft_subdag(committed_subdag, transmissions, callback).await;
527            }
528        });
529
530        // Process the unconfirmed transactions in the memory pool.
531        //
532        // This loop wakes up either when a block is committed (signaled via notify) or after a timeout.
533        // When a block is committed, the BFT workers have freed capacity for more transmissions.
534        // The timeout serves as a fallback for startup or idle periods when blocks are not being produced.
535        //
536        // TODO(kaimast): wake up the loop after a proposal is created, not only when a block commits.
537        let self_ = self.clone();
538        self.spawn(async move {
539            loop {
540                // Wait for either a block commit notification or timeout.
541                tokio::select! {
542                    _ = self_.block_commit_notify.notified() => {}
543                    _ = tokio::time::sleep(MAX_BATCH_DELAY) => {}
544                }
545                // Process the unconfirmed transactions in the memory pool.
546                if let Err(err) = self_.process_unconfirmed_transactions().await {
547                    warn!("{}", flatten_error(err.context("Cannot process unconfirmed transactions")));
548                }
549                // Process the unconfirmed solutions in the memory pool.
550                if let Err(err) = self_.process_unconfirmed_solutions().await {
551                    warn!("{}", flatten_error(err.context("Cannot process unconfirmed solutions")));
552                }
553            }
554        });
555
556        Ok(())
557    }
558
559    /// Attempts to build a new block from the given subDAG, and (tries to) advance the legder to it.
560    ///
561    /// # Returns
562    /// - `Ok(true)` if the ledger was advanced to the next block.
563    /// - `Ok(false)` if the block may be valide but the ledger already advanced.
564    /// - `Err(anyhow::Error)` for incorrect blocks and any other error.
565    async fn process_bft_subdag(
566        &self,
567        subdag: Subdag<N>,
568        transmissions: IndexMap<TransmissionID<N>, Transmission<N>>,
569        callback: oneshot::Sender<Result<bool>>,
570    ) {
571        // Try to advance to the next block.
572        let self_ = self.clone();
573        let transmissions_ = transmissions.clone();
574        let result = spawn_blocking! { self_.try_advance_to_next_block(subdag, transmissions_).with_context(|| "Unable to advance to the next block") };
575
576        // If the block failed to advance, reinsert the transmissions into the memory pool.
577        match result {
578            Ok(true) => {
579                // On success, notify that the BFT workers have freed capacity for more transmissions.
580                self.block_commit_notify.notify_one();
581            }
582            Ok(false) | Err(_) => self.reinsert_transmissions(transmissions).await,
583        }
584
585        callback.send(result).ok();
586    }
587
588    /// Attempts to advance the ledger to the next block, and updates the metrics (if enabled) accordingly.
589    ///
590    /// # Returns
591    /// - `Ok(true)` if the ledger was advanced to the next block.
592    /// - `Ok(false)` if the block may be valid but the ledger already advanced.
593    /// - `Err(anyhow::Error)` for incorrect blocks and any other error.
594    fn try_advance_to_next_block(
595        &self,
596        subdag: Subdag<N>,
597        transmissions: IndexMap<TransmissionID<N>, Transmission<N>>,
598    ) -> Result<bool> {
599        #[cfg(feature = "metrics")]
600        let start = subdag.leader_certificate().batch_header().timestamp();
601        #[cfg(feature = "metrics")]
602        let num_committed_certificates = subdag.values().map(|c| c.len()).sum::<usize>();
603        #[cfg(feature = "metrics")]
604        let current_block_timestamp = self.ledger.latest_block().header().metadata().timestamp();
605
606        // Create the candidate next block.
607        let ledger_update = self.ledger.begin_ledger_update()?;
608
609        let prepare_instant = std::time::Instant::now();
610        let block = match ledger_update.prepare_advance_to_next_quorum_block(subdag, transmissions) {
611            Ok(block) => block,
612            Err(err) => return Err(err.into_anyhow()),
613        };
614        let prepare_elapsed = prepare_instant.elapsed();
615        trace!("prepare_advance_to_next_quorum_block took {:.3}s", prepare_elapsed.as_secs_f64());
616        #[cfg(feature = "metrics")]
617        metrics::histogram(metrics::consensus::PREPARE_ADVANCE_SECS, prepare_elapsed.as_secs_f64());
618
619        let check_instant = std::time::Instant::now();
620        cfg_if! {
621            if #[cfg(feature = "test_network")] {
622                // If we are using a hotswapped dev committee, skip checking the block.
623                let result = if self.ledger.dev_committee_for_round(block.round())?.is_some() {
624                    Ok(block)
625                } else {
626                    ledger_update.check_next_block(block)
627                };
628            } else {
629                let result = ledger_update.check_next_block(block);
630            }
631        }
632
633        let block = match result {
634            Ok(block) => block,
635            Err(CheckBlockError::BlockAlreadyExists { .. }) => {
636                debug!("The given block hash already exists in the ledger");
637                return Ok(false);
638            }
639            Err(CheckBlockError::InvalidHeight { .. }) => {
640                debug!("The ledger advanced while we were constructing the next block");
641                return Ok(false);
642            }
643            Err(CheckBlockError::InvalidRound { new, previous }) => {
644                debug!("The subDAG round is too low. Expected >{previous}, got {new}");
645                return Ok(false);
646            }
647            Err(err) => return Err(err.into_anyhow()),
648        };
649
650        let check_elapsed = check_instant.elapsed();
651        trace!("check_next_block took {:.3}s", check_elapsed.as_secs_f64());
652        #[cfg(feature = "metrics")]
653        metrics::histogram(metrics::consensus::CHECK_NEXT_BLOCK_SECS, check_elapsed.as_secs_f64());
654
655        let block_height = block.height();
656
657        // Advance to the next block.
658        let advance_instant = std::time::Instant::now();
659        ledger_update.advance_to_next_block(&block)?;
660        let advance_elapsed = advance_instant.elapsed();
661        trace!("advance_to_next_block took {:.3}s", advance_elapsed.as_secs_f64());
662        #[cfg(feature = "metrics")]
663        metrics::histogram(metrics::consensus::ADVANCE_TO_NEXT_BLOCK_SECS, advance_elapsed.as_secs_f64());
664
665        #[cfg(feature = "telemetry")]
666        // Fetch the committee lookback for the latest round.
667        // Note: Do not abort here if this returns an error, because the committee is only needed for telemetry,
668        // not for block advancement itself.
669        let latest_committee = self.ledger.get_committee_lookback_for_round(self.ledger.latest_round());
670
671        // If the next block starts a new epoch, clear the existing solutions.
672        if block_height.is_multiple_of(N::NUM_BLOCKS_PER_EPOCH) {
673            // Clear the solutions queue.
674            self.solutions_queue.lock().clear();
675            // Clear the worker solutions.
676            self.bft.primary().clear_worker_solutions();
677        }
678
679        // Notify peers that we have a new block.
680        match self.block_sync.get_block_locators() {
681            Ok(locators) => self.ping.update_block_locators(locators),
682            Err(err) => error!(
683                "{}",
684                flatten_error(err.context("Failed to generate new block locators after block advancement"))
685            ),
686        }
687
688        // Make block sync aware of the new block.
689        self.block_sync.set_sync_height(block_height);
690
691        // TODO(kaimast): This should also remove any transmissions/solutions contained in the block from the mempool.
692        // Removal currently happens when Consensus eventually passes them to the worker, which then just discards them.
693
694        #[cfg(feature = "metrics")]
695        {
696            let now_utc = snarkos_node_bft::helpers::now_utc();
697            let elapsed = std::time::Duration::from_secs((now_utc.unix_timestamp() - start) as u64);
698            let next_block_timestamp = block.header().metadata().timestamp();
699            let next_block_utc = snarkos_node_bft::helpers::to_utc_datetime(next_block_timestamp);
700            let block_latency = next_block_timestamp - current_block_timestamp;
701            let block_lag = (now_utc - next_block_utc).whole_milliseconds();
702
703            let proof_target = block.header().proof_target();
704            let coinbase_target = block.header().coinbase_target();
705            let cumulative_proof_target = block.header().cumulative_proof_target();
706
707            // Calculate latency for all transmissions included in this block.
708            metrics::add_transmission_latency_metric(&self.transmissions_tracker, &block);
709
710            metrics::gauge(metrics::consensus::COMMITTED_CERTIFICATES, num_committed_certificates as f64);
711            metrics::histogram(metrics::consensus::CERTIFICATE_COMMIT_LATENCY, elapsed.as_secs_f64());
712            metrics::histogram(metrics::consensus::BLOCK_LATENCY, block_latency as f64);
713            metrics::histogram(metrics::consensus::BLOCK_LAG, block_lag as f64);
714            metrics::gauge(metrics::blocks::PROOF_TARGET, proof_target as f64);
715            metrics::gauge(metrics::blocks::COINBASE_TARGET, coinbase_target as f64);
716            metrics::gauge(metrics::blocks::CUMULATIVE_PROOF_TARGET, cumulative_proof_target as f64);
717
718            // If telemetry is enabled, update participation scores.
719            #[cfg(feature = "telemetry")]
720            {
721                match latest_committee {
722                    Ok(latest_committee) => {
723                        // Retrieve the individual participation scores.
724                        let participation_scores = self
725                            .bft()
726                            .primary()
727                            .gateway()
728                            .validator_telemetry()
729                            .get_participation_scores(&latest_committee);
730
731                        // Export the certificate and signature participation scores as individual gauges.
732                        for (address, (certificate_score, signature_score)) in participation_scores {
733                            let address_str = address.to_string();
734                            metrics::gauge_label(
735                                metrics::consensus::VALIDATOR_CERTIFICATE_PARTICIPATION,
736                                "validator_address",
737                                address_str.clone(),
738                                certificate_score,
739                            );
740                            metrics::gauge_label(
741                                metrics::consensus::VALIDATOR_SIGNATURE_PARTICIPATION,
742                                "validator_address",
743                                address_str,
744                                signature_score,
745                            );
746                        }
747                    }
748                    Err(err) => warn!("{}", flatten_error(err.context("Failed to get latest committee for telemetry"))),
749                }
750            }
751        }
752
753        Ok(true)
754    }
755
756    /// Reinserts the given transmissions into the memory pool.
757    async fn reinsert_transmissions(&self, transmissions: IndexMap<TransmissionID<N>, Transmission<N>>) {
758        // Iterate over the transmissions.
759        for (transmission_id, transmission) in transmissions.into_iter() {
760            // Reinsert the transmission into the memory pool.
761            match self.reinsert_transmission(transmission_id, transmission).await {
762                Ok(true) => {}
763                Ok(false) => debug!(
764                    "Unable to reinsert transmission {}:{} into the memory pool. Already exists.",
765                    fmt_id(transmission_id),
766                    fmt_id(transmission_id.checksum().unwrap_or_default()).dimmed()
767                ),
768                Err(err) => {
769                    let err = err.context(format!(
770                        "Unable to reinsert transmission {}.{} into the memory pool",
771                        fmt_id(transmission_id),
772                        fmt_id(transmission_id.checksum().unwrap_or_default()).dimmed()
773                    ));
774                    warn!("{}", flatten_error(err));
775                }
776            }
777        }
778    }
779
780    /// Reinserts the given transmission into the memory pool.
781    ///
782    /// # Returns
783    /// - `Ok(true)` if the transmission was added to the memory pool.
784    /// - `Ok(false)` if the transmission was valid but already exists in the memory pool.
785    /// - `Err(anyhow::Error)` if the transmission was invalid.
786    async fn reinsert_transmission(
787        &self,
788        transmission_id: TransmissionID<N>,
789        transmission: Transmission<N>,
790    ) -> Result<bool> {
791        // Initialize a callback sender and receiver.
792        let (callback, callback_receiver) = oneshot::channel();
793        // Send the transmission to the primary.
794        match (transmission_id, transmission) {
795            (TransmissionID::Ratification, Transmission::Ratification) => return Ok(true),
796            (TransmissionID::Solution(solution_id, _), Transmission::Solution(solution)) => {
797                // Send the solution to the primary.
798                self.primary_sender.tx_unconfirmed_solution.send((solution_id, solution, callback)).await?;
799            }
800            (TransmissionID::Transaction(transaction_id, _), Transmission::Transaction(transaction)) => {
801                // Send the transaction to the primary.
802                self.primary_sender.tx_unconfirmed_transaction.send((transaction_id, transaction, callback)).await?;
803            }
804            _ => bail!("Mismatching `(transmission_id, transmission)` pair in consensus"),
805        }
806        // Await the callback.
807        callback_receiver.await?
808    }
809
810    /// Spawns a task with the given future; it should only be used for long-running tasks.
811    fn spawn<T: Future<Output = ()> + Send + 'static>(&self, future: T) {
812        self.handles.lock().push(tokio::spawn(future));
813    }
814
815    /// Shuts down the consensus and BFT layers.
816    pub async fn shut_down(&self) {
817        info!("Shutting down consensus...");
818        // Shut down the BFT.
819        self.bft.shut_down().await;
820        // Abort the tasks.
821        self.handles.lock().iter().for_each(|handle| handle.abort());
822    }
823}