Skip to content

Commit

Permalink
Merge pull request #1026 from o1-labs/feature/create-nullifier
Browse files Browse the repository at this point in the history
Nullifier improvements
  • Loading branch information
Trivo25 authored Jul 13, 2023
2 parents 79a90a2 + b597544 commit 4790cb6
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 10 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
11 changes: 4 additions & 7 deletions src/examples/nullifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Field>();
@state(Field) nullifierMessage = State<Field>();
Expand Down Expand Up @@ -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, () => {
Expand Down
43 changes: 43 additions & 0 deletions src/lib/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
67 changes: 66 additions & 1 deletion src/lib/nullifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
},
};
}
}
2 changes: 1 addition & 1 deletion src/mina-signer/src/nullifier.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down

0 comments on commit 4790cb6

Please sign in to comment.