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