Standard envelope for non-transaction signed data: `0x19 <version byte> <version-specific data> <data to sign>`. The `0x19` prefix prevents signed messages from being confused with valid RLP transactions. Status: Final (Standards Track / ERC). Version `0x45` (`personal_sign`) is the default for human-readable strings; `0x01` is used by EIP-712; `0x00` for validator-prefixed data.
- 01personal_sign / login challenges (Sign-In With Ethereum)
- 02off-chain message authentication
- 03preventing tx-replay of signed payloads
- 04the 0x01 envelope underlying EIP-712
- pnpm add viem # signMessage / verifyMessage / recoverMessageAddress / hashMessage
- pnpm add ethers # signer.signMessage / verifyMessage
- pnpm add siwe # Sign-In With Ethereum (EIP-4361 over 191)
Use EIP-191 to sign arbitrary off-chain data. The standard defines three versions: `0x00` = validator-specific data (`0x19 0x00 <validator address> <data>`), `0x01` = structured data per EIP-712 (`0x19 0x01 <domainSeparator> <hashStruct>`), `0x45` = `personal_sign` (`0x19 "Ethereum Signed Message:\n" <len> <message>`). With viem: `await walletClient.signMessage({ account, message })` produces a `personal_sign` (`0x45`) signature; `hashMessage(message)` reproduces the digest; `verifyMessage({ address, message, signature })` and `recoverMessageAddress(...)` complete the loop. For SIWE, build the EIP-4361 message string and hand it to `signMessage` — it rides on top of `personal_sign`. On-chain, recover with `ecrecover(hashMessage(msg), v, r, s)`. For contract signers, pair with EIP-1271.
- ⚑`personal_sign` prefixes `"\x19Ethereum Signed Message:\n" + length(message)` where `length` is the DECIMAL ASCII string length of the byte payload (e.g. 32-byte hash → `"32"`). Off-by-one or hex-vs-bytes confusion silently produces a wrong digest.
- ⚑`eth_sign` (raw) is NOT EIP-191 — it signs the bare 32-byte hash and is dangerous (can be tricked into signing a transaction). Most wallets disable it; never use it.
- ⚑Version byte `0x01` is just the envelope; the structured-data spec (typeHash etc.) lives in EIP-712. `0x19 0x01` alone is incomplete.
- ⚑Different libraries normalize newlines differently (`\n` vs `\r\n`) — always pre-encode to UTF-8 bytes before signing if your message contains line breaks.
- ⚑Replay protection is NOT built in — include a nonce + chainId + expiry in the message body (SIWE does this). The 0x19 prefix only separates messages from transactions, not messages from each other.
- ⚑Smart-account signatures will not recover via `ecrecover` — gate verification on `code.length > 0 ? isValidSignature(...) : ecrecover(...)`.