From af742470b3ee7273d3e8f1d9f8814af5c7543d31 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sun, 2 Feb 2020 19:56:05 +0100 Subject: [PATCH] Add walletinfo command --- README.md | 46 +++++- cmd/chantools/genimportscript.go | 69 +++++++-- cmd/chantools/main.go | 36 ++++- cmd/chantools/walletinfo.go | 249 +++++++++++++++++++++++++++++++ go.mod | 2 + 5 files changed, 382 insertions(+), 20 deletions(-) create mode 100644 cmd/chantools/walletinfo.go diff --git a/README.md b/README.md index c354ce6..2d894cc 100644 --- a/README.md +++ b/README.md @@ -16,17 +16,19 @@ + [showrootkey](#showrootkey) + [summary](#summary) + [sweeptimelock](#sweeptimelock) + + [walletinfo](#walletinfo) This tool provides helper functions that can be used to rescue funds locked in -lnd channels in case lnd itself cannot run properly any more. +`lnd` channels in case `lnd` itself cannot run properly any more. **WARNING**: This tool was specifically built for a certain rescue operation and might not be well-suited for your use case. Or not all edge cases for your needs are coded properly. Please look at the code to understand what it does before you use it for anything serious. -**WARNING 2**: This tool will query public block explorer APIs, your privacy -might not be preserved. Use at your own risk. +**WARNING 2**: This tool will query public block explorer APIs for some of the +commands, your privacy might not be preserved. Use at your own risk or supply +a private API URL with `--apiurl`. ## Installation @@ -66,6 +68,7 @@ Available commands: showrootkey Extract and show the BIP32 HD root key from the 24 word lnd aezeed. summary Compile a summary about the current state of channels. sweeptimelock Sweep the force-closed state after the time lock has expired. + walletinfo Shows relevant information about an lnd wallet.db file and optionally extracts the BIP32 HD root key. ``` ## Commands @@ -145,7 +148,7 @@ Usage: --discard= A comma separated list of channel funding outpoints (format :) to remove from the backup file. ``` -Filter an lnd `channel.backup` file by removing certain channels (identified by +Filter an `lnd` `channel.backup` file by removing certain channels (identified by their funding transaction outpoints). Example command: @@ -167,7 +170,7 @@ Usage: --multi_file= The lnd channel.backup file to fix. ``` -Fixes an old channel.backup file that is affected by the lnd issue +Fixes an old channel.backup file that is affected by the `lnd` issue [#3881](https://github.com/lightningnetwork/lnd/issues/3881) ([lncli] unable to restore chan backups: rpc error: code = Unknown desc = unable to unpack chan backup: unable to derive shachain root key: unable to derive @@ -229,7 +232,7 @@ Usage: ``` Generates a script that contains all on-chain private (or public) keys derived -from an lnd 24 word aezeed wallet. That script can then be imported into other +from an `lnd` 24 word aezeed wallet. That script can then be imported into other software like bitcoind. The following script formats are currently supported: @@ -240,6 +243,8 @@ The following script formats are currently supported: `bitcoin-cli importpubkey` command. That means, only the public keys are imported into `bitcoind` to watch the UTXOs of those keys. The funds cannot be spent that way as they are watch-only. +* `bitcoin-importwallet`: Creates a text output that is compatible with + `bitcoind`'s `importwallet command. Example command: @@ -277,7 +282,7 @@ chantools --fromsummary results/summary-xxxx-yyyy.json \ ### showrootkey -This command converts the 24 word lnd aezeed phrase and password to the BIP32 +This command converts the 24 word `lnd` aezeed phrase and password to the BIP32 HD root key that is used as the `rootkey` parameter in other commands of this tool. @@ -338,3 +343,30 @@ chantools --fromsummary results/forceclose-xxxx-yyyy.json \ --publish \ --sweepaddr bc1q..... ``` + +### walletinfo + +```text +Usage: + chantools [OPTIONS] walletinfo [walletinfo-OPTIONS] + +[walletinfo command options] + --walletdb= The lnd wallet.db file to dump the contents from. + --withrootkey Should the BIP32 HD root key of the wallet be printed to standard out? +``` + +Shows some basic information about an `lnd` `wallet.db` file, like the node +identity the wallet belongs to, how many on-chain addresses are used and, if +enabled with `--withrootkey` the BIP32 HD root key of the wallet. The latter can +be useful to recover funds from a wallet if the wallet password is still known +but the seed was lost. **The 24 word seed phrase itself cannot be extracted** +because it is hashed into the extended HD root key before storing it in the +`wallet.db`. + +Example command: + +```bash +chantools walletinfo \ + --walletdb ~/.lnd/data/chain/bitcoin/mainnet/wallet.db \ + --withrootkey +``` diff --git a/cmd/chantools/genimportscript.go b/cmd/chantools/genimportscript.go index c67d581..bac993f 100644 --- a/cmd/chantools/genimportscript.go +++ b/cmd/chantools/genimportscript.go @@ -17,7 +17,7 @@ const ( type genImportScriptCommand struct { RootKey string `long:"rootkey" description:"BIP32 HD root key to use. Leave empty to prompt for lnd 24 word aezeed."` - Format string `long:"format" description:"The format of the generated import script. Currently supported are: bitcoin-cli, bitcoin-cli-watchonly."` + Format string `long:"format" description:"The format of the generated import script. Currently supported are: bitcoin-cli, bitcoin-cli-watchonly, bitcoin-importwallet."` RecoveryWindow uint32 `long:"recoverywindow" description:"The number of keys to scan per internal/external branch. The output will consist of double this amount of keys. (default 2500)"` RescanFrom uint32 `long:"rescanfrom" description:"The block number to rescan from. Will be set automatically from the wallet birthday if the lnd 24 word aezeed is entered. (default 500000)"` } @@ -55,13 +55,30 @@ func (c *genImportScriptCommand) Execute(_ []string) error { c.RescanFrom = defaultRescanFrom } + fmt.Printf("# Wallet dump created by chantools on %s\n", + time.Now().UTC()) + // Determine the format. - printFn := printBitcoinCli - if c.Format == "bitcoin-cli-watchonly" { + var printFn func(*hdkeychain.ExtendedKey, uint32, uint32) error + switch c.Format { + default: + fallthrough + + case "bitcoin-cli": + printFn = printBitcoinCli + fmt.Println("# Paste the following lines into a command line " + + "window.") + + case "bitcoin-cli-watchonly": printFn = printBitcoinCliWatchOnly - } + fmt.Println("# Paste the following lines into a command line " + + "window.") - fmt.Println("# Paste the following lines into a command line window.") + case "bitcoin-importwallet": + printFn = printBitcoinImportWallet + fmt.Println("# Save this output to a file and use the " + + "importwallet command of bitcoin core.") + } // External branch first (m/84'/'/0'/0/x). for i := uint32(0); i < c.RecoveryWindow; i++ { @@ -103,10 +120,10 @@ func (c *genImportScriptCommand) Execute(_ []string) error { return nil } -func printBitcoinCli(derivedKey *hdkeychain.ExtendedKey, branch, +func printBitcoinCli(hdKey *hdkeychain.ExtendedKey, branch, index uint32) error { - privKey, err := derivedKey.ECPrivKey() + privKey, err := hdKey.ECPrivKey() if err != nil { return fmt.Errorf("could not derive private key: %v", err) @@ -115,28 +132,58 @@ func printBitcoinCli(derivedKey *hdkeychain.ExtendedKey, branch, if err != nil { return fmt.Errorf("could not encode WIF: %v", err) } - fmt.Printf("bitcoin-cli importprivkey %s \"m/84'/%d'/0'/%d/%d/"+ "\" false\n", wif.String(), chainParams.HDCoinType, branch, index) return nil } -func printBitcoinCliWatchOnly(derivedKey *hdkeychain.ExtendedKey, branch, +func printBitcoinCliWatchOnly(hdKey *hdkeychain.ExtendedKey, branch, index uint32) error { - pubKey, err := derivedKey.ECPubKey() + pubKey, err := hdKey.ECPubKey() if err != nil { return fmt.Errorf("could not derive private key: %v", err) } - fmt.Printf("bitcoin-cli importpubkey %x \"m/84'/%d'/0'/%d/%d/"+ "\" false\n", pubKey.SerializeCompressed(), chainParams.HDCoinType, branch, index) return nil } +func printBitcoinImportWallet(hdKey *hdkeychain.ExtendedKey, branch, + index uint32) error { + + privKey, err := hdKey.ECPrivKey() + if err != nil { + return fmt.Errorf("could not derive private key: %v", + err) + } + wif, err := btcutil.NewWIF(privKey, chainParams, true) + if err != nil { + return fmt.Errorf("could not encode WIF: %v", err) + } + pubKey, err := hdKey.ECPubKey() + if err != nil { + return fmt.Errorf("could not derive private key: %v", + err) + } + addrPubkey, err := btcutil.NewAddressPubKey( + pubKey.SerializeCompressed(), chainParams, + ) + if err != nil { + return fmt.Errorf("could not create address: %v", err) + } + addr := addrPubkey.AddressPubKeyHash() + + fmt.Printf("%s 1970-01-01T00:00:01Z label=m/84'/%d'/0'/%d/%d/ "+ + "# addr=%s", wif.String(), chainParams.HDCoinType, branch, + index, addr.EncodeAddress(), + ) + return nil +} + func seedBirthdayToBlock(birthdayTimestamp time.Time) uint32 { var genesisTimestamp time.Time switch chainParams.Name { diff --git a/cmd/chantools/main.go b/cmd/chantools/main.go index 2b932dd..ef6607a 100644 --- a/cmd/chantools/main.go +++ b/cmd/chantools/main.go @@ -102,12 +102,19 @@ func runCommandParser() error { _, _ = parser.AddCommand( "fixoldbackup", "Fixes an old channel.backup file that is "+ "affected by the lnd issue #3881 (unable to derive "+ - "shachain root key).", "", &fixOldBackupCommand{}) + "shachain root key).", "", &fixOldBackupCommand{}, + ) _, _ = parser.AddCommand( "genimportscript", "Generate a script containing the on-chain "+ "keys of an lnd wallet that can be imported into "+ "other software like bitcoind.", "", - &genImportScriptCommand{}) + &genImportScriptCommand{}, + ) + _, _ = parser.AddCommand( + "walletinfo", "Shows relevant information about an lnd "+ + "wallet.db file and optionally extracts the BIP32 HD "+ + "root key.", "", &walletInfoCommand{}, + ) _, err := parser.Parse() return err @@ -216,6 +223,27 @@ func rootKeyFromConsole() (*hdkeychain.ExtendedKey, time.Time, error) { return rootKey, cipherSeed.BirthdayTime(), nil } +func passwordFromConsole(userQuery string) ([]byte, error) { + // Read from terminal (if there is one). + if terminal.IsTerminal(syscall.Stdin) { + fmt.Print(userQuery) + pw, err := terminal.ReadPassword(syscall.Stdin) + if err != nil { + return nil, err + } + fmt.Println() + return pw, nil + } + + // Read from stdin as a fallback. + reader := bufio.NewReader(os.Stdin) + pw, err := reader.ReadBytes('\n') + if err != nil { + return nil, err + } + return pw, nil +} + func setupChainParams(cfg *config) { switch { case cfg.Testnet: @@ -240,3 +268,7 @@ func setupLogging() { panic(err) } } + +func noConsole() ([]byte, error) { + return nil, fmt.Errorf("wallet db requires console access") +} diff --git a/cmd/chantools/walletinfo.go b/cmd/chantools/walletinfo.go new file mode 100644 index 0000000..2b3394c --- /dev/null +++ b/cmd/chantools/walletinfo.go @@ -0,0 +1,249 @@ +package main + +import ( + "encoding/hex" + "fmt" + "os" + "os/user" + "path/filepath" + "strings" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcwallet/snacl" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/walletdb" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnwallet" + + // This is required to register bdb as a valid walletdb driver. In the + // init function of the package, it registers itself. The import is used + // to activate the side effects w/o actually binding the package name to + // a file-level variable. + _ "github.com/btcsuite/btcwallet/walletdb/bdb" +) + +var ( + // Namespace from github.com/btcsuite/btcwallet/wallet/wallet.go + waddrmgrNamespaceKey = []byte("waddrmgr") + + // Bucket names from github.com/btcsuite/btcwallet/waddrmgr/db.go + mainBucketName = []byte("main") + masterPrivKeyName = []byte("mpriv") + cryptoPrivKeyName = []byte("cpriv") + masterHDPrivName = []byte("mhdpriv") + defaultAccount = uint32(waddrmgr.DefaultAccountNum) + openCallbacks = &waddrmgr.OpenCallbacks{ + ObtainSeed: noConsole, + ObtainPrivatePass: noConsole, + } +) + +type walletInfoCommand struct { + WalletDB string `long:"walletdb" description:"The lnd wallet.db file to dump the contents from."` + WithRootKey bool `long:"withrootkey" description:"Should the BIP32 HD root key of the wallet be printed to standard out?"` +} + +func (c *walletInfoCommand) Execute(_ []string) error { + var ( + publicWalletPw = lnwallet.DefaultPublicPassphrase + privateWalletPw = lnwallet.DefaultPrivatePassphrase + ) + + // Check that we have a wallet DB. + if c.WalletDB == "" { + return fmt.Errorf("wallet DB is required") + } + + // Ask the user for the wallet password. If it's empty, the default + // password will be used, since the lnd wallet is always encrypted. + pw, err := passwordFromConsole("Input wallet password: ") + if err != nil { + return err + } + if len(pw) > 0 { + publicWalletPw = pw + privateWalletPw = pw + } + + // Try to load and open the wallet. + db, err := walletdb.Open("bdb", cleanAndExpandPath(c.WalletDB), false) + if err != nil { + return fmt.Errorf("error opening wallet database: %v", err) + } + defer closeWalletDb(db) + w, err := wallet.Open(db, publicWalletPw, openCallbacks, chainParams, 0) + if err != nil { + return err + } + + // Start and unlock the wallet. + w.Start() + defer w.Stop() + err = w.Unlock(privateWalletPw, nil) + if err != nil { + return err + } + + // Print the wallet info and if requested the root key. + err = walletInfo(w) + if err != nil { + return err + } + if c.WithRootKey { + masterHDPrivKey, err := decryptRootKey(db, privateWalletPw) + if err != nil { + return err + } + fmt.Printf("BIP32 HD extended root key: %s\n", masterHDPrivKey) + } + return nil +} + +func walletInfo(w *wallet.Wallet) error { + keyRing := keychain.NewBtcWalletKeyRing(w, chainParams.HDCoinType) + idPrivKey, err := keyRing.DerivePrivKey(keychain.KeyDescriptor{ + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamilyNodeKey, + Index: 0, + }, + }) + if err != nil { + return err + } + idPrivKey.Curve = btcec.S256() + fmt.Printf( + "Identity Pubkey: %s\n", + hex.EncodeToString(idPrivKey.PubKey().SerializeCompressed()), + ) + + // Print information about the different addresses in use. + printScopeInfo( + "np2wkh", w, + w.Manager.ScopesForExternalAddrType( + waddrmgr.NestedWitnessPubKey, + ), + ) + printScopeInfo( + "p2wkh", w, + w.Manager.ScopesForExternalAddrType( + waddrmgr.WitnessPubKey, + ), + ) + return nil +} + +func printScopeInfo(name string, w *wallet.Wallet, scopes []waddrmgr.KeyScope) { + for _, scope := range scopes { + props, err := w.AccountProperties(scope, defaultAccount) + if err != nil { + fmt.Printf("Error fetching account properties: %v", err) + } + fmt.Printf("Scope: %s\n", scope.String()) + fmt.Printf( + " Number of internal (change) %s addresses: %d\n", + name, props.InternalKeyCount, + ) + fmt.Printf( + " Number of external %s addresses: %d\n", name, + props.ExternalKeyCount, + ) + } +} + +func decryptRootKey(db walletdb.DB, privPassphrase []byte) ([]byte, error) { + // Step 1: Load the encryption parameters and encrypted keys from the + // database. + var masterKeyPrivParams []byte + var cryptoKeyPrivEnc []byte + var masterHDPrivEnc []byte + err := walletdb.View(db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + if ns == nil { + return fmt.Errorf( + "namespace '%s' does not exist", + waddrmgrNamespaceKey, + ) + } + + mainBucket := ns.NestedReadBucket(mainBucketName) + if mainBucket == nil { + return fmt.Errorf( + "bucket '%s' does not exist", + mainBucketName, + ) + } + + val := mainBucket.Get(masterPrivKeyName) + if val != nil { + masterKeyPrivParams = make([]byte, len(val)) + copy(masterKeyPrivParams, val) + } + val = mainBucket.Get(cryptoPrivKeyName) + if val != nil { + cryptoKeyPrivEnc = make([]byte, len(val)) + copy(cryptoKeyPrivEnc, val) + } + val = mainBucket.Get(masterHDPrivName) + if val != nil { + masterHDPrivEnc = make([]byte, len(val)) + copy(masterHDPrivEnc, val) + } + return nil + }) + if err != nil { + return nil, err + } + + // Step 2: Unmarshal the master private key parameters and derive + // key from passphrase. + var masterKeyPriv snacl.SecretKey + if err := masterKeyPriv.Unmarshal(masterKeyPrivParams); err != nil { + return nil, err + } + if err := masterKeyPriv.DeriveKey(&privPassphrase); err != nil { + return nil, err + } + + // Step 3: Decrypt the keys in the correct order. + cryptoKeyPriv := &snacl.CryptoKey{} + cryptoKeyPrivBytes, err := masterKeyPriv.Decrypt(cryptoKeyPrivEnc) + if err != nil { + return nil, err + } + copy(cryptoKeyPriv[:], cryptoKeyPrivBytes) + return cryptoKeyPriv.Decrypt(masterHDPrivEnc) +} + +func closeWalletDb(db walletdb.DB) { + err := db.Close() + if err != nil { + fmt.Printf("Error closing database: %v", err) + } +} + +// cleanAndExpandPath expands environment variables and leading ~ in the +// passed path, cleans the result, and returns it. +// This function is taken from https://github.com/btcsuite/btcd +func cleanAndExpandPath(path string) string { + if path == "" { + return "" + } + + // Expand initial ~ to OS specific home directory. + if strings.HasPrefix(path, "~") { + var homeDir string + u, err := user.Current() + if err == nil { + homeDir = u.HomeDir + } else { + homeDir = os.Getenv("HOME") + } + + path = strings.Replace(path, "~", homeDir, 1) + } + + // NOTE: The os.ExpandEnv doesn't work with Windows-style %VARIABLE%, + // but the variables can still be expanded via POSIX-style $VARIABLE. + return filepath.Clean(os.ExpandEnv(path)) +} diff --git a/go.mod b/go.mod index 78a73ed..38e9cb6 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ require ( github.com/Yawning/aez v0.0.0-20180408160647-ec7426b44926 // indirect github.com/btcsuite/btcd v0.20.1-beta github.com/btcsuite/btcutil v0.0.0-20191219182022-e17c9730c422 + github.com/btcsuite/btcwallet v0.11.0 + github.com/btcsuite/btcwallet/walletdb v1.1.0 github.com/davecgh/go-spew v1.1.1 github.com/golang/protobuf v1.3.2 // indirect github.com/jessevdk/go-flags v1.4.0