|
| 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