Skip to content

Commit

Permalink
feat: new batchHashTreeRoot() method for ViewDU
Browse files Browse the repository at this point in the history
  • Loading branch information
twoeths committed Aug 7, 2024
1 parent 23557d7 commit aa7ebfb
Show file tree
Hide file tree
Showing 18 changed files with 179 additions and 202 deletions.
5 changes: 0 additions & 5 deletions packages/persistent-merkle-tree/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,5 @@
"@chainsafe/as-sha256": "0.5.0",
"@chainsafe/hashtree": "1.0.1",
"@noble/hashes": "^1.3.0"
},
"peerDependencies": {
"@chainsafe/hashtree-linux-x64-gnu": "1.0.1",
"@chainsafe/hashtree-linux-arm64-gnu": "1.0.1",
"@chainsafe/hashtree-darwin-arm64": "1.0.1"
}
}
1 change: 0 additions & 1 deletion packages/persistent-merkle-tree/src/subtree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export function subtreeFillToLength(bottom: Node, depth: number, length: number)

/**
* WARNING: Mutates the provided nodes array.
* @param hashCompRootNode is a hacky way from ssz to set `dest` of HashComputation for BranchNodeStruct
* TODO: Don't mutate the nodes array.
* hcByLevel is an output parameter that will be filled with the hash computations if exists.
*/
Expand Down
13 changes: 10 additions & 3 deletions packages/ssz/src/viewDU/abstract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,17 @@ export abstract class TreeViewDU<T extends CompositeType<unknown, unknown, unkno
* See spec for definition of hashTreeRoot:
* https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization
*/
hashTreeRoot(hcGroup: HashComputationGroup = new HashComputationGroup()): Uint8Array {
hashTreeRoot(): Uint8Array {
this.commit();
return super.hashTreeRoot();
}

/**
* The same to hashTreeRoot() but with batch hash computation.
* Consumer can provide a HashComputationGroup() to save memory allocation.
*/
batchHashTreeRoot(hcGroup: HashComputationGroup = new HashComputationGroup()): Uint8Array {
// in ethereum consensus, the only type goes with TVDU is BeaconState and it's really more efficient to hash the tree in batch
// if consumers don't want to batch hash, just go with `this.node.root` similar to what View.hashTreeRoot() does
// there should not be another ViewDU.hashTreeRoot() during this flow so it's safe to reuse nextHashComps
const offset = 0;
hcGroup.reset();
this.commit(offset, hcGroup.byLevel);
Expand Down
20 changes: 9 additions & 11 deletions packages/ssz/test/perf/eth2/beaconState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,25 @@ const expectedRoot = "0x0bd3c6caecdf5b04e8ac48e41732aa5908019e072aa4e61c5298cf31
*/
describe(`BeaconState ViewDU partially modified tree vc=${vc} numModified=${numModified}`, function () {
itBench({
id: `BeaconState ViewDU recursive hash vc=${vc}`,
id: `BeaconState ViewDU hashTreeRoot() vc=${vc}`,
beforeEach: () => createPartiallyModifiedDenebState(),
fn: (state: CompositeViewDU<typeof BeaconState>) => {
state.commit();
state.node.root;
// console.log("@@@@ root", toHexString(state.node.root));
state.hashTreeRoot();
if (toHexString(state.node.root) !== expectedRoot) {
throw new Error("hashTreeRoot does not match expectedRoot");
}
},
});

itBench({
itBench.skip({
id: `BeaconState ViewDU recursive hash - commit step vc=${vc}`,
beforeEach: () => createPartiallyModifiedDenebState(),
fn: (state: CompositeViewDU<typeof BeaconState>) => {
state.commit();
},
});

itBench({
itBench.skip({
id: `BeaconState ViewDU validator tree creation vc=${numModified}`,
beforeEach: () => {
const state = createPartiallyModifiedDenebState();
Expand All @@ -54,25 +52,25 @@ describe(`BeaconState ViewDU partially modified tree vc=${vc} numModified=${numM

const hc = new HashComputationGroup();
itBench({
id: `BeaconState ViewDU hashTreeRoot vc=${vc}`,
id: `BeaconState ViewDU batchHashTreeRoot vc=${vc}`,
beforeEach: () => createPartiallyModifiedDenebState(),
fn: (state: CompositeViewDU<typeof BeaconState>) => {
// commit() step is inside hashTreeRoot(), reuse HashComputationGroup
if (toHexString(state.hashTreeRoot(hc)) !== expectedRoot) {
throw new Error("hashTreeRoot does not match expectedRoot");
if (toHexString(state.batchHashTreeRoot(hc)) !== expectedRoot) {
throw new Error("batchHashTreeRoot does not match expectedRoot");
}
},
});

itBench({
itBench.skip({
id: `BeaconState ViewDU hashTreeRoot - commit step vc=${vc}`,
beforeEach: () => createPartiallyModifiedDenebState(),
fn: (state: CompositeViewDU<typeof BeaconState>) => {
state.commit(0, []);
},
});

itBench({
itBench.skip({
id: `BeaconState ViewDU hashTreeRoot - hash step vc=${vc}`,
beforeEach: () => {
const state = createPartiallyModifiedDenebState();
Expand Down
12 changes: 11 additions & 1 deletion packages/ssz/test/perf/eth2/hashTreeRoot.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -68,6 +68,16 @@ describe("HashTreeRoot frequent eth2 objects", () => {
},
});

const hc = new HashComputationGroup();
itBench<CompositeViewDU<typeof sszAltair.BeaconState>, 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)) {
Expand Down
14 changes: 14 additions & 0 deletions packages/ssz/test/spec/runValidTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,20 @@ export function runValidSszTest(type: Type<unknown>, 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()");
Expand Down
6 changes: 3 additions & 3 deletions packages/ssz/test/unit/byType/bitArray/tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,19 @@ for (const type of [new BitVectorType(4), new BitListType(4)]) {
});
}

describe("BitArray batchHash", () => {
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).hashTreeRoot()).to.be.deep.equal(expectedRoot);
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).hashTreeRoot()).to.be.deep.equal(expectedRoot);
expect(sszType.toViewDU(value).batchHashTreeRoot()).to.be.deep.equal(expectedRoot);
});
});

Expand Down
8 changes: 4 additions & 4 deletions packages/ssz/test/unit/byType/bitVector/tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,19 @@ runViewTestMutation({
],
});

describe("BitVector batchHash", () => {
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).hashTreeRoot()).to.be.deep.equal(expectedRoot);
expect(sszType.toViewDU(value).batchHashTreeRoot()).to.be.deep.equal(expectedRoot);
});

it("set then hashTreeRoot", () => {
it("set then batchHashTreeRoot", () => {
const viewDU = sszType.toViewDU(fromNum(4, 0b0011));
viewDU.set(0, false);
expect(sszType.toViewDU(value).hashTreeRoot()).to.be.deep.equal(expectedRoot);
expect(sszType.toViewDU(value).batchHashTreeRoot()).to.be.deep.equal(expectedRoot);
});
});

Expand Down
24 changes: 12 additions & 12 deletions packages/ssz/test/unit/byType/container/tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ runViewTestMutation({
],
});

describe("ContainerViewDU batchHash", function () {
describe("ContainerViewDU batchHashTreeRoot", function () {
const childContainerType = new ContainerType({b0: uint64NumInfType, b1: uint64NumInfType});
const parentContainerType = new ContainerType({
// a basic type
Expand All @@ -233,29 +233,29 @@ describe("ContainerViewDU batchHash", function () {
const expectedRoot = parentContainerType.toView(value).hashTreeRoot();

it("fresh ViewDU", () => {
expect(parentContainerType.toViewDU(value).hashTreeRoot()).to.be.deep.equal(expectedRoot);
expect(parentContainerType.toViewDU(value).batchHashTreeRoot()).to.be.deep.equal(expectedRoot);
});

it("full hash then modify basic type", () => {
const viewDU = parentContainerType.toViewDU({a: 9, b: {b0: 100, b1: 101}});
viewDU.hashTreeRoot();
viewDU.batchHashTreeRoot();
viewDU.a += 1;
expect(viewDU.hashTreeRoot()).to.be.deep.equal(expectedRoot);
expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot);
});

it("full hash then modify full child container", () => {
const viewDU = parentContainerType.toViewDU({a: 10, b: {b0: 99, b1: 999}});
viewDU.hashTreeRoot();
viewDU.batchHashTreeRoot();
viewDU.b = childContainerType.toViewDU({b0: 100, b1: 101});
expect(viewDU.hashTreeRoot()).to.be.deep.equal(expectedRoot);
expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot);
});

it("full hash then modify partial child container", () => {
const viewDU = parentContainerType.toViewDU({a: 10, b: {b0: 99, b1: 999}});
viewDU.hashTreeRoot();
viewDU.batchHashTreeRoot();
viewDU.b.b0 = 100;
viewDU.b.b1 = 101;
expect(viewDU.hashTreeRoot()).to.be.deep.equal(expectedRoot);
expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot);
});
});

Expand Down Expand Up @@ -286,14 +286,14 @@ describe("ContainerNodeStruct batchHash", function () {
const expectedRoot = containerType.toView(value).hashTreeRoot();

it("fresh ViewDU", () => {
expect(containerType.toViewDU(value).hashTreeRoot()).to.be.deep.equal(expectedRoot);
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.hashTreeRoot();
viewDU.batchHashTreeRoot();
viewDU.exitEpoch *= 1_000_000;
expect(viewDU.hashTreeRoot()).to.be.deep.equal(expectedRoot);
expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot);
});

it("modify basic type", () => {
Expand All @@ -304,6 +304,6 @@ describe("ContainerNodeStruct batchHash", function () {
});
viewDU.exitEpoch -= 1;
viewDU.withdrawableEpoch -= 1;
expect(viewDU.hashTreeRoot()).to.be.deep.equal(expectedRoot);
expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot);
});
});
20 changes: 10 additions & 10 deletions packages/ssz/test/unit/byType/listBasic/tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,31 +241,31 @@ describe("ListBasicType.sliceTo", () => {
}
});

describe("ListBasicType batchHash", function () {
describe("ListBasicType batchHashTreeRoot", function () {
const value = [1, 2, 3, 4];
const expectedRoot = ListN64Uint64NumberType.toView(value).hashTreeRoot();

it("fresh ViewDU", () => {
expect(ListN64Uint64NumberType.toViewDU(value).hashTreeRoot()).to.be.deep.equal(expectedRoot);
expect(ListN64Uint64NumberType.toViewDU(value).batchHashTreeRoot()).to.be.deep.equal(expectedRoot);
});

it("push then hashTreeRoot()", () => {
it("push then batchHashTreeRoot()", () => {
const viewDU = ListN64Uint64NumberType.defaultViewDU();
viewDU.push(1);
viewDU.push(2);
viewDU.push(3);
viewDU.push(4);
expect(viewDU.hashTreeRoot()).to.be.deep.equal(expectedRoot);
expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot);
});

it("push then modify then hashTreeRoot()", () => {
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.hashTreeRoot()).to.be.deep.equal(expectedRoot);
expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot);
});

it("full hash then modify", () => {
Expand All @@ -274,10 +274,10 @@ describe("ListBasicType batchHash", function () {
viewDU.push(2);
viewDU.push(33);
viewDU.push(44);
viewDU.hashTreeRoot();
viewDU.batchHashTreeRoot();
viewDU.set(2, 3);
viewDU.set(3, 4);
expect(viewDU.hashTreeRoot()).to.be.deep.equal(expectedRoot);
expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot);
});

// similar to a fresh ViewDU but it's good to test
Expand All @@ -288,7 +288,7 @@ describe("ListBasicType batchHash", function () {
viewDU.push(3);
viewDU.push(4);
viewDU.push(5);
viewDU.hashTreeRoot();
expect(viewDU.sliceTo(3).hashTreeRoot()).to.be.deep.equal(expectedRoot);
viewDU.batchHashTreeRoot();
expect(viewDU.sliceTo(3).batchHashTreeRoot()).to.be.deep.equal(expectedRoot);
});
});
Loading

0 comments on commit aa7ebfb

Please sign in to comment.