HD Wallet
BIP32(Bitcoin Improvement Proposals) defines the standard for hierarchical deterministic wallets(HD Wallets). BIP39 lists the words of valid mnemonic. BIP44 defines a logical hierarchy for deterministic wallets and purpose scheme. And SLIP44 (Satoshi Labs Improvement Proposals) lists the coin types, I found it when reading the source of go-ethereum.
- Path levels
m / purpose' / coin_type' / account' / change / address_index
The path of m/44'/60'/0'/0/0
is for Ethereum, and m/44'/1'/0'/0/0
is for testnet.
SLIP-44 https://github.com/satoshilabs/slips/blob/master/slip-0044.md assigns the `coin_type` 60' (or 0x8000003C) to Ethereum. The root path for Ethereum is m/44'/60'/0'/0 according to the specification from https://github.com/ethereum/EIPs/issues/84, albeit it's not set in stone yet whether accounts should increment the last component or the children of that. We will go with the simpler approach of incrementing the last component. @go-ethereum/accounts/hd.go
Generate Mnemonic
Using NovaCrypto's BIP39
Java library.
@NonNull
static String generateMnemonicString() {
final StringBuilder sb = new StringBuilder();
final byte[] entropy = new byte[Words.TWELVE.byteLength()];
new SecureRandom()
.nextBytes(entropy);
new MnemonicGenerator(English.INSTANCE)
.createMnemonic(entropy, sb::append);
return sb.toString();
}
Check mnemonic
- using NovaCrypto's
BIP39
library
private void checkMnemonic(@NonNull String mnemonic) {
try {
MnemonicValidator
.ofWordList(English.INSTANCE)
.validate(mnemonic);
} catch (Exception e) {
throw new RuntimeException("invalid mnemonic");
}
}
- using
bitcoinj
library
private void checkMnemonic(@NonNull String mnemonic) {
try {
new MnemonicCode(inputStream, words)
.check(mnemonic);
} catch(Exception e) {
throw new RuntimeException("invalid mnemonic");
}
}
We also can use Mnemonic Code Converter web side to check our mnemonic.
Convert mnemonic to seed
using NovaCrypto's
BIP39
librarybyte[] generateSeed(String mnemonic) { return new SeedCalculator().calculateSeed(mnemonic, ""); }
using
bitcoinj
Java librarybyte[] generateSeed(String mnemonic) { return new MnemonicCode(inputStream, words) .toSeed(mnemonic, ""); }
I wonder to the password of mnemonic is necessary or not. Then I found the answer from BIP32 document.
A user may decide to protect their mnemonic with a passphrase. If a passphrase is not present, an empty string "" is used instead. @BIP32
I notice both NovaCrypto's BIP39
and bitconj
libraries generate the seed with salt, and that salt is the string of 'mnemonic' + passphrase
. I don't know why they both use 'mnemonic'
. Then I also found the answer from BIP32 document.
NovaCrypto's
BIP39
public final class SeedCalculator { private final byte[] fixedSalt = getUtf8Bytes("mnemonic"); byte[] calculateSeed(final char[] mnemonicChars, final String passphrase) { ... final byte[] salt = combine(fixedSalt, salt2); return encoded; } }
bitcoinj
public static byte[] toSeed(List<String> words, String passphrase) { checkNotNull(passphrase, "A null passphrase is not allowed."); ... String salt = "mnemonic" + passphrase; ... return seed; }
To create a binary seed from the mnemonic, we use the PBKDF2 function with a mnemonic sentence (in UTF-8 NFKD) used as the password and the string "mnemonic" + passphrase (again in UTF-8 NFKD) used as the salt. The iteration count is set to 2048 and HMAC-SHA512 is used as the pseudo-random function. The length of the derived key is 512 bits (= 64 bytes). @BIP32
Generate master private key from seed
- using
bitcoinj
only
MnemonicCode.INSTANCE = new MnemonicCode(inputStream, words);
private byte[] toRawPrivateKey(@NonNull String mnemonic) throws UnreadableWalletException {
checkMnemonic(mnemonic);
return DeterministicKeyChain.builder()
.seed(new DeterministicSeed(mnemonic, null, "", System.currentTimeMillis()))
.build()
.getKeyByPath(ETH_ZERO_KEY_PATH, true)
.getPrivKeyBytes();
}
// m/44'/60'/0'/0/0
private static final ImmutableList<ChildNumber> ETH_ZERO_KEY_PATH = ImmutableList.of(
new ChildNumber(44, true),
new ChildNumber(60, true),
ChildNumber.ZERO_HARDENED,
ChildNumber.ZERO,
ChildNumber.ZERO);
- mix
bitconj
andBIP39
libraries
private byte[] toRawPrivateKey(@NonNull String mnemonic) throws UnreadableWalletException {
checkMnemonic(mnemonic);
return DeterministicKeyChain.builder()
.seed(new DeterministicSeed(mnemonic, generateSeed(mnemonic), "", System.currentTimeMillis()))
.build()
.getKeyByPath(ETH_ZERO_KEY_PATH, true)
.getPrivKeyBytes();
}
private byte[] generateSeed(String mnemonic) {
return new SeedCalculator().calculateSeed(mnemonic, "");
}
// m/44'/60'/0'/0/0
private static final ImmutableList<ChildNumber> ETH_ZERO_KEY_PATH = ImmutableList.of(
new ChildNumber(44, true),
new ChildNumber(60, true),
ChildNumber.ZERO_HARDENED,
ChildNumber.ZERO,
ChildNumber.ZERO);
}
Import private key to Keystore
- using
go-ethereum
library
@NonNull
public String saveToKeystore(@NonNull String mnemonic, @NonNull String password) {
try {
return keyStore.importECDSAKey(toRawPrivateKey(mnemonic), password)
.getAddress()
.getHex();
} catch (Exception e) {
throw new RuntimeException(e);
}
}