Skip to content

Commit d97ec35

Browse files
authored
feat(wallet): make mnemonic bits tweakable, default to 128 bit / 12 words (#5457)
## Issue being fixed or feature implemented Allow generating 12, 18, 24 words mnemonics. Default to 12 words as it's the most popular option/de-facto a standard now imo. ## What was done? Add `-mnemonicbits` option, add tests ## How Has This Been Tested? run tests, play with wallets on regtest ## Breaking Changes n/a, old wallets should not be affected ## Checklist: - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] 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 _(for repository code-owners and collaborators only)_
1 parent bfa585d commit d97ec35

File tree

6 files changed

+66
-13
lines changed

6 files changed

+66
-13
lines changed

src/bip39.cpp

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ bool CMnemonic::Check(SecureString mnemonic)
9191
}
9292
nWordCount++;
9393
// check number of words
94-
if (nWordCount != 12 && nWordCount != 18 && nWordCount != 24) {
94+
if (nWordCount % 3 != 0 || nWordCount < 12 || nWordCount > 24) {
9595
return false;
9696
}
9797

@@ -133,18 +133,10 @@ bool CMnemonic::Check(SecureString mnemonic)
133133
bits[32] = bits[nWordCount * 4 / 3];
134134
CSHA256().Write(bits.data(), nWordCount * 4 / 3).Finalize(bits.data());
135135

136-
bool fResult = 0;
137-
if (nWordCount == 12) {
138-
fResult = (bits[0] & 0xF0) == (bits[32] & 0xF0); // compare first 4 bits
139-
} else
140-
if (nWordCount == 18) {
141-
fResult = (bits[0] & 0xFC) == (bits[32] & 0xFC); // compare first 6 bits
142-
} else
143-
if (nWordCount == 24) {
144-
fResult = bits[0] == bits[32]; // compare 8 bits
145-
}
136+
const char checksum_length = nWordCount / 3;
137+
const char mask = (2 ^ checksum_length) << (8 - checksum_length);
146138

147-
return fResult;
139+
return (bits[0] & mask) == (bits[32] & mask);
148140
}
149141

150142
// passphrase must be at most 256 characters otherwise it would be truncated

