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

chore: migrate higher-level APIs for barretenberg to bb.js #8677

Merged
merged 24 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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
Loading