Skip to content

Commit a3ab8e1

Browse files
author
Leo
authored
Merge pull request #565 from ethereum/smt_fix_tests
Fix smtchecker callback tests and add Eldarica
2 parents f2fdcfb + 40d563f commit a3ab8e1

File tree

7 files changed

+170
-39
lines changed

7 files changed

+170
-39
lines changed

README.md

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -144,36 +144,54 @@ for (var contractName in output.contracts['test.sol']) {
144144
}
145145
```
146146

147-
The ``smtSolver`` callback function is used to solve SMT queries generated by
147+
Since version 0.5.1, the ``smtSolver`` callback function is used to solve SMT queries generated by
148148
Solidity's SMTChecker. If you have an SMT solver installed locally, it can
149149
be used to solve the given queries, where the callback must synchronously
150150
return either an error or the result from the solver. A default
151-
``smtSolver`` callback is distributed by ``solc-js``, which relies on either
152-
Z3 or CVC4 being installed locally.
151+
``smtSolver`` callback is included in this package via the module
152+
``smtchecker.js`` which exports the ``smtCallback`` function that takes 1) a
153+
function that takes queries and returns the solving result, and 2) a solver
154+
configuration object. The module ``smtsolver.js`` has a few predefined solver
155+
configurations, and relies on Z3, Eldarica or CVC4 being installed locally. It
156+
exports the list of locally found solvers and a function that invokes a given
157+
solver.
158+
159+
The API of the SMT callback is **experimental** and can change at any time.
160+
The last change was in version 0.8.11.
153161

154162
#### Example usage with smtSolver callback
155163

156164
```javascript
157165
var solc = require('solc');
158-
var smt = require('smtsolver');
166+
const smtchecker = require('solc/smtchecker');
167+
const smtsolver = require('solc/smtsolver');
159168
// Note that this example only works via node and not in the browser.
160169

161170
var input = {
162171
language: 'Solidity',
163172
sources: {
164173
'test.sol': {
165-
content: 'pragma experimental SMTChecker; contract C { function f(uint x) public { assert(x > 0); } }'
174+
content: 'contract C { function f(uint x) public { assert(x > 0); } }'
175+
}
176+
},
177+
settings: {
178+
modelChecker: {
179+
engine: "chc",
180+
solvers: [ "smtlib2" ]
166181
}
167182
}
168183
};
169184

170185
var output = JSON.parse(
171-
solc.compile(JSON.stringify(input), { smtSolver: smt.smtSolver })
186+
solc.compile(
187+
JSON.stringify(input),
188+
{ smtSolver: smtchecker.smtCallback(smtsolver.smtSolver, smtsolver.availableSolvers[0]) }
189+
)
172190
);
173191

