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