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 Scalar to JS #935

Merged
merged 30 commits into from
Jun 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0a0f4d9
scalar constructor
mitschabaude May 31, 2023
b3511e5
to fields compressed
mitschabaude May 31, 2023
b305920
constant scalar ops
mitschabaude May 31, 2023
f18dac4
provable scalar
mitschabaude May 31, 2023
925714c
from bits
mitschabaude May 31, 2023
7bc1fbe
fix api example
mitschabaude May 31, 2023
9c13a04
Merge branch 'main' into refactor/scalar
mitschabaude May 31, 2023
66b12fd
remaining methods
mitschabaude May 31, 2023
c4c3b20
move back to ml-equivalent repr for Group.scale compatibility
mitschabaude Jun 1, 2023
a9edabd
fixup scalar representation
mitschabaude Jun 1, 2023
cb497c8
bindings
mitschabaude Jun 1, 2023
80b090b
export new scalar everywhere
mitschabaude Jun 1, 2023
effad74
update tests
mitschabaude Jun 1, 2023
90b7f7e
remove new test
mitschabaude Jun 1, 2023
a286394
remove old scalar
mitschabaude Jun 1, 2023
0d3c3e8
remove old scalar class
mitschabaude Jun 1, 2023
156621c
move private key conversions & base58 to js
mitschabaude Jun 1, 2023
9171ece
add test for consistency with ml function that's no longer used
mitschabaude Jun 1, 2023
c7a6c67
move private key conversion to a saner place
mitschabaude Jun 1, 2023
60fbb5c
bindings
mitschabaude Jun 1, 2023
5baafe4
remove sign fee payer and its deps
mitschabaude Jun 1, 2023
18962c7
doccomments
mitschabaude Jun 1, 2023
6f6a693
changelog
mitschabaude Jun 1, 2023
74c8f7d
bindings
mitschabaude Jun 1, 2023
b5c5767
remove comment
mitschabaude Jun 1, 2023
ea68d95
Merge branch 'refactor/scalar' into refactor/cleanup
mitschabaude Jun 5, 2023
981bcd0
adapt group / scalar interaction
mitschabaude Jun 5, 2023
755f76a
Merge branch 'main' into refactor/scalar
mitschabaude Jun 8, 2023
aa4f06e
bindings
mitschabaude Jun 8, 2023
a5bcb22
warning for private key from bigint
mitschabaude Jun 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Rewrite of `Provable.if()` causes breaking changes to all deployed contracts https://github.com/o1-labs/snarkyjs/pull/889
- Remove all deprecated methods and properties on `Field` https://github.com/o1-labs/snarkyjs/pull/902
- The `Field(x)` constructor and other Field methods no longer accept a `boolean` as input. Instead, you can now pass in a `bigint` to all Field methods. https://github.com/o1-labs/snarkyjs/pull/902
- Remove redundant `signFeePayer()` method https://github.com/o1-labs/snarkyjs/pull/935

### Added

- Add `field.assertNotEquals()` to assert that a field element does not equal some value https://github.com/o1-labs/snarkyjs/pull/902
- More efficient than `field.equals(x).assertFalse()`
- Add `scalar.toConstant()`, `scalar.toBigInt()`, `Scalar.from()`, `privateKey.toBigInt()`, `PrivateKey.fromBigInt()` https://github.com/o1-labs/snarkyjs/pull/935

### Changed

Expand Down
2 changes: 1 addition & 1 deletion src/bindings
7 changes: 2 additions & 5 deletions src/examples/api_exploration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,12 @@ console.assert(x0.equals(x1).toBoolean());
x1 = Field(37);
console.assert(x0.equals(x1).toBoolean());

// When initializing with booleans, true corresponds to the field element 1, and false corresponds to 0
const b = Field(true);
console.assert(b.equals(Field(1)).toBoolean());

/* You can perform arithmetic operations on field elements.
The arithmetic methods can take any "fieldy" values as inputs:
Field, number, string, or boolean
*/
const z = x0.mul(x1).add(b).div(234).square().neg().sub('67').add(false);
const b = Field(1);
const z = x0.mul(x1).add(b).div(234).square().neg().sub('67').add(0);

