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