Skip to content

Commit 7d496d3

Browse files
anvacaruPetar MaksimovicpalinatolmachPetarMax
authored
Implement kevm.forgetBranch cheatcode (#899)
* draft FOUNDRYSemantics * forgetBranch * add mlEqualsTrue * minor corrections * add simplification step * formatting * add back not equal * rename FOUNDRYSemantics to KontrolSemantics * checking for negation as well * correcting indentation * expanding functionality * heuristic simplifications * further refinement * refactoring _exec_forget_custom_step * add show test * fix test * update expected output --------- Co-authored-by: Petar Maksimovic <[email protected]> Co-authored-by: Palina <[email protected]> Co-authored-by: Petar Maksimović <[email protected]>
1 parent e35b4a5 commit 7d496d3

8 files changed

+1555
-36
lines changed

src/kontrol/foundry.py

Lines changed: 138 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
collect,
2727
extract_lhs,
2828
flatten_label,
29+
free_vars,
2930
minimize_term,
3031
set_cell,
3132
top_down,
@@ -34,9 +35,10 @@
3435
from pyk.kcfg import KCFG
3536
from pyk.kcfg.kcfg import Step
3637
from pyk.kcfg.minimize import KCFGMinimizer
38+
from pyk.kdist import kdist
3739
from pyk.prelude.bytes import bytesToken
3840
from pyk.prelude.collections import map_empty
39-
from pyk.prelude.k import DOTS
41+
from pyk.prelude.k import DOTS, GENERATED_TOP_CELL
4042
from pyk.prelude.kbool import notBool
4143
from pyk.prelude.kint import INT, intToken
4244
from pyk.prelude.ml import mlEqualsFalse, mlEqualsTrue
@@ -103,7 +105,7 @@ def cut_point_rules(
103105
break_on_basic_blocks: bool,
104106
break_on_load_program: bool,
105107
) -> list[str]:
106-
return ['FOUNDRY-CHEAT-CODES.rename'] + KEVMSemantics.cut_point_rules(
108+
return ['FOUNDRY-CHEAT-CODES.rename', 'FOUNDRY-ACCOUNTS.forget'] + KEVMSemantics.cut_point_rules(
107109
break_on_jumpi,
108110
break_on_jump,
109111
break_on_calls,
@@ -153,14 +155,145 @@ def _exec_rename_custom_step(self, cterm: CTerm) -> KCFGExtendResult | None:
153155
_LOGGER.info(f'Renaming {target_var.name} to {name}')
154156
return Step(CTerm(new_cterm.config, constraints), 1, (), ['foundry_rename'], cut=True)
155157

156-
def custom_step(self, cterm: CTerm, _cterm_symbolic: CTermSymbolic) -> KCFGExtendResult | None:
158+
def _check_forget_pattern(self, cterm: CTerm) -> bool:
159+
"""Given a CTerm, check if the rule 'FOUNDRY-ACCOUNTS.forget' is at the top of the K_CELL.
160+
This method checks if the 'FOUNDRY-ACCOUNTS.forget' rule is at the top of the `K_CELL` in the given `cterm`.
161+
If the rule matches, the resulting substitution is cached in `_cached_subst` for later use in `custom_step`
162+
:param cterm: The CTerm representing the current state of the proof node.
163+
:return: `True` if the pattern matches and a custom step can be made; `False` otherwise.
164+
"""
165+
abstract_pattern = KSequence(
166+
[
167+
KApply('cheatcode_forget', [KVariable('###TERM1'), KVariable('###OPERATOR'), KVariable('###TERM2')]),
168+
KVariable('###CONTINUATION'),
169+
]
170+
)
171+
self._cached_subst = abstract_pattern.match(cterm.cell('K_CELL'))
172+
return self._cached_subst is not None
173+
174+
def _exec_forget_custom_step(self, cterm: CTerm, cterm_symbolic: CTermSymbolic) -> KCFGExtendResult | None:
175+
"""Remove the constraint at the top of K_CELL of a given CTerm from its path constraints,
176+
as part of the 'FOUNDRY-ACCOUNTS.forget' cut-rule.
177+
:param cterm: CTerm representing a proof node
178+
:param cterm_symbolic: CTermSymbolic instance
179+
:return: A Step of depth 1 carrying a new configuration in which the constraint is consumed from the top
180+
of the K cell and is removed from the initial path constraints if it existed, together with
181+
information that the `cheatcode_forget` rule has been applied.
182+
"""
183+
184+
def _find_constraints_to_keep(cterm: CTerm, constraint_vars: frozenset[str]) -> set[KInner]:
185+
range_patterns: list[KInner] = [
186+
mlEqualsTrue(KApply('_<Int_', KVariable('###VARL', INT), KVariable('###VARR', INT))),
187+
mlEqualsTrue(KApply('_<=Int_', KVariable('###VARL', INT), KVariable('###VARR', INT))),
188+
mlEqualsTrue(notBool(KApply('_==Int_', KVariable('###VARL', INT), KVariable('###VARR', INT)))),
189+
]
190+
constraints_to_keep: set[KInner] = set()
191+
for constraint in cterm.constraints:
192+
for pattern in range_patterns:
193+
subst_rcp = pattern.match(constraint)
194+
if subst_rcp is not None and (
195+
(
196+
type(subst_rcp['###VARL']) is KVariable
197+
and subst_rcp['###VARL'].name in constraint_vars
198+
and type(subst_rcp['###VARR']) is KToken
199+
)
200+
or (
201+
type(subst_rcp['###VARR']) is KVariable
202+
and subst_rcp['###VARR'].name in constraint_vars
203+
and type(subst_rcp['###VARL']) is KToken
204+
)
205+
):
206+
constraints_to_keep.add(constraint)
207+
break
208+
return constraints_to_keep
209+
210+
def _filter_constraints_by_simplification(
211+
cterm_symbolic: CTermSymbolic,
212+
initial_cterm: CTerm,
213+
constraints_to_remove: list[KInner],
214+
constraints_to_keep: set[KInner],
215+
constraints: set[KInner],
216+
empty_config: CTerm,
217+
) -> set[KInner]:
218+
for constraint_variant in constraints_to_remove:
219+
simplification_cterm = initial_cterm.add_constraint(constraint_variant)
220+
result_cterm, _ = cterm_symbolic.simplify(simplification_cterm)
221+
# Extract constraints that appear after simplification but are not in the 'to keep' set
222+
result_constraints = set(result_cterm.constraints).difference(constraints_to_keep)
223+
224+
if len(result_constraints) == 1:
225+
target_constraint = single(result_constraints)
226+
if target_constraint in constraints:
227+
_LOGGER.info(f'forgetBranch: removing constraint: {target_constraint}')
228+
constraints.remove(target_constraint)
229+
break
230+
else:
231+
_LOGGER.info(f'forgetBranch: constraint: {target_constraint} not found in current constraints')
232+
else:
233+
# If no constraints or multiple constraints appear, log this scenario.
234+
if len(result_constraints) == 0:
235+
_LOGGER.info(f'forgetBranch: constraint {constraint_variant} entailed by remaining constraints')
236+
result_cterm, _ = cterm_symbolic.simplify(CTerm(empty_config.config, [constraint_variant]))
237+
if len(result_cterm.constraints) == 1:
238+
to_remove = single(result_cterm.constraints)
239+
if to_remove in constraints:
240+
_LOGGER.info(f'forgetBranch: removing constraint: {to_remove}')
241+
constraints.remove(to_remove)
242+
else:
243+
_LOGGER.info(
244+
f'forgetBranch: more than one constraint found after simplification and removal:\n{result_constraints}'
245+
)
246+
return constraints
247+
248+
_operators = ['_==Int_', '_=/=Int_', '_<=Int_', '_<Int_', '_>=Int_', '_>Int_']
249+
subst = self._cached_subst
250+
assert subst is not None
251+
# Extract the terms and operator from the substitution
252+
fst_term = subst['###TERM1']
253+
snd_term = subst['###TERM2']
254+
operator = subst['###OPERATOR']
255+
assert isinstance(operator, KToken)
256+
# Construct the positive and negative constraints
257+
pos_constraint = mlEqualsTrue(KApply(_operators[int(operator.token)], fst_term, snd_term))
258+
neg_constraint = mlEqualsTrue(notBool(KApply(_operators[int(operator.token)], fst_term, snd_term)))
259+
# To be able to better simplify, we maintain range constraints on the variables present in the constraint
260+
constraint_vars: frozenset[str] = free_vars(fst_term).union(free_vars(snd_term))
261+
constraints_to_keep: set[KInner] = _find_constraints_to_keep(cterm, constraint_vars)
262+
263+
# Set up initial configuration for constraint simplification, and simplify it to get all
264+
# of the kept constraints in the form in which they will appear after constraint simplification
265+
kevm = KEVM(kdist.get('kontrol.foundry'))
266+
empty_config: CTerm = CTerm.from_kast(kevm.definition.empty_config(GENERATED_TOP_CELL))
267+
initial_cterm, _ = cterm_symbolic.simplify(CTerm(empty_config.config, constraints_to_keep))
268+
constraints_to_keep = set(initial_cterm.constraints)
269+
270+
# Simplify in the presence of constraints to keep, then remove the constraints to keep to
271+
# reveal simplified constraint, then remove if present in original constraints
272+
new_constraints: set[KInner] = _filter_constraints_by_simplification(
273+
cterm_symbolic=cterm_symbolic,
274+
initial_cterm=initial_cterm,
275+
constraints_to_remove=[pos_constraint, neg_constraint],
276+
constraints_to_keep=constraints_to_keep,
277+
constraints=set(cterm.constraints),
278+
empty_config=empty_config,
279+
)
280+
281+
# Update the K_CELL with the continuation
282+
new_cterm = CTerm.from_kast(set_cell(cterm.kast, 'K_CELL', KSequence(subst['###CONTINUATION'])))
283+
return Step(CTerm(new_cterm.config, new_constraints), 1, (), ['cheatcode_forget'], cut=True)
284+
285+
def custom_step(self, cterm: CTerm, cterm_symbolic: CTermSymbolic) -> KCFGExtendResult | None:
157286
if self._check_rename_pattern(cterm):
158287
return self._exec_rename_custom_step(cterm)
288+
elif self._check_forget_pattern(cterm):
289+
return self._exec_forget_custom_step(cterm, cterm_symbolic)
159290
else:
160-
return super().custom_step(cterm, _cterm_symbolic)
291+
return super().custom_step(cterm, cterm_symbolic)
161292

162293
def can_make_custom_step(self, cterm: CTerm) -> bool:
163-
return self._check_rename_pattern(cterm) or super().can_make_custom_step(cterm)
294+
return any(
295+
[self._check_rename_pattern(cterm), self._check_forget_pattern(cterm), super().can_make_custom_step(cterm)]
296+
)
164297

165298

166299
class FoundryKEVM(KEVM):

src/kontrol/kdist/cheatcodes.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,26 @@ Mock functions
11421142
[priority(30)]
11431143
```
11441144

1145+
1146+
Abstraction functions
1147+
---------------------
1148+
1149+
#### `forgetBranch` - removes a given path constraint.
1150+
1151+
```
1152+
function forgetBranch(uint256 op1, ComparisonOperator op, uint256 op2) external;
1153+
```
1154+
1155+
```k
1156+
rule [cheatcode.call.abstract]:
1157+
<k> #cheatcode_call SELECTOR ARGS
1158+
=> #forget ( #asWord(#range(ARGS,0,32)), #asWord(#range(ARGS,32,32)), #asWord(#range(ARGS,64,32)))
1159+
...
1160+
</k>
1161+
requires SELECTOR ==Int selector ( "forgetBranch(uint256,uint8,uint256)" )
1162+
1163+
```
1164+
11451165
Utils
11461166
-----
11471167

@@ -1751,6 +1771,7 @@ Selectors for **implemented** cheat code functions.
17511771
rule ( selector ( "mockCall(address,bytes,bytes)" ) => 3110212580 )
17521772
rule ( selector ( "mockFunction(address,address,bytes)" ) => 2918731041 )
17531773
rule ( selector ( "copyStorage(address,address)" ) => 540912653 )
1774+
rule ( selector ( "forgetBranch(uint256,uint8,uint256)" ) => 1720990067 )
17541775
```
17551776

17561777
Selectors for **unimplemented** cheat code functions.

src/kontrol/kdist/foundry.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ Then, we define helpers in K which can:
7878
<storage> STORAGE => STORAGE [ #loc(FoundryCheat . Failed) <- 1 ] </storage>
7979
...
8080
</account>
81+
82+
syntax KItem ::= #forget ( Int , Int , Int ) [symbol(cheatcode_forget)]
83+
// -----------------------------------------------------------------------
84+
rule [forget]: <k> #forget(_C1,_OP,_C2) => .K ... </k>
8185
```
8286

8387
#### Structure of execution

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

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
CounterTest.test_Increment()
2+
ForgetBranchTest.test_forgetBranch(uint256)
23
RandomVarTest.test_custom_names()
3-
RandomVarTest.test_randomBool()
44
RandomVarTest.test_randomAddress()
5-
RandomVarTest.test_randomUint()
6-
RandomVarTest.test_randomUint_192()
7-
RandomVarTest.test_randomUint_Range(uint256,uint256)
5+
RandomVarTest.test_randomBool()
86
RandomVarTest.test_randomBytes_length(uint256)
97
RandomVarTest.test_randomBytes4_length()
108
RandomVarTest.test_randomBytes8_length()
9+
RandomVarTest.test_randomUint_192()
10+
RandomVarTest.test_randomUint_Range(uint256,uint256)
11+
RandomVarTest.test_randomUint()
1112
TransientStorageTest.testTransientStoreLoad(uint256,uint256)
13+
UnitTest.test_assert_eq_address_darray(address[])
14+
UnitTest.test_assert_eq_bool_darray(bool[])
15+
UnitTest.test_assert_eq_bytes32_darray(bytes32[])
16+
UnitTest.test_assert_eq_int256_darray(int256[])
17+
UnitTest.test_assert_eq_uint256_darray(uint256[])
18+
UnitTest.test_assertApproxEqAbs_int_same_sign(uint256,uint256,uint256)
19+
UnitTest.test_assertApproxEqAbs_uint(uint256,uint256,uint256)
20+
UnitTest.test_assertApproxEqRel_int_same_sign_unit()
21+
UnitTest.test_assertApproxEqRel_int_zero_cases_unit()
22+
UnitTest.test_assertApproxEqRel_uint_unit()
1223
UnitTest.test_assertEq_address_err()
1324
UnitTest.test_assertEq_bool_err()
1425
UnitTest.test_assertEq_bytes32_err()
@@ -39,13 +50,3 @@ UnitTest.test_assertNotEq(bytes32,bytes32)
3950
UnitTest.test_assertNotEq(int256,int256)
4051
UnitTest.test_assertTrue_err()
4152
UnitTest.test_assertTrue(bool)
42-
UnitTest.test_assertApproxEqAbs_uint(uint256,uint256,uint256)
43-
UnitTest.test_assertApproxEqAbs_int_same_sign(uint256,uint256,uint256)
44-
UnitTest.test_assertApproxEqRel_uint_unit()
45-
UnitTest.test_assertApproxEqRel_int_same_sign_unit()
46-
UnitTest.test_assertApproxEqRel_int_zero_cases_unit()
47-
UnitTest.test_assert_eq_bytes32_darray(bytes32[])
48-
UnitTest.test_assert_eq_bool_darray(bool[])
49-
UnitTest.test_assert_eq_int256_darray(int256[])
50-
UnitTest.test_assert_eq_uint256_darray(uint256[])
51-
UnitTest.test_assert_eq_address_darray(address[])
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
ForgetBranchTest.test_forgetBranch(uint256)
12
RandomVarTest.test_custom_names()
Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
UnitTest.test_assert_eq_address_darray_err()
2+
UnitTest.test_assert_eq_bool_darray_err()
3+
UnitTest.test_assert_eq_bytes32_darray_err()
4+
UnitTest.test_assert_eq_int256_darray_err()
5+
UnitTest.test_assert_eq_uint256_darray_err()
6+
UnitTest.test_assertApproxEqAbs_int_opp_sign_err()
7+
UnitTest.test_assertApproxEqAbs_int_opp_sign(uint256,uint256,uint256)
8+
UnitTest.test_assertApproxEqAbs_int_same_sign_err()
9+
UnitTest.test_assertApproxEqAbs_int_zero_cases_err()
10+
UnitTest.test_assertApproxEqAbs_int_zero_cases(uint256,uint256)
11+
UnitTest.test_assertApproxEqAbs_uint_err()
12+
UnitTest.test_assertApproxEqRel_int_opp_sign_err()
13+
UnitTest.test_assertApproxEqRel_int_opp_sign_unit()
14+
UnitTest.test_assertApproxEqRel_int_same_sign_err()
15+
UnitTest.test_assertApproxEqRel_int_zero_cases_err()
16+
UnitTest.test_assertApproxEqRel_uint_err()
117
UnitTest.test_assertEq_address_err()
218
UnitTest.test_assertEq_bool_err()
319
UnitTest.test_assertEq_bytes32_err()
@@ -13,20 +29,4 @@ UnitTest.test_assertNotEq_bool_err()
1329
UnitTest.test_assertNotEq_bytes32_err()
1430
UnitTest.test_assertNotEq_err()
1531
UnitTest.test_assertNotEq_int256_err()
16-
UnitTest.test_assertTrue_err()
17-
UnitTest.test_assertApproxEqAbs_uint_err()
18-
UnitTest.test_assertApproxEqAbs_int_same_sign_err()
19-
UnitTest.test_assertApproxEqAbs_int_opp_sign(uint256,uint256,uint256)
20-
UnitTest.test_assertApproxEqAbs_int_opp_sign_err()
21-
UnitTest.test_assertApproxEqAbs_int_zero_cases(uint256,uint256)
22-
UnitTest.test_assertApproxEqAbs_int_zero_cases_err()
23-
UnitTest.test_assertApproxEqRel_uint_err()
24-
UnitTest.test_assertApproxEqRel_int_same_sign_err()
25-
UnitTest.test_assertApproxEqRel_int_opp_sign_unit()
26-
UnitTest.test_assertApproxEqRel_int_opp_sign_err()
27-
UnitTest.test_assertApproxEqRel_int_zero_cases_err()
28-
UnitTest.test_assert_eq_bytes32_darray_err()
29-
UnitTest.test_assert_eq_bool_darray_err()
30-
UnitTest.test_assert_eq_int256_darray_err()
31-
UnitTest.test_assert_eq_address_darray_err()
32-
UnitTest.test_assert_eq_uint256_darray_err()
32+
UnitTest.test_assertTrue_err()

0 commit comments

Comments
 (0)