Skip to content

ECDSA Signatures and Recovery (v, r, s)

The previous page turned a public key into a 20-byte address by hashing it with Keccak-256 and keeping the last 20 bytes. That address is a name the network can charge and check. This page closes the loop: it shows how the holder of the matching private key authorizes a state change, and — the part that makes Ethereum genuinely different from Bitcoin — how the network figures out which address authorized it without the transaction ever naming a sender.

The throughline of this book is: how do untrusting strangers agree on the state of a shared world computer, and what does it cost to run one? A signature is the atom of that agreement. It is the one thing that says “this specific state transition was authorized by whoever controls this account,” in a form every stranger can check independently and none can forge. No bank vouches for the sender. The math does.

Recall from secp256k1: keys as points on a curve that a private key is a scalar d, and the public key is a point Q = d·G on the curve, where G is the fixed generator and n is the (prime) order of the group. A signature is a number you can only produce with d, that anyone can verify using only Q. It nails three properties at once:

PropertyWhat it meansWhat breaks without it
Authenticityonly the holder of d could have produced itanyone could move your ether
Integrityit commits to this exact message — flip a byte, it’s invalida relay could rewrite the recipient or amount
Non-repudiationverifiable by anyone with Q, no secret needednodes couldn’t independently check

What actually gets signed is not the raw transaction but a 32-byte hash of its RLP-encoded fields (nonce, gas, recipient, value, data, and — after EIP-155 — the chain id). Call that hash z. For the rest of this page, treat the thing being signed as a single 256-bit number z.

Ethereum uses ECDSA over secp256k1 — the same scheme and the same curve Bitcoin uses. To sign message-hash z with private key d:

1. pick a per-signature nonce k in [1, n−1] ← must be secret & unique EVERY time
2. R = k · G and r = R.x mod n ← x-coordinate of a fresh point
3. s = k⁻¹ · (z + r · d) mod n
4. signature = (r, s) (Ethereum also keeps a recovery id — see below)

To verify (r, s) against public key Q and hash z — notice d and k appear nowhere:

u1 = z · s⁻¹ mod n u2 = r · s⁻¹ mod n
R' = u1 · G + u2 · Q
valid ⇔ R'.x mod n == r

The algebra closes because substituting s = k⁻¹(z + r·d) makes u1·G + u2·Q collapse back to exactly k·G = R. The verifier rebuilds the very point the signer committed to, using only public values. That is what lets a node in another country re-check your transaction with the same certainty as your own wallet.

The nonce-reuse trap — identical to the Bitcoin part

Section titled “The nonce-reuse trap — identical to the Bitcoin part”

Because Ethereum’s ECDSA is the same math as Bitcoin’s, the same fatal failure applies without a single change. If a signer reuses k across two different messages, both signatures carry the same r (the x-coordinate of the same k·G). That shared r is a public flag that k repeated — and from there recovering the key is pure arithmetic:

k = (z₁ − z₂) / (s₁ − s₂) mod n ← recover the nonce
d = (s₁·k − z₁) / r mod n ← then recover the private key

The signature scheme was never broken; the randomness leaked the key. This is the single most common way real funds have been stolen at the crypto layer, and it is why production wallets derive k deterministically from H(d, z) per RFC 6979k is then unpredictable to outsiders yet guaranteed never to repeat for two different messages.

Ethereum’s twist: recovery instead of a sender field

Section titled “Ethereum’s twist: recovery instead of a sender field”

Here is the move that separates Ethereum from Bitcoin. A Bitcoin transaction carries the spender’s public key explicitly (in the witness/scriptSig), and the verifier checks the signature against it. An Ethereum transaction carries no from field at all. So how does the network know who is paying?

It recovers the public key from the signature itself.

ECDSA verification reconstructs the point R = k·G from (r, s, z). Recovery runs that in reverse: given r, s, and z, you can solve for the public key Q — with one small ambiguity. The value r is only the x-coordinate of R, and a given x on the curve has two possible y values (a point and its reflection). Worse, r is taken mod n, so in rare cases the true x could have been r or r + n. To pin down exactly one Q, Ethereum stores a tiny recovery id alongside the signature: v.

