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

Add MerkleTree and merkletree support in TypedData #1363

Merged
merged 98 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from 94 commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
95fe8a8
Differentiate typed data files by revision
franciszekjob Jun 14, 2024
139f970
Add `HashMethod`
franciszekjob Jun 14, 2024
1317848
Add `Revision` enum; Rename `StarkNetDomain` to `Domain`
franciszekjob Jun 14, 2024
2cf0aaa
Add tests to revision 1
franciszekjob Jun 14, 2024
9d9fda3
Remove prints
franciszekjob Jun 14, 2024
f67cdb7
Add more rev 1 test cases to `test_type_hash`
franciszekjob Jun 14, 2024
5469dd3
Change values param to `List[int]` in `HashMethod.hash()`
franciszekjob Jun 16, 2024
1be0060
Add `DomainSchema`; Add `RevisionField` and `ChainIdField`
franciszekjob Jun 17, 2024
9781a21
Move `HashMethod` to separate file
franciszekjob Jun 17, 2024
a7e7f89
Refactor `Domain.to_dict()`
franciszekjob Jun 17, 2024
57c16ec
Update comment
franciszekjob Jun 17, 2024
c648f84
Add `TypedData._hash_method` field
franciszekjob Jun 17, 2024
74cf615
Remove `TypedData._hash_method` field
franciszekjob Jun 17, 2024
549e143
Update docs
franciszekjob Jun 17, 2024
9c67d18
Update docs
franciszekjob Jun 17, 2024
be1e1ec
Merge branch 'franciszekjob/1353-snip-12' of https://github.com/softw…
franciszekjob Jun 17, 2024
23562a4
Update comment
franciszekjob Jun 17, 2024
1c4cde6
Reduce `Domain._verify_types()`
franciszekjob Jun 18, 2024
e158122
Remove unnecessary variable duplication
franciszekjob Jun 18, 2024
57ed19f
Remove unused functions
franciszekjob Jun 18, 2024
64a7a6a
Add merkle tree type data jsons
franciszekjob Jun 20, 2024
599f6bd
Add `HashMethod.hash_many()`
franciszekjob Jun 20, 2024
8b6e28d
Add `MerkleTree` dataclass; Add merkle tree tests
franciszekjob Jun 20, 2024
d7540eb
Add `Parameter.contains` to handle merkle parameter
franciszekjob Jun 20, 2024
9290b5e
Introduce merkle trees in `TypedData`; Add merkle tree cases in typed…
franciszekjob Jun 20, 2024
3d92324
Convert `TypedData._hash_method` to `@property`
franciszekjob Jun 20, 2024
0c651d4
Change `Domain.revision` type to `Revision`
franciszekjob Jun 20, 2024
961e489
Use equality comparison for `resolved_revision`
franciszekjob Jun 20, 2024
e98ed80
Merge branch 'franciszekjob/1353-snip-12' of https://github.com/softw…
franciszekjob Jun 20, 2024
59a7b2e
Set `revision` in `Domain` typed dict to be `Optional`
franciszekjob Jun 20, 2024
80e9b87
Update import
franciszekjob Jun 20, 2024
9335160
Refactor `Domain.to_dixt()`
franciszekjob Jun 20, 2024
ff8431a
Resolve conflicts with upstream branch
franciszekjob Jun 20, 2024
f189b04
Format
franciszekjob Jun 20, 2024
8d31b56
Fix doc examples for `sign_message` and `verify_message`; Update func…
franciszekjob Jun 20, 2024
493f4fa
Remove unused import
franciszekjob Jun 20, 2024
186df9d
Remove `test_sign_message_rev_v0` and `test_verify_message_rev_v0`
franciszekjob Jun 20, 2024
ed0f402
Change `sign_message()` and `verify_message()` to accept `TypedData` …
franciszekjob Jun 20, 2024
73680cb
Update migration guide
franciszekjob Jun 20, 2024
1131277
Resolve conflicts
franciszekjob Jun 20, 2024
256b334
Update `test_sign_offchain_message()` to use `TypedData` class instance
franciszekjob Jun 20, 2024
6b52ea5
Update previous changes in `sign_message()` and `verify_message()`
franciszekjob Jun 20, 2024
0d3b592
Update `sign_message()` and `verify_message()` description comments
franciszekjob Jun 20, 2024
0c80872
Minor `DomainSchema.make_dataclass()` refactor
franciszekjob Jun 20, 2024
35ddc45
Merge branch 'franciszekjob/1353-snip-12' of https://github.com/softw…
franciszekjob Jun 20, 2024
10d9a1c
Minor refactor of `HashMethod`
franciszekjob Jun 20, 2024
48d90a8
Merge branch 'development' of https://github.com/software-mansion/sta…
franciszekjob Jun 20, 2024
46b8a48
Resolve conflicts with upstream branch
franciszekjob Jun 21, 2024
29e540d
Restore retrieving `revision` using `data.get()`
franciszekjob Jun 21, 2024
d0602af
Resolve conflicts with upstream branch
franciszekjob Jun 21, 2024
5c9c931
Add pylint directives to disable specific warnings
franciszekjob Jun 21, 2024
deef39d
Format
franciszekjob Jun 21, 2024
5ff98ed
Restore `**kwargs` in `make_dataclass()` methods
franciszekjob Jun 21, 2024
c4c9379
Add missing type annotation in `test_invalid_types()`
franciszekjob Jun 21, 2024
ff1d408
Merge branch 'franciszekjob/1353-snip-12' of https://github.com/softw…
franciszekjob Jun 21, 2024
39982e3
Update `RevisionField._deserialize()` return type to `Revision`
franciszekjob Jun 21, 2024
dfec5e9
Add custom error message on missing value in `Revision` enum
franciszekjob Jun 21, 2024
3ae8bd7
Add module in migration guide to enable hyperlinks
franciszekjob Jun 21, 2024
09ad981
Check error message in `test_invalid_types`
franciszekjob Jun 21, 2024
e5240c0
Add `Parameter.to_dict()` and `TypedData.to_dict()`
franciszekjob Jun 21, 2024
27c24a1
Move `Revision` to common schemas
franciszekjob Jun 21, 2024
e6a2f26
Format
franciszekjob Jun 21, 2024
f604dbb
Set `ChainIdField._deserialize()` return type to `str`; Change 'Domai…
franciszekjob Jun 21, 2024
5505baa
Remove unnecessary import
franciszekjob Jun 21, 2024
3e00175
Resolve conflicts with upstream branch
franciszekjob Jun 21, 2024
bd6fc8f
Restore previous import of `keccak256`
franciszekjob Jun 21, 2024
37b650b
Format
franciszekjob Jun 21, 2024
7e1fcd1
Add newline in typed data rev 1 example json
franciszekjob Jun 21, 2024
1383a00
Merge branch 'franciszekjob/1353-snip-12' of https://github.com/softw…
franciszekjob Jun 21, 2024
1477a3d
Add newlines in typed data example jsons
franciszekjob Jun 21, 2024
f355eca
Update starknet_py/utils/merkle_tree.py
franciszekjob Jun 21, 2024
65f3af9
Merge branch 'franciszekjob/1353-2-merkletree' of https://github.com/…
franciszekjob Jun 21, 2024
30f8bc9
Use `str` instead of `int` in `MerkleTree`
franciszekjob Jun 21, 2024
59ed61d
Format
franciszekjob Jun 24, 2024
36a6cb4
Remove unnecessary `ChainIdField`
franciszekjob Jun 24, 2024
c057544
Change chainId from `int` to `str` in all examples
franciszekjob Jun 24, 2024
2da5a66
Refactor `TypedData.to_dict()`
franciszekjob Jun 24, 2024
2b5a44d
Resolve conflicts with upstream branch
franciszekjob Jun 24, 2024
4d6d05f
Format
franciszekjob Jun 24, 2024
04a8d14
Rename `branches` to `levels` in `MerkleTree`
franciszekjob Jun 24, 2024
d429b4e
Rename variables in `MerkleTree.build()`
franciszekjob Jun 24, 2024
65e79fc
Remove `Parameter.to_dict()`; Add `RevisionField._serialize()`
franciszekjob Jun 24, 2024
f64ff2e
Merge branch 'franciszekjob/1353-snip-12' of https://github.com/softw…
franciszekjob Jun 24, 2024
3574b8c
Update starknet_py/net/schemas/common.py
franciszekjob Jun 24, 2024
0412e27
Update starknet_py/net/schemas/common.py
franciszekjob Jun 24, 2024
6d59203
Merge branch 'franciszekjob/1353-snip-12' of https://github.com/softw…
franciszekjob Jun 24, 2024
91ef12c
Remove revision check in `get_hex()`
franciszekjob Jun 24, 2024
98cd0db
Refactor `MerkleTree` implementation; Update merkle tree tests
franciszekjob Jun 25, 2024
eccf2c9
Format
franciszekjob Jun 25, 2024
643d387
Remove unused `domain_object_v1`
franciszekjob Jun 25, 2024
4552e81
Update example domain types in typed data tests
franciszekjob Jun 25, 2024
a59c8a2
Format
franciszekjob Jun 25, 2024
c490faf
Add leaves and root hash to returned merkle tree levels
franciszekjob Jun 25, 2024
faee6d4
Add check for merkle tree levels count
franciszekjob Jun 25, 2024
5c3b4f4
Refactor `TypedData._get_merkle_tree_leaves_type()`
franciszekjob Jun 26, 2024
f1dda04
Update starknet_py/utils/typed_data.py
franciszekjob Jun 26, 2024
68d6217
Update starknet_py/utils/typed_data.py
franciszekjob Jun 26, 2024
963dc34
Fix lint
franciszekjob Jun 27, 2024
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
13 changes: 10 additions & 3 deletions starknet_py/hash/hash_method.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from enum import Enum
from typing import List

