-
-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
11,917 additions
and
34 deletions.
There are no files selected for viewing
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,89 @@ | ||
import {Tree, getNode} from "./tree"; | ||
import {zeroNode} from "./zeroNode"; | ||
import {Gindex, toGindex} from "./gindex"; | ||
import {LeafNode, Node} from "./node"; | ||
|
||
type Snapshot = { | ||
finalized: Uint8Array[]; | ||
count: number; | ||
}; | ||
|
||
/** | ||
* Given a tree, return a snapshot of the tree with the root, finalized nodes, and count. | ||
* Tree could be full tree, or partial tree. See https://github.com/ChainSafe/ssz/issues/293 | ||
*/ | ||
export function toSnapshot(rootNode: Node, depth: number, count: number): Snapshot { | ||
if (count < 0) { | ||
throw new Error(`Expect count to be non-negative, got ${count}`); | ||
} | ||
|
||
const finalizedGindices = count > 0 ? indexToFinalizedGindices(depth, count - 1) : []; | ||
const finalized = finalizedGindices.map((gindex) => getNode(rootNode, gindex).root); | ||
|
||
return { | ||
finalized, | ||
count, | ||
}; | ||
} | ||
|
||
/** | ||
* Given a snapshot, return root node of a tree. | ||
* See https://github.com/ChainSafe/ssz/issues/293 | ||
*/ | ||
export function fromSnapshot(snapshot: Snapshot, depth: number): Node { | ||
const tree = new Tree(zeroNode(depth)); | ||
const {count, finalized} = snapshot; | ||
if (count < 0) { | ||
throw new Error(`Expect count to be non-negative, got ${count}`); | ||
} | ||
|
||
const finalizedGindices = count > 0 ? indexToFinalizedGindices(depth, count - 1) : []; | ||
|
||
if (finalizedGindices.length !== finalized.length) { | ||
throw new Error(`Expected ${finalizedGindices.length} finalized gindices, got ${finalized.length}`); | ||
} | ||
|
||
for (const [i, gindex] of finalizedGindices.entries()) { | ||
const node = LeafNode.fromRoot(finalized[i]); | ||
tree.setNode(gindex, node); | ||
} | ||
|
||
return tree.rootNode; | ||
} | ||
|
||
/** | ||
* A finalized gindex means that the gindex is at the root of a subtree of the tree where there is no ZERO_NODE belong to it. | ||
* Given a list of depth `depth` and an index `index`, return a list of finalized gindexes. | ||
*/ | ||
export function indexToFinalizedGindices(depth: number, index: number): Gindex[] { | ||
if (index < 0 || depth < 0) { | ||
throw new Error(`Expect index and depth to be non-negative, got ${index} and ${depth}`); | ||
} | ||
|
||
// given this tree with depth 3 and index 6 | ||
// X | ||
// X X | ||
// X X X 0 | ||
// X X X X X X 0 0 | ||
// we'll extract the root 4 left most nodes, then root node of the next 2 nodes | ||
// need to track the offset at each level to compute gindex of each root node | ||
const offsetByDepth = Array.from({length: depth + 1}, () => 0); | ||
// count starts with 1 | ||
let count = index + 1; | ||
|
||
const result: Gindex[] = []; | ||
while (count > 0) { | ||
const prevLog2 = Math.floor(Math.log2(count)); | ||
const prevPowerOf2 = 2 ** prevLog2; | ||
const depthFromRoot = depth - prevLog2; | ||
const finalizedGindex = toGindex(depthFromRoot, BigInt(offsetByDepth[depthFromRoot])); | ||
result.push(finalizedGindex); | ||
for (let i = 0; i <= prevLog2; i++) { | ||
offsetByDepth[depthFromRoot + i] += Math.pow(2, i); | ||
} | ||
|
||
count -= prevPowerOf2; | ||
} | ||
|
||
return result; | ||
} |
115 changes: 115 additions & 0 deletions
115
packages/persistent-merkle-tree/test/unit/snapshot.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,115 @@ | ||
import { expect } from "chai"; | ||
import {describe, it} from "mocha"; | ||
import {fromSnapshot, indexToFinalizedGindices, toSnapshot} from "../../src/snapshot"; | ||
import {subtreeFillToContents} from "../../src/subtree"; | ||
import { LeafNode } from "../../src/node"; | ||
import { Tree, setNodesAtDepth } from "../../src/tree"; | ||
import { toGindex } from "../../src"; | ||
|
||
describe("toSnapshot and fromSnapshot", () => { | ||
const depth = 4; | ||
const maxItems = Math.pow(2, depth); | ||
|
||
for (let count = 0; count <= maxItems; count ++) { | ||
it(`toSnapshot and fromSnapshot with count ${count}`, () => { | ||
const nodes = Array.from({length: count}, (_, i) => LeafNode.fromRoot(Buffer.alloc(32, i))); | ||
const fullListRootNode = subtreeFillToContents(nodes, depth); | ||
const snapshot = toSnapshot(fullListRootNode, depth, count); | ||
const partialListRootNode = fromSnapshot(snapshot, depth); | ||
|
||
// 1st step - check if the restored root node is the same | ||
expect(partialListRootNode.root).to.deep.equal(fullListRootNode.root); | ||
|
||
// 2nd step - make sure we can add more nodes to the restored tree | ||
const fullTree = new Tree(fullListRootNode); | ||
const partialTree = new Tree(partialListRootNode); | ||
for (let i = count; i < maxItems; i++) { | ||
const gindex = toGindex(depth, BigInt(i)); | ||
fullTree.setNode(gindex, LeafNode.fromRoot(Buffer.alloc(32, i))); | ||
partialTree.setNode(gindex, LeafNode.fromRoot(Buffer.alloc(32, i))); | ||
expect(partialTree.root).to.deep.equal(fullTree.root); | ||
|
||
// and snapshot created from 2 trees are the same | ||
const snapshot1 = toSnapshot(fullTree.rootNode, depth, i + 1); | ||
const snapshot2 = toSnapshot(partialTree.rootNode, depth, i + 1); | ||
expect(snapshot2).to.deep.equal(snapshot1); | ||
} | ||
}); | ||
|
||
// setNodesAtDepth() api is what ssz uses to grow the tree in its commit() phase | ||
it(`toSnapshot and fromSnapshot with count ${count} then grow with setNodeAtDepth`, () => { | ||
const nodes = Array.from({length: count}, (_, i) => LeafNode.fromRoot(Buffer.alloc(32, i))); | ||
const fullListRootNode = subtreeFillToContents(nodes, depth); | ||
const snapshot = toSnapshot(fullListRootNode, depth, count); | ||
const partialListRootNode = fromSnapshot(snapshot, depth); | ||
|
||
// 1st step - check if the restored root node is the same | ||
expect(partialListRootNode.root).to.deep.equal(fullListRootNode.root); | ||
|
||
// 2nd step - grow the tree with setNodesAtDepth | ||
for (let i = count; i < maxItems; i++) { | ||
const addedNodes = Array.from({length: i - count + 1}, (_, j) => LeafNode.fromRoot(Buffer.alloc(32, j))); | ||
const indices = Array.from({length: i - count + 1}, (_, j) => j + count); | ||
const root1 = setNodesAtDepth(fullListRootNode, depth, indices, addedNodes); | ||
const root2 = setNodesAtDepth(partialListRootNode, depth, indices, addedNodes); | ||
expect(root2.root).to.deep.equal(root1.root); | ||
|
||
for (let j = count; j <= i; j++) { | ||
const snapshot1 = toSnapshot(root1, depth, j); | ||
const snapshot2 = toSnapshot(root2, depth, j); | ||
expect(snapshot2).to.deep.equal(snapshot1); | ||
} | ||
} | ||
}); | ||
|
||
it(`toSnapshot() multiple times with count ${count}`, () => { | ||
const nodes = Array.from({length: count}, (_, i) => LeafNode.fromRoot(Buffer.alloc(32, i))); | ||
const fullListRootNode = subtreeFillToContents(nodes, depth); | ||
const snapshot = toSnapshot(fullListRootNode, depth, count); | ||
const partialListRootNode = fromSnapshot(snapshot, depth); | ||
|
||
// 1st step - check if the restored root node is the same | ||
expect(partialListRootNode.root).to.deep.equal(fullListRootNode.root); | ||
|
||
const snapshot2 = toSnapshot(partialListRootNode, depth, count); | ||
const restoredRootNode2 = fromSnapshot(snapshot2, depth); | ||
|
||
// 2nd step - check if the restored root node is the same | ||
expect(restoredRootNode2.root).to.deep.equal(partialListRootNode.root); | ||
}); | ||
} | ||
}); | ||
|
||
describe("indexToFinalizedGindices", () => { | ||
// given a tree with depth = 4 | ||
// 1 | ||
// 2 3 | ||
// 4 5 6 7 | ||
// 8 9 10 11 12 13 14 15 | ||
// 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | ||
const testCases: [number, number, bigint[]][] = [ | ||
[4, 0, [BigInt(16)]], | ||
[4, 1, [BigInt(8)]], | ||
[4, 2, [8, 18].map(BigInt)], | ||
[4, 3, [4].map(BigInt)], | ||
[4, 4, [4, 20].map(BigInt)], | ||
[4, 5, [4, 10].map(BigInt)], | ||
[4, 6, [4, 10, 22].map(BigInt)], | ||
[4, 7, [2].map(BigInt)], | ||
[4, 8, [2, 24].map(BigInt)], | ||
[4, 9, [2, 12].map(BigInt)], | ||
[4, 10, [2, 12, 26].map(BigInt)], | ||
[4, 11, [2, 6].map(BigInt)], | ||
[4, 12, [2, 6, 28].map(BigInt)], | ||
[4, 13, [2, 6, 14].map(BigInt)], | ||
[4, 14, [2, 6, 14, 30].map(BigInt)], | ||
[4, 15, [1].map(BigInt)], | ||
]; | ||
|
||
for (const [depth, index, finalizeGindices] of testCases) { | ||
it(`should correctly get finalized gindexes for index ${index} and depth ${depth}`, () => { | ||
const actual = indexToFinalizedGindices(depth, index); | ||
expect(actual).to.deep.equal(finalizeGindices); | ||
}); | ||
} | ||
}); |
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
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,68 @@ | ||
import {fromSnapshot, zeroNode} from "@chainsafe/persistent-merkle-tree"; | ||
import {CompositeType, CompositeView, CompositeViewDU} from "./composite"; | ||
import {ListCompositeOpts, ListCompositeType} from "./listComposite"; | ||
import {PartialListCompositeTreeViewDU} from "../viewDU/partialListComposite"; | ||
import {Snapshot} from "../util/types"; | ||
import {byteArrayEquals} from "../util/byteArray"; | ||
import {zeroSnapshot} from "../util/snapshot"; | ||
import {addLengthNode} from "./arrayBasic"; | ||
|
||
/** | ||
* Similar to ListCompositeType, this is mainly used to create a PartialListCompositeTreeViewDU from a snapshot. | ||
* The ViewDU created is a partial tree created from a snapshot, not a full tree. | ||
* Note that this class only inherits minimal methods as defined in ArrayType of ../view/arrayBasic.ts | ||
* It'll throw errors for all other methods, most of the usage is in the ViewDU class. | ||
*/ | ||
export class PartialListCompositeType< | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
ElementType extends CompositeType<any, CompositeView<ElementType>, CompositeViewDU<ElementType>> | ||
> extends ListCompositeType<ElementType> { | ||
constructor(readonly elementType: ElementType, readonly limit: number, opts?: ListCompositeOpts) { | ||
super(elementType, limit, opts); | ||
|
||
// only inherit methods in ArrayType of ../view/arrayBasic.ts | ||
const inheritedMethods = [ | ||
"tree_getLength", | ||
"tree_setLength", | ||
"tree_getChunksNode", | ||
"tree_chunksNodeOffset", | ||
"tree_setChunksNode", | ||
]; | ||
const methodNames = Object.getOwnPropertyNames(ListCompositeType.prototype).filter( | ||
(prop) => | ||
prop !== "constructor" && | ||
typeof (this as unknown as Record<string, unknown>)[prop] === "function" && | ||
!inheritedMethods.includes(prop) | ||
); | ||
|
||
// throw errors for all remaining methods | ||
for (const methodName of methodNames) { | ||
(this as unknown as Record<string, unknown>)[methodName] = () => { | ||
throw new Error(`Method ${methodName} is not implemented for PartialListCompositeType`); | ||
}; | ||
} | ||
} | ||
|
||
/** | ||
* Create a PartialListCompositeTreeViewDU from a snapshot. | ||
*/ | ||
toPartialViewDU(snapshot: Snapshot): PartialListCompositeTreeViewDU<ElementType> { | ||
const chunksNode = fromSnapshot(snapshot, this.chunkDepth); | ||
const rootNode = addLengthNode(chunksNode, snapshot.count); | ||
|
||
if (!byteArrayEquals(rootNode.root, snapshot.root)) { | ||
throw new Error(`Snapshot root is incorrect, expected ${snapshot.root}, got ${rootNode.root}`); | ||
} | ||
|
||
return new PartialListCompositeTreeViewDU(this, rootNode, snapshot); | ||
} | ||
|
||
/** | ||
* Creates a PartialListCompositeTreeViewDU from a zero snapshot. | ||
*/ | ||
defaultPartialViewDU(): PartialListCompositeTreeViewDU<ElementType> { | ||
const rootNode = addLengthNode(zeroNode(this.chunkDepth), 0); | ||
|
||
return new PartialListCompositeTreeViewDU(this, rootNode, zeroSnapshot(this.chunkDepth)); | ||
} | ||
} |
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,14 @@ | ||
import {zeroHash} from "@chainsafe/persistent-merkle-tree"; | ||
import {hash64} from "./merkleize"; | ||
import {Snapshot} from "./types"; | ||
|
||
/** | ||
* Create a zero snapshot with the given chunksDepth. | ||
*/ | ||
export function zeroSnapshot(chunkDepth: number): Snapshot { | ||
return { | ||
finalized: [], | ||
count: 0, | ||
root: hash64(zeroHash(chunkDepth), zeroHash(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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,12 @@ | ||
export type Require<T, K extends keyof T> = T & Required<Pick<T, K>>; | ||
|
||
/** | ||
* A snapshot contains the minimum amount of information needed to reconstruct a merkleized list, for the purposes of appending more items. | ||
* Note: This does not contain list elements, rather only contains intermediate merkle nodes. | ||
* This is used primarily for PartialListCompositeType. | ||
*/ | ||
export type Snapshot = { | ||
finalized: Uint8Array[]; | ||
root: Uint8Array; | ||
count: number; | ||
}; |
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
Oops, something went wrong.