1use std::collections::BTreeMap;
2
3use fedimint_client::DynGlobalClientContext;
4use fedimint_client_module::module::OutPointRange;
5use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
6use fedimint_core::core::OperationId;
7use fedimint_core::db::IDatabaseTransactionOpsCoreTyped;
8use fedimint_core::encoding::{Decodable, Encodable};
9use fedimint_core::{PeerId, runtime};
10use fedimint_mintv2_common::{Denomination, verify_note};
11use serde::{Deserialize, Serialize};
12use tbs::{
13 AggregatePublicKey, BlindedSignature, BlindedSignatureShare, PublicKeyShare,
14 aggregate_signature_shares,
15};
16
17use crate::api::MintV2ModuleApi;
18use crate::client_db::SpendableNoteKey;
19use crate::{MintClientContext, NoteIssuanceRequest};
20
21#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
22pub struct MintOutputStateMachine {
23 pub common: OutputSMCommon,
24 pub state: OutputSMState,
25}
26
27#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
28pub struct OutputSMCommon {
29 pub operation_id: OperationId,
30 pub range: Option<OutPointRange>,
31 pub issuance_requests: Vec<NoteIssuanceRequest>,
32}
33
34#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
35pub enum OutputSMState {
36 Pending,
38 Aborted,
41 Failure,
45 Success,
48}
49
50impl State for MintOutputStateMachine {
51 type ModuleContext = MintClientContext;
52
53 fn transitions(
54 &self,
55 context: &Self::ModuleContext,
56 global_context: &DynGlobalClientContext,
57 ) -> Vec<StateTransition<Self>> {
58 let context = context.clone();
59
60 match &self.state {
61 OutputSMState::Pending => {
62 vec![StateTransition::new(
63 Self::await_issuance(
64 global_context.clone(),
65 self.common.range,
66 self.common.issuance_requests.clone(),
67 context.tbs_pks.clone(),
68 context.tbs_agg_pks.clone(),
69 ),
70 move |dbtx, outcome, old_state| {
71 let balance_update_sender = context.balance_update_sender.clone();
72
73 dbtx.module_tx()
74 .on_commit(move || balance_update_sender.send_replace(()));
75
76 Box::pin(Self::transition_issuance(dbtx, outcome, old_state))
77 },
78 )]
79 }
80 OutputSMState::Aborted | OutputSMState::Failure | OutputSMState::Success => {
81 vec![]
82 }
83 }
84 }
85
86 fn operation_id(&self) -> OperationId {
87 self.common.operation_id
88 }
89}
90
91#[derive(Debug, Serialize, Deserialize)]
96pub enum IssuanceOutcome {
97 Aborted,
99 Signatures {
103 signatures: Vec<BlindedSignature>,
104 invalid: bool,
105 },
106}
107
108impl MintOutputStateMachine {
109 async fn await_issuance(
110 global_context: DynGlobalClientContext,
111 range: Option<OutPointRange>,
112 issuance_requests: Vec<NoteIssuanceRequest>,
113 tbs_pks: BTreeMap<Denomination, BTreeMap<PeerId, PublicKeyShare>>,
114 tbs_agg_pks: BTreeMap<Denomination, AggregatePublicKey>,
115 ) -> IssuanceOutcome {
116 let signature_shares = if let Some(range) = range {
117 if global_context.await_tx_accepted(range.txid).await.is_err() {
118 return IssuanceOutcome::Aborted;
119 }
120
121 global_context
122 .module_api()
123 .fetch_signature_shares(range, issuance_requests.clone(), tbs_pks)
124 .await
125 } else {
126 global_context
127 .module_api()
128 .fetch_signature_shares_recovery(issuance_requests.clone(), tbs_pks)
129 .await
130 };
131
132 runtime::spawn_blocking(move || {
135 let mut signatures = Vec::with_capacity(issuance_requests.len());
136
137 for (i, request) in issuance_requests.iter().enumerate() {
138 let agg_blind_signature = aggregate_signature_shares(
139 &signature_shares
140 .iter()
141 .map(|(peer, shares)| (peer.to_usize() as u64, shares[i]))
142 .collect(),
143 );
144
145 let spendable_note = request.finalize(agg_blind_signature);
146
147 let pk = *tbs_agg_pks
148 .get(&request.denomination)
149 .expect("No aggregated pk found for denomination");
150
151 if !verify_note(spendable_note.note(), pk) {
152 return IssuanceOutcome::Signatures {
153 signatures,
154 invalid: true,
155 };
156 }
157
158 signatures.push(agg_blind_signature);
159 }
160
161 IssuanceOutcome::Signatures {
162 signatures,
163 invalid: false,
164 }
165 })
166 .await
167 }
168
169 async fn transition_issuance(
170 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
171 outcome: IssuanceOutcome,
172 old_state: MintOutputStateMachine,
173 ) -> MintOutputStateMachine {
174 let (signatures, invalid) = match outcome {
175 IssuanceOutcome::Aborted => {
176 return MintOutputStateMachine {
177 common: old_state.common,
178 state: OutputSMState::Aborted,
179 };
180 }
181 IssuanceOutcome::Signatures {
182 signatures,
183 invalid,
184 } => (signatures, invalid),
185 };
186
187 for (request, signature) in old_state.common.issuance_requests.iter().zip(signatures) {
188 dbtx.module_tx()
189 .insert_new_entry(&SpendableNoteKey(request.finalize(signature)), &())
190 .await;
191 }
192
193 MintOutputStateMachine {
194 common: old_state.common,
195 state: if invalid {
196 OutputSMState::Failure
197 } else {
198 OutputSMState::Success
199 },
200 }
201 }
202}
203
204pub fn verify_blind_shares(
205 peer: PeerId,
206 signature_shares: Vec<BlindedSignatureShare>,
207 issuance_requests: &[NoteIssuanceRequest],
208 tbs_pks: &BTreeMap<Denomination, BTreeMap<PeerId, PublicKeyShare>>,
209) -> Result<Vec<BlindedSignatureShare>, VerifyBlindSharesError> {
210 if signature_shares.len() != issuance_requests.len() {
211 return Err(VerifyBlindSharesError::ShareCount);
212 }
213
214 for (request, share) in issuance_requests.iter().zip(signature_shares.iter()) {
215 let amount_key = tbs_pks
216 .get(&request.denomination)
217 .expect("No pk shares found for denomination")
218 .get(&peer)
219 .expect("No pk share found for peer");
220
221 if !tbs::verify_signature_share(request.blinded_message(), *share, *amount_key) {
222 return Err(VerifyBlindSharesError::InvalidShare);
223 }
224 }
225
226 Ok(signature_shares)
227}
228
229#[derive(Debug, thiserror::Error)]
231pub(crate) enum VerifyBlindSharesError {
232 #[error("Invalid number of signatures shares")]
235 ShareCount,
236
237 #[error("Invalid blind signature")]
239 InvalidShare,
240}