/* Field elements can be converted to their full, little endian binary representation. */
let bits: Bool[] = z.toBits();
Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export {
SmartContract,
method,
DeployArgs,
signFeePayer,
declareMethods,
Account,
VerificationKey,
Expand Down
53 changes: 11 additions & 42 deletions src/lib/account_update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { hashWithPrefix, packToFields } from './hash.js';
import { prefixes } from '../bindings/crypto/constants.js';
import { Context } from './global-context.js';
import { assert } from './errors.js';
import { Ml } from './ml/conversion.js';

// external API
export { AccountUpdate, Permissions, ZkappPublicInput };
Expand All @@ -40,7 +41,6 @@ export {
ZkappCommand,
addMissingSignatures,
addMissingProofs,
signJsonTransaction,
ZkappStateLength,
Events,
Actions,
Expand Down Expand Up @@ -1870,11 +1870,15 @@ function addMissingSignatures(
// there is a change signature will be added by the wallet
// if not, error will be thrown by verifyAccountUpdate
// while .send() execution
return { body, authorization: Ledger.dummySignature() }
return { body, authorization: Ledger.dummySignature() };
}
privateKey = additionalKeys[i];
}
let signature = Ledger.signFieldElement(fullCommitment, privateKey, false);
let signature = Ledger.signFieldElement(
fullCommitment,
Ml.fromPrivateKey(privateKey),
false
);
return { body, authorization: signature };
}

Expand All @@ -1894,7 +1898,9 @@ function addMissingSignatures(
// if not, error will be thrown by verifyAccountUpdate
// while .send() execution
Authorization.setSignature(accountUpdate, Ledger.dummySignature());
return accountUpdate as AccountUpdate & { lazyAuthorization: undefined };
return accountUpdate as AccountUpdate & {
lazyAuthorization: undefined;
};
}
privateKey = additionalKeys[i];
}
Expand All @@ -1903,7 +1909,7 @@ function addMissingSignatures(
: commitment;
let signature = Ledger.signFieldElement(
transactionCommitment,
privateKey,
Ml.fromPrivateKey(privateKey),
false
);
Authorization.setSignature(accountUpdate, signature);
Expand Down Expand Up @@ -2042,40 +2048,3 @@ async function addMissingProofs(
proofs,
};
}

/**
* Sign all accountUpdates of a transaction which belong to the account
* determined by [[ `privateKey` ]].
* @returns the modified transaction JSON
*/
function signJsonTransaction(
transactionJson: string,
privateKey: PrivateKey | string
) {
if (typeof privateKey === 'string')
privateKey = PrivateKey.fromBase58(privateKey);
let publicKey = privateKey.toPublicKey().toBase58();
let zkappCommand: Types.Json.ZkappCommand = JSON.parse(transactionJson);
let feePayer = zkappCommand.feePayer;
if (feePayer.body.publicKey === publicKey) {
zkappCommand = JSON.parse(
Ledger.signFeePayer(JSON.stringify(zkappCommand), privateKey)
);
}
for (let i = 0; i < zkappCommand.accountUpdates.length; i++) {
let accountUpdate = zkappCommand.accountUpdates[i];
if (
accountUpdate.body.publicKey === publicKey &&
accountUpdate.authorization.proof === null
) {
zkappCommand = JSON.parse(
Ledger.signOtherAccountUpdate(
JSON.stringify(zkappCommand),
privateKey,
i
)
);
}
}
return JSON.stringify(zkappCommand);
}
29 changes: 2 additions & 27 deletions src/lib/core.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { bytesToBigInt } from '../bindings/crypto/bigint-helpers.js';
import { defineBinable } from '../bindings/lib/binable.js';
import { sizeInBits } from '../provable/field-bigint.js';
import { Bool, Scalar } from '../snarky.js';
import { Bool } from '../snarky.js';
import { Field as InternalField } from './field.js';
import { Group as InternalGroup } from './group.js';
import { Scalar as ScalarBigint } from '../provable/curve-bigint.js';
import { mod } from '../bindings/crypto/finite_field.js';
import { Scalar } from './scalar.js';

