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