Ethereum Improvement Proposals (EIPs): The difference between EIP191 and EIP712
Signatures are ubiquitous on Ethereum, and they are also required for handling data within smart contracts.
For example, in the code of Uniswap V2, a piece of signature data can be passed, and the code will verify if the signer of the data is the owner itself.
People appreciate standardization, so a data format called EIP191 was established for signature data within smart contracts.
Conclusion First
EIP191
The data format of EIP191 is as follows:
0x19 <1 byte version> <version specific data> <data to sign>.
With this data format, people would first assemble the data according to the EIP191 format and then proceed with the signing process. The signature data is also verified within the smart contract using the EIP191 format.
There are currently a total of three version numbers.
Version 0x00
The data format for this version is as follows:
0x19 <0x00> <intended validator address> <data to sign>
Typically, the contract address is used for this purpose. The benefit of this approach is that the signature is only valid for a specific contract, to some extent mitigating replay attacks.
For example, if you have a piece of data “abc” that needs to be signed and used in the contract address 0xffff, the common steps are as follows:
Version 0x45
The data format for this version is as follows:
0x19 <0x45 (E)> <thereum Signed Message:\n" + len(message)> <data to sign>
Note that the ASCII encoding for 0x45 corresponds to the letter ‘E’. This version actually incorporates the personal_sign scheme into EIP191.
Version 0x01
This version is EIP712. In other words, EIP712 is actually a variant of EIP191.
A FEW QUESTIONS
In Ethereum, RLP (Recursive Length Prefix) encoding is widely used. In order to differentiate from RLP encoding, EIP191 adopts “0x19” as the prefix for EIP191 data.
The reason behind this choice is that in RLP encoding, if “0x19” is used as the prefix, it represents a single byte. It cannot be followed by a sequence of data like in EIP191. Therefore, EIP191 data can be distinguished from RLP-encoded data.
2. What is the difference between the “0x00” version and the “0x45” version in EIP191?
This is due to historical reasons. Initially, there was no EIP191 format, and people commonly used the personal_sign scheme originally implemented by Geth (https://github.com/ethereum/go-ethereum/pull/2940). The data format for personal_sign was as follows:
"\x19Ethereum Signed Message:\n" + length(message) + message
And people often perform a hash operation on the message. Therefore, the more common format is:
领英推荐
"\x19Ethereum Signed Message:\n32" + Keccak256(message)
Building upon this, EIP191 was expanded. EIP191 uses “0x19” as the prefix followed by a version number. However, the personal_sign scheme did not have a version number. As a workaround, the first letter “E” was used as the version number, creatively addressing the issue.
EIP712
EIP712 is a variant of EIP191.
EIP191 has a few issues:
EIP712 was designed to address these two problems:
First, let’s make a direct visual comparison of the differences:
On the left side is the EIP91 encoding, where the message appears as a series of unreadable strings. On the right side is the EIP712 encoding, which allows us to understand the specific data of the structure.
Let’s take a closer look at how EIP712 achieves it.
Still remember that EIP712 is a part of EIP191? Let’s take a look at the data format of EIP191:
0x19 <1 byte version> <version specific data> <data to sign>.
For EIP712, the version number corresponding to the 1-byte version is 0x01.
In the version-specific data, the hash of the DOMAIN_SEPARATOR is stored. The DOMAIN_SEPARATOR is a struct and has the following format:
struct EIP712Domain{
string name, //User-readable domain, such as the name of a DAPP
string version, // The current version number of the domain being signed
uint256 chainId, // The chain ID defined in EIP-155, such as Ethereum Mainnet with a chain ID of 1
address verifyingContract, // The contract address used for signature verification
bytes32 salt // Random number, which is often omitted
}
With this data, which includes the chainID, contract address, app name, version number, and other data, it becomes highly unlikely to be susceptible to replay attacks.
Now, let’s assume that we have a piece of data “abc” that needs to be signed using EIP712. The steps would be as follows:
DOMAIN_SEPARATOR_HASH = keccak256(
abi.encode(
// encodeType
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
// encodeData
keccak256(bytes(name)),
keccak256(bytes('1')),
chainId,
address(this)
)
);
0x19 0x01 DOMAIN_SEPARATOR_HASH "abc"。
2. Standardize the encoding method for the struct data
In the previous example, we discussed the purpose of the DOMAIN_SEPARATOR and provided an example using a non-struct “abc”. Now, let’s take a look at how to encode a struct.
// Mail is the struct that needs to be signed
struct Mail {
address from;
address to;
string contents;
}
//Encode the struct
messageHash = keccak256(
abi.encode(
keccak256("Mail(address from,address to,string contents)"
mail.from,
mail.to,
keccak256(bytes(mail.contents))
)
);
As you can see in the messageHash, the struct name and property names are encoded, allowing wallets and other third parties to understand the encoded structure of the data. This resolves the issue of struct encoding specifications.
Combining this with the previous example of DOMAIN_SEPARATOR, the final EIP712 encoding is as follows:
0x19 0x01 DOMAIN_SEPARATOR_HASH messageHash
After hashing the data encoded in EIP712, signing it, you can then send it to the smart contract for verification.
Ref: