diff --git a/page/docs/guide/cairo1-support.md b/page/docs/guide/cairo1-support.md new file mode 100644 index 000000000..50b25329c --- /dev/null +++ b/page/docs/guide/cairo1-support.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 18 +--- + +# Cairo 1 support + +Declaring, deploying and interacting with Cairo 1 contracts is supported in the latest version. To successfully declare, if on an x86 machine, you don't have to do anything. If on another architecture, or if you want to specify a custom version of the Cairo 1 compiler, you need to specify a local compiler for recompilation (a necessary step in the declaraion of Cairo 1 contracts). Use one of: + +- `--cairo-compiler-manifest ` +- `--sierra-compiler-path ` + +## Docker support + +Devnet's Docker image has a recompiler set up internally, so Cairo 1 is supported out-of-the-box. But to use a custom compiler, you should have a statically linked executable binary sierra compiler on your host and use it like this (use absolute paths when mounting): + +``` +$ docker run -it \ + -p :5050 \ + --mount type=bind,source=,target=/starknet-sierra-compile \ + -it \ + shardlabs/starknet-devnet: \ + --sierra-compiler-path /starknet-sierra-compile +``` diff --git a/page/docs/guide/development.md b/page/docs/guide/development.md index f9b22c36a..ce5459600 100644 --- a/page/docs/guide/development.md +++ b/page/docs/guide/development.md @@ -1,5 +1,5 @@ --- -sidebar_position: 18 +sidebar_position: 19 --- # Development diff --git a/page/docs/guide/run.md b/page/docs/guide/run.md index cc73e50a3..ad9d85d74 100644 --- a/page/docs/guide/run.md +++ b/page/docs/guide/run.md @@ -56,6 +56,8 @@ optional arguments: Disable RPC schema validation for devnet responses --cairo-compiler-manifest CAIRO_COMPILER_MANIFEST Specify the path to the manifest (Cargo.toml) of the Cairo 1.0 compiler to be used for contract recompilation; if omitted, the default x86-compatible compiler (from cairo-lang package) is used + --sierra-compiler-path SIERRA_COMPILER_PATH + Specify the path to the binary executable of starknet-sierra-compile ``` You can run `starknet-devnet` in a separate shell, or you can run it in background with `starknet-devnet &`. diff --git a/scripts/install_dev_tools.sh b/scripts/install_dev_tools.sh index 4f88173c5..c50bb52c6 100755 --- a/scripts/install_dev_tools.sh +++ b/scripts/install_dev_tools.sh @@ -37,11 +37,10 @@ if [ -z "$CAIRO_1_COMPILER_MANIFEST" ]; then fi echo "Using Cairo compiler at $CAIRO_1_COMPILER_MANIFEST" - -cargo run --bin starknet-compile \ - --manifest-path "$CAIRO_1_COMPILER_MANIFEST" \ - -- \ - --version +cargo build \ + --bin starknet-compile \ + --bin starknet-sierra-compile \ + --manifest-path "$CAIRO_1_COMPILER_MANIFEST" # install dependencies poetry install --no-ansi diff --git a/starknet_devnet/compiler.py b/starknet_devnet/compiler.py index cc96539c9..19c10d215 100644 --- a/starknet_devnet/compiler.py +++ b/starknet_devnet/compiler.py @@ -4,7 +4,8 @@ import os import subprocess import tempfile -from abc import ABC +from abc import ABC, abstractmethod +from typing import List from starkware.starknet.definitions.error_codes import StarknetErrorCode from starkware.starknet.services.api.contract_class.contract_class import ( @@ -16,6 +17,7 @@ ) from starkware.starkware_utils.error_handling import StarkException +from starknet_devnet.devnet_config import DevnetConfig from starknet_devnet.util import StarknetDevnetException @@ -52,8 +54,9 @@ def compile_contract_class(self, contract_class: ContractClass) -> CompiledClass class CustomContractClassCompiler(ContractClassCompiler): """Uses the compiler according to the compiler_manifest provided in initialization""" - def __init__(self, compiler_manifest: str): - self.compiler_manifest = compiler_manifest + @abstractmethod + def get_sierra_compiler_command(self) -> List[str]: + """Returns the shell command of the sierra compiler""" def compile_contract_class(self, contract_class: ContractClass) -> CompiledClass: with tempfile.TemporaryDirectory() as tmp_dir: @@ -66,13 +69,7 @@ def compile_contract_class(self, contract_class: ContractClass) -> CompiledClass json.dump(contract_class_dumped, tmp_file) compilation_args = [ - "cargo", - "run", - "--bin", - "starknet-sierra-compile", - "--manifest-path", - self.compiler_manifest, - "--", + *self.get_sierra_compiler_command(), "--allowed-libfuncs-list-name", "experimental_v0.1.0", "--add-pythonic-hints", @@ -92,3 +89,43 @@ def compile_contract_class(self, contract_class: ContractClass) -> CompiledClass with open(contract_casm, encoding="utf-8") as casm_file: compiled_class = CompiledClass.loads(casm_file.read()) return compiled_class + + +class ManifestContractClassCompiler(CustomContractClassCompiler): + """Sierra compiler relying on the compiler repo manifest""" + + def __init__(self, compiler_manifest: str): + super().__init__() + self._compiler_command = [ + "cargo", + "run", + "--bin", + "starknet-sierra-compile", + "--manifest-path", + compiler_manifest, + "--", + ] + + def get_sierra_compiler_command(self) -> List[str]: + return self._compiler_command + + +class BinaryContractClassCompiler(CustomContractClassCompiler): + """Sierra compiler relying on the starknet-sierra-compile binary executable""" + + def __init__(self, executable_path: str): + self._compiler_command = [executable_path] + + def get_sierra_compiler_command(self) -> List[str]: + return self._compiler_command + + +def select_compiler(config: DevnetConfig) -> ContractClassCompiler: + """Selects the compiler class according to the specification in the config object""" + if config.cairo_compiler_manifest: + return ManifestContractClassCompiler(config.cairo_compiler_manifest) + + if config.sierra_compiler_path: + return BinaryContractClassCompiler(config.sierra_compiler_path) + + return DefaultContractClassCompiler() diff --git a/starknet_devnet/devnet_config.py b/starknet_devnet/devnet_config.py index e5928602f..12949c3c6 100644 --- a/starknet_devnet/devnet_config.py +++ b/starknet_devnet/devnet_config.py @@ -210,19 +210,10 @@ def __call__(self, parser, namespace, values, option_string=None): setattr(namespace, self.dest, value) -def _parse_cairo_compiler_manifest(manifest_path: str): +def _assert_valid_compiler(command: List[str]): """Assert user machine can compile with cairo 1""" check = subprocess.run( - [ - "cargo", - "run", - "--bin", - "starknet-compile", - "--manifest-path", - manifest_path, - "--", - "--version", - ], + command, check=False, capture_output=True, ) @@ -234,9 +225,31 @@ def _parse_cairo_compiler_manifest(manifest_path: str): version_used = check.stdout.decode("utf-8") print(f"Using cairo compiler: {version_used}") + +def _parse_cairo_compiler_manifest(manifest_path: str): + command = [ + "cargo", + "run", + "--bin", + "starknet-sierra-compile", + "--manifest-path", + manifest_path, + "--", + "--version", + ] + _assert_valid_compiler(command) + return manifest_path +def _parse_sierra_compiler_path(compiler_path: str): + if not (os.path.isfile(compiler_path) and os.access(compiler_path, os.X_OK)): + sys.exit("Error: The argument of --sierra-compiler-path must be an executable") + + _assert_valid_compiler([compiler_path, "--version"]) + return compiler_path + + def parse_args(raw_args: List[str]): """ Parses CLI arguments. @@ -380,6 +393,11 @@ def parse_args(raw_args: List[str]): help="Specify the path to the manifest (Cargo.toml) of the Cairo 1.0 compiler to be used for contract recompilation; " "if omitted, the default x86-compatible compiler (from cairo-lang package) is used", ) + parser.add_argument( + "--sierra-compiler-path", + type=_parse_sierra_compiler_path, + help="Specify the path to the binary executable of starknet-sierra-compile", + ) parsed_args = parser.parse_args(raw_args) if parsed_args.dump_on and not parsed_args.dump_path: @@ -394,6 +412,11 @@ def parse_args(raw_args: List[str]): parsed_args.fork_network, parsed_args.fork_block, parsed_args.fork_retries ) + if parsed_args.cairo_compiler_manifest and parsed_args.sierra_compiler_path: + sys.exit( + "Error: Only one of {--cairo-compiler-manifest,--sierra-compiler-path} can be provided" + ) + return parsed_args @@ -421,3 +444,4 @@ def __init__(self, args: argparse.Namespace = None): self.validate_rpc_requests = not self.args.disable_rpc_request_validation self.validate_rpc_responses = not self.args.disable_rpc_response_validation self.cairo_compiler_manifest = self.args.cairo_compiler_manifest + self.sierra_compiler_path = self.args.sierra_compiler_path diff --git a/starknet_devnet/starknet_wrapper.py b/starknet_devnet/starknet_wrapper.py index a74ac6fb0..bbb97d23c 100644 --- a/starknet_devnet/starknet_wrapper.py +++ b/starknet_devnet/starknet_wrapper.py @@ -72,7 +72,7 @@ from .blocks import DevnetBlocks from .blueprints.rpc.structures.types import BlockId, Felt from .chargeable_account import ChargeableAccount -from .compiler import CustomContractClassCompiler, DefaultContractClassCompiler +from .compiler import select_compiler from .constants import ( DUMMY_STATE_ROOT, LEGACY_TX_VERSION, @@ -145,11 +145,7 @@ def __init__(self, config: DevnetConfig): self._contract_classes: Dict[int, Union[DeprecatedCompiledClass, ContractClass]] """If v2 - store sierra, otherwise store old class; needed for get_class_by_hash""" self.genesis_block_number = None - self._compiler = ( - CustomContractClassCompiler(config.cairo_compiler_manifest) - if config.cairo_compiler_manifest - else DefaultContractClassCompiler() - ) + self._compiler = select_compiler(config) if config.start_time is not None: self.set_block_time(config.start_time) diff --git a/test/test_compiler.py b/test/test_compiler.py index 32227803b..b822d9547 100644 --- a/test/test_compiler.py +++ b/test/test_compiler.py @@ -2,6 +2,7 @@ import os import subprocess +from typing import List import pytest from starkware.starknet.services.api.contract_class.contract_class import CompiledClass @@ -10,9 +11,10 @@ ) from starknet_devnet.compiler import ( + BinaryContractClassCompiler, ContractClassCompiler, - CustomContractClassCompiler, DefaultContractClassCompiler, + ManifestContractClassCompiler, ) from .account import send_declare_v2 @@ -24,17 +26,21 @@ PREDEPLOYED_ACCOUNT_PRIVATE_KEY, ) from .test_declare_v2 import assert_declare_v2_accepted, load_cairo1_contract -from .util import ( - DevnetBackgroundProc, - devnet_in_background, - read_stream, - terminate_and_wait, -) +from .util import DevnetBackgroundProc, read_stream, terminate_and_wait -SPECIFIED_MANIFEST = os.getenv("CAIRO_1_COMPILER_MANIFEST") -if not SPECIFIED_MANIFEST: +CAIRO_1_COMPILER_MANIFEST = os.getenv("CAIRO_1_COMPILER_MANIFEST") +if not CAIRO_1_COMPILER_MANIFEST: raise KeyError("CAIRO_1_COMPILER_MANIFEST env var not set") +# since the manifest file is at the root of the compiler repo, +# this allows us to get the path of the repo itself +CAIRO_1_COMPILER_REPO = os.path.dirname(CAIRO_1_COMPILER_MANIFEST) + +# assumes the artifacts were built in the repo with `cargo build --bin starknet-sierra-compile` +SIERRA_COMPILER_PATH = os.path.join( + CAIRO_1_COMPILER_REPO, "target", "debug", "starknet-sierra-compile" +) + ACTIVE_DEVNET = DevnetBackgroundProc() @@ -51,7 +57,11 @@ def run_before_and_after_test(): @pytest.mark.parametrize( "compiler", - [DefaultContractClassCompiler(), CustomContractClassCompiler(SPECIFIED_MANIFEST)], + [ + DefaultContractClassCompiler(), + ManifestContractClassCompiler(CAIRO_1_COMPILER_MANIFEST), + BinaryContractClassCompiler(SIERRA_COMPILER_PATH), + ], ) def test_contract_class_compiler_happy_path(compiler: ContractClassCompiler): """Test the class abstracting the default compiler""" @@ -64,13 +74,13 @@ def test_contract_class_compiler_happy_path(compiler: ContractClassCompiler): assert compiled == expected_compiled -@pytest.mark.parametrize("compiler_manifest", ["", "dummy-wrong"]) -def test_invalid_cairo_compiler_manifest(compiler_manifest: str): - """Test invalid cairo compiler manifest specified via CLI""" +@pytest.mark.parametrize("manifest_value", ["", "dummy-wrong"]) +def test_invalid_compiler_manifest(manifest_value: str): + """Test invalid compiler manifest specified via CLI""" execution = ACTIVE_DEVNET.start( "--cairo-compiler-manifest", - compiler_manifest, + manifest_value, stderr=subprocess.PIPE, stdout=subprocess.PIPE, ) @@ -80,25 +90,64 @@ def test_invalid_cairo_compiler_manifest(compiler_manifest: str): assert read_stream(execution.stdout) == "" -def test_valid_cairo_compiler_manifest(): - """Test valid cairo compiler manifest specified via CLI""" +@pytest.mark.parametrize("compiler_value", ["", "dummy-wrong"]) +def test_invalid_sierra_compiler(compiler_value: str): + """Test invalid sierra compiler specified via CLI""" + execution = ACTIVE_DEVNET.start( - "--cairo-compiler-manifest", - SPECIFIED_MANIFEST, + "--sierra-compiler-path", + compiler_value, stderr=subprocess.PIPE, stdout=subprocess.PIPE, ) + + assert execution.returncode != 0 + assert ( + "The argument of --sierra-compiler-path must be an executable" + in read_stream(execution.stderr) + ) + assert read_stream(execution.stdout) == "" + + +@pytest.mark.parametrize( + "cli_args", + [ + ["--cairo-compiler-manifest", CAIRO_1_COMPILER_MANIFEST], + ["--sierra-compiler-path", SIERRA_COMPILER_PATH], + ], +) +def test_valid_compiler_specification(cli_args: List[str]): + """Test valid cairo compiler specified via CLI""" + execution = ACTIVE_DEVNET.start( + *cli_args, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + terminate_and_wait(execution) assert execution.returncode == 0 - assert "Cairo compiler error" not in read_stream(execution.stderr) + + stderr = read_stream(execution.stderr) + assert "The argument of --sierra-compiler-path must be an executable" not in stderr + assert "Cairo compiler error" not in stderr assert "Using cairo compiler" in read_stream(execution.stdout) -@devnet_in_background( - *PREDEPLOY_ACCOUNT_CLI_ARGS, "--cairo-compiler-manifest", SPECIFIED_MANIFEST +@pytest.mark.usefixtures("run_devnet_in_background") +@pytest.mark.parametrize( + "run_devnet_in_background", + [ + ( + *PREDEPLOY_ACCOUNT_CLI_ARGS, + "--cairo-compiler-manifest", + CAIRO_1_COMPILER_MANIFEST, + ), + (*PREDEPLOY_ACCOUNT_CLI_ARGS, "--sierra-compiler-path", SIERRA_COMPILER_PATH), + ], + indirect=True, ) -def test_declaring_with_custom_compiler(): - """E2E test using cairo compiler specified via CLI""" +def test_declaring_with_custom_manifest(): + """E2E tests using compiler specified via CLI""" contract_class, _, compiled_class_hash = load_cairo1_contract() resp = send_declare_v2( contract_class=contract_class, @@ -107,3 +156,23 @@ def test_declaring_with_custom_compiler(): sender_key=PREDEPLOYED_ACCOUNT_PRIVATE_KEY, ) assert_declare_v2_accepted(resp) + + +def test_manifest_and_sierra_compiler_specified(): + """Should fail if both modes specified""" + execution = ACTIVE_DEVNET.start( + "--cairo-compiler-manifest", + CAIRO_1_COMPILER_MANIFEST, + "--sierra-compiler-path", + SIERRA_COMPILER_PATH, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + + terminate_and_wait(execution) + assert execution.returncode != 0 + + assert ( + "Only one of {--cairo-compiler-manifest,--sierra-compiler-path} can be provided" + in read_stream(execution.stderr) + )