0xhardman

0xhardman

twitter
medium
jike
github

Everything you need to know about converting a mnemonic phrase to an ETH address

Introduction#

Let's recall how you started your Ethereum journey? Installing the Metamask plugin, creating a mnemonic, writing it down on a piece of paper, and then clicking the confirm button! Congratulations, you now have your own Ethereum account, and you can copy your wallet address and send it to others so they can use it to receive Ether.

But do you have the following questions?

  1. Can I create my own mnemonic from a dictionary?
  2. Why are mnemonics important? Why can't I give my mnemonic to others?
  3. Can I generate my BTC address using my ETH private key?
  4. How do these 12 mnemonics become your private key and address?

Congratulations, you’ve come to the right place. This article will guide you step by step in using the Rust language to convert mnemonics into addresses. Understanding the process from mnemonic to address not only deepens your understanding of blockchain systems and cryptographic principles but also enhances job opportunities, meets relevant development needs, and improves efficiency. Therefore, this knowledge is crucial and indispensable for those who wish to delve into and engage in blockchain technology development.

Why Choose Rust#

Rust is a general-purpose compiled programming language developed by Mozilla, increasingly favored by developers for its reliability and efficiency. Therefore, I believe that more and more projects in the Ethereum ecosystem will use Rust for reconstruction and upgrades in the future. Many well-known projects based on Rust have also emerged in the Ethereum ecosystem, such as:

Foundry - A fast, portable, and modular toolkit for Ethereum application development.

Reth - A modular, contributor-friendly, and extremely fast implementation of the Ethereum protocol.

Let’s get to know Rust in Ethereum as soon as possible!

Not a Developer?#

No problem! This article also provides detailed explanations and no-code examples to help you understand the entire process.

Step 1 - Generate Mnemonic#

A mnemonic is a string of words/characters that are easy to remember and write down. Do you think mnemonics are randomly generated? In fact, they are not completely random. Let’s take a closer look.

Each word in the mnemonic can be represented by a number between 0 and 2047, totaling 2048 numbers. You can find more information from the BIP39 word list. Another interesting thing is that we can use not only English mnemonics but also Simplified Chinese, Traditional Chinese, Japanese, Korean, and Spanish mnemonics. For example:

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

Reflecting on Question 1: Can I create my own mnemonic from a dictionary?

No. The number of words/characters that can be used as mnemonics is limited, and the last word is a "checksum," meaning that the last word is the result of calculations based on all the previous words, corresponding to the previous eleven words. In fact, the last digit of our ID number is also a "checksum," and mnemonics are similar.

Next, we can convert the mnemonic into binary numbers, for example, "indoor" is 920, and the binary number is 1110011000. Each binary number is 11 bits.

Mnemonic
indoor dish desk flag debris potato excuse depart ticket judge file exit

Mnemonic in Binary
01110011000 00111111001 00111011111 01011000001 00111000011 10101000110 01001111000 00111010110 11100001101 01111000101 01010110001 01001111111

Entropy in Binary
01110011000 00111111001 00111011111 01011000001 00111000011 10101000110 01001111000 00111010110 11100001101 01111000101 01010110001 0100111

Entropy in Hex
7307e4efac13875193c1d6e1af1558a7

Checksum in Binary
1111

Length of Entropy in Binary
128

Length of Mnemonic Binary
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 the material for the mnemonic, which is entropy. The length of entropy is a multiple of 32. In the case of 12 mnemonics, the length of entropy is 128.

This also means that the length of the mnemonic is a multiple of 3, but no more than 24 words. The more words, the safer it is.

To ensure the mnemonic is valid, we need to calculate the checksum of the mnemonic. The checksum is the first few bits of the SHA256 hash of the entropy. Next, we obtain a binary length of 132 bits, which can be converted into 12 mnemonics (11 bits per word).

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 invalid 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 the mnemonic into a seed, we need to use the PBKDF2 function, taking the mnemonic as the password and the string "mnemonic" and a passphrase as the salt.

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

