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_running_in_test_env,
59    next_poll_delay,
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, weight_to_vbytes,
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    CHECKED_PEG_OUT_FEE_MODULE_CONSENSUS_VERSION, MODULE_CONSENSUS_VERSION, Rbf, RecoveryItem,
95    UnknownWalletInputVariantError, WalletInputError, 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                bail!("Unknown wallet consensus item received, 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        // `ClaimedPegInOutpoint` only covers outpoints that reached us as peg-ins, but
794        // `UTXOKey` also holds outputs we recognized as our own, which never pass
795        // through this path. Crediting one of those would overwrite a tracked UTXO, and
796        // the write below panics rather than overwrite, which in consensus is a halt
797        // rather than a rejection. Refuse the input instead, for any outpoint we
798        // already track, however it got there.
799        if dbtx.get_value(&UTXOKey(outpoint)).await.is_some() {
800            return Err(WalletInputError::PegInAlreadyClaimed);
801        }
802
803        if dbtx
804            .insert_entry(&ClaimedPegInOutpointKey(outpoint), &())
805            .await
806            .is_some()
807        {
808            return Err(WalletInputError::PegInAlreadyClaimed);
809        }
810
811        dbtx.insert_new_entry(
812            &UTXOKey(outpoint),
813            &SpendableUTXO {
814                tweak: pub_key.serialize(),
815                amount: tx_out.value,
816            },
817        )
818        .await;
819
820        let next_index = get_recovery_count(dbtx).await;
821        dbtx.insert_new_entry(
822            &RecoveryItemKey(next_index),
823            &RecoveryItem::Input {
824                outpoint,
825                script: tx_out.script_pubkey,
826            },
827        )
828        .await;
829
830        let amount = tx_out.value.into();
831
832        let fee = self.cfg.consensus.fee_consensus.peg_in_abs;
833
834        calculate_pegin_metrics(dbtx, amount, fee);
835
836        Ok(InputMeta {
837            amount: TransactionItemAmounts {
838                amounts: Amounts::new_bitcoin(amount),
839                fees: Amounts::new_bitcoin(fee),
840            },
841            pub_key,
842        })
843    }
844
845    async fn process_output<'a, 'b>(
846        &'a self,
847        dbtx: &mut DatabaseTransaction<'b>,
848        output: &'a WalletOutput,
849        out_point: OutPoint,
850    ) -> Result<TransactionItemAmounts, WalletOutputError> {
851        let output = output.ensure_v0_ref()?;
852
853        // In 0.4.0 we began preventing RBF withdrawals. Once we reach EoL support
854        // for 0.4.0, we can safely remove RBF withdrawal logic.
855        // see: https://github.com/fedimint/fedimint/issues/5453
856        //
857        // Rejection must not depend on per-guardian configuration: any
858        // conditional acceptance here lets guardians diverge on whether a
859        // transaction is valid, forking consensus.
860        if let WalletOutputV0::Rbf(_) = output {
861            return Err(DEPRECATED_RBF_ERROR);
862        }
863
864        let change_tweak = self.consensus_nonce(dbtx).await;
865
866        let mut tx = self.create_peg_out_tx(dbtx, output, &change_tweak).await?;
867
868        let fee_rate = self.consensus_fee_rate(dbtx).await;
869
870        StatelessWallet::validate_tx(&tx, output, fee_rate, self.cfg.consensus.network.0)?;
871
872        self.offline_wallet().sign_psbt(&mut tx.psbt);
873
874        let txid = tx.psbt.unsigned_tx.compute_txid();
875
876        info!(
877            target: LOG_MODULE_WALLET,
878            %txid,
879            "Signing peg out",
880        );
881
882        let sigs = tx
883            .psbt
884            .inputs
885            .iter_mut()
886            .map(|input| {
887                assert_eq!(
888                    input.partial_sigs.len(),
889                    1,
890                    "There was already more than one (our) or no signatures in input"
891                );
892
893                // TODO: don't put sig into PSBT in the first place
894                // We actually take out our own signature so everyone finalizes the tx in the
895                // same epoch.
896                let sig = std::mem::take(&mut input.partial_sigs)
897                    .into_values()
898                    .next()
899                    .expect("asserted previously");
900
901                // We drop SIGHASH_ALL, because we always use that and it is only present in the
902                // PSBT for compatibility with other tools.
903                secp256k1::ecdsa::Signature::from_der(&sig.to_vec()[..sig.to_vec().len() - 1])
904                    .expect("we serialized it ourselves that way")
905            })
906            .collect::<Vec<_>>();
907
908        // Delete used UTXOs
909        for input in &tx.psbt.unsigned_tx.input {
910            dbtx.remove_entry(&UTXOKey(input.previous_output)).await;
911        }
912
913        // Our own change output pays to the peg-in descriptor, so it is
914        // indistinguishable from a user deposit. Record it as claimed now, in the same
915        // dbtx that spends the inputs, so it is never a peg-in candidate. Doing this at
916        // confirmation time instead would leave it claimable while the transaction is
917        // in flight.
918        dbtx.insert_entry(
919            &ClaimedPegInOutpointKey(bitcoin::OutPoint {
920                txid,
921                vout: PEG_OUT_CHANGE_VOUT,
922            }),
923            &(),
924        )
925        .await;
926
927        dbtx.insert_new_entry(&UnsignedTransactionKey(txid), &tx)
928            .await;
929
930        dbtx.insert_new_entry(&PegOutTxSignatureCI(txid), &sigs)
931            .await;
932
933        dbtx.insert_new_entry(
934            &PegOutBitcoinTransaction(out_point),
935            &WalletOutputOutcome::new_v0(txid),
936        )
937        .await;
938        let amount: fedimint_core::Amount = output.amount().into();
939        let fee = self.cfg.consensus.fee_consensus.peg_out_abs;
940        calculate_pegout_metrics(dbtx, amount, fee);
941        Ok(TransactionItemAmounts {
942            amounts: Amounts::new_bitcoin(amount),
943            fees: Amounts::new_bitcoin(fee),
944        })
945    }
946
947    /// Reject a peg-out whose declared fee rate cannot produce a fee that
948    /// exists on chain.
949    ///
950    /// [`Feerate::calculate_fee`] multiplies the declared rate by the
951    /// transaction's virtual size without checking for overflow, so a rate near
952    /// `u64::MAX / vbytes` wraps to an arbitrarily small fee. Such a peg-out
953    /// passes [`StatelessWallet::validate_tx`], which compares the *declared*
954    /// rate against the consensus and relay floors and never the computed fee,
955    /// and is then signed and broadcast with a fee too low to confirm. Its
956    /// inputs leave [`UTXOKey`] the moment the peg-out is accepted and only
957    /// return once the transaction confirms, so they are stranded for as long
958    /// as it does not — and selection spends largest-first.
959    ///
960    /// This is enforced as submission policy rather than in
961    /// [`ServerModule::process_output`] because tightening the latter would
962    /// change transaction validity. Release builds wrap rather than panic here,
963    /// so such peg-outs were accepted historically and rejecting them in
964    /// consensus needs a module consensus version bump. As policy it is only as
965    /// strong as the weakest guardian.
966    #[doc(hidden)]
967    async fn verify_output_submission<'a, 'b>(
968        &'a self,
969        _dbtx: &mut DatabaseTransaction<'b>,
970        output: &'a WalletOutput,
971        _out_point: OutPoint,
972    ) -> Result<(), WalletOutputError> {
973        let fees = match output.ensure_v0_ref()? {
974            WalletOutputV0::PegOut(peg_out) => peg_out.fees,
975            WalletOutputV0::Rbf(rbf) => rbf.fees,
976        };
977
978        let fee_sats = weight_to_vbytes(fees.total_weight)
979            .checked_mul(fees.fee_rate.sats_per_kvb)
980            .map(|sats| sats / 1000);
981
982        // A fee larger than the money supply can no more be funded than an
983        // amount larger than it can, which is what this variant already reports
984        // for the peg-out amount itself.
985        match fee_sats {
986            Some(sats) if sats <= bitcoin::Amount::MAX_MONEY.to_sat() => Ok(()),
987            _ => Err(WalletOutputError::NotEnoughSpendableUTXO),
988        }
989    }
990
991    async fn output_status(
992        &self,
993        dbtx: &mut DatabaseTransaction<'_>,
994        out_point: OutPoint,
995    ) -> Option<WalletOutputOutcome> {
996        dbtx.get_value(&PegOutBitcoinTransaction(out_point)).await
997    }
998
999    async fn audit(
1000        &self,
1001        dbtx: &mut DatabaseTransaction<'_>,
1002        audit: &mut Audit,
1003        module_instance_id: ModuleInstanceId,
1004    ) {
1005        audit
1006            .add_items(dbtx, module_instance_id, &UTXOPrefixKey, |_, v| {
1007                v.amount.to_sat() as i64 * 1000
1008            })
1009            .await;
1010        audit
1011            .add_items(
1012                dbtx,
1013                module_instance_id,
1014                &UnsignedTransactionPrefixKey,
1015                |_, v| match v.rbf {
1016                    None => v.change.to_sat() as i64 * 1000,
1017                    Some(rbf) => rbf.fees.amount().to_sat() as i64 * -1000,
1018                },
1019            )
1020            .await;
1021        audit
1022            .add_items(
1023                dbtx,
1024                module_instance_id,
1025                &PendingTransactionPrefixKey,
1026                |_, v| match v.rbf {
1027                    None => v.change.to_sat() as i64 * 1000,
1028                    Some(rbf) => rbf.fees.amount().to_sat() as i64 * -1000,
1029                },
1030            )
1031            .await;
1032    }
1033
1034    fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
1035        vec![
1036            public_api_endpoint! {
1037                BLOCK_COUNT_ENDPOINT,
1038                ApiVersion::new(0, 0),
1039                async |module: &Wallet, context, _params: ()| -> u32 {
1040                    let db = context.db();
1041                    let mut dbtx = db.begin_transaction_nc().await;
1042                    Ok(module.consensus_block_count(&mut dbtx).await)
1043                }
1044            },
1045            public_api_endpoint! {
1046                BLOCK_COUNT_LOCAL_ENDPOINT,
1047                ApiVersion::new(0, 0),
1048                async |module: &Wallet, _context, _params: ()| -> Option<u32> {
1049                    Ok(module.get_block_count().ok())
1050                }
1051            },
1052            public_api_endpoint! {
1053                PEG_OUT_FEES_ENDPOINT,
1054                ApiVersion::new(0, 0),
1055                async |module: &Wallet, context, params: (Address<NetworkUnchecked>, u64)| -> Option<PegOutFees> {
1056                    let (address, sats) = params;
1057                    let db = context.db();
1058                    let mut dbtx = db.begin_transaction_nc().await;
1059                    let feerate = module.consensus_fee_rate(&mut dbtx).await;
1060
1061                    // Since we are only calculating the tx size we can use an arbitrary dummy nonce.
1062                    let dummy_tweak = [0; 33];
1063
1064                    let tx = module.offline_wallet().create_tx(
1065                        bitcoin::Amount::from_sat(sats),
1066                        // Note: While calling `assume_checked()` is generally unwise, it's fine
1067                        // here since we're only returning a fee estimate, and we would still
1068                        // reject a transaction with the wrong network upon attempted peg-out.
1069                        address.assume_checked().script_pubkey(),
1070                        vec![],
1071                        module.available_utxos(&mut dbtx).await,
1072                        feerate,
1073                        &dummy_tweak,
1074                        None,
1075                        // A quote is not consensus, so it can always refuse a
1076                        // rate whose fee could not exist on chain.
1077                        FeeArithmetic::Checked
1078                    );
1079
1080                    match tx {
1081                        Err(error) => {
1082                            // Usually from not enough spendable UTXOs
1083                            warn!(target: LOG_MODULE_WALLET, "Error returning peg-out fees {error}");
1084                            Ok(None)
1085                        }
1086                        Ok(tx) => Ok(Some(tx.fees))
1087                    }
1088                }
1089            },
1090            public_api_endpoint! {
1091                BITCOIN_KIND_ENDPOINT,
1092                ApiVersion::new(0, 1),
1093                async |module: &Wallet, _context, _params: ()| -> String {
1094                    Ok(module.btc_rpc.get_bitcoin_rpc_config().kind)
1095                }
1096            },
1097            admin_api_endpoint! {
1098                BITCOIN_RPC_CONFIG_ENDPOINT,
1099                ApiVersion::new(0, 1),
1100                async |module: &Wallet, context, _params: ()| -> BitcoinRpcConfig {
1101                        let config = module.btc_rpc.get_bitcoin_rpc_config();
1102
1103                    // we need to remove auth, otherwise we'll send over the wire
1104                    let without_auth = config.url.clone().without_auth().map_err(|()| {
1105                        ApiError::server_error("Unable to remove auth from bitcoin config URL".to_string())
1106                    })?;
1107
1108                    Ok(BitcoinRpcConfig {
1109                        url: without_auth,
1110                        ..config
1111                    })
1112                }
1113            },
1114            public_api_endpoint! {
1115                WALLET_SUMMARY_ENDPOINT,
1116                ApiVersion::new(0, 1),
1117                async |module: &Wallet, context, _params: ()| -> WalletSummary {
1118                    let db = context.db();
1119                    let mut dbtx = db.begin_transaction_nc().await;
1120                    Ok(module.get_wallet_summary(&mut dbtx).await)
1121                }
1122            },
1123            public_api_endpoint! {
1124                MODULE_CONSENSUS_VERSION_ENDPOINT,
1125                ApiVersion::new(0, 2),
1126                async |module: &Wallet, context, _params: ()| -> ModuleConsensusVersion {
1127                    let db = context.db();
1128                    let mut dbtx = db.begin_transaction_nc().await;
1129                    Ok(module.consensus_module_consensus_version(&mut dbtx).await)
1130                }
1131            },
1132            public_api_endpoint! {
1133                SUPPORTED_MODULE_CONSENSUS_VERSION_ENDPOINT,
1134                ApiVersion::new(0, 2),
1135                async |_module: &Wallet, _context, _params: ()| -> ModuleConsensusVersion {
1136                    Ok(MODULE_CONSENSUS_VERSION)
1137                }
1138            },
1139            admin_api_endpoint! {
1140                ACTIVATE_CONSENSUS_VERSION_VOTING_ENDPOINT,
1141                ApiVersion::new(0, 2),
1142                async |_module: &Wallet, context, _params: ()| -> () {
1143
1144                    let db = context.db();
1145                    let mut dbtx = db.begin_transaction().await;
1146                    dbtx.to_ref().insert_entry(&ConsensusVersionVotingActivationKey, &()).await;
1147                    dbtx.commit_tx_result().await?;
1148                    Ok(())
1149                }
1150            },
1151            public_api_endpoint! {
1152                UTXO_CONFIRMED_ENDPOINT,
1153                ApiVersion::new(0, 2),
1154                async |module: &Wallet, context, outpoint: bitcoin::OutPoint| -> bool {
1155                    let db = context.db();
1156                    let mut dbtx = db.begin_transaction_nc().await;
1157                    Ok(module.is_utxo_confirmed(&mut dbtx, outpoint).await)
1158                }
1159            },
1160            public_api_endpoint! {
1161                RECOVERY_COUNT_ENDPOINT,
1162                ApiVersion::new(0, 1),
1163                async |_module: &Wallet, context, _params: ()| -> u64 {
1164                    let db = context.db();
1165                    let mut dbtx = db.begin_transaction_nc().await;
1166                    Ok(get_recovery_count(&mut dbtx).await)
1167                }
1168            },
1169            public_api_endpoint! {
1170                RECOVERY_SLICE_ENDPOINT,
1171                ApiVersion::new(0, 1),
1172                async |_module: &Wallet, context, range: (u64, u64)| -> Vec<RecoveryItem> {
1173                    let db = context.db();
1174                    let mut dbtx = db.begin_transaction_nc().await;
1175                    Ok(get_recovery_slice(&mut dbtx, range).await)
1176                }
1177            },
1178        ]
1179    }
1180}
1181
1182async fn get_recovery_count(dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1183    dbtx.find_by_prefix_sorted_descending(&RecoveryItemKeyPrefix)
1184        .await
1185        .next()
1186        .await
1187        .map_or(0, |entry| entry.0.0 + 1)
1188}
1189
1190async fn get_recovery_slice(
1191    dbtx: &mut DatabaseTransaction<'_>,
1192    range: (u64, u64),
1193) -> Vec<RecoveryItem> {
1194    dbtx.find_by_range(RecoveryItemKey(range.0)..RecoveryItemKey(range.1))
1195        .await
1196        .map(|entry| entry.1)
1197        .collect()
1198        .await
1199}
1200
1201fn calculate_pegin_metrics(
1202    dbtx: &mut DatabaseTransaction<'_>,
1203    amount: fedimint_core::Amount,
1204    fee: fedimint_core::Amount,
1205) {
1206    dbtx.on_commit(move || {
1207        WALLET_INOUT_SATS
1208            .with_label_values(&["incoming"])
1209            .observe(amount.sats_f64());
1210        WALLET_INOUT_FEES_SATS
1211            .with_label_values(&["incoming"])
1212            .observe(fee.sats_f64());
1213        WALLET_PEGIN_SATS.observe(amount.sats_f64());
1214        WALLET_PEGIN_FEES_SATS.observe(fee.sats_f64());
1215    });
1216}
1217
1218fn calculate_pegout_metrics(
1219    dbtx: &mut DatabaseTransaction<'_>,
1220    amount: fedimint_core::Amount,
1221    fee: fedimint_core::Amount,
1222) {
1223    dbtx.on_commit(move || {
1224        WALLET_INOUT_SATS
1225            .with_label_values(&["outgoing"])
1226            .observe(amount.sats_f64());
1227        WALLET_INOUT_FEES_SATS
1228            .with_label_values(&["outgoing"])
1229            .observe(fee.sats_f64());
1230        WALLET_PEGOUT_SATS.observe(amount.sats_f64());
1231        WALLET_PEGOUT_FEES_SATS.observe(fee.sats_f64());
1232    });
1233}
1234
1235#[derive(Debug)]
1236pub struct Wallet {
1237    cfg: WalletConfig,
1238    db: Database,
1239    secp: Secp256k1<All>,
1240    btc_rpc: ServerBitcoinRpcMonitor,
1241    our_peer_id: PeerId,
1242    /// Broadcasting pending txes can be triggered immediately with this
1243    broadcast_pending: Arc<Notify>,
1244    task_group: TaskGroup,
1245    /// Maximum consensus version supported by *all* our peers. Used to
1246    /// automatically activate new consensus versions as soon as everyone
1247    /// upgrades.
1248    peer_supported_consensus_version: watch::Receiver<Option<ModuleConsensusVersion>>,
1249}
1250
1251impl Wallet {
1252    pub async fn new(
1253        cfg: WalletConfig,
1254        db: &Database,
1255        task_group: &TaskGroup,
1256        our_peer_id: PeerId,
1257        module_api: DynModuleApi,
1258        server_bitcoin_rpc_monitor: ServerBitcoinRpcMonitor,
1259    ) -> anyhow::Result<Wallet> {
1260        let broadcast_pending = Arc::new(Notify::new());
1261        Self::spawn_broadcast_pending_task(
1262            task_group,
1263            &server_bitcoin_rpc_monitor,
1264            db,
1265            broadcast_pending.clone(),
1266        );
1267
1268        let peer_supported_consensus_version =
1269            Self::spawn_peer_supported_consensus_version_task(module_api, task_group, our_peer_id);
1270
1271        let status = retry("verify network", backoff_util::aggressive_backoff(), || {
1272            std::future::ready(
1273                server_bitcoin_rpc_monitor
1274                    .status()
1275                    .context("No connection to bitcoin rpc"),
1276            )
1277        })
1278        .await?;
1279
1280        ensure!(status.network == cfg.consensus.network.0, "Wrong Network");
1281
1282        let wallet = Wallet {
1283            cfg,
1284            db: db.clone(),
1285            secp: Default::default(),
1286            btc_rpc: server_bitcoin_rpc_monitor,
1287            our_peer_id,
1288            task_group: task_group.clone(),
1289            peer_supported_consensus_version,
1290            broadcast_pending,
1291        };
1292
1293        Ok(wallet)
1294    }
1295
1296    /// Try to attach signatures to a pending peg-out tx.
1297    fn sign_peg_out_psbt(
1298        &self,
1299        psbt: &mut Psbt,
1300        peer: PeerId,
1301        signature: &PegOutSignatureItem,
1302    ) -> Result<(), ProcessPegOutSigError> {
1303        let peer_key = self
1304            .cfg
1305            .consensus
1306            .peer_peg_in_keys
1307            .get(&peer)
1308            .expect("always called with valid peer id");
1309
1310        if psbt.inputs.len() != signature.signature.len() {
1311            return Err(ProcessPegOutSigError::WrongSignatureCount(
1312                psbt.inputs.len(),
1313                signature.signature.len(),
1314            ));
1315        }
1316
1317        let mut tx_hasher = SighashCache::new(&psbt.unsigned_tx);
1318        for (idx, (input, signature)) in psbt
1319            .inputs
1320            .iter_mut()
1321            .zip(signature.signature.iter())
1322            .enumerate()
1323        {
1324            let tx_hash = tx_hasher
1325                .p2wsh_signature_hash(
1326                    idx,
1327                    input
1328                        .witness_script
1329                        .as_ref()
1330                        .expect("Missing witness script"),
1331                    input.witness_utxo.as_ref().expect("Missing UTXO").value,
1332                    EcdsaSighashType::All,
1333                )
1334                .map_err(|_| ProcessPegOutSigError::SighashError)?;
1335
1336            let tweak = input
1337                .proprietary
1338                .get(&proprietary_tweak_key())
1339                .expect("we saved it with a tweak");
1340
1341            let tweaked_peer_key = peer_key.tweak(tweak, &self.secp);
1342            self.secp
1343                .verify_ecdsa(
1344                    &Message::from_digest_slice(&tx_hash[..]).unwrap(),
1345                    signature,
1346                    &tweaked_peer_key.key,
1347                )
1348                .map_err(|_| ProcessPegOutSigError::InvalidSignature)?;
1349
1350            if input
1351                .partial_sigs
1352                .insert(tweaked_peer_key.into(), EcdsaSig::sighash_all(*signature))
1353                .is_some()
1354            {
1355                // Should never happen since peers only sign a PSBT once
1356                return Err(ProcessPegOutSigError::DuplicateSignature);
1357            }
1358        }
1359        Ok(())
1360    }
1361
1362    fn finalize_peg_out_psbt(
1363        &self,
1364        mut unsigned: UnsignedTransaction,
1365    ) -> Result<PendingTransaction, ProcessPegOutSigError> {
1366        // We need to save the change output's tweak key to be able to access the funds
1367        // later on. The tweak is extracted here because the psbt is moved next
1368        // and not available anymore when the tweak is actually needed in the
1369        // end to be put into the batch on success.
1370        let change_tweak: [u8; 33] = unsigned
1371            .psbt
1372            .outputs
1373            .iter()
1374            .find_map(|output| output.proprietary.get(&proprietary_tweak_key()).cloned())
1375            .ok_or(ProcessPegOutSigError::MissingOrMalformedChangeTweak)?
1376            .try_into()
1377            .map_err(|_| ProcessPegOutSigError::MissingOrMalformedChangeTweak)?;
1378
1379        if let Err(error) = unsigned.psbt.finalize_mut(&self.secp) {
1380            return Err(ProcessPegOutSigError::ErrorFinalizingPsbt(error));
1381        }
1382
1383        let tx = unsigned.psbt.clone().extract_tx_unchecked_fee_rate();
1384
1385        Ok(PendingTransaction {
1386            tx,
1387            tweak: change_tweak,
1388            change: unsigned.change,
1389            destination: unsigned.destination,
1390            fees: unsigned.fees,
1391            selected_utxos: unsigned.selected_utxos,
1392            peg_out_amount: unsigned.peg_out_amount,
1393            rbf: unsigned.rbf,
1394        })
1395    }
1396
1397    fn get_block_count(&self) -> anyhow::Result<u32> {
1398        self.btc_rpc
1399            .status()
1400            .context("No bitcoin rpc connection")
1401            .and_then(|status| {
1402                status
1403                    .block_count
1404                    .try_into()
1405                    .map_err(|_| format_err!("Block count exceeds u32 limits"))
1406            })
1407    }
1408
1409    pub fn get_fee_rate_opt(&self) -> Feerate {
1410        // `get_feerate_multiplier` is clamped and can't be negative
1411        // feerate sources as clamped and can't be negative or too large
1412        #[allow(clippy::cast_precision_loss)]
1413        #[allow(clippy::cast_sign_loss)]
1414        Feerate {
1415            sats_per_kvb: ((self
1416                .btc_rpc
1417                .status()
1418                .and_then(|status| status.fee_rate)
1419                .unwrap_or(self.cfg.consensus.default_fee)
1420                .sats_per_kvb as f64
1421                * get_feerate_multiplier())
1422            .round()) as u64,
1423        }
1424    }
1425
1426    pub async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u32 {
1427        let peer_count = self.cfg.consensus.peer_peg_in_keys.to_num_peers().total();
1428
1429        let mut counts = dbtx
1430            .find_by_prefix(&BlockCountVotePrefix)
1431            .await
1432            .map(|entry| entry.1)
1433            .collect::<Vec<u32>>()
1434            .await;
1435
1436        assert!(counts.len() <= peer_count);
1437
1438        while counts.len() < peer_count {
1439            counts.push(0);
1440        }
1441
1442        counts.sort_unstable();
1443
1444        counts[peer_count / 2]
1445    }
1446
1447    pub async fn consensus_fee_rate(&self, dbtx: &mut DatabaseTransaction<'_>) -> Feerate {
1448        let peer_count = self.cfg.consensus.peer_peg_in_keys.to_num_peers().total();
1449
1450        let mut rates = dbtx
1451            .find_by_prefix(&FeeRateVotePrefix)
1452            .await
1453            .map(|(.., rate)| rate)
1454            .collect::<Vec<_>>()
1455            .await;
1456
1457        assert!(rates.len() <= peer_count);
1458
1459        while rates.len() < peer_count {
1460            rates.push(self.cfg.consensus.default_fee);
1461        }
1462
1463        rates.sort_unstable();
1464
1465        rates[peer_count / 2]
1466    }
1467
1468    async fn consensus_module_consensus_version(
1469        &self,
1470        dbtx: &mut DatabaseTransaction<'_>,
1471    ) -> ModuleConsensusVersion {
1472        let num_peers = self.cfg.consensus.peer_peg_in_keys.to_num_peers();
1473
1474        let mut versions = dbtx
1475            .find_by_prefix(&ConsensusVersionVotePrefix)
1476            .await
1477            .map(|entry| entry.1)
1478            .collect::<Vec<ModuleConsensusVersion>>()
1479            .await;
1480
1481        while versions.len() < num_peers.total() {
1482            versions.push(ModuleConsensusVersion::new(2, 0));
1483        }
1484
1485        assert_eq!(versions.len(), num_peers.total());
1486
1487        versions.sort_unstable();
1488
1489        assert!(versions.first() <= versions.last());
1490
1491        versions[num_peers.max_evil()]
1492    }
1493
1494    pub async fn consensus_nonce(&self, dbtx: &mut DatabaseTransaction<'_>) -> [u8; 33] {
1495        let nonce_idx = dbtx.get_value(&PegOutNonceKey).await.unwrap_or(0);
1496        dbtx.insert_entry(&PegOutNonceKey, &(nonce_idx + 1)).await;
1497
1498        nonce_from_idx(nonce_idx)
1499    }
1500
1501    async fn sync_up_to_consensus_count(
1502        &self,
1503        dbtx: &mut DatabaseTransaction<'_>,
1504        old_count: u32,
1505        new_count: u32,
1506    ) {
1507        let sync_start = fedimint_core::time::now();
1508        info!(
1509            target: LOG_MODULE_WALLET,
1510            old_count,
1511            new_count,
1512            blocks_to_go = new_count
1513                .checked_sub(old_count)
1514                .expect("new_count must be >= old_count"),
1515            "New block count consensus, initiating sync",
1516        );
1517
1518        // Before we can safely call our bitcoin backend to process the new consensus
1519        // count, we need to ensure we observed enough confirmations
1520        self.wait_for_finality_confs_or_shutdown(new_count).await;
1521
1522        for height in old_count..new_count {
1523            let block_start = fedimint_core::time::now();
1524            info!(
1525                target: LOG_MODULE_WALLET,
1526                height,
1527                "Processing block of height {height}",
1528            );
1529
1530            // TODO: use batching for mainnet syncing
1531            trace!(block = height, "Fetching block hash");
1532            let block_hash = retry("get_block_hash", backoff_util::background_backoff(), || {
1533                self.btc_rpc.get_block_hash(u64::from(height)) // TODO: use u64 for height everywhere
1534            })
1535            .await
1536            .expect("bitcoind rpc to get block hash");
1537
1538            let block = retry("get_block", backoff_util::background_backoff(), || {
1539                self.btc_rpc.get_block(&block_hash)
1540            })
1541            .await
1542            .expect("bitcoind rpc to get block");
1543
1544            if let Some(prev_block_height) = height.checked_sub(1) {
1545                if let Some(hash) = dbtx
1546                    .get_value(&BlockHashByHeightKey(prev_block_height))
1547                    .await
1548                {
1549                    assert_eq!(block.header.prev_blockhash, hash.0);
1550                } else {
1551                    warn!(
1552                        target: LOG_MODULE_WALLET,
1553                        %height,
1554                        %block_hash,
1555                        %prev_block_height,
1556                        prev_blockhash = %block.header.prev_blockhash,
1557                        "Missing previous block hash. This should only happen on the first processed block height."
1558                    );
1559                }
1560            }
1561
1562            if self.consensus_module_consensus_version(dbtx).await
1563                >= ModuleConsensusVersion::new(2, 2)
1564            {
1565                for transaction in &block.txdata {
1566                    // We maintain the subset of unspent P2WSH transaction outputs created
1567                    // since the module was running on the new consensus version, which might be
1568                    // the same time as the genesis session.
1569
1570                    for tx_in in &transaction.input {
1571                        dbtx.remove_entry(&UnspentTxOutKey(tx_in.previous_output))
1572                            .await;
1573                    }
1574
1575                    for (vout, tx_out) in transaction.output.iter().enumerate() {
1576                        let should_track_utxo = if self.cfg.consensus.peer_peg_in_keys.len() > 1 {
1577                            tx_out.script_pubkey.is_p2wsh()
1578                        } else {
1579                            tx_out.script_pubkey.is_p2wpkh()
1580                        };
1581
1582                        if should_track_utxo {
1583                            let outpoint = bitcoin::OutPoint {
1584                                txid: transaction.compute_txid(),
1585                                vout: vout as u32,
1586                            };
1587
1588                            dbtx.insert_new_entry(&UnspentTxOutKey(outpoint), tx_out)
1589                                .await;
1590                        }
1591                    }
1592                }
1593            }
1594
1595            let pending_transactions = dbtx
1596                .find_by_prefix(&PendingTransactionPrefixKey)
1597                .await
1598                .map(|(key, transaction)| (key.0, transaction))
1599                .collect::<BTreeMap<Txid, PendingTransaction>>()
1600                .await;
1601            let pending_transactions_len = pending_transactions.len();
1602
1603            debug!(
1604                target: LOG_MODULE_WALLET,
1605                ?height,
1606                ?pending_transactions_len,
1607                "Recognizing change UTXOs"
1608            );
1609            for (txid, tx) in &pending_transactions {
1610                let is_tx_in_block = block.txdata.iter().any(|tx| tx.compute_txid() == *txid);
1611
1612                if is_tx_in_block {
1613                    debug!(
1614                        target: LOG_MODULE_WALLET,
1615                        ?txid, ?height, ?block_hash, "Recognizing change UTXO"
1616                    );
1617                    self.recognize_change_utxo(dbtx, tx).await;
1618                } else {
1619                    debug!(
1620                        target: LOG_MODULE_WALLET,
1621                        ?txid,
1622                        ?height,
1623                        ?block_hash,
1624                        "Pending transaction not yet confirmed in this block"
1625                    );
1626                }
1627            }
1628
1629            dbtx.insert_new_entry(&BlockHashKey(block_hash), &()).await;
1630            dbtx.insert_new_entry(
1631                &BlockHashByHeightKey(height),
1632                &BlockHashByHeightValue(block_hash),
1633            )
1634            .await;
1635
1636            info!(
1637                target: LOG_MODULE_WALLET,
1638                height,
1639                ?block_hash,
1640                duration = ?block_start.elapsed().unwrap_or_default(),
1641                "Successfully processed block of height {height}",
1642            );
1643        }
1644
1645        info!(
1646            target: LOG_MODULE_WALLET,
1647            old_count,
1648            new_count,
1649            blocks_processed = new_count
1650                .checked_sub(old_count)
1651                .expect("new_count must be >= old_count"),
1652            duration = ?sync_start.elapsed().unwrap_or_default(),
1653            "Block count consensus sync complete",
1654        );
1655    }
1656
1657    /// Add a change UTXO to our spendable UTXO database after it was included
1658    /// in a block that we got consensus on.
1659    async fn recognize_change_utxo(
1660        &self,
1661        dbtx: &mut DatabaseTransaction<'_>,
1662        pending_tx: &PendingTransaction,
1663    ) {
1664        self.remove_rbf_transactions(dbtx, pending_tx).await;
1665
1666        let script_pk = self
1667            .cfg
1668            .consensus
1669            .peg_in_descriptor
1670            .tweak(&pending_tx.tweak, &self.secp)
1671            .script_pubkey();
1672        for (idx, output) in pending_tx.tx.output.iter().enumerate() {
1673            if output.script_pubkey == script_pk {
1674                dbtx.insert_entry(
1675                    &UTXOKey(bitcoin::OutPoint {
1676                        txid: pending_tx.tx.compute_txid(),
1677                        vout: idx as u32,
1678                    }),
1679                    &SpendableUTXO {
1680                        tweak: pending_tx.tweak,
1681                        amount: output.value,
1682                    },
1683                )
1684                .await;
1685            }
1686        }
1687    }
1688
1689    /// Removes the `PendingTransaction` and any transactions tied to it via RBF
1690    async fn remove_rbf_transactions(
1691        &self,
1692        dbtx: &mut DatabaseTransaction<'_>,
1693        pending_tx: &PendingTransaction,
1694    ) {
1695        let mut all_transactions: BTreeMap<Txid, PendingTransaction> = dbtx
1696            .find_by_prefix(&PendingTransactionPrefixKey)
1697            .await
1698            .map(|(key, val)| (key.0, val))
1699            .collect::<BTreeMap<Txid, PendingTransaction>>()
1700            .await;
1701
1702        // We need to search and remove all `PendingTransactions` invalidated by RBF
1703        let mut pending_to_remove = vec![pending_tx.clone()];
1704        while let Some(removed) = pending_to_remove.pop() {
1705            all_transactions.remove(&removed.tx.compute_txid());
1706            dbtx.remove_entry(&PendingTransactionKey(removed.tx.compute_txid()))
1707                .await;
1708
1709            // Search for tx that this `removed` has as RBF
1710            if let Some(rbf) = &removed.rbf
1711                && let Some(tx) = all_transactions.get(&rbf.txid)
1712            {
1713                pending_to_remove.push(tx.clone());
1714            }
1715
1716            // Search for tx that wanted to RBF the `removed` one
1717            for tx in all_transactions.values() {
1718                if let Some(rbf) = &tx.rbf
1719                    && rbf.txid == removed.tx.compute_txid()
1720                {
1721                    pending_to_remove.push(tx.clone());
1722                }
1723            }
1724        }
1725    }
1726
1727    async fn block_is_known(
1728        &self,
1729        dbtx: &mut DatabaseTransaction<'_>,
1730        block_hash: BlockHash,
1731    ) -> bool {
1732        dbtx.get_value(&BlockHashKey(block_hash)).await.is_some()
1733    }
1734
1735    async fn create_peg_out_tx(
1736        &self,
1737        dbtx: &mut DatabaseTransaction<'_>,
1738        output: &WalletOutputV0,
1739        change_tweak: &[u8; 33],
1740    ) -> Result<UnsignedTransaction, WalletOutputError> {
1741        let fee_arithmetic = if CHECKED_PEG_OUT_FEE_MODULE_CONSENSUS_VERSION
1742            <= self.consensus_module_consensus_version(dbtx).await
1743        {
1744            FeeArithmetic::Checked
1745        } else {
1746            FeeArithmetic::Wrapping
1747        };
1748
1749        match output {
1750            WalletOutputV0::PegOut(peg_out) => self.offline_wallet().create_tx(
1751                peg_out.amount,
1752                // Note: While calling `assume_checked()` is generally unwise, checking the
1753                // network here could be a consensus-breaking change. Ignoring the network
1754                // is fine here since we validate it in `process_output()`.
1755                peg_out.recipient.clone().assume_checked().script_pubkey(),
1756                vec![],
1757                self.available_utxos(dbtx).await,
1758                peg_out.fees.fee_rate,
1759                change_tweak,
1760                None,
1761                fee_arithmetic,
1762            ),
1763            WalletOutputV0::Rbf(rbf) => {
1764                let tx = dbtx
1765                    .get_value(&PendingTransactionKey(rbf.txid))
1766                    .await
1767                    .ok_or(WalletOutputError::RbfTransactionIdNotFound)?;
1768
1769                self.offline_wallet().create_tx(
1770                    tx.peg_out_amount,
1771                    tx.destination,
1772                    tx.selected_utxos,
1773                    self.available_utxos(dbtx).await,
1774                    tx.fees.fee_rate,
1775                    change_tweak,
1776                    Some(rbf.clone()),
1777                    fee_arithmetic,
1778                )
1779            }
1780        }
1781    }
1782
1783    async fn available_utxos(
1784        &self,
1785        dbtx: &mut DatabaseTransaction<'_>,
1786    ) -> Vec<(UTXOKey, SpendableUTXO)> {
1787        dbtx.find_by_prefix(&UTXOPrefixKey)
1788            .await
1789            .collect::<Vec<(UTXOKey, SpendableUTXO)>>()
1790            .await
1791    }
1792
1793    pub async fn get_wallet_value(&self, dbtx: &mut DatabaseTransaction<'_>) -> bitcoin::Amount {
1794        let sat_sum = self
1795            .available_utxos(dbtx)
1796            .await
1797            .into_iter()
1798            .map(|(_, utxo)| utxo.amount.to_sat())
1799            .sum();
1800        bitcoin::Amount::from_sat(sat_sum)
1801    }
1802
1803    async fn get_wallet_summary(&self, dbtx: &mut DatabaseTransaction<'_>) -> WalletSummary {
1804        fn partition_peg_out_and_change(
1805            transactions: Vec<Transaction>,
1806        ) -> (Vec<TxOutputSummary>, Vec<TxOutputSummary>) {
1807            let mut peg_out_txos: Vec<TxOutputSummary> = Vec::new();
1808            let mut change_utxos: Vec<TxOutputSummary> = Vec::new();
1809
1810            for tx in transactions {
1811                let txid = tx.compute_txid();
1812
1813                // to identify outputs for the peg_out (idx = 0) and change (idx = 1), we lean
1814                // on how the wallet constructs the transaction
1815                let peg_out_output = tx
1816                    .output
1817                    .first()
1818                    .expect("tx must contain withdrawal output");
1819
1820                let change_output = tx.output.last().expect("tx must contain change output");
1821
1822                peg_out_txos.push(TxOutputSummary {
1823                    outpoint: bitcoin::OutPoint { txid, vout: 0 },
1824                    amount: peg_out_output.value,
1825                });
1826
1827                change_utxos.push(TxOutputSummary {
1828                    outpoint: bitcoin::OutPoint { txid, vout: 1 },
1829                    amount: change_output.value,
1830                });
1831            }
1832
1833            (peg_out_txos, change_utxos)
1834        }
1835
1836        let spendable_utxos = self
1837            .available_utxos(dbtx)
1838            .await
1839            .iter()
1840            .map(|(utxo_key, spendable_utxo)| TxOutputSummary {
1841                outpoint: utxo_key.0,
1842                amount: spendable_utxo.amount,
1843            })
1844            .collect::<Vec<_>>();
1845
1846        // constructed peg-outs without threshold signatures
1847        let unsigned_transactions = dbtx
1848            .find_by_prefix(&UnsignedTransactionPrefixKey)
1849            .await
1850            .map(|(_tx_key, tx)| tx.psbt.unsigned_tx)
1851            .collect::<Vec<_>>()
1852            .await;
1853
1854        // peg-outs with threshold signatures, awaiting finality delay confirmations
1855        let unconfirmed_transactions = dbtx
1856            .find_by_prefix(&PendingTransactionPrefixKey)
1857            .await
1858            .map(|(_tx_key, tx)| tx.tx)
1859            .collect::<Vec<_>>()
1860            .await;
1861
1862        let (unsigned_peg_out_txos, unsigned_change_utxos) =
1863            partition_peg_out_and_change(unsigned_transactions);
1864
1865        let (unconfirmed_peg_out_txos, unconfirmed_change_utxos) =
1866            partition_peg_out_and_change(unconfirmed_transactions);
1867
1868        WalletSummary {
1869            spendable_utxos,
1870            unsigned_peg_out_txos,
1871            unsigned_change_utxos,
1872            unconfirmed_peg_out_txos,
1873            unconfirmed_change_utxos,
1874        }
1875    }
1876
1877    async fn is_utxo_confirmed(
1878        &self,
1879        dbtx: &mut DatabaseTransaction<'_>,
1880        outpoint: bitcoin::OutPoint,
1881    ) -> bool {
1882        dbtx.get_value(&UnspentTxOutKey(outpoint)).await.is_some()
1883    }
1884
1885    fn offline_wallet(&'_ self) -> StatelessWallet<'_> {
1886        StatelessWallet {
1887            descriptor: &self.cfg.consensus.peg_in_descriptor,
1888            secret_key: &self.cfg.private.peg_in_key,
1889            secp: &self.secp,
1890        }
1891    }
1892
1893    fn spawn_broadcast_pending_task(
1894        task_group: &TaskGroup,
1895        server_bitcoin_rpc_monitor: &ServerBitcoinRpcMonitor,
1896        db: &Database,
1897        broadcast_pending_notify: Arc<Notify>,
1898    ) {
1899        task_group.spawn_cancellable("broadcast pending", {
1900            let btc_rpc = server_bitcoin_rpc_monitor.clone();
1901            let db = db.clone();
1902            run_broadcast_pending_tx(db, btc_rpc, broadcast_pending_notify)
1903        });
1904    }
1905
1906    /// Get the bitcoin network for UI display
1907    pub fn network_ui(&self) -> Network {
1908        self.cfg.consensus.network.0
1909    }
1910
1911    /// Get the current consensus block count for UI display
1912    pub async fn consensus_block_count_ui(&self) -> u32 {
1913        self.consensus_block_count(&mut self.db.begin_transaction_nc().await)
1914            .await
1915    }
1916
1917    /// Get the current consensus fee rate for UI display
1918    pub async fn consensus_feerate_ui(&self) -> Feerate {
1919        self.consensus_fee_rate(&mut self.db.begin_transaction_nc().await)
1920            .await
1921    }
1922
1923    /// Get the current wallet summary for UI display
1924    pub async fn get_wallet_summary_ui(&self) -> WalletSummary {
1925        self.get_wallet_summary(&mut self.db.begin_transaction_nc().await)
1926            .await
1927    }
1928
1929    /// Shutdown the task group shared throughout fedimintd, giving 60 seconds
1930    /// for other services to gracefully shutdown.
1931    async fn graceful_shutdown(&self) {
1932        if let Err(e) = self
1933            .task_group
1934            .clone()
1935            .shutdown_join_all(Some(Duration::from_mins(1)))
1936            .await
1937        {
1938            panic!("Error while shutting down fedimintd task group: {e}");
1939        }
1940    }
1941
1942    /// Returns once our bitcoin backend observes finality delay confirmations
1943    /// of the consensus block count. If we don't observe enough confirmations
1944    /// after one hour, we gracefully shutdown fedimintd. This is necessary
1945    /// since we can no longer participate in consensus if our bitcoin backend
1946    /// is unable to observe the same chain tip as our peers.
1947    async fn wait_for_finality_confs_or_shutdown(&self, consensus_block_count: u32) {
1948        let backoff = if is_running_in_test_env() {
1949            // every 100ms for 60s
1950            backoff_util::custom_backoff(
1951                Duration::from_millis(100),
1952                Duration::from_millis(100),
1953                Some(10 * 60),
1954            )
1955        } else {
1956            // every max 10s for 1 hour
1957            backoff_util::fibonacci_max_one_hour()
1958        };
1959
1960        let wait_for_finality_confs = || async {
1961            let our_chain_tip_block_count = self.get_block_count()?;
1962            let consensus_chain_tip_block_count =
1963                consensus_block_count + self.cfg.consensus.finality_delay;
1964
1965            if consensus_chain_tip_block_count <= our_chain_tip_block_count {
1966                Ok(())
1967            } else {
1968                Err(anyhow::anyhow!("not enough confirmations"))
1969            }
1970        };
1971
1972        if retry("wait_for_finality_confs", backoff, wait_for_finality_confs)
1973            .await
1974            .is_err()
1975        {
1976            self.graceful_shutdown().await;
1977        }
1978    }
1979
1980    fn spawn_peer_supported_consensus_version_task(
1981        api_client: DynModuleApi,
1982        task_group: &TaskGroup,
1983        our_peer_id: PeerId,
1984    ) -> watch::Receiver<Option<ModuleConsensusVersion>> {
1985        let (sender, receiver) = watch::channel(None);
1986        task_group.spawn_cancellable("fetch-peer-consensus-versions", async move {
1987            loop {
1988                let request_futures = api_client.all_peers().iter().filter_map(|&peer| {
1989                    if peer == our_peer_id {
1990                        return None;
1991                    }
1992
1993                    let api_client_inner = api_client.clone();
1994                    Some(async move {
1995                        api_client_inner
1996                            .request_single_peer::<ModuleConsensusVersion>(
1997                                SUPPORTED_MODULE_CONSENSUS_VERSION_ENDPOINT.to_owned(),
1998                                ApiRequestErased::default(),
1999                                peer,
2000                            )
2001                            .await
2002                            .inspect(|res| debug!(
2003                                target: LOG_MODULE_WALLET,
2004                                %peer,
2005                                %our_peer_id,
2006                                ?res,
2007                                "Fetched supported module consensus version from peer"
2008                            ))
2009                            .inspect_err(|err| warn!(
2010                                target: LOG_MODULE_WALLET,
2011                                 %peer,
2012                                 err=%err.fmt_compact(),
2013                                "Failed to fetch consensus version from peer"
2014                            ))
2015                            .ok()
2016                    })
2017                });
2018
2019                let peer_consensus_versions = join_all(request_futures)
2020                    .await
2021                    .into_iter()
2022                    .flatten()
2023                    .collect::<Vec<_>>();
2024
2025                let sorted_consensus_versions = peer_consensus_versions
2026                    .into_iter()
2027                    .chain(std::iter::once(MODULE_CONSENSUS_VERSION))
2028                    .sorted()
2029                    .collect::<Vec<_>>();
2030                let all_peers_supported_version =
2031                    if sorted_consensus_versions.len() == api_client.all_peers().len() {
2032                        let min_supported_version = *sorted_consensus_versions
2033                            .first()
2034                            .expect("at least one element");
2035
2036                        debug!(
2037                            target: LOG_MODULE_WALLET,
2038                            ?sorted_consensus_versions,
2039                            "Fetched supported consensus versions from peers"
2040                        );
2041
2042                        Some(min_supported_version)
2043                    } else {
2044                        assert!(
2045                            sorted_consensus_versions.len() <= api_client.all_peers().len(),
2046                            "Too many peer responses",
2047                        );
2048                        trace!(
2049                            target: LOG_MODULE_WALLET,
2050                            ?sorted_consensus_versions,
2051                            "Not all peers have reported their consensus version yet"
2052                        );
2053                        None
2054                    };
2055
2056                #[allow(clippy::disallowed_methods)]
2057                if sender.send(all_peers_supported_version).is_err() {
2058                    warn!(target: LOG_MODULE_WALLET, "Failed to send consensus version to watch channel, stopping task");
2059                    break;
2060                }
2061
2062                sleep(next_poll_delay(all_peers_supported_version.is_some())).await;
2063            }
2064        });
2065        receiver
2066    }
2067}
2068
2069#[instrument(target = LOG_MODULE_WALLET, level = "debug", skip_all)]
2070pub async fn run_broadcast_pending_tx(
2071    db: Database,
2072    rpc: ServerBitcoinRpcMonitor,
2073    broadcast: Arc<Notify>,
2074) {
2075    loop {
2076        // Unless something new happened, we broadcast once a minute
2077        let _ = tokio::time::timeout(Duration::from_mins(1), broadcast.notified()).await;
2078        broadcast_pending_tx(db.begin_transaction_nc().await, &rpc).await;
2079    }
2080}
2081
2082pub async fn broadcast_pending_tx(
2083    mut dbtx: DatabaseTransaction<'_>,
2084    rpc: &ServerBitcoinRpcMonitor,
2085) {
2086    let pending_tx: Vec<PendingTransaction> = dbtx
2087        .find_by_prefix(&PendingTransactionPrefixKey)
2088        .await
2089        .map(|(_, val)| val)
2090        .collect::<Vec<_>>()
2091        .await;
2092    let rbf_txids: BTreeSet<Txid> = pending_tx
2093        .iter()
2094        .filter_map(|tx| tx.rbf.clone().map(|rbf| rbf.txid))
2095        .collect();
2096    if !pending_tx.is_empty() {
2097        debug!(
2098            target: LOG_MODULE_WALLET,
2099            "Broadcasting pending transactions (total={}, rbf={})",
2100            pending_tx.len(),
2101            rbf_txids.len()
2102        );
2103    }
2104
2105    for PendingTransaction { tx, .. } in pending_tx {
2106        if !rbf_txids.contains(&tx.compute_txid()) {
2107            debug!(
2108                target: LOG_MODULE_WALLET,
2109                tx = %tx.compute_txid(),
2110                weight = tx.weight().to_wu(),
2111                output = ?tx.output,
2112                "Broadcasting peg-out",
2113            );
2114            trace!(transaction = ?tx);
2115            if let Err(err) = rpc.submit_transaction(tx).await {
2116                debug!(
2117                    target: LOG_MODULE_WALLET,
2118                    err = %err.fmt_compact_anyhow(),
2119                    "Error broadcasting peg-out transaction"
2120                );
2121            }
2122        }
2123    }
2124}
2125
2126/// Whether peg-out fee computation rejects a rate that cannot produce a fee
2127/// which exists on chain.
2128///
2129/// Pre-2.3 sessions accepted such peg-outs -- the multiplication wrapped in
2130/// release profiles -- so replaying them has to reproduce that, and the gate
2131/// cannot simply be removed once every federation has upgraded.
2132#[derive(Debug, Clone, Copy, Eq, PartialEq)]
2133pub enum FeeArithmetic {
2134    /// Pre-2.3: multiply unchecked, as historic sessions did.
2135    Wrapping,
2136    /// From 2.3 on: reject a rate whose fee cannot exist on chain.
2137    Checked,
2138}
2139
2140impl FeeArithmetic {
2141    fn calculate_fee(
2142        self,
2143        fee_rate: Feerate,
2144        weight: u64,
2145    ) -> Result<bitcoin::Amount, WalletOutputError> {
2146        match self {
2147            FeeArithmetic::Wrapping => Ok(fee_rate.wrapping_calculate_fee(weight)),
2148            FeeArithmetic::Checked => fee_rate
2149                .checked_calculate_fee(weight)
2150                .ok_or(WalletOutputError::NotEnoughSpendableUTXO),
2151        }
2152    }
2153}
2154
2155struct StatelessWallet<'a> {
2156    descriptor: &'a Descriptor<CompressedPublicKey>,
2157    secret_key: &'a secp256k1::SecretKey,
2158    secp: &'a secp256k1::Secp256k1<secp256k1::All>,
2159}
2160
2161impl StatelessWallet<'_> {
2162    /// Given a tx created from an `WalletOutput`, validate there will be no
2163    /// issues submitting the transaction to the Bitcoin network
2164    fn validate_tx(
2165        tx: &UnsignedTransaction,
2166        output: &WalletOutputV0,
2167        consensus_fee_rate: Feerate,
2168        network: Network,
2169    ) -> Result<(), WalletOutputError> {
2170        if let WalletOutputV0::PegOut(peg_out) = output
2171            && !peg_out.recipient.is_valid_for_network(network)
2172        {
2173            return Err(WalletOutputError::WrongNetwork(
2174                NetworkLegacyEncodingWrapper(network),
2175                NetworkLegacyEncodingWrapper(get_network_for_address(&peg_out.recipient)),
2176            ));
2177        }
2178
2179        // Validate the tx amount is over the dust limit
2180        if tx.peg_out_amount < tx.destination.minimal_non_dust() {
2181            return Err(WalletOutputError::PegOutUnderDustLimit);
2182        }
2183
2184        // Validate tx fee rate is above the consensus fee rate
2185        if tx.fees.fee_rate < consensus_fee_rate {
2186            return Err(WalletOutputError::PegOutFeeBelowConsensus(
2187                tx.fees.fee_rate,
2188                consensus_fee_rate,
2189            ));
2190        }
2191
2192        // Validate added fees are above the min relay tx fee
2193        // BIP-0125 requires 1 sat/vb for RBF by default (same as normal txs)
2194        let fees = match output {
2195            WalletOutputV0::PegOut(pegout) => pegout.fees,
2196            WalletOutputV0::Rbf(rbf) => rbf.fees,
2197        };
2198        if fees.fee_rate.sats_per_kvb < u64::from(DEFAULT_MIN_RELAY_TX_FEE) {
2199            return Err(WalletOutputError::BelowMinRelayFee);
2200        }
2201
2202        // Validate fees weight matches the actual weight
2203        if fees.total_weight != tx.fees.total_weight {
2204            return Err(WalletOutputError::TxWeightIncorrect(
2205                fees.total_weight,
2206                tx.fees.total_weight,
2207            ));
2208        }
2209
2210        Ok(())
2211    }
2212
2213    /// Attempts to create a tx ready to be signed from available UTXOs.
2214    //
2215    // * `peg_out_amount`: How much the peg-out should be
2216    // * `destination`: The address the user is pegging-out to
2217    // * `included_utxos`: UXTOs that must be included (for RBF)
2218    // * `remaining_utxos`: All other spendable UXTOs
2219    // * `fee_rate`: How much needs to be spent on fees
2220    // * `change_tweak`: How the federation can recognize it's change UTXO
2221    // * `rbf`: If this is an RBF transaction
2222    #[allow(clippy::too_many_arguments)]
2223    fn create_tx(
2224        &self,
2225        peg_out_amount: bitcoin::Amount,
2226        destination: ScriptBuf,
2227        mut included_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2228        mut remaining_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2229        mut fee_rate: Feerate,
2230        change_tweak: &[u8; 33],
2231        rbf: Option<Rbf>,
2232        fee_arithmetic: FeeArithmetic,
2233    ) -> Result<UnsignedTransaction, WalletOutputError> {
2234        // `peg_out_amount` arrives unvalidated from whoever asked for the peg-out,
2235        // and `bitcoin::Amount` arithmetic panics on overflow. An amount that
2236        // cannot exist on chain can never be funded by our UTXOs anyway, so reject
2237        // it before doing any arithmetic with it.
2238        if peg_out_amount > bitcoin::Amount::MAX_MONEY {
2239            return Err(WalletOutputError::NotEnoughSpendableUTXO);
2240        }
2241
2242        // Add the rbf fees to the existing tx fees
2243        if let Some(rbf) = &rbf {
2244            fee_rate.sats_per_kvb = fee_rate
2245                .sats_per_kvb
2246                .saturating_add(rbf.fees.fee_rate.sats_per_kvb);
2247        }
2248
2249        // When building a transaction we need to take care of two things:
2250        //  * We need enough input amount to fund all outputs
2251        //  * We need to keep an eye on the tx weight so we can factor the fees into out
2252        //    calculation
2253        // We then go on to calculate the base size of the transaction `total_weight`
2254        // and the maximum weight per added input which we will add every time
2255        // we select an input.
2256        let change_script = self.derive_script(change_tweak);
2257        let out_weight = (destination.len() * 4 + 1 + 32
2258            // Add change script weight, it's very likely to be needed if not we just overpay in fees
2259            + 1 // script len varint, 1 byte for all addresses we accept
2260            + change_script.len() * 4 // script len
2261            + 32) as u64; // value
2262        let mut total_weight = 16 + // version
2263            12 + // up to 2**16-1 inputs
2264            12 + // up to 2**16-1 outputs
2265            out_weight + // weight of all outputs
2266            16; // lock time
2267        // https://github.com/fedimint/fedimint/issues/4590
2268        #[allow(deprecated)]
2269        let max_input_weight = (self
2270            .descriptor
2271            .max_satisfaction_weight()
2272            .expect("is satisfyable") +
2273            128 + // TxOutHash
2274            16 + // TxOutIndex
2275            16) as u64; // sequence
2276
2277        // Ensure deterministic ordering of UTXOs for all peers
2278        included_utxos.sort_by_key(|(_, utxo)| utxo.amount);
2279        remaining_utxos.sort_by_key(|(_, utxo)| utxo.amount);
2280        included_utxos.extend(remaining_utxos);
2281
2282        // Finally we initialize our accumulator for selected input amounts
2283        let mut total_selected_value = bitcoin::Amount::from_sat(0);
2284        let mut selected_utxos: Vec<(UTXOKey, SpendableUTXO)> = vec![];
2285        let mut fees = fee_arithmetic.calculate_fee(fee_rate, total_weight)?;
2286
2287        loop {
2288            // Fees grow with every selected input and are not bounded by `MAX_MONEY`,
2289            // so the target has to be recomputed with checked arithmetic on every
2290            // iteration. A target we cannot even represent is by definition more than
2291            // our UTXOs can cover.
2292            let target = peg_out_amount
2293                .checked_add(change_script.minimal_non_dust())
2294                .and_then(|target| target.checked_add(fees))
2295                .ok_or(WalletOutputError::NotEnoughSpendableUTXO)?;
2296
2297            if total_selected_value >= target {
2298                break;
2299            }
2300
2301            let Some((utxo_key, utxo)) = included_utxos.pop() else {
2302                return Err(WalletOutputError::NotEnoughSpendableUTXO); // Not enough UTXOs
2303            };
2304
2305            total_selected_value += utxo.amount;
2306            total_weight += max_input_weight;
2307            fees = fee_arithmetic.calculate_fee(fee_rate, total_weight)?;
2308            selected_utxos.push((utxo_key, utxo));
2309        }
2310
2311        // We always pay ourselves change back to ensure that we don't lose anything due
2312        // to dust. The change output is always at index `PEG_OUT_CHANGE_VOUT`.
2313        let change = total_selected_value - fees - peg_out_amount;
2314        let output: Vec<TxOut> = vec![
2315            TxOut {
2316                value: peg_out_amount,
2317                script_pubkey: destination.clone(),
2318            },
2319            TxOut {
2320                value: change,
2321                script_pubkey: change_script,
2322            },
2323        ];
2324        let mut change_out = bitcoin::psbt::Output::default();
2325        change_out
2326            .proprietary
2327            .insert(proprietary_tweak_key(), change_tweak.to_vec());
2328
2329        info!(
2330            target: LOG_MODULE_WALLET,
2331            inputs = selected_utxos.len(),
2332            input_sats = total_selected_value.to_sat(),
2333            peg_out_sats = peg_out_amount.to_sat(),
2334            ?total_weight,
2335            fees_sats = fees.to_sat(),
2336            fee_rate = fee_rate.sats_per_kvb,
2337            change_sats = change.to_sat(),
2338            "Creating peg-out tx",
2339        );
2340
2341        let transaction = Transaction {
2342            version: bitcoin::transaction::Version(2),
2343            lock_time: LockTime::ZERO,
2344            input: selected_utxos
2345                .iter()
2346                .map(|(utxo_key, _utxo)| TxIn {
2347                    previous_output: utxo_key.0,
2348                    script_sig: Default::default(),
2349                    sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
2350                    witness: bitcoin::Witness::new(),
2351                })
2352                .collect(),
2353            output,
2354        };
2355        info!(
2356            target: LOG_MODULE_WALLET,
2357            txid = %transaction.compute_txid(), "Creating peg-out tx"
2358        );
2359
2360        // FIXME: use custom data structure that guarantees more invariants and only
2361        // convert to PSBT for finalization
2362        let psbt = Psbt {
2363            unsigned_tx: transaction,
2364            version: 0,
2365            xpub: Default::default(),
2366            proprietary: Default::default(),
2367            unknown: Default::default(),
2368            inputs: selected_utxos
2369                .iter()
2370                .map(|(_utxo_key, utxo)| {
2371                    let script_pubkey = self
2372                        .descriptor
2373                        .tweak(&utxo.tweak, self.secp)
2374                        .script_pubkey();
2375                    Input {
2376                        non_witness_utxo: None,
2377                        witness_utxo: Some(TxOut {
2378                            value: utxo.amount,
2379                            script_pubkey,
2380                        }),
2381                        partial_sigs: Default::default(),
2382                        sighash_type: None,
2383                        redeem_script: None,
2384                        witness_script: Some(
2385                            self.descriptor
2386                                .tweak(&utxo.tweak, self.secp)
2387                                .script_code()
2388                                .expect("Failed to tweak descriptor"),
2389                        ),
2390                        bip32_derivation: Default::default(),
2391                        final_script_sig: None,
2392                        final_script_witness: None,
2393                        ripemd160_preimages: Default::default(),
2394                        sha256_preimages: Default::default(),
2395                        hash160_preimages: Default::default(),
2396                        hash256_preimages: Default::default(),
2397                        proprietary: vec![(proprietary_tweak_key(), utxo.tweak.to_vec())]
2398                            .into_iter()
2399                            .collect(),
2400                        tap_key_sig: Default::default(),
2401                        tap_script_sigs: Default::default(),
2402                        tap_scripts: Default::default(),
2403                        tap_key_origins: Default::default(),
2404                        tap_internal_key: Default::default(),
2405                        tap_merkle_root: Default::default(),
2406                        unknown: Default::default(),
2407                    }
2408                })
2409                .collect(),
2410            outputs: vec![Default::default(), change_out],
2411        };
2412
2413        Ok(UnsignedTransaction {
2414            psbt,
2415            signatures: vec![],
2416            change,
2417            fees: PegOutFees {
2418                fee_rate,
2419                total_weight,
2420            },
2421            destination,
2422            selected_utxos,
2423            peg_out_amount,
2424            rbf,
2425        })
2426    }
2427
2428    fn sign_psbt(&self, psbt: &mut Psbt) {
2429        let mut tx_hasher = SighashCache::new(&psbt.unsigned_tx);
2430
2431        for (idx, (psbt_input, _tx_input)) in psbt
2432            .inputs
2433            .iter_mut()
2434            .zip(psbt.unsigned_tx.input.iter())
2435            .enumerate()
2436        {
2437            let tweaked_secret = {
2438                let tweak = psbt_input
2439                    .proprietary
2440                    .get(&proprietary_tweak_key())
2441                    .expect("Malformed PSBT: expected tweak");
2442
2443                self.secret_key.tweak(tweak, self.secp)
2444            };
2445
2446            let tx_hash = tx_hasher
2447                .p2wsh_signature_hash(
2448                    idx,
2449                    psbt_input
2450                        .witness_script
2451                        .as_ref()
2452                        .expect("Missing witness script"),
2453                    psbt_input
2454                        .witness_utxo
2455                        .as_ref()
2456                        .expect("Missing UTXO")
2457                        .value,
2458                    EcdsaSighashType::All,
2459                )
2460                .expect("Failed to create segwit sighash");
2461
2462            let signature = self.secp.sign_ecdsa(
2463                &Message::from_digest_slice(&tx_hash[..]).unwrap(),
2464                &tweaked_secret,
2465            );
2466
2467            psbt_input.partial_sigs.insert(
2468                bitcoin::PublicKey {
2469                    compressed: true,
2470                    inner: secp256k1::PublicKey::from_secret_key(self.secp, &tweaked_secret),
2471                },
2472                EcdsaSig::sighash_all(signature),
2473            );
2474        }
2475    }
2476
2477    fn derive_script(&self, tweak: &[u8]) -> ScriptBuf {
2478        struct CompressedPublicKeyTranslator<'t, 's, Ctx: Verification> {
2479            tweak: &'t [u8],
2480            secp: &'s Secp256k1<Ctx>,
2481        }
2482
2483        impl<Ctx: Verification>
2484            miniscript::Translator<CompressedPublicKey, CompressedPublicKey, Infallible>
2485            for CompressedPublicKeyTranslator<'_, '_, Ctx>
2486        {
2487            fn pk(&mut self, pk: &CompressedPublicKey) -> Result<CompressedPublicKey, Infallible> {
2488                let hashed_tweak = {
2489                    let mut hasher = HmacEngine::<sha256::Hash>::new(&pk.key.serialize()[..]);
2490                    hasher.input(self.tweak);
2491                    Hmac::from_engine(hasher).to_byte_array()
2492                };
2493
2494                Ok(CompressedPublicKey {
2495                    key: pk
2496                        .key
2497                        .add_exp_tweak(
2498                            self.secp,
2499                            &Scalar::from_be_bytes(hashed_tweak).expect("can't fail"),
2500                        )
2501                        .expect("tweaking failed"),
2502                })
2503            }
2504            translate_hash_fail!(CompressedPublicKey, CompressedPublicKey, Infallible);
2505        }
2506
2507        let descriptor = self
2508            .descriptor
2509            .translate_pk(&mut CompressedPublicKeyTranslator {
2510                tweak,
2511                secp: self.secp,
2512            })
2513            .expect("can't fail");
2514
2515        descriptor.script_pubkey()
2516    }
2517}
2518
2519pub fn nonce_from_idx(nonce_idx: u64) -> [u8; 33] {
2520    let mut nonce: [u8; 33] = [0; 33];
2521    // Make it look like a compressed pubkey, has to be either 0x02 or 0x03
2522    nonce[0] = 0x02;
2523    nonce[1..].copy_from_slice(&nonce_idx.consensus_hash::<bitcoin::hashes::sha256::Hash>()[..]);
2524
2525    nonce
2526}
2527
2528/// A peg-out tx that is ready to be broadcast with a tweak for the change UTXO
2529#[derive(Clone, Debug, Encodable, Decodable)]
2530pub struct PendingTransaction {
2531    pub tx: bitcoin::Transaction,
2532    pub tweak: [u8; 33],
2533    pub change: bitcoin::Amount,
2534    pub destination: ScriptBuf,
2535    pub fees: PegOutFees,
2536    pub selected_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2537    pub peg_out_amount: bitcoin::Amount,
2538    pub rbf: Option<Rbf>,
2539}
2540
2541impl Serialize for PendingTransaction {
2542    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2543    where
2544        S: serde::Serializer,
2545    {
2546        if serializer.is_human_readable() {
2547            serializer.serialize_str(&self.consensus_encode_to_hex())
2548        } else {
2549            serializer.serialize_bytes(&self.consensus_encode_to_vec())
2550        }
2551    }
2552}
2553
2554/// A PSBT that is awaiting enough signatures from the federation to becoming a
2555/// `PendingTransaction`
2556#[derive(Clone, Debug, Eq, PartialEq, Encodable, Decodable)]
2557pub struct UnsignedTransaction {
2558    pub psbt: Psbt,
2559    pub signatures: Vec<(PeerId, PegOutSignatureItem)>,
2560    pub change: bitcoin::Amount,
2561    pub fees: PegOutFees,
2562    pub destination: ScriptBuf,
2563    pub selected_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2564    pub peg_out_amount: bitcoin::Amount,
2565    pub rbf: Option<Rbf>,
2566}
2567
2568impl Serialize for UnsignedTransaction {
2569    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2570    where
2571        S: serde::Serializer,
2572    {
2573        if serializer.is_human_readable() {
2574            serializer.serialize_str(&self.consensus_encode_to_hex())
2575        } else {
2576            serializer.serialize_bytes(&self.consensus_encode_to_vec())
2577        }
2578    }
2579}
2580
2581#[cfg(test)]
2582mod tests {
2583
2584    use std::str::FromStr;
2585
2586    use bitcoin::Network::{Bitcoin, Testnet};
2587    use bitcoin::hashes::Hash;
2588    use bitcoin::{Address, Amount, OutPoint, Txid, secp256k1};
2589    use fedimint_core::Feerate;
2590    use fedimint_core::encoding::btc::NetworkLegacyEncodingWrapper;
2591    use fedimint_core::envs::is_automatic_consensus_version_voting_disabled;
2592    use fedimint_wallet_common::{PegOut, PegOutFees, Rbf, WalletOutputV0};
2593    use miniscript::descriptor::Wsh;
2594
2595    use crate::common::PegInDescriptor;
2596    use crate::{
2597        CompressedPublicKey, FeeArithmetic, OsRng, SpendableUTXO, StatelessWallet, UTXOKey,
2598        WalletOutputError,
2599    };
2600
2601    /// A peg-out recipient is chosen by the user, and nothing stops them from
2602    /// picking the very change script the peg-out will use — change tweaks are
2603    /// `nonce_from_idx` of a counter, so they are publicly derivable. The
2604    /// resulting transaction carries the change script on two outputs, and
2605    /// `recognize_change_utxo` matches on script across all of them, so it
2606    /// tracks an output other than [`PEG_OUT_CHANGE_VOUT`]. Recording a fixed
2607    /// change vout as claimed therefore does not cover everything `UTXOKey`
2608    /// ends up holding; `process_input` has to reject on `UTXOKey` itself.
2609    #[test]
2610    fn peg_out_destination_can_collide_with_the_change_script() {
2611        let secp = secp256k1::Secp256k1::new();
2612
2613        let descriptor = PegInDescriptor::Wsh(
2614            Wsh::new_sortedmulti(
2615                3,
2616                (0..4)
2617                    .map(|_| secp.generate_keypair(&mut OsRng))
2618                    .map(|(_, key)| CompressedPublicKey { key })
2619                    .collect(),
2620            )
2621            .unwrap(),
2622        );
2623
2624        let (secret_key, _) = secp.generate_keypair(&mut OsRng);
2625
2626        let wallet = StatelessWallet {
2627            descriptor: &descriptor,
2628            secret_key: &secret_key,
2629            secp: &secp,
2630        };
2631
2632        let change_tweak = crate::nonce_from_idx(0);
2633        let change_script = wallet.derive_script(&change_tweak);
2634
2635        let tx = wallet
2636            .create_tx(
2637                Amount::from_sat(1000),
2638                change_script.clone(),
2639                vec![],
2640                vec![(
2641                    UTXOKey(OutPoint::null()),
2642                    SpendableUTXO {
2643                        tweak: [0; 33],
2644                        amount: bitcoin::Amount::from_sat(100_000),
2645                    },
2646                )],
2647                Feerate { sats_per_kvb: 1000 },
2648                &change_tweak,
2649                None,
2650                FeeArithmetic::Checked,
2651            )
2652            .expect("tx creation succeeds");
2653
2654        let matching = tx
2655            .psbt
2656            .unsigned_tx
2657            .output
2658            .iter()
2659            .filter(|o| o.script_pubkey == change_script)
2660            .count();
2661
2662        assert_eq!(
2663            matching, 2,
2664            "both the destination and the change output carry the change script"
2665        );
2666    }
2667
2668    /// Pre-2.3 the fee multiplication wrapped in release profiles, so a peg-out
2669    /// declaring an enormous rate was accepted and broadcast with a fee too low
2670    /// to confirm. From 2.3 on it is rejected. Both regimes must keep working:
2671    /// replaying a pre-2.3 session has to reproduce the acceptance.
2672    #[test]
2673    fn fee_arithmetic_rejects_an_unpayable_rate_only_once_active() {
2674        let secp = secp256k1::Secp256k1::new();
2675
2676        let descriptor = PegInDescriptor::Wsh(
2677            Wsh::new_sortedmulti(
2678                3,
2679                (0..4)
2680                    .map(|_| secp.generate_keypair(&mut OsRng))
2681                    .map(|(_, key)| CompressedPublicKey { key })
2682                    .collect(),
2683            )
2684            .unwrap(),
2685        );
2686
2687        let absurd = Feerate {
2688            sats_per_kvb: u64::MAX,
2689        };
2690        let ordinary = Feerate { sats_per_kvb: 1000 };
2691
2692        assert_eq!(
2693            FeeArithmetic::Checked.calculate_fee(absurd, 958),
2694            Err(WalletOutputError::NotEnoughSpendableUTXO),
2695            "an unpayable rate is rejected once 2.3 is active"
2696        );
2697        assert_eq!(
2698            FeeArithmetic::Wrapping.calculate_fee(absurd, 958),
2699            Ok(absurd.wrapping_calculate_fee(958)),
2700            "pre-2.3 behaviour is reproduced exactly, wrap and all"
2701        );
2702
2703        for arithmetic in [FeeArithmetic::Checked, FeeArithmetic::Wrapping] {
2704            assert_eq!(
2705                arithmetic.calculate_fee(ordinary, 958),
2706                Ok(ordinary.calculate_fee(958)),
2707                "an ordinary rate is unaffected in either regime"
2708            );
2709        }
2710
2711        let _ = descriptor;
2712    }
2713
2714    /// A peg-out amount reaches `create_tx` straight from an unauthenticated
2715    /// caller, both via `PEG_OUT_FEES_ENDPOINT` and via a submitted peg-out
2716    /// output. Summing it with the dust limit and the fees used to overflow
2717    /// `bitcoin::Amount`'s panicking `Add`, killing the guardian process.
2718    #[test]
2719    fn create_tx_rejects_amounts_that_cannot_exist_on_chain() {
2720        let secp = secp256k1::Secp256k1::new();
2721
2722        let descriptor = PegInDescriptor::Wsh(
2723            Wsh::new_sortedmulti(
2724                3,
2725                (0..4)
2726                    .map(|_| secp.generate_keypair(&mut OsRng))
2727                    .map(|(_, key)| CompressedPublicKey { key })
2728                    .collect(),
2729            )
2730            .unwrap(),
2731        );
2732
2733        let (secret_key, _) = secp.generate_keypair(&mut OsRng);
2734
2735        let wallet = StatelessWallet {
2736            descriptor: &descriptor,
2737            secret_key: &secret_key,
2738            secp: &secp,
2739        };
2740
2741        let recipient = Address::from_str("32iVBEu4dxkUQk9dJbZUiBiQdmypcEyJRf").unwrap();
2742        let utxos = vec![(
2743            UTXOKey(OutPoint::null()),
2744            SpendableUTXO {
2745                tweak: [0; 33],
2746                amount: bitcoin::Amount::from_sat(100_000),
2747            },
2748        )];
2749
2750        for amount in [
2751            Amount::from_sat(u64::MAX),
2752            Amount::MAX_MONEY + Amount::from_sat(1),
2753        ] {
2754            let tx = wallet.create_tx(
2755                amount,
2756                recipient.clone().assume_checked().script_pubkey(),
2757                vec![],
2758                utxos.clone(),
2759                Feerate { sats_per_kvb: 1000 },
2760                &[0; 33],
2761                None,
2762                FeeArithmetic::Checked,
2763            );
2764
2765            assert_eq!(tx, Err(WalletOutputError::NotEnoughSpendableUTXO));
2766        }
2767    }
2768
2769    #[test]
2770    fn create_tx_should_validate_amounts() {
2771        let secp = secp256k1::Secp256k1::new();
2772
2773        let descriptor = PegInDescriptor::Wsh(
2774            Wsh::new_sortedmulti(
2775                3,
2776                (0..4)
2777                    .map(|_| secp.generate_keypair(&mut OsRng))
2778                    .map(|(_, key)| CompressedPublicKey { key })
2779                    .collect(),
2780            )
2781            .unwrap(),
2782        );
2783
2784        let (secret_key, _) = secp.generate_keypair(&mut OsRng);
2785
2786        let wallet = StatelessWallet {
2787            descriptor: &descriptor,
2788            secret_key: &secret_key,
2789            secp: &secp,
2790        };
2791
2792        let spendable = SpendableUTXO {
2793            tweak: [0; 33],
2794            amount: bitcoin::Amount::from_sat(3000),
2795        };
2796
2797        let recipient = Address::from_str("32iVBEu4dxkUQk9dJbZUiBiQdmypcEyJRf").unwrap();
2798
2799        let fee = Feerate { sats_per_kvb: 1000 };
2800        let weight = 875;
2801
2802        // not enough SpendableUTXO
2803        // tx fee = ceil(875 / 4) * 1 sat/vb = 219
2804        // change script dust = 330
2805        // spendable sats = 3000 - 219 - 330 = 2451
2806        let tx = wallet.create_tx(
2807            Amount::from_sat(2452),
2808            recipient.clone().assume_checked().script_pubkey(),
2809            vec![],
2810            vec![(UTXOKey(OutPoint::null()), spendable.clone())],
2811            fee,
2812            &[0; 33],
2813            None,
2814            FeeArithmetic::Checked,
2815        );
2816        assert_eq!(tx, Err(WalletOutputError::NotEnoughSpendableUTXO));
2817
2818        // successful tx creation
2819        let mut tx = wallet
2820            .create_tx(
2821                Amount::from_sat(1000),
2822                recipient.clone().assume_checked().script_pubkey(),
2823                vec![],
2824                vec![(UTXOKey(OutPoint::null()), spendable)],
2825                fee,
2826                &[0; 33],
2827                None,
2828                FeeArithmetic::Checked,
2829            )
2830            .expect("is ok");
2831
2832        // peg out weight is incorrectly set to 0
2833        let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, 0), fee, Bitcoin);
2834        assert_eq!(res, Err(WalletOutputError::TxWeightIncorrect(0, weight)));
2835
2836        // fee rate set below min relay fee to 0
2837        let res = StatelessWallet::validate_tx(&tx, &rbf(0, weight), fee, Bitcoin);
2838        assert_eq!(res, Err(WalletOutputError::BelowMinRelayFee));
2839
2840        // fees are okay
2841        let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2842        assert_eq!(res, Ok(()));
2843
2844        // tx has fee below consensus
2845        tx.fees = PegOutFees::new(0, weight);
2846        let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2847        assert_eq!(
2848            res,
2849            Err(WalletOutputError::PegOutFeeBelowConsensus(
2850                Feerate { sats_per_kvb: 0 },
2851                fee
2852            ))
2853        );
2854
2855        // tx has peg-out amount under dust limit
2856        tx.peg_out_amount = bitcoin::Amount::ZERO;
2857        let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2858        assert_eq!(res, Err(WalletOutputError::PegOutUnderDustLimit));
2859
2860        // tx is invalid for network
2861        let output = WalletOutputV0::PegOut(PegOut {
2862            recipient,
2863            amount: bitcoin::Amount::from_sat(1000),
2864            fees: PegOutFees::new(100, weight),
2865        });
2866        let res = StatelessWallet::validate_tx(&tx, &output, fee, Testnet);
2867        assert_eq!(
2868            res,
2869            Err(WalletOutputError::WrongNetwork(
2870                NetworkLegacyEncodingWrapper(Testnet),
2871                NetworkLegacyEncodingWrapper(Bitcoin)
2872            ))
2873        );
2874    }
2875
2876    fn rbf(sats_per_kvb: u64, total_weight: u64) -> WalletOutputV0 {
2877        WalletOutputV0::Rbf(Rbf {
2878            fees: PegOutFees::new(sats_per_kvb, total_weight),
2879            txid: Txid::all_zeros(),
2880        })
2881    }
2882
2883    #[test]
2884    fn automatic_vote_suppressed_when_env_set() {
2885        unsafe {
2886            std::env::set_var("FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING", "1");
2887        }
2888        assert!(is_automatic_consensus_version_voting_disabled());
2889        unsafe {
2890            std::env::remove_var("FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING");
2891        }
2892    }
2893
2894    #[test]
2895    fn automatic_vote_active_when_env_unset() {
2896        unsafe {
2897            std::env::remove_var("FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING");
2898        }
2899        assert!(!is_automatic_consensus_version_voting_disabled());
2900    }
2901}