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