Skip to content

Commit eb61949

Browse files
authored
Add cheatcodes that generate symbolic variables with custom names (#902)
* add #rename rule and new cheatcodes * update custom_step * Add tests * format * formatting * remove prints * add test to end to end * remove unimplemented random*(*,string) cheatcodes * use free_vars
1 parent d8dad9e commit eb61949

10 files changed

+4260
-22
lines changed

src/kontrol/foundry.py

Lines changed: 92 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,19 @@
2020
from kevm_pyk.kevm import KEVM, KEVMNodePrinter, KEVMSemantics
2121
from kevm_pyk.utils import byte_offset_to_lines, legacy_explore, print_failure_info, print_model
2222
from pyk.cterm import CTerm
23-
from pyk.kast.inner import KApply, KInner, KSort, KToken, KVariable
24-
from pyk.kast.manip import cell_label_to_var_name, collect, extract_lhs, flatten_label, minimize_term, top_down
23+
from pyk.kast.inner import KApply, KInner, KSequence, KSort, KToken, KVariable, Subst
24+
from pyk.kast.manip import (
25+
cell_label_to_var_name,
26+
collect,
27+
extract_lhs,
28+
flatten_label,
29+
minimize_term,
30+
set_cell,
31+
top_down,
32+
)
2533
from pyk.kast.outer import KDefinition, KFlatModule, KImport, KRequire
2634
from pyk.kcfg import KCFG
35+
from pyk.kcfg.kcfg import Step
2736
from pyk.kcfg.minimize import KCFGMinimizer
2837
from pyk.prelude.bytes import bytesToken
2938
from pyk.prelude.collections import map_empty
@@ -44,6 +53,7 @@
4453
_read_digest_file,
4554
append_to_file,
4655
empty_lemmas_file_contents,
56+
ensure_name_is_unique,
4757
foundry_toml_extra_contents,
4858
kontrol_file_contents,
4959
kontrol_toml_file_contents,
@@ -55,8 +65,10 @@
5565
from collections.abc import Iterable
5666
from typing import Any, Final
5767

68+
from pyk.cterm import CTermSymbolic
5869
from pyk.kast.outer import KAst
5970
from pyk.kcfg.kcfg import NodeIdLike
71+
from pyk.kcfg.semantics import KCFGExtendResult
6072
from pyk.kcfg.tui import KCFGElem
6173
from pyk.proof.implies import RefutationProof
6274
from pyk.proof.show import NodePrinter
@@ -82,6 +94,77 @@
8294
_LOGGER: Final = logging.getLogger(__name__)
8395

8496

97+
class KontrolSemantics(KEVMSemantics):
98+
99+
@staticmethod
100+
def cut_point_rules(
101+
break_on_jumpi: bool,
102+
break_on_jump: bool,
103+
break_on_calls: bool,
104+
break_on_storage: bool,
105+
break_on_basic_blocks: bool,
106+
break_on_load_program: bool,
107+
) -> list[str]:
108+
return ['FOUNDRY-CHEAT-CODES.rename'] + KEVMSemantics.cut_point_rules(
109+
break_on_jumpi,
110+
break_on_jump,
111+
break_on_calls,
112+
break_on_storage,
113+
break_on_basic_blocks,
114+
break_on_load_program,
115+
)
116+
117+
def _check_rename_pattern(self, cterm: CTerm) -> bool:
118+
"""Given a CTerm, check if the rule 'FOUNDRY-CHEAT-CODES.rename' is at the top of the K_CELL.
119+
120+
:param cterm: The CTerm representing the current state of the proof node.
121+
:return: `True` if the pattern matches and a custom step can be made; `False` otherwise.
122+
"""
123+
abstract_pattern = KSequence(
124+
[
125+
KApply('foundry_rename', [KVariable('###RENAME_TARGET'), KVariable('###NEW_NAME')]),
126+
KVariable('###CONTINUATION'),
127+
]
128+
)
129+
self._cached_subst = abstract_pattern.match(cterm.cell('K_CELL'))
130+
return self._cached_subst is not None
131+
132+
def _exec_rename_custom_step(self, cterm: CTerm) -> KCFGExtendResult | None:
133+
subst = self._cached_subst
134+
assert subst is not None
135+
136+
# Extract the target var and new name from the substitution
137+
target_var = subst['###RENAME_TARGET']
138+
name_token = subst['###NEW_NAME']
139+
assert type(target_var) is KVariable
140+
assert type(name_token) is KToken
141+
142+
# Ensure the name is unique
143+
name_str = name_token.token[1:-1]
144+
if len(name_str) == 0:
145+
_LOGGER.warning('Name of symbolic variable cannot be empty. Reverting to the default name.')
146+
return None
147+
name = ensure_name_is_unique(name_str, cterm)
148+
149+
# Replace var in configuration and constraints
150+
rename_subst = Subst({target_var.name: KVariable(name, target_var.sort)})
151+
config = rename_subst(cterm.config)
152+
constraints = [rename_subst(constraint) for constraint in cterm.constraints]
153+
new_cterm = CTerm.from_kast(set_cell(config, 'K_CELL', KSequence(subst['###CONTINUATION'])))
154+
155+
_LOGGER.info(f'Renaming {target_var.name} to {name}')
156+
return Step(CTerm(new_cterm.config, constraints), 1, (), ['foundry_rename'], cut=True)
157+
158+
def custom_step(self, cterm: CTerm, _cterm_symbolic: CTermSymbolic) -> KCFGExtendResult | None:
159+
if self._check_rename_pattern(cterm):
160+
return self._exec_rename_custom_step(cterm)
161+
else:
162+
return super().custom_step(cterm, _cterm_symbolic)
163+
164+
def can_make_custom_step(self, cterm: CTerm) -> bool:
165+
return self._check_rename_pattern(cterm) or super().can_make_custom_step(cterm)
166+
167+
85168
class FoundryKEVM(KEVM):
86169
foundry: Foundry
87170

@@ -841,7 +924,7 @@ def foundry_show(
841924
if options.failure_info:
842925
with legacy_explore(
843926
foundry.kevm,
844-
kcfg_semantics=KEVMSemantics(),
927+
kcfg_semantics=KontrolSemantics(),
845928
id=test_id,
846929
smt_timeout=options.smt_timeout,
847930
smt_retry_limit=options.smt_retry_limit,
@@ -908,7 +991,7 @@ def _collect_klabel(_k: KInner) -> None:
908991
]
909992
sentences = [sent for sent in sentences if not _contains_foundry_klabel(sent.body)]
910993
sentences = [
911-
sent for sent in sentences if not KEVMSemantics().is_terminal(CTerm.from_kast(extract_lhs(sent.body)))
994+
sent for sent in sentences if not KontrolSemantics().is_terminal(CTerm.from_kast(extract_lhs(sent.body)))
912995
]
913996
if len(sentences) == 0:
914997
_LOGGER.warning(f'No claims or rules retained for proof {proof.id}')
@@ -1098,7 +1181,7 @@ def foundry_simplify_node(
10981181

10991182
with legacy_explore(
11001183
foundry.kevm,
1101-
kcfg_semantics=KEVMSemantics(),
1184+
kcfg_semantics=KontrolSemantics(),
11021185
id=apr_proof.id,
11031186
bug_report=options.bug_report,
11041187
kore_rpc_command=kore_rpc_command,
@@ -1146,7 +1229,7 @@ def check_cells_equal(cell: str, nodes: Iterable[KCFG.Node]) -> bool:
11461229
check_cells_ne = [check_cell for check_cell in check_cells if not check_cells_equal(check_cell, nodes)]
11471230

11481231
if check_cells_ne:
1149-
if not all(KEVMSemantics().same_loop(nodes[0].cterm, nd.cterm) for nd in nodes):
1232+
if not all(KontrolSemantics().same_loop(nodes[0].cterm, nd.cterm) for nd in nodes):
11501233
raise ValueError(f'Nodes {options.nodes} cannot be merged because they differ in: {check_cells_ne}')
11511234

11521235
anti_unification = nodes[0].cterm
@@ -1186,7 +1269,7 @@ def foundry_step_node(
11861269

11871270
with legacy_explore(
11881271
foundry.kevm,
1189-
kcfg_semantics=KEVMSemantics(),
1272+
kcfg_semantics=KontrolSemantics(),
11901273
id=apr_proof.id,
11911274
bug_report=options.bug_report,
11921275
kore_rpc_command=kore_rpc_command,
@@ -1262,7 +1345,7 @@ def foundry_section_edge(
12621345

12631346
with legacy_explore(
12641347
foundry.kevm,
1265-
kcfg_semantics=KEVMSemantics(),
1348+
kcfg_semantics=KontrolSemantics(),
12661349
id=apr_proof.id,
12671350
bug_report=options.bug_report,
12681351
kore_rpc_command=kore_rpc_command,
@@ -1313,7 +1396,7 @@ def foundry_get_model(
13131396

13141397
with legacy_explore(
13151398
foundry.kevm,
1316-
kcfg_semantics=KEVMSemantics(),
1399+
kcfg_semantics=KontrolSemantics(),
13171400
id=proof.id,
13181401
bug_report=options.bug_report,
13191402
kore_rpc_command=kore_rpc_command,

src/kontrol/kdist/cheatcodes.md

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,10 @@ This rule then takes the address using `#asWord(#range(ARGS, 0, 32))` and makes
339339
<k> #cheatcode_call SELECTOR ARGS => #loadAccount #asWord(ARGS) ~> #setSymbolicStorage #asWord(ARGS) ... </k>
340340
requires SELECTOR ==Int selector ( "symbolicStorage(address)" )
341341
orBool SELECTOR ==Int selector ( "setArbitraryStorage(address)")
342+
343+
rule [cheatcode.call.withName.symbolicStorage]:
344+
<k> #cheatcode_call SELECTOR ARGS => #loadAccount #asWord(#range(ARGS,0,32)) ~> #setSymbolicStorage #asWord(#range(ARGS,0,32)) Bytes2String(#range(ARGS, 96, #asWord(#range(ARGS, 64, 32)))) ... </k>
345+
requires SELECTOR ==Int selector ( "symbolicStorage(address,string)" )
342346
```
343347

344348
#### `copyStorage` - Copies the storage of one account into another.
@@ -376,6 +380,14 @@ This rule returns a symbolic integer of up to the bit width that was sent as an
376380
andBool 0 <Int #asWord(ARGS) andBool #asWord(ARGS) <=Int 32
377381
ensures 0 <=Int ?WORD andBool ?WORD <Int 2 ^Int (8 *Int #asWord(ARGS))
378382
[preserves-definedness]
383+
384+
rule [cheatcode.call.withName.freshUInt]:
385+
<k> #cheatcode_call SELECTOR ARGS => #rename(?WORD, Bytes2String(#range(ARGS, 96, #asWord(#range(ARGS, 64, 32))))) ... </k>
386+
<output> _ => #buf(32, ?WORD) </output>
387+
requires SELECTOR ==Int selector ( "freshUInt(uint8,string)" )
388+
andBool 0 <Int #asWord(#range(ARGS, 0, 32)) andBool #asWord(#range(ARGS, 0, 32)) <=Int 32
389+
ensures 0 <=Int ?WORD andBool ?WORD <Int 2 ^Int (8 *Int #asWord(#range(ARGS, 0, 32)))
390+
[preserves-definedness]
379391
```
380392

381393
#### `randomUint` - Returns a single symbolic unsigned integer of a given size.
@@ -441,6 +453,13 @@ This rule returns a symbolic boolean value being either 0 (false) or 1 (true).
441453
orBool SELECTOR ==Int selector ( "randomBool()" )
442454
ensures #rangeBool(?WORD)
443455
[preserves-definedness]
456+
457+
rule [cheatcode.call.withName.freshBool]:
458+
<k> #cheatcode_call SELECTOR ARGS => #rename(?WORD, Bytes2String(#range(ARGS, 64, #asWord(#range(ARGS, 32, 32))))) ... </k>
459+
<output> _ => #buf(32, ?WORD) </output>
460+
requires SELECTOR ==Int selector ( "freshBool(string)" )
461+
ensures #rangeBool(?WORD)
462+
[preserves-definedness]
444463
```
445464

446465
#### `freshBytes` - Returns a fully symbolic byte array value of the given length.
@@ -466,6 +485,16 @@ This rule returns a fully symbolic byte array value of the given length.
466485
orBool SELECTOR ==Int selector ( "randomBytes(uint256)" )
467486
ensures lengthBytes(?BYTES) ==Int #asWord(ARGS)
468487
[preserves-definedness]
488+
489+
rule [cheatcode.call.withName.freshBytes]:
490+
<k> #cheatcode_call SELECTOR ARGS => #rename(?BYTES, Bytes2String(#range(ARGS, 96, #asWord(#range(ARGS, 64, 32))))) ... </k>
491+
<output> _ =>
492+
#buf(32, 32) +Bytes #buf(32, #asWord(#range(ARGS, 0, 32))) +Bytes ?BYTES
493+
+Bytes #buf ( ( ( notMaxUInt5 &Int ( #asWord(#range(ARGS, 0, 32)) +Int maxUInt5 ) ) -Int #asWord(#range(ARGS, 0, 32)) ) , 0 )
494+
</output>
495+
requires SELECTOR ==Int selector ( "freshBytes(uint256,string)" )
496+
ensures lengthBytes(?BYTES) ==Int #asWord(#range(ARGS, 0, 32))
497+
[preserves-definedness]
469498
```
470499

471500
This rule returns a fully symbolic byte array value of length 4.
@@ -512,6 +541,13 @@ This rule returns a symbolic address value.
512541
orBool SELECTOR ==Int selector ( "randomAddress()" )
513542
ensures #rangeAddress(?WORD) andBool ?WORD =/=Int #address(FoundryTest) andBool ?WORD =/=Int #address(FoundryCheat)
514543
[preserves-definedness]
544+
545+
rule [foundry.call.withName.freshAddress]:
546+
<k> #cheatcode_call SELECTOR ARGS => #rename(?WORD, Bytes2String(#range(ARGS, 64, #asWord(#range(ARGS, 32, 32))))) ... </k>
547+
<output> _ => #buf(32, ?WORD) </output>
548+
requires SELECTOR ==Int selector ( "freshAddress(string)" )
549+
ensures #rangeAddress(?WORD) andBool ?WORD =/=Int #address(FoundryTest) andBool ?WORD =/=Int #address(FoundryCheat)
550+
[preserves-definedness]
515551
```
516552

517553
Expecting the next call to revert
@@ -1109,6 +1145,15 @@ Mock functions
11091145
Utils
11101146
-----
11111147

1148+
- Defining a new production `#rename` for all the types for which we generate symbolic values.
1149+
We don't care about the values because they will be processed in the `custom_step` function in Python.
1150+
1151+
```k
1152+
syntax RenameTarget ::= Int | Bytes | Map
1153+
syntax KItem ::= #rename ( RenameTarget , String ) [symbol(foundry_rename)]
1154+
// ---------------------------------------------------------------------------
1155+
rule [rename]: <k> #rename(_,_) => .K ... </k>
1156+
```
11121157
- `#loadAccount ACCT` creates a new, empty account for `ACCT` if it does not already exist. Otherwise, it has no effect.
11131158

11141159
```k
@@ -1202,7 +1247,9 @@ Utils
12021247
- `#setSymbolicStorage ACCTID` takes a given account and makes its storage fully symbolic.
12031248

12041249
```k
1205-
syntax KItem ::= "#setSymbolicStorage" Int [symbol(foundry_setSymbolicStorage)]
1250+
syntax KItem ::= "#setSymbolicStorage" Int [symbol(foundry_setSymbolicStorage)]
1251+
| "#setSymbolicStorage" Int String [symbol(foundry_setSymbolicStorageWithName)]
1252+
// ----------------------------------------------------------------------------------------------
12061253
```
12071254

12081255
```{.k .symbolic}
@@ -1213,6 +1260,14 @@ Utils
12131260
<origStorage> _ => ?STORAGE </origStorage>
12141261
...
12151262
</account>
1263+
1264+
rule <k> #setSymbolicStorage ACCTID NAME => #rename(?STORAGE, NAME) ... </k>
1265+
<account>
1266+
<acctID> ACCTID </acctID>
1267+
<storage> _ => ?STORAGE </storage>
1268+
<origStorage> _ => ?STORAGE </origStorage>
1269+
...
1270+
</account>
12161271
```
12171272

12181273
- `#copyStorage ACCTFROM ACCTTO` copies the storage of ACCTFROM to be the storage of ACCTTO
@@ -1669,18 +1724,23 @@ Selectors for **implemented** cheat code functions.
16691724
rule ( selector ( "expectEmit(bool,bool,bool,bool,address)" ) => 2176505587 )
16701725
rule ( selector ( "sign(uint256,bytes32)" ) => 3812747940 )
16711726
rule ( selector ( "symbolicStorage(address)" ) => 769677742 )
1727+
rule ( selector ( "symbolicStorage(address,string)" ) => 745143816 )
16721728
rule ( selector ( "setArbitraryStorage(address)" ) => 3781367863 )
16731729
rule ( selector ( "freshUInt(uint8)" ) => 625253732 )
1730+
rule ( selector ( "freshUInt(uint8,string)" ) => 1530912521 )
16741731
rule ( selector ( "randomUint(uint256)" ) => 3481396892 )
16751732
rule ( selector ( "randomUint()" ) => 621954864 )
16761733
rule ( selector ( "randomUint(uint256,uint256)" ) => 3592095003 )
16771734
rule ( selector ( "freshBool()" ) => 2935720297 )
1735+
rule ( selector ( "freshBool(string)" ) => 525694724 )
16781736
rule ( selector ( "randomBool()" ) => 3451987645 )
16791737
rule ( selector ( "freshBytes(uint256)" ) => 1389402351 )
1738+
rule ( selector ( "freshBytes(uint256,string)" ) => 390682600 )
16801739
rule ( selector ( "randomBytes(uint256)" ) => 1818047145 )
16811740
rule ( selector ( "randomBytes4()" ) => 2608649593 )
16821741
rule ( selector ( "randomBytes8()" ) => 77050021 )
16831742
rule ( selector ( "freshAddress()" ) => 2363359817 )
1743+
rule ( selector ( "freshAddress(string)" ) => 1202084987 )
16841744
rule ( selector ( "randomAddress()" ) => 3586058741 )
16851745
rule ( selector ( "prank(address)" ) => 3395723175 )
16861746
rule ( selector ( "prank(address,address)" ) => 1206193358 )

src/kontrol/prove.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from typing import TYPE_CHECKING, Any, ContextManager, NamedTuple
1212

1313
from kevm_pyk.cli import EVMChainOptions
14-
from kevm_pyk.kevm import KEVM, KEVMSemantics, _process_jumpdests
14+
from kevm_pyk.kevm import KEVM, _process_jumpdests
1515
from kevm_pyk.utils import KDefinition__expand_macros, abstract_cell_vars, run_prover
1616
from multiprocess.pool import Pool # type: ignore
1717
from pyk.cterm import CTerm, CTermSymbolic
@@ -35,7 +35,7 @@
3535
from pyk.utils import hash_str, run_process_2, single, unique
3636
from rich.progress import Progress, SpinnerColumn, TaskID, TextColumn, TimeElapsedColumn
3737

38-
from .foundry import Foundry, foundry_to_xml
38+
from .foundry import Foundry, KontrolSemantics, foundry_to_xml
3939
from .hevm import Hevm
4040
from .options import ConfigType, TraceOptions
4141
from .solc_to_k import Contract, hex_string_to_int
@@ -381,7 +381,7 @@ def create_kcfg_explore() -> KCFGExplore:
381381
)
382382
return KCFGExplore(
383383
cterm_symbolic,
384-
kcfg_semantics=KEVMSemantics(auto_abstract_gas=options.auto_abstract_gas),
384+
kcfg_semantics=KontrolSemantics(auto_abstract_gas=options.auto_abstract_gas),
385385
id=test.id,
386386
)
387387

@@ -425,7 +425,7 @@ def create_kcfg_explore() -> KCFGExplore:
425425
}
426426
),
427427
)
428-
cut_point_rules = KEVMSemantics.cut_point_rules(
428+
cut_point_rules = KontrolSemantics.cut_point_rules(
429429
options.break_on_jumpi,
430430
options.break_on_jump,
431431
options.break_on_calls,
@@ -465,7 +465,7 @@ def create_kcfg_explore() -> KCFGExplore:
465465
max_depth=options.max_depth,
466466
max_iterations=options.max_iterations,
467467
cut_point_rules=cut_point_rules,
468-
terminal_rules=KEVMSemantics.terminal_rules(options.break_every_step),
468+
terminal_rules=KontrolSemantics.terminal_rules(options.break_every_step),
469469
counterexample_info=options.counterexample_info,
470470
max_frontier_parallel=options.max_frontier_parallel,
471471
fail_fast=options.fail_fast,

src/kontrol/utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
if TYPE_CHECKING:
1212
from typing import Final
13+
from pyk.cterm import CTerm
1314
from argparse import Namespace
1415

1516
import os
@@ -25,6 +26,21 @@
2526
_LOGGER: Final = logging.getLogger(__name__)
2627

2728

29+
def ensure_name_is_unique(name: str, cterm: CTerm) -> str:
30+
"""Ensure that a given name for a KVariable is unique within the context of a CTerm.
31+
32+
:param name: name of a KVariable
33+
:param cterm: cterm
34+
:return: Returns the name if it's not used, otherwise appends a suffix.
35+
:rtype: str
36+
"""
37+
if name not in cterm.free_vars:
38+
return name
39+
40+
index = next(i for i in range(len(cterm.free_vars) + 1) if f'{name}_{i}' not in cterm.free_vars)
41+
return f'{name}_{index}'
42+
43+
2844
def check_k_version() -> None:
2945
expected_k_version = KVersion.parse(f'v{pyk.__version__}')
3046
actual_k_version = k_version()

src/tests/integration/test-data/end-to-end-prove-all

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
CounterTest.test_Increment()
2+
RandomVarTest.test_custom_names()
23
RandomVarTest.test_randomBool()
34
RandomVarTest.test_randomAddress()
45
RandomVarTest.test_randomUint()

0 commit comments

Comments
 (0)