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