diff --git a/.github/actions/live-tests-shared/action.yml b/.github/actions/live-tests-shared/action.yml index 05d8970d97..a84bad475a 100644 --- a/.github/actions/live-tests-shared/action.yml +++ b/.github/actions/live-tests-shared/action.yml @@ -1,11 +1,11 @@ -name: "Shared steps for live testing jobs" -description: "Shared steps for live testing jobs" +name: 'Shared steps for live testing jobs' +description: 'Shared steps for live testing jobs' inputs: mina-branch-name: - description: "Mina branch name in use by service container" + description: 'Mina branch name in use by service container' required: true runs: - using: "composite" + using: 'composite' steps: - name: Wait for Mina network readiness uses: o1-labs/wait-for-mina-network-action@v1 @@ -16,15 +16,15 @@ runs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: "20" + node-version: '20' - name: Build o1js and execute tests env: - TEST_TYPE: "Live integration tests" - USE_CUSTOM_LOCAL_NETWORK: "true" + TEST_TYPE: 'Live integration tests' + USE_CUSTOM_LOCAL_NETWORK: 'true' run: | git submodule update --init --recursive npm ci - npm run build:node + npm run build touch profiling.md sh run-ci-tests.sh cat profiling.md >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/build-action.yml b/.github/workflows/build-action.yml index 3801f8ea78..43cea697ff 100644 --- a/.github/workflows/build-action.yml +++ b/.github/workflows/build-action.yml @@ -39,7 +39,7 @@ jobs: run: | git submodule update --init --recursive npm ci - npm run build:node + npm run build touch profiling.md sh run-ci-tests.sh cat profiling.md >> $GITHUB_STEP_SUMMARY @@ -91,7 +91,7 @@ jobs: run: | git submodule update --init --recursive npm ci - npm run build:node + npm run build - name: Publish to NPM if version has changed uses: JS-DevTools/npm-publish@v1 if: github.ref == 'refs/heads/main' diff --git a/CHANGELOG.md b/CHANGELOG.md index 601e2a8b0f..4961b8a2d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased](https://github.com/o1-labs/o1js/compare/7acf19d0d...HEAD) +### Added + +- Non-native elliptic curve operations exposed through `createForeignCurve()` class factory https://github.com/o1-labs/o1js/pull/1007 +- **ECDSA signature verification** exposed through `createEcdsa()` class factory https://github.com/o1-labs/o1js/pull/1240 https://github.com/o1-labs/o1js/pull/1007 + - For an example, see `./src/examples/crypto/ecdsa` + ## [0.15.0](https://github.com/o1-labs/o1js/compare/1ad7333e9e...7acf19d0d) ### Breaking changes diff --git a/package.json b/package.json index 341848220e..937e09b099 100644 --- a/package.json +++ b/package.json @@ -42,19 +42,17 @@ "node": ">=16.4.0" }, "scripts": { - "dev": "npx tsc -p tsconfig.node.json && node src/build/copy-to-dist.js", + "dev": "npx tsc -p tsconfig.test.json && node src/build/copy-to-dist.js", "make": "make -C ../../.. snarkyjs", "make:no-types": "npm run clean && make -C ../../.. snarkyjs_no_types", "wasm": "./src/bindings/scripts/update-wasm-and-types.sh", "bindings": "cd ../../.. && ./scripts/update-snarkyjs-bindings.sh && cd src/lib/snarkyjs", "build": "node src/build/copy-artifacts.js && rimraf ./dist/node && npm run dev && node src/build/buildNode.js", - "build:test": "npx tsc -p tsconfig.test.json && cp src/snarky.d.ts dist/node/snarky.d.ts", - "build:node": "npm run build", "build:web": "rimraf ./dist/web && node src/build/buildWeb.js", "build:examples": "npm run build && rimraf ./dist/examples && npx tsc -p tsconfig.examples.json", "build:docs": "npx typedoc --tsconfig ./tsconfig.web.json", "prepublish:web": "NODE_ENV=production node src/build/buildWeb.js", - "prepublish:node": "npm run build && NODE_ENV=production node src/build/buildNode.js", + "prepublish:node": "node src/build/copy-artifacts.js && rimraf ./dist/node && npx tsc -p tsconfig.node.json && node src/build/copy-to-dist.js && NODE_ENV=production node src/build/buildNode.js", "prepublishOnly": "npm run prepublish:web && npm run prepublish:node", "dump-vks": "./run tests/vk-regression/vk-regression.ts --bundle --dump", "format": "prettier --write --ignore-unknown **/*", diff --git a/run-unit-tests.sh b/run-unit-tests.sh index 44881eca5d..3e39307f36 100755 --- a/run-unit-tests.sh +++ b/run-unit-tests.sh @@ -2,8 +2,7 @@ set -e shopt -s globstar # to expand '**' into nested directories./ -# run the build:test -npm run build:test +npm run build # find all unit tests in dist/node and run them # TODO it would be nice to make this work on Mac diff --git a/src/bindings b/src/bindings index 700639bc5d..1e14e50fe2 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 700639bc5df5b7e072446e3196bb0d3594fb32db +Subproject commit 1e14e50fe270e8b401e88351b2c03a6e2ab5fa63 diff --git a/src/examples/api_exploration.ts b/src/examples/api_exploration.ts index 69e8f2db3b..43e2284950 100644 --- a/src/examples/api_exploration.ts +++ b/src/examples/api_exploration.ts @@ -150,7 +150,7 @@ console.assert(!signature.verify(pubKey, msg1).toBoolean()); /* You can initialize elements as literals as follows: */ let g0 = Group.from(-1, 2); -let g1 = new Group({ x: -2, y: 2 }); +let g1 = new Group({ x: -1, y: 2 }); /* There is also a predefined generator. */ let g2 = Group.generator; diff --git a/src/examples/benchmarks/foreign-field.ts b/src/examples/benchmarks/foreign-field.ts new file mode 100644 index 0000000000..fb32439e0f --- /dev/null +++ b/src/examples/benchmarks/foreign-field.ts @@ -0,0 +1,31 @@ +import { Crypto, Provable, createForeignField } from 'o1js'; + +class ForeignScalar extends createForeignField( + Crypto.CurveParams.Secp256k1.modulus +) {} + +function main() { + let s = Provable.witness( + ForeignScalar.Canonical.provable, + ForeignScalar.random + ); + let t = Provable.witness( + ForeignScalar.Canonical.provable, + ForeignScalar.random + ); + s.mul(t); +} + +console.time('running constant version'); +main(); +console.timeEnd('running constant version'); + +console.time('running witness generation & checks'); +Provable.runAndCheck(main); +console.timeEnd('running witness generation & checks'); + +console.time('creating constraint system'); +let cs = Provable.constraintSystem(main); +console.timeEnd('creating constraint system'); + +console.log(cs.summary()); diff --git a/src/examples/crypto/README.md b/src/examples/crypto/README.md index 60d0d50d51..c2f913defa 100644 --- a/src/examples/crypto/README.md +++ b/src/examples/crypto/README.md @@ -3,3 +3,4 @@ These examples show how to use some of the crypto primitives that are supported in provable o1js code. - Non-native field arithmetic: `foreign-field.ts` +- Non-native ECDSA verification: `ecdsa.ts` diff --git a/src/examples/crypto/ecdsa/ecdsa.ts b/src/examples/crypto/ecdsa/ecdsa.ts new file mode 100644 index 0000000000..8268117033 --- /dev/null +++ b/src/examples/crypto/ecdsa/ecdsa.ts @@ -0,0 +1,22 @@ +import { ZkProgram, Crypto, createEcdsa, createForeignCurve, Bool } from 'o1js'; + +export { ecdsaProgram, Secp256k1, Ecdsa }; + +class Secp256k1 extends createForeignCurve(Crypto.CurveParams.Secp256k1) {} +class Scalar extends Secp256k1.Scalar {} +class Ecdsa extends createEcdsa(Secp256k1) {} + +const ecdsaProgram = ZkProgram({ + name: 'ecdsa', + publicInput: Scalar.provable, + publicOutput: Bool, + + methods: { + verifyEcdsa: { + privateInputs: [Ecdsa.provable, Secp256k1.provable], + method(msgHash: Scalar, signature: Ecdsa, publicKey: Secp256k1) { + return signature.verify(msgHash, publicKey); + }, + }, + }, +}); diff --git a/src/examples/crypto/ecdsa/run.ts b/src/examples/crypto/ecdsa/run.ts new file mode 100644 index 0000000000..a2e49f7062 --- /dev/null +++ b/src/examples/crypto/ecdsa/run.ts @@ -0,0 +1,33 @@ +import { Secp256k1, Ecdsa, ecdsaProgram } from './ecdsa.js'; +import assert from 'assert'; + +// create an example ecdsa signature + +let privateKey = Secp256k1.Scalar.random(); +let publicKey = Secp256k1.generator.scale(privateKey); + +// TODO use an actual keccak hash +let messageHash = Secp256k1.Scalar.random(); + +let signature = Ecdsa.sign(messageHash.toBigInt(), privateKey.toBigInt()); + +// investigate the constraint system generated by ECDSA verify + +console.time('ecdsa verify (build constraint system)'); +let cs = ecdsaProgram.analyzeMethods().verifyEcdsa; +console.timeEnd('ecdsa verify (build constraint system)'); + +console.log(cs.summary()); + +// compile and prove + +console.time('ecdsa verify (compile)'); +await ecdsaProgram.compile(); +console.timeEnd('ecdsa verify (compile)'); + +console.time('ecdsa verify (prove)'); +let proof = await ecdsaProgram.verifyEcdsa(messageHash, signature, publicKey); +console.timeEnd('ecdsa verify (prove)'); + +proof.publicOutput.assertTrue('signature verifies'); +assert(await ecdsaProgram.verify(proof), 'proof verifies'); diff --git a/src/index.ts b/src/index.ts index 6448aede09..bbe7d8bb74 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,8 @@ export { AlmostForeignField, CanonicalForeignField, } from './lib/foreign-field.js'; +export { createForeignCurve, ForeignCurve } from './lib/foreign-curve.js'; +export { createEcdsa, EcdsaSignature } from './lib/foreign-ecdsa.js'; export { Poseidon, TokenSymbol } from './lib/hash.js'; export * from './lib/signature.js'; export type { diff --git a/src/lib/circuit_value.ts b/src/lib/circuit_value.ts index 6c4c540106..d42f267cdf 100644 --- a/src/lib/circuit_value.ts +++ b/src/lib/circuit_value.ts @@ -41,7 +41,6 @@ export { cloneCircuitValue, circuitValueEquals, toConstant, - isConstant, InferProvable, HashInput, InferJson, @@ -693,8 +692,3 @@ function toConstant(type: Provable, value: T): T { type.toAuxiliary(value) ); } - -function isConstant(type: FlexibleProvable, value: T): boolean; -function isConstant(type: Provable, value: T): boolean { - return type.toFields(value).every((x) => x.isConstant()); -} diff --git a/src/lib/foreign-curve.ts b/src/lib/foreign-curve.ts new file mode 100644 index 0000000000..08fb733bfd --- /dev/null +++ b/src/lib/foreign-curve.ts @@ -0,0 +1,312 @@ +import { + CurveParams, + CurveAffine, + createCurveAffine, +} from '../bindings/crypto/elliptic_curve.js'; +import type { Group } from './group.js'; +import { ProvablePureExtended } from './circuit_value.js'; +import { AlmostForeignField, createForeignField } from './foreign-field.js'; +import { EllipticCurve, Point } from './gadgets/elliptic-curve.js'; +import { Field3 } from './gadgets/foreign-field.js'; +import { assert } from './gadgets/common.js'; +import { Provable } from './provable.js'; +import { provableFromClass } from '../bindings/lib/provable-snarky.js'; + +// external API +export { createForeignCurve, ForeignCurve }; + +// internal API +export { toPoint, FlexiblePoint }; + +type FlexiblePoint = { + x: AlmostForeignField | Field3 | bigint | number; + y: AlmostForeignField | Field3 | bigint | number; +}; +function toPoint({ x, y }: ForeignCurve): Point { + return { x: x.value, y: y.value }; +} + +class ForeignCurve { + x: AlmostForeignField; + y: AlmostForeignField; + + /** + * Create a new {@link ForeignCurve} from an object representing the (affine) x and y coordinates. + * + * @example + * ```ts + * let x = new ForeignCurve({ x: 1n, y: 1n }); + * ``` + * + * **Important**: By design, there is no way for a `ForeignCurve` to represent the zero point. + * + * **Warning**: This fails for a constant input which does not represent an actual point on the curve. + */ + constructor(g: { + x: AlmostForeignField | Field3 | bigint | number; + y: AlmostForeignField | Field3 | bigint | number; + }) { + this.x = new this.Constructor.Field(g.x); + this.y = new this.Constructor.Field(g.y); + // don't allow constants that aren't on the curve + if (this.isConstant()) { + this.assertOnCurve(); + this.assertInSubgroup(); + } + } + + /** + * Coerce the input to a {@link ForeignCurve}. + */ + static from(g: ForeignCurve | FlexiblePoint) { + if (g instanceof this) return g; + return new this(g); + } + + /** + * The constant generator point. + */ + static get generator() { + return new this(this.Bigint.one); + } + /** + * The size of the curve's base field. + */ + static get modulus() { + return this.Bigint.modulus; + } + /** + * The size of the curve's base field. + */ + get modulus() { + return this.Constructor.Bigint.modulus; + } + + /** + * Checks whether this curve point is constant. + * + * See {@link FieldVar} to understand constants vs variables. + */ + isConstant() { + return Provable.isConstant(this.Constructor.provable, this); + } + + /** + * Convert this curve point to a point with bigint coordinates. + */ + toBigint() { + return this.Constructor.Bigint.fromNonzero({ + x: this.x.toBigInt(), + y: this.y.toBigInt(), + }); + } + + /** + * Elliptic curve addition. + * + * ```ts + * let r = p.add(q); // r = p + q + * ``` + * + * **Important**: this is _incomplete addition_ and does not handle the degenerate cases: + * - Inputs are equal, `g = h` (where you would use {@link double}). + * In this case, the result of this method is garbage and can be manipulated arbitrarily by a malicious prover. + * - Inputs are inverses of each other, `g = -h`, so that the result would be the zero point. + * In this case, the proof fails. + * + * If you want guaranteed soundness regardless of the input, use {@link addSafe} instead. + * + * @throws if the inputs are inverses of each other. + */ + add(h: ForeignCurve | FlexiblePoint) { + let Curve = this.Constructor.Bigint; + let h_ = this.Constructor.from(h); + let p = EllipticCurve.add(toPoint(this), toPoint(h_), Curve); + return new this.Constructor(p); + } + + /** + * Safe elliptic curve addition. + * + * This is the same as {@link add}, but additionally proves that the inputs are not equal. + * Therefore, the method is guaranteed to either fail or return a valid addition result. + * + * **Beware**: this is more expensive than {@link add}, and is still incomplete in that + * it does not succeed on equal or inverse inputs. + * + * @throws if the inputs are equal or inverses of each other. + */ + addSafe(h: ForeignCurve | FlexiblePoint) { + let h_ = this.Constructor.from(h); + + // prove that we have x1 != x2 => g != +-h + let x1 = this.x.assertCanonical(); + let x2 = h_.x.assertCanonical(); + x1.equals(x2).assertFalse(); + + return this.add(h_); + } + + /** + * Elliptic curve doubling. + * + * @example + * ```ts + * let r = p.double(); // r = 2 * p + * ``` + */ + double() { + let Curve = this.Constructor.Bigint; + let p = EllipticCurve.double(toPoint(this), Curve); + return new this.Constructor(p); + } + + /** + * Elliptic curve negation. + * + * @example + * ```ts + * let r = p.negate(); // r = -p + * ``` + */ + negate(): ForeignCurve { + return new this.Constructor({ x: this.x, y: this.y.neg() }); + } + + /** + * Elliptic curve scalar multiplication, where the scalar is represented as a {@link ForeignField} element. + * + * **Important**: this proves that the result of the scalar multiplication is not the zero point. + * + * @throws if the scalar multiplication results in the zero point; for example, if the scalar is zero. + * + * @example + * ```ts + * let r = p.scale(s); // r = s * p + * ``` + */ + scale(scalar: AlmostForeignField | bigint | number) { + let Curve = this.Constructor.Bigint; + let scalar_ = this.Constructor.Scalar.from(scalar); + let p = EllipticCurve.scale(scalar_.value, toPoint(this), Curve); + return new this.Constructor(p); + } + + static assertOnCurve(g: ForeignCurve) { + EllipticCurve.assertOnCurve(toPoint(g), this.Bigint); + } + + /** + * Assert that this point lies on the elliptic curve, which means it satisfies the equation + * `y^2 = x^3 + ax + b` + */ + assertOnCurve() { + this.Constructor.assertOnCurve(this); + } + + static assertInSubgroup(g: ForeignCurve) { + if (this.Bigint.hasCofactor) { + EllipticCurve.assertInSubgroup(toPoint(g), this.Bigint); + } + } + + /** + * Assert that this point lies in the subgroup defined by `order*P = 0`. + * + * Note: this is a no-op if the curve has cofactor equal to 1. Otherwise + * it performs the full scalar multiplication `order*P` and is expensive. + */ + assertInSubgroup() { + this.Constructor.assertInSubgroup(this); + } + + /** + * Check that this is a valid element of the target subgroup of the curve: + * - Check that the coordinates are valid field elements + * - Use {@link assertOnCurve()} to check that the point lies on the curve + * - If the curve has cofactor unequal to 1, use {@link assertInSubgroup()}. + */ + static check(g: ForeignCurve) { + // more efficient than the automatic check, which would do this for each field separately + this.Field.assertAlmostReduced(g.x, g.y); + this.assertOnCurve(g); + this.assertInSubgroup(g); + } + + // dynamic subclassing infra + get Constructor() { + return this.constructor as typeof ForeignCurve; + } + static _Bigint?: CurveAffine; + static _Field?: typeof AlmostForeignField; + static _Scalar?: typeof AlmostForeignField; + static _provable?: ProvablePureExtended< + ForeignCurve, + { x: string; y: string } + >; + + /** + * Curve arithmetic on JS bigints. + */ + static get Bigint() { + assert(this._Bigint !== undefined, 'ForeignCurve not initialized'); + return this._Bigint; + } + /** + * The base field of this curve as a {@link ForeignField}. + */ + static get Field() { + assert(this._Field !== undefined, 'ForeignCurve not initialized'); + return this._Field; + } + /** + * The scalar field of this curve as a {@link ForeignField}. + */ + static get Scalar() { + assert(this._Scalar !== undefined, 'ForeignCurve not initialized'); + return this._Scalar; + } + /** + * `Provable` + */ + static get provable() { + assert(this._provable !== undefined, 'ForeignCurve not initialized'); + return this._provable; + } +} + +/** + * Create a class representing an elliptic curve group, which is different from the native {@link Group}. + * + * ```ts + * const Curve = createForeignCurve(Crypto.CurveParams.Secp256k1); + * ``` + * + * `createForeignCurve(params)` takes curve parameters {@link CurveParams} as input. + * We support `modulus` and `order` to be prime numbers up to 259 bits. + * + * The returned {@link ForeignCurve} class represents a _non-zero curve point_ and supports standard + * elliptic curve operations like point addition and scalar multiplication. + * + * {@link ForeignCurve} also includes to associated foreign fields: `ForeignCurve.Field` and `ForeignCurve.Scalar`, see {@link createForeignField}. + */ +function createForeignCurve(params: CurveParams): typeof ForeignCurve { + const FieldUnreduced = createForeignField(params.modulus); + const ScalarUnreduced = createForeignField(params.order); + class Field extends FieldUnreduced.AlmostReduced {} + class Scalar extends ScalarUnreduced.AlmostReduced {} + + const BigintCurve = createCurveAffine(params); + + class Curve extends ForeignCurve { + static _Bigint = BigintCurve; + static _Field = Field; + static _Scalar = Scalar; + static _provable = provableFromClass(Curve, { + x: Field.provable, + y: Field.provable, + }); + } + + return Curve; +} diff --git a/src/lib/foreign-curve.unit-test.ts b/src/lib/foreign-curve.unit-test.ts new file mode 100644 index 0000000000..9cdac03730 --- /dev/null +++ b/src/lib/foreign-curve.unit-test.ts @@ -0,0 +1,42 @@ +import { createForeignCurve } from './foreign-curve.js'; +import { Fq } from '../bindings/crypto/finite_field.js'; +import { Vesta as V } from '../bindings/crypto/elliptic_curve.js'; +import { Provable } from './provable.js'; +import { Field } from './field.js'; +import { Crypto } from './crypto.js'; + +class Vesta extends createForeignCurve(Crypto.CurveParams.Vesta) {} +class Fp extends Vesta.Scalar {} + +let g = { x: Fq.negate(1n), y: 2n, infinity: false }; +let h = V.toAffine(V.negate(V.double(V.add(V.fromAffine(g), V.one)))); +let scalar = Field.random().toBigInt(); +let p = V.toAffine(V.scale(V.fromAffine(h), scalar)); + +function main() { + let g0 = Provable.witness(Vesta.provable, () => new Vesta(g)); + let one = Provable.witness(Vesta.provable, () => Vesta.generator); + let h0 = g0.add(one).double().negate(); + Provable.assertEqual(Vesta.provable, h0, new Vesta(h)); + + h0.assertOnCurve(); + h0.assertInSubgroup(); + + let scalar0 = Provable.witness(Fp.provable, () => new Fp(scalar)); + let p0 = h0.scale(scalar0); + Provable.assertEqual(Vesta.provable, p0, new Vesta(p)); +} + +console.time('running constant version'); +main(); +console.timeEnd('running constant version'); + +console.time('running witness generation & checks'); +Provable.runAndCheck(main); +console.timeEnd('running witness generation & checks'); + +console.time('creating constraint system'); +let cs = Provable.constraintSystem(main); +console.timeEnd('creating constraint system'); + +console.log(cs.summary()); diff --git a/src/lib/foreign-ecdsa.ts b/src/lib/foreign-ecdsa.ts new file mode 100644 index 0000000000..235e284556 --- /dev/null +++ b/src/lib/foreign-ecdsa.ts @@ -0,0 +1,172 @@ +import { provableFromClass } from '../bindings/lib/provable-snarky.js'; +import { CurveParams } from '../bindings/crypto/elliptic_curve.js'; +import { ProvablePureExtended } from './circuit_value.js'; +import { + FlexiblePoint, + ForeignCurve, + createForeignCurve, + toPoint, +} from './foreign-curve.js'; +import { AlmostForeignField } from './foreign-field.js'; +import { assert } from './gadgets/common.js'; +import { Field3 } from './gadgets/foreign-field.js'; +import { Ecdsa } from './gadgets/elliptic-curve.js'; + +// external API +export { createEcdsa, EcdsaSignature }; + +type FlexibleSignature = + | EcdsaSignature + | { + r: AlmostForeignField | Field3 | bigint | number; + s: AlmostForeignField | Field3 | bigint | number; + }; + +class EcdsaSignature { + r: AlmostForeignField; + s: AlmostForeignField; + + /** + * Create a new {@link EcdsaSignature} from an object containing the scalars r and s. + * @param signature + */ + constructor(signature: { + r: AlmostForeignField | Field3 | bigint | number; + s: AlmostForeignField | Field3 | bigint | number; + }) { + this.r = new this.Constructor.Curve.Scalar(signature.r); + this.s = new this.Constructor.Curve.Scalar(signature.s); + } + + /** + * Coerce the input to a {@link EcdsaSignature}. + */ + static from(signature: FlexibleSignature): EcdsaSignature { + if (signature instanceof this) return signature; + return new this(signature); + } + + /** + * Create an {@link EcdsaSignature} from a raw 130-char hex string as used in + * [Ethereum transactions](https://ethereum.org/en/developers/docs/transactions/#typed-transaction-envelope). + */ + static fromHex(rawSignature: string): EcdsaSignature { + let s = Ecdsa.Signature.fromHex(rawSignature); + return new this(s); + } + + /** + * Convert this signature to an object with bigint fields. + */ + toBigInt() { + return { r: this.r.toBigInt(), s: this.s.toBigInt() }; + } + + /** + * Verify the ECDSA signature given the message hash (a {@link Scalar}) and public key (a {@link Curve} point). + * + * **Important:** This method returns a {@link Bool} which indicates whether the signature is valid. + * So, to actually prove validity of a signature, you need to assert that the result is true. + * + * @throws if one of the signature scalars is zero or if the public key is not on the curve. + * + * @example + * ```ts + * // create classes for your curve + * class Secp256k1 extends createForeignCurve(Crypto.CurveParams.Secp256k1) {} + * class Scalar extends Secp256k1.Scalar {} + * class Ecdsa extends createEcdsa(Secp256k1) {} + * + * // outside provable code: create inputs + * let privateKey = Scalar.random(); + * let publicKey = Secp256k1.generator.scale(privateKey); + * let messageHash = Scalar.random(); + * let signature = Ecdsa.sign(messageHash.toBigInt(), privateKey.toBigInt()); + * + * // ... + * // in provable code: create input witnesses (or use method inputs, or constants) + * let pk = Provable.witness(Secp256k1.provable, () => publicKey); + * let msgHash = Provable.witness(Scalar.Canonical.provable, () => messageHash); + * let sig = Provable.witness(Ecdsa.provable, () => signature); + * + * // verify signature + * let isValid = sig.verify(msgHash, pk); + * isValid.assertTrue('signature verifies'); + * ``` + */ + verify(msgHash: AlmostForeignField | bigint, publicKey: FlexiblePoint) { + let msgHash_ = this.Constructor.Curve.Scalar.from(msgHash); + let publicKey_ = this.Constructor.Curve.from(publicKey); + return Ecdsa.verify( + this.Constructor.Curve.Bigint, + toObject(this), + msgHash_.value, + toPoint(publicKey_) + ); + } + + /** + * Create an {@link EcdsaSignature} by signing a message hash with a private key. + * + * Note: This method is not provable, and only takes JS bigints as input. + */ + static sign(msgHash: bigint, privateKey: bigint) { + let { r, s } = Ecdsa.sign(this.Curve.Bigint, msgHash, privateKey); + return new this({ r, s }); + } + + static check(signature: EcdsaSignature) { + // more efficient than the automatic check, which would do this for each scalar separately + this.Curve.Scalar.assertAlmostReduced(signature.r, signature.s); + } + + // dynamic subclassing infra + get Constructor() { + return this.constructor as typeof EcdsaSignature; + } + static _Curve?: typeof ForeignCurve; + static _provable?: ProvablePureExtended< + EcdsaSignature, + { r: string; s: string } + >; + + /** + * The {@link ForeignCurve} on which the ECDSA signature is defined. + */ + static get Curve() { + assert(this._Curve !== undefined, 'EcdsaSignature not initialized'); + return this._Curve; + } + /** + * `Provable` + */ + static get provable() { + assert(this._provable !== undefined, 'EcdsaSignature not initialized'); + return this._provable; + } +} + +/** + * Create a class {@link EcdsaSignature} for verifying ECDSA signatures on the given curve. + */ +function createEcdsa( + curve: CurveParams | typeof ForeignCurve +): typeof EcdsaSignature { + let Curve0: typeof ForeignCurve = + 'b' in curve ? createForeignCurve(curve) : curve; + class Curve extends Curve0 {} + + class Signature extends EcdsaSignature { + static _Curve = Curve; + static _provable = provableFromClass(Signature, { + r: Curve.Scalar.provable, + s: Curve.Scalar.provable, + }); + } + + return Signature; +} + +function toObject(signature: EcdsaSignature) { + return { r: signature.r.value, s: signature.s.value }; +} diff --git a/src/lib/foreign-field.ts b/src/lib/foreign-field.ts index 8e11c8299a..788b95129e 100644 --- a/src/lib/foreign-field.ts +++ b/src/lib/foreign-field.ts @@ -1,13 +1,19 @@ -import { ProvablePure } from '../snarky.js'; -import { mod, Fp } from '../bindings/crypto/finite_field.js'; +import { + mod, + Fp, + FiniteField, + createField, +} from '../bindings/crypto/finite_field.js'; import { Field, FieldVar, checkBitLength, withMessage } from './field.js'; import { Provable } from './provable.js'; import { Bool } from './bool.js'; import { Tuple, TupleMap, TupleN } from './util/types.js'; import { Field3 } from './gadgets/foreign-field.js'; import { Gadgets } from './gadgets/gadgets.js'; +import { ForeignField as FF } from './gadgets/foreign-field.js'; import { assert } from './gadgets/common.js'; import { l3, l } from './gadgets/range-check.js'; +import { ProvablePureExtended } from './circuit_value.js'; // external API export { createForeignField }; @@ -19,9 +25,14 @@ export type { }; class ForeignField { + static _Bigint: FiniteField | undefined = undefined; static _modulus: bigint | undefined = undefined; // static parameters + static get Bigint() { + assert(this._Bigint !== undefined, 'ForeignField class not initialized.'); + return this._Bigint; + } static get modulus() { assert(this._modulus !== undefined, 'ForeignField class not initialized.'); return this._modulus; @@ -97,17 +108,13 @@ class ForeignField { this.value = Field3.from(mod(BigInt(x), p)); } - private static toLimbs(x: bigint | number | string | ForeignField): Field3 { - if (x instanceof ForeignField) return x.value; - return Field3.from(mod(BigInt(x), this.modulus)); - } - /** * Coerce the input to a {@link ForeignField}. */ static from(x: bigint | number | string): CanonicalForeignField; + static from(x: ForeignField | bigint | number | string): ForeignField; static from(x: ForeignField | bigint | number | string): ForeignField { - if (x instanceof ForeignField) return x; + if (x instanceof this) return x; return new this.Canonical(x); } @@ -157,10 +164,8 @@ class ForeignField { // TODO: this is not very efficient, but the only way to abstract away the complicated // range check assumptions and also not introduce a global context of pending range checks. // we plan to get rid of bounds checks anyway, then this is just a multi-range check - Gadgets.ForeignField.assertAlmostReduced([this.value], this.modulus, { - skipMrc: true, - }); - return this.Constructor.AlmostReduced.unsafeFrom(this); + let [x] = this.Constructor.assertAlmostReduced(this); + return x; } /** @@ -212,8 +217,11 @@ class ForeignField { * ``` */ neg() { - let zero: ForeignField = this.Constructor.from(0n); - return this.Constructor.sum([zero, this], [-1]); + // this gets a special implementation because negation proves that the return value is almost reduced. + // it shows that r = f - x >= 0 or r = 0 (for x=0) over the integers, which implies r < f + // see also `Gadgets.ForeignField.assertLessThan()` + let xNeg = Gadgets.ForeignField.neg(this.value, this.modulus); + return new this.Constructor.AlmostReduced(xNeg); } /** @@ -250,7 +258,7 @@ class ForeignField { */ static sum(xs: (ForeignField | bigint | number)[], operations: (1 | -1)[]) { const p = this.modulus; - let fields = xs.map((x) => this.toLimbs(x)); + let fields = xs.map((x) => toLimbs(x, p)); let ops = operations.map((op) => (op === 1 ? 1n : -1n)); let z = Gadgets.ForeignField.sum(fields, ops, p); return new this.Unreduced(z); @@ -334,25 +342,6 @@ class ForeignField { } } - /** - * Check equality with a ForeignField-like value - * @example - * ```ts - * let isXZero = x.equals(0); - * ``` - */ - equals(y: ForeignField | bigint | number) { - const p = this.modulus; - if (this.isConstant() && isConstant(y)) { - return new Bool(this.toBigInt() === mod(toBigInt(y), p)); - } - return Provable.equal( - this.Constructor.provable, - this, - new this.Constructor(y) - ); - } - // bit packing /** @@ -392,6 +381,10 @@ class ForeignField { return new this.AlmostReduced([l0, l1, l2]); } + static random() { + return new this.Canonical(this.Bigint.random()); + } + /** * Instance version of `Provable.toFields`, see {@link Provable.toFields} */ @@ -460,7 +453,9 @@ class ForeignFieldWithMul extends ForeignField { class UnreducedForeignField extends ForeignField { type: 'Unreduced' | 'AlmostReduced' | 'FullyReduced' = 'Unreduced'; - static _provable: ProvablePure | undefined = undefined; + static _provable: + | ProvablePureExtended + | undefined = undefined; static get provable() { assert(this._provable !== undefined, 'ForeignField class not initialized.'); return this._provable; @@ -478,7 +473,9 @@ class AlmostForeignField extends ForeignFieldWithMul { super(x); } - static _provable: ProvablePure | undefined = undefined; + static _provable: + | ProvablePureExtended + | undefined = undefined; static get provable() { assert(this._provable !== undefined, 'ForeignField class not initialized.'); return this._provable; @@ -497,6 +494,18 @@ class AlmostForeignField extends ForeignFieldWithMul { static unsafeFrom(x: ForeignField) { return new this(x.value); } + + /** + * Check equality with a constant value. + * + * @example + * ```ts + * let isXZero = x.equals(0); + * ``` + */ + equals(y: bigint | number) { + return FF.equals(this.value, BigInt(y), this.modulus); + } } class CanonicalForeignField extends ForeignFieldWithMul { @@ -506,7 +515,9 @@ class CanonicalForeignField extends ForeignFieldWithMul { super(x); } - static _provable: ProvablePure | undefined = undefined; + static _provable: + | ProvablePureExtended + | undefined = undefined; static get provable() { assert(this._provable !== undefined, 'ForeignField class not initialized.'); return this._provable; @@ -525,6 +536,25 @@ class CanonicalForeignField extends ForeignFieldWithMul { static unsafeFrom(x: ForeignField) { return new this(x.value); } + + /** + * Check equality with a ForeignField-like value. + * + * @example + * ```ts + * let isEqual = x.equals(y); + * ``` + * + * Note: This method only exists on canonical fields; on unreduced fields, it would be easy to + * misuse, because not being exactly equal does not imply being unequal modulo p. + */ + equals(y: CanonicalForeignField | bigint | number) { + let [x0, x1, x2] = this.value; + let [y0, y1, y2] = toLimbs(y, this.modulus); + let x01 = x0.add(x1.mul(1n << l)).seal(); + let y01 = y0.add(y1.mul(1n << l)).seal(); + return x01.equals(y01).and(x2.equals(y2)); + } } function toLimbs( @@ -594,7 +624,7 @@ function isConstant(x: bigint | number | string | ForeignField) { * * @param modulus the modulus of the finite field you are instantiating */ -function createForeignField(modulus: bigint): typeof ForeignField { +function createForeignField(modulus: bigint): typeof UnreducedForeignField { assert( modulus > 0n, `ForeignField: modulus must be positive, got ${modulus}` @@ -604,7 +634,10 @@ function createForeignField(modulus: bigint): typeof ForeignField { `ForeignField: modulus exceeds the max supported size of 2^${foreignFieldMaxBits}` ); + let Bigint = createField(modulus); + class UnreducedField extends UnreducedForeignField { + static _Bigint = Bigint; static _modulus = modulus; static _provable = provable(UnreducedField); @@ -615,6 +648,7 @@ function createForeignField(modulus: bigint): typeof ForeignField { } class AlmostField extends AlmostForeignField { + static _Bigint = Bigint; static _modulus = modulus; static _provable = provable(AlmostField); @@ -626,6 +660,7 @@ function createForeignField(modulus: bigint): typeof ForeignField { } class CanonicalField extends CanonicalForeignField { + static _Bigint = Bigint; static _modulus = modulus; static _provable = provable(CanonicalField); @@ -661,7 +696,7 @@ type Constructor = new (...args: any[]) => T; function provable( Class: Constructor & { check(x: ForeignField): void } -): ProvablePure { +): ProvablePureExtended { return { toFields(x) { return x.value; @@ -679,5 +714,26 @@ function provable( check(x: ForeignField) { Class.check(x); }, + // ugh + toJSON(x: ForeignField) { + return x.toBigInt().toString(); + }, + fromJSON(x: string) { + // TODO be more strict about allowed values + return new Class(x); + }, + empty() { + return new Class(0n); + }, + toInput(x) { + let l_ = Number(l); + return { + packed: [ + [x.value[0], l_], + [x.value[1], l_], + [x.value[2], l_], + ], + }; + }, }; } diff --git a/src/lib/foreign-field.unit-test.ts b/src/lib/foreign-field.unit-test.ts index 5686b3ef49..26f85a5699 100644 --- a/src/lib/foreign-field.unit-test.ts +++ b/src/lib/foreign-field.unit-test.ts @@ -84,8 +84,8 @@ equivalent({ from: [f, f], to: f })( (x, y) => x.div(y) ); -// equality -equivalent({ from: [f, f], to: bool })( +// equality with a constant +equivalent({ from: [f, first(f)], to: bool })( (x, y) => x === y, (x, y) => x.equals(y) ); diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index b8f28764b1..78f83e6360 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -3,6 +3,7 @@ import { Ecdsa, EllipticCurve, Point, + initialAggregator, verifyEcdsaConstant, } from './elliptic-curve.js'; import { Field3 } from './foreign-field.js'; @@ -12,6 +13,7 @@ import { ZkProgram } from '../proof_system.js'; import { assert } from './common.js'; import { foreignField, uniformForeignField } from './test-utils.js'; import { + First, Second, bool, equivalentProvable, @@ -53,30 +55,48 @@ for (let Curve of curves) { // provable method we want to test const verify = (s: Second) => { + // invalid public key can lead to either a failing constraint, or verify() returning false + EllipticCurve.assertOnCurve(s.publicKey, Curve); return Ecdsa.verify(Curve, s.signature, s.msg, s.publicKey); }; + // input validation equivalent to the one implicit in verify() + const checkInputs = ({ + signature: { r, s }, + publicKey, + }: First) => { + assert(r !== 0n && s !== 0n, 'invalid signature'); + let pk = Curve.fromNonzero(publicKey); + assert(Curve.isOnCurve(pk), 'invalid public key'); + return true; + }; + // positive test - equivalentProvable({ from: [signature], to: bool })( + equivalentProvable({ from: [signature], to: bool, verbose: true })( () => true, verify, - 'valid signature verifies' + `${Curve.name}: verifies` ); // negative test - equivalentProvable({ from: [badSignature], to: bool })( - () => false, + equivalentProvable({ from: [badSignature], to: bool, verbose: true })( + (s) => checkInputs(s) && false, verify, - 'invalid signature fails' + `${Curve.name}: fails` ); // test against constant implementation, with both invalid and valid signatures - equivalentProvable({ from: [oneOf(signature, badSignature)], to: bool })( + equivalentProvable({ + from: [oneOf(signature, badSignature)], + to: bool, + verbose: true, + })( ({ signature, publicKey, msg }) => { + checkInputs({ signature, publicKey, msg }); return verifyEcdsaConstant(Curve, signature, msg, publicKey); }, verify, - 'verify' + `${Curve.name}: verify` ); } @@ -96,7 +116,7 @@ let msgHash = 0x3e91cd8bd233b3df4e4762b329e2922381da770df1b31276ec77d0557be7fcefn ); -const ia = EllipticCurve.initialAggregator(Secp256k1); +const ia = initialAggregator(Secp256k1); const config = { G: { windowSize: 4 }, P: { windowSize: 3 }, ia }; let program = ZkProgram({ @@ -137,14 +157,7 @@ console.time('ecdsa verify (build constraint system)'); let cs = program.analyzeMethods().ecdsa; console.timeEnd('ecdsa verify (build constraint system)'); -let gateTypes: Record = {}; -gateTypes['Total rows'] = cs.rows; -for (let gate of cs.gates) { - gateTypes[gate.type] ??= 0; - gateTypes[gate.type]++; -} - -console.log(gateTypes); +console.log(cs.summary()); console.time('ecdsa verify (compile)'); await program.compile(); diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index c1ff875509..426ce43aa3 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -24,13 +24,16 @@ import { arrayGet } from './basic.js'; export { EllipticCurve, Point, Ecdsa }; // internal API -export { verifyEcdsaConstant }; +export { verifyEcdsaConstant, initialAggregator, simpleMapToCurve }; const EllipticCurve = { add, double, + negate, + assertOnCurve, + scale, + assertInSubgroup, multiScalarMul, - initialAggregator, }; /** @@ -47,9 +50,10 @@ namespace Ecdsa { export type signature = { r: bigint; s: bigint }; } -function add(p1: Point, p2: Point, f: bigint) { +function add(p1: Point, p2: Point, Curve: { modulus: bigint }) { let { x: x1, y: y1 } = p1; let { x: x2, y: y2 } = p2; + let f = Curve.modulus; // constant case if (Point.isConstant(p1) && Point.isConstant(p2)) { @@ -92,8 +96,9 @@ function add(p1: Point, p2: Point, f: bigint) { return { x: x3, y: y3 }; } -function double(p1: Point, f: bigint, a: bigint) { +function double(p1: Point, Curve: { modulus: bigint; a: bigint }) { let { x: x1, y: y1 } = p1; + let f = Curve.modulus; // constant case if (Point.isConstant(p1)) { @@ -125,7 +130,8 @@ function double(p1: Point, f: bigint, a: bigint) { // 2*y1*m = 3*x1x1 + a let y1Times2 = ForeignField.Sum(y1).add(y1); let x1x1Times3PlusA = ForeignField.Sum(x1x1).add(x1x1).add(x1x1); - if (a !== 0n) x1x1Times3PlusA = x1x1Times3PlusA.add(Field3.from(a)); + if (Curve.a !== 0n) + x1x1Times3PlusA = x1x1Times3PlusA.add(Field3.from(Curve.a)); ForeignField.assertMul(y1Times2, m, x1x1Times3PlusA, f); // m^2 = 2*x1 + x3 @@ -140,11 +146,59 @@ function double(p1: Point, f: bigint, a: bigint) { return { x: x3, y: y3 }; } +function negate({ x, y }: Point, Curve: { modulus: bigint }) { + return { x, y: ForeignField.negate(y, Curve.modulus) }; +} + +function assertOnCurve( + p: Point, + { modulus: f, a, b }: { modulus: bigint; b: bigint; a: bigint } +) { + let { x, y } = p; + let x2 = ForeignField.mul(x, x, f); + let y2 = ForeignField.mul(y, y, f); + let y2MinusB = ForeignField.Sum(y2).sub(Field3.from(b)); + + // (x^2 + a) * x = y^2 - b + let x2PlusA = ForeignField.Sum(x2); + if (a !== 0n) x2PlusA = x2PlusA.add(Field3.from(a)); + let message: string | undefined; + if (Point.isConstant(p)) { + message = `assertOnCurve(): (${x}, ${y}) is not on the curve.`; + } + ForeignField.assertMul(x2PlusA, x, y2MinusB, f, message); +} + +/** + * EC scalar multiplication, `scalar*point` + * + * The result is constrained to be not zero. + */ +function scale( + scalar: Field3, + point: Point, + Curve: CurveAffine, + config: { + mode?: 'assert-nonzero' | 'assert-zero'; + windowSize?: number; + multiples?: Point[]; + } = { mode: 'assert-nonzero' } +) { + config.windowSize ??= Point.isConstant(point) ? 4 : 3; + return multiScalarMul([scalar], [point], Curve, [config], config.mode); +} + +// checks whether the elliptic curve point g is in the subgroup defined by [order]g = 0 +function assertInSubgroup(p: Point, Curve: CurveAffine) { + if (!Curve.hasCofactor) return; + scale(Field3.from(Curve.order), p, Curve, { mode: 'assert-zero' }); +} + // check whether a point equals a constant point // TODO implement the full case of two vars -function equals(p1: Point, p2: point, f: bigint) { - let xEquals = ForeignField.equals(p1.x, p2.x, f); - let yEquals = ForeignField.equals(p1.y, p2.y, f); +function equals(p1: Point, p2: point, Curve: { modulus: bigint }) { + let xEquals = ForeignField.equals(p1.x, p2.x, Curve.modulus); + let yEquals = ForeignField.equals(p1.y, p2.y, Curve.modulus); return xEquals.and(yEquals); } @@ -202,10 +256,11 @@ function verifyEcdsa( let G = Point.from(Curve.one); let R = multiScalarMul( - Curve, [u1, u2], [G, publicKey], + Curve, config && [config.G, config.P], + 'assert-nonzero', config?.ia ); // this ^ already proves that R != 0 (part of ECDSA verification) @@ -225,9 +280,14 @@ function verifyEcdsa( * * s_0 * P_0 + ... + s_(n-1) * P_(n-1) * - * where P_i are any points. The result is not allowed to be zero. + * where P_i are any points. + * + * By default, we prove that the result is not zero. + * + * If you set the `mode` parameter to `'assert-zero'`, on the other hand, + * we assert that the result is zero and just return the constant zero point. * - * We double all points together and leverage a precomputed table of size 2^c to avoid all but every cth addition. + * Implementation: We double all points together and leverage a precomputed table of size 2^c to avoid all but every cth addition. * * Note: this algorithm targets a small number of points, like 2 needed for ECDSA verification. * @@ -236,13 +296,14 @@ function verifyEcdsa( * TODO: glv trick which cuts down ec doubles by half by splitting s*P = s0*P + s1*endo(P) with s0, s1 in [0, 2^128) */ function multiScalarMul( - Curve: CurveAffine, scalars: Field3[], points: Point[], + Curve: CurveAffine, tableConfigs: ( | { windowSize?: number; multiples?: Point[] } | undefined )[] = [], + mode: 'assert-nonzero' | 'assert-zero' = 'assert-nonzero', ia?: point ): Point { let n = points.length; @@ -258,6 +319,11 @@ function multiScalarMul( for (let i = 0; i < n; i++) { sum = Curve.add(sum, Curve.scale(P[i], s[i])); } + if (mode === 'assert-zero') { + assert(sum.infinity, 'scalar multiplication: expected zero result'); + return Point.from(Curve.zero); + } + assert(!sum.infinity, 'scalar multiplication: expected non-zero result'); return Point.from(sum); } @@ -289,7 +355,7 @@ function multiScalarMul( : arrayGetGeneric(Point.provable, tables[j], sj); // ec addition - let added = add(sum, sjP, Curve.modulus); + let added = add(sum, sjP, Curve); // handle degenerate case (if sj = 0, Gj is all zeros and the add result is garbage) sum = Provable.if(sj.equals(0), Point.provable, sum, added); @@ -300,16 +366,22 @@ function multiScalarMul( // jointly double all points // (note: the highest couple of bits will not create any constraints because sum is constant; no need to handle that explicitly) - sum = double(sum, Curve.modulus, Curve.a); + sum = double(sum, Curve); } // the sum is now 2^(b-1)*IA + sum_i s_i*P_i // we assert that sum != 2^(b-1)*IA, and add -2^(b-1)*IA to get our result let iaFinal = Curve.scale(Curve.fromNonzero(ia), 1n << BigInt(b - 1)); - let isZero = equals(sum, iaFinal, Curve.modulus); - isZero.assertFalse(); - - sum = add(sum, Point.from(Curve.negate(iaFinal)), Curve.modulus); + let isZero = equals(sum, iaFinal, Curve); + + if (mode === 'assert-nonzero') { + isZero.assertFalse(); + sum = add(sum, Point.from(Curve.negate(iaFinal)), Curve); + } else { + isZero.assertTrue(); + // for type consistency with the 'assert-nonzero' case + sum = Point.from(Curve.zero); + } return sum; } @@ -374,10 +446,10 @@ function getPointTable( table = [Point.from(Curve.zero), P]; if (n === 2) return table; - let Pi = double(P, Curve.modulus, Curve.a); + let Pi = double(P, Curve); table.push(Pi); for (let i = 3; i < n; i++) { - Pi = add(Pi, P, Curve.modulus); + Pi = add(Pi, P, Curve); table.push(Pi); } return table; @@ -405,6 +477,22 @@ function initialAggregator(Curve: CurveAffine) { // use that as x coordinate const F = Curve.Field; let x = F.mod(bytesToBigInt(bytes)); + return simpleMapToCurve(x, Curve); +} + +function random(Curve: CurveAffine) { + let x = Curve.Field.random(); + return simpleMapToCurve(x, Curve); +} + +/** + * Given an x coordinate (base field element), increment it until we find one with + * a y coordinate that satisfies the curve equation, and return the point. + * + * If the curve has a cofactor, multiply by it to get a point in the correct subgroup. + */ +function simpleMapToCurve(x: bigint, Curve: CurveAffine) { + const F = Curve.Field; let y: bigint | undefined = undefined; // increment x until we find a y coordinate @@ -415,7 +503,13 @@ function initialAggregator(Curve: CurveAffine) { let y2 = F.add(x3, F.mul(Curve.a, x) + Curve.b); y = F.sqrt(y2); } - return { x, y, infinity: false }; + let p = { x, y, infinity: false }; + + // clear cofactor + if (Curve.hasCofactor) { + p = Curve.scale(p, Curve.cofactor!); + } + return p; } /** @@ -545,6 +639,13 @@ const Point = { }, isConstant: (P: Point) => Provable.isConstant(Point.provable, P), + /** + * Random point on the curve. + */ + random(Curve: CurveAffine) { + return Point.from(random(Curve)); + }, + provable: provable({ x: Field3.provable, y: Field3.provable }), }; diff --git a/src/lib/gadgets/elliptic-curve.unit-test.ts b/src/lib/gadgets/elliptic-curve.unit-test.ts new file mode 100644 index 0000000000..39a6d41ce0 --- /dev/null +++ b/src/lib/gadgets/elliptic-curve.unit-test.ts @@ -0,0 +1,82 @@ +import { CurveParams } from '../../bindings/crypto/elliptic-curve-examples.js'; +import { createCurveAffine } from '../../bindings/crypto/elliptic_curve.js'; +import { + array, + equivalentProvable, + map, + onlyIf, + spec, + unit, +} from '../testing/equivalent.js'; +import { Random } from '../testing/random.js'; +import { assert } from './common.js'; +import { EllipticCurve, Point, simpleMapToCurve } from './elliptic-curve.js'; +import { foreignField, throwError } from './test-utils.js'; + +// provable equivalence tests +const Secp256k1 = createCurveAffine(CurveParams.Secp256k1); +const Pallas = createCurveAffine(CurveParams.Pallas); +const Vesta = createCurveAffine(CurveParams.Vesta); +let curves = [Secp256k1, Pallas, Vesta]; + +for (let Curve of curves) { + // prepare test inputs + let field = foreignField(Curve.Field); + let scalar = foreignField(Curve.Scalar); + + // point shape, but with independently random components, which will never form a valid point + let badPoint = spec({ + rng: Random.record({ + x: field.rng, + y: field.rng, + infinity: Random.constant(false), + }), + there: Point.from, + back: Point.toBigint, + provable: Point.provable, + }); + + // valid random point + let point = map({ from: field, to: badPoint }, (x) => + simpleMapToCurve(x, Curve) + ); + + // two random points that are not equal, so are a valid input to EC addition + let unequalPair = onlyIf(array(point, 2), ([p, q]) => !Curve.equal(p, q)); + + // test ec gadgets witness generation + + equivalentProvable({ from: [unequalPair], to: point, verbose: true })( + ([p, q]) => Curve.add(p, q), + ([p, q]) => EllipticCurve.add(p, q, Curve), + `${Curve.name} add` + ); + + equivalentProvable({ from: [point], to: point, verbose: true })( + Curve.double, + (p) => EllipticCurve.double(p, Curve), + `${Curve.name} double` + ); + + equivalentProvable({ from: [point], to: point, verbose: true })( + Curve.negate, + (p) => EllipticCurve.negate(p, Curve), + `${Curve.name} negate` + ); + + equivalentProvable({ from: [point], to: unit, verbose: true })( + (p) => Curve.isOnCurve(p) || throwError('expect on curve'), + (p) => EllipticCurve.assertOnCurve(p, Curve), + `${Curve.name} on curve` + ); + + equivalentProvable({ from: [point, scalar], to: point, verbose: true })( + (p, s) => { + let sp = Curve.scale(p, s); + assert(!sp.infinity, 'expect nonzero'); + return sp; + }, + (p, s) => EllipticCurve.scale(s, p, Curve), + `${Curve.name} scale` + ); +} diff --git a/src/lib/gadgets/foreign-field.ts b/src/lib/gadgets/foreign-field.ts index 44b23a3c3b..51b815dfc8 100644 --- a/src/lib/gadgets/foreign-field.ts +++ b/src/lib/gadgets/foreign-field.ts @@ -232,7 +232,8 @@ function assertMulInternal( x: Field3, y: Field3, xy: Field3 | Field2, - f: bigint + f: bigint, + message?: string ) { let { r01, r2, q } = multiplyNoRangeCheck(x, y, f); @@ -242,12 +243,12 @@ function assertMulInternal( // bind remainder to input xy if (xy.length === 2) { let [xy01, xy2] = xy; - r01.assertEquals(xy01); - r2.assertEquals(xy2); + r01.assertEquals(xy01, message); + r2.assertEquals(xy2, message); } else { let xy01 = xy[0].add(xy[1].mul(1n << l)); - r01.assertEquals(xy01); - r2.assertEquals(xy[2]); + r01.assertEquals(xy01, message); + r2.assertEquals(xy[2], message); } } @@ -506,7 +507,8 @@ function assertMul( x: Field3 | Sum, y: Field3 | Sum, xy: Field3 | Sum, - f: bigint + f: bigint, + message?: string ) { x = Sum.fromUnfinished(x); y = Sum.fromUnfinished(y); @@ -537,11 +539,14 @@ function assertMul( let x_ = Field3.toBigint(x0); let y_ = Field3.toBigint(y0); let xy_ = Field3.toBigint(xy0); - assert(mod(x_ * y_, f) === xy_, 'incorrect multiplication result'); + assert( + mod(x_ * y_, f) === xy_, + message ?? 'assertMul(): incorrect multiplication result' + ); return; } - assertMulInternal(x0, y0, xy0, f); + assertMulInternal(x0, y0, xy0, f, message); } class Sum { diff --git a/src/lib/group.ts b/src/lib/group.ts index 89cf5bf249..6178032df6 100644 --- a/src/lib/group.ts +++ b/src/lib/group.ts @@ -2,7 +2,7 @@ import { Field, FieldVar, isField } from './field.js'; import { Scalar } from './scalar.js'; import { Snarky } from '../snarky.js'; import { Field as Fp } from '../provable/field-bigint.js'; -import { Pallas } from '../bindings/crypto/elliptic_curve.js'; +import { GroupAffine, Pallas } from '../bindings/crypto/elliptic_curve.js'; import { Provable } from './provable.js'; import { Bool } from './bool.js'; @@ -73,15 +73,7 @@ class Group { } // helpers - static #fromAffine({ - x, - y, - infinity, - }: { - x: bigint; - y: bigint; - infinity: boolean; - }) { + static #fromAffine({ x, y, infinity }: GroupAffine) { return infinity ? Group.zero : new Group({ x, y }); } diff --git a/src/lib/provable-context.ts b/src/lib/provable-context.ts index 0ce0030556..3b79562889 100644 --- a/src/lib/provable-context.ts +++ b/src/lib/provable-context.ts @@ -1,5 +1,5 @@ import { Context } from './global-context.js'; -import { Gate, JsonGate, Snarky } from '../snarky.js'; +import { Gate, GateType, JsonGate, Snarky } from '../snarky.js'; import { parseHexString } from '../bindings/crypto/bigint-helpers.js'; import { prettifyStacktrace } from './errors.js'; import { Fp } from '../bindings/crypto/finite_field.js'; @@ -105,6 +105,15 @@ function constraintSystem(f: () => T) { print() { printGates(gates); }, + summary() { + let gateTypes: Partial> = {}; + gateTypes['Total rows'] = rows; + for (let gate of gates) { + gateTypes[gate.type] ??= 0; + gateTypes[gate.type]!++; + } + return gateTypes; + }, }; } catch (error) { throw prettifyStacktrace(error); diff --git a/src/lib/provable.ts b/src/lib/provable.ts index 4cf97b4f8a..534818f16f 100644 --- a/src/lib/provable.ts +++ b/src/lib/provable.ts @@ -3,7 +3,6 @@ * - a namespace with tools for writing provable code * - the main interface for types that can be used in provable code */ -import { FieldVar } from './field.js'; import { Field, Bool } from './core.js'; import { Provable as Provable_, Snarky } from '../snarky.js'; import type { FlexibleProvable, ProvableExtended } from './circuit_value.js'; @@ -127,7 +126,8 @@ const Provable = { * @example * ```ts * let x = Field(42); - * Provable.isConstant(x); // true + * Provable.isConstant(Field, x); // true + * ``` */ isConstant, /** diff --git a/src/lib/testing/constraint-system.ts b/src/lib/testing/constraint-system.ts index 2922afa11d..1f8ad9ba52 100644 --- a/src/lib/testing/constraint-system.ts +++ b/src/lib/testing/constraint-system.ts @@ -389,7 +389,7 @@ function drawFieldVar(): FieldVar { let fieldType = drawFieldType(); switch (fieldType) { case FieldType.Constant: { - return FieldVar.constant(17n); + return FieldVar.constant(1n); } case FieldType.Var: { return [FieldType.Var, 0]; @@ -397,10 +397,14 @@ function drawFieldVar(): FieldVar { case FieldType.Add: { let x = drawFieldVar(); let y = drawFieldVar(); + // prevent blow-up of constant size + if (x[0] === FieldType.Constant && y[0] === FieldType.Constant) return x; return FieldVar.add(x, y); } case FieldType.Scale: { let x = drawFieldVar(); + // prevent blow-up of constant size + if (x[0] === FieldType.Constant) return x; return FieldVar.scale(3n, x); } } diff --git a/src/lib/testing/equivalent.ts b/src/lib/testing/equivalent.ts index 2eb9ba305f..2e8384d7da 100644 --- a/src/lib/testing/equivalent.ts +++ b/src/lib/testing/equivalent.ts @@ -6,6 +6,8 @@ import { Provable } from '../provable.js'; import { deepEqual } from 'node:assert/strict'; import { Bool, Field } from '../core.js'; import { AnyFunction, Tuple } from '../util/types.js'; +import { provable } from '../circuit_value.js'; +import { assert } from '../gadgets/common.js'; export { equivalent, @@ -28,6 +30,7 @@ export { array, record, map, + onlyIf, fromRandom, first, second, @@ -184,11 +187,17 @@ function equivalentAsync< // equivalence tester for provable code +function isProvable(spec: FromSpecUnion) { + return spec.specs.some((spec) => spec.provable); +} + function equivalentProvable< In extends Tuple>, Out extends ToSpec ->({ from: fromRaw, to }: { from: In; to: Out }) { +>({ from: fromRaw, to, verbose }: { from: In; to: Out; verbose?: boolean }) { let fromUnions = fromRaw.map(toUnion); + assert(fromUnions.some(isProvable), 'equivalentProvable: no provable input'); + return function run( f1: (...args: Params1) => First, f2: (...args: Params2) => Second, @@ -196,7 +205,9 @@ function equivalentProvable< ) { let generators = fromUnions.map((spec) => spec.rng); let assertEqual = to.assertEqual ?? deepEqual; - test(...generators, (...args) => { + + let start = performance.now(); + let nRuns = test.custom({ minRuns: 5 })(...generators, (...args) => { args.pop(); // figure out which spec to use for each argument @@ -227,10 +238,18 @@ function equivalentProvable< handleErrors( () => f1(...inputs), () => f2(...inputWitnesses), - (x, y) => Provable.asProver(() => assertEqual(x, to.back(y), label)) + (x, y) => Provable.asProver(() => assertEqual(x, to.back(y), label)), + label ); }); }); + if (verbose) { + let ms = (performance.now() - start).toFixed(1); + let runs = nRuns.toString().padStart(2, ' '); + console.log( + `${label.padEnd(20, ' ')} success on ${runs} runs in ${ms}ms.` + ); + } }; } @@ -329,10 +348,14 @@ function record }>( { [k in keyof Specs]: First }, { [k in keyof Specs]: Second } > { + let isProvable = Object.values(specs).every((spec) => spec.provable); return { rng: Random.record(mapObject(specs, (spec) => spec.rng)) as any, there: (x) => mapObject(specs, (spec, k) => spec.there(x[k])) as any, back: (x) => mapObject(specs, (spec, k) => spec.back(x[k])) as any, + provable: isProvable + ? provable(mapObject(specs, (spec) => spec.provable) as any) + : undefined, }; } @@ -343,6 +366,10 @@ function map( return { ...to, rng: Random.map(from.rng, there) }; } +function onlyIf(spec: Spec, onlyIf: (t: T) => boolean): Spec { + return { ...spec, rng: Random.reject(spec.rng, (x) => !onlyIf(x)) }; +} + function mapObject( t: { [k in K]: T }, map: (t: T, k: K) => S diff --git a/tests/vk-regression/vk-regression.json b/tests/vk-regression/vk-regression.json index f60540c9f8..ee63f91f3a 100644 --- a/tests/vk-regression/vk-regression.json +++ b/tests/vk-regression/vk-regression.json @@ -201,5 +201,18 @@ "data": "", "hash": "" } + }, + "ecdsa": { + "digest": "2113edb508f10afee42dd48aec81ac7d06805d76225b0b97300501136486bb30", + "methods": { + "verifyEcdsa": { + "rows": 38888, + "digest": "f75dd9e49c88eb6097a7f3abbe543467" + } + }, + "verificationKey": { + "data": "AAAdmtvKeZvyx7UyPW6rIhb96GnTZEEywf8pGpbkt+QXNIm7oWxIDWYa4EWTiadEqzk8WXg3wiZmbXWcqBQU+uIoTiBnYTRcd7RsaAjbdbIbQJ9EuopFRFewZRx9qeQeEibNeMRcRMP4LdfS3AQRxhFZzN4HFa4MbtGs+Aja820cI9VFULH2/7BvD6JjpVWjVLvvo6zhO3S5axqfDh7QqtkPo3TLpand9OVvMHhTVlz/AV7rus5E/+0cv50MaEJ/wBfUh5XNLAlGgVi7FfWR6p9P72AAymyD3lUdecJyZmCREiVgPrTdFppkp45TefJWNTySkV9c5YzpNxQoXedZDvYP/5s4KBkfIeK+zB2yJC9eZ1ZDYfM88shGDYxmBtur9AkQ49QGquR+kYUI0lpXtuNMG+ZRy0FRJ8ci/TE+PIPIFnSiGcSOA3YM2G171LYf89abU2QUoQRHSP3PmmAOy/8CoRLVro7Nl6z/Ou0oZzX7RjOEo//LBqcSWa2S9X8TQz0R3uivbovTdq0rrba56SbEnK6LWItmBc6CubYWL7UzDbD3RZM6iRz1hqTHDzDz7UIWOzHgLqW9rjnZllQCyfsSAHV05f+qac/ZBDmwqzaprv0J0hiho1m+s3yNkKQVSOkFyy3T9xnMBFjK62dF1KOp2k1Uvadd2KRyqwXiGN7JtQwgKzcNZhhPW5VfbcSYDpx5nVaU5pTEFl+2+RlcuhBpG1ksAWbD64AUKDjdyTWIC5Wn68AagPtG65V13eFS5LgkSfVtNXxGodg7SdP4AJmXpBgZfzMg4RW6Qje5ZFfrwRzoHPo0y7nO1hkaNLGV3Wvd3/pYiebXvyo+DdTZmaMbJpJaGSCysnovOrVUIpcn4h1hvA12jztQFQcbNHoVeZgslPxA54y9ynjhN7VZfT8lNXXIrRCpmPaxZW6Bw6Op/g6P1Y8pKZHixBy1UrxqWGI+49oRtRFGw9CWS21EekuBFeu9RKI6yZLDiyRC2b3koFG+Kp6oq5Ej6Q8uargE09Ag9D9DKKoexOqr3N/Z3GGptvh3qvOPyxcWf475b+B/fTIwTQQC8ykkZ35HAVW3ZT6XDz0QFSmB0NJ8A+lkaTa0JF46ddCU9VJ1JmYsYa+MYEgKjZCvABbX9AEY2ggMr1cHaA49GrGul+Sj6pAvz4oyzaR8m7WAPMDuBtVwdbDtfju3CVbhX15uBoKhuYWQgLr2rnVJ5SOZoDvlwJtcK2izLMYVAasejw4fvsehYGb88wvDbFxS6sM9gDSgTlavZRs95Qf+c1KpYf/jb8BxYNrwrqy8F++c1APDzfzQ/IbVLiaL28wkEy412qmXSjM+9hErKXFy8JIT/WBOIWMMg/7mMu1Al7Tt/kOZrDznlS/szLlpAp2jISa8VWCmlAEPrustdNqQvptSsF6hikzXZVXg5f8pU4Gpa0TP0TRFvIYfmTyl8HpdFOG7vpQavC600YgzS2YGtY7K2WQ5GtN5ZTZBHPsUSir2yKSo9Le9CWXbDtn3SBDepWypwDa3YWKtNog+y10VmpL1N+RG3u1DXSuY7y9WZgkQ7tdvyx/Gjr91kjF0s3bt7vHIAZCtzNlRlWDBz3og0cSnEucCEuKR6dL2Mz+RuF1GmLoXZXapUjVG/82BjdAMAOxPlE67lEs+JWgnrVrA5NLJoL4DZ6+fhQKpNfk0uOrEfZIWR9Sau0IBwBxu6IYVm5/XAB19dt8MAuVcRdN/JGGzo0Hr3WVJuKzbAhuFwJZzcd1J1n4xO09ECT5NQdFSFXGsy8kIFjRNEOkLl+bAExePtGCt0w6cYqB0uCeX3lTI7ugIEgdStMtHFiWngJ218l8CuVrkwTJ7ZqHLtuJDiNqlLptkHWChDfw+IgDwz85dZrfBBzQrMRWranxQmisM+wx3vC+pLURRQHZJEasGCAElj0lTColrqQ/cXS7cBaqs1tBsQDGzKYMCMwsqL53fyxGCljVvljBa99+FpYfoUK+Fi0z6uEbem+luXRScr2yPB5I08lnBY23RmBb/pfSyBfbcmnmF5BkRlJTJKY7fQL/t9bFfywoquQe9e7OQvIjppA/FO7HmZS6hoOU+eS8+W94fEF2gvrowpTeqQHM6hLN9Qzl8niwZWUIyRCfyuzQnuSz/VP1K2sMFBKnZZNDcuBh1/xSFymOH6LfNKostvc6qHTIxrTjlH6952bo1bQl+mVvBUaJuRkYh12QbcyIyzcBFUYwaFazzkHXMof0O30oL3Q6wegTvJxTSZD5VCr5D26Myzoa0JBpqL0st9/MNGZe5a/+HW1qan/VtGA5nYkJcUzwKVqqlmZeuOZekFLGxlfp0lv9IQUQWtiU5uvd5HVoolEc/teUnx/IxYe01IDxX9cbmPMJnLYXJGSY=", + "hash": "10504586047480864396273137275551599454708712068910013426206550544367939284599" + } } } \ No newline at end of file diff --git a/tests/vk-regression/vk-regression.ts b/tests/vk-regression/vk-regression.ts index 28a95542bd..5d2c6d71e9 100644 --- a/tests/vk-regression/vk-regression.ts +++ b/tests/vk-regression/vk-regression.ts @@ -3,6 +3,7 @@ import { Voting_ } from '../../src/examples/zkapps/voting/voting.js'; import { Membership_ } from '../../src/examples/zkapps/voting/membership.js'; import { HelloWorld } from '../../src/examples/zkapps/hello_world/hello_world.js'; import { TokenContract, createDex } from '../../src/examples/zkapps/dex/dex.js'; +import { ecdsaProgram } from '../../src/examples/crypto/ecdsa/ecdsa.js'; import { GroupCS, BitwiseCS } from './plain-constraint-system.js'; // toggle this for quick iteration when debugging vk regressions @@ -38,6 +39,7 @@ const ConstraintSystems: MinimumConstraintSystem[] = [ createDex().Dex, GroupCS, BitwiseCS, + ecdsaProgram, ]; let filePath = jsonPath ? jsonPath : './tests/vk-regression/vk-regression.json';