From 8a3e75a72144ca66915fdded373b72746bc7a2a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Walter?= Date: Thu, 25 Jul 2024 10:48:09 +0200 Subject: [PATCH 01/10] Serialize RLP.Item Serialize RLP.Item and refactor tests --- src/utils/rlp.cairo | 12 ++---- tests/src/utils/test_rlp.cairo | 41 +++----------------- tests/src/utils/test_rlp.py | 70 ++++++++++------------------------ tests/utils/serde.py | 24 ++++++++++++ 4 files changed, 53 insertions(+), 94 deletions(-) diff --git a/src/utils/rlp.cairo b/src/utils/rlp.cairo index 554c4d703..d3ef58f01 100644 --- a/src/utils/rlp.cairo +++ b/src/utils/rlp.cairo @@ -91,14 +91,8 @@ namespace RLP { } tempvar items = items + Item.SIZE; - let total_item_len = len + offset; - let is_lt_input = is_nn(data_len + 1 - total_item_len); - if (is_lt_input != FALSE) { - let items_len = decode( - items=items, data_len=data_len - total_item_len, data=data + total_item_len - ); - return 1 + items_len; - } - return 1; + let remaining_data_len = data_len - len - offset; + let items_len = decode(items=items, data_len=remaining_data_len, data=data + offset + len); + return 1 + items_len; } } diff --git a/tests/src/utils/test_rlp.cairo b/tests/src/utils/test_rlp.cairo index 32210fbe8..9761ce040 100644 --- a/tests/src/utils/test_rlp.cairo +++ b/tests/src/utils/test_rlp.cairo @@ -5,7 +5,7 @@ from starkware.cairo.common.cairo_builtins import HashBuiltin, BitwiseBuiltin from starkware.cairo.common.alloc import alloc from starkware.cairo.common.memcpy import memcpy -func test__decode{range_check_ptr}(output_ptr: felt*) { +func test__decode{range_check_ptr}() -> (items_len: felt, items: RLP.Item*) { alloc_locals; // Given tempvar data_len: felt; @@ -17,17 +17,12 @@ func test__decode{range_check_ptr}(output_ptr: felt*) { // When let (local items: RLP.Item*) = alloc(); - RLP.decode(items, data_len, data); + let items_len = RLP.decode(items, data_len, data); - %{ - from tests.utils.hints import flatten_rlp_list - # The cairo functions returns a single RLP list of size 1 containing the decoded objects. - flatten_rlp_list(ids.items.address_, 1, ids.output_ptr, memory, segments) - %} - return (); + return (items_len, items); } -func test__decode_type{range_check_ptr}(output_ptr: felt*) { +func test__decode_type{range_check_ptr}() -> (felt, felt, felt) { alloc_locals; // Given tempvar data_len: felt; @@ -41,31 +36,5 @@ func test__decode_type{range_check_ptr}(output_ptr: felt*) { let (type, offset, len) = RLP.decode_type(data_len, data); // Then - assert [output_ptr] = type; - assert [output_ptr + 1] = offset; - assert [output_ptr + 2] = len; - - return (); -} - -func test__decode_transaction{range_check_ptr}(output_ptr: felt*) { - alloc_locals; - // Given - tempvar data_len: felt; - let (data) = alloc(); - %{ - ids.data_len = len(program_input["data"]) - segments.write_arg(ids.data, program_input["data"]) - %} - - // When - let (local items: RLP.Item*) = alloc(); - RLP.decode(items, data_len, data); - - %{ - from tests.utils.hints import flatten_rlp_list - # The cairo functions returns a single RLP list of size 1 containing the decoded objects. - flatten_rlp_list(ids.items.address_, 1, ids.output_ptr, memory, segments) - %} - return (); + return (type, offset, len); } diff --git a/tests/src/utils/test_rlp.py b/tests/src/utils/test_rlp.py index 34475fe07..d058c0a03 100644 --- a/tests/src/utils/test_rlp.py +++ b/tests/src/utils/test_rlp.py @@ -1,25 +1,16 @@ -import random - import pytest +from hypothesis import given +from hypothesis.strategies import binary from rlp import codec, decode, encode from tests.utils.constants import TRANSACTIONS -from tests.utils.helpers import flatten, rlp_encode_signed_data +from tests.utils.helpers import rlp_encode_signed_data class TestRLP: class TestDecodeType: - @pytest.mark.parametrize( - "payload_len", - [0, 55, 256], - ) - def test_should_match_decoded_rlp_type_string(self, cairo_run, payload_len): - # generate random string of bytes. if payload_len is 0, then generate a single byte inferior to 0x80 - data = ( - random.randbytes(payload_len) - if payload_len != 0 - else random.randint(0, 0x80) - ) + @given(data=binary(min_size=0, max_size=255)) + def test_should_match_decoded_rlp_type_string(self, cairo_run, data): encoded_data = encode(data) [ @@ -30,16 +21,13 @@ def test_should_match_decoded_rlp_type_string(self, cairo_run, payload_len): ] = codec.consume_length_prefix(encoded_data, 0) expected_type = 0 if rlp_type == bytes else 1 - output = cairo_run("test__decode_type", data=encoded_data) + output = cairo_run("test__decode_type", data=list(encoded_data)) - assert output[0] == expected_type - assert output[1] == expected_offset - assert output[2] == expected_len + assert output == [expected_type, expected_offset, expected_len] - @pytest.mark.parametrize("payload_len", [0, 55, 256]) - def test_should_match_decoded_rlp_type_list(self, cairo_run, payload_len): - data = [random.randbytes(payload_len)] - encoded_data = encode(data) + @given(data=binary(min_size=0, max_size=255)) + def test_should_match_decoded_rlp_type_list(self, cairo_run, data): + encoded_data = encode([data]) [ prefix, @@ -47,31 +35,22 @@ def test_should_match_decoded_rlp_type_list(self, cairo_run, payload_len): expected_len, expected_offset, ] = codec.consume_length_prefix(encoded_data, 0) - expected_type = 1 if rlp_type == list else 0 + expected_type = 0 if rlp_type == bytes else 1 - output = cairo_run("test__decode_type", data=encoded_data) + output = cairo_run("test__decode_type", data=list(encoded_data)) - assert output[0] == expected_type - assert output[1] == expected_offset - assert output[2] == expected_len + assert output == [expected_type, expected_offset, expected_len] class TestDecode: - @pytest.mark.parametrize("payload_len", [55, 56]) + @given(data=binary(min_size=0, max_size=255)) async def test_should_match_decode_reference_implementation( - self, cairo_run, payload_len + self, cairo_run, data ): - data = [random.randbytes(payload_len - 1)] encoded_data = encode(data) - expected_result = decode(encoded_data) - # flatten the decoded data into a single bytes l - # there must be no nested lists at the end - flattened_data = flatten(list(expected_result)) - flattened_output = cairo_run( - "test__decode", data=list(encoded_data), is_list=1 - ) - - assert flattened_data == flattened_output + items_len, items = cairo_run("test__decode", data=list(encoded_data)) + decoded = items[0] if items_len == 1 else items + assert decoded == decode(encoded_data) @pytest.mark.parametrize("transaction", TRANSACTIONS) def test_should_decode_all_tx_types(self, cairo_run, transaction): @@ -84,13 +63,6 @@ def test_should_decode_all_tx_types(self, cairo_run, transaction): else: rlp_encoding = encoded_unsigned_tx - decoded_tx = decode(rlp_encoding) - - # flatten the decoded data into a single bytes l - # there must be no nested lists at the end - flattened_data = flatten(decoded_tx) - flattened_output = cairo_run( - "test__decode_transaction", data=rlp_encoding, is_list=1 - ) - - assert flattened_data == flattened_output + items_len, items = cairo_run("test__decode", data=list(rlp_encoding)) + decoded = items[0] if items_len == 1 else items + assert decoded == decode(rlp_encoding) diff --git a/tests/utils/serde.py b/tests/utils/serde.py index 3556a813a..b8460c8e7 100644 --- a/tests/utils/serde.py +++ b/tests/utils/serde.py @@ -57,6 +57,9 @@ def serialize_list(self, segment_ptr, item_scope=None, list_len=None): return output def serialize_dict(self, dict_ptr, value_scope=None, dict_size=None): + """ + Serialize a dict. + """ if dict_size is None: dict_size = self.runner.segments.get_segment_size(dict_ptr.segment_index) output = {} @@ -79,6 +82,9 @@ def serialize_dict(self, dict_ptr, value_scope=None, dict_size=None): return output def serialize_pointers(self, name, ptr): + """ + Serialize a pointer to a struct, e.g. Uint256*. + """ members = self.get_identifier(name, StructDefinition).members output = {} for name, member in members.items(): @@ -89,6 +95,9 @@ def serialize_pointers(self, name, ptr): return output def serialize_struct(self, name, ptr): + """ + Serialize a struct, e.g. Uint256. + """ if ptr is None: return None members = self.get_identifier(name, StructDefinition).members @@ -209,6 +218,19 @@ def serialize_memory(self, ptr): [f"{memory_dict.get(i, 0):032x}" for i in range(raw["words_len"] * 2)] ) + def serialize_rlp_item(self, ptr): + raw = self.serialize_list(ptr) + items = [] + for i in range(0, len(raw), 3): + data_len = raw[i] + data_ptr = raw[i + 1] + is_list = raw[i + 2] + if not is_list: + items += [bytes(self.serialize_list(data_ptr)[:data_len])] + else: + items += [self.serialize_rlp_item(data_ptr)] + return items + def serialize_scope(self, scope, scope_ptr): if scope.path[-1] == "State": return self.serialize_state(scope_ptr) @@ -228,6 +250,8 @@ def serialize_scope(self, scope, scope_ptr): return self.serialize_message(scope_ptr) if scope.path[-1] == "EVM": return self.serialize_evm(scope_ptr) + if scope.path[-2:] == ("RLP", "Item"): + return self.serialize_rlp_item(scope_ptr) try: return self.serialize_struct(str(scope), scope_ptr) except MissingIdentifierError: From 3ec278d2fbef5bc7c22a9af9d8618c3001997039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Walter?= Date: Thu, 25 Jul 2024 13:35:30 +0200 Subject: [PATCH 02/10] Remove useless hint util --- tests/utils/hints.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/utils/hints.py b/tests/utils/hints.py index d074246bf..d32db034e 100644 --- a/tests/utils/hints.py +++ b/tests/utils/hints.py @@ -17,24 +17,6 @@ def _debug_info(pc): return _debug_info -def flatten_rlp_list(list_ptr, list_len, output_ptr, memory, segments): - for i in range(list_len): - data_len = memory[list_ptr + i * 3] - data_ptr = memory[list_ptr + i * 3 + 1] - is_list = memory[list_ptr + i * 3 + 2] - - if is_list: - output_ptr = flatten_rlp_list( - data_ptr, data_len, output_ptr, memory, segments - ) - else: - data = [memory[data_ptr + j] for j in range(data_len)] - segments.write_arg(output_ptr, data) - output_ptr += len(data) - - return output_ptr - - def new_default_dict( dict_manager, segments, default_value, initial_dict, temp_segment: bool = False ): From c9649340147e24b65f3148c8f75e69410e192772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Walter?= Date: Thu, 25 Jul 2024 14:29:45 +0200 Subject: [PATCH 03/10] Enforce strict mode for decode --- src/utils/rlp.cairo | 19 ++++++++++++++++--- tests/src/utils/test_rlp.py | 16 +++++++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/utils/rlp.cairo b/src/utils/rlp.cairo index d3ef58f01..d343125f7 100644 --- a/src/utils/rlp.cairo +++ b/src/utils/rlp.cairo @@ -68,7 +68,7 @@ namespace RLP { // @param data The RLP encoded data. // @param items The pointer to the next free cell in the list of items decoded. // @return items_len The number of items decoded. - func decode{range_check_ptr}(items: Item*, data_len: felt, data: felt*) -> felt { + func decode_raw{range_check_ptr}(items: Item*, data_len: felt, data: felt*) -> felt { alloc_locals; if (data_len == 0) { @@ -80,7 +80,7 @@ namespace RLP { if (rlp_type == 1) { // Case list let (sub_items: Item*) = alloc(); - let sub_items_len = decode(items=sub_items, data_len=len, data=data + offset); + let sub_items_len = decode_raw(items=sub_items, data_len=len, data=data + offset); assert [items] = Item(data_len=sub_items_len, data=cast(sub_items, felt*), is_list=1); tempvar range_check_ptr = range_check_ptr; } else { @@ -92,7 +92,20 @@ namespace RLP { tempvar items = items + Item.SIZE; let remaining_data_len = data_len - len - offset; - let items_len = decode(items=items, data_len=remaining_data_len, data=data + offset + len); + let items_len = decode_raw( + items=items, data_len=remaining_data_len, data=data + offset + len + ); return 1 + items_len; } + + func decode{range_check_ptr}(items: Item*, data_len: felt, data: felt*) -> felt { + alloc_locals; + let (rlp_type, offset, len) = decode_type(data_len=data_len, data=data); + local extra_bytes = data_len - offset - len; + with_attr error_message("RLP string ends with {extra_bytes} superfluous bytes") { + assert extra_bytes = 0; + } + let items_len = decode_raw(items=items, data_len=data_len, data=data); + return items_len; + } } diff --git a/tests/src/utils/test_rlp.py b/tests/src/utils/test_rlp.py index d058c0a03..8e7778ca4 100644 --- a/tests/src/utils/test_rlp.py +++ b/tests/src/utils/test_rlp.py @@ -4,6 +4,7 @@ from rlp import codec, decode, encode from tests.utils.constants import TRANSACTIONS +from tests.utils.errors import cairo_error from tests.utils.helpers import rlp_encode_signed_data @@ -52,9 +53,22 @@ async def test_should_match_decode_reference_implementation( decoded = items[0] if items_len == 1 else items assert decoded == decode(encoded_data) + @given( + data=binary(min_size=0, max_size=255), + extra_data=binary(min_size=1, max_size=255), + ) + async def test_raise_when_data_contains_extra_bytes( + self, cairo_run, data, extra_data + ): + encoded_data = encode(data) + + with cairo_error( + f"RLP string ends with {len(extra_data)} superfluous bytes" + ): + cairo_run("test__decode", data=list(encoded_data + extra_data)) + @pytest.mark.parametrize("transaction", TRANSACTIONS) def test_should_decode_all_tx_types(self, cairo_run, transaction): - transaction = {**transaction, "chainId": 1} encoded_unsigned_tx = rlp_encode_signed_data(transaction) if "type" in transaction: # remove the type info from the encoded RLP From cf14a462be0400d335ddd49e6d562139f77a0dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Walter?= Date: Thu, 25 Jul 2024 14:55:48 +0200 Subject: [PATCH 04/10] Make test cases more generic --- tests/conftest.py | 2 ++ tests/fixtures/starknet.py | 3 --- tests/src/utils/test_rlp.py | 26 +++++--------------------- 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b5e9b6e4b..4da8c29b2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,9 +3,11 @@ import os import pytest +from dotenv import load_dotenv from hypothesis import Verbosity, settings from starkware.cairo.lang.instances import LAYOUTS +load_dotenv() logging.getLogger("asyncio").setLevel(logging.ERROR) logger = logging.getLogger() diff --git a/tests/fixtures/starknet.py b/tests/fixtures/starknet.py index ccbde8251..9aecd9dde 100644 --- a/tests/fixtures/starknet.py +++ b/tests/fixtures/starknet.py @@ -234,9 +234,6 @@ def _factory(entrypoint, **kwargs) -> list: output_stem = Path( f"{output_stem[:160]}_{int(time_ns())}_{md5(output_stem.encode()).digest().hex()[:8]}" ) - logger.info( - f"Test {request.node.path.stem}_{entrypoint}_{displayed_args} artifacts saved at: {output_stem}" - ) if request.config.getoption("profile_cairo"): tracer_data = TracerData( program=cairo_program, diff --git a/tests/src/utils/test_rlp.py b/tests/src/utils/test_rlp.py index 8e7778ca4..a31ff683d 100644 --- a/tests/src/utils/test_rlp.py +++ b/tests/src/utils/test_rlp.py @@ -1,6 +1,6 @@ import pytest from hypothesis import given -from hypothesis.strategies import binary +from hypothesis.strategies import binary, lists, recursive from rlp import codec, decode, encode from tests.utils.constants import TRANSACTIONS @@ -10,8 +10,8 @@ class TestRLP: class TestDecodeType: - @given(data=binary(min_size=0, max_size=255)) - def test_should_match_decoded_rlp_type_string(self, cairo_run, data): + @given(data=lists(binary()) | binary()) + def test_should_match_prefix_reference_implementation(self, cairo_run, data): encoded_data = encode(data) [ @@ -26,24 +26,8 @@ def test_should_match_decoded_rlp_type_string(self, cairo_run, data): assert output == [expected_type, expected_offset, expected_len] - @given(data=binary(min_size=0, max_size=255)) - def test_should_match_decoded_rlp_type_list(self, cairo_run, data): - encoded_data = encode([data]) - - [ - prefix, - rlp_type, - expected_len, - expected_offset, - ] = codec.consume_length_prefix(encoded_data, 0) - expected_type = 0 if rlp_type == bytes else 1 - - output = cairo_run("test__decode_type", data=list(encoded_data)) - - assert output == [expected_type, expected_offset, expected_len] - class TestDecode: - @given(data=binary(min_size=0, max_size=255)) + @given(data=recursive(binary(), lists)) async def test_should_match_decode_reference_implementation( self, cairo_run, data ): @@ -54,7 +38,7 @@ async def test_should_match_decode_reference_implementation( assert decoded == decode(encoded_data) @given( - data=binary(min_size=0, max_size=255), + data=recursive(binary(), lists), extra_data=binary(min_size=1, max_size=255), ) async def test_raise_when_data_contains_extra_bytes( From 6d97f4ccb7b5bde9ea6e976a5e648c7cdd0257e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Walter?= Date: Thu, 25 Jul 2024 15:53:39 +0200 Subject: [PATCH 05/10] Assert items len in eth_transaction --- src/utils/eth_transaction.cairo | 82 +++++++++++++------------ src/utils/rlp.cairo | 1 + tests/src/utils/test_eth_transaction.py | 5 +- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/utils/eth_transaction.cairo b/src/utils/eth_transaction.cairo index 2f29dbda5..86e7aa000 100644 --- a/src/utils/eth_transaction.cairo +++ b/src/utils/eth_transaction.cairo @@ -31,27 +31,26 @@ namespace EthTransaction { let (items: RLP.Item*) = alloc(); RLP.decode(items, tx_data_len, tx_data); - // the tx is a list of fields, hence first level RLP decoding - // is a single item, which is indeed the sought list assert [items].is_list = TRUE; - let sub_items_len = [items].data_len; - let sub_items = cast([items].data, RLP.Item*); + let items_len = [items].data_len; + let items = cast([items].data, RLP.Item*); - let nonce = Helpers.bytes_to_felt(sub_items[0].data_len, sub_items[0].data); - let gas_price = Helpers.bytes_to_felt(sub_items[1].data_len, sub_items[1].data); - let gas_limit = Helpers.bytes_to_felt(sub_items[2].data_len, sub_items[2].data); + let nonce = Helpers.bytes_to_felt(items[0].data_len, items[0].data); + let gas_price = Helpers.bytes_to_felt(items[1].data_len, items[1].data); + let gas_limit = Helpers.bytes_to_felt(items[2].data_len, items[2].data); let destination = Helpers.try_parse_destination_from_bytes( - sub_items[3].data_len, sub_items[3].data + items[3].data_len, items[3].data ); - let amount = Helpers.bytes_to_uint256(sub_items[4].data_len, sub_items[4].data); - let payload_len = sub_items[5].data_len; - let payload = sub_items[5].data; + let amount = Helpers.bytes_to_uint256(items[4].data_len, items[4].data); + let payload_len = items[5].data_len; + let payload = items[5].data; // pre eip-155 txs have 6 fields, post eip-155 txs have 9 fields - if (sub_items_len == 6) { + if (items_len == 6) { tempvar chain_id = 0; } else { - let chain_id = Helpers.bytes_to_felt(sub_items[6].data_len, sub_items[6].data); + assert items_len = 9; + let chain_id = Helpers.bytes_to_felt(items[6].data_len, items[6].data); } let chain_id = [ap - 1]; @@ -82,23 +81,27 @@ namespace EthTransaction { let (items: RLP.Item*) = alloc(); RLP.decode(items, tx_data_len - 1, tx_data + 1); - let sub_items_len = [items].data_len; - let sub_items = cast([items].data, RLP.Item*); - let chain_id = Helpers.bytes_to_felt(sub_items[0].data_len, sub_items[0].data); - let nonce = Helpers.bytes_to_felt(sub_items[1].data_len, sub_items[1].data); - let gas_price = Helpers.bytes_to_felt(sub_items[2].data_len, sub_items[2].data); - let gas_limit = Helpers.bytes_to_felt(sub_items[3].data_len, sub_items[3].data); + assert [items].is_list = TRUE; + let items_len = [items].data_len; + let items = cast([items].data, RLP.Item*); + + assert items_len = 8; + + let chain_id = Helpers.bytes_to_felt(items[0].data_len, items[0].data); + let nonce = Helpers.bytes_to_felt(items[1].data_len, items[1].data); + let gas_price = Helpers.bytes_to_felt(items[2].data_len, items[2].data); + let gas_limit = Helpers.bytes_to_felt(items[3].data_len, items[3].data); let destination = Helpers.try_parse_destination_from_bytes( - sub_items[4].data_len, sub_items[4].data + items[4].data_len, items[4].data ); - let amount = Helpers.bytes_to_uint256(sub_items[5].data_len, sub_items[5].data); - let payload_len = sub_items[6].data_len; - let payload = sub_items[6].data; + let amount = Helpers.bytes_to_uint256(items[5].data_len, items[5].data); + let payload_len = items[6].data_len; + let payload = items[6].data; let (access_list: felt*) = alloc(); let access_list_len = parse_access_list( - access_list, sub_items[7].data_len, cast(sub_items[7].data, RLP.Item*) + access_list, items[7].data_len, cast(items[7].data, RLP.Item*) ); tempvar tx = new model.EthTransaction( signer_nonce=nonce, @@ -127,28 +130,27 @@ namespace EthTransaction { let (items: RLP.Item*) = alloc(); RLP.decode(items, tx_data_len - 1, tx_data + 1); - // the tx is a list of fields, hence first level RLP decoding - // is a single item, which is indeed the sought list + assert [items].is_list = TRUE; - let sub_items_len = [items].data_len; - let sub_items = cast([items].data, RLP.Item*); + let items_len = [items].data_len; + let items = cast([items].data, RLP.Item*); - let chain_id = Helpers.bytes_to_felt(sub_items[0].data_len, sub_items[0].data); - let nonce = Helpers.bytes_to_felt(sub_items[1].data_len, sub_items[1].data); - let max_priority_fee_per_gas = Helpers.bytes_to_felt( - sub_items[2].data_len, sub_items[2].data - ); - let max_fee_per_gas = Helpers.bytes_to_felt(sub_items[3].data_len, sub_items[3].data); - let gas_limit = Helpers.bytes_to_felt(sub_items[4].data_len, sub_items[4].data); + assert items_len = 9; + + let chain_id = Helpers.bytes_to_felt(items[0].data_len, items[0].data); + let nonce = Helpers.bytes_to_felt(items[1].data_len, items[1].data); + let max_priority_fee_per_gas = Helpers.bytes_to_felt(items[2].data_len, items[2].data); + let max_fee_per_gas = Helpers.bytes_to_felt(items[3].data_len, items[3].data); + let gas_limit = Helpers.bytes_to_felt(items[4].data_len, items[4].data); let destination = Helpers.try_parse_destination_from_bytes( - sub_items[5].data_len, sub_items[5].data + items[5].data_len, items[5].data ); - let amount = Helpers.bytes_to_uint256(sub_items[6].data_len, sub_items[6].data); - let payload_len = sub_items[7].data_len; - let payload = sub_items[7].data; + let amount = Helpers.bytes_to_uint256(items[6].data_len, items[6].data); + let payload_len = items[7].data_len; + let payload = items[7].data; let (access_list: felt*) = alloc(); let access_list_len = parse_access_list( - access_list, sub_items[8].data_len, cast(sub_items[8].data, RLP.Item*) + access_list, items[8].data_len, cast(items[8].data, RLP.Item*) ); tempvar tx = new model.EthTransaction( signer_nonce=nonce, diff --git a/src/utils/rlp.cairo b/src/utils/rlp.cairo index d343125f7..d4708a7b5 100644 --- a/src/utils/rlp.cairo +++ b/src/utils/rlp.cairo @@ -106,6 +106,7 @@ namespace RLP { assert extra_bytes = 0; } let items_len = decode_raw(items=items, data_len=data_len, data=data); + assert items_len = 1; return items_len; } } diff --git a/tests/src/utils/test_eth_transaction.py b/tests/src/utils/test_eth_transaction.py index 55fbe1b8f..5d18fd105 100644 --- a/tests/src/utils/test_eth_transaction.py +++ b/tests/src/utils/test_eth_transaction.py @@ -15,10 +15,7 @@ async def test_should_decode_all_transactions_types( self, cairo_run, transaction ): encoded_unsigned_tx = rlp_encode_signed_data(transaction) - decoded_tx = cairo_run( - "test__decode", - data=list(encoded_unsigned_tx), - ) + decoded_tx = cairo_run("test__decode", data=list(encoded_unsigned_tx)) expected_data = ( "0x" + transaction["data"].hex() From 46ab98ae221883ff9fa7b4424ddc6fb76d21c5d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Walter?= Date: Thu, 25 Jul 2024 16:01:33 +0200 Subject: [PATCH 06/10] Assert items are list or not --- src/utils/eth_transaction.cairo | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/utils/eth_transaction.cairo b/src/utils/eth_transaction.cairo index 86e7aa000..95ac50924 100644 --- a/src/utils/eth_transaction.cairo +++ b/src/utils/eth_transaction.cairo @@ -35,6 +35,13 @@ namespace EthTransaction { let items_len = [items].data_len; let items = cast([items].data, RLP.Item*); + assert items[0].is_list = FALSE; + assert items[1].is_list = FALSE; + assert items[2].is_list = FALSE; + assert items[3].is_list = FALSE; + assert items[4].is_list = FALSE; + assert items[5].is_list = FALSE; + let nonce = Helpers.bytes_to_felt(items[0].data_len, items[0].data); let gas_price = Helpers.bytes_to_felt(items[1].data_len, items[1].data); let gas_limit = Helpers.bytes_to_felt(items[2].data_len, items[2].data); @@ -50,6 +57,9 @@ namespace EthTransaction { tempvar chain_id = 0; } else { assert items_len = 9; + assert items[6].is_list = FALSE; + assert items[7].is_list = FALSE; + assert items[8].is_list = FALSE; let chain_id = Helpers.bytes_to_felt(items[6].data_len, items[6].data); } let chain_id = [ap - 1]; @@ -87,6 +97,14 @@ namespace EthTransaction { let items = cast([items].data, RLP.Item*); assert items_len = 8; + assert items[0].is_list = FALSE; + assert items[1].is_list = FALSE; + assert items[2].is_list = FALSE; + assert items[3].is_list = FALSE; + assert items[4].is_list = FALSE; + assert items[5].is_list = FALSE; + assert items[6].is_list = FALSE; + assert items[7].is_list = TRUE; let chain_id = Helpers.bytes_to_felt(items[0].data_len, items[0].data); let nonce = Helpers.bytes_to_felt(items[1].data_len, items[1].data); @@ -136,6 +154,15 @@ namespace EthTransaction { let items = cast([items].data, RLP.Item*); assert items_len = 9; + assert items[0].is_list = FALSE; + assert items[1].is_list = FALSE; + assert items[2].is_list = FALSE; + assert items[3].is_list = FALSE; + assert items[4].is_list = FALSE; + assert items[5].is_list = FALSE; + assert items[6].is_list = FALSE; + assert items[7].is_list = FALSE; + assert items[8].is_list = TRUE; let chain_id = Helpers.bytes_to_felt(items[0].data_len, items[0].data); let nonce = Helpers.bytes_to_felt(items[1].data_len, items[1].data); From 36eef12077d45980ff037ff4aa4ea1087f00a878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Walter?= Date: Thu, 25 Jul 2024 16:31:54 +0200 Subject: [PATCH 07/10] Add a test for non list items in tx --- tests/src/utils/test_eth_transaction.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/src/utils/test_eth_transaction.py b/tests/src/utils/test_eth_transaction.py index 5d18fd105..e0c3d01b1 100644 --- a/tests/src/utils/test_eth_transaction.py +++ b/tests/src/utils/test_eth_transaction.py @@ -10,6 +10,19 @@ class TestEthTransaction: class TestDecodeTransaction: + + async def test_should_raise_with_list_items(self, cairo_run): + transaction = { + "nonce": 0, + "gasPrice": 234567897654321, + "gas": 2_000_000, + "to": "0xF0109fC8DF283027b6285cc889F5aA624EaC1F55", + "value": ["000000000"], + "data": b"", + } + with cairo_error(): + cairo_run("test__decode", data=list(encode(list(transaction.values())))) + @pytest.mark.parametrize("transaction", TRANSACTIONS) async def test_should_decode_all_transactions_types( self, cairo_run, transaction From 5ebf58d512509909d9e8c27c20726bd238bb8ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Walter?= Date: Thu, 25 Jul 2024 16:38:42 +0200 Subject: [PATCH 08/10] Add comment for pre EIP 155 --- src/utils/eth_transaction.cairo | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/eth_transaction.cairo b/src/utils/eth_transaction.cairo index 95ac50924..2ed8c34ec 100644 --- a/src/utils/eth_transaction.cairo +++ b/src/utils/eth_transaction.cairo @@ -35,6 +35,8 @@ namespace EthTransaction { let items_len = [items].data_len; let items = cast([items].data, RLP.Item*); + // Pre eip-155 txs have 6 fields, post eip-155 txs have 9 fields + // We check for both cases here, and do the remaining ones in the next if block assert items[0].is_list = FALSE; assert items[1].is_list = FALSE; assert items[2].is_list = FALSE; From 71eeb0479720ff7bb928863421db3781baccec33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Walter?= Date: Thu, 25 Jul 2024 17:08:46 +0200 Subject: [PATCH 09/10] Remove useless return value for decode --- src/utils/rlp.cairo | 4 ++-- tests/src/utils/test_rlp.cairo | 6 +++--- tests/src/utils/test_rlp.py | 10 ++++------ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/utils/rlp.cairo b/src/utils/rlp.cairo index d4708a7b5..e41944049 100644 --- a/src/utils/rlp.cairo +++ b/src/utils/rlp.cairo @@ -98,7 +98,7 @@ namespace RLP { return 1 + items_len; } - func decode{range_check_ptr}(items: Item*, data_len: felt, data: felt*) -> felt { + func decode{range_check_ptr}(items: Item*, data_len: felt, data: felt*) { alloc_locals; let (rlp_type, offset, len) = decode_type(data_len=data_len, data=data); local extra_bytes = data_len - offset - len; @@ -107,6 +107,6 @@ namespace RLP { } let items_len = decode_raw(items=items, data_len=data_len, data=data); assert items_len = 1; - return items_len; + return (); } } diff --git a/tests/src/utils/test_rlp.cairo b/tests/src/utils/test_rlp.cairo index 9761ce040..da12318f4 100644 --- a/tests/src/utils/test_rlp.cairo +++ b/tests/src/utils/test_rlp.cairo @@ -5,7 +5,7 @@ from starkware.cairo.common.cairo_builtins import HashBuiltin, BitwiseBuiltin from starkware.cairo.common.alloc import alloc from starkware.cairo.common.memcpy import memcpy -func test__decode{range_check_ptr}() -> (items_len: felt, items: RLP.Item*) { +func test__decode{range_check_ptr}() -> RLP.Item* { alloc_locals; // Given tempvar data_len: felt; @@ -17,9 +17,9 @@ func test__decode{range_check_ptr}() -> (items_len: felt, items: RLP.Item*) { // When let (local items: RLP.Item*) = alloc(); - let items_len = RLP.decode(items, data_len, data); + RLP.decode(items, data_len, data); - return (items_len, items); + return items; } func test__decode_type{range_check_ptr}() -> (felt, felt, felt) { diff --git a/tests/src/utils/test_rlp.py b/tests/src/utils/test_rlp.py index a31ff683d..35b78b821 100644 --- a/tests/src/utils/test_rlp.py +++ b/tests/src/utils/test_rlp.py @@ -33,9 +33,8 @@ async def test_should_match_decode_reference_implementation( ): encoded_data = encode(data) - items_len, items = cairo_run("test__decode", data=list(encoded_data)) - decoded = items[0] if items_len == 1 else items - assert decoded == decode(encoded_data) + items = cairo_run("test__decode", data=list(encoded_data)) + assert items[0] == decode(encoded_data) @given( data=recursive(binary(), lists), @@ -61,6 +60,5 @@ def test_should_decode_all_tx_types(self, cairo_run, transaction): else: rlp_encoding = encoded_unsigned_tx - items_len, items = cairo_run("test__decode", data=list(rlp_encoding)) - decoded = items[0] if items_len == 1 else items - assert decoded == decode(rlp_encoding) + items = cairo_run("test__decode", data=list(rlp_encoding)) + assert items[0] == decode(rlp_encoding) From b54f21a92fdb0e84a92bb66a931cf0de0992df23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Walter?= Date: Thu, 25 Jul 2024 17:16:47 +0200 Subject: [PATCH 10/10] Rename first items to payload --- src/utils/eth_transaction.cairo | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/utils/eth_transaction.cairo b/src/utils/eth_transaction.cairo index 2ed8c34ec..289bfd263 100644 --- a/src/utils/eth_transaction.cairo +++ b/src/utils/eth_transaction.cairo @@ -28,12 +28,12 @@ namespace EthTransaction { ) -> model.EthTransaction* { // see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md alloc_locals; - let (items: RLP.Item*) = alloc(); - RLP.decode(items, tx_data_len, tx_data); + let (tx_items: RLP.Item*) = alloc(); + RLP.decode(tx_items, tx_data_len, tx_data); - assert [items].is_list = TRUE; - let items_len = [items].data_len; - let items = cast([items].data, RLP.Item*); + assert [tx_items].is_list = TRUE; + let items_len = [tx_items].data_len; + let items = cast([tx_items].data, RLP.Item*); // Pre eip-155 txs have 6 fields, post eip-155 txs have 9 fields // We check for both cases here, and do the remaining ones in the next if block @@ -91,12 +91,12 @@ namespace EthTransaction { ) -> model.EthTransaction* { alloc_locals; - let (items: RLP.Item*) = alloc(); - RLP.decode(items, tx_data_len - 1, tx_data + 1); + let (tx_items: RLP.Item*) = alloc(); + RLP.decode(tx_items, tx_data_len - 1, tx_data + 1); - assert [items].is_list = TRUE; - let items_len = [items].data_len; - let items = cast([items].data, RLP.Item*); + assert [tx_items].is_list = TRUE; + let items_len = [tx_items].data_len; + let items = cast([tx_items].data, RLP.Item*); assert items_len = 8; assert items[0].is_list = FALSE; @@ -148,12 +148,12 @@ namespace EthTransaction { ) -> model.EthTransaction* { alloc_locals; - let (items: RLP.Item*) = alloc(); - RLP.decode(items, tx_data_len - 1, tx_data + 1); + let (tx_items: RLP.Item*) = alloc(); + RLP.decode(tx_items, tx_data_len - 1, tx_data + 1); - assert [items].is_list = TRUE; - let items_len = [items].data_len; - let items = cast([items].data, RLP.Item*); + assert [tx_items].is_list = TRUE; + let items_len = [tx_items].data_len; + let items = cast([tx_items].data, RLP.Item*); assert items_len = 9; assert items[0].is_list = FALSE;