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 10 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
19 changes: 16 additions & 3 deletions eth_account/_utils/signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
from eth_utils import (
to_bytes,
to_int,
to_text,
)

from eth_account._utils.structured_data import (
hashStruct,
)
from eth_account._utils.transactions import (
ChainAwareUnsignedTransaction,
UnsignedTransaction,
Expand All @@ -21,6 +25,7 @@
# 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


def sign_transaction_dict(eth_key, transaction_dict):
Expand All @@ -44,7 +49,9 @@ def sign_transaction_dict(eth_key, transaction_dict):
return (v, r, s, encoded_transaction)


# watch here for updates to signature format: https://github.com/ethereum/EIPs/issues/191
# watch here for updates to signature format:
# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-191.md
# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved
@curry
def signature_wrapper(message, signature_version, version_specific_data):
if not isinstance(message, bytes):
Expand All @@ -64,12 +71,18 @@ 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)
domainSeparator = hashStruct(message_string, is_domain_separator=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
3 changes: 3 additions & 0 deletions eth_account/_utils/structured_data/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .hashing import ( # noqa: F401
hashStruct,
)
212 changes: 212 additions & 0 deletions eth_account/_utils/structured_data/hashing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import json

from eth_abi import (
encode_abi,
is_encodable,
)
from eth_utils import (
keccak,
to_tuple,
)

from .validation import (
validate_structured_data,
)


def dependencies(primaryType, types):
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved
"""
Perform DFS to get all the dependencies of the primaryType
"""
deps = set()
struct_names_yet_to_be_expanded = [primaryType]

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]
for field in fields:
# If this struct type has already been seen, then don't expand on it'
if field["type"] in deps:
continue
# If this struct type is not a customized type, then no need to expand
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved
elif field["type"] not in types:
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved
continue
# Custom Struct Type
else:
struct_names_yet_to_be_expanded.append(field["type"])

# Don't need to make a struct as dependency of itself
deps.remove(primaryType)

return tuple(deps)


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


def encode_struct(struct_name, struct_types):
return "{0}({1})".format(
struct_name,
','.join(map(dict_to_type_name_converter, struct_types)),
)


def encodeType(primaryType, types):
"""
The type of a struct is encoded as name ‖ "(" ‖ member₁ ‖ "," ‖ member₂ ‖ "," ‖ … ‖ memberₙ ")"
where each member is written as type ‖ " " ‖ name.
"""
# Getting the dependencies and sorting them alphabetically as per EIP712
deps = dependencies(primaryType, types)
sorted_deps = (primaryType,) + tuple(sorted(deps))

result = ''.join(
[
encode_struct(struct_name, types[struct_name])
for struct_name in sorted_deps
]
)
return result


def typeHash(primaryType, types):
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved
return keccak(text=encodeType(primaryType, types))


def is_valid_abi_type(type_name):
"""
This function is used to make sure that the ``type_name`` is a valid ABI Type.

Please note that this is a temporary function and should be replaced by the corresponding
ABI function, once the following issue has been resolved.
https://github.com/ethereum/eth-abi/issues/125
"""
valid_abi_types = {"address", "bool", "bytes", "int", "string", "uint"}
is_bytesN = type_name.startswith("bytes") and 1 <= int(type_name[5:]) <= 32
is_intN = (
type_name.startswith("int") and
8 <= int(type_name[3:]) <= 256 and
int(type_name[3:]) % 8 == 0
)
is_uintN = (
type_name.startswith("uint") and
8 <= int(type_name[4:]) <= 256 and
int(type_name[4:]) % 8 == 0
)

if type_name in valid_abi_types:
return True
elif is_bytesN:
# bytes1 to bytes32
return True
elif is_intN:
# int8 to int256
return True
elif is_uintN:
# uint8 to uint256
return True

return False


