Skip to content

Commit 9e32c5c

Browse files
committed
feat: consume merkleizeBlockArray
1 parent a90a99d commit 9e32c5c

19 files changed

+277
-219
lines changed

packages/ssz/src/type/arrayComposite.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -211,29 +211,29 @@ export function tree_deserializeFromBytesArrayComposite<ElementType extends Comp
211211
}
212212
}
213213

214-
export function value_getChunkBytesArrayComposite<ElementType extends CompositeType<unknown, unknown, unknown>>(
214+
export function value_getBlocksBytesArrayComposite<ElementType extends CompositeType<unknown, unknown, unknown>>(
215215
elementType: ElementType,
216216
length: number,
217217
value: ValueOf<ElementType>[],
218-
chunkBytesBuffer: Uint8Array
218+
blocksBuffer: Uint8Array
219219
): Uint8Array {
220-
const isOddChunk = length % 2 === 1;
221-
const chunkBytesLen = isOddChunk ? length * 32 + 32 : length * 32;
222-
if (chunkBytesLen > chunkBytesBuffer.length) {
223-
throw new Error(`chunkBytesBuffer is too small: ${chunkBytesBuffer.length} < ${chunkBytesLen}`);
220+
const blockBytesLen = Math.ceil(length / 2) * 64;
221+
if (blockBytesLen > blocksBuffer.length) {
222+
throw new Error(`blocksBuffer is too small: ${blocksBuffer.length} < ${blockBytesLen}`);
224223
}
225-
const chunkBytes = chunkBytesBuffer.subarray(0, chunkBytesLen);
224+
const blocksBytes = blocksBuffer.subarray(0, blockBytesLen);
226225

227226
for (let i = 0; i < length; i++) {
228-
elementType.hashTreeRootInto(value[i], chunkBytes, i * 32);
227+
elementType.hashTreeRootInto(value[i], blocksBytes, i * 32);
229228
}
230229

230+
const isOddChunk = length % 2 === 1;
231231
if (isOddChunk) {
232232
// similar to append zeroHash(0)
233-
chunkBytes.subarray(length * 32, chunkBytesLen).fill(0);
233+
blocksBytes.subarray(length * 32, blockBytesLen).fill(0);
234234
}
235235

236-
return chunkBytes;
236+
return blocksBytes;
237237
}
238238

239239
function readOffsetsArrayComposite(

packages/ssz/src/type/bitArray.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {CompositeType, LENGTH_GINDEX} from "./composite";
44
import {BitArray} from "../value/bitArray";
55
import {BitArrayTreeView} from "../view/bitArray";
66
import {BitArrayTreeViewDU} from "../viewDU/bitArray";
7-
import {getChunkBytes} from "./byteArray";
7+
import {getBlocksBytes} from "./byteArray";
88

99
/* eslint-disable @typescript-eslint/member-ordering */
1010

@@ -40,15 +40,13 @@ export abstract class BitArrayType extends CompositeType<BitArray, BitArrayTreeV
4040

4141
// Merkleization
4242

43-
protected getChunkBytes(value: BitArray): Uint8Array {
44-
// reallocate this.merkleBytes if needed
45-
if (value.uint8Array.length > this.chunkBytesBuffer.length) {
43+
protected getBlocksBytes(value: BitArray): Uint8Array {
44+
// reallocate this.blocksBuffer if needed
45+
if (value.uint8Array.length > this.blocksBuffer.length) {
4646
const chunkCount = Math.ceil(value.bitLen / 8 / 32);
47-
const chunkBytes = chunkCount * 32;
48-
// pad 1 chunk if maxChunkCount is not even
49-
this.chunkBytesBuffer = chunkCount % 2 === 1 ? new Uint8Array(chunkBytes + 32) : new Uint8Array(chunkBytes);
47+
this.blocksBuffer = new Uint8Array(Math.ceil(chunkCount / 2) * 64);
5048
}
51-
return getChunkBytes(value.uint8Array, this.chunkBytesBuffer);
49+
return getBlocksBytes(value.uint8Array, this.blocksBuffer);
5250
}
5351

5452
// Proofs

packages/ssz/src/type/bitList.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {allocUnsafe} from "@chainsafe/as-sha256";
22
import {
33
getNodesAtDepth,
4-
merkleizeInto,
4+
merkleizeBlocksBytes,
55
Node,
66
packedNodeRootsToBytes,
77
packedRootsBytesToNode,
@@ -36,11 +36,11 @@ export class BitListType extends BitArrayType {
3636
readonly maxSize: number;
3737
readonly maxChunkCount: number;
3838
readonly isList = true;
39-
readonly mixInLengthChunkBytes = new Uint8Array(64);
39+
readonly mixInLengthBlockBytes = new Uint8Array(64);
4040
readonly mixInLengthBuffer = Buffer.from(
41-
this.mixInLengthChunkBytes.buffer,
42-
this.mixInLengthChunkBytes.byteOffset,
43-
this.mixInLengthChunkBytes.byteLength
41+
this.mixInLengthBlockBytes.buffer,
42+
this.mixInLengthBlockBytes.byteOffset,
43+
this.mixInLengthBlockBytes.byteLength
4444
);
4545

4646
constructor(readonly limitBits: number, opts?: BitListOptions) {
@@ -120,12 +120,12 @@ export class BitListType extends BitArrayType {
120120
}
121121

122122
hashTreeRootInto(value: BitArray, output: Uint8Array, offset: number): void {
123-
super.hashTreeRootInto(value, this.mixInLengthChunkBytes, 0);
123+
super.hashTreeRootInto(value, this.mixInLengthBlockBytes, 0);
124124
// mixInLength
125125
this.mixInLengthBuffer.writeUIntLE(value.bitLen, 32, 6);
126126
// one for hashTreeRoot(value), one for length
127127
const chunkCount = 2;
128-
merkleizeInto(this.mixInLengthChunkBytes, chunkCount, output, offset);
128+
merkleizeBlocksBytes(this.mixInLengthBlockBytes, chunkCount, output, offset);
129129
}
130130

131131
// Proofs: inherited from BitArrayType

packages/ssz/src/type/byteArray.ts

+13-15
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,13 @@ export abstract class ByteArrayType extends CompositeType<ByteArray, ByteArray,
8989

9090
// Merkleization
9191

92-
protected getChunkBytes(value: ByteArray): Uint8Array {
93-
// reallocate this.merkleBytes if needed
94-
if (value.length > this.chunkBytesBuffer.length) {
92+
protected getBlocksBytes(value: ByteArray): Uint8Array {
93+
// reallocate this.blocksBuffer if needed
94+
if (value.length > this.blocksBuffer.length) {
9595
const chunkCount = Math.ceil(value.length / 32);
96-
const chunkBytes = chunkCount * 32;
97-
// pad 1 chunk if maxChunkCount is not even
98-
this.chunkBytesBuffer = chunkCount % 2 === 1 ? new Uint8Array(chunkBytes + 32) : new Uint8Array(chunkBytes);
96+
this.blocksBuffer = new Uint8Array(Math.ceil(chunkCount / 2) * 64);
9997
}
100-
return getChunkBytes(value, this.chunkBytesBuffer);
98+
return getBlocksBytes(value, this.blocksBuffer);
10199
}
102100

103101
// Proofs
@@ -162,15 +160,15 @@ export abstract class ByteArrayType extends CompositeType<ByteArray, ByteArray,
162160
protected abstract assertValidSize(size: number): void;
163161
}
164162

165-
export function getChunkBytes(data: Uint8Array, merkleBytesBuffer: Uint8Array): Uint8Array {
166-
if (data.length > merkleBytesBuffer.length) {
167-
throw new Error(`data length ${data.length} exceeds merkleBytesBuffer length ${merkleBytesBuffer.length}`);
163+
export function getBlocksBytes(value: Uint8Array, blocksBuffer: Uint8Array): Uint8Array {
164+
if (value.length > blocksBuffer.length) {
165+
throw new Error(`data length ${value.length} exceeds blocksBuffer length ${blocksBuffer.length}`);
168166
}
169167

170-
merkleBytesBuffer.set(data);
171-
const valueLen = data.length;
172-
const chunkByteLen = Math.ceil(valueLen / 64) * 64;
168+
blocksBuffer.set(value);
169+
const valueLen = value.length;
170+
const blockByteLen = Math.ceil(valueLen / 64) * 64;
173171
// all padding bytes must be zero, this is similar to set zeroHash(0)
174-
merkleBytesBuffer.subarray(valueLen, chunkByteLen).fill(0);
175-
return merkleBytesBuffer.subarray(0, chunkByteLen);
172+
blocksBuffer.subarray(valueLen, blockByteLen).fill(0);
173+
return blocksBuffer.subarray(0, blockByteLen);
176174
}

packages/ssz/src/type/byteList.ts

+41-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {
44
Node,
55
packedNodeRootsToBytes,
66
packedRootsBytesToNode,
7-
merkleizeInto,
7+
merkleizeBlocksBytes,
8+
merkleizeBlockArray,
89
} from "@chainsafe/persistent-merkle-tree";
910
import {maxChunksToDepth} from "../util/merkleize";
1011
import {Require} from "../util/types";
@@ -40,11 +41,13 @@ export class ByteListType extends ByteArrayType {
4041
readonly maxSize: number;
4142
readonly maxChunkCount: number;
4243
readonly isList = true;
43-
readonly mixInLengthChunkBytes = new Uint8Array(64);
44+
readonly blockArray: Uint8Array[] = [];
45+
private blockBytesLen = 0;
46+
readonly mixInLengthBlockBytes = new Uint8Array(64);
4447
readonly mixInLengthBuffer = Buffer.from(
45-
this.mixInLengthChunkBytes.buffer,
46-
this.mixInLengthChunkBytes.byteOffset,
47-
this.mixInLengthChunkBytes.byteLength
48+
this.mixInLengthBlockBytes.buffer,
49+
this.mixInLengthBlockBytes.byteOffset,
50+
this.mixInLengthBlockBytes.byteLength
4851
);
4952

5053
constructor(readonly limitBytes: number, opts?: ByteListOptions) {
@@ -106,13 +109,44 @@ export class ByteListType extends ByteArrayType {
106109
return root;
107110
}
108111

112+
/**
113+
* Use merkleizeBlockArray() instead of merkleizeBlocksBytes() to avoid big memory allocation
114+
*/
109115
hashTreeRootInto(value: Uint8Array, output: Uint8Array, offset: number): void {
110-
super.hashTreeRootInto(value, this.mixInLengthChunkBytes, 0);
116+
// should not call super.hashTreeRoot() here
117+
// use merkleizeBlockArray() instead of merkleizeBlocksBytes() to avoid big memory allocation
118+
// reallocate this.blockArray if needed
119+
if (value.length > this.blockBytesLen) {
120+
const newBlockCount = Math.ceil(value.length / 64);
121+
// this.blockBytesLen should be a multiple of 64
122+
const oldBlockCount = Math.ceil(this.blockBytesLen / 64);
123+
const blockDiff = newBlockCount - oldBlockCount;
124+
const newBlocksBytes = new Uint8Array(blockDiff * 64);
125+
for (let i = 0; i < blockDiff; i++) {
126+
this.blockArray.push(newBlocksBytes.subarray(i * 64, (i + 1) * 64));
127+
this.blockBytesLen += 64;
128+
}
129+
}
130+
131+
// populate this.blockArray
132+
for (let i = 0; i < value.length; i += 64) {
133+
const block = this.blockArray[i / 64];
134+
// zero out the last block if it's over value.length
135+
if (i + 64 > value.length) {
136+
block.fill(0);
137+
}
138+
block.set(value.subarray(i, Math.min(i + 64, value.length)));
139+
}
140+
141+
// compute hashTreeRoot
142+
const blockLimit = Math.ceil(value.length / 64);
143+
merkleizeBlockArray(this.blockArray, blockLimit, this.maxChunkCount, this.mixInLengthBlockBytes, 0);
144+
111145
// mixInLength
112146
this.mixInLengthBuffer.writeUIntLE(value.length, 32, 6);
113147
// one for hashTreeRoot(value), one for length
114148
const chunkCount = 2;
115-
merkleizeInto(this.mixInLengthChunkBytes, chunkCount, output, offset);
149+
merkleizeBlocksBytes(this.mixInLengthBlockBytes, chunkCount, output, offset);
116150
}
117151

118152
// Proofs: inherited from BitArrayType

packages/ssz/src/type/composite.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
Proof,
99
ProofType,
1010
Tree,
11-
merkleizeInto,
11+
merkleizeBlocksBytes,
1212
HashComputationLevel,
1313
} from "@chainsafe/persistent-merkle-tree";
1414
import {byteArrayEquals} from "../util/byteArray";
@@ -61,7 +61,7 @@ export abstract class CompositeType<V, TV, TVDU> extends Type<V> {
6161
* Required for ContainerNodeStruct to ensure no dangerous types are constructed.
6262
*/
6363
abstract readonly isViewMutable: boolean;
64-
protected chunkBytesBuffer = new Uint8Array(0);
64+
protected blocksBuffer = new Uint8Array(0);
6565

6666
constructor(
6767
/**
@@ -238,8 +238,8 @@ export abstract class CompositeType<V, TV, TVDU> extends Type<V> {
238238
}
239239
}
240240

241-
const merkleBytes = this.getChunkBytes(value);
242-
merkleizeInto(merkleBytes, this.maxChunkCount, output, offset);
241+
const blocksBuffer = this.getBlocksBytes(value);
242+
merkleizeBlocksBytes(blocksBuffer, this.maxChunkCount, output, offset);
243243
if (this.cachePermanentRootStruct) {
244244
cacheRoot(value as ValueWithCachedPermanentRoot, output, offset, safeCache);
245245
}
@@ -258,10 +258,10 @@ export abstract class CompositeType<V, TV, TVDU> extends Type<V> {
258258
// to hashObject and back.
259259

260260
/**
261-
* Get merkle bytes of each value, the returned Uint8Array should be multiple of 64 bytes.
261+
* Get multiple SHA256 blocks, each is 64 bytes long.
262262
* If chunk count is not even, need to append zeroHash(0)
263263
*/
264-
protected abstract getChunkBytes(value: V): Uint8Array;
264+
protected abstract getBlocksBytes(value: V): Uint8Array;
265265

266266
// Proofs API
267267

packages/ssz/src/type/container.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,7 @@ export class ContainerType<Fields extends Record<string, Type<unknown>>> extends
131131
this.TreeView = opts?.getContainerTreeViewClass?.(this) ?? getContainerTreeViewClass(this);
132132
this.TreeViewDU = opts?.getContainerTreeViewDUClass?.(this) ?? getContainerTreeViewDUClass(this);
133133
const fieldBytes = this.fieldsEntries.length * 32;
134-
const chunkBytes = Math.ceil(fieldBytes / 64) * 64;
135-
this.chunkBytesBuffer = new Uint8Array(chunkBytes);
134+
this.blocksBuffer = new Uint8Array(Math.ceil(fieldBytes / 64) * 64);
136135
}
137136

138137
static named<Fields extends Record<string, Type<unknown>>>(
@@ -275,13 +274,13 @@ export class ContainerType<Fields extends Record<string, Type<unknown>>> extends
275274

276275
// Merkleization
277276

278-
protected getChunkBytes(struct: ValueOfFields<Fields>): Uint8Array {
277+
protected getBlocksBytes(struct: ValueOfFields<Fields>): Uint8Array {
279278
for (let i = 0; i < this.fieldsEntries.length; i++) {
280279
const {fieldName, fieldType} = this.fieldsEntries[i];
281-
fieldType.hashTreeRootInto(struct[fieldName], this.chunkBytesBuffer, i * 32);
280+
fieldType.hashTreeRootInto(struct[fieldName], this.blocksBuffer, i * 32);
282281
}
283282
// remaining bytes are zeroed as we never write them
284-
return this.chunkBytesBuffer;
283+
return this.blocksBuffer;
285284
}
286285

287286
// Proofs

packages/ssz/src/type/listBasic.ts

+16-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {HashComputationLevel, LeafNode, Node, Tree, merkleizeInto} from "@chainsafe/persistent-merkle-tree";
1+
import {HashComputationLevel, LeafNode, Node, Tree, merkleizeBlocksBytes} from "@chainsafe/persistent-merkle-tree";
22
import {ValueOf} from "./abstract";
33
import {BasicType} from "./basic";
44
import {ByteViews} from "./composite";
@@ -47,11 +47,11 @@ export class ListBasicType<ElementType extends BasicType<unknown>>
4747
readonly maxSize: number;
4848
readonly isList = true;
4949
readonly isViewMutable = true;
50-
readonly mixInLengthChunkBytes = new Uint8Array(64);
50+
readonly mixInLengthBlockBytes = new Uint8Array(64);
5151
readonly mixInLengthBuffer = Buffer.from(
52-
this.mixInLengthChunkBytes.buffer,
53-
this.mixInLengthChunkBytes.byteOffset,
54-
this.mixInLengthChunkBytes.byteLength
52+
this.mixInLengthBlockBytes.buffer,
53+
this.mixInLengthBlockBytes.byteOffset,
54+
this.mixInLengthBlockBytes.byteLength
5555
);
5656
protected readonly defaultLen = 0;
5757

@@ -193,34 +193,34 @@ export class ListBasicType<ElementType extends BasicType<unknown>>
193193
}
194194
}
195195

196-
super.hashTreeRootInto(value, this.mixInLengthChunkBytes, 0);
196+
super.hashTreeRootInto(value, this.mixInLengthBlockBytes, 0);
197197
// mixInLength
198198
this.mixInLengthBuffer.writeUIntLE(value.length, 32, 6);
199199
// one for hashTreeRoot(value), one for length
200200
const chunkCount = 2;
201-
merkleizeInto(this.mixInLengthChunkBytes, chunkCount, output, offset);
201+
merkleizeBlocksBytes(this.mixInLengthBlockBytes, chunkCount, output, offset);
202202

203203
if (this.cachePermanentRootStruct) {
204204
cacheRoot(value as ValueWithCachedPermanentRoot, output, offset, safeCache);
205205
}
206206
}
207207

208-
protected getChunkBytes(value: ValueOf<ElementType>[]): Uint8Array {
208+
protected getBlocksBytes(value: ValueOf<ElementType>[]): Uint8Array {
209209
const byteLen = this.value_serializedSize(value);
210-
const chunkByteLen = Math.ceil(byteLen / 64) * 64;
211-
// reallocate this.verkleBytes if needed
212-
if (byteLen > this.chunkBytesBuffer.length) {
210+
const blockByteLen = Math.ceil(byteLen / 64) * 64;
211+
// reallocate this.blocksBuffer if needed
212+
if (byteLen > this.blocksBuffer.length) {
213213
// pad 1 chunk if maxChunkCount is not even
214-
this.chunkBytesBuffer = new Uint8Array(chunkByteLen);
214+
this.blocksBuffer = new Uint8Array(blockByteLen);
215215
}
216-
const chunkBytes = this.chunkBytesBuffer.subarray(0, chunkByteLen);
217-
const uint8Array = chunkBytes.subarray(0, byteLen);
216+
const blockBytes = this.blocksBuffer.subarray(0, blockByteLen);
217+
const uint8Array = blockBytes.subarray(0, byteLen);
218218
const dataView = new DataView(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteLength);
219219
value_serializeToBytesArrayBasic(this.elementType, value.length, {uint8Array, dataView}, 0, value);
220220

221221
// all padding bytes must be zero, this is similar to set zeroHash(0)
222-
this.chunkBytesBuffer.subarray(byteLen, chunkByteLen).fill(0);
223-
return chunkBytes;
222+
this.blocksBuffer.subarray(byteLen, blockByteLen).fill(0);
223+
return blockBytes;
224224
}
225225

226226
// JSON: inherited from ArrayType

0 commit comments

Comments
 (0)