Abstract
This ERC standardizes the terminology and display of cryptographic digests in wallets for ERC-191 and EIP-712 signed data and for transaction calldata. It defines five terms:
- ERC-191 Digest
- EIP-712 Digest
- Domain Hash
- Message Hash
- Calldata Digest
It further specifies that wallets SHOULD display these values to enable independent verification by signers.
Motivation
Recent exchange hacks (Bybit: 200M, Radiant Capital: $50M) demonstrate the need for wallet-based verification independent of potentially compromised websites. Hardware wallets currently display ERC-191 messages or EIP-712 struct data across numerous screens, transaction calldata across numerous screens with hundreds of hex characters, forcing users to either compare raw data character-by-character or blindly trust what they are signing. Since many hardware wallets are air-gapped or intentionally isolated by design, extracting the calldata off the hardware device can be especially challenging, meaning the only way to verify a large transaction is to compare every character - which is infeasible in practice. Many users do not even bother to check calldata, however, security conscious users or users in charge of large treasuries or applications must have a way to verify all the data without getting security fatigue.
There are two distinct problems:
-
Inconsistent terminology. The digests produced by the ERC-191 and EIP-712 encoding functions have no canonical names. Different wallets and smart contract systems (e.g., Safe's
safeTxHash) use ad hoc terms, making cross-wallet verification confusing. -
No standard calldata digest. There is no standard way for a wallet to display a compact, verifiable summary of calldata so that a signer can confirm the full payload matches their intent.
By displaying standardized digests, signers gain a verification path: compute the expected digest independently (via a separate device, script, or tool) and compare it against what the wallet displays.
Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Definitions
ERC-191 Digest
The ERC-191 Digest is the 32-byte keccak256 hash of the ERC-191 signed_data for version 0x45 (personal_sign):
ERC-191 Digest = keccak256("\x19Ethereum Signed Message:\n" || len(message) || message)where:
messageis the raw byte sequence being signed; it need not be valid UTF-8. A text message is represented by its UTF-8 encoding, and a message transported as a0x-prefixed hexadecimal string (as inpersonal_signJSON-RPC requests) denotes the decoded bytes, not the hexadecimal characters.len(message)is the number of bytes inmessage(not the number of characters), expressed as its ASCII base-10 decimal representation with no leading zeros or padding.||denotes byte concatenation.
This is the value that is ultimately signed. Wallets and documentation MUST refer to this value as the "ERC-191 Digest" when displaying it to the user.
Note that EIP-712 typed data is itself signed as ERC-191 version 0x01; its digest is the EIP-712 Digest defined below and MUST NOT be labeled "ERC-191 Digest". This ERC defines no term for ERC-191 version 0x00.
EIP-712 Digest
The EIP-712 Digest is the 32-byte keccak256 hash of the output of the EIP-712 encode function:
EIP-712 Digest = keccak256("\x19\x01" || domainSeparator || hashStruct(message))This is the value that is ultimately signed. Wallets and documentation MUST refer to this value as the "EIP-712 Digest" when displaying it to the user.
Domain Hash
The Domain Hash is the domainSeparator value, computed as:
Domain Hash = hashStruct(eip712Domain)where eip712Domain is the EIP-712 domain struct.
Message Hash
The Message Hash is the result of hashing the structured message:
Message Hash = hashStruct(message)where message is the typed data struct being signed.
Calldata Digest
The Calldata Digest is a 32-byte digest of transaction calldata, computed as:
Calldata Digest = keccak256(uint256(len(calldata)) || calldata)where:
len(calldata)is the byte length of the calldata, encoded as a big-endianuint256(32 bytes).||denotes byte concatenation.calldatais the raw calldata bytes of the transaction.
The length prefix prevents ambiguity between calldata of different lengths that share a common prefix.
The Calldata Digest intentionally excludes chainId and other transaction envelope fields so that the same calldata produces the same digest regardless of which chain or when the transaction is submitted.
Wallet Display Requirements
ERC-191 Signing
When a wallet prompts a user to sign an ERC-191 version 0x45 (personal_sign) message, the wallet SHOULD display:
- The message itself.
- Additionally, the ERC-191 Digest (labeled exactly as "ERC-191 Digest") if the wallet does not render the complete message on a single screen (including when the message is truncated, elided, or requires scrolling), or if the message contains bytes or characters that the wallet cannot render faithfully.
The digest MUST be computed over the exact bytes being signed and MUST be displayed as a 0x-prefixed hexadecimal string.
EIP-712 Signing
When a wallet prompts a user to sign EIP-712 typed data, the wallet SHOULD display one of the following combinations:
- The EIP-712 Digest alone (RECOMMENDED, as it requires the signer to verify only one hash).
- The Domain Hash and Message Hash together.
- All three: EIP-712 Digest, Domain Hash, and Message Hash.
A wallet MUST NOT display only the Domain Hash or only the Message Hash in isolation, as neither alone is sufficient to uniquely identify the signed data.
All displayed values MUST be rendered as 0x-prefixed hexadecimal strings and labeled using the exact terms defined in this ERC.
Transaction Signing
When a wallet prompts a user to sign a transaction containing non-empty calldata, the wallet SHOULD display:
- The Calldata Digest, labeled exactly as "Calldata Digest".
The value MUST be displayed as a 0x-prefixed hexadecimal string.
Rationale
Why digests instead of human-readable descriptions
Human-readable transaction descriptions (e.g., clear-signing formats like ERC-7730) serve a complementary but different purpose. They target average users who need plain-language explanations. This ERC targets security-conscious signers who want a compact, independently verifiable fingerprint of what they are signing.
Digest-based verification has a simpler trust model: the signer only needs to trust their hash function and their independent computation, not a third-party description registry or internet-fetched metadata.
These approaches are not mutually exclusive. A wallet MAY implement both human-readable descriptions and digest display.
Why exclude chainId from Calldata Digest
The same contract call (e.g., a token transfer) should produce the same Calldata Digest on any chain. This enables users to verify calldata using tools that are not chain-aware, and allows documentation and verification guides to reference a single digest for a given operation regardless of deployment chain.
Chain-specific replay protection is already handled by the transaction signature scheme (EIP-155) and is orthogonal to calldata verification.
Why include length in Calldata Digest
Prepending the length as a fixed-width uint256 ensures that different calldata inputs that share a prefix cannot produce the same digest. Without the length prefix, truncated or extended calldata could create ambiguity.
Why prefer EIP-712 Digest over Domain Hash plus Message Hash
Verifying one hash is less error-prone than verifying two. The EIP-712 Digest uniquely identifies the combination of domain and message, so a signer who confirms the EIP-712 Digest has fully verified what they are signing. Displaying the Domain Hash and Message Hash separately is acceptable when tooling makes it easier to compute these intermediate values, but the EIP-712 Digest alone is sufficient and preferred.
Terminology standardization
The terms "ERC-191 Digest" and "EIP-712 Digest" are chosen because:
- Each directly references the standard that defines the encoding.
- "Digest" is the conventional cryptographic term for the output of a hash function.
- They avoid application-specific names (e.g.,
safeTxHash) that obscure the underlying operation.
Backwards Compatibility
This ERC introduces no changes to Ethereum protocol, transaction format, or smart contract interfaces. It is purely additive guidance for wallet implementations. Wallets that do not implement this ERC continue to function without issue.
Test Cases
ERC-191 Digest
Given the message "hello" (5 bytes), the signed_data preimage is:
"\x19Ethereum Signed Message:\n5hello"
= 0x19457468657265756d205369676e6564204d6573736167653a0a3568656c6c6fThe ERC-191 Digest is:
ERC-191 Digest = 0x50b2c43fd39106bafbba0da34fc430e1f91e3c96ea2acee2bc34119f92b37750Given the message "héllo" (5 characters but 6 bytes in UTF-8: 0x68c3a96c6c6f), the length field is the ASCII byte "6", not "5", and:
ERC-191 Digest = 0x42fd95dd8ed22d7d934d6ffff71ed0b730835b0de4bb3532a271568c8760c8d2Calldata Digest
Given calldata for an ERC-20 transfer(address,uint256) call:
calldata = 0xa9059cbb
0000000000000000000000004675c7e5baafbffbca748158becba61ef3b0a263
0000000000000000000000000000000000000000000000000de0b6b3a7640000The calldata is 68 bytes. The length prefix is:
uint256(68) = 0x0000000000000000000000000000000000000000000000000000000000000044The Calldata Digest is:
Calldata Digest = 0x812cee5d9cc7461c04bbcd7b70af9c28b243ac5d74d3453b008b93b7dac69985Reference Implementation
ERC-191 Digest (Python)
from eth_hash.auto import keccak
def compute_erc191_digest(message: bytes) -> str:
"""Compute the ERC-191 Digest (version 0x45) per this ERC."""
prefix = b"\x19Ethereum Signed Message:\n" + str(len(message)).encode("ascii")
return "0x" + keccak(prefix + message).hex()Calldata Digest (Python)
from eth_hash.auto import keccak
def compute_calldata_digest(calldata: bytes) -> str:
"""Compute the Calldata Digest per this ERC."""
if isinstance(calldata, str) and calldata.startswith("0x"):
calldata = bytes.fromhex(calldata[2:])
length_bytes = len(calldata).to_bytes(32, byteorder="big")
return "0x" + keccak(length_bytes + calldata).hex()Calldata Digest (Solidity)
function calldataDigest(bytes memory data) public pure returns (bytes32) {
return keccak256(abi.encodePacked(uint256(data.length), data));
}Security Considerations
Hash collision resistance
This ERC relies on keccak256 being collision-resistant. If a preimage or collision attack against keccak256 becomes practical, the security guarantees of digest-based verification are void. This is the same assumption underlying EIP-712 and Ethereum's account and storage model.
Digest verification is not intent verification
Displaying a digest confirms what is being signed, not why. A user who verifies a Calldata Digest confirms that the bytes match their expectation, but must still understand what those bytes do. This ERC is complementary to, not a replacement for, human-readable signing approaches like ERC-7730.
Trust model shift
Without digest display, users trust the website or dApp interface to show them correct information. With digest display, users shift trust to their independent computation tool and their ability to compare hex strings. This is a net improvement for users who can perform independent verification, but does not help users who cannot.
Partial digest comparison
Signers may be tempted to compare only the first or last few characters of a displayed digest rather than the full string. An attacker who can manipulate calldata could grind for inputs that produce a matching prefix or suffix. Signers must compare the entire digest string to achieve the security guarantees of this ERC.
Visually ambiguous message content
ERC-191 messages may contain confusable characters (e.g., Cyrillic "а" U+0430 in place of Latin "a" U+0061), zero-width characters, or bidirectional override characters that render identically to, or misleadingly differently from, the bytes actually signed. Because such characters are displayable, a wallet that shows the ERC-191 Digest only for unrenderable content will omit it in exactly the cases where visual comparison of the message text is least reliable. Wallets SHOULD display the ERC-191 Digest whenever a message contains such characters, and SHOULD indicate their presence.
Digest does not cover the destination address, or value sent
The Calldata Digest covers only the calldata bytes, not the to address or value of the transaction. Identical calldata sent to a different (potentially malicious) contract produces the same Calldata Digest, and the same calldata with an unexpected ETH value attached could also be exploited. Signers must independently verify the destination address and transaction value in addition to the Calldata Digest.
Software wallet considerations
Software wallets run on the same device as the potentially compromised dApp. Digest display in a software wallet provides weaker guarantees than in a hardware wallet, since malware could manipulate both the dApp and the wallet display. Users should prefer hardware wallets for high-value signing operations.
Quantum computing
This ERC's digest construction is hash-function-agnostic in principle. The keccak256 hash function retains approximately 128-bit security against quantum preimage attacks (via Grover's algorithm), which is considered adequate by current cryptographic standards. Should Ethereum migrate to a post-quantum hash function, the Calldata Digest definition can be updated by substituting the hash, while the ERC-191 Digest and EIP-712 Digest definitions would follow whatever their defining standards adopt. The display requirements and terminology defined here remain applicable regardless of the underlying hash or signature scheme. The greater quantum risk lies in the ECDSA signature scheme used by Ethereum, which is outside the scope of this ERC.
Copyright
Copyright and related rights waived via CC0.