@to_tuple
def _encodeData(primaryType, types, data):
# Add typehash
yield "bytes32", 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}) in the struct `{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
hashed_value = keccak(text=value)
yield "bytes32", hashed_value
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"],
primaryType,
value,
type(value),
)
)
# Special case where the values need to be keccak hashed before they are encoded
hashed_value = keccak(primitive=value)
yield "bytes32", hashed_value
elif field["type"] in types:
# This means that this type is a user defined type
hashed_value = keccak(primitive=encodeData(field["type"], types, value))
yield "bytes32", hashed_value
elif field["type"][-1] == "]":
# TODO: Replace the above conditionality with Regex for identifying arrays declaration
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved
raise NotImplementedError("TODO: Arrays currently unimplemented in encodeData")
else:
# First checking to see if type is valid as per abi
if not is_valid_abi_type(field["type"]):
raise TypeError(
"Received Invalid type `{0}` in the struct `{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.
yield field["type"], value
else:
raise TypeError(
"Value of `{0}` ({2}) in the struct `{1}` is of the type `{3}`, but expected "
"{4} value".format(
field["name"],
primaryType,
value,
type(value),
field["type"],
)
)


def encodeData(primaryType, types, data):
data_types_and_hashes = _encodeData(primaryType, types, data)
data_types, data_hashes = zip(*data_types_and_hashes)
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved
return encode_abi(data_types, data_hashes)


def hashStruct(structured_json_string_data, is_domain_separator=False):
"""
The structured_json_string_data is expected to have the ``types`` attribute and
the ``primaryType``, ``message``, ``domain`` attribute.
The ``is_domain_separator`` 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 is_domain_separator:
Bhargavasomu marked this conversation as resolved.
Show resolved Hide resolved
primaryType = "EIP712Domain"
data = structured_data["domain"]
else:
primaryType = structured_data["primaryType"]
data = structured_data["message"]
return keccak(encodeData(primaryType, types, data))
111 changes: 111 additions & 0 deletions eth_account/_utils/structured_data/validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import re

from eth_utils import (
ValidationError,
)

# 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 validate_has_attribute(attr_name, dict_data):
if attr_name not in dict_data:
raise ValidationError(
"Attribute `{0}` not found in the JSON string".
format(attr_name)
)


def validate_types_attribute(structured_data):
# Check that the data has `types` attribute
validate_has_attribute("types", structured_data)

# Check if all the `name` and the `type` attributes in each field of all the
# `types` attribute are valid (Regex Check)
for struct_name in structured_data["types"]:
# Check that `struct_name` is of the type string
if not isinstance(struct_name, str):
raise ValidationError(
"Struct Name of `types` attribute should be a string, but got type `{0}`".
format(type(struct_name))
)
for field in structured_data["types"][struct_name]:
# Check that `field["name"]` is of the type string
if not isinstance(field["name"], str):
raise ValidationError(
"Field Name `{0}` of struct `{1}` should be a string, but got type `{2}`".
format(field["name"], struct_name, type(field["name"]))
)
# Check that `field["type"]` is of the type string
if not isinstance(field["type"], str):
raise ValidationError(
"Field Type `{0}` of struct `{1}` should be a string, but got type `{2}`".
format(field["type"], struct_name, type(field["type"]))
)
# Check that field["name"] matches with IDENTIFIER_REGEX
if not re.match(IDENTIFIER_REGEX, field["name"]):
raise ValidationError(
"Invalid Identifier `{0}` in `{1}`".format(field["name"], struct_name)
)
# Check that field["type"] matches with TYPE_REGEX
if not re.match(TYPE_REGEX, field["type"]):
raise ValidationError(
"Invalid Type `{0}` in `{1}`".format(field["type"], struct_name)
)


def validate_field_declared_only_once_in_struct(field_name, struct_data, struct_name):
if len([field for field in struct_data if field["name"] == field_name]) != 1:
raise ValidationError(
"Attribute `{0}` not declared or declared more than once in {1}".
format(field_name, struct_name)
)


def validate_EIP712Domain_schema(structured_data):
# Check that the `types` attribute contains `EIP712Domain` schema declaration
if "EIP712Domain" not in structured_data["types"]:
raise ValidationError("`EIP712Domain struct` not found in types attribute")
# Check that the names and types in `EIP712Domain` are what are mentioned in the EIP-712
# and they are declared only once
EIP712Domain_data = structured_data["types"]["EIP712Domain"]
validate_field_declared_only_once_in_struct("name", EIP712Domain_data, "EIP712Domain")
validate_field_declared_only_once_in_struct("version", EIP712Domain_data, "EIP712Domain")
validate_field_declared_only_once_in_struct("chainId", EIP712Domain_data, "EIP712Domain")
validate_field_declared_only_once_in_struct(
"verifyingContract",
EIP712Domain_data,
"EIP712Domain",
)


def validate_primaryType_attribute(structured_data):
# Check that `primaryType` attribute is present
if "primaryType" not in structured_data:
raise ValidationError("The Structured Data needs to have a `primaryType` attribute")
# Check that `primaryType` value is a string
if not isinstance(structured_data["primaryType"], str):
raise ValidationError(
"Value of attribute `primaryType` should be `string`, but got type `{0}`".
format(type(structured_data["primaryType"]))
)
# Check that the value of `primaryType` is present in the `types` attribute
if not structured_data["primaryType"] in structured_data["types"]:
raise ValidationError(
"The Primary Type `{0}` is not present in the `types` attribute".
format(structured_data["primaryType"])
)


def validate_structured_data(structured_data):
# validate the `types` attribute
validate_types_attribute(structured_data)
# validate the `EIP712Domain` struct of `types` attribute
validate_EIP712Domain_schema(structured_data)
# validate the `primaryType` attribute
validate_primaryType_attribute(structured_data)
# Check that there is a `domain` attribute in the structured data
validate_has_attribute("domain", structured_data)
# Check that there is a `message` attribute in the structured data
validate_has_attribute("message", structured_data)
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>=1.0.0,<2.0.0",
"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