Skip to content

Commit

Permalink
chore: migrate higher-level APIs for barretenberg to bb.js (#8677)
Browse files Browse the repository at this point in the history
This PR moves a bunch of the code in `noir_js_backend_barretenberg` into
`bb.js` . I've kept this as separate classes for now but we can likely
migate more functionality over to the standard `Barretenberg` class
(especially with UP being deprecated as UH has less external state which
needs to be managed.)

`noir_js_backend_barretenberg` still exists as we still need to split
off public inputs, etc. to avoid this being a breaking change or
creating an inconsistency in `bb.js`. We can look at how to address this
in future.
  • Loading branch information
TomAFrench authored Sep 24, 2024
1 parent d5f16cc commit 0237a20
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 295 deletions.
29 changes: 8 additions & 21 deletions barretenberg/acir_tests/browser-test-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,21 @@ async function runTest(
witness: Uint8Array,
threads?: number
) {
const { Barretenberg, RawBuffer, Crs } = await import("@aztec/bb.js");
const CIRCUIT_SIZE = 2 ** 19;
const { UltraPlonkBackend, BarretenbergVerifier } = await import("@aztec/bb.js");

debug("starting test...");
const api = await Barretenberg.new({ threads });
const backend = new UltraPlonkBackend(bytecode, { threads });
const proof = await backend.generateProof(witness);

// Important to init slab allocator as first thing, to ensure maximum memory efficiency.
await api.commonInitSlabAllocator(CIRCUIT_SIZE);
const verificationKey = await backend.getVerificationKey();
await backend.destroy();

// Plus 1 needed!
const crs = await Crs.new(CIRCUIT_SIZE + 1);
await api.srsInitSrs(
new RawBuffer(crs.getG1Data()),
crs.numPoints,
new RawBuffer(crs.getG2Data())
);

const acirComposer = await api.acirNewAcirComposer(CIRCUIT_SIZE);
const proof = await api.acirCreateProof(
acirComposer,
bytecode,
witness,
);
debug(`verifying...`);
const verified = await api.acirVerifyProof(acirComposer, proof);
const verifier = new BarretenbergVerifier({ threads });
const verified = await verifier.verifyUltraplonkProof(proof, verificationKey);
debug(`verified: ${verified}`);

await api.destroy();
await verifier.destroy();

debug("test complete.");
return verified;
Expand Down
203 changes: 203 additions & 0 deletions barretenberg/ts/src/barretenberg/backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { BackendOptions, Barretenberg } from './index.js';
import { RawBuffer } from '../types/raw_buffer.js';

export class UltraPlonkBackend {
// 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;

constructor(protected acirUncompressedBytecode: Uint8Array, protected options: BackendOptions = { threads: 1 }) {}

/** @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 api = await Barretenberg.new(this.options);

const honkRecursion = false;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_exact, _total, subgroupSize] = await api.acirGetCircuitSizes(
this.acirUncompressedBytecode,
honkRecursion,
);

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

/** @description Generates a proof */
async generateProof(uncompressedWitness: Uint8Array): Promise<Uint8Array> {
await this.instantiate();
return this.api.acirCreateProof(this.acirComposer, this.acirUncompressedBytecode, uncompressedWitness);
}

/**
* 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(
proof: Uint8Array,
numOfPublicInputs = 0,
): Promise<{
proofAsFields: string[];
vkAsFields: string[];
vkHash: string;
}> {
await this.instantiate();
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(proof: Uint8Array): Promise<boolean> {
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 UltraHonkBackend {
// These type assertions are used so that we don't
// have to initialize `api` in the constructor.
// These are initialized asynchronously in the `init` function,
// constructors cannot be asynchronous which is why we do this.

protected api!: Barretenberg;

constructor(protected acirUncompressedBytecode: Uint8Array, protected options: BackendOptions = { threads: 1 }) {}

/** @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 api = await Barretenberg.new(this.options);
const honkRecursion = true;
await api.acirInitSRS(this.acirUncompressedBytecode, honkRecursion);

// We don't init a proving key here in the Honk API
// await api.acirInitProvingKey(this.acirComposer, this.acirUncompressedBytecode);
this.api = api;
}
}

async generateProof(uncompressedWitness: Uint8Array): Promise<Uint8Array> {
await this.instantiate();
return this.api.acirProveUltraHonk(this.acirUncompressedBytecode, uncompressedWitness);
}

async verifyProof(proof: Uint8Array): Promise<boolean> {
await this.instantiate();
const vkBuf = await this.api.acirWriteVkUltraHonk(this.acirUncompressedBytecode);

return await this.api.acirVerifyUltraHonk(proof, new RawBuffer(vkBuf));
}

async getVerificationKey(): Promise<Uint8Array> {
await this.instantiate();
return await this.api.acirWriteVkUltraHonk(this.acirUncompressedBytecode);
}

// TODO(https://github.com/noir-lang/noir/issues/5661): Update this to handle Honk recursive aggregation in the browser once it is ready in the backend itself
async generateRecursiveProofArtifacts(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_proof: Uint8Array,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_numOfPublicInputs: number,
): Promise<{ proofAsFields: string[]; vkAsFields: string[]; vkHash: string }> {
await this.instantiate();
// TODO(https://github.com/noir-lang/noir/issues/5661): This needs to be updated to handle recursive aggregation.
// There is still a proofAsFields method but we could consider getting rid of it as the proof itself
// is a list of field elements.
// UltraHonk also does not have public inputs directly prepended to the proof and they are still instead
// inserted at an offset.
// const proof = reconstructProofWithPublicInputs(proofData);
// const proofAsFields = (await this.api.acirProofAsFieldsUltraHonk(proof)).slice(numOfPublicInputs);

// TODO: perhaps we should put this in the init function. Need to benchmark
// TODO how long it takes.
const vkBuf = await this.api.acirWriteVkUltraHonk(this.acirUncompressedBytecode);
const vk = await this.api.acirVkAsFieldsUltraHonk(vkBuf);

return {
// TODO(https://github.com/noir-lang/noir/issues/5661)
proofAsFields: [],
vkAsFields: vk.map(vk => vk.toString()),
// We use an empty string for the vk hash here as it is unneeded as part of the recursive artifacts
// The user can be expected to hash the vk inside their circuit to check whether the vk is the circuit
// they expect
vkHash: '',
};
}

async destroy(): Promise<void> {
if (!this.api) {
return;
}
await this.api.destroy();
}
}
17 changes: 17 additions & 0 deletions barretenberg/ts/src/barretenberg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { BarretenbergWasmMain, BarretenbergWasmMainWorker } from '../barretenber
import { getRemoteBarretenbergWasm } from '../barretenberg_wasm/helpers/index.js';
import { BarretenbergWasmWorker, fetchModuleAndThreads } from '../barretenberg_wasm/index.js';
import createDebug from 'debug';
import { Crs } from '../crs/index.js';
import { RawBuffer } from '../types/raw_buffer.js';

export { BarretenbergVerifier } from './verifier.js';
export { UltraPlonkBackend, UltraHonkBackend } from './backend.js';

const debug = createDebug('bb.js:wasm');

Expand Down Expand Up @@ -40,6 +45,18 @@ export class Barretenberg extends BarretenbergApi {
return await this.wasm.getNumThreads();
}

async initSRSForCircuitSize(circuitSize: number): Promise<void> {
const crs = await Crs.new(circuitSize + 1);
await this.commonInitSlabAllocator(circuitSize);
await this.srsInitSrs(new RawBuffer(crs.getG1Data()), crs.numPoints, new RawBuffer(crs.getG2Data()));
}

async acirInitSRS(bytecode: Uint8Array, honkRecursion: boolean): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_exact, _total, subgroupSize] = await this.acirGetCircuitSizes(bytecode, honkRecursion);
return this.initSRSForCircuitSize(subgroupSize);
}

async destroy() {
await this.wasm.destroy();
await this.worker.terminate();
Expand Down
63 changes: 63 additions & 0 deletions barretenberg/ts/src/barretenberg/verifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { BackendOptions, Barretenberg } from './index.js';
import { RawBuffer } from '../types/raw_buffer.js';

// TODO: once UP is removed we can just roll this into the bas `Barretenberg` class.

export class BarretenbergVerifier {
// 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;

constructor(private options: BackendOptions = { threads: 1 }) {}

/** @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 api = await Barretenberg.new(this.options);
await api.initSRSForCircuitSize(0);

this.acirComposer = await api.acirNewAcirComposer(0);
this.api = api;
}
}

/** @description Verifies a proof */
async verifyUltraplonkProof(proof: Uint8Array, verificationKey: Uint8Array): Promise<boolean> {
await this.instantiate();
// The verifier can be used for a variety of ACIR programs so we should not assume that it
// is preloaded with the correct verification key.
await this.api.acirLoadVerificationKey(this.acirComposer, new RawBuffer(verificationKey));

return await this.api.acirVerifyProof(this.acirComposer, proof);
}

/** @description Verifies a proof */
async verifyUltrahonkProof(proof: Uint8Array, verificationKey: Uint8Array): Promise<boolean> {
await this.instantiate();

return await this.api.acirVerifyUltraHonk(proof, new RawBuffer(verificationKey));
}

async destroy(): Promise<void> {
if (!this.api) {
return;
}
await this.api.destroy();
}
}
9 changes: 8 additions & 1 deletion barretenberg/ts/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export { Crs } from './crs/index.js';
export { Barretenberg, BarretenbergSync } from './barretenberg/index.js';
export {
BackendOptions,
Barretenberg,
BarretenbergSync,
BarretenbergVerifier,
UltraPlonkBackend,
UltraHonkBackend,
} from './barretenberg/index.js';
export { RawBuffer, Fr } from './types/index.js';
3 changes: 3 additions & 0 deletions barretenberg/ts/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ export default {
],
resolve: {
plugins: [new ResolveTypeScriptPlugin()],
fallback: {
"os": false
}
},
devServer: {
hot: false,
Expand Down
Loading

0 comments on commit 0237a20

Please sign in to comment.