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