@chainsafe/ssz
Version:
Simple Serialize
498 lines • 19.2 kB
JavaScript
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