fedimint_mint_server/
lib.rs

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        // TODO: the aggregate pks should become part of the MintConfigConsensus as they
290        // can be obtained by evaluating the polynomial returned by the DKG at
291        // zero
292        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                        // We only care about e-cash issuances for this migration
336                        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
370// Remove now unused ECash backups from DB. Backup functionality moved to core.
371fn 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; // FIXME: pass rng
387    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/// Federated mint member mint
413#[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            // TODO: make a consensus rule against this
518            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    /// Constructs a new mint
669    ///
670    /// # Panics
671    /// * If there are no amount tiers
672    /// * If the amount tiers for secret and public keys are inconsistent
673    /// * If the pub key belonging to the secret key share is not in the pub key
674    ///   list.
675    pub fn new(cfg: MintConfig) -> Mint {
676        assert!(cfg.private.tbs_sks.tiers().count() > 0);
677
678        // The amount tiers are implicitly provided by the key sets, make sure they are
679        // internally consistent.
680        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        // Find our key index and make sure we know the private key for all our public
694        // key shares
695        let our_id = cfg
696            .consensus // FIXME: make sure we use id instead of idx everywhere
697            .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        // TODO: the aggregate pks should become part of the MintConfigConsensus as they
712        // can be obtained by evaluating the polynomial returned by the DKG at
713        // zero
714        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        // Normal spend works
861        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
862        let input = MintInput::new_v0(highest_denomination, note);
863
864        // Double spend in same session is detected
865        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}