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

Move low-level operations to JS #967

Merged
merged 24 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased](https://github.com/o1-labs/snarkyjs/compare/3fbd9678e...HEAD)

> no unreleased changes yet
### Breaking changes

- Method `equals` and `assertEquals` on `Group` now generate a different set of constraints. This breaks deployed contracts, because the circuit changed. https://github.com/o1-labs/snarkyjs/pull/967

## [0.11.0](https://github.com/o1-labs/snarkyjs/compare/a632313a...3fbd9678e)

Expand Down
2 changes: 1 addition & 1 deletion src/bindings
141 changes: 123 additions & 18 deletions src/lib/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { Field, FieldVar, isField } from './field.js';
import { Bool, Snarky } from '../snarky.js';
import { Field as Fp } from '../provable/field-bigint.js';
import { Pallas } from '../bindings/crypto/elliptic_curve.js';
import { Provable } from './provable.js';

export { Group };

const checkFinite = true;

/**
* An element of a Group.
*/
Expand All @@ -20,6 +23,25 @@ class Group {
return new Group({ x: Pallas.one.x, y: Pallas.one.y });
}

/**
* Unique representation of the `zero` element of the Group (the identity element of addition in this Group).
*
* **Note**: The `zero` element is represented as `(1, 1)`.
*
* ```typescript
* // g + -g = 0
* g.add(g.neg()).assertEquals(zero);
* // g + 0 = g
* g.add(zero).assertEquals(g);
* ```
*/
static get zero() {
return new Group({
x: 1,
y: 1,
});
}

/**
* Coerces anything group-like to a {@link Group}.
*/
Expand All @@ -34,6 +56,8 @@ class Group {
this.y = isField(y) ? y : new Field(y);

if (this.#isConstant()) {
// we also check the zero element (1, 1) here
if (this.x.equals(1).and(this.y.equals(1)).toBoolean()) return;
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
const { add, mul, square } = Fp;

let x_bigint = this.x.toBigInt();
Expand Down Expand Up @@ -76,6 +100,10 @@ class Group {
});
}

#isZero() {
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
return this.equals(Group.zero);
}

