Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new type for list of uint64 #352

Merged
merged 2 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions packages/persistent-merkle-tree/src/packedNode.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,45 @@
import {subtreeFillToContents} from "./subtree";
import {Node, LeafNode, getNodeH, setNodeH} from "./node";

const NUMBER_2_POW_32 = 2 ** 32;

export function packedRootsBytesToNode(depth: number, dataView: DataView, start: number, end: number): Node {
const leafNodes = packedRootsBytesToLeafNodes(dataView, start, end);
return subtreeFillToContents(leafNodes, depth);
}

/**
* Pack a list of uint64 numbers into a list of LeafNodes.
* Each value is UintNum64, which is 8 bytes long, which is 2 h values.
* Each 4 of them forms a LeafNode.
*
* v0 v1 v2 v3
* |-------------|-------------|-------------|-------------|
*
* h0 h1 h2 h3 h4 h5 h6 h7
* |------|------|------|------|------|------|------|------|
*/
export function packedUintNum64sToLeafNodes(values: number[]): LeafNode[] {
const leafNodes = new Array<LeafNode>(Math.ceil(values.length / 4));
for (let i = 0; i < values.length; i++) {
const nodeIndex = Math.floor(i / 4);
const leafNode = leafNodes[nodeIndex] ?? new LeafNode(0, 0, 0, 0, 0, 0, 0, 0);
const vIndex = i % 4;
const hIndex = 2 * vIndex;
const value = values[i];
// same logic to UintNumberType.value_serializeToBytes() for 8 bytes
if (value === Infinity) {
setNodeH(leafNode, hIndex, 0xffffffff);
setNodeH(leafNode, hIndex + 1, 0xffffffff);
} else {
setNodeH(leafNode, hIndex, value & 0xffffffff);
setNodeH(leafNode, hIndex + 1, (value / NUMBER_2_POW_32) & 0xffffffff);
}
leafNodes[nodeIndex] = leafNode;
}
return leafNodes;
}

/**
* Optimized deserialization of linear bytes to consecutive leaf nodes
*/
Expand Down
126 changes: 124 additions & 2 deletions packages/persistent-merkle-tree/test/unit/packedNode.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {HashObject} from "@chainsafe/as-sha256";
import {expect} from "chai";
import {LeafNode, Node} from "../../src";
import {packedNodeRootsToBytes, packedRootsBytesToLeafNodes} from "../../src/packedNode";
import {packedNodeRootsToBytes, packedRootsBytesToLeafNodes, packedUintNum64sToLeafNodes} from "../../src/packedNode";

