EIP-191
EIP-191¶
Overview¶
EIP-191 (Signed Data Standard) is Ethereum's signed data standard, defining the specification for handling signed data in Ethereum. This standard was proposed in 2016 by Martin Holst Swende and Nick Johnson, aiming to prevent signature replay attacks and ensure consistent signing behavior across different implementations.
EIP-191 ensures that signed messages cannot be mistaken for valid Ethereum transactions by introducing a special prefix and version mechanism.
Background and Motivation¶
Signature Replay Attacks Before EIP-191, signed messages could be replayed as transactions: - The structure of signed data could be identical to a transaction - Malicious actors could submit signatures as actual transactions - This led to unintended fund transfers or operations
Consistency Issues Different wallets and applications used different signing formats: - Lack of a unified standard - Difficulty in signature verification - Poor interoperability
Core Format¶
Standard Signing Format The signed data format defined by EIP-191:
Key Components
- 0x19 Prefix
- A fixed single-byte prefix
- 0x19 was chosen because it invalidates RLP encoding
- Ensures signed data cannot be parsed as a valid transaction
-
Prevents signature replay attacks
-
Version Byte
- Distinguishes between different signing methods
- Supports multiple signing purposes
-
Facilitates future extensions
-
Version-Specific Data
- Varies depending on the version byte
- Provides additional context information
-
Enhances security
-
Data to Sign
- The actual content to be signed
- Application-custom data
Three Version Types¶
Version 0x00: Data with Intended Validator
- Purpose: Bound to a specific validator contract
- Scenario: Contract signature verification
- Example: Signatures verified by a specific smart contract
// Signature format
bytes memory message = abi.encodePacked(
byte(0x19),
byte(0x00),
validatorAddress,
dataToSign
);
bytes32 hash = keccak256(message);
Version 0x01: Structured Data (EIP-712)
- Purpose: Reserved for EIP-712
- Scenario: Structured data signing
- Example: Approvals, permits, order signing
When the version byte is 0x01, wallets and tools recognize it as EIP-712 format, providing a more user-friendly signing display.
Version 0x45: Personal Signed Message
Full format:
- Purpose: Human-readable message signing
- Scenario: Login verification, message attestation
- Example:
personal_signRPC method
// Using ethers.js
const message = "Hello World"
const signature = await signer.signMessage(message)
// What is actually signed:
// "\x19Ethereum Signed Message:\n11Hello World"
Practical Applications¶
Login Verification
// Frontend: generate signature
const message = `Login to MyApp\nTimestamp: ${Date.now()}`
const signature = await signer.signMessage(message)
// Backend: verify signature
const recoveredAddress = ethers.verifyMessage(message, signature)
if (recoveredAddress === expectedAddress) {
// Login successful
}
Off-Chain Authorization
contract MyContract {
function verifySignature(
bytes32 hash,
bytes memory signature
) public pure returns (address) {
// Add EIP-191 prefix
bytes32 ethSignedHash = keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)
);
// Recover signer address
return ECDSA.recover(ethSignedHash, signature);
}
}
Meta-Transaction Signing
// Construct a signature compliant with EIP-191 version 0x00
const message = ethers.solidityPacked(
['bytes1', 'bytes1', 'address', 'bytes'],
['0x19', '0x00', contractAddress, encodedData]
)
const hash = ethers.keccak256(message)
const signature = await signer.signMessage(ethers.getBytes(hash))
Security Features¶
Preventing Transaction Replay The 0x19 prefix ensures the RLP encoding of signed data is invalid: - Ethereum transaction RLP encoding never starts with 0x19 - Signed messages can never be mistaken for transactions - Completely prevents signature replay attacks
Domain Separation Different versions of signatures serve different purposes: - Prevents cross-purpose replay - Clear semantics - Facilitates verification and auditing
Contract Binding Version 0x00 binds signatures to a specific contract: - Signatures can only be verified by the designated contract - Prevents cross-contract replay - Enhances security
Relationship with EIP-712¶
EIP-712 Builds on EIP-191 EIP-712 uses EIP-191 version 0x01:
Division of Responsibilities - EIP-191: Defines the low-level format for signed data - EIP-712: Defines the high-level format for structured data
Compatibility All EIP-712 signatures are valid EIP-191 signatures (version 0x01).
Development Examples¶
*Solidity* Verification**
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract SignatureVerifier {
using ECDSA for bytes32;
function verifyPersonalSignature(
string memory message,
bytes memory signature
) public pure returns (address) {
bytes32 messageHash = keccak256(abi.encodePacked(message));
// Apply EIP-191 prefix (version 0x45)
bytes32 ethSignedHash = messageHash.toEthSignedMessageHash();
// Recover signer
return ethSignedHash.recover(signature);
}
function verifyDataWithValidator(
address validator,
bytes memory data,
bytes memory signature
) public pure returns (address) {
// EIP-191 version 0x00
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0x19),
bytes1(0x00),
validator,
data
)
);
return hash.recover(signature);
}
}
*JavaScript* Signing and Verification**
import { ethers } from 'ethers'
// Personal signing (version 0x45)
async function personalSign(message, signer) {
// ethers.js automatically adds the EIP-191 prefix
const signature = await signer.signMessage(message)
return signature
}
// Verify personal signature
function verifyPersonalSignature(message, signature) {
const recoveredAddress = ethers.verifyMessage(message, signature)
return recoveredAddress
}
// Manually construct EIP-191 version 0x00 signature
async function signWithValidator(validatorAddress, data, signer) {
const message = ethers.solidityPacked(
['bytes1', 'bytes1', 'address', 'bytes'],
['0x19', '0x00', validatorAddress, data]
)
const hash = ethers.keccak256(message)
// Sign the hash directly (no additional prefix)
const sig = await signer.signMessage(ethers.getBytes(hash))
return sig
}
Common Pitfalls¶
Double Prefix Addition
// ❌ Wrong: double prefix addition
const hash = ethers.keccak256(message)
const signature = await signer.signMessage(hash) // signMessage already adds the prefix
// ✅ Correct: either use signMessage
const signature = await signer.signMessage(message)
// ✅ Or manually construct the full format and use _signTypedData
Version Confusion Different versions are for different scenarios; do not mix them: - Personal messages use version 0x45 - Structured data uses version 0x01 (EIP-712) - Contract verification uses version 0x00
Chain ID Considerations EIP-191 itself does not include a chain ID: - May lead to cross-chain replay - It is recommended to include the chain ID in the message - Or use EIP-712 (which includes the chain ID)
Best Practices¶
Choose the Correct Version - Simple message signing: use version 0x45 (personal_sign) - Structured data: use version 0x01 (EIP-712) - Contract-specific signing: use version 0x00
Include Timestamps and Nonces
Use *OpenZeppelin*
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
// Leverage standard libraries to avoid errors
Ecosystem Impact¶
Wallet Standardization All major wallets support EIP-191: - MetaMask - WalletConnect - Coinbase Wallet - Ledger, Trezor, and other hardware wallets
Development Tool Support Standard libraries have built-in support: - Ethers.js: signMessage(), verifyMessage() - Web3.js: eth.personal.sign(), eth.personal.ecRecover() - OpenZeppelin: ECDSA library
*DApp* Integration** EIP-191 has become: - The de facto standard for login verification - The foundation for off-chain signing - The cornerstone of EIP-712
Historical Significance¶
EIP-191 is critical infrastructure in the Ethereum ecosystem: - Proposed in 2016, it resolved early signing security issues - Paved the way for EIP-712 - Became the standard format for off-chain signing - Still widely used today
Related Links¶
- EIP-191 Official Specification
- Understanding EIP-191 (Medium)
- EIP-191 vs EIP-712 Comparison (Cyfrin)
- OpenZeppelin ECDSA Library