How Ethereum Addresses are Generated

with JavaScript and Ethers

Introduction

One of the fundamental requirements for interacting with the blockchain is a blockchain address or account (Externally Owned Account). An account on the blockchain serves as an identity. Due to its cryptographic underpinning, the account can also be used for authentication and verification.

The Ethereum account is a pair of two 32 bytes hexadecimal characters generated using Public Key Cryptography. One of the keys is shared to receive funds, and the other is used to establish ownership and control.

In this article, I will elaborate on the different types of Ethereum accounts out there, and how end-user wallets (like MetaMask) generate Ethereum accounts.

Types of Ethereum Accounts

There are two types of accounts in Ethereum:

  • Smart contract account: these are used by smart contracts
  • Externally Owned Account (EOA): these are used by users on the blockchain. Typically generated by wallets.

At face value, these two accounts cannot be differentiated by their values or prefixes. However, there are generated using different principles. While EOAs are generated using a public-private key pair, smart contract accounts are generated using the owner's address and the number of transactions executed by the owner's account.

It is worth noting that Ethereum has a long-standing plan to eventually migrate from EOA to smart contract accounts in the coming future. This has given rise to a new trend of end-user wallets called smart wallets (like sequence and stackup) which follows the EIP-4337 (account abstraction via entry point contract) specification.

Generating EOA usually involves three cryptographic steps:

  1. Generating a private key (Random sampling)

    The journey to creating an EOA begins with a set of binary digits (bits) that are 2^256 long. These bits will form a non-zero number between 1 to 1.15 * 10^77. However, the most interesting thing about this number is not the size, but how it is generated. Apart from it being large, this number has to be generated in the most random way possible so that it is unique (there should be only one number in existence till eternity). To complicate things even more, the number must be generated in complete isolation from the Ethereum network. As such, there is no database/record used to cross-check if the number has already been generated and assigned to someone already. The reason is that the number will be the foundation and private key of an Ethereum address and must be kept secret. There are cryptographically secured pseudo-random number generators out there that help in creating such a number.

    // crtypo module provides cryptographic functions in Javascript
    const crypto = require('crypto');
    
    // An 8 bit typed array to store 32 unsigned integers
    // NB: Because the array is typed Uint8array it will group it bits by 8 for each index of the array
    const randomBytes = new Uint8Array(32);
    
    // fill `randomBytes` with random 256 bits - which will grouped and stored in 8 bits per index
    // `getRandomValues` is a cryptographically secure pseudo-random number generator 
    // https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
    crypto.webcrypto.getRandomValues(randomBytes);
    
    // Convert `randomBytes` to hexadecimal string
    // [...randomBytes] => convert Uint8Array to a normal js array 
    //                     that is not restricted to hold 8 bits per index
    const secretKey = [...randomBytes].map( (val) => val.toString(16).padStart(2, "0")).join("")
    
    console.log(secretKey)
    // dab4306ba833e237658c09a6040641c5c65ae0a36337f91efc1e32690e59dc49
    

    secretKey(k) = dab4306ba833e237658c09a6040641c5c65ae0a36337f91efc1e32690e59dc49

This key is without the prefix 0x usually used to indicate a hexadecimal value.

  1. Generate a public key (The two shall be one)

    Once the private key has been generated, a cryptographic process (elliptic curve) is used to generate another key (called the public key) from the private key.

    Elliptic curve cryptography is a unidirectional mathematical manipulation. So if two numbers (x and y) are multiplied to get z, you will not be able to find y if you know z and x. When the private key (k) is put through the elliptic curve (C) manipulation two 32 bytes values (p1 and p2 ) are generated as output and the resulting two numbers are concatenated to form the public key (P).

    So we have that: C(k) = p1 , p2 and p1 + p2 = P

    Because elliptic curve cryptography is not reversible, the public key (P) cannot be used to deduce the private key even if it's shared.

    Ethereum uses an elliptic curve called secp256k1 same as Bitcoin.

    There are a number of serialization used for different types of elliptic curve public keys based on the length of the public key. Ethereum public key has a length of 65 and as such the key is prefixed with 04.

    // install elliptic curve pacakge (https://www.npmjs.com/package/elliptic)
    // npm i elliptic 
    const ec = new EC('secp256k1');
    
    // Generate key pair from secrete key
    const keyPair = ec.keyFromPrivate(secretKey);
    
    // Extract key uncompressed 65 bytes public key
    // returns 04 + (x coordinate + y coordinate) hex string
    const publicKey = keyPair.getPublic(false, "hex");
    console.log(publicKey);
    //04fe58f7b80f827ec2161cc6f490572086218e5f6edff85881db22c47d364850c7abe7119429acaee6444d8f7a76fdec798132935390d6224c8124ab7e5ae88381
    

    P = 04 fe58f7b80f827ec2161cc6f490572086218e5f6edff85881db22c47d364850c7 abe7119429acaee6444d8f7a76fdec798132935390d6224c8124ab7e5ae88381

  2. Convert the public key to an address (Hash state)

    The final address that is shared with the public and used to represent the account for receiving asserts over the Ethereum network is derived from the public key in a simple process of hashing.

    The two-part concatenated public key (P) derived from the private key is hashed with keccak256 hash function of ethereum to generate 32 bytes hexadecimal characters (H). Before hashing, the prefix(04) of the public key is replaced with 0x.

    Finally, the last 20bytes (or 40 hex) of the resulting hash is selected as the address for the Ethereum account. So whatever account this last 20 bytes of the hash represents can be managed with the private key which was used to start the whole (address generation) process.

    // install keccak256 pacakge (https://www.npmjs.com/package/keccak256)
    // npm i keccak256
    
    // Concatenate and hash x and y coordinates 
    // Replace first two characters (04) of public key with 0x then hash
    const hash = keccack256("0x"+publicKey.substr(2));
    
    // Select the last 20 bytes (or 40 hex) as address
    const address = hash.toString('hex').substr(-40);
    console.log(address);
    // 021f8FD02a7BbBb97F7271ceFCc2D435ca09016e
    

    Now we can prefix the address with 0x so that we have :

    address = 0x 021f8fd02a7bbbb97f7271cefcc2d435ca09016e

    We can verify that this address has been correctly generated by importing the account in an end-user wallet (like MetaMask) with the same private key we generated (dab4306ba833e237658c09a6040641c5c65ae0a36337f91efc1e32690e59dc49) and the resulting address should be the same as we got.

    You can view the full code on replit here

Generating Ethereum Address with Ethers

Generating Ethereum addresses with ethers is far easier than what we have done so far. We can generate an account by passing a private key, or by generating an account with a random private key.

  • Passing a private key

    const ethers = require('ethers');
    
    const wallet = new ethers.Wallet(secretKey);
    console.log(wallet.address);
    // 0x021f8FD02a7BbBb97F7271ceFCc2D435ca09016e
    
  • Generate random wallet

    const ethers = require('ethers');
    
    const wallet =  ethers.Wallet.createRandom();
    console.log(wallet.address);
    // 0x8B3968c0ed7A28E868ef67CBD7C7F1Aa731cfE2a