o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
306 lines • 10.1 kB
JavaScript
import { Field } from './field.js';
import { Scalar } from './scalar.js';
import { Fp } from '../../bindings/crypto/finite-field.js';
import { Pallas, PallasAffine } from '../../bindings/crypto/elliptic-curve.js';
import { Provable } from './provable.js';
import { assert } from '../util/assert.js';
import { add, scaleField, scaleShifted } from './gadgets/native-curve.js';
export { Group };
/**
* An element of a Group.
*/
class Group {
/**
* The generator `g` of the Group.
*/
static get generator() {
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 `(0, 0)`.
*
* ```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: 0, y: 0 });
}
/**
* Coerces anything group-like to a {@link Group}.
*/
constructor({ x, y, }) {
this.x = x instanceof Field ? x : new Field(x);
this.y = y instanceof Field ? y : new Field(y);
if (isConstant(this)) {
// we also check the zero element (0, 0) here
if (this.x.equals(0).and(this.y.equals(0)).toBoolean())
return;
const { add, mul, square } = Fp;
let x_bigint = this.x.toBigInt();
let y_bigint = this.y.toBigInt();
let onCurve = add(mul(x_bigint, mul(x_bigint, x_bigint)), Pallas.b) === square(y_bigint);
if (!onCurve) {
throw Error(`(x: ${x_bigint}, y: ${y_bigint}) is not a valid group element`);
}
}
}
/**
* Checks if this element is the `zero` element `{x: 0, y: 0}`.
*/
isZero() {
// only the zero element can have x = 0, there are no other (valid) group elements with x = 0
return this.x.equals(0);
}
/**
* Adds this {@link Group} element to another {@link Group} element.
*
* ```ts
* let g1 = Group({ x: -1, y: 2})
* let g2 = g1.add(g1)
* ```
*/
add(g) {
if (isConstant(this) && isConstant(g)) {
// we check if either operand is zero, because adding zero to g just results in g (and vice versa)
if (this.isZero().toBoolean()) {
return g;
}
else if (g.isZero().toBoolean()) {
return this;
}
else {
let g_proj = Pallas.add(toProjective(this), toProjective(g));
return fromProjective(g_proj);
}
}
else {
let { result, isInfinity } = add(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 = 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(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, allowZeroOutput = false) {
if (isConstant(this) && isConstant(g2)) {
let { x, y, infinity } = PallasAffine.add(toAffine(this), toAffine(g2));
assert(!infinity || allowZeroOutput, 'Group.addNonzero(): Result is zero');
return fromAffine({ x, y, infinity });
}
let { result, isInfinity } = add(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.
*/
sub(g) {
return 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() });
}
/**
* Elliptic curve scalar multiplication. Scales the {@link Group} element `n`-times by itself, where `n` is the {@link Scalar}.
*
* ```typescript
* let s = Scalar(5);
* let 5g = g.scale(s);
* ```
*/
scale(s) {
if (s instanceof Field)
return new Group(scaleField(this, s));
let scalar = Scalar.from(s);
if (isConstant(this) && scalar.isConstant()) {
let g_proj = Pallas.scale(toProjective(this), scalar.toBigInt());
return fromProjective(g_proj);
}
else {
let result = scaleShifted(this, scalar);
return new Group(result);
}
}
/**
* Assert that this {@link Group} element equals another {@link Group} element.
* Throws an error if the assertion fails.
*
* ```ts
* g1.assertEquals(g2);
* ```
*/
assertEquals(g, message) {
let { x: x1, y: y1 } = this;
let { x: x2, y: y2 } = g;
x1.assertEquals(x2, message);
y1.assertEquals(y2, message);
}
/**
* Check if this {@link Group} element equals another {@link Group} element.
* Returns a {@link Bool}.
*
* ```ts
* g1.equals(g1); // Bool(true)
* ```
*/
equals(g) {
let { x: x1, y: y1 } = this;
let { x: x2, y: y2 } = g;
return x1.equals(x2).and(y1.equals(y2));
}
static toValue({ x, y }) {
return { x: x.toBigInt(), y: y.toBigInt() };
}
static fromValue(g) {
return new Group(g);
}
/**
* Serializes this {@link Group} element to a JSON object.
*
* This operation does NOT affect the circuit and can't be used to prove anything about the representation of the element.
*/
toJSON() {
return {
x: this.x.toString(),
y: this.y.toString(),
};
}
/**
* Part of the {@link Provable} interface.
*
* Returns an array containing this {@link Group} element as an array of {@link Field} elements.
*/
toFields() {
return [this.x, this.y];
}
/**
* Coerces two x and y coordinates into a {@link Group} element.
*/
static from(x, y) {
return new Group({ x, y });
}
/**
* Part of the {@link Provable} interface.
*
* Returns an array containing a {@link Group} element as an array of {@link Field} elements.
*/
static toFields(g) {
return g.toFields();
}
/**
* Part of the {@link Provable} interface.
*
* Returns an empty array.
*/
static toAuxiliary(g) {
return [];
}
/**
* Part of the {@link Provable} interface.
*
* Deserializes a {@link Group} element from a list of field elements.
*/
static fromFields([x, y]) {
return new Group({ x, y });
}
/**
* Part of the {@link Provable} interface.
*
* Returns 2.
*/
static sizeInFields() {
return 2;
}
/**
* Serializes a {@link Group} element to a JSON object.
*
* This operation does NOT affect the circuit and can't be used to prove anything about the representation of the element.
*/
static toJSON(g) {
return g.toJSON();
}
/**
* Deserializes a JSON-like structure to a {@link Group} element.
*
* This operation does NOT affect the circuit and can't be used to prove anything about the representation of the element.
*/
static fromJSON({ x, y, }) {
return new Group({ x, y });
}
/**
* Checks that a {@link Group} element is constraint properly by checking that the element is on the curve.
*/
static check(g) {
try {
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 (0, 0) here
let isZero = x.equals(0).and(y.equals(0));
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${error.message}`;
}
}
static toInput(x) {
return {
fields: [x.x, x.y],
};
}
static empty() {
return Group.zero;
}
}
// internal helpers
function isConstant(g) {
return g.x.isConstant() && g.y.isConstant();
}
function toProjective(g) {
return Pallas.fromAffine({
x: g.x.toBigInt(),
y: g.y.toBigInt(),
infinity: false,
});
}
function fromProjective({ x, y, z }) {
return fromAffine(Pallas.toAffine({ x, y, z }));
}
function fromAffine({ x, y, infinity }) {
return infinity ? Group.zero : new Group({ x, y });
}
function toAffine(g) {
return PallasAffine.from({ x: g.x.toBigInt(), y: g.y.toBigInt() });
}
//# sourceMappingURL=group.js.map