Skip to content

Commit 9db752d

Browse files
authored
feat(contracts): new zktrie verifier (#1017)
1 parent 2ec984f commit 9db752d

8 files changed

+2020
-396
lines changed

circomlib.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
declare module "circomlib/src/evmasm";
12
declare module "circomlib/src/poseidon_gencontract";
3+
declare module "circomlib/src/poseidon_constants";

integration-test/PoseidonHash.spec.ts

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/* eslint-disable node/no-missing-import */
2+
/* eslint-disable node/no-unpublished-import */
3+
import { expect } from "chai";
4+
import { randomBytes } from "crypto";
5+
import { BigNumber, Contract } from "ethers";
6+
import { ethers } from "hardhat";
7+
import fs from "fs";
8+
9+
import PoseidonWithoutDomain from "circomlib/src/poseidon_gencontract";
10+
import { generateABI, createCode } from "../scripts/poseidon";
11+
12+
describe("PoseidonHash.spec", async () => {
13+
// test against with circomlib's implementation.
14+
context("domain = zero", async () => {
15+
let poseidonCircom: Contract;
16+
let poseidon: Contract;
17+
18+
beforeEach(async () => {
19+
const [deployer] = await ethers.getSigners();
20+
21+
const PoseidonWithoutDomainFactory = new ethers.ContractFactory(
22+
PoseidonWithoutDomain.generateABI(2),
23+
PoseidonWithoutDomain.createCode(2),
24+
deployer
25+
);
26+
poseidonCircom = await PoseidonWithoutDomainFactory.deploy();
27+
await poseidonCircom.deployed();
28+
29+
const PoseidonWithDomainFactory = new ethers.ContractFactory(generateABI(2), createCode(2), deployer);
30+
poseidon = await PoseidonWithDomainFactory.deploy();
31+
await poseidon.deployed();
32+
});
33+
34+
it("should succeed on zero inputs", async () => {
35+
expect(await poseidonCircom["poseidon(uint256[2])"]([0, 0])).to.eq(
36+
await poseidon["poseidon(uint256[2],uint256)"]([0, 0], 0)
37+
);
38+
});
39+
40+
it("should succeed on random inputs", async () => {
41+
for (let bytes = 1; bytes <= 32; ++bytes) {
42+
for (let i = 0; i < 5; ++i) {
43+
const a = randomBytes(bytes);
44+
const b = randomBytes(bytes);
45+
expect(await poseidonCircom["poseidon(uint256[2])"]([a, b])).to.eq(
46+
await poseidon["poseidon(uint256[2],uint256)"]([a, b], 0)
47+
);
48+
expect(await poseidonCircom["poseidon(uint256[2])"]([a, 0])).to.eq(
49+
await poseidon["poseidon(uint256[2],uint256)"]([a, 0], 0)
50+
);
51+
expect(await poseidonCircom["poseidon(uint256[2])"]([0, b])).to.eq(
52+
await poseidon["poseidon(uint256[2],uint256)"]([0, b], 0)
53+
);
54+
}
55+
}
56+
});
57+
});
58+
59+
// test against with scroll's go implementation.
60+
context("domain = nonzero", async () => {
61+
let poseidonCircom: Contract;
62+
let poseidon: Contract;
63+
64+
beforeEach(async () => {
65+
const [deployer] = await ethers.getSigners();
66+
67+
const PoseidonWithoutDomainFactory = new ethers.ContractFactory(
68+
PoseidonWithoutDomain.generateABI(2),
69+
PoseidonWithoutDomain.createCode(2),
70+
deployer
71+
);
72+
poseidonCircom = await PoseidonWithoutDomainFactory.deploy();
73+
await poseidonCircom.deployed();
74+
75+
const PoseidonWithDomainFactory = new ethers.ContractFactory(generateABI(2), createCode(2), deployer);
76+
poseidon = await PoseidonWithDomainFactory.deploy();
77+
await poseidon.deployed();
78+
});
79+
80+
it("should succeed on zero inputs", async () => {
81+
expect(await poseidon["poseidon(uint256[2],uint256)"]([0, 0], 6)).to.eq(
82+
BigNumber.from("17848312925884193353134534408113064827548730776291701343555436351962284922129")
83+
);
84+
expect(await poseidon["poseidon(uint256[2],uint256)"]([0, 0], 7)).to.eq(
85+
BigNumber.from("20994231331856095272861976502721128670019193481895476667943874333621461724676")
86+
);
87+
});
88+
89+
it("should succeed on random inputs", async () => {
90+
const lines = String(fs.readFileSync("./integration-test/testdata/poseidon_hash_with_domain.data")).split("\n");
91+
for (const line of lines) {
92+
const [domain, a, b, hash] = line.split(" ");
93+
expect(await poseidon["poseidon(uint256[2],uint256)"]([a, b], domain)).to.eq(BigNumber.from(hash));
94+
}
95+
});
96+
});
97+
});

integration-test/ZkTrieVerifier.spec.ts

+468-300
Large diffs are not rendered by default.

integration-test/testdata/poseidon_hash_with_domain.data

+1,154
Large diffs are not rendered by default.

scripts/ScrollChainCommitmentVerifier.deploy.ts

+5-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import * as dotenv from "dotenv";
33

44
import { ethers } from "hardhat";
5-
import poseidonUnit from "circomlib/src/poseidon_gencontract";
5+
import { generateABI, createCode } from "../scripts/poseidon";
66

77
dotenv.config();
88

@@ -15,11 +15,7 @@ async function main() {
1515
let PoseidonUnit2Address = process.env.POSEIDON_UNIT2_ADDR;
1616

1717
if (!PoseidonUnit2Address) {
18-
const Poseidon2Elements = new ethers.ContractFactory(
19-
poseidonUnit.generateABI(2),
20-
poseidonUnit.createCode(2),
21-
deployer
22-
);
18+
const Poseidon2Elements = new ethers.ContractFactory(generateABI(2), createCode(2), deployer);
2319

2420
const poseidon = await Poseidon2Elements.deploy();
2521
console.log("Deploy PoseidonUnit2 contract, hash:", poseidon.deployTransaction.hash);
@@ -28,7 +24,9 @@ async function main() {
2824
PoseidonUnit2Address = poseidon.address;
2925
}
3026

31-
const verifier = await ScrollChainCommitmentVerifier.deploy(PoseidonUnit2Address, L1ScrollChainAddress);
27+
const verifier = await ScrollChainCommitmentVerifier.deploy(PoseidonUnit2Address, L1ScrollChainAddress, {
28+
gasPrice: 1e9,
29+
});
3230
console.log("Deploy ScrollChainCommitmentVerifier contract, hash:", verifier.deployTransaction.hash);
3331
const receipt = await verifier.deployTransaction.wait();
3432
console.log(`✅ Deploy ScrollChainCommitmentVerifier contract at: ${verifier.address}, gas used: ${receipt.gasUsed}`);

scripts/poseidon.ts

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/* eslint-disable node/no-missing-import */
2+
import { ethers } from "ethers";
3+
4+
import Contract from "circomlib/src/evmasm";
5+
import * as constants from "circomlib/src/poseidon_constants";
6+
7+
const N_ROUNDS_F = 8;
8+
const N_ROUNDS_P = [56, 57, 56, 60, 60, 63, 64, 63];
9+
10+
export function createCode(nInputs: number) {
11+
if (nInputs < 1 || nInputs > 8) throw new Error("Invalid number of inputs. Must be 1<=nInputs<=8");
12+
const t = nInputs + 1;
13+
const nRoundsF = N_ROUNDS_F;
14+
const nRoundsP = N_ROUNDS_P[t - 2];
15+
16+
const C = new Contract();
17+
18+
function saveM() {
19+
for (let i = 0; i < t; i++) {
20+
for (let j = 0; j < t; j++) {
21+
C.push(constants.M[t - 2][i][j]);
22+
C.push((1 + i * t + j) * 32);
23+
C.mstore();
24+
}
25+
}
26+
}
27+
28+
function ark(r: number) {
29+
// st, q
30+
for (let i = 0; i < t; i++) {
31+
C.dup(t); // q, st, q
32+
C.push(constants.C[t - 2][r * t + i]); // K, q, st, q
33+
C.dup(2 + i); // st[i], K, q, st, q
34+
C.addmod(); // newSt[i], st, q
35+
C.swap(1 + i); // xx, st, q
36+
C.pop();
37+
}
38+
}
39+
40+
function sigma(p: number) {
41+
// sq, q
42+
C.dup(t); // q, st, q
43+
C.dup(1 + p); // st[p] , q , st, q
44+
C.dup(1); // q, st[p] , q , st, q
45+
C.dup(0); // q, q, st[p] , q , st, q
46+
C.dup(2); // st[p] , q, q, st[p] , q , st, q
47+
C.dup(0); // st[p] , st[p] , q, q, st[p] , q , st, q
48+
C.mulmod(); // st2[p], q, st[p] , q , st, q
49+
C.dup(0); // st2[p], st2[p], q, st[p] , q , st, q
50+
C.mulmod(); // st4[p], st[p] , q , st, q
51+
C.mulmod(); // st5[p], st, q
52+
C.swap(1 + p);
53+
C.pop(); // newst, q
54+
}
55+
56+
function mix() {
57+
C.label("mix");
58+
for (let i = 0; i < t; i++) {
59+
for (let j = 0; j < t; j++) {
60+
if (j === 0) {
61+
C.dup(i + t); // q, newSt, oldSt, q
62+
C.push((1 + i * t + j) * 32);
63+
C.mload(); // M, q, newSt, oldSt, q
64+
C.dup(2 + i + j); // oldSt[j], M, q, newSt, oldSt, q
65+
C.mulmod(); // acc, newSt, oldSt, q
66+
} else {
67+
C.dup(1 + i + t); // q, acc, newSt, oldSt, q
68+
C.push((1 + i * t + j) * 32);
69+
C.mload(); // M, q, acc, newSt, oldSt, q
70+
C.dup(3 + i + j); // oldSt[j], M, q, acc, newSt, oldSt, q
71+
C.mulmod(); // aux, acc, newSt, oldSt, q
72+
C.dup(2 + i + t); // q, aux, acc, newSt, oldSt, q
73+
C.swap(2); // acc, aux, q, newSt, oldSt, q
74+
C.addmod(); // acc, newSt, oldSt, q
75+
}
76+
}
77+
}
78+
for (let i = 0; i < t; i++) {
79+
C.swap(t - i + (t - i - 1));
80+
C.pop();
81+
}
82+
C.push(0);
83+
C.mload();
84+
C.jmp();
85+
}
86+
87+
// Check selector
88+
C.push("0x0100000000000000000000000000000000000000000000000000000000");
89+
C.push(0);
90+
C.calldataload();
91+
C.div();
92+
C.dup(0);
93+
C.push(ethers.utils.keccak256(ethers.utils.toUtf8Bytes(`poseidon(uint256[${nInputs}],uint256)`)).slice(0, 10)); // poseidon(uint256[n],uint256)
94+
C.eq();
95+
C.swap(1);
96+
C.push(ethers.utils.keccak256(ethers.utils.toUtf8Bytes(`poseidon(bytes32[${nInputs}],bytes32)`)).slice(0, 10)); // poseidon(bytes32[n],bytes32)
97+
C.eq();
98+
C.or();
99+
C.jmpi("start");
100+
C.invalid();
101+
102+
C.label("start");
103+
104+
saveM();
105+
106+
C.push("0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001"); // q
107+
108+
// Load t values from the call data.
109+
// The function has a single array param param
110+
// [Selector (4)] [item1 (32)] [item2 (32)] .... [doman (32)]
111+
// Stack positions 0-nInputs.
112+
for (let i = 0; i < nInputs; i++) {
113+
C.push(0x04 + 0x20 * (nInputs - i - 1));
114+
C.calldataload();
115+
}
116+
C.push(0x04 + 0x20 * nInputs);
117+
C.calldataload();
118+
119+
for (let i = 0; i < nRoundsF + nRoundsP; i++) {
120+
ark(i);
121+
if (i < nRoundsF / 2 || i >= nRoundsP + nRoundsF / 2) {
122+
for (let j = 0; j < t; j++) {
123+
sigma(j);
124+
}
125+
} else {
126+
sigma(0);
127+
}
128+
const strLabel = "aferMix" + i;
129+
C._pushLabel(strLabel);
130+
C.push(0);
131+
C.mstore();
132+
C.jmp("mix");
133+
C.label(strLabel);
134+
}
135+
136+
C.push("0x00");
137+
C.mstore(); // Save it to pos 0;
138+
C.push("0x20");
139+
C.push("0x00");
140+
C.return();
141+
142+
mix();
143+
144+
return C.createTxData();
145+
}
146+
147+
export function generateABI(nInputs: number) {
148+
return [
149+
{
150+
constant: true,
151+
inputs: [
152+
{
153+
internalType: `bytes32[${nInputs}]`,
154+
name: "input",
155+
type: `bytes32[${nInputs}]`,
156+
},
157+
{
158+
internalType: "bytes32",
159+
name: "domain",
160+
type: "bytes32",
161+
},
162+
],
163+
name: "poseidon",
164+
outputs: [
165+
{
166+
internalType: "bytes32",
167+
name: "",
168+
type: "bytes32",
169+
},
170+
],
171+
payable: false,
172+
stateMutability: "pure",
173+
type: "function",
174+
},
175+
{
176+
constant: true,
177+
inputs: [
178+
{
179+
internalType: `uint256[${nInputs}]`,
180+
name: "input",
181+
type: `uint256[${nInputs}]`,
182+
},
183+
{
184+
internalType: "uint256",
185+
name: "domain",
186+
type: "uint256",
187+
},
188+
],
189+
name: "poseidon",
190+
outputs: [
191+
{
192+
internalType: "uint256",
193+
name: "",
194+
type: "uint256",
195+
},
196+
],
197+
payable: false,
198+
stateMutability: "pure",
199+
type: "function",
200+
},
201+
];
202+
}

src/L1/rollup/ScrollChainCommitmentVerifier.sol

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
pragma solidity =0.8.16;
44

5-
import {ScrollChain} from "./ScrollChain.sol";
5+
import {IScrollChain} from "./IScrollChain.sol";
66
import {ZkTrieVerifier} from "../../libraries/verifier/ZkTrieVerifier.sol";
77

88
contract ScrollChainCommitmentVerifier {
@@ -49,11 +49,11 @@ contract ScrollChainCommitmentVerifier {
4949
bytes32 storageKey,
5050
bytes calldata proof
5151
) external view returns (bytes32 storageValue) {
52-
require(ScrollChain(rollup).isBatchFinalized(batchIndex), "Batch not finalized");
52+
require(IScrollChain(rollup).isBatchFinalized(batchIndex), "Batch not finalized");
5353

5454
bytes32 computedStateRoot;
5555
(computedStateRoot, storageValue) = ZkTrieVerifier.verifyZkTrieProof(poseidon, account, storageKey, proof);
56-
bytes32 expectedStateRoot = ScrollChain(rollup).finalizedStateRoots(batchIndex);
56+
bytes32 expectedStateRoot = IScrollChain(rollup).finalizedStateRoots(batchIndex);
5757
require(computedStateRoot == expectedStateRoot, "Invalid inclusion proof");
5858
}
5959
}

0 commit comments

Comments
 (0)