Sitemap

How to Convert Mnemonic to ETH Address in Rust?

7 min readMay 31, 2024

Intro

How did you start your journey to Ether? Install the Metamask plugin, create a set of mnemonics, copy it on a piece of paper and click the confirm button! Congratulations you have your Ether account, you can copy your address and send him to someone else who can use it to receive Ether.

But do you have questions like below:

  1. Why mnemonic is important? Why can’t I give away my mnemonic?
  2. Why I will lost all my asset in different Account If I lose my mnemonic?
  3. Can I create my own menemonic by dictionary?
  4. How those 12 mnemonics became your private key, your address?

Congratulations you have come to the right place, this article will take you step by step to turn your set of mnemonics into an address using the Rust language.

Why Rust?

Rust is a general-purpose, compiled programming language developed by Mozilla, which is increasingly popular among developers for its reliability and efficiency. Many well-known projects based on Rust have also emerged in the ethereum ecosystem, such as:
Foundry — a blazing fast, portable and modular toolkit for Ethereum application development.
Reth — Modular, contributor-friendly and blazing-fast implementation of the Ethereum protocol.
As a result, I believe that more and more projects in the Ethernet ecosystem will be refactored and upgraded with Rust in the future. Let’s get acquainted with Rust in Ethereum soon!

Not a developer?

It doesn’t matter! This article also provide a detailed explanation and no code examples to help you understand the whole process.

Step 1 — Generate Mnemonic

Mnemonic is a list of words, which is easy to remember and write. And do you think that mnemonic is generated randomly? Hesitate, it is not completely random. Let me explain.

Each word in the mnemonic can be represented by a number from 0 to 2047, total 2048 numbers. Such as “indoor” is 920, “dish” is 505, “abandon” is 0. You can get more information from the BIP39 wordlist. Another interesting thing is we can not only have english mnemonic we can also have the Japanese, Korean, Spanish, Chinese (Simplified), Chinese (Traditional) versions. For example:

English: indoor dish desk flag debris potato excuse depart ticket judge file exit
Korean: 수집 몸속 명의 분야 만족 인격 법원 멀리 터미널 시멘트 부작용 변명
ChineseSimplified: 诗 失 圆 块 亲 幼 杂 却 厉 齐 顶 互
ChineseTraditional: 詩 失 圓 塊 親 幼 雜 卻 厲 齊 頂 互

And then we can convert the mnemonic to a binary number, such as “indoor” is 920, the binary number is 1110011000. Each of them is 11 bits.

Mnemonic: indoor dish desk flag debris potato excuse depart ticket judge file exit
MnemonicBinary: 01110011000 00111111001 00111011111 01011000001 00111000011 10101000110 01001111000 00111010110 11100001101 01111000101 01010110001 01001111111
Entropy: 01110011000 00111111001 00111011111 01011000001 00111000011 10101000110 01001111000 00111010110 11100001101 01111000101 01010110001 0100111
EntropyInHex: 7307e4efac13875193c1d6e1af1558a7
CheckSum: 1111
lengthOfEntropy: 128
lengthOfMnemonicBinary: 132

mnemonic = entropy + checksum
checksum = SHA256(entropy)[0:len(entropy)/32]=SHA256(entropy)[0:4]=

In other words we can randomly generate a string of 01 combinations as mnemonic’s material which called entropy. And the length of the entropy is a multiple of 32 bit. In 12 words mnemonic case, the length of entropy is 128.

Also that means the length of the mnemonic is a multiple of 3, but maximum 24 words. The more words, the more secure.

To make sure the mnemonic is valid, we need to calculate the checksum of the mnemonic. The checksum is the first several bits of the SHA256 hash of the entropy. Then we get a binary length of 132 bits, and we can convert it to a 12 words mnemonic.

fn step1_generate_mnemonic() {
// generate mnemonic
let entropy = &[
0x33, 0xE4, 0x6B, 0xB1, 0x3A, 0x74, 0x6E, 0xA4, 0x1C, 0xDD, 0xE4, 0x5C, 0x90, 0x84, 0x6A,
0x79,
];
let mnemonic = Mnemonic::from_entropy(entropy, Language::English).unwrap();

// let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English);
// let mnemonic: Mnemonic = Mnemonic::from_phrase(
// "indoor dish desk flag debris potato excuse depart ticket judge file exit", // It will be unvalid, if you change any word in it.
// Language::English,
// )
// .unwrap();
let phrase = mnemonic.phrase();
println!("Generated Mnemonic: {}", phrase);
}

