Skip to content

Commit

Permalink
Adapt to cairo-lang v0.7.0 (0xSpaceShard#31)
Browse files Browse the repository at this point in the history
* Improve logging if version mismatch

* Allow passing arrays of structs

* Add execution_info to deploy receipts

* Add block_hash calculation

* Bump version to v0.1.13

* Fix raising error if assertion false

* Add complex input test to test_cli
  • Loading branch information
FabijanC committed Jan 21, 2022
1 parent 31e328f commit 2eab61e
Show file tree
Hide file tree
Showing 19 changed files with 208 additions and 881 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
HARDHAT_CONFIG_FILE: ../test/hardhat.config.dockerized.ts
TEST_FILE: test/quick-test.ts
- run:
name: Test plugin - venv
name: Test plugin - venv (tests various cases in sample-test)
command: python3 -m test.test_plugin
no_output_timeout: 1m
environment:
Expand Down
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,7 @@ If you don't specify the `HOST` part, the server will indeed be available on all
- `get_transaction`
- `invoke`
- `tx_status`
- `get_transaction_receipt`:
- returns complete data for invoke transactions
- returns partial data for deploy transactions
- `get_transaction_receipt`
- The following Starknet CLI commands are **not** supported:
- `get_contract_addresses` - L1-L2 interaction is currently not supported

Expand Down
6 changes: 3 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "starknet_devnet"
version = "0.1.12"
version = "0.1.13"
description = "A local testnet for Starknet"
authors = ["FabijanC <[email protected]>"]
license = "ISC"
Expand All @@ -14,7 +14,7 @@ keywords = ["starknet", "cairo", "testnet", "local", "server"]
python = "^3.7"
Flask = {extras = ["async"], version = "^2.0.2"}
flask-cors = "^3.0.10"
cairo-lang = "0.6.2"
cairo-lang = "0.7.0"

[tool.poetry.dev-dependencies]
pylint = "^2.12.2"
Expand Down
2 changes: 1 addition & 1 deletion starknet_devnet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Contains the server implementation and its utility classes and functions.
"""

__version__ = "0.1.12"
__version__ = "0.1.13"
28 changes: 17 additions & 11 deletions starknet_devnet/adapt.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def adapt_calldata(calldata, expected_inputs, types):
input_name = input_entry["name"]
input_type = input_entry["type"]
if calldata_i >= len(calldata):
if input_type == "felt*" and last_name == f"{input_name}_len" and last_value == 0:
if input_type.endswith("*") and last_name == f"{input_name}_len" and last_value == 0:
# This means that an empty array is provided.
# Last element was array length (0), it's replaced with the array itself
adapted_calldata[-1] = []
Expand All @@ -32,19 +32,14 @@ def adapt_calldata(calldata, expected_inputs, types):
raise StarknetDevnetException(message=message)
input_value = calldata[calldata_i]

if input_type == "felt*":
if input_type.endswith("*"):
if last_name != f"{input_name}_len":
raise StarknetDevnetException(f"Array size argument {last_name} must appear right before {input_name}.")

arr_element_type = input_type[:-1]
arr_length = int(last_value)
arr = calldata[calldata_i : calldata_i + arr_length]
if len(arr) < arr_length:
message = f"Too few function arguments provided: {len(calldata)}."
raise StarknetDevnetException(message=message)

# last element was array length, it's replaced with the array itself
adapted_calldata[-1] = arr
calldata_i += arr_length
adapted_calldata[-1], calldata_i = _adapt_calldata_array(arr_element_type, arr_length, calldata, calldata_i, types)

elif input_type == "felt":
adapted_calldata.append(input_value)
Expand All @@ -62,8 +57,8 @@ def adapt_calldata(calldata, expected_inputs, types):
def adapt_output(received):
"""
Adapts the `received` object to format expected by client (list of hex strings).
If `received` is an instance of `list`, it is understood that it corresponds to a felt*, so first its length is appended.
If `received` is iterable, it is either a struct, a tuple or a felt*.
If `received` is an instance of `list`, it is understood that it corresponds to an array, so first its length is appended.
If `received` is iterable, it is either a struct, a tuple or an array (felt* or struct*)
Otherwise it is a `felt`.
`ret` is recursively populated (and should probably be empty on first call).
Expand All @@ -85,6 +80,17 @@ def _adapt_output_rec(received, ret):
except TypeError:
ret.append(hex(received))

def _adapt_calldata_array(arr_element_type: str, arr_length: int, calldata: list, calldata_i: int, types):
arr = []
for _ in range(arr_length):
arr_element, calldata_i = _generate_complex(calldata, calldata_i, arr_element_type, types)
arr.append(arr_element)

if len(arr) < arr_length:
message = f"Too few function arguments provided: {len(calldata)}."
raise StarknetDevnetException(message=message)
return arr, calldata_i

def _generate_complex(calldata, calldata_i: int, input_type: str, types):
"""
Converts members of `calldata` to a more complex type specified by `input_type`:
Expand Down
9 changes: 9 additions & 0 deletions starknet_devnet/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Constants used across the project."""

try:
from importlib_metadata import version
except ImportError:
# >= py 3.8
from importlib.metadata import version

CAIRO_LANG_VERSION = version("cairo-lang")
12 changes: 6 additions & 6 deletions starknet_devnet/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@
from flask import Flask, request, jsonify, abort
from flask.wrappers import Response
from flask_cors import CORS
from marshmallow import ValidationError
from starkware.starknet.services.api.gateway.transaction import InvokeFunction, Transaction
from starkware.starknet.definitions.transaction_type import TransactionType
from starkware.starkware_utils.error_handling import StarkErrorCode, StarkException
from werkzeug.datastructures import MultiDict

from .util import custom_int, fixed_length_hex, parse_args
from .constants import CAIRO_LANG_VERSION
from .starknet_wrapper import StarknetWrapper
from .origin import NullOrigin
from .util import custom_int, fixed_length_hex, parse_args

app = Flask(__name__)
CORS(app)
Expand All @@ -28,15 +30,13 @@ def is_alive():

@app.route("/gateway/add_transaction", methods=["POST"])
async def add_transaction():
"""
Endpoint for accepting DEPLOY and INVOKE_FUNCTION transactions.
"""
"""Endpoint for accepting DEPLOY and INVOKE_FUNCTION transactions."""

raw_data = request.get_data()
try:
transaction = Transaction.loads(raw_data)
except TypeError:
msg = "Invalid transaction format. Try recompiling your contract with a newer version."
except (TypeError, ValidationError):
msg = f"Invalid tx. Be sure to use the correct compilation (json) artifact. Devnet-compatible cairo-lang version: {CAIRO_LANG_VERSION}"
abort(Response(msg, 400))

tx_type = transaction.tx_type.name
Expand Down
58 changes: 36 additions & 22 deletions starknet_devnet/starknet_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from starkware.starknet.testing.starknet import Starknet
from starkware.starknet.testing.objects import StarknetTransactionExecutionInfo
from starkware.starkware_utils.error_handling import StarkException
from starkware.starknet.services.api.feeder_gateway.block_hash import calculate_block_hash

from .origin import Origin
from .util import Choice, StarknetDevnetException, TxStatus, fixed_length_hex, DummyExecutionInfo
Expand Down Expand Up @@ -80,7 +81,7 @@ async def __update_state(self):

async def __get_state_root(self):
state = await self.__get_state()
return state.state.shared_state.contract_states.root.hex()
return state.state.shared_state.contract_states.root

def __is_contract_deployed(self, address: int) -> bool:
return address in self.__address2contract_wrapper
Expand Down Expand Up @@ -110,11 +111,9 @@ async def deploy(self, transaction: Transaction):
constructor_calldata=deploy_transaction.constructor_calldata,
contract_address_salt=deploy_transaction.contract_address_salt
)
# Uncomment this once contract has execution_info
# execution_info = contract.execution_info
execution_info = contract.deploy_execution_info
error_message = None
status = TxStatus.ACCEPTED_ON_L2
execution_info = DummyExecutionInfo()

