@chainsafe/ssz
Version:
Simple Serialize
159 lines • 7.47 kB
JavaScript
import { getNodeAtDepth, toGindexBitstring, zeroNode, } from "@chainsafe/persistent-merkle-tree";
import { isBasicType } from "../type/basic.js";
import { isCompositeType } from "../type/composite.js";
import { TreeView } from "./abstract.js";
/**
* 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 {
type;
tree;
constructor(type, tree) {
super();
this.type = type;
this.tree = tree;
}
get node() {
return this.tree.rootNode;
}
}
export function getContainerTreeViewClass(type) {
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 () {
const leafNode = getNodeAtDepth(this.node, this.type.depth, index);
if (optional && this.type.tree_getActiveField(this.tree.rootNode, index) === false) {
return null;
}
return fieldType.tree_getFromNode(leafNode);
},
set: function (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);
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 () {
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 (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 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 ${String(fieldName)}`);
}
}
// Change class name
Object.defineProperty(CustomContainerTreeView, "name", { value: type.typeName, writable: false });
return CustomContainerTreeView;
}
/**
* 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, fields) {
const isFixedLen = [];
const fieldRangesFixedLen = [];
const variableOffsetsPosition = [];
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,
};
}
//# sourceMappingURL=stableContainer.js.map