From 1c11551009872e708dbf9a499b952ad40970dad3 Mon Sep 17 00:00:00 2001 From: Shigoto-dev19 Date: Mon, 6 May 2024 19:16:20 +0100 Subject: [PATCH] Optimize lookup by processing on UInt8 bytes & Adapt code --- README.md | 27 +++++++++++++-------------- package-lock.json | 4 ++-- package.json | 2 +- src/base64.test.ts | 5 ++--- src/base64.ts | 46 ++++++++++++++++++++++++---------------------- src/run.ts | 16 ++-------------- 6 files changed, 44 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 1a508d8..58d7cae 100644 --- a/README.md +++ b/README.md @@ -74,26 +74,25 @@ npm run benchmark ### Preview -| Summary | | -| --------------- | ---- | -| Total rows | 8081 | -| Generic | 1921 | -| EndoMulScalar | 88 | -| RangeCheck0 | 2112 | -| RangeCheck1 | 1056 | -| Zero | 1848 | -| ForeignFieldAdd | 1056 | +| Summary | | +| ------------- | ---- | +| Total rows | 2138 | +| Generic | 1522 | +| EndoMulScalar | 616 | | Action | Time (s) | | ------- | -------- | -| Compile | 1.985 | -| Prove | 15.496 | -| Verify | 1.021 | +| Compile | 1.104 | +| Prove | 11.219 | +| Verify | 0.844 | ## Acknowledgement -This repo is inspired by the [circom base64](https://github.com/zkemail/zk-email-verify/blob/main/packages/circuits/lib/base64.circom) -implementation. +- This repo is inspired by the [circom base64](https://github.com/zkemail/zk-email-verify/blob/main/packages/circuits/lib/base64.circom) + implementation. + +- Big thanks to [Gregor Mitscha-Baude](https://twitter.com/mitschabaude) for highlighting the inefficiency in processing full field elements. + - By operating on `UInt8` instead of full field elements, the `base64Decode` circuit rows were reduced by around **75%** from **8081** to **2138**. ## License diff --git a/package-lock.json b/package-lock.json index d7be245..52b5425 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "o1js-base64", - "version": "0.0.2", + "version": "0.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "o1js-base64", - "version": "0.0.2", + "version": "0.0.3", "license": "Apache-2.0", "devDependencies": { "@babel/preset-env": "^7.16.4", diff --git a/package.json b/package.json index 0f6d16d..472bf2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "o1js-base64", - "version": "0.0.2", + "version": "0.0.3", "description": "", "author": "", "license": "Apache-2.0", diff --git a/src/base64.test.ts b/src/base64.test.ts index dbd7f52..0a8d6c9 100644 --- a/src/base64.test.ts +++ b/src/base64.test.ts @@ -14,17 +14,16 @@ describe('Base64 Decode Tests', () => { const decodedBytes = base64Decode( Bytes.fromString(base64String), decodedByteLength - ).map((x) => Number(x.toBigInt())); + ).toBytes(); // Calculate the expected decoded bytes using JS implementation const decodedString = atob(base64String); - let expectedDecodedBytes = new Array(decodedString.length); + let expectedDecodedBytes = new Uint8Array(decodedString.length); // Populate the expected decoded bytes array with character codes for (let i = 0; i < decodedString.length; i++) { expectedDecodedBytes[i] = decodedString.charCodeAt(i); } - // Assert that the decoded bytes match the expected decoded bytes expect(decodedBytes).toEqual(expectedDecodedBytes); } diff --git a/src/base64.ts b/src/base64.ts index feb48ea..fe1e18a 100644 --- a/src/base64.ts +++ b/src/base64.ts @@ -1,16 +1,16 @@ -import { Field, Bool, Bytes, assert } from 'o1js'; +import { Field, Bool, Bytes, assert, UInt8 } from 'o1js'; export { base64Decode }; /** - * Decodes a base64-encoded input bytes to the corresponding decoded field array. + * Decodes a base64-encoded input bytes to the corresponding decoded bytes. * * @param inputBytes The base64-encoded input bytes. * @param byteLength The length of the output decoded bytes. * @returns The decoded bytes array with the expected length specified by byteLength. */ function base64Decode(inputBytes: Bytes, byteLength: number) { - const encodedB64Bytes = inputBytes.toFields(); + const encodedB64Bytes = inputBytes.bytes; const charLength = encodedB64Bytes.length; assert( @@ -18,7 +18,7 @@ function base64Decode(inputBytes: Bytes, byteLength: number) { 'Input base64 byte length should be a multiple of 4!' ); - let decodedB64Bytes: Field[] = []; + let decodedB64Bytes: UInt8[] = []; let bitsIn: Bool[][][] = Array.from({ length: charLength / 4 }, () => []); let bitsOut: Bool[][][] = Array.from({ length: charLength / 4 }, () => @@ -52,53 +52,55 @@ function base64Decode(inputBytes: Bytes, byteLength: number) { for (let j = 0; j < 3; j++) { if (idx + j < byteLength) { - decodedB64Bytes[idx + j] = Field.fromBits(bitsOut[i / 4][j]); + decodedB64Bytes[idx + j] = new UInt8( + Field.fromBits(bitsOut[i / 4][j]).value + ); } } idx += 3; } - return decodedB64Bytes; + return Bytes.from(decodedB64Bytes); } // Adapted from the algorithm described in: http://0x80.pl/notesen/2016-01-17-sse-base64-decoding.html#vector-lookup-base -function base64Lookup(input: Field): Field { +function base64Lookup(input: UInt8): Field { // A variable to check if the input consists solely of valid base64 characters - let isValidBase64Chars = Field(0); + let isValidBase64Chars = new Field(0); // ['A', 'Z'] - let le_Z = input.lessThan(Field(91 + 1)); - let ge_A = input.greaterThan(Field(65 - 1)); + let le_Z = input.lessThan(91 + 1); + let ge_A = input.greaterThan(65 - 1); let range_AZ = le_Z.and(ge_A); - let sum_AZ = range_AZ.toField().mul(input.sub(65)); + let sum_AZ = range_AZ.toField().mul(input.value.sub(65)); isValidBase64Chars = isValidBase64Chars.add(range_AZ.toField()); // ['a', 'z'] - let le_z = input.lessThan(Field(122 + 1)); - let ge_a = input.greaterThan(Field(97 - 1)); + let le_z = input.lessThan(122 + 1); + let ge_a = input.greaterThan(97 - 1); let range_az = le_z.and(ge_a); - let sum_az = range_az.toField().mul(input.sub(71)).add(sum_AZ); + let sum_az = range_az.toField().mul(input.value.sub(71)).add(sum_AZ); isValidBase64Chars = isValidBase64Chars.add(range_az.toField()); // ['0', '9'] - let le_9 = input.lessThan(Field(57 + 1)); - let ge_0 = input.greaterThan(Field(48 - 1)); + let le_9 = input.lessThan(57 + 1); + let ge_0 = input.greaterThan(48 - 1); let range_09 = le_9.and(ge_0); - let sum_09 = range_09.toField().mul(input.add(4)).add(sum_az); + let sum_09 = range_09.toField().mul(input.value.add(4)).add(sum_az); isValidBase64Chars = isValidBase64Chars.add(range_09.toField()); // '+' - let equal_plus = input.equals(43); - let sum_plus = equal_plus.toField().mul(input.add(19)).add(sum_09); + let equal_plus = input.value.equals(43); + let sum_plus = equal_plus.toField().mul(input.value.add(19)).add(sum_09); isValidBase64Chars = isValidBase64Chars.add(equal_plus.toField()); // '/' - let equal_slash = input.equals(47); - let sum_slash = equal_slash.toField().mul(input.add(16)).add(sum_plus); + let equal_slash = input.value.equals(47); + let sum_slash = equal_slash.toField().mul(input.value.add(16)).add(sum_plus); isValidBase64Chars = isValidBase64Chars.add(equal_slash.toField()); // '=' - let equal_eqsign = input.equals(61); + let equal_eqsign = input.value.equals(61); isValidBase64Chars = isValidBase64Chars.add(equal_eqsign.toField()); // Validate if input contains only valid base64 characters diff --git a/src/run.ts b/src/run.ts index 2858a42..e96d32d 100644 --- a/src/run.ts +++ b/src/run.ts @@ -12,8 +12,8 @@ let base64DecodeZkProgram = ZkProgram({ privateInputs: [Bytes44.provable], async method(base64Bytes: Bytes44) { - const fields = base64Decode(base64Bytes, Bytes32.size); - return Bytes32.provable.fromFields(fields); + const decodedBytes = base64Decode(base64Bytes, Bytes32.size); + return Bytes32.from(decodedBytes); }, }, }, @@ -37,15 +37,3 @@ console.timeEnd('prove'); console.time('verify'); await base64DecodeZkProgram.verify(proof); console.timeEnd('verify'); - -/* -Summary Preview: { - 'Total rows': 8081, - Generic: 1921, - EndoMulScalar: 88, - RangeCheck0: 2112, - RangeCheck1: 1056, - Zero: 1848, - ForeignFieldAdd: 1056 -} -*/