Add walletinfo command

pull/3/head
Oliver Gugger 4 years ago
parent 305a9dab94
commit af742470b3
No known key found for this signature in database
GPG Key ID: 8E4256593F177720

@ -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 <fundingTXID>:<index>) 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) (<code>[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
```

@ -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'/<coinType>'/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 {

@ -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")
}

@ -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))
}

@ -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

Loading…
Cancel
Save