diff --git a/CHANGELOG.md b/CHANGELOG.md index 81eb525462..9df9bec662 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased](https://github.com/o1-labs/snarkyjs/compare/161b69d602...HEAD) -> No unreleased changes yet +### Added + +- Added a method `createTestNullifier` to the Nullifier class for testing purposes. It is recommended to use mina-signer to create Nullifiers in production, since it does not leak the private key of the user. The `Nullifier.createTestNullifier` method requires the private key as an input _outside of the users wallet_. https://github.com/o1-labs/snarkyjs/pull/1026 +- Added `field.isEven` to check if a Field element is odd or even. https://github.com/o1-labs/snarkyjs/pull/1026 ## [0.12.0](https://github.com/o1-labs/snarkyjs/compare/eaa39dca0...161b69d602) diff --git a/src/examples/nullifier.ts b/src/examples/nullifier.ts index 6d4ffd9a14..0df3e23d71 100644 --- a/src/examples/nullifier.ts +++ b/src/examples/nullifier.ts @@ -11,12 +11,8 @@ import { MerkleMapWitness, Mina, AccountUpdate, - Poseidon, - Scalar, } from 'snarkyjs'; -import { createNullifier } from '../mina-signer/src/nullifier.js'; - class PayoutOnlyOnce extends SmartContract { @state(Field) nullifierRoot = State(); @state(Field) nullifierMessage = State(); @@ -91,10 +87,11 @@ console.log(`zkapp balance: ${zkapp.account.balance.get().div(1e9)} MINA`); console.log('generating nullifier'); -let jsonNullifier = createNullifier( - [nullifierMessage.toBigInt()], - BigInt(privilegedKey.s.toJSON()) +let jsonNullifier = Nullifier.createTestNullifier( + [nullifierMessage], + privilegedKey ); +console.log(jsonNullifier); console.log('pay out'); tx = await Mina.transaction(sender, () => { diff --git a/src/lib/field.ts b/src/lib/field.ts index ee4b9dc66e..7341729d9e 100644 --- a/src/lib/field.ts +++ b/src/lib/field.ts @@ -376,6 +376,49 @@ class Field { return this.add(Field.from(y).neg()); } + /** + * Checks if this {@link Field} is even. Returns `true` for even elements and `false` for odd elements. + * + * @example + * ```ts + * let a = Field(5); + * a.isEven(); // false + * a.isEven().assertTrue(); // throws, as expected! + * + * let b = Field(4); + * b.isEven(); // true + * b.isEven().assertTrue(); // does not throw, as expected! + * ``` + */ + isEven() { + if (this.isConstant()) return new Bool(this.toBigInt() % 2n === 0n); + + let [, isOddVar, xDiv2Var] = Snarky.exists(2, () => { + let bits = Fp.toBits(this.toBigInt()); + let isOdd = bits.shift()! ? 1n : 0n; + + return [ + 0, + FieldConst.fromBigint(isOdd), + FieldConst.fromBigint(Fp.fromBits(bits)), + ]; + }); + + let isOdd = new Field(isOddVar); + let xDiv2 = new Field(xDiv2Var); + + // range check for 253 bits + // WARNING: this makes use of a special property of the Pasta curves, + // namely that a random field element is < 2^254 with overwhelming probability + // TODO use 88-bit RCs to make this more efficient + xDiv2.toBits(253); + + // check composition + xDiv2.mul(2).add(isOdd).assertEquals(this); + + return new Bool(isOddVar); + } + /** * Multiply another "field-like" value with this {@link Field} element. * diff --git a/src/lib/nullifier.ts b/src/lib/nullifier.ts index 30bc975aee..0b4c838751 100644 --- a/src/lib/nullifier.ts +++ b/src/lib/nullifier.ts @@ -3,7 +3,8 @@ import { Struct } from './circuit_value.js'; import { Field, Group, Scalar } from './core.js'; import { Poseidon } from './hash.js'; import { MerkleMapWitness } from './merkle_map.js'; -import { PublicKey, scaleShifted } from './signature.js'; +import { PrivateKey, PublicKey, scaleShifted } from './signature.js'; +import { Provable } from './provable.js'; export { Nullifier }; @@ -61,6 +62,10 @@ class Nullifier extends Struct({ x, y: { x0 }, } = Poseidon.hashToGroup([...message, ...pk_fields]); + + // check to prevent the prover from using the second square root and forging a non-unique nullifier + x0.isEven().assertTrue(); + let h_m_pk = Group.fromFields([x, x0]); // shifted scalar see https://github.com/o1-labs/snarkyjs/blob/5333817a62890c43ac1b9cb345748984df271b62/src/lib/signature.ts#L220 @@ -165,4 +170,64 @@ class Nullifier extends Struct({ getPublicKey() { return PublicKey.fromGroup(this.publicKey); } + + /** + * + * _Note_: This is *not* the recommended way to create a Nullifier in production. Please use mina-signer to create Nullifiers. + * Also, this function cannot be run within provable code to avoid unintended creations of Nullifiers - a Nullifier should never be created inside proveable code (e.g. a smart contract) directly, but rather created inside the users wallet (or other secure enclaves, so the private key never leaves that enclave). + * + * PLUME: An ECDSA Nullifier Scheme for Unique + * Pseudonymity within Zero Knowledge Proofs + * https://eprint.iacr.org/2022/1255.pdf chapter 3 page 14 + */ + static createTestNullifier(message: Field[], sk: PrivateKey): JsonNullifier { + if (Provable.inCheckedComputation()) { + throw Error( + 'This function cannot not be run within provable code. If you want to create a Nullifier, run this method outside provable code or use mina-signer to do so.' + ); + } + const Hash2 = Poseidon.hash; + const Hash = Poseidon.hashToGroup; + + const pk = sk.toPublicKey().toGroup(); + + const G = Group.generator; + + const r = Scalar.random(); + + const gm = Hash([...message, ...Group.toFields(pk)]); + + const h_m_pk = Group({ x: gm.x, y: gm.y.x0 }); + + const nullifier = h_m_pk.scale(sk.toBigInt()); + const h_m_pk_r = h_m_pk.scale(r.toBigInt()); + + const g_r = G.scale(r.toBigInt()); + + const c = Hash2([ + ...Group.toFields(G), + ...Group.toFields(pk), + ...Group.toFields(h_m_pk), + ...Group.toFields(nullifier), + ...Group.toFields(g_r), + ...Group.toFields(h_m_pk_r), + ]); + + // operations on scalars (r) should be in Fq, rather than Fp + // while c is in Fp (due to Poseidon.hash), c needs to be handled as an element from Fq + const s = r.add(sk.s.mul(Scalar.from(c.toBigInt()))); + + return { + publicKey: pk.toJSON(), + private: { + c: c.toString(), + g_r: g_r.toJSON(), + h_m_pk_r: h_m_pk_r.toJSON(), + }, + public: { + nullifier: nullifier.toJSON(), + s: s.toJSON(), + }, + }; + } } diff --git a/src/mina-signer/src/nullifier.ts b/src/mina-signer/src/nullifier.ts index 9416069211..5d6f7c5c6d 100644 --- a/src/mina-signer/src/nullifier.ts +++ b/src/mina-signer/src/nullifier.ts @@ -1,4 +1,4 @@ -import { Fp, Fq } from '../../bindings/crypto/finite_field.js'; +import { Fq } from '../../bindings/crypto/finite_field.js'; import { Poseidon } from '../../bindings/crypto/poseidon.js'; import { Group,