o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
302 lines • 10.9 kB
JavaScript
// generic encoding infrastructure
import { assertNonNegativeInteger, } from '../crypto/non-negative.js';
import { bytesToBigInt, bigIntToBytes } from '../crypto/bigint-helpers.js';
export { defineBinable, withVersionNumber, tuple, record, enumWithArgument, prefixToField, bytesToBits, bitsToBytes, withBits, withCheck, stringToBytes, stringFromBytes, stringLengthInBytes, BinableString, BinableInt32, BinableInt64, BinableUint32, BinableUint64, };
function defineBinable({ toBytes, readBytes, }) {
// spec: input offset has to be a non-negative integer, and be smaller than the bytes length
// output offset has to be greater or equal input, and not exceed the bytes length
let readBytes_ = (bytes, offset) => {
assertNonNegativeInteger(offset, 'readBytes: offset must be integer >= 0');
if (offset >= bytes.length)
throw Error('readBytes: offset must be within bytes length');
let [value, end] = readBytes(bytes, offset);
if (end < offset)
throw Error('offset returned by readBytes must be greater than initial offset');
if (end > bytes.length)
throw Error('offset returned by readBytes must not exceed bytes length');
return [value, end];
};
return {
toBytes,
readBytes: readBytes_,
// spec: fromBytes throws if the input bytes are not all used
fromBytes(bytes) {
let [value, offset] = readBytes_(bytes, 0);
if (offset < bytes.length)
throw Error('fromBytes: input bytes left over');
return value;
},
};
}
function withVersionNumber(binable, versionNumber) {
return defineBinable({
toBytes(t) {
let bytes = binable.toBytes(t);
bytes.unshift(versionNumber);
return bytes;
},
readBytes(bytes, offset) {
let version = bytes[offset++];
if (version !== versionNumber) {
throw Error(`fromBytes: Invalid version byte. Expected ${versionNumber}, got ${version}.`);
}
return binable.readBytes(bytes, offset);
},
});
}
function withCheck({ toBytes, readBytes }, check) {
return defineBinable({
toBytes,
readBytes(bytes, start) {
let [value, end] = readBytes(bytes, start);
check(value);
return [value, end];
},
});
}
function record(binables, keys) {
let binablesTuple = keys.map((key) => binables[key]);
let tupleBinable = tuple(binablesTuple);
return defineBinable({
toBytes(t) {
let array = keys.map((key) => t[key]);
return tupleBinable.toBytes(array);
},
readBytes(bytes, start) {
let [tupleValue, end] = tupleBinable.readBytes(bytes, start);
let value = Object.fromEntries(keys.map((key, i) => [key, tupleValue[i]]));
return [value, end];
},
});
}
function tuple(binables) {
let n = binables.length;
return defineBinable({
toBytes(t) {
let bytes = [];
for (let i = 0; i < n; i++) {
let subBytes = binables[i].toBytes(t[i]);
bytes.push(...subBytes);
}
return bytes;
},
readBytes(bytes, offset) {
let values = [];
for (let i = 0; i < n; i++) {
let [value, newOffset] = binables[i].readBytes(bytes, offset);
offset = newOffset;
values.push(value);
}
return [values, offset];
},
});
}
function enumWithArgument(types) {
let typeToIndex = Object.fromEntries(types.map(({ type }, i) => [type, i]));
return defineBinable({
toBytes(en) {
let i = typeToIndex[en.type];
let type = types[i];
if ('value' in type) {
let binable = type.value;
return [i, ...binable.toBytes(en.value)];
}
return [i];
},
readBytes(bytes, offset) {
let i = bytes[offset];
offset++;
let type = types[i];
if ('value' in type) {
let [value, end] = type.value.readBytes(bytes, offset);
return [{ type: type.type, value }, end];
}
return [{ type: type.type }, offset];
},
});
}
const BinableString = defineBinable({
toBytes(t) {
return [stringLengthInBytes(t), ...stringToBytes(t)];
},
readBytes(bytes, offset) {
let length = bytes[offset++];
let end = offset + length;
let string = stringFromBytes(bytes.slice(offset, end));
return [string, end];
},
});
const CODE_NEG_INT8 = 0xff;
const CODE_INT16 = 0xfe;
const CODE_INT32 = 0xfd;
const CODE_INT64 = 0xfc;
function BinableInt(bits) {
let maxValue = 1n << BigInt(bits - 1);
let nBytes = bits >> 3;
if (nBytes * 8 !== bits)
throw Error('bits must be evenly divisible by 8');
return defineBinable({
toBytes(n) {
if (n < -maxValue || n >= maxValue)
throw Error(`int${bits} out of range, got ${n}`);
if (n >= 0) {
if (n < 0x80n)
return bigIntToBytes(n, 1);
if (n < 0x8000n)
return [CODE_INT16, ...bigIntToBytes(n, 2)];
if (n < 0x80000000)
return [CODE_INT32, ...bigIntToBytes(n, 4)];
else
return [CODE_INT64, ...bigIntToBytes(n, 8)];
}
else {
let M = 1n << 64n;
if (n >= -0x80n)
return [CODE_NEG_INT8, ...bigIntToBytes((M + n) & 0xffn, 1)];
if (n >= -0x8000n)
return [CODE_INT16, ...bigIntToBytes((M + n) & 0xffffn, 2)];
if (n >= -0x80000000)
return [CODE_INT32, ...bigIntToBytes((M + n) & 0xffffffffn, 4)];
else
return [CODE_INT64, ...bigIntToBytes(M + n, 8)];
}
},
readBytes(bytes, offset) {
let code = bytes[offset++];
if (code < 0x80)
return [BigInt(code), offset];
let size = {
[CODE_NEG_INT8]: 1,
[CODE_INT16]: 2,
[CODE_INT32]: 4,
[CODE_INT64]: 8,
}[code];
if (size === undefined) {
throw Error('binable integer: invalid start byte');
}
let end = offset + size;
let x = fillUInt(bytes.slice(offset, end), nBytes);
// map from uint to int range
if (x >= maxValue) {
x -= 2n * maxValue;
}
if (x < -maxValue || x >= maxValue) {
throw Error(`int${bits} out of range, got ${x}`);
}
return [x, end];
},
});
}
function fillUInt(startBytes, nBytes) {
let n = startBytes.length;
// fill up int with the highest bit of startBytes
let lastBit = startBytes[n - 1] >> 7;
let fillByte = lastBit === 1 ? 0xff : 0x00;
let intBytes = startBytes.concat(Array(nBytes - n).fill(fillByte));
// interpret result as a bigint > 0
let x = bytesToBigInt(intBytes);
return x;
}
function BinableUint(bits) {
let binableInt = BinableInt(bits);
let maxValue = 1n << BigInt(bits - 1);
return iso(binableInt, {
to(uint) {
if (uint < 0n || uint >= 2n * maxValue)
throw Error(`uint${bits} out of range, got ${uint}`);
let ret = uint >= maxValue ? uint - 2n * maxValue : uint;
return ret;
},
from(int) {
let uint = int < 0n ? int + 2n * maxValue : int;
if (uint < 0n || uint >= 2n * maxValue)
throw Error(`uint${bits} out of range, got ${uint}`);
return uint;
},
});
}
const BinableInt64 = BinableInt(64);
const BinableInt32 = BinableInt(32);
const BinableUint64 = BinableUint(64);
const BinableUint32 = BinableUint(32);
// same as Random_oracle.prefix_to_field in OCaml
// converts string to bytes and bytes to field; throws if bytes don't fit in one field
function prefixToField(Field, prefix) {
let fieldSize = Field.sizeInBytes;
if (prefix.length >= fieldSize)
throw Error('prefix too long');
let stringBytes = stringToBytes(prefix);
return Field.fromBytes(stringBytes.concat(Array(fieldSize - stringBytes.length).fill(0)));
}
function bitsToBytes([...bits]) {
let bytes = [];
while (bits.length > 0) {
let byteBits = bits.splice(0, 8);
let byte = 0;
for (let i = 0; i < 8; i++) {
if (!byteBits[i])
continue;
byte |= 1 << i;
}
bytes.push(byte);
}
return bytes;
}
function bytesToBits(bytes) {
return bytes
.map((byte) => {
let bits = Array(8);
for (let i = 0; i < 8; i++) {
bits[i] = !!(byte & 1);
byte >>= 1;
}
return bits;
})
.flat();
}
/**
* This takes a `Binable<T>` plus an optional `sizeInBits`, and derives toBits() / fromBits() functions.
* - `sizeInBits` has to observe `Math.ceil(sizeInBits / 8) === sizeInBytes`, so the bit size can be slightly smaller than the byte size
* - If `sizeInBits` is `< sizeInBytes * 8`, then we assume that toBytes() returns a byte sequence where the bits
* higher than `sizeInBits` are all 0. This assumption manifests in toBits(), where we slice off those higher bits,
* to return a result that is of length `sizeInBits`.
*
* This is useful for serializing field elements, where -- depending on the circumstance -- we either want a
* 32-byte (= 256-bit) serialization, or a 255-bit serialization
*/
function withBits(binable, sizeInBits) {
return {
...binable,
toBits(t) {
return bytesToBits(binable.toBytes(t)).slice(0, sizeInBits);
},
fromBits(bits) {
return binable.fromBytes(bitsToBytes(bits));
},
sizeInBytes: Math.ceil(sizeInBits / 8),
sizeInBits,
};
}
function iso(binable, { to, from }) {
return defineBinable({
toBytes(s) {
return binable.toBytes(to(s));
},
readBytes(bytes, offset) {
let [value, end] = binable.readBytes(bytes, offset);
return [from(value), end];
},
});
}
let encoder = new TextEncoder();
let decoder = new TextDecoder();
function stringToBytes(s) {
return [...encoder.encode(s)];
}
function stringFromBytes(bytes) {
return decoder.decode(Uint8Array.from(bytes));
}
function stringLengthInBytes(s) {
return encoder.encode(s).length;
}
//# sourceMappingURL=binable.js.map