Skip to content

Commit 8dba655

Browse files
knstUdjinM6
andauthored
feat: enable HD wallets by default (dashpay#5807)
## Issue being fixed or feature implemented HD wallets are old-existsing feature, appeared in Dash years ago, but enabling HD wallets is not trivial task that requires multiple steps and command line/rpc calls. Let's have them enabled by default. ## What was done? - HD wallets are enabled by default. Currently behavior `dashd`, `dash-qt` are similar to run with option `-usehd=1` - the rpc `upgradewallet` do not let to upgrade from non-HD wallet to HD wallet to don't encourage user use non-crypted wallets (postponed till v21) - the initialization of ScriptPubKey is updated to be sure that encypted HD seed is never written on disk (if passphrase is provided) - enabled and dashified a script `wallet_upgradewallet.py` which test compatibility between different versions of wallet ## What is not done? - wallet tool still does not support passhprase, HD seed can appear on disk - there's no dialog that show user a mnemonic phrase and encourage him to make a paper backup Before removing a command line 'usehd' (backport bitcoin#11250) need to make at least one major release for fail-over option (if someone wish to use non-HD wallets only). ## How Has This Been Tested? Run unit and functional tests. Enabled new functional test `wallet_upgradewallet.py` that has been backported long time ago but waited this PR to be enabled. ## Breaking Changes HD wallets are created by default. ## Checklist: - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone --------- Co-authored-by: UdjinM6 <[email protected]>
1 parent 19681d0 commit 8dba655

File tree

10 files changed

+154
-139
lines changed

10 files changed

+154
-139
lines changed

doc/release-notes-5807.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Notable changes
2+
3+
## HD Wallets Enabled by Default
4+
5+
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.
6+
7+
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.

src/qt/forms/createwalletdialog.ui

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
<string>Encrypt Wallet</string>
5151
</property>
5252
<property name="checked">
53-
<bool>false</bool>
53+
<bool>true</bool>
5454
</property>
5555
</widget>
5656
</item>

src/wallet/rpcwallet.cpp

Lines changed: 37 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2681,76 +2681,53 @@ static UniValue upgradetohd(const JSONRPCRequest& request)
26812681
},
26822682
}.Check(request);
26832683

2684-
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
2685-
if (!wallet) return NullUniValue;
2686-
CWallet* const pwallet = wallet.get();
2687-
LegacyScriptPubKeyMan* spk_man = pwallet->GetLegacyScriptPubKeyMan();
2688-
if (!spk_man) {
2689-
throw JSONRPCError(RPC_WALLET_ERROR, "This type of wallet does not support this command");
2690-
}
2684+
std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
2685+
if (!pwallet) return NullUniValue;
26912686

26922687
bool generate_mnemonic = request.params[0].isNull() || request.params[0].get_str().empty();
2693-
2694-
{
2695-
LOCK(pwallet->cs_wallet);
2696-
2697-
// Do not do anything to HD wallets
2698-
if (pwallet->IsHDEnabled()) {
2699-
throw JSONRPCError(RPC_WALLET_ERROR, "Cannot upgrade a wallet to HD if it is already upgraded to HD.");
2700-
}
2701-
2702-
if (!pwallet->SetMaxVersion(FEATURE_HD)) {
2703-
throw JSONRPCError(RPC_WALLET_ERROR, "Cannot downgrade wallet");
2688+
SecureString secureWalletPassphrase;
2689+
secureWalletPassphrase.reserve(100);
2690+
// TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string)
2691+
// Alternately, find a way to make request.params[0] mlock()'d to begin with.
2692+
if (!request.params[2].isNull()) {
2693+
secureWalletPassphrase = request.params[2].get_str().c_str();
2694+
if (!pwallet->Unlock(secureWalletPassphrase)) {
2695+
throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "The wallet passphrase entered was incorrect");
27042696
}
2697+
}
27052698

2706-
if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
2707-
throw JSONRPCError(RPC_WALLET_ERROR, "Error: Private keys are disabled for this wallet");
2708-
}
2699+
EnsureWalletIsUnlocked(pwallet.get());
27092700

