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