Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Efficient scalar mul and other Scalar improvements #1530

Merged
merged 48 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
5008583
simplify internal repr of scalar
mitschabaude Apr 1, 2024
5420cb2
expose scale_fast_unpack
mitschabaude Apr 1, 2024
b8c19b9
first attempt at efficient scale gadget
mitschabaude Apr 1, 2024
a00a15a
wip debugging
mitschabaude Apr 1, 2024
57de1f4
fix accessing ml curve point
mitschabaude Apr 3, 2024
2eb6354
improve efficiency by using incomplete additions
mitschabaude Apr 3, 2024
d659493
rename gadget file
mitschabaude Apr 3, 2024
43a0637
refactor 3 statements to loop
mitschabaude Apr 3, 2024
dcc58ee
move addition gadget to gadget file
mitschabaude Apr 3, 2024
2192f29
remove cyclic dependency on group
mitschabaude Apr 3, 2024
bb0716c
simplify witness generation code
mitschabaude Apr 3, 2024
ddbec87
change scalar type to shifted 5 / 250 representation
mitschabaude Apr 3, 2024
011d6a8
bring back fromBits, remove unused shifting methods
mitschabaude Apr 3, 2024
bc9b4ae
add scale from field element to test
mitschabaude Apr 3, 2024
b508437
bindings
mitschabaude Apr 3, 2024
0f93cc2
dump vks
mitschabaude Apr 3, 2024
04f8648
switch to repr with 1 low bit
mitschabaude Apr 4, 2024
9f11f0c
but scaling with 0 or 1 doesn't work now
mitschabaude Apr 4, 2024
32fd2b3
0 doesn't work anyway
mitschabaude Apr 4, 2024
7ce21a7
vk regression
mitschabaude Apr 4, 2024
5bc9543
fix
mitschabaude Apr 4, 2024
4c06ce1
Revert "switch to repr with 1 low bit"
mitschabaude Apr 5, 2024
6886184
add missing range checks, document more assumptions, add comments
mitschabaude Apr 5, 2024
8fbfe9b
remove scalar limitations from group unit test
mitschabaude Apr 5, 2024
5107a95
dump vks
mitschabaude Apr 5, 2024
d8fb076
move isOdd gadget to reuse it
mitschabaude Apr 5, 2024
51ee9f5
dedicated gadget of 130 rows for scaling by Field
mitschabaude Apr 5, 2024
f88222d
use 1, 254 split and handle edge cases
mitschabaude Apr 8, 2024
00680ed
reduce constraints
mitschabaude Apr 8, 2024
0aad84e
tighten fromBits gadget
mitschabaude Apr 8, 2024
37534df
remove unused split5 gadgets
mitschabaude Apr 8, 2024
ad7e7a3
dump vks
mitschabaude Apr 8, 2024
8d5d338
renaming and test utils
mitschabaude Apr 8, 2024
494e627
test utils
mitschabaude Apr 8, 2024
a8fea23
minor
mitschabaude Apr 8, 2024
fddead2
Merge branch 'main' into feature/no-shifted-scale
mitschabaude Apr 8, 2024
35eb475
changelog
mitschabaude Apr 8, 2024
7bcc84b
remove shifting stuff from foreign field unit test
mitschabaude Apr 8, 2024
e21fa33
comment
mitschabaude Apr 8, 2024
9c799ed
add nullifier to vk-regression
mitschabaude Apr 8, 2024
9e9ca06
mina
mitschabaude Apr 8, 2024
4b4787e
Merge branch 'main' into feature/no-shifted-scale
mitschabaude Apr 10, 2024
224b95c
fix changelog
mitschabaude Apr 10, 2024
dab6eb3
dump vks
mitschabaude Apr 10, 2024
0877e26
Merge branch 'main' into feature/no-shifted-scale
mitschabaude Apr 15, 2024
523330a
fixes and tweaks to scaling gadgets
mitschabaude Apr 16, 2024
45837fc
remove redundant constraint
mitschabaude Apr 16, 2024
9822654
dump vks
mitschabaude Apr 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions src/lib/provable/gadgets/scalar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function scale(P: { x: Field; y: Field }, s: Field): Group {

/**
* 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).
* `t` is expected to be split into 250 high bits (t >> 5) and 5 low bits (t & 0x1f).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah okay, that's now 5 bits

*
* The gadget proves that `tHi` is in [0, 2^250) but assumes that `tLo` consists of bits.
*/
Expand Down Expand Up @@ -89,26 +89,25 @@ function scaleShiftedSplit5(
let R = new Group({ x: RMl[1], y: RMl[2] });
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 = Provable.if(t4, R, R.addNonZero(P.neg()));

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

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

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

// R = (t + 2^254)P
R = R.add(R);
R = Provable.if(t0, R.add(P), R);
R = R.addNonZero(R);
// in the final step, we allow a zero output to make it work for the 0 scalar
R = Provable.if(t0, R.addNonZero(P, true), R);

return R;
}
147 changes: 96 additions & 51 deletions src/lib/provable/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ import { FieldVar } from './core/fieldvar.js';
import { Scalar } from './scalar.js';
import { Snarky } from '../../snarky.js';
import { Fp } from '../../bindings/crypto/finite-field.js';
import { GroupAffine, Pallas } from '../../bindings/crypto/elliptic-curve.js';
import {
GroupAffine,
Pallas,
PallasAffine,
} from '../../bindings/crypto/elliptic-curve.js';
import { Provable } from './provable.js';
import { Bool } from './bool.js';
import { assert } from '../util/assert.js';

export { Group };

Expand Down Expand Up @@ -101,70 +106,49 @@ class Group {
return fromProjective(g_proj);
}
} else {
const { x: x1, y: y1 } = this;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code was moved to native-group.ts to reuse there. it was also rewritten a bit and a bug fixed

const { x: x2, y: y2 } = g;

let zero = new Field(0);

let same_x = Provable.witness(Field, () => x1.equals(x2).toField());

let inf = Provable.witness(Bool, () =>
x1.equals(x2).and(y1.equals(y2).not())
);

let inf_z = Provable.witness(Field, () => {
if (y1.equals(y2).toBoolean()) return zero;
else if (x1.equals(x2).toBoolean()) return y2.sub(y1).inv();
else return zero;
});

let x21_inv = Provable.witness(Field, () => {
if (x1.equals(x2).toBoolean()) return zero;
else return x2.sub(x1).inv();
});

let s = Provable.witness(Field, () => {
if (x1.equals(x2).toBoolean()) {
let x1_squared = x1.square();
return x1_squared.add(x1_squared).add(x1_squared).div(y1.add(y1));
} else return y2.sub(y1).div(x2.sub(x1));
});

let x3 = Provable.witness(Field, () => {
return s.square().sub(x1.add(x2));
});

let y3 = Provable.witness(Field, () => {
return s.mul(x1.sub(x3)).sub(y1);
});

let [, x, y] = Snarky.gates.ecAdd(
toTuple(Group.from(x1.seal(), y1.seal())),
toTuple(Group.from(x2.seal(), y2.seal())),
toTuple(Group.from(x3, y3)),
inf.toField().value,
same_x.value,
s.value,
inf_z.value,
x21_inv.value
);

let { result, isInfinity } = addBase(this, g);
// similarly to the constant implementation, we check if either operand is zero
// and the implementation above (original OCaml implementation) returns something wild -> g + 0 != g where it should be g + 0 = g
let gIsZero = g.isZero();
let onlyThisIsZero = this.isZero().and(gIsZero.not());
let isNegation = inf;
let isNegation = isInfinity;
let isNormalAddition = gIsZero.or(onlyThisIsZero).or(isNegation).not();

// note: gIsZero and isNegation are not mutually exclusive, but if both are true, we add 1*0 + 1*0 = 0 which is correct
return Provable.switch(
[gIsZero, onlyThisIsZero, isNegation, isNormalAddition],
Group,
[this, g, Group.zero, new Group({ x, y })]
[this, g, Group.zero, new Group(result)],
{ allowNonExclusive: true }
);
}
}

