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