mirror of openzeppelin-contracts
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
openzeppelin-contracts/certora/run.js

151 lines
3.9 KiB

#!/usr/bin/env node
// USAGE:
// node certora/run.js [[CONTRACT_NAME:]SPEC_NAME]* [--all] [--options OPTIONS...] [--specs PATH]
// EXAMPLES:
// node certora/run.js --all
// node certora/run.js AccessControl
// node certora/run.js AccessControlHarness:AccessControl
const proc = require('child_process');
const { PassThrough } = require('stream');
const events = require('events');
const argv = require('yargs')
.env('')
.options({
all: {
alias: 'a',
type: 'boolean',
},
spec: {
alias: 's',
type: 'string',
default: __dirname + '/specs.js',
},
parallel: {
alias: 'p',
type: 'number',
default: 4,
},
options: {
alias: 'o',
type: 'array',
default: [],
},
}).argv;
function match(entry, request) {
const [reqSpec, reqContract] = request.split(':').reverse();
return entry.spec == reqSpec && (!reqContract || entry.contract == reqContract);
}
const specs = require(argv.spec).filter(s => argv.all || argv._.some(r => match(s, r)));
const limit = require('p-limit')(argv.parallel);
if (argv._.length == 0 && !argv.all) {
console.error(`Warning: No specs requested. Did you forgot to toggle '--all'?`);
}
for (const r of argv._) {
if (!specs.some(s => match(s, r))) {
console.error(`Error: Requested spec '${r}' not found in ${argv.spec}`);
process.exitCode = 1;
}
}
if (process.exitCode) {
process.exit(process.exitCode);
}
for (const { spec, contract, files, options = [] } of specs) {
limit(runCertora, spec, contract, files, [...options, ...argv.options]);
}
// 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.flatMap(opt => opt.split(' ')),
];
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 job id, print the output link
stream.on('data', function logStatusUrl(data) {
const { '-DjobId': jobId, '-DuserId': userId } = Object.fromEntries(
data
.toString('utf8')
.match(/-D\S+=\S+/g)
?.map(s => s.split('=')) || [],
);
if (jobId && userId) {
console.error(`[${spec}] https://prover.certora.com/output/${userId}/${jobId}/`);
stream.off('data', logStatusUrl);
}
});
// 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:\/\/prover.certora.com\/output\/\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:',
url ? `[link](${url?.replace('/output/', '/jobStatus/')})` : 'error',
url ? `[link](${url})` : 'error',
),
);
}