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 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 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 .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 consensus: MintConfigConsensus {
177 peer_tbs_pks: peers
178 .iter()
179 .map(|&key_peer| {
180 let keys = params
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: if disable_base_fees {
191 FeeConsensus::zero()
192 } else {
193 params.fee_consensus().unwrap_or_else(|| {
194 FeeConsensus::new(0).expect("Relative fee is within range")
195 })
196 },
197 max_notes_per_denomination: DEFAULT_MAX_NOTES_PER_DENOMINATION,
198 },
199 private: MintConfigPrivate {
200 tbs_sks: params
201 .gen_denominations()
202 .iter()
203 .map(|amount| (*amount, tbs_keys[amount].2[peer.to_usize()]))
204 .collect(),
205 },
206 };
207 (peer, config)
208 })
209 .collect();
210
211 mint_cfg
212 .into_iter()
213 .map(|(k, v)| (k, v.to_erased()))
214 .collect()
215 }
216
217 async fn distributed_gen(
218 &self,
219 peers: &(dyn PeerHandleOps + Send + Sync),
220 params: &ConfigGenModuleParams,
221 disable_base_fees: bool,
222 ) -> anyhow::Result<ServerModuleConfig> {
223 let params = self.parse_params(params).unwrap();
224
225 let mut amount_keys = HashMap::new();
226
227 for amount in params.gen_denominations() {
228 amount_keys.insert(amount, peers.run_dkg_g2().await?);
229 }
230
231 let server = MintConfig {
232 private: MintConfigPrivate {
233 tbs_sks: amount_keys
234 .iter()
235 .map(|(amount, (_, sks))| (*amount, tbs::SecretKeyShare(*sks)))
236 .collect(),
237 },
238 consensus: MintConfigConsensus {
239 peer_tbs_pks: peers
240 .num_peers()
241 .peer_ids()
242 .map(|peer| {
243 let pks = amount_keys
244 .iter()
245 .map(|(amount, (pks, _))| {
246 (*amount, PublicKeyShare(eval_poly_g2(pks, &peer)))
247 })
248 .collect::<Tiered<_>>();
249
250 (peer, pks)
251 })
252 .collect(),
253 fee_consensus: params.fee_consensus().unwrap_or(if disable_base_fees {
254 FeeConsensus::zero()
255 } else {
256 FeeConsensus::new(0).expect("Relative fee is within range")
257 }),
258 max_notes_per_denomination: DEFAULT_MAX_NOTES_PER_DENOMINATION,
259 },
260 };
261
262 Ok(server.to_erased())
263 }
264
265 fn validate_config(&self, identity: &PeerId, config: ServerModuleConfig) -> anyhow::Result<()> {
266 let config = config.to_typed::<MintConfig>()?;
267 let sks: BTreeMap<Amount, PublicKeyShare> = config
268 .private
269 .tbs_sks
270 .iter()
271 .map(|(amount, sk)| (amount, derive_pk_share(sk)))
272 .collect();
273 let pks: BTreeMap<Amount, PublicKeyShare> = config
274 .consensus
275 .peer_tbs_pks
276 .get(identity)
277 .unwrap()
278 .as_map()
279 .iter()
280 .map(|(k, v)| (*k, *v))
281 .collect();
282 if sks != pks {
283 bail!("Mint private key doesn't match pubkey share");
284 }
285 if !sks.keys().contains(&Amount::from_msats(1)) {
286 bail!("No msat 1 denomination");
287 }
288
289 Ok(())
290 }
291
292 fn get_client_config(
293 &self,
294 config: &ServerModuleConsensusConfig,
295 ) -> anyhow::Result<MintClientConfig> {
296 let config = MintConfigConsensus::from_erased(config)?;
297 let tbs_pks =
301 TieredMulti::new_aggregate_from_tiered_iter(config.peer_tbs_pks.values().cloned())
302 .into_iter()
303 .map(|(amt, keys)| {
304 let keys = (0_u64..)
305 .zip(keys)
306 .take(config.peer_tbs_pks.to_num_peers().threshold())
307 .collect();
308
309 (amt, aggregate_public_key_shares(&keys))
310 })
311 .collect();
312
313 Ok(MintClientConfig {
314 tbs_pks,
315 fee_consensus: config.fee_consensus.clone(),
316 peer_tbs_pks: config.peer_tbs_pks.clone(),
317 max_notes_per_denomination: config.max_notes_per_denomination,
318 })
319 }
320
321 fn get_database_migrations(
322 &self,
323 ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Mint>> {
324 let mut migrations: BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<_>> =
325 BTreeMap::new();
326 migrations.insert(
327 DatabaseVersion(0),
328 Box::new(|ctx| migrate_db_v0(ctx).boxed()),
329 );
330 migrations.insert(
331 DatabaseVersion(1),
332 Box::new(|ctx| migrate_db_v1(ctx).boxed()),
333 );
334 migrations
335 }
336
337 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
338 Some(DbKeyPrefix::iter().map(|p| p as u8).collect())
339 }
340}
341
342async fn migrate_db_v0(
343 mut migration_context: ServerModuleDbMigrationFnContext<'_, Mint>,
344) -> anyhow::Result<()> {
345 let blind_nonces = migration_context
346 .get_typed_module_history_stream()
347 .await
348 .filter_map(|history_item: ModuleHistoryItem<_>| async move {
349 match history_item {
350 ModuleHistoryItem::Output(mint_output, _) => Some(
351 mint_output
352 .ensure_v0_ref()
353 .expect("This migration only runs while we only have v0 outputs")
354 .blind_nonce,
355 ),
356 _ => {
357 None
359 }
360 }
361 })
362 .collect::<Vec<_>>()
363 .await;
364
365 info!(target: LOG_MODULE_MINT, "Found {} blind nonces in history", blind_nonces.len());
366
367 let mut double_issuances = 0usize;
368 for blind_nonce in blind_nonces {
369 if migration_context
370 .dbtx()
371 .insert_entry(&BlindNonceKey(blind_nonce), &())
372 .await
373 .is_some()
374 {
375 double_issuances += 1;
376 debug!(
377 target: LOG_MODULE_MINT,
378 ?blind_nonce,
379 "Blind nonce already used, money was burned!"
380 );
381 }
382 }
383
384 if double_issuances > 0 {
385 warn!(target: LOG_MODULE_MINT, "{double_issuances} blind nonces were reused, money was burned by faulty user clients!");
386 }
387
388 Ok(())
389}
390
391async fn migrate_db_v1(
393 mut migration_context: ServerModuleDbMigrationFnContext<'_, Mint>,
394) -> anyhow::Result<()> {
395 migration_context
396 .dbtx()
397 .raw_remove_by_prefix(&[0x15])
398 .await
399 .expect("DB error");
400 Ok(())
401}
402
403fn dealer_keygen(
404 threshold: usize,
405 keys: usize,
406) -> (AggregatePublicKey, Vec<PublicKeyShare>, Vec<SecretKeyShare>) {
407 let mut rng = OsRng; let poly: Vec<Scalar> = (0..threshold).map(|_| Scalar::random(&mut rng)).collect();
409
410 let apk = (G2Projective::generator() * eval_polynomial(&poly, &Scalar::zero())).to_affine();
411
412 let sks: Vec<SecretKeyShare> = (0..keys)
413 .map(|idx| SecretKeyShare(eval_polynomial(&poly, &Scalar::from(idx as u64 + 1))))
414 .collect();
415
416 let pks = sks
417 .iter()
418 .map(|sk| PublicKeyShare((G2Projective::generator() * sk.0).to_affine()))
419 .collect();
420
421 (AggregatePublicKey(apk), pks, sks)
422}
423
424fn eval_polynomial(coefficients: &[Scalar], x: &Scalar) -> Scalar {
425 coefficients
426 .iter()
427 .copied()
428 .rev()
429 .reduce(|acc, coefficient| acc * x + coefficient)
430 .expect("We have at least one coefficient")
431}
432
433#[derive(Debug)]
435pub struct Mint {
436 cfg: MintConfig,
437 sec_key: Tiered<SecretKeyShare>,
438 pub_key: HashMap<Amount, AggregatePublicKey>,
439}
440#[apply(async_trait_maybe_send!)]
441impl ServerModule for Mint {
442 type Common = MintModuleTypes;
443 type Init = MintInit;
444
445 async fn consensus_proposal(
446 &self,
447 _dbtx: &mut DatabaseTransaction<'_>,
448 ) -> Vec<MintConsensusItem> {
449 Vec::new()
450 }
451
452 async fn process_consensus_item<'a, 'b>(
453 &'a self,
454 _dbtx: &mut DatabaseTransaction<'b>,
455 _consensus_item: MintConsensusItem,
456 _peer_id: PeerId,
457 ) -> anyhow::Result<()> {
458 bail!("Mint does not process consensus items");
459 }
460
461 fn verify_input(&self, input: &MintInput) -> Result<(), MintInputError> {
462 let input = input.ensure_v0_ref()?;
463
464 let amount_key = self
465 .pub_key
466 .get(&input.amount)
467 .ok_or(MintInputError::InvalidAmountTier(input.amount))?;
468
469 if !input.note.verify(*amount_key) {
470 return Err(MintInputError::InvalidSignature);
471 }
472
473 Ok(())
474 }
475
476 async fn process_input<'a, 'b, 'c>(
477 &'a self,
478 dbtx: &mut DatabaseTransaction<'c>,
479 input: &'b MintInput,
480 _in_point: InPoint,
481 ) -> Result<InputMeta, MintInputError> {
482 let input = input.ensure_v0_ref()?;
483
484 debug!(target: LOG_MODULE_MINT, nonce=%(input.note.nonce), "Marking note as spent");
485
486 if dbtx
487 .insert_entry(&NonceKey(input.note.nonce), &())
488 .await
489 .is_some()
490 {
491 return Err(MintInputError::SpentCoin);
492 }
493
494 dbtx.insert_new_entry(
495 &MintAuditItemKey::Redemption(NonceKey(input.note.nonce)),
496 &input.amount,
497 )
498 .await;
499
500 let amount = input.amount;
501 let fee = self.cfg.consensus.fee_consensus.fee(amount);
502
503 calculate_mint_redeemed_ecash_metrics(dbtx, amount, fee);
504
505 Ok(InputMeta {
506 amount: TransactionItemAmounts {
507 amounts: Amounts::new_bitcoin(amount),
508 fees: Amounts::new_bitcoin(fee),
509 },
510 pub_key: *input.note.spend_key(),
511 })
512 }
513
514 async fn process_output<'a, 'b>(
515 &'a self,
516 dbtx: &mut DatabaseTransaction<'b>,
517 output: &'a MintOutput,
518 out_point: OutPoint,
519 ) -> Result<TransactionItemAmounts, MintOutputError> {
520 let output = output.ensure_v0_ref()?;
521
522 let amount_key = self
523 .sec_key
524 .get(output.amount)
525 .ok_or(MintOutputError::InvalidAmountTier(output.amount))?;
526
527 dbtx.insert_new_entry(
528 &MintOutputOutcomeKey(out_point),
529 &MintOutputOutcome::new_v0(sign_message(output.blind_nonce.0, *amount_key)),
530 )
531 .await;
532
533 dbtx.insert_new_entry(&MintAuditItemKey::Issuance(out_point), &output.amount)
534 .await;
535
536 if dbtx
537 .insert_entry(&BlindNonceKey(output.blind_nonce), &())
538 .await
539 .is_some()
540 {
541 warn!(
543 target: LOG_MODULE_MINT,
544 denomination = %output.amount,
545 bnonce = ?output.blind_nonce,
546 "Blind nonce already used, money was burned!"
547 );
548 }
549
550 let amount = output.amount;
551 let fee = self.cfg.consensus.fee_consensus.fee(amount);
552
553 calculate_mint_issued_ecash_metrics(dbtx, amount, fee);
554
555 Ok(TransactionItemAmounts {
556 amounts: Amounts::new_bitcoin(amount),
557 fees: Amounts::new_bitcoin(fee),
558 })
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 pub fn new(cfg: MintConfig) -> Mint {
703 assert!(cfg.private.tbs_sks.tiers().count() > 0);
704
705 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 let our_id = cfg
724 .consensus .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 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;