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