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