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 }