o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
237 lines (219 loc) • 7.78 kB
text/typescript
export {
changeBase,
bytesToBigInt,
bigIntToBytes,
bigIntToBits,
parseHexString32,
log2,
max,
abs,
sign,
bytesToBigint32,
bigintToBytes32,
};
function bytesToBigint32(bytes: Uint8Array) {
let words = new BigUint64Array(bytes.buffer, bytes.byteOffset, 4);
return words[0] | (words[1] << 64n) | (words[2] << 128n) | (words[3] << 192n);
}
const mask64 = (1n << 64n) - 1n;
function bigintToBytes32(x: bigint, bytes: Uint8Array): Uint8Array {
let words = new BigUint64Array(bytes.buffer, bytes.byteOffset, 4);
words[0] = x & mask64;
words[1] = (x >> 64n) & mask64;
words[2] = (x >> 128n) & mask64;
words[3] = x >> 192n;
return bytes;
}
function bytesToBigInt(bytes: Uint8Array | number[]) {
let x = 0n;
let bitPosition = 0n;
for (let byte of bytes) {
x += BigInt(byte) << bitPosition;
bitPosition += 8n;
}
return x;
}
let hexToNum: { [hexCharCode: number]: number } = {};
for (let i = 0; i < 16; i++) hexToNum[i.toString(16).charCodeAt(0)] = i;
let encoder = new TextEncoder();
const tmpBytes = new Uint8Array(64);
function parseHexString32(input: string) {
// Parse the bytes explicitly, Bigint endianness is wrong
encoder.encodeInto(input, tmpBytes);
for (let j = 0; j < 32; j++) {
let n1 = hexToNum[tmpBytes[2 * j]];
let n0 = hexToNum[tmpBytes[2 * j + 1]];
tmpBytes[j] = (n1 << 4) | n0;
}
return bytesToBigint32(tmpBytes);
}
/**
* Transforms bigint to little-endian array of bytes (numbers between 0 and 255) of a given length.
* Throws an error if the bigint doesn't fit in the given number of bytes.
*/
function bigIntToBytes(x: bigint, length?: number) {
if (x < 0n) {
throw Error(`bigIntToBytes: negative numbers are not supported, got ${x}`);
}
if (length === undefined) return bigintToBytesFlexible(x);
let bytes: number[] = Array(length);
for (let i = 0; i < length; i++, x >>= 8n) {
bytes[i] = Number(x & 0xffn);
}
if (x > 0n) {
throw Error(`bigIntToBytes: input does not fit in ${length} bytes`);
}
return bytes;
}
function bigintToBytesFlexible(x: bigint) {
let bytes: number[] = [];
for (; x > 0n; x >>= 8n) {
bytes.push(Number(x & 0xffn));
}
return bytes;
}
/**
* Transforms bigint to little-endian array of bits (booleans).
* The length of the bit array is determined as needed.
*/
function bigIntToBits(x: bigint) {
if (x < 0n) {
throw Error(`bigIntToBits: negative numbers are not supported, got ${x}`);
}
let bits: boolean[] = [];
for (; x > 0n; x >>= 1n) {
let bit = !!(x & 1n);
bits.push(bit);
}
return bits;
}
function changeBase(digits: bigint[], base: bigint, newBase: bigint) {
// 1. accumulate digits into one gigantic bigint `x`
let x = fromBase(digits, base);
// 2. compute new digits from `x`
let newDigits = toBase(x, newBase);
return newDigits;
}
/**
* the algorithm for toBase / fromBase is more complicated than it naively has to be,
* but that is for performance reasons.
*
* we'll explain it for `fromBase`. this function is about taking an array of digits
* `[x0, ..., xn]`
* and returning the integer (bigint) that has those digits in the given `base`:
* ```
* let x = x0 + x1*base + x2*base**2 + ... + xn*base**n
* ```
*
* naively, we could just accumulate digits from left to right:
* ```
* let x = 0n;
* let p = 1n;
* for (let i=0; i<n; i++) {
* x += X[i] * p;
* p *= base;
* }
* ```
*
* in the ith step, `p = base**i` which is multiplied with `xi` and added to the sum.
* however, note that this algorithm is `O(n^2)`: let `l = log2(base)`. the base power `p` is a bigint of bit length `i*l`,
* which is multiplied by a "small" number `xi` (length l), which takes `O(i)` time in every step.
* since this is done for `i = 0,...,n`, we end up with an `O(n^2)` algorithm.
*
* HOWEVER, it turns out that there are fast multiplication algorithms, and JS bigints have them built in!
* the Schönhage-Strassen algorithm (implemented in the V8 engine, see https://github.com/v8/v8/blob/main/src/bigint/mul-fft.cc)
* can multiply two n-bit numbers in time `O(n log(n) loglog(n))`, when n is large.
*
* to take advantage of asymptotically fast multiplication, we need to re-structure our algorithm such that it multiplies roughly equal-sized
* numbers with each other (there is no asymptotic boost for multiplying a small with a large number). so, what we do is to go from the
* original digit array to arrays of successively larger digits:
* ```
* step 0: step 1: step 2:
* [x0, x1, x2, x3, ...] -> [x0 + base*x1, x2 + base*x3, ...] -> [x0 + base*x1 + base^2*(x2 + base*x3), ...] -> ...
* ```
*
* ...until after a log(n) number of steps we end up with a single "digit" which is equal to the entire sum.
*
* in the ith step, we multiply `n/2^i` pairs of numbers of bit length `2^i*l`. each of these multiplications takes
* time `O(2^i log(2^i) loglog(2^i))`. if we bound that with `O(2^i log(n) loglog(n))`, we get a runtime bounded by
* ```
* O(n/2^i * 2^i log(n) loglog(n)) = O(n log(n) loglog(n))
* ```
* in each step. Since we have `log(n)` steps, the result is `O(n log(n)^2 loglog(n))`.
*
* empirically, this method is a huge improvement over the naive `O(n^2)` algorithm and scales much better with n (the number of digits).
*
* similar conclusions hold for `toBase`.
*/
function fromBase(digits: bigint[], base: bigint) {
if (base <= 0n) throw Error('fromBase: base must be positive');
// compute powers base, base^2, base^4, ..., base^(2^k)
// with largest k s.t. n = 2^k < digits.length
let basePowers = [];
for (let power = base, n = 1; n < digits.length; power **= 2n, n *= 2) {
basePowers.push(power);
}
let k = basePowers.length;
// pad digits array with zeros s.t. digits.length === 2^k
digits = digits.concat(Array(2 ** k - digits.length).fill(0n));
// accumulate [x0, x1, x2, x3, ...] -> [x0 + base*x1, x2 + base*x3, ...] -> [x0 + base*x1 + base^2*(x2 + base*x3), ...] -> ...
// until we end up with a single element
for (let i = 0; i < k; i++) {
let newDigits = Array(digits.length >> 1);
let basePower = basePowers[i];
for (let j = 0; j < newDigits.length; j++) {
newDigits[j] = digits[2 * j] + basePower * digits[2 * j + 1];
}
digits = newDigits;
}
console.assert(digits.length === 1);
let [digit] = digits;
return digit;
}
function toBase(x: bigint, base: bigint) {
if (base <= 0n) throw Error('toBase: base must be positive');
// compute powers base, base^2, base^4, ..., base^(2^k)
// with largest k s.t. base^(2^k) < x
let basePowers = [];
for (let power = base; power < x; power **= 2n) {
basePowers.push(power);
}
let digits = [x]; // single digit w.r.t base^(2^(k+1))
// successively split digits w.r.t. base^(2^j) into digits w.r.t. base^(2^(j-1))
// until we arrive at digits w.r.t. base
let k = basePowers.length;
for (let i = 0; i < k; i++) {
let newDigits = Array(2 * digits.length);
let basePower = basePowers[k - 1 - i];
for (let j = 0; j < digits.length; j++) {
let x = digits[j];
let high = x / basePower;
newDigits[2 * j + 1] = high;
newDigits[2 * j] = x - high * basePower;
}
digits = newDigits;
}
// pop "leading" zero digits
while (digits[digits.length - 1] === 0n) {
digits.pop();
}
return digits;
}
/**
* ceil(log2(n))
* = smallest k such that n <= 2^k
*/
function log2(n: number | bigint) {
if (typeof n === 'number') n = BigInt(n);
if (n === 1n) return 0;
return (n - 1n).toString(2).length;
}
function max(a: bigint, b: bigint) {
return a > b ? a : b;
}
function abs(x: bigint) {
return x < 0n ? -x : x;
}
function sign(x: bigint): 1n | -1n {
return x >= 0 ? 1n : -1n;
}