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