EIP-712: eth_signTypedData as a standard for machine-verifiable and human-readable typed data signing

I discussed that with @remco at EthCC, and he confirmed that the process for hashing array (fixed or arbitrary size) isn’t formally described yet. This is one of the last points that need addressing before ERC712 can be finalized.

3 Likes

What can I do to help finalize it? I’m not sure how to tackle this.

For example how to define typehash for the struct with arbitrary size array?

2 Likes

iExec is using ERC712 for active production on mainnet since V3 was deployed in on may 15th. I’m sure we are not the only one.

It’s high time this ERC moves toward finalization. I believe one of the last obstacle is the formalisation of dynamic array hashing (which is something iExec will need for V4, so I’m trice motivated to get that sorted out). Only then will we have Ledger (and other wallet) support.

@Recmo , as far as I understand, you are the ERC712 leader (at least you are to me) … do you have any opinion on that ? I’d love to contribute to move this ERC forward but I don’t feel like I have the weight needed to lead it.

There are at least three issues to resolve / take decision on for EIP-712 in my opinion:

  • array hashing (as you mentioned)
  • the origin proposal (including the non-interactive signature bit) : see comments on EIP-712 and my blog post
    Note that while in the latter comments from me and @Recmo a solution involving a special envelope is mentioned. I actually disagree with it since it would force standards built on top of EIP-712 to choose whether they support origin or not. Origin should be part of the raw EIP-712 proposal like chainId is
  • chainId and blockNumber pair for replay protection on minority-led forks => if blockNumber is included, they can’t be part of a static domain separator as blockNumber would be updated frequently, see discussion on EIP-1344, EIP-1959 and EIP-1965
1 Like

I’m not sure where the pair came from, but chainId should be sufficient by itself as it should not change very often. Even if a rotating chainId process is add to hard fork upgrades, it would still change less than 1-2 times per year, and that is something easily tracked and submitted separately by the application, depending on the methodology used.

As stated in EIP-1344’s discussion, some applications may not require historical chainId at all, or may desire an application-specific way of updating it for increased safety of the application. We’re still unsure which methodology will be chosen in Istanbul, so I would caution against premature optimization here.

I agree that if chainId was the only thing to change, we would be fine. That is why I mention the (chainID, blockNumber) pair.

The need for blockNumber was discussed as part of our conversations.
It is explained here : EIP-1959 Valid ChainID opcode - #5 by wighawag and in the rationale of EIP-1965

It is to allow fork led by minority to not suffer from replay issues from the majority inaction (since the current chainId from the majority chain would be a past chainId on the minority chain). The blockNumber at which the message was signed can ensure replay protection by making smart contract disregard message signed at a blockcNumber where a chainID is a past one.

with EIP-1965 it simply expressed this way :

require(validChainId(message.chainId, message.blockNumber), "invalid message on that chain");

And by letting wallet setting the blockNumber as part of EIP-712, they protect users from replay issues.

A protection that EIP-1344 can’t handle properly since the required caching mechanism would leave a gap.

While it is technically true that they do not require it to function, they require it to function as expected. Indeed, smart contract that disregard past chainID will render their user dependent on the message submission timing. For example, a user could not rely anymore on being able to delay a message, since as soon as a hardfork happen, the user would have to sign the message again. This is less than ideal and why I think we should consider all application as benefiting from respecting past chainIDs.

But we should probably discuss that in EIP-1344 discussion thread if my comments there were not clear enough.

It’s a requirement if 1959 or 1965 is adopted. It is not if 1344 is adopted instead. I think the decision to update this specification relies on the outcome of what is adopted in that case.

For certain use cases, this is acceptable. For some, it is actually more ideal.

An example of the former is the meta-transaction use case, where chainId is used as a domain separator for offline signing. These transactions are meant to be resolved in under a day, so if an older value of chainId is used, it is acceptable to ask the user to re-submit with the new value.

An example of the later is Plasma Transactions. In that use case, it is required that the value of chainId chosen aligns with the value that the operator uses in their block submission summary transaction, therefore it is important that the operator has control over what value is accepted by the contract (can be updated trustlessly, during the block submission transaction). chainId might be updated in between submissions, therefore it is important that the operator only accept submissions with the correct value of chainId that they will upload, and reject others. Having it be either/or would be more dangerous.

