fedimint_wallet_server/
lib.rs

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