Skip to main content

fedimint_walletv2_server/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::similar_names)]
3#![allow(clippy::cast_possible_truncation)]
4#![allow(clippy::cast_possible_wrap)]
5#![allow(clippy::default_trait_access)]
6#![allow(clippy::missing_errors_doc)]
7#![allow(clippy::missing_panics_doc)]
8#![allow(clippy::module_name_repetitions)]
9#![allow(clippy::must_use_candidate)]
10#![allow(clippy::single_match_else)]
11#![allow(clippy::too_many_lines)]
12
13pub mod db;
14pub mod envs;
15mod metrics;
16mod pending;
17
18use std::collections::{BTreeMap, BTreeSet};
19
20use anyhow::{Context, anyhow, bail, ensure};
21use bitcoin::absolute::LockTime;
22use bitcoin::hashes::{Hash, sha256};
23use bitcoin::secp256k1::Secp256k1;
24use bitcoin::sighash::{EcdsaSighashType, SighashCache};
25use bitcoin::transaction::Version;
26use bitcoin::{Amount, Network, Sequence, Transaction, TxIn, TxOut, Txid};
27use common::config::WalletConfigConsensus;
28use common::{
29    OutputInfo, WalletCommonInit, WalletConsensusItem, WalletInput, WalletModuleTypes,
30    WalletOutput, WalletOutputOutcome,
31};
32use db::{
33    DbKeyPrefix, FederationWalletKey, FederationWalletPrefix, Output, OutputKey, OutputPrefix,
34    SignaturesKey, SignaturesPrefix, SignaturesTxidPrefix, SpentOutputKey, SpentOutputPrefix,
35    TxInfoIndexKey, TxInfoIndexPrefix,
36};
37use envs::FM_WALLETV2_DISABLE_MEMPOOL_SCAN_ENV;
38use fedimint_core::config::{
39    ServerModuleConfig, ServerModuleConsensusConfig, TypedServerModuleConfig,
40    TypedServerModuleConsensusConfig,
41};
42use fedimint_core::core::ModuleInstanceId;
43use fedimint_core::db::{
44    Database, DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped,
45};
46use fedimint_core::encoding::{Decodable, Encodable};
47use fedimint_core::envs::{
48    FM_ENABLE_MODULE_WALLETV2_ENV, is_env_var_set_opt, is_running_in_test_env,
49};
50use fedimint_core::module::audit::Audit;
51use fedimint_core::module::{
52    Amounts, ApiEndpoint, ApiVersion, CoreConsensusVersion, InputMeta, ModuleConsensusVersion,
53    ModuleInit, MultiApiVersion, TransactionItemAmounts, public_api_endpoint,
54};
55#[cfg(not(target_family = "wasm"))]
56use fedimint_core::task::TaskGroup;
57use fedimint_core::task::sleep;
58use fedimint_core::util::FmtCompact as _;
59use fedimint_core::{
60    InPoint, NumPeersExt, OutPoint, PeerId, apply, async_trait_maybe_send, push_db_pair_items, util,
61};
62use fedimint_logging::LOG_MODULE_WALLETV2;
63use fedimint_server_core::bitcoin_rpc::ServerBitcoinRpcMonitor;
64use fedimint_server_core::config::{PeerHandleOps, PeerHandleOpsExt};
65use fedimint_server_core::migration::ServerModuleDbMigrationFn;
66use fedimint_server_core::{
67    ConfigGenModuleArgs, EnvVarDoc, ServerModule, ServerModuleInit, ServerModuleInitArgs,
68};
69pub use fedimint_walletv2_common as common;
70use fedimint_walletv2_common::config::{
71    FeeConsensus, WalletClientConfig, WalletConfig, WalletConfigPrivate,
72};
73use fedimint_walletv2_common::endpoint_constants::{
74    CONSENSUS_BLOCK_COUNT_ENDPOINT, CONSENSUS_FEERATE_ENDPOINT, FEDERATION_WALLET_ENDPOINT,
75    OUTPUT_INFO_SLICE_ENDPOINT, PENDING_OUTPUTS_ENDPOINT, PENDING_TRANSACTION_CHAIN_ENDPOINT,
76    RECEIVE_FEE_ENDPOINT, SEND_FEE_ENDPOINT, TRANSACTION_CHAIN_ENDPOINT, TRANSACTION_ID_ENDPOINT,
77};
78/// Re-exported for existing callers; the constants live in the common crate so
79/// that the client can render peg-in confirmation progress against them.
80pub use fedimint_walletv2_common::{BLOCK_COUNT_VOTE_DELAY, CONFIRMATION_FINALITY_DELAY};
81use fedimint_walletv2_common::{
82    FederationWallet, MODULE_CONSENSUS_VERSION, PendingOutputs, TxInfo, WalletInputError,
83    WalletOutputError, descriptor, is_potential_receive, tweak_public_key,
84};
85use futures::StreamExt;
86use miniscript::descriptor::Wsh;
87use rand::rngs::OsRng;
88use secp256k1::ecdsa::Signature;
89use secp256k1::{PublicKey, Scalar, SecretKey};
90use serde::{Deserialize, Serialize};
91use strum::IntoEnumIterator;
92use tokio::sync::watch;
93use tracing::{debug, info};
94
95use crate::db::{
96    BlockCountVoteKey, BlockCountVotePrefix, FeeRateVoteKey, FeeRateVotePrefix, TxInfoKey,
97    TxInfoPrefix, UnconfirmedTxKey, UnconfirmedTxPrefix, UnsignedTxKey, UnsignedTxPrefix,
98};
99use crate::metrics::{
100    WALLET_BLOCK_COUNT, WALLET_INOUT_FEES_SATS, WALLET_INOUT_SATS, WALLET_PEGIN_FEES_SATS,
101    WALLET_PEGIN_SATS, WALLET_PEGOUT_FEES_SATS, WALLET_PEGOUT_SATS,
102};
103
104/// Maximum number of blocks the consensus block count can advance in a single
105/// consensus item to limit the work done in one `process_consensus_item` step.
106const MAX_BLOCK_COUNT_INCREMENT: u64 = 5;
107
108/// Devimint tests can mine many regtest blocks at once, so use a larger cap in
109/// test environments to avoid waiting on several consensus sessions.
110const TEST_MAX_BLOCK_COUNT_INCREMENT: u64 = 100;
111
112/// Minimum fee rate vote of 1 sat/vB to ensure we never propose a fee rate
113/// below what Bitcoin Core will relay.
114const MIN_FEERATE_VOTE_SATS_PER_KVB: u64 = 1000;
115
116#[derive(Clone, Debug, Eq, PartialEq, Serialize, Encodable, Decodable)]
117pub struct FederationTx {
118    pub tx: Transaction,
119    pub spent_tx_outs: Vec<SpentTxOut>,
120    pub vbytes: u64,
121    pub fee: Amount,
122}
123
124#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
125pub struct SpentTxOut {
126    pub value: Amount,
127    pub tweak: sha256::Hash,
128}
129
130async fn pending_txs_unordered(dbtx: &mut DatabaseTransaction<'_>) -> Vec<FederationTx> {
131    let unsigned: Vec<FederationTx> = dbtx
132        .find_by_prefix(&UnsignedTxPrefix)
133        .await
134        .map(|entry| entry.1)
135        .collect()
136        .await;
137
138    let unconfirmed: Vec<FederationTx> = dbtx
139        .find_by_prefix(&UnconfirmedTxPrefix)
140        .await
141        .map(|entry| entry.1)
142        .collect()
143        .await;
144
145    unsigned.into_iter().chain(unconfirmed).collect()
146}
147
148#[derive(Debug, Clone)]
149pub struct WalletInit;
150
151impl ModuleInit for WalletInit {
152    type Common = WalletCommonInit;
153
154    async fn dump_database(
155        &self,
156        dbtx: &mut DatabaseTransaction<'_>,
157        prefix_names: Vec<String>,
158    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
159        let mut wallet: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
160
161        let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
162            prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
163        });
164
165        for table in filtered_prefixes {
166            match table {
167                DbKeyPrefix::Output => {
168                    push_db_pair_items!(
169                        dbtx,
170                        OutputPrefix,
171                        OutputKey,
172                        Output,
173                        wallet,
174                        "Wallet Outputs"
175                    );
176                }
177                DbKeyPrefix::SpentOutput => {
178                    push_db_pair_items!(
179                        dbtx,
180                        SpentOutputPrefix,
181                        SpentOutputKey,
182                        (),
183                        wallet,
184                        "Wallet Spent Outputs"
185                    );
186                }
187                DbKeyPrefix::BlockCountVote => {
188                    push_db_pair_items!(
189                        dbtx,
190                        BlockCountVotePrefix,
191                        BlockCountVoteKey,
192                        u64,
193                        wallet,
194                        "Wallet Block Count Votes"
195                    );
196                }
197                DbKeyPrefix::FeeRateVote => {
198                    push_db_pair_items!(
199                        dbtx,
200                        FeeRateVotePrefix,
201                        FeeRateVoteKey,
202                        Option<u64>,
203                        wallet,
204                        "Wallet Fee Rate Votes"
205                    );
206                }
207                DbKeyPrefix::TxLog => {
208                    push_db_pair_items!(
209                        dbtx,
210                        TxInfoPrefix,
211                        TxInfoKey,
212                        TxInfo,
213                        wallet,
214                        "Wallet Tx Log"
215                    );
216                }
217                DbKeyPrefix::TxInfoIndex => {
218                    push_db_pair_items!(
219                        dbtx,
220                        TxInfoIndexPrefix,
221                        TxInfoIndexKey,
222                        u64,
223                        wallet,
224                        "Wallet Tx Info Index"
225                    );
226                }
227                DbKeyPrefix::UnsignedTx => {
228                    push_db_pair_items!(
229                        dbtx,
230                        UnsignedTxPrefix,
231                        UnsignedTxKey,
232                        FederationTx,
233                        wallet,
234                        "Wallet Unsigned Transactions"
235                    );
236                }
237                DbKeyPrefix::Signatures => {
238                    push_db_pair_items!(
239                        dbtx,
240                        SignaturesPrefix,
241                        SignaturesKey,
242                        Vec<Signature>,
243                        wallet,
244                        "Wallet Signatures"
245                    );
246                }
247                DbKeyPrefix::UnconfirmedTx => {
248                    push_db_pair_items!(
249                        dbtx,
250                        UnconfirmedTxPrefix,
251                        UnconfirmedTxKey,
252                        FederationTx,
253                        wallet,
254                        "Wallet Unconfirmed Transactions"
255                    );
256                }
257                DbKeyPrefix::FederationWallet => {
258                    push_db_pair_items!(
259                        dbtx,
260                        FederationWalletPrefix,
261                        FederationWalletKey,
262                        FederationWallet,
263                        wallet,
264                        "Federation Wallet"
265                    );
266                }
267            }
268        }
269
270        Box::new(wallet.into_iter())
271    }
272}
273
274#[apply(async_trait_maybe_send!)]
275impl ServerModuleInit for WalletInit {
276    type Module = Wallet;
277
278    fn versions(&self, _core: CoreConsensusVersion) -> &[ModuleConsensusVersion] {
279        &[MODULE_CONSENSUS_VERSION]
280    }
281
282    fn is_enabled_by_default(&self) -> bool {
283        is_env_var_set_opt(FM_ENABLE_MODULE_WALLETV2_ENV).unwrap_or(true)
284    }
285
286    fn get_documented_env_vars(&self) -> Vec<EnvVarDoc> {
287        vec![
288            EnvVarDoc {
289                name: FM_ENABLE_MODULE_WALLETV2_ENV,
290                description: "Set to 0/false to disable the WalletV2 module. Enabled by default.",
291            },
292            EnvVarDoc {
293                name: FM_WALLETV2_DISABLE_MEMPOOL_SCAN_ENV,
294                description: "Set to 1/true to stop scanning the mempool for pending receives. \
295                              Peg-in progress is then only reported once a deposit is mined, as \
296                              it already is on an esplora backend. Enabled by default.",
297            },
298        ]
299    }
300
301    async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
302        // Eagerly register metrics so the series exist before the first transaction
303        for direction in ["incoming", "outgoing"] {
304            WALLET_INOUT_SATS
305                .with_label_values(&[direction])
306                .get_sample_count();
307            WALLET_INOUT_FEES_SATS
308                .with_label_values(&[direction])
309                .get_sample_count();
310        }
311        WALLET_PEGIN_SATS.get_sample_count();
312        WALLET_PEGIN_FEES_SATS.get_sample_count();
313        WALLET_PEGOUT_SATS.get_sample_count();
314        WALLET_PEGOUT_FEES_SATS.get_sample_count();
315
316        Ok(Wallet::new(
317            args.cfg().to_typed()?,
318            args.db(),
319            args.task_group(),
320            args.server_bitcoin_rpc_monitor(),
321        ))
322    }
323
324    fn trusted_dealer_gen(
325        &self,
326        peers: &[PeerId],
327        args: &ConfigGenModuleArgs,
328    ) -> BTreeMap<PeerId, ServerModuleConfig> {
329        let fee_consensus = FeeConsensus::new(0).expect("Relative fee is within range");
330
331        let bitcoin_sks = peers
332            .iter()
333            .map(|peer| (*peer, SecretKey::new(&mut secp256k1::rand::thread_rng())))
334            .collect::<BTreeMap<PeerId, SecretKey>>();
335
336        let bitcoin_pks = bitcoin_sks
337            .iter()
338            .map(|(peer, sk)| (*peer, sk.public_key(secp256k1::SECP256K1)))
339            .collect::<BTreeMap<PeerId, PublicKey>>();
340
341        bitcoin_sks
342            .into_iter()
343            .map(|(peer, bitcoin_sk)| {
344                let config = WalletConfig {
345                    private: WalletConfigPrivate { bitcoin_sk },
346                    consensus: WalletConfigConsensus::new(
347                        bitcoin_pks.clone(),
348                        fee_consensus.clone(),
349                        args.network,
350                    ),
351                };
352
353                (peer, config.to_erased())
354            })
355            .collect()
356    }
357
358    async fn distributed_gen(
359        &self,
360        peers: &(dyn PeerHandleOps + Send + Sync),
361        args: &ConfigGenModuleArgs,
362    ) -> anyhow::Result<ServerModuleConfig> {
363        let fee_consensus = FeeConsensus::new(0).expect("Relative fee is within range");
364
365        let (bitcoin_sk, bitcoin_pk) = secp256k1::generate_keypair(&mut OsRng);
366
367        let bitcoin_pks: BTreeMap<PeerId, PublicKey> = peers
368            .exchange_encodable(bitcoin_pk)
369            .await?
370            .into_iter()
371            .collect();
372
373        let config = WalletConfig {
374            private: WalletConfigPrivate { bitcoin_sk },
375            consensus: WalletConfigConsensus::new(bitcoin_pks, fee_consensus, args.network),
376        };
377
378        Ok(config.to_erased())
379    }
380
381    fn validate_config(&self, identity: &PeerId, config: ServerModuleConfig) -> anyhow::Result<()> {
382        let config = config.to_typed::<WalletConfig>()?;
383
384        ensure!(
385            config
386                .consensus
387                .bitcoin_pks
388                .get(identity)
389                .ok_or(anyhow::anyhow!("No public key for our identity"))?
390                == &config.private.bitcoin_sk.public_key(secp256k1::SECP256K1),
391            "Bitcoin wallet private key doesn't match multisig pubkey"
392        );
393
394        Ok(())
395    }
396
397    fn get_client_config(
398        &self,
399        config: &ServerModuleConsensusConfig,
400    ) -> anyhow::Result<WalletClientConfig> {
401        let config = WalletConfigConsensus::from_erased(config)?;
402
403        Ok(WalletClientConfig {
404            bitcoin_pks: config.bitcoin_pks,
405            descriptor: config.descriptor,
406            send_tx_vbytes: config.send_tx_vbytes,
407            receive_tx_vbytes: config.receive_tx_vbytes,
408            feerate_base: config.feerate_base,
409            dust_limit: config.dust_limit,
410            fee_consensus: config.fee_consensus,
411            network: config.network,
412        })
413    }
414
415    fn get_database_migrations(
416        &self,
417    ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Wallet>> {
418        BTreeMap::new()
419    }
420
421    fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
422        Some(DbKeyPrefix::iter().map(|p| p as u8).collect())
423    }
424}
425
426#[apply(async_trait_maybe_send!)]
427impl ServerModule for Wallet {
428    type Common = WalletModuleTypes;
429    type Init = WalletInit;
430
431    async fn consensus_proposal<'a>(
432        &'a self,
433        dbtx: &mut DatabaseTransaction<'_>,
434    ) -> Vec<WalletConsensusItem> {
435        let mut items = dbtx
436            .find_by_prefix(&UnsignedTxPrefix)
437            .await
438            .map(|(key, unsigned_tx)| {
439                let signatures = self.sign_tx(&unsigned_tx);
440
441                self.verify_signatures(
442                    &unsigned_tx,
443                    &signatures,
444                    self.cfg.private.bitcoin_sk.public_key(secp256k1::SECP256K1),
445                )
446                .expect("Our signatures failed verification against our private key");
447
448                WalletConsensusItem::Signatures(key.0, signatures)
449            })
450            .collect::<Vec<WalletConsensusItem>>()
451            .await;
452
453        if let Some(status) = self.btc_rpc.status() {
454            assert_eq!(status.network, self.cfg.consensus.network);
455
456            let block_count_vote = status.block_count.saturating_sub(BLOCK_COUNT_VOTE_DELAY);
457
458            let consensus_block_count = self.consensus_block_count(dbtx).await;
459
460            let max_block_count_increment = if is_running_in_test_env() {
461                TEST_MAX_BLOCK_COUNT_INCREMENT
462            } else {
463                MAX_BLOCK_COUNT_INCREMENT
464            };
465
466            let block_count_vote = match consensus_block_count {
467                0 => block_count_vote,
468                _ => block_count_vote.min(consensus_block_count + max_block_count_increment),
469            };
470
471            WALLET_BLOCK_COUNT.set(i64::try_from(block_count_vote).unwrap_or(i64::MAX));
472
473            items.push(WalletConsensusItem::BlockCount(block_count_vote));
474
475            // `None` retracts our vote while the backend cannot estimate
476            // fees yet, e.g. while it is still syncing.
477            let feerate_vote = status
478                .fee_rate
479                .map(|fee_rate| fee_rate.sats_per_kvb.max(MIN_FEERATE_VOTE_SATS_PER_KVB));
480
481            items.push(WalletConsensusItem::Feerate(feerate_vote));
482        } else {
483            // Bitcoin backend not connected, retract fee rate vote
484            items.push(WalletConsensusItem::Feerate(None));
485        }
486
487        items
488    }
489
490    async fn process_consensus_item<'a, 'b>(
491        &'a self,
492        dbtx: &mut DatabaseTransaction<'b>,
493        consensus_item: WalletConsensusItem,
494        peer: PeerId,
495    ) -> anyhow::Result<()> {
496        match consensus_item {
497            WalletConsensusItem::BlockCount(block_count_vote) => {
498                self.process_block_count(dbtx, block_count_vote, peer).await
499            }
500            WalletConsensusItem::Feerate(feerate) => {
501                if Some(feerate) == dbtx.insert_entry(&FeeRateVoteKey(peer), &feerate).await {
502                    return Err(anyhow!("Fee rate vote is redundant"));
503                }
504
505                Ok(())
506            }
507            WalletConsensusItem::Signatures(txid, signatures) => {
508                self.process_signatures(dbtx, txid, signatures, peer).await
509            }
510            WalletConsensusItem::Default { variant, .. } => Err(anyhow!(
511                "Received wallet consensus item with unknown variant {variant}"
512            )),
513        }
514    }
515
516    async fn process_input<'a, 'b, 'c>(
517        &'a self,
518        dbtx: &mut DatabaseTransaction<'c>,
519        input: &'b WalletInput,
520        _in_point: InPoint,
521    ) -> Result<InputMeta, WalletInputError> {
522        let input = input.ensure_v0_ref()?;
523
524        if dbtx
525            .insert_entry(&SpentOutputKey(input.output_index), &())
526            .await
527            .is_some()
528        {
529            return Err(WalletInputError::OutputAlreadySpent);
530        }
531
532        let Output(tracked_outpoint, tracked_output) = dbtx
533            .get_value(&OutputKey(input.output_index))
534            .await
535            .ok_or(WalletInputError::UnknownOutputIndex)?;
536
537        let tweaked_pubkey = self
538            .descriptor(&input.tweak.consensus_hash())
539            .script_pubkey();
540
541        if tracked_output.script_pubkey != tweaked_pubkey {
542            return Err(WalletInputError::WrongTweak);
543        }
544
545        let consensus_receive_fee = self
546            .receive_fee(dbtx)
547            .await
548            .ok_or(WalletInputError::NoConsensusFeerateAvailable)?;
549
550        // We allow for a higher fee such that a guardian could construct a CPFP
551        // transaction. This is the last line of defense should the federations
552        // transactions ever get stuck due to a critical failure of the feerate
553        // estimation.
554        if input.fee < consensus_receive_fee {
555            return Err(WalletInputError::InsufficientTotalFee);
556        }
557
558        let output_value = tracked_output
559            .value
560            .checked_sub(input.fee)
561            .ok_or(WalletInputError::ArithmeticOverflow)?;
562
563        if let Some(wallet) = dbtx.remove_entry(&FederationWalletKey).await {
564            // Assuming the first receive into the federation is made through a
565            // standard transaction, its output value is over the P2WSH dust
566            // limit. By induction so is this change value.
567            let change_value = wallet
568                .value
569                .checked_add(output_value)
570                .ok_or(WalletInputError::ArithmeticOverflow)?;
571
572            let tx = Transaction {
573                version: Version(2),
574                lock_time: LockTime::ZERO,
575                input: vec![
576                    TxIn {
577                        previous_output: wallet.outpoint,
578                        script_sig: Default::default(),
579                        sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
580                        witness: bitcoin::Witness::new(),
581                    },
582                    TxIn {
583                        previous_output: tracked_outpoint,
584                        script_sig: Default::default(),
585                        sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
586                        witness: bitcoin::Witness::new(),
587                    },
588                ],
589                output: vec![TxOut {
590                    value: change_value,
591                    script_pubkey: self.descriptor(&wallet.consensus_hash()).script_pubkey(),
592                }],
593            };
594
595            dbtx.insert_new_entry(
596                &FederationWalletKey,
597                &FederationWallet {
598                    value: change_value,
599                    outpoint: bitcoin::OutPoint {
600                        txid: tx.compute_txid(),
601                        vout: 0,
602                    },
603                    tweak: wallet.consensus_hash(),
604                },
605            )
606            .await;
607
608            let tx_index = self.total_txs(dbtx).await;
609
610            let created = self.consensus_block_count(dbtx).await;
611
612            dbtx.insert_new_entry(
613                &TxInfoKey(tx_index),
614                &TxInfo {
615                    index: tx_index,
616                    txid: tx.compute_txid(),
617                    input: wallet.value,
618                    output: change_value,
619                    vbytes: self.cfg.consensus.receive_tx_vbytes,
620                    fee: input.fee,
621                    created,
622                },
623            )
624            .await;
625
626            dbtx.insert_new_entry(
627                &UnsignedTxKey(tx.compute_txid()),
628                &FederationTx {
629                    tx,
630                    spent_tx_outs: vec![
631                        SpentTxOut {
632                            value: wallet.value,
633                            tweak: wallet.tweak,
634                        },
635                        SpentTxOut {
636                            value: tracked_output.value,
637                            tweak: input.tweak.consensus_hash(),
638                        },
639                    ],
640                    vbytes: self.cfg.consensus.receive_tx_vbytes,
641                    fee: input.fee,
642                },
643            )
644            .await;
645        } else {
646            dbtx.insert_new_entry(
647                &FederationWalletKey,
648                &FederationWallet {
649                    value: tracked_output.value,
650                    outpoint: tracked_outpoint,
651                    tweak: input.tweak.consensus_hash(),
652                },
653            )
654            .await;
655        }
656
657        let amount = output_value
658            .to_sat()
659            .checked_mul(1000)
660            .map(fedimint_core::Amount::from_msats)
661            .ok_or(WalletInputError::ArithmeticOverflow)?;
662
663        let fee = self.cfg.consensus.fee_consensus.fee(amount);
664
665        calculate_pegin_metrics(dbtx, amount, fee);
666
667        Ok(InputMeta {
668            amount: TransactionItemAmounts {
669                amounts: Amounts::new_bitcoin(amount),
670                fees: Amounts::new_bitcoin(fee),
671            },
672            pub_key: input.tweak,
673        })
674    }
675
676    async fn process_output<'a, 'b>(
677        &'a self,
678        dbtx: &mut DatabaseTransaction<'b>,
679        output: &'a WalletOutput,
680        outpoint: OutPoint,
681    ) -> Result<TransactionItemAmounts, WalletOutputError> {
682        let output = output.ensure_v0_ref()?;
683
684        if output.value < self.cfg.consensus.dust_limit {
685            return Err(WalletOutputError::UnderDustLimit);
686        }
687
688        let wallet = dbtx
689            .remove_entry(&FederationWalletKey)
690            .await
691            .ok_or(WalletOutputError::NoFederationUTXO)?;
692
693        let consensus_send_fee = self
694            .send_fee(dbtx)
695            .await
696            .ok_or(WalletOutputError::NoConsensusFeerateAvailable)?;
697
698        // We allow for a higher fee such that a guardian could construct a CPFP
699        // transaction. This is the last line of defense should the federations
700        // transactions ever get stuck due to a critical failure of the feerate
701        // estimation.
702        if output.fee < consensus_send_fee {
703            return Err(WalletOutputError::InsufficientTotalFee);
704        }
705
706        let output_value = output
707            .value
708            .checked_add(output.fee)
709            .ok_or(WalletOutputError::ArithmeticOverflow)?;
710
711        let change_value = wallet
712            .value
713            .checked_sub(output_value)
714            .ok_or(WalletOutputError::ArithmeticOverflow)?;
715
716        if change_value < self.cfg.consensus.dust_limit {
717            return Err(WalletOutputError::ChangeUnderDustLimit);
718        }
719
720        let script_pubkey = output
721            .destination
722            .script_pubkey()
723            .ok_or(WalletOutputError::UnknownScriptVariant)?;
724
725        let tx = Transaction {
726            version: Version(2),
727            lock_time: LockTime::ZERO,
728            input: vec![TxIn {
729                previous_output: wallet.outpoint,
730                script_sig: Default::default(),
731                sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
732                witness: bitcoin::Witness::new(),
733            }],
734            output: vec![
735                TxOut {
736                    value: change_value,
737                    script_pubkey: self.descriptor(&wallet.consensus_hash()).script_pubkey(),
738                },
739                TxOut {
740                    value: output.value,
741                    script_pubkey,
742                },
743            ],
744        };
745
746        dbtx.insert_new_entry(
747            &FederationWalletKey,
748            &FederationWallet {
749                value: change_value,
750                outpoint: bitcoin::OutPoint {
751                    txid: tx.compute_txid(),
752                    vout: 0,
753                },
754                tweak: wallet.consensus_hash(),
755            },
756        )
757        .await;
758
759        let tx_index = self.total_txs(dbtx).await;
760
761        let created = self.consensus_block_count(dbtx).await;
762
763        dbtx.insert_new_entry(
764            &TxInfoKey(tx_index),
765            &TxInfo {
766                index: tx_index,
767                txid: tx.compute_txid(),
768                input: wallet.value,
769                output: change_value,
770                vbytes: self.cfg.consensus.send_tx_vbytes,
771                fee: output.fee,
772                created,
773            },
774        )
775        .await;
776
777        dbtx.insert_new_entry(&TxInfoIndexKey(outpoint), &tx_index)
778            .await;
779
780        dbtx.insert_new_entry(
781            &UnsignedTxKey(tx.compute_txid()),
782            &FederationTx {
783                tx,
784                spent_tx_outs: vec![SpentTxOut {
785                    value: wallet.value,
786                    tweak: wallet.tweak,
787                }],
788                vbytes: self.cfg.consensus.send_tx_vbytes,
789                fee: output.fee,
790            },
791        )
792        .await;
793
794        let amount = output_value
795            .to_sat()
796            .checked_mul(1000)
797            .map(fedimint_core::Amount::from_msats)
798            .ok_or(WalletOutputError::ArithmeticOverflow)?;
799
800        let fee = self.cfg.consensus.fee_consensus.fee(amount);
801
802        calculate_pegout_metrics(dbtx, amount, fee);
803
804        Ok(TransactionItemAmounts {
805            amounts: Amounts::new_bitcoin(amount),
806            fees: Amounts::new_bitcoin(fee),
807        })
808    }
809
810    async fn output_status(
811        &self,
812        _dbtx: &mut DatabaseTransaction<'_>,
813        _outpoint: OutPoint,
814    ) -> Option<WalletOutputOutcome> {
815        None
816    }
817
818    async fn audit(
819        &self,
820        dbtx: &mut DatabaseTransaction<'_>,
821        audit: &mut Audit,
822        module_instance_id: ModuleInstanceId,
823    ) {
824        audit
825            .add_items(
826                dbtx,
827                module_instance_id,
828                &FederationWalletPrefix,
829                |_, wallet| 1000 * wallet.value.to_sat() as i64,
830            )
831            .await;
832    }
833
834    fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
835        vec![
836            public_api_endpoint! {
837                CONSENSUS_BLOCK_COUNT_ENDPOINT,
838                ApiVersion::new(0, 0),
839                async |module: &Wallet, context, _params: ()| -> u64 {
840                    let db = context.db();
841                    let mut dbtx = db.begin_transaction_nc().await;
842                    Ok(module.consensus_block_count(&mut dbtx).await)
843                }
844            },
845            public_api_endpoint! {
846                CONSENSUS_FEERATE_ENDPOINT,
847                ApiVersion::new(0, 0),
848                async |module: &Wallet, context, _params: ()| -> Option<u64> {
849                    let db = context.db();
850                    let mut dbtx = db.begin_transaction_nc().await;
851                    Ok(module.consensus_feerate(&mut dbtx).await)
852                }
853            },
854            public_api_endpoint! {
855                FEDERATION_WALLET_ENDPOINT,
856                ApiVersion::new(0, 0),
857                async |_module: &Wallet, context, _params: ()| -> Option<FederationWallet> {
858                    let db = context.db();
859                    let mut dbtx = db.begin_transaction_nc().await;
860                    Ok(dbtx.get_value(&FederationWalletKey).await)
861                }
862            },
863            public_api_endpoint! {
864                SEND_FEE_ENDPOINT,
865                ApiVersion::new(0, 0),
866                async |module: &Wallet, context, _params: ()| -> Option<Amount> {
867                    let db = context.db();
868                    let mut dbtx = db.begin_transaction_nc().await;
869                    Ok(module.send_fee(&mut dbtx).await)
870                }
871            },
872            public_api_endpoint! {
873                RECEIVE_FEE_ENDPOINT,
874                ApiVersion::new(0, 0),
875                async |module: &Wallet, context, _params: ()| -> Option<Amount> {
876                    let db = context.db();
877                    let mut dbtx = db.begin_transaction_nc().await;
878                    Ok(module.receive_fee(&mut dbtx).await)
879                }
880            },
881            public_api_endpoint! {
882                TRANSACTION_ID_ENDPOINT,
883                ApiVersion::new(0, 0),
884                async |module: &Wallet, context, params: OutPoint| -> Option<Txid> {
885                    let db = context.db();
886                    let mut dbtx = db.begin_transaction_nc().await;
887                    Ok(module.tx_id(&mut dbtx, params).await)
888                }
889            },
890            public_api_endpoint! {
891                OUTPUT_INFO_SLICE_ENDPOINT,
892                ApiVersion::new(0, 0),
893                async |module: &Wallet, context, params: (u64, u64)| -> Vec<OutputInfo> {
894                    let db = context.db();
895                    let mut dbtx = db.begin_transaction_nc().await;
896                    Ok(module.get_outputs(&mut dbtx, params.0, params.1).await)
897                }
898            },
899            public_api_endpoint! {
900                PENDING_TRANSACTION_CHAIN_ENDPOINT,
901                ApiVersion::new(0, 0),
902                async |module: &Wallet, context, _params: ()| -> Vec<TxInfo> {
903                    let db = context.db();
904                    let mut dbtx = db.begin_transaction_nc().await;
905                    Ok(module.pending_tx_chain(&mut dbtx).await)
906                }
907            },
908            public_api_endpoint! {
909                TRANSACTION_CHAIN_ENDPOINT,
910                ApiVersion::new(0, 0),
911                async |module: &Wallet, context, _params: ()| -> Vec<TxInfo> {
912                    let db = context.db();
913                    let mut dbtx = db.begin_transaction_nc().await;
914                    Ok(module.tx_chain(&mut dbtx).await)
915                }
916            },
917            public_api_endpoint! {
918                PENDING_OUTPUTS_ENDPOINT,
919                ApiVersion::new(0, 2),
920                async |module: &Wallet, _context, _params: ()| -> PendingOutputs {
921                    // Deliberately reads local, non-consensus state rather than
922                    // the database. Clients must not request this via threshold
923                    // consensus; see the client side api for details.
924                    Ok(module.pending_outputs.borrow().clone())
925                }
926            },
927        ]
928    }
929
930    fn supported_api_versions(&self) -> MultiApiVersion {
931        MultiApiVersion::try_from_iter([ApiVersion::new(0, 2)])
932            .expect("walletv2 declares one API version per major version")
933    }
934}
935
936#[derive(Debug)]
937pub struct Wallet {
938    cfg: WalletConfig,
939    db: Database,
940    btc_rpc: ServerBitcoinRpcMonitor,
941    /// Local, non-consensus view of the receive outputs this guardian can see
942    /// ahead of the consensus output log: mempool candidates plus the receive
943    /// outputs of the last [`pending::MAX_PENDING_DEPTH`] blocks, the latter
944    /// retained past finality so that clients see continuous progress.
945    ///
946    /// Maintained by the `scan_pending_receives` background task and read by
947    /// the pending outputs endpoint. This is intentionally in-memory only: it
948    /// is per-guardian divergent data that has no business in the consensus
949    /// database, and it is cheap to rebuild from a handful of blocks on
950    /// restart.
951    pending_outputs: watch::Receiver<PendingOutputs>,
952}
953
954impl Wallet {
955    fn new(
956        cfg: WalletConfig,
957        db: &Database,
958        task_group: &TaskGroup,
959        btc_rpc: ServerBitcoinRpcMonitor,
960    ) -> Wallet {
961        Self::spawn_broadcast_unconfirmed_txs_task(btc_rpc.clone(), db.clone(), task_group);
962
963        let (pending_sender, pending_outputs) = watch::channel(PendingOutputs::default());
964
965        pending::spawn_pending_receive_scanner(
966            btc_rpc.clone(),
967            cfg.consensus.bitcoin_pks.consensus_hash(),
968            pending_sender,
969            task_group,
970        );
971
972        Wallet {
973            cfg,
974            btc_rpc,
975            db: db.clone(),
976            pending_outputs,
977        }
978    }
979
980    fn spawn_broadcast_unconfirmed_txs_task(
981        btc_rpc: ServerBitcoinRpcMonitor,
982        db: Database,
983        task_group: &TaskGroup,
984    ) {
985        task_group.spawn_cancellable("broadcast_unconfirmed_transactions", async move {
986            loop {
987                let unconfirmed_txs = db
988                    .begin_transaction_nc()
989                    .await
990                    .find_by_prefix(&UnconfirmedTxPrefix)
991                    .await
992                    .map(|entry| entry.1)
993                    .collect::<Vec<FederationTx>>()
994                    .await;
995
996                for unconfirmed_tx in unconfirmed_txs {
997                    if let Err(err) = btc_rpc.submit_transaction(unconfirmed_tx.tx).await {
998                        debug!(
999                            target: LOG_MODULE_WALLETV2,
1000                            err = %err.fmt_compact(),
1001                            "Error broadcasting unconfirmed transaction"
1002                        );
1003                    }
1004                }
1005
1006                sleep(common::sleep_duration()).await;
1007            }
1008        });
1009    }
1010
1011    async fn process_block_count(
1012        &self,
1013        dbtx: &mut DatabaseTransaction<'_>,
1014        block_count_vote: u64,
1015        peer: PeerId,
1016    ) -> anyhow::Result<()> {
1017        let old_consensus_block_count = self.consensus_block_count(dbtx).await;
1018
1019        let current_vote = dbtx
1020            .insert_entry(&BlockCountVoteKey(peer), &block_count_vote)
1021            .await
1022            .unwrap_or(0);
1023
1024        ensure!(
1025            current_vote < block_count_vote,
1026            "Block count vote is redundant"
1027        );
1028
1029        let new_consensus_block_count = self.consensus_block_count(dbtx).await;
1030
1031        assert!(old_consensus_block_count <= new_consensus_block_count);
1032
1033        debug!(
1034            target: LOG_MODULE_WALLETV2,
1035            %peer,
1036            vote = block_count_vote,
1037            old_consensus = old_consensus_block_count,
1038            new_consensus = new_consensus_block_count,
1039            advanced = new_consensus_block_count - old_consensus_block_count,
1040            "Processed block count vote"
1041        );
1042
1043        // Outside regtest, do not sync blocks that predate the federation itself.
1044        // Regtest starts from scratch, so scan from genesis to avoid races where
1045        // test deposits are mined before the first walletv2 block count
1046        // transition is processed.
1047        let scan_from_genesis = self.cfg.consensus.network == bitcoin::Network::Regtest;
1048        if old_consensus_block_count == 0 && !scan_from_genesis {
1049            return Ok(());
1050        }
1051
1052        // Our bitcoin backend needs to be synced for the following calls to the
1053        // get_block rpc to be safe for consensus.
1054        self.await_local_sync_to_block_count(
1055            new_consensus_block_count + CONFIRMATION_FINALITY_DELAY,
1056        )
1057        .await;
1058
1059        for height in old_consensus_block_count..new_consensus_block_count {
1060            // Verify network matches (status should be available after sync)
1061            if let Some(status) = self.btc_rpc.status() {
1062                assert_eq!(status.network, self.cfg.consensus.network);
1063            }
1064
1065            let block_hash = util::retry(
1066                "get_block_hash",
1067                util::backoff_util::background_backoff(),
1068                || self.btc_rpc.get_block_hash(height),
1069            )
1070            .await
1071            .expect("Bitcoind rpc to get_block_hash failed");
1072
1073            let block = util::retry(
1074                "get_block",
1075                util::backoff_util::background_backoff(),
1076                || self.btc_rpc.get_block(&block_hash),
1077            )
1078            .await
1079            .expect("Bitcoind rpc to get_block failed");
1080
1081            assert_eq!(block.block_hash(), block_hash, "Block hash mismatch");
1082
1083            let pks_hash = self.cfg.consensus.bitcoin_pks.consensus_hash();
1084
1085            let txs_num = block.txdata.len();
1086            let mut potential_receives_num: usize = 0;
1087
1088            for tx in block.txdata {
1089                dbtx.remove_entry(&UnconfirmedTxKey(tx.compute_txid()))
1090                    .await;
1091
1092                // We maintain an append-only log of transaction outputs that pass
1093                // the probabilistic receive filter created since the federation was
1094                // established. This is downloaded by clients to detect pegins and
1095                // claim them by index.
1096
1097                for (vout, tx_out) in tx.output.iter().enumerate() {
1098                    if is_potential_receive(&tx_out.script_pubkey, &pks_hash) {
1099                        let outpoint = bitcoin::OutPoint {
1100                            txid: tx.compute_txid(),
1101                            vout: u32::try_from(vout)
1102                                .expect("Bitcoin transaction has more than u32::MAX outputs"),
1103                        };
1104
1105                        let index = dbtx
1106                            .find_by_prefix_sorted_descending(&OutputPrefix)
1107                            .await
1108                            .next()
1109                            .await
1110                            .map_or(0, |entry| entry.0.0 + 1);
1111
1112                        dbtx.insert_new_entry(&OutputKey(index), &Output(outpoint, tx_out.clone()))
1113                            .await;
1114
1115                        debug!(
1116                            target: LOG_MODULE_WALLETV2,
1117                            output_index = index,
1118                            %outpoint,
1119                            value_sat = tx_out.value.to_sat(),
1120                            height,
1121                            "Recorded potential walletv2 receive"
1122                        );
1123
1124                        potential_receives_num += 1;
1125                    }
1126                }
1127            }
1128
1129            debug!(
1130                target: LOG_MODULE_WALLETV2,
1131                height,
1132                txs_num,
1133                potential_receives_num,
1134                "Scanned block"
1135            );
1136        }
1137
1138        Ok(())
1139    }
1140
1141    async fn process_signatures(
1142        &self,
1143        dbtx: &mut DatabaseTransaction<'_>,
1144        txid: bitcoin::Txid,
1145        signatures: Vec<Signature>,
1146        peer: PeerId,
1147    ) -> anyhow::Result<()> {
1148        let mut unsigned = dbtx
1149            .get_value(&UnsignedTxKey(txid))
1150            .await
1151            .context("Unsigned transaction does not exist")?;
1152
1153        let pk = self
1154            .cfg
1155            .consensus
1156            .bitcoin_pks
1157            .get(&peer)
1158            .expect("Failed to get public key of peer from config");
1159
1160        self.verify_signatures(&unsigned, &signatures, *pk)?;
1161
1162        if dbtx
1163            .insert_entry(&SignaturesKey(txid, peer), &signatures)
1164            .await
1165            .is_some()
1166        {
1167            bail!("Already received valid signatures from this peer")
1168        }
1169
1170        let signatures = dbtx
1171            .find_by_prefix(&SignaturesTxidPrefix(txid))
1172            .await
1173            .map(|(key, signatures)| (key.1, signatures))
1174            .collect::<BTreeMap<PeerId, Vec<Signature>>>()
1175            .await;
1176
1177        if signatures.len() == self.cfg.consensus.bitcoin_pks.to_num_peers().threshold() {
1178            dbtx.remove_entry(&UnsignedTxKey(txid)).await;
1179
1180            dbtx.remove_by_prefix(&SignaturesTxidPrefix(txid)).await;
1181
1182            self.finalize_tx(&mut unsigned, &signatures);
1183
1184            dbtx.insert_new_entry(&UnconfirmedTxKey(txid), &unsigned)
1185                .await;
1186
1187            if let Err(err) = self.btc_rpc.submit_transaction(unsigned.tx).await {
1188                debug!(
1189                    target: LOG_MODULE_WALLETV2,
1190                    err = %err.fmt_compact(),
1191                    "Error broadcasting finalized transaction"
1192                );
1193            }
1194        }
1195
1196        Ok(())
1197    }
1198
1199    async fn await_local_sync_to_block_count(&self, block_count: u64) {
1200        loop {
1201            if self
1202                .btc_rpc
1203                .status()
1204                .is_some_and(|status| status.block_count >= block_count)
1205            {
1206                break;
1207            }
1208
1209            info!(target: LOG_MODULE_WALLETV2, "Waiting for local bitcoin backend to sync to block count {block_count}");
1210
1211            sleep(common::sleep_duration()).await;
1212        }
1213    }
1214
1215    pub async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1216        let num_peers = self.cfg.consensus.bitcoin_pks.to_num_peers();
1217
1218        let mut counts = dbtx
1219            .find_by_prefix(&BlockCountVotePrefix)
1220            .await
1221            .map(|entry| entry.1)
1222            .collect::<Vec<u64>>()
1223            .await;
1224
1225        assert!(counts.len() <= num_peers.total());
1226
1227        counts.sort_unstable();
1228
1229        counts.reverse();
1230
1231        assert!(counts.last() <= counts.first());
1232
1233        // The block count we select guarantees that any threshold of correct peers can
1234        // increase the consensus block count and any consensus block count has been
1235        // confirmed by a threshold of peers.
1236
1237        counts.get(num_peers.threshold() - 1).copied().unwrap_or(0)
1238    }
1239
1240    pub async fn consensus_feerate(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<u64> {
1241        let num_peers = self.cfg.consensus.bitcoin_pks.to_num_peers();
1242
1243        let mut rates = dbtx
1244            .find_by_prefix(&FeeRateVotePrefix)
1245            .await
1246            .filter_map(|entry| async move { entry.1 })
1247            .collect::<Vec<u64>>()
1248            .await;
1249
1250        assert!(rates.len() <= num_peers.total());
1251
1252        rates.sort_unstable();
1253
1254        assert!(rates.first() <= rates.last());
1255
1256        rates.get(num_peers.threshold() - 1).copied()
1257    }
1258
1259    pub async fn consensus_fee(
1260        &self,
1261        dbtx: &mut DatabaseTransaction<'_>,
1262        tx_vbytes: u64,
1263    ) -> Option<Amount> {
1264        // The minimum feerate is a protection against a catastrophic error in the
1265        // feerate estimation and limits the length of the pending transaction stack.
1266
1267        let pending_txs = pending_txs_unordered(dbtx).await;
1268
1269        assert!(pending_txs.len() <= 32);
1270
1271        let feerate = self
1272            .consensus_feerate(dbtx)
1273            .await?
1274            .max(self.cfg.consensus.feerate_base << pending_txs.len());
1275
1276        let tx_fee = tx_vbytes.saturating_mul(feerate).saturating_div(1000);
1277
1278        let stack_vbytes = pending_txs
1279            .iter()
1280            .map(|t| t.vbytes)
1281            .try_fold(tx_vbytes, u64::checked_add)
1282            .expect("Stack vbytes overflow with at most 32 pending txs");
1283
1284        let stack_fee = stack_vbytes.saturating_mul(feerate).saturating_div(1000);
1285
1286        // Deduct the fees already paid by currently pending transactions
1287        let stack_fee = pending_txs
1288            .iter()
1289            .map(|t| t.fee.to_sat())
1290            .fold(stack_fee, u64::saturating_sub);
1291
1292        Some(Amount::from_sat(tx_fee.max(stack_fee)))
1293    }
1294
1295    pub async fn send_fee(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<Amount> {
1296        self.consensus_fee(dbtx, self.cfg.consensus.send_tx_vbytes)
1297            .await
1298    }
1299
1300    pub async fn receive_fee(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<Amount> {
1301        self.consensus_fee(dbtx, self.cfg.consensus.receive_tx_vbytes)
1302            .await
1303    }
1304
1305    fn descriptor(&self, tweak: &sha256::Hash) -> Wsh<secp256k1::PublicKey> {
1306        descriptor(&self.cfg.consensus.bitcoin_pks, tweak)
1307    }
1308
1309    fn sign_tx(&self, unsigned_tx: &FederationTx) -> Vec<Signature> {
1310        let mut sighash_cache = SighashCache::new(unsigned_tx.tx.clone());
1311
1312        unsigned_tx
1313            .spent_tx_outs
1314            .iter()
1315            .enumerate()
1316            .map(|(index, utxo)| {
1317                let descriptor = self.descriptor(&utxo.tweak).ecdsa_sighash_script_code();
1318
1319                let p2wsh_sighash = sighash_cache
1320                    .p2wsh_signature_hash(index, &descriptor, utxo.value, EcdsaSighashType::All)
1321                    .expect("Failed to compute P2WSH segwit sighash");
1322
1323                let scalar = &Scalar::from_be_bytes(utxo.tweak.to_byte_array())
1324                    .expect("Hash is within field order");
1325
1326                let sk = self
1327                    .cfg
1328                    .private
1329                    .bitcoin_sk
1330                    .add_tweak(scalar)
1331                    .expect("Failed to tweak bitcoin secret key");
1332
1333                Secp256k1::new().sign_ecdsa(&p2wsh_sighash.into(), &sk)
1334            })
1335            .collect()
1336    }
1337
1338    fn verify_signatures(
1339        &self,
1340        unsigned_tx: &FederationTx,
1341        signatures: &[Signature],
1342        pk: PublicKey,
1343    ) -> anyhow::Result<()> {
1344        ensure!(
1345            unsigned_tx.spent_tx_outs.len() == signatures.len(),
1346            "Incorrect number of signatures"
1347        );
1348
1349        let mut sighash_cache = SighashCache::new(unsigned_tx.tx.clone());
1350
1351        for ((index, utxo), signature) in unsigned_tx
1352            .spent_tx_outs
1353            .iter()
1354            .enumerate()
1355            .zip(signatures.iter())
1356        {
1357            let code = self.descriptor(&utxo.tweak).ecdsa_sighash_script_code();
1358
1359            let p2wsh_sighash = sighash_cache
1360                .p2wsh_signature_hash(index, &code, utxo.value, EcdsaSighashType::All)
1361                .expect("Failed to compute P2WSH segwit sighash");
1362
1363            let pk = tweak_public_key(&pk, &utxo.tweak);
1364
1365            secp256k1::SECP256K1.verify_ecdsa(&p2wsh_sighash.into(), signature, &pk)?;
1366        }
1367
1368        Ok(())
1369    }
1370
1371    fn finalize_tx(
1372        &self,
1373        federation_tx: &mut FederationTx,
1374        signatures: &BTreeMap<PeerId, Vec<Signature>>,
1375    ) {
1376        assert_eq!(
1377            federation_tx.spent_tx_outs.len(),
1378            federation_tx.tx.input.len()
1379        );
1380
1381        for (index, utxo) in federation_tx.spent_tx_outs.iter().enumerate() {
1382            let satisfier: BTreeMap<PublicKey, bitcoin::ecdsa::Signature> = signatures
1383                .iter()
1384                .map(|(peer, sigs)| {
1385                    assert_eq!(sigs.len(), federation_tx.tx.input.len());
1386
1387                    let pk = *self
1388                        .cfg
1389                        .consensus
1390                        .bitcoin_pks
1391                        .get(peer)
1392                        .expect("Failed to get public key of peer from config");
1393
1394                    let pk = tweak_public_key(&pk, &utxo.tweak);
1395
1396                    (pk, bitcoin::ecdsa::Signature::sighash_all(sigs[index]))
1397                })
1398                .collect();
1399
1400            miniscript::Descriptor::Wsh(self.descriptor(&utxo.tweak))
1401                .satisfy(&mut federation_tx.tx.input[index], satisfier)
1402                .expect("Failed to satisfy descriptor");
1403        }
1404    }
1405
1406    async fn tx_id(&self, dbtx: &mut DatabaseTransaction<'_>, outpoint: OutPoint) -> Option<Txid> {
1407        let index = dbtx.get_value(&TxInfoIndexKey(outpoint)).await?;
1408
1409        dbtx.get_value(&TxInfoKey(index))
1410            .await
1411            .map(|entry| entry.txid)
1412    }
1413
1414    async fn get_outputs(
1415        &self,
1416        dbtx: &mut DatabaseTransaction<'_>,
1417        start_index: u64,
1418        end_index: u64,
1419    ) -> Vec<OutputInfo> {
1420        let spent: BTreeSet<u64> = dbtx
1421            .find_by_range(SpentOutputKey(start_index)..SpentOutputKey(end_index))
1422            .await
1423            .map(|entry| entry.0.0)
1424            .collect()
1425            .await;
1426
1427        dbtx.find_by_range(OutputKey(start_index)..OutputKey(end_index))
1428            .await
1429            .filter_map(|entry| {
1430                std::future::ready(entry.1.1.script_pubkey.is_p2wsh().then(|| OutputInfo {
1431                    index: entry.0.0,
1432                    script: entry.1.1.script_pubkey,
1433                    value: entry.1.1.value,
1434                    spent: spent.contains(&entry.0.0),
1435                    outpoint: Some(entry.1.0),
1436                }))
1437            })
1438            .collect()
1439            .await
1440    }
1441
1442    async fn pending_tx_chain(&self, dbtx: &mut DatabaseTransaction<'_>) -> Vec<TxInfo> {
1443        let n_pending = pending_txs_unordered(dbtx).await.len();
1444
1445        dbtx.find_by_prefix_sorted_descending(&TxInfoPrefix)
1446            .await
1447            .take(n_pending)
1448            .map(|entry| entry.1)
1449            .collect()
1450            .await
1451    }
1452
1453    async fn tx_chain(&self, dbtx: &mut DatabaseTransaction<'_>) -> Vec<TxInfo> {
1454        dbtx.find_by_prefix(&TxInfoPrefix)
1455            .await
1456            .map(|entry| entry.1)
1457            .collect()
1458            .await
1459    }
1460
1461    async fn total_txs(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1462        dbtx.find_by_prefix_sorted_descending(&TxInfoPrefix)
1463            .await
1464            .next()
1465            .await
1466            .map_or(0, |entry| entry.0.0 + 1)
1467    }
1468
1469    /// Get the network for UI display
1470    pub fn network_ui(&self) -> Network {
1471        self.cfg.consensus.network
1472    }
1473
1474    /// Get the current federation wallet info for UI display
1475    pub async fn federation_wallet_ui(&self) -> Option<FederationWallet> {
1476        self.db
1477            .begin_transaction_nc()
1478            .await
1479            .get_value(&FederationWalletKey)
1480            .await
1481    }
1482
1483    /// Get the current consensus block count for UI display
1484    pub async fn consensus_block_count_ui(&self) -> u64 {
1485        self.consensus_block_count(&mut self.db.begin_transaction_nc().await)
1486            .await
1487    }
1488
1489    /// Get the current consensus feerate for UI display
1490    pub async fn consensus_feerate_ui(&self) -> Option<u64> {
1491        self.consensus_feerate(&mut self.db.begin_transaction_nc().await)
1492            .await
1493            .map(|feerate| feerate / 1000)
1494    }
1495
1496    /// Get the current send fee for UI display
1497    pub async fn send_fee_ui(&self) -> Option<Amount> {
1498        self.send_fee(&mut self.db.begin_transaction_nc().await)
1499            .await
1500    }
1501
1502    /// Get the current receive fee for UI display
1503    pub async fn receive_fee_ui(&self) -> Option<Amount> {
1504        self.receive_fee(&mut self.db.begin_transaction_nc().await)
1505            .await
1506    }
1507
1508    /// Get the current pending transaction info for UI display
1509    pub async fn pending_tx_chain_ui(&self) -> Vec<TxInfo> {
1510        self.pending_tx_chain(&mut self.db.begin_transaction_nc().await)
1511            .await
1512    }
1513
1514    /// Get the current transaction log for UI display
1515    pub async fn tx_chain_ui(&self) -> Vec<TxInfo> {
1516        self.tx_chain(&mut self.db.begin_transaction_nc().await)
1517            .await
1518    }
1519
1520    /// Export recovery keys for federation shutdown. Returns None if the
1521    /// federation wallet has not been initialized yet.
1522    pub async fn recovery_keys_ui(&self) -> Option<(BTreeMap<PeerId, String>, String)> {
1523        let wallet = self.federation_wallet_ui().await?;
1524
1525        let pks = self
1526            .cfg
1527            .consensus
1528            .bitcoin_pks
1529            .iter()
1530            .map(|(peer, pk)| (*peer, tweak_public_key(pk, &wallet.tweak).to_string()))
1531            .collect();
1532
1533        let tweak = &Scalar::from_be_bytes(wallet.tweak.to_byte_array())
1534            .expect("Hash is within field order");
1535
1536        let sk = self
1537            .cfg
1538            .private
1539            .bitcoin_sk
1540            .add_tweak(tweak)
1541            .expect("Failed to tweak bitcoin secret key");
1542
1543        let sk = bitcoin::PrivateKey::new(sk, self.cfg.consensus.network).to_wif();
1544
1545        Some((pks, sk))
1546    }
1547}
1548
1549fn calculate_pegin_metrics(
1550    dbtx: &mut DatabaseTransaction<'_>,
1551    amount: fedimint_core::Amount,
1552    fee: fedimint_core::Amount,
1553) {
1554    dbtx.on_commit(move || {
1555        WALLET_INOUT_SATS
1556            .with_label_values(&["incoming"])
1557            .observe(amount.sats_f64());
1558        WALLET_INOUT_FEES_SATS
1559            .with_label_values(&["incoming"])
1560            .observe(fee.sats_f64());
1561        WALLET_PEGIN_SATS.observe(amount.sats_f64());
1562        WALLET_PEGIN_FEES_SATS.observe(fee.sats_f64());
1563    });
1564}
1565
1566fn calculate_pegout_metrics(
1567    dbtx: &mut DatabaseTransaction<'_>,
1568    amount: fedimint_core::Amount,
1569    fee: fedimint_core::Amount,
1570) {
1571    dbtx.on_commit(move || {
1572        WALLET_INOUT_SATS
1573            .with_label_values(&["outgoing"])
1574            .observe(amount.sats_f64());
1575        WALLET_INOUT_FEES_SATS
1576            .with_label_values(&["outgoing"])
1577            .observe(fee.sats_f64());
1578        WALLET_PEGOUT_SATS.observe(amount.sats_f64());
1579        WALLET_PEGOUT_FEES_SATS.observe(fee.sats_f64());
1580    });
1581}