fedimint_wallet_server/
lib.rs

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