from poseidon_py.poseidon_hash import poseidon_hash_many
from poseidon_py.poseidon_hash import poseidon_hash, poseidon_hash_many

from starknet_py.hash.utils import compute_hash_on_elements
from starknet_py.hash.utils import compute_hash_on_elements, pedersen_hash


class HashMethod(Enum):
Expand All @@ -14,7 +14,14 @@ class HashMethod(Enum):
PEDERSEN = "pedersen"
POSEIDON = "poseidon"

def hash(self, values: List[int]):
def hash(self, left: int, right: int):
franciszekjob marked this conversation as resolved.
Show resolved Hide resolved
if self == HashMethod.PEDERSEN:
return pedersen_hash(left, right)
if self == HashMethod.POSEIDON:
return poseidon_hash(left, right)
raise ValueError(f"Unsupported hash method: {self}.")

def hash_many(self, values: List[int]):
if self == HashMethod.PEDERSEN:
return compute_hash_on_elements(values)
if self == HashMethod.POSEIDON:
Expand Down
6 changes: 6 additions & 0 deletions starknet_py/net/models/typed_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class ParameterDict(TypedDict):

name: str
type: str
contains: Optional[str]


class DomainDict(TypedDict):
Expand All @@ -36,3 +37,8 @@ class TypedDataDict(TypedDict):
primaryType: str
domain: DomainDict
message: Dict[str, Any]