(r, s) the ECDSA signature
v recovery id: which of the candidate R points was the real one
recover(z, r, s, v) → Q (the signer's public key, uniquely)

So an Ethereum signature is the triple (v, r, s). Given the transaction (which yields z) and that triple, anyone can compute the one public key that could have produced it. In its original form v was 27 or 28 — encoding the two y-parity choices, offset by the constant 27 for historical reasons.

Once you can recover Q, the sender’s identity is not stored — it is derived, using the exact machinery of the last three pages:

tx (RLP fields) ──► z = keccak256( rlp(tx fields) ) ← the message hash
(v, r, s) ──► Q = ecrecover(z, v, r, s) ← recover public key
Q ──► keccak256(Q) ──► last 20 bytes ← the address (page 4)
= the SENDER

That recovered address is the from the network charges for gas, whose nonce it checks, and whose balance it debits. There is no separate “sender” to trust or to get wrong: the sender is whatever address the signature mathematically resolves to. Change one byte of the transaction and z changes, so Q changes, so the address changes — the “sender” silently becomes some address with no funds, and the transaction is rejected. Authorization and identity are the same act.

The EVM even exposes this to contracts as the ecrecover precompile (address 0x01), which is how on-chain signature checks and meta-transactions work:

// Given a hash and a (v, r, s), recover the address that signed it.
// Returns the zero address on failure — you MUST check for that.
address signer = ecrecover(hash, v, r, s);
require(signer == expectedOwner, "bad signature");
require(signer != address(0), "recovery failed");

The original design had a quiet flaw. The hash z covered only nonce, gas, recipient, value, and data — nothing that named which chain the transaction was for. So a transaction validly signed on Ethereum mainnet was also a valid transaction on any Ethereum-compatible chain with the same account (same key, same nonce). Someone could take your mainnet transaction and replay it on another chain, draining the twin account there. This became urgent in 2016 when the Ethereum Classic fork created two live chains sharing every account and key.

EIP-155 (Spurious Dragon hard fork, November 2016) fixed it by folding the chain id into the signature. Two changes:

  1. The signed hash z now includes the chain id among the RLP fields. A signature made for chain id 1 (mainnet) produces a different z than the same fields on chain id 61 (Classic), so it simply is not a valid signature there.
  2. The recovery id v is encoded to carry the chain id, so a verifier can tell which chain a signature was meant for:
pre-EIP-155: v = 27 or 28
EIP-155: v = chain_id * 2 + 35 (or + 36) ← e.g. mainnet (id 1): v = 37 or 38

Because the chain id is now inside the thing being signed, a signature is cryptographically bound to one chain. Replay across chains stops being possible, not by policy but by math. (Modern typed transactions — EIP-2718 envelopes like EIP-1559 — carry the chain id as an explicit field and reset v to a plain 0/1 y-parity, but the principle is identical: the chain id is part of what you sign.)

ECDSA has a built-in symmetry: if (r, s) is a valid signature, then so is (r, n − s). Both verify against the same key and message, but they are different byte strings. That is signature malleability — a third party can flip a signature to its twin in flight. In Bitcoin this could change a transaction’s txid (see the Bitcoin part’s Transaction Malleability); the same symmetry exists here because it is the same curve.

Ethereum’s answer is the low-s rule from EIP-2 (Homestead, 2016): a signature is only valid if s ≤ n/2 — the lower half of the curve order. Of the two twins, exactly one has a low s, so restricting to that half makes the signature canonical: there is one and only one valid encoding of a given signature.

curve order n
├───────────────────────┬───────────────────────┤
0 n/2 n
│ valid s │ rejected s │
└── low-s (canonical) ───┘── high-s (the twin) ──┘

Why Ethereum cares enough to enforce it as a consensus rule, not mere relay policy: contracts use ecrecover to check signatures, and a contract that stored “have I seen this signature before?” (for replay protection, or a signature-based nonce) could be fooled by the malleated twin — the same authorization arriving as different bytes would look new. Forcing low-s gives every signature a single canonical form, so “have I seen this signature?” has an unambiguous answer.

  • Why does it exist? To let the holder of a secret d produce a 65-byte value anyone can check with only public data — proving I authorized this exact state transition without ever revealing d, and, uniquely in Ethereum, letting the network derive who authorized it rather than trust a stated from.
  • What problem does it solve? Authenticity, integrity, and non-repudiation at once, plus a compact ledger: recovery means transactions omit the sender entirely and still let every stranger independently compute exactly which address is being charged.
  • What are the trade-offs? ECDSA’s nonlinear s = k⁻¹(z + r·d) is fragile — a reused or leaked nonce k hands over d outright — and it is malleable, forcing the low-s rule (EIP-2) and the chain-id binding of EIP-155 to keep signatures canonical and non-replayable.
  • When should I avoid it? When you need aggregatable signatures (many signers folded into one) — ECDSA’s k⁻¹ term blocks that, which is why Bitcoin added Schnorr and why Ethereum validators use BLS signatures on the consensus layer instead of ECDSA.
  • What breaks if I remove it? Everything: with no recoverable signature there is no way to say “this account authorized this state change,” so nodes could not decide whose balance to debit, and the shared world computer could never agree on a single next state.
  1. An Ethereum transaction has no from field. Explain, step by step, how the network determines which address to charge for gas.
  2. What is the recovery id v for, and why is it needed on top of (r, s)? What are its pre-EIP-155 values, and what do they encode?
  3. Ethereum’s ECDSA is the same math as Bitcoin’s. What does that mean for the reused-nonce attack, and what is the standard defense?
  4. What problem did EIP-155 solve, and by what mechanism? Name the specific event that made it urgent.
  5. Why does Ethereum restrict s to the lower half of the curve order, and why does it matter specifically for contracts that call ecrecover?
Show answers
  1. Compute the message hash z = keccak256(rlp(tx fields)); recover the public key Q = ecrecover(z, v, r, s); then derive the address as keccak256(Q) keeping the last 20 bytes. That derived address is the sender — the account whose nonce is checked and whose balance is debited. Nothing is stored; identity is derived from the signature.
  2. v is the recovery id: it disambiguates which candidate point R (and thus which Q) the signature came from, since r is only the x-coordinate (two possible y values, and rarely r vs r + n). Pre-EIP-155 it was 27 or 28, encoding the y-parity choice offset by the constant 27.
  3. It means the nonce-reuse trap applies identically: two signatures sharing the same r reveal k via k = (z₁ − z₂)/(s₁ − s₂), then d = (s₁·k − z₁)/r. The defense is deterministic nonces (RFC 6979) — derive k from H(d, z) so it never repeats for different messages.
  4. It stopped cross-chain replay: a transaction signed on one chain was valid on any Ethereum-compatible chain with the same account. EIP-155 folds the chain id into the signed hash (and into v), binding a signature to one chain by math. The Ethereum Classic fork (2016), which left two live chains sharing every account and key, made it urgent.
  5. Because ECDSA is malleable: (r, s) and (r, n − s) both verify, so restricting to s ≤ n/2 (EIP-2) leaves exactly one canonical form. It matters for ecrecover-based contracts because a contract tracking “have I seen this signature?” could otherwise be fooled by the malleated twin — the same authorization arriving as different bytes would look new.