Repository: ethereum/eth-account

Issue:
encode_structured_data Error with Array Types (SignTypedDataV4)
## What was wrong?

`encode_structured_data` throws KeyError when there is an array type in a struct. The error doesn't give any context and seems to support some array types (ex. `Person[]` doesnt seem to throw error but `address[]` throws error).  When I simply just make the Person.wallets to be `address` instead of `address[]` it works (obviously I update the message accordingly). But for some reason, an array of address just is not working. For more context, I am trying to make it work with the SignTypedDataV4 on MetaMasks[ test Dapp](https://metamask.github.io/test-dapp/)

### Code that produced the error

```py
msg = {
      'domain': {
        'chainId': 1,
        'name': 'Ether Mail',
        'verifyingContract': '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
        'version': '1',
      },
      'message': {
        'contents': 'Hello, Bob!',
        'from': {
          'name': 'Cow',
          'wallets': ['0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
                      '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'],
        },
        'to': [
          {
            'name': 'Bob',
            'wallets': ['0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
                        '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'],
          },
        ],
      },
      'primaryType': 'Mail',
      'types': {
        'EIP712Domain': [
          { 'name': 'name', 'type': 'string' },
          { 'name': 'version', 'type': 'string' },
          { 'name': 'chainId', 'type': 'uint256' },
          { 'name': 'verifyingContract', 'type': 'address' },
        ],
        'Group': [
          { 'name': 'name', 'type': 'string' },
          { 'name': 'members', 'type': 'Person[]' },
        ],
        'Mail': [
          { 'name': 'from', 'type': 'Person' },
          { 'name': 'to', 'type': 'Person[]' },
          { 'name': 'contents', 'type': 'string' },
        ],
        'Person': [
          { 'name': 'name', 'type': 'string' },
          { 'name': 'wallets', 'type': 'address[]' },
        ],
      },
    }
  
    new_msg = json.loads(json.dumps(msg), object_hook=_decode)
    new_msg['domain']['version'] = str(new_msg['domain']['version'])
    encoded_msg = encode_structured_data(new_msg)
    signed_msg = w3.eth.account.sign_message(encoded_msg, account)
```

### Full error output 

```sh
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/user/sign_message.py", line 158, in t 
    encoded_msg = encode_structured_data(new_msg)
  File "/home/user/.local/lib/python3.8/site-packages/eth_account/messages.py", line 156, in encode_structured_data
    hash_eip712_message(structured_data),
  File "/home/user/.local/lib/python3.8/site-packages/eth_account/_utils/structured_data/hashing.py", line 286, in hash_message
    encode_data(
  File "/home/user/.local/lib/python3.8/site-packages/eth_account/_utils/structured_data/hashing.py", line 262, in encode_data
    data_types_and_hashes = _encode_data(primaryType, types, data)
  File "/home/user/.local/lib/python3.8/site-packages/eth_utils/functional.py", line 45, in inner
    return callback(fn(*args, **kwargs))
  File "/home/user/.local/lib/python3.8/site-packages/eth_account/_utils/structured_data/hashing.py", line 209, in _encode_data
    hashed_value = keccak(primitive=encode_data(field["type"], types, value))
  File "/home/user/.local/lib/python3.8/site-packages/eth_account/_utils/structured_data/hashing.py", line 262, in encode_data
    data_types_and_hashes = _encode_data(primaryType, types, data)
  File "/home/user/.local/lib/python3.8/site-packages/eth_utils/functional.py", line 45, in inner
    return callback(fn(*args, **kwargs))
  File "/home/user/.local/lib/python3.8/site-packages/eth_account/_utils/structured_data/hashing.py", line 232, in _encode_data
    array_items_encoding = [
  File "/home/user/.local/lib/python3.8/site-packages/eth_account/_utils/structured_data/hashing.py", line 233, in <listcomp>
    encode_data(parsed_type.base, types, array_item)
  File "/home/user/.local/lib/python3.8/site-packages/eth_account/_utils/structured_data/hashing.py", line 262, in encode_data
    data_types_and_hashes = _encode_data(primaryType, types, data)
  File "/home/user/.local/lib/python3.8/site-packages/eth_utils/functional.py", line 45, in inner
    return callback(fn(*args, **kwargs))
  File "/home/user/.local/lib/python3.8/site-packages/eth_account/_utils/structured_data/hashing.py", line 174, in _encode_data
    yield "bytes32", hash_struct_type(primary_type, types)
  File "/home/user/.local/lib/python3.8/site-packages/eth_account/_utils/structured_data/hashing.py", line 96, in hash_struct_type
    return keccak(text=encode_type(primary_type, types))
  File "/home/user/.local/lib/python3.8/site-packages/eth_account/_utils/structured_data/hashing.py", line 83, in encode_type
    deps = get_dependencies(primary_type, types)
  File "/home/user/.local/lib/python3.8/site-packages/eth_account/_utils/structured_data/hashing.py", line 40, in get_dependencies
    fields = types[struct_name]
KeyError: 'address'
```

### Environment

