Skip to content

Commit 27f3218

Browse files
authored
HD wallet (#1405)
* HD wallet Minimal set of changes (no refactoring) backported from Bitcoin upstream to make HD wallets work in Dash 0.12.1.x+ * minimal bip44 (hardcoded account and change) * minimal bip39 Additional cmd-line options for new wallet: -mnemonic -mnemonicpassphrase * Do not recreate HD wallet on encryption Adjusted keypool.py test * Do not store any private keys for hd wallet besides the master one Derive all keys on the fly. Original idea/implementation - btc PR9298, backported and improved * actually use bip39 * pbkdf2 test * backport wallet-hd.py test * Allow specifying hd seed, add dumphdseed rpc, fix bugs - -hdseed cmd-line param to specify HD seed on wallet creation - dumphdseed rpc to dump HD seed - allow seed of any size - fix dumpwallet rpc bug (wasn't decrypting HD seed) - print HD seed and extended public masterkey on dumpwallet * top up keypool on HD wallet encryption * split HD chain: external/internal * add missing cs_wallet lock in init.cpp * fix `const char *` issues (use strings) * default mnemonic passphrase is an empty string in all cases * store mnemonic/mnemonicpassphrase replace dumphdseed with dumphdinfo * Add fCrypted flag to CHDChain * prepare internal structures for multiple HD accounts (plus some code cleanup) * use secure allocator for storing sensitive HD data * use secure strings for mnemonic(passphrase) * small fix in GenerateNewHDChain * use 24 words for mnemonic by default * make sure mnemonic passphrase provided by user does not exceed 256 symbols * more usage of secure allocators and memory_cleanse * code cleanup * rename: CSecureVector -> SecureVector * add missing include * fix warning in rpcdump.cpp * refactor mnemonic_check (also fix a bug) * move bip39 functions to CMnemonic * Few fixes for CMnemonic: - use `SecureVector` for data, bits, seed - `Check` should return bool * init vectors with desired size where possible
1 parent 68e858f commit 27f3218

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+4189
-142
lines changed

qa/pull-tester/rpc-tests.py

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
testScripts = [
9191
'bip68-112-113-p2p.py',
9292
'wallet.py',
93+
'wallet-hd.py',
9394
'listtransactions.py',
9495
'receivedby.py',
9596
'mempool_resurrect_test.py',

qa/rpc-tests/fundrawtransaction.py

+2
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@ def run_test(self):
459459

460460
# drain the keypool
461461
self.nodes[1].getnewaddress()
462+
self.nodes[1].getrawchangeaddress()
462463
inputs = []
463464
outputs = {self.nodes[0].getnewaddress():1.1}
464465
rawTx = self.nodes[1].createrawtransaction(inputs, outputs)
@@ -472,6 +473,7 @@ def run_test(self):
472473

473474
#refill the keypool
474475
self.nodes[1].walletpassphrase("test", 100)
476+
self.nodes[1].keypoolrefill(2) #need to refill the keypool to get an internal change address
475477
self.nodes[1].walletlock()
476478

477479
try:

qa/rpc-tests/keypool.py

+45-12
Original file line numberDiff line numberDiff line change
@@ -14,36 +14,64 @@ class KeyPoolTest(BitcoinTestFramework):
1414

1515
def run_test(self):
1616
nodes = self.nodes
17+
addr_before_encrypting = nodes[0].getnewaddress()
18+
addr_before_encrypting_data = nodes[0].validateaddress(addr_before_encrypting)
19+
wallet_info_old = nodes[0].getwalletinfo()
20+
assert(addr_before_encrypting_data['hdchainid'] == wallet_info_old['hdchainid'])
21+
1722
# Encrypt wallet and wait to terminate
1823
nodes[0].encryptwallet('test')
1924
bitcoind_processes[0].wait()
2025
# Restart node 0
2126
nodes[0] = start_node(0, self.options.tmpdir)
2227
# Keep creating keys
2328
addr = nodes[0].getnewaddress()
29+
addr_data = nodes[0].validateaddress(addr)
30+
wallet_info = nodes[0].getwalletinfo()
31+
assert(addr_before_encrypting_data['hdchainid'] == wallet_info['hdchainid'])
32+
assert(addr_data['hdchainid'] == wallet_info['hdchainid'])
33+
2434
try:
2535
addr = nodes[0].getnewaddress()
2636
raise AssertionError('Keypool should be exhausted after one address')
2737
except JSONRPCException as e:
2838
assert(e.error['code']==-12)
2939

30-
# put three new keys in the keypool
40+
# put six (plus 2) new keys in the keypool (100% external-, +100% internal-keys, 1 in min)
3141
nodes[0].walletpassphrase('test', 12000)
32-
nodes[0].keypoolrefill(3)
42+
nodes[0].keypoolrefill(6)
3343
nodes[0].walletlock()
44+
wi = nodes[0].getwalletinfo()
45+
assert_equal(wi['keypoolsize_hd_internal'], 6)
46+
assert_equal(wi['keypoolsize'], 6)
47+
48+
# drain the internal keys
49+
nodes[0].getrawchangeaddress()
50+
nodes[0].getrawchangeaddress()
51+
nodes[0].getrawchangeaddress()
52+
nodes[0].getrawchangeaddress()
53+
nodes[0].getrawchangeaddress()
54+
nodes[0].getrawchangeaddress()
55+
# the next one should fail
56+
try:
57+
nodes[0].getrawchangeaddress()
58+
raise AssertionError('Keypool should be exhausted after six addresses')
59+
except JSONRPCException as e:
60+
assert(e.error['code']==-12)
3461

35-
# drain the keys
3662
addr = set()
37-
addr.add(nodes[0].getrawchangeaddress())
38-
addr.add(nodes[0].getrawchangeaddress())
39-
addr.add(nodes[0].getrawchangeaddress())
40-
addr.add(nodes[0].getrawchangeaddress())
41-
# assert that four unique addresses were returned
42-
assert(len(addr) == 4)
63+
# drain the external keys
64+
addr.add(nodes[0].getnewaddress())
65+
addr.add(nodes[0].getnewaddress())
66+
addr.add(nodes[0].getnewaddress())
67+
addr.add(nodes[0].getnewaddress())
68+
addr.add(nodes[0].getnewaddress())
69+
addr.add(nodes[0].getnewaddress())
70+
assert(len(addr) == 6)
4371
# the next one should fail
4472
try:
45-
addr = nodes[0].getrawchangeaddress()
46-
raise AssertionError('Keypool should be exhausted after three addresses')
73+
addr = nodes[0].getnewaddress()
74+
raise AssertionError('Keypool should be exhausted after six addresses')
4775
except JSONRPCException as e:
4876
assert(e.error['code']==-12)
4977

@@ -58,13 +86,18 @@ def run_test(self):
5886
nodes[0].generate(1)
5987
nodes[0].generate(1)
6088
nodes[0].generate(1)
61-
nodes[0].generate(1)
6289
try:
6390
nodes[0].generate(1)
6491
raise AssertionError('Keypool should be exhausted after three addesses')
6592
except JSONRPCException as e:
6693
assert(e.error['code']==-12)
6794

95+
nodes[0].walletpassphrase('test', 100)
96+
nodes[0].keypoolrefill(100)
97+
wi = nodes[0].getwalletinfo()
98+
assert_equal(wi['keypoolsize_hd_internal'], 100)
99+
assert_equal(wi['keypoolsize'], 100)
100+
68101
def setup_chain(self):
69102
print("Initializing test directory "+self.options.tmpdir)
70103
initialize_chain(self.options.tmpdir)

qa/rpc-tests/wallet-hd.py

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/usr/bin/env python2
2+
# coding=utf-8
3+
# ^^^^^^^^^^^^ TODO remove when supporting only Python3
4+
# Copyright (c) 2016 The Bitcoin Core developers
5+
# Distributed under the MIT software license, see the accompanying
6+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
7+
"""Test Hierarchical Deterministic wallet function."""
8+
9+
from test_framework.test_framework import BitcoinTestFramework
10+
from test_framework.util import *
11+
12+
class WalletHDTest(BitcoinTestFramework):
13+
14+
def setup_chain(self):
15+
print("Initializing test directory "+self.options.tmpdir)
16+
initialize_chain_clean(self.options.tmpdir, 2)
17+
18+
def setup_network(self):
19+
self.nodes = start_nodes(2, self.options.tmpdir, [['-usehd=0'], ['-usehd=1', '-keypool=0']])
20+
self.is_network_split = False
21+
connect_nodes_bi(self.nodes, 0, 1)
22+
self.is_network_split=False
23+
self.sync_all()
24+
25+
def run_test (self):
26+
tmpdir = self.options.tmpdir
27+
28+
# Make sure can't switch off usehd after wallet creation
29+
stop_node(self.nodes[1],1)
30+
try:
31+
start_node(1, self.options.tmpdir, ['-usehd=0'])
32+
raise AssertionError("Must not allow to turn off HD on an already existing HD wallet")
33+
except Exception as e:
34+
assert("dashd exited with status 1 during initialization" in str(e))
35+
# assert_start_raises_init_error(1, self.options.tmpdir, ['-usehd=0'], 'already existing HD wallet')
36+
# self.nodes[1] = start_node(1, self.options.tmpdir, self.node_args[1])
37+
self.nodes[1] = start_node(1, self.options.tmpdir, ['-usehd=1', '-keypool=0'])
38+
connect_nodes_bi(self.nodes, 0, 1)
39+
40+
# Make sure we use hd, keep chainid
41+
chainid = self.nodes[1].getwalletinfo()['hdchainid']
42+
assert_equal(len(chainid), 64)
43+
44+
# create an internal key
45+
change_addr = self.nodes[1].getrawchangeaddress()
46+
change_addrV= self.nodes[1].validateaddress(change_addr);
47+
assert_equal(change_addrV["hdkeypath"], "m/44'/1'/0'/1/0") #first internal child key
48+
49+
# Import a non-HD private key in the HD wallet
50+
non_hd_add = self.nodes[0].getnewaddress()
51+
self.nodes[1].importprivkey(self.nodes[0].dumpprivkey(non_hd_add))
52+
53+
# This should be enough to keep the master key and the non-HD key
54+
self.nodes[1].backupwallet(tmpdir + "/hd.bak")
55+
#self.nodes[1].dumpwallet(tmpdir + "/hd.dump")
56+
57+
# Derive some HD addresses and remember the last
58+
# Also send funds to each add
59+
self.nodes[0].generate(101)
60+
hd_add = None
61+
num_hd_adds = 300
62+
for i in range(num_hd_adds):
63+
hd_add = self.nodes[1].getnewaddress()
64+
hd_info = self.nodes[1].validateaddress(hd_add)
65+
assert_equal(hd_info["hdkeypath"], "m/44'/1'/0'/0/"+str(i+1))
66+
assert_equal(hd_info["hdchainid"], chainid)
67+
self.nodes[0].sendtoaddress(hd_add, 1)
68+
self.nodes[0].generate(1)
69+
self.nodes[0].sendtoaddress(non_hd_add, 1)
70+
self.nodes[0].generate(1)
71+
72+
# create an internal key (again)
73+
change_addr = self.nodes[1].getrawchangeaddress()
74+
change_addrV= self.nodes[1].validateaddress(change_addr);
75+
assert_equal(change_addrV["hdkeypath"], "m/44'/1'/0'/1/1") #second internal child key
76+
77+
self.sync_all()
78+
assert_equal(self.nodes[1].getbalance(), num_hd_adds + 1)
79+
80+
print("Restore backup ...")
81+
stop_node(self.nodes[1],1)
82+
os.remove(self.options.tmpdir + "/node1/regtest/wallet.dat")
83+
shutil.copyfile(tmpdir + "/hd.bak", tmpdir + "/node1/regtest/wallet.dat")
84+
self.nodes[1] = start_node(1, self.options.tmpdir, ['-usehd=1', '-keypool=0'])
85+
#connect_nodes_bi(self.nodes, 0, 1)
86+
87+
# Assert that derivation is deterministic
88+
hd_add_2 = None
89+
for _ in range(num_hd_adds):
90+
hd_add_2 = self.nodes[1].getnewaddress()
91+
hd_info_2 = self.nodes[1].validateaddress(hd_add_2)
92+
assert_equal(hd_info_2["hdkeypath"], "m/44'/1'/0'/0/"+str(_+1))
93+
assert_equal(hd_info_2["hdchainid"], chainid)
94+
assert_equal(hd_add, hd_add_2)
95+
96+
# Needs rescan
97+
stop_node(self.nodes[1],1)
98+
self.nodes[1] = start_node(1, self.options.tmpdir, ['-usehd=1', '-keypool=0', '-rescan'])
99+
#connect_nodes_bi(self.nodes, 0, 1)
100+
assert_equal(self.nodes[1].getbalance(), num_hd_adds + 1)
101+
102+
# send a tx and make sure its using the internal chain for the changeoutput
103+
txid = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 1)
104+
outs = self.nodes[1].decoderawtransaction(self.nodes[1].gettransaction(txid)['hex'])['vout'];
105+
keypath = ""
106+
for out in outs:
107+
if out['value'] != 1:
108+
keypath = self.nodes[1].validateaddress(out['scriptPubKey']['addresses'][0])['hdkeypath']
109+
110+
assert_equal(keypath[0:13], "m/44'/1'/0'/1")
111+
112+
if __name__ == '__main__':
113+
WalletHDTest().main ()

src/Makefile.am

+6-1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ BITCOIN_CORE_H = \
7373
amount.h \
7474
arith_uint256.h \
7575
base58.h \
76+
bip39.h \
77+
bip39_english.h \
7678
bloom.h \
7779
cachemap.h \
7880
cachemultimap.h \
@@ -108,6 +110,7 @@ BITCOIN_CORE_H = \
108110
governance-votedb.h \
109111
flat-database.h \
110112
hash.h \
113+
hdchain.h \
111114
httprpc.h \
112115
httpserver.h \
113116
init.h \
@@ -260,7 +263,6 @@ libbitcoin_wallet_a_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES)
260263
libbitcoin_wallet_a_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
261264
libbitcoin_wallet_a_SOURCES = \
262265
activemasternode.cpp \
263-
privatesend-client.cpp \
264266
dsnotificationinterface.cpp \
265267
instantx.cpp \
266268
masternode.cpp \
@@ -269,6 +271,7 @@ libbitcoin_wallet_a_SOURCES = \
269271
masternodeconfig.cpp \
270272
masternodeman.cpp \
271273
keepass.cpp \
274+
privatesend-client.cpp \
272275
wallet/crypter.cpp \
273276
wallet/db.cpp \
274277
wallet/rpcdump.cpp \
@@ -329,13 +332,15 @@ libbitcoin_common_a_SOURCES = \
329332
amount.cpp \
330333
arith_uint256.cpp \
331334
base58.cpp \
335+
bip39.cpp \
332336
chainparams.cpp \
333337
coins.cpp \
334338
compressor.cpp \
335339
consensus/merkle.cpp \
336340
core_read.cpp \
337341
core_write.cpp \
338342
hash.cpp \
343+
hdchain.cpp \
339344
key.cpp \
340345
keystore.cpp \
341346
netbase.cpp \

src/Makefile.qt.include

+8
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ RES_ICONS = \
194194
qt/res/icons/drkblue/eye_minus.png \
195195
qt/res/icons/drkblue/eye_plus.png \
196196
qt/res/icons/drkblue/filesave.png \
197+
qt/res/icons/drkblue/hd_disabled.png \
198+
qt/res/icons/drkblue/hd_enabled.png \
197199
qt/res/icons/drkblue/history.png \
198200
qt/res/icons/drkblue/key.png \
199201
qt/res/icons/drkblue/lock_closed.png \
@@ -242,6 +244,8 @@ RES_ICONS = \
242244
qt/res/icons/crownium/eye_minus.png \
243245
qt/res/icons/crownium/eye_plus.png \
244246
qt/res/icons/crownium/filesave.png \
247+
qt/res/icons/crownium/hd_disabled.png \
248+
qt/res/icons/crownium/hd_enabled.png \
245249
qt/res/icons/crownium/history.png \
246250
qt/res/icons/crownium/key.png \
247251
qt/res/icons/crownium/lock_closed.png \
@@ -290,6 +294,8 @@ RES_ICONS = \
290294
qt/res/icons/light/eye_minus.png \
291295
qt/res/icons/light/eye_plus.png \
292296
qt/res/icons/light/filesave.png \
297+
qt/res/icons/light/hd_disabled.png \
298+
qt/res/icons/light/hd_enabled.png \
293299
qt/res/icons/light/history.png \
294300
qt/res/icons/light/key.png \
295301
qt/res/icons/light/lock_closed.png \
@@ -338,6 +344,8 @@ RES_ICONS = \
338344
qt/res/icons/trad/eye_minus.png \
339345
qt/res/icons/trad/eye_plus.png \
340346
qt/res/icons/trad/filesave.png \
347+
qt/res/icons/trad/hd_disabled.png \
348+
qt/res/icons/trad/hd_enabled.png \
341349
qt/res/icons/trad/history.png \
342350
qt/res/icons/trad/key.png \
343351
qt/res/icons/trad/lock_closed.png \

src/Makefile.test.include

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ JSON_TEST_FILES = \
2424
test/data/base58_keys_valid.json \
2525
test/data/base58_encode_decode.json \
2626
test/data/base58_keys_invalid.json \
27+
test/data/bip39_vectors.json \
2728
test/data/tx_invalid.json \
2829
test/data/tx_valid.json \
2930
test/data/sighash.json
@@ -42,6 +43,7 @@ BITCOIN_TESTS =\
4243
test/base58_tests.cpp \
4344
test/base64_tests.cpp \
4445
test/bip32_tests.cpp \
46+
test/bip39_tests.cpp \
4547
test/bloom_tests.cpp \
4648
test/bswap_tests.cpp \
4749
test/cachemap_tests.cpp \

0 commit comments

Comments
 (0)