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 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 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
384async 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; 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#[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 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 pub fn new(cfg: MintConfig) -> Mint {
690 assert!(cfg.private.tbs_sks.tiers().count() > 0);
691
692 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 let our_id = cfg
711 .consensus .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 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;