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