/**
* Lower-level variant of {@link add} which doesn't handle the case where one of the operands is zero, and
* asserts that the output is non-zero.
*
* Optionally, zero outputs can be allowed by setting `allowZeroOutput` to `true`.
*
* **Warning**: If one of the inputs is zero, the result will be garbage and the proof useless.
* This case has to be prevented or handled separately by the caller of this method.
*/
addNonZero(g2: Group, allowZeroOutput = false): Group {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this, I think it's actually a bit more common to use this than fully complete addition

if (isConstant(this) && isConstant(g2)) {
let { x, y, infinity } = PallasAffine.add(toAffine(this), toAffine(g2));
assert(infinity === false, 'Group.addNonzero(): Result is zero');
return fromAffine({ x, y, infinity });
}
let { result, isInfinity } = addBase(this, g2);

if (allowZeroOutput) {
return Provable.if(isInfinity, Group.zero, new Group(result));
} else {
isInfinity.assertFalse('Group.addNonzero(): Result is zero');
return new Group(result);
}
}

/**
* Subtracts another {@link Group} element from this one.
*/
Expand Down Expand Up @@ -358,6 +342,63 @@ class Group {
}
}

type GroupBase = { x: Field; y: Field };

/**
* Wraps the `EC_add` gate to perform complete addition of two non-zero curve points.
*/
function addBase(g: GroupBase, h: GroupBase) {
const { x: x1, y: y1 } = g;
const { x: x2, y: y2 } = h;

let zero = new Field(0);

let same_x = Provable.witness(Field, () => x1.equals(x2).toField());

let inf = Provable.witness(Bool, () =>
x1.equals(x2).and(y1.equals(y2).not())
);

let inf_z = Provable.witness(Field, () => {
if (y1.equals(y2).toBoolean()) return zero;
else if (x1.equals(x2).toBoolean()) return y2.sub(y1).inv();
else return zero;
});

let x21_inv = Provable.witness(Field, () => {
if (x1.equals(x2).toBoolean()) return zero;
else return x2.sub(x1).inv();
});

let s = Provable.witness(Field, () => {
if (x1.equals(x2).toBoolean()) {
let x1_squared = x1.square();
return x1_squared.add(x1_squared).add(x1_squared).div(y1.add(y1));
} else return y2.sub(y1).div(x2.sub(x1));
});

let x3 = Provable.witness(Field, () => {
return s.square().sub(x1.add(x2));
});

let y3 = Provable.witness(Field, () => {
return s.mul(x1.sub(x3)).sub(y1);
});

Snarky.gates.ecAdd(
toTuple(Group.from(x1.seal(), y1.seal())),
toTuple(Group.from(x2.seal(), y2.seal())),
toTuple(Group.from(x3, y3)),
inf.toField().value,
same_x.value,
s.value,
inf_z.value,
x21_inv.value
);

return { result: { x: x3, y: y3 }, isInfinity: inf };
}

