From a9ad9c899487f5caf69fd0875d617f13ee4120c7 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Tue, 25 Mar 2025 13:00:54 -0400 Subject: [PATCH 01/39] Add ERC-XXXX: Composite EIP-712 Signatures --- assets/erc-tbd/ExampleVerifier.sol | 131 +++++++++++ assets/erc-tbd/erc-tbd.png | Bin 0 -> 36805 bytes erc-0000.md | 346 +++++++++++++++++++++++++++++ 3 files changed, 477 insertions(+) create mode 100644 assets/erc-tbd/ExampleVerifier.sol create mode 100644 assets/erc-tbd/erc-tbd.png create mode 100644 erc-0000.md diff --git a/assets/erc-tbd/ExampleVerifier.sol b/assets/erc-tbd/ExampleVerifier.sol new file mode 100644 index 00000000000..b5067f0c3ca --- /dev/null +++ b/assets/erc-tbd/ExampleVerifier.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +error Unauthorized(); + +contract ExampleVerifier { + bytes32 private immutable COMPOSITE_DOMAIN_SEPARATOR; + bytes32 private constant COMPOSITE_MESSAGE_TYPEHASH = + keccak256("CompositeMessage(bytes32 merkleRoot)"); + + bytes32 private immutable DOMAIN_SEPARATOR; + bytes32 private constant MESSAGE_TYPEHASH = + keccak256("PlaceOrder(bytes32 orderId, address user)"); + + constructor() { + COMPOSITE_DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId)" + ), + keccak256(bytes("ERC-XXXX")), + keccak256(bytes("1.0.0")), + block.chainid + ) + ); + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("MyApp")), + keccak256(bytes("1.0.0")), + block.chainid, + address(this) + ) + ); + } + + function placeOrder( + bytes32 orderId, + address user, + bytes calldata signature, + bytes32 merkleRoot, + bytes32[] calldata proof + ) public { + bytes32 messageHash = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256(abi.encode(MESSAGE_TYPEHASH, orderId, user)) + ) + ); + + if ( + !verifyCompositeSignature( + messageHash, + proof, + merkleRoot, + signature, + user + ) + ) { + revert Unauthorized(); + } + + // DO STUFF + } + + function verifyMessageInclusion( + bytes32 messageHash, + bytes32[] calldata proof, + bytes32 root + ) internal pure returns (bool) { + bytes32 computedRoot = messageHash; + + for (uint256 i = 0; i < proof.length; ++i) { + if (computedRoot < proof[i]) { + computedRoot = keccak256( + abi.encodePacked(computedRoot, proof[i]) + ); + } else { + computedRoot = keccak256( + abi.encodePacked(proof[i], computedRoot) + ); + } + } + + return computedRoot == root; + } + + function verifyCompositeSignature( + bytes32 messageHash, + bytes32[] calldata proof, + bytes32 merkleRoot, + bytes calldata signature, + address expectedSigner + ) internal view returns (bool) { + if (!verifyMessageInclusion(messageHash, proof, merkleRoot)) { + return false; + } + + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + COMPOSITE_DOMAIN_SEPARATOR, + keccak256(abi.encode(COMPOSITE_MESSAGE_TYPEHASH, merkleRoot)) + ) + ); + return recover(digest, signature) == expectedSigner; + } + + function recover( + bytes32 digest, + bytes memory signature + ) internal pure returns (address) { + require(signature.length == 65, "Invalid signature length"); + + bytes32 r; + bytes32 s; + uint8 v; + + assembly { + r := mload(add(signature, 32)) + s := mload(add(signature, 64)) + v := byte(0, mload(add(signature, 96))) + } + + return ecrecover(digest, v, r, s); + } +} diff --git a/assets/erc-tbd/erc-tbd.png b/assets/erc-tbd/erc-tbd.png new file mode 100644 index 0000000000000000000000000000000000000000..af5526fa39a123356e16ed2b25588f6c4f2b6a42 GIT binary patch literal 36805 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 G_WuAD7HByD literal 0 HcmV?d00001 diff --git a/erc-0000.md b/erc-0000.md new file mode 100644 index 00000000000..d955b52d487 --- /dev/null +++ b/erc-0000.md @@ -0,0 +1,346 @@ +--- +eip: XXXX +title: Composite EIP-712 Signatures +description: This ERC provides a standard for signing multiple typed-data messages with a single signature by encoding them into a Merkle tree. +author: Sola Ogunsakin (@_ogunsakin) +discussions-to: TBD +status: Draft +type: Standards Track +category: ERC +created: 2025-03-20 +requires: 20 +--- + +## 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 standard](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md). + +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 EIP-20 allowance (via [Permit2](https://blog.uniswap.org/permit2-and-universal-router), [EIP-2612](https://github.com/ethereum/EIPs/blob/master/EIPS/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 EIP-20 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 + +## 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 an EIP-712 message containing the Merkle root. The process is described below. + +### Composite Message Type + +This ERC defines a standard struct for a composite message: + +```solidity +struct CompositeMessage { + bytes32 merkleRoot; +} +``` + +### Generating a Composite Signature + +1. For a set of messages `[m₁, m₂, ..., mₙ]`, compute each message hash EIP-712's `hashStruct`: + + ``` + hashₙ = hashStruct(mₙ) + ``` + +2. Use these message hashes as leaf nodes in a Merkle tree. + +3. Compute the Merkle root and create a `CompositeMessage` containing this root: + + ``` + compositeMessage = { merkleRoot: merkleRoot } + ``` + +4. Sign the `CompositeMessage` according to EIP-712 encoding rules. + + ``` + signature = sign(keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(compositeMessage))) + ``` + + With `domainSeparator` defined as: + + ```solidity + keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId)"), + keccak256("ERC-XXXX"), + keccak256("1.0.0"), + block.chainid + ) + ); + ``` + +### Verification Process + +To verify that an individual message `mₓ` was included in a composite signature: + +1. Verify the signature on the `CompositeMessage` using standard EIP-712 verification: + + ``` + recoveredSigner = ecrecover(encode(domainSeparator, compositeMessage), signature) + isValidSignature = (recoveredSigner == expectedSigner) + ``` + +2. Hash message `mₓ` using EIP-712's `hashStruct` to get the leaf node and verify the Merkle proof against + the Merkle root from the `CompositeMessage`: + ``` + leafNode = hashStruct(mₓ) + isValid = verifyMerkleProof(leafNode, merkleProof, merkleRoot) + ``` + +#### Solidity Example + +Snippet below verifies a composite signature on-chain. The entrypoint, `placeOrder()` is called by a paymaster to sponsor an operation. It verifies the composite signature using the process outlined above. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +error Unauthorized(); + +contract ExampleVerifier { + bytes32 public immutable COMPOSITE_DOMAIN_SEPARATOR; + bytes32 private constant COMPOSITE_MESSAGE_TYPEHASH = + keccak256("CompositeMessage(bytes32 merkleRoot)"); + + bytes32 public immutable DOMAIN_SEPARATOR; + bytes32 private constant MESSAGE_TYPEHASH = + keccak256("PlaceOrder(bytes32 orderId, address user)"); + + constructor() { + COMPOSITE_DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId)" + ), + keccak256(bytes("ERC-XXXX")), + keccak256(bytes("1.0.0")), + block.chainid + ) + ); + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("MyApp")), + keccak256(bytes("1.0.0")), + block.chainid, + address(this) + ) + ); + } + + function placeOrder( + bytes32 orderId, + address user, + bytes calldata signature, + bytes32 merkleRoot, + bytes32[] calldata proof + ) public { + bytes32 messageHash = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256(abi.encode(MESSAGE_TYPEHASH, orderId, user)) + ) + ); + + if ( + !verifyCompositeSignature( + messageHash, + proof, + merkleRoot, + signature, + user + ) + ) { + revert Unauthorized(); + } + + // DO STUFF + } + + function verifyMessageInclusion( + bytes32 messageHash, + bytes32[] calldata proof, + bytes32 root + ) internal pure returns (bool) { + bytes32 computedRoot = messageHash; + for (uint256 i = 0; i < proof.length; ++i) { + if (computedRoot < proof[i]) { + computedRoot = keccak256( + abi.encodePacked(computedRoot, proof[i]) + ); + } else { + computedRoot = keccak256( + abi.encodePacked(proof[i], computedRoot) + ); + } + } + + return computedRoot == root; + } + + function verifyCompositeSignature( + bytes32 messageHash, + bytes32[] calldata proof, + bytes32 merkleRoot, + bytes calldata signature, + address expectedSigner + ) internal view returns (bool) { + if (!verifyMessageInclusion(messageHash, proof, merkleRoot)) { + return false; + } + + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + COMPOSITE_DOMAIN_SEPARATOR, + keccak256(abi.encode(COMPOSITE_MESSAGE_TYPEHASH, merkleRoot)) + ) + ); + return recover(digest, signature) == expectedSigner; + } + + function recover( + bytes32 digest, + bytes memory signature + ) internal pure returns (address) { + require(signature.length == 65, "Invalid signature length"); + + bytes32 r; + bytes32 s; + uint8 v; + + assembly { + r := mload(add(signature, 32)) + s := mload(add(signature, 64)) + v := byte(0, mload(add(signature, 96))) + } + + return ecrecover(digest, v, r, s); + } +} +``` + +The message is verified if and only if (1) and (2) succeed. + +### Specification of `eth_signCompositeTypedData` JSON RPC method. + +This ERC adds a new method `eth_signCompositeTypedData` to the 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. `TypedDataArray` - Array of `TypedData` objects from EIP-712. + +##### Returns + +```JavaScript +{ + signature: 'DATA', // Hex encoded 65 byte signature (same format as eth_sign) + merkleRoot: 'DATA', // 32 byte Merkle root as hex string + proofs: [ // Array of Merkle proofs (one for each input message) + ['DATA', 'DATA'], // First message proof (array of 32 byte hex strings) + ['DATA', 'DATA'], // Second message proof + ... + ] +} +``` + +##### Example + +Request: + +```shell +curl -X POST --data '{"jsonrpc":"2.0","method":"eth_signCompositeTypedData","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 + +#### UX Improvement + +A single signature that covers multiple messages + +#### Isolated Verification + +Independent verification of messages without knowledge of others + +#### Human-readable + +This ERC preserves the readability benefits of EIP-712. Giving wallets and users insight into what is being signed. + +#### Improved Wallet Security + +Certain messages signed in isolation may appear harmless but combined with may be harmful. Giving the full list of messages to users could help them better navigate their experience. + +#### Flexible Verification Modes + +Applications can require combination of messages be signed together to enhance security. + +## Backwards Compatibility + +A standalone EIP-712 signature cannot be assumed to be a composite of one. This is because composite messages are of type `CompositeMessage`. Verifiers will need to be aware of if they are verifying a composite message. + +## 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. They should provide a way for users to review all messages included in the composite signature. + +### Merkle Tree Construction + +Merkle tree should be constructed in a consistent manner. + +1. The hashing function **must** be `keccak256` +2. Hash pairs **must** be sorted lexicographically to simplify verification + +## Reference Implementation + +https://github.com/sola92/composite-712 + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). From e2e4d3f5f6b9d3e1ffbb8db16b5bfb265bded4d7 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Tue, 25 Mar 2025 13:10:18 -0400 Subject: [PATCH 02/39] lints --- erc-0000.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/erc-0000.md b/erc-0000.md index d955b52d487..aa463782aa0 100644 --- a/erc-0000.md +++ b/erc-0000.md @@ -21,8 +21,6 @@ This ERC also gives applications the flexibility to verify messages in isolation 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 EIP-20 allowance (via [Permit2](https://blog.uniswap.org/permit2-and-universal-router), [EIP-2612](https://github.com/ethereum/EIPs/blob/master/EIPS/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 EIP-20 allowance:** spend creates security vulnerabilities @@ -93,6 +91,7 @@ To verify that an individual message `mₓ` was included in a composite signatur 2. Hash message `mₓ` using EIP-712's `hashStruct` to get the leaf node and verify the Merkle proof against the Merkle root from the `CompositeMessage`: + ``` leafNode = hashStruct(mₓ) isValid = verifyMerkleProof(leafNode, merkleProof, merkleRoot) @@ -243,7 +242,7 @@ This ERC adds a new method `eth_signCompositeTypedData` to the Ethereum JSON-RPC This method returns: the signature, merkle root, and an array of proofs (each corresponding to an input message). -##### Parameters +#### Parameters 1. `Address` - Signing account 2. `TypedDataArray` - Array of `TypedData` objects from EIP-712. @@ -289,23 +288,23 @@ Result: ## Rationale -#### UX Improvement +### UX Improvement A single signature that covers multiple messages -#### Isolated Verification +### Isolated Verification Independent verification of messages without knowledge of others -#### Human-readable +### Human-readable This ERC preserves the readability benefits of EIP-712. Giving wallets and users insight into what is being signed. -#### Improved Wallet Security +### Improved Wallet Security Certain messages signed in isolation may appear harmless but combined with may be harmful. Giving the full list of messages to users could help them better navigate their experience. -#### Flexible Verification Modes +### Flexible Verification Modes Applications can require combination of messages be signed together to enhance security. From 0022d4dd4510ea23571e5e97fe836c32f5dacf2b Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Tue, 25 Mar 2025 13:35:47 -0400 Subject: [PATCH 03/39] updates --- erc-0000.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erc-0000.md b/erc-0000.md index aa463782aa0..fd22baa5e2e 100644 --- a/erc-0000.md +++ b/erc-0000.md @@ -3,7 +3,7 @@ eip: XXXX title: Composite EIP-712 Signatures description: This ERC provides a standard for signing multiple typed-data messages with a single signature by encoding them into a Merkle tree. author: Sola Ogunsakin (@_ogunsakin) -discussions-to: TBD +discussions-to: https://ethereum-magicians.org/t/composite-eip-712-signatures/23266 status: Draft type: Standards Track category: ERC @@ -97,6 +97,8 @@ To verify that an individual message `mₓ` was included in a composite signatur isValid = verifyMerkleProof(leafNode, merkleProof, merkleRoot) ``` +The message is verified if and only if (1) and (2) succeed. + #### Solidity Example Snippet below verifies a composite signature on-chain. The entrypoint, `placeOrder()` is called by a paymaster to sponsor an operation. It verifies the composite signature using the process outlined above. @@ -234,8 +236,6 @@ contract ExampleVerifier { } ``` -The message is verified if and only if (1) and (2) succeed. - ### Specification of `eth_signCompositeTypedData` JSON RPC method. This ERC adds a new method `eth_signCompositeTypedData` to the 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. From 95ce3fc706c444dabf451af67e2cad721cbac62d Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 19:50:10 -0400 Subject: [PATCH 04/39] Simplify signature by removing CompositeMessage type --- erc-0000.md => ERCS/erc-7920.md | 72 ++-------- .../erc-tbd.png => erc-7920/erc-7920.png} | Bin assets/erc-tbd/ExampleVerifier.sol | 131 ------------------ 3 files changed, 10 insertions(+), 193 deletions(-) rename erc-0000.md => ERCS/erc-7920.md (83%) rename assets/{erc-tbd/erc-tbd.png => erc-7920/erc-7920.png} (100%) delete mode 100644 assets/erc-tbd/ExampleVerifier.sol diff --git a/erc-0000.md b/ERCS/erc-7920.md similarity index 83% rename from erc-0000.md rename to ERCS/erc-7920.md index fd22baa5e2e..44bc681f490 100644 --- a/erc-0000.md +++ b/ERCS/erc-7920.md @@ -1,5 +1,5 @@ --- -eip: XXXX +eip: 7920 title: Composite EIP-712 Signatures description: This ERC provides a standard for signing multiple typed-data messages with a single signature by encoding them into a Merkle tree. author: Sola Ogunsakin (@_ogunsakin) @@ -31,17 +31,7 @@ Current solutions have significant drawbacks: ### 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 an EIP-712 message containing the Merkle root. The process is described below. - -### Composite Message Type - -This ERC defines a standard struct for a composite message: - -```solidity -struct CompositeMessage { - bytes32 merkleRoot; -} -``` +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 @@ -51,47 +41,27 @@ struct CompositeMessage { hashₙ = hashStruct(mₙ) ``` -2. Use these message hashes as leaf nodes in a Merkle tree. +2. Use these message hashes as leaf nodes in a Merkle tree and compute a `marketRoot` -3. Compute the Merkle root and create a `CompositeMessage` containing this root: +3. Sign the merkle root. ``` - compositeMessage = { merkleRoot: merkleRoot } - ``` - -4. Sign the `CompositeMessage` according to EIP-712 encoding rules. - - ``` - signature = sign(keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(compositeMessage))) - ``` - - With `domainSeparator` defined as: - - ```solidity - keccak256( - abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId)"), - keccak256("ERC-XXXX"), - keccak256("1.0.0"), - block.chainid - ) - ); + signature = sign(marketRoot) ``` ### Verification Process To verify that an individual message `mₓ` was included in a composite signature: -1. Verify the signature on the `CompositeMessage` using standard EIP-712 verification: +1. Verify the signature on the `merkleRoot`: ``` - recoveredSigner = ecrecover(encode(domainSeparator, compositeMessage), signature) + recoveredSigner = ecrecover(merkleRoot, signature) isValidSignature = (recoveredSigner == expectedSigner) ``` 2. Hash message `mₓ` using EIP-712's `hashStruct` to get the leaf node and verify the Merkle proof against - the Merkle root from the `CompositeMessage`: - + the Merkle root: ``` leafNode = hashStruct(mₓ) isValid = verifyMerkleProof(leafNode, merkleProof, merkleRoot) @@ -110,26 +80,11 @@ pragma solidity ^0.8.20; error Unauthorized(); contract ExampleVerifier { - bytes32 public immutable COMPOSITE_DOMAIN_SEPARATOR; - bytes32 private constant COMPOSITE_MESSAGE_TYPEHASH = - keccak256("CompositeMessage(bytes32 merkleRoot)"); - bytes32 public immutable DOMAIN_SEPARATOR; bytes32 private constant MESSAGE_TYPEHASH = keccak256("PlaceOrder(bytes32 orderId, address user)"); constructor() { - COMPOSITE_DOMAIN_SEPARATOR = keccak256( - abi.encode( - keccak256( - "EIP712Domain(string name,string version,uint256 chainId)" - ), - keccak256(bytes("ERC-XXXX")), - keccak256(bytes("1.0.0")), - block.chainid - ) - ); - DOMAIN_SEPARATOR = keccak256( abi.encode( keccak256( @@ -205,14 +160,7 @@ contract ExampleVerifier { return false; } - bytes32 digest = keccak256( - abi.encodePacked( - "\x19\x01", - COMPOSITE_DOMAIN_SEPARATOR, - keccak256(abi.encode(COMPOSITE_MESSAGE_TYPEHASH, merkleRoot)) - ) - ); - return recover(digest, signature) == expectedSigner; + return recover(merkleRoot, signature) == expectedSigner; } function recover( @@ -310,7 +258,7 @@ Applications can require combination of messages be signed together to enhance s ## Backwards Compatibility -A standalone EIP-712 signature cannot be assumed to be a composite of one. This is because composite messages are of type `CompositeMessage`. Verifiers will need to be aware of if they are verifying a composite message. +When the number of message is one, `eth_signCompositeTypedData` produces the same signature as `eth_signTypedData_v4` since `merkleRoot == leaf` in this scenario. This allows `eth_signCompositeTypedData` to be a drop-in replacement for `eth_signTypedData_v4`. ## Security Considerations diff --git a/assets/erc-tbd/erc-tbd.png b/assets/erc-7920/erc-7920.png similarity index 100% rename from assets/erc-tbd/erc-tbd.png rename to assets/erc-7920/erc-7920.png diff --git a/assets/erc-tbd/ExampleVerifier.sol b/assets/erc-tbd/ExampleVerifier.sol deleted file mode 100644 index b5067f0c3ca..00000000000 --- a/assets/erc-tbd/ExampleVerifier.sol +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -error Unauthorized(); - -contract ExampleVerifier { - bytes32 private immutable COMPOSITE_DOMAIN_SEPARATOR; - bytes32 private constant COMPOSITE_MESSAGE_TYPEHASH = - keccak256("CompositeMessage(bytes32 merkleRoot)"); - - bytes32 private immutable DOMAIN_SEPARATOR; - bytes32 private constant MESSAGE_TYPEHASH = - keccak256("PlaceOrder(bytes32 orderId, address user)"); - - constructor() { - COMPOSITE_DOMAIN_SEPARATOR = keccak256( - abi.encode( - keccak256( - "EIP712Domain(string name,string version,uint256 chainId)" - ), - keccak256(bytes("ERC-XXXX")), - keccak256(bytes("1.0.0")), - block.chainid - ) - ); - - DOMAIN_SEPARATOR = keccak256( - abi.encode( - keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ), - keccak256(bytes("MyApp")), - keccak256(bytes("1.0.0")), - block.chainid, - address(this) - ) - ); - } - - function placeOrder( - bytes32 orderId, - address user, - bytes calldata signature, - bytes32 merkleRoot, - bytes32[] calldata proof - ) public { - bytes32 messageHash = keccak256( - abi.encodePacked( - "\x19\x01", - DOMAIN_SEPARATOR, - keccak256(abi.encode(MESSAGE_TYPEHASH, orderId, user)) - ) - ); - - if ( - !verifyCompositeSignature( - messageHash, - proof, - merkleRoot, - signature, - user - ) - ) { - revert Unauthorized(); - } - - // DO STUFF - } - - function verifyMessageInclusion( - bytes32 messageHash, - bytes32[] calldata proof, - bytes32 root - ) internal pure returns (bool) { - bytes32 computedRoot = messageHash; - - for (uint256 i = 0; i < proof.length; ++i) { - if (computedRoot < proof[i]) { - computedRoot = keccak256( - abi.encodePacked(computedRoot, proof[i]) - ); - } else { - computedRoot = keccak256( - abi.encodePacked(proof[i], computedRoot) - ); - } - } - - return computedRoot == root; - } - - function verifyCompositeSignature( - bytes32 messageHash, - bytes32[] calldata proof, - bytes32 merkleRoot, - bytes calldata signature, - address expectedSigner - ) internal view returns (bool) { - if (!verifyMessageInclusion(messageHash, proof, merkleRoot)) { - return false; - } - - bytes32 digest = keccak256( - abi.encodePacked( - "\x19\x01", - COMPOSITE_DOMAIN_SEPARATOR, - keccak256(abi.encode(COMPOSITE_MESSAGE_TYPEHASH, merkleRoot)) - ) - ); - return recover(digest, signature) == expectedSigner; - } - - function recover( - bytes32 digest, - bytes memory signature - ) internal pure returns (address) { - require(signature.length == 65, "Invalid signature length"); - - bytes32 r; - bytes32 s; - uint8 v; - - assembly { - r := mload(add(signature, 32)) - s := mload(add(signature, 64)) - v := byte(0, mload(add(signature, 96))) - } - - return ecrecover(digest, v, r, s); - } -} From dea21b448ef57c97626d014de15ab3631b4fe782 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 20:07:47 -0400 Subject: [PATCH 05/39] fix lints --- ERCS/erc-7920.md | 28 +- assets/erc-7920/eth_signCompositeTypedData.ts | 302 ++++++++++++++++++ 2 files changed, 317 insertions(+), 13 deletions(-) create mode 100644 assets/erc-7920/eth_signCompositeTypedData.ts diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 44bc681f490..3ac97d60ef6 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -2,7 +2,7 @@ eip: 7920 title: Composite EIP-712 Signatures description: This ERC provides a standard for signing multiple typed-data messages with a single signature by encoding them into a Merkle tree. -author: Sola Ogunsakin (@_ogunsakin) +author: Sola Ogunsakin (@sola92) discussions-to: https://ethereum-magicians.org/t/composite-eip-712-signatures/23266 status: Draft type: Standards Track @@ -13,13 +13,13 @@ requires: 20 ## 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 standard](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md). +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 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 EIP-20 allowance (via [Permit2](https://blog.uniswap.org/permit2-and-universal-router), [EIP-2612](https://github.com/ethereum/EIPs/blob/master/EIPS/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. +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 EIP-20 allowance (via Permit2, EIP-2612, 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: @@ -35,10 +35,10 @@ The composite signature scheme uses a Merkle tree to hash multiple typed-data da ### Generating a Composite Signature -1. For a set of messages `[m₁, m₂, ..., mₙ]`, compute each message hash EIP-712's `hashStruct`: +1. For a set of messages `[m₁, m₂, ..., mₙ]`, compute each message hash EIP-712's `encode`: ``` - hashₙ = hashStruct(mₙ) + hashₙ = encode(mₙ) ``` 2. Use these message hashes as leaf nodes in a Merkle tree and compute a `marketRoot` @@ -60,11 +60,10 @@ To verify that an individual message `mₓ` was included in a composite signatur isValidSignature = (recoveredSigner == expectedSigner) ``` -2. Hash message `mₓ` using EIP-712's `hashStruct` to get the leaf node and verify the Merkle proof against +2. Hash message `mₓ` using EIP-712's `encode` to get the leaf node and verify the Merkle proof against the Merkle root: ``` - leafNode = hashStruct(mₓ) - isValid = verifyMerkleProof(leafNode, merkleProof, merkleRoot) + isValid = verifyMerkleProof(encode(mₓ), merkleProof, merkleRoot) ``` The message is verified if and only if (1) and (2) succeed. @@ -260,6 +259,13 @@ Applications can require combination of messages be signed together to enhance s When the number of message is one, `eth_signCompositeTypedData` produces the same signature as `eth_signTypedData_v4` since `merkleRoot == leaf` in this scenario. This allows `eth_signCompositeTypedData` to be a drop-in replacement for `eth_signTypedData_v4`. +1. The hashing function **must** be `keccak256` +2. Hash pairs **must** be sorted lexicographically to simplify verification + +## Reference Implementation + +Reference implementation of `eth_signCompositeTypedData` can be found [`here`](../assets/erc-7920/eth_signCompositeTypedData.ts). + ## Security Considerations ### Replay Protection @@ -284,10 +290,6 @@ Merkle tree should be constructed in a consistent manner. 1. The hashing function **must** be `keccak256` 2. Hash pairs **must** be sorted lexicographically to simplify verification -## Reference Implementation - -https://github.com/sola92/composite-712 - ## Copyright -Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/erc-7920/eth_signCompositeTypedData.ts b/assets/erc-7920/eth_signCompositeTypedData.ts new file mode 100644 index 00000000000..1d00bfc27a5 --- /dev/null +++ b/assets/erc-7920/eth_signCompositeTypedData.ts @@ -0,0 +1,302 @@ +import { ethers } from "ethers"; +import { MerkleTree } from "merkletreejs"; +import { keccak256 } from "@ethersproject/keccak256"; +import { Eip712TypedData } from "web3"; +import { + ecrecover, + fromRpcSig, + publicToAddress, + bytesToHex, +} from "@ethereumjs/util"; + +type MerkleProof = ReadonlyArray<`0x${string}`>; + +/** + * Signs multiple EIP-712 typed data messages with a single signature. + * + * This function creates a Merkle tree from the hashes of multiple EIP-712 typed data messages, + * then signs the Merkle root to produce a single signature that can validate any of the individual messages. + * + * @param args - The arguments for the function + * @param args.privateKey - The private key to sign with + * @param args.messages - Array of EIP-712 typed data messages to include in the composite signature + * @returns Object containing the signature, Merkle root, and proofs for each message + */ +async function eth_signCompositeTypedData(args: { + readonly privateKey: Buffer; + readonly messages: ReadonlyArray; +}): Promise<{ + readonly signature: `0x${string}`; + readonly merkleRoot: `0x${string}`; + readonly proofs: ReadonlyArray; +}> { + const { privateKey, messages } = args; + const messageHashes: ReadonlyArray = messages.map( + ({ message, domain, types }) => { + const { EIP712Domain, ...typesWithoutDomain } = types; + const hash = ethers.TypedDataEncoder.hash( + domain, + typesWithoutDomain, + message + ); + + return Buffer.from(hash.slice(2), "hex"); + } + ); + + const tree = new MerkleTree(messageHashes as Array, keccak256, { + sortPairs: true, + }); + + const merkleRoot = tree.getRoot(); + const wallet = new ethers.Wallet(`0x${privateKey.toString("hex")}`); + const signature = wallet.signingKey.sign(merkleRoot); + + const proofs: ReadonlyArray = messageHashes.map((hash) => + tree + .getProof(hash) + .map((proof) => `0x${proof.data.toString("hex")}` as `0x${string}`) + ); + + return { + signature: signature.serialized as `0x${string}`, + merkleRoot: `0x${merkleRoot.toString("hex")}`, + proofs, + }; +} + +/** + * Recovers the signer of a composite typed data signature. + * + * This function verifies that a message was included in a composite signature by: + * 1. Verifying the Merkle proof against the Merkle root + * 2. Recovering the signer from the composite signature + * + * @param args - The arguments for the function + * @param args.signature - The signature produced by eth_signCompositeTypedData + * @param args.merkleRoot - The Merkle root of all signed messages + * @param args.proof - The Merkle proof for the specific message being verified + * @param args.message - The EIP-712 typed data message to verify + * @returns The recovered signer address as a 0x-prefixed string, or undefined if the signature or proof is invalid + */ +function recoverCompositeTypedDataSig(args: { + readonly signature: `0x${string}`; + readonly merkleRoot: `0x${string}`; + readonly proof: MerkleProof; + readonly message: Eip712TypedData; +}): `0x${string}` | undefined { + const { signature, message } = args; + + const { EIP712Domain, ...typesWithoutDomain } = message.types; + const leafHex = ethers.TypedDataEncoder.hash( + message.domain, + typesWithoutDomain, + message.message + ); + const leaf = Buffer.from(leafHex.slice(2), "hex"); + + const proof = args.proof.map((d) => Buffer.from(d.slice(2), "hex")); + const merkleRoot = Buffer.from(args.merkleRoot.slice(2), "hex"); + + function _keccak256(data: Buffer): Buffer { + return Buffer.from(keccak256(data).slice(2), "hex"); + } + + let computedHash = leaf; + for (let i = 0; i < proof.length; i++) { + if (Buffer.compare(computedHash, proof[i]) == -1) { + computedHash = _keccak256(Buffer.concat([computedHash, proof[i]])); + } else { + computedHash = _keccak256(Buffer.concat([proof[i], computedHash])); + } + } + + if (Buffer.compare(computedHash, merkleRoot) != 0) { + return; + } + + const sigParams = fromRpcSig(signature); + const pubKey = ecrecover(merkleRoot, sigParams.v, sigParams.r, sigParams.s); + return bytesToHex(publicToAddress(pubKey)) as `0x${string}`; +} + +async function main() { + const messages: ReadonlyArray = [ + { + types: { + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + ], + Mail: [ + { name: "from", type: "Person" }, + { name: "to", type: "Person" }, + { name: "contents", type: "string" }, + ], + Person: [ + { name: "name", type: "string" }, + { name: "wallet", type: "address" }, + ], + }, + 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", + }, + }, + { + 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: "2000000000000000000", + recipient: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + }, + }, + { + 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: "3000000000000000000", + recipient: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + }, + }, + ]; + + const nonMessage = { + 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: "4000000000000000000", + recipient: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + }, + }; + + const wallet = ethers.Wallet.createRandom(); + const result = await eth_signCompositeTypedData({ + privateKey: Buffer.from(wallet.privateKey.slice(2), "hex"), + messages, + }); + + for (let i = 0; i < messages.length; i++) { + const recovered = recoverCompositeTypedDataSig({ + signature: result.signature, + merkleRoot: result.merkleRoot, + proof: result.proofs[i], + message: messages[i], + }); + if ( + recovered == null || + recovered.toLowerCase() != wallet.address.toLowerCase() + ) { + throw new Error("Recovered address does not match"); + } + } + + console.log("All messages recovered ✅"); + + const nonRecovered = recoverCompositeTypedDataSig({ + signature: result.signature, + merkleRoot: result.merkleRoot, + proof: result.proofs[0], + message: nonMessage, + }); + + if (nonRecovered != null) { + throw new Error("Non-message recovered ❌"); + } + + console.log("Non-message not recovered ✅"); +} + +main(); From f5d16c3724edab06b9ec8834d1e425cf842342ca Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 20:08:36 -0400 Subject: [PATCH 06/39] fixes --- ERCS/erc-7920.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 3ac97d60ef6..ea2d6f2422b 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -259,9 +259,6 @@ Applications can require combination of messages be signed together to enhance s When the number of message is one, `eth_signCompositeTypedData` produces the same signature as `eth_signTypedData_v4` since `merkleRoot == leaf` in this scenario. This allows `eth_signCompositeTypedData` to be a drop-in replacement for `eth_signTypedData_v4`. -1. The hashing function **must** be `keccak256` -2. Hash pairs **must** be sorted lexicographically to simplify verification - ## Reference Implementation Reference implementation of `eth_signCompositeTypedData` can be found [`here`](../assets/erc-7920/eth_signCompositeTypedData.ts). From 0b5df96e566d3d1e19bc35538bf9160df46b9558 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 20:20:43 -0400 Subject: [PATCH 07/39] updates --- ERCS/erc-7920.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index ea2d6f2422b..20298958c04 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -38,7 +38,7 @@ The composite signature scheme uses a Merkle tree to hash multiple typed-data da 1. For a set of messages `[m₁, m₂, ..., mₙ]`, compute each message hash EIP-712's `encode`: ``` - hashₙ = encode(mₙ) + hashₙ = keccak256(encode(mₙ)) ``` 2. Use these message hashes as leaf nodes in a Merkle tree and compute a `marketRoot` @@ -63,7 +63,7 @@ To verify that an individual message `mₓ` was included in a composite signatur 2. Hash message `mₓ` using EIP-712's `encode` to get the leaf node and verify the Merkle proof against the Merkle root: ``` - isValid = verifyMerkleProof(encode(mₓ), merkleProof, merkleRoot) + isValid = verifyMerkleProof(keccak256(encode(mₓ)), merkleProof, merkleRoot) ``` The message is verified if and only if (1) and (2) succeed. @@ -257,7 +257,7 @@ Applications can require combination of messages be signed together to enhance s ## Backwards Compatibility -When the number of message is one, `eth_signCompositeTypedData` produces the same signature as `eth_signTypedData_v4` since `merkleRoot == leaf` in this scenario. This allows `eth_signCompositeTypedData` to be a drop-in replacement for `eth_signTypedData_v4`. +When the number of message is one, `eth_signCompositeTypedData` produces the same signature as `eth_signTypedData_v4` since `merkleRoot == keccak256(encode(message))`. This allows `eth_signCompositeTypedData` to be a drop-in replacement for `eth_signTypedData_v4`. ## Reference Implementation From 11d12e774e864e4540e6489f4f454466f8f89f3a Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 20:23:24 -0400 Subject: [PATCH 08/39] verify backwards compatibility --- assets/erc-7920/eth_signCompositeTypedData.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/assets/erc-7920/eth_signCompositeTypedData.ts b/assets/erc-7920/eth_signCompositeTypedData.ts index 1d00bfc27a5..98ecec6a47c 100644 --- a/assets/erc-7920/eth_signCompositeTypedData.ts +++ b/assets/erc-7920/eth_signCompositeTypedData.ts @@ -8,6 +8,7 @@ import { publicToAddress, bytesToHex, } from "@ethereumjs/util"; +import * as sigUtil from "eth-sig-util"; type MerkleProof = ReadonlyArray<`0x${string}`>; @@ -297,6 +298,24 @@ async function main() { } console.log("Non-message not recovered ✅"); + + const singleMessage = await eth_signCompositeTypedData({ + privateKey: Buffer.from(wallet.privateKey.slice(2), "hex"), + messages: [messages[0]], + }); + + const singleMessageSig = sigUtil.signTypedData_v4( + Buffer.from(wallet.privateKey.slice(2), "hex"), + { + data: messages[0], + } + ); + + if (singleMessage.signature != singleMessageSig) { + throw new Error("Single message signature does not match"); + } + + console.log("Single message signature matches ✅"); } main(); From 3a985491e4010becd5e8ea0a5fffd09bf99d62e7 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 20:28:18 -0400 Subject: [PATCH 09/39] eth_signCompositeTypedData -> eth_signTypedData_v5 --- ERCS/erc-7920.md | 10 +++++----- ...gnCompositeTypedData.ts => eth_signTypedData_v5.ts} | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) rename assets/erc-7920/{eth_signCompositeTypedData.ts => eth_signTypedData_v5.ts} (97%) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 20298958c04..5949c7d86c2 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -183,9 +183,9 @@ contract ExampleVerifier { } ``` -### Specification of `eth_signCompositeTypedData` JSON RPC method. +### Specification of `eth_signTypedData_v5` JSON RPC method. -This ERC adds a new method `eth_signCompositeTypedData` to the 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 ERC adds a new method `eth_signTypedData_v5` to the 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). @@ -213,7 +213,7 @@ This method returns: the signature, merkle root, and an array of proofs (each co Request: ```shell -curl -X POST --data '{"jsonrpc":"2.0","method":"eth_signCompositeTypedData","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}' +curl -X POST --data '{"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: @@ -257,11 +257,11 @@ Applications can require combination of messages be signed together to enhance s ## Backwards Compatibility -When the number of message is one, `eth_signCompositeTypedData` produces the same signature as `eth_signTypedData_v4` since `merkleRoot == keccak256(encode(message))`. This allows `eth_signCompositeTypedData` to be a drop-in replacement for `eth_signTypedData_v4`. +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`. ## Reference Implementation -Reference implementation of `eth_signCompositeTypedData` can be found [`here`](../assets/erc-7920/eth_signCompositeTypedData.ts). +Reference implementation of `eth_signTypedData_v5` can be found [`here`](../assets/erc-7920/eth_signTypedData_v5.ts). ## Security Considerations diff --git a/assets/erc-7920/eth_signCompositeTypedData.ts b/assets/erc-7920/eth_signTypedData_v5.ts similarity index 97% rename from assets/erc-7920/eth_signCompositeTypedData.ts rename to assets/erc-7920/eth_signTypedData_v5.ts index 98ecec6a47c..9f04d452711 100644 --- a/assets/erc-7920/eth_signCompositeTypedData.ts +++ b/assets/erc-7920/eth_signTypedData_v5.ts @@ -23,7 +23,7 @@ type MerkleProof = ReadonlyArray<`0x${string}`>; * @param args.messages - Array of EIP-712 typed data messages to include in the composite signature * @returns Object containing the signature, Merkle root, and proofs for each message */ -async function eth_signCompositeTypedData(args: { +async function eth_signTypedData_v5(args: { readonly privateKey: Buffer; readonly messages: ReadonlyArray; }): Promise<{ @@ -74,7 +74,7 @@ async function eth_signCompositeTypedData(args: { * 2. Recovering the signer from the composite signature * * @param args - The arguments for the function - * @param args.signature - The signature produced by eth_signCompositeTypedData + * @param args.signature - The signature produced by eth_signTypedData_v5 * @param args.merkleRoot - The Merkle root of all signed messages * @param args.proof - The Merkle proof for the specific message being verified * @param args.message - The EIP-712 typed data message to verify @@ -264,7 +264,7 @@ async function main() { }; const wallet = ethers.Wallet.createRandom(); - const result = await eth_signCompositeTypedData({ + const result = await eth_signTypedData_v5({ privateKey: Buffer.from(wallet.privateKey.slice(2), "hex"), messages, }); @@ -299,7 +299,7 @@ async function main() { console.log("Non-message not recovered ✅"); - const singleMessage = await eth_signCompositeTypedData({ + const singleMessage = await eth_signTypedData_v5({ privateKey: Buffer.from(wallet.privateKey.slice(2), "hex"), messages: [messages[0]], }); From 1a095e51b2d08885941a0d0102bdfcdf827bc3e7 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 20:38:20 -0400 Subject: [PATCH 10/39] fix lints --- ERCS/erc-7920.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 5949c7d86c2..8182558251f 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -1,7 +1,7 @@ --- eip: 7920 title: Composite EIP-712 Signatures -description: This ERC provides a standard for signing multiple typed-data messages with a single signature by encoding them into a Merkle tree. +description: Composite EIP-712 signatures using merkle trees. author: Sola Ogunsakin (@sola92) discussions-to: https://ethereum-magicians.org/t/composite-eip-712-signatures/23266 status: Draft @@ -13,13 +13,13 @@ requires: 20 ## 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 standard. +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). 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 EIP-20 allowance (via Permit2, EIP-2612, 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. +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 [ERC-20](./erc-20.md) allowance (via Permit2, [ERC-2612](./erc-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: From 4ed988c79dc7251067627e4075f952fb490cfb78 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 20:41:27 -0400 Subject: [PATCH 11/39] fix lints --- ERCS/erc-7920.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 8182558251f..94c57e95d3e 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -8,7 +8,7 @@ status: Draft type: Standards Track category: ERC created: 2025-03-20 -requires: 20 +requires: 20, 712 --- ## Abstract @@ -19,11 +19,11 @@ This ERC also gives applications the flexibility to verify messages in isolation ## 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 [ERC-20](./erc-20.md) allowance (via Permit2, [ERC-2612](./erc-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. +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 x allowance (via Permit2, [ERC-2612](./erc-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 EIP-20 allowance:** spend creates security vulnerabilities +- **Pre-approving ERC-20 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 From 6e674ddbae8d28dadebe44c0a187a77d6145a635 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 20:43:13 -0400 Subject: [PATCH 12/39] fix lints --- ERCS/erc-7920.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 94c57e95d3e..d2923a7177d 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -23,7 +23,7 @@ As the ecosystem moves towards ETH-less transactions, users are often required t Current solutions have significant drawbacks: -- **Pre-approving ERC-20 allowance:** spend creates security vulnerabilities +- **Pre-approving [ERC-20](./erc-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 From cf19e4e754a4795b973f663117f4a49460bfe65a Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 20:50:13 -0400 Subject: [PATCH 13/39] fix lints --- ERCS/erc-7920.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index d2923a7177d..297b65a5616 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -63,7 +63,8 @@ To verify that an individual message `mₓ` was included in a composite signatur 2. Hash message `mₓ` using EIP-712's `encode` to get the leaf node and verify the Merkle proof against the Merkle root: ``` - isValid = verifyMerkleProof(keccak256(encode(mₓ)), merkleProof, merkleRoot) + leaf = keccak256(encode(mₓ)) + isValid = verifyMerkleProof(leaf, merkleProof, merkleRoot) ``` The message is verified if and only if (1) and (2) succeed. From 22828bae238e89f82e1c2450f909f02e9f6c075a Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 21:09:07 -0400 Subject: [PATCH 14/39] fix lints --- ERCS/erc-7920.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 297b65a5616..e1649282d87 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -19,11 +19,11 @@ This ERC also gives applications the flexibility to verify messages in isolation ## 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 x allowance (via Permit2, [ERC-2612](./erc-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. +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 x 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](./erc-20.md) allowance:** spend creates security vulnerabilities +- **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 @@ -262,7 +262,7 @@ When the number of message is one, `eth_signTypedData_v5` produces the same sign ## Reference Implementation -Reference implementation of `eth_signTypedData_v5` can be found [`here`](../assets/erc-7920/eth_signTypedData_v5.ts). +Reference implementation of `eth_signTypedData_v5` can be found `/assets/erc-7920/eth_signTypedData_v5.ts`. ## Security Considerations From 991df703c37c8734619bcb2b88f5cc8828ef2bf8 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 21:14:52 -0400 Subject: [PATCH 15/39] fix lints --- ERCS/erc-7920.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index e1649282d87..52aeaa4abd6 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -262,7 +262,7 @@ When the number of message is one, `eth_signTypedData_v5` produces the same sign ## Reference Implementation -Reference implementation of `eth_signTypedData_v5` can be found `/assets/erc-7920/eth_signTypedData_v5.ts`. +Reference implementation of `eth_signTypedData_v5` can be found the assets directory. ## Security Considerations From 12a62b3342d57c2f8ae6bfbc823cf285cb9e3196 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 21:21:20 -0400 Subject: [PATCH 16/39] fix lints --- ERCS/erc-7920.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 52aeaa4abd6..45c8a5418a7 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -128,6 +128,20 @@ contract ExampleVerifier { // DO STUFF } + function verifyCompositeSignature( + bytes32 messageHash, + bytes32[] calldata proof, + bytes32 merkleRoot, + bytes calldata signature, + address expectedSigner + ) internal view returns (bool) { + if (!verifyMessageInclusion(messageHash, proof, merkleRoot)) { + return false; + } + + return recover(merkleRoot, signature) == expectedSigner; + } + function verifyMessageInclusion( bytes32 messageHash, bytes32[] calldata proof, @@ -149,20 +163,6 @@ contract ExampleVerifier { return computedRoot == root; } - function verifyCompositeSignature( - bytes32 messageHash, - bytes32[] calldata proof, - bytes32 merkleRoot, - bytes calldata signature, - address expectedSigner - ) internal view returns (bool) { - if (!verifyMessageInclusion(messageHash, proof, merkleRoot)) { - return false; - } - - return recover(merkleRoot, signature) == expectedSigner; - } - function recover( bytes32 digest, bytes memory signature From 02941639c08e7b797b30905d8c1ec064a6779858 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 22:32:11 -0400 Subject: [PATCH 17/39] updates --- ERCS/erc-7920.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 45c8a5418a7..93c6e85ca06 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -35,7 +35,7 @@ The composite signature scheme uses a Merkle tree to hash multiple typed-data da ### Generating a Composite Signature -1. For a set of messages `[m₁, m₂, ..., mₙ]`, compute each message hash EIP-712's `encode`: +1. For a set of messages `[m₁, m₂, ..., mₙ]`, encode each using EIP-712's `encode` and compute its hash: ``` hashₙ = keccak256(encode(mₙ)) From 7d144696a6f7d99ee7235c1f3ac7733c26d24875 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 22:34:13 -0400 Subject: [PATCH 18/39] updates --- ERCS/erc-7920.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 93c6e85ca06..96dbacdb517 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -60,8 +60,7 @@ To verify that an individual message `mₓ` was included in a composite signatur isValidSignature = (recoveredSigner == expectedSigner) ``` -2. Hash message `mₓ` using EIP-712's `encode` to get the leaf node and verify the Merkle proof against - the Merkle root: +2. Compute the leaf node for message `mₓ` and verify the path to the Merkle root using the proof: ``` leaf = keccak256(encode(mₓ)) isValid = verifyMerkleProof(leaf, merkleProof, merkleRoot) From fefafd4eed46db7b852971aa53b4637abd3a34d2 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 22:35:40 -0400 Subject: [PATCH 19/39] updates --- ERCS/erc-7920.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 96dbacdb517..8ad9d4e4f79 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -113,7 +113,7 @@ contract ExampleVerifier { ); if ( - !verifyCompositeSignature( + !_verifyCompositeSignature( messageHash, proof, merkleRoot, @@ -127,21 +127,21 @@ contract ExampleVerifier { // DO STUFF } - function verifyCompositeSignature( + function _verifyCompositeSignature( bytes32 messageHash, bytes32[] calldata proof, bytes32 merkleRoot, bytes calldata signature, address expectedSigner ) internal view returns (bool) { - if (!verifyMessageInclusion(messageHash, proof, merkleRoot)) { + if (!_verifyMessageInclusion(messageHash, proof, merkleRoot)) { return false; } return recover(merkleRoot, signature) == expectedSigner; } - function verifyMessageInclusion( + function _verifyMessageInclusion( bytes32 messageHash, bytes32[] calldata proof, bytes32 root @@ -158,7 +158,6 @@ contract ExampleVerifier { ); } } - return computedRoot == root; } @@ -185,7 +184,7 @@ contract ExampleVerifier { ### Specification of `eth_signTypedData_v5` JSON RPC method. -This ERC adds a new method `eth_signTypedData_v5` to the 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 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). From 9e7b14ff1492321cb4a166aa496ed2251994ef47 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 22:39:55 -0400 Subject: [PATCH 20/39] updates --- ERCS/erc-7920.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 8ad9d4e4f79..c3b96ef6559 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -63,7 +63,7 @@ To verify that an individual message `mₓ` was included in a composite signatur 2. Compute the leaf node for message `mₓ` and verify the path to the Merkle root using the proof: ``` leaf = keccak256(encode(mₓ)) - isValid = verifyMerkleProof(leaf, merkleProof, merkleRoot) + isValid = _verifyMessageInclusion(leaf, merkleProof, merkleRoot) ``` The message is verified if and only if (1) and (2) succeed. @@ -134,14 +134,14 @@ contract ExampleVerifier { bytes calldata signature, address expectedSigner ) internal view returns (bool) { - if (!_verifyMessageInclusion(messageHash, proof, merkleRoot)) { + if (!_verifyMerkleProof(messageHash, proof, merkleRoot)) { return false; } return recover(merkleRoot, signature) == expectedSigner; } - function _verifyMessageInclusion( + function _verifyMerkleProof( bytes32 messageHash, bytes32[] calldata proof, bytes32 root @@ -246,6 +246,10 @@ Independent verification of messages without knowledge of others This ERC preserves the readability benefits of EIP-712. Giving wallets and users insight into what is being signed. +### Efficient verification on-chain + +`_verifyMerkleProof` has a runtime of `O(log(N))` where N is the number of messages that were signed. + ### Improved Wallet Security Certain messages signed in isolation may appear harmless but combined with may be harmful. Giving the full list of messages to users could help them better navigate their experience. From c6111063476350593ee187b1dfadbf562785645a Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 22:41:03 -0400 Subject: [PATCH 21/39] updates --- assets/erc-7920/eth_signTypedData_v5.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/erc-7920/eth_signTypedData_v5.ts b/assets/erc-7920/eth_signTypedData_v5.ts index 9f04d452711..77cb3a7b396 100644 --- a/assets/erc-7920/eth_signTypedData_v5.ts +++ b/assets/erc-7920/eth_signTypedData_v5.ts @@ -67,7 +67,7 @@ async function eth_signTypedData_v5(args: { } /** - * Recovers the signer of a composite typed data signature. + * Recovers the signer of a composite message. * * This function verifies that a message was included in a composite signature by: * 1. Verifying the Merkle proof against the Merkle root From 54b260af1367e1fd7b6ddacd68d8b388cbde49ae Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 22:46:51 -0400 Subject: [PATCH 22/39] updates --- assets/erc-7920/.gitignore | 50 +++++ assets/erc-7920/contracts/ExampleVerifier.sol | 124 ++++++++++++ assets/erc-7920/hardhat.config.ts | 23 +++ assets/erc-7920/package.json | 47 +++++ .../{ => src}/eth_signTypedData_v5.ts | 0 assets/erc-7920/test/solidity.test.ts | 179 ++++++++++++++++++ assets/erc-7920/tsconfig.json | 11 ++ 7 files changed, 434 insertions(+) create mode 100644 assets/erc-7920/.gitignore create mode 100644 assets/erc-7920/contracts/ExampleVerifier.sol create mode 100644 assets/erc-7920/hardhat.config.ts create mode 100644 assets/erc-7920/package.json rename assets/erc-7920/{ => src}/eth_signTypedData_v5.ts (100%) create mode 100644 assets/erc-7920/test/solidity.test.ts create mode 100644 assets/erc-7920/tsconfig.json diff --git a/assets/erc-7920/.gitignore b/assets/erc-7920/.gitignore new file mode 100644 index 00000000000..93bff94333b --- /dev/null +++ b/assets/erc-7920/.gitignore @@ -0,0 +1,50 @@ +# Dependencies +node_modules +package-lock.json +yarn.lock + +# Hardhat +cache +artifacts +typechain +typechain-types + +# Environment variables +.env +.env.* +!.env.example + +# Coverage +coverage +coverage.json + +# IDE and editors +.idea +.vscode +*.swp +*.swo + +# Build output +dist +build + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Testing +coverage +coverage.json +.nyc_output + +# Deployments (optional, you might want to track these) +# deployments + +# Miscellaneous +.DS_Store +.tmp +temp +.cache diff --git a/assets/erc-7920/contracts/ExampleVerifier.sol b/assets/erc-7920/contracts/ExampleVerifier.sol new file mode 100644 index 00000000000..99686141212 --- /dev/null +++ b/assets/erc-7920/contracts/ExampleVerifier.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +error Unauthorized(); +error NotInTree(); + +contract ExampleVerifier { + bytes32 public immutable DOMAIN_SEPARATOR; + bytes32 private constant MESSAGE_TYPEHASH = + keccak256("PlaceOrder(bytes32 orderId, address user)"); + + constructor() { + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("MyApp")), + keccak256(bytes("1.0.0")), + block.chainid, + address(this) + ) + ); + } + + function placeOrder( + bytes32 orderId, + address user, + bytes calldata signature, + bytes32 merkleRoot, + bytes32[] calldata proof + ) public { + bytes32 messageHash = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256(abi.encode(MESSAGE_TYPEHASH, orderId, user)) + ) + ); + + if ( + !_verifyCompositeSignature( + messageHash, + proof, + merkleRoot, + signature, + user + ) + ) { + revert Unauthorized(); + } + + // DO STUFF + } + + function _verifyCompositeSignature( + bytes32 messageHash, + bytes32[] calldata proof, + bytes32 merkleRoot, + bytes calldata signature, + address expectedSigner + ) internal view returns (bool) { + if (!_verifyMessageInclusion(messageHash, proof, merkleRoot)) { + revert NotInTree(); + } + + return _recover(merkleRoot, signature) == expectedSigner; + } + + function _verifyMessageInclusion( + bytes32 messageHash, + bytes32[] calldata proof, + bytes32 root + ) internal pure returns (bool) { + bytes32 computedRoot = messageHash; + for (uint256 i = 0; i < proof.length; ++i) { + if (computedRoot < proof[i]) { + computedRoot = keccak256( + abi.encodePacked(computedRoot, proof[i]) + ); + } else { + computedRoot = keccak256( + abi.encodePacked(proof[i], computedRoot) + ); + } + } + + return computedRoot == root; + } + + function _recover( + bytes32 digest, + bytes memory signature + ) internal pure returns (address) { + require(signature.length == 65, "Invalid signature length"); + + bytes32 r; + bytes32 s; + uint8 v; + + assembly { + r := mload(add(signature, 32)) + s := mload(add(signature, 64)) + v := byte(0, mload(add(signature, 96))) + } + + return ecrecover(digest, v, r, s); + } + + // Debug function to generate messageHash + function debugGenerateMessageHash( + bytes32 orderId, + address user + ) public view returns (bytes32) { + return + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256(abi.encode(MESSAGE_TYPEHASH, orderId, user)) + ) + ); + } +} diff --git a/assets/erc-7920/hardhat.config.ts b/assets/erc-7920/hardhat.config.ts new file mode 100644 index 00000000000..8b2c7c563f5 --- /dev/null +++ b/assets/erc-7920/hardhat.config.ts @@ -0,0 +1,23 @@ +import { HardhatUserConfig } from "hardhat/config"; +import "@nomicfoundation/hardhat-toolbox"; + +const config: HardhatUserConfig = { + solidity: { + version: "0.8.20", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + networks: {}, + paths: { + sources: "./contracts", + tests: "./test", + cache: "./cache", + artifacts: "./artifacts", + }, +}; + +export default config; diff --git a/assets/erc-7920/package.json b/assets/erc-7920/package.json new file mode 100644 index 00000000000..696a39d3b9e --- /dev/null +++ b/assets/erc-7920/package.json @@ -0,0 +1,47 @@ +{ + "name": "composite-712", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "@ethereumjs/util": "^9.1.0", + "@ethersproject/keccak256": "^5.8.0", + "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", + "@nomicfoundation/hardhat-ethers": "^3.0.6", + "@nomicfoundation/hardhat-ignition": "^0.15.4", + "@nomicfoundation/hardhat-ignition-ethers": "^0.15.0", + "@nomicfoundation/hardhat-network-helpers": "^1.0.0", + "@nomicfoundation/hardhat-toolbox": "^5.0.0", + "@nomicfoundation/hardhat-verify": "^2.0.0", + "@nomicfoundation/ignition-core": "^0.15.4", + "@nomiclabs/hardhat-ethers": "^2.2.3", + "@openzeppelin/contracts": "^5.0.2", + "@openzeppelin/contracts-upgradeable": "^5.0.2", + "@openzeppelin/hardhat-upgrades": "^3.1.1", + "@tenderly/hardhat-tenderly": "^2.2.2", + "@typechain/ethers-v6": "^0.5.0", + "@typechain/hardhat": "^9.0.0", + "@types/chai": "^4.2.0", + "@types/mocha": ">=9.1.0", + "chai": "^4.2.0", + "dotenv": "^16.4.5", + "eth-sig-util": "^3.0.1", + "ethers": "^6.13.5", + "hardhat-gas-reporter": "^1.0.8", + "merkletreejs": "^0.5.1", + "solidity-coverage": "^0.8.1", + "typechain": "^8.3.0", + "web3": "^4.16.0" + }, + "devDependencies": { + "hardhat": "^2.22.4", + "ts-node": "^10.9.2", + "typescript": "^5.4.5" + }, + "scripts": { + "test": "npx hardhat test" + }, + "keywords": [], + "author": "", + "description": "" +} diff --git a/assets/erc-7920/eth_signTypedData_v5.ts b/assets/erc-7920/src/eth_signTypedData_v5.ts similarity index 100% rename from assets/erc-7920/eth_signTypedData_v5.ts rename to assets/erc-7920/src/eth_signTypedData_v5.ts diff --git a/assets/erc-7920/test/solidity.test.ts b/assets/erc-7920/test/solidity.test.ts new file mode 100644 index 00000000000..6ed6eb32e56 --- /dev/null +++ b/assets/erc-7920/test/solidity.test.ts @@ -0,0 +1,179 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { ExampleVerifier } from "../typechain-types"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { MerkleTree } from "merkletreejs"; +import { keccak256 } from "@ethersproject/keccak256"; + +describe("SolidityTests", function () { + let verifier: ExampleVerifier; + let signer: SignerWithAddress; + let otherAccount: SignerWithAddress; + let signerWallet: ethers.Wallet; + let otherWallet: ethers.Wallet; + + const orders = [ + { + orderId: ethers.id("order1"), + user: "0x1234567890123456789012345678901234567890", + }, + { + orderId: ethers.id("order2"), + user: "0x2345678901234567890123456789012345678901", + }, + { + orderId: ethers.id("order3"), + user: "0x3456789012345678901234567890123456789012", + }, + ]; + + async function getOrderHash(order: { + orderId: string; + user: string; + }): Promise { + const contractHash = await verifier.debugGenerateMessageHash( + order.orderId, + order.user + ); + return Buffer.from(contractHash.slice(2), "hex"); + } + + beforeEach(async function () { + const VerifierFactory = await ethers.getContractFactory("ExampleVerifier"); + verifier = await VerifierFactory.deploy(); + await verifier.waitForDeployment(); + + [signer, otherAccount] = await ethers.getSigners(); + + signerWallet = ethers.Wallet.createRandom().connect(ethers.provider); + otherWallet = ethers.Wallet.createRandom().connect(ethers.provider); + + orders[0].user = signerWallet.address; + orders[1].user = signerWallet.address; + orders[2].user = signerWallet.address; + }); + + it("should successfully place an order with a composite signature", async function () { + const orderHashes = await Promise.all(orders.map(getOrderHash)); + const tree = new MerkleTree(orderHashes, keccak256, { + sortPairs: true, + }); + const merkleRoot = `0x${tree.getRoot().toString("hex")}`; + const signature = signerWallet.signingKey.sign(tree.getRoot()).serialized; + + const proof = tree + .getProof(orderHashes[0]) + .map((p) => `0x${p.data.toString("hex")}`); + + await expect( + verifier.placeOrder( + orders[0].orderId, + signerWallet.address, + signature, + merkleRoot, + proof + ) + ).to.not.be.reverted; + }); + + it("should reject an order with invalid proof", async function () { + const orderHashes = await Promise.all(orders.map(getOrderHash)); + const tree = new MerkleTree(orderHashes, keccak256, { + sortPairs: true, + }); + const merkleRoot = `0x${tree.getRoot().toString("hex")}`; + const signature = signerWallet.signingKey.sign(tree.getRoot()).serialized; + + // Use a valid proof but for a different order ID (intentionally wrong) + const invalidOrderId = ethers.id("invalid-order"); + const proof = tree + .getProof(orderHashes[0]) + .map((p) => `0x${p.data.toString("hex")}`); + + await expect( + verifier.placeOrder( + invalidOrderId, + signerWallet.address, + signature, + merkleRoot, + proof + ) + ).to.be.revertedWithCustomError(verifier, "NotInTree"); + }); + + it("should directly sign and verify a single-element merkle tree", async function () { + const order = orders[0]; + const messageHash = await verifier.debugGenerateMessageHash( + order.orderId, + order.user + ); + const leaf = Buffer.from(messageHash.slice(2), "hex"); + const tree = new MerkleTree([leaf], keccak256, { sortPairs: true }); + const merkleRoot = `0x${tree.getRoot().toString("hex")}`; + const signature = signerWallet.signingKey.sign(tree.getRoot()).serialized; + + await expect( + verifier.placeOrder( + order.orderId, + signerWallet.address, + signature, + merkleRoot, + // Proof for a single element tree is empty... + [] as string[] + ) + ).to.not.be.reverted; + }); + + it("should verify all orders with the same signature", async function () { + const orderHashes = await Promise.all(orders.map(getOrderHash)); + const tree = new MerkleTree(orderHashes, keccak256, { + sortPairs: true, + }); + const merkleRoot = `0x${tree.getRoot().toString("hex")}`; + const signature = signerWallet.signingKey.sign(tree.getRoot()).serialized; + + // Verify each order works with the same signature + for (let i = 0; i < orders.length; i++) { + const proof = tree + .getProof(orderHashes[i]) + .map((p) => `0x${p.data.toString("hex")}`); + + await expect( + verifier.placeOrder( + orders[i].orderId, + signerWallet.address, + signature, + merkleRoot, + proof + ) + ).to.not.be.reverted; + } + }); + + it("should reject a signature from a different signer", async function () { + const orderHashes = await Promise.all(orders.map(getOrderHash)); + const tree = new MerkleTree(orderHashes, keccak256, { + sortPairs: true, + }); + const merkleRoot = `0x${tree.getRoot().toString("hex")}`; + + // Sign with the other wallet (different than the one in the order.user field) + const wrongSignature = otherWallet.signingKey.sign( + tree.getRoot() + ).serialized; + + const proof = tree + .getProof(orderHashes[0]) + .map((p) => `0x${p.data.toString("hex")}`); + + await expect( + verifier.placeOrder( + orders[0].orderId, + signerWallet.address, // The actual order owner + wrongSignature, + merkleRoot, + proof + ) + ).to.be.revertedWithCustomError(verifier, "Unauthorized"); + }); +}); diff --git a/assets/erc-7920/tsconfig.json b/assets/erc-7920/tsconfig.json new file mode 100644 index 00000000000..574e785c71e --- /dev/null +++ b/assets/erc-7920/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +} From 3882a894f28626f19c5fe9d13d452e41e8ffe736 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 23:11:30 -0400 Subject: [PATCH 23/39] updates --- ERCS/erc-7920.md | 2 +- assets/erc-7920/src/eth_signTypedData_v5.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index c3b96ef6559..9b266b05921 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -191,7 +191,7 @@ This method returns: the signature, merkle root, and an array of proofs (each co #### Parameters 1. `Address` - Signing account -2. `TypedDataArray` - Array of `TypedData` objects from EIP-712. +2. `TypedData | TypedDataArray` - A single TypedData object or Array of `TypedData` objects from EIP-712. ##### Returns diff --git a/assets/erc-7920/src/eth_signTypedData_v5.ts b/assets/erc-7920/src/eth_signTypedData_v5.ts index 77cb3a7b396..21dc45d1ac9 100644 --- a/assets/erc-7920/src/eth_signTypedData_v5.ts +++ b/assets/erc-7920/src/eth_signTypedData_v5.ts @@ -25,13 +25,16 @@ type MerkleProof = ReadonlyArray<`0x${string}`>; */ async function eth_signTypedData_v5(args: { readonly privateKey: Buffer; - readonly messages: ReadonlyArray; + readonly messages: Eip712TypedData | ReadonlyArray; }): Promise<{ readonly signature: `0x${string}`; readonly merkleRoot: `0x${string}`; readonly proofs: ReadonlyArray; }> { - const { privateKey, messages } = args; + const { privateKey } = args; + const messages = Array.isArray(args.messages) + ? args.messages + : [args.messages]; const messageHashes: ReadonlyArray = messages.map( ({ message, domain, types }) => { const { EIP712Domain, ...typesWithoutDomain } = types; @@ -301,7 +304,7 @@ async function main() { const singleMessage = await eth_signTypedData_v5({ privateKey: Buffer.from(wallet.privateKey.slice(2), "hex"), - messages: [messages[0]], + messages: messages[0], }); const singleMessageSig = sigUtil.signTypedData_v4( From 490386c508673ea9ef9a0ac059f5af0e9ed60a62 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 23:14:14 -0400 Subject: [PATCH 24/39] updates --- ERCS/erc-7920.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 9b266b05921..c4bf661780a 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -260,7 +260,7 @@ Applications can require combination of messages be signed together to enhance s ## 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`. +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 From e1257586c40d87eb4bb8eb14db97674fecadbbea Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 23:18:23 -0400 Subject: [PATCH 25/39] updates --- assets/erc-7920/src/eth_signTypedData_v5.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/erc-7920/src/eth_signTypedData_v5.ts b/assets/erc-7920/src/eth_signTypedData_v5.ts index 21dc45d1ac9..e1dd7b993ec 100644 --- a/assets/erc-7920/src/eth_signTypedData_v5.ts +++ b/assets/erc-7920/src/eth_signTypedData_v5.ts @@ -20,7 +20,7 @@ type MerkleProof = ReadonlyArray<`0x${string}`>; * * @param args - The arguments for the function * @param args.privateKey - The private key to sign with - * @param args.messages - Array of EIP-712 typed data messages to include in the composite signature + * @param args.messages - Single message or a list of EIP-712 typed data messages to include in the composite signature * @returns Object containing the signature, Merkle root, and proofs for each message */ async function eth_signTypedData_v5(args: { From b0cb0027da360b06259ce8587861b612f8989515 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 23:34:56 -0400 Subject: [PATCH 26/39] updates --- ERCS/erc-7920.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index c4bf661780a..9d283fb6425 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -60,7 +60,7 @@ To verify that an individual message `mₓ` was included in a composite signatur isValidSignature = (recoveredSigner == expectedSigner) ``` -2. Compute the leaf node for message `mₓ` and verify the path to the Merkle root using the proof: +2. Compute the leaf node for message `mₓ` and verify it's path to the Merkle root, using the proof: ``` leaf = keccak256(encode(mₓ)) isValid = _verifyMessageInclusion(leaf, merkleProof, merkleRoot) From 11b00c6cae17e2d1ada87d0bfbfe20660672806c Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Thu, 27 Mar 2025 23:53:00 -0400 Subject: [PATCH 27/39] updates --- ERCS/erc-7920.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 9d283fb6425..319c9451392 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -104,7 +104,7 @@ contract ExampleVerifier { bytes32 merkleRoot, bytes32[] calldata proof ) public { - bytes32 messageHash = keccak256( + bytes32 message = keccak256( abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR, @@ -114,7 +114,7 @@ contract ExampleVerifier { if ( !_verifyCompositeSignature( - messageHash, + message, proof, merkleRoot, signature, @@ -128,13 +128,13 @@ contract ExampleVerifier { } function _verifyCompositeSignature( - bytes32 messageHash, + bytes32 message, bytes32[] calldata proof, bytes32 merkleRoot, bytes calldata signature, address expectedSigner ) internal view returns (bool) { - if (!_verifyMerkleProof(messageHash, proof, merkleRoot)) { + if (!_verifyMerkleProof(message, proof, merkleRoot)) { return false; } @@ -142,11 +142,11 @@ contract ExampleVerifier { } function _verifyMerkleProof( - bytes32 messageHash, + bytes32 leaf, bytes32[] calldata proof, bytes32 root ) internal pure returns (bool) { - bytes32 computedRoot = messageHash; + bytes32 computedRoot = leaf; for (uint256 i = 0; i < proof.length; ++i) { if (computedRoot < proof[i]) { computedRoot = keccak256( From 473672a4e4ccbe9118c567c25ad8f4ff8dd96780 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Fri, 28 Mar 2025 00:16:24 -0400 Subject: [PATCH 28/39] updates --- ERCS/erc-7920.md | 2 +- assets/erc-7920/contracts/ExampleVerifier.sol | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 319c9451392..6c38723ce4b 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -63,7 +63,7 @@ To verify that an individual message `mₓ` was included in a composite signatur 2. Compute the leaf node for message `mₓ` and verify it's path to the Merkle root, using the proof: ``` leaf = keccak256(encode(mₓ)) - isValid = _verifyMessageInclusion(leaf, merkleProof, merkleRoot) + isValid = _verifyMerkleProof(leaf, merkleProof, merkleRoot) ``` The message is verified if and only if (1) and (2) succeed. diff --git a/assets/erc-7920/contracts/ExampleVerifier.sol b/assets/erc-7920/contracts/ExampleVerifier.sol index 99686141212..ea0f6ae650e 100644 --- a/assets/erc-7920/contracts/ExampleVerifier.sol +++ b/assets/erc-7920/contracts/ExampleVerifier.sol @@ -30,7 +30,7 @@ contract ExampleVerifier { bytes32 merkleRoot, bytes32[] calldata proof ) public { - bytes32 messageHash = keccak256( + bytes32 message = keccak256( abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR, @@ -40,7 +40,7 @@ contract ExampleVerifier { if ( !_verifyCompositeSignature( - messageHash, + message, proof, merkleRoot, signature, @@ -54,25 +54,25 @@ contract ExampleVerifier { } function _verifyCompositeSignature( - bytes32 messageHash, + bytes32 message, bytes32[] calldata proof, bytes32 merkleRoot, bytes calldata signature, address expectedSigner ) internal view returns (bool) { - if (!_verifyMessageInclusion(messageHash, proof, merkleRoot)) { + if (!_verifyMerkleProof(message, proof, merkleRoot)) { revert NotInTree(); } return _recover(merkleRoot, signature) == expectedSigner; } - function _verifyMessageInclusion( - bytes32 messageHash, + function _verifyMerkleProof( + bytes32 message, bytes32[] calldata proof, bytes32 root ) internal pure returns (bool) { - bytes32 computedRoot = messageHash; + bytes32 computedRoot = message; for (uint256 i = 0; i < proof.length; ++i) { if (computedRoot < proof[i]) { computedRoot = keccak256( @@ -107,7 +107,7 @@ contract ExampleVerifier { return ecrecover(digest, v, r, s); } - // Debug function to generate messageHash + // Debug function to generate message function debugGenerateMessageHash( bytes32 orderId, address user From 070522349adb0d16a10de6c9b3303adce8f8ca41 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Fri, 28 Mar 2025 00:18:48 -0400 Subject: [PATCH 29/39] updates --- ERCS/erc-7920.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 6c38723ce4b..28d86f73db8 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -63,11 +63,15 @@ To verify that an individual message `mₓ` was included in a composite signatur 2. Compute the leaf node for message `mₓ` and verify it's path to the Merkle root, using the proof: ``` leaf = keccak256(encode(mₓ)) - isValid = _verifyMerkleProof(leaf, merkleProof, merkleRoot) + isValidProof = _verifyMerkleProof(leaf, merkleProof, merkleRoot) ``` The message is verified if and only if (1) and (2) succeed. +``` +isVerified = isValidSignature && isValidProof +``` + #### Solidity Example Snippet below verifies a composite signature on-chain. The entrypoint, `placeOrder()` is called by a paymaster to sponsor an operation. It verifies the composite signature using the process outlined above. From 47a76f8d37e32c4ae3538dedc4946c4a971e6bf9 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Sun, 30 Mar 2025 22:33:30 -0400 Subject: [PATCH 30/39] updates --- ERCS/erc-7920.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 28d86f73db8..beea2f70667 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -19,7 +19,7 @@ This ERC also gives applications the flexibility to verify messages in isolation ## 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 x 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. +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: From f23f0a48e73f32fd1cc14dbf8a9965bd40824095 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Sat, 12 Apr 2025 14:11:33 -0400 Subject: [PATCH 31/39] - Fix typos - Replace merkletreejs with reference implementation that follows ERC-7920 specification. - `abi.encodePacked` -> `abi.encode` - Add tests for the Merkle reference implementation - update spec to harden N=1 case --- ERCS/erc-7920.md | 86 ++++++++++++++++--- assets/erc-7920/contracts/ExampleVerifier.sol | 16 ++-- assets/erc-7920/src/eth_signTypedData_v5.ts | 9 +- assets/erc-7920/src/merkle.ts | 56 ++++++++++++ assets/erc-7920/test/merkle.test.ts | 75 ++++++++++++++++ assets/erc-7920/test/solidity.test.ts | 29 +++---- 6 files changed, 225 insertions(+), 46 deletions(-) create mode 100644 assets/erc-7920/src/merkle.ts create mode 100644 assets/erc-7920/test/merkle.test.ts diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index beea2f70667..6cd9db73ce7 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -41,12 +41,12 @@ The composite signature scheme uses a Merkle tree to hash multiple typed-data da hashₙ = keccak256(encode(mₙ)) ``` -2. Use these message hashes as leaf nodes in a Merkle tree and compute a `marketRoot` +2. Use these message hashes as leaf nodes in a Merkle tree and compute a `merkleRoot` 3. Sign the merkle root. ``` - signature = sign(marketRoot) + signature = sign(merkleRoot) ``` ### Verification Process @@ -109,7 +109,7 @@ contract ExampleVerifier { bytes32[] calldata proof ) public { bytes32 message = keccak256( - abi.encodePacked( + abi.encode( "\x19\x01", DOMAIN_SEPARATOR, keccak256(abi.encode(MESSAGE_TYPEHASH, orderId, user)) @@ -153,13 +153,9 @@ contract ExampleVerifier { bytes32 computedRoot = leaf; for (uint256 i = 0; i < proof.length; ++i) { if (computedRoot < proof[i]) { - computedRoot = keccak256( - abi.encodePacked(computedRoot, proof[i]) - ); + computedRoot = keccak256(abi.encode(computedRoot, proof[i])); } else { - computedRoot = keccak256( - abi.encodePacked(proof[i], computedRoot) - ); + computedRoot = keccak256(abi.encode(proof[i], computedRoot)); } } return computedRoot == root; @@ -252,7 +248,9 @@ This ERC preserves the readability benefits of EIP-712. Giving wallets and users ### Efficient verification on-chain -`_verifyMerkleProof` has a runtime of `O(log(N))` where N is the number of messages that were signed. +`_verifyMerkleProof` has a runtime of `O(log2(N))` where N is the number of messages that were signed. + +When N=1, merkleRoot **must** equal keccak256(encode(m₁)) and proofs MUST be empty arrays ### Improved Wallet Security @@ -285,14 +283,78 @@ During verification, care **must** be taken to ensure that **both** of these che ### User Understanding -Wallets **must** communicate to users that they are signing multiple messages at once. They should provide a way for users to review all messages included in the composite signature. +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 max number of messages is at most 10. ### Merkle Tree Construction Merkle tree should be constructed in a consistent manner. 1. The hashing function **must** be `keccak256` -2. Hash pairs **must** be sorted lexicographically to simplify verification +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. + +#### Tree Reference Implementation + +```typescript +import { keccak256 } from "@ethersproject/keccak256"; + +export function _keccak256(data: Buffer): Buffer { + return Buffer.from(keccak256(data).slice(2), "hex"); +} + +export class MerkleTree { + private readonly root: Buffer; + private readonly messages: Buffer[]; + private readonly levels: Buffer[][]; + + constructor(messages: Buffer[]) { + let k = Math.ceil(Math.log2(messages.length)); + for (let i = messages.length; i < 1 << k; i++) { + messages.push(Buffer.alloc(messages[0].length)); + } + this.messages = messages; + + let currentLevel = this.messages; + this.levels = [currentLevel]; + while (currentLevel.length > 1) { + const nextLevel = []; + for (let i = 0; i < currentLevel.length; i += 2) { + const pair = + currentLevel[i].compare(currentLevel[i + 1]) < 0 + ? [currentLevel[i], currentLevel[i + 1]] + : [currentLevel[i + 1], currentLevel[i]]; + nextLevel.push(_keccak256(Buffer.concat(pair))); + } + currentLevel = nextLevel; + this.levels.push(nextLevel); + } + this.root = currentLevel[0]; + } + + getProof(message: Buffer): readonly Buffer[] { + // ceil(8/2)-1 + let index = this.messages.findIndex((m) => m.compare(message) === 0); + if (index === -1) { + throw new Error("Message not found"); + } + let levelIndex = 0; + let level = this.levels[0]; + let proof: Buffer[] = []; + while (level.length > 1) { + proof.push(level[index ^ 1]); + index = Math.ceil((index + 1) / 2) - 1; + level = this.levels[++levelIndex]; + } + return proof as readonly Buffer[]; + } + + getRoot(): Buffer { + return this.root; + } +} +``` ## Copyright diff --git a/assets/erc-7920/contracts/ExampleVerifier.sol b/assets/erc-7920/contracts/ExampleVerifier.sol index ea0f6ae650e..8460089eeb5 100644 --- a/assets/erc-7920/contracts/ExampleVerifier.sol +++ b/assets/erc-7920/contracts/ExampleVerifier.sol @@ -31,7 +31,7 @@ contract ExampleVerifier { bytes32[] calldata proof ) public { bytes32 message = keccak256( - abi.encodePacked( + abi.encode( "\x19\x01", DOMAIN_SEPARATOR, keccak256(abi.encode(MESSAGE_TYPEHASH, orderId, user)) @@ -68,20 +68,16 @@ contract ExampleVerifier { } function _verifyMerkleProof( - bytes32 message, + bytes32 leaf, bytes32[] calldata proof, bytes32 root ) internal pure returns (bool) { - bytes32 computedRoot = message; + bytes32 computedRoot = leaf; for (uint256 i = 0; i < proof.length; ++i) { if (computedRoot < proof[i]) { - computedRoot = keccak256( - abi.encodePacked(computedRoot, proof[i]) - ); + computedRoot = keccak256(abi.encode(computedRoot, proof[i])); } else { - computedRoot = keccak256( - abi.encodePacked(proof[i], computedRoot) - ); + computedRoot = keccak256(abi.encode(proof[i], computedRoot)); } } @@ -114,7 +110,7 @@ contract ExampleVerifier { ) public view returns (bytes32) { return keccak256( - abi.encodePacked( + abi.encode( "\x19\x01", DOMAIN_SEPARATOR, keccak256(abi.encode(MESSAGE_TYPEHASH, orderId, user)) diff --git a/assets/erc-7920/src/eth_signTypedData_v5.ts b/assets/erc-7920/src/eth_signTypedData_v5.ts index e1dd7b993ec..707024e9e99 100644 --- a/assets/erc-7920/src/eth_signTypedData_v5.ts +++ b/assets/erc-7920/src/eth_signTypedData_v5.ts @@ -1,5 +1,4 @@ import { ethers } from "ethers"; -import { MerkleTree } from "merkletreejs"; import { keccak256 } from "@ethersproject/keccak256"; import { Eip712TypedData } from "web3"; import { @@ -10,6 +9,8 @@ import { } from "@ethereumjs/util"; import * as sigUtil from "eth-sig-util"; +import { MerkleTree } from "./merkle"; + type MerkleProof = ReadonlyArray<`0x${string}`>; /** @@ -48,9 +49,7 @@ async function eth_signTypedData_v5(args: { } ); - const tree = new MerkleTree(messageHashes as Array, keccak256, { - sortPairs: true, - }); + const tree = new MerkleTree(messageHashes as Array); const merkleRoot = tree.getRoot(); const wallet = new ethers.Wallet(`0x${privateKey.toString("hex")}`); @@ -59,7 +58,7 @@ async function eth_signTypedData_v5(args: { const proofs: ReadonlyArray = messageHashes.map((hash) => tree .getProof(hash) - .map((proof) => `0x${proof.data.toString("hex")}` as `0x${string}`) + .map((proof) => `0x${proof.toString("hex")}` as `0x${string}`) ); return { diff --git a/assets/erc-7920/src/merkle.ts b/assets/erc-7920/src/merkle.ts new file mode 100644 index 00000000000..5232454fbfe --- /dev/null +++ b/assets/erc-7920/src/merkle.ts @@ -0,0 +1,56 @@ +import { keccak256 } from "@ethersproject/keccak256"; + +export function _keccak256(data: Buffer): Buffer { + return Buffer.from(keccak256(data).slice(2), "hex"); +} + +export class MerkleTree { + private readonly root: Buffer; + private readonly messages: Buffer[]; + private readonly levels: Buffer[][]; + + constructor(messages: Buffer[]) { + let k = Math.ceil(Math.log2(messages.length)); + for (let i = messages.length; i < 1 << k; i++) { + messages.push(Buffer.alloc(messages[0].length)); + } + this.messages = messages; + + let currentLevel = this.messages; + this.levels = [currentLevel]; + while (currentLevel.length > 1) { + const nextLevel = []; + for (let i = 0; i < currentLevel.length; i += 2) { + const pair = + currentLevel[i].compare(currentLevel[i + 1]) < 0 + ? [currentLevel[i], currentLevel[i + 1]] + : [currentLevel[i + 1], currentLevel[i]]; + nextLevel.push(_keccak256(Buffer.concat(pair))); + } + currentLevel = nextLevel; + this.levels.push(nextLevel); + } + this.root = currentLevel[0]; + } + + getProof(message: Buffer): readonly Buffer[] { + // ceil(8/2)-1 + let index = this.messages.findIndex((m) => m.compare(message) === 0); + if (index === -1) { + throw new Error("Message not found"); + } + let levelIndex = 0; + let level = this.levels[0]; + let proof: Buffer[] = []; + while (level.length > 1) { + proof.push(level[index ^ 1]); + index = Math.ceil((index + 1) / 2) - 1; + level = this.levels[++levelIndex]; + } + return proof as readonly Buffer[]; + } + + getRoot(): Buffer { + return this.root; + } +} diff --git a/assets/erc-7920/test/merkle.test.ts b/assets/erc-7920/test/merkle.test.ts new file mode 100644 index 00000000000..266b0973593 --- /dev/null +++ b/assets/erc-7920/test/merkle.test.ts @@ -0,0 +1,75 @@ +import { expect } from "chai"; +import { MerkleTree, _keccak256 } from "../src/merkle"; +import { randomBytes } from "crypto"; + +describe("MerkleTree", function () { + function createRandomMessages(count: number, size: number = 32): Buffer[] { + return Array.from({ length: count }, () => randomBytes(size)); + } + + describe("Proof Size Tests", function () { + it("should have empty proof for single element tree", function () { + const messages = createRandomMessages(1); + const tree = new MerkleTree(messages); + const proof = tree.getProof(messages[0]); + + expect(proof).to.be.an("array").that.is.empty; + }); + + it("should have ceil(log2(n)) proof elements for even message count", function () { + const testCases = [2, 4, 6, 8, 10]; + + for (const count of testCases) { + const messages = createRandomMessages(count); + const tree = new MerkleTree(messages); + + for (const message of messages) { + const proof = tree.getProof(message); + const expectedProofSize = Math.ceil(Math.log2(count)); + + expect(proof).to.have.lengthOf( + expectedProofSize, + `Proof length for ${count} messages should be ${expectedProofSize}` + ); + } + } + }); + + it("should have ceil(log2(n)) proof elements for odd message count", function () { + const testCases = [3, 5, 7, 9, 15]; + + for (const count of testCases) { + const messages = createRandomMessages(count); + const tree = new MerkleTree(messages); + + for (const message of messages) { + const proof = tree.getProof(message); + const expectedProofSize = Math.ceil(Math.log2(count)); + + expect(proof).to.have.lengthOf( + expectedProofSize, + `Proof length for ${count} messages should be ${expectedProofSize}` + ); + } + } + }); + + it("should have consistent proof size for all elements in the same tree", function () { + for (const count of [3, 5, 8, 10]) { + const messages = createRandomMessages(count); + const tree = new MerkleTree(messages); + + const proofLengths = new Set(); + for (const message of messages) { + const proof = tree.getProof(message); + proofLengths.add(proof.length); + } + + expect(proofLengths.size).to.equal( + 1, + `All proofs for ${count} messages should have the same length` + ); + } + }); + }); +}); diff --git a/assets/erc-7920/test/solidity.test.ts b/assets/erc-7920/test/solidity.test.ts index 6ed6eb32e56..506a3c10c16 100644 --- a/assets/erc-7920/test/solidity.test.ts +++ b/assets/erc-7920/test/solidity.test.ts @@ -2,8 +2,7 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { ExampleVerifier } from "../typechain-types"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; -import { MerkleTree } from "merkletreejs"; -import { keccak256 } from "@ethersproject/keccak256"; +import { MerkleTree } from "../src/merkle"; describe("SolidityTests", function () { let verifier: ExampleVerifier; @@ -55,15 +54,13 @@ describe("SolidityTests", function () { it("should successfully place an order with a composite signature", async function () { const orderHashes = await Promise.all(orders.map(getOrderHash)); - const tree = new MerkleTree(orderHashes, keccak256, { - sortPairs: true, - }); + const tree = new MerkleTree(orderHashes); const merkleRoot = `0x${tree.getRoot().toString("hex")}`; const signature = signerWallet.signingKey.sign(tree.getRoot()).serialized; const proof = tree .getProof(orderHashes[0]) - .map((p) => `0x${p.data.toString("hex")}`); + .map((p) => `0x${p.toString("hex")}`); await expect( verifier.placeOrder( @@ -78,9 +75,7 @@ describe("SolidityTests", function () { it("should reject an order with invalid proof", async function () { const orderHashes = await Promise.all(orders.map(getOrderHash)); - const tree = new MerkleTree(orderHashes, keccak256, { - sortPairs: true, - }); + const tree = new MerkleTree(orderHashes); const merkleRoot = `0x${tree.getRoot().toString("hex")}`; const signature = signerWallet.signingKey.sign(tree.getRoot()).serialized; @@ -88,7 +83,7 @@ describe("SolidityTests", function () { const invalidOrderId = ethers.id("invalid-order"); const proof = tree .getProof(orderHashes[0]) - .map((p) => `0x${p.data.toString("hex")}`); + .map((p) => `0x${p.toString("hex")}`); await expect( verifier.placeOrder( @@ -108,7 +103,7 @@ describe("SolidityTests", function () { order.user ); const leaf = Buffer.from(messageHash.slice(2), "hex"); - const tree = new MerkleTree([leaf], keccak256, { sortPairs: true }); + const tree = new MerkleTree([leaf]); const merkleRoot = `0x${tree.getRoot().toString("hex")}`; const signature = signerWallet.signingKey.sign(tree.getRoot()).serialized; @@ -126,9 +121,7 @@ describe("SolidityTests", function () { it("should verify all orders with the same signature", async function () { const orderHashes = await Promise.all(orders.map(getOrderHash)); - const tree = new MerkleTree(orderHashes, keccak256, { - sortPairs: true, - }); + const tree = new MerkleTree(orderHashes); const merkleRoot = `0x${tree.getRoot().toString("hex")}`; const signature = signerWallet.signingKey.sign(tree.getRoot()).serialized; @@ -136,7 +129,7 @@ describe("SolidityTests", function () { for (let i = 0; i < orders.length; i++) { const proof = tree .getProof(orderHashes[i]) - .map((p) => `0x${p.data.toString("hex")}`); + .map((p) => `0x${p.toString("hex")}`); await expect( verifier.placeOrder( @@ -152,9 +145,7 @@ describe("SolidityTests", function () { it("should reject a signature from a different signer", async function () { const orderHashes = await Promise.all(orders.map(getOrderHash)); - const tree = new MerkleTree(orderHashes, keccak256, { - sortPairs: true, - }); + const tree = new MerkleTree(orderHashes); const merkleRoot = `0x${tree.getRoot().toString("hex")}`; // Sign with the other wallet (different than the one in the order.user field) @@ -164,7 +155,7 @@ describe("SolidityTests", function () { const proof = tree .getProof(orderHashes[0]) - .map((p) => `0x${p.data.toString("hex")}`); + .map((p) => `0x${p.toString("hex")}`); await expect( verifier.placeOrder( From e306df0707f4b13c30798bd0a75f9f4b77cff6e7 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Sat, 12 Apr 2025 19:33:32 -0400 Subject: [PATCH 32/39] updates --- ERCS/erc-7920.md | 4 +--- assets/erc-7920/src/merkle.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 6cd9db73ce7..94a06abf530 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -305,7 +305,6 @@ export function _keccak256(data: Buffer): Buffer { } export class MerkleTree { - private readonly root: Buffer; private readonly messages: Buffer[]; private readonly levels: Buffer[][]; @@ -330,7 +329,6 @@ export class MerkleTree { currentLevel = nextLevel; this.levels.push(nextLevel); } - this.root = currentLevel[0]; } getProof(message: Buffer): readonly Buffer[] { @@ -351,7 +349,7 @@ export class MerkleTree { } getRoot(): Buffer { - return this.root; + return this.levels[this.levels.length - 1][0]; } } ``` diff --git a/assets/erc-7920/src/merkle.ts b/assets/erc-7920/src/merkle.ts index 5232454fbfe..cd861117117 100644 --- a/assets/erc-7920/src/merkle.ts +++ b/assets/erc-7920/src/merkle.ts @@ -5,7 +5,6 @@ export function _keccak256(data: Buffer): Buffer { } export class MerkleTree { - private readonly root: Buffer; private readonly messages: Buffer[]; private readonly levels: Buffer[][]; @@ -30,7 +29,6 @@ export class MerkleTree { currentLevel = nextLevel; this.levels.push(nextLevel); } - this.root = currentLevel[0]; } getProof(message: Buffer): readonly Buffer[] { @@ -51,6 +49,6 @@ export class MerkleTree { } getRoot(): Buffer { - return this.root; + return this.levels[this.levels.length - 1][0]; } } From 35ad9fc2d8db73f0e5c746d6db1d467d635a07c7 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Sat, 12 Apr 2025 19:36:58 -0400 Subject: [PATCH 33/39] updates --- ERCS/erc-7920.md | 3 ++- assets/erc-7920/src/merkle.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 94a06abf530..b160a97dfc5 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -308,7 +308,8 @@ export class MerkleTree { private readonly messages: Buffer[]; private readonly levels: Buffer[][]; - constructor(messages: Buffer[]) { + constructor(_messages: readonly Buffer[]) { + const messages = [..._messages]; let k = Math.ceil(Math.log2(messages.length)); for (let i = messages.length; i < 1 << k; i++) { messages.push(Buffer.alloc(messages[0].length)); diff --git a/assets/erc-7920/src/merkle.ts b/assets/erc-7920/src/merkle.ts index cd861117117..5e9ae736d1e 100644 --- a/assets/erc-7920/src/merkle.ts +++ b/assets/erc-7920/src/merkle.ts @@ -8,7 +8,8 @@ export class MerkleTree { private readonly messages: Buffer[]; private readonly levels: Buffer[][]; - constructor(messages: Buffer[]) { + constructor(_messages: readonly Buffer[]) { + const messages = [..._messages]; let k = Math.ceil(Math.log2(messages.length)); for (let i = messages.length; i < 1 << k; i++) { messages.push(Buffer.alloc(messages[0].length)); From 171c1ccd8ee9fa533c74ad4b07fd02a854d9f978 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Fri, 18 Apr 2025 10:50:26 -0400 Subject: [PATCH 34/39] updates --- ERCS/erc-7920.md | 6 ++---- assets/erc-7920/src/merkle.ts | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index b160a97dfc5..a0056385389 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -305,7 +305,6 @@ export function _keccak256(data: Buffer): Buffer { } export class MerkleTree { - private readonly messages: Buffer[]; private readonly levels: Buffer[][]; constructor(_messages: readonly Buffer[]) { @@ -314,9 +313,8 @@ export class MerkleTree { for (let i = messages.length; i < 1 << k; i++) { messages.push(Buffer.alloc(messages[0].length)); } - this.messages = messages; - let currentLevel = this.messages; + let currentLevel = messages; this.levels = [currentLevel]; while (currentLevel.length > 1) { const nextLevel = []; @@ -334,7 +332,7 @@ export class MerkleTree { getProof(message: Buffer): readonly Buffer[] { // ceil(8/2)-1 - let index = this.messages.findIndex((m) => m.compare(message) === 0); + let index = this.levels[0].findIndex((m) => m.compare(message) === 0); if (index === -1) { throw new Error("Message not found"); } diff --git a/assets/erc-7920/src/merkle.ts b/assets/erc-7920/src/merkle.ts index 5e9ae736d1e..1d27e214b6a 100644 --- a/assets/erc-7920/src/merkle.ts +++ b/assets/erc-7920/src/merkle.ts @@ -5,7 +5,6 @@ export function _keccak256(data: Buffer): Buffer { } export class MerkleTree { - private readonly messages: Buffer[]; private readonly levels: Buffer[][]; constructor(_messages: readonly Buffer[]) { @@ -14,9 +13,8 @@ export class MerkleTree { for (let i = messages.length; i < 1 << k; i++) { messages.push(Buffer.alloc(messages[0].length)); } - this.messages = messages; - let currentLevel = this.messages; + let currentLevel = messages; this.levels = [currentLevel]; while (currentLevel.length > 1) { const nextLevel = []; @@ -34,7 +32,7 @@ export class MerkleTree { getProof(message: Buffer): readonly Buffer[] { // ceil(8/2)-1 - let index = this.messages.findIndex((m) => m.compare(message) === 0); + let index = this.levels[0].findIndex((m) => m.compare(message) === 0); if (index === -1) { throw new Error("Message not found"); } From bf41d3e0560a566ac7063bf913d55d52f76e3ef2 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Sun, 4 May 2025 14:11:45 -0400 Subject: [PATCH 35/39] updates --- ERCS/erc-7920.md | 216 +++++++++++------- assets/erc-7920/.gitignore | 50 ---- assets/erc-7920/contracts/ExampleVerifier.sol | 2 +- assets/erc-7920/erc-7920.png | Bin 36805 -> 0 bytes assets/erc-7920/hardhat.config.ts | 23 -- assets/erc-7920/package.json | 47 ---- assets/erc-7920/tsconfig.json | 11 - 7 files changed, 140 insertions(+), 209 deletions(-) delete mode 100644 assets/erc-7920/.gitignore delete mode 100644 assets/erc-7920/erc-7920.png delete mode 100644 assets/erc-7920/hardhat.config.ts delete mode 100644 assets/erc-7920/package.json delete mode 100644 assets/erc-7920/tsconfig.json diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index a0056385389..17ee36d83c0 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -1,7 +1,7 @@ --- eip: 7920 title: Composite EIP-712 Signatures -description: Composite EIP-712 signatures using merkle trees. +description: A standard 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 @@ -13,7 +13,7 @@ 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). +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`). @@ -60,7 +60,7 @@ To verify that an individual message `mₓ` was included in a composite signatur isValidSignature = (recoveredSigner == expectedSigner) ``` -2. Compute the leaf node for message `mₓ` and verify it's path to the Merkle root, using the proof: +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) @@ -77,7 +77,7 @@ isVerified = isValidSignature && isValidProof Snippet below verifies a composite signature on-chain. The entrypoint, `placeOrder()` is called by a paymaster to sponsor an operation. It verifies the composite signature using the process outlined above. ```solidity -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.20; error Unauthorized(); @@ -195,15 +195,11 @@ This method returns: the signature, merkle root, and an array of proofs (each co ##### Returns -```JavaScript +```Typescript { - signature: 'DATA', // Hex encoded 65 byte signature (same format as eth_sign) - merkleRoot: 'DATA', // 32 byte Merkle root as hex string - proofs: [ // Array of Merkle proofs (one for each input message) - ['DATA', 'DATA'], // First message proof (array of 32 byte hex strings) - ['DATA', 'DATA'], // Second message proof - ... - ] + 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) } ``` @@ -211,8 +207,124 @@ This method returns: the signature, merkle root, and an array of proofs (each co Request: -```shell -curl -X POST --data '{"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}' +```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: @@ -250,8 +362,6 @@ This ERC preserves the readability benefits of EIP-712. Giving wallets and users `_verifyMerkleProof` has a runtime of `O(log2(N))` where N is the number of messages that were signed. -When N=1, merkleRoot **must** equal keccak256(encode(m₁)) and proofs MUST be empty arrays - ### Improved Wallet Security Certain messages signed in isolation may appear harmless but combined with may be harmful. Giving the full list of messages to users could help them better navigate their experience. @@ -264,9 +374,19 @@ Applications can require combination of messages be signed together to enhance s 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 +## Reference Implementations + +### `eth_signTypedData_v5` + +Reference implementation of `eth_signTypedData_v5` can be found the [assets directory](../assets/erc-7920/src/eth_signTypedData_v5.ts). + +### Verifier + +Solidity implementation of a onchain verifier can be found the [assets directory](../assets/erc-7920/contracts/ExampleVerifier.sol). + +### Merkle -Reference implementation of `eth_signTypedData_v5` can be found the assets directory. +Reference Merkle tree can be found in the [assets directory](../assets/erc-7920/src/merkle.ts). ## Security Considerations @@ -285,7 +405,7 @@ During verification, care **must** be taken to ensure that **both** of these che 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 max number of messages is at most 10. +To ensure batch signature requests are digestible, it is recommended to limit the maximum number of messages to 10. ### Merkle Tree Construction @@ -295,64 +415,6 @@ Merkle tree should be constructed in a consistent manner. 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. -#### Tree Reference Implementation - -```typescript -import { keccak256 } from "@ethersproject/keccak256"; - -export function _keccak256(data: Buffer): Buffer { - return Buffer.from(keccak256(data).slice(2), "hex"); -} - -export class MerkleTree { - private readonly levels: Buffer[][]; - - constructor(_messages: readonly Buffer[]) { - const messages = [..._messages]; - let k = Math.ceil(Math.log2(messages.length)); - for (let i = messages.length; i < 1 << k; i++) { - messages.push(Buffer.alloc(messages[0].length)); - } - - let currentLevel = messages; - this.levels = [currentLevel]; - while (currentLevel.length > 1) { - const nextLevel = []; - for (let i = 0; i < currentLevel.length; i += 2) { - const pair = - currentLevel[i].compare(currentLevel[i + 1]) < 0 - ? [currentLevel[i], currentLevel[i + 1]] - : [currentLevel[i + 1], currentLevel[i]]; - nextLevel.push(_keccak256(Buffer.concat(pair))); - } - currentLevel = nextLevel; - this.levels.push(nextLevel); - } - } - - getProof(message: Buffer): readonly Buffer[] { - // ceil(8/2)-1 - let index = this.levels[0].findIndex((m) => m.compare(message) === 0); - if (index === -1) { - throw new Error("Message not found"); - } - let levelIndex = 0; - let level = this.levels[0]; - let proof: Buffer[] = []; - while (level.length > 1) { - proof.push(level[index ^ 1]); - index = Math.ceil((index + 1) / 2) - 1; - level = this.levels[++levelIndex]; - } - return proof as readonly Buffer[]; - } - - getRoot(): Buffer { - return this.levels[this.levels.length - 1][0]; - } -} -``` - ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/erc-7920/.gitignore b/assets/erc-7920/.gitignore deleted file mode 100644 index 93bff94333b..00000000000 --- a/assets/erc-7920/.gitignore +++ /dev/null @@ -1,50 +0,0 @@ -# Dependencies -node_modules -package-lock.json -yarn.lock - -# Hardhat -cache -artifacts -typechain -typechain-types - -# Environment variables -.env -.env.* -!.env.example - -# Coverage -coverage -coverage.json - -# IDE and editors -.idea -.vscode -*.swp -*.swo - -# Build output -dist -build - -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Testing -coverage -coverage.json -.nyc_output - -# Deployments (optional, you might want to track these) -# deployments - -# Miscellaneous -.DS_Store -.tmp -temp -.cache diff --git a/assets/erc-7920/contracts/ExampleVerifier.sol b/assets/erc-7920/contracts/ExampleVerifier.sol index 8460089eeb5..2e245c6e180 100644 --- a/assets/erc-7920/contracts/ExampleVerifier.sol +++ b/assets/erc-7920/contracts/ExampleVerifier.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.20; error Unauthorized(); diff --git a/assets/erc-7920/erc-7920.png b/assets/erc-7920/erc-7920.png deleted file mode 100644 index af5526fa39a123356e16ed2b25588f6c4f2b6a42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36805 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 G_WuAD7HByD diff --git a/assets/erc-7920/hardhat.config.ts b/assets/erc-7920/hardhat.config.ts deleted file mode 100644 index 8b2c7c563f5..00000000000 --- a/assets/erc-7920/hardhat.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { HardhatUserConfig } from "hardhat/config"; -import "@nomicfoundation/hardhat-toolbox"; - -const config: HardhatUserConfig = { - solidity: { - version: "0.8.20", - settings: { - optimizer: { - enabled: true, - runs: 200, - }, - }, - }, - networks: {}, - paths: { - sources: "./contracts", - tests: "./test", - cache: "./cache", - artifacts: "./artifacts", - }, -}; - -export default config; diff --git a/assets/erc-7920/package.json b/assets/erc-7920/package.json deleted file mode 100644 index 696a39d3b9e..00000000000 --- a/assets/erc-7920/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "composite-712", - "version": "1.0.0", - "main": "index.js", - "license": "MIT", - "dependencies": { - "@ethereumjs/util": "^9.1.0", - "@ethersproject/keccak256": "^5.8.0", - "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", - "@nomicfoundation/hardhat-ethers": "^3.0.6", - "@nomicfoundation/hardhat-ignition": "^0.15.4", - "@nomicfoundation/hardhat-ignition-ethers": "^0.15.0", - "@nomicfoundation/hardhat-network-helpers": "^1.0.0", - "@nomicfoundation/hardhat-toolbox": "^5.0.0", - "@nomicfoundation/hardhat-verify": "^2.0.0", - "@nomicfoundation/ignition-core": "^0.15.4", - "@nomiclabs/hardhat-ethers": "^2.2.3", - "@openzeppelin/contracts": "^5.0.2", - "@openzeppelin/contracts-upgradeable": "^5.0.2", - "@openzeppelin/hardhat-upgrades": "^3.1.1", - "@tenderly/hardhat-tenderly": "^2.2.2", - "@typechain/ethers-v6": "^0.5.0", - "@typechain/hardhat": "^9.0.0", - "@types/chai": "^4.2.0", - "@types/mocha": ">=9.1.0", - "chai": "^4.2.0", - "dotenv": "^16.4.5", - "eth-sig-util": "^3.0.1", - "ethers": "^6.13.5", - "hardhat-gas-reporter": "^1.0.8", - "merkletreejs": "^0.5.1", - "solidity-coverage": "^0.8.1", - "typechain": "^8.3.0", - "web3": "^4.16.0" - }, - "devDependencies": { - "hardhat": "^2.22.4", - "ts-node": "^10.9.2", - "typescript": "^5.4.5" - }, - "scripts": { - "test": "npx hardhat test" - }, - "keywords": [], - "author": "", - "description": "" -} diff --git a/assets/erc-7920/tsconfig.json b/assets/erc-7920/tsconfig.json deleted file mode 100644 index 574e785c71e..00000000000 --- a/assets/erc-7920/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "target": "es2020", - "module": "commonjs", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true, - "resolveJsonModule": true - } -} From bd08bc7c8affe3621b1a9070ce1c83b846062cb4 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Sun, 4 May 2025 14:22:15 -0400 Subject: [PATCH 36/39] updates --- ERCS/erc-7920.md | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 17ee36d83c0..31b6db29feb 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -27,6 +27,22 @@ Current solutions have significant drawbacks: - **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 +### UX Improvement + +A single signature that covers multiple messages + +### Isolated Verification + +Independent verification of messages without knowledge of others + +### Human-readable + +This ERC preserves the readability benefits of EIP-712. Giving wallets and users insight into what is being signed. + +### Improved Wallet Security + +Certain messages signed in isolation may appear harmless but combined with may be harmful. Giving the full list of messages to users could help them better navigate their experience. + ## Specification ### Overview @@ -346,35 +362,25 @@ Result: ## Rationale -### UX Improvement - -A single signature that covers multiple messages - -### Isolated Verification - -Independent verification of messages without knowledge of others - -### Human-readable - -This ERC preserves the readability benefits of EIP-712. Giving wallets and users insight into what is being signed. +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. -### Improved Wallet Security - -Certain messages signed in isolation may appear harmless but combined with may be harmful. Giving the full list of messages to users could help them better navigate their experience. - ### 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 Implementations +## Reference Implementation ### `eth_signTypedData_v5` From 5beb33256898a51186c19b1af4321530a1a2e8e2 Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Sun, 4 May 2025 14:29:18 -0400 Subject: [PATCH 37/39] updates --- ERCS/erc-7920.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 31b6db29feb..4131580a57c 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -27,21 +27,19 @@ Current solutions have significant drawbacks: - **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 -### UX Improvement +This ERC has the following objectives: -A single signature that covers multiple messages +### Single Signature + +A single signature should cover multiple messages ### Isolated Verification -Independent verification of messages without knowledge of others +Messages should be independently verifiable without knowledge of others ### Human-readable -This ERC preserves the readability benefits of EIP-712. Giving wallets and users insight into what is being signed. - -### Improved Wallet Security - -Certain messages signed in isolation may appear harmless but combined with may be harmful. Giving the full list of messages to users could help them better navigate their experience. +Readability benefits of EIP-712 should be preserved. Giving wallets and users insight into what is being signed. ## Specification @@ -211,7 +209,7 @@ This method returns: the signature, merkle root, and an array of proofs (each co ##### Returns -```Typescript +```typescript { signature: `0x${string}`; // Hex encoded 65 byte signature (same format as eth_sign) merkleRoot: `0x${string}`; // 32 byte Merkle root as hex string From 54bae6aa7ed767741b711d8264633e093616e5aa Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Mon, 5 May 2025 00:32:56 -0400 Subject: [PATCH 38/39] updates --- ERCS/erc-7920.md | 118 ++--------------------------------------------- 1 file changed, 4 insertions(+), 114 deletions(-) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index 4131580a57c..bd832ca3869 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -1,7 +1,7 @@ --- eip: 7920 title: Composite EIP-712 Signatures -description: A standard for signing multiple typed-data messages with a single signature +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 @@ -86,116 +86,6 @@ The message is verified if and only if (1) and (2) succeed. isVerified = isValidSignature && isValidProof ``` -#### Solidity Example - -Snippet below verifies a composite signature on-chain. The entrypoint, `placeOrder()` is called by a paymaster to sponsor an operation. It verifies the composite signature using the process outlined above. - -```solidity -// SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.20; - -error Unauthorized(); - -contract ExampleVerifier { - bytes32 public immutable DOMAIN_SEPARATOR; - bytes32 private constant MESSAGE_TYPEHASH = - keccak256("PlaceOrder(bytes32 orderId, address user)"); - - constructor() { - DOMAIN_SEPARATOR = keccak256( - abi.encode( - keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ), - keccak256(bytes("MyApp")), - keccak256(bytes("1.0.0")), - block.chainid, - address(this) - ) - ); - } - - function placeOrder( - bytes32 orderId, - address user, - bytes calldata signature, - bytes32 merkleRoot, - bytes32[] calldata proof - ) public { - bytes32 message = keccak256( - abi.encode( - "\x19\x01", - DOMAIN_SEPARATOR, - keccak256(abi.encode(MESSAGE_TYPEHASH, orderId, user)) - ) - ); - - if ( - !_verifyCompositeSignature( - message, - proof, - merkleRoot, - signature, - user - ) - ) { - revert Unauthorized(); - } - - // DO STUFF - } - - function _verifyCompositeSignature( - bytes32 message, - bytes32[] calldata proof, - bytes32 merkleRoot, - bytes calldata signature, - address expectedSigner - ) internal view returns (bool) { - if (!_verifyMerkleProof(message, proof, merkleRoot)) { - return false; - } - - return recover(merkleRoot, signature) == expectedSigner; - } - - function _verifyMerkleProof( - bytes32 leaf, - bytes32[] calldata proof, - bytes32 root - ) 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 == root; - } - - function recover( - bytes32 digest, - bytes memory signature - ) internal pure returns (address) { - require(signature.length == 65, "Invalid signature length"); - - bytes32 r; - bytes32 s; - uint8 v; - - assembly { - r := mload(add(signature, 32)) - s := mload(add(signature, 64)) - v := byte(0, mload(add(signature, 96))) - } - - return ecrecover(digest, v, r, s); - } -} -``` - ### 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. @@ -382,15 +272,15 @@ When the number of message is one, `eth_signTypedData_v5` produces the same sign ### `eth_signTypedData_v5` -Reference implementation of `eth_signTypedData_v5` can be found the [assets directory](../assets/erc-7920/src/eth_signTypedData_v5.ts). +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/erc-7920/contracts/ExampleVerifier.sol). +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/erc-7920/src/merkle.ts). +Reference Merkle tree can be found in the [assets directory](../assets/eip-7920/src/merkle.ts). ## Security Considerations From 5580d029fc43c7cffb1300788fe5db0bc7f386eb Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Tue, 6 May 2025 14:23:48 -0400 Subject: [PATCH 39/39] updates --- ERCS/erc-7920.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/ERCS/erc-7920.md b/ERCS/erc-7920.md index bd832ca3869..7347070c37d 100644 --- a/ERCS/erc-7920.md +++ b/ERCS/erc-7920.md @@ -80,6 +80,27 @@ To verify that an individual message `mₓ` was included in a composite signatur 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. ```