@nori-zk/o1js-zk-utils
Version:
o1js-zk-utils supporting Nori Bridge
200 lines • 10.7 kB
JavaScript
import { Field, MerkleTree, Poseidon } from 'o1js';
import { computeMerkleTreeDepthAndSize, getMerkleZeros, buildMerkleTree, foldMerkleLeft, getMerklePathFromTree, computeMerkleRootFromPath, getMerklePathFromLeaves, } from './merkleTree.js';
import { buildLeavesNonProvable, dummyCodeChallenge, dummyValue, nonProvableStorageSlotLeafHash, } from './testUtils.js';
// Full Merkle lifecycle test using actual hashed leaves and leaf index
function fullMerkleTest(pairs, leafIndex) {
const leaves = buildLeavesNonProvable(pairs);
const { depth, paddedSize } = computeMerkleTreeDepthAndSize(leaves.length);
const zeros = getMerkleZeros(depth);
const leavesClone = leaves.slice();
const root = foldMerkleLeft(leavesClone, paddedSize, depth, zeros);
const leavesForPath = leaves.slice();
const path = getMerklePathFromLeaves(leavesForPath, paddedSize, depth, leafIndex, zeros);
const leafHash = leaves[leafIndex] ?? Field(0);
const recomputedRoot = computeMerkleRootFromPath(leafHash, leafIndex, path);
expect(recomputedRoot.equals(root).toBoolean()).toBe(true);
return recomputedRoot;
}
// Brute-force reference: pad with Field(0), hash every pair, no zeros cache.
function referenceRoot(leaves, paddedSize) {
let level = [...leaves];
while (level.length < paddedSize)
level.push(Field(0));
while (level.length > 1) {
const next = [];
for (let i = 0; i < level.length; i += 2) {
next.push(Poseidon.hash([level[i], level[i + 1]]));
}
level = next;
}
return level[0];
}
describe('regression_a2090_zeros_indexing', () => {
// 1,3 -- no dummy pairs (sanity)
// 5,6 -- dummy pairs at depth 3
// 9 -- dummy pairs at depth 4
// 17 -- dummy pairs at depth 5
test.each([1, 3, 5, 6, 9, 17])('regression_a2090_bruteforce_reference_buildMerkleTree nLeaves=%i', (nLeaves) => {
const pairs = [];
for (let i = 0; i < nLeaves; i++) {
pairs.push([dummyCodeChallenge(i), dummyValue(i)]);
}
const leaves = buildLeavesNonProvable(pairs);
const { depth, paddedSize } = computeMerkleTreeDepthAndSize(nLeaves);
const zeros = getMerkleZeros(depth);
const expected = referenceRoot(leaves, paddedSize);
const tree = buildMerkleTree([...leaves], paddedSize, depth, zeros);
expect(tree[0][0].equals(expected).toBoolean()).toBe(true);
});
test.each([1, 3, 5, 6, 9, 17])('regression_a2090_bruteforce_reference_foldMerkleLeft nLeaves=%i', (nLeaves) => {
const pairs = [];
for (let i = 0; i < nLeaves; i++) {
pairs.push([dummyCodeChallenge(i), dummyValue(i)]);
}
const leaves = buildLeavesNonProvable(pairs);
const { depth, paddedSize } = computeMerkleTreeDepthAndSize(nLeaves);
const zeros = getMerkleZeros(depth);
const expected = referenceRoot(leaves, paddedSize);
const rootFold = foldMerkleLeft([...leaves], paddedSize, depth, zeros);
expect(rootFold.equals(expected).toBoolean()).toBe(true);
});
test.each([1, 3, 5, 6, 9, 17])('regression_a2090_o1js_merkle_tree_reference_buildMerkleTree nLeaves=%i', (nLeaves) => {
const pairs = [];
for (let i = 0; i < nLeaves; i++) {
pairs.push([dummyCodeChallenge(i), dummyValue(i)]);
}
const leaves = buildLeavesNonProvable(pairs);
const { depth, paddedSize } = computeMerkleTreeDepthAndSize(nLeaves);
const zeros = getMerkleZeros(depth);
const o1jsTree = new MerkleTree(depth + 1);
for (let i = 0; i < nLeaves; i++) {
o1jsTree.setLeaf(BigInt(i), leaves[i]);
}
const o1jsRoot = o1jsTree.getRoot();
const tree = buildMerkleTree([...leaves], paddedSize, depth, zeros);
expect(tree[0][0].equals(o1jsRoot).toBoolean()).toBe(true);
});
test.each([1, 3, 5, 6, 9, 17])('regression_a2090_o1js_merkle_tree_reference_foldMerkleLeft nLeaves=%i', (nLeaves) => {
const pairs = [];
for (let i = 0; i < nLeaves; i++) {
pairs.push([dummyCodeChallenge(i), dummyValue(i)]);
}
const leaves = buildLeavesNonProvable(pairs);
const { depth, paddedSize } = computeMerkleTreeDepthAndSize(nLeaves);
const zeros = getMerkleZeros(depth);
const o1jsTree = new MerkleTree(depth + 1);
for (let i = 0; i < nLeaves; i++) {
o1jsTree.setLeaf(BigInt(i), leaves[i]);
}
const o1jsRoot = o1jsTree.getRoot();
const rootFold = foldMerkleLeft([...leaves], paddedSize, depth, zeros);
expect(rootFold.equals(o1jsRoot).toBoolean()).toBe(true);
});
});
describe('Merkle Fixed Tests', () => {
test('test_large_slots', () => {
const n = 1000;
const pairs = [];
for (let i = 0; i < n; i++) {
pairs.push([dummyCodeChallenge(i), dummyValue(i)]);
}
const root = fullMerkleTest(pairs, 543);
console.log("root", root.toBigInt());
});
test('test_hash_storage_slot_basic', () => {
const codeChallenge = dummyCodeChallenge(2);
const value = dummyValue(3);
const leafHash = nonProvableStorageSlotLeafHash(codeChallenge, value);
expect(leafHash.equals(Field(0)).toBoolean()).toBe(false);
});
test('test_all_leaf_counts_and_indices_with_build_and_fold', () => {
const maxLeaves = 50;
// Calculate max depth from maxLeaves
const maxDepth = Math.ceil(Math.log2(maxLeaves)) || 1;
// Precompute zeros
const zeros = getMerkleZeros(maxDepth);
console.log('Testing all leaf counts and indices with both fold and build...');
for (let nLeaves = 0; nLeaves <= maxLeaves; nLeaves++) {
console.log(`→ Testing with ${nLeaves} leaves`);
const pairs = [];
for (let i = 0; i < nLeaves; i++) {
pairs.push([dummyCodeChallenge(i), dummyValue(i)]);
}
const leaves = buildLeavesNonProvable(pairs);
console.log(` leaves ${leaves.map((l) => l.toJSON().split('\n').join(' ,'))}`);
const { depth, paddedSize } = computeMerkleTreeDepthAndSize(nLeaves);
console.log(` depth=${depth}, paddedSize=${paddedSize}`);
const rootViaFold = foldMerkleLeft(leaves.slice(), paddedSize, depth, zeros);
console.log(` rootViaFold = ${rootViaFold}`);
const merkleTree = buildMerkleTree(leaves, paddedSize, depth, zeros);
console.log(` rootViaBuild = ${merkleTree[0][0]}`);
expect(merkleTree[0][0].equals(rootViaFold).toBoolean()).toBe(true);
// Verify leaf layer padding
const expectedPadded = leaves.slice();
while (expectedPadded.length < paddedSize) {
expectedPadded.push(Field(0));
}
expect(merkleTree[depth]).toEqual(expectedPadded);
for (let index = 0; index < nLeaves; index++) {
const leavesForPath = leaves.slice();
const pathFold = getMerklePathFromLeaves(leavesForPath, paddedSize, depth, index, zeros);
const pathBuild = getMerklePathFromTree(merkleTree, index);
expect(pathFold).toEqual(pathBuild);
const leafHash = leaves[index];
const recomputedRoot = computeMerkleRootFromPath(leafHash, index, pathFold);
expect(recomputedRoot.equals(rootViaFold).toBoolean()).toBe(true);
console.log(` ✅ [nLeaves=${nLeaves}, index=${index}] OK`);
}
}
});
test('huge_timed_test', () => {
const nLeaves = 1 << 16;
console.log(`\n→ Testing with ${nLeaves} leaves`);
const startTimeGetMerkleZeros = Date.now();
const maxDepth = Math.ceil(Math.log2(nLeaves)) || 1;
const zeros = getMerkleZeros(maxDepth);
console.log(`01. getMerkleZeros: ${Date.now() - startTimeGetMerkleZeros}ms`);
const startTimeGenerateDummyPairs = Date.now();
const pairs = [];
for (let i = 0; i < nLeaves; i++) {
pairs.push([dummyCodeChallenge(i), dummyValue(i)]);
}
console.log(`02. Generate dummy pairs: ${Date.now() - startTimeGenerateDummyPairs}ms`);
const startTimeBuildLeaves = Date.now();
const leaves = buildLeavesNonProvable(pairs);
console.log(`03. buildLeaves: ${Date.now() - startTimeBuildLeaves}ms`);
const startTimeComputeDepthAndSize = Date.now();
const { depth, paddedSize } = computeMerkleTreeDepthAndSize(nLeaves);
console.log(`04. compute depth and padded size: ${Date.now() - startTimeComputeDepthAndSize}ms`);
console.log(` depth=${depth}, paddedSize=${paddedSize}`);
const startTimeFoldMerkleLeft = Date.now();
const rootViaFold = foldMerkleLeft(leaves.slice(), paddedSize, depth, zeros);
console.log(`05. foldMerkleLeft: ${Date.now() - startTimeFoldMerkleLeft}ms`);
console.log(` rootViaFold = ${rootViaFold}`);
const startTimeBuildMerkleTree = Date.now();
const merkleTree = buildMerkleTree(leaves, paddedSize, depth, zeros);
console.log(`06. buildMerkleTree: ${Date.now() - startTimeBuildMerkleTree}ms`);
console.log(` rootViaBuild = ${merkleTree[0][0]}`);
expect(merkleTree[0][0].equals(rootViaFold).toBoolean()).toBe(true);
const expectedPadded = leaves.slice();
while (expectedPadded.length < paddedSize) {
expectedPadded.push(Field(0));
}
expect(merkleTree[depth]).toEqual(expectedPadded);
const index = nLeaves / 2;
const startTimeGetPathFromLeaves = Date.now();
const pathFold = getMerklePathFromLeaves(leaves.slice(), paddedSize, depth, index, zeros);
console.log(`07. getMerklePathFromLeaves: ${Date.now() - startTimeGetPathFromLeaves}ms`);
const startTimeGetPathFromTree = Date.now();
const pathBuild = getMerklePathFromTree(merkleTree, index);
console.log(`08. getMerklePathFromTree: ${Date.now() - startTimeGetPathFromTree}ms`);
expect(pathFold).toEqual(pathBuild);
const startTimeRecomputeRoot = Date.now();
const leafHash = leaves[index];
const recomputedRoot = computeMerkleRootFromPath(leafHash, index, pathFold);
console.log(`09. recompute root from path: ${Date.now() - startTimeRecomputeRoot}ms`);
expect(recomputedRoot.equals(rootViaFold).toBoolean()).toBe(true);
console.log(` ✅ [nLeaves=${nLeaves}, index=${index}] OK`);
});
});
//# sourceMappingURL=merkleTree.spec.js.map