Skip to content

Commit 358324e

Browse files
authored
Merge pull request #10679 from ethereum/backwards-compatibility-for-bytecode-comparison
Backwards compatibility for bytecode comparison
2 parents dde6353 + 7bebcb7 commit 358324e

File tree

5 files changed

+409
-56
lines changed

5 files changed

+409
-56
lines changed

scripts/bytecodecompare/prepare_report.js

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,89 @@ const fs = require('fs')
44

55
const compiler = require('./solc-js/wrapper.js')(require('./solc-js/soljson.js'))
66

7+
8+
function loadSource(sourceFileName, stripSMTPragmas)
9+
{
10+
source = fs.readFileSync(sourceFileName).toString()
11+
12+
if (stripSMTPragmas)
13+
// NOTE: replace() with string parameter replaces only the first occurrence.
14+
return source.replace('pragma experimental SMTChecker;', '');
15+
16+
return source
17+
}
18+
19+
function cleanString(string)
20+
{
21+
if (string !== undefined)
22+
string = string.trim()
23+
return (string !== '' ? string : undefined)
24+
}
25+
26+
27+
let stripSMTPragmas = false
28+
let firstFileArgumentIndex = 2
29+
30+
if (process.argv.length >= 3 && process.argv[2] === '--strip-smt-pragmas')
31+
{
32+
stripSMTPragmas = true
33+
firstFileArgumentIndex = 3
34+
}
35+
736
for (const optimize of [false, true])
837
{
9-
for (const filename of process.argv.slice(2))
38+
for (const filename of process.argv.slice(firstFileArgumentIndex))
1039
{
1140
if (filename !== undefined)
1241
{
13-
const input = {
42+
let input = {
1443
language: 'Solidity',
1544
sources: {
16-
[filename]: {content: fs.readFileSync(filename).toString()}
45+
[filename]: {content: loadSource(filename, stripSMTPragmas)}
1746
},
1847
settings: {
1948
optimizer: {enabled: optimize},
20-
outputSelection: {'*': {'*': ['evm.bytecode.object', 'metadata']}},
21-
"modelChecker": {"engine": "none"}
49+
outputSelection: {'*': {'*': ['evm.bytecode.object', 'metadata']}}
2250
}
2351
}
52+
if (!stripSMTPragmas)
53+
input['settings']['modelChecker'] = {engine: 'none'}
2454

25-
const result = JSON.parse(compiler.compile(JSON.stringify(input)))
55+
let serializedOutput
56+
let result
57+
const serializedInput = JSON.stringify(input)
2658

2759
let internalCompilerError = false
28-
if ('errors' in result)
60+
try
2961
{
30-
for (const error of result['errors'])
31-
// JSON interface still returns contract metadata in case of an internal compiler error while
32-
// CLI interface does not. To make reports comparable we must force this case to be detected as
33-
// an error in both cases.
34-
if (['UnimplementedFeatureError', 'CompilerError', 'CodeGenerationError'].includes(error['type']))
35-
{
36-
internalCompilerError = true
37-
break
38-
}
62+
serializedOutput = compiler.compile(serializedInput)
63+
}
64+
catch (exception)
65+
{
66+
internalCompilerError = true
67+
}
68+
69+
if (!internalCompilerError)
70+
{
71+
result = JSON.parse(serializedOutput)
72+
73+
if ('errors' in result)
74+
for (const error of result['errors'])
75+
// JSON interface still returns contract metadata in case of an internal compiler error while
76+
// CLI interface does not. To make reports comparable we must force this case to be detected as
77+
// an error in both cases.
78+
if (['UnimplementedFeatureError', 'CompilerError', 'CodeGenerationError'].includes(error['type']))
79+
{
80+
internalCompilerError = true
81+
break
82+
}
3983
}
4084

4185
if (
86+
internalCompilerError ||
4287
!('contracts' in result) ||
4388
Object.keys(result['contracts']).length === 0 ||
44-
Object.keys(result['contracts']).every(file => Object.keys(result['contracts'][file]).length === 0) ||
45-
internalCompilerError
89+
Object.keys(result['contracts']).every(file => Object.keys(result['contracts'][file]).length === 0)
4690
)
4791
// NOTE: do not exit here because this may be run on source which cannot be compiled
4892
console.log(filename + ': <ERROR>')
@@ -55,10 +99,15 @@ for (const optimize of [false, true])
5599
let bytecode = '<NO BYTECODE>'
56100
let metadata = '<NO METADATA>'
57101

58-
if ('evm' in contractResults && 'bytecode' in contractResults['evm'] && 'object' in contractResults['evm']['bytecode'])
59-
bytecode = contractResults.evm.bytecode.object
102+
if (
103+
'evm' in contractResults &&
104+
'bytecode' in contractResults['evm'] &&
105+
'object' in contractResults['evm']['bytecode'] &&
106+
cleanString(contractResults.evm.bytecode.object) !== undefined
107+
)
108+
bytecode = cleanString(contractResults.evm.bytecode.object)
60109

61-
if ('metadata' in contractResults)
110+
if ('metadata' in contractResults && cleanString(contractResults.metadata) !== undefined)
62111
metadata = contractResults.metadata
63112

64113
console.log(filename + ':' + contractName + ' ' + bytecode)

scripts/bytecodecompare/prepare_report.py

Lines changed: 116 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,29 @@
1313
from typing import List, Optional, Tuple, Union
1414

1515

16-
CONTRACT_SEPARATOR_PATTERN = re.compile(r'^======= (?P<file_name>.+):(?P<contract_name>[^:]+) =======$', re.MULTILINE)
17-
BYTECODE_REGEX = re.compile(r'^Binary:\n(?P<bytecode>.*)$', re.MULTILINE)
18-
METADATA_REGEX = re.compile(r'^Metadata:\n(?P<metadata>\{.*\})$', re.MULTILINE)
16+
CONTRACT_SEPARATOR_PATTERN = re.compile(
17+
r'^ *======= +(?:(?P<file_name>.+) *:)? *(?P<contract_name>[^:]+) +======= *$',
18+
re.MULTILINE
19+
)
20+
BYTECODE_REGEX = re.compile(r'^ *Binary: *\n(?P<bytecode>.*[0-9a-f$_]+.*)$', re.MULTILINE)
21+
METADATA_REGEX = re.compile(r'^ *Metadata: *\n *(?P<metadata>\{.*\}) *$', re.MULTILINE)
1922

2023

2124
class CompilerInterface(Enum):
2225
CLI = 'cli'
2326
STANDARD_JSON = 'standard-json'
2427

2528

29+
class SMTUse(Enum):
30+
PRESERVE = 'preserve'
31+
DISABLE = 'disable'
32+
STRIP_PRAGMAS = 'strip-pragmas'
33+
34+
2635
@dataclass(frozen=True)
2736
class ContractReport:
2837
contract_name: str
29-
file_name: Path
38+
file_name: Optional[Path]
3039
bytecode: Optional[str]
3140
metadata: Optional[str]
3241

@@ -54,15 +63,23 @@ def format_report(self) -> str:
5463
return report
5564

5665

57-
def load_source(path: Union[Path, str]) -> str:
66+
def load_source(path: Union[Path, str], smt_use: SMTUse) -> str:
5867
# NOTE: newline='' disables newline conversion.
5968
# We want the file exactly as is because changing even a single byte in the source affects metadata.
6069
with open(path, mode='r', encoding='utf8', newline='') as source_file:
6170
file_content = source_file.read()
6271

72+
if smt_use == SMTUse.STRIP_PRAGMAS:
73+
return file_content.replace('pragma experimental SMTChecker;', '', 1)
74+
6375
return file_content
6476

6577

78+
def clean_string(value: Optional[str]) -> Optional[str]:
79+
value = value.strip() if value is not None else None
80+
return value if value != '' else None
81+
82+
6683
def parse_standard_json_output(source_file_name: Path, standard_json_output: str) -> FileReport:
6784
decoded_json_output = json.loads(standard_json_output.strip())
6885

@@ -89,8 +106,8 @@ def parse_standard_json_output(source_file_name: Path, standard_json_output: str
89106
file_report.contract_reports.append(ContractReport(
90107
contract_name=contract_name,
91108
file_name=Path(file_name),
92-
bytecode=contract_results.get('evm', {}).get('bytecode', {}).get('object'),
93-
metadata=contract_results.get('metadata'),
109+
bytecode=clean_string(contract_results.get('evm', {}).get('bytecode', {}).get('object')),
110+
metadata=clean_string(contract_results.get('metadata')),
94111
))
95112

96113
return file_report
@@ -113,55 +130,92 @@ def parse_cli_output(source_file_name: Path, cli_output: str) -> FileReport:
113130

114131
assert file_report.contract_reports is not None
115132
file_report.contract_reports.append(ContractReport(
116-
contract_name=contract_name,
117-
file_name=Path(file_name),
118-
bytecode=bytecode_match['bytecode'] if bytecode_match is not None else None,
119-
metadata=metadata_match['metadata'] if metadata_match is not None else None,
133+
contract_name=contract_name.strip(),
134+
file_name=Path(file_name.strip()) if file_name is not None else None,
135+
bytecode=clean_string(bytecode_match['bytecode'] if bytecode_match is not None else None),
136+
metadata=clean_string(metadata_match['metadata'] if metadata_match is not None else None),
120137
))
121138

122139
return file_report
123140

124141

125-
def prepare_compiler_input(
142+
def prepare_compiler_input( # pylint: disable=too-many-arguments
126143
compiler_path: Path,
127144
source_file_name: Path,
128145
optimize: bool,
129-
interface: CompilerInterface
146+
force_no_optimize_yul: bool,
147+
interface: CompilerInterface,
148+
smt_use: SMTUse,
149+
metadata_option_supported: bool,
130150
) -> Tuple[List[str], str]:
131151

132152
if interface == CompilerInterface.STANDARD_JSON:
133153
json_input: dict = {
134154
'language': 'Solidity',
135155
'sources': {
136-
str(source_file_name): {'content': load_source(source_file_name)}
156+
str(source_file_name): {'content': load_source(source_file_name, smt_use)}
137157
},
138158
'settings': {
139159
'optimizer': {'enabled': optimize},
140160
'outputSelection': {'*': {'*': ['evm.bytecode.object', 'metadata']}},
141-
'modelChecker': {'engine': 'none'},
142161
}
143162
}
144163

164+
if smt_use == SMTUse.DISABLE:
165+
json_input['settings']['modelChecker'] = {'engine': 'none'}
166+
145167
command_line = [str(compiler_path), '--standard-json']
146168
compiler_input = json.dumps(json_input)
147169
else:
148170
assert interface == CompilerInterface.CLI
149171

150-
compiler_options = [str(source_file_name), '--bin', '--metadata', '--model-checker-engine', 'none']
172+
compiler_options = [str(source_file_name), '--bin']
173+
if metadata_option_supported:
174+
compiler_options.append('--metadata')
151175
if optimize:
152176
compiler_options.append('--optimize')
177+
elif force_no_optimize_yul:
178+
compiler_options.append('--no-optimize-yul')
179+
if smt_use == SMTUse.DISABLE:
180+
compiler_options += ['--model-checker-engine', 'none']
153181

154182
command_line = [str(compiler_path)] + compiler_options
155-
compiler_input = load_source(source_file_name)
183+
compiler_input = load_source(source_file_name, smt_use)
156184

157185
return (command_line, compiler_input)
158186

159187

160-
def run_compiler(
188+
def detect_metadata_cli_option_support(compiler_path: Path):
189+
process = subprocess.run(
190+
[str(compiler_path.absolute()), '--metadata', '-'],
191+
input="contract C {}",
192+
encoding='utf8',
193+
capture_output=True,
194+
check=False,
195+
)
196+
197+
negative_response = "unrecognised option '--metadata'".strip()
198+
if (process.returncode == 0) != (process.stderr.strip() != negative_response):
199+
# If the error is other than expected or there's an error message but no error, don't try
200+
# to guess. Just fail.
201+
print(
202+
f"Compiler exit code: {process.returncode}\n"
203+
f"Compiler output:\n{process.stderr}\n",
204+
file=sys.stderr
205+
)
206+
raise Exception("Failed to determine if the compiler supports the --metadata option.")
207+
208+
return process.returncode == 0
209+
210+
211+
def run_compiler( # pylint: disable=too-many-arguments
161212
compiler_path: Path,
162213
source_file_name: Path,
163214
optimize: bool,
215+
force_no_optimize_yul: bool,
164216
interface: CompilerInterface,
217+
smt_use: SMTUse,
218+
metadata_option_supported: bool,
165219
tmp_dir: Path,
166220
) -> FileReport:
167221

@@ -170,7 +224,10 @@ def run_compiler(
170224
compiler_path,
171225
Path(source_file_name.name),
172226
optimize,
173-
interface
227+
force_no_optimize_yul,
228+
interface,
229+
smt_use,
230+
metadata_option_supported,
174231
)
175232

176233
process = subprocess.run(
@@ -190,7 +247,10 @@ def run_compiler(
190247
compiler_path.absolute(),
191248
Path(source_file_name.name),
192249
optimize,
193-
interface
250+
force_no_optimize_yul,
251+
interface,
252+
smt_use,
253+
metadata_option_supported,
194254
)
195255

196256
# Create a copy that we can use directly with the CLI interface
@@ -211,13 +271,30 @@ def run_compiler(
211271
return parse_cli_output(Path(source_file_name), process.stdout)
212272

213273

214-
def generate_report(source_file_names: List[str], compiler_path: Path, interface: CompilerInterface):
274+
def generate_report(
275+
source_file_names: List[str],
276+
compiler_path: Path,
277+
interface: CompilerInterface,
278+
smt_use: SMTUse,
279+
force_no_optimize_yul: bool
280+
):
281+
metadata_option_supported = detect_metadata_cli_option_support(compiler_path)
282+
215283
with open('report.txt', mode='w', encoding='utf8', newline='\n') as report_file:
216284
for optimize in [False, True]:
217285
with TemporaryDirectory(prefix='prepare_report-') as tmp_dir:
218286
for source_file_name in sorted(source_file_names):
219287
try:
220-
report = run_compiler(compiler_path, Path(source_file_name), optimize, interface, Path(tmp_dir))
288+
report = run_compiler(
289+
compiler_path,
290+
Path(source_file_name),
291+
optimize,
292+
force_no_optimize_yul,
293+
interface,
294+
smt_use,
295+
metadata_option_supported,
296+
Path(tmp_dir),
297+
)
221298
report_file.write(report.format_report())
222299
except subprocess.CalledProcessError as exception:
223300
print(
@@ -250,7 +327,21 @@ def commandline_parser() -> ArgumentParser:
250327
dest='interface',
251328
default=CompilerInterface.STANDARD_JSON.value,
252329
choices=[c.value for c in CompilerInterface],
253-
help="Compiler interface to use."
330+
help="Compiler interface to use.",
331+
)
332+
parser.add_argument(
333+
'--smt-use',
334+
dest='smt_use',
335+
default=SMTUse.DISABLE.value,
336+
choices=[s.value for s in SMTUse],
337+
help="What to do about contracts that use the experimental SMT checker."
338+
)
339+
parser.add_argument(
340+
'--force-no-optimize-yul',
341+
dest='force_no_optimize_yul',
342+
default=False,
343+
action='store_true',
344+
help="Explicitly disable Yul optimizer in CLI runs without optimization to work around a bug in solc 0.6.0 and 0.6.1."
254345
)
255346
return parser;
256347

@@ -261,4 +352,6 @@ def commandline_parser() -> ArgumentParser:
261352
glob("*.sol"),
262353
Path(options.compiler_path),
263354
CompilerInterface(options.interface),
355+
SMTUse(options.smt_use),
356+
options.force_no_optimize_yul,
264357
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
contract.sol:1:1: Warning: Source file does not specify required compiler version! Consider adding "pragma solidity ^0.4.0;".
2+
contract C {}
3+
^
4+
Spanning multiple lines.
5+
6+
======= C =======
7+
Binary:
8+
6060604052600c8060106000396000f360606040526008565b600256

0 commit comments

Comments
 (0)