Skip to content

Commit

Permalink
Merge pull request #2 from o1-labs/move-bindings-2
Browse files Browse the repository at this point in the history
Move bindings, part 2
  • Loading branch information
mitschabaude committed May 3, 2023
2 parents 543ea66 + 290d7b8 commit 9f07489
Show file tree
Hide file tree
Showing 41 changed files with 7,786 additions and 17 deletions.
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/CODEOWNERS @bkase @mitschabaude @mrmr1993
/kimchi @o1-labs/crypto-eng-reviewers
. @o1-labs/eng
8 changes: 4 additions & 4 deletions compiled/node_bindings/snarky_js_node.bc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -66743,7 +66743,7 @@
_gF$_=
[0,
caml_string_of_jsbytes
("src/lib/snarkyjs/src/snarkyjs-bindings/ocaml/lib/snarky_js_bindings_lib.ml"),
("src/lib/snarkyjs/src/bindings/ocaml/lib/snarky_js_bindings_lib.ml"),
1493,
13],
_gF5_=caml_string_of_jsbytes("if: Mismatched argument types"),
Expand Down Expand Up @@ -66865,14 +66865,14 @@
_gED_=
[0,
caml_string_of_jsbytes
("src/lib/snarkyjs/src/snarkyjs-bindings/ocaml/lib/snarky_js_bindings_lib.ml"),
("src/lib/snarkyjs/src/bindings/ocaml/lib/snarky_js_bindings_lib.ml"),
692,
21],
_gEE_=caml_string_of_jsbytes("Expected array of length 1"),
_gEz_=
[0,
caml_string_of_jsbytes
("src/lib/snarkyjs/src/snarkyjs-bindings/ocaml/lib/snarky_js_bindings_lib.ml"),
("src/lib/snarkyjs/src/bindings/ocaml/lib/snarky_js_bindings_lib.ml"),
680,
34],
_gEr_=caml_string_of_jsbytes(""),
Expand All @@ -66898,7 +66898,7 @@
_gDR_=
[0,
caml_string_of_jsbytes
("src/lib/snarkyjs/src/snarkyjs-bindings/ocaml/lib/snarky_js_bindings_lib.ml"),
("src/lib/snarkyjs/src/bindings/ocaml/lib/snarky_js_bindings_lib.ml"),
493,
33],
_gDS_=caml_string_of_jsbytes("non-constant"),
Expand Down
144 changes: 144 additions & 0 deletions crypto/bigint-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
export { changeBase, bytesToBigInt, bigIntToBytes };

function bytesToBigInt(bytes: Uint8Array | number[]) {
let x = 0n;
let bitPosition = 0n;
for (let byte of bytes) {
x += BigInt(byte) << bitPosition;
bitPosition += 8n;
}
return x;
}

/**
* 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}`);
}
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 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;
}
31 changes: 31 additions & 0 deletions crypto/bigint.unit-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { expect } from 'expect';
import { shutdown } from '../../snarky.js';
import { bytesToBigInt, bigIntToBytes } from './bigint-helpers.js';
import { Fp } from './finite_field.js';

function testBigintRoundtrip(x: bigint, size: number) {
let bytes = bigIntToBytes(x, size);
let x1 = bytesToBigInt(bytes);
expect(x1).toEqual(x);
}
let fieldSize = Math.ceil(Fp.sizeInBits / 8);

testBigintRoundtrip(0n, 1);
testBigintRoundtrip(0n, fieldSize);
testBigintRoundtrip(56n, 2);
testBigintRoundtrip(40n, fieldSize);
testBigintRoundtrip(1309180n, fieldSize);
testBigintRoundtrip(0x10000000n, 4);
testBigintRoundtrip(0xffffffffn, 4);
testBigintRoundtrip(0x10ff00ffffn, fieldSize);
testBigintRoundtrip(Fp.modulus, fieldSize);

// failure cases
expect(() => bigIntToBytes(256n, 1)).toThrow(/does not fit in 1 bytes/);
expect(() => bigIntToBytes(100_000n, 2)).toThrow(/does not fit in 2 bytes/);
expect(() => bigIntToBytes(4n * Fp.modulus, 32)).toThrow(
/does not fit in 32 bytes/
);

console.log('bigint unit tests are passing! 🎉');
shutdown();
Loading

0 comments on commit 9f07489

Please sign in to comment.