Step 2 — Mnemonic to Seed

To convert a mnemonic to a seed, we need to use the PBKDF2 function with the mnemonic as the password and the string “mnemonic” and passphrase as the salt.

The main function of PBKDF2 is to convert a password into an encryption key. Unlike traditional one-shot hash functions, PBKDF2 generates the key by combining the password with a salt and repeatedly applying the hash function several times.

Normally, the number of iterations is set to 2048 and HMAC-SHA512 is used as the hash function to make it difficult to brute force the seed. So the length of the seed is 512 bits (64 bytes).

fn step2_mnemonic_to_seed(mnemonic: &Mnemonic) -> String {
let seed = Seed::new(mnemonic, "");
seed.as_bytes();
hex::encode(seed.as_bytes())
}
// return 3bd0bda567d4ea90f01e92d1921aacc5046128fd0e9bee96d070e1d606cb79225ee3e488bf6c898a857b5f980070d4d4ce9adf07d73458a271846ef3a8415320

Step 3 — seed to master

Press enter or click to view image in full size
BIP32

Now we need to dive into the BIP32 — Hierarchical Deterministic Wallets. The main purpose of HDW is to manage the wallet better. Only one seed can recover all the wallet generated by it. And with HDW, each time we make a transaction we can use a new address to better ensure anonymity. While BIP32 was originally designed for Bitcoin, its principles can be applied to other cryptocurrencies, allowing the same seed to generate wallet addresses for multiple currencies. All the hierarchy comes from master key, so let’s check it now.

fn step3_seed_to_master_key(seed_hex: &String) -> (String, String) {
let seed_bytes = hex::decode(seed_hex).unwrap();

let key = hmac::Key::new(hmac::HMAC_SHA512, b"Bitcoin seed");
let tag = hmac::sign(&key, &seed_bytes);

let (il, ir) = tag.as_ref().split_at(32);
(hex::encode(il), hex::encode(ir))
}
// return 5e01502044f205b98ba493971561284565e41f34f03494bb521654b0c35cb3a9 bccd1f17319e02baa4b2688f5656267d2eeaf8b49a49607e4b37efe815629c82

With master key we can derive child keys with different derive paths.

Step 4 — master key to private key

With master key and chain code, we can derive the private key for the first account now.

Let’s check the derivation path first:

m / purpose' / coin_type' / account' / change / address_index

m is the master key,

The purpose is 44' for BIP44, and the coin type is 0' for Bitcoin, 60' for Ethereum.

The account is the index of the account, starting from 0. You can define 0 as the main account for daily use, and 1 as the account for donate or anything else.

The change field is used to differentiate between internal and external chains.

The address index is the index of the address in the chain. You can use it to generate multiple addresses.

To get the private key, we need to derive the private key from the master key with the path of each level.

the derivation function do things below:

// CKDpriv((key_parent, chain_code_parent), i) -> (child_key_i, child_chain_code_i)
// `i` is the level number.
// CKDpriv: child key derivation (private)
pub fn derive(
child_number: u32,
private_key: SecretKey,
chain_code: [u8; 32],
) -> (SecretKey, [u8; 32]) {
println!("child_number: {:?}", child_number);

let child_fingerprint = fingerprint_from_private_key(private_key.clone());
println!("child_fingerprint: {:?}", hex::encode(child_fingerprint));

let derived = derive_ext_private_key(private_key.clone(), &chain_code, child_number);
let private_key = derived.0;
let chain_code = derived.1;

println!("private_key: {:?}", hex::encode(private_key.as_ref()));
println!("chain_code: {:?}\n", hex::encode(chain_code));

(private_key, chain_code)
}

// Calculate the fingerprint for a private key.
pub fn fingerprint_from_private_key(k: SecretKey) -> [u8; 4] {
let pk = curve_point_from_int(k);

// Serialize the public key in compressed format
let pk_compressed = serialize_curve_point(pk);

// Perform SHA256 hashing
let sha256_result = digest::digest(&digest::SHA256, &pk_compressed);

// Perform RIPEMD160 hashing

let ripemd_result = ripemd160::Hash::hash(sha256_result.as_ref());

// Return the first 4 bytes as the fingerprint
ripemd_result[0..4].try_into().unwrap()
}

