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