self.__address2contract_wrapper[contract.contract_address] = ContractWrapper(contract, deploy_transaction.contract_definition)
await self.__update_state()
Expand Down Expand Up @@ -181,7 +180,6 @@ def get_transaction_status(self, transaction_hash: str):
tx_hash_int = int(transaction_hash, 16)
if tx_hash_int in self.__transaction_wrappers:
transaction_wrapper = self.__transaction_wrappers[tx_hash_int]

transaction = transaction_wrapper.transaction

ret = {
Expand Down Expand Up @@ -221,38 +219,54 @@ def get_transaction_receipt(self, transaction_hash: str):
"transaction_hash": transaction_hash
}

def get_number_of_blocks(self):
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()

async def __generate_block(self, transaction: dict, receipt: dict):
async def __generate_block(self, tx_wrapper: TransactionWrapper):
"""
Generates a block and stores it to blocks and hash2block. The block contains just the passed transaction.
Also modifies the `transaction` and `receipt` objects received.
The `transaction` dict should also contain a key `transaction`.
The `tx_wrapper.transaction` dict should contain a key `transaction`.
Returns (block_hash, block_number).
"""

block_number = self.get_number_of_blocks()
block_hash = hex(block_number)
state = await self.__get_state()
state_root = await self.__get_state_root()
block_number = self.get_number_of_blocks()
timestamp = int(time.time())
signature = []
if "signature" in tx_wrapper.transaction["transaction"]:
signature = [int(sig_part) for sig_part in tx_wrapper.transaction["transaction"]["signature"]]

