-
Notifications
You must be signed in to change notification settings - Fork 851
Add ERC: Composite EIP-712 Signatures #993
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
a9ad9c8
Add ERC-XXXX: Composite EIP-712 Signatures
sola92 e2e4d3f
lints
sola92 0022d4d
updates
sola92 95ce3fc
Simplify signature by removing CompositeMessage type
sola92 dea21b4
fix lints
sola92 f5d16c3
fixes
sola92 0b5df96
updates
sola92 11d12e7
verify backwards compatibility
sola92 3a98549
eth_signCompositeTypedData -> eth_signTypedData_v5
sola92 1a095e5
fix lints
sola92 4ed988c
fix lints
sola92 6e674dd
fix lints
sola92 cf19e4e
fix lints
sola92 22828ba
fix lints
sola92 991df70
fix lints
sola92 12a62b3
fix lints
sola92 0294163
updates
sola92 7d14469
updates
sola92 fefafd4
updates
sola92 9e7b14f
updates
sola92 c611106
updates
sola92 54b260a
updates
sola92 3882a89
updates
sola92 490386c
updates
sola92 e125758
updates
sola92 b0cb002
updates
sola92 11b00c6
updates
sola92 473672a
updates
sola92 0705223
updates
sola92 47a76f8
updates
sola92 f23f0a4
- Fix typos
sola92 e306df0
updates
sola92 35ad9fc
updates
sola92 171c1cc
updates
sola92 bf41d3e
updates
sola92 bd08bc7
updates
sola92 5beb332
updates
sola92 54bae6a
updates
sola92 5580d02
updates
sola92 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,335 @@ | ||
| --- | ||
| 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<`0x${string}`>>; // 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). | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.