Skip to main content

fedimint_mint_server/
lib.rs

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
159/// Default denomination base for ecash notes (powers of 2)
160const DEFAULT_DENOMINATION_BASE: u16 = 2;
161
162/// Maximum denomination size (1,000,000 coins)
163const MAX_DENOMINATION_SIZE: Amount = Amount::from_bitcoins(1_000_000);
164
165/// Generate the denomination tiers based on the base
166fn 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        // TODO: the aggregate pks should become part of the MintConfigConsensus as they
333        // can be obtained by evaluating the polynomial returned by the DKG at
334        // zero
335        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                    // We only care about e-cash issuances for this migration
397                    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
430// Remove now unused ECash backups from DB. Backup functionality moved to core.
431async 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
442// Backfill RecoveryItem and RecoveryBlindNonceOutpoint from module history
443async 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; // FIXME: pass rng
508    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/// Federated mint member mint
534#[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            // TODO: make a consensus rule against this
651            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    /// Constructs a new mint
897    ///
898    /// # Panics
899    /// * If there are no amount tiers
900    /// * If the amount tiers for secret and public keys are inconsistent
901    /// * If the pub key belonging to the secret key share is not in the pub key
902    ///   list.
903    pub fn new(cfg: MintConfig) -> Mint {
904        assert!(cfg.private.tbs_sks.tiers().count() > 0);
905
906        // The amount tiers are implicitly provided by the key sets, make sure they are
907        // internally consistent.
908        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        // Find our key index and make sure we know the private key for all our public
923        // key shares
924        let our_id = cfg
925            .consensus // FIXME: make sure we use id instead of idx everywhere
926            .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        // TODO: the aggregate pks should become part of the MintConfigConsensus as they
941        // can be obtained by evaluating the polynomial returned by the DKG at
942        // zero
943        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;