Skip to content

Commit

Permalink
first attempt at efficient scale gadget
Browse files Browse the repository at this point in the history
  • Loading branch information
mitschabaude committed Apr 1, 2024
1 parent 5420cb2 commit b8c19b9
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 2 deletions.
39 changes: 37 additions & 2 deletions src/lib/provable/gadgets/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@ import { Tuple } from '../../util/types.js';
import type { Bool } from '../bool.js';
import { fieldVar } from '../gates.js';
import { existsOne } from '../core/exists.js';
import { createField } from '../core/field-constructor.js';
import { createField, isBool } from '../core/field-constructor.js';

export { toVars, toVar, isVar, assert, bitSlice, divideWithRemainder };
export {
toVars,
toVar,
isVar,
assert,
bitSlice,
bit,
divideWithRemainder,
packBits,
isConstant,
};

/**
* Given a Field, collapse its AST to a pure Var. See {@link FieldVar}.
Expand Down Expand Up @@ -56,8 +66,33 @@ function bitSlice(x: bigint, start: number, length: number) {
return (x >> BigInt(start)) & ((1n << BigInt(length)) - 1n);
}

function bit(x: bigint, i: number) {
return (x >> BigInt(i)) & 1n;
}

function divideWithRemainder(numerator: bigint, denominator: bigint) {
const quotient = numerator / denominator;
const remainder = numerator - denominator * quotient;
return { quotient, remainder };
}

// pack bools into a single field element

/**
* Helper function to pack bits into a single field element.
* Just returns the sum without any boolean checks.
*/
function packBits(bits: (Field | Bool)[]): Field {
let n = bits.length;
let sum = createField(0n);
for (let i = 0; i < n; i++) {
let bit = bits[i];
if (isBool(bit)) bit = bit.toField();
sum = sum.add(bit.mul(1n << BigInt(i)));
}
return sum.seal();
}

function isConstant(...args: (Field | Bool)[]): boolean {
return args.every((x) => x.isConstant());
}
6 changes: 6 additions & 0 deletions src/lib/provable/gadgets/comparison.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,27 @@ import { Field3, ForeignField } from './foreign-field.js';
import { l, l2, multiRangeCheck } from './range-check.js';
import { witness } from '../types/witness.js';

// external API
export {
// generic comparison gadgets for inputs in a narrower range < p/2
assertLessThanGeneric,
assertLessThanOrEqualGeneric,
lessThanGeneric,
lessThanOrEqualGeneric,

// comparison gadgets for full range inputs
assertLessThanFull,
assertLessThanOrEqualFull,
lessThanFull,
lessThanOrEqualFull,

// legacy, unused
compareCompatible,
};

// internal API
export { fieldToField3 };

/**
* Prove x <= y assuming 0 <= x, y < c.
* The upper bound c must satisfy 2c <= p, where p is the field order.
Expand Down
114 changes: 114 additions & 0 deletions src/lib/provable/gadgets/scalar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { Field } from '../field.js';
import type { Bool } from '../bool.js';
import { Fq } from '../../../bindings/crypto/finite-field.js';
import { PallasAffine } from '../../../bindings/crypto/elliptic-curve.js';
import { fieldToField3 } from './comparison.js';
import { Field3, ForeignField } from './foreign-field.js';
import { exists, existsOne } from '../core/exists.js';
import { bit, isConstant, packBits } from './common.js';
import { TupleN } from '../../util/types.js';
import { l } from './range-check.js';
import { createField } from '../core/field-constructor.js';
import { Snarky } from '../../../snarky.js';
import { Provable } from '../provable.js';
import { Group } from '../group.js';

export { scale, scaleShiftedSplit5 };

/**
* Gadget to scale a point by a scalar, where the scalar is represented as a _native_ Field.
*/
function scale(P: { x: Field; y: Field }, s: Field): Group {
// constant case
let { x, y } = P;
if (x.isConstant() && y.isConstant() && s.isConstant()) {
let sP = PallasAffine.scale(
PallasAffine.fromNonzero({ x: x.toBigInt(), y: y.toBigInt() }),
s.toBigInt()
);
return new Group({ x: createField(sP.x), y: createField(sP.y) });
}

// compute t = s - 2^254 mod q using foreign field subtraction
let sBig = fieldToField3(s);
let twoTo254 = Field3.from(1n << 254n);
let [t0, t1, t2] = ForeignField.sub(sBig, twoTo254, Fq.modulus);

// split t into 250 high bits and 5 low bits
// => split t0 into [5, 83]
let tLo = exists(5, () => {
let t = t0.toBigInt();
return [bit(t, 0), bit(t, 1), bit(t, 2), bit(t, 3), bit(t, 4)];
});
let tLoBools = TupleN.map(tLo, (x) => x.assertBool());
let tHi0 = existsOne(() => t0.toBigInt() >> 5n);

// prove split
// since we know that t0 < 2^88, this proves that t0High < 2^83
packBits(tLo)
.add(tHi0.mul(1n << 5n))
.assertEquals(t0);

// pack tHi
let tHi = tHi0
.add(t1.mul(1n << (l - 5n)))
.add(t2.mul(1n << (2n * l - 5n)))
.seal();

// return (t + 2^254)*P = (s - 2^254 + 2^254)*P = s*P
return scaleShiftedSplit5(P, tHi, tLoBools);
}

/**
* Internal helper to compute `(t + 2^254)*P`.
* `t` is expected to be split into 250 high bits (t >> 5) and 5 low bits (t & 0xf1).
*
* The gadget proves that `tHi` is in [0, 2^250) but assumes that `tLo` consists of bits.
*/
function scaleShiftedSplit5(
{ x, y }: { x: Field; y: Field },
tHi: Field,
tLo: TupleN<Bool, 5>
): Group {
// constant case
if (isConstant(x, y, tHi, ...tLo)) {
let sP = PallasAffine.scale(
PallasAffine.fromNonzero({ x: x.toBigInt(), y: y.toBigInt() }),
Fq.add(packBits(tLo).toBigInt() + (tHi.toBigInt() << 5n), 1n << 254n)
);
return new Group({ x: createField(sP.x), y: createField(sP.y) });
}

// R = (2*(t >> 5) + 1 + 2^250)P
let [, RMl] = Snarky.group.scaleFastUnpack(
[0, x.value, y.value],
[0, tHi.value],
250
);
let P = new Group({ x, y });
let R = new Group({ x: RMl[0], y: RMl[1] });
let [t0, t1, t2, t3, t4] = tLo;

// TODO: use faster group ops which don't allow zero inputs

// R = t4 ? R : R - P = ((t >> 4) + 2^250)P
R = Provable.if(t4, R, R.sub(P));

// R = ((t >> 3) + 2^251)P
R = R.add(R);
R = Provable.if(t3, R.add(P), R);

// R = ((t >> 2) + 2^252)P
R = R.add(R);
R = Provable.if(t2, R.add(P), R);

// R = ((t >> 1) + 2^253)P
R = R.add(R);
R = Provable.if(t1, R.add(P), R);

// R = (t + 2^254)P
R = R.add(R);
R = Provable.if(t0, R.add(P), R);

return R;
}

0 comments on commit b8c19b9

Please sign in to comment.