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