diff --git a/.gitignore b/.gitignore index 4459a26d..470aa6ef 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ dist/ lib/ node_modules/ .idea/ +.vscode yarn-error.log test/spec-tests-data diff --git a/src/types/basic/abstract.ts b/src/types/basic/abstract.ts index 24d22c00..2a33027d 100644 --- a/src/types/basic/abstract.ts +++ b/src/types/basic/abstract.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/member-ordering */ /* eslint-disable @typescript-eslint/no-unused-vars */ +import {HashObject} from "@chainsafe/as-sha256"; import {isTypeOf, Type} from "../type"; export const BASIC_TYPE = Symbol.for("ssz/BasicType"); @@ -65,6 +66,8 @@ export abstract class BasicType extends Type { } abstract struct_deserializeFromBytes(data: Uint8Array, offset: number): T; + struct_deserializeFromHashObject?(data: HashObject, byteOffset: number): T; + struct_serializeToHashObject?(value: T, output: HashObject, byteOffset: number): number; struct_hashTreeRoot(value: T): Uint8Array { const output = new Uint8Array(32); diff --git a/src/types/basic/uint.ts b/src/types/basic/uint.ts index 2b3609f0..0e05af42 100644 --- a/src/types/basic/uint.ts +++ b/src/types/basic/uint.ts @@ -2,6 +2,7 @@ import {Json} from "../../interface"; import {bigIntPow} from "../../util/bigInt"; import {isTypeOf, Type} from "../type"; import {BasicType} from "./abstract"; +import {HashObject} from "@chainsafe/as-sha256"; export interface IUintOptions { byteLength: number; @@ -32,6 +33,7 @@ export abstract class UintType extends BasicType { } export const NUMBER_UINT_TYPE = Symbol.for("ssz/NumberUintType"); +export const NUMBER_64_UINT_TYPE = Symbol.for("ssz/Number64UintType"); const BIGINT_4_BYTES = BigInt(32); @@ -39,6 +41,10 @@ export function isNumberUintType(type: Type): type is NumberUintType { return isTypeOf(type, NUMBER_UINT_TYPE); } +export function isNumber64UintType(type: Type): type is Number64UintType { + return isTypeOf(type, NUMBER_64_UINT_TYPE); +} + export class NumberUintType extends UintType { _maxBigInt?: BigInt; @@ -128,6 +134,98 @@ export class NumberUintType extends UintType { } } +const TWO_POWER_32 = 2 ** 32; + +/** + * For 64 bit number, we want to operator on HashObject + * over bytes to improve performance. + */ +export class Number64UintType extends NumberUintType { + constructor() { + super({byteLength: 8}); + this._typeSymbols.add(NUMBER_64_UINT_TYPE); + } + + /** + * TODO: move this logic all the way to persistent-merkle-tree? + * That's save us 1 time to traverse the tree in the applyDelta scenario + */ + struct_deserializeFromHashObject(data: HashObject, byteOffset: number): number { + const numberOffset = Math.floor(byteOffset / 8); + // a chunk contains 4 items + if (numberOffset < 0 || numberOffset > 3) { + throw new Error(`Invalid numberOffset ${numberOffset}`); + } + let low32Number = 0; + let high32Number = 0; + + switch (numberOffset) { + case 0: + low32Number = data.h0 & 0xffffffff; + high32Number = data.h1 & 0xffffffff; + break; + case 1: + low32Number = data.h2 & 0xffffffff; + high32Number = data.h3 & 0xffffffff; + break; + case 2: + low32Number = data.h4 & 0xffffffff; + high32Number = data.h5 & 0xffffffff; + break; + case 3: + low32Number = data.h6 & 0xffffffff; + high32Number = data.h7 & 0xffffffff; + break; + default: + throw new Error(`Invalid offset ${numberOffset}`); + } + if (low32Number < 0) low32Number = low32Number >>> 0; + if (high32Number === 0) { + return low32Number; + } else if (high32Number < 0) { + high32Number = high32Number >>> 0; + } + if (low32Number === 0xffffffff && high32Number === 0xffffffff) { + return Infinity; + } + return high32Number * TWO_POWER_32 + low32Number; + } + + struct_serializeToHashObject(value: number, output: HashObject, byteOffset: number): number { + const numberOffset = Math.floor(byteOffset / 8); + let low32Number: number; + let high32Number: number; + if (value !== Infinity) { + low32Number = value & 0xffffffff; + high32Number = Math.floor(value / TWO_POWER_32) & 0xffffffff; + } else { + low32Number = 0xffffffff; + high32Number = 0xffffffff; + } + switch (numberOffset) { + case 0: + output.h0 = low32Number; + output.h1 = high32Number; + break; + case 1: + output.h2 = low32Number; + output.h3 = high32Number; + break; + case 2: + output.h4 = low32Number; + output.h5 = high32Number; + break; + case 3: + output.h6 = low32Number; + output.h7 = high32Number; + break; + default: + throw new Error(`Invalid offset ${numberOffset}`); + } + return numberOffset + 1; + } +} + export const BIGINT_UINT_TYPE = Symbol.for("ssz/BigIntUintType"); export function isBigIntUintType(type: Type): type is BigIntUintType { diff --git a/src/types/composite/container.ts b/src/types/composite/container.ts index 5684f627..f8d60b42 100644 --- a/src/types/composite/container.ts +++ b/src/types/composite/container.ts @@ -15,6 +15,8 @@ import {SszErrorPath} from "../../util/errorPath"; import {toExpectedCase} from "../../util/json"; import {isTreeBacked} from "../../backings/tree/treeValue"; import {basicTypeToLeafNode} from "../../util/basic"; +import {Number64UintType, NumberUintType} from "../basic"; +import {newHashObject} from "../../util/hash"; export interface IContainerOptions { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -457,12 +459,16 @@ export class ContainerType extends CompositeT if (!fieldInfo) { return undefined; } - const {isBasic, gindex} = fieldInfo; - if (isBasic) { - const chunk = target.getRoot(gindex); + if (fieldInfo.isBasic) { + // Number64Uint wants to work on HashObject to improve performance + if ((fieldType as NumberUintType).struct_deserializeFromHashObject) { + const hashObject = target.getHashObject(fieldInfo.gindex); + return (fieldType as Number64UintType).struct_deserializeFromHashObject(hashObject, 0); + } + const chunk = target.getRoot(fieldInfo.gindex); return fieldType.struct_deserializeFromBytes(chunk, 0); } else { - return target.getSubtree(gindex); + return target.getSubtree(fieldInfo.gindex); } } @@ -472,14 +478,20 @@ export class ContainerType extends CompositeT if (!fieldInfo) { throw new Error("Invalid container field name"); } - const {isBasic, gindex} = fieldInfo; - if (isBasic) { + if (fieldInfo.isBasic) { + // Number64Uint wants to work on HashObject to improve performance + if ((fieldType as Number64UintType).struct_serializeToHashObject) { + const hashObject = newHashObject(); + (fieldType as Number64UintType).struct_serializeToHashObject(value as number, hashObject, 0); + target.setHashObject(fieldInfo.gindex, hashObject); + return true; + } const chunk = new Uint8Array(32); fieldType.struct_serializeToBytes(value, chunk, 0); - target.setRoot(gindex, chunk); + target.setRoot(fieldInfo.gindex, chunk); return true; } else { - target.setSubtree(gindex, value as Tree); + target.setSubtree(fieldInfo.gindex, value as Tree); return true; } } diff --git a/src/types/composite/list.ts b/src/types/composite/list.ts index c5a603b1..d29ebb1a 100644 --- a/src/types/composite/list.ts +++ b/src/types/composite/list.ts @@ -1,9 +1,19 @@ import {Json, List} from "../../interface"; import {IArrayOptions, BasicArrayType, CompositeArrayType} from "./array"; -import {isBasicType, number32Type} from "../basic"; +import {isBasicType, isNumber64UintType, number32Type} from "../basic"; import {IJsonOptions, isTypeOf, Type} from "../type"; import {mixInLength} from "../../util/compat"; -import {BranchNode, concatGindices, Gindex, Node, Tree, zeroNode} from "@chainsafe/persistent-merkle-tree"; +import {cloneHashObject} from "../../util/hash"; +import { + BranchNode, + concatGindices, + Gindex, + LeafNode, + Node, + subtreeFillToContents, + Tree, + zeroNode, +} from "@chainsafe/persistent-merkle-tree"; import {isTreeBacked} from "../../backings/tree/treeValue"; /** @@ -39,7 +49,9 @@ export function isListType = List>(type: Type) export const ListType: ListTypeConstructor = // eslint-disable-next-line @typescript-eslint/no-explicit-any (function ListType = List>(options: IListOptions): ListType { - if (isBasicType(options.elementType)) { + if (isNumber64UintType(options.elementType)) { + return new Number64ListType(options); + } else if (isBasicType(options.elementType)) { return new BasicListType(options); } else { return new CompositeListType(options); @@ -220,6 +232,92 @@ export class BasicListType = List> extends Basi } } +/** For Number64UintType, it takes 64 / 8 = 8 bytes per item, each chunk has 32 bytes = 4 items */ +const NUMBER64_LIST_NUM_ITEMS_PER_CHUNK = 4; + +/** + * An optimization for Number64 using HashObject and new method to work with deltas. + */ +export class Number64ListType = List> extends BasicListType { + constructor(options: IListOptions) { + super(options); + } + + /** @override */ + tree_getValueAtIndex(target: Tree, index: number): number { + const chunkGindex = this.getGindexAtChunkIndex(this.getChunkIndex(index)); + const hashObject = target.getHashObject(chunkGindex); + // 4 items per chunk + const offsetInChunk = (index % 4) * 8; + return this.elementType.struct_deserializeFromHashObject!(hashObject, offsetInChunk) as number; + } + + /** @override */ + tree_setValueAtIndex(target: Tree, index: number, value: number, expand = false): boolean { + const chunkGindex = this.getGindexAtChunkIndex(this.getChunkIndex(index)); + const hashObject = cloneHashObject(target.getHashObject(chunkGindex)); + // 4 items per chunk + const offsetInChunk = (index % 4) * 8; + this.elementType.struct_serializeToHashObject!(value as number, hashObject, offsetInChunk); + target.setHashObject(chunkGindex, hashObject, expand); + return true; + } + + /** + * delta > 0 increments the underlying value, delta < 0 decrements the underlying value + * returns the new value + **/ + tree_applyDeltaAtIndex(target: Tree, index: number, delta: number): number { + const chunkGindex = this.getGindexAtChunkIndex(this.getChunkIndex(index)); + const hashObject = cloneHashObject(target.getHashObject(chunkGindex)); + // 4 items per chunk + const offsetInChunk = (index % 4) * 8; + + let value = this.elementType.struct_deserializeFromHashObject!(hashObject, offsetInChunk) as number; + value += delta; + if (value < 0) value = 0; + this.elementType.struct_serializeToHashObject!(value as number, hashObject, offsetInChunk); + target.setHashObject(chunkGindex, hashObject); + + return value; + } + + /** + * delta > 0 means an increasement, delta < 0 means a decreasement + * returns the new tree and new values + **/ + tree_newTreeFromDeltas(target: Tree, deltas: number[]): [Tree, number[]] { + if (deltas.length !== this.tree_getLength(target)) { + throw new Error(`Expect delta length ${this.tree_getLength(target)}, actual ${deltas.length}`); + } + const chunkDepth = this.getChunkDepth(); + const length = deltas.length; + let nodeIdx = 0; + const newLeafNodes: LeafNode[] = []; + const newValues: number[] = []; + const chunkCount = Math.ceil(length / NUMBER64_LIST_NUM_ITEMS_PER_CHUNK); + const currentNodes = target.getNodesAtDepth(chunkDepth, 0, chunkCount); + for (let i = 0; i < currentNodes.length; i++) { + const node = currentNodes[i]; + const hashObject = cloneHashObject(node); + for (let offset = 0; offset < NUMBER64_LIST_NUM_ITEMS_PER_CHUNK; offset++) { + const index = nodeIdx * NUMBER64_LIST_NUM_ITEMS_PER_CHUNK + offset; + if (index >= length) break; + let value = + (this.elementType.struct_deserializeFromHashObject!(hashObject, offset * 8) as number) + deltas[index]; + if (value < 0) value = 0; + newValues.push(value); + // mutate hashObject at offset + this.elementType.struct_serializeToHashObject!(value, hashObject, offset * 8); + } + newLeafNodes.push(new LeafNode(hashObject)); + nodeIdx++; + } + const newRootNode = subtreeFillToContents(newLeafNodes, chunkDepth); + return [new Tree(newRootNode), newValues]; + } +} + export class CompositeListType = List> extends CompositeArrayType { limit: number; diff --git a/src/util/hash.ts b/src/util/hash.ts index 5f289574..3b619245 100644 --- a/src/util/hash.ts +++ b/src/util/hash.ts @@ -1,5 +1,5 @@ /** @module ssz */ -import SHA256 from "@chainsafe/as-sha256"; +import SHA256, {HashObject} from "@chainsafe/as-sha256"; /** * Hash used for hashTreeRoot @@ -7,3 +7,55 @@ import SHA256 from "@chainsafe/as-sha256"; export function hash(...inputs: Uint8Array[]): Uint8Array { return SHA256.digest(Buffer.concat(inputs)); } + +/** + * A temporary HashObject is needed in a lot of places, this HashObject is then + * applied to persistent-merkle-tree, it'll make a copy so it's safe to mutate it after that. + * It means that we could use a shared HashObject instead of having to always allocate + * a new one to save memory. This temporary HashObject is always allocated by cloneHashObject() + * or newHashObject() below. + **/ +const sharedHashObject: HashObject = { + h0: 0, + h1: 0, + h2: 0, + h3: 0, + h4: 0, + h5: 0, + h6: 0, + h7: 0, +}; + +/** + * Clone a hash object using sharedHashObject, after doing this we usually + * apply HashObject to the Tree which make a copy there so it's safe to mutate + * this HashObject after that. + **/ +export function cloneHashObject(hashObject: HashObject): HashObject { + sharedHashObject.h0 = hashObject.h0; + sharedHashObject.h1 = hashObject.h1; + sharedHashObject.h2 = hashObject.h2; + sharedHashObject.h3 = hashObject.h3; + sharedHashObject.h4 = hashObject.h4; + sharedHashObject.h5 = hashObject.h5; + sharedHashObject.h6 = hashObject.h6; + sharedHashObject.h7 = hashObject.h7; + return sharedHashObject; +} + +/** + * Reset and return sharedHashObject, after doing this we usually + * apply HashObject to the Tree which make a copy there so it's safe to mutate + * this HashObject after that. + **/ +export function newHashObject(): HashObject { + sharedHashObject.h0 = 0; + sharedHashObject.h1 = 0; + sharedHashObject.h2 = 0; + sharedHashObject.h3 = 0; + sharedHashObject.h4 = 0; + sharedHashObject.h5 = 0; + sharedHashObject.h6 = 0; + sharedHashObject.h7 = 0; + return sharedHashObject; +} diff --git a/test/perf/list.test.ts b/test/perf/list.test.ts index 3ebcf5e6..41999cfa 100644 --- a/test/perf/list.test.ts +++ b/test/perf/list.test.ts @@ -1,5 +1,6 @@ +import {LeafNode, subtreeFillToContents, Node} from "@chainsafe/persistent-merkle-tree"; import {itBench, setBenchOpts} from "@dapplion/benchmark"; -import {List, ListType, NumberUintType} from "../../src"; +import {List, ListType, Number64ListType, Number64UintType, NumberUintType, TreeBacked, Type} from "../../src"; describe("list", () => { setBenchOpts({ @@ -9,30 +10,118 @@ describe("list", () => { }); const numBalances = 250_000; - const tbBalances = createBalanceList(numBalances); - // access balances list 1.296884 ops/s 771.0793 ms/op - 38 runs 30.1 s - - itBench("get balances list", () => { + const tbBalances = createBalanceList(numBalances, new NumberUintType({byteLength: 8})); + const tbBalances64 = createBalanceList(numBalances, new Number64UintType()); + // access balances list 1.296884 ops/s 771.0793 ms/op - 38 runs 30.1 s + itBench("NumberUintType - get balances list", () => { for (let i = 0; i < numBalances; i++) { tbBalances[i]; } }); - itBench("set balances list", () => { + // using Number64UintType gives 20% improvement + itBench("Number64UintType - get balances list", () => { + for (let i = 0; i < numBalances; i++) { + tbBalances64[i]; + } + }); + + itBench("NumberUintType - set balances list", () => { for (let i = 0; i < numBalances; i++) { tbBalances[i] = 31217089836; } }); + + // using Number64UintType gives 2% - 10% improvement + itBench("Number64UintType - set balances list", () => { + for (let i = 0; i < numBalances; i++) { + tbBalances64[i] = 31217089836; + } + }); + + itBench("Number64UintType - get and increase 10 then set", () => { + const tbBalance = tbBalances64.clone(); + for (let i = 0; i < numBalances; i++) { + tbBalance[i] += 10; + } + }); + + // using applyDelta gives 70% - 100% improvement + itBench("Number64UintType - increase 10 using applyDelta", () => { + const basicArrayType = tbBalances64.type as Number64ListType; + const tree = tbBalances64.tree.clone(); + for (let i = 0; i < numBalances; i++) { + basicArrayType.tree_applyDeltaAtIndex(tree, i, 10); + } + }); }); -function createBalanceList(count: number): List { +describe("subtreeFillToContents", function () { + setBenchOpts({ + maxMs: 60 * 1000, + minMs: 40 * 1000, + runs: 500, + }); + + const numBalances = 250_000; + + const tbBalances64 = createBalanceList(numBalances, new Number64UintType()); + const delta = 100; + const deltas = Array.from({length: numBalances}, () => delta); + const tree = tbBalances64.tree; + const type = tbBalances64.type as Number64ListType; + + /** tree_newTreeFromUint64Deltas is 17% faster than unsafeUint8ArrayToTree */ + /** ✓ tree_newTreeFromUint64Deltas 28.72705 ops/s 34.81040 ms/op - 1149 runs 40.0 s */ + itBench("tree_newTreeFromUint64Deltas", () => { + type.tree_newTreeFromDeltas(tree, deltas); + }); + + const newBalances = new BigUint64Array(numBalances); + const cachedBalances64: number[] = []; + for (let i = 0; i < tbBalances64.length; i++) { + cachedBalances64.push(tbBalances64[i]); + } + /** ✓ unsafeUint8ArrayToTree 24.51560 ops/s 40.79035 ms/op - 981 runs 40.0 s */ + itBench("unsafeUint8ArrayToTree", () => { + for (let i = 0; i < numBalances; i++) { + newBalances[i] = BigInt(cachedBalances64[i] + deltas[i]); + } + unsafeUint8ArrayToTree( + new Uint8Array(newBalances.buffer, newBalances.byteOffset, newBalances.byteLength), + type.getChunkDepth() + ); + }); +}); + +function createBalanceList(count: number, elementType: Type): TreeBacked> { const VALIDATOR_REGISTRY_LIMIT = 1099511627776; const balancesList = new ListType({ - elementType: new NumberUintType({byteLength: 8}), + elementType, limit: VALIDATOR_REGISTRY_LIMIT, }); const balancesStruct = Array.from({length: count}, () => 31217089836); return balancesList.createTreeBackedFromStruct(balancesStruct); } + +function unsafeUint8ArrayToTree(data: Uint8Array, depth: number): Node { + const leaves: LeafNode[] = []; + + // Loop 32 bytes at a time, creating leaves from the backing subarray + const maxStartIndex = data.length - 31; + for (let i = 0; i < maxStartIndex; i += 32) { + leaves.push(new LeafNode(data.subarray(i, i + 32))); + } + + // If there is any extra data at the end (less than 32 bytes), append a final leaf + const lengthMod32 = data.length % 32; + if (lengthMod32 !== 0) { + const finalChunk = new Uint8Array(32); + finalChunk.set(data.subarray(data.length - lengthMod32)); + leaves.push(new LeafNode(finalChunk)); + } + + return subtreeFillToContents(leaves, depth); +} diff --git a/test/perf/uint.test.ts b/test/perf/uint.test.ts index 79ca2152..27467925 100644 --- a/test/perf/uint.test.ts +++ b/test/perf/uint.test.ts @@ -1,6 +1,6 @@ import {itBench, setBenchOpts} from "@dapplion/benchmark"; import {expect} from "chai"; -import {ContainerType, NumberUintType} from "../../src"; +import {ContainerType, Number64UintType, NumberUintType} from "../../src"; describe("Uint64 types", () => { setBenchOpts({ @@ -14,6 +14,13 @@ describe("Uint64 types", () => { slot: new NumberUintType({byteLength: 8}), }, }); + + const BeaconState2 = new ContainerType({ + fields: { + slot: new Number64UintType(), + }, + }); + type IBeaconState = { slot: number; }; @@ -36,4 +43,12 @@ describe("Uint64 types", () => { } expect(tbState.slot).to.be.equal(numLoop); }); + + itBench(`Number64UintType - increase slot to ${numLoop}`, () => { + const tbState = BeaconState2.createTreeBackedFromStruct({slot: 0}); + for (let i = 0; i < numLoop; i++) { + tbState.slot++; + } + expect(tbState.slot).to.be.equal(numLoop); + }); }); diff --git a/test/unit/deserialize.test.ts b/test/unit/deserialize.test.ts index 1cf0db54..f930e77d 100644 --- a/test/unit/deserialize.test.ts +++ b/test/unit/deserialize.test.ts @@ -19,6 +19,7 @@ import { SimpleObject, VariableSizeSimpleObject, UnionObject, + number64Type2, } from "./objects"; describe("deserialize", () => { @@ -58,6 +59,9 @@ describe("deserialize", () => { {value: "0000000001000000", type: number64Type, expected: 2 ** 32}, {value: "ffffffffffff0f00", type: number64Type, expected: 2 ** 52 - 1}, {value: "ffffffffffffffff", type: number64Type, expected: Infinity}, + {value: "0000000001000000", type: number64Type2, expected: 2 ** 32}, + {value: "ffffffffffff0f00", type: number64Type2, expected: 2 ** 52 - 1}, + {value: "ffffffffffffffff", type: number64Type2, expected: Infinity}, {value: "deadbeefdeadbeef", type: bytes8Type, expected: Buffer.from("deadbeefdeadbeef", "hex")}, { value: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", diff --git a/test/unit/objects.ts b/test/unit/objects.ts index c088e0e2..5b7cefc9 100644 --- a/test/unit/objects.ts +++ b/test/unit/objects.ts @@ -10,6 +10,7 @@ import { ListType, VectorType, BooleanType, + Number64UintType, } from "../../src"; import {NoneType} from "../../src/types/basic/none"; import {UnionType} from "../../src/types/composite/union"; @@ -50,6 +51,8 @@ export const number32Type = new NumberUintType({byteLength: 4}); export const number64Type = new NumberUintType({byteLength: 8}); +export const number64Type2 = new Number64UintType(); + export const number16Vector6Type = new VectorType({ elementType: number16Type, length: 6, @@ -123,3 +126,4 @@ export const ArrayObject2 = new ListType({ }); export const UnionObject = new UnionType({types: [new NoneType(), SimpleObject, number16Type]}); +export const balancesType = new ListType({elementType: number64Type, limit: 100}); diff --git a/test/unit/serialize.test.ts b/test/unit/serialize.test.ts index b603f3ce..78b02d19 100644 --- a/test/unit/serialize.test.ts +++ b/test/unit/serialize.test.ts @@ -15,6 +15,7 @@ import { OuterObject, SimpleObject, UnionObject, + number64Type2, } from "./objects"; describe("serialize", () => { @@ -41,6 +42,11 @@ describe("serialize", () => { {value: 2 ** 32, type: number64Type, expected: "0000000001000000"}, {value: 2 ** 52 - 1, type: number64Type, expected: "ffffffffffff0f00"}, {value: Infinity, type: number64Type, expected: "ffffffffffffffff"}, + {value: 2 ** 32, type: number64Type2, expected: "0000000001000000"}, + {value: 2 ** 52 - 1, type: number64Type2, expected: "ffffffffffff0f00"}, + {value: 2 ** 32, type: number64Type2, expected: "0000000001000000"}, + {value: 2 ** 52 - 1, type: number64Type2, expected: "ffffffffffff0f00"}, + {value: Infinity, type: number64Type2, expected: "ffffffffffffffff"}, {value: 0x01n, type: bigint64Type, expected: "0100000000000000"}, {value: 0x1000000000000000n, type: bigint64Type, expected: "0000000000000010"}, {value: 0xffffffffffffffffn, type: bigint64Type, expected: "ffffffffffffffff"}, diff --git a/test/unit/tree.test.ts b/test/unit/tree.test.ts index 4307596a..1b5028be 100644 --- a/test/unit/tree.test.ts +++ b/test/unit/tree.test.ts @@ -1,5 +1,15 @@ import {expect} from "chai"; -import {byteType} from "../../src"; +import { + BasicListType, + byteType, + ContainerType, + List, + ListType, + Number64ListType, + Number64UintType, + NumberUintType, + toHexString, +} from "../../src"; import {number16List100Type, VariableSizeSimpleObject, number16Type} from "./objects"; @@ -38,4 +48,92 @@ describe("tree simple list/vector", () => { const tree2 = VariableSizeSimpleObject.struct_convertToTree({a, b, list: list2}); expect(tree1.root).to.be.deep.equal(tree2.root); }); + + it("Container - Number64UintType vs NumberUintType", () => { + const BeaconStateType = new ContainerType({ + fields: { + slot: new NumberUintType({byteLength: 8}), + }, + }); + const BeaconState64Type = new ContainerType({ + fields: { + slot: new Number64UintType(), + }, + }); + type BeaconState = { + slot: number; + }; + const state: BeaconState = {slot: 0}; + const tbState64 = BeaconState64Type.createTreeBackedFromStruct(state); + tbState64.tree.setHashObject(BigInt(1), {h0: -1, h1: -1, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0, h7: 0}); + const tbState = BeaconStateType.createTreeBackedFromStruct(state); + tbState.tree.setHashObject(BigInt(1), {h0: -1, h1: -1, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0, h7: 0}); + expect(tbState.slot).to.be.equal(tbState64.slot); + tbState64.slot = 31217089836; + expect(tbState64.slot).to.be.equal(31217089836); + tbState.slot = 31217089836; + expect(tbState.slot).to.be.equal(31217089836); + expect(toHexString(tbState.hashTreeRoot())).to.be.equal(toHexString(tbState64.hashTreeRoot())); + }); + + it("BasicList - Number64UintType vs NumberUintType", () => { + const BalancesList64 = new ListType({elementType: new Number64UintType(), limit: 1000}); + const BalancesList = new ListType({elementType: new NumberUintType({byteLength: 8}), limit: 1000}); + const length = 200; + const struct = Array.from({length}, () => 99); + const tbBalancesList = BalancesList.createTreeBackedFromStruct(struct); + const tbBalancesList64 = BalancesList64.createTreeBackedFromStruct(struct); + expect(toHexString(tbBalancesList.tree.root)).to.be.equal(toHexString(tbBalancesList64.tree.root)); + // setter + tbBalancesList[100] = 31217089836; + // getter + expect(tbBalancesList[100]).to.be.equal(31217089836); + // setter + tbBalancesList64[100] = 31217089836; + // getter + expect(tbBalancesList64[100]).to.be.equal(31217089836); + const deltas = [1_000_000_000_000, 999, 0, -1_000_000]; + for (const delta of deltas) { + tbBalancesList64[100] = 31217089836; + const newBalance = (tbBalancesList64.type as Number64ListType).tree_applyDeltaAtIndex( + tbBalancesList64.tree, + 100, + delta + ); + expect(newBalance).to.be.equal(31217089836 + delta); + expect(tbBalancesList64[100]).to.be.equal(31217089836 + delta); + } + }); + + it("tree_newTreeFromUint64Deltas", () => { + const lengths = [200, 201, 202, 203]; + for (const length of lengths) { + const BalancesList64 = new ListType({elementType: new Number64UintType(), limit: 1000}); + const struct = Array.from({length}, () => 31217089836); + const tbBalancesList64 = BalancesList64.createTreeBackedFromStruct(struct); + const delta = 100; + const deltas = struct.map(() => delta); + const [newTree, newValues] = (tbBalancesList64.type as Number64ListType).tree_newTreeFromDeltas( + tbBalancesList64.tree, + deltas + ); + for (let i = 0; i < length; i++) { + expect(newValues[i]).to.be.equal(31217089836 + delta); + } + (tbBalancesList64.type as BasicListType>).tree_setLength(newTree, length); + const newTBalancesList64 = BalancesList64.createTreeBacked(newTree); + for (let i = 0; i < length; i++) { + expect(newTBalancesList64[i]).to.be.equal(31217089836 + delta); + } + // build a tree from scratch with deltas to confirm + const expectedStruct = Array.from({length}, () => 31217089836 + delta); + // Number64UintType + const expectedTBBalancesList64 = BalancesList64.createTreeBackedFromStruct(expectedStruct); + expect(toHexString(newTree.root)).to.be.equal(toHexString(expectedTBBalancesList64.tree.root)); + // NumberUintType + const BalancesList = new ListType({elementType: new NumberUintType({byteLength: 8}), limit: 1000}); + const expectedTBBalancesList = BalancesList.createTreeBackedFromStruct(expectedStruct); + expect(toHexString(newTree.root)).to.be.equal(toHexString(expectedTBBalancesList.tree.root)); + } + }); });