diff --git a/packages/ssz/src/view/arrayBasic.ts b/packages/ssz/src/view/arrayBasic.ts index 7f821a96..4502e865 100644 --- a/packages/ssz/src/view/arrayBasic.ts +++ b/packages/ssz/src/view/arrayBasic.ts @@ -87,14 +87,18 @@ export class ArrayBasicTreeView> extends /** * Get all values of this array as Basic element type values, from index zero to `this.length - 1` + * @param values optional output parameter, if is provided it must be an array of the same length as this array */ - getAll(): ValueOf[] { + getAll(values?: ValueOf[]): ValueOf[] { + if (values && values.length !== this.length) { + throw Error(`Expected ${this.length} values, got ${values.length}`); + } const length = this.length; const chunksNode = this.type.tree_getChunksNode(this.node); const chunkCount = Math.ceil(length / this.type.itemsPerChunk); const leafNodes = getNodesAtDepth(chunksNode, this.type.chunkDepth, 0, chunkCount) as LeafNode[]; - const values = new Array>(length); + values = values ?? new Array>(length); const itemsPerChunk = this.type.itemsPerChunk; // Prevent many access in for loop below const lenFullNodes = Math.floor(length / itemsPerChunk); const remainder = length % itemsPerChunk; diff --git a/packages/ssz/src/view/arrayComposite.ts b/packages/ssz/src/view/arrayComposite.ts index b74799b2..415dcb76 100644 --- a/packages/ssz/src/view/arrayComposite.ts +++ b/packages/ssz/src/view/arrayComposite.ts @@ -73,12 +73,16 @@ export class ArrayCompositeTreeView< * Returns an array of views of all elements in the array, from index zero to `this.length - 1`. * The returned views don't have a parent hook to this View's Tree, so changes in the returned views won't be * propagated upwards. To get linked element Views use `this.get()` + * @param views optional output parameter, if is provided it must be an array of the same length as this array */ - getAllReadonly(): CompositeView[] { + getAllReadonly(views?: CompositeView[]): CompositeView[] { + if (views && views.length !== this.length) { + throw Error(`Expected ${this.length} views, got ${views.length}`); + } const length = this.length; const chunksNode = this.type.tree_getChunksNode(this.node); const nodes = getNodesAtDepth(chunksNode, this.type.chunkDepth, 0, length); - const views = new Array>(length); + views = views ?? new Array>(length); for (let i = 0; i < length; i++) { // TODO: Optimize views[i] = this.type.elementType.getView(new Tree(nodes[i])); @@ -90,12 +94,16 @@ export class ArrayCompositeTreeView< * Returns an array of values of all elements in the array, from index zero to `this.length - 1`. * The returned values are not Views so any changes won't be propagated upwards. * To get linked element Views use `this.get()` + * @param values optional output parameter, if is provided it must be an array of the same length as this array */ - getAllReadonlyValues(): ValueOf[] { + getAllReadonlyValues(values?: ValueOf[]): ValueOf[] { + if (values && values.length !== this.length) { + throw Error(`Expected ${this.length} values, got ${values.length}`); + } const length = this.length; const chunksNode = this.type.tree_getChunksNode(this.node); const nodes = getNodesAtDepth(chunksNode, this.type.chunkDepth, 0, length); - const values = new Array>(length); + values = values ?? new Array>(length); for (let i = 0; i < length; i++) { values[i] = this.type.elementType.tree_toValue(nodes[i]); } diff --git a/packages/ssz/src/viewDU/arrayBasic.ts b/packages/ssz/src/viewDU/arrayBasic.ts index 1a06d84a..d66596ee 100644 --- a/packages/ssz/src/viewDU/arrayBasic.ts +++ b/packages/ssz/src/viewDU/arrayBasic.ts @@ -109,8 +109,12 @@ export class ArrayBasicTreeViewDU> extend /** * Get all values of this array as Basic element type values, from index zero to `this.length - 1` + * @param values optional output parameter, if is provided it must be an array of the same length as this array */ - getAll(): ValueOf[] { + getAll(values?: ValueOf[]): ValueOf[] { + if (values && values.length !== this._length) { + throw Error(`Expected ${this._length} values, got ${values.length}`); + } if (!this.nodesPopulated) { const nodesPrev = this.nodes; const chunksNode = this.type.tree_getChunksNode(this.node); @@ -125,7 +129,7 @@ export class ArrayBasicTreeViewDU> extend this.nodesPopulated = true; } - const values = new Array>(this._length); + values = values ?? new Array>(this._length); const itemsPerChunk = this.type.itemsPerChunk; // Prevent many access in for loop below const lenFullNodes = Math.floor(this._length / itemsPerChunk); const remainder = this._length % itemsPerChunk; diff --git a/packages/ssz/src/viewDU/arrayComposite.ts b/packages/ssz/src/viewDU/arrayComposite.ts index 44c50375..db3d91d2 100644 --- a/packages/ssz/src/viewDU/arrayComposite.ts +++ b/packages/ssz/src/viewDU/arrayComposite.ts @@ -146,30 +146,58 @@ export class ArrayCompositeTreeViewDU< /** * WARNING: Returns all commited changes, if there are any pending changes commit them beforehand + * @param views optional output parameter, if is provided it must be an array of the same length as this array */ - getAllReadonly(): CompositeViewDU[] { + getAllReadonly(views?: CompositeViewDU[]): CompositeViewDU[] { + if (views && views.length !== this._length) { + throw Error(`Expected ${this._length} views, got ${views.length}`); + } this.populateAllNodes(); - const views = new Array>(this._length); + views = views ?? new Array>(this._length); for (let i = 0; i < this._length; i++) { views[i] = this.type.elementType.getViewDU(this.nodes[i], this.caches[i]); } return views; } + /** + * Apply `fn` to each ViewDU in the array + */ + forEach(fn: (viewDU: CompositeViewDU, index: number) => void): void { + this.populateAllNodes(); + for (let i = 0; i < this._length; i++) { + fn(this.type.elementType.getViewDU(this.nodes[i], this.caches[i]), i); + } + } + /** * WARNING: Returns all commited changes, if there are any pending changes commit them beforehand + * @param values optional output parameter, if is provided it must be an array of the same length as this array */ - getAllReadonlyValues(): ValueOf[] { + getAllReadonlyValues(values?: ValueOf[]): ValueOf[] { + if (values && values.length !== this._length) { + throw Error(`Expected ${this._length} values, got ${values.length}`); + } this.populateAllNodes(); - const values = new Array>(this._length); + values = values ?? new Array>(this._length); for (let i = 0; i < this._length; i++) { values[i] = this.type.elementType.tree_toValue(this.nodes[i]); } return values; } + /** + * Apply `fn` to each value in the array + */ + forEachValue(fn: (value: ValueOf, index: number) => void): void { + this.populateAllNodes(); + for (let i = 0; i < this._length; i++) { + fn(this.type.elementType.tree_toValue(this.nodes[i]), i); + } + } + /** * When we need to compute HashComputations (hcByLevel != null): * - if old _rootNode is hashed, then only need to put pending changes to hcByLevel @@ -193,9 +221,12 @@ export class ArrayCompositeTreeViewDU< for (const [index, view] of this.viewsChanged) { 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}); + // there's a chance the view is not changed, no need to rebind nodes in that case + if (this.nodes[index] !== node) { + // 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}); + } // Cache the view's caches to preserve it's data after 'this.viewsChanged.clear()' const cache = this.type.elementType.cacheOfViewDU(view); diff --git a/packages/ssz/src/viewDU/container.ts b/packages/ssz/src/viewDU/container.ts index 993ff602..68b0a00b 100644 --- a/packages/ssz/src/viewDU/container.ts +++ b/packages/ssz/src/viewDU/container.ts @@ -101,9 +101,12 @@ export class BasicContainerTreeViewDU { }); } }); + +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/unchangedViewDUs.test.ts b/packages/ssz/test/unit/unchangedViewDUs.test.ts index f1e57a36..aa61dfe2 100644 --- a/packages/ssz/test/unit/unchangedViewDUs.test.ts +++ b/packages/ssz/test/unit/unchangedViewDUs.test.ts @@ -5,7 +5,7 @@ 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", () => { + it("should not recompute batchHashTreeRoot() when no fields is changed", () => { const root = state.batchHashTreeRoot(); // this causes viewsChanged inside BeaconState container state.validators.length;