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, PeerHandle,
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::config::distributedgen::PeerHandleOps;
69use fedimint_server::core::migration::ServerModuleDbMigrationFn;
70use fedimint_server::core::{ServerModule, ServerModuleInit, ServerModuleInitArgs};
71use fedimint_server::net::api::check_auth;
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: &PeerHandle,
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 secp: Secp256k1<All>,
1007 btc_rpc: DynBitcoindRpc,
1008 our_peer_id: PeerId,
1009 block_count_rx: watch::Receiver<Option<u64>>,
1011 fee_rate_rx: watch::Receiver<Option<Feerate>>,
1013
1014 broadcast_pending: Arc<Notify>,
1016
1017 task_group: TaskGroup,
1018 peer_supported_consensus_version: watch::Receiver<Option<ModuleConsensusVersion>>,
1022}
1023
1024impl Wallet {
1025 pub async fn new(
1026 cfg: WalletConfig,
1027 db: &Database,
1028 task_group: &TaskGroup,
1029 our_peer_id: PeerId,
1030 module_api: DynModuleApi,
1031 shared_bitcoin: &ServerModuleSharedBitcoin,
1032 ) -> anyhow::Result<Wallet> {
1033 let btc_rpc = create_bitcoind(&cfg.local.bitcoin_rpc)?;
1034 Ok(Self::new_with_bitcoind(
1035 cfg,
1036 db,
1037 btc_rpc,
1038 task_group,
1039 our_peer_id,
1040 module_api,
1041 shared_bitcoin,
1042 )
1043 .await?)
1044 }
1045
1046 pub async fn new_with_bitcoind(
1047 cfg: WalletConfig,
1048 db: &Database,
1049 btc_rpc: DynBitcoindRpc,
1050 task_group: &TaskGroup,
1051 our_peer_id: PeerId,
1052 module_api: DynModuleApi,
1053 shared_bitcoin: &ServerModuleSharedBitcoin,
1054 ) -> Result<Wallet, WalletCreationError> {
1055 let fee_rate_rx = shared_bitcoin
1056 .feerate_receiver(cfg.consensus.network.0, btc_rpc.clone())
1057 .await
1058 .map_err(|e| {
1059 WalletCreationError::FeerateSourceError(e.fmt_compact_anyhow().to_string())
1060 })?;
1061 let block_count_rx = shared_bitcoin
1062 .block_count_receiver(cfg.consensus.network.0, btc_rpc.clone())
1063 .await;
1064 let broadcast_pending = Arc::new(Notify::new());
1065 Self::spawn_broadcast_pending_task(task_group, &btc_rpc, db, broadcast_pending.clone());
1066
1067 let peer_supported_consensus_version =
1068 Self::spawn_peer_supported_consensus_version_task(module_api, task_group, our_peer_id);
1069
1070 let bitcoind_net = NetworkLegacyEncodingWrapper(
1071 retry("verify network", backoff_util::aggressive_backoff(), || {
1072 btc_rpc.get_network()
1073 })
1074 .await
1075 .map_err(|e| WalletCreationError::RpcError(e.to_string()))?,
1076 );
1077 if bitcoind_net != cfg.consensus.network {
1078 return Err(WalletCreationError::WrongNetwork(
1079 cfg.consensus.network,
1080 bitcoind_net,
1081 ));
1082 }
1083
1084 let wallet = Wallet {
1085 cfg,
1086 secp: Default::default(),
1087 btc_rpc,
1088 our_peer_id,
1089 block_count_rx,
1090 fee_rate_rx,
1091 task_group: task_group.clone(),
1092 peer_supported_consensus_version,
1093 broadcast_pending,
1094 };
1095
1096 Ok(wallet)
1097 }
1098
1099 fn sign_peg_out_psbt(
1101 &self,
1102 psbt: &mut Psbt,
1103 peer: PeerId,
1104 signature: &PegOutSignatureItem,
1105 ) -> Result<(), ProcessPegOutSigError> {
1106 let peer_key = self
1107 .cfg
1108 .consensus
1109 .peer_peg_in_keys
1110 .get(&peer)
1111 .expect("always called with valid peer id");
1112
1113 if psbt.inputs.len() != signature.signature.len() {
1114 return Err(ProcessPegOutSigError::WrongSignatureCount(
1115 psbt.inputs.len(),
1116 signature.signature.len(),
1117 ));
1118 }
1119
1120 let mut tx_hasher = SighashCache::new(&psbt.unsigned_tx);
1121 for (idx, (input, signature)) in psbt
1122 .inputs
1123 .iter_mut()
1124 .zip(signature.signature.iter())
1125 .enumerate()
1126 {
1127 let tx_hash = tx_hasher
1128 .p2wsh_signature_hash(
1129 idx,
1130 input
1131 .witness_script
1132 .as_ref()
1133 .expect("Missing witness script"),
1134 input.witness_utxo.as_ref().expect("Missing UTXO").value,
1135 EcdsaSighashType::All,
1136 )
1137 .map_err(|_| ProcessPegOutSigError::SighashError)?;
1138
1139 let tweak = input
1140 .proprietary
1141 .get(&proprietary_tweak_key())
1142 .expect("we saved it with a tweak");
1143
1144 let tweaked_peer_key = peer_key.tweak(tweak, &self.secp);
1145 self.secp
1146 .verify_ecdsa(
1147 &Message::from_digest_slice(&tx_hash[..]).unwrap(),
1148 signature,
1149 &tweaked_peer_key.key,
1150 )
1151 .map_err(|_| ProcessPegOutSigError::InvalidSignature)?;
1152
1153 if input
1154 .partial_sigs
1155 .insert(tweaked_peer_key.into(), EcdsaSig::sighash_all(*signature))
1156 .is_some()
1157 {
1158 return Err(ProcessPegOutSigError::DuplicateSignature);
1160 }
1161 }
1162 Ok(())
1163 }
1164
1165 fn finalize_peg_out_psbt(
1166 &self,
1167 mut unsigned: UnsignedTransaction,
1168 ) -> Result<PendingTransaction, ProcessPegOutSigError> {
1169 let change_tweak: [u8; 33] = unsigned
1174 .psbt
1175 .outputs
1176 .iter()
1177 .find_map(|output| output.proprietary.get(&proprietary_tweak_key()).cloned())
1178 .ok_or(ProcessPegOutSigError::MissingOrMalformedChangeTweak)?
1179 .try_into()
1180 .map_err(|_| ProcessPegOutSigError::MissingOrMalformedChangeTweak)?;
1181
1182 if let Err(error) = unsigned.psbt.finalize_mut(&self.secp) {
1183 return Err(ProcessPegOutSigError::ErrorFinalizingPsbt(error));
1184 }
1185
1186 let tx = unsigned.psbt.clone().extract_tx_unchecked_fee_rate();
1187
1188 Ok(PendingTransaction {
1189 tx,
1190 tweak: change_tweak,
1191 change: unsigned.change,
1192 destination: unsigned.destination,
1193 fees: unsigned.fees,
1194 selected_utxos: unsigned.selected_utxos,
1195 peg_out_amount: unsigned.peg_out_amount,
1196 rbf: unsigned.rbf,
1197 })
1198 }
1199
1200 fn get_block_count(&self) -> anyhow::Result<u32> {
1201 self.block_count_rx
1202 .borrow()
1203 .ok_or_else(|| format_err!("Block count not available yet"))
1204 .and_then(|block_count| {
1205 block_count
1206 .try_into()
1207 .map_err(|_| format_err!("Block count exceeds u32 limits"))
1208 })
1209 }
1210
1211 pub fn get_fee_rate_opt(&self) -> Feerate {
1212 #[allow(clippy::cast_precision_loss)]
1215 #[allow(clippy::cast_sign_loss)]
1216 Feerate {
1217 sats_per_kvb: ((self
1218 .fee_rate_rx
1219 .borrow()
1220 .unwrap_or(self.cfg.consensus.default_fee)
1221 .sats_per_kvb as f64
1222 * get_feerate_multiplier())
1223 .round()) as u64,
1224 }
1225 }
1226
1227 pub async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u32 {
1228 let peer_count = self.cfg.consensus.peer_peg_in_keys.to_num_peers().total();
1229
1230 let mut counts = dbtx
1231 .find_by_prefix(&BlockCountVotePrefix)
1232 .await
1233 .map(|entry| entry.1)
1234 .collect::<Vec<u32>>()
1235 .await;
1236
1237 assert!(counts.len() <= peer_count);
1238
1239 while counts.len() < peer_count {
1240 counts.push(0);
1241 }
1242
1243 counts.sort_unstable();
1244
1245 counts[peer_count / 2]
1246 }
1247
1248 pub async fn consensus_fee_rate(&self, dbtx: &mut DatabaseTransaction<'_>) -> Feerate {
1249 let peer_count = self.cfg.consensus.peer_peg_in_keys.to_num_peers().total();
1250
1251 let mut rates = dbtx
1252 .find_by_prefix(&FeeRateVotePrefix)
1253 .await
1254 .map(|(.., rate)| rate)
1255 .collect::<Vec<_>>()
1256 .await;
1257
1258 assert!(rates.len() <= peer_count);
1259
1260 while rates.len() < peer_count {
1261 rates.push(self.cfg.consensus.default_fee);
1262 }
1263
1264 rates.sort_unstable();
1265
1266 rates[peer_count / 2]
1267 }
1268
1269 async fn consensus_module_consensus_version(
1270 &self,
1271 dbtx: &mut DatabaseTransaction<'_>,
1272 ) -> ModuleConsensusVersion {
1273 let num_peers = self.cfg.consensus.peer_peg_in_keys.to_num_peers();
1274
1275 let mut versions = dbtx
1276 .find_by_prefix(&ConsensusVersionVotePrefix)
1277 .await
1278 .map(|entry| entry.1)
1279 .collect::<Vec<ModuleConsensusVersion>>()
1280 .await;
1281
1282 while versions.len() < num_peers.total() {
1283 versions.push(ModuleConsensusVersion::new(2, 0));
1284 }
1285
1286 assert_eq!(versions.len(), num_peers.total());
1287
1288 versions.sort_unstable();
1289
1290 assert!(versions.first() <= versions.last());
1291
1292 versions[num_peers.max_evil()]
1293 }
1294
1295 pub async fn consensus_nonce(&self, dbtx: &mut DatabaseTransaction<'_>) -> [u8; 33] {
1296 let nonce_idx = dbtx.get_value(&PegOutNonceKey).await.unwrap_or(0);
1297 dbtx.insert_entry(&PegOutNonceKey, &(nonce_idx + 1)).await;
1298
1299 nonce_from_idx(nonce_idx)
1300 }
1301
1302 async fn sync_up_to_consensus_count<'a>(
1303 &self,
1304 dbtx: &mut DatabaseTransaction<'a>,
1305 old_count: u32,
1306 new_count: u32,
1307 ) {
1308 info!(
1309 target: LOG_MODULE_WALLET,
1310 new_count,
1311 blocks_to_go = new_count - old_count,
1312 "New block count consensus, syncing up",
1313 );
1314
1315 self.wait_for_finality_confs_or_shutdown(new_count).await;
1318
1319 for height in old_count..new_count {
1320 if height % 100 == 0 {
1321 debug!(
1322 target: LOG_MODULE_WALLET,
1323 "Caught up to block {height}"
1324 );
1325 }
1326
1327 trace!(block = height, "Fetching block hash");
1329 let block_hash = retry("get_block_hash", backoff_util::background_backoff(), || {
1330 self.btc_rpc.get_block_hash(u64::from(height)) })
1332 .await
1333 .expect("bitcoind rpc to get block hash");
1334
1335 if self.consensus_module_consensus_version(dbtx).await
1336 >= ModuleConsensusVersion::new(2, 2)
1337 {
1338 let block = retry("get_block", backoff_util::background_backoff(), || {
1339 self.btc_rpc.get_block(&block_hash)
1340 })
1341 .await
1342 .expect("bitcoind rpc to get block");
1343
1344 for transaction in block.txdata {
1345 for tx_in in &transaction.input {
1350 dbtx.remove_entry(&UnspentTxOutKey(tx_in.previous_output))
1351 .await;
1352 }
1353
1354 for (vout, tx_out) in transaction.output.iter().enumerate() {
1355 let should_track_utxo = if self.cfg.consensus.peer_peg_in_keys.len() > 1 {
1356 tx_out.script_pubkey.is_p2wsh()
1357 } else {
1358 tx_out.script_pubkey.is_p2wpkh()
1359 };
1360
1361 if should_track_utxo {
1362 let outpoint = bitcoin::OutPoint {
1363 txid: transaction.compute_txid(),
1364 vout: vout as u32,
1365 };
1366
1367 dbtx.insert_new_entry(&UnspentTxOutKey(outpoint), tx_out)
1368 .await;
1369 }
1370 }
1371 }
1372 }
1373
1374 let pending_transactions = dbtx
1375 .find_by_prefix(&PendingTransactionPrefixKey)
1376 .await
1377 .map(|(key, transaction)| (key.0, transaction))
1378 .collect::<HashMap<Txid, PendingTransaction>>()
1379 .await;
1380 let pending_transactions_len = pending_transactions.len();
1381
1382 debug!(
1383 target: LOG_MODULE_WALLET,
1384 ?height,
1385 ?pending_transactions_len,
1386 "Recognizing change UTXOs"
1387 );
1388 for (txid, tx) in &pending_transactions {
1389 let is_tx_in_block =
1390 retry("is_tx_in_block", backoff_util::background_backoff(), || {
1391 self.btc_rpc
1392 .is_tx_in_block(txid, &block_hash, u64::from(height))
1393 })
1394 .await
1395 .unwrap_or_else(|_| {
1396 panic!("Failed checking if tx is in block height {height}")
1397 });
1398
1399 if is_tx_in_block {
1400 debug!(
1401 target: LOG_MODULE_WALLET,
1402 ?txid, ?height, ?block_hash, "Recognizing change UTXO"
1403 );
1404 self.recognize_change_utxo(dbtx, tx).await;
1405 } else {
1406 debug!(
1407 target: LOG_MODULE_WALLET,
1408 ?txid,
1409 ?height,
1410 ?block_hash,
1411 "Pending transaction not yet confirmed in this block"
1412 );
1413 }
1414 }
1415
1416 dbtx.insert_new_entry(&BlockHashKey(block_hash), &()).await;
1417 }
1418 }
1419
1420 async fn recognize_change_utxo<'a>(
1423 &self,
1424 dbtx: &mut DatabaseTransaction<'a>,
1425 pending_tx: &PendingTransaction,
1426 ) {
1427 self.remove_rbf_transactions(dbtx, pending_tx).await;
1428
1429 let script_pk = self
1430 .cfg
1431 .consensus
1432 .peg_in_descriptor
1433 .tweak(&pending_tx.tweak, &self.secp)
1434 .script_pubkey();
1435 for (idx, output) in pending_tx.tx.output.iter().enumerate() {
1436 if output.script_pubkey == script_pk {
1437 dbtx.insert_entry(
1438 &UTXOKey(bitcoin::OutPoint {
1439 txid: pending_tx.tx.compute_txid(),
1440 vout: idx as u32,
1441 }),
1442 &SpendableUTXO {
1443 tweak: pending_tx.tweak,
1444 amount: output.value,
1445 },
1446 )
1447 .await;
1448 }
1449 }
1450 }
1451
1452 async fn remove_rbf_transactions<'a>(
1454 &self,
1455 dbtx: &mut DatabaseTransaction<'a>,
1456 pending_tx: &PendingTransaction,
1457 ) {
1458 let mut all_transactions: BTreeMap<Txid, PendingTransaction> = dbtx
1459 .find_by_prefix(&PendingTransactionPrefixKey)
1460 .await
1461 .map(|(key, val)| (key.0, val))
1462 .collect::<BTreeMap<Txid, PendingTransaction>>()
1463 .await;
1464
1465 let mut pending_to_remove = vec![pending_tx.clone()];
1467 while let Some(removed) = pending_to_remove.pop() {
1468 all_transactions.remove(&removed.tx.compute_txid());
1469 dbtx.remove_entry(&PendingTransactionKey(removed.tx.compute_txid()))
1470 .await;
1471
1472 if let Some(rbf) = &removed.rbf {
1474 if let Some(tx) = all_transactions.get(&rbf.txid) {
1475 pending_to_remove.push(tx.clone());
1476 }
1477 }
1478
1479 for tx in all_transactions.values() {
1481 if let Some(rbf) = &tx.rbf {
1482 if rbf.txid == removed.tx.compute_txid() {
1483 pending_to_remove.push(tx.clone());
1484 }
1485 }
1486 }
1487 }
1488 }
1489
1490 async fn block_is_known(
1491 &self,
1492 dbtx: &mut DatabaseTransaction<'_>,
1493 block_hash: BlockHash,
1494 ) -> bool {
1495 dbtx.get_value(&BlockHashKey(block_hash)).await.is_some()
1496 }
1497
1498 async fn create_peg_out_tx(
1499 &self,
1500 dbtx: &mut DatabaseTransaction<'_>,
1501 output: &WalletOutputV0,
1502 change_tweak: &[u8; 33],
1503 ) -> Result<UnsignedTransaction, WalletOutputError> {
1504 match output {
1505 WalletOutputV0::PegOut(peg_out) => self.offline_wallet().create_tx(
1506 peg_out.amount,
1507 peg_out.recipient.clone().assume_checked().script_pubkey(),
1511 vec![],
1512 self.available_utxos(dbtx).await,
1513 peg_out.fees.fee_rate,
1514 change_tweak,
1515 None,
1516 ),
1517 WalletOutputV0::Rbf(rbf) => {
1518 let tx = dbtx
1519 .get_value(&PendingTransactionKey(rbf.txid))
1520 .await
1521 .ok_or(WalletOutputError::RbfTransactionIdNotFound)?;
1522
1523 self.offline_wallet().create_tx(
1524 tx.peg_out_amount,
1525 tx.destination,
1526 tx.selected_utxos,
1527 self.available_utxos(dbtx).await,
1528 tx.fees.fee_rate,
1529 change_tweak,
1530 Some(rbf.clone()),
1531 )
1532 }
1533 }
1534 }
1535
1536 async fn available_utxos(
1537 &self,
1538 dbtx: &mut DatabaseTransaction<'_>,
1539 ) -> Vec<(UTXOKey, SpendableUTXO)> {
1540 dbtx.find_by_prefix(&UTXOPrefixKey)
1541 .await
1542 .collect::<Vec<(UTXOKey, SpendableUTXO)>>()
1543 .await
1544 }
1545
1546 pub async fn get_wallet_value(&self, dbtx: &mut DatabaseTransaction<'_>) -> bitcoin::Amount {
1547 let sat_sum = self
1548 .available_utxos(dbtx)
1549 .await
1550 .into_iter()
1551 .map(|(_, utxo)| utxo.amount.to_sat())
1552 .sum();
1553 bitcoin::Amount::from_sat(sat_sum)
1554 }
1555
1556 async fn get_wallet_summary(&self, dbtx: &mut DatabaseTransaction<'_>) -> WalletSummary {
1557 fn partition_peg_out_and_change(
1558 transactions: Vec<Transaction>,
1559 ) -> (Vec<TxOutputSummary>, Vec<TxOutputSummary>) {
1560 let mut peg_out_txos: Vec<TxOutputSummary> = Vec::new();
1561 let mut change_utxos: Vec<TxOutputSummary> = Vec::new();
1562
1563 for tx in transactions {
1564 let txid = tx.compute_txid();
1565
1566 let peg_out_output = tx
1569 .output
1570 .first()
1571 .expect("tx must contain withdrawal output");
1572
1573 let change_output = tx.output.last().expect("tx must contain change output");
1574
1575 peg_out_txos.push(TxOutputSummary {
1576 outpoint: bitcoin::OutPoint { txid, vout: 0 },
1577 amount: peg_out_output.value,
1578 });
1579
1580 change_utxos.push(TxOutputSummary {
1581 outpoint: bitcoin::OutPoint { txid, vout: 1 },
1582 amount: change_output.value,
1583 });
1584 }
1585
1586 (peg_out_txos, change_utxos)
1587 }
1588
1589 let spendable_utxos = self
1590 .available_utxos(dbtx)
1591 .await
1592 .iter()
1593 .map(|(utxo_key, spendable_utxo)| TxOutputSummary {
1594 outpoint: utxo_key.0,
1595 amount: spendable_utxo.amount,
1596 })
1597 .collect::<Vec<_>>();
1598
1599 let unsigned_transactions = dbtx
1601 .find_by_prefix(&UnsignedTransactionPrefixKey)
1602 .await
1603 .map(|(_tx_key, tx)| tx.psbt.unsigned_tx)
1604 .collect::<Vec<_>>()
1605 .await;
1606
1607 let unconfirmed_transactions = dbtx
1609 .find_by_prefix(&PendingTransactionPrefixKey)
1610 .await
1611 .map(|(_tx_key, tx)| tx.tx)
1612 .collect::<Vec<_>>()
1613 .await;
1614
1615 let (unsigned_peg_out_txos, unsigned_change_utxos) =
1616 partition_peg_out_and_change(unsigned_transactions);
1617
1618 let (unconfirmed_peg_out_txos, unconfirmed_change_utxos) =
1619 partition_peg_out_and_change(unconfirmed_transactions);
1620
1621 WalletSummary {
1622 spendable_utxos,
1623 unsigned_peg_out_txos,
1624 unsigned_change_utxos,
1625 unconfirmed_peg_out_txos,
1626 unconfirmed_change_utxos,
1627 }
1628 }
1629
1630 async fn is_utxo_confirmed(
1631 &self,
1632 dbtx: &mut DatabaseTransaction<'_>,
1633 outpoint: bitcoin::OutPoint,
1634 ) -> bool {
1635 dbtx.get_value(&UnspentTxOutKey(outpoint)).await.is_some()
1636 }
1637
1638 fn offline_wallet(&self) -> StatelessWallet {
1639 StatelessWallet {
1640 descriptor: &self.cfg.consensus.peg_in_descriptor,
1641 secret_key: &self.cfg.private.peg_in_key,
1642 secp: &self.secp,
1643 }
1644 }
1645
1646 fn spawn_broadcast_pending_task(
1647 task_group: &TaskGroup,
1648 bitcoind: &DynBitcoindRpc,
1649 db: &Database,
1650 broadcast_pending_notify: Arc<Notify>,
1651 ) {
1652 task_group.spawn_cancellable("broadcast pending", {
1653 let bitcoind = bitcoind.clone();
1654 let db = db.clone();
1655 run_broadcast_pending_tx(db, bitcoind, broadcast_pending_notify)
1656 });
1657 }
1658
1659 async fn graceful_shutdown(&self) {
1662 if let Err(e) = self
1663 .task_group
1664 .clone()
1665 .shutdown_join_all(Some(Duration::from_secs(60)))
1666 .await
1667 {
1668 panic!("Error while shutting down fedimintd task group: {e}");
1669 }
1670 }
1671
1672 async fn wait_for_finality_confs_or_shutdown(&self, consensus_block_count: u32) {
1678 let backoff = if is_running_in_test_env() {
1679 backoff_util::custom_backoff(
1681 Duration::from_millis(100),
1682 Duration::from_millis(100),
1683 Some(10 * 60),
1684 )
1685 } else {
1686 backoff_util::fibonacci_max_one_hour()
1688 };
1689
1690 let wait_for_finality_confs = || async {
1691 let our_chain_tip_block_count = self.get_block_count()?;
1692 let consensus_chain_tip_block_count =
1693 consensus_block_count + self.cfg.consensus.finality_delay;
1694
1695 if consensus_chain_tip_block_count <= our_chain_tip_block_count {
1696 Ok(())
1697 } else {
1698 Err(anyhow::anyhow!("not enough confirmations"))
1699 }
1700 };
1701
1702 if retry("wait_for_finality_confs", backoff, wait_for_finality_confs)
1703 .await
1704 .is_err()
1705 {
1706 self.graceful_shutdown().await;
1707 }
1708 }
1709
1710 fn spawn_peer_supported_consensus_version_task(
1711 api_client: DynModuleApi,
1712 task_group: &TaskGroup,
1713 our_peer_id: PeerId,
1714 ) -> watch::Receiver<Option<ModuleConsensusVersion>> {
1715 let (sender, receiver) = watch::channel(None);
1716 task_group.spawn_cancellable("fetch-peer-consensus-versions", async move {
1717 loop {
1718 let request_futures = api_client.all_peers().iter().filter_map(|&peer| {
1719 if peer == our_peer_id {
1720 return None;
1721 }
1722
1723 let api_client_inner = api_client.clone();
1724 Some(async move {
1725 api_client_inner
1726 .request_single_peer::<ModuleConsensusVersion>(
1727 SUPPORTED_MODULE_CONSENSUS_VERSION_ENDPOINT.to_owned(),
1728 ApiRequestErased::default(),
1729 peer,
1730 )
1731 .await
1732 .inspect(|res| debug!(
1733 target: LOG_MODULE_WALLET,
1734 %peer,
1735 %our_peer_id,
1736 ?res,
1737 "Fetched supported module consensus version from peer"
1738 ))
1739 .inspect_err(|err| warn!(
1740 target: LOG_MODULE_WALLET,
1741 %peer,
1742 err=%err.fmt_compact(),
1743 "Failed to fetch consensus version from peer"
1744 ))
1745 .ok()
1746 })
1747 });
1748
1749 let peer_consensus_versions = join_all(request_futures)
1750 .await
1751 .into_iter()
1752 .flatten()
1753 .collect::<Vec<_>>();
1754
1755 let sorted_consensus_versions = peer_consensus_versions
1756 .into_iter()
1757 .chain(std::iter::once(MODULE_CONSENSUS_VERSION))
1758 .sorted()
1759 .collect::<Vec<_>>();
1760 let all_peers_supported_version =
1761 if sorted_consensus_versions.len() == api_client.all_peers().len() {
1762 let min_supported_version = *sorted_consensus_versions
1763 .first()
1764 .expect("at least one element");
1765
1766 debug!(
1767 target: LOG_MODULE_WALLET,
1768 ?sorted_consensus_versions,
1769 "Fetched supported consensus versions from peers"
1770 );
1771
1772 Some(min_supported_version)
1773 } else {
1774 assert!(
1775 sorted_consensus_versions.len() <= api_client.all_peers().len(),
1776 "Too many peer responses",
1777 );
1778 trace!(
1779 target: LOG_MODULE_WALLET,
1780 ?sorted_consensus_versions,
1781 "Not all peers have reported their consensus version yet"
1782 );
1783 None
1784 };
1785
1786 if sender.send(all_peers_supported_version).is_err() {
1787 warn!(target: LOG_MODULE_WALLET, "Failed to send consensus version to watch channel, stopping task");
1788 break;
1789 }
1790
1791 if is_running_in_test_env() {
1792 sleep(Duration::from_secs(5)).await;
1794 } else {
1795 sleep(Duration::from_secs(600)).await;
1796 }
1797 }
1798 });
1799 receiver
1800 }
1801}
1802
1803#[instrument(target = LOG_MODULE_WALLET, level = "debug", skip_all)]
1804pub async fn run_broadcast_pending_tx(db: Database, rpc: DynBitcoindRpc, broadcast: Arc<Notify>) {
1805 loop {
1806 let _ = tokio::time::timeout(Duration::from_secs(60), broadcast.notified()).await;
1808 broadcast_pending_tx(db.begin_transaction_nc().await, &rpc).await;
1809 }
1810}
1811
1812pub async fn broadcast_pending_tx(mut dbtx: DatabaseTransaction<'_>, rpc: &DynBitcoindRpc) {
1813 let pending_tx: Vec<PendingTransaction> = dbtx
1814 .find_by_prefix(&PendingTransactionPrefixKey)
1815 .await
1816 .map(|(_, val)| val)
1817 .collect::<Vec<_>>()
1818 .await;
1819 let rbf_txids: BTreeSet<Txid> = pending_tx
1820 .iter()
1821 .filter_map(|tx| tx.rbf.clone().map(|rbf| rbf.txid))
1822 .collect();
1823 if !pending_tx.is_empty() {
1824 debug!(
1825 target: LOG_MODULE_WALLET,
1826 "Broadcasting pending transactions (total={}, rbf={})",
1827 pending_tx.len(),
1828 rbf_txids.len()
1829 );
1830 }
1831
1832 for PendingTransaction { tx, .. } in pending_tx {
1833 if !rbf_txids.contains(&tx.compute_txid()) {
1834 debug!(
1835 target: LOG_MODULE_WALLET,
1836 tx = %tx.compute_txid(),
1837 weight = tx.weight().to_wu(),
1838 output = ?tx.output,
1839 "Broadcasting peg-out",
1840 );
1841 trace!(transaction = ?tx);
1842 rpc.submit_transaction(tx).await;
1843 }
1844 }
1845}
1846
1847struct StatelessWallet<'a> {
1848 descriptor: &'a Descriptor<CompressedPublicKey>,
1849 secret_key: &'a secp256k1::SecretKey,
1850 secp: &'a secp256k1::Secp256k1<secp256k1::All>,
1851}
1852
1853impl<'a> StatelessWallet<'a> {
1854 fn validate_tx(
1857 tx: &UnsignedTransaction,
1858 output: &WalletOutputV0,
1859 consensus_fee_rate: Feerate,
1860 network: Network,
1861 ) -> Result<(), WalletOutputError> {
1862 if let WalletOutputV0::PegOut(peg_out) = output {
1863 if !peg_out.recipient.is_valid_for_network(network) {
1864 return Err(WalletOutputError::WrongNetwork(
1865 NetworkLegacyEncodingWrapper(network),
1866 NetworkLegacyEncodingWrapper(get_network_for_address(&peg_out.recipient)),
1867 ));
1868 }
1869 }
1870
1871 if tx.peg_out_amount < tx.destination.minimal_non_dust() {
1873 return Err(WalletOutputError::PegOutUnderDustLimit);
1874 }
1875
1876 if tx.fees.fee_rate < consensus_fee_rate {
1878 return Err(WalletOutputError::PegOutFeeBelowConsensus(
1879 tx.fees.fee_rate,
1880 consensus_fee_rate,
1881 ));
1882 }
1883
1884 let fees = match output {
1887 WalletOutputV0::PegOut(pegout) => pegout.fees,
1888 WalletOutputV0::Rbf(rbf) => rbf.fees,
1889 };
1890 if fees.fee_rate.sats_per_kvb < u64::from(DEFAULT_MIN_RELAY_TX_FEE) {
1891 return Err(WalletOutputError::BelowMinRelayFee);
1892 }
1893
1894 if fees.total_weight != tx.fees.total_weight {
1896 return Err(WalletOutputError::TxWeightIncorrect(
1897 fees.total_weight,
1898 tx.fees.total_weight,
1899 ));
1900 }
1901
1902 Ok(())
1903 }
1904
1905 #[allow(clippy::too_many_arguments)]
1915 fn create_tx(
1916 &self,
1917 peg_out_amount: bitcoin::Amount,
1918 destination: ScriptBuf,
1919 mut included_utxos: Vec<(UTXOKey, SpendableUTXO)>,
1920 mut remaining_utxos: Vec<(UTXOKey, SpendableUTXO)>,
1921 mut fee_rate: Feerate,
1922 change_tweak: &[u8; 33],
1923 rbf: Option<Rbf>,
1924 ) -> Result<UnsignedTransaction, WalletOutputError> {
1925 if let Some(rbf) = &rbf {
1927 fee_rate.sats_per_kvb += rbf.fees.fee_rate.sats_per_kvb;
1928 }
1929
1930 let change_script = self.derive_script(change_tweak);
1938 let out_weight = (destination.len() * 4 + 1 + 32
1939 + 1 + change_script.len() * 4 + 32) as u64; let mut total_weight = 16 + 12 + 12 + out_weight + 16; #[allow(deprecated)]
1950 let max_input_weight = (self
1951 .descriptor
1952 .max_satisfaction_weight()
1953 .expect("is satisfyable") +
1954 128 + 16 + 16) as u64; included_utxos.sort_by_key(|(_, utxo)| utxo.amount);
1960 remaining_utxos.sort_by_key(|(_, utxo)| utxo.amount);
1961 included_utxos.extend(remaining_utxos);
1962
1963 let mut total_selected_value = bitcoin::Amount::from_sat(0);
1965 let mut selected_utxos: Vec<(UTXOKey, SpendableUTXO)> = vec![];
1966 let mut fees = fee_rate.calculate_fee(total_weight);
1967
1968 while total_selected_value < peg_out_amount + change_script.minimal_non_dust() + fees {
1969 match included_utxos.pop() {
1970 Some((utxo_key, utxo)) => {
1971 total_selected_value += utxo.amount;
1972 total_weight += max_input_weight;
1973 fees = fee_rate.calculate_fee(total_weight);
1974 selected_utxos.push((utxo_key, utxo));
1975 }
1976 _ => return Err(WalletOutputError::NotEnoughSpendableUTXO), }
1978 }
1979
1980 let change = total_selected_value - fees - peg_out_amount;
1983 let output: Vec<TxOut> = vec![
1984 TxOut {
1985 value: peg_out_amount,
1986 script_pubkey: destination.clone(),
1987 },
1988 TxOut {
1989 value: change,
1990 script_pubkey: change_script,
1991 },
1992 ];
1993 let mut change_out = bitcoin::psbt::Output::default();
1994 change_out
1995 .proprietary
1996 .insert(proprietary_tweak_key(), change_tweak.to_vec());
1997
1998 info!(
1999 target: LOG_MODULE_WALLET,
2000 inputs = selected_utxos.len(),
2001 input_sats = total_selected_value.to_sat(),
2002 peg_out_sats = peg_out_amount.to_sat(),
2003 ?total_weight,
2004 fees_sats = fees.to_sat(),
2005 fee_rate = fee_rate.sats_per_kvb,
2006 change_sats = change.to_sat(),
2007 "Creating peg-out tx",
2008 );
2009
2010 let transaction = Transaction {
2011 version: bitcoin::transaction::Version(2),
2012 lock_time: LockTime::ZERO,
2013 input: selected_utxos
2014 .iter()
2015 .map(|(utxo_key, _utxo)| TxIn {
2016 previous_output: utxo_key.0,
2017 script_sig: Default::default(),
2018 sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
2019 witness: bitcoin::Witness::new(),
2020 })
2021 .collect(),
2022 output,
2023 };
2024 info!(
2025 target: LOG_MODULE_WALLET,
2026 txid = %transaction.compute_txid(), "Creating peg-out tx"
2027 );
2028
2029 let psbt = Psbt {
2032 unsigned_tx: transaction,
2033 version: 0,
2034 xpub: Default::default(),
2035 proprietary: Default::default(),
2036 unknown: Default::default(),
2037 inputs: selected_utxos
2038 .iter()
2039 .map(|(_utxo_key, utxo)| {
2040 let script_pubkey = self
2041 .descriptor
2042 .tweak(&utxo.tweak, self.secp)
2043 .script_pubkey();
2044 Input {
2045 non_witness_utxo: None,
2046 witness_utxo: Some(TxOut {
2047 value: utxo.amount,
2048 script_pubkey,
2049 }),
2050 partial_sigs: Default::default(),
2051 sighash_type: None,
2052 redeem_script: None,
2053 witness_script: Some(
2054 self.descriptor
2055 .tweak(&utxo.tweak, self.secp)
2056 .script_code()
2057 .expect("Failed to tweak descriptor"),
2058 ),
2059 bip32_derivation: Default::default(),
2060 final_script_sig: None,
2061 final_script_witness: None,
2062 ripemd160_preimages: Default::default(),
2063 sha256_preimages: Default::default(),
2064 hash160_preimages: Default::default(),
2065 hash256_preimages: Default::default(),
2066 proprietary: vec![(proprietary_tweak_key(), utxo.tweak.to_vec())]
2067 .into_iter()
2068 .collect(),
2069 tap_key_sig: Default::default(),
2070 tap_script_sigs: Default::default(),
2071 tap_scripts: Default::default(),
2072 tap_key_origins: Default::default(),
2073 tap_internal_key: Default::default(),
2074 tap_merkle_root: Default::default(),
2075 unknown: Default::default(),
2076 }
2077 })
2078 .collect(),
2079 outputs: vec![Default::default(), change_out],
2080 };
2081
2082 Ok(UnsignedTransaction {
2083 psbt,
2084 signatures: vec![],
2085 change,
2086 fees: PegOutFees {
2087 fee_rate,
2088 total_weight,
2089 },
2090 destination,
2091 selected_utxos,
2092 peg_out_amount,
2093 rbf,
2094 })
2095 }
2096
2097 fn sign_psbt(&self, psbt: &mut Psbt) {
2098 let mut tx_hasher = SighashCache::new(&psbt.unsigned_tx);
2099
2100 for (idx, (psbt_input, _tx_input)) in psbt
2101 .inputs
2102 .iter_mut()
2103 .zip(psbt.unsigned_tx.input.iter())
2104 .enumerate()
2105 {
2106 let tweaked_secret = {
2107 let tweak = psbt_input
2108 .proprietary
2109 .get(&proprietary_tweak_key())
2110 .expect("Malformed PSBT: expected tweak");
2111
2112 self.secret_key.tweak(tweak, self.secp)
2113 };
2114
2115 let tx_hash = tx_hasher
2116 .p2wsh_signature_hash(
2117 idx,
2118 psbt_input
2119 .witness_script
2120 .as_ref()
2121 .expect("Missing witness script"),
2122 psbt_input
2123 .witness_utxo
2124 .as_ref()
2125 .expect("Missing UTXO")
2126 .value,
2127 EcdsaSighashType::All,
2128 )
2129 .expect("Failed to create segwit sighash");
2130
2131 let signature = self.secp.sign_ecdsa(
2132 &Message::from_digest_slice(&tx_hash[..]).unwrap(),
2133 &tweaked_secret,
2134 );
2135
2136 psbt_input.partial_sigs.insert(
2137 bitcoin::PublicKey {
2138 compressed: true,
2139 inner: secp256k1::PublicKey::from_secret_key(self.secp, &tweaked_secret),
2140 },
2141 EcdsaSig::sighash_all(signature),
2142 );
2143 }
2144 }
2145
2146 fn derive_script(&self, tweak: &[u8]) -> ScriptBuf {
2147 struct CompressedPublicKeyTranslator<'t, 's, Ctx: Verification> {
2148 tweak: &'t [u8],
2149 secp: &'s Secp256k1<Ctx>,
2150 }
2151
2152 impl<'t, 's, Ctx: Verification>
2153 miniscript::Translator<CompressedPublicKey, CompressedPublicKey, Infallible>
2154 for CompressedPublicKeyTranslator<'t, 's, Ctx>
2155 {
2156 fn pk(&mut self, pk: &CompressedPublicKey) -> Result<CompressedPublicKey, Infallible> {
2157 let hashed_tweak = {
2158 let mut hasher = HmacEngine::<sha256::Hash>::new(&pk.key.serialize()[..]);
2159 hasher.input(self.tweak);
2160 Hmac::from_engine(hasher).to_byte_array()
2161 };
2162
2163 Ok(CompressedPublicKey {
2164 key: pk
2165 .key
2166 .add_exp_tweak(
2167 self.secp,
2168 &Scalar::from_be_bytes(hashed_tweak).expect("can't fail"),
2169 )
2170 .expect("tweaking failed"),
2171 })
2172 }
2173 translate_hash_fail!(CompressedPublicKey, CompressedPublicKey, Infallible);
2174 }
2175
2176 let descriptor = self
2177 .descriptor
2178 .translate_pk(&mut CompressedPublicKeyTranslator {
2179 tweak,
2180 secp: self.secp,
2181 })
2182 .expect("can't fail");
2183
2184 descriptor.script_pubkey()
2185 }
2186}
2187
2188pub fn nonce_from_idx(nonce_idx: u64) -> [u8; 33] {
2189 let mut nonce: [u8; 33] = [0; 33];
2190 nonce[0] = 0x02;
2192 nonce[1..].copy_from_slice(&nonce_idx.consensus_hash::<bitcoin::hashes::sha256::Hash>()[..]);
2193
2194 nonce
2195}
2196
2197#[derive(Clone, Debug, Encodable, Decodable)]
2199pub struct PendingTransaction {
2200 pub tx: bitcoin::Transaction,
2201 pub tweak: [u8; 33],
2202 pub change: bitcoin::Amount,
2203 pub destination: ScriptBuf,
2204 pub fees: PegOutFees,
2205 pub selected_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2206 pub peg_out_amount: bitcoin::Amount,
2207 pub rbf: Option<Rbf>,
2208}
2209
2210impl Serialize for PendingTransaction {
2211 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2212 where
2213 S: serde::Serializer,
2214 {
2215 if serializer.is_human_readable() {
2216 serializer.serialize_str(&self.consensus_encode_to_hex())
2217 } else {
2218 serializer.serialize_bytes(&self.consensus_encode_to_vec())
2219 }
2220 }
2221}
2222
2223#[derive(Clone, Debug, Eq, PartialEq, Encodable, Decodable)]
2226pub struct UnsignedTransaction {
2227 pub psbt: Psbt,
2228 pub signatures: Vec<(PeerId, PegOutSignatureItem)>,
2229 pub change: bitcoin::Amount,
2230 pub fees: PegOutFees,
2231 pub destination: ScriptBuf,
2232 pub selected_utxos: Vec<(UTXOKey, SpendableUTXO)>,
2233 pub peg_out_amount: bitcoin::Amount,
2234 pub rbf: Option<Rbf>,
2235}
2236
2237impl Serialize for UnsignedTransaction {
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#[cfg(test)]
2251mod tests {
2252
2253 use std::str::FromStr;
2254
2255 use bitcoin::Network::{Bitcoin, Testnet};
2256 use bitcoin::hashes::Hash;
2257 use bitcoin::{Address, Amount, OutPoint, Txid, secp256k1};
2258 use fedimint_core::Feerate;
2259 use fedimint_core::encoding::btc::NetworkLegacyEncodingWrapper;
2260 use fedimint_wallet_common::{PegOut, PegOutFees, Rbf, WalletOutputV0};
2261 use miniscript::descriptor::Wsh;
2262
2263 use crate::common::PegInDescriptor;
2264 use crate::{
2265 CompressedPublicKey, OsRng, SpendableUTXO, StatelessWallet, UTXOKey, WalletOutputError,
2266 };
2267
2268 #[test]
2269 fn create_tx_should_validate_amounts() {
2270 let secp = secp256k1::Secp256k1::new();
2271
2272 let descriptor = PegInDescriptor::Wsh(
2273 Wsh::new_sortedmulti(
2274 3,
2275 (0..4)
2276 .map(|_| secp.generate_keypair(&mut OsRng))
2277 .map(|(_, key)| CompressedPublicKey { key })
2278 .collect(),
2279 )
2280 .unwrap(),
2281 );
2282
2283 let (secret_key, _) = secp.generate_keypair(&mut OsRng);
2284
2285 let wallet = StatelessWallet {
2286 descriptor: &descriptor,
2287 secret_key: &secret_key,
2288 secp: &secp,
2289 };
2290
2291 let spendable = SpendableUTXO {
2292 tweak: [0; 33],
2293 amount: bitcoin::Amount::from_sat(3000),
2294 };
2295
2296 let recipient = Address::from_str("32iVBEu4dxkUQk9dJbZUiBiQdmypcEyJRf").unwrap();
2297
2298 let fee = Feerate { sats_per_kvb: 1000 };
2299 let weight = 875;
2300
2301 let tx = wallet.create_tx(
2306 Amount::from_sat(2452),
2307 recipient.clone().assume_checked().script_pubkey(),
2308 vec![],
2309 vec![(UTXOKey(OutPoint::null()), spendable.clone())],
2310 fee,
2311 &[0; 33],
2312 None,
2313 );
2314 assert_eq!(tx, Err(WalletOutputError::NotEnoughSpendableUTXO));
2315
2316 let mut tx = wallet
2318 .create_tx(
2319 Amount::from_sat(1000),
2320 recipient.clone().assume_checked().script_pubkey(),
2321 vec![],
2322 vec![(UTXOKey(OutPoint::null()), spendable)],
2323 fee,
2324 &[0; 33],
2325 None,
2326 )
2327 .expect("is ok");
2328
2329 let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, 0), fee, Bitcoin);
2331 assert_eq!(res, Err(WalletOutputError::TxWeightIncorrect(0, weight)));
2332
2333 let res = StatelessWallet::validate_tx(&tx, &rbf(0, weight), fee, Bitcoin);
2335 assert_eq!(res, Err(WalletOutputError::BelowMinRelayFee));
2336
2337 let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2339 assert_eq!(res, Ok(()));
2340
2341 tx.fees = PegOutFees::new(0, weight);
2343 let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2344 assert_eq!(
2345 res,
2346 Err(WalletOutputError::PegOutFeeBelowConsensus(
2347 Feerate { sats_per_kvb: 0 },
2348 fee
2349 ))
2350 );
2351
2352 tx.peg_out_amount = bitcoin::Amount::ZERO;
2354 let res = StatelessWallet::validate_tx(&tx, &rbf(fee.sats_per_kvb, weight), fee, Bitcoin);
2355 assert_eq!(res, Err(WalletOutputError::PegOutUnderDustLimit));
2356
2357 let output = WalletOutputV0::PegOut(PegOut {
2359 recipient,
2360 amount: bitcoin::Amount::from_sat(1000),
2361 fees: PegOutFees::new(100, weight),
2362 });
2363 let res = StatelessWallet::validate_tx(&tx, &output, fee, Testnet);
2364 assert_eq!(
2365 res,
2366 Err(WalletOutputError::WrongNetwork(
2367 NetworkLegacyEncodingWrapper(Testnet),
2368 NetworkLegacyEncodingWrapper(Bitcoin)
2369 ))
2370 );
2371 }
2372
2373 fn rbf(sats_per_kvb: u64, total_weight: u64) -> WalletOutputV0 {
2374 WalletOutputV0::Rbf(Rbf {
2375 fees: PegOutFees::new(sats_per_kvb, total_weight),
2376 txid: Txid::all_zeros(),
2377 })
2378 }
2379}