1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_wrap)]
3#![allow(clippy::module_name_repetitions)]
4#![allow(clippy::must_use_candidate)]
5#![allow(clippy::similar_names)]
6
7pub mod db;
8mod metrics;
9
10use std::collections::{BTreeMap, BTreeSet};
11
12use anyhow::bail;
13use fedimint_core::bitcoin::hashes::sha256;
14use fedimint_core::config::{
15 ServerModuleConfig, ServerModuleConsensusConfig, TypedServerModuleConfig,
16 TypedServerModuleConsensusConfig,
17};
18use fedimint_core::core::ModuleInstanceId;
19use fedimint_core::db::{
20 DatabaseTransaction, DatabaseVersion, DbMigrationError, IDatabaseTransactionOpsCore,
21 IDatabaseTransactionOpsCoreTyped,
22};
23use fedimint_core::encoding::Encodable;
24use fedimint_core::envs::{FM_ENABLE_MODULE_MINT_ENV, is_env_var_set_opt};
25use fedimint_core::module::audit::Audit;
26use fedimint_core::module::{
27 Amounts, ApiEndpoint, ApiError, ApiVersion, CoreConsensusVersion, InputMeta,
28 ModuleConsensusVersion, ModuleInit, SerdeModuleEncodingBase64, TransactionItemAmounts,
29 public_api_endpoint,
30};
31use fedimint_core::{
32 Amount, InPoint, NumPeersExt, OutPoint, PeerId, Tiered, TieredMulti, apply,
33 async_trait_maybe_send, push_db_key_items, push_db_pair_items,
34};
35use fedimint_logging::LOG_MODULE_MINT;
36pub use fedimint_mint_common as common;
37use fedimint_mint_common::config::{
38 FeeConsensus, MintClientConfig, MintConfig, MintConfigConsensus, MintConfigPrivate,
39};
40pub use fedimint_mint_common::{BackupRequest, SignedBackupRequest};
41use fedimint_mint_common::{
42 DEFAULT_MAX_NOTES_PER_DENOMINATION, MODULE_CONSENSUS_VERSION, MintCommonInit,
43 MintConsensusItem, MintInput, MintInputError, MintModuleTypes, MintOutput, MintOutputError,
44 MintOutputOutcome,
45};
46use fedimint_server_core::config::{PeerHandleOps, eval_poly_g2};
47use fedimint_server_core::migration::{
48 ModuleHistoryItem, ServerModuleDbMigrationFn, ServerModuleDbMigrationFnContext,
49 ServerModuleDbMigrationFnContextExt as _,
50};
51use fedimint_server_core::{
52 ConfigGenModuleArgs, EnvVarDoc, ServerModule, ServerModuleInit, ServerModuleInitArgs,
53};
54use futures::{FutureExt as _, StreamExt};
55use itertools::Itertools;
56use metrics::{
57 MINT_INOUT_FEES_SATS, MINT_INOUT_SATS, MINT_ISSUED_ECASH_FEES_SATS, MINT_ISSUED_ECASH_SATS,
58 MINT_REDEEMED_ECASH_FEES_SATS, MINT_REDEEMED_ECASH_SATS,
59};
60use rand::rngs::OsRng;
61use strum::IntoEnumIterator;
62use tbs::{
63 AggregatePublicKey, PublicKeyShare, SecretKeyShare, aggregate_public_key_shares,
64 derive_pk_share, sign_message,
65};
66use threshold_crypto::ff::Field;
67use threshold_crypto::group::Curve;
68use threshold_crypto::{G2Projective, Scalar};
69use tracing::{debug, info, warn};
70
71use crate::common::endpoint_constants::{
72 BLIND_NONCE_USED_ENDPOINT, NOTE_SPENT_ENDPOINT, RECOVERY_BLIND_NONCE_OUTPOINTS_ENDPOINT,
73 RECOVERY_COUNT_ENDPOINT, RECOVERY_SLICE_ENDPOINT, RECOVERY_SLICE_HASH_ENDPOINT,
74};
75use crate::common::{BlindNonce, Nonce, RecoveryItem};
76use crate::db::{
77 BlindNonceKey, BlindNonceKeyPrefix, DbKeyPrefix, MintAuditItemKey, MintAuditItemKeyPrefix,
78 MintOutputOutcomeKey, MintOutputOutcomePrefix, NonceKey, NonceKeyPrefix,
79 RecoveryBlindNonceOutpointKey, RecoveryBlindNonceOutpointKeyPrefix, RecoveryItemKey,
80 RecoveryItemKeyPrefix,
81};
82
83#[derive(Debug, Clone)]
84pub struct MintInit;
85
86impl ModuleInit for MintInit {
87 type Common = MintCommonInit;
88
89 async fn dump_database(
90 &self,
91 dbtx: &mut DatabaseTransaction<'_>,
92 prefix_names: Vec<String>,
93 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
94 let mut mint: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
95 let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
96 prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
97 });
98 for table in filtered_prefixes {
99 match table {
100 DbKeyPrefix::NoteNonce => {
101 push_db_key_items!(dbtx, NonceKeyPrefix, NonceKey, mint, "Used Coins");
102 }
103 DbKeyPrefix::MintAuditItem => {
104 push_db_pair_items!(
105 dbtx,
106 MintAuditItemKeyPrefix,
107 MintAuditItemKey,
108 fedimint_core::Amount,
109 mint,
110 "Mint Audit Items"
111 );
112 }
113 DbKeyPrefix::OutputOutcome => {
114 push_db_pair_items!(
115 dbtx,
116 MintOutputOutcomePrefix,
117 OutputOutcomeKey,
118 MintOutputOutcome,
119 mint,
120 "Output Outcomes"
121 );
122 }
123 DbKeyPrefix::BlindNonce => {
124 push_db_key_items!(
125 dbtx,
126 BlindNonceKeyPrefix,
127 BlindNonceKey,
128 mint,
129 "Used Blind Nonces"
130 );
131 }
132 DbKeyPrefix::RecoveryItem => {
133 push_db_pair_items!(
134 dbtx,
135 RecoveryItemKeyPrefix,
136 RecoveryItemKey,
137 RecoveryItem,
138 mint,
139 "Recovery Items"
140 );
141 }
142 DbKeyPrefix::RecoveryBlindNonceOutpoint => {
143 push_db_pair_items!(
144 dbtx,
145 RecoveryBlindNonceOutpointKeyPrefix,
146 RecoveryBlindNonceOutpointKey,
147 OutPoint,
148 mint,
149 "Recovery Blind Nonce Outpoints"
150 );
151 }
152 }
153 }
154
155 Box::new(mint.into_iter())
156 }
157}
158
159const DEFAULT_DENOMINATION_BASE: u16 = 2;
161
162const MAX_DENOMINATION_SIZE: Amount = Amount::from_bitcoins(1_000_000);
164
165fn gen_denominations() -> Vec<Amount> {
167 Tiered::gen_denominations(DEFAULT_DENOMINATION_BASE, MAX_DENOMINATION_SIZE)
168 .tiers()
169 .copied()
170 .collect()
171}
172
173#[apply(async_trait_maybe_send!)]
174impl ServerModuleInit for MintInit {
175 type Module = Mint;
176
177 fn versions(&self, _core: CoreConsensusVersion) -> &[ModuleConsensusVersion] {
178 &[MODULE_CONSENSUS_VERSION]
179 }
180
181 fn is_enabled_by_default(&self) -> bool {
182 is_env_var_set_opt(FM_ENABLE_MODULE_MINT_ENV).unwrap_or(false)
183 }
184
185 fn get_documented_env_vars(&self) -> Vec<EnvVarDoc> {
186 vec![EnvVarDoc {
187 name: FM_ENABLE_MODULE_MINT_ENV,
188 description: "Set to 1/true to enable the mint (e-cash) module. Disabled by default.",
189 }]
190 }
191
192 async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
193 Ok(Mint::new(args.cfg().to_typed()?))
194 }
195
196 fn trusted_dealer_gen(
197 &self,
198 peers: &[PeerId],
199 args: &ConfigGenModuleArgs,
200 ) -> BTreeMap<PeerId, ServerModuleConfig> {
201 let denominations = gen_denominations();
202
203 let tbs_keys = denominations
204 .iter()
205 .map(|&amount| {
206 let (tbs_pk, tbs_pks, tbs_sks) =
207 dealer_keygen(peers.to_num_peers().threshold(), peers.len());
208 (amount, (tbs_pk, tbs_pks, tbs_sks))
209 })
210 .collect::<BTreeMap<_, _>>();
211
212 let mint_cfg: BTreeMap<_, MintConfig> = peers
213 .iter()
214 .map(|&peer| {
215 let config = MintConfig {
216 consensus: MintConfigConsensus {
217 peer_tbs_pks: peers
218 .iter()
219 .map(|&key_peer| {
220 let keys = denominations
221 .iter()
222 .map(|amount| {
223 (*amount, tbs_keys[amount].1[key_peer.to_usize()])
224 })
225 .collect();
226 (key_peer, keys)
227 })
228 .collect(),
229 fee_consensus: if args.disable_base_fees {
230 FeeConsensus::zero()
231 } else {
232 FeeConsensus::new(0).expect("Relative fee is within range")
233 },
234 max_notes_per_denomination: DEFAULT_MAX_NOTES_PER_DENOMINATION,
235 },
236 private: MintConfigPrivate {
237 tbs_sks: denominations
238 .iter()
239 .map(|amount| (*amount, tbs_keys[amount].2[peer.to_usize()]))
240 .collect(),
241 },
242 };
243 (peer, config)
244 })
245 .collect();
246
247 mint_cfg
248 .into_iter()
249 .map(|(k, v)| (k, v.to_erased()))
250 .collect()
251 }
252
253 async fn distributed_gen(
254 &self,
255 peers: &(dyn PeerHandleOps + Send + Sync),
256 args: &ConfigGenModuleArgs,
257 ) -> anyhow::Result<ServerModuleConfig> {
258 let denominations = gen_denominations();
259
260 let mut amount_keys = BTreeMap::new();
261
262 for amount in &denominations {
263 amount_keys.insert(*amount, peers.run_dkg_g2().await?);
264 }
265
266 let server = MintConfig {
267 private: MintConfigPrivate {
268 tbs_sks: amount_keys
269 .iter()
270 .map(|(amount, (_, sks))| (*amount, tbs::SecretKeyShare(*sks)))
271 .collect(),
272 },
273 consensus: MintConfigConsensus {
274 peer_tbs_pks: peers
275 .num_peers()
276 .peer_ids()
277 .map(|peer| {
278 let pks = amount_keys
279 .iter()
280 .map(|(amount, (pks, _))| {
281 (*amount, PublicKeyShare(eval_poly_g2(pks, &peer)))
282 })
283 .collect::<Tiered<_>>();
284
285 (peer, pks)
286 })
287 .collect(),
288 fee_consensus: if args.disable_base_fees {
289 FeeConsensus::zero()
290 } else {
291 FeeConsensus::new(0).expect("Relative fee is within range")
292 },
293 max_notes_per_denomination: DEFAULT_MAX_NOTES_PER_DENOMINATION,
294 },
295 };
296
297 Ok(server.to_erased())
298 }
299
300 fn validate_config(&self, identity: &PeerId, config: ServerModuleConfig) -> anyhow::Result<()> {
301 let config = config.to_typed::<MintConfig>()?;
302 let sks: BTreeMap<Amount, PublicKeyShare> = config
303 .private
304 .tbs_sks
305 .iter()
306 .map(|(amount, sk)| (amount, derive_pk_share(sk)))
307 .collect();
308 let pks: BTreeMap<Amount, PublicKeyShare> = config
309 .consensus
310 .peer_tbs_pks
311 .get(identity)
312 .unwrap()
313 .as_map()
314 .iter()
315 .map(|(k, v)| (*k, *v))
316 .collect();
317 if sks != pks {
318 bail!("Mint private key doesn't match pubkey share");
319 }
320 if !sks.keys().contains(&Amount::from_msats(1)) {
321 bail!("No msat 1 denomination");
322 }
323
324 Ok(())
325 }
326
327 fn get_client_config(
328 &self,
329 config: &ServerModuleConsensusConfig,
330 ) -> anyhow::Result<MintClientConfig> {
331 let config = MintConfigConsensus::from_erased(config)?;
332 let tbs_pks =
336 TieredMulti::new_aggregate_from_tiered_iter(config.peer_tbs_pks.values().cloned())
337 .into_iter()
338 .map(|(amt, keys)| {
339 let keys = (0_u64..)
340 .zip(keys)
341 .take(config.peer_tbs_pks.to_num_peers().threshold())
342 .collect();
343
344 (amt, aggregate_public_key_shares(&keys))
345 })
346 .collect();
347
348 Ok(MintClientConfig {
349 tbs_pks,
350 fee_consensus: config.fee_consensus.clone(),
351 peer_tbs_pks: config.peer_tbs_pks.clone(),
352 max_notes_per_denomination: config.max_notes_per_denomination,
353 })
354 }
355
356 fn get_database_migrations(
357 &self,
358 ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Mint>> {
359 let mut migrations: BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<_>> =
360 BTreeMap::new();
361 migrations.insert(
362 DatabaseVersion(0),
363 Box::new(|ctx| migrate_db_v0(ctx).boxed()),
364 );
365 migrations.insert(
366 DatabaseVersion(1),
367 Box::new(|ctx| migrate_db_v1(ctx).boxed()),
368 );
369 migrations.insert(
370 DatabaseVersion(2),
371 Box::new(|ctx| migrate_db_v2(ctx).boxed()),
372 );
373 migrations
374 }
375
376 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
377 Some(DbKeyPrefix::iter().map(|p| p as u8).collect())
378 }
379}
380
381async fn migrate_db_v0(
382 mut migration_context: ServerModuleDbMigrationFnContext<'_, Mint>,
383) -> Result<(), DbMigrationError> {
384 let blind_nonces = migration_context
385 .get_typed_module_history_stream()
386 .await
387 .filter_map(|history_item: ModuleHistoryItem<_>| async move {
388 match history_item {
389 ModuleHistoryItem::Output(mint_output, _) => Some(
390 mint_output
391 .ensure_v0_ref()
392 .expect("This migration only runs while we only have v0 outputs")
393 .blind_nonce,
394 ),
395 _ => {
396 None
398 }
399 }
400 })
401 .collect::<Vec<_>>()
402 .await;
403
404 info!(target: LOG_MODULE_MINT, "Found {} blind nonces in history", blind_nonces.len());
405
406 let mut double_issuances = 0usize;
407 for blind_nonce in blind_nonces {
408 if migration_context
409 .dbtx()
410 .insert_entry(&BlindNonceKey(blind_nonce), &())
411 .await
412 .is_some()
413 {
414 double_issuances += 1;
415 debug!(
416 target: LOG_MODULE_MINT,
417 ?blind_nonce,
418 "Blind nonce already used, money was burned!"
419 );
420 }
421 }
422
423 if double_issuances > 0 {
424 warn!(target: LOG_MODULE_MINT, "{double_issuances} blind nonces were reused, money was burned by faulty user clients!");
425 }
426
427 Ok(())
428}
429
430async fn migrate_db_v1(
432 mut migration_context: ServerModuleDbMigrationFnContext<'_, Mint>,
433) -> Result<(), DbMigrationError> {
434 migration_context
435 .dbtx()
436 .raw_remove_by_prefix(&[0x15])
437 .await
438 .expect("DB error");
439 Ok(())
440}
441
442async fn migrate_db_v2(
444 mut ctx: ServerModuleDbMigrationFnContext<'_, Mint>,
445) -> Result<(), DbMigrationError> {
446 let mut recovery_items = Vec::new();
447 let mut blind_nonce_outpoints = Vec::new();
448 let mut stream = ctx.get_typed_module_history_stream().await;
449
450 while let Some(history_item) = stream.next().await {
451 match history_item {
452 ModuleHistoryItem::Output(mint_output, out_point) => {
453 let output = mint_output
454 .ensure_v0_ref()
455 .expect("This migration only runs while we only have v0 outputs");
456
457 recovery_items.push(RecoveryItem::Output {
458 amount: output.amount,
459 nonce: output.blind_nonce.0.consensus_hash(),
460 });
461
462 blind_nonce_outpoints.push((output.blind_nonce, out_point));
463 }
464 ModuleHistoryItem::Input(mint_input) => {
465 let input = mint_input
466 .ensure_v0_ref()
467 .expect("This migration only runs while we only have v0 inputs");
468
469 recovery_items.push(RecoveryItem::Input {
470 nonce: input.note.nonce.consensus_hash(),
471 });
472 }
473 ModuleHistoryItem::ConsensusItem(_) => {}
474 }
475 }
476
477 drop(stream);
478
479 for (index, item) in recovery_items.into_iter().enumerate() {
480 ctx.dbtx()
481 .insert_new_entry(&RecoveryItemKey(index as u64), &item)
482 .await;
483 }
484
485 for (blind_nonce, out_point) in blind_nonce_outpoints {
486 if ctx
487 .dbtx()
488 .insert_entry(&RecoveryBlindNonceOutpointKey(blind_nonce), &out_point)
489 .await
490 .is_some()
491 {
492 warn!(
493 target: LOG_MODULE_MINT,
494 bnonce = ?blind_nonce,
495 "Recovery blind nonce outpoint overwritten; duplicate blind nonce in outputs"
496 );
497 }
498 }
499
500 Ok(())
501}
502
503fn dealer_keygen(
504 threshold: usize,
505 keys: usize,
506) -> (AggregatePublicKey, Vec<PublicKeyShare>, Vec<SecretKeyShare>) {
507 let mut rng = OsRng; let poly: Vec<Scalar> = (0..threshold).map(|_| Scalar::random(&mut rng)).collect();
509
510 let apk = (G2Projective::generator() * eval_polynomial(&poly, &Scalar::zero())).to_affine();
511
512 let sks: Vec<SecretKeyShare> = (0..keys)
513 .map(|idx| SecretKeyShare(eval_polynomial(&poly, &Scalar::from(idx as u64 + 1))))
514 .collect();
515
516 let pks = sks
517 .iter()
518 .map(|sk| PublicKeyShare((G2Projective::generator() * sk.0).to_affine()))
519 .collect();
520
521 (AggregatePublicKey(apk), pks, sks)
522}
523
524fn eval_polynomial(coefficients: &[Scalar], x: &Scalar) -> Scalar {
525 coefficients
526 .iter()
527 .copied()
528 .rev()
529 .reduce(|acc, coefficient| acc * x + coefficient)
530 .expect("We have at least one coefficient")
531}
532
533#[derive(Debug)]
535pub struct Mint {
536 cfg: MintConfig,
537 sec_key: Tiered<SecretKeyShare>,
538 pub_key: BTreeMap<Amount, AggregatePublicKey>,
539}
540#[apply(async_trait_maybe_send!)]
541impl ServerModule for Mint {
542 type Common = MintModuleTypes;
543 type Init = MintInit;
544
545 async fn consensus_proposal(
546 &self,
547 _dbtx: &mut DatabaseTransaction<'_>,
548 ) -> Vec<MintConsensusItem> {
549 Vec::new()
550 }
551
552 async fn process_consensus_item<'a, 'b>(
553 &'a self,
554 _dbtx: &mut DatabaseTransaction<'b>,
555 _consensus_item: MintConsensusItem,
556 _peer_id: PeerId,
557 ) -> anyhow::Result<()> {
558 bail!("Mint does not process consensus items");
559 }
560
561 fn verify_input(&self, input: &MintInput) -> Result<(), MintInputError> {
562 let input = input.ensure_v0_ref()?;
563
564 let amount_key = self
565 .pub_key
566 .get(&input.amount)
567 .ok_or(MintInputError::InvalidAmountTier(input.amount))?;
568
569 if !input.note.verify(*amount_key) {
570 return Err(MintInputError::InvalidSignature);
571 }
572
573 Ok(())
574 }
575
576 async fn process_input<'a, 'b, 'c>(
577 &'a self,
578 dbtx: &mut DatabaseTransaction<'c>,
579 input: &'b MintInput,
580 _in_point: InPoint,
581 ) -> Result<InputMeta, MintInputError> {
582 let input = input.ensure_v0_ref()?;
583
584 debug!(target: LOG_MODULE_MINT, nonce=%(input.note.nonce.fmt_short()), "Marking note as spent");
585
586 if dbtx
587 .insert_entry(&NonceKey(input.note.nonce), &())
588 .await
589 .is_some()
590 {
591 return Err(MintInputError::SpentCoin);
592 }
593
594 dbtx.insert_new_entry(
595 &MintAuditItemKey::Redemption(NonceKey(input.note.nonce)),
596 &input.amount,
597 )
598 .await;
599
600 let next_index = get_recovery_count(dbtx).await;
601 dbtx.insert_new_entry(
602 &RecoveryItemKey(next_index),
603 &RecoveryItem::Input {
604 nonce: input.note.nonce.consensus_hash(),
605 },
606 )
607 .await;
608
609 let amount = input.amount;
610 let fee = self.cfg.consensus.fee_consensus.fee(amount);
611
612 calculate_mint_redeemed_ecash_metrics(dbtx, amount, fee);
613
614 Ok(InputMeta {
615 amount: TransactionItemAmounts {
616 amounts: Amounts::new_bitcoin(amount),
617 fees: Amounts::new_bitcoin(fee),
618 },
619 pub_key: *input.note.spend_key(),
620 })
621 }
622
623 async fn process_output<'a, 'b>(
624 &'a self,
625 dbtx: &mut DatabaseTransaction<'b>,
626 output: &'a MintOutput,
627 out_point: OutPoint,
628 ) -> Result<TransactionItemAmounts, MintOutputError> {
629 let output = output.ensure_v0_ref()?;
630
631 let amount_key = self
632 .sec_key
633 .get(output.amount)
634 .ok_or(MintOutputError::InvalidAmountTier(output.amount))?;
635
636 dbtx.insert_new_entry(
637 &MintOutputOutcomeKey(out_point),
638 &MintOutputOutcome::new_v0(sign_message(output.blind_nonce.0, *amount_key)),
639 )
640 .await;
641
642 dbtx.insert_new_entry(&MintAuditItemKey::Issuance(out_point), &output.amount)
643 .await;
644
645 if dbtx
646 .insert_entry(&BlindNonceKey(output.blind_nonce), &())
647 .await
648 .is_some()
649 {
650 warn!(
652 target: LOG_MODULE_MINT,
653 denomination = %output.amount,
654 bnonce = ?output.blind_nonce,
655 "Blind nonce already used, money was burned!"
656 );
657 }
658
659 let next_index = get_recovery_count(dbtx).await;
660 dbtx.insert_new_entry(
661 &RecoveryItemKey(next_index),
662 &RecoveryItem::Output {
663 amount: output.amount,
664 nonce: output.blind_nonce.0.consensus_hash(),
665 },
666 )
667 .await;
668
669 if dbtx
670 .insert_entry(
671 &RecoveryBlindNonceOutpointKey(output.blind_nonce),
672 &out_point,
673 )
674 .await
675 .is_some()
676 {
677 warn!(
678 target: LOG_MODULE_MINT,
679 bnonce = ?output.blind_nonce,
680 "Recovery blind nonce outpoint overwritten; duplicate blind nonce in outputs"
681 );
682 }
683
684 let amount = output.amount;
685 let fee = self.cfg.consensus.fee_consensus.fee(amount);
686
687 calculate_mint_issued_ecash_metrics(dbtx, amount, fee);
688
689 Ok(TransactionItemAmounts {
690 amounts: Amounts::new_bitcoin(amount),
691 fees: Amounts::new_bitcoin(fee),
692 })
693 }
694
695 async fn output_status(
696 &self,
697 dbtx: &mut DatabaseTransaction<'_>,
698 out_point: OutPoint,
699 ) -> Option<MintOutputOutcome> {
700 dbtx.get_value(&MintOutputOutcomeKey(out_point)).await
701 }
702
703 #[doc(hidden)]
704 async fn verify_output_submission<'a, 'b>(
705 &'a self,
706 dbtx: &mut DatabaseTransaction<'b>,
707 output: &'a MintOutput,
708 _out_point: OutPoint,
709 ) -> Result<(), MintOutputError> {
710 let output = output.ensure_v0_ref()?;
711
712 if dbtx
713 .get_value(&BlindNonceKey(output.blind_nonce))
714 .await
715 .is_some()
716 {
717 return Err(MintOutputError::BlindNonceAlreadyUsed);
718 }
719
720 Ok(())
721 }
722
723 async fn audit(
724 &self,
725 dbtx: &mut DatabaseTransaction<'_>,
726 audit: &mut Audit,
727 module_instance_id: ModuleInstanceId,
728 ) {
729 let mut redemptions = Amount::from_sats(0);
730 let mut issuances = Amount::from_sats(0);
731 let remove_audit_keys = dbtx
732 .find_by_prefix(&MintAuditItemKeyPrefix)
733 .await
734 .map(|(key, amount)| {
735 match key {
736 MintAuditItemKey::Issuance(_) | MintAuditItemKey::IssuanceTotal => {
737 issuances += amount;
738 }
739 MintAuditItemKey::Redemption(_) | MintAuditItemKey::RedemptionTotal => {
740 redemptions += amount;
741 }
742 }
743 key
744 })
745 .collect::<Vec<_>>()
746 .await;
747
748 for key in remove_audit_keys {
749 dbtx.remove_entry(&key).await;
750 }
751
752 dbtx.insert_entry(&MintAuditItemKey::IssuanceTotal, &issuances)
753 .await;
754 dbtx.insert_entry(&MintAuditItemKey::RedemptionTotal, &redemptions)
755 .await;
756
757 audit
758 .add_items(
759 dbtx,
760 module_instance_id,
761 &MintAuditItemKeyPrefix,
762 |k, v| match k {
763 MintAuditItemKey::Issuance(_) | MintAuditItemKey::IssuanceTotal => {
764 -(v.msats as i64)
765 }
766 MintAuditItemKey::Redemption(_) | MintAuditItemKey::RedemptionTotal => {
767 v.msats as i64
768 }
769 },
770 )
771 .await;
772 }
773
774 fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
775 vec![
776 public_api_endpoint! {
777 NOTE_SPENT_ENDPOINT,
778 ApiVersion::new(0, 1),
779 async |_module: &Mint, context, nonce: Nonce| -> bool {
780 let db = context.db();
781 let mut dbtx = db.begin_transaction_nc().await;
782 Ok(dbtx.get_value(&NonceKey(nonce)).await.is_some())
783 }
784 },
785 public_api_endpoint! {
786 BLIND_NONCE_USED_ENDPOINT,
787 ApiVersion::new(0, 1),
788 async |_module: &Mint, context, blind_nonce: BlindNonce| -> bool {
789 let db = context.db();
790 let mut dbtx = db.begin_transaction_nc().await;
791 Ok(dbtx.get_value(&BlindNonceKey(blind_nonce)).await.is_some())
792 }
793 },
794 public_api_endpoint! {
795 RECOVERY_COUNT_ENDPOINT,
796 ApiVersion::new(0, 1),
797 async |_module: &Mint, context, _params: ()| -> u64 {
798 let db = context.db();
799 let mut dbtx = db.begin_transaction_nc().await;
800 Ok(get_recovery_count(&mut dbtx).await)
801 }
802 },
803 public_api_endpoint! {
804 RECOVERY_SLICE_ENDPOINT,
805 ApiVersion::new(0, 1),
806 async |_module: &Mint, context, range: (u64, u64)| -> SerdeModuleEncodingBase64<Vec<RecoveryItem>> {
807 let db = context.db();
808 let mut dbtx = db.begin_transaction_nc().await;
809 Ok((&get_recovery_slice(&mut dbtx, range).await).into())
810 }
811 },
812 public_api_endpoint! {
813 RECOVERY_SLICE_HASH_ENDPOINT,
814 ApiVersion::new(0, 1),
815 async |_module: &Mint, context, range: (u64, u64)| -> sha256::Hash {
816 let db = context.db();
817 let mut dbtx = db.begin_transaction_nc().await;
818 Ok(get_recovery_slice(&mut dbtx, range).await.consensus_hash())
819 }
820 },
821 public_api_endpoint! {
822 RECOVERY_BLIND_NONCE_OUTPOINTS_ENDPOINT,
823 ApiVersion::new(0, 1),
824 async |_module: &Mint, context, blind_nonces: Vec<BlindNonce>| -> Vec<OutPoint> {
825 let db = context.db();
826 let mut dbtx = db.begin_transaction_nc().await;
827 let mut result = Vec::with_capacity(blind_nonces.len());
828 for bn in blind_nonces {
829 let out_point = dbtx
830 .get_value(&RecoveryBlindNonceOutpointKey(bn))
831 .await
832 .ok_or_else(|| ApiError::bad_request("blind nonce not found".to_string()))?;
833 result.push(out_point);
834 }
835 Ok(result)
836 }
837 },
838 ]
839 }
840}
841
842fn calculate_mint_issued_ecash_metrics(
843 dbtx: &mut DatabaseTransaction<'_>,
844 amount: Amount,
845 fee: Amount,
846) {
847 dbtx.on_commit(move || {
848 MINT_INOUT_SATS
849 .with_label_values(&["outgoing"])
850 .observe(amount.sats_f64());
851 MINT_INOUT_FEES_SATS
852 .with_label_values(&["outgoing"])
853 .observe(fee.sats_f64());
854 MINT_ISSUED_ECASH_SATS.observe(amount.sats_f64());
855 MINT_ISSUED_ECASH_FEES_SATS.observe(fee.sats_f64());
856 });
857}
858
859fn calculate_mint_redeemed_ecash_metrics(
860 dbtx: &mut DatabaseTransaction<'_>,
861 amount: Amount,
862 fee: Amount,
863) {
864 dbtx.on_commit(move || {
865 MINT_INOUT_SATS
866 .with_label_values(&["incoming"])
867 .observe(amount.sats_f64());
868 MINT_INOUT_FEES_SATS
869 .with_label_values(&["incoming"])
870 .observe(fee.sats_f64());
871 MINT_REDEEMED_ECASH_SATS.observe(amount.sats_f64());
872 MINT_REDEEMED_ECASH_FEES_SATS.observe(fee.sats_f64());
873 });
874}
875
876async fn get_recovery_count(dbtx: &mut DatabaseTransaction<'_>) -> u64 {
877 dbtx.find_by_prefix_sorted_descending(&RecoveryItemKeyPrefix)
878 .await
879 .next()
880 .await
881 .map_or(0, |entry| entry.0.0 + 1)
882}
883
884async fn get_recovery_slice(
885 dbtx: &mut DatabaseTransaction<'_>,
886 range: (u64, u64),
887) -> Vec<RecoveryItem> {
888 dbtx.find_by_range(RecoveryItemKey(range.0)..RecoveryItemKey(range.1))
889 .await
890 .map(|entry| entry.1)
891 .collect()
892 .await
893}
894
895impl Mint {
896 pub fn new(cfg: MintConfig) -> Mint {
904 assert!(cfg.private.tbs_sks.tiers().count() > 0);
905
906 assert!(
909 cfg.consensus
910 .peer_tbs_pks
911 .values()
912 .all(|pk| pk.structural_eq(&cfg.private.tbs_sks))
913 );
914
915 let ref_pub_key = cfg
916 .private
917 .tbs_sks
918 .iter()
919 .map(|(amount, sk)| (amount, derive_pk_share(sk)))
920 .collect();
921
922 let our_id = cfg
925 .consensus .peer_tbs_pks
927 .iter()
928 .find_map(|(&id, pk)| if *pk == ref_pub_key { Some(id) } else { None })
929 .expect("Own key not found among pub keys.");
930
931 assert_eq!(
932 cfg.consensus.peer_tbs_pks[&our_id],
933 cfg.private
934 .tbs_sks
935 .iter()
936 .map(|(amount, sk)| (amount, derive_pk_share(sk)))
937 .collect()
938 );
939
940 let aggregate_pub_keys = TieredMulti::new_aggregate_from_tiered_iter(
944 cfg.consensus.peer_tbs_pks.values().cloned(),
945 )
946 .into_iter()
947 .map(|(amt, keys)| {
948 let keys = (0_u64..)
949 .zip(keys)
950 .take(cfg.consensus.peer_tbs_pks.to_num_peers().threshold())
951 .collect();
952
953 (amt, aggregate_public_key_shares(&keys))
954 })
955 .collect();
956
957 Mint {
958 cfg: cfg.clone(),
959 sec_key: cfg.private.tbs_sks,
960 pub_key: aggregate_pub_keys,
961 }
962 }
963
964 pub fn pub_key(&self) -> BTreeMap<Amount, AggregatePublicKey> {
965 self.pub_key.clone()
966 }
967}
968
969#[cfg(test)]
970mod test;