Skip to main content

fedimint_server/consensus/
engine.rs

1use std::collections::BTreeMap;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use aleph_bft::Keychain as KeychainTrait;
8use anyhow::{Context, anyhow, bail};
9use async_channel::Receiver;
10use fedimint_api_client::api::{DynGlobalApi, FederationApiExt, ServerError};
11use fedimint_api_client::query::FilterMap;
12use fedimint_core::config::P2PMessage;
13use fedimint_core::core::{DynOutput, MODULE_INSTANCE_ID_GLOBAL};
14use fedimint_core::db::{
15    Database, DatabaseError, DatabaseResult, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped,
16};
17use fedimint_core::encoding::Decodable;
18use fedimint_core::endpoint_constants::AWAIT_SIGNED_SESSION_OUTCOME_ENDPOINT;
19use fedimint_core::envs::is_running_in_test_env;
20use fedimint_core::module::audit::Audit;
21use fedimint_core::module::registry::ModuleDecoderRegistry;
22use fedimint_core::module::{ApiRequestErased, SerdeModuleEncoding};
23use fedimint_core::net::DynP2PConnections;
24use fedimint_core::runtime::spawn;
25use fedimint_core::secp256k1::schnorr;
26use fedimint_core::session_outcome::{
27    AcceptedItem, ConsensusItem, SessionOutcome, SignedSessionOutcome,
28};
29use fedimint_core::task::{TaskGroup, TaskHandle, sleep};
30use fedimint_core::timing::TimeReporter;
31use fedimint_core::util::FmtCompact as _;
32use fedimint_core::{NumPeers, NumPeersExt, PeerId, timing};
33use fedimint_server_core::{ServerModuleRegistry, ServerModuleRegistryExt};
34use futures::StreamExt;
35use rand::Rng;
36use rand::seq::IteratorRandom;
37use tokio::sync::watch;
38use tracing::{Level, debug, error, info, instrument, trace, warn};
39
40use crate::LOG_CONSENSUS;
41use crate::config::ServerConfig;
42use crate::consensus::aleph_bft::backup::{BackupReader, BackupWriter};
43use crate::consensus::aleph_bft::data_provider::{DataProvider, UnitData};
44use crate::consensus::aleph_bft::finalization_handler::{FinalizationHandler, OrderedUnit};
45use crate::consensus::aleph_bft::keychain::Keychain;
46use crate::consensus::aleph_bft::network::Network;
47use crate::consensus::aleph_bft::spawner::Spawner;
48use crate::consensus::aleph_bft::to_node_index;
49use crate::consensus::db::{
50    AcceptedItemKey, AcceptedItemPrefix, AcceptedTransactionKey, AlephUnitsPrefix,
51    SignedSessionOutcomeKey, SignedSessionOutcomePrefix,
52};
53use crate::consensus::debug::{DebugConsensusItem, DebugConsensusItemCompact};
54use crate::consensus::peer_availability::PeerActivity;
55use crate::consensus::transaction::{TxProcessingMode, process_transaction_with_dbtx};
56use crate::metrics::{
57    CONSENSUS_ITEM_PROCESSING_DURATION_SECONDS,
58    CONSENSUS_ITEM_PROCESSING_MODULE_AUDIT_DURATION_SECONDS, CONSENSUS_ITEMS_PROCESSED_TOTAL,
59    CONSENSUS_ORDERING_LATENCY_SECONDS, CONSENSUS_PEER_CONTRIBUTION_SESSION_IDX,
60    CONSENSUS_SESSION_COUNT,
61};
62
63// The name of the directory where the database checkpoints are stored.
64const DB_CHECKPOINTS_DIR: &str = "db_checkpoints";
65
66/// How many times a single ordered consensus item is processed before we give
67/// up. Only a database transaction that could not be validated at commit time
68/// costs an attempt, and reprocessing takes a fresh snapshot, so needing more
69/// than a couple of these means something is wrong beyond a stale snapshot.
70const MAX_ITEM_PROCESSING_ATTEMPTS: usize = 10;
71
72/// How an admin scheduled a shutdown.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum ShutdownRequest {
75    /// `shutdown` for a given session.
76    AtSession,
77    /// `shutdown_after_current_session`.
78    AfterCurrentSession,
79}
80
81/// A shutdown scheduled through the admin API.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub struct ScheduledShutdown {
84    /// Consensus shuts down at the first session boundary at or after the
85    /// completion of this session.
86    pub session: u64,
87    pub request: ShutdownRequest,
88}
89
90/// Why the consensus loop returned without an error.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum ConsensusStop {
93    /// The task group is shutting down; the current session may be
94    /// unfinished.
95    TaskShutdown,
96    /// A scheduled shutdown took effect after its session completed, so this
97    /// guardian holds no units of an unfinished session.
98    ScheduledShutdown(ShutdownRequest),
99}
100
101/// Runs the main server consensus loop
102pub struct ConsensusEngine {
103    pub modules: ServerModuleRegistry,
104    pub db: Database,
105    pub federation_api: DynGlobalApi,
106    pub cfg: ServerConfig,
107    pub submission_receiver: Receiver<ConsensusItem>,
108    pub shutdown_receiver: watch::Receiver<Option<ScheduledShutdown>>,
109    pub connections: DynP2PConnections<P2PMessage>,
110    pub ci_status_senders: BTreeMap<PeerId, watch::Sender<Option<u64>>>,
111    pub ord_latency_sender: watch::Sender<Option<Duration>>,
112    /// Notified whenever consensus accepts an item.
113    pub accepted_item_sender: watch::Sender<()>,
114    /// Notified whenever consensus completes a session.
115    pub completed_session_sender: watch::Sender<super::session_cache::CompletedSession>,
116    pub task_group: TaskGroup,
117    pub data_dir: PathBuf,
118    pub db_checkpoint_retention: u64,
119    pub session_timeout: Duration,
120    pub peer_activity: Arc<std::sync::Mutex<PeerActivity>>,
121}
122
123impl ConsensusEngine {
124    fn num_peers(&self) -> NumPeers {
125        self.cfg.consensus.broadcast_public_keys.to_num_peers()
126    }
127
128    fn identity(&self) -> PeerId {
129        self.cfg.local.identity
130    }
131
132    #[instrument(target = LOG_CONSENSUS, name = "run", skip_all, fields(id=%self.cfg.local.identity))]
133    pub async fn run(self) -> anyhow::Result<ConsensusStop> {
134        if self.num_peers().total() == 1 {
135            self.run_single_guardian(self.task_group.make_handle())
136                .await
137        } else {
138            self.run_consensus(self.task_group.make_handle()).await
139        }
140    }
141
142    pub async fn run_single_guardian(
143        &self,
144        task_handle: TaskHandle,
145    ) -> anyhow::Result<ConsensusStop> {
146        assert_eq!(self.num_peers(), NumPeers::from(1));
147
148        self.initialize_checkpoint_directory(self.get_finished_session_count().await)?;
149
150        while !task_handle.is_shutting_down() {
151            let session_index = self.get_finished_session_count().await;
152
153            CONSENSUS_SESSION_COUNT.set(session_index as i64);
154
155            let mut item_index = self.pending_accepted_items().await.len() as u64;
156
157            let session_start_time = std::time::Instant::now();
158
159            while let Ok(item) = self.submission_receiver.recv().await {
160                if self
161                    .process_consensus_item(session_index, item_index, item, self.identity())
162                    .await
163                    .is_ok()
164                {
165                    item_index += 1;
166                }
167
168                // we rely on the module consensus items to notice the timeout
169                if session_start_time.elapsed() > Duration::from_mins(1) {
170                    break;
171                }
172            }
173
174            let session_outcome = SessionOutcome {
175                items: self.pending_accepted_items().await,
176            };
177
178            let header = session_outcome.header(session_index);
179            let signature = Keychain::new(&self.cfg).sign_schnorr(&header);
180            let signatures = BTreeMap::from_iter([(self.identity(), signature)]);
181
182            self.complete_session(
183                session_index,
184                SignedSessionOutcome {
185                    session_outcome,
186                    signatures,
187                },
188            )
189            .await;
190
191            self.checkpoint_database(session_index);
192
193            info!(target: LOG_CONSENSUS, "Session {session_index} completed");
194
195            let due = due_shutdown(*self.shutdown_receiver.borrow(), session_index);
196            if let Some(request) = due {
197                return Ok(ConsensusStop::ScheduledShutdown(request));
198            }
199        }
200
201        info!(target: LOG_CONSENSUS, "Consensus task shut down");
202
203        Ok(ConsensusStop::TaskShutdown)
204    }
205
206    pub async fn run_consensus(&self, task_handle: TaskHandle) -> anyhow::Result<ConsensusStop> {
207        // We need four peers to run the atomic broadcast
208        assert!(self.num_peers().total() >= 4);
209
210        self.initialize_checkpoint_directory(self.get_finished_session_count().await)?;
211
212        while !task_handle.is_shutting_down() {
213            let session_index = self.get_finished_session_count().await;
214
215            CONSENSUS_SESSION_COUNT.set(session_index as i64);
216
217            let is_recovery = self.is_recovery().await;
218
219            info!(
220                target: LOG_CONSENSUS,
221                session_index,
222                is_recovery,
223                "Starting consensus session"
224            );
225
226            // If a session gets stuck for an extended period of time we exit
227            // the process such that our supervisor restarts fedimintd, which
228            // has been observed to resolve stuck sessions. Since AlephBFT
229            // persists its units to the database this is equivalent to a crash
230            // and therefore safe.
231            let session = tokio::time::timeout(
232                self.session_timeout,
233                self.run_session(self.connections.clone(), session_index),
234            )
235            .await
236            .context("Consensus session timed out, exiting...")?;
237
238            if session.is_none() {
239                return Ok(ConsensusStop::TaskShutdown);
240            }
241
242            info!(target: LOG_CONSENSUS, ?session_index, "Completed consensus session");
243
244            let due = due_shutdown(*self.shutdown_receiver.borrow(), session_index);
245            if let Some(request) = due {
246                info!(target: LOG_CONSENSUS, "Initiating shutdown, waiting for peers to complete the session...");
247
248                sleep(Duration::from_mins(1)).await;
249
250                return Ok(ConsensusStop::ScheduledShutdown(request));
251            }
252        }
253
254        info!(target: LOG_CONSENSUS, "Consensus task shut down");
255
256        Ok(ConsensusStop::TaskShutdown)
257    }
258
259    pub async fn run_session(
260        &self,
261        connections: DynP2PConnections<P2PMessage>,
262        session_index: u64,
263    ) -> Option<()> {
264        // In order to bound a sessions RAM consumption we need to bound its number of
265        // units and therefore its number of rounds. Since we use a session to
266        // create a naive secp256k1 threshold signature for the header of session
267        // outcome we have to guarantee that an attacker cannot exhaust our
268        // memory by preventing the creation of a threshold signature, thereby
269        // keeping the session open indefinitely. Hence, after a certain round
270        // index, we increase the delay between rounds exponentially such that
271        // the end of the aleph bft session would only be reached after a minimum
272        // of 10 years. In case of such an attack the broadcast stops ordering any
273        // items until the attack subsides as no items are ordered while the
274        // signatures are collected. The maximum RAM consumption of the aleph bft
275        // broadcast instance is therefore bound by:
276        //
277        // self.keychain.peer_count()
278        //      * (broadcast_rounds_per_session + EXP_SLOWDOWN_ROUNDS)
279        //      * ALEPH_BFT_UNIT_BYTE_LIMIT
280
281        const EXP_SLOWDOWN_ROUNDS: u16 = 1000;
282        const BASE: f64 = 1.02;
283
284        let rounds_per_session = self.cfg.consensus.broadcast_rounds_per_session;
285        let round_delay = f64::from(self.cfg.local.broadcast_round_delay_ms);
286
287        let mut delay_config = aleph_bft::default_delay_config();
288
289        delay_config.unit_creation_delay = Arc::new(move |round_index| {
290            // In a test environment all peers are started in lockstep, so we
291            // add jitter to the unit creation delay to break it; in production
292            // the peers' timing drifts naturally.
293            let jitter = if is_running_in_test_env() {
294                rand::thread_rng().gen_range(0.5..=1.5)
295            } else {
296                1.0
297            };
298
299            let delay = if round_index == 0 {
300                0.0
301            } else {
302                round_delay
303                    * BASE.powf(round_index.saturating_sub(rounds_per_session as usize) as f64)
304                    * jitter
305            };
306
307            Duration::from_millis(delay.round() as u64)
308        });
309
310        let config = aleph_bft::create_config(
311            self.num_peers().total().into(),
312            self.identity().to_usize().into(),
313            session_index,
314            self.cfg
315                .consensus
316                .broadcast_rounds_per_session
317                .checked_add(EXP_SLOWDOWN_ROUNDS)
318                .expect("Rounds per session exceed maximum of u16::Max - EXP_SLOWDOWN_ROUNDS"),
319            delay_config,
320            Duration::from_hours(87600),
321        )
322        .expect("The exponential slowdown exceeds 10 years");
323
324        // we can use an unbounded channel here since the number and size of units
325        // ordered in a single aleph session is bounded as described above
326        let (unit_data_sender, unit_data_receiver) = async_channel::unbounded();
327        let (signature_sender, signature_receiver) = watch::channel(None);
328        let (timestamp_sender, timestamp_receiver) = async_channel::unbounded();
329        let (terminator_sender, terminator_receiver) = futures::channel::oneshot::channel();
330
331        // Create channels for P2P session sync
332        let (signed_outcomes_sender, signed_outcomes_receiver) = async_channel::unbounded();
333        let (signatures_sender, signatures_receiver) = async_channel::unbounded();
334
335        let aleph_handle = spawn(
336            "aleph run session",
337            aleph_bft::run_session(
338                config,
339                aleph_bft::LocalIO::new(
340                    DataProvider::new(
341                        self.submission_receiver.clone(),
342                        signature_receiver,
343                        timestamp_sender,
344                        self.is_recovery().await,
345                    ),
346                    FinalizationHandler::new(unit_data_sender),
347                    BackupWriter::new(self.db.clone()).await,
348                    BackupReader::new(self.db.clone()),
349                ),
350                Network::new(
351                    connections.clone(),
352                    self.peer_activity.clone(),
353                    signed_outcomes_sender,
354                    signatures_sender,
355                    self.db.clone(),
356                ),
357                Keychain::new(&self.cfg),
358                Spawner::new(self.task_group.make_subgroup()),
359                aleph_bft::Terminator::create_root(terminator_receiver, "Terminator"),
360            ),
361        );
362
363        self.ord_latency_sender.send_replace(None);
364
365        let signed_session_outcome = self
366            .complete_signed_session_outcome(
367                session_index,
368                unit_data_receiver,
369                signature_sender,
370                timestamp_receiver,
371                signed_outcomes_receiver,
372                signatures_receiver,
373                connections,
374            )
375            .await?;
376
377        assert!(
378            self.validate_signed_session_outcome(&signed_session_outcome, session_index),
379            "Our created signed session outcome fails validation"
380        );
381
382        info!(target: LOG_CONSENSUS, ?session_index, "Terminating Aleph BFT session");
383
384        // We can terminate the session instead of waiting for other peers to complete
385        // it since they can always download the signed session outcome from us
386        terminator_sender.send(()).ok();
387        aleph_handle.await.ok();
388
389        // This method removes the backup of the current session from the database
390        // and therefore has to be called after we have waited for the session to
391        // shut down, or we risk write-write conflicts with the UnitSaver
392        self.complete_session(session_index, signed_session_outcome)
393            .await;
394
395        self.checkpoint_database(session_index);
396
397        Some(())
398    }
399
400    async fn is_recovery(&self) -> bool {
401        self.db
402            .begin_transaction_nc()
403            .await
404            .find_by_prefix(&AlephUnitsPrefix)
405            .await
406            .next()
407            .await
408            .is_some()
409    }
410
411    #[allow(clippy::too_many_arguments)]
412    pub async fn complete_signed_session_outcome(
413        &self,
414        session_index: u64,
415        ordered_unit_receiver: Receiver<OrderedUnit>,
416        signature_sender: watch::Sender<Option<schnorr::Signature>>,
417        timestamp_receiver: Receiver<Instant>,
418        signed_outcomes_receiver: Receiver<(PeerId, SignedSessionOutcome)>,
419        signatures_receiver: Receiver<(PeerId, schnorr::Signature)>,
420        _connections: DynP2PConnections<P2PMessage>,
421    ) -> Option<SignedSessionOutcome> {
422        // It is guaranteed that aleph bft will always replay all previously processed
423        // items from the current session from index zero
424        let mut item_index = 0;
425
426        // We request the signed session outcome every three seconds from a random peer
427        let mut index_broadcast_interval = tokio::time::interval(Duration::from_secs(3));
428
429        let mut request_signed_session_outcome = Box::pin(async {
430            self.request_signed_session_outcome(&self.federation_api, session_index)
431                .await
432        });
433
434        // We build a session outcome out of the ordered batches until either we have
435        // processed broadcast_rounds_per_session rounds or a threshold signed
436        // session outcome is obtained from our peers
437        loop {
438            tokio::select! {
439                result = ordered_unit_receiver.recv() => {
440                    let ordered_unit = result.ok()?;
441
442                    if ordered_unit.round >= self.cfg.consensus.broadcast_rounds_per_session {
443                        info!(
444                            target: LOG_CONSENSUS,
445                            session_index,
446                            "Reached Aleph BFT round limit, stopping item collection"
447                        );
448                        break;
449                    }
450
451                    if let Some(UnitData::Batch(bytes)) = ordered_unit.data {
452                        if ordered_unit.creator == self.identity() {
453                            match timestamp_receiver.try_recv() {
454                                Ok(timestamp) => {
455                                    let latency = match *self.ord_latency_sender.borrow() {
456                                        Some(latency) => (9 * latency +  timestamp.elapsed()) / 10,
457                                        None => timestamp.elapsed()
458                                    };
459
460                                    self.ord_latency_sender.send_replace(Some(latency));
461
462                                    CONSENSUS_ORDERING_LATENCY_SECONDS.observe(timestamp.elapsed().as_secs_f64());
463                                }
464                                Err(err) => {
465                                    debug!(target: LOG_CONSENSUS, err = %err.fmt_compact(), "Missing submission timestamp. This is normal in recovery");
466                                }
467                            }
468                        }
469
470                        match Vec::<ConsensusItem>::consensus_decode_whole(&bytes, &self.decoders()) {
471                            Ok(items) => {
472                                for item in items {
473                                    if let Ok(()) = self.process_consensus_item(
474                                        session_index,
475                                        item_index,
476                                        item,
477                                        ordered_unit.creator
478                                    ).await {
479                                        item_index += 1;
480                                    }
481                                }
482                            }
483                            Err(err) => {
484                                error!(
485                                    target: LOG_CONSENSUS,
486                                    session_index,
487                                    peer = %ordered_unit.creator,
488                                    err = %err.fmt_compact(),
489                                    "Failed to decode consensus items from peer"
490                                );
491                            }
492                        }
493                    }
494                },
495                // TODO: remove this branch in 0.11.0
496                signed_session_outcome = &mut request_signed_session_outcome => {
497                    info!(
498                        target: LOG_CONSENSUS,
499                        ?session_index,
500                        "Recovered signed session outcome from peers while processing consensus items"
501                    );
502
503                    let pending_accepted_items = self.pending_accepted_items().await;
504
505                    // this panics if we have more accepted items than the signed session outcome
506                    let (processed, unprocessed) = signed_session_outcome
507                        .session_outcome
508                        .items
509                        .split_at(pending_accepted_items.len());
510
511                    info!(
512                        target: LOG_CONSENSUS,
513                        session_index,
514                        processed = %processed.len(),
515                        unprocessed = %unprocessed.len(),
516                        "Processing remaining items..."
517                    );
518
519                    assert!(
520                        processed.iter().eq(pending_accepted_items.iter()),
521                        "Consensus Failure: pending accepted items disagree with federation consensus"
522                    );
523
524                    for (accepted_item, item_index) in unprocessed.iter().zip(processed.len()..) {
525                        if let Err(err) = self.process_consensus_item(
526                            session_index,
527                            item_index as u64,
528                            accepted_item.item.clone(),
529                            accepted_item.peer
530                        ).await {
531                            panic!(
532                                "Consensus Failure: rejected item accepted by federation consensus: {accepted_item:?}, items: {}+{}, session_idx: {session_index}, item_idx: {item_index}, err: {err}",
533                                processed.len(),
534                                unprocessed.len(),
535                            );
536                        }
537                    }
538
539                    return Some(signed_session_outcome);
540                },
541                result = signed_outcomes_receiver.recv() => {
542                    let (peer_id, p2p_outcome) = result.ok()?;
543
544                    // Validate signatures
545                    if self.validate_signed_session_outcome(&p2p_outcome, session_index) {
546                        info!(
547                            target: LOG_CONSENSUS,
548                            session_index,
549                            peer_id = %peer_id,
550                            "Received SignedSessionOutcome via P2P while collection signatures"
551                        );
552
553                        let pending_accepted_items = self.pending_accepted_items().await;
554
555                        // this panics if we have more accepted items than the signed session outcome
556                        let (processed, unprocessed) = p2p_outcome
557                            .session_outcome
558                            .items
559                            .split_at(pending_accepted_items.len());
560
561                        info!(
562                            target: LOG_CONSENSUS,
563                            ?session_index,
564                            processed = %processed.len(),
565                            unprocessed = %unprocessed.len(),
566                            "Processing remaining items..."
567                        );
568
569                        assert!(
570                            processed.iter().eq(pending_accepted_items.iter()),
571                            "Consensus Failure: pending accepted items disagree with federation consensus"
572                        );
573
574                        for (accepted_item, item_index) in unprocessed.iter().zip(processed.len()..) {
575                            if let Err(err) = self.process_consensus_item(
576                                session_index,
577                                item_index as u64,
578                                accepted_item.item.clone(),
579                                accepted_item.peer
580                            ).await {
581                                panic!(
582                                    "Consensus Failure: rejected item accepted by federation consensus: {accepted_item:?}, items: {}+{}, session_idx: {session_index}, item_idx: {item_index}, err: {err}",
583                                    processed.len(),
584                                    unprocessed.len(),
585                                );
586                            }
587                        }
588
589                        info!(
590                            target: LOG_CONSENSUS,
591                            ?session_index,
592                            peer_id = %peer_id,
593                            "Successfully recovered session via P2P"
594                        );
595
596                        return Some(p2p_outcome);
597                    }
598
599                    debug!(
600                        target: LOG_CONSENSUS,
601                        %peer_id,
602                        "Invalid P2P SignedSessionOutcome"
603                    );
604                }
605                _ = index_broadcast_interval.tick() => {
606                    // TODO: start sending the new messages in 0.11.0
607                    // connections.send(
608                    //     Recipient::Peer(self.random_peer()),
609                    //     P2PMessage::SessionIndex(session_index),
610                    // );
611                }
612            }
613        }
614
615        let items = self.pending_accepted_items().await;
616
617        assert_eq!(item_index, items.len() as u64);
618
619        info!(target: LOG_CONSENSUS, ?session_index, ?item_index, "Processed all items for session");
620
621        let session_outcome = SessionOutcome { items };
622
623        let header = session_outcome.header(session_index);
624
625        info!(
626            target: LOG_CONSENSUS,
627            ?session_index,
628            "Signing session header..."
629        );
630
631        let keychain = Keychain::new(&self.cfg);
632
633        let our_signature = keychain.sign_schnorr(&header);
634
635        // Send our own signature to the data provider to be submitted to AlephBFT
636        #[allow(clippy::disallowed_methods)]
637        signature_sender.send(Some(our_signature)).ok()?;
638
639        let mut signatures = BTreeMap::from_iter([(self.identity(), our_signature)]);
640
641        let items_dump = tokio::sync::OnceCell::new();
642
643        // We request the session signature every second to all peers
644        let mut signature_broadcast_interval = tokio::time::interval(Duration::from_secs(1));
645
646        // We collect the ordered signatures until we either obtain a threshold
647        // signature or a signed session outcome arrives from our peers
648        while signatures.len() < self.num_peers().threshold() {
649            tokio::select! {
650                // TODO: remove this branch in 0.11.0
651                result = ordered_unit_receiver.recv() => {
652                    let ordered_unit = result.ok()?;
653
654                    if let Some(UnitData::Signature(signature)) = ordered_unit.data {
655                        info!(
656                            target: LOG_CONSENSUS,
657                            ?session_index,
658                            peer = %ordered_unit.creator,
659                            "Collected signature from peer via AlephBFT, verifying..."
660                        );
661
662                        if keychain.verify(&header, &signature, to_node_index(ordered_unit.creator)){
663                            signatures.insert(ordered_unit.creator, schnorr::Signature::from_slice(&signature).expect("AlephBFT signature is valid"));
664                        } else {
665                            error!(
666                                target: LOG_CONSENSUS,
667                                ?session_index,
668                                peer = %ordered_unit.creator,
669                                "Consensus Failure: invalid header signature from peer"
670                            );
671
672                            items_dump.get_or_init(|| async {
673                                for (idx, item) in session_outcome.items.iter().enumerate() {
674                                    info!(target: LOG_CONSENSUS, idx, item = %DebugConsensusItemCompact(item), "Item");
675                                }
676                            }).await;
677                        }
678                    }
679                }
680                result = signatures_receiver.recv() => {
681                    let (peer_id, signature) = result.ok()?;
682
683                    if keychain.verify_schnorr(&header, &signature, peer_id) {
684                        signatures.insert(peer_id, signature);
685
686                        info!(
687                            target: LOG_CONSENSUS,
688                            session_index,
689                            peer_id = %peer_id,
690                            "Collected signature from peer via P2P"
691                        );
692                    }
693
694                    debug!(
695                        target: LOG_CONSENSUS,
696                        session_index,
697                        peer_id = %peer_id,
698                        "Invalid P2P signature from peer"
699                    );
700                }
701                // TODO: remove this branch in 0.11.0
702                signed_session_outcome = &mut request_signed_session_outcome => {
703                    info!(
704                        target: LOG_CONSENSUS,
705                        ?session_index,
706                        "Recovered signed session outcome from peers while collecting signatures"
707                    );
708
709                    assert_eq!(
710                        header,
711                        signed_session_outcome.session_outcome.header(session_index),
712                        "Consensus Failure: header disagrees with federation consensus"
713                    );
714
715                    return Some(signed_session_outcome);
716                },
717                result = signed_outcomes_receiver.recv() => {
718                    let (peer_id, p2p_outcome) = result.ok()?;
719
720                    if self.validate_signed_session_outcome(&p2p_outcome, session_index) {
721                        assert_eq!(
722                            header,
723                            p2p_outcome.session_outcome.header(session_index),
724                            "Consensus Failure: header disagrees with federation consensus"
725                        );
726
727                        info!(
728                            target: LOG_CONSENSUS,
729                            session_index,
730                            %peer_id,
731                            "Recovered session via P2P while collecting signatures"
732                        );
733
734                        return Some(p2p_outcome);
735                    }
736
737                    debug!(
738                        target: LOG_CONSENSUS,
739                        %peer_id,
740                        "Invalid P2P SignedSessionOutcome"
741                    );
742                }
743                _ = signature_broadcast_interval.tick() => {
744                    // TODO: start sending the new messages in 0.11.0
745                    // connections.send(
746                    //     Recipient::Everyone,
747                    //     P2PMessage::SessionSignature(our_signature),
748                    // );
749                }
750                _ = index_broadcast_interval.tick() => {
751                    // TODO: start sending the new messages in 0.11.0
752                    // connections.send(
753                    //     Recipient::Peer(self.random_peer()),
754                    //     P2PMessage::SessionIndex(session_index),
755                    // );
756                }
757            }
758        }
759
760        info!(
761            target: LOG_CONSENSUS,
762            session_index,
763            "Successfully collected threshold of signatures"
764        );
765
766        Some(SignedSessionOutcome {
767            session_outcome,
768            signatures,
769        })
770    }
771
772    /// Returns a random peer ID excluding ourselves
773    #[allow(unused)]
774    fn random_peer(&self) -> PeerId {
775        self.num_peers()
776            .peer_ids()
777            .filter(|p| *p != self.identity())
778            .choose(&mut rand::thread_rng())
779            .expect("We have at least three peers")
780    }
781
782    /// Validate a SignedSessionOutcome received via P2P
783    fn validate_signed_session_outcome(
784        &self,
785        outcome: &SignedSessionOutcome,
786        session_index: u64,
787    ) -> bool {
788        if outcome.signatures.len() != self.num_peers().threshold() {
789            return false;
790        }
791
792        let keychain = Keychain::new(&self.cfg);
793        let header = outcome.session_outcome.header(session_index);
794
795        outcome
796            .signatures
797            .iter()
798            .all(|(signer_id, sig)| keychain.verify_schnorr(&header, sig, *signer_id))
799    }
800
801    fn decoders(&self) -> ModuleDecoderRegistry {
802        self.modules.decoder_registry()
803    }
804
805    pub async fn pending_accepted_items(&self) -> Vec<AcceptedItem> {
806        self.db
807            .begin_transaction_nc()
808            .await
809            .find_by_prefix(&AcceptedItemPrefix)
810            .await
811            .map(|entry| entry.1)
812            .collect()
813            .await
814    }
815
816    pub async fn complete_session(
817        &self,
818        session_index: u64,
819        signed_session_outcome: SignedSessionOutcome,
820    ) {
821        let mut dbtx = self.db.begin_transaction().await;
822
823        dbtx.remove_by_prefix(&AlephUnitsPrefix).await;
824
825        dbtx.remove_by_prefix(&AcceptedItemPrefix).await;
826
827        if dbtx
828            .insert_entry(
829                &SignedSessionOutcomeKey(session_index),
830                &signed_session_outcome,
831            )
832            .await
833            .is_some()
834        {
835            panic!("We tried to overwrite a signed session outcome");
836        }
837
838        super::session_cache::publish_on_commit(
839            &mut dbtx,
840            self.completed_session_sender.clone(),
841            session_index,
842            &signed_session_outcome,
843        );
844        dbtx.commit_tx_result()
845            .await
846            .expect("This is the only place where we write to this key");
847    }
848
849    /// Returns the full path where the database checkpoints are stored.
850    fn db_checkpoints_dir(&self) -> PathBuf {
851        self.data_dir.join(DB_CHECKPOINTS_DIR)
852    }
853
854    /// Creates the directory within the data directory for storing the database
855    /// checkpoints or deletes checkpoints before `current_session` -
856    /// `checkpoint_retention`.
857    fn initialize_checkpoint_directory(&self, current_session: u64) -> anyhow::Result<()> {
858        let checkpoint_dir = self.db_checkpoints_dir();
859
860        if checkpoint_dir.exists() {
861            debug!(
862                target: LOG_CONSENSUS,
863                ?current_session,
864                "Removing database checkpoints up to `current_session`"
865            );
866
867            for checkpoint in fs::read_dir(checkpoint_dir)?.flatten() {
868                // Validate that the directory is a session index
869                if let Ok(file_name) = checkpoint.file_name().into_string()
870                    && let Ok(session) = file_name.parse::<u64>()
871                    && current_session >= self.db_checkpoint_retention
872                    && session < current_session - self.db_checkpoint_retention
873                {
874                    fs::remove_dir_all(checkpoint.path())?;
875                }
876            }
877        } else {
878            fs::create_dir_all(&checkpoint_dir)?;
879        }
880
881        Ok(())
882    }
883
884    /// Creates a backup of the database in the checkpoint directory. These
885    /// checkpoints can be used to restore the database in case the
886    /// federation falls out of consensus (recommended for experts only).
887    fn checkpoint_database(&self, session_index: u64) {
888        // If `checkpoint_retention` has been turned off, don't checkpoint the database
889        // at all.
890        if self.db_checkpoint_retention == 0 {
891            return;
892        }
893
894        let checkpoint_dir = self.db_checkpoints_dir();
895        let session_checkpoint_dir = checkpoint_dir.join(format!("{session_index}"));
896
897        {
898            let _timing /* logs on drop */ = timing::TimeReporter::new("database-checkpoint").level(Level::TRACE);
899            match self.db.checkpoint(&session_checkpoint_dir) {
900                Ok(()) => {
901                    debug!(target: LOG_CONSENSUS, ?session_checkpoint_dir, ?session_index, "Created db checkpoint");
902                }
903                Err(err) => {
904                    warn!(target: LOG_CONSENSUS, ?session_checkpoint_dir, ?session_index, err = %err.fmt_compact(), "Could not create db checkpoint");
905                }
906            }
907        }
908
909        {
910            // Check if any old checkpoint need to be cleaned up
911            let _timing /* logs on drop */ = timing::TimeReporter::new("remove-database-checkpoint").level(Level::TRACE);
912            if let Err(err) = self.delete_old_database_checkpoint(session_index, &checkpoint_dir) {
913                warn!(target: LOG_CONSENSUS, err = %err.fmt_compact(), "Could not delete old checkpoints");
914            }
915        }
916    }
917
918    /// Deletes the database checkpoint directory equal to `session_index` -
919    /// `checkpoint_retention`
920    fn delete_old_database_checkpoint(
921        &self,
922        session_index: u64,
923        checkpoint_dir: &Path,
924    ) -> anyhow::Result<()> {
925        if self.db_checkpoint_retention > session_index {
926            return Ok(());
927        }
928
929        let delete_session_index = session_index - self.db_checkpoint_retention;
930        let checkpoint_to_delete = checkpoint_dir.join(delete_session_index.to_string());
931        if checkpoint_to_delete.exists() {
932            fs::remove_dir_all(checkpoint_to_delete)?;
933        }
934
935        Ok(())
936    }
937
938    #[instrument(target = LOG_CONSENSUS, skip(self, item), level = "info")]
939    pub async fn process_consensus_item(
940        &self,
941        session_index: u64,
942        item_index: u64,
943        item: ConsensusItem,
944        peer: PeerId,
945    ) -> anyhow::Result<()> {
946        let _timing /* logs on drop */ = timing::TimeReporter::new("process_consensus_item").level(Level::TRACE);
947
948        let timing_prom = CONSENSUS_ITEM_PROCESSING_DURATION_SECONDS
949            .with_label_values(&[&peer.to_usize().to_string()])
950            .start_timer();
951
952        trace!(
953            target: LOG_CONSENSUS,
954            %peer,
955            item = ?DebugConsensusItem(&item),
956            "Processing consensus item"
957        );
958
959        self.ci_status_senders
960            .get(&peer)
961            .expect("No ci status sender for peer")
962            .send_replace(Some(session_index));
963
964        CONSENSUS_PEER_CONTRIBUTION_SESSION_IDX
965            .with_label_values(&[
966                &self.cfg.local.identity.to_usize().to_string(),
967                &peer.to_usize().to_string(),
968            ])
969            .set(session_index as i64);
970
971        let mut attempt: usize = 1;
972
973        let outcome = loop {
974            match self
975                .process_consensus_item_attempt(item_index, &item, peer)
976                .await
977            {
978                Ok(outcome) => break outcome,
979                Err(err) => {
980                    // A snapshot that aged out of the backend's write history is the only
981                    // commit failure we can do anything about, and the only way to recover
982                    // is to rerun the whole operation against a fresh transaction. Nothing
983                    // was written, and reprocessing an ordered item is deterministic, so
984                    // the replay is equivalent to having processed it a moment later.
985                    assert!(
986                        matches!(err, DatabaseError::SnapshotTooOld(_)),
987                        "Committing consensus item failed: {err}"
988                    );
989
990                    assert!(
991                        attempt < MAX_ITEM_PROCESSING_ATTEMPTS,
992                        "Committing consensus item failed after {attempt} attempts: {err}"
993                    );
994
995                    warn!(
996                        target: LOG_CONSENSUS,
997                        %peer,
998                        item_index,
999                        attempt,
1000                        err = %err.fmt_compact(),
1001                        "Consensus item transaction could not be validated - reprocessing item"
1002                    );
1003
1004                    attempt += 1;
1005                }
1006            }
1007        };
1008
1009        outcome?;
1010
1011        timing_prom.observe_duration();
1012
1013        Ok(())
1014    }
1015
1016    /// Process a single ordered consensus item in one database transaction.
1017    ///
1018    /// The outer `Err` means the transaction could not be committed and the
1019    /// caller has to rerun the whole operation in a fresh one. The inner `Err`
1020    /// means the item itself was rejected, which is a normal, final outcome.
1021    async fn process_consensus_item_attempt(
1022        &self,
1023        item_index: u64,
1024        item: &ConsensusItem,
1025        peer: PeerId,
1026    ) -> DatabaseResult<anyhow::Result<()>> {
1027        let mut dbtx = self.db.begin_transaction().await;
1028
1029        dbtx.ignore_uncommitted();
1030
1031        // When we recover from a mid-session crash aleph bft will replay the units that
1032        // were already processed before the crash. We therefore skip all consensus
1033        // items until we have seen every previously accepted items again.
1034        if let Some(existing_item) = dbtx.get_value(&AcceptedItemKey(item_index)).await {
1035            if existing_item.item == *item && existing_item.peer == peer {
1036                return Ok(Ok(()));
1037            }
1038
1039            return Ok(Err(anyhow!(
1040                "Item was discarded previously: existing: {existing_item:?} {}, current: {item:?}, {peer}",
1041                existing_item.peer
1042            )));
1043        }
1044
1045        if let Err(err) = self
1046            .process_consensus_item_with_db_transaction(&mut dbtx.to_ref_nc(), item, peer)
1047            .await
1048        {
1049            // Rejected items are very common, so only trace level
1050            trace!(
1051                target: LOG_CONSENSUS,
1052                %peer,
1053                item = ?DebugConsensusItem(item),
1054                err = %err.fmt_compact(),
1055                "Rejected consensus item"
1056            );
1057
1058            return Ok(Err(err));
1059        }
1060
1061        // After this point we have to commit the database transaction since the
1062        // item has been fully processed without errors
1063        dbtx.warn_uncommitted();
1064
1065        dbtx.insert_entry(
1066            &AcceptedItemKey(item_index),
1067            &AcceptedItem {
1068                item: item.clone(),
1069                peer,
1070            },
1071        )
1072        .await;
1073
1074        debug!(
1075            target: LOG_CONSENSUS,
1076            %peer,
1077            item = ?DebugConsensusItem(item),
1078            "Processed consensus item"
1079        );
1080        let mut audit = Audit::default();
1081
1082        for (module_instance_id, kind, module) in self.modules.iter_modules() {
1083            let _module_audit_timing =
1084                TimeReporter::new(format!("audit module {module_instance_id}")).level(Level::TRACE);
1085
1086            let timing_prom = CONSENSUS_ITEM_PROCESSING_MODULE_AUDIT_DURATION_SECONDS
1087                .with_label_values(&[
1088                    MODULE_INSTANCE_ID_GLOBAL.to_string().as_str(),
1089                    kind.as_str(),
1090                ])
1091                .start_timer();
1092
1093            module
1094                .audit(
1095                    &mut dbtx
1096                        .to_ref_with_prefix_module_id(module_instance_id)
1097                        .0
1098                        .into_nc(),
1099                    &mut audit,
1100                    module_instance_id,
1101                )
1102                .await;
1103
1104            timing_prom.observe_duration();
1105        }
1106
1107        assert!(
1108            audit
1109                .net_assets()
1110                .expect("Overflow while checking balance sheet")
1111                .milli_sat
1112                >= 0,
1113            "Balance sheet of the fed has gone negative, this should never happen! {audit}"
1114        );
1115
1116        dbtx.commit_tx_result().await?;
1117
1118        self.accepted_item_sender.send_replace(());
1119
1120        // Counted here rather than in the caller so that only an attempt that actually
1121        // committed is counted: the replay and rejection paths above return early, and
1122        // a failed attempt loops back around to a fresh transaction.
1123        CONSENSUS_ITEMS_PROCESSED_TOTAL
1124            .with_label_values(&[&peer.to_usize().to_string()])
1125            .inc();
1126
1127        Ok(Ok(()))
1128    }
1129
1130    async fn process_consensus_item_with_db_transaction(
1131        &self,
1132        dbtx: &mut DatabaseTransaction<'_>,
1133        consensus_item: &ConsensusItem,
1134        peer_id: PeerId,
1135    ) -> anyhow::Result<()> {
1136        // We rely on decoding rejecting any unknown module instance ids to avoid
1137        // peer-triggered panic here
1138        self.decoders().assert_reject_mode();
1139
1140        match consensus_item {
1141            ConsensusItem::Module(module_item) => {
1142                let instance_id = module_item.module_instance_id();
1143
1144                let module_dbtx = &mut dbtx.to_ref_with_prefix_module_id(instance_id).0;
1145
1146                self.modules
1147                    .get_expect(instance_id)
1148                    .process_consensus_item(module_dbtx, module_item, peer_id)
1149                    .await
1150            }
1151            ConsensusItem::Transaction(transaction) => {
1152                let txid = transaction.tx_hash();
1153                if dbtx
1154                    .get_value(&AcceptedTransactionKey(txid))
1155                    .await
1156                    .is_some()
1157                {
1158                    debug!(
1159                        target: LOG_CONSENSUS,
1160                        %txid,
1161                        "Transaction already accepted"
1162                    );
1163                    bail!("Transaction is already accepted");
1164                }
1165
1166                let modules_ids = transaction
1167                    .outputs
1168                    .iter()
1169                    .map(DynOutput::module_instance_id)
1170                    .collect::<Vec<_>>();
1171
1172                process_transaction_with_dbtx(
1173                    self.modules.clone(),
1174                    dbtx,
1175                    transaction,
1176                    self.cfg.consensus.version,
1177                    TxProcessingMode::Consensus,
1178                )
1179                .await
1180                .map_err(|error| anyhow!(error.to_string()))?;
1181
1182                debug!(target: LOG_CONSENSUS, %txid,  "Transaction accepted");
1183                dbtx.insert_entry(&AcceptedTransactionKey(txid), &modules_ids)
1184                    .await;
1185
1186                Ok(())
1187            }
1188            ConsensusItem::Default { variant, .. } => {
1189                // `ConsensusItem` has an `#[encodable_default]` variant, so an unknown
1190                // discriminant decodes successfully instead of erroring. The variant byte
1191                // is attacker-controlled: a malicious peer can put an arbitrary batch into
1192                // its unit, so this must be rejected rather than treated as unreachable.
1193                warn!(
1194                    target: LOG_CONSENSUS,
1195                    "Minor consensus version mismatch: unexpected consensus item type: {variant}"
1196                );
1197
1198                bail!("Unexpected consensus item type: {variant}")
1199            }
1200        }
1201    }
1202
1203    async fn request_signed_session_outcome(
1204        &self,
1205        federation_api: &DynGlobalApi,
1206        index: u64,
1207    ) -> SignedSessionOutcome {
1208        let decoders = self.decoders();
1209        let keychain = Keychain::new(&self.cfg);
1210        let threshold = self.num_peers().threshold();
1211
1212        let filter_map = move |response: SerdeModuleEncoding<SignedSessionOutcome>| {
1213            let signed_session_outcome = response
1214                .try_into_inner(&decoders)
1215                .map_err(|x| ServerError::ResponseDeserialization(Box::new(x)))?;
1216            let header = signed_session_outcome.session_outcome.header(index);
1217            if signed_session_outcome.signatures.len() == threshold
1218                && signed_session_outcome
1219                    .signatures
1220                    .iter()
1221                    .all(|(peer_id, sig)| keychain.verify_schnorr(&header, sig, *peer_id))
1222            {
1223                Ok(signed_session_outcome)
1224            } else {
1225                Err(ServerError::InvalidResponse(
1226                    "Invalid signatures".to_string(),
1227                ))
1228            }
1229        };
1230
1231        federation_api
1232            .request_with_strategy_retry(
1233                FilterMap::new(filter_map.clone()),
1234                AWAIT_SIGNED_SESSION_OUTCOME_ENDPOINT.to_string(),
1235                ApiRequestErased::new(index),
1236            )
1237            .await
1238    }
1239
1240    /// Returns the number of sessions already saved in the database. This count
1241    /// **does not** include the currently running session.
1242    async fn get_finished_session_count(&self) -> u64 {
1243        get_finished_session_count_static(&mut self.db.begin_transaction_nc().await).await
1244    }
1245}
1246
1247/// The request of a shutdown that takes effect now that `completed_session`
1248/// has completed: at the first session boundary at or after the scheduled
1249/// session, so a schedule that arrives after its session completed still
1250/// stops at the next boundary rather than never.
1251fn due_shutdown(
1252    scheduled: Option<ScheduledShutdown>,
1253    completed_session: u64,
1254) -> Option<ShutdownRequest> {
1255    scheduled
1256        .filter(|scheduled| scheduled.session <= completed_session)
1257        .map(|scheduled| scheduled.request)
1258}
1259
1260pub async fn get_finished_session_count_static(dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1261    dbtx.find_by_prefix_sorted_descending(&SignedSessionOutcomePrefix)
1262        .await
1263        .next()
1264        .await
1265        .map_or(0, |entry| (entry.0.0) + 1)
1266}
1267
1268#[cfg(test)]
1269mod tests;