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                .map_or(self.cfg.consensus.default_fee, |status| status.fee_rate)
1419                .sats_per_kvb as f64
1420                * get_feerate_multiplier())
1421            .round()) as u64,
1422        }
1423    }
1424
1425    pub async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u32 {
1426        let peer_count = self.cfg.consensus.peer_peg_in_keys.to_num_peers().total();
1427
1428        let mut counts = dbtx
1429            .find_by_prefix(&BlockCountVotePrefix)
1430            .await
1431            .map(|entry| entry.1)
1432            .collect::<Vec<u32>>()
1433            .await;
1434
1435        assert!(counts.len() <= peer_count);
1436
1437        while counts.len() < peer_count {
1438            counts.push(0);
1439        }
1440
1441        counts.sort_unstable();
1442
1443        counts[peer_count / 2]
1444    }
1445
1446    pub async fn consensus_fee_rate(&self, dbtx: &mut DatabaseTransaction<'_>) -> Feerate {
1447        let peer_count = self.cfg.consensus.peer_peg_in_keys.to_num_peers().total();
1448
1449        let mut rates = dbtx
1450            .find_by_prefix(&FeeRateVotePrefix)
1451            .await
1452            .map(|(.., rate)| rate)
1453            .collect::<Vec<_>>()
1454            .await;
1455
1456        assert!(rates.len() <= peer_count);
1457
1458        while rates.len() < peer_count {
1459            rates.push(self.cfg.consensus.default_fee);
1460        }
1461
1462        rates.sort_unstable();
1463
1464        rates[peer_count / 2]
1465    }
1466
1467    async fn consensus_module_consensus_version(
1468        &self,
1469        dbtx: &mut DatabaseTransaction<'_>,
1470    ) -> ModuleConsensusVersion {
1471        let num_peers = self.cfg.consensus.peer_peg_in_keys.to_num_peers();
1472
1473        let mut versions = dbtx
1474            .find_by_prefix(&ConsensusVersionVotePrefix)
1475            .await
1476            .map(|entry| entry.1)
1477            .collect::<Vec<ModuleConsensusVersion>>()
1478            .await;
1479
1480        while versions.len() < num_peers.total() {
1481            versions.push(ModuleConsensusVersion::new(2, 0));
1482        }
1483
1484        assert_eq!(versions.len(), num_peers.total());
1485
1486        versions.sort_unstable();
1487
1488        assert!(versions.first() <= versions.last());
1489
1490        versions[num_peers.max_evil()]
1491    }
1492
1493    pub async fn consensus_nonce(&self, dbtx: &mut DatabaseTransaction<'_>) -> [u8; 33] {
1494        let nonce_idx = dbtx.get_value(&PegOutNonceKey).await.unwrap_or(0);
1495        dbtx.insert_entry(&PegOutNonceKey, &(nonce_idx + 1)).await;
1496
1497        nonce_from_idx(nonce_idx)
1498    }
1499
1500    async fn sync_up_to_consensus_count(
1501        &self,
1502        dbtx: &mut DatabaseTransaction<'_>,
1503        old_count: u32,
1504        new_count: u32,
1505    ) {
1506        let sync_start = fedimint_core::time::now();
1507        info!(
1508            target: LOG_MODULE_WALLET,
1509            old_count,
1510            new_count,
1511            blocks_to_go = new_count
1512                .checked_sub(old_count)
1513                .expect("new_count must be >= old_count"),
1514            "New block count consensus, initiating sync",
1515        );
1516
1517        // Before we can safely call our bitcoin backend to process the new consensus
1518        // count, we need to ensure we observed enough confirmations
1519        self.wait_for_finality_confs_or_shutdown(new_count).await;
1520
1521        for height in old_count..new_count {
1522            let block_start = fedimint_core::time::now();
1523            info!(
1524                target: LOG_MODULE_WALLET,
1525                height,
1526                "Processing block of height {height}",
1527            );
1528
1529            // TODO: use batching for mainnet syncing
1530            trace!(block = height, "Fetching block hash");
1531            let block_hash = retry("get_block_hash", backoff_util::background_backoff(), || {
1532                self.btc_rpc.get_block_hash(u64::from(height)) // TODO: use u64 for height everywhere
1533            })
1534            .await
1535            .expect("bitcoind rpc to get block hash");
1536
1537            let block = retry("get_block", backoff_util::background_backoff(), || {
1538                self.btc_rpc.get_block(&block_hash)
1539            })
1540            .await
1541            .expect("bitcoind rpc to get block");
1542
1543            if let Some(prev_block_height) = height.checked_sub(1) {
1544                if let Some(hash) = dbtx
1545                    .get_value(&BlockHashByHeightKey(prev_block_height))
1546                    .await
1547                {
1548                    assert_eq!(block.header.prev_blockhash, hash.0);
1549                } else {
1550                    warn!(
1551                        target: LOG_MODULE_WALLET,
1552                        %height,
1553                        %block_hash,
1554                        %prev_block_height,
1555                        prev_blockhash = %block.header.prev_blockhash,
1556                        "Missing previous block hash. This should only happen on the first processed block height."
1557                    );
1558                }
1559            }
1560
1561            if self.consensus_module_consensus_version(dbtx).await
1562                >= ModuleConsensusVersion::new(2, 2)
1563            {
1564                for transaction in &block.txdata {
1565                    // We maintain the subset of unspent P2WSH transaction outputs created
1566                    // since the module was running on the new consensus version, which might be
1567                    // the same time as the genesis session.
1568
1569                    for tx_in in &transaction.input {
1570                        dbtx.remove_entry(&UnspentTxOutKey(tx_in.previous_output))
1571                            .await;
1572                    }
1573
1574                    for (vout, tx_out) in transaction.output.iter().enumerate() {
1575                        let should_track_utxo = if self.cfg.consensus.peer_peg_in_keys.len() > 1 {
1576                            tx_out.script_pubkey.is_p2wsh()
1577                        } else {
1578                            tx_out.script_pubkey.is_p2wpkh()
1579                        };
1580
1581                        if should_track_utxo {
1582                            let outpoint = bitcoin::OutPoint {
1583                                txid: transaction.compute_txid(),
1584                                vout: vout as u32,
1585                            };
1586
1587                            dbtx.insert_new_entry(&UnspentTxOutKey(outpoint), tx_out)
1588                                .await;
1589                        }
1590                    }
1591                }
1592            }
1593
1594            let pending_transactions = dbtx
1595                .find_by_prefix(&PendingTransactionPrefixKey)
1596                .await
1597                .map(|(key, transaction)| (key.0, transaction))
1598                .collect::<BTreeMap<Txid, PendingTransaction>>()
1599                .await;
1600            let pending_transactions_len = pending_transactions.len();
1601
1602            debug!(
1603                target: LOG_MODULE_WALLET,
1604                ?height,
1605                ?pending_transactions_len,
1606                "Recognizing change UTXOs"
1607            );
1608            for (txid, tx) in &pending_transactions {
1609                let is_tx_in_block = block.txdata.iter().any(|tx| tx.compute_txid() == *txid);
1610
1611                if is_tx_in_block {
1612                    debug!(
1613                        target: LOG_MODULE_WALLET,
1614                        ?txid, ?height, ?block_hash, "Recognizing change UTXO"
1615                    );
1616                    self.recognize_change_utxo(dbtx, tx).await;
1617                } else {
1618                    debug!(
1619                        target: LOG_MODULE_WALLET,
1620                        ?txid,
1621                        ?height,
1622                        ?block_hash,
1623                        "Pending transaction not yet confirmed in this block"
1624                    );
1625                }
1626            }
1627
1628            dbtx.insert_new_entry(&BlockHashKey(block_hash), &()).await;
1629            dbtx.insert_new_entry(
1630                &BlockHashByHeightKey(height),
1631                &BlockHashByHeightValue(block_hash),
1632            )
1633            .await;
1634
1635            info!(
1636                target: LOG_MODULE_WALLET,
1637                height,
1638                ?block_hash,
1639                duration = ?block_start.elapsed().unwrap_or_default(),
1640                "Successfully processed block of height {height}",
1641            );
1642        }
1643
1644        info!(
1645            target: LOG_MODULE_WALLET,
1646            old_count,
1647            new_count,
1648            blocks_processed = new_count
1649                .checked_sub(old_count)
1650                .expect("new_count must be >= old_count"),
1651            duration = ?sync_start.elapsed().unwrap_or_default(),
1652            "Block count consensus sync complete",
1653        );
1654    }
1655
1656    /// Add a change UTXO to our spendable UTXO database after it was included
1657    /// in a block that we got consensus on.
1658    async fn recognize_change_utxo(
1659        &self,
1660        dbtx: &mut DatabaseTransaction<'_>,
1661        pending_tx: &PendingTransaction,
1662    ) {
1663        self.remove_rbf_transactions(dbtx, pending_tx).await;
1664
1665        let script_pk = self
1666            .cfg
1667            .consensus
1668            .peg_in_descriptor
1669            .tweak(&pending_tx.tweak, &self.secp)
1670            .script_pubkey();
1671        for (idx, output) in pending_tx.tx.output.iter().enumerate() {
1672            if output.script_pubkey == script_pk {
1673                dbtx.insert_entry(
1674                    &UTXOKey(bitcoin::OutPoint {
1675                        txid: pending_tx.tx.compute_txid(),
1676                        vout: idx as u32,
1677                    }),
1678                    &SpendableUTXO {
1679                        tweak: pending_tx.tweak,
1680                        amount: output.value,
1681                    },
1682                )
1683                .await;
1684            }
1685        }
1686    }
1687
1688    /// Removes the `PendingTransaction` and any transactions tied to it via RBF
1689    async fn remove_rbf_transactions(
1690        &self,
1691        dbtx: &mut DatabaseTransaction<'_>,
1692        pending_tx: &PendingTransaction,
1693    ) {
1694        let mut all_transactions: BTreeMap<Txid, PendingTransaction> = dbtx
1695            .find_by_prefix(&PendingTransactionPrefixKey)
1696            .await
1697            .map(|(key, val)| (key.0, val))
1698            .collect::<BTreeMap<Txid, PendingTransaction>>()
1699            .await;
1700
1701        // We need to search and remove all `PendingTransactions` invalidated by RBF
1702        let mut pending_to_remove = vec![pending_tx.clone()];
1703        while let Some(removed) = pending_to_remove.pop() {
1704            all_transactions.remove(&removed.tx.compute_txid());
1705            dbtx.remove_entry(&PendingTransactionKey(removed.tx.compute_txid()))
1706                .await;
1707
1708            // Search for tx that this `removed` has as RBF
1709            if let Some(rbf) = &removed.rbf
1710                && let Some(tx) = all_transactions.get(&rbf.txid)
1711            {
1712                pending_to_remove.push(tx.clone());
1713            }
1714
1715            // Search for tx that wanted to RBF the `removed` one
1716            for tx in all_transactions.values() {
1717                if let Some(rbf) = &tx.rbf
1718                    && rbf.txid == removed.tx.compute_txid()
1719                {
1720                    pending_to_remove.push(tx.clone());
1721                }
1722            }
1723        }
1724    }
1725
1726    async fn block_is_known(
1727        &self,
1728        dbtx: &mut DatabaseTransaction<'_>,
1729        block_hash: BlockHash,
1730    ) -> bool {
1731        dbtx.get_value(&BlockHashKey(block_hash)).await.is_some()
1732    }
1733
1734    async fn create_peg_out_tx(
1735        &self,
1736        dbtx: &mut DatabaseTransaction<'_>,
1737        output: &WalletOutputV0,
1738        change_tweak: &[u8; 33],
1739    ) -> Result<UnsignedTransaction, WalletOutputError> {
1740        let fee_arithmetic = if CHECKED_PEG_OUT_FEE_MODULE_CONSENSUS_VERSION
1741            <= self.consensus_module_consensus_version(dbtx).await
1742        {
1743            FeeArithmetic::Checked
1744        } else {
1745            FeeArithmetic::Wrapping
1746        };
1747
1748        match output {
1749            WalletOutputV0::PegOut(peg_out) => self.offline_wallet().create_tx(
1750                peg_out.amount,
1751                // Note: While calling `assume_checked()` is generally unwise, checking the
1752                // network here could be a consensus-breaking change. Ignoring the network
1753                // is fine here since we validate it in `process_output()`.
1754                peg_out.recipient.clone().assume_checked().script_pubkey(),
1755                vec![],
1756                self.available_utxos(dbtx).await,
1757                peg_out.fees.fee_rate,
1758                change_tweak,
1759                None,
1760                fee_arithmetic,
1761            ),
1762            WalletOutputV0::Rbf(rbf) => {
1763                let tx = dbtx
1764                    .get_value(&PendingTransactionKey(rbf.txid))
1765                    .await
1766                    .ok_or(WalletOutputError::RbfTransactionIdNotFound)?;
1767
1768                self.offline_wallet().create_tx(
1769                    tx.peg_out_amount,
1770                    tx.destination,
1771                    tx.selected_utxos,
1772                    self.available_utxos(dbtx).await,
1773                    tx.fees.fee_rate,
1774                    change_tweak,
1775                    Some(rbf.clone()),
1776                    fee_arithmetic,
1777                )
1778            }
1779        }
1780    }
1781
1782    async fn available_utxos(
1783        &self,
1784        dbtx: &mut DatabaseTransaction<'_>,
1785    ) -> Vec<(UTXOKey, SpendableUTXO)> {
1786        dbtx.find_by_prefix(&UTXOPrefixKey)
1787            .await
1788            .collect::<Vec<(UTXOKey, SpendableUTXO)>>()
1789            .await
1790    }
1791
1792    pub async fn get_wallet_value(&self, dbtx: &mut DatabaseTransaction<'_>) -> bitcoin::Amount {
1793        let sat_sum = self
1794            .available_utxos(dbtx)
1795            .await
1796            .into_iter()
1797            .map(|(_, utxo)| utxo.amount.to_sat())
1798            .sum();
1799        bitcoin::Amount::from_sat(sat_sum)
1800    }
1801
1802    async fn get_wallet_summary(&self, dbtx: &mut DatabaseTransaction<'_>) -> WalletSummary {
1803        fn partition_peg_out_and_change(
1804            transactions: Vec<Transaction>,
1805        ) -> (Vec<TxOutputSummary>, Vec<TxOutputSummary>) {
1806            let mut peg_out_txos: Vec<TxOutputSummary> = Vec::new();
1807            let mut change_utxos: Vec<TxOutputSummary> = Vec::new();
1808
1809            for tx in transactions {
1810                let txid = tx.compute_txid();
1811
1812                // to identify outputs for the peg_out (idx = 0) and change (idx = 1), we lean
1813                // on how the wallet constructs the transaction
1814                let peg_out_output = tx
1815                    .output
1816                    .first()
1817                    .expect("tx must contain withdrawal output");
1818
1819                let change_output = tx.output.last().expect("tx must contain change output");
1820
1821                peg_out_txos.push(TxOutputSummary {
1822                    outpoint: bitcoin::OutPoint { txid, vout: 0 },
1823                    amount: peg_out_output.value,
1824                });
1825
1826                change_utxos.push(TxOutputSummary {
1827                    outpoint: bitcoin::OutPoint { txid, vout: 1 },
1828                    amount: change_output.value,
1829                });
1830            }
1831
1832            (peg_out_txos, change_utxos)
1833        }
1834
1835        let spendable_utxos = self
1836            .available_utxos(dbtx)
1837            .await
1838            .iter()
1839            .map(|(utxo_key, spendable_utxo)| TxOutputSummary {
1840                outpoint: utxo_key.0,
1841                amount: spendable_utxo.amount,
1842            })
1843            .collect::<Vec<_>>();
1844
1845        // constructed peg-outs without threshold signatures
1846        let unsigned_transactions = dbtx
1847            .find_by_prefix(&UnsignedTransactionPrefixKey)
1848            .await
1849            .map(|(_tx_key, tx)| tx.psbt.unsigned_tx)
1850            .collect::<Vec<_>>()
1851            .await;
1852
1853        // peg-outs with threshold signatures, awaiting finality delay confirmations
1854        let unconfirmed_transactions = dbtx
1855            .find_by_prefix(&PendingTransactionPrefixKey)
1856            .await
1857            .map(|(_tx_key, tx)| tx.tx)
1858            .collect::<Vec<_>>()
1859            .await;
1860
1861        let (unsigned_peg_out_txos, unsigned_change_utxos) =
1862            partition_peg_out_and_change(unsigned_transactions);
1863
1864        let (unconfirmed_peg_out_txos, unconfirmed_change_utxos) =
1865            partition_peg_out_and_change(unconfirmed_transactions);
1866
1867        WalletSummary {
1868            spendable_utxos,
1869            unsigned_peg_out_txos,
1870            unsigned_change_utxos,
1871            unconfirmed_peg_out_txos,
1872            unconfirmed_change_utxos,
1873        }
1874    }
1875
1876    async fn is_utxo_confirmed(
1877        &self,
1878        dbtx: &mut DatabaseTransaction<'_>,
1879        outpoint: bitcoin::OutPoint,
1880    ) -> bool {
1881        dbtx.get_value(&UnspentTxOutKey(outpoint)).await.is_some()
1882    }
1883
1884    fn offline_wallet(&'_ self) -> StatelessWallet<'_> {
1885        StatelessWallet {
1886            descriptor: &self.cfg.consensus.peg_in_descriptor,
1887            secret_key: &self.cfg.private.peg_in_key,
1888            secp: &self.secp,
1889        }
1890    }
1891
1892    fn spawn_broadcast_pending_task(
1893        task_group: &TaskGroup,
1894        server_bitcoin_rpc_monitor: &ServerBitcoinRpcMonitor,
1895        db: &Database,
1896        broadcast_pending_notify: Arc<Notify>,
1897    ) {
1898        task_group.spawn_cancellable("broadcast pending", {
1899            let btc_rpc = server_bitcoin_rpc_monitor.clone();
1900            let db = db.clone();
1901            run_broadcast_pending_tx(db, btc_rpc, broadcast_pending_notify)
1902        });
1903    }
1904
1905    /// Get the bitcoin network for UI display
1906    pub fn network_ui(&self) -> Network {
1907        self.cfg.consensus.network.0
1908    }
1909
1910    /// Get the current consensus block count for UI display
1911    pub async fn consensus_block_count_ui(&self) -> u32 {
1912        self.consensus_block_count(&mut self.db.begin_transaction_nc().await)
1913            .await
1914    }
1915
1916    /// Get the current consensus fee rate for UI display
1917    pub async fn consensus_feerate_ui(&self) -> Feerate {
1918        self.consensus_fee_rate(&mut self.db.begin_transaction_nc().await)
1919            .await
1920    }
1921
1922    /// Get the current wallet summary for UI display
1923    pub async fn get_wallet_summary_ui(&self) -> WalletSummary {
1924        self.get_wallet_summary(&mut self.db.begin_transaction_nc().await)
1925            .await
1926    }
1927
1928    /// Shutdown the task group shared throughout fedimintd, giving 60 seconds
1929    /// for other services to gracefully shutdown.
1930    async fn graceful_shutdown(&self) {
1931        if let Err(e) = self
1932            .task_group
1933            .clone()
1934            .shutdown_join_all(Some(Duration::from_mins(1)))
1935            .await
1936        {
1937            panic!("Error while shutting down fedimintd task group: {e}");
1938        }
1939    }
1940
1941    /// Returns once our bitcoin backend observes finality delay confirmations
1942    /// of the consensus block count. If we don't observe enough confirmations
1943    /// after one hour, we gracefully shutdown fedimintd. This is necessary
1944    /// since we can no longer participate in consensus if our bitcoin backend
1945    /// is unable to observe the same chain tip as our peers.
1946    async fn wait_for_finality_confs_or_shutdown(&self, consensus_block_count: u32) {
1947        let backoff = if is_running_in_test_env() {
1948            // every 100ms for 60s
1949            backoff_util::custom_backoff(
1950                Duration::from_millis(100),
1951                Duration::from_millis(100),
1952                Some(10 * 60),
1953            )
1954        } else {
1955            // every max 10s for 1 hour
1956            backoff_util::fibonacci_max_one_hour()
1957        };
1958
1959        let wait_for_finality_confs = || async {
1960            let our_chain_tip_block_count = self.get_block_count()?;
1961            let consensus_chain_tip_block_count =
1962                consensus_block_count + self.cfg.consensus.finality_delay;
1963
1964            if consensus_chain_tip_block_count <= our_chain_tip_block_count {
1965                Ok(())
1966            } else {
1967                Err(anyhow::anyhow!("not enough confirmations"))
1968            }
1969        };
1970
1971        if retry("wait_for_finality_confs", backoff, wait_for_finality_confs)
1972            .await
1973            .is_err()
1974        {
1975            self.graceful_shutdown().await;
1976        }
1977    }
1978
1979    fn spawn_peer_supported_consensus_version_task(
1980        api_client: DynModuleApi,
1981        task_group: &TaskGroup,
1982        our_peer_id: PeerId,
1983    ) -> watch::Receiver<Option<ModuleConsensusVersion>> {
1984        let (sender, receiver) = watch::channel(None);
1985        task_group.spawn_cancellable("fetch-peer-consensus-versions", async move {
1986            loop {
1987                let request_futures = api_client.all_peers().iter().filter_map(|&peer| {
1988                    if peer == our_peer_id {
1989                        return None;
1990                    }
1991
1992                    let api_client_inner = api_client.clone();
1993                    Some(async move {
1994                        api_client_inner
1995                            .request_single_peer::<ModuleConsensusVersion>(
1996                                SUPPORTED_MODULE_CONSENSUS_VERSION_ENDPOINT.to_owned(),
1997                                ApiRequestErased::default(),
1998                                peer,
1999                            )
2000                            .await
2001                            .inspect(|res| debug!(
2002                                target: LOG_MODULE_WALLET,
2003                                %peer,
2004                                %our_peer_id,
2005                                ?res,
2006                                "Fetched supported module consensus version from peer"
2007                            ))
2008                            .inspect_err(|err| warn!(
2009                                target: LOG_MODULE_WALLET,
2010                                 %peer,
2011                                 err=%err.fmt_compact(),
2012                                "Failed to fetch consensus version from peer"
2013                            ))
2014                            .ok()
2015                    })
2016                });
2017
2018                let peer_consensus_versions = join_all(request_futures)
2019                    .await
2020                    .into_iter()
2021                    .flatten()
2022                    .collect::<Vec<_>>();
2023
2024                let sorted_consensus_versions = peer_consensus_versions
2025                    .into_iter()
2026                    .chain(std::iter::once(MODULE_CONSENSUS_VERSION))
2027                    .sorted()
2028                    .collect::<Vec<_>>();
2029                let all_peers_supported_version =
2030                    if sorted_consensus_versions.len() == api_client.all_peers().len() {
2031                        let min_supported_version = *sorted_consensus_versions
2032                            .first()
2033                            .expect("at least one element");
2034
2035                        debug!(
2036                            target: LOG_MODULE_WALLET,
2037                            ?sorted_consensus_versions,
2038                            "Fetched supported consensus versions from peers"
2039                        );
2040
2041                        Some(min_supported_version)
2042                    } else {
2043                        assert!(
2044                            sorted_consensus_versions.len() <= api_client.all_peers().len(),
2045                            "Too many peer responses",
2046                        );
2047                        trace!(
2048                            target: LOG_MODULE_WALLET,
2049                            ?sorted_consensus_versions,
2050                            "Not all peers have reported their consensus version yet"
2051                        );
2052                        None
2053                    };
2054
2055                #[allow(clippy::disallowed_methods)]
2056                if sender.send(all_peers_supported_version).is_err() {
2057                    warn!(target: LOG_MODULE_WALLET, "Failed to send consensus version to watch channel, stopping task");
2058                    break;
2059                }
2060
2061                sleep(next_poll_delay(all_peers_supported_version.is_some())).await;
2062            }
2063        });
2064        receiver
2065    }
2066}
2067
2068#[instrument(target = LOG_MODULE_WALLET, level = "debug", skip_all)]
2069pub async fn run_broadcast_pending_tx(
2070    db: Database,
2071    rpc: ServerBitcoinRpcMonitor,
2072    broadcast: Arc<Notify>,
2073) {
2074    loop {
2075        // Unless something new happened, we broadcast once a minute
2076        let _ = tokio::time::timeout(Duration::from_mins(1), broadcast.notified()).await;
2077        broadcast_pending_tx(db.begin_transaction_nc().await, &rpc).await;
2078    }
2079}
2080
2081pub async fn broadcast_pending_tx(
2082    mut dbtx: DatabaseTransaction<'_>,
2083    rpc: &ServerBitcoinRpcMonitor,
2084) {
2085    let pending_tx: Vec<PendingTransaction> = dbtx
2086        .find_by_prefix(&PendingTransactionPrefixKey)
2087        .await
2088        .map(|(_, val)| val)
2089        .collect::<Vec<_>>()
2090        .await;
2091    let rbf_txids: BTreeSet<Txid> = pending_tx
2092        .iter()
2093        .filter_map(|tx| tx.rbf.clone().map(|rbf| rbf.txid))
2094        .collect();
2095    if !pending_tx.is_empty() {
2096        debug!(
2097            target: LOG_MODULE_WALLET,
2098            "Broadcasting pending transactions (total={}, rbf={})",
2099            pending_tx.len(),
2100            rbf_txids.len()
2101        );
2102    }
2103
2104    for PendingTransaction { tx, .. } in pending_tx {
2105        if !rbf_txids.contains(&tx.compute_txid()) {
2106            debug!(
2107                target: LOG_MODULE_WALLET,
2108                tx = %tx.compute_txid(),
2109                weight = tx.weight().to_wu(),
2110                output = ?tx.output,
2111                "Broadcasting peg-out",
2112            );
2113            trace!(transaction = ?tx);
2114            if let Err(err) = rpc.submit_transaction(tx).await {
2115                debug!(
2116                    target: LOG_MODULE_WALLET,
2117                    err = %err.fmt_compact_anyhow(),
2118                    "Error broadcasting peg-out transaction"
2119                );
2120            }
2121        }
2122    }
2123}
2124
2125/// Whether peg-out fee computation rejects a rate that cannot produce a fee
2126/// which exists on chain.
2127///
2128/// Pre-2.3 sessions accepted such peg-outs -- the multiplication wrapped in
2129/// release profiles -- so replaying them has to reproduce that, and the gate
2130/// cannot simply be removed once every federation has upgraded.
2131#[derive(Debug, Clone, Copy, Eq, PartialEq)]
2132pub enum FeeArithmetic {
2133    /// Pre-2.3: multiply unchecked, as historic sessions did.
2134    Wrapping,
2135    /// From 2.3 on: reject a rate whose fee cannot exist on chain.
2136    Checked,
2137}
2138
2139impl FeeArithmetic {
2140    fn calculate_fee(
2141        self,
2142        fee_rate: Feerate,
2143        weight: u64,
2144    ) -> Result<bitcoin::Amount, WalletOutputError> {
2145        match self {
2146            FeeArithmetic::Wrapping => Ok(fee_rate.wrapping_calculate_fee(weight)),
2147            FeeArithmetic::Checked => fee_rate
2148                .checked_calculate_fee(weight)
2149                .ok_or(WalletOutputError::NotEnoughSpendableUTXO),
2150        }
2151    }
2152}
2153
2154struct StatelessWallet<'a> {
2155    descriptor: &'a Descriptor<CompressedPublicKey>,
2156    secret_key: &'a secp256k1::SecretKey,
2157    secp: &'a secp256k1::Secp256k1<secp256k1::All>,
2158}
2159
2160impl StatelessWallet<'_> {
2161    /// Given a tx created from an `WalletOutput`, validate there will be no
2162    /// issues submitting the transaction to the Bitcoin network
2163    fn validate_tx(
2164        tx: &UnsignedTransaction,
2165        output: &WalletOutputV0,
2166        consensus_fee_rate: Feerate,
2167        network: Network,
2168    ) -> Result<(), WalletOutputError> {
2169        if let WalletOutputV0::PegOut(peg_out) = output
2170            && !peg_out.recipient.is_valid_for_network(network)
2171        {
2172            return Err(WalletOutputError::WrongNetwork(
2173                NetworkLegacyEncodingWrapper(network),
2174                NetworkLegacyEncodingWrapper(get_network_for_address(&peg_out.recipient)),
2175            ));
2176        }
2177
2178        // Validate the tx amount is over the dust limit
2179        if tx.peg_out_amount < tx.destination.minimal_non_dust() {
2180            return Err(WalletOutputError::PegOutUnderDustLimit);
2181        }
2182
2183        // Validate tx fee rate is above the consensus fee rate
2184        if tx.fees.fee_rate < consensus_fee_rate {
2185            return Err(WalletOutputError::PegOutFeeBelowConsensus(
2186                tx.fees.fee_rate,
2187                consensus_fee_rate,
2188            ));
2189        }
2190
2191        // Validate added fees are above the min relay tx fee
2192        // BIP-0125 requires 1 sat/vb for RBF by default (same as normal txs)
2193        let fees = match output {
2194            WalletOutputV0::PegOut(pegout) => pegout.fees,
2195            WalletOutputV0::Rbf(rbf) => rbf.fees,
2196        };
2197        if fees.fee_rate.sats_per_kvb < u64::from(DEFAULT_MIN_RELAY_TX_FEE) {
2198            return Err(WalletOutputError::BelowMinRelayFee);
2199        }
2200
2201        // Validate fees weight matches the actual weight
2202        if fees.total_weight != tx.fees.total_weight {
2203            return Err(WalletOutputError::TxWeightIncorrect(
2204                fees.total_weight,
2205                tx.fees.total_weight,
2206            ));
2207        }
2208
2209        Ok(())
2210    }
2211
2212    /// Attempts to create a tx ready to be signed from available UTXOs.
2213    //
2214    // * `peg_out_amount`: How much the peg-out should be
2215    // * `destination`: The address the user is pegging-out to
2216    // * `included_utxos`: UXTOs that must be included (for RBF)
2217    // * `remaining_utxos`: All other spendable UXTOs
2218    // * `fee_rate`: How much needs to be spent on fees
2219    // * `change_tweak`: How the federation can recognize it's change UTXO
2220    // * `rbf`: If this is an RBF transaction
2221    #[allow(clippy::too_many_arguments)]
2222    fn create_tx(
2223        &self,
2224        peg_out_amount: bitcoin::Amount,
2225        destination: ScriptBuf,
2226        mut included_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2227        mut remaining_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2228        mut fee_rate: Feerate,
2229        change_tweak: &[u8; 33],
2230        rbf: Option<Rbf>,
2231        fee_arithmetic: FeeArithmetic,
2232    ) -> Result<UnsignedTransaction, WalletOutputError> {
2233        // `peg_out_amount` arrives unvalidated from whoever asked for the peg-out,
2234        // and `bitcoin::Amount` arithmetic panics on overflow. An amount that
2235        // cannot exist on chain can never be funded by our UTXOs anyway, so reject
2236        // it before doing any arithmetic with it.
2237        if peg_out_amount > bitcoin::Amount::MAX_MONEY {
2238            return Err(WalletOutputError::NotEnoughSpendableUTXO);
2239        }
2240
2241        // Add the rbf fees to the existing tx fees
2242        if let Some(rbf) = &rbf {
2243            fee_rate.sats_per_kvb = fee_rate
2244                .sats_per_kvb
2245                .saturating_add(rbf.fees.fee_rate.sats_per_kvb);
2246        }
2247
2248        // When building a transaction we need to take care of two things:
2249        //  * We need enough input amount to fund all outputs
2250        //  * We need to keep an eye on the tx weight so we can factor the fees into out
2251        //    calculation
2252        // We then go on to calculate the base size of the transaction `total_weight`
2253        // and the maximum weight per added input which we will add every time
2254        // we select an input.
2255        let change_script = self.derive_script(change_tweak);
2256        let out_weight = (destination.len() * 4 + 1 + 32
2257            // Add change script weight, it's very likely to be needed if not we just overpay in fees
2258            + 1 // script len varint, 1 byte for all addresses we accept
2259            + change_script.len() * 4 // script len
2260            + 32) as u64; // value
2261        let mut total_weight = 16 + // version
2262            12 + // up to 2**16-1 inputs
2263            12 + // up to 2**16-1 outputs
2264            out_weight + // weight of all outputs
2265            16; // lock time
2266        // https://github.com/fedimint/fedimint/issues/4590
2267        #[allow(deprecated)]
2268        let max_input_weight = (self
2269            .descriptor
2270            .max_satisfaction_weight()
2271            .expect("is satisfyable") +
2272            128 + // TxOutHash
2273            16 + // TxOutIndex
2274            16) as u64; // sequence
2275
2276        // Ensure deterministic ordering of UTXOs for all peers
2277        included_utxos.sort_by_key(|(_, utxo)| utxo.amount);
2278        remaining_utxos.sort_by_key(|(_, utxo)| utxo.amount);
2279        included_utxos.extend(remaining_utxos);
2280
2281        // Finally we initialize our accumulator for selected input amounts
2282        let mut total_selected_value = bitcoin::Amount::from_sat(0);
2283        let mut selected_utxos: Vec<(UTXOKey, SpendableUTXO)> = vec![];
2284        let mut fees = fee_arithmetic.calculate_fee(fee_rate, total_weight)?;
2285
2286        loop {
2287            // Fees grow with every selected input and are not bounded by `MAX_MONEY`,
2288            // so the target has to be recomputed with checked arithmetic on every
2289            // iteration. A target we cannot even represent is by definition more than
2290            // our UTXOs can cover.
2291            let target = peg_out_amount
2292                .checked_add(change_script.minimal_non_dust())
2293                .and_then(|target| target.checked_add(fees))
2294                .ok_or(WalletOutputError::NotEnoughSpendableUTXO)?;
2295
2296            if total_selected_value >= target {
2297                break;
2298            }
2299
2300            let Some((utxo_key, utxo)) = included_utxos.pop() else {
2301                return Err(WalletOutputError::NotEnoughSpendableUTXO); // Not enough UTXOs
2302            };
2303
2304            total_selected_value += utxo.amount;
2305            total_weight += max_input_weight;
2306            fees = fee_arithmetic.calculate_fee(fee_rate, total_weight)?;
2307            selected_utxos.push((utxo_key, utxo));
2308        }
2309
2310        // We always pay ourselves change back to ensure that we don't lose anything due
2311        // to dust. The change output is always at index `PEG_OUT_CHANGE_VOUT`.
2312        let change = total_selected_value - fees - peg_out_amount;
2313        let output: Vec<TxOut> = vec![
2314            TxOut {
2315                value: peg_out_amount,
2316                script_pubkey: destination.clone(),
2317            },
2318            TxOut {
2319                value: change,
2320                script_pubkey: change_script,
2321            },
2322        ];
2323        let mut change_out = bitcoin::psbt::Output::default();
2324        change_out
2325            .proprietary
2326            .insert(proprietary_tweak_key(), change_tweak.to_vec());
2327
2328        info!(
2329            target: LOG_MODULE_WALLET,
2330            inputs = selected_utxos.len(),
2331            input_sats = total_selected_value.to_sat(),
2332            peg_out_sats = peg_out_amount.to_sat(),
2333            ?total_weight,
2334            fees_sats = fees.to_sat(),
2335            fee_rate = fee_rate.sats_per_kvb,
2336            change_sats = change.to_sat(),
2337            "Creating peg-out tx",
2338        );
2339
2340        let transaction = Transaction {
2341            version: bitcoin::transaction::Version(2),
2342            lock_time: LockTime::ZERO,
2343            input: selected_utxos
2344                .iter()
2345                .map(|(utxo_key, _utxo)| TxIn {
2346                    previous_output: utxo_key.0,
2347                    script_sig: Default::default(),
2348                    sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
2349                    witness: bitcoin::Witness::new(),
2350                })
2351                .collect(),
2352            output,
2353        };
2354        info!(
2355            target: LOG_MODULE_WALLET,
2356            txid = %transaction.compute_txid(), "Creating peg-out tx"
2357        );
2358
2359        // FIXME: use custom data structure that guarantees more invariants and only
2360        // convert to PSBT for finalization
2361        let psbt = Psbt {
2362            unsigned_tx: transaction,
2363            version: 0,
2364            xpub: Default::default(),
2365            proprietary: Default::default(),
2366            unknown: Default::default(),
2367            inputs: selected_utxos
2368                .iter()
2369                .map(|(_utxo_key, utxo)| {
2370                    let script_pubkey = self
2371                        .descriptor
2372                        .tweak(&utxo.tweak, self.secp)
2373                        .script_pubkey();
2374                    Input {
2375                        non_witness_utxo: None,
2376                        witness_utxo: Some(TxOut {
2377                            value: utxo.amount,
2378                            script_pubkey,
2379                        }),
2380                        partial_sigs: Default::default(),
2381                        sighash_type: None,
2382                        redeem_script: None,
2383                        witness_script: Some(
2384                            self.descriptor
2385                                .tweak(&utxo.tweak, self.secp)
2386                                .script_code()
2387                                .expect("Failed to tweak descriptor"),
2388                        ),
2389                        bip32_derivation: Default::default(),
2390                        final_script_sig: None,
2391                        final_script_witness: None,
2392                        ripemd160_preimages: Default::default(),
2393                        sha256_preimages: Default::default(),
2394                        hash160_preimages: Default::default(),
2395                        hash256_preimages: Default::default(),
2396                        proprietary: vec![(proprietary_tweak_key(), utxo.tweak.to_vec())]
2397                            .into_iter()
2398                            .collect(),
2399                        tap_key_sig: Default::default(),
2400                        tap_script_sigs: Default::default(),
2401                        tap_scripts: Default::default(),
2402                        tap_key_origins: Default::default(),
2403                        tap_internal_key: Default::default(),
2404                        tap_merkle_root: Default::default(),
2405                        unknown: Default::default(),
2406                    }
2407                })
2408                .collect(),
2409            outputs: vec![Default::default(), change_out],
2410        };
2411
2412        Ok(UnsignedTransaction {
2413            psbt,
2414            signatures: vec![],
2415            change,
2416            fees: PegOutFees {
2417                fee_rate,
2418                total_weight,
2419            },
2420            destination,
2421            selected_utxos,
2422            peg_out_amount,
2423            rbf,
2424        })
2425    }
2426
2427    fn sign_psbt(&self, psbt: &mut Psbt) {
2428        let mut tx_hasher = SighashCache::new(&psbt.unsigned_tx);
2429
2430        for (idx, (psbt_input, _tx_input)) in psbt
2431            .inputs
2432            .iter_mut()
2433            .zip(psbt.unsigned_tx.input.iter())
2434            .enumerate()
2435        {
2436            let tweaked_secret = {
2437                let tweak = psbt_input
2438                    .proprietary
2439                    .get(&proprietary_tweak_key())
2440                    .expect("Malformed PSBT: expected tweak");
2441
2442                self.secret_key.tweak(tweak, self.secp)
2443            };
2444
2445            let tx_hash = tx_hasher
2446                .p2wsh_signature_hash(
2447                    idx,
2448                    psbt_input
2449                        .witness_script
2450                        .as_ref()
2451                        .expect("Missing witness script"),
2452                    psbt_input
2453                        .witness_utxo
2454                        .as_ref()
2455                        .expect("Missing UTXO")
2456                        .value,
2457                    EcdsaSighashType::All,
2458                )
2459                .expect("Failed to create segwit sighash");
2460
2461            let signature = self.secp.sign_ecdsa(
2462                &Message::from_digest_slice(&tx_hash[..]).unwrap(),
2463                &tweaked_secret,
2464            );
2465
2466            psbt_input.partial_sigs.insert(
2467                bitcoin::PublicKey {
2468                    compressed: true,
2469                    inner: secp256k1::PublicKey::from_secret_key(self.secp, &tweaked_secret),
2470                },
2471                EcdsaSig::sighash_all(signature),
2472            );
2473        }
2474    }
2475
2476    fn derive_script(&self, tweak: &[u8]) -> ScriptBuf {
2477        struct CompressedPublicKeyTranslator<'t, 's, Ctx: Verification> {
2478            tweak: &'t [u8],
2479            secp: &'s Secp256k1<Ctx>,
2480        }
2481
2482        impl<Ctx: Verification>
2483            miniscript::Translator<CompressedPublicKey, CompressedPublicKey, Infallible>
2484            for CompressedPublicKeyTranslator<'_, '_, Ctx>
2485        {
2486            fn pk(&mut self, pk: &CompressedPublicKey) -> Result<CompressedPublicKey, Infallible> {
2487                let hashed_tweak = {
2488                    let mut hasher = HmacEngine::<sha256::Hash>::new(&pk.key.serialize()[..]);
2489                    hasher.input(self.tweak);
2490                    Hmac::from_engine(hasher).to_byte_array()
2491                };
2492
2493                Ok(CompressedPublicKey {
2494                    key: pk
2495                        .key
2496                        .add_exp_tweak(
2497                            self.secp,
2498                            &Scalar::from_be_bytes(hashed_tweak).expect("can't fail"),
2499                        )
2500                        .expect("tweaking failed"),
2501                })
2502            }
2503            translate_hash_fail!(CompressedPublicKey, CompressedPublicKey, Infallible);
2504        }
2505
2506        let descriptor = self
2507            .descriptor
2508            .translate_pk(&mut CompressedPublicKeyTranslator {
2509                tweak,
2510                secp: self.secp,
2511            })
2512            .expect("can't fail");
2513
2514        descriptor.script_pubkey()
2515    }
2516}
2517
2518pub fn nonce_from_idx(nonce_idx: u64) -> [u8; 33] {
2519    let mut nonce: [u8; 33] = [0; 33];
2520    // Make it look like a compressed pubkey, has to be either 0x02 or 0x03
2521    nonce[0] = 0x02;
2522    nonce[1..].copy_from_slice(&nonce_idx.consensus_hash::<bitcoin::hashes::sha256::Hash>()[..]);
2523
2524    nonce
2525}
2526
2527/// A peg-out tx that is ready to be broadcast with a tweak for the change UTXO
2528#[derive(Clone, Debug, Encodable, Decodable)]
2529pub struct PendingTransaction {
2530    pub tx: bitcoin::Transaction,
2531    pub tweak: [u8; 33],
2532    pub change: bitcoin::Amount,
2533    pub destination: ScriptBuf,
2534    pub fees: PegOutFees,
2535    pub selected_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2536    pub peg_out_amount: bitcoin::Amount,
2537    pub rbf: Option<Rbf>,
2538}
2539
2540impl Serialize for PendingTransaction {
2541    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2542    where
2543        S: serde::Serializer,
2544    {
2545        if serializer.is_human_readable() {
2546            serializer.serialize_str(&self.consensus_encode_to_hex())
2547        } else {
2548            serializer.serialize_bytes(&self.consensus_encode_to_vec())
2549        }
2550    }
2551}
2552
2553/// A PSBT that is awaiting enough signatures from the federation to becoming a
2554/// `PendingTransaction`
2555#[derive(Clone, Debug, Eq, PartialEq, Encodable, Decodable)]
2556pub struct UnsignedTransaction {
2557    pub psbt: Psbt,
2558    pub signatures: Vec<(PeerId, PegOutSignatureItem)>,
2559    pub change: bitcoin::Amount,
2560    pub fees: PegOutFees,
2561    pub destination: ScriptBuf,
2562    pub selected_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2563    pub peg_out_amount: bitcoin::Amount,
2564    pub rbf: Option<Rbf>,
2565}
2566
2567impl Serialize for UnsignedTransaction {
2568    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2569    where
2570        S: serde::Serializer,
2571    {
2572        if serializer.is_human_readable() {
2573            serializer.serialize_str(&self.consensus_encode_to_hex())
2574        } else {
2575            serializer.serialize_bytes(&self.consensus_encode_to_vec())
2576        }
2577    }
2578}
2579
2580#[cfg(test)]
2581mod tests {
2582
2583    use std::str::FromStr;
2584
2585    use bitcoin::Network::{Bitcoin, Testnet};
2586    use bitcoin::hashes::Hash;
2587    use bitcoin::{Address, Amount, OutPoint, Txid, secp256k1};
2588    use fedimint_core::Feerate;
2589    use fedimint_core::encoding::btc::NetworkLegacyEncodingWrapper;
2590    use fedimint_core::envs::is_automatic_consensus_version_voting_disabled;
2591    use fedimint_wallet_common::{PegOut, PegOutFees, Rbf, WalletOutputV0};
2592    use miniscript::descriptor::Wsh;
2593
2594    use crate::common::PegInDescriptor;
2595    use crate::{
2596        CompressedPublicKey, FeeArithmetic, OsRng, SpendableUTXO, StatelessWallet, UTXOKey,
2597        WalletOutputError,
2598    };
2599
2600    /// A peg-out recipient is chosen by the user, and nothing stops them from
2601    /// picking the very change script the peg-out will use — change tweaks are
2602    /// `nonce_from_idx` of a counter, so they are publicly derivable. The
2603    /// resulting transaction carries the change script on two outputs, and
2604    /// `recognize_change_utxo` matches on script across all of them, so it
2605    /// tracks an output other than [`PEG_OUT_CHANGE_VOUT`]. Recording a fixed
2606    /// change vout as claimed therefore does not cover everything `UTXOKey`
2607    /// ends up holding; `process_input` has to reject on `UTXOKey` itself.
2608    #[test]
2609    fn peg_out_destination_can_collide_with_the_change_script() {
2610        let secp = secp256k1::Secp256k1::new();
2611
2612        let descriptor = PegInDescriptor::Wsh(
2613            Wsh::new_sortedmulti(
2614                3,
2615                (0..4)
2616                    .map(|_| secp.generate_keypair(&mut OsRng))
2617                    .map(|(_, key)| CompressedPublicKey { key })
2618                    .collect(),
2619            )
2620            .unwrap(),
2621        );
2622
2623        let (secret_key, _) = secp.generate_keypair(&mut OsRng);
2624
2625        let wallet = StatelessWallet {
2626            descriptor: &descriptor,
2627            secret_key: &secret_key,
2628            secp: &secp,
2629        };
2630
2631        let change_tweak = crate::nonce_from_idx(0);
2632        let change_script = wallet.derive_script(&change_tweak);
2633
2634        let tx = wallet
2635            .create_tx(
2636                Amount::from_sat(1000),
2637                change_script.clone(),
2638                vec![],
2639                vec![(
2640                    UTXOKey(OutPoint::null()),
2641                    SpendableUTXO {
2642                        tweak: [0; 33],
2643                        amount: bitcoin::Amount::from_sat(100_000),
2644                    },
2645                )],
2646                Feerate { sats_per_kvb: 1000 },
2647                &change_tweak,
2648                None,
2649                FeeArithmetic::Checked,
2650            )
2651            .expect("tx creation succeeds");
2652
2653        let matching = tx
2654            .psbt
2655            .unsigned_tx
2656            .output
2657            .iter()
2658            .filter(|o| o.script_pubkey == change_script)
2659            .count();
2660
2661        assert_eq!(
2662            matching, 2,
2663            "both the destination and the change output carry the change script"
2664        );
2665    }
2666
2667    /// Pre-2.3 the fee multiplication wrapped in release profiles, so a peg-out
2668    /// declaring an enormous rate was accepted and broadcast with a fee too low
2669    /// to confirm. From 2.3 on it is rejected. Both regimes must keep working:
2670    /// replaying a pre-2.3 session has to reproduce the acceptance.
2671    #[test]
2672    fn fee_arithmetic_rejects_an_unpayable_rate_only_once_active() {
2673        let secp = secp256k1::Secp256k1::new();
2674
2675        let descriptor = PegInDescriptor::Wsh(
2676            Wsh::new_sortedmulti(
2677                3,
2678                (0..4)
2679                    .map(|_| secp.generate_keypair(&mut OsRng))
2680                    .map(|(_, key)| CompressedPublicKey { key })
2681                    .collect(),
2682            )
2683            .unwrap(),
2684        );
2685
2686        let absurd = Feerate {
2687            sats_per_kvb: u64::MAX,
2688        };
2689        let ordinary = Feerate { sats_per_kvb: 1000 };
2690
2691        assert_eq!(
2692            FeeArithmetic::Checked.calculate_fee(absurd, 958),
2693            Err(WalletOutputError::NotEnoughSpendableUTXO),
2694            "an unpayable rate is rejected once 2.3 is active"
2695        );
2696        assert_eq!(
2697            FeeArithmetic::Wrapping.calculate_fee(absurd, 958),
2698            Ok(absurd.wrapping_calculate_fee(958)),
2699            "pre-2.3 behaviour is reproduced exactly, wrap and all"
2700        );
2701
2702        for arithmetic in [FeeArithmetic::Checked, FeeArithmetic::Wrapping] {
2703            assert_eq!(
2704                arithmetic.calculate_fee(ordinary, 958),
2705                Ok(ordinary.calculate_fee(958)),
2706                "an ordinary rate is unaffected in either regime"
2707            );
2708        }
2709
2710        let _ = descriptor;
2711    }
2712
2713    /// A peg-out amount reaches `create_tx` straight from an unauthenticated
2714    /// caller, both via `PEG_OUT_FEES_ENDPOINT` and via a submitted peg-out
2715    /// output. Summing it with the dust limit and the fees used to overflow
2716    /// `bitcoin::Amount`'s panicking `Add`, killing the guardian process.
2717    #[test]
2718    fn create_tx_rejects_amounts_that_cannot_exist_on_chain() {
2719        let secp = secp256k1::Secp256k1::new();
2720
2721        let descriptor = PegInDescriptor::Wsh(
2722            Wsh::new_sortedmulti(
2723                3,
2724                (0..4)
2725                    .map(|_| secp.generate_keypair(&mut OsRng))
2726                    .map(|(_, key)| CompressedPublicKey { key })
2727                    .collect(),
2728            )
2729            .unwrap(),
2730        );
2731
2732        let (secret_key, _) = secp.generate_keypair(&mut OsRng);
2733
2734        let wallet = StatelessWallet {
2735            descriptor: &descriptor,
2736            secret_key: &secret_key,
2737            secp: &secp,
2738        };
2739
2740        let recipient = Address::from_str("32iVBEu4dxkUQk9dJbZUiBiQdmypcEyJRf").unwrap();
2741        let utxos = vec![(
2742            UTXOKey(OutPoint::null()),
2743            SpendableUTXO {
2744                tweak: [0; 33],
2745                amount: bitcoin::Amount::from_sat(100_000),
2746            },
2747        )];
2748
2749        for amount in [
2750            Amount::from_sat(u64::MAX),
2751            Amount::MAX_MONEY + Amount::from_sat(1),
2752        ] {
2753            let tx = wallet.create_tx(
2754                amount,
2755                recipient.clone().assume_checked().script_pubkey(),
2756                vec![],
2757                utxos.clone(),
2758                Feerate { sats_per_kvb: 1000 },
2759                &[0; 33],
2760                None,
2761                FeeArithmetic::Checked,
2762            );
2763
2764            assert_eq!(tx, Err(WalletOutputError::NotEnoughSpendableUTXO));
2765        }
2766    }
2767
2768    #[test]
2769    fn create_tx_should_validate_amounts() {
2770        let secp = secp256k1::Secp256k1::new();
2771
2772        let descriptor = PegInDescriptor::Wsh(
2773            Wsh::new_sortedmulti(
2774                3,
2775                (0..4)
2776                    .map(|_| secp.generate_keypair(&mut OsRng))
2777                    .map(|(_, key)| CompressedPublicKey { key })
2778                    .collect(),
2779            )
2780            .unwrap(),
2781        );
2782
2783        let (secret_key, _) = secp.generate_keypair(&mut OsRng);
2784
2785        let wallet = StatelessWallet {
2786            descriptor: &descriptor,
2787            secret_key: &secret_key,
2788            secp: &secp,
2789        };
2790
2791        let spendable = SpendableUTXO {
2792            tweak: [0; 33],
2793            amount: bitcoin::Amount::from_sat(3000),
2794        };
2795
2796        let recipient = Address::from_str("32iVBEu4dxkUQk9dJbZUiBiQdmypcEyJRf").unwrap();
2797
2798        let fee = Feerate { sats_per_kvb: 1000 };
2799        let weight = 875;
2800
2801        // not enough SpendableUTXO
2802        // tx fee = ceil(875 / 4) * 1 sat/vb = 219
2803        // change script dust = 330
2804        // spendable sats = 3000 - 219 - 330 = 2451
2805        let tx = wallet.create_tx(
2806            Amount::from_sat(2452),
2807            recipient.clone().assume_checked().script_pubkey(),
2808            vec![],
2809            vec![(UTXOKey(OutPoint::null()), spendable.clone())],
2810            fee,
2811            &[0; 33],
2812            None,
2813            FeeArithmetic::Checked,
2814        );
2815        assert_eq!(tx, Err(WalletOutputError::NotEnoughSpendableUTXO));
2816
2817        // successful tx creation
2818        let mut tx = wallet
2819            .create_tx(
2820                Amount::from_sat(1000),
2821                recipient.clone().assume_checked().script_pubkey(),
2822                vec![],
2823                vec![(UTXOKey(OutPoint::null()), spendable)],
2824                fee,
2825                &[0; 33],
2826                None,
2827                FeeArithmetic::Checked,
2828            )
2829            .expect("is ok");
2830
2831        // peg out weight is incorrectly set to 0
2832        let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, 0), fee, Bitcoin);
2833        assert_eq!(res, Err(WalletOutputError::TxWeightIncorrect(0, weight)));
2834
2835        // fee rate set below min relay fee to 0
2836        let res = StatelessWallet::validate_tx(&tx, &rbf(0, weight), fee, Bitcoin);
2837        assert_eq!(res, Err(WalletOutputError::BelowMinRelayFee));
2838
2839        // fees are okay
2840        let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2841        assert_eq!(res, Ok(()));
2842
2843        // tx has fee below consensus
2844        tx.fees = PegOutFees::new(0, weight);
2845        let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2846        assert_eq!(
2847            res,
2848            Err(WalletOutputError::PegOutFeeBelowConsensus(
2849                Feerate { sats_per_kvb: 0 },
2850                fee
2851            ))
2852        );
2853
2854        // tx has peg-out amount under dust limit
2855        tx.peg_out_amount = bitcoin::Amount::ZERO;
2856        let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2857        assert_eq!(res, Err(WalletOutputError::PegOutUnderDustLimit));
2858
2859        // tx is invalid for network
2860        let output = WalletOutputV0::PegOut(PegOut {
2861            recipient,
2862            amount: bitcoin::Amount::from_sat(1000),
2863            fees: PegOutFees::new(100, weight),
2864        });
2865        let res = StatelessWallet::validate_tx(&tx, &output, fee, Testnet);
2866        assert_eq!(
2867            res,
2868            Err(WalletOutputError::WrongNetwork(
2869                NetworkLegacyEncodingWrapper(Testnet),
2870                NetworkLegacyEncodingWrapper(Bitcoin)
2871            ))
2872        );
2873    }
2874
2875    fn rbf(sats_per_kvb: u64, total_weight: u64) -> WalletOutputV0 {
2876        WalletOutputV0::Rbf(Rbf {
2877            fees: PegOutFees::new(sats_per_kvb, total_weight),
2878            txid: Txid::all_zeros(),
2879        })
2880    }
2881
2882    #[test]
2883    fn automatic_vote_suppressed_when_env_set() {
2884        unsafe {
2885            std::env::set_var("FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING", "1");
2886        }
2887        assert!(is_automatic_consensus_version_voting_disabled());
2888        unsafe {
2889            std::env::remove_var("FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING");
2890        }
2891    }
2892
2893    #[test]
2894    fn automatic_vote_active_when_env_unset() {
2895        unsafe {
2896            std::env::remove_var("FM_WALLET_DISABLE_AUTOMATIC_CONSENSUS_VERSION_VOTING");
2897        }
2898        assert!(!is_automatic_consensus_version_voting_disabled());
2899    }
2900}