Add dumpbackup command

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

@ -11,6 +11,15 @@ 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.
## Installation
To install this tool, make sure you have `go 1.13.x` (or later) and `make`
installed and run the following command:
```bash
make install
```
## Overview
```text
@ -18,6 +27,7 @@ Usage:
chantools [OPTIONS] <command>
Application Options:
--testnet Set to true if testnet parameters should be used.
--apiurl= API URL to use (must be esplora compatible). (default: https://blockstream.info/api)
--listchannels= The channel input is in the format of lncli's listchannels format. Specify '-' to read from stdin.
--pendingchannels= The channel input is in the format of lncli's pendingchannels format. Specify '-' to read from stdin.
@ -28,11 +38,13 @@ Help Options:
-h, --help Show this help message
Available commands:
dumpchannels Dump all channel information from lnd's channel database
forceclose Force-close the last state that is in the channel.db provided
rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels
summary Compile a summary about the current state of channels
sweeptimelock Sweep the force-closed state after the time lock has expired
dumpbackup Dump the content of a channel.backup file.
dumpchannels Dump all channel information from lnd's channel database.
forceclose Force-close the last state that is in the channel.db provided.
rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels.
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.
```
## summary command
@ -168,3 +180,36 @@ Example command:
```bash
chantools dumpchannels --channeldb ~/.lnd/data/graph/mainnet/channel.db
```
## showrootkey command
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.
Example command:
```bash
chantools showrootkey
```
## dumpbackup command
```text
Usage:
chantools [OPTIONS] dumpbackup [dumpbackup-OPTIONS]
[dumpbackup command options]
--rootkey= BIP32 HD root key of the wallet that was used to create the backup.
--multi_file= The lnd channel.backup file to dump.
```
This command dumps all information that is inside a `channel.backup` file in a
human readable format.
Example command:
```bash
chantools dumpbackup --rootkey xprvxxxxxxxxxx \
--multi_file ~/.lnd/data/chain/bitcoin/mainnet/channel.backup
```

@ -0,0 +1,58 @@
package chantools
import (
"net"
"github.com/btcsuite/btcutil"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/lnwire"
)
type dumpMulti struct {
Version chanbackup.MultiBackupVersion
StaticBackups []dumpSingle
}
// dumpSingle is the information we want to dump from an lnd channel backup.
// See `chanbackup.Single` for information about the fields.
type dumpSingle struct {
Version chanbackup.SingleBackupVersion
IsInitiator bool
ChainHash string
FundingOutpoint string
ShortChannelID lnwire.ShortChannelID
RemoteNodePub string
Addresses []net.Addr
Capacity btcutil.Amount
LocalChanCfg dumpChanCfg
RemoteChanCfg dumpChanCfg
ShaChainRootDesc dumpDescriptor
}
func dumpChannelBackup(multi *chanbackup.Multi) error {
dumpSingles := make([]dumpSingle, len(multi.StaticBackups))
for idx, single := range multi.StaticBackups {
dumpSingles[idx] = dumpSingle{
Version: single.Version,
IsInitiator: single.IsInitiator,
ChainHash: single.ChainHash.String(),
FundingOutpoint: single.FundingOutpoint.String(),
ShortChannelID: single.ShortChannelID,
RemoteNodePub: pubKeyToString(single.RemoteNodePub),
Addresses: single.Addresses,
Capacity: single.Capacity,
LocalChanCfg: toDumpChanCfg(single.LocalChanCfg),
RemoteChanCfg: toDumpChanCfg(single.RemoteChanCfg),
ShaChainRootDesc: toDumpDescriptor(
single.ShaChainRootDesc,
),
}
}
spew.Dump(dumpMulti{
Version: multi.Version,
StaticBackups: dumpSingles,
})
return nil
}

