1use std::collections::BTreeMap;
2use std::hash;
3use std::time::Duration;
4
5use assert_matches::assert_matches;
6use fedimint_api_client::api::{
7 FederationApiExt, SerdeOutputOutcome, ServerError,
8 VERSION_THAT_INTRODUCED_AWAIT_OUTPUTS_OUTCOMES, deserialize_outcome,
9};
10use fedimint_api_client::query::FilterMapThreshold;
11use fedimint_client_module::DynGlobalClientContext;
12use fedimint_client_module::module::{ClientContext, OutPointRange};
13use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
14use fedimint_core::core::{Decoder, OperationId};
15use fedimint_core::db::IDatabaseTransactionOpsCoreTyped;
16use fedimint_core::encoding::{Decodable, Encodable};
17use fedimint_core::endpoint_constants::AWAIT_OUTPUTS_OUTCOMES_ENDPOINT;
18use fedimint_core::module::ApiRequestErased;
19use fedimint_core::secp256k1::{Keypair, Secp256k1, Signing};
20use fedimint_core::util::FmtCompact as _;
21use fedimint_core::{Amount, NumPeersExt, OutPoint, PeerId, Tiered, TransactionId, crit};
22use fedimint_derive_secret::{ChildId, DerivableSecret};
23use fedimint_logging::LOG_CLIENT_MODULE_MINT;
24use fedimint_mint_common::endpoint_constants::AWAIT_OUTPUT_OUTCOME_ENDPOINT;
25use fedimint_mint_common::{BlindNonce, MintOutputOutcome, Nonce};
26use futures::future::join_all;
27use rayon::iter::{IndexedParallelIterator, IntoParallelIterator as _, ParallelIterator as _};
28use serde::{Deserialize, Serialize};
29use tbs::{
30 AggregatePublicKey, BlindedMessage, BlindedSignature, BlindedSignatureShare, BlindingKey,
31 PublicKeyShare, aggregate_signature_shares, blind_message, unblind_signature,
32};
33use tracing::{debug, warn};
34
35use crate::client_db::NoteKey;
36use crate::error::VerifyBlindShareError;
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(|err| ServerError::InvalidResponse(err.fmt_compact().to_string()))
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(format!(
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(),
623 out_point = %OutPoint { txid: common.txid(), out_idx},
624 "Invalid signature share from peer"
625 );
626 return Err(ServerError::InvalidResponse(
627 err.fmt_compact().to_string(),
628 ));
629 }
630 }
631 } else {
632 None
633 };
634
635 verified_shares.push(share);
636 }
637
638 Ok(verified_shares)
639 },
640 api.all_peers().to_num_peers(),
641 ),
642 AWAIT_OUTPUTS_OUTCOMES_ENDPOINT.to_owned(),
643 ApiRequestErased::new(common.out_point_range),
644 )
645 .await;
646
647 let threshold = api.all_peers().to_num_peers().threshold();
649 let mut ret = vec![];
650
651 for (out_idx, (_amount, _issuance_request)) in issuance_requests {
652 let relative_idx = (out_idx - common.out_point_range.start_idx()) as usize;
653 let mut blinded_sig_shares = BTreeMap::new();
654
655 for (peer_id, shares) in &verified_shares_per_output {
657 if let Some(Some(share)) = shares.get(relative_idx) {
658 blinded_sig_shares.insert(*peer_id, *share);
659 }
660 }
661
662 assert!(threshold <= blinded_sig_shares.len());
663 ret.push((out_idx, blinded_sig_shares));
664 }
665
666 ret
667 }
668
669 async fn await_outcome_ready_legacy(
671 api: &fedimint_api_client::api::DynGlobalApi,
672 common: MintOutputCommon,
673 module_decoder: Decoder,
674 issuance_requests: BTreeMap<u64, (Amount, NoteIssuanceRequest)>,
675 tbs_pks: BTreeMap<PeerId, Tiered<PublicKeyShare>>,
676 ) -> Vec<(u64, BTreeMap<PeerId, BlindedSignatureShare>)> {
677 let mut ret = vec![];
678 let mut issuance_requests_iter = issuance_requests.into_iter();
679
680 if let Some((out_idx, (amount, issuance_request))) = issuance_requests_iter.next() {
683 let module_decoder = module_decoder.clone();
684 let tbs_pks = tbs_pks.clone();
685
686 let blinded_sig_share = api
687 .request_with_strategy_retry(
688 FilterMapThreshold::new(
689 move |peer, outcome| {
690 verify_blind_share(
691 peer,
692 &outcome,
693 amount,
694 issuance_request.blinded_message(),
695 &module_decoder,
696 &tbs_pks,
697 )
698 .map_err(|err| {
699 ServerError::InvalidResponse(err.fmt_compact().to_string())
700 })
701 },
702 api.all_peers().to_num_peers(),
703 ),
704 AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
705 ApiRequestErased::new(OutPoint {
706 txid: common.txid(),
707 out_idx,
708 }),
709 )
710 .await;
711
712 ret.push((out_idx, blinded_sig_share));
713 } else {
714 return vec![];
715 }
716
717 ret.extend(
719 join_all(
720 issuance_requests_iter.map(|(out_idx, (amount, issuance_request))| {
721 let module_decoder = module_decoder.clone();
722 let tbs_pks = tbs_pks.clone();
723 async move {
724 let blinded_sig_share = api
725 .request_with_strategy_retry(
726 FilterMapThreshold::new(
727 move |peer, outcome| {
728 verify_blind_share(
729 peer,
730 &outcome,
731 amount,
732 issuance_request.blinded_message(),
733 &module_decoder,
734 &tbs_pks,
735 )
736 .map_err(|err| {
737 ServerError::InvalidResponse(
738 err.fmt_compact().to_string(),
739 )
740 })
741 },
742 api.all_peers().to_num_peers(),
743 ),
744 AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
745 ApiRequestErased::new(OutPoint {
746 txid: common.txid(),
747 out_idx,
748 }),
749 )
750 .await;
751
752 (out_idx, blinded_sig_share)
753 }
754 }),
755 )
756 .await,
757 );
758
759 ret
760 }
761
762 async fn transition_outcome_ready(
763 client_ctx: ClientContext<MintClientModule>,
764 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
765 blinded_signature_shares: Vec<(u64, BTreeMap<PeerId, BlindedSignatureShare>)>,
766 old_state: MintOutputStateMachine,
767 tbs_pks: Tiered<AggregatePublicKey>,
768 balance_update_sender: tokio::sync::watch::Sender<()>,
769 ) -> MintOutputStateMachine {
770 let mut amount_total = Amount::ZERO;
774 let MintOutputStates::CreatedMulti(created) = old_state.state else {
775 panic!("Unexpected prior state")
776 };
777
778 let mut spendable_notes: Vec<(Amount, SpendableNote)> = vec![];
779
780 blinded_signature_shares
782 .into_par_iter()
783 .map(|(out_idx, blinded_signature_shares)| {
784 let agg_blind_signature = aggregate_signature_shares(
785 &blinded_signature_shares
786 .into_iter()
787 .map(|(peer, share)| (peer.to_usize() as u64, share))
788 .collect(),
789 );
790
791 let (amount, issuance_request) =
793 created.issuance_requests.get(&out_idx).expect("Must have");
794
795 let amount_key = tbs_pks.tier(amount).expect("Must have keys for any amount");
796
797 let spendable_note = issuance_request.finalize(agg_blind_signature);
798
799 assert!(spendable_note.note().verify(*amount_key), "We checked all signature shares in the trigger future, so the combined signature has to be valid");
800
801 (*amount, spendable_note)
802 })
803 .collect_into_vec(&mut spendable_notes);
804
805 for (amount, spendable_note) in spendable_notes {
806 debug!(target: LOG_CLIENT_MODULE_MINT, amount = %amount, note=%spendable_note, "Adding new note from transaction output");
807
808 client_ctx
809 .log_event(
810 &mut dbtx.module_tx(),
811 NoteCreated {
812 nonce: spendable_note.nonce(),
813 },
814 )
815 .await;
816
817 amount_total += amount;
818 if let Some(note) = dbtx
819 .module_tx()
820 .insert_entry(
821 &NoteKey {
822 amount,
823 nonce: spendable_note.nonce(),
824 },
825 &spendable_note.to_undecoded(),
826 )
827 .await
828 {
829 crit!(target: LOG_CLIENT_MODULE_MINT, %note, "E-cash note was replaced in DB");
830 }
831 }
832
833 client_ctx
834 .log_event(
835 &mut dbtx.module_tx(),
836 ReceivePaymentUpdateEvent {
837 operation_id: old_state.common.operation_id,
838 status: ReceivePaymentStatus::Success,
839 },
840 )
841 .await;
842
843 dbtx.module_tx()
844 .on_commit(move || balance_update_sender.send_replace(()));
845
846 MintOutputStateMachine {
847 common: old_state.common,
848 state: MintOutputStates::Succeeded(MintOutputStatesSucceeded {
849 amount: amount_total,
850 }),
851 }
852 }
853}
854
855pub fn verify_blind_share(
858 peer: PeerId,
859 outcome: &SerdeOutputOutcome,
860 amount: Amount,
861 blinded_message: BlindedMessage,
862 decoder: &Decoder,
863 peer_tbs_pks: &BTreeMap<PeerId, Tiered<PublicKeyShare>>,
864) -> Result<BlindedSignatureShare, VerifyBlindShareError> {
865 let outcome = deserialize_outcome::<MintOutputOutcome>(outcome, decoder)?;
866
867 let blinded_signature_share = outcome
868 .ensure_v0_ref()
869 .expect("We only process output outcome versions created by ourselves")
870 .0;
871
872 let amount_key = peer_tbs_pks
873 .get(&peer)
874 .ok_or(VerifyBlindShareError::UnknownPeer { peer })?
875 .tier(&amount)
876 .map_err(|_| VerifyBlindShareError::InvalidAmountTier { amount })?;
877
878 if !tbs::verify_signature_share(blinded_message, blinded_signature_share, *amount_key) {
879 return Err(VerifyBlindShareError::InvalidSignature);
880 }
881
882 Ok(blinded_signature_share)
883}
884
885#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
887pub struct MintOutputStatesAborted;
888
889#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
891pub struct MintOutputStatesFailed {
892 pub error: String,
893}
894
895#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
897pub struct MintOutputStatesSucceeded {
898 pub amount: Amount,
899}
900
901#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Encodable, Decodable)]
905pub struct NoteIssuanceRequest {
906 spend_key: Keypair,
909 blinding_key: BlindingKey,
911}
912
913impl hash::Hash for NoteIssuanceRequest {
914 fn hash<H: hash::Hasher>(&self, state: &mut H) {
915 self.spend_key.hash(state);
916 }
919}
920impl NoteIssuanceRequest {
921 pub fn new<C>(ctx: &Secp256k1<C>, secret: &DerivableSecret) -> (NoteIssuanceRequest, BlindNonce)
924 where
925 C: Signing,
926 {
927 let spend_key = secret.child_key(SPEND_KEY_CHILD_ID).to_secp_key(ctx);
928 let nonce = Nonce(spend_key.public_key());
929 let blinding_key = BlindingKey(secret.child_key(BLINDING_KEY_CHILD_ID).to_bls12_381_key());
930 let blinded_nonce = blind_message(nonce.to_message(), blinding_key);
931
932 let cr = NoteIssuanceRequest {
933 spend_key,
934 blinding_key,
935 };
936
937 (cr, BlindNonce(blinded_nonce))
938 }
939
940 pub fn nonce(&self) -> Nonce {
942 Nonce(self.spend_key.public_key())
943 }
944
945 pub fn blinded_message(&self) -> BlindedMessage {
946 blind_message(self.nonce().to_message(), self.blinding_key)
947 }
948
949 pub fn finalize(&self, blinded_signature: BlindedSignature) -> SpendableNote {
951 SpendableNote {
952 signature: unblind_signature(self.blinding_key, blinded_signature),
953 spend_key: self.spend_key,
954 }
955 }
956
957 pub fn blinding_key(&self) -> &BlindingKey {
958 &self.blinding_key
959 }
960
961 pub fn spend_key(&self) -> &Keypair {
962 &self.spend_key
963 }
964}