The Generation of Mnemonics

Mnemonic

We know that in MetaMask, when you generate a new account, you will get a set of mnemonics.

This article will explain the process of generating mnemonics.

Let’s see the code first.

function genMnemonic() {
    const randomWallet = ethers.Wallet.createRandom();
    const mnemonic = randomWallet.mnemonic.phrase;
    console.log(mnemonic);
    return mnemonic;
}

static createRandom(provider?: null | Provider): HDNodeWallet {
    const wallet = HDNodeWallet.createRandom();
    if (provider) { return wallet.connect(provider); }
    return wallet;
}

static createRandom(password?: string, path?: string, wordlist?: Wordlist): HDNodeWallet {
    if (password == null) { password = ""; }
    if (path == null) { path = defaultPath; }
    if (wordlist == null) { wordlist = LangEn.wordlist(); }
    const mnemonic = Mnemonic.fromEntropy(randomBytes(16), password, wordlist)
    return HDNodeWallet.#fromSeed(mnemonic.computeSeed(), mnemonic).derivePath(path);
 }
 
static fromEntropy(_entropy: BytesLike, password?: null | string, wordlist?: null | Wordlist): Mnemonic {
    const entropy = getBytes(_entropy, "entropy");
    const phrase = entropyToMnemonic(entropy, wordlist);
    return new Mnemonic(_guard, hexlify(entropy), phrase, password, wordlist);
}

function entropyToMnemonic(entropy: Uint8Array, wordlist?: null | Wordlist): string {

    assertArgument((entropy.length % 4) === 0 && entropy.length >= 16 && entropy.length <= 32,
        "invalid entropy size", "entropy", "[ REDACTED ]");

    if (wordlist == null) { wordlist = LangEn.wordlist(); }

    const indices: Array<number> = [ 0 ];

    let remainingBits = 11;
    for (let i = 0; i < entropy.length; i++) {

        // Consume the whole byte (with still more to go)
        if (remainingBits > 8) {
            indices[indices.length - 1] <<= 8;
            indices[indices.length - 1] |= entropy[i];

            remainingBits -= 8;

        // This byte will complete an 11-bit index
        } else {
            indices[indices.length - 1] <<= remainingBits;
            indices[indices.length - 1] |= entropy[i] >> (8 - remainingBits);

            // Start the next word
            indices.push(entropy[i] & getLowerMask(8 - remainingBits));

            remainingBits += 3;
        }
    }

    // Compute the checksum bits
    const checksumBits = entropy.length / 4;
    const checksum = parseInt(sha256(entropy).substring(2, 4), 16) & getUpperMask(checksumBits);

    // Shift the checksum into the word indices
    indices[indices.length - 1] <<= checksumBits;
    indices[indices.length - 1] |= (checksum >> (8 - checksumBits));

    return wordlist.join(indices.map((index) => (<Wordlist>wordlist).getWord(index)));
}

May be you will find the code is too long to understand. Let’s split it.

Before splitting. You should first know the basic steps of generating mnemonics

  1. generate random 16 bytes array

  2. split 16 bytes array to 11 bits array

  3. map every 11 bits data to a word

What is mnemonics

Mnemonics is s a set of easy words to help user to remember their account.

There are 2048 words in all mnemonics datasource, and each account has 12/24 mnemonics.

So if we want to map a number to a mnemonics, this number should be a 11 bit data. Cause 11 bit data can get 2048 different numbers.

post image

Generate random data

So, every mnemonic should be represented by 11 bits

The code use randomBytes(16) to generate a uint8array.

You will get 16 bytes, every bytes has 8 bit. So you will get totally 16 * 8 = 128 bits.

Well, now you can slip 128 bits to 11 * 11 bits. But we need 12 mnemonics, there are only 11 words.

We need 132 bits to split to 12 mnemonics(132 / 11 = 12). We need 4 more bits to fill it.

That’s the checksum, every 32 bits should have a bit of checksum.

Split bytes

post image

This picture explain how to change the random bytes into the list of mnemonics.

Actually the function entropyToMnemonic is the implemented code.

function entropyToMnemonic(entropy: Uint8Array, wordlist?: null | Wordlist): string {
    assertArgument((entropy.length % 4) === 0 && entropy.length >= 16 && entropy.length <= 32,
        "invalid entropy size", "entropy", "[ REDACTED ]");

    if (wordlist == null) { wordlist = LangEn.wordlist(); }

    const indices: Array<number> = [ 0 ];
        // every mnemonic need 11 bits to map word
    let remainingBits = 11;
    for (let i = 0; i < entropy.length; i++) {
        // 
        if (remainingBits > 8) {
            indices[indices.length - 1] <<= 8;
            indices[indices.length - 1] |= entropy[i];

            remainingBits -= 8;

        // This byte will complete an 11-bit index
        } else {
            indices[indices.length - 1] <<= remainingBits;
            indices[indices.length - 1] |= entropy[i] >> (8 - remainingBits);

            // Start the next word
            indices.push(entropy[i] & getLowerMask(8 - remainingBits));

            remainingBits += 3;
        }
    }

    // Compute the checksum bits
    const checksumBits = entropy.length / 4;
    const checksum = parseInt(sha256(entropy).substring(2, 4), 16) & getUpperMask(checksumBits);

    // Shift the checksum into the word indices
    indices[indices.length - 1] <<= checksumBits;
    indices[indices.length - 1] |= (checksum >> (8 - checksumBits));

    return wordlist.join(indices.map((index) => (<Wordlist>wordlist).getWord(index)));
}

indices[indices.length - 1] <<= 8; let the data left shift 8 bits to allow the next byte(8bit) to have a position to insert.

indices[indices.length - 1] |= entropy[i] >> (8 - remainingBits); insert the remained bits of a byte.

remainingBits += 3; seems confusing, we can derive this formula.

nextRemainingBits = 11 - (8 - lastRemainingBits)

So,

nextRemainingBits = 11 - 8 + lastRemainingBits = 3 + lastRemainingBits

CheckSum

indices[indices.length - 1] <<= checksumBits; indices[indices.length - 1] |= (checksum >> (8 - checksumBits));

Finally, when the randomBytes has been split, there are still 4 bits(16/4) need to fill.

It is filled by the high 4 bits of checksum.

Checksum just need the first 8 bits of sha256(entropy).

sha256(entropy).substring(2, 4) may be confusing. Actually sha256 return a string like this “0x1a2b3c”, we need to exclude 0x, so we need the substring(2,4).

And then, we will get the high CheckSumbits of Checksum, then add it into the low CheckSumBits bit of finally mnemonic