Skip to content
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: split backend_barretenburg into prover and verifier classes #4769

Merged
merged 6 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion tooling/noir_js/test/node/e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect } from 'chai';
import assert_lt_json from '../noir_compiled_examples/assert_lt/target/assert_lt.json' assert { type: 'json' };
import { Noir } from '@noir-lang/noir_js';
import { BarretenbergBackend as Backend } from '@noir-lang/backend_barretenberg';
import { BarretenbergBackend as Backend, BarretenbergVerifier as Verifier } from '@noir-lang/backend_barretenberg';
import { CompiledCircuit } from '@noir-lang/types';

const assert_lt_program = assert_lt_json as CompiledCircuit;
Expand Down Expand Up @@ -47,6 +47,28 @@ it('end-to-end proof creation and verification (outer) -- Program API', async ()
expect(isValid).to.be.true;
});

it('end-to-end proof creation and verification (outer) -- Verifier API', async () => {
// Noir.Js part
const inputs = {
x: '2',
y: '3',
};

// Initialize backend
const backend = new Backend(assert_lt_program);
// Initialize program
const program = new Noir(assert_lt_program, backend);
// Generate proof
const proof = await program.generateProof(inputs);

const verificationKey = await backend.getVerificationKey();

// Proof verification
const verifier = new Verifier();
const isValid = await verifier.verifyProof(proof, verificationKey);
expect(isValid).to.be.true;
});

