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