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
107async fn get_db(path: &Path, module_decoders: ModuleDecoderRegistry) -> Database {
108 Database::new(
109 RocksDbReadOnly::open_read_only(path)
110 .await
111 .expect("Error opening readonly DB"),
112 module_decoders,
113 )
114}
115
116#[tokio::main]
117async fn main() -> anyhow::Result<()> {
118 TracingSetup::default().init()?;
119
120 handle_version_hash_command(fedimint_build_code_version_env!());
121
122 let opts: RecoveryTool = RecoveryTool::parse();
123
124 let (base_descriptor, base_key, network) = if let Some(config) = opts.config {
125 let cfg = read_server_config(&opts.password, &config).expect("Could not read config file");
126 let wallet_cfg: WalletConfig = cfg
127 .get_module_config_typed(LEGACY_HARDCODED_INSTANCE_ID_WALLET)
128 .expect("Malformed wallet config");
129 let base_descriptor = wallet_cfg.consensus.peg_in_descriptor;
130 let base_key = wallet_cfg.private.peg_in_key;
131 let network = wallet_cfg.consensus.network.0;
132
133 (base_descriptor, base_key, network)
134 } else if let (Some(descriptor), Some(key)) = (opts.descriptor, opts.key) {
135 (descriptor, key, opts.network)
136 } else {
137 panic!("Either config or descriptor need to be provided by clap");
138 };
139
140 process_and_print_tweak_source(&opts.strategy, &base_descriptor, &base_key, network).await;
141
142 Ok(())
143}
144
145async fn process_and_print_tweak_source(
146 tweak_source: &TweakSource,
147 base_descriptor: &Descriptor<CompressedPublicKey>,
148 base_key: &SecretKey,
149 network: Network,
150) {
151 match tweak_source {
152 TweakSource::Direct { tweak } => {
153 let descriptor = tweak_descriptor(base_descriptor, base_key, tweak, network);
154 let wallets = vec![ImportableWalletMin { descriptor }];
155
156 serde_json::to_writer(std::io::stdout().lock(), &wallets)
157 .expect("Could not encode to stdout");
158 }
159 TweakSource::Utxos { legacy, db } => {
160 let db = get_db(db, ModuleRegistry::default()).await;
161
162 let db = if *legacy {
163 db
164 } else {
165 db.with_prefix_module_id(LEGACY_HARDCODED_INSTANCE_ID_WALLET)
166 .0
167 };
168
169 let utxos: Vec<ImportableWallet> = db
170 .begin_transaction_nc()
171 .await
172 .find_by_prefix(&UTXOPrefixKey)
173 .await
174 .map(|(UTXOKey(outpoint), SpendableUTXO { tweak, amount })| {
175 let descriptor = tweak_descriptor(base_descriptor, base_key, &tweak, network);
176
177 ImportableWallet {
178 outpoint,
179 descriptor,
180 amount_sat: amount,
181 }
182 })
183 .collect()
184 .await;
185
186 serde_json::to_writer(std::io::stdout().lock(), &utxos)
187 .expect("Could not encode to stdout");
188 }
189 TweakSource::Epochs { db } => {
190 let decoders = ModuleDecoderRegistry::from_iter([(
192 LEGACY_HARDCODED_INSTANCE_ID_WALLET,
193 WalletCommonInit::KIND,
194 <Wallet as ServerModule>::decoder(),
195 )])
196 .with_fallback();
197
198 let db = get_db(db, decoders).await;
199 let mut dbtx = db.begin_transaction_nc().await;
200
201 let mut change_tweak_idx: u64 = 0;
202
203 let tweaks = dbtx
204 .find_by_prefix(&SignedSessionOutcomePrefix)
205 .await
206 .flat_map(
207 |(
208 _key,
209 SignedSessionOutcome {
210 session_outcome: block,
211 ..
212 },
213 )| {
214 let transaction_cis: Vec<Transaction> = block
215 .items
216 .into_iter()
217 .filter_map(|item| match item.item {
218 ConsensusItem::Transaction(tx) => Some(tx),
219 ConsensusItem::Module(_) | ConsensusItem::Default { .. } => None,
220 })
221 .collect();
222
223 let (mut peg_in_tweaks, peg_out_count) =
226 input_tweaks_and_peg_out_count(transaction_cis.into_iter());
227
228 for _ in 0..peg_out_count {
229 info!("Found change output, adding tweak {change_tweak_idx} to list");
230 peg_in_tweaks.insert(nonce_from_idx(change_tweak_idx));
231 change_tweak_idx += 1;
232 }
233
234 futures::stream::iter(peg_in_tweaks.into_iter())
235 },
236 );
237
238 let wallets = tweaks
239 .map(|tweak| {
240 let descriptor = tweak_descriptor(base_descriptor, base_key, &tweak, network);
241 ImportableWalletMin { descriptor }
242 })
243 .collect::<Vec<_>>()
244 .await;
245
246 serde_json::to_writer(std::io::stdout().lock(), &wallets)
247 .expect("Could not encode to stdout");
248 }
249 }
250}
251
252fn input_tweaks_and_peg_out_count(
253 transactions: impl Iterator<Item = Transaction>,
254) -> (BTreeSet<[u8; 33]>, u64) {
255 let mut peg_out_count = 0;
256 let tweaks = transactions
257 .flat_map(|tx| {
258 tx.outputs.iter().for_each(|output| {
259 if output.module_instance_id() == LEGACY_HARDCODED_INSTANCE_ID_WALLET {
260 peg_out_count += 1;
261 }
262 });
263
264 tx.inputs.into_iter().filter_map(|input| {
265 if input.module_instance_id() != LEGACY_HARDCODED_INSTANCE_ID_WALLET {
266 return None;
267 }
268
269 Some(
270 match input
271 .as_any()
272 .downcast_ref::<WalletInput>()
273 .expect("Instance id mapping incorrect")
274 {
275 WalletInput::V0(input) => input.tweak_contract_key().serialize(),
276 WalletInput::V1(input) => input.tweak_contract_key.serialize(),
277 WalletInput::Default { .. } => {
278 panic!("recoverytool only supports v0 wallet inputs")
279 }
280 },
281 )
282 })
283 })
284 .collect::<BTreeSet<_>>();
285
286 (tweaks, peg_out_count)
287}
288
289fn tweak_descriptor(
290 base_descriptor: &PegInDescriptor,
291 base_sk: &SecretKey,
292 tweak: &[u8; 33],
293 network: Network,
294) -> Descriptor<Key> {
295 let secret_key = base_sk.tweak(tweak, SECP256K1);
296 let pub_key = CompressedPublicKey::new(PublicKey::from_secret_key_global(&secret_key));
297 base_descriptor
298 .tweak(tweak, SECP256K1)
299 .translate_pk(&mut SecretKeyInjector {
300 secret: bitcoin::key::PrivateKey {
301 compressed: true,
302 network: network.into(),
303 inner: secret_key,
304 },
305 public: pub_key,
306 })
307 .expect("can't fail")
308}
309
310#[derive(Debug, Serialize)]
312struct ImportableWallet {
313 outpoint: OutPoint,
314 descriptor: Descriptor<Key>,
315 #[serde(with = "bitcoin::amount::serde::as_sat")]
316 amount_sat: bitcoin::Amount,
317}
318
319#[derive(Debug, Serialize)]
321struct ImportableWalletMin {
322 descriptor: Descriptor<Key>,
323}
324
325#[derive(Debug)]
328struct SecretKeyInjector {
329 secret: bitcoin::key::PrivateKey,
330 public: CompressedPublicKey,
331}
332
333impl Translator<CompressedPublicKey, Key, ()> for SecretKeyInjector {
334 fn pk(&mut self, pk: &CompressedPublicKey) -> Result<Key, ()> {
335 if &self.public == pk {
336 Ok(Key::Private(self.secret))
337 } else {
338 Ok(Key::Public(*pk))
339 }
340 }
341
342 fn sha256(
343 &mut self,
344 _sha256: &<CompressedPublicKey as MiniscriptKey>::Sha256,
345 ) -> Result<<Key as MiniscriptKey>::Sha256, ()> {
346 unimplemented!()
347 }
348
349 fn hash256(
350 &mut self,
351 _hash256: &<CompressedPublicKey as MiniscriptKey>::Hash256,
352 ) -> Result<<Key as MiniscriptKey>::Hash256, ()> {
353 unimplemented!()
354 }
355
356 fn ripemd160(
357 &mut self,
358 _ripemd160: &<CompressedPublicKey as MiniscriptKey>::Ripemd160,
359 ) -> Result<<Key as MiniscriptKey>::Ripemd160, ()> {
360 unimplemented!()
361 }
362
363 fn hash160(
364 &mut self,
365 _hash160: &<CompressedPublicKey as MiniscriptKey>::Hash160,
366 ) -> Result<<Key as MiniscriptKey>::Hash160, ()> {
367 unimplemented!()
368 }
369}
370
371#[test]
372fn parses_valid_length_tweaks() {
373 use hex::ToHex;
374
375 let bad_length_tweak_hex = rand::random::<[u8; 32]>().encode_hex::<String>();
376 let good_length_tweak: [u8; 33] = core::array::from_fn(|_| rand::random::<u8>());
378 let good_length_tweak_hex = good_length_tweak.encode_hex::<String>();
379 assert_eq!(
380 tweak_parser(good_length_tweak_hex.as_str()).expect("should parse valid length hex"),
381 good_length_tweak
382 );
383 assert!(tweak_parser(bad_length_tweak_hex.as_str()).is_err());
384}