diff --git a/eip712/hashing.py b/eip712/hashing.py index 7a45dbb..c311488 100644 --- a/eip712/hashing.py +++ b/eip712/hashing.py @@ -1,20 +1,19 @@ # copied under the MIT license from eth-account: -# https://github.com/ethereum/eth-account/blob/41f56e45b49ec1966a72cba12955bc1185cf7284/eth_account/_utils/structured_data/hashing.py - +# https://github.com/ethereum/eth-account/blob/cc2feca919474203b0b23450ce7f2deed3ce985c/eth_account/_utils/structured_data/hashing.py import json from itertools import groupby from operator import itemgetter -from eth_abi import encode_abi, is_encodable, is_encodable_type +from eth_abi import encode, is_encodable, is_encodable_type from eth_abi.grammar import parse -from eth_utils import ValidationError, keccak, to_tuple, toolz +from eth_utils import keccak, to_tuple from .validation import validate_structured_data def get_dependencies(primary_type, types): """ - Perform DFS to get all the dependencies of the primary_type + Perform DFS to get all the dependencies of the primary_type. """ deps = set() struct_names_yet_to_be_expanded = [primary_type] @@ -25,16 +24,23 @@ def get_dependencies(primary_type, types): deps.add(struct_name) fields = types[struct_name] for field in fields: - if field["type"] not in types: + field_type = field["type"] + + # Handle array types + if is_array_type(field_type): + field_type = field_type[: field_type.index("[")] + + if field_type not in types: # We don't need to expand types that are not user defined (customized) continue - elif field["type"] in deps: + elif field_type not in deps: + # Custom Struct Type + struct_names_yet_to_be_expanded.append(field_type) + elif field_type in deps: # skip types that we have already encountered continue else: - # Custom Struct Type - struct_names_yet_to_be_expanded.append(field["type"]) - + raise TypeError(f"Unable to determine type dependencies with type `{field_type}`.") # Don't need to make a struct as dependency of itself deps.remove(primary_type) @@ -43,6 +49,7 @@ def get_dependencies(primary_type, types): def field_identifier(field): """ + Convert a field dict into a typed-name string. Given a ``field`` of the format {'name': NAME, 'type': TYPE}, this function converts it to ``TYPE NAME`` """ @@ -58,7 +65,9 @@ def encode_struct(struct_name, struct_field_types): def encode_type(primary_type, types): """ - The type of a struct is encoded as name ‖ "(" ‖ member₁ ‖ "," ‖ member₂ ‖ "," ‖ … ‖ memberₙ ")" + Serialize types into an encoded string. + 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 @@ -76,15 +85,13 @@ def hash_struct_type(primary_type, types): def is_array_type(type): - # Identify if type such as "person[]" or "person[2]" is an array - abi_type = parse(type) - return abi_type.is_array + return type.endswith("]") @to_tuple def get_depths_and_dimensions(data, depth): """ - Yields 2-length tuples of depth and dimension of each element at that depth + Yields 2-length tuples of depth and dimension of each element at that depth. """ if not isinstance(data, (list, tuple)): # Not checking for Iterable instance, because even Dictionaries and strings @@ -100,151 +107,110 @@ def get_depths_and_dimensions(data, depth): def get_array_dimensions(data): """ - Given an array type data item, check that it is an array and - return the dimensions as a tuple. - Ex: get_array_dimensions([[1, 2, 3], [4, 5, 6]]) returns (2, 3) + Given an array type data item, check that it is an array and return the dimensions + as a tuple, in order from inside to outside. + Ex: get_array_dimensions([[1, 2, 3], [4, 5, 6]]) returns (3, 2) """ depths_and_dimensions = get_depths_and_dimensions(data, 0) - # re-form as a dictionary with `depth` as key, and all of the dimensions found at that depth. + + # re-form as a dictionary with `depth` as key, and all of the dimensions + # found at that depth. grouped_by_depth = { depth: tuple(dimension for depth, dimension in group) for depth, group in groupby(depths_and_dimensions, itemgetter(0)) } - # validate that there is only one dimension for any given depth. - invalid_depths_dimensions = tuple( - (depth, dimensions) - for depth, dimensions in grouped_by_depth.items() - if len(set(dimensions)) != 1 - ) - if invalid_depths_dimensions: - raise ValidationError( - "\n".join( - [ - "Depth {0} of array data has more than one dimensions: {1}".format( - depth, dimensions - ) - for depth, dimensions in invalid_depths_dimensions - ] - ) - ) - dimensions = tuple( - toolz.first(set(dimensions)) for depth, dimensions in sorted(grouped_by_depth.items()) + # check that all dimensions are the same, else use "dynamic" + dimensions[0] if all(dim == dimensions[0] for dim in dimensions) else "dynamic" + for _depth, dimensions in sorted(grouped_by_depth.items(), reverse=True) ) return dimensions -@to_tuple -def flatten_multidimensional_array(array): - for item in array: - if isinstance(item, (list, tuple)): - # Not checking for Iterable instance, because even Dictionaries and strings - # are considered as iterables, but that's not what we want the condition to be. - yield from flatten_multidimensional_array(item) - else: - yield item +def encode_field(types, name, field_type, value): + if value is None: + raise ValueError(f"Missing value for field {name} of type {field_type}") + if field_type in types: + return ("bytes32", keccak(encode_data(field_type, types, value))) -@to_tuple -def _encode_data(primary_type, types, data): - # Add typehash - yield "bytes32", hash_struct_type(primary_type, types) + if field_type == "bytes": + if not isinstance(value, bytes): + raise TypeError( + f"Value of field `{name}` ({value}) is of the type `{type(value)}`, " + f"but expected bytes value" + ) - # Add field contents - for field in types[primary_type]: - 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"], - primary_type, - 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): + return ("bytes32", keccak(value)) + + if field_type == "string": + if not isinstance(value, str): + raise TypeError( + f"Value of field `{name}` ({value}) is of the type `{type(value)}`, " + f"but expected string value" + ) + + return ("bytes32", keccak(text=value)) + + if is_array_type(field_type): + # Get the dimensions from the value + array_dimensions = get_array_dimensions(value) + # Get the dimensions from what was declared in the schema + parsed_field_type = parse(field_type) + + for i in range(len(array_dimensions)): + if len(parsed_field_type.arrlist[i]) == 0: + # Skip empty or dynamically declared dimensions + continue + if array_dimensions[i] != parsed_field_type.arrlist[i][0]: + # Dimensions should match with declared schema 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), - ) + f"Array data `{value}` has dimensions `{array_dimensions}`" + f" whereas the schema has dimensions " + f"`{tuple(map(lambda x: x[0] if x else 'dynamic', parsed_field_type.arrlist))}`" # noqa: E501 ) - # 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=encode_data(field["type"], types, value)) - yield "bytes32", hashed_value - elif is_array_type(field["type"]): - # Get the dimensions from the value - array_dimensions = get_array_dimensions(value) - # Get the dimensions from what was declared in the schema - parsed_type = parse(field["type"]) - for i in range(len(array_dimensions)): - if len(parsed_type.arrlist[i]) == 0: - # Skip empty or dynamically declared dimensions - continue - if array_dimensions[i] != parsed_type.arrlist[i][0]: - # Dimensions should match with declared schema - raise TypeError( - "Array data `{0}` has dimensions `{1}` whereas the " - "schema has dimensions `{2}`".format( - value, - array_dimensions, - tuple(map(lambda x: x[0], parsed_type.arrlist)), - ) - ) - - array_items = flatten_multidimensional_array(value) - array_items_encoding = [ - encode_data(parsed_type.base, types, array_item) for array_item in array_items - ] - concatenated_array_encodings = b"".join(array_items_encoding) - hashed_value = keccak(concatenated_array_encodings) - yield "bytes32", hashed_value + + field_type_of_inside_array = field_type[: field_type.rindex("[")] + field_type_value_pairs = [ + encode_field(types, name, field_type_of_inside_array, item) for item in value + ] + + # handle empty array + if value: + data_types, data_hashes = zip(*field_type_value_pairs) else: - # First checking to see if type is valid as per abi - if not is_encodable_type(field["type"]): - raise TypeError( - "Received Invalid type `{0}` in the struct `{1}`".format( - field["type"], - primary_type, - ) - ) + data_types, data_hashes = [], [] + + return ("bytes32", keccak(encode(data_types, data_hashes))) + + # First checking to see if field_type is valid as per abi + if not is_encodable_type(field_type): + raise TypeError(f"Received Invalid type `{field_type}` in field `{name}`") + + # Next, see if the value is encodable as the specified field_type + if is_encodable(field_type, value): + # field_type is a valid type and the provided value is encodable as that type + return (field_type, value) + else: + raise TypeError( + f"Value of `{name}` ({value}) is not encodable as type `{field_type}`. " + f"If the base type is correct, verify that the value does not " + f"exceed the specified size for the type." + ) - # 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"], - primary_type, - value, - type(value), - field["type"], - ) - ) +def encode_data(primary_type, types, data): + encoded_types = ["bytes32"] + encoded_values = [hash_struct_type(primary_type, types)] + + for field in types[primary_type]: + type, value = encode_field(types, field["name"], field["type"], data[field["name"]]) + encoded_types.append(type) + encoded_values.append(value) -def encode_data(primaryType, types, data): - data_types_and_hashes = _encode_data(primaryType, types, data) - data_types, data_hashes = zip(*data_types_and_hashes) - return encode_abi(data_types, data_hashes) + return encode(encoded_types, encoded_values) def load_and_validate_structured_message(structured_json_string_data): @@ -261,6 +227,8 @@ def hash_domain(structured_data): def hash_message(structured_data): return keccak( encode_data( - structured_data["primaryType"], structured_data["types"], structured_data["message"] + structured_data["primaryType"], + structured_data["types"], + structured_data["message"], ) ) diff --git a/eip712/validation.py b/eip712/validation.py index afd3385..55dfb2b 100644 --- a/eip712/validation.py +++ b/eip712/validation.py @@ -3,32 +3,23 @@ """ # copied under the MIT license from the eth-account project: -# https://github.com/ethereum/eth-account/blob/41f56e45b49ec1966a72cba12955bc1185cf7284/eth_account/_utils/structured_data/validation.py +# https://github.com/ethereum/eth-account/blob/cc2feca919474203b0b23450ce7f2deed3ce985c/eth_account/_utils/structured_data/validation.py # flake8: noqa - 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*)*\])*$" +TYPE_REGEX = r"^[a-zA-Z_$][a-zA-Z_$0-9]*(\[([1-9]\d*\b)*\])*$" def validate_has_attribute(attr_name, dict_data): - """ - Validates whether ``dict_data`` contains ``attr_name``, raising - :class:`eth_utils.ValidationError` if it does not. - """ if attr_name not in dict_data: - raise ValidationError("Attribute `{0}` not found in the JSON string".format(attr_name)) + raise ValidationError(f"Attribute `{attr_name}` not found in the JSON string") def validate_types_attribute(structured_data): - """ - Validates the existence of the ``types`` field, its contents, and whether identifer and type - names are valid. Raises :class:`eth_utils.ValidationError` otherwise. - """ # Check that the data has `types` attribute validate_has_attribute("types", structured_data) @@ -38,47 +29,34 @@ def validate_types_attribute(structured_data): # 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) - ) + "Struct Name of `types` attribute should be a string, " + f"but got type `{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"]) - ) + f"Field Name `{field['name']}` of struct `{struct_name}` " + f"should be a string, but got type `{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"]) - ) + f"Field Type `{field['type']}` of struct `{struct_name}` " + f"should be a string, but got type `{type(field['name'])}`" ) # 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) - ) + raise ValidationError(f"Invalid Identifier `{field['name']}` in `{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) - ) + raise ValidationError(f"Invalid Type `{field['type']}` in `{struct_name}`") def validate_field_declared_only_once_in_struct(field_name, struct_data, struct_name): - """ - Validates that the given ``field_name`` only appears once in - ``struct_data``. Raises :class:`eth_utils.ValidationError` otherwise. - """ 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 - ) + f"Attribute `{field_name}` not declared or declared more " f"than once in {struct_name}" ) @@ -91,20 +69,15 @@ def validate_field_declared_only_once_in_struct(field_name, struct_data, struct_ def used_header_fields(EIP712Domain_data): - """Returns only the ``EIP712_DOMAIN_FIELDS`` from the given domain data.""" return [field["name"] for field in EIP712Domain_data if field["name"] in EIP712_DOMAIN_FIELDS] def validate_EIP712Domain_schema(structured_data): - """ - Verifies that the given ``structured_data`` contains a valid EIP-712 - domain schema. Raises :class:`eth_utils.ValidationError` otherwise. - """ # 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 (if defined at all) + # Check that the names and types in `EIP712Domain` are what are mentioned in the + # EIP-712 and they are declared only once (if defined at all) EIP712Domain_data = structured_data["types"]["EIP712Domain"] header_fields = used_header_fields(EIP712Domain_data) if len(header_fields) == 0: @@ -114,34 +87,24 @@ def validate_EIP712Domain_schema(structured_data): def validate_primaryType_attribute(structured_data): - """ - Verifies that the given ``structured_data`` contains a valid ``primaryType`` - definition. Raises :class:`eth_utils.ValidationError` otherwise. - """ # 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"]) - ) + "Value of attribute `primaryType` should be `string`, " + f"but got type `{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"] - ) + f"The Primary Type `{structured_data['primaryType']}` is not " + "present in the `types` attribute" ) def validate_structured_data(structured_data): - """ - Top-level validator that verifies ``structured_data`` is a valid according - to the EIP-712 spec. Raises :class:`eth_utils.ValidationError` otherwise. - """ # validate the `types` attribute validate_types_attribute(structured_data) # validate the `EIP712Domain` struct of `types` attribute