fedimint_mint_client/
output.rs

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