fedimint_mint_client/
output.rs

1use std::collections::BTreeMap;
2use std::hash;
3
4use anyhow::{anyhow, bail};
5use fedimint_api_client::api::{
6    FederationApiExt, SerdeOutputOutcome, ServerError,
7    VERSION_THAT_INTRODUCED_AWAIT_OUTPUTS_OUTCOMES, deserialize_outcome,
8};
9use fedimint_api_client::query::FilterMapThreshold;
10use fedimint_client_module::DynGlobalClientContext;
11use fedimint_client_module::module::{ClientContext, OutPointRange};
12use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
13use fedimint_core::core::{Decoder, OperationId};
14use fedimint_core::db::IDatabaseTransactionOpsCoreTyped;
15use fedimint_core::encoding::{Decodable, Encodable};
16use fedimint_core::endpoint_constants::AWAIT_OUTPUTS_OUTCOMES_ENDPOINT;
17use fedimint_core::module::ApiRequestErased;
18use fedimint_core::secp256k1::{Keypair, Secp256k1, Signing};
19use fedimint_core::util::FmtCompactAnyhow as _;
20use fedimint_core::{Amount, NumPeersExt, OutPoint, PeerId, Tiered, TransactionId, crit};
21use fedimint_derive_secret::{ChildId, DerivableSecret};
22use fedimint_logging::LOG_CLIENT_MODULE_MINT;
23use fedimint_mint_common::endpoint_constants::AWAIT_OUTPUT_OUTCOME_ENDPOINT;
24use fedimint_mint_common::{BlindNonce, MintOutputOutcome, Nonce};
25use futures::future::join_all;
26use rayon::iter::{IndexedParallelIterator, IntoParallelIterator as _, ParallelIterator as _};
27use serde::{Deserialize, Serialize};
28use tbs::{
29    AggregatePublicKey, BlindedMessage, BlindedSignature, BlindedSignatureShare, BlindingKey,
30    PublicKeyShare, aggregate_signature_shares, blind_message, unblind_signature,
31};
32use tracing::debug;
33
34use crate::client_db::NoteKey;
35use crate::event::{NoteCreated, ReceivePaymentStatus, ReceivePaymentUpdateEvent};
36use crate::{MintClientContext, MintClientModule, SpendableNote};
37
38/// Child ID used to derive the spend key from a note's [`DerivableSecret`]
39const SPEND_KEY_CHILD_ID: ChildId = ChildId(0);
40
41/// Child ID used to derive the blinding key from a note's [`DerivableSecret`]
42const BLINDING_KEY_CHILD_ID: ChildId = ChildId(1);
43
44#[cfg_attr(doc, aquamarine::aquamarine)]
45/// State machine managing the e-cash issuance process related to a mint output.
46///
47/// ```mermaid
48/// graph LR
49///     classDef virtual fill:#fff,stroke-dasharray: 5 5
50///
51///     Created -- containing tx rejected --> Aborted
52///     Created -- await output outcome --> Outcome["Outcome Received"]:::virtual
53///     subgraph Await Outcome
54///     Outcome -- valid blind signatures  --> Succeeded
55///     Outcome -- invalid blind signatures  --> Failed
56///     end
57/// ```
58#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
59pub enum MintOutputStates {
60    /// Issuance request was created, we are waiting for blind signatures
61    Created(MintOutputStatesCreated),
62    /// The transaction containing the issuance was rejected, we can stop
63    /// looking for decryption shares
64    Aborted(MintOutputStatesAborted),
65    // FIXME: handle offline federation failure mode more gracefully
66    /// The transaction containing the issuance was accepted but an unexpected
67    /// error occurred, this should never happen with a honest federation and
68    /// bug-free code.
69    Failed(MintOutputStatesFailed),
70    /// The issuance was completed successfully and the e-cash notes added to
71    /// our wallet
72    Succeeded(MintOutputStatesSucceeded),
73    /// Issuance request was created, we are waiting for blind signatures
74    CreatedMulti(MintOutputStatesCreatedMulti),
75}
76
77#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
78pub struct MintOutputCommonV0 {
79    pub(crate) operation_id: OperationId,
80    pub(crate) out_point: OutPoint,
81}
82
83#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
84pub struct MintOutputCommon {
85    pub(crate) operation_id: OperationId,
86    pub(crate) out_point_range: OutPointRange,
87}
88
89impl MintOutputCommon {
90    pub fn txid(self) -> TransactionId {
91        self.out_point_range.txid()
92    }
93}
94
95#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
96pub struct MintOutputStateMachineV0 {
97    pub(crate) common: MintOutputCommonV0,
98    pub(crate) state: MintOutputStates,
99}
100
101#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
102pub struct MintOutputStateMachine {
103    pub(crate) common: MintOutputCommon,
104    pub(crate) state: MintOutputStates,
105}
106
107impl State for MintOutputStateMachine {
108    type ModuleContext = MintClientContext;
109
110    fn transitions(
111        &self,
112        context: &Self::ModuleContext,
113        global_context: &DynGlobalClientContext,
114    ) -> Vec<StateTransition<Self>> {
115        match &self.state {
116            MintOutputStates::Created(created) => {
117                created.transitions(context, global_context, self.common)
118            }
119            MintOutputStates::CreatedMulti(created) => {
120                created.transitions(context, global_context, self.common)
121            }
122            MintOutputStates::Aborted(_)
123            | MintOutputStates::Failed(_)
124            | MintOutputStates::Succeeded(_) => {
125                vec![]
126            }
127        }
128    }
129
130    fn operation_id(&self) -> OperationId {
131        self.common.operation_id
132    }
133}
134
135/// See [`MintOutputStates`]
136#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
137pub struct MintOutputStatesCreated {
138    pub(crate) amount: Amount,
139    pub(crate) issuance_request: NoteIssuanceRequest,
140}
141
142impl MintOutputStatesCreated {
143    fn transitions(
144        &self,
145        // TODO: make cheaper to clone (Arc?)
146        context: &MintClientContext,
147        global_context: &DynGlobalClientContext,
148        common: MintOutputCommon,
149    ) -> Vec<StateTransition<MintOutputStateMachine>> {
150        let tbs_pks = context.tbs_pks.clone();
151        let client_ctx = context.client_ctx.clone();
152        let balance_update_sender = context.balance_update_sender.clone();
153
154        vec![
155            // Check if transaction was rejected
156            StateTransition::new(
157                Self::await_tx_rejected(global_context.clone(), common),
158                |_dbtx, (), state| Box::pin(async move { Self::transition_tx_rejected(&state) }),
159            ),
160            // Check for output outcome
161            StateTransition::new(
162                Self::await_outcome_ready(
163                    global_context.clone(),
164                    common,
165                    context.mint_decoder.clone(),
166                    self.amount,
167                    self.issuance_request.blinded_message(),
168                    context.peer_tbs_pks.clone(),
169                ),
170                move |dbtx, blinded_signature_shares, old_state| {
171                    Box::pin(Self::transition_outcome_ready(
172                        client_ctx.clone(),
173                        dbtx,
174                        blinded_signature_shares,
175                        old_state,
176                        tbs_pks.clone(),
177                        balance_update_sender.clone(),
178                    ))
179                },
180            ),
181        ]
182    }
183
184    async fn await_tx_rejected(global_context: DynGlobalClientContext, common: MintOutputCommon) {
185        if global_context
186            .await_tx_accepted(common.txid())
187            .await
188            .is_err()
189        {
190            return;
191        }
192        std::future::pending::<()>().await;
193    }
194
195    fn transition_tx_rejected(old_state: &MintOutputStateMachine) -> MintOutputStateMachine {
196        assert!(matches!(old_state.state, MintOutputStates::Created(_)));
197
198        MintOutputStateMachine {
199            common: old_state.common,
200            state: MintOutputStates::Aborted(MintOutputStatesAborted),
201        }
202    }
203
204    async fn await_outcome_ready(
205        global_context: DynGlobalClientContext,
206        common: MintOutputCommon,
207        module_decoder: Decoder,
208        amount: Amount,
209        message: BlindedMessage,
210        tbs_pks: BTreeMap<PeerId, Tiered<PublicKeyShare>>,
211    ) -> BTreeMap<PeerId, BlindedSignatureShare> {
212        global_context
213            .api()
214            .request_with_strategy_retry(
215                // this query collects a threshold of 2f + 1 valid blind signature shares
216                FilterMapThreshold::new(
217                    move |peer, outcome| {
218                        verify_blind_share(
219                            peer,
220                            &outcome,
221                            amount,
222                            message,
223                            &module_decoder,
224                            &tbs_pks,
225                        )
226                        .map_err(ServerError::InvalidResponse)
227                    },
228                    global_context.api().all_peers().to_num_peers(),
229                ),
230                AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
231                ApiRequestErased::new(OutPoint {
232                    txid: common.txid(),
233                    out_idx: common.out_point_range.start_idx(),
234                }),
235            )
236            .await
237    }
238
239    async fn transition_outcome_ready(
240        client_ctx: ClientContext<MintClientModule>,
241        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
242        blinded_signature_shares: BTreeMap<PeerId, BlindedSignatureShare>,
243        old_state: MintOutputStateMachine,
244        tbs_pks: Tiered<AggregatePublicKey>,
245        balance_update_sender: tokio::sync::watch::Sender<()>,
246    ) -> MintOutputStateMachine {
247        // we combine the shares, finalize the issuance request with the blind signature
248        // and store the resulting note in the database
249
250        let MintOutputStates::Created(created) = old_state.state else {
251            panic!("Unexpected prior state")
252        };
253
254        let agg_blind_signature = aggregate_signature_shares(
255            &blinded_signature_shares
256                .into_iter()
257                .map(|(peer, share)| (peer.to_usize() as u64, share))
258                .collect(),
259        );
260
261        let amount_key = tbs_pks
262            .tier(&created.amount)
263            .expect("We obtained this amount from tbs_pks when we created the output");
264
265        // this implies that the mint client config's public keys are inconsistent
266        if !tbs::verify_blinded_signature(
267            created.issuance_request.blinded_message(),
268            agg_blind_signature,
269            *amount_key,
270        ) {
271            return MintOutputStateMachine {
272                common: old_state.common,
273                state: MintOutputStates::Failed(MintOutputStatesFailed {
274                    error: "Invalid blind signature".to_string(),
275                }),
276            };
277        }
278
279        let spendable_note = created.issuance_request.finalize(agg_blind_signature);
280
281        assert!(spendable_note.note().verify(*amount_key));
282
283        debug!(target: LOG_CLIENT_MODULE_MINT, amount = %created.amount, note=%spendable_note, "Adding new note from transaction output");
284
285        client_ctx
286            .log_event(
287                &mut dbtx.module_tx(),
288                NoteCreated {
289                    nonce: spendable_note.nonce(),
290                },
291            )
292            .await;
293        if let Some(note) = dbtx
294            .module_tx()
295            .insert_entry(
296                &NoteKey {
297                    amount: created.amount,
298                    nonce: spendable_note.nonce(),
299                },
300                &spendable_note.to_undecoded(),
301            )
302            .await
303        {
304            crit!(target: LOG_CLIENT_MODULE_MINT, %note, "E-cash note was replaced in DB");
305        }
306
307        dbtx.module_tx()
308            .on_commit(move || balance_update_sender.send_replace(()));
309
310        MintOutputStateMachine {
311            common: old_state.common,
312            state: MintOutputStates::Succeeded(MintOutputStatesSucceeded {
313                amount: created.amount,
314            }),
315        }
316    }
317}
318
319/// See [`MintOutputStates`]
320#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
321pub struct MintOutputStatesCreatedMulti {
322    pub(crate) issuance_requests: BTreeMap<u64, (Amount, NoteIssuanceRequest)>,
323}
324
325impl MintOutputStatesCreatedMulti {
326    fn transitions(
327        &self,
328        // TODO: make cheaper to clone (Arc?)
329        context: &MintClientContext,
330        global_context: &DynGlobalClientContext,
331        common: MintOutputCommon,
332    ) -> Vec<StateTransition<MintOutputStateMachine>> {
333        let tbs_pks = context.tbs_pks.clone();
334        let client_ctx = context.client_ctx.clone();
335        let client_ctx_rejected = context.client_ctx.clone();
336        let balance_update_sender = context.balance_update_sender.clone();
337
338        vec![
339            // Check if transaction was rejected
340            StateTransition::new(
341                Self::await_tx_rejected(global_context.clone(), common),
342                move |dbtx, (), state| {
343                    Box::pin(Self::transition_tx_rejected(
344                        client_ctx_rejected.clone(),
345                        dbtx,
346                        state,
347                    ))
348                },
349            ),
350            // Check for output outcome
351            StateTransition::new(
352                Self::await_outcome_ready(
353                    global_context.clone(),
354                    common,
355                    context.mint_decoder.clone(),
356                    self.issuance_requests.clone(),
357                    context.peer_tbs_pks.clone(),
358                ),
359                move |dbtx, blinded_signature_shares, old_state| {
360                    Box::pin(Self::transition_outcome_ready(
361                        client_ctx.clone(),
362                        dbtx,
363                        blinded_signature_shares,
364                        old_state,
365                        tbs_pks.clone(),
366                        balance_update_sender.clone(),
367                    ))
368                },
369            ),
370        ]
371    }
372
373    async fn await_tx_rejected(global_context: DynGlobalClientContext, common: MintOutputCommon) {
374        if global_context
375            .await_tx_accepted(common.txid())
376            .await
377            .is_err()
378        {
379            return;
380        }
381        std::future::pending::<()>().await;
382    }
383
384    async fn transition_tx_rejected(
385        client_ctx: ClientContext<MintClientModule>,
386        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
387        old_state: MintOutputStateMachine,
388    ) -> MintOutputStateMachine {
389        assert!(matches!(old_state.state, MintOutputStates::CreatedMulti(_)));
390
391        client_ctx
392            .log_event(
393                &mut dbtx.module_tx(),
394                ReceivePaymentUpdateEvent {
395                    operation_id: old_state.common.operation_id,
396                    status: ReceivePaymentStatus::Rejected,
397                },
398            )
399            .await;
400
401        MintOutputStateMachine {
402            common: old_state.common,
403            state: MintOutputStates::Aborted(MintOutputStatesAborted),
404        }
405    }
406
407    async fn await_outcome_ready(
408        global_context: DynGlobalClientContext,
409        common: MintOutputCommon,
410        module_decoder: Decoder,
411        issuance_requests: BTreeMap<u64, (Amount, NoteIssuanceRequest)>,
412        tbs_pks: BTreeMap<PeerId, Tiered<PublicKeyShare>>,
413    ) -> Vec<(u64, BTreeMap<PeerId, BlindedSignatureShare>)> {
414        let api = global_context.api();
415        let core_api_version = global_context.core_api_version().await;
416
417        // Use the new efficient batch endpoint if the server supports it
418        if VERSION_THAT_INTRODUCED_AWAIT_OUTPUTS_OUTCOMES <= core_api_version {
419            Self::await_outcome_ready_batch(api, common, module_decoder, issuance_requests, tbs_pks)
420                .await
421        } else {
422            // Fall back to the old sequential approach for older servers
423            Self::await_outcome_ready_legacy(
424                api,
425                common,
426                module_decoder,
427                issuance_requests,
428                tbs_pks,
429            )
430            .await
431        }
432    }
433
434    /// Efficient batch version using `AWAIT_OUTPUTS_OUTCOMES_ENDPOINT`
435    async fn await_outcome_ready_batch(
436        api: &fedimint_api_client::api::DynGlobalApi,
437        common: MintOutputCommon,
438        module_decoder: Decoder,
439        issuance_requests: BTreeMap<u64, (Amount, NoteIssuanceRequest)>,
440        tbs_pks: BTreeMap<PeerId, Tiered<PublicKeyShare>>,
441    ) -> Vec<(u64, BTreeMap<PeerId, BlindedSignatureShare>)> {
442        if issuance_requests.is_empty() {
443            return vec![];
444        }
445
446        // Use custom query strategy to collect and verify outcomes from all guardians
447        let issuance_requests_clone = issuance_requests.clone();
448        let verified_shares_per_output: BTreeMap<PeerId, Vec<Option<BlindedSignatureShare>>> = api
449            .request_with_strategy_retry(
450                FilterMapThreshold::new(
451                    move |peer, outcomes: Vec<Option<SerdeOutputOutcome>>| {
452                        // Verify the response has the expected length
453                        if outcomes.len() != common.out_point_range.count() {
454                            return Err(ServerError::InvalidResponse(anyhow::anyhow!(
455                                "Peer {peer} returned {} outcomes but expected {}",
456                                outcomes.len(),
457                                common.out_point_range.count()
458                            )));
459                        }
460
461                        // Verify each outcome and extract valid blind signature shares
462                        // If ANY share is invalid, reject the ENTIRE response from this guardian
463                        let mut verified_shares = Vec::with_capacity(outcomes.len());
464                        for (relative_idx, outcome_opt) in outcomes.into_iter().enumerate() {
465                            let out_idx = common.out_point_range.start_idx() + relative_idx as u64;
466
467                            // We should have an issuance request for every output in the range
468                            let (amount, issuance_request) = issuance_requests_clone
469                                .get(&out_idx)
470                                .expect("issuance_request must exist for every output in range");
471
472                            let share = if let Some(outcome) = outcome_opt {
473                                match verify_blind_share(
474                                    peer,
475                                    &outcome,
476                                    *amount,
477                                    issuance_request.blinded_message(),
478                                    &module_decoder,
479                                    &tbs_pks,
480                                ) {
481                                    Ok(share) => Some(share),
482                                    Err(err) => {
483                                        // Invalid share - reject entire response from this guardian
484                                        tracing::warn!(
485                                            target: LOG_CLIENT_MODULE_MINT,
486                                            %peer,
487                                            err = %err.fmt_compact_anyhow(),
488                                            out_point = %OutPoint { txid: common.txid(), out_idx},
489                                            "Invalid signature share from peer"
490                                        );
491                                        return Err(ServerError::InvalidResponse(err));
492                                    }
493                                }
494                            } else {
495                                None
496                            };
497
498                            verified_shares.push(share);
499                        }
500
501                        Ok(verified_shares)
502                    },
503                    api.all_peers().to_num_peers(),
504                ),
505                AWAIT_OUTPUTS_OUTCOMES_ENDPOINT.to_owned(),
506                ApiRequestErased::new(common.out_point_range),
507            )
508            .await;
509
510        // Reorganize from per-peer to per-output
511        let threshold = api.all_peers().to_num_peers().threshold();
512        let mut ret = vec![];
513
514        for (out_idx, (_amount, _issuance_request)) in issuance_requests {
515            let relative_idx = (out_idx - common.out_point_range.start_idx()) as usize;
516            let mut blinded_sig_shares = BTreeMap::new();
517
518            // Collect verified shares from all peers for this output
519            for (peer_id, shares) in &verified_shares_per_output {
520                if let Some(Some(share)) = shares.get(relative_idx) {
521                    blinded_sig_shares.insert(*peer_id, *share);
522                }
523            }
524
525            assert!(threshold <= blinded_sig_shares.len());
526            ret.push((out_idx, blinded_sig_shares));
527        }
528
529        ret
530    }
531
532    /// Legacy sequential version for backwards compatibility
533    async fn await_outcome_ready_legacy(
534        api: &fedimint_api_client::api::DynGlobalApi,
535        common: MintOutputCommon,
536        module_decoder: Decoder,
537        issuance_requests: BTreeMap<u64, (Amount, NoteIssuanceRequest)>,
538        tbs_pks: BTreeMap<PeerId, Tiered<PublicKeyShare>>,
539    ) -> Vec<(u64, BTreeMap<PeerId, BlindedSignatureShare>)> {
540        let mut ret = vec![];
541        let mut issuance_requests_iter = issuance_requests.into_iter();
542
543        // Wait for the result of the first output only, to save server side
544        // resources
545        if let Some((out_idx, (amount, issuance_request))) = issuance_requests_iter.next() {
546            let module_decoder = module_decoder.clone();
547            let tbs_pks = tbs_pks.clone();
548
549            let blinded_sig_share = api
550                .request_with_strategy_retry(
551                    FilterMapThreshold::new(
552                        move |peer, outcome| {
553                            verify_blind_share(
554                                peer,
555                                &outcome,
556                                amount,
557                                issuance_request.blinded_message(),
558                                &module_decoder,
559                                &tbs_pks,
560                            )
561                            .map_err(ServerError::InvalidResponse)
562                        },
563                        api.all_peers().to_num_peers(),
564                    ),
565                    AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
566                    ApiRequestErased::new(OutPoint {
567                        txid: common.txid(),
568                        out_idx,
569                    }),
570                )
571                .await;
572
573            ret.push((out_idx, blinded_sig_share));
574        } else {
575            return vec![];
576        }
577
578        // We know the tx outcomes are ready, get all of them at once
579        ret.extend(
580            join_all(
581                issuance_requests_iter.map(|(out_idx, (amount, issuance_request))| {
582                    let module_decoder = module_decoder.clone();
583                    let tbs_pks = tbs_pks.clone();
584                    async move {
585                        let blinded_sig_share = api
586                            .request_with_strategy_retry(
587                                FilterMapThreshold::new(
588                                    move |peer, outcome| {
589                                        verify_blind_share(
590                                            peer,
591                                            &outcome,
592                                            amount,
593                                            issuance_request.blinded_message(),
594                                            &module_decoder,
595                                            &tbs_pks,
596                                        )
597                                        .map_err(ServerError::InvalidResponse)
598                                    },
599                                    api.all_peers().to_num_peers(),
600                                ),
601                                AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
602                                ApiRequestErased::new(OutPoint {
603                                    txid: common.txid(),
604                                    out_idx,
605                                }),
606                            )
607                            .await;
608
609                        (out_idx, blinded_sig_share)
610                    }
611                }),
612            )
613            .await,
614        );
615
616        ret
617    }
618
619    async fn transition_outcome_ready(
620        client_ctx: ClientContext<MintClientModule>,
621        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
622        blinded_signature_shares: Vec<(u64, BTreeMap<PeerId, BlindedSignatureShare>)>,
623        old_state: MintOutputStateMachine,
624        tbs_pks: Tiered<AggregatePublicKey>,
625        balance_update_sender: tokio::sync::watch::Sender<()>,
626    ) -> MintOutputStateMachine {
627        // we combine the shares, finalize the issuance request with the blind signature
628        // and store the resulting note in the database
629
630        let mut amount_total = Amount::ZERO;
631        let MintOutputStates::CreatedMulti(created) = old_state.state else {
632            panic!("Unexpected prior state")
633        };
634
635        let mut spendable_notes: Vec<(Amount, SpendableNote)> = vec![];
636
637        // Note verification is relatively slow and CPU-bound, so parallelize them
638        blinded_signature_shares
639            .into_par_iter()
640            .map(|(out_idx, blinded_signature_shares)| {
641                let agg_blind_signature = aggregate_signature_shares(
642                    &blinded_signature_shares
643                        .into_iter()
644                        .map(|(peer, share)| (peer.to_usize() as u64, share))
645                        .collect(),
646                );
647
648                // this implies that the mint client config's public keys are inconsistent
649                let (amount, issuance_request) =
650                    created.issuance_requests.get(&out_idx).expect("Must have");
651
652                let amount_key = tbs_pks.tier(amount).expect("Must have keys for any amount");
653
654                let spendable_note = issuance_request.finalize(agg_blind_signature);
655
656                assert!(spendable_note.note().verify(*amount_key), "We checked all signature shares in the trigger future, so the combined signature has to be valid");
657
658                (*amount, spendable_note)
659            })
660            .collect_into_vec(&mut spendable_notes);
661
662        for (amount, spendable_note) in spendable_notes {
663            debug!(target: LOG_CLIENT_MODULE_MINT, amount = %amount, note=%spendable_note, "Adding new note from transaction output");
664
665            client_ctx
666                .log_event(
667                    &mut dbtx.module_tx(),
668                    NoteCreated {
669                        nonce: spendable_note.nonce(),
670                    },
671                )
672                .await;
673
674            amount_total += amount;
675            if let Some(note) = dbtx
676                .module_tx()
677                .insert_entry(
678                    &NoteKey {
679                        amount,
680                        nonce: spendable_note.nonce(),
681                    },
682                    &spendable_note.to_undecoded(),
683                )
684                .await
685            {
686                crit!(target: LOG_CLIENT_MODULE_MINT, %note, "E-cash note was replaced in DB");
687            }
688        }
689
690        client_ctx
691            .log_event(
692                &mut dbtx.module_tx(),
693                ReceivePaymentUpdateEvent {
694                    operation_id: old_state.common.operation_id,
695                    status: ReceivePaymentStatus::Success,
696                },
697            )
698            .await;
699
700        dbtx.module_tx()
701            .on_commit(move || balance_update_sender.send_replace(()));
702
703        MintOutputStateMachine {
704            common: old_state.common,
705            state: MintOutputStates::Succeeded(MintOutputStatesSucceeded {
706                amount: amount_total,
707            }),
708        }
709    }
710}
711
712/// # Panics
713/// If the given `outcome` is not a [`MintOutputOutcome::V0`] outcome.
714pub fn verify_blind_share(
715    peer: PeerId,
716    outcome: &SerdeOutputOutcome,
717    amount: Amount,
718    blinded_message: BlindedMessage,
719    decoder: &Decoder,
720    peer_tbs_pks: &BTreeMap<PeerId, Tiered<PublicKeyShare>>,
721) -> anyhow::Result<BlindedSignatureShare> {
722    let outcome = deserialize_outcome::<MintOutputOutcome>(outcome, decoder)?;
723
724    let blinded_signature_share = outcome
725        .ensure_v0_ref()
726        .expect("We only process output outcome versions created by ourselves")
727        .0;
728
729    let amount_key = peer_tbs_pks
730        .get(&peer)
731        .ok_or(anyhow!("Unknown peer"))?
732        .tier(&amount)
733        .map_err(|_| anyhow!("Invalid Amount Tier"))?;
734
735    if !tbs::verify_signature_share(blinded_message, blinded_signature_share, *amount_key) {
736        bail!("Invalid blind signature")
737    }
738
739    Ok(blinded_signature_share)
740}
741
742/// See [`MintOutputStates`]
743#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
744pub struct MintOutputStatesAborted;
745
746/// See [`MintOutputStates`]
747#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
748pub struct MintOutputStatesFailed {
749    pub error: String,
750}
751
752/// See [`MintOutputStates`]
753#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
754pub struct MintOutputStatesSucceeded {
755    pub amount: Amount,
756}
757
758/// Keeps the data to generate [`SpendableNote`] once the
759/// mint successfully processed the transaction signing the corresponding
760/// [`BlindNonce`].
761#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Encodable, Decodable)]
762pub struct NoteIssuanceRequest {
763    /// Spend key from which the note nonce (corresponding public key) is
764    /// derived
765    spend_key: Keypair,
766    /// Key to unblind the blind signature supplied by the mint for this note
767    blinding_key: BlindingKey,
768}
769
770impl hash::Hash for NoteIssuanceRequest {
771    fn hash<H: hash::Hasher>(&self, state: &mut H) {
772        self.spend_key.hash(state);
773        // ignore `blinding_key` as it doesn't impl Hash; `spend_key` has enough
774        // entropy anyway
775    }
776}
777impl NoteIssuanceRequest {
778    /// Generate a request session for a single note and returns it plus the
779    /// corresponding blinded message
780    pub fn new<C>(ctx: &Secp256k1<C>, secret: &DerivableSecret) -> (NoteIssuanceRequest, BlindNonce)
781    where
782        C: Signing,
783    {
784        let spend_key = secret.child_key(SPEND_KEY_CHILD_ID).to_secp_key(ctx);
785        let nonce = Nonce(spend_key.public_key());
786        let blinding_key = BlindingKey(secret.child_key(BLINDING_KEY_CHILD_ID).to_bls12_381_key());
787        let blinded_nonce = blind_message(nonce.to_message(), blinding_key);
788
789        let cr = NoteIssuanceRequest {
790            spend_key,
791            blinding_key,
792        };
793
794        (cr, BlindNonce(blinded_nonce))
795    }
796
797    /// Return nonce of the e-cash note being requested
798    pub fn nonce(&self) -> Nonce {
799        Nonce(self.spend_key.public_key())
800    }
801
802    pub fn blinded_message(&self) -> BlindedMessage {
803        blind_message(self.nonce().to_message(), self.blinding_key)
804    }
805
806    /// Use the blind signature to create spendable e-cash notes
807    pub fn finalize(&self, blinded_signature: BlindedSignature) -> SpendableNote {
808        SpendableNote {
809            signature: unblind_signature(self.blinding_key, blinded_signature),
810            spend_key: self.spend_key,
811        }
812    }
813
814    pub fn blinding_key(&self) -> &BlindingKey {
815        &self.blinding_key
816    }
817
818    pub fn spend_key(&self) -> &Keypair {
819        &self.spend_key
820    }
821}