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