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 **WARNING 2**: This tool will query public block explorer APIs, your privacy
might not be preserved. Use at your own risk. 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 ## Overview
```text ```text
@ -18,6 +27,7 @@ Usage:
chantools [OPTIONS] <command> chantools [OPTIONS] <command>
Application Options: 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) --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. --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. --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 -h, --help Show this help message
Available commands: Available commands:
dumpchannels Dump all channel information from lnd's channel database dumpbackup Dump the content of a channel.backup file.
forceclose Force-close the last state that is in the channel.db provided dumpchannels Dump all channel information from lnd's channel database.
rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels forceclose Force-close the last state that is in the channel.db provided.
summary Compile a summary about the current state of channels rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels.
sweeptimelock Sweep the force-closed state after the time lock has expired 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 ## summary command
@ -168,3 +180,36 @@ Example command:
```bash ```bash
chantools dumpchannels --channeldb ~/.lnd/data/graph/mainnet/channel.db 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 ( import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"fmt"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwire" "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 // 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. // channel DB. See `channeldb.OpenChannel` for information about the fields.
type dumpInfo struct { type dumpInfo struct {
@ -48,22 +41,6 @@ type dumpInfo struct {
RemoteShutdownScript lnwire.DeliveryAddress 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 { func dumpChannelInfo(chanDb *channeldb.DB) error {
channels, err := chanDb.FetchAllChannels() channels, err := chanDb.FetchAllChannels()
if err != nil { if err != nil {
@ -124,27 +101,3 @@ func dumpChannelInfo(chanDb *channeldb.DB) error {
spew.Dump(dumpChannels) spew.Dump(dumpChannels)
return nil 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" "time"
"github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcutil/hdkeychain"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
@ -18,8 +17,6 @@ import (
) )
var ( var (
hardenedKeyStart = uint32(hdkeychain.HardenedKeyStart)
chainParams = &chaincfg.MainNetParams
cacheSize = 2000 cacheSize = 2000
cache []*cacheEntry cache []*cacheEntry
@ -184,22 +181,6 @@ func fillCache(extendedKey *hdkeychain.ExtendedKey) error {
return nil 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) { func parseAddr(addr string) ([]byte, error) {
// First parse address to get targetPubKeyHash from it later. // First parse address to get targetPubKeyHash from it later.
targetAddr, err := btcutil.DecodeAddress(addr, chainParams) 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 github.com/urfave/cli/v2 v2.0.0 // indirect
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 // indirect gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 // indirect
gitlab.com/NebulousLabs/go-upnp v0.0.0-20181011194642-3a71999ed0d3 // 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/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76 // indirect golang.org/x/sys v0.0.0-20191224085550-c709ea063b76 // indirect
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // 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 package chantools
import ( import (
"bufio"
"fmt" "fmt"
"os"
"path" "path"
"strings"
"syscall"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcutil/hdkeychain"
"github.com/jessevdk/go-flags" "github.com/jessevdk/go-flags"
"github.com/lightningnetwork/lnd/aezeed"
"github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"golang.org/x/crypto/ssh/terminal"
) )
const ( const (
@ -15,6 +23,7 @@ const (
) )
type config struct { 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)."` 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."` 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."` 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{ cfg = &config{
ApiUrl: defaultApiUrl, ApiUrl: defaultApiUrl,
} }
chainParams = &chaincfg.MainNetParams
) )
func Main() error { func Main() error {
@ -37,27 +47,32 @@ func Main() error {
parser := flags.NewParser(cfg, flags.Default) parser := flags.NewParser(cfg, flags.Default)
_, _ = parser.AddCommand( _, _ = parser.AddCommand(
"summary", "Compile a summary about the current state of "+ "summary", "Compile a summary about the current state of "+
"channels", "", &summaryCommand{}, "channels.", "", &summaryCommand{},
) )
_, _ = parser.AddCommand( _, _ = parser.AddCommand(
"rescueclosed", "Try finding the private keys for funds that "+ "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{}, &rescueClosedCommand{},
) )
_, _ = parser.AddCommand( _, _ = parser.AddCommand(
"forceclose", "Force-close the last state that is in the "+ "forceclose", "Force-close the last state that is in the "+
"channel.db provided", "", "channel.db provided.", "", &forceCloseCommand{},
&forceCloseCommand{},
) )
_, _ = parser.AddCommand( _, _ = parser.AddCommand(
"sweeptimelock", "Sweep the force-closed state after the time "+ "sweeptimelock", "Sweep the force-closed state after the time "+
"lock has expired", "", "lock has expired.", "", &sweepTimeLockCommand{},
&sweepTimeLockCommand{},
) )
_, _ = parser.AddCommand( _, _ = parser.AddCommand(
"dumpchannels", "Dump all channel information from lnd's "+ "dumpchannels", "Dump all channel information from lnd's "+
"channel database", "", "channel database.", "", &dumpChannelsCommand{},
&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() _, err := parser.Parse()
@ -184,7 +199,7 @@ type dumpChannelsCommand struct {
func (c *dumpChannelsCommand) Execute(_ []string) error { func (c *dumpChannelsCommand) Execute(_ []string) error {
// Check that we have a channel DB. // Check that we have a channel DB.
if c.ChannelDB == "" { 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)) db, err := channeldb.Open(path.Dir(c.ChannelDB))
if err != nil { if err != nil {
@ -193,6 +208,97 @@ func (c *dumpChannelsCommand) Execute(_ []string) error {
return dumpChannelInfo(db) 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() { func setupLogging() {
logWriter.RegisterSubLogger("CHAN", log) logWriter.RegisterSubLogger("CHAN", log)
err := logWriter.InitLogRotator("./results/chantools.log", 10, 3) err := logWriter.InitLogRotator("./results/chantools.log", 10, 3)

Loading…
Cancel
Save