Skip to content

Commit

Permalink
feat: getAll() apis to support output parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
twoeths committed Oct 21, 2024
1 parent cf3e1f0 commit 8d29f2b
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 19 deletions.
8 changes: 6 additions & 2 deletions packages/ssz/src/view/arrayBasic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,18 @@ export class ArrayBasicTreeView<ElementType extends BasicType<unknown>> 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<ElementType>[] {
getAll(values?: ValueOf<ElementType>[]): ValueOf<ElementType>[] {
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<ValueOf<ElementType>>(length);
values = values ?? new Array<ValueOf<ElementType>>(length);
const itemsPerChunk = this.type.itemsPerChunk; // Prevent many access in for loop below
const lenFullNodes = Math.floor(length / itemsPerChunk);
const remainder = length % itemsPerChunk;
Expand Down
16 changes: 12 additions & 4 deletions packages/ssz/src/view/arrayComposite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ElementType>[] {
getAllReadonly(views?: CompositeView<ElementType>[]): CompositeView<ElementType>[] {
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<CompositeView<ElementType>>(length);
views = views ?? new Array<CompositeView<ElementType>>(length);
for (let i = 0; i < length; i++) {
// TODO: Optimize
views[i] = this.type.elementType.getView(new Tree(nodes[i]));
Expand All @@ -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<ElementType>[] {
getAllReadonlyValues(values?: ValueOf<ElementType>[]): ValueOf<ElementType>[] {
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<ValueOf<ElementType>>(length);
values = values ?? new Array<ValueOf<ElementType>>(length);
for (let i = 0; i < length; i++) {
values[i] = this.type.elementType.tree_toValue(nodes[i]);
}
Expand Down
8 changes: 6 additions & 2 deletions packages/ssz/src/viewDU/arrayBasic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,12 @@ export class ArrayBasicTreeViewDU<ElementType extends BasicType<unknown>> 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<ElementType>[] {
getAll(values?: ValueOf<ElementType>[]): ValueOf<ElementType>[] {
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);
Expand All @@ -125,7 +129,7 @@ export class ArrayBasicTreeViewDU<ElementType extends BasicType<unknown>> extend
this.nodesPopulated = true;
}

const values = new Array<ValueOf<ElementType>>(this._length);
values = values ?? new Array<ValueOf<ElementType>>(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;
Expand Down
45 changes: 38 additions & 7 deletions packages/ssz/src/viewDU/arrayComposite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ElementType>[] {
getAllReadonly(views?: CompositeViewDU<ElementType>[]): CompositeViewDU<ElementType>[] {
if (views && views.length !== this._length) {
throw Error(`Expected ${this._length} views, got ${views.length}`);
}
this.populateAllNodes();

const views = new Array<CompositeViewDU<ElementType>>(this._length);
views = views ?? new Array<CompositeViewDU<ElementType>>(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<ElementType>, 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<ElementType>[] {
getAllReadonlyValues(values?: ValueOf<ElementType>[]): ValueOf<ElementType>[] {
if (values && values.length !== this._length) {
throw Error(`Expected ${this._length} values, got ${values.length}`);
}
this.populateAllNodes();

const values = new Array<ValueOf<ElementType>>(this._length);
values = values ?? new Array<ValueOf<ElementType>>(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<ElementType>, 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
Expand All @@ -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);
Expand Down
9 changes: 6 additions & 3 deletions packages/ssz/src/viewDU/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,12 @@ export class BasicContainerTreeViewDU<Fields extends Record<string, Type<unknown
for (const [index, view] of this.viewsChanged) {
const fieldType = this.type.fieldsEntries[index].fieldType as unknown as CompositeTypeAny;
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});
// 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 = fieldType.cacheOfViewDU(view);
Expand Down
112 changes: 112 additions & 0 deletions packages/ssz/test/unit/byType/listComposite/tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,115 @@ describe("ListCompositeType batchHashTreeRoot", () => {
});
}
});

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);
});
}
});
2 changes: 1 addition & 1 deletion packages/ssz/test/unit/unchangedViewDUs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 8d29f2b

Please sign in to comment.