Skip to content

Commit 747d7ef

Browse files
committed
Add score circuit test
1 parent 0bb7976 commit 747d7ef

File tree

4 files changed

+208
-4
lines changed

4 files changed

+208
-4
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
"ethereum-waffle": "^3.4.4",
3939
"ethers": "^5.6.8",
4040
"ffjavascript": "^0.2.54",
41-
"fixed-merkle-tree": "git+https://github.com/r0qs/fixed-merkle-tree.git#multiproof",
41+
"fixed-merkle-tree": "git+https://github.com/r0qs/fixed-merkle-tree.git",
42+
"document-tree": "file:../document-tree",
4243
"hardhat": "^2.9.9",
4344
"hardhat-gas-reporter": "^1.0.8",
4445
"snarkjs": "^0.4.16",

src/utils.js

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,21 @@ const ZERO_VALUE = process.env.ZERO_VALUE || zeroValue("zkcertree")
1616
const randomBN = (length = 32) => BigNumber.from(crypto.randomBytes(length))
1717

1818
// BigNumber to hex string of specified length
19-
const toFixedHex = (number, length = 32) => {
19+
function toFixedHex(number, length = 32) {
2020
const str = number instanceof Buffer ? number.toString('hex') : BigNumber.from(number).toHexString().replace('0x', '')
2121
return '0x' + str.padStart(length * 2, '0')
2222
}
2323

2424
// Convert bigint value into buffer of specified byte length
25-
const toBuffer = (value, length) =>
25+
function toBuffer(value, length) {
2626
Buffer.from(
2727
BigNumber.from(value)
2828
.toHexString()
2929
.slice(2)
3030
.padStart(length * 2, '0'),
3131
'hex',
3232
)
33+
}
3334

3435
async function deploy(contractName, ...args) {
3536
const Factory = await ethers.getContractFactory(contractName)
@@ -74,8 +75,17 @@ async function prepareSolidityCallData(proofData, publicSignals) {
7475

7576
// Converts a bit array to decimal
7677
function bitArrayToDecimal(array) {
78+
const arr = [...array]
7779
// TODO: ensure that array contains only 0 or 1
78-
return parseInt(array.reverse().join(""), 2)
80+
return parseInt(arr.reverse().join(""), 2)
81+
}
82+
83+
function bufferToBigIntField(buf) {
84+
let n = BigNumber.from(buf)
85+
if (n > SCALAR_FIELD_SIZE) {
86+
n = n.mod(SCALAR_FIELD_SIZE);
87+
}
88+
return n.toBigInt()
7989
}
8090

8191
// Returns the zero value of the form: keccak256(string_value) % SCALAR_FIELD_SIZE
@@ -86,15 +96,29 @@ function zeroValue(input) {
8696
return BigNumber.from(hash).mod(SCALAR_FIELD_SIZE).toString()
8797
}
8898

99+
function prepareCertreeProofInputs(certree, credentials) {
100+
const certProofs = []
101+
for (let i = 0; i < credentials.length; i++) {
102+
let p = certree.proof(credentials[i].commitment)
103+
certProofs[i] = {
104+
pathCertreeElements: [...p.pathElements],
105+
pathCertreeIndices: bitArrayToDecimal(p.pathIndices).toString()
106+
}
107+
}
108+
return certProofs
109+
}
110+
89111
module.exports = {
90112
ZERO_VALUE,
91113
MERKLE_TREE_HEIGHT,
92114
SCALAR_FIELD_SIZE,
93115
randomBN,
94116
toFixedHex,
95117
toBuffer,
118+
bufferToBigIntField,
96119
deploy,
97120
buildMerkleTree,
121+
prepareCertreeProofInputs,
98122
generateMerkleProof,
99123
generateMerkleMultiProof,
100124
prepareSolidityCallData,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pragma circom 2.0.4;
2+
3+
include "../../../circuits/score.circom";
4+
5+
component main {public [certreeRoot, requiredTags, weights, result]} = Score(0, 1, 5, 3, 8);

test/circom/score.js

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
const path = require("path")
2+
const { expect } = require("chai")
3+
const { buildEddsa } = require('circomlibjs')
4+
const { utils } = require('ffjavascript')
5+
const wasm_tester = require("circom_tester").wasm
6+
const { MerkleTree } = require('fixed-merkle-tree')
7+
const { DocumentTree, toBuffer } = require('document-tree')
8+
9+
const Poseidon = require('../../src/poseidon')
10+
const {
11+
ZERO_VALUE,
12+
randomBN,
13+
toFixedHex,
14+
prepareCertreeProofInputs,
15+
bufferToBigIntField
16+
} = require("../../src/utils")
17+
18+
CERT_TREE_HEIGHT = 8
19+
CRED_TREE_HEIGHT = 3
20+
21+
describe("Score circuit", function () {
22+
this.timeout(25000)
23+
let circuit, poseidon
24+
25+
// TODO: move to utils
26+
function poseidonHash2(a, b) {
27+
return poseidon.hash([a, b])
28+
}
29+
30+
function poseidonHash(items) {
31+
return poseidon.hash(items)
32+
}
33+
34+
function getNewCertree(leaves = [], tree_height = CERT_TREE_HEIGHT, zero = ZERO_VALUE) {
35+
return new MerkleTree(tree_height, leaves, { hashFunction: poseidonHash2, zeroElement: zero })
36+
}
37+
38+
function prepareCredInputs(n, proofFieldKeys, credtreeHeight, doctrees) {
39+
const m = 1 << credtreeHeight
40+
41+
let fields = []
42+
let pathElements = new Array()
43+
let leafIndices = new Array()
44+
// TODO: validate schema. Fields must be sorted by the schema
45+
proofFieldKeys.sort()
46+
const emptyEntry = Array(m - proofFieldKeys.length).fill([0n, 0n, 0n])
47+
for (let i = 0; i < n; i++) {
48+
fields[i] = new Array()
49+
for (let j = 0; j < proofFieldKeys.length; j++) {
50+
fields[i][j] = new Array()
51+
const leaf = doctrees[i].findLeaf(proofFieldKeys[j])
52+
fields[i][j][0] = bufferToBigIntField(toBuffer(leaf.key()))
53+
fields[i][j][1] = bufferToBigIntField(toBuffer(leaf.value))
54+
fields[i][j][2] = bufferToBigIntField(toBuffer(leaf.salt))
55+
}
56+
// TODO: multi proof by key or name
57+
const multiProof = doctrees[i].multiProof(proofFieldKeys)
58+
59+
expect(MerkleTree.verifyMultiProof(
60+
doctrees[i].root(),
61+
doctrees[i].levels(),
62+
poseidonHash2,
63+
doctrees[i].leafHashes(proofFieldKeys),
64+
multiProof.pathElements,
65+
multiProof.leafIndices
66+
)).to.be.true
67+
68+
const pe = multiProof.pathElements
69+
const li = multiProof.leafIndices
70+
pathElements.push(pe.concat(Array(m - pe.length).fill(0)))
71+
leafIndices.push(li.concat(Array(m - li.length).fill(0)))
72+
fields[i] = fields[i].concat(emptyEntry)
73+
}
74+
75+
return utils.stringifyBigInts({
76+
fields: fields,
77+
pathFieldElements: pathElements,
78+
fieldIndices: leafIndices,
79+
})
80+
}
81+
82+
function createDocumentTree(document) {
83+
let doctree = new DocumentTree({
84+
zero: ZERO_VALUE,
85+
hashFunction: poseidonHash2,
86+
leafHashFunction: poseidonHash
87+
})
88+
doctree.addLeavesFromDocument(document)
89+
return doctree
90+
}
91+
92+
function randomDoc(issuer, subject) {
93+
return {
94+
grade: Math.floor(Math.random() * 100),
95+
tag: Math.random().toString(16).substring(2, 8),
96+
subject: subject,
97+
issuer: issuer,
98+
reference: toFixedHex(randomBN()), // storage chunk reference
99+
timestamp: Math.floor(new Date().getTime() / 1000)
100+
}
101+
}
102+
103+
function generateDocuments(n, issuer, subject) {
104+
let docs = []
105+
for (let i = 0; i < n; i++) {
106+
docs.push(createDocumentTree(randomDoc(issuer, subject)))
107+
}
108+
return docs
109+
}
110+
111+
// FIXME: dry
112+
function createCredential(secret, publicKey, root) {
113+
let credential = { secret, root }
114+
credential.subject = poseidonHash2(eddsa.F.toObject(publicKey[0]), eddsa.F.toObject(publicKey[1]))
115+
credential.commitment = poseidonHash([credential.root, credential.subject, credential.secret])
116+
credential.nullifierHash = poseidonHash([credential.root])
117+
return credential
118+
}
119+
120+
function weightedSum(elements, weights) {
121+
return elements.reduce((sum, e, i) => {
122+
sum += e * weights[i]
123+
return sum
124+
}, 0)
125+
}
126+
127+
before(async () => {
128+
circuit = await wasm_tester(path.join(__dirname, "circuits", "score_test.circom"))
129+
poseidon = await Poseidon.initialize()
130+
eddsa = await buildEddsa()
131+
})
132+
133+
it("should compute the correct score of multiple credential's fields", async () => {
134+
const nCerts = 5
135+
const issuer = toFixedHex(randomBN())
136+
const subject = toFixedHex(randomBN())
137+
const privateKey = toFixedHex(randomBN())
138+
const publicKey = eddsa.prv2pub(privateKey)
139+
140+
const docs = generateDocuments(nCerts, issuer, subject)
141+
const leaves = docs.map(d => d.root())
142+
const certree = getNewCertree(leaves, CERT_TREE_HEIGHT, ZERO_VALUE)
143+
144+
const credentials = []
145+
for (let i = 0; i < nCerts; i++) {
146+
credentials[i] = createCredential(randomBN().toString(), publicKey, docs[i].root())
147+
certree.insert(credentials[i].commitment)
148+
}
149+
150+
const certProofs = prepareCertreeProofInputs(certree, credentials)
151+
const credProofInputs = prepareCredInputs(nCerts, ["tag", "grade"], CRED_TREE_HEIGHT, docs)
152+
const tags = docs.map(d => bufferToBigIntField(toBuffer(d.findLeaf("tag").value)))
153+
const weights = [...Array(nCerts)].map((_, i) => (i % 2) + 1)
154+
const grades = docs.map(d => d.findLeaf("grade").value)
155+
const result = weightedSum(grades, weights)
156+
157+
const inputs = utils.stringifyBigInts({
158+
certreeRoot: certree.root,
159+
requiredTags: tags,
160+
weights: weights,
161+
result: result,
162+
nullifierHashes: credentials.map(c => c.nullifierHash),
163+
credentialRoots: credentials.map(c => c.root),
164+
subjects: credentials.map(c => c.subject),
165+
secrets: credentials.map((c) => c.secret),
166+
pathCertreeElements: certProofs.map(p => p.pathCertreeElements),
167+
pathCertreeIndices: certProofs.map(p => p.pathCertreeIndices),
168+
...credProofInputs
169+
})
170+
171+
const w = await circuit.calculateWitness(inputs, true)
172+
await circuit.checkConstraints(w)
173+
});
174+
})

0 commit comments

Comments
 (0)