From 0853dfbab9897948c173654fdc1163ebe27bade0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 Aug 2025 09:40:01 +0000 Subject: [PATCH 1/3] Initial plan From d90c3d20e1abc06ab82407114d78f6843a5839bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 Aug 2025 09:48:11 +0000 Subject: [PATCH 2/3] Implement Uint512Serializer with comprehensive tests Co-authored-by: franciszekjob <54181625+franciszekjob@users.noreply.github.com> --- starknet_py/cairo/felt.py | 10 ++ .../data_serializers/__init__.py | 1 + .../data_serializers/uint512_serializer.py | 94 +++++++++++++ .../uint512_serializer_test.py | 133 ++++++++++++++++++ 4 files changed, 238 insertions(+) create mode 100644 starknet_py/serialization/data_serializers/uint512_serializer.py create mode 100644 starknet_py/tests/unit/serialization/data_serializers/uint512_serializer_test.py diff --git a/starknet_py/cairo/felt.py b/starknet_py/cairo/felt.py index 51c7d6087..1cba0d505 100644 --- a/starknet_py/cairo/felt.py +++ b/starknet_py/cairo/felt.py @@ -8,6 +8,9 @@ MAX_UINT256 = (1 << 256) - 1 MIN_UINT256 = 0 +MAX_UINT512 = (1 << 512) - 1 +MIN_UINT512 = 0 + def uint256_range_check(value: int): if not MIN_UINT256 <= value <= MAX_UINT256: @@ -16,6 +19,13 @@ def uint256_range_check(value: int): ) +def uint512_range_check(value: int): + if not MIN_UINT512 <= value <= MAX_UINT512: + raise ValueError( + f"Uint512 is expected to be in range [0;2**512), got: {value}." + ) + + MIN_FELT = -FIELD_PRIME // 2 MAX_FELT = FIELD_PRIME // 2 diff --git a/starknet_py/serialization/data_serializers/__init__.py b/starknet_py/serialization/data_serializers/__init__.py index 3d67c45fe..b97dfc03d 100644 --- a/starknet_py/serialization/data_serializers/__init__.py +++ b/starknet_py/serialization/data_serializers/__init__.py @@ -8,4 +8,5 @@ from .struct_serializer import StructSerializer from .tuple_serializer import TupleSerializer from .uint256_serializer import Uint256Serializer +from .uint512_serializer import Uint512Serializer from .uint_serializer import UintSerializer diff --git a/starknet_py/serialization/data_serializers/uint512_serializer.py b/starknet_py/serialization/data_serializers/uint512_serializer.py new file mode 100644 index 000000000..176928cee --- /dev/null +++ b/starknet_py/serialization/data_serializers/uint512_serializer.py @@ -0,0 +1,94 @@ +from dataclasses import dataclass +from typing import Generator, TypedDict, Union + +from starknet_py.cairo.felt import uint512_range_check +from starknet_py.serialization._context import ( + Context, + DeserializationContext, + SerializationContext, +) +from starknet_py.serialization.data_serializers.cairo_data_serializer import ( + CairoDataSerializer, +) + +U128_UPPER_BOUND = 2**128 + + +class Uint512Dict(TypedDict): + low0: int + low1: int + high0: int + high1: int + + +@dataclass +class Uint512Serializer(CairoDataSerializer[Union[int, Uint512Dict], int]): + """ + Serializer of Uint512. In Cairo it is represented by structure {low0: Uint128, low1: Uint128, high0: Uint128, high1: Uint128}. + Can serialize an int. + Deserializes data to an int. + + Examples: + 0 => [0,0,0,0] + 1 => [1,0,0,0] + 2**128 => [0,1,0,0] + 2**256 => [0,0,1,0] + 2**384 => [0,0,0,1] + 3 + 2**128 => [3,1,0,0] + """ + + def deserialize_with_context(self, context: DeserializationContext) -> int: + [low0, low1, high0, high1] = context.reader.read(4) + + # Checking if resulting value is in [0, 2**512) range is not enough. Uint512 should be made of four uint128. + with context.push_entity("low0"): + self._ensure_valid_uint128(low0, context) + with context.push_entity("low1"): + self._ensure_valid_uint128(low1, context) + with context.push_entity("high0"): + self._ensure_valid_uint128(high0, context) + with context.push_entity("high1"): + self._ensure_valid_uint128(high1, context) + + return (high1 << 384) + (high0 << 256) + (low1 << 128) + low0 + + def serialize_with_context( + self, context: SerializationContext, value: Union[int, Uint512Dict] + ) -> Generator[int, None, None]: + context.ensure_valid_type(value, isinstance(value, (int, dict)), "int or dict") + if isinstance(value, int): + yield from self._serialize_from_int(value) + else: + yield from self._serialize_from_dict(context, value) + + @staticmethod + def _serialize_from_int(value: int) -> Generator[int, None, None]: + uint512_range_check(value) + low0 = value % (2**128) + low1 = (value >> 128) % (2**128) + high0 = (value >> 256) % (2**128) + high1 = (value >> 384) % (2**128) + result = (low0, low1, high0, high1) + yield from result + + def _serialize_from_dict( + self, context: SerializationContext, value: Uint512Dict + ) -> Generator[int, None, None]: + with context.push_entity("low0"): + self._ensure_valid_uint128(value["low0"], context) + yield value["low0"] + with context.push_entity("low1"): + self._ensure_valid_uint128(value["low1"], context) + yield value["low1"] + with context.push_entity("high0"): + self._ensure_valid_uint128(value["high0"], context) + yield value["high0"] + with context.push_entity("high1"): + self._ensure_valid_uint128(value["high1"], context) + yield value["high1"] + + @staticmethod + def _ensure_valid_uint128(value: int, context: Context): + context.ensure_valid_value( + 0 <= value < U128_UPPER_BOUND, "expected value in range [0;2**128)" + ) \ No newline at end of file diff --git a/starknet_py/tests/unit/serialization/data_serializers/uint512_serializer_test.py b/starknet_py/tests/unit/serialization/data_serializers/uint512_serializer_test.py new file mode 100644 index 000000000..fdde2eae5 --- /dev/null +++ b/starknet_py/tests/unit/serialization/data_serializers/uint512_serializer_test.py @@ -0,0 +1,133 @@ +import re + +import pytest + +from starknet_py.serialization.data_serializers.uint512_serializer import ( + Uint512Serializer, +) +from starknet_py.serialization.errors import InvalidTypeException, InvalidValueException + +serializer = Uint512Serializer() +SHIFT_128 = 2**128 +SHIFT_256 = 2**256 +SHIFT_384 = 2**384 +MAX_U128 = SHIFT_128 - 1 + + +@pytest.mark.parametrize( + "value, serialized_value", + [ + (123 + 456 * SHIFT_128 + 789 * SHIFT_256 + 101 * SHIFT_384, [123, 456, 789, 101]), + ( + 21323213211421424142 + 347932774343 * SHIFT_128 + 987654321 * SHIFT_256 + 123456789 * SHIFT_384, + [21323213211421424142, 347932774343, 987654321, 123456789], + ), + (0, [0, 0, 0, 0]), + (MAX_U128, [MAX_U128, 0, 0, 0]), + (MAX_U128 * SHIFT_128, [0, MAX_U128, 0, 0]), + (MAX_U128 * SHIFT_256, [0, 0, MAX_U128, 0]), + (MAX_U128 * SHIFT_384, [0, 0, 0, MAX_U128]), + (MAX_U128 + MAX_U128 * SHIFT_128 + MAX_U128 * SHIFT_256 + MAX_U128 * SHIFT_384, [MAX_U128, MAX_U128, MAX_U128, MAX_U128]), + (1, [1, 0, 0, 0]), + (SHIFT_128, [0, 1, 0, 0]), + (SHIFT_256, [0, 0, 1, 0]), + (SHIFT_384, [0, 0, 0, 1]), + ], +) +def test_valid_values(value, serialized_value): + deserialized = serializer.deserialize(serialized_value) + assert deserialized == value + + serialized = serializer.serialize(value) + assert serialized == serialized_value + + assert serialized_value == serializer.serialize( + {"low0": serialized_value[0], "low1": serialized_value[1], "high0": serialized_value[2], "high1": serialized_value[3]} + ) + + +def test_deserialize_invalid_values(): + # We need to escape braces + low0_error_message = re.escape( + "Error at path 'low0': expected value in range [0;2**128)" + ) + with pytest.raises(InvalidValueException, match=low0_error_message): + serializer.deserialize([MAX_U128 + 1, 0, 0, 0]) + with pytest.raises(InvalidValueException, match=low0_error_message): + serializer.deserialize([MAX_U128 + 1, MAX_U128 + 1, MAX_U128 + 1, MAX_U128 + 1]) + with pytest.raises(InvalidValueException, match=low0_error_message): + serializer.deserialize([-1, 0, 0, 0]) + + low1_error_message = re.escape( + "Error at path 'low1': expected value in range [0;2**128)" + ) + with pytest.raises(InvalidValueException, match=low1_error_message): + serializer.deserialize([0, MAX_U128 + 1, 0, 0]) + with pytest.raises(InvalidValueException, match=low1_error_message): + serializer.deserialize([0, -1, 0, 0]) + + high0_error_message = re.escape( + "Error at path 'high0': expected value in range [0;2**128)" + ) + with pytest.raises(InvalidValueException, match=high0_error_message): + serializer.deserialize([0, 0, MAX_U128 + 1, 0]) + with pytest.raises(InvalidValueException, match=high0_error_message): + serializer.deserialize([0, 0, -1, 0]) + + high1_error_message = re.escape( + "Error at path 'high1': expected value in range [0;2**128)" + ) + with pytest.raises(InvalidValueException, match=high1_error_message): + serializer.deserialize([0, 0, 0, MAX_U128 + 1]) + with pytest.raises(InvalidValueException, match=high1_error_message): + serializer.deserialize([0, 0, 0, -1]) + + +def test_serialize_invalid_int_value(): + error_message = re.escape("Error: Uint512 is expected to be in range [0;2**512)") + with pytest.raises(InvalidValueException, match=error_message): + serializer.serialize(2**512) + with pytest.raises(InvalidValueException, match=error_message): + serializer.serialize(-1) + + +def test_serialize_invalid_dict_values(): + low0_error_message = re.escape( + "Error at path 'low0': expected value in range [0;2**128)" + ) + with pytest.raises(InvalidValueException, match=low0_error_message): + serializer.serialize({"low0": -1, "low1": 12324, "high0": 456, "high1": 789}) + with pytest.raises(InvalidValueException, match=low0_error_message): + serializer.serialize({"low0": MAX_U128 + 1, "low1": 4543535, "high0": 456, "high1": 789}) + + low1_error_message = re.escape( + "Error at path 'low1': expected value in range [0;2**128)" + ) + with pytest.raises(InvalidValueException, match=low1_error_message): + serializer.serialize({"low0": 652432, "low1": -1, "high0": 456, "high1": 789}) + with pytest.raises(InvalidValueException, match=low1_error_message): + serializer.serialize({"low0": 0, "low1": MAX_U128 + 1, "high0": 456, "high1": 789}) + + high0_error_message = re.escape( + "Error at path 'high0': expected value in range [0;2**128)" + ) + with pytest.raises(InvalidValueException, match=high0_error_message): + serializer.serialize({"low0": 652432, "low1": 123, "high0": -1, "high1": 789}) + with pytest.raises(InvalidValueException, match=high0_error_message): + serializer.serialize({"low0": 0, "low1": 123, "high0": MAX_U128 + 1, "high1": 789}) + + high1_error_message = re.escape( + "Error at path 'high1': expected value in range [0;2**128)" + ) + with pytest.raises(InvalidValueException, match=high1_error_message): + serializer.serialize({"low0": 652432, "low1": 123, "high0": 456, "high1": -1}) + with pytest.raises(InvalidValueException, match=high1_error_message): + serializer.serialize({"low0": 0, "low1": 123, "high0": 456, "high1": MAX_U128 + 1}) + + +def test_invalid_type(): + error_message = re.escape( + "Error: expected int or dict, received 'wololoo' of type ''." + ) + with pytest.raises(InvalidTypeException, match=error_message): + serializer.serialize("wololoo") # type: ignore \ No newline at end of file From e0c67511ac44cee0f8cc26f0d8bd0ca4d5a8b9bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:13:06 +0000 Subject: [PATCH 3/3] Update Uint512Serializer to use limb0-limb3 naming convention per Cairo u512 docs Co-authored-by: franciszekjob <54181625+franciszekjob@users.noreply.github.com> --- .../data_serializers/uint512_serializer.py | 64 +++++++------- .../uint512_serializer_test.py | 84 +++++++++---------- 2 files changed, 74 insertions(+), 74 deletions(-) diff --git a/starknet_py/serialization/data_serializers/uint512_serializer.py b/starknet_py/serialization/data_serializers/uint512_serializer.py index 176928cee..1d2abd38d 100644 --- a/starknet_py/serialization/data_serializers/uint512_serializer.py +++ b/starknet_py/serialization/data_serializers/uint512_serializer.py @@ -15,16 +15,16 @@ class Uint512Dict(TypedDict): - low0: int - low1: int - high0: int - high1: int + limb0: int + limb1: int + limb2: int + limb3: int @dataclass class Uint512Serializer(CairoDataSerializer[Union[int, Uint512Dict], int]): """ - Serializer of Uint512. In Cairo it is represented by structure {low0: Uint128, low1: Uint128, high0: Uint128, high1: Uint128}. + Serializer of Uint512. In Cairo it is represented by structure {limb0: Uint128, limb1: Uint128, limb2: Uint128, limb3: Uint128}. Can serialize an int. Deserializes data to an int. @@ -38,19 +38,19 @@ class Uint512Serializer(CairoDataSerializer[Union[int, Uint512Dict], int]): """ def deserialize_with_context(self, context: DeserializationContext) -> int: - [low0, low1, high0, high1] = context.reader.read(4) + [limb0, limb1, limb2, limb3] = context.reader.read(4) # Checking if resulting value is in [0, 2**512) range is not enough. Uint512 should be made of four uint128. - with context.push_entity("low0"): - self._ensure_valid_uint128(low0, context) - with context.push_entity("low1"): - self._ensure_valid_uint128(low1, context) - with context.push_entity("high0"): - self._ensure_valid_uint128(high0, context) - with context.push_entity("high1"): - self._ensure_valid_uint128(high1, context) + with context.push_entity("limb0"): + self._ensure_valid_uint128(limb0, context) + with context.push_entity("limb1"): + self._ensure_valid_uint128(limb1, context) + with context.push_entity("limb2"): + self._ensure_valid_uint128(limb2, context) + with context.push_entity("limb3"): + self._ensure_valid_uint128(limb3, context) - return (high1 << 384) + (high0 << 256) + (low1 << 128) + low0 + return (limb3 << 384) + (limb2 << 256) + (limb1 << 128) + limb0 def serialize_with_context( self, context: SerializationContext, value: Union[int, Uint512Dict] @@ -64,28 +64,28 @@ def serialize_with_context( @staticmethod def _serialize_from_int(value: int) -> Generator[int, None, None]: uint512_range_check(value) - low0 = value % (2**128) - low1 = (value >> 128) % (2**128) - high0 = (value >> 256) % (2**128) - high1 = (value >> 384) % (2**128) - result = (low0, low1, high0, high1) + limb0 = value % (2**128) + limb1 = (value >> 128) % (2**128) + limb2 = (value >> 256) % (2**128) + limb3 = (value >> 384) % (2**128) + result = (limb0, limb1, limb2, limb3) yield from result def _serialize_from_dict( self, context: SerializationContext, value: Uint512Dict ) -> Generator[int, None, None]: - with context.push_entity("low0"): - self._ensure_valid_uint128(value["low0"], context) - yield value["low0"] - with context.push_entity("low1"): - self._ensure_valid_uint128(value["low1"], context) - yield value["low1"] - with context.push_entity("high0"): - self._ensure_valid_uint128(value["high0"], context) - yield value["high0"] - with context.push_entity("high1"): - self._ensure_valid_uint128(value["high1"], context) - yield value["high1"] + with context.push_entity("limb0"): + self._ensure_valid_uint128(value["limb0"], context) + yield value["limb0"] + with context.push_entity("limb1"): + self._ensure_valid_uint128(value["limb1"], context) + yield value["limb1"] + with context.push_entity("limb2"): + self._ensure_valid_uint128(value["limb2"], context) + yield value["limb2"] + with context.push_entity("limb3"): + self._ensure_valid_uint128(value["limb3"], context) + yield value["limb3"] @staticmethod def _ensure_valid_uint128(value: int, context: Context): diff --git a/starknet_py/tests/unit/serialization/data_serializers/uint512_serializer_test.py b/starknet_py/tests/unit/serialization/data_serializers/uint512_serializer_test.py index fdde2eae5..45cf06dfd 100644 --- a/starknet_py/tests/unit/serialization/data_serializers/uint512_serializer_test.py +++ b/starknet_py/tests/unit/serialization/data_serializers/uint512_serializer_test.py @@ -42,44 +42,44 @@ def test_valid_values(value, serialized_value): assert serialized == serialized_value assert serialized_value == serializer.serialize( - {"low0": serialized_value[0], "low1": serialized_value[1], "high0": serialized_value[2], "high1": serialized_value[3]} + {"limb0": serialized_value[0], "limb1": serialized_value[1], "limb2": serialized_value[2], "limb3": serialized_value[3]} ) def test_deserialize_invalid_values(): # We need to escape braces - low0_error_message = re.escape( - "Error at path 'low0': expected value in range [0;2**128)" + limb0_error_message = re.escape( + "Error at path 'limb0': expected value in range [0;2**128)" ) - with pytest.raises(InvalidValueException, match=low0_error_message): + with pytest.raises(InvalidValueException, match=limb0_error_message): serializer.deserialize([MAX_U128 + 1, 0, 0, 0]) - with pytest.raises(InvalidValueException, match=low0_error_message): + with pytest.raises(InvalidValueException, match=limb0_error_message): serializer.deserialize([MAX_U128 + 1, MAX_U128 + 1, MAX_U128 + 1, MAX_U128 + 1]) - with pytest.raises(InvalidValueException, match=low0_error_message): + with pytest.raises(InvalidValueException, match=limb0_error_message): serializer.deserialize([-1, 0, 0, 0]) - low1_error_message = re.escape( - "Error at path 'low1': expected value in range [0;2**128)" + limb1_error_message = re.escape( + "Error at path 'limb1': expected value in range [0;2**128)" ) - with pytest.raises(InvalidValueException, match=low1_error_message): + with pytest.raises(InvalidValueException, match=limb1_error_message): serializer.deserialize([0, MAX_U128 + 1, 0, 0]) - with pytest.raises(InvalidValueException, match=low1_error_message): + with pytest.raises(InvalidValueException, match=limb1_error_message): serializer.deserialize([0, -1, 0, 0]) - high0_error_message = re.escape( - "Error at path 'high0': expected value in range [0;2**128)" + limb2_error_message = re.escape( + "Error at path 'limb2': expected value in range [0;2**128)" ) - with pytest.raises(InvalidValueException, match=high0_error_message): + with pytest.raises(InvalidValueException, match=limb2_error_message): serializer.deserialize([0, 0, MAX_U128 + 1, 0]) - with pytest.raises(InvalidValueException, match=high0_error_message): + with pytest.raises(InvalidValueException, match=limb2_error_message): serializer.deserialize([0, 0, -1, 0]) - high1_error_message = re.escape( - "Error at path 'high1': expected value in range [0;2**128)" + limb3_error_message = re.escape( + "Error at path 'limb3': expected value in range [0;2**128)" ) - with pytest.raises(InvalidValueException, match=high1_error_message): + with pytest.raises(InvalidValueException, match=limb3_error_message): serializer.deserialize([0, 0, 0, MAX_U128 + 1]) - with pytest.raises(InvalidValueException, match=high1_error_message): + with pytest.raises(InvalidValueException, match=limb3_error_message): serializer.deserialize([0, 0, 0, -1]) @@ -92,37 +92,37 @@ def test_serialize_invalid_int_value(): def test_serialize_invalid_dict_values(): - low0_error_message = re.escape( - "Error at path 'low0': expected value in range [0;2**128)" + limb0_error_message = re.escape( + "Error at path 'limb0': expected value in range [0;2**128)" ) - with pytest.raises(InvalidValueException, match=low0_error_message): - serializer.serialize({"low0": -1, "low1": 12324, "high0": 456, "high1": 789}) - with pytest.raises(InvalidValueException, match=low0_error_message): - serializer.serialize({"low0": MAX_U128 + 1, "low1": 4543535, "high0": 456, "high1": 789}) + with pytest.raises(InvalidValueException, match=limb0_error_message): + serializer.serialize({"limb0": -1, "limb1": 12324, "limb2": 456, "limb3": 789}) + with pytest.raises(InvalidValueException, match=limb0_error_message): + serializer.serialize({"limb0": MAX_U128 + 1, "limb1": 4543535, "limb2": 456, "limb3": 789}) - low1_error_message = re.escape( - "Error at path 'low1': expected value in range [0;2**128)" + limb1_error_message = re.escape( + "Error at path 'limb1': expected value in range [0;2**128)" ) - with pytest.raises(InvalidValueException, match=low1_error_message): - serializer.serialize({"low0": 652432, "low1": -1, "high0": 456, "high1": 789}) - with pytest.raises(InvalidValueException, match=low1_error_message): - serializer.serialize({"low0": 0, "low1": MAX_U128 + 1, "high0": 456, "high1": 789}) + with pytest.raises(InvalidValueException, match=limb1_error_message): + serializer.serialize({"limb0": 652432, "limb1": -1, "limb2": 456, "limb3": 789}) + with pytest.raises(InvalidValueException, match=limb1_error_message): + serializer.serialize({"limb0": 0, "limb1": MAX_U128 + 1, "limb2": 456, "limb3": 789}) - high0_error_message = re.escape( - "Error at path 'high0': expected value in range [0;2**128)" + limb2_error_message = re.escape( + "Error at path 'limb2': expected value in range [0;2**128)" ) - with pytest.raises(InvalidValueException, match=high0_error_message): - serializer.serialize({"low0": 652432, "low1": 123, "high0": -1, "high1": 789}) - with pytest.raises(InvalidValueException, match=high0_error_message): - serializer.serialize({"low0": 0, "low1": 123, "high0": MAX_U128 + 1, "high1": 789}) + with pytest.raises(InvalidValueException, match=limb2_error_message): + serializer.serialize({"limb0": 652432, "limb1": 123, "limb2": -1, "limb3": 789}) + with pytest.raises(InvalidValueException, match=limb2_error_message): + serializer.serialize({"limb0": 0, "limb1": 123, "limb2": MAX_U128 + 1, "limb3": 789}) - high1_error_message = re.escape( - "Error at path 'high1': expected value in range [0;2**128)" + limb3_error_message = re.escape( + "Error at path 'limb3': expected value in range [0;2**128)" ) - with pytest.raises(InvalidValueException, match=high1_error_message): - serializer.serialize({"low0": 652432, "low1": 123, "high0": 456, "high1": -1}) - with pytest.raises(InvalidValueException, match=high1_error_message): - serializer.serialize({"low0": 0, "low1": 123, "high0": 456, "high1": MAX_U128 + 1}) + with pytest.raises(InvalidValueException, match=limb3_error_message): + serializer.serialize({"limb0": 652432, "limb1": 123, "limb2": 456, "limb3": -1}) + with pytest.raises(InvalidValueException, match=limb3_error_message): + serializer.serialize({"limb0": 0, "limb1": 123, "limb2": 456, "limb3": MAX_U128 + 1}) def test_invalid_type():