Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement EIP 712 #57

Merged
merged 15 commits into from
Apr 4, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 192 additions & 2 deletions eth_account/_utils/signing.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import json
import re

from cytoolz import (
curry,
pipe,
)
from eth_abi import (
encode_abi,
is_encodable,
)
from eth_utils import (
keccak,
to_bytes,
to_int,
to_text,
)

from eth_account._utils.transactions import (
Expand All @@ -21,6 +30,11 @@
# signature versions
PERSONAL_SIGN_VERSION = b'E' # Hex value 0x45
INTENDED_VALIDATOR_SIGN_VERSION = b'\x00' # Hex value 0x00
STRUCTURED_DATA_SIGN_VERSION = b'\x01' # Hex value 0x01

# Regexes
IDENTIFIER_REGEX = r"^[a-zA-Z_$][a-zA-Z_$0-9]*$"
TYPE_REGEX = r"^[a-zA-Z_$][a-zA-Z_$0-9]*(\[([1-9]\d*)*\])*$"


def sign_transaction_dict(eth_key, transaction_dict):
Expand All @@ -44,6 +58,175 @@ def sign_transaction_dict(eth_key, transaction_dict):
return (v, r, s, encoded_transaction)


#
# EIP712 Functionalities
#
def dependencies(primaryType, types, found=None):
"""
Recursively get all the dependencies of the primaryType
"""
# This is done to avoid the by-reference call of python
found = found or []
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved

if primaryType in found:
return found
if primaryType not in types:
return found

found.append(primaryType)
for field in types[primaryType]:
for dep in dependencies(field["type"], types, found):
if dep not in found:
found.push(dep)

return found


def dict_to_type_name_converter(field):
"""
Given a dictionary ``field`` of type {'name': NAME, 'type': TYPE},
this function converts it to ``TYPE NAME``
"""
return field["type"] + " " + field["name"]
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved


def encodeType(primaryType, types):
# Getting the dependencies and sorting them alphabetically as per EIP712
deps = dependencies(primaryType, types)
deps_without_primary_type = list(filter(lambda x: x != primaryType, deps))
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved
sorted_deps = [primaryType] + sorted(deps_without_primary_type)
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved

result = ''.join(
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved
[
dep + "(" + ','.join(map(dict_to_type_name_converter, types[dep])) + ")"
for dep in sorted_deps
]
)
return result


def typeHash(primaryType, types):
return keccak(text=encodeType(primaryType, types))


def encodeData(primaryType, types, data):
encTypes = []
encValues = []

# Add typehash
encTypes.append("bytes32")
encValues.append(typeHash(primaryType, types))

# Add field contents
for field in types[primaryType]:
value = data[field["name"]]
if field["type"] == "string":
if not isinstance(value, str):
raise TypeError(
"Value of `{0}` ({2}) of field `{1}` is of the type `{3}`, but expected "
"string value".format(
field["name"],
primaryType,
value,
type(value),
)
)
# Special case where the values need to be keccak hashed before they are encoded
encTypes.append("bytes32")
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved
hashed_value = keccak(text=value)
encValues.append(hashed_value)
elif field["type"] == "bytes":
if not isinstance(value, bytes):
raise TypeError(
"Value of `{0}` ({2}) of field `{1}` is of the type `{3}`, but expected "
"bytes value".format(
field["name"],
primaryType,
value,
type(value),
)
)
# Special case where the values need to be keccak hashed before they are encoded
encTypes.append("bytes32")
hashed_value = keccak(primitive=value)
encValues.append(hashed_value)
elif field["type"] in types:
# This means that this type is a user defined type
encTypes.append("bytes32")
hashed_value = keccak(primitive=encodeData(field["type"], types, value))
encValues.append(hashed_value)
elif field["type"][-1] == "]":
# TODO: Replace the above conditionality with Regex for identifying arrays declaration
raise NotImplementedError("TODO: Arrays currently unimplemented in encodeData")
else:
# First checking to see if the individual values can be encoded
try:
is_encodable(field["type"], value)
except:
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved
raise AttributeError(
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved
"Received Invalid type `{0}` of field `{1}`".format(
field["type"],
primaryType,
)
)

# Next see if the data fits the specified encoding type
if is_encodable(field["type"], value):
# field["type"] is a valid type and this value corresponds to that type.
encTypes.append(field["type"])
encValues.append(value)
else:
raise TypeError(
"Value of `{0}` ({2}) of field `{1}` is of the type `{3}`, but expected "
"{4} value".format(
field["name"],
primaryType,
value,
type(value),
field["type"],
)
)

return encode_abi(encTypes, encValues)


def validate_structured_data(structured_data):
# Check if all the `name` and the `type` attributes in each field of all the
# `types` are valid (Regex Check)
for field_type in structured_data["types"]:
for field in structured_data["types"][field_type]:
# Check that field["name"] matches with IDENTIFIER_REGEX
if not re.match(IDENTIFIER_REGEX, field["name"]):
raise AttributeError(
"Invalid Identifier `{}` in `{}`".format(field["name"], field_type)
)
# Check that field["type"] matches with TYPE_REGEX
if not re.match(TYPE_REGEX, field["type"]):
raise AttributeError(
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved
"Invalid Type `{}` in `{}`".format(field["type"], field_type)
)


def hashStruct(structured_json_string_data, for_domain=False):
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved
"""
The structured_json_string_data is expected to have the ``types`` attribute and
the ``primaryType``, ``message``, ``domain`` attribute.
The ``for_domain`` variable is used to calculate the ``hashStruct`` as part of the
``domainSeparator`` calculation.
"""
structured_data = json.loads(structured_json_string_data)
validate_structured_data(structured_data)

types = structured_data["types"]
if for_domain:
primaryType = "EIP712Domain"
data = structured_data["domain"]
else:
primaryType = structured_data["primaryType"]
data = structured_data["message"]
return keccak(encodeData(primaryType, types, data))


# watch here for updates to signature format: https://github.com/ethereum/EIPs/issues/191
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved
@curry
def signature_wrapper(message, signature_version, version_specific_data):
Expand All @@ -64,12 +247,19 @@ def signature_wrapper(message, signature_version, version_specific_data):
raise TypeError("Invalid Wallet Address: {}".format(version_specific_data))
wrapped_message = b'\x19' + signature_version + wallet_address + message
return wrapped_message
elif signature_version == STRUCTURED_DATA_SIGN_VERSION:
message_string = to_text(primitive=message)
# Here the version_specific_data is the EIP712Domain JSON string (includes type also)
domainSeparator = hashStruct(message_string, for_domain=True)
wrapped_message = b'\x19' + signature_version + domainSeparator + hashStruct(message_string)
return wrapped_message
else:
raise NotImplementedError(
"Currently supported signature versions are: {0}, {1}. ".
"Currently supported signature versions are: {0}, {1}, {2}. ".
format(
'0x' + INTENDED_VALIDATOR_SIGN_VERSION.hex(),
'0x' + PERSONAL_SIGN_VERSION.hex()
'0x' + PERSONAL_SIGN_VERSION.hex(),
'0x' + STRUCTURED_DATA_SIGN_VERSION.hex(),
) +
"But received signature version {}".format('0x' + signature_version.hex())
)
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
include_package_data=True,
install_requires=[
"attrdict>=2.0.0,<3",
"eth-abi",
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved
"eth-keyfile>=0.5.0,<0.6.0",
"eth-keys>=0.2.0b3,<0.3.0",
"eth-utils>=1.0.2,<2",
Expand Down
Loading