Skip to content

Commit d119dcc

Browse files
authored
Merge 6efe578 into 1dc50ef
2 parents 1dc50ef + 6efe578 commit d119dcc

File tree

18 files changed

+11917
-34
lines changed

18 files changed

+11917
-34
lines changed

packages/persistent-merkle-tree/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from "./subtree";
88
export * from "./tree";
99
export * from "./zeroNode";
1010
export * from "./zeroHash";
11+
export * from "./snapshot";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import {Tree, getNode} from "./tree";
2+
import {zeroNode} from "./zeroNode";
3+
import {Gindex, toGindex} from "./gindex";
4+
import {LeafNode, Node} from "./node";
5+
6+
type Snapshot = {
7+
finalized: Uint8Array[];
8+
count: number;
9+
};
10+
11+
/**
12+
* Given a tree, return a snapshot of the tree with the root, finalized nodes, and count.
13+
* Tree could be full tree, or partial tree. See https://github.com/ChainSafe/ssz/issues/293
14+
*/
15+
export function toSnapshot(rootNode: Node, depth: number, count: number): Snapshot {
16+
if (count < 0) {
17+
throw new Error(`Expect count to be non-negative, got ${count}`);
18+
}
19+
20+
const finalizedGindices = count > 0 ? indexToFinalizedGindices(depth, count - 1) : [];
21+
const finalized = finalizedGindices.map((gindex) => getNode(rootNode, gindex).root);
22+
23+
return {
24+
finalized,
25+
count,
26+
};
27+
}
28+
29+
/**
30+
* Given a snapshot, return root node of a tree.
31+
* See https://github.com/ChainSafe/ssz/issues/293
32+
*/
33+
export function fromSnapshot(snapshot: Snapshot, depth: number): Node {
34+
const tree = new Tree(zeroNode(depth));
35+
const {count, finalized} = snapshot;
36+
if (count < 0) {
37+
throw new Error(`Expect count to be non-negative, got ${count}`);
38+
}
39+
40+
const finalizedGindices = count > 0 ? indexToFinalizedGindices(depth, count - 1) : [];
41+
42+
if (finalizedGindices.length !== finalized.length) {
43+
throw new Error(`Expected ${finalizedGindices.length} finalized gindices, got ${finalized.length}`);
44+
}
45+
46+
for (const [i, gindex] of finalizedGindices.entries()) {
47+
const node = LeafNode.fromRoot(finalized[i]);
48+
tree.setNode(gindex, node);
49+
}
50+
51+
return tree.rootNode;
52+
}
53+
54+
/**
55+
* 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.
56+
* Given a list of depth `depth` and an index `index`, return a list of finalized gindexes.
57+
*/
58+
export function indexToFinalizedGindices(depth: number, index: number): Gindex[] {
59+
if (index < 0 || depth < 0) {
60+
throw new Error(`Expect index and depth to be non-negative, got ${index} and ${depth}`);
61+
}
62+
63+
// given this tree with depth 3 and index 6
64+
// X
65+
// X X
66+
// X X X 0
67+
// X X X X X X 0 0
68+
// we'll extract the root 4 left most nodes, then root node of the next 2 nodes
69+
// need to track the offset at each level to compute gindex of each root node
70+
const offsetByDepth = Array.from({length: depth + 1}, () => 0);
71+
// count starts with 1
72+
let count = index + 1;
73+
74+
const result: Gindex[] = [];
75+
while (count > 0) {
76+
const prevLog2 = Math.floor(Math.log2(count));
77+
const prevPowerOf2 = 2 ** prevLog2;
78+
const depthFromRoot = depth - prevLog2;
79+
const finalizedGindex = toGindex(depthFromRoot, BigInt(offsetByDepth[depthFromRoot]));
80+
result.push(finalizedGindex);
81+
for (let i = 0; i <= prevLog2; i++) {
82+
offsetByDepth[depthFromRoot + i] += Math.pow(2, i);
83+
}
84+
85+
count -= prevPowerOf2;
86+
}
87+
88+
return result;
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { expect } from "chai";
2+
import {describe, it} from "mocha";
3+
import {fromSnapshot, indexToFinalizedGindices, toSnapshot} from "../../src/snapshot";
4+
import {subtreeFillToContents} from "../../src/subtree";
5+
import { LeafNode } from "../../src/node";
6+
import { Tree, setNodesAtDepth } from "../../src/tree";
7+
import { toGindex } from "../../src";
8+
9+
describe("toSnapshot and fromSnapshot", () => {
10+
const depth = 4;
11+
const maxItems = Math.pow(2, depth);
12+
13+
for (let count = 0; count <= maxItems; count ++) {
14+
it(`toSnapshot and fromSnapshot with count ${count}`, () => {
15+
const nodes = Array.from({length: count}, (_, i) => LeafNode.fromRoot(Buffer.alloc(32, i)));
16+
const fullListRootNode = subtreeFillToContents(nodes, depth);
17+
const snapshot = toSnapshot(fullListRootNode, depth, count);
18+
const partialListRootNode = fromSnapshot(snapshot, depth);
19+
20+
// 1st step - check if the restored root node is the same
21+
expect(partialListRootNode.root).to.deep.equal(fullListRootNode.root);
22+
23+
// 2nd step - make sure we can add more nodes to the restored tree
24+
const fullTree = new Tree(fullListRootNode);
25+
const partialTree = new Tree(partialListRootNode);
26+
for (let i = count; i < maxItems; i++) {
27+
const gindex = toGindex(depth, BigInt(i));
28+
fullTree.setNode(gindex, LeafNode.fromRoot(Buffer.alloc(32, i)));
29+
partialTree.setNode(gindex, LeafNode.fromRoot(Buffer.alloc(32, i)));
30+
expect(partialTree.root).to.deep.equal(fullTree.root);
31+
32+
// and snapshot created from 2 trees are the same
33+
const snapshot1 = toSnapshot(fullTree.rootNode, depth, i + 1);
34+
const snapshot2 = toSnapshot(partialTree.rootNode, depth, i + 1);
35+
expect(snapshot2).to.deep.equal(snapshot1);
36+
}
37+
});
38+
39+
// setNodesAtDepth() api is what ssz uses to grow the tree in its commit() phase
40+
it(`toSnapshot and fromSnapshot with count ${count} then grow with setNodeAtDepth`, () => {
41+
const nodes = Array.from({length: count}, (_, i) => LeafNode.fromRoot(Buffer.alloc(32, i)));
42+
const fullListRootNode = subtreeFillToContents(nodes, depth);
43+
const snapshot = toSnapshot(fullListRootNode, depth, count);
44+
const partialListRootNode = fromSnapshot(snapshot, depth);
45+
46+
// 1st step - check if the restored root node is the same
47+
expect(partialListRootNode.root).to.deep.equal(fullListRootNode.root);
48+
49+
// 2nd step - grow the tree with setNodesAtDepth
50+
for (let i = count; i < maxItems; i++) {
51+
const addedNodes = Array.from({length: i - count + 1}, (_, j) => LeafNode.fromRoot(Buffer.alloc(32, j)));
52+
const indices = Array.from({length: i - count + 1}, (_, j) => j + count);
53+
const root1 = setNodesAtDepth(fullListRootNode, depth, indices, addedNodes);
54+
const root2 = setNodesAtDepth(partialListRootNode, depth, indices, addedNodes);
55+
expect(root2.root).to.deep.equal(root1.root);
56+
57+
for (let j = count; j <= i; j++) {
58+
const snapshot1 = toSnapshot(root1, depth, j);
59+
const snapshot2 = toSnapshot(root2, depth, j);
60+
expect(snapshot2).to.deep.equal(snapshot1);
61+
}
62+
}
63+
});
64+
65+
it(`toSnapshot() multiple times with count ${count}`, () => {
66+
const nodes = Array.from({length: count}, (_, i) => LeafNode.fromRoot(Buffer.alloc(32, i)));
67+
const fullListRootNode = subtreeFillToContents(nodes, depth);
68+
const snapshot = toSnapshot(fullListRootNode, depth, count);
69+
const partialListRootNode = fromSnapshot(snapshot, depth);
70+
71+
// 1st step - check if the restored root node is the same
72+
expect(partialListRootNode.root).to.deep.equal(fullListRootNode.root);
73+
74+
const snapshot2 = toSnapshot(partialListRootNode, depth, count);
75+
const restoredRootNode2 = fromSnapshot(snapshot2, depth);
76+
77+
// 2nd step - check if the restored root node is the same
78+
expect(restoredRootNode2.root).to.deep.equal(partialListRootNode.root);
79+
});
80+
}
81+
});
82+
83+
describe("indexToFinalizedGindices", () => {
84+
// given a tree with depth = 4
85+
// 1
86+
// 2 3
87+
// 4 5 6 7
88+
// 8 9 10 11 12 13 14 15
89+
// 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
90+
const testCases: [number, number, bigint[]][] = [
91+
[4, 0, [BigInt(16)]],
92+
[4, 1, [BigInt(8)]],
93+
[4, 2, [8, 18].map(BigInt)],
94+
[4, 3, [4].map(BigInt)],
95+
[4, 4, [4, 20].map(BigInt)],
96+
[4, 5, [4, 10].map(BigInt)],
97+
[4, 6, [4, 10, 22].map(BigInt)],
98+
[4, 7, [2].map(BigInt)],
99+
[4, 8, [2, 24].map(BigInt)],
100+
[4, 9, [2, 12].map(BigInt)],
101+
[4, 10, [2, 12, 26].map(BigInt)],
102+
[4, 11, [2, 6].map(BigInt)],
103+
[4, 12, [2, 6, 28].map(BigInt)],
104+
[4, 13, [2, 6, 14].map(BigInt)],
105+
[4, 14, [2, 6, 14, 30].map(BigInt)],
106+
[4, 15, [1].map(BigInt)],
107+
];
108+
109+
for (const [depth, index, finalizeGindices] of testCases) {
110+
it(`should correctly get finalized gindexes for index ${index} and depth ${depth}`, () => {
111+
const actual = indexToFinalizedGindices(depth, index);
112+
expect(actual).to.deep.equal(finalizeGindices);
113+
});
114+
}
115+
});

packages/ssz/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,12 @@
2626
"benchmark:local": "yarn benchmark --local",
2727
"test:perf": "mocha \"test/perf/**/*.test.ts\"",
2828
"test:unit": "nyc mocha \"test/unit/**/*.test.ts\"",
29-
"test:spec": "yarn test:spec-generic && yarn test:spec-static",
29+
"test:spec": "yarn test:spec-generic && yarn test:spec-static test:spec-eip-4881",
3030
"test:spec-generic": "mocha \"test/spec/generic/**/*.test.ts\"",
3131
"test:spec-static": "yarn test:spec-static-minimal && yarn test:spec-static-mainnet",
3232
"test:spec-static-minimal": "LODESTAR_PRESET=minimal mocha test/spec/ssz_static.test.ts",
3333
"test:spec-static-mainnet": "LODESTAR_PRESET=mainnet mocha test/spec/ssz_static.test.ts",
34+
"test:spec-eip-4881": "mocha \"test/spec/eip-4881/**/*.test.ts\"",
3435
"download-spec-tests": "node -r ts-node/register test/spec/downloadTests.ts"
3536
},
3637
"types": "lib/index.d.ts",

