@chainsafe/ssz
Version:
Simple Serialize
153 lines • 6.62 kB
JavaScript
import { allocUnsafe } from "@chainsafe/as-sha256";
import { getNodesAtDepth, merkleizeBlocksBytes, packedNodeRootsToBytes, packedRootsBytesToNode, } from "@chainsafe/persistent-merkle-tree";
import { slice } from "../util/byteArray.js";
import { maxChunksToDepth } from "../util/merkleize.js";
import { namedClass } from "../util/named.js";
import { BitArray } from "../value/bitArray.js";
import { addLengthNode, getChunksNodeFromRootNode, getLengthFromRootNode } from "./arrayBasic.js";
import { BitArrayType } from "./bitArray.js";
/**
* BitList: ordered variable-length collection of boolean values, limited to N bits
* - Notation `Bitlist[N]`
* - Value: `BitArray`, @see BitArray for a justification of its memory efficiency and performance
* - View: `BitArrayTreeView`
* - ViewDU: `BitArrayTreeViewDU`
*/
export class BitListType extends BitArrayType {
limitBits;
typeName;
depth;
chunkDepth;
fixedSize = null;
minSize = 1; // +1 for the extra padding bit
maxSize;
maxChunkCount;
isList = true;
mixInLengthBlockBytes = new Uint8Array(64);
mixInLengthBuffer = Buffer.from(this.mixInLengthBlockBytes.buffer, this.mixInLengthBlockBytes.byteOffset, this.mixInLengthBlockBytes.byteLength);
constructor(limitBits, opts) {
super();
this.limitBits = limitBits;
if (limitBits === 0)
throw Error("List limit must be > 0");
this.typeName = opts?.typeName ?? `BitList[${limitBits}]`;
// TODO Check that itemsPerChunk is an integer
this.maxChunkCount = Math.ceil(this.limitBits / 8 / 32);
this.chunkDepth = maxChunksToDepth(this.maxChunkCount);
// Depth includes the extra level for the length node
this.depth = 1 + this.chunkDepth;
this.maxSize = Math.ceil(limitBits / 8) + 1; // +1 for the extra padding bit
}
static named(limitBits, opts) {
return new (namedClass(BitListType, opts.typeName))(limitBits, opts);
}
defaultValue() {
return BitArray.fromBitLen(0);
}
// Views: inherited from BitArrayType
// Serialization + deserialization
value_serializedSize(value) {
return bitLenToSerializedLength(value.bitLen);
}
value_serializeToBytes(output, offset, value) {
output.uint8Array.set(value.uint8Array, offset);
return applyPaddingBit(output.uint8Array, offset, value.bitLen);
}
value_deserializeFromBytes(data, start, end, reuseBytes) {
const { uint8Array, bitLen } = this.deserializeUint8ArrayBitListFromBytes(data.uint8Array, start, end, reuseBytes);
return new BitArray(uint8Array, bitLen);
}
tree_serializedSize(node) {
return bitLenToSerializedLength(getLengthFromRootNode(node));
}
tree_serializeToBytes(output, offset, node) {
const chunksNode = getChunksNodeFromRootNode(node);
const bitLen = getLengthFromRootNode(node);
const byteLen = Math.ceil(bitLen / 8);
const chunkLen = Math.ceil(byteLen / 32);
const nodes = getNodesAtDepth(chunksNode, this.chunkDepth, 0, chunkLen);
packedNodeRootsToBytes(output.dataView, offset, byteLen, nodes);
return applyPaddingBit(output.uint8Array, offset, bitLen);
}
tree_deserializeFromBytes(data, start, end) {
const { uint8Array, bitLen } = this.deserializeUint8ArrayBitListFromBytes(data.uint8Array, start, end);
const dataView = new DataView(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteLength);
const chunksNode = packedRootsBytesToNode(this.chunkDepth, dataView, 0, uint8Array.length);
return addLengthNode(chunksNode, bitLen);
}
tree_getByteLen(node) {
if (!node)
throw new Error("BitListType requires a node to get leaves");
return Math.ceil(getLengthFromRootNode(node) / 8);
}
// Merkleization: inherited from BitArrayType
hashTreeRoot(value) {
const root = allocUnsafe(32);
this.hashTreeRootInto(value, root, 0);
return root;
}
hashTreeRootInto(value, output, offset) {
super.hashTreeRootInto(value, this.mixInLengthBlockBytes, 0);
// mixInLength
this.mixInLengthBuffer.writeUIntLE(value.bitLen, 32, 6);
// one for hashTreeRoot(value), one for length
const chunkCount = 2;
merkleizeBlocksBytes(this.mixInLengthBlockBytes, chunkCount, output, offset);
}
// Proofs: inherited from BitArrayType
// JSON: inherited from BitArrayType
// Deserializer helpers
deserializeUint8ArrayBitListFromBytes(data, start, end, reuseBytes) {
const { uint8Array, bitLen } = deserializeUint8ArrayBitListFromBytes(data, start, end, reuseBytes);
if (bitLen > this.limitBits) {
throw Error(`bitLen over limit ${bitLen} > ${this.limitBits}`);
}
return { uint8Array, bitLen };
}
}
export function deserializeUint8ArrayBitListFromBytes(data, start, end, reuseBytes) {
if (end > data.length) {
throw Error(`BitList attempting to read byte ${end} of data length ${data.length}`);
}
if (end <= start) {
throw Error("BitList requires a padding bit");
}
const lastByte = data[end - 1];
const size = end - start;
if (lastByte === 0) {
throw new Error("Invalid deserialized bitlist, padding bit required");
}
if (lastByte === 1) {
const uint8Array = slice(data, start, end - 1, reuseBytes);
const bitLen = (size - 1) * 8;
return { uint8Array, bitLen };
}
// the last byte is > 1, so a padding bit will exist in the last byte and need to be removed
const uint8Array = slice(data, start, end, reuseBytes);
// mask lastChunkByte
const lastByteBitLength = lastByte.toString(2).length - 1;
const bitLen = (size - 1) * 8 + lastByteBitLength;
const mask = 0xff >> (8 - lastByteBitLength);
uint8Array[size - 1] &= mask;
return { uint8Array, bitLen };
}
function bitLenToSerializedLength(bitLen) {
const bytes = Math.ceil(bitLen / 8);
// +1 for the extra padding bit
return bitLen % 8 === 0 ? bytes + 1 : bytes;
}
/**
* Apply padding bit to a serialized BitList already written to `output` at `offset`
* @returns New offset after (maybe) writting a padding bit.
*/
function applyPaddingBit(output, offset, bitLen) {
const byteLen = Math.ceil(bitLen / 8);
const newOffset = offset + byteLen;
if (bitLen % 8 === 0) {
output[newOffset] = 1;
return newOffset + 1;
}
output[newOffset - 1] |= 1 << (bitLen % 8);
return newOffset;
}
//# sourceMappingURL=bitList.js.map