Ethereum Improvement Proposals (EIPs): The difference between EIP191 and EIP712

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 encoding is designed to define the format of signature data within smart contracts.
  • EIP712 is a variant of EIP191.
  • EIP712 addresses the issues of replay attacks and struct encoding specification.

EIP191

The data format of EIP191 is as follows:


0x19 <1 byte version> <version specific data> <data to sign>.        

  • “0x19” is the prefix;
  • It consists of a 1-byte version number;
  • The version number includes specific data;
  • The signature data itself.

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:

  1. Concatenate the EIP191 data format: data = 0x19 0x00 0xffff abc;
  2. Perform a hash operation on the concatenated data: hash = keccak256(data);
  3. Sign the data;
  4. Send the data to the contract, where the contract invokes ecrecover to calculate the signer’s address;
  5. Verify the legitimacy of the signer.

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

  1. Why is “0x19” used as the prefix?


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:

  1. There is no explicit provision to prevent replay attacks.
  2. If the message is a struct, there is no corresponding encoding specification. Developers can encode it in their own ways, causing compatibility issues with external components such as wallets.

EIP712 was designed to address these two problems:

  1. It introduces the use of DOMAIN_SEPARATOR to prevent replay attacks.
  2. It provides a standard for encoding struct data.

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.

  1. By setting the DOMAIN_SEPARATOR, EIP712 prevents replay attacks.


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:

  • Prepare the prefix: prefix = 0x19 0x01
  • Compute the hash of DOMAIN_SEPARATOR


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

  • Concatenate them together:


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:


要查看或添加评论,请登录

Ashwin Kolhe的更多文章

社区洞察

其他会员也浏览了