/**
* Adds this {@link Group} element to another {@link Group} element.
*
Expand All @@ -86,33 +114,106 @@ class Group {
*/
add(g: Group) {
if (this.#isConstant() && g.#isConstant()) {
if (this.x.toBigInt() === 0n) {
// we additionally check if g + 0 = g, because adding zero to g just results in g (and vise versa)
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
if (this.x.toBigInt() === 0n || this.#isZero().toBoolean()) {
return g;
} else if (g.x.toBigInt() === 0n) {
} else if (g.x.toBigInt() === 0n || g.#isZero().toBoolean()) {
return this;
} else {
let g_proj = Pallas.add(this.#toProjective(), g.#toProjective());
return Group.#fromProjective(g_proj);
}
} else {
let [, x, y] = Snarky.group.add(this.#toTuple(), g.#toTuple());
return new Group({ x, y });
const { x: x1, y: y1 } = this;
const { x: x2, y: y2 } = g;

let zero = new Field(0);

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

let inf = checkFinite
? zero
: Provable.witness(Field, () =>
x1.equals(x2).and(y1.equals(y2).not()).toField()
);
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved

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.group.ecadd(
Group.from(x1.seal(), y1.seal()).#toTuple(),
Group.from(x2.seal(), y2.seal()).#toTuple(),
Group.from(x3, y3).#toTuple(),
inf.value,
same_x.value,
s.value,
inf_z.value,
x21_inv.value
);
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved

// similarly to the constant implementation, just that I couldn't figure out a more efficient way to zero for addition with 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 thisIsZero = this.#isZero();

// if either one is the negation of the other, we just return the zero element since g + (-g) = 0 - but the OCaml implementation doesn't pick that up
let isNegation = g.neg().equals(this);
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved

let isNewElement = gIsZero.or(thisIsZero).not().and(isNegation.not());

return Provable.switch(
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
[gIsZero, thisIsZero, isNewElement, isNegation],
Group,
[this, g, new Group({ x, y }), Group.zero]
);
}
}

/**
* Subtracts another {@link Group} element from this one.
*/
sub(y: Group) {
return this.add(y.neg());
sub(g: Group) {
let gIsZero = g.#isZero();
let thisIsZero = this.#isZero();

// similar to add -- we check if g or this is zero, so g - 0 = g
return Provable.switch(
[gIsZero, thisIsZero, gIsZero.or(thisIsZero).not()],
Group,
[this, g, this.add(g.neg())]
);
}

/**
* Negates this {@link Group}. Under the hood, it simply negates the `y` coordinate and leaves the `x` coordinate as is.
*/
neg() {
let { x, y } = this;
return new Group({ x, y: y.neg() });
// negation of zero is zero
return Provable.if(this.#isZero(), this, new Group({ x, y: y.neg() }));
}

/**
Expand All @@ -138,7 +239,9 @@ class Group {
0,
...fields.map((f) => f.value).reverse(),
]);
return new Group({ x, y });

// s*0 = 0 - can't scale zero
return Provable.if(this.#isZero(), this, new Group({ x, y }));
}
}

Expand All @@ -155,7 +258,7 @@ class Group {
let { x: x2, y: y2 } = g;

x1.assertEquals(x2, message);
// y1.assertEquals(y2, message); need to enable this later on, but it breaks constraint backwards compatibility
y1.assertEquals(y2, message);
}

/**
Expand All @@ -167,15 +270,10 @@ class Group {
* ```
*/
equals(g: Group) {
if (this.#isConstant() && g.#isConstant()) {
let { x: x1, y: y1 } = this;
let { x: x2, y: y2 } = g;
let { x: x1, y: y1 } = this;
let { x: x2, y: y2 } = g;

return Bool(x1.equals(x2).and(y1.equals(y2)));
} else {
let z = Snarky.group.equals(this.#toTuple(), g.#toTuple());
return Bool.Unsafe.ofField(new Field(z));
}
return x1.equals(x2).and(y1.equals(y2));
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -350,7 +448,14 @@ class Group {
*/
static check(g: Group) {
try {
Snarky.group.assertOnCurve(g.#toTuple());
const { x, y } = g;

let x2 = x.square();
let x3 = x2.mul(x);
let ax = x.mul(Pallas.a); // this will obviously be 0, but just for the sake of correctness

// we also check the zero element (1, 1) here
g.#isZero().or(x3.add(ax).add(Pallas.b).equals(y.square())).assertTrue();
} catch (error) {
if (!(error instanceof Error)) return error;
throw `${`Element (x: ${g.x}, y: ${g.y}) is not an element of the group.`}\n${
Expand Down
31 changes: 14 additions & 17 deletions src/snarky.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type MlTuple<X, Y> = [0, X, Y];
type MlArray<T> = [0, ...T[]];
type MlList<T> = [0, T, 0 | MlList<T>];
type MlOption<T> = 0 | [0, T];
type MlGroup = MlTuple<FieldVar, FieldVar>;

declare namespace Snarky {
type Keypair = unknown;
Expand Down Expand Up @@ -178,24 +179,20 @@ declare const Snarky: {

group: {
/**
* Addition of two group elements, handles only variables.
* Low-level Elliptic Curve Addition gate.
*/
add(
p1: MlTuple<FieldVar, FieldVar>,
p2: MlTuple<FieldVar, FieldVar>
): MlTuple<FieldVar, FieldVar>;

assertOnCurve(p1: MlTuple<FieldVar, FieldVar>): void;

scale(
p: MlTuple<FieldVar, FieldVar>,
s: MlArray<BoolVar>
): MlTuple<FieldVar, FieldVar>;

equals(
p1: MlTuple<FieldVar, FieldVar>,
p2: MlTuple<FieldVar, FieldVar>
): BoolVar;
ecadd(
p1: MlGroup,
p2: MlGroup,
p3: MlGroup,
inf: FieldVar,
same_x: FieldVar,
slope: FieldVar,
inf_z: FieldVar,
x21_inv: FieldVar
): MlGroup;

scale(p: MlGroup, s: MlArray<BoolVar>): MlGroup;
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
};

/**
Expand Down