Skip to content

Commit 3167057

Browse files
Support blake hash (#1657)
* Implement blake hash function * Add possibility to use `blake` with `rpc >= 10` * Change import place * Update comments * Review changes * Remove unneded directive * Save rpc version * Fix warn display test * Nits changes * Change lock file * Resolve typecheck * Update migration guide --------- Co-authored-by: Fiiranek <[email protected]>
1 parent f9fce79 commit 3167057

File tree

13 files changed

+320
-40
lines changed

13 files changed

+320
-40
lines changed

docs/migration_guide.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Version 0.29.0-rc.0 of **starknet.py** comes with support for RPC 0.10.0-rc.0.
1212
1. :class:`StateDiff` has a new field ``migrated_compiled_classes``.
1313
2. ``storage_keys`` field in :class:`ContractsStorageKeys` is now of type ``str``.
1414
3. ``old_root`` field in :class:`PreConfirmedBlockStateUpdate` is now optional.
15+
4. Hash function for contract declaration is now automatically selected based on node's RPC version: Blake2s for RPC >= 0.10.0-rc.0, Poseidon for older versions.
1516

1617
***************************
1718
0.28.0 Migration guide

poetry.lock

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ dependencies = [
2323
"eth-keyfile>=0.8.1,<1.0.0",
2424
"eth-keys==0.7.0",
2525
"websockets>=15.0.1,<16.0.0",
26+
"semver>=3.0.0,<4.0.0",
2627
]
2728

2829
[project.optional-dependencies]

starknet_py/contract.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from starknet_py.common import create_compiled_contract, create_sierra_compiled_contract
2424
from starknet_py.constants import DEFAULT_DEPLOYER_ADDRESS
2525
from starknet_py.contract_utils import _extract_compiled_class_hash, _unpack_provider
26+
from starknet_py.hash.casm_class_hash import get_casm_hash_method_for_rpc_version
2627
from starknet_py.hash.selector import get_selector_from_name
2728
from starknet_py.net.account.base_account import BaseAccount
2829
from starknet_py.net.client import Client
@@ -721,8 +722,11 @@ async def declare_v3(
721722
:return: DeclareResult instance.
722723
"""
723724

725+
rpc_version = await account.client.spec_version()
726+
hash_method = get_casm_hash_method_for_rpc_version(rpc_version)
727+
724728
compiled_class_hash = _extract_compiled_class_hash(
725-
compiled_contract_casm, compiled_class_hash
729+
compiled_contract_casm, compiled_class_hash, hash_method=hash_method
726730
)
727731

728732
declare_tx = await account.sign_declare_v3(

starknet_py/contract_utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
from starknet_py.common import create_casm_class
44
from starknet_py.hash.casm_class_hash import compute_casm_class_hash
5+
from starknet_py.hash.hash_method import HashMethod
56
from starknet_py.net.account.base_account import BaseAccount
67
from starknet_py.net.client import Client
78

89

910
def _extract_compiled_class_hash(
1011
compiled_contract_casm: Optional[str] = None,
1112
compiled_class_hash: Optional[int] = None,
13+
hash_method: HashMethod = HashMethod.BLAKE2S,
1214
) -> int:
1315
if compiled_class_hash is None and compiled_contract_casm is None:
1416
raise ValueError(
@@ -19,7 +21,7 @@ def _extract_compiled_class_hash(
1921
if compiled_class_hash is None:
2022
assert compiled_contract_casm is not None
2123
compiled_class_hash = compute_casm_class_hash(
22-
create_casm_class(compiled_contract_casm)
24+
create_casm_class(compiled_contract_casm), hash_method=hash_method
2325
)
2426

2527
return compiled_class_hash

starknet_py/hash/blake2s.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""
2+
This module's Blake2s felt encoding and hashing logic is based on StarkWare's
3+
sequencer implementation:
4+
https://github.com/starkware-libs/sequencer/blob/b29c0e8c61f7b2340209e256cf87dfe9f2c811aa/crates/blake2s/src/lib.rs
5+
"""
6+
7+
import hashlib
8+
from typing import List
9+
10+
from starknet_py.constants import FIELD_PRIME
11+
12+
SMALL_THRESHOLD = 2**63
13+
BIG_MARKER = 1 << 31 # MSB mask for the first u32 in the 8-limb case
14+
15+
16+
def encode_felts_to_u32s(felts: List[int]) -> List[int]:
17+
"""
18+
Encode each Felt into 32-bit words following Cairo's encoding scheme.
19+
20+
Small values (< 2^63) are encoded as 2 words: [high_32_bits, low_32_bits] from the last 8 bytes.
21+
Large values (>= 2^63) are encoded as 8 words: the full 32-byte big-endian split,
22+
with the MSB of the first word set as a marker (+2^255).
23+
24+
:param felts: List of Felt values to encode
25+
:return: Flat list of u32 values
26+
"""
27+
unpacked_u32s = []
28+
for felt in felts:
29+
# Convert felt to 32-byte big-endian representation
30+
felt_as_be_bytes = felt.to_bytes(32, byteorder="big")
31+
32+
if felt < SMALL_THRESHOLD:
33+
# Small: 2 limbs only, high-32 then low-32 of the last 8 bytes
34+
high = int.from_bytes(felt_as_be_bytes[24:28], byteorder="big")
35+
low = int.from_bytes(felt_as_be_bytes[28:32], byteorder="big")
36+
unpacked_u32s.append(high)
37+
unpacked_u32s.append(low)
38+
else:
39+
# Big: 8 limbs, big-endian order
40+
start = len(unpacked_u32s)
41+
for i in range(0, 32, 4):
42+
limb = int.from_bytes(felt_as_be_bytes[i : i + 4], byteorder="big")
43+
unpacked_u32s.append(limb)
44+
# Set the MSB of the very first limb as the Cairo hint does with "+ 2**255"
45+
unpacked_u32s[start] |= BIG_MARKER
46+
47+
return unpacked_u32s
48+
49+
50+
def pack_256_le_to_felt(hash_bytes: bytes) -> int:
51+
"""
52+
Packs the first 32 bytes (256 bits) of hash_bytes into a Felt (252 bits).
53+
Interprets the bytes as a Felt (252 bits)
54+
55+
:param hash_bytes: Hash bytes (at least 32 bytes required)
56+
:return: Felt value (252-bit field element)
57+
"""
58+
assert len(hash_bytes) >= 32, "need at least 32 bytes to pack"
59+
# Interpret the 32-byte buffer as a little-endian integer and convert to Felt
60+
return int.from_bytes(hash_bytes[:32], byteorder="little") % FIELD_PRIME
61+
62+
63+
def blake2s_to_felt(data: bytes) -> int:
64+
"""
65+
Compute Blake2s-256 hash over data and return as a Felt.
66+
67+
:param data: Input data to hash
68+
:return: Blake2s-256 hash as a 252-bit field element
69+
"""
70+
hash_bytes = hashlib.blake2s(data, digest_size=32).digest()
71+
return pack_256_le_to_felt(hash_bytes)
72+
73+
74+
def encode_felt252_data_and_calc_blake_hash(felts: List[int]) -> int:
75+
"""
76+
Encodes Felt values using Cairo's encoding scheme and computes Blake2s hash.
77+
78+
This function matches Cairo's encode_felt252_to_u32s hint behavior. It encodes
79+
each Felt into 32-bit words, serializes them as little-endian bytes, then
80+
computes Blake2s-256 hash over the byte stream.
81+
82+
:param felts: List of Felt values to encode and hash
83+
:return: Blake2s-256 hash as a 252-bit field element
84+
"""
85+
# Unpack each Felt into 2 or 8 u32 limbs
86+
u32_words = encode_felts_to_u32s(felts)
87+
88+
# Serialize the u32 limbs into a little-endian byte stream
89+
byte_stream = b"".join(word.to_bytes(4, byteorder="little") for word in u32_words)
90+
91+
# Compute Blake2s-256 over the bytes and pack the result into a Felt
92+
return blake2s_to_felt(byte_stream)
93+
94+
95+
def blake2s_hash_many(values: List[int]) -> int:
96+
"""
97+
Hash multiple Felt values using Cairo-compatible Blake2s encoding.
98+
99+
This is the recommended way to hash Felt values for Starknet when using
100+
Blake2s as the hash method.
101+
102+
:param values: List of Felt values to hash
103+
:return: Blake2s-256 hash as a 252-bit field element
104+
"""
105+
return encode_felt252_data_and_calc_blake_hash(values)

starknet_py/hash/casm_class_hash.py

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import List, Optional, Sequence, Tuple
22

3-
from poseidon_py.poseidon_hash import poseidon_hash_many
3+
from semver import Version
44

55
from starknet_py.cairo.felt import encode_shortstring
66
from starknet_py.hash.compiled_class_hash_objects import (
@@ -10,40 +10,53 @@
1010
BytecodeSegmentStructure,
1111
NestedIntList,
1212
)
13+
from starknet_py.hash.hash_method import HashMethod
1314
from starknet_py.net.client_models import CasmClassEntryPoint
1415
from starknet_py.net.executable_models import CasmClass
1516

1617
CASM_CLASS_VERSION = "COMPILED_CLASS_V1"
1718

1819

19-
def compute_casm_class_hash(casm_contract_class: CasmClass) -> int:
20+
def get_casm_hash_method_for_rpc_version(rpc_version: str) -> HashMethod:
21+
# RPC 0.10.0 and later use Blake2s
22+
version = Version.parse(rpc_version)
23+
24+
if version >= Version.parse("0.10.0"):
25+
return HashMethod.BLAKE2S
26+
27+
return HashMethod.POSEIDON
28+
29+
30+
def compute_casm_class_hash(
31+
casm_contract_class: CasmClass, hash_method: HashMethod = HashMethod.POSEIDON
32+
) -> int:
2033
"""
2134
Calculate class hash of a CasmClass.
2235
"""
2336
casm_class_version = encode_shortstring(CASM_CLASS_VERSION)
2437

2538
_entry_points = casm_contract_class.entry_points_by_type
2639

27-
external_entry_points_hash = poseidon_hash_many(
28-
_entry_points_array(_entry_points.external)
40+
external_entry_points_hash = hash_method.hash_many(
41+
_entry_points_array(_entry_points.external, hash_method)
2942
)
30-
l1_handler_entry_points_hash = poseidon_hash_many(
31-
_entry_points_array(_entry_points.l1_handler)
43+
l1_handler_entry_points_hash = hash_method.hash_many(
44+
_entry_points_array(_entry_points.l1_handler, hash_method)
3245
)
33-
constructor_entry_points_hash = poseidon_hash_many(
34-
_entry_points_array(_entry_points.constructor)
46+
constructor_entry_points_hash = hash_method.hash_many(
47+
_entry_points_array(_entry_points.constructor, hash_method)
3548
)
3649

3750
if casm_contract_class.bytecode_segment_lengths is not None:
3851
bytecode_hash = create_bytecode_segment_structure(
3952
bytecode=casm_contract_class.bytecode,
4053
bytecode_segment_lengths=casm_contract_class.bytecode_segment_lengths,
4154
visited_pcs=None,
42-
).hash()
55+
).hash(hash_method)
4356
else:
44-
bytecode_hash = poseidon_hash_many(casm_contract_class.bytecode)
57+
bytecode_hash = hash_method.hash_many(casm_contract_class.bytecode)
4558

46-
return poseidon_hash_many(
59+
return hash_method.hash_many(
4760
[
4861
casm_class_version,
4962
external_entry_points_hash,
@@ -54,12 +67,14 @@ def compute_casm_class_hash(casm_contract_class: CasmClass) -> int:
5467
)
5568

5669

57-
def _entry_points_array(entry_points: List[CasmClassEntryPoint]) -> List[int]:
70+
def _entry_points_array(
71+
entry_points: List[CasmClassEntryPoint], hash_method: HashMethod
72+
) -> List[int]:
5873
entry_points_array = []
5974
for entry_point in entry_points:
6075
assert entry_point.builtins is not None
6176
_encoded_builtins = [encode_shortstring(val) for val in entry_point.builtins]
62-
builtins_hash = poseidon_hash_many(_encoded_builtins)
77+
builtins_hash = hash_method.hash_many(_encoded_builtins)
6378

6479
entry_points_array.extend(
6580
[entry_point.selector, entry_point.offset, builtins_hash]

starknet_py/hash/compiled_class_hash_objects.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
import dataclasses
55
import itertools
66
from abc import ABC, abstractmethod
7-
from typing import Any, List, Union
7+
from typing import TYPE_CHECKING, Any, List, Union
88

9-
from poseidon_py.poseidon_hash import poseidon_hash_many
9+
if TYPE_CHECKING:
10+
from starknet_py.hash.hash_method import HashMethod
1011

1112

1213
class BytecodeSegmentStructure(ABC):
@@ -17,9 +18,11 @@ class BytecodeSegmentStructure(ABC):
1718
"""
1819

1920
@abstractmethod
20-
def hash(self) -> int:
21+
def hash(self, hash_method: "HashMethod") -> int:
2122
"""
2223
Computes the hash of the node.
24+
25+
:param hash_method: Hash method to use.
2326
"""
2427

2528
def bytecode_with_skipped_segments(self):
@@ -46,8 +49,8 @@ class BytecodeLeaf(BytecodeSegmentStructure):
4649

4750
data: List[int]
4851

49-
def hash(self) -> int:
50-
return poseidon_hash_many(self.data)
52+
def hash(self, hash_method: "HashMethod") -> int:
53+
return hash_method.hash_many(self.data)
5154

5255
def add_bytecode_with_skipped_segments(self, data: List[int]):
5356
data.extend(self.data)
@@ -62,14 +65,19 @@ class BytecodeSegmentedNode(BytecodeSegmentStructure):
6265

6366
segments: List["BytecodeSegment"]
6467

65-
def hash(self) -> int:
68+
def hash(self, hash_method: "HashMethod") -> int:
6669
return (
67-
poseidon_hash_many(
68-
itertools.chain( # pyright: ignore
69-
*[
70-
(node.segment_length, node.inner_structure.hash())
71-
for node in self.segments
72-
]
70+
hash_method.hash_many(
71+
list(
72+
itertools.chain(
73+
*[
74+
(
75+
node.segment_length,
76+
node.inner_structure.hash(hash_method),
77+
)
78+
for node in self.segments
79+
]
80+
)
7381
)
7482
)
7583
+ 1

starknet_py/hash/hash_method.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from poseidon_py.poseidon_hash import poseidon_hash, poseidon_hash_many
55

6+
from starknet_py.hash.blake2s import blake2s_hash_many
67
from starknet_py.hash.utils import compute_hash_on_elements, pedersen_hash
78

89

@@ -13,17 +14,22 @@ class HashMethod(Enum):
1314

1415
PEDERSEN = "pedersen"
1516
POSEIDON = "poseidon"
17+
BLAKE2S = "blake2s"
1618

1719
def hash(self, left: int, right: int):
1820
if self == HashMethod.PEDERSEN:
1921
return pedersen_hash(left, right)
2022
if self == HashMethod.POSEIDON:
2123
return poseidon_hash(left, right)
24+
if self == HashMethod.BLAKE2S:
25+
return blake2s_hash_many([left, right])
2226
raise ValueError(f"Unsupported hash method: {self}.")
2327

2428
def hash_many(self, values: List[int]):
2529
if self == HashMethod.PEDERSEN:
2630
return compute_hash_on_elements(values)
2731
if self == HashMethod.POSEIDON:
2832
return poseidon_hash_many(values)
33+
if self == HashMethod.BLAKE2S:
34+
return blake2s_hash_many(values)
2935
raise ValueError(f"Unsupported hash method: {self}.")

starknet_py/net/client.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,3 +416,11 @@ async def get_compiled_casm(self, class_hash: int) -> CasmClass:
416416
:param class_hash: Hash of the contract class whose CASM will be returned
417417
:return: CasmClass object
418418
"""
419+
420+
@abstractmethod
421+
async def spec_version(self) -> str:
422+
"""
423+
Returns the version of the Starknet JSON-RPC specification being used.
424+
425+
:return: String with version of the Starknet JSON-RPC specification.
426+
"""

0 commit comments

Comments
 (0)