Skip to main content

fedimint_core/
session_outcome.rs

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