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