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