Move `ECDSA` message hash methods to its own `MessageHashUtils` library (#4430)
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com> Co-authored-by: Francisco <fg@frang.io>pull/4437/head
parent
996168f1f1
commit
0053ee040a
@ -0,0 +1,5 @@ |
||||
--- |
||||
'openzeppelin-solidity': major |
||||
--- |
||||
|
||||
`MessageHashUtils`: Add a new library for creating message digest to be used along with signing or recovery such as ECDSA or ERC-1271. These functions are moved from the `ECDSA` library. |
@ -0,0 +1,87 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.19; |
||||
|
||||
import {Strings} from "../Strings.sol"; |
||||
|
||||
/** |
||||
* @dev Signature message hash utilities for producing digests to be consumed by {ECDSA} recovery or signing. |
||||
* |
||||
* The library provides methods for generating a hash of a message that conforms to the |
||||
* https://eips.ethereum.org/EIPS/eip-191[EIP 191] and https://eips.ethereum.org/EIPS/eip-712[EIP 712] |
||||
* specifications. |
||||
*/ |
||||
library MessageHashUtils { |
||||
/** |
||||
* @dev Returns the keccak256 digest of an EIP-191 signed data with version |
||||
* `0x45` (`personal_sign` messages). |
||||
* |
||||
* The digest is calculated by prefixing a bytes32 `messageHash` with |
||||
* `"\x19Ethereum Signed Message:\n32"` and hashing the result. It corresponds with the |
||||
* hash signed when using the https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] JSON-RPC method. |
||||
* |
||||
* NOTE: The `hash` parameter is intended to be the result of hashing a raw message with |
||||
* keccak256, althoguh any bytes32 value can be safely used because the final digest will |
||||
* be re-hashed. |
||||
* |
||||
* See {ECDSA-recover}. |
||||
*/ |
||||
function toEthSignedMessageHash(bytes32 messageHash) internal pure returns (bytes32 digest) { |
||||
/// @solidity memory-safe-assembly |
||||
assembly { |
||||
mstore(0x00, "\x19Ethereum Signed Message:\n32") // 32 is the bytes-length of messageHash |
||||
mstore(0x1c, messageHash) // 0x1c (28) is the length of the prefix |
||||
digest := keccak256(0x00, 0x3c) // 0x3c is the length of the prefix (0x1c) + messageHash (0x20) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @dev Returns the keccak256 digest of an EIP-191 signed data with version |
||||
* `0x45` (`personal_sign` messages). |
||||
* |
||||
* The digest is calculated by prefixing an arbitrary `message` with |
||||
* `"\x19Ethereum Signed Message:\n" + len(message)` and hashing the result. It corresponds with the |
||||
* hash signed when using the https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] JSON-RPC method. |
||||
* |
||||
* See {ECDSA-recover}. |
||||
*/ |
||||
function toEthSignedMessageHash(bytes memory message) internal pure returns (bytes32 digest) { |
||||
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", Strings.toString(message.length), message)); |
||||
} |
||||
|
||||
/** |
||||
* @dev Returns the keccak256 digest of an EIP-191 signed data with version |
||||
* `0x00` (data with intended validator). |
||||
* |
||||
* The digest is calculated by prefixing an arbitrary `data` with `"\x19\x00"` and the intended |
||||
* `validator` address. Then hashing the result. |
||||
* |
||||
* See {ECDSA-recover}. |
||||
*/ |
||||
function toDataWithIntendedValidatorHash( |
||||
address validator, |
||||
bytes memory data |
||||
) internal pure returns (bytes32 digest) { |
||||
return keccak256(abi.encodePacked(hex"19_00", validator, data)); |
||||
} |
||||
|
||||
/** |
||||
* @dev Returns the keccak256 digest of an EIP-712 typed data (EIP-191 version `0x01`). |
||||
* |
||||
* The digest is calculated from a `domainSeparator` and a `structHash`, by prefixing them with |
||||
* `\x19\x01` and hashing the result. It corresponds to the hash signed by the |
||||
* https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`] JSON-RPC method as part of EIP-712. |
||||
* |
||||
* See {ECDSA-recover}. |
||||
*/ |
||||
function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 digest) { |
||||
/// @solidity memory-safe-assembly |
||||
assembly { |
||||
let ptr := mload(0x40) |
||||
mstore(ptr, hex"19_01") |
||||
mstore(add(ptr, 0x02), domainSeparator) |
||||
mstore(add(ptr, 0x22), structHash) |
||||
digest := keccak256(ptr, 0x42) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,55 @@ |
||||
require('@openzeppelin/test-helpers'); |
||||
const { toEthSignedMessageHash, toDataWithIntendedValidatorHash } = require('../../helpers/sign'); |
||||
const { domainSeparator, hashTypedData } = require('../../helpers/eip712'); |
||||
|
||||
const { expect } = require('chai'); |
||||
|
||||
const MessageHashUtils = artifacts.require('$MessageHashUtils'); |
||||
|
||||
contract('MessageHashUtils', function () { |
||||
beforeEach(async function () { |
||||
this.messageHashUtils = await MessageHashUtils.new(); |
||||
|
||||
this.message = '0x' + Buffer.from('abcd').toString('hex'); |
||||
this.messageHash = web3.utils.sha3(this.message); |
||||
this.verifyingAddress = web3.utils.toChecksumAddress(web3.utils.randomHex(20)); |
||||
}); |
||||
|
||||
context('toEthSignedMessageHash', function () { |
||||
it('prefixes bytes32 data correctly', async function () { |
||||
expect(await this.messageHashUtils.methods['$toEthSignedMessageHash(bytes32)'](this.messageHash)).to.equal( |
||||
toEthSignedMessageHash(this.messageHash), |
||||
); |
||||
}); |
||||
|
||||
it('prefixes dynamic length data correctly', async function () { |
||||
expect(await this.messageHashUtils.methods['$toEthSignedMessageHash(bytes)'](this.message)).to.equal( |
||||
toEthSignedMessageHash(this.message), |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
context('toDataWithIntendedValidatorHash', function () { |
||||
it('returns the digest correctly', async function () { |
||||
expect( |
||||
await this.messageHashUtils.$toDataWithIntendedValidatorHash(this.verifyingAddress, this.message), |
||||
).to.equal(toDataWithIntendedValidatorHash(this.verifyingAddress, this.message)); |
||||
}); |
||||
}); |
||||
|
||||
context('toTypedDataHash', function () { |
||||
it('returns the digest correctly', async function () { |
||||
const domain = { |
||||
name: 'Test', |
||||
version: 1, |
||||
chainId: 1, |
||||
verifyingContract: this.verifyingAddress, |
||||
}; |
||||
const structhash = web3.utils.randomHex(32); |
||||
const expectedDomainSeparator = await domainSeparator(domain); |
||||
expect(await this.messageHashUtils.$toTypedDataHash(expectedDomainSeparator, structhash)).to.equal( |
||||
hashTypedData(domain, structhash), |
||||
); |
||||
}); |
||||
}); |
||||
}); |
Loading…
Reference in new issue