Skip to content

Commit 5b064bc

Browse files
saleelTomAFrench
andauthoredMar 20, 2025··
fix(bb.js): remove size metadata from UH proof (#12775)
Fixes #11829 - Remove first 4 bytes from proof (metadata - length of "proof + PI" in fields) returned from `UltraHonkBackend.generateProof()` - `proof` returned is now 14080 bytes (440 fields) and can be directly verified in solidity Note: `proof` output from bb CLI also includes the size metadata in the first 4 bytes. This should go away with #11024 --------- Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com>
1 parent fa2bf95 commit 5b064bc

File tree

6 files changed

+41
-71
lines changed

6 files changed

+41
-71
lines changed
 

‎barretenberg/acir_tests/bbjs-test/src/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import assert from "assert";
77
createDebug.enable("*");
88
const debug = createDebug("bbjs-test");
99

10+
const UH_PROOF_FIELDS_LENGTH = 440;
11+
const BYTES_PER_FIELD = 32;
12+
const UH_PROOF_LENGTH_IN_BYTES = UH_PROOF_FIELDS_LENGTH * BYTES_PER_FIELD;
13+
1014
const proofPath = (dir: string) => path.join(dir, "proof");
1115
const publicInputsPath = (dir: string) => path.join(dir, "public-inputs");
1216
const vkeyPath = (dir: string) => path.join(dir, "vk");
@@ -33,6 +37,7 @@ async function generateProof({
3337

3438
const witness = await fs.readFile(witnessPath);
3539
const proof = await backend.generateProof(new Uint8Array(witness), { keccak: (oracleHash === "keccak") });
40+
assert(proof.proof.length === UH_PROOF_LENGTH_IN_BYTES, `Unexpected proof length ${proof.proof.length} for ${bytecodePath}`);
3641

3742
await fs.writeFile(proofPath(outputDirectory), Buffer.from(proof.proof));
3843
debug("Proof written to " + proofPath(outputDirectory));
@@ -53,6 +58,8 @@ async function verifyProof({ directory }: { directory: string }) {
5358
const verifier = new BarretenbergVerifier();
5459

5560
const proof = await fs.readFile(proofPath(directory));
61+
assert(proof.length === UH_PROOF_LENGTH_IN_BYTES, `Unexpected proof length ${proof.length}`);
62+
5663
const publicInputs = JSON.parse(await fs.readFile(publicInputsPath(directory), "utf8"));
5764
const vkey = await fs.readFile(vkeyPath(directory));
5865

‎barretenberg/acir_tests/flows/bb_prove_bbjs_verify.sh

+4-5
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,16 @@ $BIN write_vk \
3232
# bb.js expects proof and public inputs to be separate files, so we need to split them
3333
# this will not be needed after #11024
3434

35-
# Save public inputs as separate file (first NUM_PUBLIC_INPUTS fields of proof_fields.json)
35+
# Save public inputs as a separate file (first NUM_PUBLIC_INPUTS fields of proof_fields.json)
3636
PROOF_FIELDS_LENGTH=$(jq 'length' $output_dir/proof_fields.json)
3737
UH_PROOF_FIELDS_LENGTH=440
3838
NUM_PUBLIC_INPUTS=$((PROOF_FIELDS_LENGTH - UH_PROOF_FIELDS_LENGTH))
3939
jq ".[:$NUM_PUBLIC_INPUTS]" $output_dir/proof_fields.json > $output_dir/public-inputs
4040

41-
# Remove NUM_PUBLIC_INPUTS*32 bytes from the proof
41+
# Remove public inputs from the proof (first NUM_PUBLIC_INPUTS*32 bytes)
42+
# Also remove the first 4 bytes, which is the proof length in fields
4243
proof_hex=$(cat $output_dir/proof | xxd -p)
43-
proof_start=${proof_hex:0:8}
44-
proof_end=${proof_hex:$((8 + NUM_PUBLIC_INPUTS * 64))}
45-
echo -n $proof_start$proof_end | xxd -r -p > $output_dir/proof
44+
echo -n ${proof_hex:$((NUM_PUBLIC_INPUTS * 64 + 8))} | xxd -r -p > $output_dir/proof
4645

4746
# Verify the proof with bb.js classes
4847
node ../../bbjs-test verify \

‎barretenberg/acir_tests/flows/bbjs_prove_bb_verify.sh

+8-7
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,22 @@ node ../../bbjs-test prove \
2424
# Join the proof and public inputs to a single file
2525
# this will not be needed after #11024
2626

27+
NUM_PUBLIC_INPUTS=$(cat $output_dir/public-inputs | jq 'length')
28+
UH_PROOF_FIELDS_LENGTH=440
29+
PROOF_AND_PI_LENGTH_IN_FIELDS=$((NUM_PUBLIC_INPUTS + UH_PROOF_FIELDS_LENGTH))
30+
# First 4 bytes is PROOF_AND_PI_LENGTH_IN_FIELDS
31+
proof_header=$(printf "%08x" $PROOF_AND_PI_LENGTH_IN_FIELDS)
32+
2733
proof_bytes=$(cat $output_dir/proof | xxd -p)
2834
public_inputs=$(cat $output_dir/public-inputs | jq -r '.[]')
29-
proof_start=${proof_bytes:0:8}
30-
proof_end=${proof_bytes:8}
3135

3236
public_inputs_bytes=""
3337
for input in $public_inputs; do
3438
public_inputs_bytes+=$input
3539
done
3640

37-
# Combine proof start, public inputs, and rest of proof
38-
echo -n $proof_start$public_inputs_bytes$proof_end | xxd -r -p > $output_dir/proof
39-
40-
# Print the length of the proof file in bytes
41-
ls -l $output_dir/proof | awk '{print $5}'
41+
# Combine proof header, public inputs, and the proof to a single file
42+
echo -n $proof_header$public_inputs_bytes$proof_bytes | xxd -r -p > $output_dir/proof
4243

4344
# Verify the proof with bb cli
4445
$BIN verify \

‎barretenberg/acir_tests/sol-test/src/index.js

+4-5
Original file line numberDiff line numberDiff line change
@@ -248,12 +248,11 @@ try {
248248
);
249249

250250
proofStr = proofStr.substring(32 * 2 * numPublicInputs); // Remove the publicInput bytes from the proof
251-
}
252251

253-
// Honk proof have field length as the first 4 bytes
254-
// This should go away in the future
255-
if (testingHonk) {
256-
proofStr = proofStr.substring(8);
252+
// Honk proof from the CLI have field length as the first 4 bytes. This should go away in the future
253+
if (testingHonk) {
254+
proofStr = proofStr.substring(8);
255+
}
257256
}
258257

259258
proofStr = "0x" + proofStr;

‎barretenberg/ts/src/barretenberg/backend.ts

+7-38
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
ProofDataForRecursion,
99
reconstructHonkProof,
1010
reconstructUltraPlonkProof,
11+
splitHonkProof,
1112
} from '../proof/index.js';
1213

1314
export class AztecClientBackendError extends Error {
@@ -154,10 +155,6 @@ export class UltraPlonkBackend {
154155
}
155156
}
156157

157-
// Buffers are prepended with their size. The size takes 4 bytes.
158-
const serializedBufferSize = 4;
159-
const fieldByteSize = 32;
160-
161158
/**
162159
* Options for the UltraHonkBackend.
163160
*/
@@ -222,22 +219,8 @@ export class UltraHonkBackend {
222219
// Item at index 1 in VK is the number of public inputs
223220
const numPublicInputs = Number(vkAsFields[1].toString());
224221

225-
226-
// Account for the serialized buffer size at start
227-
// Get the part before and after the public inputs
228-
const proofStart = proofWithPublicInputs.slice(0, serializedBufferSize);
229-
const publicInputsSplitIndex = numPublicInputs * fieldByteSize;
230-
const proofEnd = proofWithPublicInputs.slice(serializedBufferSize + publicInputsSplitIndex);
231-
232-
// Construct the proof without the public inputs
233-
const proof = new Uint8Array([...proofStart, ...proofEnd]);
234-
235-
// Fetch the number of public inputs out of the proof string
236-
const publicInputsConcatenated = proofWithPublicInputs.slice(
237-
serializedBufferSize,
238-
serializedBufferSize + publicInputsSplitIndex,
239-
);
240-
const publicInputs = deflattenFields(publicInputsConcatenated);
222+
const { proof, publicInputs: publicInputsBytes } = splitHonkProof(proofWithPublicInputs, numPublicInputs);
223+
const publicInputs = deflattenFields(publicInputsBytes);
241224

242225
return { proof, publicInputs };
243226
}
@@ -265,29 +248,15 @@ export class UltraHonkBackend {
265248
const vk = await writeVKUltraHonk(this.acirUncompressedBytecode, this.circuitOptions.recursive);
266249
const vkAsFields = await this.api.acirVkAsFieldsUltraHonk(new RawBuffer(vk));
267250

268-
// proofWithPublicInputs starts with a four-byte size
269-
const numSerdeHeaderBytes = 4;
270251
// some public inputs are handled specially
271252
const numKZGAccumulatorFieldElements = 16;
272253
const publicInputsSizeIndex = 1; // index into VK for numPublicInputs
273-
274254
const numPublicInputs = Number(vkAsFields[publicInputsSizeIndex].toString()) - numKZGAccumulatorFieldElements;
275255

276-
// Construct the proof without the public inputs
277-
const numPublicInputsBytes = numPublicInputs * fieldByteSize;
278-
const proofNoPIs = new Uint8Array(proofWithPublicInputs.length - numPublicInputsBytes);
279-
// copy the elements before the public inputs
280-
proofNoPIs.set(proofWithPublicInputs.subarray(0, numSerdeHeaderBytes), 0);
281-
// copy the elements after the public inputs
282-
proofNoPIs.set(proofWithPublicInputs.subarray(numSerdeHeaderBytes + numPublicInputsBytes), numSerdeHeaderBytes);
283-
const proof: string[] = deflattenFields(proofNoPIs.slice(numSerdeHeaderBytes));
284-
285-
// Fetch the number of public inputs out of the proof string
286-
const publicInputsConcatenated = proofWithPublicInputs.slice(
287-
serializedBufferSize,
288-
serializedBufferSize + numPublicInputsBytes,
289-
);
290-
const publicInputs = deflattenFields(publicInputsConcatenated);
256+
const { proof: proofBytes, publicInputs: publicInputsBytes } = splitHonkProof(proofWithPublicInputs, numPublicInputs);
257+
258+
const publicInputs = deflattenFields(publicInputsBytes);
259+
const proof = deflattenFields(proofBytes);
291260

292261
return { proof, publicInputs };
293262
}

‎barretenberg/ts/src/proof/index.ts

+11-16
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { numToUInt32BE } from "../serialize/serialize.js";
2+
13
/**
24
* @description
35
* The representation of a proof
@@ -20,24 +22,19 @@ export type ProofDataForRecursion = {
2022
proof: string[];
2123
};
2224

23-
// Buffers are prepended with their size. The size takes 4 bytes.
24-
const serializedBufferSize = 4;
25+
// Honk proofs start with 4 bytes for the size of the proof in fields
26+
const metadataOffset = 4;
2527
const fieldByteSize = 32;
2628

2729
export function splitHonkProof(
2830
proofWithPublicInputs: Uint8Array,
2931
numPublicInputs: number,
3032
): { publicInputs: Uint8Array; proof: Uint8Array } {
31-
// Account for the serialized buffer size at start
32-
// Get the part before and after the public inputs
33-
const proofStart = proofWithPublicInputs.slice(0, serializedBufferSize);
34-
const publicInputsSplitIndex = numPublicInputs * fieldByteSize;
35-
const proofEnd = proofWithPublicInputs.slice(serializedBufferSize + publicInputsSplitIndex);
36-
// Construct the proof without the public inputs
37-
const proof = new Uint8Array([...proofStart, ...proofEnd]);
33+
// Remove the metadata (proof size in fields)
34+
const proofWithPI = proofWithPublicInputs.slice(metadataOffset);
3835

39-
// Fetch the number of public inputs out of the proof string
40-
const publicInputs = proofWithPublicInputs.slice(serializedBufferSize, serializedBufferSize + publicInputsSplitIndex);
36+
const publicInputs = proofWithPI.slice(0, numPublicInputs * fieldByteSize);
37+
const proof = proofWithPI.slice(numPublicInputs * fieldByteSize);
4138

4239
return {
4340
proof,
@@ -46,12 +43,10 @@ export function splitHonkProof(
4643
}
4744

4845
export function reconstructHonkProof(publicInputs: Uint8Array, proof: Uint8Array): Uint8Array {
49-
const proofStart = proof.slice(0, serializedBufferSize);
50-
const proofEnd = proof.slice(serializedBufferSize);
51-
52-
// Concatenate publicInputs and proof
53-
const proofWithPublicInputs = Uint8Array.from([...proofStart, ...publicInputs, ...proofEnd]);
46+
// Append proofWithPublicInputs size in fields
47+
const proofSize = numToUInt32BE((publicInputs.length + proof.length) / fieldByteSize);
5448

49+
const proofWithPublicInputs = Uint8Array.from([...proofSize, ...publicInputs, ...proof]);
5550
return proofWithPublicInputs;
5651
}
5752

0 commit comments

Comments
 (0)
Please sign in to comment.