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