class TypeContext(TypedDict):
parent: str
key: str
franciszekjob marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion starknet_py/net/schemas/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,8 @@ def _serialize(self, value: Any, attr: str, obj: Any, **kwargs):
def _deserialize(self, value, attr, data, **kwargs) -> Revision:
if isinstance(value, str):
value = int(value)
revisions = [revision.value for revision in Revision]

revisions = [revision.value for revision in Revision]
if value not in revisions:
allowed_revisions_str = "".join(list(map(str, revisions)))
raise ValidationError(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"primaryType": "Session",
"types": {
"Policy": [
{
"name": "contractAddress",
"type": "felt"
},
{
"name": "selector",
"type": "selector"
}
],
"Session": [
{
"name": "key",
"type": "felt"
},
{
"name": "expires",
"type": "felt"
},
{
"name": "root",
"type": "merkletree",
"contains": "Policy"
}
],
"StarkNetDomain": [
{
"name": "name",
"type": "felt"
},
{
"name": "version",
"type": "felt"
},
{
"name": "chainId",
"type": "felt"
}
]
},
"domain": {
"name": "StarkNet Mail",
"version": "1",
"chainId": "1"
},
"message": {
"key": "0x0000000000000000000000000000000000000000000000000000000000000000",
"expires": "0x0000000000000000000000000000000000000000000000000000000000000000",
"root": [
{
"contractAddress": "0x1",
"selector": "transfer"
},
{
"contractAddress": "0x2",
"selector": "transfer"
},
{
"contractAddress": "0x3",
"selector": "transfer"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"primaryType": "Example",
"types": {
"Example": [
{ "name": "value", "type": "felt" },
{ "name": "root", "type": "merkletree", "contains": "felt" }
],
"StarknetDomain": [
{ "name": "name", "type": "shortstring" },
{ "name": "version", "type": "shortstring" },
{ "name": "chainId", "type": "shortstring" },
{ "name": "revision", "type": "shortstring" }
]
},
"domain": {
"name": "StarkNet Mail",
"version": "1",
"chainId": "1",
"revision": "1"
},
"message": {
"value": "0x2137",
"root": [
"0x1",
"0x2",
"0x3"
]
}
}
45 changes: 45 additions & 0 deletions starknet_py/utils/merkle_tree.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from dataclasses import dataclass, field
from typing import List, Tuple

from starknet_py.hash.hash_method import HashMethod


@dataclass
class MerkleTree:
"""
Dataclass representing a MerkleTree object.
"""

leaves: List[int]
hash_method: HashMethod
root_hash: int = field(init=False)
levels: List[List[int]] = field(init=False)

def __post_init__(self):
self.root_hash, self.levels = self._build()

def _build(self) -> Tuple[int, List[List[int]]]:
if not self.leaves:
raise ValueError("Cannot build Merkle tree from an empty list of leaves.")

if len(self.leaves) == 1:
return self.leaves[0], [self.leaves]

curr_level_nodes = self.leaves[:]
levels: List[List[int]] = []

while len(curr_level_nodes) > 1:
if len(curr_level_nodes) != len(self.leaves):
levels.append(curr_level_nodes[:])

new_nodes = []
for i in range(0, len(curr_level_nodes), 2):
a, b = (
curr_level_nodes[i],
curr_level_nodes[i + 1] if i + 1 < len(curr_level_nodes) else 0,
)
new_nodes.append(self.hash_method.hash(*sorted([a, b])))

curr_level_nodes = new_nodes
levels = [self.leaves] + levels + [curr_level_nodes]
return curr_level_nodes[0], levels
140 changes: 140 additions & 0 deletions starknet_py/utils/merkle_tree_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
from typing import List

import pytest
from poseidon_py.poseidon_hash import poseidon_hash

from starknet_py.hash.hash_method import HashMethod
from starknet_py.hash.utils import pedersen_hash
from starknet_py.utils.merkle_tree import MerkleTree


@pytest.mark.parametrize(
ddoktorski marked this conversation as resolved.
Show resolved Hide resolved
"leaves, hash_method, expected_root_hash",
[
(
["0x12", "0xa"],
HashMethod.PEDERSEN,
"0x586699e3ba6f118227e094ad423313a2d51871507dcbc23116f11cdd79d80f2",
),
(
["0x12", "0xa"],
HashMethod.POSEIDON,
"0x6257f1f60f7c9fd49e2718c8ad19cd8dce6b1ba4b553b2123113f22b1e9c379",
),
(
[
"0x5bb9440e27889a364bcb678b1f679ecd1347acdedcbf36e83494f857cc58026",
"0x3",
],
HashMethod.PEDERSEN,
"0x551b4adb6c35d49c686a00b9192da9332b18c9b262507cad0ece37f3b6918d2",
),
(
[
"0x5bb9440e27889a364bcb678b1f679ecd1347acdedcbf36e83494f857cc58026",
"0x3",
],
HashMethod.POSEIDON,
"0xc118a3963c12777b0717d1dc89baa8b3ceed84dfd713a6bd1354676f03f021",
),
],
)
def test_calculate_hash(
leaves: List[str], hash_method: HashMethod, expected_root_hash: str
):
if hash_method == HashMethod.PEDERSEN:
apply_hash = pedersen_hash
elif hash_method == HashMethod.POSEIDON:
apply_hash = poseidon_hash
else:
raise ValueError(f"Unsupported hash method: {hash_method}.")

a, b = int(leaves[0], 16), int(leaves[1], 16)
merkle_hash = hash_method.hash(*sorted([b, a]))
raw_hash = apply_hash(*sorted([b, a]))

assert raw_hash == merkle_hash
assert int(expected_root_hash, 16) == merkle_hash


@pytest.mark.parametrize(
"hash_method",
[
HashMethod.PEDERSEN,
HashMethod.POSEIDON,
],
)
def test_build_from_0_elements(hash_method: HashMethod):
with pytest.raises(
ValueError, match="Cannot build Merkle tree from an empty list of leaves."
):
MerkleTree([], hash_method)


@pytest.mark.parametrize(
"leaves, hash_method, expected_root_hash, expected_levels_count",
[
(["0x1"], HashMethod.PEDERSEN, "0x1", 1),
(["0x1"], HashMethod.POSEIDON, "0x1", 1),
(
["0x1", "0x2"],
HashMethod.PEDERSEN,
"0x5bb9440e27889a364bcb678b1f679ecd1347acdedcbf36e83494f857cc58026",
2,
),
(
["0x1", "0x2"],
HashMethod.POSEIDON,
"0x5d44a3decb2b2e0cc71071f7b802f45dd792d064f0fc7316c46514f70f9891a",
2,
),
(
["0x1", "0x2", "0x3", "0x4"],
HashMethod.PEDERSEN,
"0x38118a340bbba28e678413cd3b07a9436a5e60fd6a7cbda7db958a6d501e274",
3,
),
(
["0x1", "0x2", "0x3", "0x4"],
HashMethod.POSEIDON,
"0xa4d02f1e82fc554b062b754d3a4995e0ed8fc7e5016a7ca2894a451a4bae64",
3,
),
(
["0x1", "0x2", "0x3", "0x4", "0x5", "0x6"],
HashMethod.PEDERSEN,
"0x329d5b51e352537e8424bfd85b34d0f30b77d213e9b09e2976e6f6374ecb59",
4,
),
(
["0x1", "0x2", "0x3", "0x4", "0x5", "0x6"],
HashMethod.POSEIDON,
"0x34d525f018d8d6b3e492b1c9cda9bbdc3bc7834b408a30a417186c698c34766",
4,
),
(
["0x1", "0x2", "0x3", "0x4", "0x5", "0x6", "0x7"],
HashMethod.PEDERSEN,
"0x7f748c75e5bdb7ae28013f076b8ab650c4e01d3530c6e5ab665f9f1accbe7d4",
4,
),
(
["0x1", "0x2", "0x3", "0x4", "0x5", "0x6", "0x7"],
HashMethod.POSEIDON,
"0x3308a3c50c25883753f82b21f14c644ec375b88ea5b0f83d1e6afe74d0ed790",
4,
),
],
)
def test_build_from_elements(
leaves: List[str],
hash_method: HashMethod,
expected_root_hash: str,
expected_levels_count: int,
):
tree = MerkleTree([int(leaf, 16) for leaf in leaves], hash_method)

assert tree.root_hash is not None
assert tree.levels is not None
assert tree.root_hash == int(expected_root_hash, 16)
assert len(tree.levels) == expected_levels_count
Loading
Loading