ApeWorX / eip712

Message classes for typed structured data hashing and signing in Ethereum
Apache License 2.0
42 stars 19 forks source link

Structured data v4 support (array types) #6

Closed lost-theory closed 1 year ago

lost-theory commented 3 years ago

Got this question from a potential user:

Did you implement in your package the 4th version of the typed data encoding?

From what I see the "v4" encoding is defined by Metamask here:

The method signTypedData_v4 currently represents the latest version of the EIP-712 spec (opens new window), with added support for arrays and with a breaking fix for the way structs are encoded.

We don't have support for arrays at the moment. Here's how it's currently failing when trying to encode a uint[] field:

=================================== FAILURES ===================================
___________________________ test_multilevel_message ____________________________
[gw0] linux -- Python 3.8.10 /home/cuboid/eip712_env/bin/python3

    def test_multilevel_message():
        msg = ValidMessageWithNameDomainField(value=1, sub=SubType(inner=2), foo=[1])

        assert msg.version.hex() == "01"
        assert msg.header.hex() == "336a9d2b32d1ab7ea7bbbd2565eca1910e54b74843858dec7a81f772a3c17e17"
>       assert msg.body.hex() == "306af87567fa87e55d2bd925d9a3ed2b1ec2c3e71b142785c053dc60b6ca177b"

tests/test_messages.py:27: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
eip712/messages.py:188: in body
    return hash_eip712_message(self.body_data)
eip712/hashing.py:263: in hash_message
    encode_data(
eip712/hashing.py:245: in encode_data
    data_types_and_hashes = _encode_data(primaryType, types, data)
../../eip712_env/lib/python3.8/site-packages/eth_utils/functional.py:45: in inner
    return callback(fn(*args, **kwargs))
eip712/hashing.py:211: in _encode_data
    array_items_encoding = [
eip712/hashing.py:212: in <listcomp>
    encode_data(parsed_type.base, types, array_item) for array_item in array_items
eip712/hashing.py:245: in encode_data
    data_types_and_hashes = _encode_data(primaryType, types, data)
../../eip712_env/lib/python3.8/site-packages/eth_utils/functional.py:45: in inner
    return callback(fn(*args, **kwargs))
eip712/hashing.py:153: in _encode_data
    yield "bytes32", hash_struct_type(primary_type, types)
eip712/hashing.py:75: in hash_struct_type
    return keccak(text=encode_type(primary_type, types))
eip712/hashing.py:65: in encode_type
    deps = get_dependencies(primary_type, types)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

primary_type = 'uint'
types = {'EIP712Domain': [{'name': 'name', 'type': 'string'}], 'SubType': [{'name': 'inner', 'type': 'uint256'}], 'ValidMessag..., {'name': 'foo', 'type': 'uint[]'}, {'name': 'default_value', 'type': 'address'}, {'name': 'sub', 'type': 'SubType'}]}

    def get_dependencies(primary_type, types):
        """
        Perform DFS to get all the dependencies of the primary_type
        """
        deps = set()
        struct_names_yet_to_be_expanded = [primary_type]

        while len(struct_names_yet_to_be_expanded) > 0:
            struct_name = struct_names_yet_to_be_expanded.pop()

            deps.add(struct_name)
>           fields = types[struct_name]
E           KeyError: 'uint'

eip712/hashing.py:26: KeyError
lost-theory commented 3 years ago

More info:

I used eth_account lib for structured_data encoding (your implementation looks the same). I got a result different from the result obtained when using the eth-sig-util (js library). To reproduce js result I add value = bytearray.fromhex(value.decode('utf-8').replace('0x', '')) before hashed_value = keccak(primitive=value)

in _encode_data method (eth_account._utils.structured_data.hashing.py) in case, when field["type"] == "bytes"

this case (rows 189:203 in hashing.py file)

elif field["type"] == "bytes":
            if not isinstance(value, bytes):
                raise TypeError(
                    "Value of {0} ({2}) in the struct {1} is of the type {3}, but expected "
                    "bytes value".format(
                        field["name"],
                        primary_type,
                        value,
                        type(value),
                    )
                )
            # Special case where the values need to be keccak hashed before they are encoded
            value = bytearray.fromhex(value.decode('utf-8').replace('0x', ''))
            hashed_value = keccak(primitive=value)
            yield "bytes32", hashed_value