// Derived ExtPrivate key
pub fn derive_ext_private_key(
private_key: SecretKey,
chain_code: &[u8],
child_number: u32,
) -> (SecretKey, [u8; 32]) {
let key = hmac::Key::new(hmac::HMAC_SHA512, chain_code);

let mut data = if child_number >= (1 << 31) {
[&[0u8], &private_key[..]].concat()
} else {
let p = curve_point_from_int(private_key);
serialize_curve_point(p)
// private_key.as_ref().to_vec()
};
data.extend_from_slice(&child_number.to_be_bytes());

let hmac_result = hmac::sign(&key, &data);

let (l, r) = hmac_result.as_ref().split_at(32);

let l = (*l).to_owned();
let r = (*r).to_owned();

let mut l_32 = [0u8; 32];
l_32.clone_from_slice(&l);

let private_byte = private_key.as_ref();

let l_secret = SecretKey::from_slice(&l).unwrap();
let child_private_key = l_secret
.add_tweak(&Scalar::from_be_bytes(*private_byte).unwrap())
.unwrap();
let child_chain_code = r;

(child_private_key, child_chain_code.try_into().unwrap())
}

Itereate the derive function with the path, we can get the private key for the first account with the path `m/44'/ 60'/ 0'/ 0/ 0`, and we can get the private key for the first account with the path `m/44'/ 60'/ 0'/ 0/ 1`

One more thing need to be mentioned is Apostrophe in the path indicates that BIP32 hardened derivation is used. such as 44' is a hardened derivation, while 44 is not. And 44' actually means 2³¹+44, which is a hardened derivation.

“Hardening” in BIP32 increases the security of derived keys by making it impossible to derive other child keys using just a public key and a child key, effectively preventing potential attackers from accessing your key hierarchy.

fn step3_master_kay_to_private_key(
master_secret_key_hex: String,
master_chain_code_hex: String,
derived_path: [u32; 5],
) -> String {
let master_secret_key_vec = hex::decode(master_secret_key_hex).unwrap();
let master_secret_key: &[u8] = master_secret_key_vec.as_ref();
let master_code_vec: Vec<u8> = hex::decode(master_chain_code_hex).unwrap();
let master_code: &[u8] = master_code_vec.as_ref();

let private_key = derive_with_path(
SecretKey::from_slice(master_secret_key.clone()).unwrap(),
master_code.try_into().unwrap(),
&derived_path,
);
hex::encode(private_key.as_ref())
}

pub fn derive_with_path(
master_private_key: SecretKey,
master_chain_code: [u8; 32],
path_numbers: &[u32; 5],
) -> SecretKey {
let mut depth = 0;

let mut child_number: Option<u32> = None;
let mut private_key = master_private_key;
let mut chain_code = master_chain_code;

for &i in path_numbers {
depth += 1;
println!("depth: {}", depth);

child_number = Some(i);
println!("child_number: {:?}", child_number);

(private_key, chain_code) = derive(child_number.unwrap(), private_key, chain_code);
}
private_key
}

Step 5 — private key to public key

Hard part is done, we can get the public key from the private key.

The public key is a point on the elliptic curve, and it is generated by multiplying the private key with the generator point.

fn step5_private_key_to_public_key(private_key_hex: String) -> String {
let private_key_vec = hex::decode(private_key_hex).unwrap();
let private_key = SecretKey::from_slice(private_key_vec.as_ref()).unwrap();
let public_key = curve_point_from_int(private_key);
hex::encode(serialize_curve_point(public_key))
}

Step 6 — public key to address

The address is the last 20 bytes of the Keccak-256 hash of the public key.

fn step6_public_key_to_address(pub_key_hex: String) -> String {
let public_key_vec = hex::decode(pub_key_hex).unwrap();
let public_key = PublicKey::from_slice(public_key_vec.as_ref()).unwrap();
let serialized_pub_key = public_key.serialize_uncompressed();
let public_key_bytes = &serialized_pub_key[1..];
let mut hasher = Keccak::v256();
hasher.update(public_key_bytes);
let mut output = [0u8; 32];
hasher.finalize(&mut output);

let address = &output[12..];
hex::encode(address)
}

Reference

Tool

Github

--

--

No responses yet