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
38const SPEND_KEY_CHILD_ID: ChildId = ChildId(0);
40
41const BLINDING_KEY_CHILD_ID: ChildId = ChildId(1);
43
44#[cfg_attr(doc, aquamarine::aquamarine)]
45#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
59pub enum MintOutputStates {
60 Created(MintOutputStatesCreated),
62 Aborted(MintOutputStatesAborted),
65 Failed(MintOutputStatesFailed),
70 Succeeded(MintOutputStatesSucceeded),
73 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#[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 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 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 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 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 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 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#[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
712pub 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#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
744pub struct MintOutputStatesAborted;
745
746#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
748pub struct MintOutputStatesFailed {
749 pub error: String,
750}
751
752#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
754pub struct MintOutputStatesSucceeded {
755 pub amount: Amount,
756}
757
758#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Encodable, Decodable)]
762pub struct NoteIssuanceRequest {
763 spend_key: Keypair,
766 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 }
776}
777impl NoteIssuanceRequest {
778 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 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 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}