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