Refactor genimportscript command

pull/17/head
Oliver Gugger 4 years ago
parent 2521b8d3bd
commit 4c92de59e5
No known key found for this signature in database
GPG Key ID: 8E4256593F177720

@ -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)
```

@ -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 (<DerivationPath>/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 (<DerivationPath>/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 ""
}

@ -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 (<DerivationPath>/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 (<DerivationPath>/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)
}

@ -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

Loading…
Cancel
Save