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