Signatures in Ethereum - ECDSA, ecrecover, personal_sign and EIP-712
How a signature (r, s, v) is built, how a contract verifies it via ecrecover, how personal_sign and EIP-712 differ, and what permit and EIP-1271 are.
In this article
In the article public-key cryptography we learned that a signature proves authorship. Now let us see how it is built in Ethereum, how a contract verifies it and what traps wait for developers and users.
The signature format
An ECDSA signature on the secp256k1 curve consists of three values:
r- 32 bytes;s- 32 bytes;v- 1 byte (27 or 28; sometimes 0 or 1) - a "hint" about which of two possible public keys is meant.
65 bytes in all. What is always signed is the hash of the message (32 bytes), not the message itself.
ecrecover: recovering the signer
The EVM has a built-in function (a precompile at address 0x01) ecrecover(hash, v, r, s) that returns, from the hash and signature, the signer's address:
address signer = ecrecover(digest, v, r, s);
require(signer != address(0) && signer == expectedSigner, "bad signature");Important: for an invalid signature ecrecover returns the zero address, not an error. If you do not check != address(0) and the expected signer is zero by mistake, the signature will "pass". That is why real contracts use proven libraries (for example ECDSA from OpenZeppelin).
A trap: signature malleability
For every signature (r, s) there is a "mirror" (r, n - s) which is also valid. This makes a signature malleable: an attacker can get another valid signature without knowing the key. If a contract uses the signature's hash as an identifier ("this signature was already used"), the trick bypasses the protection. The fix: accept only the "lower" half of s (the EIP-2 requirement) and guard against replay by nonce, not by signature hash.
personal_sign: a safe wrapper for messages
If arbitrary 32 bytes were signed, they could be passed off as a transaction hash and the user would unknowingly sign a transaction. So for text messages wallets add a prefix:
"\x19Ethereum Signed Message:\n" + length + messageThe hash of this string is signed. The prefix guarantees the signature cannot be used as a transaction signature. This is how wallet login ("Sign-In with Ethereum") works.
The downside: the user sees a string of characters or plain text and cannot see the data structure.
EIP-712: structured data
EIP-712 solves the "blind signing" problem. The structure being signed has a type and fields, and the wallet shows them readably: recipient, amount, deadline.
The hash to be signed is built like this:
digest = keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(message))The components:
- hashStruct - the hash of the data taking the struct type into account.
- domainSeparator - the hash of the "domain":
name,version,chainId,verifyingContract.
The domain is needed so the signature is bound to a specific application and network. A signature made for contract A on network 1 will not pass for contract B or for the same copy on network 8453. This protects against cross-chain and cross-contract replay (why chainId matters).
Replay protection inside a contract
Even with a domain the same signature could be submitted twice. So the signed data usually includes:
- nonce - a sequence number the contract increases after use;
- deadline - the signature's expiry.
The contract checks them, and replay is impossible.
Example: permit (EIP-2612)
Normally transferring tokens to a contract takes two transactions: approve, then the call (see approve). Tokens with the permit standard allow doing it in one: the owner signs an EIP-712 message Permit(owner, spender, value, nonce, deadline), the contract passes the signature to the token, and the permission is granted without a separate transaction from the owner.
token.permit(owner, spender, value, deadline, v, r, s);
token.transferFrom(owner, address(this), value);Convenient but dangerous: a scammer may talk you into signing a Permit, and then tokens can be pulled without a single transaction from you (attacks). Look in the wallet window at who is allowed how much.
EIP-1271: contract wallets' signatures
A smart wallet (for example a multisig) has no private key, so ecrecover does not work for it. The EIP-1271 standard introduces a function isValidSignature(hash, signature) that is called on the contract address: if it returns the magic value 0x1626ba7e, the signature is considered valid. Contracts that accept signatures should support both ways: ecrecover for ordinary addresses and EIP-1271 for contracts.
Meta-transactions
A signature lets one person (a relayer) send to the network a transaction on behalf of another and pay the gas. The user only signs an intent. This is the basis of "gasless" operations and account abstraction.
Key takeaways
- A signature always refers to a hash; what exactly is hashed matters.
- Use EIP-712: readable structure + binding to a domain.
- Always include
nonceanddeadline, and checkchainId. - Do not trust a bare
ecrecover: check the zero address and the shape ofs. - The user must read what they sign.
What to read next
We will see how all this mathematics is applied in a real scheme of controlled rights: access-control patterns. Or turn to the most frequent contract vulnerabilities: the catalogue of vulnerabilities.
Keep reading
- 1 · Basics
Public-key cryptography - how a signature proves it is you
How private and public keys work, where an address comes from, what an ECDSA digital signature is and why a wallet needs no password. With a clear example.
4 min read - 3 · Under the hood
Account abstraction (ERC-4337) - smart wallets, gas sponsorship and access recovery
What account abstraction and ERC-4337 are: UserOperation, bundler, paymaster, smart-wallet advantages over an EOA, social recovery and the risks.
4 min read
Mentioned in
- Hash functions in plain words - a data fingerprint that cannot be forged
- Public-key cryptography - how a signature proves it is you
- Glossary - blockchain, wallets, smart contracts and escrow from A to Z
- Phishing and address poisoning - how the attacks work and how to protect yourself
- Events and logs: eth_getLogs, bloom filters and public RPC limits
- ERC-20 from scratch - writing a token in Solidity and analysing every line
- Access-control patterns in smart contracts - owner, roles and deal parties
- A catalogue of smart contract vulnerabilities: 12 classic mistakes and defences
- Account abstraction (ERC-4337) - smart wallets, gas sponsorship and access recovery
- HD wallets - how 12 words yield all your keys (BIP-39, BIP-32, BIP-44)
Ready to try it?
Create your first deal in a couple of minutes. The money is locked in a smart contract and paid out by rules you can see in advance.

