diff --git a/docs/migration_guide.rst b/docs/migration_guide.rst index 93f497c35..e1ff25d7b 100644 --- a/docs/migration_guide.rst +++ b/docs/migration_guide.rst @@ -12,6 +12,7 @@ Version 0.29.0-rc.0 of **starknet.py** comes with support for RPC 0.10.0-rc.0. 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 diff --git a/poetry.lock b/poetry.lock index 40c235726..57544ad4c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2777,6 +2777,18 @@ files = [ {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, ] +[[package]] +name = "semver" +version = "3.0.4" +description = "Python helper for Semantic Versioning (https://semver.org)" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746"}, + {file = "semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602"}, +] + [[package]] name = "setuptools" version = "70.3.0" @@ -3467,4 +3479,4 @@ ledger = ["ledgerwallet"] [metadata] lock-version = "2.1" python-versions = ">=3.9, <3.13" -content-hash = "9576f7e71e2313c1efd9c0eb8d48583c3c8f4003e39c650d2c36bcf47621bb55" +content-hash = "db9850601a790f8c95f75d85fc3dac235303c99dab41a36ff8d0bfcda1180156" diff --git a/pyproject.toml b/pyproject.toml index 22eee7cb0..5cca2eb71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] diff --git a/starknet_py/contract.py b/starknet_py/contract.py index 39532f606..8135db4a0 100644 --- a/starknet_py/contract.py +++ b/starknet_py/contract.py @@ -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 @@ -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( diff --git a/starknet_py/contract_utils.py b/starknet_py/contract_utils.py index 161d03991..e3bad7b37 100644 --- a/starknet_py/contract_utils.py +++ b/starknet_py/contract_utils.py @@ -2,6 +2,7 @@ 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 @@ -9,6 +10,7 @@ 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( @@ -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 diff --git a/starknet_py/hash/blake2s.py b/starknet_py/hash/blake2s.py new file mode 100644 index 000000000..5171f42ae --- /dev/null +++ b/starknet_py/hash/blake2s.py @@ -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) diff --git a/starknet_py/hash/casm_class_hash.py b/starknet_py/hash/casm_class_hash.py index f9804c9a1..dfb2f6f3e 100644 --- a/starknet_py/hash/casm_class_hash.py +++ b/starknet_py/hash/casm_class_hash.py @@ -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 ( @@ -10,13 +10,26 @@ 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. """ @@ -24,14 +37,14 @@ def compute_casm_class_hash(casm_contract_class: CasmClass) -> int: _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: @@ -39,11 +52,11 @@ def compute_casm_class_hash(casm_contract_class: CasmClass) -> int: 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, @@ -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] diff --git a/starknet_py/hash/compiled_class_hash_objects.py b/starknet_py/hash/compiled_class_hash_objects.py index 9472744b4..ae2a5ced1 100644 --- a/starknet_py/hash/compiled_class_hash_objects.py +++ b/starknet_py/hash/compiled_class_hash_objects.py @@ -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): @@ -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): @@ -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) @@ -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 diff --git a/starknet_py/hash/hash_method.py b/starknet_py/hash/hash_method.py index dddced0f6..61d747c8c 100644 --- a/starknet_py/hash/hash_method.py +++ b/starknet_py/hash/hash_method.py @@ -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 @@ -13,12 +14,15 @@ 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]): @@ -26,4 +30,6 @@ def hash_many(self, values: List[int]): 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}.") diff --git a/starknet_py/net/client.py b/starknet_py/net/client.py index 446cc0852..c9a076c3a 100644 --- a/starknet_py/net/client.py +++ b/starknet_py/net/client.py @@ -416,3 +416,11 @@ async def get_compiled_casm(self, class_hash: int) -> CasmClass: :param class_hash: Hash of the contract class whose CASM will be returned :return: CasmClass object """ + + @abstractmethod + async def spec_version(self) -> str: + """ + Returns the version of the Starknet JSON-RPC specification being used. + + :return: String with version of the Starknet JSON-RPC specification. + """ diff --git a/starknet_py/net/full_node_client.py b/starknet_py/net/full_node_client.py index 77a70bfa3..641d14c20 100644 --- a/starknet_py/net/full_node_client.py +++ b/starknet_py/net/full_node_client.py @@ -113,6 +113,7 @@ def __init__( """ self.url = node_url self._client = RpcHttpClient(url=node_url, session=session) + self._spec_version: Optional[str] = None async def get_block( self, @@ -772,11 +773,15 @@ async def spec_version(self) -> str: :return: String with version of the Starknet JSON-RPC specification. """ - res = await self._client.call( - method_name="specVersion", - params={}, - ) - return res + if self._spec_version is None: + self._spec_version = cast( + str, + await self._client.call( + method_name="specVersion", + params={}, + ), + ) + return self._spec_version async def get_transaction_status(self, tx_hash: Hash) -> TransactionStatusResponse: res = await self._client.call( diff --git a/starknet_py/tests/unit/hash/blake2s_test.py b/starknet_py/tests/unit/hash/blake2s_test.py new file mode 100644 index 000000000..c597293fc --- /dev/null +++ b/starknet_py/tests/unit/hash/blake2s_test.py @@ -0,0 +1,52 @@ +""" +The test values are taken from sequencer repository: +https://github.com/starkware-libs/sequencer/blob/b29c0e8c61f7b2340209e256cf87dfe9f2c811aa/crates/blake2s/tests/blake2s_tests.rs +""" + +import pytest + +from starknet_py.hash.blake2s import encode_felt252_data_and_calc_blake_hash + + +@pytest.mark.parametrize( + "input_felts, expected_result", + [ + # Empty array + ( + [], + 874258848688468311465623299960361657518391155660316941922502367727700287818, + ), + # Boundary: small felt at (2^63 - 1) + ( + [(1 << 63) - 1], + 94160078030592802631039216199460125121854007413180444742120780261703604445, + ), + # Boundary: at 2^63 + ( + [1 << 63], + 318549634615606806810268830802792194529205864650702991817600345489579978482, + ), + # Very large felt + ( + [0x800000000000011000000000000000000000000000000000000000000000000], + 3505594194634492896230805823524239179921427575619914728883524629460058657521, + ), + # Mixed: small and large felts + ( + [42, 1 << 63, 1337], + 1127477916086913892828040583976438888091205536601278656613505514972451246501, + ), + ], + ids=[ + "empty", + "boundary_small_felt", + "boundary_at_2_63", + "very_large_felt", + "mixed_small_large", + ], +) +def test_encode_felt252_data_and_calc_blake_hash(input_felts, expected_result): + result = encode_felt252_data_and_calc_blake_hash(input_felts) + assert ( + result == expected_result + ), f"StarknetPy implementation: {result} != Cairo implementation: {expected_result}" diff --git a/starknet_py/tests/unit/hash/casm_class_hash_test.py b/starknet_py/tests/unit/hash/casm_class_hash_test.py index d1a039c67..24347f1ec 100644 --- a/starknet_py/tests/unit/hash/casm_class_hash_test.py +++ b/starknet_py/tests/unit/hash/casm_class_hash_test.py @@ -2,7 +2,11 @@ import pytest from starknet_py.common import create_casm_class -from starknet_py.hash.casm_class_hash import compute_casm_class_hash +from starknet_py.hash.casm_class_hash import ( + compute_casm_class_hash, + get_casm_hash_method_for_rpc_version, +) +from starknet_py.hash.hash_method import HashMethod from starknet_py.tests.e2e.fixtures.constants import PRECOMPILED_CONTRACTS_DIR from starknet_py.tests.e2e.fixtures.misc import ( ContractVersion, @@ -12,7 +16,7 @@ @pytest.mark.parametrize( - "contract, expected_casm_class_hash", + "contract, expected_casm_class_hash_poseidon", [ ("Account", 0x778bce178afd1b39abd9729b80931e8c71661103b16de928c3187057254f601), ("ERC20", 0x3748ca8b6c53d65b5862e6f17850033baa117075e887708474aba110cc0e77a), @@ -21,17 +25,17 @@ ("TokenBridge", 0xf364f0d735b07f5a9a50a886e1f5bf6f0d82175d1955dc737f998d33990f8e), ], ) -def test_compute_casm_class_hash(contract, expected_casm_class_hash): +def test_compute_casm_class_hash_with_poseidon(contract, expected_casm_class_hash_poseidon): casm_contract_class_str = load_contract( contract, version=ContractVersion.V2 )['casm'] casm_class = create_casm_class(casm_contract_class_str) casm_class_hash = compute_casm_class_hash(casm_class) - assert casm_class_hash == expected_casm_class_hash + assert casm_class_hash == expected_casm_class_hash_poseidon @pytest.mark.parametrize( - "casm_contract_class_source, expected_casm_class_hash", + "casm_contract_class_source, expected_casm_class_hash_poseidon", [ ("minimal_contract_compiled_v2_1.casm", 0x186f6c4ca3af40dbcbf3f08f828ab0ee072938aaaedccc74ef3b9840cbd9fb3), @@ -40,11 +44,68 @@ def test_compute_casm_class_hash(contract, expected_casm_class_hash): ("starknet_contract_v2_6.casm", 0x603dd72504d8b0bc54df4f1102fdcf87fc3b2b94750a9083a5876913eec08e4), ], ) -def test_precompiled_compute_casm_class_hash(casm_contract_class_source, expected_casm_class_hash): +def test_precompiled_compute_casm_class_hash_with_poseidon(casm_contract_class_source, expected_casm_class_hash_poseidon): # pylint: disable=line-too-long casm_contract_class_str = read_contract( casm_contract_class_source, directory=PRECOMPILED_CONTRACTS_DIR ) casm_class = create_casm_class(casm_contract_class_str) casm_class_hash = compute_casm_class_hash(casm_class) - assert casm_class_hash == expected_casm_class_hash + assert casm_class_hash == expected_casm_class_hash_poseidon + + +@pytest.mark.parametrize( + "rpc_version, expected_hash_method", + [ + ("0.8.0", HashMethod.POSEIDON), + ("0.9.0", HashMethod.POSEIDON), + ("0.9.1", HashMethod.POSEIDON), + ("0.10.0", HashMethod.BLAKE2S), + ("0.10.1", HashMethod.BLAKE2S), + ("0.11.0", HashMethod.BLAKE2S), + ("1.0.0", HashMethod.BLAKE2S), + ], +) +def test_get_casm_hash_method_for_rpc_version(rpc_version, expected_hash_method): + """Test that the correct hash method is returned for different RPC versions.""" + hash_method = get_casm_hash_method_for_rpc_version(rpc_version) + assert hash_method == expected_hash_method + +@pytest.mark.parametrize( + "contract, expected_casm_class_hash_blake2s", + [ + ("Account", 0x714c833f7b359955f6a4a495ba995cca2114158db2178aff587f643daa19c80), + ("ERC20", 0x44312efaec9c719168eee3586314b01ed7a1fd7e31d3cf0c5a17e0a5b4fbe7d), + ("HelloStarknet", 0x5aaedd0566b5dd234f5f8d3d6b8cfd299cf0a99541aa9ca34db9259d546e82f), + ("TestContract", 0x3135acde04efbc96d422c01822a517ae5b4e61f132d26bf8542e3b9d0d1500f), + ("TokenBridge", 0x6409448fd244060b15748b02b6e0bdb185d5271be231492ca33a7147e43994c), + ], +) + +def test_compute_casm_class_hash_with_blake2s(contract, expected_casm_class_hash_blake2s): + casm_contract_class_str = load_contract( + contract, version=ContractVersion.V2 + )['casm'] + + casm_class = create_casm_class(casm_contract_class_str) + casm_class_hash = compute_casm_class_hash(casm_class, hash_method=HashMethod.BLAKE2S) + assert casm_class_hash == expected_casm_class_hash_blake2s + +@pytest.mark.parametrize( + "casm_contract_class_source, expected_casm_class_hash_blake2s", + [ + ("minimal_contract_compiled_v2_1.casm", + 0x195cfeec43b384e0f0ec83937149a1a4d88571772b2806ed7e4f41a1ecb4c74), + ("minimal_contract_compiled_v2_5_4.casm", + 0x5ac03c50c46fc7b374d4e11d15693ae0d21e13f61c1704700294d1f378980f7), + ("starknet_contract_v2_6.casm", 0xf8c27dd667e50ba127e5e0e469381606ffece27d8c5148548b6bbc4cacf717), + ], +) +def test_precompiled_compute_casm_class_hash_with_blake2s(casm_contract_class_source, expected_casm_class_hash_blake2s): + casm_contract_class_str = read_contract( + casm_contract_class_source, directory=PRECOMPILED_CONTRACTS_DIR + ) + + casm_class = create_casm_class(casm_contract_class_str) + casm_class_hash = compute_casm_class_hash(casm_class, hash_method=HashMethod.BLAKE2S) + assert casm_class_hash == expected_casm_class_hash_blake2s