From 6c34c9f4074da828003e2595c778def85c7a320e Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 Nov 2023 16:37:33 +0100 Subject: [PATCH 001/102] double --- src/lib/gadgets/elliptic-curve.ts | 75 ++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index b52eda448..4539b8ed3 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -5,6 +5,7 @@ import { Provable } from '../provable.js'; import { exists } from './common.js'; import { Field3, + ForeignField, Sum, assertRank1, bigint3, @@ -40,9 +41,9 @@ function add({ x: x1, y: y1 }: Point, { x: x2, y: y2 }: Point, f: bigint) { multiRangeCheck(m); multiRangeCheck(x3); multiRangeCheck(y3); - let m2Bound = weakBound(m[2], f); + let mBound = weakBound(m[2], f); let x3Bound = weakBound(x3[2], f); - // we dont need to bounds-check y3[2] because it's never one of the inputs to a multiplication + // we dont need to bound y3[2] because it's never one of the inputs to a multiplication // (x1 - x2)*m = y1 - y2 let deltaX = new Sum(x1).sub(x2); @@ -59,11 +60,61 @@ function add({ x: x1, y: y1 }: Point, { x: x2, y: y2 }: Point, f: bigint) { let qBound3 = assertRank1(deltaX1X3, m, ySum, f); // bounds checks - multiRangeCheck([m2Bound, x3Bound, qBound1]); + multiRangeCheck([mBound, x3Bound, qBound1]); multiRangeCheck([qBound2, qBound3, Field.from(0n)]); } -let cs = Provable.constraintSystem(() => { +function double({ x: x1, y: y1 }: Point, f: bigint) { + // TODO constant case + + // witness and range-check slope, x3, y3 + let witnesses = exists(9, () => { + let [x1_, y1_] = Field3.toBigints(x1, y1); + let denom = inverse(mod(2n * y1_, f), f); + + let m = denom !== undefined ? mod(3n * mod(x1_ ** 2n, f) * denom, f) : 0n; + let m2 = mod(m * m, f); + let x3 = mod(m2 - 2n * x1_, f); + let y3 = mod(m * (x1_ - x3) - y1_, f); + + return [...split(m), ...split(x3), ...split(y3)]; + }); + let [m0, m1, m2, x30, x31, x32, y30, y31, y32] = witnesses; + let m: Field3 = [m0, m1, m2]; + let x3: Field3 = [x30, x31, x32]; + let y3: Field3 = [y30, y31, y32]; + + multiRangeCheck(m); + multiRangeCheck(x3); + multiRangeCheck(y3); + let mBound = weakBound(m[2], f); + let x3Bound = weakBound(x3[2], f); + // we dont need to bound y3[2] because it's never one of the inputs to a multiplication + + // x1^2 = x1x1 + let x1x1 = ForeignField.mul(x1, x1, f); + + // 2*y1*m = 3*x1x1 + // TODO this assumes the curve has a == 0 + let y1Times2 = new Sum(y1).add(y1); + let x1x1Times3 = new Sum(x1x1).add(x1x1).add(x1x1); + let qBound1 = assertRank1(y1Times2, m, x1x1Times3, f); + + // m^2 = 2*x1 + x3 + let xSum = new Sum(x1).add(x1).add(x3); + let qBound2 = assertRank1(m, m, xSum, f); + + // (x1 - x3)*m = y1 + y3 + let deltaX1X3 = new Sum(x1).sub(x3); + let ySum = new Sum(y1).add(y3); + let qBound3 = assertRank1(deltaX1X3, m, ySum, f); + + // bounds checks + multiRangeCheck([mBound, x3Bound, qBound1]); + multiRangeCheck([qBound2, qBound3, Field.from(0n)]); +} + +let csAdd = Provable.constraintSystem(() => { let x1 = Provable.witness(Field3.provable, () => Field3.from(0n)); let x2 = Provable.witness(Field3.provable, () => Field3.from(0n)); let y1 = Provable.witness(Field3.provable, () => Field3.from(0n)); @@ -75,5 +126,17 @@ let cs = Provable.constraintSystem(() => { add(g, h, exampleFields.secp256k1.modulus); }); -printGates(cs.gates); -console.log({ digest: cs.digest, rows: cs.rows }); +let csDouble = Provable.constraintSystem(() => { + let x1 = Provable.witness(Field3.provable, () => Field3.from(0n)); + let y1 = Provable.witness(Field3.provable, () => Field3.from(0n)); + + let g = { x: x1, y: y1 }; + + double(g, exampleFields.secp256k1.modulus); +}); + +printGates(csAdd.gates); +console.log({ digest: csAdd.digest, rows: csAdd.rows }); + +printGates(csDouble.gates); +console.log({ digest: csDouble.digest, rows: csDouble.rows }); From d41f30f2a9dfbd7cfb0a27c5f6a583d986cdfd6b Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 Nov 2023 17:24:01 +0100 Subject: [PATCH 002/102] tweak cs printing --- src/lib/testing/constraint-system.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/testing/constraint-system.ts b/src/lib/testing/constraint-system.ts index 55c0cabae..048cddeec 100644 --- a/src/lib/testing/constraint-system.ts +++ b/src/lib/testing/constraint-system.ts @@ -445,9 +445,7 @@ function wiresToPretty(wires: Gate['wires'], row: number) { if (wire.row === row) { strWires.push(`${col}->${wire.col}`); } else { - let rowDelta = wire.row - row; - let rowStr = rowDelta > 0 ? `+${rowDelta}` : `${rowDelta}`; - strWires.push(`${col}->(${rowStr},${wire.col})`); + strWires.push(`${col}->(${wire.row},${wire.col})`); } } return strWires.join(', '); From 4c3f6a5fa5ac733bf7eff1c35fae08afae131c58 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 Nov 2023 17:24:11 +0100 Subject: [PATCH 003/102] initial aggregator for scaling --- src/lib/gadgets/elliptic-curve.ts | 43 +++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 4539b8ed3..5f1c9989e 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -1,8 +1,12 @@ -import { inverse, mod } from '../../bindings/crypto/finite_field.js'; +import { + FiniteField, + inverse, + mod, +} from '../../bindings/crypto/finite_field.js'; import { exampleFields } from '../../bindings/crypto/finite-field-examples.js'; import { Field } from '../field.js'; import { Provable } from '../provable.js'; -import { exists } from './common.js'; +import { assert, exists } from './common.js'; import { Field3, ForeignField, @@ -14,6 +18,9 @@ import { } from './foreign-field.js'; import { multiRangeCheck } from './range-check.js'; import { printGates } from '../testing/constraint-system.js'; +import { sha256 } from 'js-sha256'; +import { bytesToBigInt } from '../../bindings/crypto/bigint-helpers.js'; +import { Pallas } from '../../bindings/crypto/elliptic_curve.js'; type Point = { x: Field3; y: Field3 }; type point = { x: bigint3; y: bigint3; infinity: boolean }; @@ -114,6 +121,34 @@ function double({ x: x1, y: y1 }: Point, f: bigint) { multiRangeCheck([qBound2, qBound3, Field.from(0n)]); } +/** + * For EC scalar multiplication we use an initial point which is subtracted + * at the end, to avoid encountering the point at infinity. + * + * This is a simple hash-to-group algorithm which finds that initial point. + * It's important that this point has no known discrete logarithm so that nobody + * can create an invalid proof of EC scaling. + */ +function initialAggregator(F: FiniteField, { a, b }: { a: bigint; b: bigint }) { + let h = sha256.create(); + h.update('o1js:ecdsa'); + let bytes = h.array(); + + // bytes represent a 256-bit number + // use that as x coordinate + let x = F.mod(bytesToBigInt(bytes)); + let y: bigint | undefined = undefined; + + // increment x until we find a y coordinate + while (y === undefined) { + // solve y^2 = x^3 + ax + b + let x3 = F.mul(F.square(x), x); + let y2 = F.add(x3, F.mul(a, x) + b); + y = F.sqrt(y2); + } + return { x: F.mod(x), y, infinity: false }; +} + let csAdd = Provable.constraintSystem(() => { let x1 = Provable.witness(Field3.provable, () => Field3.from(0n)); let x2 = Provable.witness(Field3.provable, () => Field3.from(0n)); @@ -140,3 +175,7 @@ console.log({ digest: csAdd.digest, rows: csAdd.rows }); printGates(csDouble.gates); console.log({ digest: csDouble.digest, rows: csDouble.rows }); + +let point = initialAggregator(exampleFields.Fp, { a: 0n, b: 5n }); +console.log({ point }); +assert(Pallas.isOnCurve(Pallas.fromAffine(point))); From c99cee26509c7a992b72e1f76db5c08b3be66100 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 Nov 2023 18:10:37 +0100 Subject: [PATCH 004/102] another helper method on Field3 --- src/lib/gadgets/foreign-field.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/lib/gadgets/foreign-field.ts b/src/lib/gadgets/foreign-field.ts index f460bea0a..bc59eeff0 100644 --- a/src/lib/gadgets/foreign-field.ts +++ b/src/lib/gadgets/foreign-field.ts @@ -60,7 +60,7 @@ function sum(x: Field3[], sign: Sign[], f: bigint) { assert(x.length === sign.length + 1, 'inputs and operators match'); // constant case - if (x.every((x) => x.every((x) => x.isConstant()))) { + if (x.every(Field3.isConstant)) { let xBig = x.map(Field3.toBigint); let sum = sign.reduce((sum, s, i) => sum + s * xBig[i + 1], xBig[0]); return Field3.from(mod(sum, f)); @@ -122,7 +122,7 @@ function multiply(a: Field3, b: Field3, f: bigint): Field3 { assert(f < 1n << 259n, 'Foreign modulus fits in 259 bits'); // constant case - if (a.every((x) => x.isConstant()) && b.every((x) => x.isConstant())) { + if (Field3.isConstant(a) && Field3.isConstant(b)) { let ab = Field3.toBigint(a) * Field3.toBigint(b); return Field3.from(mod(ab, f)); } @@ -146,7 +146,7 @@ function inverse(x: Field3, f: bigint): Field3 { assert(f < 1n << 259n, 'Foreign modulus fits in 259 bits'); // constant case - if (x.every((x) => x.isConstant())) { + if (Field3.isConstant(x)) { let xInv = modInverse(Field3.toBigint(x), f); assert(xInv !== undefined, 'inverse exists'); return Field3.from(xInv); @@ -179,7 +179,7 @@ function divide( assert(f < 1n << 259n, 'Foreign modulus fits in 259 bits'); // constant case - if (x.every((x) => x.isConstant()) && y.every((x) => x.isConstant())) { + if (Field3.isConstant(x) && Field3.isConstant(y)) { let yInv = modInverse(Field3.toBigint(y), f); assert(yInv !== undefined, 'inverse exists'); return Field3.from(mod(Field3.toBigint(x) * yInv, f)); @@ -358,6 +358,13 @@ const Field3 = { return Tuple.map(xs, Field3.toBigint); }, + /** + * Check whether a 3-tuple of Fields is constant + */ + isConstant(x: Field3) { + return x.every((x) => x.isConstant()); + }, + /** * Provable interface for `Field3 = [Field, Field, Field]`. * From 95017b30f9202963e89645e440437e080a501d77 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 Nov 2023 18:11:17 +0100 Subject: [PATCH 005/102] add constant ecdsa implementation and helper types --- src/lib/gadgets/elliptic-curve.ts | 79 ++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 5f1c9989e..b14384480 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -20,10 +20,14 @@ import { multiRangeCheck } from './range-check.js'; import { printGates } from '../testing/constraint-system.js'; import { sha256 } from 'js-sha256'; import { bytesToBigInt } from '../../bindings/crypto/bigint-helpers.js'; -import { Pallas } from '../../bindings/crypto/elliptic_curve.js'; +import { CurveAffine, Pallas } from '../../bindings/crypto/elliptic_curve.js'; +import { Bool } from '../bool.js'; type Point = { x: Field3; y: Field3 }; -type point = { x: bigint3; y: bigint3; infinity: boolean }; +type point = { x: bigint; y: bigint; infinity: boolean }; + +type Signature = { r: Field3; s: Field3 }; +type signature = { r: bigint; s: bigint }; function add({ x: x1, y: y1 }: Point, { x: x2, y: y2 }: Point, f: bigint) { // TODO constant case @@ -121,6 +125,59 @@ function double({ x: x1, y: y1 }: Point, f: bigint) { multiRangeCheck([qBound2, qBound3, Field.from(0n)]); } +function verifyEcdsa( + Curve: CurveAffine, + signature: Signature, + msgHash: Field3, + publicKey: Point +) { + // constant case + if ( + Signature.isConstant(signature) && + Field3.isConstant(msgHash) && + Point.isConstant(publicKey) + ) { + let isValid = verifyEcdsaConstant( + Curve, + Signature.toBigint(signature), + Field3.toBigint(msgHash), + Point.toBigint(publicKey) + ); + assert(isValid, 'invalid signature'); + return; + } + + // provable case + // TODO +} + +/** + * Bigint implementation of ECDSA verify + */ +function verifyEcdsaConstant( + Curve: CurveAffine, + { r, s }: { r: bigint; s: bigint }, + msgHash: bigint, + publicKey: { x: bigint; y: bigint } +) { + let q = Curve.order; + let QA = Curve.fromNonzero(publicKey); + if (!Curve.isOnCurve(QA)) return false; + if (Curve.hasCofactor && !Curve.isInSubgroup(QA)) return false; + if (r < 1n || r >= Curve.order) return false; + if (s < 1n || s >= Curve.order) return false; + + let sInv = inverse(s, q); + if (sInv === undefined) throw Error('impossible'); + let u1 = mod(msgHash * sInv, q); + let u2 = mod(r * sInv, q); + + let X = Curve.add(Curve.scale(Curve.one, u1), Curve.scale(QA, u2)); + if (Curve.equal(X, Curve.zero)) return false; + + return mod(X.x, q) === r; +} + /** * For EC scalar multiplication we use an initial point which is subtracted * at the end, to avoid encountering the point at infinity. @@ -149,6 +206,24 @@ function initialAggregator(F: FiniteField, { a, b }: { a: bigint; b: bigint }) { return { x: F.mod(x), y, infinity: false }; } +const Point = { + toBigint({ x, y }: Point): point { + return { x: Field3.toBigint(x), y: Field3.toBigint(y), infinity: false }; + }, + isConstant({ x, y }: Point) { + return Field3.isConstant(x) && Field3.isConstant(y); + }, +}; + +const Signature = { + toBigint({ r, s }: Signature): signature { + return { r: Field3.toBigint(r), s: Field3.toBigint(s) }; + }, + isConstant({ s, r }: Signature) { + return Field3.isConstant(s) && Field3.isConstant(r); + }, +}; + let csAdd = Provable.constraintSystem(() => { let x1 = Provable.witness(Field3.provable, () => Field3.from(0n)); let x2 = Provable.witness(Field3.provable, () => Field3.from(0n)); From dd2ebd391414edb71175bde1801d0a91e6809c38 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 Nov 2023 18:11:25 +0100 Subject: [PATCH 006/102] bindings --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index c8f8c631f..9b2f1abfe 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit c8f8c631f28b84c3d3859378a2fe857091207755 +Subproject commit 9b2f1abfeb891e95ccb88e536275eb6733e67f30 From fc6f1f83db74646c47ce2780a89786663138a02e Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 Nov 2023 18:47:49 +0100 Subject: [PATCH 007/102] helper --- src/lib/gadgets/foreign-field.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/lib/gadgets/foreign-field.ts b/src/lib/gadgets/foreign-field.ts index bc59eeff0..bb948c175 100644 --- a/src/lib/gadgets/foreign-field.ts +++ b/src/lib/gadgets/foreign-field.ts @@ -365,6 +365,15 @@ const Field3 = { return x.every((x) => x.isConstant()); }, + /** + * Assert that two 3-tuples of Fields are equal + */ + assertEqual(x: Field3, y: Field3) { + x[0].assertEquals(y[0]); + x[1].assertEquals(y[1]); + x[2].assertEquals(y[2]); + }, + /** * Provable interface for `Field3 = [Field, Field, Field]`. * From 00a11f7403aeefd75add0e83ea40a1fed9366efc Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 Nov 2023 18:53:52 +0100 Subject: [PATCH 008/102] stub out provable ecdsa, skipping the hard part --- src/lib/gadgets/elliptic-curve.ts | 61 ++++++++++++++++++++++++++++++- src/lib/gadgets/foreign-field.ts | 8 ++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index b14384480..03d93d397 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -73,6 +73,8 @@ function add({ x: x1, y: y1 }: Point, { x: x2, y: y2 }: Point, f: bigint) { // bounds checks multiRangeCheck([mBound, x3Bound, qBound1]); multiRangeCheck([qBound2, qBound3, Field.from(0n)]); + + return { x: x3, y: y3 }; } function double({ x: x1, y: y1 }: Point, f: bigint) { @@ -123,13 +125,20 @@ function double({ x: x1, y: y1 }: Point, f: bigint) { // bounds checks multiRangeCheck([mBound, x3Bound, qBound1]); multiRangeCheck([qBound2, qBound3, Field.from(0n)]); + + return { x: x3, y: y3 }; } function verifyEcdsa( Curve: CurveAffine, + IA: point, signature: Signature, msgHash: Field3, - publicKey: Point + publicKey: Point, + table?: { + windowSize: number; // what we called c before + multiples?: point[]; // 0, G, 2*G, ..., (2^c-1)*G + } ) { // constant case if ( @@ -148,7 +157,45 @@ function verifyEcdsa( } // provable case - // TODO + // TODO should check that the publicKey is a valid point? probably not + + let { r, s } = signature; + let sInv = ForeignField.inv(s, Curve.order); + let u1 = ForeignField.mul(msgHash, sInv, Curve.order); + let u2 = ForeignField.mul(r, sInv, Curve.order); + + let X = varPlusFixedScalarMul(Curve, IA, u1, publicKey, u2, table); + + // assert that X != IA, and add -IA + Point.equal(X, Point.from(IA)).assertFalse(); + X = add(X, Point.from(Curve.negate(IA)), Curve.order); + + // TODO reduce X.x mod the scalar order + Field3.assertEqual(X.x, r); +} + +/** + * Scalar mul that we need for ECDSA: + * + * IA + s*P + t*G, + * + * where IA is the initial aggregator, P is any point and G is the generator. + * + * We double both points together and leverage a precomputed table + * of size 2^c to avoid all but every cth addition for t*G. + */ +function varPlusFixedScalarMul( + Curve: CurveAffine, + IA: point, + s: Field3, + P: Point, + t: Field3, + table?: { + windowSize: number; // what we called c before + multiples?: point[]; // 0, G, 2*G, ..., (2^c-1)*G + } +): Point { + throw Error('TODO'); } /** @@ -207,12 +254,22 @@ function initialAggregator(F: FiniteField, { a, b }: { a: bigint; b: bigint }) { } const Point = { + from({ x, y }: point): Point { + return { x: Field3.from(x), y: Field3.from(y) }; + }, toBigint({ x, y }: Point): point { return { x: Field3.toBigint(x), y: Field3.toBigint(y), infinity: false }; }, isConstant({ x, y }: Point) { return Field3.isConstant(x) && Field3.isConstant(y); }, + assertEqual({ x: x1, y: y1 }: Point, { x: x2, y: y2 }: Point) { + Field3.assertEqual(x1, x2); + Field3.assertEqual(y1, y2); + }, + equal({ x: x1, y: y1 }: Point, { x: x2, y: y2 }: Point) { + return Field3.equal(x1, x2).and(Field3.equal(y1, y2)); + }, }; const Signature = { diff --git a/src/lib/gadgets/foreign-field.ts b/src/lib/gadgets/foreign-field.ts index bb948c175..265859dcb 100644 --- a/src/lib/gadgets/foreign-field.ts +++ b/src/lib/gadgets/foreign-field.ts @@ -374,6 +374,14 @@ const Field3 = { x[2].assertEquals(y[2]); }, + /** + * Check whether two 3-tuples of Fields are equal + */ + equal(x: Field3, y: Field3) { + let eq01 = x[0].add(x[1].mul(1n << L)).equals(y[0].add(y[1].mul(1n << L))); + return eq01.and(x[2].equals(y[2])); + }, + /** * Provable interface for `Field3 = [Field, Field, Field]`. * From ce389efcbfa58a7a9dfae1bcb6eb6577463388c0 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 Nov 2023 21:02:26 +0100 Subject: [PATCH 009/102] revert explosion of helper methods in favor of general interface --- src/lib/gadgets/elliptic-curve.ts | 31 +++++++++++-------------------- src/lib/gadgets/foreign-field.ts | 17 ----------------- src/lib/provable.ts | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 37 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 03d93d397..707fd2f42 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -22,6 +22,7 @@ import { sha256 } from 'js-sha256'; import { bytesToBigInt } from '../../bindings/crypto/bigint-helpers.js'; import { CurveAffine, Pallas } from '../../bindings/crypto/elliptic_curve.js'; import { Bool } from '../bool.js'; +import { provable } from '../circuit_value.js'; type Point = { x: Field3; y: Field3 }; type point = { x: bigint; y: bigint; infinity: boolean }; @@ -131,7 +132,7 @@ function double({ x: x1, y: y1 }: Point, f: bigint) { function verifyEcdsa( Curve: CurveAffine, - IA: point, + ia: point, signature: Signature, msgHash: Field3, publicKey: Point, @@ -142,9 +143,9 @@ function verifyEcdsa( ) { // constant case if ( - Signature.isConstant(signature) && + Provable.isConstant(Signature, signature) && Field3.isConstant(msgHash) && - Point.isConstant(publicKey) + Provable.isConstant(Point, publicKey) ) { let isValid = verifyEcdsaConstant( Curve, @@ -164,14 +165,15 @@ function verifyEcdsa( let u1 = ForeignField.mul(msgHash, sInv, Curve.order); let u2 = ForeignField.mul(r, sInv, Curve.order); + let IA = Point.from(ia); let X = varPlusFixedScalarMul(Curve, IA, u1, publicKey, u2, table); // assert that X != IA, and add -IA - Point.equal(X, Point.from(IA)).assertFalse(); - X = add(X, Point.from(Curve.negate(IA)), Curve.order); + Provable.equal(Point, X, IA).assertFalse(); + X = add(X, Point.from(Curve.negate(ia)), Curve.order); // TODO reduce X.x mod the scalar order - Field3.assertEqual(X.x, r); + Provable.assertEqual(Field3.provable, X.x, r); } /** @@ -186,7 +188,7 @@ function verifyEcdsa( */ function varPlusFixedScalarMul( Curve: CurveAffine, - IA: point, + IA: Point, s: Field3, P: Point, t: Field3, @@ -254,31 +256,20 @@ function initialAggregator(F: FiniteField, { a, b }: { a: bigint; b: bigint }) { } const Point = { + ...provable({ x: Field3.provable, y: Field3.provable }), from({ x, y }: point): Point { return { x: Field3.from(x), y: Field3.from(y) }; }, toBigint({ x, y }: Point): point { return { x: Field3.toBigint(x), y: Field3.toBigint(y), infinity: false }; }, - isConstant({ x, y }: Point) { - return Field3.isConstant(x) && Field3.isConstant(y); - }, - assertEqual({ x: x1, y: y1 }: Point, { x: x2, y: y2 }: Point) { - Field3.assertEqual(x1, x2); - Field3.assertEqual(y1, y2); - }, - equal({ x: x1, y: y1 }: Point, { x: x2, y: y2 }: Point) { - return Field3.equal(x1, x2).and(Field3.equal(y1, y2)); - }, }; const Signature = { + ...provable({ r: Field3.provable, s: Field3.provable }), toBigint({ r, s }: Signature): signature { return { r: Field3.toBigint(r), s: Field3.toBigint(s) }; }, - isConstant({ s, r }: Signature) { - return Field3.isConstant(s) && Field3.isConstant(r); - }, }; let csAdd = Provable.constraintSystem(() => { diff --git a/src/lib/gadgets/foreign-field.ts b/src/lib/gadgets/foreign-field.ts index 265859dcb..bc59eeff0 100644 --- a/src/lib/gadgets/foreign-field.ts +++ b/src/lib/gadgets/foreign-field.ts @@ -365,23 +365,6 @@ const Field3 = { return x.every((x) => x.isConstant()); }, - /** - * Assert that two 3-tuples of Fields are equal - */ - assertEqual(x: Field3, y: Field3) { - x[0].assertEquals(y[0]); - x[1].assertEquals(y[1]); - x[2].assertEquals(y[2]); - }, - - /** - * Check whether two 3-tuples of Fields are equal - */ - equal(x: Field3, y: Field3) { - let eq01 = x[0].add(x[1].mul(1n << L)).equals(y[0].add(y[1].mul(1n << L))); - return eq01.and(x[2].equals(y[2])); - }, - /** * Provable interface for `Field3 = [Field, Field, Field]`. * diff --git a/src/lib/provable.ts b/src/lib/provable.ts index 0b1a5e00a..bd90d6645 100644 --- a/src/lib/provable.ts +++ b/src/lib/provable.ts @@ -3,6 +3,7 @@ * - 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'; @@ -119,6 +120,16 @@ const Provable = { * ``` */ Array: provableArray, + /** + * Check whether a value is constant. + * See {@link FieldVar} for more information about constants and variables. + * + * @example + * ```ts + * let x = Field(42); + * Provable.isConstant(x); // true + */ + isConstant, /** * Interface to log elements within a circuit. Similar to `console.log()`. * @example @@ -392,6 +403,10 @@ function switch_>( return (type as Provable).fromFields(fields, aux); } +function isConstant(type: Provable, x: T): boolean { + return type.toFields(x).every((x) => x.isConstant()); +} + // logging in provable code function log(...args: any) { From e4589070d6c4bcca951d69124b5f0f0f0aa29ff1 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 Nov 2023 21:14:37 +0100 Subject: [PATCH 010/102] add constant cases and a bit more ecdsa logic --- src/lib/gadgets/elliptic-curve.ts | 52 ++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 707fd2f42..4f01e3c24 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -12,7 +12,6 @@ import { ForeignField, Sum, assertRank1, - bigint3, split, weakBound, } from './foreign-field.js'; @@ -20,18 +19,36 @@ import { multiRangeCheck } from './range-check.js'; import { printGates } from '../testing/constraint-system.js'; import { sha256 } from 'js-sha256'; import { bytesToBigInt } from '../../bindings/crypto/bigint-helpers.js'; -import { CurveAffine, Pallas } from '../../bindings/crypto/elliptic_curve.js'; +import { + CurveAffine, + Pallas, + affineAdd, + affineDouble, +} from '../../bindings/crypto/elliptic_curve.js'; import { Bool } from '../bool.js'; import { provable } from '../circuit_value.js'; +/** + * Non-zero elliptic curve point in affine coordinates. + */ type Point = { x: Field3; y: Field3 }; -type point = { x: bigint; y: bigint; infinity: boolean }; +type point = { x: bigint; y: bigint }; +/** + * ECDSA signature consisting of two curve scalars. + */ type Signature = { r: Field3; s: Field3 }; type signature = { r: bigint; s: bigint }; -function add({ x: x1, y: y1 }: Point, { x: x2, y: y2 }: Point, f: bigint) { - // TODO constant case +function add(p1: Point, p2: Point, f: bigint) { + let { x: x1, y: y1 } = p1; + let { x: x2, y: y2 } = p2; + + // constant case + if (Provable.isConstant(Point, p1) && Provable.isConstant(Point, p2)) { + let p3 = affineAdd(Point.toBigint(p1), Point.toBigint(p2), f); + return Point.from(p3); + } // witness and range-check slope, x3, y3 let witnesses = exists(9, () => { @@ -78,8 +95,14 @@ function add({ x: x1, y: y1 }: Point, { x: x2, y: y2 }: Point, f: bigint) { return { x: x3, y: y3 }; } -function double({ x: x1, y: y1 }: Point, f: bigint) { - // TODO constant case +function double(p1: Point, f: bigint) { + let { x: x1, y: y1 } = p1; + + // constant case + if (Provable.isConstant(Point, p1)) { + let p3 = affineDouble(Point.toBigint(p1), f); + return Point.from(p3); + } // witness and range-check slope, x3, y3 let witnesses = exists(9, () => { @@ -158,7 +181,7 @@ function verifyEcdsa( } // provable case - // TODO should check that the publicKey is a valid point? probably not + // TODO should we check that the publicKey is a valid point? probably not let { r, s } = signature; let sInv = ForeignField.inv(s, Curve.order); @@ -166,14 +189,15 @@ function verifyEcdsa( let u2 = ForeignField.mul(r, sInv, Curve.order); let IA = Point.from(ia); - let X = varPlusFixedScalarMul(Curve, IA, u1, publicKey, u2, table); + let R = varPlusFixedScalarMul(Curve, IA, u1, publicKey, u2, table); // assert that X != IA, and add -IA - Provable.equal(Point, X, IA).assertFalse(); - X = add(X, Point.from(Curve.negate(ia)), Curve.order); + Provable.equal(Point, R, IA).assertFalse(); + R = add(R, Point.from(Curve.negate(Curve.fromNonzero(ia))), Curve.order); - // TODO reduce X.x mod the scalar order - Provable.assertEqual(Field3.provable, X.x, r); + // reduce R.x modulo the curve order + let Rx = ForeignField.mul(R.x, Field3.from(1n), Curve.order); + Provable.assertEqual(Field3.provable, Rx, r); } /** @@ -260,7 +284,7 @@ const Point = { from({ x, y }: point): Point { return { x: Field3.from(x), y: Field3.from(y) }; }, - toBigint({ x, y }: Point): point { + toBigint({ x, y }: Point) { return { x: Field3.toBigint(x), y: Field3.toBigint(y), infinity: false }; }, }; From 8db8d06546e7184548bb0e8a5bd95936771a7837 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 Nov 2023 21:14:42 +0100 Subject: [PATCH 011/102] bindings --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index 9b2f1abfe..0c9a907ca 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 9b2f1abfeb891e95ccb88e536275eb6733e67f30 +Subproject commit 0c9a907ca5dad975b6677e3f7d5d3f86bb9e6fdc From ab9584fe7e11fb17e4c6284110c97ac991497bef Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 16 Nov 2023 13:18:57 +0100 Subject: [PATCH 012/102] implement ecdsa --- src/lib/gadgets/elliptic-curve.ts | 221 ++++++++++++++++++++++++++---- 1 file changed, 198 insertions(+), 23 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 4f01e3c24..3f24a8a34 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -15,10 +15,13 @@ import { split, weakBound, } from './foreign-field.js'; -import { multiRangeCheck } from './range-check.js'; +import { L, multiRangeCheck } from './range-check.js'; import { printGates } from '../testing/constraint-system.js'; import { sha256 } from 'js-sha256'; -import { bytesToBigInt } from '../../bindings/crypto/bigint-helpers.js'; +import { + bigIntToBits, + bytesToBigInt, +} from '../../bindings/crypto/bigint-helpers.js'; import { CurveAffine, Pallas, @@ -27,6 +30,7 @@ import { } from '../../bindings/crypto/elliptic_curve.js'; import { Bool } from '../bool.js'; import { provable } from '../circuit_value.js'; +import { assertPositiveInteger } from '../../bindings/crypto/non-negative.js'; /** * Non-zero elliptic curve point in affine coordinates. @@ -159,9 +163,11 @@ function verifyEcdsa( signature: Signature, msgHash: Field3, publicKey: Point, - table?: { - windowSize: number; // what we called c before - multiples?: point[]; // 0, G, 2*G, ..., (2^c-1)*G + tables?: { + windowSizeG?: number; + multiplesG?: Point[]; + windowSizeP?: number; + multiplesP?: Point[]; } ) { // constant case @@ -182,18 +188,14 @@ function verifyEcdsa( // provable case // TODO should we check that the publicKey is a valid point? probably not - let { r, s } = signature; let sInv = ForeignField.inv(s, Curve.order); let u1 = ForeignField.mul(msgHash, sInv, Curve.order); let u2 = ForeignField.mul(r, sInv, Curve.order); - let IA = Point.from(ia); - let R = varPlusFixedScalarMul(Curve, IA, u1, publicKey, u2, table); - - // assert that X != IA, and add -IA - Provable.equal(Point, R, IA).assertFalse(); - R = add(R, Point.from(Curve.negate(Curve.fromNonzero(ia))), Curve.order); + let G = Point.from(Curve.one); + let R = doubleScalarMul(Curve, ia, u1, G, u2, publicKey, tables); + // this ^ already proves that R != 0 // reduce R.x modulo the curve order let Rx = ForeignField.mul(R.x, Field3.from(1n), Curve.order); @@ -203,25 +205,75 @@ function verifyEcdsa( /** * Scalar mul that we need for ECDSA: * - * IA + s*P + t*G, + * s*G + t*P, * - * where IA is the initial aggregator, P is any point and G is the generator. + * where G, P are any points. The result is not allowed to be zero. * * We double both points together and leverage a precomputed table * of size 2^c to avoid all but every cth addition for t*G. + * + * TODO: could use lookups for picking precomputed multiples, instead of O(2^c) provable switch + * TODO: custom bit representation for the scalar that avoids 0, to get rid of the degenerate addition case + * 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 varPlusFixedScalarMul( +function doubleScalarMul( Curve: CurveAffine, - IA: Point, + ia: point, s: Field3, - P: Point, + G: Point, t: Field3, - table?: { - windowSize: number; // what we called c before - multiples?: point[]; // 0, G, 2*G, ..., (2^c-1)*G - } + P: Point, + { + // what we called c before + windowSizeG = 1, + // G, ..., (2^c-1)*G + multiplesG = undefined as Point[] | undefined, + windowSizeP = 1, + multiplesP = undefined as Point[] | undefined, + } = {} ): Point { - throw Error('TODO'); + // parse or build point tables + let Gs = getPointTable(Curve, G, windowSizeG, multiplesG); + let Ps = getPointTable(Curve, P, windowSizeP, multiplesP); + + // slice scalars + let b = Curve.order.toString(2).length; + let ss = slice(s, { maxBits: b, chunkSize: windowSizeG }); + let ts = slice(t, { maxBits: b, chunkSize: windowSizeP }); + + let sum = Point.from(ia); + + for (let i = 0; i < b; i++) { + if (i % windowSizeG === 0) { + // pick point to add based on the scalar chunk + let sj = ss[i / windowSizeG]; + let Gj = windowSizeG === 1 ? G : arrayGet(Point, Gs, sj, { offset: 1 }); + + // ec addition + let added = add(sum, Gj, Curve.p); + + // handle degenerate case (if sj = 0, Gj is all zeros and the add result is garbage) + sum = Provable.if(sj.equals(0), Point, sum, added); + } + + if (i % windowSizeP === 0) { + let tj = ts[i / windowSizeP]; + let Pj = windowSizeP === 1 ? P : arrayGet(Point, Ps, tj, { offset: 1 }); + let added = add(sum, Pj, Curve.p); + sum = Provable.if(tj.equals(0), Point, sum, added); + } + + // jointly double both points + sum = double(sum, Curve.p); + } + + // the sum is now s*G + t*P + 2^b*IA + // we assert that sum != 2^b*IA, and add -2^b*IA to get our result + let iaTimes2ToB = Curve.scale(Curve.fromNonzero(ia), 1n << BigInt(b)); + Provable.equal(Point, sum, Point.from(iaTimes2ToB)).assertFalse(); + sum = add(sum, Point.from(Curve.negate(iaTimes2ToB)), Curve.p); + + return sum; } /** @@ -251,6 +303,30 @@ function verifyEcdsaConstant( return mod(X.x, q) === r; } +function getPointTable( + Curve: CurveAffine, + P: Point, + windowSize: number, + table?: Point[] +): Point[] { + assertPositiveInteger(windowSize, 'invalid window size'); + let n = (1 << windowSize) - 1; // n >= 1 + + assert(table === undefined || table.length === n, 'invalid table'); + if (table !== undefined) return table; + + table = [P]; + if (n === 1) return table; + + let Pi = double(P, Curve.p); + table.push(Pi); + for (let i = 2; i < n; i++) { + Pi = add(Pi, P, Curve.p); + table.push(Pi); + } + return table; +} + /** * For EC scalar multiplication we use an initial point which is subtracted * at the end, to avoid encountering the point at infinity. @@ -271,12 +347,101 @@ function initialAggregator(F: FiniteField, { a, b }: { a: bigint; b: bigint }) { // increment x until we find a y coordinate while (y === undefined) { + x = F.add(x, 1n); // solve y^2 = x^3 + ax + b let x3 = F.mul(F.square(x), x); let y2 = F.add(x3, F.mul(a, x) + b); y = F.sqrt(y2); } - return { x: F.mod(x), y, infinity: false }; + return { x, y, infinity: false }; +} + +/** + * Provable method for slicing a 3x88-bit bigint into smaller bit chunks of length `windowSize` + * + * TODO: atm this uses expensive boolean checks for the bits. + * For larger chunks, we should use more efficient range checks. + * + * Note: This serves as a range check for the input limbs + */ +function slice( + [x0, x1, x2]: Field3, + { maxBits, chunkSize }: { maxBits: number; chunkSize: number } +) { + let l = Number(L); + + // first limb + let chunks0 = sliceField(x0, Math.min(l, maxBits), chunkSize); + if (maxBits <= l) return chunks0; + maxBits -= l; + + // second limb + let chunks1 = sliceField(x1, Math.min(l, maxBits), chunkSize); + if (maxBits <= l) return chunks0.concat(chunks1); + maxBits -= l; + + // third limb + let chunks2 = sliceField(x2, maxBits, chunkSize); + return chunks0.concat(chunks1).concat(chunks2); +} + +/** + * Provable method for slicing a 3x88-bit bigint into smaller bit chunks of length `windowSize` + * + * TODO: atm this uses expensive boolean checks for the bits. + * For larger chunks, we should use more efficient range checks. + * + * Note: This serves as a range check that the input is in [0, 2^k) where `k = ceil(maxBits / windowSize) * windowSize` + */ +function sliceField(x: Field, maxBits: number, chunkSize: number) { + let bits = exists(maxBits, () => { + let bits = bigIntToBits(x.toBigInt()); + // normalize length + if (bits.length > maxBits) bits = bits.slice(0, maxBits); + if (bits.length < maxBits) + bits = bits.concat(Array(maxBits - bits.length).fill(false)); + return bits.map(BigInt); + }); + + let chunks = []; + let sum = Field.from(0n); + for (let i = 0; i < maxBits; i += chunkSize) { + // prove that chunk has `windowSize` bits + // TODO: this inner sum should be replaced with a more efficient range check when possible + let chunk = Field.from(0n); + for (let j = 0; j < chunkSize; j++) { + let bit = bits[i + j]; + Bool.check(Bool.Unsafe.ofField(bit)); + chunk = chunk.add(bit.mul(1n << BigInt(j))); + } + chunk = chunk.seal(); + // prove that chunks add up to x + sum = sum.add(chunk.mul(1n << BigInt(i))); + chunks.push(chunk); + } + sum.assertEquals(x); + + return chunks; +} + +/** + * Get value from array in O(n) constraints. + * + * If the index is out of bounds, returns all-zeros version of T + */ +function arrayGet( + type: Provable, + array: T[], + index: Field, + { offset = 0 } = {} +) { + let n = array.length; + let oneHot = Array(n); + // TODO can we share computation between all those equals()? + for (let i = 0; i < n; i++) { + oneHot[i] = index.equals(i + offset); + } + return Provable.switch(oneHot, type, array); } const Point = { @@ -296,6 +461,16 @@ const Signature = { }, }; +function gcd(a: number, b: number) { + if (b > a) [a, b] = [b, a]; + while (true) { + if (b === 0) return a; + [a, b] = [b, a % b]; + } +} + +console.log(gcd(2, 4)); + let csAdd = Provable.constraintSystem(() => { let x1 = Provable.witness(Field3.provable, () => Field3.from(0n)); let x2 = Provable.witness(Field3.provable, () => Field3.from(0n)); From c39e0f8b442883831e9769af85daeb5bd6ad978e Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 16 Nov 2023 14:05:41 +0100 Subject: [PATCH 013/102] signature from hex --- src/lib/gadgets/elliptic-curve.ts | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 3f24a8a34..5179dcf1a 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -250,7 +250,7 @@ function doubleScalarMul( let Gj = windowSizeG === 1 ? G : arrayGet(Point, Gs, sj, { offset: 1 }); // ec addition - let added = add(sum, Gj, Curve.p); + let added = add(sum, Gj, Curve.modulus); // handle degenerate case (if sj = 0, Gj is all zeros and the add result is garbage) sum = Provable.if(sj.equals(0), Point, sum, added); @@ -259,19 +259,19 @@ function doubleScalarMul( if (i % windowSizeP === 0) { let tj = ts[i / windowSizeP]; let Pj = windowSizeP === 1 ? P : arrayGet(Point, Ps, tj, { offset: 1 }); - let added = add(sum, Pj, Curve.p); + let added = add(sum, Pj, Curve.modulus); sum = Provable.if(tj.equals(0), Point, sum, added); } // jointly double both points - sum = double(sum, Curve.p); + sum = double(sum, Curve.modulus); } // the sum is now s*G + t*P + 2^b*IA // we assert that sum != 2^b*IA, and add -2^b*IA to get our result let iaTimes2ToB = Curve.scale(Curve.fromNonzero(ia), 1n << BigInt(b)); Provable.equal(Point, sum, Point.from(iaTimes2ToB)).assertFalse(); - sum = add(sum, Point.from(Curve.negate(iaTimes2ToB)), Curve.p); + sum = add(sum, Point.from(Curve.negate(iaTimes2ToB)), Curve.modulus); return sum; } @@ -318,10 +318,10 @@ function getPointTable( table = [P]; if (n === 1) return table; - let Pi = double(P, Curve.p); + let Pi = double(P, Curve.modulus); table.push(Pi); for (let i = 2; i < n; i++) { - Pi = add(Pi, P, Curve.p); + Pi = add(Pi, P, Curve.modulus); table.push(Pi); } return table; @@ -456,9 +456,28 @@ const Point = { const Signature = { ...provable({ r: Field3.provable, s: Field3.provable }), + from({ r, s }: signature): Signature { + return { r: Field3.from(r), s: Field3.from(s) }; + }, toBigint({ r, s }: Signature): signature { return { r: Field3.toBigint(r), s: Field3.toBigint(s) }; }, + /** + * Create a {@link Signature} from a raw 130-char hex string as used in + * [Ethereum transactions](https://ethereum.org/en/developers/docs/transactions/#typed-transaction-envelope). + */ + fromHex(rawSignature: string): Signature { + let prefix = rawSignature.slice(0, 2); + let signature = rawSignature.slice(2, 130); + if (prefix !== '0x' || signature.length < 128) { + throw Error( + `Signature.fromHex(): Invalid signature, expected hex string 0x... of length at least 130.` + ); + } + let r = BigInt(`0x${signature.slice(0, 64)}`); + let s = BigInt(`0x${signature.slice(64)}`); + return Signature.from({ r, s }); + }, }; function gcd(a: number, b: number) { From b924cad9d7525bced296161d6d9f5f6447bac931 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 16 Nov 2023 14:22:40 +0100 Subject: [PATCH 014/102] add ecdsa test script --- src/lib/gadgets/ecdsa.unit-test.ts | 54 ++++++++++++++++++++++++++++++ src/lib/gadgets/elliptic-curve.ts | 35 +++++++++++++------ 2 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 src/lib/gadgets/ecdsa.unit-test.ts diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts new file mode 100644 index 000000000..80c657bcd --- /dev/null +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -0,0 +1,54 @@ +import { createCurveAffine } from '../../bindings/crypto/elliptic_curve.js'; +import { Ecdsa, EllipticCurve, Point } from './elliptic-curve.js'; +import { Field3 } from './foreign-field.js'; +import { secp256k1Params } from '../../bindings/crypto/elliptic-curve-examples.js'; +import { Provable } from '../provable.js'; +import { createField } from '../../bindings/crypto/finite_field.js'; + +const Secp256k1 = createCurveAffine(secp256k1Params); +const BaseField = createField(secp256k1Params.modulus); + +let publicKey = Point.from({ + x: 49781623198970027997721070672560275063607048368575198229673025608762959476014n, + y: 44999051047832679156664607491606359183507784636787036192076848057884504239143n, +}); + +let signature = Ecdsa.Signature.fromHex( + '0x82de9950cc5aac0dca7210cb4b77320ac9e844717d39b1781e9d941d920a12061da497b3c134f50b2fce514d66e20c5e43f9615f097395a5527041d14860a52f1b' +); + +let msgHash = + Field3.from( + 0x3e91cd8bd233b3df4e4762b329e2922381da770df1b31276ec77d0557be7fcefn + ); + +const ia = EllipticCurve.initialAggregator(BaseField, Secp256k1); + +function main() { + let signature0 = Provable.witness(Ecdsa.Signature, () => signature); + Ecdsa.verify(Secp256k1, ia, signature0, msgHash, publicKey, { + windowSizeG: 3, + windowSizeP: 3, + }); +} + +console.time('ecdsa verify (constant)'); +main(); +console.timeEnd('ecdsa verify (constant)'); + +console.time('ecdsa verify (witness gen / check)'); +Provable.runAndCheck(main); +console.timeEnd('ecdsa verify (witness gen / check)'); + +console.time('ecdsa verify (build constraint system)'); +let cs = Provable.constraintSystem(main); +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); diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 5179dcf1a..a2911386e 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -32,6 +32,14 @@ import { Bool } from '../bool.js'; import { provable } from '../circuit_value.js'; import { assertPositiveInteger } from '../../bindings/crypto/non-negative.js'; +export { EllipticCurve, Point, Ecdsa, EcdsaSignature }; + +const EllipticCurve = { + add, + double, + initialAggregator, +}; + /** * Non-zero elliptic curve point in affine coordinates. */ @@ -41,8 +49,8 @@ type point = { x: bigint; y: bigint }; /** * ECDSA signature consisting of two curve scalars. */ -type Signature = { r: Field3; s: Field3 }; -type signature = { r: bigint; s: bigint }; +type EcdsaSignature = { r: Field3; s: Field3 }; +type ecdsaSignature = { r: bigint; s: bigint }; function add(p1: Point, p2: Point, f: bigint) { let { x: x1, y: y1 } = p1; @@ -160,7 +168,7 @@ function double(p1: Point, f: bigint) { function verifyEcdsa( Curve: CurveAffine, ia: point, - signature: Signature, + signature: EcdsaSignature, msgHash: Field3, publicKey: Point, tables?: { @@ -172,13 +180,13 @@ function verifyEcdsa( ) { // constant case if ( - Provable.isConstant(Signature, signature) && + Provable.isConstant(EcdsaSignature, signature) && Field3.isConstant(msgHash) && Provable.isConstant(Point, publicKey) ) { let isValid = verifyEcdsaConstant( Curve, - Signature.toBigint(signature), + EcdsaSignature.toBigint(signature), Field3.toBigint(msgHash), Point.toBigint(publicKey) ); @@ -454,19 +462,19 @@ const Point = { }, }; -const Signature = { +const EcdsaSignature = { ...provable({ r: Field3.provable, s: Field3.provable }), - from({ r, s }: signature): Signature { + from({ r, s }: ecdsaSignature): EcdsaSignature { return { r: Field3.from(r), s: Field3.from(s) }; }, - toBigint({ r, s }: Signature): signature { + toBigint({ r, s }: EcdsaSignature): ecdsaSignature { return { r: Field3.toBigint(r), s: Field3.toBigint(s) }; }, /** - * Create a {@link Signature} from a raw 130-char hex string as used in + * 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). */ - fromHex(rawSignature: string): Signature { + fromHex(rawSignature: string): EcdsaSignature { let prefix = rawSignature.slice(0, 2); let signature = rawSignature.slice(2, 130); if (prefix !== '0x' || signature.length < 128) { @@ -476,10 +484,15 @@ const Signature = { } let r = BigInt(`0x${signature.slice(0, 64)}`); let s = BigInt(`0x${signature.slice(64)}`); - return Signature.from({ r, s }); + return EcdsaSignature.from({ r, s }); }, }; +const Ecdsa = { + verify: verifyEcdsa, + Signature: EcdsaSignature, +}; + function gcd(a: number, b: number) { if (b > a) [a, b] = [b, a]; while (true) { From 9a2629f576e2b69ee2ee18d78c65484eecad1f15 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 16 Nov 2023 14:22:55 +0100 Subject: [PATCH 015/102] move initial ec testing code --- src/lib/gadgets/elliptic-curve.ts | 33 ----------------- src/lib/gadgets/elliptic-curve.unit-test.ts | 40 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 33 deletions(-) create mode 100644 src/lib/gadgets/elliptic-curve.unit-test.ts diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index a2911386e..001137f97 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -500,36 +500,3 @@ function gcd(a: number, b: number) { [a, b] = [b, a % b]; } } - -console.log(gcd(2, 4)); - -let csAdd = Provable.constraintSystem(() => { - let x1 = Provable.witness(Field3.provable, () => Field3.from(0n)); - let x2 = Provable.witness(Field3.provable, () => Field3.from(0n)); - let y1 = Provable.witness(Field3.provable, () => Field3.from(0n)); - let y2 = Provable.witness(Field3.provable, () => Field3.from(0n)); - - let g = { x: x1, y: y1 }; - let h = { x: x2, y: y2 }; - - add(g, h, exampleFields.secp256k1.modulus); -}); - -let csDouble = Provable.constraintSystem(() => { - let x1 = Provable.witness(Field3.provable, () => Field3.from(0n)); - let y1 = Provable.witness(Field3.provable, () => Field3.from(0n)); - - let g = { x: x1, y: y1 }; - - double(g, exampleFields.secp256k1.modulus); -}); - -printGates(csAdd.gates); -console.log({ digest: csAdd.digest, rows: csAdd.rows }); - -printGates(csDouble.gates); -console.log({ digest: csDouble.digest, rows: csDouble.rows }); - -let point = initialAggregator(exampleFields.Fp, { a: 0n, b: 5n }); -console.log({ point }); -assert(Pallas.isOnCurve(Pallas.fromAffine(point))); 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 000000000..c595011a2 --- /dev/null +++ b/src/lib/gadgets/elliptic-curve.unit-test.ts @@ -0,0 +1,40 @@ +import { exampleFields } from 'src/bindings/crypto/finite-field-examples.js'; +import { Provable } from '../provable.js'; +import { Field3 } from './foreign-field.js'; +import { EllipticCurve } from './elliptic-curve.js'; +import { printGates } from '../testing/constraint-system.js'; +import { assert } from './common.js'; +import { Pallas } from '../../bindings/crypto/elliptic_curve.js'; + +let { add, double, initialAggregator } = EllipticCurve; + +let csAdd = Provable.constraintSystem(() => { + let x1 = Provable.witness(Field3.provable, () => Field3.from(0n)); + let x2 = Provable.witness(Field3.provable, () => Field3.from(0n)); + let y1 = Provable.witness(Field3.provable, () => Field3.from(0n)); + let y2 = Provable.witness(Field3.provable, () => Field3.from(0n)); + + let g = { x: x1, y: y1 }; + let h = { x: x2, y: y2 }; + + add(g, h, exampleFields.secp256k1.modulus); +}); + +let csDouble = Provable.constraintSystem(() => { + let x1 = Provable.witness(Field3.provable, () => Field3.from(0n)); + let y1 = Provable.witness(Field3.provable, () => Field3.from(0n)); + + let g = { x: x1, y: y1 }; + + double(g, exampleFields.secp256k1.modulus); +}); + +printGates(csAdd.gates); +console.log({ digest: csAdd.digest, rows: csAdd.rows }); + +printGates(csDouble.gates); +console.log({ digest: csDouble.digest, rows: csDouble.rows }); + +let point = initialAggregator(exampleFields.Fp, { a: 0n, b: 5n }); +console.log({ point }); +assert(Pallas.isOnCurve(Pallas.fromAffine(point))); From a1370e8c7cd5175e2d652b6072bd84bc6d1f4dd3 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 16 Nov 2023 14:31:02 +0100 Subject: [PATCH 016/102] fix bit slicing logic --- src/lib/gadgets/elliptic-curve.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 001137f97..7f5da5ad1 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -399,7 +399,7 @@ function slice( * TODO: atm this uses expensive boolean checks for the bits. * For larger chunks, we should use more efficient range checks. * - * Note: This serves as a range check that the input is in [0, 2^k) where `k = ceil(maxBits / windowSize) * windowSize` + * Note: This serves as a range check that the input is in [0, 2^maxBits) */ function sliceField(x: Field, maxBits: number, chunkSize: number) { let bits = exists(maxBits, () => { @@ -413,11 +413,13 @@ function sliceField(x: Field, maxBits: number, chunkSize: number) { let chunks = []; let sum = Field.from(0n); + for (let i = 0; i < maxBits; i += chunkSize) { - // prove that chunk has `windowSize` bits + // prove that chunk has `chunkSize` bits // TODO: this inner sum should be replaced with a more efficient range check when possible let chunk = Field.from(0n); - for (let j = 0; j < chunkSize; j++) { + let size = Math.min(maxBits - i, chunkSize); // last chunk might be smaller + for (let j = 0; j < size; j++) { let bit = bits[i + j]; Bool.check(Bool.Unsafe.ofField(bit)); chunk = chunk.add(bit.mul(1n << BigInt(j))); From 92318feea8d1b2234cbebe135aa6d23e365da912 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 16 Nov 2023 14:39:40 +0100 Subject: [PATCH 017/102] bindings --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index 0c9a907ca..8f0e0d0f8 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 0c9a907ca5dad975b6677e3f7d5d3f86bb9e6fdc +Subproject commit 8f0e0d0f874dc3952bb01791c4b31394d7e5298f From 92a627ed49827256d88bba1dd024589f7bafca76 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 16 Nov 2023 17:47:33 +0100 Subject: [PATCH 018/102] several fixes to the scaling algorithm, debugging --- src/lib/gadgets/ecdsa.unit-test.ts | 57 ++++++++++++++++++++++++++---- src/lib/gadgets/elliptic-curve.ts | 40 ++++++++++++++++----- 2 files changed, 81 insertions(+), 16 deletions(-) diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index 80c657bcd..6707b84c8 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -4,6 +4,7 @@ import { Field3 } from './foreign-field.js'; import { secp256k1Params } from '../../bindings/crypto/elliptic-curve-examples.js'; import { Provable } from '../provable.js'; import { createField } from '../../bindings/crypto/finite_field.js'; +import { ZkProgram } from '../proof_system.js'; const Secp256k1 = createCurveAffine(secp256k1Params); const BaseField = createField(secp256k1Params.modulus); @@ -23,14 +24,48 @@ let msgHash = ); const ia = EllipticCurve.initialAggregator(BaseField, Secp256k1); +// TODO doesn't work with windowSize = 3 +const tableConfig = { windowSizeG: 2, windowSizeP: 2 }; -function main() { - let signature0 = Provable.witness(Ecdsa.Signature, () => signature); - Ecdsa.verify(Secp256k1, ia, signature0, msgHash, publicKey, { - windowSizeG: 3, - windowSizeP: 3, - }); -} +let program = ZkProgram({ + name: 'ecdsa', + methods: { + scale: { + privateInputs: [], + method() { + let G = Point.from(Secp256k1.one); + let P = Provable.witness(Point, () => publicKey); + let R = EllipticCurve.doubleScalarMul( + Secp256k1, + ia, + signature.s, + G, + signature.r, + P, + tableConfig + ); + Provable.asProver(() => { + console.log(Point.toBigint(R)); + }); + }, + }, + ecdsa: { + privateInputs: [], + method() { + let signature0 = Provable.witness(Ecdsa.Signature, () => signature); + Ecdsa.verify( + Secp256k1, + ia, + signature0, + msgHash, + publicKey, + tableConfig + ); + }, + }, + }, +}); +let main = program.rawMethods.ecdsa; console.time('ecdsa verify (constant)'); main(); @@ -52,3 +87,11 @@ for (let gate of cs.gates) { } console.log(gateTypes); + +console.time('ecdsa verify (compile)'); +await program.compile(); +console.timeEnd('ecdsa verify (compile)'); + +console.time('ecdsa verify (prove)'); +let proof = await program.ecdsa(); +console.timeEnd('ecdsa verify (prove)'); diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 7f5da5ad1..1294ef9bb 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -3,7 +3,6 @@ import { inverse, mod, } from '../../bindings/crypto/finite_field.js'; -import { exampleFields } from '../../bindings/crypto/finite-field-examples.js'; import { Field } from '../field.js'; import { Provable } from '../provable.js'; import { assert, exists } from './common.js'; @@ -16,7 +15,6 @@ import { weakBound, } from './foreign-field.js'; import { L, multiRangeCheck } from './range-check.js'; -import { printGates } from '../testing/constraint-system.js'; import { sha256 } from 'js-sha256'; import { bigIntToBits, @@ -24,7 +22,6 @@ import { } from '../../bindings/crypto/bigint-helpers.js'; import { CurveAffine, - Pallas, affineAdd, affineDouble, } from '../../bindings/crypto/elliptic_curve.js'; @@ -37,6 +34,7 @@ export { EllipticCurve, Point, Ecdsa, EcdsaSignature }; const EllipticCurve = { add, double, + doubleScalarMul, initialAggregator, }; @@ -207,6 +205,11 @@ function verifyEcdsa( // reduce R.x modulo the curve order let Rx = ForeignField.mul(R.x, Field3.from(1n), Curve.order); + Provable.asProver(() => { + let [u1_, u2_, Rx_, r_] = Field3.toBigints(u1, u2, Rx, r); + let R_ = Point.toBigint(R); + console.log({ u1_, u2_, R_, Rx_, r_ }); + }); Provable.assertEqual(Field3.provable, Rx, r); } @@ -240,6 +243,21 @@ function doubleScalarMul( multiplesP = undefined as Point[] | undefined, } = {} ): Point { + // constant case + if ( + Field3.isConstant(s) && + Field3.isConstant(t) && + Provable.isConstant(Point, G) && + Provable.isConstant(Point, P) + ) { + let s_ = Field3.toBigint(s); + let t_ = Field3.toBigint(t); + let G_ = Point.toBigint(G); + let P_ = Point.toBigint(P); + let R = Curve.add(Curve.scale(G_, s_), Curve.scale(P_, t_)); + return Point.from(R); + } + // parse or build point tables let Gs = getPointTable(Curve, G, windowSizeG, multiplesG); let Ps = getPointTable(Curve, P, windowSizeP, multiplesP); @@ -249,9 +267,11 @@ function doubleScalarMul( let ss = slice(s, { maxBits: b, chunkSize: windowSizeG }); let ts = slice(t, { maxBits: b, chunkSize: windowSizeP }); + console.log({ b, windowSizeG, windowSizeP, ss: ss.length, ts: ts.length }); + let sum = Point.from(ia); - for (let i = 0; i < b; i++) { + for (let i = b - 1; i >= 0; i--) { if (i % windowSizeG === 0) { // pick point to add based on the scalar chunk let sj = ss[i / windowSizeG]; @@ -270,16 +290,17 @@ function doubleScalarMul( let added = add(sum, Pj, Curve.modulus); sum = Provable.if(tj.equals(0), Point, sum, added); } + if (i === 0) break; // jointly double both points sum = double(sum, Curve.modulus); } - // the sum is now s*G + t*P + 2^b*IA - // we assert that sum != 2^b*IA, and add -2^b*IA to get our result - let iaTimes2ToB = Curve.scale(Curve.fromNonzero(ia), 1n << BigInt(b)); - Provable.equal(Point, sum, Point.from(iaTimes2ToB)).assertFalse(); - sum = add(sum, Point.from(Curve.negate(iaTimes2ToB)), Curve.modulus); + // the sum is now s*G + t*P + 2^(b-1)*IA + // 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)); + Provable.equal(Point, sum, Point.from(iaFinal)).assertFalse(); + sum = add(sum, Point.from(Curve.negate(iaFinal)), Curve.modulus); return sum; } @@ -306,6 +327,7 @@ function verifyEcdsaConstant( let u2 = mod(r * sInv, q); let X = Curve.add(Curve.scale(Curve.one, u1), Curve.scale(QA, u2)); + console.log({ u1, u2, R: X, Rx: mod(X.x, q), r }); if (Curve.equal(X, Curve.zero)) return false; return mod(X.x, q) === r; From cc4f9b12a96a2c067ec56b21f314395d008e6931 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 16 Nov 2023 21:46:20 +0100 Subject: [PATCH 019/102] fix uneven window sizes by glueing together overlapping chunk between limbs --- src/lib/gadgets/ecdsa.unit-test.ts | 5 +++- src/lib/gadgets/elliptic-curve.ts | 46 +++++++++++++++++++++++------- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index 6707b84c8..5eeab04be 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -5,6 +5,7 @@ import { secp256k1Params } from '../../bindings/crypto/elliptic-curve-examples.j import { Provable } from '../provable.js'; import { createField } from '../../bindings/crypto/finite_field.js'; import { ZkProgram } from '../proof_system.js'; +import { assert } from './common.js'; const Secp256k1 = createCurveAffine(secp256k1Params); const BaseField = createField(secp256k1Params.modulus); @@ -25,7 +26,7 @@ let msgHash = const ia = EllipticCurve.initialAggregator(BaseField, Secp256k1); // TODO doesn't work with windowSize = 3 -const tableConfig = { windowSizeG: 2, windowSizeP: 2 }; +const tableConfig = { windowSizeG: 3, windowSizeP: 3 }; let program = ZkProgram({ name: 'ecdsa', @@ -95,3 +96,5 @@ console.timeEnd('ecdsa verify (compile)'); console.time('ecdsa verify (prove)'); let proof = await program.ecdsa(); console.timeEnd('ecdsa verify (prove)'); + +assert(await program.verify(proof), 'proof verifies'); diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 1294ef9bb..c8f205dc9 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -221,7 +221,7 @@ function verifyEcdsa( * where G, P are any points. The result is not allowed to be zero. * * We double both points together and leverage a precomputed table - * of size 2^c to avoid all but every cth addition for t*G. + * of size 2^c to avoid all but every cth addition for both s*G and t*P. * * TODO: could use lookups for picking precomputed multiples, instead of O(2^c) provable switch * TODO: custom bit representation for the scalar that avoids 0, to get rid of the degenerate addition case @@ -267,7 +267,7 @@ function doubleScalarMul( let ss = slice(s, { maxBits: b, chunkSize: windowSizeG }); let ts = slice(t, { maxBits: b, chunkSize: windowSizeP }); - console.log({ b, windowSizeG, windowSizeP, ss: ss.length, ts: ts.length }); + console.log({ b, windowSizeG, ss: ss.length }); let sum = Point.from(ia); @@ -399,20 +399,21 @@ function slice( { maxBits, chunkSize }: { maxBits: number; chunkSize: number } ) { let l = Number(L); + assert(maxBits <= 3 * l, `expected max bits <= 3*${l}, got ${maxBits}`); // first limb - let chunks0 = sliceField(x0, Math.min(l, maxBits), chunkSize); - if (maxBits <= l) return chunks0; + let result0 = sliceField(x0, Math.min(l, maxBits), chunkSize); + if (maxBits <= l) return result0.chunks; maxBits -= l; // second limb - let chunks1 = sliceField(x1, Math.min(l, maxBits), chunkSize); - if (maxBits <= l) return chunks0.concat(chunks1); + let result1 = sliceField(x1, Math.min(l, maxBits), chunkSize, result0); + if (maxBits <= l) return result0.chunks.concat(result1.chunks); maxBits -= l; // third limb - let chunks2 = sliceField(x2, maxBits, chunkSize); - return chunks0.concat(chunks1).concat(chunks2); + let result2 = sliceField(x2, maxBits, chunkSize, result1); + return result0.chunks.concat(result1.chunks, result2.chunks); } /** @@ -423,7 +424,12 @@ function slice( * * Note: This serves as a range check that the input is in [0, 2^maxBits) */ -function sliceField(x: Field, maxBits: number, chunkSize: number) { +function sliceField( + x: Field, + maxBits: number, + chunkSize: number, + leftover?: { chunks: Field[]; leftoverSize: number } +) { let bits = exists(maxBits, () => { let bits = bigIntToBits(x.toBigInt()); // normalize length @@ -436,7 +442,24 @@ function sliceField(x: Field, maxBits: number, chunkSize: number) { let chunks = []; let sum = Field.from(0n); - for (let i = 0; i < maxBits; i += chunkSize) { + // if there's a leftover chunk from a previous slizeField() call, we complete it + if (leftover !== undefined) { + let { chunks: previous, leftoverSize: size } = leftover; + let remainingChunk = Field.from(0n); + for (let i = 0; i < size; i++) { + let bit = bits[i]; + Bool.check(Bool.Unsafe.ofField(bit)); + remainingChunk = remainingChunk.add(bit.mul(1n << BigInt(i))); + } + sum = remainingChunk = remainingChunk.seal(); + let chunk = previous[previous.length - 1]; + previous[previous.length - 1] = chunk.add( + remainingChunk.mul(1n << BigInt(chunkSize - size)) + ); + } + + let i = leftover?.leftoverSize ?? 0; + for (; i < maxBits; i += chunkSize) { // prove that chunk has `chunkSize` bits // TODO: this inner sum should be replaced with a more efficient range check when possible let chunk = Field.from(0n); @@ -453,7 +476,8 @@ function sliceField(x: Field, maxBits: number, chunkSize: number) { } sum.assertEquals(x); - return chunks; + let leftoverSize = i - maxBits; + return { chunks, leftoverSize } as const; } /** From 90d341fca76224e9fc12b38412ba57152216f648 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 16 Nov 2023 21:53:54 +0100 Subject: [PATCH 020/102] remove debug logs --- src/lib/gadgets/ecdsa.unit-test.ts | 3 +-- src/lib/gadgets/elliptic-curve.ts | 8 -------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index 5eeab04be..ea25a1e5e 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -25,8 +25,7 @@ let msgHash = ); const ia = EllipticCurve.initialAggregator(BaseField, Secp256k1); -// TODO doesn't work with windowSize = 3 -const tableConfig = { windowSizeG: 3, windowSizeP: 3 }; +const tableConfig = { windowSizeG: 5, windowSizeP: 3 }; let program = ZkProgram({ name: 'ecdsa', diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index c8f205dc9..97addcc3a 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -205,11 +205,6 @@ function verifyEcdsa( // reduce R.x modulo the curve order let Rx = ForeignField.mul(R.x, Field3.from(1n), Curve.order); - Provable.asProver(() => { - let [u1_, u2_, Rx_, r_] = Field3.toBigints(u1, u2, Rx, r); - let R_ = Point.toBigint(R); - console.log({ u1_, u2_, R_, Rx_, r_ }); - }); Provable.assertEqual(Field3.provable, Rx, r); } @@ -267,8 +262,6 @@ function doubleScalarMul( let ss = slice(s, { maxBits: b, chunkSize: windowSizeG }); let ts = slice(t, { maxBits: b, chunkSize: windowSizeP }); - console.log({ b, windowSizeG, ss: ss.length }); - let sum = Point.from(ia); for (let i = b - 1; i >= 0; i--) { @@ -327,7 +320,6 @@ function verifyEcdsaConstant( let u2 = mod(r * sInv, q); let X = Curve.add(Curve.scale(Curve.one, u1), Curve.scale(QA, u2)); - console.log({ u1, u2, R: X, Rx: mod(X.x, q), r }); if (Curve.equal(X, Curve.zero)) return false; return mod(X.x, q) === r; From e7b8d235fbb9296e82261712fb2871ba285f2610 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 16 Nov 2023 21:56:49 +0100 Subject: [PATCH 021/102] fixup --- src/lib/gadgets/ecdsa.unit-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index ea25a1e5e..429d4c124 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -25,7 +25,7 @@ let msgHash = ); const ia = EllipticCurve.initialAggregator(BaseField, Secp256k1); -const tableConfig = { windowSizeG: 5, windowSizeP: 3 }; +const tableConfig = { windowSizeG: 3, windowSizeP: 3 }; let program = ZkProgram({ name: 'ecdsa', From f7c5176eb0230a09ed188b750bacdd343ae30f6a Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 17 Nov 2023 09:06:16 +0100 Subject: [PATCH 022/102] fix --- src/lib/gadgets/elliptic-curve.unit-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/gadgets/elliptic-curve.unit-test.ts b/src/lib/gadgets/elliptic-curve.unit-test.ts index c595011a2..b851590e1 100644 --- a/src/lib/gadgets/elliptic-curve.unit-test.ts +++ b/src/lib/gadgets/elliptic-curve.unit-test.ts @@ -1,4 +1,4 @@ -import { exampleFields } from 'src/bindings/crypto/finite-field-examples.js'; +import { exampleFields } from '../../bindings/crypto/finite-field-examples.js'; import { Provable } from '../provable.js'; import { Field3 } from './foreign-field.js'; import { EllipticCurve } from './elliptic-curve.js'; From 968a5aec86b70426f960f787a332097f11a8999c Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 17 Nov 2023 10:47:33 +0100 Subject: [PATCH 023/102] adapt to ffmul changes --- src/lib/gadgets/elliptic-curve.ts | 35 +++++++++++++++---------------- src/lib/gadgets/foreign-field.ts | 4 +--- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 97addcc3a..89494d47e 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -14,7 +14,7 @@ import { split, weakBound, } from './foreign-field.js'; -import { L, multiRangeCheck } from './range-check.js'; +import { l, multiRangeCheck } from './range-check.js'; import { sha256 } from 'js-sha256'; import { bigIntToBits, @@ -87,20 +87,19 @@ function add(p1: Point, p2: Point, f: bigint) { // (x1 - x2)*m = y1 - y2 let deltaX = new Sum(x1).sub(x2); let deltaY = new Sum(y1).sub(y2); - let qBound1 = assertRank1(deltaX, m, deltaY, f); + assertRank1(deltaX, m, deltaY, f); // m^2 = x1 + x2 + x3 let xSum = new Sum(x1).add(x2).add(x3); - let qBound2 = assertRank1(m, m, xSum, f); + assertRank1(m, m, xSum, f); // (x1 - x3)*m = y1 + y3 let deltaX1X3 = new Sum(x1).sub(x3); let ySum = new Sum(y1).add(y3); - let qBound3 = assertRank1(deltaX1X3, m, ySum, f); + assertRank1(deltaX1X3, m, ySum, f); // bounds checks - multiRangeCheck([mBound, x3Bound, qBound1]); - multiRangeCheck([qBound2, qBound3, Field.from(0n)]); + multiRangeCheck([mBound, x3Bound, Field.from(0n)]); return { x: x3, y: y3 }; } @@ -145,20 +144,20 @@ function double(p1: Point, f: bigint) { // TODO this assumes the curve has a == 0 let y1Times2 = new Sum(y1).add(y1); let x1x1Times3 = new Sum(x1x1).add(x1x1).add(x1x1); - let qBound1 = assertRank1(y1Times2, m, x1x1Times3, f); + assertRank1(y1Times2, m, x1x1Times3, f); // m^2 = 2*x1 + x3 let xSum = new Sum(x1).add(x1).add(x3); - let qBound2 = assertRank1(m, m, xSum, f); + assertRank1(m, m, xSum, f); // (x1 - x3)*m = y1 + y3 let deltaX1X3 = new Sum(x1).sub(x3); let ySum = new Sum(y1).add(y3); - let qBound3 = assertRank1(deltaX1X3, m, ySum, f); + assertRank1(deltaX1X3, m, ySum, f); // bounds checks - multiRangeCheck([mBound, x3Bound, qBound1]); - multiRangeCheck([qBound2, qBound3, Field.from(0n)]); + // TODO: there is a secret free spot for two bounds in ForeignField.mul; use it + multiRangeCheck([mBound, x3Bound, Field.from(0n)]); return { x: x3, y: y3 }; } @@ -390,18 +389,18 @@ function slice( [x0, x1, x2]: Field3, { maxBits, chunkSize }: { maxBits: number; chunkSize: number } ) { - let l = Number(L); - assert(maxBits <= 3 * l, `expected max bits <= 3*${l}, got ${maxBits}`); + let l_ = Number(l); + assert(maxBits <= 3 * l_, `expected max bits <= 3*${l_}, got ${maxBits}`); // first limb - let result0 = sliceField(x0, Math.min(l, maxBits), chunkSize); + let result0 = sliceField(x0, Math.min(l_, maxBits), chunkSize); if (maxBits <= l) return result0.chunks; - maxBits -= l; + maxBits -= l_; // second limb - let result1 = sliceField(x1, Math.min(l, maxBits), chunkSize, result0); - if (maxBits <= l) return result0.chunks.concat(result1.chunks); - maxBits -= l; + let result1 = sliceField(x1, Math.min(l_, maxBits), chunkSize, result0); + if (maxBits <= l_) return result0.chunks.concat(result1.chunks); + maxBits -= l_; // third limb let result2 = sliceField(x2, maxBits, chunkSize, result1); diff --git a/src/lib/gadgets/foreign-field.ts b/src/lib/gadgets/foreign-field.ts index 9f6f096e9..c8892a725 100644 --- a/src/lib/gadgets/foreign-field.ts +++ b/src/lib/gadgets/foreign-field.ts @@ -435,13 +435,11 @@ function assertRank1( // x is chained into the ffmul gate let x0 = x.finishForChaining(f); - let q2Bound = assertMul(x0, y0, xy0, f); + assertMul(x0, y0, xy0, f); // we need an extra range check on x and y, but not xy x.rangeCheck(); y.rangeCheck(); - - return q2Bound; } class Sum { From 9b29f407559f1f166cbcafd6d863407e259ae82a Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 17 Nov 2023 11:24:57 +0100 Subject: [PATCH 024/102] bound y (needed for doubling) --- src/lib/gadgets/elliptic-curve.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 89494d47e..d38cfd25c 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -82,7 +82,7 @@ function add(p1: Point, p2: Point, f: bigint) { multiRangeCheck(y3); let mBound = weakBound(m[2], f); let x3Bound = weakBound(x3[2], f); - // we dont need to bound y3[2] because it's never one of the inputs to a multiplication + let y3Bound = weakBound(y3[2], f); // (x1 - x2)*m = y1 - y2 let deltaX = new Sum(x1).sub(x2); @@ -99,7 +99,7 @@ function add(p1: Point, p2: Point, f: bigint) { assertRank1(deltaX1X3, m, ySum, f); // bounds checks - multiRangeCheck([mBound, x3Bound, Field.from(0n)]); + multiRangeCheck([mBound, x3Bound, y3Bound]); return { x: x3, y: y3 }; } @@ -135,7 +135,7 @@ function double(p1: Point, f: bigint) { multiRangeCheck(y3); let mBound = weakBound(m[2], f); let x3Bound = weakBound(x3[2], f); - // we dont need to bound y3[2] because it's never one of the inputs to a multiplication + let y3Bound = weakBound(y3[2], f); // x1^2 = x1x1 let x1x1 = ForeignField.mul(x1, x1, f); @@ -156,8 +156,7 @@ function double(p1: Point, f: bigint) { assertRank1(deltaX1X3, m, ySum, f); // bounds checks - // TODO: there is a secret free spot for two bounds in ForeignField.mul; use it - multiRangeCheck([mBound, x3Bound, Field.from(0n)]); + multiRangeCheck([mBound, x3Bound, y3Bound]); return { x: x3, y: y3 }; } From 38b9b7fbb4e0faafe42d55ec1cd2ecceccd65c93 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 17 Nov 2023 11:49:30 +0100 Subject: [PATCH 025/102] tweak initial aggregator --- src/lib/gadgets/ecdsa.unit-test.ts | 2 +- src/lib/gadgets/elliptic-curve.ts | 12 +++++++++--- src/lib/gadgets/elliptic-curve.unit-test.ts | 19 +++++++++++++------ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index 429d4c124..4c2bfead1 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -24,7 +24,7 @@ let msgHash = 0x3e91cd8bd233b3df4e4762b329e2922381da770df1b31276ec77d0557be7fcefn ); -const ia = EllipticCurve.initialAggregator(BaseField, Secp256k1); +const ia = EllipticCurve.initialAggregator(Secp256k1, BaseField); const tableConfig = { windowSizeG: 3, windowSizeP: 3 }; let program = ZkProgram({ diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index d38cfd25c..5a00485c9 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -18,6 +18,7 @@ import { l, multiRangeCheck } from './range-check.js'; import { sha256 } from 'js-sha256'; import { bigIntToBits, + bigIntToBytes, bytesToBigInt, } from '../../bindings/crypto/bigint-helpers.js'; import { @@ -355,9 +356,14 @@ function getPointTable( * It's important that this point has no known discrete logarithm so that nobody * can create an invalid proof of EC scaling. */ -function initialAggregator(F: FiniteField, { a, b }: { a: bigint; b: bigint }) { +function initialAggregator(Curve: CurveAffine, F: FiniteField) { + // hash that identifies the curve let h = sha256.create(); - h.update('o1js:ecdsa'); + h.update('ecdsa'); + h.update(bigIntToBytes(Curve.modulus)); + h.update(bigIntToBytes(Curve.order)); + h.update(bigIntToBytes(Curve.a)); + h.update(bigIntToBytes(Curve.b)); let bytes = h.array(); // bytes represent a 256-bit number @@ -370,7 +376,7 @@ function initialAggregator(F: FiniteField, { a, b }: { a: bigint; b: bigint }) { x = F.add(x, 1n); // solve y^2 = x^3 + ax + b let x3 = F.mul(F.square(x), x); - let y2 = F.add(x3, F.mul(a, x) + b); + let y2 = F.add(x3, F.mul(Curve.a, x) + Curve.b); y = F.sqrt(y2); } return { x, y, infinity: false }; diff --git a/src/lib/gadgets/elliptic-curve.unit-test.ts b/src/lib/gadgets/elliptic-curve.unit-test.ts index b851590e1..5569f4dbc 100644 --- a/src/lib/gadgets/elliptic-curve.unit-test.ts +++ b/src/lib/gadgets/elliptic-curve.unit-test.ts @@ -1,10 +1,17 @@ -import { exampleFields } from '../../bindings/crypto/finite-field-examples.js'; import { Provable } from '../provable.js'; import { Field3 } from './foreign-field.js'; import { EllipticCurve } from './elliptic-curve.js'; import { printGates } from '../testing/constraint-system.js'; import { assert } from './common.js'; -import { Pallas } from '../../bindings/crypto/elliptic_curve.js'; +import { createCurveAffine } from '../../bindings/crypto/elliptic_curve.js'; +import { Fp } from '../../bindings/crypto/finite_field.js'; +import { + pallasParams, + secp256k1Params, +} from '../../bindings/crypto/elliptic-curve-examples.js'; + +const Secp256k1 = createCurveAffine(secp256k1Params); +const Pallas = createCurveAffine(pallasParams); let { add, double, initialAggregator } = EllipticCurve; @@ -17,7 +24,7 @@ let csAdd = Provable.constraintSystem(() => { let g = { x: x1, y: y1 }; let h = { x: x2, y: y2 }; - add(g, h, exampleFields.secp256k1.modulus); + add(g, h, Secp256k1.modulus); }); let csDouble = Provable.constraintSystem(() => { @@ -26,7 +33,7 @@ let csDouble = Provable.constraintSystem(() => { let g = { x: x1, y: y1 }; - double(g, exampleFields.secp256k1.modulus); + double(g, Secp256k1.modulus); }); printGates(csAdd.gates); @@ -35,6 +42,6 @@ console.log({ digest: csAdd.digest, rows: csAdd.rows }); printGates(csDouble.gates); console.log({ digest: csDouble.digest, rows: csDouble.rows }); -let point = initialAggregator(exampleFields.Fp, { a: 0n, b: 5n }); +let point = initialAggregator(Pallas, Fp); console.log({ point }); -assert(Pallas.isOnCurve(Pallas.fromAffine(point))); +assert(Pallas.isOnCurve(point)); From 69a814d898669e2c531ad8316c2f4deff553a7dc Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 17 Nov 2023 11:49:34 +0100 Subject: [PATCH 026/102] bindings --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index 8f0e0d0f8..68df53a11 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 8f0e0d0f874dc3952bb01791c4b31394d7e5298f +Subproject commit 68df53a111e6717c594a8173f60a5c59b09aa3b2 From be15bf118c7c8e820f1b55d531c34aa3b07dbcbd Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 17 Nov 2023 12:15:42 +0100 Subject: [PATCH 027/102] remove unused function --- src/lib/gadgets/elliptic-curve.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 5a00485c9..e7f945e57 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -536,11 +536,3 @@ const Ecdsa = { verify: verifyEcdsa, Signature: EcdsaSignature, }; - -function gcd(a: number, b: number) { - if (b > a) [a, b] = [b, a]; - while (true) { - if (b === 0) return a; - [a, b] = [b, a % b]; - } -} From c0f297d2b3747784faf1090e1db71cf5f556a3b8 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 20 Nov 2023 13:31:09 +0100 Subject: [PATCH 028/102] generalize doubleScalarMul to multiScalarMul to deduplicate logic --- src/lib/gadgets/ecdsa.unit-test.ts | 12 ++- src/lib/gadgets/elliptic-curve.ts | 124 ++++++++++++++++------------- 2 files changed, 73 insertions(+), 63 deletions(-) diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index 4c2bfead1..cf1e12a87 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -25,7 +25,7 @@ let msgHash = ); const ia = EllipticCurve.initialAggregator(Secp256k1, BaseField); -const tableConfig = { windowSizeG: 3, windowSizeP: 3 }; +const tableConfig = { G: { windowSize: 3 }, P: { windowSize: 3 } }; let program = ZkProgram({ name: 'ecdsa', @@ -35,14 +35,12 @@ let program = ZkProgram({ method() { let G = Point.from(Secp256k1.one); let P = Provable.witness(Point, () => publicKey); - let R = EllipticCurve.doubleScalarMul( + let R = EllipticCurve.multiScalarMul( Secp256k1, ia, - signature.s, - G, - signature.r, - P, - tableConfig + [signature.s, signature.r], + [G, P], + [tableConfig.G, tableConfig.P] ); Provable.asProver(() => { console.log(Point.toBigint(R)); diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index e7f945e57..76f44813c 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -35,7 +35,7 @@ export { EllipticCurve, Point, Ecdsa, EcdsaSignature }; const EllipticCurve = { add, double, - doubleScalarMul, + multiScalarMul, initialAggregator, }; @@ -169,10 +169,8 @@ function verifyEcdsa( msgHash: Field3, publicKey: Point, tables?: { - windowSizeG?: number; - multiplesG?: Point[]; - windowSizeP?: number; - multiplesP?: Point[]; + G?: { windowSize: number; multiples?: Point[] }; + P?: { windowSize: number; multiples?: Point[] }; } ) { // constant case @@ -199,7 +197,13 @@ function verifyEcdsa( let u2 = ForeignField.mul(r, sInv, Curve.order); let G = Point.from(Curve.one); - let R = doubleScalarMul(Curve, ia, u1, G, u2, publicKey, tables); + let R = multiScalarMul( + Curve, + ia, + [u1, u2], + [G, publicKey], + tables && [tables.G, tables.P] + ); // this ^ already proves that R != 0 // reduce R.x modulo the curve order @@ -208,87 +212,95 @@ function verifyEcdsa( } /** - * Scalar mul that we need for ECDSA: + * Multi-scalar multiplication: * - * s*G + t*P, + * s_0 * P_0 + ... + s_(n-1) * P_(n-1) * - * where G, P are any points. The result is not allowed to be zero. + * where P_i are any points. The result is not allowed to be zero. * - * We double both points together and leverage a precomputed table - * of size 2^c to avoid all but every cth addition for both s*G and t*P. + * 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. * * TODO: could use lookups for picking precomputed multiples, instead of O(2^c) provable switch * TODO: custom bit representation for the scalar that avoids 0, to get rid of the degenerate addition case * 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 doubleScalarMul( +function multiScalarMul( Curve: CurveAffine, ia: point, - s: Field3, - G: Point, - t: Field3, - P: Point, - { - // what we called c before - windowSizeG = 1, - // G, ..., (2^c-1)*G - multiplesG = undefined as Point[] | undefined, - windowSizeP = 1, - multiplesP = undefined as Point[] | undefined, - } = {} + scalars: Field3[], + points: Point[], + tableConfigs: ( + | { + // what we called c before + windowSize?: number; + // G, ..., (2^c-1)*G + multiples?: Point[]; + } + | undefined + )[] = [] ): Point { + let n = points.length; + assert(scalars.length === n, 'Points and scalars lengths must match'); + assertPositiveInteger(n, 'Expected at least 1 point and scalar'); + // constant case if ( - Field3.isConstant(s) && - Field3.isConstant(t) && - Provable.isConstant(Point, G) && - Provable.isConstant(Point, P) + scalars.every(Field3.isConstant) && + points.every((P) => Provable.isConstant(Point, P)) ) { - let s_ = Field3.toBigint(s); - let t_ = Field3.toBigint(t); - let G_ = Point.toBigint(G); - let P_ = Point.toBigint(P); - let R = Curve.add(Curve.scale(G_, s_), Curve.scale(P_, t_)); - return Point.from(R); + // TODO dedicated MSM + let s = scalars.map(Field3.toBigint); + let P = points.map(Point.toBigint); + let sum = Curve.zero; + for (let i = 0; i < n; i++) { + sum = Curve.add(sum, Curve.scale(P[i], s[i])); + } + return Point.from(sum); } // parse or build point tables - let Gs = getPointTable(Curve, G, windowSizeG, multiplesG); - let Ps = getPointTable(Curve, P, windowSizeP, multiplesP); + let windowSizes = points.map((_, i) => tableConfigs[i]?.windowSize ?? 1); + let tables = points.map((P, i) => + getPointTable(Curve, P, windowSizes[i], tableConfigs[i]?.multiples) + ); // slice scalars let b = Curve.order.toString(2).length; - let ss = slice(s, { maxBits: b, chunkSize: windowSizeG }); - let ts = slice(t, { maxBits: b, chunkSize: windowSizeP }); + let scalarChunks = scalars.map((s, i) => + slice(s, { maxBits: b, chunkSize: windowSizes[i] }) + ); let sum = Point.from(ia); for (let i = b - 1; i >= 0; i--) { - if (i % windowSizeG === 0) { - // pick point to add based on the scalar chunk - let sj = ss[i / windowSizeG]; - let Gj = windowSizeG === 1 ? G : arrayGet(Point, Gs, sj, { offset: 1 }); - - // ec addition - let added = add(sum, Gj, Curve.modulus); - - // handle degenerate case (if sj = 0, Gj is all zeros and the add result is garbage) - sum = Provable.if(sj.equals(0), Point, sum, added); + // add in multiple of each point + for (let j = 0; j < n; j++) { + let windowSize = windowSizes[j]; + if (i % windowSize === 0) { + // pick point to add based on the scalar chunk + let sj = scalarChunks[j][i / windowSize]; + let sjP = + windowSize === 1 + ? points[j] + : arrayGet(Point, tables[j], sj, { offset: 1 }); + + // ec addition + let added = add(sum, sjP, Curve.modulus); + + // handle degenerate case (if sj = 0, Gj is all zeros and the add result is garbage) + sum = Provable.if(sj.equals(0), Point, sum, added); + } } - if (i % windowSizeP === 0) { - let tj = ts[i / windowSizeP]; - let Pj = windowSizeP === 1 ? P : arrayGet(Point, Ps, tj, { offset: 1 }); - let added = add(sum, Pj, Curve.modulus); - sum = Provable.if(tj.equals(0), Point, sum, added); - } if (i === 0) break; - // jointly double both points + // jointly double all points sum = double(sum, Curve.modulus); } - // the sum is now s*G + t*P + 2^(b-1)*IA + // 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)); Provable.equal(Point, sum, Point.from(iaFinal)).assertFalse(); From 241aae193d5b207753b0c9e60334c3ceecb5b651 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 20 Nov 2023 14:52:48 +0100 Subject: [PATCH 029/102] save >3k constraints by optimizing array get gadget --- src/lib/gadgets/basic.ts | 68 +++++++++++++++++++++++++++++++ src/lib/gadgets/elliptic-curve.ts | 47 ++++++++++++--------- 2 files changed, 96 insertions(+), 19 deletions(-) create mode 100644 src/lib/gadgets/basic.ts diff --git a/src/lib/gadgets/basic.ts b/src/lib/gadgets/basic.ts new file mode 100644 index 000000000..79209efef --- /dev/null +++ b/src/lib/gadgets/basic.ts @@ -0,0 +1,68 @@ +import { Fp } from '../../bindings/crypto/finite_field.js'; +import type { Field } from '../field.js'; +import { existsOne, toVar } from './common.js'; +import { Gates } from '../gates.js'; + +export { arrayGet }; + +// TODO: create constant versions of these and expose on Gadgets + +/** + * Get value from array in O(n) rows. + * + * Assumes that index is in [0, n), returns an unconstrained result otherwise. + * + * Note: This saves 0.5*n constraints compared to equals() + switch() + */ +function arrayGet(array: Field[], index: Field) { + index = toVar(index); + + // witness result + let a = existsOne(() => array[Number(index.toBigInt())].toBigInt()); + + // we prove a === array[j] + zj*(index - j) for some zj, for all j. + // setting j = index, this implies a === array[index] + // thanks to our assumption that the index is within bounds, we know that j = index for some j + let n = array.length; + for (let j = 0; j < n; j++) { + let zj = existsOne(() => { + let zj = Fp.div( + Fp.sub(a.toBigInt(), array[j].toBigInt()), + Fp.sub(index.toBigInt(), Fp.fromNumber(j)) + ); + return zj ?? 0n; + }); + // prove that zj*(index - j) === a - array[j] + // TODO abstract this logic into a general-purpose assertMul() gadget, + // which is able to use the constant coefficient + // (snarky's assert_r1cs somehow leads to much more constraints than this) + if (array[j].isConstant()) { + // -j*zj + zj*index - a + array[j] === 0 + Gates.generic( + { + left: -BigInt(j), + right: 0n, + out: -1n, + mul: 1n, + const: array[j].toBigInt(), + }, + { left: zj, right: index, out: a } + ); + } else { + let aMinusAj = toVar(a.sub(array[j])); + // -j*zj + zj*index - (a - array[j]) === 0 + Gates.generic( + { + left: -BigInt(j), + right: 0n, + out: -1n, + mul: 1n, + const: 0n, + }, + { left: zj, right: index, out: aMinusAj } + ); + } + } + + return a; +} diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 76f44813c..414d39490 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -29,6 +29,8 @@ import { import { Bool } from '../bool.js'; import { provable } from '../circuit_value.js'; import { assertPositiveInteger } from '../../bindings/crypto/non-negative.js'; +import { ProvablePure } from '../../snarky.js'; +import { arrayGet } from './basic.js'; export { EllipticCurve, Point, Ecdsa, EcdsaSignature }; @@ -262,9 +264,17 @@ function multiScalarMul( // parse or build point tables let windowSizes = points.map((_, i) => tableConfigs[i]?.windowSize ?? 1); - let tables = points.map((P, i) => - getPointTable(Curve, P, windowSizes[i], tableConfigs[i]?.multiples) - ); + let tables = points.map((P, i) => { + let table = getPointTable( + Curve, + P, + windowSizes[i], + tableConfigs[i]?.multiples + ); + // add zero point in the beginning + table.unshift(Point.from(Curve.zero)); + return table; + }); // slice scalars let b = Curve.order.toString(2).length; @@ -282,9 +292,7 @@ function multiScalarMul( // pick point to add based on the scalar chunk let sj = scalarChunks[j][i / windowSize]; let sjP = - windowSize === 1 - ? points[j] - : arrayGet(Point, tables[j], sj, { offset: 1 }); + windowSize === 1 ? points[j] : arrayGetGeneric(Point, tables[j], sj); // ec addition let added = add(sum, sjP, Curve.modulus); @@ -491,21 +499,22 @@ function sliceField( /** * Get value from array in O(n) constraints. * - * If the index is out of bounds, returns all-zeros version of T + * Assumes that index is in [0, n), returns an unconstrained result otherwise. */ -function arrayGet( - type: Provable, - array: T[], - index: Field, - { offset = 0 } = {} -) { - let n = array.length; - let oneHot = Array(n); - // TODO can we share computation between all those equals()? - for (let i = 0; i < n; i++) { - oneHot[i] = index.equals(i + offset); +function arrayGetGeneric(type: Provable, array: T[], index: Field) { + // witness result + let a = Provable.witness(type, () => array[Number(index)]); + let aFields = type.toFields(a); + + // constrain each field of the result + let size = type.sizeInFields(); + let arrays = array.map(type.toFields); + + for (let j = 0; j < size; j++) { + let arrayFieldsJ = arrays.map((x) => x[j]); + arrayGet(arrayFieldsJ, index).assertEquals(aFields[j]); } - return Provable.switch(oneHot, type, array); + return a; } const Point = { From f4801d0f97ae08c32eebe268d017727a4308c787 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 20 Nov 2023 14:57:27 +0100 Subject: [PATCH 030/102] clean up table logic --- src/lib/gadgets/elliptic-curve.ts | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 414d39490..4e4fb65fa 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -264,17 +264,9 @@ function multiScalarMul( // parse or build point tables let windowSizes = points.map((_, i) => tableConfigs[i]?.windowSize ?? 1); - let tables = points.map((P, i) => { - let table = getPointTable( - Curve, - P, - windowSizes[i], - tableConfigs[i]?.multiples - ); - // add zero point in the beginning - table.unshift(Point.from(Curve.zero)); - return table; - }); + let tables = points.map((P, i) => + getPointTable(Curve, P, windowSizes[i], tableConfigs[i]?.multiples) + ); // slice scalars let b = Curve.order.toString(2).length; @@ -351,17 +343,17 @@ function getPointTable( table?: Point[] ): Point[] { assertPositiveInteger(windowSize, 'invalid window size'); - let n = (1 << windowSize) - 1; // n >= 1 + let n = 1 << windowSize; // n >= 2 assert(table === undefined || table.length === n, 'invalid table'); if (table !== undefined) return table; - table = [P]; - if (n === 1) return table; + table = [Point.from(Curve.zero), P]; + if (n === 2) return table; let Pi = double(P, Curve.modulus); table.push(Pi); - for (let i = 2; i < n; i++) { + for (let i = 3; i < n; i++) { Pi = add(Pi, P, Curve.modulus); table.push(Pi); } From 80dbef6cd64f32cc50f0305b2f7828613cbd4f09 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 20 Nov 2023 15:18:41 +0100 Subject: [PATCH 031/102] optimal window size changed --- src/lib/gadgets/ecdsa.unit-test.ts | 2 +- src/lib/gadgets/elliptic-curve.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index cf1e12a87..ff303b3cc 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -25,7 +25,7 @@ let msgHash = ); const ia = EllipticCurve.initialAggregator(Secp256k1, BaseField); -const tableConfig = { G: { windowSize: 3 }, P: { windowSize: 3 } }; +const tableConfig = { G: { windowSize: 4 }, P: { windowSize: 4 } }; let program = ZkProgram({ name: 'ecdsa', diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 4e4fb65fa..9127cd789 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -297,6 +297,7 @@ function multiScalarMul( if (i === 0) break; // 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); } From 73d2b2dee9228006d0ce77733d1c2a3e43a0a6a1 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 20 Nov 2023 22:02:02 +0100 Subject: [PATCH 032/102] replace mul input rcs with generic gates --- src/lib/gadgets/elliptic-curve.ts | 1 - src/lib/gadgets/foreign-field.ts | 106 ++++++++++++++++++++++++++---- 2 files changed, 94 insertions(+), 13 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 9127cd789..a2a180adb 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -29,7 +29,6 @@ import { import { Bool } from '../bool.js'; import { provable } from '../circuit_value.js'; import { assertPositiveInteger } from '../../bindings/crypto/non-negative.js'; -import { ProvablePure } from '../../snarky.js'; import { arrayGet } from './basic.js'; export { EllipticCurve, Point, Ecdsa, EcdsaSignature }; diff --git a/src/lib/gadgets/foreign-field.ts b/src/lib/gadgets/foreign-field.ts index 49dcdceb3..374a26a0d 100644 --- a/src/lib/gadgets/foreign-field.ts +++ b/src/lib/gadgets/foreign-field.ts @@ -5,6 +5,7 @@ import { import { provableTuple } from '../../bindings/lib/provable-snarky.js'; import { Field } from '../field.js'; import { Gates, foreignFieldAdd } from '../gates.js'; +import { Provable } from '../provable.js'; import { Tuple } from '../util/types.js'; import { assert, bitSlice, exists, toVars } from './common.js'; import { @@ -402,14 +403,15 @@ function split2(x: bigint): [bigint, bigint] { /** * Optimized multiplication of sums, like (x + y)*z = a + b + c * - * We use two optimizations over naive summing and then multiplying: + * We use several optimizations over naive summing and then multiplying: * * - we skip the range check on the remainder sum, because ffmul is sound with r being a sum of range-checked values + * - we replace the range check on the input sums with an extra low limb sum using generic gates * - we chain the first input's sum into the ffmul gate * * As usual, all values are assumed to be range checked, and the left and right multiplication inputs * are assumed to be bounded such that `l * r < 2^264 * (native modulus)`. - * However, all extra checks that are needed on the sums are handled here. + * However, all extra checks that are needed on the _sums_ are handled here. * * TODO example */ @@ -419,19 +421,19 @@ function assertRank1( xy: Field3 | Sum, f: bigint ) { - x = Sum.fromUnfinished(x, f); - y = Sum.fromUnfinished(y, f); - xy = Sum.fromUnfinished(xy, f); + x = Sum.fromUnfinished(x); + y = Sum.fromUnfinished(y); + xy = Sum.fromUnfinished(xy); // finish the y and xy sums with a zero gate let y0 = y.finish(f); let xy0 = xy.finish(f); // x is chained into the ffmul gate - let x0 = x.finishForChaining(f); + let x0 = x.finish(f, true); assertMul(x0, y0, xy0, f); - // we need an extra range check on x and y, but not xy + // we need and extra range check on x and y x.rangeCheck(); y.rangeCheck(); } @@ -464,10 +466,17 @@ class Sum { return this; } - finish(f: bigint, forChaining = false) { + finishOne() { + let result = this.#summands[0]; + this.#result = result; + return result; + } + + finish(f: bigint, isChained = false) { assert(this.#result === undefined, 'sum already finished'); let signs = this.#ops; let n = signs.length; + if (n === 0) return this.finishOne(); let x = this.#summands.map(toVars); let result = x[0]; @@ -475,14 +484,87 @@ class Sum { for (let i = 0; i < n; i++) { ({ result } = singleAdd(result, x[i + 1], signs[i], f)); } - if (n > 0 && !forChaining) Gates.zero(...result); + if (!isChained) Gates.zero(...result); this.#result = result; return result; } - finishForChaining(f: bigint) { - return this.finish(f, true); + finishForMulInput(f: bigint, isChained = false) { + assert(this.#result === undefined, 'sum already finished'); + let signs = this.#ops; + let n = signs.length; + if (n === 0) return this.finishOne(); + + let x = this.#summands.map(toVars); + + // since the sum becomes a multiplication input, we need to constrain all limbs _individually_. + // sadly, ffadd only constrains the low and middle limb together. + // we could fix it with a RC just for the lower two limbs + // but it's cheaper to add generic gates which handle the lowest limb separately, and avoids the unfilled MRC slot + let f_ = split(f); + + // compute witnesses for generic gates -- overflows and carries + let nFields = Provable.Array(Field, n); + let [overflows, carries] = Provable.witness( + provableTuple([nFields, nFields]), + () => { + let overflows: bigint[] = []; + let carries: bigint[] = []; + + let r = Field3.toBigint(x[0]); + + for (let i = 0; i < n; i++) { + // this duplicates some of the logic in singleAdd + let x_ = split(r); + let y_ = bigint3(x[i + 1]); + let sign = signs[i]; + + // figure out if there's overflow + r = r + sign * collapse(y_); + let overflow = 0n; + if (sign === 1n && r >= f) overflow = 1n; + if (sign === -1n && r < 0n) overflow = -1n; + if (f === 0n) overflow = 0n; + overflows.push(overflow); + + // add with carry, only on the lowest limb + let r0 = x_[0] + sign * y_[0] - overflow * f_[0]; + carries.push(r0 >> l); + } + return [overflows.map(Field.from), carries.map(Field.from)]; + } + ); + + // generic gates for low limbs + let result0 = x[0][0]; + let r0s: Field[] = []; + for (let i = 0; i < n; i++) { + // constrain carry to 0, 1, or -1 + let c = carries[i]; + c.mul(c.sub(1n)).mul(c.add(1n)).assertEquals(0n); + + result0 = result0 + .add(x[i + 1][0].mul(signs[i])) + .add(overflows[i].mul(f_[0])) + .sub(c.mul(1n << l)) + .seal(); + r0s.push(result0); + } + + // ffadd chain + let result = x[0]; + for (let i = 0; i < n; i++) { + let r = singleAdd(result, x[i + 1], signs[i], f); + // wire low limb and overflow to previous values + r.result[0].assertEquals(r0s[i]); + r.overflow.assertEquals(overflows[i]); + result = r.result; + } + if (!isChained) Gates.zero(...result); + + this.#result = result; + return result; } rangeCheck() { @@ -490,7 +572,7 @@ class Sum { if (this.#ops.length > 0) multiRangeCheck(this.#result); } - static fromUnfinished(x: Field3 | Sum, f: bigint) { + static fromUnfinished(x: Field3 | Sum) { if (x instanceof Sum) { assert(x.#result === undefined, 'sum already finished'); return x; From 21050b6391942cabaa5edaf4f9518fb0f6b4d040 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 21 Nov 2023 11:03:43 +0100 Subject: [PATCH 033/102] enable strict typing of variable fields --- src/lib/field.ts | 15 +++++++++++++-- src/lib/gadgets/common.ts | 16 ++++++++++------ src/snarky.d.ts | 8 ++++---- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/lib/field.ts b/src/lib/field.ts index de52ecf62..9e2497e23 100644 --- a/src/lib/field.ts +++ b/src/lib/field.ts @@ -11,10 +11,12 @@ export { Field }; // internal API export { - ConstantField, FieldType, FieldVar, FieldConst, + ConstantField, + VarField, + VarFieldVar, isField, withMessage, readVarMessage, @@ -69,6 +71,7 @@ type FieldVar = | [FieldType.Scale, FieldConst, FieldVar]; type ConstantFieldVar = [FieldType.Constant, FieldConst]; +type VarFieldVar = [FieldType.Var, number]; const FieldVar = { constant(x: bigint | FieldConst): ConstantFieldVar { @@ -78,6 +81,9 @@ const FieldVar = { isConstant(x: FieldVar): x is ConstantFieldVar { return x[0] === FieldType.Constant; }, + isVar(x: FieldVar): x is VarFieldVar { + return x[0] === FieldType.Var; + }, add(x: FieldVar, y: FieldVar): FieldVar { if (FieldVar.isConstant(x) && x[1][1] === 0n) return y; if (FieldVar.isConstant(y) && y[1][1] === 0n) return x; @@ -101,6 +107,7 @@ const FieldVar = { }; type ConstantField = Field & { value: ConstantFieldVar }; +type VarField = Field & { value: VarFieldVar }; /** * A {@link Field} is an element of a prime order [finite field](https://en.wikipedia.org/wiki/Finite_field). @@ -1039,7 +1046,7 @@ class Field { seal() { if (this.isConstant()) return this; let x = Snarky.field.seal(this.value); - return new Field(x); + return VarField(x); } /** @@ -1360,3 +1367,7 @@ there is \`Provable.asProver(() => { ... })\` which allows you to use ${varName} Warning: whatever happens inside asProver() will not be part of the zk proof. `; } + +function VarField(x: VarFieldVar): VarField { + return new Field(x) as VarField; +} diff --git a/src/lib/gadgets/common.ts b/src/lib/gadgets/common.ts index 196ca64e7..1b52023ab 100644 --- a/src/lib/gadgets/common.ts +++ b/src/lib/gadgets/common.ts @@ -1,5 +1,5 @@ import { Provable } from '../provable.js'; -import { Field, FieldConst, FieldVar, FieldType } from '../field.js'; +import { Field, FieldConst, FieldVar, VarField } from '../field.js'; import { Tuple, TupleN } from '../util/types.js'; import { Snarky } from '../../snarky.js'; import { MlArray } from '../ml/base.js'; @@ -21,7 +21,7 @@ export { function existsOne(compute: () => bigint) { let varMl = Snarky.existsVar(() => FieldConst.fromBigint(compute())); - return new Field(varMl); + return VarField(varMl); } function exists TupleN>( @@ -31,7 +31,7 @@ function exists TupleN>( let varsMl = Snarky.exists(n, () => MlArray.mapTo(compute(), FieldConst.fromBigint) ); - let vars = MlArray.mapFrom(varsMl, (v) => new Field(v)); + let vars = MlArray.mapFrom(varsMl, VarField); return TupleN.fromArray(n, vars); } @@ -43,20 +43,24 @@ function exists TupleN>( * * Same as `Field.seal()` with the difference that `seal()` leaves constants as is. */ -function toVar(x: Field | bigint) { +function toVar(x: Field | bigint): VarField { // don't change existing vars - if (x instanceof Field && x.value[1] === FieldType.Var) return x; + if (isVar(x)) return x; let xVar = existsOne(() => Field.from(x).toBigInt()); xVar.assertEquals(x); return xVar; } +function isVar(x: Field | bigint): x is VarField { + return x instanceof Field && FieldVar.isVar(x.value); +} + /** * Apply {@link toVar} to each element of a tuple. */ function toVars>( fields: T -): { [k in keyof T]: Field } { +): { [k in keyof T]: VarField } { return Tuple.map(fields, toVar); } diff --git a/src/snarky.d.ts b/src/snarky.d.ts index 69fe3bcb3..343d714c4 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -1,5 +1,5 @@ import type { Account as JsonAccount } from './bindings/mina-transaction/gen/transaction-json.js'; -import type { Field, FieldConst, FieldVar } from './lib/field.js'; +import type { Field, FieldConst, FieldVar, VarFieldVar } from './lib/field.js'; import type { BoolVar, Bool } from './lib/bool.js'; import type { ScalarConst } from './lib/scalar.js'; import type { @@ -181,11 +181,11 @@ declare const Snarky: { exists( sizeInFields: number, compute: () => MlArray - ): MlArray; + ): MlArray; /** * witness a single field element variable */ - existsVar(compute: () => FieldConst): FieldVar; + existsVar(compute: () => FieldConst): VarFieldVar; /** * APIs that have to do with running provable code @@ -281,7 +281,7 @@ declare const Snarky: { * returns a new witness from an AST * (implemented with toConstantAndTerms) */ - seal(x: FieldVar): FieldVar; + seal(x: FieldVar): VarFieldVar; /** * Unfolds AST to get `x = c + c0*Var(i0) + ... + cn*Var(in)`, * returns `(c, [(c0, i0), ..., (cn, in)])`; From 89e0cddf05d357d9994bf6f5680c7ba163173e64 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 21 Nov 2023 11:07:41 +0100 Subject: [PATCH 034/102] optimized assertOneOf gadget --- src/lib/gadgets/basic.ts | 70 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/src/lib/gadgets/basic.ts b/src/lib/gadgets/basic.ts index 79209efef..c48b84e3e 100644 --- a/src/lib/gadgets/basic.ts +++ b/src/lib/gadgets/basic.ts @@ -1,9 +1,10 @@ import { Fp } from '../../bindings/crypto/finite_field.js'; -import type { Field } from '../field.js'; +import type { Field, VarField } from '../field.js'; import { existsOne, toVar } from './common.js'; import { Gates } from '../gates.js'; +import { TupleN } from '../util/types.js'; -export { arrayGet }; +export { arrayGet, assertOneOf }; // TODO: create constant versions of these and expose on Gadgets @@ -66,3 +67,68 @@ function arrayGet(array: Field[], index: Field) { return a; } + +/** + * Assert that a value equals one of a finite list of constants: + * `(x - c1)*(x - c2)*...*(x - cn) === 0` + * + * TODO: what prevents us from getting the same efficiency with snarky DSL code? + */ +function assertOneOf(x: Field, allowed: [bigint, bigint, ...bigint[]]) { + let xv = toVar(x); + let [c1, c2, ...c] = allowed; + let n = c.length; + if (n === 0) { + // (x - c1)*(x - c2) === 0 + assertBilinearZero(xv, xv, [1n, -(c1 + c2), 0n, c1 * c2]); + return; + } + // z = (x - c1)*(x - c2) + let z = bilinear(xv, xv, [1n, -(c1 + c2), 0n, c1 * c2]); + + for (let i = 0; i < n; i++) { + if (i < n - 1) { + // z = z*(x - c) + z = bilinear(z, xv, [1n, -c[i], 0n, 0n]); + } else { + // z*(x - c) === 0 + assertBilinearZero(z, xv, [1n, -c[i], 0n, 0n]); + } + } +} + +// low-level helpers to create generic gates + +/** + * Compute bilinear function of x and y: + * z = a*x*y + b*x + c*y + d + */ +function bilinear(x: VarField, y: VarField, [a, b, c, d]: TupleN) { + let z = existsOne(() => { + let x0 = x.toBigInt(); + let y0 = y.toBigInt(); + return a * x0 * y0 + b * x0 + c * y0 + d; + }); + // b*x + c*y - z + a*x*y + d === 0 + Gates.generic( + { left: b, right: c, out: -1n, mul: a, const: d }, + { left: x, right: y, out: z } + ); + return z; +} + +/** + * Assert bilinear equation on x and y: + * a*x*y + b*x + c*y + d === 0 + */ +function assertBilinearZero( + x: VarField, + y: VarField, + [a, b, c, d]: TupleN +) { + // b*x + c*y + a*x*y + d === 0 + Gates.generic( + { left: b, right: c, out: 0n, mul: a, const: d }, + { left: x, right: y, out: x } + ); +} From d0a315910a18cddebcf5f624b8f6ccc81f4cb767 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 21 Nov 2023 11:29:33 +0100 Subject: [PATCH 035/102] simplify low-level gadgets --- src/lib/gadgets/basic.ts | 63 +++++++++++++++------------------------ src/lib/gadgets/common.ts | 1 + 2 files changed, 25 insertions(+), 39 deletions(-) diff --git a/src/lib/gadgets/basic.ts b/src/lib/gadgets/basic.ts index c48b84e3e..b7cd69e44 100644 --- a/src/lib/gadgets/basic.ts +++ b/src/lib/gadgets/basic.ts @@ -16,52 +16,34 @@ export { arrayGet, assertOneOf }; * Note: This saves 0.5*n constraints compared to equals() + switch() */ function arrayGet(array: Field[], index: Field) { - index = toVar(index); + let i = toVar(index); // witness result - let a = existsOne(() => array[Number(index.toBigInt())].toBigInt()); + let a = existsOne(() => array[Number(i.toBigInt())].toBigInt()); - // we prove a === array[j] + zj*(index - j) for some zj, for all j. - // setting j = index, this implies a === array[index] - // thanks to our assumption that the index is within bounds, we know that j = index for some j + // we prove a === array[j] + zj*(i - j) for some zj, for all j. + // setting j = i, this implies a === array[i] + // thanks to our assumption that the index i is within bounds, we know that j = i for some j let n = array.length; for (let j = 0; j < n; j++) { let zj = existsOne(() => { let zj = Fp.div( Fp.sub(a.toBigInt(), array[j].toBigInt()), - Fp.sub(index.toBigInt(), Fp.fromNumber(j)) + Fp.sub(i.toBigInt(), Fp.fromNumber(j)) ); return zj ?? 0n; }); - // prove that zj*(index - j) === a - array[j] + // prove that zj*(i - j) === a - array[j] // TODO abstract this logic into a general-purpose assertMul() gadget, // which is able to use the constant coefficient // (snarky's assert_r1cs somehow leads to much more constraints than this) if (array[j].isConstant()) { - // -j*zj + zj*index - a + array[j] === 0 - Gates.generic( - { - left: -BigInt(j), - right: 0n, - out: -1n, - mul: 1n, - const: array[j].toBigInt(), - }, - { left: zj, right: index, out: a } - ); + // zj*i + (-j)*zj + 0*i + array[j] === a + assertBilinear(zj, i, [1n, -BigInt(j), 0n, array[j].toBigInt()], a); } else { let aMinusAj = toVar(a.sub(array[j])); - // -j*zj + zj*index - (a - array[j]) === 0 - Gates.generic( - { - left: -BigInt(j), - right: 0n, - out: -1n, - mul: 1n, - const: 0n, - }, - { left: zj, right: index, out: aMinusAj } - ); + // zj*i + (-j)*zj + 0*i + 0 === (a - array[j]) + assertBilinear(zj, i, [1n, -BigInt(j), 0n, 0n], aMinusAj); } } @@ -80,7 +62,7 @@ function assertOneOf(x: Field, allowed: [bigint, bigint, ...bigint[]]) { let n = c.length; if (n === 0) { // (x - c1)*(x - c2) === 0 - assertBilinearZero(xv, xv, [1n, -(c1 + c2), 0n, c1 * c2]); + assertBilinear(xv, xv, [1n, -(c1 + c2), 0n, c1 * c2]); return; } // z = (x - c1)*(x - c2) @@ -92,7 +74,7 @@ function assertOneOf(x: Field, allowed: [bigint, bigint, ...bigint[]]) { z = bilinear(z, xv, [1n, -c[i], 0n, 0n]); } else { // z*(x - c) === 0 - assertBilinearZero(z, xv, [1n, -c[i], 0n, 0n]); + assertBilinear(z, xv, [1n, -c[i], 0n, 0n]); } } } @@ -101,7 +83,7 @@ function assertOneOf(x: Field, allowed: [bigint, bigint, ...bigint[]]) { /** * Compute bilinear function of x and y: - * z = a*x*y + b*x + c*y + d + * `z = a*x*y + b*x + c*y + d` */ function bilinear(x: VarField, y: VarField, [a, b, c, d]: TupleN) { let z = existsOne(() => { @@ -118,17 +100,20 @@ function bilinear(x: VarField, y: VarField, [a, b, c, d]: TupleN) { } /** - * Assert bilinear equation on x and y: - * a*x*y + b*x + c*y + d === 0 + * Assert bilinear equation on x, y and z: + * `a*x*y + b*x + c*y + d === z` + * + * The default for z is 0. */ -function assertBilinearZero( +function assertBilinear( x: VarField, y: VarField, - [a, b, c, d]: TupleN + [a, b, c, d]: TupleN, + z?: VarField ) { - // b*x + c*y + a*x*y + d === 0 + // b*x + c*y - z + a*x*y + d === z Gates.generic( - { left: b, right: c, out: 0n, mul: a, const: d }, - { left: x, right: y, out: x } + { left: b, right: c, out: z === undefined ? 0n : -1n, mul: a, const: d }, + { left: x, right: y, out: z === undefined ? x : z } ); } diff --git a/src/lib/gadgets/common.ts b/src/lib/gadgets/common.ts index 1b52023ab..e6e6c873c 100644 --- a/src/lib/gadgets/common.ts +++ b/src/lib/gadgets/common.ts @@ -12,6 +12,7 @@ export { existsOne, toVars, toVar, + isVar, assert, bitSlice, witnessSlice, From 64610e7d13e5135ca74f4838adad2ad91abbd2b8 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 21 Nov 2023 11:30:33 +0100 Subject: [PATCH 036/102] make new assertRank1 more efficient and enable it --- src/lib/gadgets/elliptic-curve.ts | 4 +--- src/lib/gadgets/foreign-field.ts | 36 +++++++++++++++---------------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index a2a180adb..60d9646d5 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -85,6 +85,7 @@ function add(p1: Point, p2: Point, f: bigint) { let mBound = weakBound(m[2], f); let x3Bound = weakBound(x3[2], f); let y3Bound = weakBound(y3[2], f); + multiRangeCheck([mBound, x3Bound, y3Bound]); // (x1 - x2)*m = y1 - y2 let deltaX = new Sum(x1).sub(x2); @@ -100,9 +101,6 @@ function add(p1: Point, p2: Point, f: bigint) { let ySum = new Sum(y1).add(y3); assertRank1(deltaX1X3, m, ySum, f); - // bounds checks - multiRangeCheck([mBound, x3Bound, y3Bound]); - return { x: x3, y: y3 }; } diff --git a/src/lib/gadgets/foreign-field.ts b/src/lib/gadgets/foreign-field.ts index 374a26a0d..b0abeff8c 100644 --- a/src/lib/gadgets/foreign-field.ts +++ b/src/lib/gadgets/foreign-field.ts @@ -7,7 +7,8 @@ import { Field } from '../field.js'; import { Gates, foreignFieldAdd } from '../gates.js'; import { Provable } from '../provable.js'; import { Tuple } from '../util/types.js'; -import { assert, bitSlice, exists, toVars } from './common.js'; +import { assertOneOf } from './basic.js'; +import { assert, bitSlice, exists, toVar, toVars } from './common.js'; import { l, lMask, @@ -426,16 +427,12 @@ function assertRank1( xy = Sum.fromUnfinished(xy); // finish the y and xy sums with a zero gate - let y0 = y.finish(f); + let y0 = y.finishForMulInput(f); let xy0 = xy.finish(f); // x is chained into the ffmul gate - let x0 = x.finish(f, true); + let x0 = x.finishForMulInput(f, true); assertMul(x0, y0, xy0, f); - - // we need and extra range check on x and y - x.rangeCheck(); - y.rangeCheck(); } class Sum { @@ -490,6 +487,7 @@ class Sum { return result; } + // TODO this is complex and should be removed once we fix the ffadd gate to constrain all limbs individually finishForMulInput(f: bigint, isChained = false) { assert(this.#result === undefined, 'sum already finished'); let signs = this.#ops; @@ -537,19 +535,21 @@ class Sum { ); // generic gates for low limbs - let result0 = x[0][0]; - let r0s: Field[] = []; + let x0 = x[0][0]; + let x0s: Field[] = []; for (let i = 0; i < n; i++) { // constrain carry to 0, 1, or -1 let c = carries[i]; - c.mul(c.sub(1n)).mul(c.add(1n)).assertEquals(0n); - - result0 = result0 - .add(x[i + 1][0].mul(signs[i])) - .add(overflows[i].mul(f_[0])) - .sub(c.mul(1n << l)) - .seal(); - r0s.push(result0); + assertOneOf(c, [0n, 1n, -1n]); + + // x0 <- x0 + s*y0 - o*f0 - c*2^l + x0 = toVar( + x0 + .add(x[i + 1][0].mul(signs[i])) + .sub(overflows[i].mul(f_[0])) + .sub(c.mul(1n << l)) + ); + x0s.push(x0); } // ffadd chain @@ -557,7 +557,7 @@ class Sum { for (let i = 0; i < n; i++) { let r = singleAdd(result, x[i + 1], signs[i], f); // wire low limb and overflow to previous values - r.result[0].assertEquals(r0s[i]); + r.result[0].assertEquals(x0s[i]); r.overflow.assertEquals(overflows[i]); result = r.result; } From eacc1e606f06f6bdf217918d46c4c88d63cd093b Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 21 Nov 2023 12:57:42 +0100 Subject: [PATCH 037/102] expose assertMul and Sum --- src/lib/gadgets/foreign-field.ts | 44 ++++++++++++++++---------------- src/lib/gadgets/gadgets.ts | 16 +++++++++++- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/lib/gadgets/foreign-field.ts b/src/lib/gadgets/foreign-field.ts index b0abeff8c..f60663e98 100644 --- a/src/lib/gadgets/foreign-field.ts +++ b/src/lib/gadgets/foreign-field.ts @@ -19,18 +19,11 @@ import { compactMultiRangeCheck, } from './range-check.js'; -export { - ForeignField, - Field3, - bigint3, - Sign, - split, - collapse, - weakBound, - assertMul, - Sum, - assertRank1, -}; +// external API +export { ForeignField, Field3 }; + +// internal API +export { bigint3, Sign, split, collapse, weakBound, Sum, assertMul }; /** * A 3-tuple of Fields, representing a 3-limb bigint. @@ -47,10 +40,14 @@ const ForeignField = { return sum([x, y], [-1n], f); }, sum, + Sum(x: Field3) { + return new Sum(x); + }, mul: multiply, inv: inverse, div: divide, + assertMul, }; /** @@ -157,7 +154,7 @@ function inverse(x: Field3, f: bigint): Field3 { let xInv2Bound = weakBound(xInv[2], f); let one: Field2 = [Field.from(1n), Field.from(0n)]; - assertMul(x, xInv, one, f); + assertMulInternal(x, xInv, one, f); // range check on result bound // TODO: this uses two RCs too many.. need global RC stack @@ -190,7 +187,7 @@ function divide( }); multiRangeCheck(z); let z2Bound = weakBound(z[2], f); - assertMul(z, y, x, f); + assertMulInternal(z, y, x, f); // range check on result bound multiRangeCheck([z2Bound, Field.from(0n), Field.from(0n)]); @@ -212,7 +209,12 @@ function divide( /** * Common logic for gadgets that expect a certain multiplication result a priori, instead of just using the remainder. */ -function assertMul(x: Field3, y: Field3, xy: Field3 | Field2, f: bigint) { +function assertMulInternal( + x: Field3, + y: Field3, + xy: Field3 | Field2, + f: bigint +) { let { r01, r2, q } = multiplyNoRangeCheck(x, y, f); // range check on quotient @@ -413,10 +415,8 @@ function split2(x: bigint): [bigint, bigint] { * As usual, all values are assumed to be range checked, and the left and right multiplication inputs * are assumed to be bounded such that `l * r < 2^264 * (native modulus)`. * However, all extra checks that are needed on the _sums_ are handled here. - * - * TODO example */ -function assertRank1( +function assertMul( x: Field3 | Sum, y: Field3 | Sum, xy: Field3 | Sum, @@ -432,7 +432,7 @@ function assertRank1( // x is chained into the ffmul gate let x0 = x.finishForMulInput(f, true); - assertMul(x0, y0, xy0, f); + assertMulInternal(x0, y0, xy0, f); } class Sum { @@ -463,7 +463,7 @@ class Sum { return this; } - finishOne() { + #finishOne() { let result = this.#summands[0]; this.#result = result; return result; @@ -473,7 +473,7 @@ class Sum { assert(this.#result === undefined, 'sum already finished'); let signs = this.#ops; let n = signs.length; - if (n === 0) return this.finishOne(); + if (n === 0) return this.#finishOne(); let x = this.#summands.map(toVars); let result = x[0]; @@ -492,7 +492,7 @@ class Sum { assert(this.#result === undefined, 'sum already finished'); let signs = this.#ops; let n = signs.length; - if (n === 0) return this.finishOne(); + if (n === 0) return this.#finishOne(); let x = this.#summands.map(toVars); diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index dc4569369..e5c267653 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -8,7 +8,7 @@ import { } from './range-check.js'; import { not, rotate, xor, and, leftShift, rightShift } from './bitwise.js'; import { Field } from '../core.js'; -import { ForeignField, Field3 } from './foreign-field.js'; +import { ForeignField, Field3, Sum } from './foreign-field.js'; export { Gadgets }; @@ -470,6 +470,20 @@ const Gadgets = { div(x: Field3, y: Field3, f: bigint) { return ForeignField.div(x, y, f); }, + + /** + * TODO + */ + assertMul(x: Field3 | Sum, y: Field3 | Sum, z: Field3 | Sum, f: bigint) { + return ForeignField.assertMul(x, y, z, f); + }, + + /** + * TODO + */ + Sum(x: Field3) { + return ForeignField.Sum(x); + }, }, /** From 003f29d8a48b0e8f3141102dd31f7b7b7aae2d15 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 21 Nov 2023 13:00:27 +0100 Subject: [PATCH 038/102] adapt elliptic curve gadget --- src/lib/gadgets/elliptic-curve.ts | 41 +++++++++++++------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 60d9646d5..9b9b7417a 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -6,14 +6,7 @@ import { import { Field } from '../field.js'; import { Provable } from '../provable.js'; import { assert, exists } from './common.js'; -import { - Field3, - ForeignField, - Sum, - assertRank1, - split, - weakBound, -} from './foreign-field.js'; +import { Field3, ForeignField, split, weakBound } from './foreign-field.js'; import { l, multiRangeCheck } from './range-check.js'; import { sha256 } from 'js-sha256'; import { @@ -88,18 +81,18 @@ function add(p1: Point, p2: Point, f: bigint) { multiRangeCheck([mBound, x3Bound, y3Bound]); // (x1 - x2)*m = y1 - y2 - let deltaX = new Sum(x1).sub(x2); - let deltaY = new Sum(y1).sub(y2); - assertRank1(deltaX, m, deltaY, f); + let deltaX = ForeignField.Sum(x1).sub(x2); + let deltaY = ForeignField.Sum(y1).sub(y2); + ForeignField.assertMul(deltaX, m, deltaY, f); // m^2 = x1 + x2 + x3 - let xSum = new Sum(x1).add(x2).add(x3); - assertRank1(m, m, xSum, f); + let xSum = ForeignField.Sum(x1).add(x2).add(x3); + ForeignField.assertMul(m, m, xSum, f); // (x1 - x3)*m = y1 + y3 - let deltaX1X3 = new Sum(x1).sub(x3); - let ySum = new Sum(y1).add(y3); - assertRank1(deltaX1X3, m, ySum, f); + let deltaX1X3 = ForeignField.Sum(x1).sub(x3); + let ySum = ForeignField.Sum(y1).add(y3); + ForeignField.assertMul(deltaX1X3, m, ySum, f); return { x: x3, y: y3 }; } @@ -142,18 +135,18 @@ function double(p1: Point, f: bigint) { // 2*y1*m = 3*x1x1 // TODO this assumes the curve has a == 0 - let y1Times2 = new Sum(y1).add(y1); - let x1x1Times3 = new Sum(x1x1).add(x1x1).add(x1x1); - assertRank1(y1Times2, m, x1x1Times3, f); + let y1Times2 = ForeignField.Sum(y1).add(y1); + let x1x1Times3 = ForeignField.Sum(x1x1).add(x1x1).add(x1x1); + ForeignField.assertMul(y1Times2, m, x1x1Times3, f); // m^2 = 2*x1 + x3 - let xSum = new Sum(x1).add(x1).add(x3); - assertRank1(m, m, xSum, f); + let xSum = ForeignField.Sum(x1).add(x1).add(x3); + ForeignField.assertMul(m, m, xSum, f); // (x1 - x3)*m = y1 + y3 - let deltaX1X3 = new Sum(x1).sub(x3); - let ySum = new Sum(y1).add(y3); - assertRank1(deltaX1X3, m, ySum, f); + let deltaX1X3 = ForeignField.Sum(x1).sub(x3); + let ySum = ForeignField.Sum(y1).add(y3); + ForeignField.assertMul(deltaX1X3, m, ySum, f); // bounds checks multiRangeCheck([mBound, x3Bound, y3Bound]); From 347524a4aa3fce103b0b98adb98cb7955a243729 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 21 Nov 2023 14:30:28 +0100 Subject: [PATCH 039/102] add assertion to prevent invalid multiplication --- src/lib/gadgets/foreign-field.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/lib/gadgets/foreign-field.ts b/src/lib/gadgets/foreign-field.ts index f60663e98..dd89d04e4 100644 --- a/src/lib/gadgets/foreign-field.ts +++ b/src/lib/gadgets/foreign-field.ts @@ -426,6 +426,15 @@ function assertMul( y = Sum.fromUnfinished(y); xy = Sum.fromUnfinished(xy); + // conservative estimate to ensure that multiplication bound is satisfied + // we assume that all summands si are bounded with si[2] <= f[2] checks, which implies si < 2^k where k := ceil(log(f)) + // our assertion below gives us + // |x|*|y| + q*f + |r| < (x.length * y.length) 2^2k + 2^2k + 2^2k < 3 * 2^(2*258) < 2^264 * (native modulus) + assert( + BigInt(Math.ceil(Math.sqrt(x.length * y.length))) * f < 1n << 258n, + `Foreign modulus is too large for multiplication of sums of lengths ${x.length} and ${y.length}` + ); + // finish the y and xy sums with a zero gate let y0 = y.finishForMulInput(f); let xy0 = xy.finish(f); @@ -449,6 +458,10 @@ class Sum { return this.#result; } + get length() { + return this.#summands.length; + } + add(y: Field3) { assert(this.#result === undefined, 'sum already finished'); this.#ops.push(1n); From 24a0bf6b9b0e5db0500970184ed6d7b3b39f31a3 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 21 Nov 2023 14:33:16 +0100 Subject: [PATCH 040/102] document assertMul --- src/lib/gadgets/gadgets.ts | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index e5c267653..ec389e92e 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -472,14 +472,39 @@ const Gadgets = { }, /** - * TODO + * Optimized multiplication of sums in a foreign field, for example: `(x - y)*z = a + b + c mod f` + * + * Note: This is much more efficient than using {@link ForeignField.add} and {@link ForeignField.sub} separately to + * compute the multiplication inputs and outputs, and then using {@link ForeignField.mul} to constrain the result. + * + * The sums passed into this gadgets are "lazy sums" created with {@link ForeignField.Sum}. + * You can also pass in plain {@link Field3} elements. + * + * **Assumptions**: The assumptions on the _summands_ are analogous to the assumptions described in {@link ForeignField.mul}: + * - each summand's limbs are in the range [0, 2^88) + * - summands that are part of a multiplication input satisfy `x[2] <= f[2]` + * + * @throws if the modulus is so large that the second assumption no longer suffices for validity of the multiplication. + * For small sums and moduli < 2^256, this will not fail. + * + * @throws if the provided multiplication result is not correct modulo f. + * + * @example + * ```ts + * // we assume that x, y, z, a, b, c are range-checked, analogous to `ForeignField.mul()` + * let xMinusY = ForeignField.Sum(x).sub(y); + * let aPlusBPlusC = ForeignField.Sum(a).add(b).add(c); + * + * // assert that (x - y)*z = a + b + c mod f + * ForeignField.assertMul(xMinusY, z, aPlusBPlusC, f); + * ``` */ assertMul(x: Field3 | Sum, y: Field3 | Sum, z: Field3 | Sum, f: bigint) { return ForeignField.assertMul(x, y, z, f); }, /** - * TODO + * Lazy sum of {@link Field3} elements, which can be used as input to {@link ForeignField.assertMul}. */ Sum(x: Field3) { return ForeignField.Sum(x); @@ -499,4 +524,12 @@ export namespace Gadgets { * A 3-tuple of Fields, representing a 3-limb bigint. */ export type Field3 = [Field, Field, Field]; + + export namespace ForeignField { + /** + * Lazy sum of {@link Field3} elements, which can be used as input to {@link ForeignField.assertMul}. + */ + export type Sum = Sum_; + } } +type Sum_ = Sum; From 569e1c62945ff2729e4a1e38b964360fede142f0 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 22 Nov 2023 16:45:10 +0100 Subject: [PATCH 041/102] some API and comment tweaks --- src/lib/gadgets/ecdsa.unit-test.ts | 14 ++------ src/lib/gadgets/elliptic-curve.ts | 51 +++++++++++++++--------------- 2 files changed, 28 insertions(+), 37 deletions(-) diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index ff303b3cc..d1cd6350c 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -25,7 +25,7 @@ let msgHash = ); const ia = EllipticCurve.initialAggregator(Secp256k1, BaseField); -const tableConfig = { G: { windowSize: 4 }, P: { windowSize: 4 } }; +const config = { G: { windowSize: 4 }, P: { windowSize: 4 }, ia }; let program = ZkProgram({ name: 'ecdsa', @@ -37,10 +37,9 @@ let program = ZkProgram({ let P = Provable.witness(Point, () => publicKey); let R = EllipticCurve.multiScalarMul( Secp256k1, - ia, [signature.s, signature.r], [G, P], - [tableConfig.G, tableConfig.P] + [config.G, config.P] ); Provable.asProver(() => { console.log(Point.toBigint(R)); @@ -51,14 +50,7 @@ let program = ZkProgram({ privateInputs: [], method() { let signature0 = Provable.witness(Ecdsa.Signature, () => signature); - Ecdsa.verify( - Secp256k1, - ia, - signature0, - msgHash, - publicKey, - tableConfig - ); + Ecdsa.verify(Secp256k1, signature0, msgHash, publicKey, config); }, }, }, diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 9b9b7417a..736e724bb 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -1,5 +1,6 @@ import { FiniteField, + createField, inverse, mod, } from '../../bindings/crypto/finite_field.js'; @@ -129,6 +130,7 @@ function double(p1: Point, f: bigint) { let mBound = weakBound(m[2], f); let x3Bound = weakBound(x3[2], f); let y3Bound = weakBound(y3[2], f); + multiRangeCheck([mBound, x3Bound, y3Bound]); // x1^2 = x1x1 let x1x1 = ForeignField.mul(x1, x1, f); @@ -148,21 +150,18 @@ function double(p1: Point, f: bigint) { let ySum = ForeignField.Sum(y1).add(y3); ForeignField.assertMul(deltaX1X3, m, ySum, f); - // bounds checks - multiRangeCheck([mBound, x3Bound, y3Bound]); - return { x: x3, y: y3 }; } function verifyEcdsa( Curve: CurveAffine, - ia: point, signature: EcdsaSignature, msgHash: Field3, publicKey: Point, - tables?: { + config?: { G?: { windowSize: number; multiples?: Point[] }; P?: { windowSize: number; multiples?: Point[] }; + ia?: point; } ) { // constant case @@ -191,14 +190,16 @@ function verifyEcdsa( let G = Point.from(Curve.one); let R = multiScalarMul( Curve, - ia, [u1, u2], [G, publicKey], - tables && [tables.G, tables.P] + config && [config.G, config.P], + config?.ia ); // this ^ already proves that R != 0 // reduce R.x modulo the curve order + // note: we don't check that the result Rx is canonical, because Rx === r and r is an input: + // it's the callers responsibility to check that the signature is valid/unique in whatever way it makes sense for the application let Rx = ForeignField.mul(R.x, Field3.from(1n), Curve.order); Provable.assertEqual(Field3.provable, Rx, r); } @@ -220,18 +221,13 @@ function verifyEcdsa( */ function multiScalarMul( Curve: CurveAffine, - ia: point, scalars: Field3[], points: Point[], tableConfigs: ( - | { - // what we called c before - windowSize?: number; - // G, ..., (2^c-1)*G - multiples?: Point[]; - } + | { windowSize?: number; multiples?: Point[] } | undefined - )[] = [] + )[] = [], + ia?: point ): Point { let n = points.length; assert(scalars.length === n, 'Points and scalars lengths must match'); @@ -264,6 +260,8 @@ function multiScalarMul( slice(s, { maxBits: b, chunkSize: windowSizes[i] }) ); + // TODO: use Curve.Field + ia ??= initialAggregator(Curve, createField(Curve.modulus)); let sum = Point.from(ia); for (let i = b - 1; i >= 0; i--) { @@ -362,7 +360,7 @@ function getPointTable( function initialAggregator(Curve: CurveAffine, F: FiniteField) { // hash that identifies the curve let h = sha256.create(); - h.update('ecdsa'); + h.update('initial-aggregator'); h.update(bigIntToBytes(Curve.modulus)); h.update(bigIntToBytes(Curve.order)); h.update(bigIntToBytes(Curve.a)); @@ -386,12 +384,9 @@ function initialAggregator(Curve: CurveAffine, F: FiniteField) { } /** - * Provable method for slicing a 3x88-bit bigint into smaller bit chunks of length `windowSize` + * Provable method for slicing a 3x88-bit bigint into smaller bit chunks of length `chunkSize` * - * TODO: atm this uses expensive boolean checks for the bits. - * For larger chunks, we should use more efficient range checks. - * - * Note: This serves as a range check for the input limbs + * This serves as a range check that the input is in [0, 2^maxBits) */ function slice( [x0, x1, x2]: Field3, @@ -402,7 +397,7 @@ function slice( // first limb let result0 = sliceField(x0, Math.min(l_, maxBits), chunkSize); - if (maxBits <= l) return result0.chunks; + if (maxBits <= l_) return result0.chunks; maxBits -= l_; // second limb @@ -416,12 +411,16 @@ function slice( } /** - * Provable method for slicing a 3x88-bit bigint into smaller bit chunks of length `windowSize` + * Provable method for slicing a field element into smaller bit chunks of length `chunkSize`. * - * TODO: atm this uses expensive boolean checks for the bits. - * For larger chunks, we should use more efficient range checks. + * This serves as a range check that the input is in [0, 2^maxBits) * - * Note: This serves as a range check that the input is in [0, 2^maxBits) + * If `chunkSize` does not divide `maxBits`, the last chunk will be smaller. + * We return the number of free bits in the last chunk, and optionally accept such a result from a previous call, + * so that this function can be used to slice up a bigint of multiple limbs into homogeneous chunks. + * + * TODO: atm this uses expensive boolean checks for each bit. + * For larger chunks, we should use more efficient range checks. */ function sliceField( x: Field, From a4b11378ca4e026fb076d35c3fdc22510c2b28bb Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 22 Nov 2023 17:08:21 +0100 Subject: [PATCH 042/102] code moving --- src/lib/gadgets/foreign-field.unit-test.ts | 37 +++--------------- src/lib/gadgets/test-utils.ts | 44 ++++++++++++++++++++++ 2 files changed, 49 insertions(+), 32 deletions(-) create mode 100644 src/lib/gadgets/test-utils.ts diff --git a/src/lib/gadgets/foreign-field.unit-test.ts b/src/lib/gadgets/foreign-field.unit-test.ts index 8dc7f1eb5..371325cf6 100644 --- a/src/lib/gadgets/foreign-field.unit-test.ts +++ b/src/lib/gadgets/foreign-field.unit-test.ts @@ -1,7 +1,6 @@ import type { FiniteField } from '../../bindings/crypto/finite_field.js'; import { exampleFields } from '../../bindings/crypto/finite-field-examples.js'; import { - ProvableSpec, array, equivalentAsync, equivalentProvable, @@ -26,36 +25,14 @@ import { } from '../testing/constraint-system.js'; import { GateType } from '../../snarky.js'; import { AnyTuple } from '../util/types.js'; +import { + foreignField, + throwError, + unreducedForeignField, +} from './test-utils.js'; const { ForeignField, Field3 } = Gadgets; -function foreignField(F: FiniteField): ProvableSpec { - return { - rng: Random.otherField(F), - there: Field3.from, - back: Field3.toBigint, - provable: Field3.provable, - }; -} - -// for testing with inputs > f -function unreducedForeignField( - maxBits: number, - F: FiniteField -): ProvableSpec { - return { - rng: Random.bignat(1n << BigInt(maxBits)), - there: Field3.from, - back: Field3.toBigint, - provable: Field3.provable, - assertEqual(x, y, message) { - // need weak equality here because, while ffadd works on bigints larger than the modulus, - // it can't fully reduce them - assert(F.equal(x, y), message); - }, - }; -} - let sign = fromRandom(Random.oneOf(1n as const, -1n as const)); let fields = [ @@ -340,7 +317,3 @@ function sum(xs: bigint[], signs: (1n | -1n)[], F: FiniteField) { } return sum; } - -function throwError(message: string): T { - throw Error(message); -} diff --git a/src/lib/gadgets/test-utils.ts b/src/lib/gadgets/test-utils.ts new file mode 100644 index 000000000..7f963d49a --- /dev/null +++ b/src/lib/gadgets/test-utils.ts @@ -0,0 +1,44 @@ +import type { FiniteField } from '../../bindings/crypto/finite_field.js'; +import { ProvableSpec } from '../testing/equivalent.js'; +import { Random } from '../testing/random.js'; +import { Gadgets } from './gadgets.js'; +import { assert } from './common.js'; + +export { foreignField, unreducedForeignField, throwError }; + +const { Field3 } = Gadgets; + +// test input specs + +function foreignField(F: FiniteField): ProvableSpec { + return { + rng: Random.otherField(F), + there: Field3.from, + back: Field3.toBigint, + provable: Field3.provable, + }; +} + +// for testing with inputs > f +function unreducedForeignField( + maxBits: number, + F: FiniteField +): ProvableSpec { + return { + rng: Random.bignat(1n << BigInt(maxBits)), + there: Field3.from, + back: Field3.toBigint, + provable: Field3.provable, + assertEqual(x, y, message) { + // need weak equality here because, while ffadd works on bigints larger than the modulus, + // it can't fully reduce them + assert(F.equal(x, y), message); + }, + }; +} + +// helper + +function throwError(message: string): T { + throw Error(message); +} From ea17473c5d0d6e9ff463636f0a33e8f356376ad0 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 22 Nov 2023 17:08:25 +0100 Subject: [PATCH 043/102] bindings --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index 67468138a..3a40f7d2b 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 67468138a2b54b25276a7b475e3028be395d7a77 +Subproject commit 3a40f7d2bb360d33203cb03fda1177a87acfffed From 2898db3c0185cbb34247e7739d71c4495f00f970 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 22 Nov 2023 17:08:42 +0100 Subject: [PATCH 044/102] adapt to bindings --- src/lib/gadgets/ecdsa.unit-test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index d1cd6350c..532ffd21a 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -1,14 +1,18 @@ import { createCurveAffine } from '../../bindings/crypto/elliptic_curve.js'; import { Ecdsa, EllipticCurve, Point } from './elliptic-curve.js'; import { Field3 } from './foreign-field.js'; -import { secp256k1Params } from '../../bindings/crypto/elliptic-curve-examples.js'; +import { CurveParams } from '../../bindings/crypto/elliptic-curve-examples.js'; import { Provable } from '../provable.js'; import { createField } from '../../bindings/crypto/finite_field.js'; import { ZkProgram } from '../proof_system.js'; import { assert } from './common.js'; -const Secp256k1 = createCurveAffine(secp256k1Params); -const BaseField = createField(secp256k1Params.modulus); +// quick tests +// TODO + +// full end-to-end test with proving +const Secp256k1 = createCurveAffine(CurveParams.Secp256k1); +const BaseField = createField(Secp256k1.modulus); let publicKey = Point.from({ x: 49781623198970027997721070672560275063607048368575198229673025608762959476014n, From 9fd235337b1b7246561796ea3847caa31a34c2a4 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 22 Nov 2023 17:14:27 +0100 Subject: [PATCH 045/102] fields on curve --- src/lib/gadgets/elliptic-curve.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 736e724bb..85a2e0b45 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -260,8 +260,7 @@ function multiScalarMul( slice(s, { maxBits: b, chunkSize: windowSizes[i] }) ); - // TODO: use Curve.Field - ia ??= initialAggregator(Curve, createField(Curve.modulus)); + ia ??= initialAggregator(Curve); let sum = Point.from(ia); for (let i = b - 1; i >= 0; i--) { @@ -357,7 +356,7 @@ function getPointTable( * It's important that this point has no known discrete logarithm so that nobody * can create an invalid proof of EC scaling. */ -function initialAggregator(Curve: CurveAffine, F: FiniteField) { +function initialAggregator(Curve: CurveAffine) { // hash that identifies the curve let h = sha256.create(); h.update('initial-aggregator'); @@ -369,6 +368,7 @@ function initialAggregator(Curve: CurveAffine, F: FiniteField) { // bytes represent a 256-bit number // use that as x coordinate + const F = Curve.Field; let x = F.mod(bytesToBigInt(bytes)); let y: bigint | undefined = undefined; From 3e9ee6b87c45cc93e86d02ff433b738bb6b9eefb Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 22 Nov 2023 17:22:30 +0100 Subject: [PATCH 046/102] ecdsa sign --- src/lib/gadgets/elliptic-curve.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 85a2e0b45..332d15ab1 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -1,9 +1,4 @@ -import { - FiniteField, - createField, - inverse, - mod, -} from '../../bindings/crypto/finite_field.js'; +import { inverse, mod } from '../../bindings/crypto/finite_field.js'; import { Field } from '../field.js'; import { Provable } from '../provable.js'; import { assert, exists } from './common.js'; @@ -324,6 +319,20 @@ function verifyEcdsaConstant( return mod(X.x, q) === r; } +/** + * Sign a message hash using ECDSA. + */ +function signEcdsa(Curve: CurveAffine, msgHash: bigint, privateKey: bigint) { + let { Scalar } = Curve; + let k = Scalar.random(); + let R = Curve.scale(Curve.one, k); + let r = Scalar.mod(R.x); + let kInv = Scalar.inverse(k); + assert(kInv !== undefined); + let s = Scalar.mul(kInv, Scalar.add(msgHash, Scalar.mul(r, privateKey))); + return { r, s }; +} + function getPointTable( Curve: CurveAffine, P: Point, @@ -536,6 +545,7 @@ const EcdsaSignature = { }; const Ecdsa = { + sign: signEcdsa, verify: verifyEcdsa, Signature: EcdsaSignature, }; From c501badd91468100089e53f35fb6e030e06778cd Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 22 Nov 2023 17:22:40 +0100 Subject: [PATCH 047/102] adapt --- src/lib/gadgets/ecdsa.unit-test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index 532ffd21a..ad71411cd 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -3,7 +3,6 @@ import { Ecdsa, EllipticCurve, Point } from './elliptic-curve.js'; import { Field3 } from './foreign-field.js'; import { CurveParams } from '../../bindings/crypto/elliptic-curve-examples.js'; import { Provable } from '../provable.js'; -import { createField } from '../../bindings/crypto/finite_field.js'; import { ZkProgram } from '../proof_system.js'; import { assert } from './common.js'; @@ -12,7 +11,6 @@ import { assert } from './common.js'; // full end-to-end test with proving const Secp256k1 = createCurveAffine(CurveParams.Secp256k1); -const BaseField = createField(Secp256k1.modulus); let publicKey = Point.from({ x: 49781623198970027997721070672560275063607048368575198229673025608762959476014n, @@ -28,7 +26,7 @@ let msgHash = 0x3e91cd8bd233b3df4e4762b329e2922381da770df1b31276ec77d0557be7fcefn ); -const ia = EllipticCurve.initialAggregator(Secp256k1, BaseField); +const ia = EllipticCurve.initialAggregator(Secp256k1); const config = { G: { windowSize: 4 }, P: { windowSize: 4 }, ia }; let program = ZkProgram({ From 321f963689b1bcae7fddda8776ac22b9ce17b03b Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 22 Nov 2023 17:22:55 +0100 Subject: [PATCH 048/102] bindings --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index 3a40f7d2b..916bc2145 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 3a40f7d2bb360d33203cb03fda1177a87acfffed +Subproject commit 916bc21458bdb00a9277de755af15a1a5e111ba2 From c705ed577b96462b7def8bd558a79569894a47ca Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 22 Nov 2023 20:38:11 +0100 Subject: [PATCH 049/102] fix test compilation --- src/lib/gadgets/elliptic-curve.unit-test.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.unit-test.ts b/src/lib/gadgets/elliptic-curve.unit-test.ts index 5569f4dbc..f820fc0a9 100644 --- a/src/lib/gadgets/elliptic-curve.unit-test.ts +++ b/src/lib/gadgets/elliptic-curve.unit-test.ts @@ -5,13 +5,10 @@ import { printGates } from '../testing/constraint-system.js'; import { assert } from './common.js'; import { createCurveAffine } from '../../bindings/crypto/elliptic_curve.js'; import { Fp } from '../../bindings/crypto/finite_field.js'; -import { - pallasParams, - secp256k1Params, -} from '../../bindings/crypto/elliptic-curve-examples.js'; +import { CurveParams } from '../../bindings/crypto/elliptic-curve-examples.js'; -const Secp256k1 = createCurveAffine(secp256k1Params); -const Pallas = createCurveAffine(pallasParams); +const Secp256k1 = createCurveAffine(CurveParams.Secp256k1); +const Pallas = createCurveAffine(CurveParams.Pallas); let { add, double, initialAggregator } = EllipticCurve; @@ -42,6 +39,6 @@ console.log({ digest: csAdd.digest, rows: csAdd.rows }); printGates(csDouble.gates); console.log({ digest: csDouble.digest, rows: csDouble.rows }); -let point = initialAggregator(Pallas, Fp); +let point = initialAggregator(Pallas); console.log({ point }); assert(Pallas.isOnCurve(point)); From 9d480986e8485d59fd15a57bd61d2c18a02c571b Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 22 Nov 2023 21:38:57 +0100 Subject: [PATCH 050/102] map rng pf spec --- src/lib/testing/equivalent.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib/testing/equivalent.ts b/src/lib/testing/equivalent.ts index ab1241b94..1397d6d06 100644 --- a/src/lib/testing/equivalent.ts +++ b/src/lib/testing/equivalent.ts @@ -25,6 +25,7 @@ export { unit, array, record, + map, fromRandom, }; export { Spec, ToSpec, FromSpec, SpecFromFunctions, ProvableSpec }; @@ -288,6 +289,13 @@ function record }>( }; } +function map( + { from, to }: { from: FromSpec; to: Spec }, + there: (t: T1) => S1 +): Spec { + return { ...to, rng: Random.map(from.rng, there) }; +} + function mapObject( t: { [k in K]: T }, map: (t: T, k: K) => S From 5780bfec0fc7625f515da7bf9a240f8778c1a4e1 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 22 Nov 2023 21:39:36 +0100 Subject: [PATCH 051/102] wip quick ecdsa tests --- src/lib/gadgets/ecdsa.unit-test.ts | 76 ++++++++++++++++++++- src/lib/gadgets/elliptic-curve.ts | 4 ++ src/lib/gadgets/elliptic-curve.unit-test.ts | 1 - 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index ad71411cd..e7fe3540f 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -1,16 +1,86 @@ import { createCurveAffine } from '../../bindings/crypto/elliptic_curve.js'; -import { Ecdsa, EllipticCurve, Point } from './elliptic-curve.js'; +import { + Ecdsa, + EllipticCurve, + Point, + verifyEcdsaConstant, +} from './elliptic-curve.js'; import { Field3 } from './foreign-field.js'; import { CurveParams } from '../../bindings/crypto/elliptic-curve-examples.js'; import { Provable } from '../provable.js'; import { ZkProgram } from '../proof_system.js'; import { assert } from './common.js'; +import { foreignField, throwError } from './test-utils.js'; +import { + equivalentProvable, + map, + oneOf, + record, + unit, +} from '../testing/equivalent.js'; // quick tests -// TODO +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); + + let pseudoPoint = record({ x: field, y: field }); + let point = map({ from: scalar, to: pseudoPoint }, (x) => + Curve.scale(Curve.one, x) + ); + + let pseudoSignature = record({ + signature: record({ r: scalar, s: scalar }), + msg: scalar, + publicKey: point, + }); + let signatureInputs = record({ privateKey: scalar, msg: scalar }); + let signature = map( + { from: signatureInputs, to: pseudoSignature }, + ({ privateKey, msg }) => { + let signature = Ecdsa.sign(Curve, msg, privateKey); + let publicKey = Curve.scale(Curve.one, privateKey); + return { signature, msg, publicKey }; + } + ); + + // positive test + equivalentProvable({ from: [signature], to: unit })( + () => {}, + ({ signature, publicKey, msg }) => { + Ecdsa.verify(Curve, signature, msg, publicKey); + }, + 'valid signature verifies' + ); + + // negative test + equivalentProvable({ from: [pseudoSignature], to: unit })( + () => throwError('invalid signature'), + ({ signature, publicKey, msg }) => { + Ecdsa.verify(Curve, signature, msg, publicKey); + }, + 'invalid signature fails' + ); + + // test against constant implementation, with both invalid and valid signatures + equivalentProvable({ from: [oneOf(signature, pseudoSignature)], to: unit })( + ({ signature, publicKey, msg }) => { + assert(verifyEcdsaConstant(Curve, signature, msg, publicKey), 'verifies'); + }, + ({ signature, publicKey, msg }) => { + Ecdsa.verify(Curve, signature, msg, publicKey); + }, + 'verify' + ); +} // full end-to-end test with proving -const Secp256k1 = createCurveAffine(CurveParams.Secp256k1); let publicKey = Point.from({ x: 49781623198970027997721070672560275063607048368575198229673025608762959476014n, diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 332d15ab1..93a8527ab 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -20,8 +20,12 @@ import { provable } from '../circuit_value.js'; import { assertPositiveInteger } from '../../bindings/crypto/non-negative.js'; import { arrayGet } from './basic.js'; +// external API export { EllipticCurve, Point, Ecdsa, EcdsaSignature }; +// internal API +export { verifyEcdsaConstant }; + const EllipticCurve = { add, double, diff --git a/src/lib/gadgets/elliptic-curve.unit-test.ts b/src/lib/gadgets/elliptic-curve.unit-test.ts index f820fc0a9..722ca3137 100644 --- a/src/lib/gadgets/elliptic-curve.unit-test.ts +++ b/src/lib/gadgets/elliptic-curve.unit-test.ts @@ -4,7 +4,6 @@ import { EllipticCurve } from './elliptic-curve.js'; import { printGates } from '../testing/constraint-system.js'; import { assert } from './common.js'; import { createCurveAffine } from '../../bindings/crypto/elliptic_curve.js'; -import { Fp } from '../../bindings/crypto/finite_field.js'; import { CurveParams } from '../../bindings/crypto/elliptic-curve-examples.js'; const Secp256k1 = createCurveAffine(CurveParams.Secp256k1); From 4fee5294089b2cc738ac52bf12e150f3e36be65d Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 23 Nov 2023 10:04:07 +0100 Subject: [PATCH 052/102] finish unit test --- src/lib/gadgets/ecdsa.unit-test.ts | 34 ++++++++++++++---------------- src/lib/gadgets/test-utils.ts | 14 +++++++++++- src/lib/testing/equivalent.ts | 30 ++++++++++++++++---------- 3 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index e7fe3540f..ea380c171 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -10,8 +10,9 @@ import { CurveParams } from '../../bindings/crypto/elliptic-curve-examples.js'; import { Provable } from '../provable.js'; import { ZkProgram } from '../proof_system.js'; import { assert } from './common.js'; -import { foreignField, throwError } from './test-utils.js'; +import { foreignField, throwError, uniformForeignField } from './test-utils.js'; import { + Second, equivalentProvable, map, oneOf, @@ -29,42 +30,41 @@ for (let Curve of curves) { // prepare test inputs let field = foreignField(Curve.Field); let scalar = foreignField(Curve.Scalar); - - let pseudoPoint = record({ x: field, y: field }); - let point = map({ from: scalar, to: pseudoPoint }, (x) => - Curve.scale(Curve.one, x) - ); + let privateKey = uniformForeignField(Curve.Scalar); let pseudoSignature = record({ signature: record({ r: scalar, s: scalar }), msg: scalar, - publicKey: point, + publicKey: record({ x: field, y: field }), }); - let signatureInputs = record({ privateKey: scalar, msg: scalar }); + + let signatureInputs = record({ privateKey, msg: scalar }); + let signature = map( { from: signatureInputs, to: pseudoSignature }, ({ privateKey, msg }) => { - let signature = Ecdsa.sign(Curve, msg, privateKey); let publicKey = Curve.scale(Curve.one, privateKey); + let signature = Ecdsa.sign(Curve, msg, privateKey); return { signature, msg, publicKey }; } ); + // provable method we want to test + const verify = (s: Second) => { + Ecdsa.verify(Curve, s.signature, s.msg, s.publicKey); + }; + // positive test equivalentProvable({ from: [signature], to: unit })( () => {}, - ({ signature, publicKey, msg }) => { - Ecdsa.verify(Curve, signature, msg, publicKey); - }, + verify, 'valid signature verifies' ); // negative test equivalentProvable({ from: [pseudoSignature], to: unit })( () => throwError('invalid signature'), - ({ signature, publicKey, msg }) => { - Ecdsa.verify(Curve, signature, msg, publicKey); - }, + verify, 'invalid signature fails' ); @@ -73,9 +73,7 @@ for (let Curve of curves) { ({ signature, publicKey, msg }) => { assert(verifyEcdsaConstant(Curve, signature, msg, publicKey), 'verifies'); }, - ({ signature, publicKey, msg }) => { - Ecdsa.verify(Curve, signature, msg, publicKey); - }, + verify, 'verify' ); } diff --git a/src/lib/gadgets/test-utils.ts b/src/lib/gadgets/test-utils.ts index 7f963d49a..309dac616 100644 --- a/src/lib/gadgets/test-utils.ts +++ b/src/lib/gadgets/test-utils.ts @@ -4,7 +4,7 @@ import { Random } from '../testing/random.js'; import { Gadgets } from './gadgets.js'; import { assert } from './common.js'; -export { foreignField, unreducedForeignField, throwError }; +export { foreignField, unreducedForeignField, uniformForeignField, throwError }; const { Field3 } = Gadgets; @@ -37,6 +37,18 @@ function unreducedForeignField( }; } +// for fields that must follow an unbiased distribution, like private keys +function uniformForeignField( + F: FiniteField +): ProvableSpec { + return { + rng: Random(F.random), + there: Field3.from, + back: Field3.toBigint, + provable: Field3.provable, + }; +} + // helper function throwError(message: string): T { diff --git a/src/lib/testing/equivalent.ts b/src/lib/testing/equivalent.ts index 1397d6d06..c02cf23af 100644 --- a/src/lib/testing/equivalent.ts +++ b/src/lib/testing/equivalent.ts @@ -28,7 +28,15 @@ export { map, fromRandom, }; -export { Spec, ToSpec, FromSpec, SpecFromFunctions, ProvableSpec }; +export { + Spec, + ToSpec, + FromSpec, + SpecFromFunctions, + ProvableSpec, + First, + Second, +}; // a `Spec` tells us how to compare two functions @@ -109,8 +117,8 @@ function equivalent< Out extends ToSpec >({ from, to }: { from: In; to: Out }) { return function run( - f1: (...args: Params1) => Result1, - f2: (...args: Params2) => Result2, + f1: (...args: Params1) => First, + f2: (...args: Params2) => Second, label = 'expect equal results' ) { let generators = from.map((spec) => spec.rng); @@ -138,8 +146,8 @@ function equivalentAsync< Out extends ToSpec >({ from, to }: { from: In; to: Out }, { runs = 1 } = {}) { return async function run( - f1: (...args: Params1) => Promise> | Result1, - f2: (...args: Params2) => Promise> | Result2, + f1: (...args: Params1) => Promise> | First, + f2: (...args: Params2) => Promise> | Second, label = 'expect equal results' ) { let generators = from.map((spec) => spec.rng); @@ -178,8 +186,8 @@ function equivalentProvable< >({ from: fromRaw, to }: { from: In; to: Out }) { let fromUnions = fromRaw.map(toUnion); return function run( - f1: (...args: Params1) => Result1, - f2: (...args: Params2) => Result2, + f1: (...args: Params1) => First, + f2: (...args: Params2) => Second, label = 'expect equal results' ) { let generators = fromUnions.map((spec) => spec.rng); @@ -279,8 +287,8 @@ function array( function record }>( specs: Specs ): Spec< - { [k in keyof Specs]: Result1 }, - { [k in keyof Specs]: Result2 } + { [k in keyof Specs]: First }, + { [k in keyof Specs]: Second } > { return { rng: Random.record(mapObject(specs, (spec) => spec.rng)) as any, @@ -405,9 +413,9 @@ type Params2>> = { [k in keyof Ins]: Param2; }; -type Result1> = Out extends ToSpec +type First> = Out extends ToSpec ? Out1 : never; -type Result2> = Out extends ToSpec +type Second> = Out extends ToSpec ? Out2 : never; From b35bf20e778e3d38b5cefc9513eada760866b2cc Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 23 Nov 2023 10:04:16 +0100 Subject: [PATCH 053/102] clean up constant ecdsa --- src/lib/gadgets/elliptic-curve.ts | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 93a8527ab..b1436a178 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -301,26 +301,25 @@ function multiScalarMul( */ function verifyEcdsaConstant( Curve: CurveAffine, - { r, s }: { r: bigint; s: bigint }, + { r, s }: ecdsaSignature, msgHash: bigint, - publicKey: { x: bigint; y: bigint } + publicKey: point ) { - let q = Curve.order; - let QA = Curve.fromNonzero(publicKey); - if (!Curve.isOnCurve(QA)) return false; - if (Curve.hasCofactor && !Curve.isInSubgroup(QA)) return false; + let pk = Curve.fromNonzero(publicKey); + if (!Curve.isOnCurve(pk)) return false; + if (Curve.hasCofactor && !Curve.isInSubgroup(pk)) return false; if (r < 1n || r >= Curve.order) return false; if (s < 1n || s >= Curve.order) return false; - let sInv = inverse(s, q); - if (sInv === undefined) throw Error('impossible'); - let u1 = mod(msgHash * sInv, q); - let u2 = mod(r * sInv, q); + let sInv = Curve.Scalar.inverse(s); + assert(sInv !== undefined); + let u1 = Curve.Scalar.mul(msgHash, sInv); + let u2 = Curve.Scalar.mul(r, sInv); - let X = Curve.add(Curve.scale(Curve.one, u1), Curve.scale(QA, u2)); - if (Curve.equal(X, Curve.zero)) return false; + let R = Curve.add(Curve.scale(Curve.one, u1), Curve.scale(pk, u2)); + if (Curve.equal(R, Curve.zero)) return false; - return mod(X.x, q) === r; + return Curve.Scalar.equal(R.x, r); } /** From 3369ae30c491a83fd0957c243f0087a638a3cddc Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 23 Nov 2023 10:12:41 +0100 Subject: [PATCH 054/102] comment --- src/lib/gadgets/elliptic-curve.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index b1436a178..943f8c517 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -336,6 +336,10 @@ function signEcdsa(Curve: CurveAffine, msgHash: bigint, privateKey: bigint) { return { r, s }; } +/** + * Given a point P, create the list of multiples [0, P, 2P, 3P, ..., (2^windowSize-1) * P]. + * This method is provable, but won't create any constraints given a constant point. + */ function getPointTable( Curve: CurveAffine, P: Point, From cf27df57dd246668f6d87e49867613d834191172 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 23 Nov 2023 10:23:33 +0100 Subject: [PATCH 055/102] api cleanup --- src/lib/gadgets/ecdsa.unit-test.ts | 7 +++++-- src/lib/gadgets/elliptic-curve.ts | 31 +++++++++++++++++------------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index ea380c171..47a4f0228 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -104,7 +104,7 @@ let program = ZkProgram({ privateInputs: [], method() { let G = Point.from(Secp256k1.one); - let P = Provable.witness(Point, () => publicKey); + let P = Provable.witness(Point.provable, () => publicKey); let R = EllipticCurve.multiScalarMul( Secp256k1, [signature.s, signature.r], @@ -119,7 +119,10 @@ let program = ZkProgram({ ecdsa: { privateInputs: [], method() { - let signature0 = Provable.witness(Ecdsa.Signature, () => signature); + let signature0 = Provable.witness( + Ecdsa.Signature.provable, + () => signature + ); Ecdsa.verify(Secp256k1, signature0, msgHash, publicKey, config); }, }, diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 943f8c517..8d960b6dd 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -50,7 +50,7 @@ function add(p1: Point, p2: Point, f: bigint) { let { x: x2, y: y2 } = p2; // constant case - if (Provable.isConstant(Point, p1) && Provable.isConstant(Point, p2)) { + if (Point.isConstant(p1) && Point.isConstant(p2)) { let p3 = affineAdd(Point.toBigint(p1), Point.toBigint(p2), f); return Point.from(p3); } @@ -101,7 +101,7 @@ function double(p1: Point, f: bigint) { let { x: x1, y: y1 } = p1; // constant case - if (Provable.isConstant(Point, p1)) { + if (Point.isConstant(p1)) { let p3 = affineDouble(Point.toBigint(p1), f); return Point.from(p3); } @@ -165,9 +165,9 @@ function verifyEcdsa( ) { // constant case if ( - Provable.isConstant(EcdsaSignature, signature) && + EcdsaSignature.isConstant(signature) && Field3.isConstant(msgHash) && - Provable.isConstant(Point, publicKey) + Point.isConstant(publicKey) ) { let isValid = verifyEcdsaConstant( Curve, @@ -233,10 +233,7 @@ function multiScalarMul( assertPositiveInteger(n, 'Expected at least 1 point and scalar'); // constant case - if ( - scalars.every(Field3.isConstant) && - points.every((P) => Provable.isConstant(Point, P)) - ) { + if (scalars.every(Field3.isConstant) && points.every(Point.isConstant)) { // TODO dedicated MSM let s = scalars.map(Field3.toBigint); let P = points.map(Point.toBigint); @@ -270,13 +267,15 @@ function multiScalarMul( // pick point to add based on the scalar chunk let sj = scalarChunks[j][i / windowSize]; let sjP = - windowSize === 1 ? points[j] : arrayGetGeneric(Point, tables[j], sj); + windowSize === 1 + ? points[j] + : arrayGetGeneric(Point.provable, tables[j], sj); // ec addition let added = add(sum, sjP, Curve.modulus); // handle degenerate case (if sj = 0, Gj is all zeros and the add result is garbage) - sum = Provable.if(sj.equals(0), Point, sum, added); + sum = Provable.if(sj.equals(0), Point.provable, sum, added); } } @@ -290,7 +289,7 @@ function multiScalarMul( // 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)); - Provable.equal(Point, sum, Point.from(iaFinal)).assertFalse(); + Provable.equal(Point.provable, sum, Point.from(iaFinal)).assertFalse(); sum = add(sum, Point.from(Curve.negate(iaFinal)), Curve.modulus); return sum; @@ -516,23 +515,27 @@ function arrayGetGeneric(type: Provable, array: T[], index: Field) { } const Point = { - ...provable({ x: Field3.provable, y: Field3.provable }), from({ x, y }: point): Point { return { x: Field3.from(x), y: Field3.from(y) }; }, toBigint({ x, y }: Point) { return { x: Field3.toBigint(x), y: Field3.toBigint(y), infinity: false }; }, + isConstant: (P: Point) => Provable.isConstant(Point.provable, P), + + provable: provable({ x: Field3.provable, y: Field3.provable }), }; const EcdsaSignature = { - ...provable({ r: Field3.provable, s: Field3.provable }), from({ r, s }: ecdsaSignature): EcdsaSignature { return { r: Field3.from(r), s: Field3.from(s) }; }, toBigint({ r, s }: EcdsaSignature): ecdsaSignature { return { r: Field3.toBigint(r), s: Field3.toBigint(s) }; }, + isConstant: (S: EcdsaSignature) => + Provable.isConstant(EcdsaSignature.provable, S), + /** * 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). @@ -549,6 +552,8 @@ const EcdsaSignature = { let s = BigInt(`0x${signature.slice(64)}`); return EcdsaSignature.from({ r, s }); }, + + provable: provable({ r: Field3.provable, s: Field3.provable }), }; const Ecdsa = { From bf6d2e96194c0fc10f6da5ca3f404551cafb6555 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 23 Nov 2023 11:08:22 +0100 Subject: [PATCH 056/102] comments and make sure r,s are valid --- src/lib/gadgets/elliptic-curve.ts | 39 +++++++++++++++++++------------ 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 8d960b6dd..016a625e8 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -21,7 +21,7 @@ import { assertPositiveInteger } from '../../bindings/crypto/non-negative.js'; import { arrayGet } from './basic.js'; // external API -export { EllipticCurve, Point, Ecdsa, EcdsaSignature }; +export { EllipticCurve, Point, Ecdsa }; // internal API export { verifyEcdsaConstant }; @@ -39,11 +39,13 @@ const EllipticCurve = { type Point = { x: Field3; y: Field3 }; type point = { x: bigint; y: bigint }; -/** - * ECDSA signature consisting of two curve scalars. - */ -type EcdsaSignature = { r: Field3; s: Field3 }; -type ecdsaSignature = { r: bigint; s: bigint }; +namespace Ecdsa { + /** + * ECDSA signature consisting of two curve scalars. + */ + export type Signature = { r: Field3; s: Field3 }; + export type signature = { r: bigint; s: bigint }; +} function add(p1: Point, p2: Point, f: bigint) { let { x: x1, y: y1 } = p1; @@ -154,7 +156,7 @@ function double(p1: Point, f: bigint) { function verifyEcdsa( Curve: CurveAffine, - signature: EcdsaSignature, + signature: Ecdsa.Signature, msgHash: Field3, publicKey: Point, config?: { @@ -180,9 +182,11 @@ function verifyEcdsa( } // provable case - // TODO should we check that the publicKey is a valid point? probably not + // note: usually we don't check validity of inputs, like that the public key is a valid curve point + // we make an exception for the two non-standard conditions s != 0 and r != 0, + // which are unusual to capture in types and could be considered part of the verification algorithm let { r, s } = signature; - let sInv = ForeignField.inv(s, Curve.order); + let sInv = ForeignField.inv(s, Curve.order); // proves s != 0 let u1 = ForeignField.mul(msgHash, sInv, Curve.order); let u2 = ForeignField.mul(r, sInv, Curve.order); @@ -194,7 +198,10 @@ function verifyEcdsa( config && [config.G, config.P], config?.ia ); - // this ^ already proves that R != 0 + // this ^ already proves that R != 0 (part of ECDSA verification) + // if b is not a square, then R != 0 already proves that r === R.x != 0, because R.y^2 = b has no solutions + // Otherwise we check the condition r != 0 explicitly (important, because r = 0 => u2 = 0 kills the contribution of the private key) + if (Curve.Field.isSquare(Curve.b)) ForeignField.inv(r, Curve.modulus); // reduce R.x modulo the curve order // note: we don't check that the result Rx is canonical, because Rx === r and r is an input: @@ -300,7 +307,7 @@ function multiScalarMul( */ function verifyEcdsaConstant( Curve: CurveAffine, - { r, s }: ecdsaSignature, + { r, s }: Ecdsa.signature, msgHash: bigint, publicKey: point ) { @@ -514,6 +521,8 @@ function arrayGetGeneric(type: Provable, array: T[], index: Field) { return a; } +// type/conversion helpers + const Point = { from({ x, y }: point): Point { return { x: Field3.from(x), y: Field3.from(y) }; @@ -527,20 +536,20 @@ const Point = { }; const EcdsaSignature = { - from({ r, s }: ecdsaSignature): EcdsaSignature { + from({ r, s }: Ecdsa.signature): Ecdsa.Signature { return { r: Field3.from(r), s: Field3.from(s) }; }, - toBigint({ r, s }: EcdsaSignature): ecdsaSignature { + toBigint({ r, s }: Ecdsa.Signature): Ecdsa.signature { return { r: Field3.toBigint(r), s: Field3.toBigint(s) }; }, - isConstant: (S: EcdsaSignature) => + isConstant: (S: Ecdsa.Signature) => Provable.isConstant(EcdsaSignature.provable, S), /** * 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). */ - fromHex(rawSignature: string): EcdsaSignature { + fromHex(rawSignature: string): Ecdsa.Signature { let prefix = rawSignature.slice(0, 2); let signature = rawSignature.slice(2, 130); if (prefix !== '0x' || signature.length < 128) { From 4bb7b1a3a50ce6de540bde0ad329c7fa26ab394b Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 23 Nov 2023 11:12:34 +0100 Subject: [PATCH 057/102] fix --- src/lib/gadgets/elliptic-curve.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 016a625e8..12c5eb722 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -183,9 +183,10 @@ function verifyEcdsa( // provable case // note: usually we don't check validity of inputs, like that the public key is a valid curve point - // we make an exception for the two non-standard conditions s != 0 and r != 0, + // we make an exception for the two non-standard conditions r != 0 and s != 0, // which are unusual to capture in types and could be considered part of the verification algorithm let { r, s } = signature; + ForeignField.inv(r, Curve.order); // proves r != 0 (important, because r = 0 => u2 = 0 kills the private key contribution) let sInv = ForeignField.inv(s, Curve.order); // proves s != 0 let u1 = ForeignField.mul(msgHash, sInv, Curve.order); let u2 = ForeignField.mul(r, sInv, Curve.order); @@ -199,9 +200,6 @@ function verifyEcdsa( config?.ia ); // this ^ already proves that R != 0 (part of ECDSA verification) - // if b is not a square, then R != 0 already proves that r === R.x != 0, because R.y^2 = b has no solutions - // Otherwise we check the condition r != 0 explicitly (important, because r = 0 => u2 = 0 kills the contribution of the private key) - if (Curve.Field.isSquare(Curve.b)) ForeignField.inv(r, Curve.modulus); // reduce R.x modulo the curve order // note: we don't check that the result Rx is canonical, because Rx === r and r is an input: From 415f8ae84e450e43bb9083d75842a69cb022f22a Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 23 Nov 2023 11:33:36 +0100 Subject: [PATCH 058/102] sketch public API --- src/lib/gadgets/elliptic-curve.ts | 4 +-- src/lib/gadgets/gadgets.ts | 45 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 12c5eb722..413c7fe7f 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -159,11 +159,11 @@ function verifyEcdsa( signature: Ecdsa.Signature, msgHash: Field3, publicKey: Point, - config?: { + config: { G?: { windowSize: number; multiples?: Point[] }; P?: { windowSize: number; multiples?: Point[] }; ia?: point; - } + } = { G: { windowSize: 4 }, P: { windowSize: 4 } } ) { // constant case if ( diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index a35e94e56..d6670c3e9 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -9,6 +9,8 @@ import { import { not, rotate, xor, and, leftShift, rightShift } from './bitwise.js'; import { Field } from '../core.js'; import { ForeignField, Field3, Sum } from './foreign-field.js'; +import { Ecdsa, Point } from './elliptic-curve.js'; +import { CurveAffine } from '../../bindings/crypto/elliptic_curve.js'; export { Gadgets }; @@ -515,6 +517,49 @@ const Gadgets = { }, }, + /** + * TODO + */ + Ecdsa: { + /** + * TODO + * + * @example + * ```ts + * let Curve = Curves.Secp256k1; // TODO provide this somehow + * // TODO easy way to check that foreign field elements are valid + * let signature = { r, s }; + * // TODO need a way to check that publicKey is on curve + * let publicKey = { x, y }; + * + * Gadgets.Ecdsa.verify(Curve, signature, msgHash, publicKey); + * ``` + */ + verify( + Curve: CurveAffine, + signature: Ecdsa.Signature, + msgHash: Field3, + publicKey: Point + ) { + Ecdsa.verify(Curve, signature, msgHash, publicKey); + }, + + /** + * TODO + * + * should this be here, given that it's not a provable method? + * maybe assert that we are not running in provable context + */ + sign(Curve: CurveAffine, msgHash: bigint, privateKey: bigint) { + return Ecdsa.sign(Curve, msgHash, privateKey); + }, + + /** + * Non-provable helper methods for interacting with ECDSA signatures. + */ + Signature: Ecdsa.Signature, + }, + /** * Helper methods to interact with 3-limb vectors of Fields. * From a9e75e8ae0fef780169c55e531d87518e3cff0ea Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 23 Nov 2023 11:33:40 +0100 Subject: [PATCH 059/102] bindings --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index 916bc2145..12fdc5af7 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 916bc21458bdb00a9277de755af15a1a5e111ba2 +Subproject commit 12fdc5af742978ea32c3ce87696d43ee64738006 From b451ebe95cf0bef51d64d55b32b3ce9d14f4d822 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 24 Nov 2023 09:28:24 +0100 Subject: [PATCH 060/102] add helper to apply all ff range checks --- src/lib/gadgets/foreign-field.ts | 29 +++++++++++++++++++++++-- src/lib/gadgets/gadgets.ts | 36 +++++++++++++++++++++++++++++++- src/lib/util/types.ts | 4 ++++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/lib/gadgets/foreign-field.ts b/src/lib/gadgets/foreign-field.ts index 8f718cf25..d45a80033 100644 --- a/src/lib/gadgets/foreign-field.ts +++ b/src/lib/gadgets/foreign-field.ts @@ -9,8 +9,7 @@ import { provableTuple } from '../../bindings/lib/provable-snarky.js'; import { Unconstrained } from '../circuit_value.js'; import { Field } from '../field.js'; import { Gates, foreignFieldAdd } from '../gates.js'; -import { Provable } from '../provable.js'; -import { Tuple } from '../util/types.js'; +import { Tuple, TupleN } from '../util/types.js'; import { assertOneOf } from './basic.js'; import { assert, bitSlice, exists, toVar, toVars } from './common.js'; import { @@ -52,6 +51,8 @@ const ForeignField = { inv: inverse, div: divide, assertMul, + + assertAlmostFieldElements, }; /** @@ -345,6 +346,30 @@ function weakBound(x: Field, f: bigint) { return x.add(lMask - (f >> l2)); } +/** + * Apply range checks and weak bounds checks to a list of Field3s. + * Optimal if the list length is a multiple of 3. + */ +function assertAlmostFieldElements(xs: Field3[], f: bigint) { + let bounds: Field[] = []; + + for (let x of xs) { + multiRangeCheck(x); + + bounds.push(weakBound(x[2], f)); + if (TupleN.hasLength(3, bounds)) { + multiRangeCheck(bounds); + bounds = []; + } + } + if (TupleN.hasLength(1, bounds)) { + multiRangeCheck([...bounds, Field.from(0n), Field.from(0n)]); + } + if (TupleN.hasLength(2, bounds)) { + multiRangeCheck([...bounds, Field.from(0n)]); + } +} + const Field3 = { /** * Turn a bigint into a 3-tuple of Fields diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index d6670c3e9..3cb2c9ce8 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -515,6 +515,36 @@ const Gadgets = { Sum(x: Field3) { return ForeignField.Sum(x); }, + + /** + * Prove that each of the given {@link Field3} elements is "almost" reduced modulo f, + * i.e., satisfies the assumptions required by {@link ForeignField.mul} and other gadgets: + * - each limb is in the range [0, 2^88) + * - the most significant limb is less or equal than the modulus, x[2] <= f[2] + * + * **Note**: This method is most efficient when the number of input elements is a multiple of 3. + * + * @throws if any of the assumptions is violated. + * + * @example + * ```ts + * let x = Provable.witness(Field3.provable, () => Field3.from(4n)); + * let y = Provable.witness(Field3.provable, () => Field3.from(5n)); + * let z = Provable.witness(Field3.provable, () => Field3.from(10n)); + * + * ForeignField.assertAlmostFieldElements([x, y, z], f); + * + * // now we can use x, y, z as inputs to foreign field multiplication + * let xy = ForeignField.mul(x, y, f); + * let xyz = ForeignField.mul(xy, z, f); + * + * // since xy is an input to another multiplication, we need to prove that it is almost reduced again! + * ForeignField.assertAlmostFieldElements([xy], f); // TODO: would be more efficient to batch this with 2 other elements + * ``` + */ + assertAlmostFieldElements(xs: Field3[], f: bigint) { + ForeignField.assertAlmostFieldElements(xs, f); + }, }, /** @@ -550,7 +580,11 @@ const Gadgets = { * should this be here, given that it's not a provable method? * maybe assert that we are not running in provable context */ - sign(Curve: CurveAffine, msgHash: bigint, privateKey: bigint) { + sign( + Curve: CurveAffine, + msgHash: bigint, + privateKey: bigint + ): Ecdsa.signature { return Ecdsa.sign(Curve, msgHash, privateKey); }, diff --git a/src/lib/util/types.ts b/src/lib/util/types.ts index f343a89b7..600f5f705 100644 --- a/src/lib/util/types.ts +++ b/src/lib/util/types.ts @@ -38,6 +38,10 @@ const TupleN = { ); return arr as any; }, + + hasLength(n: N, tuple: T[]): tuple is TupleN { + return tuple.length === n; + }, }; type TupleRec = R['length'] extends N From d89133073a5676027c4efbab2c52307a628782e5 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 24 Nov 2023 09:40:38 +0100 Subject: [PATCH 061/102] add more documentation --- src/lib/gadgets/gadgets.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 3cb2c9ce8..75466c9dc 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -420,9 +420,11 @@ const Gadgets = { * **Assumptions**: In addition to the assumption that input limbs are in the range [0, 2^88), as in all foreign field gadgets, * this assumes an additional bound on the inputs: `x * y < 2^264 * p`, where p is the native modulus. * We usually assert this bound by proving that `x[2] < f[2] + 1`, where `x[2]` is the most significant limb of x. - * To do this, use an 88-bit range check on `2^88 - x[2] - (f[2] + 1)`, and same for y. + * To do this, we use an 88-bit range check on `2^88 - x[2] - (f[2] + 1)`, and same for y. * The implication is that x and y are _almost_ reduced modulo f. * + * All of the above assumptions are checked by {@link ForeignField.assertAlmostFieldElements}. + * * **Warning**: This gadget does not add the extra bound check on the result. * So, to use the result in another foreign field multiplication, you have to add the bound check on it yourself, again. * @@ -434,14 +436,8 @@ const Gadgets = { * let x = Provable.witness(Field3.provable, () => Field3.from(f - 1n)); * let y = Provable.witness(Field3.provable, () => Field3.from(f - 2n)); * - * // range check x, y - * Gadgets.multiRangeCheck(x); - * Gadgets.multiRangeCheck(y); - * - * // prove additional bounds - * let x2Bound = x[2].add((1n << 88n) - 1n - (f >> 176n)); - * let y2Bound = y[2].add((1n << 88n) - 1n - (f >> 176n)); - * Gadgets.multiRangeCheck([x2Bound, y2Bound, Field(0n)]); + * // range check x, y and prove additional bounds x[2] <= f[2] + * ForeignField.assertAlmostFieldElements([x, y], f); * * // compute x * y mod f * let z = ForeignField.mul(x, y, f); @@ -497,7 +493,13 @@ const Gadgets = { * * @example * ```ts - * // we assume that x, y, z, a, b, c are range-checked, analogous to `ForeignField.mul()` + * // range-check x, y, z, a, b, c + * ForeignField.assertAlmostFieldElements([x, y, z], f); + * Gadgets.multiRangeCheck(a); + * Gadgets.multiRangeCheck(b); + * Gadgets.multiRangeCheck(c); + * + * // create lazy input sums * let xMinusY = ForeignField.Sum(x).sub(y); * let aPlusBPlusC = ForeignField.Sum(a).add(b).add(c); * From 697ddb37d1a56472a193368c58d6ede1e327df3b Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 24 Nov 2023 09:40:57 +0100 Subject: [PATCH 062/102] use range check helper in ec gadgets --- src/lib/gadgets/elliptic-curve.ts | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 413c7fe7f..c237bcbd6 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -73,14 +73,7 @@ function add(p1: Point, p2: Point, f: bigint) { let m: Field3 = [m0, m1, m2]; let x3: Field3 = [x30, x31, x32]; let y3: Field3 = [y30, y31, y32]; - - multiRangeCheck(m); - multiRangeCheck(x3); - multiRangeCheck(y3); - let mBound = weakBound(m[2], f); - let x3Bound = weakBound(x3[2], f); - let y3Bound = weakBound(y3[2], f); - multiRangeCheck([mBound, x3Bound, y3Bound]); + ForeignField.assertAlmostFieldElements([m, x3, y3], f); // (x1 - x2)*m = y1 - y2 let deltaX = ForeignField.Sum(x1).sub(x2); @@ -124,14 +117,7 @@ function double(p1: Point, f: bigint) { let m: Field3 = [m0, m1, m2]; let x3: Field3 = [x30, x31, x32]; let y3: Field3 = [y30, y31, y32]; - - multiRangeCheck(m); - multiRangeCheck(x3); - multiRangeCheck(y3); - let mBound = weakBound(m[2], f); - let x3Bound = weakBound(x3[2], f); - let y3Bound = weakBound(y3[2], f); - multiRangeCheck([mBound, x3Bound, y3Bound]); + ForeignField.assertAlmostFieldElements([m, x3, y3], f); // x1^2 = x1x1 let x1x1 = ForeignField.mul(x1, x1, f); From 86da89002cc5932477632338a5758e73abf07f2c Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 24 Nov 2023 10:45:07 +0100 Subject: [PATCH 063/102] add tests --- src/lib/gadgets/foreign-field.unit-test.ts | 29 +++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/lib/gadgets/foreign-field.unit-test.ts b/src/lib/gadgets/foreign-field.unit-test.ts index 371325cf6..2e9c4732f 100644 --- a/src/lib/gadgets/foreign-field.unit-test.ts +++ b/src/lib/gadgets/foreign-field.unit-test.ts @@ -2,6 +2,7 @@ import type { FiniteField } from '../../bindings/crypto/finite_field.js'; import { exampleFields } from '../../bindings/crypto/finite-field-examples.js'; import { array, + equivalent, equivalentAsync, equivalentProvable, fromRandom, @@ -30,6 +31,7 @@ import { throwError, unreducedForeignField, } from './test-utils.js'; +import { l2 } from './range-check.js'; const { ForeignField, Field3 } = Gadgets; @@ -105,6 +107,11 @@ for (let F of fields) { 'div unreduced' ); + equivalent({ from: [big264], to: unit })( + (x) => assertWeakBound(x, F.modulus), + (x) => ForeignField.assertAlmostFieldElements([x], F.modulus) + ); + // sumchain of 5 equivalentProvable({ from: [array(f, 5), array(sign, 4)], to: f })( (xs, signs) => sum(xs, signs, F), @@ -134,6 +141,7 @@ for (let F of fields) { let F = exampleFields.secp256k1; let f = foreignField(F); +let big264 = unreducedForeignField(264, F); let chainLength = 5; let signs = [1n, -1n, -1n, 1n] satisfies (-1n | 1n)[]; @@ -147,6 +155,13 @@ let ffProgram = ZkProgram({ return ForeignField.sum(xs, signs, F.modulus); }, }, + mulWithBoundsCheck: { + privateInputs: [Field3.provable, Field3.provable], + method(x, y) { + ForeignField.assertAlmostFieldElements([x, y], F.modulus); + return ForeignField.mul(x, y, F.modulus); + }, + }, mul: { privateInputs: [Field3.provable, Field3.provable], method(x, y) { @@ -220,10 +235,14 @@ await equivalentAsync({ from: [array(f, chainLength)], to: f }, { runs })( 'prove chain' ); -await equivalentAsync({ from: [f, f], to: f }, { runs })( - F.mul, +await equivalentAsync({ from: [big264, big264], to: f }, { runs })( + (x, y) => { + assertWeakBound(x, F.modulus); + assertWeakBound(y, F.modulus); + return F.mul(x, y); + }, async (x, y) => { - let proof = await ffProgram.mul(x, y); + let proof = await ffProgram.mulWithBoundsCheck(x, y); assert(await ffProgram.verify(proof), 'verifies'); return proof.publicOutput; }, @@ -317,3 +336,7 @@ function sum(xs: bigint[], signs: (1n | -1n)[], F: FiniteField) { } return sum; } + +function assertWeakBound(x: bigint, f: bigint) { + assert(x >= 0n && x >> l2 <= f >> l2, 'weak bound'); +} From 206440df41f29a28f1896ff0bf9bfd54fad514b7 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 27 Nov 2023 12:41:37 +0100 Subject: [PATCH 064/102] expose ecdsa types --- src/lib/gadgets/elliptic-curve.ts | 4 ++-- src/lib/gadgets/gadgets.ts | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index c237bcbd6..885f3e9ad 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -2,8 +2,8 @@ import { inverse, mod } from '../../bindings/crypto/finite_field.js'; import { Field } from '../field.js'; import { Provable } from '../provable.js'; import { assert, exists } from './common.js'; -import { Field3, ForeignField, split, weakBound } from './foreign-field.js'; -import { l, multiRangeCheck } from './range-check.js'; +import { Field3, ForeignField, split } from './foreign-field.js'; +import { l } from './range-check.js'; import { sha256 } from 'js-sha256'; import { bigIntToBits, diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 75466c9dc..45954a263 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -616,5 +616,15 @@ export namespace Gadgets { */ export type Sum = Sum_; } + + export namespace Ecdsa { + /** + * ECDSA signature consisting of two curve scalars. + */ + export type Signature = EcdsaSignature; + export type signature = ecdsaSignature; + } } type Sum_ = Sum; +type EcdsaSignature = Ecdsa.Signature; +type ecdsaSignature = Ecdsa.signature; From 8aff97a1315b4a831c9fe0d1565c3d3ead861b3a Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 27 Nov 2023 12:41:46 +0100 Subject: [PATCH 065/102] start ecdsa example --- src/examples/zkprogram/ecdsa/ecdsa.ts | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/examples/zkprogram/ecdsa/ecdsa.ts diff --git a/src/examples/zkprogram/ecdsa/ecdsa.ts b/src/examples/zkprogram/ecdsa/ecdsa.ts new file mode 100644 index 000000000..8f3070389 --- /dev/null +++ b/src/examples/zkprogram/ecdsa/ecdsa.ts @@ -0,0 +1,32 @@ +import { Gadgets, ZkProgram, Struct } from 'o1js'; + +export { ecdsaProgram }; + +let { ForeignField, Field3, Ecdsa } = Gadgets; + +type PublicKey = { x: Gadgets.Field3; y: Gadgets.Field3 }; +const PublicKey = Struct({ x: Field3.provable, y: Field3.provable }); + +const ecdsaProgram = ZkProgram({ + name: 'ecdsa', + publicInput: PublicKey, + + methods: { + verifyEcdsa: { + privateInputs: [Ecdsa.Signature.provable, Field3.provable], + method( + publicKey: PublicKey, + signature: Gadgets.Ecdsa.Signature, + msgHash: Gadgets.Field3 + ) { + // assert that private inputs are valid + ForeignField.assertAlmostFieldElements( + [signature.r, signature.s, msgHash], + 0n + ); + + Ecdsa.verify(Secp256k1, signature, msgHash, publicKey); + }, + }, + }, +}); From 05925b2aee58e7716aad8f0a584ca84eb85250a6 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 27 Nov 2023 13:03:30 +0100 Subject: [PATCH 066/102] expose elliptic curve intf on new Crypto namespace --- src/index.ts | 2 ++ src/lib/crypto.ts | 31 +++++++++++++++++++++++++++++++ src/lib/gadgets/gadgets.ts | 3 ++- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/lib/crypto.ts diff --git a/src/index.ts b/src/index.ts index 5c617fced..791e40e01 100644 --- a/src/index.ts +++ b/src/index.ts @@ -81,6 +81,8 @@ export { Nullifier } from './lib/nullifier.js'; import { ExperimentalZkProgram, ZkProgram } from './lib/proof_system.js'; export { ZkProgram }; +export { Crypto } from './lib/crypto.js'; + // experimental APIs import { Callback } from './lib/zkapp.js'; import { createChildAccountUpdate } from './lib/account_update.js'; diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts new file mode 100644 index 000000000..3786284a9 --- /dev/null +++ b/src/lib/crypto.ts @@ -0,0 +1,31 @@ +import { CurveParams as CurveParams_ } from '../bindings/crypto/elliptic-curve-examples.js'; +import { + CurveAffine, + createCurveAffine, +} from '../bindings/crypto/elliptic_curve.js'; + +// crypto namespace +const Crypto = { + /** + * Create elliptic curve arithmetic methods. + */ + createCurve(params: Crypto.CurveParams): Crypto.Curve { + return createCurveAffine(params); + }, + /** + * Parameters defining an elliptic curve in short Weierstraß form + * y^2 = x^3 + ax + b + */ + CurveParams: CurveParams_, +}; + +namespace Crypto { + /** + * Parameters defining an elliptic curve in short Weierstraß form + * y^2 = x^3 + ax + b + */ + export type CurveParams = CurveParams_; + + export type Curve = CurveAffine; +} +export { Crypto }; diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 45954a263..e7c97d77d 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -11,6 +11,7 @@ import { Field } from '../core.js'; import { ForeignField, Field3, Sum } from './foreign-field.js'; import { Ecdsa, Point } from './elliptic-curve.js'; import { CurveAffine } from '../../bindings/crypto/elliptic_curve.js'; +import { Crypto } from '../crypto.js'; export { Gadgets }; @@ -583,7 +584,7 @@ const Gadgets = { * maybe assert that we are not running in provable context */ sign( - Curve: CurveAffine, + Curve: Crypto.Curve, msgHash: bigint, privateKey: bigint ): Ecdsa.signature { From 89f7d01050c2cbdcf2c23183caac4ec5201914cc Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 27 Nov 2023 13:21:28 +0100 Subject: [PATCH 067/102] finish example --- src/examples/zkprogram/ecdsa/ecdsa.ts | 23 +++++++++++----- src/examples/zkprogram/ecdsa/run.ts | 38 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 src/examples/zkprogram/ecdsa/run.ts diff --git a/src/examples/zkprogram/ecdsa/ecdsa.ts b/src/examples/zkprogram/ecdsa/ecdsa.ts index 8f3070389..183ec3e7f 100644 --- a/src/examples/zkprogram/ecdsa/ecdsa.ts +++ b/src/examples/zkprogram/ecdsa/ecdsa.ts @@ -1,30 +1,39 @@ -import { Gadgets, ZkProgram, Struct } from 'o1js'; +import { Gadgets, ZkProgram, Struct, Crypto } from 'o1js'; -export { ecdsaProgram }; +export { ecdsaProgram, Point, Secp256k1 }; let { ForeignField, Field3, Ecdsa } = Gadgets; -type PublicKey = { x: Gadgets.Field3; y: Gadgets.Field3 }; -const PublicKey = Struct({ x: Field3.provable, y: Field3.provable }); +// TODO expose this as part of Gadgets.Curve + +class Point extends Struct({ x: Field3.provable, y: Field3.provable }) { + // point from bigints + static from({ x, y }: { x: bigint; y: bigint }) { + return new Point({ x: Field3.from(x), y: Field3.from(y) }); + } +} + +const Secp256k1 = Crypto.createCurve(Crypto.CurveParams.Secp256k1); const ecdsaProgram = ZkProgram({ name: 'ecdsa', - publicInput: PublicKey, + publicInput: Point, methods: { verifyEcdsa: { privateInputs: [Ecdsa.Signature.provable, Field3.provable], method( - publicKey: PublicKey, + publicKey: Point, signature: Gadgets.Ecdsa.Signature, msgHash: Gadgets.Field3 ) { // assert that private inputs are valid ForeignField.assertAlmostFieldElements( [signature.r, signature.s, msgHash], - 0n + Secp256k1.order ); + // verify signature Ecdsa.verify(Secp256k1, signature, msgHash, publicKey); }, }, diff --git a/src/examples/zkprogram/ecdsa/run.ts b/src/examples/zkprogram/ecdsa/run.ts new file mode 100644 index 000000000..936c896d5 --- /dev/null +++ b/src/examples/zkprogram/ecdsa/run.ts @@ -0,0 +1,38 @@ +import { Gadgets } from 'o1js'; +import { Point, Secp256k1, ecdsaProgram } from './ecdsa.js'; +import assert from 'assert'; + +// create an example ecdsa signature +let privateKey = Secp256k1.Scalar.random(); +let publicKey = Secp256k1.scale(Secp256k1.one, privateKey); + +// TODO make this use an actual keccak hash +let messageHash = Secp256k1.Scalar.random(); + +let signature = Gadgets.Ecdsa.sign(Secp256k1, messageHash, privateKey); + +console.time('ecdsa verify (build constraint system)'); +let cs = ecdsaProgram.analyzeMethods().verifyEcdsa; +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.time('ecdsa verify (compile)'); +await ecdsaProgram.compile(); +console.timeEnd('ecdsa verify (compile)'); + +console.time('ecdsa verify (prove)'); +let proof = await ecdsaProgram.verifyEcdsa( + Point.from(publicKey), + Gadgets.Ecdsa.Signature.from(signature), + Gadgets.Field3.from(messageHash) +); +console.timeEnd('ecdsa verify (prove)'); + +assert(await ecdsaProgram.verify(proof), 'proof verifies'); From 2094f6878e168c7ea84909e77510966d89ba6be2 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 27 Nov 2023 13:40:26 +0100 Subject: [PATCH 068/102] make vk returned by zkprogram consistent with smart contract --- src/index.ts | 2 +- src/lib/proof_system.ts | 21 +++++++++++++++------ src/lib/zkapp.ts | 18 +----------------- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/index.ts b/src/index.ts index 791e40e01..e47eb9e46 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,7 +32,6 @@ export { method, declareMethods, Account, - VerificationKey, Reducer, } from './lib/zkapp.js'; export { state, State, declareState } from './lib/state.js'; @@ -45,6 +44,7 @@ export { Empty, Undefined, Void, + VerificationKey, } from './lib/proof_system.js'; export { Cache, CacheHeader } from './lib/proof-system/cache.js'; diff --git a/src/lib/proof_system.ts b/src/lib/proof_system.ts index 08adef5f6..1d3ace221 100644 --- a/src/lib/proof_system.ts +++ b/src/lib/proof_system.ts @@ -18,6 +18,8 @@ import { FlexibleProvablePure, InferProvable, ProvablePureExtended, + Struct, + provable, provablePure, toConstant, } from './circuit_value.js'; @@ -25,7 +27,7 @@ import { Provable } from './provable.js'; import { assert, prettifyStacktracePromise } from './errors.js'; import { snarkContext } from './provable-context.js'; import { hashConstant } from './hash.js'; -import { MlArray, MlBool, MlResult, MlPair, MlUnit } from './ml/base.js'; +import { MlArray, MlBool, MlResult, MlPair } from './ml/base.js'; import { MlFieldArray, MlFieldConstArray } from './ml/fields.js'; import { FieldConst, FieldVar } from './field.js'; import { Cache, readCache, writeCache } from './proof-system/cache.js'; @@ -47,6 +49,7 @@ export { Empty, Undefined, Void, + VerificationKey, }; // internal API @@ -260,10 +263,9 @@ function ZkProgram< } ): { name: string; - compile: (options?: { - cache?: Cache; - forceRecompile?: boolean; - }) => Promise<{ verificationKey: string }>; + compile: (options?: { cache?: Cache; forceRecompile?: boolean }) => Promise<{ + verificationKey: { data: string; hash: Field }; + }>; verify: ( proof: Proof< InferProvableOrUndefined>, @@ -361,7 +363,7 @@ function ZkProgram< overrideWrapDomain: config.overrideWrapDomain, }); compileOutput = { provers, verify }; - return { verificationKey: verificationKey.data }; + return { verificationKey }; } function toProver( @@ -485,6 +487,13 @@ class SelfProof extends Proof< PublicOutput > {} +class VerificationKey extends Struct({ + ...provable({ data: String, hash: Field }), + toJSON({ data }: { data: string }) { + return data; + }, +}) {} + function sortMethodArguments( programName: string, methodName: string, diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index a0df136ba..45dd3a042 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -66,7 +66,6 @@ export { declareMethods, Callback, Account, - VerificationKey, Reducer, }; @@ -681,11 +680,7 @@ class SmartContract { // run methods once to get information that we need already at compile time let methodsMeta = this.analyzeMethods(); let gates = methodIntfs.map((intf) => methodsMeta[intf.methodName].gates); - let { - verificationKey: verificationKey_, - provers, - verify, - } = await compileProgram({ + let { verificationKey, provers, verify } = await compileProgram({ publicInputType: ZkappPublicInput, publicOutputType: Empty, methodIntfs, @@ -695,10 +690,6 @@ class SmartContract { cache, forceRecompile, }); - let verificationKey = { - data: verificationKey_.data, - hash: Field(verificationKey_.hash), - } satisfies VerificationKey; this._provers = provers; this._verificationKey = verificationKey; // TODO: instead of returning provers, return an artifact from which provers can be recovered @@ -1488,13 +1479,6 @@ Use the optional \`maxTransactionsWithActions\` argument to increase this number }; } -class VerificationKey extends Struct({ - ...provable({ data: String, hash: Field }), - toJSON({ data }: { data: string }) { - return data; - }, -}) {} - function selfAccountUpdate(zkapp: SmartContract, methodName?: string) { let body = Body.keepAll(zkapp.address, zkapp.tokenId); let update = new (AccountUpdate as any)(body, {}, true) as AccountUpdate; From 9ad5794371387b225e1b3586312783797a4ad442 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 27 Nov 2023 13:40:34 +0100 Subject: [PATCH 069/102] changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9aa81a19..ecbdb943a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,14 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased](https://github.com/o1-labs/o1js/compare/1ad7333e9e...HEAD) +# Breaking changes + +- `ZkProgram.compile()` now returns the verification key and its hash, to be consistent with `SmartContract.compile()` https://github.com/o1-labs/o1js/pull/1240 + # Added +- **ECDSA signature verification**: new provable method `Gadgets.Ecdsa.verify()` and helpers on `Gadgets.Ecdsa.Signature` https://github.com/o1-labs/o1js/pull/1240 + - For an example, see `./src/examples/zkprogram/ecdsa` - `Gadgets.ForeignField.assertMul()` for efficiently constraining products of sums in non-native arithmetic https://github.com/o1-labs/o1js/pull/1262 - `Unconstrained` for safely maintaining unconstrained values in provable code https://github.com/o1-labs/o1js/pull/1262 From e7efc5d0e4dda1b5b2adeef3622b2f746c909022 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 27 Nov 2023 13:42:28 +0100 Subject: [PATCH 070/102] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecbdb943a..acab4eb01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - **ECDSA signature verification**: new provable method `Gadgets.Ecdsa.verify()` and helpers on `Gadgets.Ecdsa.Signature` https://github.com/o1-labs/o1js/pull/1240 - For an example, see `./src/examples/zkprogram/ecdsa` +- `Crypto` namespace which exposes elliptic curve and finite field arithmetic on bigints, as well as example curve parameters https://github.com/o1-labs/o1js/pull/1240 - `Gadgets.ForeignField.assertMul()` for efficiently constraining products of sums in non-native arithmetic https://github.com/o1-labs/o1js/pull/1262 - `Unconstrained` for safely maintaining unconstrained values in provable code https://github.com/o1-labs/o1js/pull/1262 From e0c43f779a59517f50ad4cdf4818c6c6f5d1b3e1 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 27 Nov 2023 13:57:52 +0100 Subject: [PATCH 071/102] fix: use a variable for public key when analyzing ecdsa constraints --- src/lib/gadgets/ecdsa.unit-test.ts | 9 ++++++--- src/lib/gadgets/elliptic-curve.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index 47a4f0228..144578c2c 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -95,7 +95,7 @@ let msgHash = ); const ia = EllipticCurve.initialAggregator(Secp256k1); -const config = { G: { windowSize: 4 }, P: { windowSize: 4 }, ia }; +const config = { G: { windowSize: 4 }, P: { windowSize: 3 }, ia }; let program = ZkProgram({ name: 'ecdsa', @@ -119,11 +119,14 @@ let program = ZkProgram({ ecdsa: { privateInputs: [], method() { - let signature0 = Provable.witness( + let signature_ = Provable.witness( Ecdsa.Signature.provable, () => signature ); - Ecdsa.verify(Secp256k1, signature0, msgHash, publicKey, config); + let msgHash_ = Provable.witness(Field3.provable, () => msgHash); + let publicKey_ = Provable.witness(Point.provable, () => publicKey); + + Ecdsa.verify(Secp256k1, signature_, msgHash_, publicKey_, config); }, }, }, diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 885f3e9ad..da8bbe924 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -149,7 +149,7 @@ function verifyEcdsa( G?: { windowSize: number; multiples?: Point[] }; P?: { windowSize: number; multiples?: Point[] }; ia?: point; - } = { G: { windowSize: 4 }, P: { windowSize: 4 } } + } = { G: { windowSize: 4 }, P: { windowSize: 3 } } ) { // constant case if ( From a2fe70f1ea3b5178a76d1b02b6582f08f703a0cb Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 27 Nov 2023 14:00:05 +0100 Subject: [PATCH 072/102] add vk regression test for ecdsa --- src/examples/zkprogram/ecdsa/run.ts | 2 +- tests/vk-regression/vk-regression.json | 13 +++++++++++++ tests/vk-regression/vk-regression.ts | 2 ++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/examples/zkprogram/ecdsa/run.ts b/src/examples/zkprogram/ecdsa/run.ts index 936c896d5..9e3b5518f 100644 --- a/src/examples/zkprogram/ecdsa/run.ts +++ b/src/examples/zkprogram/ecdsa/run.ts @@ -6,7 +6,7 @@ import assert from 'assert'; let privateKey = Secp256k1.Scalar.random(); let publicKey = Secp256k1.scale(Secp256k1.one, privateKey); -// TODO make this use an actual keccak hash +// TODO use an actual keccak hash let messageHash = Secp256k1.Scalar.random(); let signature = Gadgets.Ecdsa.sign(Secp256k1, messageHash, privateKey); diff --git a/tests/vk-regression/vk-regression.json b/tests/vk-regression/vk-regression.json index f60540c9f..9c4f4f259 100644 --- a/tests/vk-regression/vk-regression.json +++ b/tests/vk-regression/vk-regression.json @@ -201,5 +201,18 @@ "data": "", "hash": "" } + }, + "ecdsa": { + "digest": "1025b5f3a56c5366fd44d13f2678bba563c9581c6bacfde2b82a8dd49e33f2a2", + "methods": { + "verifyEcdsa": { + "rows": 38823, + "digest": "65c6f9efe1069f73adf3a842398f95d2" + } + }, + "verificationKey": { + "data": "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", + "hash": "2818165315298159349200200610054154136732885996642834698461943280515655745528" + } } } \ No newline at end of file diff --git a/tests/vk-regression/vk-regression.ts b/tests/vk-regression/vk-regression.ts index 28a95542b..a051f2113 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/zkprogram/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'; From 665158ba129f246241122dfb747fcfc166753767 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 27 Nov 2023 14:12:34 +0100 Subject: [PATCH 073/102] expose cs printing --- src/examples/constraint_system.ts | 4 +- src/lib/provable-context.ts | 69 +++++++++++++++++++++++++++- src/lib/testing/constraint-system.ts | 56 +--------------------- 3 files changed, 70 insertions(+), 59 deletions(-) diff --git a/src/examples/constraint_system.ts b/src/examples/constraint_system.ts index 6acb7fde5..f2da63ad3 100644 --- a/src/examples/constraint_system.ts +++ b/src/examples/constraint_system.ts @@ -2,7 +2,7 @@ import { Field, Poseidon, Provable } from 'o1js'; let hash = Poseidon.hash([Field(1), Field(-1)]); -let { rows, digest, gates, publicInputSize } = Provable.constraintSystem(() => { +let { rows, digest, publicInputSize, print } = Provable.constraintSystem(() => { let x = Provable.witness(Field, () => Field(1)); let y = Provable.witness(Field, () => Field(-1)); x.add(y).assertEquals(Field(0)); @@ -10,5 +10,5 @@ let { rows, digest, gates, publicInputSize } = Provable.constraintSystem(() => { z.assertEquals(hash); }); -console.log(JSON.stringify(gates)); +print(); console.log({ rows, digest, publicInputSize }); diff --git a/src/lib/provable-context.ts b/src/lib/provable-context.ts index 076e9f830..0ce003055 100644 --- a/src/lib/provable-context.ts +++ b/src/lib/provable-context.ts @@ -2,6 +2,7 @@ import { Context } from './global-context.js'; import { Gate, 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'; // internal API export { @@ -17,6 +18,7 @@ export { inCompile, inCompileMode, gatesFromJson, + printGates, }; // global circuit-related context @@ -94,7 +96,16 @@ function constraintSystem(f: () => T) { result = f(); }); let { gates, publicInputSize } = gatesFromJson(json); - return { rows, digest, result: result! as T, gates, publicInputSize }; + return { + rows, + digest, + result: result! as T, + gates, + publicInputSize, + print() { + printGates(gates); + }, + }; } catch (error) { throw prettifyStacktrace(error); } finally { @@ -106,8 +117,62 @@ function constraintSystem(f: () => T) { function gatesFromJson(cs: { gates: JsonGate[]; public_input_size: number }) { let gates: Gate[] = cs.gates.map(({ typ, wires, coeffs: hexCoeffs }) => { - let coeffs = hexCoeffs.map(hex => parseHexString(hex).toString()); + let coeffs = hexCoeffs.map((hex) => parseHexString(hex).toString()); return { type: typ, wires, coeffs }; }); return { publicInputSize: cs.public_input_size, gates }; } + +// print a constraint system + +function printGates(gates: Gate[]) { + for (let i = 0, n = gates.length; i < n; i++) { + let { type, wires, coeffs } = gates[i]; + console.log( + i.toString().padEnd(4, ' '), + type.padEnd(15, ' '), + coeffsToPretty(type, coeffs).padEnd(30, ' '), + wiresToPretty(wires, i) + ); + } + console.log(); +} + +let minusRange = Fp.modulus - (1n << 64n); + +function coeffsToPretty(type: Gate['type'], coeffs: Gate['coeffs']): string { + if (coeffs.length === 0) return ''; + if (type === 'Generic' && coeffs.length > 5) { + let first = coeffsToPretty(type, coeffs.slice(0, 5)); + let second = coeffsToPretty(type, coeffs.slice(5)); + return `${first} ${second}`; + } + if (type === 'Poseidon' && coeffs.length > 3) { + return `${coeffsToPretty(type, coeffs.slice(0, 3)).slice(0, -1)} ...]`; + } + let str = coeffs + .map((c) => { + let c0 = BigInt(c); + if (c0 > minusRange) c0 -= Fp.modulus; + let cStr = c0.toString(); + if (cStr.length > 4) return `${cStr.slice(0, 4)}..`; + return cStr; + }) + .join(' '); + return `[${str}]`; +} + +function wiresToPretty(wires: Gate['wires'], row: number) { + let strWires: string[] = []; + let n = wires.length; + for (let col = 0; col < n; col++) { + let wire = wires[col]; + if (wire.row === row && wire.col === col) continue; + if (wire.row === row) { + strWires.push(`${col}->${wire.col}`); + } else { + strWires.push(`${col}->(${wire.row},${wire.col})`); + } + } + return strWires.join(', '); +} diff --git a/src/lib/testing/constraint-system.ts b/src/lib/testing/constraint-system.ts index d39c6f699..2922afa11 100644 --- a/src/lib/testing/constraint-system.ts +++ b/src/lib/testing/constraint-system.ts @@ -12,6 +12,7 @@ import { Tuple } from '../util/types.js'; import { Random } from './random.js'; import { test } from './property.js'; import { Undefined, ZkProgram } from '../proof_system.js'; +import { printGates } from '../provable-context.js'; export { constraintSystem, @@ -27,7 +28,6 @@ export { withoutGenerics, print, repeat, - printGates, ConstraintSystemTest, }; @@ -446,57 +446,3 @@ type CsParams>> = { [k in keyof In]: InferCsVar; }; type TypeAndValue = { type: Provable; value: T }; - -// print a constraint system - -function printGates(gates: Gate[]) { - for (let i = 0, n = gates.length; i < n; i++) { - let { type, wires, coeffs } = gates[i]; - console.log( - i.toString().padEnd(4, ' '), - type.padEnd(15, ' '), - coeffsToPretty(type, coeffs).padEnd(30, ' '), - wiresToPretty(wires, i) - ); - } - console.log(); -} - -let minusRange = Field.ORDER - (1n << 64n); - -function coeffsToPretty(type: Gate['type'], coeffs: Gate['coeffs']): string { - if (coeffs.length === 0) return ''; - if (type === 'Generic' && coeffs.length > 5) { - let first = coeffsToPretty(type, coeffs.slice(0, 5)); - let second = coeffsToPretty(type, coeffs.slice(5)); - return `${first} ${second}`; - } - if (type === 'Poseidon' && coeffs.length > 3) { - return `${coeffsToPretty(type, coeffs.slice(0, 3)).slice(0, -1)} ...]`; - } - let str = coeffs - .map((c) => { - let c0 = BigInt(c); - if (c0 > minusRange) c0 -= Field.ORDER; - let cStr = c0.toString(); - if (cStr.length > 4) return `${cStr.slice(0, 4)}..`; - return cStr; - }) - .join(' '); - return `[${str}]`; -} - -function wiresToPretty(wires: Gate['wires'], row: number) { - let strWires: string[] = []; - let n = wires.length; - for (let col = 0; col < n; col++) { - let wire = wires[col]; - if (wire.row === row && wire.col === col) continue; - if (wire.row === row) { - strWires.push(`${col}->${wire.col}`); - } else { - strWires.push(`${col}->(${wire.row},${wire.col})`); - } - } - return strWires.join(', '); -} From 71b7dcffac9550190f4f9cb8604b808e869566de Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 27 Nov 2023 14:13:14 +0100 Subject: [PATCH 074/102] minor --- src/examples/zkprogram/ecdsa/run.ts | 5 +++++ src/lib/gadgets/elliptic-curve.unit-test.ts | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/examples/zkprogram/ecdsa/run.ts b/src/examples/zkprogram/ecdsa/run.ts index 9e3b5518f..b1d502c7e 100644 --- a/src/examples/zkprogram/ecdsa/run.ts +++ b/src/examples/zkprogram/ecdsa/run.ts @@ -3,6 +3,7 @@ import { Point, Secp256k1, ecdsaProgram } from './ecdsa.js'; import assert from 'assert'; // create an example ecdsa signature + let privateKey = Secp256k1.Scalar.random(); let publicKey = Secp256k1.scale(Secp256k1.one, privateKey); @@ -11,6 +12,8 @@ let messageHash = Secp256k1.Scalar.random(); let signature = Gadgets.Ecdsa.sign(Secp256k1, messageHash, privateKey); +// 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)'); @@ -23,6 +26,8 @@ for (let gate of cs.gates) { } console.log(gateTypes); +// compile and prove + console.time('ecdsa verify (compile)'); await ecdsaProgram.compile(); console.timeEnd('ecdsa verify (compile)'); diff --git a/src/lib/gadgets/elliptic-curve.unit-test.ts b/src/lib/gadgets/elliptic-curve.unit-test.ts index 722ca3137..00b555239 100644 --- a/src/lib/gadgets/elliptic-curve.unit-test.ts +++ b/src/lib/gadgets/elliptic-curve.unit-test.ts @@ -1,7 +1,6 @@ import { Provable } from '../provable.js'; import { Field3 } from './foreign-field.js'; import { EllipticCurve } from './elliptic-curve.js'; -import { printGates } from '../testing/constraint-system.js'; import { assert } from './common.js'; import { createCurveAffine } from '../../bindings/crypto/elliptic_curve.js'; import { CurveParams } from '../../bindings/crypto/elliptic-curve-examples.js'; @@ -32,10 +31,10 @@ let csDouble = Provable.constraintSystem(() => { double(g, Secp256k1.modulus); }); -printGates(csAdd.gates); +csAdd.print(); console.log({ digest: csAdd.digest, rows: csAdd.rows }); -printGates(csDouble.gates); +csDouble.print(); console.log({ digest: csDouble.digest, rows: csDouble.rows }); let point = initialAggregator(Pallas); From febd9c9b4d625f05194a470ac59f6b604b15973d Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 27 Nov 2023 14:13:31 +0100 Subject: [PATCH 075/102] delete ec unit test --- src/lib/gadgets/elliptic-curve.unit-test.ts | 42 --------------------- 1 file changed, 42 deletions(-) delete mode 100644 src/lib/gadgets/elliptic-curve.unit-test.ts diff --git a/src/lib/gadgets/elliptic-curve.unit-test.ts b/src/lib/gadgets/elliptic-curve.unit-test.ts deleted file mode 100644 index 00b555239..000000000 --- a/src/lib/gadgets/elliptic-curve.unit-test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Provable } from '../provable.js'; -import { Field3 } from './foreign-field.js'; -import { EllipticCurve } from './elliptic-curve.js'; -import { assert } from './common.js'; -import { createCurveAffine } from '../../bindings/crypto/elliptic_curve.js'; -import { CurveParams } from '../../bindings/crypto/elliptic-curve-examples.js'; - -const Secp256k1 = createCurveAffine(CurveParams.Secp256k1); -const Pallas = createCurveAffine(CurveParams.Pallas); - -let { add, double, initialAggregator } = EllipticCurve; - -let csAdd = Provable.constraintSystem(() => { - let x1 = Provable.witness(Field3.provable, () => Field3.from(0n)); - let x2 = Provable.witness(Field3.provable, () => Field3.from(0n)); - let y1 = Provable.witness(Field3.provable, () => Field3.from(0n)); - let y2 = Provable.witness(Field3.provable, () => Field3.from(0n)); - - let g = { x: x1, y: y1 }; - let h = { x: x2, y: y2 }; - - add(g, h, Secp256k1.modulus); -}); - -let csDouble = Provable.constraintSystem(() => { - let x1 = Provable.witness(Field3.provable, () => Field3.from(0n)); - let y1 = Provable.witness(Field3.provable, () => Field3.from(0n)); - - let g = { x: x1, y: y1 }; - - double(g, Secp256k1.modulus); -}); - -csAdd.print(); -console.log({ digest: csAdd.digest, rows: csAdd.rows }); - -csDouble.print(); -console.log({ digest: csDouble.digest, rows: csDouble.rows }); - -let point = initialAggregator(Pallas); -console.log({ point }); -assert(Pallas.isOnCurve(point)); From f27f2e1c96d639e88b908864d1e4bf445108980e Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 27 Nov 2023 14:17:25 +0100 Subject: [PATCH 076/102] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index acab4eb01..3f361304c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - `this.account.x.getAndAssertEquals(x)` is now `this.account.x.requireEquals(x)` https://github.com/o1-labs/o1js/pull/1265 - `this.account.x.assertBetween()` is now `this.account.x.requireBetween()` https://github.com/o1-labs/o1js/pull/1265 - `this.network.x.getAndAssertEquals()` is now `this.network.x.getAndRequireEquals()` https://github.com/o1-labs/o1js/pull/1265 +- `Provable.constraintSystem()` and `{ZkProgram,SmartContract}.analyzeMethods()` return a `print()` method for pretty-printing the constraint system https://github.com/o1-labs/o1js/pull/1240 ## [0.14.2](https://github.com/o1-labs/o1js/compare/26363465d...1ad7333e9e) From 191971da1d52dd2c8ba2cc5b56e806c63cf6b302 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 27 Nov 2023 14:24:48 +0100 Subject: [PATCH 077/102] finish doccomments --- src/lib/gadgets/gadgets.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index e7c97d77d..bcfc0a41b 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -551,20 +551,24 @@ const Gadgets = { }, /** - * TODO + * ECDSA verification gadget and helper methods. */ Ecdsa: { /** - * TODO + * Verify an ECDSA signature. * * @example * ```ts - * let Curve = Curves.Secp256k1; // TODO provide this somehow - * // TODO easy way to check that foreign field elements are valid - * let signature = { r, s }; - * // TODO need a way to check that publicKey is on curve - * let publicKey = { x, y }; + * const Curve = Crypto.createCurve(Crypto.CurveParams.Secp256k1); * + * // assert that message hash and signature are valid scalar field elements + * Gadgets.ForeignField.assertAlmostFieldElements( + * [signature.r, signature.s, msgHash], + * Curve.order + * ); + * // TODO add an easy way to prove that the public key lies on the curve + * + * // verify signature * Gadgets.Ecdsa.verify(Curve, signature, msgHash, publicKey); * ``` */ @@ -578,10 +582,9 @@ const Gadgets = { }, /** - * TODO + * Sign a message hash using ECDSA. * - * should this be here, given that it's not a provable method? - * maybe assert that we are not running in provable context + * _This method is not provable._ */ sign( Curve: Crypto.Curve, From 6a449b25b1a6c59e0f685c173529a3add890fcca Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 27 Nov 2023 14:24:54 +0100 Subject: [PATCH 078/102] bindings --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index b67e80608..b432ffd75 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit b67e80608e467b791662ddc01965863848bb38b5 +Subproject commit b432ffd750b4ee961e4b9924f69b5e07bc5368a8 From d0dc254c533e5750c29f2baf7b066a349e9426b4 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 27 Nov 2023 15:11:14 +0100 Subject: [PATCH 079/102] fix unit test --- src/lib/proof_system.unit-test.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/lib/proof_system.unit-test.ts b/src/lib/proof_system.unit-test.ts index b89f1f3dd..54b95e6d4 100644 --- a/src/lib/proof_system.unit-test.ts +++ b/src/lib/proof_system.unit-test.ts @@ -16,13 +16,15 @@ const EmptyProgram = ZkProgram({ }); const emptyMethodsMetadata = EmptyProgram.analyzeMethods(); -expect(emptyMethodsMetadata.run).toEqual({ - rows: 0, - digest: '4f5ddea76d29cfcfd8c595f14e31f21b', - result: undefined, - gates: [], - publicInputSize: 0, -}); +expect(emptyMethodsMetadata.run).toEqual( + expect.objectContaining({ + rows: 0, + digest: '4f5ddea76d29cfcfd8c595f14e31f21b', + result: undefined, + gates: [], + publicInputSize: 0, + }) +); class CounterPublicInput extends Struct({ current: UInt64, From e7ad795ea97931fa65b07e634633dd61c8954b9e Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 30 Nov 2023 14:51:09 +0100 Subject: [PATCH 080/102] move todo out of doccomment --- src/lib/gadgets/gadgets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index bcfc0a41b..9384aaa76 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -554,6 +554,7 @@ const Gadgets = { * ECDSA verification gadget and helper methods. */ Ecdsa: { + // TODO add an easy way to prove that the public key lies on the curve, and show in the example /** * Verify an ECDSA signature. * @@ -566,7 +567,6 @@ const Gadgets = { * [signature.r, signature.s, msgHash], * Curve.order * ); - * // TODO add an easy way to prove that the public key lies on the curve * * // verify signature * Gadgets.Ecdsa.verify(Curve, signature, msgHash, publicKey); From 660f1a4eceb32051f0a82cff70efe6c7fb029f1c Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 30 Nov 2023 15:09:37 +0100 Subject: [PATCH 081/102] return a bool from verify() --- src/bindings | 2 +- src/lib/gadgets/ecdsa.unit-test.ts | 31 ++++++++++++++++++------------ src/lib/gadgets/elliptic-curve.ts | 8 ++++---- src/lib/gadgets/gadgets.ts | 6 +++++- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/bindings b/src/bindings index b432ffd75..80cf218dc 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit b432ffd750b4ee961e4b9924f69b5e07bc5368a8 +Subproject commit 80cf218dca605cc91fbde65bff79f2cce9284a47 diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index 144578c2c..9cbcbe4e7 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -13,12 +13,13 @@ import { assert } from './common.js'; import { foreignField, throwError, uniformForeignField } from './test-utils.js'; import { Second, + bool, equivalentProvable, map, oneOf, record, - unit, } from '../testing/equivalent.js'; +import { Bool } from '../bool.js'; // quick tests const Secp256k1 = createCurveAffine(CurveParams.Secp256k1); @@ -51,27 +52,27 @@ for (let Curve of curves) { // provable method we want to test const verify = (s: Second) => { - Ecdsa.verify(Curve, s.signature, s.msg, s.publicKey); + return Ecdsa.verify(Curve, s.signature, s.msg, s.publicKey); }; // positive test - equivalentProvable({ from: [signature], to: unit })( - () => {}, + equivalentProvable({ from: [signature], to: bool })( + () => true, verify, 'valid signature verifies' ); // negative test - equivalentProvable({ from: [pseudoSignature], to: unit })( - () => throwError('invalid signature'), + equivalentProvable({ from: [pseudoSignature], to: bool })( + () => false, verify, 'invalid signature fails' ); // test against constant implementation, with both invalid and valid signatures - equivalentProvable({ from: [oneOf(signature, pseudoSignature)], to: unit })( + equivalentProvable({ from: [oneOf(signature, pseudoSignature)], to: bool })( ({ signature, publicKey, msg }) => { - assert(verifyEcdsaConstant(Curve, signature, msg, publicKey), 'verifies'); + return verifyEcdsaConstant(Curve, signature, msg, publicKey); }, verify, 'verify' @@ -99,6 +100,7 @@ const config = { G: { windowSize: 4 }, P: { windowSize: 3 }, ia }; let program = ZkProgram({ name: 'ecdsa', + publicOutput: Bool, methods: { scale: { privateInputs: [], @@ -111,9 +113,7 @@ let program = ZkProgram({ [G, P], [config.G, config.P] ); - Provable.asProver(() => { - console.log(Point.toBigint(R)); - }); + return new Bool(true); }, }, ecdsa: { @@ -126,7 +126,13 @@ let program = ZkProgram({ let msgHash_ = Provable.witness(Field3.provable, () => msgHash); let publicKey_ = Provable.witness(Point.provable, () => publicKey); - Ecdsa.verify(Secp256k1, signature_, msgHash_, publicKey_, config); + return Ecdsa.verify( + Secp256k1, + signature_, + msgHash_, + publicKey_, + config + ); }, }, }, @@ -163,3 +169,4 @@ let proof = await program.ecdsa(); console.timeEnd('ecdsa verify (prove)'); assert(await program.verify(proof), 'proof verifies'); +proof.publicOutput.assertTrue('signature verifies'); diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index da8bbe924..203921bc9 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -163,8 +163,7 @@ function verifyEcdsa( Field3.toBigint(msgHash), Point.toBigint(publicKey) ); - assert(isValid, 'invalid signature'); - return; + return new Bool(isValid); } // provable case @@ -191,7 +190,7 @@ function verifyEcdsa( // note: we don't check that the result Rx is canonical, because Rx === r and r is an input: // it's the callers responsibility to check that the signature is valid/unique in whatever way it makes sense for the application let Rx = ForeignField.mul(R.x, Field3.from(1n), Curve.order); - Provable.assertEqual(Field3.provable, Rx, r); + return Provable.equal(Field3.provable, Rx, r); } /** @@ -295,7 +294,8 @@ function verifyEcdsaConstant( msgHash: bigint, publicKey: point ) { - let pk = Curve.fromNonzero(publicKey); + let pk = Curve.from(publicKey); + if (Curve.equal(pk, Curve.zero)) return false; if (!Curve.isOnCurve(pk)) return false; if (Curve.hasCofactor && !Curve.isInSubgroup(pk)) return false; if (r < 1n || r >= Curve.order) return false; diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 9384aaa76..6f9f24b9a 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -558,6 +558,9 @@ const Gadgets = { /** * Verify an ECDSA signature. * + * **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. + * * @example * ```ts * const Curve = Crypto.createCurve(Crypto.CurveParams.Secp256k1); @@ -569,7 +572,8 @@ const Gadgets = { * ); * * // verify signature - * Gadgets.Ecdsa.verify(Curve, signature, msgHash, publicKey); + * let isValid = Gadgets.Ecdsa.verify(Curve, signature, msgHash, publicKey); + * isValid.assertTrue(); * ``` */ verify( From 1a55a5d3f7613684db02d42ccd0f3eb650d58c9c Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 30 Nov 2023 15:49:16 +0100 Subject: [PATCH 082/102] update vk --- tests/vk-regression/vk-regression.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/vk-regression/vk-regression.json b/tests/vk-regression/vk-regression.json index 9c4f4f259..e2899eff8 100644 --- a/tests/vk-regression/vk-regression.json +++ b/tests/vk-regression/vk-regression.json @@ -203,16 +203,16 @@ } }, "ecdsa": { - "digest": "1025b5f3a56c5366fd44d13f2678bba563c9581c6bacfde2b82a8dd49e33f2a2", + "digest": "1ab8f6c699e144aeaa39f88c8392a5a5053dcf58249a7ff561ac3fe343b15ede", "methods": { "verifyEcdsa": { - "rows": 38823, - "digest": "65c6f9efe1069f73adf3a842398f95d2" + "rows": 38830, + "digest": "513f0fcca515c10ba593720f7d62aedb" } }, "verificationKey": { - "data": "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", - "hash": "2818165315298159349200200610054154136732885996642834698461943280515655745528" + "data": "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", + "hash": "24873772351090152857267160919303237684044478603326712805175843693860023193162" } } } \ No newline at end of file From 57576fae8e828a778213abb9e23275b5ccd6ab71 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 30 Nov 2023 16:15:37 +0100 Subject: [PATCH 083/102] wip adapting ecdsa soundness to bool return --- src/lib/gadgets/elliptic-curve.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 203921bc9..888dfaab4 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -187,9 +187,11 @@ function verifyEcdsa( // this ^ already proves that R != 0 (part of ECDSA verification) // reduce R.x modulo the curve order - // note: we don't check that the result Rx is canonical, because Rx === r and r is an input: - // it's the callers responsibility to check that the signature is valid/unique in whatever way it makes sense for the application let Rx = ForeignField.mul(R.x, Field3.from(1n), Curve.order); + // we have to check that the result Rx is canonical, we then check if it _exactly_ equal to the input r. + // if we would allow non-canonical Rx, a prover could make verify() return false on a valid signature, by adding a multiple of `Curve.order` to Rx. + // TODO + return Provable.equal(Field3.provable, Rx, r); } From 5f5cd316a8418349b4d3195d028364a724158781 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 30 Nov 2023 16:22:19 +0100 Subject: [PATCH 084/102] cherry pick assertLessThan --- src/lib/gadgets/foreign-field.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/lib/gadgets/foreign-field.ts b/src/lib/gadgets/foreign-field.ts index d45a80033..4b92c5429 100644 --- a/src/lib/gadgets/foreign-field.ts +++ b/src/lib/gadgets/foreign-field.ts @@ -42,6 +42,9 @@ const ForeignField = { sub(x: Field3, y: Field3, f: bigint) { return sum([x, y], [-1n], f); }, + negate(x: Field3, f: bigint) { + return sum([Field3.from(0n), x], [-1n], f); + }, sum, Sum(x: Field3) { return new Sum(x); @@ -53,6 +56,21 @@ const ForeignField = { assertMul, assertAlmostFieldElements, + + assertLessThan(x: Field3, f: bigint) { + assert(f > 0n, 'assertLessThan: upper bound must be positive'); + + // constant case + if (Field3.isConstant(x)) { + assert(Field3.toBigint(x) < f, 'assertLessThan: got x >= f'); + return; + } + // provable case + // we can just use negation `(f - 1) - x`. because the result is range-checked, it proves that x < f: + // `f - 1 - x \in [0, 2^3l) => x <= x + (f - 1 - x) = f - 1 < f` + // (note: ffadd can't add higher multiples of (f - 1). it must always use an overflow of -1, except for x = 0 or 1) + ForeignField.negate(x, f - 1n); + }, }; /** From af6de47cb7203344de9954a2c0289eac6c6fcfb6 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 30 Nov 2023 16:28:35 +0100 Subject: [PATCH 085/102] finish adapting ecdsa final step --- src/lib/gadgets/elliptic-curve.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 888dfaab4..ea64483b6 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -188,9 +188,10 @@ function verifyEcdsa( // reduce R.x modulo the curve order let Rx = ForeignField.mul(R.x, Field3.from(1n), Curve.order); - // we have to check that the result Rx is canonical, we then check if it _exactly_ equal to the input r. - // if we would allow non-canonical Rx, a prover could make verify() return false on a valid signature, by adding a multiple of `Curve.order` to Rx. - // TODO + + // we have to prove that Rx is canonical, because we check signature validity based on whether Rx _exactly_ equals the input r. + // if we allowed non-canonical Rx, the prover could make verify() return false on a valid signature, by adding a multiple of `Curve.order` to Rx. + ForeignField.assertLessThan(Rx, Curve.order); return Provable.equal(Field3.provable, Rx, r); } From c193bd7e2fd0ed17739c1e05243698f105300ba4 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 30 Nov 2023 16:29:57 +0100 Subject: [PATCH 086/102] update vk --- tests/vk-regression/vk-regression.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/vk-regression/vk-regression.json b/tests/vk-regression/vk-regression.json index e2899eff8..6044f0bcf 100644 --- a/tests/vk-regression/vk-regression.json +++ b/tests/vk-regression/vk-regression.json @@ -203,16 +203,16 @@ } }, "ecdsa": { - "digest": "1ab8f6c699e144aeaa39f88c8392a5a5053dcf58249a7ff561ac3fe343b15ede", + "digest": "339463a449bf05b95a8c33f61cfe8ce434517139ca76d46acd8156fa148fa17d", "methods": { "verifyEcdsa": { - "rows": 38830, - "digest": "513f0fcca515c10ba593720f7d62aedb" + "rows": 38836, + "digest": "1d54d1addf7630e3eb226959de232cc3" } }, "verificationKey": { - "data": "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", - "hash": "24873772351090152857267160919303237684044478603326712805175843693860023193162" + "data": "AAAdmtvKeZvyx7UyPW6rIhb96GnTZEEywf8pGpbkt+QXNIm7oWxIDWYa4EWTiadEqzk8WXg3wiZmbXWcqBQU+uIoTiBnYTRcd7RsaAjbdbIbQJ9EuopFRFewZRx9qeQeEibNeMRcRMP4LdfS3AQRxhFZzN4HFa4MbtGs+Aja820cI9VFULH2/7BvD6JjpVWjVLvvo6zhO3S5axqfDh7QqtkPo3TLpand9OVvMHhTVlz/AV7rus5E/+0cv50MaEJ/wBfUh5XNLAlGgVi7FfWR6p9P72AAymyD3lUdecJyZmCREiVgPrTdFppkp45TefJWNTySkV9c5YzpNxQoXedZDvYP/5s4KBkfIeK+zB2yJC9eZ1ZDYfM88shGDYxmBtur9AkQ49QGquR+kYUI0lpXtuNMG+ZRy0FRJ8ci/TE+PIPIFnSiGcSOA3YM2G171LYf89abU2QUoQRHSP3PmmAOy/8CoRLVro7Nl6z/Ou0oZzX7RjOEo//LBqcSWa2S9X8TQz0R3uivbovTdq0rrba56SbEnK6LWItmBc6CubYWL7UzDbD3RZM6iRz1hqTHDzDz7UIWOzHgLqW9rjnZllQCyfsSAGIC/c5mlvJEtw3GmFSOhllzNMb5rze5XPBrZhVM6zUgjHCxrGo9kh7sUu/CJtpp5k56fIqgmrAEIdVJ3IloJAUgKzcNZhhPW5VfbcSYDpx5nVaU5pTEFl+2+RlcuhBpG1ksAWbD64AUKDjdyTWIC5Wn68AagPtG65V13eFS5LgkSfVtNXxGodg7SdP4AJmXpBgZfzMg4RW6Qje5ZFfrwRzoHPo0y7nO1hkaNLGV3Wvd3/pYiebXvyo+DdTZmaMbJpJaGSCysnovOrVUIpcn4h1hvA12jztQFQcbNHoVeZgslPxA54y9ynjhN7VZfT8lNXXIrRCpmPaxZW6Bw6Op/g6P1Y8pKZHixBy1UrxqWGI+49oRtRFGw9CWS21EekuBFeu9RKI6yZLDiyRC2b3koFG+Kp6oq5Ej6Q8uargE09AgtRrSDSqUoCxu0FXG8VDNFSk+wPBfgTjvTmC9gbTjPgfr95gk0HAAeyDFGWwKsDvU4bkqsacYCSWVZ7OFcx2LDV46ddCU9VJ1JmYsYa+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": "27986645693165294918748566661718640959750969878894001222795403177870132383423" } } } \ No newline at end of file From 66a9dc138689c83f7a49f7b0f6f027606a807f7d Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 30 Nov 2023 17:10:54 +0100 Subject: [PATCH 087/102] document `config` --- src/lib/gadgets/elliptic-curve.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index ea64483b6..8d45b6e42 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -140,6 +140,22 @@ function double(p1: Point, f: bigint) { return { x: x3, y: y3 }; } +/** + * Verify an ECDSA signature. + * + * Details about the `config` parameter: + * - For both the generator point `G` and public key `P`, `config` allows you to specify: + * - the `windowSize` which is used in scalar multiplication for this point. + * flexibility is good because the optimal window size is different for constant and non-constant points. + * empirically, `windowSize=4` for constants and 3 for variables leads to the fewest constraints. + * our defaults reflect that the generator is always constant and the public key is variable in typical applications. + * - a table of multiples of those points, of length `2^windowSize`, which is used in the scalar multiplication gadget to speed up the computation. + * if these are not provided, they are computed on the fly. + * for the constant G, computing multiples costs no constraints, so passing them in makes no real difference. + * for variable public key, there is a possible use case: if the public key is a public input, then its multiples could also be. + * in that case, passing them in would avoid computing them in-circuit and save a few constraints. + * - The initial aggregator `ia`, see {@link initialAggregator}. By default, `ia` is computed deterministically on the fly. + */ function verifyEcdsa( Curve: CurveAffine, signature: Ecdsa.Signature, From 1ad26021b39076e229b02e936a8fae8b1492ea40 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 1 Dec 2023 13:30:30 +0100 Subject: [PATCH 088/102] foreign field: split assert non zero into function --- src/lib/gadgets/foreign-field.ts | 34 +++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/lib/gadgets/foreign-field.ts b/src/lib/gadgets/foreign-field.ts index 4b92c5429..20b9b3a24 100644 --- a/src/lib/gadgets/foreign-field.ts +++ b/src/lib/gadgets/foreign-field.ts @@ -71,6 +71,30 @@ const ForeignField = { // (note: ffadd can't add higher multiples of (f - 1). it must always use an overflow of -1, except for x = 0 or 1) ForeignField.negate(x, f - 1n); }, + + /** + * prove that x != 0 mod f + * + * assumes that x is almost reduced mod f, so we know that x can at most be 0 or f, but not 2f, 3f,... + */ + assertNonZero(x: Field3, f: bigint) { + // constant case + if (Field3.isConstant(x)) { + assert(Field3.toBigint(x) !== 0n, 'assertNonZero: got x = 0'); + return; + } + // provable case + // check that x != 0 and x != f + let x01 = toVar(x[0].add(x[1].mul(1n << l))); + + // (x01, x2) != (0, 0) + x01.equals(0n).and(x[2].equals(0n)).assertFalse(); + // (x01, x2) != (f01, f2) + x01 + .equals(f & l2Mask) + .and(x[2].equals(f >> l2)) + .assertFalse(); + }, }; /** @@ -217,16 +241,8 @@ function divide( multiRangeCheck([z2Bound, Field.from(0n), Field.from(0n)]); if (!allowZeroOverZero) { - // assert that y != 0 mod f by checking that it doesn't equal 0 or f - // this works because we assume y[2] <= f2 - // TODO is this the most efficient way? - let y01 = y[0].add(y[1].mul(1n << l)); - y01.equals(0n).and(y[2].equals(0n)).assertFalse(); - let [f0, f1, f2] = split(f); - let f01 = combine2([f0, f1]); - y01.equals(f01).and(y[2].equals(f2)).assertFalse(); + ForeignField.assertNonZero(y, f); } - return z; } From 1c90b526cece020cc9ff795f4d61ac1542aba4fe Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 1 Dec 2023 13:46:03 +0100 Subject: [PATCH 089/102] assertNotEquals -> equals --- src/lib/gadgets/foreign-field.ts | 34 +++++++++++++++++++------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/lib/gadgets/foreign-field.ts b/src/lib/gadgets/foreign-field.ts index 20b9b3a24..a1583fe98 100644 --- a/src/lib/gadgets/foreign-field.ts +++ b/src/lib/gadgets/foreign-field.ts @@ -6,6 +6,7 @@ import { mod, } from '../../bindings/crypto/finite_field.js'; import { provableTuple } from '../../bindings/lib/provable-snarky.js'; +import { Bool } from '../bool.js'; import { Unconstrained } from '../circuit_value.js'; import { Field } from '../field.js'; import { Gates, foreignFieldAdd } from '../gates.js'; @@ -73,27 +74,32 @@ const ForeignField = { }, /** - * prove that x != 0 mod f + * check whether x = c mod f * - * assumes that x is almost reduced mod f, so we know that x can at most be 0 or f, but not 2f, 3f,... + * c is a constant, and we require c in [0, f) + * + * assumes that x is almost reduced mod f, so we know that x might be c or c + f, but not c + 2f, c + 3f, ... */ - assertNonZero(x: Field3, f: bigint) { + equals(x: Field3, c: bigint, f: bigint) { + assert(c >= 0n && c < f, 'equals: c must be in [0, f)'); + // constant case if (Field3.isConstant(x)) { - assert(Field3.toBigint(x) !== 0n, 'assertNonZero: got x = 0'); - return; + return new Bool(mod(Field3.toBigint(x), f) === c); } + // provable case - // check that x != 0 and x != f + // check whether x = 0 or x = f let x01 = toVar(x[0].add(x[1].mul(1n << l))); + let [c01, c2] = [c & l2Mask, c >> l2]; + let [cPlusF01, cPlusF2] = [(c + f) & l2Mask, (c + f) >> l2]; + + // (x01, x2) = (c01, c2) + let isC = x01.equals(c01).and(x[2].equals(c2)); + // (x01, x2) = (cPlusF01, cPlusF2) + let isCPlusF = x01.equals(cPlusF01).and(x[2].equals(cPlusF2)); - // (x01, x2) != (0, 0) - x01.equals(0n).and(x[2].equals(0n)).assertFalse(); - // (x01, x2) != (f01, f2) - x01 - .equals(f & l2Mask) - .and(x[2].equals(f >> l2)) - .assertFalse(); + return isC.or(isCPlusF); }, }; @@ -241,7 +247,7 @@ function divide( multiRangeCheck([z2Bound, Field.from(0n), Field.from(0n)]); if (!allowZeroOverZero) { - ForeignField.assertNonZero(y, f); + ForeignField.equals(y, 0n, f).assertFalse(); } return z; } From 7886bef437ad96679b226ad646091d71da1e324f Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 1 Dec 2023 13:49:34 +0100 Subject: [PATCH 090/102] fix final check in scalar mul --- src/lib/gadgets/elliptic-curve.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 8d45b6e42..c658039e0 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -298,7 +298,13 @@ function multiScalarMul( // 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)); - Provable.equal(Point.provable, sum, Point.from(iaFinal)).assertFalse(); + let xEquals = ForeignField.equals(sum.x, iaFinal.x, Curve.modulus); + let yEquals = ForeignField.equals(sum.y, iaFinal.y, Curve.modulus); + let isZero = xEquals.and(yEquals); + + // TODO: return isZero instead of asserting here + isZero.assertFalse(); + sum = add(sum, Point.from(Curve.negate(iaFinal)), Curve.modulus); return sum; From ba0c36160b195d5063bee0a4c87a031e9fecec76 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 1 Dec 2023 14:16:18 +0100 Subject: [PATCH 091/102] split out equals --- src/lib/gadgets/elliptic-curve.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index c658039e0..20155c7e2 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -140,6 +140,14 @@ function double(p1: Point, f: bigint) { return { x: x3, y: y3 }; } +// 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); + return xEquals.and(yEquals); +} + /** * Verify an ECDSA signature. * @@ -298,11 +306,7 @@ function multiScalarMul( // 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 xEquals = ForeignField.equals(sum.x, iaFinal.x, Curve.modulus); - let yEquals = ForeignField.equals(sum.y, iaFinal.y, Curve.modulus); - let isZero = xEquals.and(yEquals); - - // TODO: return isZero instead of asserting here + let isZero = equals(sum, iaFinal, Curve.modulus); isZero.assertFalse(); sum = add(sum, Point.from(Curve.negate(iaFinal)), Curve.modulus); From dba58f372fddbcd610d39a430eedbde07ed9167a Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 1 Dec 2023 14:29:12 +0100 Subject: [PATCH 092/102] vk update --- tests/vk-regression/vk-regression.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/vk-regression/vk-regression.json b/tests/vk-regression/vk-regression.json index 6044f0bcf..0297b30a0 100644 --- a/tests/vk-regression/vk-regression.json +++ b/tests/vk-regression/vk-regression.json @@ -203,16 +203,16 @@ } }, "ecdsa": { - "digest": "339463a449bf05b95a8c33f61cfe8ce434517139ca76d46acd8156fa148fa17d", + "digest": "6e4078c6df944db119dc1656eb68e6c272c3f2e9cab6746913759537cbfcfa9", "methods": { "verifyEcdsa": { - "rows": 38836, - "digest": "1d54d1addf7630e3eb226959de232cc3" + "rows": 38846, + "digest": "892b0a1fad0f13d92ba6099cd54e6780" } }, "verificationKey": { - "data": "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", - "hash": "27986645693165294918748566661718640959750969878894001222795403177870132383423" + "data": "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", + "hash": "21152043351405736424630704375244880683906889913335338686836578165782029001213" } } } \ No newline at end of file From d0b09b9c1d86061a3f6bea97b9086a4caf33d792 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 4 Dec 2023 12:29:15 +0100 Subject: [PATCH 093/102] apply small review suggestions --- src/lib/gadgets/basic.ts | 4 ++-- src/lib/gadgets/ecdsa.unit-test.ts | 30 ++++++++---------------------- src/lib/gadgets/elliptic-curve.ts | 4 ++-- 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/src/lib/gadgets/basic.ts b/src/lib/gadgets/basic.ts index 6d534ac37..dfff5f1de 100644 --- a/src/lib/gadgets/basic.ts +++ b/src/lib/gadgets/basic.ts @@ -24,7 +24,7 @@ function arrayGet(array: Field[], index: Field) { // witness result let a = existsOne(() => array[Number(i.toBigInt())].toBigInt()); - // we prove a === array[j] + zj*(i - j) for some zj, for all j. + // we prove a === array[j] + z[j]*(i - j) for some z[j], for all j. // setting j = i, this implies a === array[i] // thanks to our assumption that the index i is within bounds, we know that j = i for some j let n = array.length; @@ -36,7 +36,7 @@ function arrayGet(array: Field[], index: Field) { ); return zj ?? 0n; }); - // prove that zj*(i - j) === a - array[j] + // prove that z[j]*(i - j) === a - array[j] // TODO abstract this logic into a general-purpose assertMul() gadget, // which is able to use the constant coefficient // (snarky's assert_r1cs somehow leads to much more constraints than this) diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index 9cbcbe4e7..33c1f8ce6 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -33,7 +33,8 @@ for (let Curve of curves) { let scalar = foreignField(Curve.Scalar); let privateKey = uniformForeignField(Curve.Scalar); - let pseudoSignature = record({ + // correct signature shape, but independently random components, which will never form a valid signature + let badSignature = record({ signature: record({ r: scalar, s: scalar }), msg: scalar, publicKey: record({ x: field, y: field }), @@ -42,7 +43,7 @@ for (let Curve of curves) { let signatureInputs = record({ privateKey, msg: scalar }); let signature = map( - { from: signatureInputs, to: pseudoSignature }, + { from: signatureInputs, to: badSignature }, ({ privateKey, msg }) => { let publicKey = Curve.scale(Curve.one, privateKey); let signature = Ecdsa.sign(Curve, msg, privateKey); @@ -63,14 +64,14 @@ for (let Curve of curves) { ); // negative test - equivalentProvable({ from: [pseudoSignature], to: bool })( + equivalentProvable({ from: [badSignature], to: bool })( () => false, verify, 'invalid signature fails' ); // test against constant implementation, with both invalid and valid signatures - equivalentProvable({ from: [oneOf(signature, pseudoSignature)], to: bool })( + equivalentProvable({ from: [oneOf(signature, badSignature)], to: bool })( ({ signature, publicKey, msg }) => { return verifyEcdsaConstant(Curve, signature, msg, publicKey); }, @@ -102,20 +103,6 @@ let program = ZkProgram({ name: 'ecdsa', publicOutput: Bool, methods: { - scale: { - privateInputs: [], - method() { - let G = Point.from(Secp256k1.one); - let P = Provable.witness(Point.provable, () => publicKey); - let R = EllipticCurve.multiScalarMul( - Secp256k1, - [signature.s, signature.r], - [G, P], - [config.G, config.P] - ); - return new Bool(true); - }, - }, ecdsa: { privateInputs: [], method() { @@ -137,18 +124,17 @@ let program = ZkProgram({ }, }, }); -let main = program.rawMethods.ecdsa; console.time('ecdsa verify (constant)'); -main(); +program.rawMethods.ecdsa(); console.timeEnd('ecdsa verify (constant)'); console.time('ecdsa verify (witness gen / check)'); -Provable.runAndCheck(main); +Provable.runAndCheck(program.rawMethods.ecdsa); console.timeEnd('ecdsa verify (witness gen / check)'); console.time('ecdsa verify (build constraint system)'); -let cs = Provable.constraintSystem(main); +let cs = program.analyzeMethods().ecdsa; console.timeEnd('ecdsa verify (build constraint system)'); let gateTypes: Record = {}; diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 20155c7e2..8077248e8 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -154,7 +154,7 @@ function equals(p1: Point, p2: point, f: bigint) { * Details about the `config` parameter: * - For both the generator point `G` and public key `P`, `config` allows you to specify: * - the `windowSize` which is used in scalar multiplication for this point. - * flexibility is good because the optimal window size is different for constant and non-constant points. + * this flexibility is good because the optimal window size is different for constant and non-constant points. * empirically, `windowSize=4` for constants and 3 for variables leads to the fewest constraints. * our defaults reflect that the generator is always constant and the public key is variable in typical applications. * - a table of multiples of those points, of length `2^windowSize`, which is used in the scalar multiplication gadget to speed up the computation. @@ -475,7 +475,7 @@ function sliceField( let chunks = []; let sum = Field.from(0n); - // if there's a leftover chunk from a previous slizeField() call, we complete it + // if there's a leftover chunk from a previous sliceField() call, we complete it if (leftover !== undefined) { let { chunks: previous, leftoverSize: size } = leftover; let remainingChunk = Field.from(0n); From f68c1487a634fe0f1858aa2a1b0a5430c7ead34d Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 4 Dec 2023 12:41:42 +0100 Subject: [PATCH 094/102] support for a!=0 in double gadget --- src/lib/gadgets/elliptic-curve.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 8077248e8..49b6b7ef5 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -92,7 +92,7 @@ function add(p1: Point, p2: Point, f: bigint) { return { x: x3, y: y3 }; } -function double(p1: Point, f: bigint) { +function double(p1: Point, f: bigint, a: bigint) { let { x: x1, y: y1 } = p1; // constant case @@ -122,11 +122,11 @@ function double(p1: Point, f: bigint) { // x1^2 = x1x1 let x1x1 = ForeignField.mul(x1, x1, f); - // 2*y1*m = 3*x1x1 - // TODO this assumes the curve has a == 0 + // 2*y1*m = 3*x1x1 + a let y1Times2 = ForeignField.Sum(y1).add(y1); - let x1x1Times3 = ForeignField.Sum(x1x1).add(x1x1).add(x1x1); - ForeignField.assertMul(y1Times2, m, x1x1Times3, f); + let x1x1Times3PlusA = ForeignField.Sum(x1x1).add(x1x1).add(x1x1); + if (a !== 0n) x1x1Times3PlusA = x1x1Times3PlusA.add(Field3.from(a)); + ForeignField.assertMul(y1Times2, m, x1x1Times3PlusA, f); // m^2 = 2*x1 + x3 let xSum = ForeignField.Sum(x1).add(x1).add(x3); @@ -300,7 +300,7 @@ 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); + sum = double(sum, Curve.modulus, Curve.a); } // the sum is now 2^(b-1)*IA + sum_i s_i*P_i @@ -374,7 +374,7 @@ function getPointTable( table = [Point.from(Curve.zero), P]; if (n === 2) return table; - let Pi = double(P, Curve.modulus); + let Pi = double(P, Curve.modulus, Curve.a); table.push(Pi); for (let i = 3; i < n; i++) { Pi = add(Pi, P, Curve.modulus); From 124d9180a8b201865ece92379ac48d799bbb6b44 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 4 Dec 2023 12:52:33 +0100 Subject: [PATCH 095/102] move ff equals gadget and handle an edge case --- src/lib/gadgets/foreign-field.ts | 66 ++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/src/lib/gadgets/foreign-field.ts b/src/lib/gadgets/foreign-field.ts index a1583fe98..42899a865 100644 --- a/src/lib/gadgets/foreign-field.ts +++ b/src/lib/gadgets/foreign-field.ts @@ -73,34 +73,7 @@ const ForeignField = { ForeignField.negate(x, f - 1n); }, - /** - * check whether x = c mod f - * - * c is a constant, and we require c in [0, f) - * - * assumes that x is almost reduced mod f, so we know that x might be c or c + f, but not c + 2f, c + 3f, ... - */ - equals(x: Field3, c: bigint, f: bigint) { - assert(c >= 0n && c < f, 'equals: c must be in [0, f)'); - - // constant case - if (Field3.isConstant(x)) { - return new Bool(mod(Field3.toBigint(x), f) === c); - } - - // provable case - // check whether x = 0 or x = f - let x01 = toVar(x[0].add(x[1].mul(1n << l))); - let [c01, c2] = [c & l2Mask, c >> l2]; - let [cPlusF01, cPlusF2] = [(c + f) & l2Mask, (c + f) >> l2]; - - // (x01, x2) = (c01, c2) - let isC = x01.equals(c01).and(x[2].equals(c2)); - // (x01, x2) = (cPlusF01, cPlusF2) - let isCPlusF = x01.equals(cPlusF01).and(x[2].equals(cPlusF2)); - - return isC.or(isCPlusF); - }, + equals, }; /** @@ -410,6 +383,43 @@ function assertAlmostFieldElements(xs: Field3[], f: bigint) { } } +/** + * check whether x = c mod f + * + * c is a constant, and we require c in [0, f) + * + * assumes that x is almost reduced mod f, so we know that x might be c or c + f, but not c + 2f, c + 3f, ... + */ +function equals(x: Field3, c: bigint, f: bigint) { + assert(c >= 0n && c < f, 'equals: c must be in [0, f)'); + + // constant case + if (Field3.isConstant(x)) { + return new Bool(mod(Field3.toBigint(x), f) === c); + } + + // provable case + if (f >= 1n << l2) { + // check whether x = 0 or x = f + let x01 = toVar(x[0].add(x[1].mul(1n << l))); + let [c01, c2] = [c & l2Mask, c >> l2]; + let [cPlusF01, cPlusF2] = [(c + f) & l2Mask, (c + f) >> l2]; + + // (x01, x2) = (c01, c2) + let isC = x01.equals(c01).and(x[2].equals(c2)); + // (x01, x2) = (cPlusF01, cPlusF2) + let isCPlusF = x01.equals(cPlusF01).and(x[2].equals(cPlusF2)); + + return isC.or(isCPlusF); + } else { + // if f < 2^2l, the approach above doesn't work (we don't know from x[2] = 0 that x < 2f), + // so in that case we assert that x < f and then check whether it's equal to c + ForeignField.assertLessThan(x, f); + let x012 = toVar(x[0].add(x[1].mul(1n << l)).add(x[2].mul(1n << l2))); + return x012.equals(c); + } +} + const Field3 = { /** * Turn a bigint into a 3-tuple of Fields From 2495901b57c51958423f0694a514835590bc0f6f Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 4 Dec 2023 12:56:33 +0100 Subject: [PATCH 096/102] rename assertAlmostFieldElements --- src/lib/gadgets/elliptic-curve.ts | 4 +-- src/lib/gadgets/foreign-field.ts | 4 +-- src/lib/gadgets/foreign-field.unit-test.ts | 4 +-- src/lib/gadgets/gadgets.ts | 40 +++++++++++----------- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 49b6b7ef5..c1ff87550 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -73,7 +73,7 @@ function add(p1: Point, p2: Point, f: bigint) { let m: Field3 = [m0, m1, m2]; let x3: Field3 = [x30, x31, x32]; let y3: Field3 = [y30, y31, y32]; - ForeignField.assertAlmostFieldElements([m, x3, y3], f); + ForeignField.assertAlmostReduced([m, x3, y3], f); // (x1 - x2)*m = y1 - y2 let deltaX = ForeignField.Sum(x1).sub(x2); @@ -117,7 +117,7 @@ function double(p1: Point, f: bigint, a: bigint) { let m: Field3 = [m0, m1, m2]; let x3: Field3 = [x30, x31, x32]; let y3: Field3 = [y30, y31, y32]; - ForeignField.assertAlmostFieldElements([m, x3, y3], f); + ForeignField.assertAlmostReduced([m, x3, y3], f); // x1^2 = x1x1 let x1x1 = ForeignField.mul(x1, x1, f); diff --git a/src/lib/gadgets/foreign-field.ts b/src/lib/gadgets/foreign-field.ts index 42899a865..f1929df99 100644 --- a/src/lib/gadgets/foreign-field.ts +++ b/src/lib/gadgets/foreign-field.ts @@ -56,7 +56,7 @@ const ForeignField = { div: divide, assertMul, - assertAlmostFieldElements, + assertAlmostReduced, assertLessThan(x: Field3, f: bigint) { assert(f > 0n, 'assertLessThan: upper bound must be positive'); @@ -363,7 +363,7 @@ function weakBound(x: Field, f: bigint) { * Apply range checks and weak bounds checks to a list of Field3s. * Optimal if the list length is a multiple of 3. */ -function assertAlmostFieldElements(xs: Field3[], f: bigint) { +function assertAlmostReduced(xs: Field3[], f: bigint) { let bounds: Field[] = []; for (let x of xs) { diff --git a/src/lib/gadgets/foreign-field.unit-test.ts b/src/lib/gadgets/foreign-field.unit-test.ts index 2e9c4732f..f722a3938 100644 --- a/src/lib/gadgets/foreign-field.unit-test.ts +++ b/src/lib/gadgets/foreign-field.unit-test.ts @@ -109,7 +109,7 @@ for (let F of fields) { equivalent({ from: [big264], to: unit })( (x) => assertWeakBound(x, F.modulus), - (x) => ForeignField.assertAlmostFieldElements([x], F.modulus) + (x) => ForeignField.assertAlmostReduced([x], F.modulus) ); // sumchain of 5 @@ -158,7 +158,7 @@ let ffProgram = ZkProgram({ mulWithBoundsCheck: { privateInputs: [Field3.provable, Field3.provable], method(x, y) { - ForeignField.assertAlmostFieldElements([x, y], F.modulus); + ForeignField.assertAlmostReduced([x, y], F.modulus); return ForeignField.mul(x, y, F.modulus); }, }, diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 6f9f24b9a..ce3a829a6 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -372,7 +372,7 @@ const Gadgets = { /** * Foreign field subtraction: `x - y mod f` * - * See {@link ForeignField.add} for assumptions and usage examples. + * See {@link Gadgets.ForeignField.add} for assumptions and usage examples. * * @throws fails if `x - y < -f`, where the result cannot be brought back to a positive number by adding `f` once. */ @@ -390,7 +390,7 @@ const Gadgets = { * **Note**: For 3 or more inputs, `sum()` uses fewer constraints than a sequence of `add()` and `sub()` calls, * because we can avoid range checks on intermediate results. * - * See {@link ForeignField.add} for assumptions on inputs. + * See {@link Gadgets.ForeignField.add} for assumptions on inputs. * * @example * ```ts @@ -424,7 +424,7 @@ const Gadgets = { * To do this, we use an 88-bit range check on `2^88 - x[2] - (f[2] + 1)`, and same for y. * The implication is that x and y are _almost_ reduced modulo f. * - * All of the above assumptions are checked by {@link ForeignField.assertAlmostFieldElements}. + * All of the above assumptions are checked by {@link Gadgets.ForeignField.assertAlmostReduced}. * * **Warning**: This gadget does not add the extra bound check on the result. * So, to use the result in another foreign field multiplication, you have to add the bound check on it yourself, again. @@ -438,7 +438,7 @@ const Gadgets = { * let y = Provable.witness(Field3.provable, () => Field3.from(f - 2n)); * * // range check x, y and prove additional bounds x[2] <= f[2] - * ForeignField.assertAlmostFieldElements([x, y], f); + * ForeignField.assertAlmostReduced([x, y], f); * * // compute x * y mod f * let z = ForeignField.mul(x, y, f); @@ -453,7 +453,7 @@ const Gadgets = { /** * Foreign field inverse: `x^(-1) mod f` * - * See {@link ForeignField.mul} for assumptions on inputs and usage examples. + * See {@link Gadgets.ForeignField.mul} for assumptions on inputs and usage examples. * * This gadget adds an extra bound check on the result, so it can be used directly in another foreign field multiplication. */ @@ -464,11 +464,11 @@ const Gadgets = { /** * Foreign field division: `x * y^(-1) mod f` * - * See {@link ForeignField.mul} for assumptions on inputs and usage examples. + * See {@link Gadgets.ForeignField.mul} for assumptions on inputs and usage examples. * * This gadget adds an extra bound check on the result, so it can be used directly in another foreign field multiplication. * - * @throws Different than {@link ForeignField.mul}, this fails on unreduced input `x`, because it checks that `x === (x/y)*y` and the right side will be reduced. + * @throws Different than {@link Gadgets.ForeignField.mul}, this fails on unreduced input `x`, because it checks that `x === (x/y)*y` and the right side will be reduced. */ div(x: Field3, y: Field3, f: bigint) { return ForeignField.div(x, y, f); @@ -477,13 +477,13 @@ const Gadgets = { /** * Optimized multiplication of sums in a foreign field, for example: `(x - y)*z = a + b + c mod f` * - * Note: This is much more efficient than using {@link ForeignField.add} and {@link ForeignField.sub} separately to - * compute the multiplication inputs and outputs, and then using {@link ForeignField.mul} to constrain the result. + * Note: This is much more efficient than using {@link Gadgets.ForeignField.add} and {@link Gadgets.ForeignField.sub} separately to + * compute the multiplication inputs and outputs, and then using {@link Gadgets.ForeignField.mul} to constrain the result. * - * The sums passed into this gadgets are "lazy sums" created with {@link ForeignField.Sum}. + * The sums passed into this gadgets are "lazy sums" created with {@link Gadgets.ForeignField.Sum}. * You can also pass in plain {@link Field3} elements. * - * **Assumptions**: The assumptions on the _summands_ are analogous to the assumptions described in {@link ForeignField.mul}: + * **Assumptions**: The assumptions on the _summands_ are analogous to the assumptions described in {@link Gadgets.ForeignField.mul}: * - each summand's limbs are in the range [0, 2^88) * - summands that are part of a multiplication input satisfy `x[2] <= f[2]` * @@ -495,7 +495,7 @@ const Gadgets = { * @example * ```ts * // range-check x, y, z, a, b, c - * ForeignField.assertAlmostFieldElements([x, y, z], f); + * ForeignField.assertAlmostReduced([x, y, z], f); * Gadgets.multiRangeCheck(a); * Gadgets.multiRangeCheck(b); * Gadgets.multiRangeCheck(c); @@ -513,7 +513,7 @@ const Gadgets = { }, /** - * Lazy sum of {@link Field3} elements, which can be used as input to {@link ForeignField.assertMul}. + * Lazy sum of {@link Field3} elements, which can be used as input to {@link Gadgets.ForeignField.assertMul}. */ Sum(x: Field3) { return ForeignField.Sum(x); @@ -521,7 +521,7 @@ const Gadgets = { /** * Prove that each of the given {@link Field3} elements is "almost" reduced modulo f, - * i.e., satisfies the assumptions required by {@link ForeignField.mul} and other gadgets: + * i.e., satisfies the assumptions required by {@link Gadgets.ForeignField.mul} and other gadgets: * - each limb is in the range [0, 2^88) * - the most significant limb is less or equal than the modulus, x[2] <= f[2] * @@ -535,18 +535,18 @@ const Gadgets = { * let y = Provable.witness(Field3.provable, () => Field3.from(5n)); * let z = Provable.witness(Field3.provable, () => Field3.from(10n)); * - * ForeignField.assertAlmostFieldElements([x, y, z], f); + * ForeignField.assertAlmostReduced([x, y, z], f); * * // now we can use x, y, z as inputs to foreign field multiplication * let xy = ForeignField.mul(x, y, f); * let xyz = ForeignField.mul(xy, z, f); * * // since xy is an input to another multiplication, we need to prove that it is almost reduced again! - * ForeignField.assertAlmostFieldElements([xy], f); // TODO: would be more efficient to batch this with 2 other elements + * ForeignField.assertAlmostReduced([xy], f); // TODO: would be more efficient to batch this with 2 other elements * ``` */ - assertAlmostFieldElements(xs: Field3[], f: bigint) { - ForeignField.assertAlmostFieldElements(xs, f); + assertAlmostReduced(xs: Field3[], f: bigint) { + ForeignField.assertAlmostReduced(xs, f); }, }, @@ -566,7 +566,7 @@ const Gadgets = { * const Curve = Crypto.createCurve(Crypto.CurveParams.Secp256k1); * * // assert that message hash and signature are valid scalar field elements - * Gadgets.ForeignField.assertAlmostFieldElements( + * Gadgets.ForeignField.assertAlmostReduced( * [signature.r, signature.s, msgHash], * Curve.order * ); @@ -620,7 +620,7 @@ export namespace Gadgets { export namespace ForeignField { /** - * Lazy sum of {@link Field3} elements, which can be used as input to {@link ForeignField.assertMul}. + * Lazy sum of {@link Field3} elements, which can be used as input to {@link Gadgets.ForeignField.assertMul}. */ export type Sum = Sum_; } From 6fc4e86f0dadc84af6cb87cef57edc2695e1b0db Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 4 Dec 2023 16:39:19 +0100 Subject: [PATCH 097/102] fixup vk test --- src/examples/zkprogram/ecdsa/ecdsa.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/examples/zkprogram/ecdsa/ecdsa.ts b/src/examples/zkprogram/ecdsa/ecdsa.ts index 183ec3e7f..810cb33e5 100644 --- a/src/examples/zkprogram/ecdsa/ecdsa.ts +++ b/src/examples/zkprogram/ecdsa/ecdsa.ts @@ -28,7 +28,7 @@ const ecdsaProgram = ZkProgram({ msgHash: Gadgets.Field3 ) { // assert that private inputs are valid - ForeignField.assertAlmostFieldElements( + ForeignField.assertAlmostReduced( [signature.r, signature.s, msgHash], Secp256k1.order ); From 621d2ef1bde826398e5a92fe3e58ced6a73d43e9 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 4 Dec 2023 20:17:14 +0100 Subject: [PATCH 098/102] fixups --- src/lib/foreign-field.ts | 4 +-- src/lib/gadgets/foreign-field.ts | 2 +- src/lib/gadgets/gadgets.ts | 52 ++++++++++++++++---------------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/lib/foreign-field.ts b/src/lib/foreign-field.ts index d5ef7447b..8b705d620 100644 --- a/src/lib/foreign-field.ts +++ b/src/lib/foreign-field.ts @@ -157,7 +157,7 @@ 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.assertAlmostFieldElements([this.value], this.modulus, { + Gadgets.ForeignField.assertAlmostReduced([this.value], this.modulus, { skipMrc: true, }); return this.Constructor.AlmostReduced.unsafeFrom(this); @@ -172,7 +172,7 @@ class ForeignField { static assertAlmostReduced>( ...xs: T ): TupleMap { - Gadgets.ForeignField.assertAlmostFieldElements( + Gadgets.ForeignField.assertAlmostReduced( xs.map((x) => x.value), this.modulus, { skipMrc: true } diff --git a/src/lib/gadgets/foreign-field.ts b/src/lib/gadgets/foreign-field.ts index 46dc63043..1a8c0d878 100644 --- a/src/lib/gadgets/foreign-field.ts +++ b/src/lib/gadgets/foreign-field.ts @@ -10,7 +10,7 @@ import { Bool } from '../bool.js'; import { Unconstrained } from '../circuit_value.js'; import { Field } from '../field.js'; import { Gates, foreignFieldAdd } from '../gates.js'; -import { Tuple, TupleN, TupleN } from '../util/types.js'; +import { Tuple, TupleN } from '../util/types.js'; import { assertOneOf } from './basic.js'; import { assert, bitSlice, exists, toVar, toVars } from './common.js'; import { diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 4a2102bdc..79bdaf577 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -570,33 +570,33 @@ const Gadgets = { assertAlmostReduced(xs: Field3[], f: bigint, { skipMrc = false } = {}) { ForeignField.assertAlmostReduced(xs, f, skipMrc); }, - }, - /** - * Prove that x < f for any constant f < 2^264. - * - * If f is a finite field modulus, this means that the given field element is fully reduced modulo f. - * This is a stronger statement than {@link ForeignField.assertAlmostFieldElements} - * and also uses more constraints; it should not be needed in most use cases. - * - * **Note**: This assumes that the limbs of x are in the range [0, 2^88), in contrast to - * {@link ForeignField.assertAlmostFieldElements} which adds that check itself. - * - * @throws if x is greater or equal to f. - * - * @example - * ```ts - * let x = Provable.witness(Field3.provable, () => Field3.from(0x1235n)); - * - * // range check limbs of x - * Gadgets.multiRangeCheck(x); - * - * // prove that x is fully reduced mod f - * Gadgets.ForeignField.assertLessThan(x, f); - * ``` - */ - assertLessThan(x: Field3, f: bigint) { - ForeignField.assertLessThan(x, f); + /** + * Prove that x < f for any constant f < 2^264. + * + * If f is a finite field modulus, this means that the given field element is fully reduced modulo f. + * This is a stronger statement than {@link ForeignField.assertAlmostReduced} + * and also uses more constraints; it should not be needed in most use cases. + * + * **Note**: This assumes that the limbs of x are in the range [0, 2^88), in contrast to + * {@link ForeignField.assertAlmostReduced} which adds that check itself. + * + * @throws if x is greater or equal to f. + * + * @example + * ```ts + * let x = Provable.witness(Field3.provable, () => Field3.from(0x1235n)); + * + * // range check limbs of x + * Gadgets.multiRangeCheck(x); + * + * // prove that x is fully reduced mod f + * Gadgets.ForeignField.assertLessThan(x, f); + * ``` + */ + assertLessThan(x: Field3, f: bigint) { + ForeignField.assertLessThan(x, f); + }, }, /** From f51e5c057152087529c97e6367da22422ee85c53 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 6 Dec 2023 20:28:03 +0100 Subject: [PATCH 099/102] remove gadgets.ecdsa which ended up in a different final form --- src/examples/zkprogram/ecdsa/ecdsa.ts | 41 ---------------- src/examples/zkprogram/ecdsa/run.ts | 43 ----------------- src/lib/gadgets/ecdsa.unit-test.ts | 2 +- src/lib/gadgets/gadgets.ts | 67 --------------------------- 4 files changed, 1 insertion(+), 152 deletions(-) delete mode 100644 src/examples/zkprogram/ecdsa/ecdsa.ts delete mode 100644 src/examples/zkprogram/ecdsa/run.ts diff --git a/src/examples/zkprogram/ecdsa/ecdsa.ts b/src/examples/zkprogram/ecdsa/ecdsa.ts deleted file mode 100644 index 810cb33e5..000000000 --- a/src/examples/zkprogram/ecdsa/ecdsa.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Gadgets, ZkProgram, Struct, Crypto } from 'o1js'; - -export { ecdsaProgram, Point, Secp256k1 }; - -let { ForeignField, Field3, Ecdsa } = Gadgets; - -// TODO expose this as part of Gadgets.Curve - -class Point extends Struct({ x: Field3.provable, y: Field3.provable }) { - // point from bigints - static from({ x, y }: { x: bigint; y: bigint }) { - return new Point({ x: Field3.from(x), y: Field3.from(y) }); - } -} - -const Secp256k1 = Crypto.createCurve(Crypto.CurveParams.Secp256k1); - -const ecdsaProgram = ZkProgram({ - name: 'ecdsa', - publicInput: Point, - - methods: { - verifyEcdsa: { - privateInputs: [Ecdsa.Signature.provable, Field3.provable], - method( - publicKey: Point, - signature: Gadgets.Ecdsa.Signature, - msgHash: Gadgets.Field3 - ) { - // assert that private inputs are valid - ForeignField.assertAlmostReduced( - [signature.r, signature.s, msgHash], - Secp256k1.order - ); - - // verify signature - Ecdsa.verify(Secp256k1, signature, msgHash, publicKey); - }, - }, - }, -}); diff --git a/src/examples/zkprogram/ecdsa/run.ts b/src/examples/zkprogram/ecdsa/run.ts deleted file mode 100644 index b1d502c7e..000000000 --- a/src/examples/zkprogram/ecdsa/run.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Gadgets } from 'o1js'; -import { Point, Secp256k1, ecdsaProgram } from './ecdsa.js'; -import assert from 'assert'; - -// create an example ecdsa signature - -let privateKey = Secp256k1.Scalar.random(); -let publicKey = Secp256k1.scale(Secp256k1.one, privateKey); - -// TODO use an actual keccak hash -let messageHash = Secp256k1.Scalar.random(); - -let signature = Gadgets.Ecdsa.sign(Secp256k1, messageHash, privateKey); - -// 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)'); - -let gateTypes: Record = {}; -gateTypes['Total rows'] = cs.rows; -for (let gate of cs.gates) { - gateTypes[gate.type] ??= 0; - gateTypes[gate.type]++; -} -console.log(gateTypes); - -// 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( - Point.from(publicKey), - Gadgets.Ecdsa.Signature.from(signature), - Gadgets.Field3.from(messageHash) -); -console.timeEnd('ecdsa verify (prove)'); - -assert(await ecdsaProgram.verify(proof), 'proof verifies'); diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index 33c1f8ce6..b8f28764b 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -10,7 +10,7 @@ import { CurveParams } from '../../bindings/crypto/elliptic-curve-examples.js'; import { Provable } from '../provable.js'; import { ZkProgram } from '../proof_system.js'; import { assert } from './common.js'; -import { foreignField, throwError, uniformForeignField } from './test-utils.js'; +import { foreignField, uniformForeignField } from './test-utils.js'; import { Second, bool, diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index bc5a64fee..038646d03 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -10,9 +10,6 @@ import { import { not, rotate, xor, and, leftShift, rightShift } from './bitwise.js'; import { Field } from '../field.js'; import { ForeignField, Field3, Sum } from './foreign-field.js'; -import { Ecdsa, Point } from './elliptic-curve.js'; -import { CurveAffine } from '../../bindings/crypto/elliptic_curve.js'; -import { Crypto } from '../crypto.js'; export { Gadgets }; @@ -599,60 +596,6 @@ const Gadgets = { }, }, - /** - * ECDSA verification gadget and helper methods. - */ - Ecdsa: { - // TODO add an easy way to prove that the public key lies on the curve, and show in the example - /** - * Verify an ECDSA signature. - * - * **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. - * - * @example - * ```ts - * const Curve = Crypto.createCurve(Crypto.CurveParams.Secp256k1); - * - * // assert that message hash and signature are valid scalar field elements - * Gadgets.ForeignField.assertAlmostReduced( - * [signature.r, signature.s, msgHash], - * Curve.order - * ); - * - * // verify signature - * let isValid = Gadgets.Ecdsa.verify(Curve, signature, msgHash, publicKey); - * isValid.assertTrue(); - * ``` - */ - verify( - Curve: CurveAffine, - signature: Ecdsa.Signature, - msgHash: Field3, - publicKey: Point - ) { - Ecdsa.verify(Curve, signature, msgHash, publicKey); - }, - - /** - * Sign a message hash using ECDSA. - * - * _This method is not provable._ - */ - sign( - Curve: Crypto.Curve, - msgHash: bigint, - privateKey: bigint - ): Ecdsa.signature { - return Ecdsa.sign(Curve, msgHash, privateKey); - }, - - /** - * Non-provable helper methods for interacting with ECDSA signatures. - */ - Signature: Ecdsa.Signature, - }, - /** * Helper methods to interact with 3-limb vectors of Fields. * @@ -673,15 +616,5 @@ export namespace Gadgets { */ export type Sum = Sum_; } - - export namespace Ecdsa { - /** - * ECDSA signature consisting of two curve scalars. - */ - export type Signature = EcdsaSignature; - export type signature = ecdsaSignature; - } } type Sum_ = Sum; -type EcdsaSignature = Ecdsa.Signature; -type ecdsaSignature = Ecdsa.signature; From bbcfb17e2c5bd977b2bb2251ad838ed089d9f31e Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 6 Dec 2023 20:46:35 +0100 Subject: [PATCH 100/102] fix build:examples while we're here --- package.json | 2 +- src/build/copy-to-dist.js | 6 +++++- src/examples/api_exploration.ts | 2 +- src/examples/zkapps/local_events_zkapp.ts | 2 +- src/examples/zkapps/sudoku/sudoku.ts | 2 +- src/examples/zkapps/voting/member.ts | 4 ++-- src/examples/zkprogram/program-with-input.ts | 8 ++++---- src/examples/zkprogram/program.ts | 8 ++++---- src/lib/account_update.ts | 3 +++ 9 files changed, 22 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index b890fd3db..c2b9510c5 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "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": "rimraf ./dist/examples && npx tsc -p tsconfig.examples.json || exit 0", + "build:examples": "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", diff --git a/src/build/copy-to-dist.js b/src/build/copy-to-dist.js index 96bc96937..621841471 100644 --- a/src/build/copy-to-dist.js +++ b/src/build/copy-to-dist.js @@ -2,7 +2,11 @@ import { copyFromTo } from './utils.js'; await copyFromTo( - ['src/snarky.d.ts', 'src/bindings/compiled/_node_bindings'], + [ + 'src/snarky.d.ts', + 'src/bindings/compiled/_node_bindings', + 'src/bindings/compiled/node_bindings/plonk_wasm.d.cts', + ], 'src/', 'dist/node/' ); diff --git a/src/examples/api_exploration.ts b/src/examples/api_exploration.ts index a797656d2..69e8f2db3 100644 --- a/src/examples/api_exploration.ts +++ b/src/examples/api_exploration.ts @@ -149,7 +149,7 @@ console.assert(!signature.verify(pubKey, msg1).toBoolean()); */ /* You can initialize elements as literals as follows: */ -let g0 = new Group(-1, 2); +let g0 = Group.from(-1, 2); let g1 = new Group({ x: -2, y: 2 }); /* There is also a predefined generator. */ diff --git a/src/examples/zkapps/local_events_zkapp.ts b/src/examples/zkapps/local_events_zkapp.ts index 560862568..993ed31e1 100644 --- a/src/examples/zkapps/local_events_zkapp.ts +++ b/src/examples/zkapps/local_events_zkapp.ts @@ -91,7 +91,7 @@ let events = await zkapp.fetchEvents(UInt32.from(0)); console.log(events); console.log('---- emitted events: ----'); // fetches all events from zkapp starting block height 0 and ending at block height 10 -events = await zkapp.fetchEvents(UInt32.from(0), UInt64.from(10)); +events = await zkapp.fetchEvents(UInt32.from(0), UInt32.from(10)); console.log(events); console.log('---- emitted events: ----'); // fetches all events diff --git a/src/examples/zkapps/sudoku/sudoku.ts b/src/examples/zkapps/sudoku/sudoku.ts index 5175d011a..9af8952ec 100644 --- a/src/examples/zkapps/sudoku/sudoku.ts +++ b/src/examples/zkapps/sudoku/sudoku.ts @@ -8,7 +8,7 @@ import { isReady, Poseidon, Struct, - Circuit, + Provable, } from 'o1js'; export { Sudoku, SudokuZkApp }; diff --git a/src/examples/zkapps/voting/member.ts b/src/examples/zkapps/voting/member.ts index 1618914b3..c4b0f6e6c 100644 --- a/src/examples/zkapps/voting/member.ts +++ b/src/examples/zkapps/voting/member.ts @@ -52,8 +52,8 @@ export class Member extends CircuitValue { return this; } - static empty() { - return new Member(PublicKey.empty(), UInt64.zero); + static empty any>(): InstanceType { + return new Member(PublicKey.empty(), UInt64.zero) as any; } static from(publicKey: PublicKey, balance: UInt64) { diff --git a/src/examples/zkprogram/program-with-input.ts b/src/examples/zkprogram/program-with-input.ts index 005cce1b1..83265082a 100644 --- a/src/examples/zkprogram/program-with-input.ts +++ b/src/examples/zkprogram/program-with-input.ts @@ -42,7 +42,7 @@ console.log('program digest', MyProgram.digest()); console.log('compiling MyProgram...'); let { verificationKey } = await MyProgram.compile(); -console.log('verification key', verificationKey.slice(0, 10) + '..'); +console.log('verification key', verificationKey.data.slice(0, 10) + '..'); console.log('proving base case...'); let proof = await MyProgram.baseCase(Field(0)); @@ -52,7 +52,7 @@ proof = testJsonRoundtrip(MyProof, proof); proof satisfies Proof; console.log('verify...'); -let ok = await verify(proof.toJSON(), verificationKey); +let ok = await verify(proof.toJSON(), verificationKey.data); console.log('ok?', ok); console.log('verify alternative...'); @@ -64,7 +64,7 @@ proof = await MyProgram.inductiveCase(Field(1), proof); proof = testJsonRoundtrip(MyProof, proof); console.log('verify...'); -ok = await verify(proof, verificationKey); +ok = await verify(proof, verificationKey.data); console.log('ok?', ok); console.log('verify alternative...'); @@ -76,7 +76,7 @@ proof = await MyProgram.inductiveCase(Field(2), proof); proof = testJsonRoundtrip(MyProof, proof); console.log('verify...'); -ok = await verify(proof.toJSON(), verificationKey); +ok = await verify(proof.toJSON(), verificationKey.data); console.log('ok?', ok && proof.publicInput.toString() === '2'); diff --git a/src/examples/zkprogram/program.ts b/src/examples/zkprogram/program.ts index 40b526385..0b75880b0 100644 --- a/src/examples/zkprogram/program.ts +++ b/src/examples/zkprogram/program.ts @@ -43,7 +43,7 @@ console.log('program digest', MyProgram.digest()); console.log('compiling MyProgram...'); let { verificationKey } = await MyProgram.compile(); -console.log('verification key', verificationKey.slice(0, 10) + '..'); +console.log('verification key', verificationKey.data.slice(0, 10) + '..'); console.log('proving base case...'); let proof = await MyProgram.baseCase(); @@ -53,7 +53,7 @@ proof = testJsonRoundtrip(MyProof, proof); proof satisfies Proof; console.log('verify...'); -let ok = await verify(proof.toJSON(), verificationKey); +let ok = await verify(proof.toJSON(), verificationKey.data); console.log('ok?', ok); console.log('verify alternative...'); @@ -65,7 +65,7 @@ proof = await MyProgram.inductiveCase(proof); proof = testJsonRoundtrip(MyProof, proof); console.log('verify...'); -ok = await verify(proof, verificationKey); +ok = await verify(proof, verificationKey.data); console.log('ok?', ok); console.log('verify alternative...'); @@ -77,7 +77,7 @@ proof = await MyProgram.inductiveCase(proof); proof = testJsonRoundtrip(MyProof, proof); console.log('verify...'); -ok = await verify(proof.toJSON(), verificationKey); +ok = await verify(proof.toJSON(), verificationKey.data); console.log('ok?', ok && proof.publicOutput.toString() === '2'); diff --git a/src/lib/account_update.ts b/src/lib/account_update.ts index c1938bd5e..2ab0c5d50 100644 --- a/src/lib/account_update.ts +++ b/src/lib/account_update.ts @@ -1277,6 +1277,9 @@ class AccountUpdate implements Types.AccountUpdate { return [{ lazyAuthorization, children, parent, id, label }, aux]; } static toInput = Types.AccountUpdate.toInput; + static empty() { + return AccountUpdate.dummy(); + } static check = Types.AccountUpdate.check; static fromFields(fields: Field[], [other, aux]: any[]): AccountUpdate { let accountUpdate = Types.AccountUpdate.fromFields(fields, aux); From 7ec8090f57a8b3ec71915516936961f58c77f0a6 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 6 Dec 2023 20:52:42 +0100 Subject: [PATCH 101/102] remove ecdsa from vk test for now --- tests/vk-regression/vk-regression.json | 13 ------------- tests/vk-regression/vk-regression.ts | 2 -- 2 files changed, 15 deletions(-) diff --git a/tests/vk-regression/vk-regression.json b/tests/vk-regression/vk-regression.json index 0297b30a0..f60540c9f 100644 --- a/tests/vk-regression/vk-regression.json +++ b/tests/vk-regression/vk-regression.json @@ -201,18 +201,5 @@ "data": "", "hash": "" } - }, - "ecdsa": { - "digest": "6e4078c6df944db119dc1656eb68e6c272c3f2e9cab6746913759537cbfcfa9", - "methods": { - "verifyEcdsa": { - "rows": 38846, - "digest": "892b0a1fad0f13d92ba6099cd54e6780" - } - }, - "verificationKey": { - "data": "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", - "hash": "21152043351405736424630704375244880683906889913335338686836578165782029001213" - } } } \ No newline at end of file diff --git a/tests/vk-regression/vk-regression.ts b/tests/vk-regression/vk-regression.ts index a051f2113..28a95542b 100644 --- a/tests/vk-regression/vk-regression.ts +++ b/tests/vk-regression/vk-regression.ts @@ -3,7 +3,6 @@ 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/zkprogram/ecdsa/ecdsa.js'; import { GroupCS, BitwiseCS } from './plain-constraint-system.js'; // toggle this for quick iteration when debugging vk regressions @@ -39,7 +38,6 @@ const ConstraintSystems: MinimumConstraintSystem[] = [ createDex().Dex, GroupCS, BitwiseCS, - ecdsaProgram, ]; let filePath = jsonPath ? jsonPath : './tests/vk-regression/vk-regression.json'; From 507a04be0b23ff3bb918536d328e37c0fa37fe64 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 6 Dec 2023 20:58:21 +0100 Subject: [PATCH 102/102] fixup --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c2b9510c5..8d37f3882 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "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": "rimraf ./dist/examples && npx tsc -p tsconfig.examples.json", + "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",