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 AmountUnit, Amounts, ApiEndpoint, ApiError, ApiVersion, CoreConsensusVersion, InputMeta,
26 ModuleConsensusVersion, ModuleInit, TransactionItemAmounts, api_endpoint,
27};
28use fedimint_core::{
29 Amount, BitcoinHash, InPoint, NumPeers, NumPeersExt, OutPoint, PeerId, apply,
30 async_trait_maybe_send, push_db_key_items, push_db_pair_items,
31};
32use fedimint_mintv2_common::config::{
33 FeeConsensus, MintClientConfig, MintConfig, MintConfigConsensus, MintConfigPrivate,
34 consensus_denominations,
35};
36use fedimint_mintv2_common::endpoint_constants::{
37 RECOVERY_COUNT_ENDPOINT, RECOVERY_SLICE_ENDPOINT, RECOVERY_SLICE_HASH_ENDPOINT,
38 SIGNATURE_SHARES_ENDPOINT, SIGNATURE_SHARES_RECOVERY_ENDPOINT,
39};
40use fedimint_mintv2_common::{
41 Denomination, MODULE_CONSENSUS_VERSION, MintCommonInit, MintConsensusItem, MintInput,
42 MintInputError, MintModuleTypes, MintOutput, MintOutputError, MintOutputOutcome, RecoveryItem,
43 verify_note,
44};
45use fedimint_server_core::config::{PeerHandleOps, eval_poly_g2};
46use fedimint_server_core::migration::ServerModuleDbMigrationFn;
47use fedimint_server_core::{
48 ConfigGenModuleArgs, EnvVarDoc, 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 is_enabled_by_default(&self) -> bool {
143 is_env_var_set_opt(FM_ENABLE_MODULE_MINTV2_ENV).unwrap_or(true)
144 }
145
146 fn get_documented_env_vars(&self) -> Vec<EnvVarDoc> {
147 vec![EnvVarDoc {
148 name: FM_ENABLE_MODULE_MINTV2_ENV,
149 description: "Set to 0/false to disable the MintV2 module. Enabled by default.",
150 }]
151 }
152
153 async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
154 args.cfg().to_typed().map(|cfg| Mint {
155 cfg,
156 db: args.db().clone(),
157 })
158 }
159
160 fn trusted_dealer_gen(
161 &self,
162 peers: &[PeerId],
163 args: &ConfigGenModuleArgs,
164 ) -> BTreeMap<PeerId, ServerModuleConfig> {
165 let fee_consensus = if args.disable_base_fees {
166 FeeConsensus::zero()
167 } else {
168 FeeConsensus::new(0).expect("Relative fee is within range")
169 };
170
171 let tbs_agg_pks = consensus_denominations()
172 .map(|denomination| (denomination, dealer_agg_pk(denomination.amount())))
173 .collect::<BTreeMap<Denomination, AggregatePublicKey>>();
174
175 let tbs_pks = consensus_denominations()
176 .map(|denomination| {
177 let pks = peers
178 .iter()
179 .map(|peer| {
180 (
181 *peer,
182 dealer_pk(denomination.amount(), peers.to_num_peers(), *peer),
183 )
184 })
185 .collect();
186
187 (denomination, pks)
188 })
189 .collect::<BTreeMap<Denomination, BTreeMap<PeerId, PublicKeyShare>>>();
190
191 peers
192 .iter()
193 .map(|peer| {
194 let cfg = MintConfig {
195 consensus: MintConfigConsensus {
196 tbs_agg_pks: tbs_agg_pks.clone(),
197 tbs_pks: tbs_pks.clone(),
198 fee_consensus: fee_consensus.clone(),
199 amount_unit: AmountUnit::BITCOIN,
200 },
201 private: MintConfigPrivate {
202 tbs_sks: consensus_denominations()
203 .map(|denomination| {
204 (
205 denomination,
206 dealer_sk(denomination.amount(), peers.to_num_peers(), *peer),
207 )
208 })
209 .collect(),
210 },
211 };
212
213 (*peer, cfg.to_erased())
214 })
215 .collect()
216 }
217
218 async fn distributed_gen(
219 &self,
220 peers: &(dyn PeerHandleOps + Send + Sync),
221 args: &ConfigGenModuleArgs,
222 ) -> anyhow::Result<ServerModuleConfig> {
223 let fee_consensus = if args.disable_base_fees {
224 FeeConsensus::zero()
225 } else {
226 FeeConsensus::new(0).expect("Relative fee is within range")
227 };
228
229 let mut tbs_sks = BTreeMap::new();
230 let mut tbs_agg_pks = BTreeMap::new();
231 let mut tbs_pks = BTreeMap::new();
232
233 for denomination in consensus_denominations() {
234 let (poly, sk) = peers.run_dkg_g2().await?;
235
236 tbs_sks.insert(denomination, tbs::SecretKeyShare(sk));
237
238 tbs_agg_pks.insert(denomination, AggregatePublicKey(poly[0].to_affine()));
239
240 let pks = peers
241 .num_peers()
242 .peer_ids()
243 .map(|peer| (peer, PublicKeyShare(eval_poly_g2(&poly, &peer))))
244 .collect();
245
246 tbs_pks.insert(denomination, pks);
247 }
248
249 let cfg = MintConfig {
250 private: MintConfigPrivate { tbs_sks },
251 consensus: MintConfigConsensus {
252 tbs_agg_pks,
253 tbs_pks,
254 fee_consensus,
255 amount_unit: AmountUnit::BITCOIN,
256 },
257 };
258
259 Ok(cfg.to_erased())
260 }
261
262 fn validate_config(&self, identity: &PeerId, config: ServerModuleConfig) -> anyhow::Result<()> {
263 let config = config.to_typed::<MintConfig>()?;
264
265 for denomination in consensus_denominations() {
266 let pk = derive_pk_share(&config.private.tbs_sks[&denomination]);
267
268 ensure!(
269 pk == config.consensus.tbs_pks[&denomination][identity],
270 "Mint private key doesn't match pubkey share"
271 );
272 }
273
274 Ok(())
275 }
276
277 fn get_client_config(
278 &self,
279 config: &ServerModuleConsensusConfig,
280 ) -> anyhow::Result<MintClientConfig> {
281 let config = MintConfigConsensus::from_erased(config)?;
282
283 Ok(MintClientConfig {
284 tbs_agg_pks: config.tbs_agg_pks,
285 tbs_pks: config.tbs_pks.clone(),
286 fee_consensus: config.fee_consensus.clone(),
287 amount_unit: config.amount_unit,
288 })
289 }
290
291 fn get_database_migrations(
292 &self,
293 ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Mint>> {
294 BTreeMap::new()
295 }
296}
297
298fn dealer_agg_pk(amount: Amount) -> AggregatePublicKey {
299 AggregatePublicKey((G2Projective::generator() * coefficient(amount, 0)).to_affine())
300}
301
302fn dealer_pk(amount: Amount, num_peers: NumPeers, peer: PeerId) -> PublicKeyShare {
303 derive_pk_share(&dealer_sk(amount, num_peers, peer))
304}
305
306fn dealer_sk(amount: Amount, num_peers: NumPeers, peer: PeerId) -> SecretKeyShare {
307 let x = Scalar::from(peer.to_usize() as u64 + 1);
308
309 let y = (0..num_peers.threshold())
313 .map(|index| coefficient(amount, index as u64))
314 .rev()
315 .reduce(|accumulator, c| accumulator * x + c)
316 .expect("We have at least one coefficient");
317
318 SecretKeyShare(y)
319}
320
321fn coefficient(amount: Amount, index: u64) -> Scalar {
322 Scalar::random(&mut ChaChaRng::from_seed(
323 *(amount, index)
324 .consensus_hash::<sha256::Hash>()
325 .as_byte_array(),
326 ))
327}
328
329#[derive(Debug)]
330pub struct Mint {
331 cfg: MintConfig,
332 db: Database,
333}
334
335impl Mint {
336 pub async fn note_distribution_ui(&self) -> BTreeMap<Denomination, u64> {
337 self.db
338 .begin_transaction_nc()
339 .await
340 .find_by_prefix(&IssuanceCounterPrefix)
341 .await
342 .filter(|entry| std::future::ready(entry.1 > 0))
343 .map(|(key, count)| (key.0, count))
344 .collect()
345 .await
346 }
347}
348
349#[apply(async_trait_maybe_send!)]
350impl ServerModule for Mint {
351 type Common = MintModuleTypes;
352 type Init = MintInit;
353
354 async fn consensus_proposal(
355 &self,
356 _dbtx: &mut DatabaseTransaction<'_>,
357 ) -> Vec<MintConsensusItem> {
358 Vec::new()
359 }
360
361 async fn process_consensus_item<'a, 'b>(
362 &'a self,
363 _dbtx: &mut DatabaseTransaction<'b>,
364 _consensus_item: MintConsensusItem,
365 _peer_id: PeerId,
366 ) -> anyhow::Result<()> {
367 bail!("Mint does not process consensus items");
368 }
369
370 async fn process_input<'a, 'b, 'c>(
371 &'a self,
372 dbtx: &mut DatabaseTransaction<'c>,
373 input: &'b MintInput,
374 _in_point: InPoint,
375 ) -> Result<InputMeta, MintInputError> {
376 let input = input.ensure_v0_ref()?;
377
378 let pk = self
379 .cfg
380 .consensus
381 .tbs_agg_pks
382 .get(&input.note.denomination)
383 .ok_or(MintInputError::InvalidDenomination)?;
384
385 if !verify_note(input.note, *pk) {
386 return Err(MintInputError::InvalidSignature);
387 }
388
389 if dbtx
390 .insert_entry(&NonceKey(input.note.nonce), &())
391 .await
392 .is_some()
393 {
394 return Err(MintInputError::SpentCoin);
395 }
396
397 let new_count = dbtx
398 .remove_entry(&IssuanceCounterKey(input.note.denomination))
399 .await
400 .unwrap_or(0)
401 .checked_sub(1)
402 .expect("Failed to decrement issuance counter");
403
404 dbtx.insert_new_entry(&IssuanceCounterKey(input.note.denomination), &new_count)
405 .await;
406
407 let next_index = get_recovery_count(dbtx).await;
408
409 dbtx.insert_new_entry(
410 &RecoveryItemKey(next_index),
411 &RecoveryItem::Input {
412 nonce_hash: input.note.nonce.consensus_hash(),
413 },
414 )
415 .await;
416
417 let amount = input.note.amount();
418 let unit = self.cfg.consensus.amount_unit;
419
420 Ok(InputMeta {
421 amount: TransactionItemAmounts {
422 amounts: Amounts::new_custom(unit, amount),
423 fees: Amounts::new_custom(unit, self.cfg.consensus.fee_consensus.fee(amount)),
424 },
425 pub_key: input.note.nonce,
426 })
427 }
428
429 async fn process_output<'a, 'b>(
430 &'a self,
431 dbtx: &mut DatabaseTransaction<'b>,
432 output: &'a MintOutput,
433 outpoint: OutPoint,
434 ) -> Result<TransactionItemAmounts, MintOutputError> {
435 let output = output.ensure_v0_ref()?;
436
437 let signature = self
438 .cfg
439 .private
440 .tbs_sks
441 .get(&output.denomination)
442 .map(|key| tbs::sign_message(output.nonce, *key))
443 .ok_or(MintOutputError::InvalidDenomination)?;
444
445 dbtx.insert_entry(&BlindedSignatureShareKey(outpoint), &signature)
447 .await;
448
449 dbtx.insert_entry(&BlindedSignatureShareRecoveryKey(output.nonce), &signature)
451 .await;
452
453 let new_count = dbtx
454 .remove_entry(&IssuanceCounterKey(output.denomination))
455 .await
456 .unwrap_or(0)
457 .checked_add(1)
458 .expect("Failed to increment issuance counter");
459
460 dbtx.insert_new_entry(&IssuanceCounterKey(output.denomination), &new_count)
461 .await;
462
463 let next_index = get_recovery_count(dbtx).await;
464
465 dbtx.insert_new_entry(
466 &RecoveryItemKey(next_index),
467 &RecoveryItem::Output {
468 denomination: output.denomination,
469 nonce_hash: output.nonce.consensus_hash(),
470 tweak: output.tweak,
471 },
472 )
473 .await;
474
475 let amount = output.amount();
476 let unit = self.cfg.consensus.amount_unit;
477
478 Ok(TransactionItemAmounts {
479 amounts: Amounts::new_custom(unit, amount),
480 fees: Amounts::new_custom(unit, self.cfg.consensus.fee_consensus.fee(amount)),
481 })
482 }
483
484 async fn output_status(
485 &self,
486 _dbtx: &mut DatabaseTransaction<'_>,
487 _outpoint: OutPoint,
488 ) -> Option<MintOutputOutcome> {
489 None
490 }
491
492 async fn audit(
493 &self,
494 dbtx: &mut DatabaseTransaction<'_>,
495 audit: &mut Audit,
496 module_instance_id: ModuleInstanceId,
497 ) {
498 audit
499 .add_items(dbtx, module_instance_id, &IssuanceCounterPrefix, |k, v| {
500 -((k.0.amount().msats * v) as i64)
501 })
502 .await;
503 }
504
505 fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
506 vec![
507 api_endpoint! {
508 SIGNATURE_SHARES_ENDPOINT,
509 ApiVersion::new(0, 1),
510 async |_module: &Mint, context, range: fedimint_core::OutPointRange| -> Vec<BlindedSignatureShare> {
511 let db = context.db();
512 let mut dbtx = db.begin_transaction_nc().await;
513 Ok(get_signature_shares(&mut dbtx, range).await)
514 }
515 },
516 api_endpoint! {
517 SIGNATURE_SHARES_RECOVERY_ENDPOINT,
518 ApiVersion::new(0, 1),
519 async |_module: &Mint, context, messages: Vec<tbs::BlindedMessage>| -> Vec<BlindedSignatureShare> {
520 let db = context.db();
521 let mut dbtx = db.begin_transaction_nc().await;
522 get_signature_shares_recovery(&mut dbtx, messages).await
523 }
524 },
525 api_endpoint! {
526 RECOVERY_SLICE_ENDPOINT,
527 ApiVersion::new(0, 1),
528 async |_module: &Mint, context, range: (u64, u64)| -> Vec<RecoveryItem> {
529 let db = context.db();
530 let mut dbtx = db.begin_transaction_nc().await;
531 Ok(get_recovery_slice(&mut dbtx, range).await)
532 }
533 },
534 api_endpoint! {
535 RECOVERY_SLICE_HASH_ENDPOINT,
536 ApiVersion::new(0, 1),
537 async |_module: &Mint, context, range: (u64, u64)| -> bitcoin::hashes::sha256::Hash {
538 let db = context.db();
539 let mut dbtx = db.begin_transaction_nc().await;
540 Ok(get_recovery_slice(&mut dbtx, range).await.consensus_hash())
541 }
542 },
543 api_endpoint! {
544 RECOVERY_COUNT_ENDPOINT,
545 ApiVersion::new(0, 1),
546 async |_module: &Mint, context, _params: ()| -> u64 {
547 let db = context.db();
548 let mut dbtx = db.begin_transaction_nc().await;
549 Ok(get_recovery_count(&mut dbtx).await)
550 }
551 },
552 ]
553 }
554}
555
556async fn get_signature_shares(
557 dbtx: &mut DatabaseTransaction<'_>,
558 range: fedimint_core::OutPointRange,
559) -> Vec<BlindedSignatureShare> {
560 let start_key = BlindedSignatureShareKey(range.start_out_point());
561 let end_key = BlindedSignatureShareKey(range.end_out_point());
562
563 dbtx.find_by_range(start_key..end_key)
564 .await
565 .map(|entry| entry.1)
566 .collect()
567 .await
568}
569
570async fn get_signature_shares_recovery(
571 dbtx: &mut DatabaseTransaction<'_>,
572 messages: Vec<tbs::BlindedMessage>,
573) -> Result<Vec<BlindedSignatureShare>, ApiError> {
574 let mut shares = Vec::new();
575
576 for message in messages {
577 let share = dbtx
578 .get_value(&BlindedSignatureShareRecoveryKey(message))
579 .await
580 .ok_or(ApiError::bad_request(
581 "No blinded signature share found".to_string(),
582 ))?;
583
584 shares.push(share);
585 }
586
587 Ok(shares)
588}
589
590async fn get_recovery_count(dbtx: &mut DatabaseTransaction<'_>) -> u64 {
591 dbtx.find_by_prefix_sorted_descending(&RecoveryItemPrefix)
592 .await
593 .next()
594 .await
595 .map_or(0, |entry| entry.0.0 + 1)
596}
597
598async fn get_recovery_slice(
599 dbtx: &mut DatabaseTransaction<'_>,
600 range: (u64, u64),
601) -> Vec<RecoveryItem> {
602 dbtx.find_by_range(RecoveryItemKey(range.0)..RecoveryItemKey(range.1))
603 .await
604 .map(|entry| entry.1)
605 .collect()
606 .await
607}