o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
172 lines (153 loc) • 5.77 kB
text/typescript
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);
}