Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add nullifier to snarkyjs and mina-signer #882

Merged
merged 63 commits into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
c97f677
update bindings, add hashToCurve
Trivo25 Apr 17, 2023
bec9a16
start implementing native toGroup
Trivo25 Apr 20, 2023
0da3324
remove dead code
Trivo25 Apr 20, 2023
ef4f905
flip curves and add constants
Trivo25 Apr 20, 2023
1a4e4fa
change return type
Trivo25 Apr 21, 2023
51b0103
make naming uniform
Trivo25 Apr 21, 2023
311484e
move group map related code to ec file
Trivo25 Apr 21, 2023
9e51a05
add reference link and rename
Trivo25 Apr 21, 2023
18c14d4
uniform name
Trivo25 Apr 21, 2023
905742b
finish hashToGroup implementation
Trivo25 Apr 21, 2023
f27d4e1
bump bindings
Trivo25 Apr 21, 2023
731656a
return both sqrts of y2
Trivo25 Apr 26, 2023
5fc5160
return both roots of y
Trivo25 Apr 26, 2023
a4835c7
start implementing nullifier
Trivo25 May 1, 2023
884272e
implement nullifier in mina-signer
Trivo25 May 1, 2023
d998170
add types, add to client
Trivo25 May 1, 2023
5781146
change type remote import
Trivo25 May 1, 2023
5333817
Merge remote-tracking branch 'origin/feature/hashToCurve-bindings' in…
Trivo25 May 3, 2023
14b677d
change return type to Group
Trivo25 May 3, 2023
785cb1d
Merge branch 'feature/hashToCurve-bindings' into feature/nullifier
Trivo25 May 4, 2023
882e0de
implement nullifier
Trivo25 May 4, 2023
4dadffb
Merge branch 'feature/nullifier' of https://github.com/o1-labs/snarky…
Trivo25 May 4, 2023
e067397
undo fromsjon
Trivo25 May 4, 2023
b0b7e93
type fromJSON
Trivo25 May 4, 2023
7d33ff8
change Field.fromJSON typing
Trivo25 May 4, 2023
33a1684
bindings
Trivo25 May 4, 2023
fd500f3
cleanup
Trivo25 May 4, 2023
6cd68dd
bump bindings
Trivo25 May 4, 2023
aed7475
changelog and run tests again
Trivo25 May 4, 2023
d248cae
changelog
Trivo25 May 4, 2023
846ae9e
add nullifier contract example
Trivo25 May 5, 2023
9f9b830
add comments, cleanup
Trivo25 May 5, 2023
7442973
add rfc and paper reference
Trivo25 May 5, 2023
de1b245
add some helper methods
Trivo25 May 5, 2023
3d0bc8a
minor comments
Trivo25 May 5, 2023
b27b249
minor
Trivo25 May 5, 2023
9302aed
Merge branch 'main' into feature/nullifier
Trivo25 May 5, 2023
b57ddb5
address feedback
Trivo25 May 5, 2023
391faf8
Merge branch 'feature/nullifier' of https://github.com/o1-labs/snarky…
Trivo25 May 5, 2023
7f4779c
claculate nullifier differently
Trivo25 May 5, 2023
f5012a1
export scaleShifted
Trivo25 May 5, 2023
c3bc6ea
fix example
Trivo25 May 8, 2023
eabc715
serialize pk as x,y not y, x
Trivo25 May 8, 2023
949cfdf
simple tests
Trivo25 May 8, 2023
43ca0ca
bindings
Trivo25 May 8, 2023
5053eea
make message a dynamic array
Trivo25 May 9, 2023
f531a07
reverse order of publickey serialization
Trivo25 May 9, 2023
587843c
bump bindings
Trivo25 May 9, 2023
86531a8
comments
Trivo25 May 9, 2023
d15b06d
bigint to field, random
Trivo25 May 9, 2023
615ed4e
try fix conflic
Trivo25 May 9, 2023
ca83f2c
merge conflict
Trivo25 May 9, 2023
360c813
Merge branch 'main' into feature/nullifier
Trivo25 May 9, 2023
256b9ea
building bindings
Trivo25 May 9, 2023
6adab25
address feedback
Trivo25 May 9, 2023
bec0b6c
doc comments
Trivo25 May 9, 2023
1dd6d0d
assert key
Trivo25 May 9, 2023
d761bd3
doc comments
Trivo25 May 23, 2023
50a3b3c
Merge branch 'main' into feature/nullifier
Trivo25 Jun 13, 2023
7881cb0
bindings and merge conflicts
Trivo25 Jun 13, 2023
e65cc23
build bindings
Trivo25 Jun 13, 2023
98695fe
fix json
Trivo25 Jun 13, 2023
a3c1010
fix bindings (again)
Trivo25 Jun 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased](https://github.com/o1-labs/snarkyjs/compare/bcc666f2...HEAD)

> No unreleased changes yet
### Added

- `Poseidon.hashToGroup` enables hashing to a group https://github.com/o1-labs/snarkyjs/pull/887
- Implemented `Nullifier` as a new primitive https://github.com/o1-labs/snarkyjs/pull/882
- mina-signer can now be used to generate a Nullifier, which can be consumed by zkApps using the newly added Nullifier Struct

## [0.10.0](https://github.com/o1-labs/snarkyjs/compare/97e393ed...bcc666f2)

Expand Down
2 changes: 1 addition & 1 deletion src/bindings
131 changes: 131 additions & 0 deletions src/examples/nullifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import {
PrivateKey,
Nullifier,
Field,
SmartContract,
state,
State,
method,
MerkleMap,
Circuit,
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>();

@method payout(nullifier: Nullifier) {
let nullifierRoot = this.nullifierRoot.getAndAssertEquals();
let nullifierMessage = this.nullifierMessage.getAndAssertEquals();
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved

// making sure the nullifier really belongs to this contract
nullifier.message.assertEquals(nullifierMessage);

// verify the nullifier
nullifier.verify();

let nullifierWitness = Circuit.witness(MerkleMapWitness, () =>
NullifierTree.getWitness(nullifier.key())
);

// we compute the current root and make sure the entry is set to 0 (= unused)
let isUnused = nullifier.isUnused(nullifierWitness, nullifierRoot);
isUnused.assertTrue(
'Nullifier root does not match on-chain root - the nullifier is either incorrect or has already been set!'
);

// we set the nullifier to 1 (= used) and calculate the new root
let newRoot = nullifier.setUsed(nullifierWitness);

// we update the on-chain root
this.nullifierRoot.set(newRoot);

// we pay out a reward
let balance = this.account.balance.getAndAssertEquals();

let halfBalance = balance.div(2);
this.send({ to: nullifier.publicKey, amount: halfBalance });
}
}

const NullifierTree = new MerkleMap();

let Local = Mina.LocalBlockchain({ proofsEnabled: true });
Mina.setActiveInstance(Local);

// a test account that pays all the fees, and puts additional funds into the zkapp
let { privateKey: senderKey, publicKey: sender } = Local.testAccounts[0];

// the zkapp account
let zkappKey = PrivateKey.random();
let zkappAddress = zkappKey.toPublicKey();

// a special account that is allowed to pull out half of the zkapp balance, once
let privilegedKey = PrivateKey.random();
let privilegedAddress = privilegedKey.toPublicKey();

let initialBalance = 10_000_000_000;
let zkapp = new PayoutOnlyOnce(zkappAddress);

// a unique message
let nullifierMessage = Field(5);

console.log('compile');
await PayoutOnlyOnce.compile();

console.log('deploy');
let tx = await Mina.transaction(sender, () => {
let senderUpdate = AccountUpdate.fundNewAccount(sender);
senderUpdate.send({ to: zkappAddress, amount: initialBalance });
zkapp.deploy({ zkappKey });

zkapp.nullifierRoot.set(NullifierTree.getRoot());
zkapp.nullifierMessage.set(nullifierMessage);
});
await tx.prove();
await tx.sign([senderKey]).send();

console.log(`zkapp balance: ${zkapp.account.balance.get().div(1e9)} MINA`);

console.log('generating nullifier');

let jsonNullifier = createNullifier(
nullifierMessage.toBigInt(),
BigInt(privilegedKey.s.toJSON())
);

console.log('pay out');
tx = await Mina.transaction(sender, () => {
AccountUpdate.fundNewAccount(sender);
zkapp.payout(Nullifier.fromJSON(jsonNullifier));
});
await tx.prove();
await tx.sign([senderKey]).send();

console.log(`zkapp balance: ${zkapp.account.balance.get().div(1e9)} MINA`);
console.log(
`user balance: ${Mina.getAccount(privilegedAddress).balance.div(1e9)} MINA`
);

console.log('trying second pay out');

try {
tx = await Mina.transaction(sender, () => {
zkapp.payout(Nullifier.fromJSON(jsonNullifier));
});

await tx.prove();
await tx.sign([senderKey]).send();
} catch (error: any) {
console.log(
'transaction failed, as expected! received the following error message:'
);
console.log(error.message);
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export { Character, CircuitString } from './lib/string.js';
export { MerkleTree, MerkleWitness } from './lib/merkle_tree.js';
export { MerkleMap, MerkleMapWitness } from './lib/merkle_map.js';

export { Nullifier } from './lib/nullifier.js';

// experimental APIs
import { ZkProgram } from './lib/proof_system.js';
import { Callback } from './lib/zkapp.js';
Expand Down
105 changes: 105 additions & 0 deletions src/lib/nullifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import {
Poseidon,
PublicKey,
Field,
Group,
Struct,
MerkleMapWitness,
Scalar,
} from 'snarkyjs';
import type { Nullifier as JsonNullifier } from '../mina-signer/src/TSTypes.js';
import { scaleShifted } from './signature.js';

export { Nullifier };

/**
* RFC: https://github.com/o1-labs/snarkyjs/issues/756
*
* Paper: https://eprint.iacr.org/2022/1255.pdf
*/
class Nullifier extends Struct({
message: Field,
publicKey: PublicKey,
public: {
nullifier: Group,
s: Scalar,
},
private: {
c: Field,
g_r: Group,
h_m_pk_r: Group,
},
}) {
static fromJSON(json: JsonNullifier): Nullifier {
return super.fromJSON(json as any) as Nullifier;
}

/**
* Verifies the correctness of the Nullifier. Throws an error if the Nullifier is incorrect.
*/
verify() {
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
let {
message,
publicKey,
public: { nullifier, s },
private: { c },
} = this;

// generator
let G = Group.generator;

let pk_fields = publicKey.toFields();

let {
x,
y: { x0 },
} = Poseidon.hashToGroup([message, ...pk_fields]);

// see https://github.com/o1-labs/snarkyjs/blob/5333817a62890c43ac1b9cb345748984df271b62/src/lib/signature.ts#L220
let pk_c = scaleShifted(publicKey.toGroup(), Scalar.fromBits(c.toBits()));

// g^r = g^s / pk^c
let g_r = G.scale(s).sub(pk_c);

let h_m_pk = Group.fromFields([x, x0]);

let h_m_pk_s = Group.scale(h_m_pk, 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()))
);

Poseidon.hash([
...Group.toFields(G),
...pk_fields,
x,
x0,
...Group.toFields(nullifier),
...Group.toFields(g_r),
...Group.toFields(h_m_pk_s_div_nullifier_s),
]).assertEquals(c, 'Nullifier does not match private input!');
}

/**
* The key of the nullifier, which belongs to a unique message and a public key.
* Used as an index in Merkle trees.
*/
key() {
return Poseidon.hash(Group.toFields(this.public.nullifier));
}

/**
* Checks if the Nullifier has been used before.
*/
isUnused(witness: MerkleMapWitness, root: Field) {
return witness.computeRootAndKey(Field(0))[0].equals(root);
}
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved

/**
* Sets the Nullifier, returns the new Merkle root.
*/
setUsed(witness: MerkleMapWitness) {
return witness.computeRootAndKey(Field(1))[0];
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
}
}
49 changes: 49 additions & 0 deletions src/lib/nullifier.unit-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { createNullifier } from '../mina-signer/src/nullifier.js';
import { Field } from './core.js';
import { Nullifier } from './nullifier.js';
import { PrivateKey } from './signature.js';

