diff --git a/qa/pull-tester/rpc-tests.py b/qa/pull-tester/rpc-tests.py index f56422f3a9bd..99e96aab6c5e 100755 --- a/qa/pull-tester/rpc-tests.py +++ b/qa/pull-tester/rpc-tests.py @@ -90,6 +90,7 @@ testScripts = [ 'bip68-112-113-p2p.py', 'wallet.py', + 'wallet-hd.py', 'listtransactions.py', 'receivedby.py', 'mempool_resurrect_test.py', diff --git a/qa/rpc-tests/fundrawtransaction.py b/qa/rpc-tests/fundrawtransaction.py index e204a691360c..467ba1423f5c 100755 --- a/qa/rpc-tests/fundrawtransaction.py +++ b/qa/rpc-tests/fundrawtransaction.py @@ -459,6 +459,7 @@ def run_test(self): # drain the keypool self.nodes[1].getnewaddress() + self.nodes[1].getrawchangeaddress() inputs = [] outputs = {self.nodes[0].getnewaddress():1.1} rawTx = self.nodes[1].createrawtransaction(inputs, outputs) @@ -472,6 +473,7 @@ def run_test(self): #refill the keypool self.nodes[1].walletpassphrase("test", 100) + self.nodes[1].keypoolrefill(2) #need to refill the keypool to get an internal change address self.nodes[1].walletlock() try: diff --git a/qa/rpc-tests/keypool.py b/qa/rpc-tests/keypool.py index b86c085e0009..05f946831cd1 100755 --- a/qa/rpc-tests/keypool.py +++ b/qa/rpc-tests/keypool.py @@ -14,6 +14,11 @@ class KeyPoolTest(BitcoinTestFramework): def run_test(self): nodes = self.nodes + addr_before_encrypting = nodes[0].getnewaddress() + addr_before_encrypting_data = nodes[0].validateaddress(addr_before_encrypting) + wallet_info_old = nodes[0].getwalletinfo() + assert(addr_before_encrypting_data['hdchainid'] == wallet_info_old['hdchainid']) + # Encrypt wallet and wait to terminate nodes[0].encryptwallet('test') bitcoind_processes[0].wait() @@ -21,29 +26,52 @@ def run_test(self): nodes[0] = start_node(0, self.options.tmpdir) # Keep creating keys addr = nodes[0].getnewaddress() + addr_data = nodes[0].validateaddress(addr) + wallet_info = nodes[0].getwalletinfo() + assert(addr_before_encrypting_data['hdchainid'] == wallet_info['hdchainid']) + assert(addr_data['hdchainid'] == wallet_info['hdchainid']) + try: addr = nodes[0].getnewaddress() raise AssertionError('Keypool should be exhausted after one address') except JSONRPCException as e: assert(e.error['code']==-12) - # put three new keys in the keypool + # put six (plus 2) new keys in the keypool (100% external-, +100% internal-keys, 1 in min) nodes[0].walletpassphrase('test', 12000) - nodes[0].keypoolrefill(3) + nodes[0].keypoolrefill(6) nodes[0].walletlock() + wi = nodes[0].getwalletinfo() + assert_equal(wi['keypoolsize_hd_internal'], 6) + assert_equal(wi['keypoolsize'], 6) + + # drain the internal keys + nodes[0].getrawchangeaddress() + nodes[0].getrawchangeaddress() + nodes[0].getrawchangeaddress() + nodes[0].getrawchangeaddress() + nodes[0].getrawchangeaddress() + nodes[0].getrawchangeaddress() + # the next one should fail + try: + nodes[0].getrawchangeaddress() + raise AssertionError('Keypool should be exhausted after six addresses') + except JSONRPCException as e: + assert(e.error['code']==-12) - # drain the keys addr = set() - addr.add(nodes[0].getrawchangeaddress()) - addr.add(nodes[0].getrawchangeaddress()) - addr.add(nodes[0].getrawchangeaddress()) - addr.add(nodes[0].getrawchangeaddress()) - # assert that four unique addresses were returned - assert(len(addr) == 4) + # drain the external keys + addr.add(nodes[0].getnewaddress()) + addr.add(nodes[0].getnewaddress()) + addr.add(nodes[0].getnewaddress()) + addr.add(nodes[0].getnewaddress()) + addr.add(nodes[0].getnewaddress()) + addr.add(nodes[0].getnewaddress()) + assert(len(addr) == 6) # the next one should fail try: - addr = nodes[0].getrawchangeaddress() - raise AssertionError('Keypool should be exhausted after three addresses') + addr = nodes[0].getnewaddress() + raise AssertionError('Keypool should be exhausted after six addresses') except JSONRPCException as e: assert(e.error['code']==-12) @@ -58,13 +86,18 @@ def run_test(self): nodes[0].generate(1) nodes[0].generate(1) nodes[0].generate(1) - nodes[0].generate(1) try: nodes[0].generate(1) raise AssertionError('Keypool should be exhausted after three addesses') except JSONRPCException as e: assert(e.error['code']==-12) + nodes[0].walletpassphrase('test', 100) + nodes[0].keypoolrefill(100) + wi = nodes[0].getwalletinfo() + assert_equal(wi['keypoolsize_hd_internal'], 100) + assert_equal(wi['keypoolsize'], 100) + def setup_chain(self): print("Initializing test directory "+self.options.tmpdir) initialize_chain(self.options.tmpdir) diff --git a/qa/rpc-tests/wallet-hd.py b/qa/rpc-tests/wallet-hd.py new file mode 100755 index 000000000000..6553a62eadab --- /dev/null +++ b/qa/rpc-tests/wallet-hd.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python2 +# coding=utf-8 +# ^^^^^^^^^^^^ TODO remove when supporting only Python3 +# Copyright (c) 2016 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test Hierarchical Deterministic wallet function.""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import * + +class WalletHDTest(BitcoinTestFramework): + + def setup_chain(self): + print("Initializing test directory "+self.options.tmpdir) + initialize_chain_clean(self.options.tmpdir, 2) + + def setup_network(self): + self.nodes = start_nodes(2, self.options.tmpdir, [['-usehd=0'], ['-usehd=1', '-keypool=0']]) + self.is_network_split = False + connect_nodes_bi(self.nodes, 0, 1) + self.is_network_split=False + self.sync_all() + + def run_test (self): + tmpdir = self.options.tmpdir + + # Make sure can't switch off usehd after wallet creation + stop_node(self.nodes[1],1) + try: + start_node(1, self.options.tmpdir, ['-usehd=0']) + raise AssertionError("Must not allow to turn off HD on an already existing HD wallet") + except Exception as e: + assert("dashd exited with status 1 during initialization" in str(e)) + # assert_start_raises_init_error(1, self.options.tmpdir, ['-usehd=0'], 'already existing HD wallet') + # self.nodes[1] = start_node(1, self.options.tmpdir, self.node_args[1]) + self.nodes[1] = start_node(1, self.options.tmpdir, ['-usehd=1', '-keypool=0']) + connect_nodes_bi(self.nodes, 0, 1) + + # Make sure we use hd, keep chainid + chainid = self.nodes[1].getwalletinfo()['hdchainid'] + assert_equal(len(chainid), 64) + + # create an internal key + change_addr = self.nodes[1].getrawchangeaddress() + change_addrV= self.nodes[1].validateaddress(change_addr); + assert_equal(change_addrV["hdkeypath"], "m/44'/1'/0'/1/0") #first internal child key + + # Import a non-HD private key in the HD wallet + non_hd_add = self.nodes[0].getnewaddress() + self.nodes[1].importprivkey(self.nodes[0].dumpprivkey(non_hd_add)) + + # This should be enough to keep the master key and the non-HD key + self.nodes[1].backupwallet(tmpdir + "/hd.bak") + #self.nodes[1].dumpwallet(tmpdir + "/hd.dump") + + # Derive some HD addresses and remember the last + # Also send funds to each add + self.nodes[0].generate(101) + hd_add = None + num_hd_adds = 300 + for i in range(num_hd_adds): + hd_add = self.nodes[1].getnewaddress() + hd_info = self.nodes[1].validateaddress(hd_add) + assert_equal(hd_info["hdkeypath"], "m/44'/1'/0'/0/"+str(i+1)) + assert_equal(hd_info["hdchainid"], chainid) + self.nodes[0].sendtoaddress(hd_add, 1) + self.nodes[0].generate(1) + self.nodes[0].sendtoaddress(non_hd_add, 1) + self.nodes[0].generate(1) + + # create an internal key (again) + change_addr = self.nodes[1].getrawchangeaddress() + change_addrV= self.nodes[1].validateaddress(change_addr); + assert_equal(change_addrV["hdkeypath"], "m/44'/1'/0'/1/1") #second internal child key + + self.sync_all() + assert_equal(self.nodes[1].getbalance(), num_hd_adds + 1) + + print("Restore backup ...") + stop_node(self.nodes[1],1) + os.remove(self.options.tmpdir + "/node1/regtest/wallet.dat") + shutil.copyfile(tmpdir + "/hd.bak", tmpdir + "/node1/regtest/wallet.dat") + self.nodes[1] = start_node(1, self.options.tmpdir, ['-usehd=1', '-keypool=0']) + #connect_nodes_bi(self.nodes, 0, 1) + + # Assert that derivation is deterministic + hd_add_2 = None + for _ in range(num_hd_adds): + hd_add_2 = self.nodes[1].getnewaddress() + hd_info_2 = self.nodes[1].validateaddress(hd_add_2) + assert_equal(hd_info_2["hdkeypath"], "m/44'/1'/0'/0/"+str(_+1)) + assert_equal(hd_info_2["hdchainid"], chainid) + assert_equal(hd_add, hd_add_2) + + # Needs rescan + stop_node(self.nodes[1],1) + self.nodes[1] = start_node(1, self.options.tmpdir, ['-usehd=1', '-keypool=0', '-rescan']) + #connect_nodes_bi(self.nodes, 0, 1) + assert_equal(self.nodes[1].getbalance(), num_hd_adds + 1) + + # send a tx and make sure its using the internal chain for the changeoutput + txid = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 1) + outs = self.nodes[1].decoderawtransaction(self.nodes[1].gettransaction(txid)['hex'])['vout']; + keypath = "" + for out in outs: + if out['value'] != 1: + keypath = self.nodes[1].validateaddress(out['scriptPubKey']['addresses'][0])['hdkeypath'] + + assert_equal(keypath[0:13], "m/44'/1'/0'/1") + +if __name__ == '__main__': + WalletHDTest().main () diff --git a/src/Makefile.am b/src/Makefile.am index 4dfec82fd058..085645e5298d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -73,6 +73,8 @@ BITCOIN_CORE_H = \ amount.h \ arith_uint256.h \ base58.h \ + bip39.h \ + bip39_english.h \ bloom.h \ cachemap.h \ cachemultimap.h \ @@ -108,6 +110,7 @@ BITCOIN_CORE_H = \ governance-votedb.h \ flat-database.h \ hash.h \ + hdchain.h \ httprpc.h \ httpserver.h \ init.h \ @@ -260,7 +263,6 @@ libbitcoin_wallet_a_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) libbitcoin_wallet_a_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) libbitcoin_wallet_a_SOURCES = \ activemasternode.cpp \ - privatesend-client.cpp \ dsnotificationinterface.cpp \ instantx.cpp \ masternode.cpp \ @@ -269,6 +271,7 @@ libbitcoin_wallet_a_SOURCES = \ masternodeconfig.cpp \ masternodeman.cpp \ keepass.cpp \ + privatesend-client.cpp \ wallet/crypter.cpp \ wallet/db.cpp \ wallet/rpcdump.cpp \ @@ -329,6 +332,7 @@ libbitcoin_common_a_SOURCES = \ amount.cpp \ arith_uint256.cpp \ base58.cpp \ + bip39.cpp \ chainparams.cpp \ coins.cpp \ compressor.cpp \ @@ -336,6 +340,7 @@ libbitcoin_common_a_SOURCES = \ core_read.cpp \ core_write.cpp \ hash.cpp \ + hdchain.cpp \ key.cpp \ keystore.cpp \ netbase.cpp \ diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 7a98e4b74b0e..992aae2f27e6 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -194,6 +194,8 @@ RES_ICONS = \ qt/res/icons/drkblue/eye_minus.png \ qt/res/icons/drkblue/eye_plus.png \ qt/res/icons/drkblue/filesave.png \ + qt/res/icons/drkblue/hd_disabled.png \ + qt/res/icons/drkblue/hd_enabled.png \ qt/res/icons/drkblue/history.png \ qt/res/icons/drkblue/key.png \ qt/res/icons/drkblue/lock_closed.png \ @@ -242,6 +244,8 @@ RES_ICONS = \ qt/res/icons/crownium/eye_minus.png \ qt/res/icons/crownium/eye_plus.png \ qt/res/icons/crownium/filesave.png \ + qt/res/icons/crownium/hd_disabled.png \ + qt/res/icons/crownium/hd_enabled.png \ qt/res/icons/crownium/history.png \ qt/res/icons/crownium/key.png \ qt/res/icons/crownium/lock_closed.png \ @@ -290,6 +294,8 @@ RES_ICONS = \ qt/res/icons/light/eye_minus.png \ qt/res/icons/light/eye_plus.png \ qt/res/icons/light/filesave.png \ + qt/res/icons/light/hd_disabled.png \ + qt/res/icons/light/hd_enabled.png \ qt/res/icons/light/history.png \ qt/res/icons/light/key.png \ qt/res/icons/light/lock_closed.png \ @@ -338,6 +344,8 @@ RES_ICONS = \ qt/res/icons/trad/eye_minus.png \ qt/res/icons/trad/eye_plus.png \ qt/res/icons/trad/filesave.png \ + qt/res/icons/trad/hd_disabled.png \ + qt/res/icons/trad/hd_enabled.png \ qt/res/icons/trad/history.png \ qt/res/icons/trad/key.png \ qt/res/icons/trad/lock_closed.png \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index eb72b785acc4..b6ef12394f02 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -24,6 +24,7 @@ JSON_TEST_FILES = \ test/data/base58_keys_valid.json \ test/data/base58_encode_decode.json \ test/data/base58_keys_invalid.json \ + test/data/bip39_vectors.json \ test/data/tx_invalid.json \ test/data/tx_valid.json \ test/data/sighash.json @@ -42,6 +43,7 @@ BITCOIN_TESTS =\ test/base58_tests.cpp \ test/base64_tests.cpp \ test/bip32_tests.cpp \ + test/bip39_tests.cpp \ test/bloom_tests.cpp \ test/bswap_tests.cpp \ test/cachemap_tests.cpp \ diff --git a/src/bip39.cpp b/src/bip39.cpp new file mode 100644 index 000000000000..608d4d9433c4 --- /dev/null +++ b/src/bip39.cpp @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2013-2014 Tomas Dzetkulic + * Copyright (c) 2013-2014 Pavol Rusnak + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES + * OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +// Source: +// https://github.com/trezor/trezor-crypto + +#include "bip39.h" +#include "bip39_english.h" +#include "crypto/sha256.h" +#include "random.h" + +#include + +SecureString CMnemonic::Generate(int strength) +{ + if (strength % 32 || strength < 128 || strength > 256) { + return SecureString(); + } + SecureVector data(32); + GetRandBytes(&data[0], 32); + SecureString mnemonic = FromData(data, strength / 8); + return mnemonic; +} + +// SecureString CMnemonic::FromData(const uint8_t *data, int len) +SecureString CMnemonic::FromData(const SecureVector& data, int len) +{ + if (len % 4 || len < 16 || len > 32) { + return SecureString(); + } + + SecureVector checksum(32); + CSHA256().Write(&data[0], len).Finalize(&checksum[0]); + + // data + SecureVector bits(len); + memcpy(&bits[0], &data[0], len); + // checksum + bits.push_back(checksum[0]); + + int mlen = len * 3 / 4; + SecureString mnemonic; + + int i, j, idx; + for (i = 0; i < mlen; i++) { + idx = 0; + for (j = 0; j < 11; j++) { + idx <<= 1; + idx += (bits[(i * 11 + j) / 8] & (1 << (7 - ((i * 11 + j) % 8)))) > 0; + } + mnemonic.append(wordlist[idx]); + if (i < mlen - 1) { + mnemonic += ' '; + } + } + + return mnemonic; +} + +bool CMnemonic::Check(SecureString mnemonic) +{ + if (mnemonic.empty()) { + return false; + } + + uint32_t nWordCount{}; + + for (size_t i = 0; i < mnemonic.size(); ++i) { + if (mnemonic[i] == ' ') { + nWordCount++; + } + } + nWordCount++; + // check number of words + if (nWordCount != 12 && nWordCount != 18 && nWordCount != 24) { + return false; + } + + SecureString ssCurrentWord; + SecureVector bits(32 + 1); + + uint32_t nWordIndex, ki, nBitsCount{}; + + for (size_t i = 0; i < mnemonic.size(); ++i) + { + ssCurrentWord = ""; + while (i + ssCurrentWord.size() < mnemonic.size() && mnemonic[i + ssCurrentWord.size()] != ' ') { + if (ssCurrentWord.size() >= 9) { + return false; + } + ssCurrentWord += mnemonic[i + ssCurrentWord.size()]; + } + i += ssCurrentWord.size(); + nWordIndex = 0; + for (;;) { + if (!wordlist[nWordIndex]) { // word not found + return false; + } + if (ssCurrentWord == wordlist[nWordIndex]) { // word found on index nWordIndex + for (ki = 0; ki < 11; ki++) { + if (nWordIndex & (1 << (10 - ki))) { + bits[nBitsCount / 8] |= 1 << (7 - (nBitsCount % 8)); + } + nBitsCount++; + } + break; + } + nWordIndex++; + } + } + if (nBitsCount != nWordCount * 11) { + return false; + } + bits[32] = bits[nWordCount * 4 / 3]; + CSHA256().Write(&bits[0], nWordCount * 4 / 3).Finalize(&bits[0]); + + bool fResult = 0; + if (nWordCount == 12) { + fResult = (bits[0] & 0xF0) == (bits[32] & 0xF0); // compare first 4 bits + } else + if (nWordCount == 18) { + fResult = (bits[0] & 0xFC) == (bits[32] & 0xFC); // compare first 6 bits + } else + if (nWordCount == 24) { + fResult = bits[0] == bits[32]; // compare 8 bits + } + + return fResult; +} + +// passphrase must be at most 256 characters or code may crash +void CMnemonic::ToSeed(SecureString mnemonic, SecureString passphrase, SecureVector& seedRet) +{ + SecureString ssSalt = SecureString("mnemonic") + passphrase; + SecureVector vchSalt(ssSalt.begin(), ssSalt.end()); + seedRet.resize(64); + // int PKCS5_PBKDF2_HMAC(const char *pass, int passlen, + // const unsigned char *salt, int saltlen, int iter, + // const EVP_MD *digest, + // int keylen, unsigned char *out); + PKCS5_PBKDF2_HMAC(mnemonic.c_str(), mnemonic.size(), &vchSalt[0], vchSalt.size(), 2048, EVP_sha512(), 64, &seedRet[0]); +} diff --git a/src/bip39.h b/src/bip39.h new file mode 100644 index 000000000000..4d74383817ee --- /dev/null +++ b/src/bip39.h @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2013-2014 Tomas Dzetkulic + * Copyright (c) 2013-2014 Pavol Rusnak + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES + * OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef DASH_BIP39_H +#define DASH_BIP39_H + +#include "support/allocators/secure.h" + +class CMnemonic +{ +public: + static SecureString Generate(int strength); // strength in bits + static SecureString FromData(const SecureVector& data, int len); + static bool Check(SecureString mnemonic); + // passphrase must be at most 256 characters or code may crash + static void ToSeed(SecureString mnemonic, SecureString passphrase, SecureVector& seedRet); +}; + +#endif diff --git a/src/bip39_english.h b/src/bip39_english.h new file mode 100644 index 000000000000..233acc2f85a9 --- /dev/null +++ b/src/bip39_english.h @@ -0,0 +1,2074 @@ +/** + * Copyright (c) 2013-2014 Tomas Dzetkulic + * Copyright (c) 2013-2014 Pavol Rusnak + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES + * OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +const char * const wordlist[] = { +"abandon", +"ability", +"able", +"about", +"above", +"absent", +"absorb", +"abstract", +"absurd", +"abuse", +"access", +"accident", +"account", +"accuse", +"achieve", +"acid", +"acoustic", +"acquire", +"across", +"act", +"action", +"actor", +"actress", +"actual", +"adapt", +"add", +"addict", +"address", +"adjust", +"admit", +"adult", +"advance", +"advice", +"aerobic", +"affair", +"afford", +"afraid", +"again", +"age", +"agent", +"agree", +"ahead", +"aim", +"air", +"airport", +"aisle", +"alarm", +"album", +"alcohol", +"alert", +"alien", +"all", +"alley", +"allow", +"almost", +"alone", +"alpha", +"already", +"also", +"alter", +"always", +"amateur", +"amazing", +"among", +"amount", +"amused", +"analyst", +"anchor", +"ancient", +"anger", +"angle", +"angry", +"animal", +"ankle", +"announce", +"annual", +"another", +"answer", +"antenna", +"antique", +"anxiety", +"any", +"apart", +"apology", +"appear", +"apple", +"approve", +"april", +"arch", +"arctic", +"area", +"arena", +"argue", +"arm", +"armed", +"armor", +"army", +"around", +"arrange", +"arrest", +"arrive", +"arrow", +"art", +"artefact", +"artist", +"artwork", +"ask", +"aspect", +"assault", +"asset", +"assist", +"assume", +"asthma", +"athlete", +"atom", +"attack", +"attend", +"attitude", +"attract", +"auction", +"audit", +"august", +"aunt", +"author", +"auto", +"autumn", +"average", +"avocado", +"avoid", +"awake", +"aware", +"away", +"awesome", +"awful", +"awkward", +"axis", +"baby", +"bachelor", +"bacon", +"badge", +"bag", +"balance", +"balcony", +"ball", +"bamboo", +"banana", +"banner", +"bar", +"barely", +"bargain", +"barrel", +"base", +"basic", +"basket", +"battle", +"beach", +"bean", +"beauty", +"because", +"become", +"beef", +"before", +"begin", +"behave", +"behind", +"believe", +"below", +"belt", +"bench", +"benefit", +"best", +"betray", +"better", +"between", +"beyond", +"bicycle", +"bid", +"bike", +"bind", +"biology", +"bird", +"birth", +"bitter", +"black", +"blade", +"blame", +"blanket", +"blast", +"bleak", +"bless", +"blind", +"blood", +"blossom", +"blouse", +"blue", +"blur", +"blush", +"board", +"boat", +"body", +"boil", +"bomb", +"bone", +"bonus", +"book", +"boost", +"border", +"boring", +"borrow", +"boss", +"bottom", +"bounce", +"box", +"boy", +"bracket", +"brain", +"brand", +"brass", +"brave", +"bread", +"breeze", +"brick", +"bridge", +"brief", +"bright", +"bring", +"brisk", +"broccoli", +"broken", +"bronze", +"broom", +"brother", +"brown", +"brush", +"bubble", +"buddy", +"budget", +"buffalo", +"build", +"bulb", +"bulk", +"bullet", +"bundle", +"bunker", +"burden", +"burger", +"burst", +"bus", +"business", +"busy", +"butter", +"buyer", +"buzz", +"cabbage", +"cabin", +"cable", +"cactus", +"cage", +"cake", +"call", +"calm", +"camera", +"camp", +"can", +"canal", +"cancel", +"candy", +"cannon", +"canoe", +"canvas", +"canyon", +"capable", +"capital", +"captain", +"car", +"carbon", +"card", +"cargo", +"carpet", +"carry", +"cart", +"case", +"cash", +"casino", +"castle", +"casual", +"cat", +"catalog", +"catch", +"category", +"cattle", +"caught", +"cause", +"caution", +"cave", +"ceiling", +"celery", +"cement", +"census", +"century", +"cereal", +"certain", +"chair", +"chalk", +"champion", +"change", +"chaos", +"chapter", +"charge", +"chase", +"chat", +"cheap", +"check", +"cheese", +"chef", +"cherry", +"chest", +"chicken", +"chief", +"child", +"chimney", +"choice", +"choose", +"chronic", +"chuckle", +"chunk", +"churn", +"cigar", +"cinnamon", +"circle", +"citizen", +"city", +"civil", +"claim", +"clap", +"clarify", +"claw", +"clay", +"clean", +"clerk", +"clever", +"click", +"client", +"cliff", +"climb", +"clinic", +"clip", +"clock", +"clog", +"close", +"cloth", +"cloud", +"clown", +"club", +"clump", +"cluster", +"clutch", +"coach", +"coast", +"coconut", +"code", +"coffee", +"coil", +"coin", +"collect", +"color", +"column", +"combine", +"come", +"comfort", +"comic", +"common", +"company", +"concert", +"conduct", +"confirm", +"congress", +"connect", +"consider", +"control", +"convince", +"cook", +"cool", +"copper", +"copy", +"coral", +"core", +"corn", +"correct", +"cost", +"cotton", +"couch", +"country", +"couple", +"course", +"cousin", +"cover", +"coyote", +"crack", +"cradle", +"craft", +"cram", +"crane", +"crash", +"crater", +"crawl", +"crazy", +"cream", +"credit", +"creek", +"crew", +"cricket", +"crime", +"crisp", +"critic", +"crop", +"cross", +"crouch", +"crowd", +"crucial", +"cruel", +"cruise", +"crumble", +"crunch", +"crush", +"cry", +"crystal", +"cube", +"culture", +"cup", +"cupboard", +"curious", +"current", +"curtain", +"curve", +"cushion", +"custom", +"cute", +"cycle", +"dad", +"damage", +"damp", +"dance", +"danger", +"daring", +"dash", +"daughter", +"dawn", +"day", +"deal", +"debate", +"debris", +"decade", +"december", +"decide", +"decline", +"decorate", +"decrease", +"deer", +"defense", +"define", +"defy", +"degree", +"delay", +"deliver", +"demand", +"demise", +"denial", +"dentist", +"deny", +"depart", +"depend", +"deposit", +"depth", +"deputy", +"derive", +"describe", +"desert", +"design", +"desk", +"despair", +"destroy", +"detail", +"detect", +"develop", +"device", +"devote", +"diagram", +"dial", +"diamond", +"diary", +"dice", +"diesel", +"diet", +"differ", +"digital", +"dignity", +"dilemma", +"dinner", +"dinosaur", +"direct", +"dirt", +"disagree", +"discover", +"disease", +"dish", +"dismiss", +"disorder", +"display", +"distance", +"divert", +"divide", +"divorce", +"dizzy", +"doctor", +"document", +"dog", +"doll", +"dolphin", +"domain", +"donate", +"donkey", +"donor", +"door", +"dose", +"double", +"dove", +"draft", +"dragon", +"drama", +"drastic", +"draw", +"dream", +"dress", +"drift", +"drill", +"drink", +"drip", +"drive", +"drop", +"drum", +"dry", +"duck", +"dumb", +"dune", +"during", +"dust", +"dutch", +"duty", +"dwarf", +"dynamic", +"eager", +"eagle", +"early", +"earn", +"earth", +"easily", +"east", +"easy", +"echo", +"ecology", +"economy", +"edge", +"edit", +"educate", +"effort", +"egg", +"eight", +"either", +"elbow", +"elder", +"electric", +"elegant", +"element", +"elephant", +"elevator", +"elite", +"else", +"embark", +"embody", +"embrace", +"emerge", +"emotion", +"employ", +"empower", +"empty", +"enable", +"enact", +"end", +"endless", +"endorse", +"enemy", +"energy", +"enforce", +"engage", +"engine", +"enhance", +"enjoy", +"enlist", +"enough", +"enrich", +"enroll", +"ensure", +"enter", +"entire", +"entry", +"envelope", +"episode", +"equal", +"equip", +"era", +"erase", +"erode", +"erosion", +"error", +"erupt", +"escape", +"essay", +"essence", +"estate", +"eternal", +"ethics", +"evidence", +"evil", +"evoke", +"evolve", +"exact", +"example", +"excess", +"exchange", +"excite", +"exclude", +"excuse", +"execute", +"exercise", +"exhaust", +"exhibit", +"exile", +"exist", +"exit", +"exotic", +"expand", +"expect", +"expire", +"explain", +"expose", +"express", +"extend", +"extra", +"eye", +"eyebrow", +"fabric", +"face", +"faculty", +"fade", +"faint", +"faith", +"fall", +"false", +"fame", +"family", +"famous", +"fan", +"fancy", +"fantasy", +"farm", +"fashion", +"fat", +"fatal", +"father", +"fatigue", +"fault", +"favorite", +"feature", +"february", +"federal", +"fee", +"feed", +"feel", +"female", +"fence", +"festival", +"fetch", +"fever", +"few", +"fiber", +"fiction", +"field", +"figure", +"file", +"film", +"filter", +"final", +"find", +"fine", +"finger", +"finish", +"fire", +"firm", +"first", +"fiscal", +"fish", +"fit", +"fitness", +"fix", +"flag", +"flame", +"flash", +"flat", +"flavor", +"flee", +"flight", +"flip", +"float", +"flock", +"floor", +"flower", +"fluid", +"flush", +"fly", +"foam", +"focus", +"fog", +"foil", +"fold", +"follow", +"food", +"foot", +"force", +"forest", +"forget", +"fork", +"fortune", +"forum", +"forward", +"fossil", +"foster", +"found", +"fox", +"fragile", +"frame", +"frequent", +"fresh", +"friend", +"fringe", +"frog", +"front", +"frost", +"frown", +"frozen", +"fruit", +"fuel", +"fun", +"funny", +"furnace", +"fury", +"future", +"gadget", +"gain", +"galaxy", +"gallery", +"game", +"gap", +"garage", +"garbage", +"garden", +"garlic", +"garment", +"gas", +"gasp", +"gate", +"gather", +"gauge", +"gaze", +"general", +"genius", +"genre", +"gentle", +"genuine", +"gesture", +"ghost", +"giant", +"gift", +"giggle", +"ginger", +"giraffe", +"girl", +"give", +"glad", +"glance", +"glare", +"glass", +"glide", +"glimpse", +"globe", +"gloom", +"glory", +"glove", +"glow", +"glue", +"goat", +"goddess", +"gold", +"good", +"goose", +"gorilla", +"gospel", +"gossip", +"govern", +"gown", +"grab", +"grace", +"grain", +"grant", +"grape", +"grass", +"gravity", +"great", +"green", +"grid", +"grief", +"grit", +"grocery", +"group", +"grow", +"grunt", +"guard", +"guess", +"guide", +"guilt", +"guitar", +"gun", +"gym", +"habit", +"hair", +"half", +"hammer", +"hamster", +"hand", +"happy", +"harbor", +"hard", +"harsh", +"harvest", +"hat", +"have", +"hawk", +"hazard", +"head", +"health", +"heart", +"heavy", +"hedgehog", +"height", +"hello", +"helmet", +"help", +"hen", +"hero", +"hidden", +"high", +"hill", +"hint", +"hip", +"hire", +"history", +"hobby", +"hockey", +"hold", +"hole", +"holiday", +"hollow", +"home", +"honey", +"hood", +"hope", +"horn", +"horror", +"horse", +"hospital", +"host", +"hotel", +"hour", +"hover", +"hub", +"huge", +"human", +"humble", +"humor", +"hundred", +"hungry", +"hunt", +"hurdle", +"hurry", +"hurt", +"husband", +"hybrid", +"ice", +"icon", +"idea", +"identify", +"idle", +"ignore", +"ill", +"illegal", +"illness", +"image", +"imitate", +"immense", +"immune", +"impact", +"impose", +"improve", +"impulse", +"inch", +"include", +"income", +"increase", +"index", +"indicate", +"indoor", +"industry", +"infant", +"inflict", +"inform", +"inhale", +"inherit", +"initial", +"inject", +"injury", +"inmate", +"inner", +"innocent", +"input", +"inquiry", +"insane", +"insect", +"inside", +"inspire", +"install", +"intact", +"interest", +"into", +"invest", +"invite", +"involve", +"iron", +"island", +"isolate", +"issue", +"item", +"ivory", +"jacket", +"jaguar", +"jar", +"jazz", +"jealous", +"jeans", +"jelly", +"jewel", +"job", +"join", +"joke", +"journey", +"joy", +"judge", +"juice", +"jump", +"jungle", +"junior", +"junk", +"just", +"kangaroo", +"keen", +"keep", +"ketchup", +"key", +"kick", +"kid", +"kidney", +"kind", +"kingdom", +"kiss", +"kit", +"kitchen", +"kite", +"kitten", +"kiwi", +"knee", +"knife", +"knock", +"know", +"lab", +"label", +"labor", +"ladder", +"lady", +"lake", +"lamp", +"language", +"laptop", +"large", +"later", +"latin", +"laugh", +"laundry", +"lava", +"law", +"lawn", +"lawsuit", +"layer", +"lazy", +"leader", +"leaf", +"learn", +"leave", +"lecture", +"left", +"leg", +"legal", +"legend", +"leisure", +"lemon", +"lend", +"length", +"lens", +"leopard", +"lesson", +"letter", +"level", +"liar", +"liberty", +"library", +"license", +"life", +"lift", +"light", +"like", +"limb", +"limit", +"link", +"lion", +"liquid", +"list", +"little", +"live", +"lizard", +"load", +"loan", +"lobster", +"local", +"lock", +"logic", +"lonely", +"long", +"loop", +"lottery", +"loud", +"lounge", +"love", +"loyal", +"lucky", +"luggage", +"lumber", +"lunar", +"lunch", +"luxury", +"lyrics", +"machine", +"mad", +"magic", +"magnet", +"maid", +"mail", +"main", +"major", +"make", +"mammal", +"man", +"manage", +"mandate", +"mango", +"mansion", +"manual", +"maple", +"marble", +"march", +"margin", +"marine", +"market", +"marriage", +"mask", +"mass", +"master", +"match", +"material", +"math", +"matrix", +"matter", +"maximum", +"maze", +"meadow", +"mean", +"measure", +"meat", +"mechanic", +"medal", +"media", +"melody", +"melt", +"member", +"memory", +"mention", +"menu", +"mercy", +"merge", +"merit", +"merry", +"mesh", +"message", +"metal", +"method", +"middle", +"midnight", +"milk", +"million", +"mimic", +"mind", +"minimum", +"minor", +"minute", +"miracle", +"mirror", +"misery", +"miss", +"mistake", +"mix", +"mixed", +"mixture", +"mobile", +"model", +"modify", +"mom", +"moment", +"monitor", +"monkey", +"monster", +"month", +"moon", +"moral", +"more", +"morning", +"mosquito", +"mother", +"motion", +"motor", +"mountain", +"mouse", +"move", +"movie", +"much", +"muffin", +"mule", +"multiply", +"muscle", +"museum", +"mushroom", +"music", +"must", +"mutual", +"myself", +"mystery", +"myth", +"naive", +"name", +"napkin", +"narrow", +"nasty", +"nation", +"nature", +"near", +"neck", +"need", +"negative", +"neglect", +"neither", +"nephew", +"nerve", +"nest", +"net", +"network", +"neutral", +"never", +"news", +"next", +"nice", +"night", +"noble", +"noise", +"nominee", +"noodle", +"normal", +"north", +"nose", +"notable", +"note", +"nothing", +"notice", +"novel", +"now", +"nuclear", +"number", +"nurse", +"nut", +"oak", +"obey", +"object", +"oblige", +"obscure", +"observe", +"obtain", +"obvious", +"occur", +"ocean", +"october", +"odor", +"off", +"offer", +"office", +"often", +"oil", +"okay", +"old", +"olive", +"olympic", +"omit", +"once", +"one", +"onion", +"online", +"only", +"open", +"opera", +"opinion", +"oppose", +"option", +"orange", +"orbit", +"orchard", +"order", +"ordinary", +"organ", +"orient", +"original", +"orphan", +"ostrich", +"other", +"outdoor", +"outer", +"output", +"outside", +"oval", +"oven", +"over", +"own", +"owner", +"oxygen", +"oyster", +"ozone", +"pact", +"paddle", +"page", +"pair", +"palace", +"palm", +"panda", +"panel", +"panic", +"panther", +"paper", +"parade", +"parent", +"park", +"parrot", +"party", +"pass", +"patch", +"path", +"patient", +"patrol", +"pattern", +"pause", +"pave", +"payment", +"peace", +"peanut", +"pear", +"peasant", +"pelican", +"pen", +"penalty", +"pencil", +"people", +"pepper", +"perfect", +"permit", +"person", +"pet", +"phone", +"photo", +"phrase", +"physical", +"piano", +"picnic", +"picture", +"piece", +"pig", +"pigeon", +"pill", +"pilot", +"pink", +"pioneer", +"pipe", +"pistol", +"pitch", +"pizza", +"place", +"planet", +"plastic", +"plate", +"play", +"please", +"pledge", +"pluck", +"plug", +"plunge", +"poem", +"poet", +"point", +"polar", +"pole", +"police", +"pond", +"pony", +"pool", +"popular", +"portion", +"position", +"possible", +"post", +"potato", +"pottery", +"poverty", +"powder", +"power", +"practice", +"praise", +"predict", +"prefer", +"prepare", +"present", +"pretty", +"prevent", +"price", +"pride", +"primary", +"print", +"priority", +"prison", +"private", +"prize", +"problem", +"process", +"produce", +"profit", +"program", +"project", +"promote", +"proof", +"property", +"prosper", +"protect", +"proud", +"provide", +"public", +"pudding", +"pull", +"pulp", +"pulse", +"pumpkin", +"punch", +"pupil", +"puppy", +"purchase", +"purity", +"purpose", +"purse", +"push", +"put", +"puzzle", +"pyramid", +"quality", +"quantum", +"quarter", +"question", +"quick", +"quit", +"quiz", +"quote", +"rabbit", +"raccoon", +"race", +"rack", +"radar", +"radio", +"rail", +"rain", +"raise", +"rally", +"ramp", +"ranch", +"random", +"range", +"rapid", +"rare", +"rate", +"rather", +"raven", +"raw", +"razor", +"ready", +"real", +"reason", +"rebel", +"rebuild", +"recall", +"receive", +"recipe", +"record", +"recycle", +"reduce", +"reflect", +"reform", +"refuse", +"region", +"regret", +"regular", +"reject", +"relax", +"release", +"relief", +"rely", +"remain", +"remember", +"remind", +"remove", +"render", +"renew", +"rent", +"reopen", +"repair", +"repeat", +"replace", +"report", +"require", +"rescue", +"resemble", +"resist", +"resource", +"response", +"result", +"retire", +"retreat", +"return", +"reunion", +"reveal", +"review", +"reward", +"rhythm", +"rib", +"ribbon", +"rice", +"rich", +"ride", +"ridge", +"rifle", +"right", +"rigid", +"ring", +"riot", +"ripple", +"risk", +"ritual", +"rival", +"river", +"road", +"roast", +"robot", +"robust", +"rocket", +"romance", +"roof", +"rookie", +"room", +"rose", +"rotate", +"rough", +"round", +"route", +"royal", +"rubber", +"rude", +"rug", +"rule", +"run", +"runway", +"rural", +"sad", +"saddle", +"sadness", +"safe", +"sail", +"salad", +"salmon", +"salon", +"salt", +"salute", +"same", +"sample", +"sand", +"satisfy", +"satoshi", +"sauce", +"sausage", +"save", +"say", +"scale", +"scan", +"scare", +"scatter", +"scene", +"scheme", +"school", +"science", +"scissors", +"scorpion", +"scout", +"scrap", +"screen", +"script", +"scrub", +"sea", +"search", +"season", +"seat", +"second", +"secret", +"section", +"security", +"seed", +"seek", +"segment", +"select", +"sell", +"seminar", +"senior", +"sense", +"sentence", +"series", +"service", +"session", +"settle", +"setup", +"seven", +"shadow", +"shaft", +"shallow", +"share", +"shed", +"shell", +"sheriff", +"shield", +"shift", +"shine", +"ship", +"shiver", +"shock", +"shoe", +"shoot", +"shop", +"short", +"shoulder", +"shove", +"shrimp", +"shrug", +"shuffle", +"shy", +"sibling", +"sick", +"side", +"siege", +"sight", +"sign", +"silent", +"silk", +"silly", +"silver", +"similar", +"simple", +"since", +"sing", +"siren", +"sister", +"situate", +"six", +"size", +"skate", +"sketch", +"ski", +"skill", +"skin", +"skirt", +"skull", +"slab", +"slam", +"sleep", +"slender", +"slice", +"slide", +"slight", +"slim", +"slogan", +"slot", +"slow", +"slush", +"small", +"smart", +"smile", +"smoke", +"smooth", +"snack", +"snake", +"snap", +"sniff", +"snow", +"soap", +"soccer", +"social", +"sock", +"soda", +"soft", +"solar", +"soldier", +"solid", +"solution", +"solve", +"someone", +"song", +"soon", +"sorry", +"sort", +"soul", +"sound", +"soup", +"source", +"south", +"space", +"spare", +"spatial", +"spawn", +"speak", +"special", +"speed", +"spell", +"spend", +"sphere", +"spice", +"spider", +"spike", +"spin", +"spirit", +"split", +"spoil", +"sponsor", +"spoon", +"sport", +"spot", +"spray", +"spread", +"spring", +"spy", +"square", +"squeeze", +"squirrel", +"stable", +"stadium", +"staff", +"stage", +"stairs", +"stamp", +"stand", +"start", +"state", +"stay", +"steak", +"steel", +"stem", +"step", +"stereo", +"stick", +"still", +"sting", +"stock", +"stomach", +"stone", +"stool", +"story", +"stove", +"strategy", +"street", +"strike", +"strong", +"struggle", +"student", +"stuff", +"stumble", +"style", +"subject", +"submit", +"subway", +"success", +"such", +"sudden", +"suffer", +"sugar", +"suggest", +"suit", +"summer", +"sun", +"sunny", +"sunset", +"super", +"supply", +"supreme", +"sure", +"surface", +"surge", +"surprise", +"surround", +"survey", +"suspect", +"sustain", +"swallow", +"swamp", +"swap", +"swarm", +"swear", +"sweet", +"swift", +"swim", +"swing", +"switch", +"sword", +"symbol", +"symptom", +"syrup", +"system", +"table", +"tackle", +"tag", +"tail", +"talent", +"talk", +"tank", +"tape", +"target", +"task", +"taste", +"tattoo", +"taxi", +"teach", +"team", +"tell", +"ten", +"tenant", +"tennis", +"tent", +"term", +"test", +"text", +"thank", +"that", +"theme", +"then", +"theory", +"there", +"they", +"thing", +"this", +"thought", +"three", +"thrive", +"throw", +"thumb", +"thunder", +"ticket", +"tide", +"tiger", +"tilt", +"timber", +"time", +"tiny", +"tip", +"tired", +"tissue", +"title", +"toast", +"tobacco", +"today", +"toddler", +"toe", +"together", +"toilet", +"token", +"tomato", +"tomorrow", +"tone", +"tongue", +"tonight", +"tool", +"tooth", +"top", +"topic", +"topple", +"torch", +"tornado", +"tortoise", +"toss", +"total", +"tourist", +"toward", +"tower", +"town", +"toy", +"track", +"trade", +"traffic", +"tragic", +"train", +"transfer", +"trap", +"trash", +"travel", +"tray", +"treat", +"tree", +"trend", +"trial", +"tribe", +"trick", +"trigger", +"trim", +"trip", +"trophy", +"trouble", +"truck", +"true", +"truly", +"trumpet", +"trust", +"truth", +"try", +"tube", +"tuition", +"tumble", +"tuna", +"tunnel", +"turkey", +"turn", +"turtle", +"twelve", +"twenty", +"twice", +"twin", +"twist", +"two", +"type", +"typical", +"ugly", +"umbrella", +"unable", +"unaware", +"uncle", +"uncover", +"under", +"undo", +"unfair", +"unfold", +"unhappy", +"uniform", +"unique", +"unit", +"universe", +"unknown", +"unlock", +"until", +"unusual", +"unveil", +"update", +"upgrade", +"uphold", +"upon", +"upper", +"upset", +"urban", +"urge", +"usage", +"use", +"used", +"useful", +"useless", +"usual", +"utility", +"vacant", +"vacuum", +"vague", +"valid", +"valley", +"valve", +"van", +"vanish", +"vapor", +"various", +"vast", +"vault", +"vehicle", +"velvet", +"vendor", +"venture", +"venue", +"verb", +"verify", +"version", +"very", +"vessel", +"veteran", +"viable", +"vibrant", +"vicious", +"victory", +"video", +"view", +"village", +"vintage", +"violin", +"virtual", +"virus", +"visa", +"visit", +"visual", +"vital", +"vivid", +"vocal", +"voice", +"void", +"volcano", +"volume", +"vote", +"voyage", +"wage", +"wagon", +"wait", +"walk", +"wall", +"walnut", +"want", +"warfare", +"warm", +"warrior", +"wash", +"wasp", +"waste", +"water", +"wave", +"way", +"wealth", +"weapon", +"wear", +"weasel", +"weather", +"web", +"wedding", +"weekend", +"weird", +"welcome", +"west", +"wet", +"whale", +"what", +"wheat", +"wheel", +"when", +"where", +"whip", +"whisper", +"wide", +"width", +"wife", +"wild", +"will", +"win", +"window", +"wine", +"wing", +"wink", +"winner", +"winter", +"wire", +"wisdom", +"wise", +"wish", +"witness", +"wolf", +"woman", +"wonder", +"wood", +"wool", +"word", +"work", +"world", +"worry", +"worth", +"wrap", +"wreck", +"wrestle", +"wrist", +"write", +"wrong", +"yard", +"year", +"yellow", +"you", +"young", +"youth", +"zebra", +"zero", +"zone", +"zoo", +0, +}; diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 435afb7d1527..c7ba5f1b5ff8 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -142,8 +142,9 @@ class CMainParams : public CChainParams { base58Prefixes[EXT_PUBLIC_KEY] = boost::assign::list_of(0x04)(0x88)(0xB2)(0x1E).convert_to_container >(); // Dash BIP32 prvkeys start with 'xprv' (Bitcoin defaults) base58Prefixes[EXT_SECRET_KEY] = boost::assign::list_of(0x04)(0x88)(0xAD)(0xE4).convert_to_container >(); + // Dash BIP44 coin type is '5' - base58Prefixes[EXT_COIN_TYPE] = boost::assign::list_of(0x80)(0x00)(0x00)(0x05).convert_to_container >(); + nExtCoinType = 5; vFixedSeeds = std::vector(pnSeed6_main, pnSeed6_main + ARRAYLEN(pnSeed6_main)); @@ -260,8 +261,9 @@ class CTestNetParams : public CChainParams { base58Prefixes[EXT_PUBLIC_KEY] = boost::assign::list_of(0x04)(0x35)(0x87)(0xCF).convert_to_container >(); // Testnet Dash BIP32 prvkeys start with 'tprv' (Bitcoin defaults) base58Prefixes[EXT_SECRET_KEY] = boost::assign::list_of(0x04)(0x35)(0x83)(0x94).convert_to_container >(); + // Testnet Dash BIP44 coin type is '1' (All coin's testnet default) - base58Prefixes[EXT_COIN_TYPE] = boost::assign::list_of(0x80)(0x00)(0x00)(0x01).convert_to_container >(); + nExtCoinType = 1; vFixedSeeds = std::vector(pnSeed6_test, pnSeed6_test + ARRAYLEN(pnSeed6_test)); @@ -377,8 +379,9 @@ class CRegTestParams : public CChainParams { base58Prefixes[EXT_PUBLIC_KEY] = boost::assign::list_of(0x04)(0x35)(0x87)(0xCF).convert_to_container >(); // Regtest Dash BIP32 prvkeys start with 'tprv' (Bitcoin defaults) base58Prefixes[EXT_SECRET_KEY] = boost::assign::list_of(0x04)(0x35)(0x83)(0x94).convert_to_container >(); + // Regtest Dash BIP44 coin type is '1' (All coin's testnet default) - base58Prefixes[EXT_COIN_TYPE] = boost::assign::list_of(0x80)(0x00)(0x00)(0x01).convert_to_container >(); + nExtCoinType = 1; } }; static CRegTestParams regTestParams; diff --git a/src/chainparams.h b/src/chainparams.h index 4588983cd6a7..d1697a088150 100644 --- a/src/chainparams.h +++ b/src/chainparams.h @@ -48,7 +48,6 @@ class CChainParams SECRET_KEY, // BIP16 EXT_PUBLIC_KEY, // BIP32 EXT_SECRET_KEY, // BIP32 - EXT_COIN_TYPE, // BIP44 MAX_BASE58_TYPES }; @@ -75,6 +74,7 @@ class CChainParams std::string NetworkIDString() const { return strNetworkID; } const std::vector& DNSSeeds() const { return vSeeds; } const std::vector& Base58Prefix(Base58Type type) const { return base58Prefixes[type]; } + int ExtCoinType() const { return nExtCoinType; } const std::vector& FixedSeeds() const { return vFixedSeeds; } const CCheckpointData& Checkpoints() const { return checkpointData; } int PoolMaxTransactions() const { return nPoolMaxTransactions; } @@ -93,6 +93,7 @@ class CChainParams uint64_t nPruneAfterHeight; std::vector vSeeds; std::vector base58Prefixes[MAX_BASE58_TYPES]; + int nExtCoinType; std::string strNetworkID; CBlock genesis; std::vector vFixedSeeds; diff --git a/src/hdchain.cpp b/src/hdchain.cpp new file mode 100644 index 000000000000..e861e5bba7c9 --- /dev/null +++ b/src/hdchain.cpp @@ -0,0 +1,212 @@ +// Copyright (c) 2014-2017 The Dash Core developers +// Distributed under the MIT software license, see the accompanying + +#include "base58.h" +#include "bip39.h" +#include "chainparams.h" +#include "hdchain.h" +#include "tinyformat.h" +#include "util.h" +#include "utilstrencodings.h" + +bool CHDChain::SetNull() +{ + LOCK(cs_accounts); + nVersion = CURRENT_VERSION; + id = uint256(); + fCrypted = false; + vchSeed.clear(); + vchMnemonic.clear(); + vchMnemonicPassphrase.clear(); + mapAccounts.clear(); + // default blank account + mapAccounts.insert(std::pair(0, CHDAccount())); + return IsNull(); +} + +bool CHDChain::IsNull() const +{ + return vchSeed.empty() || id == uint256(); +} + +void CHDChain::SetCrypted(bool fCryptedIn) +{ + fCrypted = fCryptedIn; +} + +bool CHDChain::IsCrypted() const +{ + return fCrypted; +} + +void CHDChain::Debug(std::string strName) const +{ + DBG( + std::cout << __func__ << ": ---" << strName << "---" << std::endl; + if (fCrypted) { + std::cout << "mnemonic: ***CRYPTED***" << std::endl; + std::cout << "mnemonicpassphrase: ***CRYPTED***" << std::endl; + std::cout << "seed: ***CRYPTED***" << std::endl; + } else { + std::cout << "mnemonic: " << std::string(vchMnemonic.begin(), vchMnemonic.end()).c_str() << std::endl; + std::cout << "mnemonicpassphrase: " << std::string(vchMnemonicPassphrase.begin(), vchMnemonicPassphrase.end()).c_str() << std::endl; + std::cout << "seed: " << HexStr(vchSeed).c_str() << std::endl; + + CExtKey extkey; + extkey.SetMaster(&vchSeed[0], vchSeed.size()); + + CBitcoinExtKey b58extkey; + b58extkey.SetKey(extkey); + std::cout << "extended private masterkey: " << b58extkey.ToString().c_str() << std::endl; + + CExtPubKey extpubkey; + extpubkey = extkey.Neuter(); + + CBitcoinExtPubKey b58extpubkey; + b58extpubkey.SetKey(extpubkey); + std::cout << "extended public masterkey: " << b58extpubkey.ToString().c_str() << std::endl; + } + ); +} + +bool CHDChain::SetMnemonic(const SecureVector& vchMnemonic, const SecureVector& vchMnemonicPassphrase, bool fUpdateID) +{ + return SetMnemonic(SecureString(vchMnemonic.begin(), vchMnemonic.end()), SecureString(vchMnemonicPassphrase.begin(), vchMnemonicPassphrase.end()), fUpdateID); +} + +bool CHDChain::SetMnemonic(const SecureString& ssMnemonic, const SecureString& ssMnemonicPassphrase, bool fUpdateID) +{ + SecureString ssMnemonicTmp = ssMnemonic; + + if (fUpdateID) { + // can't (re)set mnemonic if seed was already set + if (!IsNull()) + return false; + + // empty mnemonic i.e. "generate a new one" + if (ssMnemonic.empty()) { + ssMnemonicTmp = CMnemonic::Generate(256); + } + // NOTE: default mnemonic passphrase is an empty string + + // printf("mnemonic: %s\n", ssMnemonicTmp.c_str()); + if (!CMnemonic::Check(ssMnemonicTmp)) { + throw std::runtime_error(std::string(__func__) + ": invalid mnemonic: `" + std::string(ssMnemonicTmp.c_str()) + "`"); + } + + CMnemonic::ToSeed(ssMnemonicTmp, ssMnemonicPassphrase, vchSeed); + id = GetSeedHash(); + } + + vchMnemonic = SecureVector(ssMnemonicTmp.begin(), ssMnemonicTmp.end()); + vchMnemonicPassphrase = SecureVector(ssMnemonicPassphrase.begin(), ssMnemonicPassphrase.end()); + + return !IsNull(); +} + +bool CHDChain::GetMnemonic(SecureVector& vchMnemonicRet, SecureVector& vchMnemonicPassphraseRet) const +{ + // mnemonic was not set, fail + if (vchMnemonic.empty()) + return false; + + vchMnemonicRet = vchMnemonic; + vchMnemonicPassphraseRet = vchMnemonicPassphrase; + return true; +} + +bool CHDChain::GetMnemonic(SecureString& ssMnemonicRet, SecureString& ssMnemonicPassphraseRet) const +{ + // mnemonic was not set, fail + if (vchMnemonic.empty()) + return false; + + ssMnemonicRet = SecureString(vchMnemonic.begin(), vchMnemonic.end()); + ssMnemonicPassphraseRet = SecureString(vchMnemonicPassphrase.begin(), vchMnemonicPassphrase.end()); + + return true; +} + +bool CHDChain::SetSeed(const SecureVector& vchSeedIn, bool fUpdateID) +{ + vchSeed = vchSeedIn; + + if (fUpdateID) { + id = GetSeedHash(); + } + + return !IsNull(); +} + +SecureVector CHDChain::GetSeed() const +{ + return vchSeed; +} + +uint256 CHDChain::GetSeedHash() +{ + return Hash(vchSeed.begin(), vchSeed.end()); +} + +void CHDChain::DeriveChildExtKey(uint32_t nAccountIndex, bool fInternal, uint32_t nChildIndex, CExtKey& extKeyRet) +{ + // Use BIP44 keypath scheme i.e. m / purpose' / coin_type' / account' / change / address_index + CExtKey masterKey; //hd master key + CExtKey purposeKey; //key at m/purpose' + CExtKey cointypeKey; //key at m/purpose'/coin_type' + CExtKey accountKey; //key at m/purpose'/coin_type'/account' + CExtKey changeKey; //key at m/purpose'/coin_type'/account'/change + CExtKey childKey; //key at m/purpose'/coin_type'/account'/change/address_index + + masterKey.SetMaster(&vchSeed[0], vchSeed.size()); + + // Use hardened derivation for purpose, coin_type and account + // (keys >= 0x80000000 are hardened after bip32) + + // derive m/purpose' + masterKey.Derive(purposeKey, 44 | 0x80000000); + // derive m/purpose'/coin_type' + purposeKey.Derive(cointypeKey, Params().ExtCoinType() | 0x80000000); + // derive m/purpose'/coin_type'/account' + cointypeKey.Derive(accountKey, nAccountIndex | 0x80000000); + // derive m/purpose'/coin_type'/account/change + accountKey.Derive(changeKey, fInternal ? 1 : 0); + // derive m/purpose'/coin_type'/account/change/address_index + changeKey.Derive(extKeyRet, nChildIndex); +} + +void CHDChain::AddAccount() +{ + LOCK(cs_accounts); + mapAccounts.insert(std::pair(mapAccounts.size(), CHDAccount())); +} + +bool CHDChain::GetAccount(uint32_t nAccountIndex, CHDAccount& hdAccountRet) +{ + LOCK(cs_accounts); + if (nAccountIndex > mapAccounts.size() - 1) + return false; + hdAccountRet = mapAccounts[nAccountIndex]; + return true; +} + +bool CHDChain::SetAccount(uint32_t nAccountIndex, const CHDAccount& hdAccount) +{ + LOCK(cs_accounts); + // can only replace existing accounts + if (nAccountIndex > mapAccounts.size() - 1) + return false; + mapAccounts[nAccountIndex] = hdAccount; + return true; +} + +size_t CHDChain::CountAccounts() +{ + LOCK(cs_accounts); + return mapAccounts.size(); +} + +std::string CHDPubKey::GetKeyPath() const +{ + return strprintf("m/44'/%d'/%d'/%d/%d", Params().ExtCoinType(), nAccountIndex, nChangeIndex, extPubKey.nChild); +} diff --git a/src/hdchain.h b/src/hdchain.h new file mode 100644 index 000000000000..ab3ee6ed8edc --- /dev/null +++ b/src/hdchain.h @@ -0,0 +1,152 @@ +// Copyright (c) 2014-2017 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +#ifndef DASH_HDCHAIN_H +#define DASH_HDCHAIN_H + +#include "key.h" +#include "sync.h" + +/* hd account data model */ +class CHDAccount +{ +public: + uint32_t nExternalChainCounter; + uint32_t nInternalChainCounter; + + CHDAccount() : nExternalChainCounter(0), nInternalChainCounter(0) {} + + ADD_SERIALIZE_METHODS; + template + inline void SerializationOp(Stream& s, Operation ser_action, int nType, int nVersion) + { + READWRITE(nExternalChainCounter); + READWRITE(nInternalChainCounter); + } +}; + +/* simple HD chain data model */ +class CHDChain +{ +private: + static const int CURRENT_VERSION = 1; + int nVersion; + + uint256 id; + + bool fCrypted; + + SecureVector vchSeed; + SecureVector vchMnemonic; + SecureVector vchMnemonicPassphrase; + + std::map mapAccounts; + // critical section to protect mapAccounts + mutable CCriticalSection cs_accounts; + +public: + + CHDChain() : nVersion(CHDChain::CURRENT_VERSION) { SetNull(); } + CHDChain(const CHDChain& other) : + nVersion(other.nVersion), + id(other.id), + fCrypted(other.fCrypted), + vchSeed(other.vchSeed), + vchMnemonic(other.vchMnemonic), + vchMnemonicPassphrase(other.vchMnemonicPassphrase), + mapAccounts(other.mapAccounts) + {} + + ADD_SERIALIZE_METHODS; + template + inline void SerializationOp(Stream& s, Operation ser_action, int nType, int nVersion) + { + LOCK(cs_accounts); + READWRITE(this->nVersion); + nVersion = this->nVersion; + READWRITE(id); + READWRITE(fCrypted); + READWRITE(vchSeed); + READWRITE(vchMnemonic); + READWRITE(vchMnemonicPassphrase); + READWRITE(mapAccounts); + } + + void swap(CHDChain& first, CHDChain& second) // nothrow + { + // enable ADL (not necessary in our case, but good practice) + using std::swap; + + // by swapping the members of two classes, + // the two classes are effectively swapped + swap(first.nVersion, second.nVersion); + swap(first.id, second.id); + swap(first.fCrypted, second.fCrypted); + swap(first.vchSeed, second.vchSeed); + swap(first.vchMnemonic, second.vchMnemonic); + swap(first.vchMnemonicPassphrase, second.vchMnemonicPassphrase); + swap(first.mapAccounts, second.mapAccounts); + } + CHDChain& operator=(CHDChain from) + { + swap(*this, from); + return *this; + } + + bool SetNull(); + bool IsNull() const; + + void SetCrypted(bool fCryptedIn); + bool IsCrypted() const; + + void Debug(std::string strName) const; + + bool SetMnemonic(const SecureVector& vchMnemonic, const SecureVector& vchMnemonicPassphrase, bool fUpdateID); + bool SetMnemonic(const SecureString& ssMnemonic, const SecureString& ssMnemonicPassphrase, bool fUpdateID); + bool GetMnemonic(SecureVector& vchMnemonicRet, SecureVector& vchMnemonicPassphraseRet) const; + bool GetMnemonic(SecureString& ssMnemonicRet, SecureString& ssMnemonicPassphraseRet) const; + + bool SetSeed(const SecureVector& vchSeedIn, bool fUpdateID); + SecureVector GetSeed() const; + + uint256 GetID() const { return id; } + + uint256 GetSeedHash(); + void DeriveChildExtKey(uint32_t nAccountIndex, bool fInternal, uint32_t nChildIndex, CExtKey& extKeyRet); + + void AddAccount(); + bool GetAccount(uint32_t nAccountIndex, CHDAccount& hdAccountRet); + bool SetAccount(uint32_t nAccountIndex, const CHDAccount& hdAccount); + size_t CountAccounts(); +}; + +/* hd pubkey data model */ +class CHDPubKey +{ +private: + static const int CURRENT_VERSION = 1; + int nVersion; + +public: + CExtPubKey extPubKey; + uint256 hdchainID; + uint32_t nAccountIndex; + uint32_t nChangeIndex; + + CHDPubKey() : nVersion(CHDPubKey::CURRENT_VERSION), nAccountIndex(0), nChangeIndex(0) {} + + ADD_SERIALIZE_METHODS; + template + inline void SerializationOp(Stream& s, Operation ser_action, int nType, int nVersion) + { + READWRITE(this->nVersion); + nVersion = this->nVersion; + READWRITE(extPubKey); + READWRITE(hdchainID); + READWRITE(nAccountIndex); + READWRITE(nChangeIndex); + } + + std::string GetKeyPath() const; +}; + +#endif // DASH_HDCHAIN_H diff --git a/src/init.cpp b/src/init.cpp index 8435caad413a..e01c1792345b 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -477,6 +477,10 @@ std::string HelpMessage(HelpMessageMode mode) strUsage += HelpMessageOpt("-txconfirmtarget=", strprintf(_("If paytxfee is not set, include enough fee so transactions begin confirmation on average within n blocks (default: %u)"), DEFAULT_TX_CONFIRM_TARGET)); strUsage += HelpMessageOpt("-maxtxfee=", strprintf(_("Maximum total fees (in %s) to use in a single wallet transaction; setting this too low may abort large transactions (default: %s)"), CURRENCY_UNIT, FormatMoney(DEFAULT_TRANSACTION_MAXFEE))); + strUsage += HelpMessageOpt("-usehd", _("Use hierarchical deterministic key generation (HD) after bip32. Only has effect during wallet creation/first start") + " " + strprintf(_("(default: %u)"), DEFAULT_USE_HD_WALLET)); + strUsage += HelpMessageOpt("-mnemonic", _("User defined mnemonic for HD wallet (bip39). Only has effect during wallet creation/first start (default: randomly generated)")); + strUsage += HelpMessageOpt("-mnemonicpassphrase", _("User defined memonic passphrase for HD wallet (bip39). Only has effect during wallet creation/first start (default: randomly generated)")); + strUsage += HelpMessageOpt("-hdseed", _("User defined seed for HD wallet (should be in hex). Only has effect during wallet creation/first start (default: randomly generated)")); strUsage += HelpMessageOpt("-upgradewallet", _("Upgrade wallet to latest format on startup")); strUsage += HelpMessageOpt("-wallet=", _("Specify wallet file (within data directory)") + " " + strprintf(_("(default: %s)"), "wallet.dat")); strUsage += HelpMessageOpt("-walletbroadcast", _("Make the wallet broadcast transactions") + " " + strprintf(_("(default: %u)"), DEFAULT_WALLETBROADCAST)); @@ -892,6 +896,12 @@ void InitParameterInteraction() mapArgs["-privatesendmultisession"] = "0"; LogPrintf("%s: parameter interaction: -liquidityprovider=%d -> setting -privatesendmultisession=0\n", __func__, nLiqProvTmp); } + + if (mapArgs.count("-hdseed") && IsHex(GetArg("-hdseed", "not hex")) && (mapArgs.count("-mnemonic") || mapArgs.count("-mnemonicpassphrase"))) { + mapArgs.erase("-mnemonic"); + mapArgs.erase("-mnemonicpassphrase"); + LogPrintf("%s: parameter interaction: can't use -hdseed and -mnemonic/-mnemonicpassphrase together, will prefer -seed\n", __func__); + } } void InitLogging() @@ -1661,8 +1671,15 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) // Create new keyUser and set as default key RandAddSeedPerfmon(); + if (GetBoolArg("-usehd", DEFAULT_USE_HD_WALLET) && !pwalletMain->IsHDEnabled()) { + if (GetArg("-mnemonicpassphrase", "").size() > 256) + return InitError(_("Mnemonic passphrase is too long, must be at most 256 characters")); + // generate a new master key + pwalletMain->GenerateNewHDChain(); + } + CPubKey newDefaultKey; - if (pwalletMain->GetKeyFromPool(newDefaultKey)) { + if (pwalletMain->GetKeyFromPool(newDefaultKey, false)) { pwalletMain->SetDefaultKey(newDefaultKey); if (!pwalletMain->SetAddressBook(pwalletMain->vchDefaultKey.GetID(), "", "receive")) strErrors << _("Cannot write default address") << "\n"; @@ -1681,6 +1698,18 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) } } + else if (mapArgs.count("-usehd")) { + bool useHD = GetBoolArg("-usehd", DEFAULT_USE_HD_WALLET); + if (pwalletMain->IsHDEnabled() && !useHD) + return InitError(strprintf(_("Error loading %s: You can't disable HD on a already existing HD wallet"), strWalletFile)); + if (!pwalletMain->IsHDEnabled() && useHD) + return InitError(strprintf(_("Error loading %s: You can't enable HD on a already existing non-HD wallet"), strWalletFile)); + } + + // Warn user every time he starts non-encrypted HD wallet + if (GetBoolArg("-usehd", DEFAULT_USE_HD_WALLET) && !pwalletMain->IsLocked()) { + InitWarning(_("Make sure to encrypt your wallet and delete all non-encrypted backups after you verified that wallet works!")); + } LogPrintf("%s", strErrors.str()); LogPrintf(" wallet %15dms\n", GetTimeMillis() - nStart); @@ -1939,9 +1968,15 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) LogPrintf("mapBlockIndex.size() = %u\n", mapBlockIndex.size()); LogPrintf("chainActive.Height() = %d\n", chainActive.Height()); #ifdef ENABLE_WALLET - LogPrintf("setKeyPool.size() = %u\n", pwalletMain ? pwalletMain->setKeyPool.size() : 0); - LogPrintf("mapWallet.size() = %u\n", pwalletMain ? pwalletMain->mapWallet.size() : 0); - LogPrintf("mapAddressBook.size() = %u\n", pwalletMain ? pwalletMain->mapAddressBook.size() : 0); + if (pwalletMain) { + LOCK(pwalletMain->cs_wallet); + LogPrintf("setExternalKeyPool.size() = %u\n", pwalletMain->KeypoolCountExternalKeys()); + LogPrintf("setInternalKeyPool.size() = %u\n", pwalletMain->KeypoolCountInternalKeys()); + LogPrintf("mapWallet.size() = %u\n", pwalletMain->mapWallet.size()); + LogPrintf("mapAddressBook.size() = %u\n", pwalletMain->mapAddressBook.size()); + } else { + LogPrintf("wallet is NULL\n"); + } #endif if (GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)) diff --git a/src/key.h b/src/key.h index 6c820d49cd5f..d4e85e61a0dc 100644 --- a/src/key.h +++ b/src/key.h @@ -169,6 +169,23 @@ struct CExtKey { bool Derive(CExtKey& out, unsigned int nChild) const; CExtPubKey Neuter() const; void SetMaster(const unsigned char* seed, unsigned int nSeedLen); + template + void Serialize(Stream& s, int nType, int nVersion) const + { + unsigned int len = 74; + ::WriteCompactSize(s, len); + unsigned char code[74]; + Encode(code); + s.write((const char *)&code[0], len); + } + template + void Unserialize(Stream& s, int nType, int nVersion) + { + unsigned int len = ::ReadCompactSize(s); + unsigned char code[74]; + s.read((char *)&code[0], len); + Decode(code); + } }; /** Initialize the elliptic curve support. May not be called twice without calling ECC_Stop first. */ diff --git a/src/keystore.cpp b/src/keystore.cpp index d568a7435098..cc4b1dc4f086 100644 --- a/src/keystore.cpp +++ b/src/keystore.cpp @@ -113,3 +113,9 @@ bool CBasicKeyStore::HaveWatchOnly() const LOCK(cs_KeyStore); return (!setWatchOnly.empty()); } + +bool CBasicKeyStore::GetHDChain(CHDChain& hdChainRet) const +{ + hdChainRet = hdChain; + return !hdChain.IsNull(); +} diff --git a/src/keystore.h b/src/keystore.h index d9290722e1b1..ed103659ecfe 100644 --- a/src/keystore.h +++ b/src/keystore.h @@ -6,6 +6,7 @@ #ifndef BITCOIN_KEYSTORE_H #define BITCOIN_KEYSTORE_H +#include "hdchain.h" #include "key.h" #include "pubkey.h" #include "script/script.h" @@ -59,6 +60,8 @@ class CBasicKeyStore : public CKeyStore WatchKeyMap mapWatchKeys; ScriptMap mapScripts; WatchOnlySet setWatchOnly; + /* the HD chain data model*/ + CHDChain hdChain; public: bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey); @@ -106,6 +109,8 @@ class CBasicKeyStore : public CKeyStore virtual bool RemoveWatchOnly(const CScript &dest); virtual bool HaveWatchOnly(const CScript &dest) const; virtual bool HaveWatchOnly() const; + + bool GetHDChain(CHDChain& hdChainRet) const; }; typedef std::vector > CKeyingMaterial; diff --git a/src/privatesend-client.cpp b/src/privatesend-client.cpp index 46ae26056abf..064e9c7bbbff 100644 --- a/src/privatesend-client.cpp +++ b/src/privatesend-client.cpp @@ -1070,15 +1070,15 @@ bool CPrivateSendClient::PrepareDenominate(int nMinRounds, int nMaxRounds, std:: vecTxIn.erase(it); vCoins.erase(it2); - CScript scriptChange; + CScript scriptDenom; CPubKey vchPubKey; - // use a unique change address - assert(reservekey.GetReservedKey(vchPubKey)); // should never fail, as we just unlocked - scriptChange = GetScriptForDestination(vchPubKey.GetID()); + // use unique address + assert(reservekey.GetReservedKey(vchPubKey, false)); // should never fail, as we just unlocked + scriptDenom = GetScriptForDestination(vchPubKey.GetID()); reservekey.KeepKey(); // add new output - CTxOut txout(nValueDenom, scriptChange); + CTxOut txout(nValueDenom, scriptDenom); vecTxOutRet.push_back(txout); // subtract denomination amount @@ -1151,7 +1151,7 @@ bool CPrivateSendClient::MakeCollateralAmounts(const CompactTallyItem& tallyItem CScript scriptCollateral; CPubKey vchPubKey; - assert(reservekeyCollateral.GetReservedKey(vchPubKey)); // should never fail, as we just unlocked + assert(reservekeyCollateral.GetReservedKey(vchPubKey, false)); // should never fail, as we just unlocked scriptCollateral = GetScriptForDestination(vchPubKey.GetID()); vecSend.push_back((CRecipient){scriptCollateral, PRIVATESEND_COLLATERAL*4, false}); @@ -1231,7 +1231,7 @@ bool CPrivateSendClient::CreateDenominated(const CompactTallyItem& tallyItem, bo CScript scriptCollateral; CPubKey vchPubKey; - assert(reservekeyCollateral.GetReservedKey(vchPubKey)); // should never fail, as we just unlocked + assert(reservekeyCollateral.GetReservedKey(vchPubKey, false)); // should never fail, as we just unlocked scriptCollateral = GetScriptForDestination(vchPubKey.GetID()); // ****** Add collateral outputs ************ / @@ -1275,11 +1275,11 @@ bool CPrivateSendClient::CreateDenominated(const CompactTallyItem& tallyItem, bo while(nValueLeft - nDenomValue >= 0 && nOutputs <= 10) { CScript scriptDenom; CPubKey vchPubKey; - //use a unique change address + // use a unique address std::shared_ptr reservekeyDenom = std::make_shared(pwalletMain); reservekeyDenomVec.push_back(reservekeyDenom); - assert(reservekeyDenom->GetReservedKey(vchPubKey)); // should never fail, as we just unlocked + assert(reservekeyDenom->GetReservedKey(vchPubKey, false)); // should never fail, as we just unlocked scriptDenom = GetScriptForDestination(vchPubKey.GetID()); vecSend.push_back((CRecipient){ scriptDenom, nDenomValue, false }); diff --git a/src/pubkey.h b/src/pubkey.h index e1a17b658261..bb47485a7361 100644 --- a/src/pubkey.h +++ b/src/pubkey.h @@ -208,6 +208,30 @@ struct CExtPubKey { void Encode(unsigned char code[74]) const; void Decode(const unsigned char code[74]); bool Derive(CExtPubKey& out, unsigned int nChild) const; + + unsigned int GetSerializeSize(int nType, int nVersion) const + { + return 74+1; //add one byte for the size (compact int) + } + template + void Serialize(Stream& s, int nType, int nVersion) const + { + unsigned int len = 74; + ::WriteCompactSize(s, len); + unsigned char code[74]; + Encode(code); + s.write((const char *)&code[0], len); + } + template + void Unserialize(Stream& s, int nType, int nVersion) + { + unsigned int len = ::ReadCompactSize(s); + unsigned char code[74]; + if (len != 74) + throw std::runtime_error("Invalid extended key size\n"); + s.read((char *)&code[0], len); + Decode(code); + } }; /** Users of this module must hold an ECCVerifyHandle. The constructor and diff --git a/src/qt/addresstablemodel.cpp b/src/qt/addresstablemodel.cpp index 92e0a3946bb7..3339abf83db9 100644 --- a/src/qt/addresstablemodel.cpp +++ b/src/qt/addresstablemodel.cpp @@ -371,7 +371,7 @@ QString AddressTableModel::addRow(const QString &type, const QString &label, con { // Generate a new address to associate with given label CPubKey newKey; - if(!wallet->GetKeyFromPool(newKey)) + if(!wallet->GetKeyFromPool(newKey, false)) { WalletModel::UnlockContext ctx(walletModel->requestUnlock()); if(!ctx.isValid()) @@ -380,7 +380,7 @@ QString AddressTableModel::addRow(const QString &type, const QString &label, con editStatus = WALLET_UNLOCK_FAILURE; return QString(); } - if(!wallet->GetKeyFromPool(newKey)) + if(!wallet->GetKeyFromPool(newKey, false)) { editStatus = KEY_GENERATION_FAILURE; return QString(); diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index aeca0641af26..d9e666eac3df 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -80,6 +80,7 @@ BitcoinGUI::BitcoinGUI(const PlatformStyle *platformStyle, const NetworkStyle *n walletFrame(0), unitDisplayControl(0), labelEncryptionIcon(0), + labelWalletHDStatusIcon(0), labelConnectionsIcon(0), labelBlocksIcon(0), progressBarLabel(0), @@ -201,6 +202,7 @@ BitcoinGUI::BitcoinGUI(const PlatformStyle *platformStyle, const NetworkStyle *n frameBlocksLayout->setSpacing(3); unitDisplayControl = new UnitDisplayStatusBarControl(platformStyle); labelEncryptionIcon = new QLabel(); + labelWalletHDStatusIcon = new QLabel(); labelConnectionsIcon = new QPushButton(); labelConnectionsIcon->setFlat(true); // Make the button look like a label, but clickable labelConnectionsIcon->setStyleSheet(".QPushButton { background-color: rgba(255, 255, 255, 0);}"); @@ -215,6 +217,7 @@ BitcoinGUI::BitcoinGUI(const PlatformStyle *platformStyle, const NetworkStyle *n frameBlocksLayout->addWidget(unitDisplayControl); frameBlocksLayout->addStretch(); frameBlocksLayout->addWidget(labelEncryptionIcon); + frameBlocksLayout->addWidget(labelWalletHDStatusIcon); } frameBlocksLayout->addStretch(); frameBlocksLayout->addWidget(labelConnectionsIcon); @@ -1208,6 +1211,17 @@ bool BitcoinGUI::handlePaymentRequest(const SendCoinsRecipient& recipient) return false; } +void BitcoinGUI::setHDStatus(int hdEnabled) +{ + QString theme = GUIUtil::getThemeName(); + + labelWalletHDStatusIcon->setPixmap(platformStyle->SingleColorIcon(hdEnabled ? ":/icons/" + theme + "/hd_enabled" : ":/icons/" + theme + "/hd_disabled").pixmap(STATUSBAR_ICONSIZE,STATUSBAR_ICONSIZE)); + labelWalletHDStatusIcon->setToolTip(hdEnabled ? tr("HD key generation is enabled") : tr("HD key generation is disabled")); + + // eventually disable the QLabel to set its opacity to 50% + labelWalletHDStatusIcon->setEnabled(hdEnabled); +} + void BitcoinGUI::setEncryptionStatus(int status) { QString theme = GUIUtil::getThemeName(); diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index c42b6cd14a65..7f518be50062 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -85,6 +85,7 @@ class BitcoinGUI : public QMainWindow UnitDisplayStatusBarControl *unitDisplayControl; QLabel *labelEncryptionIcon; + QLabel *labelWalletHDStatusIcon; QPushButton *labelConnectionsIcon; QLabel *labelBlocksIcon; QLabel *progressBarLabel; @@ -183,6 +184,12 @@ public Q_SLOTS: void message(const QString &title, const QString &message, unsigned int style, bool *ret = NULL); #ifdef ENABLE_WALLET + /** Set the hd-enabled status as shown in the UI. + @param[in] status current hd enabled status + @see WalletModel::EncryptionStatus + */ + void setHDStatus(int hdEnabled); + /** Set the encryption status as shown in the UI. @param[in] status current encryption status @see WalletModel::EncryptionStatus diff --git a/src/qt/dash.qrc b/src/qt/dash.qrc index 8f733941c7ab..29d6025c9e2f 100644 --- a/src/qt/dash.qrc +++ b/src/qt/dash.qrc @@ -51,6 +51,8 @@ res/icons/drkblue/about.png res/icons/drkblue/about_qt.png res/icons/drkblue/verify.png + res/icons/drkblue/hd_enabled.png + res/icons/drkblue/hd_disabled.png res/icons/crownium/address-book.png @@ -101,6 +103,8 @@ res/icons/crownium/about.png res/icons/crownium/about_qt.png res/icons/crownium/verify.png + res/icons/crownium/hd_enabled.png + res/icons/crownium/hd_disabled.png res/icons/light/address-book.png @@ -151,6 +155,8 @@ res/icons/light/about.png res/icons/light/about_qt.png res/icons/light/verify.png + res/icons/light/hd_enabled.png + res/icons/light/hd_disabled.png res/icons/trad/address-book.png @@ -201,6 +207,8 @@ res/icons/trad/about.png res/icons/trad/about_qt.png res/icons/trad/verify.png + res/icons/trad/hd_enabled.png + res/icons/trad/hd_disabled.png res/css/drkblue.css diff --git a/src/qt/paymentserver.cpp b/src/qt/paymentserver.cpp index 440eaf77e68b..a15a43e5f33c 100644 --- a/src/qt/paymentserver.cpp +++ b/src/qt/paymentserver.cpp @@ -649,7 +649,7 @@ void PaymentServer::fetchPaymentACK(CWallet* wallet, SendCoinsRecipient recipien } else { CPubKey newKey; - if (wallet->GetKeyFromPool(newKey)) { + if (wallet->GetKeyFromPool(newKey, false)) { CKeyID keyID = newKey.GetID(); wallet->SetAddressBook(keyID, strAccount, "refund"); diff --git a/src/qt/res/icons/crownium/hd_disabled.png b/src/qt/res/icons/crownium/hd_disabled.png new file mode 100644 index 000000000000..687b6d2e38e9 Binary files /dev/null and b/src/qt/res/icons/crownium/hd_disabled.png differ diff --git a/src/qt/res/icons/crownium/hd_enabled.png b/src/qt/res/icons/crownium/hd_enabled.png new file mode 100644 index 000000000000..568dde1cd1c1 Binary files /dev/null and b/src/qt/res/icons/crownium/hd_enabled.png differ diff --git a/src/qt/res/icons/drkblue/hd_disabled.png b/src/qt/res/icons/drkblue/hd_disabled.png new file mode 100644 index 000000000000..687b6d2e38e9 Binary files /dev/null and b/src/qt/res/icons/drkblue/hd_disabled.png differ diff --git a/src/qt/res/icons/drkblue/hd_enabled.png b/src/qt/res/icons/drkblue/hd_enabled.png new file mode 100644 index 000000000000..568dde1cd1c1 Binary files /dev/null and b/src/qt/res/icons/drkblue/hd_enabled.png differ diff --git a/src/qt/res/icons/light/hd_disabled.png b/src/qt/res/icons/light/hd_disabled.png new file mode 100644 index 000000000000..687b6d2e38e9 Binary files /dev/null and b/src/qt/res/icons/light/hd_disabled.png differ diff --git a/src/qt/res/icons/light/hd_enabled.png b/src/qt/res/icons/light/hd_enabled.png new file mode 100644 index 000000000000..568dde1cd1c1 Binary files /dev/null and b/src/qt/res/icons/light/hd_enabled.png differ diff --git a/src/qt/res/icons/trad/hd_disabled.png b/src/qt/res/icons/trad/hd_disabled.png new file mode 100644 index 000000000000..687b6d2e38e9 Binary files /dev/null and b/src/qt/res/icons/trad/hd_disabled.png differ diff --git a/src/qt/res/icons/trad/hd_enabled.png b/src/qt/res/icons/trad/hd_enabled.png new file mode 100644 index 000000000000..568dde1cd1c1 Binary files /dev/null and b/src/qt/res/icons/trad/hd_enabled.png differ diff --git a/src/qt/res/src/hd_disabled.svg b/src/qt/res/src/hd_disabled.svg new file mode 100644 index 000000000000..ab576cdd6751 --- /dev/null +++ b/src/qt/res/src/hd_disabled.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/qt/res/src/hd_enabled.svg b/src/qt/res/src/hd_enabled.svg new file mode 100644 index 000000000000..e55fbecc83a6 --- /dev/null +++ b/src/qt/res/src/hd_enabled.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index d8f5836ded0d..8261d685d1d3 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -742,3 +742,8 @@ bool WalletModel::saveReceiveRequest(const std::string &sAddress, const int64_t else return wallet->AddDestData(dest, key, sRequest); } + +bool WalletModel::hdEnabled() const +{ + return wallet->IsHDEnabled(); +} diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index 369df01f0f2f..055a94fbbc2c 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -207,6 +207,8 @@ class WalletModel : public QObject void loadReceiveRequests(std::vector& vReceiveRequests); bool saveReceiveRequest(const std::string &sAddress, const int64_t nId, const std::string &sRequest); + bool hdEnabled() const; + private: CWallet *wallet; bool fHaveWatchOnly; diff --git a/src/qt/walletview.cpp b/src/qt/walletview.cpp index f03fceab1ac7..e36faf9e8c02 100644 --- a/src/qt/walletview.cpp +++ b/src/qt/walletview.cpp @@ -125,6 +125,9 @@ void WalletView::setBitcoinGUI(BitcoinGUI *gui) // Pass through transaction notifications connect(this, SIGNAL(incomingTransaction(QString,int,CAmount,QString,QString,QString)), gui, SLOT(incomingTransaction(QString,int,CAmount,QString,QString,QString))); + + // Connect HD enabled state signal + connect(this, SIGNAL(hdEnabledStatusChanged(int)), gui, SLOT(setHDStatus(int))); } } @@ -165,6 +168,9 @@ void WalletView::setWalletModel(WalletModel *walletModel) connect(walletModel, SIGNAL(encryptionStatusChanged(int)), this, SIGNAL(encryptionStatusChanged(int))); updateEncryptionStatus(); + // update HD status + Q_EMIT hdEnabledStatusChanged(walletModel->hdEnabled()); + // Balloon pop-up for new transaction connect(walletModel->getTransactionTableModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(processNewTransaction(QModelIndex,int,int))); diff --git a/src/qt/walletview.h b/src/qt/walletview.h index 6e6be2ab3cbe..b28b30502974 100644 --- a/src/qt/walletview.h +++ b/src/qt/walletview.h @@ -128,6 +128,8 @@ public Q_SLOTS: void message(const QString &title, const QString &message, unsigned int style); /** Encryption status of wallet changed */ void encryptionStatusChanged(int status); + /** HD-Enabled status of wallet changed (only possible during startup) */ + void hdEnabledStatusChanged(int hdEnabled); /** Notify that a new transaction appeared */ void incomingTransaction(const QString& date, int unit, const CAmount& amount, const QString& type, const QString& address, const QString& label); }; diff --git a/src/rpcmisc.cpp b/src/rpcmisc.cpp index 5f4ce18ff5fb..bcf522b9ae6e 100644 --- a/src/rpcmisc.cpp +++ b/src/rpcmisc.cpp @@ -280,6 +280,8 @@ UniValue validateaddress(const UniValue& params, bool fHelp) " \"pubkey\" : \"publickeyhex\", (string) The hex value of the raw public key\n" " \"iscompressed\" : true|false, (boolean) If the address is compressed\n" " \"account\" : \"account\" (string) DEPRECATED. The account associated with the address, \"\" is the default account\n" + " \"hdkeypath\" : \"keypath\" (string, optional) The HD keypath if the key is HD and available\n" + " \"hdchainid\" : \"\" (string, optional) The ID of the HD chain\n" "}\n" "\nExamples:\n" + HelpExampleCli("validateaddress", "\"XwnLY9Tf7Zsef8gMGL2fhWA9ZmMjt4KPwg\"") @@ -314,6 +316,13 @@ UniValue validateaddress(const UniValue& params, bool fHelp) ret.pushKVs(detail); if (pwalletMain && pwalletMain->mapAddressBook.count(dest)) ret.push_back(Pair("account", pwalletMain->mapAddressBook[dest].name)); + CKeyID keyID; + CHDChain hdChainCurrent; + if (pwalletMain && address.GetKeyID(keyID) && pwalletMain->mapHdPubKeys.count(keyID) && pwalletMain->GetHDChain(hdChainCurrent)) + { + ret.push_back(Pair("hdkeypath", pwalletMain->mapHdPubKeys[keyID].GetKeyPath())); + ret.push_back(Pair("hdchainid", hdChainCurrent.GetID().GetHex())); + } #endif } return ret; diff --git a/src/rpcserver.cpp b/src/rpcserver.cpp index 984128ebd6a8..f687b9a5ae5d 100644 --- a/src/rpcserver.cpp +++ b/src/rpcserver.cpp @@ -362,6 +362,7 @@ static const CRPCCommand vRPCCommands[] = { "wallet", "addmultisigaddress", &addmultisigaddress, true }, { "wallet", "backupwallet", &backupwallet, true }, { "wallet", "dumpprivkey", &dumpprivkey, true }, + { "wallet", "dumphdinfo", &dumphdinfo, true }, { "wallet", "dumpwallet", &dumpwallet, true }, { "wallet", "encryptwallet", &encryptwallet, true }, { "wallet", "getaccountaddress", &getaccountaddress, true }, diff --git a/src/rpcserver.h b/src/rpcserver.h index dd852803f8be..8f81ba1ed2ac 100644 --- a/src/rpcserver.h +++ b/src/rpcserver.h @@ -192,6 +192,7 @@ extern UniValue dumpprivkey(const UniValue& params, bool fHelp); // in rpcdump.c extern UniValue importprivkey(const UniValue& params, bool fHelp); extern UniValue importaddress(const UniValue& params, bool fHelp); extern UniValue importpubkey(const UniValue& params, bool fHelp); +extern UniValue dumphdinfo(const UniValue& params, bool fHelp); extern UniValue dumpwallet(const UniValue& params, bool fHelp); extern UniValue importwallet(const UniValue& params, bool fHelp); extern UniValue importelectrumwallet(const UniValue& params, bool fHelp); diff --git a/src/support/allocators/secure.h b/src/support/allocators/secure.h index 1ec40fe83003..c57000e44d9c 100644 --- a/src/support/allocators/secure.h +++ b/src/support/allocators/secure.h @@ -9,6 +9,7 @@ #include "support/pagelocker.h" #include +#include // // Allocator that locks its contents from being paged @@ -59,4 +60,6 @@ struct secure_allocator : public std::allocator { // This is exactly like std::string, but with a custom allocator. typedef std::basic_string, secure_allocator > SecureString; +typedef std::vector > SecureVector; + #endif // BITCOIN_SUPPORT_ALLOCATORS_SECURE_H diff --git a/src/test/bip32_tests.cpp b/src/test/bip32_tests.cpp index 1d4cd7af2d9e..1dfbcdfd56db 100644 --- a/src/test/bip32_tests.cpp +++ b/src/test/bip32_tests.cpp @@ -117,6 +117,22 @@ void RunTest(const TestVector &test) { } key = keyNew; pubkey = pubkeyNew; + + CDataStream ssPub(SER_DISK, CLIENT_VERSION); + ssPub << pubkeyNew; + BOOST_CHECK(ssPub.size() == 74+1); + + CDataStream ssPriv(SER_DISK, CLIENT_VERSION); + ssPriv << keyNew; + BOOST_CHECK(ssPriv.size() == 74+1); + + CExtPubKey pubCheck; + CExtKey privCheck; + ssPub >> pubCheck; + ssPriv >> privCheck; + + BOOST_CHECK(pubCheck == pubkeyNew); + BOOST_CHECK(privCheck == keyNew); } } diff --git a/src/test/bip39_tests.cpp b/src/test/bip39_tests.cpp new file mode 100644 index 000000000000..79c9ba464839 --- /dev/null +++ b/src/test/bip39_tests.cpp @@ -0,0 +1,67 @@ +// Copyright (c) 2014-2017 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "base58.h" +#include "data/bip39_vectors.json.h" +#include "key.h" +#include "util.h" +#include "utilstrencodings.h" +#include "test/test_dash.h" +#include "bip39.h" + +#include + +#include + +// In script_tests.cpp +extern UniValue read_json(const std::string& jsondata); + +BOOST_FIXTURE_TEST_SUITE(bip39_tests, BasicTestingSetup) + +// https://github.com/trezor/python-mnemonic/blob/b502451a33a440783926e04428115e0bed87d01f/vectors.json +BOOST_AUTO_TEST_CASE(bip39_vectors) +{ + UniValue tests = read_json(std::string(json_tests::bip39_vectors, json_tests::bip39_vectors + sizeof(json_tests::bip39_vectors))); + + for (unsigned int i = 0; i < tests.size(); i++) { + // printf("%d\n", i); + UniValue test = tests[i]; + std::string strTest = test.write(); + if (test.size() < 4) // Allow for extra stuff (useful for comments) + { + BOOST_ERROR("Bad test: " << strTest); + continue; + } + + std::vector vData = ParseHex(test[0].get_str()); + SecureVector data(vData.begin(), vData.end()); + + SecureString m = CMnemonic::FromData(data, data.size()); + std::string strMnemonic = test[1].get_str(); + SecureString mnemonic(strMnemonic.begin(), strMnemonic.end()); + + // printf("%s\n%s\n", m.c_str(), mnemonic.c_str()); + BOOST_CHECK(m == mnemonic); + BOOST_CHECK(CMnemonic::Check(mnemonic)); + + SecureVector seed; + SecureString passphrase("TREZOR"); + CMnemonic::ToSeed(mnemonic, passphrase, seed); + // printf("seed: %s\n", HexStr(std::string(seed.begin(), seed.end())).c_str()); + BOOST_CHECK(HexStr(std::string(seed.begin(), seed.end())) == test[2].get_str()); + + CExtKey key; + CExtPubKey pubkey; + + key.SetMaster(&seed[0], 64); + pubkey = key.Neuter(); + + CBitcoinExtKey b58key; + b58key.SetKey(key); + // printf("CBitcoinExtKey: %s\n", b58key.ToString().c_str()); + BOOST_CHECK(b58key.ToString() == test[3].get_str()); + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/crypto_tests.cpp b/src/test/crypto_tests.cpp index 9e2467e60b33..4102c07f3ea4 100644 --- a/src/test/crypto_tests.cpp +++ b/src/test/crypto_tests.cpp @@ -17,6 +17,8 @@ #include #include +#include + BOOST_FIXTURE_TEST_SUITE(crypto_tests, BasicTestingSetup) template @@ -248,4 +250,27 @@ BOOST_AUTO_TEST_CASE(hmac_sha512_testvectors) { "b6022cac3c4982b10d5eeb55c3e4de15134676fb6de0446065c97440fa8c6a58"); } +BOOST_AUTO_TEST_CASE(pbkdf2_hmac_sha512_test) { + // test vectors from + // https://github.com/trezor/trezor-crypto/blob/87c920a7e747f7ed40b6ae841327868ab914435b/tests.c#L1936-L1957 + // https://stackoverflow.com/questions/15593184/pbkdf2-hmac-sha-512-test-vectors + uint8_t k[64], s[40]; + + strcpy((char *)s, "salt"); + PKCS5_PBKDF2_HMAC("password", 8, s, 4, 1, EVP_sha512(), 64, k); + BOOST_CHECK(HexStr(k, k + 64) == "867f70cf1ade02cff3752599a3a53dc4af34c7a669815ae5d513554e1c8cf252c02d470a285a0501bad999bfe943c08f050235d7d68b1da55e63f73b60a57fce"); + + strcpy((char *)s, "salt"); + PKCS5_PBKDF2_HMAC("password", 8, s, 4, 2, EVP_sha512(), 64, k); + BOOST_CHECK(HexStr(k, k + 64) == "e1d9c16aa681708a45f5c7c4e215ceb66e011a2e9f0040713f18aefdb866d53cf76cab2868a39b9f7840edce4fef5a82be67335c77a6068e04112754f27ccf4e"); + + strcpy((char *)s, "salt"); + PKCS5_PBKDF2_HMAC("password", 8, s, 4, 4096, EVP_sha512(), 64, k); + BOOST_CHECK(HexStr(k, k + 64) == "d197b1b33db0143e018b12f3d1d1479e6cdebdcc97c5c0f87f6902e072f457b5143f30602641b3d55cd335988cb36b84376060ecd532e039b742a239434af2d5"); + + strcpy((char *)s, "saltSALTsaltSALTsaltSALTsaltSALTsalt"); + PKCS5_PBKDF2_HMAC("passwordPASSWORDpassword", 3*8, s, 9*4, 4096, EVP_sha512(), 64, k); + BOOST_CHECK(HexStr(k, k + 64) == "8c0511f4c6e597c6ac6315d8f0362e225f3c501495ba23b868c005174dc4ee71115b59f9e60cd9532fa33e0f75aefe30225c583a186cd82bd4daea9724a3d3b8"); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/data/bip39_vectors.json b/src/test/data/bip39_vectors.json new file mode 100644 index 000000000000..86b38162815e --- /dev/null +++ b/src/test/data/bip39_vectors.json @@ -0,0 +1,146 @@ +[ + [ + "00000000000000000000000000000000", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04", + "xprv9s21ZrQH143K3h3fDYiay8mocZ3afhfULfb5GX8kCBdno77K4HiA15Tg23wpbeF1pLfs1c5SPmYHrEpTuuRhxMwvKDwqdKiGJS9XFKzUsAF" + ], + [ + "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", + "legal winner thank year wave sausage worth useful legal winner thank yellow", + "2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607", + "xprv9s21ZrQH143K2gA81bYFHqU68xz1cX2APaSq5tt6MFSLeXnCKV1RVUJt9FWNTbrrryem4ZckN8k4Ls1H6nwdvDTvnV7zEXs2HgPezuVccsq" + ], + [ + "80808080808080808080808080808080", + "letter advice cage absurd amount doctor acoustic avoid letter advice cage above", + "d71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8", + "xprv9s21ZrQH143K2shfP28KM3nr5Ap1SXjz8gc2rAqqMEynmjt6o1qboCDpxckqXavCwdnYds6yBHZGKHv7ef2eTXy461PXUjBFQg6PrwY4Gzq" + ], + [ + "ffffffffffffffffffffffffffffffff", + "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong", + "ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069", + "xprv9s21ZrQH143K2V4oox4M8Zmhi2Fjx5XK4Lf7GKRvPSgydU3mjZuKGCTg7UPiBUD7ydVPvSLtg9hjp7MQTYsW67rZHAXeccqYqrsx8LcXnyd" + ], + [ + "000000000000000000000000000000000000000000000000", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent", + "035895f2f481b1b0f01fcf8c289c794660b289981a78f8106447707fdd9666ca06da5a9a565181599b79f53b844d8a71dd9f439c52a3d7b3e8a79c906ac845fa", + "xprv9s21ZrQH143K3mEDrypcZ2usWqFgzKB6jBBx9B6GfC7fu26X6hPRzVjzkqkPvDqp6g5eypdk6cyhGnBngbjeHTe4LsuLG1cCmKJka5SMkmU" + ], + [ + "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", + "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will", + "f2b94508732bcbacbcc020faefecfc89feafa6649a5491b8c952cede496c214a0c7b3c392d168748f2d4a612bada0753b52a1c7ac53c1e93abd5c6320b9e95dd", + "xprv9s21ZrQH143K3Lv9MZLj16np5GzLe7tDKQfVusBni7toqJGcnKRtHSxUwbKUyUWiwpK55g1DUSsw76TF1T93VT4gz4wt5RM23pkaQLnvBh7" + ], + [ + "808080808080808080808080808080808080808080808080", + "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always", + "107d7c02a5aa6f38c58083ff74f04c607c2d2c0ecc55501dadd72d025b751bc27fe913ffb796f841c49b1d33b610cf0e91d3aa239027f5e99fe4ce9e5088cd65", + "xprv9s21ZrQH143K3VPCbxbUtpkh9pRG371UCLDz3BjceqP1jz7XZsQ5EnNkYAEkfeZp62cDNj13ZTEVG1TEro9sZ9grfRmcYWLBhCocViKEJae" + ], + [ + "ffffffffffffffffffffffffffffffffffffffffffffffff", + "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when", + "0cd6e5d827bb62eb8fc1e262254223817fd068a74b5b449cc2f667c3f1f985a76379b43348d952e2265b4cd129090758b3e3c2c49103b5051aac2eaeb890a528", + "xprv9s21ZrQH143K36Ao5jHRVhFGDbLP6FCx8BEEmpru77ef3bmA928BxsqvVM27WnvvyfWywiFN8K6yToqMaGYfzS6Db1EHAXT5TuyCLBXUfdm" + ], + [ + "0000000000000000000000000000000000000000000000000000000000000000", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art", + "bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8", + "xprv9s21ZrQH143K32qBagUJAMU2LsHg3ka7jqMcV98Y7gVeVyNStwYS3U7yVVoDZ4btbRNf4h6ibWpY22iRmXq35qgLs79f312g2kj5539ebPM" + ], + [ + "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", + "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title", + "bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87", + "xprv9s21ZrQH143K3Y1sd2XVu9wtqxJRvybCfAetjUrMMco6r3v9qZTBeXiBZkS8JxWbcGJZyio8TrZtm6pkbzG8SYt1sxwNLh3Wx7to5pgiVFU" + ], + [ + "8080808080808080808080808080808080808080808080808080808080808080", + "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless", + "c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f", + "xprv9s21ZrQH143K3CSnQNYC3MqAAqHwxeTLhDbhF43A4ss4ciWNmCY9zQGvAKUSqVUf2vPHBTSE1rB2pg4avopqSiLVzXEU8KziNnVPauTqLRo" + ], + [ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote", + "dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad", + "xprv9s21ZrQH143K2WFF16X85T2QCpndrGwx6GueB72Zf3AHwHJaknRXNF37ZmDrtHrrLSHvbuRejXcnYxoZKvRquTPyp2JiNG3XcjQyzSEgqCB" + ], + [ + "9e885d952ad362caeb4efe34a8e91bd2", + "ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic", + "274ddc525802f7c828d8ef7ddbcdc5304e87ac3535913611fbbfa986d0c9e5476c91689f9c8a54fd55bd38606aa6a8595ad213d4c9c9f9aca3fb217069a41028", + "xprv9s21ZrQH143K2oZ9stBYpoaZ2ktHj7jLz7iMqpgg1En8kKFTXJHsjxry1JbKH19YrDTicVwKPehFKTbmaxgVEc5TpHdS1aYhB2s9aFJBeJH" + ], + [ + "6610b25967cdcca9d59875f5cb50b0ea75433311869e930b", + "gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog", + "628c3827a8823298ee685db84f55caa34b5cc195a778e52d45f59bcf75aba68e4d7590e101dc414bc1bbd5737666fbbef35d1f1903953b66624f910feef245ac", + "xprv9s21ZrQH143K3uT8eQowUjsxrmsA9YUuQQK1RLqFufzybxD6DH6gPY7NjJ5G3EPHjsWDrs9iivSbmvjc9DQJbJGatfa9pv4MZ3wjr8qWPAK" + ], + [ + "68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c", + "hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length", + "64c87cde7e12ecf6704ab95bb1408bef047c22db4cc7491c4271d170a1b213d20b385bc1588d9c7b38f1b39d415665b8a9030c9ec653d75e65f847d8fc1fc440", + "xprv9s21ZrQH143K2XTAhys3pMNcGn261Fi5Ta2Pw8PwaVPhg3D8DWkzWQwjTJfskj8ofb81i9NP2cUNKxwjueJHHMQAnxtivTA75uUFqPFeWzk" + ], + [ + "c0ba5a8e914111210f2bd131f3d5e08d", + "scheme spot photo card baby mountain device kick cradle pact join borrow", + "ea725895aaae8d4c1cf682c1bfd2d358d52ed9f0f0591131b559e2724bb234fca05aa9c02c57407e04ee9dc3b454aa63fbff483a8b11de949624b9f1831a9612", + "xprv9s21ZrQH143K3FperxDp8vFsFycKCRcJGAFmcV7umQmcnMZaLtZRt13QJDsoS5F6oYT6BB4sS6zmTmyQAEkJKxJ7yByDNtRe5asP2jFGhT6" + ], + [ + "6d9be1ee6ebd27a258115aad99b7317b9c8d28b6d76431c3", + "horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave", + "fd579828af3da1d32544ce4db5c73d53fc8acc4ddb1e3b251a31179cdb71e853c56d2fcb11aed39898ce6c34b10b5382772db8796e52837b54468aeb312cfc3d", + "xprv9s21ZrQH143K3R1SfVZZLtVbXEB9ryVxmVtVMsMwmEyEvgXN6Q84LKkLRmf4ST6QrLeBm3jQsb9gx1uo23TS7vo3vAkZGZz71uuLCcywUkt" + ], + [ + "9f6a2878b2520799a44ef18bc7df394e7061a224d2c33cd015b157d746869863", + "panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside", + "72be8e052fc4919d2adf28d5306b5474b0069df35b02303de8c1729c9538dbb6fc2d731d5f832193cd9fb6aeecbc469594a70e3dd50811b5067f3b88b28c3e8d", + "xprv9s21ZrQH143K2WNnKmssvZYM96VAr47iHUQUTUyUXH3sAGNjhJANddnhw3i3y3pBbRAVk5M5qUGFr4rHbEWwXgX4qrvrceifCYQJbbFDems" + ], + [ + "23db8160a31d3e0dca3688ed941adbf3", + "cat swing flag economy stadium alone churn speed unique patch report train", + "deb5f45449e615feff5640f2e49f933ff51895de3b4381832b3139941c57b59205a42480c52175b6efcffaa58a2503887c1e8b363a707256bdd2b587b46541f5", + "xprv9s21ZrQH143K4G28omGMogEoYgDQuigBo8AFHAGDaJdqQ99QKMQ5J6fYTMfANTJy6xBmhvsNZ1CJzRZ64PWbnTFUn6CDV2FxoMDLXdk95DQ" + ], + [ + "8197a4a47f0425faeaa69deebc05ca29c0a5b5cc76ceacc0", + "light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access", + "4cbdff1ca2db800fd61cae72a57475fdc6bab03e441fd63f96dabd1f183ef5b782925f00105f318309a7e9c3ea6967c7801e46c8a58082674c860a37b93eda02", + "xprv9s21ZrQH143K3wtsvY8L2aZyxkiWULZH4vyQE5XkHTXkmx8gHo6RUEfH3Jyr6NwkJhvano7Xb2o6UqFKWHVo5scE31SGDCAUsgVhiUuUDyh" + ], + [ + "066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad", + "all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform", + "26e975ec644423f4a4c4f4215ef09b4bd7ef924e85d1d17c4cf3f136c2863cf6df0a475045652c57eb5fb41513ca2a2d67722b77e954b4b3fc11f7590449191d", + "xprv9s21ZrQH143K3rEfqSM4QZRVmiMuSWY9wugscmaCjYja3SbUD3KPEB1a7QXJoajyR2T1SiXU7rFVRXMV9XdYVSZe7JoUXdP4SRHTxsT1nzm" + ], + [ + "f30f8c1da665478f49b001d94c5fc452", + "vessel ladder alter error federal sibling chat ability sun glass valve picture", + "2aaa9242daafcee6aa9d7269f17d4efe271e1b9a529178d7dc139cd18747090bf9d60295d0ce74309a78852a9caadf0af48aae1c6253839624076224374bc63f", + "xprv9s21ZrQH143K2QWV9Wn8Vvs6jbqfF1YbTCdURQW9dLFKDovpKaKrqS3SEWsXCu6ZNky9PSAENg6c9AQYHcg4PjopRGGKmdD313ZHszymnps" + ], + [ + "c10ec20dc3cd9f652c7fac2f1230f7a3c828389a14392f05", + "scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump", + "7b4a10be9d98e6cba265566db7f136718e1398c71cb581e1b2f464cac1ceedf4f3e274dc270003c670ad8d02c4558b2f8e39edea2775c9e232c7cb798b069e88", + "xprv9s21ZrQH143K4aERa2bq7559eMCCEs2QmmqVjUuzfy5eAeDX4mqZffkYwpzGQRE2YEEeLVRoH4CSHxianrFaVnMN2RYaPUZJhJx8S5j6puX" + ], + [ + "f585c11aec520db57dd353c69554b21a89b20fb0650966fa0a9d6f74fd989d8f", + "void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold", + "01f5bced59dec48e362f2c45b5de68b9fd6c92c6634f44d6d40aab69056506f0e35524a518034ddc1192e1dacd32c1ed3eaa3c3b131c88ed8e7e54c49a5d0998", + "xprv9s21ZrQH143K39rnQJknpH1WEPFJrzmAqqasiDcVrNuk926oizzJDDQkdiTvNPr2FYDYzWgiMiC63YmfPAa2oPyNB23r2g7d1yiK6WpqaQS" + ] +] diff --git a/src/test/rpc_wallet_tests.cpp b/src/test/rpc_wallet_tests.cpp index f75037ec27f5..9997b02ccc0b 100644 --- a/src/test/rpc_wallet_tests.cpp +++ b/src/test/rpc_wallet_tests.cpp @@ -74,7 +74,7 @@ BOOST_AUTO_TEST_CASE(rpc_wallet) { LOCK(pwalletMain->cs_wallet); - demoPubkey = pwalletMain->GenerateNewKey(); + demoPubkey = pwalletMain->GenerateNewKey(0, false); demoAddress = CBitcoinAddress(CTxDestination(demoPubkey.GetID())); string strPurpose = "receive"; BOOST_CHECK_NO_THROW({ /*Initialize Wallet with an account */ @@ -85,7 +85,7 @@ BOOST_AUTO_TEST_CASE(rpc_wallet) walletdb.WriteAccount(strAccount, account); }); - CPubKey setaccountDemoPubkey = pwalletMain->GenerateNewKey(); + CPubKey setaccountDemoPubkey = pwalletMain->GenerateNewKey(0, false); setaccountDemoAddress = CBitcoinAddress(CTxDestination(setaccountDemoPubkey.GetID())); } /********************************* diff --git a/src/wallet/crypter.cpp b/src/wallet/crypter.cpp index c6562518726b..d3d42c20284a 100644 --- a/src/wallet/crypter.cpp +++ b/src/wallet/crypter.cpp @@ -274,9 +274,24 @@ bool CCryptoKeyStore::Unlock(const CKeyingMaterial& vMasterKeyIn, bool fForMixin LogPrintf("The wallet is probably corrupted: Some keys decrypt but not all.\n"); assert(false); } - if (keyFail || !keyPass) + if (keyFail || (!keyPass && cryptedHDChain.IsNull())) return false; + vMasterKey = vMasterKeyIn; + + if(!cryptedHDChain.IsNull()) { + bool chainPass = false; + // try to decrypt seed and make sure it matches + CHDChain hdChainTmp; + if (DecryptHDChain(hdChainTmp)) { + // make sure seed matches this chain + chainPass = cryptedHDChain.GetID() == hdChainTmp.GetSeedHash(); + } + if (!chainPass) { + vMasterKey.clear(); + return false; + } + } fDecryptionThoroughlyChecked = true; } fOnlyMixingAllowed = fForMixingOnly; @@ -378,3 +393,142 @@ bool CCryptoKeyStore::EncryptKeys(CKeyingMaterial& vMasterKeyIn) } return true; } + +bool CCryptoKeyStore::EncryptHDChain(const CKeyingMaterial& vMasterKeyIn) +{ + // should call EncryptKeys first + if (!IsCrypted()) + return false; + + if (!cryptedHDChain.IsNull()) + return true; + + if (cryptedHDChain.IsCrypted()) + return true; + + // make sure seed matches this chain + if (hdChain.GetID() != hdChain.GetSeedHash()) + return false; + + std::vector vchCryptedSeed; + if (!EncryptSecret(vMasterKeyIn, hdChain.GetSeed(), hdChain.GetID(), vchCryptedSeed)) + return false; + + hdChain.Debug(__func__); + cryptedHDChain = hdChain; + cryptedHDChain.SetCrypted(true); + + SecureVector vchSecureCryptedSeed(vchCryptedSeed.begin(), vchCryptedSeed.end()); + if (!cryptedHDChain.SetSeed(vchSecureCryptedSeed, false)) + return false; + + SecureVector vchMnemonic; + SecureVector vchMnemonicPassphrase; + + // it's ok to have no mnemonic if wallet was initialized via hdseed + if (hdChain.GetMnemonic(vchMnemonic, vchMnemonicPassphrase)) { + std::vector vchCryptedMnemonic; + std::vector vchCryptedMnemonicPassphrase; + + if (!vchMnemonic.empty() && !EncryptSecret(vMasterKeyIn, vchMnemonic, hdChain.GetID(), vchCryptedMnemonic)) + return false; + if (!vchMnemonicPassphrase.empty() && !EncryptSecret(vMasterKeyIn, vchMnemonicPassphrase, hdChain.GetID(), vchCryptedMnemonicPassphrase)) + return false; + + SecureVector vchSecureCryptedMnemonic(vchCryptedMnemonic.begin(), vchCryptedMnemonic.end()); + SecureVector vchSecureCryptedMnemonicPassphrase(vchCryptedMnemonicPassphrase.begin(), vchCryptedMnemonicPassphrase.end()); + if (!cryptedHDChain.SetMnemonic(vchSecureCryptedMnemonic, vchSecureCryptedMnemonicPassphrase, false)) + return false; + } + + if (!hdChain.SetNull()) + return false; + + return true; +} + +bool CCryptoKeyStore::DecryptHDChain(CHDChain& hdChainRet) const +{ + if (!IsCrypted()) + return true; + + if (cryptedHDChain.IsNull()) + return false; + + if (!cryptedHDChain.IsCrypted()) + return false; + + SecureVector vchSecureSeed; + SecureVector vchSecureCryptedSeed = cryptedHDChain.GetSeed(); + std::vector vchCryptedSeed(vchSecureCryptedSeed.begin(), vchSecureCryptedSeed.end()); + if (!DecryptSecret(vMasterKey, vchCryptedSeed, cryptedHDChain.GetID(), vchSecureSeed)) + return false; + + hdChainRet = cryptedHDChain; + if (!hdChainRet.SetSeed(vchSecureSeed, false)) + return false; + + // hash of decrypted seed must match chain id + if (hdChainRet.GetSeedHash() != cryptedHDChain.GetID()) + return false; + + SecureVector vchSecureCryptedMnemonic; + SecureVector vchSecureCryptedMnemonicPassphrase; + + // it's ok to have no mnemonic if wallet was initialized via hdseed + if (cryptedHDChain.GetMnemonic(vchSecureCryptedMnemonic, vchSecureCryptedMnemonicPassphrase)) { + SecureVector vchSecureMnemonic; + SecureVector vchSecureMnemonicPassphrase; + + std::vector vchCryptedMnemonic(vchSecureCryptedMnemonic.begin(), vchSecureCryptedMnemonic.end()); + std::vector vchCryptedMnemonicPassphrase(vchSecureCryptedMnemonicPassphrase.begin(), vchSecureCryptedMnemonicPassphrase.end()); + + if (!vchCryptedMnemonic.empty() && !DecryptSecret(vMasterKey, vchCryptedMnemonic, cryptedHDChain.GetID(), vchSecureMnemonic)) + return false; + if (!vchCryptedMnemonicPassphrase.empty() && !DecryptSecret(vMasterKey, vchCryptedMnemonicPassphrase, cryptedHDChain.GetID(), vchSecureMnemonicPassphrase)) + return false; + + if (!hdChainRet.SetMnemonic(vchSecureMnemonic, vchSecureMnemonicPassphrase, false)) + return false; + } + + hdChainRet.SetCrypted(false); + hdChainRet.Debug(__func__); + + return true; +} + +bool CCryptoKeyStore::SetHDChain(const CHDChain& chain) +{ + if (IsCrypted()) + return false; + + if (chain.IsCrypted()) + return false; + + hdChain = chain; + return true; +} + +bool CCryptoKeyStore::SetCryptedHDChain(const CHDChain& chain) +{ + if (!SetCrypted()) + return false; + + if (!chain.IsCrypted()) + return false; + + cryptedHDChain = chain; + return true; +} + +bool CCryptoKeyStore::GetHDChain(CHDChain& hdChainRet) const +{ + if(IsCrypted()) { + hdChainRet = cryptedHDChain; + return !cryptedHDChain.IsNull(); + } + + hdChainRet = hdChain; + return !hdChain.IsNull(); +} diff --git a/src/wallet/crypter.h b/src/wallet/crypter.h index e30341213836..2c0a4c8a21ce 100644 --- a/src/wallet/crypter.h +++ b/src/wallet/crypter.h @@ -118,6 +118,7 @@ class CCryptoKeyStore : public CBasicKeyStore { private: CryptedKeyMap mapCryptedKeys; + CHDChain cryptedHDChain; CKeyingMaterial vMasterKey; @@ -137,6 +138,11 @@ class CCryptoKeyStore : public CBasicKeyStore //! will encrypt previously unencrypted keys bool EncryptKeys(CKeyingMaterial& vMasterKeyIn); + bool EncryptHDChain(const CKeyingMaterial& vMasterKeyIn); + bool DecryptHDChain(CHDChain& hdChainRet) const; + bool SetHDChain(const CHDChain& chain); + bool SetCryptedHDChain(const CHDChain& chain); + bool Unlock(const CKeyingMaterial& vMasterKeyIn, bool fForMixingOnly = false); public: @@ -210,6 +216,8 @@ class CCryptoKeyStore : public CBasicKeyStore } } + bool GetHDChain(CHDChain& hdChainRet) const; + /** * Wallet status (encrypted, locked) changed. * Note: Called without locks held. diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index 8c7f8c802b68..e2685c751d56 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -580,6 +580,51 @@ UniValue dumpprivkey(const UniValue& params, bool fHelp) return CBitcoinSecret(vchSecret).ToString(); } +UniValue dumphdinfo(const UniValue& params, bool fHelp) +{ + if (!EnsureWalletIsAvailable(fHelp)) + return NullUniValue; + + if (fHelp || params.size() != 0) + throw runtime_error( + "dumphdinfo\n" + "Returns an object containing sensitive private info about this HD wallet.\n" + "\nResult:\n" + "{\n" + " \"hdseed\": \"seed\", (string) The HD seed (bip32, in hex)\n" + " \"mnemonic\": \"words\", (string) The mnemonic for this HD wallet (bip39, english words) \n" + " \"mnemonicpassphrase\": \"passphrase\", (string) The mnemonic passphrase for this HD wallet (bip39)\n" + "}\n" + "\nExamples:\n" + + HelpExampleCli("dumphdinfo", "") + + HelpExampleRpc("dumphdinfo", "") + ); + + LOCK(pwalletMain->cs_wallet); + + EnsureWalletIsUnlocked(); + + // add the base58check encoded extended master if the wallet uses HD + CHDChain hdChainCurrent; + if (pwalletMain->GetHDChain(hdChainCurrent)) + { + if (!pwalletMain->GetDecryptedHDChain(hdChainCurrent)) + throw JSONRPCError(RPC_INTERNAL_ERROR, "Cannot decrypt HD seed"); + + SecureString ssMnemonic; + SecureString ssMnemonicPassphrase; + hdChainCurrent.GetMnemonic(ssMnemonic, ssMnemonicPassphrase); + + UniValue obj(UniValue::VOBJ); + obj.push_back(Pair("hdseed", HexStr(hdChainCurrent.GetSeed()))); + obj.push_back(Pair("mnemonic", ssMnemonic.c_str())); + obj.push_back(Pair("mnemonicpassphrase", ssMnemonicPassphrase.c_str())); + + return obj; + } + + return NullUniValue; +} UniValue dumpwallet(const UniValue& params, bool fHelp) { @@ -625,19 +670,66 @@ UniValue dumpwallet(const UniValue& params, bool fHelp) file << strprintf("# * Best block at time of backup was %i (%s),\n", chainActive.Height(), chainActive.Tip()->GetBlockHash().ToString()); file << strprintf("# mined on %s\n", EncodeDumpTime(chainActive.Tip()->GetBlockTime())); file << "\n"; + + // add the base58check encoded extended master if the wallet uses HD + CHDChain hdChainCurrent; + if (pwalletMain->GetHDChain(hdChainCurrent)) + { + + if (!pwalletMain->GetDecryptedHDChain(hdChainCurrent)) + throw JSONRPCError(RPC_INTERNAL_ERROR, "Cannot decrypt HD chain"); + + SecureString ssMnemonic; + SecureString ssMnemonicPassphrase; + hdChainCurrent.GetMnemonic(ssMnemonic, ssMnemonicPassphrase); + file << "# mnemonic: " << ssMnemonic << "\n"; + file << "# mnemonic passphrase: " << ssMnemonicPassphrase << "\n\n"; + + SecureVector vchSeed = hdChainCurrent.GetSeed(); + file << "# HD seed: " << HexStr(vchSeed) << "\n\n"; + + CExtKey masterKey; + masterKey.SetMaster(&vchSeed[0], vchSeed.size()); + + CBitcoinExtKey b58extkey; + b58extkey.SetKey(masterKey); + + file << "# extended private masterkey: " << b58extkey.ToString() << "\n"; + + CExtPubKey masterPubkey; + masterPubkey = masterKey.Neuter(); + + CBitcoinExtPubKey b58extpubkey; + b58extpubkey.SetKey(masterPubkey); + file << "# extended public masterkey: " << b58extpubkey.ToString() << "\n\n"; + + for (size_t i = 0; i < hdChainCurrent.CountAccounts(); ++i) + { + CHDAccount acc; + if(hdChainCurrent.GetAccount(i, acc)) { + file << "# external chain counter: " << acc.nExternalChainCounter << "\n"; + file << "# internal chain counter: " << acc.nInternalChainCounter << "\n\n"; + } else { + file << "# WARNING: ACCOUNT " << i << " IS MISSING!" << "\n\n"; + } + } + } + for (std::vector >::const_iterator it = vKeyBirth.begin(); it != vKeyBirth.end(); it++) { const CKeyID &keyid = it->second; std::string strTime = EncodeDumpTime(it->first); std::string strAddr = CBitcoinAddress(keyid).ToString(); CKey key; if (pwalletMain->GetKey(keyid, key)) { + file << strprintf("%s %s ", CBitcoinSecret(key).ToString(), strTime); if (pwalletMain->mapAddressBook.count(keyid)) { - file << strprintf("%s %s label=%s # addr=%s\n", CBitcoinSecret(key).ToString(), strTime, EncodeDumpString(pwalletMain->mapAddressBook[keyid].name), strAddr); + file << strprintf("label=%s", EncodeDumpString(pwalletMain->mapAddressBook[keyid].name)); } else if (setKeyPool.count(keyid)) { - file << strprintf("%s %s reserve=1 # addr=%s\n", CBitcoinSecret(key).ToString(), strTime, strAddr); + file << "reserve=1"; } else { - file << strprintf("%s %s change=1 # addr=%s\n", CBitcoinSecret(key).ToString(), strTime, strAddr); + file << "change=1"; } + file << strprintf(" # addr=%s%s\n", strAddr, (pwalletMain->mapHdPubKeys.count(keyid) ? " hdkeypath="+pwalletMain->mapHdPubKeys[keyid].GetKeyPath() : "")); } } file << "\n"; diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 42e9362e08e2..582aeb0bc6da 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -142,7 +142,7 @@ UniValue getnewaddress(const UniValue& params, bool fHelp) // Generate a new key that is added to wallet CPubKey newKey; - if (!pwalletMain->GetKeyFromPool(newKey)) + if (!pwalletMain->GetKeyFromPool(newKey, false)) throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first"); CKeyID keyID = newKey.GetID(); @@ -179,7 +179,7 @@ CBitcoinAddress GetAccountAddress(string strAccount, bool bForceNew=false) // Generate a new key if (!account.vchPubKey.IsValid() || bForceNew || bKeyUsed) { - if (!pwalletMain->GetKeyFromPool(account.vchPubKey)) + if (!pwalletMain->GetKeyFromPool(account.vchPubKey, false)) throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first"); pwalletMain->SetAddressBook(account.vchPubKey.GetID(), strAccount, "receive"); @@ -245,7 +245,7 @@ UniValue getrawchangeaddress(const UniValue& params, bool fHelp) CReserveKey reservekey(pwalletMain); CPubKey vchPubKey; - if (!reservekey.GetReservedKey(vchPubKey)) + if (!reservekey.GetReservedKey(vchPubKey, true)) throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first"); reservekey.KeepKey(); @@ -1977,7 +1977,7 @@ UniValue keypoolrefill(const UniValue& params, bool fHelp) EnsureWalletIsUnlocked(); pwalletMain->TopUpKeyPool(kpSize); - if (pwalletMain->GetKeyPoolSize() < kpSize) + if (pwalletMain->GetKeyPoolSize() < (pwalletMain->IsHDEnabled() ? kpSize * 2 : kpSize)) throw JSONRPCError(RPC_WALLET_ERROR, "Error refreshing keypool."); return NullUniValue; @@ -2197,7 +2197,7 @@ UniValue encryptwallet(const UniValue& params, bool fHelp) // slack space in .dat files; that is bad if the old data is // unencrypted private keys. So: StartShutdown(); - return "Wallet encrypted; Dash Core server stopping, restart to run with encrypted wallet. The keypool has been flushed, you need to make a new backup."; + return "Wallet encrypted; Dash Core server stopping, restart to run with encrypted wallet. The keypool has been flushed and a new HD seed was generated (if you are using HD). You need to make a new backup."; } UniValue lockunspent(const UniValue& params, bool fHelp) @@ -2377,10 +2377,21 @@ UniValue getwalletinfo(const UniValue& params, bool fHelp) " \"immature_balance\": xxxxxx, (numeric) the total immature balance of the wallet in " + CURRENCY_UNIT + "\n" " \"txcount\": xxxxxxx, (numeric) the total number of transactions in the wallet\n" " \"keypoololdest\": xxxxxx, (numeric) the timestamp (seconds since GMT epoch) of the oldest pre-generated key in the key pool\n" - " \"keypoolsize\": xxxx, (numeric) how many new keys are pre-generated\n" + " \"keypoolsize\": xxxx, (numeric) how many new keys are pre-generated (only counts external keys)\n" + " \"keypoolsize_hd_internal\": xxxx, (numeric) how many new keys are pre-generated for internal use (used for change outputs, only appears if the wallet is using this feature, otherwise external keys are used)\n" " \"keys_left\": xxxx, (numeric) how many new keys are left since last automatic backup\n" " \"unlocked_until\": ttt, (numeric) the timestamp in seconds since epoch (midnight Jan 1 1970 GMT) that the wallet is unlocked for transfers, or 0 if the wallet is locked\n" " \"paytxfee\": x.xxxx, (numeric) the transaction fee configuration, set in " + CURRENCY_UNIT + "/kB\n" + " \"hdchainid\": \"\", (string) the ID of the HD chain\n" + " \"hdaccountcount\": xxx, (numeric) how many accounts of the HD chain are in this wallet\n" + " [\n" + " {\n" + " \"hdaccountindex\": xxx, (numeric) the index of the account\n" + " \"hdexternalkeyindex\": xxxx, (numeric) current external childkey index\n" + " \"hdinternalkeyindex\": xxxx, (numeric) current internal childkey index\n" + " }\n" + " ,...\n" + " ]\n" "}\n" "\nExamples:\n" + HelpExampleCli("getwalletinfo", "") @@ -2389,6 +2400,8 @@ UniValue getwalletinfo(const UniValue& params, bool fHelp) LOCK2(cs_main, pwalletMain->cs_wallet); + CHDChain hdChainCurrent; + bool fHDEnabled = pwalletMain->GetHDChain(hdChainCurrent); UniValue obj(UniValue::VOBJ); obj.push_back(Pair("walletversion", pwalletMain->GetVersion())); obj.push_back(Pair("balance", ValueFromAmount(pwalletMain->GetBalance()))); @@ -2396,11 +2409,33 @@ UniValue getwalletinfo(const UniValue& params, bool fHelp) obj.push_back(Pair("immature_balance", ValueFromAmount(pwalletMain->GetImmatureBalance()))); obj.push_back(Pair("txcount", (int)pwalletMain->mapWallet.size())); obj.push_back(Pair("keypoololdest", pwalletMain->GetOldestKeyPoolTime())); - obj.push_back(Pair("keypoolsize", (int)pwalletMain->GetKeyPoolSize())); + obj.push_back(Pair("keypoolsize", (int64_t)pwalletMain->KeypoolCountExternalKeys())); + if (fHDEnabled) { + obj.push_back(Pair("keypoolsize_hd_internal", (int64_t)(pwalletMain->KeypoolCountInternalKeys()))); + } obj.push_back(Pair("keys_left", pwalletMain->nKeysLeftSinceAutoBackup)); if (pwalletMain->IsCrypted()) obj.push_back(Pair("unlocked_until", nWalletUnlockTime)); obj.push_back(Pair("paytxfee", ValueFromAmount(payTxFee.GetFeePerK()))); + if (fHDEnabled) { + obj.push_back(Pair("hdchainid", hdChainCurrent.GetID().GetHex())); + obj.push_back(Pair("hdaccountcount", (int64_t)hdChainCurrent.CountAccounts())); + UniValue accounts(UniValue::VARR); + for (int i = 0; i < hdChainCurrent.CountAccounts(); ++i) + { + CHDAccount acc; + UniValue account(UniValue::VOBJ); + account.push_back(Pair("hdaccountindex", (int64_t)i)); + if(hdChainCurrent.GetAccount(i, acc)) { + account.push_back(Pair("hdexternalkeyindex", (int64_t)acc.nExternalChainCounter)); + account.push_back(Pair("hdinternalkeyindex", (int64_t)acc.nInternalChainCounter)); + } else { + account.push_back(Pair("error", strprintf("account %d is missing", i))); + } + accounts.push_back(account); + } + obj.push_back(Pair("hdaccounts", accounts)); + } return obj; } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index c91f7a167b25..92b6fa38bc90 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -100,32 +100,191 @@ const CWalletTx* CWallet::GetWalletTx(const uint256& hash) const return &(it->second); } -CPubKey CWallet::GenerateNewKey() +CPubKey CWallet::GenerateNewKey(uint32_t nAccountIndex, bool fInternal) { AssertLockHeld(cs_wallet); // mapKeyMetadata bool fCompressed = CanSupportFeature(FEATURE_COMPRPUBKEY); // default to compressed public keys if we want 0.6.0 wallets CKey secret; - secret.MakeNewKey(fCompressed); - - // Compressed public keys were introduced in version 0.6.0 - if (fCompressed) - SetMinVersion(FEATURE_COMPRPUBKEY); - - CPubKey pubkey = secret.GetPubKey(); - assert(secret.VerifyPubKey(pubkey)); // Create new metadata int64_t nCreationTime = GetTime(); - mapKeyMetadata[pubkey.GetID()] = CKeyMetadata(nCreationTime); - if (!nTimeFirstKey || nCreationTime < nTimeFirstKey) - nTimeFirstKey = nCreationTime; + CKeyMetadata metadata(nCreationTime); + + CPubKey pubkey; + // use HD key derivation if HD was enabled during wallet creation + if (IsHDEnabled()) { + DeriveNewChildKey(metadata, secret, nAccountIndex, fInternal); + pubkey = secret.GetPubKey(); + } else { + secret.MakeNewKey(fCompressed); + + // Compressed public keys were introduced in version 0.6.0 + if (fCompressed) + SetMinVersion(FEATURE_COMPRPUBKEY); + + pubkey = secret.GetPubKey(); + assert(secret.VerifyPubKey(pubkey)); - if (!AddKeyPubKey(secret, pubkey)) - throw std::runtime_error("CWallet::GenerateNewKey(): AddKey failed"); + // Create new metadata + mapKeyMetadata[pubkey.GetID()] = metadata; + if (!nTimeFirstKey || nCreationTime < nTimeFirstKey) + nTimeFirstKey = nCreationTime; + + if (!AddKeyPubKey(secret, pubkey)) + throw std::runtime_error(std::string(__func__) + ": AddKey failed"); + } return pubkey; } +void CWallet::DeriveNewChildKey(const CKeyMetadata& metadata, CKey& secretRet, uint32_t nAccountIndex, bool fInternal) +{ + CHDChain hdChainTmp; + if (!GetHDChain(hdChainTmp)) { + throw std::runtime_error(std::string(__func__) + ": GetHDChain failed"); + } + + if (!DecryptHDChain(hdChainTmp)) + throw std::runtime_error(std::string(__func__) + ": DecryptHDChainSeed failed"); + // make sure seed matches this chain + if (hdChainTmp.GetID() != hdChainTmp.GetSeedHash()) + throw std::runtime_error(std::string(__func__) + ": Wrong HD chain!"); + + CHDAccount acc; + if (!hdChainTmp.GetAccount(nAccountIndex, acc)) + throw std::runtime_error(std::string(__func__) + ": Wrong HD account!"); + + // derive child key at next index, skip keys already known to the wallet + CExtKey childKey; + uint32_t nChildIndex = fInternal ? acc.nInternalChainCounter : acc.nExternalChainCounter; + do { + hdChainTmp.DeriveChildExtKey(nAccountIndex, fInternal, nChildIndex, childKey); + // increment childkey index + nChildIndex++; + } while (HaveKey(childKey.key.GetPubKey().GetID())); + secretRet = childKey.key; + + CPubKey pubkey = secretRet.GetPubKey(); + assert(secretRet.VerifyPubKey(pubkey)); + + // store metadata + mapKeyMetadata[pubkey.GetID()] = metadata; + if (!nTimeFirstKey || metadata.nCreateTime < nTimeFirstKey) + nTimeFirstKey = metadata.nCreateTime; + + // update the chain model in the database + CHDChain hdChainCurrent; + GetHDChain(hdChainCurrent); + + if (fInternal) { + acc.nInternalChainCounter = nChildIndex; + } + else { + acc.nExternalChainCounter = nChildIndex; + } + + if (!hdChainCurrent.SetAccount(nAccountIndex, acc)) + throw std::runtime_error(std::string(__func__) + ": SetAccount failed"); + + if (IsCrypted()) { + if (!SetCryptedHDChain(hdChainCurrent, false)) + throw std::runtime_error(std::string(__func__) + ": SetCryptedHDChain failed"); + } + else { + if (!SetHDChain(hdChainCurrent, false)) + throw std::runtime_error(std::string(__func__) + ": SetHDChain failed"); + } + + if (!AddHDPubKey(childKey.Neuter(), fInternal)) + throw std::runtime_error(std::string(__func__) + ": AddHDPubKey failed"); +} + +bool CWallet::GetPubKey(const CKeyID &address, CPubKey& vchPubKeyOut) const +{ + LOCK(cs_wallet); + std::map::const_iterator mi = mapHdPubKeys.find(address); + if (mi != mapHdPubKeys.end()) + { + const CHDPubKey &hdPubKey = (*mi).second; + vchPubKeyOut = hdPubKey.extPubKey.pubkey; + return true; + } + else + return CCryptoKeyStore::GetPubKey(address, vchPubKeyOut); +} + +bool CWallet::GetKey(const CKeyID &address, CKey& keyOut) const +{ + LOCK(cs_wallet); + std::map::const_iterator mi = mapHdPubKeys.find(address); + if (mi != mapHdPubKeys.end()) + { + // if the key has been found in mapHdPubKeys, derive it on the fly + const CHDPubKey &hdPubKey = (*mi).second; + CHDChain hdChainCurrent; + if (!GetHDChain(hdChainCurrent)) + throw std::runtime_error(std::string(__func__) + ": GetHDChain failed"); + if (!DecryptHDChain(hdChainCurrent)) + throw std::runtime_error(std::string(__func__) + ": DecryptHDChainSeed failed"); + // make sure seed matches this chain + if (hdChainCurrent.GetID() != hdChainCurrent.GetSeedHash()) + throw std::runtime_error(std::string(__func__) + ": Wrong HD chain!"); + + CExtKey extkey; + hdChainCurrent.DeriveChildExtKey(hdPubKey.nAccountIndex, hdPubKey.nChangeIndex != 0, hdPubKey.extPubKey.nChild, extkey); + keyOut = extkey.key; + + return true; + } + else { + return CCryptoKeyStore::GetKey(address, keyOut); + } +} + +bool CWallet::HaveKey(const CKeyID &address) const +{ + LOCK(cs_wallet); + if (mapHdPubKeys.count(address) > 0) + return true; + return CCryptoKeyStore::HaveKey(address); +} + +bool CWallet::LoadHDPubKey(const CHDPubKey &hdPubKey) +{ + AssertLockHeld(cs_wallet); + + mapHdPubKeys[hdPubKey.extPubKey.pubkey.GetID()] = hdPubKey; + return true; +} + +bool CWallet::AddHDPubKey(const CExtPubKey &extPubKey, bool fInternal) +{ + AssertLockHeld(cs_wallet); + + CHDChain hdChainCurrent; + GetHDChain(hdChainCurrent); + + CHDPubKey hdPubKey; + hdPubKey.extPubKey = extPubKey; + hdPubKey.hdchainID = hdChainCurrent.GetID(); + hdPubKey.nChangeIndex = fInternal ? 1 : 0; + mapHdPubKeys[extPubKey.pubkey.GetID()] = hdPubKey; + + // check if we need to remove from watch-only + CScript script; + script = GetScriptForDestination(extPubKey.pubkey.GetID()); + if (HaveWatchOnly(script)) + RemoveWatchOnly(script); + script = GetScriptForRawPubKey(extPubKey.pubkey); + if (HaveWatchOnly(script)) + RemoveWatchOnly(script); + + if (!fFileBacked) + return true; + + return CWalletDB(strWalletFile).WriteHDPubKey(hdPubKey, mapKeyMetadata[extPubKey.pubkey.GetID()]); +} + bool CWallet::AddKeyPubKey(const CKey& secret, const CPubKey &pubkey) { AssertLockHeld(cs_wallet); // mapKeyMetadata @@ -611,6 +770,10 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase) pwalletdbEncryption->WriteMasterKey(nMasterKeyMaxID, kMasterKey); } + // must get current HD chain before EncryptKeys + CHDChain hdChainCurrent; + GetHDChain(hdChainCurrent); + if (!EncryptKeys(vMasterKey)) { if (fFileBacked) { @@ -622,6 +785,24 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase) assert(false); } + if (!hdChainCurrent.IsNull()) { + assert(EncryptHDChain(vMasterKey)); + + CHDChain hdChainCrypted; + assert(GetHDChain(hdChainCrypted)); + + DBG( + printf("EncryptWallet -- current seed: '%s'\n", HexStr(hdChainCurrent.GetSeed()).c_str()); + printf("EncryptWallet -- crypted seed: '%s'\n", HexStr(hdChainCrypted.GetSeed()).c_str()); + ); + + // ids should match, seed hashes should not + assert(hdChainCurrent.GetID() == hdChainCrypted.GetID()); + assert(hdChainCurrent.GetSeedHash() != hdChainCrypted.GetSeedHash()); + + assert(SetCryptedHDChain(hdChainCrypted, false)); + } + // Encryption was introduced in version 0.4.0 SetMinVersion(FEATURE_WALLETCRYPT, pwalletdbEncryption, true); @@ -640,7 +821,15 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase) Lock(); Unlock(strWalletPassphrase); - NewKeyPool(); + + // if we are not using HD, generate new keypool + if(IsHDEnabled()) { + TopUpKeyPool(); + } + else { + NewKeyPool(); + } + Lock(); // Need to completely rewrite the wallet file; if we don't, bdb might keep @@ -1194,6 +1383,105 @@ CAmount CWallet::GetChange(const CTxOut& txout) const return (IsChange(txout) ? txout.nValue : 0); } +void CWallet::GenerateNewHDChain() +{ + CHDChain newHdChain; + + std::string strSeed = GetArg("-hdseed", "not hex"); + + if(mapArgs.count("-hdseed") && IsHex(strSeed)) { + std::vector vchSeed = ParseHex(strSeed); + if (!newHdChain.SetSeed(SecureVector(vchSeed.begin(), vchSeed.end()), true)) + throw std::runtime_error(std::string(__func__) + ": SetSeed failed"); + } + else { + if (mapArgs.count("-hdseed") && !IsHex(strSeed)) + LogPrintf("CWallet::GenerateNewHDChain -- Incorrect seed, generating random one instead\n"); + + // NOTE: empty mnemonic means "generate a new one for me" + std::string strMnemonic = GetArg("-mnemonic", ""); + // NOTE: default mnemonic passphrase is an empty string + std::string strMnemonicPassphrase = GetArg("-mnemonicpassphrase", ""); + + SecureVector vchMnemonic(strMnemonic.begin(), strMnemonic.end()); + SecureVector vchMnemonicPassphrase(strMnemonicPassphrase.begin(), strMnemonicPassphrase.end()); + + if (!newHdChain.SetMnemonic(vchMnemonic, vchMnemonicPassphrase, true)) + throw std::runtime_error(std::string(__func__) + ": SetMnemonic failed"); + } + newHdChain.Debug(__func__); + + if (!SetHDChain(newHdChain, false)) + throw std::runtime_error(std::string(__func__) + ": SetHDChain failed"); + + // clean up + mapArgs.erase("-hdseed"); + mapArgs.erase("-mnemonic"); + mapArgs.erase("-mnemonicpassphrase"); +} + +bool CWallet::SetHDChain(const CHDChain& chain, bool memonly) +{ + LOCK(cs_wallet); + + if (!CCryptoKeyStore::SetHDChain(chain)) + return false; + + if (!memonly && !CWalletDB(strWalletFile).WriteHDChain(chain)) + throw std::runtime_error(std::string(__func__) + ": WriteHDChain failed"); + + return true; +} + +bool CWallet::SetCryptedHDChain(const CHDChain& chain, bool memonly) +{ + LOCK(cs_wallet); + + if (!CCryptoKeyStore::SetCryptedHDChain(chain)) + return false; + + if (!memonly) { + if (!fFileBacked) + return false; + if (pwalletdbEncryption) { + if (!pwalletdbEncryption->WriteCryptedHDChain(chain)) + throw std::runtime_error(std::string(__func__) + ": WriteCryptedHDChain failed"); + } else { + if (!CWalletDB(strWalletFile).WriteCryptedHDChain(chain)) + throw std::runtime_error(std::string(__func__) + ": WriteCryptedHDChain failed"); + } + } + + return true; +} + +bool CWallet::GetDecryptedHDChain(CHDChain& hdChainRet) +{ + LOCK(cs_wallet); + + CHDChain hdChainTmp; + if (!GetHDChain(hdChainTmp)) { + return false; + } + + if (!DecryptHDChain(hdChainTmp)) + return false; + + // make sure seed matches this chain + if (hdChainTmp.GetID() != hdChainTmp.GetSeedHash()) + return false; + + hdChainRet = hdChainTmp; + + return true; +} + +bool CWallet::IsHDEnabled() +{ + CHDChain hdChainCurrent; + return GetHDChain(hdChainCurrent); +} + bool CWallet::IsMine(const CTransaction& tx) const { BOOST_FOREACH(const CTxOut& txout, tx.vout) @@ -2811,7 +3099,7 @@ bool CWallet::CreateCollateralTransaction(CMutableTransaction& txCollateral, std // make our change address CScript scriptChange; CPubKey vchPubKey; - assert(reservekey.GetReservedKey(vchPubKey)); // should never fail, as we just unlocked + assert(reservekey.GetReservedKey(vchPubKey, true)); // should never fail, as we just unlocked scriptChange = GetScriptForDestination(vchPubKey.GetID()); reservekey.KeepKey(); @@ -3069,7 +3357,7 @@ bool CWallet::CreateTransaction(const vector& vecSend, CWalletTx& wt // Reserve a new key pair from key pool CPubKey vchPubKey; - if (!reservekey.GetReservedKey(vchPubKey)) + if (!reservekey.GetReservedKey(vchPubKey, true)) { strFailReason = _("Keypool ran out, please call keypoolrefill first"); return false; @@ -3335,7 +3623,8 @@ DBErrors CWallet::LoadWallet(bool& fFirstRunRet) if (CDB::Rewrite(strWalletFile, "\x04pool")) { LOCK(cs_wallet); - setKeyPool.clear(); + setInternalKeyPool.clear(); + setExternalKeyPool.clear(); nKeysLeftSinceAutoBackup = 0; // Note: can't top-up keypool here, because wallet is locked. // User will be prompted to unlock wallet the next operation @@ -3363,7 +3652,8 @@ DBErrors CWallet::ZapWalletTx(std::vector& vWtx) if (CDB::Rewrite(strWalletFile, "\x04pool")) { LOCK(cs_wallet); - setKeyPool.clear(); + setInternalKeyPool.clear(); + setExternalKeyPool.clear(); nKeysLeftSinceAutoBackup = 0; // Note: can't top-up keypool here, because wallet is locked. // User will be prompted to unlock wallet the next operation @@ -3443,27 +3733,37 @@ bool CWallet::NewKeyPool() { LOCK(cs_wallet); CWalletDB walletdb(strWalletFile); - BOOST_FOREACH(int64_t nIndex, setKeyPool) + BOOST_FOREACH(int64_t nIndex, setInternalKeyPool) { walletdb.ErasePool(nIndex); - setKeyPool.clear(); + } + setInternalKeyPool.clear(); + BOOST_FOREACH(int64_t nIndex, setExternalKeyPool) { + walletdb.ErasePool(nIndex); + } + setExternalKeyPool.clear(); privateSendClient.fEnablePrivateSend = false; nKeysLeftSinceAutoBackup = 0; - if (IsLocked(true)) + if (!TopUpKeyPool()) return false; - int64_t nKeys = max(GetArg("-keypool", DEFAULT_KEYPOOL_SIZE), (int64_t)0); - for (int i = 0; i < nKeys; i++) - { - int64_t nIndex = i+1; - walletdb.WritePool(nIndex, CKeyPool(GenerateNewKey())); - setKeyPool.insert(nIndex); - } - LogPrintf("CWallet::NewKeyPool wrote %d new keys\n", nKeys); + LogPrintf("CWallet::NewKeyPool rewrote keypool\n"); } return true; } +size_t CWallet::KeypoolCountExternalKeys() +{ + AssertLockHeld(cs_wallet); // setExternalKeyPool + return setExternalKeyPool.size(); +} + +size_t CWallet::KeypoolCountInternalKeys() +{ + AssertLockHeld(cs_wallet); // setInternalKeyPool + return setInternalKeyPool.size(); +} + bool CWallet::TopUpKeyPool(unsigned int kpSize) { { @@ -3472,8 +3772,6 @@ bool CWallet::TopUpKeyPool(unsigned int kpSize) if (IsLocked(true)) return false; - CWalletDB walletdb(strWalletFile); - // Top up key pool unsigned int nTargetSize; if (kpSize > 0) @@ -3481,15 +3779,45 @@ bool CWallet::TopUpKeyPool(unsigned int kpSize) else nTargetSize = max(GetArg("-keypool", DEFAULT_KEYPOOL_SIZE), (int64_t) 0); - while (setKeyPool.size() < (nTargetSize + 1)) + // count amount of available keys (internal, external) + // make sure the keypool of external and internal keys fits the user selected target (-keypool) + int64_t amountExternal = setExternalKeyPool.size(); + int64_t amountInternal = setInternalKeyPool.size(); + int64_t missingExternal = std::max(std::max((int64_t) nTargetSize, (int64_t) 1) - amountExternal, (int64_t) 0); + int64_t missingInternal = std::max(std::max((int64_t) nTargetSize, (int64_t) 1) - amountInternal, (int64_t) 0); + + if (!IsHDEnabled()) + { + // don't create extra internal keys + missingInternal = 0; + } else { + nTargetSize *= 2; + } + bool fInternal = false; + CWalletDB walletdb(strWalletFile); + for (int64_t i = missingInternal + missingExternal; i--;) { int64_t nEnd = 1; - if (!setKeyPool.empty()) - nEnd = *(--setKeyPool.end()) + 1; - if (!walletdb.WritePool(nEnd, CKeyPool(GenerateNewKey()))) + if (i < missingInternal) { + fInternal = true; + } + if (!setInternalKeyPool.empty()) { + nEnd = *(--setInternalKeyPool.end()) + 1; + } + if (!setExternalKeyPool.empty()) { + nEnd = std::max(nEnd, *(--setExternalKeyPool.end()) + 1); + } + // TODO: implement keypools for all accounts? + if (!walletdb.WritePool(nEnd, CKeyPool(GenerateNewKey(0, fInternal), fInternal))) throw runtime_error("TopUpKeyPool(): writing generated key failed"); - setKeyPool.insert(nEnd); - LogPrintf("keypool added key %d, size=%u\n", nEnd, setKeyPool.size()); + + if (fInternal) { + setInternalKeyPool.insert(nEnd); + } else { + setExternalKeyPool.insert(nEnd); + } + LogPrintf("keypool added key %d, size=%u, internal=%d\n", nEnd, setInternalKeyPool.size() + setExternalKeyPool.size(), fInternal); + double dProgress = 100.f * nEnd / (nTargetSize + 1); std::string strMsg = strprintf(_("Loading wallet... (%3.2f %%)"), dProgress); uiInterface.InitMessage(strMsg); @@ -3498,7 +3826,7 @@ bool CWallet::TopUpKeyPool(unsigned int kpSize) return true; } -void CWallet::ReserveKeyFromKeyPool(int64_t& nIndex, CKeyPool& keypool) +void CWallet::ReserveKeyFromKeyPool(int64_t& nIndex, CKeyPool& keypool, bool fInternal) { nIndex = -1; keypool.vchPubKey = CPubKey(); @@ -3508,18 +3836,27 @@ void CWallet::ReserveKeyFromKeyPool(int64_t& nIndex, CKeyPool& keypool) if (!IsLocked(true)) TopUpKeyPool(); + fInternal = fInternal && IsHDEnabled(); + std::set& setKeyPool = fInternal ? setInternalKeyPool : setExternalKeyPool; + // Get the oldest key if(setKeyPool.empty()) return; CWalletDB walletdb(strWalletFile); - nIndex = *(setKeyPool.begin()); - setKeyPool.erase(setKeyPool.begin()); - if (!walletdb.ReadPool(nIndex, keypool)) - throw runtime_error("ReserveKeyFromKeyPool(): read failed"); - if (!HaveKey(keypool.vchPubKey.GetID())) - throw runtime_error("ReserveKeyFromKeyPool(): unknown key in key pool"); + nIndex = *setKeyPool.begin(); + setKeyPool.erase(nIndex); + if (!walletdb.ReadPool(nIndex, keypool)) { + throw std::runtime_error(std::string(__func__) + ": read failed"); + } + if (!HaveKey(keypool.vchPubKey.GetID())) { + throw std::runtime_error(std::string(__func__) + ": unknown key in key pool"); + } + if (keypool.fInternal != fInternal) { + throw std::runtime_error(std::string(__func__) + ": keypool entry misclassified"); + } + assert(keypool.vchPubKey.IsValid()); LogPrintf("keypool reserve %d\n", nIndex); } @@ -3537,27 +3874,32 @@ void CWallet::KeepKey(int64_t nIndex) LogPrintf("keypool keep %d\n", nIndex); } -void CWallet::ReturnKey(int64_t nIndex) +void CWallet::ReturnKey(int64_t nIndex, bool fInternal) { // Return to key pool { LOCK(cs_wallet); - setKeyPool.insert(nIndex); + if (fInternal) { + setInternalKeyPool.insert(nIndex); + } else { + setExternalKeyPool.insert(nIndex); + } } LogPrintf("keypool return %d\n", nIndex); } -bool CWallet::GetKeyFromPool(CPubKey& result) +bool CWallet::GetKeyFromPool(CPubKey& result, bool fInternal) { int64_t nIndex = 0; CKeyPool keypool; { LOCK(cs_wallet); - ReserveKeyFromKeyPool(nIndex, keypool); + ReserveKeyFromKeyPool(nIndex, keypool, fInternal); if (nIndex == -1) { if (IsLocked(true)) return false; - result = GenerateNewKey(); + // TODO: implement keypool for all accouts? + result = GenerateNewKey(0, fInternal); return true; } KeepKey(nIndex); @@ -3566,15 +3908,35 @@ bool CWallet::GetKeyFromPool(CPubKey& result) return true; } +static int64_t GetOldestKeyInPool(const std::set& setKeyPool, CWalletDB& walletdb) { + CKeyPool keypool; + int64_t nIndex = *(setKeyPool.begin()); + if (!walletdb.ReadPool(nIndex, keypool)) { + throw std::runtime_error(std::string(__func__) + ": read oldest key in keypool failed"); + } + assert(keypool.vchPubKey.IsValid()); + return keypool.nTime; +} + int64_t CWallet::GetOldestKeyPoolTime() { - int64_t nIndex = 0; - CKeyPool keypool; - ReserveKeyFromKeyPool(nIndex, keypool); - if (nIndex == -1) + LOCK(cs_wallet); + + // if the keypool is empty, return + if (setExternalKeyPool.empty() && setInternalKeyPool.empty()) return GetTime(); - ReturnKey(nIndex); - return keypool.nTime; + + CWalletDB walletdb(strWalletFile); + int64_t oldestKey = -1; + + // load oldest key from keypool, get time and return + if (!setInternalKeyPool.empty()) { + oldestKey = std::max(GetOldestKeyInPool(setInternalKeyPool, walletdb), oldestKey); + } + if (!setExternalKeyPool.empty()) { + oldestKey = std::max(GetOldestKeyInPool(setExternalKeyPool, walletdb), oldestKey); + } + return oldestKey; } std::map CWallet::GetAddressBalances() @@ -3724,17 +4086,19 @@ std::set CWallet::GetAccountAddresses(const std::string& strAcco return result; } -bool CReserveKey::GetReservedKey(CPubKey& pubkey) +bool CReserveKey::GetReservedKey(CPubKey& pubkey, bool fInternalIn) { if (nIndex == -1) { CKeyPool keypool; - pwallet->ReserveKeyFromKeyPool(nIndex, keypool); - if (nIndex != -1) + pwallet->ReserveKeyFromKeyPool(nIndex, keypool, fInternalIn); + if (nIndex != -1) { vchPubKey = keypool.vchPubKey; + } else { return false; } + fInternal = keypool.fInternal; } assert(vchPubKey.IsValid()); pubkey = vchPubKey; @@ -3743,27 +4107,24 @@ bool CReserveKey::GetReservedKey(CPubKey& pubkey) void CReserveKey::KeepKey() { - if (nIndex != -1) + if (nIndex != -1) { pwallet->KeepKey(nIndex); + } nIndex = -1; vchPubKey = CPubKey(); } void CReserveKey::ReturnKey() { - if (nIndex != -1) - pwallet->ReturnKey(nIndex); + if (nIndex != -1) { + pwallet->ReturnKey(nIndex, fInternal); + } nIndex = -1; vchPubKey = CPubKey(); } -void CWallet::GetAllReserveKeys(set& setAddress) const +static void LoadReserveKeysToSet(std::set& setAddress, const std::set& setKeyPool, CWalletDB& walletdb) { - setAddress.clear(); - - CWalletDB walletdb(strWalletFile); - - LOCK2(cs_main, cs_wallet); BOOST_FOREACH(const int64_t& id, setKeyPool) { CKeyPool keypool; @@ -3771,12 +4132,27 @@ void CWallet::GetAllReserveKeys(set& setAddress) const throw runtime_error("GetAllReserveKeyHashes(): read failed"); assert(keypool.vchPubKey.IsValid()); CKeyID keyID = keypool.vchPubKey.GetID(); - if (!HaveKey(keyID)) - throw runtime_error("GetAllReserveKeyHashes(): unknown key in key pool"); setAddress.insert(keyID); } } +void CWallet::GetAllReserveKeys(std::set& setAddress) const +{ + setAddress.clear(); + + CWalletDB walletdb(strWalletFile); + + LOCK2(cs_main, cs_wallet); + LoadReserveKeysToSet(setAddress, setInternalKeyPool, walletdb); + LoadReserveKeysToSet(setAddress, setExternalKeyPool, walletdb); + + BOOST_FOREACH (const CKeyID& keyID, setAddress) { + if (!HaveKey(keyID)) { + throw std::runtime_error(std::string(__func__) + ": unknown key in key pool"); + } + } +} + bool CWallet::UpdatedTransaction(const uint256 &hashTx) { { @@ -3795,7 +4171,7 @@ void CWallet::GetScriptForMining(boost::shared_ptr &script) { boost::shared_ptr rKey(new CReserveKey(this)); CPubKey pubkey; - if (!rKey->GetReservedKey(pubkey)) + if (!rKey->GetReservedKey(pubkey, false)) return; script = rKey; @@ -3979,12 +4355,14 @@ bool CWallet::GetDestData(const CTxDestination &dest, const std::string &key, st CKeyPool::CKeyPool() { nTime = GetTime(); + fInternal = false; } -CKeyPool::CKeyPool(const CPubKey& vchPubKeyIn) +CKeyPool::CKeyPool(const CPubKey& vchPubKeyIn, bool fInternalIn) { nTime = GetTime(); vchPubKey = vchPubKeyIn; + fInternal = fInternalIn; } CWalletKey::CWalletKey(int64_t nExpires) diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index b5ce77ccec4f..ba2f8e2ead62 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -71,6 +71,9 @@ static const CAmount nHighTransactionMaxFeeWarning = 100 * nHighTransactionFeeWa static const unsigned int MAX_FREE_TRANSACTION_CREATE_SIZE = 1000; static const bool DEFAULT_WALLETBROADCAST = true; +//! if set, all keys will be derived by using BIP32 +static const bool DEFAULT_USE_HD_WALLET = true; + class CBlockIndex; class CCoinControl; class COutput; @@ -86,6 +89,8 @@ enum WalletFeature FEATURE_WALLETCRYPT = 40000, // wallet encryption FEATURE_COMPRPUBKEY = 60000, // compressed public keys + FEATURE_HD = 120200, // Hierarchical key derivation after BIP32 (HD Wallet), BIP44 (multi-coin), BIP39 (mnemonic) + // which uses on-the-fly private key derivation FEATURE_LATEST = 61000 }; @@ -117,9 +122,10 @@ class CKeyPool public: int64_t nTime; CPubKey vchPubKey; + bool fInternal; // for change outputs CKeyPool(); - CKeyPool(const CPubKey& vchPubKeyIn); + CKeyPool(const CPubKey& vchPubKeyIn, bool fInternalIn); ADD_SERIALIZE_METHODS; @@ -129,6 +135,19 @@ class CKeyPool READWRITE(nVersion); READWRITE(nTime); READWRITE(vchPubKey); + if (ser_action.ForRead()) { + try { + READWRITE(fInternal); + } + catch (std::ios_base::failure&) { + /* flag as external address if we can't read the internal boolean + (this will be the case for any wallet before the HD chain split version) */ + fInternal = false; + } + } + else { + READWRITE(fInternal); + } } }; @@ -633,6 +652,9 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface void SyncMetaData(std::pair); + /* HD derive new child key (on internal or external chain) */ + void DeriveNewChildKey(const CKeyMetadata& metadata, CKey& secretRet, uint32_t nAccountIndex, bool fInternal /*= false*/); + public: /* * Main wallet lock. @@ -646,7 +668,24 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface bool fFileBacked; const std::string strWalletFile; - std::set setKeyPool; + void LoadKeyPool(int nIndex, const CKeyPool &keypool) + { + if (keypool.fInternal) { + setInternalKeyPool.insert(nIndex); + } else { + setExternalKeyPool.insert(nIndex); + } + + // If no metadata exists yet, create a default with the pool key's + // creation time. Note that this may be overwritten by actually + // stored metadata for that key later, which is fine. + CKeyID keyid = keypool.vchPubKey.GetID(); + if (mapKeyMetadata.count(keyid) == 0) + mapKeyMetadata[keyid] = CKeyMetadata(keypool.nTime); + } + + std::set setInternalKeyPool; + std::set setExternalKeyPool; std::map mapKeyMetadata; typedef std::map MasterKeyMap; @@ -709,6 +748,8 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface int64_t nTimeFirstKey; int64_t nKeysLeftSinceAutoBackup; + std::map mapHdPubKeys; //& setAddress) const; @@ -918,8 +971,8 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface unsigned int GetKeyPoolSize() { - AssertLockHeld(cs_wallet); // setKeyPool - return setKeyPool.size(); + AssertLockHeld(cs_wallet); // set{Ex,In}ternalKeyPool + return setInternalKeyPool.size() + setExternalKeyPool.size(); } bool SetDefaultKey(const CPubKey &vchPubKey); @@ -971,6 +1024,19 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface /* Mark a transaction (and it in-wallet descendants) as abandoned so its inputs may be respent. */ bool AbandonTransaction(const uint256& hashTx); + + /** + * HD Wallet Functions + */ + + /* Returns true if HD is enabled */ + bool IsHDEnabled(); + /* Generates a new HD chain */ + void GenerateNewHDChain(); + /* Set the HD chain model (chain child index counters) */ + bool SetHDChain(const CHDChain& chain, bool memonly); + bool SetCryptedHDChain(const CHDChain& chain, bool memonly); + bool GetDecryptedHDChain(CHDChain& hdChainRet); }; /** A key allocated from the key pool. */ @@ -980,11 +1046,13 @@ class CReserveKey : public CReserveScript CWallet* pwallet; int64_t nIndex; CPubKey vchPubKey; + bool fInternal; public: CReserveKey(CWallet* pwalletIn) { nIndex = -1; pwallet = pwalletIn; + fInternal = false; } ~CReserveKey() @@ -993,7 +1061,7 @@ class CReserveKey : public CReserveScript } void ReturnKey(); - bool GetReservedKey(CPubKey &pubkey); + bool GetReservedKey(CPubKey &pubkey, bool fInternalIn /*= false*/); void KeepKey(); void KeepScript() { KeepKey(); } }; diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 0076b52d055a..cad954f5b3a9 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -556,14 +556,7 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, ssKey >> nIndex; CKeyPool keypool; ssValue >> keypool; - pwallet->setKeyPool.insert(nIndex); - - // If no metadata exists yet, create a default with the pool key's - // creation time. Note that this may be overwritten by actually - // stored metadata for that key later, which is fine. - CKeyID keyid = keypool.vchPubKey.GetID(); - if (pwallet->mapKeyMetadata.count(keyid) == 0) - pwallet->mapKeyMetadata[keyid] = CKeyMetadata(keypool.nTime); + pwallet->LoadKeyPool(nIndex, keypool); } else if (strType == "version") { @@ -599,6 +592,45 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, return false; } } + else if (strType == "hdchain") + { + CHDChain chain; + ssValue >> chain; + if (!pwallet->SetHDChain(chain, true)) + { + strErr = "Error reading wallet database: SetHDChain failed"; + return false; + } + } + else if (strType == "chdchain") + { + CHDChain chain; + ssValue >> chain; + if (!pwallet->SetCryptedHDChain(chain, true)) + { + strErr = "Error reading wallet database: SetHDCryptedChain failed"; + return false; + } + } + else if (strType == "hdpubkey") + { + CPubKey vchPubKey; + ssKey >> vchPubKey; + + CHDPubKey hdPubKey; + ssValue >> hdPubKey; + + if(vchPubKey != hdPubKey.extPubKey.pubkey) + { + strErr = "Error reading wallet database: CHDPubKey corrupt"; + return false; + } + if (!pwallet->LoadHDPubKey(hdPubKey)) + { + strErr = "Error reading wallet database: LoadHDPubKey failed"; + return false; + } + } } catch (...) { return false; @@ -609,7 +641,8 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, static bool IsKeyType(string strType) { return (strType== "key" || strType == "wkey" || - strType == "mkey" || strType == "ckey"); + strType == "mkey" || strType == "ckey" || + strType == "hdchain" || strType == "chdchain"); } DBErrors CWalletDB::LoadWallet(CWallet* pwallet) @@ -673,8 +706,8 @@ DBErrors CWalletDB::LoadWallet(CWallet* pwallet) } pcursor->close(); - // Store initial pool size - pwallet->nKeysLeftSinceAutoBackup = pwallet->GetKeyPoolSize(); + // Store initial external keypool size since we mostly use external keys in mixing + pwallet->nKeysLeftSinceAutoBackup = pwallet->KeypoolCountExternalKeys(); LogPrintf("nKeysLeftSinceAutoBackup: %d\n", pwallet->nKeysLeftSinceAutoBackup); } catch (const boost::thread_interrupted&) { @@ -947,8 +980,8 @@ bool AutoBackupWallet (CWallet* wallet, std::string strWalletFile, std::string& nWalletBackups = -1; return false; } - // Update nKeysLeftSinceAutoBackup using current pool size - wallet->nKeysLeftSinceAutoBackup = wallet->GetKeyPoolSize(); + // Update nKeysLeftSinceAutoBackup using current external keypool size + wallet->nKeysLeftSinceAutoBackup = wallet->KeypoolCountExternalKeys(); LogPrintf("nKeysLeftSinceAutoBackup: %d\n", wallet->nKeysLeftSinceAutoBackup); if(wallet->IsLocked(true)) { strBackupWarning = _("Wallet is locked, can't replenish keypool! Automatic backups and mixing are disabled, please unlock your wallet to replenish keypool."); @@ -1091,7 +1124,7 @@ bool CWalletDB::Recover(CDBEnv& dbenv, const std::string& filename, bool fOnlyKe fReadOK = ReadKeyValue(&dummyWallet, ssKey, ssValue, wss, strType, strErr); } - if (!IsKeyType(strType)) + if (!IsKeyType(strType) && strType != "hdpubkey") continue; if (!fReadOK) { @@ -1127,3 +1160,31 @@ bool CWalletDB::EraseDestData(const std::string &address, const std::string &key nWalletDBUpdated++; return Erase(std::make_pair(std::string("destdata"), std::make_pair(address, key))); } + +bool CWalletDB::WriteHDChain(const CHDChain& chain) +{ + nWalletDBUpdated++; + return Write(std::string("hdchain"), chain); +} + +bool CWalletDB::WriteCryptedHDChain(const CHDChain& chain) +{ + nWalletDBUpdated++; + + if (!Write(std::string("chdchain"), chain)) + return false; + + Erase(std::string("hdchain")); + + return true; +} + +bool CWalletDB::WriteHDPubKey(const CHDPubKey& hdPubKey, const CKeyMetadata& keyMeta) +{ + nWalletDBUpdated++; + + if (!Write(std::make_pair(std::string("keymeta"), hdPubKey.extPubKey.pubkey), keyMeta, false)) + return false; + + return Write(std::make_pair(std::string("hdpubkey"), hdPubKey.extPubKey.pubkey), hdPubKey, false); +} diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index 64431ef0b6ab..4e8333381a9a 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -8,6 +8,7 @@ #include "amount.h" #include "wallet/db.h" +#include "hdchain.h" #include "key.h" #include @@ -53,7 +54,7 @@ class CKeyMetadata } CKeyMetadata(int64_t nCreateTime_) { - nVersion = CKeyMetadata::CURRENT_VERSION; + SetNull(); nCreateTime = nCreateTime_; } @@ -133,6 +134,11 @@ class CWalletDB : public CDB static bool Recover(CDBEnv& dbenv, const std::string& filename, bool fOnlyKeys); static bool Recover(CDBEnv& dbenv, const std::string& filename); + //! write the hdchain model (external chain child index counter) + bool WriteHDChain(const CHDChain& chain); + bool WriteCryptedHDChain(const CHDChain& chain); + bool WriteHDPubKey(const CHDPubKey& hdPubKey, const CKeyMetadata& keyMeta); + private: CWalletDB(const CWalletDB&); void operator=(const CWalletDB&);