Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
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: 13 additions & 0 deletions docs/migration_guide.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
Migration guide
===============

***************************
0.29.0-rc.0 Migration guide
***************************

Version 0.29.0-rc.0 of **starknet.py** comes with support for RPC 0.10.0-rc.0.

.. py:currentmodule:: starknet_py.net.client_models

1. :class:`StateDiff` has a new field ``migrated_compiled_classes``.
2. ``storage_keys`` field in :class:`ContractsStorageKeys` is now of type ``str``.
3. ``old_root`` field in :class:`PreConfirmedBlockStateUpdate` is now optional.
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.

***************************
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
6 changes: 5 additions & 1 deletion starknet_py/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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_rpc_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 +722,11 @@ async def declare_v3(
:return: DeclareResult instance.
"""

rpc_version = await account.client.spec_version()
hash_method = get_casm_hash_method_for_rpc_version(rpc_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
105 changes: 105 additions & 0 deletions starknet_py/hash/blake2s.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""
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)
41 changes: 28 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,53 @@
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_rpc_version(rpc_version: str) -> HashMethod:
# RPC 0.10.0 and later use Blake2s
version = Version.parse(rpc_version)

if version >= Version.parse("0.10.0"):
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 +67,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
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