Skip to content

Commit 9fadec9

Browse files
committed
multi: add bitcoin-descriptors to genimportscript
1 parent f35a469 commit 9fadec9

File tree

5 files changed

+173
-12
lines changed

5 files changed

+173
-12
lines changed

btc/bitcoind.go

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const (
1717
FormatCli = "bitcoin-cli"
1818
FormatCliWatchOnly = "bitcoin-cli-watchonly"
1919
FormatImportwallet = "bitcoin-importwallet"
20+
FormatDescriptors = "bitcoin-descriptors"
2021
FormatElectrum = "electrum"
2122
)
2223

@@ -40,6 +41,9 @@ func ParseFormat(format string) (KeyExporter, error) {
4041
case FormatImportwallet:
4142
return &ImportWallet{}, nil
4243

44+
case FormatDescriptors:
45+
return &Descriptors{}, nil
46+
4347
case FormatElectrum:
4448
return &Electrum{}, nil
4549

@@ -179,19 +183,14 @@ func (c *CliWatchOnly) Format(hdKey *hdkeychain.ExtendedKey,
179183
if err != nil {
180184
return "", fmt.Errorf("could not create address: %w", err)
181185
}
182-
addrP2TR, err := lnd.P2TRAddr(pubKey, params)
183-
if err != nil {
184-
return "", fmt.Errorf("could not create address: %w", err)
185-
}
186186

187187
flags := ""
188188
if params.Net == wire.TestNet || params.Net == wire.TestNet3 {
189189
flags = " -testnet"
190190
}
191191
return fmt.Sprintf("bitcoin-cli%s importpubkey %x \"%s/%d/%d/\" "+
192-
"false # addr=%s,%s,%s,%s", flags, pubKey.SerializeCompressed(),
193-
path, branch, index, addrP2PKH, addrP2WKH, addrNP2WKH,
194-
addrP2TR), nil
192+
"false # addr=%s,%s,%s", flags, pubKey.SerializeCompressed(),
193+
path, branch, index, addrP2PKH, addrP2WKH, addrNP2WKH), nil
195194
}
196195

