Security keys are used almost everywhere within Ethereum during user interactions, and the Ethereum address is derived from the private key. In other words, the Ethereum address is the last 20 bytes of hash of the public key controlling the account with 0x appended in front.
How are Ethereum addresses generated?
There are three main steps to get from private -> address:
- Create a random private key (64 (hex) characters / 256 bits / 32 bytes)
- Derive the public key from this private key (128 (hex) characters / 512 bits / 64 bytes)
- Derive the address from this public key. (40 (hex) characters / 160 bits / 20 bytes)
Even though a lot of people call the address the public key, it’s actually not the case in Ethereum. There is a separate public key that acts as a middleman that you won’t ever see, unless you go poking around a pre-sale wallet JSON file.
Generating private key
The procedure for generating private keys relies on pseudo-random number generators (PRNG) with enough entropy. The most important thing to remember about a private key is that it needs to be selected randomly from the integer space 2²⁵⁶-1. Any number can be a private key as long as it’s within the value of 1 and 2²⁵⁶ — 1.
Note, if you plan on generating a new account, you should be sure these are seeded with a proper RNG.
A Private Key is a randomly selected positive integer (represented as a byte array of length 32 in big-endian form) in the range [1, secp256k1n − 1]. Every single string of 64 hex are, hypothetically, an Ethereum private key, that will access an account.
Private Key -> Public Key
To generate an Public Key from this Private Key, we need to do Elliptic Curve point multiplication, where we multiply the generator point and concatenate the coordinates into a single value. This can be complicated, so to simplify, using Elliptic Curve Digital Signature Algorithm (ECDSA), you end up with a public key that is 64 bytes.
Public key -> Address
An Ethereum address represents an account. For Externally Owned Accounts (EOA), the address is derived as the last 20 bytes of the public key controlling the account, e.g., `cd2a3d9f938e13cd947ec05abc7fe734df8dd826`. This is a hexadecimal format (base 16 notation), which is often indicated explicitly by appending 0x to the address.
To get one, start with the public key (128 characters / 64 bytes). Take the Keccak-256 hash of the public key. You should now have a string that is 64 characters / 32 bytes. (note: SHA3–256 eventually became the standard, but Ethereum uses Keccak). Take the last 40 characters / 20 bytes of this public key (Keccak-256). Or, in other words, drop the first 24 characters / 12 bytes. These 40 characters / 20 bytes are the address. Since each byte of the address is represented by 2 hex characters, a 0x prefixed address is 42 characters long.
For a given private key, the Ethereum address A is defined as the rightmost 160-bits of the Keccak hash of the corresponding ECDSA public key.
Transactions and keys
R, S and V
It is also possible to retrieve the public key from the signed transaction. However, you can do so if and only if a transaction has been sent from the account. When you send a tx, you sign the transaction and it includes the r, s and v values, where r and s are the 2 values used in standard ECDSA signatures; and v is the recovery value.
Specifically, the (r, s) is the normal output of an ECDSA signature, and with great probability, 𝑥 represents 𝑟 and 𝑦 represents 𝑠. The r is computed as the X coordinate of a point R, modulo the curve order n. The fact is that if you have the full R point (not just its X coordinate) and s, and a message, you can compute for which public key this would be a valid signature. What this allows is to ‘verify’ a message with an address, without needing to know the full key (we just do public key recovery on the signature, and then hash the recovered key and compare it with the address).
There can be up to 4 different points with a given “X coordinate modulo n”. (2 because each X coordinate has two possible Y coordinates, and 2 because r+n may still be a valid X coordinate). That number between 0 and 3 we call the recovery id, or recid. Therefore, we return an extra byte, which also functions as a header byte, by using 27+recid (for uncompressed recovered pubkeys) or 31+recid (for compressed recovered pubkeys) — which is our v with the following options:
- 27/31 = lower X even Y
- 28/32 = lower X odd Y
- 29/33 = higher X even Y
- 30/34 = higher X odd Y
Strictly speaking the recid is not necessary, as we can just cycle through all the possible coordinate pairs and check if any of them match the signature. The recid just speeds up this verification. Note that 29 and 30 (or their compressed versions 33 and 34) are exceedingly rarely, and will in practice only ever be seen in specifically generated examples. There are only two possible X values if r is between 1 and (p mod n), which has a chance of about 0.000000000000000000000000000000000000373 % to happen randomly. For any v >= 35 you might be dealing with Ethereum signatures as per EIP-155: v = recovery_id + CHAIN_ID * 2 + 35
Moving on…
One can parse the r, s and v parameters from the signed tx and then pass these values and the hash of the transaction back into a special recovery function and it’ll spit out the public key (Solidity provides a built-in function ecrecover that accepts a message along with the r, s, and v parameters and returns the address that was used to sign the message).
Note, that r, s, v, are specific to a transaction (and nonce), so using r, s, v on another transaction will almost always have an invalid effect.
Also, it is not possible to uniquely recover the public key only from an ECDSA signature (r, s). This remains true even if we also assume you know the curve, the hash function used, and you also have the message that was signed. With the signature and the message that was signed, and the knowledge of the curve, it is possible to generate two public keys; only one of which will be the public key corresponding to the private key used.