You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
chantools/cmd/chantools/walletinfo.go

237 lines
5.8 KiB
Go

package main
import (
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/walletdb"
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/keychain"
"github.com/spf13/cobra"
)
const (
walletInfoFormat = `
Identity Pubkey: %x
BIP32 HD extended root key: %s
Wallet scopes:
%s
`
keyScopeformat = `
Scope: m/%d'/%d'
Number of internal %s addresses: %d
Number of external %s addresses: %d
`
)
var (
defaultAccount = uint32(waddrmgr.DefaultAccountNum)
)
type walletInfoCommand struct {
WalletDB string
WithRootKey bool
DumpAddrs bool
cmd *cobra.Command
}
func newWalletInfoCommand() *cobra.Command {
cc := &walletInfoCommand{}
cc.cmd = &cobra.Command{
Use: "walletinfo",
Short: "Shows info about an lnd wallet.db file and optionally " +
"extracts the BIP32 HD root key",
Long: `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.
In case lnd was started with "--noseedbackup=true" your wallet has the default
password. To unlock the wallet set the environment variable WALLET_PASSWORD="-"
or simply press <enter> without entering a password when being prompted.`,
Example: `chantools walletinfo --withrootkey \
--walletdb ~/.lnd/data/chain/bitcoin/mainnet/wallet.db`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.WalletDB, "walletdb", "", "lnd wallet.db file to dump the "+
"contents from",
)
cc.cmd.Flags().BoolVar(
&cc.WithRootKey, "withrootkey", false, "print BIP32 HD root "+
"key of wallet to standard out",
)
cc.cmd.Flags().BoolVar(
&cc.DumpAddrs, "dumpaddrs", false, "print all addresses, "+
"including private keys",
)
return cc.cmd
}
func (c *walletInfoCommand) Execute(_ *cobra.Command, _ []string) error {
// Check that we have a wallet DB.
if c.WalletDB == "" {
return fmt.Errorf("wallet DB is required")
}
w, privateWalletPw, cleanup, err := lnd.OpenWallet(
c.WalletDB, chainParams,
)
if err != nil {
return fmt.Errorf("error opening wallet file '%s': %w",
c.WalletDB, err)
}
defer func() {
if err := cleanup(); err != nil {
log.Errorf("error closing wallet: %v", err)
}
}()
// Print the wallet info and if requested the root key.
identityKey, scopeInfo, err := walletInfo(w, c.DumpAddrs)
if err != nil {
return err
}
rootKey := na
if c.WithRootKey {
masterHDPrivKey, err := lnd.DecryptWalletRootKey(
w.Database(), privateWalletPw,
)
if err != nil {
return err
}
rootKey = string(masterHDPrivKey)
}
result := fmt.Sprintf(
walletInfoFormat, identityKey.SerializeCompressed(), rootKey,
scopeInfo,
)
fmt.Println(result)
// For the tests, also log as trace level which is disabled by default.
log.Tracef(result)
return nil
}
func walletInfo(w *wallet.Wallet, dumpAddrs bool) (*btcec.PublicKey, string,
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 nil, "", fmt.Errorf("unable to open key ring for coin "+
"type %d: %v", chainParams.HDCoinType, err)
}
// Collect information about the different addresses in use.
scopeNp2wkh, err := printScopeInfo(
"np2wkh", w, w.Manager.ScopesForExternalAddrType(
waddrmgr.NestedWitnessPubKey,
),
)
if err != nil {
return nil, "", err
}
scopeP2wkh, err := printScopeInfo(
"p2wkh", w, w.Manager.ScopesForExternalAddrType(
waddrmgr.WitnessPubKey,
),
)
if err != nil {
return nil, "", err
}
scopeAddrs := "\n"
if dumpAddrs {
printAddr := func(a waddrmgr.ManagedAddress) error {
pka, ok := a.(waddrmgr.ManagedPubKeyAddress)
if !ok {
return fmt.Errorf("key is not a managed pubkey")
}
privKey, err := pka.PrivKey()
if err != nil {
return fmt.Errorf("error deriving priv key: %w",
err)
}
scope, path, _ := pka.DerivationInfo()
scopeAddrs += fmt.Sprintf(
"path=m/%d'/%d'/%d'/%d/%d, pubkey=%x, "+
"addr=%s, hash160=%x, priv=%x\n",
scope.Purpose, scope.Coin, path.InternalAccount,
path.Branch, path.Index,
pka.PubKey().SerializeCompressed(),
pka.Address().String(), a.AddrHash(),
privKey.Serialize(),
)
return nil
}
for _, mgr := range w.Manager.ActiveScopedKeyManagers() {
var addrs []waddrmgr.ManagedAddress
err = walletdb.View(
w.Database(), func(tx walletdb.ReadTx) error {
waddrmgrNs := tx.ReadBucket(
lnd.WaddrmgrNamespaceKey,
)
return mgr.ForEachAccountAddress(
waddrmgrNs, 0,
func(a waddrmgr.ManagedAddress) error {
addrs = append(addrs, a)
return nil
},
)
},
)
if err != nil {
return nil, "", err
}
for _, addr := range addrs {
if err := printAddr(addr); err != nil {
return nil, "", err
}
}
}
}
return idPrivKey.PubKey(), scopeNp2wkh + scopeP2wkh + scopeAddrs, nil
}
func printScopeInfo(name string, w *wallet.Wallet,
scopes []waddrmgr.KeyScope) (string, error) {
scopeInfo := ""
for _, scope := range scopes {
props, err := w.AccountProperties(scope, defaultAccount)
if err != nil {
return "", fmt.Errorf("error fetching account "+
"properties: %w", err)
}
scopeInfo += fmt.Sprintf(
keyScopeformat, scope.Purpose, scope.Coin, name,
props.InternalKeyCount, name, props.ExternalKeyCount,
)
}
return scopeInfo, nil
}