UNPKG

@chainsafe/ssz

Version:
566 lines 22.8 kB
import { BranchNode, LeafNode, Tree, concatGindices, getHashComputations, getNode, setNode, zeroNode, } from "@chainsafe/persistent-merkle-tree"; import { byteArrayEquals } from "../util/byteArray.js"; import { cacheRoot, symbolCachedPermanentRoot } from "../util/merkleize.js"; import { namedClass } from "../util/named.js"; import { Case } from "../util/strings.js"; import { BitArray } from "../value/bitArray.js"; import { TreeView } from "../view/abstract.js"; import { TreeViewDU } from "../viewDU/abstract.js"; import { isBasicType } from "./basic.js"; import { CompositeType, isCompositeType, } from "./composite.js"; import { merkleizeProgressiveBytes, progressiveChunkGindex, progressiveSubtreeFillToContents } from "./progressive.js"; import { mixInActiveFields } from "./stableContainer.js"; const FIELDS_GINDEX = BigInt(2); /** * ProgressiveContainer: ordered heterogeneous collection with EIP-7916 progressive merkleization. * - Serialization is identical to Container over the active fields. * - Hash tree root mixes in the active-fields bitvector from the type definition. */ export class ProgressiveContainerType extends CompositeType { fields; opts; typeName; depth = 2; maxChunkCount; fixedSize; minSize; maxSize; isList = false; isViewMutable = true; fieldsEntries; activeFields; fieldsGindex; jsonKeyToFieldName; isFixedLen; fieldRangesFixedLen; variableOffsetsPosition; fixedEnd; TreeView; TreeViewDU; tempRoot = new Uint8Array(32); constructor(fields, activeFields, opts) { super(opts?.cachePermanentRootStruct); this.fields = fields; this.opts = opts; this.activeFields = Array.isArray(activeFields) ? BitArray.fromBoolArray(activeFields) : activeFields.clone(); validateActiveFields(this.activeFields, Object.keys(fields).length); this.typeName = opts?.typeName ?? renderProgressiveContainerTypeName(fields); this.maxChunkCount = this.activeFields.bitLen; const activeFieldIndexes = this.activeFields.getTrueBitIndexes(); const fieldNames = Object.keys(fields); this.fieldsEntries = []; for (let i = 0; i < fieldNames.length; i++) { const fieldName = fieldNames[i]; const chunkIndex = activeFieldIndexes[i]; this.fieldsEntries.push({ fieldName, fieldType: fields[fieldName], jsonKey: precomputeJsonKey(fieldName, opts?.casingMap, opts?.jsonCase), gindex: concatGindices([FIELDS_GINDEX, progressiveChunkGindex(chunkIndex)]), chunkIndex, }); } this.fieldsGindex = {}; for (const { fieldName, gindex } of this.fieldsEntries) { this.fieldsGindex[fieldName] = gindex; } 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; const { isFixedLen, fieldRangesFixedLen, variableOffsetsPosition, fixedEnd } = precomputeSerdesData(fields); this.isFixedLen = isFixedLen; this.fieldRangesFixedLen = fieldRangesFixedLen; this.variableOffsetsPosition = variableOffsetsPosition; this.fixedEnd = fixedEnd; this.TreeView = getProgressiveContainerTreeViewClass(this); this.TreeViewDU = getProgressiveContainerTreeViewDUClass(this); const fieldBytes = this.activeFields.bitLen * 32; this.blocksBuffer = new Uint8Array(Math.ceil(fieldBytes / 64) * 64); } static named(fields, activeFields, opts) { return new (namedClass(ProgressiveContainerType, opts.typeName))(fields, activeFields, opts); } defaultValue() { const value = {}; for (const { fieldName, fieldType } of this.fieldsEntries) { value[fieldName] = fieldType.defaultValue(); } return value; } getView(tree) { return new this.TreeView(this, tree); } getViewDU(node, cache) { return new this.TreeViewDU(this, node, cache); } cacheOfViewDU(view) { return view.cache; } commitView(view) { return view.node; } commitViewDU(view, hcOffset = 0, hcByLevel = null) { view.commit(hcOffset, hcByLevel); return view.node; } createFromProof(proof, root) { const rootNode = Tree.createFromProof(proof).rootNode; if (root !== undefined && !byteArrayEquals(rootNode.root, root)) { throw new Error("Proof does not match trusted root"); } return this.getView(new Tree(rootNode)); } value_serializedSize(value) { let totalSize = 0; for (const { fieldName, fieldType } of this.fieldsEntries) { totalSize += fieldType.fixedSize === null ? 4 + fieldType.value_serializedSize(value[fieldName]) : fieldType.fixedSize; } return totalSize; } value_serializeToBytes(output, offset, value) { let fixedIndex = offset; let variableIndex = offset + this.fixedEnd; for (const { fieldName, fieldType } of this.fieldsEntries) { if (fieldType.fixedSize === null) { output.dataView.setUint32(fixedIndex, variableIndex - offset, true); fixedIndex += 4; variableIndex = fieldType.value_serializeToBytes(output, variableIndex, value[fieldName]); } else { fixedIndex = fieldType.value_serializeToBytes(output, fixedIndex, value[fieldName]); } } return variableIndex; } value_deserializeFromBytes(data, start, end, reuseBytes) { const fieldRanges = this.getFieldRanges(data.dataView, start, end); const value = {}; for (let i = 0; i < this.fieldsEntries.length; i++) { const { fieldName, fieldType } = this.fieldsEntries[i]; const fieldRange = fieldRanges[i]; value[fieldName] = fieldType.value_deserializeFromBytes(data, start + fieldRange.start, start + fieldRange.end, reuseBytes); } return value; } tree_serializedSize(node) { let totalSize = 0; for (const { fieldType, gindex } of this.fieldsEntries) { const fieldNode = getNode(node, gindex); totalSize += fieldType.fixedSize === null ? 4 + fieldType.tree_serializedSize(fieldNode) : fieldType.fixedSize; } return totalSize; } tree_serializeToBytes(output, offset, node) { let fixedIndex = offset; let variableIndex = offset + this.fixedEnd; for (const { fieldType, gindex } of this.fieldsEntries) { const fieldNode = getNode(node, gindex); if (fieldType.fixedSize === null) { output.dataView.setUint32(fixedIndex, variableIndex - offset, true); fixedIndex += 4; variableIndex = fieldType.tree_serializeToBytes(output, variableIndex, fieldNode); } else { fixedIndex = fieldType.tree_serializeToBytes(output, fixedIndex, fieldNode); } } return variableIndex; } tree_deserializeFromBytes(data, start, end) { const fieldRanges = this.getFieldRanges(data.dataView, start, end); const nodes = new Array(this.activeFields.bitLen).fill(zeroNode(0)); for (let i = 0; i < this.fieldsEntries.length; i++) { const { fieldType, chunkIndex } = this.fieldsEntries[i]; const fieldRange = fieldRanges[i]; nodes[chunkIndex] = fieldType.tree_deserializeFromBytes(data, start + fieldRange.start, start + fieldRange.end); } return new BranchNode(progressiveSubtreeFillToContents(nodes), activeFieldsToNode(this.activeFields)); } hashTreeRootInto(value, output, offset, safeCache = false) { if (this.cachePermanentRootStruct) { const cachedRoot = value[symbolCachedPermanentRoot]; if (cachedRoot) { output.set(cachedRoot, offset); return; } } const blocksBytes = this.getBlocksBytes(value); merkleizeProgressiveBytes(blocksBytes, this.activeFields.bitLen, this.tempRoot, 0); mixInActiveFields(this.tempRoot, this.activeFields, output, offset); if (this.cachePermanentRootStruct) { cacheRoot(value, output, offset, safeCache); } } getBlocksBytes(struct) { this.blocksBuffer.fill(0); for (const { fieldName, fieldType, chunkIndex } of this.fieldsEntries) { fieldType.hashTreeRootInto(struct[fieldName], this.blocksBuffer, chunkIndex * 32); } return this.blocksBuffer; } getPropertyGindex(prop) { const gindex = this.fieldsGindex[prop] ?? this.fieldsGindex[this.jsonKeyToFieldName[prop]]; if (gindex === undefined) throw Error(`Unknown container property ${prop}`); return gindex; } getPropertyType(prop) { const fieldName = this.fields[prop] ? prop : this.jsonKeyToFieldName[prop]; const type = this.fields[fieldName]; if (type === undefined) throw Error(`Unknown container property ${prop}`); return type; } getIndexProperty(index) { const entry = this.fieldsEntries.find((entry) => entry.chunkIndex === index); return entry ? entry.fieldName : null; } tree_getLeafGindices(rootGindex, rootNode) { const gindices = []; for (const { fieldName, fieldType } of this.fieldsEntries) { const fieldGindex = this.fieldsGindex[fieldName]; const fieldGindexFromRoot = concatGindices([rootGindex, fieldGindex]); if (fieldType.isBasic) { gindices.push(fieldGindexFromRoot); } else { const compositeType = fieldType; 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; } tree_fromProofNode(node) { return { node, done: true }; } fromJson(json) { if (typeof json !== "object") throw Error("JSON must be of type object"); if (json === null) throw Error("JSON must not be null"); const value = {}; for (const { fieldName, fieldType, jsonKey } of this.fieldsEntries) { const jsonValue = json[jsonKey]; if (jsonValue === undefined) throw Error(`JSON expected key ${jsonKey} is undefined`); value[fieldName] = fieldType.fromJson(jsonValue); } return value; } toJson(value) { const json = {}; for (const { fieldName, fieldType, jsonKey } of this.fieldsEntries) { json[jsonKey] = fieldType.toJson(value[fieldName]); } return json; } clone(value) { const newValue = {}; for (const { fieldName, fieldType } of this.fieldsEntries) { newValue[fieldName] = fieldType.clone(value[fieldName]); } return newValue; } equals(a, b) { for (const { fieldName, fieldType } of this.fieldsEntries) { if (!fieldType.equals(a[fieldName], b[fieldName])) { return false; } } return true; } getFieldRanges(data, start, end) { if (this.variableOffsetsPosition.length === 0) { const size = end - start; if (size !== this.fixedEnd) { throw Error(`${this.typeName} size ${size} not equal fixed size ${this.fixedEnd}`); } return this.fieldRangesFixedLen; } const offsets = readVariableOffsets(data, start, end, this.fixedEnd, this.variableOffsetsPosition); offsets.push(end - start); let variableIdx = 0; let fixedIdx = 0; const fieldRanges = new Array(this.isFixedLen.length); for (let i = 0; i < this.isFixedLen.length; i++) { if (this.isFixedLen[i]) { fieldRanges[i] = this.fieldRangesFixedLen[fixedIdx++]; } else { fieldRanges[i] = { start: offsets[variableIdx], end: offsets[variableIdx + 1] }; variableIdx++; } } return fieldRanges; } } class ProgressiveContainerTreeView extends TreeView { type; tree; constructor(type, tree) { super(); this.type = type; this.tree = tree; } get node() { return this.tree.rootNode; } } class ProgressiveContainerTreeViewDU extends TreeViewDU { type; _rootNode; nodes = []; caches = []; nodesChanged = new Set(); viewsChanged = new Map(); constructor(type, _rootNode, cache) { super(); this.type = type; this._rootNode = _rootNode; if (cache) { this.nodes = cache.nodes; this.caches = cache.caches; } } get node() { return this._rootNode; } get cache() { return { nodes: this.nodes, caches: this.caches }; } commit(hcOffset = 0, hcByLevel = null) { for (const [index, view] of this.viewsChanged) { const { fieldType, gindex } = this.type.fieldsEntries[index]; const compositeType = fieldType; const node = compositeType.commitViewDU(view); this.nodes[index] = node; this.caches[index] = compositeType.cacheOfViewDU(view); this._rootNode = setNode(this._rootNode, gindex, node); } for (const index of this.nodesChanged) { this._rootNode = setNode(this._rootNode, this.type.fieldsEntries[index].gindex, this.nodes[index]); } this.nodesChanged.clear(); this.viewsChanged.clear(); if (hcByLevel !== null && this._rootNode.h0 === null) { getHashComputations(this._rootNode, hcOffset, hcByLevel); } } clearCache() { this.nodes = []; this.caches = []; this.nodesChanged.clear(); this.viewsChanged.clear(); } } function getProgressiveContainerTreeViewClass(containerType) { class CustomProgressiveContainerTreeView extends ProgressiveContainerTreeView { } for (const [index, { fieldName, fieldType, gindex }] of containerType.fieldsEntries.entries()) { if (isBasicType(fieldType)) { Object.defineProperty(CustomProgressiveContainerTreeView.prototype, fieldName, { configurable: false, enumerable: true, get: function () { return fieldType.tree_getFromNode(this.tree.getNode(gindex)); }, set: function (value) { const leafNode = this.tree.getNode(gindex).clone(); fieldType.tree_setToNode(leafNode, value); this.tree.setNode(gindex, leafNode); }, }); } else if (isCompositeType(fieldType)) { Object.defineProperty(CustomProgressiveContainerTreeView.prototype, fieldName, { configurable: false, enumerable: true, get: function () { return fieldType.getView(this.tree.getSubtree(gindex)); }, set: function (view) { this.tree.setNode(gindex, fieldType.commitView(view)); }, }); } else { throw Error(`Unknown fieldType ${fieldType.typeName} at index ${index}`); } } Object.defineProperty(CustomProgressiveContainerTreeView, "name", { value: containerType.typeName, writable: false }); return CustomProgressiveContainerTreeView; } function getProgressiveContainerTreeViewDUClass(containerType) { class CustomProgressiveContainerTreeViewDU extends ProgressiveContainerTreeViewDU { } for (const [index, { fieldName, fieldType, gindex }] of containerType.fieldsEntries.entries()) { if (isBasicType(fieldType)) { Object.defineProperty(CustomProgressiveContainerTreeViewDU.prototype, fieldName, { configurable: false, enumerable: true, get: function () { let node = this.nodes[index]; if (node === undefined) { node = getNode(this._rootNode, gindex); this.nodes[index] = node; } return fieldType.tree_getFromNode(node); }, set: function (value) { let nodeChanged; if (this.nodesChanged.has(index)) { nodeChanged = this.nodes[index]; } else { nodeChanged = (this.nodes[index] ?? getNode(this._rootNode, gindex)).clone(); this.nodes[index] = nodeChanged; this.nodesChanged.add(index); } fieldType.tree_setToNode(nodeChanged, value); }, }); } else if (isCompositeType(fieldType)) { Object.defineProperty(CustomProgressiveContainerTreeViewDU.prototype, fieldName, { configurable: false, enumerable: true, get: function () { const viewChanged = this.viewsChanged.get(index); if (viewChanged) { return viewChanged; } let node = this.nodes[index]; if (node === undefined) { node = getNode(this._rootNode, gindex); this.nodes[index] = node; } const view = fieldType.getViewDU(node, this.caches[index]); if (fieldType.isViewMutable) { this.viewsChanged.set(index, view); } return view; }, set: function (view) { this.viewsChanged.set(index, view); }, }); } else { throw Error(`Unknown fieldType ${fieldType.typeName} at index ${index}`); } } Object.defineProperty(CustomProgressiveContainerTreeViewDU, "name", { value: containerType.typeName, writable: false }); return CustomProgressiveContainerTreeViewDU; } function validateActiveFields(activeFields, fieldCount) { if (activeFields.bitLen === 0) { throw Error("ProgressiveContainer activeFields must not be empty"); } if (activeFields.bitLen > 256) { throw Error("ProgressiveContainer activeFields bit length must be <= 256"); } if (!activeFields.get(activeFields.bitLen - 1)) { throw Error("ProgressiveContainer activeFields must not end in false"); } if (activeFields.getTrueBitIndexes().length !== fieldCount) { throw Error("ProgressiveContainer activeFields true bit count must equal field count"); } if (fieldCount === 0) { throw Error("ProgressiveContainer must have > 0 fields"); } } function activeFieldsToNode(activeFields) { const root = new Uint8Array(32); root.set(activeFields.uint8Array); return LeafNode.fromRoot(root); } function readVariableOffsets(data, start, end, fixedEnd, variableOffsetsPosition) { const size = end - start; const offsets = new Array(variableOffsetsPosition.length); for (let i = 0; i < variableOffsetsPosition.length; i++) { const offset = data.getUint32(start + variableOffsetsPosition[i], true); 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; } function precomputeSerdesData(fields) { const isFixedLen = []; const fieldRangesFixedLen = []; const variableOffsetsPosition = []; let pointerFixed = 0; for (const fieldType of Object.values(fields)) { isFixedLen.push(fieldType.fixedSize !== null); if (fieldType.fixedSize === null) { variableOffsetsPosition.push(pointerFixed); pointerFixed += 4; } else { fieldRangesFixedLen.push({ start: pointerFixed, end: pointerFixed + fieldType.fixedSize }); pointerFixed += fieldType.fixedSize; } } return { isFixedLen, fieldRangesFixedLen, variableOffsetsPosition, fixedEnd: pointerFixed }; } function precomputeSizes(fields) { let minLen = 0; let maxLen = 0; let fixedSize = 0; for (const fieldType of Object.values(fields)) { minLen += fieldType.minSize; maxLen += fieldType.maxSize; if (fieldType.fixedSize === null) { minLen += 4; maxLen += 4; fixedSize = null; } else if (fixedSize !== null) { fixedSize += fieldType.fixedSize; } } return { minLen, maxLen, fixedSize }; } function precomputeJsonKey(fieldName, casingMap, jsonCase) { if (casingMap) { const keyFromCaseMap = casingMap[fieldName]; if (keyFromCaseMap === undefined) { throw Error(`casingMap[${String(fieldName)}] not defined`); } return keyFromCaseMap; } if (jsonCase) return Case[jsonCase](fieldName); return fieldName; } function renderProgressiveContainerTypeName(fields) { const fieldNames = Object.keys(fields); const fieldTypeNames = fieldNames .map((fieldName) => `${String(fieldName)}: ${fields[fieldName].typeName}`) .join(", "); return `ProgressiveContainer({${fieldTypeNames}})`; } //# sourceMappingURL=progressiveContainer.js.map