From 8dff64c5e67bb1faca9ffae3ddbeca14287fb337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20T=C3=B3th?= Date: Mon, 4 May 2026 11:01:00 +0000 Subject: [PATCH 01/10] Change imports to relative --- src/skribe/contract.py | 3 +-- src/skribe/skribe.py | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/skribe/contract.py b/src/skribe/contract.py index 5ad254d..90d95f1 100644 --- a/src/skribe/contract.py +++ b/src/skribe/contract.py @@ -14,10 +14,9 @@ from pyk.kast.inner import KSort from pyk.utils import run_process, single -from skribe.simulation import call_data +from .simulation import call_data if TYPE_CHECKING: - from hypothesis.strategies import SearchStrategy diff --git a/src/skribe/skribe.py b/src/skribe/skribe.py index 23bad3f..a19662a 100644 --- a/src/skribe/skribe.py +++ b/src/skribe/skribe.py @@ -21,8 +21,7 @@ from pyk.utils import run_process from pykwasm.wasm2kast import wasm2kast -from skribe.contract import StylusContract, argument_strategy, get_arg_types, is_foundry_test, setup_method - +from .contract import StylusContract, argument_strategy, get_arg_types, is_foundry_test, setup_method from .kast.syntax import ( call_stylus, check_foundry_success, @@ -61,7 +60,6 @@ class Skribe: - definition: SkribeDefinition contract_dir: Path From 12d1af21fac20440d699170b1d1b9b870bf3207e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20T=C3=B3th?= Date: Mon, 4 May 2026 11:15:09 +0000 Subject: [PATCH 02/10] Exclude `src/tests` from source distribution --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 7e0475a..0eba732 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,9 @@ dev = [ [tool.hatch.metadata] allow-direct-references = true +[tool.hatch.build.targets.sdist] +exclude = ["src/tests/"] + [tool.isort] profile = "black" line_length = 120 From 6b06119ba4c0fbb62a198833ac2c1ee9ff57a75a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20T=C3=B3th?= Date: Mon, 4 May 2026 13:02:29 +0000 Subject: [PATCH 03/10] Pull KORE template setup from `run_test` --- src/skribe/skribe.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/skribe/skribe.py b/src/skribe/skribe.py index a19662a..3e04556 100644 --- a/src/skribe/skribe.py +++ b/src/skribe/skribe.py @@ -153,8 +153,7 @@ def call_setup(setup: bool) -> tuple[KInner, ...]: def run_test( self, - conf: KInner, - subst: dict[str, KInner], + template_pattern: Pattern, binding: Method, max_examples: int, task: FuzzTask, @@ -162,9 +161,7 @@ def run_test( """Given a configuration with a deployed test contract, fuzz over the tests for the supplied binding. Args: - conf: The template configuration. - subst: A substitution mapping such that 'Subst(subst).apply(conf)' gives the initial configuration with the - deployed contract. + template_pattern: The template KORE configuration. binding: The contract binding that specifies the test name and parameters. max_examples: The maximum number of fuzzing test cases to generate and execute. @@ -175,22 +172,12 @@ def run_test( def calldata_to_kore(data: bytes) -> Pattern: return kast_to_kore(self.definition.kdefinition, bytesToken(data), BYTES) - k_steps = [ - set_exit_code(1), - call_stylus(TEST_CALLER_ID, TEST_CONTRACT_ID, CALLDATA, 0), - check_foundry_success(), - set_exit_code(0), - ] - subst['K_CELL'] = steps_of(k_steps) - - template_config = Subst(subst).apply(conf) - template_config_kore = kast_to_kore(self.definition.kdefinition, template_config, GENERATED_TOP_CELL) template_subst = {CALLDATA_EVAR: argument_strategy(binding).map(calldata_to_kore)} task.start() fuzz( self.definition.path, - template_config_kore, + template_pattern, template_subst, check_exit_code=True, max_examples=max_examples, @@ -248,13 +235,22 @@ def deploy_and_run_contract( init_config = self.deploy_test(contract_kast, setup is not None) template_conf, init_subst = split_config_from(init_config) + k_steps = [ + set_exit_code(1), + call_stylus(TEST_CALLER_ID, TEST_CONTRACT_ID, CALLDATA, 0), + check_foundry_success(), + set_exit_code(0), + ] + init_subst['K_CELL'] = steps_of(k_steps) + template_conf = Subst(init_subst).apply(template_conf) + template_pattern = kast_to_kore(self.definition.kdefinition, template_conf, GENERATED_TOP_CELL) tests = self.select_tests(contract, id) errors: list[FuzzError] = [] with FuzzProgress(tests, max_examples) as progress: for task in progress.fuzz_tasks: try: - self.run_test(template_conf, init_subst, task.binding, max_examples, task) + self.run_test(template_pattern, task.binding, max_examples, task) except FuzzError as e: task.fail() errors.append(e) From 35186cc73bbd4845d933b9b93b2eac7a3eb7d9c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20T=C3=B3th?= Date: Mon, 4 May 2026 13:12:51 +0000 Subject: [PATCH 04/10] Extract method `create_template_pattern` --- src/skribe/skribe.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/skribe/skribe.py b/src/skribe/skribe.py index 3e04556..231d91c 100644 --- a/src/skribe/skribe.py +++ b/src/skribe/skribe.py @@ -221,7 +221,20 @@ def deploy_and_run(self, max_examples: int, id: str | None = None) -> list[FuzzE def deploy_and_run_contract( self, contract: ArbitrumContract, max_examples: int, id: str | None = None ) -> list[FuzzError]: + template_pattern = self.create_template_pattern(contract) + tests = self.select_tests(contract, id) + errors: list[FuzzError] = [] + with FuzzProgress(tests, max_examples) as progress: + for task in progress.fuzz_tasks: + try: + self.run_test(template_pattern, task.binding, max_examples, task) + except FuzzError as e: + task.fail() + errors.append(e) + + return errors + def create_template_pattern(self, contract: ArbitrumContract) -> Pattern: contract_kast: KInner if isinstance(contract, StylusContract): contract_kast = wasm2kast(BytesIO(contract.deployed_bytecode)) @@ -245,17 +258,7 @@ def deploy_and_run_contract( template_conf = Subst(init_subst).apply(template_conf) template_pattern = kast_to_kore(self.definition.kdefinition, template_conf, GENERATED_TOP_CELL) - tests = self.select_tests(contract, id) - errors: list[FuzzError] = [] - with FuzzProgress(tests, max_examples) as progress: - for task in progress.fuzz_tasks: - try: - self.run_test(template_pattern, task.binding, max_examples, task) - except FuzzError as e: - task.fail() - errors.append(e) - - return errors + return template_pattern class KometFuzzHandler(KFuzzHandler): From 9755278468b26f8b9bd302cf9b9775193ebe39db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20T=C3=B3th?= Date: Mon, 4 May 2026 13:53:53 +0000 Subject: [PATCH 05/10] Extract class `Signature` Encapsulates the minimal information necessary to fuzz a method. --- src/skribe/contract.py | 26 +++++++++++++++++++------- src/skribe/skribe.py | 4 ++-- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/skribe/contract.py b/src/skribe/contract.py index 90d95f1..34c5dfb 100644 --- a/src/skribe/contract.py +++ b/src/skribe/contract.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from functools import cached_property, partial from pathlib import Path -from typing import TYPE_CHECKING, Any, TypeAlias +from typing import TYPE_CHECKING, Any, NamedTuple, TypeAlias from eth_abi.grammar import TupleType, parse from eth_abi.tools._strategies import get_abi_strategy @@ -140,9 +140,21 @@ def get_arg_types(m: Method) -> tuple[str, ...]: return tuple(c.to_type_str() for c in parsed_arg_types.components) -def argument_strategy(m: Method) -> SearchStrategy[bytes]: - arg_types = get_arg_types(m) - input_strategies = (get_abi_strategy(arg) for arg in arg_types) - tuple_strategy = strategies.tuples(*input_strategies) - encoder = partial(call_data, m.name, arg_types) - return tuple_strategy.map(encoder) +class Signature(NamedTuple): + contract_name: str + name: str + arg_types: tuple[str, ...] + + @staticmethod + def from_method(method: Method) -> Signature: + return Signature( + contract_name=method.contract_name, + name=method.name, + arg_types=get_arg_types(method), + ) + + def argument_strategy(self) -> SearchStrategy[bytes]: + input_strategies = (get_abi_strategy(arg) for arg in self.arg_types) + tuple_strategy = strategies.tuples(*input_strategies) + encoder = partial(call_data, self.name, self.arg_types) + return tuple_strategy.map(encoder) diff --git a/src/skribe/skribe.py b/src/skribe/skribe.py index 231d91c..c4681ca 100644 --- a/src/skribe/skribe.py +++ b/src/skribe/skribe.py @@ -21,7 +21,7 @@ from pyk.utils import run_process from pykwasm.wasm2kast import wasm2kast -from .contract import StylusContract, argument_strategy, get_arg_types, is_foundry_test, setup_method +from .contract import Signature, StylusContract, get_arg_types, is_foundry_test, setup_method from .kast.syntax import ( call_stylus, check_foundry_success, @@ -172,7 +172,7 @@ def run_test( def calldata_to_kore(data: bytes) -> Pattern: return kast_to_kore(self.definition.kdefinition, bytesToken(data), BYTES) - template_subst = {CALLDATA_EVAR: argument_strategy(binding).map(calldata_to_kore)} + template_subst = {CALLDATA_EVAR: Signature.from_method(binding).argument_strategy().map(calldata_to_kore)} task.start() fuzz( From a4f11d099469f5d50dc23fe1ff2fc60e4ebc85d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20T=C3=B3th?= Date: Mon, 4 May 2026 14:15:49 +0000 Subject: [PATCH 06/10] Replace `method` by `signature` in `FuzzTask` --- src/skribe/contract.py | 4 ++++ src/skribe/progress.py | 16 ++++++++-------- src/skribe/skribe.py | 21 ++++++++++++--------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/skribe/contract.py b/src/skribe/contract.py index 34c5dfb..978db54 100644 --- a/src/skribe/contract.py +++ b/src/skribe/contract.py @@ -153,6 +153,10 @@ def from_method(method: Method) -> Signature: arg_types=get_arg_types(method), ) + @property + def qualified_name(self) -> str: + return f'{self.contract_name}.{self.name}' + def argument_strategy(self) -> SearchStrategy[bytes]: input_strategies = (get_abi_strategy(arg) for arg in self.arg_types) tuple_strategy = strategies.tuples(*input_strategies) diff --git a/src/skribe/progress.py b/src/skribe/progress.py index 128362f..6276b79 100644 --- a/src/skribe/progress.py +++ b/src/skribe/progress.py @@ -9,13 +9,13 @@ from rich.progress import TaskID - from .contract import Method + from .contract import Signature class FuzzProgress(Progress): fuzz_tasks: list[FuzzTask] - def __init__(self, bindings: Iterable[Method], max_examples: int): + def __init__(self, signatures: Iterable[Signature], max_examples: int): super().__init__( TextColumn('[progress.description]{task.description}'), BarColumn(), @@ -27,19 +27,19 @@ def __init__(self, bindings: Iterable[Method], max_examples: int): self.fuzz_tasks = [] # Add all tests to the progress display before running them - for binding in bindings: - description = f'{binding.contract_name}.{binding.name}' + for signature in signatures: + description = signature.qualified_name task_id = self.add_task(description, total=max_examples, start=False, status='Waiting') - self.fuzz_tasks.append(FuzzTask(binding, task_id, self)) + self.fuzz_tasks.append(FuzzTask(signature, task_id, self)) class FuzzTask: - binding: Method + signature: Signature task_id: TaskID progress: FuzzProgress - def __init__(self, binding: Method, task_id: TaskID, progress: FuzzProgress): - self.binding = binding + def __init__(self, signature: Signature, task_id: TaskID, progress: FuzzProgress): + self.signature = signature self.task_id = task_id self.progress = progress diff --git a/src/skribe/skribe.py b/src/skribe/skribe.py index c4681ca..fc1ed2b 100644 --- a/src/skribe/skribe.py +++ b/src/skribe/skribe.py @@ -21,7 +21,7 @@ from pyk.utils import run_process from pykwasm.wasm2kast import wasm2kast -from .contract import Signature, StylusContract, get_arg_types, is_foundry_test, setup_method +from .contract import Signature, StylusContract, is_foundry_test, setup_method from .kast.syntax import ( call_stylus, check_foundry_success, @@ -154,15 +154,15 @@ def call_setup(setup: bool) -> tuple[KInner, ...]: def run_test( self, template_pattern: Pattern, - binding: Method, + signature: Signature, max_examples: int, task: FuzzTask, ) -> None: - """Given a configuration with a deployed test contract, fuzz over the tests for the supplied binding. + """Given a configuration with a deployed test contract, fuzz over the tests for the supplied signature. Args: template_pattern: The template KORE configuration. - binding: The contract binding that specifies the test name and parameters. + signature: The signature of the test to fuzz over. max_examples: The maximum number of fuzzing test cases to generate and execute. Raises: @@ -172,7 +172,7 @@ def run_test( def calldata_to_kore(data: bytes) -> Pattern: return kast_to_kore(self.definition.kdefinition, bytesToken(data), BYTES) - template_subst = {CALLDATA_EVAR: Signature.from_method(binding).argument_strategy().map(calldata_to_kore)} + template_subst = {CALLDATA_EVAR: signature.argument_strategy().map(calldata_to_kore)} task.start() fuzz( @@ -222,12 +222,15 @@ def deploy_and_run_contract( self, contract: ArbitrumContract, max_examples: int, id: str | None = None ) -> list[FuzzError]: template_pattern = self.create_template_pattern(contract) + tests = self.select_tests(contract, id) + signatures = [Signature.from_method(test) for test in tests] + errors: list[FuzzError] = [] - with FuzzProgress(tests, max_examples) as progress: + with FuzzProgress(signatures, max_examples) as progress: for task in progress.fuzz_tasks: try: - self.run_test(template_pattern, task.binding, max_examples, task) + self.run_test(template_pattern, task.signature, max_examples, task) except FuzzError as e: task.fail() errors.append(e) @@ -291,8 +294,8 @@ def handle_failure(self, args: Mapping[EVar, Pattern]) -> None: calldata_kast = self.definition.krun.kore_to_kast(args[CALLDATA_EVAR]) assert isinstance(calldata_kast, KToken) calldata = pretty_bytes(calldata_kast) - decoded = decode(get_arg_types(self.task.binding), calldata[4:]) - description = f'{self.task.binding.contract_name}.{self.task.binding.name}' + decoded = decode(self.task.signature.arg_types, calldata[4:]) + description = self.task.signature.qualified_name raise FuzzError(description, decoded) From 8c4cb3e7385f32abad93c4bd7423cf481c9e390a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20T=C3=B3th?= Date: Mon, 4 May 2026 14:21:28 +0000 Subject: [PATCH 07/10] Make `get_arg_types` a private method --- src/skribe/contract.py | 44 +++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/skribe/contract.py b/src/skribe/contract.py index 978db54..08b8871 100644 --- a/src/skribe/contract.py +++ b/src/skribe/contract.py @@ -119,27 +119,6 @@ def is_foundry_test(ctr: EVMContract) -> bool: return False -def get_arg_types(m: Method) -> tuple[str, ...]: - """ - Return the argument type strings of a method. - - This function exists because `Method.arg_types` flattens tuple arguments, - into their component types. That flattening loses information about the - original ABI shape (for example, whether an argument was a tuple or an - array of tuples). - """ - - sig = m.signature - arg_types_from_sig = sig[sig.index('(') :] - - if arg_types_from_sig == '()': - return () - else: - parsed_arg_types = parse(arg_types_from_sig) - assert isinstance(parsed_arg_types, TupleType) - return tuple(c.to_type_str() for c in parsed_arg_types.components) - - class Signature(NamedTuple): contract_name: str name: str @@ -150,9 +129,30 @@ def from_method(method: Method) -> Signature: return Signature( contract_name=method.contract_name, name=method.name, - arg_types=get_arg_types(method), + arg_types=Signature._extract_arg_types(method), ) + @staticmethod + def _extract_arg_types(method: Method) -> tuple[str, ...]: + """ + Return the argument type strings of a method. + + This function exists because `Method.arg_types` flattens tuple arguments, + into their component types. That flattening loses information about the + original ABI shape (for example, whether an argument was a tuple or an + array of tuples). + """ + + sig = method.signature + arg_types_from_sig = sig[sig.index('(') :] + + if arg_types_from_sig == '()': + return () + else: + parsed_arg_types = parse(arg_types_from_sig) + assert isinstance(parsed_arg_types, TupleType) + return tuple(c.to_type_str() for c in parsed_arg_types.components) + @property def qualified_name(self) -> str: return f'{self.contract_name}.{self.name}' From de03c6538c43d306e580a0e55ece8ccd703cb8d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20T=C3=B3th?= Date: Mon, 4 May 2026 14:36:31 +0000 Subject: [PATCH 08/10] Extract class `FuzzSpec` --- src/skribe/skribe.py | 66 ++++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/src/skribe/skribe.py b/src/skribe/skribe.py index fc1ed2b..5db5cd4 100644 --- a/src/skribe/skribe.py +++ b/src/skribe/skribe.py @@ -5,7 +5,7 @@ from functools import cached_property from io import BytesIO from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, NamedTuple from eth_abi import decode, encode from kontrol.foundry import Foundry @@ -36,14 +36,13 @@ from .utils import RECURSION_LIMIT, PykHooks, SkribeError, subst_on_k_cell if TYPE_CHECKING: - from collections.abc import Mapping + from collections.abc import Iterable, Mapping from typing import Any from pyk.kast.inner import KInner from pyk.kore.syntax import Pattern - from skribe.contract import ArbitrumContract, Method - + from .contract import ArbitrumContract from .progress import FuzzTask from .utils import SkribeDefinition @@ -59,6 +58,25 @@ TEST_CONTRACT_ID = 0x7FA9385BE102AC3EAC297483DD6233D62B3E1496 +class FuzzSpec(NamedTuple): + template: Pattern + signatures: tuple[Signature, ...] + + @property + def dict(self) -> dict[str, Any]: + return { + 'template': self.template.text, + 'signatures': [ + { + 'contract_name': signature.contract_name, + 'name': signature.name, + 'arg_types': list(signature.arg_types), + } + for signature in self.signatures + ], + } + + class Skribe: definition: SkribeDefinition contract_dir: Path @@ -187,21 +205,6 @@ def calldata_to_kore(data: bytes) -> Pattern: ) task.end() - def select_tests(self, contract: ArbitrumContract, id: str | None) -> list[Method]: - test_methods = [] - for m in contract.methods: - if m.is_test: - test_methods.append(m) - - if id is None: - tests = test_methods - else: - tests = [b for b in test_methods if b.name == id] - if not tests: - raise KeyError(f'Test function {id!r} not found.') - - return tests - def deploy_and_run(self, max_examples: int, id: str | None = None) -> list[FuzzError]: test_contracts: list[ArbitrumContract] @@ -221,22 +224,25 @@ def deploy_and_run(self, max_examples: int, id: str | None = None) -> list[FuzzE def deploy_and_run_contract( self, contract: ArbitrumContract, max_examples: int, id: str | None = None ) -> list[FuzzError]: - template_pattern = self.create_template_pattern(contract) - - tests = self.select_tests(contract, id) - signatures = [Signature.from_method(test) for test in tests] + spec = self.create_spec(contract) + signatures = _filter_signatures(spec.signatures, id=id) errors: list[FuzzError] = [] with FuzzProgress(signatures, max_examples) as progress: for task in progress.fuzz_tasks: try: - self.run_test(template_pattern, task.signature, max_examples, task) + self.run_test(spec.template, task.signature, max_examples, task) except FuzzError as e: task.fail() errors.append(e) return errors + def create_spec(self, contract: ArbitrumContract) -> FuzzSpec: + template = self.create_template_pattern(contract) + signatures = tuple(Signature.from_method(method) for method in contract.methods if method.is_test) + return FuzzSpec(template=template, signatures=signatures) + def create_template_pattern(self, contract: ArbitrumContract) -> Pattern: contract_kast: KInner if isinstance(contract, StylusContract): @@ -313,3 +319,15 @@ def __init__(self, description: str, counterexample: tuple[KInner, ...]): class InitializationError(SkribeError): ... + + +def _filter_signatures(signatures: Iterable[Signature], id: str | None) -> list[Signature]: + if id is None: + return list(signatures) + + else: + res = [sig for sig in signatures if sig.name == id] + if res: + raise KeyError(f'Test function {id!r} not found.') + + return res From 2b588b8443efaddc4ec612e1385334b4d8683691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20T=C3=B3th?= Date: Wed, 6 May 2026 09:50:50 +0000 Subject: [PATCH 09/10] Remove exception from `_filter_signatures` --- src/skribe/skribe.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/skribe/skribe.py b/src/skribe/skribe.py index 5db5cd4..9f2b842 100644 --- a/src/skribe/skribe.py +++ b/src/skribe/skribe.py @@ -325,9 +325,4 @@ def _filter_signatures(signatures: Iterable[Signature], id: str | None) -> list[ if id is None: return list(signatures) - else: - res = [sig for sig in signatures if sig.name == id] - if res: - raise KeyError(f'Test function {id!r} not found.') - - return res + return [sig for sig in signatures if sig.name == id] From 14065de07c56c8ce000887d0238add8d85bdc9dc Mon Sep 17 00:00:00 2001 From: devops Date: Wed, 6 May 2026 10:11:16 +0000 Subject: [PATCH 10/10] Set Version: 0.1.29 --- package/version | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package/version b/package/version index baec65a..5ef49d2 100644 --- a/package/version +++ b/package/version @@ -1 +1 @@ -0.1.28 +0.1.29 diff --git a/pyproject.toml b/pyproject.toml index 0eba732..8c26fa8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "skribe" -version = "0.1.28" +version = "0.1.29" description = "Property testing for Stylus smart contracts" readme = "README.md" requires-python = "~=3.10" diff --git a/uv.lock b/uv.lock index da349bf..4f1472d 100644 --- a/uv.lock +++ b/uv.lock @@ -1815,7 +1815,7 @@ wheels = [ [[package]] name = "skribe" -version = "0.1.28" +version = "0.1.29" source = { editable = "." } dependencies = [ { name = "kontrol" },