1#![deny(clippy::pedantic)]
2#![allow(clippy::similar_names)]
3#![allow(clippy::cast_possible_truncation)]
4#![allow(clippy::cast_possible_wrap)]
5#![allow(clippy::default_trait_access)]
6#![allow(clippy::missing_errors_doc)]
7#![allow(clippy::missing_panics_doc)]
8#![allow(clippy::module_name_repetitions)]
9#![allow(clippy::must_use_candidate)]
10#![allow(clippy::single_match_else)]
11#![allow(clippy::too_many_lines)]
12
13pub mod db;
14
15use std::collections::{BTreeMap, BTreeSet};
16
17use anyhow::{Context, anyhow, bail, ensure};
18use bitcoin::absolute::LockTime;
19use bitcoin::hashes::{Hash, sha256};
20use bitcoin::secp256k1::Secp256k1;
21use bitcoin::sighash::{EcdsaSighashType, SighashCache};
22use bitcoin::transaction::Version;
23use bitcoin::{Amount, Network, Sequence, Transaction, TxIn, TxOut, Txid};
24use common::config::WalletConfigConsensus;
25use common::{
26 DepositRange, WalletCommonInit, WalletConsensusItem, WalletInput, WalletModuleTypes,
27 WalletOutput, WalletOutputOutcome,
28};
29use db::{
30 DbKeyPrefix, Deposit, DepositKey, DepositPrefix, FederationWalletKey, FederationWalletPrefix,
31 SignaturesKey, SignaturesPrefix, SignaturesTxidPrefix, SpentDepositKey, SpentDepositPrefix,
32 TxInfoIndexKey, TxInfoIndexPrefix,
33};
34use fedimint_core::config::{
35 ServerModuleConfig, ServerModuleConsensusConfig, TypedServerModuleConfig,
36 TypedServerModuleConsensusConfig,
37};
38use fedimint_core::core::ModuleInstanceId;
39use fedimint_core::db::{
40 Database, DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped,
41};
42use fedimint_core::encoding::{Decodable, Encodable};
43use fedimint_core::envs::{FM_ENABLE_MODULE_WALLETV2_ENV, is_env_var_set_opt};
44use fedimint_core::module::audit::Audit;
45use fedimint_core::module::{
46 Amounts, ApiEndpoint, ApiVersion, CORE_CONSENSUS_VERSION, CoreConsensusVersion, InputMeta,
47 ModuleConsensusVersion, ModuleInit, SupportedModuleApiVersions, TransactionItemAmounts,
48 api_endpoint,
49};
50#[cfg(not(target_family = "wasm"))]
51use fedimint_core::task::TaskGroup;
52use fedimint_core::task::sleep;
53use fedimint_core::{
54 InPoint, NumPeersExt, OutPoint, PeerId, apply, async_trait_maybe_send, push_db_pair_items, util,
55};
56use fedimint_logging::LOG_MODULE_WALLETV2;
57use fedimint_server_core::bitcoin_rpc::ServerBitcoinRpcMonitor;
58use fedimint_server_core::config::{PeerHandleOps, PeerHandleOpsExt};
59use fedimint_server_core::migration::ServerModuleDbMigrationFn;
60use fedimint_server_core::{
61 ConfigGenModuleArgs, ServerModule, ServerModuleInit, ServerModuleInitArgs,
62};
63pub use fedimint_walletv2_common as common;
64use fedimint_walletv2_common::config::{
65 FeeConsensus, WalletClientConfig, WalletConfig, WalletConfigPrivate,
66};
67use fedimint_walletv2_common::endpoint_constants::{
68 CONSENSUS_BLOCK_COUNT_ENDPOINT, CONSENSUS_FEERATE_ENDPOINT, DEPOSIT_RANGE_ENDPOINT,
69 FEDERATION_WALLET_ENDPOINT, PENDING_TRANSACTION_CHAIN_ENDPOINT, RECEIVE_FEE_ENDPOINT,
70 SEND_FEE_ENDPOINT, TRANSACTION_CHAIN_ENDPOINT, TRANSACTION_ID_ENDPOINT,
71};
72use fedimint_walletv2_common::{
73 FederationWallet, MODULE_CONSENSUS_VERSION, TxInfo, WalletInputError, WalletOutputError,
74 descriptor, is_potential_receive, tweak_public_key,
75};
76use futures::StreamExt;
77use miniscript::descriptor::Wsh;
78use rand::rngs::OsRng;
79use secp256k1::ecdsa::Signature;
80use secp256k1::{PublicKey, Scalar, SecretKey};
81use serde::{Deserialize, Serialize};
82use strum::IntoEnumIterator;
83use tracing::info;
84
85use crate::db::{
86 BlockCountVoteKey, BlockCountVotePrefix, FeeRateVoteKey, FeeRateVotePrefix, TxInfoKey,
87 TxInfoPrefix, UnconfirmedTxKey, UnconfirmedTxPrefix, UnsignedTxKey, UnsignedTxPrefix,
88};
89
90pub const CONFIRMATION_FINALITY_DELAY: u64 = 6;
94
95const MAX_BLOCK_COUNT_INCREMENT: u64 = 5;
98
99#[derive(Clone, Debug, Eq, PartialEq, Serialize, Encodable, Decodable)]
100pub struct FederationTx {
101 pub tx: Transaction,
102 pub spent_tx_outs: Vec<SpentTxOut>,
103 pub vbytes: u64,
104 pub fee: Amount,
105}
106
107#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
108pub struct SpentTxOut {
109 pub value: Amount,
110 pub tweak: sha256::Hash,
111}
112
113async fn pending_txs_unordered(dbtx: &mut DatabaseTransaction<'_>) -> Vec<FederationTx> {
114 let unsigned: Vec<FederationTx> = dbtx
115 .find_by_prefix(&UnsignedTxPrefix)
116 .await
117 .map(|entry| entry.1)
118 .collect()
119 .await;
120
121 let unconfirmed: Vec<FederationTx> = dbtx
122 .find_by_prefix(&UnconfirmedTxPrefix)
123 .await
124 .map(|entry| entry.1)
125 .collect()
126 .await;
127
128 unsigned.into_iter().chain(unconfirmed).collect()
129}
130
131#[derive(Debug, Clone)]
132pub struct WalletInit;
133
134impl ModuleInit for WalletInit {
135 type Common = WalletCommonInit;
136
137 async fn dump_database(
138 &self,
139 dbtx: &mut DatabaseTransaction<'_>,
140 prefix_names: Vec<String>,
141 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
142 let mut wallet: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
143
144 let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
145 prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
146 });
147
148 for table in filtered_prefixes {
149 match table {
150 DbKeyPrefix::Deposit => {
151 push_db_pair_items!(
152 dbtx,
153 DepositPrefix,
154 DepositKey,
155 Deposit,
156 wallet,
157 "Wallet Deposits"
158 );
159 }
160 DbKeyPrefix::SpentDeposit => {
161 push_db_pair_items!(
162 dbtx,
163 SpentDepositPrefix,
164 SpentDepositKey,
165 (),
166 wallet,
167 "Wallet Spent Deposits"
168 );
169 }
170 DbKeyPrefix::BlockCountVote => {
171 push_db_pair_items!(
172 dbtx,
173 BlockCountVotePrefix,
174 BlockCountVoteKey,
175 u64,
176 wallet,
177 "Wallet Block Count Votes"
178 );
179 }
180 DbKeyPrefix::FeeRateVote => {
181 push_db_pair_items!(
182 dbtx,
183 FeeRateVotePrefix,
184 FeeRateVoteKey,
185 Option<u64>,
186 wallet,
187 "Wallet Fee Rate Votes"
188 );
189 }
190 DbKeyPrefix::TxLog => {
191 push_db_pair_items!(
192 dbtx,
193 TxInfoPrefix,
194 TxInfoKey,
195 TxInfo,
196 wallet,
197 "Wallet Tx Log"
198 );
199 }
200 DbKeyPrefix::TxInfoIndex => {
201 push_db_pair_items!(
202 dbtx,
203 TxInfoIndexPrefix,
204 TxInfoIndexKey,
205 u64,
206 wallet,
207 "Wallet Tx Info Index"
208 );
209 }
210 DbKeyPrefix::UnsignedTx => {
211 push_db_pair_items!(
212 dbtx,
213 UnsignedTxPrefix,
214 UnsignedTxKey,
215 FederationTx,
216 wallet,
217 "Wallet Unsigned Transactions"
218 );
219 }
220 DbKeyPrefix::Signatures => {
221 push_db_pair_items!(
222 dbtx,
223 SignaturesPrefix,
224 SignaturesKey,
225 Vec<Signature>,
226 wallet,
227 "Wallet Signatures"
228 );
229 }
230 DbKeyPrefix::UnconfirmedTx => {
231 push_db_pair_items!(
232 dbtx,
233 UnconfirmedTxPrefix,
234 UnconfirmedTxKey,
235 FederationTx,
236 wallet,
237 "Wallet Unconfirmed Transactions"
238 );
239 }
240 DbKeyPrefix::FederationWallet => {
241 push_db_pair_items!(
242 dbtx,
243 FederationWalletPrefix,
244 FederationWalletKey,
245 FederationWallet,
246 wallet,
247 "Federation Wallet"
248 );
249 }
250 }
251 }
252
253 Box::new(wallet.into_iter())
254 }
255}
256
257#[apply(async_trait_maybe_send!)]
258impl ServerModuleInit for WalletInit {
259 type Module = Wallet;
260
261 fn versions(&self, _core: CoreConsensusVersion) -> &[ModuleConsensusVersion] {
262 &[MODULE_CONSENSUS_VERSION]
263 }
264
265 fn supported_api_versions(&self) -> SupportedModuleApiVersions {
266 SupportedModuleApiVersions::from_raw(
267 (CORE_CONSENSUS_VERSION.major, CORE_CONSENSUS_VERSION.minor),
268 (
269 MODULE_CONSENSUS_VERSION.major,
270 MODULE_CONSENSUS_VERSION.minor,
271 ),
272 &[(0, 1)],
273 )
274 }
275
276 fn is_enabled_by_default(&self) -> bool {
277 is_env_var_set_opt(FM_ENABLE_MODULE_WALLETV2_ENV).unwrap_or(false)
278 }
279
280 async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
281 Ok(Wallet::new(
282 args.cfg().to_typed()?,
283 args.db(),
284 args.task_group(),
285 args.server_bitcoin_rpc_monitor(),
286 ))
287 }
288
289 fn trusted_dealer_gen(
290 &self,
291 peers: &[PeerId],
292 args: &ConfigGenModuleArgs,
293 ) -> BTreeMap<PeerId, ServerModuleConfig> {
294 let fee_consensus = FeeConsensus::new(0).expect("Relative fee is within range");
295
296 let bitcoin_sks = peers
297 .iter()
298 .map(|peer| (*peer, SecretKey::new(&mut secp256k1::rand::thread_rng())))
299 .collect::<BTreeMap<PeerId, SecretKey>>();
300
301 let bitcoin_pks = bitcoin_sks
302 .iter()
303 .map(|(peer, sk)| (*peer, sk.public_key(secp256k1::SECP256K1)))
304 .collect::<BTreeMap<PeerId, PublicKey>>();
305
306 bitcoin_sks
307 .into_iter()
308 .map(|(peer, bitcoin_sk)| {
309 let config = WalletConfig {
310 private: WalletConfigPrivate { bitcoin_sk },
311 consensus: WalletConfigConsensus::new(
312 bitcoin_pks.clone(),
313 fee_consensus.clone(),
314 args.network,
315 ),
316 };
317
318 (peer, config.to_erased())
319 })
320 .collect()
321 }
322
323 async fn distributed_gen(
324 &self,
325 peers: &(dyn PeerHandleOps + Send + Sync),
326 args: &ConfigGenModuleArgs,
327 ) -> anyhow::Result<ServerModuleConfig> {
328 let fee_consensus = FeeConsensus::new(0).expect("Relative fee is within range");
329
330 let (bitcoin_sk, bitcoin_pk) = secp256k1::generate_keypair(&mut OsRng);
331
332 let bitcoin_pks: BTreeMap<PeerId, PublicKey> = peers
333 .exchange_encodable(bitcoin_pk)
334 .await?
335 .into_iter()
336 .collect();
337
338 let config = WalletConfig {
339 private: WalletConfigPrivate { bitcoin_sk },
340 consensus: WalletConfigConsensus::new(bitcoin_pks, fee_consensus, args.network),
341 };
342
343 Ok(config.to_erased())
344 }
345
346 fn validate_config(&self, identity: &PeerId, config: ServerModuleConfig) -> anyhow::Result<()> {
347 let config = config.to_typed::<WalletConfig>()?;
348
349 ensure!(
350 config
351 .consensus
352 .bitcoin_pks
353 .get(identity)
354 .ok_or(anyhow::anyhow!("No public key for our identity"))?
355 == &config.private.bitcoin_sk.public_key(secp256k1::SECP256K1),
356 "Bitcoin wallet private key doesn't match multisig pubkey"
357 );
358
359 Ok(())
360 }
361
362 fn get_client_config(
363 &self,
364 config: &ServerModuleConsensusConfig,
365 ) -> anyhow::Result<WalletClientConfig> {
366 let config = WalletConfigConsensus::from_erased(config)?;
367 Ok(WalletClientConfig {
368 bitcoin_pks: config.bitcoin_pks,
369 dust_limit: config.dust_limit,
370 fee_consensus: config.fee_consensus,
371 network: config.network,
372 })
373 }
374
375 fn get_database_migrations(
376 &self,
377 ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Wallet>> {
378 BTreeMap::new()
379 }
380
381 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
382 Some(DbKeyPrefix::iter().map(|p| p as u8).collect())
383 }
384}
385
386#[apply(async_trait_maybe_send!)]
387impl ServerModule for Wallet {
388 type Common = WalletModuleTypes;
389 type Init = WalletInit;
390
391 async fn consensus_proposal<'a>(
392 &'a self,
393 dbtx: &mut DatabaseTransaction<'_>,
394 ) -> Vec<WalletConsensusItem> {
395 let mut items = dbtx
396 .find_by_prefix(&UnsignedTxPrefix)
397 .await
398 .map(|(key, unsigned_tx)| {
399 let signatures = self.sign_tx(&unsigned_tx);
400
401 assert!(
402 self.verify_signatures(
403 &unsigned_tx,
404 &signatures,
405 self.cfg.private.bitcoin_sk.public_key(secp256k1::SECP256K1)
406 )
407 .is_ok(),
408 "Our signatures failed verification against our private key"
409 );
410
411 WalletConsensusItem::Signatures(key.0, signatures)
412 })
413 .collect::<Vec<WalletConsensusItem>>()
414 .await;
415
416 if let Some(status) = self.btc_rpc.status() {
417 assert_eq!(status.network, self.cfg.consensus.network);
418
419 let consensus_block_count = self.consensus_block_count(dbtx).await;
420
421 items.push(WalletConsensusItem::BlockCount(
422 status
423 .block_count
424 .saturating_sub(CONFIRMATION_FINALITY_DELAY)
425 .min(consensus_block_count + MAX_BLOCK_COUNT_INCREMENT),
426 ));
427
428 items.push(WalletConsensusItem::Feerate(Some(
429 status.fee_rate.sats_per_kvb,
430 )));
431 } else {
432 items.push(WalletConsensusItem::Feerate(None));
434 }
435
436 items
437 }
438
439 async fn process_consensus_item<'a, 'b>(
440 &'a self,
441 dbtx: &mut DatabaseTransaction<'b>,
442 consensus_item: WalletConsensusItem,
443 peer: PeerId,
444 ) -> anyhow::Result<()> {
445 match consensus_item {
446 WalletConsensusItem::BlockCount(block_count_vote) => {
447 self.process_block_count(dbtx, block_count_vote, peer).await
448 }
449 WalletConsensusItem::Feerate(feerate) => {
450 if Some(feerate) == dbtx.insert_entry(&FeeRateVoteKey(peer), &feerate).await {
451 return Err(anyhow!("Fee rate vote is redundant"));
452 }
453
454 Ok(())
455 }
456 WalletConsensusItem::Signatures(txid, signatures) => {
457 self.process_signatures(dbtx, txid, signatures, peer).await
458 }
459 WalletConsensusItem::Default { variant, .. } => Err(anyhow!(
460 "Received wallet consensus item with unknown variant {variant}"
461 )),
462 }
463 }
464
465 async fn process_input<'a, 'b, 'c>(
466 &'a self,
467 dbtx: &mut DatabaseTransaction<'c>,
468 input: &'b WalletInput,
469 _in_point: InPoint,
470 ) -> Result<InputMeta, WalletInputError> {
471 let input = input.ensure_v0_ref()?;
472
473 if dbtx
474 .insert_entry(&SpentDepositKey(input.deposit_index), &())
475 .await
476 .is_some()
477 {
478 return Err(WalletInputError::DepositAlreadySpent);
479 }
480
481 let Deposit(tracked_outpoint, tracked_out) = dbtx
482 .get_value(&DepositKey(input.deposit_index))
483 .await
484 .ok_or(WalletInputError::UnknownDepositIndex)?;
485
486 let tweaked_pubkey = self
487 .descriptor(&input.tweak.consensus_hash())
488 .script_pubkey();
489
490 if tracked_out.script_pubkey != tweaked_pubkey {
491 return Err(WalletInputError::WrongTweak);
492 }
493
494 let consensus_receive_fee = self
495 .receive_fee(dbtx)
496 .await
497 .ok_or(WalletInputError::NoConsensusFeerateAvailable)?;
498
499 if input.fee < consensus_receive_fee {
504 return Err(WalletInputError::InsufficientTotalFee);
505 }
506
507 let deposit_value = tracked_out
508 .value
509 .checked_sub(input.fee)
510 .ok_or(WalletInputError::ArithmeticOverflow)?;
511
512 if let Some(wallet) = dbtx.remove_entry(&FederationWalletKey).await {
513 let change_value = wallet
517 .value
518 .checked_add(deposit_value)
519 .ok_or(WalletInputError::ArithmeticOverflow)?;
520
521 let tx = Transaction {
522 version: Version(2),
523 lock_time: LockTime::ZERO,
524 input: vec![
525 TxIn {
526 previous_output: wallet.outpoint,
527 script_sig: Default::default(),
528 sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
529 witness: bitcoin::Witness::new(),
530 },
531 TxIn {
532 previous_output: tracked_outpoint,
533 script_sig: Default::default(),
534 sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
535 witness: bitcoin::Witness::new(),
536 },
537 ],
538 output: vec![TxOut {
539 value: change_value,
540 script_pubkey: self.descriptor(&wallet.consensus_hash()).script_pubkey(),
541 }],
542 };
543
544 dbtx.insert_new_entry(
545 &FederationWalletKey,
546 &FederationWallet {
547 value: change_value,
548 outpoint: bitcoin::OutPoint {
549 txid: tx.compute_txid(),
550 vout: 0,
551 },
552 tweak: wallet.consensus_hash(),
553 },
554 )
555 .await;
556
557 let tx_index = self.total_txs(dbtx).await;
558
559 let created = self.consensus_block_count(dbtx).await;
560
561 dbtx.insert_new_entry(
562 &TxInfoKey(tx_index),
563 &TxInfo {
564 index: tx_index,
565 txid: tx.compute_txid(),
566 input: wallet.value,
567 output: change_value,
568 vbytes: self.cfg.consensus.receive_tx_vbytes,
569 fee: input.fee,
570 created,
571 },
572 )
573 .await;
574
575 dbtx.insert_new_entry(
576 &UnsignedTxKey(tx.compute_txid()),
577 &FederationTx {
578 tx,
579 spent_tx_outs: vec![
580 SpentTxOut {
581 value: wallet.value,
582 tweak: wallet.tweak,
583 },
584 SpentTxOut {
585 value: tracked_out.value,
586 tweak: input.tweak.consensus_hash(),
587 },
588 ],
589 vbytes: self.cfg.consensus.receive_tx_vbytes,
590 fee: input.fee,
591 },
592 )
593 .await;
594 } else {
595 dbtx.insert_new_entry(
596 &FederationWalletKey,
597 &FederationWallet {
598 value: tracked_out.value,
599 outpoint: tracked_outpoint,
600 tweak: input.tweak.consensus_hash(),
601 },
602 )
603 .await;
604 }
605
606 let amount = deposit_value
607 .to_sat()
608 .checked_mul(1000)
609 .map(fedimint_core::Amount::from_msats)
610 .ok_or(WalletInputError::ArithmeticOverflow)?;
611
612 Ok(InputMeta {
613 amount: TransactionItemAmounts {
614 amounts: Amounts::new_bitcoin(amount),
615 fees: Amounts::new_bitcoin(self.cfg.consensus.fee_consensus.fee(amount)),
616 },
617 pub_key: input.tweak,
618 })
619 }
620
621 async fn process_output<'a, 'b>(
622 &'a self,
623 dbtx: &mut DatabaseTransaction<'b>,
624 output: &'a WalletOutput,
625 outpoint: OutPoint,
626 ) -> Result<TransactionItemAmounts, WalletOutputError> {
627 let output = output.ensure_v0_ref()?;
628
629 if output.value < self.cfg.consensus.dust_limit {
630 return Err(WalletOutputError::UnderDustLimit);
631 }
632
633 let wallet = dbtx
634 .remove_entry(&FederationWalletKey)
635 .await
636 .ok_or(WalletOutputError::NoFederationUTXO)?;
637
638 let consensus_send_fee = self
639 .send_fee(dbtx)
640 .await
641 .ok_or(WalletOutputError::NoConsensusFeerateAvailable)?;
642
643 if output.fee < consensus_send_fee {
648 return Err(WalletOutputError::InsufficientTotalFee);
649 }
650
651 let output_value = output
652 .value
653 .checked_add(output.fee)
654 .ok_or(WalletOutputError::ArithmeticOverflow)?;
655
656 let change_value = wallet
657 .value
658 .checked_sub(output_value)
659 .ok_or(WalletOutputError::ArithmeticOverflow)?;
660
661 if change_value < self.cfg.consensus.dust_limit {
662 return Err(WalletOutputError::ChangeUnderDustLimit);
663 }
664
665 let script_pubkey = output
666 .destination
667 .script_pubkey()
668 .ok_or(WalletOutputError::UnknownScriptVariant)?;
669
670 let tx = Transaction {
671 version: Version(2),
672 lock_time: LockTime::ZERO,
673 input: vec![TxIn {
674 previous_output: wallet.outpoint,
675 script_sig: Default::default(),
676 sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
677 witness: bitcoin::Witness::new(),
678 }],
679 output: vec![
680 TxOut {
681 value: change_value,
682 script_pubkey: self.descriptor(&wallet.consensus_hash()).script_pubkey(),
683 },
684 TxOut {
685 value: output.value,
686 script_pubkey,
687 },
688 ],
689 };
690
691 dbtx.insert_new_entry(
692 &FederationWalletKey,
693 &FederationWallet {
694 value: change_value,
695 outpoint: bitcoin::OutPoint {
696 txid: tx.compute_txid(),
697 vout: 0,
698 },
699 tweak: wallet.consensus_hash(),
700 },
701 )
702 .await;
703
704 let tx_index = self.total_txs(dbtx).await;
705
706 let created = self.consensus_block_count(dbtx).await;
707
708 dbtx.insert_new_entry(
709 &TxInfoKey(tx_index),
710 &TxInfo {
711 index: tx_index,
712 txid: tx.compute_txid(),
713 input: wallet.value,
714 output: change_value,
715 vbytes: self.cfg.consensus.send_tx_vbytes,
716 fee: output.fee,
717 created,
718 },
719 )
720 .await;
721
722 dbtx.insert_new_entry(&TxInfoIndexKey(outpoint), &tx_index)
723 .await;
724
725 dbtx.insert_new_entry(
726 &UnsignedTxKey(tx.compute_txid()),
727 &FederationTx {
728 tx,
729 spent_tx_outs: vec![SpentTxOut {
730 value: wallet.value,
731 tweak: wallet.tweak,
732 }],
733 vbytes: self.cfg.consensus.send_tx_vbytes,
734 fee: output.fee,
735 },
736 )
737 .await;
738
739 let amount = output_value
740 .to_sat()
741 .checked_mul(1000)
742 .map(fedimint_core::Amount::from_msats)
743 .ok_or(WalletOutputError::ArithmeticOverflow)?;
744
745 Ok(TransactionItemAmounts {
746 amounts: Amounts::new_bitcoin(amount),
747 fees: Amounts::new_bitcoin(self.cfg.consensus.fee_consensus.fee(amount)),
748 })
749 }
750
751 async fn output_status(
752 &self,
753 _dbtx: &mut DatabaseTransaction<'_>,
754 _outpoint: OutPoint,
755 ) -> Option<WalletOutputOutcome> {
756 None
757 }
758
759 async fn audit(
760 &self,
761 dbtx: &mut DatabaseTransaction<'_>,
762 audit: &mut Audit,
763 module_instance_id: ModuleInstanceId,
764 ) {
765 audit
766 .add_items(
767 dbtx,
768 module_instance_id,
769 &FederationWalletPrefix,
770 |_, wallet| 1000 * wallet.value.to_sat() as i64,
771 )
772 .await;
773 }
774
775 fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
776 vec![
777 api_endpoint! {
778 CONSENSUS_BLOCK_COUNT_ENDPOINT,
779 ApiVersion::new(0, 0),
780 async |module: &Wallet, context, _params: ()| -> u64 {
781 let db = context.db();
782 let mut dbtx = db.begin_transaction_nc().await;
783 Ok(module.consensus_block_count(&mut dbtx).await)
784 }
785 },
786 api_endpoint! {
787 CONSENSUS_FEERATE_ENDPOINT,
788 ApiVersion::new(0, 0),
789 async |module: &Wallet, context, _params: ()| -> Option<u64> {
790 let db = context.db();
791 let mut dbtx = db.begin_transaction_nc().await;
792 Ok(module.consensus_feerate(&mut dbtx).await)
793 }
794 },
795 api_endpoint! {
796 FEDERATION_WALLET_ENDPOINT,
797 ApiVersion::new(0, 0),
798 async |_module: &Wallet, context, _params: ()| -> Option<FederationWallet> {
799 let db = context.db();
800 let mut dbtx = db.begin_transaction_nc().await;
801 Ok(dbtx.get_value(&FederationWalletKey).await)
802 }
803 },
804 api_endpoint! {
805 SEND_FEE_ENDPOINT,
806 ApiVersion::new(0, 0),
807 async |module: &Wallet, context, _params: ()| -> Option<Amount> {
808 let db = context.db();
809 let mut dbtx = db.begin_transaction_nc().await;
810 Ok(module.send_fee(&mut dbtx).await)
811 }
812 },
813 api_endpoint! {
814 RECEIVE_FEE_ENDPOINT,
815 ApiVersion::new(0, 0),
816 async |module: &Wallet, context, _params: ()| -> Option<Amount> {
817 let db = context.db();
818 let mut dbtx = db.begin_transaction_nc().await;
819 Ok(module.receive_fee(&mut dbtx).await)
820 }
821 },
822 api_endpoint! {
823 TRANSACTION_ID_ENDPOINT,
824 ApiVersion::new(0, 0),
825 async |module: &Wallet, context, params: OutPoint| -> Option<Txid> {
826 let db = context.db();
827 let mut dbtx = db.begin_transaction_nc().await;
828 Ok(module.tx_id(&mut dbtx, params).await)
829 }
830 },
831 api_endpoint! {
832 DEPOSIT_RANGE_ENDPOINT,
833 ApiVersion::new(0, 0),
834 async |module: &Wallet, context, params: (u64, u64)| -> DepositRange {
835 let db = context.db();
836 let mut dbtx = db.begin_transaction_nc().await;
837 Ok(module.get_deposits(&mut dbtx, params.0, params.1).await)
838 }
839 },
840 api_endpoint! {
841 PENDING_TRANSACTION_CHAIN_ENDPOINT,
842 ApiVersion::new(0, 0),
843 async |module: &Wallet, context, _params: ()| -> Vec<TxInfo> {
844 let db = context.db();
845 let mut dbtx = db.begin_transaction_nc().await;
846 Ok(module.pending_tx_chain(&mut dbtx).await)
847 }
848 },
849 api_endpoint! {
850 TRANSACTION_CHAIN_ENDPOINT,
851 ApiVersion::new(0, 0),
852 async |module: &Wallet, context, params: usize| -> Vec<TxInfo> {
853 let db = context.db();
854 let mut dbtx = db.begin_transaction_nc().await;
855 Ok(module.tx_chain(&mut dbtx, params).await)
856 }
857 },
858 ]
859 }
860}
861
862#[derive(Debug)]
863pub struct Wallet {
864 cfg: WalletConfig,
865 db: Database,
866 btc_rpc: ServerBitcoinRpcMonitor,
867}
868
869impl Wallet {
870 fn new(
871 cfg: WalletConfig,
872 db: &Database,
873 task_group: &TaskGroup,
874 btc_rpc: ServerBitcoinRpcMonitor,
875 ) -> Wallet {
876 Self::spawn_broadcast_unconfirmed_txs_task(btc_rpc.clone(), db.clone(), task_group);
877
878 Wallet {
879 cfg,
880 btc_rpc,
881 db: db.clone(),
882 }
883 }
884
885 fn spawn_broadcast_unconfirmed_txs_task(
886 btc_rpc: ServerBitcoinRpcMonitor,
887 db: Database,
888 task_group: &TaskGroup,
889 ) {
890 task_group.spawn_cancellable("broadcast_unconfirmed_transactions", async move {
891 loop {
892 let unconfirmed_txs = db
893 .begin_transaction_nc()
894 .await
895 .find_by_prefix(&UnconfirmedTxPrefix)
896 .await
897 .map(|entry| entry.1)
898 .collect::<Vec<FederationTx>>()
899 .await;
900
901 for unconfirmed_tx in unconfirmed_txs {
902 btc_rpc.submit_transaction(unconfirmed_tx.tx).await;
903 }
904
905 sleep(common::sleep_duration()).await;
906 }
907 });
908 }
909
910 async fn process_block_count(
911 &self,
912 dbtx: &mut DatabaseTransaction<'_>,
913 block_count_vote: u64,
914 peer: PeerId,
915 ) -> anyhow::Result<()> {
916 let old_consensus_block_count = self.consensus_block_count(dbtx).await;
917
918 let current_vote = dbtx
919 .insert_entry(&BlockCountVoteKey(peer), &block_count_vote)
920 .await
921 .unwrap_or(0);
922
923 ensure!(
924 current_vote < block_count_vote,
925 "Block count vote is redundant"
926 );
927
928 let new_consensus_block_count = self.consensus_block_count(dbtx).await;
929
930 assert!(old_consensus_block_count <= new_consensus_block_count);
931
932 if old_consensus_block_count == 0 {
934 return Ok(());
935 }
936
937 self.await_local_sync_to_block_count(
940 new_consensus_block_count + CONFIRMATION_FINALITY_DELAY,
941 )
942 .await;
943
944 for height in old_consensus_block_count..new_consensus_block_count {
945 if let Some(status) = self.btc_rpc.status() {
947 assert_eq!(status.network, self.cfg.consensus.network);
948 }
949
950 let block_hash = util::retry(
951 "get_block_hash",
952 util::backoff_util::background_backoff(),
953 || self.btc_rpc.get_block_hash(height),
954 )
955 .await
956 .expect("Bitcoind rpc to get_block_hash failed");
957
958 let block = util::retry(
959 "get_block",
960 util::backoff_util::background_backoff(),
961 || self.btc_rpc.get_block(&block_hash),
962 )
963 .await
964 .expect("Bitcoind rpc to get_block failed");
965
966 assert_eq!(block.block_hash(), block_hash, "Block hash mismatch");
967
968 let pks_hash = self.cfg.consensus.bitcoin_pks.consensus_hash();
969
970 for tx in block.txdata {
971 dbtx.remove_entry(&UnconfirmedTxKey(tx.compute_txid()))
972 .await;
973
974 for (vout, tx_out) in tx.output.iter().enumerate() {
979 if is_potential_receive(&tx_out.script_pubkey, &pks_hash)
980 && tx_out.script_pubkey.is_p2wsh()
981 {
982 let outpoint = bitcoin::OutPoint {
983 txid: tx.compute_txid(),
984 vout: u32::try_from(vout)
985 .expect("Bitcoin transaction has more than u32::MAX outputs"),
986 };
987
988 let index = dbtx
989 .find_by_prefix_sorted_descending(&DepositPrefix)
990 .await
991 .next()
992 .await
993 .map_or(0, |entry| entry.0.0 + 1);
994
995 dbtx.insert_new_entry(
996 &DepositKey(index),
997 &Deposit(outpoint, tx_out.clone()),
998 )
999 .await;
1000 }
1001 }
1002 }
1003 }
1004
1005 Ok(())
1006 }
1007
1008 async fn process_signatures(
1009 &self,
1010 dbtx: &mut DatabaseTransaction<'_>,
1011 txid: bitcoin::Txid,
1012 signatures: Vec<Signature>,
1013 peer: PeerId,
1014 ) -> anyhow::Result<()> {
1015 let mut unsigned = dbtx
1016 .get_value(&UnsignedTxKey(txid))
1017 .await
1018 .context("Unsigned transaction does not exist")?;
1019
1020 let pk = self
1021 .cfg
1022 .consensus
1023 .bitcoin_pks
1024 .get(&peer)
1025 .expect("Failed to get public key of peer from config");
1026
1027 self.verify_signatures(&unsigned, &signatures, *pk)?;
1028
1029 if dbtx
1030 .insert_entry(&SignaturesKey(txid, peer), &signatures)
1031 .await
1032 .is_some()
1033 {
1034 bail!("Already received valid signatures from this peer")
1035 }
1036
1037 let signatures = dbtx
1038 .find_by_prefix(&SignaturesTxidPrefix(txid))
1039 .await
1040 .map(|(key, signatures)| (key.1, signatures))
1041 .collect::<BTreeMap<PeerId, Vec<Signature>>>()
1042 .await;
1043
1044 if signatures.len() == self.cfg.consensus.bitcoin_pks.to_num_peers().threshold() {
1045 dbtx.remove_entry(&UnsignedTxKey(txid)).await;
1046
1047 dbtx.remove_by_prefix(&SignaturesTxidPrefix(txid)).await;
1048
1049 self.finalize_tx(&mut unsigned, &signatures);
1050
1051 dbtx.insert_new_entry(&UnconfirmedTxKey(txid), &unsigned)
1052 .await;
1053
1054 self.btc_rpc.submit_transaction(unsigned.tx).await;
1055 }
1056
1057 Ok(())
1058 }
1059
1060 async fn await_local_sync_to_block_count(&self, block_count: u64) {
1061 loop {
1062 if self
1063 .btc_rpc
1064 .status()
1065 .is_some_and(|status| status.block_count >= block_count)
1066 {
1067 break;
1068 }
1069
1070 info!(target: LOG_MODULE_WALLETV2, "Waiting for local bitcoin backend to sync to block count {block_count}");
1071
1072 sleep(common::sleep_duration()).await;
1073 }
1074 }
1075
1076 pub async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1077 let num_peers = self.cfg.consensus.bitcoin_pks.to_num_peers();
1078
1079 let mut counts = dbtx
1080 .find_by_prefix(&BlockCountVotePrefix)
1081 .await
1082 .map(|entry| entry.1)
1083 .collect::<Vec<u64>>()
1084 .await;
1085
1086 assert!(counts.len() <= num_peers.total());
1087
1088 counts.sort_unstable();
1089
1090 counts.reverse();
1091
1092 assert!(counts.last() <= counts.first());
1093
1094 counts.get(num_peers.threshold() - 1).copied().unwrap_or(0)
1099 }
1100
1101 pub async fn consensus_feerate(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<u64> {
1102 let num_peers = self.cfg.consensus.bitcoin_pks.to_num_peers();
1103
1104 let mut rates = dbtx
1105 .find_by_prefix(&FeeRateVotePrefix)
1106 .await
1107 .filter_map(|entry| async move { entry.1 })
1108 .collect::<Vec<u64>>()
1109 .await;
1110
1111 assert!(rates.len() <= num_peers.total());
1112
1113 rates.sort_unstable();
1114
1115 assert!(rates.first() <= rates.last());
1116
1117 rates.get(num_peers.threshold() - 1).copied()
1118 }
1119
1120 pub async fn consensus_fee(
1121 &self,
1122 dbtx: &mut DatabaseTransaction<'_>,
1123 tx_vbytes: u64,
1124 ) -> Option<Amount> {
1125 let pending_txs = pending_txs_unordered(dbtx).await;
1129
1130 assert!(pending_txs.len() <= 32);
1131
1132 let feerate = self
1133 .consensus_feerate(dbtx)
1134 .await?
1135 .max(self.cfg.consensus.min_feerate << pending_txs.len());
1136
1137 let tx_fee = tx_vbytes.saturating_mul(feerate).saturating_div(1000);
1138
1139 let stack_vbytes = pending_txs
1140 .iter()
1141 .map(|t| t.vbytes)
1142 .try_fold(tx_vbytes, u64::checked_add)
1143 .expect("Stack vbytes overflow with at most 32 pending txs");
1144
1145 let stack_fee = stack_vbytes.saturating_mul(feerate).saturating_div(1000);
1146
1147 let stack_fee = pending_txs
1149 .iter()
1150 .map(|t| t.fee.to_sat())
1151 .fold(stack_fee, u64::saturating_sub);
1152
1153 Some(Amount::from_sat(tx_fee.max(stack_fee)))
1154 }
1155
1156 pub async fn send_fee(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<Amount> {
1157 self.consensus_fee(dbtx, self.cfg.consensus.send_tx_vbytes)
1158 .await
1159 }
1160
1161 pub async fn receive_fee(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<Amount> {
1162 self.consensus_fee(dbtx, self.cfg.consensus.receive_tx_vbytes)
1163 .await
1164 }
1165
1166 fn descriptor(&self, tweak: &sha256::Hash) -> Wsh<secp256k1::PublicKey> {
1167 descriptor(&self.cfg.consensus.bitcoin_pks, tweak)
1168 }
1169
1170 fn sign_tx(&self, unsigned_tx: &FederationTx) -> Vec<Signature> {
1171 let mut sighash_cache = SighashCache::new(unsigned_tx.tx.clone());
1172
1173 unsigned_tx
1174 .spent_tx_outs
1175 .iter()
1176 .enumerate()
1177 .map(|(index, utxo)| {
1178 let descriptor = self.descriptor(&utxo.tweak).ecdsa_sighash_script_code();
1179
1180 let p2wsh_sighash = sighash_cache
1181 .p2wsh_signature_hash(index, &descriptor, utxo.value, EcdsaSighashType::All)
1182 .expect("Failed to compute P2WSH segwit sighash");
1183
1184 let scalar = &Scalar::from_be_bytes(utxo.tweak.to_byte_array())
1185 .expect("Hash is within field order");
1186
1187 let sk = self
1188 .cfg
1189 .private
1190 .bitcoin_sk
1191 .add_tweak(scalar)
1192 .expect("Failed to tweak bitcoin secret key");
1193
1194 Secp256k1::new().sign_ecdsa(&p2wsh_sighash.into(), &sk)
1195 })
1196 .collect()
1197 }
1198
1199 fn verify_signatures(
1200 &self,
1201 unsigned_tx: &FederationTx,
1202 signatures: &[Signature],
1203 pk: PublicKey,
1204 ) -> anyhow::Result<()> {
1205 ensure!(
1206 unsigned_tx.spent_tx_outs.len() == signatures.len(),
1207 "Incorrect number of signatures"
1208 );
1209
1210 let mut sighash_cache = SighashCache::new(unsigned_tx.tx.clone());
1211
1212 for ((index, utxo), signature) in unsigned_tx
1213 .spent_tx_outs
1214 .iter()
1215 .enumerate()
1216 .zip(signatures.iter())
1217 {
1218 let descriptor = self.descriptor(&utxo.tweak).ecdsa_sighash_script_code();
1219
1220 let p2wsh_sighash = sighash_cache
1221 .p2wsh_signature_hash(index, &descriptor, utxo.value, EcdsaSighashType::All)
1222 .expect("Failed to compute P2WSH segwit sighash");
1223
1224 let pk = tweak_public_key(&pk, &utxo.tweak);
1225
1226 secp256k1::SECP256K1.verify_ecdsa(&p2wsh_sighash.into(), signature, &pk)?;
1227 }
1228
1229 Ok(())
1230 }
1231
1232 fn finalize_tx(
1233 &self,
1234 federation_tx: &mut FederationTx,
1235 signatures: &BTreeMap<PeerId, Vec<Signature>>,
1236 ) {
1237 assert_eq!(
1238 federation_tx.spent_tx_outs.len(),
1239 federation_tx.tx.input.len()
1240 );
1241
1242 for (index, utxo) in federation_tx.spent_tx_outs.iter().enumerate() {
1243 let satisfier: BTreeMap<PublicKey, bitcoin::ecdsa::Signature> = signatures
1244 .iter()
1245 .map(|(peer, sigs)| {
1246 assert_eq!(sigs.len(), federation_tx.tx.input.len());
1247
1248 let pk = *self
1249 .cfg
1250 .consensus
1251 .bitcoin_pks
1252 .get(peer)
1253 .expect("Failed to get public key of peer from config");
1254
1255 let pk = tweak_public_key(&pk, &utxo.tweak);
1256
1257 (pk, bitcoin::ecdsa::Signature::sighash_all(sigs[index]))
1258 })
1259 .collect();
1260
1261 miniscript::Descriptor::Wsh(self.descriptor(&utxo.tweak))
1262 .satisfy(&mut federation_tx.tx.input[index], satisfier)
1263 .expect("Failed to satisfy descriptor");
1264 }
1265 }
1266
1267 async fn tx_id(&self, dbtx: &mut DatabaseTransaction<'_>, outpoint: OutPoint) -> Option<Txid> {
1268 let index = dbtx.get_value(&TxInfoIndexKey(outpoint)).await?;
1269
1270 dbtx.get_value(&TxInfoKey(index))
1271 .await
1272 .map(|entry| entry.txid)
1273 }
1274
1275 async fn get_deposits(
1276 &self,
1277 dbtx: &mut DatabaseTransaction<'_>,
1278 start_index: u64,
1279 end_index: u64,
1280 ) -> DepositRange {
1281 let deposits = dbtx
1282 .find_by_range(DepositKey(start_index)..DepositKey(end_index))
1283 .await
1284 .map(|entry| entry.1.1)
1285 .collect()
1286 .await;
1287
1288 let spent = dbtx
1289 .find_by_range(SpentDepositKey(start_index)..SpentDepositKey(end_index))
1290 .await
1291 .map(|entry| entry.0.0)
1292 .collect()
1293 .await;
1294
1295 DepositRange { deposits, spent }
1296 }
1297
1298 async fn pending_tx_chain(&self, dbtx: &mut DatabaseTransaction<'_>) -> Vec<TxInfo> {
1299 let n_pending = pending_txs_unordered(dbtx).await.len();
1300
1301 dbtx.find_by_prefix_sorted_descending(&TxInfoPrefix)
1302 .await
1303 .take(n_pending)
1304 .map(|entry| entry.1)
1305 .collect()
1306 .await
1307 }
1308
1309 async fn tx_chain(&self, dbtx: &mut DatabaseTransaction<'_>, n: usize) -> Vec<TxInfo> {
1310 dbtx.find_by_prefix_sorted_descending(&TxInfoPrefix)
1311 .await
1312 .take(n)
1313 .map(|entry| entry.1)
1314 .collect()
1315 .await
1316 }
1317
1318 async fn total_txs(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1319 dbtx.find_by_prefix_sorted_descending(&TxInfoPrefix)
1320 .await
1321 .next()
1322 .await
1323 .map_or(0, |entry| entry.0.0 + 1)
1324 }
1325
1326 pub fn network_ui(&self) -> Network {
1328 self.cfg.consensus.network
1329 }
1330
1331 pub async fn federation_wallet_ui(&self) -> Option<FederationWallet> {
1333 self.db
1334 .begin_transaction_nc()
1335 .await
1336 .get_value(&FederationWalletKey)
1337 .await
1338 }
1339
1340 pub async fn consensus_block_count_ui(&self) -> u64 {
1342 self.consensus_block_count(&mut self.db.begin_transaction_nc().await)
1343 .await
1344 }
1345
1346 pub async fn consensus_feerate_ui(&self) -> Option<u64> {
1348 self.consensus_feerate(&mut self.db.begin_transaction_nc().await)
1349 .await
1350 .map(|feerate| feerate / 1000)
1351 }
1352
1353 pub async fn send_fee_ui(&self) -> Option<Amount> {
1355 self.send_fee(&mut self.db.begin_transaction_nc().await)
1356 .await
1357 }
1358
1359 pub async fn receive_fee_ui(&self) -> Option<Amount> {
1361 self.receive_fee(&mut self.db.begin_transaction_nc().await)
1362 .await
1363 }
1364
1365 pub async fn pending_tx_chain_ui(&self) -> Vec<TxInfo> {
1367 self.pending_tx_chain(&mut self.db.begin_transaction_nc().await)
1368 .await
1369 }
1370
1371 pub async fn tx_chain_ui(&self, n: usize) -> Vec<TxInfo> {
1373 self.tx_chain(&mut self.db.begin_transaction_nc().await, n)
1374 .await
1375 }
1376
1377 pub async fn recovery_keys_ui(&self) -> Option<(BTreeMap<PeerId, String>, String)> {
1380 let wallet = self.federation_wallet_ui().await?;
1381
1382 let pks = self
1383 .cfg
1384 .consensus
1385 .bitcoin_pks
1386 .iter()
1387 .map(|(peer, pk)| (*peer, tweak_public_key(pk, &wallet.tweak).to_string()))
1388 .collect();
1389
1390 let tweak = &Scalar::from_be_bytes(wallet.tweak.to_byte_array())
1391 .expect("Hash is within field order");
1392
1393 let sk = self
1394 .cfg
1395 .private
1396 .bitcoin_sk
1397 .add_tweak(tweak)
1398 .expect("Failed to tweak bitcoin secret key");
1399
1400 let sk = bitcoin::PrivateKey::new(sk, self.cfg.consensus.network).to_wif();
1401
1402 Some((pks, sk))
1403 }
1404}