-
Notifications
You must be signed in to change notification settings - Fork 283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: run solidity tests for all acir artifacts #3161
Changes from all commits
36de5b9
f1de40c
267cea2
2a452fa
de859ee
b2ab76b
66aa9c1
ea049a0
a36de68
0e669ec
6666f35
681c6c7
299f2be
442490a
e0cc577
6fb8d98
3f82eb3
f247a12
e271243
ce051b8
e9a87d4
c817ed3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
FROM 278380418400.dkr.ecr.eu-west-2.amazonaws.com/barretenberg-x86_64-linux-clang-assert | ||
FROM 278380418400.dkr.ecr.eu-west-2.amazonaws.com/barretenberg-x86_64-linux-clang-sol | ||
|
||
FROM node:18-alpine | ||
RUN apk update && apk add git bash curl jq | ||
COPY --from=0 /usr/src/barretenberg/cpp/build /usr/src/barretenberg/cpp/build | ||
COPY --from=1 /usr/src/barretenberg/sol/src/ultra/BaseUltraVerifier.sol /usr/src/barretenberg/sol/src/ultra/BaseUltraVerifier.sol | ||
COPY --from=ghcr.io/foundry-rs/foundry:latest /usr/local/bin/anvil /usr/local/bin/anvil | ||
WORKDIR /usr/src/barretenberg/acir_tests | ||
COPY . . | ||
# Run every acir test through a solidity verifier". | ||
RUN (cd sol-test && yarn) | ||
RUN PARALLEL=1 FLOW=sol ./run_acir_tests.sh |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
#!/bin/bash | ||
|
||
# Handler for SIGCHLD, cleanup if child exit with error | ||
handle_sigchild() { | ||
for pid in "${pids[@]}"; do | ||
# If process is no longer running | ||
if ! kill -0 "$pid" 2>/dev/null; then | ||
# Wait for the process and get exit status | ||
wait "$pid" | ||
status=$? | ||
|
||
# If exit status is error | ||
if [ $status -ne 0 ]; then | ||
# Create error file | ||
touch "$error_file" | ||
fi | ||
fi | ||
done | ||
} | ||
|
||
check_error_file() { | ||
# If error file exists, exit with error | ||
if [ -f "$error_file" ]; then | ||
rm "$error_file" | ||
echo "Error occurred in one or more child processes. Exiting..." | ||
exit 1 | ||
fi | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
#!/bin/sh | ||
set -eu | ||
|
||
export PROOF="$(pwd)/proof" | ||
export PROOF_AS_FIELDS="$(pwd)/proof_fields.json" | ||
|
||
# Create a proof, write the solidity contract, write the proof as fields in order to extract the public inputs | ||
$BIN prove -o proof | ||
$BIN write_vk -o vk | ||
$BIN proof_as_fields -k vk -c $CRS_PATH -p $PROOF | ||
$BIN contract -k vk -c $CRS_PATH -b ./target/acir.gz -o Key.sol | ||
|
||
# Export the paths to the environment variables for the js test runner | ||
export KEY_PATH="$(pwd)/Key.sol" | ||
export VERIFIER_PATH=$(realpath "../../sol-test/Verifier.sol") | ||
export TEST_PATH=$(realpath "../../sol-test/Test.sol") | ||
export BASE_PATH=$(realpath "../../../sol/src/ultra/BaseUltraVerifier.sol") | ||
|
||
# Use solcjs to compile the generated key contract with the template verifier and test contract | ||
# index.js will start an anvil, on a random port | ||
# Deploy the verifier then send a test transaction | ||
export TEST_NAME=$(basename $(pwd)) | ||
node ../../sol-test/src/index.js |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// THIS FILE WILL NOT COMPILE BY ITSELF | ||
// Compilation is handled in `src/index.js` where solcjs gathers the dependencies | ||
|
||
pragma solidity >=0.8.4; | ||
|
||
import {Verifier} from "./Verifier.sol"; | ||
|
||
contract Test { | ||
Verifier verifier; | ||
|
||
constructor() { | ||
verifier = new Verifier(); | ||
} | ||
|
||
function test(bytes calldata proof, bytes32[] calldata publicInputs) view public returns(bool) { | ||
return verifier.verify(proof, publicInputs); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// THIS FILE WILL NOT COMPILE BY ITSELF | ||
// Compilation is handled in `src/index.js` where solcjs gathers the dependencies | ||
|
||
// SPDX-License-Identifier: Apache-2.0 | ||
// Copyright 2022 Aztec | ||
pragma solidity >=0.8.4; | ||
|
||
import {UltraVerificationKey} from "./Key.sol"; | ||
import {BaseUltraVerifier} from "./BaseUltraVerifier.sol"; | ||
|
||
contract Verifier is BaseUltraVerifier { | ||
function getVerificationKeyHash() public pure override(BaseUltraVerifier) returns (bytes32) { | ||
return UltraVerificationKey.verificationKeyHash(); | ||
} | ||
|
||
function loadVerificationKey(uint256 vk, uint256 _omegaInverseLoc) internal pure virtual override(BaseUltraVerifier) { | ||
UltraVerificationKey.loadVerificationKey(vk, _omegaInverseLoc); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{ | ||
"name": "headless-test", | ||
"version": "1.0.0", | ||
"main": "index.js", | ||
"license": "MIT", | ||
"type": "module", | ||
"scripts": { | ||
"start": "node ./src/index.js" | ||
}, | ||
"dependencies": { | ||
"ethers": "^6.8.1", | ||
"solc": "^0.8.22" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
import fs from "fs"; | ||
const {readFileSync, promises: fsPromises} = fs; | ||
import {spawn} from "child_process"; | ||
import {ethers} from "ethers"; | ||
import solc from "solc"; | ||
|
||
const NUMBER_OF_FIELDS_IN_PROOF = 93; | ||
|
||
// We use the solcjs compiler version in this test, although it is slower than foundry, to run the test end to end | ||
// it simplifies of parallelising the test suite | ||
|
||
// What does this file do? | ||
// | ||
// 1. Launch an instance of anvil { on a random port, for parallelism } | ||
// 2. Compile the solidity files using solcjs | ||
// 3. Deploy the contract | ||
// 4. Read the previously created proof, and append public inputs | ||
// 5. Run the test against the deployed contract | ||
// 6. Kill the anvil instance | ||
|
||
const getEnvVar = (envvar) => { | ||
const varVal = process.env[envvar]; | ||
if (!varVal) { | ||
throw new Error(`Missing environment variable ${envvar}`); | ||
} | ||
return varVal; | ||
} | ||
|
||
// Test name is passed into environment from `flows/sol.sh` | ||
const testName = getEnvVar("TEST_NAME"); | ||
|
||
// Get solidity files, passed into environment from `flows/sol.sh` | ||
const keyPath = getEnvVar("KEY_PATH"); | ||
const verifierPath = getEnvVar("VERIFIER_PATH"); | ||
const testPath = getEnvVar("TEST_PATH"); | ||
const basePath = getEnvVar("BASE_PATH"); | ||
const encoding = {encoding: "utf8"}; | ||
const [key, test, verifier, base] = await Promise.all( | ||
[ | ||
fsPromises.readFile(keyPath, encoding), | ||
fsPromises.readFile(testPath, encoding), | ||
fsPromises.readFile(verifierPath, encoding), | ||
fsPromises.readFile(basePath, encoding) | ||
]); | ||
|
||
var input = { | ||
language: 'Solidity', | ||
sources: { | ||
'Key.sol': { | ||
content: key | ||
}, | ||
'Test.sol': { | ||
content: test | ||
}, | ||
'Verifier.sol': { | ||
content: verifier | ||
}, | ||
'BaseUltraVerifier.sol': { | ||
content: base | ||
} | ||
}, | ||
settings: { // we require the optimiser | ||
optimizer: { | ||
enabled: true, | ||
runs: 200 | ||
}, | ||
outputSelection: { | ||
'*': { | ||
'*': ['evm.bytecode.object', 'abi'] | ||
} | ||
} | ||
} | ||
}; | ||
|
||
var output = JSON.parse(solc.compile(JSON.stringify(input))); | ||
const contract = output.contracts['Test.sol']['Test']; | ||
const bytecode = contract.evm.bytecode.object; | ||
const abi = contract.abi; | ||
|
||
/** | ||
* Launch anvil on the given port, | ||
* Resolves when ready, rejects when port is already allocated | ||
* @param {Number} port | ||
*/ | ||
const launchAnvil = async (port) => { | ||
const handle = spawn("anvil", ["-p", port]); | ||
|
||
// wait until the anvil instance is ready on port | ||
await new Promise((resolve, reject) => { | ||
// If we get an error reject, which will cause the caller to retry on a new port | ||
handle.stderr.on("data", (data) => { | ||
const str = data.toString(); | ||
if (str.includes("error binding")) { | ||
reject("we go again baby") | ||
} | ||
}); | ||
|
||
// If we get a success resolve, anvil is ready | ||
handle.stdout.on("data", (data) => { | ||
const str = data.toString(); | ||
if (str.includes("Listening on")) { | ||
resolve(undefined); | ||
} | ||
}); | ||
}); | ||
|
||
return handle; | ||
} | ||
|
||
/** | ||
* Deploys the contract | ||
* @param {ethers.Signer} signer | ||
*/ | ||
const deploy = async (signer) => { | ||
const factory = new ethers.ContractFactory(abi, bytecode, signer); | ||
const deployment = await factory.deploy(); | ||
const deployed = await deployment.waitForDeployment(); | ||
return await deployed.getAddress(); | ||
} | ||
|
||
/** | ||
* Takes in a proof as fields, and returns the public inputs, as well as the number of public inputs | ||
* @param {Array<String>} proofAsFields | ||
* @return {Array} [number, Array<String>] | ||
*/ | ||
const readPublicInputs = (proofAsFields) => { | ||
const publicInputs = []; | ||
// A proof with no public inputs is 93 fields long | ||
const numPublicInputs = proofAsFields.length - NUMBER_OF_FIELDS_IN_PROOF; | ||
for (let i = 0; i < numPublicInputs; i++) { | ||
publicInputs.push(proofAsFields[i]); | ||
} | ||
return [numPublicInputs, publicInputs]; | ||
} | ||
|
||
/** | ||
* Get Anvil | ||
* | ||
* Creates an anvil instance on a random port, and returns the instance and the port | ||
* If the port is alredy allocated, it will try again | ||
* @returns {[ChildProcess, Number]} [anvil, port] | ||
*/ | ||
const getAnvil = async () => { | ||
const port = Math.floor(Math.random() * 10000) + 10000; | ||
try { | ||
return [await launchAnvil(port), port]; | ||
} catch (e) { | ||
// Recursive call should try again on a new port in the rare case the port is already taken | ||
// yes this looks dangerous, but it relies on 0-10000 being hard to collide on | ||
return getAnvil(); | ||
} | ||
} | ||
|
||
const [anvil, randomPort] = await getAnvil(); | ||
const killAnvil = () => { | ||
anvil.kill(); | ||
console.log(testName, " complete") | ||
} | ||
|
||
try { | ||
const proofAsFieldsPath = getEnvVar("PROOF_AS_FIELDS"); | ||
const proofAsFields = readFileSync(proofAsFieldsPath); | ||
const [numPublicInputs, publicInputs] = readPublicInputs(JSON.parse(proofAsFields.toString())); | ||
|
||
const proofPath = getEnvVar("PROOF"); | ||
const proof = readFileSync(proofPath); | ||
|
||
// Cut the number of public inputs off of the proof string | ||
const proofStr = `0x${proof.toString("hex").substring(64*numPublicInputs)}`; | ||
|
||
const key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; | ||
const provider = new ethers.JsonRpcProvider(`http://localhost:${randomPort}`); | ||
const signer = new ethers.Wallet(key, provider); | ||
|
||
// deploy | ||
const address = await deploy(signer); | ||
const contract = new ethers.Contract(address, abi, signer); | ||
|
||
const result = await contract.test(proofStr, publicInputs); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is the test contract actually needed? Any issues calling verify directly? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Calling verify directly should do the job, I will probably not run this in CI since we are running all commands, but it is useful to have this in the future. |
||
if (!result) throw new Error("Test failed"); | ||
} | ||
catch (e) { | ||
console.error(testName, " failed") | ||
console.log(e) | ||
throw e; | ||
} | ||
finally { | ||
// Kill anvil at the end of running | ||
killAnvil(); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like. Skibbidy.