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