1use std::collections::BTreeMap;
2use std::hash;
3
4use anyhow::{anyhow, bail};
5use fedimint_api_client::api::{
6 FederationApiExt, PeerError, SerdeOutputOutcome, deserialize_outcome,
7};
8use fedimint_api_client::query::FilterMapThreshold;
9use fedimint_client_module::DynGlobalClientContext;
10use fedimint_client_module::module::{ClientContext, OutPointRange};
11use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
12use fedimint_core::core::{Decoder, OperationId};
13use fedimint_core::db::IDatabaseTransactionOpsCoreTyped;
14use fedimint_core::encoding::{Decodable, Encodable};
15use fedimint_core::module::ApiRequestErased;
16use fedimint_core::secp256k1::{Keypair, Secp256k1, Signing};
17use fedimint_core::{Amount, NumPeersExt, OutPoint, PeerId, Tiered, TransactionId, crit};
18use fedimint_derive_secret::{ChildId, DerivableSecret};
19use fedimint_logging::LOG_CLIENT_MODULE_MINT;
20use fedimint_mint_common::endpoint_constants::AWAIT_OUTPUT_OUTCOME_ENDPOINT;
21use fedimint_mint_common::{BlindNonce, MintOutputOutcome, Nonce};
22use futures::future::join_all;
23use rayon::iter::{IndexedParallelIterator, IntoParallelIterator as _, ParallelIterator as _};
24use serde::{Deserialize, Serialize};
25use tbs::{
26 AggregatePublicKey, BlindedMessage, BlindedSignature, BlindedSignatureShare, BlindingKey,
27 PublicKeyShare, aggregate_signature_shares, blind_message, unblind_signature,
28};
29use tracing::debug;
30
31use crate::client_db::NoteKey;
32use crate::event::NoteCreated;
33use crate::{MintClientContext, MintClientModule, SpendableNote};
34
35const SPEND_KEY_CHILD_ID: ChildId = ChildId(0);
37
38const BLINDING_KEY_CHILD_ID: ChildId = ChildId(1);
40
41#[cfg_attr(doc, aquamarine::aquamarine)]
42#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
56pub enum MintOutputStates {
57 Created(MintOutputStatesCreated),
59 Aborted(MintOutputStatesAborted),
62 Failed(MintOutputStatesFailed),
67 Succeeded(MintOutputStatesSucceeded),
70 CreatedMulti(MintOutputStatesCreatedMulti),
72}
73
74#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
75pub struct MintOutputCommonV0 {
76 pub(crate) operation_id: OperationId,
77 pub(crate) out_point: OutPoint,
78}
79
80#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
81pub struct MintOutputCommon {
82 pub(crate) operation_id: OperationId,
83 pub(crate) out_point_range: OutPointRange,
84}
85
86impl MintOutputCommon {
87 pub fn txid(self) -> TransactionId {
88 self.out_point_range.txid()
89 }
90}
91
92#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
93pub struct MintOutputStateMachineV0 {
94 pub(crate) common: MintOutputCommonV0,
95 pub(crate) state: MintOutputStates,
96}
97
98#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
99pub struct MintOutputStateMachine {
100 pub(crate) common: MintOutputCommon,
101 pub(crate) state: MintOutputStates,
102}
103
104impl State for MintOutputStateMachine {
105 type ModuleContext = MintClientContext;
106
107 fn transitions(
108 &self,
109 context: &Self::ModuleContext,
110 global_context: &DynGlobalClientContext,
111 ) -> Vec<StateTransition<Self>> {
112 match &self.state {
113 MintOutputStates::Created(created) => {
114 created.transitions(context, global_context, self.common)
115 }
116 MintOutputStates::CreatedMulti(created) => {
117 created.transitions(context, global_context, self.common)
118 }
119 MintOutputStates::Aborted(_)
120 | MintOutputStates::Failed(_)
121 | MintOutputStates::Succeeded(_) => {
122 vec![]
123 }
124 }
125 }
126
127 fn operation_id(&self) -> OperationId {
128 self.common.operation_id
129 }
130}
131
132#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
134pub struct MintOutputStatesCreated {
135 pub(crate) amount: Amount,
136 pub(crate) issuance_request: NoteIssuanceRequest,
137}
138
139impl MintOutputStatesCreated {
140 fn transitions(
141 &self,
142 context: &MintClientContext,
144 global_context: &DynGlobalClientContext,
145 common: MintOutputCommon,
146 ) -> Vec<StateTransition<MintOutputStateMachine>> {
147 let tbs_pks = context.tbs_pks.clone();
148 let client_ctx = context.client_ctx.clone();
149
150 vec![
151 StateTransition::new(
153 Self::await_tx_rejected(global_context.clone(), common),
154 |_dbtx, (), state| Box::pin(async move { Self::transition_tx_rejected(&state) }),
155 ),
156 StateTransition::new(
158 Self::await_outcome_ready(
159 global_context.clone(),
160 common,
161 context.mint_decoder.clone(),
162 self.amount,
163 self.issuance_request.blinded_message(),
164 context.peer_tbs_pks.clone(),
165 ),
166 move |dbtx, blinded_signature_shares, old_state| {
167 Box::pin(Self::transition_outcome_ready(
168 client_ctx.clone(),
169 dbtx,
170 blinded_signature_shares,
171 old_state,
172 tbs_pks.clone(),
173 ))
174 },
175 ),
176 ]
177 }
178
179 async fn await_tx_rejected(global_context: DynGlobalClientContext, common: MintOutputCommon) {
180 if global_context
181 .await_tx_accepted(common.txid())
182 .await
183 .is_err()
184 {
185 return;
186 }
187 std::future::pending::<()>().await;
188 }
189
190 fn transition_tx_rejected(old_state: &MintOutputStateMachine) -> MintOutputStateMachine {
191 assert!(matches!(old_state.state, MintOutputStates::Created(_)));
192
193 MintOutputStateMachine {
194 common: old_state.common,
195 state: MintOutputStates::Aborted(MintOutputStatesAborted),
196 }
197 }
198
199 async fn await_outcome_ready(
200 global_context: DynGlobalClientContext,
201 common: MintOutputCommon,
202 module_decoder: Decoder,
203 amount: Amount,
204 message: BlindedMessage,
205 tbs_pks: BTreeMap<PeerId, Tiered<PublicKeyShare>>,
206 ) -> BTreeMap<PeerId, BlindedSignatureShare> {
207 global_context
208 .api()
209 .request_with_strategy_retry(
210 FilterMapThreshold::new(
212 move |peer, outcome| {
213 verify_blind_share(
214 peer,
215 &outcome,
216 amount,
217 message,
218 &module_decoder,
219 &tbs_pks,
220 )
221 .map_err(PeerError::InvalidResponse)
222 },
223 global_context.api().all_peers().to_num_peers(),
224 ),
225 AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
226 ApiRequestErased::new(OutPoint {
227 txid: common.txid(),
228 out_idx: common.out_point_range.start_idx(),
229 }),
230 )
231 .await
232 }
233
234 async fn transition_outcome_ready(
235 client_ctx: ClientContext<MintClientModule>,
236 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
237 blinded_signature_shares: BTreeMap<PeerId, BlindedSignatureShare>,
238 old_state: MintOutputStateMachine,
239 tbs_pks: Tiered<AggregatePublicKey>,
240 ) -> MintOutputStateMachine {
241 let MintOutputStates::Created(created) = old_state.state else {
245 panic!("Unexpected prior state")
246 };
247
248 let agg_blind_signature = aggregate_signature_shares(
249 &blinded_signature_shares
250 .into_iter()
251 .map(|(peer, share)| (peer.to_usize() as u64, share))
252 .collect(),
253 );
254
255 let amount_key = tbs_pks
256 .tier(&created.amount)
257 .expect("We obtained this amount from tbs_pks when we created the output");
258
259 if !tbs::verify_blinded_signature(
261 created.issuance_request.blinded_message(),
262 agg_blind_signature,
263 *amount_key,
264 ) {
265 return MintOutputStateMachine {
266 common: old_state.common,
267 state: MintOutputStates::Failed(MintOutputStatesFailed {
268 error: "Invalid blind signature".to_string(),
269 }),
270 };
271 }
272
273 let spendable_note = created.issuance_request.finalize(agg_blind_signature);
274
275 assert!(spendable_note.note().verify(*amount_key));
276
277 debug!(target: LOG_CLIENT_MODULE_MINT, amount = %created.amount, note=%spendable_note, "Adding new note from transaction output");
278
279 client_ctx
280 .log_event(
281 &mut dbtx.module_tx(),
282 NoteCreated {
283 nonce: spendable_note.nonce(),
284 },
285 )
286 .await;
287 if let Some(note) = dbtx
288 .module_tx()
289 .insert_entry(
290 &NoteKey {
291 amount: created.amount,
292 nonce: spendable_note.nonce(),
293 },
294 &spendable_note.to_undecoded(),
295 )
296 .await
297 {
298 crit!(target: LOG_CLIENT_MODULE_MINT, %note, "E-cash note was replaced in DB");
299 }
300
301 MintOutputStateMachine {
302 common: old_state.common,
303 state: MintOutputStates::Succeeded(MintOutputStatesSucceeded {
304 amount: created.amount,
305 }),
306 }
307 }
308}
309
310#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
312pub struct MintOutputStatesCreatedMulti {
313 pub(crate) issuance_requests: BTreeMap<u64, (Amount, NoteIssuanceRequest)>,
314}
315
316impl MintOutputStatesCreatedMulti {
317 fn transitions(
318 &self,
319 context: &MintClientContext,
321 global_context: &DynGlobalClientContext,
322 common: MintOutputCommon,
323 ) -> Vec<StateTransition<MintOutputStateMachine>> {
324 let tbs_pks = context.tbs_pks.clone();
325 let client_ctx = context.client_ctx.clone();
326
327 vec![
328 StateTransition::new(
330 Self::await_tx_rejected(global_context.clone(), common),
331 |_dbtx, (), state| Box::pin(async move { Self::transition_tx_rejected(&state) }),
332 ),
333 StateTransition::new(
335 Self::await_outcome_ready(
336 global_context.clone(),
337 common,
338 context.mint_decoder.clone(),
339 self.issuance_requests.clone(),
340 context.peer_tbs_pks.clone(),
341 ),
342 move |dbtx, blinded_signature_shares, old_state| {
343 Box::pin(Self::transition_outcome_ready(
344 client_ctx.clone(),
345 dbtx,
346 blinded_signature_shares,
347 old_state,
348 tbs_pks.clone(),
349 ))
350 },
351 ),
352 ]
353 }
354
355 async fn await_tx_rejected(global_context: DynGlobalClientContext, common: MintOutputCommon) {
356 if global_context
357 .await_tx_accepted(common.txid())
358 .await
359 .is_err()
360 {
361 return;
362 }
363 std::future::pending::<()>().await;
364 }
365
366 fn transition_tx_rejected(old_state: &MintOutputStateMachine) -> MintOutputStateMachine {
367 assert!(matches!(old_state.state, MintOutputStates::CreatedMulti(_)));
368
369 MintOutputStateMachine {
370 common: old_state.common,
371 state: MintOutputStates::Aborted(MintOutputStatesAborted),
372 }
373 }
374
375 async fn await_outcome_ready(
376 global_context: DynGlobalClientContext,
377 common: MintOutputCommon,
378 module_decoder: Decoder,
379 issuance_requests: BTreeMap<u64, (Amount, NoteIssuanceRequest)>,
380 tbs_pks: BTreeMap<PeerId, Tiered<PublicKeyShare>>,
381 ) -> Vec<(u64, BTreeMap<PeerId, BlindedSignatureShare>)> {
382 let mut ret = vec![];
383 let api = global_context.api();
386 let mut issuance_requests_iter = issuance_requests.into_iter();
387
388 if let Some((out_idx, (amount, issuance_request))) = issuance_requests_iter.next() {
391 let module_decoder = module_decoder.clone();
392 let tbs_pks = tbs_pks.clone();
393
394 let blinded_sig_share = api
395 .request_with_strategy_retry(
396 FilterMapThreshold::new(
399 move |peer, outcome| {
400 verify_blind_share(
401 peer,
402 &outcome,
403 amount,
404 issuance_request.blinded_message(),
405 &module_decoder,
406 &tbs_pks,
407 )
408 .map_err(PeerError::InvalidResponse)
409 },
410 api.all_peers().to_num_peers(),
411 ),
412 AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
413 ApiRequestErased::new(OutPoint {
414 txid: common.txid(),
415 out_idx,
416 }),
417 )
418 .await;
419
420 ret.push((out_idx, blinded_sig_share));
421 } else {
422 return vec![];
424 }
425
426 ret.extend(
428 join_all(
429 issuance_requests_iter.map(|(out_idx, (amount, issuance_request))| {
430 let module_decoder = module_decoder.clone();
431 let tbs_pks = tbs_pks.clone();
432 async move {
433 let blinded_sig_share = api
434 .request_with_strategy_retry(
435 FilterMapThreshold::new(
438 move |peer, outcome| {
439 verify_blind_share(
440 peer,
441 &outcome,
442 amount,
443 issuance_request.blinded_message(),
444 &module_decoder,
445 &tbs_pks,
446 )
447 .map_err(PeerError::InvalidResponse)
448 },
449 api.all_peers().to_num_peers(),
450 ),
451 AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
452 ApiRequestErased::new(OutPoint {
453 txid: common.txid(),
454 out_idx,
455 }),
456 )
457 .await;
458
459 (out_idx, blinded_sig_share)
460 }
461 }),
462 )
463 .await,
464 );
465
466 ret
467 }
468
469 async fn transition_outcome_ready(
470 client_ctx: ClientContext<MintClientModule>,
471 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
472 blinded_signature_shares: Vec<(u64, BTreeMap<PeerId, BlindedSignatureShare>)>,
473 old_state: MintOutputStateMachine,
474 tbs_pks: Tiered<AggregatePublicKey>,
475 ) -> MintOutputStateMachine {
476 let mut amount_total = Amount::ZERO;
480 let MintOutputStates::CreatedMulti(created) = old_state.state else {
481 panic!("Unexpected prior state")
482 };
483
484 let mut spendable_notes: Vec<(Amount, SpendableNote)> = vec![];
485
486 blinded_signature_shares
488 .into_par_iter()
489 .map(|(out_idx, blinded_signature_shares)| {
490 let agg_blind_signature = aggregate_signature_shares(
491 &blinded_signature_shares
492 .into_iter()
493 .map(|(peer, share)| (peer.to_usize() as u64, share))
494 .collect(),
495 );
496
497 let (amount, issuance_request) =
499 created.issuance_requests.get(&out_idx).expect("Must have");
500
501 let amount_key = tbs_pks.tier(amount).expect("Must have keys for any amount");
502
503 let spendable_note = issuance_request.finalize(agg_blind_signature);
504
505 assert!(spendable_note.note().verify(*amount_key), "We checked all signature shares in the trigger future, so the combined signature has to be valid");
506
507 (*amount, spendable_note)
508 })
509 .collect_into_vec(&mut spendable_notes);
510
511 for (amount, spendable_note) in spendable_notes {
512 debug!(target: LOG_CLIENT_MODULE_MINT, amount = %amount, note=%spendable_note, "Adding new note from transaction output");
513
514 client_ctx
515 .log_event(
516 &mut dbtx.module_tx(),
517 NoteCreated {
518 nonce: spendable_note.nonce(),
519 },
520 )
521 .await;
522
523 amount_total += amount;
524 if let Some(note) = dbtx
525 .module_tx()
526 .insert_entry(
527 &NoteKey {
528 amount,
529 nonce: spendable_note.nonce(),
530 },
531 &spendable_note.to_undecoded(),
532 )
533 .await
534 {
535 crit!(target: LOG_CLIENT_MODULE_MINT, %note, "E-cash note was replaced in DB");
536 }
537 }
538 MintOutputStateMachine {
539 common: old_state.common,
540 state: MintOutputStates::Succeeded(MintOutputStatesSucceeded {
541 amount: amount_total,
542 }),
543 }
544 }
545}
546
547pub fn verify_blind_share(
550 peer: PeerId,
551 outcome: &SerdeOutputOutcome,
552 amount: Amount,
553 blinded_message: BlindedMessage,
554 decoder: &Decoder,
555 peer_tbs_pks: &BTreeMap<PeerId, Tiered<PublicKeyShare>>,
556) -> anyhow::Result<BlindedSignatureShare> {
557 let outcome = deserialize_outcome::<MintOutputOutcome>(outcome, decoder)?;
558
559 let blinded_signature_share = outcome
560 .ensure_v0_ref()
561 .expect("We only process output outcome versions created by ourselves")
562 .0;
563
564 let amount_key = peer_tbs_pks
565 .get(&peer)
566 .ok_or(anyhow!("Unknown peer"))?
567 .tier(&amount)
568 .map_err(|_| anyhow!("Invalid Amount Tier"))?;
569
570 if !tbs::verify_signature_share(blinded_message, blinded_signature_share, *amount_key) {
571 bail!("Invalid blind signature")
572 }
573
574 Ok(blinded_signature_share)
575}
576
577#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
579pub struct MintOutputStatesAborted;
580
581#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
583pub struct MintOutputStatesFailed {
584 pub error: String,
585}
586
587#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
589pub struct MintOutputStatesSucceeded {
590 pub amount: Amount,
591}
592
593#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Encodable, Decodable)]
597pub struct NoteIssuanceRequest {
598 spend_key: Keypair,
601 blinding_key: BlindingKey,
603}
604
605impl hash::Hash for NoteIssuanceRequest {
606 fn hash<H: hash::Hasher>(&self, state: &mut H) {
607 self.spend_key.hash(state);
608 }
611}
612impl NoteIssuanceRequest {
613 pub fn new<C>(ctx: &Secp256k1<C>, secret: &DerivableSecret) -> (NoteIssuanceRequest, BlindNonce)
616 where
617 C: Signing,
618 {
619 let spend_key = secret.child_key(SPEND_KEY_CHILD_ID).to_secp_key(ctx);
620 let nonce = Nonce(spend_key.public_key());
621 let blinding_key = BlindingKey(secret.child_key(BLINDING_KEY_CHILD_ID).to_bls12_381_key());
622 let blinded_nonce = blind_message(nonce.to_message(), blinding_key);
623
624 let cr = NoteIssuanceRequest {
625 spend_key,
626 blinding_key,
627 };
628
629 (cr, BlindNonce(blinded_nonce))
630 }
631
632 pub fn nonce(&self) -> Nonce {
634 Nonce(self.spend_key.public_key())
635 }
636
637 pub fn blinded_message(&self) -> BlindedMessage {
638 blind_message(self.nonce().to_message(), self.blinding_key)
639 }
640
641 pub fn finalize(&self, blinded_signature: BlindedSignature) -> SpendableNote {
643 SpendableNote {
644 signature: unblind_signature(self.blinding_key, blinded_signature),
645 spend_key: self.spend_key,
646 }
647 }
648
649 pub fn blinding_key(&self) -> &BlindingKey {
650 &self.blinding_key
651 }
652
653 pub fn spend_key(&self) -> &Keypair {
654 &self.spend_key
655 }
656}