Skip to content

Commit 3151369

Browse files
committed
support importdescriptors cmd of Core 0.21
1 parent 7094878 commit 3151369

File tree

6 files changed

+177
-31
lines changed

6 files changed

+177
-31
lines changed

docs/bitcoin-core-usage.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,36 @@ needs a USB connection and additional software such as [HWI](https://github.com/
55

66
## Setup Steps
77

8+
### Bitcoin Core v0.21.0+
9+
10+
As of Coldcard firmware v4.1.3, we recommend using the "importdescriptors"
11+
command with a native descriptor wallet in Core, so Core can generate
12+
and receive PSBT files natively from the GUI. The resulting wallet is
13+
no longer just a watch wallet, but can be used for spending by creating
14+
PSBT files for signing offline at the Coldcard.
15+
16+
Step 1: Create a new descriptor-based wallet in Bitcoin Core
17+
18+
- File -> Create Wallet ...
19+
- give it a unique name
20+
- check "Descriptor Wallet"
21+
22+
Step 2: Export descriptor from Coldcard to Core
23+
24+
- on Coldcard, go to Advanced -> MicroSD card -> Export Wallet -> Bitcoin Core
25+
- on your computer, open `bitcoin-core-XX.txt`, copy the `importdescriptor` command line
26+
- in Bitcoin Core, go to Windows -> Console
27+
- select your newly created descriptor wallet in the wallet pulldown (top left)
28+
- paste the `importdescriptor` command. It should respond with a success message
29+
30+
NOTE: If you are importing an existing wallet this way, with UTXO on the blockchain,
31+
you may need to rescan and/or delete "timestamp=now" from the command. If the
32+
balance is zero this is why.
33+
834
### Bitcoin Core v0.19.0+
935

36+
(no longer recommended)
37+
1038
For compatibility with other wallet software we use the BIP84 address derivation
1139
(m/84'/0'/{account}'/{change}/{index}) and native SegWit (bech32) addresses. It's
1240
recommended to set `addresstype=bech32` in [bitcoin.conf](https://github.com/bitcoin/bitcoin/blob/9546a785953b7f61a3a50e2175283cbf30bc2151/doc/bitcoin-conf.md).
@@ -19,8 +47,8 @@ The public keys can exported via an SD card, or via USB.
1947

2048
To export via SD card:
2149

22-
- go to Advanced -> MicroSD card -> Bitcoin Core
23-
- on your computer open public.txt, copy the `importmulti` command
50+
- go to Advanced -> MicroSD card -> Export Wallet -> Bitcoin Core
51+
- on your computer open `bitcoin-core-XX.txt`, copy the `importmulti` command line
2452
- in Bitcoin Core, go to Windows -> Console
2553
- select Coldcard in the wallet dropdown
2654
- paste the `importmulti` command. It should respond with a success message
@@ -46,6 +74,11 @@ createwallet Coldcard true
4674

4775
## Day-to-day Operation
4876

77+
### Bitcoin Core v0.21.0+
78+
79+
PSBT files can be directly created and loaded from the Bitcoin Core Qt GUI! HWI is not
80+
required, and air-gap via MicroSD is easy to use.
81+
4982
### Bitcoin Core v0.18.0+
5083

5184
See HWI [instructions for usage](https://github.com/bitcoin-core/HWI/blob/master/docs/bitcoin-core-usage.md#usage).

releases/ChangeLog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## 4.1.3 - Sep 2, 2021
22

3+
- Enhancement: support "importdescriptors" command in Bitcoin Core 0.21 so that
4+
a descriptor-based wallet is created. PSBT files are then supported natively by
5+
Core, and the resulting desktop wallet can be used for spending (ie. create PSBT
6+
via GUI) and also watching. Translation: Easy air-gap PSBT operation with Bitcoin Core!
37
- Enhancement: remove "m/0/0" derivations from public.txt and address explorer,
48
since that path is obsolete and not used by any major wallets now. We can still
59
sign PSBT files with that path, but it's an unnecessary risk to show derived

shared/export.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,14 @@ async def make_bitcoin_core_wallet(account_num=0, fname_pattern='bitcoin-core.tx
162162

163163
# make the data
164164
examples = []
165-
payload = ujson.dumps(list(generate_bitcoin_core_wallet(examples, account_num)))
165+
imp_multi = []
166+
imp_desc = []
167+
for a,b in generate_bitcoin_core_wallet(account_num, examples):
168+
imp_multi.append(a)
169+
imp_desc.append(b)
170+
171+
imp_multi = ujson.dumps(imp_multi)
172+
imp_desc = ujson.dumps(imp_desc)
166173

167174
body = '''\
168175
# Bitcoin Core Wallet Import File
@@ -178,19 +185,26 @@ async def make_bitcoin_core_wallet(account_num=0, fname_pattern='bitcoin-core.tx
178185
The following command can be entered after opening Window -> Console
179186
in Bitcoin Core, or using bitcoin-cli:
180187
181-
importmulti '{payload}'
188+
importdescriptors '{imp_desc}'
189+
190+
### Bitcoin Core before v0.21.0
191+
192+
This command can be used on older versions, but it is not as robust
193+
and "importdescriptors" should be prefered if possible:
194+
195+
importmulti '{imp_multi}'
182196
183197
## Resulting Addresses (first 3)
184198
185-
'''.format(payload=payload, xfp=xfp, nb=chains.current_chain().name)
199+
'''.format(imp_multi=imp_multi, imp_desc=imp_desc, xfp=xfp, nb=chains.current_chain().name)
186200

187201
body += '\n'.join('%s => %s' % t for t in examples)
188202

189203
body += '\n'
190204

191205
await write_text_file(fname_pattern, body, 'Bitcoin Core')
192206

193-
def generate_bitcoin_core_wallet(example_addrs, account_num):
207+
def generate_bitcoin_core_wallet(account_num, example_addrs):
194208
# Generate the data for an RPC command to import keys into Bitcoin Core
195209
# - yields dicts for json purposes
196210
from descriptor import append_checksum
@@ -226,17 +240,32 @@ def generate_bitcoin_core_wallet(example_addrs, account_num):
226240
coin_type=chain.b44_cointype,
227241
account=0,
228242
xpub=xpub,
229-
change=(1 if internal else 0))
243+
change=(1 if internal else 0) )
244+
245+
desc = append_checksum(desc)
230246

231-
yield {
232-
'desc': append_checksum(desc),
247+
# for importmulti
248+
imm = {
249+
'desc': desc,
233250
'range': [0, 1000],
234251
'timestamp': 'now',
235252
'internal': internal,
236253
'keypool': True,
237254
'watchonly': True
238255
}
239256

257+
# for importdescriptors
258+
imd = {
259+
'desc': desc,
260+
'active': True,
261+
'timestamp': 'now',
262+
'internal': internal,
263+
}
264+
if not internal:
265+
imd['label'] = "Coldcard " + txt_xfp
266+
267+
yield (imm, imd)
268+
240269
def generate_wasabi_wallet():
241270
# Generate the data for a JSON file which Wasabi can open directly as a new wallet.
242271
import ustruct, version

testing/api.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def doit(*parts):
112112
@pytest.fixture(scope='function')
113113
def bitcoind_wallet(bitcoind):
114114
# Use bitcoind to create a temporary wallet file, and then do cleanup after
115-
# - wallet will not have any keys, and is watch only
115+
# - wallet will not have any keys, and is watch-only
116116
import os, shutil
117117

118118
fname = '/tmp/ckcc-test-wallet-%d' % os.getpid()
@@ -137,5 +137,38 @@ def bitcoind_wallet(bitcoind):
137137
assert fname.startswith('/tmp/ckcc-test-wallet')
138138
shutil.rmtree(fname)
139139

140+
@pytest.fixture(scope='function')
141+
def bitcoind_d_wallet(bitcoind):
142+
# Use bitcoind to create a temporary DESCRIPTOR-based wallet file, and then do cleanup after
143+
# - wallet will not have any keys until a descriptor is added, and is not just watch-only
144+
import os, shutil
145+
146+
fname = '/tmp/ckcc-test-desc-wallet-%d' % os.getpid()
147+
148+
disable_private_keys = True
149+
blank = True
150+
password = None
151+
avoid_reuse = False
152+
descriptors = True
153+
w = bitcoind.createwallet(fname, disable_private_keys, blank,
154+
password, avoid_reuse, descriptors)
155+
156+
assert w['name'] == fname
157+
158+
# give them an object they can do API calls w/ rpcwallet filled-in
159+
cookie = get_cookie()
160+
url = 'http://' + cookie + '@' + URL + '/wallet/' + fname.replace('/', '%2f')
161+
#print(url)
162+
conn = AuthServiceProxy(url)
163+
assert conn.getblockchaininfo()['chain'] == 'test'
164+
165+
yield conn
166+
167+
# cleanup
168+
bitcoind.unloadwallet(fname)
169+
assert fname.startswith('/tmp/ckcc-test-desc-wallet')
170+
shutil.rmtree(fname)
171+
172+
140173

141174
# EOF

testing/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from ckcc.protocol import CCProtocolPacker, CCProtoError
66
from helpers import B2A, U2SAT, prandom
77
from api import bitcoind, match_key, bitcoind_finalizer, bitcoind_analyze, bitcoind_decode, explora
8-
from api import bitcoind_wallet
8+
from api import bitcoind_wallet, bitcoind_d_wallet
99
from binascii import b2a_hex, a2b_hex
1010
from constants import *
1111

testing/test_export.py

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
import json
1313
from conftest import simulator_fixed_xfp, simulator_fixed_xprv
1414
from ckcc_protocol.constants import AF_CLASSIC, AF_P2WPKH, AF_P2WSH_P2SH
15+
from pprint import pprint
1516

1617
@pytest.mark.parametrize('acct_num', [ None, '0', '99', '123'])
17-
def test_export_core(dev, acct_num, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress, microsd_path, bitcoind_wallet):
18+
def test_export_core(dev, acct_num, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress, microsd_path, bitcoind_wallet, bitcoind_d_wallet):
1819
# test UX and operation of the 'bitcoin core' wallet export
1920
from pycoin.contrib.segwit_addr import encode as sw_encode
2021

@@ -51,14 +52,21 @@ def test_export_core(dev, acct_num, cap_menu, pick_menu_item, goto_home, cap_sto
5152

5253
path = microsd_path(fname)
5354
addrs = []
54-
js = None
55+
imm_js = None
56+
imd_js = None
5557
with open(path, 'rt') as fp:
5658
for ln in fp:
5759
if 'importmulti' in ln:
60+
# PLAN: this will become obsolete
5861
assert ln.startswith("importmulti '")
5962
assert ln.endswith("'\n")
60-
assert not js, "dup importmulti lines"
61-
js = ln[13:-2]
63+
assert not imm_js, "dup importmulti lines"
64+
imm_js = ln[13:-2]
65+
elif "importdescriptors '" in ln:
66+
assert ln.startswith("importdescriptors '")
67+
assert ln.endswith("'\n")
68+
assert not imd_js, "dup importdesc lines"
69+
imd_js = ln[19:-2]
6270
elif '=>' in ln:
6371
path, addr = ln.strip().split(' => ', 1)
6472
assert path.startswith(f"m/84'/1'/{acct_num}'/0")
@@ -70,15 +78,51 @@ def test_export_core(dev, acct_num, cap_menu, pick_menu_item, goto_home, cap_sto
7078

7179
assert len(addrs) == 3
7280

73-
obj = json.loads(js)
7481
xfp = xfp2str(simulator_fixed_xfp).lower()
7582

83+
if imm_js:
84+
obj = json.loads(imm_js)
85+
for n, here in enumerate(obj):
86+
assert here['range'] == [0, 1000]
87+
assert here['timestamp'] == 'now'
88+
assert here['internal'] == bool(n)
89+
assert here['keypool'] == True
90+
assert here['watchonly'] == True
91+
92+
d = here['desc']
93+
desc, chk = d.split('#', 1)
94+
assert len(chk) == 8
95+
assert desc.startswith(f'wpkh([{xfp}/84h/1h/{acct_num}h]')
96+
97+
expect = BIP32Node.from_wallet_key(simulator_fixed_xprv)\
98+
.subkey_for_path(f"84'/1'/{acct_num}'.pub").hwif()
99+
100+
assert expect in desc
101+
assert expect+f'/{n}/*' in desc
102+
103+
# test against bitcoind
104+
for x in obj:
105+
x['label'] = 'testcase'
106+
bitcoind_wallet.importmulti(obj)
107+
x = bitcoind_wallet.getaddressinfo(addrs[-1])
108+
pprint(x)
109+
assert x['address'] == addrs[-1]
110+
if 'label' in x:
111+
# pre 0.21.?
112+
assert x['label'] == 'testcase'
113+
else:
114+
assert x['labels'] == ['testcase']
115+
assert x['iswatchonly'] == True
116+
assert x['iswitness'] == True
117+
assert x['hdkeypath'] == f"m/84'/1'/{acct_num}'/0/%d" % (len(addrs)-1)
118+
119+
# importdescriptors -- its better
120+
assert imd_js
121+
obj = json.loads(imd_js)
76122
for n, here in enumerate(obj):
77-
assert here['range'] == [0, 1000]
123+
assert range not in here
78124
assert here['timestamp'] == 'now'
79125
assert here['internal'] == bool(n)
80-
assert here['keypool'] == True
81-
assert here['watchonly'] == True
82126

83127
d = here['desc']
84128
desc, chk = d.split('#', 1)
@@ -91,18 +135,21 @@ def test_export_core(dev, acct_num, cap_menu, pick_menu_item, goto_home, cap_sto
91135
assert expect in desc
92136
assert expect+f'/{n}/*' in desc
93137

94-
# test against bitcoind
95-
for x in obj:
96-
x['label'] = 'testcase'
97-
bitcoind_wallet.importmulti(obj)
98-
x = bitcoind_wallet.getaddressinfo(addrs[-1])
99-
from pprint import pprint
100-
pprint(x)
101-
assert x['address'] == addrs[-1]
102-
assert x['label'] == 'testcase'
103-
assert x['iswatchonly'] == True
104-
assert x['iswitness'] == True
105-
assert x['hdkeypath'] == f"m/84'/1'/{acct_num}'/0/%d" % (len(addrs)-1)
138+
if n == 0:
139+
assert here['label'] == 'Coldcard ' + xfp
140+
141+
# test against bitcoind -- needs a "descriptor native" wallet
142+
bitcoind_d_wallet.importdescriptors(obj)
143+
144+
x = bitcoind_d_wallet.getaddressinfo(addrs[-1])
145+
pprint(x)
146+
assert x['address'] == addrs[-1]
147+
assert x['iswatchonly'] == False
148+
assert x['iswitness'] == True
149+
assert x['ismine'] == True
150+
assert x['solvable'] == True
151+
assert x['hdmasterfingerprint'] == xfp2str(dev.master_fingerprint).lower()
152+
#assert x['hdkeypath'] == f"m/84'/1'/{acct_num}'/0/%d" % (len(addrs)-1)
106153

107154
def test_export_wasabi(dev, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress, microsd_path):
108155
# test UX and operation of the 'wasabi wallet export'

0 commit comments

Comments
 (0)