Skip to content

Commit 0c1d1d7

Browse files
authored
Factor out Foundry modules (#1021)
* src/kontrol/{foundry,display,__main__}: separate out display related module * kontrol/{foundry,__main__}: avoid passing entire Foundry object into foundry_state_load * kontrol/{utils,state_record,foundry,__main__}: separate out foundry_state_load into its own module * kontrol/{state_record,prove}: move more helpers for state recording to state_record module * src/kontrol/prove: pull computation of initial accounts from recorded state to top-level * kontrol/{prove,__main__}: lift use of state_record out of foundry_prove * kontrol/{foundry,display}: move Foundry.custom_view to display module instead * kontrol/state_record: fix pyupgrade error
1 parent 650a620 commit 0c1d1d7

File tree

11 files changed

+504
-476
lines changed

11 files changed

+504
-476
lines changed

src/kontrol/__main__.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from . import VERSION
1313
from .cli import _create_argument_parser, generate_options, get_argument_type_setter, get_option_string_destination
14+
from .display import foundry_show, foundry_view
1415
from .foundry import (
1516
Foundry,
1617
foundry_clean,
@@ -21,19 +22,20 @@
2122
foundry_refute_node,
2223
foundry_remove_node,
2324
foundry_section_edge,
24-
foundry_show,
2525
foundry_simplify_node,
2626
foundry_split_node,
27-
foundry_state_load,
2827
foundry_step_node,
2928
foundry_unrefute_node,
30-
foundry_view,
3129
init_project,
32-
read_recorded_state_diff,
33-
read_recorded_state_dump,
3430
)
3531
from .kompile import foundry_kompile
3632
from .prove import foundry_prove
33+
from .state_record import (
34+
foundry_state_load,
35+
read_recorded_state_diff,
36+
read_recorded_state_dump,
37+
recorded_state_to_account_cells,
38+
)
3739
from .utils import _LOG_FORMAT, _rv_blue, _rv_yellow, check_k_version, config_file_path, console, loglevel
3840

3941
if TYPE_CHECKING:
@@ -123,10 +125,11 @@ def main() -> None:
123125

124126

125127
def exec_load_state(options: LoadStateOptions) -> None:
126-
foundry_state_load(
127-
options=options,
128-
foundry=_load_foundry(options.foundry_root, add_enum_constraints=options.enum_constraints),
129-
)
128+
foundry = _load_foundry(options.foundry_root, add_enum_constraints=options.enum_constraints)
129+
if options.output_dir_name is None:
130+
options.output_dir_name = foundry.profile.get('test', '')
131+
output_dir = foundry._root / options.output_dir_name
132+
foundry_state_load(options=options, output_dir=output_dir)
130133

131134

132135
def exec_version(options: VersionOptions) -> None:
@@ -166,12 +169,13 @@ def exec_prove(options: ProveOptions) -> None:
166169
if options.recorded_diff_state_path and options.recorded_dump_state_path:
167170
raise AssertionError('Provide only one file for recorded state updates')
168171

169-
recorded_diff_entries = (
170-
read_recorded_state_diff(options.recorded_diff_state_path) if options.recorded_diff_state_path else None
171-
)
172-
recorded_dump_entries = (
173-
read_recorded_state_dump(options.recorded_dump_state_path) if options.recorded_dump_state_path else None
174-
)
172+
init_accounts = []
173+
if options.recorded_dump_state_path is not None:
174+
recorded_dump_entries = read_recorded_state_dump(options.recorded_dump_state_path)
175+
init_accounts = recorded_state_to_account_cells(recorded_dump_entries)
176+
elif options.recorded_diff_state_path is not None:
177+
recorded_diff_entries = read_recorded_state_diff(options.recorded_diff_state_path)
178+
init_accounts = recorded_state_to_account_cells(recorded_diff_entries)
175179

176180
if options.verbose or options.debug:
177181
proving_message = f'[{_rv_blue()}]:person_running: [bold]Running [{_rv_yellow()}]Kontrol[/{_rv_yellow()}] proofs[/bold] :person_running:[/{_rv_blue()}]'
@@ -184,7 +188,7 @@ def exec_prove(options: ProveOptions) -> None:
184188
options.foundry_root, options.bug_report, add_enum_constraints=options.enum_constraints
185189
),
186190
options=options,
187-
recorded_state_entries=recorded_dump_entries if recorded_dump_entries else recorded_diff_entries,
191+
init_accounts=init_accounts,
188192
)
189193
except CTermSMTError as err:
190194
raise RuntimeError(

src/kontrol/display.py

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
from __future__ import annotations
2+
3+
import ast
4+
import logging
5+
from pathlib import Path
6+
from typing import TYPE_CHECKING
7+
8+
from kevm_pyk.kevm import KEVMNodePrinter
9+
from kevm_pyk.utils import legacy_explore, print_failure_info
10+
from pyk.cterm import CTerm
11+
from pyk.kast.inner import KApply, KToken, KVariable
12+
from pyk.kast.manip import collect, extract_lhs, flatten_label
13+
from pyk.kast.outer import KDefinition, KFlatModule, KImport, KRequire
14+
from pyk.kcfg import KCFG
15+
from pyk.kcfg.minimize import KCFGMinimizer
16+
from pyk.prelude.kint import INT
17+
from pyk.proof.reachability import APRProof
18+
from pyk.proof.show import APRProofNodePrinter, APRProofShow
19+
from pyk.proof.tui import APRProofViewer
20+
from pyk.utils import single, unique
21+
22+
from .foundry import Foundry, KontrolSemantics
23+
from .solc import CompilationUnit
24+
from .solc_to_k import Contract
25+
26+
if TYPE_CHECKING:
27+
from collections.abc import Iterable
28+
from typing import Final
29+
30+
from pyk.kast.inner import KInner
31+
from pyk.kcfg.show import NodePrinter
32+
from pyk.kcfg.tui import KCFGElem
33+
34+
from .options import ShowOptions, ViewKcfgOptions
35+
36+
_LOGGER: Final = logging.getLogger(__name__)
37+
38+
39+
class FoundryNodePrinter(KEVMNodePrinter):
40+
foundry: Foundry
41+
contract_name: str
42+
omit_unstable_output: bool
43+
compilation_unit: CompilationUnit
44+
45+
def __init__(self, foundry: Foundry, contract_name: str, omit_unstable_output: bool = False):
46+
KEVMNodePrinter.__init__(self, foundry.kevm)
47+
self.foundry = foundry
48+
self.contract_name = contract_name
49+
self.omit_unstable_output = omit_unstable_output
50+
self.compilation_unit = CompilationUnit.load_build_info(foundry.build_info)
51+
52+
def print_node(self, kcfg: KCFG, node: KCFG.Node) -> list[str]:
53+
ret_strs = super().print_node(kcfg, node)
54+
_pc = node.cterm.try_cell('PC_CELL')
55+
program_cell = node.cterm.try_cell('PROGRAM_CELL')
56+
57+
if type(_pc) is KToken and _pc.sort == INT:
58+
if type(program_cell) is KToken:
59+
try:
60+
bytecode = ast.literal_eval(program_cell.token)
61+
instruction = self.compilation_unit.get_instruction(bytecode, int(_pc.token))
62+
ast_node = instruction.node()
63+
start_line, _, end_line, _ = ast_node.source_range()
64+
ret_strs.append(f'src: {str(Path(ast_node.source.name))}:{start_line}:{end_line}')
65+
except Exception:
66+
pass
67+
68+
calldata_cell = node.cterm.try_cell('CALLDATA_CELL')
69+
70+
if type(program_cell) is KToken:
71+
selector_bytes = None
72+
if type(calldata_cell) is KToken:
73+
selector_bytes = ast.literal_eval(calldata_cell.token)
74+
selector_bytes = selector_bytes[:4]
75+
elif (
76+
type(calldata_cell) is KApply and calldata_cell.label.name == '_+Bytes__BYTES-HOOKED_Bytes_Bytes_Bytes'
77+
):
78+
first_bytes = flatten_label(label='_+Bytes__BYTES-HOOKED_Bytes_Bytes_Bytes', kast=calldata_cell)[0]
79+
if type(first_bytes) is KToken:
80+
selector_bytes = ast.literal_eval(first_bytes.token)
81+
selector_bytes = selector_bytes[:4]
82+
83+
if selector_bytes is not None:
84+
selector = int.from_bytes(selector_bytes, 'big')
85+
current_contract_name = self.foundry.contract_name_from_bytecode(ast.literal_eval(program_cell.token))
86+
for contract_name, contract_obj in self.foundry.contracts.items():
87+
if current_contract_name == contract_name:
88+
for method in contract_obj.methods:
89+
if method.id == selector:
90+
ret_strs.append(f'method: {method.qualified_name}')
91+
92+
return ret_strs
93+
94+
95+
class FoundryAPRNodePrinter(FoundryNodePrinter, APRProofNodePrinter):
96+
def __init__(self, foundry: Foundry, contract_name: str, proof: APRProof, omit_unstable_output: bool = False):
97+
FoundryNodePrinter.__init__(self, foundry, contract_name, omit_unstable_output=omit_unstable_output)
98+
APRProofNodePrinter.__init__(self, proof, foundry.kevm)
99+
100+
101+
def foundry_node_printer(
102+
foundry: Foundry, contract_name: str, proof: APRProof, omit_unstable_output: bool = False
103+
) -> NodePrinter:
104+
if type(proof) is APRProof:
105+
return FoundryAPRNodePrinter(foundry, contract_name, proof, omit_unstable_output=omit_unstable_output)
106+
raise ValueError(f'Cannot build NodePrinter for proof type: {type(proof)}')
107+
108+
109+
def foundry_show(
110+
foundry: Foundry,
111+
options: ShowOptions,
112+
) -> str:
113+
test_id = foundry.get_test_id(options.test, options.version)
114+
contract_name, _ = single(foundry.matching_tests([options.test])).split('.')
115+
proof = foundry.get_apr_proof(test_id)
116+
117+
nodes: Iterable[int | str] = options.nodes
118+
if options.pending:
119+
nodes = list(nodes) + [node.id for node in proof.pending]
120+
if options.failing:
121+
nodes = list(nodes) + [node.id for node in proof.failing]
122+
nodes = unique(nodes)
123+
124+
unstable_cells = [
125+
'<program>',
126+
'<jumpDests>',
127+
'<pc>',
128+
'<gas>',
129+
'<code>',
130+
]
131+
132+
node_printer = foundry_node_printer(
133+
foundry, contract_name, proof, omit_unstable_output=options.omit_unstable_output
134+
)
135+
proof_show = APRProofShow(foundry.kevm, node_printer=node_printer)
136+
137+
if options.minimize_kcfg:
138+
KCFGMinimizer(proof.kcfg).minimize()
139+
140+
res_lines = proof_show.show(
141+
proof,
142+
nodes=nodes,
143+
node_deltas=options.node_deltas,
144+
to_module=options.to_module,
145+
minimize=options.minimize,
146+
sort_collections=options.sort_collections,
147+
omit_cells=(unstable_cells if options.omit_unstable_output else []),
148+
)
149+
150+
start_server = options.port is None
151+
152+
if options.failure_info:
153+
with legacy_explore(
154+
foundry.kevm,
155+
kcfg_semantics=KontrolSemantics(),
156+
id=test_id,
157+
smt_timeout=options.smt_timeout,
158+
smt_retry_limit=options.smt_retry_limit,
159+
start_server=start_server,
160+
port=options.port,
161+
extra_module=foundry.load_lemmas(options.lemmas),
162+
) as kcfg_explore:
163+
res_lines += print_failure_info(proof, kcfg_explore, options.counterexample_info)
164+
res_lines += Foundry.help_info(proof.id, False)
165+
166+
if options.to_kevm_claims or options.to_kevm_rules:
167+
_foundry_labels = [
168+
prod.klabel
169+
for prod in foundry.kevm.definition.all_modules_dict['FOUNDRY-CHEAT-CODES'].productions
170+
if prod.klabel is not None
171+
]
172+
173+
def _remove_foundry_config(_cterm: CTerm) -> CTerm:
174+
kevm_config_pattern = KApply(
175+
'<generatedTop>',
176+
[
177+
KApply(
178+
'<foundry>',
179+
[
180+
KVariable('KEVM_CELL'),
181+
KVariable('STACKCHECKS_CELL'),
182+
KVariable('CHEATCODES_CELL'),
183+
KVariable('KEVMTRACING_CELL'),
184+
],
185+
),
186+
KVariable('GENERATEDCOUNTER_CELL'),
187+
],
188+
)
189+
kevm_config_match = kevm_config_pattern.match(_cterm.config)
190+
if kevm_config_match is None:
191+
_LOGGER.warning('Unable to match on <kevm> cell.')
192+
return _cterm
193+
return CTerm(kevm_config_match['KEVM_CELL'], _cterm.constraints)
194+
195+
def _contains_foundry_klabel(_kast: KInner) -> bool:
196+
_contains = False
197+
198+
def _collect_klabel(_k: KInner) -> None:
199+
nonlocal _contains
200+
if type(_k) is KApply and _k.label.name in _foundry_labels:
201+
_contains = True
202+
203+
collect(_collect_klabel, _kast)
204+
return _contains
205+
206+
for node in proof.kcfg.nodes:
207+
proof.kcfg.let_node(node.id, cterm=_remove_foundry_config(node.cterm))
208+
209+
# Due to bug in KCFG.replace_node: https://github.com/runtimeverification/pyk/issues/686
210+
proof.kcfg = KCFG.from_dict(proof.kcfg.to_dict())
211+
212+
sentences = [
213+
edge.to_rule(
214+
'BASIC-BLOCK',
215+
claim=(not options.to_kevm_rules),
216+
defunc_with=foundry.kevm.definition,
217+
minimize=options.minimize,
218+
)
219+
for edge in proof.kcfg.edges()
220+
]
221+
sentences = [sent for sent in sentences if not _contains_foundry_klabel(sent.body)]
222+
sentences = [
223+
sent for sent in sentences if not KontrolSemantics().is_terminal(CTerm.from_kast(extract_lhs(sent.body)))
224+
]
225+
if len(sentences) == 0:
226+
_LOGGER.warning(f'No claims or rules retained for proof {proof.id}')
227+
228+
else:
229+
module_name = Contract.escaped(proof.id.upper() + '-SPEC', '')
230+
module = KFlatModule(module_name, sentences=sentences, imports=[KImport('VERIFICATION')])
231+
defn = KDefinition(module_name, [module], requires=[KRequire('verification.k')])
232+
233+
defn_lines = foundry.kevm.pretty_print(defn, in_module='EVM').split('\n')
234+
235+
res_lines += defn_lines
236+
237+
if options.kevm_claim_dir is not None:
238+
kevm_sentences_file = options.kevm_claim_dir / (module_name.lower() + '.k')
239+
kevm_sentences_file.write_text('\n'.join(line.rstrip() for line in defn_lines))
240+
241+
return '\n'.join([line.rstrip() for line in res_lines])
242+
243+
244+
def solidity_src_print(path: Path, start: int, end: int) -> Iterable[str]:
245+
lines = path.read_text().split('\n')
246+
prefix_lines = [f' {l}' for l in lines[:start]]
247+
actual_lines = [f' | {l}' for l in lines[start:end]]
248+
suffix_lines = [f' {l}' for l in lines[end:]]
249+
return prefix_lines + actual_lines + suffix_lines
250+
251+
252+
def custom_view(contract_name: str, element: KCFGElem, compilation_unit: CompilationUnit) -> Iterable[str]:
253+
if type(element) is KCFG.Node:
254+
pc_cell = element.cterm.try_cell('PC_CELL')
255+
program_cell = element.cterm.try_cell('PROGRAM_CELL')
256+
if type(pc_cell) is KToken and pc_cell.sort == INT:
257+
if type(program_cell) is KToken:
258+
try:
259+
bytecode = ast.literal_eval(program_cell.token)
260+
instruction = compilation_unit.get_instruction(bytecode, int(pc_cell.token))
261+
node = instruction.node()
262+
start_line, _, end_line, _ = node.source_range()
263+
return solidity_src_print(Path(node.source.name), start_line - 1, end_line)
264+
except Exception:
265+
return [f'No sourcemap data for contract at pc {contract_name}: {int(pc_cell.token)}']
266+
return ['NO DATA']
267+
elif type(element) is KCFG.Edge:
268+
return list(element.rules)
269+
elif type(element) is KCFG.NDBranch:
270+
return list(element.rules)
271+
return ['NO DATA']
272+
273+
274+
def foundry_view(foundry: Foundry, options: ViewKcfgOptions) -> None:
275+
test_id = foundry.get_test_id(options.test, options.version)
276+
contract_name, _ = test_id.split('.')
277+
proof = foundry.get_apr_proof(test_id)
278+
279+
compilation_unit = CompilationUnit.load_build_info(foundry.build_info)
280+
281+
def _custom_view(elem: KCFGElem) -> Iterable[str]:
282+
return custom_view(contract_name, elem, compilation_unit)
283+
284+
node_printer = foundry_node_printer(foundry, contract_name, proof)
285+
viewer = APRProofViewer(proof, foundry.kevm, node_printer=node_printer, custom_view=_custom_view)
286+
viewer.run()

0 commit comments

Comments
 (0)