197196
func (c *CliWatchOnly) Trailer(birthdayBlock uint32) string {
@@ -276,3 +275,40 @@ func (p *Electrum) Format(hdKey *hdkeychain.ExtendedKey,
276275
func (p *Electrum) Trailer(_ uint32) string {
277276
return ""
278277
}
278+
279+
type Descriptors struct{}
280+
281+
func (d *Descriptors) Header() string {
282+
return "# Paste the following lines into a command line window."
283+
}
284+
285+
func (d *Descriptors) Format(hdKey *hdkeychain.ExtendedKey,
286+
params *chaincfg.Params, path string, branch, index uint32) (string,
287+
error) {
288+
289+
privKey, err := hdKey.ECPrivKey()
290+
if err != nil {
291+
return "", fmt.Errorf("could not derive private key: %w", err)
292+
}
293+
wif, err := btcutil.NewWIF(privKey, params, true)
294+
if err != nil {
295+
return "", fmt.Errorf("could not encode WIF: %w", err)
296+
}
297+
298+
np2wkh := makeDescriptor("sh(wpkh(%s))", wif.String())
299+
p2wkh := makeDescriptor("wpkh(%s)", wif.String())
300+
p2tr := makeDescriptor("tr(%s)", wif.String())
301+
302+
return fmt.Sprintf("bitcoin-cli importdescriptors '[%s,%s,%s]'",
303+
np2wkh, p2wkh, p2tr), nil
304+
}
305+
306+
func (d *Descriptors) Trailer(birthdayBlock uint32) string {
307+
return fmt.Sprintf("bitcoin-cli rescanblockchain %d\n", birthdayBlock)
308+
}
309+
310+
func makeDescriptor(format, wif string) string {
311+
descriptor := fmt.Sprintf(format, wif)
312+
return fmt.Sprintf("{\"desc\":\"%s\",\"timestamp\":\"now\"}",
313+
DescriptorSumCreate(descriptor))
314+
}

btc/descriptors.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package btc
2+
3+
import (
4+
"strings"
5+
)
6+
7+
var (
8+
inputCharset = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ" +
9+
"&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\\\"\\\\ "
10+
checksumCharset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
11+
generator = []uint64{
12+
0xf5dee51989, 0xa9fdca3312, 0x1bab10e32d, 0x3706b1677a,
13+
0x644d626ffd,
14+
}
15+
)
16+
17+
func descriptorSumPolymod(symbols []uint64) uint64 {
18+
chk := uint64(1)
19+
for _, value := range symbols {
20+
top := chk >> 35
21+
chk = (chk&0x7ffffffff)<<5 ^ value
22+
for i := 0; i < 5; i++ {
23+
if (top>>i)&1 != 0 {
24+
chk ^= generator[i]
25+
}
26+
}
27+
}
28+
return chk
29+
}
30+
31+
func descriptorSumExpand(s string) []uint64 {
32+
groups := []uint64{}
33+
symbols := []uint64{}
34+
for _, c := range s {
35+
v := strings.IndexRune(inputCharset, c)
36+
if v < 0 {
37+
return nil
38+
}
39+
symbols = append(symbols, uint64(v&31))
40+
groups = append(groups, uint64(v>>5))
41+
if len(groups) == 3 {
42+
symbols = append(
43+
symbols, groups[0]*9+groups[1]*3+groups[2],
44+
)
45+
groups = []uint64{}
46+
}
47+
}
48+
if len(groups) == 1 {
49+
symbols = append(symbols, groups[0])
50+
} else if len(groups) == 2 {
51+
symbols = append(symbols, groups[0]*3+groups[1])
52+
}
53+
return symbols
54+
}
55+
56+
func DescriptorSumCreate(s string) string {
57+
symbols := append(descriptorSumExpand(s), 0, 0, 0, 0, 0, 0, 0, 0)
58+
checksum := descriptorSumPolymod(symbols) ^ 1
59+
builder := strings.Builder{}
60+
for i := 0; i < 8; i++ {
61+
builder.WriteByte(checksumCharset[(checksum>>(5*(7-i)))&31])
62+
}
63+
return s + "#" + builder.String()
64+
}
65+
66+
func DescriptorSumCheck(s string, require bool) bool {
67+
if !strings.Contains(s, "#") {
68+
return !require
69+
}
70+
if s[len(s)-9] != '#' {
71+
return false
72+
}
73+
for _, c := range s[len(s)-8:] {
74+
if !strings.ContainsRune(checksumCharset, c) {
75+
return false
76+
}
77+
}
78+
symbols := append(
79+
descriptorSumExpand(s[:len(s)-9]),
80+
uint64(strings.Index(checksumCharset, s[len(s)-8:])),
81+
)
82+
return descriptorSumPolymod(symbols) == 1
83+
}

btc/descriptors_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package btc
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
var testCases = []struct {
10+
descriptor string
11+
expectedSum string
12+
}{{
13+
descriptor: "addr(mkmZxiEcEd8ZqjQWVZuC6so5dFMKEFpN2j)",
14+
expectedSum: "#02wpgw69",
15+
}, {
16+
descriptor: "tr(cRhCT5vC5NdnSrQ2Jrah6NPCcth41uT8DWFmA6uD8R4x2ufucnYX)",
17+
expectedSum: "#gwfmkgga",
18+
}}
19+
20+
func TestDescriptorSum(t *testing.T) {
21+
for _, tc := range testCases {
22+
sum := DescriptorSumCreate(tc.descriptor)
23+
require.Equal(t, tc.descriptor+tc.expectedSum, sum)
24+
25+
DescriptorSumCheck(sum, true)
26+
}
27+
}

cmd/chantools/genimportscript.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,22 @@ imported into other software like bitcoind.
4141
The following script formats are currently supported:
4242
* bitcoin-cli: Creates a list of bitcoin-cli importprivkey commands that can
4343
be used in combination with a bitcoind full node to recover the funds locked
44-
in those private keys.
44+
in those private keys. NOTE: This will only work for legacy wallets and only
45+
for legacy, p2sh-segwit and bech32 (p2pkh, np2wkh and p2wkh) addresses. Use
46+
bitcoin-descriptors and a descriptor wallet for bech32m (p2tr).
4547
* bitcoin-cli-watchonly: Does the same as bitcoin-cli but with the
4648
bitcoin-cli importpubkey command. That means, only the public keys are
4749
imported into bitcoind to watch the UTXOs of those keys. The funds cannot be
4850
spent that way as they are watch-only.
4951
* bitcoin-importwallet: Creates a text output that is compatible with
5052
bitcoind's importwallet command.
5153
* electrum: Creates a text output that contains one private key per line with
52-
the address type as the prefix, the way Electrum expects them.`,
54+
the address type as the prefix, the way Electrum expects them.
55+
* bitcoin-descriptors: Create a list of bitcoin-cli importdescriptors commands
56+
that can be used in combination with a bitcoind full node that has a
57+
descriptor wallet to recover the funds locked in those private keys.
58+
NOTE: This will only work for descriptor wallets and only for
59+
p2sh-segwit, bech32 and bech32m (np2wkh, p2wkh and p2tr) addresses.`,
5360
Example: `chantools genimportscript --format bitcoin-cli \
5461
--recoverywindow 5000`,
5562
RunE: cc.Execute,
@@ -58,7 +65,8 @@ The following script formats are currently supported:
5865
&cc.Format, "format", "bitcoin-importwallet", "format of the "+
5966
"generated import script; currently supported are: "+
6067
"bitcoin-importwallet, bitcoin-cli, "+
61-
"bitcoin-cli-watchonly and electrum",
68+
"bitcoin-cli-watchonly, bitcoin-descriptors and "+
69+
"electrum",
6270
)
6371
cc.cmd.Flags().BoolVar(
6472
&cc.LndPaths, "lndpaths", false, "use all derivation paths "+

doc/chantools_genimportscript.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ imported into other software like bitcoind.
1111
The following script formats are currently supported:
1212
* bitcoin-cli: Creates a list of bitcoin-cli importprivkey commands that can
1313
be used in combination with a bitcoind full node to recover the funds locked
14-
in those private keys.
14+
in those private keys. NOTE: This will only work for legacy wallets and only
15+
for legacy, p2sh-segwit and bech32 (p2pkh, np2wkh and p2wkh) addresses. Use
16+
bitcoin-descriptors and a descriptor wallet for bech32m (p2tr).
1517
* bitcoin-cli-watchonly: Does the same as bitcoin-cli but with the
1618
bitcoin-cli importpubkey command. That means, only the public keys are
1719
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:
2022
bitcoind's importwallet command.
2123
* electrum: Creates a text output that contains one private key per line with
2224
the address type as the prefix, the way Electrum expects them.
25+
* bitcoin-descriptors: Create a list of bitcoin-cli importdescriptors commands
26+
that can be used in combination with a bitcoind full node that has a
27+
descriptor wallet to recover the funds locked in those private keys.
28+
NOTE: This will only work for descriptor wallets and only for
29+
p2sh-segwit, bech32 and bech32m (np2wkh, p2wkh and p2tr) addresses.
2330

2431
```
2532
chantools genimportscript [flags]
@@ -37,7 +44,7 @@ chantools genimportscript --format bitcoin-cli \
3744
```
3845
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
3946
--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
40-
--format string format of the generated import script; currently supported are: bitcoin-importwallet, bitcoin-cli, bitcoin-cli-watchonly and electrum (default "bitcoin-importwallet")
47+
--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")
4148
-h, --help help for genimportscript
4249
--lndpaths use all derivation paths that lnd used; results in a large number of results; cannot be used in conjunction with --derivationpath
4350
--recoverywindow uint32 number of keys to scan per internal/external branch; output will consist of double this amount of keys (default 2500)

0 commit comments

Comments
 (0)