Reported by @dooglus.
Fixes #8383
2080 | @@ -2081,7 +2081,7 @@ UniValue encryptwallet(const UniValue& params, bool fHelp) 2081 | // slack space in .dat files; that is bad if the old data is 2082 | // unencrypted private keys. So: 2083 | StartShutdown(); 2084 | - return "wallet encrypted; Bitcoin server stopping, restart to run with encrypted wallet. The keypool has been flushed, you need to make a new backup."; 2085 | + return "wallet encrypted; Bitcoin 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.";
Maybe the code should just check if HD is being used?
I have though about that but I think it's not worth adding another check here.
I don't think that's worth it either, just for the message
625 | @@ -626,6 +626,16 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase) 626 | 627 | Lock(); 628 | Unlock(strWalletPassphrase); 629 | + 630 | + // if we are using HD, replace the HD master key with a new one 631 | + if (!hdChain.masterKeyID.IsNull()) { 632 | + CKey key; 633 | + key.MakeNewKey(true); 634 | + return false;
??????????
It will report that the encryption did fail, restarting bitcoin-core will result in using the old seed with a valid wallet. Not sure if this is ideal.
Arg. No. This was for testing. Will remove it.
Will this destroy the old HD seed? Seems like it'd be valuable to keep it around somewhere...
@jonasschnelli Could you open this against master, please? (The backport can be done after merge and discussion/nits etc.)
@luke-jr The old HD seed is still available over dumpwallet. You can get the CKeyID from the used masterkey when calling validateaddress.
11 | @@ -12,13 +12,23 @@ class KeyPoolTest(BitcoinTestFramework): 12 | 13 | def run_test(self): 14 | nodes = self.nodes 15 | + addrBeforeEncrypting = nodes[0].getnewaddress() 16 | + addrBeforeEncryptingData = nodes[0].validateaddress(addrBeforeEncrypting) 17 | + walletInfoOld = nodes[0].getwalletinfo() 18 | + assert(addrBeforeEncryptingData['hdmasterkeyid'] == walletInfoOld['masterkeyid'])
Nit: please_use_lowercase_and_underscore
Fixed the nits. Sorry for opening this against 0.13 in the first place.
So this makes it impossible to encrypt a seed which is already in use and is planned to be used in the future, but I guess for 0.13 this is fair enough.
Concept ACK
625 | @@ -626,6 +626,15 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase) 626 | 627 | Lock(); 628 | Unlock(strWalletPassphrase); 629 | + 630 | + // if we are using HD, replace the HD master key with a new one 631 | + if (!hdChain.masterKeyID.IsNull()) { 632 | + CKey key; 633 | + key.MakeNewKey(true); 634 | + if (!SetHDMasterKey(key))
SetHDMasterKey() can only return true. When it fails it throws an exception. Do we need to catch that exception?
Agree that it is confusing to have bool SomeFunction(), where the return value indicates if something bad happens but at the same time this is unused and exceptions are thrown. This should be made consistent.
I think this solution is fine for the moment. The return value of SetHDMasterKey() is for future extensions and I think its useful even if its not possible to get a false returned with the current implementation.
The exception will already be caught though the RPC/Server logic (EncryptWallet() will only be called in the RPC context where we "handle" exceptions).
@jonasschnelli Does BDB guarantee the order in which keys are iterated? If not then this isn't guaranteed to work.
@jonasschnelli Indeed you're going to need to use the key metadata timestamp to select which hdchain to use.
@pstratem It reads to me as if the old hdchain key is overwritten by the new one:
bool CWalletDB::WriteHDChain(const CHDChain& chain)
{
nWalletDBUpdated++;
return Write(std::string("hdchain"), chain);
}
This WriteHDChain() function is used each time a new address is generated:
// update the chain model in the database
if (!CWalletDB(strWalletFile).WriteHDChain(hdChain))
The original chain could be stored under a different database key if we need to keep it around somewhere.
@pstratem: CWalletDB and BDB is a key value storage as far as I know. Writing the same key at later stage should replace the current value. It may be possible to have the old K/V still stored in the database, though, CWalletDB/BDB needs to make sure the later write operation will result in the laster read/mem-map operation during load wallet.
But I agree we need to keep track of the older/replaced seed. Maybe we should add something to the CKeyMetadata in that case (or at least CAddressBook).
Pushed a commit that...
CKeyMetadata record together with a HD master key where we set a hdkeypath="m" for later identifying this key as hd master keyhttps://github.com/bitcoin/bitcoin/pull/8206/files would make use of this. It would show old/rewritten master keys in the dumped wallet.
899 | @@ -900,8 +900,11 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface 900 | /* Set the HD chain model (chain child index counters) */ 901 | bool SetHDChain(const CHDChain& chain, bool memonly); 902 | 903 | + /* Generates a new HD master key (will not be activated) */ 904 | + CPubKey GenerateNewHDMasterKey(CKey& key);
nit: argument is never used anywhere, but I could see this being useful in future
1185 | + 1186 | + // calculate the pubkey 1187 | + CPubKey pubkey = key.GetPubKey(); 1188 | + assert(key.VerifyPubKey(pubkey)); 1189 | + 1190 | + // set the hd keypath to "m" -> Master, refere the masterkeyid to itself
refers*
630 | + // if we are using HD, replace the HD master key (seed) with a new one 631 | + if (!hdChain.masterKeyID.IsNull()) { 632 | + CKey key; 633 | + CPubKey masterPubKey = GenerateNewHDMasterKey(key);; 634 | + if (!SetHDMasterKey(masterPubKey)) 635 | + return false;
I think this is ok, but someone more knowledgeable should make sure that returning now before re-locking doesn't have a negative side-effect vs previous behavior.
630 | + // if we are using HD, replace the HD master key (seed) with a new one 631 | + if (!hdChain.masterKeyID.IsNull()) { 632 | + CKey key; 633 | + CPubKey masterPubKey = GenerateNewHDMasterKey(); 634 | + if (!SetHDMasterKey(masterPubKey)) 635 | + return false;
I think this error detection case is extremely unlikely and probably on the same level then an exception during NewKeyPool().
I don't see a better way of handling this case.
Fixed @instagibbs nits.
@jonasschnelli needs to be one commit
Is this only aimed at 0.13 on purpose?
No. I should have opened this PR against master. I'll open a PR against master as soon as this one is "final" (maybe it cleanly applies to master).
Tested ACK 77c912d
Sorry, wrong issue - will test this one later
Rebased (against 0.13).
code-review ACK de45c06, going to test
I did this:
$ rm ~/.bitcoin/regtest/wallet.dat
$ src/bitcoind -regtest -rpcport=1234 -daemon -usehd=1
Bitcoin server starting
$ src/bitcoin-cli -regtest -rpcport=1234 dumpwallet "/tmp/dump_pre"
$ src/bitcoin-cli -regtest -rpcport=1234 encryptwallet "pass"
wallet encrypted; Bitcoin server stopping, restart to run with encrypted wallet. The keypool has been flushed, you need to make a new backup.
$ src/bitcoind -regtest -rpcport=1234 -daemon -usehd=1
Bitcoin server starting
$ src/bitcoin-cli -regtest -rpcport=1234 walletpassphrase "pass" 12345
$ src/bitcoin-cli -regtest -rpcport=1234 dumpwallet "/tmp/dump_post"
$ src/bitcoin-cli -regtest -rpcport=1234 stop
$ diff -du /tmp/dump_pre /tmp/dump_post
Result: http://www.hastebin.com/ajuhahofan.diff
Looks OK to me. The old reserve keys have been demoted to "change", and there are new reserve keys in the keypool. These keys are different, suggesting a new master key was used to generate them.
The old master key was kept around as "oldhdmaster":
-cSoMD7tEsoJNfSS3cL6N3E9gXpuQh7wgs79RDUBE6NjTEWS6Ptda 2016-07-28T10:48:28Z hdmaster=1 # addr=msqHNkZcD6ybT7xdCkquN4esLPLmoRyk8b hdkeypath=m
+cSoMD7tEsoJNfSS3cL6N3E9gXpuQh7wgs79RDUBE6NjTEWS6Ptda 2016-07-28T10:48:28Z inactivehdmaster=1 # addr=msqHNkZcD6ybT7xdCkquN4esLPLmoRyk8b hdkeypath=m
And a new one was added
+cRrbkixFuTz1esCbJTz3QqNGyEcPjFCjVyHRTy1JDxKDbHYXn9Qd 2016-07-28T10:48:43Z hdmaster=1 # addr=n3phnt6MDc2xUvKXrf8iW6ZiwtL2i7Rusm hdkeypath=m
The single key that is automatically generated (with empty label) at initialization stayed the same
cRj4c74AFYdH9W1vbM3UgnaCeGbwztu3qb11NgDReiudtS2Q1ZAr 2016-07-28T10:48:28Z label= # addr=mwR8SR9sfKk7Q8z99xpdcCUNtccq81iRup hdkeypath=m/0'/0'/0'
For the new reserve keys it starts counting again at m/0'/0'/0'
+cNRrj6BtSxcXySqU6WgWgGEHMARifd1VXCVECdsXx4ervHE7iS4t 2016-07-28T10:48:43Z reserve=1 # addr=moBwgfzRf8H4ZjweCbQcckuJbMC16jwtD1 hdkeypath=m/0'/0'/0'
I think everything is as expected. Tested ACK.
Removed "needs backport"
625 | @@ -626,6 +626,15 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase) 626 | 627 | Lock(); 628 | Unlock(strWalletPassphrase); 629 | + 630 | + // if we are using HD, replace the HD master key (seed) with a new one 631 | + if (!hdChain.masterKeyID.IsNull()) { 632 | + CKey key;
Unused variable.
1198 | + // mem store the metadata 1199 | + mapKeyMetadata[pubkey.GetID()] = metadata; 1200 | + 1201 | + // write the key&metadata to the database 1202 | + if (!AddKeyPubKey(key, pubkey)) 1203 | + throw std::runtime_error("CWallet::GenerateNewKey(): AddKey failed");
Nit: Wrong name