---
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).
