UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

258 lines 10.6 kB
import { provableFromClass } from './types/provable-derivers.js'; import { assert } from './gadgets/common.js'; import { chunk, chunkString } from '../util/arrays.js'; import { Provable } from './provable.js'; import { UInt8 } from './int.js'; import { randomBytes } from '../../bindings/crypto/random.js'; import { Field } from './field.js'; import { Bool } from './bool.js'; // external API export { Bytes }; // internal API export { createBytes }; /** * A provable type representing an array of bytes. */ class Bytes { constructor(bytes) { let size = this.constructor.size; // assert that data is not too long assert(bytes.length <= size, `Expected at most ${size} bytes, got ${bytes.length}`); // pad the data with zeros let padding = Array.from({ length: size - bytes.length }, () => new UInt8(0)); this.bytes = bytes.concat(padding); } /** * Coerce the input to {@link Bytes}. * * Inputs smaller than `this.size` are padded with zero bytes. */ static from(data) { if (data instanceof Bytes) return data; if (this._size === undefined) { let Bytes_ = createBytes(data.length); return Bytes_.from(data); } return new this([...data].map(UInt8.from)); } toBytes() { return Uint8Array.from(this.bytes.map((x) => x.toNumber())); } toFields() { return this.bytes.map((x) => x.value); } /** * Create {@link Bytes} from a string. * * Inputs smaller than `this.size` are padded with zero bytes. */ static fromString(s) { let bytes = new TextEncoder().encode(s); return this.from(bytes); } /** * Create random {@link Bytes} using secure builtin randomness. */ static random() { let bytes = randomBytes(this.size); return this.from(bytes); } /** * Create {@link Bytes} from a hex string. * * Inputs smaller than `this.size` are padded with zero bytes. */ static fromHex(xs) { let bytes = chunkString(xs, 2).map((s) => parseInt(s, 16)); return this.from(bytes); } /** * Convert {@link Bytes} to a hex string. */ toHex() { return this.bytes.map((x) => x.toBigInt().toString(16).padStart(2, '0')).join(''); } /** * Base64 encode bytes. */ base64Encode() { const uint8Bytes = this.bytes; // Convert each byte to its 8-bit binary representation and reverse endianness let plainBits = uint8Bytes.map((b) => b.value.toBits(8).reverse()).flat(); // Calculate the bit padding required to make the total bits length a multiple of 6 const bitPadding = plainBits.length % 6 !== 0 ? 6 - (plainBits.length % 6) : 0; // Add the required bit padding with 0 bits plainBits.push(...Array(bitPadding).fill(new Bool(false))); let encodedBytes = []; // Process the bits 6 at a time and encode to Base64 for (let i = 0; i < plainBits.length; i += 6) { // Slice the next 6 bits and reverse endianness let byteBits = plainBits.slice(i, i + 6).reverse(); // Convert the 6-bit chunk to a UInt8 value for indexing the Base64 table const indexTableByte = UInt8.Unsafe.fromField(Field.fromBits(byteBits)); // Use the index to get the corresponding Base64 character and add to the result encodedBytes.push(base64EncodeLookup(indexTableByte)); } // Add '=' padding to the encoded output if required const paddingLength = uint8Bytes.length % 3 !== 0 ? 3 - (uint8Bytes.length % 3) : 0; encodedBytes.push(...Array(paddingLength).fill(UInt8.from(61))); return Bytes.from(encodedBytes); } /** * Decode Base64-encoded bytes. * * @param byteLength The length of the output decoded bytes. * @returns Decoded bytes as {@link Bytes}. * * @warning * Ensure the input Base64 string does not contain '=' characters in the middle, * as it can cause unexpected decoding results. */ base64Decode(byteLength) { const encodedB64Bytes = this.bytes; const charLength = encodedB64Bytes.length; assert(charLength % 4 === 0, 'Input base64 byte length should be a multiple of 4!'); let decodedB64Bytes = new Array(byteLength).fill(UInt8.from(0)); let bitsIn = Array.from({ length: charLength / 4 }, () => []); let bitsOut = Array.from({ length: charLength / 4 }, () => Array.from({ length: 4 }, () => [])); let idx = 0; for (let i = 0; i < charLength / 4; i++) { for (let j = 0; j < 4; j++) { const translated = base64DecodeLookup(encodedB64Bytes[4 * i + j]); bitsIn[i][j] = translated.toBits(6); } // Convert from four 6-bit words to three 8-bit words, unpacking the base64 encoding bitsOut[i][0] = [bitsIn[i][1][4], bitsIn[i][1][5], ...bitsIn[i][0]]; for (let j = 0; j < 4; j++) { bitsOut[i][1][j] = bitsIn[i][2][j + 2]; bitsOut[i][1][j + 4] = bitsIn[i][1][j]; } bitsOut[i][2] = [...bitsIn[i][3], bitsIn[i][2][0], bitsIn[i][2][1]]; for (let j = 0; j < 3; j++) { if (idx + j < byteLength) { decodedB64Bytes[idx + j] = UInt8.Unsafe.fromField(Field.fromBits(bitsOut[i][j])); } } idx += 3; } return Bytes.from(decodedB64Bytes); } /** * Returns an array of chunks, each of size `size`. * @param size size of each chunk * @returns an array of {@link UInt8} chunks */ chunk(size) { return chunk(this.bytes, size); } /** * The size of the {@link Bytes}. */ static get size() { assert(this._size !== undefined, 'Bytes not initialized'); return this._size; } get length() { return this.bytes.length; } /** * `Provable<Bytes>` */ static get provable() { assert(this._provable !== undefined, 'Bytes not initialized'); return this._provable; } } function createBytes(size) { var _a; return _a = class Bytes_ extends Bytes { }, _a._size = size, _a._provable = provableFromClass(_a, { bytes: Provable.Array(UInt8, size), }), _a; } /** * Decodes a Base64 character to its original value. * Adapted from the algorithm described in: http://0x80.pl/notesen/2016-01-17-sse-base64-decoding.html#vector-lookup-base * * @param input - The Base64 encoded byte to be decoded. * @returns - The corresponding decoded value as a Field. */ function base64DecodeLookup(input) { // Initialize a Field to validate if the input byte is a valid Base64 character let isValidBase64Chars = new Field(0); // ['A' - 'Z'] range const le_Z = input.lessThan(91); const ge_A = input.greaterThan(64); const range_AZ = le_Z.and(ge_A); const sum_AZ = range_AZ.toField().mul(input.value.sub(65)); isValidBase64Chars = isValidBase64Chars.add(range_AZ.toField()); // ['a' - 'z'] range const le_z = input.lessThan(123); const ge_a = input.greaterThan(96); const range_az = le_z.and(ge_a); const sum_az = range_az.toField().mul(input.value.sub(71)).add(sum_AZ); isValidBase64Chars = isValidBase64Chars.add(range_az.toField()); // ['0' - '9'] range const le_9 = input.lessThan(58); const ge_0 = input.greaterThan(47); const range_09 = le_9.and(ge_0); const sum_09 = range_09.toField().mul(input.value.add(4)).add(sum_az); isValidBase64Chars = isValidBase64Chars.add(range_09.toField()); // '+' character const equal_plus = input.value.equals(43); const sum_plus = equal_plus.toField().mul(input.value.add(19)).add(sum_09); isValidBase64Chars = isValidBase64Chars.add(equal_plus.toField()); // '/' character const equal_slash = input.value.equals(47); const sum_slash = equal_slash.toField().mul(input.value.add(16)).add(sum_plus); isValidBase64Chars = isValidBase64Chars.add(equal_slash.toField()); // '=' character const equal_eqsign = input.value.equals(61); isValidBase64Chars = isValidBase64Chars.add(equal_eqsign.toField()); // Validate if input contains only valid Base64 characters isValidBase64Chars.assertEquals(1, 'Please provide Base64-encoded bytes containing only alphanumeric characters and +/='); return sum_slash; } /** * Encodes a byte into its Base64 character representation. * * @param input - The byte to be encoded to Base64. * @returns - The corresponding Base64 encoded character as a UInt8. */ function base64EncodeLookup(input) { // Initialize a Field to validate if the input byte is included in the Base64 index table let isValidBase64Chars = new Field(0); // ['A', 'Z'] - Note: Remove greater than zero check because a UInt8 byte is always positive const le_Z = input.lessThanOrEqual(25); const range_AZ = le_Z; const sum_AZ = range_AZ.toField().mul(input.value.add(65)); isValidBase64Chars = isValidBase64Chars.add(range_AZ.toField()); // ['a', 'z'] const le_z = input.lessThanOrEqual(51); const ge_a = input.greaterThanOrEqual(26); const range_az = le_z.and(ge_a); const sum_az = range_az.toField().mul(input.value.add(71)).add(sum_AZ); isValidBase64Chars = isValidBase64Chars.add(range_az.toField()); // ['0', '9'] const le_9 = input.lessThanOrEqual(61); const ge_0 = input.greaterThanOrEqual(52); const range_09 = le_9.and(ge_0); const sum_09 = range_09.toField().mul(input.value.sub(4)).add(sum_az); isValidBase64Chars = isValidBase64Chars.add(range_09.toField()); // '+' const equal_plus = input.value.equals(62); const sum_plus = equal_plus.toField().mul(input.value.sub(19)).add(sum_09); isValidBase64Chars = isValidBase64Chars.add(equal_plus.toField()); // '/' const equal_slash = input.value.equals(63); const sum_slash = equal_slash.toField().mul(input.value.sub(16)).add(sum_plus); isValidBase64Chars = isValidBase64Chars.add(equal_slash.toField()); // Validate if input contains only valid base64 characters isValidBase64Chars.assertEquals(1, 'Invalid character detected: The input contains a byte that is not present in the BASE64 index table!'); return UInt8.Unsafe.fromField(sum_slash); } //# sourceMappingURL=bytes.js.map