Skip to main content

fedimint_walletv2_server/
lib.rs

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