-
Notifications
You must be signed in to change notification settings - Fork 107
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 Group to JavaScript #932
Conversation
@@ -0,0 +1,66 @@ | |||
import { Field, Group, Poseidon, Provable, Scalar } from 'snarkyjs'; | |||
|
|||
function mock(obj: { [K: string]: (...args: any) => void }, name: string) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We didn't really have any regression tests that tested Group
and its constraints, I just hacked this mock contract-like thing together to integrate it into the contract regression tests
}; | ||
} | ||
|
||
const GroupMock = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this one adds some "tests" for Group constraints
|
||
// usage ./run ./src/examples/vk_regression.ts --bundle --dump ./src/examples/regression_test.json | ||
let dump = process.argv[4] === '--dump'; | ||
let jsonPath = process.argv[dump ? 5 : 4]; | ||
|
||
const Contracts: (typeof SmartContract)[] = [ | ||
type MinimumConstraintSystem = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some minor changes to accommodate the Group constraint tests, see above
* Coerces anything group-like to a {@link Group}. | ||
*/ | ||
constructor(g: { | ||
x: FieldVar | Field | number | string | bigint; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the overloaded constructor so that the new implementation is equivalent to the old OCaml one, which allowed both (x, y)
as well as {x, y}
as arguments. I am not using the FieldLike
or GroupLike
type from above so users arent confused by it and see "the actual type".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this a spot where you can use satisfies
to assert it's the same as FieldLike
? Would add nice info to the vscode hints too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so (or at least I am unsure?) plus I actually just pushed a change that doesnt overload the constructor since that didn't work out with the toFunctionConstructor
we use now so we dont need that anymore
src/lib/group.ts
Outdated
|
||
export { Group }; | ||
|
||
type FieldLike = FieldVar | Field | number | string | bigint; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just to make the internal code a bit more readable
src/lib/group.ts
Outdated
let equal_y = y1.equals(y2); | ||
return equal_x.and(equal_y);*/ | ||
|
||
let z = Snarky.group.equals(Group.#toTuple(this), Group.#toTuple(g)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am still trying to figure out why this isn't producing the same set of constraints:
I tried implementing it with x1.equals(x2).and(y1.equals(y2))
- which is (at least to my knowledge) equaivalent to the OCaml implementation of
Boolean.all [ Impl.Field.equal x1 x2; Impl.Field.equal y1 y2 ]
where Boolean.all
just calls b1 && b2
for tuples. it produces the same amount of rows/gates but with slightly different coefficients?
src/lib/group.ts
Outdated
return Group.#fromProjective(g_proj); | ||
} | ||
} else { | ||
let [, x, y] = Snarky.group.add(Group.#toTuple(this), Group.#toTuple(g)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to move this entirely to JavaScript, but wanted to establish a baseline first. I will stack another PR with the detailed implementation on top of this one soon
src/lib/group.ts
Outdated
let g_proj = Pallas.scale(this.#toAffine(), BigInt(scalar.toJSON())); | ||
return Group.#fromProjective(g_proj); | ||
} else { | ||
let [, x, y] = Snarky.group.scale(Group.#toTuple(this), [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
similar to the comment above
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The OCaml implementation only checked if the x
coordinates are equal, allowing devs to prove g1 === g2
even if g1.y !== g2.y
src/lib/group.ts
Outdated
) { | ||
let x, y: FieldLike; | ||
|
||
if (isGroupLike(arg1)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i feel like this is a bit hacky, if anyone has recommendations let me know :D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome work, I like how this keeps the OCaml API slim.
A few comments, most notably regarding consistent treatment of the zero
Group element. I haven't looked up myself how the in-snark OCaml methods treat zero, so I'll leave it to you to identify a consistent way of doing it (and ideally document it)
} else { | ||
let [, x, y] = Snarky.group.scale(this.#toTuple(), [ | ||
0, | ||
...fields.map((f) => f.value).reverse(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
while I appreciate keeping the Snarky
interface low-level and having logic like reversing the scalar bits on the JS side, it would be good if a doccomment on Snarky.group.scale
documented this unexpected behaviour
/** | ||
* Adds a {@link Group} element to another one. | ||
*/ | ||
static add(g1: Group, g2: Group) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO all these extra static methods are weird and they should either be deprecated or be present consistently on all classes, i.e. added to Field
for example
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am also not a massive fan, I would rather deprecate them
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can always recreate them with bind
anyway -- JS devs are used to this; so I agree we should deprecate
note: this PR has some conflicts with the Scalar move. I prepared a branch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This great!
There are a lot of subtle security gotchas in this sort of code -- as a side-effect of moving more of our logic to TypeScript, it becomes more likely that auditors will be productive reviewing this kind of code. Maybe we should be making a queue of std library code that we should get third-party audited and figuring out when to prioritize it.
@@ -1,22 +1,39 @@ | |||
import fs from 'fs'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add this to CI? We can remove it later when we make breaking changes to the backwards compatibility on purpose
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it already is running in the CI! The PR gets blocked if a constrained mismatch is detected
}).not.toThrow(); | ||
}); | ||
|
||
it('((2^64/2)+(2^64/2)) does not throw', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why did you remove these other test cases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The values we used to test weren't valid Group elements (OCaml implementation never checked that, except for variables), and since I added a check that makes sure we only accept valid elements, the old tests started to fail - I added new ones
* Coerces anything group-like to a {@link Group}. | ||
*/ | ||
constructor(g: { | ||
x: FieldVar | Field | number | string | bigint; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this a spot where you can use satisfies
to assert it's the same as FieldLike
? Would add nice info to the vscode hints too
let onCurve = | ||
add(mul(x_bigint, mul(x_bigint, x_bigint)), Pallas.b) === | ||
square(y_bigint); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should support zero and pay the constraint costs if we have to pick one -- we can make a TODO to make some way to create a more efficient Group later.
let onCurve = | ||
add(mul(x_bigint, mul(x_bigint, x_bigint)), Pallas.b) === | ||
square(y_bigint); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
safe and unsafe is a great idea 👍 , but yeah can come after
This PR aims to move the Group implementation from OCaml to JavaScript, reducing OCaml dependencies as best as possible. The new Group implementation covers the same API surface as the old one and produces the exact same constraints (important for compatibility with already existent projects)
This will allow us to easier maintain more of the SnarkyJS elements by relying less on OCaml as well as giving us more control over what we are doing. Additionally, in the future this will enable a
snarkyjs/light
version by relying on native JS arithmetic instead of requiring users to load a lot of bindings for simple calculations -- builds on top of #902 and is motivated by the same reasonso1-labs/o1js-bindings#25
closes #924