EIP-155: binding a signature to its chain
A pre-155 Ethereum signature said who signed and what they signed — but never which network it was for. So the exact same signed bytes were valid on every chain that shared your address, and a transaction could be replayed across a fork like ETH/ETC. EIP-155 folds a chainId into what you sign, so a signature is locked to one chain.
- digital signatures
- what a transaction is
A signature proves two things: who signed, and what they signed. Early Ethereum transactions signed over [nonce, gasPrice, gasLimit, to, value, data] — the action, but not the network it was meant for. That omission seems harmless until you remember that your private key derives the same address on every Ethereum-like chain. So a perfectly valid signed transaction on one network is a perfectly valid signed transaction on another — copy the bytes, submit them elsewhere, and they execute again. This turned catastrophic at the 2016 ETH/ETC split, when two chains suddenly shared addresses, balances, and nonces. EIP-155 is the one-line idea that fixes it: put the chain into the signature. Let’s derive it.
The problem: a signature that’s valid everywhere
To send a transaction you sign the hash of its fields. Before EIP-155 those fields were just [nonce, gasPrice, gasLimit, to, value, data], and the signature (v, r, s) used v = 27 or 28 — purely the recovery id The extra bit (parity) stored in v that lets a verifier recover the signer's public key from r and s. Pre-155 it was just 27 or 28 — one bit, carrying no other information. , telling the verifier which of two possible public keys recovered. Nowhere in the signed data is there anything that says “this transaction is for Ethereum mainnet.” So the signature authorizes the action on any chain that recognizes your address.
This is a replay attack Re-broadcasting a validly-signed transaction on a different chain (or after a fork) where the signer never intended it. Because the signature is still valid, the transaction executes again, moving funds the user never meant to move there. . After the DAO hard fork, ETH and ETC shared identical history up to the split, so a transaction you made on one was instantly replayable on the other — draining or duplicating transfers with no extra signing.
→ Step 2: make the chain part of what you sign.
Fold the chainId into the signed data
Give every network a public number — a chainId A unique integer identifying a chain: 1 for Ethereum mainnet, 11155111 for Sepolia, 61 for Ethereum Classic, and so on. Registered so wallets and nodes agree on which network they're signing for. (mainnet is 1, Ethereum Classic is 61). Now, instead of hashing just the six fields, hash the list with the chainId appended plus two zero placeholders: [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0]. That whole list is what gets keccak-hashed and signed. Because the chainId is now inside the hash, the signed digest is different on every chain — a signature computed for chainId 1 simply isn’t a signature over the digest that chainId 61 would compute.
The two trailing zeros are a neat trick: they make the to-be-signed structure look like a nine-element transaction whose (v, r, s) slots are (chainId, 0, 0), reusing the existing encoding shape rather than inventing a new one.
→ Step 3: smuggle the chainId into v.
Encode the chainId in v — backward-compatibly
There’s no room for a new field, so reuse v. Redefine it as v = chainId·2 + 35 + parity, where parity is the 0/1 recovery bit. On mainnet (chainId 1) that gives v = 37 or 38; the receiver reads v, backs out the chainId, and knows exactly which digest to reconstruct before recovering the signer. And crucially, every legacy value is below this range: v = 27/28 still means “unprotected, no chainId,” so old transactions remain valid forever — they’re just replayable, by their own choice.
- 1 the network's id — 1 for mainnet, 61 for Ethereum Classic
- 2 the EIP-155 offset; keeps legacy v = 27/28 valid
- 3 the signature's recovery bit, 0 or 1
→ Step 4: watch the wrong chain reject it.
The payoff: a replay fails signer recovery
Now follow an attacker copying your chainId-1 transaction onto chainId 61. That chain validates by reconstructing the signed digest with its own chainId, 61 — not 1. The hash it builds is different from the one you actually signed, so when it runs ECDSA recovery on your (r, s), the public key that pops out is not your address. The transaction looks like it was signed by some random account that has no funds, and it’s rejected. The signature was cryptographically welded to chain 1; it cannot travel.