diff --git a/README.md b/README.md index 6151e06..c354ce6 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ + [dumpchannels](#dumpchannels) + [filterbackup](#filterbackup) + [fixoldbackup](#fixoldbackup) + + [genimportscript](#genimportscript) + [forceclose](#forceclose) + [rescueclosed](#rescueclosed) + [showrootkey](#showrootkey) @@ -54,16 +55,17 @@ Help Options: -h, --help Show this help message Available commands: - derivekey Derive a key with a specific derivation path from the BIP32 HD root key. - 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. - summary Compile a summary about the current state of channels. - sweeptimelock Sweep the force-closed state after the time lock has expired. + derivekey Derive a key with a specific derivation path from the BIP32 HD root key. + 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. + genimportscript Generate a script containing the on-chain keys of an lnd wallet that can be imported into other software like bitcoind. + 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. ``` ## Commands @@ -213,6 +215,38 @@ chantools --fromsummary results/summary-xxxx-yyyy.json \ --publish ``` +### genimportscript + +```text +Usage: + chantools [OPTIONS] genimportscript [genimportscript-OPTIONS] + +[genimportscript command options] + --rootkey= BIP32 HD root key to use. Leave empty to prompt for lnd 24 word aezeed. + --format= The format of the generated import script. Currently supported are: bitcoin-cli, bitcoin-cli-watchonly. + --recoverywindow= The number of keys to scan per internal/external branch. The output will consist of double this amount of keys. (default 2500) + --rescanfrom= The block number to rescan from. Will be set automatically from the wallet birthday if the lnd 24 word aezeed is entered. (default 500000) +``` + +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 +software like bitcoind. + +The following script formats are currently supported: +* `bitcoin-cli`: Creates a list of `bitcoin-cli importprivkey` commands that can + be used in combination with a `bitcoind` full node to recover the funds locked + in those private keys. +* `bitcoin-cli-watchonly`: Does the same as `bitcoin-cli` but with the + `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. + +Example command: + +```bash +chantools genimportscript --format bitcoin-cli --recoverywindow 5000 +``` + ### rescueclosed ```text diff --git a/cmd/chantools/derivekey.go b/cmd/chantools/derivekey.go index 7a2d063..4085eae 100644 --- a/cmd/chantools/derivekey.go +++ b/cmd/chantools/derivekey.go @@ -28,7 +28,7 @@ func (c *deriveKeyCommand) Execute(_ []string) error { extendedKey, err = hdkeychain.NewKeyFromString(c.RootKey) default: - extendedKey, err = rootKeyFromConsole() + extendedKey, _, err = rootKeyFromConsole() } if err != nil { return fmt.Errorf("error reading root key: %v", err) diff --git a/cmd/chantools/dumpbackup.go b/cmd/chantools/dumpbackup.go index 27ef1da..f35dd56 100644 --- a/cmd/chantools/dumpbackup.go +++ b/cmd/chantools/dumpbackup.go @@ -30,7 +30,7 @@ func (c *dumpBackupCommand) Execute(_ []string) error { extendedKey, err = hdkeychain.NewKeyFromString(c.RootKey) default: - extendedKey, err = rootKeyFromConsole() + extendedKey, _, err = rootKeyFromConsole() } if err != nil { return fmt.Errorf("error reading root key: %v", err) diff --git a/cmd/chantools/filterbackup.go b/cmd/chantools/filterbackup.go index 7b8ef39..2eca6b7 100644 --- a/cmd/chantools/filterbackup.go +++ b/cmd/chantools/filterbackup.go @@ -32,7 +32,7 @@ func (c *filterBackupCommand) Execute(_ []string) error { extendedKey, err = hdkeychain.NewKeyFromString(c.RootKey) default: - extendedKey, err = rootKeyFromConsole() + extendedKey, _, err = rootKeyFromConsole() } if err != nil { return fmt.Errorf("error reading root key: %v", err) diff --git a/cmd/chantools/fixoldbackup.go b/cmd/chantools/fixoldbackup.go index 490d190..6a5dc18 100644 --- a/cmd/chantools/fixoldbackup.go +++ b/cmd/chantools/fixoldbackup.go @@ -30,7 +30,7 @@ func (c *fixOldBackupCommand) Execute(_ []string) error { extendedKey, err = hdkeychain.NewKeyFromString(c.RootKey) default: - extendedKey, err = rootKeyFromConsole() + extendedKey, _, err = rootKeyFromConsole() } if err != nil { return fmt.Errorf("error reading root key: %v", err) diff --git a/cmd/chantools/forceclose.go b/cmd/chantools/forceclose.go index 5204de9..1b62c3f 100644 --- a/cmd/chantools/forceclose.go +++ b/cmd/chantools/forceclose.go @@ -36,7 +36,7 @@ func (c *forceCloseCommand) Execute(_ []string) error { extendedKey, err = hdkeychain.NewKeyFromString(c.RootKey) default: - extendedKey, err = rootKeyFromConsole() + extendedKey, _, err = rootKeyFromConsole() } if err != nil { return fmt.Errorf("error reading root key: %v", err) diff --git a/cmd/chantools/genimportscript.go b/cmd/chantools/genimportscript.go new file mode 100644 index 0000000..c67d581 --- /dev/null +++ b/cmd/chantools/genimportscript.go @@ -0,0 +1,159 @@ +package main + +import ( + "fmt" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/guggero/chantools/btc" +) + +const ( + defaultRecoveryWindow = 2500 + defaultRescanFrom = 500000 +) + +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."` + 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)"` +} + +func (c *genImportScriptCommand) Execute(_ []string) error { + var ( + extendedKey *hdkeychain.ExtendedKey + err error + birthday time.Time + ) + + // Check that root key is valid or fall back to console input. + switch { + case c.RootKey != "": + extendedKey, err = hdkeychain.NewKeyFromString(c.RootKey) + if err != nil { + return fmt.Errorf("error reading root key: %v", err) + } + + default: + extendedKey, birthday, err = rootKeyFromConsole() + if err != nil { + return fmt.Errorf("error reading root key: %v", err) + } + // The btcwallet gives the birthday a slack of 48 hours, let's + // do the same. + c.RescanFrom = seedBirthdayToBlock(birthday.Add(-48 * time.Hour)) + } + + // Set default values. + if c.RecoveryWindow == 0 { + c.RecoveryWindow = defaultRecoveryWindow + } + if c.RescanFrom == 0 { + c.RescanFrom = defaultRescanFrom + } + + // Determine the format. + printFn := printBitcoinCli + if c.Format == "bitcoin-cli-watchonly" { + printFn = printBitcoinCliWatchOnly + } + + fmt.Println("# Paste the following lines into a command line window.") + + // External branch first (m/84'/'/0'/0/x). + for i := uint32(0); i < c.RecoveryWindow; i++ { + derivedKey, err := btc.DeriveChildren(extendedKey, []uint32{ + btc.HardenedKeyStart + uint32(84), + btc.HardenedKeyStart + chainParams.HDCoinType, + btc.HardenedKeyStart + uint32(0), + 0, + i, + }) + if err != nil { + return err + } + err = printFn(derivedKey, 0, i) + if err != nil { + return err + } + } + + // Now the internal branch (m/84'/'/0'/1/x). + for i := uint32(0); i < c.RecoveryWindow; i++ { + derivedKey, err := btc.DeriveChildren(extendedKey, []uint32{ + btc.HardenedKeyStart + uint32(84), + btc.HardenedKeyStart + chainParams.HDCoinType, + btc.HardenedKeyStart + uint32(0), + 1, + i, + }) + if err != nil { + return err + } + err = printFn(derivedKey, 1, i) + if err != nil { + return err + } + } + + fmt.Printf("bitcoin-cli rescanblockchain %d\n", c.RescanFrom) + return nil +} + +func printBitcoinCli(derivedKey *hdkeychain.ExtendedKey, branch, + index uint32) error { + + privKey, err := derivedKey.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) + } + + 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, + index uint32) error { + + pubKey, err := derivedKey.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 seedBirthdayToBlock(birthdayTimestamp time.Time) uint32 { + var genesisTimestamp time.Time + switch chainParams.Name { + case "mainnet": + genesisTimestamp = + chaincfg.MainNetParams.GenesisBlock.Header.Timestamp + + case "testnet": + genesisTimestamp = + chaincfg.TestNet3Params.GenesisBlock.Header.Timestamp + + default: + panic(fmt.Errorf("unimplemented network %v", chainParams.Name)) + } + + // With the timestamps retrieved, we can estimate a block height by + // taking the difference between them and dividing by the average block + // time (10 minutes). + return uint32(birthdayTimestamp.Sub(genesisTimestamp).Seconds() / 600) +} diff --git a/cmd/chantools/main.go b/cmd/chantools/main.go index 399c7b9..2b932dd 100644 --- a/cmd/chantools/main.go +++ b/cmd/chantools/main.go @@ -9,6 +9,7 @@ import ( "os" "strings" "syscall" + "time" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcutil/hdkeychain" @@ -95,13 +96,18 @@ func runCommandParser() error { "from the BIP32 HD root key.", "", &deriveKeyCommand{}, ) _, _ = parser.AddCommand( - "filterbackup", "Filter an lnd channel.backup file and " + + "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 " + + "fixoldbackup", "Fixes an old channel.backup file that is "+ + "affected by the lnd issue #3881 (unable to derive "+ "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{}) _, err := parser.Parse() return err @@ -158,13 +164,13 @@ func readInput(input string) ([]byte, error) { return ioutil.ReadFile(input) } -func rootKeyFromConsole() (*hdkeychain.ExtendedKey, error) { +func rootKeyFromConsole() (*hdkeychain.ExtendedKey, time.Time, 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 nil, err + return nil, time.Unix(0, 0), err } // We'll trim off extra spaces, and ensure the mnemonic is all @@ -177,8 +183,8 @@ func rootKeyFromConsole() (*hdkeychain.ExtendedKey, error) { fmt.Println() if len(cipherSeedMnemonic) != 24 { - return nil, fmt.Errorf("wrong cipher seed mnemonic "+ - "length: got %v words, expecting %v words", + return nil, time.Unix(0, 0), fmt.Errorf("wrong cipher seed "+ + "mnemonic length: got %v words, expecting %v words", len(cipherSeedMnemonic), 24) } @@ -189,7 +195,7 @@ func rootKeyFromConsole() (*hdkeychain.ExtendedKey, error) { "your seed doesn't have a passphrase): ") passphrase, err := terminal.ReadPassword(syscall.Stdin) if err != nil { - return nil, err + return nil, time.Unix(0, 0), err } var mnemonic aezeed.Mnemonic @@ -199,14 +205,15 @@ func rootKeyFromConsole() (*hdkeychain.ExtendedKey, error) { // mnemonic is wrong, or the passphrase is wrong. cipherSeed, err := mnemonic.ToCipherSeed(passphrase) if err != nil { - return nil, fmt.Errorf("failed to decrypt seed with passphrase"+ - ": %v", err) + return nil, time.Unix(0, 0), fmt.Errorf("failed to decrypt "+ + "seed with passphrase: %v", err) } rootKey, err := hdkeychain.NewMaster(cipherSeed.Entropy[:], chainParams) if err != nil { - return nil, fmt.Errorf("failed to derive master extended key") + return nil, time.Unix(0, 0), fmt.Errorf("failed to derive " + + "master extended key") } - return rootKey, nil + return rootKey, cipherSeed.BirthdayTime(), nil } func setupChainParams(cfg *config) { diff --git a/cmd/chantools/rescueclosed.go b/cmd/chantools/rescueclosed.go index 7b50d56..d66010f 100644 --- a/cmd/chantools/rescueclosed.go +++ b/cmd/chantools/rescueclosed.go @@ -48,7 +48,7 @@ func (c *rescueClosedCommand) Execute(_ []string) error { extendedKey, err = hdkeychain.NewKeyFromString(c.RootKey) default: - extendedKey, err = rootKeyFromConsole() + extendedKey, _, err = rootKeyFromConsole() } if err != nil { return fmt.Errorf("error reading root key: %v", err) diff --git a/cmd/chantools/showrootkey.go b/cmd/chantools/showrootkey.go index 2d03c32..4339e10 100644 --- a/cmd/chantools/showrootkey.go +++ b/cmd/chantools/showrootkey.go @@ -7,7 +7,7 @@ import ( type showRootKeyCommand struct{} func (c *showRootKeyCommand) Execute(_ []string) error { - rootKey, err := rootKeyFromConsole() + rootKey, _, err := rootKeyFromConsole() if err != nil { return fmt.Errorf("failed to read root key from console: %v", err) diff --git a/cmd/chantools/sweeptimelock.go b/cmd/chantools/sweeptimelock.go index aa4ab88..158d0da 100644 --- a/cmd/chantools/sweeptimelock.go +++ b/cmd/chantools/sweeptimelock.go @@ -38,7 +38,7 @@ func (c *sweepTimeLockCommand) Execute(_ []string) error { extendedKey, err = hdkeychain.NewKeyFromString(c.RootKey) default: - extendedKey, err = rootKeyFromConsole() + extendedKey, _, err = rootKeyFromConsole() } if err != nil { return fmt.Errorf("error reading root key: %v", err) diff --git a/dump/dump.go b/dump/dump.go index 312077d..880001e 100644 --- a/dump/dump.go +++ b/dump/dump.go @@ -3,10 +3,10 @@ package dump import ( "encoding/hex" "fmt" - "github.com/btcsuite/btcd/chaincfg" "net" "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/chanbackup"