@chainsafe/ssz
Version:
Simple Serialize
173 lines • 7.53 kB
JavaScript
import { allocUnsafe } from "@chainsafe/as-sha256";
import { merkleizeBlockArray, merkleizeBlocksBytes, } from "@chainsafe/persistent-merkle-tree";
import { cacheRoot, maxChunksToDepth, symbolCachedPermanentRoot, } from "../util/merkleize.js";
import { namedClass } from "../util/named.js";
import { ListCompositeTreeView } from "../view/listComposite.js";
import { ListCompositeTreeViewDU } from "../viewDU/listComposite.js";
import { ArrayType } from "./array.js";
import { addLengthNode, getLengthFromRootNode, setChunksNode } from "./arrayBasic.js";
import { maxSizeArrayComposite, tree_deserializeFromBytesArrayComposite, tree_serializeToBytesArrayComposite, tree_serializedSizeArrayComposite, value_deserializeFromBytesArrayComposite, value_serializeToBytesArrayComposite, value_serializedSizeArrayComposite, } from "./arrayComposite.js";
/**
* List: ordered variable-length homogeneous collection, limited to N values
*
* Array of Composite type:
* - Composite types always take at least one chunk
* - Composite types are always returned as views
*/
export class ListCompositeType extends ArrayType {
elementType;
limit;
typeName;
itemsPerChunk = 1;
depth;
chunkDepth;
maxChunkCount;
fixedSize = null;
minSize;
maxSize;
isList = true;
isViewMutable = true;
blockArray = [];
mixInLengthBlockBytes = new Uint8Array(64);
mixInLengthBuffer = Buffer.from(this.mixInLengthBlockBytes.buffer, this.mixInLengthBlockBytes.byteOffset, this.mixInLengthBlockBytes.byteLength);
defaultLen = 0;
constructor(elementType, limit, opts) {
super(elementType, opts?.cachePermanentRootStruct);
this.elementType = elementType;
this.limit = limit;
if (elementType.isBasic)
throw Error("elementType must not be basic");
if (limit === 0)
throw Error("List limit must be > 0");
this.typeName = opts?.typeName ?? `List[${elementType.typeName}, ${limit}]`;
this.maxChunkCount = this.limit;
this.chunkDepth = maxChunksToDepth(this.maxChunkCount);
// Depth includes the extra level for the length node
this.depth = this.chunkDepth + 1;
this.minSize = 0;
this.maxSize = maxSizeArrayComposite(elementType, this.limit);
}
// biome-ignore lint/suspicious/noExplicitAny: We need to use `any` here explicitly
static named(elementType, limit, opts) {
return new (namedClass(ListCompositeType, opts.typeName))(elementType, limit, opts);
}
getView(tree) {
return new ListCompositeTreeView(this, tree);
}
getViewDU(node, cache) {
// cache type should be validated (if applicate) in the view
// biome-ignore lint/suspicious/noExplicitAny: We need to use `any` here explicitly
return new ListCompositeTreeViewDU(this, node, cache);
}
commitView(view) {
return view.node;
}
commitViewDU(view, hcOffset = 0, hcByLevel = null) {
view.commit(hcOffset, hcByLevel);
return view.node;
}
cacheOfViewDU(view) {
return view.cache;
}
// Serialization + deserialization
value_serializedSize(value) {
return value_serializedSizeArrayComposite(this.elementType, value.length, value);
}
value_serializeToBytes(output, offset, value) {
return value_serializeToBytesArrayComposite(this.elementType, value.length, output, offset, value);
}
value_deserializeFromBytes(data, start, end) {
return value_deserializeFromBytesArrayComposite(this.elementType, data, start, end, this);
}
tree_serializedSize(node) {
const chunksNode = this.tree_getChunksNode(node);
const length = this.tree_getLength(node);
return tree_serializedSizeArrayComposite(this.elementType, length, this.chunkDepth, chunksNode);
}
tree_serializeToBytes(output, offset, node) {
const chunksNode = this.tree_getChunksNode(node);
const length = this.tree_getLength(node);
return tree_serializeToBytesArrayComposite(this.elementType, length, this.chunkDepth, chunksNode, output, offset);
}
tree_deserializeFromBytes(data, start, end) {
return tree_deserializeFromBytesArrayComposite(this.elementType, this.chunkDepth, data, start, end, this);
}
// Helpers for TreeView
tree_getLength(node) {
return getLengthFromRootNode(node);
}
tree_setLength(tree, length) {
tree.rootNode = addLengthNode(tree.rootNode.left, length);
}
tree_getChunksNode(node) {
return node.left;
}
tree_chunksNodeOffset() {
// one more level for length, see setChunksNode below
return 1;
}
tree_setChunksNode(rootNode, chunksNode, newLength, hcOffset = 0, hcByLevel = null) {
return setChunksNode(rootNode, chunksNode, newLength, hcOffset, hcByLevel);
}
// Merkleization
hashTreeRoot(value) {
// Return cached mutable root if any
if (this.cachePermanentRootStruct) {
const cachedRoot = value[symbolCachedPermanentRoot];
if (cachedRoot) {
return cachedRoot;
}
}
const root = allocUnsafe(32);
const safeCache = true;
this.hashTreeRootInto(value, root, 0, safeCache);
// hashTreeRootInto will cache the root if cachePermanentRootStruct is true
return root;
}
hashTreeRootInto(value, output, offset, safeCache = false) {
if (this.cachePermanentRootStruct) {
const cachedRoot = value[symbolCachedPermanentRoot];
if (cachedRoot) {
output.set(cachedRoot, offset);
return;
}
}
// should not call super.hashTreeRootInto() here
// use merkleizeBlockArray() instead of merkleizeBlocksBytes() to avoid big memory allocation
// reallocate this.blockArray if needed
if (value.length > this.blockArray.length) {
const blockDiff = value.length - this.blockArray.length;
const newBlocksBytes = new Uint8Array(blockDiff * 64);
for (let i = 0; i < blockDiff; i++) {
this.blockArray.push(newBlocksBytes.subarray(i * 64, (i + 1) * 64));
}
}
// populate this.blockArray
for (let i = 0; i < value.length; i++) {
// 2 values share a block
const block = this.blockArray[Math.floor(i / 2)];
const offset = i % 2 === 0 ? 0 : 32;
this.elementType.hashTreeRootInto(value[i], block, offset);
}
const blockLimit = Math.ceil(value.length / 2);
// zero out the last block if needed
if (value.length % 2 === 1) {
this.blockArray[blockLimit - 1].fill(0, 32);
}
// compute hashTreeRoot
merkleizeBlockArray(this.blockArray, blockLimit, this.maxChunkCount, this.mixInLengthBlockBytes, 0);
// mixInLength
this.mixInLengthBuffer.writeUIntLE(value.length, 32, 6);
// one for hashTreeRoot(value), one for length
const chunkCount = 2;
merkleizeBlocksBytes(this.mixInLengthBlockBytes, chunkCount, output, offset);
if (this.cachePermanentRootStruct) {
cacheRoot(value, output, offset, safeCache);
}
}
getBlocksBytes() {
// we use merkleizeBlockArray for hashTreeRoot() computation
throw Error("getBlockBytes should not be called for ListCompositeType");
}
}
//# sourceMappingURL=listComposite.js.map