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