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 OutputInfo, WalletCommonInit, WalletConsensusItem, WalletInput, WalletModuleTypes,
27 WalletOutput, WalletOutputOutcome,
28};
29use db::{
30 DbKeyPrefix, FederationWalletKey, FederationWalletPrefix, Output, OutputKey, OutputPrefix,
31 SignaturesKey, SignaturesPrefix, SignaturesTxidPrefix, SpentOutputKey, SpentOutputPrefix,
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, FEDERATION_WALLET_ENDPOINT,
69 OUTPUT_INFO_SLICE_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::Output => {
155 push_db_pair_items!(
156 dbtx,
157 OutputPrefix,
158 OutputKey,
159 Output,
160 wallet,
161 "Wallet Outputs"
162 );
163 }
164 DbKeyPrefix::SpentOutput => {
165 push_db_pair_items!(
166 dbtx,
167 SpentOutputPrefix,
168 SpentOutputKey,
169 (),
170 wallet,
171 "Wallet Spent Outputs"
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 self.verify_signatures(
410 &unsigned_tx,
411 &signatures,
412 self.cfg.private.bitcoin_sk.public_key(secp256k1::SECP256K1),
413 )
414 .expect("Our signatures failed verification against our private key");
415
416 WalletConsensusItem::Signatures(key.0, signatures)
417 })
418 .collect::<Vec<WalletConsensusItem>>()
419 .await;
420
421 if let Some(status) = self.btc_rpc.status() {
422 assert_eq!(status.network, self.cfg.consensus.network);
423
424 let block_count_vote = status
425 .block_count
426 .saturating_sub(CONFIRMATION_FINALITY_DELAY);
427
428 let consensus_block_count = self.consensus_block_count(dbtx).await;
429
430 let block_count_vote = match consensus_block_count {
431 0 => block_count_vote,
432 _ => block_count_vote.min(consensus_block_count + MAX_BLOCK_COUNT_INCREMENT),
433 };
434
435 items.push(WalletConsensusItem::BlockCount(block_count_vote));
436
437 let feerate_vote = status
438 .fee_rate
439 .sats_per_kvb
440 .max(MIN_FEERATE_VOTE_SATS_PER_KVB);
441
442 items.push(WalletConsensusItem::Feerate(Some(feerate_vote)));
443 } else {
444 items.push(WalletConsensusItem::Feerate(None));
446 }
447
448 items
449 }
450
451 async fn process_consensus_item<'a, 'b>(
452 &'a self,
453 dbtx: &mut DatabaseTransaction<'b>,
454 consensus_item: WalletConsensusItem,
455 peer: PeerId,
456 ) -> anyhow::Result<()> {
457 match consensus_item {
458 WalletConsensusItem::BlockCount(block_count_vote) => {
459 self.process_block_count(dbtx, block_count_vote, peer).await
460 }
461 WalletConsensusItem::Feerate(feerate) => {
462 if Some(feerate) == dbtx.insert_entry(&FeeRateVoteKey(peer), &feerate).await {
463 return Err(anyhow!("Fee rate vote is redundant"));
464 }
465
466 Ok(())
467 }
468 WalletConsensusItem::Signatures(txid, signatures) => {
469 self.process_signatures(dbtx, txid, signatures, peer).await
470 }
471 WalletConsensusItem::Default { variant, .. } => Err(anyhow!(
472 "Received wallet consensus item with unknown variant {variant}"
473 )),
474 }
475 }
476
477 async fn process_input<'a, 'b, 'c>(
478 &'a self,
479 dbtx: &mut DatabaseTransaction<'c>,
480 input: &'b WalletInput,
481 _in_point: InPoint,
482 ) -> Result<InputMeta, WalletInputError> {
483 let input = input.ensure_v0_ref()?;
484
485 if dbtx
486 .insert_entry(&SpentOutputKey(input.output_index), &())
487 .await
488 .is_some()
489 {
490 return Err(WalletInputError::OutputAlreadySpent);
491 }
492
493 let Output(tracked_outpoint, tracked_output) = dbtx
494 .get_value(&OutputKey(input.output_index))
495 .await
496 .ok_or(WalletInputError::UnknownOutputIndex)?;
497
498 let tweaked_pubkey = self
499 .descriptor(&input.tweak.consensus_hash())
500 .script_pubkey();
501
502 if tracked_output.script_pubkey != tweaked_pubkey {
503 return Err(WalletInputError::WrongTweak);
504 }
505
506 let consensus_receive_fee = self
507 .receive_fee(dbtx)
508 .await
509 .ok_or(WalletInputError::NoConsensusFeerateAvailable)?;
510
511 if input.fee < consensus_receive_fee {
516 return Err(WalletInputError::InsufficientTotalFee);
517 }
518
519 let output_value = tracked_output
520 .value
521 .checked_sub(input.fee)
522 .ok_or(WalletInputError::ArithmeticOverflow)?;
523
524 if let Some(wallet) = dbtx.remove_entry(&FederationWalletKey).await {
525 let change_value = wallet
529 .value
530 .checked_add(output_value)
531 .ok_or(WalletInputError::ArithmeticOverflow)?;
532
533 let tx = Transaction {
534 version: Version(2),
535 lock_time: LockTime::ZERO,
536 input: vec![
537 TxIn {
538 previous_output: wallet.outpoint,
539 script_sig: Default::default(),
540 sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
541 witness: bitcoin::Witness::new(),
542 },
543 TxIn {
544 previous_output: tracked_outpoint,
545 script_sig: Default::default(),
546 sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
547 witness: bitcoin::Witness::new(),
548 },
549 ],
550 output: vec![TxOut {
551 value: change_value,
552 script_pubkey: self.descriptor(&wallet.consensus_hash()).script_pubkey(),
553 }],
554 };
555
556 dbtx.insert_new_entry(
557 &FederationWalletKey,
558 &FederationWallet {
559 value: change_value,
560 outpoint: bitcoin::OutPoint {
561 txid: tx.compute_txid(),
562 vout: 0,
563 },
564 tweak: wallet.consensus_hash(),
565 },
566 )
567 .await;
568
569 let tx_index = self.total_txs(dbtx).await;
570
571 let created = self.consensus_block_count(dbtx).await;
572
573 dbtx.insert_new_entry(
574 &TxInfoKey(tx_index),
575 &TxInfo {
576 index: tx_index,
577 txid: tx.compute_txid(),
578 input: wallet.value,
579 output: change_value,
580 vbytes: self.cfg.consensus.receive_tx_vbytes,
581 fee: input.fee,
582 created,
583 },
584 )
585 .await;
586
587 dbtx.insert_new_entry(
588 &UnsignedTxKey(tx.compute_txid()),
589 &FederationTx {
590 tx,
591 spent_tx_outs: vec![
592 SpentTxOut {
593 value: wallet.value,
594 tweak: wallet.tweak,
595 },
596 SpentTxOut {
597 value: tracked_output.value,
598 tweak: input.tweak.consensus_hash(),
599 },
600 ],
601 vbytes: self.cfg.consensus.receive_tx_vbytes,
602 fee: input.fee,
603 },
604 )
605 .await;
606 } else {
607 dbtx.insert_new_entry(
608 &FederationWalletKey,
609 &FederationWallet {
610 value: tracked_output.value,
611 outpoint: tracked_outpoint,
612 tweak: input.tweak.consensus_hash(),
613 },
614 )
615 .await;
616 }
617
618 let amount = output_value
619 .to_sat()
620 .checked_mul(1000)
621 .map(fedimint_core::Amount::from_msats)
622 .ok_or(WalletInputError::ArithmeticOverflow)?;
623
624 Ok(InputMeta {
625 amount: TransactionItemAmounts {
626 amounts: Amounts::new_bitcoin(amount),
627 fees: Amounts::new_bitcoin(self.cfg.consensus.fee_consensus.fee(amount)),
628 },
629 pub_key: input.tweak,
630 })
631 }
632
633 async fn process_output<'a, 'b>(
634 &'a self,
635 dbtx: &mut DatabaseTransaction<'b>,
636 output: &'a WalletOutput,
637 outpoint: OutPoint,
638 ) -> Result<TransactionItemAmounts, WalletOutputError> {
639 let output = output.ensure_v0_ref()?;
640
641 if output.value < self.cfg.consensus.dust_limit {
642 return Err(WalletOutputError::UnderDustLimit);
643 }
644
645 let wallet = dbtx
646 .remove_entry(&FederationWalletKey)
647 .await
648 .ok_or(WalletOutputError::NoFederationUTXO)?;
649
650 let consensus_send_fee = self
651 .send_fee(dbtx)
652 .await
653 .ok_or(WalletOutputError::NoConsensusFeerateAvailable)?;
654
655 if output.fee < consensus_send_fee {
660 return Err(WalletOutputError::InsufficientTotalFee);
661 }
662
663 let output_value = output
664 .value
665 .checked_add(output.fee)
666 .ok_or(WalletOutputError::ArithmeticOverflow)?;
667
668 let change_value = wallet
669 .value
670 .checked_sub(output_value)
671 .ok_or(WalletOutputError::ArithmeticOverflow)?;
672
673 if change_value < self.cfg.consensus.dust_limit {
674 return Err(WalletOutputError::ChangeUnderDustLimit);
675 }
676
677 let script_pubkey = output
678 .destination
679 .script_pubkey()
680 .ok_or(WalletOutputError::UnknownScriptVariant)?;
681
682 let tx = Transaction {
683 version: Version(2),
684 lock_time: LockTime::ZERO,
685 input: vec![TxIn {
686 previous_output: wallet.outpoint,
687 script_sig: Default::default(),
688 sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
689 witness: bitcoin::Witness::new(),
690 }],
691 output: vec![
692 TxOut {
693 value: change_value,
694 script_pubkey: self.descriptor(&wallet.consensus_hash()).script_pubkey(),
695 },
696 TxOut {
697 value: output.value,
698 script_pubkey,
699 },
700 ],
701 };
702
703 dbtx.insert_new_entry(
704 &FederationWalletKey,
705 &FederationWallet {
706 value: change_value,
707 outpoint: bitcoin::OutPoint {
708 txid: tx.compute_txid(),
709 vout: 0,
710 },
711 tweak: wallet.consensus_hash(),
712 },
713 )
714 .await;
715
716 let tx_index = self.total_txs(dbtx).await;
717
718 let created = self.consensus_block_count(dbtx).await;
719
720 dbtx.insert_new_entry(
721 &TxInfoKey(tx_index),
722 &TxInfo {
723 index: tx_index,
724 txid: tx.compute_txid(),
725 input: wallet.value,
726 output: change_value,
727 vbytes: self.cfg.consensus.send_tx_vbytes,
728 fee: output.fee,
729 created,
730 },
731 )
732 .await;
733
734 dbtx.insert_new_entry(&TxInfoIndexKey(outpoint), &tx_index)
735 .await;
736
737 dbtx.insert_new_entry(
738 &UnsignedTxKey(tx.compute_txid()),
739 &FederationTx {
740 tx,
741 spent_tx_outs: vec![SpentTxOut {
742 value: wallet.value,
743 tweak: wallet.tweak,
744 }],
745 vbytes: self.cfg.consensus.send_tx_vbytes,
746 fee: output.fee,
747 },
748 )
749 .await;
750
751 let amount = output_value
752 .to_sat()
753 .checked_mul(1000)
754 .map(fedimint_core::Amount::from_msats)
755 .ok_or(WalletOutputError::ArithmeticOverflow)?;
756
757 Ok(TransactionItemAmounts {
758 amounts: Amounts::new_bitcoin(amount),
759 fees: Amounts::new_bitcoin(self.cfg.consensus.fee_consensus.fee(amount)),
760 })
761 }
762
763 async fn output_status(
764 &self,
765 _dbtx: &mut DatabaseTransaction<'_>,
766 _outpoint: OutPoint,
767 ) -> Option<WalletOutputOutcome> {
768 None
769 }
770
771 async fn audit(
772 &self,
773 dbtx: &mut DatabaseTransaction<'_>,
774 audit: &mut Audit,
775 module_instance_id: ModuleInstanceId,
776 ) {
777 audit
778 .add_items(
779 dbtx,
780 module_instance_id,
781 &FederationWalletPrefix,
782 |_, wallet| 1000 * wallet.value.to_sat() as i64,
783 )
784 .await;
785 }
786
787 fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
788 vec![
789 api_endpoint! {
790 CONSENSUS_BLOCK_COUNT_ENDPOINT,
791 ApiVersion::new(0, 0),
792 async |module: &Wallet, context, _params: ()| -> u64 {
793 let db = context.db();
794 let mut dbtx = db.begin_transaction_nc().await;
795 Ok(module.consensus_block_count(&mut dbtx).await)
796 }
797 },
798 api_endpoint! {
799 CONSENSUS_FEERATE_ENDPOINT,
800 ApiVersion::new(0, 0),
801 async |module: &Wallet, context, _params: ()| -> Option<u64> {
802 let db = context.db();
803 let mut dbtx = db.begin_transaction_nc().await;
804 Ok(module.consensus_feerate(&mut dbtx).await)
805 }
806 },
807 api_endpoint! {
808 FEDERATION_WALLET_ENDPOINT,
809 ApiVersion::new(0, 0),
810 async |_module: &Wallet, context, _params: ()| -> Option<FederationWallet> {
811 let db = context.db();
812 let mut dbtx = db.begin_transaction_nc().await;
813 Ok(dbtx.get_value(&FederationWalletKey).await)
814 }
815 },
816 api_endpoint! {
817 SEND_FEE_ENDPOINT,
818 ApiVersion::new(0, 0),
819 async |module: &Wallet, context, _params: ()| -> Option<Amount> {
820 let db = context.db();
821 let mut dbtx = db.begin_transaction_nc().await;
822 Ok(module.send_fee(&mut dbtx).await)
823 }
824 },
825 api_endpoint! {
826 RECEIVE_FEE_ENDPOINT,
827 ApiVersion::new(0, 0),
828 async |module: &Wallet, context, _params: ()| -> Option<Amount> {
829 let db = context.db();
830 let mut dbtx = db.begin_transaction_nc().await;
831 Ok(module.receive_fee(&mut dbtx).await)
832 }
833 },
834 api_endpoint! {
835 TRANSACTION_ID_ENDPOINT,
836 ApiVersion::new(0, 0),
837 async |module: &Wallet, context, params: OutPoint| -> Option<Txid> {
838 let db = context.db();
839 let mut dbtx = db.begin_transaction_nc().await;
840 Ok(module.tx_id(&mut dbtx, params).await)
841 }
842 },
843 api_endpoint! {
844 OUTPUT_INFO_SLICE_ENDPOINT,
845 ApiVersion::new(0, 0),
846 async |module: &Wallet, context, params: (u64, u64)| -> Vec<OutputInfo> {
847 let db = context.db();
848 let mut dbtx = db.begin_transaction_nc().await;
849 Ok(module.get_outputs(&mut dbtx, params.0, params.1).await)
850 }
851 },
852 api_endpoint! {
853 PENDING_TRANSACTION_CHAIN_ENDPOINT,
854 ApiVersion::new(0, 0),
855 async |module: &Wallet, context, _params: ()| -> Vec<TxInfo> {
856 let db = context.db();
857 let mut dbtx = db.begin_transaction_nc().await;
858 Ok(module.pending_tx_chain(&mut dbtx).await)
859 }
860 },
861 api_endpoint! {
862 TRANSACTION_CHAIN_ENDPOINT,
863 ApiVersion::new(0, 0),
864 async |module: &Wallet, context, _params: ()| -> Vec<TxInfo> {
865 let db = context.db();
866 let mut dbtx = db.begin_transaction_nc().await;
867 Ok(module.tx_chain(&mut dbtx).await)
868 }
869 },
870 ]
871 }
872}
873
874#[derive(Debug)]
875pub struct Wallet {
876 cfg: WalletConfig,
877 db: Database,
878 btc_rpc: ServerBitcoinRpcMonitor,
879}
880
881impl Wallet {
882 fn new(
883 cfg: WalletConfig,
884 db: &Database,
885 task_group: &TaskGroup,
886 btc_rpc: ServerBitcoinRpcMonitor,
887 ) -> Wallet {
888 Self::spawn_broadcast_unconfirmed_txs_task(btc_rpc.clone(), db.clone(), task_group);
889
890 Wallet {
891 cfg,
892 btc_rpc,
893 db: db.clone(),
894 }
895 }
896
897 fn spawn_broadcast_unconfirmed_txs_task(
898 btc_rpc: ServerBitcoinRpcMonitor,
899 db: Database,
900 task_group: &TaskGroup,
901 ) {
902 task_group.spawn_cancellable("broadcast_unconfirmed_transactions", async move {
903 loop {
904 let unconfirmed_txs = db
905 .begin_transaction_nc()
906 .await
907 .find_by_prefix(&UnconfirmedTxPrefix)
908 .await
909 .map(|entry| entry.1)
910 .collect::<Vec<FederationTx>>()
911 .await;
912
913 for unconfirmed_tx in unconfirmed_txs {
914 btc_rpc.submit_transaction(unconfirmed_tx.tx).await;
915 }
916
917 sleep(common::sleep_duration()).await;
918 }
919 });
920 }
921
922 async fn process_block_count(
923 &self,
924 dbtx: &mut DatabaseTransaction<'_>,
925 block_count_vote: u64,
926 peer: PeerId,
927 ) -> anyhow::Result<()> {
928 let old_consensus_block_count = self.consensus_block_count(dbtx).await;
929
930 let current_vote = dbtx
931 .insert_entry(&BlockCountVoteKey(peer), &block_count_vote)
932 .await
933 .unwrap_or(0);
934
935 ensure!(
936 current_vote < block_count_vote,
937 "Block count vote is redundant"
938 );
939
940 let new_consensus_block_count = self.consensus_block_count(dbtx).await;
941
942 assert!(old_consensus_block_count <= new_consensus_block_count);
943
944 if old_consensus_block_count == 0 {
946 return Ok(());
947 }
948
949 self.await_local_sync_to_block_count(
952 new_consensus_block_count + CONFIRMATION_FINALITY_DELAY,
953 )
954 .await;
955
956 for height in old_consensus_block_count..new_consensus_block_count {
957 if let Some(status) = self.btc_rpc.status() {
959 assert_eq!(status.network, self.cfg.consensus.network);
960 }
961
962 let block_hash = util::retry(
963 "get_block_hash",
964 util::backoff_util::background_backoff(),
965 || self.btc_rpc.get_block_hash(height),
966 )
967 .await
968 .expect("Bitcoind rpc to get_block_hash failed");
969
970 let block = util::retry(
971 "get_block",
972 util::backoff_util::background_backoff(),
973 || self.btc_rpc.get_block(&block_hash),
974 )
975 .await
976 .expect("Bitcoind rpc to get_block failed");
977
978 assert_eq!(block.block_hash(), block_hash, "Block hash mismatch");
979
980 let pks_hash = self.cfg.consensus.bitcoin_pks.consensus_hash();
981
982 for tx in block.txdata {
983 dbtx.remove_entry(&UnconfirmedTxKey(tx.compute_txid()))
984 .await;
985
986 for (vout, tx_out) in tx.output.iter().enumerate() {
992 if is_potential_receive(&tx_out.script_pubkey, &pks_hash) {
993 let outpoint = bitcoin::OutPoint {
994 txid: tx.compute_txid(),
995 vout: u32::try_from(vout)
996 .expect("Bitcoin transaction has more than u32::MAX outputs"),
997 };
998
999 let index = dbtx
1000 .find_by_prefix_sorted_descending(&OutputPrefix)
1001 .await
1002 .next()
1003 .await
1004 .map_or(0, |entry| entry.0.0 + 1);
1005
1006 dbtx.insert_new_entry(&OutputKey(index), &Output(outpoint, tx_out.clone()))
1007 .await;
1008 }
1009 }
1010 }
1011 }
1012
1013 Ok(())
1014 }
1015
1016 async fn process_signatures(
1017 &self,
1018 dbtx: &mut DatabaseTransaction<'_>,
1019 txid: bitcoin::Txid,
1020 signatures: Vec<Signature>,
1021 peer: PeerId,
1022 ) -> anyhow::Result<()> {
1023 let mut unsigned = dbtx
1024 .get_value(&UnsignedTxKey(txid))
1025 .await
1026 .context("Unsigned transaction does not exist")?;
1027
1028 let pk = self
1029 .cfg
1030 .consensus
1031 .bitcoin_pks
1032 .get(&peer)
1033 .expect("Failed to get public key of peer from config");
1034
1035 self.verify_signatures(&unsigned, &signatures, *pk)?;
1036
1037 if dbtx
1038 .insert_entry(&SignaturesKey(txid, peer), &signatures)
1039 .await
1040 .is_some()
1041 {
1042 bail!("Already received valid signatures from this peer")
1043 }
1044
1045 let signatures = dbtx
1046 .find_by_prefix(&SignaturesTxidPrefix(txid))
1047 .await
1048 .map(|(key, signatures)| (key.1, signatures))
1049 .collect::<BTreeMap<PeerId, Vec<Signature>>>()
1050 .await;
1051
1052 if signatures.len() == self.cfg.consensus.bitcoin_pks.to_num_peers().threshold() {
1053 dbtx.remove_entry(&UnsignedTxKey(txid)).await;
1054
1055 dbtx.remove_by_prefix(&SignaturesTxidPrefix(txid)).await;
1056
1057 self.finalize_tx(&mut unsigned, &signatures);
1058
1059 dbtx.insert_new_entry(&UnconfirmedTxKey(txid), &unsigned)
1060 .await;
1061
1062 self.btc_rpc.submit_transaction(unsigned.tx).await;
1063 }
1064
1065 Ok(())
1066 }
1067
1068 async fn await_local_sync_to_block_count(&self, block_count: u64) {
1069 loop {
1070 if self
1071 .btc_rpc
1072 .status()
1073 .is_some_and(|status| status.block_count >= block_count)
1074 {
1075 break;
1076 }
1077
1078 info!(target: LOG_MODULE_WALLETV2, "Waiting for local bitcoin backend to sync to block count {block_count}");
1079
1080 sleep(common::sleep_duration()).await;
1081 }
1082 }
1083
1084 pub async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1085 let num_peers = self.cfg.consensus.bitcoin_pks.to_num_peers();
1086
1087 let mut counts = dbtx
1088 .find_by_prefix(&BlockCountVotePrefix)
1089 .await
1090 .map(|entry| entry.1)
1091 .collect::<Vec<u64>>()
1092 .await;
1093
1094 assert!(counts.len() <= num_peers.total());
1095
1096 counts.sort_unstable();
1097
1098 counts.reverse();
1099
1100 assert!(counts.last() <= counts.first());
1101
1102 counts.get(num_peers.threshold() - 1).copied().unwrap_or(0)
1107 }
1108
1109 pub async fn consensus_feerate(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<u64> {
1110 let num_peers = self.cfg.consensus.bitcoin_pks.to_num_peers();
1111
1112 let mut rates = dbtx
1113 .find_by_prefix(&FeeRateVotePrefix)
1114 .await
1115 .filter_map(|entry| async move { entry.1 })
1116 .collect::<Vec<u64>>()
1117 .await;
1118
1119 assert!(rates.len() <= num_peers.total());
1120
1121 rates.sort_unstable();
1122
1123 assert!(rates.first() <= rates.last());
1124
1125 rates.get(num_peers.threshold() - 1).copied()
1126 }
1127
1128 pub async fn consensus_fee(
1129 &self,
1130 dbtx: &mut DatabaseTransaction<'_>,
1131 tx_vbytes: u64,
1132 ) -> Option<Amount> {
1133 let pending_txs = pending_txs_unordered(dbtx).await;
1137
1138 assert!(pending_txs.len() <= 32);
1139
1140 let feerate = self
1141 .consensus_feerate(dbtx)
1142 .await?
1143 .max(self.cfg.consensus.feerate_base << pending_txs.len());
1144
1145 let tx_fee = tx_vbytes.saturating_mul(feerate).saturating_div(1000);
1146
1147 let stack_vbytes = pending_txs
1148 .iter()
1149 .map(|t| t.vbytes)
1150 .try_fold(tx_vbytes, u64::checked_add)
1151 .expect("Stack vbytes overflow with at most 32 pending txs");
1152
1153 let stack_fee = stack_vbytes.saturating_mul(feerate).saturating_div(1000);
1154
1155 let stack_fee = pending_txs
1157 .iter()
1158 .map(|t| t.fee.to_sat())
1159 .fold(stack_fee, u64::saturating_sub);
1160
1161 Some(Amount::from_sat(tx_fee.max(stack_fee)))
1162 }
1163
1164 pub async fn send_fee(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<Amount> {
1165 self.consensus_fee(dbtx, self.cfg.consensus.send_tx_vbytes)
1166 .await
1167 }
1168
1169 pub async fn receive_fee(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<Amount> {
1170 self.consensus_fee(dbtx, self.cfg.consensus.receive_tx_vbytes)
1171 .await
1172 }
1173
1174 fn descriptor(&self, tweak: &sha256::Hash) -> Wsh<secp256k1::PublicKey> {
1175 descriptor(&self.cfg.consensus.bitcoin_pks, tweak)
1176 }
1177
1178 fn sign_tx(&self, unsigned_tx: &FederationTx) -> Vec<Signature> {
1179 let mut sighash_cache = SighashCache::new(unsigned_tx.tx.clone());
1180
1181 unsigned_tx
1182 .spent_tx_outs
1183 .iter()
1184 .enumerate()
1185 .map(|(index, utxo)| {
1186 let descriptor = self.descriptor(&utxo.tweak).ecdsa_sighash_script_code();
1187
1188 let p2wsh_sighash = sighash_cache
1189 .p2wsh_signature_hash(index, &descriptor, utxo.value, EcdsaSighashType::All)
1190 .expect("Failed to compute P2WSH segwit sighash");
1191
1192 let scalar = &Scalar::from_be_bytes(utxo.tweak.to_byte_array())
1193 .expect("Hash is within field order");
1194
1195 let sk = self
1196 .cfg
1197 .private
1198 .bitcoin_sk
1199 .add_tweak(scalar)
1200 .expect("Failed to tweak bitcoin secret key");
1201
1202 Secp256k1::new().sign_ecdsa(&p2wsh_sighash.into(), &sk)
1203 })
1204 .collect()
1205 }
1206
1207 fn verify_signatures(
1208 &self,
1209 unsigned_tx: &FederationTx,
1210 signatures: &[Signature],
1211 pk: PublicKey,
1212 ) -> anyhow::Result<()> {
1213 ensure!(
1214 unsigned_tx.spent_tx_outs.len() == signatures.len(),
1215 "Incorrect number of signatures"
1216 );
1217
1218 let mut sighash_cache = SighashCache::new(unsigned_tx.tx.clone());
1219
1220 for ((index, utxo), signature) in unsigned_tx
1221 .spent_tx_outs
1222 .iter()
1223 .enumerate()
1224 .zip(signatures.iter())
1225 {
1226 let code = self.descriptor(&utxo.tweak).ecdsa_sighash_script_code();
1227
1228 let p2wsh_sighash = sighash_cache
1229 .p2wsh_signature_hash(index, &code, utxo.value, EcdsaSighashType::All)
1230 .expect("Failed to compute P2WSH segwit sighash");
1231
1232 let pk = tweak_public_key(&pk, &utxo.tweak);
1233
1234 secp256k1::SECP256K1.verify_ecdsa(&p2wsh_sighash.into(), signature, &pk)?;
1235 }
1236
1237 Ok(())
1238 }
1239
1240 fn finalize_tx(
1241 &self,
1242 federation_tx: &mut FederationTx,
1243 signatures: &BTreeMap<PeerId, Vec<Signature>>,
1244 ) {
1245 assert_eq!(
1246 federation_tx.spent_tx_outs.len(),
1247 federation_tx.tx.input.len()
1248 );
1249
1250 for (index, utxo) in federation_tx.spent_tx_outs.iter().enumerate() {
1251 let satisfier: BTreeMap<PublicKey, bitcoin::ecdsa::Signature> = signatures
1252 .iter()
1253 .map(|(peer, sigs)| {
1254 assert_eq!(sigs.len(), federation_tx.tx.input.len());
1255
1256 let pk = *self
1257 .cfg
1258 .consensus
1259 .bitcoin_pks
1260 .get(peer)
1261 .expect("Failed to get public key of peer from config");
1262
1263 let pk = tweak_public_key(&pk, &utxo.tweak);
1264
1265 (pk, bitcoin::ecdsa::Signature::sighash_all(sigs[index]))
1266 })
1267 .collect();
1268
1269 miniscript::Descriptor::Wsh(self.descriptor(&utxo.tweak))
1270 .satisfy(&mut federation_tx.tx.input[index], satisfier)
1271 .expect("Failed to satisfy descriptor");
1272 }
1273 }
1274
1275 async fn tx_id(&self, dbtx: &mut DatabaseTransaction<'_>, outpoint: OutPoint) -> Option<Txid> {
1276 let index = dbtx.get_value(&TxInfoIndexKey(outpoint)).await?;
1277
1278 dbtx.get_value(&TxInfoKey(index))
1279 .await
1280 .map(|entry| entry.txid)
1281 }
1282
1283 async fn get_outputs(
1284 &self,
1285 dbtx: &mut DatabaseTransaction<'_>,
1286 start_index: u64,
1287 end_index: u64,
1288 ) -> Vec<OutputInfo> {
1289 let spent: BTreeSet<u64> = dbtx
1290 .find_by_range(SpentOutputKey(start_index)..SpentOutputKey(end_index))
1291 .await
1292 .map(|entry| entry.0.0)
1293 .collect()
1294 .await;
1295
1296 dbtx.find_by_range(OutputKey(start_index)..OutputKey(end_index))
1297 .await
1298 .filter_map(|entry| {
1299 std::future::ready(entry.1.1.script_pubkey.is_p2wsh().then(|| OutputInfo {
1300 index: entry.0.0,
1301 script: entry.1.1.script_pubkey,
1302 value: entry.1.1.value,
1303 spent: spent.contains(&entry.0.0),
1304 }))
1305 })
1306 .collect()
1307 .await
1308 }
1309
1310 async fn pending_tx_chain(&self, dbtx: &mut DatabaseTransaction<'_>) -> Vec<TxInfo> {
1311 let n_pending = pending_txs_unordered(dbtx).await.len();
1312
1313 dbtx.find_by_prefix_sorted_descending(&TxInfoPrefix)
1314 .await
1315 .take(n_pending)
1316 .map(|entry| entry.1)
1317 .collect()
1318 .await
1319 }
1320
1321 async fn tx_chain(&self, dbtx: &mut DatabaseTransaction<'_>) -> Vec<TxInfo> {
1322 dbtx.find_by_prefix(&TxInfoPrefix)
1323 .await
1324 .map(|entry| entry.1)
1325 .collect()
1326 .await
1327 }
1328
1329 async fn total_txs(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1330 dbtx.find_by_prefix_sorted_descending(&TxInfoPrefix)
1331 .await
1332 .next()
1333 .await
1334 .map_or(0, |entry| entry.0.0 + 1)
1335 }
1336
1337 pub fn network_ui(&self) -> Network {
1339 self.cfg.consensus.network
1340 }
1341
1342 pub async fn federation_wallet_ui(&self) -> Option<FederationWallet> {
1344 self.db
1345 .begin_transaction_nc()
1346 .await
1347 .get_value(&FederationWalletKey)
1348 .await
1349 }
1350
1351 pub async fn consensus_block_count_ui(&self) -> u64 {
1353 self.consensus_block_count(&mut self.db.begin_transaction_nc().await)
1354 .await
1355 }
1356
1357 pub async fn consensus_feerate_ui(&self) -> Option<u64> {
1359 self.consensus_feerate(&mut self.db.begin_transaction_nc().await)
1360 .await
1361 .map(|feerate| feerate / 1000)
1362 }
1363
1364 pub async fn send_fee_ui(&self) -> Option<Amount> {
1366 self.send_fee(&mut self.db.begin_transaction_nc().await)
1367 .await
1368 }
1369
1370 pub async fn receive_fee_ui(&self) -> Option<Amount> {
1372 self.receive_fee(&mut self.db.begin_transaction_nc().await)
1373 .await
1374 }
1375
1376 pub async fn pending_tx_chain_ui(&self) -> Vec<TxInfo> {
1378 self.pending_tx_chain(&mut self.db.begin_transaction_nc().await)
1379 .await
1380 }
1381
1382 pub async fn tx_chain_ui(&self) -> Vec<TxInfo> {
1384 self.tx_chain(&mut self.db.begin_transaction_nc().await)
1385 .await
1386 }
1387
1388 pub async fn recovery_keys_ui(&self) -> Option<(BTreeMap<PeerId, String>, String)> {
1391 let wallet = self.federation_wallet_ui().await?;
1392
1393 let pks = self
1394 .cfg
1395 .consensus
1396 .bitcoin_pks
1397 .iter()
1398 .map(|(peer, pk)| (*peer, tweak_public_key(pk, &wallet.tweak).to_string()))
1399 .collect();
1400
1401 let tweak = &Scalar::from_be_bytes(wallet.tweak.to_byte_array())
1402 .expect("Hash is within field order");
1403
1404 let sk = self
1405 .cfg
1406 .private
1407 .bitcoin_sk
1408 .add_tweak(tweak)
1409 .expect("Failed to tweak bitcoin secret key");
1410
1411 let sk = bitcoin::PrivateKey::new(sk, self.cfg.consensus.network).to_wif();
1412
1413 Some((pks, sk))
1414 }
1415}