fedimint_core/
session_outcome.rs

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