We can continue this conversation in the EIP-1344 thread, as it relates to prior discussion we’ve had on this exact topic. My point was not to prematurely optimize for an EIP that is not yet confirmed to be in the next hard fork, as we are still figuring out which path will be chosen.

An example of the former is the meta-transaction … it is acceptable to ask the user to re-submit with the new value.

Acceptable but not desirable

An example of the later is Plasma Transactions. …the operator has control over what value is accepted by the contract (can be updated trustlessly, during the block submission transaction).

As already mentioned in our previous conversation, such chainId information can be provided by other means. It does not need to be coupled with EIP-712 where the role of chainId is replay protection

I agree, the reason I posted my 3 points here is that I realized not everywhere is aware of the current dicussion about chainID. EIP-712 was designed before that.

I think the decision to update this specification relies on the outcome of what is adopted in that case.

My goal was not to update the spec, it was just to bring awareness to the current discussion. Did not want EIP-712 to get finalised without that info first.

1 Like

Can’t we simply re-use the EIP-712 hashStruct for byte arrays? So long as one of the contract down the line can interpret this embeded hashStruct, the original contract called doesn’t need to know what this hashStruct is. It will simply take the hash of that byte array before doing an EIP-712 hash, like specified in the spec and can then pass that bytes array to another contract, which understands what the structure of that bytes array is.

