From 9fadec97a7c3e83db15ebf173ebce4101dc1ba8d Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 21 Apr 2023 11:02:11 +0200 Subject: [PATCH] multi: add bitcoin-descriptors to genimportscript --- btc/bitcoind.go | 50 ++++++++++++++++--- btc/descriptors.go | 83 ++++++++++++++++++++++++++++++++ btc/descriptors_test.go | 27 +++++++++++ cmd/chantools/genimportscript.go | 14 ++++-- doc/chantools_genimportscript.md | 11 ++++- 5 files changed, 173 insertions(+), 12 deletions(-) create mode 100644 btc/descriptors.go create mode 100644 btc/descriptors_test.go diff --git a/btc/bitcoind.go b/btc/bitcoind.go index 7fd78c2..b5cf3e2 100644 --- a/btc/bitcoind.go +++ b/btc/bitcoind.go @@ -17,6 +17,7 @@ const ( FormatCli = "bitcoin-cli" FormatCliWatchOnly = "bitcoin-cli-watchonly" FormatImportwallet = "bitcoin-importwallet" + FormatDescriptors = "bitcoin-descriptors" FormatElectrum = "electrum" ) @@ -40,6 +41,9 @@ func ParseFormat(format string) (KeyExporter, error) { case FormatImportwallet: return &ImportWallet{}, nil + case FormatDescriptors: + return &Descriptors{}, nil + case FormatElectrum: return &Electrum{}, nil @@ -179,19 +183,14 @@ func (c *CliWatchOnly) Format(hdKey *hdkeychain.ExtendedKey, if err != nil { return "", fmt.Errorf("could not create address: %w", err) } - addrP2TR, err := lnd.P2TRAddr(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,%s", flags, pubKey.SerializeCompressed(), - path, branch, index, addrP2PKH, addrP2WKH, addrNP2WKH, - addrP2TR), nil + "false # addr=%s,%s,%s", flags, pubKey.SerializeCompressed(), + path, branch, index, addrP2PKH, addrP2WKH, addrNP2WKH), nil } func (c *CliWatchOnly) Trailer(birthdayBlock uint32) string { @@ -276,3 +275,40 @@ func (p *Electrum) Format(hdKey *hdkeychain.ExtendedKey, func (p *Electrum) Trailer(_ uint32) string { return "" } + +type Descriptors struct{} + +func (d *Descriptors) Header() string { + return "# Paste the following lines into a command line window." +} + +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) + } + + np2wkh := makeDescriptor("sh(wpkh(%s))", wif.String()) + p2wkh := makeDescriptor("wpkh(%s)", wif.String()) + p2tr := makeDescriptor("tr(%s)", wif.String()) + + 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) string { + descriptor := fmt.Sprintf(format, wif) + return fmt.Sprintf("{\"desc\":\"%s\",\"timestamp\":\"now\"}", + DescriptorSumCreate(descriptor)) +} diff --git a/btc/descriptors.go b/btc/descriptors.go new file mode 100644 index 0000000..1d2afcd --- /dev/null +++ b/btc/descriptors.go @@ -0,0 +1,83 @@ +package btc + +import ( + "strings" +) + +var ( + inputCharset = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ" + + "&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\\\"\\\\ " + checksumCharset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + generator = []uint64{ + 0xf5dee51989, 0xa9fdca3312, 0x1bab10e32d, 0x3706b1677a, + 0x644d626ffd, + } +) + +func descriptorSumPolymod(symbols []uint64) uint64 { + chk := uint64(1) + for _, value := range symbols { + top := chk >> 35 + chk = (chk&0x7ffffffff)<<5 ^ value + for i := 0; i < 5; i++ { + if (top>>i)&1 != 0 { + chk ^= generator[i] + } + } + } + return chk +} + +func descriptorSumExpand(s string) []uint64 { + groups := []uint64{} + symbols := []uint64{} + for _, c := range s { + v := strings.IndexRune(inputCharset, c) + if v < 0 { + return nil + } + symbols = append(symbols, uint64(v&31)) + groups = append(groups, uint64(v>>5)) + if len(groups) == 3 { + symbols = append( + symbols, groups[0]*9+groups[1]*3+groups[2], + ) + groups = []uint64{} + } + } + if len(groups) == 1 { + symbols = append(symbols, groups[0]) + } else if len(groups) == 2 { + symbols = append(symbols, groups[0]*3+groups[1]) + } + return symbols +} + +func DescriptorSumCreate(s string) string { + symbols := append(descriptorSumExpand(s), 0, 0, 0, 0, 0, 0, 0, 0) + checksum := descriptorSumPolymod(symbols) ^ 1 + builder := strings.Builder{} + for i := 0; i < 8; i++ { + builder.WriteByte(checksumCharset[(checksum>>(5*(7-i)))&31]) + } + return s + "#" + builder.String() +} + +func DescriptorSumCheck(s string, require bool) bool { + if !strings.Contains(s, "#") { + return !require + } + if s[len(s)-9] != '#' { + return false + } + for _, c := range s[len(s)-8:] { + if !strings.ContainsRune(checksumCharset, c) { + return false + } + } + symbols := append( + descriptorSumExpand(s[:len(s)-9]), + uint64(strings.Index(checksumCharset, s[len(s)-8:])), + ) + return descriptorSumPolymod(symbols) == 1 +} diff --git a/btc/descriptors_test.go b/btc/descriptors_test.go new file mode 100644 index 0000000..6f72228 --- /dev/null +++ b/btc/descriptors_test.go @@ -0,0 +1,27 @@ +package btc + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +var testCases = []struct { + descriptor string + expectedSum string +}{{ + descriptor: "addr(mkmZxiEcEd8ZqjQWVZuC6so5dFMKEFpN2j)", + expectedSum: "#02wpgw69", +}, { + descriptor: "tr(cRhCT5vC5NdnSrQ2Jrah6NPCcth41uT8DWFmA6uD8R4x2ufucnYX)", + expectedSum: "#gwfmkgga", +}} + +func TestDescriptorSum(t *testing.T) { + for _, tc := range testCases { + sum := DescriptorSumCreate(tc.descriptor) + require.Equal(t, tc.descriptor+tc.expectedSum, sum) + + DescriptorSumCheck(sum, true) + } +} diff --git a/cmd/chantools/genimportscript.go b/cmd/chantools/genimportscript.go index 7e9cff5..b8bf72d 100644 --- a/cmd/chantools/genimportscript.go +++ b/cmd/chantools/genimportscript.go @@ -41,7 +41,9 @@ 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. + in those private keys. NOTE: This will only work for legacy wallets and only + for legacy, p2sh-segwit and bech32 (p2pkh, np2wkh and p2wkh) addresses. Use + bitcoin-descriptors and a descriptor wallet for bech32m (p2tr). * 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 @@ -49,7 +51,12 @@ The following script formats are currently supported: * bitcoin-importwallet: Creates a text output that is compatible with bitcoind's importwallet command. * electrum: Creates a text output that contains one private key per line with - the address type as the prefix, the way Electrum expects them.`, + the address type as the prefix, the way Electrum expects them. +* bitcoin-descriptors: Create a list of bitcoin-cli importdescriptors commands + that can be used in combination with a bitcoind full node that has a + descriptor wallet to recover the funds locked in those private keys. + NOTE: This will only work for descriptor wallets and only for + p2sh-segwit, bech32 and bech32m (np2wkh, p2wkh and p2tr) addresses.`, Example: `chantools genimportscript --format bitcoin-cli \ --recoverywindow 5000`, RunE: cc.Execute, @@ -58,7 +65,8 @@ The following script formats are currently supported: &cc.Format, "format", "bitcoin-importwallet", "format of the "+ "generated import script; currently supported are: "+ "bitcoin-importwallet, bitcoin-cli, "+ - "bitcoin-cli-watchonly and electrum", + "bitcoin-cli-watchonly, bitcoin-descriptors and "+ + "electrum", ) cc.cmd.Flags().BoolVar( &cc.LndPaths, "lndpaths", false, "use all derivation paths "+ diff --git a/doc/chantools_genimportscript.md b/doc/chantools_genimportscript.md index 6e48556..1ba5ea4 100644 --- a/doc/chantools_genimportscript.md +++ b/doc/chantools_genimportscript.md @@ -11,7 +11,9 @@ 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. + in those private keys. NOTE: This will only work for legacy wallets and only + for legacy, p2sh-segwit and bech32 (p2pkh, np2wkh and p2wkh) addresses. Use + bitcoin-descriptors and a descriptor wallet for bech32m (p2tr). * 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 @@ -20,6 +22,11 @@ The following script formats are currently supported: bitcoind's importwallet command. * electrum: Creates a text output that contains one private key per line with the address type as the prefix, the way Electrum expects them. +* bitcoin-descriptors: Create a list of bitcoin-cli importdescriptors commands + that can be used in combination with a bitcoind full node that has a + descriptor wallet to recover the funds locked in those private keys. + NOTE: This will only work for descriptor wallets and only for + p2sh-segwit, bech32 and bech32m (np2wkh, p2wkh and p2tr) addresses. ``` chantools genimportscript [flags] @@ -37,7 +44,7 @@ chantools genimportscript --format bitcoin-cli \ ``` --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag --derivationpath string 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 - --format string format of the generated import script; currently supported are: bitcoin-importwallet, bitcoin-cli, bitcoin-cli-watchonly and electrum (default "bitcoin-importwallet") + --format string format of the generated import script; currently supported are: bitcoin-importwallet, bitcoin-cli, bitcoin-cli-watchonly, bitcoin-descriptors and electrum (default "bitcoin-importwallet") -h, --help help for genimportscript --lndpaths use all derivation paths that lnd used; results in a large number of results; cannot be used in conjunction with --derivationpath --recoverywindow uint32 number of keys to scan per internal/external branch; output will consist of double this amount of keys (default 2500)