From 78a029128878685248806ec71100fcedf3cb77ec Mon Sep 17 00:00:00 2001 From: Cayman Date: Tue, 8 Oct 2024 22:27:56 -0400 Subject: [PATCH] feat: add StableContainer (#373) * feat: add StableContainer * fix: fix stablecontainer with variable fields * feat: add SimpleVariantType * chore: add `yarn pack` tarball for testing * chore: rename variant to profile * chore: refresh `yarn pack` tarball * chore: fix lint * fix: Shape3 StableContainerType unit tests * fix: compilation error due to zerohash * chore: extract BasicContainerTreeViewDU from ContainerTreeViewDU * fix: support StableContainerTreeViewDU.batchHashTreeRoot() * fix: more StableContainer tests * fix: more tests for Profile and fix bugs * chore: StableContainer vs Profile merkleization test * fix: StableContainer to pad false bits up to length N (BitVector[N]) * fix: StableContainer BitVector[N] for view + value * chore: refactor Optional utils to common place * feat: Profile to support OptionalType * chore: StableContainer BitVector[N] test * chore: cachePermanentRootStruct for Profile and naming refactor --------- Co-authored-by: Tuyen Nguyen --- packages/ssz/package.tgz | Bin 0 -> 133591 bytes packages/ssz/src/index.ts | 2 + packages/ssz/src/type/optional.ts | 13 + packages/ssz/src/type/profile.ts | 670 ++++++++++++++ packages/ssz/src/type/stableContainer.ts | 829 ++++++++++++++++++ packages/ssz/src/view/container.ts | 10 +- packages/ssz/src/view/profile.ts | 219 +++++ packages/ssz/src/view/stableContainer.ts | 247 ++++++ packages/ssz/src/viewDU/container.ts | 31 +- packages/ssz/src/viewDU/profile.ts | 252 ++++++ packages/ssz/src/viewDU/stableContainer.ts | 257 ++++++ .../ssz/test/unit/byType/profile/tree.test.ts | 743 ++++++++++++++++ .../test/unit/byType/profile/valid.test.ts | 49 ++ .../ssz/test/unit/byType/runTypeProofTest.ts | 29 +- .../unit/byType/stableContainer/tree.test.ts | 723 +++++++++++++++ .../unit/byType/stableContainer/valid.test.ts | 132 +++ 16 files changed, 4188 insertions(+), 18 deletions(-) create mode 100644 packages/ssz/package.tgz create mode 100644 packages/ssz/src/type/profile.ts create mode 100644 packages/ssz/src/type/stableContainer.ts create mode 100644 packages/ssz/src/view/profile.ts create mode 100644 packages/ssz/src/view/stableContainer.ts create mode 100644 packages/ssz/src/viewDU/profile.ts create mode 100644 packages/ssz/src/viewDU/stableContainer.ts create mode 100644 packages/ssz/test/unit/byType/profile/tree.test.ts create mode 100644 packages/ssz/test/unit/byType/profile/valid.test.ts create mode 100644 packages/ssz/test/unit/byType/stableContainer/tree.test.ts create mode 100644 packages/ssz/test/unit/byType/stableContainer/valid.test.ts diff --git a/packages/ssz/package.tgz b/packages/ssz/package.tgz new file mode 100644 index 0000000000000000000000000000000000000000..0cc5135df13a3912d73e2b980de5c09512218503 GIT binary patch literal 133591 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

D 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 literal 0 HcmV?d00001 diff --git a/packages/ssz/src/index.ts b/packages/ssz/src/index.ts index a59f2e1c..ab3330c6 100644 --- a/packages/ssz/src/index.ts +++ b/packages/ssz/src/index.ts @@ -16,6 +16,8 @@ export {OptionalType} from "./type/optional"; export {VectorBasicType} from "./type/vectorBasic"; export {VectorCompositeType} from "./type/vectorComposite"; export {ListUintNum64Type} from "./type/listUintNum64"; +export {StableContainerType} from "./type/stableContainer"; +export {ProfileType} from "./type/profile"; // Base types export {ArrayType} from "./type/array"; diff --git a/packages/ssz/src/type/optional.ts b/packages/ssz/src/type/optional.ts index 5ae7e2bb..7c5f9baf 100644 --- a/packages/ssz/src/type/optional.ts +++ b/packages/ssz/src/type/optional.ts @@ -15,6 +15,11 @@ import {CompositeType, isCompositeType} from "./composite"; import {addLengthNode, getLengthFromRootNode} from "./arrayBasic"; /* eslint-disable @typescript-eslint/member-ordering */ +export type NonOptionalType> = T extends OptionalType ? U : T; +export type NonOptionalFields>> = { + [K in keyof Fields]: NonOptionalType; +}; + export type OptionalOpts = { typeName?: string; }; @@ -258,3 +263,11 @@ export class OptionalType> extends CompositeTy return this.elementType.equals(a, b); } } + +export function isOptionalType(type: Type): type is OptionalType> { + return type instanceof OptionalType; +} + +export function toNonOptionalType>(type: T): NonOptionalType { + return (isOptionalType(type) ? type.elementType : type) as NonOptionalType; +} diff --git a/packages/ssz/src/type/profile.ts b/packages/ssz/src/type/profile.ts new file mode 100644 index 00000000..f9469fe0 --- /dev/null +++ b/packages/ssz/src/type/profile.ts @@ -0,0 +1,670 @@ +import { + Node, + getNodesAtDepth, + subtreeFillToContents, + Tree, + Gindex, + toGindex, + concatGindices, + getNode, + BranchNode, + zeroHash, + zeroNode, +} from "@chainsafe/persistent-merkle-tree"; +import {ValueWithCachedPermanentRoot, maxChunksToDepth, symbolCachedPermanentRoot} from "../util/merkleize"; +import {Require} from "../util/types"; +import {namedClass} from "../util/named"; +import {Type, ValueOf} from "./abstract"; +import {CompositeType, ByteViews, CompositeTypeAny} from "./composite"; +import { + getProfileTreeViewClass, + ValueOfFields, + FieldEntry, + ContainerTreeViewType, + ContainerTreeViewTypeConstructor, + computeSerdesData, +} from "../view/profile"; +import { + getProfileTreeViewDUClass, + ContainerTreeViewDUType, + ContainerTreeViewDUTypeConstructor, +} from "../viewDU/profile"; +import {Case} from "../util/strings"; +import {BitArray} from "../value/bitArray"; +import {mixInActiveFields, setActiveFields} from "./stableContainer"; +import {NonOptionalFields, isOptionalType, toNonOptionalType} from "./optional"; +/* eslint-disable @typescript-eslint/member-ordering */ + +type BytesRange = {start: number; end: number}; + +export type ProfileOptions> = { + typeName?: string; + jsonCase?: KeyCase; + casingMap?: CasingMap; + cachePermanentRootStruct?: boolean; + getProfileTreeViewClass?: typeof getProfileTreeViewClass; + getProfileTreeViewDUClass?: typeof getProfileTreeViewDUClass; +}; + +export type KeyCase = + | "eth2" + | "snake" + | "constant" + | "camel" + | "header" + //Same as squish + | "pascal"; + +type CasingMap> = Partial<{[K in keyof Fields]: string}>; + +/** + * Profile: ordered heterogeneous collection of values that inherits merkleization from a base stable container + * - EIP: https://eips.ethereum.org/EIPS/eip-7495 + * - No reordering of fields for merkleization + */ +export class ProfileType>> extends CompositeType< + ValueOfFields, + ContainerTreeViewType, + ContainerTreeViewDUType +> { + readonly typeName: string; + readonly depth: number; + readonly maxChunkCount: number; + readonly fixedSize: number | null; + readonly minSize: number; + readonly maxSize: number; + readonly isList = false; + readonly isViewMutable = true; + readonly activeFields: BitArray; + + // Precomputed data for faster serdes + readonly fieldsEntries: FieldEntry>[]; + /** End of fixed section of serialized Container */ + protected readonly fieldsGindex: Record; + protected readonly jsonKeyToFieldName: Record; + + /** Cached TreeView constuctor with custom prototype for this Type's properties */ + protected readonly TreeView: ContainerTreeViewTypeConstructor; + protected readonly TreeViewDU: ContainerTreeViewDUTypeConstructor; + private optionalFieldsCount: number; + + constructor(readonly fields: Fields, activeFields: BitArray, readonly opts?: ProfileOptions) { + super(); + + // Render detailed typeName. Consumers should overwrite since it can get long + this.typeName = opts?.typeName ?? renderContainerTypeName(fields); + + if (activeFields.getTrueBitIndexes().length !== Object.keys(fields).length) { + throw new Error("activeFields must have the same number of true bits as fields"); + } + + this.activeFields = activeFields; + this.maxChunkCount = this.activeFields.bitLen; + this.depth = maxChunksToDepth(this.maxChunkCount) + 1; + + // Precalculated data for faster serdes + this.fieldsEntries = []; + const fieldNames = Object.keys(fields) as (keyof Fields)[]; + this.optionalFieldsCount = 0; + for (let i = 0, fieldIx = 0; i < this.activeFields.bitLen; i++) { + if (!this.activeFields.get(i)) { + continue; + } + + const fieldName = fieldNames[fieldIx++]; + const fieldType = fields[fieldName]; + const optional = isOptionalType(fieldType); + this.fieldsEntries.push({ + fieldName, + fieldType: toNonOptionalType(fieldType), + jsonKey: precomputeJsonKey(fieldName, opts?.casingMap, opts?.jsonCase), + gindex: toGindex(this.depth, BigInt(i)), + chunkIndex: i, + optional, + }); + + if (optional) { + this.optionalFieldsCount++; + } + } + + if (this.fieldsEntries.length === 0) { + throw Error("Container must have > 0 fields"); + } + + // Precalculate for Proofs API + this.fieldsGindex = {} as Record; + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, chunkIndex} = this.fieldsEntries[i]; + this.fieldsGindex[fieldName] = toGindex(this.depth, BigInt(chunkIndex)); + } + + // To resolve JSON paths in fieldName notation and jsonKey notation + this.jsonKeyToFieldName = {}; + for (const {fieldName, jsonKey} of this.fieldsEntries) { + this.jsonKeyToFieldName[jsonKey] = fieldName; + } + + const {minLen, maxLen, fixedSize} = precomputeSizes(fields); + this.minSize = minLen; + this.maxSize = maxLen; + this.fixedSize = fixedSize; + + // TODO: This options are necessary for ContainerNodeStruct to override this. + // Refactor this constructor to allow customization without pollutin the options + this.TreeView = opts?.getProfileTreeViewClass?.(this) ?? getProfileTreeViewClass(this); + this.TreeViewDU = opts?.getProfileTreeViewDUClass?.(this) ?? getProfileTreeViewDUClass(this); + } + + static named>>( + fields: Fields, + activeFields: BitArray, + opts: Require, "typeName"> + ): ProfileType { + return new (namedClass(ProfileType, opts.typeName))(fields, activeFields, opts); + } + + defaultValue(): ValueOfFields { + const value = {} as ValueOfFields; + for (const {fieldName, fieldType, optional} of this.fieldsEntries) { + value[fieldName] = (optional ? null : fieldType.defaultValue()) as ValueOf; + } + return value; + } + + getView(tree: Tree): ContainerTreeViewType { + return new this.TreeView(this, tree); + } + + getViewDU(node: Node, cache?: unknown): ContainerTreeViewDUType { + return new this.TreeViewDU(this, node, cache); + } + + cacheOfViewDU(view: ContainerTreeViewDUType): unknown { + return view.cache; + } + + commitView(view: ContainerTreeViewType): Node { + return view.node; + } + + commitViewDU(view: ContainerTreeViewDUType): Node { + view.commit(); + return view.node; + } + + // Serialization + deserialization + // ------------------------------- + // Containers can mix fixed length and variable length data. + // + // Fixed part Variable part + // [field1 offset][field2 data ][field1 data ] + // [0x000000c] [0xaabbaabbaabbaabb][0xffffffffffffffffffffffff] + + value_serializedSize(value: ValueOfFields): number { + let totalSize = Math.ceil(this.optionalFieldsCount / 8); + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType, optional} = this.fieldsEntries[i]; + if (optional && value[fieldName] == null) { + continue; + } + // Offset (4 bytes) + size + totalSize += + fieldType.fixedSize === null ? 4 + fieldType.value_serializedSize(value[fieldName]) : fieldType.fixedSize; + } + return totalSize; + } + + value_serializeToBytes(output: ByteViews, offset: number, value: ValueOfFields): number { + const optionalFields = BitArray.fromBitLen(this.optionalFieldsCount); + let optionalIndex = 0; + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, optional} = this.fieldsEntries[i]; + if (optional) { + optionalFields.set(optionalIndex++, value[fieldName] !== null); + } + } + + output.uint8Array.set(optionalFields.uint8Array, offset); + + const {fixedEnd} = computeSerdesData(optionalFields, this.fieldsEntries); + + const optionalFieldsLen = optionalFields.uint8Array.length; + let fixedIndex = offset + optionalFieldsLen; + let variableIndex = offset + fixedEnd + optionalFieldsLen; + + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType, optional} = this.fieldsEntries[i]; + // skip optional fields with nullish values + if (optional && value[fieldName] == null) { + continue; + } + + if (fieldType.fixedSize === null) { + // write offset relative to the start of serialized active fields, after the Bitvector[N] + output.dataView.setUint32(fixedIndex, variableIndex - offset - optionalFieldsLen, true); + fixedIndex += 4; + // write serialized element to variable section + variableIndex = fieldType.value_serializeToBytes(output, variableIndex, value[fieldName]); + } else { + fixedIndex = fieldType.value_serializeToBytes(output, fixedIndex, value[fieldName]); + } + } + return variableIndex; + } + + value_deserializeFromBytes(data: ByteViews, start: number, end: number): ValueOfFields { + const {optionalFields, fieldRanges} = this.getFieldRanges(data, start, end); + const value = {} as {[K in keyof Fields]: unknown}; + const optionalFieldsLen = optionalFields.uint8Array.length; + start += optionalFieldsLen; + + let optionalIndex = 0; + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType, optional} = this.fieldsEntries[i]; + if (optional && !optionalFields.get(optionalIndex++)) { + value[fieldName] = null; + continue; + } + const fieldRange = fieldRanges[i]; + value[fieldName] = fieldType.value_deserializeFromBytes(data, start + fieldRange.start, start + fieldRange.end); + } + + return value as ValueOfFields; + } + + tree_serializedSize(node: Node): number { + let totalSize = Math.ceil(this.optionalFieldsCount / 8); + const nodes = getNodesAtDepth(node, this.depth, 0, this.activeFields.bitLen) as Node[]; + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldType, chunkIndex, optional} = this.fieldsEntries[i]; + const node = nodes[chunkIndex]; + // zeroNode() means optional field is null, it's different from a node with all zeros + if (optional && node === zeroNode(0)) { + continue; + } + // Offset (4 bytes) + size + totalSize += fieldType.fixedSize === null ? 4 + fieldType.tree_serializedSize(node) : fieldType.fixedSize; + } + return totalSize; + } + + tree_serializeToBytes(output: ByteViews, offset: number, node: Node): number { + const optionalFields = BitArray.fromBitLen(this.optionalFieldsCount); + const optionalFieldsLen = optionalFields.uint8Array.length; + + const nodes = getNodesAtDepth(node, this.depth, 0, this.activeFields.bitLen); + let optionalIndex = -1; + if (this.optionalFieldsCount > 0) { + // 1st loop to compute optional fields + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {chunkIndex, optional} = this.fieldsEntries[i]; + const node = nodes[chunkIndex]; + if (optional) { + optionalIndex++; + if (node !== zeroNode(0)) { + optionalFields.set(optionalIndex, true); + } + } + } + } + + output.uint8Array.set(optionalFields.uint8Array, offset); + + const {fixedEnd} = computeSerdesData(optionalFields, this.fieldsEntries); + let fixedIndex = offset + optionalFieldsLen; + let variableIndex = offset + fixedEnd + optionalFieldsLen; + + // 2nd loop to serialize fields + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldType, chunkIndex, optional} = this.fieldsEntries[i]; + const node = nodes[chunkIndex]; + if (optional && node === zeroNode(0)) { + continue; + } + + if (fieldType.fixedSize === null) { + // write offset relative to the start of serialized active fields, after the Bitvector[N] + output.dataView.setUint32(fixedIndex, variableIndex - offset - optionalFieldsLen, true); + fixedIndex += 4; + // write serialized element to variable section + variableIndex = fieldType.tree_serializeToBytes(output, variableIndex, node); + } else { + fixedIndex = fieldType.tree_serializeToBytes(output, fixedIndex, node); + } + } + return variableIndex; + } + + tree_deserializeFromBytes(data: ByteViews, start: number, end: number): Node { + const {optionalFields, fieldRanges} = this.getFieldRanges(data, start, end); + const nodes = new Array(this.activeFields.bitLen).fill(zeroNode(0)); + const optionalFieldsLen = optionalFields.uint8Array.length; + start += optionalFieldsLen; + + let optionalIndex = -1; + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldType, chunkIndex, optional} = this.fieldsEntries[i]; + if (optional) { + optionalIndex++; + if (!optionalFields.get(optionalIndex)) { + continue; + } + } + + const fieldRange = fieldRanges[i]; + nodes[chunkIndex] = fieldType.tree_deserializeFromBytes(data, start + fieldRange.start, start + fieldRange.end); + } + + const root = new BranchNode(subtreeFillToContents(nodes, this.depth - 1), zeroNode(0)); + return setActiveFields(root, this.activeFields); + } + + // Merkleization + hashTreeRoot(value: ValueOfFields): Uint8Array { + // Return cached mutable root if any + if (this.cachePermanentRootStruct) { + const cachedRoot = (value as ValueWithCachedPermanentRoot)[symbolCachedPermanentRoot]; + if (cachedRoot) { + return cachedRoot; + } + } + + const root = mixInActiveFields(super.hashTreeRoot(value), this.activeFields); + + if (this.cachePermanentRootStruct) { + (value as ValueWithCachedPermanentRoot)[symbolCachedPermanentRoot] = root; + } + + return root; + } + + protected getRoots(struct: ValueOfFields): Uint8Array[] { + const roots = new Array(this.activeFields.bitLen).fill(zeroHash(0)); + + // already asserted that # of active fields in bitvector === # of fields + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType, chunkIndex, optional} = this.fieldsEntries[i]; + if (optional && struct[fieldName] == null) { + continue; + } + roots[chunkIndex] = fieldType.hashTreeRoot(struct[fieldName]); + } + + return roots; + } + + // Proofs + + /** INTERNAL METHOD: For view's API, create proof from a tree */ + + getPropertyGindex(prop: string): Gindex | null { + const gindex = this.fieldsGindex[prop] ?? this.fieldsGindex[this.jsonKeyToFieldName[prop]]; + if (gindex === undefined) throw Error(`Unknown container property ${prop}`); + return gindex; + } + + getPropertyType(prop: string): Type { + const type = this.fields[prop] ?? this.fields[this.jsonKeyToFieldName[prop]]; + if (type === undefined) throw Error(`Unknown container property ${prop}`); + return type; + } + + getIndexProperty(index: number): string | null { + if (index >= this.fieldsEntries.length) { + return null; + } + return this.fieldsEntries[index].fieldName as string; + } + + tree_getLeafGindices(rootGindex: Gindex, rootNode?: Node): Gindex[] { + const gindices: Gindex[] = []; + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType} = this.fieldsEntries[i]; + const fieldGindex = this.fieldsGindex[fieldName]; + const fieldGindexFromRoot = concatGindices([rootGindex, fieldGindex]); + + if (fieldType.isBasic) { + gindices.push(fieldGindexFromRoot); + } else { + const compositeType = fieldType as unknown as CompositeTypeAny; + if (fieldType.fixedSize === null) { + if (!rootNode) { + throw new Error("variable type requires tree argument to get leaves"); + } + gindices.push(...compositeType.tree_getLeafGindices(fieldGindexFromRoot, getNode(rootNode, fieldGindex))); + } else { + gindices.push(...compositeType.tree_getLeafGindices(fieldGindexFromRoot)); + } + } + } + + return gindices; + } + + // JSON + + fromJson(json: unknown): ValueOfFields { + if (typeof json !== "object") { + throw Error("JSON must be of type object"); + } + if (json === null) { + throw Error("JSON must not be null"); + } + + const value = {} as ValueOfFields; + + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType, jsonKey} = this.fieldsEntries[i]; + const jsonValue = (json as Record)[jsonKey]; + if (jsonValue === undefined) { + throw Error(`JSON expected key ${jsonKey} is undefined`); + } + value[fieldName] = fieldType.fromJson(jsonValue) as ValueOf; + } + + return value; + } + + toJson(value: ValueOfFields): Record { + const json: Record = {}; + + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType, jsonKey} = this.fieldsEntries[i]; + json[jsonKey] = fieldType.toJson(value[fieldName]); + } + + return json; + } + + clone(value: ValueOfFields): ValueOfFields { + const newValue = {} as ValueOfFields; + + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType} = this.fieldsEntries[i]; + newValue[fieldName] = fieldType.clone(value[fieldName]) as ValueOf; + } + + return newValue; + } + + equals(a: ValueOfFields, b: ValueOfFields): boolean { + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType} = this.fieldsEntries[i]; + if (!fieldType.equals(a[fieldName], b[fieldName])) { + return false; + } + } + + return true; + } + + /** + * Deserializer helper: Returns the bytes ranges of all fields, both variable and fixed size. + * Fields may not be contiguous in the serialized bytes, so the returned ranges are [start, end]. + * - For fixed size fields re-uses the pre-computed values this.fieldRangesFixedLen + * - For variable size fields does a first pass over the fixed section to read offsets + * - offsets are relative to the start of serialized active fields, after the Bitvector[N] + */ + getFieldRanges(data: ByteViews, start: number, end: number): {optionalFields: BitArray; fieldRanges: BytesRange[]} { + const optionalFieldsByteLen = Math.ceil(this.optionalFieldsCount / 8); + const optionalFields = new BitArray( + data.uint8Array.subarray(start, start + optionalFieldsByteLen), + this.optionalFieldsCount + ); + + const {variableOffsetsPosition, fixedEnd, fieldRangesFixedLen, isFixedLen} = computeSerdesData( + optionalFields, + this.fieldsEntries + ); + + if (variableOffsetsPosition.length === 0) { + // Validate fixed length container + const size = end - start; + if (size !== fixedEnd + optionalFieldsByteLen) { + throw Error( + `${this.typeName} size ${size} not equal fixed end plus optionalFieldsByteLen ${ + fixedEnd + optionalFieldsByteLen + }` + ); + } + + return {optionalFields, fieldRanges: fieldRangesFixedLen}; + } + + // Read offsets in one pass + const offsets = readVariableOffsets( + data.dataView, + start, + end, + optionalFieldsByteLen, + fixedEnd, + variableOffsetsPosition + ); + offsets.push(end - start - optionalFieldsByteLen); // The offsets are relative to the start of serialized optional fields + + // Merge fieldRangesFixedLen + offsets in one array + let variableIdx = 0; + let fixedIdx = 0; + const fieldRanges = new Array(isFixedLen.length); + + for (let i = 0; i < isFixedLen.length; i++) { + if (isFixedLen[i]) { + // push from fixLen ranges ++ + fieldRanges[i] = fieldRangesFixedLen[fixedIdx++]; + } else { + // push from varLen ranges ++ + fieldRanges[i] = {start: offsets[variableIdx], end: offsets[variableIdx + 1]}; + variableIdx++; + } + } + + return {optionalFields, fieldRanges}; + } +} + +/** + * Returns the byte ranges of all variable size fields. + * Offsets are relative to the start of serialized active fields, after the Bitvector[N] + */ +function readVariableOffsets( + data: DataView, + start: number, + end: number, + optionalFieldsEnd: number, + fixedEnd: number, + variableOffsetsPosition: number[] +): number[] { + // Since variable-sized values can be interspersed with fixed-sized values, we precalculate + // the offset indices so we can more easily deserialize the fields in once pass first we get the fixed sizes + // Note: `fixedSizes[i] = null` if that field has variable length + + const size = end - start; + const optionalFieldsByteLen = optionalFieldsEnd - start; + + // with the fixed sizes, we can read the offsets, and store for our single pass + const offsets = new Array(variableOffsetsPosition.length); + for (let i = 0; i < variableOffsetsPosition.length; i++) { + const offset = data.getUint32(start + variableOffsetsPosition[i] + optionalFieldsByteLen, true); + + // Validate offsets. If the list is empty the offset points to the end of the buffer, offset == size + if (offset > size) { + throw new Error(`Offset out of bounds ${offset} > ${size}`); + } + if (i === 0) { + if (offset !== fixedEnd) { + throw new Error(`First offset must equal to fixedEnd ${offset} != ${fixedEnd}`); + } + } else { + if (offset < offsets[i - 1]) { + throw new Error(`Offsets must be increasing ${offset} < ${offsets[i - 1]}`); + } + } + + offsets[i] = offset; + } + + return offsets; +} + +/** + * Precompute sizes of the Container doing one pass over fields + */ +function precomputeSizes(fields: Record>): { + minLen: number; + maxLen: number; + fixedSize: number | null; +} { + let minLen = 0; + let maxLen = 0; + let fixedSize: number | null = 0; + + for (const fieldType of Object.values(fields)) { + minLen += fieldType.minSize; + maxLen += fieldType.maxSize; + + if (fieldType.fixedSize === null) { + // +4 for the offset + minLen += 4; + maxLen += 4; + fixedSize = null; + } else if (fixedSize !== null) { + fixedSize += fieldType.fixedSize; + } + } + return {minLen, maxLen, fixedSize}; +} + +/** + * Compute the JSON key for each fieldName. There will exist a single JSON representation for each type. + * To transform JSON payloads to a casing that is different from the type's defined use external tooling. + */ +export function precomputeJsonKey>>( + fieldName: keyof Fields, + casingMap?: CasingMap, + jsonCase?: KeyCase +): string { + if (casingMap) { + const keyFromCaseMap = casingMap[fieldName]; + if (keyFromCaseMap === undefined) { + throw Error(`casingMap[${fieldName}] not defined`); + } + return keyFromCaseMap as string; + } else if (jsonCase) { + return Case[jsonCase](fieldName as string); + } else { + return fieldName as string; + } +} + +/** + * Render field typeNames for a detailed typeName of this Container + */ +export function renderContainerTypeName>>( + fields: Fields, + prefix = "Profile" +): string { + const fieldNames = Object.keys(fields) as (keyof Fields)[]; + const fieldTypeNames = fieldNames.map((fieldName) => `${fieldName}: ${fields[fieldName].typeName}`).join(", "); + return `${prefix}({${fieldTypeNames}})`; +} diff --git a/packages/ssz/src/type/stableContainer.ts b/packages/ssz/src/type/stableContainer.ts new file mode 100644 index 00000000..bf8b94fa --- /dev/null +++ b/packages/ssz/src/type/stableContainer.ts @@ -0,0 +1,829 @@ +import { + Node, + BranchNode, + LeafNode, + getNodesAtDepth, + subtreeFillToContents, + Tree, + Gindex, + toGindex, + concatGindices, + getNode, + zeroNode, + zeroHash, + countToDepth, + getNodeH, + setNode, + setNodeWithFn, +} from "@chainsafe/persistent-merkle-tree"; +import { + ValueWithCachedPermanentRoot, + hash64, + maxChunksToDepth, + merkleize, + splitIntoRootChunks, + symbolCachedPermanentRoot, +} from "../util/merkleize"; +import {Require} from "../util/types"; +import {namedClass} from "../util/named"; +import {JsonPath, Type, ValueOf} from "./abstract"; +import {CompositeType, ByteViews, CompositeTypeAny, isCompositeType} from "./composite"; +import { + getContainerTreeViewClass, + ValueOfFields, + FieldEntry, + ContainerTreeViewType, + ContainerTreeViewTypeConstructor, + computeSerdesData, +} from "../view/stableContainer"; +import { + getContainerTreeViewDUClass, + ContainerTreeViewDUType, + ContainerTreeViewDUTypeConstructor, +} from "../viewDU/stableContainer"; +import {Case} from "../util/strings"; +import {isOptionalType, toNonOptionalType, NonOptionalFields} from "./optional"; +import {BitArray} from "../value/bitArray"; +/* eslint-disable @typescript-eslint/member-ordering */ + +type BytesRange = {start: number; end: number}; + +export type StableContainerOptions> = { + typeName?: string; + jsonCase?: KeyCase; + casingMap?: CasingMap; + cachePermanentRootStruct?: boolean; + getContainerTreeViewClass?: typeof getContainerTreeViewClass; + getContainerTreeViewDUClass?: typeof getContainerTreeViewDUClass; +}; + +export type KeyCase = + | "eth2" + | "snake" + | "constant" + | "camel" + | "header" + //Same as squish + | "pascal"; + +type CasingMap> = Partial<{[K in keyof Fields]: string}>; + +/** + * StableContainer: ordered heterogeneous collection of values + * - EIP: https://eips.ethereum.org/EIPS/eip-7495 + * - Notation: Custom name per instance + */ +export class StableContainerType>> extends CompositeType< + ValueOfFields, + ContainerTreeViewType, + ContainerTreeViewDUType +> { + readonly typeName: string; + readonly depth: number; + readonly maxChunkCount: number; + readonly fixedSize: number | null; + readonly minSize: number; + readonly maxSize: number; + readonly isList = false; + readonly isViewMutable = true; + + readonly fields: Fields; + // Precomputed data for faster serdes + readonly fieldsEntries: FieldEntry>[]; + /** End of fixed section of serialized Container */ + // readonly fixedEnd: number; + protected readonly fieldsGindex: Record; + protected readonly jsonKeyToFieldName: Record; + + /** Cached TreeView constuctor with custom prototype for this Type's properties */ + protected readonly TreeView: ContainerTreeViewTypeConstructor; + protected readonly TreeViewDU: ContainerTreeViewDUTypeConstructor; + private padActiveFields: boolean[]; + + constructor(fields: Fields, readonly maxFields: number, readonly opts?: StableContainerOptions) { + super(); + + this.fields = fields; + + // Render detailed typeName. Consumers should overwrite since it can get long + this.typeName = opts?.typeName ?? renderContainerTypeName(fields); + + this.maxChunkCount = maxFields; + // Add 1 for the mixed-in bitvector + this.depth = maxChunksToDepth(this.maxChunkCount) + 1; + + // Precalculated data for faster serdes + this.fieldsEntries = []; + for (const fieldName of Object.keys(fields) as (keyof Fields)[]) { + const fieldType = fields[fieldName]; + + this.fieldsEntries.push({ + fieldName, + fieldType: toNonOptionalType(fieldType), + jsonKey: precomputeJsonKey(fieldName, opts?.casingMap, opts?.jsonCase), + gindex: toGindex(this.depth, BigInt(this.fieldsEntries.length)), + optional: isOptionalType(fieldType), + }); + } + + this.padActiveFields = Array.from({length: this.maxChunkCount - this.fieldsEntries.length}, () => false); + + if (this.fieldsEntries.length === 0) { + throw Error("StableContainer must have > 0 fields"); + } + + // Precalculate for Proofs API + this.fieldsGindex = {} as Record; + for (let i = 0; i < this.fieldsEntries.length; i++) { + this.fieldsGindex[this.fieldsEntries[i].fieldName] = toGindex(this.depth, BigInt(i)); + } + + // To resolve JSON paths in fieldName notation and jsonKey notation + this.jsonKeyToFieldName = {}; + for (const {fieldName, jsonKey} of this.fieldsEntries) { + this.jsonKeyToFieldName[jsonKey] = fieldName; + } + + const {minLen, maxLen, fixedSize} = precomputeSizes(this.fieldsEntries); + this.minSize = minLen; + this.maxSize = maxLen; + this.fixedSize = fixedSize; + + // TODO: This options are necessary for ContainerNodeStruct to override this. + // Refactor this constructor to allow customization without pollutin the options + this.TreeView = opts?.getContainerTreeViewClass?.(this) ?? getContainerTreeViewClass(this); + this.TreeViewDU = opts?.getContainerTreeViewDUClass?.(this) ?? getContainerTreeViewDUClass(this); + } + + static named>>( + fields: Fields, + maxFields: number, + opts: Require, "typeName"> + ): StableContainerType { + return new (namedClass(StableContainerType, opts.typeName))(fields, maxFields, opts); + } + + defaultValue(): ValueOfFields { + const value = {} as ValueOfFields; + for (const {fieldName, fieldType, optional} of this.fieldsEntries) { + value[fieldName] = (optional ? null : fieldType.defaultValue()) as ValueOf; + } + return value; + } + + getView(tree: Tree): ContainerTreeViewType { + return new this.TreeView(this, tree); + } + + getViewDU(node: Node, cache?: unknown): ContainerTreeViewDUType { + return new this.TreeViewDU(this, node, cache); + } + + cacheOfViewDU(view: ContainerTreeViewDUType): unknown { + return view.cache; + } + + commitView(view: ContainerTreeViewType): Node { + return view.node; + } + + commitViewDU(view: ContainerTreeViewDUType): Node { + view.commit(); + return view.node; + } + + // Serialization + deserialization + // ------------------------------- + // Containers can mix fixed length and variable length data. + // + // Fixed part Variable part + // [field1 offset][field2 data ][field1 data ] + // [0x000000c] [0xaabbaabbaabbaabb][0xffffffffffffffffffffffff] + + value_serializedSize(value: ValueOfFields): number { + let totalSize = Math.ceil(this.maxChunkCount / 8); + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType, optional} = this.fieldsEntries[i]; + // skip optional fields with nullish values + if (optional && value[fieldName] == null) { + continue; + } + + // Offset (4 bytes) + size + totalSize += + fieldType.fixedSize === null ? 4 + fieldType.value_serializedSize(value[fieldName]) : fieldType.fixedSize; + } + + return totalSize; + } + + value_serializeToBytes(output: ByteViews, offset: number, value: ValueOfFields): number { + // compute active field bitvector + const activeFields = BitArray.fromBoolArray([ + ...this.fieldsEntries.map(({fieldName}) => value[fieldName] != null), + ...this.padActiveFields, + ]); + // write active field bitvector + output.uint8Array.set(activeFields.uint8Array, offset); + + const {fixedEnd} = computeSerdesData(activeFields, this.fieldsEntries); + + const activeFieldsLen = activeFields.uint8Array.length; + let fixedIndex = offset + activeFieldsLen; + let variableIndex = offset + fixedEnd; + + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType, optional} = this.fieldsEntries[i]; + // skip optional fields with nullish values + if (optional && value[fieldName] == null) { + continue; + } + + if (fieldType.fixedSize === null) { + // write offset relative to the start of serialized active fields, after the Bitvector[N] + output.dataView.setUint32(fixedIndex, variableIndex - offset - activeFieldsLen, true); + fixedIndex += 4; + // write serialized element to variable section + variableIndex = fieldType.value_serializeToBytes(output, variableIndex, value[fieldName]); + } else { + fixedIndex = fieldType.value_serializeToBytes(output, fixedIndex, value[fieldName]); + } + } + return variableIndex; + } + + value_deserializeFromBytes(data: ByteViews, start: number, end: number): ValueOfFields { + const {activeFields, fieldRanges} = this.getFieldRanges(data, start, end); + const value = {} as {[K in keyof Fields]: unknown}; + + for (let i = 0, rangesIx = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType, optional} = this.fieldsEntries[i]; + if (optional && !activeFields.get(i)) { + value[fieldName] = null; + continue; + } + + const fieldRange = fieldRanges[rangesIx++]; + value[fieldName] = fieldType.value_deserializeFromBytes(data, start + fieldRange.start, start + fieldRange.end); + } + + return value as ValueOfFields; + } + + tree_serializedSize(node: Node): number { + const activeFields = this.tree_getActiveFields(node); + let totalSize = Math.ceil(activeFields.bitLen / 8); + const nodes = getNodesAtDepth(node, this.depth, 0, this.fieldsEntries.length) as Node[]; + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldType, optional} = this.fieldsEntries[i]; + const node = nodes[i]; + if (optional && !activeFields.get(i)) { + continue; + } + + // Offset (4 bytes) + size + totalSize += fieldType.fixedSize === null ? 4 + fieldType.tree_serializedSize(node) : fieldType.fixedSize; + } + return totalSize; + } + + tree_serializeToBytes(output: ByteViews, offset: number, node: Node): number { + // compute active field bitvector + const activeFields = this.tree_getActiveFields(node); + // write active field bitvector + output.uint8Array.set(activeFields.uint8Array, offset); + + const {fixedEnd} = computeSerdesData(activeFields, this.fieldsEntries); + + const activeFieldsLen = activeFields.uint8Array.length; + let fixedIndex = offset + activeFieldsLen; + let variableIndex = offset + fixedEnd; + + const nodes = getNodesAtDepth(node, this.depth, 0, this.fieldsEntries.length); + + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldType, optional} = this.fieldsEntries[i]; + if (optional && !activeFields.get(i)) { + continue; + } + + const node = nodes[i]; + if (fieldType.fixedSize === null) { + // write offset relative to the start of serialized active fields, after the Bitvector[N] + output.dataView.setUint32(fixedIndex, variableIndex - offset - activeFieldsLen, true); + fixedIndex += 4; + // write serialized element to variable section + variableIndex = fieldType.tree_serializeToBytes(output, variableIndex, node); + } else { + fixedIndex = fieldType.tree_serializeToBytes(output, fixedIndex, node); + } + } + return variableIndex; + } + + tree_deserializeFromBytes(data: ByteViews, start: number, end: number): Node { + const {activeFields, fieldRanges} = this.getFieldRanges(data, start, end); + const nodes = new Array(this.fieldsEntries.length); + + for (let i = 0, rangesIx = 0; i < this.fieldsEntries.length; i++) { + const {fieldType, optional} = this.fieldsEntries[i]; + if (optional && !activeFields.get(i)) { + nodes[i] = zeroNode(0); + continue; + } + + const fieldRange = fieldRanges[rangesIx++]; + nodes[i] = fieldType.tree_deserializeFromBytes(data, start + fieldRange.start, start + fieldRange.end); + } + + const rootNode = new BranchNode(subtreeFillToContents(nodes, this.depth - 1), zeroNode(0)); + return this.tree_setActiveFields(rootNode, activeFields); + } + + // Merkleization + hashTreeRoot(value: ValueOfFields): Uint8Array { + // Return cached mutable root if any + if (this.cachePermanentRootStruct) { + const cachedRoot = (value as ValueWithCachedPermanentRoot)[symbolCachedPermanentRoot]; + if (cachedRoot) { + return cachedRoot; + } + } + + // compute active field bitvector + const activeFields = BitArray.fromBoolArray([ + ...this.fieldsEntries.map(({fieldName}) => value[fieldName] != null), + ...this.padActiveFields, + ]); + const root = mixInActiveFields(super.hashTreeRoot(value), activeFields); + + if (this.cachePermanentRootStruct) { + (value as ValueWithCachedPermanentRoot)[symbolCachedPermanentRoot] = root; + } + + return root; + } + + protected getRoots(struct: ValueOfFields): Uint8Array[] { + const roots = new Array(this.fieldsEntries.length); + + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType, optional} = this.fieldsEntries[i]; + if (optional && struct[fieldName] == null) { + roots[i] = zeroHash(0); + continue; + } + + roots[i] = fieldType.hashTreeRoot(struct[fieldName]); + } + + return roots; + } + + // Proofs + + getPropertyGindex(prop: string): Gindex | null { + const gindex = this.fieldsGindex[prop] ?? this.fieldsGindex[this.jsonKeyToFieldName[prop]]; + if (gindex === undefined) throw Error(`Unknown container property ${prop}`); + return gindex; + } + + getPropertyType(prop: string): Type { + const fieldName = this.fields[prop] ? prop : this.jsonKeyToFieldName[prop]; + const entry = this.fieldsEntries.find((entry) => entry.fieldName === fieldName); + if (entry === undefined) throw Error(`Unknown container property ${prop}`); + return entry.fieldType; + } + + getIndexProperty(index: number): string | null { + if (index >= this.fieldsEntries.length) { + return null; + } + return this.fieldsEntries[index].fieldName as string; + } + + tree_createProofGindexes(node: Node, jsonPaths: JsonPath[]): Gindex[] { + const gindexes: Gindex[] = []; + const activeFields = this.tree_getActiveFields(node); + + for (const jsonPath of jsonPaths) { + const prop = jsonPath[0]; + if (prop == null) { + continue; + } + const fieldIndex = this.fieldsEntries.findIndex((entry) => entry.fieldName === prop); + if (fieldIndex === -1) throw Error(`Unknown container property ${prop}`); + const entry = this.fieldsEntries[fieldIndex]; + if (entry.optional && !activeFields.get(fieldIndex)) { + // field is inactive and doesn't count as a leaf + continue; + } + + // same to Composite + const {type, gindex} = this.getPathInfo(jsonPath); + if (!isCompositeType(type)) { + gindexes.push(gindex); + } else { + // if the path subtype is composite, include the gindices of all the leaves + const leafGindexes = type.tree_getLeafGindices( + gindex, + type.fixedSize === null ? getNode(node, gindex) : undefined + ); + for (const gindex of leafGindexes) { + gindexes.push(gindex); + } + } + } + + return gindexes; + } + + tree_getLeafGindices(rootGindex: Gindex, rootNode?: Node): Gindex[] { + const gindices: Gindex[] = []; + if (!rootNode) { + throw new Error("StableContainer.tree_getLeafGindices requires tree argument to get leaves"); + } + const activeFields = this.tree_getActiveFields(rootNode); + + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType, optional} = this.fieldsEntries[i]; + if (optional && !activeFields.get(i)) { + // field is inactive and doesn't count as a leaf + continue; + } + + const fieldGindex = this.fieldsGindex[fieldName]; + const fieldGindexFromRoot = concatGindices([rootGindex, fieldGindex]); + + if (fieldType.isBasic) { + gindices.push(fieldGindexFromRoot); + } else { + const compositeType = fieldType as unknown as CompositeTypeAny; + if (fieldType.fixedSize === null) { + if (!rootNode) { + throw new Error("variable type requires tree argument to get leaves"); + } + gindices.push(...compositeType.tree_getLeafGindices(fieldGindexFromRoot, getNode(rootNode, fieldGindex))); + } else { + gindices.push(...compositeType.tree_getLeafGindices(fieldGindexFromRoot)); + } + } + } + + return gindices; + } + + // JSON + + fromJson(json: unknown): ValueOfFields { + if (typeof json !== "object") { + throw Error("JSON must be of type object"); + } + if (json === null) { + throw Error("JSON must not be null"); + } + + const value = {} as ValueOfFields; + + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType, jsonKey, optional} = this.fieldsEntries[i]; + const jsonValue = (json as Record)[jsonKey]; + if (optional && jsonValue == null) { + value[fieldName] = null as ValueOf; + continue; + } + + if (jsonValue === undefined) { + throw Error(`JSON expected key ${jsonKey} is undefined`); + } + value[fieldName] = fieldType.fromJson(jsonValue) as ValueOf; + } + + return value; + } + + toJson(value: ValueOfFields): Record { + const json: Record = {}; + + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType, jsonKey, optional} = this.fieldsEntries[i]; + if (optional && value[fieldName] == null) { + json[jsonKey] = null; + continue; + } + + json[jsonKey] = fieldType.toJson(value[fieldName]); + } + + return json; + } + + clone(value: ValueOfFields): ValueOfFields { + const newValue = {} as ValueOfFields; + + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType, optional} = this.fieldsEntries[i]; + if (optional && value[fieldName] == null) { + newValue[fieldName] = null as ValueOf; + continue; + } + + newValue[fieldName] = fieldType.clone(value[fieldName]) as ValueOf; + } + + return newValue; + } + + equals(a: ValueOfFields, b: ValueOfFields): boolean { + for (let i = 0; i < this.fieldsEntries.length; i++) { + const {fieldName, fieldType, optional} = this.fieldsEntries[i]; + if (optional) { + if (a[fieldName] == null && b[fieldName] == null) { + continue; + } + if (a[fieldName] == null || b[fieldName] == null) { + return false; + } + } + if (!fieldType.equals(a[fieldName], b[fieldName])) { + return false; + } + } + + return true; + } + + /** + * `activeFields` is a bitvector prepended to the serialized data. + */ + getFieldRanges(data: ByteViews, start: number, end: number): {activeFields: BitArray; fieldRanges: BytesRange[]} { + // this.maxChunkCount = maxFields + const activeFieldsByteLen = Math.ceil(this.maxChunkCount / 8); + // active fields bitvector, do not mutate + const activeFields = new BitArray(data.uint8Array.subarray(start, start + activeFieldsByteLen), this.maxChunkCount); + + const {variableOffsetsPosition, fixedEnd, fieldRangesFixedLen, isFixedLen} = computeSerdesData( + activeFields, + this.fieldsEntries + ); + + if (variableOffsetsPosition.length === 0) { + // Validate fixed length container + const size = end - start; + if (size !== fixedEnd) { + throw Error(`${this.typeName} size ${size} not equal fixed size ${fixedEnd}`); + } + + return {activeFields, fieldRanges: fieldRangesFixedLen}; + } + + // Read offsets in one pass + const offsets = readVariableOffsets( + data.dataView, + start, + end, + activeFieldsByteLen, + fixedEnd, + variableOffsetsPosition + ); + offsets.push(end - start); // The offsets are relative to the start + + // Merge fieldRangesFixedLen + offsets in one array + let variableIdx = 0; + let fixedIdx = 0; + const fieldRanges = new Array(isFixedLen.length); + + for (let i = 0; i < isFixedLen.length; i++) { + if (isFixedLen[i]) { + // push from fixLen ranges ++ + fieldRanges[i] = fieldRangesFixedLen[fixedIdx++]; + } else { + // push from varLen ranges ++ + fieldRanges[i] = {start: offsets[variableIdx], end: offsets[variableIdx + 1]}; + variableIdx++; + } + } + return {activeFields, fieldRanges}; + } + + // helpers for the active fields + tree_getActiveFields(rootNode: Node): BitArray { + // this.maxChunkCount = maxFields + return getActiveFields(rootNode, this.maxChunkCount); + } + + tree_setActiveFields(rootNode: Node, activeFields: BitArray): Node { + return setActiveFields(rootNode, activeFields); + } + + tree_getActiveField(rootNode: Node, fieldIndex: number): boolean { + return getActiveField(rootNode, this.maxChunkCount, fieldIndex); + } + + tree_setActiveField(rootNode: Node, fieldIndex: number, value: boolean): Node { + return setActiveField(rootNode, this.maxChunkCount, fieldIndex, value); + } +} + +/** + * Returns the byte ranges of all variable size fields. + */ +function readVariableOffsets( + data: DataView, + start: number, + end: number, + activeFieldsEnd: number, + fixedEnd: number, + variableOffsetsPosition: number[] +): number[] { + // Since variable-sized values can be interspersed with fixed-sized values, we precalculate + // the offset indices so we can more easily deserialize the fields in once pass first we get the fixed sizes + // Note: `fixedSizes[i] = null` if that field has variable length + + const size = end - start; + const activeFieldsByteLen = activeFieldsEnd - start; + + // with the fixed sizes, we can read the offsets, and store for our single pass + const offsets = new Array(variableOffsetsPosition.length); + for (let i = 0; i < variableOffsetsPosition.length; i++) { + const offset = data.getUint32(start + variableOffsetsPosition[i], true) + activeFieldsByteLen; + + // Validate offsets. If the list is empty the offset points to the end of the buffer, offset == size + if (offset > size) { + throw new Error(`Offset out of bounds ${offset} > ${size}`); + } + if (i === 0) { + if (offset !== fixedEnd) { + throw new Error(`First offset must equal to fixedEnd ${offset} != ${fixedEnd}`); + } + } else { + if (offset < offsets[i - 1]) { + throw new Error(`Offsets must be increasing ${offset} < ${offsets[i - 1]}`); + } + } + + offsets[i] = offset; + } + + return offsets; +} + +/** + * Precompute sizes of the Container doing one pass over fields + */ +function precomputeSizes>>( + fields: FieldEntry[] +): { + minLen: number; + maxLen: number; + fixedSize: number | null; +} { + // at a minimum, the active fields bitvector is prepended + const activeFieldsLen = Math.ceil(fields.length / 8); + + let minLen = activeFieldsLen; + let maxLen = activeFieldsLen; + const fixedSize = null; + + for (const {fieldType, optional} of fields) { + minLen += optional ? 0 : fieldType.minSize; + maxLen += fieldType.maxSize; + + if (fieldType.fixedSize === null) { + // +4 for the offset + minLen += optional ? 0 : 4; + maxLen += 4; + } + } + return {minLen, maxLen, fixedSize}; +} + +/** + * Compute the JSON key for each fieldName. There will exist a single JSON representation for each type. + * To transform JSON payloads to a casing that is different from the type's defined use external tooling. + */ +export function precomputeJsonKey>>( + fieldName: keyof Fields, + casingMap?: CasingMap, + jsonCase?: KeyCase +): string { + if (casingMap) { + const keyFromCaseMap = casingMap[fieldName]; + if (keyFromCaseMap === undefined) { + throw Error(`casingMap[${fieldName}] not defined`); + } + return keyFromCaseMap as string; + } else if (jsonCase) { + return Case[jsonCase](fieldName as string); + } else { + return fieldName as string; + } +} + +/** + * Render field typeNames for a detailed typeName of this Container + */ +export function renderContainerTypeName>>( + fields: Fields, + prefix = "StableContainer" +): string { + const fieldNames = Object.keys(fields) as (keyof Fields)[]; + const fieldTypeNames = fieldNames.map((fieldName) => `${fieldName}: ${fields[fieldName].typeName}`).join(", "); + return `${prefix}({${fieldTypeNames}})`; +} + +/** + * Get the active field bitvector, given the root of the tree and # of fields + */ +export function getActiveFields(rootNode: Node, bitLen: number): BitArray { + // fast path for depth 1, the bitvector fits in one chunk + if (bitLen <= 256) { + return new BitArray(rootNode.right.root.subarray(0, Math.ceil(bitLen / 8)), bitLen); + } + + const activeFieldsBuf = new Uint8Array(Math.ceil(bitLen / 8)); + const depth = countToDepth(BigInt(Math.ceil(activeFieldsBuf.length / 32))); + const nodes = getNodesAtDepth(rootNode.right, depth, 0, Math.ceil(bitLen / 256)); + for (let i = 0; i < nodes.length; i++) { + activeFieldsBuf.set(nodes[i].root, i * 32); + } + + return new BitArray(activeFieldsBuf, bitLen); +} + +export function setActiveFields(rootNode: Node, activeFields: BitArray): Node { + // fast path for depth 1, the bitvector fits in one chunk + if (activeFields.bitLen <= 256) { + const activeFieldsBuf = new Uint8Array(32); + activeFieldsBuf.set(activeFields.uint8Array); + return new BranchNode(rootNode.left, LeafNode.fromRoot(activeFieldsBuf)); + } + + const activeFieldsChunkCount = Math.ceil(activeFields.bitLen / 256); + const nodes: Node[] = []; + for (let i = 0; i < activeFieldsChunkCount; i++) { + const activeFieldsBuf = new Uint8Array(32); + activeFieldsBuf.set(activeFields.uint8Array.subarray(i * 32, (i + 1) * 32)); + nodes.push(LeafNode.fromRoot(activeFieldsBuf)); + } + + return new BranchNode(rootNode.left, subtreeFillToContents(nodes, Math.ceil(Math.log2(activeFieldsChunkCount)))); +} + +export function getActiveField(rootNode: Node, bitLen: number, fieldIndex: number): boolean { + const hIndex = Math.floor(fieldIndex / 32); + const hBitIndex = fieldIndex % 32; + + // fast path for depth 1, the bitvector fits in one chunk + if (bitLen <= 256) { + const h = getNodeH(rootNode.right, hIndex); + return Boolean(h & (1 << hBitIndex)); + } + + const chunkCount = Math.ceil(bitLen / 256); + const chunkIx = bitLen % 256; + const depth = Math.ceil(Math.log2(chunkCount)); + + const chunk = getNode(rootNode, toGindex(depth, BigInt(chunkIx))); + const h = getNodeH(chunk, hIndex); + return Boolean(h & (1 << hBitIndex)); +} + +export function setActiveField(rootNode: Node, bitLen: number, fieldIndex: number, value: boolean): Node { + const byteIx = Math.floor(fieldIndex / 8); + const bitIx = fieldIndex % 8; + + // fast path for depth 1, the bitvector fits in one chunk + if (bitLen <= 256) { + const activeFieldsBuf = rootNode.right.root; + activeFieldsBuf[byteIx] |= (value ? 1 : 0) << bitIx; + + const activeFieldGindex = BigInt(3); + return setNode(rootNode, activeFieldGindex, LeafNode.fromRoot(activeFieldsBuf)); + } + + const chunkCount = Math.ceil(bitLen / 256); + const chunkIx = bitLen % 256; + const depth = Math.ceil(Math.log2(chunkCount)); + const activeFieldsNode = rootNode.right; + const newActiveFieldsNode = setNodeWithFn(activeFieldsNode, BigInt(2 * depth + chunkIx), (node) => { + const chunkBuf = node.root; + chunkBuf[byteIx] |= (value ? 1 : 0) << bitIx; + return LeafNode.fromRoot(chunkBuf); + }); + + return new BranchNode(rootNode.left, newActiveFieldsNode); +} + +export function mixInActiveFields(root: Uint8Array, activeFields: BitArray): Uint8Array { + // fast path for depth 1, the bitvector fits in one chunk + if (activeFields.bitLen <= 256) { + const activeFieldsChunk = new Uint8Array(32); + activeFieldsChunk.set(activeFields.uint8Array); + return hash64(root, activeFieldsChunk); + } + + const activeFieldsChunks = splitIntoRootChunks(activeFields.uint8Array); + const activeFieldsRoot = merkleize(activeFieldsChunks, activeFieldsChunks.length); + return hash64(root, activeFieldsRoot); +} diff --git a/packages/ssz/src/view/container.ts b/packages/ssz/src/view/container.ts index c062e2cd..34519246 100644 --- a/packages/ssz/src/view/container.ts +++ b/packages/ssz/src/view/container.ts @@ -3,6 +3,7 @@ import {Type, ValueOf} from "../type/abstract"; import {isBasicType, BasicType} from "../type/basic"; import {isCompositeType, CompositeType} from "../type/composite"; import {TreeView} from "./abstract"; +import {NonOptionalFields} from "../type/optional"; export type FieldEntry>> = { fieldName: keyof Fields; @@ -12,13 +13,16 @@ export type FieldEntry>> = { }; /** Expected API of this View's type. This interface allows to break a recursive dependency between types and views */ -export type ContainerTypeGeneric>> = CompositeType< +export type BasicContainerTypeGeneric>> = CompositeType< ValueOfFields, ContainerTreeViewType, unknown > & { readonly fields: Fields; - readonly fieldsEntries: FieldEntry[]; + readonly fieldsEntries: (FieldEntry | FieldEntry>)[]; +}; + +export type ContainerTypeGeneric>> = BasicContainerTypeGeneric & { readonly fixedEnd: number; }; @@ -35,7 +39,7 @@ export type FieldsView>> = { }; export type ContainerTreeViewType>> = FieldsView & - TreeView>; + TreeView>; export type ContainerTreeViewTypeConstructor>> = { new (type: ContainerTypeGeneric, tree: Tree): ContainerTreeViewType; }; diff --git a/packages/ssz/src/view/profile.ts b/packages/ssz/src/view/profile.ts new file mode 100644 index 00000000..7f0bcd4e --- /dev/null +++ b/packages/ssz/src/view/profile.ts @@ -0,0 +1,219 @@ +import { + getNodeAtDepth, + Gindex, + zeroNode, + LeafNode, + Node, + toGindexBitstring, + Tree, +} from "@chainsafe/persistent-merkle-tree"; +import {Type, ValueOf} from "../type/abstract"; +import {isBasicType, BasicType} from "../type/basic"; +import {isCompositeType, CompositeType} from "../type/composite"; +import {TreeView} from "./abstract"; +import {BitArray} from "../value/bitArray"; +import {NonOptionalFields} from "../type/optional"; + +export type FieldEntry>> = { + fieldName: keyof Fields; + fieldType: Fields[keyof Fields]; + jsonKey: string; + gindex: Gindex; + // the position within the activeFields + chunkIndex: number; + optional: boolean; +}; + +/** Expected API of this View's type. This interface allows to break a recursive dependency between types and views */ +export type ContainerTypeGeneric>> = CompositeType< + ValueOfFields, + ContainerTreeViewType, + unknown +> & { + readonly fields: Fields; + readonly fieldsEntries: FieldEntry>[]; + readonly activeFields: BitArray; +}; + +export type ValueOfFields>> = {[K in keyof Fields]: ValueOf}; + +export type FieldsView>> = { + [K in keyof Fields]: Fields[K] extends CompositeType + ? // If composite, return view. MAY propagate changes updwards + TV + : // If basic, return struct value. Will NOT propagate changes upwards + Fields[K] extends BasicType + ? V + : never; +}; + +export type ContainerTreeViewType>> = FieldsView & + TreeView>; +export type ContainerTreeViewTypeConstructor>> = { + new (type: ContainerTypeGeneric, tree: Tree): ContainerTreeViewType; +}; + +/** + * Intented usage: + * + * - Get initial BeaconState from disk. + * - Before applying next block, switch to mutable + * - Get some field, create a view in mutable mode + * - Do modifications of the state in the state transition function + * - When done, commit and apply new root node once to og BeaconState + * - However, keep all the caches and transfer them to the new BeaconState + * + * Questions: + * - Can the child views created in mutable mode switch to not mutable? If so, it seems that it needs to recursively + * iterate the entire data structure and views + * + */ +class ProfileTreeView>> extends TreeView> { + constructor(readonly type: ContainerTypeGeneric, readonly tree: Tree) { + super(); + } + + get node(): Node { + return this.tree.rootNode; + } +} + +export function getProfileTreeViewClass>>( + type: ContainerTypeGeneric +): ContainerTreeViewTypeConstructor { + class CustomProfileTreeView extends ProfileTreeView {} + + // Dynamically define prototype methods + for (let index = 0; index < type.fieldsEntries.length; index++) { + const {fieldName, fieldType, chunkIndex, optional} = type.fieldsEntries[index]; + + // If the field type is basic, the value to get and set will be the actual 'struct' value (i.e. a JS number). + // The view must use the tree_getFromNode() and tree_setToNode() methods to persist the struct data to the node, + // and use the cached views array to store the new node. + if (isBasicType(fieldType)) { + Object.defineProperty(CustomProfileTreeView.prototype, fieldName, { + configurable: false, + enumerable: true, + + // TODO: Review the memory cost of this closures + get: function (this: CustomProfileTreeView) { + const leafNode = getNodeAtDepth(this.node, this.type.depth, chunkIndex) as LeafNode; + if (optional && leafNode === zeroNode(0)) { + return null; + } + + return fieldType.tree_getFromNode(leafNode); + }, + + set: function (this: CustomProfileTreeView, value) { + if (optional && value == null) { + const leafNode = zeroNode(0); + this.tree.setNodeAtDepth(this.type.depth, chunkIndex, leafNode); + return; + } + + const leafNodePrev = getNodeAtDepth(this.node, this.type.depth, chunkIndex) as LeafNode; + const leafNode = leafNodePrev.clone(); + fieldType.tree_setToNode(leafNode, value); + this.tree.setNodeAtDepth(this.type.depth, chunkIndex, leafNode); + }, + }); + } + + // If the field type is composite, the value to get and set will be another TreeView. The parent TreeView must + // cache the view itself to retain the caches of the child view. To set a value the view must return a node to + // set it to the parent tree in the field gindex. + else if (isCompositeType(fieldType)) { + Object.defineProperty(CustomProfileTreeView.prototype, fieldName, { + configurable: false, + enumerable: true, + + // Returns TreeView of fieldName + get: function (this: CustomProfileTreeView) { + const gindex = toGindexBitstring(this.type.depth, chunkIndex); + const tree = this.tree.getSubtree(gindex); + if (optional && tree.rootNode === zeroNode(0)) { + return null; + } + + return fieldType.getView(tree); + }, + + // Expects TreeView of fieldName + set: function (this: CustomProfileTreeView, value: unknown) { + if (optional && value == null) { + this.tree.setNodeAtDepth(this.type.depth, chunkIndex, zeroNode(0)); + } + + const node = fieldType.commitView(value); + this.tree.setNodeAtDepth(this.type.depth, chunkIndex, node); + }, + }); + } + + // Should never happen + else { + /* istanbul ignore next - unreachable code */ + throw Error(`Unknown fieldType ${fieldType.typeName} for fieldName ${fieldName}`); + } + } + + // Change class name + Object.defineProperty(CustomProfileTreeView, "name", {value: type.typeName, writable: false}); + + return CustomProfileTreeView as unknown as ContainerTreeViewTypeConstructor; +} + +// TODO: deduplicate +type BytesRange = {start: number; end: number}; + +/** + * Precompute fixed and variable offsets position for faster deserialization. + * @returns Does a single pass over all fields and returns: + * - isFixedLen: If field index [i] is fixed length + * - fieldRangesFixedLen: For fields with fixed length, their range of bytes + * - variableOffsetsPosition: Position of the 4 bytes offset for variable size fields + * - fixedEnd: End of the fixed size range + * - offsets are relative to the start of serialized active fields, after the Bitvector[N] of optional fields + */ +export function computeSerdesData>>( + optionalFields: BitArray, + fields: FieldEntry[] +): { + isFixedLen: boolean[]; + fieldRangesFixedLen: BytesRange[]; + variableOffsetsPosition: number[]; + fixedEnd: number; +} { + const isFixedLen: boolean[] = []; + const fieldRangesFixedLen: BytesRange[] = []; + const variableOffsetsPosition: number[] = []; + // should not be optionalFields.uint8Array.length because offsets are relative to the start of serialized active fields + let pointerFixed = 0; + + let optionalIndex = 0; + for (const {optional, fieldType} of fields) { + if (optional) { + if (!optionalFields.get(optionalIndex++)) { + continue; + } + } + + isFixedLen.push(fieldType.fixedSize !== null); + if (fieldType.fixedSize === null) { + // Variable length + variableOffsetsPosition.push(pointerFixed); + pointerFixed += 4; + } else { + fieldRangesFixedLen.push({start: pointerFixed, end: pointerFixed + fieldType.fixedSize}); + pointerFixed += fieldType.fixedSize; + } + } + + return { + isFixedLen, + fieldRangesFixedLen, + variableOffsetsPosition, + fixedEnd: pointerFixed, + }; +} diff --git a/packages/ssz/src/view/stableContainer.ts b/packages/ssz/src/view/stableContainer.ts new file mode 100644 index 00000000..f7619619 --- /dev/null +++ b/packages/ssz/src/view/stableContainer.ts @@ -0,0 +1,247 @@ +import { + getNodeAtDepth, + Gindex, + LeafNode, + Node, + toGindexBitstring, + Tree, + zeroNode, +} from "@chainsafe/persistent-merkle-tree"; +import {Type, ValueOf} from "../type/abstract"; +import {isBasicType, BasicType} from "../type/basic"; +import {isCompositeType, CompositeType} from "../type/composite"; +import {TreeView} from "./abstract"; +import {NonOptionalFields, OptionalType} from "../type/optional"; +import {BitArray} from "../value/bitArray"; + +// some code is here to break the circular dependency between type, view, and viewDU + +export type FieldEntry>> = { + fieldName: keyof Fields; + fieldType: Fields[keyof Fields]; + jsonKey: string; + gindex: Gindex; + optional: boolean; +}; + +/** Expected API of this View's type. This interface allows to break a recursive dependency between types and views */ +export type StableContainerTypeGeneric>> = CompositeType< + ValueOfFields, + ContainerTreeViewType, + unknown +> & { + readonly fields: Fields; + readonly fieldsEntries: FieldEntry>[]; + + tree_getActiveFields: (node: Node) => BitArray; + tree_setActiveFields: (node: Node, activeFields: BitArray) => Node; + tree_getActiveField: (node: Node, fieldIndex: number) => boolean; + tree_setActiveField: (node: Node, fieldIndex: number, value: boolean) => Node; +}; + +export type ValueOfFields>> = {[K in keyof Fields]: ValueOf}; + +export type ViewType> = T extends CompositeType + ? // If composite, return view. MAY propagate changes updwards + TV + : // If basic, return struct value. Will NOT propagate changes upwards + T extends BasicType + ? V + : never; + +export type OptionalViewType> = T extends CompositeType + ? // If composite, return view. MAY propagate changes updwards if not nullish + TV | null | undefined + : // If basic, return struct value or nullish. Will NOT propagate changes upwards + T extends BasicType + ? V | null | undefined + : never; + +export type FieldsView>> = { + [K in keyof Fields]: Fields[K] extends OptionalType ? OptionalViewType : ViewType; +}; + +export type ContainerTreeViewType>> = FieldsView & + TreeView>; +export type ContainerTreeViewTypeConstructor>> = { + new (type: StableContainerTypeGeneric, tree: Tree): ContainerTreeViewType; +}; + +/** + * Intented usage: + * + * - Get initial BeaconState from disk. + * - Before applying next block, switch to mutable + * - Get some field, create a view in mutable mode + * - Do modifications of the state in the state transition function + * - When done, commit and apply new root node once to og BeaconState + * - However, keep all the caches and transfer them to the new BeaconState + * + * Questions: + * - Can the child views created in mutable mode switch to not mutable? If so, it seems that it needs to recursively + * iterate the entire data structure and views + * + */ +class ContainerTreeView>> extends TreeView< + StableContainerTypeGeneric +> { + constructor(readonly type: StableContainerTypeGeneric, readonly tree: Tree) { + super(); + } + + get node(): Node { + return this.tree.rootNode; + } +} + +export function getContainerTreeViewClass>>( + type: StableContainerTypeGeneric +): ContainerTreeViewTypeConstructor { + class CustomContainerTreeView extends ContainerTreeView {} + + // Dynamically define prototype methods + for (let index = 0; index < type.fieldsEntries.length; index++) { + const {fieldName, fieldType, optional} = type.fieldsEntries[index]; + + // If the field type is basic, the value to get and set will be the actual 'struct' value (i.e. a JS number). + // The view must use the tree_getFromNode() and tree_setToNode() methods to persist the struct data to the node, + // and use the cached views array to store the new node. + if (isBasicType(fieldType)) { + Object.defineProperty(CustomContainerTreeView.prototype, fieldName, { + configurable: false, + enumerable: true, + + // TODO: Review the memory cost of this closures + get: function (this: CustomContainerTreeView) { + const leafNode = getNodeAtDepth(this.node, this.type.depth, index) as LeafNode; + if (optional && this.type.tree_getActiveField(this.tree.rootNode, index) === false) { + return null; + } + return fieldType.tree_getFromNode(leafNode); + }, + + set: function (this: CustomContainerTreeView, value) { + if (optional && value == null) { + this.tree.setNodeAtDepth(this.type.depth, index, zeroNode(0)); + // only update the active field if necessary + if (this.type.tree_getActiveField(this.tree.rootNode, index)) { + this.tree.rootNode = this.type.tree_setActiveField(this.tree.rootNode, index, false); + } + return; + } + const leafNodePrev = getNodeAtDepth(this.node, this.type.depth, index) as LeafNode; + const leafNode = leafNodePrev.clone(); + fieldType.tree_setToNode(leafNode, value); + this.tree.setNodeAtDepth(this.type.depth, index, leafNode); + // only update the active field if necessary + if (!this.type.tree_getActiveField(this.tree.rootNode, index)) { + this.tree.rootNode = this.type.tree_setActiveField(this.tree.rootNode, index, true); + } + }, + }); + } + + // If the field type is composite, the value to get and set will be another TreeView (if not nullish). The parent TreeView must + // cache the view itself to retain the caches of the child view. To set a value the view must return a node to + // set it to the parent tree in the field gindex. + else if (isCompositeType(fieldType)) { + Object.defineProperty(CustomContainerTreeView.prototype, fieldName, { + configurable: false, + enumerable: true, + + // Returns TreeView of fieldName + get: function (this: CustomContainerTreeView) { + const gindex = toGindexBitstring(this.type.depth, index); + const subtree = this.tree.getSubtree(gindex); + if (optional && this.type.tree_getActiveField(this.tree.rootNode, index) === false) { + return null; + } + return fieldType.getView(subtree); + }, + + // Expects TreeView of fieldName + set: function (this: CustomContainerTreeView, value: unknown) { + if (optional && value == null) { + this.tree.setNodeAtDepth(this.type.depth, index, zeroNode(0)); + // only update the active field if necessary + if (this.type.tree_getActiveField(this.tree.rootNode, index)) { + this.tree.rootNode = this.type.tree_setActiveField(this.tree.rootNode, index, false); + } + return; + } + const node = fieldType.commitView(value); + this.tree.setNodeAtDepth(this.type.depth, index, node); + // only update the active field if necessary + if (!this.type.tree_getActiveField(this.tree.rootNode, index)) { + this.tree.rootNode = this.type.tree_setActiveField(this.tree.rootNode, index, false); + } + }, + }); + } + + // Should never happen + else { + /* istanbul ignore next - unreachable code */ + throw Error(`Unknown fieldType ${fieldType.typeName} for fieldName ${fieldName}`); + } + } + + // Change class name + Object.defineProperty(CustomContainerTreeView, "name", {value: type.typeName, writable: false}); + + return CustomContainerTreeView as unknown as ContainerTreeViewTypeConstructor; +} + +type BytesRange = {start: number; end: number}; + +/** + * Precompute fixed and variable offsets position for faster deserialization. + * @throws when activeFields does not align with non-optional field types + * @returns Does a single pass over all fields and returns: + * - isFixedLen: If field index [i] is fixed length + * - fieldRangesFixedLen: For fields with fixed length, their range of bytes + * - variableOffsetsPosition: Position of the 4 bytes offset for variable size fields + * - fixedEnd: End of the fixed size range + * - + */ +export function computeSerdesData>>( + activeFields: BitArray, + fields: FieldEntry[] +): { + isFixedLen: boolean[]; + fieldRangesFixedLen: BytesRange[]; + variableOffsetsPosition: number[]; + fixedEnd: number; +} { + const isFixedLen: boolean[] = []; + const fieldRangesFixedLen: BytesRange[] = []; + const variableOffsetsPosition: number[] = []; + let pointerFixed = Math.ceil(activeFields.bitLen / 8); + + for (const [i, {fieldName, fieldType, optional}] of fields.entries()) { + // if the field is inactive + if (!activeFields.get(i)) { + if (!optional) { + throw new Error(`Field "${String(fieldName)}" must be active since it is not optional`); + } + continue; + } + + isFixedLen.push(fieldType.fixedSize !== null); + if (fieldType.fixedSize === null) { + // Variable length + variableOffsetsPosition.push(pointerFixed); + pointerFixed += 4; + } else { + fieldRangesFixedLen.push({start: pointerFixed, end: pointerFixed + fieldType.fixedSize}); + pointerFixed += fieldType.fixedSize; + } + } + + return { + isFixedLen, + fieldRangesFixedLen, + variableOffsetsPosition, + fixedEnd: pointerFixed, + }; +} diff --git a/packages/ssz/src/viewDU/container.ts b/packages/ssz/src/viewDU/container.ts index 3e037e0a..993ff602 100644 --- a/packages/ssz/src/viewDU/container.ts +++ b/packages/ssz/src/viewDU/container.ts @@ -9,7 +9,7 @@ import { import {ByteViews, Type} from "../type/abstract"; import {BasicType, isBasicType} from "../type/basic"; import {CompositeType, isCompositeType, CompositeTypeAny} from "../type/composite"; -import {ContainerTypeGeneric} from "../view/container"; +import {BasicContainerTypeGeneric, ContainerTypeGeneric} from "../view/container"; import {TreeViewDU} from "./abstract"; /* eslint-disable @typescript-eslint/member-ordering */ @@ -30,14 +30,16 @@ export type ContainerTreeViewDUTypeConstructor, node: Node, cache?: unknown): ContainerTreeViewDUType; }; +export type ChangedNode = {index: number; node: Node}; + type ContainerTreeViewDUCache = { nodes: Node[]; caches: unknown[]; nodesPopulated: boolean; }; -class ContainerTreeViewDU>> extends TreeViewDU< - ContainerTypeGeneric +export class BasicContainerTreeViewDU>> extends TreeViewDU< + BasicContainerTypeGeneric > { protected nodes: Node[] = []; protected caches: unknown[]; @@ -46,7 +48,7 @@ class ContainerTreeViewDU>> extends private nodesPopulated: boolean; constructor( - readonly type: ContainerTypeGeneric, + readonly type: BasicContainerTypeGeneric, protected _rootNode: Node, cache?: ContainerTreeViewDUCache ) { @@ -94,7 +96,7 @@ class ContainerTreeViewDU>> extends // if old root is not hashed, no need to pass hcByLevel to child view bc we need to do full traversal here const byLevelView = hcByLevel != null && isOldRootHashed ? hcByLevel : null; - const nodesChanged: {index: number; node: Node}[] = []; + const nodesChanged: ChangedNode[] = []; for (const [index, view] of this.viewsChanged) { const fieldType = this.type.fieldsEntries[index].fieldType as unknown as CompositeTypeAny; @@ -114,8 +116,7 @@ class ContainerTreeViewDU>> extends // TODO: Optimize to loop only once, Numerical sort ascending const nodesChangedSorted = nodesChanged.sort((a, b) => a.index - b.index); - const indexes = nodesChangedSorted.map((entry) => entry.index); - const nodes = nodesChangedSorted.map((entry) => entry.node); + const {indexes, nodes} = this.parseNodesChanged(nodesChangedSorted); this._rootNode = setNodesAtDepth( this._rootNode, @@ -135,6 +136,12 @@ class ContainerTreeViewDU>> extends this.viewsChanged.clear(); } + protected parseNodesChanged(nodes: ChangedNode[]): {indexes: number[]; nodes: Node[]} { + const indexes = nodes.map((entry) => entry.index); + const nodesArray = nodes.map((entry) => entry.node); + return {indexes, nodes: nodesArray}; + } + protected clearCache(): void { this.nodes = []; this.caches = []; @@ -147,6 +154,16 @@ class ContainerTreeViewDU>> extends // However preserving _SOME_ caches results in a very unpredictable experience. this.viewsChanged.clear(); } +} + +class ContainerTreeViewDU>> extends BasicContainerTreeViewDU { + constructor( + readonly type: ContainerTypeGeneric, + protected _rootNode: Node, + cache?: ContainerTreeViewDUCache + ) { + super(type, _rootNode, cache); + } /** * Same method to `type/container.ts` that call ViewDU.serializeToBytes() of internal fields. diff --git a/packages/ssz/src/viewDU/profile.ts b/packages/ssz/src/viewDU/profile.ts new file mode 100644 index 00000000..3a7eaa9d --- /dev/null +++ b/packages/ssz/src/viewDU/profile.ts @@ -0,0 +1,252 @@ +import {getNodeAtDepth, LeafNode, Node, zeroNode} from "@chainsafe/persistent-merkle-tree"; +import {ByteViews, Type} from "../type/abstract"; +import {BasicType, isBasicType} from "../type/basic"; +import {CompositeType, isCompositeType} from "../type/composite"; +import {computeSerdesData, ContainerTypeGeneric} from "../view/profile"; +import {TreeViewDU} from "./abstract"; +import {BasicContainerTreeViewDU, ChangedNode} from "./container"; +import {OptionalType} from "../type/optional"; +import {BitArray} from "../value/bitArray"; + +/* eslint-disable @typescript-eslint/member-ordering */ + +export type ViewDUValue> = T extends CompositeType + ? // If composite, return view. MAY propagate changes updwards + TVDU + : // If basic, return struct value. Will NOT propagate changes upwards + T extends BasicType + ? V + : never; + +export type OptionalViewDUValue> = T extends CompositeType + ? // If composite, return view. MAY propagate changes updwards + TVDU | null | undefined + : // If basic, return struct value. Will NOT propagate changes upwards + T extends BasicType + ? V | null | undefined + : never; + +export type FieldsViewDU>> = { + [K in keyof Fields]: Fields[K] extends OptionalType ? OptionalViewDUValue : ViewDUValue; +}; + +export type ContainerTreeViewDUType>> = FieldsViewDU & + TreeViewDU>; +export type ContainerTreeViewDUTypeConstructor>> = { + new (type: ContainerTypeGeneric, node: Node, cache?: unknown): ContainerTreeViewDUType; +}; + +type ContainerTreeViewDUCache = { + nodes: Node[]; + caches: unknown[]; + nodesPopulated: boolean; +}; + +class ProfileTreeViewDU>> extends BasicContainerTreeViewDU { + constructor( + readonly type: ContainerTypeGeneric, + protected _rootNode: Node, + cache?: ContainerTreeViewDUCache + ) { + super(type, _rootNode, cache); + } + + protected parseNodesChanged(nodesArray: ChangedNode[]): {indexes: number[]; nodes: Node[]} { + const indexes = new Array(nodesArray.length); + const nodes = new Array(nodesArray.length); + for (const [i, change] of nodesArray.entries()) { + const {index, node} = change; + const chunkIndex = this.type.fieldsEntries[index].chunkIndex; + indexes[i] = chunkIndex; + nodes[i] = node; + } + return {indexes, nodes}; + } + + /** + * Same method to `type/profile.ts` that call ViewDU.serializeToBytes() of internal fields. + */ + serializeToBytes(output: ByteViews, offset: number): number { + this.commit(); + + const optionalArr: boolean[] = []; + for (let index = 0; index < this.type.fieldsEntries.length; index++) { + const {chunkIndex, optional} = this.type.fieldsEntries[index]; + let node = this.nodes[index]; + if (node === undefined) { + node = getNodeAtDepth(this._rootNode, this.type.depth, chunkIndex); + this.nodes[index] = node; + } + if (optional) { + optionalArr.push(node !== zeroNode(0)); + } + } + + const optionalFields = BitArray.fromBoolArray(optionalArr); + output.uint8Array.set(optionalFields.uint8Array, offset); + + const {fixedEnd} = computeSerdesData(optionalFields, this.type.fieldsEntries); + + const optionalFieldsLen = optionalFields.uint8Array.length; + let fixedIndex = offset + optionalFieldsLen; + let variableIndex = offset + fixedEnd + optionalFieldsLen; + for (let index = 0; index < this.type.fieldsEntries.length; index++) { + const {fieldType, optional} = this.type.fieldsEntries[index]; + const node = this.nodes[index]; + // all nodes are populated above + if (optional && node === zeroNode(0)) { + continue; + } + + if (fieldType.fixedSize === null) { + // write offset relative to the start of serialized active fields, after the Bitvector[N] + output.dataView.setUint32(fixedIndex, variableIndex - offset - optionalFieldsLen, true); + fixedIndex += 4; + // write serialized element to variable section + // basic types always have fixedSize + if (isCompositeType(fieldType)) { + const view = fieldType.getViewDU(node, this.caches[index]) as TreeViewDU; + if (view.serializeToBytes !== undefined) { + variableIndex = view.serializeToBytes(output, variableIndex); + } else { + // some types don't define ViewDU as TreeViewDU, like the UnionType, in that case view.serializeToBytes = undefined + variableIndex = fieldType.tree_serializeToBytes(output, variableIndex, node); + } + } + } else { + fixedIndex = fieldType.tree_serializeToBytes(output, fixedIndex, node); + } + } + + return variableIndex; + } +} + +export function getProfileTreeViewDUClass>>( + type: ContainerTypeGeneric +): ContainerTreeViewDUTypeConstructor { + class CustomProfileTreeViewDU extends ProfileTreeViewDU {} + + // Dynamically define prototype methods + for (let index = 0; index < type.fieldsEntries.length; index++) { + const {fieldName, fieldType, chunkIndex, optional} = type.fieldsEntries[index]; + + // If the field type is basic, the value to get and set will be the actual 'struct' value (i.e. a JS number). + // The view must use the tree_getFromNode() and tree_setToNode() methods to persist the struct data to the node, + // and use the cached views array to store the new node. + if (isBasicType(fieldType)) { + Object.defineProperty(CustomProfileTreeViewDU.prototype, fieldName, { + configurable: false, + enumerable: true, + + // TODO: Review the memory cost of this closures + get: function (this: CustomProfileTreeViewDU) { + // First walk through the tree to get the root node for that index + let node = this.nodes[index]; + if (node === undefined) { + node = getNodeAtDepth(this._rootNode, this.type.depth, chunkIndex); + this.nodes[index] = node; + } + + if (optional && node === zeroNode(0)) { + return null; + } + + return fieldType.tree_getFromNode(node as LeafNode) as unknown; + }, + + set: function (this: CustomProfileTreeViewDU, value) { + if (optional && value == null) { + this.nodes[index] = zeroNode(0); + this.nodesChanged.add(index); + return; + } + + // Create new node if current leafNode is not dirty + let nodeChanged: LeafNode; + if (this.nodesChanged.has(index)) { + // TODO: This assumes that node has already been populated + nodeChanged = this.nodes[index] as LeafNode; + } else { + const nodePrev = (this.nodes[index] ?? + getNodeAtDepth(this._rootNode, this.type.depth, chunkIndex)) as LeafNode; + + nodeChanged = nodePrev.clone(); + // Store the changed node in the nodes cache + this.nodes[index] = nodeChanged; + this.nodesChanged.add(index); + } + + fieldType.tree_setToNode(nodeChanged, value); + }, + }); + } + + // If the field type is composite, the value to get and set will be another TreeView. The parent TreeView must + // cache the view itself to retain the caches of the child view. To set a value the view must return a node to + // set it to the parent tree in the field gindex. + else if (isCompositeType(fieldType)) { + Object.defineProperty(CustomProfileTreeViewDU.prototype, fieldName, { + configurable: false, + enumerable: true, + + // Returns TreeViewDU of fieldName + get: function (this: CustomProfileTreeViewDU) { + const viewChanged = this.viewsChanged.get(index); + if (viewChanged) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return viewChanged; + } + + let node = this.nodes[index]; + if (node === undefined) { + node = getNodeAtDepth(this._rootNode, this.type.depth, chunkIndex); + this.nodes[index] = node; + } + + if (optional && node === zeroNode(0)) { + return null; + } + + // Keep a reference to the new view to call .commit on it latter, only if mutable + const view = fieldType.getViewDU(node, this.caches[index]); + if (fieldType.isViewMutable) { + this.viewsChanged.set(index, view); + } + + // No need to persist the child's view cache since a second get returns this view instance. + // The cache is only persisted on commit where the viewsChanged map is dropped. + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return view; + }, + + // Expects TreeViewDU of fieldName + set: function (this: CustomProfileTreeViewDU, view: unknown) { + if (optional && view == null) { + this.nodes[index] = zeroNode(0); + this.nodesChanged.add(index); + return; + } + + // When setting a view: + // - Not necessary to commit node + // - Not necessary to persist cache + // Just keeping a reference to the view in this.viewsChanged ensures consistency + this.viewsChanged.set(index, view); + }, + }); + } + + // Should never happen + else { + /* istanbul ignore next - unreachable code */ + throw Error(`Unknown fieldType ${fieldType.typeName} for fieldName ${fieldName}`); + } + } + + // Change class name + Object.defineProperty(CustomProfileTreeViewDU, "name", {value: type.typeName, writable: false}); + + return CustomProfileTreeViewDU as unknown as ContainerTreeViewDUTypeConstructor; +} diff --git a/packages/ssz/src/viewDU/stableContainer.ts b/packages/ssz/src/viewDU/stableContainer.ts new file mode 100644 index 00000000..dc892ba4 --- /dev/null +++ b/packages/ssz/src/viewDU/stableContainer.ts @@ -0,0 +1,257 @@ +import {getNodeAtDepth, LeafNode, Node, zeroNode, HashComputationLevel} from "@chainsafe/persistent-merkle-tree"; +import {ByteViews, Type} from "../type/abstract"; +import {BasicType, isBasicType} from "../type/basic"; +import {CompositeType, isCompositeType} from "../type/composite"; +import {computeSerdesData, StableContainerTypeGeneric} from "../view/stableContainer"; +import {TreeViewDU} from "./abstract"; +import {OptionalType} from "../type/optional"; +import {BitArray} from "../value/bitArray"; +import {BasicContainerTreeViewDU} from "./container"; + +/* eslint-disable @typescript-eslint/member-ordering */ + +export type ViewDUValue> = T extends CompositeType + ? // If composite, return view. MAY propagate changes updwards + TVDU + : // If basic, return struct value. Will NOT propagate changes upwards + T extends BasicType + ? V + : never; + +export type OptionalViewDUValue> = T extends CompositeType + ? // If composite, return view. MAY propagate changes updwards + TVDU | null | undefined + : // If basic, return struct value. Will NOT propagate changes upwards + T extends BasicType + ? V | null | undefined + : never; + +export type FieldsViewDU>> = { + [K in keyof Fields]: Fields[K] extends OptionalType ? OptionalViewDUValue : ViewDUValue; +}; + +export type ContainerTreeViewDUType>> = FieldsViewDU & + TreeViewDU>; +export type ContainerTreeViewDUTypeConstructor>> = { + new (type: StableContainerTypeGeneric, node: Node, cache?: unknown): ContainerTreeViewDUType; +}; + +type ContainerTreeViewDUCache = { + activeFields: BitArray; + nodes: Node[]; + caches: unknown[]; + nodesPopulated: boolean; +}; + +class StableContainerTreeViewDU>> extends BasicContainerTreeViewDU { + /** pending active fields bitvector */ + protected activeFields: BitArray; + + constructor( + readonly type: StableContainerTypeGeneric, + protected _rootNode: Node, + cache?: ContainerTreeViewDUCache + ) { + super(type, _rootNode, cache); + + if (cache) { + this.activeFields = cache.activeFields; + } else { + this.activeFields = type.tree_getActiveFields(_rootNode); + } + } + + get cache(): ContainerTreeViewDUCache { + const result = super.cache; + return {...result, activeFields: this.activeFields}; + } + + commit(hcOffset = 0, hcByLevel: HashComputationLevel[] | null = null): void { + super.commit(hcOffset, hcByLevel); + this._rootNode = this.type.tree_setActiveFields(this._rootNode, this.activeFields); + if (hcByLevel !== null) { + hcByLevel[hcOffset].push(this._rootNode.left, this._rootNode.right, this._rootNode); + } + } + + /** + * Same method to `type/container.ts` that call ViewDU.serializeToBytes() of internal fields. + */ + serializeToBytes(output: ByteViews, offset: number): number { + this.commit(); + + const activeFields = this.type.tree_getActiveFields(this.node); + // write active fields bitvector + output.uint8Array.set(activeFields.uint8Array, offset); + + const {fixedEnd} = computeSerdesData(activeFields, this.type.fieldsEntries); + + const activeFieldsLen = activeFields.uint8Array.length; + let fixedIndex = offset + activeFieldsLen; + let variableIndex = offset + fixedEnd; + for (let index = 0; index < this.type.fieldsEntries.length; index++) { + const {fieldType, optional} = this.type.fieldsEntries[index]; + if (optional && !activeFields.get(index)) { + continue; + } + + let node = this.nodes[index]; + if (node === undefined) { + node = getNodeAtDepth(this._rootNode, this.type.depth, index); + this.nodes[index] = node; + } + if (fieldType.fixedSize === null) { + // write offset relative to the start of serialized active fields, after the Bitvector[N] + output.dataView.setUint32(fixedIndex, variableIndex - offset - activeFieldsLen, true); + fixedIndex += 4; + // write serialized element to variable section + // basic types always have fixedSize + if (isCompositeType(fieldType)) { + const view = fieldType.getViewDU(node, this.caches[index]) as TreeViewDU; + if (view.serializeToBytes !== undefined) { + variableIndex = view.serializeToBytes(output, variableIndex); + } else { + // some types don't define ViewDU as TreeViewDU, like the UnionType, in that case view.serializeToBytes = undefined + variableIndex = fieldType.tree_serializeToBytes(output, variableIndex, node); + } + } + } else { + fixedIndex = fieldType.tree_serializeToBytes(output, fixedIndex, node); + } + } + + return variableIndex; + } +} + +export function getContainerTreeViewDUClass>>( + type: StableContainerTypeGeneric +): ContainerTreeViewDUTypeConstructor { + class CustomContainerTreeViewDU extends StableContainerTreeViewDU {} + + // Dynamically define prototype methods + for (let index = 0; index < type.fieldsEntries.length; index++) { + const {fieldName, fieldType, optional} = type.fieldsEntries[index]; + + // If the field type is basic, the value to get and set will be the actual 'struct' value (i.e. a JS number). + // The view must use the tree_getFromNode() and tree_setToNode() methods to persist the struct data to the node, + // and use the cached views array to store the new node. + if (isBasicType(fieldType)) { + Object.defineProperty(CustomContainerTreeViewDU.prototype, fieldName, { + configurable: false, + enumerable: true, + + // TODO: Review the memory cost of this closures + get: function (this: CustomContainerTreeViewDU) { + if (optional && this.activeFields.get(index) === false) { + return null; + } + + // First walk through the tree to get the root node for that index + let node = this.nodes[index]; + if (node === undefined) { + node = getNodeAtDepth(this._rootNode, this.type.depth, index); + this.nodes[index] = node; + } + + return fieldType.tree_getFromNode(node as LeafNode) as unknown; + }, + + set: function (this: CustomContainerTreeViewDU, value) { + if (optional && value == null) { + this.nodes[index] = zeroNode(0); + this.nodesChanged.add(index); + this.activeFields.set(index, false); + return; + } + + // Create new node if current leafNode is not dirty + let nodeChanged: LeafNode; + if (this.nodesChanged.has(index)) { + // TODO: This assumes that node has already been populated + nodeChanged = this.nodes[index] as LeafNode; + } else { + const nodePrev = (this.nodes[index] ?? getNodeAtDepth(this._rootNode, this.type.depth, index)) as LeafNode; + + nodeChanged = nodePrev.clone(); + // Store the changed node in the nodes cache + this.nodes[index] = nodeChanged; + this.nodesChanged.add(index); + } + + fieldType.tree_setToNode(nodeChanged, value); + this.activeFields.set(index, true); + }, + }); + } + + // If the field type is composite, the value to get and set will be another TreeView. The parent TreeView must + // cache the view itself to retain the caches of the child view. To set a value the view must return a node to + // set it to the parent tree in the field gindex. + else if (isCompositeType(fieldType)) { + Object.defineProperty(CustomContainerTreeViewDU.prototype, fieldName, { + configurable: false, + enumerable: true, + + // Returns TreeViewDU of fieldName + get: function (this: CustomContainerTreeViewDU) { + if (optional && this.activeFields.get(index) === false) { + return null; + } + + const viewChanged = this.viewsChanged.get(index); + if (viewChanged) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return viewChanged; + } + + let node = this.nodes[index]; + if (node === undefined) { + node = getNodeAtDepth(this._rootNode, this.type.depth, index); + this.nodes[index] = node; + } + + // Keep a reference to the new view to call .commit on it latter, only if mutable + const view = fieldType.getViewDU(node, this.caches[index]); + if (fieldType.isViewMutable) { + this.viewsChanged.set(index, view); + } + + // No need to persist the child's view cache since a second get returns this view instance. + // The cache is only persisted on commit where the viewsChanged map is dropped. + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return view; + }, + + // Expects TreeViewDU of fieldName + set: function (this: CustomContainerTreeViewDU, view: unknown) { + if (optional && view == null) { + this.nodes[index] = zeroNode(0); + this.nodesChanged.add(index); + this.activeFields.set(index, false); + return; + } + + // When setting a view: + // - Not necessary to commit node + // - Not necessary to persist cache + // Just keeping a reference to the view in this.viewsChanged ensures consistency + this.viewsChanged.set(index, view); + this.activeFields.set(index, true); + }, + }); + } + + // Should never happen + else { + /* istanbul ignore next - unreachable code */ + throw Error(`Unknown fieldType ${fieldType.typeName} for fieldName ${fieldName}`); + } + } + + // Change class name + Object.defineProperty(CustomContainerTreeViewDU, "name", {value: type.typeName, writable: false}); + + return CustomContainerTreeViewDU as unknown as ContainerTreeViewDUTypeConstructor; +} diff --git a/packages/ssz/test/unit/byType/profile/tree.test.ts b/packages/ssz/test/unit/byType/profile/tree.test.ts new file mode 100644 index 00000000..a29662af --- /dev/null +++ b/packages/ssz/test/unit/byType/profile/tree.test.ts @@ -0,0 +1,743 @@ +import {expect} from "chai"; +import {Tree} from "@chainsafe/persistent-merkle-tree"; +import { + BitArray, + BitListType, + BitVectorType, + BooleanType, + ByteListType, + ByteVectorType, + ContainerNodeStructType, + ContainerType, + ListBasicType, + ListCompositeType, + NoneType, + OptionalType, + ProfileType, + StableContainerType, + toHexString, + UnionType, + ValueOf, + VectorBasicType, + VectorCompositeType, +} from "../../../../src"; +import {uint64NumInfType, uint64NumType} from "../../../utils/primitiveTypes"; +import {runViewTestMutation} from "../runViewTestMutation"; + +// Test both ContainerType, ContainerNodeStructType only if +// - All fields are immutable + +// TODO: different activeFields + +runViewTestMutation({ + // Use Number64UintType and NumberUintType to test they work the same + type: new ProfileType({a: uint64NumInfType, b: uint64NumInfType}, BitArray.fromBoolArray([false, true, true, false])), + mutations: [ + { + id: "set basic", + valueBefore: {a: 1, b: 2}, + valueAfter: {a: 10, b: 2}, + fn: (tv) => { + tv.a = 10; + }, + }, + { + id: "set basic x2", + valueBefore: {a: 1, b: 2}, + valueAfter: {a: 10, b: 20}, + fn: (tv) => { + tv.a = 10; + tv.b = 20; + }, + }, + // Test that reading a uin64 value that spans two hashObject h values works + // the same with Number64UintType and NumberUintType + { + id: "swap props", + valueBefore: {a: 0xffffffff + 1, b: 0xffffffff + 2}, + valueAfter: {a: 0xffffffff + 2, b: 0xffffffff + 1}, + fn: (tv) => { + const a = tv.a; + const b = tv.b; + tv.a = b; + tv.b = a; + }, + }, + ], +}); + +const containerUintsType = new ProfileType( + {a: uint64NumInfType, b: uint64NumInfType}, + BitArray.fromBoolArray([false, true, true, false]) +); + +runViewTestMutation({ + type: containerUintsType, + treeViewToStruct: (tv) => ({a: tv.a, b: tv.b}), + mutations: [ + { + id: "set all properties", + valueBefore: {a: 1, b: 2}, + valueAfter: {a: 10, b: 20}, + fn: (tv) => { + tv.a = 10; + tv.b = 21; + // Change twice on purpose to trigger a branch in set basic + tv.b = 20; + }, + }, + ], +}); + +const byte32 = new ByteVectorType(32); +const containerBytesType = new ProfileType({a: byte32, b: byte32}, BitArray.fromBoolArray([false, true, true, false])); +const rootOf = (i: number): Buffer => Buffer.alloc(32, i); + +runViewTestMutation({ + type: containerBytesType, + treeViewToStruct: (tv) => ({a: tv.a, b: tv.b}), + mutations: [ + { + id: "set all properties", + valueBefore: {a: rootOf(1), b: rootOf(2)}, + valueAfter: {a: rootOf(3), b: rootOf(4)}, + fn: (tv) => { + tv.a = rootOf(3); + tv.b = rootOf(4); + }, + }, + ], +}); + +const profileUint64 = new ProfileType({a: uint64NumType}, BitArray.fromBoolArray([false, true, false, false])); + +describe(`${profileUint64.typeName} drop caches`, () => { + it("Make some changes then get previous value", () => { + const view = profileUint64.defaultViewDU(); + const bytesBefore = toHexString(view.serialize()); + + // Make changes to view and clone them to new view + view.a = 1; + view.clone(); + + const bytesAfter = toHexString(view.serialize()); + expect(bytesAfter).to.equal(bytesBefore, "view retained changes"); + }); +}); + +// Test only ContainerType if +// - Some fields are mutable + +const list8Uint64NumInfType = new ListBasicType(uint64NumInfType, 8); + +runViewTestMutation({ + type: new ProfileType( + {a: uint64NumInfType, b: uint64NumInfType, list: list8Uint64NumInfType}, + BitArray.fromBoolArray([false, true, true, true]) + ), + mutations: [ + { + id: "set composite entire list", + valueBefore: {a: 1, b: 2, list: []}, + valueAfter: {a: 1, b: 2, list: [10, 20]}, + fn: (tv) => { + tv.list = list8Uint64NumInfType.toViewDU([10, 20]); + }, + }, + { + id: "set composite list with push", + valueBefore: {a: 1, b: 2, list: []}, + valueAfter: {a: 1, b: 2, list: [10, 20]}, + fn: (tv) => { + tv.list.push(10); + tv.list.push(20); + }, + }, + // Test that keeping a reference to `list` and pushing twice mutates the original tv value + { + id: "set composite list with push and reference", + valueBefore: {a: 1, b: 2, list: []}, + valueAfter: {a: 1, b: 2, list: [10, 20]}, + fn: (tv) => { + const list = tv.list; + list.push(10); + list.push(20); + }, + }, + ], +}); + +const containerUint64 = new ProfileType({a: uint64NumType}, BitArray.fromBoolArray([false, true, false, false])); +const listOfContainers = new ListCompositeType(containerUint64, 4); + +runViewTestMutation({ + // Ensure mutations of child array are commited + type: new ContainerType({list: listOfContainers}), + treeViewToStruct: (tv) => { + const listArr: ValueOf = []; + for (let i = 0; i < tv.list.length; i++) { + const item = tv.list.get(i); + listArr.push({a: item.a}); + } + return {list: listArr}; + }, + mutations: [ + { + id: "Push two values", + valueBefore: {list: []}, + valueAfter: {list: [{a: 1}, {a: 2}]}, + fn: (tv) => { + tv.list.push(containerUint64.toViewDU({a: 1})); + tv.list.push(containerUint64.toViewDU({a: 2})); + }, + }, + ], +}); + +// to test new the VietDU.serialize() implementation for different types +const mixedContainer = new ProfileType( + { + // a basic type + a: uint64NumType, + // a list basic type + b: new ListBasicType(uint64NumType, 10), + // a list composite type + c: new ListCompositeType(new ContainerType({a: uint64NumInfType, b: uint64NumInfType}), 10), + // embedded container type + d: new ContainerType({a: uint64NumInfType}), + // a union type, cannot mutate through this test + e: new UnionType([new NoneType(), uint64NumInfType]), + }, + BitArray.fromBoolArray([false, true, true, false, true, true, true, false]) +); + +runViewTestMutation({ + type: mixedContainer, + mutations: [ + { + id: "increase by 1", + valueBefore: {a: 10, b: [0, 1], c: [{a: 100, b: 101}], d: {a: 1000}, e: {selector: 1, value: 2000}}, + // View/ViewDU of Union is a value so we cannot mutate + valueAfter: {a: 11, b: [1, 2], c: [{a: 101, b: 102}], d: {a: 1001}, e: {selector: 1, value: 2000}}, + fn: (tv) => { + tv.a += 1; + const b = tv.b; + for (let i = 0; i < b.length; i++) { + b.set(i, b.get(i) + 1); + } + const c = tv.c; + for (let i = 0; i < c.length; i++) { + const item = c.get(i); + item.a += 1; + item.b += 1; + } + tv.d.a += 1; + // does not affect anyway, leaving here to make it explicit + tv.e = {selector: 1, value: tv.e.value ?? 0 + 1}; + }, + }, + ], +}); + +describe("ProfileViewDU batchHashTreeRoot", function () { + const childContainerType = new ContainerType({f0: uint64NumInfType, f1: uint64NumInfType}); + const unionType = new UnionType([new NoneType(), uint64NumType]); + const listBasicType = new ListBasicType(uint64NumType, 10); + const vectorBasicType = new VectorBasicType(uint64NumType, 2); + const listCompositeType = new ListCompositeType(childContainerType, 10); + const vectorCompositeType = new VectorCompositeType(childContainerType, 1); + const bitVectorType = new BitVectorType(64); + const bitListType = new BitListType(4); + const childContainerStruct = new ContainerNodeStructType({g0: uint64NumInfType, g1: uint64NumInfType}); + const optionalType = new OptionalType(listBasicType); + const parentContainerType = new ProfileType( + { + a: uint64NumType, + b: new BooleanType(), + c: unionType, + d: new ByteListType(64), + e: new ByteVectorType(64), + // a child container type + f: childContainerType, + g: childContainerStruct, + h: listBasicType, + i: vectorBasicType, + j: listCompositeType, + k: vectorCompositeType, + l: bitVectorType, + m: bitListType, + n: optionalType, + }, + BitArray.fromBoolArray([ + false, + true, + true, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ]) + ); + + const value: ValueOf = { + a: 10, + b: true, + c: {selector: 1, value: 100}, + d: Buffer.alloc(64, 2), + e: Buffer.alloc(64, 1), + f: {f0: 100, f1: 101}, + g: {g0: 100, g1: 101}, + h: [1, 2], + i: [1, 2], + j: [{f0: 1, f1: 2}], + k: [{f0: 1, f1: 2}], + l: BitArray.fromSingleBit(64, 5), + m: BitArray.fromSingleBit(4, 1), + n: [1, 2], + }; + const expectedRoot = parentContainerType.hashTreeRoot(value); + + it("fresh ViewDU", () => { + expect(parentContainerType.toViewDU(value).batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify Number type", () => { + const viewDU = parentContainerType.toViewDU({...value, a: 9}); + viewDU.batchHashTreeRoot(); + viewDU.a += 1; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.a = 10; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify BooleanType", () => { + const viewDU = parentContainerType.toViewDU({...value, b: false}); + viewDU.batchHashTreeRoot(); + viewDU.b = true; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.b = true; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify UnionType", () => { + const viewDU = parentContainerType.toViewDU({...value, c: {selector: 1, value: 101}}); + viewDU.batchHashTreeRoot(); + viewDU.c = unionType.toViewDU({selector: 1, value: 100}); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.c = unionType.toViewDU({selector: 1, value: 100}); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify ByteVectorType", () => { + const viewDU = parentContainerType.toViewDU(value); + viewDU.batchHashTreeRoot(); + // this takes more than 1 chunk so the resulting node is a branch node + viewDU.e = viewDU.e.slice(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.e = viewDU.e.slice(); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify ByteListType", () => { + const viewDU = parentContainerType.toViewDU(value); + viewDU.batchHashTreeRoot(); + // this takes more than 1 chunk so the resulting node is a branch node + viewDU.d = viewDU.d.slice(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.d = viewDU.d.slice(); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify full child container", () => { + const viewDU = parentContainerType.toViewDU({...value, f: {f0: 99, f1: 999}}); + viewDU.batchHashTreeRoot(); + viewDU.f = childContainerType.toViewDU({f0: 100, f1: 101}); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.f = childContainerType.toViewDU({f0: 100, f1: 101}); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify partial child container", () => { + const viewDU = parentContainerType.toViewDU({...value, f: {f0: 99, f1: 999}}); + viewDU.batchHashTreeRoot(); + viewDU.f.f0 = 100; + viewDU.f.f1 = 101; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.f.f0 = 100; + viewDU.f.f1 = 101; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify ContainerNodeStructType", () => { + const viewDU = parentContainerType.toViewDU({...value, g: {g0: 99, g1: 999}}); + viewDU.batchHashTreeRoot(); + viewDU.g = childContainerStruct.toViewDU({g0: 100, g1: 101}); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.g = childContainerStruct.toViewDU({g0: 100, g1: 101}); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify partial ContainerNodeStructType", () => { + const viewDU = parentContainerType.toViewDU({...value, g: {g0: 99, g1: 999}}); + viewDU.batchHashTreeRoot(); + viewDU.g.g0 = 100; + viewDU.g.g1 = 101; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.g.g0 = 100; + viewDU.g.g1 = 101; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify ListBasicType", () => { + const viewDU = parentContainerType.toViewDU({...value, h: []}); + viewDU.batchHashTreeRoot(); + viewDU.h = listBasicType.toViewDU([1, 2]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.h = listBasicType.toViewDU([1, 2]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then push 1 item to ListBasicType", () => { + const viewDU = parentContainerType.toViewDU({...value, h: [1]}); + viewDU.batchHashTreeRoot(); + viewDU.h.push(2); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.h = listBasicType.toViewDU([1, 2]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 item of ListBasicType", () => { + const viewDU = parentContainerType.toViewDU({...value, h: [1, 3]}); + viewDU.batchHashTreeRoot(); + viewDU.h.set(1, 2); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.h.set(1, 2); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify VectorBasicType", () => { + const viewDU = parentContainerType.toViewDU({...value, i: []}); + viewDU.batchHashTreeRoot(); + viewDU.i = vectorBasicType.toViewDU([1, 2]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.i = vectorBasicType.toViewDU([1, 2]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 item of VectorBasicType", () => { + const viewDU = parentContainerType.toViewDU({...value, i: [1, 3]}); + viewDU.batchHashTreeRoot(); + viewDU.i.set(1, 2); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.i.set(1, 2); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify ListCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, j: []}); + viewDU.batchHashTreeRoot(); + viewDU.j = listCompositeType.toViewDU([{f0: 1, f1: 2}]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.j = listCompositeType.toViewDU([{f0: 1, f1: 2}]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then push 1 item to ListCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, j: []}); + viewDU.batchHashTreeRoot(); + viewDU.j.push(childContainerType.toViewDU({f0: 1, f1: 2})); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.j = listCompositeType.toViewDU([{f0: 1, f1: 2}]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 item of ListCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, j: [{f0: 1, f1: 3}]}); + viewDU.batchHashTreeRoot(); + viewDU.j.set(0, childContainerType.toViewDU({f0: 1, f1: 2})); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.j.set(0, childContainerType.toViewDU({f0: 1, f1: 2})); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 field of 1 item of ListCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, j: [{f0: 1, f1: 3}]}); + viewDU.batchHashTreeRoot(); + viewDU.j.get(0).f1 = 2; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.j.get(0).f1 = 2; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify VectorCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, k: [{f0: 9, f1: 9}]}); + viewDU.batchHashTreeRoot(); + viewDU.k = vectorCompositeType.toViewDU([{f0: 1, f1: 2}]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.k = vectorCompositeType.toViewDU([{f0: 1, f1: 2}]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 item of VectorCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, k: [{f0: 1, f1: 3}]}); + viewDU.batchHashTreeRoot(); + viewDU.k.set(0, childContainerType.toViewDU({f0: 1, f1: 2})); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.k.set(0, childContainerType.toViewDU({f0: 1, f1: 2})); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 field 1 item of VectorCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, k: [{f0: 1, f1: 3}]}); + viewDU.batchHashTreeRoot(); + viewDU.k.get(0).f1 = 2; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.k.get(0).f1 = 2; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify BitVectorType", () => { + const viewDU = parentContainerType.toViewDU({...value, l: BitArray.fromSingleBit(64, 4)}); + viewDU.batchHashTreeRoot(); + viewDU.l = bitVectorType.toViewDU(BitArray.fromSingleBit(64, 5)); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.l = bitVectorType.toViewDU(BitArray.fromSingleBit(64, 5)); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify BitVectorType bit", () => { + const viewDU = parentContainerType.toViewDU({...value, l: BitArray.fromSingleBit(64, 4)}); + viewDU.batchHashTreeRoot(); + viewDU.l.set(4, false); + viewDU.l.set(5, true); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.l.set(4, false); + viewDU.l.set(5, true); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify BitListType", () => { + const viewDU = parentContainerType.toViewDU({...value, m: BitArray.fromSingleBit(4, 0)}); + viewDU.batchHashTreeRoot(); + viewDU.m = bitListType.toViewDU(BitArray.fromSingleBit(4, 1)); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.m = bitListType.toViewDU(BitArray.fromSingleBit(4, 1)); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify BitListType bit", () => { + const viewDU = parentContainerType.toViewDU({...value, m: BitArray.fromSingleBit(4, 0)}); + viewDU.batchHashTreeRoot(); + viewDU.m.set(0, false); + viewDU.m.set(1, true); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.m.set(0, false); + viewDU.m.set(1, true); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify OptionalType", () => { + const viewDU = parentContainerType.toViewDU({...value, n: null}); + viewDU.batchHashTreeRoot(); + viewDU.n = listBasicType.toViewDU([1, 2]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.n = listBasicType.toViewDU([1, 2]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); +}); + +describe("Optional types", () => { + it("offsets are relative to the start of serialized active fields, after the Bitvector[N]", () => { + const parentContainer = new ProfileType( + { + a: new OptionalType(list8Uint64NumInfType), + }, + BitArray.fromBoolArray([true]) + ); + + const value = {a: [1]}; + // 1st byte is for OptionalFields, 2nd byte is the start of fixed parts + // value 4 is relative to 2nd byte + const expectedBytes = new Uint8Array([1, 4, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]); + const data = {uint8Array: expectedBytes, dataView: new DataView(expectedBytes.buffer, 0, expectedBytes.length)}; + const expectedRoot = parentContainer.hashTreeRoot(value); + expect(parentContainer.serialize(value)).to.deep.equal(expectedBytes); + + let viewDU = parentContainer.toViewDU(value); + expect(viewDU.serialize()).to.deep.equal(expectedBytes); + expect(viewDU.batchHashTreeRoot()).to.deep.equal(expectedRoot); + + viewDU = parentContainer.getViewDU(parentContainer.tree_deserializeFromBytes(data, 0, data.uint8Array.length)); + expect(viewDU.serialize()).to.deep.equal(expectedBytes); + expect(viewDU.batchHashTreeRoot()).to.deep.equal(expectedRoot); + + let view = parentContainer.toView(value); + expect(view.serialize()).to.deep.equal(expectedBytes); + expect(view.hashTreeRoot()).to.deep.equal(expectedRoot); + + view = parentContainer.getView( + new Tree(parentContainer.tree_deserializeFromBytes(data, 0, data.uint8Array.length)) + ); + expect(view.serialize()).to.deep.equal(expectedBytes); + expect(view.hashTreeRoot()).to.deep.equal(expectedRoot); + + const value2 = parentContainer.deserialize(expectedBytes); + expect(value2).to.be.deep.equal(value); + expect(parentContainer.serialize(value2)).to.deep.equal(expectedBytes); + expect(parentContainer.hashTreeRoot(value2)).to.deep.equal(expectedRoot); + }); + + it("set null vs 0", () => { + const parentContainer = new ProfileType( + { + a: new OptionalType(uint64NumInfType), + }, + BitArray.fromBoolArray([true]) + ); + + const value = {a: 0}; + const valueNull = {a: null}; + + // value + const value2 = parentContainer.deserialize(parentContainer.serialize(value)); + expect(value2).to.be.deep.equal(value); + const valueNull2 = parentContainer.deserialize(parentContainer.serialize(valueNull)); + expect(valueNull2).to.be.deep.equal(valueNull); + + // ViewDU + const viewDU = parentContainer.toViewDU(value); + expect(viewDU.a).to.be.equal(0); + const viewDUNull = parentContainer.toViewDU(valueNull); + expect(viewDUNull.a).to.be.null; + + // View + const view = parentContainer.toView(value); + expect(view.a).to.be.equal(0); + const viewNull = parentContainer.toView(valueNull); + expect(viewNull.a).to.be.null; + }); +}); + +describe("Profile type merkleization vs StableContainer", () => { + const optionalType = new OptionalType(uint64NumType); + const listBasicType = new ListBasicType(uint64NumType, 10); + + const stableType = new StableContainerType( + { + a: optionalType, + b: uint64NumType, + c: listBasicType, + d: optionalType, + }, + 8 + ); + + const profileType = new ProfileType( + { + b: uint64NumType, + c: listBasicType, + }, + BitArray.fromBoolArray([false, true, true, false, false, false, false, false]) + ); + + const value = {b: 100, c: [10, 200]}; + + it("batchHashTreeRoot()", () => { + const stableViewDU = stableType.toViewDU({...value, a: null, d: null}); + const profileViewDU = profileType.toViewDU(value); + expect(stableViewDU.batchHashTreeRoot()).to.be.deep.equal(profileViewDU.batchHashTreeRoot()); + }); + + it("hashTreeRoot()", () => { + const stableViewDU = stableType.toViewDU({...value, a: null, d: null}); + const profileViewDU = profileType.toViewDU(value); + expect(stableViewDU.hashTreeRoot()).to.be.deep.equal(profileViewDU.hashTreeRoot()); + }); +}); diff --git a/packages/ssz/test/unit/byType/profile/valid.test.ts b/packages/ssz/test/unit/byType/profile/valid.test.ts new file mode 100644 index 00000000..9a1f67e2 --- /dev/null +++ b/packages/ssz/test/unit/byType/profile/valid.test.ts @@ -0,0 +1,49 @@ +import {BitArray, ProfileType, UintNumberType} from "../../../../src"; +import {runTypeTestValid} from "../runTypeTestValid"; + +// taken from eip spec tests + +const uint16 = new UintNumberType(2); +const byteType = new UintNumberType(1); + +const Square = new ProfileType( + { + side: uint16, + color: byteType, + }, + BitArray.fromBoolArray([true, true, false, false]) +); + +const Circle = new ProfileType( + { + color: byteType, + radius: uint16, + }, + BitArray.fromBoolArray([false, true, true, false]) +); + +runTypeTestValid({ + type: Square, + defaultValue: {side: 0, color: 0}, + values: [ + { + id: "circle1-0", + serialized: "0x420001", + json: {side: 0x42, color: 1}, + root: "0xbfdb6fda9d02805e640c0f5767b8d1bb9ff4211498a5e2d7c0f36e1b88ce57ff", + }, + ], +}); + +runTypeTestValid({ + type: Circle, + defaultValue: {color: 0, radius: 0}, + values: [ + { + id: "square1-0", + serialized: "0x014200", + json: {color: 1, radius: 0x42}, + root: "0xf66d2c38c8d2afbd409e86c529dff728e9a4208215ca20ee44e49c3d11e145d8", + }, + ], +}); diff --git a/packages/ssz/test/unit/byType/runTypeProofTest.ts b/packages/ssz/test/unit/byType/runTypeProofTest.ts index aa7a7441..e0b36e2a 100644 --- a/packages/ssz/test/unit/byType/runTypeProofTest.ts +++ b/packages/ssz/test/unit/byType/runTypeProofTest.ts @@ -1,6 +1,15 @@ import {Node} from "@chainsafe/persistent-merkle-tree"; import {expect} from "chai"; -import {BitArray, ContainerType, fromHexString, JsonPath, OptionalType, Type} from "../../../src"; +import { + BitArray, + ContainerType, + fromHexString, + JsonPath, + OptionalType, + ProfileType, + StableContainerType, + Type, +} from "../../../src"; import {CompositeTypeAny, isCompositeType} from "../../../src/type/composite"; import {ArrayBasicTreeView} from "../../../src/view/arrayBasic"; import {RootHex} from "../../lodestarTypes"; @@ -37,9 +46,12 @@ export function runProofTestOnAllJsonPaths({ const viewLeafFromProof = getJsonPathView(type, viewFromProof, jsonPath); const jsonLeaf = getJsonPathValue(type, json, jsonPath); - const jsonLeafFromProof = typeLeaf.toJson( - isCompositeType(typeLeaf) ? typeLeaf.toValueFromView(viewLeafFromProof) : viewLeafFromProof - ); + const jsonLeafFromProof = + viewLeafFromProof == null + ? viewLeafFromProof + : typeLeaf.toJson( + isCompositeType(typeLeaf) ? typeLeaf.toValueFromView(viewLeafFromProof) : viewLeafFromProof + ); expect(jsonLeafFromProof).to.deep.equal(jsonLeaf, "Wrong value fromProof"); @@ -108,9 +120,10 @@ function getJsonPathView(type: Type, view: unknown, jsonPath: JsonPath) if (typeof jsonProp === "number") { view = (view as ArrayBasicTreeView).get(jsonProp); } else if (typeof jsonProp === "string") { - if (type instanceof ContainerType) { + if (type instanceof ContainerType || type instanceof StableContainerType || type instanceof ProfileType) { // Coerce jsonProp to a fieldName. JSON paths may be in JSON notation or fieldName notation - const fieldName = type["jsonKeyToFieldName"][jsonProp] ?? jsonProp; + const fieldName = + (type as ContainerType>>)["jsonKeyToFieldName"][jsonProp] ?? jsonProp; view = (view as Record)[fieldName as string]; } else { throw Error(`type ${type.typeName} is not a ContainerType - jsonProp '${jsonProp}'`); @@ -139,8 +152,8 @@ function getJsonPathValue(type: Type, json: unknown, jsonPath: JsonPath if (typeof jsonProp === "number") { json = (json as unknown[])[jsonProp]; } else if (typeof jsonProp === "string") { - if (type instanceof ContainerType) { - if (type["jsonKeyToFieldName"][jsonProp] === undefined) { + if (type instanceof ContainerType || type instanceof StableContainerType || type instanceof ProfileType) { + if ((type as ContainerType>>)["jsonKeyToFieldName"][jsonProp] === undefined) { throw Error(`Unknown jsonProp ${jsonProp} for type ${type.typeName}`); } diff --git a/packages/ssz/test/unit/byType/stableContainer/tree.test.ts b/packages/ssz/test/unit/byType/stableContainer/tree.test.ts new file mode 100644 index 00000000..36d40dfe --- /dev/null +++ b/packages/ssz/test/unit/byType/stableContainer/tree.test.ts @@ -0,0 +1,723 @@ +import {expect} from "chai"; +import { + BitArray, + BitListType, + BitVectorType, + BooleanType, + ByteListType, + ByteVectorType, + ContainerNodeStructType, + ContainerType, + ListBasicType, + ListCompositeType, + NoneType, + OptionalType, + StableContainerType, + toHexString, + UnionType, + ValueOf, + VectorBasicType, + VectorCompositeType, +} from "../../../../src"; +import {uint64NumInfType, uint64NumType} from "../../../utils/primitiveTypes"; +import {runViewTestMutation} from "../runViewTestMutation"; +import {getNodesAtDepth, Tree, zeroHash} from "@chainsafe/persistent-merkle-tree"; + +// Test both ContainerType, ContainerNodeStructType only if +// - All fields are immutable + +// TODO: test different number of fields to test the serialization + +runViewTestMutation({ + // Use Number64UintType and NumberUintType to test they work the same + type: new StableContainerType({a: uint64NumInfType, b: uint64NumInfType}, 8), + mutations: [ + { + id: "set basic", + valueBefore: {a: 1, b: 2}, + valueAfter: {a: 10, b: 2}, + fn: (tv) => { + tv.a = 10; + }, + }, + { + id: "set basic x2", + valueBefore: {a: 1, b: 2}, + valueAfter: {a: 10, b: 20}, + fn: (tv) => { + tv.a = 10; + tv.b = 20; + }, + }, + // Test that reading a uin64 value that spans two hashObject h values works + // the same with Number64UintType and NumberUintType + { + id: "swap props", + valueBefore: {a: 0xffffffff + 1, b: 0xffffffff + 2}, + valueAfter: {a: 0xffffffff + 2, b: 0xffffffff + 1}, + fn: (tv) => { + const a = tv.a; + const b = tv.b; + tv.a = b; + tv.b = a; + }, + }, + ], +}); + +const containerUintsType = new StableContainerType({a: uint64NumInfType, b: uint64NumInfType}, 8); + +runViewTestMutation({ + type: containerUintsType, + treeViewToStruct: (tv) => ({a: tv.a, b: tv.b}), + mutations: [ + { + id: "set all properties", + valueBefore: {a: 1, b: 2}, + valueAfter: {a: 10, b: 20}, + fn: (tv) => { + tv.a = 10; + tv.b = 21; + // Change twice on purpose to trigger a branch in set basic + tv.b = 20; + }, + }, + ], +}); + +const byte32 = new ByteVectorType(32); +const containerBytesType = new StableContainerType({a: byte32, b: byte32}, 8); +const rootOf = (i: number): Buffer => Buffer.alloc(32, i); + +runViewTestMutation({ + type: containerBytesType, + treeViewToStruct: (tv) => ({a: tv.a, b: tv.b}), + mutations: [ + { + id: "set all properties", + valueBefore: {a: rootOf(1), b: rootOf(2)}, + valueAfter: {a: rootOf(3), b: rootOf(4)}, + fn: (tv) => { + tv.a = rootOf(3); + tv.b = rootOf(4); + }, + }, + ], +}); + +const stableContainerUint64 = new StableContainerType({a: uint64NumType}, 8); + +describe(`${stableContainerUint64.typeName} drop caches`, () => { + it("Make some changes then get previous value", () => { + const view = stableContainerUint64.defaultViewDU(); + const bytesBefore = toHexString(view.serialize()); + + // Make changes to view and clone them to new view + view.a = 1; + view.clone(); + + const bytesAfter = toHexString(view.serialize()); + expect(bytesAfter).to.equal(bytesBefore, "view retained changes"); + }); +}); + +// Test only ContainerType if +// - Some fields are mutable + +const list8Uint64NumInfType = new ListBasicType(uint64NumInfType, 8); + +runViewTestMutation({ + type: new StableContainerType({a: uint64NumInfType, b: uint64NumInfType, list: list8Uint64NumInfType}, 8), + mutations: [ + { + id: "set composite entire list", + valueBefore: {a: 1, b: 2, list: []}, + valueAfter: {a: 1, b: 2, list: [10, 20]}, + fn: (tv) => { + tv.list = list8Uint64NumInfType.toViewDU([10, 20]); + }, + }, + { + id: "set composite list with push", + valueBefore: {a: 1, b: 2, list: []}, + valueAfter: {a: 1, b: 2, list: [10, 20]}, + fn: (tv) => { + tv.list.push(10); + tv.list.push(20); + }, + }, + // Test that keeping a reference to `list` and pushing twice mutates the original tv value + { + id: "set composite list with push and reference", + valueBefore: {a: 1, b: 2, list: []}, + valueAfter: {a: 1, b: 2, list: [10, 20]}, + fn: (tv) => { + const list = tv.list; + list.push(10); + list.push(20); + }, + }, + ], +}); + +const containerUint64 = new StableContainerType({a: uint64NumType}, 8); +const listOfContainers = new ListCompositeType(containerUint64, 4); + +runViewTestMutation({ + // Ensure mutations of child array are commited + type: new ContainerType({list: listOfContainers}), + treeViewToStruct: (tv) => { + const listArr: ValueOf = []; + for (let i = 0; i < tv.list.length; i++) { + const item = tv.list.get(i); + listArr.push({a: item.a}); + } + return {list: listArr}; + }, + mutations: [ + { + id: "Push two values", + valueBefore: {list: []}, + valueAfter: {list: [{a: 1}, {a: 2}]}, + fn: (tv) => { + tv.list.push(containerUint64.toViewDU({a: 1})); + tv.list.push(containerUint64.toViewDU({a: 2})); + }, + }, + ], +}); + +// to test new the VietDU.serialize() implementation for different types +const mixedContainer = new StableContainerType( + { + // a basic type + a: uint64NumType, + // a list basic type + b: new ListBasicType(uint64NumType, 10), + // a list composite type + c: new ListCompositeType(new ContainerType({a: uint64NumInfType, b: uint64NumInfType}), 10), + // embedded container type + d: new ContainerType({a: uint64NumInfType}), + // a union type, cannot mutate through this test + e: new UnionType([new NoneType(), uint64NumInfType]), + }, + 8 +); + +runViewTestMutation({ + type: mixedContainer, + mutations: [ + { + id: "increase by 1", + valueBefore: {a: 10, b: [0, 1], c: [{a: 100, b: 101}], d: {a: 1000}, e: {selector: 1, value: 2000}}, + // View/ViewDU of Union is a value so we cannot mutate + valueAfter: {a: 11, b: [1, 2], c: [{a: 101, b: 102}], d: {a: 1001}, e: {selector: 1, value: 2000}}, + fn: (tv) => { + tv.a += 1; + const b = tv.b; + for (let i = 0; i < b.length; i++) { + b.set(i, b.get(i) + 1); + } + const c = tv.c; + for (let i = 0; i < c.length; i++) { + const item = c.get(i); + item.a += 1; + item.b += 1; + } + tv.d.a += 1; + // does not affect anyway, leaving here to make it explicit + tv.e = {selector: 1, value: tv.e.value ?? 0 + 1}; + }, + }, + ], +}); + +describe("StableContainerViewDU batchHashTreeRoot", function () { + const childContainerType = new ContainerType({f0: uint64NumInfType, f1: uint64NumInfType}); + const unionType = new UnionType([new NoneType(), uint64NumType]); + const listBasicType = new ListBasicType(uint64NumType, 10); + const vectorBasicType = new VectorBasicType(uint64NumType, 2); + const listCompositeType = new ListCompositeType(childContainerType, 10); + const vectorCompositeType = new VectorCompositeType(childContainerType, 1); + const bitVectorType = new BitVectorType(64); + const bitListType = new BitListType(4); + const childContainerStruct = new ContainerNodeStructType({g0: uint64NumInfType, g1: uint64NumInfType}); + const optionalType = new OptionalType(listBasicType); + const parentContainerType = new StableContainerType( + { + a: uint64NumType, + b: new BooleanType(), + c: unionType, + d: new ByteListType(64), + e: new ByteVectorType(64), + // a child container type + f: childContainerType, + g: childContainerStruct, + h: listBasicType, + i: vectorBasicType, + j: listCompositeType, + k: vectorCompositeType, + l: bitVectorType, + m: bitListType, + n: optionalType, + }, + 64 + ); + + const value: ValueOf = { + a: 10, + b: true, + c: {selector: 1, value: 100}, + d: Buffer.alloc(64, 2), + e: Buffer.alloc(64, 1), + f: {f0: 100, f1: 101}, + g: {g0: 100, g1: 101}, + h: [1, 2], + i: [1, 2], + j: [{f0: 1, f1: 2}], + k: [{f0: 1, f1: 2}], + l: BitArray.fromSingleBit(64, 5), + m: BitArray.fromSingleBit(4, 1), + n: [1, 2], + }; + const expectedRoot = parentContainerType.hashTreeRoot(value); + + it("fresh ViewDU", () => { + expect(parentContainerType.toViewDU(value).batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify Number type", () => { + const viewDU = parentContainerType.toViewDU({...value, a: 9}); + viewDU.batchHashTreeRoot(); + viewDU.a += 1; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.a = 10; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify BooleanType", () => { + const viewDU = parentContainerType.toViewDU({...value, b: false}); + viewDU.batchHashTreeRoot(); + viewDU.b = true; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.b = true; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify UnionType", () => { + const viewDU = parentContainerType.toViewDU({...value, c: {selector: 1, value: 101}}); + viewDU.batchHashTreeRoot(); + viewDU.c = unionType.toViewDU({selector: 1, value: 100}); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.c = unionType.toViewDU({selector: 1, value: 100}); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify ByteVectorType", () => { + const viewDU = parentContainerType.toViewDU(value); + viewDU.batchHashTreeRoot(); + // this takes more than 1 chunk so the resulting node is a branch node + viewDU.e = viewDU.e.slice(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.e = viewDU.e.slice(); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify ByteListType", () => { + const viewDU = parentContainerType.toViewDU(value); + viewDU.batchHashTreeRoot(); + // this takes more than 1 chunk so the resulting node is a branch node + viewDU.d = viewDU.d.slice(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.d = viewDU.d.slice(); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify full child container", () => { + const viewDU = parentContainerType.toViewDU({...value, f: {f0: 99, f1: 999}}); + viewDU.batchHashTreeRoot(); + viewDU.f = childContainerType.toViewDU({f0: 100, f1: 101}); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.f = childContainerType.toViewDU({f0: 100, f1: 101}); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify partial child container", () => { + const viewDU = parentContainerType.toViewDU({...value, f: {f0: 99, f1: 999}}); + viewDU.batchHashTreeRoot(); + viewDU.f.f0 = 100; + viewDU.f.f1 = 101; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.f.f0 = 100; + viewDU.f.f1 = 101; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify ContainerNodeStructType", () => { + const viewDU = parentContainerType.toViewDU({...value, g: {g0: 99, g1: 999}}); + viewDU.batchHashTreeRoot(); + viewDU.g = childContainerStruct.toViewDU({g0: 100, g1: 101}); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.g = childContainerStruct.toViewDU({g0: 100, g1: 101}); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify partial ContainerNodeStructType", () => { + const viewDU = parentContainerType.toViewDU({...value, g: {g0: 99, g1: 999}}); + viewDU.batchHashTreeRoot(); + viewDU.g.g0 = 100; + viewDU.g.g1 = 101; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.g.g0 = 100; + viewDU.g.g1 = 101; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify ListBasicType", () => { + const viewDU = parentContainerType.toViewDU({...value, h: []}); + viewDU.batchHashTreeRoot(); + viewDU.h = listBasicType.toViewDU([1, 2]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.h = listBasicType.toViewDU([1, 2]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then push 1 item to ListBasicType", () => { + const viewDU = parentContainerType.toViewDU({...value, h: [1]}); + viewDU.batchHashTreeRoot(); + viewDU.h.push(2); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.h = listBasicType.toViewDU([1, 2]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 item of ListBasicType", () => { + const viewDU = parentContainerType.toViewDU({...value, h: [1, 3]}); + viewDU.batchHashTreeRoot(); + viewDU.h.set(1, 2); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.h.set(1, 2); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify VectorBasicType", () => { + const viewDU = parentContainerType.toViewDU({...value, i: []}); + viewDU.batchHashTreeRoot(); + viewDU.i = vectorBasicType.toViewDU([1, 2]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.i = vectorBasicType.toViewDU([1, 2]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 item of VectorBasicType", () => { + const viewDU = parentContainerType.toViewDU({...value, i: [1, 3]}); + viewDU.batchHashTreeRoot(); + viewDU.i.set(1, 2); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.i.set(1, 2); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify ListCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, j: []}); + viewDU.batchHashTreeRoot(); + viewDU.j = listCompositeType.toViewDU([{f0: 1, f1: 2}]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.j = listCompositeType.toViewDU([{f0: 1, f1: 2}]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then push 1 item to ListCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, j: []}); + viewDU.batchHashTreeRoot(); + viewDU.j.push(childContainerType.toViewDU({f0: 1, f1: 2})); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.j = listCompositeType.toViewDU([{f0: 1, f1: 2}]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 item of ListCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, j: [{f0: 1, f1: 3}]}); + viewDU.batchHashTreeRoot(); + viewDU.j.set(0, childContainerType.toViewDU({f0: 1, f1: 2})); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.j.set(0, childContainerType.toViewDU({f0: 1, f1: 2})); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 field of 1 item of ListCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, j: [{f0: 1, f1: 3}]}); + viewDU.batchHashTreeRoot(); + viewDU.j.get(0).f1 = 2; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.j.get(0).f1 = 2; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify VectorCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, k: [{f0: 9, f1: 9}]}); + viewDU.batchHashTreeRoot(); + viewDU.k = vectorCompositeType.toViewDU([{f0: 1, f1: 2}]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.k = vectorCompositeType.toViewDU([{f0: 1, f1: 2}]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 item of VectorCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, k: [{f0: 1, f1: 3}]}); + viewDU.batchHashTreeRoot(); + viewDU.k.set(0, childContainerType.toViewDU({f0: 1, f1: 2})); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.k.set(0, childContainerType.toViewDU({f0: 1, f1: 2})); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify 1 field 1 item of VectorCompositeType", () => { + const viewDU = parentContainerType.toViewDU({...value, k: [{f0: 1, f1: 3}]}); + viewDU.batchHashTreeRoot(); + viewDU.k.get(0).f1 = 2; + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.k.get(0).f1 = 2; + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify BitVectorType", () => { + const viewDU = parentContainerType.toViewDU({...value, l: BitArray.fromSingleBit(64, 4)}); + viewDU.batchHashTreeRoot(); + viewDU.l = bitVectorType.toViewDU(BitArray.fromSingleBit(64, 5)); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.l = bitVectorType.toViewDU(BitArray.fromSingleBit(64, 5)); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify BitVectorType bit", () => { + const viewDU = parentContainerType.toViewDU({...value, l: BitArray.fromSingleBit(64, 4)}); + viewDU.batchHashTreeRoot(); + viewDU.l.set(4, false); + viewDU.l.set(5, true); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.l.set(4, false); + viewDU.l.set(5, true); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify BitListType", () => { + const viewDU = parentContainerType.toViewDU({...value, m: BitArray.fromSingleBit(4, 0)}); + viewDU.batchHashTreeRoot(); + viewDU.m = bitListType.toViewDU(BitArray.fromSingleBit(4, 1)); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.m = bitListType.toViewDU(BitArray.fromSingleBit(4, 1)); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify BitListType bit", () => { + const viewDU = parentContainerType.toViewDU({...value, m: BitArray.fromSingleBit(4, 0)}); + viewDU.batchHashTreeRoot(); + viewDU.m.set(0, false); + viewDU.m.set(1, true); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.m.set(0, false); + viewDU.m.set(1, true); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); + + it("full hash then modify OptionalType", () => { + const viewDU = parentContainerType.toViewDU({...value, n: null}); + viewDU.batchHashTreeRoot(); + viewDU.n = listBasicType.toViewDU([1, 2]); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + // assign again but commit before batchHashTreeRoot() + viewDU.n = listBasicType.toViewDU([1, 2]); + viewDU.commit(); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + }); +}); + +describe("StableContainer BitVector[N]", () => { + const optionalType = new OptionalType(uint64NumType); + + it("should have correct serialized size", () => { + for (const maxFields of [8, 16, 64, 256]) { + const stableType = new StableContainerType({a: optionalType}, maxFields); + // should prepend with BitVector[N] + const bytesLength = Math.ceil(maxFields / 8); + expect(stableType.defaultView().serialize().length).to.be.equal(bytesLength); + expect(stableType.defaultViewDU().serialize().length).to.be.equal(bytesLength); + expect(stableType.serialize(stableType.defaultValue()).length).to.be.equal(bytesLength); + } + }); + + it("should have correct active_fields", () => { + for (const maxFields of [64, 256, 512, 1024]) { + const stableType = new StableContainerType({a: optionalType}, maxFields); + for (const view of [stableType.defaultView(), stableType.defaultViewDU()]) { + view.a = 1; + // commit() inside ViewDU + view.hashTreeRoot(); + const activeFieldsDepth = Math.ceil(Math.log2(Math.ceil(maxFields / 256))); + const activeFieldsRootNodes = getNodesAtDepth(view.node.right, activeFieldsDepth, 0, 4); + let isFirst = true; + for (const node of activeFieldsRootNodes) { + if (isFirst) { + const root = node.root; + expect(root[0]).to.be.equal(1); + root[0] = 0; + expect(root).to.deep.equal(zeroHash(0)); + } else { + expect(node.root).to.deep.equal(zeroHash(0)); + } + isFirst = false; + } + + expect(view.node.root).to.be.deep.equal(stableType.hashTreeRoot({a: 1})); + } + } + }); + + it("offsets are relative to the start of serialized active fields, after the Bitvector[N]", () => { + const stableType = new StableContainerType({a: new OptionalType(list8Uint64NumInfType)}, 64); + const value = {a: [1]}; + // first 8 bytes are the active fields + // value 4 at 9th byte is relative to the 9th byte + const expectedBytes = new Uint8Array([1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]); + const data = {uint8Array: expectedBytes, dataView: new DataView(expectedBytes.buffer, 0, expectedBytes.length)}; + const expectedRoot = stableType.hashTreeRoot(value); + expect(stableType.serialize(value)).to.be.deep.equal(expectedBytes); + + let viewDU = stableType.toViewDU(value); + expect(viewDU.serialize()).to.be.deep.equal(expectedBytes); + expect(viewDU.batchHashTreeRoot()).to.be.deep.equal(expectedRoot); + + viewDU = stableType.getViewDU(stableType.tree_deserializeFromBytes(data, 0, data.uint8Array.length)); + expect(viewDU.serialize()).to.deep.equal(expectedBytes); + expect(viewDU.batchHashTreeRoot()).to.deep.equal(expectedRoot); + + let view = stableType.toView(value); + expect(view.serialize()).to.deep.equal(expectedBytes); + expect(view.hashTreeRoot()).to.deep.equal(expectedRoot); + + view = stableType.getView(new Tree(stableType.tree_deserializeFromBytes(data, 0, data.uint8Array.length))); + expect(view.serialize()).to.deep.equal(expectedBytes); + expect(view.hashTreeRoot()).to.deep.equal(expectedRoot); + + const value2 = stableType.deserialize(expectedBytes); + expect(value2).to.be.deep.equal(value); + expect(stableType.serialize(value2)).to.deep.equal(expectedBytes); + expect(stableType.hashTreeRoot(value2)).to.deep.equal(expectedRoot); + }); +}); + +describe("StableContainer backward compatibility", () => { + it("add 1 optional field", () => { + const optionalType = new OptionalType(uint64NumType); + const listBasicType = new ListBasicType(uint64NumType, 10); + const Type1 = new StableContainerType( + { + a: optionalType, + b: optionalType, + c: listBasicType, + }, + 8 + ); + + // grow the container with type c + const Type2 = new StableContainerType( + { + a: optionalType, + b: optionalType, + c: listBasicType, + d: optionalType, + }, + 8 + ); + + const value = {a: null, b: 2, c: [1, 2], d: null}; + const viewDU2 = Type2.toViewDU(value); + const serialized = viewDU2.serialize(); + const byteView = { + uint8Array: serialized, + dataView: new DataView(serialized.buffer, serialized.byteOffset, serialized.byteLength), + }; + // can deserialize + const node = Type1.tree_deserializeFromBytes(byteView, 0, serialized.length); + const viewDU = Type2.getViewDU(node); + // hashTreeRoot is the same + expect(viewDU.hashTreeRoot()).to.deep.equal(viewDU2.hashTreeRoot()); + }); +}); diff --git a/packages/ssz/test/unit/byType/stableContainer/valid.test.ts b/packages/ssz/test/unit/byType/stableContainer/valid.test.ts new file mode 100644 index 00000000..90bfe688 --- /dev/null +++ b/packages/ssz/test/unit/byType/stableContainer/valid.test.ts @@ -0,0 +1,132 @@ +import {ListBasicType, OptionalType, StableContainerType, UintNumberType} from "../../../../src"; +import {runTypeTestValid} from "../runTypeTestValid"; + +// taken from eip spec tests + +const optionalUint16 = new OptionalType(new UintNumberType(2)); +const byteType = new UintNumberType(1); +const Shape1 = new StableContainerType( + { + side: optionalUint16, + color: byteType, + radius: optionalUint16, + }, + 4 +); + +const Shape2 = new StableContainerType( + { + side: optionalUint16, + color: byteType, + radius: optionalUint16, + }, + 8 +); + +const Shape3 = new StableContainerType( + { + side: optionalUint16, + colors: new OptionalType(new ListBasicType(byteType, 4)), + radius: optionalUint16, + }, + 8 +); + +runTypeTestValid({ + type: Shape1, + defaultValue: {side: null, color: 0, radius: null}, + values: [ + { + id: "shape1-0", + serialized: "0x074200014200", + json: {side: 0x42, color: 1, radius: 0x42}, + root: "0x37b28eab19bc3e246e55d2e2b2027479454c27ee006d92d4847c84893a162e6d", + }, + { + id: "shape1-1", + serialized: "0x03420001", + json: {side: 0x42, color: 1, radius: null}, + root: "0xbfdb6fda9d02805e640c0f5767b8d1bb9ff4211498a5e2d7c0f36e1b88ce57ff", + }, + { + id: "shape1-2", + serialized: "0x0201", + json: {side: null, color: 1, radius: null}, + root: "0x522edd7309c0041b8eb6a218d756af558e9cf4c816441ec7e6eef42dfa47bb98", + }, + { + id: "shape1-3", + serialized: "0x06014200", + json: {side: null, color: 1, radius: 0x42}, + root: "0xf66d2c38c8d2afbd409e86c529dff728e9a4208215ca20ee44e49c3d11e145d8", + }, + ], +}); +// +runTypeTestValid({ + type: Shape2, + defaultValue: {side: null, color: 0, radius: null}, + values: [ + { + id: "shape2-0", + serialized: "0x074200014200", + json: {side: 0x42, color: 1, radius: 0x42}, + root: "0x0792fb509377ee2ff3b953dd9a88eee11ac7566a8df41c6c67a85bc0b53efa4e", + }, + { + id: "shape2-1", + serialized: "0x03420001", + json: {side: 0x42, color: 1, radius: null}, + root: "0xddc7acd38ae9d6d6788c14bd7635aeb1d7694768d7e00e1795bb6d328ec14f28", + }, + { + id: "shape2-2", + serialized: "0x0201", + json: {side: null, color: 1, radius: null}, + root: "0x9893ecf9b68030ff23c667a5f2e4a76538a8e2ab48fd060a524888a66fb938c9", + }, + { + id: "shape2-3", + serialized: "0x06014200", + json: {side: null, color: 1, radius: 0x42}, + root: "0xe823471310312d52aa1135d971a3ed72ba041ade3ec5b5077c17a39d73ab17c5", + }, + ], +}); + +runTypeTestValid({ + type: Shape3, + defaultValue: {side: null, colors: null, radius: null}, + values: [ + { + id: "shape2-0", + serialized: "0x0742000800000042000102", + json: {side: 0x42, colors: [1, 2], radius: 0x42}, + root: "0x1093b0f1d88b1b2b458196fa860e0df7a7dc1837fe804b95d664279635cb302f", + }, + { + id: "shape2-1", + serialized: "0x014200", + json: {side: 0x42, colors: null, radius: null}, + root: "0x28df3f1c3eebd92504401b155c5cfe2f01c0604889e46ed3d22a3091dde1371f", + }, + { + id: "shape2-2", + serialized: "0x02040000000102", + json: {side: null, colors: [1, 2], radius: null}, + root: "0x659638368467b2c052ca698fcb65902e9b42ce8e94e1f794dd5296ceac2dec3e", + }, + { + id: "shape2-3", + serialized: "0x044200", + json: {side: null, colors: null, radius: 0x42}, + root: "0xd585dd0561c718bf4c29e4c1bd7d4efd4a5fe3c45942a7f778acb78fd0b2a4d2", + }, + { + id: "shape2-4", + serialized: "0x060600000042000102", + json: {side: null, colors: [1, 2], radius: 0x42}, + root: "0x00fc0cecc200a415a07372d5d5b8bc7ce49f52504ed3da0336f80a26d811c7bf", + }, + ], +});