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