-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: enable HD wallets by default #5807
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
Changes from all commits
4ae116d
4620a0e
2a5a08d
3de3c7e
bfec081
c7ee63d
b698a9f
af49d5a
8c29608
73b5372
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Notable changes | ||
|
|
||
| ## HD Wallets Enabled by Default | ||
|
|
||
| In this release, we are taking a significant step towards enhancing the Dash wallet's usability by enabling Hierarchical Deterministic (HD) wallets by default. This change aligns the behavior of `dashd` and `dash-qt` with the previously optional `-usehd=1` flag, making HD wallets the standard for all users. | ||
|
|
||
| While HD wallets are now enabled by default to improve user experience, Dash Core still allows for the creation of non-HD wallets by using the `-usehd=0` flag. However, users should be aware that this option is intended for legacy support and will be removed in future releases. Importantly, even with an HD wallet, users can still import non-HD private keys, ensuring flexibility in managing their funds. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2681,76 +2681,53 @@ static UniValue upgradetohd(const JSONRPCRequest& request) | |
| }, | ||
| }.Check(request); | ||
|
|
||
| std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request); | ||
| if (!wallet) return NullUniValue; | ||
| CWallet* const pwallet = wallet.get(); | ||
| LegacyScriptPubKeyMan* spk_man = pwallet->GetLegacyScriptPubKeyMan(); | ||
| if (!spk_man) { | ||
| throw JSONRPCError(RPC_WALLET_ERROR, "This type of wallet does not support this command"); | ||
| } | ||
| std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request); | ||
| if (!pwallet) return NullUniValue; | ||
|
|
||
| bool generate_mnemonic = request.params[0].isNull() || request.params[0].get_str().empty(); | ||
|
|
||
| { | ||
| LOCK(pwallet->cs_wallet); | ||
|
|
||
| // Do not do anything to HD wallets | ||
| if (pwallet->IsHDEnabled()) { | ||
| throw JSONRPCError(RPC_WALLET_ERROR, "Cannot upgrade a wallet to HD if it is already upgraded to HD."); | ||
| } | ||
|
|
||
| if (!pwallet->SetMaxVersion(FEATURE_HD)) { | ||
| throw JSONRPCError(RPC_WALLET_ERROR, "Cannot downgrade wallet"); | ||
| SecureString secureWalletPassphrase; | ||
| secureWalletPassphrase.reserve(100); | ||
| // TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string) | ||
|
||
| // Alternately, find a way to make request.params[0] mlock()'d to begin with. | ||
| if (!request.params[2].isNull()) { | ||
| secureWalletPassphrase = request.params[2].get_str().c_str(); | ||
| if (!pwallet->Unlock(secureWalletPassphrase)) { | ||
| throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "The wallet passphrase entered was incorrect"); | ||
| } | ||
| } | ||
|
|
||
| if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { | ||
| throw JSONRPCError(RPC_WALLET_ERROR, "Error: Private keys are disabled for this wallet"); | ||
| } | ||
| EnsureWalletIsUnlocked(pwallet.get()); | ||
|
|
||
| bool prev_encrypted = pwallet->IsCrypted(); | ||
| SecureString secureMnemonic; | ||
| secureMnemonic.reserve(256); | ||
| if (!generate_mnemonic) { | ||
| secureMnemonic = request.params[0].get_str().c_str(); | ||
| } | ||
|
|
||
| SecureString secureWalletPassphrase; | ||
| secureWalletPassphrase.reserve(100); | ||
| // TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string) | ||
| // Alternately, find a way to make request.params[0] mlock()'d to begin with. | ||
| if (request.params[2].isNull()) { | ||
| if (prev_encrypted) { | ||
| throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Cannot upgrade encrypted wallet to HD without the wallet passphrase"); | ||
| } | ||
| } else { | ||
| secureWalletPassphrase = request.params[2].get_str().c_str(); | ||
| if (!pwallet->Unlock(secureWalletPassphrase)) { | ||
| throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "The wallet passphrase entered was incorrect"); | ||
| } | ||
| } | ||
| SecureString secureMnemonicPassphrase; | ||
| secureMnemonicPassphrase.reserve(256); | ||
| if (!request.params[1].isNull()) { | ||
| secureMnemonicPassphrase = request.params[1].get_str().c_str(); | ||
| } | ||
| // TODO: breaking changes kept for v21! | ||
| // instead upgradetohd let's use more straightforward 'sethdseed' | ||
| constexpr bool is_v21 = false; | ||
| const int previous_version{pwallet->GetVersion()}; | ||
| if (is_v21 && previous_version >= FEATURE_HD) { | ||
| return JSONRPCError(RPC_WALLET_ERROR, "Already at latest version. Wallet version unchanged."); | ||
| } | ||
|
|
||
| SecureString secureMnemonic; | ||
| secureMnemonic.reserve(256); | ||
| if (!generate_mnemonic) { | ||
| secureMnemonic = request.params[0].get_str().c_str(); | ||
| } | ||
| bilingual_str error; | ||
| const bool wallet_upgraded{pwallet->UpgradeToHD(secureMnemonic, secureMnemonicPassphrase, secureWalletPassphrase, error)}; | ||
|
|
||
| SecureString secureMnemonicPassphrase; | ||
| secureMnemonicPassphrase.reserve(256); | ||
| if (!request.params[1].isNull()) { | ||
| secureMnemonicPassphrase = request.params[1].get_str().c_str(); | ||
| if (!secureWalletPassphrase.empty() && !pwallet->IsCrypted()) { | ||
| if (!pwallet->EncryptWallet(secureWalletPassphrase)) { | ||
| throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Failed to encrypt HD wallet"); | ||
| } | ||
| } | ||
|
|
||
| pwallet->WalletLogPrintf("Upgrading wallet to HD\n"); | ||
| pwallet->SetMinVersion(FEATURE_HD); | ||
|
|
||
| if (prev_encrypted) { | ||
| if (!pwallet->GenerateNewHDChainEncrypted(secureMnemonic, secureMnemonicPassphrase, secureWalletPassphrase)) { | ||
| throw JSONRPCError(RPC_WALLET_ERROR, "Failed to generate encrypted HD wallet"); | ||
| } | ||
| } else { | ||
| spk_man->GenerateNewHDChain(secureMnemonic, secureMnemonicPassphrase); | ||
| if (!secureWalletPassphrase.empty()) { | ||
| if (!pwallet->EncryptWallet(secureWalletPassphrase)) { | ||
| throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Failed to encrypt HD wallet"); | ||
| } | ||
| } | ||
| } | ||
| if (!wallet_upgraded) { | ||
| throw JSONRPCError(RPC_WALLET_ERROR, error.original); | ||
| } | ||
|
|
||
| // If you are generating new mnemonic it is assumed that the addresses have never gotten a transaction before, so you don't need to rescan for transactions | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -297,6 +297,10 @@ std::shared_ptr<CWallet> CreateWallet(interfaces::Chain& chain, interfaces::Coin | |
| status = DatabaseStatus::FAILED_CREATE; | ||
| return nullptr; | ||
| } | ||
| if (gArgs.GetBoolArg("-usehd", DEFAULT_USE_HD_WALLET)) { | ||
| wallet->WalletLogPrintf("Set HD by default\n"); | ||
| wallet->SetMinVersion(FEATURE_HD); | ||
| } | ||
|
|
||
| // Encrypt the wallet | ||
| if (!passphrase.empty() && !(wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { | ||
|
|
@@ -306,29 +310,25 @@ std::shared_ptr<CWallet> CreateWallet(interfaces::Chain& chain, interfaces::Coin | |
| return nullptr; | ||
| } | ||
| if (!create_blank) { | ||
| { | ||
| // TODO: drop this condition after removing option to create non-HD wallets | ||
| // related backport bitcoin#11250 | ||
| if (wallet->GetVersion() >= FEATURE_HD) { | ||
| if (!wallet->GenerateNewHDChainEncrypted(/*secureMnemonic=*/"", /*secureMnemonicPassphrase=*/"", passphrase)) { | ||
UdjinM6 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| error = Untranslated("Error: Failed to generate encrypted HD wallet"); | ||
| status = DatabaseStatus::FAILED_CREATE; | ||
| return nullptr; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Unlock the wallet | ||
| if (!wallet->Unlock(passphrase)) { | ||
| error = Untranslated("Error: Wallet was encrypted but could not be unlocked"); | ||
| status = DatabaseStatus::FAILED_ENCRYPT; | ||
| return nullptr; | ||
| } | ||
|
|
||
| // Set a HD chain for the wallet | ||
| // TODO: re-enable this and `keypoolsize_hd_internal` check in `wallet_createwallet.py` | ||
| // when HD is the default mode (make sure this actually works!)... | ||
| // if (auto spk_man = wallet->GetLegacyScriptPubKeyMan() { | ||
| // if (!spk_man->GenerateNewHDChainEncrypted("", "", passphrase)) { | ||
| // throw JSONRPCError(RPC_WALLET_ERROR, "Failed to generate encrypted HD wallet"); | ||
| // } | ||
| // } | ||
| // ... and drop this | ||
| LOCK(wallet->cs_wallet); | ||
| wallet->UnsetWalletFlag(WALLET_FLAG_BLANK_WALLET); | ||
| if (auto spk_man = wallet->GetLegacyScriptPubKeyMan()) { | ||
| spk_man->NewKeyPool(); | ||
| } | ||
| // end TODO | ||
|
|
||
| // backup the wallet we just encrypted | ||
| if (!wallet->AutoBackupWallet("", error, warnings) && !error.original.empty()) { | ||
| status = DatabaseStatus::FAILED_ENCRYPT; | ||
|
|
@@ -4601,6 +4601,10 @@ std::shared_ptr<CWallet> CWallet::Create(interfaces::Chain& chain, interfaces::C | |
| if (gArgs.GetBoolArg("-usehd", DEFAULT_USE_HD_WALLET) && !walletInstance->IsHDEnabled()) { | ||
| std::string strSeed = gArgs.GetArg("-hdseed", "not hex"); | ||
|
|
||
| // ensure this wallet.dat can only be opened by clients supporting HD | ||
| walletInstance->WalletLogPrintf("Upgrading wallet to HD\n"); | ||
| walletInstance->SetMinVersion(FEATURE_HD); | ||
|
|
||
| if (gArgs.IsArgSet("-hdseed") && IsHex(strSeed)) { | ||
| CHDChain newHdChain; | ||
| std::vector<unsigned char> vchSeed = ParseHex(strSeed); | ||
|
|
@@ -4630,10 +4634,6 @@ std::shared_ptr<CWallet> CWallet::Create(interfaces::Chain& chain, interfaces::C | |
| } | ||
| } | ||
|
|
||
| // ensure this wallet.dat can only be opened by clients supporting HD | ||
| walletInstance->WalletLogPrintf("Upgrading wallet to HD\n"); | ||
| walletInstance->SetMinVersion(FEATURE_HD); | ||
|
|
||
| // clean up | ||
| gArgs.ForceRemoveArg("hdseed"); | ||
| gArgs.ForceRemoveArg("mnemonic"); | ||
|
|
@@ -4897,7 +4897,45 @@ bool CWallet::UpgradeWallet(int version, bilingual_str& error) | |
| return false; | ||
| } | ||
|
|
||
| // TODO: consider discourage users to skip passphrase for HD wallets for v21 | ||
|
||
| if (false && nMaxVersion >= FEATURE_HD && !IsHDEnabled()) { | ||
| error = Untranslated("You should use upgradetohd RPC to upgrade non-HD wallet to HD"); | ||
| return false; | ||
| } | ||
|
|
||
| SetMaxVersion(nMaxVersion); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| bool CWallet::UpgradeToHD(const SecureString& secureMnemonic, const SecureString& secureMnemonicPassphrase, const SecureString& secureWalletPassphrase, bilingual_str& error) | ||
| { | ||
| LOCK(cs_wallet); | ||
|
|
||
| // Do not do anything to HD wallets | ||
| if (IsHDEnabled()) { | ||
| error = Untranslated("Cannot upgrade a wallet to HD if it is already upgraded to HD."); | ||
| return false; | ||
| } | ||
|
|
||
| if (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { | ||
| error = Untranslated("Private keys are disabled for this wallet"); | ||
| return false; | ||
| } | ||
|
|
||
| WalletLogPrintf("Upgrading wallet to HD\n"); | ||
| SetMinVersion(FEATURE_HD); | ||
|
|
||
| auto spk_man = GetOrCreateLegacyScriptPubKeyMan(); | ||
| bool prev_encrypted = IsCrypted(); | ||
| if (prev_encrypted) { | ||
| if (!GenerateNewHDChainEncrypted(secureMnemonic, secureMnemonicPassphrase, secureWalletPassphrase)) { | ||
| error = Untranslated("Failed to generate encrypted HD wallet"); | ||
| return false; | ||
| } | ||
| } else { | ||
| spk_man->GenerateNewHDChain(secureMnemonic, secureMnemonicPassphrase); | ||
| } | ||
| return 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.
The GUI reserves
MAX_PASSPHRASE_SIZEfor the wallet.It's unlikely someone will actually come around with a password >100 characters but since we allow longer passwords in the GUI, we should have RPC behave in the same way.
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.
This call
reservehas been introduced before introducing SecureString (which use custom allocator).That call is needed to be sure that string is not accidentally re-allocated and it would be impossible to wipe a data by rewriting bytes.
This call is just for legacy and completely useless now. I'd better to consider removing
reservecompletely rather than keep it here.btw, that's not new code, I just moved it a bit; I will consider removing
reserveat all, not planning to refactor it in this PR