src/hdchain.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ bool CHDChain::SetMnemonic(const SecureString& ssMnemonic, const SecureString& s
8888

8989
// empty mnemonic i.e. "generate a new one"
9090
if (ssMnemonic.empty()) {
91-
ssMnemonicTmp = CMnemonic::Generate(256);
91+
ssMnemonicTmp = CMnemonic::Generate(gArgs.GetArg("-mnemonicbits", DEFAULT_MNEMONIC_BITS));
9292
}
9393
// NOTE: default mnemonic passphrase is an empty string
9494

src/hdchain.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ class CHDChain
4242
std::map<uint32_t, CHDAccount> GUARDED_BY(cs) mapAccounts;
4343

4444
public:
45+
/** Default for -mnemonicbits */
46+
static constexpr int DEFAULT_MNEMONIC_BITS = 128; // 128 bits == 12 words
4547

4648
CHDChain() = default;
4749
CHDChain(const CHDChain& other) :

src/wallet/init.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
#include <wallet/wallet.h>
2525
#include <walletinitinterface.h>
2626

27+
#include <bip39.h>
2728
#include <coinjoin/client.h>
2829
#include <coinjoin/options.h>
30+
#include <hdchain.h>
2931

3032
class WalletInit : public WalletInitInterface
3133
{
@@ -85,6 +87,7 @@ void WalletInit::AddWalletOptions(ArgsManager& argsman) const
8587

8688
argsman.AddArg("-hdseed=<hex>", "User defined seed for HD wallet (should be in hex). Only has effect during wallet creation/first start (default: randomly generated)", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::WALLET_HD);
8789
argsman.AddArg("-mnemonic=<text>", "User defined mnemonic for HD wallet (bip39). Only has effect during wallet creation/first start (default: randomly generated)", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::WALLET_HD);
90+
argsman.AddArg("-mnemonicbits=<n>", strprintf("User defined mnemonic security for HD wallet in bits (BIP39). Only has effect during wallet creation/first start (allowed values: 128, 160, 192, 224, 256; default: %u)", CHDChain::DEFAULT_MNEMONIC_BITS), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET_HD);
8891
argsman.AddArg("-mnemonicpassphrase=<text>", "User defined mnemonic passphrase for HD wallet (BIP39). Only has effect during wallet creation/first start (default: empty string)", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::WALLET_HD);
8992
argsman.AddArg("-usehd", strprintf("Use hierarchical deterministic key generation (HD) after BIP39/BIP44. Only has effect during wallet creation/first start (default: %u)", DEFAULT_USE_HD_WALLET), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET_HD);
9093

@@ -168,6 +171,10 @@ bool WalletInit::ParameterInteraction() const
168171
return InitError(strprintf(_("%s can't be lower than %s"), "-coinjoindenomshardcap", "-coinjoindenomsgoal"));
169172
}
170173

174+
if (CMnemonic::Generate(gArgs.GetArg("-mnemonicbits", CHDChain::DEFAULT_MNEMONIC_BITS)) == SecureString()) {
175+
return InitError(strprintf(_("Invalid '%s'. Allowed values: 128, 160, 192, 224, 256."), "-mnemonicbits"));
176+
}
177+
171178
return true;
172179
}
173180

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@
213213
'wallet_import_rescan.py',
214214
'wallet_import_with_label.py',
215215
'wallet_upgradewallet.py',
216+
'wallet_mnemonicbits.py',
216217
'rpc_bind.py --ipv4',
217218
'rpc_bind.py --ipv6',
218219
'rpc_bind.py --nonloopback',
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2023 The Dash Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Test -mnemonicbits wallet option."""
6+
7+
from test_framework.test_framework import BitcoinTestFramework
8+
from test_framework.util import (
9+
assert_equal,
10+
)
11+
12+
class WalletMnemonicbitsTest(BitcoinTestFramework):
13+
def set_test_params(self):
14+
self.setup_clean_chain = True
15+
self.num_nodes = 1
16+
self.extra_args = [['-usehd=1']]
17+
18+
def skip_test_if_missing_module(self):
19+
self.skip_if_no_wallet()
20+
21+
def run_test(self):
22+
self.log.info("Test -mnemonicbits")
23+
24+
self.log.info("Invalid -mnemonicbits should rise an error")
25+
self.stop_node(0)
26+
self.nodes[0].assert_start_raises_init_error(self.extra_args[0] + ['-mnemonicbits=123'], "Error: Invalid '-mnemonicbits'. Allowed values: 128, 160, 192, 224, 256.")
27+
self.start_node(0)
28+
assert_equal(len(self.nodes[0].dumphdinfo()["mnemonic"].split()), 12) # 12 words by default
29+
30+
self.log.info("Can have multiple wallets with different mnemonic length loaded at the same time")
31+
self.restart_node(0, extra_args=self.extra_args[0] + ["-mnemonicbits=160"])
32+
self.nodes[0].createwallet("wallet_160")
33+
self.restart_node(0, extra_args=self.extra_args[0] + ["-mnemonicbits=192"])
34+
self.nodes[0].createwallet("wallet_192")
35+
self.restart_node(0, extra_args=self.extra_args[0] + ["-mnemonicbits=224"])
36+
self.nodes[0].createwallet("wallet_224")
37+
self.restart_node(0, extra_args=self.extra_args[0] + ["-mnemonicbits=256"])
38+
self.nodes[0].loadwallet("wallet_160")
39+
self.nodes[0].loadwallet("wallet_192")
40+
self.nodes[0].loadwallet("wallet_224")
41+
self.nodes[0].createwallet("wallet_256", False, True) # blank
42+
self.nodes[0].get_wallet_rpc("wallet_256").upgradetohd()
43+
assert_equal(len(self.nodes[0].get_wallet_rpc(self.default_wallet_name).dumphdinfo()["mnemonic"].split()), 12) # 12 words by default
44+
assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_160").dumphdinfo()["mnemonic"].split()), 15) # 15 words
45+
assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_192").dumphdinfo()["mnemonic"].split()), 18) # 18 words
46+
assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_224").dumphdinfo()["mnemonic"].split()), 21) # 21 words
47+
assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_256").dumphdinfo()["mnemonic"].split()), 24) # 24 words
48+
49+
50+
if __name__ == '__main__':
51+
WalletMnemonicbitsTest().main ()

0 commit comments

Comments
 (0)