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