parent_block_hash = self.__get_last_block()["block_hash"] if block_number else fixed_length_hex(0)

block_hash = await calculate_block_hash(
general_config=state.general_config,
parent_hash=int(parent_block_hash, 16),
block_number=block_number,
global_state_root=state_root,
block_timestamp=timestamp,
tx_hashes=[int(tx_wrapper.transaction_hash, 16)],
tx_signatures=[signature],
event_hashes=[]
)

block_hash_hexed = fixed_length_hex(block_hash)
block = {
"block_hash": block_hash,
"block_hash": block_hash_hexed,
"block_number": block_number,
"parent_block_hash": self.__get_last_block()["block_hash"] if self.__num2block else "0x0",
"state_root": state_root,
"parent_block_hash": parent_block_hash,
"state_root": state_root.hex(),
"status": TxStatus.ACCEPTED_ON_L2.name,
"timestamp": int(time.time()),
"transaction_receipts": [receipt],
"transactions": [transaction["transaction"]],
"timestamp": timestamp,
"transaction_receipts": [tx_wrapper.receipt],
"transactions": [tx_wrapper.transaction["transaction"]],
}

number_of_blocks = self.get_number_of_blocks()
self.__num2block[number_of_blocks] = block
self.__hash2block[int(block_hash, 16)] = block
self.__num2block[block_number] = block
self.__hash2block[block_hash] = block

return block_hash, block_number
return block_hash_hexed, block_number

def __get_last_block(self):
number_of_blocks = self.get_number_of_blocks()
Expand Down Expand Up @@ -301,7 +315,7 @@ async def __store_transaction(self, internal_tx: InternalTransaction, status: Tx
assert error_message, "error_message must be present if tx rejected"
tx_wrapper.set_failure_reason(error_message)
else:
block_hash, block_number = await self.__generate_block(tx_wrapper.transaction, tx_wrapper.receipt)
block_hash, block_number = await self.__generate_block(tx_wrapper)
tx_wrapper.set_block_data(block_hash, block_number)

numeric_hash = int(tx_wrapper.transaction_hash, 16)
Expand Down
5 changes: 3 additions & 2 deletions starknet_devnet/transaction_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ class DeployTransactionDetails(TransactionDetails):
class InvokeTransactionDetails(TransactionDetails):
"""Transcation details of `InvokeTransaction`."""
calldata: List[str]
signature: List[str]
entry_point_selector: str


class TransactionWrapper(ABC):
"""Transaction Wrapper base class."""

Expand Down Expand Up @@ -108,6 +108,7 @@ def __init__(self, internal_tx: InternalInvokeFunction, status: TxStatus, execut
contract_address=fixed_length_hex(internal_tx.contract_address),
transaction_hash=fixed_length_hex(internal_tx.hash_value),
calldata=[str(arg) for arg in internal_tx.calldata],
entry_point_selector=str(internal_tx.entry_point_selector)
entry_point_selector=str(internal_tx.entry_point_selector),
signature=[str(sig_part) for sig_part in internal_tx.signature]
)
)
Loading

0 comments on commit 2eab61e

Please sign in to comment.