2710-
bool prev_encrypted = pwallet->IsCrypted();
2701+
SecureString secureMnemonic;
2702+
secureMnemonic.reserve(256);
2703+
if (!generate_mnemonic) {
2704+
secureMnemonic = request.params[0].get_str().c_str();
2705+
}
27112706

2712-
SecureString secureWalletPassphrase;
2713-
secureWalletPassphrase.reserve(100);
2714-
// TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string)
2715-
// Alternately, find a way to make request.params[0] mlock()'d to begin with.
2716-
if (request.params[2].isNull()) {
2717-
if (prev_encrypted) {
2718-
throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Cannot upgrade encrypted wallet to HD without the wallet passphrase");
2719-
}
2720-
} else {
2721-
secureWalletPassphrase = request.params[2].get_str().c_str();
2722-
if (!pwallet->Unlock(secureWalletPassphrase)) {
2723-
throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "The wallet passphrase entered was incorrect");
2724-
}
2725-
}
2707+
SecureString secureMnemonicPassphrase;
2708+
secureMnemonicPassphrase.reserve(256);
2709+
if (!request.params[1].isNull()) {
2710+
secureMnemonicPassphrase = request.params[1].get_str().c_str();
2711+
}
2712+
// TODO: breaking changes kept for v21!
2713+
// instead upgradetohd let's use more straightforward 'sethdseed'
2714+
constexpr bool is_v21 = false;
2715+
const int previous_version{pwallet->GetVersion()};
2716+
if (is_v21 && previous_version >= FEATURE_HD) {
2717+
return JSONRPCError(RPC_WALLET_ERROR, "Already at latest version. Wallet version unchanged.");
2718+
}
27262719

2727-
SecureString secureMnemonic;
2728-
secureMnemonic.reserve(256);
2729-
if (!generate_mnemonic) {
2730-
secureMnemonic = request.params[0].get_str().c_str();
2731-
}
2720+
bilingual_str error;
2721+
const bool wallet_upgraded{pwallet->UpgradeToHD(secureMnemonic, secureMnemonicPassphrase, secureWalletPassphrase, error)};
27322722