Typically, the iteration count is set to 2048, and HMAC-SHA512 is used as the hash function to increase the difficulty of brute-force attacks. SHA512 can be understood as a function that generates a 512-bit hash value, and thus the seed length 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 - Transfer Seed to Master Key#

image

BIP32

Now, we need to delve into BIP32 - Hierarchical Deterministic Wallets (HDW). The main purpose of HDW is to better manage wallets. With just one seed, you can recover all wallets generated by it. With HDW, we can use a new address for each transaction, thus better ensuring anonymity. Although BIP32 was initially designed for Bitcoin, its principles can be applied to other cryptocurrencies, allowing the same seed to generate multiple wallet addresses for various currencies. All hierarchies stem from the master key, so let’s take a closer look.

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 the master key, we can derive child keys using different derivation paths.

Reflecting on Question 2: Why are mnemonics important? Why can't I give my mnemonic to others?

The mnemonic is the key to recovering all your wallets. If you lose your private key, you will not be able to access your assets. If you leak your mnemonic, all assets in the accounts derived from that mnemonic may be stolen by malicious actors.

Step 4 - Master Key to Private Key#

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

Let’s first check the derivation path:

m / purpose' / coin_type' / account' / change / address_index
  1. m is the master key;
  2. purpose is 44', indicating BIP44; coin type is 0', indicating Bitcoin; 60', indicating Ethereum.
  3. account is the index of the account, starting from 0. You can define 0 as the main account for daily use and 1 as an account for donations or other purposes.
  4. change field is used to distinguish between internal and external chains.
  5. address is the index of the address on the chain. You can use it to generate multiple addresses.

To obtain the private key, we need to derive the private key and the path parameters from the master key.

The derivation function works as follows:

// 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_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
}

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

The derive_with_path function iteratively calls the derive function with the derivation path (path_numbers), master key (master_private_key), and master chain code (master_chain_code) to compute the corresponding account's private key. For example, we can obtain the private key for the first account through the path "m/44'/60'/0'/0/0" and for the second account through the path "m/44'/60'/0'/0/1".

Reflecting on Question 3: Can I generate my BTC address using my ETH private key?

No. The derivation path for Bitcoin is m/44'/0'/0'/0, while the derivation path for ETH is m/44'/60'/0'/0/0. We cannot calculate the parent private key from the child private key, which also means that if we have the master key, we can compute all wallets on the chain.

One more point to clarify is that the apostrophe in the path indicates that BIP32 hardened derivation is used, such as 44' is hardened derivation, while 44 is not. The actual meaning of 44' is 2³¹+44.

The "hardened" in BIP32 enhances the security of derived keys, making it impossible to derive other child keys using just one public key and one 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())
}

Step 5: Private Key to Public Key#

The difficult part is over, and now we can obtain the public key from the private key!

The public key is a point on the elliptic curve 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 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)
}

Reflecting on Question 4: How do these 12 mnemonics become your private key and address?

In summary, the mnemonic is generated from entropy and then converted into a seed using PBKDF2. The seed is used to generate the master key and chain code through HMAC-SHA512. The private key is extracted from the master key through a specific path. Finally, the private key generates the public key, and the last 20 bytes of the Keccak-256 hash of the public key form the Ethereum address.

References#

Ethereum 201: MnemonicsA guided tour of BIP 39 mnemonic words and seed generation with Python exampleswolovim.medium.com

Ethereum 201: HD Wallets*From mnemonic words to public address — a walkthrough of BIP 32 and BIP 44 with Python examples.*wolovim.medium.com

How to Convert Mnemonic (12 Word) to Private Key & Address Wallet Bitcoin and Ethereum*Convert Mnemonic 12 Word to Private Key Hex (SHA256) and Address Bitcoin and Ethereum Wallet.*mdrza.medium.com

BIP39 - Mnemonic Codebitcoin mnemonic converteriancoleman.io

Github#

GitHub - 0xhardman/rust-mnemonic-to-address*Contribute to 0xhardman/rust-mnemonic-to-address development by creating an account on GitHub.*github.com

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.