describe("subtree / packedNode single node", () => {
const testCases: {
id: string;
size: number;
nodes: Node[];
outStr: string;
testPackedNumbers?: boolean;
}[] = [
{
id: "One byte",
Expand Down Expand Up @@ -48,11 +49,37 @@ describe("subtree / packedNode single node", () => {
nodes: [LeafNode.fromHashObject({h0: 0x0708090a, h1: 0x01020304, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0, h7: 0})],
outStr: "0x0a09080704030201",
},
{
id: "2 h values fits uint64 number",
size: 8,
nodes: [LeafNode.fromHashObject({h0: 0x0708090a, h1: 0x0102030, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0, h7: 0})],
outStr: "0x0a09080730201000",
testPackedNumbers: true,
},
{
id: "32 bytes zero",
size: 32,
nodes: [LeafNode.fromHashObject({h0: 0, h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0, h7: 0})],
outStr: "0x0000000000000000000000000000000000000000000000000000000000000000",
testPackedNumbers: true,
},
{
id: "32 bytes max",
size: 32,
nodes: [
LeafNode.fromHashObject({
h0: 0xffffffff,
h1: 0xffffffff,
h2: 0xffffffff,
h3: 0xffffffff,
h4: 0xffffffff,
h5: 0xffffffff,
h6: 0xffffffff,
h7: 0xffffffff,
}),
],
outStr: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
testPackedNumbers: true,
},
{
id: "32 bytes same",
Expand All @@ -77,9 +104,82 @@ describe("subtree / packedNode single node", () => {
],
outStr: "0x18735c375d8e7b2922ef64f165d10b9ace103f326627f0a21d0fe8a0a573f83f",
},
// same to random tests but trim h1, h3, h5, h7 to make h{2*i} + h{2*i + 1} fits uint64
{
id: "8 bytes random fits unit64 number",
size: 32,
nodes: [
LeafNode.fromHashObject({
h0: 928805656,
h1: 6959632,
h2: 0,
h3: 0,
h4: 0,
h5: 0,
h6: 0,
h7: 0,
}),
],
outStr: "0x18735c3710326a00000000000000000000000000000000000000000000000000",
testPackedNumbers: true,
},
{
id: "16 bytes random fits unit64 number",
size: 32,
nodes: [
LeafNode.fromHashObject({
h0: 928805656,
h1: 6959632,
h2: 4049923874,
h3: 258446,
h4: 0,
h5: 0,
h6: 0,
h7: 0,
}),
],
outStr: "0x18735c3710326a0022ef64f18ef1030000000000000000000000000000000000",
testPackedNumbers: true,
},
{
id: "24 bytes random fits unit64 number",
size: 32,
nodes: [
LeafNode.fromHashObject({
h0: 928805656,
h1: 6959632,
h2: 4049923874,
h3: 258446,
h4: 842993870,
h5: 273364,
h6: 0,
h7: 0,
}),
],
outStr: "0x18735c3710326a0022ef64f18ef10300ce103f32d42b04000000000000000000",
testPackedNumbers: true,
},
{
id: "32 bytes random fits unit64 number",
size: 32,
nodes: [
LeafNode.fromHashObject({
h0: 928805656,
h1: 6959632,
h2: 4049923874,
h3: 258446,
h4: 842993870,
h5: 273364,
h6: 2699562781,
h7: 107324,
}),
],
outStr: "0x18735c3710326a0022ef64f18ef10300ce103f32d42b04001d0fe8a03ca30100",
testPackedNumbers: true,
},
];

for (const {id, size, nodes, outStr} of testCases) {
for (const {id, size, nodes, outStr, testPackedNumbers} of testCases) {
it(`${id} - packedNodeRootsToBytes`, () => {
const uint8Array = new Uint8Array(size);
const dataView = new DataView(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteLength);
Expand All @@ -93,6 +193,28 @@ describe("subtree / packedNode single node", () => {
const nodesRes = packedRootsBytesToLeafNodes(dataView, 0, size);
expect(onlyHashObject(nodesRes[0].rootHashObject)).to.deep.equal(onlyHashObject(nodes[0].rootHashObject));
});

// 1 UintNum64 = 8 bytes
if (testPackedNumbers) {
const NUMBER_2_POW_32 = 2 ** 32;
it(`${id} - packedUintNum64sToLeafNodes, value size=${Math.floor(size / 8)}`, () => {
const values: number[] = [];
const uint8Array = new Uint8Array(Buffer.from(outStr.replace("0x", ""), "hex"));
const dataView = new DataView(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteLength);
for (let i = 0; i < size; i += 8) {
const a = dataView.getUint32(i, true);
const b = dataView.getUint32(i + 4, true);
if (a === 0xffffffff && b === 0xffffffff) {
values.push(Infinity);
} else {
values.push(b * NUMBER_2_POW_32 + a);
}
}

const nodesRes = packedUintNum64sToLeafNodes(values);
expect(onlyHashObject(nodesRes[0].rootHashObject)).to.deep.equal(onlyHashObject(nodes[0].rootHashObject));
});
}
}
});

Expand Down
49 changes: 49 additions & 0 deletions packages/ssz/src/type/listUintNum64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {LeafNode, Node, packedUintNum64sToLeafNodes, subtreeFillToContents} from "@chainsafe/persistent-merkle-tree";

import {ListBasicTreeViewDU} from "../viewDU/listBasic";
import {ListBasicOpts, ListBasicType} from "./listBasic";
import {UintNumberType} from "./uint";
import {addLengthNode} from "./arrayBasic";

/**
* Specific implementation of ListBasicType for UintNumberType with some optimizations.
*/
export class ListUintNum64Type extends ListBasicType<UintNumberType> {
constructor(limit: number, opts?: ListBasicOpts) {
super(new UintNumberType(8), limit, opts);
}

/**
* Return a ListBasicTreeViewDU with nodes populated
*/
toViewDU(value: number[]): ListBasicTreeViewDU<UintNumberType> {
// no need to serialize and deserialize like in the abstract class
const {treeNode, leafNodes} = this.packedUintNum64sToNode(value);
// cache leaf nodes in the ViewDU
return this.getViewDU(treeNode, {
nodes: leafNodes,
length: value.length,
nodesPopulated: true,
});
}

/**
* No need to serialize and deserialize like in the abstract class
*/
value_toTree(value: number[]): Node {
const {treeNode} = this.packedUintNum64sToNode(value);
return treeNode;
}

private packedUintNum64sToNode(value: number[]): {treeNode: Node; leafNodes: LeafNode[]} {
if (value.length > this.limit) {
throw new Error(`Exceeds limit: ${value.length} > ${this.limit}`);
}

const leafNodes = packedUintNum64sToLeafNodes(value);
// subtreeFillToContents mutates the leafNodes array
const rootNode = subtreeFillToContents([...leafNodes], this.chunkDepth);
const treeNode = addLengthNode(rootNode, value.length);
return {treeNode, leafNodes};
}
}
3 changes: 2 additions & 1 deletion packages/ssz/test/lodestarTypes/phase0/sszTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
VectorBasicType,
VectorCompositeType,
} from "../../../src";
import {ListUintNum64Type} from "../../../src/type/listUintNum64";
import {
preset,
MAX_REQUEST_BLOCKS,
Expand Down Expand Up @@ -250,7 +251,7 @@ export const Validator = ValidatorNodeStruct;

// Export as stand-alone for direct tree optimizations
export const Validators = new ListCompositeType(ValidatorNodeStruct, VALIDATOR_REGISTRY_LIMIT);
export const Balances = new ListBasicType(UintNum64, VALIDATOR_REGISTRY_LIMIT);
export const Balances = new ListUintNum64Type(VALIDATOR_REGISTRY_LIMIT);
export const RandaoMixes = new VectorCompositeType(Bytes32, EPOCHS_PER_HISTORICAL_VECTOR);
export const Slashings = new VectorBasicType(Gwei, EPOCHS_PER_SLASHINGS_VECTOR);
export const JustificationBits = new BitVectorType(JUSTIFICATION_BITS_LENGTH);
Expand Down
Loading
Loading