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
63const DB_CHECKPOINTS_DIR: &str = "db_checkpoints";
65
66const MAX_ITEM_PROCESSING_ATTEMPTS: usize = 10;
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum ShutdownRequest {
75 AtSession,
77 AfterCurrentSession,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub struct ScheduledShutdown {
84 pub session: u64,
87 pub request: ShutdownRequest,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum ConsensusStop {
93 TaskShutdown,
96 ScheduledShutdown(ShutdownRequest),
99}
100
101pub 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 pub accepted_item_sender: watch::Sender<()>,
114 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 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 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 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 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 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 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 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 terminator_sender.send(()).ok();
387 aleph_handle.await.ok();
388
389 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 let mut item_index = 0;
425
426 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 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 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 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 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 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 }
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 #[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 let mut signature_broadcast_interval = tokio::time::interval(Duration::from_secs(1));
645
646 while signatures.len() < self.num_peers().threshold() {
649 tokio::select! {
650 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 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 }
750 _ = index_broadcast_interval.tick() => {
751 }
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 #[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 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 fn db_checkpoints_dir(&self) -> PathBuf {
851 self.data_dir.join(DB_CHECKPOINTS_DIR)
852 }
853
854 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 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 fn checkpoint_database(&self, session_index: u64) {
888 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 = 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 let _timing = 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 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 = 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 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 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 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 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 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 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 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 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 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
1247fn 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;