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

BIP32 @bitcoin GitHub

BIP39 @bitcon GitHub

SLIP44 @satoshilabs GitHub

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 library

    byte[] generateSeed(String mnemonic) {
      return new SeedCalculator().calculateSeed(mnemonic, "");
    }
    
  • using bitcoinj Java library

    byte[] 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 and BIP39 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);
}

NovaCrypto's BIP39
bitcoinj

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);
  }
}

results matching ""

    No results matching ""