Copy link
Member

Choose a reason for hiding this comment

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

Do we want to add the function constructor for the Scalar as well? I am a bit unsure - while it would be uniform, Scalar is also not really a primitive like Field or Bool, especially since you cant prove operations like addition 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. I think the need is not pressing and requires refactoring the current constructor, so would save it for a future PR

export { Field, Bool, Scalar, Group };

Expand Down Expand Up @@ -69,7 +67,6 @@ type InferReturn<T> = T extends new (...args: any) => infer Return
// patching ocaml classes

Bool.toAuxiliary = () => [];
Scalar.toAuxiliary = () => [];

Bool.toInput = function (x) {
return { packed: [[x.toField(), 1] as [Field, number]] };
Expand All @@ -88,25 +85,3 @@ Bool.toBytes = BoolBinable.toBytes;
Bool.fromBytes = BoolBinable.fromBytes;
Bool.readBytes = BoolBinable.readBytes;
Bool.sizeInBytes = () => 1;

Scalar.toFieldsCompressed = function (s: Scalar) {
let isConstant = s.toFields().every((x) => x.isConstant());
let constantValue: Uint8Array | undefined = (s as any).constantValue;
if (!isConstant || constantValue === undefined)
throw Error(
`Scalar.toFieldsCompressed is not available in provable code.
That means it can't be called in a @method or similar environment, and there's no alternative implemented to achieve that.`
);
let x = bytesToBigInt(constantValue);
let lowBitSize = BigInt(sizeInBits - 1);
let lowBitMask = (1n << lowBitSize) - 1n;
return {
field: Field(x & lowBitMask),
highBit: Bool(x >> lowBitSize === 1n),
};
};

Scalar.fromBigInt = function (scalar: bigint) {
scalar = mod(scalar, ScalarBigint.modulus);
return Scalar.fromJSON(scalar.toString());
};
3 changes: 1 addition & 2 deletions src/lib/encryption.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Scalar } from '../snarky.js';
import { Field, Group } from './core.js';
import { Field, Scalar, Group } from './core.js';
import { Poseidon } from './hash.js';
import { Provable } from './provable.js';
import { PrivateKey, PublicKey } from './signature.js';
Expand Down
9 changes: 9 additions & 0 deletions src/lib/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ function constFromBigint(x: Fp) {
const FieldConst = {
fromBigint: constFromBigint,
toBigint: constToBigint,
equal(x: FieldConst, y: FieldConst) {
for (let i = 0, n = Fp.sizeInBytes(); i < n; i++) {
if (x[i] !== y[i]) return false;
}
return true;
},
[0]: constFromBigint(0n),
[1]: constFromBigint(1n),
[-1]: constFromBigint(Fp(-1n)),
Expand Down Expand Up @@ -59,6 +65,9 @@ const FieldVar = {
let x0 = typeof x === 'bigint' ? FieldConst.fromBigint(x) : x;
return [FieldType.Constant, x0];
},
isConstant(x: FieldVar): x is ConstantFieldVar {
return x[0] === FieldType.Constant;
},
// TODO: handle (special) constants
add(x: FieldVar, y: FieldVar): FieldVar {
return [FieldType.Add, x, y];
Expand Down
16 changes: 8 additions & 8 deletions src/lib/group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ describe('group', () => {
it('x*g+y*g = (x+y)*g', () => {
expect(() => {
Provable.runAndCheck(() => {
const x = Scalar.fromJSON(2)!;
const y = Scalar.fromJSON(3)!;
const x = Scalar.from(2);
const y = Scalar.from(3);
const left = g.scale(x).add(g.scale(y));
const right = g.scale(x.add(y));
left.assertEquals(right);
Expand All @@ -87,8 +87,8 @@ describe('group', () => {
it('x*(y*g) = (x*y)*g', () => {
expect(() => {
Provable.runAndCheck(() => {
const x = Scalar.fromJSON(2)!;
const y = Scalar.fromJSON(3)!;
const x = Scalar.from(2);
const y = Scalar.from(3);
const left = g.scale(y).scale(x);
const right = g.scale(y.mul(x));
left.assertEquals(right);
Expand Down Expand Up @@ -187,16 +187,16 @@ describe('group', () => {
});

it('x*g+y*g = (x+y)*g', () => {
const x = Scalar.fromJSON(2)!;
const y = Scalar.fromJSON(3)!;
const x = Scalar.from(2);
const y = Scalar.from(3);
const left = g.scale(x).add(g.scale(y));
const right = g.scale(x.add(y));
expect(left).toEqual(right);
});

it('x*(y*g) = (x*y)*g', () => {
const x = Scalar.fromJSON(2)!;
const y = Scalar.fromJSON(3)!;
const x = Scalar.from(2);
const y = Scalar.from(3);
const left = g.scale(y).scale(x);
const right = g.scale(y.mul(x));
expect(left).toEqual(right);
Expand Down
21 changes: 8 additions & 13 deletions src/lib/group.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Scalar } from './core.js';
import { Field, FieldVar, isField } from './field.js';
import { Scalar } from './scalar.js';
import { Bool, Snarky } from '../snarky.js';
import { Field as Fp } from '../provable/field-bigint.js';
import { Pallas } from '../bindings/crypto/elliptic_curve.js';
Expand Down Expand Up @@ -124,20 +124,15 @@ class Group {
* ```
*/
scale(s: Scalar | number | bigint) {
let scalar =
typeof s === 'bigint' || typeof s === 'number'
? Scalar.fromBigInt(BigInt(s))
: s;
let fields = scalar.toFields();

if (this.#isConstant() && fields.every((f) => f.isConstant())) {
let g_proj = Pallas.scale(this.#toProjective(), BigInt(scalar.toJSON()));
let scalar = Scalar.from(s);

if (this.#isConstant() && scalar.isConstant()) {
let g_proj = Pallas.scale(this.#toProjective(), scalar.toBigInt());
return Group.#fromProjective(g_proj);
} else {
let [, x, y] = Snarky.group.scale(this.#toTuple(), [
0,
...fields.map((f) => f.value).reverse(),
]);
let [, ...bits] = scalar.value;
bits.reverse();
let [, x, y] = Snarky.group.scale(this.#toTuple(), [0, ...bits]);
return new Group({ x, y });
}
}
Expand Down
24 changes: 24 additions & 0 deletions src/lib/ml/consistency.unit-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Test } from '../../snarky.js';
import { Random, test } from '../testing/property.js';
import { PrivateKey } from '../signature.js';
import { Ml } from './conversion.js';
import { expect } from 'expect';

// PrivateKey.toBase58, fromBase58

test(Random.privateKey, (s) => {
// private key to/from bigint
let sk = PrivateKey.fromBigInt(s);
expect(sk.toBigInt()).toEqual(s);

let skMl = Ml.fromPrivateKey(sk);

// toBase58 - check consistency with ml
let ml = Test.encoding.privateKeyToBase58(skMl);
let js = sk.toBase58();
expect(js).toEqual(ml);

// fromBase58 - check consistency with where we started
expect(PrivateKey.fromBase58(js)).toEqual(sk);
expect(Test.encoding.privateKeyOfBase58(ml)).toEqual(skMl);
});
29 changes: 29 additions & 0 deletions src/lib/ml/conversion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* this file contains conversion functions between JS and OCaml
*/

import { Scalar, ScalarConst } from '../scalar.js';
import { PrivateKey } from '../signature.js';

export { Ml };

const Ml = {
fromScalar,
toScalar,
fromPrivateKey,
toPrivateKey,
};

function fromScalar(s: Scalar) {
return s.toConstant().constantValue;
}
function toScalar(s: ScalarConst) {
return Scalar.from(s);
}

function fromPrivateKey(sk: PrivateKey) {
return fromScalar(sk.s);
}
function toPrivateKey(sk: ScalarConst) {
return new PrivateKey(Scalar.from(sk));
}