```sh
Python version:
3.8.10 (default, Nov 26 2021, 20:14:08) 
[GCC 9.3.0]

Operating System: Linux-5.10.16.3-microsoft-standard-WSL2-x86_64-with-glibc2.29

pip freeze result:
aiohttp==3.8.1
aiosignal==1.2.0
async-timeout==4.0.2
attrs==19.3.0
Automat==0.8.0
base58==2.1.1
bitarray==1.2.2
blinker==1.4
certifi==2019.11.28
chardet==3.0.4
charset-normalizer==2.0.12
click==8.0.4
cloud-init==20.1
colorama==0.4.3
command-not-found==0.3
configobj==5.0.6
constantly==15.1.0
cryptography==2.8
cytoolz==0.11.2
dbus-python==1.2.16
distro==1.4.0
distro-info===0.23ubuntu1
dnspython==2.2.0
entrypoints==0.3
eth-abi==3.0.0
eth-account==0.6.1
eth-hash==0.3.2
eth-keyfile==0.6.0
eth-keys==0.4.0
eth-rlp==0.3.0
eth-typing==3.0.0
eth-utils==2.0.0
Flask==2.0.1
Flask-Cors==3.0.10
frozenlist==1.3.0
hexbytes==0.2.2
httplib2==0.14.0
hyperlink==19.0.0
idna==2.8
importlib-metadata==1.5.0
incremental==16.10.1
ipfshttpclient==0.8.0a2
itsdangerous==2.1.0
Jinja2==3.0.3
jsonpatch==1.22
jsonpointer==2.0
jsonschema==3.2.0
keyring==18.0.1
language-selector==0.1
launchpadlib==1.10.13
lazr.restfulclient==0.14.2
lazr.uri==1.0.3
lru-dict==1.1.7
MarkupSafe==2.1.0
more-itertools==4.2.0
multiaddr==0.0.9
multidict==6.0.2
netaddr==0.8.0
netifaces==0.10.4
oauthlib==3.1.0
parsimonious==0.8.1
protobuf==3.19.4
pyasn1==0.4.2
pyasn1-modules==0.2.1
pycryptodome==3.14.1
PyGObject==3.36.0
PyHamcrest==1.9.0
PyJWT==1.7.1
pymacaroons==0.13.0
pymongo==3.11.4
PyNaCl==1.3.0
pyOpenSSL==19.0.0
pyrsistent==0.15.5
pyserial==3.4
python-apt==2.0.0
python-debian===0.1.36ubuntu1
python-dotenv==0.19.2
PyYAML==5.3.1
requests==2.25.1
requests-unixsocket==0.2.0
rlp==3.0.0
SecretStorage==2.3.1
service-identity==18.1.0
simplejson==3.16.0
six==1.16.0
ssh-import-id==5.10
stripe==2.66.0
systemd-python==234
toolz==0.11.2
Twisted==18.9.0
ubuntu-advantage-tools==20.3
ufw==0.36
unattended-upgrades==0.1
urllib3==1.25.8
varint==1.0.2
wadllib==1.3.3
web3==5.28.0
websockets==9.1
Werkzeug==2.0.3
yarl==1.7.2
zipp==1.0.0
zope.interface==4.7.1
``` 

## How can it be fixed?

Unsure of how to solve it


Discussion:
pacrob: Sorry for the delay on this, but I'm still checking edge cases. This fix does provide the same output as Metamask for a single-depth array of a user-defined type, but chokes on single-depth arrays of `string` or `uint256`, for example. And the outputs diverge on multi-dimensional arrays.

Thanks for your patience! I'll have it out asap.
pacrob: The main functionality and error messaging has been added. I'll do more testing tomorrow.
fselmo: @doubleukay @pacrob thanks again for taking the time to get this moving! This is in pretty high in demand especially as of late.

I added a commit to address the test cases I was seeing, from my review yesterday, when comparing with how solidity accepts nested array types. It seems we had it inverted in the library, which was fine in the past since we purposefully at some point decided to keep all arrays to a single dimension. Now that we opened it up to multiple dimensions, it appeared to be inverted.

It's important to note all tests added here by @pacrob and the tweaks I made to them were tested against the current `eth-sig-utils` which is what Metamask is using under the hood and they all match.

I don't see too much at the moment except I'd like to eventually look into some ABI types that might be possible to define in the schema that may not be acceptable array types... but this is a huge improvement on what was already here, especially towards EIP-712 / signTypedData_V4 compatibility.

Anything else you can think of that might be useful to include here? Any more tests you can think of?

Patch:
diff --git a/.pydocstyle.ini b/.pydocstyle.ini
index 0d40aa8..5aab5d0 100644
--- a/.pydocstyle.ini
+++ b/.pydocstyle.ini
@@ -21,7 +21,8 @@ select=D2,D3,D4
 ; D400 - First line should end with a period
 ; D401 - First line should be in imperative mood
 ; D412 - No blank lines allowed between a section header and its content
-add-ignore=D200,D203,D204,D205,D212,D302,D400,D401,D412
+; D415 - First line should end with a period, question mark, or exclamation point
+add-ignore=D200,D203,D204,D205,D212,D302,D400,D401,D412,D415
 
 ; Explanation:
 ; D400 - Enabling this error code seems to make it a requirement that the first
