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::{
44 FM_ENABLE_MODULE_WALLETV2_ENV, is_env_var_set_opt, is_running_in_test_env,
45};
46use fedimint_core::module::audit::Audit;
47use fedimint_core::module::{
48 Amounts, ApiEndpoint, ApiVersion, CoreConsensusVersion, InputMeta, ModuleConsensusVersion,
49 ModuleInit, MultiApiVersion, TransactionItemAmounts, public_api_endpoint,
50};
51#[cfg(not(target_family = "wasm"))]
52use fedimint_core::task::TaskGroup;
53use fedimint_core::task::sleep;
54use fedimint_core::util::FmtCompactAnyhow as _;
55use fedimint_core::{
56 InPoint, NumPeersExt, OutPoint, PeerId, apply, async_trait_maybe_send, push_db_pair_items, util,
57};
58use fedimint_logging::LOG_MODULE_WALLETV2;
59use fedimint_server_core::bitcoin_rpc::ServerBitcoinRpcMonitor;
60use fedimint_server_core::config::{PeerHandleOps, PeerHandleOpsExt};
61use fedimint_server_core::migration::ServerModuleDbMigrationFn;
62use fedimint_server_core::{
63 ConfigGenModuleArgs, EnvVarDoc, ServerModule, ServerModuleInit, ServerModuleInitArgs,
64};
65pub use fedimint_walletv2_common as common;
66use fedimint_walletv2_common::config::{
67 FeeConsensus, WalletClientConfig, WalletConfig, WalletConfigPrivate,
68};
69use fedimint_walletv2_common::endpoint_constants::{
70 CONSENSUS_BLOCK_COUNT_ENDPOINT, CONSENSUS_FEERATE_ENDPOINT, FEDERATION_WALLET_ENDPOINT,
71 OUTPUT_INFO_SLICE_ENDPOINT, PENDING_TRANSACTION_CHAIN_ENDPOINT, RECEIVE_FEE_ENDPOINT,
72 SEND_FEE_ENDPOINT, TRANSACTION_CHAIN_ENDPOINT, TRANSACTION_ID_ENDPOINT,
73};
74use fedimint_walletv2_common::{
75 FederationWallet, MODULE_CONSENSUS_VERSION, TxInfo, WalletInputError, WalletOutputError,
76 descriptor, is_potential_receive, tweak_public_key,
77};
78use futures::StreamExt;
79use miniscript::descriptor::Wsh;
80use rand::rngs::OsRng;
81use secp256k1::ecdsa::Signature;
82use secp256k1::{PublicKey, Scalar, SecretKey};
83use serde::{Deserialize, Serialize};
84use strum::IntoEnumIterator;
85use tracing::{debug, info};
86
87use crate::db::{
88 BlockCountVoteKey, BlockCountVotePrefix, FeeRateVoteKey, FeeRateVotePrefix, TxInfoKey,
89 TxInfoPrefix, UnconfirmedTxKey, UnconfirmedTxPrefix, UnsignedTxKey, UnsignedTxPrefix,
90};
91
92pub const CONFIRMATION_FINALITY_DELAY: u64 = 6;
96
97const MAX_BLOCK_COUNT_INCREMENT: u64 = 5;
100
101const TEST_MAX_BLOCK_COUNT_INCREMENT: u64 = 100;
104
105const MIN_FEERATE_VOTE_SATS_PER_KVB: u64 = 1000;
108
109#[derive(Clone, Debug, Eq, PartialEq, Serialize, Encodable, Decodable)]
110pub struct FederationTx {
111 pub tx: Transaction,
112 pub spent_tx_outs: Vec<SpentTxOut>,
113 pub vbytes: u64,
114 pub fee: Amount,
115}
116
117#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
118pub struct SpentTxOut {
119 pub value: Amount,
120 pub tweak: sha256::Hash,
121}
122
123async fn pending_txs_unordered(dbtx: &mut DatabaseTransaction<'_>) -> Vec<FederationTx> {
124 let unsigned: Vec<FederationTx> = dbtx
125 .find_by_prefix(&UnsignedTxPrefix)
126 .await
127 .map(|entry| entry.1)
128 .collect()
129 .await;
130
131 let unconfirmed: Vec<FederationTx> = dbtx
132 .find_by_prefix(&UnconfirmedTxPrefix)
133 .await
134 .map(|entry| entry.1)
135 .collect()
136 .await;
137
138 unsigned.into_iter().chain(unconfirmed).collect()
139}
140
141#[derive(Debug, Clone)]
142pub struct WalletInit;
143
144impl ModuleInit for WalletInit {
145 type Common = WalletCommonInit;
146
147 async fn dump_database(
148 &self,
149 dbtx: &mut DatabaseTransaction<'_>,
150 prefix_names: Vec<String>,
151 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
152 let mut wallet: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
153
154 let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
155 prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
156 });
157
158 for table in filtered_prefixes {
159 match table {
160 DbKeyPrefix::Output => {
161 push_db_pair_items!(
162 dbtx,
163 OutputPrefix,
164 OutputKey,
165 Output,
166 wallet,
167 "Wallet Outputs"
168 );
169 }
170 DbKeyPrefix::SpentOutput => {
171 push_db_pair_items!(
172 dbtx,
173 SpentOutputPrefix,
174 SpentOutputKey,
175 (),
176 wallet,
177 "Wallet Spent Outputs"
178 );
179 }
180 DbKeyPrefix::BlockCountVote => {
181 push_db_pair_items!(
182 dbtx,
183 BlockCountVotePrefix,
184 BlockCountVoteKey,
185 u64,
186 wallet,
187 "Wallet Block Count Votes"
188 );
189 }
190 DbKeyPrefix::FeeRateVote => {
191 push_db_pair_items!(
192 dbtx,
193 FeeRateVotePrefix,
194 FeeRateVoteKey,
195 Option<u64>,
196 wallet,
197 "Wallet Fee Rate Votes"
198 );
199 }
200 DbKeyPrefix::TxLog => {
201 push_db_pair_items!(
202 dbtx,
203 TxInfoPrefix,
204 TxInfoKey,
205 TxInfo,
206 wallet,
207 "Wallet Tx Log"
208 );
209 }
210 DbKeyPrefix::TxInfoIndex => {
211 push_db_pair_items!(
212 dbtx,
213 TxInfoIndexPrefix,
214 TxInfoIndexKey,
215 u64,
216 wallet,
217 "Wallet Tx Info Index"
218 );
219 }
220 DbKeyPrefix::UnsignedTx => {
221 push_db_pair_items!(
222 dbtx,
223 UnsignedTxPrefix,
224 UnsignedTxKey,
225 FederationTx,
226 wallet,
227 "Wallet Unsigned Transactions"
228 );
229 }
230 DbKeyPrefix::Signatures => {
231 push_db_pair_items!(
232 dbtx,
233 SignaturesPrefix,
234 SignaturesKey,
235 Vec<Signature>,
236 wallet,
237 "Wallet Signatures"
238 );
239 }
240 DbKeyPrefix::UnconfirmedTx => {
241 push_db_pair_items!(
242 dbtx,
243 UnconfirmedTxPrefix,
244 UnconfirmedTxKey,
245 FederationTx,
246 wallet,
247 "Wallet Unconfirmed Transactions"
248 );
249 }
250 DbKeyPrefix::FederationWallet => {
251 push_db_pair_items!(
252 dbtx,
253 FederationWalletPrefix,
254 FederationWalletKey,
255 FederationWallet,
256 wallet,
257 "Federation Wallet"
258 );
259 }
260 }
261 }
262
263 Box::new(wallet.into_iter())
264 }
265}
266
267#[apply(async_trait_maybe_send!)]
268impl ServerModuleInit for WalletInit {
269 type Module = Wallet;
270
271 fn versions(&self, _core: CoreConsensusVersion) -> &[ModuleConsensusVersion] {
272 &[MODULE_CONSENSUS_VERSION]
273 }
274
275 fn is_enabled_by_default(&self) -> bool {
276 is_env_var_set_opt(FM_ENABLE_MODULE_WALLETV2_ENV).unwrap_or(true)
277 }
278
279 fn get_documented_env_vars(&self) -> Vec<EnvVarDoc> {
280 vec![EnvVarDoc {
281 name: FM_ENABLE_MODULE_WALLETV2_ENV,
282 description: "Set to 0/false to disable the WalletV2 module. Enabled by default.",
283 }]
284 }
285
286 async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
287 Ok(Wallet::new(
288 args.cfg().to_typed()?,
289 args.db(),
290 args.task_group(),
291 args.server_bitcoin_rpc_monitor(),
292 ))
293 }
294
295 fn trusted_dealer_gen(
296 &self,
297 peers: &[PeerId],
298 args: &ConfigGenModuleArgs,
299 ) -> BTreeMap<PeerId, ServerModuleConfig> {
300 let fee_consensus = FeeConsensus::new(0).expect("Relative fee is within range");
301
302 let bitcoin_sks = peers
303 .iter()
304 .map(|peer| (*peer, SecretKey::new(&mut secp256k1::rand::thread_rng())))
305 .collect::<BTreeMap<PeerId, SecretKey>>();
306
307 let bitcoin_pks = bitcoin_sks
308 .iter()
309 .map(|(peer, sk)| (*peer, sk.public_key(secp256k1::SECP256K1)))
310 .collect::<BTreeMap<PeerId, PublicKey>>();
311
312 bitcoin_sks
313 .into_iter()
314 .map(|(peer, bitcoin_sk)| {
315 let config = WalletConfig {
316 private: WalletConfigPrivate { bitcoin_sk },
317 consensus: WalletConfigConsensus::new(
318 bitcoin_pks.clone(),
319 fee_consensus.clone(),
320 args.network,
321 ),
322 };
323
324 (peer, config.to_erased())
325 })
326 .collect()
327 }
328
329 async fn distributed_gen(
330 &self,
331 peers: &(dyn PeerHandleOps + Send + Sync),
332 args: &ConfigGenModuleArgs,
333 ) -> anyhow::Result<ServerModuleConfig> {
334 let fee_consensus = FeeConsensus::new(0).expect("Relative fee is within range");
335
336 let (bitcoin_sk, bitcoin_pk) = secp256k1::generate_keypair(&mut OsRng);
337
338 let bitcoin_pks: BTreeMap<PeerId, PublicKey> = peers
339 .exchange_encodable(bitcoin_pk)
340 .await?
341 .into_iter()
342 .collect();
343
344 let config = WalletConfig {
345 private: WalletConfigPrivate { bitcoin_sk },
346 consensus: WalletConfigConsensus::new(bitcoin_pks, fee_consensus, args.network),
347 };
348
349 Ok(config.to_erased())
350 }
351
352 fn validate_config(&self, identity: &PeerId, config: ServerModuleConfig) -> anyhow::Result<()> {
353 let config = config.to_typed::<WalletConfig>()?;
354
355 ensure!(
356 config
357 .consensus
358 .bitcoin_pks
359 .get(identity)
360 .ok_or(anyhow::anyhow!("No public key for our identity"))?
361 == &config.private.bitcoin_sk.public_key(secp256k1::SECP256K1),
362 "Bitcoin wallet private key doesn't match multisig pubkey"
363 );
364
365 Ok(())
366 }
367
368 fn get_client_config(
369 &self,
370 config: &ServerModuleConsensusConfig,
371 ) -> anyhow::Result<WalletClientConfig> {
372 let config = WalletConfigConsensus::from_erased(config)?;
373
374 Ok(WalletClientConfig {
375 bitcoin_pks: config.bitcoin_pks,
376 descriptor: config.descriptor,
377 send_tx_vbytes: config.send_tx_vbytes,
378 receive_tx_vbytes: config.receive_tx_vbytes,
379 feerate_base: config.feerate_base,
380 dust_limit: config.dust_limit,
381 fee_consensus: config.fee_consensus,
382 network: config.network,
383 })
384 }
385
386 fn get_database_migrations(
387 &self,
388 ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Wallet>> {
389 BTreeMap::new()
390 }
391
392 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
393 Some(DbKeyPrefix::iter().map(|p| p as u8).collect())
394 }
395}
396
397#[apply(async_trait_maybe_send!)]
398impl ServerModule for Wallet {
399 type Common = WalletModuleTypes;
400 type Init = WalletInit;
401
402 async fn consensus_proposal<'a>(
403 &'a self,
404 dbtx: &mut DatabaseTransaction<'_>,
405 ) -> Vec<WalletConsensusItem> {
406 let mut items = dbtx
407 .find_by_prefix(&UnsignedTxPrefix)
408 .await
409 .map(|(key, unsigned_tx)| {
410 let signatures = self.sign_tx(&unsigned_tx);
411
412 self.verify_signatures(
413 &unsigned_tx,
414 &signatures,
415 self.cfg.private.bitcoin_sk.public_key(secp256k1::SECP256K1),
416 )
417 .expect("Our signatures failed verification against our private key");
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 max_block_count_increment = if is_running_in_test_env() {
434 TEST_MAX_BLOCK_COUNT_INCREMENT
435 } else {
436 MAX_BLOCK_COUNT_INCREMENT
437 };
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 public_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 public_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 public_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 public_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 public_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 public_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 public_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 public_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 public_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 fn supported_api_versions(&self) -> MultiApiVersion {
883 MultiApiVersion::try_from_iter([ApiVersion::new(0, 1)])
884 .expect("walletv2 declares one API version per major version")
885 }
886}
887
888#[derive(Debug)]
889pub struct Wallet {
890 cfg: WalletConfig,
891 db: Database,
892 btc_rpc: ServerBitcoinRpcMonitor,
893}
894
895impl Wallet {
896 fn new(
897 cfg: WalletConfig,
898 db: &Database,
899 task_group: &TaskGroup,
900 btc_rpc: ServerBitcoinRpcMonitor,
901 ) -> Wallet {
902 Self::spawn_broadcast_unconfirmed_txs_task(btc_rpc.clone(), db.clone(), task_group);
903
904 Wallet {
905 cfg,
906 btc_rpc,
907 db: db.clone(),
908 }
909 }
910
911 fn spawn_broadcast_unconfirmed_txs_task(
912 btc_rpc: ServerBitcoinRpcMonitor,
913 db: Database,
914 task_group: &TaskGroup,
915 ) {
916 task_group.spawn_cancellable("broadcast_unconfirmed_transactions", async move {
917 loop {
918 let unconfirmed_txs = db
919 .begin_transaction_nc()
920 .await
921 .find_by_prefix(&UnconfirmedTxPrefix)
922 .await
923 .map(|entry| entry.1)
924 .collect::<Vec<FederationTx>>()
925 .await;
926
927 for unconfirmed_tx in unconfirmed_txs {
928 if let Err(err) = btc_rpc.submit_transaction(unconfirmed_tx.tx).await {
929 debug!(
930 target: LOG_MODULE_WALLETV2,
931 err = %err.fmt_compact_anyhow(),
932 "Error broadcasting unconfirmed transaction"
933 );
934 }
935 }
936
937 sleep(common::sleep_duration()).await;
938 }
939 });
940 }
941
942 async fn process_block_count(
943 &self,
944 dbtx: &mut DatabaseTransaction<'_>,
945 block_count_vote: u64,
946 peer: PeerId,
947 ) -> anyhow::Result<()> {
948 let old_consensus_block_count = self.consensus_block_count(dbtx).await;
949
950 let current_vote = dbtx
951 .insert_entry(&BlockCountVoteKey(peer), &block_count_vote)
952 .await
953 .unwrap_or(0);
954
955 ensure!(
956 current_vote < block_count_vote,
957 "Block count vote is redundant"
958 );
959
960 let new_consensus_block_count = self.consensus_block_count(dbtx).await;
961
962 assert!(old_consensus_block_count <= new_consensus_block_count);
963
964 debug!(
965 target: LOG_MODULE_WALLETV2,
966 %peer,
967 vote = block_count_vote,
968 old_consensus = old_consensus_block_count,
969 new_consensus = new_consensus_block_count,
970 advanced = new_consensus_block_count - old_consensus_block_count,
971 "Processed block count vote"
972 );
973
974 let scan_from_genesis = self.cfg.consensus.network == bitcoin::Network::Regtest;
979 if old_consensus_block_count == 0 && !scan_from_genesis {
980 return Ok(());
981 }
982
983 self.await_local_sync_to_block_count(
986 new_consensus_block_count + CONFIRMATION_FINALITY_DELAY,
987 )
988 .await;
989
990 for height in old_consensus_block_count..new_consensus_block_count {
991 if let Some(status) = self.btc_rpc.status() {
993 assert_eq!(status.network, self.cfg.consensus.network);
994 }
995
996 let block_hash = util::retry(
997 "get_block_hash",
998 util::backoff_util::background_backoff(),
999 || self.btc_rpc.get_block_hash(height),
1000 )
1001 .await
1002 .expect("Bitcoind rpc to get_block_hash failed");
1003
1004 let block = util::retry(
1005 "get_block",
1006 util::backoff_util::background_backoff(),
1007 || self.btc_rpc.get_block(&block_hash),
1008 )
1009 .await
1010 .expect("Bitcoind rpc to get_block failed");
1011
1012 assert_eq!(block.block_hash(), block_hash, "Block hash mismatch");
1013
1014 let pks_hash = self.cfg.consensus.bitcoin_pks.consensus_hash();
1015
1016 let txs_num = block.txdata.len();
1017 let mut potential_receives_num: usize = 0;
1018
1019 for tx in block.txdata {
1020 dbtx.remove_entry(&UnconfirmedTxKey(tx.compute_txid()))
1021 .await;
1022
1023 for (vout, tx_out) in tx.output.iter().enumerate() {
1029 if is_potential_receive(&tx_out.script_pubkey, &pks_hash) {
1030 let outpoint = bitcoin::OutPoint {
1031 txid: tx.compute_txid(),
1032 vout: u32::try_from(vout)
1033 .expect("Bitcoin transaction has more than u32::MAX outputs"),
1034 };
1035
1036 let index = dbtx
1037 .find_by_prefix_sorted_descending(&OutputPrefix)
1038 .await
1039 .next()
1040 .await
1041 .map_or(0, |entry| entry.0.0 + 1);
1042
1043 dbtx.insert_new_entry(&OutputKey(index), &Output(outpoint, tx_out.clone()))
1044 .await;
1045
1046 debug!(
1047 target: LOG_MODULE_WALLETV2,
1048 output_index = index,
1049 %outpoint,
1050 value_sat = tx_out.value.to_sat(),
1051 height,
1052 "Recorded potential walletv2 receive"
1053 );
1054
1055 potential_receives_num += 1;
1056 }
1057 }
1058 }
1059
1060 debug!(
1061 target: LOG_MODULE_WALLETV2,
1062 height,
1063 txs_num,
1064 potential_receives_num,
1065 "Scanned block"
1066 );
1067 }
1068
1069 Ok(())
1070 }
1071
1072 async fn process_signatures(
1073 &self,
1074 dbtx: &mut DatabaseTransaction<'_>,
1075 txid: bitcoin::Txid,
1076 signatures: Vec<Signature>,
1077 peer: PeerId,
1078 ) -> anyhow::Result<()> {
1079 let mut unsigned = dbtx
1080 .get_value(&UnsignedTxKey(txid))
1081 .await
1082 .context("Unsigned transaction does not exist")?;
1083
1084 let pk = self
1085 .cfg
1086 .consensus
1087 .bitcoin_pks
1088 .get(&peer)
1089 .expect("Failed to get public key of peer from config");
1090
1091 self.verify_signatures(&unsigned, &signatures, *pk)?;
1092
1093 if dbtx
1094 .insert_entry(&SignaturesKey(txid, peer), &signatures)
1095 .await
1096 .is_some()
1097 {
1098 bail!("Already received valid signatures from this peer")
1099 }
1100
1101 let signatures = dbtx
1102 .find_by_prefix(&SignaturesTxidPrefix(txid))
1103 .await
1104 .map(|(key, signatures)| (key.1, signatures))
1105 .collect::<BTreeMap<PeerId, Vec<Signature>>>()
1106 .await;
1107
1108 if signatures.len() == self.cfg.consensus.bitcoin_pks.to_num_peers().threshold() {
1109 dbtx.remove_entry(&UnsignedTxKey(txid)).await;
1110
1111 dbtx.remove_by_prefix(&SignaturesTxidPrefix(txid)).await;
1112
1113 self.finalize_tx(&mut unsigned, &signatures);
1114
1115 dbtx.insert_new_entry(&UnconfirmedTxKey(txid), &unsigned)
1116 .await;
1117
1118 if let Err(err) = self.btc_rpc.submit_transaction(unsigned.tx).await {
1119 debug!(
1120 target: LOG_MODULE_WALLETV2,
1121 err = %err.fmt_compact_anyhow(),
1122 "Error broadcasting finalized transaction"
1123 );
1124 }
1125 }
1126
1127 Ok(())
1128 }
1129
1130 async fn await_local_sync_to_block_count(&self, block_count: u64) {
1131 loop {
1132 if self
1133 .btc_rpc
1134 .status()
1135 .is_some_and(|status| status.block_count >= block_count)
1136 {
1137 break;
1138 }
1139
1140 info!(target: LOG_MODULE_WALLETV2, "Waiting for local bitcoin backend to sync to block count {block_count}");
1141
1142 sleep(common::sleep_duration()).await;
1143 }
1144 }
1145
1146 pub async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1147 let num_peers = self.cfg.consensus.bitcoin_pks.to_num_peers();
1148
1149 let mut counts = dbtx
1150 .find_by_prefix(&BlockCountVotePrefix)
1151 .await
1152 .map(|entry| entry.1)
1153 .collect::<Vec<u64>>()
1154 .await;
1155
1156 assert!(counts.len() <= num_peers.total());
1157
1158 counts.sort_unstable();
1159
1160 counts.reverse();
1161
1162 assert!(counts.last() <= counts.first());
1163
1164 counts.get(num_peers.threshold() - 1).copied().unwrap_or(0)
1169 }
1170
1171 pub async fn consensus_feerate(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<u64> {
1172 let num_peers = self.cfg.consensus.bitcoin_pks.to_num_peers();
1173
1174 let mut rates = dbtx
1175 .find_by_prefix(&FeeRateVotePrefix)
1176 .await
1177 .filter_map(|entry| async move { entry.1 })
1178 .collect::<Vec<u64>>()
1179 .await;
1180
1181 assert!(rates.len() <= num_peers.total());
1182
1183 rates.sort_unstable();
1184
1185 assert!(rates.first() <= rates.last());
1186
1187 rates.get(num_peers.threshold() - 1).copied()
1188 }
1189
1190 pub async fn consensus_fee(
1191 &self,
1192 dbtx: &mut DatabaseTransaction<'_>,
1193 tx_vbytes: u64,
1194 ) -> Option<Amount> {
1195 let pending_txs = pending_txs_unordered(dbtx).await;
1199
1200 assert!(pending_txs.len() <= 32);
1201
1202 let feerate = self
1203 .consensus_feerate(dbtx)
1204 .await?
1205 .max(self.cfg.consensus.feerate_base << pending_txs.len());
1206
1207 let tx_fee = tx_vbytes.saturating_mul(feerate).saturating_div(1000);
1208
1209 let stack_vbytes = pending_txs
1210 .iter()
1211 .map(|t| t.vbytes)
1212 .try_fold(tx_vbytes, u64::checked_add)
1213 .expect("Stack vbytes overflow with at most 32 pending txs");
1214
1215 let stack_fee = stack_vbytes.saturating_mul(feerate).saturating_div(1000);
1216
1217 let stack_fee = pending_txs
1219 .iter()
1220 .map(|t| t.fee.to_sat())
1221 .fold(stack_fee, u64::saturating_sub);
1222
1223 Some(Amount::from_sat(tx_fee.max(stack_fee)))
1224 }
1225
1226 pub async fn send_fee(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<Amount> {
1227 self.consensus_fee(dbtx, self.cfg.consensus.send_tx_vbytes)
1228 .await
1229 }
1230
1231 pub async fn receive_fee(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<Amount> {
1232 self.consensus_fee(dbtx, self.cfg.consensus.receive_tx_vbytes)
1233 .await
1234 }
1235
1236 fn descriptor(&self, tweak: &sha256::Hash) -> Wsh<secp256k1::PublicKey> {
1237 descriptor(&self.cfg.consensus.bitcoin_pks, tweak)
1238 }
1239
1240 fn sign_tx(&self, unsigned_tx: &FederationTx) -> Vec<Signature> {
1241 let mut sighash_cache = SighashCache::new(unsigned_tx.tx.clone());
1242
1243 unsigned_tx
1244 .spent_tx_outs
1245 .iter()
1246 .enumerate()
1247 .map(|(index, utxo)| {
1248 let descriptor = self.descriptor(&utxo.tweak).ecdsa_sighash_script_code();
1249
1250 let p2wsh_sighash = sighash_cache
1251 .p2wsh_signature_hash(index, &descriptor, utxo.value, EcdsaSighashType::All)
1252 .expect("Failed to compute P2WSH segwit sighash");
1253
1254 let scalar = &Scalar::from_be_bytes(utxo.tweak.to_byte_array())
1255 .expect("Hash is within field order");
1256
1257 let sk = self
1258 .cfg
1259 .private
1260 .bitcoin_sk
1261 .add_tweak(scalar)
1262 .expect("Failed to tweak bitcoin secret key");
1263
1264 Secp256k1::new().sign_ecdsa(&p2wsh_sighash.into(), &sk)
1265 })
1266 .collect()
1267 }
1268
1269 fn verify_signatures(
1270 &self,
1271 unsigned_tx: &FederationTx,
1272 signatures: &[Signature],
1273 pk: PublicKey,
1274 ) -> anyhow::Result<()> {
1275 ensure!(
1276 unsigned_tx.spent_tx_outs.len() == signatures.len(),
1277 "Incorrect number of signatures"
1278 );
1279
1280 let mut sighash_cache = SighashCache::new(unsigned_tx.tx.clone());
1281
1282 for ((index, utxo), signature) in unsigned_tx
1283 .spent_tx_outs
1284 .iter()
1285 .enumerate()
1286 .zip(signatures.iter())
1287 {
1288 let code = self.descriptor(&utxo.tweak).ecdsa_sighash_script_code();
1289
1290 let p2wsh_sighash = sighash_cache
1291 .p2wsh_signature_hash(index, &code, utxo.value, EcdsaSighashType::All)
1292 .expect("Failed to compute P2WSH segwit sighash");
1293
1294 let pk = tweak_public_key(&pk, &utxo.tweak);
1295
1296 secp256k1::SECP256K1.verify_ecdsa(&p2wsh_sighash.into(), signature, &pk)?;
1297 }
1298
1299 Ok(())
1300 }
1301
1302 fn finalize_tx(
1303 &self,
1304 federation_tx: &mut FederationTx,
1305 signatures: &BTreeMap<PeerId, Vec<Signature>>,
1306 ) {
1307 assert_eq!(
1308 federation_tx.spent_tx_outs.len(),
1309 federation_tx.tx.input.len()
1310 );
1311
1312 for (index, utxo) in federation_tx.spent_tx_outs.iter().enumerate() {
1313 let satisfier: BTreeMap<PublicKey, bitcoin::ecdsa::Signature> = signatures
1314 .iter()
1315 .map(|(peer, sigs)| {
1316 assert_eq!(sigs.len(), federation_tx.tx.input.len());
1317
1318 let pk = *self
1319 .cfg
1320 .consensus
1321 .bitcoin_pks
1322 .get(peer)
1323 .expect("Failed to get public key of peer from config");
1324
1325 let pk = tweak_public_key(&pk, &utxo.tweak);
1326
1327 (pk, bitcoin::ecdsa::Signature::sighash_all(sigs[index]))
1328 })
1329 .collect();
1330
1331 miniscript::Descriptor::Wsh(self.descriptor(&utxo.tweak))
1332 .satisfy(&mut federation_tx.tx.input[index], satisfier)
1333 .expect("Failed to satisfy descriptor");
1334 }
1335 }
1336
1337 async fn tx_id(&self, dbtx: &mut DatabaseTransaction<'_>, outpoint: OutPoint) -> Option<Txid> {
1338 let index = dbtx.get_value(&TxInfoIndexKey(outpoint)).await?;
1339
1340 dbtx.get_value(&TxInfoKey(index))
1341 .await
1342 .map(|entry| entry.txid)
1343 }
1344
1345 async fn get_outputs(
1346 &self,
1347 dbtx: &mut DatabaseTransaction<'_>,
1348 start_index: u64,
1349 end_index: u64,
1350 ) -> Vec<OutputInfo> {
1351 let spent: BTreeSet<u64> = dbtx
1352 .find_by_range(SpentOutputKey(start_index)..SpentOutputKey(end_index))
1353 .await
1354 .map(|entry| entry.0.0)
1355 .collect()
1356 .await;
1357
1358 dbtx.find_by_range(OutputKey(start_index)..OutputKey(end_index))
1359 .await
1360 .filter_map(|entry| {
1361 std::future::ready(entry.1.1.script_pubkey.is_p2wsh().then(|| OutputInfo {
1362 index: entry.0.0,
1363 script: entry.1.1.script_pubkey,
1364 value: entry.1.1.value,
1365 spent: spent.contains(&entry.0.0),
1366 outpoint: Some(entry.1.0),
1367 }))
1368 })
1369 .collect()
1370 .await
1371 }
1372
1373 async fn pending_tx_chain(&self, dbtx: &mut DatabaseTransaction<'_>) -> Vec<TxInfo> {
1374 let n_pending = pending_txs_unordered(dbtx).await.len();
1375
1376 dbtx.find_by_prefix_sorted_descending(&TxInfoPrefix)
1377 .await
1378 .take(n_pending)
1379 .map(|entry| entry.1)
1380 .collect()
1381 .await
1382 }
1383
1384 async fn tx_chain(&self, dbtx: &mut DatabaseTransaction<'_>) -> Vec<TxInfo> {
1385 dbtx.find_by_prefix(&TxInfoPrefix)
1386 .await
1387 .map(|entry| entry.1)
1388 .collect()
1389 .await
1390 }
1391
1392 async fn total_txs(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1393 dbtx.find_by_prefix_sorted_descending(&TxInfoPrefix)
1394 .await
1395 .next()
1396 .await
1397 .map_or(0, |entry| entry.0.0 + 1)
1398 }
1399
1400 pub fn network_ui(&self) -> Network {
1402 self.cfg.consensus.network
1403 }
1404
1405 pub async fn federation_wallet_ui(&self) -> Option<FederationWallet> {
1407 self.db
1408 .begin_transaction_nc()
1409 .await
1410 .get_value(&FederationWalletKey)
1411 .await
1412 }
1413
1414 pub async fn consensus_block_count_ui(&self) -> u64 {
1416 self.consensus_block_count(&mut self.db.begin_transaction_nc().await)
1417 .await
1418 }
1419
1420 pub async fn consensus_feerate_ui(&self) -> Option<u64> {
1422 self.consensus_feerate(&mut self.db.begin_transaction_nc().await)
1423 .await
1424 .map(|feerate| feerate / 1000)
1425 }
1426
1427 pub async fn send_fee_ui(&self) -> Option<Amount> {
1429 self.send_fee(&mut self.db.begin_transaction_nc().await)
1430 .await
1431 }
1432
1433 pub async fn receive_fee_ui(&self) -> Option<Amount> {
1435 self.receive_fee(&mut self.db.begin_transaction_nc().await)
1436 .await
1437 }
1438
1439 pub async fn pending_tx_chain_ui(&self) -> Vec<TxInfo> {
1441 self.pending_tx_chain(&mut self.db.begin_transaction_nc().await)
1442 .await
1443 }
1444
1445 pub async fn tx_chain_ui(&self) -> Vec<TxInfo> {
1447 self.tx_chain(&mut self.db.begin_transaction_nc().await)
1448 .await
1449 }
1450
1451 pub async fn recovery_keys_ui(&self) -> Option<(BTreeMap<PeerId, String>, String)> {
1454 let wallet = self.federation_wallet_ui().await?;
1455
1456 let pks = self
1457 .cfg
1458 .consensus
1459 .bitcoin_pks
1460 .iter()
1461 .map(|(peer, pk)| (*peer, tweak_public_key(pk, &wallet.tweak).to_string()))
1462 .collect();
1463
1464 let tweak = &Scalar::from_be_bytes(wallet.tweak.to_byte_array())
1465 .expect("Hash is within field order");
1466
1467 let sk = self
1468 .cfg
1469 .private
1470 .bitcoin_sk
1471 .add_tweak(tweak)
1472 .expect("Failed to tweak bitcoin secret key");
1473
1474 let sk = bitcoin::PrivateKey::new(sk, self.cfg.consensus.network).to_wif();
1475
1476 Some((pks, sk))
1477 }
1478}