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