From a6191d46b6d9de24eb28f206e750b5710a0f3793 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 9 Aug 2024 13:39:29 +0700 Subject: [PATCH] feat: implement ViewDU.batchHashTreeRoot() --- packages/ssz/src/type/arrayBasic.ts | 18 +- packages/ssz/src/type/bitArray.ts | 6 +- packages/ssz/src/type/byteArray.ts | 19 +- packages/ssz/src/type/composite.ts | 3 +- packages/ssz/src/type/container.ts | 9 +- packages/ssz/src/type/listBasic.ts | 25 +- packages/ssz/src/type/listComposite.ts | 25 +- packages/ssz/src/type/optional.ts | 18 +- packages/ssz/src/type/union.ts | 18 +- packages/ssz/src/type/vectorBasic.ts | 14 +- packages/ssz/src/type/vectorComposite.ts | 14 +- packages/ssz/src/view/arrayBasic.ts | 12 +- packages/ssz/src/view/arrayComposite.ts | 12 +- packages/ssz/src/viewDU/abstract.ts | 39 +- packages/ssz/src/viewDU/arrayBasic.ts | 34 +- packages/ssz/src/viewDU/arrayComposite.ts | 40 +- packages/ssz/src/viewDU/bitArray.ts | 8 +- packages/ssz/src/viewDU/container.ts | 41 +- .../ssz/src/viewDU/containerNodeStruct.ts | 32 +- .../ssz/test/perf/eth2/hashTreeRoot.test.ts | 12 +- packages/ssz/test/spec/runValidTest.ts | 14 + .../test/unit/byType/bitArray/tree.test.ts | 17 + .../test/unit/byType/bitVector/tree.test.ts | 17 + .../test/unit/byType/container/tree.test.ts | 417 ++++++++++++++++++ .../test/unit/byType/listBasic/tree.test.ts | 75 ++++ .../unit/byType/listComposite/tree.test.ts | 124 +++++- .../test/unit/byType/optional/tree.test.ts | 4 +- .../test/unit/byType/runViewTestMutation.ts | 85 ++-- .../test/unit/byType/vectorBasic/tree.test.ts | 67 +++ .../unit/byType/vectorComposite/tree.test.ts | 121 +++++ .../ssz/test/unit/eth2/beaconState.test.ts | 200 +++++++++ .../ssz/test/unit/eth2/validators.test.ts | 26 +- packages/ssz/test/unit/regressions.test.ts | 2 + .../ssz/test/unit/unchangedViewDUs.test.ts | 29 ++ yarn.lock | 21 + 35 files changed, 1496 insertions(+), 122 deletions(-) create mode 100644 packages/ssz/test/unit/byType/vectorBasic/tree.test.ts create mode 100644 packages/ssz/test/unit/byType/vectorComposite/tree.test.ts create mode 100644 packages/ssz/test/unit/eth2/beaconState.test.ts create mode 100644 packages/ssz/test/unit/unchangedViewDUs.test.ts diff --git a/packages/ssz/src/type/arrayBasic.ts b/packages/ssz/src/type/arrayBasic.ts index c731e02a..2ad5d3c3 100644 --- a/packages/ssz/src/type/arrayBasic.ts +++ b/packages/ssz/src/type/arrayBasic.ts @@ -5,6 +5,8 @@ import { getNodesAtDepth, packedNodeRootsToBytes, packedRootsBytesToNode, + HashComputationLevel, + levelAtIndex, } from "@chainsafe/persistent-merkle-tree"; import {Type, ValueOf, ByteViews} from "./abstract"; import {BasicType} from "./basic"; @@ -39,14 +41,24 @@ export function addLengthNode(chunksNode: Node, length: number): Node { return new BranchNode(chunksNode, LeafNode.fromUint32(length)); } -export function setChunksNode(rootNode: Node, chunksNode: Node, newLength?: number): Node { +export function setChunksNode( + rootNode: Node, + chunksNode: Node, + newLength: number | null, + hcOffset = 0, + hcByLevel: HashComputationLevel[] | null = null +): Node { const lengthNode = - newLength !== undefined + newLength !== null ? // If newLength is set, create a new node for length LeafNode.fromUint32(newLength) : // else re-use existing node (rootNode.right as LeafNode); - return new BranchNode(chunksNode, lengthNode); + const branchNode = new BranchNode(chunksNode, lengthNode); + if (hcByLevel !== null) { + levelAtIndex(hcByLevel, hcOffset).push(chunksNode, lengthNode, branchNode); + } + return branchNode; } export type ArrayProps = {isList: true; limit: number} | {isList: false; length: number}; diff --git a/packages/ssz/src/type/bitArray.ts b/packages/ssz/src/type/bitArray.ts index 5351286f..5071550c 100644 --- a/packages/ssz/src/type/bitArray.ts +++ b/packages/ssz/src/type/bitArray.ts @@ -1,4 +1,4 @@ -import {concatGindices, Gindex, Node, toGindex, Tree} from "@chainsafe/persistent-merkle-tree"; +import {concatGindices, Gindex, Node, toGindex, Tree, HashComputationLevel} from "@chainsafe/persistent-merkle-tree"; import {fromHexString, toHexString, byteArrayEquals} from "../util/byteArray"; import {splitIntoRootChunks} from "../util/merkleize"; import {CompositeType, LENGTH_GINDEX} from "./composite"; @@ -29,8 +29,8 @@ export abstract class BitArrayType extends CompositeType extends Type { /** INTERNAL METHOD: Given a Tree View, returns a `Node` with all its updated data */ abstract commitView(view: TV): Node; /** INTERNAL METHOD: Given a Deferred Update Tree View returns a `Node` with all its updated data */ - abstract commitViewDU(view: TVDU): Node; + abstract commitViewDU(view: TVDU, hcOffset?: number, hcByLevel?: HashComputationLevel[] | null): Node; /** INTERNAL METHOD: Return the cache of a Deferred Update Tree View. May return `undefined` if this ViewDU has no cache */ abstract cacheOfViewDU(view: TVDU): unknown; diff --git a/packages/ssz/src/type/container.ts b/packages/ssz/src/type/container.ts index 97b10aa1..daa1911d 100644 --- a/packages/ssz/src/type/container.ts +++ b/packages/ssz/src/type/container.ts @@ -7,6 +7,7 @@ import { toGindex, concatGindices, getNode, + HashComputationLevel, } from "@chainsafe/persistent-merkle-tree"; import {maxChunksToDepth} from "../util/merkleize"; import {Require} from "../util/types"; @@ -162,8 +163,12 @@ export class ContainerType>> extends return view.node; } - commitViewDU(view: ContainerTreeViewDUType): Node { - view.commit(); + commitViewDU( + view: ContainerTreeViewDUType, + hcOffset = 0, + hcByLevel: HashComputationLevel[] | null = null + ): Node { + view.commit(hcOffset, hcByLevel); return view.node; } diff --git a/packages/ssz/src/type/listBasic.ts b/packages/ssz/src/type/listBasic.ts index 53ae8783..c9e397e6 100644 --- a/packages/ssz/src/type/listBasic.ts +++ b/packages/ssz/src/type/listBasic.ts @@ -1,4 +1,4 @@ -import {LeafNode, Node, Tree} from "@chainsafe/persistent-merkle-tree"; +import {LeafNode, Node, Tree, HashComputationLevel} from "@chainsafe/persistent-merkle-tree"; import {ValueOf} from "./abstract"; import {BasicType} from "./basic"; import {ByteViews} from "./composite"; @@ -93,8 +93,12 @@ export class ListBasicType> return view.node; } - commitViewDU(view: ListBasicTreeViewDU): Node { - view.commit(); + commitViewDU( + view: ListBasicTreeViewDU, + hcOffset = 0, + hcByLevel: HashComputationLevel[] | null = null + ): Node { + view.commit(hcOffset, hcByLevel); return view.node; } @@ -144,8 +148,19 @@ export class ListBasicType> return node.left; } - tree_setChunksNode(rootNode: Node, chunksNode: Node, newLength?: number): Node { - return setChunksNode(rootNode, chunksNode, newLength); + tree_chunksNodeOffset(): number { + // one more level for length, see setChunksNode below + return 1; + } + + tree_setChunksNode( + rootNode: Node, + chunksNode: Node, + newLength: number | null, + hcOffset = 0, + hcByLevel: HashComputationLevel[] | null = null + ): Node { + return setChunksNode(rootNode, chunksNode, newLength, hcOffset, hcByLevel); } // Merkleization diff --git a/packages/ssz/src/type/listComposite.ts b/packages/ssz/src/type/listComposite.ts index 48fc37df..dad8e77c 100644 --- a/packages/ssz/src/type/listComposite.ts +++ b/packages/ssz/src/type/listComposite.ts @@ -1,4 +1,4 @@ -import {Node, Tree} from "@chainsafe/persistent-merkle-tree"; +import {Node, Tree, HashComputationLevel} from "@chainsafe/persistent-merkle-tree"; import { mixInLength, maxChunksToDepth, @@ -97,8 +97,12 @@ export class ListCompositeType< return view.node; } - commitViewDU(view: ListCompositeTreeViewDU): Node { - view.commit(); + commitViewDU( + view: ListCompositeTreeViewDU, + hcOffset = 0, + hcByLevel: HashComputationLevel[] | null = null + ): Node { + view.commit(hcOffset, hcByLevel); return view.node; } @@ -150,8 +154,19 @@ export class ListCompositeType< return node.left; } - tree_setChunksNode(rootNode: Node, chunksNode: Node, newLength?: number): Node { - return setChunksNode(rootNode, chunksNode, newLength); + tree_chunksNodeOffset(): number { + // one more level for length, see setChunksNode below + return 1; + } + + tree_setChunksNode( + rootNode: Node, + chunksNode: Node, + newLength: number | null, + hcOffset = 0, + hcByLevel: HashComputationLevel[] | null = null + ): Node { + return setChunksNode(rootNode, chunksNode, newLength, hcOffset, hcByLevel); } // Merkleization diff --git a/packages/ssz/src/type/optional.ts b/packages/ssz/src/type/optional.ts index 59c38d6b..5ae7e2bb 100644 --- a/packages/ssz/src/type/optional.ts +++ b/packages/ssz/src/type/optional.ts @@ -1,4 +1,12 @@ -import {concatGindices, Gindex, Node, Tree, zeroNode} from "@chainsafe/persistent-merkle-tree"; +import { + concatGindices, + Gindex, + Node, + Tree, + zeroNode, + HashComputationLevel, + getHashComputations, +} from "@chainsafe/persistent-merkle-tree"; import {mixInLength} from "../util/merkleize"; import {Require} from "../util/types"; import {namedClass} from "../util/named"; @@ -75,8 +83,12 @@ export class OptionalType> extends CompositeTy } // TODO add an OptionalViewDU - commitViewDU(view: ValueOfType): Node { - return this.value_toTree(view); + commitViewDU(view: ValueOfType, hcOffset = 0, hcByLevel: HashComputationLevel[] | null = null): Node { + const node = this.value_toTree(view); + if (hcByLevel !== null && node.h0 === null) { + getHashComputations(node, hcOffset, hcByLevel); + } + return node; } // TODO add an OptionalViewDU diff --git a/packages/ssz/src/type/union.ts b/packages/ssz/src/type/union.ts index c6fea712..fbd7f97a 100644 --- a/packages/ssz/src/type/union.ts +++ b/packages/ssz/src/type/union.ts @@ -1,4 +1,12 @@ -import {concatGindices, getNode, Gindex, Node, Tree} from "@chainsafe/persistent-merkle-tree"; +import { + concatGindices, + getNode, + Gindex, + Node, + Tree, + HashComputationLevel, + getHashComputations, +} from "@chainsafe/persistent-merkle-tree"; import {mixInLength} from "../util/merkleize"; import {Require} from "../util/types"; import {namedClass} from "../util/named"; @@ -106,8 +114,12 @@ export class UnionType[]> extends CompositeType< return this.value_toTree(view); } - commitViewDU(view: ValueOfTypes): Node { - return this.value_toTree(view); + commitViewDU(view: ValueOfTypes, hcOffset = 0, hcByLevel: HashComputationLevel[] | null = null): Node { + const node = this.value_toTree(view); + if (hcByLevel !== null && node.h0 === null) { + getHashComputations(node, hcOffset, hcByLevel); + } + return node; } value_serializedSize(value: ValueOfTypes): number { diff --git a/packages/ssz/src/type/vectorBasic.ts b/packages/ssz/src/type/vectorBasic.ts index 061008e0..d52a9405 100644 --- a/packages/ssz/src/type/vectorBasic.ts +++ b/packages/ssz/src/type/vectorBasic.ts @@ -1,4 +1,4 @@ -import {Node, Tree} from "@chainsafe/persistent-merkle-tree"; +import {Node, Tree, HashComputationLevel} from "@chainsafe/persistent-merkle-tree"; import {maxChunksToDepth, splitIntoRootChunks} from "../util/merkleize"; import {Require} from "../util/types"; import {namedClass} from "../util/named"; @@ -83,8 +83,12 @@ export class VectorBasicType> return view.node; } - commitViewDU(view: ArrayBasicTreeViewDU): Node { - view.commit(); + commitViewDU( + view: ArrayBasicTreeViewDU, + hcOffset = 0, + hcByLevel: HashComputationLevel[] | null = null + ): Node { + view.commit(hcOffset, hcByLevel); return view.node; } @@ -132,6 +136,10 @@ export class VectorBasicType> return node; } + tree_chunksNodeOffset(): number { + return 0; + } + tree_setChunksNode(rootNode: Node, chunksNode: Node): Node { return chunksNode; } diff --git a/packages/ssz/src/type/vectorComposite.ts b/packages/ssz/src/type/vectorComposite.ts index 68455bb1..e1af8dd4 100644 --- a/packages/ssz/src/type/vectorComposite.ts +++ b/packages/ssz/src/type/vectorComposite.ts @@ -1,4 +1,4 @@ -import {Node, Tree} from "@chainsafe/persistent-merkle-tree"; +import {Node, Tree, HashComputationLevel} from "@chainsafe/persistent-merkle-tree"; import {maxChunksToDepth} from "../util/merkleize"; import {Require} from "../util/types"; import {namedClass} from "../util/named"; @@ -90,8 +90,12 @@ export class VectorCompositeType< return view.node; } - commitViewDU(view: ArrayCompositeTreeViewDU): Node { - view.commit(); + commitViewDU( + view: ArrayCompositeTreeViewDU, + hcOffset = 0, + hcByLevel: HashComputationLevel[] | null = null + ): Node { + view.commit(hcOffset, hcByLevel); return view.node; } @@ -139,6 +143,10 @@ export class VectorCompositeType< return node; } + tree_chunksNodeOffset(): number { + return 0; + } + tree_setChunksNode(rootNode: Node, chunksNode: Node): Node { return chunksNode; } diff --git a/packages/ssz/src/view/arrayBasic.ts b/packages/ssz/src/view/arrayBasic.ts index e96ce1d1..3b58051b 100644 --- a/packages/ssz/src/view/arrayBasic.ts +++ b/packages/ssz/src/view/arrayBasic.ts @@ -1,4 +1,4 @@ -import {getNodesAtDepth, LeafNode, Node, Tree} from "@chainsafe/persistent-merkle-tree"; +import {getNodesAtDepth, LeafNode, Node, Tree, HashComputationLevel} from "@chainsafe/persistent-merkle-tree"; import {ValueOf} from "../type/abstract"; import {BasicType} from "../type/basic"; import {CompositeType} from "../type/composite"; @@ -21,8 +21,16 @@ export type ArrayBasicType> = CompositeTy tree_setLength(tree: Tree, length: number): void; /** INTERNAL METHOD: Return the chunks node from a root node */ tree_getChunksNode(rootNode: Node): Node; + /** INTERNAL METHOD: Return the offset from root for HashComputation */ + tree_chunksNodeOffset(): number; /** INTERNAL METHOD: Return a new root node with changed chunks node and length */ - tree_setChunksNode(rootNode: Node, chunksNode: Node, newLength?: number): Node; + tree_setChunksNode( + rootNode: Node, + chunksNode: Node, + newLength: number | null, + hcOffset?: number, + hcByLevel?: HashComputationLevel[] | null + ): Node; }; export class ArrayBasicTreeView> extends TreeView> { diff --git a/packages/ssz/src/view/arrayComposite.ts b/packages/ssz/src/view/arrayComposite.ts index 252a3587..4bac64e0 100644 --- a/packages/ssz/src/view/arrayComposite.ts +++ b/packages/ssz/src/view/arrayComposite.ts @@ -1,4 +1,4 @@ -import {getNodesAtDepth, Node, toGindexBitstring, Tree} from "@chainsafe/persistent-merkle-tree"; +import {getNodesAtDepth, Node, toGindexBitstring, Tree, HashComputationLevel} from "@chainsafe/persistent-merkle-tree"; import {ValueOf} from "../type/abstract"; import {CompositeType, CompositeView, CompositeViewDU} from "../type/composite"; import {TreeView} from "./abstract"; @@ -16,8 +16,16 @@ export type ArrayCompositeType< tree_setLength(tree: Tree, length: number): void; /** INTERNAL METHOD: Return the chunks node from a root node */ tree_getChunksNode(rootNode: Node): Node; + /** INTERNAL METHOD: Return the offset from root for HashComputation */ + tree_chunksNodeOffset(): number; /** INTERNAL METHOD: Return a new root node with changed chunks node and length */ - tree_setChunksNode(rootNode: Node, chunksNode: Node, newLength?: number): Node; + tree_setChunksNode( + rootNode: Node, + chunksNode: Node, + newLength: number | null, + hcOffset?: number, + hcByLevel?: HashComputationLevel[] | null + ): Node; }; export class ArrayCompositeTreeView< diff --git a/packages/ssz/src/viewDU/abstract.ts b/packages/ssz/src/viewDU/abstract.ts index 29878637..144268f5 100644 --- a/packages/ssz/src/viewDU/abstract.ts +++ b/packages/ssz/src/viewDU/abstract.ts @@ -1,6 +1,17 @@ +import {HashComputationLevel, executeHashComputations, HashComputationGroup} from "@chainsafe/persistent-merkle-tree"; import {ByteViews, CompositeType} from "../type/composite"; import {TreeView} from "../view/abstract"; +/** + * Always allocating a new HashComputationGroup for each hashTreeRoot() is not great for gc + * because a lot of ViewDUs are not changed and computed root already. + */ +const symbolCachedTreeRoot = Symbol("ssz_cached_tree_root"); + +export type NodeWithCachedTreeRoot = { + [symbolCachedTreeRoot]?: Uint8Array; +}; + /* eslint-disable @typescript-eslint/member-ordering */ /** @@ -19,7 +30,7 @@ export abstract class TreeViewDU> extend return values; } - commit(): void { + /** + * When we need to compute HashComputations (hcByLevel != null): + * - if old _rootNode is hashed, then only need to put pending changes to hcByLevel + * - if old _rootNode is not hashed, need to traverse and put to hcByLevel + */ + commit(hcOffset = 0, hcByLevel: HashComputationLevel[] | null = null): void { + const isOldRootHashed = this._rootNode.h0 !== null; if (this.nodesChanged.size === 0) { + if (!isOldRootHashed && hcByLevel !== null) { + getHashComputations(this._rootNode, hcOffset, hcByLevel); + } return; } @@ -164,15 +181,22 @@ export class ArrayBasicTreeViewDU> extend } const chunksNode = this.type.tree_getChunksNode(this._rootNode); - // TODO: Ensure fast setNodesAtDepth() method is correct - const newChunksNode = setNodesAtDepth(chunksNode, this.type.chunkDepth, indexes, nodes); + const offsetThis = hcOffset + this.type.tree_chunksNodeOffset(); + const byLevelThis = hcByLevel != null && isOldRootHashed ? hcByLevel : null; + const newChunksNode = setNodesAtDepth(chunksNode, this.type.chunkDepth, indexes, nodes, offsetThis, byLevelThis); this._rootNode = this.type.tree_setChunksNode( this._rootNode, newChunksNode, - this.dirtyLength ? this._length : undefined + this.dirtyLength ? this._length : null, + hcOffset, + isOldRootHashed ? hcByLevel : null ); + if (!isOldRootHashed && hcByLevel !== null) { + getHashComputations(this._rootNode, hcOffset, hcByLevel); + } + this.nodesChanged.clear(); this.dirtyLength = false; } diff --git a/packages/ssz/src/viewDU/arrayComposite.ts b/packages/ssz/src/viewDU/arrayComposite.ts index 81ca9e02..44c50375 100644 --- a/packages/ssz/src/viewDU/arrayComposite.ts +++ b/packages/ssz/src/viewDU/arrayComposite.ts @@ -1,4 +1,11 @@ -import {getNodeAtDepth, getNodesAtDepth, Node, setNodesAtDepth} from "@chainsafe/persistent-merkle-tree"; +import { + getHashComputations, + getNodeAtDepth, + getNodesAtDepth, + HashComputationLevel, + Node, + setNodesAtDepth, +} from "@chainsafe/persistent-merkle-tree"; import {ValueOf} from "../type/abstract"; import {CompositeType, CompositeView, CompositeViewDU} from "../type/composite"; import {ArrayCompositeType} from "../view/arrayComposite"; @@ -163,15 +170,29 @@ export class ArrayCompositeTreeViewDU< return values; } - commit(): void { + /** + * When we need to compute HashComputations (hcByLevel != null): + * - if old _rootNode is hashed, then only need to put pending changes to hcByLevel + * - if old _rootNode is not hashed, need to traverse and put to hcByLevel + */ + commit(hcOffset = 0, hcByLevel: HashComputationLevel[] | null = null): void { + const isOldRootHashed = this._rootNode.h0 !== null; if (this.viewsChanged.size === 0) { + if (!isOldRootHashed && hcByLevel !== null) { + getHashComputations(this._rootNode, hcOffset, hcByLevel); + } return; } + // each view may mutate hcByLevel at offset + depth + const offsetView = hcOffset + this.type.depth; + // Depth includes the extra level for the length node + const byLevelView = hcByLevel != null && isOldRootHashed ? hcByLevel : null; + const nodesChanged: {index: number; node: Node}[] = []; for (const [index, view] of this.viewsChanged) { - const node = this.type.elementType.commitViewDU(view); + const node = this.type.elementType.commitViewDU(view, offsetView, byLevelView); // Set new node in nodes array to ensure data represented in the tree and fast nodes access is equal this.nodes[index] = node; nodesChanged.push({index, node}); @@ -187,15 +208,22 @@ export class ArrayCompositeTreeViewDU< const nodes = nodesChangedSorted.map((entry) => entry.node); const chunksNode = this.type.tree_getChunksNode(this._rootNode); - // TODO: Ensure fast setNodesAtDepth() method is correct - const newChunksNode = setNodesAtDepth(chunksNode, this.type.chunkDepth, indexes, nodes); + const offsetThis = hcOffset + this.type.tree_chunksNodeOffset(); + const byLevelThis = hcByLevel != null && isOldRootHashed ? hcByLevel : null; + const newChunksNode = setNodesAtDepth(chunksNode, this.type.chunkDepth, indexes, nodes, offsetThis, byLevelThis); this._rootNode = this.type.tree_setChunksNode( this._rootNode, newChunksNode, - this.dirtyLength ? this._length : undefined + this.dirtyLength ? this._length : null, + hcOffset, + hcByLevel ); + if (!isOldRootHashed && hcByLevel !== null) { + getHashComputations(this._rootNode, hcOffset, hcByLevel); + } + this.viewsChanged.clear(); this.dirtyLength = false; } diff --git a/packages/ssz/src/viewDU/bitArray.ts b/packages/ssz/src/viewDU/bitArray.ts index 1c3a5421..b9c12d14 100644 --- a/packages/ssz/src/viewDU/bitArray.ts +++ b/packages/ssz/src/viewDU/bitArray.ts @@ -1,4 +1,4 @@ -import {Node} from "@chainsafe/persistent-merkle-tree"; +import {HashComputationLevel, Node, getHashComputations} from "@chainsafe/persistent-merkle-tree"; import {BitArray} from "../value/bitArray"; import {CompositeType} from "../type/composite"; import {TreeViewDU} from "./abstract"; @@ -22,10 +22,14 @@ export class BitArrayTreeViewDU extends TreeViewDU>> extends }; } - commit(): void { + /** + * When we need to compute HashComputations (hcByLevel != null): + * - if old _rootNode is hashed, then only need to put pending changes to hcByLevel + * - if old _rootNode is not hashed, need to traverse and put to hcByLevel + */ + commit(hcOffset = 0, hcByLevel: HashComputationLevel[] | null = null): void { + const isOldRootHashed = this._rootNode.h0 !== null; if (this.nodesChanged.size === 0 && this.viewsChanged.size === 0) { + if (!isOldRootHashed && hcByLevel !== null) { + getHashComputations(this._rootNode, hcOffset, hcByLevel); + } return; } + // each view may mutate hcByLevel at offset + depth + const offsetView = hcOffset + this.type.depth; + // if old root is not hashed, no need to pass hcByLevel to child view bc we need to do full traversal here + const byLevelView = hcByLevel != null && isOldRootHashed ? hcByLevel : null; + const nodesChanged: {index: number; node: Node}[] = []; for (const [index, view] of this.viewsChanged) { const fieldType = this.type.fieldsEntries[index].fieldType as unknown as CompositeTypeAny; - const node = fieldType.commitViewDU(view); + const node = fieldType.commitViewDU(view, offsetView, byLevelView); // Set new node in nodes array to ensure data represented in the tree and fast nodes access is equal this.nodes[index] = node; nodesChanged.push({index, node}); @@ -96,7 +117,19 @@ class ContainerTreeViewDU>> extends const indexes = nodesChangedSorted.map((entry) => entry.index); const nodes = nodesChangedSorted.map((entry) => entry.node); - this._rootNode = setNodesAtDepth(this._rootNode, this.type.depth, indexes, nodes); + this._rootNode = setNodesAtDepth( + this._rootNode, + this.type.depth, + indexes, + nodes, + hcOffset, + isOldRootHashed ? hcByLevel : null + ); + + // old root is not hashed, need to traverse + if (!isOldRootHashed && hcByLevel !== null) { + getHashComputations(this._rootNode, hcOffset, hcByLevel); + } this.nodesChanged.clear(); this.viewsChanged.clear(); diff --git a/packages/ssz/src/viewDU/containerNodeStruct.ts b/packages/ssz/src/viewDU/containerNodeStruct.ts index c69cd45a..9aa45ed7 100644 --- a/packages/ssz/src/viewDU/containerNodeStruct.ts +++ b/packages/ssz/src/viewDU/containerNodeStruct.ts @@ -1,4 +1,4 @@ -import {Node} from "@chainsafe/persistent-merkle-tree"; +import {Node, HashComputationLevel} from "@chainsafe/persistent-merkle-tree"; import {Type, ValueOf} from "../type/abstract"; import {isCompositeType} from "../type/composite"; import {BranchNodeStruct} from "../branchNodeStruct"; @@ -8,7 +8,7 @@ import {TreeViewDU} from "./abstract"; /* eslint-disable @typescript-eslint/member-ordering */ -class ContainerTreeViewDU>> extends TreeViewDU< +export class ContainerNodeStructTreeViewDU>> extends TreeViewDU< ContainerTypeGeneric > { protected valueChanged: ValueOfFields | null = null; @@ -27,15 +27,27 @@ class ContainerTreeViewDU>> extends return; } - commit(): void { - if (this.valueChanged === null) { - return; - } + get value(): ValueOfFields { + return this.valueChanged ?? this._rootNode.value; + } - const value = this.valueChanged; - this.valueChanged = null; + /** + * There are 2 cases: + * - normal commit() or hashTreeRoot(): hcByLevel is null, no need to compute root + * - batchHashTreeRoot(): hcByLevel is not null, need to compute root because this does not support HashComputation + */ + commit(_?: number, hcByLevel: HashComputationLevel[] | null = null): void { + if (this.valueChanged !== null) { + const value = this.valueChanged; + this.valueChanged = null; + + this._rootNode = this.type.value_toTree(value) as BranchNodeStruct>; + } - this._rootNode = this.type.value_toTree(value) as BranchNodeStruct>; + if (this._rootNode.h0 === null && hcByLevel !== null) { + // consumer is batchHashTreeRoot() + this._rootNode.rootHashObject; + } } protected clearCache(): void { @@ -46,7 +58,7 @@ class ContainerTreeViewDU>> extends export function getContainerTreeViewDUClass>>( type: ContainerTypeGeneric ): ContainerTreeViewDUTypeConstructor { - class CustomContainerTreeViewDU extends ContainerTreeViewDU {} + class CustomContainerTreeViewDU extends ContainerNodeStructTreeViewDU {} // Dynamically define prototype methods for (let index = 0; index < type.fieldsEntries.length; index++) { diff --git a/packages/ssz/test/perf/eth2/hashTreeRoot.test.ts b/packages/ssz/test/perf/eth2/hashTreeRoot.test.ts index f94cc6ec..1ae6da27 100644 --- a/packages/ssz/test/perf/eth2/hashTreeRoot.test.ts +++ b/packages/ssz/test/perf/eth2/hashTreeRoot.test.ts @@ -1,5 +1,5 @@ import {itBench} from "@dapplion/benchmark"; -import {hasher, uint8ArrayToHashObject} from "@chainsafe/persistent-merkle-tree"; +import {HashComputationGroup, hasher, uint8ArrayToHashObject} from "@chainsafe/persistent-merkle-tree"; import * as sszPhase0 from "../../lodestarTypes/phase0/sszTypes"; import * as sszAltair from "../../lodestarTypes/altair/sszTypes"; import { @@ -68,6 +68,16 @@ describe("HashTreeRoot frequent eth2 objects", () => { }, }); + const hc = new HashComputationGroup(); + itBench, Uint8Array>({ + id: `BeaconState vc ${validatorCount} - batchHashTreeRoot tree`, + before: () => getStateViewDU().serialize(), + beforeEach: (bytes) => sszAltair.BeaconState.deserializeToViewDU(bytes), + fn: (state) => { + state.batchHashTreeRoot(hc); + }, + }); + for (const {fieldName, fieldType} of sszAltair.BeaconState.fieldsEntries) { // Only benchmark big data structures if (fieldType.maxSize < 10e6 || !isCompositeType(fieldType)) { diff --git a/packages/ssz/test/spec/runValidTest.ts b/packages/ssz/test/spec/runValidTest.ts index 56307baf..1bac7760 100644 --- a/packages/ssz/test/spec/runValidTest.ts +++ b/packages/ssz/test/spec/runValidTest.ts @@ -117,6 +117,20 @@ export function runValidSszTest(type: Type, testData: ValidTestCaseData assertRoot(root, "type.hashTreeRoot()"); } + if (isCompositeType(type)) { + // batchHashTreeRoot() + const root = wrapErr(() => { + const node = type.value_toTree(testDataValue); + const viewDU = type.getViewDU(node); + if (viewDU instanceof TreeViewDU) { + return viewDU.batchHashTreeRoot(); + } else { + return type.hashTreeRoot(testDataValue); + } + }, "type.hashTreeRoot()"); + assertRoot(root, "ViewDU.batchHashTreeRoot()"); + } + // value -> tree - value_toTree() const node = wrapErr(() => type.value_toTree(testDataValue), "type.value_toTree()"); assertNode(node, "type.value_toTree()"); diff --git a/packages/ssz/test/unit/byType/bitArray/tree.test.ts b/packages/ssz/test/unit/byType/bitArray/tree.test.ts index 8d33314b..456243c5 100644 --- a/packages/ssz/test/unit/byType/bitArray/tree.test.ts +++ b/packages/ssz/test/unit/byType/bitArray/tree.test.ts @@ -1,3 +1,4 @@ +import {expect} from "chai"; import {BitVectorType, BitListType, BitArray} from "../../../../src"; import {runViewTestMutation} from "../runViewTestMutation"; @@ -50,6 +51,22 @@ for (const type of [new BitVectorType(4), new BitListType(4)]) { }); } +describe("BitArray batchHashTreeRoot", () => { + const sszType = new BitListType(4); + const value = fromNum(4, 0b0010); + const expectedRoot = sszType.toView(value).hashTreeRoot(); + + it("fresh ViewDU", () => { + expect(sszType.toViewDU(value).batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("set then hashTreeRoot", () => { + const viewDU = sszType.toViewDU(fromNum(4, 0b0011)); + viewDU.set(0, false); + expect(sszType.toViewDU(value).batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); +}); + function fromNum(bitLen: number, num: number): BitArray { const bitArray = BitArray.fromBitLen(bitLen); for (let i = 0; i < bitLen; i++) { diff --git a/packages/ssz/test/unit/byType/bitVector/tree.test.ts b/packages/ssz/test/unit/byType/bitVector/tree.test.ts index 8dbe47ef..04f2ee14 100644 --- a/packages/ssz/test/unit/byType/bitVector/tree.test.ts +++ b/packages/ssz/test/unit/byType/bitVector/tree.test.ts @@ -1,3 +1,4 @@ +import {expect} from "chai"; import {BitVectorType, BitArray} from "../../../../src"; import {runViewTestMutation} from "../runViewTestMutation"; @@ -48,6 +49,22 @@ runViewTestMutation({ ], }); +describe("BitVector batchHashTreeRoot", () => { + const sszType = new BitVectorType(4); + const value = fromNum(4, 0b0010); + const expectedRoot = sszType.toView(value).hashTreeRoot(); + + it("fresh ViewDU", () => { + expect(sszType.toViewDU(value).batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("set then batchHashTreeRoot", () => { + const viewDU = sszType.toViewDU(fromNum(4, 0b0011)); + viewDU.set(0, false); + expect(sszType.toViewDU(value).batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); +}); + function fromNum(bitLen: number, num: number): BitArray { const bitArray = BitArray.fromBitLen(bitLen); for (let i = 0; i < bitLen; i++) { diff --git a/packages/ssz/test/unit/byType/container/tree.test.ts b/packages/ssz/test/unit/byType/container/tree.test.ts index 91a68c4a..6b545792 100644 --- a/packages/ssz/test/unit/byType/container/tree.test.ts +++ b/packages/ssz/test/unit/byType/container/tree.test.ts @@ -1,5 +1,10 @@ import {expect} from "chai"; import { + BitArray, + BitListType, + BitVectorType, + BooleanType, + ByteListType, ByteVectorType, ContainerNodeStructType, ContainerType, @@ -7,8 +12,11 @@ import { ListCompositeType, NoneType, toHexString, + UintNumberType, UnionType, ValueOf, + VectorBasicType, + VectorCompositeType, } from "../../../../src"; import {uint64NumInfType, uint64NumType} from "../../../utils/primitiveTypes"; import {runViewTestMutation} from "../runViewTestMutation"; @@ -218,3 +226,412 @@ runViewTestMutation({ }, ], }); + +describe("ContainerViewDU batchHashTreeRoot", function () { + const childContainerType = new ContainerType({f0: uint64NumInfType, f1: uint64NumInfType}); + const unionType = new UnionType([new NoneType(), uint64NumType]); + const listBasicType = new ListBasicType(uint64NumType, 10); + const vectorBasicType = new VectorBasicType(uint64NumType, 2); + const listCompositeType = new ListCompositeType(childContainerType, 10); + const vectorCompositeType = new VectorCompositeType(childContainerType, 1); + const bitVectorType = new BitVectorType(64); + const bitListType = new BitListType(4); + const childContainerStruct = new ContainerNodeStructType({g0: uint64NumInfType, g1: uint64NumInfType}); + const parentContainerType = new ContainerType({ + a: uint64NumType, + b: new BooleanType(), + c: unionType, + d: new ByteListType(64), + e: new ByteVectorType(64), + // a child container type + f: childContainerType, + g: childContainerStruct, + h: listBasicType, + i: vectorBasicType, + j: listCompositeType, + k: vectorCompositeType, + l: bitVectorType, + m: bitListType, + // TODO: add more tests when OptionalType is implemented + }); + + const value: ValueOf = { + a: 10, + b: true, + c: {selector: 1, value: 100}, + d: Buffer.alloc(64, 2), + e: Buffer.alloc(64, 1), + f: {f0: 100, f1: 101}, + g: {g0: 100, g1: 101}, + h: [1, 2], + i: [1, 2], + j: [{f0: 1, f1: 2}], + k: [{f0: 1, f1: 2}], + l: BitArray.fromSingleBit(64, 5), + m: BitArray.fromSingleBit(4, 1), + }; + const expectedRoot = parentContainerType.toView(value).hashTreeRoot(); + + it("fresh ViewDU", () => { + expect(parentContainerType.toViewDU(value).batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify Number type", () => { + const viewDU = parentContainerType.toViewDU({...value, a: 9}); + viewDU.batchHashTreeRoot(); + viewDU.a += 1; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.a = 10; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify BooleanType", () => { + const viewDU = parentContainerType.toViewDU({...value, b: false}); + viewDU.batchHashTreeRoot(); + viewDU.b = true; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.b = true; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify UnionType", () => { + const viewDU = parentContainerType.toViewDU({...value, c: {selector: 1, value: 101}}); + viewDU.batchHashTreeRoot(); + viewDU.c = unionType.toViewDU({selector: 1, value: 100}); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.c = unionType.toViewDU({selector: 1, value: 100}); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify ByteVectorType", () => { + const viewDU = parentContainerType.toViewDU(value); + viewDU.batchHashTreeRoot(); + // this takes more than 1 chunk so the resulting node is a branch node + viewDU.e = viewDU.e.slice(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.e = viewDU.e.slice(); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify ByteListType", () => { + const viewDU = parentContainerType.toViewDU(value); + viewDU.batchHashTreeRoot(); + // this takes more than 1 chunk so the resulting node is a branch node + viewDU.d = viewDU.d.slice(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.d = viewDU.d.slice(); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify full child container", () => { + const viewDU = parentContainerType.toViewDU({...value, f: {f0: 99, f1: 999}}); + viewDU.batchHashTreeRoot(); + viewDU.f = childContainerType.toViewDU({f0: 100, f1: 101}); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.f = childContainerType.toViewDU({f0: 100, f1: 101}); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify partial child container", () => { + const viewDU = parentContainerType.toViewDU({...value, f: {f0: 99, f1: 999}}); + viewDU.batchHashTreeRoot(); + viewDU.f.f0 = 100; + viewDU.f.f1 = 101; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.f.f0 = 100; + viewDU.f.f1 = 101; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify ContainerNodeStructType", () => { + const viewDU = parentContainerType.toViewDU({...value, g: {g0: 99, g1: 999}}); + viewDU.batchHashTreeRoot(); + viewDU.g = childContainerStruct.toViewDU({g0: 100, g1: 101}); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.g = childContainerStruct.toViewDU({g0: 100, g1: 101}); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify partial ContainerNodeStructType", () => { + const viewDU = parentContainerType.toViewDU({...value, g: {g0: 99, g1: 999}}); + viewDU.batchHashTreeRoot(); + viewDU.g.g0 = 100; + viewDU.g.g1 = 101; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.g.g0 = 100; + viewDU.g.g1 = 101; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify ListBasicType", () => { + const viewDU = parentContainerType.toViewDU({...value, h: []}); + viewDU.batchHashTreeRoot(); + viewDU.h = listBasicType.toViewDU([1, 2]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.h = listBasicType.toViewDU([1, 2]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then push 1 item to ListBasicType", () => { + const viewDU = parentContainerType.toViewDU({...value, h: [1]}); + viewDU.batchHashTreeRoot(); + viewDU.h.push(2); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.h = listBasicType.toViewDU([1, 2]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 item of ListBasicType", () => { + const viewDU = parentContainerType.toViewDU({...value, h: [1, 3]}); + viewDU.batchHashTreeRoot(); + viewDU.h.set(1, 2); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.h.set(1, 2); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify VectorBasicType", () => { + const viewDU = parentContainerType.toViewDU({...value, i: []}); + viewDU.batchHashTreeRoot(); + viewDU.i = vectorBasicType.toViewDU([1, 2]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.i = vectorBasicType.toViewDU([1, 2]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 item of VectorBasicType", () => { + const viewDU = parentContainerType.toViewDU({...value, i: [1, 3]}); + viewDU.batchHashTreeRoot(); + viewDU.i.set(1, 2); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.i.set(1, 2); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify ListCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, j: []}); + viewDU.batchHashTreeRoot(); + viewDU.j = listCompositeType.toViewDU([{f0: 1, f1: 2}]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.j = listCompositeType.toViewDU([{f0: 1, f1: 2}]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then push 1 item to ListCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, j: []}); + viewDU.batchHashTreeRoot(); + viewDU.j.push(childContainerType.toViewDU({f0: 1, f1: 2})); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.j = listCompositeType.toViewDU([{f0: 1, f1: 2}]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 item of ListCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, j: [{f0: 1, f1: 3}]}); + viewDU.batchHashTreeRoot(); + viewDU.j.set(0, childContainerType.toViewDU({f0: 1, f1: 2})); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.j.set(0, childContainerType.toViewDU({f0: 1, f1: 2})); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 field of 1 item of ListCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, j: [{f0: 1, f1: 3}]}); + viewDU.batchHashTreeRoot(); + viewDU.j.get(0).f1 = 2; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.j.get(0).f1 = 2; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify VectorCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, k: [{f0: 9, f1: 9}]}); + viewDU.batchHashTreeRoot(); + viewDU.k = vectorCompositeType.toViewDU([{f0: 1, f1: 2}]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.k = vectorCompositeType.toViewDU([{f0: 1, f1: 2}]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 item of VectorCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, k: [{f0: 1, f1: 3}]}); + viewDU.batchHashTreeRoot(); + viewDU.k.set(0, childContainerType.toViewDU({f0: 1, f1: 2})); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.k.set(0, childContainerType.toViewDU({f0: 1, f1: 2})); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 field 1 item of VectorCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, k: [{f0: 1, f1: 3}]}); + viewDU.batchHashTreeRoot(); + viewDU.k.get(0).f1 = 2; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.k.get(0).f1 = 2; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify BitVectorType", () => { + const viewDU = parentContainerType.toViewDU({...value, l: BitArray.fromSingleBit(64, 4)}); + viewDU.batchHashTreeRoot(); + viewDU.l = bitVectorType.toViewDU(BitArray.fromSingleBit(64, 5)); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.l = bitVectorType.toViewDU(BitArray.fromSingleBit(64, 5)); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify BitVectorType bit", () => { + const viewDU = parentContainerType.toViewDU({...value, l: BitArray.fromSingleBit(64, 4)}); + viewDU.batchHashTreeRoot(); + viewDU.l.set(4, false); + viewDU.l.set(5, true); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.l.set(4, false); + viewDU.l.set(5, true); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify BitListType", () => { + const viewDU = parentContainerType.toViewDU({...value, m: BitArray.fromSingleBit(4, 0)}); + viewDU.batchHashTreeRoot(); + viewDU.m = bitListType.toViewDU(BitArray.fromSingleBit(4, 1)); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.m = bitListType.toViewDU(BitArray.fromSingleBit(4, 1)); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify BitListType bit", () => { + const viewDU = parentContainerType.toViewDU({...value, m: BitArray.fromSingleBit(4, 0)}); + viewDU.batchHashTreeRoot(); + viewDU.m.set(0, false); + viewDU.m.set(1, true); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.m.set(0, false); + viewDU.m.set(1, true); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); +}); + +describe("ContainerNodeStruct batchHashTreeRoot", function () { + const EpochInf = new UintNumberType(8, {clipInfinity: true}); + + // Ethereum consensus validator type + const containerType = new ContainerNodeStructType({ + pubkey: new ByteVectorType(48), + withdrawalCredentials: new ByteVectorType(32), + effectiveBalance: new UintNumberType(8), + slashed: new BooleanType(), + activationEligibilityEpoch: EpochInf, + activationEpoch: EpochInf, + exitEpoch: EpochInf, + withdrawableEpoch: EpochInf, + }); + const value = { + pubkey: Buffer.alloc(48, 0xaa), + withdrawalCredentials: Buffer.alloc(32, 0xbb), + effectiveBalance: 32e9, + slashed: false, + activationEligibilityEpoch: 1_000_000, + activationEpoch: 2_000_000, + exitEpoch: 3_000_000, + withdrawableEpoch: 4_000_000, + }; + const expectedRoot = containerType.toView(value).hashTreeRoot(); + + it("fresh ViewDU", () => { + expect(containerType.toViewDU(value).batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify basic type", () => { + const viewDU = containerType.toViewDU({...value, exitEpoch: 3}); + viewDU.batchHashTreeRoot(); + viewDU.exitEpoch *= 1_000_000; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("modify basic type", () => { + const viewDU = containerType.toViewDU({ + ...value, + exitEpoch: value.exitEpoch + 1, + withdrawableEpoch: value.withdrawableEpoch + 1, + }); + viewDU.exitEpoch -= 1; + viewDU.withdrawableEpoch -= 1; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); +}); diff --git a/packages/ssz/test/unit/byType/listBasic/tree.test.ts b/packages/ssz/test/unit/byType/listBasic/tree.test.ts index d7f56b4d..22b5f50d 100644 --- a/packages/ssz/test/unit/byType/listBasic/tree.test.ts +++ b/packages/ssz/test/unit/byType/listBasic/tree.test.ts @@ -240,3 +240,78 @@ describe("ListBasicType.sliceTo", () => { }); } }); + +describe("ListBasicType batchHashTreeRoot", function () { + const value = [1, 2, 3, 4]; + const expectedRoot = ListN64Uint64NumberType.toView(value).hashTreeRoot(); + + it("fresh ViewDU", () => { + expect(ListN64Uint64NumberType.toViewDU(value).batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("push then batchHashTreeRoot()", () => { + const viewDU = ListN64Uint64NumberType.defaultViewDU(); + viewDU.push(1); + viewDU.push(2); + viewDU.push(3); + viewDU.push(4); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign the same value again, commit() then batchHashTreeRoot() + viewDU.set(0, 1); + viewDU.set(1, 2); + viewDU.set(2, 3); + viewDU.set(3, 4); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("push then modify then batchHashTreeRoot()", () => { + const viewDU = ListN64Uint64NumberType.defaultViewDU(); + viewDU.push(1); + viewDU.push(2); + viewDU.push(3); + viewDU.push(44); + viewDU.set(3, 4); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign the same value again, commit() then batchHashTreeRoot() + viewDU.set(3, 44); + viewDU.set(3, 4); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify", () => { + const viewDU = ListN64Uint64NumberType.defaultViewDU(); + viewDU.push(1); + viewDU.push(2); + viewDU.push(33); + viewDU.push(44); + viewDU.batchHashTreeRoot(); + viewDU.set(2, 3); + viewDU.set(3, 4); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign the same value again, commit() then batchHashTreeRoot() + viewDU.set(2, 33); + viewDU.set(3, 44); + viewDU.commit(); + viewDU.set(2, 3); + viewDU.set(3, 4); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + // similar to a fresh ViewDU but it's good to test + it("sliceTo()", () => { + const viewDU = ListN64Uint64NumberType.defaultViewDU(); + viewDU.push(1); + viewDU.push(2); + viewDU.push(3); + viewDU.push(4); + viewDU.push(5); + viewDU.batchHashTreeRoot(); + expect(viewDU.sliceTo(3).batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); +}); diff --git a/packages/ssz/test/unit/byType/listComposite/tree.test.ts b/packages/ssz/test/unit/byType/listComposite/tree.test.ts index 21fab6f1..f1de130e 100644 --- a/packages/ssz/test/unit/byType/listComposite/tree.test.ts +++ b/packages/ssz/test/unit/byType/listComposite/tree.test.ts @@ -1,5 +1,13 @@ import {expect} from "chai"; -import {CompositeView, ContainerType, ListCompositeType, toHexString, UintNumberType, ValueOf} from "../../../../src"; +import { + CompositeView, + ContainerNodeStructType, + ContainerType, + ListCompositeType, + toHexString, + UintNumberType, + ValueOf, +} from "../../../../src"; import {ArrayCompositeTreeViewDU} from "../../../../src/viewDU/arrayComposite"; import {ssz} from "../../../lodestarTypes/primitive"; import {runViewTestMutation} from "../runViewTestMutation"; @@ -9,7 +17,7 @@ const containerUintsType = new ContainerType( {a: uint64NumInfType, b: uint64NumInfType}, {typeName: "Container(uint64)"} ); -const listOfContainersType = new ListCompositeType(containerUintsType, 4); +const listOfContainersType = new ListCompositeType(containerUintsType, 4, {typeName: "ListCompositeType(Container)"}); runViewTestMutation({ type: listOfContainersType, @@ -213,3 +221,115 @@ describe("ListCompositeType.sliceFrom", () => { } }); }); + +describe("ListCompositeType batchHashTreeRoot", () => { + const value = [ + {a: 1, b: 2}, + {a: 3, b: 4}, + ]; + const containerStructUintsType = new ContainerNodeStructType( + {a: uint64NumInfType, b: uint64NumInfType}, + {typeName: "ContainerNodeStruct(uint64)"} + ); + const listOfContainersType2 = new ListCompositeType(containerStructUintsType, 4, { + typeName: "ListCompositeType(ContainerNodeStructType)", + }); + + for (const list of [listOfContainersType, listOfContainersType2]) { + const typeName = list.typeName; + const expectedRoot = list.toView(value).hashTreeRoot(); + + it(`${typeName} - fresh ViewDU`, () => { + expect(listOfContainersType.toViewDU(value).batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it(`${typeName} - push then batchHashTreeRoot()`, () => { + const viewDU = listOfContainersType.defaultViewDU(); + viewDU.push(containerUintsType.toViewDU({a: 1, b: 2})); + viewDU.push(containerUintsType.toViewDU({a: 3, b: 4})); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again, commit() then batchHashTreeRoot() + viewDU.set(0, containerUintsType.toViewDU({a: 1, b: 2})); + viewDU.set(1, containerUintsType.toViewDU({a: 3, b: 4})); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it(`${typeName} - full hash then modify full non-hashed child element`, () => { + const viewDU = listOfContainersType.defaultViewDU(); + viewDU.push(containerUintsType.toViewDU({a: 1, b: 2})); + viewDU.push(containerUintsType.toViewDU({a: 33, b: 44})); + viewDU.batchHashTreeRoot(); + viewDU.set(1, containerUintsType.toViewDU({a: 3, b: 4})); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign the same value again, commit() then batchHashTreeRoot() + viewDU.set(1, containerUintsType.toViewDU({a: 3, b: 4})); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it(`${typeName} - full hash then modify partially hashed child element`, () => { + const viewDU = listOfContainersType.defaultViewDU(); + viewDU.push(containerUintsType.toViewDU({a: 1, b: 2})); + viewDU.push(containerUintsType.toViewDU({a: 33, b: 44})); + viewDU.batchHashTreeRoot(); + const item1 = containerUintsType.toViewDU({a: 3, b: 44}); + item1.batchHashTreeRoot(); + item1.b = 4; + viewDU.set(1, item1); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign the same value again, commit() then batchHashTreeRoot() + const item2 = viewDU.get(1); + item2.a = 3; + item2.b = 4; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it(`${typeName} - full hash then modify full hashed child element`, () => { + const viewDU = listOfContainersType.defaultViewDU(); + viewDU.push(containerUintsType.toViewDU({a: 1, b: 2})); + viewDU.push(containerUintsType.toViewDU({a: 33, b: 44})); + viewDU.batchHashTreeRoot(); + const item1 = containerUintsType.toViewDU({a: 3, b: 4}); + item1.batchHashTreeRoot(); + viewDU.set(1, item1); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign the same value again, commit() then batchHashTreeRoot() + const newItem = containerUintsType.toViewDU({a: 3, b: 4}); + viewDU.set(1, newItem); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it(`${typeName} - full hash then modify partial child element`, () => { + const viewDU = listOfContainersType.defaultViewDU(); + viewDU.push(containerUintsType.toViewDU({a: 1, b: 2})); + viewDU.push(containerUintsType.toViewDU({a: 33, b: 44})); + viewDU.batchHashTreeRoot(); + viewDU.get(1).a = 3; + viewDU.get(1).b = 4; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign the same value again, commit() then batchHashTreeRoot() + viewDU.get(1).a = 3; + viewDU.get(1).b = 4; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + // similar to a fresh ViewDU but it's good to test + it(`${typeName} - sliceTo()`, () => { + const viewDU = listOfContainersType.defaultViewDU(); + viewDU.push(containerUintsType.toViewDU({a: 1, b: 2})); + viewDU.push(containerUintsType.toViewDU({a: 3, b: 4})); + viewDU.push(containerUintsType.toViewDU({a: 5, b: 6})); + viewDU.batchHashTreeRoot(); + expect(viewDU.sliceTo(1).batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + } +}); diff --git a/packages/ssz/test/unit/byType/optional/tree.test.ts b/packages/ssz/test/unit/byType/optional/tree.test.ts index c61b9478..5b69d17a 100644 --- a/packages/ssz/test/unit/byType/optional/tree.test.ts +++ b/packages/ssz/test/unit/byType/optional/tree.test.ts @@ -8,7 +8,7 @@ const SimpleObject = new ContainerType({ }); describe("Optional view tests", () => { - // unimplemented + // TODO: implement // eslint-disable-next-line @typescript-eslint/no-unsafe-call it.skip("optional simple type", () => { const type = new OptionalType(byteType); @@ -22,7 +22,7 @@ describe("Optional view tests", () => { expect(toHexString(type.commitViewDU(viewDU).root)).equals(toHexString(root)); }); - // unimplemented + // TODO: implement // eslint-disable-next-line @typescript-eslint/no-unsafe-call it.skip("optional composite type", () => { const type = new OptionalType(SimpleObject); diff --git a/packages/ssz/test/unit/byType/runViewTestMutation.ts b/packages/ssz/test/unit/byType/runViewTestMutation.ts index d6cae2f5..85298774 100644 --- a/packages/ssz/test/unit/byType/runViewTestMutation.ts +++ b/packages/ssz/test/unit/byType/runViewTestMutation.ts @@ -32,15 +32,22 @@ const runViewTestMutationFn = function runViewTestMutation, value: ValueOf, message: string): void { + function assertValidView(view: TreeViewDU, value: ValueOf, message: string, batchHash: boolean): void { expect(type.toJson(view.toValue())).to.deep.equal(type.toJson(value), `Wrong json - ${message}`); expect(toHexString(view.serialize())).to.equal(toHexString(type.serialize(value)), `Wrong serialized - ${message}`); - expect(toHexString(view.hashTreeRoot())).to.equal( - toHexString(type.hashTreeRoot(value)), - `Wrong hashTreeRoot - ${message}` - ); + if (batchHash) { + expect(toHexString(view.batchHashTreeRoot())).to.equal( + toHexString(type.hashTreeRoot(value)), + `Wrong batchHashTreeRoot - ${message}` + ); + } else { + expect(toHexString(view.hashTreeRoot())).to.equal( + toHexString(type.hashTreeRoot(value)), + `Wrong hashTreeRoot - ${message}` + ); + } } // eslint-disable-next-line no-only-tests/no-only-tests @@ -61,46 +68,48 @@ const runViewTestMutationFn = function runViewTestMutation) ?? tvBefore; - assertValidView(tvAfter as TreeViewDU, valueAfter, "after mutation"); + assertValidView(tvAfter as TreeViewDU, valueAfter, "after mutation", false); if (assertFn) assertFn(tvAfter as CompositeViewDU); }); } - const treeViewDUId = `${id} - TreeViewDU`; - if ((!onlyId || treeViewDUId.includes(onlyId)) && !skipTreeViewDU) { - it(treeViewDUId, () => { - const tvBefore = type.toViewDU(valueBefore) as TreeViewDU; - - // Set to mutable, and edit - const tvAfter = (fn(tvBefore as CompositeViewDU) ?? tvBefore) as CompositeViewDU; - - if (treeViewToStruct) { - const tvAfterStruct = treeViewToStruct(tvAfter); - expect(type.toJson(tvAfterStruct)).to.deep.equal( - type.toJson(valueAfter), - "Wrong value after mutation before commit" - ); - } - - if (assertFn) assertFn(tvAfter as CompositeViewDU); + for (const batchHash of [false, true]) { + const treeViewDUId = `${id} - TreeViewDU, batchHash = ${batchHash}`; + if ((!onlyId || treeViewDUId.includes(onlyId)) && !skipTreeViewDU) { + it(treeViewDUId, () => { + const tvBefore = type.toViewDU(valueBefore) as TreeViewDU; - type.commitViewDU(tvAfter); - assertValidView(tvAfter as TreeViewDU, valueAfter, "after mutation"); - - if (assertFn) assertFn(tvAfter as CompositeViewDU); - - if (!skipCloneMutabilityViewDU) { - // Ensure correct mutability of clone and caches // Set to mutable, and edit - const tvBefore2 = type.toViewDU(valueBefore) as TreeViewDU; - const tvAfter2 = (fn(tvBefore2 as CompositeViewDU) ?? tvBefore2) as CompositeViewDU; - // Drop changes - (tvAfter2 as TreeViewDU).clone(); - // Assert same value as before - assertValidView(tvAfter2 as TreeViewDU, valueBefore, "dropped mutation"); - } - }); + const tvAfter = (fn(tvBefore as CompositeViewDU) ?? tvBefore) as CompositeViewDU; + + if (treeViewToStruct) { + const tvAfterStruct = treeViewToStruct(tvAfter); + expect(type.toJson(tvAfterStruct)).to.deep.equal( + type.toJson(valueAfter), + "Wrong value after mutation before commit" + ); + } + + if (assertFn) assertFn(tvAfter as CompositeViewDU); + + type.commitViewDU(tvAfter); + assertValidView(tvAfter as TreeViewDU, valueAfter, "after mutation", batchHash); + + if (assertFn) assertFn(tvAfter as CompositeViewDU); + + if (!skipCloneMutabilityViewDU) { + // Ensure correct mutability of clone and caches + // Set to mutable, and edit + const tvBefore2 = type.toViewDU(valueBefore) as TreeViewDU; + const tvAfter2 = (fn(tvBefore2 as CompositeViewDU) ?? tvBefore2) as CompositeViewDU; + // Drop changes + (tvAfter2 as TreeViewDU).clone(); + // Assert same value as before + assertValidView(tvAfter2 as TreeViewDU, valueBefore, "dropped mutation", batchHash); + } + }); + } } } }); diff --git a/packages/ssz/test/unit/byType/vectorBasic/tree.test.ts b/packages/ssz/test/unit/byType/vectorBasic/tree.test.ts new file mode 100644 index 00000000..69cebbd8 --- /dev/null +++ b/packages/ssz/test/unit/byType/vectorBasic/tree.test.ts @@ -0,0 +1,67 @@ +import {expect} from "chai"; +import {UintNumberType, VectorBasicType} from "../../../../src"; +import {runViewTestMutation} from "../runViewTestMutation"; + +const uint64NumInf = new UintNumberType(8, {clipInfinity: true}); +const vectorType = new VectorBasicType(uint64NumInf, 8); + +runViewTestMutation({ + type: vectorType, + mutations: [ + { + id: "set basic", + valueBefore: [1, 2, 3, 4, 5, 6, 7, 8], + valueAfter: [0, 1, 2, 3, 4, 5, 6, 7], + fn: (tv) => { + tv.set(0, 0); + tv.set(1, 1); + tv.set(2, 2); + tv.set(3, 3); + tv.set(4, 4); + tv.set(5, 5); + tv.set(6, 6); + tv.set(7, 7); + }, + }, + { + id: "swap two indices", + valueBefore: [1, 2, 3, 4, 5, 6, 7, 8], + valueAfter: [8, 2, 3, 4, 5, 6, 7, 1], + fn: (tv) => { + const i0 = tv.get(0); + const i7 = tv.get(7); + tv.set(0, i7); + tv.set(7, i0); + }, + }, + ], +}); + +describe("VectorBasicType batchHashTreeRoot", () => { + const value = [0, 1, 2, 3, 4, 5, 6, 7, 8]; + const expectedRoot = vectorType.hashTreeRoot(value); + + it("fresh ViewDU", () => { + expect(vectorType.toViewDU(value).batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify", () => { + const viewDU = vectorType.defaultViewDU(); + viewDU.hashTreeRoot(); + viewDU.set(0, 0); + viewDU.set(1, 1); + viewDU.set(2, 2); + viewDU.set(3, 3); + viewDU.set(4, 4); + viewDU.set(5, 5); + viewDU.set(6, 6); + viewDU.set(7, 7); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign the same value again, commit() then batchHashTreeRoot() + viewDU.set(0, 0); + viewDU.set(7, 7); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); +}); diff --git a/packages/ssz/test/unit/byType/vectorComposite/tree.test.ts b/packages/ssz/test/unit/byType/vectorComposite/tree.test.ts new file mode 100644 index 00000000..b013cae5 --- /dev/null +++ b/packages/ssz/test/unit/byType/vectorComposite/tree.test.ts @@ -0,0 +1,121 @@ +import {expect} from "chai"; +import {ContainerNodeStructType, ContainerType, UintNumberType, ValueOf, VectorCompositeType} from "../../../../src"; +import {runViewTestMutation} from "../runViewTestMutation"; + +const uint64NumInfType = new UintNumberType(8, {clipInfinity: true}); +const containerUintsType = new ContainerType( + {a: uint64NumInfType, b: uint64NumInfType}, + {typeName: "Container(uint64)"} +); +const vectorOfContainersType = new VectorCompositeType(containerUintsType, 2, {typeName: "VectorComposite(Container)"}); + +runViewTestMutation({ + type: vectorOfContainersType, + treeViewToStruct: (tv) => { + const arr: ValueOf = []; + for (let i = 0; i < tv.length; i++) { + const item = tv.get(i); + arr.push({a: item.a, b: item.b}); + } + return arr; + }, + mutations: [ + { + id: "set", + valueBefore: [ + {a: 1, b: 2}, + {a: 3, b: 4}, + ], + valueAfter: [ + {a: 5, b: 6}, + {a: 7, b: 8}, + ], + fn: (tv) => { + tv.set(0, containerUintsType.toViewDU({a: 5, b: 6})); + tv.set(1, containerUintsType.toViewDU({a: 7, b: 8})); + }, + }, + { + id: "set child properties", + valueBefore: [ + {a: 1, b: 2}, + {a: 3, b: 4}, + ], + valueAfter: [ + {a: 5, b: 2}, + {a: 3, b: 8}, + ], + fn: (tv) => { + tv.get(0).a = 5; + tv.get(1).b = 8; + }, + }, + { + id: "swap indices", + valueBefore: [ + {a: 1, b: 2}, + {a: 3, b: 4}, + ], + valueAfter: [ + {a: 3, b: 4}, + {a: 1, b: 2}, + ], + fn: (tv) => { + const item0 = tv.get(0); + const item1 = tv.get(1); + tv.set(0, item1); + tv.set(1, item0); + }, + }, + ], +}); + +describe("VectorCompositeType batchHashTreeRoot", () => { + const value = [ + {a: 1, b: 2}, + {a: 3, b: 4}, + ]; + const containerUintsType = new ContainerNodeStructType( + {a: uint64NumInfType, b: uint64NumInfType}, + {typeName: "ContainerNodeStruct(uint64)"} + ); + const vectorOfContainersType2 = new VectorCompositeType(containerUintsType, 2, { + typeName: "VectorComposite(ContainerNodeStruct)", + }); + for (const vector of [vectorOfContainersType, vectorOfContainersType2]) { + const typeName = vector.typeName; + const expectedRoot = vectorOfContainersType.toView(value).hashTreeRoot(); + + it(`${typeName} - fresh ViewDU`, () => { + expect(vectorOfContainersType.toViewDU(value).batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it(`${typeName} - modify 1 full element`, () => { + const viewDU = vectorOfContainersType.toViewDU([ + {a: 1, b: 2}, + {a: 0, b: 0}, + ]); + viewDU.set(1, containerUintsType.toViewDU({a: 3, b: 4})); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign the same value again, commit() then batchHashTreeRoot(); + viewDU.set(1, containerUintsType.toViewDU({a: 3, b: 4})); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it(`${typeName} - modify 1 property of 1 element`, () => { + const viewDU = vectorOfContainersType.toViewDU([ + {a: 1, b: 2}, + {a: 3, b: 0}, + ]); + viewDU.get(1).b = 4; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign the same value again, commit() then batchHashTreeRoot(); + viewDU.get(1).b = 4; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + } +}); diff --git a/packages/ssz/test/unit/eth2/beaconState.test.ts b/packages/ssz/test/unit/eth2/beaconState.test.ts new file mode 100644 index 00000000..9b886f82 --- /dev/null +++ b/packages/ssz/test/unit/eth2/beaconState.test.ts @@ -0,0 +1,200 @@ +import {expect} from "chai"; +import {BeaconState} from "../../lodestarTypes/deneb/sszTypes"; +import {ListUintNum64Type} from "../../../src/type/listUintNum64"; +import {altair, phase0, ssz} from "../../lodestarTypes"; +import {BitArray, fromHexString} from "../../../src"; + +const VALIDATOR_REGISTRY_LIMIT = 1099511627776; +export const Balances = new ListUintNum64Type(VALIDATOR_REGISTRY_LIMIT); + +describe("BeaconState ViewDU batchHashTreeRoot", function () { + const view = BeaconState.defaultView(); + const viewDU = BeaconState.defaultViewDU(); + + it("BeaconState ViewDU should have same hashTreeRoot() to View", () => { + // genesisTime + viewDU.genesisTime = view.genesisTime = 1e9; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // genesisValidatorsRoot + viewDU.genesisValidatorsRoot = view.genesisValidatorsRoot = Buffer.alloc(32, 1); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // fork + const fork: phase0.Fork = { + epoch: 1000, + previousVersion: fromHexString("0x03001020"), + currentVersion: fromHexString("0x04001020"), + }; + view.fork = BeaconState.fields.fork.toView(fork); + viewDU.fork = BeaconState.fields.fork.toViewDU(fork); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // latestBlockHeader + const latestBlockHeader: phase0.BeaconBlockHeader = { + slot: 1000, + proposerIndex: 1, + parentRoot: fromHexString("0xac80c66f413218e2c9c7bcb2408ccdceacf3bcd7e7df58474e0c6aa9d7f328a0"), + stateRoot: fromHexString("0xed29eed3dbee72caf3b13df84d01ebda1482dbd0ce084e1ce8862b4acb740ed8"), + bodyRoot: fromHexString("0x32c644ca1b5d1583d445e9d41c81b3e98465fefad4f0db16084cbce7f1b7b849"), + }; + view.latestBlockHeader = BeaconState.fields.latestBlockHeader.toView(latestBlockHeader); + viewDU.latestBlockHeader = BeaconState.fields.latestBlockHeader.toViewDU(latestBlockHeader); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // blockRoots + const blockRoots = ssz.phase0.HistoricalBlockRoots.defaultValue(); + blockRoots[0] = fromHexString("0x1234"); + view.blockRoots = ssz.phase0.HistoricalBlockRoots.toView(blockRoots); + viewDU.blockRoots = ssz.phase0.HistoricalBlockRoots.toViewDU(blockRoots); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // stateRoots + const stateRoots = ssz.phase0.HistoricalStateRoots.defaultValue(); + stateRoots[0] = fromHexString("0x5678"); + view.stateRoots = ssz.phase0.HistoricalStateRoots.toView(stateRoots); + viewDU.stateRoots = ssz.phase0.HistoricalStateRoots.toViewDU(stateRoots); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // historical_roots Frozen in Capella, replaced by historical_summaries + // Eth1 + const eth1Data: phase0.Eth1Data = { + depositRoot: fromHexString("0x1234"), + depositCount: 1000, + blockHash: fromHexString("0x5678"), + }; + view.eth1Data = BeaconState.fields.eth1Data.toView(eth1Data); + viewDU.eth1Data = BeaconState.fields.eth1Data.toViewDU(eth1Data); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // Eth1DataVotes + const eth1DataVotes = ssz.phase0.Eth1DataVotes.defaultValue(); + eth1DataVotes[0] = eth1Data; + view.eth1DataVotes = ssz.phase0.Eth1DataVotes.toView(eth1DataVotes); + viewDU.eth1DataVotes = ssz.phase0.Eth1DataVotes.toViewDU(eth1DataVotes); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // Eth1DepositIndex + view.eth1DepositIndex = 1000; + viewDU.eth1DepositIndex = 1000; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // validators + const validator = { + pubkey: Buffer.alloc(48, 0xaa), + withdrawalCredentials: Buffer.alloc(32, 0xbb), + effectiveBalance: 32e9, + slashed: false, + activationEligibilityEpoch: 1_000_000, + activationEpoch: 2_000_000, + exitEpoch: 3_000_000, + withdrawableEpoch: 4_000_000, + }; + view.validators = BeaconState.fields.validators.toView([validator]); + viewDU.validators = BeaconState.fields.validators.toViewDU([validator]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // balances + view.balances = BeaconState.fields.balances.toView([1000, 2000, 3000]); + viewDU.balances = Balances.toViewDU([1000, 2000, 3000]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // randaoMixes + const randaoMixes = ssz.phase0.RandaoMixes.defaultValue(); + randaoMixes[0] = fromHexString("0x1234"); + view.randaoMixes = ssz.phase0.RandaoMixes.toView(randaoMixes); + viewDU.randaoMixes = ssz.phase0.RandaoMixes.toViewDU(randaoMixes); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // slashings + view.slashings = BeaconState.fields.slashings.toView(Array.from({length: 64}, () => BigInt(1000))); + viewDU.slashings = BeaconState.fields.slashings.toViewDU(Array.from({length: 64}, () => BigInt(1000))); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // previousEpochAttestations + view.previousEpochParticipation = BeaconState.fields.previousEpochParticipation.toView([1, 2, 3]); + viewDU.previousEpochParticipation = BeaconState.fields.previousEpochParticipation.toViewDU([1, 2, 3]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // currentEpochAttestations + view.currentEpochParticipation = BeaconState.fields.currentEpochParticipation.toView([1, 2, 3]); + viewDU.currentEpochParticipation = BeaconState.fields.currentEpochParticipation.toViewDU([1, 2, 3]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // justificationBits + view.justificationBits = BeaconState.fields.justificationBits.toView( + BitArray.fromBoolArray([true, false, true, true]) + ); + viewDU.justificationBits = BeaconState.fields.justificationBits.toViewDU( + BitArray.fromBoolArray([true, false, true, true]) + ); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // previousJustifiedCheckpoint + const checkpoint: phase0.Checkpoint = { + epoch: 1000, + root: fromHexString("0x1234"), + }; + view.previousJustifiedCheckpoint = BeaconState.fields.previousJustifiedCheckpoint.toView(checkpoint); + viewDU.previousJustifiedCheckpoint = BeaconState.fields.previousJustifiedCheckpoint.toViewDU(checkpoint); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // currentJustifiedCheckpoint + view.currentJustifiedCheckpoint = BeaconState.fields.currentJustifiedCheckpoint.toView(checkpoint); + viewDU.currentJustifiedCheckpoint = BeaconState.fields.currentJustifiedCheckpoint.toViewDU(checkpoint); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // finalizedCheckpoint + view.finalizedCheckpoint = BeaconState.fields.finalizedCheckpoint.toView(checkpoint); + viewDU.finalizedCheckpoint = BeaconState.fields.finalizedCheckpoint.toViewDU(checkpoint); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // inactivityScores + view.inactivityScores = BeaconState.fields.inactivityScores.toView([1, 2, 3]); + viewDU.inactivityScores = BeaconState.fields.inactivityScores.toViewDU([1, 2, 3]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // currentSyncCommittee + const syncCommittee: altair.SyncCommittee = { + pubkeys: Array.from({length: 32}, () => Buffer.alloc(48, 0xaa)), + aggregatePubkey: fromHexString("0x1234"), + }; + view.currentSyncCommittee = BeaconState.fields.currentSyncCommittee.toView(syncCommittee); + viewDU.currentSyncCommittee = BeaconState.fields.currentSyncCommittee.toViewDU(syncCommittee); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // nextSyncCommittee + view.nextSyncCommittee = BeaconState.fields.nextSyncCommittee.toView(syncCommittee); + viewDU.nextSyncCommittee = BeaconState.fields.nextSyncCommittee.toViewDU(syncCommittee); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // latestExecutionPayloadHeader + const latestExecutionPayloadHeader = BeaconState.fields.latestExecutionPayloadHeader.defaultValue(); + latestExecutionPayloadHeader.blockNumber = 1000; + latestExecutionPayloadHeader.parentHash = fromHexString( + "0xac80c66f413218e2c9c7bcb2408ccdceacf3bcd7e7df58474e0c6aa9d7f328a0" + ); + view.latestExecutionPayloadHeader = + BeaconState.fields.latestExecutionPayloadHeader.toView(latestExecutionPayloadHeader); + viewDU.latestExecutionPayloadHeader = + BeaconState.fields.latestExecutionPayloadHeader.toViewDU(latestExecutionPayloadHeader); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // nextWithdrawalIndex + viewDU.nextWithdrawalIndex = view.nextWithdrawalIndex = 1000; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // nextWithdrawalValidatorIndex + viewDU.nextWithdrawalValidatorIndex = view.nextWithdrawalValidatorIndex = 1000; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + + // historicalSummaries + const historicalSummaries = { + blockSummaryRoot: fromHexString("0xac80c66f413218e2c9c7bcb2408ccdceacf3bcd7e7df58474e0c6aa9d7f328a0"), + stateSummaryRoot: fromHexString("0x32c644ca1b5d1583d445e9d41c81b3e98465fefad4f0db16084cbce7f1b7b849"), + }; + view.historicalSummaries = BeaconState.fields.historicalSummaries.toView([historicalSummaries]); + viewDU.historicalSummaries = BeaconState.fields.historicalSummaries.toViewDU([historicalSummaries]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(view.hashTreeRoot()); + }); +}); diff --git a/packages/ssz/test/unit/eth2/validators.test.ts b/packages/ssz/test/unit/eth2/validators.test.ts index a0878f9c..1cccd691 100644 --- a/packages/ssz/test/unit/eth2/validators.test.ts +++ b/packages/ssz/test/unit/eth2/validators.test.ts @@ -3,22 +3,21 @@ import {describe, it} from "mocha"; import {toHexString, ListCompositeType, ValueOf, CompositeViewDU} from "../../../src"; import {ValidatorContainer, ValidatorNodeStruct} from "../../lodestarTypes/phase0/sszTypes"; +type Validator = ValueOf; +const validator: Validator = { + pubkey: Buffer.alloc(48, 0xaa), + withdrawalCredentials: Buffer.alloc(32, 0xbb), + effectiveBalance: 32e9, + slashed: false, + activationEligibilityEpoch: 1_000_000, + activationEpoch: 2_000_000, + exitEpoch: 3_000_000, + withdrawableEpoch: 4_000_000, +}; + describe("Container with BranchNodeStruct", function () { this.timeout(0); - type Validator = ValueOf; - - const validator: Validator = { - pubkey: Buffer.alloc(48, 0xaa), - withdrawalCredentials: Buffer.alloc(32, 0xbb), - effectiveBalance: 32e9, - slashed: false, - activationEligibilityEpoch: 1_000_000, - activationEpoch: 2_000_000, - exitEpoch: 3_000_000, - withdrawableEpoch: 4_000_000, - }; - const validatorViewDU = ValidatorContainer.toViewDU(validator); const validatorNodeStructViewDU = ValidatorNodeStruct.toViewDU(validator); @@ -34,6 +33,7 @@ describe("Container with BranchNodeStruct", function () { getExitEpoch: (treeBacked) => treeBacked.exitEpoch, getPubkey: (treeBacked) => toHexString(treeBacked.pubkey), hashTreeRoot: (treeBacked) => treeBacked.hashTreeRoot(), + batchHashTreeRoot: (treeBacked) => treeBacked.batchHashTreeRoot(), getProof: (treeBacked) => treeBacked.createProof(validatorProofJsonPaths), serialize: (treeBacked) => treeBacked.serialize(), }; diff --git a/packages/ssz/test/unit/regressions.test.ts b/packages/ssz/test/unit/regressions.test.ts index 4f5ecaf9..6dc0c22f 100644 --- a/packages/ssz/test/unit/regressions.test.ts +++ b/packages/ssz/test/unit/regressions.test.ts @@ -32,6 +32,8 @@ describe("Regressions / known issues", () => { const bytes = SyncCommitteeBits.serialize(bitArray); const rootByTreeBacked = SyncCommitteeBits.deserializeToViewDU(bytes).hashTreeRoot(); expect(toHexString(rootByStruct)).to.be.equal(toHexString(rootByTreeBacked), "Inconsistent hashTreeRoot"); + const rootByBatch = SyncCommitteeBits.deserializeToViewDU(bytes).batchHashTreeRoot(); + expect(toHexString(rootByStruct)).to.be.equal(toHexString(rootByBatch), "Inconsistent hashTreeRoot"); }); it("converts bit arrays to tree", function () { diff --git a/packages/ssz/test/unit/unchangedViewDUs.test.ts b/packages/ssz/test/unit/unchangedViewDUs.test.ts new file mode 100644 index 00000000..f1e57a36 --- /dev/null +++ b/packages/ssz/test/unit/unchangedViewDUs.test.ts @@ -0,0 +1,29 @@ +import {expect} from "chai"; +import * as sszAltair from "../lodestarTypes/altair/sszTypes"; +import {getRandomState} from "../utils/generateEth2Objs"; + +describe("Unchanged ViewDUs", () => { + const state = sszAltair.BeaconState.toViewDU(getRandomState(100)); + + it.skip("should not recompute batchHashTreeRoot() when no fields is changed", () => { + const root = state.batchHashTreeRoot(); + // this causes viewsChanged inside BeaconState container + state.validators.length; + state.balances.length; + // but we should not recompute root, should get from cache instead + const root2 = state.batchHashTreeRoot(); + expect(root2).to.equal(root, "should not recompute batchHashTreeRoot() when no fields are changed"); + }); + + it("handle childViewDU.batchHashTreeRoot()", () => { + const state2 = state.clone(); + state2.latestBlockHeader.stateRoot = Buffer.alloc(32, 3); + const root2 = state2.batchHashTreeRoot(); + const state3 = state.clone(); + state3.latestBlockHeader.stateRoot = Buffer.alloc(32, 3); + // batchHashTreeRoot() also does the commit() + state3.latestBlockHeader.commit(); + const root3 = state3.batchHashTreeRoot(); + expect(root3).to.be.deep.equal(root2); + }); +}); diff --git a/yarn.lock b/yarn.lock index 7e92c77c..fef8f425 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1244,6 +1244,11 @@ "@babel/helper-validator-identifier" "^7.24.5" to-fast-properties "^2.0.0" +"@chainsafe/as-sha256@^0.4.1", "@chainsafe/as-sha256@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@chainsafe/as-sha256/-/as-sha256-0.4.2.tgz#21ad1371e2245e430c1a554a05f10d333c6f42cc" + integrity sha512-HJ8GZBRjLeWtRsAXf3EbNsNzmTGpzTFjfpSf4yHkLYC+E52DhT6hwz+7qpj6I/EmFzSUm5tYYvT9K8GZokLQCQ== + "@chainsafe/babel-plugin-inline-binary-import@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@chainsafe/babel-plugin-inline-binary-import/-/babel-plugin-inline-binary-import-1.0.3.tgz#08dde20d91cf5d18f2c253edf32547943a16e409" @@ -1276,6 +1281,22 @@ "@chainsafe/hashtree-linux-arm64-gnu" "1.0.1" "@chainsafe/hashtree-linux-x64-gnu" "1.0.1" +"@chainsafe/persistent-merkle-tree@^0.7.1", "@chainsafe/persistent-merkle-tree@^0.7.2": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.7.2.tgz#f0ef91daf36752f827432333cbc965f4bf6e750e" + integrity sha512-BUAqrmSUmy6bZhXxnhpR+aYoEDdCeS1dQvq/aje0CDEB14ZHF9UVN2mL9MolOD0ANUiP1OaPG3KfVBxvuW8aTg== + dependencies: + "@chainsafe/as-sha256" "^0.4.2" + "@noble/hashes" "^1.3.0" + +"@chainsafe/ssz@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.16.0.tgz#262c491ac037777a16e8d8db479da2ba27539b8d" + integrity sha512-CgTDyrkbAKvrKwHxPT5rerXAHP3NB+uOvpnN9Gn8aJ/4TGOKhOboj4131bSFUZ679uPJ6pu6391cvInuOdrglw== + dependencies: + "@chainsafe/as-sha256" "^0.4.2" + "@chainsafe/persistent-merkle-tree" "^0.7.2" + "@chainsafe/ssz@^0.15.1": version "0.15.1" resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.15.1.tgz#008a711c3bcdc0d207cd4be15108870b0b1c60c0"