From c38adacfba95b97f582f1bd6ebddfa17f09d469b Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 30 Jan 2023 21:40:38 +0100 Subject: [PATCH 1/9] make in-snark signatures compatible --- src/lib/signature.ts | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/lib/signature.ts b/src/lib/signature.ts index 8072c7658f..f93d3d8517 100644 --- a/src/lib/signature.ts +++ b/src/lib/signature.ts @@ -1,6 +1,8 @@ -import { Group, Field, Bool, Scalar, Ledger, Circuit } from '../snarky.js'; +import { Group, Field, Bool, Scalar, Ledger } from '../snarky.js'; import { prop, CircuitValue, AnyConstructor } from './circuit_value.js'; -import { Poseidon } from './hash.js'; +import { hashWithPrefix } from './hash.js'; +import { Signature as SignatureBigint } from '../mina-signer/src/signature.js'; +import { prefixes } from '../js_crypto/constants.js'; // external API export { PrivateKey, PublicKey, Signature }; @@ -192,12 +194,16 @@ class Signature extends CircuitValue { static create(privKey: PrivateKey, msg: Field[]): Signature { const publicKey = PublicKey.fromPrivateKey(privKey).toGroup(); const d = privKey.s; + // TODO: derive nonce deterministically const kPrime = Scalar.random(); let { x: r, y: ry } = Group.generator.scale(kPrime); const k = ry.toBits()[0].toBoolean() ? kPrime.neg() : kPrime; - const e = Scalar.fromBits( - Poseidon.hash(msg.concat([publicKey.x, publicKey.y, r])).toBits() + let h = hashWithPrefix( + prefixes.signatureTestnet, + msg.concat([publicKey.x, publicKey.y, r]) ); + // FIXME: this changes the value! + const e = Scalar.fromBits(h.toBits()); const s = e.mul(d).add(k); return new Signature(r, s); } @@ -208,10 +214,27 @@ class Signature extends CircuitValue { */ verify(publicKey: PublicKey, msg: Field[]): Bool { const point = publicKey.toGroup(); - let e = Scalar.fromBits( - Poseidon.hash(msg.concat([point.x, point.y, this.r])).toBits() + let h = hashWithPrefix( + prefixes.signatureTestnet, + msg.concat([point.x, point.y, this.r]) ); + // FIXME: this changes the value! + let e = Scalar.fromBits(h.toBits()); let r = point.scale(e).neg().add(Group.generator.scale(this.s)); return Bool.and(r.x.equals(this.r), r.y.toBits()[0].equals(false)); } + + // TODO: doccomments + static fromBase58(signatureBase58: string) { + let { r, s } = SignatureBigint.fromBase58(signatureBase58); + return Signature.fromObject({ + r: Field(r), + s: Scalar.fromJSON(s.toString()), + }); + } + toBase58() { + let r = this.r.toBigInt(); + let s = BigInt(this.s.toJSON()); + return SignatureBigint.toBase58({ r, s }); + } } From 3ed2c248bdf5bf6be02bd8d17d66b3e8f35b4141 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 30 Jan 2023 21:42:22 +0100 Subject: [PATCH 2/9] sign fields in mina-signer --- src/mina-signer/MinaSigner.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/mina-signer/MinaSigner.ts b/src/mina-signer/MinaSigner.ts index ec99b8a613..ea46318674 100644 --- a/src/mina-signer/MinaSigner.ts +++ b/src/mina-signer/MinaSigner.ts @@ -23,6 +23,7 @@ import { publicKeyToHex, rosettaTransactionToSignedCommand, } from './src/rosetta.js'; +import { sign, Signature } from './src/signature.js'; export { Client as default }; @@ -96,6 +97,33 @@ class Client { return PublicKey.toBase58(publicKey); } + /** + * Signs an arbitrary list of field elements in a SNARK-compatible way. + * The resulting signature can be verified in SnarkyJS as follows: + * ```ts + * // sign field elements with mina-signer + * let signed = client.signFieldElements(fields, privateKey); + * + * // read signature in snarkyjs and verify + * let signature = Signature.fromBase58(signed.signature); + * let isValid: Bool = signature.verify(publicKey, fields.map(Field)); + * ``` + * + * @param fields An arbitrary list of field elements + * @param privateKey The private key used for signing + * @returns The signed field elements + */ + signFields(fields: bigint[], privateKey: Json.PrivateKey): Signed { + let privateKey_ = PrivateKey.fromBase58(privateKey); + let signature = sign({ fields }, privateKey_, 'testnet'); + return { + signature: Signature.toBase58(signature), + publicKey: PublicKey.toBase58(PrivateKey.toPublicKey(privateKey_)), + data: fields, + }; + } + // TODO verifyFields + /** * Signs an arbitrary message * From 4b78b566e56c9c48417580ca05bd43dd72f01cc3 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 30 Jan 2023 21:42:40 +0100 Subject: [PATCH 3/9] test for compatible signatures (failing) --- .../tests/verify-in-snark.unit-test.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/mina-signer/tests/verify-in-snark.unit-test.ts diff --git a/src/mina-signer/tests/verify-in-snark.unit-test.ts b/src/mina-signer/tests/verify-in-snark.unit-test.ts new file mode 100644 index 0000000000..6822a6c4c8 --- /dev/null +++ b/src/mina-signer/tests/verify-in-snark.unit-test.ts @@ -0,0 +1,36 @@ +import { Field, isReady, shutdown } from '../../snarky.js'; +import { ZkProgram } from '../../lib/proof_system.js'; +import Client from '../MinaSigner.js'; +import { PrivateKey, Signature } from '../../lib/signature.js'; +import { provablePure } from '../../lib/circuit_value.js'; + +await isReady; +let fields = [10n, 20n, 30n, 340817401n, 2091283n, 1n, 0n]; +let privateKey = 'EKENaWFuAiqktsnWmxq8zaoR8bSgVdscsghJE5tV6hPoNm8qBKWM'; + +// sign with mina-signer +let client = new Client({ network: 'mainnet' }); +let signed = client.signFields(fields, privateKey); + +// verify out-of-snark with snarkyjs +let publicKey = PrivateKey.fromBase58(privateKey).toPublicKey(); +let message = fields.map(Field); +let signature = Signature.fromBase58(signed.signature); +signature.verify(publicKey, message).assertTrue(); + +// verify in-snark with snarkyjs +const MyProgram = ZkProgram({ + publicInput: provablePure(null), + methods: { + verifySignature: { + privateInputs: [Signature], + method(_: null, signature: Signature) { + signature.verify(publicKey, message).assertTrue(); + }, + }, + }, +}); +let proof = await MyProgram.verifySignature(null, signature); +await MyProgram.verify(proof); + +shutdown(); From c47db0f8d40f5238270bcd7cdad9e028d49ca8aa Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 31 Jan 2023 09:56:24 +0100 Subject: [PATCH 4/9] fix --- src/lib/account_update.ts | 6 +++++- src/snarky.d.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/account_update.ts b/src/lib/account_update.ts index 0f35b3459d..4069cbd6f7 100644 --- a/src/lib/account_update.ts +++ b/src/lib/account_update.ts @@ -1877,7 +1877,11 @@ function signJsonTransaction( accountUpdate.authorization.proof === null ) { zkappCommand = JSON.parse( - Ledger.signAccountUpdate(JSON.stringify(zkappCommand), privateKey, i) + Ledger.signOtherAccountUpdate( + JSON.stringify(zkappCommand), + privateKey, + i + ) ); } } diff --git a/src/snarky.d.ts b/src/snarky.d.ts index fe1500ecf9..cd99d3f827 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -1274,7 +1274,7 @@ declare class Ledger { /** * Signs an account update. */ - static signAccountUpdate( + static signOtherAccountUpdate( txJson: string, privateKey: { s: Scalar }, i: number From b24051154101f9be5d6004298bc623f7d84923c9 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 31 Jan 2023 13:59:17 +0100 Subject: [PATCH 5/9] add Scalar.fromBigInt --- src/lib/core.ts | 7 +++++++ src/snarky.d.ts | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/lib/core.ts b/src/lib/core.ts index 0cb039d1a2..bf63e56365 100644 --- a/src/lib/core.ts +++ b/src/lib/core.ts @@ -2,6 +2,8 @@ import { bytesToBigInt } from '../js_crypto/bigint-helpers.js'; import { defineBinable } from '../provable/binable.js'; import { sizeInBits } from '../provable/field-bigint.js'; import { Bool, Field, Scalar, Group } from '../snarky.js'; +import { Scalar as ScalarBigint } from '../provable/curve-bigint.js'; +import { mod } from '../js_crypto/finite_field.js'; export { Field, Bool, Scalar, Group }; @@ -72,3 +74,8 @@ That means it can't be called in a @method or similar environment, and there's n highBit: Bool(x >> lowBitSize === 1n), }; }; + +Scalar.fromBigInt = function (scalar: bigint) { + scalar = mod(scalar, ScalarBigint.modulus); + return Scalar.fromJSON(scalar.toString()); +}; diff --git a/src/snarky.d.ts b/src/snarky.d.ts index cd99d3f827..d406515926 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -990,6 +990,11 @@ declare class Scalar { * This operation does NOT affect the circuit and can't be used to prove anything about the string representation of the Scalar. */ static fromJSON(x: string | number | boolean): Scalar; + /** + * Create a constant {@link Scalar} from a bigint. + * If the bigint is too large, it is reduced modulo the scalar field order. + */ + static fromBigInt(s: bigint): Scalar; static check(x: Scalar): void; } From 027da35dfdfe384a14748507f0f31cf6f42950fb Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 31 Jan 2023 14:00:56 +0100 Subject: [PATCH 6/9] fix snarkyjs signatures which used shifted scalar --- src/lib/signature.ts | 51 +++++++++++++++++++++++++++----- src/mina-signer/src/signature.ts | 1 + 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/lib/signature.ts b/src/lib/signature.ts index f93d3d8517..0e6ad1f480 100644 --- a/src/lib/signature.ts +++ b/src/lib/signature.ts @@ -1,7 +1,11 @@ import { Group, Field, Bool, Scalar, Ledger } from '../snarky.js'; import { prop, CircuitValue, AnyConstructor } from './circuit_value.js'; import { hashWithPrefix } from './hash.js'; -import { Signature as SignatureBigint } from '../mina-signer/src/signature.js'; +import { + deriveNonce, + Signature as SignatureBigint, +} from '../mina-signer/src/signature.js'; +import { Scalar as ScalarBigint } from '../provable/curve-bigint.js'; import { prefixes } from '../js_crypto/constants.js'; // external API @@ -194,16 +198,23 @@ class Signature extends CircuitValue { static create(privKey: PrivateKey, msg: Field[]): Signature { const publicKey = PublicKey.fromPrivateKey(privKey).toGroup(); const d = privKey.s; - // TODO: derive nonce deterministically - const kPrime = Scalar.random(); + const kPrime = Scalar.fromBigInt( + deriveNonce( + { fields: msg.map((f) => f.toBigInt()) }, + { x: publicKey.x.toBigInt(), y: publicKey.y.toBigInt() }, + BigInt(d.toJSON()), + 'testnet' + ) + ); let { x: r, y: ry } = Group.generator.scale(kPrime); const k = ry.toBits()[0].toBoolean() ? kPrime.neg() : kPrime; let h = hashWithPrefix( prefixes.signatureTestnet, msg.concat([publicKey.x, publicKey.y, r]) ); - // FIXME: this changes the value! - const e = Scalar.fromBits(h.toBits()); + // 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); return new Signature(r, s); } @@ -218,13 +229,16 @@ class Signature extends CircuitValue { prefixes.signatureTestnet, msg.concat([point.x, point.y, this.r]) ); - // FIXME: this changes the value! + // 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 = point.scale(e).neg().add(Group.generator.scale(this.s)); + let r = scaleShifted(point, e).neg().add(Group.generator.scale(this.s)); return Bool.and(r.x.equals(this.r), r.y.toBits()[0].equals(false)); } - // TODO: doccomments + /** + * Decodes a base58 encoded signature into a {@link Signature}. + */ static fromBase58(signatureBase58: string) { let { r, s } = SignatureBigint.fromBase58(signatureBase58); return Signature.fromObject({ @@ -232,9 +246,30 @@ class Signature extends CircuitValue { s: Scalar.fromJSON(s.toString()), }); } + /** + * Encodes a {@link Signature} in base58 format. + */ toBase58() { let r = this.r.toBigInt(); let s = BigInt(this.s.toJSON()); 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.fromBigInt(oneHalf)); + let shiftGroup = oneHalfGroup.scale(Scalar.fromBigInt(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.fromBigInt(shift)) + .mul(Scalar.fromBigInt(oneHalf)); +} + +let shift = ScalarBigint(1n + 2n ** 255n); +let oneHalf = ScalarBigint.inverse(2n)!; diff --git a/src/mina-signer/src/signature.ts b/src/mina-signer/src/signature.ts index 425fe9ebdb..cc57075295 100644 --- a/src/mina-signer/src/signature.ts +++ b/src/mina-signer/src/signature.ts @@ -37,6 +37,7 @@ export { NetworkId, signLegacy, verifyLegacy, + deriveNonce, }; const networkIdMainnet = 0x01n; From 4527eb6504491fb57b71b90f6d846aaa5763199e Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 31 Jan 2023 14:17:28 +0100 Subject: [PATCH 7/9] implement verification --- src/mina-signer/MinaSigner.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/mina-signer/MinaSigner.ts b/src/mina-signer/MinaSigner.ts index ea46318674..fb7dbb454d 100644 --- a/src/mina-signer/MinaSigner.ts +++ b/src/mina-signer/MinaSigner.ts @@ -23,7 +23,7 @@ import { publicKeyToHex, rosettaTransactionToSignedCommand, } from './src/rosetta.js'; -import { sign, Signature } from './src/signature.js'; +import { sign, Signature, verify } from './src/signature.js'; export { Client as default }; @@ -122,7 +122,22 @@ class Client { data: fields, }; } - // TODO verifyFields + + /** + * Verifies a signature created by {@link signFields}. + * + * @param signedFields The signed field elements + * @returns True if the `signedFields` contains a valid signature matching + * the fields and publicKey. + */ + verifyFields({ data, signature, publicKey }: Signed) { + return verify( + Signature.fromBase58(signature), + { fields: data }, + PublicKey.fromBase58(publicKey), + 'testnet' + ); + } /** * Signs an arbitrary message @@ -142,7 +157,7 @@ class Client { } /** - * Verifies that a signature matches a message. + * Verifies a signature created by {@link signMessage}. * * @param signedMessage A signed message * @returns True if the `signedMessage` contains a valid signature matching From a68a910dc22bc3e4b257625764fabad10d6d546f Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 31 Jan 2023 14:18:44 +0100 Subject: [PATCH 8/9] finish unit test --- .../tests/verify-in-snark.unit-test.ts | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/mina-signer/tests/verify-in-snark.unit-test.ts b/src/mina-signer/tests/verify-in-snark.unit-test.ts index 6822a6c4c8..3fb159c61e 100644 --- a/src/mina-signer/tests/verify-in-snark.unit-test.ts +++ b/src/mina-signer/tests/verify-in-snark.unit-test.ts @@ -3,8 +3,8 @@ import { ZkProgram } from '../../lib/proof_system.js'; import Client from '../MinaSigner.js'; import { PrivateKey, Signature } from '../../lib/signature.js'; import { provablePure } from '../../lib/circuit_value.js'; +import { expect } from 'expect'; -await isReady; let fields = [10n, 20n, 30n, 340817401n, 2091283n, 1n, 0n]; let privateKey = 'EKENaWFuAiqktsnWmxq8zaoR8bSgVdscsghJE5tV6hPoNm8qBKWM'; @@ -12,11 +12,21 @@ let privateKey = 'EKENaWFuAiqktsnWmxq8zaoR8bSgVdscsghJE5tV6hPoNm8qBKWM'; let client = new Client({ network: 'mainnet' }); let signed = client.signFields(fields, privateKey); +// verify with mina-signer +let ok = client.verifyFields(signed); +expect(ok).toEqual(true); + +// sign with snarkyjs and check that we get the same signature +await isReady; +let fieldsSnarky = fields.map(Field); +let privateKeySnarky = PrivateKey.fromBase58(privateKey); +let signatureSnarky = Signature.create(privateKeySnarky, fieldsSnarky); +expect(signatureSnarky.toBase58()).toEqual(signed.signature); + // verify out-of-snark with snarkyjs -let publicKey = PrivateKey.fromBase58(privateKey).toPublicKey(); -let message = fields.map(Field); +let publicKey = privateKeySnarky.toPublicKey(); let signature = Signature.fromBase58(signed.signature); -signature.verify(publicKey, message).assertTrue(); +signature.verify(publicKey, fieldsSnarky).assertTrue(); // verify in-snark with snarkyjs const MyProgram = ZkProgram({ @@ -25,12 +35,15 @@ const MyProgram = ZkProgram({ verifySignature: { privateInputs: [Signature], method(_: null, signature: Signature) { - signature.verify(publicKey, message).assertTrue(); + signature.verify(publicKey, fieldsSnarky).assertTrue(); }, }, }, }); + +await MyProgram.compile(); let proof = await MyProgram.verifySignature(null, signature); -await MyProgram.verify(proof); +ok = await MyProgram.verify(proof); +expect(ok).toEqual(true); shutdown(); From 1249cc20b6fdfcd91efba3a37bbb8f6f14a2c6af Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 31 Jan 2023 14:29:28 +0100 Subject: [PATCH 9/9] changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f80e90d349..44962087ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Transaction.fromJSON` to recover transaction object from JSON https://github.com/o1-labs/snarkyjs/pull/705 +### Changed + +- BREAKING CHANGE: Modify signature algorithm used by `Signature.{create,verify}` to be compatible with mina-signer https://github.com/o1-labs/snarkyjs/pull/710 + - Signatures created with mina-signer's `client.signFields()` can now be verified inside a SNARK! + - Breaks existing deployed smart contracts which use `Signature.verify()` + ## [0.8.0](https://github.com/o1-labs/snarkyjs/compare/d880bd6e...c5a36207) ### Added