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