Skip to main content

fedimint_wallet_server/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::cast_possible_wrap)]
4#![allow(clippy::default_trait_access)]
5#![allow(clippy::missing_errors_doc)]
6#![allow(clippy::missing_panics_doc)]
7#![allow(clippy::module_name_repetitions)]
8#![allow(clippy::must_use_candidate)]
9#![allow(clippy::needless_lifetimes)]
10#![allow(clippy::too_many_lines)]
11
12pub mod db;
13pub mod envs;
14
15use std::clone::Clone;
16use std::cmp::min;
17use std::collections::{BTreeMap, BTreeSet};
18use std::convert::Infallible;
19use std::sync::Arc;
20#[cfg(not(target_family = "wasm"))]
21use std::time::Duration;
22
23use anyhow::{Context, bail, ensure, format_err};
24use bitcoin::absolute::LockTime;
25use bitcoin::address::NetworkUnchecked;
26use bitcoin::ecdsa::Signature as EcdsaSig;
27use bitcoin::hashes::{Hash as BitcoinHash, HashEngine, Hmac, HmacEngine, sha256};
28use bitcoin::policy::DEFAULT_MIN_RELAY_TX_FEE;
29use bitcoin::psbt::{Input, Psbt};
30use bitcoin::secp256k1::{self, All, Message, Scalar, Secp256k1, Verification};
31use bitcoin::sighash::{EcdsaSighashType, SighashCache};
32use bitcoin::{Address, BlockHash, Network, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid};
33use common::config::WalletConfigConsensus;
34use common::{
35    DEPRECATED_RBF_ERROR, PegOutFees, PegOutSignatureItem, ProcessPegOutSigError, SpendableUTXO,
36    TxOutputSummary, WalletCommonInit, WalletConsensusItem, WalletInput, WalletModuleTypes,
37    WalletOutput, WalletOutputOutcome, WalletSummary, proprietary_tweak_key,
38};
39use db::{
40    BlockHashByHeightKey, BlockHashByHeightKeyPrefix, BlockHashByHeightValue, RecoveryItemKey,
41    RecoveryItemKeyPrefix,
42};
43use envs::get_feerate_multiplier;
44use fedimint_api_client::api::{DynModuleApi, FederationApiExt};
45use fedimint_core::config::{
46    ServerModuleConfig, ServerModuleConsensusConfig, TypedServerModuleConfig,
47    TypedServerModuleConsensusConfig,
48};
49use fedimint_core::core::ModuleInstanceId;
50use fedimint_core::db::{
51    Database, DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped,
52};
53use fedimint_core::encoding::btc::NetworkLegacyEncodingWrapper;
54use fedimint_core::encoding::{Decodable, Encodable};
55use fedimint_core::envs::{
56    BitcoinRpcConfig, FM_ENABLE_MODULE_WALLET_ENV,
57    FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING_ENV, FM_WALLET_FEERATE_SOURCES_ENV,
58    is_automatic_consensus_version_voting_disabled, is_env_var_set_opt, is_rbf_withdrawal_enabled,
59    is_running_in_test_env,
60};
61use fedimint_core::module::audit::Audit;
62use fedimint_core::module::{
63    Amounts, ApiEndpoint, ApiError, ApiRequestErased, ApiVersion, CoreConsensusVersion, InputMeta,
64    ModuleConsensusVersion, ModuleInit, TransactionItemAmounts, api_endpoint,
65};
66use fedimint_core::net::auth::check_auth;
67use fedimint_core::task::TaskGroup;
68#[cfg(not(target_family = "wasm"))]
69use fedimint_core::task::sleep;
70use fedimint_core::util::{FmtCompact, FmtCompactAnyhow as _, backoff_util, retry};
71use fedimint_core::{
72    Feerate, InPoint, NumPeersExt, OutPoint, PeerId, apply, async_trait_maybe_send,
73    get_network_for_address, push_db_key_items, push_db_pair_items,
74};
75use fedimint_logging::LOG_MODULE_WALLET;
76use fedimint_server_core::bitcoin_rpc::ServerBitcoinRpcMonitor;
77use fedimint_server_core::config::{PeerHandleOps, PeerHandleOpsExt};
78use fedimint_server_core::migration::ServerModuleDbMigrationFn;
79use fedimint_server_core::{
80    ConfigGenModuleArgs, EnvVarDoc, ServerModule, ServerModuleInit, ServerModuleInitArgs,
81};
82pub use fedimint_wallet_common as common;
83use fedimint_wallet_common::config::{FeeConsensus, WalletClientConfig, WalletConfig};
84use fedimint_wallet_common::endpoint_constants::{
85    ACTIVATE_CONSENSUS_VERSION_VOTING_ENDPOINT, BITCOIN_KIND_ENDPOINT, BITCOIN_RPC_CONFIG_ENDPOINT,
86    BLOCK_COUNT_ENDPOINT, BLOCK_COUNT_LOCAL_ENDPOINT, MODULE_CONSENSUS_VERSION_ENDPOINT,
87    PEG_OUT_FEES_ENDPOINT, RECOVERY_COUNT_ENDPOINT, RECOVERY_SLICE_ENDPOINT,
88    SUPPORTED_MODULE_CONSENSUS_VERSION_ENDPOINT, UTXO_CONFIRMED_ENDPOINT, WALLET_SUMMARY_ENDPOINT,
89};
90use fedimint_wallet_common::envs::FM_PORT_ESPLORA_ENV;
91use fedimint_wallet_common::keys::CompressedPublicKey;
92use fedimint_wallet_common::tweakable::Tweakable;
93use fedimint_wallet_common::{
94    MODULE_CONSENSUS_VERSION, Rbf, RecoveryItem, UnknownWalletInputVariantError, WalletInputError,
95    WalletOutputError, WalletOutputV0,
96};
97use futures::future::join_all;
98use futures::{FutureExt, StreamExt};
99use itertools::Itertools;
100use metrics::{
101    WALLET_INOUT_FEES_SATS, WALLET_INOUT_SATS, WALLET_PEGIN_FEES_SATS, WALLET_PEGIN_SATS,
102    WALLET_PEGOUT_FEES_SATS, WALLET_PEGOUT_SATS,
103};
104use miniscript::psbt::PsbtExt;
105use miniscript::{Descriptor, TranslatePk, translate_hash_fail};
106use rand::rngs::OsRng;
107use serde::Serialize;
108use strum::IntoEnumIterator;
109use tokio::sync::{Notify, watch};
110use tracing::{debug, info, instrument, trace, warn};
111
112use crate::db::{
113    BlockCountVoteKey, BlockCountVotePrefix, BlockHashKey, BlockHashKeyPrefix,
114    ClaimedPegInOutpointKey, ClaimedPegInOutpointPrefixKey, ConsensusVersionVoteKey,
115    ConsensusVersionVotePrefix, ConsensusVersionVotingActivationKey,
116    ConsensusVersionVotingActivationPrefix, DbKeyPrefix, FeeRateVoteKey, FeeRateVotePrefix,
117    PegOutBitcoinTransaction, PegOutBitcoinTransactionPrefix, PegOutNonceKey, PegOutTxSignatureCI,
118    PegOutTxSignatureCIPrefix, PendingTransactionKey, PendingTransactionPrefixKey, UTXOKey,
119    UTXOPrefixKey, UnsignedTransactionKey, UnsignedTransactionPrefixKey, UnspentTxOutKey,
120    UnspentTxOutPrefix, migrate_to_v1, migrate_to_v2,
121};
122use crate::metrics::WALLET_BLOCK_COUNT;
123
124mod metrics;
125
126#[derive(Debug, Clone)]
127pub struct WalletInit;
128
129impl ModuleInit for WalletInit {
130    type Common = WalletCommonInit;
131
132    async fn dump_database(
133        &self,
134        dbtx: &mut DatabaseTransaction<'_>,
135        prefix_names: Vec<String>,
136    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
137        let mut wallet: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
138        let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
139            prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
140        });
141        for table in filtered_prefixes {
142            match table {
143                DbKeyPrefix::BlockHash => {
144                    push_db_key_items!(dbtx, BlockHashKeyPrefix, BlockHashKey, wallet, "Blocks");
145                }
146                DbKeyPrefix::BlockHashByHeight => {
147                    push_db_key_items!(
148                        dbtx,
149                        BlockHashByHeightKeyPrefix,
150                        BlockHashByHeightKey,
151                        wallet,
152                        "Blocks by height"
153                    );
154                }
155                DbKeyPrefix::PegOutBitcoinOutPoint => {
156                    push_db_pair_items!(
157                        dbtx,
158                        PegOutBitcoinTransactionPrefix,
159                        PegOutBitcoinTransaction,
160                        WalletOutputOutcome,
161                        wallet,
162                        "Peg Out Bitcoin Transaction"
163                    );
164                }
165                DbKeyPrefix::PegOutTxSigCi => {
166                    push_db_pair_items!(
167                        dbtx,
168                        PegOutTxSignatureCIPrefix,
169                        PegOutTxSignatureCI,
170                        Vec<secp256k1::ecdsa::Signature>,
171                        wallet,
172                        "Peg Out Transaction Signatures"
173                    );
174                }
175                DbKeyPrefix::PendingTransaction => {
176                    push_db_pair_items!(
177                        dbtx,
178                        PendingTransactionPrefixKey,
179                        PendingTransactionKey,
180                        PendingTransaction,
181                        wallet,
182                        "Pending Transactions"
183                    );
184                }
185                DbKeyPrefix::PegOutNonce => {
186                    if let Some(nonce) = dbtx.get_value(&PegOutNonceKey).await {
187                        wallet.insert("Peg Out Nonce".to_string(), Box::new(nonce));
188                    }
189                }
190                DbKeyPrefix::UnsignedTransaction => {
191                    push_db_pair_items!(
192                        dbtx,
193                        UnsignedTransactionPrefixKey,
194                        UnsignedTransactionKey,
195                        UnsignedTransaction,
196                        wallet,
197                        "Unsigned Transactions"
198                    );
199                }
200                DbKeyPrefix::Utxo => {
201                    push_db_pair_items!(
202                        dbtx,
203                        UTXOPrefixKey,
204                        UTXOKey,
205                        SpendableUTXO,
206                        wallet,
207                        "UTXOs"
208                    );
209                }
210                DbKeyPrefix::BlockCountVote => {
211                    push_db_pair_items!(
212                        dbtx,
213                        BlockCountVotePrefix,
214                        BlockCountVoteKey,
215                        u32,
216                        wallet,
217                        "Block Count Votes"
218                    );
219                }
220                DbKeyPrefix::FeeRateVote => {
221                    push_db_pair_items!(
222                        dbtx,
223                        FeeRateVotePrefix,
224                        FeeRateVoteKey,
225                        Feerate,
226                        wallet,
227                        "Fee Rate Votes"
228                    );
229                }
230                DbKeyPrefix::ClaimedPegInOutpoint => {
231                    push_db_pair_items!(
232                        dbtx,
233                        ClaimedPegInOutpointPrefixKey,
234                        PeggedInOutpointKey,
235                        (),
236                        wallet,
237                        "Claimed Peg-in Outpoint"
238                    );
239                }
240                DbKeyPrefix::ConsensusVersionVote => {
241                    push_db_pair_items!(
242                        dbtx,
243                        ConsensusVersionVotePrefix,
244                        ConsensusVersionVoteKey,
245                        ModuleConsensusVersion,
246                        wallet,
247                        "Consensus Version Votes"
248                    );
249                }
250                DbKeyPrefix::UnspentTxOut => {
251                    push_db_pair_items!(
252                        dbtx,
253                        UnspentTxOutPrefix,
254                        UnspentTxOutKey,
255                        TxOut,
256                        wallet,
257                        "Consensus Version Votes"
258                    );
259                }
260                DbKeyPrefix::ConsensusVersionVotingActivation => {
261                    push_db_pair_items!(
262                        dbtx,
263                        ConsensusVersionVotingActivationPrefix,
264                        ConsensusVersionVotingActivationKey,
265                        (),
266                        wallet,
267                        "Consensus Version Voting Activation Key"
268                    );
269                }
270                DbKeyPrefix::RecoveryItem => {
271                    push_db_pair_items!(
272                        dbtx,
273                        RecoveryItemKeyPrefix,
274                        RecoveryItemKey,
275                        RecoveryItem,
276                        wallet,
277                        "Recovery Items"
278                    );
279                }
280            }
281        }
282
283        Box::new(wallet.into_iter())
284    }
285}
286
287/// Default finality delay based on network
288fn default_finality_delay(network: Network) -> u32 {
289    match network {
290        Network::Bitcoin | Network::Regtest => 10,
291        Network::Testnet | Network::Signet | Network::Testnet4 => 2,
292    }
293}
294
295/// Default Bitcoin RPC config for clients
296fn default_client_bitcoin_rpc(network: Network) -> BitcoinRpcConfig {
297    let url = match network {
298        Network::Bitcoin => "https://mempool.space/api/".to_string(),
299        Network::Testnet => "https://mempool.space/testnet/api/".to_string(),
300        Network::Testnet4 => "https://mempool.space/testnet4/api/".to_string(),
301        Network::Signet => "https://mutinynet.com/api/".to_string(),
302        Network::Regtest => format!(
303            "http://127.0.0.1:{}/",
304            std::env::var(FM_PORT_ESPLORA_ENV).unwrap_or_else(|_| String::from("50002"))
305        ),
306    };
307
308    BitcoinRpcConfig {
309        kind: "esplora".to_string(),
310        url: fedimint_core::util::SafeUrl::parse(&url).expect("hardcoded URL is valid"),
311    }
312}
313
314#[apply(async_trait_maybe_send!)]
315impl ServerModuleInit for WalletInit {
316    type Module = Wallet;
317
318    fn versions(&self, _core: CoreConsensusVersion) -> &[ModuleConsensusVersion] {
319        &[MODULE_CONSENSUS_VERSION]
320    }
321
322    fn is_enabled_by_default(&self) -> bool {
323        is_env_var_set_opt(FM_ENABLE_MODULE_WALLET_ENV).unwrap_or(false)
324    }
325
326    fn get_documented_env_vars(&self) -> Vec<EnvVarDoc> {
327        vec![
328            EnvVarDoc {
329                name: FM_ENABLE_MODULE_WALLET_ENV,
330                description: "Set to 1/true to enable the wallet (on-chain Bitcoin) module. Disabled by default.",
331            },
332            EnvVarDoc {
333                name: FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING_ENV,
334                description: "Set to 1/true to disable automatic consensus version voting. Useful for testing and development.",
335            },
336            EnvVarDoc {
337                name: envs::FM_WALLET_FEERATE_MULTIPLIER_ENV,
338                description: "Multiplier applied to fee rate estimates (float, clamped 1.0–32.0). Defaults to 1.0.",
339            },
340            EnvVarDoc {
341                name: FM_WALLET_FEERATE_SOURCES_ENV,
342                description: "Semicolon-separated list of JSON API URLs (with optional `#<jq-filter>`) used as fee rate sources.",
343            },
344        ]
345    }
346
347    async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
348        for direction in ["incoming", "outgoing"] {
349            WALLET_INOUT_FEES_SATS
350                .with_label_values(&[direction])
351                .get_sample_count();
352            WALLET_INOUT_SATS
353                .with_label_values(&[direction])
354                .get_sample_count();
355        }
356        // Eagerly initialize metrics that trigger infrequently
357        WALLET_PEGIN_FEES_SATS.get_sample_count();
358        WALLET_PEGIN_SATS.get_sample_count();
359        WALLET_PEGOUT_SATS.get_sample_count();
360        WALLET_PEGOUT_FEES_SATS.get_sample_count();
361
362        Ok(Wallet::new(
363            args.cfg().to_typed()?,
364            args.db(),
365            args.task_group(),
366            args.our_peer_id(),
367            args.module_api().clone(),
368            args.server_bitcoin_rpc_monitor(),
369        )
370        .await?)
371    }
372
373    fn trusted_dealer_gen(
374        &self,
375        peers: &[PeerId],
376        args: &ConfigGenModuleArgs,
377    ) -> BTreeMap<PeerId, ServerModuleConfig> {
378        let secp = bitcoin::secp256k1::Secp256k1::new();
379        let finality_delay = default_finality_delay(args.network);
380        let client_default_bitcoin_rpc = default_client_bitcoin_rpc(args.network);
381
382        let btc_pegin_keys = peers
383            .iter()
384            .map(|&id| (id, secp.generate_keypair(&mut OsRng)))
385            .collect::<Vec<_>>();
386
387        let wallet_cfg: BTreeMap<PeerId, WalletConfig> = btc_pegin_keys
388            .iter()
389            .map(|(id, (sk, _))| {
390                let cfg = WalletConfig::new(
391                    btc_pegin_keys
392                        .iter()
393                        .map(|(peer_id, (_, pk))| (*peer_id, CompressedPublicKey { key: *pk }))
394                        .collect(),
395                    *sk,
396                    peers.to_num_peers().threshold(),
397                    args.network,
398                    finality_delay,
399                    client_default_bitcoin_rpc.clone(),
400                    FeeConsensus::default(),
401                );
402                (*id, cfg)
403            })
404            .collect();
405
406        wallet_cfg
407            .into_iter()
408            .map(|(k, v)| (k, v.to_erased()))
409            .collect()
410    }
411
412    async fn distributed_gen(
413        &self,
414        peers: &(dyn PeerHandleOps + Send + Sync),
415        args: &ConfigGenModuleArgs,
416    ) -> anyhow::Result<ServerModuleConfig> {
417        let secp = secp256k1::Secp256k1::new();
418        let (sk, pk) = secp.generate_keypair(&mut OsRng);
419        let our_key = CompressedPublicKey { key: pk };
420        let peer_peg_in_keys: BTreeMap<PeerId, CompressedPublicKey> = peers
421            .exchange_encodable(our_key.key)
422            .await?
423            .into_iter()
424            .map(|(k, key)| (k, CompressedPublicKey { key }))
425            .collect();
426
427        let finality_delay = default_finality_delay(args.network);
428        let client_default_bitcoin_rpc = default_client_bitcoin_rpc(args.network);
429
430        let wallet_cfg = WalletConfig::new(
431            peer_peg_in_keys,
432            sk,
433            peers.num_peers().threshold(),
434            args.network,
435            finality_delay,
436            client_default_bitcoin_rpc,
437            FeeConsensus::default(),
438        );
439
440        Ok(wallet_cfg.to_erased())
441    }
442
443    fn validate_config(&self, identity: &PeerId, config: ServerModuleConfig) -> anyhow::Result<()> {
444        let config = config.to_typed::<WalletConfig>()?;
445        let pubkey = secp256k1::PublicKey::from_secret_key_global(&config.private.peg_in_key);
446
447        if config
448            .consensus
449            .peer_peg_in_keys
450            .get(identity)
451            .ok_or_else(|| format_err!("Secret key doesn't match any public key"))?
452            != &CompressedPublicKey::new(pubkey)
453        {
454            bail!(" Bitcoin wallet private key doesn't match multisig pubkey");
455        }
456
457        Ok(())
458    }
459
460    fn get_client_config(
461        &self,
462        config: &ServerModuleConsensusConfig,
463    ) -> anyhow::Result<WalletClientConfig> {
464        let config = WalletConfigConsensus::from_erased(config)?;
465        Ok(WalletClientConfig {
466            peg_in_descriptor: config.peg_in_descriptor,
467            network: config.network,
468            fee_consensus: config.fee_consensus,
469            finality_delay: config.finality_delay,
470            default_bitcoin_rpc: config.client_default_bitcoin_rpc,
471        })
472    }
473
474    /// DB migrations to move from old to newer versions
475    fn get_database_migrations(
476        &self,
477    ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Wallet>> {
478        let mut migrations: BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Wallet>> =
479            BTreeMap::new();
480        migrations.insert(
481            DatabaseVersion(0),
482            Box::new(|ctx| migrate_to_v1(ctx).boxed()),
483        );
484        migrations.insert(
485            DatabaseVersion(1),
486            Box::new(|ctx| migrate_to_v2(ctx).boxed()),
487        );
488        migrations
489    }
490
491    fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
492        Some(DbKeyPrefix::iter().map(|p| p as u8).collect())
493    }
494}
495
496#[apply(async_trait_maybe_send!)]
497impl ServerModule for Wallet {
498    type Common = WalletModuleTypes;
499    type Init = WalletInit;
500
501    async fn consensus_proposal<'a>(
502        &'a self,
503        dbtx: &mut DatabaseTransaction<'_>,
504    ) -> Vec<WalletConsensusItem> {
505        let mut items = dbtx
506            .find_by_prefix(&PegOutTxSignatureCIPrefix)
507            .await
508            .map(|(key, val)| {
509                WalletConsensusItem::PegOutSignature(PegOutSignatureItem {
510                    txid: key.0,
511                    signature: val,
512                })
513            })
514            .collect::<Vec<WalletConsensusItem>>()
515            .await;
516
517        // If we are unable to get a block count from the node we skip adding a block
518        // count vote to consensus items.
519        //
520        // The potential impact of not including the latest block count from our peer's
521        // node is delayed processing of change outputs for the federation, which is an
522        // acceptable risk since subsequent rounds of consensus will reattempt to fetch
523        // the latest block count.
524        match self.get_block_count() {
525            Ok(block_count) => {
526                let mut block_count_vote =
527                    block_count.saturating_sub(self.cfg.consensus.finality_delay);
528
529                let current_consensus_block_count = self.consensus_block_count(dbtx).await;
530
531                // This will prevent that more then five blocks are synced in a single database
532                // transaction if the federation was offline for a prolonged period of time.
533                if current_consensus_block_count != 0 {
534                    block_count_vote = min(
535                        block_count_vote,
536                        current_consensus_block_count
537                            + if is_running_in_test_env() {
538                                // We have some tests that mine *a lot* of blocks (empty)
539                                // and need them processed fast, so we raise the max.
540                                100
541                            } else {
542                                5
543                            },
544                    );
545                }
546
547                let current_vote = dbtx
548                    .get_value(&BlockCountVoteKey(self.our_peer_id))
549                    .await
550                    .unwrap_or(0);
551
552                trace!(
553                    target: LOG_MODULE_WALLET,
554                    ?current_vote,
555                    ?block_count_vote,
556                    ?block_count,
557                    ?current_consensus_block_count,
558                    "Proposing block count"
559                );
560
561                WALLET_BLOCK_COUNT.set(i64::from(block_count_vote));
562                items.push(WalletConsensusItem::BlockCount(block_count_vote));
563            }
564            Err(err) => {
565                warn!(target: LOG_MODULE_WALLET, err = %err.fmt_compact_anyhow(), "Can't update block count");
566            }
567        }
568
569        let fee_rate_proposal = self.get_fee_rate_opt();
570
571        items.push(WalletConsensusItem::Feerate(fee_rate_proposal));
572
573        // Consensus upgrade activation voting
574        let manual_vote = dbtx
575            .get_value(&ConsensusVersionVotingActivationKey)
576            .await
577            .map(|()| {
578                // TODO: allow voting on any version between the currently active and max
579                // supported one in case we support a too high one already
580                MODULE_CONSENSUS_VERSION
581            });
582
583        let active_consensus_version = self.consensus_module_consensus_version(dbtx).await;
584        let automatic_vote = if is_automatic_consensus_version_voting_disabled() {
585            None
586        } else {
587            self.peer_supported_consensus_version
588                .borrow()
589                .and_then(|supported_consensus_version| {
590                    // Only automatically vote if the commonly supported version is higher than the
591                    // currently active one
592                    (active_consensus_version < supported_consensus_version)
593                        .then_some(supported_consensus_version)
594                })
595        };
596
597        // Prioritizing automatic vote for now since the manual vote never resets. Once
598        // that is fixed this should be switched around.
599        if let Some(vote_version) = automatic_vote.or(manual_vote) {
600            items.push(WalletConsensusItem::ModuleConsensusVersion(vote_version));
601        }
602
603        items
604    }
605
606    async fn process_consensus_item<'a, 'b>(
607        &'a self,
608        dbtx: &mut DatabaseTransaction<'b>,
609        consensus_item: WalletConsensusItem,
610        peer: PeerId,
611    ) -> anyhow::Result<()> {
612        trace!(target: LOG_MODULE_WALLET, ?consensus_item, "Processing consensus item proposal");
613
614        match consensus_item {
615            WalletConsensusItem::BlockCount(block_count_vote) => {
616                let current_vote = dbtx.get_value(&BlockCountVoteKey(peer)).await.unwrap_or(0);
617
618                if block_count_vote < current_vote {
619                    warn!(target: LOG_MODULE_WALLET, ?peer, ?block_count_vote, "Block count vote is outdated");
620                }
621
622                ensure!(
623                    block_count_vote > current_vote,
624                    "Block count vote is redundant"
625                );
626
627                let old_consensus_block_count = self.consensus_block_count(dbtx).await;
628
629                dbtx.insert_entry(&BlockCountVoteKey(peer), &block_count_vote)
630                    .await;
631
632                let new_consensus_block_count = self.consensus_block_count(dbtx).await;
633
634                debug!(
635                    target: LOG_MODULE_WALLET,
636                    ?peer,
637                    ?current_vote,
638                    ?block_count_vote,
639                    ?old_consensus_block_count,
640                    ?new_consensus_block_count,
641                    "Received block count vote"
642                );
643
644                assert!(old_consensus_block_count <= new_consensus_block_count);
645
646                if new_consensus_block_count != old_consensus_block_count {
647                    // We do not sync blocks that predate the federation itself
648                    if old_consensus_block_count != 0 {
649                        self.sync_up_to_consensus_count(
650                            dbtx,
651                            old_consensus_block_count,
652                            new_consensus_block_count,
653                        )
654                        .await;
655                    } else {
656                        info!(
657                            target: LOG_MODULE_WALLET,
658                            ?old_consensus_block_count,
659                            ?new_consensus_block_count,
660                            "Not syncing up to consensus block count because we are at block 0"
661                        );
662                    }
663                }
664            }
665            WalletConsensusItem::Feerate(feerate) => {
666                if Some(feerate) == dbtx.insert_entry(&FeeRateVoteKey(peer), &feerate).await {
667                    bail!("Fee rate vote is redundant");
668                }
669            }
670            WalletConsensusItem::PegOutSignature(peg_out_signature) => {
671                let txid = peg_out_signature.txid;
672
673                if dbtx.get_value(&PendingTransactionKey(txid)).await.is_some() {
674                    bail!("Already received a threshold of valid signatures");
675                }
676
677                let mut unsigned = dbtx
678                    .get_value(&UnsignedTransactionKey(txid))
679                    .await
680                    .context("Unsigned transaction does not exist")?;
681
682                self.sign_peg_out_psbt(&mut unsigned.psbt, peer, &peg_out_signature)
683                    .context("Peg out signature is invalid")?;
684
685                dbtx.insert_entry(&UnsignedTransactionKey(txid), &unsigned)
686                    .await;
687
688                if let Ok(pending_tx) = self.finalize_peg_out_psbt(unsigned) {
689                    // We were able to finalize the transaction, so we will delete the
690                    // PSBT and instead keep the extracted tx for periodic transmission
691                    // as well as to accept the change into our wallet eventually once
692                    // it confirms.
693                    dbtx.insert_new_entry(&PendingTransactionKey(txid), &pending_tx)
694                        .await;
695
696                    dbtx.remove_entry(&PegOutTxSignatureCI(txid)).await;
697                    dbtx.remove_entry(&UnsignedTransactionKey(txid)).await;
698                    let broadcast_pending = self.broadcast_pending.clone();
699                    dbtx.on_commit(move || {
700                        broadcast_pending.notify_one();
701                    });
702                }
703            }
704            WalletConsensusItem::ModuleConsensusVersion(module_consensus_version) => {
705                let current_vote = dbtx
706                    .get_value(&ConsensusVersionVoteKey(peer))
707                    .await
708                    .unwrap_or(ModuleConsensusVersion::new(2, 0));
709
710                ensure!(
711                    module_consensus_version > current_vote,
712                    "Module consensus version vote is redundant"
713                );
714
715                dbtx.insert_entry(&ConsensusVersionVoteKey(peer), &module_consensus_version)
716                    .await;
717
718                assert!(
719                    self.consensus_module_consensus_version(dbtx).await <= MODULE_CONSENSUS_VERSION,
720                    "Wallet module does not support new consensus version, please upgrade the module"
721                );
722            }
723            WalletConsensusItem::Default { variant, .. } => {
724                panic!("Received wallet consensus item with unknown variant {variant}");
725            }
726        }
727
728        Ok(())
729    }
730
731    async fn process_input<'a, 'b, 'c>(
732        &'a self,
733        dbtx: &mut DatabaseTransaction<'c>,
734        input: &'b WalletInput,
735        _in_point: InPoint,
736    ) -> Result<InputMeta, WalletInputError> {
737        let (outpoint, tx_out, pub_key) = match input {
738            WalletInput::V0(input) => {
739                if !self.block_is_known(dbtx, input.proof_block()).await {
740                    return Err(WalletInputError::UnknownPegInProofBlock(
741                        input.proof_block(),
742                    ));
743                }
744
745                input.verify(&self.secp, &self.cfg.consensus.peg_in_descriptor)?;
746
747                debug!(target: LOG_MODULE_WALLET, outpoint = %input.outpoint(), "Claiming peg-in");
748
749                (input.0.outpoint(), input.tx_output(), input.tweak_key())
750            }
751            WalletInput::V1(input) => {
752                let input_tx_out = dbtx
753                    .get_value(&UnspentTxOutKey(input.outpoint))
754                    .await
755                    .ok_or(WalletInputError::UnknownUTXO)?;
756
757                if input_tx_out.script_pubkey
758                    != self
759                        .cfg
760                        .consensus
761                        .peg_in_descriptor
762                        .tweak(&input.tweak_key, secp256k1::SECP256K1)
763                        .script_pubkey()
764                {
765                    return Err(WalletInputError::WrongOutputScript);
766                }
767
768                // Verifying this is not strictly necessary for the server as the tx_out is only
769                // used in backup and recovery.
770                if input.tx_out != input_tx_out {
771                    return Err(WalletInputError::WrongTxOut);
772                }
773
774                (input.outpoint, input_tx_out, input.tweak_key)
775            }
776            WalletInput::Default { variant, .. } => {
777                return Err(WalletInputError::UnknownInputVariant(
778                    UnknownWalletInputVariantError { variant: *variant },
779                ));
780            }
781        };
782
783        if dbtx
784            .insert_entry(&ClaimedPegInOutpointKey(outpoint), &())
785            .await
786            .is_some()
787        {
788            return Err(WalletInputError::PegInAlreadyClaimed);
789        }
790
791        dbtx.insert_new_entry(
792            &UTXOKey(outpoint),
793            &SpendableUTXO {
794                tweak: pub_key.serialize(),
795                amount: tx_out.value,
796            },
797        )
798        .await;
799
800        let next_index = get_recovery_count(dbtx).await;
801        dbtx.insert_new_entry(
802            &RecoveryItemKey(next_index),
803            &RecoveryItem::Input {
804                outpoint,
805                script: tx_out.script_pubkey,
806            },
807        )
808        .await;
809
810        let amount = tx_out.value.into();
811
812        let fee = self.cfg.consensus.fee_consensus.peg_in_abs;
813
814        calculate_pegin_metrics(dbtx, amount, fee);
815
816        Ok(InputMeta {
817            amount: TransactionItemAmounts {
818                amounts: Amounts::new_bitcoin(amount),
819                fees: Amounts::new_bitcoin(fee),
820            },
821            pub_key,
822        })
823    }
824
825    async fn process_output<'a, 'b>(
826        &'a self,
827        dbtx: &mut DatabaseTransaction<'b>,
828        output: &'a WalletOutput,
829        out_point: OutPoint,
830    ) -> Result<TransactionItemAmounts, WalletOutputError> {
831        let output = output.ensure_v0_ref()?;
832
833        // In 0.4.0 we began preventing RBF withdrawals. Once we reach EoL support
834        // for 0.4.0, we can safely remove RBF withdrawal logic.
835        // see: https://github.com/fedimint/fedimint/issues/5453
836        if let WalletOutputV0::Rbf(_) = output {
837            // This exists as an escape hatch for any federations that successfully
838            // processed an RBF withdrawal due to having a single UTXO owned by the
839            // federation. If a peer needs to resync the federation's history, they can
840            // enable this variable until they've successfully synced, then restart with
841            // this disabled.
842            if is_rbf_withdrawal_enabled() {
843                warn!(target: LOG_MODULE_WALLET, "processing rbf withdrawal");
844            } else {
845                return Err(DEPRECATED_RBF_ERROR);
846            }
847        }
848
849        let change_tweak = self.consensus_nonce(dbtx).await;
850
851        let mut tx = self.create_peg_out_tx(dbtx, output, &change_tweak).await?;
852
853        let fee_rate = self.consensus_fee_rate(dbtx).await;
854
855        StatelessWallet::validate_tx(&tx, output, fee_rate, self.cfg.consensus.network.0)?;
856
857        self.offline_wallet().sign_psbt(&mut tx.psbt);
858
859        let txid = tx.psbt.unsigned_tx.compute_txid();
860
861        info!(
862            target: LOG_MODULE_WALLET,
863            %txid,
864            "Signing peg out",
865        );
866
867        let sigs = tx
868            .psbt
869            .inputs
870            .iter_mut()
871            .map(|input| {
872                assert_eq!(
873                    input.partial_sigs.len(),
874                    1,
875                    "There was already more than one (our) or no signatures in input"
876                );
877
878                // TODO: don't put sig into PSBT in the first place
879                // We actually take out our own signature so everyone finalizes the tx in the
880                // same epoch.
881                let sig = std::mem::take(&mut input.partial_sigs)
882                    .into_values()
883                    .next()
884                    .expect("asserted previously");
885
886                // We drop SIGHASH_ALL, because we always use that and it is only present in the
887                // PSBT for compatibility with other tools.
888                secp256k1::ecdsa::Signature::from_der(&sig.to_vec()[..sig.to_vec().len() - 1])
889                    .expect("we serialized it ourselves that way")
890            })
891            .collect::<Vec<_>>();
892
893        // Delete used UTXOs
894        for input in &tx.psbt.unsigned_tx.input {
895            dbtx.remove_entry(&UTXOKey(input.previous_output)).await;
896        }
897
898        dbtx.insert_new_entry(&UnsignedTransactionKey(txid), &tx)
899            .await;
900
901        dbtx.insert_new_entry(&PegOutTxSignatureCI(txid), &sigs)
902            .await;
903
904        dbtx.insert_new_entry(
905            &PegOutBitcoinTransaction(out_point),
906            &WalletOutputOutcome::new_v0(txid),
907        )
908        .await;
909        let amount: fedimint_core::Amount = output.amount().into();
910        let fee = self.cfg.consensus.fee_consensus.peg_out_abs;
911        calculate_pegout_metrics(dbtx, amount, fee);
912        Ok(TransactionItemAmounts {
913            amounts: Amounts::new_bitcoin(amount),
914            fees: Amounts::new_bitcoin(fee),
915        })
916    }
917
918    async fn output_status(
919        &self,
920        dbtx: &mut DatabaseTransaction<'_>,
921        out_point: OutPoint,
922    ) -> Option<WalletOutputOutcome> {
923        dbtx.get_value(&PegOutBitcoinTransaction(out_point)).await
924    }
925
926    async fn audit(
927        &self,
928        dbtx: &mut DatabaseTransaction<'_>,
929        audit: &mut Audit,
930        module_instance_id: ModuleInstanceId,
931    ) {
932        audit
933            .add_items(dbtx, module_instance_id, &UTXOPrefixKey, |_, v| {
934                v.amount.to_sat() as i64 * 1000
935            })
936            .await;
937        audit
938            .add_items(
939                dbtx,
940                module_instance_id,
941                &UnsignedTransactionPrefixKey,
942                |_, v| match v.rbf {
943                    None => v.change.to_sat() as i64 * 1000,
944                    Some(rbf) => rbf.fees.amount().to_sat() as i64 * -1000,
945                },
946            )
947            .await;
948        audit
949            .add_items(
950                dbtx,
951                module_instance_id,
952                &PendingTransactionPrefixKey,
953                |_, v| match v.rbf {
954                    None => v.change.to_sat() as i64 * 1000,
955                    Some(rbf) => rbf.fees.amount().to_sat() as i64 * -1000,
956                },
957            )
958            .await;
959    }
960
961    fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
962        vec![
963            api_endpoint! {
964                BLOCK_COUNT_ENDPOINT,
965                ApiVersion::new(0, 0),
966                async |module: &Wallet, context, _params: ()| -> u32 {
967                    let db = context.db();
968                    let mut dbtx = db.begin_transaction_nc().await;
969                    Ok(module.consensus_block_count(&mut dbtx).await)
970                }
971            },
972            api_endpoint! {
973                BLOCK_COUNT_LOCAL_ENDPOINT,
974                ApiVersion::new(0, 0),
975                async |module: &Wallet, _context, _params: ()| -> Option<u32> {
976                    Ok(module.get_block_count().ok())
977                }
978            },
979            api_endpoint! {
980                PEG_OUT_FEES_ENDPOINT,
981                ApiVersion::new(0, 0),
982                async |module: &Wallet, context, params: (Address<NetworkUnchecked>, u64)| -> Option<PegOutFees> {
983                    let (address, sats) = params;
984                    let db = context.db();
985                    let mut dbtx = db.begin_transaction_nc().await;
986                    let feerate = module.consensus_fee_rate(&mut dbtx).await;
987
988                    // Since we are only calculating the tx size we can use an arbitrary dummy nonce.
989                    let dummy_tweak = [0; 33];
990
991                    let tx = module.offline_wallet().create_tx(
992                        bitcoin::Amount::from_sat(sats),
993                        // Note: While calling `assume_checked()` is generally unwise, it's fine
994                        // here since we're only returning a fee estimate, and we would still
995                        // reject a transaction with the wrong network upon attempted peg-out.
996                        address.assume_checked().script_pubkey(),
997                        vec![],
998                        module.available_utxos(&mut dbtx).await,
999                        feerate,
1000                        &dummy_tweak,
1001                        None
1002                    );
1003
1004                    match tx {
1005                        Err(error) => {
1006                            // Usually from not enough spendable UTXOs
1007                            warn!(target: LOG_MODULE_WALLET, "Error returning peg-out fees {error}");
1008                            Ok(None)
1009                        }
1010                        Ok(tx) => Ok(Some(tx.fees))
1011                    }
1012                }
1013            },
1014            api_endpoint! {
1015                BITCOIN_KIND_ENDPOINT,
1016                ApiVersion::new(0, 1),
1017                async |module: &Wallet, _context, _params: ()| -> String {
1018                    Ok(module.btc_rpc.get_bitcoin_rpc_config().kind)
1019                }
1020            },
1021            api_endpoint! {
1022                BITCOIN_RPC_CONFIG_ENDPOINT,
1023                ApiVersion::new(0, 1),
1024                async |module: &Wallet, context, _params: ()| -> BitcoinRpcConfig {
1025                    check_auth(context)?;
1026                    let config = module.btc_rpc.get_bitcoin_rpc_config();
1027
1028                    // we need to remove auth, otherwise we'll send over the wire
1029                    let without_auth = config.url.clone().without_auth().map_err(|()| {
1030                        ApiError::server_error("Unable to remove auth from bitcoin config URL".to_string())
1031                    })?;
1032
1033                    Ok(BitcoinRpcConfig {
1034                        url: without_auth,
1035                        ..config
1036                    })
1037                }
1038            },
1039            api_endpoint! {
1040                WALLET_SUMMARY_ENDPOINT,
1041                ApiVersion::new(0, 1),
1042                async |module: &Wallet, context, _params: ()| -> WalletSummary {
1043                    let db = context.db();
1044                    let mut dbtx = db.begin_transaction_nc().await;
1045                    Ok(module.get_wallet_summary(&mut dbtx).await)
1046                }
1047            },
1048            api_endpoint! {
1049                MODULE_CONSENSUS_VERSION_ENDPOINT,
1050                ApiVersion::new(0, 2),
1051                async |module: &Wallet, context, _params: ()| -> ModuleConsensusVersion {
1052                    let db = context.db();
1053                    let mut dbtx = db.begin_transaction_nc().await;
1054                    Ok(module.consensus_module_consensus_version(&mut dbtx).await)
1055                }
1056            },
1057            api_endpoint! {
1058                SUPPORTED_MODULE_CONSENSUS_VERSION_ENDPOINT,
1059                ApiVersion::new(0, 2),
1060                async |_module: &Wallet, _context, _params: ()| -> ModuleConsensusVersion {
1061                    Ok(MODULE_CONSENSUS_VERSION)
1062                }
1063            },
1064            api_endpoint! {
1065                ACTIVATE_CONSENSUS_VERSION_VOTING_ENDPOINT,
1066                ApiVersion::new(0, 2),
1067                async |_module: &Wallet, context, _params: ()| -> () {
1068                    check_auth(context)?;
1069
1070                    let db = context.db();
1071                    let mut dbtx = db.begin_transaction().await;
1072                    dbtx.to_ref().insert_entry(&ConsensusVersionVotingActivationKey, &()).await;
1073                    dbtx.commit_tx_result().await?;
1074                    Ok(())
1075                }
1076            },
1077            api_endpoint! {
1078                UTXO_CONFIRMED_ENDPOINT,
1079                ApiVersion::new(0, 2),
1080                async |module: &Wallet, context, outpoint: bitcoin::OutPoint| -> bool {
1081                    let db = context.db();
1082                    let mut dbtx = db.begin_transaction_nc().await;
1083                    Ok(module.is_utxo_confirmed(&mut dbtx, outpoint).await)
1084                }
1085            },
1086            api_endpoint! {
1087                RECOVERY_COUNT_ENDPOINT,
1088                ApiVersion::new(0, 1),
1089                async |_module: &Wallet, context, _params: ()| -> u64 {
1090                    let db = context.db();
1091                    let mut dbtx = db.begin_transaction_nc().await;
1092                    Ok(get_recovery_count(&mut dbtx).await)
1093                }
1094            },
1095            api_endpoint! {
1096                RECOVERY_SLICE_ENDPOINT,
1097                ApiVersion::new(0, 1),
1098                async |_module: &Wallet, context, range: (u64, u64)| -> Vec<RecoveryItem> {
1099                    let db = context.db();
1100                    let mut dbtx = db.begin_transaction_nc().await;
1101                    Ok(get_recovery_slice(&mut dbtx, range).await)
1102                }
1103            },
1104        ]
1105    }
1106}
1107
1108async fn get_recovery_count(dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1109    dbtx.find_by_prefix_sorted_descending(&RecoveryItemKeyPrefix)
1110        .await
1111        .next()
1112        .await
1113        .map_or(0, |entry| entry.0.0 + 1)
1114}
1115
1116async fn get_recovery_slice(
1117    dbtx: &mut DatabaseTransaction<'_>,
1118    range: (u64, u64),
1119) -> Vec<RecoveryItem> {
1120    dbtx.find_by_range(RecoveryItemKey(range.0)..RecoveryItemKey(range.1))
1121        .await
1122        .map(|entry| entry.1)
1123        .collect()
1124        .await
1125}
1126
1127fn calculate_pegin_metrics(
1128    dbtx: &mut DatabaseTransaction<'_>,
1129    amount: fedimint_core::Amount,
1130    fee: fedimint_core::Amount,
1131) {
1132    dbtx.on_commit(move || {
1133        WALLET_INOUT_SATS
1134            .with_label_values(&["incoming"])
1135            .observe(amount.sats_f64());
1136        WALLET_INOUT_FEES_SATS
1137            .with_label_values(&["incoming"])
1138            .observe(fee.sats_f64());
1139        WALLET_PEGIN_SATS.observe(amount.sats_f64());
1140        WALLET_PEGIN_FEES_SATS.observe(fee.sats_f64());
1141    });
1142}
1143
1144fn calculate_pegout_metrics(
1145    dbtx: &mut DatabaseTransaction<'_>,
1146    amount: fedimint_core::Amount,
1147    fee: fedimint_core::Amount,
1148) {
1149    dbtx.on_commit(move || {
1150        WALLET_INOUT_SATS
1151            .with_label_values(&["outgoing"])
1152            .observe(amount.sats_f64());
1153        WALLET_INOUT_FEES_SATS
1154            .with_label_values(&["outgoing"])
1155            .observe(fee.sats_f64());
1156        WALLET_PEGOUT_SATS.observe(amount.sats_f64());
1157        WALLET_PEGOUT_FEES_SATS.observe(fee.sats_f64());
1158    });
1159}
1160
1161#[derive(Debug)]
1162pub struct Wallet {
1163    cfg: WalletConfig,
1164    db: Database,
1165    secp: Secp256k1<All>,
1166    btc_rpc: ServerBitcoinRpcMonitor,
1167    our_peer_id: PeerId,
1168    /// Broadcasting pending txes can be triggered immediately with this
1169    broadcast_pending: Arc<Notify>,
1170    task_group: TaskGroup,
1171    /// Maximum consensus version supported by *all* our peers. Used to
1172    /// automatically activate new consensus versions as soon as everyone
1173    /// upgrades.
1174    peer_supported_consensus_version: watch::Receiver<Option<ModuleConsensusVersion>>,
1175}
1176
1177impl Wallet {
1178    pub async fn new(
1179        cfg: WalletConfig,
1180        db: &Database,
1181        task_group: &TaskGroup,
1182        our_peer_id: PeerId,
1183        module_api: DynModuleApi,
1184        server_bitcoin_rpc_monitor: ServerBitcoinRpcMonitor,
1185    ) -> anyhow::Result<Wallet> {
1186        let broadcast_pending = Arc::new(Notify::new());
1187        Self::spawn_broadcast_pending_task(
1188            task_group,
1189            &server_bitcoin_rpc_monitor,
1190            db,
1191            broadcast_pending.clone(),
1192        );
1193
1194        let peer_supported_consensus_version =
1195            Self::spawn_peer_supported_consensus_version_task(module_api, task_group, our_peer_id);
1196
1197        let status = retry("verify network", backoff_util::aggressive_backoff(), || {
1198            std::future::ready(
1199                server_bitcoin_rpc_monitor
1200                    .status()
1201                    .context("No connection to bitcoin rpc"),
1202            )
1203        })
1204        .await?;
1205
1206        ensure!(status.network == cfg.consensus.network.0, "Wrong Network");
1207
1208        let wallet = Wallet {
1209            cfg,
1210            db: db.clone(),
1211            secp: Default::default(),
1212            btc_rpc: server_bitcoin_rpc_monitor,
1213            our_peer_id,
1214            task_group: task_group.clone(),
1215            peer_supported_consensus_version,
1216            broadcast_pending,
1217        };
1218
1219        Ok(wallet)
1220    }
1221
1222    /// Try to attach signatures to a pending peg-out tx.
1223    fn sign_peg_out_psbt(
1224        &self,
1225        psbt: &mut Psbt,
1226        peer: PeerId,
1227        signature: &PegOutSignatureItem,
1228    ) -> Result<(), ProcessPegOutSigError> {
1229        let peer_key = self
1230            .cfg
1231            .consensus
1232            .peer_peg_in_keys
1233            .get(&peer)
1234            .expect("always called with valid peer id");
1235
1236        if psbt.inputs.len() != signature.signature.len() {
1237            return Err(ProcessPegOutSigError::WrongSignatureCount(
1238                psbt.inputs.len(),
1239                signature.signature.len(),
1240            ));
1241        }
1242
1243        let mut tx_hasher = SighashCache::new(&psbt.unsigned_tx);
1244        for (idx, (input, signature)) in psbt
1245            .inputs
1246            .iter_mut()
1247            .zip(signature.signature.iter())
1248            .enumerate()
1249        {
1250            let tx_hash = tx_hasher
1251                .p2wsh_signature_hash(
1252                    idx,
1253                    input
1254                        .witness_script
1255                        .as_ref()
1256                        .expect("Missing witness script"),
1257                    input.witness_utxo.as_ref().expect("Missing UTXO").value,
1258                    EcdsaSighashType::All,
1259                )
1260                .map_err(|_| ProcessPegOutSigError::SighashError)?;
1261
1262            let tweak = input
1263                .proprietary
1264                .get(&proprietary_tweak_key())
1265                .expect("we saved it with a tweak");
1266
1267            let tweaked_peer_key = peer_key.tweak(tweak, &self.secp);
1268            self.secp
1269                .verify_ecdsa(
1270                    &Message::from_digest_slice(&tx_hash[..]).unwrap(),
1271                    signature,
1272                    &tweaked_peer_key.key,
1273                )
1274                .map_err(|_| ProcessPegOutSigError::InvalidSignature)?;
1275
1276            if input
1277                .partial_sigs
1278                .insert(tweaked_peer_key.into(), EcdsaSig::sighash_all(*signature))
1279                .is_some()
1280            {
1281                // Should never happen since peers only sign a PSBT once
1282                return Err(ProcessPegOutSigError::DuplicateSignature);
1283            }
1284        }
1285        Ok(())
1286    }
1287
1288    fn finalize_peg_out_psbt(
1289        &self,
1290        mut unsigned: UnsignedTransaction,
1291    ) -> Result<PendingTransaction, ProcessPegOutSigError> {
1292        // We need to save the change output's tweak key to be able to access the funds
1293        // later on. The tweak is extracted here because the psbt is moved next
1294        // and not available anymore when the tweak is actually needed in the
1295        // end to be put into the batch on success.
1296        let change_tweak: [u8; 33] = unsigned
1297            .psbt
1298            .outputs
1299            .iter()
1300            .find_map(|output| output.proprietary.get(&proprietary_tweak_key()).cloned())
1301            .ok_or(ProcessPegOutSigError::MissingOrMalformedChangeTweak)?
1302            .try_into()
1303            .map_err(|_| ProcessPegOutSigError::MissingOrMalformedChangeTweak)?;
1304
1305        if let Err(error) = unsigned.psbt.finalize_mut(&self.secp) {
1306            return Err(ProcessPegOutSigError::ErrorFinalizingPsbt(error));
1307        }
1308
1309        let tx = unsigned.psbt.clone().extract_tx_unchecked_fee_rate();
1310
1311        Ok(PendingTransaction {
1312            tx,
1313            tweak: change_tweak,
1314            change: unsigned.change,
1315            destination: unsigned.destination,
1316            fees: unsigned.fees,
1317            selected_utxos: unsigned.selected_utxos,
1318            peg_out_amount: unsigned.peg_out_amount,
1319            rbf: unsigned.rbf,
1320        })
1321    }
1322
1323    fn get_block_count(&self) -> anyhow::Result<u32> {
1324        self.btc_rpc
1325            .status()
1326            .context("No bitcoin rpc connection")
1327            .and_then(|status| {
1328                status
1329                    .block_count
1330                    .try_into()
1331                    .map_err(|_| format_err!("Block count exceeds u32 limits"))
1332            })
1333    }
1334
1335    pub fn get_fee_rate_opt(&self) -> Feerate {
1336        // `get_feerate_multiplier` is clamped and can't be negative
1337        // feerate sources as clamped and can't be negative or too large
1338        #[allow(clippy::cast_precision_loss)]
1339        #[allow(clippy::cast_sign_loss)]
1340        Feerate {
1341            sats_per_kvb: ((self
1342                .btc_rpc
1343                .status()
1344                .map_or(self.cfg.consensus.default_fee, |status| status.fee_rate)
1345                .sats_per_kvb as f64
1346                * get_feerate_multiplier())
1347            .round()) as u64,
1348        }
1349    }
1350
1351    pub async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u32 {
1352        let peer_count = self.cfg.consensus.peer_peg_in_keys.to_num_peers().total();
1353
1354        let mut counts = dbtx
1355            .find_by_prefix(&BlockCountVotePrefix)
1356            .await
1357            .map(|entry| entry.1)
1358            .collect::<Vec<u32>>()
1359            .await;
1360
1361        assert!(counts.len() <= peer_count);
1362
1363        while counts.len() < peer_count {
1364            counts.push(0);
1365        }
1366
1367        counts.sort_unstable();
1368
1369        counts[peer_count / 2]
1370    }
1371
1372    pub async fn consensus_fee_rate(&self, dbtx: &mut DatabaseTransaction<'_>) -> Feerate {
1373        let peer_count = self.cfg.consensus.peer_peg_in_keys.to_num_peers().total();
1374
1375        let mut rates = dbtx
1376            .find_by_prefix(&FeeRateVotePrefix)
1377            .await
1378            .map(|(.., rate)| rate)
1379            .collect::<Vec<_>>()
1380            .await;
1381
1382        assert!(rates.len() <= peer_count);
1383
1384        while rates.len() < peer_count {
1385            rates.push(self.cfg.consensus.default_fee);
1386        }
1387
1388        rates.sort_unstable();
1389
1390        rates[peer_count / 2]
1391    }
1392
1393    async fn consensus_module_consensus_version(
1394        &self,
1395        dbtx: &mut DatabaseTransaction<'_>,
1396    ) -> ModuleConsensusVersion {
1397        let num_peers = self.cfg.consensus.peer_peg_in_keys.to_num_peers();
1398
1399        let mut versions = dbtx
1400            .find_by_prefix(&ConsensusVersionVotePrefix)
1401            .await
1402            .map(|entry| entry.1)
1403            .collect::<Vec<ModuleConsensusVersion>>()
1404            .await;
1405
1406        while versions.len() < num_peers.total() {
1407            versions.push(ModuleConsensusVersion::new(2, 0));
1408        }
1409
1410        assert_eq!(versions.len(), num_peers.total());
1411
1412        versions.sort_unstable();
1413
1414        assert!(versions.first() <= versions.last());
1415
1416        versions[num_peers.max_evil()]
1417    }
1418
1419    pub async fn consensus_nonce(&self, dbtx: &mut DatabaseTransaction<'_>) -> [u8; 33] {
1420        let nonce_idx = dbtx.get_value(&PegOutNonceKey).await.unwrap_or(0);
1421        dbtx.insert_entry(&PegOutNonceKey, &(nonce_idx + 1)).await;
1422
1423        nonce_from_idx(nonce_idx)
1424    }
1425
1426    async fn sync_up_to_consensus_count(
1427        &self,
1428        dbtx: &mut DatabaseTransaction<'_>,
1429        old_count: u32,
1430        new_count: u32,
1431    ) {
1432        let sync_start = fedimint_core::time::now();
1433        info!(
1434            target: LOG_MODULE_WALLET,
1435            old_count,
1436            new_count,
1437            blocks_to_go = new_count
1438                .checked_sub(old_count)
1439                .expect("new_count must be >= old_count"),
1440            "New block count consensus, initiating sync",
1441        );
1442
1443        // Before we can safely call our bitcoin backend to process the new consensus
1444        // count, we need to ensure we observed enough confirmations
1445        self.wait_for_finality_confs_or_shutdown(new_count).await;
1446
1447        for height in old_count..new_count {
1448            let block_start = fedimint_core::time::now();
1449            info!(
1450                target: LOG_MODULE_WALLET,
1451                height,
1452                "Processing block of height {height}",
1453            );
1454
1455            // TODO: use batching for mainnet syncing
1456            trace!(block = height, "Fetching block hash");
1457            let block_hash = retry("get_block_hash", backoff_util::background_backoff(), || {
1458                self.btc_rpc.get_block_hash(u64::from(height)) // TODO: use u64 for height everywhere
1459            })
1460            .await
1461            .expect("bitcoind rpc to get block hash");
1462
1463            let block = retry("get_block", backoff_util::background_backoff(), || {
1464                self.btc_rpc.get_block(&block_hash)
1465            })
1466            .await
1467            .expect("bitcoind rpc to get block");
1468
1469            if let Some(prev_block_height) = height.checked_sub(1) {
1470                if let Some(hash) = dbtx
1471                    .get_value(&BlockHashByHeightKey(prev_block_height))
1472                    .await
1473                {
1474                    assert_eq!(block.header.prev_blockhash, hash.0);
1475                } else {
1476                    warn!(
1477                        target: LOG_MODULE_WALLET,
1478                        %height,
1479                        %block_hash,
1480                        %prev_block_height,
1481                        prev_blockhash = %block.header.prev_blockhash,
1482                        "Missing previous block hash. This should only happen on the first processed block height."
1483                    );
1484                }
1485            }
1486
1487            if self.consensus_module_consensus_version(dbtx).await
1488                >= ModuleConsensusVersion::new(2, 2)
1489            {
1490                for transaction in &block.txdata {
1491                    // We maintain the subset of unspent P2WSH transaction outputs created
1492                    // since the module was running on the new consensus version, which might be
1493                    // the same time as the genesis session.
1494
1495                    for tx_in in &transaction.input {
1496                        dbtx.remove_entry(&UnspentTxOutKey(tx_in.previous_output))
1497                            .await;
1498                    }
1499
1500                    for (vout, tx_out) in transaction.output.iter().enumerate() {
1501                        let should_track_utxo = if self.cfg.consensus.peer_peg_in_keys.len() > 1 {
1502                            tx_out.script_pubkey.is_p2wsh()
1503                        } else {
1504                            tx_out.script_pubkey.is_p2wpkh()
1505                        };
1506
1507                        if should_track_utxo {
1508                            let outpoint = bitcoin::OutPoint {
1509                                txid: transaction.compute_txid(),
1510                                vout: vout as u32,
1511                            };
1512
1513                            dbtx.insert_new_entry(&UnspentTxOutKey(outpoint), tx_out)
1514                                .await;
1515                        }
1516                    }
1517                }
1518            }
1519
1520            let pending_transactions = dbtx
1521                .find_by_prefix(&PendingTransactionPrefixKey)
1522                .await
1523                .map(|(key, transaction)| (key.0, transaction))
1524                .collect::<BTreeMap<Txid, PendingTransaction>>()
1525                .await;
1526            let pending_transactions_len = pending_transactions.len();
1527
1528            debug!(
1529                target: LOG_MODULE_WALLET,
1530                ?height,
1531                ?pending_transactions_len,
1532                "Recognizing change UTXOs"
1533            );
1534            for (txid, tx) in &pending_transactions {
1535                let is_tx_in_block = block.txdata.iter().any(|tx| tx.compute_txid() == *txid);
1536
1537                if is_tx_in_block {
1538                    debug!(
1539                        target: LOG_MODULE_WALLET,
1540                        ?txid, ?height, ?block_hash, "Recognizing change UTXO"
1541                    );
1542                    self.recognize_change_utxo(dbtx, tx).await;
1543                } else {
1544                    debug!(
1545                        target: LOG_MODULE_WALLET,
1546                        ?txid,
1547                        ?height,
1548                        ?block_hash,
1549                        "Pending transaction not yet confirmed in this block"
1550                    );
1551                }
1552            }
1553
1554            dbtx.insert_new_entry(&BlockHashKey(block_hash), &()).await;
1555            dbtx.insert_new_entry(
1556                &BlockHashByHeightKey(height),
1557                &BlockHashByHeightValue(block_hash),
1558            )
1559            .await;
1560
1561            info!(
1562                target: LOG_MODULE_WALLET,
1563                height,
1564                ?block_hash,
1565                duration = ?block_start.elapsed().unwrap_or_default(),
1566                "Successfully processed block of height {height}",
1567            );
1568        }
1569
1570        info!(
1571            target: LOG_MODULE_WALLET,
1572            old_count,
1573            new_count,
1574            blocks_processed = new_count
1575                .checked_sub(old_count)
1576                .expect("new_count must be >= old_count"),
1577            duration = ?sync_start.elapsed().unwrap_or_default(),
1578            "Block count consensus sync complete",
1579        );
1580    }
1581
1582    /// Add a change UTXO to our spendable UTXO database after it was included
1583    /// in a block that we got consensus on.
1584    async fn recognize_change_utxo(
1585        &self,
1586        dbtx: &mut DatabaseTransaction<'_>,
1587        pending_tx: &PendingTransaction,
1588    ) {
1589        self.remove_rbf_transactions(dbtx, pending_tx).await;
1590
1591        let script_pk = self
1592            .cfg
1593            .consensus
1594            .peg_in_descriptor
1595            .tweak(&pending_tx.tweak, &self.secp)
1596            .script_pubkey();
1597        for (idx, output) in pending_tx.tx.output.iter().enumerate() {
1598            if output.script_pubkey == script_pk {
1599                dbtx.insert_entry(
1600                    &UTXOKey(bitcoin::OutPoint {
1601                        txid: pending_tx.tx.compute_txid(),
1602                        vout: idx as u32,
1603                    }),
1604                    &SpendableUTXO {
1605                        tweak: pending_tx.tweak,
1606                        amount: output.value,
1607                    },
1608                )
1609                .await;
1610            }
1611        }
1612    }
1613
1614    /// Removes the `PendingTransaction` and any transactions tied to it via RBF
1615    async fn remove_rbf_transactions(
1616        &self,
1617        dbtx: &mut DatabaseTransaction<'_>,
1618        pending_tx: &PendingTransaction,
1619    ) {
1620        let mut all_transactions: BTreeMap<Txid, PendingTransaction> = dbtx
1621            .find_by_prefix(&PendingTransactionPrefixKey)
1622            .await
1623            .map(|(key, val)| (key.0, val))
1624            .collect::<BTreeMap<Txid, PendingTransaction>>()
1625            .await;
1626
1627        // We need to search and remove all `PendingTransactions` invalidated by RBF
1628        let mut pending_to_remove = vec![pending_tx.clone()];
1629        while let Some(removed) = pending_to_remove.pop() {
1630            all_transactions.remove(&removed.tx.compute_txid());
1631            dbtx.remove_entry(&PendingTransactionKey(removed.tx.compute_txid()))
1632                .await;
1633
1634            // Search for tx that this `removed` has as RBF
1635            if let Some(rbf) = &removed.rbf
1636                && let Some(tx) = all_transactions.get(&rbf.txid)
1637            {
1638                pending_to_remove.push(tx.clone());
1639            }
1640
1641            // Search for tx that wanted to RBF the `removed` one
1642            for tx in all_transactions.values() {
1643                if let Some(rbf) = &tx.rbf
1644                    && rbf.txid == removed.tx.compute_txid()
1645                {
1646                    pending_to_remove.push(tx.clone());
1647                }
1648            }
1649        }
1650    }
1651
1652    async fn block_is_known(
1653        &self,
1654        dbtx: &mut DatabaseTransaction<'_>,
1655        block_hash: BlockHash,
1656    ) -> bool {
1657        dbtx.get_value(&BlockHashKey(block_hash)).await.is_some()
1658    }
1659
1660    async fn create_peg_out_tx(
1661        &self,
1662        dbtx: &mut DatabaseTransaction<'_>,
1663        output: &WalletOutputV0,
1664        change_tweak: &[u8; 33],
1665    ) -> Result<UnsignedTransaction, WalletOutputError> {
1666        match output {
1667            WalletOutputV0::PegOut(peg_out) => self.offline_wallet().create_tx(
1668                peg_out.amount,
1669                // Note: While calling `assume_checked()` is generally unwise, checking the
1670                // network here could be a consensus-breaking change. Ignoring the network
1671                // is fine here since we validate it in `process_output()`.
1672                peg_out.recipient.clone().assume_checked().script_pubkey(),
1673                vec![],
1674                self.available_utxos(dbtx).await,
1675                peg_out.fees.fee_rate,
1676                change_tweak,
1677                None,
1678            ),
1679            WalletOutputV0::Rbf(rbf) => {
1680                let tx = dbtx
1681                    .get_value(&PendingTransactionKey(rbf.txid))
1682                    .await
1683                    .ok_or(WalletOutputError::RbfTransactionIdNotFound)?;
1684
1685                self.offline_wallet().create_tx(
1686                    tx.peg_out_amount,
1687                    tx.destination,
1688                    tx.selected_utxos,
1689                    self.available_utxos(dbtx).await,
1690                    tx.fees.fee_rate,
1691                    change_tweak,
1692                    Some(rbf.clone()),
1693                )
1694            }
1695        }
1696    }
1697
1698    async fn available_utxos(
1699        &self,
1700        dbtx: &mut DatabaseTransaction<'_>,
1701    ) -> Vec<(UTXOKey, SpendableUTXO)> {
1702        dbtx.find_by_prefix(&UTXOPrefixKey)
1703            .await
1704            .collect::<Vec<(UTXOKey, SpendableUTXO)>>()
1705            .await
1706    }
1707
1708    pub async fn get_wallet_value(&self, dbtx: &mut DatabaseTransaction<'_>) -> bitcoin::Amount {
1709        let sat_sum = self
1710            .available_utxos(dbtx)
1711            .await
1712            .into_iter()
1713            .map(|(_, utxo)| utxo.amount.to_sat())
1714            .sum();
1715        bitcoin::Amount::from_sat(sat_sum)
1716    }
1717
1718    async fn get_wallet_summary(&self, dbtx: &mut DatabaseTransaction<'_>) -> WalletSummary {
1719        fn partition_peg_out_and_change(
1720            transactions: Vec<Transaction>,
1721        ) -> (Vec<TxOutputSummary>, Vec<TxOutputSummary>) {
1722            let mut peg_out_txos: Vec<TxOutputSummary> = Vec::new();
1723            let mut change_utxos: Vec<TxOutputSummary> = Vec::new();
1724
1725            for tx in transactions {
1726                let txid = tx.compute_txid();
1727
1728                // to identify outputs for the peg_out (idx = 0) and change (idx = 1), we lean
1729                // on how the wallet constructs the transaction
1730                let peg_out_output = tx
1731                    .output
1732                    .first()
1733                    .expect("tx must contain withdrawal output");
1734
1735                let change_output = tx.output.last().expect("tx must contain change output");
1736
1737                peg_out_txos.push(TxOutputSummary {
1738                    outpoint: bitcoin::OutPoint { txid, vout: 0 },
1739                    amount: peg_out_output.value,
1740                });
1741
1742                change_utxos.push(TxOutputSummary {
1743                    outpoint: bitcoin::OutPoint { txid, vout: 1 },
1744                    amount: change_output.value,
1745                });
1746            }
1747
1748            (peg_out_txos, change_utxos)
1749        }
1750
1751        let spendable_utxos = self
1752            .available_utxos(dbtx)
1753            .await
1754            .iter()
1755            .map(|(utxo_key, spendable_utxo)| TxOutputSummary {
1756                outpoint: utxo_key.0,
1757                amount: spendable_utxo.amount,
1758            })
1759            .collect::<Vec<_>>();
1760
1761        // constructed peg-outs without threshold signatures
1762        let unsigned_transactions = dbtx
1763            .find_by_prefix(&UnsignedTransactionPrefixKey)
1764            .await
1765            .map(|(_tx_key, tx)| tx.psbt.unsigned_tx)
1766            .collect::<Vec<_>>()
1767            .await;
1768
1769        // peg-outs with threshold signatures, awaiting finality delay confirmations
1770        let unconfirmed_transactions = dbtx
1771            .find_by_prefix(&PendingTransactionPrefixKey)
1772            .await
1773            .map(|(_tx_key, tx)| tx.tx)
1774            .collect::<Vec<_>>()
1775            .await;
1776
1777        let (unsigned_peg_out_txos, unsigned_change_utxos) =
1778            partition_peg_out_and_change(unsigned_transactions);
1779
1780        let (unconfirmed_peg_out_txos, unconfirmed_change_utxos) =
1781            partition_peg_out_and_change(unconfirmed_transactions);
1782
1783        WalletSummary {
1784            spendable_utxos,
1785            unsigned_peg_out_txos,
1786            unsigned_change_utxos,
1787            unconfirmed_peg_out_txos,
1788            unconfirmed_change_utxos,
1789        }
1790    }
1791
1792    async fn is_utxo_confirmed(
1793        &self,
1794        dbtx: &mut DatabaseTransaction<'_>,
1795        outpoint: bitcoin::OutPoint,
1796    ) -> bool {
1797        dbtx.get_value(&UnspentTxOutKey(outpoint)).await.is_some()
1798    }
1799
1800    fn offline_wallet(&'_ self) -> StatelessWallet<'_> {
1801        StatelessWallet {
1802            descriptor: &self.cfg.consensus.peg_in_descriptor,
1803            secret_key: &self.cfg.private.peg_in_key,
1804            secp: &self.secp,
1805        }
1806    }
1807
1808    fn spawn_broadcast_pending_task(
1809        task_group: &TaskGroup,
1810        server_bitcoin_rpc_monitor: &ServerBitcoinRpcMonitor,
1811        db: &Database,
1812        broadcast_pending_notify: Arc<Notify>,
1813    ) {
1814        task_group.spawn_cancellable("broadcast pending", {
1815            let btc_rpc = server_bitcoin_rpc_monitor.clone();
1816            let db = db.clone();
1817            run_broadcast_pending_tx(db, btc_rpc, broadcast_pending_notify)
1818        });
1819    }
1820
1821    /// Get the bitcoin network for UI display
1822    pub fn network_ui(&self) -> Network {
1823        self.cfg.consensus.network.0
1824    }
1825
1826    /// Get the current consensus block count for UI display
1827    pub async fn consensus_block_count_ui(&self) -> u32 {
1828        self.consensus_block_count(&mut self.db.begin_transaction_nc().await)
1829            .await
1830    }
1831
1832    /// Get the current consensus fee rate for UI display
1833    pub async fn consensus_feerate_ui(&self) -> Feerate {
1834        self.consensus_fee_rate(&mut self.db.begin_transaction_nc().await)
1835            .await
1836    }
1837
1838    /// Get the current wallet summary for UI display
1839    pub async fn get_wallet_summary_ui(&self) -> WalletSummary {
1840        self.get_wallet_summary(&mut self.db.begin_transaction_nc().await)
1841            .await
1842    }
1843
1844    /// Shutdown the task group shared throughout fedimintd, giving 60 seconds
1845    /// for other services to gracefully shutdown.
1846    async fn graceful_shutdown(&self) {
1847        if let Err(e) = self
1848            .task_group
1849            .clone()
1850            .shutdown_join_all(Some(Duration::from_mins(1)))
1851            .await
1852        {
1853            panic!("Error while shutting down fedimintd task group: {e}");
1854        }
1855    }
1856
1857    /// Returns once our bitcoin backend observes finality delay confirmations
1858    /// of the consensus block count. If we don't observe enough confirmations
1859    /// after one hour, we gracefully shutdown fedimintd. This is necessary
1860    /// since we can no longer participate in consensus if our bitcoin backend
1861    /// is unable to observe the same chain tip as our peers.
1862    async fn wait_for_finality_confs_or_shutdown(&self, consensus_block_count: u32) {
1863        let backoff = if is_running_in_test_env() {
1864            // every 100ms for 60s
1865            backoff_util::custom_backoff(
1866                Duration::from_millis(100),
1867                Duration::from_millis(100),
1868                Some(10 * 60),
1869            )
1870        } else {
1871            // every max 10s for 1 hour
1872            backoff_util::fibonacci_max_one_hour()
1873        };
1874
1875        let wait_for_finality_confs = || async {
1876            let our_chain_tip_block_count = self.get_block_count()?;
1877            let consensus_chain_tip_block_count =
1878                consensus_block_count + self.cfg.consensus.finality_delay;
1879
1880            if consensus_chain_tip_block_count <= our_chain_tip_block_count {
1881                Ok(())
1882            } else {
1883                Err(anyhow::anyhow!("not enough confirmations"))
1884            }
1885        };
1886
1887        if retry("wait_for_finality_confs", backoff, wait_for_finality_confs)
1888            .await
1889            .is_err()
1890        {
1891            self.graceful_shutdown().await;
1892        }
1893    }
1894
1895    fn spawn_peer_supported_consensus_version_task(
1896        api_client: DynModuleApi,
1897        task_group: &TaskGroup,
1898        our_peer_id: PeerId,
1899    ) -> watch::Receiver<Option<ModuleConsensusVersion>> {
1900        let (sender, receiver) = watch::channel(None);
1901        task_group.spawn_cancellable("fetch-peer-consensus-versions", async move {
1902            loop {
1903                let request_futures = api_client.all_peers().iter().filter_map(|&peer| {
1904                    if peer == our_peer_id {
1905                        return None;
1906                    }
1907
1908                    let api_client_inner = api_client.clone();
1909                    Some(async move {
1910                        api_client_inner
1911                            .request_single_peer::<ModuleConsensusVersion>(
1912                                SUPPORTED_MODULE_CONSENSUS_VERSION_ENDPOINT.to_owned(),
1913                                ApiRequestErased::default(),
1914                                peer,
1915                            )
1916                            .await
1917                            .inspect(|res| debug!(
1918                                target: LOG_MODULE_WALLET,
1919                                %peer,
1920                                %our_peer_id,
1921                                ?res,
1922                                "Fetched supported module consensus version from peer"
1923                            ))
1924                            .inspect_err(|err| warn!(
1925                                target: LOG_MODULE_WALLET,
1926                                 %peer,
1927                                 err=%err.fmt_compact(),
1928                                "Failed to fetch consensus version from peer"
1929                            ))
1930                            .ok()
1931                    })
1932                });
1933
1934                let peer_consensus_versions = join_all(request_futures)
1935                    .await
1936                    .into_iter()
1937                    .flatten()
1938                    .collect::<Vec<_>>();
1939
1940                let sorted_consensus_versions = peer_consensus_versions
1941                    .into_iter()
1942                    .chain(std::iter::once(MODULE_CONSENSUS_VERSION))
1943                    .sorted()
1944                    .collect::<Vec<_>>();
1945                let all_peers_supported_version =
1946                    if sorted_consensus_versions.len() == api_client.all_peers().len() {
1947                        let min_supported_version = *sorted_consensus_versions
1948                            .first()
1949                            .expect("at least one element");
1950
1951                        debug!(
1952                            target: LOG_MODULE_WALLET,
1953                            ?sorted_consensus_versions,
1954                            "Fetched supported consensus versions from peers"
1955                        );
1956
1957                        Some(min_supported_version)
1958                    } else {
1959                        assert!(
1960                            sorted_consensus_versions.len() <= api_client.all_peers().len(),
1961                            "Too many peer responses",
1962                        );
1963                        trace!(
1964                            target: LOG_MODULE_WALLET,
1965                            ?sorted_consensus_versions,
1966                            "Not all peers have reported their consensus version yet"
1967                        );
1968                        None
1969                    };
1970
1971                #[allow(clippy::disallowed_methods)]
1972                if sender.send(all_peers_supported_version).is_err() {
1973                    warn!(target: LOG_MODULE_WALLET, "Failed to send consensus version to watch channel, stopping task");
1974                    break;
1975                }
1976
1977                if is_running_in_test_env() {
1978                    // Even in tests we don't want to spam the federation with requests about it
1979                    sleep(Duration::from_secs(5)).await;
1980                } else {
1981                    sleep(Duration::from_mins(10)).await;
1982                }
1983            }
1984        });
1985        receiver
1986    }
1987}
1988
1989#[instrument(target = LOG_MODULE_WALLET, level = "debug", skip_all)]
1990pub async fn run_broadcast_pending_tx(
1991    db: Database,
1992    rpc: ServerBitcoinRpcMonitor,
1993    broadcast: Arc<Notify>,
1994) {
1995    loop {
1996        // Unless something new happened, we broadcast once a minute
1997        let _ = tokio::time::timeout(Duration::from_mins(1), broadcast.notified()).await;
1998        broadcast_pending_tx(db.begin_transaction_nc().await, &rpc).await;
1999    }
2000}
2001
2002pub async fn broadcast_pending_tx(
2003    mut dbtx: DatabaseTransaction<'_>,
2004    rpc: &ServerBitcoinRpcMonitor,
2005) {
2006    let pending_tx: Vec<PendingTransaction> = dbtx
2007        .find_by_prefix(&PendingTransactionPrefixKey)
2008        .await
2009        .map(|(_, val)| val)
2010        .collect::<Vec<_>>()
2011        .await;
2012    let rbf_txids: BTreeSet<Txid> = pending_tx
2013        .iter()
2014        .filter_map(|tx| tx.rbf.clone().map(|rbf| rbf.txid))
2015        .collect();
2016    if !pending_tx.is_empty() {
2017        debug!(
2018            target: LOG_MODULE_WALLET,
2019            "Broadcasting pending transactions (total={}, rbf={})",
2020            pending_tx.len(),
2021            rbf_txids.len()
2022        );
2023    }
2024
2025    for PendingTransaction { tx, .. } in pending_tx {
2026        if !rbf_txids.contains(&tx.compute_txid()) {
2027            debug!(
2028                target: LOG_MODULE_WALLET,
2029                tx = %tx.compute_txid(),
2030                weight = tx.weight().to_wu(),
2031                output = ?tx.output,
2032                "Broadcasting peg-out",
2033            );
2034            trace!(transaction = ?tx);
2035            if let Err(err) = rpc.submit_transaction(tx).await {
2036                debug!(
2037                    target: LOG_MODULE_WALLET,
2038                    err = %err.fmt_compact_anyhow(),
2039                    "Error broadcasting peg-out transaction"
2040                );
2041            }
2042        }
2043    }
2044}
2045
2046struct StatelessWallet<'a> {
2047    descriptor: &'a Descriptor<CompressedPublicKey>,
2048    secret_key: &'a secp256k1::SecretKey,
2049    secp: &'a secp256k1::Secp256k1<secp256k1::All>,
2050}
2051
2052impl StatelessWallet<'_> {
2053    /// Given a tx created from an `WalletOutput`, validate there will be no
2054    /// issues submitting the transaction to the Bitcoin network
2055    fn validate_tx(
2056        tx: &UnsignedTransaction,
2057        output: &WalletOutputV0,
2058        consensus_fee_rate: Feerate,
2059        network: Network,
2060    ) -> Result<(), WalletOutputError> {
2061        if let WalletOutputV0::PegOut(peg_out) = output
2062            && !peg_out.recipient.is_valid_for_network(network)
2063        {
2064            return Err(WalletOutputError::WrongNetwork(
2065                NetworkLegacyEncodingWrapper(network),
2066                NetworkLegacyEncodingWrapper(get_network_for_address(&peg_out.recipient)),
2067            ));
2068        }
2069
2070        // Validate the tx amount is over the dust limit
2071        if tx.peg_out_amount < tx.destination.minimal_non_dust() {
2072            return Err(WalletOutputError::PegOutUnderDustLimit);
2073        }
2074
2075        // Validate tx fee rate is above the consensus fee rate
2076        if tx.fees.fee_rate < consensus_fee_rate {
2077            return Err(WalletOutputError::PegOutFeeBelowConsensus(
2078                tx.fees.fee_rate,
2079                consensus_fee_rate,
2080            ));
2081        }
2082
2083        // Validate added fees are above the min relay tx fee
2084        // BIP-0125 requires 1 sat/vb for RBF by default (same as normal txs)
2085        let fees = match output {
2086            WalletOutputV0::PegOut(pegout) => pegout.fees,
2087            WalletOutputV0::Rbf(rbf) => rbf.fees,
2088        };
2089        if fees.fee_rate.sats_per_kvb < u64::from(DEFAULT_MIN_RELAY_TX_FEE) {
2090            return Err(WalletOutputError::BelowMinRelayFee);
2091        }
2092
2093        // Validate fees weight matches the actual weight
2094        if fees.total_weight != tx.fees.total_weight {
2095            return Err(WalletOutputError::TxWeightIncorrect(
2096                fees.total_weight,
2097                tx.fees.total_weight,
2098            ));
2099        }
2100
2101        Ok(())
2102    }
2103
2104    /// Attempts to create a tx ready to be signed from available UTXOs.
2105    //
2106    // * `peg_out_amount`: How much the peg-out should be
2107    // * `destination`: The address the user is pegging-out to
2108    // * `included_utxos`: UXTOs that must be included (for RBF)
2109    // * `remaining_utxos`: All other spendable UXTOs
2110    // * `fee_rate`: How much needs to be spent on fees
2111    // * `change_tweak`: How the federation can recognize it's change UTXO
2112    // * `rbf`: If this is an RBF transaction
2113    #[allow(clippy::too_many_arguments)]
2114    fn create_tx(
2115        &self,
2116        peg_out_amount: bitcoin::Amount,
2117        destination: ScriptBuf,
2118        mut included_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2119        mut remaining_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2120        mut fee_rate: Feerate,
2121        change_tweak: &[u8; 33],
2122        rbf: Option<Rbf>,
2123    ) -> Result<UnsignedTransaction, WalletOutputError> {
2124        // Add the rbf fees to the existing tx fees
2125        if let Some(rbf) = &rbf {
2126            fee_rate.sats_per_kvb += rbf.fees.fee_rate.sats_per_kvb;
2127        }
2128
2129        // When building a transaction we need to take care of two things:
2130        //  * We need enough input amount to fund all outputs
2131        //  * We need to keep an eye on the tx weight so we can factor the fees into out
2132        //    calculation
2133        // We then go on to calculate the base size of the transaction `total_weight`
2134        // and the maximum weight per added input which we will add every time
2135        // we select an input.
2136        let change_script = self.derive_script(change_tweak);
2137        let out_weight = (destination.len() * 4 + 1 + 32
2138            // Add change script weight, it's very likely to be needed if not we just overpay in fees
2139            + 1 // script len varint, 1 byte for all addresses we accept
2140            + change_script.len() * 4 // script len
2141            + 32) as u64; // value
2142        let mut total_weight = 16 + // version
2143            12 + // up to 2**16-1 inputs
2144            12 + // up to 2**16-1 outputs
2145            out_weight + // weight of all outputs
2146            16; // lock time
2147        // https://github.com/fedimint/fedimint/issues/4590
2148        #[allow(deprecated)]
2149        let max_input_weight = (self
2150            .descriptor
2151            .max_satisfaction_weight()
2152            .expect("is satisfyable") +
2153            128 + // TxOutHash
2154            16 + // TxOutIndex
2155            16) as u64; // sequence
2156
2157        // Ensure deterministic ordering of UTXOs for all peers
2158        included_utxos.sort_by_key(|(_, utxo)| utxo.amount);
2159        remaining_utxos.sort_by_key(|(_, utxo)| utxo.amount);
2160        included_utxos.extend(remaining_utxos);
2161
2162        // Finally we initialize our accumulator for selected input amounts
2163        let mut total_selected_value = bitcoin::Amount::from_sat(0);
2164        let mut selected_utxos: Vec<(UTXOKey, SpendableUTXO)> = vec![];
2165        let mut fees = fee_rate.calculate_fee(total_weight);
2166
2167        while total_selected_value < peg_out_amount + change_script.minimal_non_dust() + fees {
2168            match included_utxos.pop() {
2169                Some((utxo_key, utxo)) => {
2170                    total_selected_value += utxo.amount;
2171                    total_weight += max_input_weight;
2172                    fees = fee_rate.calculate_fee(total_weight);
2173                    selected_utxos.push((utxo_key, utxo));
2174                }
2175                _ => return Err(WalletOutputError::NotEnoughSpendableUTXO), // Not enough UTXOs
2176            }
2177        }
2178
2179        // We always pay ourselves change back to ensure that we don't lose anything due
2180        // to dust
2181        let change = total_selected_value - fees - peg_out_amount;
2182        let output: Vec<TxOut> = vec![
2183            TxOut {
2184                value: peg_out_amount,
2185                script_pubkey: destination.clone(),
2186            },
2187            TxOut {
2188                value: change,
2189                script_pubkey: change_script,
2190            },
2191        ];
2192        let mut change_out = bitcoin::psbt::Output::default();
2193        change_out
2194            .proprietary
2195            .insert(proprietary_tweak_key(), change_tweak.to_vec());
2196
2197        info!(
2198            target: LOG_MODULE_WALLET,
2199            inputs = selected_utxos.len(),
2200            input_sats = total_selected_value.to_sat(),
2201            peg_out_sats = peg_out_amount.to_sat(),
2202            ?total_weight,
2203            fees_sats = fees.to_sat(),
2204            fee_rate = fee_rate.sats_per_kvb,
2205            change_sats = change.to_sat(),
2206            "Creating peg-out tx",
2207        );
2208
2209        let transaction = Transaction {
2210            version: bitcoin::transaction::Version(2),
2211            lock_time: LockTime::ZERO,
2212            input: selected_utxos
2213                .iter()
2214                .map(|(utxo_key, _utxo)| TxIn {
2215                    previous_output: utxo_key.0,
2216                    script_sig: Default::default(),
2217                    sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
2218                    witness: bitcoin::Witness::new(),
2219                })
2220                .collect(),
2221            output,
2222        };
2223        info!(
2224            target: LOG_MODULE_WALLET,
2225            txid = %transaction.compute_txid(), "Creating peg-out tx"
2226        );
2227
2228        // FIXME: use custom data structure that guarantees more invariants and only
2229        // convert to PSBT for finalization
2230        let psbt = Psbt {
2231            unsigned_tx: transaction,
2232            version: 0,
2233            xpub: Default::default(),
2234            proprietary: Default::default(),
2235            unknown: Default::default(),
2236            inputs: selected_utxos
2237                .iter()
2238                .map(|(_utxo_key, utxo)| {
2239                    let script_pubkey = self
2240                        .descriptor
2241                        .tweak(&utxo.tweak, self.secp)
2242                        .script_pubkey();
2243                    Input {
2244                        non_witness_utxo: None,
2245                        witness_utxo: Some(TxOut {
2246                            value: utxo.amount,
2247                            script_pubkey,
2248                        }),
2249                        partial_sigs: Default::default(),
2250                        sighash_type: None,
2251                        redeem_script: None,
2252                        witness_script: Some(
2253                            self.descriptor
2254                                .tweak(&utxo.tweak, self.secp)
2255                                .script_code()
2256                                .expect("Failed to tweak descriptor"),
2257                        ),
2258                        bip32_derivation: Default::default(),
2259                        final_script_sig: None,
2260                        final_script_witness: None,
2261                        ripemd160_preimages: Default::default(),
2262                        sha256_preimages: Default::default(),
2263                        hash160_preimages: Default::default(),
2264                        hash256_preimages: Default::default(),
2265                        proprietary: vec![(proprietary_tweak_key(), utxo.tweak.to_vec())]
2266                            .into_iter()
2267                            .collect(),
2268                        tap_key_sig: Default::default(),
2269                        tap_script_sigs: Default::default(),
2270                        tap_scripts: Default::default(),
2271                        tap_key_origins: Default::default(),
2272                        tap_internal_key: Default::default(),
2273                        tap_merkle_root: Default::default(),
2274                        unknown: Default::default(),
2275                    }
2276                })
2277                .collect(),
2278            outputs: vec![Default::default(), change_out],
2279        };
2280
2281        Ok(UnsignedTransaction {
2282            psbt,
2283            signatures: vec![],
2284            change,
2285            fees: PegOutFees {
2286                fee_rate,
2287                total_weight,
2288            },
2289            destination,
2290            selected_utxos,
2291            peg_out_amount,
2292            rbf,
2293        })
2294    }
2295
2296    fn sign_psbt(&self, psbt: &mut Psbt) {
2297        let mut tx_hasher = SighashCache::new(&psbt.unsigned_tx);
2298
2299        for (idx, (psbt_input, _tx_input)) in psbt
2300            .inputs
2301            .iter_mut()
2302            .zip(psbt.unsigned_tx.input.iter())
2303            .enumerate()
2304        {
2305            let tweaked_secret = {
2306                let tweak = psbt_input
2307                    .proprietary
2308                    .get(&proprietary_tweak_key())
2309                    .expect("Malformed PSBT: expected tweak");
2310
2311                self.secret_key.tweak(tweak, self.secp)
2312            };
2313
2314            let tx_hash = tx_hasher
2315                .p2wsh_signature_hash(
2316                    idx,
2317                    psbt_input
2318                        .witness_script
2319                        .as_ref()
2320                        .expect("Missing witness script"),
2321                    psbt_input
2322                        .witness_utxo
2323                        .as_ref()
2324                        .expect("Missing UTXO")
2325                        .value,
2326                    EcdsaSighashType::All,
2327                )
2328                .expect("Failed to create segwit sighash");
2329
2330            let signature = self.secp.sign_ecdsa(
2331                &Message::from_digest_slice(&tx_hash[..]).unwrap(),
2332                &tweaked_secret,
2333            );
2334
2335            psbt_input.partial_sigs.insert(
2336                bitcoin::PublicKey {
2337                    compressed: true,
2338                    inner: secp256k1::PublicKey::from_secret_key(self.secp, &tweaked_secret),
2339                },
2340                EcdsaSig::sighash_all(signature),
2341            );
2342        }
2343    }
2344
2345    fn derive_script(&self, tweak: &[u8]) -> ScriptBuf {
2346        struct CompressedPublicKeyTranslator<'t, 's, Ctx: Verification> {
2347            tweak: &'t [u8],
2348            secp: &'s Secp256k1<Ctx>,
2349        }
2350
2351        impl<Ctx: Verification>
2352            miniscript::Translator<CompressedPublicKey, CompressedPublicKey, Infallible>
2353            for CompressedPublicKeyTranslator<'_, '_, Ctx>
2354        {
2355            fn pk(&mut self, pk: &CompressedPublicKey) -> Result<CompressedPublicKey, Infallible> {
2356                let hashed_tweak = {
2357                    let mut hasher = HmacEngine::<sha256::Hash>::new(&pk.key.serialize()[..]);
2358                    hasher.input(self.tweak);
2359                    Hmac::from_engine(hasher).to_byte_array()
2360                };
2361
2362                Ok(CompressedPublicKey {
2363                    key: pk
2364                        .key
2365                        .add_exp_tweak(
2366                            self.secp,
2367                            &Scalar::from_be_bytes(hashed_tweak).expect("can't fail"),
2368                        )
2369                        .expect("tweaking failed"),
2370                })
2371            }
2372            translate_hash_fail!(CompressedPublicKey, CompressedPublicKey, Infallible);
2373        }
2374
2375        let descriptor = self
2376            .descriptor
2377            .translate_pk(&mut CompressedPublicKeyTranslator {
2378                tweak,
2379                secp: self.secp,
2380            })
2381            .expect("can't fail");
2382
2383        descriptor.script_pubkey()
2384    }
2385}
2386
2387pub fn nonce_from_idx(nonce_idx: u64) -> [u8; 33] {
2388    let mut nonce: [u8; 33] = [0; 33];
2389    // Make it look like a compressed pubkey, has to be either 0x02 or 0x03
2390    nonce[0] = 0x02;
2391    nonce[1..].copy_from_slice(&nonce_idx.consensus_hash::<bitcoin::hashes::sha256::Hash>()[..]);
2392
2393    nonce
2394}
2395
2396/// A peg-out tx that is ready to be broadcast with a tweak for the change UTXO
2397#[derive(Clone, Debug, Encodable, Decodable)]
2398pub struct PendingTransaction {
2399    pub tx: bitcoin::Transaction,
2400    pub tweak: [u8; 33],
2401    pub change: bitcoin::Amount,
2402    pub destination: ScriptBuf,
2403    pub fees: PegOutFees,
2404    pub selected_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2405    pub peg_out_amount: bitcoin::Amount,
2406    pub rbf: Option<Rbf>,
2407}
2408
2409impl Serialize for PendingTransaction {
2410    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2411    where
2412        S: serde::Serializer,
2413    {
2414        if serializer.is_human_readable() {
2415            serializer.serialize_str(&self.consensus_encode_to_hex())
2416        } else {
2417            serializer.serialize_bytes(&self.consensus_encode_to_vec())
2418        }
2419    }
2420}
2421
2422/// A PSBT that is awaiting enough signatures from the federation to becoming a
2423/// `PendingTransaction`
2424#[derive(Clone, Debug, Eq, PartialEq, Encodable, Decodable)]
2425pub struct UnsignedTransaction {
2426    pub psbt: Psbt,
2427    pub signatures: Vec<(PeerId, PegOutSignatureItem)>,
2428    pub change: bitcoin::Amount,
2429    pub fees: PegOutFees,
2430    pub destination: ScriptBuf,
2431    pub selected_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2432    pub peg_out_amount: bitcoin::Amount,
2433    pub rbf: Option<Rbf>,
2434}
2435
2436impl Serialize for UnsignedTransaction {
2437    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2438    where
2439        S: serde::Serializer,
2440    {
2441        if serializer.is_human_readable() {
2442            serializer.serialize_str(&self.consensus_encode_to_hex())
2443        } else {
2444            serializer.serialize_bytes(&self.consensus_encode_to_vec())
2445        }
2446    }
2447}
2448
2449#[cfg(test)]
2450mod tests {
2451
2452    use std::str::FromStr;
2453
2454    use bitcoin::Network::{Bitcoin, Testnet};
2455    use bitcoin::hashes::Hash;
2456    use bitcoin::{Address, Amount, OutPoint, Txid, secp256k1};
2457    use fedimint_core::Feerate;
2458    use fedimint_core::encoding::btc::NetworkLegacyEncodingWrapper;
2459    use fedimint_core::envs::is_automatic_consensus_version_voting_disabled;
2460    use fedimint_wallet_common::{PegOut, PegOutFees, Rbf, WalletOutputV0};
2461    use miniscript::descriptor::Wsh;
2462
2463    use crate::common::PegInDescriptor;
2464    use crate::{
2465        CompressedPublicKey, OsRng, SpendableUTXO, StatelessWallet, UTXOKey, WalletOutputError,
2466    };
2467
2468    #[test]
2469    fn create_tx_should_validate_amounts() {
2470        let secp = secp256k1::Secp256k1::new();
2471
2472        let descriptor = PegInDescriptor::Wsh(
2473            Wsh::new_sortedmulti(
2474                3,
2475                (0..4)
2476                    .map(|_| secp.generate_keypair(&mut OsRng))
2477                    .map(|(_, key)| CompressedPublicKey { key })
2478                    .collect(),
2479            )
2480            .unwrap(),
2481        );
2482
2483        let (secret_key, _) = secp.generate_keypair(&mut OsRng);
2484
2485        let wallet = StatelessWallet {
2486            descriptor: &descriptor,
2487            secret_key: &secret_key,
2488            secp: &secp,
2489        };
2490
2491        let spendable = SpendableUTXO {
2492            tweak: [0; 33],
2493            amount: bitcoin::Amount::from_sat(3000),
2494        };
2495
2496        let recipient = Address::from_str("32iVBEu4dxkUQk9dJbZUiBiQdmypcEyJRf").unwrap();
2497
2498        let fee = Feerate { sats_per_kvb: 1000 };
2499        let weight = 875;
2500
2501        // not enough SpendableUTXO
2502        // tx fee = ceil(875 / 4) * 1 sat/vb = 219
2503        // change script dust = 330
2504        // spendable sats = 3000 - 219 - 330 = 2451
2505        let tx = wallet.create_tx(
2506            Amount::from_sat(2452),
2507            recipient.clone().assume_checked().script_pubkey(),
2508            vec![],
2509            vec![(UTXOKey(OutPoint::null()), spendable.clone())],
2510            fee,
2511            &[0; 33],
2512            None,
2513        );
2514        assert_eq!(tx, Err(WalletOutputError::NotEnoughSpendableUTXO));
2515
2516        // successful tx creation
2517        let mut tx = wallet
2518            .create_tx(
2519                Amount::from_sat(1000),
2520                recipient.clone().assume_checked().script_pubkey(),
2521                vec![],
2522                vec![(UTXOKey(OutPoint::null()), spendable)],
2523                fee,
2524                &[0; 33],
2525                None,
2526            )
2527            .expect("is ok");
2528
2529        // peg out weight is incorrectly set to 0
2530        let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, 0), fee, Bitcoin);
2531        assert_eq!(res, Err(WalletOutputError::TxWeightIncorrect(0, weight)));
2532
2533        // fee rate set below min relay fee to 0
2534        let res = StatelessWallet::validate_tx(&tx, &rbf(0, weight), fee, Bitcoin);
2535        assert_eq!(res, Err(WalletOutputError::BelowMinRelayFee));
2536
2537        // fees are okay
2538        let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2539        assert_eq!(res, Ok(()));
2540
2541        // tx has fee below consensus
2542        tx.fees = PegOutFees::new(0, weight);
2543        let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2544        assert_eq!(
2545            res,
2546            Err(WalletOutputError::PegOutFeeBelowConsensus(
2547                Feerate { sats_per_kvb: 0 },
2548                fee
2549            ))
2550        );
2551
2552        // tx has peg-out amount under dust limit
2553        tx.peg_out_amount = bitcoin::Amount::ZERO;
2554        let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2555        assert_eq!(res, Err(WalletOutputError::PegOutUnderDustLimit));
2556
2557        // tx is invalid for network
2558        let output = WalletOutputV0::PegOut(PegOut {
2559            recipient,
2560            amount: bitcoin::Amount::from_sat(1000),
2561            fees: PegOutFees::new(100, weight),
2562        });
2563        let res = StatelessWallet::validate_tx(&tx, &output, fee, Testnet);
2564        assert_eq!(
2565            res,
2566            Err(WalletOutputError::WrongNetwork(
2567                NetworkLegacyEncodingWrapper(Testnet),
2568                NetworkLegacyEncodingWrapper(Bitcoin)
2569            ))
2570        );
2571    }
2572
2573    fn rbf(sats_per_kvb: u64, total_weight: u64) -> WalletOutputV0 {
2574        WalletOutputV0::Rbf(Rbf {
2575            fees: PegOutFees::new(sats_per_kvb, total_weight),
2576            txid: Txid::all_zeros(),
2577        })
2578    }
2579
2580    #[test]
2581    fn automatic_vote_suppressed_when_env_set() {
2582        unsafe {
2583            std::env::set_var("FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING", "1");
2584        }
2585        assert!(is_automatic_consensus_version_voting_disabled());
2586        unsafe {
2587            std::env::remove_var("FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING");
2588        }
2589    }
2590
2591    #[test]
2592    fn automatic_vote_active_when_env_unset() {
2593        unsafe {
2594            std::env::remove_var("FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING");
2595        }
2596        assert!(!is_automatic_consensus_version_voting_disabled());
2597    }
2598}