let priv = PrivateKey.random();
let sk = BigInt(priv.s.toJSON());

let jsonNullifier1 = createNullifier(5000n, sk);

let nullifier1 = Nullifier.fromJSON(jsonNullifier1);
nullifier1.verify();

console.log('nullifier correctly deserializes, serializes and verifies');

// random sk that does not belong to a pk
let sk_faulty = BigInt(PrivateKey.random().s.toJSON());

let jsonNullifier2 = createNullifier(5000n, sk_faulty);

// trying to manipulate the nullifier to take a real pk that it doesnt know the sk to
jsonNullifier2.publicKey = priv.toPublicKey().toBase58();

let nullifier2 = Nullifier.fromJSON(jsonNullifier2);
try {
nullifier2.verify();
console.log('incorrect nullifier was verified');
console.log(JSON.stringify(nullifier2));
process.exit(1);
Trivo25 marked this conversation as resolved.
Show resolved Hide resolved
} catch {
console.log('invalid nullifier correctly throws an error (sk not known)');
}

let jsonNullifier3 = createNullifier(5000n, sk);

// trying to manipulate the nullifier to take a different message
jsonNullifier3.message = 5511n;

let nullifier3 = Nullifier.fromJSON(jsonNullifier3);
try {
nullifier3.verify();
console.log('incorrect nullifier was verified');
console.log(JSON.stringify(nullifier3));
process.exit(1);
} catch {
console.log(
'invalid nullifier correctly throws an error (manipulated message)'
);
}
3 changes: 3 additions & 0 deletions src/lib/signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { prefixes } from '../bindings/crypto/constants.js';
// external API
export { PrivateKey, PublicKey, Signature };

