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
7mod db;
8
9use std::collections::BTreeMap;
10
11use anyhow::{bail, ensure};
12use bitcoin::hashes::sha256;
13use fedimint_core::config::{
14 ServerModuleConfig, ServerModuleConsensusConfig, TypedServerModuleConfig,
15 TypedServerModuleConsensusConfig,
16};
17use fedimint_core::core::ModuleInstanceId;
18use fedimint_core::db::{
19 Database, DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped,
20};
21use fedimint_core::encoding::Encodable;
22use fedimint_core::envs::{FM_ENABLE_MODULE_MINTV2_ENV, is_env_var_set_opt};
23use fedimint_core::module::audit::Audit;
24use fedimint_core::module::{
25 Amounts, ApiEndpoint, ApiError, ApiVersion, CORE_CONSENSUS_VERSION, CoreConsensusVersion,
26 InputMeta, ModuleConsensusVersion, ModuleInit, SupportedModuleApiVersions,
27 TransactionItemAmounts, api_endpoint,
28};
29use fedimint_core::{
30 Amount, BitcoinHash, InPoint, NumPeers, NumPeersExt, OutPoint, PeerId, apply,
31 async_trait_maybe_send, push_db_key_items, push_db_pair_items,
32};
33use fedimint_mintv2_common::config::{
34 FeeConsensus, MintClientConfig, MintConfig, MintConfigConsensus, MintConfigPrivate,
35 consensus_denominations,
36};
37use fedimint_mintv2_common::endpoint_constants::{
38 RECOVERY_COUNT_ENDPOINT, RECOVERY_SLICE_ENDPOINT, RECOVERY_SLICE_HASH_ENDPOINT,
39 SIGNATURE_SHARES_ENDPOINT, SIGNATURE_SHARES_RECOVERY_ENDPOINT,
40};
41use fedimint_mintv2_common::{
42 Denomination, MODULE_CONSENSUS_VERSION, MintCommonInit, MintConsensusItem, MintInput,
43 MintInputError, MintModuleTypes, MintOutput, MintOutputError, MintOutputOutcome, RecoveryItem,
44};
45use fedimint_server_core::config::{PeerHandleOps, eval_poly_g2};
46use fedimint_server_core::migration::ServerModuleDbMigrationFn;
47use fedimint_server_core::{
48 ConfigGenModuleArgs, ServerModule, ServerModuleInit, ServerModuleInitArgs,
49};
50use futures::StreamExt;
51use rand::SeedableRng;
52use rand_chacha::ChaChaRng;
53use strum::IntoEnumIterator;
54use tbs::{
55 AggregatePublicKey, BlindedSignatureShare, PublicKeyShare, SecretKeyShare, derive_pk_share,
56};
57use threshold_crypto::ff::Field;
58use threshold_crypto::group::Curve;
59use threshold_crypto::{G2Projective, Scalar};
60
61use crate::db::{
62 BlindedSignatureShareKey, BlindedSignatureSharePrefix, BlindedSignatureShareRecoveryKey,
63 BlindedSignatureShareRecoveryPrefix, DbKeyPrefix, IssuanceCounterKey, IssuanceCounterPrefix,
64 NonceKey, NonceKeyPrefix, RecoveryItemKey, RecoveryItemPrefix,
65};
66
67#[derive(Debug, Clone)]
68pub struct MintInit;
69
70impl ModuleInit for MintInit {
71 type Common = MintCommonInit;
72
73 async fn dump_database(
74 &self,
75 dbtx: &mut DatabaseTransaction<'_>,
76 prefix_names: Vec<String>,
77 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
78 let mut mint: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
79 let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
80 prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
81 });
82 for table in filtered_prefixes {
83 match table {
84 DbKeyPrefix::NoteNonce => {
85 push_db_key_items!(dbtx, NonceKeyPrefix, NonceKey, mint, "Used Coins");
86 }
87 DbKeyPrefix::BlindedSignatureShare => {
88 push_db_pair_items!(
89 dbtx,
90 BlindedSignatureSharePrefix,
91 BlindedSignatureShareKey,
92 BlindedSignatureShare,
93 mint,
94 "Blinded Signature Shares"
95 );
96 }
97 DbKeyPrefix::BlindedSignatureShareRecovery => {
98 push_db_pair_items!(
99 dbtx,
100 BlindedSignatureShareRecoveryPrefix,
101 BlindedSignatureShareRecoveryKey,
102 BlindedSignatureShare,
103 mint,
104 "Blinded Signature Shares (Recovery)"
105 );
106 }
107 DbKeyPrefix::MintAuditItem => {
108 push_db_pair_items!(
109 dbtx,
110 IssuanceCounterPrefix,
111 IssuanceCounterKey,
112 u64,
113 mint,
114 "Issuance Counter"
115 );
116 }
117 DbKeyPrefix::RecoveryItem => {
118 push_db_pair_items!(
119 dbtx,
120 RecoveryItemPrefix,
121 RecoveryItemKey,
122 RecoveryItem,
123 mint,
124 "Recovery Items"
125 );
126 }
127 }
128 }
129
130 Box::new(mint.into_iter())
131 }
132}
133
134#[apply(async_trait_maybe_send!)]
135impl ServerModuleInit for MintInit {
136 type Module = Mint;
137
138 fn versions(&self, _core: CoreConsensusVersion) -> &[ModuleConsensusVersion] {
139 &[MODULE_CONSENSUS_VERSION]
140 }
141
142 fn supported_api_versions(&self) -> SupportedModuleApiVersions {
143 SupportedModuleApiVersions::from_raw(
144 (CORE_CONSENSUS_VERSION.major, CORE_CONSENSUS_VERSION.minor),
145 (
146 MODULE_CONSENSUS_VERSION.major,
147 MODULE_CONSENSUS_VERSION.minor,
148 ),
149 &[(0, 1)],
150 )
151 }
152
153 fn is_enabled_by_default(&self) -> bool {
154 is_env_var_set_opt(FM_ENABLE_MODULE_MINTV2_ENV).unwrap_or(false)
155 }
156
157 async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
158 args.cfg().to_typed().map(|cfg| Mint {
159 cfg,
160 db: args.db().clone(),
161 })
162 }
163
164 fn trusted_dealer_gen(
165 &self,
166 peers: &[PeerId],
167 args: &ConfigGenModuleArgs,
168 ) -> BTreeMap<PeerId, ServerModuleConfig> {
169 let fee_consensus = if args.disable_base_fees {
170 FeeConsensus::zero()
171 } else {
172 FeeConsensus::new(0).expect("Relative fee is within range")
173 };
174
175 let tbs_agg_pks = consensus_denominations()
176 .map(|denomination| (denomination, dealer_agg_pk(denomination.amount())))
177 .collect::<BTreeMap<Denomination, AggregatePublicKey>>();
178
179 let tbs_pks = consensus_denominations()
180 .map(|denomination| {
181 let pks = peers
182 .iter()
183 .map(|peer| {
184 (
185 *peer,
186 dealer_pk(denomination.amount(), peers.to_num_peers(), *peer),
187 )
188 })
189 .collect();
190
191 (denomination, pks)
192 })
193 .collect::<BTreeMap<Denomination, BTreeMap<PeerId, PublicKeyShare>>>();
194
195 peers
196 .iter()
197 .map(|peer| {
198 let cfg = MintConfig {
199 consensus: MintConfigConsensus {
200 tbs_agg_pks: tbs_agg_pks.clone(),
201 tbs_pks: tbs_pks.clone(),
202 fee_consensus: fee_consensus.clone(),
203 },
204 private: MintConfigPrivate {
205 tbs_sks: consensus_denominations()
206 .map(|denomination| {
207 (
208 denomination,
209 dealer_sk(denomination.amount(), peers.to_num_peers(), *peer),
210 )
211 })
212 .collect(),
213 },
214 };
215
216 (*peer, cfg.to_erased())
217 })
218 .collect()
219 }
220
221 async fn distributed_gen(
222 &self,
223 peers: &(dyn PeerHandleOps + Send + Sync),
224 args: &ConfigGenModuleArgs,
225 ) -> anyhow::Result<ServerModuleConfig> {
226 let fee_consensus = if args.disable_base_fees {
227 FeeConsensus::zero()
228 } else {
229 FeeConsensus::new(0).expect("Relative fee is within range")
230 };
231
232 let mut tbs_sks = BTreeMap::new();
233 let mut tbs_agg_pks = BTreeMap::new();
234 let mut tbs_pks = BTreeMap::new();
235
236 for denomination in consensus_denominations() {
237 let (poly, sk) = peers.run_dkg_g2().await?;
238
239 tbs_sks.insert(denomination, tbs::SecretKeyShare(sk));
240
241 tbs_agg_pks.insert(denomination, AggregatePublicKey(poly[0].to_affine()));
242
243 let pks = peers
244 .num_peers()
245 .peer_ids()
246 .map(|peer| (peer, PublicKeyShare(eval_poly_g2(&poly, &peer))))
247 .collect();
248
249 tbs_pks.insert(denomination, pks);
250 }
251
252 let cfg = MintConfig {
253 private: MintConfigPrivate { tbs_sks },
254 consensus: MintConfigConsensus {
255 tbs_agg_pks,
256 tbs_pks,
257 fee_consensus,
258 },
259 };
260
261 Ok(cfg.to_erased())
262 }
263
264 fn validate_config(&self, identity: &PeerId, config: ServerModuleConfig) -> anyhow::Result<()> {
265 let config = config.to_typed::<MintConfig>()?;
266
267 for denomination in consensus_denominations() {
268 let pk = derive_pk_share(&config.private.tbs_sks[&denomination]);
269
270 ensure!(
271 pk == config.consensus.tbs_pks[&denomination][identity],
272 "Mint private key doesn't match pubkey share"
273 );
274 }
275
276 Ok(())
277 }
278
279 fn get_client_config(
280 &self,
281 config: &ServerModuleConsensusConfig,
282 ) -> anyhow::Result<MintClientConfig> {
283 let config = MintConfigConsensus::from_erased(config)?;
284
285 Ok(MintClientConfig {
286 tbs_agg_pks: config.tbs_agg_pks,
287 tbs_pks: config.tbs_pks.clone(),
288 fee_consensus: config.fee_consensus.clone(),
289 })
290 }
291
292 fn get_database_migrations(
293 &self,
294 ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Mint>> {
295 BTreeMap::new()
296 }
297}
298
299fn dealer_agg_pk(amount: Amount) -> AggregatePublicKey {
300 AggregatePublicKey((G2Projective::generator() * coefficient(amount, 0)).to_affine())
301}
302
303fn dealer_pk(amount: Amount, num_peers: NumPeers, peer: PeerId) -> PublicKeyShare {
304 derive_pk_share(&dealer_sk(amount, num_peers, peer))
305}
306
307fn dealer_sk(amount: Amount, num_peers: NumPeers, peer: PeerId) -> SecretKeyShare {
308 let x = Scalar::from(peer.to_usize() as u64 + 1);
309
310 let y = (0..num_peers.threshold())
314 .map(|index| coefficient(amount, index as u64))
315 .rev()
316 .reduce(|accumulator, c| accumulator * x + c)
317 .expect("We have at least one coefficient");
318
319 SecretKeyShare(y)
320}
321
322fn coefficient(amount: Amount, index: u64) -> Scalar {
323 Scalar::random(&mut ChaChaRng::from_seed(
324 *(amount, index)
325 .consensus_hash::<sha256::Hash>()
326 .as_byte_array(),
327 ))
328}
329
330#[derive(Debug)]
331pub struct Mint {
332 cfg: MintConfig,
333 db: Database,
334}
335
336impl Mint {
337 pub async fn note_distribution_ui(&self) -> BTreeMap<Denomination, u64> {
338 self.db
339 .begin_transaction_nc()
340 .await
341 .find_by_prefix(&IssuanceCounterPrefix)
342 .await
343 .filter(|entry| std::future::ready(entry.1 > 0))
344 .map(|(key, count)| (key.0, count))
345 .collect()
346 .await
347 }
348}
349
350#[apply(async_trait_maybe_send!)]
351impl ServerModule for Mint {
352 type Common = MintModuleTypes;
353 type Init = MintInit;
354
355 async fn consensus_proposal(
356 &self,
357 _dbtx: &mut DatabaseTransaction<'_>,
358 ) -> Vec<MintConsensusItem> {
359 Vec::new()
360 }
361
362 async fn process_consensus_item<'a, 'b>(
363 &'a self,
364 _dbtx: &mut DatabaseTransaction<'b>,
365 _consensus_item: MintConsensusItem,
366 _peer_id: PeerId,
367 ) -> anyhow::Result<()> {
368 bail!("Mint does not process consensus items");
369 }
370
371 async fn process_input<'a, 'b, 'c>(
372 &'a self,
373 dbtx: &mut DatabaseTransaction<'c>,
374 input: &'b MintInput,
375 _in_point: InPoint,
376 ) -> Result<InputMeta, MintInputError> {
377 let input = input.ensure_v0_ref()?;
378
379 let pk = self
380 .cfg
381 .consensus
382 .tbs_agg_pks
383 .get(&input.note.denomination)
384 .ok_or(MintInputError::InvalidAmountTier)?;
385
386 if !input.note.verify(*pk) {
387 return Err(MintInputError::InvalidSignature);
388 }
389
390 if dbtx
391 .insert_entry(&NonceKey(input.note.nonce), &())
392 .await
393 .is_some()
394 {
395 return Err(MintInputError::SpentCoin);
396 }
397
398 let new_count = dbtx
399 .remove_entry(&IssuanceCounterKey(input.note.denomination))
400 .await
401 .unwrap_or(0)
402 .checked_sub(1)
403 .expect("Failed to decrement issuance counter");
404
405 dbtx.insert_new_entry(&IssuanceCounterKey(input.note.denomination), &new_count)
406 .await;
407
408 let next_index = get_recovery_count(dbtx).await;
409
410 dbtx.insert_new_entry(
411 &RecoveryItemKey(next_index),
412 &RecoveryItem::Input {
413 nonce_hash: input.note.nonce.consensus_hash(),
414 },
415 )
416 .await;
417
418 let amount = input.note.amount();
419 Ok(InputMeta {
420 amount: TransactionItemAmounts {
421 amounts: Amounts::new_bitcoin(amount),
422 fees: Amounts::new_bitcoin(self.cfg.consensus.fee_consensus.fee(amount)),
423 },
424 pub_key: input.note.nonce,
425 })
426 }
427
428 async fn process_output<'a, 'b>(
429 &'a self,
430 dbtx: &mut DatabaseTransaction<'b>,
431 output: &'a MintOutput,
432 outpoint: OutPoint,
433 ) -> Result<TransactionItemAmounts, MintOutputError> {
434 let output = output.ensure_v0_ref()?;
435
436 let signature = self
437 .cfg
438 .private
439 .tbs_sks
440 .get(&output.denomination)
441 .map(|key| tbs::sign_message(output.nonce, *key))
442 .ok_or(MintOutputError::InvalidAmountTier)?;
443
444 dbtx.insert_entry(&BlindedSignatureShareKey(outpoint), &signature)
446 .await;
447
448 dbtx.insert_entry(&BlindedSignatureShareRecoveryKey(output.nonce), &signature)
450 .await;
451
452 let new_count = dbtx
453 .remove_entry(&IssuanceCounterKey(output.denomination))
454 .await
455 .unwrap_or(0)
456 .checked_add(1)
457 .expect("Failed to increment issuance counter");
458
459 dbtx.insert_new_entry(&IssuanceCounterKey(output.denomination), &new_count)
460 .await;
461
462 let next_index = get_recovery_count(dbtx).await;
463
464 dbtx.insert_new_entry(
465 &RecoveryItemKey(next_index),
466 &RecoveryItem::Output {
467 denomination: output.denomination,
468 nonce_hash: output.nonce.consensus_hash(),
469 tweak: output.tweak,
470 },
471 )
472 .await;
473
474 let amount = output.amount();
475 Ok(TransactionItemAmounts {
476 amounts: Amounts::new_bitcoin(amount),
477 fees: Amounts::new_bitcoin(self.cfg.consensus.fee_consensus.fee(amount)),
478 })
479 }
480
481 async fn output_status(
482 &self,
483 _dbtx: &mut DatabaseTransaction<'_>,
484 _outpoint: OutPoint,
485 ) -> Option<MintOutputOutcome> {
486 None
487 }
488
489 async fn audit(
490 &self,
491 dbtx: &mut DatabaseTransaction<'_>,
492 audit: &mut Audit,
493 module_instance_id: ModuleInstanceId,
494 ) {
495 audit
496 .add_items(dbtx, module_instance_id, &IssuanceCounterPrefix, |k, v| {
497 -((k.0.amount().msats * v) as i64)
498 })
499 .await;
500 }
501
502 fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
503 vec![
504 api_endpoint! {
505 SIGNATURE_SHARES_ENDPOINT,
506 ApiVersion::new(0, 1),
507 async |_module: &Mint, context, range: fedimint_core::OutPointRange| -> Vec<BlindedSignatureShare> {
508 let start_key = BlindedSignatureShareKey(range.start_out_point());
509 let end_key = BlindedSignatureShareKey(range.end_out_point());
510
511 let db = context.db();
512 let mut dbtx = db.begin_transaction_nc().await;
513 Ok(dbtx
514 .find_by_range(start_key..end_key)
515 .await
516 .map(|entry| entry.1)
517 .collect()
518 .await)
519 }
520 },
521 api_endpoint! {
522 SIGNATURE_SHARES_RECOVERY_ENDPOINT,
523 ApiVersion::new(0, 1),
524 async |_module: &Mint, context, messages: Vec<tbs::BlindedMessage>| -> Vec<BlindedSignatureShare> {
525 let db = context.db();
526 let mut dbtx = db.begin_transaction_nc().await;
527 let mut shares = Vec::new();
528
529 for message in messages {
530 let share = dbtx.get_value(&BlindedSignatureShareRecoveryKey(message))
531 .await
532 .ok_or(ApiError::bad_request("No blinded signature share found".to_string()))?;
533
534 shares.push(share);
535 }
536
537 Ok(shares)
538 }
539 },
540 api_endpoint! {
541 RECOVERY_SLICE_ENDPOINT,
542 ApiVersion::new(0, 1),
543 async |_module: &Mint, context, range: (u64, u64)| -> Vec<RecoveryItem> {
544 let db = context.db();
545 let mut dbtx = db.begin_transaction_nc().await;
546 Ok(get_recovery_slice(&mut dbtx, range).await)
547 }
548 },
549 api_endpoint! {
550 RECOVERY_SLICE_HASH_ENDPOINT,
551 ApiVersion::new(0, 1),
552 async |_module: &Mint, context, range: (u64, u64)| -> bitcoin::hashes::sha256::Hash {
553 let db = context.db();
554 let mut dbtx = db.begin_transaction_nc().await;
555 Ok(get_recovery_slice(&mut dbtx, range).await.consensus_hash())
556 }
557 },
558 api_endpoint! {
559 RECOVERY_COUNT_ENDPOINT,
560 ApiVersion::new(0, 1),
561 async |_module: &Mint, context, _params: ()| -> u64 {
562 let db = context.db();
563 let mut dbtx = db.begin_transaction_nc().await;
564 Ok(get_recovery_count(&mut dbtx).await)
565 }
566 },
567 ]
568 }
569}
570
571async fn get_recovery_count(dbtx: &mut DatabaseTransaction<'_>) -> u64 {
572 dbtx.find_by_prefix_sorted_descending(&RecoveryItemPrefix)
573 .await
574 .next()
575 .await
576 .map_or(0, |entry| entry.0.0 + 1)
577}
578
579async fn get_recovery_slice(
580 dbtx: &mut DatabaseTransaction<'_>,
581 range: (u64, u64),
582) -> Vec<RecoveryItem> {
583 dbtx.find_by_range(RecoveryItemKey(range.0)..RecoveryItemKey(range.1))
584 .await
585 .map(|entry| entry.1)
586 .collect()
587 .await
588}