Skip to content

Commit 8ce7035

Browse files
Allow TypedData revision 1 (#1360)
* Differentiate typed data files by revision * Add `HashMethod` * Add `Revision` enum; Rename `StarkNetDomain` to `Domain` * Add tests to revision 1 * Remove prints * Add more rev 1 test cases to `test_type_hash` * Change values param to `List[int]` in `HashMethod.hash()` * Add `DomainSchema`; Add `RevisionField` and `ChainIdField` * Move `HashMethod` to separate file * Refactor `Domain.to_dict()` * Update comment * Add `TypedData._hash_method` field * Remove `TypedData._hash_method` field * Update docs * Update docs * Update comment * Reduce `Domain._verify_types()` * Remove unnecessary variable duplication * Remove unused functions * Convert `TypedData._hash_method` to `@property` * Change `Domain.revision` type to `Revision` * Use equality comparison for `resolved_revision` * Set `revision` in `Domain` typed dict to be `Optional` * Update import * Refactor `Domain.to_dixt()` * Fix doc examples for `sign_message` and `verify_message`; Update function descriptions * Remove unused import * Remove `test_sign_message_rev_v0` and `test_verify_message_rev_v0` * Change `sign_message()` and `verify_message()` to accept `TypedData` dataclass * Update migration guide * Update `test_sign_offchain_message()` to use `TypedData` class instance * Update previous changes in `sign_message()` and `verify_message()` * Update `sign_message()` and `verify_message()` description comments * Minor `DomainSchema.make_dataclass()` refactor * Restore retrieving `revision` using `data.get()` * Add missing type annotation in `test_invalid_types()` * Update `RevisionField._deserialize()` return type to `Revision` * Add custom error message on missing value in `Revision` enum * Add module in migration guide to enable hyperlinks * Check error message in `test_invalid_types` * Add `Parameter.to_dict()` and `TypedData.to_dict()` * Move `Revision` to common schemas * Format * Set `ChainIdField._deserialize()` return type to `str`; Change 'DomainDict.chainId' type to `str` * Remove unnecessary import * Restore previous import of `keccak256` * Format * Add newline in typed data rev 1 example json * Remove unnecessary `ChainIdField` * Change chainId from `int` to `str` in all examples * Refactor `TypedData.to_dict()` * Remove `Parameter.to_dict()`; Add `RevisionField._serialize()` * Update starknet_py/net/schemas/common.py Co-authored-by: ddoktorski <[email protected]> * Update starknet_py/net/schemas/common.py Co-authored-by: ddoktorski <[email protected]> --------- Co-authored-by: ddoktorski <[email protected]>
1 parent 56c60c1 commit 8ce7035

16 files changed

+371
-55
lines changed

docs/api/typed_data.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ Parameter
1717
:exclude-members: __init__
1818

1919
--------------
20-
StarkNetDomain
20+
Domain
2121
--------------
2222

23-
.. autoclass:: starknet_py.net.models.typed_data.StarkNetDomainDict
23+
.. autoclass:: starknet_py.utils.typed_data.Domain
2424
:members:
2525
:undoc-members:
26+
:exclude-members: __init__
27+
:member-order: bysource
28+

docs/migration_guide.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
Migration guide
22
===============
33

4+
******************************
5+
0.23.0 Migration guide
6+
******************************
7+
8+
0.23.0 Targeted versions
9+
------------------------
10+
11+
- Starknet - `0.13.1.1 <https://docs.starknet.io/documentation/starknet_versions/version_notes/#version0.13.1.1>`_
12+
- RPC - `0.7.1 <https://github.com/starkware-libs/starknet-specs/releases/tag/v0.7.1>`_
13+
14+
0.23.0 Breaking changes
15+
-----------------------
16+
.. currentmodule:: starknet_py.utils.typed_data
17+
18+
1. :class:`StarkNetDomain` has been renamed to :class:`Domain`
19+
20+
2. :class:`TypedData` field ``domain`` has been changed from ``dict`` to :class:`Domain`
21+
422
******************************
523
0.22.0 Migration guide
624
******************************

starknet_py/hash/hash_method.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from enum import Enum
2+
from typing import List
3+
4+
from poseidon_py.poseidon_hash import poseidon_hash_many
5+
6+
from starknet_py.hash.utils import compute_hash_on_elements
7+
8+
9+
class HashMethod(Enum):
10+
"""
11+
Enum representing hash method.
12+
"""
13+
14+
PEDERSEN = "pedersen"
15+
POSEIDON = "poseidon"
16+
17+
def hash(self, values: List[int]):
18+
if self == HashMethod.PEDERSEN:
19+
return compute_hash_on_elements(values)
20+
if self == HashMethod.POSEIDON:
21+
return poseidon_hash_many(values)
22+
raise ValueError(f"Unsupported hash method: {self}.")

starknet_py/net/account/base_account.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ def sign_message(self, typed_data: TypedDataDict) -> List[int]:
318318
"""
319319
Sign an TypedData TypedDict for off-chain usage with the Starknet private key and return the signature.
320320
This adds a message prefix, so it can't be interchanged with transactions.
321+
Both v0 and v1 domain revision versions are supported.
321322
322323
:param typed_data: TypedData TypedDict to be signed.
323324
:return: The signature of the TypedData TypedDict.
@@ -327,6 +328,7 @@ def sign_message(self, typed_data: TypedDataDict) -> List[int]:
327328
def verify_message(self, typed_data: TypedDataDict, signature: List[int]) -> bool:
328329
"""
329330
Verify a signature of a TypedData dict on Starknet.
331+
Both v0 and v1 domain revision versions are supported.
330332
331333
:param typed_data: TypedData TypedDict to be verified.
332334
:param signature: signature of the TypedData TypedDict.

starknet_py/net/models/typed_data.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
TypedDict structures for TypedData
33
"""
44

5-
from typing import Any, Dict, List, TypedDict, Union
5+
from typing import Any, Dict, List, Optional, TypedDict
6+
7+
from starknet_py.net.schemas.common import Revision
68

79

810
class ParameterDict(TypedDict):
@@ -14,14 +16,15 @@ class ParameterDict(TypedDict):
1416
type: str
1517

1618

17-
class StarkNetDomainDict(TypedDict):
19+
class DomainDict(TypedDict):
1820
"""
19-
TypedDict representing a StarkNetDomain object
21+
TypedDict representing a domain object (both StarkNetDomain, StarknetDomain).
2022
"""
2123

2224
name: str
2325
version: str
24-
chainId: Union[str, int]
26+
chainId: str
27+
revision: Optional[Revision]
2528

2629

2730
class TypedDataDict(TypedDict):
@@ -31,5 +34,5 @@ class TypedDataDict(TypedDict):
3134

3235
types: Dict[str, List[ParameterDict]]
3336
primaryType: str
34-
domain: StarkNetDomainDict
37+
domain: DomainDict
3538
message: Dict[str, Any]

starknet_py/net/schemas/common.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import re
22
import sys
3+
from enum import Enum
34
from typing import Any, Mapping, Optional, Union
45

56
from marshmallow import Schema, ValidationError, fields, post_load
@@ -336,3 +337,32 @@ class StorageEntrySchema(Schema):
336337
def make_dataclass(self, data, **kwargs):
337338
# pylint: disable=no-self-use
338339
return StorageEntry(**data)
340+
341+
342+
class Revision(Enum):
343+
"""
344+
Enum representing the revision of the specification to be used.
345+
"""
346+
347+
V0 = 0
348+
V1 = 1
349+
350+
351+
class RevisionField(fields.Field):
352+
def _serialize(self, value: Any, attr: str, obj: Any, **kwargs):
353+
if value is None or value == Revision.V0:
354+
return str(Revision.V0.value)
355+
return value.value
356+
357+
def _deserialize(self, value, attr, data, **kwargs) -> Revision:
358+
if isinstance(value, str):
359+
value = int(value)
360+
revisions = [revision.value for revision in Revision]
361+
362+
if value not in revisions:
363+
allowed_revisions_str = "".join(list(map(str, revisions)))
364+
raise ValidationError(
365+
f"Invalid value provided for Revision: {value}. Allowed values are {allowed_revisions_str}."
366+
)
367+
368+
return Revision(value)

starknet_py/tests/e2e/docs/code_examples/test_account.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def test_sign_message(account):
7878
],
7979
},
8080
primaryType="Example",
81-
domain={"name": "StarkNet Example", "version": "1", "chainId": 1},
81+
domain={"name": "StarkNet Example", "version": "1", "chainId": "1"},
8282
message={"value": 1},
8383
)
8484
)
@@ -100,7 +100,7 @@ def test_verify_message(account):
100100
],
101101
},
102102
primaryType="Example",
103-
domain={"name": "StarkNet Example", "version": "1", "chainId": 1},
103+
domain={"name": "StarkNet Example", "version": "1", "chainId": "1"},
104104
message={"value": 1},
105105
),
106106
signature=[12, 34],

starknet_py/tests/e2e/docs/guide/test_sign_offchain_message.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ async def test_sign_offchain_message(account):
3131
],
3232
},
3333
"primaryType": "Mail",
34-
"domain": {"name": "StarkNet Mail", "version": "1", "chainId": 1},
34+
"domain": {"name": "StarkNet Mail", "version": "1", "chainId": "1"},
3535
"message": {
3636
"from": {
3737
"name": "Cow",

starknet_py/tests/e2e/fixtures/misc.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ def pytest_addoption(parser):
3030

3131
@pytest.fixture(
3232
params=[
33-
"typed_data_example.json",
34-
"typed_data_felt_array_example.json",
35-
"typed_data_long_string_example.json",
36-
"typed_data_struct_array_example.json",
33+
"typed_data_rev_0_example.json",
34+
"typed_data_rev_0_felt_array_example.json",
35+
"typed_data_rev_0_long_string_example.json",
36+
"typed_data_rev_0_struct_array_example.json",
37+
"typed_data_rev_1_example.json",
3738
],
3839
)
3940
def typed_data(request) -> TypedDataDict:

starknet_py/tests/e2e/mock/typed_data/typed_data_example.json renamed to starknet_py/tests/e2e/mock/typed_data/typed_data_rev_0_example.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"domain": {
2020
"name": "StarkNet Mail",
2121
"version": "1",
22-
"chainId": 1
22+
"chainId": "1"
2323
},
2424
"message": {
2525
"from": {

starknet_py/tests/e2e/mock/typed_data/typed_data_felt_array_example.json renamed to starknet_py/tests/e2e/mock/typed_data/typed_data_rev_0_felt_array_example.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"domain": {
2121
"name": "StarkNet Mail",
2222
"version": "1",
23-
"chainId": 1
23+
"chainId": "1"
2424
},
2525
"message": {
2626
"from": {

starknet_py/tests/e2e/mock/typed_data/typed_data_long_string_example.json renamed to starknet_py/tests/e2e/mock/typed_data/typed_data_rev_0_long_string_example.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"domain": {
2424
"name": "StarkNet Mail",
2525
"version": "1",
26-
"chainId": 1
26+
"chainId": "1"
2727
},
2828
"message": {
2929
"from": {

starknet_py/tests/e2e/mock/typed_data/typed_data_struct_array_example.json renamed to starknet_py/tests/e2e/mock/typed_data/typed_data_rev_0_struct_array_example.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"domain": {
2525
"name": "StarkNet Mail",
2626
"version": "1",
27-
"chainId": 1
27+
"chainId": "1"
2828
},
2929
"message": {
3030
"from": {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"types": {
3+
"StarknetDomain": [
4+
{ "name": "name", "type": "shortstring" },
5+
{ "name": "version", "type": "shortstring" },
6+
{ "name": "chainId", "type": "shortstring" },
7+
{ "name": "revision", "type": "shortstring" }
8+
],
9+
"Person": [
10+
{ "name": "name", "type": "felt" },
11+
{ "name": "wallet", "type": "felt" }
12+
],
13+
"Mail": [
14+
{ "name": "from", "type": "Person" },
15+
{ "name": "to", "type": "Person" },
16+
{ "name": "contents", "type": "felt" }
17+
]
18+
},
19+
"primaryType": "Mail",
20+
"domain": {
21+
"name": "StarkNet Mail",
22+
"version": "1",
23+
"chainId": "1",
24+
"revision": 1
25+
},
26+
"message": {
27+
"from": {
28+
"name": "Cow",
29+
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
30+
},
31+
"to": {
32+
"name": "Bob",
33+
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
34+
},
35+
"contents": "Hello, Bob!"
36+
}
37+
}

0 commit comments

Comments
 (0)