174192
```
175193
The assertion is clearly false, and an ``assertion failure`` warning
176-
should be returned.
194+
should be returned, together with a counterexample.
177195

178196
#### Low-level API
179197

smtchecker.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// The function runs an SMT solver on each query and adjusts the input for
44
// another run.
55
// Returns null if no solving is requested.
6-
function handleSMTQueries (inputJSON, outputJSON, solver) {
6+
function handleSMTQueries (inputJSON, outputJSON, solverFunction, solver) {
77
const auxInputReq = outputJSON.auxiliaryInputRequested;
88
if (!auxInputReq) {
99
return null;
@@ -16,7 +16,7 @@ function handleSMTQueries (inputJSON, outputJSON, solver) {
1616

1717
const responses = {};
1818
for (const query in queries) {
19-
responses[query] = solver(queries[query]);
19+
responses[query] = solverFunction(queries[query], solver);
2020
}
2121

2222
// Note: all existing solved queries are replaced.
@@ -25,10 +25,10 @@ function handleSMTQueries (inputJSON, outputJSON, solver) {
2525
return inputJSON;
2626
}
2727

28-
function smtCallback (solver) {
28+
function smtCallback (solverFunction, solver) {
2929
return function (query) {
3030
try {
31-
const result = solver(query);
31+
const result = solverFunction(query, solver);
3232
return { contents: result };
3333
} catch (err) {
3434
return { error: err };

smtsolver.js

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,48 @@ const execSync = require('child_process').execSync;
33
const fs = require('fs');
44
const tmp = require('tmp');
55

6+
// Timeout in ms.
67
const timeout = 10000;
78

89
const potentialSolvers = [
910
{
1011
name: 'z3',
12+
command: 'z3',
1113
params: '-smt2 rlimit=20000000 rewriter.pull_cheap_ite=true fp.spacer.q3.use_qgen=true fp.spacer.mbqi=false fp.spacer.ground_pobs=false'
1214
},
15+
{
16+
name: 'Eldarica',
17+
command: 'eld',
18+
params: '-horn -t:' + (timeout / 1000) // Eldarica takes timeout in seconds.
19+
},
1320
{
1421
name: 'cvc4',
22+
command: 'cvc4',
1523
params: '--lang=smt2 --tlimit=' + timeout
1624
}
1725
];
18-
const solvers = potentialSolvers.filter(solver => commandExistsSync(solver.name));
1926

20-
function solve (query) {
21-
if (solvers.length === 0) {
22-
throw new Error('No SMT solver available. Assertion checking will not be performed.');
27+
const solvers = potentialSolvers.filter(solver => commandExistsSync(solver.command));
28+
29+
function solve (query, solver) {
30+
if (solver === undefined) {
31+
if (solvers.length === 0) {
32+
throw new Error('No SMT solver available. Assertion checking will not be performed.');
33+
} else {
34+
solver = solvers[0];
35+
}
2336
}
2437

2538
const tmpFile = tmp.fileSync({ postfix: '.smt2' });
2639
fs.writeFileSync(tmpFile.name, query);
27-
// TODO For now only the first SMT solver found is used.
28-
// At some point a computation similar to the one done in
29-
// SMTPortfolio::check should be performed, where the results
30-
// given by different solvers are compared and an error is
31-
// reported if solvers disagree (i.e. SAT vs UNSAT).
3240
let solverOutput;
3341
try {
3442
solverOutput = execSync(
35-
solvers[0].name + ' ' + solvers[0].params + ' ' + tmpFile.name, {
36-
stdio: 'pipe'
43+
solver.command + ' ' + solver.params + ' ' + tmpFile.name, {
44+
encoding: 'utf8',
45+
maxBuffer: 1024 * 1024 * 1024,
46+
stdio: 'pipe',
47+
timeout: timeout // Enforce timeout on the process, since solvers can sometimes go around it.
3748
}
3849
).toString();
3950
} catch (e) {
@@ -44,7 +55,9 @@ function solve (query) {
4455
if (
4556
!solverOutput.startsWith('sat') &&
4657
!solverOutput.startsWith('unsat') &&
47-
!solverOutput.startsWith('unknown')
58+
!solverOutput.startsWith('unknown') &&
59+
!solverOutput.startsWith('(error') && // Eldarica reports errors in an sexpr, for example: '(error "Failed to reconstruct array model")'
60+
!solverOutput.startsWith('error')
4861
) {
4962
throw new Error('Failed to solve SMT query. ' + e.toString());
5063
}
@@ -56,5 +69,5 @@ function solve (query) {
5669

5770
module.exports = {
5871
smtSolver: solve,
59-
availableSolvers: solvers.length
72+
availableSolvers: solvers
6073
};

solc.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,14 @@ if (options.standardJson) {
129129
let output = reformatJsonIfRequested(solc.compile(input, callbacks));
130130

131131
try {
132-
const inputJSON = smtchecker.handleSMTQueries(JSON.parse(input), JSON.parse(output), smtsolver.smtSolver);
133-
if (inputJSON) {
134-
if (program.verbose) { console.log('>>> Retrying compilation with SMT:\n' + toFormattedJson(inputJSON) + '\n'); }
135-
output = reformatJsonIfRequested(solc.compile(JSON.stringify(inputJSON), callbacks));
132+
if (smtsolver.availableSolvers.length === 0) {
133+
console.log('>>> Cannot retry compilation with SMT because there are no SMT solvers available.');
134+
} else {
135+
const inputJSON = smtchecker.handleSMTQueries(JSON.parse(input), JSON.parse(output), smtsolver.smtSolver, smtsolver.availableSolvers[0]);
136+
if (inputJSON) {
137+
if (program.verbose) { console.log('>>> Retrying compilation with SMT:\n' + toFormattedJson(inputJSON) + '\n'); }
138+
output = reformatJsonIfRequested(solc.compile(JSON.stringify(inputJSON), callbacks));
139+
}
136140
}
137141
} catch (e) {
138142
const addError = {

test/smtCheckerTests/loop.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
contract C {
2+
function f(uint x) public pure {
3+
uint i = 0;
4+
while (i < x)
5+
++i;
6+
assert(i == x);
7+
}
8+
}

test/smtcallback.js

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ tape('SMTCheckerCallback', function (t) {
119119
{ smtSolver: test.cb }
120120
));
121121
const errors = collectErrors(output);
122-
st.ok(expectErrors(errors, test.expectations));
122+
st.ok(expectErrors(errors, test.expectations, false));
123123
}
124124
st.end();
125125
});
@@ -132,8 +132,10 @@ tape('SMTCheckerCallback', function (t) {
132132
return;
133133
}
134134

135-
if (smtsolver.availableSolvers === 0) {
136-
st.skip('No SMT solver available.');
135+
// For these tests we actually need z3/Spacer.
136+
const z3HornSolvers = smtsolver.availableSolvers.filter(solver => solver.command === 'z3');
137+
if (z3HornSolvers.length === 0) {
138+
st.skip('z3/Spacer not available.');
137139
st.end();
138140
return;
139141
}
@@ -167,9 +169,8 @@ tape('SMTCheckerCallback', function (t) {
167169
if (source.includes(option)) {
168170
const idx = source.indexOf(option);
169171
if (source.indexOf(option, idx + 1) !== -1) {
170-
st.skip('SMTEngine option given multiple times.');
171-
st.end();
172-
return;
172+
st.comment('SMTEngine option given multiple times.');
173+
continue;
173174
}
174175
const re = new RegExp(option + '(\\w+)');
175176
const m = source.match(re);
@@ -211,15 +212,23 @@ tape('SMTCheckerCallback', function (t) {
211212
// `pragma experimental SMTChecker;` was deprecated in 0.8.4
212213
if (semver.gt(solc.semver(), '0.8.3')) {
213214
const engine = test.engine !== undefined ? test.engine : 'all';
214-
settings = { modelChecker: { engine: engine } };
215+
settings = {
216+
modelChecker: {
217+
engine: engine,
218+
solvers: [
219+
'smtlib2'
220+
]
221+
}
222+
};
215223
}
216224
const output = JSON.parse(solc.compile(
217225
JSON.stringify({
218226
language: 'Solidity',
219227
sources: test.solidity,
220228
settings: settings
221229
}),
222-
{ smtSolver: smtchecker.smtCallback(smtsolver.smtSolver) }
230+
// This test needs z3 specifically.
231+
{ smtSolver: smtchecker.smtCallback(smtsolver.smtSolver, z3HornSolvers[0]) }
223232
));
224233
st.ok(output);
225234

test/smtchecker.js

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,104 @@
11
const tape = require('tape');
2+
const semver = require('semver');
3+
const solc = require('../index.js');
24
const smtchecker = require('../smtchecker.js');
5+
const smtsolver = require('../smtsolver.js');
36

7+
const preamble = 'pragma solidity >=0.0;\n// SPDX-License-Identifier: GPL-3.0\n';
8+
//
49
tape('SMTChecker', function (t) {
10+
// We use null for `solverFunction` and `solver` when calling `handleSMTQueries`
11+
// because these tests do not call a solver.
12+
513
t.test('smoke test with no axuiliaryInputRequested', function (st) {
614
const input = {};
715
const output = {};
8-
st.equal(smtchecker.handleSMTQueries(input, output), null);
16+
st.equal(smtchecker.handleSMTQueries(input, output, null, null), null);
917
st.end();
1018
});
1119

1220
t.test('smoke test with no smtlib2queries', function (st) {
1321
const input = {};
1422
const output = { auxiliaryInputRequested: {} };
15-
st.equal(smtchecker.handleSMTQueries(input, output), null);
23+
st.equal(smtchecker.handleSMTQueries(input, output, null, null), null);
1624
st.end();
1725
});
1826

1927
t.test('smoke test with empty smtlib2queries', function (st) {
2028
const input = {};
2129
const output = { auxiliaryInputRequested: { smtlib2queries: { } } };
22-
st.equal(smtchecker.handleSMTQueries(input, output), null);
30+
st.equal(smtchecker.handleSMTQueries(input, output, null, null), null);
31+
st.end();
32+
});
33+
34+
t.test('smtCallback should return type function', (st) => {
35+
const response = smtchecker.smtCallback(() => {});
36+
st.equal(typeof response, 'function');
37+
st.end();
38+
});
39+
40+
t.test('smtCallback should error when passed parser fails', (st) => {
41+
const cbFun = smtchecker.smtCallback((content) => { throw new Error(content); });
42+
const response = cbFun('expected-error-message');
43+
44+
st.deepEqual(response, { error: new Error('expected-error-message') });
45+
st.end();
46+
});
47+
48+
t.test('smtCallback should return content when passed parser does not fail', (st) => {
49+
const cbFun = smtchecker.smtCallback((content) => { return content; });
50+
const response = cbFun('expected-content-message');
51+
52+
st.deepEqual(response, { contents: 'expected-content-message' });
53+
st.end();
54+
});
55+
});
56+
57+
tape('SMTCheckerWithSolver', function (t) {
58+
// In these tests we require z3 to actually run the solver.
59+
// This uses the SMT double run mechanism instead of the callback.
60+
61+
t.test('Simple test with axuiliaryInputRequested', function (st) {
62+
const z3 = smtsolver.availableSolvers.filter(solver => solver.command === 'z3');
63+
if (z3.length === 0) {
64+
st.skip('Test requires z3.');
65+
st.end();
66+
return;
67+
}
68+
69+
if (semver.lt(solc.semver(), '0.8.7')) {
70+
st.skip('This test requires Solidity 0.8.7 to enable all SMTChecker options.');
71+
st.end();
72+
return;
73+
}
74+
75+
const settings = {
76+
modelChecker: {
77+
engine: 'chc',
78+
solvers: ['smtlib2']
79+
}
80+
};
81+
82+
const source = { a: { content: preamble + '\ncontract C { function f(uint x) public pure { assert(x > 0); } }' } };
83+
84+
const input = {
85+
language: 'Solidity',
86+
sources: source,
87+
settings: settings
88+
};
89+
90+
const output = JSON.parse(solc.compile(JSON.stringify(input)));
91+
st.ok(output);
92+
93+
const newInput = smtchecker.handleSMTQueries(input, output, smtsolver.smtSolver, z3[0]);
94+
st.notEqual(newInput, null);
95+
96+
const newOutput = JSON.parse(solc.compile(JSON.stringify(newInput)));
97+
st.ok(newOutput);
98+
99+
const smtErrors = newOutput.errors.filter(e => e.errorCode === '6328');
100+
st.equal(smtErrors.length, 1);
101+
23102
st.end();
24103
});
25104
});

0 commit comments

Comments
 (0)