Skip to content

Commit 1d1a9e7

Browse files
committed
🕶️ Add HumanIDv1 blinding commitments
1 parent 1716b0f commit 1d1a9e7

File tree

5 files changed

+125
-89
lines changed

5 files changed

+125
-89
lines changed

mina/examples/Airdrop.test.ts

+21-59
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import {
22
AccountUpdate,
3-
Bool,
43
Field,
54
MerkleTree,
65
Mina,
76
PrivateKey,
8-
Signature,
97
UInt64
108
} from "o1js";
11-
import { HumanIDWitness, Signatures, authenticate } from "../humanIDv1";
9+
import { HumanIDWitness } from "../humanIDv1";
10+
import { signHumanIDv1, truncateHumanIDv1 } from "../humanIDv1.test";
1211
import { Airdrop } from "./Airdrop";
1312

1413
describe('Example Airdrop zkApp', () => {
@@ -18,26 +17,6 @@ describe('Example Airdrop zkApp', () => {
1817
const sender = senderKey.toPublicKey();
1918
const appKey = PrivateKey.random();
2019
const appAddr = appKey.toPublicKey();
21-
const id1 = Field(1);
22-
const id2 = Field(2);
23-
const privKey1 = PrivateKey.fromBigInt(1n);
24-
const privKey2 = PrivateKey.fromBigInt(2n);
25-
const privKey3 = PrivateKey.fromBigInt(3n);
26-
const sigs = new Signatures({
27-
sig1: Signature.create(privKey1, [Field(100), sender.x.add(sender.isOdd.toField())]),
28-
sig2: Signature.create(privKey2, [Field(100), sender.x.add(sender.isOdd.toField())]),
29-
sig3: Signature.create(privKey3, [Field(100), sender.x.add(sender.isOdd.toField())])
30-
});
31-
const sigs1 = new Signatures({
32-
sig1: Signature.create(privKey1, [id1, sender.x.add(sender.isOdd.toField())]),
33-
sig2: Signature.create(privKey2, [id1, sender.x.add(sender.isOdd.toField())]),
34-
sig3: Signature.create(privKey3, [id1, sender.x.add(sender.isOdd.toField())])
35-
});
36-
const sigs2 = new Signatures({
37-
sig1: Signature.create(privKey1, [id2, sender.x.add(sender.isOdd.toField())]),
38-
sig2: Signature.create(privKey2, [id2, sender.x.add(sender.isOdd.toField())]),
39-
sig3: Signature.create(privKey3, [id2, sender.x.add(sender.isOdd.toField())])
40-
});
4120
let tree: MerkleTree;
4221
let app: Airdrop;
4322

@@ -52,6 +31,13 @@ describe('Example Airdrop zkApp', () => {
5231
local.addAccount(sender, "100000000000");
5332
}));
5433

34+
const getWitnessAndInsert = (humanIDv1Key: bigint) => {
35+
const truncated = truncateHumanIDv1(humanIDv1Key);
36+
const witness = new HumanIDWitness(tree.getWitness(truncated));
37+
tree.setLeaf(truncated, Field(1));
38+
return witness;
39+
}
40+
5541
const fundZkApp = () => Mina.transaction(sender, async () => {
5642
let senderUpdate = AccountUpdate.create(sender);
5743
senderUpdate.requireSignature();
@@ -65,11 +51,6 @@ describe('Example Airdrop zkApp', () => {
6551
}).then((txn) => txn.prove())
6652
.then((txn) => txn.sign([deployerKey, appKey]).send())
6753

68-
it('should verify signatures', () => {
69-
authenticate(id1, sigs1, sender);
70-
authenticate(id2, sigs2, sender);
71-
})
72-
7354
it('should deploy the app and fund it', async () => {
7455
await deploy();
7556
await fundZkApp();
@@ -81,7 +62,7 @@ describe('Example Airdrop zkApp', () => {
8162
await fundZkApp()
8263

8364
await Mina.transaction(sender, () => {
84-
return app.claimReward(Field(100), sigs, new HumanIDWitness(tree.getWitness(100n)));
65+
return app.claimReward(...signHumanIDv1(100n, sender), getWitnessAndInsert(100n));
8566
}).then((txn) => txn.prove())
8667
.then((txn) => txn.sign([senderKey]).send());
8768
});
@@ -91,31 +72,17 @@ describe('Example Airdrop zkApp', () => {
9172
await fundZkApp();
9273

9374
const id1 = 123123123123123123123123123123n;
94-
const truncatedId1 = id1 & 0xFFFFFFFFn;
95-
const sigsTest1 = new Signatures({
96-
sig1: Signature.create(privKey1, [Field(id1), sender.x.add(sender.isOdd.toField())]),
97-
sig2: Signature.create(privKey2, [Field(id1), sender.x.add(sender.isOdd.toField())]),
98-
sig3: Signature.create(privKey3, [Field(id1), sender.x.add(sender.isOdd.toField())])
99-
});
10075
await Mina.transaction(
10176
sender,
102-
() => app.claimReward(Field(id1), sigsTest1, new HumanIDWitness(tree.getWitness(truncatedId1)))
77+
() => app.claimReward(...signHumanIDv1(id1, sender), getWitnessAndInsert(id1))
10378
)
10479
.then((txn) => txn.prove())
10580
.then((txn) => txn.sign([senderKey]).send());
10681

107-
tree.setLeaf(truncatedId1, Field(1));
108-
10982
const id2 = 123123123123123123123123123124n;
110-
const truncatedId2 = id2 & 0xFFFFFFFFn;
111-
const sigsTest2 = new Signatures({
112-
sig1: Signature.create(privKey1, [Field(id2), sender.x.add(sender.isOdd.toField())]),
113-
sig2: Signature.create(privKey2, [Field(id2), sender.x.add(sender.isOdd.toField())]),
114-
sig3: Signature.create(privKey3, [Field(id2), sender.x.add(sender.isOdd.toField())])
115-
});
11683
await Mina.transaction(
11784
sender,
118-
() => app.claimReward(Field(id2), sigsTest2, new HumanIDWitness(tree.getWitness(truncatedId2)))
85+
() => app.claimReward(...signHumanIDv1(id2, sender), getWitnessAndInsert(id2))
11986
)
12087
.then((txn) => txn.prove())
12188
.then((txn) => txn.sign([senderKey]).send());
@@ -125,35 +92,30 @@ describe('Example Airdrop zkApp', () => {
12592
await deploy();
12693
await fundZkApp();
12794

128-
const sigsTest3 = new Signatures({
129-
sig1: Signature.create(privKey1, [Field(123123123123123n), sender.x.add(sender.isOdd.toField())]),
130-
sig2: Signature.create(privKey2, [Field(123123123123123n), sender.x.add(sender.isOdd.toField())]),
131-
sig3: Signature.create(privKey3, [Field(123123123123123n), sender.x.add(sender.isOdd.toField())])
132-
});
133-
await expect(() => Mina.transaction(
95+
const id = 123123123123123123123123123124n;
96+
expect(() => Mina.transaction(
13497
sender,
135-
() => app.claimReward(Field(123123123123123n), sigsTest3, new HumanIDWitness(tree.getWitness(100n)))
98+
() => app.claimReward(...signHumanIDv1(id, sender), getWitnessAndInsert(100n))
13699
)
137100
.then((txn) => txn.prove())
138-
.then((txn) => txn.sign([senderKey]).send())).rejects.toThrow(/does not match the witness/);
101+
.then((txn) => txn.sign([senderKey]).send())).rejects.toThrow(/does not match/);
139102
})
140103

141104
it('should not let double claimReward()', async () => {
142105
await deploy();
143106
await fundZkApp();
144107

108+
const id = 123123123123123123123123123123n;
145109
await Mina.transaction(
146110
sender,
147-
() => app.claimReward(Field(100), sigs, new HumanIDWitness(tree.getWitness(100n)))
111+
() => app.claimReward(...signHumanIDv1(id, sender), getWitnessAndInsert(id))
148112
)
149113
.then((txn) => txn.prove())
150114
.then((txn) => txn.sign([senderKey]).send());
151115

152-
tree.setLeaf(100n, Field(1));
153-
154-
await expect(() => Mina.transaction(
116+
expect(() => Mina.transaction(
155117
sender,
156-
() => app.claimReward(Field(100), sigs, new HumanIDWitness(tree.getWitness(100n)))
118+
() => app.claimReward(...signHumanIDv1(id, sender), getWitnessAndInsert(id))
157119
)
158120
.then((txn) => txn.prove())
159121
.then((txn) => txn.sign([senderKey]).send())).rejects.toThrow(/already exists/);
@@ -167,7 +129,7 @@ describe('Example Airdrop zkApp', () => {
167129

168130
await Mina.transaction(
169131
sender,
170-
() => app.claimReward(Field(100), sigs, new HumanIDWitness(tree.getWitness(100n)))
132+
() => app.claimReward(...signHumanIDv1(100n, sender), getWitnessAndInsert(100n))
171133
)
172134
.then((txn) => txn.prove())
173135
.then((txn) => txn.sign([senderKey]).send());

mina/examples/Airdrop.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import {
1515
const MINA = 1e9;
1616

1717
/**
18-
* Example airdrop zkApp, which gives 10 MINA rewards to the first 1000
19-
* unique humans.
18+
* Example airdrop zkApp, which gives 10 MINA rewards to each unique human.
2019
*/
2120
class Airdrop extends SmartContract {
2221
@state(Field) treeRoot = State<Field>();
@@ -28,11 +27,13 @@ class Airdrop extends SmartContract {
2827

2928
@method async claimReward(
3029
humanIDv1: Field,
30+
commitmentR: Field,
3131
sigs: Signatures,
3232
witness: HumanIDWitness,
3333
) {
34-
acceptHumanIDv1(humanIDv1, sigs, this.treeRoot, witness, this.sender.getAndRequireSignature());
35-
this.send({ to: this.sender.getUnconstrained(), amount: 10 * MINA });
34+
const sender = this.sender.getUnconstrained();
35+
acceptHumanIDv1(sender, humanIDv1, commitmentR, sigs, this.treeRoot, witness);
36+
this.send({ to: sender, amount: 10 * MINA });
3637
}
3738
}
3839

mina/humanIDv1.test.ts

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Field, Poseidon, PrivateKey, PublicKey, Signature } from "o1js";
2+
import { Signatures, authenticate, requireConsistent } from "./humanIDv1";
3+
4+
const Nodes = [
5+
PrivateKey.fromBigInt(1n),
6+
PrivateKey.fromBigInt(2n),
7+
PrivateKey.fromBigInt(3n),
8+
];
9+
10+
const blindingCommit = (sender: PublicKey) => {
11+
const commitmentR = Field.random();
12+
return [
13+
commitmentR,
14+
Poseidon.hash([commitmentR, sender.x.add(sender.isOdd.toField())]),
15+
];
16+
};
17+
18+
const signHumanIDv1 = (
19+
humanIDv1Key: bigint,
20+
sender: PublicKey
21+
): [Field, Field, Signatures] => {
22+
const humanIDv1 = Field(humanIDv1Key);
23+
const [commitmentR, commitment] = blindingCommit(sender);
24+
const sigs = new Signatures({
25+
sig0: Signature.create(Nodes[0], [humanIDv1, commitment]),
26+
sig1: Signature.create(Nodes[1], [humanIDv1, commitment]),
27+
sig2: Signature.create(Nodes[2], [humanIDv1, commitment]),
28+
});
29+
return [humanIDv1, commitmentR, sigs];
30+
};
31+
32+
const truncateHumanIDv1 = (humanIDv1Key: bigint) => humanIDv1Key & 0xffffffffn;
33+
34+
const badSignHumanIDv1 = (
35+
humanIDv1Key: bigint,
36+
sender: PublicKey
37+
): [Field, Field, Signatures] => {
38+
const humanIDv1 = Field(humanIDv1Key);
39+
const [commitmentR, commitment] = blindingCommit(sender);
40+
const sigs = new Signatures({
41+
sig0: Signature.create(Nodes[0], [humanIDv1, commitment]),
42+
sig1: Signature.create(Nodes[1], [humanIDv1, commitment]),
43+
sig2: Signature.create(Nodes[3], [humanIDv1, commitment]),
44+
});
45+
return [humanIDv1, commitmentR, sigs];
46+
};
47+
48+
describe("humanIDv1 SDK tests", () => {
49+
it("should authenticate geniune humanIDv1s and reject others", () => {
50+
const claimant = PrivateKey.fromBigInt(0x1337n).toPublicKey();
51+
expect(() =>
52+
authenticate(claimant, ...signHumanIDv1(100n, claimant))
53+
).not.toThrow();
54+
expect(() =>
55+
authenticate(claimant, ...badSignHumanIDv1(200n, claimant))
56+
).toThrow();
57+
});
58+
59+
it("should accept only matching (humanIDv1, witness) pairs", () => {
60+
const id1 = 98708374501874509283475982345n;
61+
expect(() =>
62+
requireConsistent(Field(id1), Field(truncateHumanIDv1(id1)))
63+
).not.toThrow();
64+
expect(() =>
65+
requireConsistent(Field(id1), Field(truncateHumanIDv1(id1 + 1n)))
66+
).toThrow();
67+
});
68+
});
69+
70+
export { blindingCommit, signHumanIDv1, truncateHumanIDv1 };

mina/humanIDv1.ts

+27-25
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
Field,
33
MerkleWitness,
4+
Poseidon,
45
PrivateKey,
56
PublicKey,
67
Signature,
@@ -9,19 +10,18 @@ import {
910
} from "o1js";
1011

1112
class Signatures extends Struct({
13+
sig0: Signature,
1214
sig1: Signature,
1315
sig2: Signature,
14-
sig3: Signature,
15-
}) {}
16+
}) { }
1617

17-
const node1PrivKey = PrivateKey.fromBigInt(1n);
18-
const node1PublicKey = node1PrivKey.toPublicKey();
19-
const node2PrivKey = PrivateKey.fromBigInt(2n);
20-
const node2PublicKey = node2PrivKey.toPublicKey();
21-
const node3PrivKey = PrivateKey.fromBigInt(3n);
22-
const node3PublicKey = node3PrivKey.toPublicKey();
18+
const Nodes = [
19+
PrivateKey.fromBigInt(1n).toPublicKey(),
20+
PrivateKey.fromBigInt(2n).toPublicKey(),
21+
PrivateKey.fromBigInt(3n).toPublicKey(),
22+
];
2323

24-
class HumanIDWitness extends MerkleWitness(33) {}
24+
class HumanIDWitness extends MerkleWitness(33) { }
2525

2626
const addToMerkleTree = (treeRoot: State<Field>, witness: HumanIDWitness) => {
2727
const currentTreeRoot = treeRoot.getAndRequireEquals();
@@ -33,13 +33,15 @@ const addToMerkleTree = (treeRoot: State<Field>, witness: HumanIDWitness) => {
3333
};
3434

3535
const authenticate = (
36+
claimant: PublicKey,
3637
humanIDv1: Field,
38+
commitmentR: Field,
3739
sigs: Signatures,
38-
claimant: PublicKey
3940
) => {
40-
sigs.sig1.verify(node1PublicKey, [humanIDv1, claimant.x.add(claimant.isOdd.toField())]).assertTrue();
41-
sigs.sig2.verify(node2PublicKey, [humanIDv1, claimant.x.add(claimant.isOdd.toField())]).assertTrue();
42-
sigs.sig3.verify(node3PublicKey, [humanIDv1, claimant.x.add(claimant.isOdd.toField())]).assertTrue();
41+
const commitment = Poseidon.hash([commitmentR, claimant.x.add(claimant.isOdd.toField())]);
42+
sigs.sig0.verify(Nodes[0], [humanIDv1, commitment]).assertTrue();
43+
sigs.sig1.verify(Nodes[1], [humanIDv1, commitment]).assertTrue();
44+
sigs.sig2.verify(Nodes[2], [humanIDv1, commitment]).assertTrue();
4345
};
4446

4547
const EmptyRoot =
@@ -48,34 +50,34 @@ const EmptyRoot =
4850
const Inverse2Exp32 =
4951
Field(0x3fffffffc00000000000000000000000224698fbe706601f8fe037d166d2cf14n);
5052

51-
const requireConsistent = (humanIDv1: Field, truncatedHumanIDv1: Field) => {
52-
humanIDv1
53-
.sub(truncatedHumanIDv1)
54-
.mul(Inverse2Exp32)
55-
.assertLessThan(
56-
(1n << 222n) + 0x224698fc094cf91b992d30edn,
57-
"HumanID does not match the witness"
58-
);
59-
};
53+
const requireConsistent = (humanIDv1: Field, truncatedHumanIDv1: Field) => humanIDv1
54+
.sub(truncatedHumanIDv1)
55+
.mul(Inverse2Exp32)
56+
.assertLessThan(
57+
(1n << 222n) + 0x224698fc094cf91b992d30edn,
58+
"HumanID does not match the witness"
59+
);
6060

6161
const acceptHumanIDv1 = (
62+
claimant: PublicKey,
6263
humanIDv1: Field,
64+
commitmentR: Field,
6365
sigs: Signatures,
6466
treeRoot: State<Field>,
6567
witness: HumanIDWitness,
66-
claimant: PublicKey
6768
) => {
68-
authenticate(humanIDv1, sigs, claimant);
69+
authenticate(claimant, humanIDv1, commitmentR, sigs);
6970
requireConsistent(humanIDv1, witness.calculateIndex());
7071
addToMerkleTree(treeRoot, witness);
7172
};
7273

7374
export {
7475
EmptyRoot,
7576
HumanIDWitness,
77+
Nodes,
7678
Signatures,
7779
acceptHumanIDv1,
7880
addToMerkleTree,
7981
authenticate,
80-
requireConsistent,
82+
requireConsistent
8183
};

mina/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"useDefineForClassFields": false,
2525
},
2626
"include": [
27-
"./examples"
27+
"./examples",
28+
"./"
2829
],
2930
}

0 commit comments

Comments
 (0)