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 let db = context.db();
925 let mut dbtx = db.begin_transaction_nc().await;
926 Ok(module.consensus_block_count(&mut dbtx).await)
927 }
928 },
929 api_endpoint! {
930 BLOCK_COUNT_LOCAL_ENDPOINT,
931 ApiVersion::new(0, 0),
932 async |module: &Wallet, _context, _params: ()| -> Option<u32> {
933 Ok(module.get_block_count().ok())
934 }
935 },
936 api_endpoint! {
937 PEG_OUT_FEES_ENDPOINT,
938 ApiVersion::new(0, 0),
939 async |module: &Wallet, context, params: (Address<NetworkUnchecked>, u64)| -> Option<PegOutFees> {
940 let (address, sats) = params;
941 let db = context.db();
942 let mut dbtx = db.begin_transaction_nc().await;
943 let feerate = module.consensus_fee_rate(&mut dbtx).await;
944
945 let dummy_tweak = [0; 33];
947
948 let tx = module.offline_wallet().create_tx(
949 bitcoin::Amount::from_sat(sats),
950 address.assume_checked().script_pubkey(),
954 vec![],
955 module.available_utxos(&mut dbtx).await,
956 feerate,
957 &dummy_tweak,
958 None
959 );
960
961 match tx {
962 Err(error) => {
963 warn!(target: LOG_MODULE_WALLET, "Error returning peg-out fees {error}");
965 Ok(None)
966 }
967 Ok(tx) => Ok(Some(tx.fees))
968 }
969 }
970 },
971 api_endpoint! {
972 BITCOIN_KIND_ENDPOINT,
973 ApiVersion::new(0, 1),
974 async |module: &Wallet, _context, _params: ()| -> String {
975 Ok(module.btc_rpc.get_bitcoin_rpc_config().kind)
976 }
977 },
978 api_endpoint! {
979 BITCOIN_RPC_CONFIG_ENDPOINT,
980 ApiVersion::new(0, 1),
981 async |module: &Wallet, context, _params: ()| -> BitcoinRpcConfig {
982 check_auth(context)?;
983 let config = module.btc_rpc.get_bitcoin_rpc_config();
984
985 let without_auth = config.url.clone().without_auth().map_err(|()| {
987 ApiError::server_error("Unable to remove auth from bitcoin config URL".to_string())
988 })?;
989
990 Ok(BitcoinRpcConfig {
991 url: without_auth,
992 ..config
993 })
994 }
995 },
996 api_endpoint! {
997 WALLET_SUMMARY_ENDPOINT,
998 ApiVersion::new(0, 1),
999 async |module: &Wallet, context, _params: ()| -> WalletSummary {
1000 let db = context.db();
1001 let mut dbtx = db.begin_transaction_nc().await;
1002 Ok(module.get_wallet_summary(&mut dbtx).await)
1003 }
1004 },
1005 api_endpoint! {
1006 MODULE_CONSENSUS_VERSION_ENDPOINT,
1007 ApiVersion::new(0, 2),
1008 async |module: &Wallet, context, _params: ()| -> ModuleConsensusVersion {
1009 let db = context.db();
1010 let mut dbtx = db.begin_transaction_nc().await;
1011 Ok(module.consensus_module_consensus_version(&mut dbtx).await)
1012 }
1013 },
1014 api_endpoint! {
1015 SUPPORTED_MODULE_CONSENSUS_VERSION_ENDPOINT,
1016 ApiVersion::new(0, 2),
1017 async |_module: &Wallet, _context, _params: ()| -> ModuleConsensusVersion {
1018 Ok(MODULE_CONSENSUS_VERSION)
1019 }
1020 },
1021 api_endpoint! {
1022 ACTIVATE_CONSENSUS_VERSION_VOTING_ENDPOINT,
1023 ApiVersion::new(0, 2),
1024 async |_module: &Wallet, context, _params: ()| -> () {
1025 check_auth(context)?;
1026
1027 let db = context.db();
1028 let mut dbtx = db.begin_transaction().await;
1029 dbtx.to_ref().insert_entry(&ConsensusVersionVotingActivationKey, &()).await;
1030 dbtx.commit_tx_result().await?;
1031 Ok(())
1032 }
1033 },
1034 api_endpoint! {
1035 UTXO_CONFIRMED_ENDPOINT,
1036 ApiVersion::new(0, 2),
1037 async |module: &Wallet, context, outpoint: bitcoin::OutPoint| -> bool {
1038 let db = context.db();
1039 let mut dbtx = db.begin_transaction_nc().await;
1040 Ok(module.is_utxo_confirmed(&mut dbtx, outpoint).await)
1041 }
1042 },
1043 ]
1044 }
1045}
1046
1047fn calculate_pegin_metrics(
1048 dbtx: &mut DatabaseTransaction<'_>,
1049 amount: fedimint_core::Amount,
1050 fee: fedimint_core::Amount,
1051) {
1052 dbtx.on_commit(move || {
1053 WALLET_INOUT_SATS
1054 .with_label_values(&["incoming"])
1055 .observe(amount.sats_f64());
1056 WALLET_INOUT_FEES_SATS
1057 .with_label_values(&["incoming"])
1058 .observe(fee.sats_f64());
1059 WALLET_PEGIN_SATS.observe(amount.sats_f64());
1060 WALLET_PEGIN_FEES_SATS.observe(fee.sats_f64());
1061 });
1062}
1063
1064fn calculate_pegout_metrics(
1065 dbtx: &mut DatabaseTransaction<'_>,
1066 amount: fedimint_core::Amount,
1067 fee: fedimint_core::Amount,
1068) {
1069 dbtx.on_commit(move || {
1070 WALLET_INOUT_SATS
1071 .with_label_values(&["outgoing"])
1072 .observe(amount.sats_f64());
1073 WALLET_INOUT_FEES_SATS
1074 .with_label_values(&["outgoing"])
1075 .observe(fee.sats_f64());
1076 WALLET_PEGOUT_SATS.observe(amount.sats_f64());
1077 WALLET_PEGOUT_FEES_SATS.observe(fee.sats_f64());
1078 });
1079}
1080
1081#[derive(Debug)]
1082pub struct Wallet {
1083 cfg: WalletConfig,
1084 db: Database,
1085 secp: Secp256k1<All>,
1086 btc_rpc: ServerBitcoinRpcMonitor,
1087 our_peer_id: PeerId,
1088 broadcast_pending: Arc<Notify>,
1090 task_group: TaskGroup,
1091 peer_supported_consensus_version: watch::Receiver<Option<ModuleConsensusVersion>>,
1095}
1096
1097impl Wallet {
1098 pub async fn new(
1099 cfg: WalletConfig,
1100 db: &Database,
1101 task_group: &TaskGroup,
1102 our_peer_id: PeerId,
1103 module_api: DynModuleApi,
1104 server_bitcoin_rpc_monitor: ServerBitcoinRpcMonitor,
1105 ) -> anyhow::Result<Wallet> {
1106 let broadcast_pending = Arc::new(Notify::new());
1107 Self::spawn_broadcast_pending_task(
1108 task_group,
1109 &server_bitcoin_rpc_monitor,
1110 db,
1111 broadcast_pending.clone(),
1112 );
1113
1114 let peer_supported_consensus_version =
1115 Self::spawn_peer_supported_consensus_version_task(module_api, task_group, our_peer_id);
1116
1117 let status = retry("verify network", backoff_util::aggressive_backoff(), || {
1118 std::future::ready(
1119 server_bitcoin_rpc_monitor
1120 .status()
1121 .context("No connection to bitcoin rpc"),
1122 )
1123 })
1124 .await?;
1125
1126 ensure!(status.network == cfg.consensus.network.0, "Wrong Network");
1127
1128 let wallet = Wallet {
1129 cfg,
1130 db: db.clone(),
1131 secp: Default::default(),
1132 btc_rpc: server_bitcoin_rpc_monitor,
1133 our_peer_id,
1134 task_group: task_group.clone(),
1135 peer_supported_consensus_version,
1136 broadcast_pending,
1137 };
1138
1139 Ok(wallet)
1140 }
1141
1142 fn sign_peg_out_psbt(
1144 &self,
1145 psbt: &mut Psbt,
1146 peer: PeerId,
1147 signature: &PegOutSignatureItem,
1148 ) -> Result<(), ProcessPegOutSigError> {
1149 let peer_key = self
1150 .cfg
1151 .consensus
1152 .peer_peg_in_keys
1153 .get(&peer)
1154 .expect("always called with valid peer id");
1155
1156 if psbt.inputs.len() != signature.signature.len() {
1157 return Err(ProcessPegOutSigError::WrongSignatureCount(
1158 psbt.inputs.len(),
1159 signature.signature.len(),
1160 ));
1161 }
1162
1163 let mut tx_hasher = SighashCache::new(&psbt.unsigned_tx);
1164 for (idx, (input, signature)) in psbt
1165 .inputs
1166 .iter_mut()
1167 .zip(signature.signature.iter())
1168 .enumerate()
1169 {
1170 let tx_hash = tx_hasher
1171 .p2wsh_signature_hash(
1172 idx,
1173 input
1174 .witness_script
1175 .as_ref()
1176 .expect("Missing witness script"),
1177 input.witness_utxo.as_ref().expect("Missing UTXO").value,
1178 EcdsaSighashType::All,
1179 )
1180 .map_err(|_| ProcessPegOutSigError::SighashError)?;
1181
1182 let tweak = input
1183 .proprietary
1184 .get(&proprietary_tweak_key())
1185 .expect("we saved it with a tweak");
1186
1187 let tweaked_peer_key = peer_key.tweak(tweak, &self.secp);
1188 self.secp
1189 .verify_ecdsa(
1190 &Message::from_digest_slice(&tx_hash[..]).unwrap(),
1191 signature,
1192 &tweaked_peer_key.key,
1193 )
1194 .map_err(|_| ProcessPegOutSigError::InvalidSignature)?;
1195
1196 if input
1197 .partial_sigs
1198 .insert(tweaked_peer_key.into(), EcdsaSig::sighash_all(*signature))
1199 .is_some()
1200 {
1201 return Err(ProcessPegOutSigError::DuplicateSignature);
1203 }
1204 }
1205 Ok(())
1206 }
1207
1208 fn finalize_peg_out_psbt(
1209 &self,
1210 mut unsigned: UnsignedTransaction,
1211 ) -> Result<PendingTransaction, ProcessPegOutSigError> {
1212 let change_tweak: [u8; 33] = unsigned
1217 .psbt
1218 .outputs
1219 .iter()
1220 .find_map(|output| output.proprietary.get(&proprietary_tweak_key()).cloned())
1221 .ok_or(ProcessPegOutSigError::MissingOrMalformedChangeTweak)?
1222 .try_into()
1223 .map_err(|_| ProcessPegOutSigError::MissingOrMalformedChangeTweak)?;
1224
1225 if let Err(error) = unsigned.psbt.finalize_mut(&self.secp) {
1226 return Err(ProcessPegOutSigError::ErrorFinalizingPsbt(error));
1227 }
1228
1229 let tx = unsigned.psbt.clone().extract_tx_unchecked_fee_rate();
1230
1231 Ok(PendingTransaction {
1232 tx,
1233 tweak: change_tweak,
1234 change: unsigned.change,
1235 destination: unsigned.destination,
1236 fees: unsigned.fees,
1237 selected_utxos: unsigned.selected_utxos,
1238 peg_out_amount: unsigned.peg_out_amount,
1239 rbf: unsigned.rbf,
1240 })
1241 }
1242
1243 fn get_block_count(&self) -> anyhow::Result<u32> {
1244 self.btc_rpc
1245 .status()
1246 .context("No bitcoin rpc connection")
1247 .and_then(|status| {
1248 status
1249 .block_count
1250 .try_into()
1251 .map_err(|_| format_err!("Block count exceeds u32 limits"))
1252 })
1253 }
1254
1255 pub fn get_fee_rate_opt(&self) -> Feerate {
1256 #[allow(clippy::cast_precision_loss)]
1259 #[allow(clippy::cast_sign_loss)]
1260 Feerate {
1261 sats_per_kvb: ((self
1262 .btc_rpc
1263 .status()
1264 .map_or(self.cfg.consensus.default_fee, |status| status.fee_rate)
1265 .sats_per_kvb as f64
1266 * get_feerate_multiplier())
1267 .round()) as u64,
1268 }
1269 }
1270
1271 pub async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u32 {
1272 let peer_count = self.cfg.consensus.peer_peg_in_keys.to_num_peers().total();
1273
1274 let mut counts = dbtx
1275 .find_by_prefix(&BlockCountVotePrefix)
1276 .await
1277 .map(|entry| entry.1)
1278 .collect::<Vec<u32>>()
1279 .await;
1280
1281 assert!(counts.len() <= peer_count);
1282
1283 while counts.len() < peer_count {
1284 counts.push(0);
1285 }
1286
1287 counts.sort_unstable();
1288
1289 counts[peer_count / 2]
1290 }
1291
1292 pub async fn consensus_fee_rate(&self, dbtx: &mut DatabaseTransaction<'_>) -> Feerate {
1293 let peer_count = self.cfg.consensus.peer_peg_in_keys.to_num_peers().total();
1294
1295 let mut rates = dbtx
1296 .find_by_prefix(&FeeRateVotePrefix)
1297 .await
1298 .map(|(.., rate)| rate)
1299 .collect::<Vec<_>>()
1300 .await;
1301
1302 assert!(rates.len() <= peer_count);
1303
1304 while rates.len() < peer_count {
1305 rates.push(self.cfg.consensus.default_fee);
1306 }
1307
1308 rates.sort_unstable();
1309
1310 rates[peer_count / 2]
1311 }
1312
1313 async fn consensus_module_consensus_version(
1314 &self,
1315 dbtx: &mut DatabaseTransaction<'_>,
1316 ) -> ModuleConsensusVersion {
1317 let num_peers = self.cfg.consensus.peer_peg_in_keys.to_num_peers();
1318
1319 let mut versions = dbtx
1320 .find_by_prefix(&ConsensusVersionVotePrefix)
1321 .await
1322 .map(|entry| entry.1)
1323 .collect::<Vec<ModuleConsensusVersion>>()
1324 .await;
1325
1326 while versions.len() < num_peers.total() {
1327 versions.push(ModuleConsensusVersion::new(2, 0));
1328 }
1329
1330 assert_eq!(versions.len(), num_peers.total());
1331
1332 versions.sort_unstable();
1333
1334 assert!(versions.first() <= versions.last());
1335
1336 versions[num_peers.max_evil()]
1337 }
1338
1339 pub async fn consensus_nonce(&self, dbtx: &mut DatabaseTransaction<'_>) -> [u8; 33] {
1340 let nonce_idx = dbtx.get_value(&PegOutNonceKey).await.unwrap_or(0);
1341 dbtx.insert_entry(&PegOutNonceKey, &(nonce_idx + 1)).await;
1342
1343 nonce_from_idx(nonce_idx)
1344 }
1345
1346 async fn sync_up_to_consensus_count(
1347 &self,
1348 dbtx: &mut DatabaseTransaction<'_>,
1349 old_count: u32,
1350 new_count: u32,
1351 ) {
1352 info!(
1353 target: LOG_MODULE_WALLET,
1354 old_count,
1355 new_count,
1356 blocks_to_go = new_count - old_count,
1357 "New block count consensus, initiating sync",
1358 );
1359
1360 self.wait_for_finality_confs_or_shutdown(new_count).await;
1363
1364 for height in old_count..new_count {
1365 info!(
1366 target: LOG_MODULE_WALLET,
1367 height,
1368 "Processing block of height {height}",
1369 );
1370
1371 trace!(block = height, "Fetching block hash");
1373 let block_hash = retry("get_block_hash", backoff_util::background_backoff(), || {
1374 self.btc_rpc.get_block_hash(u64::from(height)) })
1376 .await
1377 .expect("bitcoind rpc to get block hash");
1378
1379 let block = retry("get_block", backoff_util::background_backoff(), || {
1380 self.btc_rpc.get_block(&block_hash)
1381 })
1382 .await
1383 .expect("bitcoind rpc to get block");
1384
1385 if let Some(prev_block_height) = height.checked_sub(1) {
1386 if let Some(hash) = dbtx
1387 .get_value(&BlockHashByHeightKey(prev_block_height))
1388 .await
1389 {
1390 assert_eq!(block.header.prev_blockhash, hash.0);
1391 } else {
1392 warn!(
1393 target: LOG_MODULE_WALLET,
1394 %height,
1395 %block_hash,
1396 %prev_block_height,
1397 prev_blockhash = %block.header.prev_blockhash,
1398 "Missing previous block hash. This should only happen on the first processed block height."
1399 );
1400 }
1401 }
1402
1403 if self.consensus_module_consensus_version(dbtx).await
1404 >= ModuleConsensusVersion::new(2, 2)
1405 {
1406 for transaction in &block.txdata {
1407 for tx_in in &transaction.input {
1412 dbtx.remove_entry(&UnspentTxOutKey(tx_in.previous_output))
1413 .await;
1414 }
1415
1416 for (vout, tx_out) in transaction.output.iter().enumerate() {
1417 let should_track_utxo = if self.cfg.consensus.peer_peg_in_keys.len() > 1 {
1418 tx_out.script_pubkey.is_p2wsh()
1419 } else {
1420 tx_out.script_pubkey.is_p2wpkh()
1421 };
1422
1423 if should_track_utxo {
1424 let outpoint = bitcoin::OutPoint {
1425 txid: transaction.compute_txid(),
1426 vout: vout as u32,
1427 };
1428
1429 dbtx.insert_new_entry(&UnspentTxOutKey(outpoint), tx_out)
1430 .await;
1431 }
1432 }
1433 }
1434 }
1435
1436 let pending_transactions = dbtx
1437 .find_by_prefix(&PendingTransactionPrefixKey)
1438 .await
1439 .map(|(key, transaction)| (key.0, transaction))
1440 .collect::<HashMap<Txid, PendingTransaction>>()
1441 .await;
1442 let pending_transactions_len = pending_transactions.len();
1443
1444 debug!(
1445 target: LOG_MODULE_WALLET,
1446 ?height,
1447 ?pending_transactions_len,
1448 "Recognizing change UTXOs"
1449 );
1450 for (txid, tx) in &pending_transactions {
1451 let is_tx_in_block = block.txdata.iter().any(|tx| tx.compute_txid() == *txid);
1452
1453 if is_tx_in_block {
1454 debug!(
1455 target: LOG_MODULE_WALLET,
1456 ?txid, ?height, ?block_hash, "Recognizing change UTXO"
1457 );
1458 self.recognize_change_utxo(dbtx, tx).await;
1459 } else {
1460 debug!(
1461 target: LOG_MODULE_WALLET,
1462 ?txid,
1463 ?height,
1464 ?block_hash,
1465 "Pending transaction not yet confirmed in this block"
1466 );
1467 }
1468 }
1469
1470 dbtx.insert_new_entry(&BlockHashKey(block_hash), &()).await;
1471 dbtx.insert_new_entry(
1472 &BlockHashByHeightKey(height),
1473 &BlockHashByHeightValue(block_hash),
1474 )
1475 .await;
1476
1477 info!(
1478 target: LOG_MODULE_WALLET,
1479 height,
1480 ?block_hash,
1481 "Successfully processed block of height {height}",
1482 );
1483 }
1484 }
1485
1486 async fn recognize_change_utxo(
1489 &self,
1490 dbtx: &mut DatabaseTransaction<'_>,
1491 pending_tx: &PendingTransaction,
1492 ) {
1493 self.remove_rbf_transactions(dbtx, pending_tx).await;
1494
1495 let script_pk = self
1496 .cfg
1497 .consensus
1498 .peg_in_descriptor
1499 .tweak(&pending_tx.tweak, &self.secp)
1500 .script_pubkey();
1501 for (idx, output) in pending_tx.tx.output.iter().enumerate() {
1502 if output.script_pubkey == script_pk {
1503 dbtx.insert_entry(
1504 &UTXOKey(bitcoin::OutPoint {
1505 txid: pending_tx.tx.compute_txid(),
1506 vout: idx as u32,
1507 }),
1508 &SpendableUTXO {
1509 tweak: pending_tx.tweak,
1510 amount: output.value,
1511 },
1512 )
1513 .await;
1514 }
1515 }
1516 }
1517
1518 async fn remove_rbf_transactions(
1520 &self,
1521 dbtx: &mut DatabaseTransaction<'_>,
1522 pending_tx: &PendingTransaction,
1523 ) {
1524 let mut all_transactions: BTreeMap<Txid, PendingTransaction> = dbtx
1525 .find_by_prefix(&PendingTransactionPrefixKey)
1526 .await
1527 .map(|(key, val)| (key.0, val))
1528 .collect::<BTreeMap<Txid, PendingTransaction>>()
1529 .await;
1530
1531 let mut pending_to_remove = vec![pending_tx.clone()];
1533 while let Some(removed) = pending_to_remove.pop() {
1534 all_transactions.remove(&removed.tx.compute_txid());
1535 dbtx.remove_entry(&PendingTransactionKey(removed.tx.compute_txid()))
1536 .await;
1537
1538 if let Some(rbf) = &removed.rbf
1540 && let Some(tx) = all_transactions.get(&rbf.txid)
1541 {
1542 pending_to_remove.push(tx.clone());
1543 }
1544
1545 for tx in all_transactions.values() {
1547 if let Some(rbf) = &tx.rbf
1548 && rbf.txid == removed.tx.compute_txid()
1549 {
1550 pending_to_remove.push(tx.clone());
1551 }
1552 }
1553 }
1554 }
1555
1556 async fn block_is_known(
1557 &self,
1558 dbtx: &mut DatabaseTransaction<'_>,
1559 block_hash: BlockHash,
1560 ) -> bool {
1561 dbtx.get_value(&BlockHashKey(block_hash)).await.is_some()
1562 }
1563
1564 async fn create_peg_out_tx(
1565 &self,
1566 dbtx: &mut DatabaseTransaction<'_>,
1567 output: &WalletOutputV0,
1568 change_tweak: &[u8; 33],
1569 ) -> Result<UnsignedTransaction, WalletOutputError> {
1570 match output {
1571 WalletOutputV0::PegOut(peg_out) => self.offline_wallet().create_tx(
1572 peg_out.amount,
1573 peg_out.recipient.clone().assume_checked().script_pubkey(),
1577 vec![],
1578 self.available_utxos(dbtx).await,
1579 peg_out.fees.fee_rate,
1580 change_tweak,
1581 None,
1582 ),
1583 WalletOutputV0::Rbf(rbf) => {
1584 let tx = dbtx
1585 .get_value(&PendingTransactionKey(rbf.txid))
1586 .await
1587 .ok_or(WalletOutputError::RbfTransactionIdNotFound)?;
1588
1589 self.offline_wallet().create_tx(
1590 tx.peg_out_amount,
1591 tx.destination,
1592 tx.selected_utxos,
1593 self.available_utxos(dbtx).await,
1594 tx.fees.fee_rate,
1595 change_tweak,
1596 Some(rbf.clone()),
1597 )
1598 }
1599 }
1600 }
1601
1602 async fn available_utxos(
1603 &self,
1604 dbtx: &mut DatabaseTransaction<'_>,
1605 ) -> Vec<(UTXOKey, SpendableUTXO)> {
1606 dbtx.find_by_prefix(&UTXOPrefixKey)
1607 .await
1608 .collect::<Vec<(UTXOKey, SpendableUTXO)>>()
1609 .await
1610 }
1611
1612 pub async fn get_wallet_value(&self, dbtx: &mut DatabaseTransaction<'_>) -> bitcoin::Amount {
1613 let sat_sum = self
1614 .available_utxos(dbtx)
1615 .await
1616 .into_iter()
1617 .map(|(_, utxo)| utxo.amount.to_sat())
1618 .sum();
1619 bitcoin::Amount::from_sat(sat_sum)
1620 }
1621
1622 async fn get_wallet_summary(&self, dbtx: &mut DatabaseTransaction<'_>) -> WalletSummary {
1623 fn partition_peg_out_and_change(
1624 transactions: Vec<Transaction>,
1625 ) -> (Vec<TxOutputSummary>, Vec<TxOutputSummary>) {
1626 let mut peg_out_txos: Vec<TxOutputSummary> = Vec::new();
1627 let mut change_utxos: Vec<TxOutputSummary> = Vec::new();
1628
1629 for tx in transactions {
1630 let txid = tx.compute_txid();
1631
1632 let peg_out_output = tx
1635 .output
1636 .first()
1637 .expect("tx must contain withdrawal output");
1638
1639 let change_output = tx.output.last().expect("tx must contain change output");
1640
1641 peg_out_txos.push(TxOutputSummary {
1642 outpoint: bitcoin::OutPoint { txid, vout: 0 },
1643 amount: peg_out_output.value,
1644 });
1645
1646 change_utxos.push(TxOutputSummary {
1647 outpoint: bitcoin::OutPoint { txid, vout: 1 },
1648 amount: change_output.value,
1649 });
1650 }
1651
1652 (peg_out_txos, change_utxos)
1653 }
1654
1655 let spendable_utxos = self
1656 .available_utxos(dbtx)
1657 .await
1658 .iter()
1659 .map(|(utxo_key, spendable_utxo)| TxOutputSummary {
1660 outpoint: utxo_key.0,
1661 amount: spendable_utxo.amount,
1662 })
1663 .collect::<Vec<_>>();
1664
1665 let unsigned_transactions = dbtx
1667 .find_by_prefix(&UnsignedTransactionPrefixKey)
1668 .await
1669 .map(|(_tx_key, tx)| tx.psbt.unsigned_tx)
1670 .collect::<Vec<_>>()
1671 .await;
1672
1673 let unconfirmed_transactions = dbtx
1675 .find_by_prefix(&PendingTransactionPrefixKey)
1676 .await
1677 .map(|(_tx_key, tx)| tx.tx)
1678 .collect::<Vec<_>>()
1679 .await;
1680
1681 let (unsigned_peg_out_txos, unsigned_change_utxos) =
1682 partition_peg_out_and_change(unsigned_transactions);
1683
1684 let (unconfirmed_peg_out_txos, unconfirmed_change_utxos) =
1685 partition_peg_out_and_change(unconfirmed_transactions);
1686
1687 WalletSummary {
1688 spendable_utxos,
1689 unsigned_peg_out_txos,
1690 unsigned_change_utxos,
1691 unconfirmed_peg_out_txos,
1692 unconfirmed_change_utxos,
1693 }
1694 }
1695
1696 async fn is_utxo_confirmed(
1697 &self,
1698 dbtx: &mut DatabaseTransaction<'_>,
1699 outpoint: bitcoin::OutPoint,
1700 ) -> bool {
1701 dbtx.get_value(&UnspentTxOutKey(outpoint)).await.is_some()
1702 }
1703
1704 fn offline_wallet(&'_ self) -> StatelessWallet<'_> {
1705 StatelessWallet {
1706 descriptor: &self.cfg.consensus.peg_in_descriptor,
1707 secret_key: &self.cfg.private.peg_in_key,
1708 secp: &self.secp,
1709 }
1710 }
1711
1712 fn spawn_broadcast_pending_task(
1713 task_group: &TaskGroup,
1714 server_bitcoin_rpc_monitor: &ServerBitcoinRpcMonitor,
1715 db: &Database,
1716 broadcast_pending_notify: Arc<Notify>,
1717 ) {
1718 task_group.spawn_cancellable("broadcast pending", {
1719 let btc_rpc = server_bitcoin_rpc_monitor.clone();
1720 let db = db.clone();
1721 run_broadcast_pending_tx(db, btc_rpc, broadcast_pending_notify)
1722 });
1723 }
1724
1725 pub fn network_ui(&self) -> Network {
1727 self.cfg.consensus.network.0
1728 }
1729
1730 pub async fn consensus_block_count_ui(&self) -> u32 {
1732 self.consensus_block_count(&mut self.db.begin_transaction_nc().await)
1733 .await
1734 }
1735
1736 pub async fn consensus_feerate_ui(&self) -> Feerate {
1738 self.consensus_fee_rate(&mut self.db.begin_transaction_nc().await)
1739 .await
1740 }
1741
1742 pub async fn get_wallet_summary_ui(&self) -> WalletSummary {
1744 self.get_wallet_summary(&mut self.db.begin_transaction_nc().await)
1745 .await
1746 }
1747
1748 async fn graceful_shutdown(&self) {
1751 if let Err(e) = self
1752 .task_group
1753 .clone()
1754 .shutdown_join_all(Some(Duration::from_secs(60)))
1755 .await
1756 {
1757 panic!("Error while shutting down fedimintd task group: {e}");
1758 }
1759 }
1760
1761 async fn wait_for_finality_confs_or_shutdown(&self, consensus_block_count: u32) {
1767 let backoff = if is_running_in_test_env() {
1768 backoff_util::custom_backoff(
1770 Duration::from_millis(100),
1771 Duration::from_millis(100),
1772 Some(10 * 60),
1773 )
1774 } else {
1775 backoff_util::fibonacci_max_one_hour()
1777 };
1778
1779 let wait_for_finality_confs = || async {
1780 let our_chain_tip_block_count = self.get_block_count()?;
1781 let consensus_chain_tip_block_count =
1782 consensus_block_count + self.cfg.consensus.finality_delay;
1783
1784 if consensus_chain_tip_block_count <= our_chain_tip_block_count {
1785 Ok(())
1786 } else {
1787 Err(anyhow::anyhow!("not enough confirmations"))
1788 }
1789 };
1790
1791 if retry("wait_for_finality_confs", backoff, wait_for_finality_confs)
1792 .await
1793 .is_err()
1794 {
1795 self.graceful_shutdown().await;
1796 }
1797 }
1798
1799 fn spawn_peer_supported_consensus_version_task(
1800 api_client: DynModuleApi,
1801 task_group: &TaskGroup,
1802 our_peer_id: PeerId,
1803 ) -> watch::Receiver<Option<ModuleConsensusVersion>> {
1804 let (sender, receiver) = watch::channel(None);
1805 task_group.spawn_cancellable("fetch-peer-consensus-versions", async move {
1806 loop {
1807 let request_futures = api_client.all_peers().iter().filter_map(|&peer| {
1808 if peer == our_peer_id {
1809 return None;
1810 }
1811
1812 let api_client_inner = api_client.clone();
1813 Some(async move {
1814 api_client_inner
1815 .request_single_peer::<ModuleConsensusVersion>(
1816 SUPPORTED_MODULE_CONSENSUS_VERSION_ENDPOINT.to_owned(),
1817 ApiRequestErased::default(),
1818 peer,
1819 )
1820 .await
1821 .inspect(|res| debug!(
1822 target: LOG_MODULE_WALLET,
1823 %peer,
1824 %our_peer_id,
1825 ?res,
1826 "Fetched supported module consensus version from peer"
1827 ))
1828 .inspect_err(|err| warn!(
1829 target: LOG_MODULE_WALLET,
1830 %peer,
1831 err=%err.fmt_compact(),
1832 "Failed to fetch consensus version from peer"
1833 ))
1834 .ok()
1835 })
1836 });
1837
1838 let peer_consensus_versions = join_all(request_futures)
1839 .await
1840 .into_iter()
1841 .flatten()
1842 .collect::<Vec<_>>();
1843
1844 let sorted_consensus_versions = peer_consensus_versions
1845 .into_iter()
1846 .chain(std::iter::once(MODULE_CONSENSUS_VERSION))
1847 .sorted()
1848 .collect::<Vec<_>>();
1849 let all_peers_supported_version =
1850 if sorted_consensus_versions.len() == api_client.all_peers().len() {
1851 let min_supported_version = *sorted_consensus_versions
1852 .first()
1853 .expect("at least one element");
1854
1855 debug!(
1856 target: LOG_MODULE_WALLET,
1857 ?sorted_consensus_versions,
1858 "Fetched supported consensus versions from peers"
1859 );
1860
1861 Some(min_supported_version)
1862 } else {
1863 assert!(
1864 sorted_consensus_versions.len() <= api_client.all_peers().len(),
1865 "Too many peer responses",
1866 );
1867 trace!(
1868 target: LOG_MODULE_WALLET,
1869 ?sorted_consensus_versions,
1870 "Not all peers have reported their consensus version yet"
1871 );
1872 None
1873 };
1874
1875 #[allow(clippy::disallowed_methods)]
1876 if sender.send(all_peers_supported_version).is_err() {
1877 warn!(target: LOG_MODULE_WALLET, "Failed to send consensus version to watch channel, stopping task");
1878 break;
1879 }
1880
1881 if is_running_in_test_env() {
1882 sleep(Duration::from_secs(5)).await;
1884 } else {
1885 sleep(Duration::from_secs(600)).await;
1886 }
1887 }
1888 });
1889 receiver
1890 }
1891}
1892
1893#[instrument(target = LOG_MODULE_WALLET, level = "debug", skip_all)]
1894pub async fn run_broadcast_pending_tx(
1895 db: Database,
1896 rpc: ServerBitcoinRpcMonitor,
1897 broadcast: Arc<Notify>,
1898) {
1899 loop {
1900 let _ = tokio::time::timeout(Duration::from_secs(60), broadcast.notified()).await;
1902 broadcast_pending_tx(db.begin_transaction_nc().await, &rpc).await;
1903 }
1904}
1905
1906pub async fn broadcast_pending_tx(
1907 mut dbtx: DatabaseTransaction<'_>,
1908 rpc: &ServerBitcoinRpcMonitor,
1909) {
1910 let pending_tx: Vec<PendingTransaction> = dbtx
1911 .find_by_prefix(&PendingTransactionPrefixKey)
1912 .await
1913 .map(|(_, val)| val)
1914 .collect::<Vec<_>>()
1915 .await;
1916 let rbf_txids: BTreeSet<Txid> = pending_tx
1917 .iter()
1918 .filter_map(|tx| tx.rbf.clone().map(|rbf| rbf.txid))
1919 .collect();
1920 if !pending_tx.is_empty() {
1921 debug!(
1922 target: LOG_MODULE_WALLET,
1923 "Broadcasting pending transactions (total={}, rbf={})",
1924 pending_tx.len(),
1925 rbf_txids.len()
1926 );
1927 }
1928
1929 for PendingTransaction { tx, .. } in pending_tx {
1930 if !rbf_txids.contains(&tx.compute_txid()) {
1931 debug!(
1932 target: LOG_MODULE_WALLET,
1933 tx = %tx.compute_txid(),
1934 weight = tx.weight().to_wu(),
1935 output = ?tx.output,
1936 "Broadcasting peg-out",
1937 );
1938 trace!(transaction = ?tx);
1939 rpc.submit_transaction(tx).await;
1940 }
1941 }
1942}
1943
1944struct StatelessWallet<'a> {
1945 descriptor: &'a Descriptor<CompressedPublicKey>,
1946 secret_key: &'a secp256k1::SecretKey,
1947 secp: &'a secp256k1::Secp256k1<secp256k1::All>,
1948}
1949
1950impl StatelessWallet<'_> {
1951 fn validate_tx(
1954 tx: &UnsignedTransaction,
1955 output: &WalletOutputV0,
1956 consensus_fee_rate: Feerate,
1957 network: Network,
1958 ) -> Result<(), WalletOutputError> {
1959 if let WalletOutputV0::PegOut(peg_out) = output
1960 && !peg_out.recipient.is_valid_for_network(network)
1961 {
1962 return Err(WalletOutputError::WrongNetwork(
1963 NetworkLegacyEncodingWrapper(network),
1964 NetworkLegacyEncodingWrapper(get_network_for_address(&peg_out.recipient)),
1965 ));
1966 }
1967
1968 if tx.peg_out_amount < tx.destination.minimal_non_dust() {
1970 return Err(WalletOutputError::PegOutUnderDustLimit);
1971 }
1972
1973 if tx.fees.fee_rate < consensus_fee_rate {
1975 return Err(WalletOutputError::PegOutFeeBelowConsensus(
1976 tx.fees.fee_rate,
1977 consensus_fee_rate,
1978 ));
1979 }
1980
1981 let fees = match output {
1984 WalletOutputV0::PegOut(pegout) => pegout.fees,
1985 WalletOutputV0::Rbf(rbf) => rbf.fees,
1986 };
1987 if fees.fee_rate.sats_per_kvb < u64::from(DEFAULT_MIN_RELAY_TX_FEE) {
1988 return Err(WalletOutputError::BelowMinRelayFee);
1989 }
1990
1991 if fees.total_weight != tx.fees.total_weight {
1993 return Err(WalletOutputError::TxWeightIncorrect(
1994 fees.total_weight,
1995 tx.fees.total_weight,
1996 ));
1997 }
1998
1999 Ok(())
2000 }
2001
2002 #[allow(clippy::too_many_arguments)]
2012 fn create_tx(
2013 &self,
2014 peg_out_amount: bitcoin::Amount,
2015 destination: ScriptBuf,
2016 mut included_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2017 mut remaining_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2018 mut fee_rate: Feerate,
2019 change_tweak: &[u8; 33],
2020 rbf: Option<Rbf>,
2021 ) -> Result<UnsignedTransaction, WalletOutputError> {
2022 if let Some(rbf) = &rbf {
2024 fee_rate.sats_per_kvb += rbf.fees.fee_rate.sats_per_kvb;
2025 }
2026
2027 let change_script = self.derive_script(change_tweak);
2035 let out_weight = (destination.len() * 4 + 1 + 32
2036 + 1 + change_script.len() * 4 + 32) as u64; let mut total_weight = 16 + 12 + 12 + out_weight + 16; #[allow(deprecated)]
2047 let max_input_weight = (self
2048 .descriptor
2049 .max_satisfaction_weight()
2050 .expect("is satisfyable") +
2051 128 + 16 + 16) as u64; included_utxos.sort_by_key(|(_, utxo)| utxo.amount);
2057 remaining_utxos.sort_by_key(|(_, utxo)| utxo.amount);
2058 included_utxos.extend(remaining_utxos);
2059
2060 let mut total_selected_value = bitcoin::Amount::from_sat(0);
2062 let mut selected_utxos: Vec<(UTXOKey, SpendableUTXO)> = vec![];
2063 let mut fees = fee_rate.calculate_fee(total_weight);
2064
2065 while total_selected_value < peg_out_amount + change_script.minimal_non_dust() + fees {
2066 match included_utxos.pop() {
2067 Some((utxo_key, utxo)) => {
2068 total_selected_value += utxo.amount;
2069 total_weight += max_input_weight;
2070 fees = fee_rate.calculate_fee(total_weight);
2071 selected_utxos.push((utxo_key, utxo));
2072 }
2073 _ => return Err(WalletOutputError::NotEnoughSpendableUTXO), }
2075 }
2076
2077 let change = total_selected_value - fees - peg_out_amount;
2080 let output: Vec<TxOut> = vec![
2081 TxOut {
2082 value: peg_out_amount,
2083 script_pubkey: destination.clone(),
2084 },
2085 TxOut {
2086 value: change,
2087 script_pubkey: change_script,
2088 },
2089 ];
2090 let mut change_out = bitcoin::psbt::Output::default();
2091 change_out
2092 .proprietary
2093 .insert(proprietary_tweak_key(), change_tweak.to_vec());
2094
2095 info!(
2096 target: LOG_MODULE_WALLET,
2097 inputs = selected_utxos.len(),
2098 input_sats = total_selected_value.to_sat(),
2099 peg_out_sats = peg_out_amount.to_sat(),
2100 ?total_weight,
2101 fees_sats = fees.to_sat(),
2102 fee_rate = fee_rate.sats_per_kvb,
2103 change_sats = change.to_sat(),
2104 "Creating peg-out tx",
2105 );
2106
2107 let transaction = Transaction {
2108 version: bitcoin::transaction::Version(2),
2109 lock_time: LockTime::ZERO,
2110 input: selected_utxos
2111 .iter()
2112 .map(|(utxo_key, _utxo)| TxIn {
2113 previous_output: utxo_key.0,
2114 script_sig: Default::default(),
2115 sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
2116 witness: bitcoin::Witness::new(),
2117 })
2118 .collect(),
2119 output,
2120 };
2121 info!(
2122 target: LOG_MODULE_WALLET,
2123 txid = %transaction.compute_txid(), "Creating peg-out tx"
2124 );
2125
2126 let psbt = Psbt {
2129 unsigned_tx: transaction,
2130 version: 0,
2131 xpub: Default::default(),
2132 proprietary: Default::default(),
2133 unknown: Default::default(),
2134 inputs: selected_utxos
2135 .iter()
2136 .map(|(_utxo_key, utxo)| {
2137 let script_pubkey = self
2138 .descriptor
2139 .tweak(&utxo.tweak, self.secp)
2140 .script_pubkey();
2141 Input {
2142 non_witness_utxo: None,
2143 witness_utxo: Some(TxOut {
2144 value: utxo.amount,
2145 script_pubkey,
2146 }),
2147 partial_sigs: Default::default(),
2148 sighash_type: None,
2149 redeem_script: None,
2150 witness_script: Some(
2151 self.descriptor
2152 .tweak(&utxo.tweak, self.secp)
2153 .script_code()
2154 .expect("Failed to tweak descriptor"),
2155 ),
2156 bip32_derivation: Default::default(),
2157 final_script_sig: None,
2158 final_script_witness: None,
2159 ripemd160_preimages: Default::default(),
2160 sha256_preimages: Default::default(),
2161 hash160_preimages: Default::default(),
2162 hash256_preimages: Default::default(),
2163 proprietary: vec![(proprietary_tweak_key(), utxo.tweak.to_vec())]
2164 .into_iter()
2165 .collect(),
2166 tap_key_sig: Default::default(),
2167 tap_script_sigs: Default::default(),
2168 tap_scripts: Default::default(),
2169 tap_key_origins: Default::default(),
2170 tap_internal_key: Default::default(),
2171 tap_merkle_root: Default::default(),
2172 unknown: Default::default(),
2173 }
2174 })
2175 .collect(),
2176 outputs: vec![Default::default(), change_out],
2177 };
2178
2179 Ok(UnsignedTransaction {
2180 psbt,
2181 signatures: vec![],
2182 change,
2183 fees: PegOutFees {
2184 fee_rate,
2185 total_weight,
2186 },
2187 destination,
2188 selected_utxos,
2189 peg_out_amount,
2190 rbf,
2191 })
2192 }
2193
2194 fn sign_psbt(&self, psbt: &mut Psbt) {
2195 let mut tx_hasher = SighashCache::new(&psbt.unsigned_tx);
2196
2197 for (idx, (psbt_input, _tx_input)) in psbt
2198 .inputs
2199 .iter_mut()
2200 .zip(psbt.unsigned_tx.input.iter())
2201 .enumerate()
2202 {
2203 let tweaked_secret = {
2204 let tweak = psbt_input
2205 .proprietary
2206 .get(&proprietary_tweak_key())
2207 .expect("Malformed PSBT: expected tweak");
2208
2209 self.secret_key.tweak(tweak, self.secp)
2210 };
2211
2212 let tx_hash = tx_hasher
2213 .p2wsh_signature_hash(
2214 idx,
2215 psbt_input
2216 .witness_script
2217 .as_ref()
2218 .expect("Missing witness script"),
2219 psbt_input
2220 .witness_utxo
2221 .as_ref()
2222 .expect("Missing UTXO")
2223 .value,
2224 EcdsaSighashType::All,
2225 )
2226 .expect("Failed to create segwit sighash");
2227
2228 let signature = self.secp.sign_ecdsa(
2229 &Message::from_digest_slice(&tx_hash[..]).unwrap(),
2230 &tweaked_secret,
2231 );
2232
2233 psbt_input.partial_sigs.insert(
2234 bitcoin::PublicKey {
2235 compressed: true,
2236 inner: secp256k1::PublicKey::from_secret_key(self.secp, &tweaked_secret),
2237 },
2238 EcdsaSig::sighash_all(signature),
2239 );
2240 }
2241 }
2242
2243 fn derive_script(&self, tweak: &[u8]) -> ScriptBuf {
2244 struct CompressedPublicKeyTranslator<'t, 's, Ctx: Verification> {
2245 tweak: &'t [u8],
2246 secp: &'s Secp256k1<Ctx>,
2247 }
2248
2249 impl<Ctx: Verification>
2250 miniscript::Translator<CompressedPublicKey, CompressedPublicKey, Infallible>
2251 for CompressedPublicKeyTranslator<'_, '_, Ctx>
2252 {
2253 fn pk(&mut self, pk: &CompressedPublicKey) -> Result<CompressedPublicKey, Infallible> {
2254 let hashed_tweak = {
2255 let mut hasher = HmacEngine::<sha256::Hash>::new(&pk.key.serialize()[..]);
2256 hasher.input(self.tweak);
2257 Hmac::from_engine(hasher).to_byte_array()
2258 };
2259
2260 Ok(CompressedPublicKey {
2261 key: pk
2262 .key
2263 .add_exp_tweak(
2264 self.secp,
2265 &Scalar::from_be_bytes(hashed_tweak).expect("can't fail"),
2266 )
2267 .expect("tweaking failed"),
2268 })
2269 }
2270 translate_hash_fail!(CompressedPublicKey, CompressedPublicKey, Infallible);
2271 }
2272
2273 let descriptor = self
2274 .descriptor
2275 .translate_pk(&mut CompressedPublicKeyTranslator {
2276 tweak,
2277 secp: self.secp,
2278 })
2279 .expect("can't fail");
2280
2281 descriptor.script_pubkey()
2282 }
2283}
2284
2285pub fn nonce_from_idx(nonce_idx: u64) -> [u8; 33] {
2286 let mut nonce: [u8; 33] = [0; 33];
2287 nonce[0] = 0x02;
2289 nonce[1..].copy_from_slice(&nonce_idx.consensus_hash::<bitcoin::hashes::sha256::Hash>()[..]);
2290
2291 nonce
2292}
2293
2294#[derive(Clone, Debug, Encodable, Decodable)]
2296pub struct PendingTransaction {
2297 pub tx: bitcoin::Transaction,
2298 pub tweak: [u8; 33],
2299 pub change: bitcoin::Amount,
2300 pub destination: ScriptBuf,
2301 pub fees: PegOutFees,
2302 pub selected_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2303 pub peg_out_amount: bitcoin::Amount,
2304 pub rbf: Option<Rbf>,
2305}
2306
2307impl Serialize for PendingTransaction {
2308 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2309 where
2310 S: serde::Serializer,
2311 {
2312 if serializer.is_human_readable() {
2313 serializer.serialize_str(&self.consensus_encode_to_hex())
2314 } else {
2315 serializer.serialize_bytes(&self.consensus_encode_to_vec())
2316 }
2317 }
2318}
2319
2320#[derive(Clone, Debug, Eq, PartialEq, Encodable, Decodable)]
2323pub struct UnsignedTransaction {
2324 pub psbt: Psbt,
2325 pub signatures: Vec<(PeerId, PegOutSignatureItem)>,
2326 pub change: bitcoin::Amount,
2327 pub fees: PegOutFees,
2328 pub destination: ScriptBuf,
2329 pub selected_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2330 pub peg_out_amount: bitcoin::Amount,
2331 pub rbf: Option<Rbf>,
2332}
2333
2334impl Serialize for UnsignedTransaction {
2335 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2336 where
2337 S: serde::Serializer,
2338 {
2339 if serializer.is_human_readable() {
2340 serializer.serialize_str(&self.consensus_encode_to_hex())
2341 } else {
2342 serializer.serialize_bytes(&self.consensus_encode_to_vec())
2343 }
2344 }
2345}
2346
2347#[cfg(test)]
2348mod tests {
2349
2350 use std::str::FromStr;
2351
2352 use bitcoin::Network::{Bitcoin, Testnet};
2353 use bitcoin::hashes::Hash;
2354 use bitcoin::{Address, Amount, OutPoint, Txid, secp256k1};
2355 use fedimint_core::Feerate;
2356 use fedimint_core::encoding::btc::NetworkLegacyEncodingWrapper;
2357 use fedimint_wallet_common::{PegOut, PegOutFees, Rbf, WalletOutputV0};
2358 use miniscript::descriptor::Wsh;
2359
2360 use crate::common::PegInDescriptor;
2361 use crate::{
2362 CompressedPublicKey, OsRng, SpendableUTXO, StatelessWallet, UTXOKey, WalletOutputError,
2363 };
2364
2365 #[test]
2366 fn create_tx_should_validate_amounts() {
2367 let secp = secp256k1::Secp256k1::new();
2368
2369 let descriptor = PegInDescriptor::Wsh(
2370 Wsh::new_sortedmulti(
2371 3,
2372 (0..4)
2373 .map(|_| secp.generate_keypair(&mut OsRng))
2374 .map(|(_, key)| CompressedPublicKey { key })
2375 .collect(),
2376 )
2377 .unwrap(),
2378 );
2379
2380 let (secret_key, _) = secp.generate_keypair(&mut OsRng);
2381
2382 let wallet = StatelessWallet {
2383 descriptor: &descriptor,
2384 secret_key: &secret_key,
2385 secp: &secp,
2386 };
2387
2388 let spendable = SpendableUTXO {
2389 tweak: [0; 33],
2390 amount: bitcoin::Amount::from_sat(3000),
2391 };
2392
2393 let recipient = Address::from_str("32iVBEu4dxkUQk9dJbZUiBiQdmypcEyJRf").unwrap();
2394
2395 let fee = Feerate { sats_per_kvb: 1000 };
2396 let weight = 875;
2397
2398 let tx = wallet.create_tx(
2403 Amount::from_sat(2452),
2404 recipient.clone().assume_checked().script_pubkey(),
2405 vec![],
2406 vec![(UTXOKey(OutPoint::null()), spendable.clone())],
2407 fee,
2408 &[0; 33],
2409 None,
2410 );
2411 assert_eq!(tx, Err(WalletOutputError::NotEnoughSpendableUTXO));
2412
2413 let mut tx = wallet
2415 .create_tx(
2416 Amount::from_sat(1000),
2417 recipient.clone().assume_checked().script_pubkey(),
2418 vec![],
2419 vec![(UTXOKey(OutPoint::null()), spendable)],
2420 fee,
2421 &[0; 33],
2422 None,
2423 )
2424 .expect("is ok");
2425
2426 let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, 0), fee, Bitcoin);
2428 assert_eq!(res, Err(WalletOutputError::TxWeightIncorrect(0, weight)));
2429
2430 let res = StatelessWallet::validate_tx(&tx, &rbf(0, weight), fee, Bitcoin);
2432 assert_eq!(res, Err(WalletOutputError::BelowMinRelayFee));
2433
2434 let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2436 assert_eq!(res, Ok(()));
2437
2438 tx.fees = PegOutFees::new(0, weight);
2440 let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2441 assert_eq!(
2442 res,
2443 Err(WalletOutputError::PegOutFeeBelowConsensus(
2444 Feerate { sats_per_kvb: 0 },
2445 fee
2446 ))
2447 );
2448
2449 tx.peg_out_amount = bitcoin::Amount::ZERO;
2451 let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2452 assert_eq!(res, Err(WalletOutputError::PegOutUnderDustLimit));
2453
2454 let output = WalletOutputV0::PegOut(PegOut {
2456 recipient,
2457 amount: bitcoin::Amount::from_sat(1000),
2458 fees: PegOutFees::new(100, weight),
2459 });
2460 let res = StatelessWallet::validate_tx(&tx, &output, fee, Testnet);
2461 assert_eq!(
2462 res,
2463 Err(WalletOutputError::WrongNetwork(
2464 NetworkLegacyEncodingWrapper(Testnet),
2465 NetworkLegacyEncodingWrapper(Bitcoin)
2466 ))
2467 );
2468 }
2469
2470 fn rbf(sats_per_kvb: u64, total_weight: u64) -> WalletOutputV0 {
2471 WalletOutputV0::Rbf(Rbf {
2472 fees: PegOutFees::new(sats_per_kvb, total_weight),
2473 txid: Txid::all_zeros(),
2474 })
2475 }
2476}