Skip to content

Commit

Permalink
feat: split backend_barretenburg into prover and verifier classes (#…
Browse files Browse the repository at this point in the history
…4769)

# Description

## Problem\*

Resolves #3450

## Summary\*

- In order to prove + verify, we have proving and verification keys
+ The proving key is derived from the circuit representation and the
verification key is derived from the proving key
+ The proving key is generally quite big, so we always generate it from
the circuit, which is easier than lugging it around
  + The verification key is much smaller and can be compressed
- Ideally, a user verifying a proof should be able to simply send the
verification key and proof to BB

## Additional Context



## Documentation\*

Check one:
- [x] No documentation needed.
- [ ] Documentation included in this PR.
- [ ] **[For Experimental Features]** Documentation to be submitted in a
separate PR.

# PR Checklist\*

- [x] I have tested the changes locally.
- [x] I have formatted the changes with [Prettier](https://prettier.io/)
and/or `cargo fmt` on default settings.

---------

Co-authored-by: TomAFrench <[email protected]>
Co-authored-by: Tom French <[email protected]>
  • Loading branch information
3 people authored Apr 12, 2024
1 parent 9fd91b7 commit ce1e662
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 157 deletions.
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
143 changes: 143 additions & 0 deletions tooling/noir_js_backend_barretenberg/src/backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
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();
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

0 comments on commit ce1e662

Please sign in to comment.