// internal API
export { scaleShifted };

/**
* A signing key. You can generate one via {@link PrivateKey.random}.
*/
Expand Down
16 changes: 16 additions & 0 deletions src/mina-signer/MinaSigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
rosettaTransactionToSignedCommand,
} from './src/rosetta.js';
import { sign, Signature, verify } from './src/signature.js';
import { createNullifier } from './src/nullifier.js';

export { Client as default };

Expand Down Expand Up @@ -477,6 +478,21 @@ class Client {
getAccountUpdateMinimumFee(accountUpdates: TransactionJson.AccountUpdate[]) {
return 0.001 * accountUpdates.length;
}

/**
* Creates a nullifier
*
* @param message A unique message that belongs to a specific nullifier
* @param privateKeyBase58 The private key used to create the nullifier
* @returns A nullifier
*/
createNullifier(
message: bigint,
privateKeyBase58: Json.PrivateKey
): Json.Nullifier {
let sk = PrivateKey.fromBase58(privateKeyBase58);
return createNullifier(message, sk);
}
}

function validNonNegative(n: number | string | bigint): string {
Expand Down
21 changes: 21 additions & 0 deletions src/mina-signer/src/TSTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { SignatureJson } from './signature.js';
export type UInt32 = number | bigint | string;
export type UInt64 = number | bigint | string;

export type Field = number | bigint | string;

export type PublicKey = string;
export type PrivateKey = string;
export type Signature = SignatureJson;
Expand Down Expand Up @@ -68,3 +70,22 @@ export type Signed<T> = {
};

export type SignedAny = SignedLegacy<SignableData> | Signed<ZkappCommand>;

export type Group = {
x: Field;
y: Field;
};

export type Nullifier = {
message: Field;
publicKey: PublicKey;
public: {
nullifier: Group;
s: Field;
};
private: {
c: Field;
g_r: Group;
h_m_pk_r: Group;
};
};
Loading