We could also use the functions structure in some instances, where the first 4 bytes are a “function signature” (e.g. bytes4(keccak256(buyTickets(unit256 _amount)) and the rest of the bytes array would be the encoded arguments for that “function signature”. Of course, the bytes4 don’t need to be functions per say either.

Reusing the logic of byte arrays for uint[], bytes32[] or any other array of “native types <= 1 word” is ok, but it goes against the whole proposal for arrays of struct (such as a Recipient[] where Recipient would be a struct with multiple entries).

I believe the logic would be to hash each entry, turning the Recipient[] into a bytes32[] and then hashing the bytes32[]. The drawback is that this cannot be done in place.

In the end, it’s pointless what I believe the logic should or should not be, as long as we have ONE way to do it that is part of the standard

Here’s maybe a way to do general array hashing: signTypedData for arrays by cag · Pull Request #54 · MetaMask/eth-sig-util · GitHub

Arrays of atomic types have been encoded as a hash of its contents, where its contents are word-aligned according to the type.

Arrays of dynamic types have been encoded as a hash of each element’s hash.

Arrays of structs retain their encoding as a hash of its contents’ concatenated hashStructs.

Arrays of arrays are encoded recursively, as a hash of the contained arrays’ hash encodings concatenated.


Additionally, how should clients handle missing values in an eth_signTypedData structure? eth-sig-util currently skips over fields which are missing, but it uses the same type hash, meaning a struct like Foo(uint256 bar,bytes32 baz) may only have hashStruct(typeHash(Foo) . baz) if bar is undefined on the message. In the PR, because it was simpler to do so in the rewrite, I’ve made every field specified in the typeHash required, but Dan wanted to know if there was a preference for the optional behavior.

Note that this is different from requiring fields like string name to be specified in the EIP712Domain struct. I’m asking about the behavior once the type has been specified whether or not those fields specified should be considered optional.

Personally, I think this should be an error, since using the Foo example, how do you know whether second part of the hashStruct payload refers to bar or baz?


I have a feeling that maybe asking to limit the scope of EIP-712 and introducing this in an EIP extension may be the way to go, as I may have heard of some hardware vendors claiming EIP-712 support, where for them it might actually mean something along the lines of “flat structures containing primitive types” support (I think somebody might have mentioned that being a EIP finalization target). If so, it would be good to get the opinion of relevant stakeholders in this discussion, as I would guess it wouldn’t be feasible to upgrade existing hardware that’s out there claiming EIP 712 support, but I haven’t read through the original discussion thread on Github yet, as it’s pretty long…

It could be that it’s just the crypto primitives being implemented in hardware though, and that software support for this may be added after the fact, in which making this a patch to the EIP could make sense instead.


I’ve done some looking into whether or not this extension can be added to hardware, and it seems that for the majority of the cases, hardware support is for building blocks, and support just has to be added on the Ethereum app level in the case of Ledger or at the firmware level in the case of Trezor. Since this is the case, and because the recent discussion seems to imply dynamic and nested types will be part of the final spec for this EIP, I’ll write an update to this EIP accordingly.


I’ve written a PR to ERC-712 here.

In addition to the changes quoted above, I’ve made all specified struct members mandatory except for structs. The reason why structs are not mandatory is because I’ve realized that the standard intended for recursive types to be possible. As an example, consider:

struct Node {
    bytes data;
    Node next;
}

This was a valid type in the standard, but it wasn’t clear how to encode instances of this type. With mandatory struct members, this type would not be possible to express. I’ve taken the liberty to state that given the example, the next field may either be encoded recursively as its structHash, or as bytes32(0) when expressing its absence from an instance.

2 Likes

Regarding ABIv2 for encodeData, I was investigating and asked Chris from the solidity dev team what he thought. Here is his response ;

Any insight @Recmo @dekz ?

1 Like

Link for the lazy: https://eips.ethereum.org/EIPS/eip-712

1 Like

That’s problematic. It’s a good thing there isn’t much uptake of this standard yet, although it is very useful and it would be good to clear any issues up.

1 Like

In case of a fork, the signatures would only work in the fork that didn’t changed chainId, to solve that, please use EIP-1344 in the 712 standard.

Hi everyone, I am not sure where is the best place to ask this so I’m trying here.
I’m looking at EIP-712 v4 and the scheme to encode arrays.

eth-sig-util is managing it well (and I guess correctly) on the client side but I can find no example on how to do that in solidity.

My understanding is that encoding an array of 3 strings arr = [“1”, “2”, “3”] is equialent to keccak256(abi.encode(keccak256(abi.encodePacked(arr[0])), keccak256(abi.encodePacked(arr[1])), keccak256(abi.encodePacked(arr[2])))

So first thing, is this correct? if yes, how can I implement that in solidity?

I’m sure you can do better / in place by writing it in assembly, but here is a “quick” answer:

function hash(string[] calldata array) internal pure returns (bytes32 result) {
    bytes32[] memory _array = new bytes32[](array.length);
    for (uint256 i = 0; i < array.length; ++i) {
        _array[i] = keccak256(bytes(array[i]));
    }
    result = keccak256(abi.encodePacked(_array));
}
1 Like

Hey everyone, I just wanted to bring up what I believe to be a minor issue in the eth_signTypedData JSON RPC specification. Specifically, it doesn’t go into much detail about the JSON representation of the message object and the supported Solidity primitive values.

For example, what is an acceptable encoding for a uint32? Are all 100, 1e2, "100", "0x64" accepted? Should they be treated like QUANTITIES so only "0x64" is valid? In this regard, MetaMask for example, seems to accept both hexadecimal and decimal strings for uint256.

While I don’t have any particular suggestion on what the supported formats should be, I do believe it should be specified in the EIP.

4 Likes

@Recmo @dekz Do you need help finishing this spec?

How are clients expected to know what parameters to use in the domain separator? This is easy when signing for a particular contract with a known domain separator, but there is no standard mechanism for use cases where it is not known beforehand.

For a concrete example, people have run into this problem in EIP-2612 (ERC20 permit). Some tokens include the version parameter (OpenZeppelin, USDC), others don’t (COMP), and those who do may use different values (OpenZeppelin: default "1", USDC: "2"). There is no way to query the contract if version is used and with what value. EIP-2612 includes a DOMAIN_SEPARATOR getter, but it would be wrong to use this value without verifying that it corresponds to the current chain id, for which all the parameters are necessary.

I imagine any other EIP that builds on EIP-712 signatures will run into the same issue. The only solution right now is to make it a part of those EIPs what the domain separator is supposed to look like, but I think it would be better to include a standard mechanism either in EIP-712 or in a complementary EIP.