// TODO: maybe switch to using assert_statement_recursive here to test both options
it('end-to-end proof creation and verification (inner)', async () => {
// Noir.Js part
Expand Down
144 changes: 144 additions & 0 deletions tooling/noir_js_backend_barretenberg/src/backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { decompressSync as gunzip } from 'fflate';
import { acirToUint8Array } from './serialize.js';
import { Backend, CompiledCircuit, ProofData, VerifierBackend } from '@noir-lang/types';
import { BackendOptions } from './types.js';
import { deflattenPublicInputs } from './public_inputs.js';
import { reconstructProofWithPublicInputs } from './verifier.js';
import { type Barretenberg } from '@aztec/bb.js';

// This is the number of bytes in a UltraPlonk proof
// minus the public inputs.
const numBytesInProofWithoutPublicInputs: number = 2144;

export class BarretenbergVerifierBackend implements VerifierBackend {
// These type assertions are used so that we don't
// have to initialize `api` and `acirComposer` in the constructor.
// These are initialized asynchronously in the `init` function,
// constructors cannot be asynchronous which is why we do this.

protected api!: Barretenberg;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected acirComposer: any;
protected acirUncompressedBytecode: Uint8Array;

constructor(
acirCircuit: CompiledCircuit,
protected options: BackendOptions = { threads: 1 },
) {
const acirBytecodeBase64 = acirCircuit.bytecode;
this.acirUncompressedBytecode = acirToUint8Array(acirBytecodeBase64);
}

/** @ignore */
async instantiate(): Promise<void> {
if (!this.api) {
if (typeof navigator !== 'undefined' && navigator.hardwareConcurrency) {
this.options.threads = navigator.hardwareConcurrency;
} else {
try {
const os = await import('os');
this.options.threads = os.cpus().length;
} catch (e) {
console.log('Could not detect environment. Falling back to one thread.', e);
}
}
const { Barretenberg, RawBuffer, Crs } = await import('@aztec/bb.js');
const api = await Barretenberg.new(this.options);

const [_exact, _total, subgroupSize] = await api.acirGetCircuitSizes(this.acirUncompressedBytecode);
const crs = await Crs.new(subgroupSize + 1);
await api.commonInitSlabAllocator(subgroupSize);
await api.srsInitSrs(new RawBuffer(crs.getG1Data()), crs.numPoints, new RawBuffer(crs.getG2Data()));

this.acirComposer = await api.acirNewAcirComposer(subgroupSize);
await api.acirInitProvingKey(this.acirComposer, this.acirUncompressedBytecode);
this.api = api;
}
}

/** @description Verifies a proof */
async verifyProof(proofData: ProofData): Promise<boolean> {
const proof = reconstructProofWithPublicInputs(proofData);
await this.instantiate();
await this.api.acirInitVerificationKey(this.acirComposer);
return await this.api.acirVerifyProof(this.acirComposer, proof);
}

async getVerificationKey(): Promise<Uint8Array> {
await this.instantiate();
await this.api.acirInitVerificationKey(this.acirComposer);
return await this.api.acirGetVerificationKey(this.acirComposer);
}

async destroy(): Promise<void> {
if (!this.api) {
return;
}
await this.api.destroy();
}
}

export class BarretenbergBackend extends BarretenbergVerifierBackend implements Backend {
/** @description Generates a proof */
async generateProof(compressedWitness: Uint8Array): Promise<ProofData> {
await this.instantiate();
// TODO: Change once `@aztec/bb.js` version is updated to use methods without isRecursive flag
TomAFrench marked this conversation as resolved.
Show resolved Hide resolved
const proofWithPublicInputs = await this.api.acirCreateProof(
this.acirComposer,
this.acirUncompressedBytecode,
gunzip(compressedWitness),
);

const splitIndex = proofWithPublicInputs.length - numBytesInProofWithoutPublicInputs;

const publicInputsConcatenated = proofWithPublicInputs.slice(0, splitIndex);
const proof = proofWithPublicInputs.slice(splitIndex);
const publicInputs = deflattenPublicInputs(publicInputsConcatenated);

return { proof, publicInputs };
}

/**
* Generates artifacts that will be passed to a circuit that will verify this proof.
*
* Instead of passing the proof and verification key as a byte array, we pass them
* as fields which makes it cheaper to verify in a circuit.
*
* The proof that is passed here will have been created using a circuit
* that has the #[recursive] attribute on its `main` method.
*
* The number of public inputs denotes how many public inputs are in the inner proof.
*
* @example
* ```typescript
* const artifacts = await backend.generateRecursiveProofArtifacts(proof, numOfPublicInputs);
* ```
*/
async generateRecursiveProofArtifacts(
proofData: ProofData,
numOfPublicInputs = 0,
): Promise<{
proofAsFields: string[];
vkAsFields: string[];
vkHash: string;
}> {
await this.instantiate();
const proof = reconstructProofWithPublicInputs(proofData);
const proofAsFields = (
await this.api.acirSerializeProofIntoFields(this.acirComposer, proof, numOfPublicInputs)
).slice(numOfPublicInputs);

// TODO: perhaps we should put this in the init function. Need to benchmark
// TODO how long it takes.
await this.api.acirInitVerificationKey(this.acirComposer);

// Note: If you don't init verification key, `acirSerializeVerificationKeyIntoFields`` will just hang on serialization
const vk = await this.api.acirSerializeVerificationKeyIntoFields(this.acirComposer);

return {
proofAsFields: proofAsFields.map((p) => p.toString()),
vkAsFields: vk[0].map((vk) => vk.toString()),
vkHash: vk[1].toString(),
};
}
}
151 changes: 4 additions & 147 deletions tooling/noir_js_backend_barretenberg/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,150 +1,7 @@
import { decompressSync as gunzip } from 'fflate';
import { acirToUint8Array } from './serialize.js';
import { Backend, CompiledCircuit, ProofData } from '@noir-lang/types';
import { BackendOptions } from './types.js';
import { deflattenPublicInputs, flattenPublicInputsAsArray } from './public_inputs.js';
import { type Barretenberg } from '@aztec/bb.js';

export { BarretenbergBackend } from './backend.js';
export { BarretenbergVerifier } from './verifier.js';
export { publicInputsToWitnessMap } from './public_inputs.js';

// This is the number of bytes in a UltraPlonk proof
// minus the public inputs.
const numBytesInProofWithoutPublicInputs: number = 2144;

export class BarretenbergBackend implements Backend {
// These type assertions are used so that we don't
// have to initialize `api` and `acirComposer` in the constructor.
// These are initialized asynchronously in the `init` function,
// constructors cannot be asynchronous which is why we do this.

private api!: Barretenberg;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private acirComposer: any;
private acirUncompressedBytecode: Uint8Array;

constructor(
acirCircuit: CompiledCircuit,
private options: BackendOptions = { threads: 1 },
) {
const acirBytecodeBase64 = acirCircuit.bytecode;
this.acirUncompressedBytecode = acirToUint8Array(acirBytecodeBase64);
}

/** @ignore */
async instantiate(): Promise<void> {
if (!this.api) {
if (typeof navigator !== 'undefined' && navigator.hardwareConcurrency) {
this.options.threads = navigator.hardwareConcurrency;
} else {
try {
const os = await import('os');
this.options.threads = os.cpus().length;
} catch (e) {
console.log('Could not detect environment. Falling back to one thread.', e);
}
}
const { Barretenberg, RawBuffer, Crs } = await import('@aztec/bb.js');
const api = await Barretenberg.new(this.options);
const [_exact, _total, subgroupSize] = await api.acirGetCircuitSizes(this.acirUncompressedBytecode);
const crs = await Crs.new(subgroupSize + 1);
await api.commonInitSlabAllocator(subgroupSize);
await api.srsInitSrs(new RawBuffer(crs.getG1Data()), crs.numPoints, new RawBuffer(crs.getG2Data()));

this.acirComposer = await api.acirNewAcirComposer(subgroupSize);
await api.acirInitProvingKey(this.acirComposer, this.acirUncompressedBytecode);
this.api = api;
}
}

/** @description Generates a proof */
async generateProof(compressedWitness: Uint8Array): Promise<ProofData> {
await this.instantiate();
// TODO: Change once `@aztec/bb.js` version is updated to use methods without isRecursive flag
const proofWithPublicInputs = await this.api.acirCreateProof(
this.acirComposer,
this.acirUncompressedBytecode,
gunzip(compressedWitness),
);

const splitIndex = proofWithPublicInputs.length - numBytesInProofWithoutPublicInputs;

const publicInputsConcatenated = proofWithPublicInputs.slice(0, splitIndex);
const proof = proofWithPublicInputs.slice(splitIndex);
const publicInputs = deflattenPublicInputs(publicInputsConcatenated);

return { proof, publicInputs };
}

/**
* Generates artifacts that will be passed to a circuit that will verify this proof.
*
* Instead of passing the proof and verification key as a byte array, we pass them
* as fields which makes it cheaper to verify in a circuit.
*
* The proof that is passed here will have been created using a circuit
* that has the #[recursive] attribute on its `main` method.
*
* The number of public inputs denotes how many public inputs are in the inner proof.
*
* @example
* ```typescript
* const artifacts = await backend.generateRecursiveProofArtifacts(proof, numOfPublicInputs);
* ```
*/
async generateRecursiveProofArtifacts(
proofData: ProofData,
numOfPublicInputs = 0,
): Promise<{
proofAsFields: string[];
vkAsFields: string[];
vkHash: string;
}> {
await this.instantiate();
const proof = reconstructProofWithPublicInputs(proofData);
const proofAsFields = (
await this.api.acirSerializeProofIntoFields(this.acirComposer, proof, numOfPublicInputs)
).slice(numOfPublicInputs);

// TODO: perhaps we should put this in the init function. Need to benchmark
// TODO how long it takes.
await this.api.acirInitVerificationKey(this.acirComposer);

// Note: If you don't init verification key, `acirSerializeVerificationKeyIntoFields`` will just hang on serialization
const vk = await this.api.acirSerializeVerificationKeyIntoFields(this.acirComposer);

return {
proofAsFields: proofAsFields.map((p) => p.toString()),
vkAsFields: vk[0].map((vk) => vk.toString()),
vkHash: vk[1].toString(),
};
}

/** @description Verifies a proof */
async verifyProof(proofData: ProofData): Promise<boolean> {
const proof = reconstructProofWithPublicInputs(proofData);
await this.instantiate();
await this.api.acirInitVerificationKey(this.acirComposer);
// TODO: Change once `@aztec/bb.js` version is updated to use methods without isRecursive flag
return await this.api.acirVerifyProof(this.acirComposer, proof);
}

async destroy(): Promise<void> {
if (!this.api) {
return;
}
await this.api.destroy();
}
}

function reconstructProofWithPublicInputs(proofData: ProofData): Uint8Array {
// Flatten publicInputs
const publicInputsConcatenated = flattenPublicInputsAsArray(proofData.publicInputs);

// Concatenate publicInputs and proof
const proofWithPublicInputs = Uint8Array.from([...publicInputsConcatenated, ...proofData.proof]);

return proofWithPublicInputs;
}

// typedoc exports
export { Backend, BackendOptions, CompiledCircuit, ProofData };
export { Backend, CompiledCircuit, ProofData } from '@noir-lang/types';
export { BackendOptions } from './types.js';
Loading
Loading