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