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