o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
765 lines • 28.9 kB
JavaScript
import { Bool } from './bool.js';
import { Field } from './field.js';
import { Provable } from './provable.js';
import { Gadgets } from './gadgets/gadgets.js';
import { assert } from './gadgets/common.js';
import { provable, provableFromClass } from './types/provable-derivers.js';
import { Unconstrained } from './types/unconstrained.js';
export { createProvableBigInt, ProvableBigInt };
/**
* Creates a class representing a ProvableBigInt with modular arithmetic capabilities.
* This is particularly useful for implementing prime fields that don't fit into the native field.
*
* ```ts
* const BigInt521 = createProvableBigInt(2n ** 521n - 1n); // creates a class for 521-bit integers
* ```
*
* `createProvableBigInt(modulus, config?)` takes two parameters:
* - `modulus`: The modulus of the field (must be a prime)
* - `config`: Optional configuration for custom limb size and numbers
*
* The returned class supports comprehensive arithmetic operations including:
* - Basic operations: addition, double, subtraction, multiplication, square, division
* - Advanced operations: inverse, negate, sqrt, power
* - Comparison operations: equals, assertEquals, greaterThan, lessthan, greaterThanOrEqual, lessThanOrEqual
* - Conversion methods: fromBigInt, toBigInt, fromFields, toFields, fromBits, toBits
*
* Implementation details:
*
* Internally, a ProvableBigInt is represented as an array of Field elements (limbs),
* where each limb holds 116 bits as default. The total size is determined by the configuration,
* with preset options supporting different bit lengths:
* - 348 bits (3 limbs)
* - 464 bits (4 limbs)
* - 580 bits (5 limbs)
* - 1044 bits (9 limbs)
* - 2088 bits (18 limbs)
* - 4176 bits (36 limbs)
*
* Each arithmetic operation ensures the result is a valid element of the prime field.
*
* @example
* ```ts
* // Create a Provable BigInt class with modulus 2^521 - 1
* const BigInt521 = createProvableBigInt(2n ** 521n - 1n);
*
* // Create instances
* const a = BigInt521.fromBigInt(123n);
* const b = BigInt521.fromBigInt(456n);
* const c = BigInt521.fromBigInt(1024n);
*
* // Perform operations
* const sum = a.add(b);
* const double = a.double();
* const diff = a.sub(b);
* const product = a.mul(b);
* const square = a.square();
* const quotient = a.div(b);
* const inverse = a.inverse();
* const negation = a.negate();
* const power = a.pow(b);
* const sqrt = c.sqrt();
* ```
*
* The class automatically handles modular reduction after
* arithmetic operations to maintain valid representations. All operations are
* designed to be provable and optimised for less constraints.
*
* @param modulus The modulus for the big integer arithmetic (must be prime)
* @param config Optional configuration specifying a custom limb size and number
* @returns A class representing ProvableBigInts with the specified modulus
* @throws If the modulus is zero, negative, or exceeds the maximum supported size
*/
function createProvableBigInt(modulus, config) {
var _a;
const config_ = config ?? findConfig(modulus);
assert(modulus !== 0n, `ProvableBigInt: modulus must be non-zero, got ${modulus}`);
assert(modulus > 0n, `ProvableBigInt: modulus must be positive, got ${modulus}`);
assert(modulus < config_.max, `ProvableBigInt: modulus exceeds the max supported size of 2^${config_.max}`);
assert(isPrime(modulus), 'ProvableBigInt: modulus must be prime');
let fields = bigintToLimbs(modulus, config_);
class ProvableBigInt_ extends ProvableBigInt {
constructor(fields, value) {
super(fields, Unconstrained.from(value));
}
get Constructor() {
return this.constructor;
}
/**
* Returns a ProvableBigInt representing zero
* @returns A ProvableBigInt representing zero
*/
static zero() {
return _a.fromBigInt(0n);
}
/**
* Returns a ProvableBigInt representing one
* @returns A ProvableBigInt representing one
*/
static one() {
return _a.fromBigInt(1n);
}
/**
* Returns a ProvableBigInt representing one
* @returns A ProvableBigInt representing one
*/
static max() {
return _a.fromBigInt(modulus - 1n);
}
/**
* Creates a ProvableBigInt instance from a JS bigint, string, number, or boolean
* @param x
* @returns ProvableBigInt instance from the input
*/
static from(x) {
return _a.fromBigInt(BigInt(x));
}
/**
* Creates a ProvableBigInt instance from a JS bigint
* @param x
* @returns ProvableBigInt instance from the input
*/
static fromBigInt(x) {
let value = x;
if (value < 0n) {
value = ((x % modulus) + modulus) % modulus;
}
if (value >= _a.modulus.toBigInt()) {
value = value % modulus;
}
let fields = bigintToLimbs(value, _a.config);
return new _a(fields, Unconstrained.from(value));
}
/**
* Converts a ProvableBigInt instance to a JS bigint
* @returns JS bigint representation of the ProvableBigInt
*/
toBigInt() {
let result = 0n;
for (let i = 0; i < this.Constructor.config.limbNum; i++) {
result |= this.fields[i].toBigInt() << BigInt(this.Constructor.config.limbSize * i);
}
return result;
}
/**
* Converts a ProvableBigInt instance to field array representation of the limbs
* @returns Limbs of the ProvableBigInt
*/
toFields() {
return this.fields.slice(0, this.Constructor.config.limbNum);
}
/**
* Converts a ProvableBigInt instance to an array of bits
* @returns An array of bits representing the ProvableBigInt
*/
toBits() {
return this.fields.flatMap((field) => {
return field.toBits(this.Constructor.config.limbSize);
});
}
/**
* Clones a ProvableBigInt instance
* @returns A new ProvableBigInt instance with the same value
*/
clone() {
return new _a(this.fields, this.value);
}
/**
* Adds two ProvableBigInt instances
* Cost: Cheap
* @param a The ProvableBigInt to add
* @returns The sum as a ProvableBigInt
*/
add(a, isDouble = false) {
if (isDouble)
a = this;
// witness q, r so that x+y = q*p + r
let { q, r } = Provable.witness(provable({ q: _a, r: _a }), () => {
let xPlusY = this.toBigInt() + a.toBigInt();
let p0 = this.Constructor.modulus.toBigInt();
let q = xPlusY / p0;
let r = xPlusY - q * p0;
return {
q: _a.fromBigInt(q),
r: _a.fromBigInt(r),
};
});
let delta = Array.from({ length: this.Constructor.config.limbNum }, () => Field.from(0));
let [X, Y, Q, R, P] = [
this.fields,
a.fields,
q.fields,
r.fields,
this.Constructor.modulus.fields,
];
// compute X + Y limb-by-limb
for (let i = 0; i < this.Constructor.config.limbNum; i++) {
if (isDouble)
delta[i] = X[i].mul(2);
else
delta[i] = X[i].add(Y[i]);
}
// subtract q*p limb-by-limb
for (let i = 0; i < this.Constructor.config.limbNum; i++) {
for (let j = 0; j < this.Constructor.config.limbNum; j++) {
if (i + j < this.Constructor.config.limbNum) {
delta[i + j] = delta[i + j].sub(Q[i].mul(P[j]));
}
}
}
// subtract r limb-by-limb
for (let i = 0; i < this.Constructor.config.limbNum; i++) {
delta[i] = delta[i].sub(R[i]).seal();
}
let carry = Field.from(0);
for (let i = 0; i < this.Constructor.config.limbNum - 1; i++) {
let deltaPlusCarry = delta[i].add(carry).seal();
carry = Provable.witness(Field, () => deltaPlusCarry.div(1n << BigInt(this.Constructor.config.limbSize)));
rangeCheck(carry, 128, true);
// ensure that after adding the carry, the limb is a multiple of 2^limbSize
deltaPlusCarry.assertEquals(carry.mul(1n << BigInt(this.Constructor.config.limbSize)));
}
// the final limb plus carry should be zero to assert correctness
delta[this.Constructor.config.limbNum - 1].add(carry).assertEquals(0n);
return r;
}
/**
* Doubles a ProvableBigInt
* Cost: Cheap
* @returns The double of a ProvableBigInt
*/
double() {
return this.add(this, true);
}
/**
* Subtracts one ProvableBigInt from another
* Cost: Cheap
* @param a The ProvableBigInt to subtract
* @returns The difference as a ProvableBigInt
*/
sub(a) {
return this.add(a.negate());
}
/**
* Multiplies two ProvableBigInt instances
* Cost: Cheap
* @param a The ProvableBigInt to multiply
* @returns The product as a ProvableBigInt
*/
mul(a, isSquare = false) {
if (isSquare)
a = this;
let { q, r } = Provable.witness(provable({ q: _a, r: _a }), () => {
let xy = this.toBigInt() * a.toBigInt();
let p0 = this.Constructor.modulus.toBigInt();
let q = xy / p0;
let r = xy - q * p0;
return {
q: _a.fromBigInt(q),
r: _a.fromBigInt(r),
};
});
let delta = Array.from({ length: 2 * this.Constructor.config.limbNum - 1 }, () => new Field(0));
let [X, Y, Q, R, P] = [
this.fields,
a.fields,
q.fields,
r.fields,
this.Constructor.modulus.fields,
];
for (let i = 0; i < this.Constructor.config.limbNum; i++) {
if (isSquare) {
for (let j = 0; j < i; j++) {
delta[i + j] = delta[i + j].add(X[i].mul(X[j]).mul(2n));
}
delta[2 * i] = delta[2 * i].add(X[i].mul(X[i]));
}
else {
for (let j = 0; j < this.Constructor.config.limbNum; j++) {
delta[i + j] = delta[i + j].add(X[i].mul(Y[j]));
}
}
for (let j = 0; j < this.Constructor.config.limbNum; j++) {
delta[i + j] = delta[i + j].sub(Q[i].mul(P[j]));
}
delta[i] = delta[i].sub(R[i]).seal();
}
let carry = new Field(0);
for (let i = 0; i < 2 * this.Constructor.config.limbNum - 2; i++) {
let deltaPlusCarry = delta[i].add(carry).seal();
carry = Provable.witness(Field, () => deltaPlusCarry.div(1n << BigInt(this.Constructor.config.limbSize)));
rangeCheck(carry, 128, true);
deltaPlusCarry.assertEquals(carry.mul(1n << BigInt(this.Constructor.config.limbSize)));
}
delta[2 * this.Constructor.config.limbNum - 2].add(carry).assertEquals(0n);
return r;
}
/**
* Computes the square root of a ProvableBigInt
* Cost: Cheap
* @returns The square root as a ProvableBigInt
*/
square() {
return this.mul(this, true);
}
/**
* Divides one ProvableBigInt by another.
* Cost: Cheap-Moderate
* @param a The divisor as a ProvableBigInt
* @returns The quotient as ProvableBigInt
*/
div(a) {
const inv_a = a.inverse();
let res = this.mul(inv_a);
return res;
}
/**
* Computes the modular inverse of a ProvableBigInt
* Cost: Cheap
* @returns The inverse as a ProvableBigInt
*/
inverse() {
let { res } = Provable.witness(provable({ res: _a }), () => {
const p = this.Constructor.modulus.toBigInt();
let t = 0n;
let newT = 1n;
let r = p;
let newR = this.toBigInt();
// Loop until newR is equal to zero
while (newR !== 0n) {
const quotient = r / newR;
[t, newT] = [newT, t - quotient * newT];
[r, newR] = [newR, r - quotient * newR];
}
// If r is bigger than 1, a is not invertible
if (r > 1n) {
throw new Error('a is not invertible');
}
// If t is smaller than zero, add modulus
if (t < 0n) {
t = t + p;
}
return { res: _a.fromBigInt(t) };
});
res.mul(this).assertEquals(_a.one());
return res;
}
/**
* Computes the additive inverse of a ProvableBigInt
* Cost: Cheap
* @returns The additive inverse as a ProvableBigInt
*/
negate() {
let { negation } = Provable.witness(provable({ negation: _a }), () => {
let thisVal = this.toBigInt();
let p = this.Constructor.modulus.toBigInt();
let negVal = 0n;
if (thisVal !== 0n) {
negVal = p - thisVal;
}
return {
negation: _a.fromBigInt(negVal),
};
});
this.add(negation).assertEquals(_a.zero());
return negation;
}
/**
* Computes the power of a ProvableBigInt raised to an exponent
* Cost: Expensive
* @param exp The exponent
* @returns The result as a ProvableBigInt
*/
pow(exp) {
const exponentBits = exp.toBits();
const processChunk = function* (bits, chunkSize) {
for (let i = 0; i < bits.length; i += chunkSize) {
yield bits.slice(i, i + chunkSize);
}
};
let result = _a.one();
let base = this.clone();
for (const chunk of processChunk(exponentBits, 100)) {
for (const bit of chunk) {
result = Provable.if(bit, _a, result.mul(base), result);
base = base.mul(base);
}
}
return result;
}
/**
* Computes the square root of a Provable BigInt
* Cost: Cheap
* @returns The square root as a ProvableBigInt
*/
sqrt() {
let r = Provable.witness(_a, () => {
const p = this.Constructor.modulus.toBigInt();
// Special cases
// Case 1: If input is 0, square root is 0
if (this.toBigInt() === 0n)
return _a.fromBigInt(0n);
// Case 2: If p ≡ 3 (mod 4), we can use a simpler formula
// In this case, the square root is a^((p+1)/4) mod p
if (p % 4n === 3n) {
const pplusonedivfour = (p + 1n) / 4n;
const pplusonedivfour_reduced = pplusonedivfour % p;
const sqrt = modularExponentiation(this.toBigInt(), pplusonedivfour_reduced, p);
return _a.fromBigInt(sqrt);
}
// Tonelli-Shanks Algorithm
// Step 1: Factor out powers of 2 from p-1
// Write p-1 = Q * 2^S where Q is odd
let Q = p - 1n;
let S = 0n;
while (Q % 2n === 0n) {
Q /= 2n;
S += 1n;
}
// Step 2: Find a quadratic non-residue z
// This is any number z where z^((p-1)/2) ≡ -1 (mod p)
let z = 2n;
while (modularExponentiation(z, (p - 1n) / 2n, p) !== p - 1n) {
z += 1n;
}
// Step 3: Initialize main loop variables
let M = S;
let c = modularExponentiation(z, Q, p);
let t = modularExponentiation(this.toBigInt(), Q, p);
let R = modularExponentiation(this.toBigInt(), (Q + 1n) / 2n, p);
// Main loop of Tonelli-Shanks algorithm
while (t !== 0n && t !== 1n) {
// Find least value of i, 0 < i < M, such that t^(2^i) = 1
let t2i = t;
let i = 0n;
for (i = 1n; i < M; i++) {
t2i = t2i ** 2n;
t2i %= p;
if (t2i === 1n)
break;
}
// If no solution found, the input has no square root
if (i === M && M - i - 1n < 0n) {
throw new Error('Tonelli-Shanks algorithm failed to find a solution. Make sure modulo is prime!');
}
// Update variables for next iteration
const b = modularExponentiation(c, 2n ** (M - i - 1n), p);
M = i;
c = b ** 2n % p;
t = (t * c) % p;
R = (R * b) % p;
}
return _a.fromBigInt(R);
});
r.square().assertEquals(this);
return r;
}
/**
* Checks if one ProvableBigInt is greater than another
* Cost: Moderate
* @param a The ProvableBigInt to compare
* @returns A Bool indicating if a is greater than b
*
* TODO: Comparators should ensure than inputs are canonical fields (value < p).
* e.g.
* ```ts
* let delta = x.sub(x); // not guaranteed to be < p, could be = p
* delta.greaterThan(ProvableBigInt.zero()).assertTrue(); // (p > 0) = true, (0 > 0) = false
* ```
*
*/
greaterThan(a) {
return this.fields
.map((field, i) => ({
isGreater: field.greaterThan(a.fields[i]),
isEqual: field.equals(a.fields[i]),
}))
.reduce((result, { isGreater, isEqual }) => isGreater.or(result.and(isEqual)), new Bool(false));
}
/**
* Checks if one ProvableBigInt is greater than or equal to another
* Cost: Moderate
* @param a The ProvableBigInt to compare
* @returns A Bool indicating if a is greater than or equal to b
*
* TODO: @see {@link greaterThan}
*
*/
greaterThanOrEqual(a) {
return this.fields
.map((field, i) => ({
isGreater: field.greaterThan(a.fields[i]),
isEqual: field.equals(a.fields[i]),
}))
.reduce((result, { isGreater, isEqual }) => isGreater.or(result.and(isEqual)), new Bool(false))
.or(this.equals(a));
}
/**
* Checks if one ProvableBigInt is less than another
* Cost: Moderate
* @param a The ProvableBigInt to compare
* @returns A Bool indicating if a is less than b
*
* TODO: @see {@link greaterThan}
*
*/
lessThan(a) {
return this.fields
.map((field, i) => ({
isLess: field.lessThan(a.fields[i]),
isEqual: field.equals(a.fields[i]),
}))
.reduce((result, { isLess, isEqual }) => isLess.or(result.and(isEqual)), new Bool(false));
}
/**
* Checks if one ProvableBigInt is less than or equal to another
* Cost: Moderate
* @param a The ProvableBigInt to compare
* @returns A Bool indicating if a is less than or equal to b
*
* TODO: @see {@link greaterThan}
*/
lessThanOrEqual(a) {
return this.fields
.map((field, i) => ({
isLess: field.lessThan(a.fields[i]),
isEqual: field.equals(a.fields[i]),
}))
.reduce((result, { isLess, isEqual }) => isLess.or(result.and(isEqual)), new Bool(false))
.or(this.equals(a));
}
/**
* Checks if one ProvableBigInt is equal to another
* Cost: Cheap
* @param a The ProvableBigInt to compare
* @returns A Bool indicating if a is equal to b
*
* TODO: @see {@link greaterThan}
*/
equals(a) {
return this.fields
.map((field, i) => field.equals(a.fields[i]))
.reduce((result, isEqual) => result.and(isEqual), new Bool(true));
}
/**
* Checks if one ProvableBigInt is less than or equal to another
* Cost: Cheap
* @param a The ProvableBigInt to compare
* @returns A Bool indicating if a is less than or equal to b
*
* TODO: @see {@link greaterThan}
*/
assertEquals(a) {
this.equals(a).assertTrue('ProvableBigInts are not equal');
}
}
_a = ProvableBigInt_;
(() => {
_a._modulus = new _a(fields, Unconstrained.from(modulus));
_a._config = config_;
_a._provable = provableFromClass(_a, {
fields: Provable.Array(Field, config_.limbNum),
});
})();
ProvableBigInt_.Unsafe = {
/**
* Creates a ProvableBigInt instance from an limbs as an array of fields
* **WARNING**: This method is UNSAFE and lacks checks on the fields. Use with caution as it can lead to incorrect results or security vulnerabilities.
* @param fields The limbs of the ProvableBigInt. Must be of the correct length.
* @returns A ProvableBigInt instance from the fields
*/
fromFields(fields) {
let value = 0n;
for (let i = 0; i < _a.config.limbNum; i++) {
value |= BigInt(fields[i].toBigInt()) << BigInt(_a.config.limbSize * i);
}
return new _a(fields, Unconstrained.from(value));
},
/**
* Creates a ProvableBigInt instance from an array of bits
* **WARNING**: This method is UNSAFE and lacks checks on the bits. Use with caution as it can lead to incorrect results or security vulnerabilities.
* @param bits
* @returns A ProvableBigInt instance from the bits
*/
fromBits(bits) {
let value = 0n;
let bigint = Provable.witness(_a, () => {
for (let i = 0; i < bits.length; i++) {
if (bits[i].toBoolean()) {
value |= 1n << BigInt(i);
}
}
return _a.fromBigInt(value);
});
return bigint;
},
};
return ProvableBigInt_;
}
class ProvableBigInt {
static get provable() {
assert(this._provable !== undefined, 'ProvableBigInt not initialized');
return this._provable;
}
static get modulus() {
assert(this._modulus !== undefined, 'Modulus not initialized');
return this._modulus;
}
static get config() {
assert(this._config !== undefined, 'Config not initialized');
return this._config;
}
constructor(fields, value) {
this.fields = fields;
this.value = Unconstrained.from(value);
}
}
function rangeCheck(x, bits, signed) {
const supportedBits = new Set([32, 48, 64, 116, 128]);
if (!supportedBits.has(bits)) {
throw new Error(`Unsupported bit size: ${bits}`);
}
switch (bits) {
case 32:
Gadgets.rangeCheck32(x);
break;
case 48:
rangeCheck48(x);
break;
case 64:
Gadgets.rangeCheck64(x);
break;
case 116:
rangeCheck116(x);
break;
case 128:
if (signed) {
rangeCheck128Signed(x);
}
else {
throw new Error('128-bit unsigned range check not implemented');
}
break;
}
}
function rangeCheck48(x) {
let [x0, x1] = Provable.witnessFields(2, () => [
x.toBigInt() & ((1n << 32n) - 1n),
x.toBigInt() >> 32n,
]);
Gadgets.rangeCheck32(x0); // 32 bits
Gadgets.rangeCheck16(x1); // 16 bits
x0.add(x1.mul(1n << 32n)).assertEquals(x); // 48 bits
}
function rangeCheck116(x) {
let [x0, x1] = Provable.witnessFields(2, () => [
x.toBigInt() & ((1n << 64n) - 1n),
x.toBigInt() >> 64n,
]);
Gadgets.rangeCheck64(x0); // 64 bits
let [x52] = Gadgets.rangeCheck64(x1);
x52.assertEquals(0n); // 52 bits
x0.add(x1.mul(1n << 64n)).assertEquals(x);
}
function rangeCheck128Signed(xSigned) {
let x = xSigned.add(1n << 127n);
let [x0, x1] = Provable.witnessFields(2, () => {
const x0 = x.toBigInt() & ((1n << 64n) - 1n);
const x1 = x.toBigInt() >> 64n;
return [x0, x1];
});
Gadgets.rangeCheck64(x0);
Gadgets.rangeCheck64(x1);
x0.add(x1.mul(1n << 64n)).assertEquals(x);
}
function findConfig(modulus) {
const bitLength = modulus.toString(2).length;
const defaultLimbSize = 116;
const limbCount = Math.ceil(bitLength / defaultLimbSize);
const maxBitLength = limbCount * defaultLimbSize;
return {
limbNum: limbCount,
limbSize: defaultLimbSize,
mask: (1n << BigInt(defaultLimbSize)) - 1n,
max: (1n << BigInt(maxBitLength)) - 1n,
};
}
function modularExponentiation(base, exponent, modulus) {
let result = 1n;
base = base % modulus;
while (exponent > 0n) {
if (exponent % 2n === 1n) {
result = (result * base) % modulus;
}
exponent = exponent >> 1n;
base = (base * base) % modulus;
}
return result;
}
/**
* Miller-Rabin primality test
* @param n The number to test for primality
* @param k Number of iterations (higher = more accurate)
* @returns true if n is probably prime, false if n is definitely composite
*/
function isPrime(n, k = 10) {
if (n === 2n || n === 3n)
return true;
if (n < 2n)
return false;
if (n % 2n === 0n)
return false;
// write n - 1 = 2^r * d, d is odd
let d = n - 1n;
let r = 0n;
for (; d % 2n === 0n; d /= 2n, r++)
;
WitnessLoop: for (let i = 0; i < k; i++) {
let a = randomBigintInRange(2n, n - 2n);
let x = modularExponentiation(a, d, n);
if (x === 1n || x === n - 1n)
continue;
for (let j = 0; j + 1 < r; j++) {
x = (x * x) % n;
if (x === 1n)
return false;
if (x === n - 1n)
continue WitnessLoop;
}
return false;
}
return true;
}
/**
* Generates a random bigint in the range [min, max)
* @param min The minimum value (inclusive)
* @param max The maximum value (exclusive)
* @returns A random bigint in the range [min, max)
*/
function randomBigintInRange(min, max) {
const range = max - min;
const length = Math.ceil(range.toString(2).length / 8);
while (true) {
// Generate random bytes
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes);
// Convert bytes to bigint
let x = BigInt(0);
for (const byte of bytes) {
x = (x << 8n) + BigInt(byte);
}
// Check if x is within the range
if (x < range) {
return min + x;
}
}
}
function bigintToLimbs(x, config) {
let fields = [];
for (let i = 0; i < config.limbNum; i++) {
fields.push(Field.from(x & config.mask)); // fields[i] = x & mask
x >>= BigInt(config.limbSize); // x = x >> limbSize
}
return fields;
}
//# sourceMappingURL=bigint.js.map