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