1use std::collections::BTreeMap;
2use std::hash;
3use std::time::Duration;
4
5use anyhow::{anyhow, bail};
6use assert_matches::assert_matches;
7use fedimint_api_client::api::{
8 FederationApiExt, SerdeOutputOutcome, ServerError,
9 VERSION_THAT_INTRODUCED_AWAIT_OUTPUTS_OUTCOMES, deserialize_outcome,
10};
11use fedimint_api_client::query::FilterMapThreshold;
12use fedimint_client_module::DynGlobalClientContext;
13use fedimint_client_module::module::{ClientContext, OutPointRange};
14use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
15use fedimint_core::core::{Decoder, OperationId};
16use fedimint_core::db::IDatabaseTransactionOpsCoreTyped;
17use fedimint_core::encoding::{Decodable, Encodable};
18use fedimint_core::endpoint_constants::AWAIT_OUTPUTS_OUTCOMES_ENDPOINT;
19use fedimint_core::module::ApiRequestErased;
20use fedimint_core::secp256k1::{Keypair, Secp256k1, Signing};
21use fedimint_core::util::FmtCompactAnyhow as _;
22use fedimint_core::{Amount, NumPeersExt, OutPoint, PeerId, Tiered, TransactionId, crit};
23use fedimint_derive_secret::{ChildId, DerivableSecret};
24use fedimint_logging::LOG_CLIENT_MODULE_MINT;
25use fedimint_mint_common::endpoint_constants::AWAIT_OUTPUT_OUTCOME_ENDPOINT;
26use fedimint_mint_common::{BlindNonce, MintOutputOutcome, Nonce};
27use futures::future::join_all;
28use rayon::iter::{IndexedParallelIterator, IntoParallelIterator as _, ParallelIterator as _};
29use serde::{Deserialize, Serialize};
30use tbs::{
31 AggregatePublicKey, BlindedMessage, BlindedSignature, BlindedSignatureShare, BlindingKey,
32 PublicKeyShare, aggregate_signature_shares, blind_message, unblind_signature,
33};
34use tracing::{debug, warn};
35
36use crate::client_db::NoteKey;
37use crate::events::{NoteCreated, ReceivePaymentStatus, ReceivePaymentUpdateEvent};
38use crate::{MintClientContext, MintClientModule, SpendableNote};
39
40const SPEND_KEY_CHILD_ID: ChildId = ChildId(0);
42
43const BLINDING_KEY_CHILD_ID: ChildId = ChildId(1);
45
46#[cfg_attr(doc, aquamarine::aquamarine)]
47#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
61pub enum MintOutputStates {
62 Created(MintOutputStatesCreated),
64 Aborted(MintOutputStatesAborted),
67 Failed(MintOutputStatesFailed),
72 Succeeded(MintOutputStatesSucceeded),
75 CreatedMulti(MintOutputStatesCreatedMulti),
77}
78
79#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
80pub struct MintOutputCommonV0 {
81 pub(crate) operation_id: OperationId,
82 pub(crate) out_point: OutPoint,
83}
84
85#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
86pub struct MintOutputCommon {
87 pub(crate) operation_id: OperationId,
88 pub(crate) out_point_range: OutPointRange,
89}
90
91impl MintOutputCommon {
92 pub fn txid(self) -> TransactionId {
93 self.out_point_range.txid()
94 }
95}
96
97#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
98pub struct MintOutputStateMachineV0 {
99 pub(crate) common: MintOutputCommonV0,
100 pub(crate) state: MintOutputStates,
101}
102
103#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
104pub struct MintOutputStateMachine {
105 pub(crate) common: MintOutputCommon,
106 pub(crate) state: MintOutputStates,
107}
108
109impl MintOutputStateMachine {
110 pub fn txid(&self) -> TransactionId {
112 self.common.out_point_range.txid()
113 }
114
115 pub fn created_nonces(&self) -> Vec<(u64, Amount, Nonce, BlindNonce)> {
122 match &self.state {
123 MintOutputStates::Created(c) => {
124 vec![(
125 self.common.out_point_range.start_idx(),
126 c.amount,
127 c.issuance_request.nonce(),
128 BlindNonce(c.issuance_request.blinded_message()),
129 )]
130 }
131 MintOutputStates::CreatedMulti(c) => c
132 .issuance_requests
133 .iter()
134 .map(|(idx, (amount, req))| {
135 (
136 *idx,
137 *amount,
138 req.nonce(),
139 BlindNonce(req.blinded_message()),
140 )
141 })
142 .collect(),
143 _ => vec![],
144 }
145 }
146}
147
148impl State for MintOutputStateMachine {
149 type ModuleContext = MintClientContext;
150
151 fn transitions(
152 &self,
153 context: &Self::ModuleContext,
154 global_context: &DynGlobalClientContext,
155 ) -> Vec<StateTransition<Self>> {
156 match &self.state {
157 MintOutputStates::Created(created) => {
158 created.transitions(context, global_context, self.common)
159 }
160 MintOutputStates::CreatedMulti(created) => {
161 created.transitions(context, global_context, self.common)
162 }
163 MintOutputStates::Aborted(_)
164 | MintOutputStates::Failed(_)
165 | MintOutputStates::Succeeded(_) => {
166 vec![]
167 }
168 }
169 }
170
171 fn operation_id(&self) -> OperationId {
172 self.common.operation_id
173 }
174
175 fn fmt_visualization(&self, f: &mut dyn std::fmt::Write, indent: &str) -> std::fmt::Result {
176 let txid = self.common.out_point_range.txid();
177 let start = self.common.out_point_range.start_idx();
178 let count = self.common.out_point_range.count();
179 match &self.state {
180 MintOutputStates::Created(c) => {
181 let nonce = c.issuance_request.nonce();
182 let blind_nonce = BlindNonce(c.issuance_request.blinded_message());
183 write!(
184 f,
185 "{indent}MintOutputStateMachine\n\
186 {indent} state: Created tx={}:[{start},{end})\n\
187 {indent} note: amount={} nonce={} blind_nonce={}",
188 txid.fmt_short(),
189 c.amount,
190 nonce.fmt_short(),
191 blind_nonce.fmt_short(),
192 end = start + count as u64,
193 )
194 }
195 MintOutputStates::CreatedMulti(c) => {
196 let total: Amount = c.issuance_requests.values().map(|(a, _)| *a).sum();
197 write!(
198 f,
199 "{indent}MintOutputStateMachine\n\
200 {indent} state: CreatedMulti tx={}:[{start},{end}) {} notes, total={total}",
201 txid.fmt_short(),
202 c.issuance_requests.len(),
203 end = start + count as u64,
204 )?;
205 for (idx, (amount, req)) in &c.issuance_requests {
206 let nonce = req.nonce();
207 let blind_nonce = BlindNonce(req.blinded_message());
208 write!(
209 f,
210 "\n{indent} [{idx}] amount={amount} nonce={} blind_nonce={}",
211 nonce.fmt_short(),
212 blind_nonce.fmt_short(),
213 )?;
214 }
215 Ok(())
216 }
217 MintOutputStates::Succeeded(s) => {
218 write!(
219 f,
220 "{indent}MintOutputStateMachine\n{indent} state: Succeeded amount={}",
221 s.amount,
222 )
223 }
224 MintOutputStates::Aborted(_) => {
225 write!(
226 f,
227 "{indent}MintOutputStateMachine\n{indent} state: Aborted tx={}",
228 txid.fmt_short(),
229 )
230 }
231 MintOutputStates::Failed(fail) => {
232 write!(
233 f,
234 "{indent}MintOutputStateMachine\n{indent} state: Failed error={}",
235 fail.error,
236 )
237 }
238 }
239 }
240}
241
242#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
244pub struct MintOutputStatesCreated {
245 pub(crate) amount: Amount,
246 pub(crate) issuance_request: NoteIssuanceRequest,
247}
248
249impl MintOutputStatesCreated {
250 fn transitions(
251 &self,
252 context: &MintClientContext,
254 global_context: &DynGlobalClientContext,
255 common: MintOutputCommon,
256 ) -> Vec<StateTransition<MintOutputStateMachine>> {
257 let tbs_pks = context.tbs_pks.clone();
258 let client_ctx = context.client_ctx.clone();
259 let balance_update_sender = context.balance_update_sender.clone();
260
261 vec![
262 StateTransition::new(
264 Self::await_tx_rejected(global_context.clone(), common),
265 |_dbtx, (), state| Box::pin(async move { Self::transition_tx_rejected(&state) }),
266 ),
267 StateTransition::new(
269 Self::await_outcome_ready(
270 global_context.clone(),
271 common,
272 context.mint_decoder.clone(),
273 self.amount,
274 self.issuance_request.blinded_message(),
275 context.peer_tbs_pks.clone(),
276 ),
277 move |dbtx, blinded_signature_shares, old_state| {
278 Box::pin(Self::transition_outcome_ready(
279 client_ctx.clone(),
280 dbtx,
281 blinded_signature_shares,
282 old_state,
283 tbs_pks.clone(),
284 balance_update_sender.clone(),
285 ))
286 },
287 ),
288 ]
289 }
290
291 async fn await_tx_rejected(global_context: DynGlobalClientContext, common: MintOutputCommon) {
292 let txid = common.txid();
293 debug!(target: LOG_CLIENT_MODULE_MINT, %txid, "Awaiting tx rejection");
294 let accept_fut = global_context.await_tx_accepted(common.txid());
295 tokio::pin!(accept_fut);
296 let result = tokio::select! {
297 result = &mut accept_fut => result,
298 () = fedimint_core::runtime::sleep(Duration::from_secs(300)) => {
299 warn!(
300 target: LOG_CLIENT_MODULE_MINT,
301 %txid,
302 "Transaction not accepted or rejected after 5 minutes, possibly stuck or never submitted",
303 );
304 accept_fut.await
305 }
306 };
307 if result.is_err() {
308 return;
309 }
310 std::future::pending::<()>().await;
311 }
312
313 fn transition_tx_rejected(old_state: &MintOutputStateMachine) -> MintOutputStateMachine {
314 assert_matches!(old_state.state, MintOutputStates::Created(_));
315
316 MintOutputStateMachine {
317 common: old_state.common,
318 state: MintOutputStates::Aborted(MintOutputStatesAborted),
319 }
320 }
321
322 async fn await_outcome_ready(
323 global_context: DynGlobalClientContext,
324 common: MintOutputCommon,
325 module_decoder: Decoder,
326 amount: Amount,
327 message: BlindedMessage,
328 tbs_pks: BTreeMap<PeerId, Tiered<PublicKeyShare>>,
329 ) -> BTreeMap<PeerId, BlindedSignatureShare> {
330 let txid = common.txid();
331 let out_idx = common.out_point_range.start_idx();
332 debug!(target: LOG_CLIENT_MODULE_MINT, %txid, %out_idx, "Awaiting output outcome");
333 global_context
334 .api()
335 .request_with_strategy_retry(
336 FilterMapThreshold::new(
338 move |peer, outcome| {
339 verify_blind_share(
340 peer,
341 &outcome,
342 amount,
343 message,
344 &module_decoder,
345 &tbs_pks,
346 )
347 .map_err(ServerError::InvalidResponse)
348 },
349 global_context.api().all_peers().to_num_peers(),
350 ),
351 AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
352 ApiRequestErased::new(OutPoint {
353 txid: common.txid(),
354 out_idx: common.out_point_range.start_idx(),
355 }),
356 )
357 .await
358 }
359
360 async fn transition_outcome_ready(
361 client_ctx: ClientContext<MintClientModule>,
362 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
363 blinded_signature_shares: BTreeMap<PeerId, BlindedSignatureShare>,
364 old_state: MintOutputStateMachine,
365 tbs_pks: Tiered<AggregatePublicKey>,
366 balance_update_sender: tokio::sync::watch::Sender<()>,
367 ) -> MintOutputStateMachine {
368 let MintOutputStates::Created(created) = old_state.state else {
372 panic!("Unexpected prior state")
373 };
374
375 let agg_blind_signature = aggregate_signature_shares(
376 &blinded_signature_shares
377 .into_iter()
378 .map(|(peer, share)| (peer.to_usize() as u64, share))
379 .collect(),
380 );
381
382 let amount_key = tbs_pks
383 .tier(&created.amount)
384 .expect("We obtained this amount from tbs_pks when we created the output");
385
386 if !tbs::verify_blinded_signature(
388 created.issuance_request.blinded_message(),
389 agg_blind_signature,
390 *amount_key,
391 ) {
392 return MintOutputStateMachine {
393 common: old_state.common,
394 state: MintOutputStates::Failed(MintOutputStatesFailed {
395 error: "Invalid blind signature".to_string(),
396 }),
397 };
398 }
399
400 let spendable_note = created.issuance_request.finalize(agg_blind_signature);
401
402 assert!(spendable_note.note().verify(*amount_key));
403
404 debug!(target: LOG_CLIENT_MODULE_MINT, amount = %created.amount, note=%spendable_note, "Adding new note from transaction output");
405
406 client_ctx
407 .log_event(
408 &mut dbtx.module_tx(),
409 NoteCreated {
410 nonce: spendable_note.nonce(),
411 },
412 )
413 .await;
414 if let Some(note) = dbtx
415 .module_tx()
416 .insert_entry(
417 &NoteKey {
418 amount: created.amount,
419 nonce: spendable_note.nonce(),
420 },
421 &spendable_note.to_undecoded(),
422 )
423 .await
424 {
425 crit!(target: LOG_CLIENT_MODULE_MINT, %note, "E-cash note was replaced in DB");
426 }
427
428 dbtx.module_tx()
429 .on_commit(move || balance_update_sender.send_replace(()));
430
431 MintOutputStateMachine {
432 common: old_state.common,
433 state: MintOutputStates::Succeeded(MintOutputStatesSucceeded {
434 amount: created.amount,
435 }),
436 }
437 }
438}
439
440#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
442pub struct MintOutputStatesCreatedMulti {
443 pub(crate) issuance_requests: BTreeMap<u64, (Amount, NoteIssuanceRequest)>,
444}
445
446impl MintOutputStatesCreatedMulti {
447 fn transitions(
448 &self,
449 context: &MintClientContext,
451 global_context: &DynGlobalClientContext,
452 common: MintOutputCommon,
453 ) -> Vec<StateTransition<MintOutputStateMachine>> {
454 let tbs_pks = context.tbs_pks.clone();
455 let client_ctx = context.client_ctx.clone();
456 let client_ctx_rejected = context.client_ctx.clone();
457 let balance_update_sender = context.balance_update_sender.clone();
458
459 vec![
460 StateTransition::new(
462 Self::await_tx_rejected(global_context.clone(), common),
463 move |dbtx, (), state| {
464 Box::pin(Self::transition_tx_rejected(
465 client_ctx_rejected.clone(),
466 dbtx,
467 state,
468 ))
469 },
470 ),
471 StateTransition::new(
473 Self::await_outcome_ready(
474 global_context.clone(),
475 common,
476 context.mint_decoder.clone(),
477 self.issuance_requests.clone(),
478 context.peer_tbs_pks.clone(),
479 ),
480 move |dbtx, blinded_signature_shares, old_state| {
481 Box::pin(Self::transition_outcome_ready(
482 client_ctx.clone(),
483 dbtx,
484 blinded_signature_shares,
485 old_state,
486 tbs_pks.clone(),
487 balance_update_sender.clone(),
488 ))
489 },
490 ),
491 ]
492 }
493
494 async fn await_tx_rejected(global_context: DynGlobalClientContext, common: MintOutputCommon) {
495 let txid = common.txid();
496 debug!(target: LOG_CLIENT_MODULE_MINT, %txid, "Awaiting tx rejection");
497 let accept_fut = global_context.await_tx_accepted(common.txid());
498 tokio::pin!(accept_fut);
499 let result = tokio::select! {
500 result = &mut accept_fut => result,
501 () = fedimint_core::runtime::sleep(Duration::from_secs(300)) => {
502 warn!(
503 target: LOG_CLIENT_MODULE_MINT,
504 %txid,
505 "Transaction not accepted or rejected after 5 minutes, possibly stuck or never submitted",
506 );
507 accept_fut.await
508 }
509 };
510 if result.is_err() {
511 return;
512 }
513 std::future::pending::<()>().await;
514 }
515
516 async fn transition_tx_rejected(
517 client_ctx: ClientContext<MintClientModule>,
518 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
519 old_state: MintOutputStateMachine,
520 ) -> MintOutputStateMachine {
521 assert_matches!(old_state.state, MintOutputStates::CreatedMulti(_));
522
523 client_ctx
524 .log_event(
525 &mut dbtx.module_tx(),
526 ReceivePaymentUpdateEvent {
527 operation_id: old_state.common.operation_id,
528 status: ReceivePaymentStatus::Rejected,
529 },
530 )
531 .await;
532
533 MintOutputStateMachine {
534 common: old_state.common,
535 state: MintOutputStates::Aborted(MintOutputStatesAborted),
536 }
537 }
538
539 async fn await_outcome_ready(
540 global_context: DynGlobalClientContext,
541 common: MintOutputCommon,
542 module_decoder: Decoder,
543 issuance_requests: BTreeMap<u64, (Amount, NoteIssuanceRequest)>,
544 tbs_pks: BTreeMap<PeerId, Tiered<PublicKeyShare>>,
545 ) -> Vec<(u64, BTreeMap<PeerId, BlindedSignatureShare>)> {
546 let txid = common.txid();
547 let out_idx = common.out_point_range.start_idx();
548 debug!(target: LOG_CLIENT_MODULE_MINT, %txid, %out_idx, "Awaiting output outcome");
549 let api = global_context.api();
550 let core_api_version = global_context.core_api_version().await;
551
552 if VERSION_THAT_INTRODUCED_AWAIT_OUTPUTS_OUTCOMES <= core_api_version {
554 Self::await_outcome_ready_batch(api, common, module_decoder, issuance_requests, tbs_pks)
555 .await
556 } else {
557 Self::await_outcome_ready_legacy(
559 api,
560 common,
561 module_decoder,
562 issuance_requests,
563 tbs_pks,
564 )
565 .await
566 }
567 }
568
569 async fn await_outcome_ready_batch(
571 api: &fedimint_api_client::api::DynGlobalApi,
572 common: MintOutputCommon,
573 module_decoder: Decoder,
574 issuance_requests: BTreeMap<u64, (Amount, NoteIssuanceRequest)>,
575 tbs_pks: BTreeMap<PeerId, Tiered<PublicKeyShare>>,
576 ) -> Vec<(u64, BTreeMap<PeerId, BlindedSignatureShare>)> {
577 if issuance_requests.is_empty() {
578 return vec![];
579 }
580
581 let issuance_requests_clone = issuance_requests.clone();
583 let verified_shares_per_output: BTreeMap<PeerId, Vec<Option<BlindedSignatureShare>>> = api
584 .request_with_strategy_retry(
585 FilterMapThreshold::new(
586 move |peer, outcomes: Vec<Option<SerdeOutputOutcome>>| {
587 if outcomes.len() != common.out_point_range.count() {
589 return Err(ServerError::InvalidResponse(anyhow::anyhow!(
590 "Peer {peer} returned {} outcomes but expected {}",
591 outcomes.len(),
592 common.out_point_range.count()
593 )));
594 }
595
596 let mut verified_shares = Vec::with_capacity(outcomes.len());
599 for (relative_idx, outcome_opt) in outcomes.into_iter().enumerate() {
600 let out_idx = common.out_point_range.start_idx() + relative_idx as u64;
601
602 let (amount, issuance_request) = issuance_requests_clone
604 .get(&out_idx)
605 .expect("issuance_request must exist for every output in range");
606
607 let share = if let Some(outcome) = outcome_opt {
608 match verify_blind_share(
609 peer,
610 &outcome,
611 *amount,
612 issuance_request.blinded_message(),
613 &module_decoder,
614 &tbs_pks,
615 ) {
616 Ok(share) => Some(share),
617 Err(err) => {
618 tracing::warn!(
620 target: LOG_CLIENT_MODULE_MINT,
621 %peer,
622 err = %err.fmt_compact_anyhow(),
623 out_point = %OutPoint { txid: common.txid(), out_idx},
624 "Invalid signature share from peer"
625 );
626 return Err(ServerError::InvalidResponse(err));
627 }
628 }
629 } else {
630 None
631 };
632
633 verified_shares.push(share);
634 }
635
636 Ok(verified_shares)
637 },
638 api.all_peers().to_num_peers(),
639 ),
640 AWAIT_OUTPUTS_OUTCOMES_ENDPOINT.to_owned(),
641 ApiRequestErased::new(common.out_point_range),
642 )
643 .await;
644
645 let threshold = api.all_peers().to_num_peers().threshold();
647 let mut ret = vec![];
648
649 for (out_idx, (_amount, _issuance_request)) in issuance_requests {
650 let relative_idx = (out_idx - common.out_point_range.start_idx()) as usize;
651 let mut blinded_sig_shares = BTreeMap::new();
652
653 for (peer_id, shares) in &verified_shares_per_output {
655 if let Some(Some(share)) = shares.get(relative_idx) {
656 blinded_sig_shares.insert(*peer_id, *share);
657 }
658 }
659
660 assert!(threshold <= blinded_sig_shares.len());
661 ret.push((out_idx, blinded_sig_shares));
662 }
663
664 ret
665 }
666
667 async fn await_outcome_ready_legacy(
669 api: &fedimint_api_client::api::DynGlobalApi,
670 common: MintOutputCommon,
671 module_decoder: Decoder,
672 issuance_requests: BTreeMap<u64, (Amount, NoteIssuanceRequest)>,
673 tbs_pks: BTreeMap<PeerId, Tiered<PublicKeyShare>>,
674 ) -> Vec<(u64, BTreeMap<PeerId, BlindedSignatureShare>)> {
675 let mut ret = vec![];
676 let mut issuance_requests_iter = issuance_requests.into_iter();
677
678 if let Some((out_idx, (amount, issuance_request))) = issuance_requests_iter.next() {
681 let module_decoder = module_decoder.clone();
682 let tbs_pks = tbs_pks.clone();
683
684 let blinded_sig_share = api
685 .request_with_strategy_retry(
686 FilterMapThreshold::new(
687 move |peer, outcome| {
688 verify_blind_share(
689 peer,
690 &outcome,
691 amount,
692 issuance_request.blinded_message(),
693 &module_decoder,
694 &tbs_pks,
695 )
696 .map_err(ServerError::InvalidResponse)
697 },
698 api.all_peers().to_num_peers(),
699 ),
700 AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
701 ApiRequestErased::new(OutPoint {
702 txid: common.txid(),
703 out_idx,
704 }),
705 )
706 .await;
707
708 ret.push((out_idx, blinded_sig_share));
709 } else {
710 return vec![];
711 }
712
713 ret.extend(
715 join_all(
716 issuance_requests_iter.map(|(out_idx, (amount, issuance_request))| {
717 let module_decoder = module_decoder.clone();
718 let tbs_pks = tbs_pks.clone();
719 async move {
720 let blinded_sig_share = api
721 .request_with_strategy_retry(
722 FilterMapThreshold::new(
723 move |peer, outcome| {
724 verify_blind_share(
725 peer,
726 &outcome,
727 amount,
728 issuance_request.blinded_message(),
729 &module_decoder,
730 &tbs_pks,
731 )
732 .map_err(ServerError::InvalidResponse)
733 },
734 api.all_peers().to_num_peers(),
735 ),
736 AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
737 ApiRequestErased::new(OutPoint {
738 txid: common.txid(),
739 out_idx,
740 }),
741 )
742 .await;
743
744 (out_idx, blinded_sig_share)
745 }
746 }),
747 )
748 .await,
749 );
750
751 ret
752 }
753
754 async fn transition_outcome_ready(
755 client_ctx: ClientContext<MintClientModule>,
756 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
757 blinded_signature_shares: Vec<(u64, BTreeMap<PeerId, BlindedSignatureShare>)>,
758 old_state: MintOutputStateMachine,
759 tbs_pks: Tiered<AggregatePublicKey>,
760 balance_update_sender: tokio::sync::watch::Sender<()>,
761 ) -> MintOutputStateMachine {
762 let mut amount_total = Amount::ZERO;
766 let MintOutputStates::CreatedMulti(created) = old_state.state else {
767 panic!("Unexpected prior state")
768 };
769
770 let mut spendable_notes: Vec<(Amount, SpendableNote)> = vec![];
771
772 blinded_signature_shares
774 .into_par_iter()
775 .map(|(out_idx, blinded_signature_shares)| {
776 let agg_blind_signature = aggregate_signature_shares(
777 &blinded_signature_shares
778 .into_iter()
779 .map(|(peer, share)| (peer.to_usize() as u64, share))
780 .collect(),
781 );
782
783 let (amount, issuance_request) =
785 created.issuance_requests.get(&out_idx).expect("Must have");
786
787 let amount_key = tbs_pks.tier(amount).expect("Must have keys for any amount");
788
789 let spendable_note = issuance_request.finalize(agg_blind_signature);
790
791 assert!(spendable_note.note().verify(*amount_key), "We checked all signature shares in the trigger future, so the combined signature has to be valid");
792
793 (*amount, spendable_note)
794 })
795 .collect_into_vec(&mut spendable_notes);
796
797 for (amount, spendable_note) in spendable_notes {
798 debug!(target: LOG_CLIENT_MODULE_MINT, amount = %amount, note=%spendable_note, "Adding new note from transaction output");
799
800 client_ctx
801 .log_event(
802 &mut dbtx.module_tx(),
803 NoteCreated {
804 nonce: spendable_note.nonce(),
805 },
806 )
807 .await;
808
809 amount_total += amount;
810 if let Some(note) = dbtx
811 .module_tx()
812 .insert_entry(
813 &NoteKey {
814 amount,
815 nonce: spendable_note.nonce(),
816 },
817 &spendable_note.to_undecoded(),
818 )
819 .await
820 {
821 crit!(target: LOG_CLIENT_MODULE_MINT, %note, "E-cash note was replaced in DB");
822 }
823 }
824
825 client_ctx
826 .log_event(
827 &mut dbtx.module_tx(),
828 ReceivePaymentUpdateEvent {
829 operation_id: old_state.common.operation_id,
830 status: ReceivePaymentStatus::Success,
831 },
832 )
833 .await;
834
835 dbtx.module_tx()
836 .on_commit(move || balance_update_sender.send_replace(()));
837
838 MintOutputStateMachine {
839 common: old_state.common,
840 state: MintOutputStates::Succeeded(MintOutputStatesSucceeded {
841 amount: amount_total,
842 }),
843 }
844 }
845}
846
847pub fn verify_blind_share(
850 peer: PeerId,
851 outcome: &SerdeOutputOutcome,
852 amount: Amount,
853 blinded_message: BlindedMessage,
854 decoder: &Decoder,
855 peer_tbs_pks: &BTreeMap<PeerId, Tiered<PublicKeyShare>>,
856) -> anyhow::Result<BlindedSignatureShare> {
857 let outcome = deserialize_outcome::<MintOutputOutcome>(outcome, decoder)?;
858
859 let blinded_signature_share = outcome
860 .ensure_v0_ref()
861 .expect("We only process output outcome versions created by ourselves")
862 .0;
863
864 let amount_key = peer_tbs_pks
865 .get(&peer)
866 .ok_or(anyhow!("Unknown peer"))?
867 .tier(&amount)
868 .map_err(|_| anyhow!("Invalid Amount Tier"))?;
869
870 if !tbs::verify_signature_share(blinded_message, blinded_signature_share, *amount_key) {
871 bail!("Invalid blind signature")
872 }
873
874 Ok(blinded_signature_share)
875}
876
877#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
879pub struct MintOutputStatesAborted;
880
881#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
883pub struct MintOutputStatesFailed {
884 pub error: String,
885}
886
887#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
889pub struct MintOutputStatesSucceeded {
890 pub amount: Amount,
891}
892
893#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Encodable, Decodable)]
897pub struct NoteIssuanceRequest {
898 spend_key: Keypair,
901 blinding_key: BlindingKey,
903}
904
905impl hash::Hash for NoteIssuanceRequest {
906 fn hash<H: hash::Hasher>(&self, state: &mut H) {
907 self.spend_key.hash(state);
908 }
911}
912impl NoteIssuanceRequest {
913 pub fn new<C>(ctx: &Secp256k1<C>, secret: &DerivableSecret) -> (NoteIssuanceRequest, BlindNonce)
916 where
917 C: Signing,
918 {
919 let spend_key = secret.child_key(SPEND_KEY_CHILD_ID).to_secp_key(ctx);
920 let nonce = Nonce(spend_key.public_key());
921 let blinding_key = BlindingKey(secret.child_key(BLINDING_KEY_CHILD_ID).to_bls12_381_key());
922 let blinded_nonce = blind_message(nonce.to_message(), blinding_key);
923
924 let cr = NoteIssuanceRequest {
925 spend_key,
926 blinding_key,
927 };
928
929 (cr, BlindNonce(blinded_nonce))
930 }
931
932 pub fn nonce(&self) -> Nonce {
934 Nonce(self.spend_key.public_key())
935 }
936
937 pub fn blinded_message(&self) -> BlindedMessage {
938 blind_message(self.nonce().to_message(), self.blinding_key)
939 }
940
941 pub fn finalize(&self, blinded_signature: BlindedSignature) -> SpendableNote {
943 SpendableNote {
944 signature: unblind_signature(self.blinding_key, blinded_signature),
945 spend_key: self.spend_key,
946 }
947 }
948
949 pub fn blinding_key(&self) -> &BlindingKey {
950 &self.blinding_key
951 }
952
953 pub fn spend_key(&self) -> &Keypair {
954 &self.spend_key
955 }
956}