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
generate random 16 bytes array
split 16 bytes array to 11 bits array
map every 11 bits data to a word
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.

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.

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