packages/ssz/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export {ContainerType} from "./type/container";
88
export {ContainerNodeStructType} from "./type/containerNodeStruct";
99
export {ListBasicType} from "./type/listBasic";
1010
export {ListCompositeType} from "./type/listComposite";
11+
export {PartialListCompositeType} from "./type/partialListComposite";
1112
export {NoneType} from "./type/none";
1213
export {UintBigintType, UintNumberType} from "./type/uint";
1314
export {UnionType} from "./type/union";
@@ -34,5 +35,5 @@ export {BitArray, getUint8ByteToBitBooleanArray} from "./value/bitArray";
3435

3536
// Utils
3637
export {fromHexString, toHexString, byteArrayEquals} from "./util/byteArray";
37-
38+
export {Snapshot} from "./util/types";
3839
export {hash64, symbolCachedPermanentRoot} from "./util/merkleize";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {fromSnapshot, zeroNode} from "@chainsafe/persistent-merkle-tree";
2+
import {CompositeType, CompositeView, CompositeViewDU} from "./composite";
3+
import {ListCompositeOpts, ListCompositeType} from "./listComposite";
4+
import {PartialListCompositeTreeViewDU} from "../viewDU/partialListComposite";
5+
import {Snapshot} from "../util/types";
6+
import {byteArrayEquals} from "../util/byteArray";
7+
import {zeroSnapshot} from "../util/snapshot";
8+
import {addLengthNode} from "./arrayBasic";
9+
10+
/**
11+
* Similar to ListCompositeType, this is mainly used to create a PartialListCompositeTreeViewDU from a snapshot.
12+
* The ViewDU created is a partial tree created from a snapshot, not a full tree.
13+
* Note that this class only inherits minimal methods as defined in ArrayType of ../view/arrayBasic.ts
14+
* It'll throw errors for all other methods, most of the usage is in the ViewDU class.
15+
*/
16+
export class PartialListCompositeType<
17+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
18+
ElementType extends CompositeType<any, CompositeView<ElementType>, CompositeViewDU<ElementType>>
19+
> extends ListCompositeType<ElementType> {
20+
constructor(readonly elementType: ElementType, readonly limit: number, opts?: ListCompositeOpts) {
21+
super(elementType, limit, opts);
22+
23+
// only inherit methods in ArrayType of ../view/arrayBasic.ts
24+
const inheritedMethods = [
25+
"tree_getLength",
26+
"tree_setLength",
27+
"tree_getChunksNode",
28+
"tree_chunksNodeOffset",
29+
"tree_setChunksNode",
30+
];
31+
const methodNames = Object.getOwnPropertyNames(ListCompositeType.prototype).filter(
32+
(prop) =>
33+
prop !== "constructor" &&
34+
typeof (this as unknown as Record<string, unknown>)[prop] === "function" &&
35+
!inheritedMethods.includes(prop)
36+
);
37+
38+
// throw errors for all remaining methods
39+
for (const methodName of methodNames) {
40+
(this as unknown as Record<string, unknown>)[methodName] = () => {
41+
throw new Error(`Method ${methodName} is not implemented for PartialListCompositeType`);
42+
};
43+
}
44+
}
45+
46+
/**
47+
* Create a PartialListCompositeTreeViewDU from a snapshot.
48+
*/
49+
toPartialViewDU(snapshot: Snapshot): PartialListCompositeTreeViewDU<ElementType> {
50+
const chunksNode = fromSnapshot(snapshot, this.chunkDepth);
51+
const rootNode = addLengthNode(chunksNode, snapshot.count);
52+
53+
if (!byteArrayEquals(rootNode.root, snapshot.root)) {
54+
throw new Error(`Snapshot root is incorrect, expected ${snapshot.root}, got ${rootNode.root}`);
55+
}
56+
57+
return new PartialListCompositeTreeViewDU(this, rootNode, snapshot);
58+
}
59+
60+
/**
61+
* Creates a PartialListCompositeTreeViewDU from a zero snapshot.
62+
*/
63+
defaultPartialViewDU(): PartialListCompositeTreeViewDU<ElementType> {
64+
const rootNode = addLengthNode(zeroNode(this.chunkDepth), 0);
65+
66+
return new PartialListCompositeTreeViewDU(this, rootNode, zeroSnapshot(this.chunkDepth));
67+
}
68+
}

