certora-cli-beta-mirror 7.28.0__py3-none-any.whl → 8.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- certora_cli/CertoraProver/Compiler/CompilerCollectorFactory.py +10 -3
- certora_cli/CertoraProver/Compiler/CompilerCollectorVy.py +51 -16
- certora_cli/CertoraProver/Compiler/CompilerCollectorYul.py +3 -0
- certora_cli/CertoraProver/castingInstrumenter.py +192 -0
- certora_cli/CertoraProver/certoraApp.py +52 -0
- certora_cli/CertoraProver/certoraBuild.py +694 -207
- certora_cli/CertoraProver/certoraBuildCacheManager.py +21 -17
- certora_cli/CertoraProver/certoraBuildDataClasses.py +8 -2
- certora_cli/CertoraProver/certoraBuildRust.py +88 -54
- certora_cli/CertoraProver/certoraBuildSui.py +112 -0
- certora_cli/CertoraProver/certoraCloudIO.py +97 -96
- certora_cli/CertoraProver/certoraCollectConfigurationLayout.py +230 -84
- certora_cli/CertoraProver/certoraCollectRunMetadata.py +52 -6
- certora_cli/CertoraProver/certoraCompilerParameters.py +11 -0
- certora_cli/CertoraProver/certoraConfigIO.py +43 -35
- certora_cli/CertoraProver/certoraContext.py +128 -54
- certora_cli/CertoraProver/certoraContextAttributes.py +415 -234
- certora_cli/CertoraProver/certoraContextValidator.py +152 -105
- certora_cli/CertoraProver/certoraContractFuncs.py +34 -1
- certora_cli/CertoraProver/certoraParseBuildScript.py +8 -10
- certora_cli/CertoraProver/certoraType.py +10 -1
- certora_cli/CertoraProver/certoraVerifyGenerator.py +22 -4
- certora_cli/CertoraProver/erc7201.py +45 -0
- certora_cli/CertoraProver/splitRules.py +23 -18
- certora_cli/CertoraProver/storageExtension.py +351 -0
- certora_cli/EquivalenceCheck/Eq_default.conf +0 -1
- certora_cli/EquivalenceCheck/Eq_sanity.conf +0 -1
- certora_cli/EquivalenceCheck/equivCheck.py +2 -1
- certora_cli/Mutate/mutateApp.py +41 -22
- certora_cli/Mutate/mutateAttributes.py +11 -0
- certora_cli/Mutate/mutateValidate.py +42 -2
- certora_cli/Shared/certoraAttrUtil.py +21 -5
- certora_cli/Shared/certoraUtils.py +180 -60
- certora_cli/Shared/certoraValidateFuncs.py +68 -26
- certora_cli/Shared/proverCommon.py +308 -0
- certora_cli/certoraCVLFormatter.py +76 -0
- certora_cli/certoraConcord.py +39 -0
- certora_cli/certoraEVMProver.py +4 -3
- certora_cli/certoraRanger.py +39 -0
- certora_cli/certoraRun.py +83 -223
- certora_cli/certoraSolanaProver.py +40 -128
- certora_cli/certoraSorobanProver.py +59 -4
- certora_cli/certoraSuiProver.py +93 -0
- certora_cli_beta_mirror-8.5.0.dist-info/LICENSE +15 -0
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/METADATA +21 -5
- certora_cli_beta_mirror-8.5.0.dist-info/RECORD +81 -0
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/WHEEL +1 -1
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/entry_points.txt +3 -0
- certora_jars/ASTExtraction.jar +0 -0
- certora_jars/CERTORA-CLI-VERSION-METADATA.json +1 -1
- certora_jars/Typechecker.jar +0 -0
- certora_cli_beta_mirror-7.28.0.dist-info/LICENSE +0 -22
- certora_cli_beta_mirror-7.28.0.dist-info/RECORD +0 -70
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/top_level.txt +0 -0
|
@@ -18,11 +18,14 @@ import sys
|
|
|
18
18
|
import re
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
import subprocess
|
|
21
|
+
import tempfile
|
|
21
22
|
import uuid
|
|
22
23
|
|
|
23
24
|
from typing import List, Set, Optional
|
|
24
25
|
|
|
26
|
+
import CertoraProver.certoraContext as Ctx
|
|
25
27
|
import CertoraProver.certoraContextAttributes as Attrs
|
|
28
|
+
import CertoraProver.certoraApp as App
|
|
26
29
|
from CertoraProver.certoraContextClass import CertoraContext
|
|
27
30
|
from Shared import certoraUtils as Util
|
|
28
31
|
|
|
@@ -71,31 +74,33 @@ class SplitRulesHandler():
|
|
|
71
74
|
def jar_list_value(list_attr: List[str]) -> str:
|
|
72
75
|
return ','.join(list_attr)
|
|
73
76
|
|
|
74
|
-
|
|
77
|
+
with tempfile.NamedTemporaryFile("r", dir=Util.get_build_dir()) as tmp_file:
|
|
78
|
+
args = ["-listRules", tmp_file.name]
|
|
75
79
|
|
|
76
|
-
|
|
80
|
+
if self.context.exclude_rule:
|
|
81
|
+
args += ['-excludeRule', jar_list_value(self.context.exclude_rule)]
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
if not split_rules and self.context.rule:
|
|
84
|
+
args += ['-rule', jar_list_value(self.context.rule)]
|
|
85
|
+
elif split_rules and self.context.split_rules:
|
|
86
|
+
args += ['-rule', jar_list_value(self.context.split_rules)]
|
|
80
87
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
try:
|
|
88
|
+
try:
|
|
89
|
+
Ctx.run_local_spec_check(False, self.context, args, print_errors=False)
|
|
90
|
+
lines = tmp_file.read().split("\n")
|
|
91
|
+
return set(lines)
|
|
86
92
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
filtered_lines = [s for s in lines if not (s.startswith("Warning:") or " " in s)]
|
|
90
|
-
return set(filtered_lines)
|
|
91
|
-
|
|
92
|
-
except subprocess.CalledProcessError as e:
|
|
93
|
-
raise Util.CertoraUserInputError(f"Failed to get {'split ' if split_rules else ''}rules\ncommand: {command}\n{e}")
|
|
93
|
+
except Exception as e:
|
|
94
|
+
raise Util.CertoraUserInputError(f"Failed to get {'split ' if split_rules else ''}rules\n{e}")
|
|
94
95
|
|
|
95
96
|
def run_commands(self) -> int:
|
|
96
97
|
rule_flag = Attrs.EvmProverAttributes.RULE.get_flag()
|
|
97
98
|
split_rules_flag = Attrs.EvmProverAttributes.SPLIT_RULES.get_flag()
|
|
98
99
|
msg_flag = Attrs.CommonAttributes.MSG.get_flag()
|
|
100
|
+
|
|
101
|
+
# it is important to use the cache, when the difference between the runs is only the rules that apply
|
|
102
|
+
build_cache_flag = Attrs.EvmProverAttributes.BUILD_CACHE.get_flag()
|
|
103
|
+
|
|
99
104
|
group_id_flag = Attrs.EvmProverAttributes.GROUP_ID.get_flag()
|
|
100
105
|
disable_local_typechecking_flag = Attrs.EvmProverAttributes.DISABLE_LOCAL_TYPECHECKING.get_flag()
|
|
101
106
|
|
|
@@ -118,7 +123,7 @@ class SplitRulesHandler():
|
|
|
118
123
|
if called as library then if running in local mode we use certoraRun.py otherwise certoraRun (from package)
|
|
119
124
|
:return:
|
|
120
125
|
"""
|
|
121
|
-
assert
|
|
126
|
+
assert self.context.app == App.EvmApp, "Split rules is supported only for EVM apps"
|
|
122
127
|
if hasattr(self.context, 'prover_cmd'):
|
|
123
128
|
return self.context.prover_cmd
|
|
124
129
|
if self.context.local:
|
|
@@ -137,7 +142,7 @@ class SplitRulesHandler():
|
|
|
137
142
|
self.context.msg = ''
|
|
138
143
|
|
|
139
144
|
cmd = [get_cmd()] + args + [group_id_flag, self.context.group_id, disable_local_typechecking_flag,
|
|
140
|
-
split_rules_flag]
|
|
145
|
+
build_cache_flag, split_rules_flag]
|
|
141
146
|
|
|
142
147
|
if self.split_rules:
|
|
143
148
|
for rule in self.split_rules:
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# The Certora Prover
|
|
2
|
+
# Copyright (C) 2025 Certora Ltd.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, version 3 of the License.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
import time
|
|
18
|
+
import hashlib
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
import logging
|
|
22
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
23
|
+
try:
|
|
24
|
+
from typing import TypeAlias
|
|
25
|
+
except ImportError:
|
|
26
|
+
from typing_extensions import TypeAlias
|
|
27
|
+
|
|
28
|
+
scripts_dir_path = Path(__file__).parent.parent.resolve() # containing directory
|
|
29
|
+
sys.path.insert(0, str(scripts_dir_path))
|
|
30
|
+
|
|
31
|
+
from CertoraProver.certoraBuildDataClasses import ContractInSDC
|
|
32
|
+
from CertoraProver import erc7201
|
|
33
|
+
from Shared import certoraUtils as Util
|
|
34
|
+
from CertoraProver.certoraBuildDataClasses import SDC
|
|
35
|
+
|
|
36
|
+
NameSpacedStorage: TypeAlias = Tuple[str, str]
|
|
37
|
+
NewStorageFields = List[Dict[str, Any]]
|
|
38
|
+
NewStorageTypes = Dict[str, Any]
|
|
39
|
+
NewStorageInfo = Tuple[NewStorageFields, NewStorageTypes]
|
|
40
|
+
|
|
41
|
+
storage_extension_logger = logging.getLogger("storage_extension")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def erc7201_of_node(n: Dict[str, Any]) -> Optional[NameSpacedStorage]:
|
|
45
|
+
"""
|
|
46
|
+
If n is a StructDefinition node, try and extract the @custom:storage-location
|
|
47
|
+
structured documentation, if it exists.
|
|
48
|
+
|
|
49
|
+
@returns (type, namespace) where 'type' is the name of the annotated type and
|
|
50
|
+
'namespace' is the namespace string
|
|
51
|
+
"""
|
|
52
|
+
if n.get("nodeType") != "StructDefinition":
|
|
53
|
+
return None
|
|
54
|
+
typeName = n.get("canonicalName")
|
|
55
|
+
doc = n.get("documentation")
|
|
56
|
+
if doc is None or doc.get("nodeType") != "StructuredDocumentation" or typeName is None:
|
|
57
|
+
return None
|
|
58
|
+
storage_location_regex = r'@custom:storage-location erc7201:([a-zA-Z.0-9]+)'
|
|
59
|
+
match = re.search(storage_location_regex, doc.get("text"))
|
|
60
|
+
if match is None:
|
|
61
|
+
return None
|
|
62
|
+
ns = match.group(1)
|
|
63
|
+
return (typeName, ns)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def generate_harness_name(original_file: str) -> str:
|
|
67
|
+
"""
|
|
68
|
+
Generate a unique name for the harness contract based on the original file name.
|
|
69
|
+
The name is generated by hashing the original file name and appending a suffix to ensure uniqueness.
|
|
70
|
+
Args:
|
|
71
|
+
original_file (str): The path to the original file used to generate the harness name.
|
|
72
|
+
Returns:
|
|
73
|
+
str: A unique name for the harness contract.
|
|
74
|
+
"""
|
|
75
|
+
stem = Path(original_file).stem
|
|
76
|
+
# 1) Compute an 8-hex salt from path+time
|
|
77
|
+
digest = hashlib.sha1(f"{original_file}{time.time()}".encode()).hexdigest()[:8]
|
|
78
|
+
suffix = f"_{digest}_Harness"
|
|
79
|
+
# 2) Reserve space for suffix so whole name ≤31 chars
|
|
80
|
+
max_stem = 31 - len(suffix)
|
|
81
|
+
if len(stem) > max_stem:
|
|
82
|
+
stem = stem[:max_stem]
|
|
83
|
+
# 3) Build, sanitize, and ensure start‐with‐letter
|
|
84
|
+
raw = f"{stem}{suffix}"
|
|
85
|
+
name = re.sub(r'[^A-Za-z0-9_]', '_', raw)
|
|
86
|
+
if not name[0].isalpha():
|
|
87
|
+
name = f"H{name[1:]}"
|
|
88
|
+
return name
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_next_file_index(self_file_to_sdc_name: Dict[Path, str], max_index: int = 1000) -> int:
|
|
92
|
+
"""
|
|
93
|
+
Gets the next available file index for temporary files to avoid naming conflicts.
|
|
94
|
+
|
|
95
|
+
This method examines the existing SDC names in `file_to_sdc_name` dictionary and
|
|
96
|
+
extracts numeric indices from them. It identifies the highest index currently in use
|
|
97
|
+
and returns that value plus one. If no valid indices are found or if an error occurs,
|
|
98
|
+
a default value of 1000 is returned.
|
|
99
|
+
|
|
100
|
+
The method assumes SDC names follow the format "some_prefix_NUMBER", where NUMBER
|
|
101
|
+
is an integer that can be extracted from the last segment after splitting by underscore.
|
|
102
|
+
Args:
|
|
103
|
+
self_file_to_sdc_name (Dict[Path, str]): A dictionary mapping file paths to their SDC names.
|
|
104
|
+
max_index (int): The maximum index to return if no valid indices are found. Default is 1000.
|
|
105
|
+
Returns:
|
|
106
|
+
int: The next available file index, or a default value of 1000 if no valid indices are found.
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
# If file_to_sdc_name is empty, return a default starting index
|
|
110
|
+
if not self_file_to_sdc_name:
|
|
111
|
+
return max_index
|
|
112
|
+
|
|
113
|
+
indices = []
|
|
114
|
+
for sdc_name in self_file_to_sdc_name.values():
|
|
115
|
+
parts = sdc_name.split("_")
|
|
116
|
+
if len(parts) > 1: # Make sure there's at least one underscore
|
|
117
|
+
try:
|
|
118
|
+
# Try to convert the last part to an integer
|
|
119
|
+
index = int(parts[-1])
|
|
120
|
+
indices.append(index)
|
|
121
|
+
except ValueError:
|
|
122
|
+
# Skip if the last part isn't a number
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
# If we found no valid indices, return a default value
|
|
126
|
+
if not indices:
|
|
127
|
+
storage_extension_logger.debug(f"No valid indices found in file_to_sdc_name, using default value of {max_index}")
|
|
128
|
+
return max_index
|
|
129
|
+
|
|
130
|
+
return max(indices) + 1
|
|
131
|
+
except Exception:
|
|
132
|
+
# Fallback in case of any unexpected errors
|
|
133
|
+
storage_extension_logger.debug(f"Error determining next file index, using default value of {max_index}")
|
|
134
|
+
return max_index
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def write_harness_contract(tmp_file: Any,
|
|
138
|
+
harness_name: str,
|
|
139
|
+
ns_storage: Set[NameSpacedStorage]) -> Dict[str, str]:
|
|
140
|
+
"""
|
|
141
|
+
Write the harness contract with dummy fields to the temporary file.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
tmp_file: The temporary file to write to
|
|
145
|
+
harness_name: Name of the harness contract
|
|
146
|
+
ns_storage: Set of namespace storage declarations
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Dict[str, str]: Mapping from variable names to their slots
|
|
150
|
+
"""
|
|
151
|
+
tmp_file.write(f"contract {harness_name} {{\n")
|
|
152
|
+
|
|
153
|
+
# Map from variable names to their slots
|
|
154
|
+
var_to_slot = {}
|
|
155
|
+
|
|
156
|
+
# Add dummy fields for each namespaced storage
|
|
157
|
+
for type_name, namespace in ns_storage:
|
|
158
|
+
# Create a variable name by replacing dots with underscores and appending the hash
|
|
159
|
+
# Add a prefix to ensure the variable name is valid in Solidity (e.g., no leading digits)
|
|
160
|
+
var_name = f"ext_{namespace.replace('.', '_')}"
|
|
161
|
+
|
|
162
|
+
# Calculate the slot using ERC-7201 formula
|
|
163
|
+
# UTF-8 is the standard encoding for Ethereum and Solidity
|
|
164
|
+
slot = str(erc7201.erc7201(namespace.encode('utf-8')))
|
|
165
|
+
|
|
166
|
+
var_to_slot[var_name] = slot
|
|
167
|
+
tmp_file.write(f"\t{type_name} {var_name};\n")
|
|
168
|
+
|
|
169
|
+
tmp_file.write("}\n")
|
|
170
|
+
return var_to_slot
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def extract_harness_contract_layout(sdcs: List[SDC], harness_name: str) -> Dict[str, Any]:
|
|
174
|
+
"""
|
|
175
|
+
Extract the storage layout of the harness contract.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
sdcs: List of SDCs containing the compiled contracts
|
|
179
|
+
harness_name: Name of the harness contract
|
|
180
|
+
Returns:
|
|
181
|
+
Dict[str, Any]: The storage layout of the harness contract
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
# Search through all SDC's to find the correct contract
|
|
185
|
+
harness_contract = None
|
|
186
|
+
for sdc in sdcs:
|
|
187
|
+
harness_contract = sdc.find_contract(harness_name)
|
|
188
|
+
if harness_contract:
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
if not harness_contract:
|
|
192
|
+
raise RuntimeError(f"Could not find harness contract {harness_name} in compiled output")
|
|
193
|
+
# Extract the storage layout
|
|
194
|
+
layout = harness_contract.storage_layout
|
|
195
|
+
if not layout or 'storage' not in layout:
|
|
196
|
+
raise RuntimeError(f"Invalid storage layout for harness contract {harness_name}")
|
|
197
|
+
return layout
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def remapped_fields_from_layout(layout: Dict[str, Any], var_to_slot: Dict[str, str]) -> NewStorageFields:
|
|
201
|
+
"""
|
|
202
|
+
Remap the fields in the storage layout according to the variable to slot mapping.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
layout: The storage layout of the harness contract
|
|
206
|
+
var_to_slot: Mapping from variable names to their slots
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
List[Dict[str, Any]]: A list of remapped fields with updated slot information
|
|
210
|
+
"""
|
|
211
|
+
remapped_fields = []
|
|
212
|
+
for storage_item in layout['storage']:
|
|
213
|
+
cloned_item = storage_item.copy()
|
|
214
|
+
var_name = cloned_item["label"]
|
|
215
|
+
if var_name in var_to_slot:
|
|
216
|
+
cloned_item["slot"] = var_to_slot[var_name]
|
|
217
|
+
remapped_fields.append(cloned_item)
|
|
218
|
+
else:
|
|
219
|
+
storage_extension_logger.warning(f"Skipping adding variable {var_name} not found in variable to slot mapping")
|
|
220
|
+
|
|
221
|
+
remapped_fields.sort(key=lambda f: int(f["slot"]))
|
|
222
|
+
return remapped_fields
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def get_namespace_storage_from_ast(def_node: Dict[str, Any]) -> Set[NameSpacedStorage]:
|
|
226
|
+
"""
|
|
227
|
+
Extracts namespaced storage information from the AST nodes.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
def_node: The AST node representing the contract definition.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Set[NameSpacedStorage]: A set of namespaced storage information.
|
|
234
|
+
"""
|
|
235
|
+
ns_storage: Set[NameSpacedStorage] = set()
|
|
236
|
+
nodes = def_node.get("nodes")
|
|
237
|
+
if not nodes:
|
|
238
|
+
# No nodes found in the contract definition
|
|
239
|
+
return ns_storage
|
|
240
|
+
for n in nodes:
|
|
241
|
+
sinfo = erc7201_of_node(n)
|
|
242
|
+
if sinfo is not None:
|
|
243
|
+
storage_extension_logger.debug(f"Found namespaced storage: {sinfo}")
|
|
244
|
+
ns_storage.add(sinfo)
|
|
245
|
+
return ns_storage
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def apply_extensions(target_contract: ContractInSDC,
|
|
249
|
+
extensions: Set[str],
|
|
250
|
+
to_add: Dict[str, NewStorageInfo]) -> None:
|
|
251
|
+
"""
|
|
252
|
+
Apply the fields from each extension to the target contract,
|
|
253
|
+
@param target_contract contract to which to apply extensions
|
|
254
|
+
@param extensions set of extension contract names
|
|
255
|
+
@param to_add maps extension name in extensions to (storage layouts, new types)
|
|
256
|
+
"""
|
|
257
|
+
storage_layout = target_contract.storage_layout
|
|
258
|
+
# Check if the target contract has a storage layout
|
|
259
|
+
if not storage_layout:
|
|
260
|
+
storage_extension_logger.warning(f"Target contract {target_contract.name} has no storage layout")
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
# Check if the target contract has a storage layout with 'storage' key
|
|
264
|
+
if "storage" not in storage_layout:
|
|
265
|
+
storage_extension_logger.warning(f"Target contract {target_contract.name} storage layout does not contain 'storage' key")
|
|
266
|
+
storage_layout["storage"] = []
|
|
267
|
+
elif not isinstance(storage_layout["storage"], list):
|
|
268
|
+
storage_extension_logger.warning(f"Target contract {target_contract.name} 'storage' is not a list but {type(storage_layout['storage']).__name__}: {storage_layout['storage']}")
|
|
269
|
+
storage_layout["storage"] = []
|
|
270
|
+
|
|
271
|
+
# Check if the target contract has a storage layout with 'types' key
|
|
272
|
+
if "types" not in storage_layout:
|
|
273
|
+
storage_extension_logger.warning(f"Target contract {target_contract.name} storage layout does not contain 'types' key")
|
|
274
|
+
storage_layout["types"] = {}
|
|
275
|
+
elif not isinstance(storage_layout["types"], dict):
|
|
276
|
+
storage_extension_logger.warning(f"Target contract {target_contract.name} 'types' is not a dict but {type(storage_layout['types']).__name__}: {storage_layout['types']}")
|
|
277
|
+
storage_layout["types"] = {}
|
|
278
|
+
|
|
279
|
+
target_slots = {storage["slot"] for storage in storage_layout["storage"]}
|
|
280
|
+
target_vars = {storage["label"] for storage in storage_layout["storage"]}
|
|
281
|
+
# Keep track of slots we've added, and error if we
|
|
282
|
+
# find two extensions extending the same slot
|
|
283
|
+
added_slots: Dict[str, str] = {}
|
|
284
|
+
added_vars: Dict[str, str] = {}
|
|
285
|
+
for ext in extensions:
|
|
286
|
+
# Check if the extension is in the to_add mapping
|
|
287
|
+
if ext not in to_add:
|
|
288
|
+
storage_extension_logger.warning(f"Extension {ext} not found in to_add mapping")
|
|
289
|
+
continue
|
|
290
|
+
(new_fields, new_types) = to_add[ext]
|
|
291
|
+
for field in new_fields:
|
|
292
|
+
# See if any of the new fields is a slot or variable name we've already added
|
|
293
|
+
slot = field["slot"]
|
|
294
|
+
var = field["label"]
|
|
295
|
+
validate_new_fields(
|
|
296
|
+
target_contract,
|
|
297
|
+
ext,
|
|
298
|
+
slot,
|
|
299
|
+
var,
|
|
300
|
+
added_slots,
|
|
301
|
+
added_vars,
|
|
302
|
+
target_slots,
|
|
303
|
+
target_vars
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
added_slots[slot] = ext
|
|
307
|
+
added_vars[var] = ext
|
|
308
|
+
|
|
309
|
+
# Add the fields to the storage layout
|
|
310
|
+
storage_layout["storage"].extend(new_fields)
|
|
311
|
+
storage_extension_logger.debug(f"Added {len(new_fields)} fields from extension {ext} to contract {target_contract.name}: {[field['label'] for field in new_fields]}")
|
|
312
|
+
for (new_id, new_ty) in new_types.items():
|
|
313
|
+
if new_id in storage_layout["types"]:
|
|
314
|
+
continue
|
|
315
|
+
storage_layout["types"][new_id] = new_ty
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def validate_new_fields(
|
|
319
|
+
target_contract: ContractInSDC,
|
|
320
|
+
ext: str,
|
|
321
|
+
slot: str,
|
|
322
|
+
var: str,
|
|
323
|
+
added_slots: Dict[str, str],
|
|
324
|
+
added_vars: Dict[str, str],
|
|
325
|
+
target_slots: Set[str],
|
|
326
|
+
target_vars: Set[str]) -> None:
|
|
327
|
+
"""
|
|
328
|
+
Validate that the new fields being added to the target contract
|
|
329
|
+
do not conflict with existing fields or variables.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
target_contract: The target contract to which the fields are being added
|
|
333
|
+
ext: The name of the extension contract
|
|
334
|
+
slot: The slot being added
|
|
335
|
+
var: The variable being added
|
|
336
|
+
added_slots: Dictionary of slots already added
|
|
337
|
+
added_vars: Dictionary of variables already added
|
|
338
|
+
target_slots: Set of slots in the target contract
|
|
339
|
+
target_vars: Set of variables in the target contract
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
if slot in added_slots:
|
|
343
|
+
seen = added_slots[slot]
|
|
344
|
+
raise Util.CertoraUserInputError(f"Slot {slot} added to {target_contract.name} by {ext} was already added by {seen}")
|
|
345
|
+
if var in added_vars:
|
|
346
|
+
seen = added_vars[var]
|
|
347
|
+
raise Util.CertoraUserInputError(f"Var '{var}' added to {target_contract.name} by {ext} was already added by {seen}")
|
|
348
|
+
if slot in target_slots:
|
|
349
|
+
raise Util.CertoraUserInputError(f"Slot {slot} added to {target_contract.name} by {ext} is already mapped by {target_contract.name}")
|
|
350
|
+
if var in target_vars:
|
|
351
|
+
raise Util.CertoraUserInputError(f"Var '{var}' added to {target_contract.name} by {ext} is already declared by {target_contract.name}")
|
|
@@ -30,6 +30,7 @@ from CertoraProver.certoraBuild import InputConfig, CertoraBuildGenerator, Funct
|
|
|
30
30
|
from CertoraProver.certoraVerifyGenerator import CertoraVerifyGenerator
|
|
31
31
|
from CertoraProver.certoraContext import get_args
|
|
32
32
|
from certoraRun import run_certora
|
|
33
|
+
import CertoraProver.certoraApp as App
|
|
33
34
|
|
|
34
35
|
LOG_PATH = Path('EqCheck_log.log')
|
|
35
36
|
SANITY_PATH = Path('sanity.spec')
|
|
@@ -140,7 +141,7 @@ class EquivalenceChecker:
|
|
|
140
141
|
tmp_conf = self.path / TMP_SANITY_CONF
|
|
141
142
|
with tmp_conf.open('w') as temp_sanity:
|
|
142
143
|
json.dump(contents, temp_sanity, indent=4)
|
|
143
|
-
context = get_args([str(tmp_conf)])
|
|
144
|
+
context = get_args([str(tmp_conf)], App.EvmApp)
|
|
144
145
|
config = InputConfig(context)
|
|
145
146
|
cfb = CertoraBuildGenerator(config, context)
|
|
146
147
|
certora_verify_generator = CertoraVerifyGenerator(context)
|
certora_cli/Mutate/mutateApp.py
CHANGED
|
@@ -28,7 +28,6 @@ from typing import Optional, Any, List, Dict, Tuple, Set, Union
|
|
|
28
28
|
import logging
|
|
29
29
|
from types import SimpleNamespace
|
|
30
30
|
import tarfile
|
|
31
|
-
import json5
|
|
32
31
|
import csv
|
|
33
32
|
import time
|
|
34
33
|
import requests
|
|
@@ -44,7 +43,9 @@ from CertoraProver.certoraContextValidator import KEY_ENV_VAR
|
|
|
44
43
|
from Mutate import mutateConstants as MConstants
|
|
45
44
|
from Shared import certoraUtils as Util
|
|
46
45
|
from Shared.certoraLogging import LoggingManager
|
|
47
|
-
from certoraRun import run_certora
|
|
46
|
+
from certoraRun import run_certora
|
|
47
|
+
from Shared.proverCommon import CertoraRunResult, CertoraFoundViolations
|
|
48
|
+
from certoraSorobanProver import run_soroban_prover
|
|
48
49
|
from Shared import certoraValidateFuncs as Vf
|
|
49
50
|
from CertoraProver.Compiler.CompilerCollectorFactory import get_relevant_compiler
|
|
50
51
|
from Mutate import mutateUtil as MutUtil
|
|
@@ -52,6 +53,7 @@ from Mutate import mutateAttributes as MutAttrs
|
|
|
52
53
|
import CertoraProver.certoraContextAttributes as Attrs
|
|
53
54
|
from CertoraProver.certoraContextClass import CertoraContext
|
|
54
55
|
from rustMutator import run_universal_mutator
|
|
56
|
+
from CertoraProver import certoraContextValidator as Cv
|
|
55
57
|
|
|
56
58
|
class RunTimedout(Exception):
|
|
57
59
|
pass
|
|
@@ -496,7 +498,7 @@ class WebUtils:
|
|
|
496
498
|
mutation_test_domain = MConstants.MUTATION_TEST_REPORT_DEV
|
|
497
499
|
else:
|
|
498
500
|
raise Util.CertoraUserInputError(f"Invalid server name {args.server}")
|
|
499
|
-
self.mutation_test_id_url = f"https://{domain}/
|
|
501
|
+
self.mutation_test_id_url = f"https://{domain}/v1/domain/mutation-tests/initiate/"
|
|
500
502
|
self.mutation_test_submit_final_result_url = f"https://{domain}/mutationTesting/getUploadInfo/"
|
|
501
503
|
self.mutation_test_final_result_url = f"https://{mutation_test_domain}"
|
|
502
504
|
mutation_logger.debug(f"Using server {args.server} with mutation_test_id_url {self.mutation_test_id_url}")
|
|
@@ -542,8 +544,8 @@ class WebUtils:
|
|
|
542
544
|
try:
|
|
543
545
|
return requests.put(url, json=body, timeout=self.request_timeout,
|
|
544
546
|
headers=headers)
|
|
545
|
-
except Exception:
|
|
546
|
-
mutation_logger.debug(f"attempt {i} failed to put url {url}.")
|
|
547
|
+
except Exception as e:
|
|
548
|
+
mutation_logger.debug(f"attempt {i} failed to put url {url}. {e}")
|
|
547
549
|
return None
|
|
548
550
|
|
|
549
551
|
def get_response_with_timeout(self, url: str,
|
|
@@ -708,6 +710,7 @@ class MutateApp:
|
|
|
708
710
|
dump_csv: Optional[Path]
|
|
709
711
|
sync: bool
|
|
710
712
|
wait_for_original_run: bool
|
|
713
|
+
url_visibility: Vf.UrlVisibilityOptions
|
|
711
714
|
collect_file: Optional[Path]
|
|
712
715
|
poll_timeout: Optional[int]
|
|
713
716
|
max_timeout_attempts_count: Optional[int]
|
|
@@ -720,7 +723,7 @@ class MutateApp:
|
|
|
720
723
|
def __init__(self, args_list: List[str]) -> None:
|
|
721
724
|
self.mutation_test_id = ''
|
|
722
725
|
self.numb_of_jobs: int = 0
|
|
723
|
-
self.
|
|
726
|
+
self.backup_path: Optional[Path] = None
|
|
724
727
|
self.sources_dir: Optional[Path] = None
|
|
725
728
|
self.with_split_stats_data = False
|
|
726
729
|
self.manual_mutants_list: List[Mutant] = list()
|
|
@@ -879,9 +882,9 @@ class MutateApp:
|
|
|
879
882
|
" verification results. Please remove or replace the mutation")
|
|
880
883
|
|
|
881
884
|
def restore_backups(self) -> None:
|
|
882
|
-
|
|
883
|
-
Util.restore_backup(backup_path)
|
|
884
|
-
self.
|
|
885
|
+
if self.backup_path:
|
|
886
|
+
Util.restore_backup(self.backup_path)
|
|
887
|
+
self.backup_path = None
|
|
885
888
|
|
|
886
889
|
def submit_soroban(self) -> None:
|
|
887
890
|
|
|
@@ -928,12 +931,14 @@ class MutateApp:
|
|
|
928
931
|
# run mutants
|
|
929
932
|
mutant_runs = []
|
|
930
933
|
try:
|
|
931
|
-
self.backup_paths = []
|
|
932
934
|
for mutant in all_mutants:
|
|
933
935
|
# call certoraRun for each mutant we found
|
|
934
936
|
mutant_runs.append(self.run_mutant_soroban(mutant))
|
|
937
|
+
self.restore_backups()
|
|
935
938
|
finally:
|
|
936
|
-
self.
|
|
939
|
+
if self.backup_path:
|
|
940
|
+
mutation_logger.warning("Not empty backup path")
|
|
941
|
+
self.restore_backups()
|
|
937
942
|
|
|
938
943
|
# wrap it all up and make the input for the 2nd step: the collector
|
|
939
944
|
assert self.collect_file, "submit_soroban: no collect_file"
|
|
@@ -1141,10 +1146,11 @@ class MutateApp:
|
|
|
1141
1146
|
def run_mutant_soroban(self, mutant: Mutant) -> MutantJob:
|
|
1142
1147
|
file_to_mutate = Path(mutant.original_filename)
|
|
1143
1148
|
# create a backup copy (if needed) by adding '.backup' to the end of the file
|
|
1144
|
-
if Util.get_backup_path(file_to_mutate)
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
self.
|
|
1149
|
+
if self.backup_path and Util.get_backup_path(file_to_mutate) != self.backup_path:
|
|
1150
|
+
raise Util.ImplementationError("backup_path is set but does not match the file to mutate")
|
|
1151
|
+
if not self.backup_path:
|
|
1152
|
+
self.backup_path = Util.create_backup(file_to_mutate)
|
|
1153
|
+
assert self.backup_path, f"run_mutant_soroban: create_backup for {file_to_mutate} failed"
|
|
1148
1154
|
shutil.copy(mutant.filename, file_to_mutate)
|
|
1149
1155
|
|
|
1150
1156
|
try:
|
|
@@ -1180,6 +1186,7 @@ class MutateApp:
|
|
|
1180
1186
|
if solc:
|
|
1181
1187
|
compiler = solc
|
|
1182
1188
|
elif compiler_map:
|
|
1189
|
+
Cv.check_contract_name_arg_inputs(self.prover_context)
|
|
1183
1190
|
compiler = get_relevant_compiler(path_to_file, self.prover_context)
|
|
1184
1191
|
if not compiler:
|
|
1185
1192
|
raise Util.CertoraUserInputError(f"Cannot resolve Solidity compiler for {path_to_file}: "
|
|
@@ -1255,6 +1262,8 @@ class MutateApp:
|
|
|
1255
1262
|
for gambit_obj in self.gambit:
|
|
1256
1263
|
if MConstants.NUM_MUTANTS not in gambit_obj:
|
|
1257
1264
|
gambit_obj[MConstants.NUM_MUTANTS] = DEFAULT_NUM_MUTANTS
|
|
1265
|
+
else:
|
|
1266
|
+
gambit_obj[MConstants.NUM_MUTANTS] = int(gambit_obj[MConstants.NUM_MUTANTS])
|
|
1258
1267
|
gambit_obj[MConstants.SOLC] = self.get_solc_version(Path(gambit_obj[MConstants.FILENAME]))
|
|
1259
1268
|
gambit_obj.update(shared_attributes)
|
|
1260
1269
|
with MConstants.TMP_GAMBIT_PATH.open('w') as f:
|
|
@@ -1373,7 +1382,7 @@ class MutateApp:
|
|
|
1373
1382
|
for mutant in self.universal_mutator:
|
|
1374
1383
|
file_to_mutate = Path(os.path.normpath(mutant[MConstants.FILE_TO_MUTATE]))
|
|
1375
1384
|
mutants_location = Path(mutant[MConstants.MUTANTS_LOCATION])
|
|
1376
|
-
num_of_mutants = mutant[MConstants.NUM_MUTANTS]
|
|
1385
|
+
num_of_mutants = int(mutant[MConstants.NUM_MUTANTS])
|
|
1377
1386
|
run_universal_mutator(file_to_mutate, self.prover_context.build_script, mutants_location, num_of_mutants)
|
|
1378
1387
|
self.add_dir_to_mutants(ret_mutants, mutants_location, file_to_mutate)
|
|
1379
1388
|
return ret_mutants
|
|
@@ -1385,6 +1394,8 @@ class MutateApp:
|
|
|
1385
1394
|
args = ["--compilation_steps_only"]
|
|
1386
1395
|
if self.prover_version:
|
|
1387
1396
|
args += ['--prover_version', self.prover_version]
|
|
1397
|
+
if self.server:
|
|
1398
|
+
args += ['--server', self.server]
|
|
1388
1399
|
run_certora([str(self.conf)] + args)
|
|
1389
1400
|
except CertoraFoundViolations: # violations should not stop execution
|
|
1390
1401
|
pass
|
|
@@ -1401,6 +1412,8 @@ class MutateApp:
|
|
|
1401
1412
|
certora_args = [str(conf_file), "--run_source", "MUTATION", "--msg", msg]
|
|
1402
1413
|
if self.wait_for_original_run:
|
|
1403
1414
|
certora_args.append("--wait_for_results")
|
|
1415
|
+
if self.url_visibility:
|
|
1416
|
+
certora_args.extend(["--url_visibility", str(self.url_visibility)])
|
|
1404
1417
|
if self.with_split_stats_data and os.environ.get("WITH_AUTOCONFING", False) == '1':
|
|
1405
1418
|
certora_args += ['--prover_resource_files', f"ac:{MConstants.SPLIT_STATS_DATA}"]
|
|
1406
1419
|
if mutation_test_id:
|
|
@@ -1416,7 +1429,10 @@ class MutateApp:
|
|
|
1416
1429
|
certora_args.extend(["--disable_local_typechecking"])
|
|
1417
1430
|
mutation_logger.debug(f"Running the Prover: {certora_args}")
|
|
1418
1431
|
try:
|
|
1419
|
-
|
|
1432
|
+
if self.is_soroban_run():
|
|
1433
|
+
certora_run_result = run_soroban_prover(certora_args)
|
|
1434
|
+
else:
|
|
1435
|
+
certora_run_result = run_certora(certora_args)
|
|
1420
1436
|
except CertoraFoundViolations as e:
|
|
1421
1437
|
assert e.results, "expect e.results not to be None"
|
|
1422
1438
|
certora_run_result = e.results
|
|
@@ -1865,7 +1881,7 @@ class MutateApp:
|
|
|
1865
1881
|
|
|
1866
1882
|
with open(self.conf, 'r') as conf_file:
|
|
1867
1883
|
try:
|
|
1868
|
-
self.prover_context = CertoraContext(**
|
|
1884
|
+
self.prover_context = CertoraContext(**Util.read_conf_file(conf_file))
|
|
1869
1885
|
self.check_prover_context()
|
|
1870
1886
|
except Exception as e:
|
|
1871
1887
|
raise Util.CertoraUserInputError(f"Failed to parse {self.conf} as JSON", e)
|
|
@@ -1893,7 +1909,7 @@ class MutateApp:
|
|
|
1893
1909
|
common_flags.extend(['--optimize'])
|
|
1894
1910
|
|
|
1895
1911
|
if hasattr(self.prover_context, MConstants.SOLC_ALLOW_PATH):
|
|
1896
|
-
common_flags.extend([
|
|
1912
|
+
common_flags.extend(['--allow-paths', '/'])
|
|
1897
1913
|
|
|
1898
1914
|
if hasattr(self.prover_context, MConstants.SOLC_EVM_VERSION):
|
|
1899
1915
|
common_flags.extend(['--evm-version', self.prover_context.solc_evm_version])
|
|
@@ -1908,7 +1924,9 @@ class MutateApp:
|
|
|
1908
1924
|
if not self.manual_mutants:
|
|
1909
1925
|
return
|
|
1910
1926
|
|
|
1911
|
-
solc = self.get_solc_version(
|
|
1927
|
+
solc = self.get_solc_version(Path(mutant.original_filename))
|
|
1928
|
+
if not solc:
|
|
1929
|
+
raise Util.CertoraUserInputError(f"Unable to find a compiler for manual mutant {mutant.original_filename}")
|
|
1912
1930
|
|
|
1913
1931
|
via_ir_flag = []
|
|
1914
1932
|
if getattr(self.prover_context, MConstants.SOLC_VIA_IR, '') or \
|
|
@@ -1919,10 +1937,11 @@ class MutateApp:
|
|
|
1919
1937
|
if self.test == str(Util.TestValue.CHECK_MANUAL_COMPILATION):
|
|
1920
1938
|
raise Util.TestResultsReady(' '.join(args))
|
|
1921
1939
|
with Util.change_working_directory(find_cwd(trg_dir)):
|
|
1922
|
-
result = subprocess.run(args, stdout=subprocess.DEVNULL, stderr=subprocess.
|
|
1940
|
+
result = subprocess.run(args, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
|
1923
1941
|
if result.returncode:
|
|
1924
1942
|
mutation_logger.debug(f"mutation compilation failed: cmd: {' '.join(args)}\n cwd: {os.getcwd()}")
|
|
1925
|
-
raise Util.CertoraUserInputError(f"mutation file {mutant.filename} failed to compile"
|
|
1943
|
+
raise Util.CertoraUserInputError(f"mutation file {mutant.filename} failed to compile\n\n"
|
|
1944
|
+
f"{result.stderr.decode()}")
|
|
1926
1945
|
|
|
1927
1946
|
|
|
1928
1947
|
def rec_collect_statuses_children(rule: Dict[str, Any], statuses: List[str]) -> None:
|