You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
chantools/btc/bitcoind.go

332 lines
8.7 KiB
Go

package btc
import (
"fmt"
"io"
"strings"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/chantools/lnd"
)
const (
FormatCli = "bitcoin-cli"
FormatCliWatchOnly = "bitcoin-cli-watchonly"
FormatImportwallet = "bitcoin-importwallet"
FormatDescriptors = "bitcoin-descriptors"
FormatElectrum = "electrum"
PasteString = "# Paste the following lines into a command line window."
)
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, error) {
switch format {
case FormatCli:
return &Cli{}, nil
case FormatCliWatchOnly:
return &CliWatchOnly{}, nil
case FormatImportwallet:
return &ImportWallet{}, nil
case FormatDescriptors:
return &Descriptors{}, nil
case FormatElectrum:
return &Electrum{}, nil
default:
return nil, fmt.Errorf("invalid format: %s", format)
}
}
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 PasteString
}
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: %w", err)
}
wif, err := btcutil.NewWIF(privKey, params, true)
if err != nil {
return "", fmt.Errorf("could not encode WIF: %w", err)
}
flags := ""
if params.Net == wire.TestNet || params.Net == wire.TestNet3 {
flags = " -testnet"
}
return fmt.Sprintf("bitcoin-cli%s importprivkey %s \"%s/%d/%d/\" false",
flags, 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 PasteString
}
func (c *CliWatchOnly) Format(hdKey *hdkeychain.ExtendedKey,
params *chaincfg.Params, path string, branch, index uint32) (string,
error) {
pubKey, err := hdKey.ECPubKey()
if err != nil {
return "", fmt.Errorf("could not derive private key: %w", err)
}
addrP2PKH, err := lnd.P2PKHAddr(pubKey, params)
if err != nil {
return "", fmt.Errorf("could not create address: %w", err)
}
addrP2WKH, err := lnd.P2WKHAddr(pubKey, params)
if err != nil {
return "", fmt.Errorf("could not create address: %w", err)
}
addrNP2WKH, err := lnd.NP2WKHAddr(pubKey, params)
if err != nil {
return "", fmt.Errorf("could not create address: %w", err)
}
flags := ""
if params.Net == wire.TestNet || params.Net == wire.TestNet3 {
flags = " -testnet"
}
return fmt.Sprintf("bitcoin-cli%s importpubkey %x \"%s/%d/%d/\" "+
"false # addr=%s,%s,%s", flags, pubKey.SerializeCompressed(),
path, branch, index, addrP2PKH, addrP2WKH, addrNP2WKH), 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: %w", err)
}
wif, err := btcutil.NewWIF(privKey, params, true)
if err != nil {
return "", fmt.Errorf("could not encode WIF: %w", err)
}
addrP2PKH, err := lnd.P2PKHAddr(privKey.PubKey(), params)
if err != nil {
return "", fmt.Errorf("could not create address: %w", err)
}
addrP2WKH, err := lnd.P2WKHAddr(privKey.PubKey(), params)
if err != nil {
return "", fmt.Errorf("could not create address: %w", err)
}
addrNP2WKH, err := lnd.NP2WKHAddr(privKey.PubKey(), params)
if err != nil {
return "", fmt.Errorf("could not create address: %w", err)
}
addrP2TR, err := lnd.P2TRAddr(privKey.PubKey(), params)
if err != nil {
return "", fmt.Errorf("could not create address: %w", err)
}
return fmt.Sprintf("%s 1970-01-01T00:00:01Z label=%s/%d/%d/ "+
"# addr=%s,%s,%s,%s", wif.String(), path, branch, index,
addrP2PKH.EncodeAddress(), addrNP2WKH.EncodeAddress(),
addrP2WKH.EncodeAddress(), addrP2TR.EncodeAddress(),
), nil
}
func (i *ImportWallet) Trailer(_ uint32) string {
return ""
}
type Electrum struct{}
func (p *Electrum) Header() string {
return "# Copy the content of this file (without this line) into " +
"Electrum."
}
func (p *Electrum) 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: %w", err)
}
wif, err := btcutil.NewWIF(privKey, params, true)
if err != nil {
return "", fmt.Errorf("could not encode WIF: %w", err)
}
prefix := "p2wpkh"
if strings.HasPrefix(path, lnd.WalletBIP49DerivationPath) {
prefix = "p2wpkh-p2sh"
}
return fmt.Sprintf("%s:%s", prefix, wif.String()), nil
}
func (p *Electrum) Trailer(_ uint32) string {
return ""
}
type Descriptors struct{}
func (d *Descriptors) Header() string {
return PasteString
}
func (d *Descriptors) 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: %w", err)
}
wif, err := btcutil.NewWIF(privKey, params, true)
if err != nil {
return "", fmt.Errorf("could not encode WIF: %w", err)
}
addrP2WKH, err := lnd.P2WKHAddr(privKey.PubKey(), params)
if err != nil {
return "", fmt.Errorf("could not create address: %w", err)
}
addrNP2WKH, err := lnd.NP2WKHAddr(privKey.PubKey(), params)
if err != nil {
return "", fmt.Errorf("could not create address: %w", err)
}
addrP2TR, err := lnd.P2TRAddr(privKey.PubKey(), params)
if err != nil {
return "", fmt.Errorf("could not create address: %w", err)
}
np2wkh := makeDescriptor("sh(wpkh(%s))", wif.String(), addrNP2WKH)
p2wkh := makeDescriptor("wpkh(%s)", wif.String(), addrP2WKH)
p2tr := makeDescriptor("tr(%s)", wif.String(), addrP2TR)
return fmt.Sprintf("bitcoin-cli importdescriptors '[%s,%s,%s]'",
np2wkh, p2wkh, p2tr), nil
}
func (d *Descriptors) Trailer(birthdayBlock uint32) string {
return fmt.Sprintf("bitcoin-cli rescanblockchain %d\n", birthdayBlock)
}
func makeDescriptor(format, wif string, address btcutil.Address) string {
descriptor := fmt.Sprintf(format, wif)
return fmt.Sprintf(
"{\"desc\":\"%s\",\"timestamp\":\"now\",\"label\":\"%s\"}",
DescriptorSumCreate(descriptor),
address.String(),
)
}