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, HashMap};
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, 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, CORE_CONSENSUS_VERSION, CoreConsensusVersion,
28 InputMeta, ModuleConsensusVersion, ModuleInit, SerdeModuleEncodingBase64,
29 SupportedModuleApiVersions, TransactionItemAmounts, 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 supported_api_versions(&self) -> SupportedModuleApiVersions {
182 SupportedModuleApiVersions::from_raw(
183 (CORE_CONSENSUS_VERSION.major, CORE_CONSENSUS_VERSION.minor),
184 (
185 MODULE_CONSENSUS_VERSION.major,
186 MODULE_CONSENSUS_VERSION.minor,
187 ),
188 &[(0, 1)],
189 )
190 }
191
192 fn is_enabled_by_default(&self) -> bool {
193 is_env_var_set_opt(FM_ENABLE_MODULE_MINT_ENV).unwrap_or(true)
194 }
195
196 fn get_documented_env_vars(&self) -> Vec<EnvVarDoc> {
197 vec![EnvVarDoc {
198 name: FM_ENABLE_MODULE_MINT_ENV,
199 description: "Set to 0/false to disable the mint (e-cash) module. Enabled by default.",
200 }]
201 }
202
203 async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
204 Ok(Mint::new(args.cfg().to_typed()?))
205 }
206
207 fn trusted_dealer_gen(
208 &self,
209 peers: &[PeerId],
210 args: &ConfigGenModuleArgs,
211 ) -> BTreeMap<PeerId, ServerModuleConfig> {
212 let denominations = gen_denominations();
213
214 let tbs_keys = denominations
215 .iter()
216 .map(|&amount| {
217 let (tbs_pk, tbs_pks, tbs_sks) =
218 dealer_keygen(peers.to_num_peers().threshold(), peers.len());
219 (amount, (tbs_pk, tbs_pks, tbs_sks))
220 })
221 .collect::<HashMap<_, _>>();
222
223 let mint_cfg: BTreeMap<_, MintConfig> = peers
224 .iter()
225 .map(|&peer| {
226 let config = MintConfig {
227 consensus: MintConfigConsensus {
228 peer_tbs_pks: peers
229 .iter()
230 .map(|&key_peer| {
231 let keys = denominations
232 .iter()
233 .map(|amount| {
234 (*amount, tbs_keys[amount].1[key_peer.to_usize()])
235 })
236 .collect();
237 (key_peer, keys)
238 })
239 .collect(),
240 fee_consensus: if args.disable_base_fees {
241 FeeConsensus::zero()
242 } else {
243 FeeConsensus::new(0).expect("Relative fee is within range")
244 },
245 max_notes_per_denomination: DEFAULT_MAX_NOTES_PER_DENOMINATION,
246 },
247 private: MintConfigPrivate {
248 tbs_sks: denominations
249 .iter()
250 .map(|amount| (*amount, tbs_keys[amount].2[peer.to_usize()]))
251 .collect(),
252 },
253 };
254 (peer, config)
255 })
256 .collect();
257
258 mint_cfg
259 .into_iter()
260 .map(|(k, v)| (k, v.to_erased()))
261 .collect()
262 }
263
264 async fn distributed_gen(
265 &self,
266 peers: &(dyn PeerHandleOps + Send + Sync),
267 args: &ConfigGenModuleArgs,
268 ) -> anyhow::Result<ServerModuleConfig> {
269 let denominations = gen_denominations();
270
271 let mut amount_keys = HashMap::new();
272
273 for amount in &denominations {
274 amount_keys.insert(*amount, peers.run_dkg_g2().await?);
275 }
276
277 let server = MintConfig {
278 private: MintConfigPrivate {
279 tbs_sks: amount_keys
280 .iter()
281 .map(|(amount, (_, sks))| (*amount, tbs::SecretKeyShare(*sks)))
282 .collect(),
283 },
284 consensus: MintConfigConsensus {
285 peer_tbs_pks: peers
286 .num_peers()
287 .peer_ids()
288 .map(|peer| {
289 let pks = amount_keys
290 .iter()
291 .map(|(amount, (pks, _))| {
292 (*amount, PublicKeyShare(eval_poly_g2(pks, &peer)))
293 })
294 .collect::<Tiered<_>>();
295
296 (peer, pks)
297 })
298 .collect(),
299 fee_consensus: if args.disable_base_fees {
300 FeeConsensus::zero()
301 } else {
302 FeeConsensus::new(0).expect("Relative fee is within range")
303 },
304 max_notes_per_denomination: DEFAULT_MAX_NOTES_PER_DENOMINATION,
305 },
306 };
307
308 Ok(server.to_erased())
309 }
310
311 fn validate_config(&self, identity: &PeerId, config: ServerModuleConfig) -> anyhow::Result<()> {
312 let config = config.to_typed::<MintConfig>()?;
313 let sks: BTreeMap<Amount, PublicKeyShare> = config
314 .private
315 .tbs_sks
316 .iter()
317 .map(|(amount, sk)| (amount, derive_pk_share(sk)))
318 .collect();
319 let pks: BTreeMap<Amount, PublicKeyShare> = config
320 .consensus
321 .peer_tbs_pks
322 .get(identity)
323 .unwrap()
324 .as_map()
325 .iter()
326 .map(|(k, v)| (*k, *v))
327 .collect();
328 if sks != pks {
329 bail!("Mint private key doesn't match pubkey share");
330 }
331 if !sks.keys().contains(&Amount::from_msats(1)) {
332 bail!("No msat 1 denomination");
333 }
334
335 Ok(())
336 }
337
338 fn get_client_config(
339 &self,
340 config: &ServerModuleConsensusConfig,
341 ) -> anyhow::Result<MintClientConfig> {
342 let config = MintConfigConsensus::from_erased(config)?;
343 let tbs_pks =
347 TieredMulti::new_aggregate_from_tiered_iter(config.peer_tbs_pks.values().cloned())
348 .into_iter()
349 .map(|(amt, keys)| {
350 let keys = (0_u64..)
351 .zip(keys)
352 .take(config.peer_tbs_pks.to_num_peers().threshold())
353 .collect();
354
355 (amt, aggregate_public_key_shares(&keys))
356 })
357 .collect();
358
359 Ok(MintClientConfig {
360 tbs_pks,
361 fee_consensus: config.fee_consensus.clone(),
362 peer_tbs_pks: config.peer_tbs_pks.clone(),
363 max_notes_per_denomination: config.max_notes_per_denomination,
364 })
365 }
366
367 fn get_database_migrations(
368 &self,
369 ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Mint>> {
370 let mut migrations: BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<_>> =
371 BTreeMap::new();
372 migrations.insert(
373 DatabaseVersion(0),
374 Box::new(|ctx| migrate_db_v0(ctx).boxed()),
375 );
376 migrations.insert(
377 DatabaseVersion(1),
378 Box::new(|ctx| migrate_db_v1(ctx).boxed()),
379 );
380 migrations.insert(
381 DatabaseVersion(2),
382 Box::new(|ctx| migrate_db_v2(ctx).boxed()),
383 );
384 migrations
385 }
386
387 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
388 Some(DbKeyPrefix::iter().map(|p| p as u8).collect())
389 }
390}
391
392async fn migrate_db_v0(
393 mut migration_context: ServerModuleDbMigrationFnContext<'_, Mint>,
394) -> anyhow::Result<()> {
395 let blind_nonces = migration_context
396 .get_typed_module_history_stream()
397 .await
398 .filter_map(|history_item: ModuleHistoryItem<_>| async move {
399 match history_item {
400 ModuleHistoryItem::Output(mint_output, _) => Some(
401 mint_output
402 .ensure_v0_ref()
403 .expect("This migration only runs while we only have v0 outputs")
404 .blind_nonce,
405 ),
406 _ => {
407 None
409 }
410 }
411 })
412 .collect::<Vec<_>>()
413 .await;
414
415 info!(target: LOG_MODULE_MINT, "Found {} blind nonces in history", blind_nonces.len());
416
417 let mut double_issuances = 0usize;
418 for blind_nonce in blind_nonces {
419 if migration_context
420 .dbtx()
421 .insert_entry(&BlindNonceKey(blind_nonce), &())
422 .await
423 .is_some()
424 {
425 double_issuances += 1;
426 debug!(
427 target: LOG_MODULE_MINT,
428 ?blind_nonce,
429 "Blind nonce already used, money was burned!"
430 );
431 }
432 }
433
434 if double_issuances > 0 {
435 warn!(target: LOG_MODULE_MINT, "{double_issuances} blind nonces were reused, money was burned by faulty user clients!");
436 }
437
438 Ok(())
439}
440
441async fn migrate_db_v1(
443 mut migration_context: ServerModuleDbMigrationFnContext<'_, Mint>,
444) -> anyhow::Result<()> {
445 migration_context
446 .dbtx()
447 .raw_remove_by_prefix(&[0x15])
448 .await
449 .expect("DB error");
450 Ok(())
451}
452
453async fn migrate_db_v2(mut ctx: ServerModuleDbMigrationFnContext<'_, Mint>) -> anyhow::Result<()> {
455 let mut recovery_items = Vec::new();
456 let mut blind_nonce_outpoints = Vec::new();
457 let mut stream = ctx.get_typed_module_history_stream().await;
458
459 while let Some(history_item) = stream.next().await {
460 match history_item {
461 ModuleHistoryItem::Output(mint_output, out_point) => {
462 let output = mint_output
463 .ensure_v0_ref()
464 .expect("This migration only runs while we only have v0 outputs");
465
466 recovery_items.push(RecoveryItem::Output {
467 amount: output.amount,
468 nonce: output.blind_nonce.0.consensus_hash(),
469 });
470
471 blind_nonce_outpoints.push((output.blind_nonce, out_point));
472 }
473 ModuleHistoryItem::Input(mint_input) => {
474 let input = mint_input
475 .ensure_v0_ref()
476 .expect("This migration only runs while we only have v0 inputs");
477
478 recovery_items.push(RecoveryItem::Input {
479 nonce: input.note.nonce.consensus_hash(),
480 });
481 }
482 ModuleHistoryItem::ConsensusItem(_) => {}
483 }
484 }
485
486 drop(stream);
487
488 for (index, item) in recovery_items.into_iter().enumerate() {
489 ctx.dbtx()
490 .insert_new_entry(&RecoveryItemKey(index as u64), &item)
491 .await;
492 }
493
494 for (blind_nonce, out_point) in blind_nonce_outpoints {
495 if ctx
496 .dbtx()
497 .insert_entry(&RecoveryBlindNonceOutpointKey(blind_nonce), &out_point)
498 .await
499 .is_some()
500 {
501 warn!(
502 target: LOG_MODULE_MINT,
503 bnonce = ?blind_nonce,
504 "Recovery blind nonce outpoint overwritten; duplicate blind nonce in outputs"
505 );
506 }
507 }
508
509 Ok(())
510}
511
512fn dealer_keygen(
513 threshold: usize,
514 keys: usize,
515) -> (AggregatePublicKey, Vec<PublicKeyShare>, Vec<SecretKeyShare>) {
516 let mut rng = OsRng; let poly: Vec<Scalar> = (0..threshold).map(|_| Scalar::random(&mut rng)).collect();
518
519 let apk = (G2Projective::generator() * eval_polynomial(&poly, &Scalar::zero())).to_affine();
520
521 let sks: Vec<SecretKeyShare> = (0..keys)
522 .map(|idx| SecretKeyShare(eval_polynomial(&poly, &Scalar::from(idx as u64 + 1))))
523 .collect();
524
525 let pks = sks
526 .iter()
527 .map(|sk| PublicKeyShare((G2Projective::generator() * sk.0).to_affine()))
528 .collect();
529
530 (AggregatePublicKey(apk), pks, sks)
531}
532
533fn eval_polynomial(coefficients: &[Scalar], x: &Scalar) -> Scalar {
534 coefficients
535 .iter()
536 .copied()
537 .rev()
538 .reduce(|acc, coefficient| acc * x + coefficient)
539 .expect("We have at least one coefficient")
540}
541
542#[derive(Debug)]
544pub struct Mint {
545 cfg: MintConfig,
546 sec_key: Tiered<SecretKeyShare>,
547 pub_key: HashMap<Amount, AggregatePublicKey>,
548}
549#[apply(async_trait_maybe_send!)]
550impl ServerModule for Mint {
551 type Common = MintModuleTypes;
552 type Init = MintInit;
553
554 async fn consensus_proposal(
555 &self,
556 _dbtx: &mut DatabaseTransaction<'_>,
557 ) -> Vec<MintConsensusItem> {
558 Vec::new()
559 }
560
561 async fn process_consensus_item<'a, 'b>(
562 &'a self,
563 _dbtx: &mut DatabaseTransaction<'b>,
564 _consensus_item: MintConsensusItem,
565 _peer_id: PeerId,
566 ) -> anyhow::Result<()> {
567 bail!("Mint does not process consensus items");
568 }
569
570 fn verify_input(&self, input: &MintInput) -> Result<(), MintInputError> {
571 let input = input.ensure_v0_ref()?;
572
573 let amount_key = self
574 .pub_key
575 .get(&input.amount)
576 .ok_or(MintInputError::InvalidAmountTier(input.amount))?;
577
578 if !input.note.verify(*amount_key) {
579 return Err(MintInputError::InvalidSignature);
580 }
581
582 Ok(())
583 }
584
585 async fn process_input<'a, 'b, 'c>(
586 &'a self,
587 dbtx: &mut DatabaseTransaction<'c>,
588 input: &'b MintInput,
589 _in_point: InPoint,
590 ) -> Result<InputMeta, MintInputError> {
591 let input = input.ensure_v0_ref()?;
592
593 debug!(target: LOG_MODULE_MINT, nonce=%(input.note.nonce.fmt_short()), "Marking note as spent");
594
595 if dbtx
596 .insert_entry(&NonceKey(input.note.nonce), &())
597 .await
598 .is_some()
599 {
600 return Err(MintInputError::SpentCoin);
601 }
602
603 dbtx.insert_new_entry(
604 &MintAuditItemKey::Redemption(NonceKey(input.note.nonce)),
605 &input.amount,
606 )
607 .await;
608
609 let next_index = get_recovery_count(dbtx).await;
610 dbtx.insert_new_entry(
611 &RecoveryItemKey(next_index),
612 &RecoveryItem::Input {
613 nonce: input.note.nonce.consensus_hash(),
614 },
615 )
616 .await;
617
618 let amount = input.amount;
619 let fee = self.cfg.consensus.fee_consensus.fee(amount);
620
621 calculate_mint_redeemed_ecash_metrics(dbtx, amount, fee);
622
623 Ok(InputMeta {
624 amount: TransactionItemAmounts {
625 amounts: Amounts::new_bitcoin(amount),
626 fees: Amounts::new_bitcoin(fee),
627 },
628 pub_key: *input.note.spend_key(),
629 })
630 }
631
632 async fn process_output<'a, 'b>(
633 &'a self,
634 dbtx: &mut DatabaseTransaction<'b>,
635 output: &'a MintOutput,
636 out_point: OutPoint,
637 ) -> Result<TransactionItemAmounts, MintOutputError> {
638 let output = output.ensure_v0_ref()?;
639
640 let amount_key = self
641 .sec_key
642 .get(output.amount)
643 .ok_or(MintOutputError::InvalidAmountTier(output.amount))?;
644
645 dbtx.insert_new_entry(
646 &MintOutputOutcomeKey(out_point),
647 &MintOutputOutcome::new_v0(sign_message(output.blind_nonce.0, *amount_key)),
648 )
649 .await;
650
651 dbtx.insert_new_entry(&MintAuditItemKey::Issuance(out_point), &output.amount)
652 .await;
653
654 if dbtx
655 .insert_entry(&BlindNonceKey(output.blind_nonce), &())
656 .await
657 .is_some()
658 {
659 warn!(
661 target: LOG_MODULE_MINT,
662 denomination = %output.amount,
663 bnonce = ?output.blind_nonce,
664 "Blind nonce already used, money was burned!"
665 );
666 }
667
668 let next_index = get_recovery_count(dbtx).await;
669 dbtx.insert_new_entry(
670 &RecoveryItemKey(next_index),
671 &RecoveryItem::Output {
672 amount: output.amount,
673 nonce: output.blind_nonce.0.consensus_hash(),
674 },
675 )
676 .await;
677
678 if dbtx
679 .insert_entry(
680 &RecoveryBlindNonceOutpointKey(output.blind_nonce),
681 &out_point,
682 )
683 .await
684 .is_some()
685 {
686 warn!(
687 target: LOG_MODULE_MINT,
688 bnonce = ?output.blind_nonce,
689 "Recovery blind nonce outpoint overwritten; duplicate blind nonce in outputs"
690 );
691 }
692
693 let amount = output.amount;
694 let fee = self.cfg.consensus.fee_consensus.fee(amount);
695
696 calculate_mint_issued_ecash_metrics(dbtx, amount, fee);
697
698 Ok(TransactionItemAmounts {
699 amounts: Amounts::new_bitcoin(amount),
700 fees: Amounts::new_bitcoin(fee),
701 })
702 }
703
704 async fn output_status(
705 &self,
706 dbtx: &mut DatabaseTransaction<'_>,
707 out_point: OutPoint,
708 ) -> Option<MintOutputOutcome> {
709 dbtx.get_value(&MintOutputOutcomeKey(out_point)).await
710 }
711
712 #[doc(hidden)]
713 async fn verify_output_submission<'a, 'b>(
714 &'a self,
715 dbtx: &mut DatabaseTransaction<'b>,
716 output: &'a MintOutput,
717 _out_point: OutPoint,
718 ) -> Result<(), MintOutputError> {
719 let output = output.ensure_v0_ref()?;
720
721 if dbtx
722 .get_value(&BlindNonceKey(output.blind_nonce))
723 .await
724 .is_some()
725 {
726 return Err(MintOutputError::BlindNonceAlreadyUsed);
727 }
728
729 Ok(())
730 }
731
732 async fn audit(
733 &self,
734 dbtx: &mut DatabaseTransaction<'_>,
735 audit: &mut Audit,
736 module_instance_id: ModuleInstanceId,
737 ) {
738 let mut redemptions = Amount::from_sats(0);
739 let mut issuances = Amount::from_sats(0);
740 let remove_audit_keys = dbtx
741 .find_by_prefix(&MintAuditItemKeyPrefix)
742 .await
743 .map(|(key, amount)| {
744 match key {
745 MintAuditItemKey::Issuance(_) | MintAuditItemKey::IssuanceTotal => {
746 issuances += amount;
747 }
748 MintAuditItemKey::Redemption(_) | MintAuditItemKey::RedemptionTotal => {
749 redemptions += amount;
750 }
751 }
752 key
753 })
754 .collect::<Vec<_>>()
755 .await;
756
757 for key in remove_audit_keys {
758 dbtx.remove_entry(&key).await;
759 }
760
761 dbtx.insert_entry(&MintAuditItemKey::IssuanceTotal, &issuances)
762 .await;
763 dbtx.insert_entry(&MintAuditItemKey::RedemptionTotal, &redemptions)
764 .await;
765
766 audit
767 .add_items(
768 dbtx,
769 module_instance_id,
770 &MintAuditItemKeyPrefix,
771 |k, v| match k {
772 MintAuditItemKey::Issuance(_) | MintAuditItemKey::IssuanceTotal => {
773 -(v.msats as i64)
774 }
775 MintAuditItemKey::Redemption(_) | MintAuditItemKey::RedemptionTotal => {
776 v.msats as i64
777 }
778 },
779 )
780 .await;
781 }
782
783 fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
784 vec![
785 api_endpoint! {
786 NOTE_SPENT_ENDPOINT,
787 ApiVersion::new(0, 1),
788 async |_module: &Mint, context, nonce: Nonce| -> bool {
789 let db = context.db();
790 let mut dbtx = db.begin_transaction_nc().await;
791 Ok(dbtx.get_value(&NonceKey(nonce)).await.is_some())
792 }
793 },
794 api_endpoint! {
795 BLIND_NONCE_USED_ENDPOINT,
796 ApiVersion::new(0, 1),
797 async |_module: &Mint, context, blind_nonce: BlindNonce| -> bool {
798 let db = context.db();
799 let mut dbtx = db.begin_transaction_nc().await;
800 Ok(dbtx.get_value(&BlindNonceKey(blind_nonce)).await.is_some())
801 }
802 },
803 api_endpoint! {
804 RECOVERY_COUNT_ENDPOINT,
805 ApiVersion::new(0, 1),
806 async |_module: &Mint, context, _params: ()| -> u64 {
807 let db = context.db();
808 let mut dbtx = db.begin_transaction_nc().await;
809 Ok(get_recovery_count(&mut dbtx).await)
810 }
811 },
812 api_endpoint! {
813 RECOVERY_SLICE_ENDPOINT,
814 ApiVersion::new(0, 1),
815 async |_module: &Mint, context, range: (u64, u64)| -> SerdeModuleEncodingBase64<Vec<RecoveryItem>> {
816 let db = context.db();
817 let mut dbtx = db.begin_transaction_nc().await;
818 Ok((&get_recovery_slice(&mut dbtx, range).await).into())
819 }
820 },
821 api_endpoint! {
822 RECOVERY_SLICE_HASH_ENDPOINT,
823 ApiVersion::new(0, 1),
824 async |_module: &Mint, context, range: (u64, u64)| -> sha256::Hash {
825 let db = context.db();
826 let mut dbtx = db.begin_transaction_nc().await;
827 Ok(get_recovery_slice(&mut dbtx, range).await.consensus_hash())
828 }
829 },
830 api_endpoint! {
831 RECOVERY_BLIND_NONCE_OUTPOINTS_ENDPOINT,
832 ApiVersion::new(0, 1),
833 async |_module: &Mint, context, blind_nonces: Vec<BlindNonce>| -> Vec<OutPoint> {
834 let db = context.db();
835 let mut dbtx = db.begin_transaction_nc().await;
836 let mut result = Vec::with_capacity(blind_nonces.len());
837 for bn in blind_nonces {
838 let out_point = dbtx
839 .get_value(&RecoveryBlindNonceOutpointKey(bn))
840 .await
841 .ok_or_else(|| ApiError::bad_request("blind nonce not found".to_string()))?;
842 result.push(out_point);
843 }
844 Ok(result)
845 }
846 },
847 ]
848 }
849}
850
851fn calculate_mint_issued_ecash_metrics(
852 dbtx: &mut DatabaseTransaction<'_>,
853 amount: Amount,
854 fee: Amount,
855) {
856 dbtx.on_commit(move || {
857 MINT_INOUT_SATS
858 .with_label_values(&["outgoing"])
859 .observe(amount.sats_f64());
860 MINT_INOUT_FEES_SATS
861 .with_label_values(&["outgoing"])
862 .observe(fee.sats_f64());
863 MINT_ISSUED_ECASH_SATS.observe(amount.sats_f64());
864 MINT_ISSUED_ECASH_FEES_SATS.observe(fee.sats_f64());
865 });
866}
867
868fn calculate_mint_redeemed_ecash_metrics(
869 dbtx: &mut DatabaseTransaction<'_>,
870 amount: Amount,
871 fee: Amount,
872) {
873 dbtx.on_commit(move || {
874 MINT_INOUT_SATS
875 .with_label_values(&["incoming"])
876 .observe(amount.sats_f64());
877 MINT_INOUT_FEES_SATS
878 .with_label_values(&["incoming"])
879 .observe(fee.sats_f64());
880 MINT_REDEEMED_ECASH_SATS.observe(amount.sats_f64());
881 MINT_REDEEMED_ECASH_FEES_SATS.observe(fee.sats_f64());
882 });
883}
884
885async fn get_recovery_count(dbtx: &mut DatabaseTransaction<'_>) -> u64 {
886 dbtx.find_by_prefix_sorted_descending(&RecoveryItemKeyPrefix)
887 .await
888 .next()
889 .await
890 .map_or(0, |entry| entry.0.0 + 1)
891}
892
893async fn get_recovery_slice(
894 dbtx: &mut DatabaseTransaction<'_>,
895 range: (u64, u64),
896) -> Vec<RecoveryItem> {
897 dbtx.find_by_range(RecoveryItemKey(range.0)..RecoveryItemKey(range.1))
898 .await
899 .map(|entry| entry.1)
900 .collect()
901 .await
902}
903
904impl Mint {
905 pub fn new(cfg: MintConfig) -> Mint {
913 assert!(cfg.private.tbs_sks.tiers().count() > 0);
914
915 assert!(
918 cfg.consensus
919 .peer_tbs_pks
920 .values()
921 .all(|pk| pk.structural_eq(&cfg.private.tbs_sks))
922 );
923
924 let ref_pub_key = cfg
925 .private
926 .tbs_sks
927 .iter()
928 .map(|(amount, sk)| (amount, derive_pk_share(sk)))
929 .collect();
930
931 let our_id = cfg
934 .consensus .peer_tbs_pks
936 .iter()
937 .find_map(|(&id, pk)| if *pk == ref_pub_key { Some(id) } else { None })
938 .expect("Own key not found among pub keys.");
939
940 assert_eq!(
941 cfg.consensus.peer_tbs_pks[&our_id],
942 cfg.private
943 .tbs_sks
944 .iter()
945 .map(|(amount, sk)| (amount, derive_pk_share(sk)))
946 .collect()
947 );
948
949 let aggregate_pub_keys = TieredMulti::new_aggregate_from_tiered_iter(
953 cfg.consensus.peer_tbs_pks.values().cloned(),
954 )
955 .into_iter()
956 .map(|(amt, keys)| {
957 let keys = (0_u64..)
958 .zip(keys)
959 .take(cfg.consensus.peer_tbs_pks.to_num_peers().threshold())
960 .collect();
961
962 (amt, aggregate_public_key_shares(&keys))
963 })
964 .collect();
965
966 Mint {
967 cfg: cfg.clone(),
968 sec_key: cfg.private.tbs_sks,
969 pub_key: aggregate_pub_keys,
970 }
971 }
972
973 pub fn pub_key(&self) -> HashMap<Amount, AggregatePublicKey> {
974 self.pub_key.clone()
975 }
976}
977
978#[cfg(test)]
979mod test;