From 4c92de59e5c11b1bc48732ac9190c366c84d6d6b Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 2 Apr 2020 10:27:38 +0200 Subject: [PATCH] Refactor genimportscript command --- README.md | 4 +- btc/bitcoind.go | 217 +++++++++++++++++++++++++++++++ cmd/chantools/genimportscript.go | 184 +++++--------------------- lnd/hdkeychain.go | 36 ++++- 4 files changed, 285 insertions(+), 156 deletions(-) create mode 100644 btc/bitcoind.go diff --git a/README.md b/README.md index fd4b20b..e36a898 100644 --- a/README.md +++ b/README.md @@ -274,7 +274,9 @@ Usage: [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. + --format= The format of the generated import script. Currently supported are: bitcoin-cli, bitcoin-cli-watchonly, bitcoin-importwallet. + --lndpaths Use all derivation paths that lnd uses. Results in a large number of results. Cannot be used in conjunction with --derivationpath. + --derivationpath= Use one specific derivation path. Specify the first levels of the derivation path before any internal/external branch. Cannot be used in conjunction with --lndpaths. (default m/84'/0'/0') --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) ``` diff --git a/btc/bitcoind.go b/btc/bitcoind.go new file mode 100644 index 0000000..6de4207 --- /dev/null +++ b/btc/bitcoind.go @@ -0,0 +1,217 @@ +package btc + +import ( + "fmt" + "io" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/guggero/chantools/lnd" +) + +const ( + FormatCli = "bitcoin-cli" + FormatCliWatchOnly = "bitcoin-cli-watchonly" + FormatImportwallet = "bitcoin-importwallet" +) + +type KeyExporter interface { + Header() string + Format(*hdkeychain.ExtendedKey, *chaincfg.Params, string, uint32, + uint32) (string, error) + Trailer(uint32) string +} + +// ParseFormat parses the given format name and returns its associated print +// function. +func ParseFormat(format string) KeyExporter { + switch format { + default: + fallthrough + + case FormatCli: + return &Cli{} + + case FormatCliWatchOnly: + return &CliWatchOnly{} + + case FormatImportwallet: + return &ImportWallet{} + } +} + +func ExportKeys(extendedKey *hdkeychain.ExtendedKey, strPaths []string, + paths [][]uint32, params *chaincfg.Params, recoveryWindow, + rescanFrom uint32, exporter KeyExporter, writer io.Writer) error { + + _, _ = fmt.Fprintf( + writer, "# Wallet dump created by chantools on %s\n", + time.Now().UTC(), + ) + _, _ = fmt.Fprintf(writer, "%s\n", exporter.Header()) + for idx, strPath := range strPaths { + path := paths[idx] + + // External branch first (/0/i). + for i := uint32(0); i < recoveryWindow; i++ { + path := append(path, 0, i) + derivedKey, err := lnd.DeriveChildren(extendedKey, path) + if err != nil { + return err + } + result, err := exporter.Format( + derivedKey, params, strPath, 0, i, + ) + if err != nil { + return err + } + _, _ = fmt.Fprintf(writer, "%s\n", result) + } + + // Now the internal branch (/1/i). + for i := uint32(0); i < recoveryWindow; i++ { + path := append(path, 1, i) + derivedKey, err := lnd.DeriveChildren(extendedKey, path) + if err != nil { + return err + } + result, err := exporter.Format( + derivedKey, params, strPath, 1, i, + ) + if err != nil { + return err + } + _, _ = fmt.Fprintf(writer, "%s\n", result) + } + } + + _, _ = fmt.Fprintf(writer, "%s\n", exporter.Trailer(rescanFrom)) + return nil +} + +func SeedBirthdayToBlock(params *chaincfg.Params, + birthdayTimestamp time.Time) uint32 { + + var genesisTimestamp time.Time + switch params.Name { + case "mainnet": + genesisTimestamp = + chaincfg.MainNetParams.GenesisBlock.Header.Timestamp + + case "testnet3": + genesisTimestamp = + chaincfg.TestNet3Params.GenesisBlock.Header.Timestamp + + case "regtest", "simnet": + return 0 + + default: + panic(fmt.Errorf("unimplemented network %v", params.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) +} + +type Cli struct{} + +func (c *Cli) Header() string { + return "# Paste the following lines into a command line window." +} + +func (c *Cli) Format(hdKey *hdkeychain.ExtendedKey, params *chaincfg.Params, + path string, branch, index uint32) (string, error) { + + privKey, err := hdKey.ECPrivKey() + if err != nil { + return "", fmt.Errorf("could not derive private key: %v", err) + } + wif, err := btcutil.NewWIF(privKey, params, true) + if err != nil { + return "", fmt.Errorf("could not encode WIF: %v", err) + } + return fmt.Sprintf("bitcoin-cli importprivkey %s \"%s/%d/%d/\" false", + wif.String(), path, branch, index), nil +} + +func (c *Cli) Trailer(birthdayBlock uint32) string { + return fmt.Sprintf("bitcoin-cli rescanblockchain %d\n", birthdayBlock) +} + +type CliWatchOnly struct{} + +func (c *CliWatchOnly) Header() string { + return "# Paste the following lines into a command line window." +} + +func (c *CliWatchOnly) Format(hdKey *hdkeychain.ExtendedKey, _ *chaincfg.Params, + path string, branch, index uint32) (string, error) { + + pubKey, err := hdKey.ECPubKey() + if err != nil { + return "", fmt.Errorf("could not derive private key: %v", err) + } + return fmt.Sprintf("bitcoin-cli importpubkey %x \"%s/%d/%d/\" false", + pubKey.SerializeCompressed(), path, branch, index), nil +} + +func (c *CliWatchOnly) Trailer(birthdayBlock uint32) string { + return fmt.Sprintf("bitcoin-cli rescanblockchain %d\n", birthdayBlock) +} + +type ImportWallet struct{} + +func (i *ImportWallet) Header() string { + return "# Save this output to a file and use the importwallet " + + "command of bitcoin core." +} + +func (i *ImportWallet) Format(hdKey *hdkeychain.ExtendedKey, + params *chaincfg.Params, path string, branch, index uint32) (string, + error) { + + privKey, err := hdKey.ECPrivKey() + if err != nil { + return "", fmt.Errorf("could not derive private key: %v", err) + } + wif, err := btcutil.NewWIF(privKey, params, true) + if err != nil { + return "", fmt.Errorf("could not encode WIF: %v", err) + } + pubKey, err := hdKey.ECPubKey() + if err != nil { + return "", fmt.Errorf("could not derive private key: %v", err) + } + hash160 := btcutil.Hash160(pubKey.SerializeCompressed()) + addrP2PKH, err := btcutil.NewAddressPubKeyHash(hash160, params) + if err != nil { + return "", fmt.Errorf("could not create address: %v", err) + } + addrP2WKH, err := btcutil.NewAddressWitnessPubKeyHash(hash160, params) + if err != nil { + return "", fmt.Errorf("could not create address: %v", err) + } + script, err := txscript.PayToAddrScript(addrP2WKH) + if err != nil { + return "", fmt.Errorf("could not create script: %v", err) + } + addrNP2WKH, err := btcutil.NewAddressScriptHash(script, params) + if err != nil { + return "", fmt.Errorf("could not create address: %v", err) + } + + return fmt.Sprintf("%s 1970-01-01T00:00:01Z label=%s/%d/%d/ "+ + "# addr=%s,%s,%s", wif.String(), path, branch, index, + addrP2PKH.EncodeAddress(), addrNP2WKH.EncodeAddress(), + addrP2WKH.EncodeAddress(), + ), nil +} + +func (i *ImportWallet) Trailer(_ uint32) string { + return "" +} diff --git a/cmd/chantools/genimportscript.go b/cmd/chantools/genimportscript.go index a11b481..613c6f0 100644 --- a/cmd/chantools/genimportscript.go +++ b/cmd/chantools/genimportscript.go @@ -2,25 +2,24 @@ package main import ( "fmt" + "os" "time" - "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/txscript" - "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/hdkeychain" + "github.com/guggero/chantools/btc" "github.com/guggero/chantools/lnd" ) const ( defaultRecoveryWindow = 2500 defaultRescanFrom = 500000 - defaultDerivationPath = "m/84'/0'/0'" ) 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, bitcoin-importwallet."` - DerivationPath string `long:"derivationpath" description:"The first levels of the derivation path before any internal/external branch. (default m/84'/0'/0')"` + LndPaths bool `long:"lndpaths" description:"Use all derivation paths that lnd uses. Results in a large number of results. Cannot be used in conjunction with --derivationpath."` + DerivationPath string `long:"derivationpath" description:"Use one specific derivation path. Specify the first levels of the derivation path before any internal/external branch. Cannot be used in conjunction with --lndpaths. (default m/84'/0'/0')"` 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)"` } @@ -32,6 +31,8 @@ func (c *genImportScriptCommand) Execute(_ []string) error { extendedKey *hdkeychain.ExtendedKey err error birthday time.Time + strPaths []string + paths [][]uint32 ) // Check that root key is valid or fall back to console input. @@ -49,7 +50,9 @@ func (c *genImportScriptCommand) Execute(_ []string) error { } // The btcwallet gives the birthday a slack of 48 hours, let's // do the same. - c.RescanFrom = seedBirthdayToBlock(birthday.Add(-48 * time.Hour)) + c.RescanFrom = btc.SeedBirthdayToBlock( + chainParams, birthday.Add(-48*time.Hour), + ) } // Set default values. @@ -59,167 +62,40 @@ func (c *genImportScriptCommand) Execute(_ []string) error { if c.RescanFrom == 0 { c.RescanFrom = defaultRescanFrom } - if c.DerivationPath == "" { - c.DerivationPath = defaultDerivationPath - } - derivationPath, err := lnd.ParsePath(c.DerivationPath) - if err != nil { - return fmt.Errorf("error parsing path: %v", err) - } + // Decide what derivation path(s) to use. + switch { + case c.LndPaths && c.DerivationPath != "": + return fmt.Errorf("cannot use --lndpaths and --derivationpath " + + "at the same time") - fmt.Printf("# Wallet dump created by chantools on %s\n", - time.Now().UTC()) + case c.LndPaths: + strPaths, paths, err = lnd.AllDerivationPaths(chainParams) + if err != nil { + return fmt.Errorf("error getting lnd paths: %v", err) + } - // Determine the format. - var printFn func(*hdkeychain.ExtendedKey, string, uint32, uint32) error - switch c.Format { default: + c.DerivationPath = lnd.WalletDefaultDerivationPath fallthrough - case "bitcoin-cli": - printFn = printBitcoinCli - fmt.Println("# Paste the following lines into a command line " + - "window.") - - case "bitcoin-cli-watchonly": - printFn = printBitcoinCliWatchOnly - fmt.Println("# Paste the following lines into a command line " + - "window.") - - case "bitcoin-importwallet": - printFn = printBitcoinImportWallet - fmt.Println("# Save this output to a file and use the " + - "importwallet command of bitcoin core.") - } - - // External branch first (/0/i). - for i := uint32(0); i < c.RecoveryWindow; i++ { - path := append(derivationPath, 0, i) - derivedKey, err := lnd.DeriveChildren(extendedKey, path) - if err != nil { - return err - } - err = printFn(derivedKey, c.DerivationPath, 0, i) + case c.DerivationPath != "": + derivationPath, err := lnd.ParsePath(c.DerivationPath) if err != nil { - return err + return fmt.Errorf("error parsing path: %v", err) } + strPaths = []string{c.DerivationPath} + paths = [][]uint32{derivationPath} } - // Now the internal branch (/1/i). - for i := uint32(0); i < c.RecoveryWindow; i++ { - path := append(derivationPath, 1, i) - derivedKey, err := lnd.DeriveChildren(extendedKey, path) - if err != nil { - return err - } - err = printFn(derivedKey, c.DerivationPath, 1, i) - if err != nil { - return err - } - } - - fmt.Printf("bitcoin-cli rescanblockchain %d\n", c.RescanFrom) - return nil -} - -func printBitcoinCli(hdKey *hdkeychain.ExtendedKey, path string, - branch, index uint32) error { - - privKey, err := hdKey.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 \"%s/%d/%d/"+ - "\" false\n", wif.String(), path, branch, - index) - return nil -} - -func printBitcoinCliWatchOnly(hdKey *hdkeychain.ExtendedKey, path string, - branch, index uint32) error { - - pubKey, err := hdKey.ECPubKey() - if err != nil { - return fmt.Errorf("could not derive private key: %v", - err) - } - fmt.Printf("bitcoin-cli importpubkey %x \"%s/%d/%d/"+ - "\" false\n", pubKey.SerializeCompressed(), - path, branch, index) - return nil -} - -func printBitcoinImportWallet(hdKey *hdkeychain.ExtendedKey, path string, - branch, index uint32) error { - - privKey, err := hdKey.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) - } - pubKey, err := hdKey.ECPubKey() - if err != nil { - return fmt.Errorf("could not derive private key: %v", - err) - } - hash160 := btcutil.Hash160(pubKey.SerializeCompressed()) - addrP2PKH, err := btcutil.NewAddressPubKeyHash(hash160, chainParams) - if err != nil { - return fmt.Errorf("could not create address: %v", err) - } - addrP2WKH, err := btcutil.NewAddressWitnessPubKeyHash( - hash160, chainParams, + exporter := btc.ParseFormat(c.Format) + err = btc.ExportKeys( + extendedKey, strPaths, paths, chainParams, c.RecoveryWindow, + c.RescanFrom, exporter, os.Stdout, ) if err != nil { - return fmt.Errorf("could not create address: %v", err) - } - script, err := txscript.PayToAddrScript(addrP2WKH) - if err != nil { - return fmt.Errorf("could not create script: %v", err) - } - addrNP2WKH, err := btcutil.NewAddressScriptHash(script, chainParams) - if err != nil { - return fmt.Errorf("could not create address: %v", err) + return fmt.Errorf("error exporting keys: %v", err) } - fmt.Printf("%s 1970-01-01T00:00:01Z label=%s/%d/%d/ "+ - "# addr=%s,%s,%s\n", wif.String(), path, branch, index, - addrP2PKH.EncodeAddress(), addrNP2WKH.EncodeAddress(), - addrP2WKH.EncodeAddress(), - ) return nil } - -func seedBirthdayToBlock(birthdayTimestamp time.Time) uint32 { - var genesisTimestamp time.Time - switch chainParams.Name { - case "mainnet": - genesisTimestamp = - chaincfg.MainNetParams.GenesisBlock.Header.Timestamp - - case "testnet3": - genesisTimestamp = - chaincfg.TestNet3Params.GenesisBlock.Header.Timestamp - - case "regtest", "simnet": - return 0 - - 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/lnd/hdkeychain.go b/lnd/hdkeychain.go index 9898ebd..7e1cb39 100644 --- a/lnd/hdkeychain.go +++ b/lnd/hdkeychain.go @@ -13,7 +13,9 @@ import ( ) const ( - HardenedKeyStart = uint32(hdkeychain.HardenedKeyStart) + HardenedKeyStart = uint32(hdkeychain.HardenedKeyStart) + WalletDefaultDerivationPath = "m/84'/0'/0'" + LndDerivationPath = "m/1017'/%d'/%d'" ) func DeriveChildren(key *hdkeychain.ExtendedKey, path []uint32) ( @@ -93,6 +95,38 @@ func DeriveKey(extendedKey *hdkeychain.ExtendedKey, path string, return pubKey, wif, nil } +func AllDerivationPaths(params *chaincfg.Params) ([]string, [][]uint32, error) { + mkPath := func(f keychain.KeyFamily) string { + return fmt.Sprintf( + LndDerivationPath, params.HDCoinType, uint32(f), + ) + } + pathStrings := []string{ + "m/44'/0'/0'", + "m/49'/0'/0'", + WalletDefaultDerivationPath, + mkPath(keychain.KeyFamilyMultiSig), + mkPath(keychain.KeyFamilyRevocationBase), + mkPath(keychain.KeyFamilyHtlcBase), + mkPath(keychain.KeyFamilyPaymentBase), + mkPath(keychain.KeyFamilyDelayBase), + mkPath(keychain.KeyFamilyRevocationRoot), + mkPath(keychain.KeyFamilyNodeKey), + mkPath(keychain.KeyFamilyStaticBackup), + mkPath(keychain.KeyFamilyTowerSession), + mkPath(keychain.KeyFamilyTowerID), + } + paths := make([][]uint32, len(pathStrings)) + for idx, path := range pathStrings { + p, err := ParsePath(path) + if err != nil { + return nil, nil, err + } + paths[idx] = p + } + return pathStrings, paths, nil +} + type HDKeyRing struct { ExtendedKey *hdkeychain.ExtendedKey ChainParams *chaincfg.Params