UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

172 lines (153 loc) 5.77 kB
import { bytesToBigInt, changeBase } from '../crypto/bigint-helpers.js'; import { Field } from '../../lib/provable/wrapped.js'; export { stringToFields, stringFromFields, bytesToFields, bytesFromFields, Bijective }; // functions for encoding data as field elements // these methods are not for in-snark computation -- from the snark POV, // encryption operates on an array of field elements. // we also assume here that all fields are constant! // caveat: this is suitable for encoding arbitrary bytes as fields, but not the other way round // to encode fields as bytes in a recoverable way, you need different methods /** * Encodes a JavaScript string into a list of {@link Field} elements. * * This function is not a valid in-snark computation. */ function stringToFields(message: string) { let bytes = new TextEncoder().encode(message); return bytesToFields(bytes); } /** * Decodes a list of {@link Field} elements into a JavaScript string. * * This function is not a valid in-snark computation. */ function stringFromFields(fields: Field[]) { let bytes = bytesFromFields(fields); return new TextDecoder().decode(bytes); } const STOP = 0x01; /** * Encodes a {@link Uint8Array} into {@link Field} elements. */ function bytesToFields(bytes: Uint8Array) { // we encode 248 bits (31 bytes) at a time into one field element let fields = []; let currentBigInt = 0n; let bitPosition = 0n; for (let byte of bytes) { currentBigInt += BigInt(byte) << bitPosition; bitPosition += 8n; if (bitPosition === 248n) { fields.push(Field(currentBigInt.toString())); currentBigInt = 0n; bitPosition = 0n; } } // encode the final chunk, with an added STOP byte to make the mapping invertible currentBigInt += BigInt(STOP) << bitPosition; fields.push(Field(currentBigInt.toString())); return fields; } /** * Decodes a list of {@link Field} elements into a {@link Uint8Array}. */ function bytesFromFields(fields: Field[]) { // find STOP byte in last chunk to determine length of byte array let lastChunk = fields.pop(); if (lastChunk === undefined) return new Uint8Array(); let lastChunkBytes = bytesOfConstantField(lastChunk); let i = lastChunkBytes.lastIndexOf(STOP, 30); if (i === -1) throw Error('Error (bytesFromFields): Invalid encoding.'); let bytes = new Uint8Array(fields.length * 31 + i); bytes.set(lastChunkBytes.subarray(0, i), fields.length * 31); // convert the remaining fields i = 0; for (let field of fields) { bytes.set(bytesOfConstantField(field).subarray(0, 31), i); i += 31; } fields.push(lastChunk); return bytes; } // bijective fields <--> bytes mapping // this is suitable for converting *arbitrary* fields AND bytes back and forth // the interpretation of the fields/bytes array is as digits of a single big integer // which implies the small caveat that trailing zeroes in the field/bytes array get ignored // another caveat: the algorithm is O(n^(1 + t)) with t > 0; ~1MB of field elements take about 1-2s to convert // this needs the exact field size let p = 0x40000000000000000000000000000000224698fc094cf91b992d30ed00000001n; let q = 0x40000000000000000000000000000000224698fc0994a8dd8c46eb2100000001n; let bytesPerBigInt = 32; let bytesBase = 256n ** BigInt(bytesPerBigInt); const Bijective = { Fp: { toBytes: (fields: Field[]) => toBytesBijective(fields, p), fromBytes: (bytes: Uint8Array) => toFieldsBijective(bytes, p), toString(fields: Field[]) { return new TextDecoder().decode(toBytesBijective(fields, p)); }, fromString(message: string) { let bytes = new TextEncoder().encode(message); return toFieldsBijective(bytes, p); }, }, Fq: { toBytes: (fields: Field[]) => toBytesBijective(fields, q), fromBytes: (bytes: Uint8Array) => toFieldsBijective(bytes, q), toString(fields: Field[]) { return new TextDecoder().decode(toBytesBijective(fields, q)); }, fromString(message: string) { let bytes = new TextEncoder().encode(message); return toFieldsBijective(bytes, q); }, }, }; function toBytesBijective(fields: Field[], p: bigint) { let fieldsBigInts = fields.map((x) => x.toBigInt()); let bytesBig = changeBase(fieldsBigInts, p, bytesBase); let bytes = bigIntArrayToBytes(bytesBig, bytesPerBigInt); return bytes; } function toFieldsBijective(bytes: Uint8Array, p: bigint) { let bytesBig = bytesToBigIntArray(bytes, bytesPerBigInt); let fieldsBigInts = changeBase(bytesBig, bytesBase, p); let fields = fieldsBigInts.map(Field); return fields; } function bytesOfConstantField(field: Field): Uint8Array { return Uint8Array.from(Field.toBytes(field)); } function bigIntToBytes(x: bigint, length: number) { let bytes = []; for (; x > 0; x >>= 8n) { bytes.push(Number(x & 0xffn)); } let array = new Uint8Array(bytes); if (length === undefined) return array; if (array.length > length) throw Error(`bigint doesn't fit into ${length} bytes.`); let sizedArray = new Uint8Array(length); sizedArray.set(array); return sizedArray; } function bytesToBigIntArray(bytes: Uint8Array, bytesPerBigInt: number) { let bigints = []; for (let i = 0; i < bytes.byteLength; i += bytesPerBigInt) { bigints.push(bytesToBigInt(bytes.subarray(i, i + bytesPerBigInt))); } return bigints; } function bigIntArrayToBytes(bigints: bigint[], bytesPerBigInt: number) { let bytes = new Uint8Array(bigints.length * bytesPerBigInt); let offset = 0; for (let b of bigints) { bytes.set(bigIntToBytes(b, bytesPerBigInt), offset); offset += bytesPerBigInt; } // remove zero bytes let i = bytes.byteLength - 1; for (; i >= 0; i--) { if (bytes[i] !== 0) break; } return bytes.slice(0, i + 1); }