/ recoverytool / src / main.rs
main.rs
  1  pub mod envs;
  2  
  3  use std::cmp::Ordering;
  4  use std::collections::BTreeSet;
  5  use std::fmt::{Display, Formatter};
  6  use std::hash::Hasher;
  7  use std::path::{Path, PathBuf};
  8  
  9  use anyhow::anyhow;
 10  use bitcoin::network::constants::Network;
 11  use bitcoin::OutPoint;
 12  use clap::{ArgGroup, Parser, Subcommand};
 13  use fedimint_core::core::{
 14      LEGACY_HARDCODED_INSTANCE_ID_LN, LEGACY_HARDCODED_INSTANCE_ID_MINT,
 15      LEGACY_HARDCODED_INSTANCE_ID_WALLET,
 16  };
 17  use fedimint_core::db::{Database, IDatabaseTransactionOpsCoreTyped};
 18  use fedimint_core::epoch::ConsensusItem;
 19  use fedimint_core::module::registry::ModuleDecoderRegistry;
 20  use fedimint_core::module::CommonModuleInit;
 21  use fedimint_core::session_outcome::SignedSessionOutcome;
 22  use fedimint_core::transaction::Transaction;
 23  use fedimint_core::ServerModule;
 24  use fedimint_ln_common::LightningCommonInit;
 25  use fedimint_ln_server::Lightning;
 26  use fedimint_logging::TracingSetup;
 27  use fedimint_mint_server::common::MintCommonInit;
 28  use fedimint_mint_server::Mint;
 29  use fedimint_rocksdb::{RocksDb, RocksDbReadOnly};
 30  use fedimint_server::config::io::read_server_config;
 31  use fedimint_server::consensus::db::SignedSessionOutcomePrefix;
 32  use fedimint_wallet_server::common::config::WalletConfig;
 33  use fedimint_wallet_server::common::keys::CompressedPublicKey;
 34  use fedimint_wallet_server::common::tweakable::Tweakable;
 35  use fedimint_wallet_server::common::{
 36      PegInDescriptor, SpendableUTXO, WalletCommonInit, WalletInput,
 37  };
 38  use fedimint_wallet_server::db::{UTXOKey, UTXOPrefixKey};
 39  use fedimint_wallet_server::{nonce_from_idx, Wallet};
 40  use futures::stream::StreamExt;
 41  use hex::FromHex;
 42  use miniscript::{Descriptor, MiniscriptKey, ToPublicKey, TranslatePk, Translator};
 43  use secp256k1::SecretKey;
 44  use serde::Serialize;
 45  use tracing::info;
 46  
 47  use crate::envs::FM_PASSWORD_ENV;
 48  
 49  /// Tool to recover the on-chain wallet of a Fedimint federation
 50  #[derive(Debug, Parser)]
 51  #[command(version)]
 52  #[command(group(
 53      ArgGroup::new("keysource")
 54          .required(true)
 55          .args(["config", "descriptor"]),
 56  ))]
 57  struct RecoveryTool {
 58      /// Directory containing server config files
 59      #[arg(long = "cfg")]
 60      config: Option<PathBuf>,
 61      /// The password that encrypts the configs
 62      #[arg(long, env = FM_PASSWORD_ENV, requires = "config")]
 63      password: String,
 64      /// Wallet descriptor, can be used instead of --cfg
 65      #[arg(long)]
 66      descriptor: Option<PegInDescriptor>,
 67      /// Wallet secret key, can be used instead of config together with
 68      /// --descriptor
 69      #[arg(long, requires = "descriptor")]
 70      key: Option<SecretKey>,
 71      /// Network to operate on, has to be specified if --cfg isn't present
 72      #[arg(long, default_value = "bitcoin", requires = "descriptor")]
 73      network: Network,
 74      /// Open the database in read-only mode, useful for debugging, should not be
 75      /// used in production
 76      #[arg(long)]
 77      readonly: bool,
 78      #[command(subcommand)]
 79      strategy: TweakSource,
 80  }
 81  
 82  #[derive(Debug, Clone, Subcommand)]
 83  enum TweakSource {
 84      /// Derive the wallet descriptor using a single tweak
 85      Direct {
 86          #[arg(long, value_parser = tweak_parser)]
 87          tweak: [u8; 33],
 88      },
 89      /// Derive all wallet descriptors of confirmed UTXOs in the on-chain wallet.
 90      /// Note that unconfirmed change UTXOs will not appear here.
 91      Utxos {
 92          /// Extract UTXOs from a database without module partitioning
 93          #[arg(long)]
 94          legacy: bool,
 95          /// Path to database
 96          #[arg(long)]
 97          db: PathBuf,
 98      },
 99      /// Derive all wallet descriptors of tweaks that were ever used according to
100      /// the epoch log. In a long-running and busy federation this list will
101      /// contain many empty descriptors.
102      Epochs {
103          /// Path to database
104          #[arg(long)]
105          db: PathBuf,
106      },
107  }
108  
109  fn tweak_parser(hex: &str) -> anyhow::Result<[u8; 33]> {
110      <Vec<u8> as FromHex>::from_hex(hex)?
111          .try_into()
112          .map_err(|_| anyhow!("tweaks have to be 33 bytes long"))
113  }
114  
115  fn get_db(readonly: bool, path: &Path, module_decoders: ModuleDecoderRegistry) -> Database {
116      if readonly {
117          Database::new(
118              RocksDbReadOnly::open_read_only(path).expect("Error opening readonly DB"),
119              module_decoders,
120          )
121      } else {
122          Database::new(
123              RocksDb::open(path).expect("Error opening DB"),
124              module_decoders,
125          )
126      }
127  }
128  
129  #[tokio::main]
130  async fn main() -> anyhow::Result<()> {
131      TracingSetup::default().init()?;
132  
133      let opts: RecoveryTool = RecoveryTool::parse();
134  
135      let (base_descriptor, base_key, network) = if let Some(config) = opts.config {
136          let cfg = read_server_config(&opts.password, config).expect("Could not read config file");
137          let wallet_cfg: WalletConfig = cfg
138              .get_module_config_typed(LEGACY_HARDCODED_INSTANCE_ID_WALLET)
139              .expect("Malformed wallet config");
140          let base_descriptor = wallet_cfg.consensus.peg_in_descriptor;
141          let base_key = wallet_cfg.private.peg_in_key;
142          let network = wallet_cfg.consensus.network;
143  
144          (base_descriptor, base_key, network)
145      } else if let (Some(descriptor), Some(key)) = (opts.descriptor, opts.key) {
146          (descriptor, key, opts.network)
147      } else {
148          panic!("Either config or descriptor need to be provided by clap");
149      };
150  
151      match opts.strategy {
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(opts.readonly, &db, Default::default());
161  
162              let db = if legacy {
163                  db
164              } else {
165                  db.with_prefix_module_id(LEGACY_HARDCODED_INSTANCE_ID_WALLET)
166              };
167  
168              let utxos: Vec<ImportableWallet> = db
169                  .begin_transaction()
170                  .await
171                  .find_by_prefix(&UTXOPrefixKey)
172                  .await
173                  .map(|(UTXOKey(outpoint), SpendableUTXO { tweak, amount })| {
174                      let descriptor = tweak_descriptor(&base_descriptor, &base_key, &tweak, network);
175  
176                      ImportableWallet {
177                          outpoint,
178                          descriptor,
179                          amount_sat: amount,
180                      }
181                  })
182                  .collect()
183                  .await;
184  
185              serde_json::to_writer(std::io::stdout().lock(), &utxos)
186                  .expect("Could not encode to stdout")
187          }
188          TweakSource::Epochs { db } => {
189              let decoders = ModuleDecoderRegistry::from_iter([
190                  (
191                      LEGACY_HARDCODED_INSTANCE_ID_LN,
192                      LightningCommonInit::KIND,
193                      <Lightning as ServerModule>::decoder(),
194                  ),
195                  (
196                      LEGACY_HARDCODED_INSTANCE_ID_MINT,
197                      MintCommonInit::KIND,
198                      <Mint as ServerModule>::decoder(),
199                  ),
200                  (
201                      LEGACY_HARDCODED_INSTANCE_ID_WALLET,
202                      WalletCommonInit::KIND,
203                      <Wallet as ServerModule>::decoder(),
204                  ),
205              ]);
206  
207              let db = get_db(opts.readonly, &db, decoders);
208              let mut dbtx = db.begin_transaction().await;
209  
210              let mut change_tweak_idx: u64 = 0;
211  
212              let tweaks = dbtx
213                  .find_by_prefix(&SignedSessionOutcomePrefix)
214                  .await
215                  .flat_map(
216                      |(
217                          _key,
218                          SignedSessionOutcome {
219                              session_outcome: block,
220                              ..
221                          },
222                      )| {
223                          let transaction_cis: Vec<Transaction> = block
224                              .items
225                              .into_iter()
226                              .filter_map(|item| match item.item {
227                                  ConsensusItem::Transaction(tx) => Some(tx),
228                                  ConsensusItem::Module(_) => None,
229                                  ConsensusItem::Default { .. } => None,
230                              })
231                              .collect();
232  
233                          // Get all user-submitted tweaks and if we did a peg-out tx also return the
234                          // consensus round's tweak used for change
235                          let (mut peg_in_tweaks, peg_out_present) =
236                              input_tweaks_output_present(transaction_cis.into_iter());
237  
238                          if peg_out_present {
239                              info!("Found change output, adding tweak {change_tweak_idx} to list");
240                              peg_in_tweaks.insert(nonce_from_idx(change_tweak_idx));
241                              change_tweak_idx += 1;
242                          }
243  
244                          futures::stream::iter(peg_in_tweaks.into_iter())
245                      },
246                  );
247  
248              let wallets = tweaks
249                  .map(|tweak| {
250                      let descriptor = tweak_descriptor(&base_descriptor, &base_key, &tweak, network);
251                      ImportableWalletMin { descriptor }
252                  })
253                  .collect::<Vec<_>>()
254                  .await;
255  
256              serde_json::to_writer(std::io::stdout().lock(), &wallets)
257                  .expect("Could not encode to stdout")
258          }
259      }
260  
261      Ok(())
262  }
263  
264  fn input_tweaks_output_present(
265      transactions: impl Iterator<Item = Transaction>,
266  ) -> (BTreeSet<[u8; 33]>, bool) {
267      let mut contains_peg_out = false;
268      let tweaks =
269          transactions
270              .flat_map(|tx| {
271                  if tx.outputs.iter().any(|output| {
272                      output.module_instance_id() == LEGACY_HARDCODED_INSTANCE_ID_WALLET
273                  }) {
274                      contains_peg_out = true;
275                  }
276  
277                  tx.inputs.into_iter().filter_map(|input| {
278                      if input.module_instance_id() != LEGACY_HARDCODED_INSTANCE_ID_WALLET {
279                          return None;
280                      }
281  
282                      Some(
283                          input
284                              .as_any()
285                              .downcast_ref::<WalletInput>()
286                              .expect("Instance id mapping incorrect")
287                              .ensure_v0_ref()
288                              .expect("recoverytool only supports v0 wallet inputs")
289                              .0
290                              .tweak_contract_key()
291                              .serialize(),
292                      )
293                  })
294              })
295              .collect::<BTreeSet<_>>();
296  
297      (tweaks, contains_peg_out)
298  }
299  
300  fn tweak_descriptor(
301      base_descriptor: &PegInDescriptor,
302      base_sk: &SecretKey,
303      tweak: &[u8; 33],
304      network: Network,
305  ) -> Descriptor<Key> {
306      let secret_key = base_sk.tweak(tweak, secp256k1::SECP256K1);
307      let pub_key =
308          CompressedPublicKey::new(secp256k1::PublicKey::from_secret_key_global(&secret_key));
309      base_descriptor
310          .tweak(tweak, secp256k1::SECP256K1)
311          .translate_pk(&mut SecretKeyInjector {
312              secret: bitcoin::key::PrivateKey {
313                  compressed: true,
314                  network,
315                  inner: secret_key,
316              },
317              public: pub_key,
318          })
319          .expect("can't fail")
320  }
321  
322  /// A UTXO with its Bitcoin Core importable descriptor
323  #[derive(Debug, Serialize)]
324  struct ImportableWallet {
325      outpoint: OutPoint,
326      descriptor: Descriptor<Key>,
327      #[serde(with = "bitcoin::amount::serde::as_sat")]
328      amount_sat: bitcoin::Amount,
329  }
330  
331  /// A Bitcoin Core importable descriptor
332  #[derive(Debug, Serialize)]
333  struct ImportableWalletMin {
334      descriptor: Descriptor<Key>,
335  }
336  
337  /// `MiniscriptKey` that is either a WIF-encoded private key or a compressed,
338  /// hex-encoded public key
339  #[derive(Debug, Clone, Copy, Eq)]
340  enum Key {
341      Public(CompressedPublicKey),
342      Private(bitcoin::key::PrivateKey),
343  }
344  
345  impl PartialOrd for Key {
346      fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
347          Some(
348              self.to_compressed_public_key()
349                  .cmp(&other.to_compressed_public_key()),
350          )
351      }
352  }
353  
354  impl Ord for Key {
355      fn cmp(&self, other: &Self) -> Ordering {
356          self.to_compressed_public_key()
357              .cmp(&other.to_compressed_public_key())
358      }
359  }
360  
361  impl PartialEq for Key {
362      fn eq(&self, other: &Self) -> bool {
363          self.to_compressed_public_key()
364              .eq(&other.to_compressed_public_key())
365      }
366  }
367  
368  impl std::hash::Hash for Key {
369      fn hash<H: Hasher>(&self, state: &mut H) {
370          self.to_compressed_public_key().hash(state)
371      }
372  }
373  
374  impl Key {
375      fn to_compressed_public_key(self) -> CompressedPublicKey {
376          match self {
377              Key::Public(pk) => pk,
378              Key::Private(sk) => {
379                  CompressedPublicKey::new(secp256k1::PublicKey::from_secret_key_global(&sk.inner))
380              }
381          }
382      }
383  }
384  
385  impl Display for Key {
386      fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
387          match self {
388              Key::Public(pk) => Display::fmt(pk, f),
389              Key::Private(sk) => Display::fmt(sk, f),
390          }
391      }
392  }
393  
394  impl MiniscriptKey for Key {
395      fn is_uncompressed(&self) -> bool {
396          false
397      }
398  
399      fn num_der_paths(&self) -> usize {
400          0
401      }
402  
403      type Sha256 = bitcoin::hashes::sha256::Hash;
404      type Hash256 = miniscript::hash256::Hash;
405      type Ripemd160 = bitcoin::hashes::ripemd160::Hash;
406      type Hash160 = bitcoin::hashes::hash160::Hash;
407  }
408  
409  impl ToPublicKey for Key {
410      fn to_public_key(&self) -> miniscript::bitcoin::PublicKey {
411          self.to_compressed_public_key().to_public_key()
412      }
413  
414      fn to_sha256(
415          hash: &<Self as MiniscriptKey>::Sha256,
416      ) -> miniscript::bitcoin::hashes::sha256::Hash {
417          *hash
418      }
419  
420      fn to_hash256(hash: &<Self as MiniscriptKey>::Hash256) -> miniscript::hash256::Hash {
421          *hash
422      }
423  
424      fn to_ripemd160(
425          hash: &<Self as MiniscriptKey>::Ripemd160,
426      ) -> miniscript::bitcoin::hashes::ripemd160::Hash {
427          *hash
428      }
429  
430      fn to_hash160(
431          hash: &<Self as MiniscriptKey>::Hash160,
432      ) -> miniscript::bitcoin::hashes::hash160::Hash {
433          *hash
434      }
435  }
436  
437  /// Miniscript [`Translator`] that replaces a public key with a private key we
438  /// know.
439  #[derive(Debug)]
440  struct SecretKeyInjector {
441      secret: bitcoin::key::PrivateKey,
442      public: CompressedPublicKey,
443  }
444  
445  impl Translator<CompressedPublicKey, Key, ()> for SecretKeyInjector {
446      fn pk(&mut self, pk: &CompressedPublicKey) -> Result<Key, ()> {
447          if &self.public == pk {
448              Ok(Key::Private(self.secret))
449          } else {
450              Ok(Key::Public(*pk))
451          }
452      }
453  
454      fn sha256(
455          &mut self,
456          _sha256: &<CompressedPublicKey as MiniscriptKey>::Sha256,
457      ) -> Result<<Key as MiniscriptKey>::Sha256, ()> {
458          unimplemented!()
459      }
460  
461      fn hash256(
462          &mut self,
463          _hash256: &<CompressedPublicKey as MiniscriptKey>::Hash256,
464      ) -> Result<<Key as MiniscriptKey>::Hash256, ()> {
465          unimplemented!()
466      }
467  
468      fn ripemd160(
469          &mut self,
470          _ripemd160: &<CompressedPublicKey as MiniscriptKey>::Ripemd160,
471      ) -> Result<<Key as MiniscriptKey>::Ripemd160, ()> {
472          unimplemented!()
473      }
474  
475      fn hash160(
476          &mut self,
477          _hash160: &<CompressedPublicKey as MiniscriptKey>::Hash160,
478      ) -> Result<<Key as MiniscriptKey>::Hash160, ()> {
479          unimplemented!()
480      }
481  }
482  
483  #[test]
484  fn parses_valid_length_tweaks() {
485      use hex::ToHex;
486  
487      let bad_length_tweak_hex = rand::random::<[u8; 32]>().encode_hex::<String>();
488      // rand::random only supports random byte arrays up to 32 bytes
489      let good_length_tweak: [u8; 33] = core::array::from_fn(|_| rand::random::<u8>());
490      let good_length_tweak_hex = good_length_tweak.encode_hex::<String>();
491      assert_eq!(
492          tweak_parser(good_length_tweak_hex.as_str()).expect("should parse valid length hex"),
493          good_length_tweak
494      );
495      assert!(tweak_parser(bad_length_tweak_hex.as_str()).is_err());
496  }