@lodestar/era 1.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +76 -0
- package/lib/e2s.d.ts +68 -0
- package/lib/e2s.d.ts.map +1 -0
- package/lib/e2s.js +129 -0
- package/lib/e2s.js.map +1 -0
- package/lib/era/index.d.ts +4 -0
- package/lib/era/index.d.ts.map +1 -0
- package/lib/era/index.js +4 -0
- package/lib/era/index.js.map +1 -0
- package/lib/era/reader.d.ts +43 -0
- package/lib/era/reader.d.ts.map +1 -0
- package/lib/era/reader.js +160 -0
- package/lib/era/reader.js.map +1 -0
- package/lib/era/util.d.ts +46 -0
- package/lib/era/util.d.ts.map +1 -0
- package/lib/era/util.js +92 -0
- package/lib/era/util.js.map +1 -0
- package/lib/era/writer.d.ts +48 -0
- package/lib/era/writer.d.ts.map +1 -0
- package/lib/era/writer.js +163 -0
- package/lib/era/writer.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -0
- package/lib/util.d.ts +19 -0
- package/lib/util.d.ts.map +1 -0
- package/lib/util.js +49 -0
- package/lib/util.js.map +1 -0
- package/package.json +49 -0
- package/src/e2s.ts +178 -0
- package/src/era/index.ts +3 -0
- package/src/era/reader.ts +196 -0
- package/src/era/util.ts +134 -0
- package/src/era/writer.ts +206 -0
- package/src/index.ts +2 -0
- package/src/util.ts +60 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {type FileHandle, open, rename} from "node:fs/promises";
|
|
2
|
+
import {format, parse} from "node:path";
|
|
3
|
+
import {ChainForkConfig} from "@lodestar/config";
|
|
4
|
+
import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
|
|
5
|
+
import {BeaconState, SignedBeaconBlock, Slot} from "@lodestar/types";
|
|
6
|
+
import {E2STORE_HEADER_SIZE, EntryType, SlotIndex, serializeSlotIndex, writeEntry} from "../e2s.ts";
|
|
7
|
+
import {snappyCompress} from "../util.ts";
|
|
8
|
+
import {
|
|
9
|
+
computeStartBlockSlotFromEraNumber,
|
|
10
|
+
getShortHistoricalRoot,
|
|
11
|
+
isSlotInRange,
|
|
12
|
+
isValidEraStateSlot,
|
|
13
|
+
} from "./util.ts";
|
|
14
|
+
|
|
15
|
+
enum WriterStateType {
|
|
16
|
+
InitGroup,
|
|
17
|
+
WriteGroup,
|
|
18
|
+
FinishedGroup,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type WriterState =
|
|
22
|
+
| {
|
|
23
|
+
type: WriterStateType.InitGroup;
|
|
24
|
+
eraNumber: number;
|
|
25
|
+
currentOffset: number;
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
type: WriterStateType.WriteGroup;
|
|
29
|
+
eraNumber: number;
|
|
30
|
+
currentOffset: number;
|
|
31
|
+
blockOffsets: number[];
|
|
32
|
+
lastSlot: Slot;
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
type: WriterStateType.FinishedGroup;
|
|
36
|
+
eraNumber: number;
|
|
37
|
+
currentOffset: number;
|
|
38
|
+
shortHistoricalRoot: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* EraWriter is responsible for writing ERA files.
|
|
43
|
+
*
|
|
44
|
+
* See https://github.com/eth-clients/e2store-format-specs/blob/main/formats/era.md
|
|
45
|
+
*/
|
|
46
|
+
export class EraWriter {
|
|
47
|
+
config: ChainForkConfig;
|
|
48
|
+
path: string;
|
|
49
|
+
fh: FileHandle;
|
|
50
|
+
eraNumber: number;
|
|
51
|
+
state: WriterState;
|
|
52
|
+
|
|
53
|
+
constructor(config: ChainForkConfig, path: string, fh: FileHandle, eraNumber: number) {
|
|
54
|
+
this.config = config;
|
|
55
|
+
this.path = path;
|
|
56
|
+
this.fh = fh;
|
|
57
|
+
this.eraNumber = eraNumber;
|
|
58
|
+
this.state = {
|
|
59
|
+
type: WriterStateType.InitGroup,
|
|
60
|
+
eraNumber,
|
|
61
|
+
currentOffset: 0,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static async create(config: ChainForkConfig, path: string, eraNumber: number): Promise<EraWriter> {
|
|
66
|
+
const fh = await open(path, "w");
|
|
67
|
+
return new EraWriter(config, path, fh, eraNumber);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async finish(): Promise<string> {
|
|
71
|
+
if (this.state.type !== WriterStateType.FinishedGroup) {
|
|
72
|
+
throw new Error("Writer has not been finished");
|
|
73
|
+
}
|
|
74
|
+
await this.fh.close();
|
|
75
|
+
|
|
76
|
+
const pathParts = parse(this.path);
|
|
77
|
+
const newPath = format({
|
|
78
|
+
...pathParts,
|
|
79
|
+
base: `${this.config.CONFIG_NAME}-${String(this.eraNumber).padStart(5, "0")}-${this.state.shortHistoricalRoot}.era`,
|
|
80
|
+
});
|
|
81
|
+
await rename(this.path, newPath);
|
|
82
|
+
|
|
83
|
+
return newPath;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async writeVersion(): Promise<void> {
|
|
87
|
+
if (this.state.type === WriterStateType.FinishedGroup) {
|
|
88
|
+
this.state = {
|
|
89
|
+
type: WriterStateType.InitGroup,
|
|
90
|
+
eraNumber: this.state.eraNumber + 1,
|
|
91
|
+
currentOffset: this.state.currentOffset,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (this.state.type !== WriterStateType.InitGroup) {
|
|
95
|
+
throw new Error("Writer has already been initialized");
|
|
96
|
+
}
|
|
97
|
+
await writeEntry(this.fh, this.state.currentOffset, EntryType.Version, new Uint8Array(0));
|
|
98
|
+
// Move to writing blocks/state
|
|
99
|
+
this.state = {
|
|
100
|
+
type: WriterStateType.WriteGroup,
|
|
101
|
+
eraNumber: this.state.eraNumber,
|
|
102
|
+
currentOffset: this.state.currentOffset + E2STORE_HEADER_SIZE,
|
|
103
|
+
blockOffsets: [],
|
|
104
|
+
lastSlot: computeStartBlockSlotFromEraNumber(this.state.eraNumber) - 1,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async writeCompressedState(slot: Slot, shortHistoricalRoot: string, data: Uint8Array): Promise<void> {
|
|
109
|
+
if (this.state.type === WriterStateType.InitGroup) {
|
|
110
|
+
await this.writeVersion();
|
|
111
|
+
}
|
|
112
|
+
if (this.state.type !== WriterStateType.WriteGroup) {
|
|
113
|
+
throw new Error("unreachable");
|
|
114
|
+
}
|
|
115
|
+
const expectedSlot = this.state.eraNumber * SLOTS_PER_HISTORICAL_ROOT;
|
|
116
|
+
if (!isValidEraStateSlot(slot, this.state.eraNumber)) {
|
|
117
|
+
throw new Error(`State slot must be ${expectedSlot} for era ${this.eraNumber}, got ${slot}`);
|
|
118
|
+
}
|
|
119
|
+
for (let s = this.state.lastSlot + 1; s < slot; s++) {
|
|
120
|
+
this.state.blockOffsets.push(0); // Empty slot
|
|
121
|
+
}
|
|
122
|
+
const stateOffset = this.state.currentOffset;
|
|
123
|
+
await writeEntry(this.fh, this.state.currentOffset, EntryType.CompressedBeaconState, data);
|
|
124
|
+
this.state.currentOffset += E2STORE_HEADER_SIZE + data.length;
|
|
125
|
+
|
|
126
|
+
if (this.state.eraNumber !== 0) {
|
|
127
|
+
const blocksIndex: SlotIndex = {
|
|
128
|
+
type: EntryType.SlotIndex,
|
|
129
|
+
startSlot: computeStartBlockSlotFromEraNumber(this.state.eraNumber),
|
|
130
|
+
offsets: this.state.blockOffsets.map((o) => o - this.state.currentOffset),
|
|
131
|
+
recordStart: this.state.currentOffset,
|
|
132
|
+
};
|
|
133
|
+
const blocksIndexPayload = serializeSlotIndex(blocksIndex);
|
|
134
|
+
await writeEntry(this.fh, this.state.currentOffset, EntryType.SlotIndex, blocksIndexPayload);
|
|
135
|
+
this.state.currentOffset += E2STORE_HEADER_SIZE + blocksIndexPayload.length;
|
|
136
|
+
}
|
|
137
|
+
const stateIndex: SlotIndex = {
|
|
138
|
+
type: EntryType.SlotIndex,
|
|
139
|
+
startSlot: slot,
|
|
140
|
+
offsets: [stateOffset - this.state.currentOffset],
|
|
141
|
+
recordStart: this.state.currentOffset,
|
|
142
|
+
};
|
|
143
|
+
const stateIndexPayload = serializeSlotIndex(stateIndex);
|
|
144
|
+
await writeEntry(this.fh, this.state.currentOffset, EntryType.SlotIndex, stateIndexPayload);
|
|
145
|
+
this.state.currentOffset += E2STORE_HEADER_SIZE + stateIndexPayload.length;
|
|
146
|
+
|
|
147
|
+
this.state = {
|
|
148
|
+
type: WriterStateType.FinishedGroup,
|
|
149
|
+
eraNumber: this.state.eraNumber,
|
|
150
|
+
currentOffset: this.state.currentOffset,
|
|
151
|
+
shortHistoricalRoot,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async writeSerializedState(slot: Slot, shortHistoricalRoot: string, data: Uint8Array): Promise<void> {
|
|
156
|
+
const compressed = await snappyCompress(data);
|
|
157
|
+
await this.writeCompressedState(slot, shortHistoricalRoot, compressed);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async writeState(state: BeaconState): Promise<void> {
|
|
161
|
+
const slot = state.slot;
|
|
162
|
+
const shortHistoricalRoot = getShortHistoricalRoot(this.config, state);
|
|
163
|
+
const ssz = this.config.getForkTypes(slot).BeaconState.serialize(state);
|
|
164
|
+
|
|
165
|
+
await this.writeSerializedState(slot, shortHistoricalRoot, ssz);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async writeCompressedBlock(slot: Slot, data: Uint8Array): Promise<void> {
|
|
169
|
+
if (this.state.type === WriterStateType.InitGroup) {
|
|
170
|
+
await this.writeVersion();
|
|
171
|
+
}
|
|
172
|
+
if (this.state.type !== WriterStateType.WriteGroup) {
|
|
173
|
+
throw new Error("Cannot write blocks after writing canonical state");
|
|
174
|
+
}
|
|
175
|
+
if (this.eraNumber === 0) {
|
|
176
|
+
throw new Error("Genesis era (era 0) does not contain blocks");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const blockEra = this.state.eraNumber;
|
|
180
|
+
if (!isSlotInRange(slot, blockEra)) {
|
|
181
|
+
throw new Error(`Slot ${slot} is not in valid block range for era ${blockEra}`);
|
|
182
|
+
}
|
|
183
|
+
if (slot <= this.state.lastSlot) {
|
|
184
|
+
throw new Error(`Slots must be written in ascending order. Last slot: ${this.state.lastSlot}, got: ${slot}`);
|
|
185
|
+
}
|
|
186
|
+
for (let s = this.state.lastSlot + 1; s < slot; s++) {
|
|
187
|
+
this.state.blockOffsets.push(0); // Empty slot
|
|
188
|
+
}
|
|
189
|
+
await writeEntry(this.fh, this.state.currentOffset, EntryType.CompressedSignedBeaconBlock, data);
|
|
190
|
+
this.state.blockOffsets.push(this.state.currentOffset);
|
|
191
|
+
this.state.currentOffset += E2STORE_HEADER_SIZE + data.length;
|
|
192
|
+
this.state.lastSlot = slot;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async writeSerializedBlock(slot: Slot, data: Uint8Array): Promise<void> {
|
|
196
|
+
const compressed = await snappyCompress(data);
|
|
197
|
+
await this.writeCompressedBlock(slot, compressed);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async writeBlock(block: SignedBeaconBlock): Promise<void> {
|
|
201
|
+
const slot = block.message.slot;
|
|
202
|
+
const types = this.config.getForkTypes(slot);
|
|
203
|
+
const ssz = types.SignedBeaconBlock.serialize(block);
|
|
204
|
+
await this.writeSerializedBlock(slot, ssz);
|
|
205
|
+
}
|
|
206
|
+
}
|
package/src/index.ts
ADDED
package/src/util.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {Uint8ArrayList} from "uint8arraylist";
|
|
2
|
+
import {SnappyFramesUncompress, encodeSnappy} from "@lodestar/reqresp/utils";
|
|
3
|
+
|
|
4
|
+
/** Read 48-bit signed integer (little-endian) at offset. */
|
|
5
|
+
export function readInt48(bytes: Uint8Array, offset: number): number {
|
|
6
|
+
return Buffer.prototype.readIntLE.call(bytes, offset, 6);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Read 48-bit unsigned integer (little-endian) at offset. */
|
|
10
|
+
export function readUint48(bytes: Uint8Array, offset: number): number {
|
|
11
|
+
return Buffer.prototype.readUintLE.call(bytes, offset, 6);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Read 16-bit unsigned integer (little-endian) at offset. */
|
|
15
|
+
export function readUint16(bytes: Uint8Array, offset: number): number {
|
|
16
|
+
return Buffer.prototype.readUint16LE.call(bytes, offset);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Read 32-bit unsigned integer (little-endian) at offset. */
|
|
20
|
+
export function readUint32(bytes: Uint8Array, offset: number): number {
|
|
21
|
+
return Buffer.prototype.readUint32LE.call(bytes, offset);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Write 48-bit signed integer (little-endian) into target at offset. */
|
|
25
|
+
export function writeInt48(target: Uint8Array, offset: number, v: number): void {
|
|
26
|
+
Buffer.prototype.writeIntLE.call(target, v, offset, 6);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Write 16-bit unsigned integer (little-endian) into target at offset. */
|
|
30
|
+
export function writeUint16(target: Uint8Array, offset: number, v: number): void {
|
|
31
|
+
Buffer.prototype.writeUint16LE.call(target, v, offset);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Write 32-bit unsigned integer (little-endian) into target at offset. */
|
|
35
|
+
export function writeUint32(target: Uint8Array, offset: number, v: number): void {
|
|
36
|
+
Buffer.prototype.writeUint32LE.call(target, v, offset);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Decompress snappy-framed data */
|
|
40
|
+
export function snappyUncompress(compressedData: Uint8Array): Uint8Array {
|
|
41
|
+
const decompressor = new SnappyFramesUncompress();
|
|
42
|
+
|
|
43
|
+
const input = new Uint8ArrayList(compressedData);
|
|
44
|
+
const result = decompressor.uncompress(input);
|
|
45
|
+
|
|
46
|
+
if (result === null) {
|
|
47
|
+
throw new Error("Snappy decompression failed - no data returned");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return result.subarray();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Compress data using snappy framing */
|
|
54
|
+
export async function snappyCompress(data: Uint8Array): Promise<Uint8Array> {
|
|
55
|
+
const buffers: Buffer[] = [];
|
|
56
|
+
for await (const chunk of encodeSnappy(Buffer.from(data.buffer, data.byteOffset, data.byteLength))) {
|
|
57
|
+
buffers.push(chunk);
|
|
58
|
+
}
|
|
59
|
+
return Buffer.concat(buffers);
|
|
60
|
+
}
|