From b0c62eec66fb1e8f113b80615e44266fb99861b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Walter?= Date: Tue, 27 Feb 2024 11:14:20 +0100 Subject: [PATCH] Add EVM serialization (#995) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Time spent on this PR: 0.5 ## Pull request type Please check the type of change your PR introduces: - [ ] Bugfix - [x] Feature - [ ] Code style update (formatting, renaming) - [ ] Refactoring (no functional changes, no api changes) - [ ] Build related changes - [ ] Documentation content changes - [ ] Other (please describe): ## What is the current behavior? Not possible to return `model.EVM` from a test Resolves #940 ## What is the new behavior? Serialize model.EVM + included struct + fix when returning struct and not struct* - - - This change is [Reviewable](https://reviewable.io/reviews/kkrt-labs/kakarot/995) --- .gitignore | 3 + .trunk/trunk.yaml | 1 + Makefile | 23 +- pyproject.toml | 1 + scripts/{ef-tests => ef_tests}/debug.py | 22 +- scripts/ef_tests/fetch.py | 52 ++++ .../{ef_tests.py => ef_tests/resources.py} | 0 scripts/utils/kakarot.py | 3 + tests/ef_tests/.gitignore | 1 - tests/ef_tests/conftest.py | 174 ------------- tests/ef_tests/test_data/.gitkeep | 0 tests/ef_tests/test_ef_blockchain_tests.py | 228 ------------------ tests/ef_tests/utils.py | 2 - tests/fixtures/starknet.py | 28 +-- .../accounts/test_contract_account.cairo | 11 +- .../kakarot/accounts/test_contract_account.py | 6 +- .../instructions/test_block_information.cairo | 7 +- .../instructions/test_block_information.py | 20 +- .../test_duplication_operations.cairo | 6 +- .../test_duplication_operations.py | 2 +- .../test_environmental_information.cairo | 19 +- .../test_environmental_information.py | 23 +- .../test_exchange_operations.cairo | 7 +- .../instructions/test_exchange_operations.py | 4 +- .../instructions/test_push_operations.cairo | 6 +- .../instructions/test_push_operations.py | 6 +- tests/src/kakarot/test_account.cairo | 12 +- tests/src/kakarot/test_account.py | 6 +- tests/src/kakarot/test_evm.cairo | 7 +- tests/src/kakarot/test_evm.py | 4 +- .../src/kakarot/test_execution_context.cairo | 11 +- tests/src/kakarot/test_execution_context.py | 8 +- tests/src/kakarot/test_gas.cairo | 21 +- tests/src/kakarot/test_gas.py | 8 +- tests/src/kakarot/test_kakarot.cairo | 54 +++++ tests/src/kakarot/test_kakarot.py | 153 ++++++++++++ tests/src/kakarot/test_state.py | 4 +- tests/utils/helpers.cairo | 2 +- tests/utils/helpers.py | 4 +- tests/utils/serde.py | 86 ++++++- tests/utils/syscall_handler.py | 132 +++++++++- 41 files changed, 581 insertions(+), 586 deletions(-) rename scripts/{ef-tests => ef_tests}/debug.py (91%) create mode 100644 scripts/ef_tests/fetch.py rename scripts/{ef_tests.py => ef_tests/resources.py} (100%) delete mode 100644 tests/ef_tests/.gitignore delete mode 100644 tests/ef_tests/conftest.py create mode 100644 tests/ef_tests/test_data/.gitkeep delete mode 100644 tests/ef_tests/test_ef_blockchain_tests.py delete mode 100644 tests/ef_tests/utils.py create mode 100644 tests/src/kakarot/test_kakarot.cairo create mode 100644 tests/src/kakarot/test_kakarot.py diff --git a/.gitignore b/.gitignore index bef42869a..882b9c22d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ resources/* *.pb.gz profile*.png logs + +tests/ef_tests/test_data +!tests/ef_tests/test_data/.gitkeep diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index cfe8e1f72..102c8fb21 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -59,6 +59,7 @@ lint: - logs* - lib* - resources* + - tests/ef_tests/test_data actions: disabled: - trunk-announce diff --git a/Makefile b/Makefile index 06e02afdd..b39420605 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,3 @@ -# The release tag of https://github.com/ethereum/tests to use for EF tests -EF_TESTS_TAG := v12.4 -EF_TESTS_URL := https://github.com/ethereum/tests/archive/refs/tags/$(EF_TESTS_TAG).tar.gz -EF_TESTS_DIR := ./tests/ef_tests/test_data - -# Downloads and unpacks Ethereum Foundation tests in the `$(EF_TESTS_DIR)` directory. -# Requires `wget` and `tar` -$(EF_TESTS_DIR): - mkdir -p $(EF_TESTS_DIR) - wget $(EF_TESTS_URL) -O ethereum-tests.tar.gz - tar -xzf ethereum-tests.tar.gz --strip-components=1 -C $(EF_TESTS_DIR) - rm ethereum-tests.tar.gz - - -.PHONY: build test coverage $(EF_TESTS_DIR) - # Include .env file to get GITHUB_TOKEN ifneq ("$(wildcard .env)","") include .env @@ -32,6 +16,9 @@ build: check check: poetry check --lock +fetch-ef-tests: + poetry run python ./scripts/ef_tests/fetch.py + # This action fetches the latest Kakarot SSJ (Cairo compiler version >=2) artifacts # from the main branch and unzips them into the build/ssj directory. # This is required because Kakarot Zero (Cairo Zero, compiler version <1) uses some SSJ Cairo programs. @@ -47,11 +34,11 @@ setup: fetch-ssj-artifacts poetry install test: build-sol deploy - poetry run pytest tests/src -m "not EFTests" --log-cli-level=INFO -n logical + poetry run pytest tests/src -m "not NoCI" --log-cli-level=INFO -n logical poetry run pytest tests/end_to_end test-unit: - poetry run pytest tests/src -n logical + poetry run pytest tests/src -m "not NoCI" -n logical test-end-to-end: build-sol deploy poetry run pytest tests/end_to_end diff --git a/pyproject.toml b/pyproject.toml index 0189c481d..490be878f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -144,6 +144,7 @@ markers = [ "EFTests", "SSTORE", "SLOAD", + "NoCI", ] env = [ "PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION = python", diff --git a/scripts/ef-tests/debug.py b/scripts/ef_tests/debug.py similarity index 91% rename from scripts/ef-tests/debug.py rename to scripts/ef_tests/debug.py index 41ffc0574..2273f792c 100644 --- a/scripts/ef-tests/debug.py +++ b/scripts/ef_tests/debug.py @@ -12,6 +12,8 @@ from eth.vm.forks.shanghai.blocks import ShanghaiBlock from web3 import Web3 +from scripts.ef_tests.fetch import EF_TESTS_PARSED_DIR + logging.basicConfig() logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) @@ -22,7 +24,7 @@ TEST_NAME = os.getenv("TEST_NAME") if TEST_NAME is None: raise ValueError("Please set TEST_NAME") -TEST_PARENT_FOLDER = os.getenv("TEST_PARENT_FOLDER") +TEST_PARENT_FOLDER = os.getenv("TEST_PARENT_FOLDER", "") RPC_ENDPOINT = "http://127.0.0.1:8545" @@ -59,29 +61,23 @@ def signal_handler(self, signum, _): def get_test_file(): tests = [ - (content, str(file_path)) - for file_path in TESTS_PATH.glob("**/*.json") - for name, content in json.load(open(file_path)).items() - if TEST_NAME in name and content["network"] == "Shanghai" + file_name + for file_name in os.listdir(EF_TESTS_PARSED_DIR) + if TEST_NAME in file_name and TEST_PARENT_FOLDER in file_name ] if len(tests) == 0: raise ValueError(f"Test {TEST_NAME} not found") if len(tests) > 1: - if TEST_PARENT_FOLDER is None: + if TEST_PARENT_FOLDER == "": raise ValueError( f"Test {TEST_NAME} is ambiguous, please set TEST_PARENT_FOLDER to test file folder" ) - test = [content for (content, path) in tests if TEST_PARENT_FOLDER in path] - if len(test) == 0: - raise ValueError(f"Test {TEST_NAME} not found") - - return test[0] + raise ValueError(f"Test {TEST_NAME} not found") - else: - return tests[0][0] + return json.loads((EF_TESTS_PARSED_DIR / tests[0]).read_text()) def connect_anvil(): diff --git a/scripts/ef_tests/fetch.py b/scripts/ef_tests/fetch.py new file mode 100644 index 000000000..acfc3501b --- /dev/null +++ b/scripts/ef_tests/fetch.py @@ -0,0 +1,52 @@ +import io +import json +import logging +import os +import shutil +import tarfile +from pathlib import Path + +import requests + +EF_TESTS_TAG = "v12.4" +EF_TESTS_URL = ( + f"https://github.com/ethereum/tests/archive/refs/tags/{EF_TESTS_TAG}.tar.gz" +) +EF_TESTS_DIR = Path("tests") / "ef_tests" / "test_data" / EF_TESTS_TAG +EF_TESTS_PARSED_DIR = Path("tests") / "ef_tests" / "test_data" / "parsed" + +DEFAULT_NETWORK = "Shanghai" + +logging.basicConfig() +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +def generate_tests(): + if not EF_TESTS_DIR.exists(): + response = requests.get(EF_TESTS_URL) + with tarfile.open(fileobj=io.BytesIO(response.content), mode="r:gz") as tar: + tar.extractall(EF_TESTS_DIR) + + test_cases = { + f"{os.path.basename(root)}_{name}": content + for (root, _, files) in os.walk(EF_TESTS_DIR) + for file in files + if file.endswith(".json") and "BlockchainTests/GeneralStateTests" in root + for name, content in json.loads((Path(root) / file).read_text()).items() + if content.get("network") == DEFAULT_NETWORK + } + + shutil.rmtree(EF_TESTS_PARSED_DIR, ignore_errors=True) + EF_TESTS_PARSED_DIR.mkdir(parents=True, exist_ok=True) + + for test_name, test_case in test_cases.items(): + json.dump( + test_case, + open(EF_TESTS_PARSED_DIR / f"{test_name}.json", "w"), + indent=4, + ) + + +if __name__ == "__main__": + generate_tests() diff --git a/scripts/ef_tests.py b/scripts/ef_tests/resources.py similarity index 100% rename from scripts/ef_tests.py rename to scripts/ef_tests/resources.py diff --git a/scripts/utils/kakarot.py b/scripts/utils/kakarot.py index 258c05333..9b2a70879 100644 --- a/scripts/utils/kakarot.py +++ b/scripts/utils/kakarot.py @@ -109,6 +109,9 @@ def get_contract( bytecode=target_compilation_output["bytecode"]["object"], ), ) + contract.bytecode_runtime = HexBytes( + target_compilation_output["deployedBytecode"]["object"] + ) try: for fun in contract.functions: diff --git a/tests/ef_tests/.gitignore b/tests/ef_tests/.gitignore deleted file mode 100644 index bb23d6779..000000000 --- a/tests/ef_tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -test_data diff --git a/tests/ef_tests/conftest.py b/tests/ef_tests/conftest.py deleted file mode 100644 index 672039a16..000000000 --- a/tests/ef_tests/conftest.py +++ /dev/null @@ -1,174 +0,0 @@ -import json -import logging -import os -from pathlib import Path - -import pytest -import pytest_asyncio -from starkware.starknet.core.os.contract_address.contract_address import ( - calculate_contract_address_from_hash, -) -from starkware.starknet.testing.contract import DeclaredClass, StarknetContract -from starkware.starknet.testing.starknet import Starknet - -# Root of the GeneralStateTest in BlockchainTest format -EF_GENERAL_STATE_TEST_ROOT_PATH = Path( - "./tests/ef_tests/test_data/BlockchainTests/GeneralStateTests/" -) - - -DEFAULT_NETWORK = "Shanghai" - -logging.basicConfig() -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) - - -def pytest_generate_tests(metafunc): - """ - Parametrizes `ef_blockchain_test` fixture with cases loaded from the - Ethereum Foundation tests repository, see: - https://github.com/kkrt-labs/kakarot/blob/main/.gitmodules#L7. - """ - if ( - "ef_blockchain_test" not in metafunc.fixturenames - or os.getenv("EF_TESTS") is None - ): - return - - if not EF_GENERAL_STATE_TEST_ROOT_PATH.exists(): - logger.warning( - "EFTests directory %s doesn't exist. Run `make setup`", - str(EF_GENERAL_STATE_TEST_ROOT_PATH), - ) - metafunc.parametrize("ef_blockchain_test", []) - return - - test_ids, test_cases = zip( - *[ - (name, content) - for (root, _, files) in os.walk(EF_GENERAL_STATE_TEST_ROOT_PATH) - for file in files - if file.endswith(".json") - for name, content in json.loads((Path(root) / file).read_text()).items() - if content["network"] == DEFAULT_NETWORK - ] - ) - - metafunc.parametrize( - "ef_blockchain_test", - test_cases, - ids=test_ids, - ) - - -@pytest_asyncio.fixture(scope="session") -async def kakarot( - starknet: Starknet, - eth: StarknetContract, - contract_account_class: DeclaredClass, - externally_owned_account_class: DeclaredClass, - account_proxy_class: DeclaredClass, -) -> StarknetContract: - owner = 1 - class_hash = await starknet.deprecated_declare( - source="./src/kakarot/kakarot.cairo", - cairo_path=["src"], - disable_hint_validation=True, - ) - kakarot = await starknet.deploy( - class_hash=class_hash.class_hash, - constructor_calldata=[ - owner, # owner - eth.contract_address, # native_token_address_ - contract_account_class.class_hash, # contract_account_class_hash_ - externally_owned_account_class.class_hash, # externally_owned_account_class_hash - account_proxy_class.class_hash, # account_proxy_class_hash - ], - ) - return kakarot - - -@pytest.fixture(scope="session") -def get_starknet_address(account_proxy_class, kakarot): - """ - Fixture to return the starknet address of a contract deployed by kakarot. - """ - - def _factory(evm_contract_address): - return calculate_contract_address_from_hash( - salt=evm_contract_address, - class_hash=account_proxy_class.class_hash, - constructor_calldata=[], - deployer_address=kakarot.contract_address, - ) - - return _factory - - -@pytest_asyncio.fixture(scope="session") -async def eth(starknet: Starknet): - class_hash = await starknet.deprecated_declare( - source="./tests/fixtures/ERC20.cairo" - ) - return await starknet.deploy( - class_hash=class_hash.class_hash, - constructor_calldata=[ - int.from_bytes(b"Ether", "big"), # name - int.from_bytes(b"ETH", "big"), # symbol - 18, # decimals - ], - ) - - -@pytest.fixture() -def starknet_snapshot(starknet): - """ - Use this fixture to snapshot the starknet state before each test and reset it at teardown. - """ - initial_state = starknet.state.copy() - - yield - - initial_cache_state = initial_state.state._copy() - starknet.state.state = initial_cache_state - - -@pytest_asyncio.fixture(scope="session") -async def contract_account_class(starknet: Starknet) -> DeclaredClass: - return await starknet.deprecated_declare( - source="./src/kakarot/accounts/contract/contract_account.cairo", - cairo_path=["src"], - disable_hint_validation=True, - ) - - -@pytest_asyncio.fixture(scope="session") -async def externally_owned_account_class(starknet: Starknet): - return await starknet.deprecated_declare( - source="src/kakarot/accounts/eoa/externally_owned_account.cairo", - cairo_path=["src"], - disable_hint_validation=True, - ) - - -@pytest_asyncio.fixture(scope="session") -async def account_proxy_class(starknet: Starknet): - return await starknet.deprecated_declare( - source="src/kakarot/accounts/proxy/proxy.cairo", - cairo_path=["src"], - disable_hint_validation=True, - ) - - -@pytest_asyncio.fixture(scope="session") -def get_contract_account(starknet, contract_account_class): - def _factory(starknet_address): - return StarknetContract( - starknet.state, - contract_account_class.abi, - starknet_address, - None, - ) - - return _factory diff --git a/tests/ef_tests/test_data/.gitkeep b/tests/ef_tests/test_data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/ef_tests/test_ef_blockchain_tests.py b/tests/ef_tests/test_ef_blockchain_tests.py deleted file mode 100644 index 548454654..000000000 --- a/tests/ef_tests/test_ef_blockchain_tests.py +++ /dev/null @@ -1,228 +0,0 @@ -import pytest -from starkware.starknet.public.abi import get_storage_var_address -from starkware.starknet.testing.contract import DeclaredClass, StarknetContract - -from tests.ef_tests.utils import is_account_eoa -from tests.utils.constants import MAX_INT -from tests.utils.helpers import hex_string_to_bytes_array -from tests.utils.uint256 import int_to_uint256, uint256_to_int - - -@pytest.mark.usefixtures("starknet_snapshot") -@pytest.mark.EFTests -class TestEFBlockchain: - @staticmethod - async def setup_test_state( - account_proxy_class: DeclaredClass, - contract_account_class: DeclaredClass, - externally_owned_account_class: DeclaredClass, - get_contract_account, - get_starknet_address, - eth: StarknetContract, - kakarot: StarknetContract, - starknet: StarknetContract, - pre_state, - ): - """ - Initialize the Starknet state using an ef-test BlockchainTest format case 'pre' field. - - See: https://ethereum-tests.readthedocs.io/en/latest/test_filler/blockchain_filler.html#pre for details. - """ - - for address, account in pre_state.items(): - evm_address = int(address, 16) - starknet_address = get_starknet_address(evm_address) - - await starknet.state.state.set_class_hash_at( - contract_address=starknet_address, - class_hash=account_proxy_class.class_hash, - ) - - storage_entries = ( - (("is_initialized",), 1), - (("evm_address",), evm_address), - ) + ( - ( - (("kakarot_address",), kakarot.contract_address), - ( - ("_implementation",), - externally_owned_account_class.class_hash, - ), - ) - if is_account_eoa(account) - else ( - (("Ownable_owner",), kakarot.contract_address), - (("_implementation",), contract_account_class.class_hash), - (("nonce",), int(account["nonce"], 16)), - ) - ) - - for storage_var, value in storage_entries: - await starknet.state.state.set_storage_at( - contract_address=starknet_address, - key=get_storage_var_address(*storage_var), - value=value, - ) - - # Store the evm_to_starknet_address mapping inside Kakarot storage - await starknet.state.state.set_storage_at( - contract_address=kakarot.contract_address, - key=get_storage_var_address("evm_to_starknet_address", evm_address), - value=starknet_address, - ) - - # an EOA's nonce is managed at the starknet level - if is_account_eoa(account): - starknet.state.state.cache._nonce_writes[starknet_address] = int( - account["nonce"], 16 - ) - - balance = int_to_uint256(int(account["balance"], 16)) - - await eth.mint(starknet_address, balance).execute() - # In regular kakarot deployment, this is handled on construction. - # We do this manually in order to be able to do tranfsers from our - # manually deployed accounts. - await eth.approve( - kakarot.contract_address, int_to_uint256(MAX_INT) - ).execute(caller_address=starknet_address) - - if not is_account_eoa(account): - # Write bytecode of contract - contract = get_contract_account(starknet_address) - await contract.write_bytecode( - hex_string_to_bytes_array(account["code"]) - ).execute(caller_address=kakarot.contract_address) - - # Write storage at correct values - # Get keys and values to store - storage_keys = list(account["storage"].keys()) - storage_values = list(account["storage"].values()) - - # Split the integers into two felts - # Mask to extract the lower u128 part - mask_u128 = (1 << 128) - 1 - - # Add an invariant: There must be as many storage keys as storage values - assert len(storage_keys) == len(storage_values) - - for index in range(len(storage_keys)): - # Key for storage is u256, it should be split into {high: u128, low: u128} - key_high = int(storage_keys[index], 16) >> 128 - key_low = int(storage_keys[index], 16) & mask_u128 - - # Value for storage is u256, it should be split into {high: u128, low: u128} - value_high = int(storage_values[index], 16) >> 128 - value_low = int(storage_values[index], 16) & mask_u128 - - # Now we have to perform low level custom storage for Starknet - # Reference: https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/contract-storage/ - - # Note that when serializing, we need to store low first and then high: - # {low: u128, high: 128} => serde => [low, high] - # The address for storage is pedersen(, key_1, key_2) - # Here: pedersen("storage_", high, low) - await starknet.state.state.set_storage_at( - contract_address=starknet_address, - key=get_storage_var_address("storage_", key_low, key_high), - value=value_low, - ) - - # Since we need to store both a high and low element, the second value to store is simply located - # At the address calculated above + 1 - # We're able to store 256 felts this way in a custom Struct. - # Above, we'll need to compute a new address to prevent collision - await starknet.state.state.set_storage_at( - contract_address=starknet_address, - key=get_storage_var_address("storage_", key_low, key_high) + 1, - value=value_high, - ) - - @staticmethod - async def assert_post_state( - get_contract_account, - get_starknet_address, - starknet: StarknetContract, - expected_post_state, - ): - for address, account in expected_post_state.items(): - evm_address = int(address, 16) - starknet_address = get_starknet_address(evm_address) - contract = get_contract_account(starknet_address) - - actual_nonce = ( - # For EOA's, nonces are mapped to system level nonce. - await starknet.state.state.get_nonce_at(starknet_address) - if is_account_eoa(account) - # For evm contracts, nonces are managed by Kakarot as contract state. - else await starknet.state.state.get_storage_at( - starknet_address, get_storage_var_address("nonce") - ) - ) - - expected_nonce = int(account["nonce"], 16) - - assert ( - actual_nonce == expected_nonce - ), f"Contract {address=}: {expected_nonce=} is not {actual_nonce=}" - - for key, expected_storage in account["storage"].items(): - key = int_to_uint256(int(key, 16)) - expected_storage = int_to_uint256(int(expected_storage, 16)) - actual_storage = (await contract.storage(key).call()).result.value - - assert ( - actual_storage == expected_storage - ), f"Contract {address}: expected storage=0x{uint256_to_int(*expected_storage):x} is not actual_storage=0x{uint256_to_int(*actual_storage):x}" - - async def test_case( - self, - account_proxy_class: DeclaredClass, - contract_account_class: DeclaredClass, - externally_owned_account_class: DeclaredClass, - get_contract_account, - get_starknet_address, - eth: StarknetContract, - kakarot: StarknetContract, - starknet: StarknetContract, - ef_blockchain_test, - ): - """ - Run a single test case based on the Ethereum Foundation Blockchain test format data. - - See https://ethereum-tests.readthedocs.io/en/latest/blockchain-ref.html - """ - await self.setup_test_state( - account_proxy_class, - contract_account_class, - externally_owned_account_class, - get_contract_account, - get_starknet_address, - eth, - kakarot, - starknet, - ef_blockchain_test["pre"], - ) - - # See: https://ethereum-tests.readthedocs.io/en/latest/test_filler/blockchain_filler.html#blocks for details. - for block in ef_blockchain_test["blocks"]: - for transaction in block["transactions"]: - starknet_address = get_starknet_address(int(transaction["sender"], 16)) - - await kakarot.eth_send_transaction( - to=int(transaction["to"] or "0", 16), - gas_limit=int(transaction["gasLimit"], 16), - gas_price=int(transaction["gasPrice"], 16), - value=int(transaction["value"], 16), - data=hex_string_to_bytes_array(transaction["data"]), - ).execute(caller_address=starknet_address) - - # do we really have to do this manually? (yes) - await starknet.state.state.increment_nonce(starknet_address) - - await self.assert_post_state( - get_contract_account, - get_starknet_address, - starknet, - ef_blockchain_test["postState"], - ) diff --git a/tests/ef_tests/utils.py b/tests/ef_tests/utils.py deleted file mode 100644 index a3b313867..000000000 --- a/tests/ef_tests/utils.py +++ /dev/null @@ -1,2 +0,0 @@ -def is_account_eoa(account: dict) -> bool: - return account.get("code") in [None, "0x"] and not account.get("storage") diff --git a/tests/fixtures/starknet.py b/tests/fixtures/starknet.py index ced11d377..07f18feff 100644 --- a/tests/fixtures/starknet.py +++ b/tests/fixtures/starknet.py @@ -191,8 +191,8 @@ def _factory(entrypoint, **kwargs) -> list: ).members.keys() ) if add_output: - output = runner.segments.add() - stack.append(output) + output_ptr = runner.segments.add() + stack.append(output_ptr) return_fp = runner.segments.add() end = runner.segments.add() @@ -245,18 +245,18 @@ def _factory(entrypoint, **kwargs) -> list: ) final_output = None if add_output: - final_output = serde.serialize_list(output) - if returned_data is not None: - function_output = serde.serialize(returned_data.cairo_type) - if final_output is not None: - function_output = ( - function_output - if isinstance(function_output, list) - else [function_output] - ) - final_output += function_output - else: - final_output = function_output + final_output = serde.serialize_list(output_ptr) + function_output = serde.serialize(returned_data.cairo_type) + if final_output is not None: + function_output = ( + function_output + if isinstance(function_output, list) + else [function_output] + ) + if len(function_output) > 0: + final_output = (final_output, *function_output) + else: + final_output = function_output return final_output diff --git a/tests/src/kakarot/accounts/test_contract_account.cairo b/tests/src/kakarot/accounts/test_contract_account.cairo index af7d92c86..1686b2aca 100644 --- a/tests/src/kakarot/accounts/test_contract_account.cairo +++ b/tests/src/kakarot/accounts/test_contract_account.cairo @@ -27,12 +27,10 @@ func test__initialize__should_store_given_evm_address{ func test__get_evm_address__should_return_stored_address{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* -}(output_ptr: felt*) { +}() -> felt { let (evm_address) = ContractAccount.get_evm_address(); - assert [output_ptr] = evm_address; - - return (); + return evm_address; } func test__write_bytecode{ @@ -55,9 +53,8 @@ func test__write_bytecode{ func test__read_bytecode{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* -}(output_ptr: felt*) { +}() -> (bytecode_len: felt, bytecode: felt*) { alloc_locals; let (bytecode_len, bytecode) = ContractAccount.bytecode(); - memcpy(output_ptr, bytecode, bytecode_len); - return (); + return (bytecode_len, bytecode); } diff --git a/tests/src/kakarot/accounts/test_contract_account.py b/tests/src/kakarot/accounts/test_contract_account.py index 6992608c3..e55ae293c 100644 --- a/tests/src/kakarot/accounts/test_contract_account.py +++ b/tests/src/kakarot/accounts/test_contract_account.py @@ -74,7 +74,7 @@ class TestGetEvmAddress: @SyscallHandler.patch("evm_address", 0xABDE1) def test_should_return_stored_address(self, cairo_run): output = cairo_run("test__get_evm_address__should_return_stored_address") - assert output == [0xABDE1] + assert output == 0xABDE1 class TestWriteBytecode: @SyscallHandler.patch("Ownable_owner", 0xDEAD) @@ -112,9 +112,9 @@ def test_should_read_bytecode(self, cairo_run, bytecode, storage): with patch.object( SyscallHandler, "mock_storage", side_effect=storage ) as mock_storage: - output = cairo_run("test__read_bytecode") + output_len, output = cairo_run("test__read_bytecode") chunk_counts, remainder = divmod(len(bytecode), 31) addresses = list(range(chunk_counts + (remainder > 0))) calls = [call(address=address) for address in addresses] mock_storage.assert_has_calls(calls) - assert output == list(bytecode) + assert output[:output_len] == list(bytecode) diff --git a/tests/src/kakarot/instructions/test_block_information.cairo b/tests/src/kakarot/instructions/test_block_information.cairo index fb9fdd150..79bd218b0 100644 --- a/tests/src/kakarot/instructions/test_block_information.cairo +++ b/tests/src/kakarot/instructions/test_block_information.cairo @@ -2,6 +2,7 @@ from starkware.cairo.common.alloc import alloc from starkware.cairo.common.cairo_builtins import HashBuiltin, BitwiseBuiltin +from starkware.cairo.common.uint256 import Uint256 from kakarot.model import model from kakarot.stack import Stack @@ -12,7 +13,7 @@ from tests.utils.helpers import TestHelpers func test__exec_block_information{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* -}(output_ptr: felt*) { +}() -> Uint256* { // Given alloc_locals; tempvar opcode: felt; @@ -32,7 +33,5 @@ func test__exec_block_information{ } // Then - assert [output_ptr] = result.low; - assert [output_ptr + 1] = result.high; - return (); + return result; } diff --git a/tests/src/kakarot/instructions/test_block_information.py b/tests/src/kakarot/instructions/test_block_information.py index d41fa8941..65d2b927c 100644 --- a/tests/src/kakarot/instructions/test_block_information.py +++ b/tests/src/kakarot/instructions/test_block_information.py @@ -1,8 +1,9 @@ +from unittest.mock import patch + import pytest from tests.utils.constants import BLOCK_GAS_LIMIT, CHAIN_ID, Opcodes from tests.utils.syscall_handler import SyscallHandler -from tests.utils.uint256 import int_to_uint256 class TestBlockInformation: @@ -11,17 +12,18 @@ class TestBlockInformation: [ ( Opcodes.COINBASE, - int_to_uint256(0xCA40796AFB5472ABAED28907D5ED6FC74C04954A), + 0xCA40796AFB5472ABAED28907D5ED6FC74C04954A, ), - (Opcodes.TIMESTAMP, [SyscallHandler.block_timestamp, 0]), - (Opcodes.NUMBER, [SyscallHandler.block_number, 0]), - (Opcodes.PREVRANDAO, [0, 0]), - (Opcodes.GASLIMIT, [BLOCK_GAS_LIMIT, 0]), - (Opcodes.CHAINID, [CHAIN_ID, 0]), - (Opcodes.BASEFEE, [0, 0]), + (Opcodes.TIMESTAMP, 0x1234), + (Opcodes.NUMBER, SyscallHandler.block_number), + (Opcodes.PREVRANDAO, 0), + (Opcodes.GASLIMIT, BLOCK_GAS_LIMIT), + (Opcodes.CHAINID, CHAIN_ID), + (Opcodes.BASEFEE, 0), ], ) @SyscallHandler.patch("coinbase", 0xCA40796AFB5472ABAED28907D5ED6FC74C04954A) + @patch.object(SyscallHandler, "block_timestamp", 0x1234) def test__exec_block_information(self, cairo_run, opcode, expected_result): output = cairo_run("test__exec_block_information", opcode=opcode) - assert output == list(expected_result) + assert output == hex(expected_result) diff --git a/tests/src/kakarot/instructions/test_duplication_operations.cairo b/tests/src/kakarot/instructions/test_duplication_operations.cairo index 655fbf678..f206aee12 100644 --- a/tests/src/kakarot/instructions/test_duplication_operations.cairo +++ b/tests/src/kakarot/instructions/test_duplication_operations.cairo @@ -14,7 +14,7 @@ from tests.utils.helpers import TestHelpers func test__exec_dup{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* -}(output_ptr: felt*) { +}() -> model.Stack* { alloc_locals; local i: felt; local initial_stack_len: felt; @@ -39,7 +39,5 @@ func test__exec_dup{ let (top) = Stack.peek(0); } - assert [output_ptr] = top.low; - assert [output_ptr + 1] = top.high; - return (); + return stack; } diff --git a/tests/src/kakarot/instructions/test_duplication_operations.py b/tests/src/kakarot/instructions/test_duplication_operations.py index e477b1362..244818ed8 100644 --- a/tests/src/kakarot/instructions/test_duplication_operations.py +++ b/tests/src/kakarot/instructions/test_duplication_operations.py @@ -6,4 +6,4 @@ class TestDupOperations: def test__exec_dup(self, cairo_run, i): stack = [[v, 0] for v in range(16)] output = cairo_run("test__exec_dup", initial_stack=stack, i=i) - assert output == stack[i - 1] + assert output == [hex(i[0]) for i in stack][::-1] + [hex(stack[i - 1][0])] diff --git a/tests/src/kakarot/instructions/test_environmental_information.cairo b/tests/src/kakarot/instructions/test_environmental_information.cairo index bda1753ee..c19b2b4e5 100644 --- a/tests/src/kakarot/instructions/test_environmental_information.cairo +++ b/tests/src/kakarot/instructions/test_environmental_information.cairo @@ -42,7 +42,7 @@ func test__exec_address__should_push_address_to_stack{ func test__exec_extcodesize{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* -}(output_ptr: felt*) { +}() -> Uint256* { // Given alloc_locals; local address: felt; @@ -60,15 +60,12 @@ func test__exec_extcodesize{ } // Then - assert [output_ptr] = extcodesize.low; - assert [output_ptr + 1] = extcodesize.high; - - return (); + return extcodesize; } func test__exec_extcodecopy{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* -}(output_ptr: felt*) { +}() -> model.Memory* { // Given alloc_locals; local address: felt; @@ -97,12 +94,11 @@ func test__exec_extcodecopy{ Stack.push(item_1); // dest_offset Stack.push(item_0); // address let evm = EnvironmentalInformation.exec_extcodecopy(evm); - Memory.load_n(size, output_ptr, dest_offset); } // Then assert stack.size = 0; - return (); + return memory; } func test__exec_gasprice{ @@ -130,7 +126,7 @@ func test__exec_gasprice{ func test__exec_extcodehash{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* -}(output_ptr: felt*) { +}() -> Uint256* { // Given alloc_locals; local address: felt; @@ -148,8 +144,5 @@ func test__exec_extcodehash{ } // Then - assert [output_ptr] = extcodehash.low; - assert [output_ptr + 1] = extcodehash.high; - - return (); + return extcodehash; } diff --git a/tests/src/kakarot/instructions/test_environmental_information.py b/tests/src/kakarot/instructions/test_environmental_information.py index deddb0960..bc6837d05 100644 --- a/tests/src/kakarot/instructions/test_environmental_information.py +++ b/tests/src/kakarot/instructions/test_environmental_information.py @@ -49,8 +49,7 @@ def test_extcodesize_should_push_code_size(self, cairo_run, bytecode, address): ): output = cairo_run("test__exec_extcodesize", address=address) - assert output[0] == (len(bytecode) if address == EXISTING_ACCOUNT else 0) - assert output[1] == 0 + assert output == hex(len(bytecode) if address == EXISTING_ACCOUNT else 0) class TestExtCodeCopy: @pytest.mark.parametrize( @@ -92,7 +91,7 @@ def test_extcodecopy_should_copy_code(self, cairo_run, case, bytecode, address): with SyscallHandler.patch( "IAccount.bytecode", lambda addr, data: [len(bytecode), *bytecode] ): - output = cairo_run( + memory = cairo_run( "test__exec_extcodecopy", size=size, offset=offset, @@ -100,13 +99,15 @@ def test_extcodecopy_should_copy_code(self, cairo_run, case, bytecode, address): address=address, ) - expected = ( - (bytecode + [0] * (offset + size))[offset : (offset + size)] - if address == EXISTING_ACCOUNT - else [0] * size + deployed_bytecode = bytecode if address == EXISTING_ACCOUNT else [] + copied_bytecode = bytes( + # bytecode is padded with surely enough 0 and then sliced + (deployed_bytecode + [0] * (offset + size))[offset : offset + size] + ) + assert ( + bytes.fromhex(memory)[dest_offset : dest_offset + size] + == copied_bytecode ) - - assert output == expected class TestGasPrice: def test_gasprice(self, cairo_run): @@ -128,7 +129,5 @@ def test_extcodehash__should_push_hash( output = cairo_run("test__exec_extcodehash", address=address) assert output == ( - [bytecode_hash % (2**128), bytecode_hash >> 128] - if address == EXISTING_ACCOUNT - else [0, 0] + hex(bytecode_hash) if address == EXISTING_ACCOUNT else "0x0" ) diff --git a/tests/src/kakarot/instructions/test_exchange_operations.cairo b/tests/src/kakarot/instructions/test_exchange_operations.cairo index 5162dbdb5..77bdf73c1 100644 --- a/tests/src/kakarot/instructions/test_exchange_operations.cairo +++ b/tests/src/kakarot/instructions/test_exchange_operations.cairo @@ -10,12 +10,13 @@ from starkware.cairo.common.memcpy import memcpy from kakarot.stack import Stack from kakarot.memory import Memory from kakarot.state import State +from kakarot.model import model from kakarot.instructions.exchange_operations import ExchangeOperations from tests.utils.helpers import TestHelpers func test__exec_swap{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* -}(output_ptr: felt*) { +}() -> model.Stack* { alloc_locals; local i: felt; @@ -43,7 +44,5 @@ func test__exec_swap{ let (swapped) = Stack.peek(i); } - memcpy(output_ptr, cast(top, felt*), 2); - memcpy(output_ptr + 2, cast(swapped, felt*), 2); - return (); + return stack; } diff --git a/tests/src/kakarot/instructions/test_exchange_operations.py b/tests/src/kakarot/instructions/test_exchange_operations.py index 95995ae4d..a07bfe122 100644 --- a/tests/src/kakarot/instructions/test_exchange_operations.py +++ b/tests/src/kakarot/instructions/test_exchange_operations.py @@ -6,5 +6,5 @@ class TestSwapOperations: def test__exec_swap(self, cairo_run, i): stack = [[v, 0] for v in range(17)] output = cairo_run("test__exec_swap", i=i, initial_stack=stack) - assert output[:2] == stack[i] - assert output[2:] == stack[0] + stack[i], stack[0] = stack[0], stack[i] + assert output == [hex(i[0]) for i in stack][::-1] diff --git a/tests/src/kakarot/instructions/test_push_operations.cairo b/tests/src/kakarot/instructions/test_push_operations.cairo index e7ff3df62..47979abcb 100644 --- a/tests/src/kakarot/instructions/test_push_operations.cairo +++ b/tests/src/kakarot/instructions/test_push_operations.cairo @@ -16,7 +16,7 @@ from tests.utils.helpers import TestHelpers func test__exec_push{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* -}(output_ptr: felt*) { +}() -> model.Stack* { alloc_locals; local i: felt; %{ ids.i = program_input["i"] %} @@ -31,9 +31,7 @@ func test__exec_push{ with stack, memory, state { let evm = PushOperations.exec_push(evm); - let (result) = Stack.peek(0); } - memcpy(output_ptr, cast(result, felt*), 2); - return (); + return stack; } diff --git a/tests/src/kakarot/instructions/test_push_operations.py b/tests/src/kakarot/instructions/test_push_operations.py index b46b4b63c..6247fb388 100644 --- a/tests/src/kakarot/instructions/test_push_operations.py +++ b/tests/src/kakarot/instructions/test_push_operations.py @@ -1,7 +1,5 @@ import pytest -from tests.utils.uint256 import int_to_uint256 - @pytest.mark.asyncio class TestPushOperations: @@ -12,5 +10,5 @@ class TestPushOperations: # which forms the basis of our assertion in this test. @pytest.mark.parametrize("i", range(0, 33)) async def test__exec_push(self, cairo_run, i): - output = cairo_run("test__exec_push", i=i) - assert tuple(output) == int_to_uint256(256**i - 1) + stack = cairo_run("test__exec_push", i=i) + assert stack == [hex(256**i - 1)] diff --git a/tests/src/kakarot/test_account.cairo b/tests/src/kakarot/test_account.cairo index 40616617d..f31e79894 100644 --- a/tests/src/kakarot/test_account.cairo +++ b/tests/src/kakarot/test_account.cairo @@ -139,7 +139,7 @@ func test__write_storage__should_store_value_at_key{ func test__fetch_original_storage__state_modified{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr -}(output_ptr: felt*) { +}() -> Uint256 { alloc_locals; // Given let (key_ptr) = alloc(); @@ -166,14 +166,11 @@ func test__fetch_original_storage__state_modified{ // Then let original_storage = Account.fetch_original_storage(account, key); - assert [output_ptr] = original_storage.low; - assert [output_ptr + 1] = original_storage.high; - return (); + return original_storage; } func test__has_code_or_nonce{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( - output_ptr: felt* -) { + ) -> felt { alloc_locals; // Given local code_len: felt; @@ -194,6 +191,5 @@ func test__has_code_or_nonce{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, ran let result = Account.has_code_or_nonce(account); // Then - assert [output_ptr] = result; - return (); + return result; } diff --git a/tests/src/kakarot/test_account.py b/tests/src/kakarot/test_account.py index 6ce0e4094..0b5ba9ae4 100644 --- a/tests/src/kakarot/test_account.py +++ b/tests/src/kakarot/test_account.py @@ -62,7 +62,7 @@ def test_should_return_original_storage_when_state_modified( key=int_to_uint256(key), value=int_to_uint256(value), ) - assert output == [0x1337, 0] + assert output == "0x1337" @SyscallHandler.patch( "evm_to_starknet_address", @@ -78,7 +78,7 @@ def test_should_return_zero_account_not_registered(self, cairo_run, key, value): key=int_to_uint256(key), value=int_to_uint256(value), ) - assert output == [0, 0] + assert output == "0x0" class TestHasCodeOrNonce: @pytest.mark.parametrize( @@ -94,4 +94,4 @@ def test_should_return_true_when_nonce( self, cairo_run, nonce, code, expected_result ): output = cairo_run("test__has_code_or_nonce", nonce=nonce, code=code) - assert output[0] == expected_result + assert output == expected_result diff --git a/tests/src/kakarot/test_evm.cairo b/tests/src/kakarot/test_evm.cairo index 21ad5862a..51177df71 100644 --- a/tests/src/kakarot/test_evm.cairo +++ b/tests/src/kakarot/test_evm.cairo @@ -7,11 +7,12 @@ from kakarot.stack import Stack from kakarot.interpreter import Interpreter from kakarot.memory import Memory from kakarot.state import State +from kakarot.model import model from tests.utils.helpers import TestHelpers func test__unknown_opcode{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* -}(output_ptr: felt*) { +}() -> model.EVM* { alloc_locals; let evm = TestHelpers.init_evm(); let stack = Stack.init(); @@ -22,7 +23,5 @@ func test__unknown_opcode{ let evm = Interpreter.unknown_opcode(evm); } - memcpy(output_ptr, evm.return_data, evm.return_data_len); - - return (); + return evm; } diff --git a/tests/src/kakarot/test_evm.py b/tests/src/kakarot/test_evm.py index 1495dbcc9..7aacfc3b5 100644 --- a/tests/src/kakarot/test_evm.py +++ b/tests/src/kakarot/test_evm.py @@ -1,4 +1,4 @@ class TestInstructions: def test__unknown_opcode(self, cairo_run): - output = cairo_run("test__unknown_opcode") - assert output == list(b"Kakarot: UnknownOpcode") + evm = cairo_run("test__unknown_opcode") + assert evm["return_data"] == list(b"Kakarot: UnknownOpcode") diff --git a/tests/src/kakarot/test_execution_context.cairo b/tests/src/kakarot/test_execution_context.cairo index 1edd867ce..c71313cd9 100644 --- a/tests/src/kakarot/test_execution_context.cairo +++ b/tests/src/kakarot/test_execution_context.cairo @@ -4,12 +4,11 @@ from starkware.cairo.common.cairo_builtins import HashBuiltin from starkware.cairo.common.alloc import alloc from starkware.cairo.common.memcpy import memcpy +from kakarot.model import model from kakarot.evm import EVM from tests.utils.helpers import TestHelpers -func test__jump{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( - output_ptr: felt* -) { +func test__jump{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> model.EVM* { alloc_locals; local bytecode_len: felt; let (bytecode) = alloc(); @@ -22,9 +21,5 @@ func test__jump{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr} let evm = TestHelpers.init_evm_with_bytecode(bytecode_len, bytecode); let evm = EVM.jump(evm, jumpdest); - assert [output_ptr] = evm.program_counter; - assert [output_ptr + 1] = evm.return_data_len; - memcpy(output_ptr + 2, evm.return_data, evm.return_data_len); - - return (); + return evm; } diff --git a/tests/src/kakarot/test_execution_context.py b/tests/src/kakarot/test_execution_context.py index e007f3e38..cfc8a4f3d 100644 --- a/tests/src/kakarot/test_execution_context.py +++ b/tests/src/kakarot/test_execution_context.py @@ -18,8 +18,6 @@ class TestExecutionContext: ], ) def test_jump(self, cairo_run, bytecode, jumpdest, new_pc, expected_return_data): - pc, _, *return_data = cairo_run( - "test__jump", bytecode=bytecode, jumpdest=jumpdest - ) - assert pc == new_pc - assert return_data == expected_return_data + evm = cairo_run("test__jump", bytecode=bytecode, jumpdest=jumpdest) + assert evm["program_counter"] == new_pc + assert evm["return_data"] == expected_return_data diff --git a/tests/src/kakarot/test_gas.cairo b/tests/src/kakarot/test_gas.cairo index 1bcc40a15..b15023513 100644 --- a/tests/src/kakarot/test_gas.cairo +++ b/tests/src/kakarot/test_gas.cairo @@ -4,17 +4,15 @@ from kakarot.gas import Gas from starkware.cairo.common.uint256 import Uint256 from starkware.cairo.lang.compiler.lib.registers import get_fp_and_pc -func test__memory_cost{range_check_ptr}(output_ptr: felt*) { +func test__memory_cost{range_check_ptr}() -> felt { tempvar words_len: felt; %{ ids.words_len = program_input["words_len"]; %} let cost = Gas.memory_cost(words_len); - assert [output_ptr] = cost; - - return (); + return cost; } -func test__memory_expansion_cost{range_check_ptr}(output_ptr: felt*) { +func test__memory_expansion_cost{range_check_ptr}() -> felt { tempvar words_len: felt; tempvar max_offset: felt; %{ @@ -23,11 +21,10 @@ func test__memory_expansion_cost{range_check_ptr}(output_ptr: felt*) { %} let cost = Gas.calculate_gas_extend_memory(words_len, max_offset); - assert [output_ptr] = cost; - return (); + return cost; } -func test__max_memory_expansion_cost{range_check_ptr}(output_ptr: felt*) { +func test__max_memory_expansion_cost{range_check_ptr}() -> felt { alloc_locals; let fp_and_pc = get_fp_and_pc(); local __fp__: felt* = fp_and_pc.fp_val; @@ -49,11 +46,10 @@ func test__max_memory_expansion_cost{range_check_ptr}(output_ptr: felt*) { %} let cost = Gas.max_memory_expansion_cost(words_len, &offset_1, &size_1, &offset_2, &size_2); - assert [output_ptr] = cost; - return (); + return cost; } -func test__compute_message_call_gas{range_check_ptr}(output_ptr: felt*) { +func test__compute_message_call_gas{range_check_ptr}() -> felt { tempvar gas_param: Uint256; tempvar gas_left: felt; %{ @@ -63,6 +59,5 @@ func test__compute_message_call_gas{range_check_ptr}(output_ptr: felt*) { %} let gas = Gas.compute_message_call_gas(gas_param, gas_left); - assert [output_ptr] = gas; - return (); + return gas; } diff --git a/tests/src/kakarot/test_gas.py b/tests/src/kakarot/test_gas.py index 75660c364..b35324d00 100644 --- a/tests/src/kakarot/test_gas.py +++ b/tests/src/kakarot/test_gas.py @@ -17,7 +17,7 @@ class TestCost: ) def test_should_return_same_as_execution_specs(self, cairo_run, max_offset): output = cairo_run("test__memory_cost", words_len=((max_offset + 31) // 32)) - assert calculate_memory_gas_cost(max_offset) == output[0] + assert calculate_memory_gas_cost(max_offset) == output @pytest.mark.parametrize( "bytes_len", [random.randint(0, 0xFFFFFF) for _ in range(5)] @@ -39,7 +39,7 @@ def test_should_return_correct_expansion_cost( words_len=words_len, max_offset=max_offset, ) - assert diff == output[0] + assert diff == output @pytest.mark.parametrize( "offset_1", [random.randint(0, 0xFFFFF) for _ in range(3)] @@ -65,7 +65,7 @@ def test_should_return_max_expansion_cost( size_2=size_2, ) assert ( - output[0] + output == calculate_gas_extend_memory( b"", [ @@ -91,4 +91,4 @@ def test_should_return_message_base_gas( output = cairo_run( "test__compute_message_call_gas", gas_param=gas_param, gas_left=gas_left ) - assert output[0] == expected + assert output == expected diff --git a/tests/src/kakarot/test_kakarot.cairo b/tests/src/kakarot/test_kakarot.cairo new file mode 100644 index 000000000..92ecc3072 --- /dev/null +++ b/tests/src/kakarot/test_kakarot.cairo @@ -0,0 +1,54 @@ +%lang starknet + +from starkware.cairo.common.cairo_builtins import HashBuiltin, BitwiseBuiltin +from starkware.cairo.common.alloc import alloc +from starkware.cairo.common.uint256 import Uint256 + +from kakarot.library import Kakarot +from kakarot.model import model + +func eth_call{ + syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* +}() -> (model.EVM*, model.State*, felt) { + // Given + + tempvar origin; + tempvar to: model.Option; + tempvar gas_limit; + tempvar gas_price; + let (value_ptr) = alloc(); + tempvar data_len: felt; + let (data) = alloc(); + tempvar access_list_len: felt; + let (access_list) = alloc(); + + %{ + from tests.utils.uint256 import int_to_uint256 + + + ids.origin = program_input.get("origin", 0) + ids.to.is_some = int(bool(program_input.get("to") is not None)) + ids.to.value = program_input.get("to", 0) + ids.gas_limit = program_input.get("gas_limit", int(2**63 - 1)) + ids.gas_price = program_input.get("gas_price", 0) + segments.write_arg(ids.value_ptr, int_to_uint256(program_input.get("value", 0))) + data = bytes.fromhex(program_input.get("data", "").replace("0x", "")) + ids.data_len = len(data) + segments.write_arg(ids.data, list(data)) + ids.access_list_len = 0 + %} + + let (evm, state, gas_used) = Kakarot.eth_call( + origin=origin, + to=to, + gas_limit=gas_limit, + gas_price=gas_price, + value=cast(value_ptr, Uint256*), + data_len=data_len, + data=data, + access_list_len=access_list_len, + access_list=access_list, + ); + + return (evm, state, gas_used); +} diff --git a/tests/src/kakarot/test_kakarot.py b/tests/src/kakarot/test_kakarot.py new file mode 100644 index 000000000..6ded01fb0 --- /dev/null +++ b/tests/src/kakarot/test_kakarot.py @@ -0,0 +1,153 @@ +import json +from types import MethodType + +import pytest +from eth_abi import encode +from eth_utils import keccak +from eth_utils.address import to_checksum_address +from web3 import Web3 +from web3._utils.abi import map_abi_data +from web3._utils.normalizers import BASE_RETURN_NORMALIZERS +from web3.exceptions import NoABIFunctionsFound + +from scripts.ef_tests.fetch import EF_TESTS_PARSED_DIR +from tests.utils.syscall_handler import SyscallHandler, parse_state + +CONTRACT_ADDRESS = 1234 +OWNER = to_checksum_address(f"0x{0xABDE1:040x}") +OTHER = to_checksum_address(f"0x{0xE1A5:040x}") + + +@pytest.fixture(scope="module") +def get_contract(cairo_run): + from scripts.utils.kakarot import get_contract as get_solidity_contract + + def _factory(contract_app, contract_name): + def _wrap_cairo_run(fun): + def _wrapper(self, *args, **kwargs): + origin = kwargs.pop("origin", 0) + gas_limit = kwargs.pop("gas_limit", int(1e9)) + gas_price = kwargs.pop("gas_price", 0) + value = kwargs.pop("value", 0) + data = self.get_function_by_name(fun)( + *args, **kwargs + )._encode_transaction_data() + evm, state, gas = cairo_run( + "eth_call", + origin=origin, + to=CONTRACT_ADDRESS, + gas_limit=gas_limit, + gas_price=gas_price, + value=value, + data=data, + ) + abi = self.get_function_by_name(fun).abi + if abi["stateMutability"] not in ["pure", "view"]: + return evm, state, gas + + codec = Web3().codec + types = [o["type"] for o in abi["outputs"]] + decoded = codec.decode(types, bytes(evm["return_data"])) + normalized = map_abi_data(BASE_RETURN_NORMALIZERS, types, decoded) + return normalized[0] if len(normalized) == 1 else normalized + + return _wrapper + + contract = get_solidity_contract(contract_app, contract_name) + try: + for fun in contract.functions: + setattr(contract, fun, MethodType(_wrap_cairo_run(fun), contract)) + except NoABIFunctionsFound: + pass + + return contract + + return _factory + + +@pytest.mark.NoCI +class TestKakarot: + class TestEthCall: + @pytest.mark.SolmateERC20 + async def test_erc20_transfer(self, get_contract): + erc20 = get_contract("Solmate", "ERC20") + amount = int(1e18) + initial_state = { + CONTRACT_ADDRESS: { + "code": list(erc20.bytecode_runtime), + "storage": { + "0x2": amount, + keccak(encode(["address", "uint8"], [OWNER, 3])).hex(): amount, + }, + "balance": 0, + "nonce": 0, + } + } + with SyscallHandler.patch_state(parse_state(initial_state)): + evm, *_ = erc20.transfer(OTHER, amount, origin=int(OWNER, 16)) + assert not evm["reverted"] + + @pytest.mark.SolmateERC721 + async def test_erc721_transfer(self, get_contract): + erc721 = get_contract("Solmate", "ERC721") + token_id = 1337 + initial_state = { + CONTRACT_ADDRESS: { + "code": list(erc721.bytecode_runtime), + "storage": { + keccak(encode(["uint256", "uint8"], [token_id, 2])).hex(): int( + OWNER, 16 + ), + keccak(encode(["address", "uint8"], [OWNER, 3])).hex(): 1, + }, + "balance": 0, + "nonce": 0, + } + } + with SyscallHandler.patch_state(parse_state(initial_state)): + evm, *_ = erc721.transferFrom( + OWNER, OTHER, token_id, origin=int(OWNER, 16) + ) + assert not evm["reverted"] + + @pytest.mark.EFTests + @pytest.mark.parametrize( + "ef_blockchain_test", EF_TESTS_PARSED_DIR.glob("*.json") + ) + async def test_case( + self, + cairo_run, + ef_blockchain_test, + ): + test_case = json.loads( + (EF_TESTS_PARSED_DIR / ef_blockchain_test).read_text() + ) + block = test_case["blocks"][0] + with SyscallHandler.patch_state(parse_state(test_case["pre"])): + tx = block["transactions"][0] + evm, state, gas_used = cairo_run( + "eth_call", + origin=int(tx["sender"], 16), + to=int(tx.get("to"), 16) if tx.get("to") else None, + gas_limit=int(tx["gasLimit"], 16), + gas_price=int(tx["gasPrice"], 16), + value=int(tx["value"], 16), + data=tx["data"], + ) + + parsed_state = { + int(address, 16): { + "balance": int(account["balance"], 16), + "code": account["code"], + "nonce": account["nonce"], + "storage": { + key: int(value, 16) + for key, value in account["storage"].items() + if int(value, 16) > 0 + }, + } + for address, account in state["accounts"].items() + if int(address, 16) > 10 + } + assert parsed_state == parse_state(test_case["postState"]) + assert gas_used == int(block["blockHeader"]["gasUsed"], 16) diff --git a/tests/src/kakarot/test_state.py b/tests/src/kakarot/test_state.py index 832bf6254..162106197 100644 --- a/tests/src/kakarot/test_state.py +++ b/tests/src/kakarot/test_state.py @@ -84,7 +84,9 @@ class TestCachePreaccessedAddresses: @SyscallHandler.patch("IERC20.balanceOf", lambda addr, data: [0, 1]) def test_should_cache_precompiles(self, cairo_run): state = cairo_run("test__cache_precompiles") - assert list(state["accounts"].keys()) == list(range(1, 10)) + assert list(state["accounts"].keys()) == [ + f"0x{i:040x}" for i in range(1, 10) + ] @SyscallHandler.patch("IERC20.balanceOf", lambda addr, data: [0, 1]) @pytest.mark.parametrize("transaction", TRANSACTIONS) diff --git a/tests/utils/helpers.cairo b/tests/utils/helpers.cairo index bb3090e4f..185b36e31 100644 --- a/tests/utils/helpers.cairo +++ b/tests/utils/helpers.cairo @@ -50,7 +50,7 @@ namespace TestHelpers { valid_jumpdests_start=valid_jumpdests_start, valid_jumpdests=valid_jumpdests, calldata=calldata, - calldata_len=1, + calldata_len=0, value=zero, parent=cast(0, model.Parent*), address=address, diff --git a/tests/utils/helpers.py b/tests/utils/helpers.py index 226d4fa1d..6b4f0c23c 100644 --- a/tests/utils/helpers.py +++ b/tests/utils/helpers.py @@ -221,9 +221,7 @@ def merge_access_list(access_list): """ merged_list = defaultdict(set) for access in access_list: - merged_list[int(access["address"], 16)] = merged_list[ - int(access["address"], 16) - ].union( + merged_list[access["address"]] = merged_list[access["address"]].union( { get_storage_var_address("storage_", *int_to_uint256(int(key, 16))) for key in access["storageKeys"] diff --git a/tests/utils/serde.py b/tests/utils/serde.py index 8e8df38f8..bccf0f38d 100644 --- a/tests/utils/serde.py +++ b/tests/utils/serde.py @@ -27,11 +27,6 @@ def get_identifier(self, struct_name, expected_type): return identifiers[0] def serialize_list(self, segment_ptr, item_scope=None, list_len=None): - list_len = ( - list_len - if list_len is not None - else self.runner.segments.get_segment_size(segment_ptr.segment_index) - ) item_identifier = ( self.get_identifier(item_scope, StructDefinition) if item_scope is not None @@ -43,6 +38,11 @@ def serialize_list(self, segment_ptr, item_scope=None, list_len=None): else TypeFelt() ) item_size = item_identifier.size if item_identifier is not None else 1 + list_len = ( + list_len * item_size + if list_len is not None + else self.runner.segments.get_segment_size(segment_ptr.segment_index) + ) output = [] for i in range(0, list_len, item_size): try: @@ -86,6 +86,8 @@ def serialize_pointers(self, name, ptr): return output def serialize_struct(self, name, ptr): + if ptr is None: + return None members = self.get_identifier(name, StructDefinition).members return { name: self._serialize(member.cairo_type, ptr + member.offset) @@ -107,7 +109,7 @@ def serialize_account(self, ptr): raw = self.serialize_pointers("model.Account", ptr) return { "address": self.serialize_address(raw["address"]), - "code": self.serialize_list(raw["code"]), + "code": self.serialize_list(raw["code"], list_len=raw["code_len"]), "storage": self.serialize_dict(raw["storage_start"], "Uint256"), "nonce": raw["nonce"], "balance": self.serialize_uint256(raw["balance"]), @@ -117,9 +119,18 @@ def serialize_account(self, ptr): def serialize_state(self, ptr): raw = self.serialize_pointers("model.State", ptr) return { - "accounts": self.serialize_dict(raw["accounts_start"], "model.Account"), - "events": self.serialize_list(raw["events"], "model.Event"), - "transfers": self.serialize_list(raw["transfers"], "model.Transfer"), + "accounts": { + to_checksum_address(f"{key:040x}"): value + for key, value in self.serialize_dict( + raw["accounts_start"], "model.Account" + ).items() + }, + "events": self.serialize_list( + raw["events"], "model.Event", list_len=raw["events_len"] + ), + "transfers": self.serialize_list( + raw["transfers"], "model.Transfer", list_len=raw["transfers_len"] + ), } def serialize_eth_transaction(self, ptr): @@ -144,6 +155,40 @@ def serialize_eth_transaction(self, ptr): "chain_id": raw["chain_id"], } + def serialize_message(self, ptr): + raw = self.serialize_pointers("model.Message", ptr) + return { + "bytecode": self.serialize_list( + raw["bytecode"], list_len=raw["bytecode_len"] + ), + "valid_jumpdest": list( + self.serialize_dict(raw["valid_jumpdests_start"]).keys() + ), + "calldata": self.serialize_list( + raw["calldata"], list_len=raw["calldata_len"] + ), + "value": self.serialize_uint256(raw["value"]), + "parent": self.serialize_struct("model.Parent", raw["parent"]), + "address": self.serialize_address(raw["address"]), + "code_address": raw["code_address"], + "read_only": bool(raw["read_only"]), + "is_create": bool(raw["is_create"]), + "depth": raw["depth"], + "env": self.serialize_struct("model.Environment", raw["env"]), + } + + def serialize_evm(self, ptr): + evm = self.serialize_struct("model.EVM", ptr) + return { + "message": evm["message"], + "return_data": evm["return_data"][: evm["return_data_len"]], + "program_counter": evm["program_counter"], + "stopped": bool(evm["stopped"]), + "gas_left": evm["gas_left"], + "gas_refund": evm["gas_refund"], + "reverted": evm["reverted"], + } + def serialize_stack(self, ptr): raw = self.serialize_pointers("model.Stack", ptr) stack_dict = self.serialize_dict(raw["dict_ptr_start"], "Uint256") @@ -171,12 +216,16 @@ def serialize_scope(self, scope, scope_ptr): return self.serialize_memory(scope_ptr) if scope.path[-1] == "Uint256": return self.serialize_uint256(scope_ptr) + if scope.path[-1] == "Message": + return self.serialize_message(scope_ptr) + if scope.path[-1] == "EVM": + return self.serialize_evm(scope_ptr) try: return self.serialize_struct(str(scope), scope_ptr) except MissingIdentifierError: return scope_ptr - def _serialize(self, cairo_type, ptr): + def _serialize(self, cairo_type, ptr, length=1): if isinstance(cairo_type, TypePointer): # A pointer can be a pointer to one single struct or to the beginning of a list of structs. # As such, every pointer is considered a list of structs, with length 1 or more. @@ -186,7 +235,9 @@ def _serialize(self, cairo_type, ptr): return None if isinstance(cairo_type.pointee, TypeFelt): return self.serialize_list(pointee) - serialized = self.serialize_list(pointee, str(cairo_type.pointee.scope)) + serialized = self.serialize_list( + pointee, str(cairo_type.pointee.scope), list_len=length + ) if len(serialized) == 1: return serialized[0] return serialized @@ -202,5 +253,14 @@ def _serialize(self, cairo_type, ptr): raise ValueError(f"Unknown type {cairo_type}") def serialize(self, cairo_type): - shift = hasattr(cairo_type, "members") and len(cairo_type.members) or 1 - return self._serialize(cairo_type, self.runner.vm.run_context.ap - shift) + if hasattr(cairo_type, "members"): + shift = len(cairo_type.members) + else: + try: + identifier = self.get_identifier( + str(cairo_type.scope), StructDefinition + ) + shift = len(identifier.members) + except (ValueError, AttributeError): + shift = 1 + return self._serialize(cairo_type, self.runner.vm.run_context.ap - shift, shift) diff --git a/tests/utils/syscall_handler.py b/tests/utils/syscall_handler.py index 2669802f3..eb7ea2d9a 100644 --- a/tests/utils/syscall_handler.py +++ b/tests/utils/syscall_handler.py @@ -11,6 +11,58 @@ ) from tests.utils.constants import CHAIN_ID +from tests.utils.uint256 import int_to_uint256 + + +def parse_state(state): + """ + Parse a serialized state as a dict of string, mainly converting hex strings to + integers and computing the corresponding kakarot storage key from the EVM one. + + Input state be like: + { + '0x1000000000000000000000000000000000000000': { + 'balance': '0x00', + 'code': '0x6000600060006000346000355af1600055600160015500', + 'nonce': '0x00', + 'storage': {} + }, + '0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b': { + 'balance': '0xffffffffffffffffffffffffffffffff', + 'code': '0x', + 'nonce': '0x00', + 'storage': {}, + } + } + """ + return { + (int(address, 16) if not isinstance(address, int) else address): { + "balance": ( + int(account["balance"], 16) + if not isinstance(account["balance"], int) + else account["balance"] + ), + "code": ( + list(bytes.fromhex(account["code"].replace("0x", ""))) + if isinstance(account["code"], str) + else account["code"] + ), + "nonce": ( + int(account["nonce"], 16) + if not isinstance(account["nonce"], int) + else account["nonce"] + ), + "storage": { + ( + get_storage_var_address("storage_", *int_to_uint256(int(key, 16))) + if not isinstance(key, int) + else key + ): (int(value, 16) if not isinstance(value, int) else value) + for key, value in account["storage"].items() + }, + } + for address, account in state.items() + } @dataclass @@ -87,7 +139,8 @@ def get_caller_address(self, segments, syscall_ptr): """ segments.write_arg(syscall_ptr + 1, [self.caller_address]) - def get_block_number(self, segments, syscall_ptr): + @classmethod + def get_block_number(cls, segments, syscall_ptr): """ Return a constant value for the get block number system call. @@ -106,9 +159,10 @@ def get_block_number(self, segments, syscall_ptr): response: GetBlockNumberResponse, } """ - segments.write_arg(syscall_ptr + 1, [self.block_number]) + segments.write_arg(syscall_ptr + 1, [cls.block_number]) - def get_block_timestamp(self, segments, syscall_ptr): + @classmethod + def get_block_timestamp(cls, segments, syscall_ptr): """ Return a constant value for the get block timestamp system call. @@ -127,7 +181,7 @@ def get_block_timestamp(self, segments, syscall_ptr): response: GetBlockTimestampResponse, } """ - segments.write_arg(syscall_ptr + 1, [self.block_timestamp]) + segments.write_arg(syscall_ptr + 1, [cls.block_timestamp]) def get_tx_info(self, segments, syscall_ptr): """ @@ -302,3 +356,73 @@ def patch(cls, target: str, *args, value: Optional[Union[callable, int]] = None) del cls.patches[selector_if_call] if "selector_if_storage" in globals(): del cls.patches[selector_if_storage] + + @classmethod + @contextmanager + def patch_state(cls, state: dict): + """ + Patch sycalls to match a given EVM state. + + Actual corresponding Starknet address are unknown but it doesn't matter since the + evm_to_starknet_address storage is also patched. + + :param state: the state to patch with, an output dictionary of parse_state + """ + patched_before = set(cls.patches.keys()) + + def _balance_of(erc20_address, calldata): + return int_to_uint256(state.get(calldata[0], {}).get("balance", 0)) + + balance_selector = get_selector_from_name("balanceOf") + cls.patches[balance_selector] = _balance_of + + def _bytecode(contract_address, calldata): + code = state.get(contract_address, {}).get("code", []) + return [len(code), *code] + + bytecode_selector = get_selector_from_name("bytecode") + cls.patches[bytecode_selector] = _bytecode + + def _bytecode_len(contract_address, calldata): + code = state.get(contract_address, {}).get("code", []) + return [len(code)] + + bytecode_len_selector = get_selector_from_name("bytecode_len") + cls.patches[bytecode_len_selector] = _bytecode_len + + def _get_nonce(contract_address, calldata): + return [state.get(contract_address, {}).get("nonce", 0)] + + nonce_selector = get_selector_from_name("get_nonce") + cls.patches[nonce_selector] = _get_nonce + + def _storage(contract_address, calldata): + return int_to_uint256( + state.get(contract_address, {}).get("storage", {}).get(calldata[0], 0) + ) + + storage_selector = get_selector_from_name("storage") + cls.patches[storage_selector] = _storage + + # Set account types + # We set all account types to be CA (contract account) as the only difference is that + # with EOA it doesn't try to fetch the nonce from the syscall, while here we actually + # want to have the EOA with the patched nonce. + def _account_type(contract_address, calldata): + return [int.from_bytes(b"CA", "big")] + + account_type_selector = get_selector_from_name("account_type") + cls.patches[account_type_selector] = _account_type + + # Register accounts + for address in state.keys(): + address_selector = get_storage_var_address( + "evm_to_starknet_address", address + ) + cls.patches[address_selector] = address + + yield + + patched = set(cls.patches.keys()) + for selector in patched - patched_before: + del cls.patches[selector]