Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5d72c05
Bump pyright
cptartur Jul 3, 2025
f636cdd
RPC 0.9.0-rc.1 changes
cptartur Jul 3, 2025
55277fe
Add missing methods to `Client` ABC
cptartur Jul 3, 2025
901a3e3
Fix `get_messages_status` argument
cptartur Jul 3, 2025
0923db5
Fix `subscription_id` type in schemas
cptartur Jul 3, 2025
3ec22b7
Add migration guide
cptartur Jul 3, 2025
5298ac7
Bump devnet to 0.5.0-rc.0
cptartur Jul 4, 2025
115e4be
Update `MessageStatus` following the spec
cptartur Jul 4, 2025
9635837
Improve docs of `wait_for_tx`, check for error code explicitly
cptartur Jul 4, 2025
dd2249f
Fix tests
cptartur Jul 4, 2025
29c51d7
Fix docs
cptartur Jul 4, 2025
4117ce6
Lint and format
cptartur Jul 4, 2025
bfa0c25
Fix `test_get_compiled_casm`
cptartur Jul 7, 2025
6d33811
Fix `test_get_storage_proof`
cptartur Jul 7, 2025
6296505
Update starknet_py/net/client_models.py
cptartur Jul 7, 2025
1be4180
Update starknet_py/tests/e2e/client/client_test.py
cptartur Jul 7, 2025
442b99c
Update starknet_py/net/client.py
cptartur Jul 7, 2025
ec246fe
Update docs/migration_guide.rst
cptartur Jul 7, 2025
08f145f
Update docs/migration_guide.rst
cptartur Jul 7, 2025
b9b496d
Fix comments
cptartur Jul 7, 2025
c3d9abe
Mention supported rpc version in migration_guide.rst
cptartur Jul 7, 2025
c7a70f9
Replace `_to_hex_number` with `_to_rpc_felt`
cptartur Jul 7, 2025
6c3fc7c
Add docstrings
cptartur Jul 7, 2025
cd610fc
Update devnet to 0.5.0-rc.1
cptartur Jul 7, 2025
1f2d21e
Add support for tip
cptartur Jul 8, 2025
cd8a70b
Add tests
cptartur Jul 8, 2025
15d06ba
Update migration_guide.rst
cptartur Jul 8, 2025
014f19e
Merge tag 'v0.28.0' of https://github.com/software-mansion/starknet.p…
franciszekjob Nov 20, 2025
af44b05
Merge tag 'v0.28.0' of https://github.com/software-mansion/starknet.p…
franciszekjob Nov 20, 2025
0059ce3
Remove changes
franciszekjob Nov 20, 2025
68aafd3
Support blake hash
franciszekjob Nov 20, 2025
5922b9a
Update migration guide
franciszekjob Nov 20, 2025
b1eb73f
Add missing files
franciszekjob Nov 20, 2025
772ea1d
Little fixes
franciszekjob Nov 20, 2025
765949a
update migration guide
franciszekjob Nov 20, 2025
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
15 changes: 15 additions & 0 deletions docs/migration_guide.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
Migration guide
===============

***************************
0.28.1 Migration guide
***************************

1. This version adds support for Blake hash, in order to allow compatibility for Starknet versions >= 0.14.1.

0.28.1 Breaking changes
-----------------------

.. py:currentmodule:: starknet_py.hash.compiled_class_hash_objects

1. :meth:`BytecodeSegmentStructure.hash` has new param ``hash_method``.
2. :meth:`BytecodeLeaf.hash` has new param ``hash_method``.
3. :meth:`BytecodeSegmentedNode.hash` has new param ``hash_method``.

***************************
0.28.0 Migration guide
***************************
Expand Down
14 changes: 13 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ dependencies = [
"eth-keyfile>=0.8.1,<1.0.0",
"eth-keys==0.7.0",
"websockets>=15.0.1,<16.0.0",
"semver>=3.0.0,<4.0.0",
]

[project.optional-dependencies]
Expand Down
8 changes: 7 additions & 1 deletion starknet_py/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import Dict, List, Optional, Tuple, TypeVar, Union

