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