@ -3,22 +3,15 @@ package chantools
import (
"bytes"
"encoding/hex"
"fmt"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcutil"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwire"
)
const (
lndInternalDerivationPath = "m/1017'/0'/%d'/0/%d"
)
// dumpInfo is the information we want to dump from an open channel in lnd's
// channel DB. See `channeldb.OpenChannel` for information about the fields.
type dumpInfo struct {
@ -48,22 +41,6 @@ type dumpInfo struct {
RemoteShutdownScript lnwire.DeliveryAddress
}
// dumpChanCfg is the information we want to dump from a channel configuration.
// See `channeldb.ChannelConfig` for more information about the fields.
type dumpChanCfg struct {
channeldb.ChannelConstraints
MultiSigKey dumpDescriptor
RevocationBasePoint dumpDescriptor
PaymentBasePoint dumpDescriptor
DelayBasePoint dumpDescriptor
HtlcBasePoint dumpDescriptor
}
type dumpDescriptor struct {
Path string
Pubkey string
}
func dumpChannelInfo(chanDb *channeldb.DB) error {
channels, err := chanDb.FetchAllChannels()
if err != nil {
@ -124,27 +101,3 @@ func dumpChannelInfo(chanDb *channeldb.DB) error {
spew.Dump(dumpChannels)
return nil
}
func toDumpChanCfg(cfg channeldb.ChannelConfig) dumpChanCfg {
return dumpChanCfg{
ChannelConstraints: cfg.ChannelConstraints,
MultiSigKey: toDumpDescriptor(cfg.MultiSigKey),
RevocationBasePoint: toDumpDescriptor(cfg.RevocationBasePoint),
PaymentBasePoint: toDumpDescriptor(cfg.PaymentBasePoint),
DelayBasePoint: toDumpDescriptor(cfg.DelayBasePoint),
HtlcBasePoint: toDumpDescriptor(cfg.HtlcBasePoint),
}
}
func toDumpDescriptor(desc keychain.KeyDescriptor) dumpDescriptor {
return dumpDescriptor{
Path: fmt.Sprintf(
lndInternalDerivationPath, desc.Family, desc.Index,
),
Pubkey: pubKeyToString(desc.PubKey),
}
}
func pubKeyToString(pubkey *btcec.PublicKey) string {
return hex.EncodeToString(pubkey.SerializeCompressed())
}

@ -9,7 +9,6 @@ import (
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/lightningnetwork/lnd/channeldb"
@ -18,8 +17,6 @@ import (
)
var (
hardenedKeyStart = uint32(hdkeychain.HardenedKeyStart)
chainParams = &chaincfg.MainNetParams
cacheSize = 2000
cache []*cacheEntry
@ -184,22 +181,6 @@ func fillCache(extendedKey *hdkeychain.ExtendedKey) error {
return nil
}
func deriveChildren(key *hdkeychain.ExtendedKey, path []uint32) (
*hdkeychain.ExtendedKey, error) {
var (
currentKey = key
err error
)
for _, pathPart := range path {
currentKey, err = currentKey.Child(pathPart)
if err != nil {
return nil, err
}
}
return currentKey, nil
}
func parseAddr(addr string) ([]byte, error) {
// First parse address to get targetPubKeyHash from it later.
targetAddr, err := btcutil.DecodeAddress(addr, chainParams)

@ -0,0 +1,57 @@
package chantools
import (
"encoding/hex"
"fmt"
"github.com/btcsuite/btcd/btcec"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/keychain"
)
const (
lndInternalDerivationPath = "m/1017'/0'/%d'/0/%d"
)
// dumpChanCfg is the information we want to dump from a channel configuration.
// See `channeldb.ChannelConfig` for more information about the fields.
type dumpChanCfg struct {
channeldb.ChannelConstraints
MultiSigKey dumpDescriptor
RevocationBasePoint dumpDescriptor
PaymentBasePoint dumpDescriptor
DelayBasePoint dumpDescriptor
HtlcBasePoint dumpDescriptor
}
type dumpDescriptor struct {
Path string
Pubkey string
}
func toDumpChanCfg(cfg channeldb.ChannelConfig) dumpChanCfg {
return dumpChanCfg{
ChannelConstraints: cfg.ChannelConstraints,
MultiSigKey: toDumpDescriptor(cfg.MultiSigKey),
RevocationBasePoint: toDumpDescriptor(cfg.RevocationBasePoint),
PaymentBasePoint: toDumpDescriptor(cfg.PaymentBasePoint),
DelayBasePoint: toDumpDescriptor(cfg.DelayBasePoint),
HtlcBasePoint: toDumpDescriptor(cfg.HtlcBasePoint),
}
}
func toDumpDescriptor(desc keychain.KeyDescriptor) dumpDescriptor {
return dumpDescriptor{
Path: fmt.Sprintf(
lndInternalDerivationPath, desc.Family, desc.Index,
),
Pubkey: pubKeyToString(desc.PubKey),
}
}
func pubKeyToString(pubkey *btcec.PublicKey) string {
if pubkey == nil {
return "<nil>"
}
return hex.EncodeToString(pubkey.SerializeCompressed())
}

@ -21,7 +21,7 @@ require (
github.com/urfave/cli/v2 v2.0.0 // indirect
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 // indirect
gitlab.com/NebulousLabs/go-upnp v0.0.0-20181011194642-3a71999ed0d3 // indirect
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 // indirect
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76 // indirect
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect

@ -0,0 +1,66 @@
package chantools
import (
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/lightningnetwork/lnd/keychain"
)
const (
hardenedKeyStart = uint32(hdkeychain.HardenedKeyStart)
)
func deriveChildren(key *hdkeychain.ExtendedKey, path []uint32) (
*hdkeychain.ExtendedKey, error) {
var (
currentKey = key
err error
)
for _, pathPart := range path {
currentKey, err = currentKey.Child(pathPart)
if err != nil {
return nil, err
}
}
return currentKey, nil
}
type channelBackupEncryptionRing struct {
extendedKey *hdkeychain.ExtendedKey
chainParams *chaincfg.Params
}
func (r *channelBackupEncryptionRing) DeriveNextKey(_ keychain.KeyFamily) (
keychain.KeyDescriptor, error) {
return keychain.KeyDescriptor{}, nil
}
func (r *channelBackupEncryptionRing) DeriveKey(keyLoc keychain.KeyLocator) (
keychain.KeyDescriptor, error) {
var empty = keychain.KeyDescriptor{}
keyBackup, err := deriveChildren(r.extendedKey, []uint32{
hardenedKeyStart + uint32(keychain.BIP0043Purpose),
hardenedKeyStart + r.chainParams.HDCoinType,
hardenedKeyStart + uint32(keyLoc.Family),
0,
keyLoc.Index,
})
if err != nil {
return empty, err
}
backupPubKey, err := keyBackup.ECPubKey()
if err != nil {
return empty, err
}
return keychain.KeyDescriptor{
KeyLocator: keychain.KeyLocator{
Family: keyLoc.Family,
Index: keyLoc.Index,
},
PubKey: backupPubKey,
}, nil
}

@ -1,13 +1,21 @@
package chantools
import (
"bufio"
"fmt"
"os"
"path"
"strings"
"syscall"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/jessevdk/go-flags"
"github.com/lightningnetwork/lnd/aezeed"
"github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/channeldb"
"golang.org/x/crypto/ssh/terminal"
)
const (
@ -15,6 +23,7 @@ const (
)
type config struct {
Testnet bool `long:"testnet" description:"Set to true if testnet parameters should be used."`
ApiUrl string `long:"apiurl" description:"API URL to use (must be esplora compatible)."`
ListChannels string `long:"listchannels" description:"The channel input is in the format of lncli's listchannels format. Specify '-' to read from stdin."`
PendingChannels string `long:"pendingchannels" description:"The channel input is in the format of lncli's pendingchannels format. Specify '-' to read from stdin."`
@ -28,6 +37,7 @@ var (
cfg = &config{
ApiUrl: defaultApiUrl,
}
chainParams = &chaincfg.MainNetParams
)
func Main() error {
@ -37,27 +47,32 @@ func Main() error {
parser := flags.NewParser(cfg, flags.Default)
_, _ = parser.AddCommand(
"summary", "Compile a summary about the current state of "+
"channels", "", &summaryCommand{},
"channels.", "", &summaryCommand{},
)
_, _ = parser.AddCommand(
"rescueclosed", "Try finding the private keys for funds that "+
"are in outputs of remotely force-closed channels", "",
"are in outputs of remotely force-closed channels.", "",
&rescueClosedCommand{},
)
_, _ = parser.AddCommand(
"forceclose", "Force-close the last state that is in the "+
"channel.db provided", "",
&forceCloseCommand{},
"channel.db provided.", "", &forceCloseCommand{},
)
_, _ = parser.AddCommand(
"sweeptimelock", "Sweep the force-closed state after the time "+
"lock has expired", "",
&sweepTimeLockCommand{},
"lock has expired.", "", &sweepTimeLockCommand{},
)
_, _ = parser.AddCommand(
"dumpchannels", "Dump all channel information from lnd's "+
"channel database", "",
&dumpChannelsCommand{},
"channel database.", "", &dumpChannelsCommand{},
)
_, _ = parser.AddCommand(
"showrootkey", "Extract and show the BIP32 HD root key from "+
"the 24 word lnd aezeed.", "", &showRootKeyCommand{},
)
_, _ = parser.AddCommand(
"dumpbackup", "Dump the content of a channel.backup file.", "",
&dumpBackupCommand{},
)
_, err := parser.Parse()
@ -184,7 +199,7 @@ type dumpChannelsCommand struct {
func (c *dumpChannelsCommand) Execute(_ []string) error {
// Check that we have a channel DB.
if c.ChannelDB == "" {
return fmt.Errorf("rescue DB is required")
return fmt.Errorf("channel DB is required")
}
db, err := channeldb.Open(path.Dir(c.ChannelDB))
if err != nil {
@ -193,6 +208,97 @@ func (c *dumpChannelsCommand) Execute(_ []string) error {
return dumpChannelInfo(db)
}
type showRootKeyCommand struct{}
func (c *showRootKeyCommand) Execute(_ []string) error {
// We'll now prompt the user to enter in their 24-word mnemonic.
fmt.Printf("Input your 24-word mnemonic separated by spaces: ")
reader := bufio.NewReader(os.Stdin)
mnemonicStr, err := reader.ReadString('\n')
if err != nil {
return err
}
// We'll trim off extra spaces, and ensure the mnemonic is all
// lower case, then populate our request.
mnemonicStr = strings.TrimSpace(mnemonicStr)
mnemonicStr = strings.ToLower(mnemonicStr)
cipherSeedMnemonic := strings.Split(mnemonicStr, " ")
fmt.Println()
if len(cipherSeedMnemonic) != 24 {
return fmt.Errorf("wrong cipher seed mnemonic "+
"length: got %v words, expecting %v words",
len(cipherSeedMnemonic), 24)
}
// Additionally, the user may have a passphrase, that will also
// need to be provided so the daemon can properly decipher the
// cipher seed.
fmt.Printf("Input your cipher seed passphrase (press enter if " +
"your seed doesn't have a passphrase): ")
passphrase, err := terminal.ReadPassword(syscall.Stdin)
if err != nil {
return err
}
var mnemonic aezeed.Mnemonic
copy(mnemonic[:], cipherSeedMnemonic[:])
// If we're unable to map it back into the ciphertext, then either the
// mnemonic is wrong, or the passphrase is wrong.
cipherSeed, err := mnemonic.ToCipherSeed(passphrase)
if err != nil {
return err
}
rootKey, err := hdkeychain.NewMaster(cipherSeed.Entropy[:], chainParams)
if err != nil {
return fmt.Errorf("failed to derive master extended key")
}
fmt.Printf("\nYour BIP32 HD root key is: %s\n", rootKey.String())
return nil
}
type dumpBackupCommand struct {
RootKey string `long:"rootkey" description:"BIP32 HD root key of the wallet that was used to create the backup."`
MultiFile string `long:"multi_file" description:"The lnd channel.backup file to dump."`
}
func (c *dumpBackupCommand) Execute(_ []string) error {
setupChainParams(cfg)
// Check that root key is valid.
if c.RootKey == "" {
return fmt.Errorf("root key is required")
}
extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey)
if err != nil {
return fmt.Errorf("error parsing root key: %v", err)
}
// Check that we have a backup file.
if c.MultiFile == "" {
return fmt.Errorf("backup file is required")
}
multiFile := chanbackup.NewMultiFile(c.MultiFile)
multi, err := multiFile.ExtractMulti(&channelBackupEncryptionRing{
extendedKey: extendedKey,
chainParams: chainParams,
})
if err != nil {
return fmt.Errorf("could not extract multi file: %v", err)
}
return dumpChannelBackup(multi)
}
func setupChainParams(cfg *config) {
if cfg.Testnet {
chainParams = &chaincfg.TestNet3Params
}
}
func setupLogging() {
logWriter.RegisterSubLogger("CHAN", log)
err := logWriter.InitLogRotator("./results/chantools.log", 10, 3)

Loading…
Cancel
Save