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