packages/ssz/src/util/snapshot.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {zeroHash} from "@chainsafe/persistent-merkle-tree";
2+
import {hash64} from "./merkleize";
3+
import {Snapshot} from "./types";
4+
5+
/**
6+
* Create a zero snapshot with the given chunksDepth.
7+
*/
8+
export function zeroSnapshot(chunkDepth: number): Snapshot {
9+
return {
10+
finalized: [],
11+
count: 0,
12+
root: hash64(zeroHash(chunkDepth), zeroHash(0)),
13+
};
14+
}

packages/ssz/src/util/types.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1 +1,12 @@
11
export type Require<T, K extends keyof T> = T & Required<Pick<T, K>>;
2+
3+
/**
4+
* A snapshot contains the minimum amount of information needed to reconstruct a merkleized list, for the purposes of appending more items.
5+
* Note: This does not contain list elements, rather only contains intermediate merkle nodes.
6+
* This is used primarily for PartialListCompositeType.
7+
*/
8+
export type Snapshot = {
9+
finalized: Uint8Array[];
10+
root: Uint8Array;
11+
count: number;
12+
};

packages/ssz/src/view/arrayBasic.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ export type ArrayBasicType<ElementType extends BasicType<unknown>> = CompositeTy
1010
ValueOf<ElementType>[],
1111
TreeView<ArrayBasicType<ElementType>>,
1212
TreeViewDU<ArrayBasicType<ElementType>>
13-
> & {
14-
readonly elementType: ElementType;
15-
readonly itemsPerChunk: number;
16-
readonly chunkDepth: number;
17-
13+
> &
14+
ArrayType & {
15+
readonly elementType: ElementType;
16+
readonly itemsPerChunk: number;
17+
readonly chunkDepth: number;
18+
};
19+
20+
/** Common type for both ArrayBasicType and ArrayCompositeTypesrc/view/arrayBasic.ts */
21+
export type ArrayType = {
1822
/** INTERNAL METHOD: Return the length of this type from an Array's root node */
1923
tree_getLength(node: Node): number;
2024
/** INTERNAL METHOD: Mutate a tree's rootNode with a new length value */

packages/ssz/src/view/arrayComposite.ts

+7-22
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,17 @@
1-
import {getNodesAtDepth, Node, toGindexBitstring, Tree, HashComputationLevel} from "@chainsafe/persistent-merkle-tree";
1+
import {getNodesAtDepth, Node, toGindexBitstring, Tree} from "@chainsafe/persistent-merkle-tree";
22
import {ValueOf} from "../type/abstract";
33
import {CompositeType, CompositeView, CompositeViewDU} from "../type/composite";
44
import {TreeView} from "./abstract";
5+
import {ArrayType} from "./arrayBasic";
56

67
/** Expected API of this View's type. This interface allows to break a recursive dependency between types and views */
78
export type ArrayCompositeType<
89
ElementType extends CompositeType<unknown, CompositeView<ElementType>, CompositeViewDU<ElementType>>
9-
> = CompositeType<ValueOf<ElementType>[], unknown, unknown> & {
10-
readonly elementType: ElementType;
11-
readonly chunkDepth: number;
12-
13-
/** INTERNAL METHOD: Return the length of this type from an Array's root node */
14-
tree_getLength(node: Node): number;
15-
/** INTERNAL METHOD: Mutate a tree's rootNode with a new length value */
16-
tree_setLength(tree: Tree, length: number): void;
17-
/** INTERNAL METHOD: Return the chunks node from a root node */
18-
tree_getChunksNode(rootNode: Node): Node;
19-
/** INTERNAL METHOD: Return the offset from root for HashComputation */
20-
tree_chunksNodeOffset(): number;
21-
/** INTERNAL METHOD: Return a new root node with changed chunks node and length */
22-
tree_setChunksNode(
23-
rootNode: Node,
24-
chunksNode: Node,
25-
newLength: number | null,
26-
hcOffset?: number,
27-
hcByLevel?: HashComputationLevel[] | null
28-
): Node;
29-
};
10+
> = CompositeType<ValueOf<ElementType>[], unknown, unknown> &
11+
ArrayType & {
12+
readonly elementType: ElementType;
13+
readonly chunkDepth: number;
14+
};
3015

3116
export class ArrayCompositeTreeView<
3217
ElementType extends CompositeType<ValueOf<ElementType>, CompositeView<ElementType>, CompositeViewDU<ElementType>>

0 commit comments

Comments
 (0)