diff --git a/eth_account/_utils/structured_data/hashing.py b/eth_account/_utils/structured_data/hashing.py
index fa5d345..9ac5298 100644
--- a/eth_account/_utils/structured_data/hashing.py
+++ b/eth_account/_utils/structured_data/hashing.py
@@ -15,10 +15,8 @@ from eth_abi.grammar import (
     parse,
 )
 from eth_utils import (
-    ValidationError,
     keccak,
     to_tuple,
-    toolz,
 )
 
 from .validation import (
@@ -39,16 +37,25 @@ 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)
 
@@ -97,9 +104,7 @@ 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
@@ -121,147 +126,116 @@ 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.
+    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 (2, 3)
+    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))}`"
                 )
-            # 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, array_dimension in enumerate(array_dimensions):
-                if len(parsed_type.arrlist[i]) == 0:
-                    # Skip empty or dynamically declared dimensions
-                    continue
-                if array_dimension != 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_abi(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 value is encodable as the specified type
-            if is_encodable(field["type"], value):
-                # field["type"] is a valid type and the provided value is encodable as that type
-                yield field["type"], value
-            else:
-                raise TypeError(
-                    f"Value of `{field['name']}` ({value}) in the struct `{primary_type}` is not "
-                    f"encodable as the specified type `{field['type']}`. If the base type is "
-                    "correct, make sure the value does not exceed the specified size for the type."
-                )
 
+def encode_data(primary_type, types, data):
+    encoded_types = ['bytes32']
+    encoded_values = [hash_struct_type(primary_type, types)]
 
-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)
+    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)
+
+    return encode_abi(encoded_types, encoded_values)
 
 
 def load_and_validate_structured_message(structured_json_string_data):
diff --git a/newsfragments/175.breaking.rst b/newsfragments/175.breaking.rst
new file mode 100644
index 0000000..05974ea
--- /dev/null
+++ b/newsfragments/175.breaking.rst
@@ -0,0 +1,1 @@
+Fix errors in EIP-712 signing


Test patch:
diff --git a/tests/core/test_structured_data_signing.py b/tests/core/test_structured_data_signing.py
index ec4a156..1f1046b 100644
--- a/tests/core/test_structured_data_signing.py
+++ b/tests/core/test_structured_data_signing.py
@@ -114,6 +114,189 @@ def eip712_with_array_message_encodings(request, eip712_example_with_array_json_
         raise Exception("Unreachable")
 
 
+# --- EIP 712 example with added multiple array fields --- #
+
+@pytest.fixture
+def eip712_example_with_multi_array_json_string():
+    return open("tests/fixtures/valid_eip712_example_with_multi_array.json", "r").read()
+
+
+@pytest.fixture
+def eip712_example_with_multi_array_types(eip712_example_with_multi_array_json_string):
+    return json.loads(eip712_example_with_multi_array_json_string)["types"]
+
+
+@pytest.fixture
+def eip712_example_with_multi_array_domain_type(eip712_example_with_multi_array_json_string):
+    return json.loads(eip712_example_with_multi_array_json_string)["types"]["EIP712Domain"]
+
+
+@pytest.fixture
+def eip712_example_with_multi_array_message(eip712_example_with_multi_array_json_string):
+    return json.loads(eip712_example_with_multi_array_json_string)["message"]
+
+
+@pytest.fixture
+def eip712_example_with_multi_array_domain(eip712_example_with_multi_array_json_string):
+    return json.loads(eip712_example_with_multi_array_json_string)["domain"]
+
+
+@pytest.fixture(params=("text", "dict", "primitive", "hexstr"))
+def eip712_with_multi_array_message_encodings(request, eip712_example_with_multi_array_json_string):
+    if request.param == "text":
+        return {"text": eip712_example_with_multi_array_json_string}
+    elif request.param == "primitive":
+        return {"primitive": eip712_example_with_multi_array_json_string.encode()}
+    elif request.param == "dict":
+        return {"primitive": json.loads(eip712_example_with_multi_array_json_string)}
+    elif request.param == "hexstr":
+        return {"hexstr": eip712_example_with_multi_array_json_string.encode().hex()}
+    else:
+        raise Exception("Unreachable")
+
+
+@pytest.mark.parametrize(
+    'primary_type, types, eip712_data, expected_hex',
+    (
+        (
+            "BoolInt",
+            {
+                "BoolInt":
+                    [
+                        {"name": "bool", "type": "bool"},
+                        {"name": "bool_a", "type": "bool[]"},
+                        {"name": "int", "type": "uint256"},
+                        {"name": "int_a", "type": "uint256[][3]"},
+                    ]
+            },
+            {
+                "bool": True,
+                "bool_a": [False, True],
+                "int": 212,
+                "int_a": [[212], [], [12, 24, 36]],
+            },
+            "8d06a59c4f3179fc688615a1d69d61ee86c9683a5c3af150874396b3021892200000000000"
+            "000000000000000000000000000000000000000000000000000001a6eef7e35abe70267296"
+            "41147f7915573c7e97b47efa546f5f6e3230263bcb49000000000000000000000000000000"
+            "00000000000000000000000000000000d4eb11d95b7e83141a4db340518d5fc187abde6f73"
+            "e97cb1a5c7854c23c8cdf8fa"
+        ),
+        (
+
+            "StringBytes",
+            {
+                "StringBytes":
+                    [
+                        {"name": "string", "type": "string"},
+                        {"name": "string_a", "type": "string[]"},
+                        {"name": "bytes", "type": "bytes"},
+                        {"name": "bytes_a", "type": "bytes[]"},
+                    ]
+            },
+            {
+                "string": 'spam',
+                "string_a": ['spam', 'eggs'],
+                "bytes": b'snekshak',
+                "bytes_a": [b'shake', b'snake'],
+            },
+            "763a70467632fd4d114a365d4f03c5886ce9447950874f45cbd42421cbc5f17d"
+            "000e3bc84207015e1ae7e42b8679963a82088323f7ef1b456c44eda274f579f6"
+            "3b78c672c715dfcb45484f0c5a488c1a20679e7e4ec1d980f43165671b70a3e4"
+            "9079dbeec4d62f4344389883b7fe5fcedc73483b9d6a4774ab0857bedd7dd14d"
+            "7578ae1ed519e90d30b2e031348369679d8c09f47f717eb17de4bce1a7ad5d6b"
+        ),
+    )
+)
+def test_encode_data_basic(primary_type, types, eip712_data, expected_hex):
+    assert encode_data(primary_type, types, eip712_data).hex() == expected_hex  # noqa: E501
+
+
+@pytest.mark.parametrize(
+    'primary_type, types, eip712_data, error_type, error_message',
+    (
+        (
+            "Missing",
+            {
+                "Missing":
+                    [
+                        {"name": "bool", "type": "bool"},
+                    ]
+            },
+            {
+                "bool": None,
+            },
+            ValueError,
+            "Missing value for field bool of type bool"
+        ),
+        (
+            "NotBytes",
+            {
+                "NotBytes":
+                    [
+                        {"name": "bytes", "type": "bytes"},
+                    ]
+            },
+            {
+                "bytes": 212,
+            },
+            TypeError,
+            "Value of field `bytes` (212) is of the type `<class 'int'>`, "
+            "but expected bytes value"
+        ),
+        (
+            "NotString",
+            {
+                "NotString":
+                    [
+                        {"name": "string", "type": "string"},
+                    ]
+            },
+            {
+                "string": b'snakes',
+            },
+            TypeError,
+            "Value of field `string` (b'snakes') is of the type `<class 'bytes'>`, "
+            "but expected string value"
+        ),
+        (
+            "NotEncodableType",
+            {
+                "NotEncodableType":
+                    [
+                        {"name": "wat", "type": "wat"},
+                    ]
+            },
+            {
+                "wat": "huh",
+            },
+            TypeError,
+            "Received Invalid type `wat` in field `wat`"
+        ),
+        (
+            "NotEncodableAsType",
+            {
+                "NotEncodableAsType":
+                    [
+                        {"name": "wat", "type": "uint256"},
+                    ]
+            },
+            {
+                "wat": True,
+            },
+            TypeError,
+            "Value of `wat` (True) is not encodable as type `uint256`. "
+            "If the base type is correct, verify that the value does not "
+            "exceed the specified size for the type."
+        ),
+    )
+)
+def test_encode_data_error_messages(primary_type, types, eip712_data, error_type, error_message):
+
+    with pytest.raises(error_type) as e:
+        assert encode_data(primary_type, types, eip712_data)
+    assert str(e.value) == error_message
+
+
 @pytest.mark.parametrize(
     'primary_type, expected',
     (
@@ -138,6 +321,21 @@ def test_get_dependencies_eip712_with_array(
     assert get_dependencies(primary_type, eip712_example_with_array_types) == expected
 
 
+@pytest.mark.parametrize(
+    'primary_type, expected',
+    (
+        ('Mail', ('Person', 'Company')),
+        ('Person', ('Company',)),
+        ('Company', ()),
+    )
+)
+def test_get_dependencies_eip712_with_multi_array(
+    primary_type, expected, eip712_example_with_multi_array_types
+):
+    assert set(get_dependencies(
+        primary_type, eip712_example_with_multi_array_types)) == set(expected)
+
+
 @pytest.mark.parametrize(
     'struct_name, expected',
     (
@@ -162,6 +360,26 @@ def test_encode_struct_eip712_with_array(struct_name, expected, eip712_example_w
     assert encode_struct(struct_name, eip712_example_with_array_types[struct_name]) == expected
 
 
+@pytest.mark.parametrize(
+    'struct_name, expected',
+    (
+        ("Mail", "Mail(uint256[] ids,Person from,Person to,Person[] cc,Person[][] bcc,string contents,string[3][4][2] tags)"),  # noqa: E501
+        ("Person", "Person(string name,Company company,string[] aliases,address wallet)"),
+        ("Company", "Company(string name,uint256 id)"),
+        ("EIP712Domain", "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),  # noqa: E501
+    )
+)
+def test_encode_struct_eip712_with_multi_array(
+    struct_name,
+    expected,
+    eip712_example_with_multi_array_types
+):
+    assert encode_struct(
+        struct_name,
+        eip712_example_with_multi_array_types[struct_name]
+    ) == expected
+
+
 @pytest.mark.parametrize(
     'primary_type, expected',
     (
@@ -184,6 +402,23 @@ def test_encode_type_eip712_with_array(primary_type, expected, eip712_example_wi
     assert encode_type(primary_type, eip712_example_with_array_types) == expected
 
 
+@pytest.mark.parametrize(
+    'primary_type, expected',
+    (
+        ('Mail', 'Mail(uint256[] ids,Person from,Person to,Person[] cc,Person[][] bcc,string contents,string[3][4][2] tags)Company(string name,uint256 id)Person(string name,Company company,string[] aliases,address wallet)'),  # noqa: E501
+        ('Person', 'Person(string name,Company company,string[] aliases,address wallet)Company(string name,uint256 id)'),  # noqa: E501
+
+        ('Company', 'Company(string name,uint256 id)'),
+    )
+)
+def test_encode_type_eip712_with_multi_array(
+    primary_type,
+    expected,
+    eip712_example_with_multi_array_types
+):
+    assert encode_type(primary_type, eip712_example_with_multi_array_types) == expected
+
+
 @pytest.mark.parametrize(
     'primary_type, expected_hex',
     (
@@ -208,6 +443,20 @@ def test_hash_struct_type_eip712_with_array(
     assert hash_struct_type(primary_type, eip712_example_with_array_types).hex() == expected_hex
 
 
+@pytest.mark.parametrize(
+    'primary_type, expected_hex',
+    (
+        ('Company', '6ca77e7c32d0c89b799964b908f09a67d27441f2c23a6fb1fe12744a6ad629f7'),
+        ('Person', 'c7b964db9fb195cb39c83204fdf1f0f406b6a2f3e463db9cad34aa0c040380a9'),
+        ('Mail', '1cf41a63c532d5c0c81650d839a38629cc2ab400a6a4bd000b4188adf9dd7540'),
+    )
+)
+def test_hash_struct_type_eip712_with_multi_array(
+    primary_type, expected_hex, eip712_example_with_multi_array_types
+):
+    assert hash_struct_type(primary_type, eip712_example_with_multi_array_types).hex() == expected_hex  # noqa: E501
+
+
 def test_encode_data_eip712(eip712_example_types, eip712_example_message):
     primary_type = "Mail"
     expected_hex = (
@@ -226,12 +475,28 @@ def test_encode_data_eip712_with_array(
     expected_hex = (
         "b19c5c7781083ed7325660d847f8c547b744917d67b38858b00bce7e62a4866bfc71e5fa27ff56c350aa"
         "531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221"
-        "c8ac3492d9d872b041d703d1a88b5e35e8f6bccc3d72ef467ed581615888f8cf34df19d068d04225439b"
-        "e65ab5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8"
+        "c8ac3492d9d872b041d703d105a7f44bc01de4515b63df31009d8ce4fe8b06eb06a1ff4eec0b562d1c70"
+        "3535b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8"
     )
     assert encode_data(primary_type, eip712_example_with_array_types, eip712_example_with_array_message).hex() == expected_hex  # noqa: E501
 
 
+def test_encode_data_eip712_with_multi_array(
+    eip712_example_with_multi_array_types, eip712_example_with_multi_array_message
+):
+    primary_type = "Mail"
+    expected_hex = (
+        "1cf41a63c532d5c0c81650d839a38629cc2ab400a6a4bd000b4188adf9dd754082ebb1c2e604ce"
+        "0ff44d8591ae5cf16cbafd622284d40b37011b12dca2e2d9e4a30aec196a390f2dc9de7320f587"
+        "4214e5a7929e08a7a81d9ef96666fdcbab35e1f45419ba2d57832f702e17b344e470f556cda74e"
+        "91e703303656404b77fec1c6c072aa6915cd93401ec3be997eedadb9bb3f6300ab5c9e61808efa"
+        "bf7da54687a9c71c4a727b1ca222ac101d1fc58980c48ad66baac15c14a68bbaaced314fb5aadf"
+        "3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc888e7f5caefc6a01fd300"
+        "aaa708d99f8c04ed97879c2fc16950cb82957ff79099"
+    )
+    assert encode_data(primary_type, eip712_example_with_multi_array_types, eip712_example_with_multi_array_message).hex() == expected_hex  # noqa: E501
+
+
 def test_hash_struct_main_message_eip712(eip712_example_json_string):
     structured_data = json.loads(eip712_example_json_string)
     expected_hex = "c52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e"
@@ -240,7 +505,13 @@ def test_hash_struct_main_message_eip712(eip712_example_json_string):
 
 def test_hash_struct_main_message_eip712_with_array(eip712_example_with_array_json_string):
     structured_data = json.loads(eip712_example_with_array_json_string)
-    expected_hex = "76649452daa3e4101beef5b01669fd0de45ece35188d529703c73526a2454521"
+    expected_hex = "04e58c373e4cd0695f0c2d6e2d8e379d6b51b987cf27d582dd94a03532d430d3"
+    assert hash_message(structured_data).hex() == expected_hex
+
+
+def test_hash_struct_main_message_eip712_with_multi_array(eip712_example_with_multi_array_json_string):  # noqa: E501
+    structured_data = json.loads(eip712_example_with_multi_array_json_string)
+    expected_hex = "6c5faac29b3f4552147e5cfef75b4ac09fdc7668ba1208c5f153f047a0da4819"
     assert hash_message(structured_data).hex() == expected_hex
 
 
@@ -256,6 +527,12 @@ def test_hash_struct_domain_eip712_with_array(eip712_example_with_array_json_str
     assert hash_domain(structured_data).hex() == expected_hex
 
 
+def test_hash_struct_domain_eip712_with_multi_array(eip712_example_with_multi_array_json_string):
+    structured_data = json.loads(eip712_example_with_multi_array_json_string)
+    expected_hex = 'f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f'
+    assert hash_domain(structured_data).hex() == expected_hex
+
+
 def test_hashed_structured_data_eip712(eip712_message_encodings):
     structured_msg = encode_structured_data(**eip712_message_encodings)
     hashed_structured_msg = _hash_eip191_message(structured_msg)
@@ -263,10 +540,17 @@ def test_hashed_structured_data_eip712(eip712_message_encodings):
     assert hashed_structured_msg.hex() == expected_hex
 
 
-def test_hashed_structured_data_eip127_with_array(eip712_with_array_message_encodings):
+def test_hashed_structured_data_eip712_with_array(eip712_with_array_message_encodings):
     structured_msg = encode_structured_data(**eip712_with_array_message_encodings)
     hashed_structured_msg = _hash_eip191_message(structured_msg)
-    expected_hex = '4e3c4173651b3054c0635dfae57a3b65ae1527ed0ba9ff76cecb6d9807687a7f'
+    expected_hex = '1780e7e042fa9ec126ccb68cd707d61580d00601b3eff8a5ec05116b46007fdb'
+    assert hashed_structured_msg.hex() == expected_hex
+
+
+def test_hashed_structured_data_eip712_with_multi_array(eip712_with_multi_array_message_encodings):
+    structured_msg = encode_structured_data(**eip712_with_multi_array_message_encodings)
+    hashed_structured_msg = _hash_eip191_message(structured_msg)
+    expected_hex = "da275374c3a0790d389b6490d6fdf499f39c72105423f09d7b9be62d2fc08671"
     assert hashed_structured_msg.hex() == expected_hex
 
 
@@ -286,6 +570,14 @@ def test_signature_verification_eip712_with_array(eip712_with_array_message_enco
     assert new_addr == account.address
 
 
+def test_signature_verification_eip712_with_multi_array(eip712_with_multi_array_message_encodings):
+    account = Account.create()
+    structured_msg = encode_structured_data(**eip712_with_multi_array_message_encodings)
+    signed = Account.sign_message(structured_msg, account.key)
+    new_addr = Account.recover_message(structured_msg, signature=signed.signature)
+    assert new_addr == account.address
+
+
 def test_signature_variables_eip712(eip712_message_encodings):
     # Check that the signature of typed message is the same as that
     # mentioned in the EIP. The link is as follows
@@ -311,8 +603,21 @@ def test_signature_variables_eip712_with_array(eip712_with_array_message_encodin
 
     sig = Account.sign_message(structured_msg, private_key)
     assert sig.v == 27
-    assert hex(sig.r) == "0x58635e9afd7a2a5338cf2af3d711b50235a1955c43f8bca1657c9d0834fcdb5a"
-    assert hex(sig.s) == "0x44a7c0169616cfdfc16815714c9bc1c94139e17a0761a17530cf3dd1746bc10b"
+    assert hex(sig.r) == "0x315bd45341016e306aa54a7aaf002104b7a5b51903be6ca04cef9d49555319f1"
+    assert hex(sig.s) == "0x46afff92b3504a3ab106dd96c414e38709515d884433816801ddde8d10136c8c"
+
+
+def test_signature_variables_eip712_with_multi_array(eip712_with_multi_array_message_encodings):
+    structured_msg = encode_structured_data(**eip712_with_multi_array_message_encodings)
+
+    private_key = keccak(text="cow")
+    acc = Account.from_key(private_key)
+    assert HexBytes(acc.address) == HexBytes("0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826")
+
+    sig = Account.sign_message(structured_msg, private_key)
+    assert sig.v == 27
+    assert hex(sig.r) == "0x873be7f5ac9688ea3675b0c44916340883103053de9d1f9d0c1b6e9758a78a2b"
+    assert hex(sig.s) == "0x6309769ff21b7a4f610316b249b004d4f9e9e9d6df2634d9bc0f492cda6b9c06"
 
 
 def test_hashed_structured_data_with_bytes(eip712_example_with_array_json_string):
@@ -324,7 +629,7 @@ def test_hashed_structured_data_with_bytes(eip712_example_with_array_json_string
 
     structured_msg = encode_structured_data(structured_data)
     hashed_structured_msg = _hash_eip191_message(structured_msg)
-    expected_hash_value_hex = "a06e87f57db20fed78428850bfe02a67d4c6c0f9bcb582b860299b21f591b1c6"
+    expected_hash_value_hex = "6de2148c1b98a586b42eaac9bbd72c4af5ee490fc1ec53fe483667c8fb38d10d"
     assert hashed_structured_msg.hex() == expected_hash_value_hex
 
 
@@ -337,7 +642,7 @@ def test_hashed_structured_data_with_bytes32(eip712_example_with_array_json_stri
 
     structured_msg = encode_structured_data(structured_data)
     hashed_structured_msg = _hash_eip191_message(structured_msg)
-    expected_hash_value_hex = "ff79c92bdd076a8ec12b146f82a278b30f4da5d402382e215abda4c3c186257e"
+    expected_hash_value_hex = "c9f862d796deff552a90d7de1faff9a4b7f99726bf7518dc00073d23373d0530"
     assert hashed_structured_msg.hex() == expected_hash_value_hex
 
 
@@ -350,7 +655,7 @@ def test_hashed_structured_data_with_nested_structs():
     structured_msg = encode_structured_data(structured_data)
     hashed_structured_msg = _hash_eip191_message(structured_msg)
 
-    expected_hash_value_hex = "1a9c2ba3822f4f5498e4dfe3593ad69c9135a9f038334b49fa54537cfa9f5244"
+    expected_hash_value_hex = "0c9b68928afbff5af30e99ae29fcdf1cc36974f7ba4a6e4336cc98daa4f0c2af"
     assert hashed_structured_msg.hex() == expected_hash_value_hex
 
 
@@ -419,16 +724,16 @@ def test_structured_data_invalid_type_filtered_by_regex():
     assert str(e.value) == "Invalid Type `Hello Person` in `Mail`"
 
 
-def test_invalid_structured_data_value_type_mismatch_in_primary_type():
+def test_invalid_structured_data_value_type_mismatch_in_type():
     # Given type is valid (string), but the value (int) is not of the mentioned type
     invalid_structured_data_string = open(
-        "tests/fixtures/invalid_message_value_type_mismatch_primary_type.json"
+        "tests/fixtures/invalid_message_value_type_mismatch_type.json"
     ).read()
     invalid_structured_data = json.loads(invalid_structured_data_string)
-
     with pytest.raises(
         TypeError,
-        match="in the struct `Mail` is of the type `<class 'int'>`, but expected string value"
+        match="Value of field `contents` \\(12345\\) is of the type `<class 'int'>`, "
+              "but expected string value"
     ):
         hash_message(invalid_structured_data)
 
@@ -439,7 +744,7 @@ def test_invalid_structured_data_invalid_abi_type():
         "tests/fixtures/invalid_message_invalid_abi_type.json"
     ).read()
     invalid_structured_data = json.loads(invalid_structured_data_string)
-    with pytest.raises(TypeError, match="Received Invalid type `uint25689` in the struct `Person`"):
+    with pytest.raises(TypeError, match="Received Invalid type `uint25689` in field `balance`"):
         hash_message(invalid_structured_data)
 
 
@@ -450,22 +755,26 @@ def test_structured_data_invalid_identifier_filtered_by_abi_encodable_function()
         "tests/fixtures/invalid_message_valid_abi_type_invalid_value.json"
     ).read()
     invalid_structured_data = json.loads(invalid_structured_data_string)
-    with pytest.raises(TypeError) as e:
+    with pytest.raises(
+        TypeError,
+        match="Value of `balance` \\(how do you do\\?\\) is not encodable as type "
+              "`uint256`. If the base type is correct, verify that the "
+              "value does not exceed the specified size for the type."
+    ):
         hash_message(invalid_structured_data)
-    assert (
-        str(e.value) == "Value of `balance` (how do you do?) in the struct `Person` is not "
-                        "encodable as the specified type `uint256`. If the base type is correct, "
-                        "make sure the value does not exceed the specified size for the type."
-    )
 
 
 @pytest.mark.parametrize(
     'data, expected',
     (
-        ([[1, 2, 3], [4, 5, 6]], (2, 3)),
-        ([[1, 2, 3]], (1, 3)),
+        ([[1, 2, 3], [4, 5, 6]], (3, 2)),
+        ([[1, 2, 3]], (3, 1)),
         ([1, 2, 3], (3,)),
         ([[[1, 2], [3, 4], [5, 6]], [[7, 8], [9, 10], [11, 12]]], (2, 3, 2)),
+
+        # test dynamic when varying array sizes exist within the same dimension
+        ([[212], [], [12, 24, 36]], ("dynamic", 3)),
+        ([[212], [], [12, [22, 24], [36]]], ("dynamic", "dynamic", 3)),
     )
 )
 def test_get_array_dimensions(data, expected):
@@ -503,10 +812,10 @@ def test_unequal_array_dimension_between_schema_and_data():
         str(e.value) == (
             "Array data "
             "`[{'name': 'Bob', 'wallet': '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'}]` has "
-            "dimensions `(1,)` whereas the schema has dimensions `(2, 3, 4)`"
+            "dimensions `(1,)` whereas the schema has dimensions `(2, 'dynamic', 4)`"
         ) or str(e.value) == (
             "Array data "
             "`[{'wallet': '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', 'name': 'Bob'}]` has "
-            "dimensions `(1,)` whereas the schema has dimensions `(2, 3, 4)`"
+            "dimensions `(1,)` whereas the schema has dimensions `(2, 'dynamic', 4)`"
         )
     )
diff --git a/tests/fixtures/invalid_message_unequal_array_dimensions.json b/tests/fixtures/invalid_message_unequal_array_dimensions.json
index 345f7df..2ac4781 100644
--- a/tests/fixtures/invalid_message_unequal_array_dimensions.json
+++ b/tests/fixtures/invalid_message_unequal_array_dimensions.json
@@ -12,7 +12,7 @@
 		],
 		"Mail": [
 			{"name": "from", "type": "person"},
-			{"name": "to", "type": "person[2][3][4]"},
+			{"name": "to", "type": "person[2][][4]"},
 			{"name": "contents", "type": "string"}
 		]
 	},
diff --git a/tests/fixtures/invalid_message_value_type_mismatch_primary_type.json b/tests/fixtures/invalid_message_value_type_mismatch_type.json
similarity index 100%
rename from tests/fixtures/invalid_message_value_type_mismatch_primary_type.json
rename to tests/fixtures/invalid_message_value_type_mismatch_type.json
diff --git a/tests/fixtures/valid_eip712_example_with_multi_array.json b/tests/fixtures/valid_eip712_example_with_multi_array.json
new file mode 100644
index 0000000..636693e
--- /dev/null
+++ b/tests/fixtures/valid_eip712_example_with_multi_array.json
@@ -0,0 +1,112 @@
+{
+	"types": {
+		"EIP712Domain": [
+			{"name": "name", "type": "string"},
+			{"name": "version", "type": "string"},
+			{"name": "chainId", "type": "uint256"},
+			{"name": "verifyingContract", "type": "address"}
+		],
+		"Company": [
+			{"name": "name", "type": "string"},
+			{"name": "id", "type": "uint256"}
+		],
+		"Person": [
+			{"name": "name", "type": "string"},
+			{"name": "company", "type": "Company"},
+			{"name": "aliases", "type": "string[]"},
+			{"name": "wallet", "type": "address"}
+		],
+		"Mail": [
+			{"name": "ids", "type": "uint256[]"},
+			{"name": "from", "type": "Person"},
+			{"name": "to", "type": "Person"},
+			{"name": "cc", "type": "Person[]"},
+			{"name": "bcc", "type": "Person[][]"},
+			{"name": "contents", "type": "string"},
+			{"name": "tags", "type": "string[3][4][2]"}
+		]
+	},
+	"primaryType": "Mail",
+	"domain": {
+		"name": "Ether Mail",
+		"version": "1",
+		"chainId": 1,
+		"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
+	},
+	"message": {
+		"ids": [2, 3, 5, 7, 11, 13, 17, 19],
+		"from": {
+			"name": "Cow",
+			"company": {
+				"name": "farm",
+				"id": 1
+			},
+			"aliases": ["Vaca", "Корова", "Koe"],
+			"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
+		},
+		"to": {
+			"name": "Bob",
+			"company": {
+				"name": "fishery",
+				"id": 2
+			},
+			"aliases": ["Bib", "Bab"],
+			"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
+		},
+		"cc": [
+			{
+				"name": "Alice",
+				"company": {
+					"name": "theme-park",
+					"id": 3
+				},
+				"aliases": [],
+				"wallet": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+			},
+			{
+				"name": "Dot",
+				"company": {
+					"name": "ice-cream",
+					"id": 4
+				},
+				"aliases": ["Dash"],
+				"wallet": "0xdddddddddddddddddddddddddddddddddddddddd"
+			}
+		],
+		"bcc": [[
+			{
+				"name": "Earnest",
+				"company": {
+					"name": "eatery",
+					"id": 5
+				},
+				"aliases": [],
+				"wallet": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
+			},
+			{
+				"name": "Felicia",
+				"company": {
+					"name": "fine dining",
+					"id": 6
+				},
+				"aliases": ["Fiona"],
+				"wallet": "0xffffffffffffffffffffffffffffffffffffffff"
+			}
+		]],
+		"contents": "Hello, Bob!",
+		"tags": [
+			[
+				["ab", "cd", "ss"],
+				["ef", "gh", "ss"],
+				["ij", "kl", "ss"],
+				["mn", "op", "ss"]
+			],
+			[
+				["ch", "de", "ss"],
+				["gb", "cn", "ss"],
+				["mx", "cz", "ss"],
+				["jp", "co", "ss"]
+			]
+		]
+	}
+}