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
39const SPEND_KEY_CHILD_ID: ChildId = ChildId(0);
41
42const BLINDING_KEY_CHILD_ID: ChildId = ChildId(1);
44
45#[cfg_attr(doc, aquamarine::aquamarine)]
46#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
60pub enum MintOutputStates {
61 Created(MintOutputStatesCreated),
63 Aborted(MintOutputStatesAborted),
66 Failed(MintOutputStatesFailed),
71 Succeeded(MintOutputStatesSucceeded),
74 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#[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 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 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 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 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 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 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#[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
713pub 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#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
745pub struct MintOutputStatesAborted;
746
747#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
749pub struct MintOutputStatesFailed {
750 pub error: String,
751}
752
753#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
755pub struct MintOutputStatesSucceeded {
756 pub amount: Amount,
757}
758
759#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Encodable, Decodable)]
763pub struct NoteIssuanceRequest {
764 spend_key: Keypair,
767 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 }
777}
778impl NoteIssuanceRequest {
779 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 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 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}