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