from marshmallow import ValidationError
from semver import Version

from starknet_py.abi.v0 import Abi as AbiV0
from starknet_py.abi.v0 import AbiParser as AbiParserV0
Expand All @@ -23,6 +24,7 @@
from starknet_py.common import create_compiled_contract, create_sierra_compiled_contract
from starknet_py.constants import DEFAULT_DEPLOYER_ADDRESS
from starknet_py.contract_utils import _extract_compiled_class_hash, _unpack_provider
from starknet_py.hash.casm_class_hash import get_casm_hash_method_for_starknet_version
from starknet_py.hash.selector import get_selector_from_name
from starknet_py.net.account.base_account import BaseAccount
from starknet_py.net.client import Client
Expand Down Expand Up @@ -721,8 +723,12 @@ async def declare_v3(
:return: DeclareResult instance.
"""

block = await account.client.get_block()
starknet_version = Version.parse(block.starknet_version)
hash_method = get_casm_hash_method_for_starknet_version(starknet_version)

compiled_class_hash = _extract_compiled_class_hash(
compiled_contract_casm, compiled_class_hash
compiled_contract_casm, compiled_class_hash, hash_method=hash_method
)

declare_tx = await account.sign_declare_v3(
Expand Down
4 changes: 3 additions & 1 deletion starknet_py/contract_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

from starknet_py.common import create_casm_class
from starknet_py.hash.casm_class_hash import compute_casm_class_hash
from starknet_py.hash.hash_method import HashMethod
from starknet_py.net.account.base_account import BaseAccount
from starknet_py.net.client import Client


def _extract_compiled_class_hash(
compiled_contract_casm: Optional[str] = None,
compiled_class_hash: Optional[int] = None,
hash_method: HashMethod = HashMethod.BLAKE2S,
) -> int:
if compiled_class_hash is None and compiled_contract_casm is None:
raise ValueError(
Expand All @@ -19,7 +21,7 @@ def _extract_compiled_class_hash(
if compiled_class_hash is None:
assert compiled_contract_casm is not None
compiled_class_hash = compute_casm_class_hash(
create_casm_class(compiled_contract_casm)
create_casm_class(compiled_contract_casm), hash_method=hash_method
)

return compiled_class_hash
Expand Down
97 changes: 97 additions & 0 deletions starknet_py/hash/blake2s.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""
This module's Blake2s felt encoding and hashing logic is based on StarkWare's
sequencer implementation:
https://github.com/starkware-libs/sequencer/blob/b29c0e8c61f7b2340209e256cf87dfe9f2c811aa/crates/blake2s/src/lib.rs
"""

import hashlib
from typing import List

from starknet_py.constants import FIELD_PRIME

SMALL_THRESHOLD = 2**63
BIG_MARKER = 1 << 31 # MSB mask for the first u32 in the 8-limb case


def encode_felts_to_u32s(felts: List[int]) -> List[int]:
"""
Encode each Felt into 32-bit words following Cairo's encoding scheme.
Small values (< 2^63) are encoded as 2 words: [high_32_bits, low_32_bits] from the last 8 bytes.
Large values (>= 2^63) are encoded as 8 words: the full 32-byte big-endian split,
with the MSB of the first word set as a marker (+2^255).
:param felts: List of Felt values to encode
:return: Flat list of u32 values
"""
unpacked_u32s = []
for felt in felts:
# Convert felt to 32-byte big-endian representation
felt_as_be_bytes = felt.to_bytes(32, byteorder="big")

if felt < SMALL_THRESHOLD:
# Small: 2 limbs only, high-32 then low-32 of the last 8 bytes
high = int.from_bytes(felt_as_be_bytes[24:28], byteorder="big")
low = int.from_bytes(felt_as_be_bytes[28:32], byteorder="big")
unpacked_u32s.append(high)
unpacked_u32s.append(low)
else:
# Big: 8 limbs, big-endian order
start = len(unpacked_u32s)
for i in range(0, 32, 4):
limb = int.from_bytes(felt_as_be_bytes[i : i + 4], byteorder="big")
unpacked_u32s.append(limb)
# Set the MSB of the very first limb as the Cairo hint does with "+ 2**255"
unpacked_u32s[start] |= BIG_MARKER

return unpacked_u32s


def pack_256_le_to_felt(hash_bytes: bytes) -> int:
"""
Packs the first 32 bytes (256 bits) of hash_bytes into a Felt (252 bits).
Interprets the bytes as a Felt (252 bits)
:param hash_bytes: Hash bytes (at least 32 bytes required)
:return: Felt value (252-bit field element)
"""
assert len(hash_bytes) >= 32, "need at least 32 bytes to pack"
# Interpret the 32-byte buffer as a little-endian integer and convert to Felt
return int.from_bytes(hash_bytes[:32], byteorder="little") % FIELD_PRIME


def blake2s_to_felt(data: bytes) -> int:
"""
Compute Blake2s-256 hash over data and return as a Felt.
:param data: Input data to hash
:return: Blake2s-256 hash as a 252-bit field element
"""
hash_bytes = hashlib.blake2s(data, digest_size=32).digest()
return pack_256_le_to_felt(hash_bytes)


def encode_felt252_data_and_calc_blake_hash(felts: List[int]) -> int:
"""
Encodes Felt values using Cairo's encoding scheme and computes Blake2s hash.
This function matches Cairo's encode_felt252_to_u32s hint behavior. It encodes
each Felt into 32-bit words, serializes them as little-endian bytes, then
computes Blake2s-256 hash over the byte stream.
:param felts: List of Felt values to encode and hash
:return: Blake2s-256 hash as a 252-bit field element
"""
# Unpack each Felt into 2 or 8 u32 limbs
u32_words = encode_felts_to_u32s(felts)

# Serialize the u32 limbs into a little-endian byte stream
byte_stream = b"".join(word.to_bytes(4, byteorder="little") for word in u32_words)

# Compute Blake2s-256 over the bytes and pack the result into a Felt
return blake2s_to_felt(byte_stream)


def blake2s_hash_many(values: List[int]) -> int:
"""
Hash multiple Felt values using Cairo-compatible Blake2s encoding.
This is the recommended way to hash Felt values for Starknet when using
Blake2s as the hash method.
:param values: List of Felt values to hash
:return: Blake2s-256 hash as a 252-bit field element
"""
return encode_felt252_data_and_calc_blake_hash(values)
39 changes: 26 additions & 13 deletions starknet_py/hash/casm_class_hash.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import List, Optional, Sequence, Tuple

from poseidon_py.poseidon_hash import poseidon_hash_many
from semver import Version

from starknet_py.cairo.felt import encode_shortstring
from starknet_py.hash.compiled_class_hash_objects import (
Expand All @@ -10,40 +10,51 @@
BytecodeSegmentStructure,
NestedIntList,
)
from starknet_py.hash.hash_method import HashMethod
from starknet_py.net.client_models import CasmClassEntryPoint
from starknet_py.net.executable_models import CasmClass

CASM_CLASS_VERSION = "COMPILED_CLASS_V1"


def compute_casm_class_hash(casm_contract_class: CasmClass) -> int:
def get_casm_hash_method_for_starknet_version(starknet_version: Version) -> HashMethod:
# Starknet 0.14.1 and later use Blake2s
if starknet_version >= Version.parse("0.14.1"):
return HashMethod.BLAKE2S

return HashMethod.POSEIDON


def compute_casm_class_hash(
casm_contract_class: CasmClass, hash_method: HashMethod = HashMethod.POSEIDON
) -> int:
"""
Calculate class hash of a CasmClass.
"""
casm_class_version = encode_shortstring(CASM_CLASS_VERSION)

_entry_points = casm_contract_class.entry_points_by_type

external_entry_points_hash = poseidon_hash_many(
_entry_points_array(_entry_points.external)
external_entry_points_hash = hash_method.hash_many(
_entry_points_array(_entry_points.external, hash_method)
)
l1_handler_entry_points_hash = poseidon_hash_many(
_entry_points_array(_entry_points.l1_handler)
l1_handler_entry_points_hash = hash_method.hash_many(
_entry_points_array(_entry_points.l1_handler, hash_method)
)
constructor_entry_points_hash = poseidon_hash_many(
_entry_points_array(_entry_points.constructor)
constructor_entry_points_hash = hash_method.hash_many(
_entry_points_array(_entry_points.constructor, hash_method)
)

if casm_contract_class.bytecode_segment_lengths is not None:
bytecode_hash = create_bytecode_segment_structure(
bytecode=casm_contract_class.bytecode,
bytecode_segment_lengths=casm_contract_class.bytecode_segment_lengths,
visited_pcs=None,
).hash()
).hash(hash_method)
else:
bytecode_hash = poseidon_hash_many(casm_contract_class.bytecode)
bytecode_hash = hash_method.hash_many(casm_contract_class.bytecode)

return poseidon_hash_many(
return hash_method.hash_many(
[
casm_class_version,
external_entry_points_hash,
Expand All @@ -54,12 +65,14 @@ def compute_casm_class_hash(casm_contract_class: CasmClass) -> int:
)


def _entry_points_array(entry_points: List[CasmClassEntryPoint]) -> List[int]:
def _entry_points_array(
entry_points: List[CasmClassEntryPoint], hash_method: HashMethod
) -> List[int]:
entry_points_array = []
for entry_point in entry_points:
assert entry_point.builtins is not None
_encoded_builtins = [encode_shortstring(val) for val in entry_point.builtins]
builtins_hash = poseidon_hash_many(_encoded_builtins)
builtins_hash = hash_method.hash_many(_encoded_builtins)

entry_points_array.extend(
[entry_point.selector, entry_point.offset, builtins_hash]
Expand Down
32 changes: 20 additions & 12 deletions starknet_py/hash/compiled_class_hash_objects.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@franciszekjob Let's include these changes in changelog for completness sake

Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import dataclasses
import itertools
from abc import ABC, abstractmethod
from typing import Any, List, Union
from typing import TYPE_CHECKING, Any, List, Union

from poseidon_py.poseidon_hash import poseidon_hash_many
if TYPE_CHECKING:
from starknet_py.hash.hash_method import HashMethod


class BytecodeSegmentStructure(ABC):
Expand All @@ -17,9 +18,11 @@ class BytecodeSegmentStructure(ABC):
"""

@abstractmethod
def hash(self) -> int:
def hash(self, hash_method: "HashMethod") -> int:
"""
Computes the hash of the node.

:param hash_method: Hash method to use.
"""

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

data: List[int]

def hash(self) -> int:
return poseidon_hash_many(self.data)
def hash(self, hash_method: "HashMethod") -> int:
return hash_method.hash_many(self.data)

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

segments: List["BytecodeSegment"]

def hash(self) -> int:
def hash(self, hash_method: "HashMethod") -> int:
return (
poseidon_hash_many(
itertools.chain( # pyright: ignore
*[
(node.segment_length, node.inner_structure.hash())
for node in self.segments
]
hash_method.hash_many(
list(
itertools.chain(
*[
(
node.segment_length,
node.inner_structure.hash(hash_method),
)
for node in self.segments
]
)
)
)
+ 1
Expand Down
6 changes: 6 additions & 0 deletions starknet_py/hash/hash_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from poseidon_py.poseidon_hash import poseidon_hash, poseidon_hash_many

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


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

PEDERSEN = "pedersen"
POSEIDON = "poseidon"
BLAKE2S = "blake2s"

def hash(self, left: int, right: int):
if self == HashMethod.PEDERSEN:
return pedersen_hash(left, right)
if self == HashMethod.POSEIDON:
return poseidon_hash(left, right)
if self == HashMethod.BLAKE2S:
return blake2s_hash_many([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:
return poseidon_hash_many(values)
if self == HashMethod.BLAKE2S:
return blake2s_hash_many(values)
raise ValueError(f"Unsupported hash method: {self}.")
Loading