EIP-191 introduced a standard format for signing messages in Ethereum smart contracts.
EIP-191 is an Ethereum standard for signing data. It defines how signed messages should be formatted. Its formal structure helps applications verify signatures accurately and shows users they’re signing a message, not a transaction.
EIP-191 introduces a fixed prefix, a version byte, and version-specific data that wrap around the actual message, making the structure easy to identify and interpret.
EIP-191 addresses problems caused by the lack of a structured signature format:
Before EIP-191, there was no formal way to mark a signed payload as a message rather than a transaction. The lack of distinction made it easy to trick users into signing something dangerous.
For example, if you tried signing a message like “Withdraw 1 ETH from treasury” on MetaMask, you might see something like the following that includes no human-readable information:
Based on the MetaMask interface, it’s nearly impossible to know what you are signing. It could be anything, including a disguised approval to transfer funds from a MultiSig wallet.
Before EIP-191, smart contracts, dApps, and wallets all used their custom methods to sign and verify messages. This lack of standardization introduced inconsistencies. Wallets couldn’t reliably decode a message’s intent, and failed to verify signed payloads outside the tool that generated them.
Before EIP-191, it was possible to trick users into signing data that could later be replayed as a valid Ethereum transaction, because the signed message followed the same recursive length prefix (RLP) encoding.
While developers sometimes added custom safeguards, such as nonces or contract addresses, there was no standard way to prevent these misinterpretations. EIP-191 introduced a structure with a 0x19
prefix that invalidates RLP, preventing signed messages from being replayed as transactions altogether.
EIP-191 was the Ethereum community’s response to the widespread inconsistency in message signing across smart contracts.
The EIP introduced a standard message signing format that the Ethereum ecosystem could adopt. This made signatures unambiguous, verifiable, and safer across platforms and tools.
Using the new format, messages are easily decoded and understood by all dApps and wallets. The same example from above, when signing 'Withdraw 1 ETH from treasury', in a wallet that implements EIP-191 might display:
Here, the wallet has decoded the message into human-readable form so users know exactly what they’re signing.
The format also supports safeguards against signature replay attacks by allowing you to specify the intended validator address and other application-specific context, like the nonce
.
Following EIP-191, contracts can enforce message formats, and wallets can display signatures to users with more clarity and accuracy. This ensures signed messages can't be misused outside their original purpose.
Let’s break down the message format introduced by EIP-191:0x19 <1 byte version> <version-specific data> <message>
The prefix 0x19 ensures the message cannot be misinterpreted as a raw Ethereum transaction, which are encoded using RLP, Ethereum's serialization format used to encode data structures like transactions. A typical transaction might look like this:RLP([nonce, gasPrice, gasLimit, to, value, data])
And be encoded like this:0xf86c808504a817c80082520894a0df... (RLP hex bytes)
Before EIP-191, it was possible to trick users into signing a payload that’s valid RLP but doesn't represent a proper transaction, and replay it as an actual transaction. This worked because, without an RLP syntax error, the signed message could be interpreted as a valid transaction.
The 0x19
prefix prevents this by making the pre-signed message an invalid RLP.
RLP has simple rules for small values: any single byte less than 0x80
, like 0x05
or 0x7f
, is encoded as-is. So 0x19
alone is valid RLP since it is less than 0x80
. But when followed by more data like 0x19 00 deadbeef…
it breaks the encoding pattern, since RLP expects nothing after such a byte. This deliberate violation makes EIP-191 messages impossible to decode as valid transactions.
The byte directly after 0x19 is the one-byte version and tells wallets and smart contracts how to interpret the remaining data. EIP-191 currently defines three versions:
Used when signing messages intended for validation by a specific contract (like a multisig wallet). The format is:0x19 00 <validator address> <message>
For example, in a multisig contract where members approve transactions off-chain, each user signs a message, and the contract verifies the signature before executing. To prevent signature reuse, the contract includes its own address in the hash:
bytes32 hash = keccak256(abi.encodePacked(
byte(0x19),
byte(0x00),
address(this), // the validator or the contract’s address
msg.value,
nonce,
payload
));
address(this)
binds the message to the specific contract. If an attacker reuses the signature with another contract, even if all other data matches, the recovered hash would be different, and the signature fails verification.
This version is reserved for EIP-712, a structured data signing standard that builds on EIP-191. When the version byte is 0x01
, wallets and tools recognize the format as EIP-712 and expect the typed data layout defined by that standard.
This version uses the version byte (0x45
) as a prefix for the version-specific string, whereas 0x00
uses the validator contract address. It’s commonly used for signing human-readable messages like login prompts or off-chain approvals. The format for this version is:0x19 <0x45 (E)> <thereum Signed Message:\n" + len(message)> <data to sign>
The version byte 0x45 corresponds to the ASCII character E in ‘Ethereum,’ which is used as a prefix for the version-specific message.
For example, when signing "Withdraw 1 ETH" with MetaMask, the hash would look like this:
The <data to sign>
part is flexible and can contain any string or arbitrary data. In practice, it could be plain text, a JSON object, or even a serialized data structure.
In practice, Ethereum wallets and tools use different signature methods to request signatures. The most common methods are eth_sign
, personal_sign
, and eth_signTypedData
.
eth_sign
allows users to sign raw data as-is. It does not follow EIP-191, which exposes users to the challenges that EIP-191 was intended to solve. Some wallets, like MetaMask, reject eth_sign
, while others have deprecated it and display a warning message.
Here is an example of how you’d use eth_sign to sign data:
async function signWithEthSign() {
const message = "Withdraw 1 ETH from treasury";
const messageHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(message));
let accounts = await ethereum.request({ method: 'eth_requestAccounts' });
let signature = await ethereum.request({ method: 'eth_sign', params: [accounts[0], messageHash] });
}
This method encodes a message using EIP-191 version 0x45, which includes the prefix:"\x19Ethereum Signed Message:\n" + message.length + message
You provide the message
, and personal_sign
constructs the data in EIP-191 version 0x45
format and appends the prefix. This is what most wallets, like MetaMask, use to sign messages. A typical implementation looks like:
async function signWithPersonalSign() {
const signer = await getSigner();
const address = await signer.getAddress();
const message = "Withdraw 1 ETH from treasury";
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [ethers.utils.hexlify(ethers.utils.toUtf8Bytes(message)), address]
});
}
Part of EIP-712 (version 0x01
), eth_signTypedData
enables the signing of structured data, such as typed JSON. It's more secure and user-friendly since the data shown to users is clearly labeled.
async function signWithEIP712() {
const domain = {
name: 'SignatureDemo',
version: '1',
chainId: 11155111,
verifyingContract: '0x000000…'
};
const types = {
Transaction: [
{ name: 'action', type: 'string' },
{ name: 'amount', type: 'uint256' },
{ name: 'currency', type: 'string' },
{ name: 'recipient', type: 'address' }
]
};
const signer = await getSigner();
const value = {
action: "Withdraw",
amount: ethers.utils.parseEther("1").toString(),
currency: "ETH",
recipient: await signer.getAddress()
};
const signature = await signer._signTypedData(domain, types, value);
}
Not all wallets and dApps use the same signing method:
personal_sign
when eth_signTypedData
fails.personal_sign
for backward compatibility.
EIP-191 standardized how the Ethereum ecosystem signs off-chain data. Signing data is essential for proving identity, confirming intent, and authorizing actions.
Notable impacts of EIP-191 include:
EIP-191 significantly improved the UX of message signing, allowing developers to create more precise and more intuitive signing flows. This reduces errors, improves transparency, and helps users clearly understand what they’re signing and why.
EIP-191 helps reduce the risk of user compromise in phishing attempts by providing context to the content and intent of the message users are signing.
EIP-191 enhances smart contract interoperability and unlocks new possibilities for web3 systems. One such innovation is account sign-in with a wallet. Today, many dApps let users authenticate using wallets like MetaMask or WalletConnect, similar to logging in with a Google account.
These sign-ins happen off-chain, with verification handled by the app’s backend. This approach provides a smoother user experience without compromising security.
EIP-712 builds on EIP-191 to further develop structured and human-readable message formats. EIP-712 makes it easier for users to understand what they are signing even better and prevents replay attacks.
When signing a token swap or NFT purchase transaction, EIP-712 shows clear, structured fields, amounts, token names, and recipient addresses for users to verify, as shown below:
This MetaMask signing prompt illustrates how far the standard has evolved. Below is a visual comparison across the three phases: before EIP-191, with EIP-191, and then with EIP-712:
We also created a tiny demo frontend code in this GitHub gist to illustrate these concepts.
EIP-191 laid the groundwork for safer message signing in Ethereum by adding structure and intent to off-chain signatures. We’ve seen how its influence continues in standards like EIP-712, which expand on the same idea for typed data.
If you want to learn more about EIP-712, explore the free EIP-712 lesson on Cyfrin Updraft.