Skip to content

Commit d09f667

Browse files
authored
feat: run solidity tests for all acir artifacts (AztecProtocol#3161)
fixes: AztecProtocol#3048 This pr: - Adds secp256k1 circuit and solidity verifier to the tests - Runs a solidity verifier test on ALL acir artifacts - Generate a proof - Generate a vk - Generate sol verification key - We create anvil on a random port - Compile the contract using solcjs to orchestrate compilation - Deploy the contract - Test the contract with the created prood This pr builds on AztecProtocol#3215 which fixes verifier issues ( in a blanket manner ) not directly addressing the dummy constraints issue that appears to be causing it.
1 parent f3d8aae commit d09f667

32 files changed

+789
-122
lines changed

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@
1212
url = https://github.com/Arachnid/solidity-stringutils
1313
[submodule "barretenberg/sol/lib/openzeppelin-contracts"]
1414
path = barretenberg/sol/lib/openzeppelin-contracts
15-
url = https://github.com/OpenZeppelin/openzeppelin-contracts
15+
url = https://github.com/OpenZeppelin/openzeppelin-contracts

barretenberg/acir_tests/Dockerfile.bb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ COPY . .
88
# Run every acir test through native bb build "prove_and_verify".
99
RUN FLOW=all_cmds ./run_acir_tests.sh
1010
# Run 1_mul through native bb build, all_cmds flow, to test all cli args.
11-
RUN VERBOSE=1 FLOW=all_cmds ./run_acir_tests.sh 1_mul
11+
RUN VERBOSE=1 FLOW=all_cmds ./run_acir_tests.sh 1_mul
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM 278380418400.dkr.ecr.eu-west-2.amazonaws.com/barretenberg-x86_64-linux-clang-assert
2+
FROM 278380418400.dkr.ecr.eu-west-2.amazonaws.com/barretenberg-x86_64-linux-clang-sol
3+
4+
FROM node:18-alpine
5+
RUN apk update && apk add git bash curl jq
6+
COPY --from=0 /usr/src/barretenberg/cpp/build /usr/src/barretenberg/cpp/build
7+
COPY --from=1 /usr/src/barretenberg/sol/src/ultra/BaseUltraVerifier.sol /usr/src/barretenberg/sol/src/ultra/BaseUltraVerifier.sol
8+
COPY --from=ghcr.io/foundry-rs/foundry:latest /usr/local/bin/anvil /usr/local/bin/anvil
9+
WORKDIR /usr/src/barretenberg/acir_tests
10+
COPY . .
11+
# Run every acir test through a solidity verifier".
12+
RUN (cd sol-test && yarn)
13+
RUN PARALLEL=1 FLOW=sol ./run_acir_tests.sh
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/bin/bash
2+
3+
# Handler for SIGCHLD, cleanup if child exit with error
4+
handle_sigchild() {
5+
for pid in "${pids[@]}"; do
6+
# If process is no longer running
7+
if ! kill -0 "$pid" 2>/dev/null; then
8+
# Wait for the process and get exit status
9+
wait "$pid"
10+
status=$?
11+
12+
# If exit status is error
13+
if [ $status -ne 0 ]; then
14+
# Create error file
15+
touch "$error_file"
16+
fi
17+
fi
18+
done
19+
}
20+
21+
check_error_file() {
22+
# If error file exists, exit with error
23+
if [ -f "$error_file" ]; then
24+
rm "$error_file"
25+
echo "Error occurred in one or more child processes. Exiting..."
26+
exit 1
27+
fi
28+
}

barretenberg/acir_tests/flows/sol.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
export PROOF="$(pwd)/proof"
5+
export PROOF_AS_FIELDS="$(pwd)/proof_fields.json"
6+
7+
# Create a proof, write the solidity contract, write the proof as fields in order to extract the public inputs
8+
$BIN prove -o proof
9+
$BIN write_vk -o vk
10+
$BIN proof_as_fields -k vk -c $CRS_PATH -p $PROOF
11+
$BIN contract -k vk -c $CRS_PATH -b ./target/acir.gz -o Key.sol
12+
13+
# Export the paths to the environment variables for the js test runner
14+
export KEY_PATH="$(pwd)/Key.sol"
15+
export VERIFIER_PATH=$(realpath "../../sol-test/Verifier.sol")
16+
export TEST_PATH=$(realpath "../../sol-test/Test.sol")
17+
export BASE_PATH=$(realpath "../../../sol/src/ultra/BaseUltraVerifier.sol")
18+
19+
# Use solcjs to compile the generated key contract with the template verifier and test contract
20+
# index.js will start an anvil, on a random port
21+
# Deploy the verifier then send a test transaction
22+
export TEST_NAME=$(basename $(pwd))
23+
node ../../sol-test/src/index.js

barretenberg/acir_tests/run_acir_tests.sh

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
# VERBOSE: to enable logging for each test.
55
set -eu
66

7+
# Catch when running in parallel
8+
error_file="/tmp/error.$$"
9+
pids=()
10+
source ./bash_helpers/catch.sh
11+
trap handle_sigchild SIGCHLD
12+
713
BIN=${BIN:-../cpp/build/bin/bb}
814
FLOW=${FLOW:-prove_and_verify}
915
CRS_PATH=~/.bb-crs
@@ -43,6 +49,7 @@ function test() {
4349
echo -e "\033[32mPASSED\033[0m ($duration ms)"
4450
else
4551
echo -e "\033[31mFAILED\033[0m"
52+
touch "$error_file"
4653
exit 1
4754
fi
4855

@@ -68,6 +75,16 @@ else
6875
continue
6976
fi
7077

71-
test $TEST_NAME
78+
# If parallel flag is set, run in parallel
79+
if [ -n "${PARALLEL:-}" ]; then
80+
test $TEST_NAME &
81+
else
82+
test $TEST_NAME
83+
fi
7284
done
7385
fi
86+
87+
wait
88+
89+
# Check for parallel errors
90+
check_error_file
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// THIS FILE WILL NOT COMPILE BY ITSELF
2+
// Compilation is handled in `src/index.js` where solcjs gathers the dependencies
3+
4+
pragma solidity >=0.8.4;
5+
6+
import {Verifier} from "./Verifier.sol";
7+
8+
contract Test {
9+
Verifier verifier;
10+
11+
constructor() {
12+
verifier = new Verifier();
13+
}
14+
15+
function test(bytes calldata proof, bytes32[] calldata publicInputs) view public returns(bool) {
16+
return verifier.verify(proof, publicInputs);
17+
}
18+
}
19+
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// THIS FILE WILL NOT COMPILE BY ITSELF
2+
// Compilation is handled in `src/index.js` where solcjs gathers the dependencies
3+
4+
// SPDX-License-Identifier: Apache-2.0
5+
// Copyright 2022 Aztec
6+
pragma solidity >=0.8.4;
7+
8+
import {UltraVerificationKey} from "./Key.sol";
9+
import {BaseUltraVerifier} from "./BaseUltraVerifier.sol";
10+
11+
contract Verifier is BaseUltraVerifier {
12+
function getVerificationKeyHash() public pure override(BaseUltraVerifier) returns (bytes32) {
13+
return UltraVerificationKey.verificationKeyHash();
14+
}
15+
16+
function loadVerificationKey(uint256 vk, uint256 _omegaInverseLoc) internal pure virtual override(BaseUltraVerifier) {
17+
UltraVerificationKey.loadVerificationKey(vk, _omegaInverseLoc);
18+
}
19+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "headless-test",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"type": "module",
7+
"scripts": {
8+
"start": "node ./src/index.js"
9+
},
10+
"dependencies": {
11+
"ethers": "^6.8.1",
12+
"solc": "^0.8.22"
13+
}
14+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import fs from "fs";
2+
const {readFileSync, promises: fsPromises} = fs;
3+
import {spawn} from "child_process";
4+
import {ethers} from "ethers";
5+
import solc from "solc";
6+
7+
const NUMBER_OF_FIELDS_IN_PROOF = 93;
8+
9+
// We use the solcjs compiler version in this test, although it is slower than foundry, to run the test end to end
10+
// it simplifies of parallelising the test suite
11+
12+
// What does this file do?
13+
//
14+
// 1. Launch an instance of anvil { on a random port, for parallelism }
15+
// 2. Compile the solidity files using solcjs
16+
// 3. Deploy the contract
17+
// 4. Read the previously created proof, and append public inputs
18+
// 5. Run the test against the deployed contract
19+
// 6. Kill the anvil instance
20+
21+
const getEnvVar = (envvar) => {
22+
const varVal = process.env[envvar];
23+
if (!varVal) {
24+
throw new Error(`Missing environment variable ${envvar}`);
25+
}
26+
return varVal;
27+
}
28+
29+
// Test name is passed into environment from `flows/sol.sh`
30+
const testName = getEnvVar("TEST_NAME");
31+
32+
// Get solidity files, passed into environment from `flows/sol.sh`
33+
const keyPath = getEnvVar("KEY_PATH");
34+
const verifierPath = getEnvVar("VERIFIER_PATH");
35+
const testPath = getEnvVar("TEST_PATH");
36+
const basePath = getEnvVar("BASE_PATH");
37+
const encoding = {encoding: "utf8"};
38+
const [key, test, verifier, base] = await Promise.all(
39+
[
40+
fsPromises.readFile(keyPath, encoding),
41+
fsPromises.readFile(testPath, encoding),
42+
fsPromises.readFile(verifierPath, encoding),
43+
fsPromises.readFile(basePath, encoding)
44+
]);
45+
46+
var input = {
47+
language: 'Solidity',
48+
sources: {
49+
'Key.sol': {
50+
content: key
51+
},
52+
'Test.sol': {
53+
content: test
54+
},
55+
'Verifier.sol': {
56+
content: verifier
57+
},
58+
'BaseUltraVerifier.sol': {
59+
content: base
60+
}
61+
},
62+
settings: { // we require the optimiser
63+
optimizer: {
64+
enabled: true,
65+
runs: 200
66+
},
67+
outputSelection: {
68+
'*': {
69+
'*': ['evm.bytecode.object', 'abi']
70+
}
71+
}
72+
}
73+
};
74+
75+
var output = JSON.parse(solc.compile(JSON.stringify(input)));
76+
const contract = output.contracts['Test.sol']['Test'];
77+
const bytecode = contract.evm.bytecode.object;
78+
const abi = contract.abi;
79+
80+
/**
81+
* Launch anvil on the given port,
82+
* Resolves when ready, rejects when port is already allocated
83+
* @param {Number} port
84+
*/
85+
const launchAnvil = async (port) => {
86+
const handle = spawn("anvil", ["-p", port]);
87+
88+
// wait until the anvil instance is ready on port
89+
await new Promise((resolve, reject) => {
90+
// If we get an error reject, which will cause the caller to retry on a new port
91+
handle.stderr.on("data", (data) => {
92+
const str = data.toString();
93+
if (str.includes("error binding")) {
94+
reject("we go again baby")
95+
}
96+
});
97+
98+
// If we get a success resolve, anvil is ready
99+
handle.stdout.on("data", (data) => {
100+
const str = data.toString();
101+
if (str.includes("Listening on")) {
102+
resolve(undefined);
103+
}
104+
});
105+
});
106+
107+
return handle;
108+
}
109+
110+
/**
111+
* Deploys the contract
112+
* @param {ethers.Signer} signer
113+
*/
114+
const deploy = async (signer) => {
115+
const factory = new ethers.ContractFactory(abi, bytecode, signer);
116+
const deployment = await factory.deploy();
117+
const deployed = await deployment.waitForDeployment();
118+
return await deployed.getAddress();
119+
}
120+
121+
/**
122+
* Takes in a proof as fields, and returns the public inputs, as well as the number of public inputs
123+
* @param {Array<String>} proofAsFields
124+
* @return {Array} [number, Array<String>]
125+
*/
126+
const readPublicInputs = (proofAsFields) => {
127+
const publicInputs = [];
128+
// A proof with no public inputs is 93 fields long
129+
const numPublicInputs = proofAsFields.length - NUMBER_OF_FIELDS_IN_PROOF;
130+
for (let i = 0; i < numPublicInputs; i++) {
131+
publicInputs.push(proofAsFields[i]);
132+
}
133+
return [numPublicInputs, publicInputs];
134+
}
135+
136+
/**
137+
* Get Anvil
138+
*
139+
* Creates an anvil instance on a random port, and returns the instance and the port
140+
* If the port is alredy allocated, it will try again
141+
* @returns {[ChildProcess, Number]} [anvil, port]
142+
*/
143+
const getAnvil = async () => {
144+
const port = Math.floor(Math.random() * 10000) + 10000;
145+
try {
146+
return [await launchAnvil(port), port];
147+
} catch (e) {
148+
// Recursive call should try again on a new port in the rare case the port is already taken
149+
// yes this looks dangerous, but it relies on 0-10000 being hard to collide on
150+
return getAnvil();
151+
}
152+
}
153+
154+
const [anvil, randomPort] = await getAnvil();
155+
const killAnvil = () => {
156+
anvil.kill();
157+
console.log(testName, " complete")
158+
}
159+
160+
try {
161+
const proofAsFieldsPath = getEnvVar("PROOF_AS_FIELDS");
162+
const proofAsFields = readFileSync(proofAsFieldsPath);
163+
const [numPublicInputs, publicInputs] = readPublicInputs(JSON.parse(proofAsFields.toString()));
164+
165+
const proofPath = getEnvVar("PROOF");
166+
const proof = readFileSync(proofPath);
167+
168+
// Cut the number of public inputs off of the proof string
169+
const proofStr = `0x${proof.toString("hex").substring(64*numPublicInputs)}`;
170+
171+
const key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
172+
const provider = new ethers.JsonRpcProvider(`http://localhost:${randomPort}`);
173+
const signer = new ethers.Wallet(key, provider);
174+
175+
// deploy
176+
const address = await deploy(signer);
177+
const contract = new ethers.Contract(address, abi, signer);
178+
179+
const result = await contract.test(proofStr, publicInputs);
180+
if (!result) throw new Error("Test failed");
181+
}
182+
catch (e) {
183+
console.error(testName, " failed")
184+
console.log(e)
185+
throw e;
186+
}
187+
finally {
188+
// Kill anvil at the end of running
189+
killAnvil();
190+
}
191+

0 commit comments

Comments
 (0)