UNPKG

@chainsafe/ssz

Version:
498 lines 19.2 kB
import { allocUnsafe } from "@chainsafe/as-sha256"; import { ProofType, Tree, concatGindices, createProof, getHashComputations, getNode, merkleizeBlocksBytes, } from "@chainsafe/persistent-merkle-tree"; import { byteArrayEquals } from "../util/byteArray.js"; import { namedClass } from "../util/named.js"; import { TreeView } from "../view/abstract.js"; import { TreeViewDU } from "../viewDU/abstract.js"; import { addLengthNode, getLengthFromRootNode } from "./arrayBasic.js"; import { isBasicType } from "./basic.js"; import { BitListType } from "./bitList.js"; import { BitVectorType } from "./bitVector.js"; import { BooleanType } from "./boolean.js"; import { ByteListType } from "./byteList.js"; import { ByteVectorType } from "./byteVector.js"; import { CompositeType, isCompositeType } from "./composite.js"; import { ContainerType } from "./container.js"; import { ListBasicType } from "./listBasic.js"; import { ListCompositeType } from "./listComposite.js"; import { ProgressiveBitListType } from "./progressiveBitList.js"; import { ProgressiveByteListType } from "./progressiveByteList.js"; import { ProgressiveContainerType } from "./progressiveContainer.js"; import { ProgressiveListBasicType, ProgressiveListCompositeType } from "./progressiveList.js"; import { UintBigintType, UintNumberType } from "./uint.js"; import { VectorBasicType } from "./vectorBasic.js"; import { VectorCompositeType } from "./vectorComposite.js"; const VALUE_GINDEX = BigInt(2); const SELECTOR_GINDEX = BigInt(3); /** * CompatibleUnion: union type containing one of the given subtypes with compatible Merkleization. * - Notation: CompatibleUnion({selector: type}), e.g. CompatibleUnion({1: Square, 2: Circle}) * * The right leaf encodes the selector and reuses existing list length-node helpers for tree operations. */ export class CompatibleUnionType extends CompositeType { typeName; depth = 1; maxChunkCount = 1; fixedSize = null; minSize; maxSize; isList = true; isViewMutable = true; mixInSelectorBlockBytes = new Uint8Array(64); mixInSelectorBuffer = Buffer.from(this.mixInSelectorBlockBytes.buffer, this.mixInSelectorBlockBytes.byteOffset, this.mixInSelectorBlockBytes.byteLength); selectors; selectorToType; representativeType; selectorType = new UintNumberType(1); constructor(options, opts) { super(); const selectorToType = {}; const selectors = Object.keys(options) .map((selector) => Number(selector)) .sort((a, b) => a - b); if (selectors.length === 0) { throw Error("CompatibleUnion must have at least one type option"); } for (const selector of selectors) { if (!Number.isSafeInteger(selector) || selector < 1 || selector > 127) { throw Error(`CompatibleUnion selector ${selector} must be in range 1..127`); } selectorToType[selector] = options[selector]; } for (let i = 0; i < selectors.length; i++) { for (let j = i + 1; j < selectors.length; j++) { const selectorA = selectors[i]; const selectorB = selectors[j]; if (!areTypesCompatible(selectorToType[selectorA], selectorToType[selectorB])) { throw Error(`CompatibleUnion options ${selectorA} and ${selectorB} are not compatible`); } } } this.selectorToType = selectorToType; this.selectors = selectors; this.representativeType = selectorToType[selectors[0]]; this.typeName = opts?.typeName ?? `CompatibleUnion({${selectors.map((selector) => `${selector}: ${selectorToType[selector].typeName}`).join(",")}})`; this.minSize = 1 + Math.min(...selectors.map((selector) => selectorToType[selector].minSize)); this.maxSize = 1 + Math.max(...selectors.map((selector) => selectorToType[selector].maxSize)); this.blocksBuffer = new Uint8Array(32); } static named(options, opts) { return new (namedClass(CompatibleUnionType, opts.typeName))(options, opts); } defaultValue() { throw Error("CompatibleUnion does not define a default value"); } getView(tree) { return new CompatibleUnionTreeView(this, tree); } getViewDU(node) { return new CompatibleUnionTreeViewDU(this, node); } cacheOfViewDU() { return; } commitView(view) { return view.node; } commitViewDU(view, hcOffset = 0, hcByLevel = null) { view.commit(hcOffset, hcByLevel); return view.node; } value_serializedSize(value) { return 1 + this.getType(value.selector).value_serializedSize(value.data); } value_serializeToBytes(output, offset, value) { output.uint8Array[offset] = value.selector; return this.getType(value.selector).value_serializeToBytes(output, offset + 1, value.data); } value_deserializeFromBytes(data, start, end, reuseBytes) { if (end <= start) { throw Error("CompatibleUnion requires a selector byte"); } const selector = data.uint8Array[start]; const type = this.getType(selector); return { selector, data: type.value_deserializeFromBytes(data, start + 1, end, reuseBytes), }; } tree_serializedSize(node) { const selector = getLengthFromRootNode(node); return 1 + this.getType(selector).tree_serializedSize(node.left); } tree_serializeToBytes(output, offset, node) { const selector = getLengthFromRootNode(node); output.uint8Array[offset] = selector; return this.getType(selector).tree_serializeToBytes(output, offset + 1, node.left); } tree_deserializeFromBytes(data, start, end) { if (end <= start) { throw Error("CompatibleUnion requires a selector byte"); } const selector = data.uint8Array[start]; const valueNode = this.getType(selector).tree_deserializeFromBytes(data, start + 1, end); return addLengthNode(valueNode, selector); } hashTreeRoot(value) { const root = allocUnsafe(32); this.hashTreeRootInto(value, root, 0); return root; } hashTreeRootInto(value, output, offset) { const type = this.getType(value.selector); type.hashTreeRootInto(value.data, this.mixInSelectorBlockBytes, 0); this.mixInSelectorBlockBytes.subarray(32).fill(0); this.mixInSelectorBuffer.writeUIntLE(value.selector, 32, 6); merkleizeBlocksBytes(this.mixInSelectorBlockBytes, 2, output, offset); } getBlocksBytes(value) { this.getType(value.selector).hashTreeRootInto(value.data, this.blocksBuffer, 0); return this.blocksBuffer; } getPropertyGindex(prop) { switch (prop) { case "data": return VALUE_GINDEX; case "selector": return SELECTOR_GINDEX; default: throw new Error(`Invalid CompatibleUnion type property ${prop}`); } } 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)); } tree_createProof(node, jsonPaths) { const gindices = this.tree_createProofGindexes(node, jsonPaths); return createProof(node, { type: ProofType.treeOffset, gindices, }); } tree_createProofGindexes(node, jsonPaths) { const gindices = []; const selector = getLengthFromRootNode(node); const selectedType = this.getType(selector); const dataNode = getNode(node, VALUE_GINDEX); for (const jsonPath of jsonPaths) { const [prop, ...remainingPath] = jsonPath; if (prop === undefined) { gindices.push(...this.tree_getLeafGindices(BigInt(1), node)); continue; } if (prop === "selector") { if (remainingPath.length > 0) { throw Error("Invalid path: cannot navigate beyond CompatibleUnion selector"); } gindices.push(SELECTOR_GINDEX); continue; } if (prop !== "data") { throw Error(`Invalid CompatibleUnion type property ${String(prop)}`); } if (remainingPath.length === 0) { if (isCompositeType(selectedType)) { gindices.push(...selectedType.tree_getLeafGindices(VALUE_GINDEX, selectedType.fixedSize === null ? dataNode : undefined)); } else { gindices.push(VALUE_GINDEX); } continue; } if (!isCompositeType(selectedType)) { throw Error("Invalid path: cannot navigate beyond CompatibleUnion basic data"); } const childGindices = selectedType.tree_createProofGindexes(dataNode, [remainingPath]); for (const childGindex of childGindices) { gindices.push(concatGindices([VALUE_GINDEX, childGindex])); } } return gindices; } getPropertyType(prop) { switch (prop) { case "selector": return this.selectorType; case "data": return this.representativeType; default: throw new Error(`Invalid CompatibleUnion type property ${prop}`); } } getIndexProperty(index) { if (index === 0) return "data"; if (index === 1) return "selector"; throw Error("CompatibleUnion index of out bounds"); } tree_getLeafGindices(rootGindex, rootNode) { if (!rootNode) { throw Error("rootNode required"); } const selector = getLengthFromRootNode(rootNode); const type = this.getType(selector); const valueGindex = concatGindices([rootGindex, VALUE_GINDEX]); const gindices = []; if (isCompositeType(type)) { gindices.push(...type.tree_getLeafGindices(valueGindex, getNode(rootNode, VALUE_GINDEX))); } else { gindices.push(valueGindex); } gindices.push(concatGindices([rootGindex, SELECTOR_GINDEX])); return gindices; } tree_fromProofNode(node) { return { node, done: true }; } fromJson(json) { if (typeof json !== "object" || json === null) { throw new Error("JSON must be of type object"); } const union = json; const selector = parseSelector(union.selector); const type = this.getType(selector); if (!("data" in union)) { throw new Error("JSON CompatibleUnion missing data"); } return { selector, data: type.fromJson(union.data), }; } toJson(value) { return { selector: value.selector.toString(10), data: this.getType(value.selector).toJson(value.data), }; } clone(value) { return { selector: value.selector, data: this.getType(value.selector).clone(value.data), }; } equals(a, b) { if (a.selector !== b.selector) { return false; } return this.getType(a.selector).equals(a.data, b.data); } getType(selector) { const type = this.selectorToType[selector]; if (type === undefined) { throw Error(`Invalid CompatibleUnion selector ${selector}`); } return type; } } export class CompatibleUnionTreeView extends TreeView { type; tree; constructor(type, tree) { super(); this.type = type; this.tree = tree; } get node() { return this.tree.rootNode; } get selector() { return getLengthFromRootNode(this.tree.rootNode); } get data() { const selector = this.selector; return this.type.getType(selector).tree_toValue(this.tree.rootNode.left); } } export class CompatibleUnionTreeViewDU extends TreeViewDU { type; _rootNode; constructor(type, _rootNode) { super(); this.type = type; this._rootNode = _rootNode; } get node() { return this._rootNode; } get cache() { return undefined; } get selector() { return getLengthFromRootNode(this._rootNode); } get data() { const selector = this.selector; return this.type.getType(selector).tree_toValue(this._rootNode.left); } commit(hcOffset = 0, hcByLevel = null) { if (hcByLevel !== null && this._rootNode.h0 === null) { getHashComputations(this._rootNode, hcOffset, hcByLevel); } } clearCache() { return; } } export function areTypesCompatible(a, b) { if (a === b) { return true; } if (a instanceof CompatibleUnionType && b instanceof CompatibleUnionType) { return a.selectors.every((selectorA) => b.selectors.every((selectorB) => areTypesCompatible(a.getType(selectorA), b.getType(selectorB)))); } if (isBasicType(a) && isBasicType(b)) { return areBasicTypesCompatible(a, b); } if (a instanceof BitListType && b instanceof BitListType) { return a.limitBits === b.limitBits; } if (a instanceof BitVectorType && b instanceof BitVectorType) { return a.lengthBits === b.lengthBits; } if (a instanceof ProgressiveBitListType && b instanceof ProgressiveBitListType) { return true; } if (isByteListCompatibleType(a) && isByteListCompatibleType(b)) { return getByteListCompatibleLimit(a) === getByteListCompatibleLimit(b); } if (isByteVectorCompatibleType(a) && isByteVectorCompatibleType(b)) { return getByteVectorCompatibleLength(a) === getByteVectorCompatibleLength(b); } if (isProgressiveByteListCompatibleType(a) && isProgressiveByteListCompatibleType(b)) { return true; } if (isLimitedListType(a) && isLimitedListType(b)) { return a.limit === b.limit && areTypesCompatible(a.elementType, b.elementType); } if (isVectorType(a) && isVectorType(b)) { return a.length === b.length && areTypesCompatible(a.elementType, b.elementType); } if (isProgressiveListType(a) && isProgressiveListType(b)) { return areTypesCompatible(a.elementType, b.elementType); } if (a instanceof ContainerType && b instanceof ContainerType) { return areContainersCompatible(a, b); } if (a instanceof ProgressiveContainerType && b instanceof ProgressiveContainerType) { return areProgressiveContainersCompatible(a, b); } return false; } function parseSelector(selector) { if (typeof selector === "number") { if (Number.isSafeInteger(selector)) { return selector; } } else if (typeof selector === "bigint") { if (selector >= BigInt(Number.MIN_SAFE_INTEGER) && selector <= BigInt(Number.MAX_SAFE_INTEGER)) { return Number(selector); } } else if (typeof selector === "string") { const parsed = Number(selector); if (Number.isSafeInteger(parsed) && parsed.toString(10) === selector) { return parsed; } } throw Error("Invalid JSON CompatibleUnion selector"); } function areBasicTypesCompatible(a, b) { if (a instanceof BooleanType || b instanceof BooleanType) { return a instanceof BooleanType && b instanceof BooleanType; } if (isUintType(a) && isUintType(b)) { return a.byteLength === b.byteLength; } return a.constructor === b.constructor && a.byteLength === b.byteLength; } function isUintType(type) { return type instanceof UintNumberType || type instanceof UintBigintType; } function isByteBasicType(type) { return isUintType(type) && type.byteLength === 1; } function isByteListCompatibleType(type) { return type instanceof ByteListType || (type instanceof ListBasicType && isByteBasicType(type.elementType)); } function getByteListCompatibleLimit(type) { return type instanceof ByteListType ? type.limitBytes : type.limit; } function isByteVectorCompatibleType(type) { return type instanceof ByteVectorType || (type instanceof VectorBasicType && isByteBasicType(type.elementType)); } function getByteVectorCompatibleLength(type) { return type instanceof ByteVectorType ? type.lengthBytes : type.length; } function isProgressiveByteListCompatibleType(type) { return (type instanceof ProgressiveByteListType || (type instanceof ProgressiveListBasicType && isByteBasicType(type.elementType))); } function isLimitedListType(type) { return type instanceof ListBasicType || type instanceof ListCompositeType; } function isVectorType(type) { return type instanceof VectorBasicType || type instanceof VectorCompositeType; } function isProgressiveListType(type) { return type instanceof ProgressiveListBasicType || type instanceof ProgressiveListCompositeType; } function areContainersCompatible(a, b) { if (a.fieldsEntries.length !== b.fieldsEntries.length) { return false; } for (let i = 0; i < a.fieldsEntries.length; i++) { const fieldA = a.fieldsEntries[i]; const fieldB = b.fieldsEntries[i]; if (fieldA.fieldName !== fieldB.fieldName || !areTypesCompatible(fieldA.fieldType, fieldB.fieldType)) { return false; } } return true; } function areProgressiveContainersCompatible(a, b) { const fieldsA = progressiveContainerFieldMap(a); const fieldsB = progressiveContainerFieldMap(b); const maxActiveFields = Math.max(a.activeFields.bitLen, b.activeFields.bitLen); for (let i = 0; i < maxActiveFields; i++) { if (a.activeFields.get(i) && b.activeFields.get(i)) { const fieldA = a.getIndexProperty(i); const fieldB = b.getIndexProperty(i); if (fieldA === null || fieldB === null || fieldA !== fieldB) { return false; } const fieldInfoA = fieldsA.get(fieldA); const fieldInfoB = fieldsB.get(fieldB); if (fieldInfoA === undefined || fieldInfoB === undefined || !areTypesCompatible(fieldInfoA.type, fieldInfoB.type)) { return false; } } } for (const [fieldName, fieldA] of fieldsA) { const fieldB = fieldsB.get(fieldName); if (fieldB !== undefined && fieldA.chunkIndex !== fieldB.chunkIndex) { return false; } } return true; } function progressiveContainerFieldMap(type) { const fields = new Map(); for (const entry of type.fieldsEntries) { fields.set(entry.fieldName, { chunkIndex: entry.chunkIndex, type: entry.fieldType }); } return fields; } //# sourceMappingURL=compatibleUnion.js.map