2733-
SecureString secureMnemonicPassphrase;
2734-
secureMnemonicPassphrase.reserve(256);
2735-
if (!request.params[1].isNull()) {
2736-
secureMnemonicPassphrase = request.params[1].get_str().c_str();
2723+
if (!secureWalletPassphrase.empty() && !pwallet->IsCrypted()) {
2724+
if (!pwallet->EncryptWallet(secureWalletPassphrase)) {
2725+
throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Failed to encrypt HD wallet");
27372726
}
2727+
}
27382728

2739-
pwallet->WalletLogPrintf("Upgrading wallet to HD\n");
2740-
pwallet->SetMinVersion(FEATURE_HD);
2741-
2742-
if (prev_encrypted) {
2743-
if (!pwallet->GenerateNewHDChainEncrypted(secureMnemonic, secureMnemonicPassphrase, secureWalletPassphrase)) {
2744-
throw JSONRPCError(RPC_WALLET_ERROR, "Failed to generate encrypted HD wallet");
2745-
}
2746-
} else {
2747-
spk_man->GenerateNewHDChain(secureMnemonic, secureMnemonicPassphrase);
2748-
if (!secureWalletPassphrase.empty()) {
2749-
if (!pwallet->EncryptWallet(secureWalletPassphrase)) {
2750-
throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Failed to encrypt HD wallet");
2751-
}
2752-
}
2753-
}
2729+
if (!wallet_upgraded) {
2730+
throw JSONRPCError(RPC_WALLET_ERROR, error.original);
27542731
}
27552732

27562733
// 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

src/wallet/wallet.cpp

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ std::shared_ptr<CWallet> CreateWallet(interfaces::Chain& chain, interfaces::Coin
297297
status = DatabaseStatus::FAILED_CREATE;
298298
return nullptr;
299299
}
300+
if (gArgs.GetBoolArg("-usehd", DEFAULT_USE_HD_WALLET)) {
301+
wallet->WalletLogPrintf("Set HD by default\n");
302+
wallet->SetMinVersion(FEATURE_HD);
303+
}
300304

301305
// Encrypt the wallet
302306
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
306310
return nullptr;
307311
}
308312
if (!create_blank) {
313+
{
314+
// TODO: drop this condition after removing option to create non-HD wallets
315+
// related backport bitcoin#11250
316+
if (wallet->GetVersion() >= FEATURE_HD) {
317+
if (!wallet->GenerateNewHDChainEncrypted(/*secureMnemonic=*/"", /*secureMnemonicPassphrase=*/"", passphrase)) {
318+
error = Untranslated("Error: Failed to generate encrypted HD wallet");
319+
status = DatabaseStatus::FAILED_CREATE;
320+
return nullptr;
321+
}
322+
}
323+
}
324+
309325
// Unlock the wallet
310326
if (!wallet->Unlock(passphrase)) {
311327
error = Untranslated("Error: Wallet was encrypted but could not be unlocked");
312328
status = DatabaseStatus::FAILED_ENCRYPT;
313329
return nullptr;
314330
}
315331

316-
// Set a HD chain for the wallet
317-
// TODO: re-enable this and `keypoolsize_hd_internal` check in `wallet_createwallet.py`
318-
// when HD is the default mode (make sure this actually works!)...
319-
// if (auto spk_man = wallet->GetLegacyScriptPubKeyMan() {
320-
// if (!spk_man->GenerateNewHDChainEncrypted("", "", passphrase)) {
321-
// throw JSONRPCError(RPC_WALLET_ERROR, "Failed to generate encrypted HD wallet");
322-
// }
323-
// }
324-
// ... and drop this
325-
LOCK(wallet->cs_wallet);
326-
wallet->UnsetWalletFlag(WALLET_FLAG_BLANK_WALLET);
327-
if (auto spk_man = wallet->GetLegacyScriptPubKeyMan()) {
328-
spk_man->NewKeyPool();
329-
}
330-
// end TODO
331-
332332
// backup the wallet we just encrypted
333333
if (!wallet->AutoBackupWallet("", error, warnings) && !error.original.empty()) {
334334
status = DatabaseStatus::FAILED_ENCRYPT;
@@ -4617,6 +4617,10 @@ std::shared_ptr<CWallet> CWallet::Create(interfaces::Chain& chain, interfaces::C
46174617
if (gArgs.GetBoolArg("-usehd", DEFAULT_USE_HD_WALLET) && !walletInstance->IsHDEnabled()) {
46184618
std::string strSeed = gArgs.GetArg("-hdseed", "not hex");
46194619

4620+
// ensure this wallet.dat can only be opened by clients supporting HD
4621+
walletInstance->WalletLogPrintf("Upgrading wallet to HD\n");
4622+
walletInstance->SetMinVersion(FEATURE_HD);
4623+
46204624
if (gArgs.IsArgSet("-hdseed") && IsHex(strSeed)) {
46214625
CHDChain newHdChain;
46224626
std::vector<unsigned char> vchSeed = ParseHex(strSeed);
@@ -4646,10 +4650,6 @@ std::shared_ptr<CWallet> CWallet::Create(interfaces::Chain& chain, interfaces::C
46464650
}
46474651
}
46484652

4649-
// ensure this wallet.dat can only be opened by clients supporting HD
4650-
walletInstance->WalletLogPrintf("Upgrading wallet to HD\n");
4651-
walletInstance->SetMinVersion(FEATURE_HD);
4652-
46534653
// clean up
46544654
gArgs.ForceRemoveArg("hdseed");
46554655
gArgs.ForceRemoveArg("mnemonic");
@@ -4913,7 +4913,45 @@ bool CWallet::UpgradeWallet(int version, bilingual_str& error)
49134913
return false;
49144914
}
49154915

4916+
// TODO: consider discourage users to skip passphrase for HD wallets for v21
4917+
if (false && nMaxVersion >= FEATURE_HD && !IsHDEnabled()) {
4918+
error = Untranslated("You should use upgradetohd RPC to upgrade non-HD wallet to HD");
4919+
return false;
4920+
}
4921+
49164922
SetMaxVersion(nMaxVersion);
4923+
4924+
return true;
4925+
}
4926+
4927+
bool CWallet::UpgradeToHD(const SecureString& secureMnemonic, const SecureString& secureMnemonicPassphrase, const SecureString& secureWalletPassphrase, bilingual_str& error)
4928+
{
4929+
LOCK(cs_wallet);
4930+
4931+
// Do not do anything to HD wallets
4932+
if (IsHDEnabled()) {
4933+
error = Untranslated("Cannot upgrade a wallet to HD if it is already upgraded to HD.");
4934+
return false;
4935+
}
4936+
4937+
if (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
4938+
error = Untranslated("Private keys are disabled for this wallet");
4939+
return false;
4940+
}
4941+
4942+
WalletLogPrintf("Upgrading wallet to HD\n");
4943+
SetMinVersion(FEATURE_HD);
4944+
4945+
auto spk_man = GetOrCreateLegacyScriptPubKeyMan();
4946+
bool prev_encrypted = IsCrypted();
4947+
if (prev_encrypted) {
4948+
if (!GenerateNewHDChainEncrypted(secureMnemonic, secureMnemonicPassphrase, secureWalletPassphrase)) {
4949+
error = Untranslated("Failed to generate encrypted HD wallet");
4950+
return false;
4951+
}
4952+
} else {
4953+
spk_man->GenerateNewHDChain(secureMnemonic, secureMnemonicPassphrase);
4954+
}
49174955
return true;
49184956
}
49194957

src/wallet/wallet.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ static const CAmount HIGH_TX_FEE_PER_KB = COIN / 100;
9191
static const CAmount HIGH_MAX_TX_FEE = 100 * HIGH_TX_FEE_PER_KB;
9292

9393
//! if set, all keys will be derived by using BIP39/BIP44
94-
static const bool DEFAULT_USE_HD_WALLET = false;
94+
static const bool DEFAULT_USE_HD_WALLET = true;
9595

9696
class CCoinControl;
9797
class CKey;
@@ -1279,6 +1279,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
12791279
/* Returns true if HD is enabled */
12801280
bool IsHDEnabled() const;
12811281

1282+
// TODO: move it to scriptpubkeyman
12821283
/* Generates a new HD chain */
12831284
bool GenerateNewHDChainEncrypted(const SecureString& secureMnemonic, const SecureString& secureMnemonicPassphrase, const SecureString& secureWalletPassphrase);
12841285

@@ -1331,6 +1332,9 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
13311332
/** Upgrade the wallet */
13321333
bool UpgradeWallet(int version, bilingual_str& error);
13331334

1335+
/** Upgrade non-HD wallet to HD wallet */
1336+
bool UpgradeToHD(const SecureString& secureMnemonic, const SecureString& secureMnemonicPassphrase, const SecureString& secureWalletPassphrase, bilingual_str& error);
1337+
13341338
//! Returns all unique ScriptPubKeyMans in m_internal_spk_managers and m_external_spk_managers
13351339
std::set<ScriptPubKeyMan*> GetActiveScriptPubKeyMans() const;
13361340

src/wallet/wallettool.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ static void WalletCreate(CWallet* wallet_instance)
2828

2929
// generate a new HD seed
3030
wallet_instance->SetupLegacyScriptPubKeyMan();
31-
// NOTE: we do not yet create HD wallets by default
32-
// auto spk_man = wallet_instance->GetLegacyScriptPubKeyMan();
33-
// spk_man->GenerateNewHDChain("", "");
31+
auto spk_man = wallet_instance->GetLegacyScriptPubKeyMan();
32+
// NOTE: drop this condition after removing option to create non-HD wallets
33+
if (spk_man->IsHDEnabled()) {
34+
spk_man->GenerateNewHDChain(/*secureMnemonic=*/"", /*secureMnemonicPassphrase=*/"");
35+
}
3436

3537
tfm::format(std::cout, "Topping up keypool...\n");
3638
wallet_instance->TopUpKeyPool();

test/functional/tool_wallet.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -97,21 +97,6 @@ def test_tool_wallet_info(self):
9797
# shasum_before = self.wallet_shasum()
9898
timestamp_before = self.wallet_timestamp()
9999
self.log.debug('Wallet file timestamp before calling info: {}'.format(timestamp_before))
100-
out = textwrap.dedent('''\
101-
Wallet info
102-
===========
103-
Encrypted: no
104-
HD (hd seed available): no
105-
Keypool Size: 1
106-
Transactions: 0
107-
Address Book: 1
108-
''')
109-
self.assert_tool_output(out, '-wallet=' + self.default_wallet_name, 'info')
110-
111-
self.start_node(0)
112-
self.nodes[0].upgradetohd()
113-
self.stop_node(0)
114-
115100
out = textwrap.dedent('''\
116101
Wallet info
117102
===========
@@ -122,6 +107,7 @@ def test_tool_wallet_info(self):
122107
Address Book: 1
123108
''')
124109
self.assert_tool_output(out, '-wallet=' + self.default_wallet_name, 'info')
110+
125111
timestamp_after = self.wallet_timestamp()
126112
self.log.debug('Wallet file timestamp after calling info: {}'.format(timestamp_after))
127113
self.log_wallet_timestamp_comparison(timestamp_before, timestamp_after)

test/functional/wallet_createwallet.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,7 @@ def run_test(self):
113113
# There should only be 1 key
114114
walletinfo = w6.getwalletinfo()
115115
assert_equal(walletinfo['keypoolsize'], 1)
116-
# TODO: re-enable this when HD is the default mode
117-
# assert_equal(walletinfo['keypoolsize_hd_internal'], 1)
118-
# end TODO
116+
assert_equal(walletinfo['keypoolsize_hd_internal'], 1)
119117
# Allow empty passphrase, but there should be a warning
120118
resp = self.nodes[0].createwallet(wallet_name='w7', disable_private_keys=False, blank=False, passphrase='')
121119
assert_equal(resp['warning'], 'Empty string given as passphrase, wallet will not be encrypted.')

test/functional/wallet_upgradetohd.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@
2121
class WalletUpgradeToHDTest(BitcoinTestFramework):
2222
def set_test_params(self):
2323
self.num_nodes = 1
24+
self.extra_args = [['-usehd=0']]
2425

2526
def skip_test_if_missing_module(self):
2627
self.skip_if_no_wallet()
2728

2829
def setup_network(self):
29-
self.add_nodes(self.num_nodes)
30+
self.add_nodes(self.num_nodes, self.extra_args)
3031
self.start_nodes()
3132
self.import_deterministic_coinbase_privkeys()
3233

@@ -69,7 +70,8 @@ def run_test(self):
6970
self.log.info("Should no longer be able to start it with HD disabled")
7071
self.stop_node(0)
7172
node.assert_start_raises_init_error(['-usehd=0'], "Error: Error loading %s: You can't disable HD on an already existing HD wallet" % self.default_wallet_name)
72-
self.start_node(0)
73+
self.extra_args = []
74+
self.start_node(0, [])
7375
balance_after = node.getbalance()
7476

7577
self.recover_non_hd()
@@ -188,7 +190,7 @@ def run_test(self):
188190
node.stop()
189191
node.wait_until_stopped()
190192
self.start_node(0, extra_args=['-rescan'])
191-
assert_raises_rpc_error(-14, "Cannot upgrade encrypted wallet to HD without the wallet passphrase", node.upgradetohd, mnemonic)
193+
assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", node.upgradetohd, mnemonic)
192194
assert_raises_rpc_error(-14, "The wallet passphrase entered was incorrect", node.upgradetohd, mnemonic, "", "wrongpass")
193195
assert node.upgradetohd(mnemonic, "", walletpass)
194196
assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", node.dumphdinfo)

0 commit comments

Comments
 (0)