fedimint_core/
session_outcome.rs

1use std::collections::BTreeMap;
2use std::io::Write as _;
3
4use bitcoin::hashes::{Hash, sha256};
5use secp256k1::{Message, PublicKey, SECP256K1};
6
7use crate::encoding::{Decodable, Encodable};
8use crate::epoch::ConsensusItem;
9use crate::{NumPeersExt as _, PeerId, secp256k1};
10
11/// A consensus item accepted in the consensus
12///
13/// If two correct nodes obtain two ordered items from the broadcast they
14/// are guaranteed to be in the same order. However, an ordered items is
15/// only guaranteed to be seen by all correct nodes if a correct node decides to
16/// accept it.
17#[derive(Clone, Debug, PartialEq, Eq, Encodable, Decodable)]
18pub struct AcceptedItem {
19    pub item: ConsensusItem,
20    pub peer: PeerId,
21}
22
23/// Items ordered in a single session that have been accepted by Fedimint
24/// consensus.
25///
26/// A running Federation produces a [`SessionOutcome`] every couple of minutes.
27/// Therefore, just like in Bitcoin, a [`SessionOutcome`] might be empty if no
28/// items are ordered in that time or all ordered items are discarded by
29/// Fedimint Consensus.
30///
31/// When session is closed it is signed over by the peers and produces a
32/// [`SignedSessionOutcome`].
33#[derive(Clone, Debug, PartialEq, Eq, Encodable, Decodable)]
34pub struct SessionOutcome {
35    pub items: Vec<AcceptedItem>,
36}
37
38impl SessionOutcome {
39    /// A blocks header consists of 40 bytes formed by its index in big endian
40    /// bytes concatenated with the merkle root build from the consensus
41    /// hashes of its [`AcceptedItem`]s or 32 zero bytes if the block is
42    /// empty. The use of a merkle tree allows for efficient inclusion
43    /// proofs of accepted consensus items for clients.
44    pub fn header(&self, index: u64) -> [u8; 40] {
45        let mut header = [0; 40];
46
47        header[..8].copy_from_slice(&index.to_be_bytes());
48
49        let leaf_hashes = self
50            .items
51            .iter()
52            .map(Encodable::consensus_hash::<sha256::Hash>);
53
54        if let Some(root) = bitcoin::merkle_tree::calculate_root(leaf_hashes) {
55            header[8..].copy_from_slice(&root.to_byte_array());
56        } else {
57            assert!(self.items.is_empty());
58        }
59
60        header
61    }
62}
63
64/// A [`SessionOutcome`], signed by the Federation.
65///
66/// A signed block combines a block with the naive threshold secp schnorr
67/// signature for its header created by the federation. The signed blocks allow
68/// clients and recovering guardians to verify the federations consensus
69/// history. After a signed block has been created it is stored in the database.
70#[derive(Clone, Debug, Encodable, Decodable, Eq, PartialEq)]
71pub struct SignedSessionOutcome {
72    pub session_outcome: SessionOutcome,
73    pub signatures: std::collections::BTreeMap<PeerId, secp256k1::schnorr::Signature>,
74}
75
76impl SignedSessionOutcome {
77    pub fn verify(
78        &self,
79        broadcast_public_keys: &BTreeMap<PeerId, PublicKey>,
80        block_index: u64,
81    ) -> bool {
82        let message = {
83            let mut engine = sha256::HashEngine::default();
84            engine
85                .write_all(broadcast_public_keys.consensus_hash_sha256().as_ref())
86                .expect("Writing to a hash engine can not fail");
87            engine
88                .write_all(&self.session_outcome.header(block_index))
89                .expect("Writing to a hash engine can not fail");
90            Message::from_digest(sha256::Hash::from_engine(engine).to_byte_array())
91        };
92
93        let threshold = broadcast_public_keys.to_num_peers().threshold();
94        if self.signatures.len() < threshold {
95            return false;
96        }
97
98        self.signatures.iter().all(|(peer_id, signature)| {
99            let Some(pub_key) = broadcast_public_keys.get(peer_id) else {
100                return false;
101            };
102
103            SECP256K1
104                .verify_schnorr(signature, &message, &pub_key.x_only_public_key().0)
105                .is_ok()
106        })
107    }
108}
109
110#[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable)]
111pub enum SessionStatus {
112    Initial,
113    Pending(Vec<AcceptedItem>),
114    Complete(SessionOutcome),
115}
116
117#[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable)]
118pub enum SessionStatusV2 {
119    Initial,
120    Pending(Vec<AcceptedItem>),
121    Complete(SignedSessionOutcome),
122}
123
124impl From<SessionStatusV2> for SessionStatus {
125    fn from(value: SessionStatusV2) -> Self {
126        match value {
127            SessionStatusV2::Initial => Self::Initial,
128            SessionStatusV2::Pending(items) => Self::Pending(items),
129            SessionStatusV2::Complete(signed_session_outcome) => {
130                Self::Complete(signed_session_outcome.session_outcome)
131            }
132        }
133    }
134}