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