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