// internal helpers

function isConstant(g: Group) {
Expand All @@ -383,3 +424,7 @@ function fromProjective({ x, y, z }: { x: bigint; y: bigint; z: bigint }) {
function fromAffine({ x, y, infinity }: GroupAffine) {
return infinity ? Group.zero : new Group({ x, y });
}

function toAffine(g: Group): GroupAffine {
return PallasAffine.from({ x: g.x.toBigInt(), y: g.y.toBigInt() });
}
4 changes: 3 additions & 1 deletion src/lib/provable/provable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,8 @@ function ifImplicit<T extends ToFieldable>(condition: Bool, x: T, y: T): T {
function switch_<T, A extends FlexibleProvable<T>>(
mask: Bool[],
type: A,
values: T[]
values: T[],
{ allowNonExclusive = false } = {}
): T {
// picks the value at the index where mask is true
let nValues = values.length;
Expand All @@ -348,6 +349,7 @@ function switch_<T, A extends FlexibleProvable<T>>(
`Provable.switch: \`values\` and \`mask\` have different lengths (${values.length} vs. ${mask.length}), which is not allowed.`
);
let checkMask = () => {
if (allowNonExclusive) return;
let nTrue = mask.filter((b) => b.toBoolean()).length;
if (nTrue > 1) {
throw Error(
Expand Down