-
-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: add dynamic multiproofs * Fix logic and rename to compact proof * Add perf tests * Fix benchmark * Update packages/persistent-merkle-tree/src/proof/compactMulti.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Fix linter errors * PR review Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com>
- Loading branch information
1 parent
7bd63c6
commit 5f1ea99
Showing
6 changed files
with
309 additions
and
12 deletions.
There are no files selected for viewing
151 changes: 151 additions & 0 deletions
151
packages/persistent-merkle-tree/src/proof/compactMulti.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import {convertGindexToBitstring, Gindex, GindexBitstring} from "../gindex"; | ||
import {BranchNode, LeafNode, Node} from "../node"; | ||
import {computeProofBitstrings} from "./util"; | ||
|
||
export function computeDescriptor(indices: Gindex[]): Uint8Array { | ||
// include all helper indices | ||
const proofBitstrings = new Set<GindexBitstring>(); | ||
const pathBitstrings = new Set<GindexBitstring>(); | ||
for (const leafIndex of indices) { | ||
const leafBitstring = convertGindexToBitstring(leafIndex); | ||
proofBitstrings.add(leafBitstring); | ||
const {branch, path} = computeProofBitstrings(leafBitstring); | ||
path.delete(leafBitstring); | ||
for (const pathIndex of path) { | ||
pathBitstrings.add(pathIndex); | ||
} | ||
for (const branchIndex of branch) { | ||
proofBitstrings.add(branchIndex); | ||
} | ||
} | ||
for (const pathIndex of pathBitstrings) { | ||
proofBitstrings.delete(pathIndex); | ||
} | ||
|
||
// sort gindex bitstrings in-order | ||
const allBitstringsSorted = Array.from(proofBitstrings).sort((a, b) => a.localeCompare(b)); | ||
|
||
// convert gindex bitstrings into descriptor bitstring | ||
let descriptorBitstring = ""; | ||
for (const gindexBitstring of allBitstringsSorted) { | ||
for (let i = 0; i < gindexBitstring.length; i++) { | ||
if (gindexBitstring[gindexBitstring.length - 1 - i] === "1") { | ||
descriptorBitstring += "1".padStart(i + 1, "0"); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
// append zero bits to byte-alignt | ||
if (descriptorBitstring.length % 8 != 0) { | ||
descriptorBitstring = descriptorBitstring.padEnd( | ||
8 - (descriptorBitstring.length % 8) + descriptorBitstring.length, | ||
"0" | ||
); | ||
} | ||
|
||
// convert descriptor bitstring to bytes | ||
const descriptor = new Uint8Array(descriptorBitstring.length / 8); | ||
for (let i = 0; i < descriptor.length; i++) { | ||
descriptor[i] = Number("0b" + descriptorBitstring.substring(i * 8, (i + 1) * 8)); | ||
} | ||
return descriptor; | ||
} | ||
|
||
function getBit(bitlist: Uint8Array, bitIndex: number): boolean { | ||
const bit = bitIndex % 8; | ||
const byteIdx = Math.floor(bitIndex / 8); | ||
const byte = bitlist[byteIdx]; | ||
switch (bit) { | ||
case 0: | ||
return (byte & 0b1000_0000) !== 0; | ||
case 1: | ||
return (byte & 0b0100_0000) !== 0; | ||
case 2: | ||
return (byte & 0b0010_0000) !== 0; | ||
case 3: | ||
return (byte & 0b0001_0000) !== 0; | ||
case 4: | ||
return (byte & 0b0000_1000) !== 0; | ||
case 5: | ||
return (byte & 0b0000_0100) !== 0; | ||
case 6: | ||
return (byte & 0b0000_0010) !== 0; | ||
case 7: | ||
return (byte & 0b0000_0001) !== 0; | ||
default: | ||
throw new Error("unreachable"); | ||
} | ||
} | ||
|
||
export function descriptorToBitlist(descriptor: Uint8Array): boolean[] { | ||
const bools: boolean[] = []; | ||
const maxBitLength = descriptor.length * 8; | ||
let count0 = 0; | ||
let count1 = 0; | ||
for (let i = 0; i < maxBitLength; i++) { | ||
const bit = getBit(descriptor, i); | ||
bools.push(bit); | ||
if (bit) { | ||
count1++; | ||
} else { | ||
count0++; | ||
} | ||
if (count1 > count0) { | ||
i++; | ||
if (i + 7 < maxBitLength) { | ||
throw new Error("Invalid descriptor: too many bytes"); | ||
} | ||
for (; i < maxBitLength; i++) { | ||
const bit = getBit(descriptor, i); | ||
if (bit) { | ||
throw new Error("Invalid descriptor: too many 1 bits"); | ||
} | ||
} | ||
return bools; | ||
} | ||
} | ||
throw new Error("Invalid descriptor: not enough 1 bits"); | ||
} | ||
|
||
export function nodeToCompactMultiProof(node: Node, bitlist: boolean[], bitIndex: number): Uint8Array[] { | ||
if (bitlist[bitIndex]) { | ||
return [node.root]; | ||
} else { | ||
const left = nodeToCompactMultiProof(node.left, bitlist, bitIndex + 1); | ||
const right = nodeToCompactMultiProof(node.right, bitlist, bitIndex + left.length * 2); | ||
return [...left, ...right]; | ||
} | ||
} | ||
|
||
/** | ||
* Create a Node given a validated bitlist, leaves, and a pointer into the bitlist and leaves | ||
* | ||
* Recursive definition | ||
*/ | ||
export function compactMultiProofToNode( | ||
bitlist: boolean[], | ||
leaves: Uint8Array[], | ||
pointer: {bitIndex: number; leafIndex: number} | ||
): Node { | ||
if (bitlist[pointer.bitIndex++]) { | ||
return LeafNode.fromRoot(leaves[pointer.leafIndex++]); | ||
} else { | ||
return new BranchNode( | ||
compactMultiProofToNode(bitlist, leaves, pointer), | ||
compactMultiProofToNode(bitlist, leaves, pointer) | ||
); | ||
} | ||
} | ||
|
||
export function createCompactMultiProof(rootNode: Node, descriptor: Uint8Array): Uint8Array[] { | ||
return nodeToCompactMultiProof(rootNode, descriptorToBitlist(descriptor), 0); | ||
} | ||
|
||
export function createNodeFromCompactMultiProof(leaves: Uint8Array[], descriptor: Uint8Array): Node { | ||
const bools = descriptorToBitlist(descriptor); | ||
if (bools.length !== leaves.length * 2 - 1) { | ||
throw new Error("Invalid multiproof: invalid number of leaves"); | ||
} | ||
return compactMultiProofToNode(bools, leaves, {bitIndex: 0, leafIndex: 0}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import {itBench} from "@dapplion/benchmark"; | ||
import {computeDescriptor, createProof, ProofType} from "../../src/proof"; | ||
import {createTree} from "../utils/tree"; | ||
|
||
describe("Proofs", () => { | ||
const depth = 15; | ||
const tree = createTree(depth); | ||
const maxNumLeaves = 10; | ||
const allLeafIndices = Array.from( | ||
{length: maxNumLeaves}, | ||
(_, i) => BigInt(2) ** BigInt(depth) + BigInt(i) ** BigInt(2) | ||
); | ||
for (let numLeaves = 1; numLeaves < 5; numLeaves++) { | ||
const leafIndices = allLeafIndices.slice(0, numLeaves); | ||
|
||
itBench({ | ||
id: `multiproof - depth ${depth}, ${numLeaves} requested leaves`, | ||
fn: () => { | ||
createProof(tree, {type: ProofType.multi, gindices: leafIndices}); | ||
}, | ||
}); | ||
|
||
itBench({ | ||
id: `tree offset multiproof - depth ${depth}, ${numLeaves} requested leaves`, | ||
fn: () => { | ||
createProof(tree, {type: ProofType.treeOffset, gindices: leafIndices}); | ||
}, | ||
}); | ||
|
||
itBench({ | ||
id: `compact multiproof - depth ${depth}, ${numLeaves} requested leaves`, | ||
beforeEach: () => { | ||
return computeDescriptor(leafIndices); | ||
}, | ||
fn: (descriptor) => { | ||
createProof(tree, {type: ProofType.compactMulti, descriptor}); | ||
}, | ||
}); | ||
} | ||
}); |
63 changes: 63 additions & 0 deletions
63
packages/persistent-merkle-tree/test/unit/proof/compactMulti.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import {expect} from "chai"; | ||
import { | ||
createNodeFromCompactMultiProof, | ||
createCompactMultiProof, | ||
descriptorToBitlist, | ||
computeDescriptor, | ||
} from "../../../src/proof/compactMulti"; | ||
import {createTree} from "../../utils/tree"; | ||
|
||
describe("CompactMultiProof", () => { | ||
const descriptorTestCases = [ | ||
{ | ||
input: Uint8Array.from([0b1000_0000]), | ||
output: [1].map(Boolean), | ||
}, | ||
{ | ||
input: Uint8Array.from([0b0010_0101, 0b1110_0000]), | ||
output: [0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1].map(Boolean), | ||
}, | ||
{ | ||
input: Uint8Array.from([0b0101_0101, 0b1000_0000]), | ||
output: [0, 1, 0, 1, 0, 1, 0, 1, 1].map(Boolean), | ||
}, | ||
{ | ||
input: Uint8Array.from([0b0101_0110]), | ||
output: [0, 1, 0, 1, 0, 1, 1].map(Boolean), | ||
}, | ||
]; | ||
describe("descriptorToBitlist", () => { | ||
it("should convert valid descriptor to a bitlist", () => { | ||
for (const {input, output} of descriptorTestCases) { | ||
expect(descriptorToBitlist(input)).to.deep.equal(output); | ||
} | ||
}); | ||
it("should throw on invalid descriptors", () => { | ||
const errorCases = [ | ||
Uint8Array.from([0b1000_0000, 0]), | ||
Uint8Array.from([0b0000_0001, 0]), | ||
Uint8Array.from([0b0101_0111]), | ||
Uint8Array.from([0b0101_0110, 0]), | ||
]; | ||
for (const input of errorCases) { | ||
expect(() => descriptorToBitlist(input)).to.throw(); | ||
} | ||
}); | ||
}); | ||
describe("computeDescriptor", () => { | ||
it("should convert gindices to a descriptor", () => { | ||
const index = 42n; | ||
const expected = Uint8Array.from([0x25, 0xe0]); | ||
expect(computeDescriptor([index])).to.deep.equal(expected); | ||
}); | ||
}); | ||
|
||
const tree = createTree(5); | ||
it("should roundtrip node -> proof -> node", () => { | ||
for (const {input} of descriptorTestCases) { | ||
const proof = createCompactMultiProof(tree, input); | ||
const newNode = createNodeFromCompactMultiProof(proof, input); | ||
expect(newNode.root).to.deep.equal(tree.root); | ||
} | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
5f1ea99
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possible performance regression was detected for some benchmarks.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold.
Full benchmark results