Skip to main content

fedimint_mint_client/
output.rs

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
40/// Child ID used to derive the spend key from a note's [`DerivableSecret`]
41const SPEND_KEY_CHILD_ID: ChildId = ChildId(0);
42
43/// Child ID used to derive the blinding key from a note's [`DerivableSecret`]
44const BLINDING_KEY_CHILD_ID: ChildId = ChildId(1);
45
46#[cfg_attr(doc, aquamarine::aquamarine)]
47/// State machine managing the e-cash issuance process related to a mint output.
48///
49/// ```mermaid
50/// graph LR
51///     classDef virtual fill:#fff,stroke-dasharray: 5 5
52///
53///     Created -- containing tx rejected --> Aborted
54///     Created -- await output outcome --> Outcome["Outcome Received"]:::virtual
55///     subgraph Await Outcome
56///     Outcome -- valid blind signatures  --> Succeeded
57///     Outcome -- invalid blind signatures  --> Failed
58///     end
59/// ```
60#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
61pub enum MintOutputStates {
62    /// Issuance request was created, we are waiting for blind signatures
63    Created(MintOutputStatesCreated),
64    /// The transaction containing the issuance was rejected, we can stop
65    /// looking for decryption shares
66    Aborted(MintOutputStatesAborted),
67    // FIXME: handle offline federation failure mode more gracefully
68    /// The transaction containing the issuance was accepted but an unexpected
69    /// error occurred, this should never happen with a honest federation and
70    /// bug-free code.
71    Failed(MintOutputStatesFailed),
72    /// The issuance was completed successfully and the e-cash notes added to
73    /// our wallet
74    Succeeded(MintOutputStatesSucceeded),
75    /// Issuance request was created, we are waiting for blind signatures
76    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    /// Transaction ID this output belongs to
111    pub fn txid(&self) -> TransactionId {
112        self.common.out_point_range.txid()
113    }
114
115    /// Returns `(out_idx, amount, nonce, blind_nonce)` for each note being
116    /// created.
117    ///
118    /// Only available when the state machine is in a `Created` or
119    /// `CreatedMulti` state (i.e. before the blind signature is finalized).
120    /// Returns an empty vec for terminal states.
121    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/// See [`MintOutputStates`]
243#[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        // TODO: make cheaper to clone (Arc?)
253        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            // Check if transaction was rejected
263            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            // Check for output outcome
268            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                // this query collects a threshold of 2f + 1 valid blind signature shares
337                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        // we combine the shares, finalize the issuance request with the blind signature
369        // and store the resulting note in the database
370
371        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        // this implies that the mint client config's public keys are inconsistent
387        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/// See [`MintOutputStates`]
441#[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        // TODO: make cheaper to clone (Arc?)
450        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            // Check if transaction was rejected
461            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            // Check for output outcome
472            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        // Use the new efficient batch endpoint if the server supports it
553        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            // Fall back to the old sequential approach for older servers
558            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    /// Efficient batch version using `AWAIT_OUTPUTS_OUTCOMES_ENDPOINT`
570    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        // Use custom query strategy to collect and verify outcomes from all guardians
582        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                        // Verify the response has the expected length
588                        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                        // Verify each outcome and extract valid blind signature shares
597                        // If ANY share is invalid, reject the ENTIRE response from this guardian
598                        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                            // We should have an issuance request for every output in the range
603                            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                                        // Invalid share - reject entire response from this guardian
619                                        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        // Reorganize from per-peer to per-output
646        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            // Collect verified shares from all peers for this output
654            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    /// Legacy sequential version for backwards compatibility
668    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        // Wait for the result of the first output only, to save server side
679        // resources
680        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        // We know the tx outcomes are ready, get all of them at once
714        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        // we combine the shares, finalize the issuance request with the blind signature
763        // and store the resulting note in the database
764
765        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        // Note verification is relatively slow and CPU-bound, so parallelize them
773        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                // this implies that the mint client config's public keys are inconsistent
784                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
847/// # Panics
848/// If the given `outcome` is not a [`MintOutputOutcome::V0`] outcome.
849pub 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/// See [`MintOutputStates`]
878#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
879pub struct MintOutputStatesAborted;
880
881/// See [`MintOutputStates`]
882#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
883pub struct MintOutputStatesFailed {
884    pub error: String,
885}
886
887/// See [`MintOutputStates`]
888#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
889pub struct MintOutputStatesSucceeded {
890    pub amount: Amount,
891}
892
893/// Keeps the data to generate [`SpendableNote`] once the
894/// mint successfully processed the transaction signing the corresponding
895/// [`BlindNonce`].
896#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Encodable, Decodable)]
897pub struct NoteIssuanceRequest {
898    /// Spend key from which the note nonce (corresponding public key) is
899    /// derived
900    spend_key: Keypair,
901    /// Key to unblind the blind signature supplied by the mint for this note
902    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        // ignore `blinding_key` as it doesn't impl Hash; `spend_key` has enough
909        // entropy anyway
910    }
911}
912impl NoteIssuanceRequest {
913    /// Generate a request session for a single note and returns it plus the
914    /// corresponding blinded message
915    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    /// Return nonce of the e-cash note being requested
933    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    /// Use the blind signature to create spendable e-cash notes
942    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}