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