1#![deny(clippy::pedantic)]
2
3mod envs;
4mod key;
5
6use std::collections::BTreeSet;
7use std::path::{Path, PathBuf};
8
9use anyhow::anyhow;
10use bitcoin::OutPoint;
11use bitcoin::network::Network;
12use bitcoin::secp256k1::{PublicKey, SECP256K1, SecretKey};
13use clap::{ArgGroup, Parser, Subcommand};
14use fedimint_core::core::LEGACY_HARDCODED_INSTANCE_ID_WALLET;
15use fedimint_core::db::{Database, IDatabaseTransactionOpsCoreTyped};
16use fedimint_core::epoch::ConsensusItem;
17use fedimint_core::fedimint_build_code_version_env;
18use fedimint_core::module::CommonModuleInit;
19use fedimint_core::module::registry::{ModuleDecoderRegistry, ModuleRegistry};
20use fedimint_core::session_outcome::SignedSessionOutcome;
21use fedimint_core::transaction::Transaction;
22use fedimint_core::util::handle_version_hash_command;
23use fedimint_logging::TracingSetup;
24use fedimint_rocksdb::RocksDbReadOnly;
25use fedimint_server::config::io::read_server_config;
26use fedimint_server::consensus::db::SignedSessionOutcomePrefix;
27use fedimint_server::core::ServerModule;
28use fedimint_wallet_server::common::config::WalletConfig;
29use fedimint_wallet_server::common::keys::CompressedPublicKey;
30use fedimint_wallet_server::common::tweakable::Tweakable;
31use fedimint_wallet_server::common::{
32 PegInDescriptor, SpendableUTXO, WalletCommonInit, WalletInput,
33};
34use fedimint_wallet_server::db::{UTXOKey, UTXOPrefixKey};
35use fedimint_wallet_server::{Wallet, nonce_from_idx};
36use futures::stream::StreamExt;
37use hex::FromHex;
38use miniscript::{Descriptor, MiniscriptKey, TranslatePk, Translator};
39use serde::Serialize;
40use tracing::info;
41
42use crate::envs::FM_PASSWORD_ENV;
43use crate::key::Key;
44
45#[derive(Debug, Parser)]
47#[command(version)]
48#[command(group(
49 ArgGroup::new("keysource")
50 .required(true)
51 .args(["config", "descriptor"]),
52))]
53struct RecoveryTool {
54 #[arg(long = "cfg")]
56 config: Option<PathBuf>,
57 #[arg(long, env = FM_PASSWORD_ENV, requires = "config")]
59 password: String,
60 #[arg(long)]
62 descriptor: Option<PegInDescriptor>,
63 #[arg(long, requires = "descriptor")]
66 key: Option<SecretKey>,
67 #[arg(long, default_value = "bitcoin", requires = "descriptor")]
69 network: Network,
70 #[command(subcommand)]
71 strategy: TweakSource,
72}
73
74#[derive(Debug, Clone, Subcommand)]
75enum TweakSource {
76 Direct {
78 #[arg(long, value_parser = tweak_parser)]
79 tweak: [u8; 33],
80 },
81 Utxos {
84 #[arg(long)]
86 legacy: bool,
87 #[arg(long)]
89 db: PathBuf,
90 },
91 Epochs {
95 #[arg(long)]
97 db: PathBuf,
98 },
99}
100
101fn tweak_parser(hex: &str) -> anyhow::Result<[u8; 33]> {
102 <Vec<u8> as FromHex>::from_hex(hex)?
103 .try_into()
104 .map_err(|_| anyhow!("tweaks have to be 33 bytes long"))
105}
106
107fn get_db(path: &Path, module_decoders: ModuleDecoderRegistry) -> Database {
108 Database::new(
109 RocksDbReadOnly::open_read_only(path).expect("Error opening readonly DB"),
110 module_decoders,
111 )
112}
113
114#[tokio::main]
115async fn main() -> anyhow::Result<()> {
116 TracingSetup::default().init()?;
117
118 handle_version_hash_command(fedimint_build_code_version_env!());
119
120 let opts: RecoveryTool = RecoveryTool::parse();
121
122 let (base_descriptor, base_key, network) = if let Some(config) = opts.config {
123 let cfg = read_server_config(&opts.password, &config).expect("Could not read config file");
124 let wallet_cfg: WalletConfig = cfg
125 .get_module_config_typed(LEGACY_HARDCODED_INSTANCE_ID_WALLET)
126 .expect("Malformed wallet config");
127 let base_descriptor = wallet_cfg.consensus.peg_in_descriptor;
128 let base_key = wallet_cfg.private.peg_in_key;
129 let network = wallet_cfg.consensus.network.0;
130
131 (base_descriptor, base_key, network)
132 } else if let (Some(descriptor), Some(key)) = (opts.descriptor, opts.key) {
133 (descriptor, key, opts.network)
134 } else {
135 panic!("Either config or descriptor need to be provided by clap");
136 };
137
138 process_and_print_tweak_source(&opts.strategy, &base_descriptor, &base_key, network).await;
139
140 Ok(())
141}
142
143async fn process_and_print_tweak_source(
144 tweak_source: &TweakSource,
145 base_descriptor: &Descriptor<CompressedPublicKey>,
146 base_key: &SecretKey,
147 network: Network,
148) {
149 match tweak_source {
150 TweakSource::Direct { tweak } => {
151 let descriptor = tweak_descriptor(base_descriptor, base_key, tweak, network);
152 let wallets = vec![ImportableWalletMin { descriptor }];
153
154 serde_json::to_writer(std::io::stdout().lock(), &wallets)
155 .expect("Could not encode to stdout");
156 }
157 TweakSource::Utxos { legacy, db } => {
158 let db = get_db(db, ModuleRegistry::default());
159
160 let db = if *legacy {
161 db
162 } else {
163 db.with_prefix_module_id(LEGACY_HARDCODED_INSTANCE_ID_WALLET)
164 .0
165 };
166
167 let utxos: Vec<ImportableWallet> = db
168 .begin_transaction_nc()
169 .await
170 .find_by_prefix(&UTXOPrefixKey)
171 .await
172 .map(|(UTXOKey(outpoint), SpendableUTXO { tweak, amount })| {
173 let descriptor = tweak_descriptor(base_descriptor, base_key, &tweak, network);
174
175 ImportableWallet {
176 outpoint,
177 descriptor,
178 amount_sat: amount,
179 }
180 })
181 .collect()
182 .await;
183
184 serde_json::to_writer(std::io::stdout().lock(), &utxos)
185 .expect("Could not encode to stdout");
186 }
187 TweakSource::Epochs { db } => {
188 let decoders = ModuleDecoderRegistry::from_iter([(
190 LEGACY_HARDCODED_INSTANCE_ID_WALLET,
191 WalletCommonInit::KIND,
192 <Wallet as ServerModule>::decoder(),
193 )])
194 .with_fallback();
195
196 let db = get_db(db, decoders);
197 let mut dbtx = db.begin_transaction_nc().await;
198
199 let mut change_tweak_idx: u64 = 0;
200
201 let tweaks = dbtx
202 .find_by_prefix(&SignedSessionOutcomePrefix)
203 .await
204 .flat_map(
205 |(
206 _key,
207 SignedSessionOutcome {
208 session_outcome: block,
209 ..
210 },
211 )| {
212 let transaction_cis: Vec<Transaction> = block
213 .items
214 .into_iter()
215 .filter_map(|item| match item.item {
216 ConsensusItem::Transaction(tx) => Some(tx),
217 ConsensusItem::Module(_) | ConsensusItem::Default { .. } => None,
218 })
219 .collect();
220
221 let (mut peg_in_tweaks, peg_out_count) =
224 input_tweaks_and_peg_out_count(transaction_cis.into_iter());
225
226 for _ in 0..peg_out_count {
227 info!("Found change output, adding tweak {change_tweak_idx} to list");
228 peg_in_tweaks.insert(nonce_from_idx(change_tweak_idx));
229 change_tweak_idx += 1;
230 }
231
232 futures::stream::iter(peg_in_tweaks.into_iter())
233 },
234 );
235
236 let wallets = tweaks
237 .map(|tweak| {
238 let descriptor = tweak_descriptor(base_descriptor, base_key, &tweak, network);
239 ImportableWalletMin { descriptor }
240 })
241 .collect::<Vec<_>>()
242 .await;
243
244 serde_json::to_writer(std::io::stdout().lock(), &wallets)
245 .expect("Could not encode to stdout");
246 }
247 }
248}
249
250fn input_tweaks_and_peg_out_count(
251 transactions: impl Iterator<Item = Transaction>,
252) -> (BTreeSet<[u8; 33]>, u64) {
253 let mut peg_out_count = 0;
254 let tweaks = transactions
255 .flat_map(|tx| {
256 tx.outputs.iter().for_each(|output| {
257 if output.module_instance_id() == LEGACY_HARDCODED_INSTANCE_ID_WALLET {
258 peg_out_count += 1;
259 }
260 });
261
262 tx.inputs.into_iter().filter_map(|input| {
263 if input.module_instance_id() != LEGACY_HARDCODED_INSTANCE_ID_WALLET {
264 return None;
265 }
266
267 Some(
268 match input
269 .as_any()
270 .downcast_ref::<WalletInput>()
271 .expect("Instance id mapping incorrect")
272 {
273 WalletInput::V0(input) => input.tweak_contract_key().serialize(),
274 WalletInput::V1(input) => input.tweak_contract_key.serialize(),
275 WalletInput::Default { .. } => {
276 panic!("recoverytool only supports v0 wallet inputs")
277 }
278 },
279 )
280 })
281 })
282 .collect::<BTreeSet<_>>();
283
284 (tweaks, peg_out_count)
285}
286
287fn tweak_descriptor(
288 base_descriptor: &PegInDescriptor,
289 base_sk: &SecretKey,
290 tweak: &[u8; 33],
291 network: Network,
292) -> Descriptor<Key> {
293 let secret_key = base_sk.tweak(tweak, SECP256K1);
294 let pub_key = CompressedPublicKey::new(PublicKey::from_secret_key_global(&secret_key));
295 base_descriptor
296 .tweak(tweak, SECP256K1)
297 .translate_pk(&mut SecretKeyInjector {
298 secret: bitcoin::key::PrivateKey {
299 compressed: true,
300 network: network.into(),
301 inner: secret_key,
302 },
303 public: pub_key,
304 })
305 .expect("can't fail")
306}
307
308#[derive(Debug, Serialize)]
310struct ImportableWallet {
311 outpoint: OutPoint,
312 descriptor: Descriptor<Key>,
313 #[serde(with = "bitcoin::amount::serde::as_sat")]
314 amount_sat: bitcoin::Amount,
315}
316
317#[derive(Debug, Serialize)]
319struct ImportableWalletMin {
320 descriptor: Descriptor<Key>,
321}
322
323#[derive(Debug)]
326struct SecretKeyInjector {
327 secret: bitcoin::key::PrivateKey,
328 public: CompressedPublicKey,
329}
330
331impl Translator<CompressedPublicKey, Key, ()> for SecretKeyInjector {
332 fn pk(&mut self, pk: &CompressedPublicKey) -> Result<Key, ()> {
333 if &self.public == pk {
334 Ok(Key::Private(self.secret))
335 } else {
336 Ok(Key::Public(*pk))
337 }
338 }
339
340 fn sha256(
341 &mut self,
342 _sha256: &<CompressedPublicKey as MiniscriptKey>::Sha256,
343 ) -> Result<<Key as MiniscriptKey>::Sha256, ()> {
344 unimplemented!()
345 }
346
347 fn hash256(
348 &mut self,
349 _hash256: &<CompressedPublicKey as MiniscriptKey>::Hash256,
350 ) -> Result<<Key as MiniscriptKey>::Hash256, ()> {
351 unimplemented!()
352 }
353
354 fn ripemd160(
355 &mut self,
356 _ripemd160: &<CompressedPublicKey as MiniscriptKey>::Ripemd160,
357 ) -> Result<<Key as MiniscriptKey>::Ripemd160, ()> {
358 unimplemented!()
359 }
360
361 fn hash160(
362 &mut self,
363 _hash160: &<CompressedPublicKey as MiniscriptKey>::Hash160,
364 ) -> Result<<Key as MiniscriptKey>::Hash160, ()> {
365 unimplemented!()
366 }
367}
368
369#[test]
370fn parses_valid_length_tweaks() {
371 use hex::ToHex;
372
373 let bad_length_tweak_hex = rand::random::<[u8; 32]>().encode_hex::<String>();
374 let good_length_tweak: [u8; 33] = core::array::from_fn(|_| rand::random::<u8>());
376 let good_length_tweak_hex = good_length_tweak.encode_hex::<String>();
377 assert_eq!(
378 tweak_parser(good_length_tweak_hex.as_str()).expect("should parse valid length hex"),
379 good_length_tweak
380 );
381 assert!(tweak_parser(bad_length_tweak_hex.as_str()).is_err());
382}