From 21e0c545205ba87aecf0fa21466a5ba9eb2a8c89 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 10 Sep 2024 13:41:40 +0700 Subject: [PATCH] feat: implement type.defaultPartialViewDU() and zeroSnapshot() --- .../persistent-merkle-tree/src/snapshot.ts | 18 +++++++++++++----- .../test/unit/snapshot.test.ts | 2 +- packages/ssz/src/type/partialListComposite.ts | 18 ++++++++++++++++-- packages/ssz/src/util/snapshot.ts | 14 ++++++++++++++ packages/ssz/src/util/types.ts | 8 -------- packages/ssz/src/viewDU/listComposite.ts | 7 ++++++- .../ssz/src/viewDU/partialListComposite.ts | 9 +++++---- .../byType/partialListComposite/tree.test.ts | 10 +++++----- packages/ssz/test/unit/snapshot.test.ts | 8 ++++---- 9 files changed, 64 insertions(+), 30 deletions(-) create mode 100644 packages/ssz/src/util/snapshot.ts diff --git a/packages/persistent-merkle-tree/src/snapshot.ts b/packages/persistent-merkle-tree/src/snapshot.ts index bc076d0e..6ba01f0f 100644 --- a/packages/persistent-merkle-tree/src/snapshot.ts +++ b/packages/persistent-merkle-tree/src/snapshot.ts @@ -13,7 +13,11 @@ type Snapshot = { * Tree could be full tree, or partial tree. See https://github.com/ChainSafe/ssz/issues/293 */ export function toSnapshot(rootNode: Node, depth: number, count: number): Snapshot { - const finalizedGindices = indexToFinalizedGindices(depth, count - 1); + if (count < 0) { + throw new Error(`Expect count to be non-negative, got ${count}`); + } + + const finalizedGindices = count > 0 ? indexToFinalizedGindices(depth, count - 1) : []; const finalized = finalizedGindices.map((gindex) => getNode(rootNode, gindex).root); return { @@ -28,15 +32,19 @@ export function toSnapshot(rootNode: Node, depth: number, count: number): Snapsh */ export function fromSnapshot(snapshot: Snapshot, depth: number): Node { const tree = new Tree(zeroNode(depth)); + const {count, finalized} = snapshot; + if (count < 0) { + throw new Error(`Expect count to be non-negative, got ${count}`); + } - const finalizedGindices = indexToFinalizedGindices(depth, snapshot.count - 1); + const finalizedGindices = count > 0 ? indexToFinalizedGindices(depth, count - 1) : []; - if (finalizedGindices.length !== snapshot.finalized.length) { - throw new Error(`Expected ${finalizedGindices.length} finalized gindices, got ${snapshot.finalized.length}`); + if (finalizedGindices.length !== finalized.length) { + throw new Error(`Expected ${finalizedGindices.length} finalized gindices, got ${finalized.length}`); } for (const [i, gindex] of finalizedGindices.entries()) { - const node = LeafNode.fromRoot(snapshot.finalized[i]); + const node = LeafNode.fromRoot(finalized[i]); tree.setNode(gindex, node); } diff --git a/packages/persistent-merkle-tree/test/unit/snapshot.test.ts b/packages/persistent-merkle-tree/test/unit/snapshot.test.ts index 8ff45b4f..4747335e 100644 --- a/packages/persistent-merkle-tree/test/unit/snapshot.test.ts +++ b/packages/persistent-merkle-tree/test/unit/snapshot.test.ts @@ -10,7 +10,7 @@ describe("toSnapshot and fromSnapshot", () => { const depth = 4; const maxItems = Math.pow(2, depth); - for (let count = 1; count <= maxItems; count ++) { + for (let count = 0; count <= maxItems; count ++) { it(`toSnapshot and fromSnapshot with count ${count}`, () => { const nodes = Array.from({length: count}, (_, i) => LeafNode.fromRoot(Buffer.alloc(32, i))); const fullListRootNode = subtreeFillToContents(nodes, depth); diff --git a/packages/ssz/src/type/partialListComposite.ts b/packages/ssz/src/type/partialListComposite.ts index 310295bb..64d87162 100644 --- a/packages/ssz/src/type/partialListComposite.ts +++ b/packages/ssz/src/type/partialListComposite.ts @@ -4,6 +4,7 @@ import {ListCompositeOpts, ListCompositeType} from "./listComposite"; import {PartialListCompositeTreeViewDU} from "../viewDU/partialListComposite"; import {Snapshot} from "../util/types"; import {byteArrayEquals} from "../util/byteArray"; +import {zeroSnapshot} from "../util/snapshot"; /** * Similar to ListCompositeType, this is mainly used to create a PartialListCompositeTreeViewDU from a snapshot. @@ -41,7 +42,10 @@ export class PartialListCompositeType< } } - snapshotToViewDU(snapshot: Snapshot): PartialListCompositeTreeViewDU { + /** + * Create a PartialListCompositeTreeViewDU from a snapshot. + */ + toPartialViewDU(snapshot: Snapshot): PartialListCompositeTreeViewDU { const chunksNode = fromSnapshot(snapshot, this.chunkDepth); // old root node could be whatever, so leave zeroNode(0) here const rootNode = this.tree_setChunksNode(zeroNode(0), chunksNode, snapshot.count); @@ -49,7 +53,17 @@ export class PartialListCompositeType< if (!byteArrayEquals(rootNode.root, snapshot.root)) { throw new Error(`Snapshot root is incorrect, expected ${snapshot.root}, got ${rootNode.root}`); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new PartialListCompositeTreeViewDU(this, rootNode, snapshot); } + + /** + * Creates a PartialListCompositeTreeViewDU from a zero snapshot. + */ + defaultPartialViewDU(): PartialListCompositeTreeViewDU { + // old root node could be whatever, so leave zeroNode(0) here + const rootNode = this.tree_setChunksNode(zeroNode(0), zeroNode(this.chunkDepth), 0); + + return new PartialListCompositeTreeViewDU(this, rootNode, zeroSnapshot(this.chunkDepth)); + } } diff --git a/packages/ssz/src/util/snapshot.ts b/packages/ssz/src/util/snapshot.ts new file mode 100644 index 00000000..48f2bd0e --- /dev/null +++ b/packages/ssz/src/util/snapshot.ts @@ -0,0 +1,14 @@ +import {zeroHash} from "@chainsafe/persistent-merkle-tree"; +import {hash64} from "./merkleize"; +import {Snapshot} from "./types"; + +/** + * Create a zero snapshot with the given chunksDepth. + */ +export function zeroSnapshot(chunkDepth: number): Snapshot { + return { + finalized: [], + count: 0, + root: hash64(zeroHash(chunkDepth), zeroHash(0)), + }; +} diff --git a/packages/ssz/src/util/types.ts b/packages/ssz/src/util/types.ts index 6cf476e1..a456ed58 100644 --- a/packages/ssz/src/util/types.ts +++ b/packages/ssz/src/util/types.ts @@ -1,5 +1,3 @@ -import {zeroNode} from "@chainsafe/persistent-merkle-tree"; - export type Require = T & Required>; export type Snapshot = { @@ -7,9 +5,3 @@ export type Snapshot = { root: Uint8Array; count: number; }; - -export const ZERO_SNAPSHOT = { - finalized: [], - root: zeroNode(0).root, - count: 0, -}; diff --git a/packages/ssz/src/viewDU/listComposite.ts b/packages/ssz/src/viewDU/listComposite.ts index 38b7431b..1957acc5 100644 --- a/packages/ssz/src/viewDU/listComposite.ts +++ b/packages/ssz/src/viewDU/listComposite.ts @@ -5,6 +5,7 @@ import {ListCompositeType} from "../view/listComposite"; import {ArrayCompositeTreeViewDU, ArrayCompositeTreeViewDUCache} from "./arrayComposite"; import {tree_serializeToBytesArrayComposite} from "../type/arrayComposite"; import {Snapshot} from "../util/types"; +import {zeroSnapshot} from "../util/snapshot"; export class ListCompositeTreeViewDU< ElementType extends CompositeType, CompositeView, CompositeViewDU> @@ -116,10 +117,14 @@ export class ListCompositeTreeViewDU< toSnapshot(count: number): Snapshot { // Commit before getting rootNode to ensure all pending data is in the rootNode this.commit(); - if (count <= 0 || count > this._length) { + if (count < 0 || count > this._length) { throw Error(`Invalid count ${count}, length is ${this._length}`); } + if (count === 0) { + return zeroSnapshot(this.type.chunkDepth); + } + // sliceTo is inclusive const rootNode = this.sliceTo(count - 1)._rootNode; const chunksNode = this.type.tree_getChunksNode(rootNode); diff --git a/packages/ssz/src/viewDU/partialListComposite.ts b/packages/ssz/src/viewDU/partialListComposite.ts index 0f6b3f86..b99e0785 100644 --- a/packages/ssz/src/viewDU/partialListComposite.ts +++ b/packages/ssz/src/viewDU/partialListComposite.ts @@ -4,7 +4,8 @@ import {CompositeType, CompositeView, CompositeViewDU} from "../type/composite"; import {ArrayCompositeTreeViewDUCache} from "./arrayComposite"; import {ListCompositeTreeViewDU} from "./listComposite"; import {PartialListCompositeType} from "../type/partialListComposite"; -import {Snapshot, ZERO_SNAPSHOT} from "../util/types"; +import {Snapshot} from "../util/types"; +import {zeroSnapshot} from "../util/snapshot"; /** * Similar to ListCompositeTreeViewDU but this is created from a snapshot so some methods are not supported @@ -93,7 +94,7 @@ export class PartialListCompositeTreeViewDU< if (index === this.snapshot.count - 1) { // super.sliceTo() uses treeZeroAfterIndex() which does not work well in this case // this is because treeZeroAfterIndex() requires navigating the tree to index first which we don't have in this case - return this.type.snapshotToViewDU(this.snapshot) as this; + return this.type.toPartialViewDU(this.snapshot) as this; } return super.sliceTo(index); @@ -126,8 +127,8 @@ export class PartialListCompositeTreeViewDU< const newRootNode = this.type.tree_setChunksNode(this._rootNode, newChunksNode, newLength); - // Use ZERO_SNAPSHOT because this is equivalent to a full tree - return new PartialListCompositeTreeViewDU(this.type, newRootNode, ZERO_SNAPSHOT) as this; + // Use zeroSnapshot because this is equivalent to a full tree + return new PartialListCompositeTreeViewDU(this.type, newRootNode, zeroSnapshot(this.type.chunkDepth)) as this; } serializeToBytes(): number { diff --git a/packages/ssz/test/unit/byType/partialListComposite/tree.test.ts b/packages/ssz/test/unit/byType/partialListComposite/tree.test.ts index f6ab45ad..eea5d9aa 100644 --- a/packages/ssz/test/unit/byType/partialListComposite/tree.test.ts +++ b/packages/ssz/test/unit/byType/partialListComposite/tree.test.ts @@ -29,7 +29,7 @@ describe("PartialListCompositeType ViewDU", function () { for (let snapshotCount = 1; snapshotCount <= maxItem; snapshotCount++) { it(`snapshotCount=${snapshotCount}`, function () { const snapshot = fullList.toSnapshot(snapshotCount); - const partialList = partialListType.snapshotToViewDU(snapshot); + const partialList = partialListType.toPartialViewDU(snapshot); expect(partialList.hashTreeRoot()).to.deep.equal(fullList.sliceTo(snapshotCount - 1).hashTreeRoot()); expect(partialList.toValue()).to.deep.equal(new Array(snapshotCount)); @@ -52,7 +52,7 @@ describe("PartialListCompositeType ViewDU", function () { for (let snapshotCount = 1; snapshotCount <= maxItem; snapshotCount++) { it(`snapshotCount=${snapshotCount}`, function () { const snapshot = fullList.toSnapshot(snapshotCount); - const partialList = partialListType.snapshotToViewDU(snapshot); + const partialList = partialListType.toPartialViewDU(snapshot); expect(partialList.hashTreeRoot()).to.deep.equal(fullList.sliceTo(snapshotCount - 1).hashTreeRoot()); expect(partialList.toValue()).to.deep.equal(new Array(snapshotCount)); @@ -71,7 +71,7 @@ describe("PartialListCompositeType ViewDU", function () { for (let snapshotCount = 1; snapshotCount <= maxItem; snapshotCount++) { it(`snapshotCount=${snapshotCount}`, function () { const snapshot = fullList.toSnapshot(snapshotCount); - const partialList = partialListType.snapshotToViewDU(snapshot); + const partialList = partialListType.toPartialViewDU(snapshot); expect(partialList.hashTreeRoot()).to.deep.equal(fullList.sliceTo(snapshotCount - 1).hashTreeRoot()); expect(partialList.toValue()).to.deep.equal(new Array(snapshotCount)); @@ -94,7 +94,7 @@ describe("PartialListCompositeType ViewDU", function () { for (let snapshotCount = 1; snapshotCount <= maxItem; snapshotCount++) { it(`snapshotCount=${snapshotCount}`, function () { const snapshot = fullList.toSnapshot(snapshotCount); - const partialList = partialListType.snapshotToViewDU(snapshot); + const partialList = partialListType.toPartialViewDU(snapshot); expect(partialList.hashTreeRoot()).to.deep.equal(fullList.sliceTo(snapshotCount - 1).hashTreeRoot()); expect(partialList.toValue()).to.deep.equal(new Array(snapshotCount)); @@ -120,7 +120,7 @@ describe("PartialListCompositeType ViewDU", function () { for (let snapshotCount = 1; snapshotCount <= maxItem; snapshotCount++) { it(`snapshotCount=${snapshotCount}`, function () { const snapshot = fullList.toSnapshot(snapshotCount); - const partialList = partialListType.snapshotToViewDU(snapshot); + const partialList = partialListType.toPartialViewDU(snapshot); expect(partialList.hashTreeRoot()).to.deep.equal(fullList.sliceTo(snapshotCount - 1).hashTreeRoot()); expect(partialList.toValue()).to.deep.equal(new Array(snapshotCount)); diff --git a/packages/ssz/test/unit/snapshot.test.ts b/packages/ssz/test/unit/snapshot.test.ts index 9c1a8a23..6c07ffea 100644 --- a/packages/ssz/test/unit/snapshot.test.ts +++ b/packages/ssz/test/unit/snapshot.test.ts @@ -18,11 +18,11 @@ describe("snapshot", () => { } fullList.commit(); - for (let snapshotCount = 1; snapshotCount <= maxItems; snapshotCount++) { + for (let snapshotCount = 0; snapshotCount <= maxItems; snapshotCount++) { // toSnapshot uses sliceTo, it's good to test sliceFrom too it(`toSnapshot and fromSnapshot then sliceTo/sliceFrom with snapshotCount ${snapshotCount}`, () => { const snapshot = fullList.toSnapshot(snapshotCount); - const partialList = partialListType.snapshotToViewDU(snapshot); + const partialList = partialListType.toPartialViewDU(snapshot); // 1st step - check if the restored root node is the same expect(partialList.hashTreeRoot()).to.deep.equal(fullList.sliceTo(snapshotCount - 1).hashTreeRoot()); @@ -48,7 +48,7 @@ describe("snapshot", () => { it(`toSnapshot and fromSnapshot then subsequent toSnapshot calls with snapshotCount ${snapshotCount}`, () => { const snapshot = fullList.toSnapshot(snapshotCount); - const partialList = partialListType.snapshotToViewDU(snapshot); + const partialList = partialListType.toPartialViewDU(snapshot); // 1st step - check if the restored root node is the same expect(partialList.hashTreeRoot()).to.deep.equal(fullList.sliceTo(snapshotCount - 1).hashTreeRoot()); @@ -61,7 +61,7 @@ describe("snapshot", () => { // confirm toSnapshot() works for (let j = snapshotCount; j <= i; j++) { const snapshot2 = partialList.toSnapshot(j); - const partialList2 = partialListType.snapshotToViewDU(snapshot2); + const partialList2 = partialListType.toPartialViewDU(snapshot2); // sliceTo() is inclusive const fullListToJ = fullList.sliceTo(j - 1); expect(partialList2.length).to.equal(fullListToJ.length);