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    FeeConsensus, MintClientConfig, MintConfig, MintConfigConsensus, MintConfigPrivate,
36    MintGenParams,
37};
38pub use fedimint_mint_common::{BackupRequest, SignedBackupRequest};
39use fedimint_mint_common::{
40    DEFAULT_MAX_NOTES_PER_DENOMINATION, MODULE_CONSENSUS_VERSION, MintCommonInit,
41    MintConsensusItem, MintInput, MintInputError, MintModuleTypes, MintOutput, MintOutputError,
42    MintOutputOutcome,
43};
44use fedimint_server_core::config::{PeerHandleOps, eval_poly_g2};
45use fedimint_server_core::migration::{
46    ModuleHistoryItem, ServerModuleDbMigrationFn, ServerModuleDbMigrationFnContext,
47    ServerModuleDbMigrationFnContextExt as _,
48};
49use fedimint_server_core::{ServerModule, ServerModuleInit, ServerModuleInitArgs};
50use futures::{FutureExt as _, StreamExt};
51use itertools::Itertools;
52use metrics::{
53    MINT_INOUT_FEES_SATS, MINT_INOUT_SATS, MINT_ISSUED_ECASH_FEES_SATS, MINT_ISSUED_ECASH_SATS,
54    MINT_REDEEMED_ECASH_FEES_SATS, MINT_REDEEMED_ECASH_SATS,
55};
56use rand::rngs::OsRng;
57use strum::IntoEnumIterator;
58use tbs::{
59    AggregatePublicKey, PublicKeyShare, SecretKeyShare, aggregate_public_key_shares,
60    derive_pk_share, sign_message,
61};
62use threshold_crypto::ff::Field;
63use threshold_crypto::group::Curve;
64use threshold_crypto::{G2Projective, Scalar};
65use tracing::{debug, info, warn};
66
67use crate::common::endpoint_constants::{BLIND_NONCE_USED_ENDPOINT, NOTE_SPENT_ENDPOINT};
68use crate::common::{BlindNonce, Nonce};
69use crate::db::{
70    BlindNonceKey, BlindNonceKeyPrefix, DbKeyPrefix, MintAuditItemKey, MintAuditItemKeyPrefix,
71    MintOutputOutcomeKey, MintOutputOutcomePrefix, NonceKey, NonceKeyPrefix,
72};
73
74#[derive(Debug, Clone)]
75pub struct MintInit;
76
77impl ModuleInit for MintInit {
78    type Common = MintCommonInit;
79
80    async fn dump_database(
81        &self,
82        dbtx: &mut DatabaseTransaction<'_>,
83        prefix_names: Vec<String>,
84    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
85        let mut mint: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
86        let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
87            prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
88        });
89        for table in filtered_prefixes {
90            match table {
91                DbKeyPrefix::NoteNonce => {
92                    push_db_key_items!(dbtx, NonceKeyPrefix, NonceKey, mint, "Used Coins");
93                }
94                DbKeyPrefix::MintAuditItem => {
95                    push_db_pair_items!(
96                        dbtx,
97                        MintAuditItemKeyPrefix,
98                        MintAuditItemKey,
99                        fedimint_core::Amount,
100                        mint,
101                        "Mint Audit Items"
102                    );
103                }
104                DbKeyPrefix::OutputOutcome => {
105                    push_db_pair_items!(
106                        dbtx,
107                        MintOutputOutcomePrefix,
108                        OutputOutcomeKey,
109                        MintOutputOutcome,
110                        mint,
111                        "Output Outcomes"
112                    );
113                }
114                DbKeyPrefix::BlindNonce => {
115                    push_db_key_items!(
116                        dbtx,
117                        BlindNonceKeyPrefix,
118                        BlindNonceKey,
119                        mint,
120                        "Used Blind Nonces"
121                    );
122                }
123            }
124        }
125
126        Box::new(mint.into_iter())
127    }
128}
129
130#[apply(async_trait_maybe_send!)]
131impl ServerModuleInit for MintInit {
132    type Module = Mint;
133    type Params = MintGenParams;
134
135    fn versions(&self, _core: CoreConsensusVersion) -> &[ModuleConsensusVersion] {
136        &[MODULE_CONSENSUS_VERSION]
137    }
138
139    fn supported_api_versions(&self) -> SupportedModuleApiVersions {
140        SupportedModuleApiVersions::from_raw(
141            (CORE_CONSENSUS_VERSION.major, CORE_CONSENSUS_VERSION.minor),
142            (
143                MODULE_CONSENSUS_VERSION.major,
144                MODULE_CONSENSUS_VERSION.minor,
145            ),
146            &[(0, 1)],
147        )
148    }
149
150    async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
151        Ok(Mint::new(args.cfg().to_typed()?))
152    }
153
154    fn trusted_dealer_gen(
155        &self,
156        peers: &[PeerId],
157        params: &ConfigGenModuleParams,
158        disable_base_fees: bool,
159    ) -> BTreeMap<PeerId, ServerModuleConfig> {
160        let params = self.parse_params(params).unwrap();
161
162        let tbs_keys = params
163            .consensus
164            .gen_denominations()
165            .iter()
166            .map(|&amount| {
167                let (tbs_pk, tbs_pks, tbs_sks) =
168                    dealer_keygen(peers.to_num_peers().threshold(), peers.len());
169                (amount, (tbs_pk, tbs_pks, tbs_sks))
170            })
171            .collect::<HashMap<_, _>>();
172
173        let mint_cfg: BTreeMap<_, MintConfig> = peers
174            .iter()
175            .map(|&peer| {
176                let config = MintConfig {
177                    consensus: MintConfigConsensus {
178                        peer_tbs_pks: peers
179                            .iter()
180                            .map(|&key_peer| {
181                                let keys = params
182                                    .consensus
183                                    .gen_denominations()
184                                    .iter()
185                                    .map(|amount| {
186                                        (*amount, tbs_keys[amount].1[key_peer.to_usize()])
187                                    })
188                                    .collect();
189                                (key_peer, keys)
190                            })
191                            .collect(),
192                        fee_consensus: params.consensus.fee_consensus().unwrap_or(
193                            if disable_base_fees {
194                                FeeConsensus::zero()
195                            } else {
196                                FeeConsensus::new(0).expect("Relative fee is within range")
197                            },
198                        ),
199                        max_notes_per_denomination: DEFAULT_MAX_NOTES_PER_DENOMINATION,
200                    },
201                    private: MintConfigPrivate {
202                        tbs_sks: params
203                            .consensus
204                            .gen_denominations()
205                            .iter()
206                            .map(|amount| (*amount, tbs_keys[amount].2[peer.to_usize()]))
207                            .collect(),
208                    },
209                };
210                (peer, config)
211            })
212            .collect();
213
214        mint_cfg
215            .into_iter()
216            .map(|(k, v)| (k, v.to_erased()))
217            .collect()
218    }
219
220    async fn distributed_gen(
221        &self,
222        peers: &(dyn PeerHandleOps + Send + Sync),
223        params: &ConfigGenModuleParams,
224        disable_base_fees: bool,
225    ) -> anyhow::Result<ServerModuleConfig> {
226        let params = self.parse_params(params).unwrap();
227
228        let mut amount_keys = HashMap::new();
229
230        for amount in params.consensus.gen_denominations() {
231            amount_keys.insert(amount, peers.run_dkg_g2().await?);
232        }
233
234        let server = MintConfig {
235            private: MintConfigPrivate {
236                tbs_sks: amount_keys
237                    .iter()
238                    .map(|(amount, (_, sks))| (*amount, tbs::SecretKeyShare(*sks)))
239                    .collect(),
240            },
241            consensus: MintConfigConsensus {
242                peer_tbs_pks: peers
243                    .num_peers()
244                    .peer_ids()
245                    .map(|peer| {
246                        let pks = amount_keys
247                            .iter()
248                            .map(|(amount, (pks, _))| {
249                                (*amount, PublicKeyShare(eval_poly_g2(pks, &peer)))
250                            })
251                            .collect::<Tiered<_>>();
252
253                        (peer, pks)
254                    })
255                    .collect(),
256                fee_consensus: params
257                    .consensus
258                    .fee_consensus()
259                    .unwrap_or(if disable_base_fees {
260                        FeeConsensus::zero()
261                    } else {
262                        FeeConsensus::new(0).expect("Relative fee is within range")
263                    }),
264                max_notes_per_denomination: DEFAULT_MAX_NOTES_PER_DENOMINATION,
265            },
266        };
267
268        Ok(server.to_erased())
269    }
270
271    fn validate_config(&self, identity: &PeerId, config: ServerModuleConfig) -> anyhow::Result<()> {
272        let config = config.to_typed::<MintConfig>()?;
273        let sks: BTreeMap<Amount, PublicKeyShare> = config
274            .private
275            .tbs_sks
276            .iter()
277            .map(|(amount, sk)| (amount, derive_pk_share(sk)))
278            .collect();
279        let pks: BTreeMap<Amount, PublicKeyShare> = config
280            .consensus
281            .peer_tbs_pks
282            .get(identity)
283            .unwrap()
284            .as_map()
285            .iter()
286            .map(|(k, v)| (*k, *v))
287            .collect();
288        if sks != pks {
289            bail!("Mint private key doesn't match pubkey share");
290        }
291        if !sks.keys().contains(&Amount::from_msats(1)) {
292            bail!("No msat 1 denomination");
293        }
294
295        Ok(())
296    }
297
298    fn get_client_config(
299        &self,
300        config: &ServerModuleConsensusConfig,
301    ) -> anyhow::Result<MintClientConfig> {
302        let config = MintConfigConsensus::from_erased(config)?;
303        // TODO: the aggregate pks should become part of the MintConfigConsensus as they
304        // can be obtained by evaluating the polynomial returned by the DKG at
305        // zero
306        let tbs_pks =
307            TieredMulti::new_aggregate_from_tiered_iter(config.peer_tbs_pks.values().cloned())
308                .into_iter()
309                .map(|(amt, keys)| {
310                    let keys = (0_u64..)
311                        .zip(keys)
312                        .take(config.peer_tbs_pks.to_num_peers().threshold())
313                        .collect();
314
315                    (amt, aggregate_public_key_shares(&keys))
316                })
317                .collect();
318
319        Ok(MintClientConfig {
320            tbs_pks,
321            fee_consensus: config.fee_consensus.clone(),
322            peer_tbs_pks: config.peer_tbs_pks.clone(),
323            max_notes_per_denomination: config.max_notes_per_denomination,
324        })
325    }
326
327    fn get_database_migrations(
328        &self,
329    ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Mint>> {
330        let mut migrations: BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<_>> =
331            BTreeMap::new();
332        migrations.insert(
333            DatabaseVersion(0),
334            Box::new(|ctx| migrate_db_v0(ctx).boxed()),
335        );
336        migrations.insert(
337            DatabaseVersion(1),
338            Box::new(|ctx| migrate_db_v1(ctx).boxed()),
339        );
340        migrations
341    }
342
343    fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
344        Some(DbKeyPrefix::iter().map(|p| p as u8).collect())
345    }
346}
347
348async fn migrate_db_v0(
349    mut migration_context: ServerModuleDbMigrationFnContext<'_, Mint>,
350) -> anyhow::Result<()> {
351    let blind_nonces = migration_context
352        .get_typed_module_history_stream()
353        .await
354        .filter_map(|history_item: ModuleHistoryItem<_>| async move {
355            match history_item {
356                ModuleHistoryItem::Output(mint_output) => Some(
357                    mint_output
358                        .ensure_v0_ref()
359                        .expect("This migration only runs while we only have v0 outputs")
360                        .blind_nonce,
361                ),
362                _ => {
363                    // We only care about e-cash issuances for this migration
364                    None
365                }
366            }
367        })
368        .collect::<Vec<_>>()
369        .await;
370
371    info!(target: LOG_MODULE_MINT, "Found {} blind nonces in history", blind_nonces.len());
372
373    let mut double_issuances = 0usize;
374    for blind_nonce in blind_nonces {
375        if migration_context
376            .dbtx()
377            .insert_entry(&BlindNonceKey(blind_nonce), &())
378            .await
379            .is_some()
380        {
381            double_issuances += 1;
382            debug!(
383                target: LOG_MODULE_MINT,
384                ?blind_nonce,
385                "Blind nonce already used, money was burned!"
386            );
387        }
388    }
389
390    if double_issuances > 0 {
391        warn!(target: LOG_MODULE_MINT, "{double_issuances} blind nonces were reused, money was burned by faulty user clients!");
392    }
393
394    Ok(())
395}
396
397// Remove now unused ECash backups from DB. Backup functionality moved to core.
398async fn migrate_db_v1(
399    mut migration_context: ServerModuleDbMigrationFnContext<'_, Mint>,
400) -> anyhow::Result<()> {
401    migration_context
402        .dbtx()
403        .raw_remove_by_prefix(&[0x15])
404        .await
405        .expect("DB error");
406    Ok(())
407}
408
409fn dealer_keygen(
410    threshold: usize,
411    keys: usize,
412) -> (AggregatePublicKey, Vec<PublicKeyShare>, Vec<SecretKeyShare>) {
413    let mut rng = OsRng; // FIXME: pass rng
414    let poly: Vec<Scalar> = (0..threshold).map(|_| Scalar::random(&mut rng)).collect();
415
416    let apk = (G2Projective::generator() * eval_polynomial(&poly, &Scalar::zero())).to_affine();
417
418    let sks: Vec<SecretKeyShare> = (0..keys)
419        .map(|idx| SecretKeyShare(eval_polynomial(&poly, &Scalar::from(idx as u64 + 1))))
420        .collect();
421
422    let pks = sks
423        .iter()
424        .map(|sk| PublicKeyShare((G2Projective::generator() * sk.0).to_affine()))
425        .collect();
426
427    (AggregatePublicKey(apk), pks, sks)
428}
429
430fn eval_polynomial(coefficients: &[Scalar], x: &Scalar) -> Scalar {
431    coefficients
432        .iter()
433        .copied()
434        .rev()
435        .reduce(|acc, coefficient| acc * x + coefficient)
436        .expect("We have at least one coefficient")
437}
438
439/// Federated mint member mint
440#[derive(Debug)]
441pub struct Mint {
442    cfg: MintConfig,
443    sec_key: Tiered<SecretKeyShare>,
444    pub_key: HashMap<Amount, AggregatePublicKey>,
445}
446#[apply(async_trait_maybe_send!)]
447impl ServerModule for Mint {
448    type Common = MintModuleTypes;
449    type Init = MintInit;
450
451    async fn consensus_proposal(
452        &self,
453        _dbtx: &mut DatabaseTransaction<'_>,
454    ) -> Vec<MintConsensusItem> {
455        Vec::new()
456    }
457
458    async fn process_consensus_item<'a, 'b>(
459        &'a self,
460        _dbtx: &mut DatabaseTransaction<'b>,
461        _consensus_item: MintConsensusItem,
462        _peer_id: PeerId,
463    ) -> anyhow::Result<()> {
464        bail!("Mint does not process consensus items");
465    }
466
467    fn verify_input(&self, input: &MintInput) -> Result<(), MintInputError> {
468        let input = input.ensure_v0_ref()?;
469
470        let amount_key = self
471            .pub_key
472            .get(&input.amount)
473            .ok_or(MintInputError::InvalidAmountTier(input.amount))?;
474
475        if !input.note.verify(*amount_key) {
476            return Err(MintInputError::InvalidSignature);
477        }
478
479        Ok(())
480    }
481
482    async fn process_input<'a, 'b, 'c>(
483        &'a self,
484        dbtx: &mut DatabaseTransaction<'c>,
485        input: &'b MintInput,
486        _in_point: InPoint,
487    ) -> Result<InputMeta, MintInputError> {
488        let input = input.ensure_v0_ref()?;
489
490        debug!(target: LOG_MODULE_MINT, nonce=%(input.note.nonce), "Marking note as spent");
491
492        if dbtx
493            .insert_entry(&NonceKey(input.note.nonce), &())
494            .await
495            .is_some()
496        {
497            return Err(MintInputError::SpentCoin);
498        }
499
500        dbtx.insert_new_entry(
501            &MintAuditItemKey::Redemption(NonceKey(input.note.nonce)),
502            &input.amount,
503        )
504        .await;
505
506        let amount = input.amount;
507        let fee = self.cfg.consensus.fee_consensus.fee(amount);
508
509        calculate_mint_redeemed_ecash_metrics(dbtx, amount, fee);
510
511        Ok(InputMeta {
512            amount: TransactionItemAmount { amount, fee },
513            pub_key: *input.note.spend_key(),
514        })
515    }
516
517    async fn process_output<'a, 'b>(
518        &'a self,
519        dbtx: &mut DatabaseTransaction<'b>,
520        output: &'a MintOutput,
521        out_point: OutPoint,
522    ) -> Result<TransactionItemAmount, MintOutputError> {
523        let output = output.ensure_v0_ref()?;
524
525        let amount_key = self
526            .sec_key
527            .get(output.amount)
528            .ok_or(MintOutputError::InvalidAmountTier(output.amount))?;
529
530        dbtx.insert_new_entry(
531            &MintOutputOutcomeKey(out_point),
532            &MintOutputOutcome::new_v0(sign_message(output.blind_nonce.0, *amount_key)),
533        )
534        .await;
535
536        dbtx.insert_new_entry(&MintAuditItemKey::Issuance(out_point), &output.amount)
537            .await;
538
539        if dbtx
540            .insert_entry(&BlindNonceKey(output.blind_nonce), &())
541            .await
542            .is_some()
543        {
544            // TODO: make a consensus rule against this
545            warn!(
546                target: LOG_MODULE_MINT,
547                denomination = %output.amount,
548                bnonce = ?output.blind_nonce,
549                "Blind nonce already used, money was burned!"
550            );
551        }
552
553        let amount = output.amount;
554        let fee = self.cfg.consensus.fee_consensus.fee(amount);
555
556        calculate_mint_issued_ecash_metrics(dbtx, amount, fee);
557
558        Ok(TransactionItemAmount { amount, fee })
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;