Ethereum Signed Message Encoding
Ethereum signs more than one kind of data: transactions, raw byte strings, and (since EIP-712) typed structured data. All three must be distinguishable from each other at the signature layer, or else an attacker could take a signature intended for one purpose and replay it as another. The leading-byte encoding scheme defined by ERC-191 and extended by EIP-712 is the mechanism that keeps them safely separated.
The three cases
The full set of signable messages is 𝕋 ∪ 𝔹⁸ⁿ ∪ 𝕊:
| Case | Encoding |
|---|---|
Transaction 𝕋 | RLP_encode(transaction) |
Raw byte string 𝔹⁸ⁿ | "\x19Ethereum Signed Message:\n" ‖ len(message) ‖ message |
Typed struct 𝕊 | "\x19\x01" ‖ domainSeparator ‖ hashStruct(message) |
The signer always hashes the encoded form with keccak256 and signs the digest.
Why the leading byte matters
For the three encodings to be safely composable, they must be injective across all three cases — no valid encoding of one case can ever equal a valid encoding of another. EIP-712 achieves this through leading-byte disambiguation:
RLP_encode(transaction): The first byte of a valid RLP-encoded Ethereum transaction is determined by the RLP encoding rules. For legacy transactions, this is a value≥ 0xc0(list prefix). For typed transactions (EIP-2718), it is a single byte in the range0x00–0x7f. Notably, a valid RLP encoding cannot start with\x19.- Personal signed message: Always starts with
\x19, followed by the literal ASCII"Ethereum Signed Message:\n". The byte after\x19is'E'(0x45). - EIP-712 typed data: Always starts with
\x19\x01. The byte after\x19is\x01, not'E'.
So:
\x19\x01...(EIP-712) cannot collide with\x19Ethereum...(ERC-191 personal sign) because the second byte differs.- Neither can collide with an RLP-encoded transaction because those never start with
\x19.
ERC-191: the version byte scheme
ERC-191 defined the general shape:
0x19 <version> <version-specific data> <data to sign>
Three versions currently defined:
| Version | Meaning | Structure |
|---|---|---|
0x00 | Data with intended validator | 0x19 0x00 <validator address> <data> |
0x45 ('E') | Personal sign | 0x19 "Ethereum Signed Message:\n" ‖ len ‖ message |
0x01 | EIP-712 structured data | 0x19 0x01 <domain separator> <hashStruct(message)> |
EIP-712 is version 0x01. The version byte is immediately after the \x19 prefix; the 32-byte domain separator is the version-specific data; the 32-byte hashStruct(message) is the data to sign.
Why not just start with the domain separator?
An early question in EIP-712 design: why not drop the \x19\x01 prefix and start directly with the domain separator or the typeHash?
The answer is injectivity with respect to RLP-encoded transactions. Without the \x19\x01 prefix, it might be possible (while hard) to construct a typeHash or domain separator whose leading bytes happen to form a valid RLP transaction prefix. The attacker could then trick a wallet into signing what it believes is a typed message but which is actually a transaction authorizing transfer of funds.
The \x19 prefix is cheap insurance: it costs 1 byte and eliminates the entire class of transaction-collision attacks by definition. The second byte (0x01 for EIP-712, 'E' for personal sign, 0x00 for validator-scoped) further separates the ERC-191 versions.
Why len(message) in personal sign
The personal-sign encoding is:
"\x19Ethereum Signed Message:\n" ‖ len(message) ‖ message
Where len(message) is the non-zero-padded ASCII decimal encoding of the number of bytes in message. For a 32-byte message, this is the two-byte string "32". For an 11-byte message, "11".
Including the length prevents length extension: without it, encode("foo") and encode("foobar") would be distinguishable only by their content, not by any structural marker. With an explicit length prefix, the encoding is a prefix-free code.
Injectivity in practice
The three-way separation gives Ethereum a useful property: a signature produced via eth_signTypedData can never accidentally authorize a transaction, and vice versa. Users can sign arbitrary typed data in their wallets without worrying that the DApp might trick them into signing an actual transaction. The wallet enforces this by refusing to pass raw bytes through eth_signTypedData — the RPC only accepts JSON-encoded typed data that goes through the \x19\x01 path.
Connections
- EIP-712: Typed structured data hashing and signing: the standard that uses the
\x19\x01prefix - EIP-712 Domain Separator: the 32-byte value that follows the prefix
- EIP-712 hashStruct: the 32-byte value at the end of the encoding
- Off-Chain Signatures: the broader pattern this encoding secures