Formal verification using Certora (#4084)
Co-authored-by: Francisco Giordano <fg@frang.io>
This commit is contained in:
109
certora/run.js
Normal file
109
certora/run.js
Normal file
@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// USAGE:
|
||||
// node certora/run.js [[CONTRACT_NAME:]SPEC_NAME] [OPTIONS...]
|
||||
// EXAMPLES:
|
||||
// node certora/run.js AccessControl
|
||||
// node certora/run.js AccessControlHarness:AccessControl
|
||||
|
||||
const MAX_PARALLEL = 4;
|
||||
|
||||
const specs = require(__dirname + '/specs.json');
|
||||
|
||||
const proc = require('child_process');
|
||||
const { PassThrough } = require('stream');
|
||||
const events = require('events');
|
||||
const limit = require('p-limit')(MAX_PARALLEL);
|
||||
|
||||
let [, , request = '', ...extraOptions] = process.argv;
|
||||
if (request.startsWith('-')) {
|
||||
extraOptions.unshift(request);
|
||||
request = '';
|
||||
}
|
||||
const [reqSpec, reqContract] = request.split(':').reverse();
|
||||
|
||||
for (const { spec, contract, files, options = [] } of Object.values(specs)) {
|
||||
if ((!reqSpec || reqSpec === spec) && (!reqContract || reqContract === contract)) {
|
||||
limit(runCertora, spec, contract, files, [...options, ...extraOptions]);
|
||||
}
|
||||
}
|
||||
|
||||
// Run certora, aggregate the output and print it at the end
|
||||
async function runCertora(spec, contract, files, options = []) {
|
||||
const args = [...files, '--verify', `${contract}:certora/specs/${spec}.spec`, ...options];
|
||||
const child = proc.spawn('certoraRun', args);
|
||||
|
||||
const stream = new PassThrough();
|
||||
const output = collect(stream);
|
||||
|
||||
child.stdout.pipe(stream, { end: false });
|
||||
child.stderr.pipe(stream, { end: false });
|
||||
|
||||
// as soon as we have a jobStatus link, print it
|
||||
stream.on('data', function logStatusUrl(data) {
|
||||
const urls = data.toString('utf8').match(/https?:\S*/g);
|
||||
for (const url of urls ?? []) {
|
||||
if (url.includes('/jobStatus/')) {
|
||||
console.error(`[${spec}] ${url}`);
|
||||
stream.off('data', logStatusUrl);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// wait for process end
|
||||
const [code, signal] = await events.once(child, 'exit');
|
||||
|
||||
// error
|
||||
if (code || signal) {
|
||||
console.error(`[${spec}] Exited with code ${code || signal}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
// get all output
|
||||
stream.end();
|
||||
|
||||
// write results in markdown format
|
||||
writeEntry(spec, contract, code || signal, (await output).match(/https:\S*/)[0]);
|
||||
|
||||
// write all details
|
||||
console.error(`+ certoraRun ${args.join(' ')}\n` + (await output));
|
||||
}
|
||||
|
||||
// Collects stream data into a string
|
||||
async function collect(stream) {
|
||||
const buffers = [];
|
||||
for await (const data of stream) {
|
||||
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
||||
buffers.push(buf);
|
||||
}
|
||||
return Buffer.concat(buffers).toString('utf8');
|
||||
}
|
||||
|
||||
// Formatting
|
||||
let hasHeader = false;
|
||||
|
||||
function formatRow(...array) {
|
||||
return ['', ...array, ''].join(' | ');
|
||||
}
|
||||
|
||||
function writeHeader() {
|
||||
console.log(formatRow('spec', 'contract', 'result', 'status', 'output'));
|
||||
console.log(formatRow('-', '-', '-', '-', '-'));
|
||||
}
|
||||
|
||||
function writeEntry(spec, contract, success, url) {
|
||||
if (!hasHeader) {
|
||||
hasHeader = true;
|
||||
writeHeader();
|
||||
}
|
||||
console.log(
|
||||
formatRow(
|
||||
spec,
|
||||
contract,
|
||||
success ? ':x:' : ':heavy_check_mark:',
|
||||
`[link](${url})`,
|
||||
`[link](${url.replace('/jobStatus/', '/output/')})`,
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user