From f17e2a817521a2529587f0680105ea60e27f9a97 Mon Sep 17 00:00:00 2001 From: Antun Badurina <31538513+badurinantun@users.noreply.github.com> Date: Wed, 23 Mar 2022 17:14:53 +0100 Subject: [PATCH] Get transaction trace (#58) * Add shared testing resources * Resolve pylint duplicate code issue --- .circleci/config.yml | 3 + .pylintrc | 4 + README.md | 7 +- pyproject.toml | 1 + starknet_devnet/origin.py | 12 +++ starknet_devnet/server.py | 16 +++ starknet_devnet/starknet_wrapper.py | 17 +++ starknet_devnet/transaction_wrapper.py | 6 ++ test/expected/deploy_function_invocation.json | 24 +++++ test/expected/invoke_function_invocation.json | 24 +++++ test/shared.py | 20 ++++ test/test_cli.py | 22 ++-- test/test_cli_auth.py | 7 +- test/test_dump.py | 4 +- test/test_postman.py | 3 +- test/test_state_update.py | 6 +- test/test_transaction_trace.py | 101 ++++++++++++++++++ 17 files changed, 249 insertions(+), 28 deletions(-) create mode 100644 test/expected/deploy_function_invocation.json create mode 100644 test/expected/invoke_function_invocation.json create mode 100644 test/shared.py create mode 100644 test/test_transaction_trace.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 63b13e9ba..e6fa5ee08 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -47,6 +47,9 @@ jobs: - run: name: Test get state update endpoint command: poetry run pytest -s -vv test/test_state_update.py + - run: + name: Test get transaction trace endpoint + command: poetry run pytest -s -vv test/test_transaction_trace.py - run: name: Test plugin - dockerized command: ./test/test_plugin.sh diff --git a/.pylintrc b/.pylintrc index 02dd8c7cb..32a71464b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -3,3 +3,7 @@ max-line-length=150 [BASIC] min-public-methods=1 + +[SIMILARITIES] +min-similarity-lines=20 +ignore-imports=no diff --git a/README.md b/README.md index 862a6320f..aca250c3e 100644 --- a/README.md +++ b/README.md @@ -86,13 +86,14 @@ If you don't specify the `HOST` part, the server will indeed be available on all - `deploy` - `get_block` - `get_code` + - `get_full_contract` + - `get_state_update` - `get_storage_at` + - `get_transaction_receipt` + - `get_transaction_trace` - `get_transaction` - `invoke` - `tx_status` - - `get_transaction_receipt` - - `get_full_contract` - - `get_state_update` - The following Starknet CLI commands are **not** supported: - `get_contract_addresses` - `estimate_fee` (currently always returning 0) diff --git a/pyproject.toml b/pyproject.toml index 23e87b469..5e4dae358 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ markers = [ "deploy", "invoke", "state_update", + "transaction_trace", "web3_deploy", "web3_messaging" ] diff --git a/starknet_devnet/origin.py b/starknet_devnet/origin.py index 828bca238..d172ab5bb 100644 --- a/starknet_devnet/origin.py +++ b/starknet_devnet/origin.py @@ -21,6 +21,10 @@ def get_transaction_receipt(self, transaction_hash: str): """Returns the transaction receipt object.""" raise NotImplementedError + def get_transaction_trace(self, transaction_hash: str): + """Returns the transaction trace object.""" + raise NotImplementedError + def get_block_by_hash(self, block_hash: str): """Returns the block identified with either its hash.""" raise NotImplementedError @@ -75,6 +79,11 @@ def get_transaction_receipt(self, transaction_hash: str): "events": [] } + def get_transaction_trace(self, transaction_hash: str): + tx_hash_int = int(transaction_hash, 16) + message=f"Transaction corresponding to hash {tx_hash_int} is not found." + raise StarknetDevnetException(message=message) + def get_block_by_hash(self, block_hash: str): message=f"Block hash not found; got: {block_hash}." raise StarknetDevnetException(message=message) @@ -126,6 +135,9 @@ def get_transaction_status(self, transaction_hash: str): def get_transaction(self, transaction_hash: str): raise NotImplementedError + def get_transaction_trace(self, transaction_hash: str): + raise NotImplementedError + def get_block_by_hash(self, block_hash: str): raise NotImplementedError diff --git a/starknet_devnet/server.py b/starknet_devnet/server.py index d716ad254..d7bae3e5c 100644 --- a/starknet_devnet/server.py +++ b/starknet_devnet/server.py @@ -123,6 +123,7 @@ async def get_block(): result_dict = starknet_wrapper.get_block_by_number(block_number) except StarkException as err: abort(Response(err.message, 500)) + return jsonify(result_dict) @app.route("/feeder_gateway/get_code", methods=["GET"]) @@ -194,6 +195,21 @@ def get_transaction_receipt(): ret = starknet_wrapper.get_transaction_receipt(transaction_hash) return jsonify(ret) +@app.route("/feeder_gateway/get_transaction_trace", methods=["GET"]) +def get_transaction_trace(): + """ + Returns the trace of the transaction identified by the transactionHash argument in the GET request. + """ + + transaction_hash = request.args.get("transactionHash") + + try: + transaction_trace = starknet_wrapper.get_transaction_trace(transaction_hash) + except StarkException as err: + abort(Response(err, 500)) + + return jsonify(transaction_trace) + @app.route("/feeder_gateway/get_state_update", methods=["GET"]) def get_state_update(): """ diff --git a/starknet_devnet/starknet_wrapper.py b/starknet_devnet/starknet_wrapper.py index 925d021e6..d08137210 100644 --- a/starknet_devnet/starknet_wrapper.py +++ b/starknet_devnet/starknet_wrapper.py @@ -264,6 +264,23 @@ def get_transaction_receipt(self, transaction_hash: str): return self.__origin.get_transaction_receipt(transaction_hash) + def get_transaction_trace(self, transaction_hash:str): + """Returns the transaction trace of the tranasction indetified by `transaction_hash`""" + + tx_hash_int = int(transaction_hash, 16) + if tx_hash_int in self.__transaction_wrappers: + status = self.__transaction_wrappers[tx_hash_int].transaction["status"] + transaction_wrapper = self.__transaction_wrappers[tx_hash_int] + + if not hasattr(transaction_wrapper, "trace"): + raise StarknetDevnetException( + f"Transaction corresponding to hash {tx_hash_int} has no trace; status: {status}." + ) + + return transaction_wrapper.trace + + return self.__origin.get_transaction_trace(transaction_hash) + def get_number_of_blocks(self) -> int: """Returns the number of blocks stored so far.""" return len(self.__num2block) + self.__origin.get_number_of_blocks() diff --git a/starknet_devnet/transaction_wrapper.py b/starknet_devnet/transaction_wrapper.py index ba1d0d4fd..2e3d56ac6 100644 --- a/starknet_devnet/transaction_wrapper.py +++ b/starknet_devnet/transaction_wrapper.py @@ -81,6 +81,12 @@ def __init__( "transaction_index": 0 # always the first (and only) tx in the block } + if status is not TxStatus.REJECTED: + self.trace = { + "function_invocation": execution_info.call_info.dump(), + "signature": tx_details.to_dict().get("signature", []) + } + def set_block_data(self, block_hash: str, block_number: int): """Sets `block_hash` and `block_number` to the wrapped transaction and receipt.""" self.transaction["block_hash"] = self.receipt["block_hash"] = block_hash diff --git a/test/expected/deploy_function_invocation.json b/test/expected/deploy_function_invocation.json new file mode 100644 index 000000000..8a43e7f56 --- /dev/null +++ b/test/expected/deploy_function_invocation.json @@ -0,0 +1,24 @@ +{ + "calldata": ["0x0"], + "caller_address": "0x0", + "code_address": "0x3ffc1d4aca9668dfd9b5b6d374367b9dc52daed0ae23c54cb93ea2ea6e2dc72", + "contract_address": "0x3ffc1d4aca9668dfd9b5b6d374367b9dc52daed0ae23c54cb93ea2ea6e2dc72", + "entry_point_type": "CONSTRUCTOR", + "events": [], + "execution_resources": { + "builtin_instance_counter": { + "bitwise_builtin": 0, + "ec_op_builtin": 0, + "ecdsa_builtin": 0, + "output_builtin": 0, + "pedersen_builtin": 0, + "range_check_builtin": 0 + }, + "n_memory_holes": 0, + "n_steps": 40 + }, + "internal_calls": [], + "messages": [], + "result": [], + "selector": "0x28ffe4ff0f226a9107253e17a904099aa4f63a02a5621de0576e5aa71bc5194" +} diff --git a/test/expected/invoke_function_invocation.json b/test/expected/invoke_function_invocation.json new file mode 100644 index 000000000..5cacd1590 --- /dev/null +++ b/test/expected/invoke_function_invocation.json @@ -0,0 +1,24 @@ +{ + "calldata": ["0xa", "0x14"], + "caller_address": "0x0", + "code_address": "0x3ffc1d4aca9668dfd9b5b6d374367b9dc52daed0ae23c54cb93ea2ea6e2dc72", + "contract_address": "0x3ffc1d4aca9668dfd9b5b6d374367b9dc52daed0ae23c54cb93ea2ea6e2dc72", + "entry_point_type": "EXTERNAL", + "events": [], + "execution_resources": { + "builtin_instance_counter": { + "bitwise_builtin": 0, + "ec_op_builtin": 0, + "ecdsa_builtin": 0, + "output_builtin": 0, + "pedersen_builtin": 0, + "range_check_builtin": 0 + }, + "n_memory_holes": 0, + "n_steps": 67 + }, + "internal_calls": [], + "messages": [], + "result": [], + "selector": "0x362398bec32bc0ebb411203221a35a0301193a96f317ebe5e40be9f60d15320" +} diff --git a/test/shared.py b/test/shared.py new file mode 100644 index 000000000..f12483d3c --- /dev/null +++ b/test/shared.py @@ -0,0 +1,20 @@ +"""Shared values between tests""" + +ARTIFACTS_PATH = "starknet-hardhat-example/starknet-artifacts/contracts" +CONTRACT_PATH = f"{ARTIFACTS_PATH}/contract.cairo/contract.json" +ABI_PATH = f"{ARTIFACTS_PATH}/contract.cairo/contract_abi.json" +EVENTS_CONTRACT_PATH = f"{ARTIFACTS_PATH}/events.cairo/events.json" +EVENTS_ABI_PATH = f"{ARTIFACTS_PATH}/events.cairo/events_abi.json" +FAILING_CONTRACT_PATH = f"{ARTIFACTS_PATH}/always_fail.cairo/always_fail.json" + +BALANCE_KEY = "916907772491729262376534102982219947830828984996257231353398618781993312401" + +SIGNATURE = [ + "1225578735933442828068102633747590437426782890965066746429241472187377583468", + "3568809569741913715045370357918125425757114920266578211811626257903121825123" +] + +EXPECTED_SALTY_DEPLOY_ADDRESS = "0x07c3a0c91048930f0258601db4211a3aa0578d9e746f15526a74eaabd38c56a4" +EXPECTED_SALTY_DEPLOY_HASH = "0x11ea05c61d78383e95cf44b70cfe15e74a55c7ceb1186c0c2ed743219f1f2ca" + +NONEXISTENT_TX_HASH = "0x1" diff --git a/test/test_cli.py b/test/test_cli.py index 6b5cd0af4..4fbd2b0ef 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -13,16 +13,17 @@ call, deploy, invoke ) -ARTIFACTS_PATH = "starknet-hardhat-example/starknet-artifacts/contracts" -CONTRACT_PATH = f"{ARTIFACTS_PATH}/contract.cairo/contract.json" -ABI_PATH = f"{ARTIFACTS_PATH}/contract.cairo/contract_abi.json" -EVENTS_CONTRACT_PATH = f"{ARTIFACTS_PATH}/events.cairo/events.json" -EVENTS_ABI_PATH = f"{ARTIFACTS_PATH}/events.cairo/events_abi.json" -FAILING_CONTRACT_PATH = f"{ARTIFACTS_PATH}/always_fail.cairo/always_fail.json" - -EXPECTED_SALTY_DEPLOY_ADDRESS = "0x07c3a0c91048930f0258601db4211a3aa0578d9e746f15526a74eaabd38c56a4" -EXPECTED_SALTY_DEPLOY_HASH = "0x11ea05c61d78383e95cf44b70cfe15e74a55c7ceb1186c0c2ed743219f1f2ca" -NONEXISTENT_TX_HASH = "0x1" +from .shared import ( + ABI_PATH, + BALANCE_KEY, + CONTRACT_PATH, + EVENTS_ABI_PATH, + EVENTS_CONTRACT_PATH, + EXPECTED_SALTY_DEPLOY_ADDRESS, + EXPECTED_SALTY_DEPLOY_HASH, + FAILING_CONTRACT_PATH, + NONEXISTENT_TX_HASH +) run_devnet_in_background(sleep_seconds=1) deploy_info = deploy(CONTRACT_PATH, ["0"]) @@ -33,7 +34,6 @@ assert_transaction_not_received(NONEXISTENT_TX_HASH) # check storage after deployment -BALANCE_KEY = "916907772491729262376534102982219947830828984996257231353398618781993312401" assert_storage(deploy_info["address"], BALANCE_KEY, "0x0") # check block and receipt after deployment diff --git a/test/test_cli_auth.py b/test/test_cli_auth.py index fb7ad77ce..551319afe 100644 --- a/test/test_cli_auth.py +++ b/test/test_cli_auth.py @@ -9,7 +9,8 @@ call, deploy, invoke ) -ARTIFACTS_PATH = "starknet-hardhat-example/starknet-artifacts/contracts" +from .shared import ARTIFACTS_PATH, SIGNATURE + CONTRACT_PATH = f"{ARTIFACTS_PATH}/auth_contract.cairo/auth_contract.json" ABI_PATH = f"{ARTIFACTS_PATH}/auth_contract.cairo/auth_contract_abi.json" @@ -28,10 +29,6 @@ assert_block(0, deploy_info["tx_hash"]) assert_receipt(deploy_info["tx_hash"], "test/expected/deploy_receipt_auth.json") -SIGNATURE = [ - "1225578735933442828068102633747590437426782890965066746429241472187377583468", - "3568809569741913715045370357918125425757114920266578211811626257903121825123" -] # increase and assert balance invoke_tx_hash = invoke( function="increase_balance", diff --git a/test/test_dump.py b/test/test_dump.py index c591a9dca..08495be5c 100644 --- a/test/test_dump.py +++ b/test/test_dump.py @@ -11,10 +11,8 @@ from .util import call, deploy, invoke, run_devnet_in_background from .settings import GATEWAY_URL +from .shared import CONTRACT_PATH, ABI_PATH -ARTIFACTS_PATH = "starknet-hardhat-example/starknet-artifacts/contracts" -CONTRACT_PATH = f"{ARTIFACTS_PATH}/contract.cairo/contract.json" -ABI_PATH = f"{ARTIFACTS_PATH}/contract.cairo/contract_abi.json" DUMP_PATH = "dump.pkl" @pytest.fixture(autouse=True) diff --git a/test/test_postman.py b/test/test_postman.py index 6774bd69c..2d2b57a98 100644 --- a/test/test_postman.py +++ b/test/test_postman.py @@ -17,7 +17,8 @@ from web3 import Web3 -ARTIFACTS_PATH = "starknet-hardhat-example/starknet-artifacts/contracts" +from .shared import ARTIFACTS_PATH + CONTRACT_PATH = f"{ARTIFACTS_PATH}/l1l2.cairo/l1l2.json" ABI_PATH = f"{ARTIFACTS_PATH}/l1l2.cairo/l1l2_abi.json" diff --git a/test/test_state_update.py b/test/test_state_update.py index c0f14fed0..cf9784010 100644 --- a/test/test_state_update.py +++ b/test/test_state_update.py @@ -9,11 +9,7 @@ from .util import deploy, invoke, load_contract_definition, run_devnet_in_background, get_block from .settings import FEEDER_GATEWAY_URL - -ARTIFACTS_PATH = "starknet-hardhat-example/starknet-artifacts/contracts" -CONTRACT_PATH = f"{ARTIFACTS_PATH}/contract.cairo/contract.json" -ABI_PATH = f"{ARTIFACTS_PATH}/contract.cairo/contract_abi.json" -BALANCE_KEY = "916907772491729262376534102982219947830828984996257231353398618781993312401" +from .shared import CONTRACT_PATH, ABI_PATH, BALANCE_KEY @pytest.fixture(autouse=True) def run_before_and_after_test(): diff --git a/test/test_transaction_trace.py b/test/test_transaction_trace.py new file mode 100644 index 000000000..0627c316c --- /dev/null +++ b/test/test_transaction_trace.py @@ -0,0 +1,101 @@ +""" +Test get_transaction endpoint +""" + +import pytest +import requests + +from .util import deploy, invoke, load_json_from_path, run_devnet_in_background +from .settings import FEEDER_GATEWAY_URL +from .shared import ABI_PATH, CONTRACT_PATH, SIGNATURE, NONEXISTENT_TX_HASH + +@pytest.fixture(autouse=True) +def run_before_and_after_test(): + """Run devnet before and kill it after the test run""" + # before test + devnet_proc = run_devnet_in_background() + + yield + + # after test + devnet_proc.kill() + +def get_transaction_trace_response(tx_hash=None): + """Get transaction trace response""" + params = { + "transactionHash": tx_hash, + } + + res = requests.get( + f"{FEEDER_GATEWAY_URL}/feeder_gateway/get_transaction_trace", + params=params + ) + + return res + +def deploy_empty_contract(): + """ + Deploy sample contract with balance = 0. + Returns transaction hash. + """ + return deploy(CONTRACT_PATH, inputs=["0"], salt="0x99") + +def assert_function_invocation(function_invocation, expected_path): + """Asserts function invocation""" + expected_function_invocation = load_json_from_path(expected_path) + assert function_invocation == expected_function_invocation + +@pytest.mark.transaction_trace +def test_deploy_transaction_trace(): + """Test deploy transaction trace""" + tx_hash = deploy_empty_contract()["tx_hash"] + res = get_transaction_trace_response(tx_hash) + + assert res.status_code == 200 + + transaction_trace = res.json() + assert transaction_trace["signature"] == [] + assert_function_invocation( + transaction_trace["function_invocation"], + "test/expected/deploy_function_invocation.json" + ) + +@pytest.mark.transaction_trace +def test_invoke_transaction_hash(): + """Test invoke transaction trace""" + contract_address = deploy_empty_contract()["address"] + tx_hash = invoke("increase_balance", ["10", "20"], contract_address, ABI_PATH) + res = get_transaction_trace_response(tx_hash) + + assert res.status_code == 200 + + transaction_trace = res.json() + assert transaction_trace["signature"] == [] + assert_function_invocation( + transaction_trace["function_invocation"], + "test/expected/invoke_function_invocation.json" + ) + + +@pytest.mark.transaction_trace +def test_invoke_transaction_hash_with_signature(): + """Test invoke transaction trace with signature""" + contract_address = deploy_empty_contract()["address"] + tx_hash = invoke("increase_balance", ["10", "20"], contract_address, ABI_PATH, SIGNATURE) + res = get_transaction_trace_response(tx_hash) + + assert res.status_code == 200 + + transaction_trace = res.json() + assert transaction_trace["signature"] == SIGNATURE + assert_function_invocation( + transaction_trace["function_invocation"], + "test/expected/invoke_function_invocation.json" + ) + +@pytest.mark.transaction_trace +def test_nonexistent_transaction_hash(): + """Test if it throws 500 for nonexistent transaction trace""" + res = get_transaction_trace_response(NONEXISTENT_TX_HASH) + + assert res.status_code == 500