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, 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    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) -> anyhow::Result<()> {
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) -> anyhow::Result<()> {
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(mut ctx: ServerModuleDbMigrationFnContext<'_, Mint>) -> anyhow::Result<()> {
444    let mut recovery_items = Vec::new();
445    let mut blind_nonce_outpoints = Vec::new();
446    let mut stream = ctx.get_typed_module_history_stream().await;
447
448    while let Some(history_item) = stream.next().await {
449        match history_item {
450            ModuleHistoryItem::Output(mint_output, out_point) => {
451                let output = mint_output
452                    .ensure_v0_ref()
453                    .expect("This migration only runs while we only have v0 outputs");
454
455                recovery_items.push(RecoveryItem::Output {
456                    amount: output.amount,
457                    nonce: output.blind_nonce.0.consensus_hash(),
458                });
459
460                blind_nonce_outpoints.push((output.blind_nonce, out_point));
461            }
462            ModuleHistoryItem::Input(mint_input) => {
463                let input = mint_input
464                    .ensure_v0_ref()
465                    .expect("This migration only runs while we only have v0 inputs");
466
467                recovery_items.push(RecoveryItem::Input {
468                    nonce: input.note.nonce.consensus_hash(),
469                });
470            }
471            ModuleHistoryItem::ConsensusItem(_) => {}
472        }
473    }
474
475    drop(stream);
476
477    for (index, item) in recovery_items.into_iter().enumerate() {
478        ctx.dbtx()
479            .insert_new_entry(&RecoveryItemKey(index as u64), &item)
480            .await;
481    }
482
483    for (blind_nonce, out_point) in blind_nonce_outpoints {
484        if ctx
485            .dbtx()
486            .insert_entry(&RecoveryBlindNonceOutpointKey(blind_nonce), &out_point)
487            .await
488            .is_some()
489        {
490            warn!(
491                target: LOG_MODULE_MINT,
492                bnonce = ?blind_nonce,
493                "Recovery blind nonce outpoint overwritten; duplicate blind nonce in outputs"
494            );
495        }
496    }
497
498    Ok(())
499}
500
501fn dealer_keygen(
502    threshold: usize,
503    keys: usize,
504) -> (AggregatePublicKey, Vec<PublicKeyShare>, Vec<SecretKeyShare>) {
505    let mut rng = OsRng; // FIXME: pass rng
506    let poly: Vec<Scalar> = (0..threshold).map(|_| Scalar::random(&mut rng)).collect();
507
508    let apk = (G2Projective::generator() * eval_polynomial(&poly, &Scalar::zero())).to_affine();
509
510    let sks: Vec<SecretKeyShare> = (0..keys)
511        .map(|idx| SecretKeyShare(eval_polynomial(&poly, &Scalar::from(idx as u64 + 1))))
512        .collect();
513
514    let pks = sks
515        .iter()
516        .map(|sk| PublicKeyShare((G2Projective::generator() * sk.0).to_affine()))
517        .collect();
518
519    (AggregatePublicKey(apk), pks, sks)
520}
521
522fn eval_polynomial(coefficients: &[Scalar], x: &Scalar) -> Scalar {
523    coefficients
524        .iter()
525        .copied()
526        .rev()
527        .reduce(|acc, coefficient| acc * x + coefficient)
528        .expect("We have at least one coefficient")
529}
530
531/// Federated mint member mint
532#[derive(Debug)]
533pub struct Mint {
534    cfg: MintConfig,
535    sec_key: Tiered<SecretKeyShare>,
536    pub_key: BTreeMap<Amount, AggregatePublicKey>,
537}
538#[apply(async_trait_maybe_send!)]
539impl ServerModule for Mint {
540    type Common = MintModuleTypes;
541    type Init = MintInit;
542
543    async fn consensus_proposal(
544        &self,
545        _dbtx: &mut DatabaseTransaction<'_>,
546    ) -> Vec<MintConsensusItem> {
547        Vec::new()
548    }
549
550    async fn process_consensus_item<'a, 'b>(
551        &'a self,
552        _dbtx: &mut DatabaseTransaction<'b>,
553        _consensus_item: MintConsensusItem,
554        _peer_id: PeerId,
555    ) -> anyhow::Result<()> {
556        bail!("Mint does not process consensus items");
557    }
558
559    fn verify_input(&self, input: &MintInput) -> Result<(), MintInputError> {
560        let input = input.ensure_v0_ref()?;
561
562        let amount_key = self
563            .pub_key
564            .get(&input.amount)
565            .ok_or(MintInputError::InvalidAmountTier(input.amount))?;
566
567        if !input.note.verify(*amount_key) {
568            return Err(MintInputError::InvalidSignature);
569        }
570
571        Ok(())
572    }
573
574    async fn process_input<'a, 'b, 'c>(
575        &'a self,
576        dbtx: &mut DatabaseTransaction<'c>,
577        input: &'b MintInput,
578        _in_point: InPoint,
579    ) -> Result<InputMeta, MintInputError> {
580        let input = input.ensure_v0_ref()?;
581
582        debug!(target: LOG_MODULE_MINT, nonce=%(input.note.nonce.fmt_short()), "Marking note as spent");
583
584        if dbtx
585            .insert_entry(&NonceKey(input.note.nonce), &())
586            .await
587            .is_some()
588        {
589            return Err(MintInputError::SpentCoin);
590        }
591
592        dbtx.insert_new_entry(
593            &MintAuditItemKey::Redemption(NonceKey(input.note.nonce)),
594            &input.amount,
595        )
596        .await;
597
598        let next_index = get_recovery_count(dbtx).await;
599        dbtx.insert_new_entry(
600            &RecoveryItemKey(next_index),
601            &RecoveryItem::Input {
602                nonce: input.note.nonce.consensus_hash(),
603            },
604        )
605        .await;
606
607        let amount = input.amount;
608        let fee = self.cfg.consensus.fee_consensus.fee(amount);
609
610        calculate_mint_redeemed_ecash_metrics(dbtx, amount, fee);
611
612        Ok(InputMeta {
613            amount: TransactionItemAmounts {
614                amounts: Amounts::new_bitcoin(amount),
615                fees: Amounts::new_bitcoin(fee),
616            },
617            pub_key: *input.note.spend_key(),
618        })
619    }
620
621    async fn process_output<'a, 'b>(
622        &'a self,
623        dbtx: &mut DatabaseTransaction<'b>,
624        output: &'a MintOutput,
625        out_point: OutPoint,
626    ) -> Result<TransactionItemAmounts, MintOutputError> {
627        let output = output.ensure_v0_ref()?;
628
629        let amount_key = self
630            .sec_key
631            .get(output.amount)
632            .ok_or(MintOutputError::InvalidAmountTier(output.amount))?;
633
634        dbtx.insert_new_entry(
635            &MintOutputOutcomeKey(out_point),
636            &MintOutputOutcome::new_v0(sign_message(output.blind_nonce.0, *amount_key)),
637        )
638        .await;
639
640        dbtx.insert_new_entry(&MintAuditItemKey::Issuance(out_point), &output.amount)
641            .await;
642
643        if dbtx
644            .insert_entry(&BlindNonceKey(output.blind_nonce), &())
645            .await
646            .is_some()
647        {
648            // TODO: make a consensus rule against this
649            warn!(
650                target: LOG_MODULE_MINT,
651                denomination = %output.amount,
652                bnonce = ?output.blind_nonce,
653                "Blind nonce already used, money was burned!"
654            );
655        }
656
657        let next_index = get_recovery_count(dbtx).await;
658        dbtx.insert_new_entry(
659            &RecoveryItemKey(next_index),
660            &RecoveryItem::Output {
661                amount: output.amount,
662                nonce: output.blind_nonce.0.consensus_hash(),
663            },
664        )
665        .await;
666
667        if dbtx
668            .insert_entry(
669                &RecoveryBlindNonceOutpointKey(output.blind_nonce),
670                &out_point,
671            )
672            .await
673            .is_some()
674        {
675            warn!(
676                target: LOG_MODULE_MINT,
677                bnonce = ?output.blind_nonce,
678                "Recovery blind nonce outpoint overwritten; duplicate blind nonce in outputs"
679            );
680        }
681
682        let amount = output.amount;
683        let fee = self.cfg.consensus.fee_consensus.fee(amount);
684
685        calculate_mint_issued_ecash_metrics(dbtx, amount, fee);
686
687        Ok(TransactionItemAmounts {
688            amounts: Amounts::new_bitcoin(amount),
689            fees: Amounts::new_bitcoin(fee),
690        })
691    }
692
693    async fn output_status(
694        &self,
695        dbtx: &mut DatabaseTransaction<'_>,
696        out_point: OutPoint,
697    ) -> Option<MintOutputOutcome> {
698        dbtx.get_value(&MintOutputOutcomeKey(out_point)).await
699    }
700
701    #[doc(hidden)]
702    async fn verify_output_submission<'a, 'b>(
703        &'a self,
704        dbtx: &mut DatabaseTransaction<'b>,
705        output: &'a MintOutput,
706        _out_point: OutPoint,
707    ) -> Result<(), MintOutputError> {
708        let output = output.ensure_v0_ref()?;
709
710        if dbtx
711            .get_value(&BlindNonceKey(output.blind_nonce))
712            .await
713            .is_some()
714        {
715            return Err(MintOutputError::BlindNonceAlreadyUsed);
716        }
717
718        Ok(())
719    }
720
721    async fn audit(
722        &self,
723        dbtx: &mut DatabaseTransaction<'_>,
724        audit: &mut Audit,
725        module_instance_id: ModuleInstanceId,
726    ) {
727        let mut redemptions = Amount::from_sats(0);
728        let mut issuances = Amount::from_sats(0);
729        let remove_audit_keys = dbtx
730            .find_by_prefix(&MintAuditItemKeyPrefix)
731            .await
732            .map(|(key, amount)| {
733                match key {
734                    MintAuditItemKey::Issuance(_) | MintAuditItemKey::IssuanceTotal => {
735                        issuances += amount;
736                    }
737                    MintAuditItemKey::Redemption(_) | MintAuditItemKey::RedemptionTotal => {
738                        redemptions += amount;
739                    }
740                }
741                key
742            })
743            .collect::<Vec<_>>()
744            .await;
745
746        for key in remove_audit_keys {
747            dbtx.remove_entry(&key).await;
748        }
749
750        dbtx.insert_entry(&MintAuditItemKey::IssuanceTotal, &issuances)
751            .await;
752        dbtx.insert_entry(&MintAuditItemKey::RedemptionTotal, &redemptions)
753            .await;
754
755        audit
756            .add_items(
757                dbtx,
758                module_instance_id,
759                &MintAuditItemKeyPrefix,
760                |k, v| match k {
761                    MintAuditItemKey::Issuance(_) | MintAuditItemKey::IssuanceTotal => {
762                        -(v.msats as i64)
763                    }
764                    MintAuditItemKey::Redemption(_) | MintAuditItemKey::RedemptionTotal => {
765                        v.msats as i64
766                    }
767                },
768            )
769            .await;
770    }
771
772    fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
773        vec![
774            api_endpoint! {
775                NOTE_SPENT_ENDPOINT,
776                ApiVersion::new(0, 1),
777                async |_module: &Mint, context, nonce: Nonce| -> bool {
778                    let db = context.db();
779                    let mut dbtx = db.begin_transaction_nc().await;
780                    Ok(dbtx.get_value(&NonceKey(nonce)).await.is_some())
781                }
782            },
783            api_endpoint! {
784                BLIND_NONCE_USED_ENDPOINT,
785                ApiVersion::new(0, 1),
786                async |_module: &Mint, context, blind_nonce: BlindNonce| -> bool {
787                    let db = context.db();
788                    let mut dbtx = db.begin_transaction_nc().await;
789                    Ok(dbtx.get_value(&BlindNonceKey(blind_nonce)).await.is_some())
790                }
791            },
792            api_endpoint! {
793                RECOVERY_COUNT_ENDPOINT,
794                ApiVersion::new(0, 1),
795                async |_module: &Mint, context, _params: ()| -> u64 {
796                    let db = context.db();
797                    let mut dbtx = db.begin_transaction_nc().await;
798                    Ok(get_recovery_count(&mut dbtx).await)
799                }
800            },
801            api_endpoint! {
802                RECOVERY_SLICE_ENDPOINT,
803                ApiVersion::new(0, 1),
804                async |_module: &Mint, context, range: (u64, u64)| -> SerdeModuleEncodingBase64<Vec<RecoveryItem>> {
805                    let db = context.db();
806                    let mut dbtx = db.begin_transaction_nc().await;
807                    Ok((&get_recovery_slice(&mut dbtx, range).await).into())
808                }
809            },
810            api_endpoint! {
811                RECOVERY_SLICE_HASH_ENDPOINT,
812                ApiVersion::new(0, 1),
813                async |_module: &Mint, context, range: (u64, u64)| -> sha256::Hash {
814                    let db = context.db();
815                    let mut dbtx = db.begin_transaction_nc().await;
816                    Ok(get_recovery_slice(&mut dbtx, range).await.consensus_hash())
817                }
818            },
819            api_endpoint! {
820                RECOVERY_BLIND_NONCE_OUTPOINTS_ENDPOINT,
821                ApiVersion::new(0, 1),
822                async |_module: &Mint, context, blind_nonces: Vec<BlindNonce>| -> Vec<OutPoint> {
823                    let db = context.db();
824                    let mut dbtx = db.begin_transaction_nc().await;
825                    let mut result = Vec::with_capacity(blind_nonces.len());
826                    for bn in blind_nonces {
827                        let out_point = dbtx
828                            .get_value(&RecoveryBlindNonceOutpointKey(bn))
829                            .await
830                            .ok_or_else(|| ApiError::bad_request("blind nonce not found".to_string()))?;
831                        result.push(out_point);
832                    }
833                    Ok(result)
834                }
835            },
836        ]
837    }
838}
839
840fn calculate_mint_issued_ecash_metrics(
841    dbtx: &mut DatabaseTransaction<'_>,
842    amount: Amount,
843    fee: Amount,
844) {
845    dbtx.on_commit(move || {
846        MINT_INOUT_SATS
847            .with_label_values(&["outgoing"])
848            .observe(amount.sats_f64());
849        MINT_INOUT_FEES_SATS
850            .with_label_values(&["outgoing"])
851            .observe(fee.sats_f64());
852        MINT_ISSUED_ECASH_SATS.observe(amount.sats_f64());
853        MINT_ISSUED_ECASH_FEES_SATS.observe(fee.sats_f64());
854    });
855}
856
857fn calculate_mint_redeemed_ecash_metrics(
858    dbtx: &mut DatabaseTransaction<'_>,
859    amount: Amount,
860    fee: Amount,
861) {
862    dbtx.on_commit(move || {
863        MINT_INOUT_SATS
864            .with_label_values(&["incoming"])
865            .observe(amount.sats_f64());
866        MINT_INOUT_FEES_SATS
867            .with_label_values(&["incoming"])
868            .observe(fee.sats_f64());
869        MINT_REDEEMED_ECASH_SATS.observe(amount.sats_f64());
870        MINT_REDEEMED_ECASH_FEES_SATS.observe(fee.sats_f64());
871    });
872}
873
874async fn get_recovery_count(dbtx: &mut DatabaseTransaction<'_>) -> u64 {
875    dbtx.find_by_prefix_sorted_descending(&RecoveryItemKeyPrefix)
876        .await
877        .next()
878        .await
879        .map_or(0, |entry| entry.0.0 + 1)
880}
881
882async fn get_recovery_slice(
883    dbtx: &mut DatabaseTransaction<'_>,
884    range: (u64, u64),
885) -> Vec<RecoveryItem> {
886    dbtx.find_by_range(RecoveryItemKey(range.0)..RecoveryItemKey(range.1))
887        .await
888        .map(|entry| entry.1)
889        .collect()
890        .await
891}
892
893impl Mint {
894    /// Constructs a new mint
895    ///
896    /// # Panics
897    /// * If there are no amount tiers
898    /// * If the amount tiers for secret and public keys are inconsistent
899    /// * If the pub key belonging to the secret key share is not in the pub key
900    ///   list.
901    pub fn new(cfg: MintConfig) -> Mint {
902        assert!(cfg.private.tbs_sks.tiers().count() > 0);
903
904        // The amount tiers are implicitly provided by the key sets, make sure they are
905        // internally consistent.
906        assert!(
907            cfg.consensus
908                .peer_tbs_pks
909                .values()
910                .all(|pk| pk.structural_eq(&cfg.private.tbs_sks))
911        );
912
913        let ref_pub_key = cfg
914            .private
915            .tbs_sks
916            .iter()
917            .map(|(amount, sk)| (amount, derive_pk_share(sk)))
918            .collect();
919
920        // Find our key index and make sure we know the private key for all our public
921        // key shares
922        let our_id = cfg
923            .consensus // FIXME: make sure we use id instead of idx everywhere
924            .peer_tbs_pks
925            .iter()
926            .find_map(|(&id, pk)| if *pk == ref_pub_key { Some(id) } else { None })
927            .expect("Own key not found among pub keys.");
928
929        assert_eq!(
930            cfg.consensus.peer_tbs_pks[&our_id],
931            cfg.private
932                .tbs_sks
933                .iter()
934                .map(|(amount, sk)| (amount, derive_pk_share(sk)))
935                .collect()
936        );
937
938        // TODO: the aggregate pks should become part of the MintConfigConsensus as they
939        // can be obtained by evaluating the polynomial returned by the DKG at
940        // zero
941        let aggregate_pub_keys = TieredMulti::new_aggregate_from_tiered_iter(
942            cfg.consensus.peer_tbs_pks.values().cloned(),
943        )
944        .into_iter()
945        .map(|(amt, keys)| {
946            let keys = (0_u64..)
947                .zip(keys)
948                .take(cfg.consensus.peer_tbs_pks.to_num_peers().threshold())
949                .collect();
950
951            (amt, aggregate_public_key_shares(&keys))
952        })
953        .collect();
954
955        Mint {
956            cfg: cfg.clone(),
957            sec_key: cfg.private.tbs_sks,
958            pub_key: aggregate_pub_keys,
959        }
960    }
961
962    pub fn pub_key(&self) -> BTreeMap<Amount, AggregatePublicKey> {
963        self.pub_key.clone()
964    }
965}
966
967#[cfg(test)]
968mod test;