fedimint_core/
session_outcome.rs1use 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#[derive(Debug, Clone, Eq, PartialEq, Hash, Encodable, Decodable)]
14pub enum ConsensusItem {
15 Transaction(Transaction),
17 Module(ModuleConsensusItem),
19 #[encodable_default]
22 Default { variant: u64, bytes: Vec<u8> },
23}
24
25#[derive(Clone, Debug, PartialEq, Eq, Encodable, Decodable)]
32pub struct AcceptedItem {
33 pub item: ConsensusItem,
34 pub peer: PeerId,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq, Encodable, Decodable)]
48pub struct SessionOutcome {
49 pub items: Vec<AcceptedItem>,
50}
51
52impl SessionOutcome {
53 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#[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}