1use std::collections::BTreeMap;
2use std::future::ready;
3use std::hash;
4use std::time::Duration;
5
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::FmtCompact 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::error::VerifyBlindShareError;
38use crate::events::{NoteCreated, ReceivePaymentStatus, ReceivePaymentUpdateEvent};
39use crate::{MintClientContext, MintClientModule, SpendableNote};
40
41const SPEND_KEY_CHILD_ID: ChildId = ChildId(0);
43
44const BLINDING_KEY_CHILD_ID: ChildId = ChildId(1);
46
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 ready(
340 verify_blind_share(
341 peer,
342 &outcome,
343 amount,
344 message,
345 &module_decoder,
346 &tbs_pks,
347 )
348 .map_err(|err| {
349 ServerError::InvalidResponse(err.fmt_compact().to_string())
350 }),
351 )
352 },
353 global_context.api().all_peers().to_num_peers(),
354 ),
355 AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
356 ApiRequestErased::new(OutPoint {
357 txid: common.txid(),
358 out_idx: common.out_point_range.start_idx(),
359 }),
360 )
361 .await
362 }
363
364 async fn transition_outcome_ready(
365 client_ctx: ClientContext<MintClientModule>,
366 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
367 blinded_signature_shares: BTreeMap<PeerId, BlindedSignatureShare>,
368 old_state: MintOutputStateMachine,
369 tbs_pks: Tiered<AggregatePublicKey>,
370 balance_update_sender: tokio::sync::watch::Sender<()>,
371 ) -> MintOutputStateMachine {
372 let MintOutputStates::Created(created) = old_state.state else {
376 panic!("Unexpected prior state")
377 };
378
379 let agg_blind_signature = aggregate_signature_shares(
380 &blinded_signature_shares
381 .into_iter()
382 .map(|(peer, share)| (peer.to_usize() as u64, share))
383 .collect(),
384 );
385
386 let amount_key = tbs_pks
387 .tier(&created.amount)
388 .expect("We obtained this amount from tbs_pks when we created the output");
389
390 if !tbs::verify_blinded_signature(
392 created.issuance_request.blinded_message(),
393 agg_blind_signature,
394 *amount_key,
395 ) {
396 return MintOutputStateMachine {
397 common: old_state.common,
398 state: MintOutputStates::Failed(MintOutputStatesFailed {
399 error: "Invalid blind signature".to_string(),
400 }),
401 };
402 }
403
404 let spendable_note = created.issuance_request.finalize(agg_blind_signature);
405
406 assert!(spendable_note.note().verify(*amount_key));
407
408 debug!(target: LOG_CLIENT_MODULE_MINT, amount = %created.amount, note=%spendable_note, "Adding new note from transaction output");
409
410 client_ctx
411 .log_event(
412 &mut dbtx.module_tx(),
413 NoteCreated {
414 nonce: spendable_note.nonce(),
415 },
416 )
417 .await;
418 if let Some(note) = dbtx
419 .module_tx()
420 .insert_entry(
421 &NoteKey {
422 amount: created.amount,
423 nonce: spendable_note.nonce(),
424 },
425 &spendable_note.to_undecoded(),
426 )
427 .await
428 {
429 crit!(target: LOG_CLIENT_MODULE_MINT, %note, "E-cash note was replaced in DB");
430 }
431
432 dbtx.module_tx()
433 .on_commit(move || balance_update_sender.send_replace(()));
434
435 MintOutputStateMachine {
436 common: old_state.common,
437 state: MintOutputStates::Succeeded(MintOutputStatesSucceeded {
438 amount: created.amount,
439 }),
440 }
441 }
442}
443
444#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
446pub struct MintOutputStatesCreatedMulti {
447 pub(crate) issuance_requests: BTreeMap<u64, (Amount, NoteIssuanceRequest)>,
448}
449
450impl MintOutputStatesCreatedMulti {
451 fn transitions(
452 &self,
453 context: &MintClientContext,
455 global_context: &DynGlobalClientContext,
456 common: MintOutputCommon,
457 ) -> Vec<StateTransition<MintOutputStateMachine>> {
458 let tbs_pks = context.tbs_pks.clone();
459 let client_ctx = context.client_ctx.clone();
460 let client_ctx_rejected = context.client_ctx.clone();
461 let balance_update_sender = context.balance_update_sender.clone();
462
463 vec![
464 StateTransition::new(
466 Self::await_tx_rejected(global_context.clone(), common),
467 move |dbtx, (), state| {
468 Box::pin(Self::transition_tx_rejected(
469 client_ctx_rejected.clone(),
470 dbtx,
471 state,
472 ))
473 },
474 ),
475 StateTransition::new(
477 Self::await_outcome_ready(
478 global_context.clone(),
479 common,
480 context.mint_decoder.clone(),
481 self.issuance_requests.clone(),
482 context.peer_tbs_pks.clone(),
483 ),
484 move |dbtx, blinded_signature_shares, old_state| {
485 Box::pin(Self::transition_outcome_ready(
486 client_ctx.clone(),
487 dbtx,
488 blinded_signature_shares,
489 old_state,
490 tbs_pks.clone(),
491 balance_update_sender.clone(),
492 ))
493 },
494 ),
495 ]
496 }
497
498 async fn await_tx_rejected(global_context: DynGlobalClientContext, common: MintOutputCommon) {
499 let txid = common.txid();
500 debug!(target: LOG_CLIENT_MODULE_MINT, %txid, "Awaiting tx rejection");
501 let accept_fut = global_context.await_tx_accepted(common.txid());
502 tokio::pin!(accept_fut);
503 let result = tokio::select! {
504 result = &mut accept_fut => result,
505 () = fedimint_core::runtime::sleep(Duration::from_secs(300)) => {
506 warn!(
507 target: LOG_CLIENT_MODULE_MINT,
508 %txid,
509 "Transaction not accepted or rejected after 5 minutes, possibly stuck or never submitted",
510 );
511 accept_fut.await
512 }
513 };
514 if result.is_err() {
515 return;
516 }
517 std::future::pending::<()>().await;
518 }
519
520 async fn transition_tx_rejected(
521 client_ctx: ClientContext<MintClientModule>,
522 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
523 old_state: MintOutputStateMachine,
524 ) -> MintOutputStateMachine {
525 assert_matches!(old_state.state, MintOutputStates::CreatedMulti(_));
526
527 client_ctx
528 .log_event(
529 &mut dbtx.module_tx(),
530 ReceivePaymentUpdateEvent {
531 operation_id: old_state.common.operation_id,
532 status: ReceivePaymentStatus::Rejected,
533 },
534 )
535 .await;
536
537 MintOutputStateMachine {
538 common: old_state.common,
539 state: MintOutputStates::Aborted(MintOutputStatesAborted),
540 }
541 }
542
543 async fn await_outcome_ready(
544 global_context: DynGlobalClientContext,
545 common: MintOutputCommon,
546 module_decoder: Decoder,
547 issuance_requests: BTreeMap<u64, (Amount, NoteIssuanceRequest)>,
548 tbs_pks: BTreeMap<PeerId, Tiered<PublicKeyShare>>,
549 ) -> Vec<(u64, BTreeMap<PeerId, BlindedSignatureShare>)> {
550 let txid = common.txid();
551 let out_idx = common.out_point_range.start_idx();
552 debug!(target: LOG_CLIENT_MODULE_MINT, %txid, %out_idx, "Awaiting output outcome");
553 let api = global_context.api();
554 let core_api_version = global_context.core_api_version().await;
555
556 if VERSION_THAT_INTRODUCED_AWAIT_OUTPUTS_OUTCOMES <= core_api_version {
558 Self::await_outcome_ready_batch(api, common, module_decoder, issuance_requests, tbs_pks)
559 .await
560 } else {
561 Self::await_outcome_ready_legacy(
563 api,
564 common,
565 module_decoder,
566 issuance_requests,
567 tbs_pks,
568 )
569 .await
570 }
571 }
572
573 async fn await_outcome_ready_batch(
575 api: &fedimint_api_client::api::DynGlobalApi,
576 common: MintOutputCommon,
577 module_decoder: Decoder,
578 issuance_requests: BTreeMap<u64, (Amount, NoteIssuanceRequest)>,
579 tbs_pks: BTreeMap<PeerId, Tiered<PublicKeyShare>>,
580 ) -> Vec<(u64, BTreeMap<PeerId, BlindedSignatureShare>)> {
581 if issuance_requests.is_empty() {
582 return vec![];
583 }
584
585 let issuance_requests_clone = issuance_requests.clone();
587 let verify = move |peer: PeerId, outcomes: Vec<Option<SerdeOutputOutcome>>| {
589 if outcomes.len() != common.out_point_range.count() {
591 return Err(ServerError::InvalidResponse(format!(
592 "Peer {peer} returned {} outcomes but expected {}",
593 outcomes.len(),
594 common.out_point_range.count()
595 )));
596 }
597
598 let mut verified_shares = Vec::with_capacity(outcomes.len());
601 for (relative_idx, outcome_opt) in outcomes.into_iter().enumerate() {
602 let out_idx = common.out_point_range.start_idx() + relative_idx as u64;
603
604 let (amount, issuance_request) = issuance_requests_clone
606 .get(&out_idx)
607 .expect("issuance_request must exist for every output in range");
608
609 let share = if let Some(outcome) = outcome_opt {
610 match verify_blind_share(
611 peer,
612 &outcome,
613 *amount,
614 issuance_request.blinded_message(),
615 &module_decoder,
616 &tbs_pks,
617 ) {
618 Ok(share) => Some(share),
619 Err(err) => {
620 tracing::warn!(
622 target: LOG_CLIENT_MODULE_MINT,
623 %peer,
624 err = %err.fmt_compact(),
625 out_point = %OutPoint { txid: common.txid(), out_idx},
626 "Invalid signature share from peer"
627 );
628 return Err(ServerError::InvalidResponse(
629 err.fmt_compact().to_string(),
630 ));
631 }
632 }
633 } else {
634 None
635 };
636
637 verified_shares.push(share);
638 }
639
640 Ok(verified_shares)
641 };
642
643 let verified_shares_per_output: BTreeMap<PeerId, Vec<Option<BlindedSignatureShare>>> = api
644 .request_with_strategy_retry(
645 FilterMapThreshold::new(
646 move |peer, outcomes| ready(verify(peer, outcomes)),
647 api.all_peers().to_num_peers(),
648 ),
649 AWAIT_OUTPUTS_OUTCOMES_ENDPOINT.to_owned(),
650 ApiRequestErased::new(common.out_point_range),
651 )
652 .await;
653
654 let threshold = api.all_peers().to_num_peers().threshold();
656 let mut ret = vec![];
657
658 for (out_idx, (_amount, _issuance_request)) in issuance_requests {
659 let relative_idx = (out_idx - common.out_point_range.start_idx()) as usize;
660 let mut blinded_sig_shares = BTreeMap::new();
661
662 for (peer_id, shares) in &verified_shares_per_output {
664 if let Some(Some(share)) = shares.get(relative_idx) {
665 blinded_sig_shares.insert(*peer_id, *share);
666 }
667 }
668
669 assert!(threshold <= blinded_sig_shares.len());
670 ret.push((out_idx, blinded_sig_shares));
671 }
672
673 ret
674 }
675
676 async fn await_outcome_ready_legacy(
678 api: &fedimint_api_client::api::DynGlobalApi,
679 common: MintOutputCommon,
680 module_decoder: Decoder,
681 issuance_requests: BTreeMap<u64, (Amount, NoteIssuanceRequest)>,
682 tbs_pks: BTreeMap<PeerId, Tiered<PublicKeyShare>>,
683 ) -> Vec<(u64, BTreeMap<PeerId, BlindedSignatureShare>)> {
684 let mut ret = vec![];
685 let mut issuance_requests_iter = issuance_requests.into_iter();
686
687 if let Some((out_idx, (amount, issuance_request))) = issuance_requests_iter.next() {
690 let module_decoder = module_decoder.clone();
691 let tbs_pks = tbs_pks.clone();
692
693 let blinded_sig_share = api
694 .request_with_strategy_retry(
695 FilterMapThreshold::new(
696 move |peer, outcome| {
697 ready(
698 verify_blind_share(
699 peer,
700 &outcome,
701 amount,
702 issuance_request.blinded_message(),
703 &module_decoder,
704 &tbs_pks,
705 )
706 .map_err(|err| {
707 ServerError::InvalidResponse(err.fmt_compact().to_string())
708 }),
709 )
710 },
711 api.all_peers().to_num_peers(),
712 ),
713 AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
714 ApiRequestErased::new(OutPoint {
715 txid: common.txid(),
716 out_idx,
717 }),
718 )
719 .await;
720
721 ret.push((out_idx, blinded_sig_share));
722 } else {
723 return vec![];
724 }
725
726 ret.extend(
728 join_all(
729 issuance_requests_iter.map(|(out_idx, (amount, issuance_request))| {
730 let module_decoder = module_decoder.clone();
731 let tbs_pks = tbs_pks.clone();
732 async move {
733 let blinded_sig_share = api
734 .request_with_strategy_retry(
735 FilterMapThreshold::new(
736 move |peer, outcome| {
737 ready(
738 verify_blind_share(
739 peer,
740 &outcome,
741 amount,
742 issuance_request.blinded_message(),
743 &module_decoder,
744 &tbs_pks,
745 )
746 .map_err(
747 |err| {
748 ServerError::InvalidResponse(
749 err.fmt_compact().to_string(),
750 )
751 },
752 ),
753 )
754 },
755 api.all_peers().to_num_peers(),
756 ),
757 AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
758 ApiRequestErased::new(OutPoint {
759 txid: common.txid(),
760 out_idx,
761 }),
762 )
763 .await;
764
765 (out_idx, blinded_sig_share)
766 }
767 }),
768 )
769 .await,
770 );
771
772 ret
773 }
774
775 async fn transition_outcome_ready(
776 client_ctx: ClientContext<MintClientModule>,
777 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
778 blinded_signature_shares: Vec<(u64, BTreeMap<PeerId, BlindedSignatureShare>)>,
779 old_state: MintOutputStateMachine,
780 tbs_pks: Tiered<AggregatePublicKey>,
781 balance_update_sender: tokio::sync::watch::Sender<()>,
782 ) -> MintOutputStateMachine {
783 let mut amount_total = Amount::ZERO;
787 let MintOutputStates::CreatedMulti(created) = old_state.state else {
788 panic!("Unexpected prior state")
789 };
790
791 let mut spendable_notes: Vec<(Amount, SpendableNote)> = vec![];
792
793 blinded_signature_shares
795 .into_par_iter()
796 .map(|(out_idx, blinded_signature_shares)| {
797 let agg_blind_signature = aggregate_signature_shares(
798 &blinded_signature_shares
799 .into_iter()
800 .map(|(peer, share)| (peer.to_usize() as u64, share))
801 .collect(),
802 );
803
804 let (amount, issuance_request) =
806 created.issuance_requests.get(&out_idx).expect("Must have");
807
808 let amount_key = tbs_pks.tier(amount).expect("Must have keys for any amount");
809
810 let spendable_note = issuance_request.finalize(agg_blind_signature);
811
812 assert!(spendable_note.note().verify(*amount_key), "We checked all signature shares in the trigger future, so the combined signature has to be valid");
813
814 (*amount, spendable_note)
815 })
816 .collect_into_vec(&mut spendable_notes);
817
818 for (amount, spendable_note) in spendable_notes {
819 debug!(target: LOG_CLIENT_MODULE_MINT, amount = %amount, note=%spendable_note, "Adding new note from transaction output");
820
821 client_ctx
822 .log_event(
823 &mut dbtx.module_tx(),
824 NoteCreated {
825 nonce: spendable_note.nonce(),
826 },
827 )
828 .await;
829
830 amount_total += amount;
831 if let Some(note) = dbtx
832 .module_tx()
833 .insert_entry(
834 &NoteKey {
835 amount,
836 nonce: spendable_note.nonce(),
837 },
838 &spendable_note.to_undecoded(),
839 )
840 .await
841 {
842 crit!(target: LOG_CLIENT_MODULE_MINT, %note, "E-cash note was replaced in DB");
843 }
844 }
845
846 client_ctx
847 .log_event(
848 &mut dbtx.module_tx(),
849 ReceivePaymentUpdateEvent {
850 operation_id: old_state.common.operation_id,
851 status: ReceivePaymentStatus::Success,
852 },
853 )
854 .await;
855
856 dbtx.module_tx()
857 .on_commit(move || balance_update_sender.send_replace(()));
858
859 MintOutputStateMachine {
860 common: old_state.common,
861 state: MintOutputStates::Succeeded(MintOutputStatesSucceeded {
862 amount: amount_total,
863 }),
864 }
865 }
866}
867
868pub fn verify_blind_share(
871 peer: PeerId,
872 outcome: &SerdeOutputOutcome,
873 amount: Amount,
874 blinded_message: BlindedMessage,
875 decoder: &Decoder,
876 peer_tbs_pks: &BTreeMap<PeerId, Tiered<PublicKeyShare>>,
877) -> Result<BlindedSignatureShare, VerifyBlindShareError> {
878 let outcome = deserialize_outcome::<MintOutputOutcome>(outcome, decoder)?;
879
880 let blinded_signature_share = outcome
881 .ensure_v0_ref()
882 .expect("We only process output outcome versions created by ourselves")
883 .0;
884
885 let amount_key = peer_tbs_pks
886 .get(&peer)
887 .ok_or(VerifyBlindShareError::UnknownPeer { peer })?
888 .tier(&amount)
889 .map_err(|_| VerifyBlindShareError::InvalidAmountTier { amount })?;
890
891 if !tbs::verify_signature_share(blinded_message, blinded_signature_share, *amount_key) {
892 return Err(VerifyBlindShareError::InvalidSignature);
893 }
894
895 Ok(blinded_signature_share)
896}
897
898#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
900pub struct MintOutputStatesAborted;
901
902#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
904pub struct MintOutputStatesFailed {
905 pub error: String,
906}
907
908#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
910pub struct MintOutputStatesSucceeded {
911 pub amount: Amount,
912}
913
914#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Encodable, Decodable)]
918pub struct NoteIssuanceRequest {
919 spend_key: Keypair,
922 blinding_key: BlindingKey,
924}
925
926impl hash::Hash for NoteIssuanceRequest {
927 fn hash<H: hash::Hasher>(&self, state: &mut H) {
928 self.spend_key.hash(state);
929 }
932}
933impl NoteIssuanceRequest {
934 pub fn new<C>(ctx: &Secp256k1<C>, secret: &DerivableSecret) -> (NoteIssuanceRequest, BlindNonce)
937 where
938 C: Signing,
939 {
940 let spend_key = secret.child_key(SPEND_KEY_CHILD_ID).to_secp_key(ctx);
941 let nonce = Nonce(spend_key.public_key());
942 let blinding_key = BlindingKey(secret.child_key(BLINDING_KEY_CHILD_ID).to_bls12_381_key());
943 let blinded_nonce = blind_message(nonce.to_message(), blinding_key);
944
945 let cr = NoteIssuanceRequest {
946 spend_key,
947 blinding_key,
948 };
949
950 (cr, BlindNonce(blinded_nonce))
951 }
952
953 pub fn nonce(&self) -> Nonce {
955 Nonce(self.spend_key.public_key())
956 }
957
958 pub fn blinded_message(&self) -> BlindedMessage {
959 blind_message(self.nonce().to_message(), self.blinding_key)
960 }
961
962 pub fn finalize(&self, blinded_signature: BlindedSignature) -> SpendableNote {
964 SpendableNote {
965 signature: unblind_signature(self.blinding_key, blinded_signature),
966 spend_key: self.spend_key,
967 }
968 }
969
970 pub fn blinding_key(&self) -> &BlindingKey {
971 &self.blinding_key
972 }
973
974 pub fn spend_key(&self) -> &Keypair {
975 &self.spend_key
976 }
977}