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, HashMap};
11
12use anyhow::bail;
13use fedimint_core::config::{
14 ConfigGenModuleParams, DkgResult, ServerModuleConfig, ServerModuleConsensusConfig,
15 TypedServerModuleConfig, TypedServerModuleConsensusConfig,
16};
17use fedimint_core::core::ModuleInstanceId;
18use fedimint_core::db::{
19 CoreMigrationFn, DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped,
20 MigrationContext,
21};
22use fedimint_core::module::audit::Audit;
23use fedimint_core::module::{
24 api_endpoint, ApiEndpoint, ApiError, ApiVersion, CoreConsensusVersion, InputMeta,
25 ModuleConsensusVersion, ModuleInit, PeerHandle, ServerModuleInit, ServerModuleInitArgs,
26 SupportedModuleApiVersions, TransactionItemAmount, CORE_CONSENSUS_VERSION,
27};
28use fedimint_core::server::DynServerModule;
29use fedimint_core::util::BoxFuture;
30use fedimint_core::{
31 apply, async_trait_maybe_send, push_db_key_items, push_db_pair_items, secp256k1, Amount,
32 NumPeersExt, OutPoint, PeerId, ServerModule, Tiered, TieredMulti,
33};
34use fedimint_logging::LOG_MODULE_MINT;
35pub use fedimint_mint_common as common;
36use fedimint_mint_common::config::{
37 MintClientConfig, MintConfig, MintConfigConsensus, MintConfigLocal, MintConfigPrivate,
38 MintGenParams,
39};
40use fedimint_mint_common::endpoint_constants::{BACKUP_ENDPOINT, RECOVER_ENDPOINT};
41pub use fedimint_mint_common::{BackupRequest, SignedBackupRequest};
42use fedimint_mint_common::{
43 MintCommonInit, MintConsensusItem, MintInput, MintInputError, MintModuleTypes, MintOutput,
44 MintOutputError, MintOutputOutcome, DEFAULT_MAX_NOTES_PER_DENOMINATION,
45 MODULE_CONSENSUS_VERSION,
46};
47use fedimint_server::config::distributedgen::{evaluate_polynomial_g2, scalar, PeerHandleOps};
48use fedimint_server::consensus::db::{MigrationContextExt, TypedModuleHistoryItem};
49use futures::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 aggregate_public_key_shares, sign_blinded_msg, AggregatePublicKey, PublicKeyShare,
59 SecretKeyShare,
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, ECashUserBackupSnapshot, EcashBackupKey,
70 EcashBackupKeyPrefix, MintAuditItemKey, MintAuditItemKeyPrefix, MintOutputOutcomeKey,
71 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::EcashBackup => {
115 push_db_pair_items!(
116 dbtx,
117 EcashBackupKeyPrefix,
118 EcashBackupKey,
119 ECashUserBackupSnapshot,
120 mint,
121 "User Ecash Backup"
122 );
123 }
124 DbKeyPrefix::BlindNonce => {
125 push_db_key_items!(
126 dbtx,
127 BlindNonceKeyPrefix,
128 BlindNonceKey,
129 mint,
130 "Used Blind Nonces"
131 );
132 }
133 }
134 }
135
136 Box::new(mint.into_iter())
137 }
138}
139
140#[apply(async_trait_maybe_send!)]
141impl ServerModuleInit for MintInit {
142 type Params = MintGenParams;
143
144 fn versions(&self, _core: CoreConsensusVersion) -> &[ModuleConsensusVersion] {
145 &[MODULE_CONSENSUS_VERSION]
146 }
147
148 fn supported_api_versions(&self) -> SupportedModuleApiVersions {
149 SupportedModuleApiVersions::from_raw(
150 (CORE_CONSENSUS_VERSION.major, CORE_CONSENSUS_VERSION.minor),
151 (
152 MODULE_CONSENSUS_VERSION.major,
153 MODULE_CONSENSUS_VERSION.minor,
154 ),
155 &[(0, 1)],
156 )
157 }
158
159 async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<DynServerModule> {
160 Ok(Mint::new(args.cfg().to_typed()?).into())
161 }
162
163 fn trusted_dealer_gen(
164 &self,
165 peers: &[PeerId],
166 params: &ConfigGenModuleParams,
167 ) -> BTreeMap<PeerId, ServerModuleConfig> {
168 let params = self.parse_params(params).unwrap();
169
170 let tbs_keys = params
171 .consensus
172 .gen_denominations()
173 .iter()
174 .map(|&amount| {
175 let (tbs_pk, tbs_pks, tbs_sks) =
176 dealer_keygen(peers.to_num_peers().threshold(), peers.len());
177 (amount, (tbs_pk, tbs_pks, tbs_sks))
178 })
179 .collect::<HashMap<_, _>>();
180
181 let mint_cfg: BTreeMap<_, MintConfig> = peers
182 .iter()
183 .map(|&peer| {
184 let config = MintConfig {
185 local: MintConfigLocal,
186 consensus: MintConfigConsensus {
187 peer_tbs_pks: peers
188 .iter()
189 .map(|&key_peer| {
190 let keys = params
191 .consensus
192 .gen_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: params.consensus.fee_consensus(),
202 max_notes_per_denomination: DEFAULT_MAX_NOTES_PER_DENOMINATION,
203 },
204 private: MintConfigPrivate {
205 tbs_sks: params
206 .consensus
207 .gen_denominations()
208 .iter()
209 .map(|amount| (*amount, tbs_keys[amount].2[peer.to_usize()]))
210 .collect(),
211 },
212 };
213 (peer, config)
214 })
215 .collect();
216
217 mint_cfg
218 .into_iter()
219 .map(|(k, v)| (k, v.to_erased()))
220 .collect()
221 }
222
223 async fn distributed_gen(
224 &self,
225 peers: &PeerHandle,
226 params: &ConfigGenModuleParams,
227 ) -> DkgResult<ServerModuleConfig> {
228 let params = self.parse_params(params).unwrap();
229
230 let g2 = peers
231 .run_dkg_multi_g2(params.consensus.gen_denominations())
232 .await?;
233
234 let amounts_keys = g2
235 .into_iter()
236 .map(|(amount, keys)| (amount, keys.tbs()))
237 .collect::<HashMap<_, _>>();
238
239 let server = MintConfig {
240 local: MintConfigLocal,
241 private: MintConfigPrivate {
242 tbs_sks: amounts_keys
243 .iter()
244 .map(|(amount, (_, sks))| (*amount, *sks))
245 .collect(),
246 },
247 consensus: MintConfigConsensus {
248 peer_tbs_pks: peers
249 .peer_ids()
250 .iter()
251 .map(|peer| {
252 let pks = amounts_keys
253 .iter()
254 .map(|(amount, (pks, _))| {
255 (
256 *amount,
257 PublicKeyShare(evaluate_polynomial_g2(pks, &scalar(peer))),
258 )
259 })
260 .collect::<Tiered<_>>();
261
262 (*peer, pks)
263 })
264 .collect(),
265 fee_consensus: params.consensus.fee_consensus(),
266 max_notes_per_denomination: DEFAULT_MAX_NOTES_PER_DENOMINATION,
267 },
268 };
269
270 Ok(server.to_erased())
271 }
272
273 fn validate_config(&self, identity: &PeerId, config: ServerModuleConfig) -> anyhow::Result<()> {
274 let config = config.to_typed::<MintConfig>()?;
275 let sks: BTreeMap<Amount, PublicKeyShare> = config
276 .private
277 .tbs_sks
278 .iter()
279 .map(|(amount, sk)| (amount, sk.to_pub_key_share()))
280 .collect();
281 let pks: BTreeMap<Amount, PublicKeyShare> = config
282 .consensus
283 .peer_tbs_pks
284 .get(identity)
285 .unwrap()
286 .as_map()
287 .iter()
288 .map(|(k, v)| (*k, *v))
289 .collect();
290 if sks != pks {
291 bail!("Mint private key doesn't match pubkey share");
292 }
293 if !sks.keys().contains(&Amount::from_msats(1)) {
294 bail!("No msat 1 denomination");
295 }
296
297 Ok(())
298 }
299
300 fn get_client_config(
301 &self,
302 config: &ServerModuleConsensusConfig,
303 ) -> anyhow::Result<MintClientConfig> {
304 let config = MintConfigConsensus::from_erased(config)?;
305 let tbs_pks =
309 TieredMulti::new_aggregate_from_tiered_iter(config.peer_tbs_pks.values().cloned())
310 .into_iter()
311 .map(|(amt, keys)| {
312 let keys = (1_u64..)
313 .zip(keys)
314 .take(config.peer_tbs_pks.to_num_peers().threshold())
315 .collect();
316
317 (amt, aggregate_public_key_shares(&keys))
318 })
319 .collect();
320
321 Ok(MintClientConfig {
322 tbs_pks,
323 fee_consensus: config.fee_consensus.clone(),
324 peer_tbs_pks: config.peer_tbs_pks.clone(),
325 max_notes_per_denomination: config.max_notes_per_denomination,
326 })
327 }
328
329 fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, CoreMigrationFn> {
330 let mut migrations = BTreeMap::new();
331 migrations.insert(DatabaseVersion(0), migrate_db_v0 as CoreMigrationFn);
332 migrations
333 }
334}
335
336fn migrate_db_v0(mut migration_context: MigrationContext<'_>) -> BoxFuture<anyhow::Result<()>> {
337 Box::pin(async move {
338 let blind_nonces = migration_context
339 .get_typed_module_history_stream::<MintModuleTypes>()
340 .await
341 .filter_map(|history_item: TypedModuleHistoryItem<_>| async move {
342 match history_item {
343 TypedModuleHistoryItem::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!("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!(?blind_nonce, "Blind nonce already used, money was burned!");
370 }
371 }
372
373 if double_issuances > 0 {
374 warn!("{double_issuances} blind nonces were reused, money was burned by faulty user clients!");
375 }
376
377 Ok(())
378 })
379}
380
381fn dealer_keygen(
382 threshold: usize,
383 keys: usize,
384) -> (AggregatePublicKey, Vec<PublicKeyShare>, Vec<SecretKeyShare>) {
385 let mut rng = OsRng; let poly: Vec<Scalar> = (0..threshold).map(|_| Scalar::random(&mut rng)).collect();
387
388 let apk = (G2Projective::generator() * eval_polynomial(&poly, &Scalar::zero())).to_affine();
389
390 let sks: Vec<SecretKeyShare> = (0..keys)
391 .map(|idx| SecretKeyShare(eval_polynomial(&poly, &Scalar::from(idx as u64 + 1))))
392 .collect();
393
394 let pks = sks
395 .iter()
396 .map(|sk| PublicKeyShare((G2Projective::generator() * sk.0).to_affine()))
397 .collect();
398
399 (AggregatePublicKey(apk), pks, sks)
400}
401
402fn eval_polynomial(coefficients: &[Scalar], x: &Scalar) -> Scalar {
403 coefficients
404 .iter()
405 .copied()
406 .rev()
407 .reduce(|acc, coefficient| acc * x + coefficient)
408 .expect("We have at least one coefficient")
409}
410
411#[derive(Debug)]
413pub struct Mint {
414 cfg: MintConfig,
415 sec_key: Tiered<SecretKeyShare>,
416 pub_key: HashMap<Amount, AggregatePublicKey>,
417}
418#[apply(async_trait_maybe_send!)]
419impl ServerModule for Mint {
420 type Common = MintModuleTypes;
421 type Init = MintInit;
422
423 async fn consensus_proposal(
424 &self,
425 _dbtx: &mut DatabaseTransaction<'_>,
426 ) -> Vec<MintConsensusItem> {
427 Vec::new()
428 }
429
430 async fn process_consensus_item<'a, 'b>(
431 &'a self,
432 _dbtx: &mut DatabaseTransaction<'b>,
433 _consensus_item: MintConsensusItem,
434 _peer_id: PeerId,
435 ) -> anyhow::Result<()> {
436 bail!("Mint does not process consensus items");
437 }
438
439 fn verify_input(&self, input: &MintInput) -> Result<(), MintInputError> {
440 let input = input.ensure_v0_ref()?;
441
442 let amount_key = self
443 .pub_key
444 .get(&input.amount)
445 .ok_or(MintInputError::InvalidAmountTier(input.amount))?;
446
447 if !input.note.verify(*amount_key) {
448 return Err(MintInputError::InvalidSignature);
449 }
450
451 Ok(())
452 }
453
454 async fn process_input<'a, 'b, 'c>(
455 &'a self,
456 dbtx: &mut DatabaseTransaction<'c>,
457 input: &'b MintInput,
458 ) -> Result<InputMeta, MintInputError> {
459 let input = input.ensure_v0_ref()?;
460
461 debug!(target: LOG_MODULE_MINT, nonce=%(input.note.nonce), "Marking note as spent");
462
463 if dbtx
464 .insert_entry(&NonceKey(input.note.nonce), &())
465 .await
466 .is_some()
467 {
468 return Err(MintInputError::SpentCoin);
469 }
470
471 dbtx.insert_new_entry(
472 &MintAuditItemKey::Redemption(NonceKey(input.note.nonce)),
473 &input.amount,
474 )
475 .await;
476
477 let amount = input.amount;
478 let fee = self.cfg.consensus.fee_consensus.fee(amount);
479
480 calculate_mint_redeemed_ecash_metrics(dbtx, amount, fee);
481
482 Ok(InputMeta {
483 amount: TransactionItemAmount { amount, fee },
484 pub_key: *input.note.spend_key(),
485 })
486 }
487
488 async fn process_output<'a, 'b>(
489 &'a self,
490 dbtx: &mut DatabaseTransaction<'b>,
491 output: &'a MintOutput,
492 out_point: OutPoint,
493 ) -> Result<TransactionItemAmount, MintOutputError> {
494 let output = output.ensure_v0_ref()?;
495
496 let amount_key = self
497 .sec_key
498 .get(output.amount)
499 .ok_or(MintOutputError::InvalidAmountTier(output.amount))?;
500
501 dbtx.insert_new_entry(
502 &MintOutputOutcomeKey(out_point),
503 &MintOutputOutcome::new_v0(sign_blinded_msg(output.blind_nonce.0, *amount_key)),
504 )
505 .await;
506
507 dbtx.insert_new_entry(&MintAuditItemKey::Issuance(out_point), &output.amount)
508 .await;
509
510 if dbtx
511 .insert_entry(&BlindNonceKey(output.blind_nonce), &())
512 .await
513 .is_some()
514 {
515 warn!(
517 denomination = %output.amount,
518 bnonce = ?output.blind_nonce,
519 "Blind nonce already used, money was burned!"
520 );
521 }
522
523 let amount = output.amount;
524 let fee = self.cfg.consensus.fee_consensus.fee(amount);
525
526 calculate_mint_issued_ecash_metrics(dbtx, amount, fee);
527
528 Ok(TransactionItemAmount { amount, fee })
529 }
530
531 async fn output_status(
532 &self,
533 dbtx: &mut DatabaseTransaction<'_>,
534 out_point: OutPoint,
535 ) -> Option<MintOutputOutcome> {
536 dbtx.get_value(&MintOutputOutcomeKey(out_point)).await
537 }
538
539 async fn audit(
540 &self,
541 dbtx: &mut DatabaseTransaction<'_>,
542 audit: &mut Audit,
543 module_instance_id: ModuleInstanceId,
544 ) {
545 let mut redemptions = Amount::from_sats(0);
546 let mut issuances = Amount::from_sats(0);
547 let remove_audit_keys = dbtx
548 .find_by_prefix(&MintAuditItemKeyPrefix)
549 .await
550 .map(|(key, amount)| {
551 match key {
552 MintAuditItemKey::Issuance(_) | MintAuditItemKey::IssuanceTotal => {
553 issuances += amount;
554 }
555 MintAuditItemKey::Redemption(_) | MintAuditItemKey::RedemptionTotal => {
556 redemptions += amount;
557 }
558 }
559 key
560 })
561 .collect::<Vec<_>>()
562 .await;
563
564 for key in remove_audit_keys {
565 dbtx.remove_entry(&key).await;
566 }
567
568 dbtx.insert_entry(&MintAuditItemKey::IssuanceTotal, &issuances)
569 .await;
570 dbtx.insert_entry(&MintAuditItemKey::RedemptionTotal, &redemptions)
571 .await;
572
573 audit
574 .add_items(
575 dbtx,
576 module_instance_id,
577 &MintAuditItemKeyPrefix,
578 |k, v| match k {
579 MintAuditItemKey::Issuance(_) | MintAuditItemKey::IssuanceTotal => {
580 -(v.msats as i64)
581 }
582 MintAuditItemKey::Redemption(_) | MintAuditItemKey::RedemptionTotal => {
583 v.msats as i64
584 }
585 },
586 )
587 .await;
588 }
589
590 fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
591 vec![
592 api_endpoint! {
593 BACKUP_ENDPOINT,
594 ApiVersion::new(0, 0),
595 async |module: &Mint, context, request: SignedBackupRequest| -> () {
596 module
597 .handle_backup_request(&mut context.dbtx().into_nc(), request).await?;
598 Ok(())
599 }
600 },
601 api_endpoint! {
602 RECOVER_ENDPOINT,
603 ApiVersion::new(0, 0),
604 async |module: &Mint, context, id: secp256k1::PublicKey| -> Option<ECashUserBackupSnapshot> {
605 Ok(module
606 .handle_recover_request(&mut context.dbtx().into_nc(), id).await)
607 }
608 },
609 api_endpoint! {
610 NOTE_SPENT_ENDPOINT,
611 ApiVersion::new(0, 1),
612 async |_module: &Mint, context, nonce: Nonce| -> bool {
613 Ok(context.dbtx().get_value(&NonceKey(nonce)).await.is_some())
614 }
615 },
616 api_endpoint! {
617 BLIND_NONCE_USED_ENDPOINT,
618 ApiVersion::new(0, 1),
619 async |_module: &Mint, context, blind_nonce: BlindNonce| -> bool {
620 Ok(context.dbtx().get_value(&BlindNonceKey(blind_nonce)).await.is_some())
621 }
622 },
623 ]
624 }
625}
626
627impl Mint {
628 async fn handle_backup_request(
629 &self,
630 dbtx: &mut DatabaseTransaction<'_>,
631 request: SignedBackupRequest,
632 ) -> Result<(), ApiError> {
633 let request = request
634 .verify_valid(secp256k1::SECP256K1)
635 .map_err(|_| ApiError::bad_request("invalid request".into()))?;
636
637 debug!(id = %request.id, len = request.payload.len(), "Received user e-cash backup request");
638 if let Some(prev) = dbtx.get_value(&EcashBackupKey(request.id)).await {
639 if request.timestamp <= prev.timestamp {
640 debug!(id = %request.id, len = request.payload.len(), "Received user e-cash backup request with old timestamp - ignoring");
641 return Err(ApiError::bad_request("timestamp too small".into()));
642 }
643 }
644
645 info!(id = %request.id, len = request.payload.len(), "Storing new user e-cash backup");
646 dbtx.insert_entry(
647 &EcashBackupKey(request.id),
648 &ECashUserBackupSnapshot {
649 timestamp: request.timestamp,
650 data: request.payload.clone(),
651 },
652 )
653 .await;
654
655 Ok(())
656 }
657
658 async fn handle_recover_request(
659 &self,
660 dbtx: &mut DatabaseTransaction<'_>,
661 id: secp256k1::PublicKey,
662 ) -> Option<ECashUserBackupSnapshot> {
663 dbtx.get_value(&EcashBackupKey(id)).await
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!(cfg
715 .consensus
716 .peer_tbs_pks
717 .values()
718 .all(|pk| pk.structural_eq(&cfg.private.tbs_sks)));
719
720 let ref_pub_key = cfg
721 .private
722 .tbs_sks
723 .iter()
724 .map(|(amt, key)| (amt, key.to_pub_key_share()))
725 .collect();
726
727 let our_id = cfg
730 .consensus .peer_tbs_pks
732 .iter()
733 .find_map(|(&id, pk)| if *pk == ref_pub_key { Some(id) } else { None })
734 .expect("Own key not found among pub keys.");
735
736 assert_eq!(
737 cfg.consensus.peer_tbs_pks[&our_id],
738 cfg.private
739 .tbs_sks
740 .iter()
741 .map(|(amount, sk)| (amount, sk.to_pub_key_share()))
742 .collect()
743 );
744
745 let aggregate_pub_keys = TieredMulti::new_aggregate_from_tiered_iter(
749 cfg.consensus.peer_tbs_pks.values().cloned(),
750 )
751 .into_iter()
752 .map(|(amt, keys)| {
753 let keys = (1_u64..)
754 .zip(keys)
755 .take(cfg.consensus.peer_tbs_pks.to_num_peers().threshold())
756 .collect();
757
758 (amt, aggregate_public_key_shares(&keys))
759 })
760 .collect();
761
762 Mint {
763 cfg: cfg.clone(),
764 sec_key: cfg.private.tbs_sks,
765 pub_key: aggregate_pub_keys,
766 }
767 }
768
769 pub fn pub_key(&self) -> HashMap<Amount, AggregatePublicKey> {
770 self.pub_key.clone()
771 }
772}
773
774#[cfg(test)]
775mod test {
776 use assert_matches::assert_matches;
777 use fedimint_core::config::{
778 ClientModuleConfig, ConfigGenModuleParams, EmptyGenParams, ServerModuleConfig,
779 };
780 use fedimint_core::db::mem_impl::MemDatabase;
781 use fedimint_core::db::Database;
782 use fedimint_core::module::registry::ModuleRegistry;
783 use fedimint_core::module::{ModuleConsensusVersion, ServerModuleInit};
784 use fedimint_core::{secp256k1, Amount, PeerId, ServerModule};
785 use fedimint_mint_common::config::FeeConsensus;
786 use fedimint_mint_common::{MintInput, Nonce, Note};
787 use tbs::blind_message;
788
789 use crate::common::config::MintGenParamsConsensus;
790 use crate::{
791 Mint, MintConfig, MintConfigConsensus, MintConfigLocal, MintConfigPrivate, MintGenParams,
792 MintInit,
793 };
794
795 const MINTS: u16 = 5;
796
797 fn build_configs() -> (Vec<ServerModuleConfig>, ClientModuleConfig) {
798 let peers = (0..MINTS).map(PeerId::from).collect::<Vec<_>>();
799 let mint_cfg = MintInit.trusted_dealer_gen(
800 &peers,
801 &ConfigGenModuleParams::from_typed(MintGenParams {
802 local: EmptyGenParams::default(),
803 consensus: MintGenParamsConsensus::new(
804 2,
805 FeeConsensus::new(1000).expect("Relative fee is within range"),
806 ),
807 })
808 .unwrap(),
809 );
810 let client_cfg = ClientModuleConfig::from_typed(
811 0,
812 MintInit::kind(),
813 ModuleConsensusVersion::new(0, 0),
814 MintInit
815 .get_client_config(&mint_cfg[&PeerId::from(0)].consensus)
816 .unwrap(),
817 )
818 .unwrap();
819
820 (mint_cfg.into_values().collect(), client_cfg)
821 }
822
823 #[test_log::test]
824 #[should_panic(expected = "Own key not found among pub keys.")]
825 fn test_new_panic_without_own_pub_key() {
826 let (mint_server_cfg1, _) = build_configs();
827 let (mint_server_cfg2, _) = build_configs();
828
829 Mint::new(MintConfig {
830 local: MintConfigLocal,
831 consensus: MintConfigConsensus {
832 peer_tbs_pks: mint_server_cfg2[0]
833 .to_typed::<MintConfig>()
834 .unwrap()
835 .consensus
836 .peer_tbs_pks,
837 fee_consensus: FeeConsensus::new(1000).expect("Relative fee is within range"),
838 max_notes_per_denomination: 0,
839 },
840 private: MintConfigPrivate {
841 tbs_sks: mint_server_cfg1[0]
842 .to_typed::<MintConfig>()
843 .unwrap()
844 .private
845 .tbs_sks,
846 },
847 });
848 }
849
850 fn issue_note(
851 server_cfgs: &[ServerModuleConfig],
852 denomination: Amount,
853 ) -> (secp256k1::Keypair, Note) {
854 let note_key = secp256k1::Keypair::new(secp256k1::SECP256K1, &mut rand::thread_rng());
855 let nonce = Nonce(note_key.public_key());
856 let message = nonce.to_message();
857 let blinding_key = tbs::BlindingKey::random();
858 let blind_msg = blind_message(message, blinding_key);
859
860 let bsig_shares = (1_u64..)
861 .zip(server_cfgs.iter().map(|cfg| {
862 let sks = *cfg
863 .to_typed::<MintConfig>()
864 .unwrap()
865 .private
866 .tbs_sks
867 .get(denomination)
868 .expect("Mint cannot issue a note of this denomination");
869 tbs::sign_blinded_msg(blind_msg, sks)
870 }))
871 .take(server_cfgs.len() - ((server_cfgs.len() - 1) / 3))
872 .collect();
873
874 let blind_signature = tbs::aggregate_signature_shares(&bsig_shares);
875 let signature = tbs::unblind_signature(blinding_key, blind_signature);
876
877 (note_key, Note { nonce, signature })
878 }
879
880 #[test_log::test(tokio::test)]
881 async fn test_detect_double_spends() {
882 let (mint_server_cfg, _) = build_configs();
883 let mint = Mint::new(mint_server_cfg[0].to_typed().unwrap());
884 let (_, tiered) = mint
885 .cfg
886 .consensus
887 .peer_tbs_pks
888 .first_key_value()
889 .expect("mint has peers");
890 let highest_denomination = *tiered.max_tier();
891 let (_, note) = issue_note(&mint_server_cfg, highest_denomination);
892
893 let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
895 let input = MintInput::new_v0(highest_denomination, note);
896
897 let mut dbtx = db.begin_transaction_nc().await;
899 mint.process_input(
900 &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
901 &input,
902 )
903 .await
904 .expect("Spend of valid e-cash works");
905 assert_matches!(
906 mint.process_input(
907 &mut dbtx.to_ref_with_prefix_module_id(42).0.into_nc(),
908 &input,
909 )
910 .await,
911 Err(_)
912 );
913 }
914}