Signing & Replay Protection (EIP-155)
The previous page laid out every field a transaction carries — nonce, gas, to, value, data — with one field conspicuously absent: there is no from. A transaction never states who sent it. That is not an oversight; it is the whole trick. This page explains how a transaction proves who sent it without ever naming them, and how a single number wired into that proof stops the same signed transaction from being replayed on another chain.
Recall the book’s throughline: how do untrusting strangers agree on the state of a shared world computer? A validator on the far side of the planet, who has never met you and never will, must be able to look at a blob of bytes and conclude, with certainty, “this instruction was authorized by the account whose balance it drains.” No password, no login, no trusted middleman. The mechanism that makes that possible is a digital signature, and getting it exactly right — including a subtlety that cost real users real money in 2016 — is what this page is about.
The problem: authorization without identity
Section titled “The problem: authorization without identity”The world state is a map from address to account (we met it in foundations). A transaction that spends from 0xAlice… must be authorized by whoever controls 0xAlice…. But “control” of an account is not a login. An Ethereum account is not registered anywhere; it is simply an address for which someone, somewhere, holds a private key. There is no server to ask “is this really Alice?”
So authorization has to be self-contained. The transaction must carry, inside itself, cryptographic proof that its author holds the private key for the account it spends from — proof that anyone can check with only public information, and that no one can forge without the key. That proof is an ECDSA signature over the transaction’s contents.
private key ──(sign)──▶ signature (v, r, s) ──(recover)──▶ address (secret, (travels with (anyone can never shared) the transaction) compute this)The elegance: you never transmit the private key, and you never even transmit the address. You transmit a signature, and the address falls out of it. Let us build that flow from the bottom up.
Step 1 — Serialize and hash the transaction
Section titled “Step 1 — Serialize and hash the transaction”You cannot sign a struct; you can only sign bytes. So the first move is to turn the transaction’s meaningful fields into one canonical byte string. Ethereum uses RLP (Recursive Length Prefix) encoding — the same serialization the state tries use — to lay out the fields in a fixed, unambiguous order:
RLP( nonce, gasPrice, gasLimit, to, value, data ) ← the six signed fields │ keccak256 │ ▼ 32-byte message hash (this is what actually gets signed)Two properties matter here, and both are load-bearing:
- Canonical. RLP has exactly one valid encoding for a given set of fields. If two nodes could serialize the same transaction two different ways, they would hash it two different ways, and a signature valid on one node would be invalid on another. Consensus would fracture. A single canonical byte layout is what lets thousands of strangers compute the identical message hash.
- Complete. Every field that defines what the transaction does goes into the hash. Change the recipient, the value, the nonce, even one byte of calldata, and the hash changes — so the old signature no longer matches. The signature therefore commits to the entire instruction, not just to “Alice approves something.”
The output is a 32-byte digest. From the cryptography’s point of view, that hash is the message. Everything after this step operates on 32 bytes and never looks at the transaction again.
Step 2 — Sign the hash to produce (v, r, s)
Section titled “Step 2 — Sign the hash to produce (v, r, s)”Now the private key does its one job. Ethereum uses ECDSA on the secp256k1 curve (the same curve Bitcoin uses — one of the few primitives the two chains share). Signing takes two inputs — the 32-byte message hash and the private key — and produces three outputs:
rands— two 32-byte numbers that together are the signature. They are derived from the message hash, the private key, and a fresh random value chosen per signature.v— a tiny recovery identifier, originally just 27 or 28. It carries no secret; its whole purpose is to make the next step (recovery) unambiguous.
Why does v exist? ECDSA’s math has a quirk: given a signature (r, s) and a message hash, there are two public keys that could have produced it (the curve is symmetric, so the point could sit in one of two places). Recovering the signer would be ambiguous — until you store one extra bit saying “it was the upper one, not the lower one.” That bit is v. With it, recovery becomes deterministic: exactly one public key comes back.
Step 3 — Recover the sender with ecrecover
Section titled “Step 3 — Recover the sender with ecrecover”Here is the payoff for having no from field. When a node receives the transaction, it runs the process in reverse:
received: RLP(fields...) + (v, r, s) │ ├─ recompute the message hash from the fields (keccak256 ∘ RLP) │ └─ ecrecover(hash, v, r, s) ──▶ public key ──▶ address = fromThe ecrecover operation takes the message hash and the signature (v, r, s) and mathematically reconstructs the public key that must have signed it. Hash that public key with keccak256, take the last 20 bytes, and you have the sender’s address. That recovered address is the from — the network derives it rather than trusting a claimed value.
This is why a from field would be worse than redundant: it would be forgeable. If a transaction stated its own sender, an attacker could write any address there. Because the sender is recovered from the signature instead, the only address that can appear is the one whose private key actually signed. You cannot spend from an account you don’t control, because you cannot produce a signature that recovers to it.
The check the state-transition function then performs is exactly the affordability-and-ordering logic from the account model:
from = ecrecover(hash, v, r, s) // who signed? require account[from].nonce == tx.nonce // right order? (replay guard) require account[from].balance >= value + gas*gasPrice // can they pay? // ...only now apply the state change, and bump account[from].nonceThe replay problem
Section titled “The replay problem”We now have a transaction that authorizes itself and can only be attributed to the real signer. But a signature that is valid forever creates a new danger: a valid signed transaction is a reusable object. Whoever holds a copy — the recipient, an eavesdropper, anyone who read it from the public mempool — can rebroadcast it. This is a replay attack, and there are two flavors.
Same-chain replay is already solved, and we solved it a part ago. The nonce is a per-account counter that a transaction must name and that applying it increments. Rebroadcast the same signed transaction and it now names a stale nonce — the account has already moved past it — so the network rejects it. The nonce makes each signed transaction usable exactly once on its own chain.
Cross-chain replay is the one the nonce does not catch, because the two chains keep separate nonces. Consider what a signature commits to: RLP(nonce, gasPrice, gasLimit, to, value, data). Nothing in that list says which chain. So a transaction Alice signs to send 10 ETH on Ethereum mainnet is a perfectly valid signature for “send 10 [native coin] to the same address with the same nonce” on any chain that shares Ethereum’s transaction format and where Alice has an account at the same address — which, after a fork, is every account.
Alice signs once on Chain A: tx = { nonce:5, to:0xBob, value:10, ... } + sig(v,r,s)
The identical bytes replayed on Chain B: same nonce (5 is valid there too), same recovered from (Alice's key → same address), same signature (it never mentioned a chain) ──▶ moves Alice's coins on Chain B, which she never intendedEIP-155: fold the chain ID into the signature
Section titled “EIP-155: fold the chain ID into the signature”EIP-155 (Simple Replay Attack Protection, live at Ethereum’s Spurious Dragon fork in November 2016) fixes cross-chain replay with a minimal change: include the chain ID in the bytes that get hashed and signed. Once the chain ID is part of the message, a signature is valid only for the chain whose ID it was signed with. Replay it elsewhere and the recovered signer comes out wrong — the transaction fails to authorize.
The clever part is how it smuggles the chain ID in without adding a visible field. Before EIP-155, the six signed fields were (nonce, gasPrice, gasLimit, to, value, data). EIP-155 says: when computing the hash to sign, append three more items so you hash nine fields:
pre-155 signed data: RLP( nonce, gasPrice, gasLimit, to, value, data ) EIP-155 signed data: RLP( nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0 ) ▲ the chain ID is now inside the hashThe trailing chainId, 0, 0 change the message hash, so the signature commits to the chain. But the transaction that actually travels on the wire still carries only (v, r, s) — no explicit chainId field. Where did it go? Into v.
How v encodes the chain ID
Section titled “How v encodes the chain ID”Before EIP-155, v was just 27 or 28 (recovery bit + 27). EIP-155 overloads it to carry the chain ID as well:
pre-155: v = 27 or 28 (recovery bit only) EIP-155: v = chainId * 2 + 35 or + 36 (recovery bit + chain ID)
so on Ethereum mainnet (chainId = 1): v = 1*2 + 35 = 37 (recovery bit 0) v = 1*2 + 36 = 38 (recovery bit 1)A verifier reverses it: from v it can both recover the signing bit and read back the chain ID (chainId = (v - 35) / 2). It then reconstructs the nine-field message hash using its own chain ID and runs ecrecover. If the transaction was signed for a different chain, the hash won’t match what the signature expects, and recovery yields a garbage address that owns nothing — the transaction is rejected. One overloaded byte, and the signature is now chain-bound, with no new field on the wire and full backward compatibility with pre-155 transactions (which still use v = 27/28).
sign for chain 1 ──▶ hash includes chainId=1, v=37/38 replay on chain 1 ✓ verifier rebuilds hash with chainId=1 → matches → Alice replay on chain 5 ✗ verifier rebuilds hash with chainId=5 → mismatch → not Alice → rejectedTwo locks, two doors
Section titled “Two locks, two doors”The two replay defenses are complementary, and it is worth holding them side by side because they guard different doors:
same-chain replay cross-chain replay ───────────────── ────────────────── guarded by: the nonce EIP-155 (chain ID in v) how: each signed tx names the chain ID is signed, a counter used up once so a sig only fits one chain without it: one signed tx could one signed tx could drain run N times on-chain the twin account on a forkThe nonce makes a signature single-use within a chain; EIP-155 makes it valid only on one chain. You need both. Drop the nonce and Alice’s payment can be replayed on the same chain until her balance is gone; drop EIP-155 and it can be replayed onto every sibling chain and testnet where she shares an address. Neither replaces the other — together they make a signed transaction a genuinely one-time, one-place authorization.
The architect’s lens
Section titled “The architect’s lens”- Why does it exist? Because a world computer has no notion of login or identity — an account is just an address someone holds a key for. Signing is the only way to prove authorization in a network of strangers, and replay protection is what stops that proof from being reused where it wasn’t meant to be.
- What problem does it solve? Two, in layers: the signature proves who authorized an instruction (without a trusted authority or a forgeable
fromfield), and the nonce + EIP-155 chain ID prove that authorization applies once and here, not repeatedly or elsewhere. - What are the trade-offs? A fixed 65-byte signature and a keccak/ECDSA verification on every single transaction — pure overhead that buys trustless, offline-verifiable authorization. And overloading
vto carry the chain ID is a clever hack that keeps the wire format small but makesvnon-obvious to read; newer typed transactions moved the chain ID to an explicit field precisely to end that overloading. - When should I avoid it? You never avoid signing a real transaction — it is the authorization. But you should never sign a pre-EIP-155 (chain-agnostic) transaction on a chain with living forks or twins, and you should treat any signing prompt whose chain ID you can’t see as a replay risk.
- What breaks if I remove it? Remove the signature and anyone can spend from anyone — the ledger is worthless. Remove the nonce and every payment is replayable on its own chain. Remove EIP-155 and every fork becomes a free replay of every transaction across the split — which is not hypothetical, it is exactly what happened at the ETH/ETC break.
Check your understanding
Section titled “Check your understanding”- A transaction has no
fromfield. How does the network determine who sent it, and why is deriving the sender safer than letting the transaction state it? - Walk the signing flow from a private key to a broadcastable transaction: what gets serialized, what gets hashed, and what three values does signing produce?
- Explain the difference between same-chain replay and cross-chain replay. Which defense stops each, and why does one not cover the other?
- EIP-155 adds no visible
chainIdfield to the transaction on the wire, yet the chain ID is committed to the signature. Explain both halves: where the chain ID enters the signed data, and where it is carried on the wire. - Alice signs a transaction for chain ID 1. Someone replays the exact bytes on chain ID 5. Trace what a chain-5 verifier computes and why the transaction is rejected.
Show answers
- The network runs
ecrecover(hash, v, r, s), which reconstructs the public key that produced the signature, hashes it with keccak256, and takes the last 20 bytes to get the sender’s address — that recovered address is thefrom. Deriving it is safer because a statedfromwould be forgeable (write any address you like), whereas the recovered address can only be the one whose private key actually signed; you cannot produce a signature that recovers to an account you don’t control. - Serialize the six meaningful fields with RLP —
(nonce, gasPrice, gasLimit, to, value, data)(EIP-155 appendschainId, 0, 0) — then keccak256 that byte string into a 32-byte message hash. Signing that hash with the private key on secp256k1 producesr,s(the 64-byte signature proper) andv(a recovery identifier that also encodes the chain ID under EIP-155). - Same-chain replay is rebroadcasting a valid signed transaction on the same chain; the nonce stops it, because applying the transaction increments the account’s nonce and the rebroadcast now names a stale one. Cross-chain replay is broadcasting the identical bytes on another chain (a fork or twin); EIP-155 stops it by baking the chain ID into the signed hash. The nonce doesn’t cover cross-chain because each chain keeps its own nonce, so the same nonce is often still valid on the other side.
- Into the signed data: EIP-155 appends
chainId, 0, 0to the RLP list before hashing, so those bytes change the message hash the signature commits to. On the wire: the transaction still carries only(v, r, s)— the chain ID rides insidev, which becomeschainId * 2 + 35(or+36), letting a verifier read the chain ID back out while also recovering the signing bit. - The chain-5 verifier reconstructs the nine-field message hash using its own chain ID (5), then runs
ecrecoveron that hash with the signature(v, r, s). But the signature was made over a hash that used chain ID 1, so the hashes differ;ecrecoverreturns some other address (not Alice’s), one that owns nothing relevant. The nonce/balance/authorization checks fail against that bogus address, so the transaction is rejected — Alice’s chain-5 funds are untouched.