diff --git a/CHANGELOG.md b/CHANGELOG.md index dd07f2d931..b88f6ffa83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Breaking changes +- Native curve improvements https://github.com/o1-labs/o1js/pull/1530 + - Change the internal representation of `Scalar` from 255 Bools to 1 Bool and 1 Field (low bit and high 254 bits) + - Make `Group.scale()` support all scalars (previously did not support 0, 1 and -1) + - Make `Group.scale()` directly accept `Field` elements, and much more efficient than previous methods of scaling by Fields + - As a result, `Signature.verify()` and `Nullifier.verify()` use much fewer constraints + - Fix `Scalar.fromBits()` to not produce a shifted scalar; shifting is no longer exposed to users of `Scalar`. - Add assertion to the foreign EC addition gadget that prevents degenerate cases https://github.com/o1-labs/o1js/pull/1545 - Fixes soundness of ECDSA; slightly increases its constraints from ~28k to 29k - Breaks circuits that used EC addition, like ECDSA diff --git a/src/bindings b/src/bindings index 13d42834c3..a4dd716fde 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 13d42834c3ccbe5ce35285979fbe667aa59dfdb7 +Subproject commit a4dd716fdea62b81ad5957c73f8800e1a2cc57bd diff --git a/src/examples/nullifier.ts b/src/examples/nullifier.ts index 9693583dcf..52dec59094 100644 --- a/src/examples/nullifier.ts +++ b/src/examples/nullifier.ts @@ -7,7 +7,6 @@ import { State, method, MerkleMap, - Circuit, MerkleMapWitness, Mina, AccountUpdate, diff --git a/src/lib/ml/conversion.ts b/src/lib/ml/conversion.ts index 91b938d105..94a9358c51 100644 --- a/src/lib/ml/conversion.ts +++ b/src/lib/ml/conversion.ts @@ -71,18 +71,18 @@ function varToField(x: FieldVar): Field { return Field(x); } -function fromScalar(s: Scalar) { - return s.toConstant().constantValue; +function fromScalar(s: Scalar): ScalarConst { + return [0, s.toBigInt()]; } function toScalar(s: ScalarConst) { - return Scalar.from(s); + return Scalar.from(s[1]); } function fromPrivateKey(sk: PrivateKey) { return fromScalar(sk.s); } function toPrivateKey(sk: ScalarConst) { - return new PrivateKey(Scalar.from(sk)); + return new PrivateKey(Scalar.from(sk[1])); } function fromPublicKey(pk: PublicKey): MlPublicKey { diff --git a/src/lib/provable/crypto/nullifier.ts b/src/lib/provable/crypto/nullifier.ts index f0ce72a65b..de7fb11428 100644 --- a/src/lib/provable/crypto/nullifier.ts +++ b/src/lib/provable/crypto/nullifier.ts @@ -3,7 +3,7 @@ import { Struct } from '../types/struct.js'; import { Field, Group, Scalar } from '../wrapped.js'; import { Poseidon } from './poseidon.js'; import { MerkleMapWitness } from '../merkle-map.js'; -import { PrivateKey, PublicKey, scaleShifted } from './signature.js'; +import { PrivateKey, PublicKey } from './signature.js'; import { Provable } from '../provable.js'; export { Nullifier }; @@ -50,7 +50,6 @@ class Nullifier extends Struct({ public: { nullifier, s }, private: { c }, } = this; - // generator let G = Group.generator; @@ -68,9 +67,8 @@ class Nullifier extends Struct({ let h_m_pk = Group.fromFields([x, x0]); - // shifted scalar see https://github.com/o1-labs/o1js/blob/5333817a62890c43ac1b9cb345748984df271b62/src/lib/signature.ts#L220 // pk^c - let pk_c = scaleShifted(this.publicKey, Scalar.fromBits(c.toBits())); + let pk_c = this.publicKey.scale(c); // g^r = g^s / pk^c let g_r = G.scale(s).sub(pk_c); @@ -79,9 +77,7 @@ class Nullifier extends Struct({ let h_m_pk_s = h_m_pk.scale(s); // h_m_pk_r = h(m,pk)^s / nullifier^c - let h_m_pk_s_div_nullifier_s = h_m_pk_s.sub( - scaleShifted(nullifier, Scalar.fromBits(c.toBits())) - ); + let h_m_pk_s_div_nullifier_s = h_m_pk_s.sub(nullifier.scale(c)); // this is supposed to match the entries generated on "the other side" of the nullifier (mina-signer, in an wallet enclave) Poseidon.hash([ diff --git a/src/lib/provable/crypto/signature.ts b/src/lib/provable/crypto/signature.ts index 9a6159f933..396e50cdea 100644 --- a/src/lib/provable/crypto/signature.ts +++ b/src/lib/provable/crypto/signature.ts @@ -1,7 +1,6 @@ import { Field, Bool, Group, Scalar } from '../wrapped.js'; import { AnyConstructor } from '../types/struct.js'; import { hashWithPrefix } from './poseidon.js'; -import { Fq } from '../../../bindings/crypto/finite-field.js'; import { deriveNonce, Signature as SignatureBigint, @@ -11,16 +10,12 @@ import { PrivateKey as PrivateKeyBigint, PublicKey as PublicKeyBigint, } from '../../../mina-signer/src/curve-bigint.js'; -import { constantScalarToBigint } from '../scalar.js'; import { toConstantField } from '../field.js'; import { CircuitValue, prop } from '../types/circuit-value.js'; // external API export { PrivateKey, PublicKey, Signature }; -// internal API -export { scaleShifted }; - /** * A signing key. You can generate one via {@link PrivateKey.random}. */ @@ -71,7 +66,7 @@ class PrivateKey extends CircuitValue { * Convert this {@link PrivateKey} to a bigint */ toBigInt() { - return constantScalarToBigint(this.s, 'PrivateKey.toBigInt'); + return this.s.toBigInt(); } /** @@ -117,9 +112,7 @@ class PrivateKey extends CircuitValue { * @returns a base58 encoded string */ static toBase58(privateKey: { s: Scalar }) { - return PrivateKeyBigint.toBase58( - constantScalarToBigint(privateKey.s, 'PrivateKey.toBase58') - ); + return PrivateKeyBigint.toBase58(privateKey.s.toBigInt()); } } @@ -249,12 +242,13 @@ class Signature extends CircuitValue { * @returns a {@link Signature} */ static create(privKey: PrivateKey, msg: Field[]): Signature { - const publicKey = PublicKey.fromPrivateKey(privKey).toGroup(); - const d = privKey.s; + let publicKey = PublicKey.fromPrivateKey(privKey).toGroup(); + let d = privKey.s; + // we chose an arbitrary prefix for the signature, and it happened to be 'testnet' // there's no consequences in practice and the signatures can be used with any network // if there needs to be a custom nonce, include it in the message itself - const kPrime = Scalar.from( + let kPrime = Scalar.from( deriveNonce( { fields: msg.map((f) => f.toBigInt()) }, { x: publicKey.x.toBigInt(), y: publicKey.y.toBigInt() }, @@ -262,16 +256,15 @@ class Signature extends CircuitValue { 'testnet' ) ); + let { x: r, y: ry } = Group.generator.scale(kPrime); - const k = ry.isOdd().toBoolean() ? kPrime.neg() : kPrime; + let k = ry.isOdd().toBoolean() ? kPrime.neg() : kPrime; let h = hashWithPrefix( signaturePrefix('testnet'), msg.concat([publicKey.x, publicKey.y, r]) ); - // TODO: Scalar.fromBits interprets the input as a "shifted scalar" - // therefore we have to unshift e before using it - let e = unshift(Scalar.fromBits(h.toBits())); - const s = e.mul(d).add(k); + let e = Scalar.fromField(h); + let s = e.mul(d).add(k); return new Signature(r, s); } @@ -280,7 +273,8 @@ class Signature extends CircuitValue { * @returns a {@link Bool} */ verify(publicKey: PublicKey, msg: Field[]): Bool { - const point = publicKey.toGroup(); + let point = publicKey.toGroup(); + // we chose an arbitrary prefix for the signature, and it happened to be 'testnet' // there's no consequences in practice and the signatures can be used with any network // if there needs to be a custom nonce, include it in the message itself @@ -288,10 +282,8 @@ class Signature extends CircuitValue { signaturePrefix('testnet'), msg.concat([point.x, point.y, this.r]) ); - // TODO: Scalar.fromBits interprets the input as a "shifted scalar" - // therefore we have to use scaleShifted which is very inefficient - let e = Scalar.fromBits(h.toBits()); - let r = scaleShifted(point, e).neg().add(Group.generator.scale(this.s)); + + let r = point.scale(h).neg().add(Group.generator.scale(this.s)); return r.x.equals(this.r).and(r.y.isEven()); } @@ -311,19 +303,3 @@ class Signature extends CircuitValue { return SignatureBigint.toBase58({ r, s }); } } - -// performs scalar multiplication s*G assuming that instead of s, we got s' = 2s + 1 + 2^255 -// cost: 2x scale by constant, 1x scale by variable -function scaleShifted(point: Group, shiftedScalar: Scalar) { - let oneHalfGroup = point.scale(Scalar.from(oneHalf)); - let shiftGroup = oneHalfGroup.scale(Scalar.from(shift)); - return oneHalfGroup.scale(shiftedScalar).sub(shiftGroup); -} -// returns s, assuming that instead of s, we got s' = 2s + 1 + 2^255 -// (only works out of snark) -function unshift(shiftedScalar: Scalar) { - return shiftedScalar.sub(Scalar.from(shift)).mul(Scalar.from(oneHalf)); -} - -let shift = Fq.mod(1n + 2n ** 255n); -let oneHalf = Fq.inverse(2n)!; diff --git a/src/lib/provable/field.ts b/src/lib/provable/field.ts index 0199b6cf58..244d4382ae 100644 --- a/src/lib/provable/field.ts +++ b/src/lib/provable/field.ts @@ -26,6 +26,7 @@ import { setFieldConstructor } from './core/field-constructor.js'; import { assertLessThanFull, assertLessThanOrEqualFull, + isOddAndHigh, lessThanFull, lessThanOrEqualFull, } from './gadgets/comparison.js'; @@ -320,24 +321,7 @@ class Field { * See {@link Field.isEven} for examples. */ isOdd() { - if (this.isConstant()) return new Bool((this.toBigInt() & 1n) === 1n); - - // witness a bit b such that x = b + 2z for some z <= (p-1)/2 - // this is always possible, and unique _except_ in the edge case where x = 0 = 0 + 2*0 = 1 + 2*(p-1)/2 - // so we can compute isOdd = b AND (x != 0) - let [b, z] = exists(2, () => { - let x = this.toBigInt(); - return [x & 1n, x >> 1n]; - }); - let isOdd = b.assertBool(); - z.assertLessThan((Field.ORDER + 1n) / 2n); - - // x == b + 2z - b.add(z.mul(2)).assertEquals(this); - - // avoid overflow case when x = 0 - let isNonZero = this.equals(0).not(); - return isOdd.and(isNonZero); + return isOddAndHigh(this).isOdd; } /** diff --git a/src/lib/provable/gadgets/common.ts b/src/lib/provable/gadgets/common.ts index a1a065f2fd..4c8c1ddca0 100644 --- a/src/lib/provable/gadgets/common.ts +++ b/src/lib/provable/gadgets/common.ts @@ -4,9 +4,19 @@ import { Tuple } from '../../util/types.js'; import type { Bool } from '../bool.js'; import { fieldVar } from '../gates.js'; import { existsOne } from '../core/exists.js'; -import { createField } from '../core/field-constructor.js'; +import { createField, isBool } from '../core/field-constructor.js'; -export { toVars, toVar, isVar, assert, bitSlice, divideWithRemainder }; +export { + toVars, + toVar, + isVar, + assert, + bitSlice, + bit, + divideWithRemainder, + packBits, + isConstant, +}; /** * Given a Field, collapse its AST to a pure Var. See {@link FieldVar}. @@ -56,8 +66,33 @@ function bitSlice(x: bigint, start: number, length: number) { return (x >> BigInt(start)) & ((1n << BigInt(length)) - 1n); } +function bit(x: bigint, i: number) { + return (x >> BigInt(i)) & 1n; +} + function divideWithRemainder(numerator: bigint, denominator: bigint) { const quotient = numerator / denominator; const remainder = numerator - denominator * quotient; return { quotient, remainder }; } + +// pack bools into a single field element + +/** + * Helper function to provably pack bits into a single field element. + * Just returns the sum without any boolean checks. + */ +function packBits(bits: (Field | Bool)[]): Field { + let n = bits.length; + let sum = createField(0n); + for (let i = 0; i < n; i++) { + let bit = bits[i]; + if (isBool(bit)) bit = bit.toField(); + sum = sum.add(bit.mul(1n << BigInt(i))); + } + return sum.seal(); +} + +function isConstant(...args: (Field | Bool)[]): boolean { + return args.every((x) => x.isConstant()); +} diff --git a/src/lib/provable/gadgets/comparison.ts b/src/lib/provable/gadgets/comparison.ts index 15ae06061c..e784f54caf 100644 --- a/src/lib/provable/gadgets/comparison.ts +++ b/src/lib/provable/gadgets/comparison.ts @@ -1,6 +1,10 @@ import type { Field } from '../field.js'; import type { Bool } from '../bool.js'; -import { createBoolUnsafe, createField } from '../core/field-constructor.js'; +import { + createBool, + createBoolUnsafe, + createField, +} from '../core/field-constructor.js'; import { Fp } from '../../../bindings/crypto/finite-field.js'; import { assert } from '../../../lib/util/assert.js'; import { exists, existsOne } from '../core/exists.js'; @@ -15,13 +19,21 @@ export { assertLessThanOrEqualGeneric, lessThanGeneric, lessThanOrEqualGeneric, + // comparison gadgets for full range inputs assertLessThanFull, assertLessThanOrEqualFull, lessThanFull, lessThanOrEqualFull, + + // gadgets that are based on full comparisons + isOddAndHigh, + // legacy, unused compareCompatible, + + // internal helper + fieldToField3, }; /** @@ -181,6 +193,40 @@ function lessThanOrEqualFull(x: Field, y: Field) { return lessThanFull(y, x).not(); } +/** + * Splits a field element into a low bit `isOdd` and a 254-bit `high` part. + * + * There are no assumptions on the range of x and y, they can occupy the full range [0, p). + */ +function isOddAndHigh(x: Field) { + if (x.isConstant()) { + let x0 = x.toBigInt(); + return { isOdd: createBool((x0 & 1n) === 1n), high: createField(x0 >> 1n) }; + } + + // witness a bit b such that x = b + 2z for some z <= (p-1)/2 + // this is always possible, and unique _except_ in the edge case where x = 0 = 0 + 2*0 = 1 + 2*(p-1)/2 + // so we must assert that x = 0 implies b = 0 + let [b, z] = exists(2, () => { + let x0 = x.toBigInt(); + return [x0 & 1n, x0 >> 1n]; + }); + let isOdd = b.assertBool(); + z.assertLessThan((Fp.modulus + 1n) / 2n); + + // x == b + 2z + b.add(z.mul(2n)).assertEquals(x); + + // prevent overflow case when x = 0 + // we witness x' such that b == x * x', which makes it impossible to have x = 0 and b = 1 + let x_ = existsOne(() => + b.toBigInt() === 0n ? 0n : Fp.inverse(x.toBigInt()) ?? 0n + ); + x.mul(x_).assertEquals(b); + + return { isOdd, high: z }; +} + /** * internal helper, split Field into a 3-limb bigint * diff --git a/src/lib/provable/gadgets/foreign-field.ts b/src/lib/provable/gadgets/foreign-field.ts index df864a7a6b..685c138881 100644 --- a/src/lib/provable/gadgets/foreign-field.ts +++ b/src/lib/provable/gadgets/foreign-field.ts @@ -23,13 +23,27 @@ import { l3, compactMultiRangeCheck, } from './range-check.js'; -import { createBool, createField } from '../core/field-constructor.js'; +import { + createBool, + createField, + getField, +} from '../core/field-constructor.js'; +import type { Bool } from '../bool.js'; // external API export { ForeignField, Field3 }; // internal API -export { bigint3, Sign, split, combine, weakBound, Sum, assertMul }; +export { + bigint3, + Sign, + split, + combine, + weakBound, + Sum, + assertMul, + field3FromBits, +}; /** * A 3-tuple of Fields, representing a 3-limb bigint. @@ -770,6 +784,17 @@ function assertLessThanOrEqual(x: Field3, y: bigint | Field3) { sum([y_, x], [-1n], 0n); } +// Field3 from/to bits + +function field3FromBits(bits: Bool[]): Field3 { + const Field = getField(); + let limbSize = Number(l); + let l0 = Field.fromBits(bits.slice(0 * limbSize, 1 * limbSize)); + let l1 = Field.fromBits(bits.slice(1 * limbSize, 2 * limbSize)); + let l2 = Field.fromBits(bits.slice(2 * limbSize, 3 * limbSize)); + return [l0, l1, l2]; +} + // helpers /** diff --git a/src/lib/provable/gadgets/native-curve.ts b/src/lib/provable/gadgets/native-curve.ts new file mode 100644 index 0000000000..21a9531747 --- /dev/null +++ b/src/lib/provable/gadgets/native-curve.ts @@ -0,0 +1,312 @@ +import type { Field } from '../field.js'; +import type { Bool } from '../bool.js'; +import { Fp, Fq } from '../../../bindings/crypto/finite-field.js'; +import { PallasAffine } from '../../../bindings/crypto/elliptic-curve.js'; +import { isOddAndHigh } from './comparison.js'; +import { Field3, ForeignField } from './foreign-field.js'; +import { exists } from '../core/exists.js'; +import { assert, bit, bitSlice, isConstant } from './common.js'; +import { l, rangeCheck64 } from './range-check.js'; +import { + createBool, + createBoolUnsafe, + createField, + getField, +} from '../core/field-constructor.js'; +import { Snarky } from '../../../snarky.js'; +import { Provable } from '../provable.js'; +import { MlPair } from '../../ml/base.js'; +import { provable } from '../types/provable-derivers.js'; + +export { + scaleField, + fieldToShiftedScalar, + field3ToShiftedScalar, + scaleShifted, + add, + ShiftedScalar, +}; + +type Point = { x: Field; y: Field }; +type ShiftedScalar = { lowBit: Bool; high254: Field }; + +/** + * Dedicated gadget to scale a point by a scalar, where the scalar is represented as a _native_ Field. + */ +function scaleField(P: Point, s: Field): Point { + // constant case + let { x, y } = P; + if (x.isConstant() && y.isConstant() && s.isConstant()) { + let sP = PallasAffine.scale( + PallasAffine.fromNonzero({ x: x.toBigInt(), y: y.toBigInt() }), + s.toBigInt() + ); + return { x: createField(sP.x), y: createField(sP.y) }; + } + const Field = getField(); + const Point = provable({ x: Field, y: Field }); + + /** + * Strategy: + * - use a (1, 254) split and compute s - 2^255 with manual add-and-carry + * - use all 255 rounds of `scaleFastUnpack` for the high part + * - pass in s or a dummy replacement if s = 0, 1 (which are the disallowed values) + * - return sP or 0P = 0 or 1P = P + */ + + // compute t = s + (-2^255 mod q) in (1, 254) arithmetic + let { isOdd: sLo, high: sHi } = isOddAndHigh(s); + + let shift = Fq.mod(-(1n << 255n)); + assert((shift & 1n) === 0n); // shift happens to be even, so we don't need to worry about a carry + let shiftHi = shift >> 1n; + + let tLo = sLo; + let tHi = sHi.add(shiftHi).seal(); + + // tHi does not overflow: + // tHi = sHi + shiftHi < p/2 + p/2 = p + // sHi < p/2 is guaranteed by isOddAndHigh + assert(shiftHi < Fp.modulus / 2n); + + // the 4 values for s not supported by `scaleFastUnpack` are q-2, q-1, 0, 1 + // since s came from a `Field`, we can exclude q-2, q-1 + // s = 0 or 1 iff sHi = 0 + let isEdgeCase = sHi.equals(0n); + let tHiSafe = Provable.if(isEdgeCase, createField(0n), tHi); + + // R = (2*(t >> 1) + 1 + 2^255)P + // also returns a 255-bit representation of tHi + let [, RMl, [, ...tHiBitsMl]] = Snarky.group.scaleFastUnpack( + [0, x.value, y.value], + [0, tHiSafe.value], + 255 + ); + let R = { x: createField(RMl[1]), y: createField(RMl[2]) }; + + // prove that tHi has only 254 bits set + createField(tHiBitsMl[254]).assertEquals(0n); + + // R = tLo ? R : R - P = (t + 2^255)P = sP + let RminusP = addNonZero(R, negate(P)); // can only be zero if s = 0, which we handle later + R = Provable.if(tLo, Point, R, RminusP); + + // now handle the two edge cases s=0 and s=1 + let zero = createField(0n); + let zeroPoint = { x: zero, y: zero }; + let edgeCaseResult = Provable.if(sLo, Point, P, zeroPoint); + return Provable.if(isEdgeCase, Point, edgeCaseResult, R); +} + +/** + * Internal helper to compute `(t + 2^255)*P`. + * `t` is expected to be split into 254 high bits (t >> 1) and a low bit (t & 1). + * + * The gadget proves that `tHi` is in [0, 2^254) but assumes that `tLo` is a single bit. + * + * Optionally, you can specify a different number of high bits by passing in `numHighBits`. + */ +function scaleShifted( + { x, y }: Point, + { lowBit: tLo, high254: tHi }: ShiftedScalar, + numHighBits = 254 +): Point { + // constant case + if (isConstant(x, y, tHi, tLo)) { + let sP = PallasAffine.scale( + PallasAffine.fromNonzero({ x: x.toBigInt(), y: y.toBigInt() }), + Fq.mod(tLo.toField().toBigInt() + 2n * tHi.toBigInt() + (1n << 255n)) + ); + return { x: createField(sP.x), y: createField(sP.y) }; + } + const Field = getField(); + const Point = provable({ x: Field, y: Field }); + let zero = createField(0n); + + /** + * Strategy: + * - use all 255 rounds of `scaleFastUnpack` for the high part + * - handle two disallowed tHi values separately: -2^254, -2^254 - 1 + * - don't handle disallowed tHi = -2^254 - 1/2 because it wouldn't normally be used, as it's > q/2 + */ + let equalsMinusShift = tHi.equals(Fq.modulus - (1n << 254n)); + let equalsMinusShiftMinus1 = tHi.equals(Fq.modulus - (1n << 254n) - 1n); + let isEdgeCase = equalsMinusShift.or(equalsMinusShiftMinus1); + let tHiSafe = Provable.if(isEdgeCase, zero, tHi); + + // R = (2*(t >> 1) + 1 + 2^255)P + // also returns a 255-bit representation of tHi + let [, RMl, [, ...tHiBitsMl]] = Snarky.group.scaleFastUnpack( + [0, x.value, y.value], + [0, tHiSafe.value], + 255 + ); + let P = { x, y }; + let R = { x: createField(RMl[1]), y: createField(RMl[2]) }; + + // prove that tHi has only `numHighBits` bits set + for (let i = numHighBits; i < 255; i++) { + createField(tHiBitsMl[i]).assertEquals(zero); + } + + // handle edge cases + // 2*(-2^254) + 1 + 2^255 = 1 + // 2*(-2^254 - 1) + 1 + 2^255 = -1 + // so the result is (x,+-y) + let edgeCaseY = y.mul(equalsMinusShift.toField().mul(2n).sub(1n)); // y*(2b - 1) = y or -y + let edgeCaseResult = { x, y: edgeCaseY }; + R = Provable.if(isEdgeCase, Point, edgeCaseResult, R); + + // R = tLo ? R : R - P = (t + 2^255)P + // we also handle a zero R-P result to make the 0 scalar work + let { result: RminusP, isInfinity } = add(R, negate(P)); + RminusP = Provable.if(isInfinity, Point, { x: zero, y: zero }, RminusP); + R = Provable.if(tLo, Point, R, RminusP); + + return R; +} + +/** + * Converts a field element s to a shifted representation t = s - 2^254 mod q, + * where t is represented as a low bit and a 254-bit high part. + * + * This is the representation we use for scalars, since it can be used as input to `scaleShifted()`. + */ +function fieldToShiftedScalar(s: Field): ShiftedScalar { + // constant case + if (s.isConstant()) { + let t = Fq.mod(s.toBigInt() - (1n << 255n)); + let lowBit = createBool((t & 1n) === 1n); + let high254 = createField(t >> 1n); + return { lowBit, high254 }; + } + + // compute t = s + (-2^255 mod q) in (1, 254) arithmetic + let { isOdd: sLo, high: sHi } = isOddAndHigh(s); + + let shift = Fq.mod(-(1n << 255n)); + assert((shift & 1n) === 0n); // shift happens to be even, so we don't need to worry about a carry + let shiftHi = shift >> 1n; + + let tLo = sLo; + let tHi = sHi.add(shiftHi).seal(); + + // tHi does not overflow: + // tHi = sHi + shiftHi < p/2 + p/2 = p + // sHi < p/2 is guaranteed by isOddAndHigh + assert(shiftHi < Fp.modulus / 2n); + + return { lowBit: tLo, high254: tHi }; +} + +/** + * Converts a 3-limb bigint to a shifted representation t = s - 2^255 mod q, + * where t is represented as a low bit and a 254-bit high part. + */ +function field3ToShiftedScalar(s: Field3): ShiftedScalar { + // constant case + if (Field3.isConstant(s)) { + let t = Fq.mod(Field3.toBigint(s) - (1n << 255n)); + let lowBit = createBool((t & 1n) === 1n); + let high254 = createField(t >> 1n); + return { lowBit, high254 }; + } + + // compute t = s - (2^255 mod q) using foreign field subtraction + let twoTo255 = Field3.from(Fq.mod(1n << 255n)); + let t = ForeignField.sub(s, twoTo255, Fq.modulus); + let [t0, t1, t2] = t; + + // to fully constrain the output scalar, we need to prove that t is canonical + // otherwise, the subtraction above can add +q to the result, which yields an alternative bit representation + // this also provides a bound on the high part, to that the computation of tHi can't overflow + ForeignField.assertLessThan(t, Fq.modulus); + + // split t into 254 high bits and a low bit + // => split t0 into [1, 87] => split t0 into [1, 64, 23] so we can efficiently range-check + let [tLo, tHi00, tHi01] = exists(3, () => { + let t = t0.toBigInt(); + return [bit(t, 0), bitSlice(t, 1, 64), bitSlice(t, 65, 23)]; + }); + let tLoBool = tLo.assertBool(); + rangeCheck64(tHi00); + rangeCheck64(tHi01); + + // prove (tLo, tHi0) split + // since we know that t0 < 2^88 and tHi0 < 2^128, this even proves that tHi0 < 2^87 + // (the bound on tHi0 is necessary so that 2*tHi0 can't overflow) + let tHi0 = tHi00.add(tHi01.mul(1n << 64n)); + tLo.add(tHi0.mul(2n)).assertEquals(t0); + + // pack tHi + // this can't overflow the native field because: + // -) we showed t < q + // -) the three combined limbs here represent the bigint (t >> 1) < q/2 < p + let tHi = tHi0 + .add(t1.mul(1n << (l - 1n))) + .add(t2.mul(1n << (2n * l - 1n))) + .seal(); + + return { lowBit: tLoBool, high254: tHi }; +} + +/** + * Wraps the `EC_add` gate to perform complete addition of two non-zero curve points. + */ +function add(g: Point, h: Point): { result: Point; isInfinity: Bool } { + // compute witnesses + let witnesses = exists(7, () => { + let x1 = g.x.toBigInt(); + let y1 = g.y.toBigInt(); + let x2 = h.x.toBigInt(); + let y2 = h.y.toBigInt(); + + let sameX = BigInt(x1 === x2); + let inf = BigInt(sameX && y1 !== y2); + let infZ = sameX ? Fp.inverse(y2 - y1) ?? 0n : 0n; + let x21Inv = Fp.inverse(x2 - x1) ?? 0n; + + let slopeDouble = Fp.div(3n * x1 ** 2n, 2n * y1) ?? 0n; + let slopeAdd = Fp.mul(y2 - y1, x21Inv); + let s = sameX ? slopeDouble : slopeAdd; + + let x3 = Fp.mod(s ** 2n - x1 - x2); + let y3 = Fp.mod(s * (x1 - x3) - y1); + + return [sameX, inf, infZ, x21Inv, s, x3, y3]; + }); + + let [same_x, inf, inf_z, x21_inv, s, x3, y3] = witnesses; + + Snarky.gates.ecAdd( + MlPair(g.x.seal().value, g.y.seal().value), + MlPair(h.x.seal().value, h.y.seal().value), + MlPair(x3.value, y3.value), + inf.value, + same_x.value, + s.value, + inf_z.value, + x21_inv.value + ); + + // the ecAdd gate constrains `inf` to be boolean + let isInfinity = createBoolUnsafe(inf); + + return { result: { x: x3, y: y3 }, isInfinity }; +} + +/** + * Addition that asserts the result is non-zero. + */ +function addNonZero(g: Point, h: Point) { + let { result, isInfinity } = add(g, h); + isInfinity.assertFalse(); + return result; +} + +/** + * Negates a point. + */ +function negate(g: Point): Point { + return { x: g.x, y: g.y.neg() }; +} diff --git a/src/lib/provable/group.ts b/src/lib/provable/group.ts index 31cee90cfd..7b154eeae0 100644 --- a/src/lib/provable/group.ts +++ b/src/lib/provable/group.ts @@ -1,11 +1,16 @@ import { Field } from './field.js'; import { FieldVar } from './core/fieldvar.js'; import { Scalar } from './scalar.js'; -import { Snarky } from '../../snarky.js'; import { Fp } from '../../bindings/crypto/finite-field.js'; -import { GroupAffine, Pallas } from '../../bindings/crypto/elliptic-curve.js'; +import { + GroupAffine, + Pallas, + PallasAffine, +} from '../../bindings/crypto/elliptic-curve.js'; import { Provable } from './provable.js'; import { Bool } from './bool.js'; +import { assert } from '../util/assert.js'; +import { add, scaleField, scaleShifted } from './gadgets/native-curve.js'; export { Group }; @@ -101,67 +106,49 @@ class Group { return fromProjective(g_proj); } } else { - const { x: x1, y: y1 } = this; - const { x: x2, y: y2 } = g; - - let zero = new Field(0); - - let same_x = Provable.witness(Field, () => x1.equals(x2).toField()); - - let inf = Provable.witness(Bool, () => - x1.equals(x2).and(y1.equals(y2).not()) - ); - - let inf_z = Provable.witness(Field, () => { - if (y1.equals(y2).toBoolean()) return zero; - else if (x1.equals(x2).toBoolean()) return y2.sub(y1).inv(); - else return zero; - }); - - let x21_inv = Provable.witness(Field, () => { - if (x1.equals(x2).toBoolean()) return zero; - else return x2.sub(x1).inv(); - }); - - let s = Provable.witness(Field, () => { - if (x1.equals(x2).toBoolean()) { - let x1_squared = x1.square(); - return x1_squared.add(x1_squared).add(x1_squared).div(y1.add(y1)); - } else return y2.sub(y1).div(x2.sub(x1)); - }); - - let x3 = Provable.witness(Field, () => { - return s.square().sub(x1.add(x2)); - }); - - let y3 = Provable.witness(Field, () => { - return s.mul(x1.sub(x3)).sub(y1); - }); - - let [, x, y] = Snarky.gates.ecAdd( - toTuple(Group.from(x1.seal(), y1.seal())), - toTuple(Group.from(x2.seal(), y2.seal())), - toTuple(Group.from(x3, y3)), - inf.toField().value, - same_x.value, - s.value, - inf_z.value, - x21_inv.value - ); - + let { result, isInfinity } = add(this, g); // similarly to the constant implementation, we check if either operand is zero // and the implementation above (original OCaml implementation) returns something wild -> g + 0 != g where it should be g + 0 = g let gIsZero = g.isZero(); let onlyThisIsZero = this.isZero().and(gIsZero.not()); - let isNegation = inf; + let isNegation = isInfinity; let isNormalAddition = gIsZero.or(onlyThisIsZero).or(isNegation).not(); // note: gIsZero and isNegation are not mutually exclusive, but if both are true, we add 1*0 + 1*0 = 0 which is correct return Provable.switch( [gIsZero, onlyThisIsZero, isNegation, isNormalAddition], Group, - [this, g, Group.zero, new Group({ x, y })] + [this, g, Group.zero, new Group(result)], + { allowNonExclusive: true } + ); + } + } + + /** + * Lower-level variant of {@link add} which doesn't handle the case where one of the operands is zero, and + * asserts that the output is non-zero. + * + * Optionally, zero outputs can be allowed by setting `allowZeroOutput` to `true`. + * + * **Warning**: If one of the inputs is zero, the result will be garbage and the proof useless. + * This case has to be prevented or handled separately by the caller of this method. + */ + addNonZero(g2: Group, allowZeroOutput = false): Group { + if (isConstant(this) && isConstant(g2)) { + let { x, y, infinity } = PallasAffine.add(toAffine(this), toAffine(g2)); + assert( + !infinity || allowZeroOutput, + 'Group.addNonzero(): Result is zero' ); + return fromAffine({ x, y, infinity }); + } + let { result, isInfinity } = add(this, g2); + + if (allowZeroOutput) { + return Provable.if(isInfinity, Group.zero, new Group(result)); + } else { + isInfinity.assertFalse('Group.addNonzero(): Result is zero'); + return new Group(result); } } @@ -189,17 +176,16 @@ class Group { * let 5g = g.scale(s); * ``` */ - scale(s: Scalar | number | bigint) { + scale(s: Scalar | Field | number | bigint) { + if (s instanceof Field) return new Group(scaleField(this, s)); let scalar = Scalar.from(s); if (isConstant(this) && scalar.isConstant()) { let g_proj = Pallas.scale(toProjective(this), scalar.toBigInt()); return fromProjective(g_proj); } else { - let [, ...bits] = scalar.value; - bits.reverse(); - let [, x, y] = Snarky.group.scale(toTuple(this), [0, ...bits]); - return new Group({ x, y }); + let result = scaleShifted(this, scalar); + return new Group(result); } } @@ -383,3 +369,7 @@ function fromProjective({ x, y, z }: { x: bigint; y: bigint; z: bigint }) { function fromAffine({ x, y, infinity }: GroupAffine) { return infinity ? Group.zero : new Group({ x, y }); } + +function toAffine(g: Group): GroupAffine { + return PallasAffine.from({ x: g.x.toBigInt(), y: g.y.toBigInt() }); +} diff --git a/src/lib/provable/provable.ts b/src/lib/provable/provable.ts index 20c4bb6f01..6db7e9c47d 100644 --- a/src/lib/provable/provable.ts +++ b/src/lib/provable/provable.ts @@ -288,6 +288,9 @@ function if_(condition: Bool, typeOrX: any, xOrY: any, yOrUndefined?: any) { } function ifField(b: Field, x: Field, y: Field) { + // TODO: this is suboptimal if one of x, y is constant + // it uses 2-3 generic gates in that case, where 1 would be enough + // b*(x - y) + y // NOTE: the R1CS constraint used by Field.if_ in snarky-ml // leads to a different but equivalent layout (same # constraints) @@ -339,7 +342,8 @@ function ifImplicit(condition: Bool, x: T, y: T): T { function switch_>( mask: Bool[], type: A, - values: T[] + values: T[], + { allowNonExclusive = false } = {} ): T { // picks the value at the index where mask is true let nValues = values.length; @@ -348,6 +352,7 @@ function switch_>( `Provable.switch: \`values\` and \`mask\` have different lengths (${values.length} vs. ${mask.length}), which is not allowed.` ); let checkMask = () => { + if (allowNonExclusive) return; let nTrue = mask.filter((b) => b.toBoolean()).length; if (nTrue > 1) { throw Error( diff --git a/src/lib/provable/scalar.ts b/src/lib/provable/scalar.ts index 87b9f1ce76..d44cdbe0a8 100644 --- a/src/lib/provable/scalar.ts +++ b/src/lib/provable/scalar.ts @@ -1,47 +1,40 @@ -import { Snarky } from '../../snarky.js'; import { Fq } from '../../bindings/crypto/finite-field.js'; import { Scalar as SignableFq } from '../../mina-signer/src/curve-bigint.js'; -import { Field } from './field.js'; -import { FieldVar, FieldConst } from './core/fieldvar.js'; -import { MlArray } from '../ml/base.js'; +import { Field, checkBitLength } from './field.js'; +import { FieldVar } from './core/fieldvar.js'; import { Bool } from './bool.js'; +import { + ShiftedScalar, + field3ToShiftedScalar, + fieldToShiftedScalar, +} from './gadgets/native-curve.js'; +import { isConstant } from './gadgets/common.js'; +import { Provable } from './provable.js'; +import { assert } from '../util/assert.js'; +import type { HashInput } from './types/provable-derivers.js'; +import { field3FromBits } from './gadgets/foreign-field.js'; + +export { Scalar, ScalarConst }; -export { Scalar, ScalarConst, unshift, shift }; - -// internal API -export { constantScalarToBigint }; - -type BoolVar = FieldVar; type ScalarConst = [0, bigint]; -const ScalarConst = { - fromBigint: constFromBigint, - toBigint: constToBigint, - is(x: any): x is ScalarConst { - return Array.isArray(x) && x[0] === 0 && typeof x[1] === 'bigint'; - }, -}; - -let scalarShift = Fq.mod(1n + 2n ** 255n); -let oneHalf = Fq.inverse(2n)!; - -type ConstantScalar = Scalar & { constantValue: ScalarConst }; - /** * Represents a {@link Scalar}. */ -class Scalar { - value: MlArray; - constantValue?: ScalarConst; +class Scalar implements ShiftedScalar { + /** + * We represent a scalar s in shifted form t = s - 2^255 mod q, + * split into its low bit (t & 1) and high 254 bits (t >> 1). + * The reason is that we can efficiently compute the scalar multiplication `(t + 2^255) * P = s * P`. + */ + lowBit: Bool; + high254: Field; static ORDER = Fq.modulus; - private constructor(bits: MlArray, constantValue?: bigint) { - this.value = bits; - constantValue ??= toConstantScalar(bits); - if (constantValue !== undefined) { - this.constantValue = ScalarConst.fromBigint(constantValue); - } + private constructor(lowBit: Bool, high254: Field) { + this.lowBit = lowBit; + this.high254 = high254; } /** @@ -49,20 +42,31 @@ class Scalar { * * If the input is too large, it is reduced modulo the scalar field size. */ - static from(x: Scalar | ScalarConst | bigint | number | string) { - if (x instanceof Scalar) return x; - let x_ = ScalarConst.is(x) ? constToBigint(x) : x; - let scalar = Fq.mod(BigInt(x_)); - let bits = toBits(scalar); - return new Scalar(bits, scalar); + static from(s: Scalar | bigint | number | string): Scalar { + if (s instanceof Scalar) return s; + let t = Fq.mod(BigInt(s) - (1n << 255n)); + let lowBit = new Bool((t & 1n) === 1n); + let high254 = new Field(t >> 1n); + return new Scalar(lowBit, high254); + } + + /** + * Provable method to convert a {@link Field} into a {@link Scalar}. + * + * This is always possible and unambiguous, since the scalar field is larger than the base field. + */ + static fromField(s: Field): Scalar { + let { lowBit, high254 } = fieldToShiftedScalar(s); + return new Scalar(lowBit, high254); } /** * Check whether this {@link Scalar} is a hard-coded constant in the constraint system. * If a {@link Scalar} is constructed outside provable code, it is a constant. */ - isConstant(): this is Scalar & { constantValue: ScalarConst } { - return this.constantValue !== undefined; + isConstant() { + let { lowBit, high254 } = this; + return isConstant(lowBit, high254); } /** @@ -72,32 +76,34 @@ class Scalar { * * See {@link FieldVar} for an explanation of constants vs. variables. */ - toConstant(): ConstantScalar { - if (this.constantValue !== undefined) return this as ConstantScalar; - let [, ...bits] = this.value; - let constBits = bits.map((b) => FieldVar.constant(Snarky.field.readVar(b))); - return new Scalar([0, ...constBits]) as ConstantScalar; + toConstant() { + if (this.isConstant()) return this; + return Provable.toConstant(Scalar, this); } /** * Convert this {@link Scalar} into a bigint */ toBigInt() { - return assertConstant(this, 'toBigInt'); + let { lowBit, high254 } = this.toConstant(); + let t = lowBit.toField().toBigInt() + 2n * high254.toBigInt(); + return Fq.mod(t + (1n << 255n)); } - // TODO: fix this API. we should represent "shifted status" internally and use - // and use shifted Group.scale only if the scalar bits representation is shifted /** - * Creates a data structure from an array of serialized {@link Bool}. - * - * **Warning**: The bits are interpreted as the bits of 2s + 1 + 2^255, where s is the Scalar. + * Creates a Scalar from an array of {@link Bool}. + * This method is provable. */ - static fromBits(bits: Bool[]) { - return Scalar.fromFields([ - ...bits.map((b) => b.toField()), - ...Array(Fq.sizeInBits - bits.length).fill(new Bool(false)), - ]); + static fromBits(bits: Bool[]): Scalar { + let length = bits.length; + checkBitLength('Scalar.fromBits()', length, 255); + + // convert bits to a 3-limb bigint + let sBig = field3FromBits(bits); + + // convert to shifted representation + let { lowBit, high254 } = field3ToShiftedScalar(sBig); + return new Scalar(lowBit, high254); } /** @@ -171,16 +177,6 @@ class Scalar { return Scalar.from(z); } - // TODO don't leak 'shifting' to the user and remove these methods - shift() { - let x = assertConstant(this, 'shift'); - return Scalar.from(shift(x)); - } - unshift() { - let x = assertConstant(this, 'unshift'); - return Scalar.from(unshift(x)); - } - /** * Serialize a Scalar into a Field element plus one bit, where the bit is represented as a Bool. * @@ -213,8 +209,7 @@ class Scalar { * The fields are not constrained to be boolean. */ static toFields(x: Scalar) { - let [, ...bits] = x.value; - return bits.map((b) => new Field(b)); + return [x.lowBit.toField(), x.high254]; } /** @@ -241,8 +236,8 @@ class Scalar { * @return An object where the `fields` key is a {@link Field} array of length 1 created from this {@link Field}. * */ - static toInput(x: Scalar): { packed: [Field, number][] } { - return { packed: Scalar.toFields(x).map((f) => [f, 1]) }; + static toInput(x: Scalar): HashInput { + return { fields: [x.high254], packed: [[x.lowBit.toField(), 1]] }; } /** @@ -260,7 +255,13 @@ class Scalar { * Creates a data structure from an array of serialized {@link Field} elements. */ static fromFields(fields: Field[]): Scalar { - return new Scalar([0, ...fields.map((x) => x.value)]); + assert( + fields.length === 2, + `Scalar.fromFields(): expected 2 fields, got ${fields.length}` + ); + let lowBit = Bool.Unsafe.fromField(fields[0]); + let high254 = fields[1]; + return new Scalar(lowBit, high254); } /** @@ -269,28 +270,18 @@ class Scalar { * Returns the size of this type in {@link Field} elements. */ static sizeInFields(): number { - return Fq.sizeInBits; + return 2; } /** * Part of the {@link Provable} interface. - * - * Does nothing. */ - static check() { - /* It is not necessary to boolean constrain the bits of a scalar for the following - reasons: - - The only provable methods which can be called with a scalar value are - - - if - - assertEqual - - equal - - Group.scale - - The only one of these whose behavior depends on the bit values of the input scalars - is Group.scale, and that function boolean constrains the scalar input itself. + static check(s: Scalar) { + /** + * It is not necessary to constrain the range of high254, because the only provable operation on Scalar + * which relies on that range is scalar multiplication -- which constrains the range itself. */ + return Bool.check(s.lowBit); } // ProvableExtended @@ -322,60 +313,11 @@ class Scalar { // internal helpers -function assertConstant(x: Scalar, name: string) { - return constantScalarToBigint(x, `Scalar.${name}`); -} - -function toConstantScalar([, ...bits]: MlArray): bigint | undefined { - if (bits.length !== Fq.sizeInBits) - throw Error( - `Scalar: expected bits array of length ${Fq.sizeInBits}, got ${bits.length}` - ); - let constantBits = Array(bits.length); - for (let i = 0; i < bits.length; i++) { - let bool = bits[i]; - if (!FieldVar.isConstant(bool)) return undefined; - constantBits[i] = FieldConst.equal(bool[1], FieldConst[1]); - } - let sShifted = SignableFq.fromBits(constantBits); - return shift(sShifted); -} - -function toBits(constantValue: bigint): MlArray { - return [ - 0, - ...SignableFq.toBits(unshift(constantValue)).map((b) => - FieldVar.constant(BigInt(b)) - ), - ]; -} - -/** - * s -> 2s + 1 + 2^255 - */ -function shift(s: bigint) { - return Fq.add(Fq.add(s, s), scalarShift); -} - -/** - * inverse of shift, 2s + 1 + 2^255 -> s - */ -function unshift(s: bigint) { - return Fq.mul(Fq.sub(s, scalarShift), oneHalf); -} - -function constToBigint(x: ScalarConst) { - return x[1]; -} -function constFromBigint(x: bigint): ScalarConst { - return [0, x]; -} - -function constantScalarToBigint(s: Scalar, name: string) { - if (s.constantValue === undefined) - throw Error( - `${name}() is not available in provable code. +function assertConstant(x: Scalar, name: string): bigint { + assert( + x.isConstant(), + `${name}() is not available in provable code. That means it can't be called in a @method or similar environment, and there's no alternative implemented to achieve that.` - ); - return ScalarConst.toBigint(s.constantValue); + ); + return x.toBigInt(); } diff --git a/src/lib/provable/test/foreign-field.unit-test.ts b/src/lib/provable/test/foreign-field.unit-test.ts index c7f228ddaf..fb8872ab13 100644 --- a/src/lib/provable/test/foreign-field.unit-test.ts +++ b/src/lib/provable/test/foreign-field.unit-test.ts @@ -1,23 +1,17 @@ -import { Field, Group } from '../wrapped.js'; -import { ForeignField, createForeignField } from '../foreign-field.js'; +import { Field } from '../wrapped.js'; +import { createForeignField } from '../foreign-field.js'; import { Fq } from '../../../bindings/crypto/finite-field.js'; -import { Pallas } from '../../../bindings/crypto/elliptic-curve.js'; import { expect } from 'expect'; import { bool, equivalentProvable as equivalent, - equivalent as equivalentNonProvable, first, spec, throwError, unit, } from '../../testing/equivalent.js'; import { test, Random } from '../../testing/property.js'; -import { Provable } from '../provable.js'; -import { Circuit, circuitMain } from '../../proof-system/circuit.js'; -import { Scalar } from '../scalar.js'; import { l } from '../gadgets/range-check.js'; -import { assert } from '../gadgets/common.js'; import { ProvablePure } from '../types/provable-intf.js'; // toy example - F_17 @@ -108,83 +102,3 @@ equivalent({ from: [f], to: f })( return ForeignScalar.fromBits(bits); } ); - -// scalar shift in foreign field arithmetic vs in the exponent - -let scalarShift = Fq.mod(1n + 2n ** 255n); -let oneHalf = Fq.inverse(2n)!; - -function unshift(s: ForeignField) { - return s.sub(scalarShift).assertAlmostReduced().mul(oneHalf); -} -function scaleShifted(point: Group, shiftedScalar: Scalar) { - let oneHalfGroup = point.scale(oneHalf); - let shiftGroup = oneHalfGroup.scale(scalarShift); - return oneHalfGroup.scale(shiftedScalar).sub(shiftGroup); -} - -let scalarBigint = Fq.random(); -let pointBigint = Pallas.toAffine(Pallas.scale(Pallas.one, scalarBigint)); - -// perform a "scalar unshift" in foreign field arithmetic, -// then convert to scalar from bits (which shifts it back) and scale a point by the scalar -function main0() { - let ffScalar = Provable.witness( - ForeignScalar.provable, - () => new ForeignScalar(scalarBigint) - ); - let bitsUnshifted = unshift(ffScalar).toBits(); - let scalar = Scalar.fromBits(bitsUnshifted); - - let generator = Provable.witness(Group, () => Group.generator); - let point = generator.scale(scalar); - point.assertEquals(Group(pointBigint)); -} - -// go directly from foreign scalar to scalar and perform a shifted scale -// = same end result as main0 -function main1() { - let ffScalar = Provable.witness( - ForeignScalar.provable, - () => new ForeignScalar(scalarBigint) - ); - let bits = ffScalar.toBits(); - let scalarShifted = Scalar.fromBits(bits); - - let generator = Provable.witness(Group, () => Group.generator); - let point = scaleShifted(generator, scalarShifted); - point.assertEquals(Group(pointBigint)); -} - -// check provable and non-provable versions are correct -main0(); -main1(); -await Provable.runAndCheck(main0); -await Provable.runAndCheck(main1); - -// using foreign field arithmetic should result in much fewer constraints -let { rows: rows0 } = await Provable.constraintSystem(main0); -let { rows: rows1 } = await Provable.constraintSystem(main1); -expect(rows0 + 100).toBeLessThan(rows1); - -// test with proving - -class Main extends Circuit { - @circuitMain - static main() { - main0(); - } -} - -let kp = await Main.generateKeypair(); - -let cs = kp.constraintSystem(); -assert( - cs.length === 1 << 13, - `should have ${cs.length} = 2^13 rows, the smallest supported number` -); - -let proof = await Main.prove([], [], kp); - -let ok = await Main.verify([], kp.verificationKey(), proof); -assert(ok, 'proof should verify'); diff --git a/src/lib/provable/test/group.unit-test.ts b/src/lib/provable/test/group.unit-test.ts index a6d3205b5d..3eba9d1b5e 100644 --- a/src/lib/provable/test/group.unit-test.ts +++ b/src/lib/provable/test/group.unit-test.ts @@ -3,9 +3,27 @@ import { test, Random } from '../../testing/property.js'; import { Provable } from '../provable.js'; import { Poseidon } from '../../../mina-signer/src/poseidon-bigint.js'; import { runAndCheckSync } from '../core/provable-context.js'; +import { Scalar } from '../scalar.js'; +import { Field } from '../field.js'; console.log('group consistency tests'); +test(Random.field, Random.scalar, Random.field, (a, s0, x0, assert) => { + const { + x: x1, + y: { x0: y1 }, + } = Poseidon.hashToGroup([a])!; + const g = Group.from(x1, y1); + + // scale by a scalar + const s = Scalar.from(s0); + runScale(g, s, (g, s) => g.scale(s), assert); + + // scale by a field + const x = Field.from(x0); + runScale(g, x, (g, x) => g.scale(x), assert); +}); + // tests consistency between in- and out-circuit implementations test(Random.field, Random.field, (a, b, assert) => { const { @@ -71,3 +89,31 @@ function run( }); }); } + +function runScale( + g: Group, + s: T, + f: (g1: Group, s: T) => Group, + assert: (b: boolean, message?: string | undefined) => void +) { + let result_out_circuit = f(g, s); + + runAndCheckSync(() => { + let result_in_circuit = f( + Provable.witness(Group, () => g), + Provable.witness(s.constructor as any, (): T => s) + ); + + Provable.asProver(() => { + assert( + result_out_circuit.equals(result_in_circuit).toBoolean(), + `Result for x does not match. g: ${JSON.stringify( + g + )}, s: ${JSON.stringify(s)} + + out_circuit: ${JSON.stringify(result_out_circuit)} + in_circuit: ${JSON.stringify(result_in_circuit)}` + ); + }); + }); +} diff --git a/src/lib/provable/test/scalar.test.ts b/src/lib/provable/test/scalar.test.ts index caf418c362..bf0f717d21 100644 --- a/src/lib/provable/test/scalar.test.ts +++ b/src/lib/provable/test/scalar.test.ts @@ -53,10 +53,10 @@ describe('scalar', () => { }); describe('fromBits', () => { - it('should return a shifted scalar', () => { + it('should return a scalar with the same bigint value', () => { let x = Field.random(); let bits_ = x.toBits(); - let s = Scalar.fromBits(bits_).unshift(); + let s = Scalar.fromBits(bits_); expect(x.toBigInt()).toEqual(s.toBigInt()); }); }); diff --git a/src/lib/provable/test/test-utils.ts b/src/lib/provable/test/test-utils.ts index 0406cc0d2a..208fdccc68 100644 --- a/src/lib/provable/test/test-utils.ts +++ b/src/lib/provable/test/test-utils.ts @@ -1,15 +1,19 @@ import type { FiniteField } from '../../../bindings/crypto/finite-field.js'; -import { ProvableSpec, spec } from '../../testing/equivalent.js'; +import { ProvableSpec, map, spec } from '../../testing/equivalent.js'; import { Random } from '../../testing/random.js'; import { Field3 } from '../gadgets/gadgets.js'; import { assert } from '../gadgets/common.js'; import { Bytes } from '../wrapped-classes.js'; +import { CurveAffine } from '../../../bindings/crypto/elliptic-curve.js'; +import { simpleMapToCurve } from '../gadgets/elliptic-curve.js'; +import { provable } from '../types/struct.js'; export { foreignField, unreducedForeignField, uniformForeignField, bytes, + pointSpec, throwError, }; @@ -62,6 +66,26 @@ function bytes(length: number) { }); } +function pointSpec(field: ProvableSpec, Curve: CurveAffine) { + // point but with independently random components, which will never form a valid point + let pointShape = spec({ + rng: Random.record({ x: field.rng, y: field.rng }), + there({ x, y }) { + return { x: field.there(x), y: field.there(y) }; + }, + back({ x, y }) { + return { x: field.back(x), y: field.back(y), infinity: false }; + }, + provable: provable({ x: field.provable, y: field.provable }), + }); + + // valid random point + let point = map({ from: field, to: pointShape }, (x) => + simpleMapToCurve(x, Curve) + ); + return point; +} + // helper function throwError(message: string): T { diff --git a/src/mina b/src/mina index 898f7596ff..a883196f55 160000 --- a/src/mina +++ b/src/mina @@ -1 +1 @@ -Subproject commit 898f7596ff73817b3f2ee0fc4557ca867816e391 +Subproject commit a883196f55b1a6c7ce41f9f3cc68c5a5e7ae2422 diff --git a/src/snarky.d.ts b/src/snarky.d.ts index e8f72dca55..f86e4d564d 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -347,7 +347,19 @@ declare const Snarky: { }; group: { - scale(p: MlGroup, s: MlArray): MlGroup; + /** + * Computes `(2*s + 1 + 2^numBits) * P` and also returns the bits of s (which are proven correct). + * + * `numBits` must be a multiple of 5, and s must be in the range [0, 2^numBits). + * The [soundness proof](https://github.com/zcash/zcash/issues/3924) assumes + * `numBits <= n - 2` where `n` is the bit length of the scalar field. + * In our case, n=255 so numBits <= 253. + */ + scaleFastUnpack( + P: MlGroup, + shiftedValue: [_: 0, s: FieldVar], + numBits: number + ): MlPair>; }; /** diff --git a/tests/vk-regression/plain-constraint-system.ts b/tests/vk-regression/plain-constraint-system.ts index 17414694e6..740abf170b 100644 --- a/tests/vk-regression/plain-constraint-system.ts +++ b/tests/vk-regression/plain-constraint-system.ts @@ -8,9 +8,10 @@ import { Bytes, Bool, UInt64, + Nullifier, } from 'o1js'; -export { GroupCS, BitwiseCS, HashCS, BasicCS }; +export { GroupCS, BitwiseCS, HashCS, BasicCS, CryptoCS }; const GroupCS = constraintSystem('Group Primitive', { add() { @@ -173,6 +174,16 @@ const BasicCS = constraintSystem('Basic', { }, }); +const CryptoCS = constraintSystem('Crypto', { + nullifier() { + let nullifier = Provable.witness(Nullifier, (): Nullifier => { + throw Error('not implemented'); + }); + let x = Provable.witness(Field, () => Field(0)); + nullifier.verify([x, x, x]); + }, +}); + // mock ZkProgram API for testing function constraintSystem( diff --git a/tests/vk-regression/vk-regression.json b/tests/vk-regression/vk-regression.json index 91d522d0ca..bca6e7274d 100644 --- a/tests/vk-regression/vk-regression.json +++ b/tests/vk-regression/vk-regression.json @@ -50,16 +50,16 @@ } }, "HelloWorld": { - "digest": "1cc4745c7a9a21c6d0be2aa1b9416106227efb91209af60c7fbfb1d3817533d", + "digest": "309ebdd97930d6c753207d62c84147fe540f62d1623a1251f1d0b4f5f8bd400d", "methods": { "update": { - "rows": 585, - "digest": "88c83c391be40dc26611ad774c74a693" + "rows": 475, + "digest": "f1ace9d626cb47fb5612579a1a608fc5" } }, "verificationKey": { - "data": "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", - "hash": "6961633262831992186341985260741050866418324859323346905102769972301543308141" + "data": "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", + "hash": "4671417744577823084023376770685664948710807365406389658915321449520576005042" } }, "TokenContract": { @@ -116,16 +116,16 @@ "digest": "Group Primitive", "methods": { "add": { - "rows": 30, - "digest": "8179f9497cc9b6624912033324c27b6d" + "rows": 29, + "digest": "c9da933b8ee30b9467e4b0abacb503bf" }, "sub": { "rows": 30, - "digest": "ddb709883792aa08b3bdfb69206a9f69" + "digest": "ad4216530ea5b80273e0115825a5dce1" }, "scale": { - "rows": 113, - "digest": "b912611500f01c57177285f538438abc" + "rows": 132, + "digest": "752c83aacb96abdf457a54018fcfda1a" }, "equals": { "rows": 37, @@ -248,6 +248,19 @@ "hash": "" } }, + "Crypto": { + "digest": "Crypto", + "methods": { + "nullifier": { + "rows": 730, + "digest": "aa8ec2538a8d3133af3f2dd7eb682738" + } + }, + "verificationKey": { + "data": "", + "hash": "" + } + }, "ecdsa-only": { "digest": "10f4849c3f8b361abf19c0fedebb445d98f039c137f48e2b130110568e2054a3", "methods": { @@ -288,7 +301,7 @@ } }, "diverse": { - "digest": "3be798c095572431c030159f9692e8f7e1d9549c1c227e3fcec405dde7f6a213", + "digest": "36308476aa4b0f23cef29b266a581832b549576aa604e4275670781feebd74d3", "methods": { "ecdsa": { "rows": 29105, @@ -299,8 +312,8 @@ "digest": "c23e00e466878466ae8ab23bde562792" }, "pallas": { - "rows": 891, - "digest": "e5f666ba87d050daea47081212cec058" + "rows": 469, + "digest": "6ee38bce85ca4c6a852fdbe25bf096a9" }, "poseidon": { "rows": 946, @@ -316,8 +329,8 @@ } }, "verificationKey": { - "data": "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", - "hash": "23745901147458247015601269628927263790388520800344100071721346138178756267260" + "data": "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", + "hash": "12697506505683930771562355619882993515591767596875424801784257474849667200879" } } } \ No newline at end of file diff --git a/tests/vk-regression/vk-regression.ts b/tests/vk-regression/vk-regression.ts index 9af98395a9..fe2c921407 100644 --- a/tests/vk-regression/vk-regression.ts +++ b/tests/vk-regression/vk-regression.ts @@ -13,6 +13,7 @@ import { BitwiseCS, HashCS, BasicCS, + CryptoCS, } from './plain-constraint-system.js'; import { diverse } from './diverse-zk-program.js'; @@ -56,6 +57,7 @@ const ConstraintSystems: MinimumConstraintSystem[] = [ BitwiseCS, HashCS, BasicCS, + CryptoCS, ecdsa, keccakAndEcdsa, SHA256Program,