--- eip: 7920 title: Composite EIP-712 Signatures description: A scheme for signing multiple typed-data messages with a single signature author: Sola Ogunsakin (@sola92) discussions-to: https://ethereum-magicians.org/t/composite-eip-712-signatures/23266 status: Draft type: Standards Track category: ERC created: 2025-03-20 requires: 20, 712 --- ## Abstract This ERC provides a standard for signing multiple typed-data messages with a single signature by encoding them into a Merkle tree. This allows components to independently verify messages, without requiring full knowledge of the others. It provides a significant UX improvement by reducing the number of signature prompts to one, while preserving the security and flexibility of the [EIP-712](./eip-712.md) standard. This ERC also gives applications the flexibility to verify messages in isolation, or in aggregate. This opens up new verification modalities: for e.g, an application can require that message (`x`) is only valid when signed in combination message (`y`). ## Motivation As the ecosystem moves towards ETH-less transactions, users are often required to sign multiple off-chain messages in quick succession. Typically, a first signature is needed for a precise spend allowance (via Permit2, [ERC-2612](./eip-2612.md), etc.), followed by subsequent messages to direct the use of funds. This creates a frictional user experience as each signature requires a separate wallet interaction and creates confusion about what, in aggregate, is being approved. Current solutions have significant drawbacks: - **Pre-approving [ERC-20](./eip-20.md) allowance:** spend creates security vulnerabilities - **Merging multiple messages into a single message:** prevents independent verifiability. Each message cannot be verified without knowledge of the entire batch - **Separate signature requests:** creates friction in the user experience This ERC has the following objectives: ### Single Signature A single signature should cover multiple messages ### Isolated Verification Messages should be independently verifiable without knowledge of others ### Human-readable Readability benefits of EIP-712 should be preserved. Giving wallets and users insight into what is being signed. ## Specification ### Overview The composite signature scheme uses a Merkle tree to hash multiple typed-data data messages together under a single root. The user signs only the Merkle root. The process is described below. ### Generating a Composite Signature 1. For a set of messages `[m₁, m₂, ..., mₙ]`, encode each using EIP-712's `encode` and compute its hash: ``` hashₙ = keccak256(encode(mₙ)) ``` 2. Use these message hashes as leaf nodes in a Merkle tree and compute a `merkleRoot` 3. Sign the merkle root. ``` signature = sign(merkleRoot) ``` ### Verification Process To verify that an individual message `mₓ` was included in a composite signature: 1. Verify the signature on the `merkleRoot`: ``` recoveredSigner = ecrecover(merkleRoot, signature) isValidSignature = (recoveredSigner == expectedSigner) ``` 2. Compute the leaf node for message `mₓ` and verify its path to the Merkle root, using the proof: ``` leaf = keccak256(encode(mₓ)) isValidProof = _verifyMerkleProof(leaf, merkleProof, merkleRoot) ``` Where `_verifyMerkleProof()` is defined as: ```solidity function _verifyMerkleProof( bytes32 leaf, bytes32[] calldata proof, bytes32 merkleRoot ) internal pure returns (bool) { bytes32 computedRoot = leaf; for (uint256 i = 0; i < proof.length; ++i) { if (computedRoot < proof[i]) { computedRoot = keccak256(abi.encode(computedRoot, proof[i])); } else { computedRoot = keccak256(abi.encode(proof[i], computedRoot)); } } return computedRoot == merkleRoot; } ``` The message is verified if and only if (1) and (2) succeed. ``` isVerified = isValidSignature && isValidProof ``` ### Specification of `eth_signTypedData_v5` JSON RPC method. This ERC adds a new method `eth_signTypedData_v5` to Ethereum JSON-RPC. This method allows signing multiple typed data messages with a single signature using the specification described above. The signing account must be prior unlocked. This method returns: the signature, merkle root, and an array of proofs (each corresponding to an input message). #### Parameters 1. `Address` - Signing account 2. `TypedData | TypedDataArray` - A single TypedData object or Array of `TypedData` objects from EIP-712. ##### Returns ```typescript { signature: `0x${string}`; // Hex encoded 65 byte signature (same format as eth_sign) merkleRoot: `0x${string}`; // 32 byte Merkle root as hex string proofs: Array>; // Array of Merkle proofs (one for each input message) } ``` ##### Example Request: ```json { "jsonrpc": "2.0", "method": "eth_signTypedData_v5", "params": [ "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", [ { "types": { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "version", "type": "string" }, { "name": "chainId", "type": "uint256" }, { "name": "verifyingContract", "type": "address" } ], "Person": [ { "name": "name", "type": "string" }, { "name": "wallet", "type": "address" } ], "Mail": [ { "name": "from", "type": "Person" }, { "name": "to", "type": "Person" }, { "name": "contents", "type": "string" } ] }, "primaryType": "Mail", "domain": { "name": "Ether Mail", "version": "1", "chainId": 1, "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" }, "message": { "from": { "name": "Cow", "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" }, "to": { "name": "Bob", "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" }, "contents": "Hello, Bob!" } }, { "types": { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "version", "type": "string" }, { "name": "chainId", "type": "uint256" }, { "name": "verifyingContract", "type": "address" } ], "Transfer": [ { "name": "amount", "type": "uint256" }, { "name": "recipient", "type": "address" } ] }, "primaryType": "Transfer", "domain": { "name": "Ether Mail", "version": "1", "chainId": 1, "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" }, "message": { "amount": "1000000000000000000", "recipient": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" } } ] ], "id": 1 } ``` Result: ```JavaScript { "id": 1, "jsonrpc": "2.0", "result": { "signature": "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c", "merkleRoot": "0x7de103665e21d6c9d9f82ae59675443bd895ed42b571c7f952c2fdc1a5b6e8d2", "proofs": [ ["0x4bdbac3830d492ac3f4b0ef674786940fb33481b32392e88edafd45d507429f2"], ["0x95be87f8abefcddc8116061a06b18906f32298a4644882d06baff852164858c6"] ] } } ``` ## Rationale The choice of using a Merkle tree to bundle messages provides the following additional benefits: ### Efficient verification on-chain `_verifyMerkleProof` has a runtime of `O(log2(N))` where N is the number of messages that were signed. ### Flexible Verification Modes Applications can require combination of messages be signed together to enhance security. ### `N=1` backwards compatibility Merkle signature for single message bundles are equal to `eth_signTypedData_v4`. Requiring no onchain changes. ## Backwards Compatibility When the number of message is one, `eth_signTypedData_v5` produces the same signature as `eth_signTypedData_v4` since `merkleRoot == keccak256(encode(message))`. This allows `eth_signTypedData_v5` to be a drop-in replacement for `eth_signTypedData_v4` with no changes to on-chain verification. ## Reference Implementation ### `eth_signTypedData_v5` Reference implementation of `eth_signTypedData_v5` can be found the [assets directory](../assets/eip-7920/src/eth_signTypedData_v5.ts). ### Verifier Solidity implementation of a onchain verifier can be found the [assets directory](../assets/eip-7920/contracts/ExampleVerifier.sol). ### Merkle Reference Merkle tree can be found in the [assets directory](../assets/eip-7920/src/merkle.ts). ## Security Considerations ### Replay Protection This ERC focuses on generating composite messages and verifying their signatures. It does not contain mechanisms to prevent replays. Developers **must** ensure their applications can handle receiving the same message twice. ### Partial Message Verification During verification, care **must** be taken to ensure that **both** of these checks pass: 1. EIP-712 signature on the Merkle root is valid 2. Merkle proof is valid against the root ### User Understanding Wallets **must** communicate to users that they are signing multiple messages at once. Wallets **must** display of all message types before signing. To ensure batch signature requests are digestible, it is recommended to limit the maximum number of messages to 10. ### Merkle Tree Construction Merkle tree should be constructed in a consistent manner. 1. The hashing function **must** be `keccak256` 2. To ensure predictable/consistent proof sizes, implementations **must** pad leaves with zero hashes to reach next power of two to ensure balance. Let `n` be the number of messages. Before constructing the tree, compute the smallest `k` such that `2^(k-1) < n ≤ 2^k`. Insert zero hashes into the list of messages until list of messages is equal to `2^k`. 3. To ensure an implicit verification path, pairs **must** be sorted lexicographically before constructing parent hash. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).