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 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 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
381async 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; 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#[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 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 pub fn new(cfg: MintConfig) -> Mint {
687 assert!(cfg.private.tbs_sks.tiers().count() > 0);
688
689 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 let our_id = cfg
708 .consensus .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 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;