-
Notifications
You must be signed in to change notification settings - Fork 37.8k
Add 'sethdseed' RPC to initialize or replace HD seed. #11085
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not entirely sure if we should allow mixed wallets (IMO multiwallet would provide a more sane path to run HD and nonHD side a side).
I could imagine that sethdseed
is available and marked as expert feature. It's one of the commands that has the possibility to footgun and may results in lost funds (==careful concept-thinking required)
src/wallet/wallet.cpp
Outdated
|
||
CPubKey CWallet::GenerateNewHDMasterKey(CKey key) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be called DeriveNewMasterHDKey()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point.
src/wallet/rpcwallet.cpp
Outdated
|
||
masterPubKey = pwallet->GenerateNewHDMasterKey(key); | ||
} else | ||
masterPubKey = pwallet->GenerateNewHDMasterKey(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does prevent from setting a new seed in an existing HD wallet? Or do we want to allow that? If so, we should at least warn in the help text or even add an additional boolean to make sure the user is aware of the implications (hopefully).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It allows you to replace your existing HD seed with a new one, for example if you are worried that your old seed was exposed to a third party, you can switch to a new one without having to lose transaction history.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But shouldn't that worry (compromised seed key or compromised sub-key) lead to moving the funds to a new seed? It may hurts privacy is done in a single N-to-1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you would want to move funds from keys you suspect are compromised. The user is responsible for deciding how and when to move the funds. This pull request simply makes it possible to switch to a new uncompromised set of addresses.
src/wallet/rpcwallet.cpp
Outdated
throw JSONRPCError(RPC_WALLET_ERROR, "Storing master key failed"); | ||
|
||
if (fFlushKeyPool) | ||
pwallet->NewKeyPool(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not flushing the pool may lead to unrecoverable funds. If one sets an HD seed without flushing, backup directly, he may miss -keypoolsize
keys (maybe 100, maybe 1000).
I'd say a flush is mandatory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand. How do old un-flushed keys in the mempool cause loss of funds if the HD seed is changed but not if it isn't? They're either backed up or they're not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your right. It would only be a problem if the user considers only backing up the seed (user may thinks I just need to backup the 64 char hex string) at this point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If he only backs up the seed, he loses all his funds in pre-HD addresses whether we flush the keypool or not.
We default to flushing the keypool. If a user chooses not to flush the pool I think he likely understands what he's doing.
I wasn't aware of 'multiwallet' until I merged my changes to the master branch, and still haven't looked into it in any detail. If it allows me to see my pre-HD and post-HD transactions in a single unified list then I agree. |
This RPC should probably also bump the version number to at 139900 so that HD chain split is supported as well. |
@jonasschnelli hmm, multiwallet has a rather significant usability burden (eg not being able to spend from both sides) vs just allowing people to add an HD master key to their wallet. I'm not sure why we wouldn't allow people to upgrade their wallet using the same -upgradewallet path. Independantly, I do think we need the option to allow someone to rotate their HD master key if they desire (with appropriate backup warnings). |
if (request.fHelp || request.params.size() > 2) { | ||
throw std::runtime_error( | ||
"sethdseed ( \"flushkeypool\" \"seed\" )\n" | ||
"\nSet or reset the HD wallet seed.\n" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs way more documentation here (note it keeps old keys, you probably want to backup immediately after using this and before otherwise using your wallet, etc).
@TheBlueMatt > I'm not sure why we wouldn't allow people to upgrade their wallet using the same -upgradewallet path. I don't think -upgradewallet should invalidate old backups, whereas setting the HD wallet seed does invalidate old backups if it flushes the keypool, which it does do by default. Perhaps that's an argument for keeping the seed-setting code separate from -upgradewallet, especially since a user may want to change his HD seed after setting it. It would make sense to have -upgradewallet set an HD seed if it's not already set, but not flush the keypool. In that way the old backup isn't invalidated, and the user's old strategy of "make new backup every 100 transactions" remains safe. |
@dooglus hmm, well if we dont want to use the same system (we could, of course, call the argument something different for this upgrade), then we've horribly fucked up (we used a wallet version number for HD support, so if we dont allow upgrade using the same version number stuff then we're left without any good options to add any other future features, eg segwit support). I'm fine with, eg, refusing to upgrade past HD unless you use -upgradetohd, and also it seems perfectly reasonable to not flush keypool with the HD upgrade (why tie them together? The user can flush separately if they want). I do think we need the ability to rotate the HD key before we add any other features past HD (cause we shouldnt force users into HD unless they can still use it as before by rotating their HD seed), so thanks for this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing tests for errors and success case.
src/wallet/rpcwallet.cpp
Outdated
fFlushKeyPool = request.params[0].get_bool(); | ||
|
||
CPubKey masterPubKey; | ||
if (request.params.size() > 1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (!request.params[1].isNull()) {
src/wallet/rpcwallet.cpp
Outdated
EnsureWalletIsUnlocked(pwallet); | ||
|
||
bool fFlushKeyPool = true; | ||
if (request.params.size() > 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (!request.params[0].isNull()) {
src/wallet/rpcwallet.cpp
Outdated
LOCK2(cs_main, pwallet->cs_wallet); | ||
EnsureWalletIsUnlocked(pwallet); | ||
|
||
bool fFlushKeyPool = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bool flush_key_pool = true;
src/wallet/rpcwallet.cpp
Outdated
if (request.params.size() > 1) { | ||
CKey key; | ||
std::string strKey = AccountFromValue(request.params[1]); | ||
if (strKey.length() != 64 || !IsHex(strKey)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing { }
.
src/wallet/rpcwallet.cpp
Outdated
if (strKey.length() != 64 || !IsHex(strKey)) | ||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Key should be 64 hex characters"); | ||
|
||
std::vector <unsigned char> vch = ParseHex(strKey); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove space before <
.
src/wallet/rpcwallet.cpp
Outdated
if (!pwallet->SetHDMasterKey(masterPubKey)) | ||
throw JSONRPCError(RPC_WALLET_ERROR, "Storing master key failed"); | ||
|
||
if (fFlushKeyPool) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing { }
.
src/wallet/wallet.cpp
Outdated
|
||
CPubKey CWallet::DeriveNewMasterHDKey(CKey key) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const CKey& key
?
src/wallet/rpcwallet.cpp
Outdated
@@ -3132,6 +3132,66 @@ UniValue generate(const JSONRPCRequest& request) | |||
return generateBlocks(coinbase_script, num_generate, max_tries, true); | |||
} | |||
|
|||
UniValue sethdseed(const JSONRPCRequest& request) | |||
{ | |||
CWallet * const pwallet = GetWalletForJSONRPCRequest(request); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove space before *
.
src/wallet/rpcwallet.cpp
Outdated
if (request.params.size() > 0) | ||
fFlushKeyPool = request.params[0].get_bool(); | ||
|
||
CPubKey masterPubKey; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
master_pub_key
?
src/wallet/rpcwallet.cpp
Outdated
key.Set(vch.begin(), vch.end(), true); | ||
|
||
if (pwallet->HaveKey(key.GetPubKey().GetID())) | ||
throw JSONRPCError(RPC_WALLET_ERROR, "Key already exists in wallet"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TheBlueMatt Sure, I'll get right on that. Hopefully today. I wasn't previously aware of the developer notes but now I am. |
Needs rebase too. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK. Comments inline.
This also needs rebase, more documentation, and tests.
I'm not sure if it is necessary or useful to be able to set your own seed. Or if it is necessary or useful to have the option to not regenerate the keypool from the seed.
src/wallet/rpcwallet.cpp
Outdated
CPubKey masterPubKey; | ||
if (request.params.size() > 1) { | ||
CKey key; | ||
std::string strKey = AccountFromValue(request.params[1]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you doing AccountFromValue
? There are no accounts here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
src/wallet/rpcwallet.cpp
Outdated
if (request.params.size() > 1) { | ||
CKey key; | ||
std::string strKey = AccountFromValue(request.params[1]); | ||
if (strKey.length() != 64 || !IsHex(strKey)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use a WIF key instead of a hex seed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. I wanted to import the HD seed from the wallet in which I had been testing HD derivation into my main wallet, and happened to have it in hex format.
I'll switch to requiring a WIF key.
Addressed @promag's formatting nits. Removed trailing period from commit message. Rebased. Squashed. Didn't add tests or backup warnings yet. |
I want to be able to make a backup of my wallet on paper, by writing down nothing but my seed. In order to restore from the paper backup I need to be able to set my own seed. For this use case we would also need a 'dumpprivkey' for the master seed.
It's possible the user makes regular weekly backups of his wallet. If we regenerate the keypool, he's at risk of loss of funds until the next weekly backup is made. If we give him the option of not regenerating the keypool, his previous backup will remain valid until the next backup is made. It's also possible he makes a backup immediately, but it is somehow corrupted. If we allow him to not regenerate his keypool then he has the possibility of recovering funds using older backups. If we insist he regenerates his keypool, we're forcing him to rely entirely on the new backup he made, for no good reason. |
TODO:
|
Now the code accepts WIF keys, not hex, and I added a warning to the doc string saying that the user needs to make a new backup. Should I add tests to the existing |
utACK ddad8fc, awaiting tests
Travis error is unrelated and was fixed in #11271
I think that would make the most sense since this is directly manipulating HD wallets, yep |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In-line comments are mostly nits, but we need some thing to keep people from putting an HD key in their wallet without a walletupgrade here, probably making SetHDMasterKey fail unless wallet version supports HD. In the future we may have an upgrade function which this could use (with appropriate documentation).
"\nNote that you will need to MAKE A NEW BACKUP of your wallet after setting the HD wallet seed.\n" | ||
+ HelpRequiringPassphrase(pwallet) + | ||
"\nArguments:\n" | ||
"1. \"flushkeypool\" (boolean, optional, default=true) Whether to flush old unused addresses from the keypool.\n" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe clarify a bit further what this implies? (eg "ie the next address from getnewaddress will be from this new seed. If this is not set, the old seed's derived addresses will be used until the keypool has been depleted.").
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So:
"1. \"flushkeypool\" (boolean, optional, default=true) Whether to flush old unused addresses from the keypool.\n"
" If true, the next address from getnewaddress will be from this new seed.\n"
" If false, addresses from the existing keypool will be used until it has been depleted.\n"
Note that the keypool may not contain addresses derived from an old seed, and may contain pre-HD random addresses.
src/wallet/rpcwallet.cpp
Outdated
// check that we don't already have this key, compressed or otherwise | ||
key2.Set(key.begin(), key.end(), !key.IsCompressed()); | ||
if (pwallet->HaveKey(key.GetPubKey().GetID()) || pwallet->HaveKey(key2.GetPubKey().GetID())) { | ||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Already have this key"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: maybe "(either as an HD seed or as a loose private key)" to note that you cant use an existing loose private key as an HD seed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this comment, @promag said that there was a similar error elsewhere. I think he meant that I should use the same string so as not to unnecessarily increase the required translation effort. I'll switch to using your string.
@dooglus Can you rebase? There were some unrelated Travis that affected your PR, but have been fixed in master since. |
ddad8fc
to
e0cb3a3
Compare
Rebased. |
@TheBlueMatt > we need some thing to keep people from putting an HD key in their wallet without a walletupgrade here, probably making SetHDMasterKey fail unless wallet version supports HD I'm not familiar with walletupgrade. I made my wallet in 2011 and never (to my knowledge) ran walletupgrade. I was able to use this PR to add an HD key to my 2011 wallet and it seems to work. What am I missing? |
e0cb3a3
to
4a565cb
Compare
@dooglus I think it may work, but a) it wont use hd-split (which it should, if possible) and b) old versions opening the wallet which has had HD key added to it will (I believe) open normally and add non-HD keys instead of failing to open, which doing a -walletupgrade will ensure. |
Needs rebase Wasn't the plan to have this (or something similar) in for 0.16 since creating non-HD wallets is now a thing? |
@dooglus Are you still working on this? |
No I'm not. I don't think I have what it takes to get this merged. |
I've picked this up in #12560 |
Thank you. |
This should be closed as it is replaced by #12560 |
OK. |
a8da482 Bump wallet version for pre split keypool (Andrew Chow) dfcd9f3 Use a keypool of presplit keys after upgrading to hd chain split (Andrew Chow) 5c50e93 Allow -upgradewallet to upgradewallets to HD (Andrew Chow) 2bcf2b5 Test sethdseed (Andrew Chow) b5ba01a Add 'sethdseed' RPC to initialize or replace HD seed (Chris Moore) dd3c07a Separate HaveKey function that checks whether a key is in a keystore (Andrew Chow) Pull request description: Revival/rebase of #11085 Adds a new command `sethdseed` which allows you to either set or generate a new HD seed to be used. A new keypool can be generated or the original one kept and new keys added to the keypool will come from the new HD seed. Wallets that are not HD will be upgraded to be version FEATURE_HD_SPLIT when the `sethdseed` RPC command is used. I have also add some tests for this. Additionally `-upgradewallet` can now be used to upgrade a wallet from non-HD to HD. When it is used for such an upgrade, the keypool will be regenerated. Tree-SHA512: e56c792e150590429ac4a1061e8d6f7b20cca06366e184eb9bbade4cd6ae82699a28fe84f87031eadba97ad2c1606517a105f00fb7b45779c979243020071adb
As mentioned in #11070, I wanted to be able to use HD addresses in my legacy wallet.
Is a change along these lines acceptable? What needs changing?
I'm aware it's missing tests. I'll add those if there's a chance this could be merged. If not, my wallet now uses HD addresses, so that's good enough for me.