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, PeerHandle,
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::config::distributedgen::PeerHandleOps;
69use fedimint_server::core::migration::ServerModuleDbMigrationFn;
70use fedimint_server::core::{ServerModule, ServerModuleInit, ServerModuleInitArgs};
71use fedimint_server::net::api::check_auth;
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: &PeerHandle,
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    secp: Secp256k1<All>,
1007    btc_rpc: DynBitcoindRpc,
1008    our_peer_id: PeerId,
1009    /// Block count updated periodically by a background task
1010    block_count_rx: watch::Receiver<Option<u64>>,
1011    /// Fee rate updated periodically by a background task
1012    fee_rate_rx: watch::Receiver<Option<Feerate>>,
1013
1014    /// Broadcasting pending txes can be triggered immediately with this
1015    broadcast_pending: Arc<Notify>,
1016
1017    task_group: TaskGroup,
1018    /// Maximum consensus version supported by *all* our peers. Used to
1019    /// automatically activate new consensus versions as soon as everyone
1020    /// upgrades.
1021    peer_supported_consensus_version: watch::Receiver<Option<ModuleConsensusVersion>>,
1022}
1023
1024impl Wallet {
1025    pub async fn new(
1026        cfg: WalletConfig,
1027        db: &Database,
1028        task_group: &TaskGroup,
1029        our_peer_id: PeerId,
1030        module_api: DynModuleApi,
1031        shared_bitcoin: &ServerModuleSharedBitcoin,
1032    ) -> anyhow::Result<Wallet> {
1033        let btc_rpc = create_bitcoind(&cfg.local.bitcoin_rpc)?;
1034        Ok(Self::new_with_bitcoind(
1035            cfg,
1036            db,
1037            btc_rpc,
1038            task_group,
1039            our_peer_id,
1040            module_api,
1041            shared_bitcoin,
1042        )
1043        .await?)
1044    }
1045
1046    pub async fn new_with_bitcoind(
1047        cfg: WalletConfig,
1048        db: &Database,
1049        btc_rpc: DynBitcoindRpc,
1050        task_group: &TaskGroup,
1051        our_peer_id: PeerId,
1052        module_api: DynModuleApi,
1053        shared_bitcoin: &ServerModuleSharedBitcoin,
1054    ) -> Result<Wallet, WalletCreationError> {
1055        let fee_rate_rx = shared_bitcoin
1056            .feerate_receiver(cfg.consensus.network.0, btc_rpc.clone())
1057            .await
1058            .map_err(|e| {
1059                WalletCreationError::FeerateSourceError(e.fmt_compact_anyhow().to_string())
1060            })?;
1061        let block_count_rx = shared_bitcoin
1062            .block_count_receiver(cfg.consensus.network.0, btc_rpc.clone())
1063            .await;
1064        let broadcast_pending = Arc::new(Notify::new());
1065        Self::spawn_broadcast_pending_task(task_group, &btc_rpc, db, broadcast_pending.clone());
1066
1067        let peer_supported_consensus_version =
1068            Self::spawn_peer_supported_consensus_version_task(module_api, task_group, our_peer_id);
1069
1070        let bitcoind_net = NetworkLegacyEncodingWrapper(
1071            retry("verify network", backoff_util::aggressive_backoff(), || {
1072                btc_rpc.get_network()
1073            })
1074            .await
1075            .map_err(|e| WalletCreationError::RpcError(e.to_string()))?,
1076        );
1077        if bitcoind_net != cfg.consensus.network {
1078            return Err(WalletCreationError::WrongNetwork(
1079                cfg.consensus.network,
1080                bitcoind_net,
1081            ));
1082        }
1083
1084        let wallet = Wallet {
1085            cfg,
1086            secp: Default::default(),
1087            btc_rpc,
1088            our_peer_id,
1089            block_count_rx,
1090            fee_rate_rx,
1091            task_group: task_group.clone(),
1092            peer_supported_consensus_version,
1093            broadcast_pending,
1094        };
1095
1096        Ok(wallet)
1097    }
1098
1099    /// Try to attach signatures to a pending peg-out tx.
1100    fn sign_peg_out_psbt(
1101        &self,
1102        psbt: &mut Psbt,
1103        peer: PeerId,
1104        signature: &PegOutSignatureItem,
1105    ) -> Result<(), ProcessPegOutSigError> {
1106        let peer_key = self
1107            .cfg
1108            .consensus
1109            .peer_peg_in_keys
1110            .get(&peer)
1111            .expect("always called with valid peer id");
1112
1113        if psbt.inputs.len() != signature.signature.len() {
1114            return Err(ProcessPegOutSigError::WrongSignatureCount(
1115                psbt.inputs.len(),
1116                signature.signature.len(),
1117            ));
1118        }
1119
1120        let mut tx_hasher = SighashCache::new(&psbt.unsigned_tx);
1121        for (idx, (input, signature)) in psbt
1122            .inputs
1123            .iter_mut()
1124            .zip(signature.signature.iter())
1125            .enumerate()
1126        {
1127            let tx_hash = tx_hasher
1128                .p2wsh_signature_hash(
1129                    idx,
1130                    input
1131                        .witness_script
1132                        .as_ref()
1133                        .expect("Missing witness script"),
1134                    input.witness_utxo.as_ref().expect("Missing UTXO").value,
1135                    EcdsaSighashType::All,
1136                )
1137                .map_err(|_| ProcessPegOutSigError::SighashError)?;
1138
1139            let tweak = input
1140                .proprietary
1141                .get(&proprietary_tweak_key())
1142                .expect("we saved it with a tweak");
1143
1144            let tweaked_peer_key = peer_key.tweak(tweak, &self.secp);
1145            self.secp
1146                .verify_ecdsa(
1147                    &Message::from_digest_slice(&tx_hash[..]).unwrap(),
1148                    signature,
1149                    &tweaked_peer_key.key,
1150                )
1151                .map_err(|_| ProcessPegOutSigError::InvalidSignature)?;
1152
1153            if input
1154                .partial_sigs
1155                .insert(tweaked_peer_key.into(), EcdsaSig::sighash_all(*signature))
1156                .is_some()
1157            {
1158                // Should never happen since peers only sign a PSBT once
1159                return Err(ProcessPegOutSigError::DuplicateSignature);
1160            }
1161        }
1162        Ok(())
1163    }
1164
1165    fn finalize_peg_out_psbt(
1166        &self,
1167        mut unsigned: UnsignedTransaction,
1168    ) -> Result<PendingTransaction, ProcessPegOutSigError> {
1169        // We need to save the change output's tweak key to be able to access the funds
1170        // later on. The tweak is extracted here because the psbt is moved next
1171        // and not available anymore when the tweak is actually needed in the
1172        // end to be put into the batch on success.
1173        let change_tweak: [u8; 33] = unsigned
1174            .psbt
1175            .outputs
1176            .iter()
1177            .find_map(|output| output.proprietary.get(&proprietary_tweak_key()).cloned())
1178            .ok_or(ProcessPegOutSigError::MissingOrMalformedChangeTweak)?
1179            .try_into()
1180            .map_err(|_| ProcessPegOutSigError::MissingOrMalformedChangeTweak)?;
1181
1182        if let Err(error) = unsigned.psbt.finalize_mut(&self.secp) {
1183            return Err(ProcessPegOutSigError::ErrorFinalizingPsbt(error));
1184        }
1185
1186        let tx = unsigned.psbt.clone().extract_tx_unchecked_fee_rate();
1187
1188        Ok(PendingTransaction {
1189            tx,
1190            tweak: change_tweak,
1191            change: unsigned.change,
1192            destination: unsigned.destination,
1193            fees: unsigned.fees,
1194            selected_utxos: unsigned.selected_utxos,
1195            peg_out_amount: unsigned.peg_out_amount,
1196            rbf: unsigned.rbf,
1197        })
1198    }
1199
1200    fn get_block_count(&self) -> anyhow::Result<u32> {
1201        self.block_count_rx
1202            .borrow()
1203            .ok_or_else(|| format_err!("Block count not available yet"))
1204            .and_then(|block_count| {
1205                block_count
1206                    .try_into()
1207                    .map_err(|_| format_err!("Block count exceeds u32 limits"))
1208            })
1209    }
1210
1211    pub fn get_fee_rate_opt(&self) -> Feerate {
1212        // `get_feerate_multiplier` is clamped and can't be negative
1213        // feerate sources as clamped and can't be negative or too large
1214        #[allow(clippy::cast_precision_loss)]
1215        #[allow(clippy::cast_sign_loss)]
1216        Feerate {
1217            sats_per_kvb: ((self
1218                .fee_rate_rx
1219                .borrow()
1220                .unwrap_or(self.cfg.consensus.default_fee)
1221                .sats_per_kvb as f64
1222                * get_feerate_multiplier())
1223            .round()) as u64,
1224        }
1225    }
1226
1227    pub async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u32 {
1228        let peer_count = self.cfg.consensus.peer_peg_in_keys.to_num_peers().total();
1229
1230        let mut counts = dbtx
1231            .find_by_prefix(&BlockCountVotePrefix)
1232            .await
1233            .map(|entry| entry.1)
1234            .collect::<Vec<u32>>()
1235            .await;
1236
1237        assert!(counts.len() <= peer_count);
1238
1239        while counts.len() < peer_count {
1240            counts.push(0);
1241        }
1242
1243        counts.sort_unstable();
1244
1245        counts[peer_count / 2]
1246    }
1247
1248    pub async fn consensus_fee_rate(&self, dbtx: &mut DatabaseTransaction<'_>) -> Feerate {
1249        let peer_count = self.cfg.consensus.peer_peg_in_keys.to_num_peers().total();
1250
1251        let mut rates = dbtx
1252            .find_by_prefix(&FeeRateVotePrefix)
1253            .await
1254            .map(|(.., rate)| rate)
1255            .collect::<Vec<_>>()
1256            .await;
1257
1258        assert!(rates.len() <= peer_count);
1259
1260        while rates.len() < peer_count {
1261            rates.push(self.cfg.consensus.default_fee);
1262        }
1263
1264        rates.sort_unstable();
1265
1266        rates[peer_count / 2]
1267    }
1268
1269    async fn consensus_module_consensus_version(
1270        &self,
1271        dbtx: &mut DatabaseTransaction<'_>,
1272    ) -> ModuleConsensusVersion {
1273        let num_peers = self.cfg.consensus.peer_peg_in_keys.to_num_peers();
1274
1275        let mut versions = dbtx
1276            .find_by_prefix(&ConsensusVersionVotePrefix)
1277            .await
1278            .map(|entry| entry.1)
1279            .collect::<Vec<ModuleConsensusVersion>>()
1280            .await;
1281
1282        while versions.len() < num_peers.total() {
1283            versions.push(ModuleConsensusVersion::new(2, 0));
1284        }
1285
1286        assert_eq!(versions.len(), num_peers.total());
1287
1288        versions.sort_unstable();
1289
1290        assert!(versions.first() <= versions.last());
1291
1292        versions[num_peers.max_evil()]
1293    }
1294
1295    pub async fn consensus_nonce(&self, dbtx: &mut DatabaseTransaction<'_>) -> [u8; 33] {
1296        let nonce_idx = dbtx.get_value(&PegOutNonceKey).await.unwrap_or(0);
1297        dbtx.insert_entry(&PegOutNonceKey, &(nonce_idx + 1)).await;
1298
1299        nonce_from_idx(nonce_idx)
1300    }
1301
1302    async fn sync_up_to_consensus_count<'a>(
1303        &self,
1304        dbtx: &mut DatabaseTransaction<'a>,
1305        old_count: u32,
1306        new_count: u32,
1307    ) {
1308        info!(
1309            target: LOG_MODULE_WALLET,
1310            new_count,
1311            blocks_to_go = new_count - old_count,
1312            "New block count consensus, syncing up",
1313        );
1314
1315        // Before we can safely call our bitcoin backend to process the new consensus
1316        // count, we need to ensure we observed enough confirmations
1317        self.wait_for_finality_confs_or_shutdown(new_count).await;
1318
1319        for height in old_count..new_count {
1320            if height % 100 == 0 {
1321                debug!(
1322                    target: LOG_MODULE_WALLET,
1323                    "Caught up to block {height}"
1324                );
1325            }
1326
1327            // TODO: use batching for mainnet syncing
1328            trace!(block = height, "Fetching block hash");
1329            let block_hash = retry("get_block_hash", backoff_util::background_backoff(), || {
1330                self.btc_rpc.get_block_hash(u64::from(height)) // TODO: use u64 for height everywhere
1331            })
1332            .await
1333            .expect("bitcoind rpc to get block hash");
1334
1335            if self.consensus_module_consensus_version(dbtx).await
1336                >= ModuleConsensusVersion::new(2, 2)
1337            {
1338                let block = retry("get_block", backoff_util::background_backoff(), || {
1339                    self.btc_rpc.get_block(&block_hash)
1340                })
1341                .await
1342                .expect("bitcoind rpc to get block");
1343
1344                for transaction in block.txdata {
1345                    // We maintain the subset of unspent P2WSH transaction outputs created
1346                    // since the module was running on the new consensus version, which might be
1347                    // the same time as the genesis session.
1348
1349                    for tx_in in &transaction.input {
1350                        dbtx.remove_entry(&UnspentTxOutKey(tx_in.previous_output))
1351                            .await;
1352                    }
1353
1354                    for (vout, tx_out) in transaction.output.iter().enumerate() {
1355                        let should_track_utxo = if self.cfg.consensus.peer_peg_in_keys.len() > 1 {
1356                            tx_out.script_pubkey.is_p2wsh()
1357                        } else {
1358                            tx_out.script_pubkey.is_p2wpkh()
1359                        };
1360
1361                        if should_track_utxo {
1362                            let outpoint = bitcoin::OutPoint {
1363                                txid: transaction.compute_txid(),
1364                                vout: vout as u32,
1365                            };
1366
1367                            dbtx.insert_new_entry(&UnspentTxOutKey(outpoint), tx_out)
1368                                .await;
1369                        }
1370                    }
1371                }
1372            }
1373
1374            let pending_transactions = dbtx
1375                .find_by_prefix(&PendingTransactionPrefixKey)
1376                .await
1377                .map(|(key, transaction)| (key.0, transaction))
1378                .collect::<HashMap<Txid, PendingTransaction>>()
1379                .await;
1380            let pending_transactions_len = pending_transactions.len();
1381
1382            debug!(
1383                target: LOG_MODULE_WALLET,
1384                ?height,
1385                ?pending_transactions_len,
1386                "Recognizing change UTXOs"
1387            );
1388            for (txid, tx) in &pending_transactions {
1389                let is_tx_in_block =
1390                    retry("is_tx_in_block", backoff_util::background_backoff(), || {
1391                        self.btc_rpc
1392                            .is_tx_in_block(txid, &block_hash, u64::from(height))
1393                    })
1394                    .await
1395                    .unwrap_or_else(|_| {
1396                        panic!("Failed checking if tx is in block height {height}")
1397                    });
1398
1399                if is_tx_in_block {
1400                    debug!(
1401                        target: LOG_MODULE_WALLET,
1402                        ?txid, ?height, ?block_hash, "Recognizing change UTXO"
1403                    );
1404                    self.recognize_change_utxo(dbtx, tx).await;
1405                } else {
1406                    debug!(
1407                        target: LOG_MODULE_WALLET,
1408                        ?txid,
1409                        ?height,
1410                        ?block_hash,
1411                        "Pending transaction not yet confirmed in this block"
1412                    );
1413                }
1414            }
1415
1416            dbtx.insert_new_entry(&BlockHashKey(block_hash), &()).await;
1417        }
1418    }
1419
1420    /// Add a change UTXO to our spendable UTXO database after it was included
1421    /// in a block that we got consensus on.
1422    async fn recognize_change_utxo<'a>(
1423        &self,
1424        dbtx: &mut DatabaseTransaction<'a>,
1425        pending_tx: &PendingTransaction,
1426    ) {
1427        self.remove_rbf_transactions(dbtx, pending_tx).await;
1428
1429        let script_pk = self
1430            .cfg
1431            .consensus
1432            .peg_in_descriptor
1433            .tweak(&pending_tx.tweak, &self.secp)
1434            .script_pubkey();
1435        for (idx, output) in pending_tx.tx.output.iter().enumerate() {
1436            if output.script_pubkey == script_pk {
1437                dbtx.insert_entry(
1438                    &UTXOKey(bitcoin::OutPoint {
1439                        txid: pending_tx.tx.compute_txid(),
1440                        vout: idx as u32,
1441                    }),
1442                    &SpendableUTXO {
1443                        tweak: pending_tx.tweak,
1444                        amount: output.value,
1445                    },
1446                )
1447                .await;
1448            }
1449        }
1450    }
1451
1452    /// Removes the `PendingTransaction` and any transactions tied to it via RBF
1453    async fn remove_rbf_transactions<'a>(
1454        &self,
1455        dbtx: &mut DatabaseTransaction<'a>,
1456        pending_tx: &PendingTransaction,
1457    ) {
1458        let mut all_transactions: BTreeMap<Txid, PendingTransaction> = dbtx
1459            .find_by_prefix(&PendingTransactionPrefixKey)
1460            .await
1461            .map(|(key, val)| (key.0, val))
1462            .collect::<BTreeMap<Txid, PendingTransaction>>()
1463            .await;
1464
1465        // We need to search and remove all `PendingTransactions` invalidated by RBF
1466        let mut pending_to_remove = vec![pending_tx.clone()];
1467        while let Some(removed) = pending_to_remove.pop() {
1468            all_transactions.remove(&removed.tx.compute_txid());
1469            dbtx.remove_entry(&PendingTransactionKey(removed.tx.compute_txid()))
1470                .await;
1471
1472            // Search for tx that this `removed` has as RBF
1473            if let Some(rbf) = &removed.rbf {
1474                if let Some(tx) = all_transactions.get(&rbf.txid) {
1475                    pending_to_remove.push(tx.clone());
1476                }
1477            }
1478
1479            // Search for tx that wanted to RBF the `removed` one
1480            for tx in all_transactions.values() {
1481                if let Some(rbf) = &tx.rbf {
1482                    if rbf.txid == removed.tx.compute_txid() {
1483                        pending_to_remove.push(tx.clone());
1484                    }
1485                }
1486            }
1487        }
1488    }
1489
1490    async fn block_is_known(
1491        &self,
1492        dbtx: &mut DatabaseTransaction<'_>,
1493        block_hash: BlockHash,
1494    ) -> bool {
1495        dbtx.get_value(&BlockHashKey(block_hash)).await.is_some()
1496    }
1497
1498    async fn create_peg_out_tx(
1499        &self,
1500        dbtx: &mut DatabaseTransaction<'_>,
1501        output: &WalletOutputV0,
1502        change_tweak: &[u8; 33],
1503    ) -> Result<UnsignedTransaction, WalletOutputError> {
1504        match output {
1505            WalletOutputV0::PegOut(peg_out) => self.offline_wallet().create_tx(
1506                peg_out.amount,
1507                // Note: While calling `assume_checked()` is generally unwise, checking the
1508                // network here could be a consensus-breaking change. Ignoring the network
1509                // is fine here since we validate it in `process_output()`.
1510                peg_out.recipient.clone().assume_checked().script_pubkey(),
1511                vec![],
1512                self.available_utxos(dbtx).await,
1513                peg_out.fees.fee_rate,
1514                change_tweak,
1515                None,
1516            ),
1517            WalletOutputV0::Rbf(rbf) => {
1518                let tx = dbtx
1519                    .get_value(&PendingTransactionKey(rbf.txid))
1520                    .await
1521                    .ok_or(WalletOutputError::RbfTransactionIdNotFound)?;
1522
1523                self.offline_wallet().create_tx(
1524                    tx.peg_out_amount,
1525                    tx.destination,
1526                    tx.selected_utxos,
1527                    self.available_utxos(dbtx).await,
1528                    tx.fees.fee_rate,
1529                    change_tweak,
1530                    Some(rbf.clone()),
1531                )
1532            }
1533        }
1534    }
1535
1536    async fn available_utxos(
1537        &self,
1538        dbtx: &mut DatabaseTransaction<'_>,
1539    ) -> Vec<(UTXOKey, SpendableUTXO)> {
1540        dbtx.find_by_prefix(&UTXOPrefixKey)
1541            .await
1542            .collect::<Vec<(UTXOKey, SpendableUTXO)>>()
1543            .await
1544    }
1545
1546    pub async fn get_wallet_value(&self, dbtx: &mut DatabaseTransaction<'_>) -> bitcoin::Amount {
1547        let sat_sum = self
1548            .available_utxos(dbtx)
1549            .await
1550            .into_iter()
1551            .map(|(_, utxo)| utxo.amount.to_sat())
1552            .sum();
1553        bitcoin::Amount::from_sat(sat_sum)
1554    }
1555
1556    async fn get_wallet_summary(&self, dbtx: &mut DatabaseTransaction<'_>) -> WalletSummary {
1557        fn partition_peg_out_and_change(
1558            transactions: Vec<Transaction>,
1559        ) -> (Vec<TxOutputSummary>, Vec<TxOutputSummary>) {
1560            let mut peg_out_txos: Vec<TxOutputSummary> = Vec::new();
1561            let mut change_utxos: Vec<TxOutputSummary> = Vec::new();
1562
1563            for tx in transactions {
1564                let txid = tx.compute_txid();
1565
1566                // to identify outputs for the peg_out (idx = 0) and change (idx = 1), we lean
1567                // on how the wallet constructs the transaction
1568                let peg_out_output = tx
1569                    .output
1570                    .first()
1571                    .expect("tx must contain withdrawal output");
1572
1573                let change_output = tx.output.last().expect("tx must contain change output");
1574
1575                peg_out_txos.push(TxOutputSummary {
1576                    outpoint: bitcoin::OutPoint { txid, vout: 0 },
1577                    amount: peg_out_output.value,
1578                });
1579
1580                change_utxos.push(TxOutputSummary {
1581                    outpoint: bitcoin::OutPoint { txid, vout: 1 },
1582                    amount: change_output.value,
1583                });
1584            }
1585
1586            (peg_out_txos, change_utxos)
1587        }
1588
1589        let spendable_utxos = self
1590            .available_utxos(dbtx)
1591            .await
1592            .iter()
1593            .map(|(utxo_key, spendable_utxo)| TxOutputSummary {
1594                outpoint: utxo_key.0,
1595                amount: spendable_utxo.amount,
1596            })
1597            .collect::<Vec<_>>();
1598
1599        // constructed peg-outs without threshold signatures
1600        let unsigned_transactions = dbtx
1601            .find_by_prefix(&UnsignedTransactionPrefixKey)
1602            .await
1603            .map(|(_tx_key, tx)| tx.psbt.unsigned_tx)
1604            .collect::<Vec<_>>()
1605            .await;
1606
1607        // peg-outs with threshold signatures, awaiting finality delay confirmations
1608        let unconfirmed_transactions = dbtx
1609            .find_by_prefix(&PendingTransactionPrefixKey)
1610            .await
1611            .map(|(_tx_key, tx)| tx.tx)
1612            .collect::<Vec<_>>()
1613            .await;
1614
1615        let (unsigned_peg_out_txos, unsigned_change_utxos) =
1616            partition_peg_out_and_change(unsigned_transactions);
1617
1618        let (unconfirmed_peg_out_txos, unconfirmed_change_utxos) =
1619            partition_peg_out_and_change(unconfirmed_transactions);
1620
1621        WalletSummary {
1622            spendable_utxos,
1623            unsigned_peg_out_txos,
1624            unsigned_change_utxos,
1625            unconfirmed_peg_out_txos,
1626            unconfirmed_change_utxos,
1627        }
1628    }
1629
1630    async fn is_utxo_confirmed(
1631        &self,
1632        dbtx: &mut DatabaseTransaction<'_>,
1633        outpoint: bitcoin::OutPoint,
1634    ) -> bool {
1635        dbtx.get_value(&UnspentTxOutKey(outpoint)).await.is_some()
1636    }
1637
1638    fn offline_wallet(&self) -> StatelessWallet {
1639        StatelessWallet {
1640            descriptor: &self.cfg.consensus.peg_in_descriptor,
1641            secret_key: &self.cfg.private.peg_in_key,
1642            secp: &self.secp,
1643        }
1644    }
1645
1646    fn spawn_broadcast_pending_task(
1647        task_group: &TaskGroup,
1648        bitcoind: &DynBitcoindRpc,
1649        db: &Database,
1650        broadcast_pending_notify: Arc<Notify>,
1651    ) {
1652        task_group.spawn_cancellable("broadcast pending", {
1653            let bitcoind = bitcoind.clone();
1654            let db = db.clone();
1655            run_broadcast_pending_tx(db, bitcoind, broadcast_pending_notify)
1656        });
1657    }
1658
1659    /// Shutdown the task group shared throughout fedimintd, giving 60 seconds
1660    /// for other services to gracefully shutdown.
1661    async fn graceful_shutdown(&self) {
1662        if let Err(e) = self
1663            .task_group
1664            .clone()
1665            .shutdown_join_all(Some(Duration::from_secs(60)))
1666            .await
1667        {
1668            panic!("Error while shutting down fedimintd task group: {e}");
1669        }
1670    }
1671
1672    /// Returns once our bitcoin backend observes finality delay confirmations
1673    /// of the consensus block count. If we don't observe enough confirmations
1674    /// after one hour, we gracefully shutdown fedimintd. This is necessary
1675    /// since we can no longer participate in consensus if our bitcoin backend
1676    /// is unable to observe the same chain tip as our peers.
1677    async fn wait_for_finality_confs_or_shutdown(&self, consensus_block_count: u32) {
1678        let backoff = if is_running_in_test_env() {
1679            // every 100ms for 60s
1680            backoff_util::custom_backoff(
1681                Duration::from_millis(100),
1682                Duration::from_millis(100),
1683                Some(10 * 60),
1684            )
1685        } else {
1686            // every max 10s for 1 hour
1687            backoff_util::fibonacci_max_one_hour()
1688        };
1689
1690        let wait_for_finality_confs = || async {
1691            let our_chain_tip_block_count = self.get_block_count()?;
1692            let consensus_chain_tip_block_count =
1693                consensus_block_count + self.cfg.consensus.finality_delay;
1694
1695            if consensus_chain_tip_block_count <= our_chain_tip_block_count {
1696                Ok(())
1697            } else {
1698                Err(anyhow::anyhow!("not enough confirmations"))
1699            }
1700        };
1701
1702        if retry("wait_for_finality_confs", backoff, wait_for_finality_confs)
1703            .await
1704            .is_err()
1705        {
1706            self.graceful_shutdown().await;
1707        }
1708    }
1709
1710    fn spawn_peer_supported_consensus_version_task(
1711        api_client: DynModuleApi,
1712        task_group: &TaskGroup,
1713        our_peer_id: PeerId,
1714    ) -> watch::Receiver<Option<ModuleConsensusVersion>> {
1715        let (sender, receiver) = watch::channel(None);
1716        task_group.spawn_cancellable("fetch-peer-consensus-versions", async move {
1717            loop {
1718                let request_futures = api_client.all_peers().iter().filter_map(|&peer| {
1719                    if peer == our_peer_id {
1720                        return None;
1721                    }
1722
1723                    let api_client_inner = api_client.clone();
1724                    Some(async move {
1725                        api_client_inner
1726                            .request_single_peer::<ModuleConsensusVersion>(
1727                                SUPPORTED_MODULE_CONSENSUS_VERSION_ENDPOINT.to_owned(),
1728                                ApiRequestErased::default(),
1729                                peer,
1730                            )
1731                            .await
1732                            .inspect(|res| debug!(
1733                                target: LOG_MODULE_WALLET,
1734                                %peer,
1735                                %our_peer_id,
1736                                ?res,
1737                                "Fetched supported module consensus version from peer"
1738                            ))
1739                            .inspect_err(|err| warn!(
1740                                target: LOG_MODULE_WALLET,
1741                                 %peer,
1742                                 err=%err.fmt_compact(),
1743                                "Failed to fetch consensus version from peer"
1744                            ))
1745                            .ok()
1746                    })
1747                });
1748
1749                let peer_consensus_versions = join_all(request_futures)
1750                    .await
1751                    .into_iter()
1752                    .flatten()
1753                    .collect::<Vec<_>>();
1754
1755                let sorted_consensus_versions = peer_consensus_versions
1756                    .into_iter()
1757                    .chain(std::iter::once(MODULE_CONSENSUS_VERSION))
1758                    .sorted()
1759                    .collect::<Vec<_>>();
1760                let all_peers_supported_version =
1761                    if sorted_consensus_versions.len() == api_client.all_peers().len() {
1762                        let min_supported_version = *sorted_consensus_versions
1763                            .first()
1764                            .expect("at least one element");
1765
1766                        debug!(
1767                            target: LOG_MODULE_WALLET,
1768                            ?sorted_consensus_versions,
1769                            "Fetched supported consensus versions from peers"
1770                        );
1771
1772                        Some(min_supported_version)
1773                    } else {
1774                        assert!(
1775                            sorted_consensus_versions.len() <= api_client.all_peers().len(),
1776                            "Too many peer responses",
1777                        );
1778                        trace!(
1779                            target: LOG_MODULE_WALLET,
1780                            ?sorted_consensus_versions,
1781                            "Not all peers have reported their consensus version yet"
1782                        );
1783                        None
1784                    };
1785
1786                if sender.send(all_peers_supported_version).is_err() {
1787                    warn!(target: LOG_MODULE_WALLET, "Failed to send consensus version to watch channel, stopping task");
1788                    break;
1789                }
1790
1791                if is_running_in_test_env() {
1792                    // Even in tests we don't want to spam the federation with requests about it
1793                    sleep(Duration::from_secs(5)).await;
1794                } else {
1795                    sleep(Duration::from_secs(600)).await;
1796                }
1797            }
1798        });
1799        receiver
1800    }
1801}
1802
1803#[instrument(target = LOG_MODULE_WALLET, level = "debug", skip_all)]
1804pub async fn run_broadcast_pending_tx(db: Database, rpc: DynBitcoindRpc, broadcast: Arc<Notify>) {
1805    loop {
1806        // Unless something new happened, we broadcast once a minute
1807        let _ = tokio::time::timeout(Duration::from_secs(60), broadcast.notified()).await;
1808        broadcast_pending_tx(db.begin_transaction_nc().await, &rpc).await;
1809    }
1810}
1811
1812pub async fn broadcast_pending_tx(mut dbtx: DatabaseTransaction<'_>, rpc: &DynBitcoindRpc) {
1813    let pending_tx: Vec<PendingTransaction> = dbtx
1814        .find_by_prefix(&PendingTransactionPrefixKey)
1815        .await
1816        .map(|(_, val)| val)
1817        .collect::<Vec<_>>()
1818        .await;
1819    let rbf_txids: BTreeSet<Txid> = pending_tx
1820        .iter()
1821        .filter_map(|tx| tx.rbf.clone().map(|rbf| rbf.txid))
1822        .collect();
1823    if !pending_tx.is_empty() {
1824        debug!(
1825            target: LOG_MODULE_WALLET,
1826            "Broadcasting pending transactions (total={}, rbf={})",
1827            pending_tx.len(),
1828            rbf_txids.len()
1829        );
1830    }
1831
1832    for PendingTransaction { tx, .. } in pending_tx {
1833        if !rbf_txids.contains(&tx.compute_txid()) {
1834            debug!(
1835                target: LOG_MODULE_WALLET,
1836                tx = %tx.compute_txid(),
1837                weight = tx.weight().to_wu(),
1838                output = ?tx.output,
1839                "Broadcasting peg-out",
1840            );
1841            trace!(transaction = ?tx);
1842            rpc.submit_transaction(tx).await;
1843        }
1844    }
1845}
1846
1847struct StatelessWallet<'a> {
1848    descriptor: &'a Descriptor<CompressedPublicKey>,
1849    secret_key: &'a secp256k1::SecretKey,
1850    secp: &'a secp256k1::Secp256k1<secp256k1::All>,
1851}
1852
1853impl<'a> StatelessWallet<'a> {
1854    /// Given a tx created from an `WalletOutput`, validate there will be no
1855    /// issues submitting the transaction to the Bitcoin network
1856    fn validate_tx(
1857        tx: &UnsignedTransaction,
1858        output: &WalletOutputV0,
1859        consensus_fee_rate: Feerate,
1860        network: Network,
1861    ) -> Result<(), WalletOutputError> {
1862        if let WalletOutputV0::PegOut(peg_out) = output {
1863            if !peg_out.recipient.is_valid_for_network(network) {
1864                return Err(WalletOutputError::WrongNetwork(
1865                    NetworkLegacyEncodingWrapper(network),
1866                    NetworkLegacyEncodingWrapper(get_network_for_address(&peg_out.recipient)),
1867                ));
1868            }
1869        }
1870
1871        // Validate the tx amount is over the dust limit
1872        if tx.peg_out_amount < tx.destination.minimal_non_dust() {
1873            return Err(WalletOutputError::PegOutUnderDustLimit);
1874        }
1875
1876        // Validate tx fee rate is above the consensus fee rate
1877        if tx.fees.fee_rate < consensus_fee_rate {
1878            return Err(WalletOutputError::PegOutFeeBelowConsensus(
1879                tx.fees.fee_rate,
1880                consensus_fee_rate,
1881            ));
1882        }
1883
1884        // Validate added fees are above the min relay tx fee
1885        // BIP-0125 requires 1 sat/vb for RBF by default (same as normal txs)
1886        let fees = match output {
1887            WalletOutputV0::PegOut(pegout) => pegout.fees,
1888            WalletOutputV0::Rbf(rbf) => rbf.fees,
1889        };
1890        if fees.fee_rate.sats_per_kvb < u64::from(DEFAULT_MIN_RELAY_TX_FEE) {
1891            return Err(WalletOutputError::BelowMinRelayFee);
1892        }
1893
1894        // Validate fees weight matches the actual weight
1895        if fees.total_weight != tx.fees.total_weight {
1896            return Err(WalletOutputError::TxWeightIncorrect(
1897                fees.total_weight,
1898                tx.fees.total_weight,
1899            ));
1900        }
1901
1902        Ok(())
1903    }
1904
1905    /// Attempts to create a tx ready to be signed from available UTXOs.
1906    //
1907    // * `peg_out_amount`: How much the peg-out should be
1908    // * `destination`: The address the user is pegging-out to
1909    // * `included_utxos`: UXTOs that must be included (for RBF)
1910    // * `remaining_utxos`: All other spendable UXTOs
1911    // * `fee_rate`: How much needs to be spent on fees
1912    // * `change_tweak`: How the federation can recognize it's change UTXO
1913    // * `rbf`: If this is an RBF transaction
1914    #[allow(clippy::too_many_arguments)]
1915    fn create_tx(
1916        &self,
1917        peg_out_amount: bitcoin::Amount,
1918        destination: ScriptBuf,
1919        mut included_utxos: Vec<(UTXOKey, SpendableUTXO)>,
1920        mut remaining_utxos: Vec<(UTXOKey, SpendableUTXO)>,
1921        mut fee_rate: Feerate,
1922        change_tweak: &[u8; 33],
1923        rbf: Option<Rbf>,
1924    ) -> Result<UnsignedTransaction, WalletOutputError> {
1925        // Add the rbf fees to the existing tx fees
1926        if let Some(rbf) = &rbf {
1927            fee_rate.sats_per_kvb += rbf.fees.fee_rate.sats_per_kvb;
1928        }
1929
1930        // When building a transaction we need to take care of two things:
1931        //  * We need enough input amount to fund all outputs
1932        //  * We need to keep an eye on the tx weight so we can factor the fees into out
1933        //    calculation
1934        // We then go on to calculate the base size of the transaction `total_weight`
1935        // and the maximum weight per added input which we will add every time
1936        // we select an input.
1937        let change_script = self.derive_script(change_tweak);
1938        let out_weight = (destination.len() * 4 + 1 + 32
1939            // Add change script weight, it's very likely to be needed if not we just overpay in fees
1940            + 1 // script len varint, 1 byte for all addresses we accept
1941            + change_script.len() * 4 // script len
1942            + 32) as u64; // value
1943        let mut total_weight = 16 + // version
1944            12 + // up to 2**16-1 inputs
1945            12 + // up to 2**16-1 outputs
1946            out_weight + // weight of all outputs
1947            16; // lock time
1948        // https://github.com/fedimint/fedimint/issues/4590
1949        #[allow(deprecated)]
1950        let max_input_weight = (self
1951            .descriptor
1952            .max_satisfaction_weight()
1953            .expect("is satisfyable") +
1954            128 + // TxOutHash
1955            16 + // TxOutIndex
1956            16) as u64; // sequence
1957
1958        // Ensure deterministic ordering of UTXOs for all peers
1959        included_utxos.sort_by_key(|(_, utxo)| utxo.amount);
1960        remaining_utxos.sort_by_key(|(_, utxo)| utxo.amount);
1961        included_utxos.extend(remaining_utxos);
1962
1963        // Finally we initialize our accumulator for selected input amounts
1964        let mut total_selected_value = bitcoin::Amount::from_sat(0);
1965        let mut selected_utxos: Vec<(UTXOKey, SpendableUTXO)> = vec![];
1966        let mut fees = fee_rate.calculate_fee(total_weight);
1967
1968        while total_selected_value < peg_out_amount + change_script.minimal_non_dust() + fees {
1969            match included_utxos.pop() {
1970                Some((utxo_key, utxo)) => {
1971                    total_selected_value += utxo.amount;
1972                    total_weight += max_input_weight;
1973                    fees = fee_rate.calculate_fee(total_weight);
1974                    selected_utxos.push((utxo_key, utxo));
1975                }
1976                _ => return Err(WalletOutputError::NotEnoughSpendableUTXO), // Not enough UTXOs
1977            }
1978        }
1979
1980        // We always pay ourselves change back to ensure that we don't lose anything due
1981        // to dust
1982        let change = total_selected_value - fees - peg_out_amount;
1983        let output: Vec<TxOut> = vec![
1984            TxOut {
1985                value: peg_out_amount,
1986                script_pubkey: destination.clone(),
1987            },
1988            TxOut {
1989                value: change,
1990                script_pubkey: change_script,
1991            },
1992        ];
1993        let mut change_out = bitcoin::psbt::Output::default();
1994        change_out
1995            .proprietary
1996            .insert(proprietary_tweak_key(), change_tweak.to_vec());
1997
1998        info!(
1999            target: LOG_MODULE_WALLET,
2000            inputs = selected_utxos.len(),
2001            input_sats = total_selected_value.to_sat(),
2002            peg_out_sats = peg_out_amount.to_sat(),
2003            ?total_weight,
2004            fees_sats = fees.to_sat(),
2005            fee_rate = fee_rate.sats_per_kvb,
2006            change_sats = change.to_sat(),
2007            "Creating peg-out tx",
2008        );
2009
2010        let transaction = Transaction {
2011            version: bitcoin::transaction::Version(2),
2012            lock_time: LockTime::ZERO,
2013            input: selected_utxos
2014                .iter()
2015                .map(|(utxo_key, _utxo)| TxIn {
2016                    previous_output: utxo_key.0,
2017                    script_sig: Default::default(),
2018                    sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
2019                    witness: bitcoin::Witness::new(),
2020                })
2021                .collect(),
2022            output,
2023        };
2024        info!(
2025            target: LOG_MODULE_WALLET,
2026            txid = %transaction.compute_txid(), "Creating peg-out tx"
2027        );
2028
2029        // FIXME: use custom data structure that guarantees more invariants and only
2030        // convert to PSBT for finalization
2031        let psbt = Psbt {
2032            unsigned_tx: transaction,
2033            version: 0,
2034            xpub: Default::default(),
2035            proprietary: Default::default(),
2036            unknown: Default::default(),
2037            inputs: selected_utxos
2038                .iter()
2039                .map(|(_utxo_key, utxo)| {
2040                    let script_pubkey = self
2041                        .descriptor
2042                        .tweak(&utxo.tweak, self.secp)
2043                        .script_pubkey();
2044                    Input {
2045                        non_witness_utxo: None,
2046                        witness_utxo: Some(TxOut {
2047                            value: utxo.amount,
2048                            script_pubkey,
2049                        }),
2050                        partial_sigs: Default::default(),
2051                        sighash_type: None,
2052                        redeem_script: None,
2053                        witness_script: Some(
2054                            self.descriptor
2055                                .tweak(&utxo.tweak, self.secp)
2056                                .script_code()
2057                                .expect("Failed to tweak descriptor"),
2058                        ),
2059                        bip32_derivation: Default::default(),
2060                        final_script_sig: None,
2061                        final_script_witness: None,
2062                        ripemd160_preimages: Default::default(),
2063                        sha256_preimages: Default::default(),
2064                        hash160_preimages: Default::default(),
2065                        hash256_preimages: Default::default(),
2066                        proprietary: vec![(proprietary_tweak_key(), utxo.tweak.to_vec())]
2067                            .into_iter()
2068                            .collect(),
2069                        tap_key_sig: Default::default(),
2070                        tap_script_sigs: Default::default(),
2071                        tap_scripts: Default::default(),
2072                        tap_key_origins: Default::default(),
2073                        tap_internal_key: Default::default(),
2074                        tap_merkle_root: Default::default(),
2075                        unknown: Default::default(),
2076                    }
2077                })
2078                .collect(),
2079            outputs: vec![Default::default(), change_out],
2080        };
2081
2082        Ok(UnsignedTransaction {
2083            psbt,
2084            signatures: vec![],
2085            change,
2086            fees: PegOutFees {
2087                fee_rate,
2088                total_weight,
2089            },
2090            destination,
2091            selected_utxos,
2092            peg_out_amount,
2093            rbf,
2094        })
2095    }
2096
2097    fn sign_psbt(&self, psbt: &mut Psbt) {
2098        let mut tx_hasher = SighashCache::new(&psbt.unsigned_tx);
2099
2100        for (idx, (psbt_input, _tx_input)) in psbt
2101            .inputs
2102            .iter_mut()
2103            .zip(psbt.unsigned_tx.input.iter())
2104            .enumerate()
2105        {
2106            let tweaked_secret = {
2107                let tweak = psbt_input
2108                    .proprietary
2109                    .get(&proprietary_tweak_key())
2110                    .expect("Malformed PSBT: expected tweak");
2111
2112                self.secret_key.tweak(tweak, self.secp)
2113            };
2114
2115            let tx_hash = tx_hasher
2116                .p2wsh_signature_hash(
2117                    idx,
2118                    psbt_input
2119                        .witness_script
2120                        .as_ref()
2121                        .expect("Missing witness script"),
2122                    psbt_input
2123                        .witness_utxo
2124                        .as_ref()
2125                        .expect("Missing UTXO")
2126                        .value,
2127                    EcdsaSighashType::All,
2128                )
2129                .expect("Failed to create segwit sighash");
2130
2131            let signature = self.secp.sign_ecdsa(
2132                &Message::from_digest_slice(&tx_hash[..]).unwrap(),
2133                &tweaked_secret,
2134            );
2135
2136            psbt_input.partial_sigs.insert(
2137                bitcoin::PublicKey {
2138                    compressed: true,
2139                    inner: secp256k1::PublicKey::from_secret_key(self.secp, &tweaked_secret),
2140                },
2141                EcdsaSig::sighash_all(signature),
2142            );
2143        }
2144    }
2145
2146    fn derive_script(&self, tweak: &[u8]) -> ScriptBuf {
2147        struct CompressedPublicKeyTranslator<'t, 's, Ctx: Verification> {
2148            tweak: &'t [u8],
2149            secp: &'s Secp256k1<Ctx>,
2150        }
2151
2152        impl<'t, 's, Ctx: Verification>
2153            miniscript::Translator<CompressedPublicKey, CompressedPublicKey, Infallible>
2154            for CompressedPublicKeyTranslator<'t, 's, Ctx>
2155        {
2156            fn pk(&mut self, pk: &CompressedPublicKey) -> Result<CompressedPublicKey, Infallible> {
2157                let hashed_tweak = {
2158                    let mut hasher = HmacEngine::<sha256::Hash>::new(&pk.key.serialize()[..]);
2159                    hasher.input(self.tweak);
2160                    Hmac::from_engine(hasher).to_byte_array()
2161                };
2162
2163                Ok(CompressedPublicKey {
2164                    key: pk
2165                        .key
2166                        .add_exp_tweak(
2167                            self.secp,
2168                            &Scalar::from_be_bytes(hashed_tweak).expect("can't fail"),
2169                        )
2170                        .expect("tweaking failed"),
2171                })
2172            }
2173            translate_hash_fail!(CompressedPublicKey, CompressedPublicKey, Infallible);
2174        }
2175
2176        let descriptor = self
2177            .descriptor
2178            .translate_pk(&mut CompressedPublicKeyTranslator {
2179                tweak,
2180                secp: self.secp,
2181            })
2182            .expect("can't fail");
2183
2184        descriptor.script_pubkey()
2185    }
2186}
2187
2188pub fn nonce_from_idx(nonce_idx: u64) -> [u8; 33] {
2189    let mut nonce: [u8; 33] = [0; 33];
2190    // Make it look like a compressed pubkey, has to be either 0x02 or 0x03
2191    nonce[0] = 0x02;
2192    nonce[1..].copy_from_slice(&nonce_idx.consensus_hash::<bitcoin::hashes::sha256::Hash>()[..]);
2193
2194    nonce
2195}
2196
2197/// A peg-out tx that is ready to be broadcast with a tweak for the change UTXO
2198#[derive(Clone, Debug, Encodable, Decodable)]
2199pub struct PendingTransaction {
2200    pub tx: bitcoin::Transaction,
2201    pub tweak: [u8; 33],
2202    pub change: bitcoin::Amount,
2203    pub destination: ScriptBuf,
2204    pub fees: PegOutFees,
2205    pub selected_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2206    pub peg_out_amount: bitcoin::Amount,
2207    pub rbf: Option<Rbf>,
2208}
2209
2210impl Serialize for PendingTransaction {
2211    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2212    where
2213        S: serde::Serializer,
2214    {
2215        if serializer.is_human_readable() {
2216            serializer.serialize_str(&self.consensus_encode_to_hex())
2217        } else {
2218            serializer.serialize_bytes(&self.consensus_encode_to_vec())
2219        }
2220    }
2221}
2222
2223/// A PSBT that is awaiting enough signatures from the federation to becoming a
2224/// `PendingTransaction`
2225#[derive(Clone, Debug, Eq, PartialEq, Encodable, Decodable)]
2226pub struct UnsignedTransaction {
2227    pub psbt: Psbt,
2228    pub signatures: Vec<(PeerId, PegOutSignatureItem)>,
2229    pub change: bitcoin::Amount,
2230    pub fees: PegOutFees,
2231    pub destination: ScriptBuf,
2232    pub selected_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2233    pub peg_out_amount: bitcoin::Amount,
2234    pub rbf: Option<Rbf>,
2235}
2236
2237impl Serialize for UnsignedTransaction {
2238    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2239    where
2240        S: serde::Serializer,
2241    {
2242        if serializer.is_human_readable() {
2243            serializer.serialize_str(&self.consensus_encode_to_hex())
2244        } else {
2245            serializer.serialize_bytes(&self.consensus_encode_to_vec())
2246        }
2247    }
2248}
2249
2250#[cfg(test)]
2251mod tests {
2252
2253    use std::str::FromStr;
2254
2255    use bitcoin::Network::{Bitcoin, Testnet};
2256    use bitcoin::hashes::Hash;
2257    use bitcoin::{Address, Amount, OutPoint, Txid, secp256k1};
2258    use fedimint_core::Feerate;
2259    use fedimint_core::encoding::btc::NetworkLegacyEncodingWrapper;
2260    use fedimint_wallet_common::{PegOut, PegOutFees, Rbf, WalletOutputV0};
2261    use miniscript::descriptor::Wsh;
2262
2263    use crate::common::PegInDescriptor;
2264    use crate::{
2265        CompressedPublicKey, OsRng, SpendableUTXO, StatelessWallet, UTXOKey, WalletOutputError,
2266    };
2267
2268    #[test]
2269    fn create_tx_should_validate_amounts() {
2270        let secp = secp256k1::Secp256k1::new();
2271
2272        let descriptor = PegInDescriptor::Wsh(
2273            Wsh::new_sortedmulti(
2274                3,
2275                (0..4)
2276                    .map(|_| secp.generate_keypair(&mut OsRng))
2277                    .map(|(_, key)| CompressedPublicKey { key })
2278                    .collect(),
2279            )
2280            .unwrap(),
2281        );
2282
2283        let (secret_key, _) = secp.generate_keypair(&mut OsRng);
2284
2285        let wallet = StatelessWallet {
2286            descriptor: &descriptor,
2287            secret_key: &secret_key,
2288            secp: &secp,
2289        };
2290
2291        let spendable = SpendableUTXO {
2292            tweak: [0; 33],
2293            amount: bitcoin::Amount::from_sat(3000),
2294        };
2295
2296        let recipient = Address::from_str("32iVBEu4dxkUQk9dJbZUiBiQdmypcEyJRf").unwrap();
2297
2298        let fee = Feerate { sats_per_kvb: 1000 };
2299        let weight = 875;
2300
2301        // not enough SpendableUTXO
2302        // tx fee = ceil(875 / 4) * 1 sat/vb = 219
2303        // change script dust = 330
2304        // spendable sats = 3000 - 219 - 330 = 2451
2305        let tx = wallet.create_tx(
2306            Amount::from_sat(2452),
2307            recipient.clone().assume_checked().script_pubkey(),
2308            vec![],
2309            vec![(UTXOKey(OutPoint::null()), spendable.clone())],
2310            fee,
2311            &[0; 33],
2312            None,
2313        );
2314        assert_eq!(tx, Err(WalletOutputError::NotEnoughSpendableUTXO));
2315
2316        // successful tx creation
2317        let mut tx = wallet
2318            .create_tx(
2319                Amount::from_sat(1000),
2320                recipient.clone().assume_checked().script_pubkey(),
2321                vec![],
2322                vec![(UTXOKey(OutPoint::null()), spendable)],
2323                fee,
2324                &[0; 33],
2325                None,
2326            )
2327            .expect("is ok");
2328
2329        // peg out weight is incorrectly set to 0
2330        let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, 0), fee, Bitcoin);
2331        assert_eq!(res, Err(WalletOutputError::TxWeightIncorrect(0, weight)));
2332
2333        // fee rate set below min relay fee to 0
2334        let res = StatelessWallet::validate_tx(&tx, &rbf(0, weight), fee, Bitcoin);
2335        assert_eq!(res, Err(WalletOutputError::BelowMinRelayFee));
2336
2337        // fees are okay
2338        let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2339        assert_eq!(res, Ok(()));
2340
2341        // tx has fee below consensus
2342        tx.fees = PegOutFees::new(0, weight);
2343        let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2344        assert_eq!(
2345            res,
2346            Err(WalletOutputError::PegOutFeeBelowConsensus(
2347                Feerate { sats_per_kvb: 0 },
2348                fee
2349            ))
2350        );
2351
2352        // tx has peg-out amount under dust limit
2353        tx.peg_out_amount = bitcoin::Amount::ZERO;
2354        let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2355        assert_eq!(res, Err(WalletOutputError::PegOutUnderDustLimit));
2356
2357        // tx is invalid for network
2358        let output = WalletOutputV0::PegOut(PegOut {
2359            recipient,
2360            amount: bitcoin::Amount::from_sat(1000),
2361            fees: PegOutFees::new(100, weight),
2362        });
2363        let res = StatelessWallet::validate_tx(&tx, &output, fee, Testnet);
2364        assert_eq!(
2365            res,
2366            Err(WalletOutputError::WrongNetwork(
2367                NetworkLegacyEncodingWrapper(Testnet),
2368                NetworkLegacyEncodingWrapper(Bitcoin)
2369            ))
2370        );
2371    }
2372
2373    fn rbf(sats_per_kvb: u64, total_weight: u64) -> WalletOutputV0 {
2374        WalletOutputV0::Rbf(Rbf {
2375            fees: PegOutFees::new(sats_per_kvb, total_weight),
2376            txid: Txid::all_zeros(),
2377        })
2378    }
2379}