From 0a517d1a7508db5f382b38c4fc34b8f15d4e06cb Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 6 Jan 2020 17:21:29 +0100 Subject: [PATCH] Add fixoldbackup command --- README.md | 26 ++++++++ btc/hdkeychain.go | 55 +++++++++++++++-- cmd/chantools/dumpbackup.go | 2 +- cmd/chantools/filterbackup.go | 2 +- cmd/chantools/fixoldbackup.go | 108 ++++++++++++++++++++++++++++++++++ cmd/chantools/main.go | 4 ++ 6 files changed, 189 insertions(+), 8 deletions(-) create mode 100644 cmd/chantools/fixoldbackup.go diff --git a/README.md b/README.md index dd6ed55..6151e06 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ + [dumpbackup](#dumpbackup) + [dumpchannels](#dumpchannels) + [filterbackup](#filterbackup) + + [fixoldbackup](#fixoldbackup) + [forceclose](#forceclose) + [rescueclosed](#rescueclosed) + [showrootkey](#showrootkey) @@ -57,6 +58,7 @@ Available commands: dumpbackup Dump the content of a channel.backup file. dumpchannels Dump all channel information from lnd's channel database. filterbackup Filter an lnd channel.backup file and remove certain channels. + fixoldbackup Fixes an old channel.backup file that is affected by the lnd issue #3881 (unable to derive shachain root key). 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. @@ -152,6 +154,30 @@ chantools filterbackup --rootkey xprvxxxxxxxxxx \ --discard 2abcdef2b2bffaaa...db0abadd:1,4abcdef2b2bffaaa...db8abadd:0 ``` +### fixoldbackup + +```text +Usage: + chantools [OPTIONS] fixoldbackup [fixoldbackup-OPTIONS] + +[fixoldbackup command options] + --rootkey= BIP32 HD root key of the wallet that was used to create the backup. Leave empty to prompt for lnd 24 word aezeed. + --multi_file= The lnd channel.backup file to fix. +``` + +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 +private key). + +Example command: + +```bash +chantools fixoldbackup --rootkey xprvxxxxxxxxxx \ + --multi_file ~/.lnd/data/chain/bitcoin/mainnet/channel.backup +``` + ### forceclose ```text diff --git a/btc/hdkeychain.go b/btc/hdkeychain.go index 663b3c4..d21cc4a 100644 --- a/btc/hdkeychain.go +++ b/btc/hdkeychain.go @@ -57,22 +57,22 @@ func ParsePath(path string) ([]uint32, error) { return indices, nil } -type ChannelBackupEncryptionRing struct { +type HDKeyRing struct { ExtendedKey *hdkeychain.ExtendedKey ChainParams *chaincfg.Params } -func (r *ChannelBackupEncryptionRing) DeriveNextKey(_ keychain.KeyFamily) ( +func (r *HDKeyRing) DeriveNextKey(_ keychain.KeyFamily) ( keychain.KeyDescriptor, error) { return keychain.KeyDescriptor{}, nil } -func (r *ChannelBackupEncryptionRing) DeriveKey(keyLoc keychain.KeyLocator) ( +func (r *HDKeyRing) DeriveKey(keyLoc keychain.KeyLocator) ( keychain.KeyDescriptor, error) { var empty = keychain.KeyDescriptor{} - keyBackup, err := DeriveChildren(r.ExtendedKey, []uint32{ + derivedKey, err := DeriveChildren(r.ExtendedKey, []uint32{ HardenedKeyStart + uint32(keychain.BIP0043Purpose), HardenedKeyStart + r.ChainParams.HDCoinType, HardenedKeyStart + uint32(keyLoc.Family), @@ -83,7 +83,7 @@ func (r *ChannelBackupEncryptionRing) DeriveKey(keyLoc keychain.KeyLocator) ( return empty, err } - backupPubKey, err := keyBackup.ECPubKey() + derivedPubKey, err := derivedKey.ECPubKey() if err != nil { return empty, err } @@ -92,6 +92,49 @@ func (r *ChannelBackupEncryptionRing) DeriveKey(keyLoc keychain.KeyLocator) ( Family: keyLoc.Family, Index: keyLoc.Index, }, - PubKey: backupPubKey, + PubKey: derivedPubKey, }, nil } + +// Check if a key descriptor is correct by making sure that we can derive the +// key that it describes. +func (r *HDKeyRing) CheckDescriptor( + keyDesc keychain.KeyDescriptor) error { + + // A check doesn't make sense if there is no public key set. + if keyDesc.PubKey == nil { + return fmt.Errorf("no public key provided to check") + } + + // Performance fix, derive static path only once. + familyKey, err := DeriveChildren(r.ExtendedKey, []uint32{ + HardenedKeyStart + uint32(keychain.BIP0043Purpose), + HardenedKeyStart + r.ChainParams.HDCoinType, + HardenedKeyStart + uint32(keyDesc.Family), + 0, + }) + if err != nil { + return err + } + + // Scan the same key range as lnd would do on channel restore. + for i := 0; i < keychain.MaxKeyRangeScan; i++ { + child, err := DeriveChildren(familyKey, []uint32{uint32(i)}) + if err != nil { + return err + } + pubKey, err := child.ECPubKey() + if err != nil { + return err + } + if !pubKey.IsEqual(keyDesc.PubKey) { + continue + } + // If we found the key, we can abort and signal success. + return nil + } + + // We scanned the max range and didn't find a key. It's very likely not + // derivable with the given information. + return keychain.ErrCannotDerivePrivKey +} diff --git a/cmd/chantools/dumpbackup.go b/cmd/chantools/dumpbackup.go index 65233b1..27ef1da 100644 --- a/cmd/chantools/dumpbackup.go +++ b/cmd/chantools/dumpbackup.go @@ -41,7 +41,7 @@ func (c *dumpBackupCommand) Execute(_ []string) error { return fmt.Errorf("backup file is required") } multiFile := chanbackup.NewMultiFile(c.MultiFile) - keyRing := &btc.ChannelBackupEncryptionRing{ + keyRing := &btc.HDKeyRing{ ExtendedKey: extendedKey, ChainParams: chainParams, } diff --git a/cmd/chantools/filterbackup.go b/cmd/chantools/filterbackup.go index e4714eb..7b8ef39 100644 --- a/cmd/chantools/filterbackup.go +++ b/cmd/chantools/filterbackup.go @@ -46,7 +46,7 @@ func (c *filterBackupCommand) Execute(_ []string) error { return fmt.Errorf("backup file is required") } multiFile := chanbackup.NewMultiFile(c.MultiFile) - keyRing := &btc.ChannelBackupEncryptionRing{ + keyRing := &btc.HDKeyRing{ ExtendedKey: extendedKey, ChainParams: chainParams, } diff --git a/cmd/chantools/fixoldbackup.go b/cmd/chantools/fixoldbackup.go new file mode 100644 index 0000000..4d24a19 --- /dev/null +++ b/cmd/chantools/fixoldbackup.go @@ -0,0 +1,108 @@ +package main + +import ( + "fmt" + "os" + "time" + + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/guggero/chantools/btc" + "github.com/lightningnetwork/lnd/chanbackup" + "github.com/lightningnetwork/lnd/keychain" +) + +type fixOldBackupCommand struct { + RootKey string `long:"rootkey" description:"BIP32 HD root key of the wallet that was used to create the backup. Leave empty to prompt for lnd 24 word aezeed."` + MultiFile string `long:"multi_file" description:"The lnd channel.backup file to fix."` +} + +func (c *fixOldBackupCommand) Execute(_ []string) error { + setupChainParams(cfg) + + var ( + extendedKey *hdkeychain.ExtendedKey + err error + ) + + // Check that root key is valid or fall back to console input. + switch { + case c.RootKey != "": + extendedKey, err = hdkeychain.NewKeyFromString(c.RootKey) + + default: + extendedKey, err = rootKeyFromConsole() + } + if err != nil { + return fmt.Errorf("error reading 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) + keyRing := &btc.HDKeyRing{ + ExtendedKey: extendedKey, + ChainParams: chainParams, + } + return fixOldChannelBackup(multiFile, keyRing) +} + +func fixOldChannelBackup(multiFile *chanbackup.MultiFile, + ring *btc.HDKeyRing) error { + + multi, err := multiFile.ExtractMulti(ring) + if err != nil { + return fmt.Errorf("could not extract multi file: %v", err) + } + + log.Infof("Checking shachain root of %d channels, this might take a "+ + "while.", len(multi.StaticBackups)) + fixedChannels := 0 + for _, single := range multi.StaticBackups { + err := ring.CheckDescriptor(single.ShaChainRootDesc) + switch err { + case nil: + continue + + case keychain.ErrCannotDerivePrivKey: + // Fix the incorrect descriptor by deriving a default + // one and overwriting it in the backup. + log.Infof("The shachain root for channel %s could "+ + "not be derived, must be in old format. "+ + "Fixing...", single.FundingOutpoint.String()) + baseKeyDesc, err := ring.DeriveKey(keychain.KeyLocator{ + Family: keychain.KeyFamilyRevocationRoot, + Index: 0, + }) + if err != nil { + return err + } + single.ShaChainRootDesc = baseKeyDesc + fixedChannels++ + + default: + return fmt.Errorf("could not check shachain root "+ + "descriptor: %v", err) + } + } + if fixedChannels == 0 { + log.Info("No channels were affected by issue #3881, nothing " + + "to fix.") + return nil + } + + log.Infof("Fixed shachain root of %d channels.", fixedChannels) + fileName := fmt.Sprintf("results/backup-fixed-%s.backup", + time.Now().Format("2006-01-02-15-04-05")) + log.Infof("Writing result to %s", fileName) + f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return err + } + err = multi.PackToWriter(f, ring) + _ = f.Close() + if err != nil { + return err + } + return nil +} diff --git a/cmd/chantools/main.go b/cmd/chantools/main.go index 5d67f57..399c7b9 100644 --- a/cmd/chantools/main.go +++ b/cmd/chantools/main.go @@ -98,6 +98,10 @@ func runCommandParser() error { "filterbackup", "Filter an lnd channel.backup file and " + "remove certain channels.", "", &filterBackupCommand{}, ) + _, _ = parser.AddCommand( + "fixoldbackup", "Fixes an old channel.backup file that is " + + "affected by the lnd issue #3881 (unable to derive " + + "shachain root key).", "", &fixOldBackupCommand{}) _, err := parser.Parse() return err