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, 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        // TODO: the aggregate pks should become part of the MintConfigConsensus as they
306        // can be obtained by evaluating the polynomial returned by the DKG at
307        // zero
308        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                        // We only care about e-cash issuances for this migration
351                        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; // FIXME: pass rng
386    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/// Federated mint member mint
412#[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            // TODO: make a consensus rule against this
516            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    /// Constructs a new mint
703    ///
704    /// # Panics
705    /// * If there are no amount tiers
706    /// * If the amount tiers for secret and public keys are inconsistent
707    /// * If the pub key belonging to the secret key share is not in the pub key
708    ///   list.
709    pub fn new(cfg: MintConfig) -> Mint {
710        assert!(cfg.private.tbs_sks.tiers().count() > 0);
711
712        // The amount tiers are implicitly provided by the key sets, make sure they are
713        // internally consistent.
714        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        // Find our key index and make sure we know the private key for all our public
728        // key shares
729        let our_id = cfg
730            .consensus // FIXME: make sure we use id instead of idx everywhere
731            .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        // TODO: the aggregate pks should become part of the MintConfigConsensus as they
746        // can be obtained by evaluating the polynomial returned by the DKG at
747        // zero
748        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        // Normal spend works
894        let db = Database::new(MemDatabase::new(), ModuleRegistry::default());
895        let input = MintInput::new_v0(highest_denomination, note);
896
897        // Double spend in same session is detected
898        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}