Skip to content

Commit dcf1bdf

Browse files
authored
Merge pull request #23 from OriginTrail/feature/calculate-merkle-root-from-proof
Calculate merkle root from proof
2 parents 42c9936 + 42ba9a9 commit dcf1bdf

File tree

4 files changed

+182
-11
lines changed

4 files changed

+182
-11
lines changed

index.cjs

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -259,23 +259,108 @@ function calculateMerkleRoot(quads, chunkSizeBytes = 32) {
259259

260260
function calculateMerkleProof(quads, chunkSizeBytes, challenge) {
261261
const chunks = splitIntoChunks(quads, chunkSizeBytes);
262+
263+
// Step 1: Generate leaf hashes using Solidity-compatible hashing
262264
const leaves = chunks.map((chunk, index) =>
263265
Buffer.from(
264266
ethers.utils
265267
.solidityKeccak256(["string", "uint256"], [chunk, index])
266-
.replace("0x", ""),
268+
.slice(2), // strip "0x"
267269
"hex"
268270
)
269271
);
270272

271-
const tree = new merkletreejs.MerkleTree(leaves, arraifyKeccak256, { sortPairs: true });
273+
const proof = [];
274+
let index = challenge;
275+
let currentLevel = leaves;
276+
277+
// Step 2: Traverse tree upward and build proof path
278+
while (currentLevel.length > 1) {
279+
const nextLevel = [];
280+
281+
for (let i = 0; i < currentLevel.length; i += 2) {
282+
const left = currentLevel[i];
283+
const right = i + 1 < currentLevel.length ? currentLevel[i + 1] : null;
284+
285+
if (right) {
286+
// Sort pair like Solidity (<)
287+
const [first, second] =
288+
Buffer.compare(left, right) < 0 ? [left, right] : [right, left];
289+
290+
const combined = Buffer.concat([first, second]);
291+
const parent = Buffer.from(
292+
ethers.utils.keccak256(combined).slice(2),
293+
"hex"
294+
);
295+
296+
nextLevel.push(parent);
297+
298+
// Collect sibling if current index is part of this pair
299+
if (i === index || i + 1 === index) {
300+
const sibling = i === index ? right : left;
301+
proof.push(`0x${sibling.toString("hex")}`);
302+
index = Math.floor(i / 2);
303+
}
304+
} else {
305+
// Odd number of nodes – carry unpaired node up
306+
nextLevel.push(left);
307+
308+
if (i === index) {
309+
index = Math.floor(i / 2);
310+
}
311+
}
312+
}
313+
314+
currentLevel = nextLevel;
315+
}
316+
317+
const leaf = leaves[challenge];
318+
const root = currentLevel[0];
272319

273320
return {
274-
leaf: arraifyKeccak256(chunks[challenge]),
275-
proof: tree.getHexProof(leaves[challenge]),
321+
root: `0x${root.toString("hex")}`,
322+
proof,
323+
leaf: `0x${leaf.toString("hex")}`,
324+
chunk: chunks[challenge],
325+
chunkId: challenge,
276326
};
277327
}
278328

329+
function computeMerkleRootFromProof(chunks, chunkId, proof) {
330+
// Get the specific chunk we're proving
331+
const challengeChunk = chunks[chunkId];
332+
if (!challengeChunk) {
333+
throw new Error(`Chunk ${chunkId} not found in chunks array`);
334+
}
335+
336+
// Calculate initial hash from the chunk and its ID
337+
let currentHash = Buffer.from(
338+
ethers.utils
339+
.solidityKeccak256(["string", "uint256"], [challengeChunk, chunkId])
340+
.slice(2),
341+
"hex"
342+
);
343+
344+
// Process each proof element
345+
for (const siblingHex of proof) {
346+
const sibling = Buffer.from(siblingHex.slice(2), "hex");
347+
348+
// Ensure deterministic ordering of hashes
349+
const [first, second] =
350+
Buffer.compare(currentHash, sibling) < 0
351+
? [currentHash, sibling]
352+
: [sibling, currentHash];
353+
354+
// Compute parent hash
355+
currentHash = Buffer.from(
356+
ethers.utils.keccak256(Buffer.concat([first, second])).slice(2),
357+
"hex"
358+
);
359+
}
360+
361+
return `0x${currentHash.toString("hex")}`;
362+
}
363+
279364
function groupNquadsBySubject(nquadsArray, sort = false) {
280365
const parser = new N3.Parser({ format: "star" });
281366
const grouped = {};
@@ -448,6 +533,7 @@ var knowledgeCollectionTools = /*#__PURE__*/Object.freeze({
448533
calculateMerkleProof: calculateMerkleProof,
449534
calculateMerkleRoot: calculateMerkleRoot,
450535
calculateNumberOfChunks: calculateNumberOfChunks,
536+
computeMerkleRootFromProof: computeMerkleRootFromProof,
451537
countDistinctSubjects: countDistinctSubjects,
452538
filterTriplesByAnnotation: filterTriplesByAnnotation,
453539
formatDataset: formatDataset,

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "assertion-tools",
3-
"version": "8.0.2",
3+
"version": "8.0.3",
44
"description": "Common assertion tools used in ot-node and dkg.js",
55
"main": "index.js",
66
"type": "module",

src/knowledge-collection-tools.js

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,23 +127,108 @@ export function calculateMerkleRoot(quads, chunkSizeBytes = 32) {
127127

128128
export function calculateMerkleProof(quads, chunkSizeBytes, challenge) {
129129
const chunks = splitIntoChunks(quads, chunkSizeBytes);
130+
131+
// Step 1: Generate leaf hashes using Solidity-compatible hashing
130132
const leaves = chunks.map((chunk, index) =>
131133
Buffer.from(
132134
ethers.utils
133135
.solidityKeccak256(["string", "uint256"], [chunk, index])
134-
.replace("0x", ""),
136+
.slice(2), // strip "0x"
135137
"hex"
136138
)
137139
);
138140

139-
const tree = new MerkleTree(leaves, arraifyKeccak256, { sortPairs: true });
141+
const proof = [];
142+
let index = challenge;
143+
let currentLevel = leaves;
144+
145+
// Step 2: Traverse tree upward and build proof path
146+
while (currentLevel.length > 1) {
147+
const nextLevel = [];
148+
149+
for (let i = 0; i < currentLevel.length; i += 2) {
150+
const left = currentLevel[i];
151+
const right = i + 1 < currentLevel.length ? currentLevel[i + 1] : null;
152+
153+
if (right) {
154+
// Sort pair like Solidity (<)
155+
const [first, second] =
156+
Buffer.compare(left, right) < 0 ? [left, right] : [right, left];
157+
158+
const combined = Buffer.concat([first, second]);
159+
const parent = Buffer.from(
160+
ethers.utils.keccak256(combined).slice(2),
161+
"hex"
162+
);
163+
164+
nextLevel.push(parent);
165+
166+
// Collect sibling if current index is part of this pair
167+
if (i === index || i + 1 === index) {
168+
const sibling = i === index ? right : left;
169+
proof.push(`0x${sibling.toString("hex")}`);
170+
index = Math.floor(i / 2);
171+
}
172+
} else {
173+
// Odd number of nodes – carry unpaired node up
174+
nextLevel.push(left);
175+
176+
if (i === index) {
177+
index = Math.floor(i / 2);
178+
}
179+
}
180+
}
181+
182+
currentLevel = nextLevel;
183+
}
184+
185+
const leaf = leaves[challenge];
186+
const root = currentLevel[0];
140187

141188
return {
142-
leaf: arraifyKeccak256(chunks[challenge]),
143-
proof: tree.getHexProof(leaves[challenge]),
189+
root: `0x${root.toString("hex")}`,
190+
proof,
191+
leaf: `0x${leaf.toString("hex")}`,
192+
chunk: chunks[challenge],
193+
chunkId: challenge,
144194
};
145195
}
146196

197+
export function computeMerkleRootFromProof(chunks, chunkId, proof) {
198+
// Get the specific chunk we're proving
199+
const challengeChunk = chunks[chunkId];
200+
if (!challengeChunk) {
201+
throw new Error(`Chunk ${chunkId} not found in chunks array`);
202+
}
203+
204+
// Calculate initial hash from the chunk and its ID
205+
let currentHash = Buffer.from(
206+
ethers.utils
207+
.solidityKeccak256(["string", "uint256"], [challengeChunk, chunkId])
208+
.slice(2),
209+
"hex"
210+
);
211+
212+
// Process each proof element
213+
for (const siblingHex of proof) {
214+
const sibling = Buffer.from(siblingHex.slice(2), "hex");
215+
216+
// Ensure deterministic ordering of hashes
217+
const [first, second] =
218+
Buffer.compare(currentHash, sibling) < 0
219+
? [currentHash, sibling]
220+
: [sibling, currentHash];
221+
222+
// Compute parent hash
223+
currentHash = Buffer.from(
224+
ethers.utils.keccak256(Buffer.concat([first, second])).slice(2),
225+
"hex"
226+
);
227+
}
228+
229+
return `0x${currentHash.toString("hex")}`;
230+
}
231+
147232
export function groupNquadsBySubject(nquadsArray, sort = false) {
148233
const parser = new N3.Parser({ format: "star" });
149234
const grouped = {};

0 commit comments

Comments
 (0)