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