fedimint_wallet_server/
lib.rs

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